@stackmemoryai/stackmemory
Version:
Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.
871 lines (870 loc) • 28.5 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 { FrameManager } from "./index.js";
import { SQLiteAdapter } from "../database/sqlite-adapter.js";
import { logger } from "../monitoring/logger.js";
import { ValidationError, DatabaseError, ErrorCode } from "../errors/index.js";
import {
validateInput,
CreateSharedStackSchema,
SwitchStackSchema
} from "./validation.js";
import { PermissionManager } from "./permission-manager.js";
class DualStackManager {
adapter;
individualStack;
sharedStacks = /* @__PURE__ */ new Map();
activeContext;
handoffRequests = /* @__PURE__ */ new Map();
permissionManager;
constructor(adapter, projectId, userId, defaultTeamId) {
this.adapter = adapter;
this.permissionManager = new PermissionManager();
const rawDb = adapter instanceof SQLiteAdapter ? adapter.getRawDatabase() : null;
if (!rawDb) {
throw new DatabaseError(
"DualStackManager requires SQLiteAdapter with connected database",
ErrorCode.DB_CONNECTION_FAILED,
{ adapter: adapter.constructor.name }
);
}
this.individualStack = new FrameManager(rawDb, projectId, userId);
this.activeContext = {
stackId: `individual-${userId}`,
type: "individual",
projectId,
ownerId: userId,
permissions: this.getDefaultIndividualPermissions(),
metadata: {},
createdAt: /* @__PURE__ */ new Date(),
lastActive: /* @__PURE__ */ new Date()
};
this.permissionManager.setStackPermissions(
userId,
`individual-${userId}`,
this.getDefaultIndividualPermissions()
);
this.initializeSchema();
}
async initializeSchema() {
try {
await this.adapter.beginTransaction();
const createStackContextsTable = `
CREATE TABLE IF NOT EXISTS stack_contexts (
stack_id TEXT PRIMARY KEY,
type TEXT NOT NULL CHECK (type IN ('individual', 'shared')),
project_id TEXT NOT NULL,
owner_id TEXT,
team_id TEXT,
permissions TEXT NOT NULL,
metadata TEXT DEFAULT '{}',
created_at INTEGER NOT NULL,
last_active INTEGER NOT NULL,
CONSTRAINT valid_ownership CHECK (
(type = 'individual' AND owner_id IS NOT NULL AND team_id IS NULL) OR
(type = 'shared' AND team_id IS NOT NULL)
)
)
`;
const createHandoffRequestsTable = `
CREATE TABLE IF NOT EXISTS handoff_requests (
request_id TEXT PRIMARY KEY,
source_stack_id TEXT NOT NULL,
target_stack_id TEXT NOT NULL,
frame_ids TEXT NOT NULL,
requester_id TEXT NOT NULL,
target_user_id TEXT,
message TEXT,
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'accepted', 'rejected', 'expired')),
created_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
FOREIGN KEY (source_stack_id) REFERENCES stack_contexts(stack_id),
FOREIGN KEY (target_stack_id) REFERENCES stack_contexts(stack_id)
)
`;
const createStackSyncLogTable = `
CREATE TABLE IF NOT EXISTS stack_sync_log (
sync_id TEXT PRIMARY KEY,
source_stack_id TEXT NOT NULL,
target_stack_id TEXT NOT NULL,
operation TEXT NOT NULL CHECK (operation IN ('handoff', 'merge', 'sync')),
frame_count INTEGER NOT NULL,
conflicts TEXT DEFAULT '[]',
resolution TEXT,
timestamp INTEGER NOT NULL,
FOREIGN KEY (source_stack_id) REFERENCES stack_contexts(stack_id),
FOREIGN KEY (target_stack_id) REFERENCES stack_contexts(stack_id)
)
`;
if (this.adapter.isConnected()) {
await this.adapter.execute?.(createStackContextsTable) || this.executeSchemaQuery(createStackContextsTable);
await this.adapter.execute?.(createHandoffRequestsTable) || this.executeSchemaQuery(createHandoffRequestsTable);
await this.adapter.execute?.(createStackSyncLogTable) || this.executeSchemaQuery(createStackSyncLogTable);
}
await this.adapter.commitTransaction();
logger.info("Dual stack schema initialized successfully");
} catch (error) {
await this.adapter.rollbackTransaction();
logger.error("Failed to initialize dual stack schema", error);
throw new DatabaseError(
"Schema initialization failed",
ErrorCode.DB_SCHEMA_ERROR,
{ adapter: this.adapter.constructor.name },
error instanceof Error ? error : void 0
);
}
}
async executeSchemaQuery(sql) {
logger.debug(
"Using fallback schema creation - implement execute method in adapter"
);
const rawDb = this.adapter instanceof SQLiteAdapter ? this.adapter.getRawDatabase() : null;
if (rawDb) {
try {
rawDb.exec(sql);
logger.debug("Executed schema query successfully");
} catch (error) {
logger.error("Failed to execute schema query", { sql, error });
throw error;
}
} else {
throw new DatabaseError(
"Cannot execute schema query: raw database not available",
ErrorCode.DB_CONNECTION_FAILED,
{ operation: "executeSchemaQuery" }
);
}
}
getDefaultIndividualPermissions() {
return {
canRead: true,
canWrite: true,
canHandoff: true,
canMerge: true,
canAdminister: true
};
}
getSharedStackPermissions(role) {
const basePermissions = {
canRead: true,
canWrite: true,
canHandoff: true,
canMerge: false,
canAdminister: false
};
switch (role) {
case "lead":
return { ...basePermissions, canMerge: true };
case "admin":
return { ...basePermissions, canMerge: true, canAdminister: true };
default:
return basePermissions;
}
}
/**
* Switch between individual and shared stacks
*/
async switchToStack(stackId) {
const input = validateInput(SwitchStackSchema, { stackId });
try {
if (input.stackId.startsWith("individual-")) {
this.activeContext = {
...this.activeContext,
stackId: input.stackId,
type: "individual"
};
return;
}
const stackContext = await this.loadStackContext(input.stackId);
if (!stackContext) {
throw new ValidationError(
`Stack context not found: ${input.stackId}`,
ErrorCode.STACK_CONTEXT_NOT_FOUND
);
}
await this.permissionManager.enforcePermission(
this.permissionManager.createContext(
this.activeContext.ownerId || "unknown",
"read",
"stack",
input.stackId,
stackContext
)
);
this.activeContext = stackContext;
if (!this.sharedStacks.has(input.stackId)) {
const rawDb = this.adapter instanceof SQLiteAdapter ? this.adapter.getRawDatabase() : null;
if (!rawDb) {
throw new DatabaseError(
"Failed to get raw database for shared stack",
ErrorCode.DB_CONNECTION_FAILED,
{ stackId: input.stackId, operation: "switchToStack" }
);
}
const sharedStack = new FrameManager(
rawDb,
stackContext.projectId,
input.stackId
);
this.sharedStacks.set(input.stackId, sharedStack);
}
await this.updateStackActivity(input.stackId);
logger.info(`Switched to stack: ${input.stackId}`, {
type: stackContext.type
});
} catch (error) {
throw new ValidationError(
`Failed to switch to stack: ${input.stackId}`,
ErrorCode.OPERATION_FAILED,
{ stackId: input.stackId },
error instanceof Error ? error : void 0
);
}
}
/**
* Get the current active stack manager
*/
getActiveStack() {
if (this.activeContext.type === "individual") {
return this.individualStack;
}
const sharedStack = this.sharedStacks.get(this.activeContext.stackId);
if (!sharedStack) {
throw new DatabaseError(
`Active shared stack not initialized: ${this.activeContext.stackId}`,
ErrorCode.INVALID_STATE
);
}
return sharedStack;
}
/**
* Create a new shared stack for team collaboration
*/
async createSharedStack(teamId, name, ownerId, permissions) {
const input = validateInput(CreateSharedStackSchema, {
teamId,
name,
ownerId,
permissions
});
await this.permissionManager.enforcePermission(
this.permissionManager.createContext(
input.ownerId,
"administer",
"stack",
`shared-${input.teamId}`,
this.activeContext
)
);
const stackId = `shared-${input.teamId}-${Date.now()}`;
const stackContext = {
stackId,
type: "shared",
projectId: this.activeContext.projectId,
teamId: input.teamId,
permissions: input.permissions || this.getSharedStackPermissions("admin"),
metadata: { name: input.name, ownerId: input.ownerId },
createdAt: /* @__PURE__ */ new Date(),
lastActive: /* @__PURE__ */ new Date()
};
try {
await this.saveStackContext(stackContext);
const rawDb = this.adapter instanceof SQLiteAdapter ? this.adapter.getRawDatabase() : null;
if (!rawDb) {
throw new DatabaseError(
"Failed to get raw database for new shared stack",
ErrorCode.DB_CONNECTION_FAILED,
{ teamId, operation: "createSharedStack" }
);
}
const sharedStack = new FrameManager(
rawDb,
stackContext.projectId,
stackId
);
this.sharedStacks.set(stackId, sharedStack);
const stackPermissions = stackContext.permissions;
this.permissionManager.setStackPermissions(
input.ownerId,
stackId,
stackPermissions
);
logger.info(`Created shared stack: ${stackId}`, { teamId, name });
return stackId;
} catch (error) {
throw new DatabaseError(
`Failed to create shared stack`,
ErrorCode.OPERATION_FAILED,
{ teamId, name },
error instanceof Error ? error : void 0
);
}
}
/**
* Initiate handoff of frames between stacks
*/
async initiateHandoff(targetStackId, frameIds, targetUserId, message) {
await this.permissionManager.enforcePermission(
this.permissionManager.createContext(
this.activeContext.ownerId || "unknown",
"handoff",
"stack",
this.activeContext.stackId,
this.activeContext
)
);
const requestId = `handoff-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const request = {
requestId,
sourceStackId: this.activeContext.stackId,
targetStackId,
frameIds,
requesterId: this.activeContext.ownerId,
targetUserId,
message,
status: "pending",
createdAt: /* @__PURE__ */ new Date(),
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1e3)
// 24 hours
};
try {
await this.saveHandoffRequest(request);
this.handoffRequests.set(requestId, request);
logger.info(`Initiated handoff request: ${requestId}`, {
sourceStack: this.activeContext.stackId,
targetStack: targetStackId,
frameCount: frameIds.length
});
return requestId;
} catch (error) {
throw new DatabaseError(
`Failed to initiate handoff`,
ErrorCode.OPERATION_FAILED,
{ targetStackId, frameIds },
error instanceof Error ? error : void 0
);
}
}
/**
* Accept a handoff request and move frames
*/
async acceptHandoff(requestId) {
logger.debug("acceptHandoff called", { requestId });
const request = await this.loadHandoffRequest(requestId);
logger.debug("loadHandoffRequest returned", {
requestId,
found: !!request
});
if (!request) {
logger.error("Handoff request not found", {
requestId,
availableRequests: Array.from(this.handoffRequests.keys())
});
throw new DatabaseError(
`Handoff request not found: ${requestId}`,
ErrorCode.RESOURCE_NOT_FOUND
);
}
if (request.status !== "pending") {
throw new DatabaseError(
`Handoff request is not pending: ${request.status}`,
ErrorCode.INVALID_STATE
);
}
if (request.expiresAt < /* @__PURE__ */ new Date()) {
throw new DatabaseError(
`Handoff request has expired`,
ErrorCode.OPERATION_EXPIRED
);
}
try {
logger.debug("Starting moveFramesBetweenStacks", { requestId });
const syncResult = await this.moveFramesBetweenStacks(
request.sourceStackId,
request.targetStackId,
request.frameIds
);
logger.debug("moveFramesBetweenStacks completed", {
requestId,
success: syncResult.success
});
logger.debug("Updating request status", { requestId });
request.status = "accepted";
logger.debug("Calling saveHandoffRequest", { requestId });
await this.saveHandoffRequest(request);
logger.debug("saveHandoffRequest completed", { requestId });
logger.info(`Accepted handoff request: ${requestId}`, {
frameCount: request.frameIds.length,
conflicts: syncResult.conflictFrames.length
});
return syncResult;
} catch (error) {
logger.error("acceptHandoff caught error", {
error: error instanceof Error ? error.message : error
});
request.status = "rejected";
await this.saveHandoffRequest(request);
throw new DatabaseError(
`Failed to accept handoff`,
ErrorCode.OPERATION_FAILED,
{ requestId },
error instanceof Error ? error : void 0
);
}
}
/**
* Sync frames between individual and shared stacks
*/
async syncStacks(sourceStackId, targetStackId, options) {
try {
const sourceStack = this.getStackManager(sourceStackId);
const targetStack = this.getStackManager(targetStackId);
const framesToSync = options.frameIds || (await sourceStack.getActiveFrames()).map((f) => f.frame_id);
const result = {
success: true,
conflictFrames: [],
mergedFrames: [],
errors: []
};
for (const frameId of framesToSync) {
try {
const sourceFrame = await sourceStack.getFrame(frameId);
if (!sourceFrame) {
result.errors.push({
frameId,
error: "Source frame not found",
resolution: "skipped"
});
continue;
}
const existingFrame = await targetStack.getFrame(frameId);
if (existingFrame) {
switch (options.conflictResolution) {
case "skip":
result.conflictFrames.push(frameId);
result.errors.push({
frameId,
error: "Frame already exists",
resolution: "skipped"
});
continue;
case "merge":
if (!options.dryRun) {
await this.mergeFrames(
existingFrame,
sourceFrame,
targetStack
);
}
result.mergedFrames.push(frameId);
break;
case "overwrite":
if (!options.dryRun) {
await targetStack.deleteFrame(frameId);
await this.copyFrame(sourceFrame, targetStack);
}
result.mergedFrames.push(frameId);
break;
}
} else {
if (!options.dryRun) {
await this.copyFrame(sourceFrame, targetStack);
}
result.mergedFrames.push(frameId);
}
} catch (error) {
result.errors.push({
frameId,
error: error instanceof Error ? error.message : String(error),
resolution: "skipped"
});
result.success = false;
}
}
logger.info(`Stack sync completed`, {
source: sourceStackId,
target: targetStackId,
merged: result.mergedFrames.length,
conflicts: result.conflictFrames.length,
errors: result.errors.length
});
return result;
} catch (error) {
throw new DatabaseError(
`Stack sync failed`,
ErrorCode.OPERATION_FAILED,
{ sourceStackId, targetStackId },
error instanceof Error ? error : void 0
);
}
}
getStackManager(stackId) {
logger.debug("getStackManager called", {
stackId,
availableStacks: Array.from(this.sharedStacks.keys())
});
if (stackId.startsWith("individual-")) {
logger.debug("Returning individual stack", { stackId });
return this.individualStack;
}
const sharedStack = this.sharedStacks.get(stackId);
if (!sharedStack) {
logger.error("Stack manager not found", {
stackId,
availableSharedStacks: Array.from(this.sharedStacks.keys()),
message: "getStackManager could not find shared stack"
});
throw new DatabaseError(
`Stack manager not found: ${stackId}`,
ErrorCode.RESOURCE_NOT_FOUND
);
}
logger.debug("Returning shared stack", { stackId });
return sharedStack;
}
async moveFramesBetweenStacks(sourceStackId, targetStackId, frameIds) {
const syncResult = await this.syncStacks(sourceStackId, targetStackId, {
frameIds,
conflictResolution: "merge"
});
if (syncResult.success && syncResult.errors.length === 0) {
const sourceStack = this.getStackManager(sourceStackId);
for (const frameId of frameIds) {
try {
sourceStack.deleteFrame(frameId);
logger.debug("Deleted frame from source stack", {
frameId,
sourceStackId
});
} catch (error) {
logger.warn("Failed to delete frame from source stack", {
frameId,
error
});
}
}
logger.debug("Completed frame cleanup from source stack", {
frameIds: frameIds.length
});
}
return syncResult;
}
async copyFrame(frame, targetStack) {
await targetStack.createFrame({
type: frame.type,
name: frame.name,
inputs: frame.inputs
});
const events = await this.individualStack.getFrameEvents(frame.frame_id);
for (const event of events) {
await targetStack.addEvent(frame.frame_id, {
type: event.type,
text: event.text,
metadata: event.metadata
});
}
const anchors = await this.individualStack.getFrameAnchors(frame.frame_id);
for (const anchor of anchors) {
await targetStack.addAnchor(frame.frame_id, {
type: anchor.type,
text: anchor.text,
priority: anchor.priority,
metadata: anchor.metadata
});
}
}
async mergeFrames(existingFrame, sourceFrame, targetStack) {
const sourceEvents = await this.individualStack.getFrameEvents(
sourceFrame.frame_id
);
for (const event of sourceEvents) {
await targetStack.addEvent(existingFrame.frame_id, {
type: event.type,
text: event.text,
metadata: { ...event.metadata, merged: true }
});
}
const sourceAnchors = await this.individualStack.getFrameAnchors(
sourceFrame.frame_id
);
for (const anchor of sourceAnchors) {
await targetStack.addAnchor(existingFrame.frame_id, {
type: anchor.type,
text: anchor.text,
priority: anchor.priority,
metadata: { ...anchor.metadata, merged: true }
});
}
}
async loadStackContext(stackId) {
try {
const rawDb = this.adapter instanceof SQLiteAdapter ? this.adapter.getRawDatabase() : null;
if (!rawDb) {
return null;
}
const query = rawDb.prepare(`
SELECT stack_id, type, project_id, owner_id, team_id, permissions, metadata, created_at, last_active
FROM stack_contexts
WHERE stack_id = ?
`);
const row = query.get(stackId);
if (!row) {
return null;
}
return {
stackId: row.stack_id,
type: row.type,
projectId: row.project_id,
ownerId: row.owner_id,
teamId: row.team_id,
permissions: JSON.parse(row.permissions),
metadata: JSON.parse(row.metadata || "{}"),
createdAt: new Date(row.created_at),
lastActive: new Date(row.last_active)
};
} catch (error) {
logger.error("Failed to load stack context", { stackId, error });
return null;
}
}
async saveStackContext(context) {
try {
const rawDb = this.adapter instanceof SQLiteAdapter ? this.adapter.getRawDatabase() : null;
if (!rawDb) {
throw new DatabaseError(
"SQLite database not available for stack context save",
ErrorCode.DB_CONNECTION_FAILED,
{ stackId: context.stackId, operation: "saveStackContext" }
);
}
const query = rawDb.prepare(`
INSERT OR REPLACE INTO stack_contexts
(stack_id, type, project_id, owner_id, team_id, permissions, metadata, created_at, last_active)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
query.run(
context.stackId,
context.type,
context.projectId,
context.ownerId || null,
context.teamId || null,
JSON.stringify(context.permissions),
JSON.stringify(context.metadata || {}),
context.createdAt.getTime(),
context.lastActive.getTime()
);
logger.debug("Saved stack context", { stackId: context.stackId });
} catch (error) {
logger.error("Failed to save stack context", {
stackId: context.stackId,
error
});
throw error;
}
}
async updateStackActivity(stackId) {
try {
const rawDb = this.adapter instanceof SQLiteAdapter ? this.adapter.getRawDatabase() : null;
if (!rawDb) {
logger.warn("SQLite database not available for activity update");
return;
}
const query = rawDb.prepare(`
UPDATE stack_contexts
SET last_active = ?
WHERE stack_id = ?
`);
query.run(Date.now(), stackId);
logger.debug("Updated stack activity", { stackId });
} catch (error) {
logger.error("Failed to update stack activity", { stackId, error });
}
}
async loadHandoffRequest(requestId) {
const memoryRequest = this.handoffRequests.get(requestId);
if (memoryRequest) {
return memoryRequest;
}
try {
const rawDb = this.adapter instanceof SQLiteAdapter ? this.adapter.getRawDatabase() : null;
if (rawDb) {
const query = rawDb.prepare(`
SELECT * FROM handoff_requests WHERE request_id = ?
`);
const row = query.get(requestId);
if (row) {
const request = {
requestId: row.request_id,
sourceStackId: row.source_stack_id,
targetStackId: row.target_stack_id,
frameIds: JSON.parse(row.frame_ids),
status: row.status,
createdAt: new Date(row.created_at),
expiresAt: new Date(row.expires_at),
targetUserId: row.target_user_id,
message: row.message
};
this.handoffRequests.set(requestId, request);
return request;
}
}
} catch (error) {
logger.error("Failed to load handoff request from database", {
requestId,
error
});
}
return null;
}
async saveHandoffRequest(request) {
try {
const rawDb = this.adapter instanceof SQLiteAdapter ? this.adapter.getRawDatabase() : null;
if (rawDb) {
const query = rawDb.prepare(`
INSERT OR REPLACE INTO handoff_requests
(request_id, source_stack_id, target_stack_id, frame_ids, status, created_at, expires_at, target_user_id, message)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
query.run(
request.requestId,
request.sourceStackId,
request.targetStackId,
JSON.stringify(request.frameIds),
request.status,
request.createdAt.getTime(),
request.expiresAt.getTime(),
request.targetUserId || null,
request.message || null
);
logger.debug("Saved handoff request to database", {
requestId: request.requestId
});
}
this.handoffRequests.set(request.requestId, request);
} catch (error) {
logger.error("Failed to save handoff request", {
requestId: request.requestId,
error
});
this.handoffRequests.set(request.requestId, request);
}
}
/**
* Get list of available stacks for the current user
*/
async getAvailableStacks() {
try {
const stacks = [];
stacks.push(this.activeContext);
const rawDb = this.adapter instanceof SQLiteAdapter ? this.adapter.getRawDatabase() : null;
if (rawDb) {
const query = rawDb.prepare(`
SELECT stack_id, type, project_id, owner_id, team_id, permissions, metadata, created_at, last_active
FROM stack_contexts
WHERE type = 'shared' AND project_id = ?
`);
const rows = query.all(this.activeContext.projectId);
for (const row of rows) {
const context = {
stackId: row.stack_id,
type: row.type,
projectId: row.project_id,
ownerId: row.owner_id,
teamId: row.team_id,
permissions: JSON.parse(row.permissions),
metadata: JSON.parse(row.metadata || "{}"),
createdAt: new Date(row.created_at),
lastActive: new Date(row.last_active)
};
try {
await this.permissionManager.enforcePermission(
this.permissionManager.createContext(
this.activeContext.ownerId || "unknown",
"read",
"stack",
context.stackId,
context
)
);
stacks.push(context);
} catch (permissionError) {
logger.debug("User lacks access to stack", {
stackId: context.stackId,
userId: this.activeContext.ownerId
});
}
}
}
return stacks;
} catch (error) {
logger.error("Failed to get available stacks", error);
return [this.activeContext];
}
}
/**
* Get pending handoff requests for the current user
*/
async getPendingHandoffRequests() {
return Array.from(this.handoffRequests.values()).filter(
(request) => request.status === "pending" && request.expiresAt > /* @__PURE__ */ new Date()
);
}
/**
* Get current stack context
*/
getCurrentContext() {
return { ...this.activeContext };
}
/**
* Get permission manager for external access
*/
getPermissionManager() {
return this.permissionManager;
}
/**
* Add user to shared stack with specific permissions
*/
async addUserToSharedStack(stackId, userId, permissions, requesterId) {
await this.permissionManager.enforcePermission(
this.permissionManager.createContext(
requesterId,
"administer",
"stack",
stackId
)
);
this.permissionManager.setStackPermissions(userId, stackId, permissions);
logger.info(`Added user to shared stack`, {
stackId,
userId,
permissions,
requesterId
});
}
/**
* Remove user from shared stack
*/
async removeUserFromSharedStack(stackId, userId, requesterId) {
await this.permissionManager.enforcePermission(
this.permissionManager.createContext(
requesterId,
"administer",
"stack",
stackId
)
);
const userPermissions = this.permissionManager.getUserPermissions(userId);
userPermissions.delete(stackId);
logger.info(`Removed user from shared stack`, {
stackId,
userId,
requesterId
});
}
}
export {
DualStackManager
};
//# sourceMappingURL=dual-stack-manager.js.map