@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
169 lines (168 loc) • 5.91 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 { Command } from "commander";
import Database from "better-sqlite3";
import { join } from "path";
import { existsSync } from "fs";
import { v4 as uuidv4 } from "uuid";
import { FrameManager } from "../../core/context/index.js";
const VALID_TYPES = [
"FACT",
"DECISION",
"CONSTRAINT",
"INTERFACE_CONTRACT",
"TODO",
"RISK"
];
const MAX_CONTENT_LENGTH = 2e3;
function findProjectRoot(startDir) {
let dir = startDir;
for (let i = 0; i < 20; i++) {
if (existsSync(join(dir, ".stackmemory", "context.db"))) {
return dir;
}
const parent = join(dir, "..");
if (parent === dir) break;
dir = parent;
}
return null;
}
function createTeamCommands() {
const team = new Command("team").description(
"Team shared context (cross-agent memory)"
);
team.command("share").description("Share context with other agents (creates shared anchor)").requiredOption("-c, --content <text>", "Anchor text to share").option(
"-t, --type <type>",
"Anchor type (FACT|DECISION|CONSTRAINT|TODO|RISK)",
"FACT"
).option("-p, --priority <n>", "Priority 1-10", "7").option(
"--source <src>",
"Origin: subagent|teammate-idle|task-complete|manual",
"manual"
).option("--agent-id <id>", "Source agent ID").option("--task-id <id>", "Source task ID").action(async (options) => {
const projectRoot = findProjectRoot(process.cwd());
if (!projectRoot) {
console.error(
'StackMemory not initialized. Run "stackmemory init" first.'
);
process.exitCode = 1;
return;
}
const dbPath = join(projectRoot, ".stackmemory", "context.db");
const db = new Database(dbPath);
db.pragma("journal_mode = WAL");
try {
let projectId = "default";
try {
const row = db.prepare(`SELECT value FROM metadata WHERE key = 'project_id'`).get();
if (row?.value) projectId = row.value;
} catch {
}
const frameManager = new FrameManager(db, projectId, {
skipContextBridge: true
});
let frameId = frameManager.getCurrentFrameId();
if (!frameId) {
frameId = frameManager.createFrame({
type: "tool_scope",
name: "team_share"
});
}
const type = options.type.toUpperCase();
if (!VALID_TYPES.includes(type)) {
console.error(
`Invalid type "${type}". Must be one of: ${VALID_TYPES.join(", ")}`
);
process.exitCode = 1;
return;
}
const content = options.content.slice(0, MAX_CONTENT_LENGTH);
const priority = Math.max(1, Math.min(10, parseInt(options.priority)));
const runId = process.env["CLAUDE_INSTANCE_ID"] || process.env["STACKMEMORY_RUN_ID"] || uuidv4();
const metadata = {
shared: true,
sharedBy: runId,
sharedAt: Date.now(),
source: options.source
};
if (options.agentId) metadata.agentId = options.agentId;
if (options.taskId) metadata.taskId = options.taskId;
frameManager.addAnchor(type, content, priority, metadata);
console.log(
`Shared [${type}] (priority ${priority}): ${content.slice(0, 80)}${content.length > 80 ? "..." : ""}`
);
} catch (error) {
console.error("Failed to share context:", error.message);
process.exitCode = 1;
} finally {
db.close();
}
});
team.command("list").description("List recent shared context from all agents").option("-l, --limit <n>", "Max results", "10").option("--since <epoch>", "Filter by timestamp (ms since epoch)").action(async (options) => {
const projectRoot = findProjectRoot(process.cwd());
if (!projectRoot) {
console.error(
'StackMemory not initialized. Run "stackmemory init" first.'
);
process.exitCode = 1;
return;
}
const dbPath = join(projectRoot, ".stackmemory", "context.db");
const db = new Database(dbPath);
try {
let projectId = "default";
try {
const row = db.prepare(`SELECT value FROM metadata WHERE key = 'project_id'`).get();
if (row?.value) projectId = row.value;
} catch {
}
const limit = parseInt(options.limit) || 10;
const since = options.since ? parseInt(options.since) : void 0;
const params = [projectId];
let whereExtra = "";
if (since) {
whereExtra = " AND a.created_at > ?";
params.push(Math.floor(since / 1e3));
}
params.push(limit);
const rows = db.prepare(
`SELECT a.*, f.name as frame_name, f.run_id
FROM anchors a
JOIN frames f ON a.frame_id = f.frame_id
WHERE f.project_id = ?
AND a.metadata LIKE '%"shared":true%'${whereExtra}
ORDER BY a.priority DESC, a.created_at DESC
LIMIT ?`
).all(...params);
if (rows.length === 0) {
console.log("No shared context found.");
return;
}
console.log(`
Shared Context (${rows.length} anchors):
`);
for (const row of rows) {
const meta = JSON.parse(row.metadata || "{}");
const date = new Date(row.created_at * 1e3).toLocaleString();
const source = meta.source || "unknown";
const runSlice = row.run_id?.slice(0, 8) || "?";
console.log(
` [${row.type}] p${row.priority} | ${row.text.slice(0, 100)}${row.text.length > 100 ? "..." : ""}`
);
console.log(` source: ${source} | run: ${runSlice} | ${date}`);
}
console.log("");
} catch (error) {
console.error("Failed to list context:", error.message);
process.exitCode = 1;
} finally {
db.close();
}
});
return team;
}
export {
createTeamCommands
};