@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.
1,449 lines (1,447 loc) • 85.4 kB
JavaScript
#!/usr/bin/env node
import { fileURLToPath as __fileURLToPath } from 'url';
import { dirname as __pathDirname } from 'path';
const __filename = __fileURLToPath(import.meta.url);
const __dirname = __pathDirname(__filename);
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import Database from "better-sqlite3";
import {
validateInput,
StartFrameSchema,
AddAnchorSchema,
CreateTaskSchema
} from "./schemas.js";
import { readFileSync, existsSync, mkdirSync, writeFileSync } from "fs";
import { compactPlan } from "../../orchestrators/multimodal/utils.js";
import { filterPending } from "./pending-utils.js";
import { join, dirname } from "path";
import { execSync } from "child_process";
import { FrameManager } from "../../core/context/index.js";
import { logger } from "../../core/monitoring/logger.js";
import { isFeatureEnabled } from "../../core/config/feature-flags.js";
import {
TaskPriority,
TaskStatus
} from "../../features/tasks/linear-task-manager.js";
import { BrowserMCPIntegration } from "../../features/browser/browser-mcp.js";
import { TraceDetector } from "../../core/trace/trace-detector.js";
import { LLMContextRetrieval } from "../../core/retrieval/index.js";
import { DiscoveryHandlers } from "./handlers/discovery-handlers.js";
import { DiffMemHandlers } from "./handlers/diffmem-handlers.js";
import { v4 as uuidv4 } from "uuid";
import {
DEFAULT_PLANNER_MODEL,
DEFAULT_IMPLEMENTER,
DEFAULT_MAX_ITERS
} from "../../orchestrators/multimodal/constants.js";
function getEnv(key, defaultValue) {
const value = process.env[key];
if (value === void 0) {
if (defaultValue !== void 0) return defaultValue;
throw new Error(`Environment variable ${key} is required`);
}
return value;
}
function getOptionalEnv(key) {
return process.env[key];
}
class LocalStackMemoryMCP {
server;
db;
projectRoot;
frameManager;
taskStore = null;
linearAuthManager = null;
linearSync = null;
projectId;
contexts = /* @__PURE__ */ new Map();
browserMCP;
traceDetector;
contextRetrieval;
discoveryHandlers;
diffMemHandlers;
pendingPlans = /* @__PURE__ */ new Map();
constructor() {
this.projectRoot = this.findProjectRoot();
this.projectId = this.getProjectId();
const dbDir = join(this.projectRoot, ".stackmemory");
if (!existsSync(dbDir)) {
mkdirSync(dbDir, { recursive: true });
}
const dbPath = join(dbDir, "context.db");
this.db = new Database(dbPath);
this.initDB();
this.frameManager = new FrameManager(this.db, this.projectId);
this.initLinearIfEnabled();
this.server = new Server(
{
name: "stackmemory-local",
version: "0.1.0"
},
{
capabilities: {
tools: {}
}
}
);
this.browserMCP = new BrowserMCPIntegration({
headless: process.env["BROWSER_HEADLESS"] !== "false",
defaultViewport: { width: 1280, height: 720 }
});
this.traceDetector = new TraceDetector({}, void 0, this.db);
this.contextRetrieval = new LLMContextRetrieval(
this.db,
this.frameManager,
this.projectId
);
this.discoveryHandlers = new DiscoveryHandlers({
frameManager: this.frameManager,
contextRetrieval: this.contextRetrieval,
db: this.db,
projectRoot: this.projectRoot
});
this.diffMemHandlers = new DiffMemHandlers();
this.setupHandlers();
this.loadInitialContext();
this.loadPendingPlans();
this.browserMCP.initialize(this.server).catch((error) => {
logger.error("Failed to initialize Browser MCP", error);
});
logger.info("StackMemory MCP Server initialized", {
projectRoot: this.projectRoot,
projectId: this.projectId
});
}
findProjectRoot() {
let dir = process.cwd();
while (dir !== "/") {
if (existsSync(join(dir, ".git"))) {
return dir;
}
dir = dirname(dir);
}
return process.cwd();
}
/**
* Initialize Linear integration if enabled and credentials available
*/
async initLinearIfEnabled() {
if (!isFeatureEnabled("linear")) {
logger.info("Linear integration disabled (no API key or LOCAL mode)");
return;
}
try {
const { LinearTaskManager } = await import("../../features/tasks/linear-task-manager.js");
const { LinearAuthManager } = await import("../linear/auth.js");
const { LinearSyncEngine, DEFAULT_SYNC_CONFIG: DEFAULT_SYNC_CONFIG2 } = await import("../linear/sync.js");
this.taskStore = new LinearTaskManager(this.projectRoot, this.db);
this.linearAuthManager = new LinearAuthManager(this.projectRoot);
this.linearSync = new LinearSyncEngine(
this.taskStore,
this.linearAuthManager,
DEFAULT_SYNC_CONFIG2
);
logger.info("Linear integration initialized");
} catch (error) {
logger.warn("Failed to initialize Linear integration", { error });
}
}
initDB() {
this.db.exec(`
CREATE TABLE IF NOT EXISTS contexts (
id TEXT PRIMARY KEY,
type TEXT NOT NULL,
content TEXT NOT NULL,
importance REAL DEFAULT 0.5,
created_at INTEGER DEFAULT (unixepoch()),
last_accessed INTEGER DEFAULT (unixepoch()),
access_count INTEGER DEFAULT 1
);
CREATE TABLE IF NOT EXISTS attention_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
context_id TEXT,
query TEXT,
response TEXT,
influence_score REAL,
timestamp INTEGER DEFAULT (unixepoch())
);
`);
}
loadInitialContext() {
const projectInfo = this.getProjectInfo();
this.addContext(
"project",
`Project: ${projectInfo.name}
Path: ${projectInfo.path}`,
0.9
);
try {
const recentCommits = execSync("git log --oneline -10", {
cwd: this.projectRoot
}).toString();
this.addContext("git_history", `Recent commits:
${recentCommits}`, 0.6);
} catch {
}
const readmePath = join(this.projectRoot, "README.md");
if (existsSync(readmePath)) {
const readme = readFileSync(readmePath, "utf-8");
const summary = readme.substring(0, 500);
this.addContext("readme", `Project README:
${summary}...`, 0.8);
}
this.loadStoredContexts();
}
getProjectId() {
let identifier;
try {
identifier = execSync("git config --get remote.origin.url", {
cwd: this.projectRoot,
stdio: "pipe",
timeout: 5e3
}).toString().trim();
} catch {
identifier = this.projectRoot;
}
const cleaned = identifier.replace(/\.git$/, "").replace(/[^a-zA-Z0-9-]/g, "-").toLowerCase();
return cleaned.substring(cleaned.length - 50) || "unknown";
}
getProjectInfo() {
const packageJsonPath = join(this.projectRoot, "package.json");
if (existsSync(packageJsonPath)) {
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
return {
name: pkg.name || "unknown",
path: this.projectRoot
};
}
return {
name: this.projectRoot.split("/").pop() || "unknown",
path: this.projectRoot
};
}
addContext(type, content, importance = 0.5) {
const id = `${type}_${Date.now()}`;
this.db.prepare(
`
INSERT OR REPLACE INTO contexts (id, type, content, importance)
VALUES (?, ?, ?, ?)
`
).run(id, type, content, importance);
this.contexts.set(id, { type, content, importance });
return id;
}
loadStoredContexts() {
const stored = this.db.prepare(
`
SELECT * FROM contexts
ORDER BY importance DESC, last_accessed DESC
LIMIT 50
`
).all();
stored.forEach((ctx) => {
this.contexts.set(ctx.id, ctx);
});
}
setupHandlers() {
this.server.setRequestHandler(
z.object({
method: z.literal("tools/list")
}),
async () => {
return {
tools: [
{
name: "get_context",
description: "Get current project context",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "What you want to know"
},
limit: {
type: "number",
description: "Max contexts to return"
}
}
}
},
{
name: "plan_and_code",
description: "Generate a plan (Claude), attempt implementation (Codex/Claude), and return JSON result. Quiet by default.",
inputSchema: {
type: "object",
properties: {
task: { type: "string", description: "Task description" },
implementer: {
type: "string",
enum: ["codex", "claude"],
default: "codex",
description: "Which agent implements code"
},
maxIters: {
type: "number",
default: 2,
description: "Retry loop iterations"
},
execute: {
type: "boolean",
default: false,
description: "Actually call implementer (otherwise dry-run)"
},
record: {
type: "boolean",
default: false,
description: "Record plan & critique into StackMemory context"
},
recordFrame: {
type: "boolean",
default: false,
description: "Record as real frame with anchors"
}
},
required: ["task"]
}
},
{
name: "plan_gate",
description: "Phase 1: Generate a plan and return an approvalId for later execution",
inputSchema: {
type: "object",
properties: {
task: { type: "string", description: "Task description" },
plannerModel: {
type: "string",
description: "Claude model (optional)"
}
},
required: ["task"]
}
},
{
name: "approve_plan",
description: "Phase 2: Execute a previously generated plan by approvalId (runs implement + critique)",
inputSchema: {
type: "object",
properties: {
approvalId: {
type: "string",
description: "Id from plan_gate"
},
implementer: {
type: "string",
enum: ["codex", "claude"],
default: "codex",
description: "Which agent implements code"
},
maxIters: { type: "number", default: 2 },
recordFrame: { type: "boolean", default: true },
execute: { type: "boolean", default: true }
},
required: ["approvalId"]
}
},
{
name: "pending_list",
description: "List pending approval-gated plans (supports filters)",
inputSchema: {
type: "object",
properties: {
taskContains: {
type: "string",
description: "Filter tasks containing this substring"
},
olderThanMs: {
type: "number",
description: "Only items older than this age (ms)"
},
newerThanMs: {
type: "number",
description: "Only items newer than this age (ms)"
},
sort: {
type: "string",
enum: ["asc", "desc"],
description: "Sort by createdAt"
},
limit: { type: "number", description: "Max items to return" }
}
}
},
{
name: "pending_clear",
description: "Clear pending approval-gated plans (by id, all, or olderThanMs)",
inputSchema: {
type: "object",
properties: {
approvalId: {
type: "string",
description: "Clear a single approval by id"
},
all: {
type: "boolean",
description: "Clear all pending approvals",
default: false
},
olderThanMs: {
type: "number",
description: "Clear approvals older than this age (ms)"
}
}
}
},
{
name: "pending_show",
description: "Show a pending plan by approvalId",
inputSchema: {
type: "object",
properties: {
approvalId: {
type: "string",
description: "Approval id from plan_gate"
}
},
required: ["approvalId"]
}
},
{
name: "plan_only",
description: "Generate an implementation plan (Claude) and return JSON only",
inputSchema: {
type: "object",
properties: {
task: { type: "string", description: "Task description" },
plannerModel: {
type: "string",
description: "Claude model for planning (optional)"
}
},
required: ["task"]
}
},
{
name: "call_codex",
description: "Invoke Codex via codex-sm with a prompt and args; dry-run by default",
inputSchema: {
type: "object",
properties: {
prompt: { type: "string", description: "Prompt for Codex" },
args: {
type: "array",
items: { type: "string" },
description: "Additional CLI args for codex-sm"
},
execute: {
type: "boolean",
default: false,
description: "Actually run codex-sm (otherwise dry-run)"
}
},
required: ["prompt"]
}
},
{
name: "call_claude",
description: "Invoke Claude with a prompt (Anthropic SDK)",
inputSchema: {
type: "object",
properties: {
prompt: { type: "string", description: "Prompt for Claude" },
model: {
type: "string",
description: "Claude model (optional)"
},
system: {
type: "string",
description: "System prompt (optional)"
}
},
required: ["prompt"]
}
},
{
name: "add_decision",
description: "Record a decision or important information",
inputSchema: {
type: "object",
properties: {
content: {
type: "string",
description: "The decision or information"
},
type: {
type: "string",
enum: ["decision", "constraint", "learning"]
}
},
required: ["content", "type"]
}
},
{
name: "start_frame",
description: "Start a new frame (task/subtask) on the call stack",
inputSchema: {
type: "object",
properties: {
name: { type: "string", description: "Frame name/goal" },
type: {
type: "string",
enum: [
"task",
"subtask",
"tool_scope",
"review",
"write",
"debug"
],
description: "Frame type"
},
constraints: {
type: "array",
items: { type: "string" },
description: "Constraints for this frame"
}
},
required: ["name", "type"]
}
},
{
name: "close_frame",
description: "Close current frame and generate digest",
inputSchema: {
type: "object",
properties: {
result: {
type: "string",
description: "Frame completion result"
},
outputs: {
type: "object",
description: "Final outputs from frame"
}
}
}
},
{
name: "add_anchor",
description: "Add anchored fact/decision/constraint to current frame",
inputSchema: {
type: "object",
properties: {
type: {
type: "string",
enum: [
"FACT",
"DECISION",
"CONSTRAINT",
"INTERFACE_CONTRACT",
"TODO",
"RISK"
],
description: "Anchor type"
},
text: { type: "string", description: "Anchor content" },
priority: {
type: "number",
description: "Priority (0-10)",
minimum: 0,
maximum: 10
}
},
required: ["type", "text"]
}
},
{
name: "get_hot_stack",
description: "Get current active frames and context",
inputSchema: {
type: "object",
properties: {
maxEvents: {
type: "number",
description: "Max recent events per frame",
default: 20
}
}
}
},
{
name: "create_task",
description: "Create a new task in git-tracked JSONL storage",
inputSchema: {
type: "object",
properties: {
title: { type: "string", description: "Task title" },
description: {
type: "string",
description: "Task description"
},
priority: {
type: "string",
enum: ["low", "medium", "high", "urgent"],
description: "Task priority"
},
estimatedEffort: {
type: "number",
description: "Estimated effort in minutes"
},
dependsOn: {
type: "array",
items: { type: "string" },
description: "Task IDs this depends on"
},
tags: {
type: "array",
items: { type: "string" },
description: "Tags for categorization"
}
},
required: ["title"]
}
},
{
name: "update_task_status",
description: "Update task status with automatic time tracking",
inputSchema: {
type: "object",
properties: {
taskId: { type: "string", description: "Task ID to update" },
status: {
type: "string",
enum: [
"pending",
"in_progress",
"completed",
"blocked",
"cancelled"
],
description: "New status"
},
reason: {
type: "string",
description: "Reason for status change (especially for blocked)"
}
},
required: ["taskId", "status"]
}
},
{
name: "get_active_tasks",
description: "Get currently active tasks synced from Linear",
inputSchema: {
type: "object",
properties: {
frameId: {
type: "string",
description: "Filter by specific frame ID"
},
status: {
type: "string",
enum: [
"pending",
"in_progress",
"completed",
"blocked",
"cancelled"
],
description: "Filter by status"
},
priority: {
type: "string",
enum: ["low", "medium", "high", "urgent"],
description: "Filter by priority"
},
search: {
type: "string",
description: "Search in task title or description"
},
limit: {
type: "number",
description: "Max number of tasks to return (default: 20)"
}
}
}
},
{
name: "get_task_metrics",
description: "Get project task metrics and analytics",
inputSchema: {
type: "object",
properties: {}
}
},
{
name: "add_task_dependency",
description: "Add dependency relationship between tasks",
inputSchema: {
type: "object",
properties: {
taskId: {
type: "string",
description: "Task that depends on another"
},
dependsOnId: {
type: "string",
description: "Task ID that this depends on"
}
},
required: ["taskId", "dependsOnId"]
}
},
{
name: "linear_sync",
description: "Sync tasks with Linear",
inputSchema: {
type: "object",
properties: {
direction: {
type: "string",
enum: ["bidirectional", "to_linear", "from_linear"],
description: "Sync direction"
}
}
}
},
{
name: "linear_update_task",
description: "Update a Linear task status",
inputSchema: {
type: "object",
properties: {
issueId: {
type: "string",
description: "Linear issue ID or identifier (e.g., STA-34)"
},
status: {
type: "string",
enum: ["todo", "in-progress", "done", "canceled"],
description: "New status for the task"
},
title: {
type: "string",
description: "Update task title (optional)"
},
description: {
type: "string",
description: "Update task description (optional)"
},
priority: {
type: "number",
enum: [1, 2, 3, 4],
description: "Priority (1=urgent, 2=high, 3=medium, 4=low)"
}
},
required: ["issueId"]
}
},
{
name: "linear_get_tasks",
description: "Get Linear tasks",
inputSchema: {
type: "object",
properties: {
status: {
type: "string",
enum: ["todo", "in-progress", "done", "all"],
description: "Filter by status"
},
limit: {
type: "number",
description: "Maximum number of tasks to return"
}
}
}
},
{
name: "linear_status",
description: "Get Linear integration status",
inputSchema: {
type: "object",
properties: {}
}
},
{
name: "get_traces",
description: "Get detected traces (bundled tool call sequences)",
inputSchema: {
type: "object",
properties: {
type: {
type: "string",
enum: [
"search_driven",
"error_recovery",
"feature_implementation",
"refactoring",
"testing",
"exploration",
"debugging",
"documentation",
"build_deploy",
"unknown"
],
description: "Filter by trace type"
},
minScore: {
type: "number",
description: "Minimum importance score (0-1)"
},
limit: {
type: "number",
description: "Maximum number of traces to return"
}
}
}
},
{
name: "get_trace_statistics",
description: "Get statistics about detected traces",
inputSchema: {
type: "object",
properties: {}
}
},
{
name: "flush_traces",
description: "Flush any pending trace and finalize detection",
inputSchema: {
type: "object",
properties: {}
}
},
{
name: "compress_old_traces",
description: "Compress traces older than specified hours",
inputSchema: {
type: "object",
properties: {
ageHours: {
type: "number",
description: "Age threshold in hours (default: 24)"
}
}
}
},
{
name: "smart_context",
description: "LLM-driven context retrieval - intelligently selects relevant frames based on query",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Natural language query describing what context you need"
},
tokenBudget: {
type: "number",
description: "Maximum tokens to use for context (default: 4000)"
},
forceRefresh: {
type: "boolean",
description: "Force refresh of cached summaries"
}
},
required: ["query"]
}
},
{
name: "get_summary",
description: "Get compressed summary of project memory for analysis",
inputSchema: {
type: "object",
properties: {
forceRefresh: {
type: "boolean",
description: "Force refresh of cached summary"
}
}
}
},
// Discovery tools
{
name: "sm_discover",
description: "Discover relevant files based on current context. Extracts keywords from active frames and searches codebase.",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Optional query to focus the discovery"
},
depth: {
type: "string",
enum: ["shallow", "medium", "deep"],
description: "Search depth"
},
maxFiles: {
type: "number",
description: "Maximum files to return"
}
}
}
},
{
name: "sm_related_files",
description: "Find files related to a specific file or concept",
inputSchema: {
type: "object",
properties: {
file: {
type: "string",
description: "File path to find related files for"
},
concept: {
type: "string",
description: "Concept to search for"
},
maxFiles: {
type: "number",
description: "Maximum files to return"
}
}
}
},
{
name: "sm_session_summary",
description: "Get summary of current session with active tasks, files, and decisions",
inputSchema: {
type: "object",
properties: {
includeFiles: {
type: "boolean",
description: "Include recently accessed files"
},
includeDecisions: {
type: "boolean",
description: "Include recent decisions"
}
}
}
},
{
name: "sm_search",
description: "Search across StackMemory - frames, events, decisions, tasks",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query"
},
scope: {
type: "string",
enum: ["all", "frames", "events", "decisions", "tasks"],
description: "Scope of search"
},
limit: {
type: "number",
description: "Maximum results"
}
},
required: ["query"]
}
},
// DiffMem tools for user memory management
{
name: "diffmem_get_user_context",
description: "Fetch user knowledge and preferences from memory. Use to personalize responses based on learned user patterns.",
inputSchema: {
type: "object",
properties: {
categories: {
type: "array",
items: {
type: "string",
enum: [
"preference",
"expertise",
"project_knowledge",
"pattern",
"correction"
]
},
description: "Filter by memory categories"
},
limit: {
type: "number",
default: 10,
description: "Maximum memories to return"
}
}
}
},
{
name: "diffmem_store_learning",
description: "Store a new insight about the user (preference, expertise, pattern, or correction)",
inputSchema: {
type: "object",
properties: {
content: {
type: "string",
description: "The insight to store"
},
category: {
type: "string",
enum: [
"preference",
"expertise",
"project_knowledge",
"pattern",
"correction"
],
description: "Category of the insight"
},
confidence: {
type: "number",
minimum: 0,
maximum: 1,
default: 0.7,
description: "Confidence level (0-1)"
},
context: {
type: "object",
description: "Additional context for the insight"
}
},
required: ["content", "category"]
}
},
{
name: "diffmem_search",
description: "Semantic search across user memories. Find relevant past insights and preferences.",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query"
},
timeRange: {
type: "string",
enum: ["day", "week", "month", "all"],
default: "all",
description: "Time range filter"
},
minConfidence: {
type: "number",
minimum: 0,
maximum: 1,
default: 0.5,
description: "Minimum confidence threshold"
},
limit: {
type: "number",
default: 10,
description: "Maximum results"
}
},
required: ["query"]
}
},
{
name: "diffmem_status",
description: "Check DiffMem connection status and memory statistics",
inputSchema: {
type: "object",
properties: {}
}
}
]
};
}
);
this.server.setRequestHandler(
z.object({
method: z.literal("tools/call"),
params: z.object({
name: z.string(),
arguments: z.record(z.unknown())
})
}),
async (request) => {
const { name, arguments: args } = request.params;
const callId = uuidv4();
const startTime = Date.now();
const currentFrameId = this.frameManager.getCurrentFrameId();
if (currentFrameId) {
this.frameManager.addEvent("tool_call", {
tool_name: name,
arguments: args,
timestamp: startTime
});
}
const toolCall = {
id: callId,
tool: name,
arguments: args,
timestamp: startTime
};
let result;
let error;
try {
switch (name) {
case "get_context":
result = await this.handleGetContext(args);
break;
case "add_decision":
result = await this.handleAddDecision(args);
break;
case "start_frame":
result = await this.handleStartFrame(args);
break;
case "close_frame":
result = await this.handleCloseFrame(args);
break;
case "add_anchor":
result = await this.handleAddAnchor(args);
break;
case "get_hot_stack":
result = await this.handleGetHotStack(args);
break;
case "create_task":
result = await this.handleCreateTask(args);
break;
case "update_task_status":
result = await this.handleUpdateTaskStatus(args);
break;
case "get_active_tasks":
result = await this.handleGetActiveTasks(args);
break;
case "get_task_metrics":
result = await this.handleGetTaskMetrics(args);
break;
case "add_task_dependency":
result = await this.handleAddTaskDependency(args);
break;
case "linear_sync":
result = await this.handleLinearSync(args);
break;
case "linear_update_task":
result = await this.handleLinearUpdateTask(args);
break;
case "linear_get_tasks":
result = await this.handleLinearGetTasks(args);
break;
case "linear_status":
result = await this.handleLinearStatus(args);
break;
case "get_traces":
result = await this.handleGetTraces(args);
break;
case "get_trace_statistics":
result = await this.handleGetTraceStatistics(args);
break;
case "flush_traces":
result = await this.handleFlushTraces(args);
break;
case "compress_old_traces":
result = await this.handleCompressOldTraces(args);
break;
case "plan_only":
result = await this.handlePlanOnly(args);
break;
case "call_codex":
result = await this.handleCallCodex(args);
break;
case "call_claude":
result = await this.handleCallClaude(args);
break;
case "plan_gate":
result = await this.handlePlanGate(args);
break;
case "approve_plan":
result = await this.handleApprovePlan(args);
break;
case "pending_list":
result = await this.handlePendingList();
break;
case "pending_clear":
result = await this.handlePendingClear(args);
break;
case "pending_show":
result = await this.handlePendingShow(args);
break;
case "smart_context":
result = await this.handleSmartContext(args);
break;
case "get_summary":
result = await this.handleGetSummary(args);
break;
// Discovery tools
case "sm_discover":
result = await this.handleSmDiscover(args);
break;
case "sm_related_files":
result = await this.handleSmRelatedFiles(args);
break;
case "sm_session_summary":
result = await this.handleSmSessionSummary(args);
break;
case "sm_search":
result = await this.handleSmSearch(args);
break;
// DiffMem handlers
case "diffmem_get_user_context":
result = await this.diffMemHandlers.handleGetUserContext(args);
break;
case "diffmem_store_learning":
result = await this.diffMemHandlers.handleStoreLearning(args);
break;
case "diffmem_search":
result = await this.diffMemHandlers.handleSearch(args);
break;
case "diffmem_status":
result = await this.diffMemHandlers.handleStatus();
break;
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (err) {
error = err instanceof Error ? err : new Error(String(err));
toolCall.error = error.message;
throw err;
} finally {
const endTime = Date.now();
if (currentFrameId && name !== "close_frame") {
try {
this.frameManager.addEvent("tool_result", {
tool_name: name,
success: !error,
result: error ? { error: error.message } : result,
timestamp: endTime
});
} catch {
}
}
toolCall.result = error ? void 0 : result;
toolCall.duration = endTime - startTime;
if (args.file_path || args.path) {
toolCall.filesAffected = [args.file_path || args.path].filter(
Boolean
);
} else if (result?.files) {
const files = result.files;
toolCall.filesAffected = Array.isArray(files) ? files : [files];
}
this.traceDetector.addToolCall(toolCall);
}
return result;
}
);
}
// Handle plan_and_code tool by invoking the mm harness
async handlePlanAndCode(args) {
const { runSpike } = await import("../../orchestrators/multimodal/harness.js");
const envPlanner = process.env["STACKMEMORY_MM_PLANNER_MODEL"];
const plannerModel = envPlanner || DEFAULT_PLANNER_MODEL;
const reviewerModel = process.env["STACKMEMORY_MM_REVIEWER_MODEL"] || plannerModel;
const implementer = args.implementer || process.env["STACKMEMORY_MM_IMPLEMENTER"] || DEFAULT_IMPLEMENTER;
const maxIters = Number(
args.maxIters ?? process.env["STACKMEMORY_MM_MAX_ITERS"] ?? DEFAULT_MAX_ITERS
);
const execute = Boolean(args.execute);
const record = Boolean(args.record);
const recordFrame = Boolean(args.recordFrame);
const compact = Boolean(args.compact);
const task = String(args.task || "Plan and implement change");
const result = await runSpike(
{
task,
repoPath: this.projectRoot
},
{
plannerModel,
reviewerModel,
implementer: implementer === "claude" ? "claude" : "codex",
maxIters: isFinite(maxIters) ? Math.max(1, maxIters) : 2,
dryRun: !execute,
auditDir: void 0,
recordFrame
}
);
if (record || recordFrame) {
try {
const planSummary = result.plan.summary || task;
this.addContext("decision", `Plan: ${planSummary}`, 0.8);
const approved = result.critique?.approved ? "approved" : "needs_changes";
this.addContext("decision", `Critique: ${approved}`, 0.6);
} catch {
}
}
const payload = compact ? { ...result, plan: compactPlan(result.plan) } : result;
return {
content: [
{
type: "text",
text: JSON.stringify({ ok: true, result: payload })
}
],
isError: false
};
}
async handlePlanOnly(args) {
const { runPlanOnly } = await import("../../orchestrators/multimodal/harness.js");
const task = String(args.task || "Plan change");
const plannerModel = args.plannerModel || process.env["STACKMEMORY_MM_PLANNER_MODEL"] || DEFAULT_PLANNER_MODEL;
const plan = await runPlanOnly(
{ task, repoPath: this.projectRoot },
{ plannerModel }
);
return {
content: [
{
type: "text",
text: JSON.stringify({ ok: true, plan })
}
],
isError: false
};
}
async handleCallCodex(args) {
const { callCodexCLI } = await import("../../orchestrators/multimodal/providers.js");
const prompt = String(args.prompt || "");
const extraArgs = Array.isArray(args.args) ? args.args : [];
const execute = Boolean(args.execute);
const resp = callCodexCLI(prompt, extraArgs, !execute);
return {
content: [
{
type: "text",
text: JSON.stringify({
ok: resp.ok,
command: resp.command,
output: resp.output
})
}
],
isError: false
};
}
async handleCallClaude(args) {
const { callClaude } = await import("../../orchestrators/multimodal/providers.js");
const prompt = String(args.prompt || "");
const model = args.model || process.env["STACKMEMORY_MM_PLANNER_MODEL"] || DEFAULT_PLANNER_MODEL;
const system = args.system || "You are a precise assistant. Return plain text unless asked for JSON.";
const text = await callClaude(prompt, { model, system });
return {
content: [
{
type: "text",
text: JSON.stringify({ ok: true, text })
}
],
isError: false
};
}
// Pending plan persistence (best-effort)
getPendingStoreDir() {
return join(this.projectRoot, ".stackmemory", "build");
}
getPendingStorePath() {
return join(this.getPendingStoreDir(), "pending.json");
}
loadPendingPlans() {
try {
const file = this.getPendingStorePath();
let sourceFile = file;
if (!existsSync(file)) {
const legacy = join(
this.projectRoot,
".stackmemory",
"mm-spike",
"pending.json"
);
if (existsSync(legacy)) sourceFile = legacy;
else return;
}
const data = JSON.parse(readFileSync(sourceFile, "utf-8"));
if (data && typeof data === "object") {
this.pendingPlans = new Map(Object.entries(data));
if (sourceFile !== file) this.savePendingPlans();
}
} catch {
}
}
savePendingPlans() {
try {
const dir = this.getPendingStoreDir();
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
const file = this.getPendingStorePath();
const obj = Object.fromEntries(this.pendingPlans);
writeFileSync(file, JSON.stringify(obj, null, 2));
} catch {
}
}
async handlePlanGate(args) {
const { runPlanOnly } = await import("../../orchestrators/multimodal/harness.js");
const task = String(args.task || "Plan change");
const plannerModel = args.plannerModel || process.env["STACKMEMORY_MM_PLANNER_MODEL"] || DEFAULT_PLANNER_MODEL;
const plan = await runPlanOnly(
{ task, repoPath: this.projectRoot },
{ plannerModel }
);
const approvalId = `appr_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
this.pendingPlans.set(approvalId, { task, plan, createdAt: Date.now() });
this.savePendingPlans();
const compact = Boolean(args.compact);
const planOut = compact ? compactPlan(plan) : plan;
return {
content: [
{
type: "text",
text: JSON.stringify({ ok: true, approvalId, plan: planOut })
}
],
isError: false
};
}
async handleApprovePlan(args) {
const { runSpike } = await import("../../orchestrators/multimodal/harness.js");
const approvalId = String(args.approvalId || "");
const pending = this.pendingPlans.get(approvalId);
if (!pending) {
return {
content: [
{
type: "text",
text: JSON.stringify({ ok: false, error: "Invalid approvalId" })
}
],
isError: false
};
}
const implementer = args.implementer || process.env["STACKMEMORY_MM_IMPLEMENTER"] || DEFAULT_IMPLEMENTER;
const maxIters = Number(
args.maxIters ?? process.env["STACKMEMORY_MM_MAX_ITERS"] ?? DEFAULT_MAX_ITERS
);
const recordFrame = args.recordFrame !== false;
const execute = args.execute !== false;
const result = await runSpike(
{ task: pending.task, repoPath: this.projectRoot },
{
plannerModel: process.env["STACKMEMORY_MM_PLANNER_MODEL"] || DEFAULT_PLANNER_MODEL,
reviewerModel: process.env["STACKMEMORY_MM_REVIEWER_MODEL"] || process.env["STACKMEMORY_MM_PLANNER_MODEL"] || DEFAULT_PLANNER_MODEL,
implementer: implementer === "claude" ? "claude" : "codex",
maxIters: isFinite(maxIters) ? Math.max(1, maxIters) : 2,
dryRun: !execute,
recordFrame
}
);
this.pendingPlans.delete(approvalId);
this.savePendingPlans();
const compact = Boolean(args.compact);
const payload = compact ? { ...result, plan: compactPlan(result.plan) } : result;
return {
content: [
{
type: "text",
text: JSON.stringify({ ok: true, approvalId, result: payload })
}
],
isError: false
};
}