UNPKG

@stackmemoryai/stackmemory

Version:

Lossless, project-scoped memory for AI coding tools. Durable context across sessions with 56 MCP tools, FTS5 search, conductor orchestrator, loop/watch monitoring, snapshot capture, pre-flight overlap checks, Claude/Codex/OpenCode wrappers, Linear sync, a

215 lines (214 loc) 6.78 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { execSync } from "child_process"; import { createHash } from "crypto"; import { logger } from "../monitoring/logger.js"; class ProjectIsolationManager { static instance; projectCache = /* @__PURE__ */ new Map(); static getInstance() { if (!ProjectIsolationManager.instance) { ProjectIsolationManager.instance = new ProjectIsolationManager(); } return ProjectIsolationManager.instance; } /** * Get stable project identification based on git remote URL */ getProjectIdentification(projectRoot) { const cacheKey = projectRoot; if (this.projectCache.has(cacheKey)) { return this.projectCache.get(cacheKey); } try { const remoteUrl = this.getGitRemoteUrl(projectRoot); const gitInfo = this.parseGitRemote(remoteUrl); const projectId = this.createStableProjectId( gitInfo.organization, gitInfo.repository ); const workspaceFilter = this.createWorkspaceFilter( gitInfo.organization, gitInfo.repository, projectRoot ); const linearConfig = this.getLinearConfiguration( gitInfo.organization, gitInfo.repository, projectRoot ); const identification = { projectId, organization: gitInfo.organization, repository: gitInfo.repository, workspaceFilter, linearTeamId: linearConfig.teamId, linearOrganization: linearConfig.organization, projectPrefix: linearConfig.prefix }; this.projectCache.set(cacheKey, identification); logger.info("Project identification created", { projectId: identification.projectId, workspaceFilter: identification.workspaceFilter, linearTeam: identification.linearTeamId }); return identification; } catch (error) { logger.warn( "Could not determine git remote, using fallback identification", { error } ); const fallback = this.createFallbackIdentification(projectRoot); this.projectCache.set(cacheKey, fallback); return fallback; } } /** * Find git repository root and get remote URL */ getGitRemoteUrl(projectRoot) { try { const gitRoot = execSync("git rev-parse --show-toplevel", { cwd: projectRoot, encoding: "utf8", timeout: 5e3 }).trim(); const result = execSync("git config --get remote.origin.url", { cwd: gitRoot, encoding: "utf8", timeout: 5e3 }); return result.trim(); } catch (error) { throw new Error(`Failed to get git remote URL: ${error}`); } } /** * Get project name from git repository root directory */ getProjectNameFromGitRoot(projectRoot) { try { const gitRoot = execSync("git rev-parse --show-toplevel", { cwd: projectRoot, encoding: "utf8", timeout: 5e3 }).trim(); return gitRoot.split("/").pop() || "unknown"; } catch { return projectRoot.split("/").pop() || "unknown"; } } /** * Parse git remote URL to extract organization and repository */ parseGitRemote(remoteUrl) { let match = remoteUrl.match(/github\.com[/:]([\w-]+)\/([\w-]+)(?:\.git)?/); if (match) { return { organization: match[1], repository: match[2] }; } match = remoteUrl.match(/[:/]([\w-]+)\/([\w-]+)(?:\.git)?$/); if (match) { return { organization: match[1], repository: match[2] }; } throw new Error(`Could not parse git remote URL: ${remoteUrl}`); } /** * Create stable project ID from organization and repository */ createStableProjectId(organization, repository) { const content = `${organization}/${repository}`; const hash = createHash("sha256").update(content).digest("hex"); return `proj-${hash.substring(0, 12)}`; } /** * Create stable workspace filter using git root folder name */ createWorkspaceFilter(organization, repository, projectRoot) { const projectName = this.getProjectNameFromGitRoot(projectRoot); return `${projectName}:${organization}`; } /** * Get Linear configuration based on project */ getLinearConfiguration(organization, repository, projectRoot) { const projectName = projectRoot ? this.getProjectNameFromGitRoot(projectRoot) : repository; const projectConfigs = { "jonathanpeterwu/stackmemory": { teamId: process.env.LINEAR_TEAM_ID || "stackmemory", organization: process.env.LINEAR_ORGANIZATION || "stackmemoryai", prefix: "SM" }, "Lift-Coefficient/*": { teamId: "STA", organization: "lift-cl", prefix: "STA" } }; const exactKey = `${organization}/${repository}`; if (projectConfigs[exactKey]) { return projectConfigs[exactKey]; } const wildcardKey = `${organization}/*`; if (projectConfigs[wildcardKey]) { return projectConfigs[wildcardKey]; } const sanitizedProjectName = projectName.replace(/[^a-zA-Z0-9]/g, ""); return { teamId: sanitizedProjectName.toLowerCase(), organization: `${organization.toLowerCase()}ai`, prefix: sanitizedProjectName.substring(0, 3).toUpperCase() }; } /** * Create fallback identification for non-git projects */ createFallbackIdentification(projectRoot) { const folderName = projectRoot.split("/").pop() || "unknown"; const projectId = this.createStableProjectId("local", folderName); return { projectId, organization: "local", repository: folderName, workspaceFilter: `local:${folderName}`, linearTeamId: folderName.toLowerCase().replace(/[^a-z0-9]/g, ""), linearOrganization: "local", projectPrefix: folderName.substring(0, 3).toUpperCase() }; } /** * Validate that current project isolation is working */ validateProjectIsolation(projectRoot) { try { const identification = this.getProjectIdentification(projectRoot); if (!identification.projectId || !identification.workspaceFilter) { return false; } const secondCall = this.getProjectIdentification(projectRoot); if (identification.workspaceFilter !== secondCall.workspaceFilter) { return false; } return true; } catch (error) { logger.error("Project isolation validation failed", { error }); return false; } } /** * Clear project cache (for testing) */ clearCache() { this.projectCache.clear(); } } export { ProjectIsolationManager };