@nxuss/lemma 1.11.0 → 1.14.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.
- package/README.md +98 -821
- package/dist/cjs/cli/lemma-proxy.d.ts.map +1 -1
- package/dist/cjs/cli/lemma-proxy.js +14 -0
- package/dist/cjs/cli/lemma-proxy.js.map +1 -1
- package/dist/cjs/infra/mcp-tools.d.ts +7 -0
- package/dist/cjs/infra/mcp-tools.d.ts.map +1 -1
- package/dist/cjs/infra/mcp-tools.js +22 -0
- package/dist/cjs/infra/mcp-tools.js.map +1 -1
- package/dist/cjs/mcp/index.js +35 -7
- package/dist/cjs/mcp/index.js.map +1 -1
- package/dist/cjs/mcp/prompts.d.ts +1 -0
- package/dist/cjs/mcp/prompts.d.ts.map +1 -1
- package/dist/cjs/mcp/prompts.js +98 -0
- package/dist/cjs/mcp/prompts.js.map +1 -1
- package/dist/cjs/mcp/resources.d.ts +2 -0
- package/dist/cjs/mcp/resources.d.ts.map +1 -1
- package/dist/cjs/mcp/resources.js +26 -0
- package/dist/cjs/mcp/resources.js.map +1 -1
- package/dist/cjs/mcp/tasks.d.ts +20 -0
- package/dist/cjs/mcp/tasks.d.ts.map +1 -0
- package/dist/cjs/mcp/tasks.js +71 -0
- package/dist/cjs/mcp/tasks.js.map +1 -0
- package/dist/cjs/mcp/tool-profiles.d.ts.map +1 -1
- package/dist/cjs/mcp/tool-profiles.js +29 -50
- package/dist/cjs/mcp/tool-profiles.js.map +1 -1
- package/dist/cjs/mcp/tools.d.ts.map +1 -1
- package/dist/cjs/mcp/tools.js +575 -93
- package/dist/cjs/mcp/tools.js.map +1 -1
- package/dist/cjs/proxy/Gatekeeper.d.ts +29 -0
- package/dist/cjs/proxy/Gatekeeper.d.ts.map +1 -1
- package/dist/cjs/proxy/Gatekeeper.js +122 -1
- package/dist/cjs/proxy/Gatekeeper.js.map +1 -1
- package/dist/cjs/subconscious/TheBrainV2.d.ts +50 -1
- package/dist/cjs/subconscious/TheBrainV2.d.ts.map +1 -1
- package/dist/cjs/subconscious/TheBrainV2.js +149 -8
- package/dist/cjs/subconscious/TheBrainV2.js.map +1 -1
- package/dist/cjs/utils/PatchMatcher.d.ts +1 -0
- package/dist/cjs/utils/PatchMatcher.d.ts.map +1 -1
- package/dist/cjs/utils/PatchMatcher.js +4 -4
- package/dist/cjs/utils/PatchMatcher.js.map +1 -1
- package/dist/esm/cli/lemma-proxy.d.ts.map +1 -1
- package/dist/esm/cli/lemma-proxy.js +14 -0
- package/dist/esm/cli/lemma-proxy.js.map +1 -1
- package/dist/esm/infra/mcp-tools.d.ts +7 -0
- package/dist/esm/infra/mcp-tools.d.ts.map +1 -1
- package/dist/esm/infra/mcp-tools.js +22 -0
- package/dist/esm/infra/mcp-tools.js.map +1 -1
- package/dist/esm/mcp/index.js +37 -9
- package/dist/esm/mcp/index.js.map +1 -1
- package/dist/esm/mcp/prompts.d.ts +1 -0
- package/dist/esm/mcp/prompts.d.ts.map +1 -1
- package/dist/esm/mcp/prompts.js +95 -1
- package/dist/esm/mcp/prompts.js.map +1 -1
- package/dist/esm/mcp/resources.d.ts +2 -0
- package/dist/esm/mcp/resources.d.ts.map +1 -1
- package/dist/esm/mcp/resources.js +27 -2
- package/dist/esm/mcp/resources.js.map +1 -1
- package/dist/esm/mcp/tasks.d.ts +20 -0
- package/dist/esm/mcp/tasks.d.ts.map +1 -0
- package/dist/esm/mcp/tasks.js +66 -0
- package/dist/esm/mcp/tasks.js.map +1 -0
- package/dist/esm/mcp/tool-profiles.d.ts.map +1 -1
- package/dist/esm/mcp/tool-profiles.js +29 -50
- package/dist/esm/mcp/tool-profiles.js.map +1 -1
- package/dist/esm/mcp/tools.d.ts.map +1 -1
- package/dist/esm/mcp/tools.js +577 -95
- package/dist/esm/mcp/tools.js.map +1 -1
- package/dist/esm/proxy/Gatekeeper.d.ts +29 -0
- package/dist/esm/proxy/Gatekeeper.d.ts.map +1 -1
- package/dist/esm/proxy/Gatekeeper.js +120 -0
- package/dist/esm/proxy/Gatekeeper.js.map +1 -1
- package/dist/esm/subconscious/TheBrainV2.d.ts +50 -1
- package/dist/esm/subconscious/TheBrainV2.d.ts.map +1 -1
- package/dist/esm/subconscious/TheBrainV2.js +149 -8
- package/dist/esm/subconscious/TheBrainV2.js.map +1 -1
- package/dist/esm/utils/PatchMatcher.d.ts +1 -0
- package/dist/esm/utils/PatchMatcher.d.ts.map +1 -1
- package/dist/esm/utils/PatchMatcher.js +4 -4
- package/dist/esm/utils/PatchMatcher.js.map +1 -1
- package/package.json +2 -2
package/dist/esm/mcp/tools.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import path from "path";
|
|
4
|
-
import { execSync, execFileSync,
|
|
4
|
+
import { execSync, execFileSync, spawn } from "child_process";
|
|
5
5
|
import os from "os";
|
|
6
6
|
import axios from "axios";
|
|
7
7
|
import * as ts from "typescript";
|
|
@@ -36,6 +36,7 @@ import { lookupStateHash, storeStateHash } from "../utils/StateHashCache.js";
|
|
|
36
36
|
import { recordReceiptEvent, getReceiptSummary, getLedgerLength } from "../utils/TokenReceipt.js";
|
|
37
37
|
import { distillCommandOutput, buildDistillFooter, readRegion } from "../utils/CommandOutputDistiller.js";
|
|
38
38
|
import { findMatch, reindentReplacement } from "../utils/PatchMatcher.js";
|
|
39
|
+
import { registerTaskHandlers, runAsTask, TASK_CAPABLE_TOOLS } from "./tasks.js";
|
|
39
40
|
import { searchWorkspace, groupSearchResults, parseExtensionFilter } from "../utils/WorkspaceSearch.js";
|
|
40
41
|
import { rankCandidates } from "../utils/LocalPrefilter.js";
|
|
41
42
|
import { checkAlreadySent, recordSent, getLastEmitted } from "../utils/ReadWorkspaceCache.js";
|
|
@@ -44,7 +45,7 @@ import { ledgerKey, checkOutput, recordOutput as recordLedgerOutput } from "../u
|
|
|
44
45
|
import { buildDepGraphMap, bfsDepGraph } from "../utils/DepGraph.js";
|
|
45
46
|
import { getOrExtractSymbols } from "../utils/AstSymbolCache.js";
|
|
46
47
|
import { warmNeighbors, registerSymbolExtractor } from "../utils/SpeculativeWarmer.js";
|
|
47
|
-
import { findAffectedTests, formatAffectedTests } from "../utils/AffectedTests.js";
|
|
48
|
+
import { findAffectedTests, formatAffectedTests, changedFilesFromGit } from "../utils/AffectedTests.js";
|
|
48
49
|
import { mapSqueezedToOriginal, withLineNumbers, sequentialNumbers } from "../utils/LineNumberMap.js";
|
|
49
50
|
import { fileOutline, formatOutline } from "../utils/FileOutline.js";
|
|
50
51
|
import { getCachedSearch, setCachedSearch } from "../utils/SearchCache.js";
|
|
@@ -67,7 +68,7 @@ function isProUser() {
|
|
|
67
68
|
}
|
|
68
69
|
const FREE_TOOLS = new Set([
|
|
69
70
|
// Cache & Memory — the hook that shows instant value
|
|
70
|
-
"smarter_cache", "state_hash_cache", "token_receipt", "search_memory", "store_memory", "get_project_history",
|
|
71
|
+
"smarter_cache", "state_hash_cache", "token_receipt", "search_memory", "store_memory", "downvote_memory", "get_project_history",
|
|
71
72
|
// Token optimization — shows what they're saving
|
|
72
73
|
"token_budget", "squeeze_prompt", "turbosqueeze",
|
|
73
74
|
// Utility — just enough to function
|
|
@@ -92,9 +93,94 @@ const PRO_GATE_MESSAGE = [
|
|
|
92
93
|
"🚀 Get Lemma Pro:",
|
|
93
94
|
" https://lemma.nxus.studio/upgrade",
|
|
94
95
|
].join("\n");
|
|
96
|
+
// ── Elicitation (spec 2026-07-28's recommended pattern for destructive actions) ────
|
|
97
|
+
//
|
|
98
|
+
// Set once by setupToolsHandlers so the standalone handler functions below — which don't
|
|
99
|
+
// otherwise see the Server instance — can ask the connected client to confirm before a
|
|
100
|
+
// mutating tool runs.
|
|
101
|
+
let mcpServerRef = null;
|
|
102
|
+
/**
|
|
103
|
+
* Ask the client to confirm a destructive action before it happens.
|
|
104
|
+
*
|
|
105
|
+
* Mirrors the defensive shape of trySamplingContext() in src/mcp/index.ts: most clients
|
|
106
|
+
* today don't support elicitation, so any failure (unsupported capability, timeout,
|
|
107
|
+
* malformed response) must fall back to "proceed without confirmation" rather than block
|
|
108
|
+
* or error out the tool. This is a courtesy prompt for clients that support it, not a
|
|
109
|
+
* security boundary — the allowlist/path-safety checks each handler already does are that.
|
|
110
|
+
*/
|
|
111
|
+
async function tryElicitConfirmation(summary) {
|
|
112
|
+
if (!mcpServerRef)
|
|
113
|
+
return { supported: false, confirmed: true };
|
|
114
|
+
try {
|
|
115
|
+
const result = await mcpServerRef.elicitInput({
|
|
116
|
+
message: summary,
|
|
117
|
+
requestedSchema: {
|
|
118
|
+
type: "object",
|
|
119
|
+
properties: {
|
|
120
|
+
confirm: {
|
|
121
|
+
type: "boolean",
|
|
122
|
+
title: "Proceed?",
|
|
123
|
+
description: "Confirm this action should be applied.",
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
required: ["confirm"],
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
if (result.action !== "accept") {
|
|
130
|
+
// "decline" or "cancel" — the user (or client policy) said no.
|
|
131
|
+
return { supported: true, confirmed: false };
|
|
132
|
+
}
|
|
133
|
+
const confirmed = result.content?.confirm !== false;
|
|
134
|
+
return { supported: true, confirmed };
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
logWarn("elicitation", "Client does not support elicitation (or the request failed) — proceeding without confirmation");
|
|
138
|
+
return { supported: false, confirmed: true };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Ask the client to pick one of several candidates via a typed (oneOf) elicitation form,
|
|
143
|
+
* instead of the plain confirm/deny shape tryElicitConfirmation uses. Same defensive
|
|
144
|
+
* contract: no support, a decline, or a malformed response all just mean "couldn't
|
|
145
|
+
* resolve it this way" — the caller falls back to its own error/default behavior, never
|
|
146
|
+
* blocks or throws.
|
|
147
|
+
*/
|
|
148
|
+
async function tryElicitChoice(message, options) {
|
|
149
|
+
if (!mcpServerRef || options.length === 0)
|
|
150
|
+
return { supported: false, index: null };
|
|
151
|
+
try {
|
|
152
|
+
const result = await mcpServerRef.elicitInput({
|
|
153
|
+
message,
|
|
154
|
+
requestedSchema: {
|
|
155
|
+
type: "object",
|
|
156
|
+
properties: {
|
|
157
|
+
choice: {
|
|
158
|
+
type: "string",
|
|
159
|
+
title: "Which one?",
|
|
160
|
+
oneOf: options.map((opt, i) => ({ const: String(i), title: opt.title })),
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
required: ["choice"],
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
if (result.action !== "accept" || !result.content) {
|
|
167
|
+
return { supported: true, index: null };
|
|
168
|
+
}
|
|
169
|
+
const idx = Number(result.content.choice);
|
|
170
|
+
if (!Number.isInteger(idx) || idx < 0 || idx >= options.length) {
|
|
171
|
+
return { supported: true, index: null };
|
|
172
|
+
}
|
|
173
|
+
return { supported: true, index: idx };
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
logWarn("elicitation", "Client does not support elicitation (or the request failed) — no choice made");
|
|
177
|
+
return { supported: false, index: null };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
95
180
|
const toolDefinitions = [
|
|
96
181
|
{
|
|
97
182
|
name: "scrub_privacy",
|
|
183
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
98
184
|
description: "Mask sensitive data (PII, API Keys, Credentials) from a text block using Lemma's Privacy Firewall.",
|
|
99
185
|
inputSchema: {
|
|
100
186
|
type: "object",
|
|
@@ -106,7 +192,8 @@ const toolDefinitions = [
|
|
|
106
192
|
},
|
|
107
193
|
{
|
|
108
194
|
name: "search_memory",
|
|
109
|
-
|
|
195
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
196
|
+
description: "Search Lemma's semantic memory (The Brain) before investigating something from scratch — retrieves past solutions, fixes, and context from ALL your projects globally. Results are split into 'fresh' (safe to reuse — either untracked general knowledge, or every file it depended on still hashes the same) and 'stale' (a similar question was answered before, but a tracked file changed since — re-verify against current state before reusing). Never treat a stale result as current. If a returned memory turns out wrong once acted on, call downvote_memory with its id.",
|
|
110
197
|
inputSchema: {
|
|
111
198
|
type: "object",
|
|
112
199
|
properties: {
|
|
@@ -117,12 +204,17 @@ const toolDefinitions = [
|
|
|
117
204
|
description: "Similarity floor (0.0-1.0) a memory must clear to be returned. Lowering this surfaces loosely related memories that are usually noise — leave it alone unless a known-relevant memory is being filtered out.",
|
|
118
205
|
default: 0.75,
|
|
119
206
|
},
|
|
207
|
+
domain: {
|
|
208
|
+
type: "string",
|
|
209
|
+
description: "Prefer memories tagged with this domain (e.g. 'auth', 'billing' — matches cognitive_map domains). Soft preference, not a filter: an untagged or cross-domain memory can still be the best match.",
|
|
210
|
+
},
|
|
120
211
|
},
|
|
121
212
|
required: ["query"],
|
|
122
213
|
},
|
|
123
214
|
},
|
|
124
215
|
{
|
|
125
216
|
name: "store_memory",
|
|
217
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
126
218
|
description: "Persist a technical solution, bug fix, architecture decision, or key fact into Lemma's Brain — so future questions on the same topic (even phrased differently) don't require re-investigating the repo. Pass filePaths for anything derived from specific files (e.g. 'where is X implemented', 'how does Y work') so the memory auto-invalidates the moment those files change, instead of risking a stale answer being reused. If the answer is really about one function/class rather than the whole file, pass `symbols` instead (or in addition) so an unrelated edit elsewhere in that file doesn't stale it out. Pass outcome='failed' for an approach that was tried and did NOT work — that's just as worth remembering as a fix, so the Brain can warn 'already tried, didn't work' instead of only ever suggesting reuse.",
|
|
127
219
|
inputSchema: {
|
|
128
220
|
type: "object",
|
|
@@ -168,12 +260,29 @@ const toolDefinitions = [
|
|
|
168
260
|
},
|
|
169
261
|
description: "Split `response` into independently-verifiable sub-claims when it makes more than one assertion about different parts of the code. Each claim tracks its own filePaths/symbols, so one claim going stale (e.g. one function changed) doesn't discard the others that are still true. Skip this for a single-fact response — plain filePaths/symbols above already covers that case.",
|
|
170
262
|
},
|
|
263
|
+
domain: {
|
|
264
|
+
type: "string",
|
|
265
|
+
description: "Tag this memory with a domain (e.g. 'auth', 'billing' — matches cognitive_map domains) so domain-scoped searches prefer it. Optional: when omitted and filePaths is set, Lemma auto-derives a domain from the first file via cognitive_map (or a lightweight path heuristic if the map was never built).",
|
|
266
|
+
},
|
|
171
267
|
},
|
|
172
268
|
required: ["query", "response"],
|
|
173
269
|
},
|
|
174
270
|
},
|
|
271
|
+
{
|
|
272
|
+
name: "downvote_memory",
|
|
273
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
274
|
+
description: "Tell the Brain a specific memory from a prior search_memory result was wrong or misleading in practice — not just irrelevant to skip, but actually acted on and it didn't hold up. Lowers that memory's future ranking and moves it toward eviction sooner, without deleting it (it may still be right for a different query later). Use the `id` from the search_memory result you're downvoting.",
|
|
275
|
+
inputSchema: {
|
|
276
|
+
type: "object",
|
|
277
|
+
properties: {
|
|
278
|
+
id: { type: "string", description: "The memory's id, from a prior search_memory result." },
|
|
279
|
+
},
|
|
280
|
+
required: ["id"],
|
|
281
|
+
},
|
|
282
|
+
},
|
|
175
283
|
{
|
|
176
284
|
name: "get_routing_advice",
|
|
285
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
177
286
|
description: "Analyzes a prompt and suggests the best model based on Lemma's Complexity Router.",
|
|
178
287
|
inputSchema: {
|
|
179
288
|
type: "object",
|
|
@@ -183,9 +292,20 @@ const toolDefinitions = [
|
|
|
183
292
|
},
|
|
184
293
|
required: ["prompt"],
|
|
185
294
|
},
|
|
295
|
+
outputSchema: {
|
|
296
|
+
type: "object",
|
|
297
|
+
properties: {
|
|
298
|
+
recommendedModel: { type: "string", description: "The model the router recommends" },
|
|
299
|
+
complexity: { type: "string", enum: ["low", "high"], description: "Estimated complexity of the prompt" },
|
|
300
|
+
intendedModel: { type: ["string", "null"], description: "The model passed in intended_model, or null if omitted" },
|
|
301
|
+
switched: { type: "boolean", description: "true if recommendedModel differs from intendedModel" },
|
|
302
|
+
},
|
|
303
|
+
required: ["recommendedModel", "complexity", "switched"],
|
|
304
|
+
},
|
|
186
305
|
},
|
|
187
306
|
{
|
|
188
307
|
name: "auto_heal",
|
|
308
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
189
309
|
description: "Diagnose and auto-heal the latest local server crash registered in Lemma's context logs.",
|
|
190
310
|
inputSchema: {
|
|
191
311
|
type: "object",
|
|
@@ -196,6 +316,7 @@ const toolDefinitions = [
|
|
|
196
316
|
},
|
|
197
317
|
{
|
|
198
318
|
name: "read_workspace_file",
|
|
319
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
199
320
|
description: "Read a file inside the workspace. Compresses comments/whitespace and scrubs API keys. Every line carries its REAL line number in the original file (compression removes lines, it never renumbers them), so you can cite file:line and patch precisely without re-reading. The header lists which imports were compressed away. Use offset/limit to read a line range of a large file. If this exact view was already read this session and hasn't changed on disk, returns an UNCHANGED marker plus a symbol outline. If it changed only slightly, returns a CHANGED marker with a unified diff to apply to the copy you already hold. Either way, pass force:true to get the full file when it is no longer in your context.",
|
|
200
321
|
inputSchema: {
|
|
201
322
|
type: "object",
|
|
@@ -212,6 +333,7 @@ const toolDefinitions = [
|
|
|
212
333
|
},
|
|
213
334
|
{
|
|
214
335
|
name: "write_workspace_file",
|
|
336
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
215
337
|
description: "Write full contents to a file inside the workspace. Creates parent directories automatically.",
|
|
216
338
|
inputSchema: {
|
|
217
339
|
type: "object",
|
|
@@ -224,6 +346,7 @@ const toolDefinitions = [
|
|
|
224
346
|
},
|
|
225
347
|
{
|
|
226
348
|
name: "create_workspace_file",
|
|
349
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
227
350
|
description: "Create a NEW file in the workspace. Fails immediately if the file already exists — use write_workspace_file or apply_workspace_patch to modify existing files. Creates parent directories automatically. Returns a minimal token-free ACK: no file content is echoed back, saving provider output tokens.",
|
|
228
351
|
inputSchema: {
|
|
229
352
|
type: "object",
|
|
@@ -236,6 +359,7 @@ const toolDefinitions = [
|
|
|
236
359
|
},
|
|
237
360
|
{
|
|
238
361
|
name: "apply_workspace_patch",
|
|
362
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
239
363
|
description: "Apply a smart search-and-replace patch to an existing file. Falls back through whitespace-normalized, indent-anchored, and fuzzy matching if the exact block isn't found — so a searchContent built from a compacted read still applies. Safe against duplicate matches. On total failure, the error includes a diff against the closest candidate block.",
|
|
240
364
|
inputSchema: {
|
|
241
365
|
type: "object",
|
|
@@ -249,6 +373,8 @@ const toolDefinitions = [
|
|
|
249
373
|
},
|
|
250
374
|
{
|
|
251
375
|
name: "run_workspace_command",
|
|
376
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
377
|
+
execution: { taskSupport: "optional" },
|
|
252
378
|
description: "Execute a bash command in the workspace root. Default timeout 120s — raise it with timeoutMs for a full test or build run (max 600s). Captures both stdout and stderr, and returns whatever was produced even if the command times out. Long test/build/lint output is distilled deterministically — for jest, vitest and tsc it returns the failing tests or diagnostics with their locations and reasons, dropping code frames and node_modules stack frames; anything else falls back to head/tail plus error-matching lines. The complete output is always stored first and the reply carries a handle: nothing is lost, use output_region to retrieve any part verbatim. Pass raw:true to skip distillation.",
|
|
253
379
|
inputSchema: {
|
|
254
380
|
type: "object",
|
|
@@ -270,6 +396,7 @@ const toolDefinitions = [
|
|
|
270
396
|
},
|
|
271
397
|
{
|
|
272
398
|
name: "output_region",
|
|
399
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
273
400
|
description: "Retrieve any part of a command output previously stored by run_workspace_command, verbatim. This is what makes distillation lossless — use it when the distilled view isn't enough. Pass all:true for the whole output, section for one named block (a failing test title or a file path, as listed in the distilled view), or startLine/endLine for a range.",
|
|
274
401
|
inputSchema: {
|
|
275
402
|
type: "object",
|
|
@@ -285,6 +412,7 @@ const toolDefinitions = [
|
|
|
285
412
|
},
|
|
286
413
|
{
|
|
287
414
|
name: "list_workspace_dir",
|
|
415
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
288
416
|
description: "List files and subdirectories recursively to navigate the repository structure.",
|
|
289
417
|
inputSchema: {
|
|
290
418
|
type: "object",
|
|
@@ -296,6 +424,7 @@ const toolDefinitions = [
|
|
|
296
424
|
},
|
|
297
425
|
{
|
|
298
426
|
name: "search_workspace",
|
|
427
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
299
428
|
description: "Perform a fast local text search (grep) across all files in the project workspace. Respects .gitignore, skips binaries and files over 500KB, groups results by file (capped per file), and stops at maxResults. For code files (.ts/.tsx/.js/.jsx), matches inside the same function/class/method are collapsed into one entry with its signature and line range instead of raw duplicate lines. Repeating an identical query while the workspace is unchanged returns the cached result instantly instead of re-scanning.",
|
|
300
429
|
inputSchema: {
|
|
301
430
|
type: "object",
|
|
@@ -309,6 +438,7 @@ const toolDefinitions = [
|
|
|
309
438
|
},
|
|
310
439
|
{
|
|
311
440
|
name: "squeeze_prompt",
|
|
441
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
312
442
|
description: "Compress code blocks, comments, and boilerplate in any prompt. Saves up to 80% tokens.",
|
|
313
443
|
inputSchema: {
|
|
314
444
|
type: "object",
|
|
@@ -321,6 +451,7 @@ const toolDefinitions = [
|
|
|
321
451
|
},
|
|
322
452
|
{
|
|
323
453
|
name: "get_project_onboarding",
|
|
454
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
324
455
|
description: "Fetch dynamic architectural and stack overview of the current project in markdown.",
|
|
325
456
|
inputSchema: {
|
|
326
457
|
type: "object",
|
|
@@ -329,6 +460,7 @@ const toolDefinitions = [
|
|
|
329
460
|
},
|
|
330
461
|
{
|
|
331
462
|
name: "get_project_history",
|
|
463
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
332
464
|
description: "Answers 'what have we done in this project': merges recent git commits, session checkpoints, and The Brain's memories (decisions, fixes, prior PR reviews) for the current project into one narrative. Use this instead of piecing the same picture together from search_memory + git log + reading checkpoint files separately.",
|
|
333
465
|
inputSchema: {
|
|
334
466
|
type: "object",
|
|
@@ -339,6 +471,7 @@ const toolDefinitions = [
|
|
|
339
471
|
},
|
|
340
472
|
{
|
|
341
473
|
name: "get_ast_hologram",
|
|
474
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
342
475
|
description: "Generate a dense, token-efficient Holographic AST Map of the workspace using the TypeScript compiler. Returns structured JSON of all exported symbols with file paths and line numbers.",
|
|
343
476
|
inputSchema: {
|
|
344
477
|
type: "object",
|
|
@@ -355,6 +488,7 @@ const toolDefinitions = [
|
|
|
355
488
|
},
|
|
356
489
|
{
|
|
357
490
|
name: "validate_patch_sandbox",
|
|
491
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
358
492
|
description: "Validate proposed code in isolated sandbox via tsc + syntax check before applying.",
|
|
359
493
|
inputSchema: {
|
|
360
494
|
type: "object",
|
|
@@ -367,6 +501,7 @@ const toolDefinitions = [
|
|
|
367
501
|
},
|
|
368
502
|
{
|
|
369
503
|
name: "query_hybrid_consensus",
|
|
504
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
370
505
|
description: "Search The Brain before reasoning. Brain HIT returns cached answer instantly. Brain MISS proceeds to cloud.",
|
|
371
506
|
inputSchema: {
|
|
372
507
|
type: "object",
|
|
@@ -380,6 +515,7 @@ const toolDefinitions = [
|
|
|
380
515
|
},
|
|
381
516
|
{
|
|
382
517
|
name: "get_telepathic_hints",
|
|
518
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
383
519
|
description: "Surface relevant past solutions from The Brain based on the active file path.",
|
|
384
520
|
inputSchema: {
|
|
385
521
|
type: "object",
|
|
@@ -392,6 +528,7 @@ const toolDefinitions = [
|
|
|
392
528
|
},
|
|
393
529
|
{
|
|
394
530
|
name: "summarize_long_text",
|
|
531
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
395
532
|
description: "Summarize long text locally using Ollama. Compresses verbose content into concise summaries, saving context window for subsequent turns.",
|
|
396
533
|
inputSchema: {
|
|
397
534
|
type: "object",
|
|
@@ -404,6 +541,7 @@ const toolDefinitions = [
|
|
|
404
541
|
},
|
|
405
542
|
{
|
|
406
543
|
name: "prune_conversation_history",
|
|
544
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
407
545
|
description: "Compress conversation history by decimating old turns and compacting large code blocks in historical messages. Keeps recent context intact. Refuses to mutate anything by default because pruning invalidates the prompt-cache prefix, which is usually a net loss — pass force:true to actually apply it, or call without force to just get the estimated cache-invalidation cost back.",
|
|
408
546
|
inputSchema: {
|
|
409
547
|
type: "object",
|
|
@@ -424,6 +562,7 @@ const toolDefinitions = [
|
|
|
424
562
|
},
|
|
425
563
|
{
|
|
426
564
|
name: "diff_only",
|
|
565
|
+
annotations: { readOnlyHint: true, idempotentHint: false, openWorldHint: false },
|
|
427
566
|
description: "Compute a compact line-by-line diff between the last known state of a file and its current content. Tracks file state in memory per session.",
|
|
428
567
|
inputSchema: {
|
|
429
568
|
type: "object",
|
|
@@ -436,6 +575,7 @@ const toolDefinitions = [
|
|
|
436
575
|
},
|
|
437
576
|
{
|
|
438
577
|
name: "batch_tool_calls",
|
|
578
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
439
579
|
description: "Execute multiple tools in a single MCP call. Accepts an array of { tool, args } and runs them in parallel via Promise.all. Results include per-tool success/error status.",
|
|
440
580
|
inputSchema: {
|
|
441
581
|
type: "object",
|
|
@@ -458,6 +598,7 @@ const toolDefinitions = [
|
|
|
458
598
|
},
|
|
459
599
|
{
|
|
460
600
|
name: "turbosqueeze",
|
|
601
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
461
602
|
description: "COMPRIME prompts hasta 90%. Elimina comentarios, imports redundantes, whitespace excesivo, y compacta JSON. Usar ANTES de enviar código largo al LLM para maximizar ahorro de tokens.",
|
|
462
603
|
inputSchema: {
|
|
463
604
|
type: "object",
|
|
@@ -472,6 +613,7 @@ const toolDefinitions = [
|
|
|
472
613
|
},
|
|
473
614
|
{
|
|
474
615
|
name: "get_symbol_surgical_context",
|
|
616
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
475
617
|
description: "Extrae quirúrgicamente la implementación de un símbolo específico (clase, función, interfaz) y las firmas (pero no la implementación completa) de todas sus dependencias locales del workspace. Ahorra hasta 90% de tokens.",
|
|
476
618
|
inputSchema: {
|
|
477
619
|
type: "object",
|
|
@@ -484,6 +626,7 @@ const toolDefinitions = [
|
|
|
484
626
|
},
|
|
485
627
|
{
|
|
486
628
|
name: "wormhole_squeeze",
|
|
629
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
487
630
|
description: "Comprime código a formato WORMHOLE súper denso usando tokens de palabra clave y abreviación de variables reversible. Ideal para alimentar contextos de lectura al LLM.",
|
|
488
631
|
inputSchema: {
|
|
489
632
|
type: "object",
|
|
@@ -495,6 +638,7 @@ const toolDefinitions = [
|
|
|
495
638
|
},
|
|
496
639
|
{
|
|
497
640
|
name: "generate_executive_roi_report",
|
|
641
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
498
642
|
description: "Genera un reporte ejecutivo en formato Markdown de Retorno de Inversión (ROI), dinero ahorrado en API, horas de desarrollo recuperadas y proyecciones de escala para la gerencia.",
|
|
499
643
|
inputSchema: {
|
|
500
644
|
type: "object",
|
|
@@ -507,6 +651,7 @@ const toolDefinitions = [
|
|
|
507
651
|
},
|
|
508
652
|
{
|
|
509
653
|
name: "surgical_ast_insert",
|
|
654
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
510
655
|
description: "Inserta quirúrgicamente código (métodos, propiedades, funciones) en una clase, interfaz o ámbito de archivo de TypeScript utilizando el AST Compiler. Evita escribir diffs de búsqueda y reemplazo grandes o reescribir todo el archivo. Ahorra 95% de tokens de salida.",
|
|
511
656
|
inputSchema: {
|
|
512
657
|
type: "object",
|
|
@@ -522,6 +667,7 @@ const toolDefinitions = [
|
|
|
522
667
|
},
|
|
523
668
|
{
|
|
524
669
|
name: "local_semantic_autofix",
|
|
670
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
525
671
|
description: "Intenta resolver errores de compilación o ejecución localmente consultando The Brain por tracebacks/mensajes similares. Si hay coincidencia, devuelve la solución/parche instantáneamente sin gastar tokens de LLM.",
|
|
526
672
|
inputSchema: {
|
|
527
673
|
type: "object",
|
|
@@ -533,6 +679,7 @@ const toolDefinitions = [
|
|
|
533
679
|
},
|
|
534
680
|
{
|
|
535
681
|
name: "compress_context",
|
|
682
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
536
683
|
description: "Comprime el historial de conversación: resume turns antiguos vía Ollama, mantiene solo los últimos N intactos. Reduce contexto 70-80%. USAR antes de cada turno extendido.",
|
|
537
684
|
inputSchema: {
|
|
538
685
|
type: "object",
|
|
@@ -549,6 +696,7 @@ const toolDefinitions = [
|
|
|
549
696
|
},
|
|
550
697
|
{
|
|
551
698
|
name: "smarter_cache",
|
|
699
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
552
700
|
description: "Cache predictivo con threshold bajo (75%). Busca en The Brain antes de cualquier razonamiento. Si hay hit >= 75%, devuelve respuesta instantánea sin gastar tokens en LLM.",
|
|
553
701
|
inputSchema: {
|
|
554
702
|
type: "object",
|
|
@@ -559,9 +707,25 @@ const toolDefinitions = [
|
|
|
559
707
|
},
|
|
560
708
|
required: ["query"],
|
|
561
709
|
},
|
|
710
|
+
outputSchema: {
|
|
711
|
+
type: "object",
|
|
712
|
+
properties: {
|
|
713
|
+
hit: { type: "boolean", description: "true si hubo un hit por encima del threshold" },
|
|
714
|
+
source: { type: "string", enum: ["lemma-brain", "llm-call-required", "cache-unavailable"], description: "De dónde salió (o no) la respuesta" },
|
|
715
|
+
similarity: { type: "number", description: "Similitud del mejor match (0.0-1.0). Ausente en la rama de error." },
|
|
716
|
+
threshold: { type: "number", description: "Threshold usado. Ausente en la rama de error." },
|
|
717
|
+
answer: { type: ["string", "null"], description: "Respuesta cacheada si hit=true; null si no. Ausente en la rama de error." },
|
|
718
|
+
tokensSaved: { type: "number", description: "Solo presente cuando hit=true" },
|
|
719
|
+
tokensSavedFormatted: { type: "string", description: "Solo presente cuando hit=true" },
|
|
720
|
+
hint: { type: "string", description: "Sugerencia de siguiente paso; presente en miss y en error" },
|
|
721
|
+
error: { type: "string", description: "Mensaje de error; solo presente si el brain local falló" },
|
|
722
|
+
},
|
|
723
|
+
required: ["hit", "source"],
|
|
724
|
+
},
|
|
562
725
|
},
|
|
563
726
|
{
|
|
564
727
|
name: "state_hash_cache",
|
|
728
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
565
729
|
description: "Cache de razonamiento con invalidación EXACTA (no probabilística): la respuesta queda atada al sha256 del contenido de los archivos de los que depende. Si esos archivos no cambiaron, el hit es matemáticamente válido, no una adivinanza por similitud. Usa action='lookup' antes de razonar sobre una pregunta que dependa de archivos específicos, y action='store' después de responder para cachearla.",
|
|
566
730
|
inputSchema: {
|
|
567
731
|
type: "object",
|
|
@@ -573,9 +737,24 @@ const toolDefinitions = [
|
|
|
573
737
|
},
|
|
574
738
|
required: ["action", "query", "filePaths"],
|
|
575
739
|
},
|
|
740
|
+
outputSchema: {
|
|
741
|
+
type: "object",
|
|
742
|
+
properties: {
|
|
743
|
+
status: { type: "string", enum: ["hit", "miss", "stored"], description: "Resultado de lookup (hit/miss) o de store (stored)" },
|
|
744
|
+
answer: { type: "string", description: "Solo presente cuando status='hit'" },
|
|
745
|
+
note: { type: "string", description: "Solo presente cuando status='hit'" },
|
|
746
|
+
tokensSaved: { type: "number", description: "Solo presente cuando status='hit'" },
|
|
747
|
+
reason: { type: "string", description: "Solo presente cuando status='miss'" },
|
|
748
|
+
hint: { type: "string", description: "Solo presente cuando status='miss'" },
|
|
749
|
+
id: { type: "string", description: "Solo presente cuando status='stored'" },
|
|
750
|
+
filesTracked: { type: "array", items: { type: "string" }, description: "Solo presente cuando status='stored'" },
|
|
751
|
+
},
|
|
752
|
+
required: ["status"],
|
|
753
|
+
},
|
|
576
754
|
},
|
|
577
755
|
{
|
|
578
756
|
name: "token_receipt",
|
|
757
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
579
758
|
description: "Recibo auditable de la sesión: de dónde vino cada respuesta (cache exacto, cache semántico, lectura de archivo, o razonamiento fresco). No es una estadística de 'ahorro' — es una bitácora verificable para que el usuario pueda comprobar que no se le está mintiendo con un cache hit inventado. Llamar con action='summary' para ver el reporte.",
|
|
580
759
|
inputSchema: {
|
|
581
760
|
type: "object",
|
|
@@ -584,9 +763,42 @@ const toolDefinitions = [
|
|
|
584
763
|
limit: { type: "number", description: "Cuántos eventos recientes incluir en el detalle", default: 20 },
|
|
585
764
|
},
|
|
586
765
|
},
|
|
766
|
+
outputSchema: {
|
|
767
|
+
type: "object",
|
|
768
|
+
properties: {
|
|
769
|
+
totalEvents: { type: "number", description: "Total de eventos registrados en la sesión" },
|
|
770
|
+
byType: {
|
|
771
|
+
type: "object",
|
|
772
|
+
properties: {
|
|
773
|
+
exact_cache_hit: { type: "number" },
|
|
774
|
+
semantic_cache_hit: { type: "number" },
|
|
775
|
+
file_read: { type: "number" },
|
|
776
|
+
reasoning: { type: "number" },
|
|
777
|
+
tool_call: { type: "number" },
|
|
778
|
+
},
|
|
779
|
+
description: "Conteo de eventos por tipo de origen",
|
|
780
|
+
},
|
|
781
|
+
recentEvents: {
|
|
782
|
+
type: "array",
|
|
783
|
+
items: {
|
|
784
|
+
type: "object",
|
|
785
|
+
properties: {
|
|
786
|
+
type: { type: "string" },
|
|
787
|
+
label: { type: "string" },
|
|
788
|
+
timestamp: { type: "number" },
|
|
789
|
+
meta: { type: "object" },
|
|
790
|
+
},
|
|
791
|
+
},
|
|
792
|
+
description: "Últimos `limit` eventos, en orden cronológico",
|
|
793
|
+
},
|
|
794
|
+
note: { type: "string" },
|
|
795
|
+
},
|
|
796
|
+
required: ["totalEvents", "byType", "recentEvents"],
|
|
797
|
+
},
|
|
587
798
|
},
|
|
588
799
|
{
|
|
589
800
|
name: "token_budget",
|
|
801
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
590
802
|
description: "Muestra el dashboard de consumo: multiplicador actual de membresía, tokens ahorrados, tools usadas, y recomendaciones para optimizar más. Consultar periódicamente para auto-regular consumo.",
|
|
591
803
|
inputSchema: {
|
|
592
804
|
type: "object",
|
|
@@ -595,6 +807,7 @@ const toolDefinitions = [
|
|
|
595
807
|
},
|
|
596
808
|
{
|
|
597
809
|
name: "entropy_score",
|
|
810
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
598
811
|
description: "Calcula la entropía matemática (complejidad) de uno o más archivos usando el TypeScript Compiler API. Sin LLM. Sin tokens. Devuelve: cyclomatic complexity, nesting depth, ratio de 'any', tamaño de funciones, y un score compuesto 0-100 (0=limpio, 100=caos puro). Usa esto para identificar qué archivos necesitan refactor ANTES de tocarlos.",
|
|
599
812
|
inputSchema: {
|
|
600
813
|
type: "object",
|
|
@@ -606,6 +819,7 @@ const toolDefinitions = [
|
|
|
606
819
|
},
|
|
607
820
|
{
|
|
608
821
|
name: "coupling_radar",
|
|
822
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
609
823
|
description: "Construye un grafo de acoplamiento entre módulos analizando imports/exports con el TypeScript Compiler. Sin LLM. Sin tokens. Detecta: ciclos de dependencia, módulos 'dios' (importados por todo), islas muertas (nadie los importa), y fan-in/fan-out por módulo. Esencial antes de refactors grandes.",
|
|
610
824
|
inputSchema: {
|
|
611
825
|
type: "object",
|
|
@@ -617,6 +831,7 @@ const toolDefinitions = [
|
|
|
617
831
|
},
|
|
618
832
|
{
|
|
619
833
|
name: "pattern_fossil",
|
|
834
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
620
835
|
description: "Detecta código zombie: patrones que ya fueron reemplazados en la mayoría del codebase pero siguen vivos en archivos viejos. Sin LLM. Sin tokens. Detecta: callbacks vs async/await, var vs let/const, require() vs import, any vs generics. Devuelve porcentaje de adopción del patrón nuevo vs. los fósiles que quedan.",
|
|
621
836
|
inputSchema: {
|
|
622
837
|
type: "object",
|
|
@@ -627,6 +842,7 @@ const toolDefinitions = [
|
|
|
627
842
|
},
|
|
628
843
|
{
|
|
629
844
|
name: "git_heatmap_risk",
|
|
845
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
630
846
|
description: "Analiza el historial de git para identificar zonas de alto riesgo. Sin LLM. Sin tokens. Calcula: churn rate (frecuencia de cambios), co-edición oculta (archivos que siempre cambian juntos = acoplamiento implícito), y un risk score compuesto por archivo. Detecta los archivos que estadísticamente tienen más probabilidad de tener un bug.",
|
|
631
847
|
inputSchema: {
|
|
632
848
|
type: "object",
|
|
@@ -638,6 +854,7 @@ const toolDefinitions = [
|
|
|
638
854
|
},
|
|
639
855
|
{
|
|
640
856
|
name: "precrime_static",
|
|
857
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
641
858
|
description: "MINORITY REPORT para tu código. Combina entropy_score + coupling_radar + git_heatmap_risk en un predictor de riesgo compuesto. Sin LLM. Sin tokens. Devuelve un ranking de archivos y funciones con mayor probabilidad de causar un bug, con justificación matemática de cada factor. Úsalo antes de un deploy o un PR review.",
|
|
642
859
|
inputSchema: {
|
|
643
860
|
type: "object",
|
|
@@ -650,6 +867,7 @@ const toolDefinitions = [
|
|
|
650
867
|
},
|
|
651
868
|
{
|
|
652
869
|
name: "semantic_dedup_guard",
|
|
870
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
653
871
|
description: "Firewall anti-redundancia para The Brain. Antes de hacer store_memory, pasa el contenido por aquí. Consulta ChromaDB localmente sin gastar tokens. Si hay un hit >= 92% → rechaza el store y devuelve el duplicado. Si 75-91% → advierte y muestra el similar. Mantiene The Brain denso y limpio.",
|
|
654
872
|
inputSchema: {
|
|
655
873
|
type: "object",
|
|
@@ -663,6 +881,7 @@ const toolDefinitions = [
|
|
|
663
881
|
},
|
|
664
882
|
{
|
|
665
883
|
name: "dead_export_necromancer",
|
|
884
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
666
885
|
description: "Resucita el código muerto. Usa el TypeScript Compiler para mapear TODOS los exports del workspace y los cruza contra TODOS los imports. Lo que se exporta pero nadie importa = código zombie que está inflando tu contexto y desperdiciando tokens. Sin LLM. Sin tokens. Devuelve lista de exports muertos con estimación de tokens desperdiciados.",
|
|
667
886
|
inputSchema: {
|
|
668
887
|
type: "object",
|
|
@@ -673,6 +892,7 @@ const toolDefinitions = [
|
|
|
673
892
|
},
|
|
674
893
|
{
|
|
675
894
|
name: "review_diff",
|
|
895
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
676
896
|
description: "Analyze a raw git diff locally for regressions, security issues, code quality, and static analysis. Zero-cost static analysis using local TypeScript compiler API — no LLM calls. Returns score, verdict, and detailed findings with suggestions.",
|
|
677
897
|
inputSchema: {
|
|
678
898
|
type: "object",
|
|
@@ -686,6 +906,7 @@ const toolDefinitions = [
|
|
|
686
906
|
},
|
|
687
907
|
{
|
|
688
908
|
name: "review_pr",
|
|
909
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
689
910
|
description: "Analyze a Pull Request via GitHub, Azure DevOps, or GitLab API. Detects regressions, security issues, code quality problems, and breaking changes. Posts results as PR comments and can auto-approve if quality threshold is met. Pro license required.",
|
|
690
911
|
inputSchema: {
|
|
691
912
|
type: "object",
|
|
@@ -702,6 +923,7 @@ const toolDefinitions = [
|
|
|
702
923
|
},
|
|
703
924
|
{
|
|
704
925
|
name: "pr_status",
|
|
926
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
705
927
|
description: "Get the status and detailed results of a previously reviewed PR. Returns score, verdict, findings, and approval status.",
|
|
706
928
|
inputSchema: {
|
|
707
929
|
type: "object",
|
|
@@ -715,6 +937,7 @@ const toolDefinitions = [
|
|
|
715
937
|
},
|
|
716
938
|
{
|
|
717
939
|
name: "generate_pr_workflow",
|
|
940
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
718
941
|
description: "Generate a CI/CD workflow file (GitHub Actions or Azure Pipelines) that runs Lemma PR Review on every pull request. Writes the file to the workspace.",
|
|
719
942
|
inputSchema: {
|
|
720
943
|
type: "object",
|
|
@@ -729,6 +952,7 @@ const toolDefinitions = [
|
|
|
729
952
|
},
|
|
730
953
|
{
|
|
731
954
|
name: "depgraph",
|
|
955
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
732
956
|
description: "Builds a real-time dependency graph for any file in the workspace using the TypeScript Compiler API. Shows what a file imports, what imports it (reverse deps), and what it exports. Zero LLM calls. Essential before any refactor to understand blast radius.",
|
|
733
957
|
inputSchema: {
|
|
734
958
|
type: "object",
|
|
@@ -745,6 +969,7 @@ const toolDefinitions = [
|
|
|
745
969
|
},
|
|
746
970
|
{
|
|
747
971
|
name: "affected_tests",
|
|
972
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
748
973
|
description: "Given the current diff (or an explicit file list), returns only the test files that transitively import the changed code, plus the exact command to run just those. Uses the TypeScript import graph — zero LLM calls. Run this instead of the full suite after an edit: fewer minutes, and far less test output in context. Reports honestly when the full suite is the right answer (unbounded change, runtime-only coupling, most of the suite affected).",
|
|
749
974
|
inputSchema: {
|
|
750
975
|
type: "object",
|
|
@@ -753,9 +978,24 @@ const toolDefinitions = [
|
|
|
753
978
|
baseRef: { type: "string", description: "Additional git ref to diff against, e.g. 'main'. The working tree is always included." },
|
|
754
979
|
},
|
|
755
980
|
},
|
|
981
|
+
outputSchema: {
|
|
982
|
+
type: "object",
|
|
983
|
+
properties: {
|
|
984
|
+
changed: { type: "array", items: { type: "string" }, description: "Files the diff touched, repo-relative" },
|
|
985
|
+
directTests: { type: "array", items: { type: "string" }, description: "Changed files that are themselves tests" },
|
|
986
|
+
affected: { type: "array", items: { type: "string" }, description: "Test files that transitively import a changed file" },
|
|
987
|
+
totalTests: { type: "number", description: "Total test files in the repo" },
|
|
988
|
+
runner: { type: "string", enum: ["jest", "vitest", "unknown"] },
|
|
989
|
+
command: { type: ["string", "null"], description: "Command to run just the affected tests, or null if the full suite is recommended" },
|
|
990
|
+
fullSuiteReason: { type: ["string", "null"], description: "Why the full suite is recommended instead, when command is null" },
|
|
991
|
+
ungraphed: { type: "array", items: { type: "string" }, description: "Changed files absent from the import graph — blast radius unknown" },
|
|
992
|
+
},
|
|
993
|
+
required: ["changed", "directTests", "affected", "totalTests", "runner", "command", "fullSuiteReason", "ungraphed"],
|
|
994
|
+
},
|
|
756
995
|
},
|
|
757
996
|
{
|
|
758
997
|
name: "refactor",
|
|
998
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
759
999
|
description: "Declarative multi-file codemod engine. Rename symbols across the entire workspace or move files updating all imports. Uses TypeScript Compiler API — zero LLM calls. Supports dry-run diff preview and post-refactor tsc verification.",
|
|
760
1000
|
inputSchema: {
|
|
761
1001
|
type: "object",
|
|
@@ -773,6 +1013,7 @@ const toolDefinitions = [
|
|
|
773
1013
|
},
|
|
774
1014
|
{
|
|
775
1015
|
name: "smart_file_slice",
|
|
1016
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
776
1017
|
description: "Read only the lines semánticamente relevantes around a search query in a file, avoiding sending the entire file context. Returns lines around the matches.",
|
|
777
1018
|
inputSchema: {
|
|
778
1019
|
type: "object",
|
|
@@ -786,6 +1027,7 @@ const toolDefinitions = [
|
|
|
786
1027
|
},
|
|
787
1028
|
{
|
|
788
1029
|
name: "test_oracle",
|
|
1030
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
789
1031
|
description: "Execute unit tests locally and output ONLY the failing tests and compressed stack traces, keeping context clean.",
|
|
790
1032
|
inputSchema: {
|
|
791
1033
|
type: "object",
|
|
@@ -796,6 +1038,7 @@ const toolDefinitions = [
|
|
|
796
1038
|
},
|
|
797
1039
|
{
|
|
798
1040
|
name: "schema_extract",
|
|
1041
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
799
1042
|
description: "Extract high-level schemas, Zod types, interfaces, or database models from a file using the TypeScript AST, removing all implementation code.",
|
|
800
1043
|
inputSchema: {
|
|
801
1044
|
type: "object",
|
|
@@ -807,6 +1050,7 @@ const toolDefinitions = [
|
|
|
807
1050
|
},
|
|
808
1051
|
{
|
|
809
1052
|
name: "changelog_auto",
|
|
1053
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
810
1054
|
description: "Generate a clean, token-efficient changelog summary from git logs based on Conventional Commits.",
|
|
811
1055
|
inputSchema: {
|
|
812
1056
|
type: "object",
|
|
@@ -817,6 +1061,7 @@ const toolDefinitions = [
|
|
|
817
1061
|
},
|
|
818
1062
|
{
|
|
819
1063
|
name: "spec_to_stub",
|
|
1064
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
820
1065
|
description: "Parse a TypeScript interface/type definition from a file and output a template mock/stub object configuration using pure AST analysis.",
|
|
821
1066
|
inputSchema: {
|
|
822
1067
|
type: "object",
|
|
@@ -829,6 +1074,7 @@ const toolDefinitions = [
|
|
|
829
1074
|
},
|
|
830
1075
|
{
|
|
831
1076
|
name: "env_snapshot",
|
|
1077
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
832
1078
|
description: "Capture environmental state like Node version, dependencies in package.json, and variables declared in .env files (hiding actual values/credentials).",
|
|
833
1079
|
inputSchema: {
|
|
834
1080
|
type: "object",
|
|
@@ -837,6 +1083,7 @@ const toolDefinitions = [
|
|
|
837
1083
|
},
|
|
838
1084
|
{
|
|
839
1085
|
name: "migration_tracer",
|
|
1086
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
840
1087
|
description: "Inspect changes in database schemas (tables, columns) across local migrations (e.g. Prisma migration directories).",
|
|
841
1088
|
inputSchema: {
|
|
842
1089
|
type: "object",
|
|
@@ -847,6 +1094,7 @@ const toolDefinitions = [
|
|
|
847
1094
|
},
|
|
848
1095
|
{
|
|
849
1096
|
name: "multi_file_patch",
|
|
1097
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
850
1098
|
description: "Apply search-and-replace patches to multiple workspace files in a single turn. Decreases round-trips for multi-file refactoring.",
|
|
851
1099
|
inputSchema: {
|
|
852
1100
|
type: "object",
|
|
@@ -871,6 +1119,7 @@ const toolDefinitions = [
|
|
|
871
1119
|
},
|
|
872
1120
|
{
|
|
873
1121
|
name: "file_intent_index",
|
|
1122
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
874
1123
|
description: "Map and index files inside the workspace by domain intent (e.g., auth, billing, routes, config) to speed up navigation.",
|
|
875
1124
|
inputSchema: {
|
|
876
1125
|
type: "object",
|
|
@@ -881,6 +1130,7 @@ const toolDefinitions = [
|
|
|
881
1130
|
},
|
|
882
1131
|
{
|
|
883
1132
|
name: "cognitive_map",
|
|
1133
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
884
1134
|
description: "Access and interact with the Auto-Cognitive Mind Map of the workspace (persisted globally). Holds structured high-level system domains, patterns, decisions, and bugs.",
|
|
885
1135
|
inputSchema: {
|
|
886
1136
|
type: "object",
|
|
@@ -903,6 +1153,7 @@ const toolDefinitions = [
|
|
|
903
1153
|
},
|
|
904
1154
|
{
|
|
905
1155
|
name: "semantic_grep",
|
|
1156
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
906
1157
|
description: "Search the codebase for conceptual terms using natural language and BM25 token relevance instead of exact substring matching.",
|
|
907
1158
|
inputSchema: {
|
|
908
1159
|
type: "object",
|
|
@@ -916,6 +1167,7 @@ const toolDefinitions = [
|
|
|
916
1167
|
},
|
|
917
1168
|
{
|
|
918
1169
|
name: "imports_skeleton_resolver",
|
|
1170
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
919
1171
|
description: "Resolve all imported local files inside a file and print only their signatures (classes, functions, interfaces) in a consolidated view.",
|
|
920
1172
|
inputSchema: {
|
|
921
1173
|
type: "object",
|
|
@@ -927,6 +1179,7 @@ const toolDefinitions = [
|
|
|
927
1179
|
},
|
|
928
1180
|
{
|
|
929
1181
|
name: "ast_flow_visualizer",
|
|
1182
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
930
1183
|
description: "Analyze a function's control flow statements (ifs, loops, try/catch) inside a file using AST compilation and output a visual flowchart in Mermaid syntax.",
|
|
931
1184
|
inputSchema: {
|
|
932
1185
|
type: "object",
|
|
@@ -939,6 +1192,7 @@ const toolDefinitions = [
|
|
|
939
1192
|
},
|
|
940
1193
|
{
|
|
941
1194
|
name: "file_fingerprint",
|
|
1195
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
942
1196
|
description: "Compute SHA256 fingerprint of a file. Returns 'unchanged' if hash matches cache, avoiding full re-reads. Saves 60-70% tokens on repeated file access.",
|
|
943
1197
|
inputSchema: {
|
|
944
1198
|
type: "object",
|
|
@@ -951,6 +1205,7 @@ const toolDefinitions = [
|
|
|
951
1205
|
},
|
|
952
1206
|
{
|
|
953
1207
|
name: "git_blame_heat",
|
|
1208
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
954
1209
|
description: "Analyze git blame history to identify high-risk lines and authors. Zero LLM cost. Returns risk scores per line, hotspots, and top contributors by risk.",
|
|
955
1210
|
inputSchema: {
|
|
956
1211
|
type: "object",
|
|
@@ -962,6 +1217,7 @@ const toolDefinitions = [
|
|
|
962
1217
|
},
|
|
963
1218
|
{
|
|
964
1219
|
name: "type_coverage_report",
|
|
1220
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
965
1221
|
description: "Analyze TypeScript type coverage of a file using AST. Returns declaration count, typed vs untyped ratio, any-usage count, and a safety score 0-100. Zero LLM cost.",
|
|
966
1222
|
inputSchema: {
|
|
967
1223
|
type: "object",
|
|
@@ -973,6 +1229,7 @@ const toolDefinitions = [
|
|
|
973
1229
|
},
|
|
974
1230
|
{
|
|
975
1231
|
name: "import_tree_context",
|
|
1232
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
976
1233
|
description: "Build a bidirectional import graph for a file. Shows what it imports, what imports it, and external dependencies. Zero LLM cost. Essential before refactors.",
|
|
977
1234
|
inputSchema: {
|
|
978
1235
|
type: "object",
|
|
@@ -985,6 +1242,7 @@ const toolDefinitions = [
|
|
|
985
1242
|
},
|
|
986
1243
|
{
|
|
987
1244
|
name: "read_token_budgeted",
|
|
1245
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
988
1246
|
description: "Read a file with a hard token budget. Returns only the most relevant sections up to maxTokens. Saves 70-90% vs full reads.",
|
|
989
1247
|
inputSchema: {
|
|
990
1248
|
type: "object",
|
|
@@ -998,6 +1256,7 @@ const toolDefinitions = [
|
|
|
998
1256
|
},
|
|
999
1257
|
{
|
|
1000
1258
|
name: "bulk_file_digest",
|
|
1259
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
1001
1260
|
description: "Read multiple files in one call and return a compressed digest. Saves 80% round-trips vs reading files individually.",
|
|
1002
1261
|
inputSchema: {
|
|
1003
1262
|
type: "object",
|
|
@@ -1015,6 +1274,7 @@ const toolDefinitions = [
|
|
|
1015
1274
|
},
|
|
1016
1275
|
{
|
|
1017
1276
|
name: "auto_context_bundle",
|
|
1277
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
1018
1278
|
description: "Get everything about a file in one call: compressed content, import graph, schema types, and telepathic hints from The Brain. Replaces 4-5 separate tool calls.",
|
|
1019
1279
|
inputSchema: {
|
|
1020
1280
|
type: "object",
|
|
@@ -1026,6 +1286,7 @@ const toolDefinitions = [
|
|
|
1026
1286
|
},
|
|
1027
1287
|
{
|
|
1028
1288
|
name: "repeat_guard",
|
|
1289
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
1029
1290
|
description: "Check if a draft output is repetitive compared to recent responses. Returns similarity score and suggestion. Prevents duplicate explanations.",
|
|
1030
1291
|
inputSchema: {
|
|
1031
1292
|
type: "object",
|
|
@@ -1038,6 +1299,7 @@ const toolDefinitions = [
|
|
|
1038
1299
|
},
|
|
1039
1300
|
{
|
|
1040
1301
|
name: "conversation_checkpoint",
|
|
1302
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1041
1303
|
description: "Save or load session state. Prevents re-explaining context across turns. Save current progress or load last checkpoint.",
|
|
1042
1304
|
inputSchema: {
|
|
1043
1305
|
type: "object",
|
|
@@ -1053,6 +1315,7 @@ const toolDefinitions = [
|
|
|
1053
1315
|
},
|
|
1054
1316
|
{
|
|
1055
1317
|
name: "test_autofix_interceptor",
|
|
1318
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
1056
1319
|
description: "Run tests and auto-search The Brain for fixes to failures. Returns failure list with suggested fixes from past sessions. Saves 4-5 turns of debugging.",
|
|
1057
1320
|
inputSchema: {
|
|
1058
1321
|
type: "object",
|
|
@@ -1063,6 +1326,7 @@ const toolDefinitions = [
|
|
|
1063
1326
|
},
|
|
1064
1327
|
{
|
|
1065
1328
|
name: "prompt_pattern_cache",
|
|
1329
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
1066
1330
|
description: "Find or store optimized prompt templates. Reuses past prompt patterns to save 20-30% tokens on repetitive operations.",
|
|
1067
1331
|
inputSchema: {
|
|
1068
1332
|
type: "object",
|
|
@@ -1082,6 +1346,7 @@ const _infraToolDefs = getInfraToolDefinitions();
|
|
|
1082
1346
|
// than in the main array because it must never itself be hidden.
|
|
1083
1347
|
const TOOLBOX_TOOL_DEFINITION = {
|
|
1084
1348
|
name: "lemma_toolbox",
|
|
1349
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
1085
1350
|
description: "Discover and invoke Lemma tools whose schemas are not loaded this session. Use action='list' (optionally with a query) to see what exists, 'schema' to read one tool's inputs, and 'call' to run it. Every Lemma capability is reachable here.",
|
|
1086
1351
|
inputSchema: {
|
|
1087
1352
|
type: "object",
|
|
@@ -1143,6 +1408,7 @@ const toolHandlers = {
|
|
|
1143
1408
|
scrub_privacy: handleScrubPrivacy,
|
|
1144
1409
|
search_memory: handleSearchMemory,
|
|
1145
1410
|
store_memory: handleStoreMemory,
|
|
1411
|
+
downvote_memory: handleDownvoteMemory,
|
|
1146
1412
|
get_routing_advice: handleGetRoutingAdvice,
|
|
1147
1413
|
auto_heal: handleAutoHeal,
|
|
1148
1414
|
read_workspace_file: handleReadWorkspaceFile,
|
|
@@ -1287,11 +1553,12 @@ function receiptLabelMeta(name, args) {
|
|
|
1287
1553
|
return meta;
|
|
1288
1554
|
}
|
|
1289
1555
|
export function setupToolsHandlers(server, onToolCall) {
|
|
1556
|
+
mcpServerRef = server;
|
|
1290
1557
|
registerSymbolExtractor(extractSymbolsWithTsCompiler);
|
|
1291
1558
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
1292
1559
|
tools: toolDefinitionsArray,
|
|
1293
1560
|
}));
|
|
1294
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1561
|
+
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
1295
1562
|
const { name, arguments: args } = request.params;
|
|
1296
1563
|
const startTime = Date.now();
|
|
1297
1564
|
// ── Pro License Gate (granular: free tools work without license) ────────
|
|
@@ -1320,6 +1587,39 @@ export function setupToolsHandlers(server, onToolCall) {
|
|
|
1320
1587
|
});
|
|
1321
1588
|
throw new Error(`Unknown tool: ${name}`);
|
|
1322
1589
|
}
|
|
1590
|
+
// ── Task-augmented call: hand off to tasks.ts instead of awaiting inline ────
|
|
1591
|
+
// A task-unaware client never sends `request.params.task`, so this only triggers when
|
|
1592
|
+
// the caller actually asked for it — today's synchronous behavior is unchanged otherwise.
|
|
1593
|
+
if (request.params.task && TASK_CAPABLE_TOOLS.has(name)) {
|
|
1594
|
+
const ledgerBefore = getLedgerLength();
|
|
1595
|
+
return runAsTask(name, (args || {}), request, extra.requestId, handler, (result, error) => {
|
|
1596
|
+
if (error) {
|
|
1597
|
+
onToolCall?.({
|
|
1598
|
+
tool: name,
|
|
1599
|
+
args: args || {},
|
|
1600
|
+
result: "ERROR",
|
|
1601
|
+
latency: Date.now() - startTime,
|
|
1602
|
+
error: error instanceof Error ? error.message.substring(0, 100) : String(error).substring(0, 100),
|
|
1603
|
+
});
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
if (getLedgerLength() === ledgerBefore) {
|
|
1607
|
+
recordReceiptEvent(receiptTypeForTool(name), name, {
|
|
1608
|
+
tool: name,
|
|
1609
|
+
...receiptLabelMeta(name, args),
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
const tokensImpact = estimateTokensFromResult(result);
|
|
1613
|
+
onToolCall?.({
|
|
1614
|
+
tool: name,
|
|
1615
|
+
args: args || {},
|
|
1616
|
+
result: "OK",
|
|
1617
|
+
latency: Date.now() - startTime,
|
|
1618
|
+
tokensImpact,
|
|
1619
|
+
});
|
|
1620
|
+
reportCost({ source: "toolResult", tokens: tokensImpact, toolName: name });
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1323
1623
|
try {
|
|
1324
1624
|
// Sampled around the handler so a tool that logs its own, more specific event
|
|
1325
1625
|
// (a cache hit, a miss) isn't double-counted by the generic entry below.
|
|
@@ -1355,6 +1655,9 @@ export function setupToolsHandlers(server, onToolCall) {
|
|
|
1355
1655
|
throw error;
|
|
1356
1656
|
}
|
|
1357
1657
|
});
|
|
1658
|
+
// Registered after ListTools/CallTool: tests and other callers that assume "ListTools is
|
|
1659
|
+
// first, CallTool is second" (see tests/unit/tool-regressions.test.ts) key off that order.
|
|
1660
|
+
registerTaskHandlers(server);
|
|
1358
1661
|
}
|
|
1359
1662
|
// ── Tool Implementations ────────────────────────────────────────────
|
|
1360
1663
|
async function handleScrubPrivacy(args) {
|
|
@@ -1378,11 +1681,12 @@ async function handleSearchMemory(args) {
|
|
|
1378
1681
|
// formatting below then presents it as a reusable memory. Match smarter_cache's
|
|
1379
1682
|
// threshold so both paths agree on what counts as a hit.
|
|
1380
1683
|
const minSimilarity = typeof args?.minSimilarity === "number" ? args.minSimilarity : DEFAULT_MEMORY_SIMILARITY_FLOOR;
|
|
1684
|
+
const domain = typeof args?.domain === "string" && args.domain.trim() ? args.domain.trim() : undefined;
|
|
1381
1685
|
if (!query)
|
|
1382
1686
|
throw new Error("Query is required");
|
|
1383
1687
|
try {
|
|
1384
1688
|
const brain = getBrain();
|
|
1385
|
-
const results = brain.search(query, limit, minSimilarity, { projectId: deriveProjectId() });
|
|
1689
|
+
const results = brain.search(query, limit, minSimilarity, { projectId: deriveProjectId(), domain });
|
|
1386
1690
|
if (results.length === 0) {
|
|
1387
1691
|
recordReceiptEvent("reasoning", query.substring(0, 100), {
|
|
1388
1692
|
tool: "search_memory",
|
|
@@ -1406,7 +1710,7 @@ async function handleSearchMemory(args) {
|
|
|
1406
1710
|
const staleCount = r.claims.filter((c) => !c.fresh).length;
|
|
1407
1711
|
return `\nClaims (${r.claims.length - staleCount}/${r.claims.length} still fresh — trust only the ✓ ones):\n${lines.join("\n")}`;
|
|
1408
1712
|
};
|
|
1409
|
-
const formatResult = (r, i) => `Result ${i + 1} (Similarity: ${(r.similarity * 100).toFixed(1)}%)${r.outcome === "failed" ? "\n⚠️ TAGGED AS A FAILED ATTEMPT — this was tried before and did NOT work. Do not repeat it; treat this as a warning, not a suggestion." : ""}\nPrompt: ${r.query.substring(0, 300)}...\nResponse: ${typeof r.response === "string" ? r.response : JSON.stringify(r.response, null, 2)}${formatClaims(r)}`;
|
|
1713
|
+
const formatResult = (r, i) => `Result ${i + 1} (id: ${r.id}, Similarity: ${(r.similarity * 100).toFixed(1)}%)${r.outcome === "failed" ? "\n⚠️ TAGGED AS A FAILED ATTEMPT — this was tried before and did NOT work. Do not repeat it; treat this as a warning, not a suggestion." : ""}\nPrompt: ${r.query.substring(0, 300)}...\nResponse: ${typeof r.response === "string" ? r.response : JSON.stringify(r.response, null, 2)}${formatClaims(r)}`;
|
|
1410
1714
|
if (fresh.length > 0) {
|
|
1411
1715
|
// Only a fresh hit (or an untracked, purely conceptual entry) counts as a real
|
|
1412
1716
|
// avoided re-investigation — this is the only case worth crediting as savings.
|
|
@@ -1454,11 +1758,15 @@ async function handleStoreMemory(args) {
|
|
|
1454
1758
|
const claims = Array.isArray(args?.claims)
|
|
1455
1759
|
? args.claims.filter((c) => c?.text)
|
|
1456
1760
|
: undefined;
|
|
1761
|
+
const explicitDomain = typeof args?.domain === "string" && args.domain.trim() ? args.domain.trim() : undefined;
|
|
1762
|
+
// Auto-derive from the first tracked file when the caller didn't say — see
|
|
1763
|
+
// deriveDomainForFile for why this is a best-effort tag, not a hard classification.
|
|
1764
|
+
const domain = explicitDomain || (filePaths && filePaths.length > 0 ? deriveDomainForFile(filePaths[0]) : undefined);
|
|
1457
1765
|
if (!query || !responseText)
|
|
1458
1766
|
throw new Error("Query and response are required");
|
|
1459
1767
|
try {
|
|
1460
1768
|
const brain = getBrain();
|
|
1461
|
-
const storeRes = brain.store(query, responseText, provider, 0.92, filePaths, undefined, outcome, symbols, claims);
|
|
1769
|
+
const storeRes = brain.store(query, responseText, provider, 0.92, filePaths, undefined, outcome, symbols, claims, domain);
|
|
1462
1770
|
// Storing a memory saves nothing — it only creates the chance of a saving later.
|
|
1463
1771
|
// Crediting tokens here inflated the ledger on write and then credited the same
|
|
1464
1772
|
// answer again on every read. The saving is booked by search_memory on a fresh hit.
|
|
@@ -1471,19 +1779,40 @@ async function handleStoreMemory(args) {
|
|
|
1471
1779
|
const symbolNote = symbols && symbols.length > 0 ? ` Tracking freshness against ${symbols.length} symbol(s) specifically — unrelated edits elsewhere in those files won't stale this out.` : "";
|
|
1472
1780
|
const outcomeNote = outcome === "failed" ? " Tagged as a FAILED attempt — future searches will surface it as a warning, not a suggestion." : "";
|
|
1473
1781
|
const claimsNote = claims && claims.length > 0 ? ` Split into ${claims.length} independently-verifiable claim(s) — a future search can trust the ones still fresh even if another goes stale.` : "";
|
|
1474
|
-
|
|
1782
|
+
const domainNote = domain
|
|
1783
|
+
? ` Tagged with domain '${domain}'${explicitDomain ? "" : " (auto-derived from filePaths)"} — domain-scoped searches will prefer it.`
|
|
1784
|
+
: "";
|
|
1785
|
+
const conflictsNote = storeRes.conflicts && storeRes.conflicts.length > 0
|
|
1786
|
+
? `\n\n⚠️ ${storeRes.conflicts.length} existing ${storeRes.conflicts.length === 1 ? "memory shares" : "memories share"} a symbol with this one but disagree on outcome — review before trusting either:\n` +
|
|
1787
|
+
storeRes.conflicts.map((c) => ` - [${c.outcome}] ${c.id}: ${c.query.substring(0, 120)}`).join("\n")
|
|
1788
|
+
: "";
|
|
1789
|
+
return { content: [{ type: "text", text: `Success: Memory stored. ${storeRes.reason}${trackingNote}${symbolNote}${outcomeNote}${claimsNote}${domainNote}${conflictsNote}` }] };
|
|
1475
1790
|
}
|
|
1476
1791
|
catch (e) {
|
|
1477
1792
|
logError("store_memory", e);
|
|
1478
1793
|
return { content: [{ type: "text", text: `Failed to store memory in local Brain: ${e.message}` }] };
|
|
1479
1794
|
}
|
|
1480
1795
|
}
|
|
1796
|
+
async function handleDownvoteMemory(args) {
|
|
1797
|
+
const id = args?.id;
|
|
1798
|
+
if (!id)
|
|
1799
|
+
throw new Error("id is required");
|
|
1800
|
+
try {
|
|
1801
|
+
const result = getBrain().downvote(id);
|
|
1802
|
+
return { content: [{ type: "text", text: result.message }] };
|
|
1803
|
+
}
|
|
1804
|
+
catch (e) {
|
|
1805
|
+
logError("downvote_memory", e);
|
|
1806
|
+
return { content: [{ type: "text", text: `Failed to downvote memory: ${e.message}` }] };
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1481
1809
|
async function handleGetRoutingAdvice(args) {
|
|
1482
1810
|
const prompt = args?.prompt;
|
|
1483
1811
|
const intendedModel = args?.intended_model;
|
|
1484
1812
|
const router = new ComplexityRouter();
|
|
1485
1813
|
const decision = router.evaluate(prompt, intendedModel);
|
|
1486
|
-
|
|
1814
|
+
const switched = !!intendedModel && decision.model !== intendedModel;
|
|
1815
|
+
if (decision.complexity === "low" && switched) {
|
|
1487
1816
|
const estimatedPromptTokens = Math.floor((prompt?.length || 0) / 4);
|
|
1488
1817
|
reportSavings({
|
|
1489
1818
|
source: "complexityRouting",
|
|
@@ -1492,6 +1821,12 @@ async function handleGetRoutingAdvice(args) {
|
|
|
1492
1821
|
query: prompt?.substring(0, 100),
|
|
1493
1822
|
});
|
|
1494
1823
|
}
|
|
1824
|
+
const structured = {
|
|
1825
|
+
recommendedModel: decision.model,
|
|
1826
|
+
complexity: decision.complexity,
|
|
1827
|
+
intendedModel: intendedModel || null,
|
|
1828
|
+
switched,
|
|
1829
|
+
};
|
|
1495
1830
|
return {
|
|
1496
1831
|
content: [
|
|
1497
1832
|
{
|
|
@@ -1499,6 +1834,7 @@ async function handleGetRoutingAdvice(args) {
|
|
|
1499
1834
|
text: `Lemma Routing Advice: Use ${decision.model}. Reason: Complexity is ${decision.complexity}.`,
|
|
1500
1835
|
},
|
|
1501
1836
|
],
|
|
1837
|
+
structuredContent: structured,
|
|
1502
1838
|
};
|
|
1503
1839
|
}
|
|
1504
1840
|
async function handleAutoHeal(args) {
|
|
@@ -1708,6 +2044,30 @@ function stripLineNumberGutter(text) {
|
|
|
1708
2044
|
return text;
|
|
1709
2045
|
return lines.map((l) => l.replace(GUTTER, "")).join("\n");
|
|
1710
2046
|
}
|
|
2047
|
+
// Splices one resolved match into the file and writes it. Shared by the normal
|
|
2048
|
+
// single-match path and the ambiguous-match-resolved-via-elicitation path below.
|
|
2049
|
+
async function applyMatch(match, filePath, resolved, originalContent, searchContent, replaceContent) {
|
|
2050
|
+
let updatedContent;
|
|
2051
|
+
if (match.strategy === "exact" && match.charStart !== undefined && match.charEnd !== undefined) {
|
|
2052
|
+
// Splice by character offset — the search block may be a mid-line fragment, and
|
|
2053
|
+
// a whole-line splice would discard everything else sharing that line.
|
|
2054
|
+
updatedContent = originalContent.slice(0, match.charStart) + replaceContent + originalContent.slice(match.charEnd);
|
|
2055
|
+
}
|
|
2056
|
+
else {
|
|
2057
|
+
const contentLines = originalContent.split("\n");
|
|
2058
|
+
const finalReplace = reindentReplacement(replaceContent, searchContent, contentLines[match.startLine]);
|
|
2059
|
+
const updatedLines = [...contentLines.slice(0, match.startLine), ...finalReplace.split("\n"), ...contentLines.slice(match.endLine + 1)];
|
|
2060
|
+
updatedContent = updatedLines.join("\n");
|
|
2061
|
+
}
|
|
2062
|
+
const elicited = await tryElicitConfirmation(`Apply a patch to ${filePath}? This replaces ${searchContent.length} char(s) with ${replaceContent.length} char(s) ` +
|
|
2063
|
+
`starting at line ${match.startLine + 1}.`);
|
|
2064
|
+
if (elicited.supported && !elicited.confirmed) {
|
|
2065
|
+
return { content: [{ type: "text", text: `Patch to ${filePath} was not applied: declined during confirmation.` }] };
|
|
2066
|
+
}
|
|
2067
|
+
fs.writeFileSync(resolved, updatedContent, "utf8");
|
|
2068
|
+
const note = match.strategy === "exact" ? "" : ` (matched via ${match.strategy}, score ${match.score.toFixed(2)})`;
|
|
2069
|
+
return { content: [{ type: "text", text: `Success: Patch successfully applied to ${filePath}${note}` }] };
|
|
2070
|
+
}
|
|
1711
2071
|
async function handleApplyWorkspacePatch(args) {
|
|
1712
2072
|
const filePath = args?.filePath;
|
|
1713
2073
|
const rawSearch = args?.searchContent;
|
|
@@ -1730,6 +2090,15 @@ async function handleApplyWorkspacePatch(args) {
|
|
|
1730
2090
|
const matchResult = findMatch(originalContent, searchContent);
|
|
1731
2091
|
if (!Array.isArray(matchResult)) {
|
|
1732
2092
|
if (matchResult.reason === "ambiguous") {
|
|
2093
|
+
const candidates = matchResult.candidates ?? [];
|
|
2094
|
+
if (candidates.length > 0) {
|
|
2095
|
+
const chosen = await tryElicitChoice(`${candidates.length} equally good matches found for the search content in ${filePath}. Which one should be patched?`, candidates.map((c) => ({
|
|
2096
|
+
title: `line ${c.startLine + 1}: ${c.matchedText.split("\n")[0].slice(0, 80)}`,
|
|
2097
|
+
})));
|
|
2098
|
+
if (chosen.index !== null) {
|
|
2099
|
+
return applyMatch(candidates[chosen.index], filePath, resolved, originalContent, searchContent, replaceContent);
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
1733
2102
|
return {
|
|
1734
2103
|
content: [
|
|
1735
2104
|
{
|
|
@@ -1758,28 +2127,71 @@ async function handleApplyWorkspacePatch(args) {
|
|
|
1758
2127
|
],
|
|
1759
2128
|
};
|
|
1760
2129
|
}
|
|
1761
|
-
|
|
1762
|
-
let updatedContent;
|
|
1763
|
-
if (match.strategy === "exact" && match.charStart !== undefined && match.charEnd !== undefined) {
|
|
1764
|
-
// Splice by character offset — the search block may be a mid-line fragment, and
|
|
1765
|
-
// a whole-line splice would discard everything else sharing that line.
|
|
1766
|
-
updatedContent = originalContent.slice(0, match.charStart) + replaceContent + originalContent.slice(match.charEnd);
|
|
1767
|
-
}
|
|
1768
|
-
else {
|
|
1769
|
-
const contentLines = originalContent.split("\n");
|
|
1770
|
-
const finalReplace = reindentReplacement(replaceContent, searchContent, contentLines[match.startLine]);
|
|
1771
|
-
const updatedLines = [...contentLines.slice(0, match.startLine), ...finalReplace.split("\n"), ...contentLines.slice(match.endLine + 1)];
|
|
1772
|
-
updatedContent = updatedLines.join("\n");
|
|
1773
|
-
}
|
|
1774
|
-
fs.writeFileSync(resolved, updatedContent, "utf8");
|
|
1775
|
-
const note = match.strategy === "exact" ? "" : ` (matched via ${match.strategy}, score ${match.score.toFixed(2)})`;
|
|
1776
|
-
return { content: [{ type: "text", text: `Success: Patch successfully applied to ${filePath}${note}` }] };
|
|
2130
|
+
return applyMatch(matchResult[0], filePath, resolved, originalContent, searchContent, replaceContent);
|
|
1777
2131
|
}
|
|
1778
2132
|
catch (err) {
|
|
1779
2133
|
logError("apply_workspace_patch", err);
|
|
1780
2134
|
return { content: [{ type: "text", text: `Error applying patch: ${err.message}` }] };
|
|
1781
2135
|
}
|
|
1782
2136
|
}
|
|
2137
|
+
// Async replacement for spawnSync: a blocking syscall inside a `tools/call` handler
|
|
2138
|
+
// freezes the whole (single-threaded) MCP server for the run's entire duration — including
|
|
2139
|
+
// task-status polls for THIS run once it's task-augmented (see runAsTask in tasks.ts), which
|
|
2140
|
+
// would otherwise defeat the point of making the tool pollable. `spawn` keeps the event loop
|
|
2141
|
+
// free while the command runs. Shape of the resolved value mirrors spawnSync's return value
|
|
2142
|
+
// on the fields the caller actually reads, so the rest of handleRunWorkspaceCommand is unchanged.
|
|
2143
|
+
function spawnCollect(command, cwd, timeoutMs) {
|
|
2144
|
+
return new Promise((resolve) => {
|
|
2145
|
+
const child = spawn(command, {
|
|
2146
|
+
cwd,
|
|
2147
|
+
shell: true,
|
|
2148
|
+
env: { ...process.env },
|
|
2149
|
+
// spawnSync closed stdin immediately (no `input` option given), so a command reading
|
|
2150
|
+
// from stdin (e.g. `sed` with no file argument) saw EOF right away. Plain `spawn`
|
|
2151
|
+
// leaves stdin as an open pipe by default — nothing ever writes to or closes it — so
|
|
2152
|
+
// the same command hangs until the timeout kills it. "ignore" reproduces the old,
|
|
2153
|
+
// immediate-EOF behavior.
|
|
2154
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2155
|
+
});
|
|
2156
|
+
let stdout = "";
|
|
2157
|
+
let stderr = "";
|
|
2158
|
+
let stdoutBytes = 0;
|
|
2159
|
+
let stderrBytes = 0;
|
|
2160
|
+
const MAX_BUFFER = 20 * 1024 * 1024;
|
|
2161
|
+
let timedOut = false;
|
|
2162
|
+
let settled = false;
|
|
2163
|
+
const timer = setTimeout(() => {
|
|
2164
|
+
timedOut = true;
|
|
2165
|
+
child.kill("SIGTERM");
|
|
2166
|
+
}, timeoutMs);
|
|
2167
|
+
child.stdout?.on("data", (chunk) => {
|
|
2168
|
+
if (stdoutBytes >= MAX_BUFFER)
|
|
2169
|
+
return;
|
|
2170
|
+
stdoutBytes += chunk.length;
|
|
2171
|
+
stdout += chunk.toString("utf8");
|
|
2172
|
+
});
|
|
2173
|
+
child.stderr?.on("data", (chunk) => {
|
|
2174
|
+
if (stderrBytes >= MAX_BUFFER)
|
|
2175
|
+
return;
|
|
2176
|
+
stderrBytes += chunk.length;
|
|
2177
|
+
stderr += chunk.toString("utf8");
|
|
2178
|
+
});
|
|
2179
|
+
child.on("error", (err) => {
|
|
2180
|
+
if (settled)
|
|
2181
|
+
return;
|
|
2182
|
+
settled = true;
|
|
2183
|
+
clearTimeout(timer);
|
|
2184
|
+
resolve({ stdout, stderr, status: null, timedOut, error: err });
|
|
2185
|
+
});
|
|
2186
|
+
child.on("close", (code) => {
|
|
2187
|
+
if (settled)
|
|
2188
|
+
return;
|
|
2189
|
+
settled = true;
|
|
2190
|
+
clearTimeout(timer);
|
|
2191
|
+
resolve({ stdout, stderr, status: code, timedOut });
|
|
2192
|
+
});
|
|
2193
|
+
});
|
|
2194
|
+
}
|
|
1783
2195
|
async function handleRunWorkspaceCommand(args) {
|
|
1784
2196
|
const command = args?.command;
|
|
1785
2197
|
if (!command)
|
|
@@ -1809,10 +2221,10 @@ async function handleRunWorkspaceCommand(args) {
|
|
|
1809
2221
|
// ── End allowlist check ────────────────────────────────────────────────
|
|
1810
2222
|
const workspaceRoot = process.cwd();
|
|
1811
2223
|
try {
|
|
1812
|
-
//
|
|
1813
|
-
// stdio is overridden, which silently dropped the output of every tool
|
|
1814
|
-
// stderr (jest and tsc both do) on a successful run. It also gives us
|
|
1815
|
-
// the exit code through one code path instead of a throw-on-failure split.
|
|
2224
|
+
// spawn (via spawnCollect above) rather than execSync: execSync leaves stderr attached to
|
|
2225
|
+
// the parent unless stdio is overridden, which silently dropped the output of every tool
|
|
2226
|
+
// that reports on stderr (jest and tsc both do) on a successful run. It also gives us
|
|
2227
|
+
// stdout, stderr and the exit code through one code path instead of a throw-on-failure split.
|
|
1816
2228
|
// 15s was below the runtime of the commands this tool exists for: a real `npm test` or
|
|
1817
2229
|
// `tsc --noEmit` always tripped it, so the distillation pipeline never ran on the output
|
|
1818
2230
|
// it was built for and the call was pure overhead before falling back to a plain shell.
|
|
@@ -1820,15 +2232,12 @@ async function handleRunWorkspaceCommand(args) {
|
|
|
1820
2232
|
const MAX_TIMEOUT_MS = 600000;
|
|
1821
2233
|
const requested = typeof args?.timeoutMs === "number" && args.timeoutMs > 0 ? args.timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
1822
2234
|
const timeoutMs = Math.min(Math.floor(requested), MAX_TIMEOUT_MS);
|
|
1823
|
-
const
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
env: { ...process.env },
|
|
1830
|
-
});
|
|
1831
|
-
const timedOut = !!result.error && result.error.code === "ETIMEDOUT";
|
|
2235
|
+
const elicited = await tryElicitConfirmation(`Run this command in the workspace root?\n\n${command}`);
|
|
2236
|
+
if (elicited.supported && !elicited.confirmed) {
|
|
2237
|
+
return { content: [{ type: "text", text: `Command was not run: declined during confirmation.\n\nCommand: ${command}` }] };
|
|
2238
|
+
}
|
|
2239
|
+
const result = await spawnCollect(command, workspaceRoot, timeoutMs);
|
|
2240
|
+
const timedOut = result.timedOut;
|
|
1832
2241
|
if (result.error && !timedOut) {
|
|
1833
2242
|
return {
|
|
1834
2243
|
content: [{ type: "text", text: `Command could not be executed: ${result.error.message}` }],
|
|
@@ -2842,47 +3251,44 @@ async function handleSmarterCache(args) {
|
|
|
2842
3251
|
reportSavings({ source: "cache", tokens: tokensSaved, toolName: "smarter_cache", query: query.substring(0, 100) });
|
|
2843
3252
|
}
|
|
2844
3253
|
catch { }
|
|
3254
|
+
const structured = {
|
|
3255
|
+
hit: true,
|
|
3256
|
+
similarity: topHit.similarity,
|
|
3257
|
+
threshold,
|
|
3258
|
+
answer: responseText,
|
|
3259
|
+
source: "lemma-brain",
|
|
3260
|
+
tokensSaved,
|
|
3261
|
+
tokensSavedFormatted: `~${tokensSaved.toLocaleString()} tokens`,
|
|
3262
|
+
};
|
|
2845
3263
|
return {
|
|
2846
|
-
content: [{
|
|
2847
|
-
|
|
2848
|
-
text: JSON.stringify({
|
|
2849
|
-
hit: true,
|
|
2850
|
-
similarity: topHit.similarity,
|
|
2851
|
-
threshold,
|
|
2852
|
-
answer: responseText,
|
|
2853
|
-
source: "lemma-brain",
|
|
2854
|
-
tokensSaved,
|
|
2855
|
-
tokensSavedFormatted: `~${tokensSaved.toLocaleString()} tokens`,
|
|
2856
|
-
}, null, 2),
|
|
2857
|
-
}],
|
|
3264
|
+
content: [{ type: "text", text: JSON.stringify(structured, null, 2) }],
|
|
3265
|
+
structuredContent: structured,
|
|
2858
3266
|
};
|
|
2859
3267
|
}
|
|
3268
|
+
const structured = {
|
|
3269
|
+
hit: false,
|
|
3270
|
+
similarity: topHit?.similarity || 0,
|
|
3271
|
+
threshold,
|
|
3272
|
+
answer: null,
|
|
3273
|
+
source: "llm-call-required",
|
|
3274
|
+
hint: "Después de resolver, llama store_memory para cachear y no gastar tokens en esto otra vez.",
|
|
3275
|
+
};
|
|
2860
3276
|
return {
|
|
2861
|
-
content: [{
|
|
2862
|
-
|
|
2863
|
-
text: JSON.stringify({
|
|
2864
|
-
hit: false,
|
|
2865
|
-
similarity: topHit?.similarity || 0,
|
|
2866
|
-
threshold,
|
|
2867
|
-
answer: null,
|
|
2868
|
-
source: "llm-call-required",
|
|
2869
|
-
hint: "Después de resolver, llama store_memory para cachear y no gastar tokens en esto otra vez.",
|
|
2870
|
-
}, null, 2),
|
|
2871
|
-
}],
|
|
3277
|
+
content: [{ type: "text", text: JSON.stringify(structured, null, 2) }],
|
|
3278
|
+
structuredContent: structured,
|
|
2872
3279
|
};
|
|
2873
3280
|
}
|
|
2874
3281
|
catch (e) {
|
|
2875
3282
|
logError("smarter_cache", e);
|
|
3283
|
+
const structured = {
|
|
3284
|
+
hit: false,
|
|
3285
|
+
error: e.message,
|
|
3286
|
+
source: "cache-unavailable",
|
|
3287
|
+
hint: "El cache local del brain falló. El LLM funcionará normalmente sin cache.",
|
|
3288
|
+
};
|
|
2876
3289
|
return {
|
|
2877
|
-
content: [{
|
|
2878
|
-
|
|
2879
|
-
text: JSON.stringify({
|
|
2880
|
-
hit: false,
|
|
2881
|
-
error: e.message,
|
|
2882
|
-
source: "cache-unavailable",
|
|
2883
|
-
hint: "El cache local del brain falló. El LLM funcionará normalmente sin cache.",
|
|
2884
|
-
}, null, 2),
|
|
2885
|
-
}],
|
|
3290
|
+
content: [{ type: "text", text: JSON.stringify(structured, null, 2) }],
|
|
3291
|
+
structuredContent: structured,
|
|
2886
3292
|
};
|
|
2887
3293
|
}
|
|
2888
3294
|
}
|
|
@@ -2903,23 +3309,21 @@ async function handleStateHashCache(args) {
|
|
|
2903
3309
|
reportSavings({ source: "cache", tokens: tokensSaved, toolName: "state_hash_cache", query: query.substring(0, 100) });
|
|
2904
3310
|
}
|
|
2905
3311
|
catch { }
|
|
3312
|
+
const structured = {
|
|
3313
|
+
status: "hit",
|
|
3314
|
+
answer: result.entry.answer,
|
|
3315
|
+
note: "Respuesta desde caché exacto (hash de archivos sin cambios) — no necesita re-razonar.",
|
|
3316
|
+
tokensSaved,
|
|
3317
|
+
};
|
|
2906
3318
|
return {
|
|
2907
|
-
content: [{
|
|
2908
|
-
|
|
2909
|
-
text: JSON.stringify({
|
|
2910
|
-
status: "hit",
|
|
2911
|
-
answer: result.entry.answer,
|
|
2912
|
-
note: "Respuesta desde caché exacto (hash de archivos sin cambios) — no necesita re-razonar.",
|
|
2913
|
-
tokensSaved,
|
|
2914
|
-
}, null, 2),
|
|
2915
|
-
}],
|
|
3319
|
+
content: [{ type: "text", text: JSON.stringify(structured, null, 2) }],
|
|
3320
|
+
structuredContent: structured,
|
|
2916
3321
|
};
|
|
2917
3322
|
}
|
|
3323
|
+
const structured = { status: "miss", reason: result.reason, hint: "Razona normalmente y luego llama action='store' con la respuesta." };
|
|
2918
3324
|
return {
|
|
2919
|
-
content: [{
|
|
2920
|
-
|
|
2921
|
-
text: JSON.stringify({ status: "miss", reason: result.reason, hint: "Razona normalmente y luego llama action='store' con la respuesta." }, null, 2),
|
|
2922
|
-
}],
|
|
3325
|
+
content: [{ type: "text", text: JSON.stringify(structured, null, 2) }],
|
|
3326
|
+
structuredContent: structured,
|
|
2923
3327
|
};
|
|
2924
3328
|
}
|
|
2925
3329
|
if (action === "store") {
|
|
@@ -2927,8 +3331,10 @@ async function handleStateHashCache(args) {
|
|
|
2927
3331
|
if (!answer)
|
|
2928
3332
|
throw new Error("answer is required for action='store'");
|
|
2929
3333
|
const entry = storeStateHash(workspaceRoot, query, answer, filePaths);
|
|
3334
|
+
const structured = { status: "stored", id: entry.id, filesTracked: Object.keys(entry.fileHashes) };
|
|
2930
3335
|
return {
|
|
2931
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
3336
|
+
content: [{ type: "text", text: JSON.stringify(structured, null, 2) }],
|
|
3337
|
+
structuredContent: structured,
|
|
2932
3338
|
};
|
|
2933
3339
|
}
|
|
2934
3340
|
throw new Error(`Unknown action: ${action}. Use 'lookup' or 'store'.`);
|
|
@@ -2937,16 +3343,15 @@ async function handleStateHashCache(args) {
|
|
|
2937
3343
|
async function handleTokenReceipt(args) {
|
|
2938
3344
|
const limit = typeof args?.limit === "number" ? args.limit : 20;
|
|
2939
3345
|
const summary = getReceiptSummary(limit);
|
|
3346
|
+
const structured = {
|
|
3347
|
+
totalEvents: summary.totalEvents,
|
|
3348
|
+
byType: summary.byType,
|
|
3349
|
+
recentEvents: summary.recent,
|
|
3350
|
+
note: "Esto es una bitácora real de esta sesión, no una proyección de ahorro. Si byType.reasoning es alto, no hubo mucho cache — y está bien, es honesto.",
|
|
3351
|
+
};
|
|
2940
3352
|
return {
|
|
2941
|
-
content: [{
|
|
2942
|
-
|
|
2943
|
-
text: JSON.stringify({
|
|
2944
|
-
totalEvents: summary.totalEvents,
|
|
2945
|
-
byType: summary.byType,
|
|
2946
|
-
recentEvents: summary.recent,
|
|
2947
|
-
note: "Esto es una bitácora real de esta sesión, no una proyección de ahorro. Si byType.reasoning es alto, no hubo mucho cache — y está bien, es honesto.",
|
|
2948
|
-
}, null, 2),
|
|
2949
|
-
}],
|
|
3353
|
+
content: [{ type: "text", text: JSON.stringify(structured, null, 2) }],
|
|
3354
|
+
structuredContent: structured,
|
|
2950
3355
|
};
|
|
2951
3356
|
}
|
|
2952
3357
|
// ── Token Budget ─────────────────────────────────────────────────
|
|
@@ -4230,7 +4635,17 @@ async function handleAffectedTests(args) {
|
|
|
4230
4635
|
totalTests: result.totalTests,
|
|
4231
4636
|
narrowed: result.command !== null,
|
|
4232
4637
|
});
|
|
4233
|
-
|
|
4638
|
+
const structured = {
|
|
4639
|
+
changed: result.changed,
|
|
4640
|
+
directTests: result.directTests,
|
|
4641
|
+
affected: result.affected,
|
|
4642
|
+
totalTests: result.totalTests,
|
|
4643
|
+
runner: result.runner,
|
|
4644
|
+
command: result.command,
|
|
4645
|
+
fullSuiteReason: result.fullSuiteReason,
|
|
4646
|
+
ungraphed: result.ungraphed,
|
|
4647
|
+
};
|
|
4648
|
+
return { content: [{ type: "text", text }], structuredContent: structured };
|
|
4234
4649
|
}
|
|
4235
4650
|
catch (err) {
|
|
4236
4651
|
logError("affected_tests", err);
|
|
@@ -4735,6 +5150,37 @@ async function handleSmartFileSlice(args) {
|
|
|
4735
5150
|
}
|
|
4736
5151
|
}
|
|
4737
5152
|
// ── Test Oracle ──────────────────────────────────────────────────────
|
|
5153
|
+
/**
|
|
5154
|
+
* Auto-capture a green test run into the Brain, tagged `outcome: 'confirmed'` and tracked
|
|
5155
|
+
* against the files actually changed. This is the auto-store hook: today store_memory only
|
|
5156
|
+
* fires when the agent remembers to call it, so working fixes routinely go uncaptured. A
|
|
5157
|
+
* passing suite over an uncommitted diff is real, cheap evidence something worked — reusing
|
|
5158
|
+
* it costs nothing store_memory wouldn't have cost anyway, and store()'s own dedup guard
|
|
5159
|
+
* means re-running the same green suite repeatedly just no-ops instead of piling up entries.
|
|
5160
|
+
*
|
|
5161
|
+
* Silent on a clean tree (nothing changed, nothing to attribute the pass to) or when the
|
|
5162
|
+
* repo isn't git-tracked. Returns a short suffix for resultText — this must never happen
|
|
5163
|
+
* invisibly, or a future search_memory hit would look reasoned-from-scratch instead of
|
|
5164
|
+
* traced back to an automated test run.
|
|
5165
|
+
*/
|
|
5166
|
+
function autoStoreGreenTests(command, workspaceRoot) {
|
|
5167
|
+
try {
|
|
5168
|
+
const changed = changedFilesFromGit(workspaceRoot);
|
|
5169
|
+
if (changed.length === 0)
|
|
5170
|
+
return "";
|
|
5171
|
+
const domain = deriveDomainForFile(changed[0]);
|
|
5172
|
+
const query = `tests passing after changes to ${changed.slice(0, 5).join(", ")}${changed.length > 5 ? ` and ${changed.length - 5} more file(s)` : ""}`;
|
|
5173
|
+
const response = `\`${command}\` passed with these files changed (uncommitted at capture time):\n${changed.join("\n")}`;
|
|
5174
|
+
const storeRes = getBrain().store(query, response, "test_oracle_auto", 0.92, changed, undefined, "confirmed", undefined, undefined, domain);
|
|
5175
|
+
if (!storeRes.stored)
|
|
5176
|
+
return "";
|
|
5177
|
+
return `\n\n[Lemma] Auto-captured this green state in Brain (${changed.length} file(s), tagged confirmed) — search_memory will surface it, and will flag it stale the moment any of these files change again.`;
|
|
5178
|
+
}
|
|
5179
|
+
catch {
|
|
5180
|
+
// Never let auto-capture break the actual test result the caller asked for.
|
|
5181
|
+
return "";
|
|
5182
|
+
}
|
|
5183
|
+
}
|
|
4738
5184
|
async function handleTestOracle(args) {
|
|
4739
5185
|
const command = args.command || "npm test";
|
|
4740
5186
|
const workspaceRoot = process.cwd();
|
|
@@ -4745,6 +5191,7 @@ async function handleTestOracle(args) {
|
|
|
4745
5191
|
try {
|
|
4746
5192
|
const out = execSync(command, { cwd: workspaceRoot, encoding: "utf8", timeout: 30000 });
|
|
4747
5193
|
resultText = `All tests passed!\n\nOutput:\n${out.substring(0, 1000)}`;
|
|
5194
|
+
resultText += autoStoreGreenTests(command, workspaceRoot);
|
|
4748
5195
|
}
|
|
4749
5196
|
catch (e) {
|
|
4750
5197
|
const stdout = e.stdout || "";
|
|
@@ -5060,6 +5507,41 @@ async function handleFileIntentIndex(args) {
|
|
|
5060
5507
|
}
|
|
5061
5508
|
}
|
|
5062
5509
|
const COG_MAP_FILE = path.join(os.homedir(), ".lemma-cache", "cog_map.json");
|
|
5510
|
+
/**
|
|
5511
|
+
* Best-effort domain for a repo-relative file path, so store_memory and the test-oracle
|
|
5512
|
+
* auto-store below don't require a human to type `domain` by hand every time.
|
|
5513
|
+
*
|
|
5514
|
+
* Prefers the persisted Cognitive Map (built via `cognitive_map` action "build") when it
|
|
5515
|
+
* has this exact file listed — that reflects an actual workspace scan. Falls back to the
|
|
5516
|
+
* same lightweight substring heuristic "build" uses, so a project that never ran "build"
|
|
5517
|
+
* still gets a reasonable tag instead of none at all. Returns undefined rather than
|
|
5518
|
+
* "general" when nothing matches — an absent domain stays visible to every domain-scoped
|
|
5519
|
+
* search (see BrainEntry.domain), while a wrong guess would actively mislead one.
|
|
5520
|
+
*/
|
|
5521
|
+
function deriveDomainForFile(relPath) {
|
|
5522
|
+
const norm = relPath.replace(/^\//, "");
|
|
5523
|
+
if (fs.existsSync(COG_MAP_FILE)) {
|
|
5524
|
+
try {
|
|
5525
|
+
const map = JSON.parse(fs.readFileSync(COG_MAP_FILE, "utf8"));
|
|
5526
|
+
for (const node of Object.values(map.nodes || {})) {
|
|
5527
|
+
if (node.files.includes(norm))
|
|
5528
|
+
return node.domain;
|
|
5529
|
+
}
|
|
5530
|
+
}
|
|
5531
|
+
catch { /* fall through to heuristic */ }
|
|
5532
|
+
}
|
|
5533
|
+
if (norm.includes("mcp"))
|
|
5534
|
+
return "mcp";
|
|
5535
|
+
if (norm.includes("subconscious") || norm.includes("brain"))
|
|
5536
|
+
return "memory";
|
|
5537
|
+
if (norm.includes("security") || norm.includes("scrubber"))
|
|
5538
|
+
return "security";
|
|
5539
|
+
if (norm.includes("pr-review"))
|
|
5540
|
+
return "pr-review";
|
|
5541
|
+
if (norm.includes("utils"))
|
|
5542
|
+
return "utils";
|
|
5543
|
+
return undefined;
|
|
5544
|
+
}
|
|
5063
5545
|
async function handleCognitiveMap(args) {
|
|
5064
5546
|
const action = args.action;
|
|
5065
5547
|
const domain = args.domain;
|