@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
697 lines (693 loc) • 21.3 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 { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
import { join, basename, dirname } from "path";
import { homedir } from "os";
import Database from "better-sqlite3";
import { logger } from "../monitoring/logger.js";
import {
DatabaseError,
ProjectError,
ErrorCode,
createErrorHandler
} from "../errors/index.js";
import { retry } from "../errors/recovery.js";
class ProjectManager {
static instance;
db;
configPath;
organizations = /* @__PURE__ */ new Map();
projectCache = /* @__PURE__ */ new Map();
currentProject;
constructor() {
this.configPath = join(homedir(), ".stackmemory");
this.ensureDirectoryStructure();
this.initializeDatabase();
this.loadOrganizations();
this.autoDiscoverOrganizations();
}
static getInstance() {
if (!ProjectManager.instance) {
ProjectManager.instance = new ProjectManager();
}
return ProjectManager.instance;
}
/**
* Auto-detect project from current directory
*/
async detectProject(projectPath) {
const path = projectPath || process.cwd();
const errorHandler = createErrorHandler({
operation: "detectProject",
projectPath: path
});
try {
const cached = this.projectCache.get(path);
if (cached && this.isCacheValid(cached)) {
return cached;
}
const project = await this.analyzeProject(path);
if (project.gitRemote) {
project.organization = this.extractOrganization(project.gitRemote);
project.accountType = this.determineAccountType(
project.gitRemote,
project.organization
);
project.isPrivate = this.isPrivateRepo(project.gitRemote);
}
project.primaryLanguage = this.detectPrimaryLanguage(path);
project.framework = this.detectFramework(path);
await retry(() => Promise.resolve(this.saveProject(project)), {
maxAttempts: 3,
initialDelay: 100,
onRetry: (attempt, error) => {
logger.warn(`Retrying project save (attempt ${attempt})`, {
projectId: project.id,
error: error instanceof Error ? error.message : String(error)
});
}
});
this.projectCache.set(path, project);
this.currentProject = project;
logger.info("Project auto-detected", {
id: project.id,
org: project.organization,
type: project.accountType
});
return project;
} catch (error) {
const _wrappedError = errorHandler(error, {
projectPath: path,
operation: "detectProject"
});
throw new ProjectError(
`Failed to detect project at path: ${path}`,
ErrorCode.PROJECT_INVALID_PATH,
{
projectPath: path,
operation: "detectProject"
}
);
}
}
/**
* Analyze project directory
*/
async analyzeProject(projectPath) {
const gitInfo = this.getGitInfo(projectPath);
const projectName = gitInfo.name || basename(projectPath);
return {
id: this.generateProjectId(gitInfo.remote || projectPath),
name: projectName,
path: projectPath,
gitRemote: gitInfo.remote,
organization: void 0,
accountType: "personal",
isPrivate: false,
lastAccessed: /* @__PURE__ */ new Date(),
metadata: {
branch: gitInfo.branch,
lastCommit: gitInfo.lastCommit,
isDirty: gitInfo.isDirty
}
};
}
/**
* Extract Git information
*/
getGitInfo(projectPath) {
const info = {};
const _errorHandler = createErrorHandler({
operation: "getGitInfo",
projectPath
});
try {
info.remote = execSync("git config --get remote.origin.url", {
cwd: projectPath,
encoding: "utf-8",
timeout: 5e3
// 5 second timeout
}).trim();
info.branch = execSync("git branch --show-current", {
cwd: projectPath,
encoding: "utf-8",
timeout: 5e3
}).trim();
info.lastCommit = execSync("git log -1 --format=%H", {
cwd: projectPath,
encoding: "utf-8",
timeout: 5e3
}).trim();
const status = execSync("git status --porcelain", {
cwd: projectPath,
encoding: "utf-8",
timeout: 5e3
});
info.isDirty = status.length > 0;
const match = info.remote.match(/\/([^\/]+?)(\.git)?$/);
info.name = match ? match[1] : basename(projectPath);
} catch (error) {
logger.debug("Git info extraction failed, using directory name", {
projectPath,
error: error instanceof Error ? error.message : String(error)
});
info.name = basename(projectPath);
}
return info;
}
/**
* Extract organization from Git remote
*/
extractOrganization(gitRemote) {
const githubMatch = gitRemote.match(/github\.com[:/]([^/]+)\//);
if (githubMatch) return githubMatch[1];
const gitlabMatch = gitRemote.match(/gitlab\.com[:/]([^/]+)\//);
if (gitlabMatch) return gitlabMatch[1];
const bitbucketMatch = gitRemote.match(/bitbucket\.org[:/]([^/]+)\//);
if (bitbucketMatch) return bitbucketMatch[1];
const customMatch = gitRemote.match(/@([^:]+)[:/]([^/]+)\//);
if (customMatch) return customMatch[2];
return "unknown";
}
/**
* Determine account type based on patterns
*/
determineAccountType(gitRemote, organization) {
for (const [, org] of this.organizations) {
if (org.githubOrgs.includes(organization || "")) {
return org.accountType;
}
for (const domain of org.domains) {
if (gitRemote.includes(domain)) {
return org.accountType;
}
}
}
if (organization) {
if (organization.includes("corp") || organization.includes("company") || organization.includes("team") || organization.includes("work")) {
return "work";
}
if (organization.includes("apache") || organization.includes("mozilla") || organization.includes("foundation") || gitRemote.includes("gitlab.freedesktop")) {
return "opensource";
}
const username = this.getCurrentGitUser();
if (username && organization.toLowerCase() === username.toLowerCase()) {
return "personal";
}
}
if (this.isPrivateRepo(gitRemote)) {
const currentPath = process.cwd();
if (currentPath.includes("/work/") || currentPath.includes("/Work/") || currentPath.includes("/company/") || currentPath.includes("/job/")) {
return "work";
}
}
return "personal";
}
/**
* Check if repository is private
*/
isPrivateRepo(gitRemote) {
if (gitRemote.startsWith("git@")) {
return true;
}
if (gitRemote.includes("@")) {
return true;
}
return false;
}
/**
* Detect primary programming language
*/
detectPrimaryLanguage(projectPath) {
const checks = [
{ file: "package.json", language: "JavaScript/TypeScript" },
{ file: "Cargo.toml", language: "Rust" },
{ file: "go.mod", language: "Go" },
{ file: "pom.xml", language: "Java" },
{ file: "requirements.txt", language: "Python" },
{ file: "Gemfile", language: "Ruby" },
{ file: "composer.json", language: "PHP" },
{ file: "*.csproj", language: "C#" },
{ file: "Podfile", language: "Swift/Objective-C" }
];
for (const check of checks) {
if (check.file.includes("*")) {
try {
const files = execSync(
`find ${projectPath} -maxdepth 2 -name "${check.file}" 2>/dev/null`,
{
encoding: "utf-8"
}
);
if (files.trim()) return check.language;
} catch {
}
} else if (existsSync(join(projectPath, check.file))) {
return check.language;
}
}
return void 0;
}
/**
* Detect framework
*/
detectFramework(projectPath) {
const packageJsonPath = join(projectPath, "package.json");
if (existsSync(packageJsonPath)) {
try {
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
if (deps["next"]) return "Next.js";
if (deps["react"]) return "React";
if (deps["vue"]) return "Vue";
if (deps["@angular/core"]) return "Angular";
if (deps["express"]) return "Express";
if (deps["fastify"]) return "Fastify";
if (deps["@nestjs/core"]) return "NestJS";
} catch {
}
}
if (existsSync(join(projectPath, "Cargo.toml"))) {
const cargo = readFileSync(join(projectPath, "Cargo.toml"), "utf-8");
if (cargo.includes("actix-web")) return "Actix";
if (cargo.includes("rocket")) return "Rocket";
}
return void 0;
}
/**
* Get current Git user
*/
getCurrentGitUser() {
try {
const email = execSync("git config --global user.email", {
encoding: "utf-8"
}).trim();
const username = email.split("@")[0];
return username;
} catch {
return void 0;
}
}
/**
* Generate unique project ID
*/
generateProjectId(identifier) {
const cleaned = identifier.replace(/\.git$/, "").replace(/[^a-zA-Z0-9-]/g, "-").toLowerCase();
return cleaned.substring(cleaned.length - 50);
}
/**
* Initialize database
*/
initializeDatabase() {
const dbPath = join(this.configPath, "projects.db");
const errorHandler = createErrorHandler({
operation: "initializeDatabase",
dbPath
});
try {
this.db = new Database(dbPath);
this.db.exec(`
CREATE TABLE IF NOT EXISTS projects (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
path TEXT NOT NULL UNIQUE,
git_remote TEXT,
organization TEXT,
account_type TEXT,
is_private BOOLEAN,
primary_language TEXT,
framework TEXT,
last_accessed DATETIME,
metadata JSON,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS organizations (
name TEXT PRIMARY KEY,
type TEXT,
account_type TEXT,
config JSON,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS project_contexts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id TEXT NOT NULL,
context_type TEXT,
content TEXT,
metadata JSON,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (project_id) REFERENCES projects(id)
);
CREATE INDEX IF NOT EXISTS idx_projects_org ON projects(organization);
CREATE INDEX IF NOT EXISTS idx_projects_type ON projects(account_type);
CREATE INDEX IF NOT EXISTS idx_contexts_project ON project_contexts(project_id);
`);
} catch (error) {
const _dbError = errorHandler(error, {
dbPath,
operation: "initializeDatabase"
});
throw new DatabaseError(
"Failed to initialize projects database",
ErrorCode.DB_MIGRATION_FAILED,
{
dbPath,
configPath: this.configPath,
operation: "initializeDatabase"
},
error instanceof Error ? error : void 0
);
}
}
/**
* Save project to database
*/
saveProject(project) {
try {
const stmt = this.db.prepare(`
INSERT OR REPLACE INTO projects
(id, name, path, git_remote, organization, account_type, is_private,
primary_language, framework, last_accessed, metadata, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
`);
stmt.run(
project.id,
project.name,
project.path,
project.gitRemote,
project.organization,
project.accountType,
project.isPrivate ? 1 : 0,
project.primaryLanguage,
project.framework,
project.lastAccessed.toISOString(),
JSON.stringify(project.metadata)
);
} catch (error) {
throw new DatabaseError(
`Failed to save project: ${project.name}`,
ErrorCode.DB_QUERY_FAILED,
{
projectId: project.id,
projectName: project.name,
projectPath: project.path,
operation: "saveProject"
},
error instanceof Error ? error : void 0
);
}
}
/**
* Load organizations configuration
*/
loadOrganizations() {
const configFile = join(this.configPath, "organizations.json");
if (existsSync(configFile)) {
try {
const config = JSON.parse(readFileSync(configFile, "utf-8"));
for (const org of config.organizations || []) {
this.organizations.set(org.name, org);
}
} catch (error) {
logger.error(
"Failed to load organizations config",
error instanceof Error ? error : void 0
);
}
}
}
/**
* Auto-discover organizations from existing projects
*/
autoDiscoverOrganizations() {
const errorHandler = createErrorHandler({
operation: "autoDiscoverOrganizations"
});
try {
const stmt = this.db.prepare(`
SELECT DISTINCT organization, account_type, COUNT(*) as project_count
FROM projects
WHERE organization IS NOT NULL
GROUP BY organization, account_type
`);
const orgs = stmt.all();
for (const org of orgs) {
if (!this.organizations.has(org.organization)) {
this.organizations.set(org.organization, {
name: org.organization,
type: org.account_type === "work" ? "company" : "personal",
domains: [],
githubOrgs: [org.organization],
accountType: org.account_type,
autoPatterns: []
});
}
}
} catch (error) {
const _wrappedError = errorHandler(error, {
operation: "autoDiscoverOrganizations"
});
logger.error(
"Failed to auto-discover organizations",
error instanceof Error ? error : new Error(String(error)),
{
operation: "autoDiscoverOrganizations"
}
);
}
}
/**
* Ensure directory structure exists
*/
ensureDirectoryStructure() {
const dirs = [
this.configPath,
join(this.configPath, "accounts"),
join(this.configPath, "accounts", "personal"),
join(this.configPath, "accounts", "work"),
join(this.configPath, "accounts", "opensource"),
join(this.configPath, "accounts", "client"),
join(this.configPath, "contexts"),
join(this.configPath, "patterns")
];
for (const dir of dirs) {
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
}
}
/**
* Check if cache is still valid
*/
isCacheValid(project) {
const cacheAge = Date.now() - project.lastAccessed.getTime();
return cacheAge < 5 * 60 * 1e3;
}
/**
* Get all projects
*/
getAllProjects() {
try {
const stmt = this.db.prepare(`
SELECT * FROM projects
ORDER BY last_accessed DESC
`);
const projects = stmt.all();
return projects.map((p) => ({
...p,
isPrivate: p.is_private === 1,
lastAccessed: new Date(p.last_accessed),
metadata: JSON.parse(p.metadata || "{}")
}));
} catch (error) {
throw new DatabaseError(
"Failed to get all projects",
ErrorCode.DB_QUERY_FAILED,
{
operation: "getAllProjects"
},
error instanceof Error ? error : void 0
);
}
}
/**
* Get projects by organization
*/
getProjectsByOrganization(organization) {
try {
const stmt = this.db.prepare(`
SELECT * FROM projects
WHERE organization = ?
ORDER BY last_accessed DESC
`);
const projects = stmt.all(organization);
return projects.map((p) => ({
...p,
isPrivate: p.is_private === 1,
lastAccessed: new Date(p.last_accessed),
metadata: JSON.parse(p.metadata || "{}")
}));
} catch (error) {
throw new DatabaseError(
`Failed to get projects by organization: ${organization}`,
ErrorCode.DB_QUERY_FAILED,
{
organization,
operation: "getProjectsByOrganization"
},
error instanceof Error ? error : void 0
);
}
}
/**
* Get projects by account type
*/
getProjectsByAccountType(accountType) {
try {
const stmt = this.db.prepare(`
SELECT * FROM projects
WHERE account_type = ?
ORDER BY last_accessed DESC
`);
const projects = stmt.all(accountType);
return projects.map((p) => ({
...p,
isPrivate: p.is_private === 1,
lastAccessed: new Date(p.last_accessed),
metadata: JSON.parse(p.metadata || "{}")
}));
} catch (error) {
throw new DatabaseError(
`Failed to get projects by account type: ${accountType}`,
ErrorCode.DB_QUERY_FAILED,
{
accountType,
operation: "getProjectsByAccountType"
},
error instanceof Error ? error : void 0
);
}
}
/**
* Get current project
*/
getCurrentProject() {
if (!this.currentProject) {
this.detectProject();
}
return this.currentProject;
}
/**
* Save organization config
*/
saveOrganization(org) {
const errorHandler = createErrorHandler({
operation: "saveOrganization",
orgName: org.name
});
try {
this.organizations.set(org.name, org);
const configFile = join(this.configPath, "organizations.json");
const config = {
organizations: Array.from(this.organizations.values())
};
writeFileSync(configFile, JSON.stringify(config, null, 2));
const stmt = this.db.prepare(`
INSERT OR REPLACE INTO organizations (name, type, account_type, config)
VALUES (?, ?, ?, ?)
`);
stmt.run(org.name, org.type, org.accountType, JSON.stringify(org));
} catch (error) {
const _wrappedError = errorHandler(error, {
orgName: org.name,
operation: "saveOrganization"
});
throw new DatabaseError(
`Failed to save organization: ${org.name}`,
ErrorCode.DB_QUERY_FAILED,
{
orgName: org.name,
operation: "saveOrganization"
},
error instanceof Error ? error : void 0
);
}
}
/**
* Auto-categorize all Git repositories in home directory
*/
async scanAndCategorizeAllProjects(basePaths) {
const paths = basePaths || [
join(homedir(), "Dev"),
join(homedir(), "dev"),
join(homedir(), "Projects"),
join(homedir(), "projects"),
join(homedir(), "Work"),
join(homedir(), "work"),
join(homedir(), "Documents/GitHub"),
join(homedir(), "code")
];
logger.info("Scanning for Git repositories...");
for (const basePath of paths) {
if (!existsSync(basePath)) continue;
try {
const gitDirs = execSync(
`find ${basePath} -type d -name .git -maxdepth 4 2>/dev/null`,
{ encoding: "utf-8", timeout: 3e4 }
// 30 second timeout
).trim().split("\n").filter(Boolean);
for (const gitDir of gitDirs) {
const projectPath = dirname(gitDir);
try {
await this.detectProject(projectPath);
logger.info(`Discovered project: ${projectPath}`);
} catch (error) {
logger.warn(`Failed to analyze project: ${projectPath}`, {
projectPath,
error: error instanceof Error ? error.message : String(error),
operation: "scanAndCategorizeAllProjects"
});
}
}
} catch (error) {
logger.warn(
`Failed to scan ${basePath}`,
error instanceof Error ? { error } : void 0
);
}
}
logger.info(`Scan complete. Found ${this.projectCache.size} projects`);
}
/**
* Generate summary report
*/
generateReport() {
const allProjects = this.getAllProjects();
const report = {
total: allProjects.length,
byAccountType: {},
byOrganization: {},
byLanguage: {},
byFramework: {}
};
for (const project of allProjects) {
report.byAccountType[project.accountType] = (report.byAccountType[project.accountType] || 0) + 1;
if (project.organization) {
report.byOrganization[project.organization] = (report.byOrganization[project.organization] || 0) + 1;
}
if (project.primaryLanguage) {
report.byLanguage[project.primaryLanguage] = (report.byLanguage[project.primaryLanguage] || 0) + 1;
}
if (project.framework) {
report.byFramework[project.framework] = (report.byFramework[project.framework] || 0) + 1;
}
}
return JSON.stringify(report, null, 2);
}
}
export {
ProjectManager
};