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

457 lines (456 loc) 13.8 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { DEFAULT_GREPTILE_CONFIG } from "../../greptile/config.js"; import { GreptileClient, GreptileClientError } from "../../greptile/client.js"; import { logger } from "../../../core/monitoring/logger.js"; class GreptileHandlers { config; client = null; constructor(deps) { this.config = { ...DEFAULT_GREPTILE_CONFIG, ...deps?.config }; if (this.config.enabled && this.config.apiKey) { try { this.client = new GreptileClient(this.config); } catch { this.client = null; } } } getToolDefinitions() { return [ { name: "greptile_pr_comments", description: "Get PR review comments from Greptile. Returns unaddressed comments with suggestedCode for auto-fix workflows.", inputSchema: { type: "object", properties: { name: { type: "string", description: 'Repository full name (e.g., "owner/repo")' }, remote: { type: "string", enum: ["github", "gitlab", "azure", "bitbucket"], description: "Remote provider" }, defaultBranch: { type: "string", description: 'Default branch (e.g., "main")' }, prNumber: { type: "number", description: "Pull request number" }, greptileGenerated: { type: "boolean", description: "Filter for only Greptile review comments" }, addressed: { type: "boolean", description: "Filter by comment addressed status" } }, required: ["name", "remote", "defaultBranch", "prNumber"] } }, { name: "greptile_pr_details", description: "Get detailed PR information including metadata, statistics, and review analysis from Greptile.", inputSchema: { type: "object", properties: { name: { type: "string", description: 'Repository full name (e.g., "owner/repo")' }, remote: { type: "string", enum: ["github", "gitlab", "azure", "bitbucket"], description: "Remote provider" }, defaultBranch: { type: "string", description: "Default branch" }, prNumber: { type: "number", description: "Pull request number" } }, required: ["name", "remote", "defaultBranch", "prNumber"] } }, { name: "greptile_list_prs", description: "List pull requests. Filter by repository, branch, author, or state.", inputSchema: { type: "object", properties: { name: { type: "string", description: 'Repository full name (e.g., "owner/repo")' }, remote: { type: "string", enum: ["github", "gitlab", "azure", "bitbucket"], description: "Remote provider" }, defaultBranch: { type: "string", description: "Default branch" }, sourceBranch: { type: "string", description: "Filter by source branch name" }, authorLogin: { type: "string", description: "Filter by PR author username" }, state: { type: "string", enum: ["open", "closed", "merged"], description: "Filter by PR state" }, limit: { type: "number", description: "Max results (default 20)" } } } }, { name: "greptile_trigger_review", description: "Trigger a Greptile code review for a pull request.", inputSchema: { type: "object", properties: { name: { type: "string", description: 'Repository full name (e.g., "owner/repo")' }, remote: { type: "string", enum: ["github", "gitlab", "azure", "bitbucket"], description: "Remote provider" }, defaultBranch: { type: "string", description: "Default branch" }, prNumber: { type: "number", description: "Pull request number" }, branch: { type: "string", description: "Current working branch" } }, required: ["name", "remote", "prNumber"] } }, { name: "greptile_search_patterns", description: "Search custom coding patterns and instructions in Greptile.", inputSchema: { type: "object", properties: { query: { type: "string", description: "Search query for pattern content" }, limit: { type: "number", description: "Max results (default 10)" } }, required: ["query"] } }, { name: "greptile_create_pattern", description: "Create a new custom coding pattern or instruction in Greptile.", inputSchema: { type: "object", properties: { body: { type: "string", description: "Pattern content" }, type: { type: "string", enum: ["CUSTOM_INSTRUCTION", "PATTERN"], description: "Context type (default: CUSTOM_INSTRUCTION)" }, scopes: { type: "object", description: "Boolean expression defining where this pattern applies" } }, required: ["body"] } }, { name: "greptile_status", description: "Check Greptile integration connection status.", inputSchema: { type: "object", properties: {} } } ]; } async handleListPRComments(args) { if (!this.client) return this.disabledResponse(); try { const toolArgs = { name: args.name, remote: args.remote, defaultBranch: args.defaultBranch, prNumber: args.prNumber }; if (args.greptileGenerated !== void 0) toolArgs.greptileGenerated = args.greptileGenerated; if (args.addressed !== void 0) toolArgs.addressed = args.addressed; const result = await this.client.callTool( "list_merge_request_comments", toolArgs ); let actionableCount = 0; if (Array.isArray(result)) { actionableCount = result.filter( (c) => !c.addressed && (c.suggestedCode || typeof c.body === "string" && c.body.includes("```suggestion")) ).length; } return { content: [ { type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) } ], metadata: { actionableCount, tool: "list_merge_request_comments" } }; } catch (error) { return this.handleError("listPRComments", error); } } async handleGetMergeRequest(args) { if (!this.client) return this.disabledResponse(); try { const result = await this.client.callTool("get_merge_request", { name: args.name, remote: args.remote, defaultBranch: args.defaultBranch, prNumber: args.prNumber }); return { content: [ { type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) } ], metadata: { tool: "get_merge_request" } }; } catch (error) { return this.handleError("getMergeRequest", error); } } async handleListPullRequests(args) { if (!this.client) return this.disabledResponse(); try { const toolArgs = {}; if (args.name) toolArgs.name = args.name; if (args.remote) toolArgs.remote = args.remote; if (args.defaultBranch) toolArgs.defaultBranch = args.defaultBranch; if (args.sourceBranch) toolArgs.sourceBranch = args.sourceBranch; if (args.authorLogin) toolArgs.authorLogin = args.authorLogin; if (args.state) toolArgs.state = args.state; if (args.limit) toolArgs.limit = args.limit; const result = await this.client.callTool("list_pull_requests", toolArgs); return { content: [ { type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) } ], metadata: { tool: "list_pull_requests" } }; } catch (error) { return this.handleError("listPullRequests", error); } } async handleTriggerCodeReview(args) { if (!this.client) return this.disabledResponse(); try { const toolArgs = { name: args.name, remote: args.remote, prNumber: args.prNumber }; if (args.defaultBranch) toolArgs.defaultBranch = args.defaultBranch; if (args.branch) toolArgs.branch = args.branch; const result = await this.client.callTool( "trigger_code_review", toolArgs ); return { content: [ { type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) } ], metadata: { tool: "trigger_code_review" } }; } catch (error) { return this.handleError("triggerCodeReview", error); } } async handleSearchPatterns(args) { if (!this.client) return this.disabledResponse(); try { const toolArgs = { query: args.query }; if (args.limit) toolArgs.limit = args.limit; const result = await this.client.callTool( "search_custom_context", toolArgs ); return { content: [ { type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) } ], metadata: { tool: "search_custom_context" } }; } catch (error) { return this.handleError("searchPatterns", error); } } async handleCreatePattern(args) { if (!this.client) return this.disabledResponse(); try { const toolArgs = { body: args.body }; if (args.type) toolArgs.type = args.type; if (args.scopes) toolArgs.scopes = args.scopes; const result = await this.client.callTool( "create_custom_context", toolArgs ); return { content: [ { type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) } ], metadata: { tool: "create_custom_context" } }; } catch (error) { return this.handleError("createPattern", error); } } async handleStatus() { if (!this.client) { return { content: [ { type: "text", text: JSON.stringify( { connected: false, message: "Greptile integration disabled (GREPTILE_API_KEY not set)" }, null, 2 ) } ], metadata: { connected: false } }; } try { await this.client.callTool("list_pull_requests", { limit: 1 }); return { content: [ { type: "text", text: JSON.stringify( { connected: true, endpoint: this.config.mcpEndpoint }, null, 2 ) } ], metadata: { connected: true } }; } catch (error) { const msg = error instanceof Error ? error.message : String(error); return { content: [ { type: "text", text: JSON.stringify( { connected: false, error: msg, endpoint: this.config.mcpEndpoint }, null, 2 ) } ], metadata: { connected: false, error: msg } }; } } disabledResponse() { return { content: [ { type: "text", text: "Greptile integration disabled (GREPTILE_API_KEY not set)" } ] }; } handleError(operation, error) { const msg = error instanceof Error ? error.message : String(error); const isConnectionError = msg.includes("ECONNREFUSED") || msg.includes("fetch failed") || msg.includes("network") || error instanceof GreptileClientError; logger.debug(`Greptile ${operation} failed`, { error: msg }); if (isConnectionError) { return { content: [ { type: "text", text: `Greptile unavailable (${operation}). The service may be temporarily unreachable.` } ], metadata: { error: true, unavailable: true, operation } }; } return { content: [ { type: "text", text: `Greptile ${operation} error: ${msg}` } ], metadata: { error: true, operation, message: msg } }; } } export { GreptileHandlers };