@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
JavaScript
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
};