UNPKG

@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

1,420 lines (1,418 loc) 110 kB
#!/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, readdirSync, existsSync, mkdirSync, writeFileSync, appendFileSync } from "fs"; import { homedir } from "os"; 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 { 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 { GreptileHandlers } from "./handlers/greptile-handlers.js"; import { CordHandlers } from "./handlers/cord-handlers.js"; import { TeamHandlers } from "./handlers/team-handlers.js"; import { SQLiteAdapter } from "../../core/database/sqlite-adapter.js"; import { generateChronologicalDigest } from "../../core/digest/chronological-digest.js"; import { fuzzyEdit } from "../../utils/fuzzy-edit.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; greptileHandlers; providerHandlers = null; cordHandlers = null; teamHandlers = null; 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.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()) ); CREATE TABLE IF NOT EXISTS edit_telemetry ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER NOT NULL DEFAULT (unixepoch()), session_id TEXT, tool_name TEXT NOT NULL, file_path TEXT, success INTEGER NOT NULL DEFAULT 1, error_type TEXT, error_message TEXT ); CREATE INDEX IF NOT EXISTS idx_edit_telemetry_ts ON edit_telemetry(timestamp); `); 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.greptileHandlers = new GreptileHandlers(); this.initCordTeamHandlers(); this.initProviderHandlers(); this.setupHandlers(); this.loadInitialContext(); this.loadPendingPlans(); this.browserMCP.initialize(this.server).catch((error) => { logger.error("Failed to initialize Browser MCP", error); }); import("../../core/storage/obsidian-vault-adapter.js").then(({ initObsidianVault }) => initObsidianVault()).catch(() => { }); 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 }); } } 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); }); } async initCordTeamHandlers() { try { const dbPath = join(this.projectRoot, ".stackmemory", "context.db"); const adapter = new SQLiteAdapter(this.projectId, { dbPath, walMode: true }); await adapter.connect(); this.cordHandlers = new CordHandlers({ frameManager: this.frameManager, dbAdapter: adapter }); this.teamHandlers = new TeamHandlers({ frameManager: this.frameManager, dbAdapter: adapter }); logger.info("Cord and Team handlers initialized"); } catch (error) { logger.warn("Failed to initialize Cord/Team handlers", { error }); } } async initProviderHandlers() { if (!isFeatureEnabled("multiProvider")) return; try { const { ProviderHandlers } = await import("./handlers/provider-handlers.js"); this.providerHandlers = new ProviderHandlers(); logger.info("Provider handlers initialized (multiProvider enabled)"); } catch (error) { logger.warn("Failed to initialize provider handlers", { error }); } } 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" } } } }, // Planning tools (only when ANTHROPIC_API_KEY is set) ...process.env.ANTHROPIC_API_KEY ? [ { 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", 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: "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 (only when DIFFMEM_ENDPOINT or DIFFMEM_ENABLED is set) ...process.env.DIFFMEM_ENDPOINT || process.env.DIFFMEM_ENABLED === "true" ? this.diffMemHandlers.getToolDefinitions() : [], // Cord task orchestration tools { name: "cord_spawn", description: "Create a subtask with clean context (spawn). Child sees only its prompt and completed blocker results.", inputSchema: { type: "object", properties: { goal: { type: "string", description: "What this task should accomplish" }, prompt: { type: "string", description: "Detailed instructions for the task" }, blocked_by: { type: "array", items: { type: "string" }, description: "Task IDs that must complete first" }, parent_id: { type: "string", description: "Parent task ID" } }, required: ["goal"] } }, { name: "cord_fork", description: "Create a subtask with full sibling context (fork). Child sees prompt, blocker results, AND completed sibling results.", inputSchema: { type: "object", properties: { goal: { type: "string", description: "What this task should accomplish" }, prompt: { type: "string", description: "Detailed instructions for the task" }, blocked_by: { type: "array", items: { type: "string" }, description: "Task IDs that must complete first" }, parent_id: { type: "string", description: "Parent task ID" } }, required: ["goal"] } }, { name: "cord_complete", description: "Mark a cord task as completed with a result. Automatically unblocks dependent tasks.", inputSchema: { type: "object", properties: { task_id: { type: "string", description: "Task ID to complete" }, result: { type: "string", description: "The result/output of this task" } }, required: ["task_id", "result"] } }, { name: "cord_ask", description: "Create an ask task \u2014 a question that needs an answer before dependent tasks can proceed.", inputSchema: { type: "object", properties: { question: { type: "string", description: "The question to ask" }, options: { type: "array", items: { type: "string" }, description: "Optional list of answer choices" }, parent_id: { type: "string", description: "Parent task ID" } }, required: ["question"] } }, { name: "cord_tree", description: "View the cord task tree with context scoping. Shows active, blocked, or completed tasks.", inputSchema: { type: "object", properties: { task_id: { type: "string", description: "Root task ID to show subtree (omit for full tree)" }, include_results: { type: "boolean", default: false, description: "Include task results in output" } } } }, // Team collaboration tools { name: "team_context_get", description: "Get context from other agents working on the same project. Returns recent frames and shared anchors.", inputSchema: { type: "object", properties: { limit: { type: "number", default: 10, description: "Max frames to return" }, types: { type: "array", items: { type: "string" }, description: "Filter by frame types" }, since: { type: "number", description: "Only frames created after this timestamp (epoch ms)" } } } }, { name: "team_context_share", description: "Share a piece of context with other agents. Creates a high-priority anchor visible to team_context_get.", inputSchema: { type: "object", properties: { content: { type: "string", description: "The context to share" }, type: { type: "string", enum: [ "FACT", "DECISION", "CONSTRAINT", "INTERFACE_CONTRACT", "TODO", "RISK" ], default: "FACT", description: "Type of context" }, priority: { type: "number", minimum: 1, maximum: 10, default: 8, description: "Priority level (1-10)" } }, required: ["content"] } }, { name: "team_search", description: "Search across all agents' context in the project. Uses full-text search across all sessions.", inputSchema: { type: "object", properties: { query: { type: "string", description: "Search query" }, limit: { type: "number", default: 20, description: "Maximum results to return" }, include_events: { type: "boolean", default: false, description: "Include events in results" } }, required: ["query"] } }, // Greptile tools (only active when GREPTILE_API_KEY is set) ...process.env.GREPTILE_API_KEY ? this.greptileHandlers.getToolDefinitions() : [], // Provider tools (only active when STACKMEMORY_MULTI_PROVIDER=true) ...isFeatureEnabled("multiProvider") ? [ { name: "delegate_to_model", description: "Route a prompt to a specific provider/model. Uses smart cost-based routing by default.", inputSchema: { type: "object", properties: { prompt: { type: "string", description: "The prompt to send" }, provider: { type: "string", enum: [ "anthropic", "cerebras", "deepinfra", "openai", "openrouter" ], description: "Override provider (auto-routes if omitted)" }, model: { type: "string", description: "Override model name" }, taskType: { type: "string", enum: [ "linting", "context", "code", "testing", "review", "plan" ], description: "Task type for auto-routing" }, maxTokens: { type: "number", description: "Max tokens" }, temperature: { type: "number" }, system: { type: "string", description: "System prompt" } }, required: ["prompt"] } }, { name: "batch_submit", description: "Submit prompts to Anthropic Batch API (50% discount, async)", inputSchema: { type: "object", properties: { prompts: { type: "array", items: { type: "object", properties: { id: { type: "string" }, prompt: { type: "string" }, model: { type: "string" }, maxTokens: { type: "number" }, system: { type: "string" } }, required: ["id", "prompt"] }, description: "Array of prompts to batch" }, description: { type: "string", description: "Batch job description" } }, required: ["prompts"] } }, { name: "batch_check", description: "Check status or retrieve results for a batch job", inputSchema: { type: "object", properties: { batchId: { type: "string", description: "Batch job ID" }, retrieve: { type: "boolean", description: "Retrieve results if complete", default: false } }, required: ["batchId"] } } ] : [], // Digest tool { name: "sm_digest", description: "Generate a chronological activity digest for a time period (today/yesterday/week)", inputSchema: { type: "object", properties: { period: { type: "string", enum: ["today", "yesterday", "week"], description: "Time period for the digest" } }, required: ["period"] } } ] }; } ); 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 "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 "linear_create_comment": result = await this.handleLinearCreateComment(args); break; case "linear_update_comment": result = await this.handleLinearUpdateComment(args); break; case "linear_list_comments": result = await this.handleLinearListComments(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.hand