@stackmemoryai/stackmemory 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/src/cli/commands/audit.js +134 -0
  2. package/dist/src/cli/commands/bench.js +252 -0
  3. package/dist/src/cli/commands/dashboard.js +2 -1
  4. package/dist/src/cli/commands/stats.js +118 -0
  5. package/dist/src/cli/index.js +6 -0
  6. package/dist/src/core/config/feature-flags.js +7 -1
  7. package/dist/src/core/context/enhanced-rehydration.js +24 -5
  8. package/dist/src/core/extensions/cerebras-adapter.js +28 -0
  9. package/dist/src/core/extensions/deepinfra-adapter.js +32 -0
  10. package/dist/src/core/extensions/provider-adapter.js +33 -240
  11. package/dist/src/core/models/complexity-scorer.js +154 -0
  12. package/dist/src/core/models/model-router.js +230 -36
  13. package/dist/src/core/models/provider-pricing.js +63 -0
  14. package/dist/src/core/models/sensitive-guard.js +112 -0
  15. package/dist/src/core/monitoring/feedback-loops.js +88 -0
  16. package/dist/src/hooks/schemas.js +12 -1
  17. package/dist/src/integrations/anthropic/batch-client.js +256 -0
  18. package/dist/src/integrations/anthropic/client.js +87 -72
  19. package/dist/src/integrations/claude-code/subagent-client.js +133 -12
  20. package/dist/src/integrations/graphiti/client.js +16 -4
  21. package/dist/src/integrations/mcp/handlers/index.js +25 -1
  22. package/dist/src/integrations/mcp/handlers/provider-handlers.js +227 -0
  23. package/dist/src/integrations/mcp/server.js +207 -1
  24. package/dist/src/integrations/mcp/tool-definitions.js +38 -1
  25. package/dist/src/orchestrators/multimodal/baselines.js +128 -0
  26. package/dist/src/orchestrators/multimodal/constants.js +9 -1
  27. package/dist/src/orchestrators/multimodal/harness.js +86 -6
  28. package/dist/src/orchestrators/multimodal/providers.js +113 -2
  29. package/dist/src/skills/recursive-agent-orchestrator.js +15 -10
  30. package/dist/src/utils/fuzzy-edit.js +162 -0
  31. package/package.json +5 -1
@@ -32,12 +32,19 @@ class GraphitiClient {
32
32
  const res = await fetch(`${this.endpoint}${path}`, {
33
33
  ...options,
34
34
  signal: controller.signal,
35
- headers: { "Content-Type": "application/json", ...options.headers || {} }
35
+ headers: {
36
+ "Content-Type": "application/json",
37
+ ...options.headers || {}
38
+ }
36
39
  });
37
40
  clearTimeout(timeoutId);
38
41
  if (!res.ok) {
39
42
  const msg = await res.text().catch(() => res.statusText);
40
- throw new GraphitiClientError(`Request failed: ${msg}`, "HTTP_ERROR", res.status);
43
+ throw new GraphitiClientError(
44
+ `Request failed: ${msg}`,
45
+ "HTTP_ERROR",
46
+ res.status
47
+ );
41
48
  }
42
49
  return await res.json();
43
50
  } catch (err) {
@@ -53,7 +60,10 @@ class GraphitiClient {
53
60
  }
54
61
  }
55
62
  clearTimeout(timeoutId);
56
- throw new GraphitiClientError(lastError?.message || "Network error", "NETWORK_ERROR");
63
+ throw new GraphitiClientError(
64
+ lastError?.message || "Network error",
65
+ "NETWORK_ERROR"
66
+ );
57
67
  }
58
68
  // Episodes
