@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/cjs/mcp/tools.js
CHANGED
|
@@ -79,6 +79,7 @@ const StateHashCache_1 = require("../utils/StateHashCache");
|
|
|
79
79
|
const TokenReceipt_1 = require("../utils/TokenReceipt");
|
|
80
80
|
const CommandOutputDistiller_1 = require("../utils/CommandOutputDistiller");
|
|
81
81
|
const PatchMatcher_1 = require("../utils/PatchMatcher");
|
|
82
|
+
const tasks_1 = require("./tasks");
|
|
82
83
|
const WorkspaceSearch_1 = require("../utils/WorkspaceSearch");
|
|
83
84
|
const LocalPrefilter_1 = require("../utils/LocalPrefilter");
|
|
84
85
|
const ReadWorkspaceCache_1 = require("../utils/ReadWorkspaceCache");
|
|
@@ -110,7 +111,7 @@ function isProUser() {
|
|
|
110
111
|
}
|
|
111
112
|
const FREE_TOOLS = new Set([
|
|
112
113
|
// Cache & Memory — the hook that shows instant value
|
|
113
|
-
"smarter_cache", "state_hash_cache", "token_receipt", "search_memory", "store_memory", "get_project_history",
|
|
114
|
+
"smarter_cache", "state_hash_cache", "token_receipt", "search_memory", "store_memory", "downvote_memory", "get_project_history",
|
|
114
115
|
// Token optimization — shows what they're saving
|
|
115
116
|
"token_budget", "squeeze_prompt", "turbosqueeze",
|
|
116
117
|
// Utility — just enough to function
|
|
@@ -135,9 +136,94 @@ const PRO_GATE_MESSAGE = [
|
|
|
135
136
|
"🚀 Get Lemma Pro:",
|
|
136
137
|
" https://lemma.nxus.studio/upgrade",
|
|
137
138
|
].join("\n");
|
|
139
|
+
// ── Elicitation (spec 2026-07-28's recommended pattern for destructive actions) ────
|
|
140
|
+
//
|
|
141
|
+
// Set once by setupToolsHandlers so the standalone handler functions below — which don't
|
|
142
|
+
// otherwise see the Server instance — can ask the connected client to confirm before a
|
|
143
|
+
// mutating tool runs.
|
|
144
|
+
let mcpServerRef = null;
|
|
145
|
+
/**
|
|
146
|
+
* Ask the client to confirm a destructive action before it happens.
|
|
147
|
+
*
|
|
148
|
+
* Mirrors the defensive shape of trySamplingContext() in src/mcp/index.ts: most clients
|
|
149
|
+
* today don't support elicitation, so any failure (unsupported capability, timeout,
|
|
150
|
+
* malformed response) must fall back to "proceed without confirmation" rather than block
|
|
151
|
+
* or error out the tool. This is a courtesy prompt for clients that support it, not a
|
|
152
|
+
* security boundary — the allowlist/path-safety checks each handler already does are that.
|
|
153
|
+
*/
|
|
154
|
+
async function tryElicitConfirmation(summary) {
|
|
155
|
+
if (!mcpServerRef)
|
|
156
|
+
return { supported: false, confirmed: true };
|
|
157
|
+
try {
|
|
158
|
+
const result = await mcpServerRef.elicitInput({
|
|
159
|
+
message: summary,
|
|
160
|
+
requestedSchema: {
|
|
161
|
+
type: "object",
|
|
162
|
+
properties: {
|
|
163
|
+
confirm: {
|
|
164
|
+
type: "boolean",
|
|
165
|
+
title: "Proceed?",
|
|
166
|
+
description: "Confirm this action should be applied.",
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
required: ["confirm"],
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
if (result.action !== "accept") {
|
|
173
|
+
// "decline" or "cancel" — the user (or client policy) said no.
|
|
174
|
+
return { supported: true, confirmed: false };
|
|
175
|
+
}
|
|
176
|
+
const confirmed = result.content?.confirm !== false;
|
|
177
|
+
return { supported: true, confirmed };
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
(0, utils_1.logWarn)("elicitation", "Client does not support elicitation (or the request failed) — proceeding without confirmation");
|
|
181
|
+
return { supported: false, confirmed: true };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Ask the client to pick one of several candidates via a typed (oneOf) elicitation form,
|
|
186
|
+
* instead of the plain confirm/deny shape tryElicitConfirmation uses. Same defensive
|
|
187
|
+
* contract: no support, a decline, or a malformed response all just mean "couldn't
|
|
188
|
+
* resolve it this way" — the caller falls back to its own error/default behavior, never
|
|
189
|
+
* blocks or throws.
|
|
190
|
+
*/
|
|
191
|
+
async function tryElicitChoice(message, options) {
|
|
192
|
+
if (!mcpServerRef || options.length === 0)
|
|
193
|
+
return { supported: false, index: null };
|
|
194
|
+
try {
|
|
195
|
+
const result = await mcpServerRef.elicitInput({
|
|
196
|
+
message,
|
|
197
|
+
requestedSchema: {
|
|
198
|
+
type: "object",
|
|
199
|
+
properties: {
|
|
200
|
+
choice: {
|
|
201
|
+
type: "string",
|
|
202
|
+
title: "Which one?",
|
|
203
|
+
oneOf: options.map((opt, i) => ({ const: String(i), title: opt.title })),
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
required: ["choice"],
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
if (result.action !== "accept" || !result.content) {
|
|
210
|
+
return { supported: true, index: null };
|
|
211
|
+
}
|
|
212
|
+
const idx = Number(result.content.choice);
|
|
213
|
+
if (!Number.isInteger(idx) || idx < 0 || idx >= options.length) {
|
|
214
|
+
return { supported: true, index: null };
|
|
215
|
+
}
|
|
216
|
+
return { supported: true, index: idx };
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
(0, utils_1.logWarn)("elicitation", "Client does not support elicitation (or the request failed) — no choice made");
|
|
220
|
+
return { supported: false, index: null };
|
|
221
|
+
}
|
|
222
|
+
}
|
|
138
223
|
const toolDefinitions = [
|
|
139
224
|
{
|
|
140
225
|
name: "scrub_privacy",
|
|
226
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
141
227
|
description: "Mask sensitive data (PII, API Keys, Credentials) from a text block using Lemma's Privacy Firewall.",
|
|
142
228
|
inputSchema: {
|
|
143
229
|
type: "object",
|
|
@@ -149,7 +235,8 @@ const toolDefinitions = [
|
|
|
149
235
|
},
|
|
150
236
|
{
|
|
151
237
|
name: "search_memory",
|
|
152
|
-
|
|
238
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
239
|
+
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.",
|
|
153
240
|
inputSchema: {
|
|
154
241
|
type: "object",
|
|
155
242
|
properties: {
|
|
@@ -160,12 +247,17 @@ const toolDefinitions = [
|
|
|
160
247
|
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.",
|
|
161
248
|
default: 0.75,
|
|
162
249
|
},
|
|
250
|
+
domain: {
|
|
251
|
+
type: "string",
|
|
252
|
+
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.",
|
|
253
|
+
},
|
|
163
254
|
},
|
|
164
255
|
required: ["query"],
|
|
165
256
|
},
|
|
166
257
|
},
|
|
167
258
|
{
|
|
168
259
|
name: "store_memory",
|
|
260
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
169
261
|
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.",
|
|
170
262
|
inputSchema: {
|
|
171
263
|
type: "object",
|
|
@@ -211,12 +303,29 @@ const toolDefinitions = [
|
|
|
211
303
|
},
|
|
212
304
|
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.",
|
|
213
305
|
},
|
|
306
|
+
domain: {
|
|
307
|
+
type: "string",
|
|
308
|
+
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).",
|
|
309
|
+
},
|
|
214
310
|
},
|
|
215
311
|
required: ["query", "response"],
|
|
216
312
|
},
|
|
217
313
|
},
|
|
314
|
+
{
|
|
315
|
+
name: "downvote_memory",
|
|
316
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
317
|
+
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.",
|
|
318
|
+
inputSchema: {
|
|
319
|
+
type: "object",
|
|
320
|
+
properties: {
|
|
321
|
+
id: { type: "string", description: "The memory's id, from a prior search_memory result." },
|
|
322
|
+
},
|
|
323
|
+
required: ["id"],
|
|
324
|
+
},
|
|
325
|
+
},
|
|
218
326
|
{
|
|
219
327
|
name: "get_routing_advice",
|
|
328
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
220
329
|
description: "Analyzes a prompt and suggests the best model based on Lemma's Complexity Router.",
|
|
221
330
|
inputSchema: {
|
|
222
331
|
type: "object",
|
|
@@ -226,9 +335,20 @@ const toolDefinitions = [
|
|
|
226
335
|
},
|
|
227
336
|
required: ["prompt"],
|
|
228
337
|
},
|
|
338
|
+
outputSchema: {
|
|
339
|
+
type: "object",
|
|
340
|
+
properties: {
|
|
341
|
+
recommendedModel: { type: "string", description: "The model the router recommends" },
|
|
342
|
+
complexity: { type: "string", enum: ["low", "high"], description: "Estimated complexity of the prompt" },
|
|
343
|
+
intendedModel: { type: ["string", "null"], description: "The model passed in intended_model, or null if omitted" },
|
|
344
|
+
switched: { type: "boolean", description: "true if recommendedModel differs from intendedModel" },
|
|
345
|
+
},
|
|
346
|
+
required: ["recommendedModel", "complexity", "switched"],
|
|
347
|
+
},
|
|
229
348
|
},
|
|
230
349
|
{
|
|
231
350
|
name: "auto_heal",
|
|
351
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
232
352
|
description: "Diagnose and auto-heal the latest local server crash registered in Lemma's context logs.",
|
|
233
353
|
inputSchema: {
|
|
234
354
|
type: "object",
|
|
@@ -239,6 +359,7 @@ const toolDefinitions = [
|
|
|
239
359
|
},
|
|
240
360
|
{
|
|
241
361
|
name: "read_workspace_file",
|
|
362
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
242
363
|
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.",
|
|
243
364
|
inputSchema: {
|
|
244
365
|
type: "object",
|
|
@@ -255,6 +376,7 @@ const toolDefinitions = [
|
|
|
255
376
|
},
|
|
256
377
|
{
|
|
257
378
|
name: "write_workspace_file",
|
|
379
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
258
380
|
description: "Write full contents to a file inside the workspace. Creates parent directories automatically.",
|
|
259
381
|
inputSchema: {
|
|
260
382
|
type: "object",
|
|
@@ -267,6 +389,7 @@ const toolDefinitions = [
|
|
|
267
389
|
},
|
|
268
390
|
{
|
|
269
391
|
name: "create_workspace_file",
|
|
392
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
270
393
|
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.",
|
|
271
394
|
inputSchema: {
|
|
272
395
|
type: "object",
|
|
@@ -279,6 +402,7 @@ const toolDefinitions = [
|
|
|
279
402
|
},
|
|
280
403
|
{
|
|
281
404
|
name: "apply_workspace_patch",
|
|
405
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
282
406
|
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.",
|
|
283
407
|
inputSchema: {
|
|
284
408
|
type: "object",
|
|
@@ -292,6 +416,8 @@ const toolDefinitions = [
|
|
|
292
416
|
},
|
|
293
417
|
{
|
|
294
418
|
name: "run_workspace_command",
|
|
419
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
420
|
+
execution: { taskSupport: "optional" },
|
|
295
421
|
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.",
|
|
296
422
|
inputSchema: {
|
|
297
423
|
type: "object",
|
|
@@ -313,6 +439,7 @@ const toolDefinitions = [
|
|
|
313
439
|
},
|
|
314
440
|
{
|
|
315
441
|
name: "output_region",
|
|
442
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
316
443
|
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.",
|
|
317
444
|
inputSchema: {
|
|
318
445
|
type: "object",
|
|
@@ -328,6 +455,7 @@ const toolDefinitions = [
|
|
|
328
455
|
},
|
|
329
456
|
{
|
|
330
457
|
name: "list_workspace_dir",
|
|
458
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
331
459
|
description: "List files and subdirectories recursively to navigate the repository structure.",
|
|
332
460
|
inputSchema: {
|
|
333
461
|
type: "object",
|
|
@@ -339,6 +467,7 @@ const toolDefinitions = [
|
|
|
339
467
|
},
|
|
340
468
|
{
|
|
341
469
|
name: "search_workspace",
|
|
470
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
342
471
|
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.",
|
|
343
472
|
inputSchema: {
|
|
344
473
|
type: "object",
|
|
@@ -352,6 +481,7 @@ const toolDefinitions = [
|
|
|
352
481
|
},
|
|
353
482
|
{
|
|
354
483
|
name: "squeeze_prompt",
|
|
484
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
355
485
|
description: "Compress code blocks, comments, and boilerplate in any prompt. Saves up to 80% tokens.",
|
|
356
486
|
inputSchema: {
|
|
357
487
|
type: "object",
|
|
@@ -364,6 +494,7 @@ const toolDefinitions = [
|
|
|
364
494
|
},
|
|
365
495
|
{
|
|
366
496
|
name: "get_project_onboarding",
|
|
497
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
367
498
|
description: "Fetch dynamic architectural and stack overview of the current project in markdown.",
|
|
368
499
|
inputSchema: {
|
|
369
500
|
type: "object",
|
|
@@ -372,6 +503,7 @@ const toolDefinitions = [
|
|
|
372
503
|
},
|
|
373
504
|
{
|
|
374
505
|
name: "get_project_history",
|
|
506
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
375
507
|
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.",
|
|
376
508
|
inputSchema: {
|
|
377
509
|
type: "object",
|
|
@@ -382,6 +514,7 @@ const toolDefinitions = [
|
|
|
382
514
|
},
|
|
383
515
|
{
|
|
384
516
|
name: "get_ast_hologram",
|
|
517
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
385
518
|
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.",
|
|
386
519
|
inputSchema: {
|
|
387
520
|
type: "object",
|
|
@@ -398,6 +531,7 @@ const toolDefinitions = [
|
|
|
398
531
|
},
|
|
399
532
|
{
|
|
400
533
|
name: "validate_patch_sandbox",
|
|
534
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
401
535
|
description: "Validate proposed code in isolated sandbox via tsc + syntax check before applying.",
|
|
402
536
|
inputSchema: {
|
|
403
537
|
type: "object",
|
|
@@ -410,6 +544,7 @@ const toolDefinitions = [
|
|
|
410
544
|
},
|
|
411
545
|
{
|
|
412
546
|
name: "query_hybrid_consensus",
|
|
547
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
413
548
|
description: "Search The Brain before reasoning. Brain HIT returns cached answer instantly. Brain MISS proceeds to cloud.",
|
|
414
549
|
inputSchema: {
|
|
415
550
|
type: "object",
|
|
@@ -423,6 +558,7 @@ const toolDefinitions = [
|
|
|
423
558
|
},
|
|
424
559
|
{
|
|
425
560
|
name: "get_telepathic_hints",
|
|
561
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
426
562
|
description: "Surface relevant past solutions from The Brain based on the active file path.",
|
|
427
563
|
inputSchema: {
|
|
428
564
|
type: "object",
|
|
@@ -435,6 +571,7 @@ const toolDefinitions = [
|
|
|
435
571
|
},
|
|
436
572
|
{
|
|
437
573
|
name: "summarize_long_text",
|
|
574
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
438
575
|
description: "Summarize long text locally using Ollama. Compresses verbose content into concise summaries, saving context window for subsequent turns.",
|
|
439
576
|
inputSchema: {
|
|
440
577
|
type: "object",
|
|
@@ -447,6 +584,7 @@ const toolDefinitions = [
|
|
|
447
584
|
},
|
|
448
585
|
{
|
|
449
586
|
name: "prune_conversation_history",
|
|
587
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
450
588
|
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.",
|
|
451
589
|
inputSchema: {
|
|
452
590
|
type: "object",
|
|
@@ -467,6 +605,7 @@ const toolDefinitions = [
|
|
|
467
605
|
},
|
|
468
606
|
{
|
|
469
607
|
name: "diff_only",
|
|
608
|
+
annotations: { readOnlyHint: true, idempotentHint: false, openWorldHint: false },
|
|
470
609
|
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.",
|
|
471
610
|
inputSchema: {
|
|
472
611
|
type: "object",
|
|
@@ -479,6 +618,7 @@ const toolDefinitions = [
|
|
|
479
618
|
},
|
|
480
619
|
{
|
|
481
620
|
name: "batch_tool_calls",
|
|
621
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
482
622
|
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.",
|
|
483
623
|
inputSchema: {
|
|
484
624
|
type: "object",
|
|
@@ -501,6 +641,7 @@ const toolDefinitions = [
|
|
|
501
641
|
},
|
|
502
642
|
{
|
|
503
643
|
name: "turbosqueeze",
|
|
644
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
504
645
|
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.",
|
|
505
646
|
inputSchema: {
|
|
506
647
|
type: "object",
|
|
@@ -515,6 +656,7 @@ const toolDefinitions = [
|
|
|
515
656
|
},
|
|
516
657
|
{
|
|
517
658
|
name: "get_symbol_surgical_context",
|
|
659
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
518
660
|
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.",
|
|
519
661
|
inputSchema: {
|
|
520
662
|
type: "object",
|
|
@@ -527,6 +669,7 @@ const toolDefinitions = [
|
|
|
527
669
|
},
|
|
528
670
|
{
|
|
529
671
|
name: "wormhole_squeeze",
|
|
672
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
530
673
|
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.",
|
|
531
674
|
inputSchema: {
|
|
532
675
|
type: "object",
|
|
@@ -538,6 +681,7 @@ const toolDefinitions = [
|
|
|
538
681
|
},
|
|
539
682
|
{
|
|
540
683
|
name: "generate_executive_roi_report",
|
|
684
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
541
685
|
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.",
|
|
542
686
|
inputSchema: {
|
|
543
687
|
type: "object",
|
|
@@ -550,6 +694,7 @@ const toolDefinitions = [
|
|
|
550
694
|
},
|
|
551
695
|
{
|
|
552
696
|
name: "surgical_ast_insert",
|
|
697
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
553
698
|
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.",
|
|
554
699
|
inputSchema: {
|
|
555
700
|
type: "object",
|
|
@@ -565,6 +710,7 @@ const toolDefinitions = [
|
|
|
565
710
|
},
|
|
566
711
|
{
|
|
567
712
|
name: "local_semantic_autofix",
|
|
713
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
568
714
|
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.",
|
|
569
715
|
inputSchema: {
|
|
570
716
|
type: "object",
|
|
@@ -576,6 +722,7 @@ const toolDefinitions = [
|
|
|
576
722
|
},
|
|
577
723
|
{
|
|
578
724
|
name: "compress_context",
|
|
725
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
579
726
|
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.",
|
|
580
727
|
inputSchema: {
|
|
581
728
|
type: "object",
|
|
@@ -592,6 +739,7 @@ const toolDefinitions = [
|
|
|
592
739
|
},
|
|
593
740
|
{
|
|
594
741
|
name: "smarter_cache",
|
|
742
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
595
743
|
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.",
|
|
596
744
|
inputSchema: {
|
|
597
745
|
type: "object",
|
|
@@ -602,9 +750,25 @@ const toolDefinitions = [
|
|
|
602
750
|
},
|
|
603
751
|
required: ["query"],
|
|
604
752
|
},
|
|
753
|
+
outputSchema: {
|
|
754
|
+
type: "object",
|
|
755
|
+
properties: {
|
|
756
|
+
hit: { type: "boolean", description: "true si hubo un hit por encima del threshold" },
|
|
757
|
+
source: { type: "string", enum: ["lemma-brain", "llm-call-required", "cache-unavailable"], description: "De dónde salió (o no) la respuesta" },
|
|
758
|
+
similarity: { type: "number", description: "Similitud del mejor match (0.0-1.0). Ausente en la rama de error." },
|
|
759
|
+
threshold: { type: "number", description: "Threshold usado. Ausente en la rama de error." },
|
|
760
|
+
answer: { type: ["string", "null"], description: "Respuesta cacheada si hit=true; null si no. Ausente en la rama de error." },
|
|
761
|
+
tokensSaved: { type: "number", description: "Solo presente cuando hit=true" },
|
|
762
|
+
tokensSavedFormatted: { type: "string", description: "Solo presente cuando hit=true" },
|
|
763
|
+
hint: { type: "string", description: "Sugerencia de siguiente paso; presente en miss y en error" },
|
|
764
|
+
error: { type: "string", description: "Mensaje de error; solo presente si el brain local falló" },
|
|
765
|
+
},
|
|
766
|
+
required: ["hit", "source"],
|
|
767
|
+
},
|
|
605
768
|
},
|
|
606
769
|
{
|
|
607
770
|
name: "state_hash_cache",
|
|
771
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
608
772
|
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.",
|
|
609
773
|
inputSchema: {
|
|
610
774
|
type: "object",
|
|
@@ -616,9 +780,24 @@ const toolDefinitions = [
|
|
|
616
780
|
},
|
|
617
781
|
required: ["action", "query", "filePaths"],
|
|
618
782
|
},
|
|
783
|
+
outputSchema: {
|
|
784
|
+
type: "object",
|
|
785
|
+
properties: {
|
|
786
|
+
status: { type: "string", enum: ["hit", "miss", "stored"], description: "Resultado de lookup (hit/miss) o de store (stored)" },
|
|
787
|
+
answer: { type: "string", description: "Solo presente cuando status='hit'" },
|
|
788
|
+
note: { type: "string", description: "Solo presente cuando status='hit'" },
|
|
789
|
+
tokensSaved: { type: "number", description: "Solo presente cuando status='hit'" },
|
|
790
|
+
reason: { type: "string", description: "Solo presente cuando status='miss'" },
|
|
791
|
+
hint: { type: "string", description: "Solo presente cuando status='miss'" },
|
|
792
|
+
id: { type: "string", description: "Solo presente cuando status='stored'" },
|
|
793
|
+
filesTracked: { type: "array", items: { type: "string" }, description: "Solo presente cuando status='stored'" },
|
|
794
|
+
},
|
|
795
|
+
required: ["status"],
|
|
796
|
+
},
|
|
619
797
|
},
|
|
620
798
|
{
|
|
621
799
|
name: "token_receipt",
|
|
800
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
622
801
|
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.",
|
|
623
802
|
inputSchema: {
|
|
624
803
|
type: "object",
|
|
@@ -627,9 +806,42 @@ const toolDefinitions = [
|
|
|
627
806
|
limit: { type: "number", description: "Cuántos eventos recientes incluir en el detalle", default: 20 },
|
|
628
807
|
},
|
|
629
808
|
},
|
|
809
|
+
outputSchema: {
|
|
810
|
+
type: "object",
|
|
811
|
+
properties: {
|
|
812
|
+
totalEvents: { type: "number", description: "Total de eventos registrados en la sesión" },
|
|
813
|
+
byType: {
|
|
814
|
+
type: "object",
|
|
815
|
+
properties: {
|
|
816
|
+
exact_cache_hit: { type: "number" },
|
|
817
|
+
semantic_cache_hit: { type: "number" },
|
|
818
|
+
file_read: { type: "number" },
|
|
819
|
+
reasoning: { type: "number" },
|
|
820
|
+
tool_call: { type: "number" },
|
|
821
|
+
},
|
|
822
|
+
description: "Conteo de eventos por tipo de origen",
|
|
823
|
+
},
|
|
824
|
+
recentEvents: {
|
|
825
|
+
type: "array",
|
|
826
|
+
items: {
|
|
827
|
+
type: "object",
|
|
828
|
+
properties: {
|
|
829
|
+
type: { type: "string" },
|
|
830
|
+
label: { type: "string" },
|
|
831
|
+
timestamp: { type: "number" },
|
|
832
|
+
meta: { type: "object" },
|
|
833
|
+
},
|
|
834
|
+
},
|
|
835
|
+
description: "Últimos `limit` eventos, en orden cronológico",
|
|
836
|
+
},
|
|
837
|
+
note: { type: "string" },
|
|
838
|
+
},
|
|
839
|
+
required: ["totalEvents", "byType", "recentEvents"],
|
|
840
|
+
},
|
|
630
841
|
},
|
|
631
842
|
{
|
|
632
843
|
name: "token_budget",
|
|
844
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
633
845
|
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.",
|
|
634
846
|
inputSchema: {
|
|
635
847
|
type: "object",
|
|
@@ -638,6 +850,7 @@ const toolDefinitions = [
|
|
|
638
850
|
},
|
|
639
851
|
{
|
|
640
852
|
name: "entropy_score",
|
|
853
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
641
854
|
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.",
|
|
642
855
|
inputSchema: {
|
|
643
856
|
type: "object",
|
|
@@ -649,6 +862,7 @@ const toolDefinitions = [
|
|
|
649
862
|
},
|
|
650
863
|
{
|
|
651
864
|
name: "coupling_radar",
|
|
865
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
652
866
|
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.",
|
|
653
867
|
inputSchema: {
|
|
654
868
|
type: "object",
|
|
@@ -660,6 +874,7 @@ const toolDefinitions = [
|
|
|
660
874
|
},
|
|
661
875
|
{
|
|
662
876
|
name: "pattern_fossil",
|
|
877
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
663
878
|
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.",
|
|
664
879
|
inputSchema: {
|
|
665
880
|
type: "object",
|
|
@@ -670,6 +885,7 @@ const toolDefinitions = [
|
|
|
670
885
|
},
|
|
671
886
|
{
|
|
672
887
|
name: "git_heatmap_risk",
|
|
888
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
673
889
|
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.",
|
|
674
890
|
inputSchema: {
|
|
675
891
|
type: "object",
|
|
@@ -681,6 +897,7 @@ const toolDefinitions = [
|
|
|
681
897
|
},
|
|
682
898
|
{
|
|
683
899
|
name: "precrime_static",
|
|
900
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
684
901
|
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.",
|
|
685
902
|
inputSchema: {
|
|
686
903
|
type: "object",
|
|
@@ -693,6 +910,7 @@ const toolDefinitions = [
|
|
|
693
910
|
},
|
|
694
911
|
{
|
|
695
912
|
name: "semantic_dedup_guard",
|
|
913
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
696
914
|
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.",
|
|
697
915
|
inputSchema: {
|
|
698
916
|
type: "object",
|
|
@@ -706,6 +924,7 @@ const toolDefinitions = [
|
|
|
706
924
|
},
|
|
707
925
|
{
|
|
708
926
|
name: "dead_export_necromancer",
|
|
927
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
709
928
|
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.",
|
|
710
929
|
inputSchema: {
|
|
711
930
|
type: "object",
|
|
@@ -716,6 +935,7 @@ const toolDefinitions = [
|
|
|
716
935
|
},
|
|
717
936
|
{
|
|
718
937
|
name: "review_diff",
|
|
938
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
719
939
|
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.",
|
|
720
940
|
inputSchema: {
|
|
721
941
|
type: "object",
|
|
@@ -729,6 +949,7 @@ const toolDefinitions = [
|
|
|
729
949
|
},
|
|
730
950
|
{
|
|
731
951
|
name: "review_pr",
|
|
952
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
732
953
|
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.",
|
|
733
954
|
inputSchema: {
|
|
734
955
|
type: "object",
|
|
@@ -745,6 +966,7 @@ const toolDefinitions = [
|
|
|
745
966
|
},
|
|
746
967
|
{
|
|
747
968
|
name: "pr_status",
|
|
969
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
748
970
|
description: "Get the status and detailed results of a previously reviewed PR. Returns score, verdict, findings, and approval status.",
|
|
749
971
|
inputSchema: {
|
|
750
972
|
type: "object",
|
|
@@ -758,6 +980,7 @@ const toolDefinitions = [
|
|
|
758
980
|
},
|
|
759
981
|
{
|
|
760
982
|
name: "generate_pr_workflow",
|
|
983
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
761
984
|
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.",
|
|
762
985
|
inputSchema: {
|
|
763
986
|
type: "object",
|
|
@@ -772,6 +995,7 @@ const toolDefinitions = [
|
|
|
772
995
|
},
|
|
773
996
|
{
|
|
774
997
|
name: "depgraph",
|
|
998
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
775
999
|
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.",
|
|
776
1000
|
inputSchema: {
|
|
777
1001
|
type: "object",
|
|
@@ -788,6 +1012,7 @@ const toolDefinitions = [
|
|
|
788
1012
|
},
|
|
789
1013
|
{
|
|
790
1014
|
name: "affected_tests",
|
|
1015
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
791
1016
|
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).",
|
|
792
1017
|
inputSchema: {
|
|
793
1018
|
type: "object",
|
|
@@ -796,9 +1021,24 @@ const toolDefinitions = [
|
|
|
796
1021
|
baseRef: { type: "string", description: "Additional git ref to diff against, e.g. 'main'. The working tree is always included." },
|
|
797
1022
|
},
|
|
798
1023
|
},
|
|
1024
|
+
outputSchema: {
|
|
1025
|
+
type: "object",
|
|
1026
|
+
properties: {
|
|
1027
|
+
changed: { type: "array", items: { type: "string" }, description: "Files the diff touched, repo-relative" },
|
|
1028
|
+
directTests: { type: "array", items: { type: "string" }, description: "Changed files that are themselves tests" },
|
|
1029
|
+
affected: { type: "array", items: { type: "string" }, description: "Test files that transitively import a changed file" },
|
|
1030
|
+
totalTests: { type: "number", description: "Total test files in the repo" },
|
|
1031
|
+
runner: { type: "string", enum: ["jest", "vitest", "unknown"] },
|
|
1032
|
+
command: { type: ["string", "null"], description: "Command to run just the affected tests, or null if the full suite is recommended" },
|
|
1033
|
+
fullSuiteReason: { type: ["string", "null"], description: "Why the full suite is recommended instead, when command is null" },
|
|
1034
|
+
ungraphed: { type: "array", items: { type: "string" }, description: "Changed files absent from the import graph — blast radius unknown" },
|
|
1035
|
+
},
|
|
1036
|
+
required: ["changed", "directTests", "affected", "totalTests", "runner", "command", "fullSuiteReason", "ungraphed"],
|
|
1037
|
+
},
|
|
799
1038
|
},
|
|
800
1039
|
{
|
|
801
1040
|
name: "refactor",
|
|
1041
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
802
1042
|
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.",
|
|
803
1043
|
inputSchema: {
|
|
804
1044
|
type: "object",
|
|
@@ -816,6 +1056,7 @@ const toolDefinitions = [
|
|
|
816
1056
|
},
|
|
817
1057
|
{
|
|
818
1058
|
name: "smart_file_slice",
|
|
1059
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
819
1060
|
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.",
|
|
820
1061
|
inputSchema: {
|
|
821
1062
|
type: "object",
|
|
@@ -829,6 +1070,7 @@ const toolDefinitions = [
|
|
|
829
1070
|
},
|
|
830
1071
|
{
|
|
831
1072
|
name: "test_oracle",
|
|
1073
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
832
1074
|
description: "Execute unit tests locally and output ONLY the failing tests and compressed stack traces, keeping context clean.",
|
|
833
1075
|
inputSchema: {
|
|
834
1076
|
type: "object",
|
|
@@ -839,6 +1081,7 @@ const toolDefinitions = [
|
|
|
839
1081
|
},
|
|
840
1082
|
{
|
|
841
1083
|
name: "schema_extract",
|
|
1084
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
842
1085
|
description: "Extract high-level schemas, Zod types, interfaces, or database models from a file using the TypeScript AST, removing all implementation code.",
|
|
843
1086
|
inputSchema: {
|
|
844
1087
|
type: "object",
|
|
@@ -850,6 +1093,7 @@ const toolDefinitions = [
|
|
|
850
1093
|
},
|
|
851
1094
|
{
|
|
852
1095
|
name: "changelog_auto",
|
|
1096
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
853
1097
|
description: "Generate a clean, token-efficient changelog summary from git logs based on Conventional Commits.",
|
|
854
1098
|
inputSchema: {
|
|
855
1099
|
type: "object",
|
|
@@ -860,6 +1104,7 @@ const toolDefinitions = [
|
|
|
860
1104
|
},
|
|
861
1105
|
{
|
|
862
1106
|
name: "spec_to_stub",
|
|
1107
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
863
1108
|
description: "Parse a TypeScript interface/type definition from a file and output a template mock/stub object configuration using pure AST analysis.",
|
|
864
1109
|
inputSchema: {
|
|
865
1110
|
type: "object",
|
|
@@ -872,6 +1117,7 @@ const toolDefinitions = [
|
|
|
872
1117
|
},
|
|
873
1118
|
{
|
|
874
1119
|
name: "env_snapshot",
|
|
1120
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
875
1121
|
description: "Capture environmental state like Node version, dependencies in package.json, and variables declared in .env files (hiding actual values/credentials).",
|
|
876
1122
|
inputSchema: {
|
|
877
1123
|
type: "object",
|
|
@@ -880,6 +1126,7 @@ const toolDefinitions = [
|
|
|
880
1126
|
},
|
|
881
1127
|
{
|
|
882
1128
|
name: "migration_tracer",
|
|
1129
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
883
1130
|
description: "Inspect changes in database schemas (tables, columns) across local migrations (e.g. Prisma migration directories).",
|
|
884
1131
|
inputSchema: {
|
|
885
1132
|
type: "object",
|
|
@@ -890,6 +1137,7 @@ const toolDefinitions = [
|
|
|
890
1137
|
},
|
|
891
1138
|
{
|
|
892
1139
|
name: "multi_file_patch",
|
|
1140
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
893
1141
|
description: "Apply search-and-replace patches to multiple workspace files in a single turn. Decreases round-trips for multi-file refactoring.",
|
|
894
1142
|
inputSchema: {
|
|
895
1143
|
type: "object",
|
|
@@ -914,6 +1162,7 @@ const toolDefinitions = [
|
|
|
914
1162
|
},
|
|
915
1163
|
{
|
|
916
1164
|
name: "file_intent_index",
|
|
1165
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
917
1166
|
description: "Map and index files inside the workspace by domain intent (e.g., auth, billing, routes, config) to speed up navigation.",
|
|
918
1167
|
inputSchema: {
|
|
919
1168
|
type: "object",
|
|
@@ -924,6 +1173,7 @@ const toolDefinitions = [
|
|
|
924
1173
|
},
|
|
925
1174
|
{
|
|
926
1175
|
name: "cognitive_map",
|
|
1176
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
927
1177
|
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.",
|
|
928
1178
|
inputSchema: {
|
|
929
1179
|
type: "object",
|
|
@@ -946,6 +1196,7 @@ const toolDefinitions = [
|
|
|
946
1196
|
},
|
|
947
1197
|
{
|
|
948
1198
|
name: "semantic_grep",
|
|
1199
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
949
1200
|
description: "Search the codebase for conceptual terms using natural language and BM25 token relevance instead of exact substring matching.",
|
|
950
1201
|
inputSchema: {
|
|
951
1202
|
type: "object",
|
|
@@ -959,6 +1210,7 @@ const toolDefinitions = [
|
|
|
959
1210
|
},
|
|
960
1211
|
{
|
|
961
1212
|
name: "imports_skeleton_resolver",
|
|
1213
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
962
1214
|
description: "Resolve all imported local files inside a file and print only their signatures (classes, functions, interfaces) in a consolidated view.",
|
|
963
1215
|
inputSchema: {
|
|
964
1216
|
type: "object",
|
|
@@ -970,6 +1222,7 @@ const toolDefinitions = [
|
|
|
970
1222
|
},
|
|
971
1223
|
{
|
|
972
1224
|
name: "ast_flow_visualizer",
|
|
1225
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
973
1226
|
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.",
|
|
974
1227
|
inputSchema: {
|
|
975
1228
|
type: "object",
|
|
@@ -982,6 +1235,7 @@ const toolDefinitions = [
|
|
|
982
1235
|
},
|
|
983
1236
|
{
|
|
984
1237
|
name: "file_fingerprint",
|
|
1238
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
985
1239
|
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.",
|
|
986
1240
|
inputSchema: {
|
|
987
1241
|
type: "object",
|
|
@@ -994,6 +1248,7 @@ const toolDefinitions = [
|
|
|
994
1248
|
},
|
|
995
1249
|
{
|
|
996
1250
|
name: "git_blame_heat",
|
|
1251
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
997
1252
|
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.",
|
|
998
1253
|
inputSchema: {
|
|
999
1254
|
type: "object",
|
|
@@ -1005,6 +1260,7 @@ const toolDefinitions = [
|
|
|
1005
1260
|
},
|
|
1006
1261
|
{
|
|
1007
1262
|
name: "type_coverage_report",
|
|
1263
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
1008
1264
|
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.",
|
|
1009
1265
|
inputSchema: {
|
|
1010
1266
|
type: "object",
|
|
@@ -1016,6 +1272,7 @@ const toolDefinitions = [
|
|
|
1016
1272
|
},
|
|
1017
1273
|
{
|
|
1018
1274
|
name: "import_tree_context",
|
|
1275
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
1019
1276
|
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.",
|
|
1020
1277
|
inputSchema: {
|
|
1021
1278
|
type: "object",
|
|
@@ -1028,6 +1285,7 @@ const toolDefinitions = [
|
|
|
1028
1285
|
},
|
|
1029
1286
|
{
|
|
1030
1287
|
name: "read_token_budgeted",
|
|
1288
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
1031
1289
|
description: "Read a file with a hard token budget. Returns only the most relevant sections up to maxTokens. Saves 70-90% vs full reads.",
|
|
1032
1290
|
inputSchema: {
|
|
1033
1291
|
type: "object",
|
|
@@ -1041,6 +1299,7 @@ const toolDefinitions = [
|
|
|
1041
1299
|
},
|
|
1042
1300
|
{
|
|
1043
1301
|
name: "bulk_file_digest",
|
|
1302
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
1044
1303
|
description: "Read multiple files in one call and return a compressed digest. Saves 80% round-trips vs reading files individually.",
|
|
1045
1304
|
inputSchema: {
|
|
1046
1305
|
type: "object",
|
|
@@ -1058,6 +1317,7 @@ const toolDefinitions = [
|
|
|
1058
1317
|
},
|
|
1059
1318
|
{
|
|
1060
1319
|
name: "auto_context_bundle",
|
|
1320
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
1061
1321
|
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.",
|
|
1062
1322
|
inputSchema: {
|
|
1063
1323
|
type: "object",
|
|
@@ -1069,6 +1329,7 @@ const toolDefinitions = [
|
|
|
1069
1329
|
},
|
|
1070
1330
|
{
|
|
1071
1331
|
name: "repeat_guard",
|
|
1332
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
1072
1333
|
description: "Check if a draft output is repetitive compared to recent responses. Returns similarity score and suggestion. Prevents duplicate explanations.",
|
|
1073
1334
|
inputSchema: {
|
|
1074
1335
|
type: "object",
|
|
@@ -1081,6 +1342,7 @@ const toolDefinitions = [
|
|
|
1081
1342
|
},
|
|
1082
1343
|
{
|
|
1083
1344
|
name: "conversation_checkpoint",
|
|
1345
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1084
1346
|
description: "Save or load session state. Prevents re-explaining context across turns. Save current progress or load last checkpoint.",
|
|
1085
1347
|
inputSchema: {
|
|
1086
1348
|
type: "object",
|
|
@@ -1096,6 +1358,7 @@ const toolDefinitions = [
|
|
|
1096
1358
|
},
|
|
1097
1359
|
{
|
|
1098
1360
|
name: "test_autofix_interceptor",
|
|
1361
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
1099
1362
|
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.",
|
|
1100
1363
|
inputSchema: {
|
|
1101
1364
|
type: "object",
|
|
@@ -1106,6 +1369,7 @@ const toolDefinitions = [
|
|
|
1106
1369
|
},
|
|
1107
1370
|
{
|
|
1108
1371
|
name: "prompt_pattern_cache",
|
|
1372
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
1109
1373
|
description: "Find or store optimized prompt templates. Reuses past prompt patterns to save 20-30% tokens on repetitive operations.",
|
|
1110
1374
|
inputSchema: {
|
|
1111
1375
|
type: "object",
|
|
@@ -1125,6 +1389,7 @@ const _infraToolDefs = (0, mcp_tools_1.getInfraToolDefinitions)();
|
|
|
1125
1389
|
// than in the main array because it must never itself be hidden.
|
|
1126
1390
|
const TOOLBOX_TOOL_DEFINITION = {
|
|
1127
1391
|
name: "lemma_toolbox",
|
|
1392
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
1128
1393
|
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.",
|
|
1129
1394
|
inputSchema: {
|
|
1130
1395
|
type: "object",
|
|
@@ -1186,6 +1451,7 @@ const toolHandlers = {
|
|
|
1186
1451
|
scrub_privacy: handleScrubPrivacy,
|
|
1187
1452
|
search_memory: handleSearchMemory,
|
|
1188
1453
|
store_memory: handleStoreMemory,
|
|
1454
|
+
downvote_memory: handleDownvoteMemory,
|
|
1189
1455
|
get_routing_advice: handleGetRoutingAdvice,
|
|
1190
1456
|
auto_heal: handleAutoHeal,
|
|
1191
1457
|
read_workspace_file: handleReadWorkspaceFile,
|
|
@@ -1330,11 +1596,12 @@ function receiptLabelMeta(name, args) {
|
|
|
1330
1596
|
return meta;
|
|
1331
1597
|
}
|
|
1332
1598
|
function setupToolsHandlers(server, onToolCall) {
|
|
1599
|
+
mcpServerRef = server;
|
|
1333
1600
|
(0, SpeculativeWarmer_1.registerSymbolExtractor)(extractSymbolsWithTsCompiler);
|
|
1334
1601
|
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
1335
1602
|
tools: toolDefinitionsArray,
|
|
1336
1603
|
}));
|
|
1337
|
-
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
1604
|
+
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request, extra) => {
|
|
1338
1605
|
const { name, arguments: args } = request.params;
|
|
1339
1606
|
const startTime = Date.now();
|
|
1340
1607
|
// ── Pro License Gate (granular: free tools work without license) ────────
|
|
@@ -1363,6 +1630,39 @@ function setupToolsHandlers(server, onToolCall) {
|
|
|
1363
1630
|
});
|
|
1364
1631
|
throw new Error(`Unknown tool: ${name}`);
|
|
1365
1632
|
}
|
|
1633
|
+
// ── Task-augmented call: hand off to tasks.ts instead of awaiting inline ────
|
|
1634
|
+
// A task-unaware client never sends `request.params.task`, so this only triggers when
|
|
1635
|
+
// the caller actually asked for it — today's synchronous behavior is unchanged otherwise.
|
|
1636
|
+
if (request.params.task && tasks_1.TASK_CAPABLE_TOOLS.has(name)) {
|
|
1637
|
+
const ledgerBefore = (0, TokenReceipt_1.getLedgerLength)();
|
|
1638
|
+
return (0, tasks_1.runAsTask)(name, (args || {}), request, extra.requestId, handler, (result, error) => {
|
|
1639
|
+
if (error) {
|
|
1640
|
+
onToolCall?.({
|
|
1641
|
+
tool: name,
|
|
1642
|
+
args: args || {},
|
|
1643
|
+
result: "ERROR",
|
|
1644
|
+
latency: Date.now() - startTime,
|
|
1645
|
+
error: error instanceof Error ? error.message.substring(0, 100) : String(error).substring(0, 100),
|
|
1646
|
+
});
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
if ((0, TokenReceipt_1.getLedgerLength)() === ledgerBefore) {
|
|
1650
|
+
(0, TokenReceipt_1.recordReceiptEvent)(receiptTypeForTool(name), name, {
|
|
1651
|
+
tool: name,
|
|
1652
|
+
...receiptLabelMeta(name, args),
|
|
1653
|
+
});
|
|
1654
|
+
}
|
|
1655
|
+
const tokensImpact = (0, utils_1.estimateTokensFromResult)(result);
|
|
1656
|
+
onToolCall?.({
|
|
1657
|
+
tool: name,
|
|
1658
|
+
args: args || {},
|
|
1659
|
+
result: "OK",
|
|
1660
|
+
latency: Date.now() - startTime,
|
|
1661
|
+
tokensImpact,
|
|
1662
|
+
});
|
|
1663
|
+
(0, reportSavings_1.reportCost)({ source: "toolResult", tokens: tokensImpact, toolName: name });
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1366
1666
|
try {
|
|
1367
1667
|
// Sampled around the handler so a tool that logs its own, more specific event
|
|
1368
1668
|
// (a cache hit, a miss) isn't double-counted by the generic entry below.
|
|
@@ -1398,6 +1698,9 @@ function setupToolsHandlers(server, onToolCall) {
|
|
|
1398
1698
|
throw error;
|
|
1399
1699
|
}
|
|
1400
1700
|
});
|
|
1701
|
+
// Registered after ListTools/CallTool: tests and other callers that assume "ListTools is
|
|
1702
|
+
// first, CallTool is second" (see tests/unit/tool-regressions.test.ts) key off that order.
|
|
1703
|
+
(0, tasks_1.registerTaskHandlers)(server);
|
|
1401
1704
|
}
|
|
1402
1705
|
// ── Tool Implementations ────────────────────────────────────────────
|
|
1403
1706
|
async function handleScrubPrivacy(args) {
|
|
@@ -1421,11 +1724,12 @@ async function handleSearchMemory(args) {
|
|
|
1421
1724
|
// formatting below then presents it as a reusable memory. Match smarter_cache's
|
|
1422
1725
|
// threshold so both paths agree on what counts as a hit.
|
|
1423
1726
|
const minSimilarity = typeof args?.minSimilarity === "number" ? args.minSimilarity : DEFAULT_MEMORY_SIMILARITY_FLOOR;
|
|
1727
|
+
const domain = typeof args?.domain === "string" && args.domain.trim() ? args.domain.trim() : undefined;
|
|
1424
1728
|
if (!query)
|
|
1425
1729
|
throw new Error("Query is required");
|
|
1426
1730
|
try {
|
|
1427
1731
|
const brain = (0, TheBrainV2_1.getBrain)();
|
|
1428
|
-
const results = brain.search(query, limit, minSimilarity, { projectId: (0, TheBrainV2_1.deriveProjectId)() });
|
|
1732
|
+
const results = brain.search(query, limit, minSimilarity, { projectId: (0, TheBrainV2_1.deriveProjectId)(), domain });
|
|
1429
1733
|
if (results.length === 0) {
|
|
1430
1734
|
(0, TokenReceipt_1.recordReceiptEvent)("reasoning", query.substring(0, 100), {
|
|
1431
1735
|
tool: "search_memory",
|
|
@@ -1449,7 +1753,7 @@ async function handleSearchMemory(args) {
|
|
|
1449
1753
|
const staleCount = r.claims.filter((c) => !c.fresh).length;
|
|
1450
1754
|
return `\nClaims (${r.claims.length - staleCount}/${r.claims.length} still fresh — trust only the ✓ ones):\n${lines.join("\n")}`;
|
|
1451
1755
|
};
|
|
1452
|
-
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)}`;
|
|
1756
|
+
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)}`;
|
|
1453
1757
|
if (fresh.length > 0) {
|
|
1454
1758
|
// Only a fresh hit (or an untracked, purely conceptual entry) counts as a real
|
|
1455
1759
|
// avoided re-investigation — this is the only case worth crediting as savings.
|
|
@@ -1497,11 +1801,15 @@ async function handleStoreMemory(args) {
|
|
|
1497
1801
|
const claims = Array.isArray(args?.claims)
|
|
1498
1802
|
? args.claims.filter((c) => c?.text)
|
|
1499
1803
|
: undefined;
|
|
1804
|
+
const explicitDomain = typeof args?.domain === "string" && args.domain.trim() ? args.domain.trim() : undefined;
|
|
1805
|
+
// Auto-derive from the first tracked file when the caller didn't say — see
|
|
1806
|
+
// deriveDomainForFile for why this is a best-effort tag, not a hard classification.
|
|
1807
|
+
const domain = explicitDomain || (filePaths && filePaths.length > 0 ? deriveDomainForFile(filePaths[0]) : undefined);
|
|
1500
1808
|
if (!query || !responseText)
|
|
1501
1809
|
throw new Error("Query and response are required");
|
|
1502
1810
|
try {
|
|
1503
1811
|
const brain = (0, TheBrainV2_1.getBrain)();
|
|
1504
|
-
const storeRes = brain.store(query, responseText, provider, 0.92, filePaths, undefined, outcome, symbols, claims);
|
|
1812
|
+
const storeRes = brain.store(query, responseText, provider, 0.92, filePaths, undefined, outcome, symbols, claims, domain);
|
|
1505
1813
|
// Storing a memory saves nothing — it only creates the chance of a saving later.
|
|
1506
1814
|
// Crediting tokens here inflated the ledger on write and then credited the same
|
|
1507
1815
|
// answer again on every read. The saving is booked by search_memory on a fresh hit.
|
|
@@ -1514,19 +1822,40 @@ async function handleStoreMemory(args) {
|
|
|
1514
1822
|
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.` : "";
|
|
1515
1823
|
const outcomeNote = outcome === "failed" ? " Tagged as a FAILED attempt — future searches will surface it as a warning, not a suggestion." : "";
|
|
1516
1824
|
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.` : "";
|
|
1517
|
-
|
|
1825
|
+
const domainNote = domain
|
|
1826
|
+
? ` Tagged with domain '${domain}'${explicitDomain ? "" : " (auto-derived from filePaths)"} — domain-scoped searches will prefer it.`
|
|
1827
|
+
: "";
|
|
1828
|
+
const conflictsNote = storeRes.conflicts && storeRes.conflicts.length > 0
|
|
1829
|
+
? `\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` +
|
|
1830
|
+
storeRes.conflicts.map((c) => ` - [${c.outcome}] ${c.id}: ${c.query.substring(0, 120)}`).join("\n")
|
|
1831
|
+
: "";
|
|
1832
|
+
return { content: [{ type: "text", text: `Success: Memory stored. ${storeRes.reason}${trackingNote}${symbolNote}${outcomeNote}${claimsNote}${domainNote}${conflictsNote}` }] };
|
|
1518
1833
|
}
|
|
1519
1834
|
catch (e) {
|
|
1520
1835
|
(0, utils_1.logError)("store_memory", e);
|
|
1521
1836
|
return { content: [{ type: "text", text: `Failed to store memory in local Brain: ${e.message}` }] };
|
|
1522
1837
|
}
|
|
1523
1838
|
}
|
|
1839
|
+
async function handleDownvoteMemory(args) {
|
|
1840
|
+
const id = args?.id;
|
|
1841
|
+
if (!id)
|
|
1842
|
+
throw new Error("id is required");
|
|
1843
|
+
try {
|
|
1844
|
+
const result = (0, TheBrainV2_1.getBrain)().downvote(id);
|
|
1845
|
+
return { content: [{ type: "text", text: result.message }] };
|
|
1846
|
+
}
|
|
1847
|
+
catch (e) {
|
|
1848
|
+
(0, utils_1.logError)("downvote_memory", e);
|
|
1849
|
+
return { content: [{ type: "text", text: `Failed to downvote memory: ${e.message}` }] };
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1524
1852
|
async function handleGetRoutingAdvice(args) {
|
|
1525
1853
|
const prompt = args?.prompt;
|
|
1526
1854
|
const intendedModel = args?.intended_model;
|
|
1527
1855
|
const router = new ComplexityRouter_1.ComplexityRouter();
|
|
1528
1856
|
const decision = router.evaluate(prompt, intendedModel);
|
|
1529
|
-
|
|
1857
|
+
const switched = !!intendedModel && decision.model !== intendedModel;
|
|
1858
|
+
if (decision.complexity === "low" && switched) {
|
|
1530
1859
|
const estimatedPromptTokens = Math.floor((prompt?.length || 0) / 4);
|
|
1531
1860
|
(0, reportSavings_1.reportSavings)({
|
|
1532
1861
|
source: "complexityRouting",
|
|
@@ -1535,6 +1864,12 @@ async function handleGetRoutingAdvice(args) {
|
|
|
1535
1864
|
query: prompt?.substring(0, 100),
|
|
1536
1865
|
});
|
|
1537
1866
|
}
|
|
1867
|
+
const structured = {
|
|
1868
|
+
recommendedModel: decision.model,
|
|
1869
|
+
complexity: decision.complexity,
|
|
1870
|
+
intendedModel: intendedModel || null,
|
|
1871
|
+
switched,
|
|
1872
|
+
};
|
|
1538
1873
|
return {
|
|
1539
1874
|
content: [
|
|
1540
1875
|
{
|
|
@@ -1542,6 +1877,7 @@ async function handleGetRoutingAdvice(args) {
|
|
|
1542
1877
|
text: `Lemma Routing Advice: Use ${decision.model}. Reason: Complexity is ${decision.complexity}.`,
|
|
1543
1878
|
},
|
|
1544
1879
|
],
|
|
1880
|
+
structuredContent: structured,
|
|
1545
1881
|
};
|
|
1546
1882
|
}
|
|
1547
1883
|
async function handleAutoHeal(args) {
|
|
@@ -1751,6 +2087,30 @@ function stripLineNumberGutter(text) {
|
|
|
1751
2087
|
return text;
|
|
1752
2088
|
return lines.map((l) => l.replace(GUTTER, "")).join("\n");
|
|
1753
2089
|
}
|
|
2090
|
+
// Splices one resolved match into the file and writes it. Shared by the normal
|
|
2091
|
+
// single-match path and the ambiguous-match-resolved-via-elicitation path below.
|
|
2092
|
+
async function applyMatch(match, filePath, resolved, originalContent, searchContent, replaceContent) {
|
|
2093
|
+
let updatedContent;
|
|
2094
|
+
if (match.strategy === "exact" && match.charStart !== undefined && match.charEnd !== undefined) {
|
|
2095
|
+
// Splice by character offset — the search block may be a mid-line fragment, and
|
|
2096
|
+
// a whole-line splice would discard everything else sharing that line.
|
|
2097
|
+
updatedContent = originalContent.slice(0, match.charStart) + replaceContent + originalContent.slice(match.charEnd);
|
|
2098
|
+
}
|
|
2099
|
+
else {
|
|
2100
|
+
const contentLines = originalContent.split("\n");
|
|
2101
|
+
const finalReplace = (0, PatchMatcher_1.reindentReplacement)(replaceContent, searchContent, contentLines[match.startLine]);
|
|
2102
|
+
const updatedLines = [...contentLines.slice(0, match.startLine), ...finalReplace.split("\n"), ...contentLines.slice(match.endLine + 1)];
|
|
2103
|
+
updatedContent = updatedLines.join("\n");
|
|
2104
|
+
}
|
|
2105
|
+
const elicited = await tryElicitConfirmation(`Apply a patch to ${filePath}? This replaces ${searchContent.length} char(s) with ${replaceContent.length} char(s) ` +
|
|
2106
|
+
`starting at line ${match.startLine + 1}.`);
|
|
2107
|
+
if (elicited.supported && !elicited.confirmed) {
|
|
2108
|
+
return { content: [{ type: "text", text: `Patch to ${filePath} was not applied: declined during confirmation.` }] };
|
|
2109
|
+
}
|
|
2110
|
+
fs_1.default.writeFileSync(resolved, updatedContent, "utf8");
|
|
2111
|
+
const note = match.strategy === "exact" ? "" : ` (matched via ${match.strategy}, score ${match.score.toFixed(2)})`;
|
|
2112
|
+
return { content: [{ type: "text", text: `Success: Patch successfully applied to ${filePath}${note}` }] };
|
|
2113
|
+
}
|
|
1754
2114
|
async function handleApplyWorkspacePatch(args) {
|
|
1755
2115
|
const filePath = args?.filePath;
|
|
1756
2116
|
const rawSearch = args?.searchContent;
|
|
@@ -1773,6 +2133,15 @@ async function handleApplyWorkspacePatch(args) {
|
|
|
1773
2133
|
const matchResult = (0, PatchMatcher_1.findMatch)(originalContent, searchContent);
|
|
1774
2134
|
if (!Array.isArray(matchResult)) {
|
|
1775
2135
|
if (matchResult.reason === "ambiguous") {
|
|
2136
|
+
const candidates = matchResult.candidates ?? [];
|
|
2137
|
+
if (candidates.length > 0) {
|
|
2138
|
+
const chosen = await tryElicitChoice(`${candidates.length} equally good matches found for the search content in ${filePath}. Which one should be patched?`, candidates.map((c) => ({
|
|
2139
|
+
title: `line ${c.startLine + 1}: ${c.matchedText.split("\n")[0].slice(0, 80)}`,
|
|
2140
|
+
})));
|
|
2141
|
+
if (chosen.index !== null) {
|
|
2142
|
+
return applyMatch(candidates[chosen.index], filePath, resolved, originalContent, searchContent, replaceContent);
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
1776
2145
|
return {
|
|
1777
2146
|
content: [
|
|
1778
2147
|
{
|
|
@@ -1801,28 +2170,71 @@ async function handleApplyWorkspacePatch(args) {
|
|
|
1801
2170
|
],
|
|
1802
2171
|
};
|
|
1803
2172
|
}
|
|
1804
|
-
|
|
1805
|
-
let updatedContent;
|
|
1806
|
-
if (match.strategy === "exact" && match.charStart !== undefined && match.charEnd !== undefined) {
|
|
1807
|
-
// Splice by character offset — the search block may be a mid-line fragment, and
|
|
1808
|
-
// a whole-line splice would discard everything else sharing that line.
|
|
1809
|
-
updatedContent = originalContent.slice(0, match.charStart) + replaceContent + originalContent.slice(match.charEnd);
|
|
1810
|
-
}
|
|
1811
|
-
else {
|
|
1812
|
-
const contentLines = originalContent.split("\n");
|
|
1813
|
-
const finalReplace = (0, PatchMatcher_1.reindentReplacement)(replaceContent, searchContent, contentLines[match.startLine]);
|
|
1814
|
-
const updatedLines = [...contentLines.slice(0, match.startLine), ...finalReplace.split("\n"), ...contentLines.slice(match.endLine + 1)];
|
|
1815
|
-
updatedContent = updatedLines.join("\n");
|
|
1816
|
-
}
|
|
1817
|
-
fs_1.default.writeFileSync(resolved, updatedContent, "utf8");
|
|
1818
|
-
const note = match.strategy === "exact" ? "" : ` (matched via ${match.strategy}, score ${match.score.toFixed(2)})`;
|
|
1819
|
-
return { content: [{ type: "text", text: `Success: Patch successfully applied to ${filePath}${note}` }] };
|
|
2173
|
+
return applyMatch(matchResult[0], filePath, resolved, originalContent, searchContent, replaceContent);
|
|
1820
2174
|
}
|
|
1821
2175
|
catch (err) {
|
|
1822
2176
|
(0, utils_1.logError)("apply_workspace_patch", err);
|
|
1823
2177
|
return { content: [{ type: "text", text: `Error applying patch: ${err.message}` }] };
|
|
1824
2178
|
}
|
|
1825
2179
|
}
|
|
2180
|
+
// Async replacement for spawnSync: a blocking syscall inside a `tools/call` handler
|
|
2181
|
+
// freezes the whole (single-threaded) MCP server for the run's entire duration — including
|
|
2182
|
+
// task-status polls for THIS run once it's task-augmented (see runAsTask in tasks.ts), which
|
|
2183
|
+
// would otherwise defeat the point of making the tool pollable. `spawn` keeps the event loop
|
|
2184
|
+
// free while the command runs. Shape of the resolved value mirrors spawnSync's return value
|
|
2185
|
+
// on the fields the caller actually reads, so the rest of handleRunWorkspaceCommand is unchanged.
|
|
2186
|
+
function spawnCollect(command, cwd, timeoutMs) {
|
|
2187
|
+
return new Promise((resolve) => {
|
|
2188
|
+
const child = (0, child_process_1.spawn)(command, {
|
|
2189
|
+
cwd,
|
|
2190
|
+
shell: true,
|
|
2191
|
+
env: { ...process.env },
|
|
2192
|
+
// spawnSync closed stdin immediately (no `input` option given), so a command reading
|
|
2193
|
+
// from stdin (e.g. `sed` with no file argument) saw EOF right away. Plain `spawn`
|
|
2194
|
+
// leaves stdin as an open pipe by default — nothing ever writes to or closes it — so
|
|
2195
|
+
// the same command hangs until the timeout kills it. "ignore" reproduces the old,
|
|
2196
|
+
// immediate-EOF behavior.
|
|
2197
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2198
|
+
});
|
|
2199
|
+
let stdout = "";
|
|
2200
|
+
let stderr = "";
|
|
2201
|
+
let stdoutBytes = 0;
|
|
2202
|
+
let stderrBytes = 0;
|
|
2203
|
+
const MAX_BUFFER = 20 * 1024 * 1024;
|
|
2204
|
+
let timedOut = false;
|
|
2205
|
+
let settled = false;
|
|
2206
|
+
const timer = setTimeout(() => {
|
|
2207
|
+
timedOut = true;
|
|
2208
|
+
child.kill("SIGTERM");
|
|
2209
|
+
}, timeoutMs);
|
|
2210
|
+
child.stdout?.on("data", (chunk) => {
|
|
2211
|
+
if (stdoutBytes >= MAX_BUFFER)
|
|
2212
|
+
return;
|
|
2213
|
+
stdoutBytes += chunk.length;
|
|
2214
|
+
stdout += chunk.toString("utf8");
|
|
2215
|
+
});
|
|
2216
|
+
child.stderr?.on("data", (chunk) => {
|
|
2217
|
+
if (stderrBytes >= MAX_BUFFER)
|
|
2218
|
+
return;
|
|
2219
|
+
stderrBytes += chunk.length;
|
|
2220
|
+
stderr += chunk.toString("utf8");
|
|
2221
|
+
});
|
|
2222
|
+
child.on("error", (err) => {
|
|
2223
|
+
if (settled)
|
|
2224
|
+
return;
|
|
2225
|
+
settled = true;
|
|
2226
|
+
clearTimeout(timer);
|
|
2227
|
+
resolve({ stdout, stderr, status: null, timedOut, error: err });
|
|
2228
|
+
});
|
|
2229
|
+
child.on("close", (code) => {
|
|
2230
|
+
if (settled)
|
|
2231
|
+
return;
|
|
2232
|
+
settled = true;
|
|
2233
|
+
clearTimeout(timer);
|
|
2234
|
+
resolve({ stdout, stderr, status: code, timedOut });
|
|
2235
|
+
});
|
|
2236
|
+
});
|
|
2237
|
+
}
|
|
1826
2238
|
async function handleRunWorkspaceCommand(args) {
|
|
1827
2239
|
const command = args?.command;
|
|
1828
2240
|
if (!command)
|
|
@@ -1852,10 +2264,10 @@ async function handleRunWorkspaceCommand(args) {
|
|
|
1852
2264
|
// ── End allowlist check ────────────────────────────────────────────────
|
|
1853
2265
|
const workspaceRoot = process.cwd();
|
|
1854
2266
|
try {
|
|
1855
|
-
//
|
|
1856
|
-
// stdio is overridden, which silently dropped the output of every tool
|
|
1857
|
-
// stderr (jest and tsc both do) on a successful run. It also gives us
|
|
1858
|
-
// the exit code through one code path instead of a throw-on-failure split.
|
|
2267
|
+
// spawn (via spawnCollect above) rather than execSync: execSync leaves stderr attached to
|
|
2268
|
+
// the parent unless stdio is overridden, which silently dropped the output of every tool
|
|
2269
|
+
// that reports on stderr (jest and tsc both do) on a successful run. It also gives us
|
|
2270
|
+
// stdout, stderr and the exit code through one code path instead of a throw-on-failure split.
|
|
1859
2271
|
// 15s was below the runtime of the commands this tool exists for: a real `npm test` or
|
|
1860
2272
|
// `tsc --noEmit` always tripped it, so the distillation pipeline never ran on the output
|
|
1861
2273
|
// it was built for and the call was pure overhead before falling back to a plain shell.
|
|
@@ -1863,15 +2275,12 @@ async function handleRunWorkspaceCommand(args) {
|
|
|
1863
2275
|
const MAX_TIMEOUT_MS = 600000;
|
|
1864
2276
|
const requested = typeof args?.timeoutMs === "number" && args.timeoutMs > 0 ? args.timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
1865
2277
|
const timeoutMs = Math.min(Math.floor(requested), MAX_TIMEOUT_MS);
|
|
1866
|
-
const
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
env: { ...process.env },
|
|
1873
|
-
});
|
|
1874
|
-
const timedOut = !!result.error && result.error.code === "ETIMEDOUT";
|
|
2278
|
+
const elicited = await tryElicitConfirmation(`Run this command in the workspace root?\n\n${command}`);
|
|
2279
|
+
if (elicited.supported && !elicited.confirmed) {
|
|
2280
|
+
return { content: [{ type: "text", text: `Command was not run: declined during confirmation.\n\nCommand: ${command}` }] };
|
|
2281
|
+
}
|
|
2282
|
+
const result = await spawnCollect(command, workspaceRoot, timeoutMs);
|
|
2283
|
+
const timedOut = result.timedOut;
|
|
1875
2284
|
if (result.error && !timedOut) {
|
|
1876
2285
|
return {
|
|
1877
2286
|
content: [{ type: "text", text: `Command could not be executed: ${result.error.message}` }],
|
|
@@ -2885,47 +3294,44 @@ async function handleSmarterCache(args) {
|
|
|
2885
3294
|
reportSavings({ source: "cache", tokens: tokensSaved, toolName: "smarter_cache", query: query.substring(0, 100) });
|
|
2886
3295
|
}
|
|
2887
3296
|
catch { }
|
|
3297
|
+
const structured = {
|
|
3298
|
+
hit: true,
|
|
3299
|
+
similarity: topHit.similarity,
|
|
3300
|
+
threshold,
|
|
3301
|
+
answer: responseText,
|
|
3302
|
+
source: "lemma-brain",
|
|
3303
|
+
tokensSaved,
|
|
3304
|
+
tokensSavedFormatted: `~${tokensSaved.toLocaleString()} tokens`,
|
|
3305
|
+
};
|
|
2888
3306
|
return {
|
|
2889
|
-
content: [{
|
|
2890
|
-
|
|
2891
|
-
text: JSON.stringify({
|
|
2892
|
-
hit: true,
|
|
2893
|
-
similarity: topHit.similarity,
|
|
2894
|
-
threshold,
|
|
2895
|
-
answer: responseText,
|
|
2896
|
-
source: "lemma-brain",
|
|
2897
|
-
tokensSaved,
|
|
2898
|
-
tokensSavedFormatted: `~${tokensSaved.toLocaleString()} tokens`,
|
|
2899
|
-
}, null, 2),
|
|
2900
|
-
}],
|
|
3307
|
+
content: [{ type: "text", text: JSON.stringify(structured, null, 2) }],
|
|
3308
|
+
structuredContent: structured,
|
|
2901
3309
|
};
|
|
2902
3310
|
}
|
|
3311
|
+
const structured = {
|
|
3312
|
+
hit: false,
|
|
3313
|
+
similarity: topHit?.similarity || 0,
|
|
3314
|
+
threshold,
|
|
3315
|
+
answer: null,
|
|
3316
|
+
source: "llm-call-required",
|
|
3317
|
+
hint: "Después de resolver, llama store_memory para cachear y no gastar tokens en esto otra vez.",
|
|
3318
|
+
};
|
|
2903
3319
|
return {
|
|
2904
|
-
content: [{
|
|
2905
|
-
|
|
2906
|
-
text: JSON.stringify({
|
|
2907
|
-
hit: false,
|
|
2908
|
-
similarity: topHit?.similarity || 0,
|
|
2909
|
-
threshold,
|
|
2910
|
-
answer: null,
|
|
2911
|
-
source: "llm-call-required",
|
|
2912
|
-
hint: "Después de resolver, llama store_memory para cachear y no gastar tokens en esto otra vez.",
|
|
2913
|
-
}, null, 2),
|
|
2914
|
-
}],
|
|
3320
|
+
content: [{ type: "text", text: JSON.stringify(structured, null, 2) }],
|
|
3321
|
+
structuredContent: structured,
|
|
2915
3322
|
};
|
|
2916
3323
|
}
|
|
2917
3324
|
catch (e) {
|
|
2918
3325
|
(0, utils_1.logError)("smarter_cache", e);
|
|
3326
|
+
const structured = {
|
|
3327
|
+
hit: false,
|
|
3328
|
+
error: e.message,
|
|
3329
|
+
source: "cache-unavailable",
|
|
3330
|
+
hint: "El cache local del brain falló. El LLM funcionará normalmente sin cache.",
|
|
3331
|
+
};
|
|
2919
3332
|
return {
|
|
2920
|
-
content: [{
|
|
2921
|
-
|
|
2922
|
-
text: JSON.stringify({
|
|
2923
|
-
hit: false,
|
|
2924
|
-
error: e.message,
|
|
2925
|
-
source: "cache-unavailable",
|
|
2926
|
-
hint: "El cache local del brain falló. El LLM funcionará normalmente sin cache.",
|
|
2927
|
-
}, null, 2),
|
|
2928
|
-
}],
|
|
3333
|
+
content: [{ type: "text", text: JSON.stringify(structured, null, 2) }],
|
|
3334
|
+
structuredContent: structured,
|
|
2929
3335
|
};
|
|
2930
3336
|
}
|
|
2931
3337
|
}
|
|
@@ -2946,23 +3352,21 @@ async function handleStateHashCache(args) {
|
|
|
2946
3352
|
(0, reportSavings_1.reportSavings)({ source: "cache", tokens: tokensSaved, toolName: "state_hash_cache", query: query.substring(0, 100) });
|
|
2947
3353
|
}
|
|
2948
3354
|
catch { }
|
|
3355
|
+
const structured = {
|
|
3356
|
+
status: "hit",
|
|
3357
|
+
answer: result.entry.answer,
|
|
3358
|
+
note: "Respuesta desde caché exacto (hash de archivos sin cambios) — no necesita re-razonar.",
|
|
3359
|
+
tokensSaved,
|
|
3360
|
+
};
|
|
2949
3361
|
return {
|
|
2950
|
-
content: [{
|
|
2951
|
-
|
|
2952
|
-
text: JSON.stringify({
|
|
2953
|
-
status: "hit",
|
|
2954
|
-
answer: result.entry.answer,
|
|
2955
|
-
note: "Respuesta desde caché exacto (hash de archivos sin cambios) — no necesita re-razonar.",
|
|
2956
|
-
tokensSaved,
|
|
2957
|
-
}, null, 2),
|
|
2958
|
-
}],
|
|
3362
|
+
content: [{ type: "text", text: JSON.stringify(structured, null, 2) }],
|
|
3363
|
+
structuredContent: structured,
|
|
2959
3364
|
};
|
|
2960
3365
|
}
|
|
3366
|
+
const structured = { status: "miss", reason: result.reason, hint: "Razona normalmente y luego llama action='store' con la respuesta." };
|
|
2961
3367
|
return {
|
|
2962
|
-
content: [{
|
|
2963
|
-
|
|
2964
|
-
text: JSON.stringify({ status: "miss", reason: result.reason, hint: "Razona normalmente y luego llama action='store' con la respuesta." }, null, 2),
|
|
2965
|
-
}],
|
|
3368
|
+
content: [{ type: "text", text: JSON.stringify(structured, null, 2) }],
|
|
3369
|
+
structuredContent: structured,
|
|
2966
3370
|
};
|
|
2967
3371
|
}
|
|
2968
3372
|
if (action === "store") {
|
|
@@ -2970,8 +3374,10 @@ async function handleStateHashCache(args) {
|
|
|
2970
3374
|
if (!answer)
|
|
2971
3375
|
throw new Error("answer is required for action='store'");
|
|
2972
3376
|
const entry = (0, StateHashCache_1.storeStateHash)(workspaceRoot, query, answer, filePaths);
|
|
3377
|
+
const structured = { status: "stored", id: entry.id, filesTracked: Object.keys(entry.fileHashes) };
|
|
2973
3378
|
return {
|
|
2974
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
3379
|
+
content: [{ type: "text", text: JSON.stringify(structured, null, 2) }],
|
|
3380
|
+
structuredContent: structured,
|
|
2975
3381
|
};
|
|
2976
3382
|
}
|
|
2977
3383
|
throw new Error(`Unknown action: ${action}. Use 'lookup' or 'store'.`);
|
|
@@ -2980,16 +3386,15 @@ async function handleStateHashCache(args) {
|
|
|
2980
3386
|
async function handleTokenReceipt(args) {
|
|
2981
3387
|
const limit = typeof args?.limit === "number" ? args.limit : 20;
|
|
2982
3388
|
const summary = (0, TokenReceipt_1.getReceiptSummary)(limit);
|
|
3389
|
+
const structured = {
|
|
3390
|
+
totalEvents: summary.totalEvents,
|
|
3391
|
+
byType: summary.byType,
|
|
3392
|
+
recentEvents: summary.recent,
|
|
3393
|
+
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.",
|
|
3394
|
+
};
|
|
2983
3395
|
return {
|
|
2984
|
-
content: [{
|
|
2985
|
-
|
|
2986
|
-
text: JSON.stringify({
|
|
2987
|
-
totalEvents: summary.totalEvents,
|
|
2988
|
-
byType: summary.byType,
|
|
2989
|
-
recentEvents: summary.recent,
|
|
2990
|
-
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.",
|
|
2991
|
-
}, null, 2),
|
|
2992
|
-
}],
|
|
3396
|
+
content: [{ type: "text", text: JSON.stringify(structured, null, 2) }],
|
|
3397
|
+
structuredContent: structured,
|
|
2993
3398
|
};
|
|
2994
3399
|
}
|
|
2995
3400
|
// ── Token Budget ─────────────────────────────────────────────────
|
|
@@ -4273,7 +4678,17 @@ async function handleAffectedTests(args) {
|
|
|
4273
4678
|
totalTests: result.totalTests,
|
|
4274
4679
|
narrowed: result.command !== null,
|
|
4275
4680
|
});
|
|
4276
|
-
|
|
4681
|
+
const structured = {
|
|
4682
|
+
changed: result.changed,
|
|
4683
|
+
directTests: result.directTests,
|
|
4684
|
+
affected: result.affected,
|
|
4685
|
+
totalTests: result.totalTests,
|
|
4686
|
+
runner: result.runner,
|
|
4687
|
+
command: result.command,
|
|
4688
|
+
fullSuiteReason: result.fullSuiteReason,
|
|
4689
|
+
ungraphed: result.ungraphed,
|
|
4690
|
+
};
|
|
4691
|
+
return { content: [{ type: "text", text }], structuredContent: structured };
|
|
4277
4692
|
}
|
|
4278
4693
|
catch (err) {
|
|
4279
4694
|
(0, utils_1.logError)("affected_tests", err);
|
|
@@ -4778,6 +5193,37 @@ async function handleSmartFileSlice(args) {
|
|
|
4778
5193
|
}
|
|
4779
5194
|
}
|
|
4780
5195
|
// ── Test Oracle ──────────────────────────────────────────────────────
|
|
5196
|
+
/**
|
|
5197
|
+
* Auto-capture a green test run into the Brain, tagged `outcome: 'confirmed'` and tracked
|
|
5198
|
+
* against the files actually changed. This is the auto-store hook: today store_memory only
|
|
5199
|
+
* fires when the agent remembers to call it, so working fixes routinely go uncaptured. A
|
|
5200
|
+
* passing suite over an uncommitted diff is real, cheap evidence something worked — reusing
|
|
5201
|
+
* it costs nothing store_memory wouldn't have cost anyway, and store()'s own dedup guard
|
|
5202
|
+
* means re-running the same green suite repeatedly just no-ops instead of piling up entries.
|
|
5203
|
+
*
|
|
5204
|
+
* Silent on a clean tree (nothing changed, nothing to attribute the pass to) or when the
|
|
5205
|
+
* repo isn't git-tracked. Returns a short suffix for resultText — this must never happen
|
|
5206
|
+
* invisibly, or a future search_memory hit would look reasoned-from-scratch instead of
|
|
5207
|
+
* traced back to an automated test run.
|
|
5208
|
+
*/
|
|
5209
|
+
function autoStoreGreenTests(command, workspaceRoot) {
|
|
5210
|
+
try {
|
|
5211
|
+
const changed = (0, AffectedTests_1.changedFilesFromGit)(workspaceRoot);
|
|
5212
|
+
if (changed.length === 0)
|
|
5213
|
+
return "";
|
|
5214
|
+
const domain = deriveDomainForFile(changed[0]);
|
|
5215
|
+
const query = `tests passing after changes to ${changed.slice(0, 5).join(", ")}${changed.length > 5 ? ` and ${changed.length - 5} more file(s)` : ""}`;
|
|
5216
|
+
const response = `\`${command}\` passed with these files changed (uncommitted at capture time):\n${changed.join("\n")}`;
|
|
5217
|
+
const storeRes = (0, TheBrainV2_1.getBrain)().store(query, response, "test_oracle_auto", 0.92, changed, undefined, "confirmed", undefined, undefined, domain);
|
|
5218
|
+
if (!storeRes.stored)
|
|
5219
|
+
return "";
|
|
5220
|
+
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.`;
|
|
5221
|
+
}
|
|
5222
|
+
catch {
|
|
5223
|
+
// Never let auto-capture break the actual test result the caller asked for.
|
|
5224
|
+
return "";
|
|
5225
|
+
}
|
|
5226
|
+
}
|
|
4781
5227
|
async function handleTestOracle(args) {
|
|
4782
5228
|
const command = args.command || "npm test";
|
|
4783
5229
|
const workspaceRoot = process.cwd();
|
|
@@ -4788,6 +5234,7 @@ async function handleTestOracle(args) {
|
|
|
4788
5234
|
try {
|
|
4789
5235
|
const out = (0, child_process_1.execSync)(command, { cwd: workspaceRoot, encoding: "utf8", timeout: 30000 });
|
|
4790
5236
|
resultText = `All tests passed!\n\nOutput:\n${out.substring(0, 1000)}`;
|
|
5237
|
+
resultText += autoStoreGreenTests(command, workspaceRoot);
|
|
4791
5238
|
}
|
|
4792
5239
|
catch (e) {
|
|
4793
5240
|
const stdout = e.stdout || "";
|
|
@@ -5103,6 +5550,41 @@ async function handleFileIntentIndex(args) {
|
|
|
5103
5550
|
}
|
|
5104
5551
|
}
|
|
5105
5552
|
const COG_MAP_FILE = path_1.default.join(os_1.default.homedir(), ".lemma-cache", "cog_map.json");
|
|
5553
|
+
/**
|
|
5554
|
+
* Best-effort domain for a repo-relative file path, so store_memory and the test-oracle
|
|
5555
|
+
* auto-store below don't require a human to type `domain` by hand every time.
|
|
5556
|
+
*
|
|
5557
|
+
* Prefers the persisted Cognitive Map (built via `cognitive_map` action "build") when it
|
|
5558
|
+
* has this exact file listed — that reflects an actual workspace scan. Falls back to the
|
|
5559
|
+
* same lightweight substring heuristic "build" uses, so a project that never ran "build"
|
|
5560
|
+
* still gets a reasonable tag instead of none at all. Returns undefined rather than
|
|
5561
|
+
* "general" when nothing matches — an absent domain stays visible to every domain-scoped
|
|
5562
|
+
* search (see BrainEntry.domain), while a wrong guess would actively mislead one.
|
|
5563
|
+
*/
|
|
5564
|
+
function deriveDomainForFile(relPath) {
|
|
5565
|
+
const norm = relPath.replace(/^\//, "");
|
|
5566
|
+
if (fs_1.default.existsSync(COG_MAP_FILE)) {
|
|
5567
|
+
try {
|
|
5568
|
+
const map = JSON.parse(fs_1.default.readFileSync(COG_MAP_FILE, "utf8"));
|
|
5569
|
+
for (const node of Object.values(map.nodes || {})) {
|
|
5570
|
+
if (node.files.includes(norm))
|
|
5571
|
+
return node.domain;
|
|
5572
|
+
}
|
|
5573
|
+
}
|
|
5574
|
+
catch { /* fall through to heuristic */ }
|
|
5575
|
+
}
|
|
5576
|
+
if (norm.includes("mcp"))
|
|
5577
|
+
return "mcp";
|
|
5578
|
+
if (norm.includes("subconscious") || norm.includes("brain"))
|
|
5579
|
+
return "memory";
|
|
5580
|
+
if (norm.includes("security") || norm.includes("scrubber"))
|
|
5581
|
+
return "security";
|
|
5582
|
+
if (norm.includes("pr-review"))
|
|
5583
|
+
return "pr-review";
|
|
5584
|
+
if (norm.includes("utils"))
|
|
5585
|
+
return "utils";
|
|
5586
|
+
return undefined;
|
|
5587
|
+
}
|
|
5106
5588
|
async function handleCognitiveMap(args) {
|
|
5107
5589
|
const action = args.action;
|
|
5108
5590
|
const domain = args.domain;
|