59
69
  async upsertEpisode(episode) {
@@ -91,7 +101,9 @@ class GraphitiClient {
91
101
  // Health/status
92
102
  async getStatus() {
93
103
  try {
94
- return await this.request(`/status?namespace=${encodeURIComponent(this.namespace)}`);
104
+ return await this.request(
105
+ `/status?namespace=${encodeURIComponent(this.namespace)}`
106
+ );
95
107
  } catch {
96
108
  return { connected: false };
97
109
  }
@@ -15,6 +15,9 @@ import {
15
15
  import {
16
16
  DiscoveryHandlers
17
17
  } from "./discovery-handlers.js";
18
+ import {
19
+ ProviderHandlers
20
+ } from "./provider-handlers.js";
18
21
  import {
19
22
  ContextHandlers as ContextHandlers2
20
23
  } from "./context-handlers.js";
@@ -23,11 +26,13 @@ import {
23
26
  LinearHandlers as LinearHandlers2
24
27
  } from "./linear-handlers.js";
25
28
  import { TraceHandlers as TraceHandlers2 } from "./trace-handlers.js";
29
+ import { ProviderHandlers as ProviderHandlers2 } from "./provider-handlers.js";
26
30
  class MCPHandlerFactory {
27
31
  contextHandlers;
28
32
  taskHandlers;
29
33
  linearHandlers;
30
34
  traceHandlers;
35
+ providerHandlers;
31
36
  constructor(deps) {
32
37
  this.contextHandlers = new ContextHandlers2({
33
38
  frameManager: deps.frameManager
@@ -45,6 +50,7 @@ class MCPHandlerFactory {
45
50
  traceDetector: deps.traceDetector,
46
51
  browserMCP: deps.browserMCP
47
52
  });
53
+ this.providerHandlers = new ProviderHandlers2();
48
54
  }
49
55
  /**
50
56
  * Get handler for a specific tool
@@ -111,6 +117,19 @@ class MCPHandlerFactory {
111
117
  return this.traceHandlers.handleStopBrowserDebug.bind(
112
118
  this.traceHandlers
113
119
  );
120
+ // Provider handlers
121
+ case "delegate_to_model":
122
+ return this.providerHandlers.handleDelegateToModel.bind(
123
+ this.providerHandlers
124
+ );
125
+ case "batch_submit":
126
+ return this.providerHandlers.handleBatchSubmit.bind(
127
+ this.providerHandlers
128
+ );
129
+ case "batch_check":
130
+ return this.providerHandlers.handleBatchCheck.bind(
131
+ this.providerHandlers
132
+ );
114
133
  default:
115
134
  throw new Error(`Unknown tool: ${toolName}`);
116
135
  }
@@ -144,7 +163,11 @@ class MCPHandlerFactory {
144
163
  "start_browser_debug",
145
164
  "take_screenshot",
146
165
  "execute_script",
147
- "stop_browser_debug"
166
+ "stop_browser_debug",
167
+ // Provider tools (conditionally active)
168
+ "delegate_to_model",
169
+ "batch_submit",
170
+ "batch_check"
148
171
  ];
149
172
  }
150
173
  /**
@@ -159,6 +182,7 @@ export {
159
182
  DiscoveryHandlers,
160
183
  LinearHandlers,
161
184
  MCPHandlerFactory,
185
+ ProviderHandlers,
162
186
  TaskHandlers,
163
187
  TraceHandlers
164
188
  };
@@ -0,0 +1,227 @@
1
+ import { fileURLToPath as __fileURLToPath } from 'url';
2
+ import { dirname as __pathDirname } from 'path';
3
+ const __filename = __fileURLToPath(import.meta.url);
4
+ const __dirname = __pathDirname(__filename);
5
+ import { logger } from "../../../core/monitoring/logger.js";
6
+ import { isFeatureEnabled } from "../../../core/config/feature-flags.js";
7
+ import {
8
+ createProvider
9
+ } from "../../../core/extensions/provider-adapter.js";
10
+ import {
11
+ getOptimalProvider
12
+ } from "../../../core/models/model-router.js";
13
+ import { scoreComplexity } from "../../../core/models/complexity-scorer.js";
14
+ import {
15
+ AnthropicBatchClient
16
+ } from "../../anthropic/batch-client.js";
17
+ function errorResponse(err) {
18
+ return {
19
+ content: [{ type: "text", text: JSON.stringify(err, null, 2) }]
20
+ };
21
+ }
22
+ function classifyApiError(error, provider) {
23
+ const msg = error.message || String(error);
24
+ const status = error.status || (msg.match(/(\d{3})/)?.[1] ? parseInt(msg.match(/(\d{3})/)[1]) : void 0);
25
+ if (status === 429) {
26
+ return {
27
+ errorType: "rate_limit",
28
+ message: msg,
29
+ recommendation: `Rate limited by ${provider}. Retry after a delay or switch to a different provider.`,
30
+ provider
31
+ };
32
+ }
33
+ if (status && status >= 500) {
34
+ return {
35
+ errorType: "server_error",
36
+ message: msg,
37
+ recommendation: `${provider} returned a server error. Try a different provider or retry later.`,
38
+ provider
39
+ };
40
+ }
41
+ return {
42
+ errorType: "api_error",
43
+ message: msg,
44
+ recommendation: `API call to ${provider} failed. Check the model name, API key, and base URL.`,
45
+ provider
46
+ };
47
+ }
48
+ class ProviderHandlers {
49
+ batchClient;
50
+ constructor(_deps) {
51
+ }
52
+ getBatchClient() {
53
+ if (!this.batchClient) {
54
+ this.batchClient = new AnthropicBatchClient();
55
+ }
56
+ return this.batchClient;
57
+ }
58
+ /**
59
+ * delegate_to_model — route a prompt to a specific provider+model
60
+ */
61
+ async handleDelegateToModel(args) {
62
+ if (!isFeatureEnabled("multiProvider")) {
63
+ return errorResponse({
64
+ errorType: "feature_disabled",
65
+ message: "Multi-provider routing is disabled.",
66
+ recommendation: "Set STACKMEMORY_MULTI_PROVIDER=true to enable multi-provider routing."
67
+ });
68
+ }
69
+ const taskType = args.taskType || "default";
70
+ const preference = args.provider;
71
+ const complexity = scoreComplexity(args.prompt);
72
+ const optimal = getOptimalProvider(taskType, preference, {
73
+ task: args.prompt
74
+ });
75
+ logger.info("delegate_to_model routing", {
76
+ taskType,
77
+ complexity: complexity.tier,
78
+ score: complexity.score,
79
+ provider: optimal.provider
80
+ });
81
+ const providerModel = args.model || optimal.model;
82
+ const apiKey = process.env[optimal.apiKeyEnv] || "";
83
+ if (!apiKey) {
84
+ return errorResponse({
85
+ errorType: "missing_api_key",
86
+ message: `No API key found for ${optimal.provider} (env: ${optimal.apiKeyEnv})`,
87
+ recommendation: `Set the ${optimal.apiKeyEnv} environment variable or choose a different provider.`,
88
+ provider: optimal.provider
89
+ });
90
+ }
91
+ try {
92
+ const adapter = createProvider(optimal.provider, {
93
+ apiKey,
94
+ baseUrl: optimal.baseUrl
95
+ });
96
+ const messages = [{ role: "user", content: args.prompt }];
97
+ const result = await adapter.complete(messages, {
98
+ model: providerModel,
99
+ maxTokens: args.maxTokens || 4096,
100
+ temperature: args.temperature,
101
+ system: args.system
102
+ });
103
+ const text = result.content.filter((c) => c.type === "text").map((c) => c.text).join("");
104
+ return {
105
+ content: [
106
+ {
107
+ type: "text",
108
+ text: JSON.stringify(
109
+ {
110
+ provider: optimal.provider,
111
+ model: providerModel,
112
+ response: text,
113
+ usage: result.usage
114
+ },
115
+ null,
116
+ 2
117
+ )
118
+ }
119
+ ]
120
+ };
121
+ } catch (error) {
122
+ logger.error("delegate_to_model failed", { error: error.message });
123
+ return errorResponse(classifyApiError(error, optimal.provider));
124
+ }
125
+ }
126
+ /**
127
+ * batch_submit — submit prompts to Anthropic Batch API
128
+ */
129
+ async handleBatchSubmit(args) {
130
+ if (!isFeatureEnabled("multiProvider")) {
131
+ return errorResponse({
132
+ errorType: "feature_disabled",
133
+ message: "Multi-provider routing is disabled.",
134
+ recommendation: "Set STACKMEMORY_MULTI_PROVIDER=true to enable multi-provider routing."
135
+ });
136
+ }
137
+ try {
138
+ const batchClient = this.getBatchClient();
139
+ const requests = args.prompts.map((p) => ({
140
+ custom_id: p.id,
141
+ params: {
142
+ model: p.model || "claude-sonnet-4-5-20250929",
143
+ max_tokens: p.maxTokens || 4096,
144
+ messages: [{ role: "user", content: p.prompt }],
145
+ system: p.system
146
+ }
147
+ }));
148
+ const batchId = await batchClient.submit(requests, args.description);
149
+ return {
150
+ content: [
151
+ {
152
+ type: "text",
153
+ text: JSON.stringify(
154
+ {
155
+ batchId,
156
+ status: "submitted",
157
+ requestCount: requests.length
158
+ },
159
+ null,
160
+ 2
161
+ )
162
+ }
163
+ ]
164
+ };
165
+ } catch (error) {
166
+ return errorResponse({
167
+ errorType: "batch_error",
168
+ message: error.message,
169
+ recommendation: "Check ANTHROPIC_API_KEY and batch request format."
170
+ });
171
+ }
172
+ }
173
+ /**
174
+ * batch_check — poll status / retrieve results
175
+ */
176
+ async handleBatchCheck(args) {
177
+ if (!isFeatureEnabled("multiProvider")) {
178
+ return errorResponse({
179
+ errorType: "feature_disabled",
180
+ message: "Multi-provider routing is disabled.",
181
+ recommendation: "Set STACKMEMORY_MULTI_PROVIDER=true to enable multi-provider routing."
182
+ });
183
+ }
184
+ try {
185
+ const batchClient = this.getBatchClient();
186
+ const job = await batchClient.poll(args.batchId);
187
+ if (args.retrieve && job.processing_status === "ended") {
188
+ const results = await batchClient.retrieve(args.batchId);
189
+ return {
190
+ content: [
191
+ {
192
+ type: "text",
193
+ text: JSON.stringify({ job, results }, null, 2)
194
+ }
195
+ ]
196
+ };
197
+ }
198
+ return {
199
+ content: [
200
+ {
201
+ type: "text",
202
+ text: JSON.stringify(
203
+ {
204
+ batchId: args.batchId,
205
+ status: job.processing_status,
206
+ counts: job.request_counts,
207
+ createdAt: job.created_at,
208
+ endedAt: job.ended_at
209
+ },
210
+ null,
211
+ 2
212
+ )
213
+ }
214
+ ]
215
+ };
216
+ } catch (error) {
217
+ return errorResponse({
218
+ errorType: "batch_error",
219
+ message: error.message,
220
+ recommendation: "Check the batchId is valid and ANTHROPIC_API_KEY is set."
221
+ });
222
+ }
223
+ }
224
+ }
225
+ export {
226
+ ProviderHandlers
227
+ };
@@ -30,6 +30,7 @@ import { TraceDetector } from "../../core/trace/trace-detector.js";
30
30
  import { LLMContextRetrieval } from "../../core/retrieval/index.js";
31
31
  import { DiscoveryHandlers } from "./handlers/discovery-handlers.js";
32
32
  import { DiffMemHandlers } from "./handlers/diffmem-handlers.js";
33
+ import { fuzzyEdit } from "../../utils/fuzzy-edit.js";
33
34
  import { v4 as uuidv4 } from "uuid";
34
35
  import {
35
36
  DEFAULT_PLANNER_MODEL,
@@ -62,6 +63,7 @@ class LocalStackMemoryMCP {
62
63
  contextRetrieval;
63
64
  discoveryHandlers;
64
65
  diffMemHandlers;
66
+ providerHandlers = null;
65
67
  pendingPlans = /* @__PURE__ */ new Map();
66
68
  constructor() {
67
69
  this.projectRoot = this.findProjectRoot();
@@ -91,6 +93,18 @@ class LocalStackMemoryMCP {
91
93
  influence_score REAL,
92
94
  timestamp INTEGER DEFAULT (unixepoch())
93
95
  );
96
+
97
+ CREATE TABLE IF NOT EXISTS edit_telemetry (
98
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
99
+ timestamp INTEGER NOT NULL DEFAULT (unixepoch()),
100
+ session_id TEXT,
101
+ tool_name TEXT NOT NULL,
102
+ file_path TEXT,
103
+ success INTEGER NOT NULL DEFAULT 1,
104
+ error_type TEXT,
105
+ error_message TEXT
106
+ );
107
+ CREATE INDEX IF NOT EXISTS idx_edit_telemetry_ts ON edit_telemetry(timestamp);
94
108
  `);
95
109
  this.frameManager = new FrameManager(this.db, this.projectId);
96
110
  this.initLinearIfEnabled();
@@ -122,6 +136,7 @@ class LocalStackMemoryMCP {
122
136
  projectRoot: this.projectRoot
123
137
  });
124
138
  this.diffMemHandlers = new DiffMemHandlers();
139
+ this.initProviderHandlers();
125
140
  this.setupHandlers();
126
141
  this.loadInitialContext();
127
142
  this.loadPendingPlans();
@@ -243,6 +258,16 @@ ${summary}...`, 0.8);
243
258
  this.contexts.set(ctx.id, ctx);
244
259
  });
245
260
  }
261
+ async initProviderHandlers() {
262
+ if (!isFeatureEnabled("multiProvider")) return;
263
+ try {
264
+ const { ProviderHandlers } = await import("./handlers/provider-handlers.js");
265
+ this.providerHandlers = new ProviderHandlers();
266
+ logger.info("Provider handlers initialized (multiProvider enabled)");
267
+ } catch (error) {
268
+ logger.warn("Failed to initialize provider handlers", { error });
269
+ }
270
+ }
246
271
  setupHandlers() {
247
272
  this.server.setRequestHandler(
248
273
  z.object({
@@ -1047,7 +1072,108 @@ ${summary}...`, 0.8);
1047
1072
  type: "object",
1048
1073
  properties: {}
1049
1074
  }
1050
- }
1075
+ },
1076
+ // Provider tools (only active when STACKMEMORY_MULTI_PROVIDER=true)
1077
+ ...isFeatureEnabled("multiProvider") ? [
1078
+ {
1079
+ name: "delegate_to_model",
1080
+ description: "Route a prompt to a specific provider/model. Uses smart cost-based routing by default.",
1081
+ inputSchema: {
1082
+ type: "object",
1083
+ properties: {
1084
+ prompt: {
1085
+ type: "string",
1086
+ description: "The prompt to send"
1087
+ },
1088
+ provider: {
1089
+ type: "string",
1090
+ enum: [
1091
+ "anthropic",
1092
+ "cerebras",
1093
+ "deepinfra",
1094
+ "openai",
1095
+ "openrouter"
1096
+ ],
1097
+ description: "Override provider (auto-routes if omitted)"
1098
+ },
1099
+ model: {
1100
+ type: "string",
1101
+ description: "Override model name"
1102
+ },
1103
+ taskType: {
1104
+ type: "string",
1105
+ enum: [
1106
+ "linting",
1107
+ "context",
1108
+ "code",
1109
+ "testing",
1110
+ "review",
1111
+ "plan"
1112
+ ],
1113
+ description: "Task type for auto-routing"
1114
+ },
1115
+ maxTokens: {
1116
+ type: "number",
1117
+ description: "Max tokens"
1118
+ },
1119
+ temperature: { type: "number" },
1120
+ system: {
1121
+ type: "string",
1122
+ description: "System prompt"
1123
+ }
1124
+ },
1125
+ required: ["prompt"]
1126
+ }
1127
+ },
1128
+ {
1129
+ name: "batch_submit",
1130
+ description: "Submit prompts to Anthropic Batch API (50% discount, async)",
1131
+ inputSchema: {
1132
+ type: "object",
1133
+ properties: {
1134
+ prompts: {
1135
+ type: "array",
1136
+ items: {
1137
+ type: "object",
1138
+ properties: {
1139
+ id: { type: "string" },
1140
+ prompt: { type: "string" },
1141
+ model: { type: "string" },
1142
+ maxTokens: { type: "number" },
1143
+ system: { type: "string" }
1144
+ },
1145
+ required: ["id", "prompt"]
1146
+ },
1147
+ description: "Array of prompts to batch"
1148
+ },
1149
+ description: {
1150
+ type: "string",
1151
+ description: "Batch job description"
1152
+ }
1153
+ },
1154
+ required: ["prompts"]
1155
+ }
1156
+ },
1157
+ {
1158
+ name: "batch_check",
1159
+ description: "Check status or retrieve results for a batch job",
1160
+ inputSchema: {
1161
+ type: "object",
1162
+ properties: {
1163
+ batchId: {
1164
+ type: "string",
1165
+ description: "Batch job ID"
1166
+ },
1167
+ retrieve: {
1168
+ type: "boolean",
1169
+ description: "Retrieve results if complete",
1170
+ default: false
1171
+ }
1172
+ },
1173
+ required: ["batchId"]
1174
+ }
1175
+ }
1176
+ ] : []
1051
1177
  ]
1052
1178
  };
1053
1179
  }
@@ -1195,6 +1321,58 @@ ${summary}...`, 0.8);
1195
1321
  case "diffmem_status":
1196
1322
  result = await this.diffMemHandlers.handleStatus();
1197
1323
  break;
1324
+ case "sm_edit":
1325
+ result = await this.handleSmEdit(args);
1326
+ break;
1327
+ // Provider tools
1328
+ case "delegate_to_model":
1329
+ if (!this.providerHandlers) {
1330
+ result = {
1331
+ content: [
1332
+ {
1333
+ type: "text",
1334
+ text: "Multi-provider routing is disabled."
1335
+ }
1336
+ ]
1337
+ };
1338
+ } else {
1339
+ result = await this.providerHandlers.handleDelegateToModel(
1340
+ args
1341
+ );
1342
+ }
1343
+ break;
1344
+ case "batch_submit":
1345
+ if (!this.providerHandlers) {
1346
+ result = {
1347
+ content: [
1348
+ {
1349
+ type: "text",
1350
+ text: "Multi-provider routing is disabled."
1351
+ }
1352
+ ]
1353
+ };
1354
+ } else {
1355
+ result = await this.providerHandlers.handleBatchSubmit(
1356
+ args
1357
+ );
1358
+ }
1359
+ break;
1360
+ case "batch_check":
1361
+ if (!this.providerHandlers) {
1362
+ result = {
1363
+ content: [
1364
+ {
1365
+ type: "text",
1366
+ text: "Multi-provider routing is disabled."
1367
+ }
1368
+ ]
1369
+ };
1370
+ } else {
1371
+ result = await this.providerHandlers.handleBatchCheck(
1372
+ args
1373
+ );
1374
+ }
1375
+ break;
1198
1376
  default:
1199
1377
  throw new Error(`Unknown tool: ${name}`);
1200
1378
  }
@@ -2639,6 +2817,34 @@ ${typeBreakdown}`
2639
2817
  };
2640
2818
  }
2641
2819
  }
2820
+ async handleSmEdit(args) {
2821
+ const {
2822
+ file_path: filePath,
2823
+ old_string: oldString,
2824
+ new_string: newString,
2825
+ threshold = 0.85
2826
+ } = args;
2827
+ if (!filePath || !oldString || newString === void 0) {
2828
+ throw new Error("file_path, old_string, and new_string are required");
2829
+ }
2830
+ const { readFileSync: readFileSync2, writeFileSync: writeFileSync2 } = await import("fs");
2831
+ const content = readFileSync2(filePath, "utf-8");
2832
+ const editResult = fuzzyEdit(content, oldString, newString, threshold);
2833
+ if (!editResult) {
2834
+ return {
2835
+ success: false,
2836
+ error: "No match found above threshold",
2837
+ threshold
2838
+ };
2839
+ }
2840
+ writeFileSync2(filePath, editResult.content, "utf-8");
2841
+ return {
2842
+ success: true,
2843
+ method: editResult.match.method,
2844
+ confidence: editResult.match.confidence,
2845
+ matchedText: editResult.match.matchedText.length > 200 ? editResult.match.matchedText.slice(0, 200) + "..." : editResult.match.matchedText
2846
+ };
2847
+ }
2642
2848
  async start() {
2643
2849
  const transport = new StdioServerTransport();
2644
2850
  await this.server.connect(transport);
@@ -12,7 +12,8 @@ class MCPToolDefinitions {
12
12
  ...this.getTaskTools(),
13
13
  ...this.getLinearTools(),
14
14
  ...this.getTraceTools(),
15
- ...this.getDiscoveryTools()
15
+ ...this.getDiscoveryTools(),
16
+ ...this.getEditTools()
16
17
  ];
17
18
  }
18
19
  /**
@@ -670,6 +671,40 @@ class MCPToolDefinitions {
670
671
  }
671
672
  ];
672
673
  }
674
+ /**
675
+ * Edit tools (fuzzy edit fallback)
676
+ */
677
+ getEditTools() {
678
+ return [
679
+ {
680
+ name: "sm_edit",
681
+ description: "Fuzzy file edit \u2014 fallback when Claude Code's Edit tool fails on whitespace or indentation mismatches. Uses four-tier matching: exact, whitespace-normalized, indentation-insensitive, and line-level fuzzy (Levenshtein).",
682
+ inputSchema: {
683
+ type: "object",
684
+ properties: {
685
+ file_path: {
686
+ type: "string",
687
+ description: "Absolute path to the file to edit"
688
+ },
689
+ old_string: {
690
+ type: "string",
691
+ description: "The text to find and replace"
692
+ },
693
+ new_string: {
694
+ type: "string",
695
+ description: "The replacement text"
696
+ },
697
+ threshold: {
698
+ type: "number",
699
+ default: 0.85,
700
+ description: "Minimum similarity threshold for fuzzy matching (0-1). Default 0.85."
701
+ }
702
+ },
703
+ required: ["file_path", "old_string", "new_string"]
704
+ }
705
+ }
706
+ ];
707
+ }
673
708
  /**
674
709
  * Get tool definition by name
675
710
  */
@@ -691,6 +726,8 @@ class MCPToolDefinitions {
691
726
  return this.getTraceTools();
692
727
  case "discovery":
693
728
  return this.getDiscoveryTools();
729
+ case "edit":
730
+ return this.getEditTools();
694
731
  default:
695
732
  return [];
696
733
  }