@nxuss/lemma 1.12.0 → 1.15.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 +15 -8
- package/dist/cjs/mcp/index.js +3 -0
- package/dist/cjs/mcp/index.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 +1 -0
- 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 +360 -42
- package/dist/cjs/mcp/tools.js.map +1 -1
- package/dist/cjs/subconscious/TheBrainV2.d.ts +149 -1
- package/dist/cjs/subconscious/TheBrainV2.d.ts.map +1 -1
- package/dist/cjs/subconscious/TheBrainV2.js +314 -33
- 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/cjs/utils/SymbolSurgicalContext.d.ts +9 -0
- package/dist/cjs/utils/SymbolSurgicalContext.d.ts.map +1 -1
- package/dist/cjs/utils/SymbolSurgicalContext.js +19 -0
- package/dist/cjs/utils/SymbolSurgicalContext.js.map +1 -1
- package/dist/esm/mcp/index.js +3 -0
- package/dist/esm/mcp/index.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 +1 -0
- 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 +362 -44
- package/dist/esm/mcp/tools.js.map +1 -1
- package/dist/esm/subconscious/TheBrainV2.d.ts +149 -1
- package/dist/esm/subconscious/TheBrainV2.d.ts.map +1 -1
- package/dist/esm/subconscious/TheBrainV2.js +310 -33
- 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/dist/esm/utils/SymbolSurgicalContext.d.ts +9 -0
- package/dist/esm/utils/SymbolSurgicalContext.d.ts.map +1 -1
- package/dist/esm/utils/SymbolSurgicalContext.js +18 -0
- package/dist/esm/utils/SymbolSurgicalContext.js.map +1 -1
- package/package.json +1 -1
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", "verify_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
|
|
@@ -137,6 +138,45 @@ async function tryElicitConfirmation(summary) {
|
|
|
137
138
|
return { supported: false, confirmed: true };
|
|
138
139
|
}
|
|
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
|
+
}
|
|
140
180
|
const toolDefinitions = [
|
|
141
181
|
{
|
|
142
182
|
name: "scrub_privacy",
|
|
@@ -153,7 +193,7 @@ const toolDefinitions = [
|
|
|
153
193
|
{
|
|
154
194
|
name: "search_memory",
|
|
155
195
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
156
|
-
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
|
|
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 split into 'fresh' (safe to reuse) and 'stale' (a tracked file changed since — re-verify 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.",
|
|
157
197
|
inputSchema: {
|
|
158
198
|
type: "object",
|
|
159
199
|
properties: {
|
|
@@ -164,6 +204,15 @@ const toolDefinitions = [
|
|
|
164
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.",
|
|
165
205
|
default: 0.75,
|
|
166
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
|
+
},
|
|
211
|
+
semanticDiff: {
|
|
212
|
+
type: "boolean",
|
|
213
|
+
description: "Don't count a comment/whitespace-only change as stale.",
|
|
214
|
+
default: false,
|
|
215
|
+
},
|
|
167
216
|
},
|
|
168
217
|
required: ["query"],
|
|
169
218
|
},
|
|
@@ -171,7 +220,7 @@ const toolDefinitions = [
|
|
|
171
220
|
{
|
|
172
221
|
name: "store_memory",
|
|
173
222
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
174
|
-
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
|
|
223
|
+
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 so the memory auto-invalidates the moment those files change. If the answer is really about one function/class, 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.",
|
|
175
224
|
inputSchema: {
|
|
176
225
|
type: "object",
|
|
177
226
|
properties: {
|
|
@@ -216,10 +265,48 @@ const toolDefinitions = [
|
|
|
216
265
|
},
|
|
217
266
|
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.",
|
|
218
267
|
},
|
|
268
|
+
domain: {
|
|
269
|
+
type: "string",
|
|
270
|
+
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).",
|
|
271
|
+
},
|
|
272
|
+
derivedFrom: {
|
|
273
|
+
type: "array",
|
|
274
|
+
items: { type: "string" },
|
|
275
|
+
description: "Ids of memories this one depends on; stale if they go stale.",
|
|
276
|
+
},
|
|
219
277
|
},
|
|
220
278
|
required: ["query", "response"],
|
|
221
279
|
},
|
|
222
280
|
},
|
|
281
|
+
{
|
|
282
|
+
name: "verify_memory",
|
|
283
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
284
|
+
description: "Cheaply revalidate ids from a prior search_memory/store_memory result — batch hash-compare, no BM25/re-search. Each id comes back 'fresh', 'stale', or 'unknown' (purged/renamed, never reported as fresh). Accepts entry ids and claim ids together.",
|
|
285
|
+
inputSchema: {
|
|
286
|
+
type: "object",
|
|
287
|
+
properties: {
|
|
288
|
+
ids: { type: "array", items: { type: "string" }, description: "Entry and/or claim ids to revalidate, from a prior search_memory/store_memory result." },
|
|
289
|
+
semanticDiff: {
|
|
290
|
+
type: "boolean",
|
|
291
|
+
description: "Don't count a comment/whitespace-only change as stale.",
|
|
292
|
+
default: false,
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
required: ["ids"],
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
name: "downvote_memory",
|
|
300
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
301
|
+
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.",
|
|
302
|
+
inputSchema: {
|
|
303
|
+
type: "object",
|
|
304
|
+
properties: {
|
|
305
|
+
id: { type: "string", description: "The memory's id, from a prior search_memory result." },
|
|
306
|
+
},
|
|
307
|
+
required: ["id"],
|
|
308
|
+
},
|
|
309
|
+
},
|
|
223
310
|
{
|
|
224
311
|
name: "get_routing_advice",
|
|
225
312
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
@@ -314,6 +401,7 @@ const toolDefinitions = [
|
|
|
314
401
|
{
|
|
315
402
|
name: "run_workspace_command",
|
|
316
403
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
404
|
+
execution: { taskSupport: "optional" },
|
|
317
405
|
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.",
|
|
318
406
|
inputSchema: {
|
|
319
407
|
type: "object",
|
|
@@ -1347,6 +1435,8 @@ const toolHandlers = {
|
|
|
1347
1435
|
scrub_privacy: handleScrubPrivacy,
|
|
1348
1436
|
search_memory: handleSearchMemory,
|
|
1349
1437
|
store_memory: handleStoreMemory,
|
|
1438
|
+
downvote_memory: handleDownvoteMemory,
|
|
1439
|
+
verify_memory: handleVerifyMemory,
|
|
1350
1440
|
get_routing_advice: handleGetRoutingAdvice,
|
|
1351
1441
|
auto_heal: handleAutoHeal,
|
|
1352
1442
|
read_workspace_file: handleReadWorkspaceFile,
|
|
@@ -1496,7 +1586,7 @@ export function setupToolsHandlers(server, onToolCall) {
|
|
|
1496
1586
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
1497
1587
|
tools: toolDefinitionsArray,
|
|
1498
1588
|
}));
|
|
1499
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1589
|
+
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
1500
1590
|
const { name, arguments: args } = request.params;
|
|
1501
1591
|
const startTime = Date.now();
|
|
1502
1592
|
// ── Pro License Gate (granular: free tools work without license) ────────
|
|
@@ -1525,6 +1615,39 @@ export function setupToolsHandlers(server, onToolCall) {
|
|
|
1525
1615
|
});
|
|
1526
1616
|
throw new Error(`Unknown tool: ${name}`);
|
|
1527
1617
|
}
|
|
1618
|
+
// ── Task-augmented call: hand off to tasks.ts instead of awaiting inline ────
|
|
1619
|
+
// A task-unaware client never sends `request.params.task`, so this only triggers when
|
|
1620
|
+
// the caller actually asked for it — today's synchronous behavior is unchanged otherwise.
|
|
1621
|
+
if (request.params.task && TASK_CAPABLE_TOOLS.has(name)) {
|
|
1622
|
+
const ledgerBefore = getLedgerLength();
|
|
1623
|
+
return runAsTask(name, (args || {}), request, extra.requestId, handler, (result, error) => {
|
|
1624
|
+
if (error) {
|
|
1625
|
+
onToolCall?.({
|
|
1626
|
+
tool: name,
|
|
1627
|
+
args: args || {},
|
|
1628
|
+
result: "ERROR",
|
|
1629
|
+
latency: Date.now() - startTime,
|
|
1630
|
+
error: error instanceof Error ? error.message.substring(0, 100) : String(error).substring(0, 100),
|
|
1631
|
+
});
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
if (getLedgerLength() === ledgerBefore) {
|
|
1635
|
+
recordReceiptEvent(receiptTypeForTool(name), name, {
|
|
1636
|
+
tool: name,
|
|
1637
|
+
...receiptLabelMeta(name, args),
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
const tokensImpact = estimateTokensFromResult(result);
|
|
1641
|
+
onToolCall?.({
|
|
1642
|
+
tool: name,
|
|
1643
|
+
args: args || {},
|
|
1644
|
+
result: "OK",
|
|
1645
|
+
latency: Date.now() - startTime,
|
|
1646
|
+
tokensImpact,
|
|
1647
|
+
});
|
|
1648
|
+
reportCost({ source: "toolResult", tokens: tokensImpact, toolName: name });
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1528
1651
|
try {
|
|
1529
1652
|
// Sampled around the handler so a tool that logs its own, more specific event
|
|
1530
1653
|
// (a cache hit, a miss) isn't double-counted by the generic entry below.
|
|
@@ -1560,6 +1683,9 @@ export function setupToolsHandlers(server, onToolCall) {
|
|
|
1560
1683
|
throw error;
|
|
1561
1684
|
}
|
|
1562
1685
|
});
|
|
1686
|
+
// Registered after ListTools/CallTool: tests and other callers that assume "ListTools is
|
|
1687
|
+
// first, CallTool is second" (see tests/unit/tool-regressions.test.ts) key off that order.
|
|
1688
|
+
registerTaskHandlers(server);
|
|
1563
1689
|
}
|
|
1564
1690
|
// ── Tool Implementations ────────────────────────────────────────────
|
|
1565
1691
|
async function handleScrubPrivacy(args) {
|
|
@@ -1583,11 +1709,13 @@ async function handleSearchMemory(args) {
|
|
|
1583
1709
|
// formatting below then presents it as a reusable memory. Match smarter_cache's
|
|
1584
1710
|
// threshold so both paths agree on what counts as a hit.
|
|
1585
1711
|
const minSimilarity = typeof args?.minSimilarity === "number" ? args.minSimilarity : DEFAULT_MEMORY_SIMILARITY_FLOOR;
|
|
1712
|
+
const domain = typeof args?.domain === "string" && args.domain.trim() ? args.domain.trim() : undefined;
|
|
1713
|
+
const semanticDiff = args?.semanticDiff === true;
|
|
1586
1714
|
if (!query)
|
|
1587
1715
|
throw new Error("Query is required");
|
|
1588
1716
|
try {
|
|
1589
1717
|
const brain = getBrain();
|
|
1590
|
-
const results = brain.search(query, limit, minSimilarity, { projectId: deriveProjectId() });
|
|
1718
|
+
const results = brain.search(query, limit, minSimilarity, { projectId: deriveProjectId(), domain, semanticDiff });
|
|
1591
1719
|
if (results.length === 0) {
|
|
1592
1720
|
recordReceiptEvent("reasoning", query.substring(0, 100), {
|
|
1593
1721
|
tool: "search_memory",
|
|
@@ -1611,7 +1739,9 @@ async function handleSearchMemory(args) {
|
|
|
1611
1739
|
const staleCount = r.claims.filter((c) => !c.fresh).length;
|
|
1612
1740
|
return `\nClaims (${r.claims.length - staleCount}/${r.claims.length} still fresh — trust only the ✓ ones):\n${lines.join("\n")}`;
|
|
1613
1741
|
};
|
|
1614
|
-
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)}
|
|
1742
|
+
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)}${Array.isArray(r.cosmeticChanges) && r.cosmeticChanges.length > 0
|
|
1743
|
+
? `\n(comment/whitespace-only change, ignored under semanticDiff: ${r.cosmeticChanges.join(", ")})`
|
|
1744
|
+
: ""}`;
|
|
1615
1745
|
if (fresh.length > 0) {
|
|
1616
1746
|
// Only a fresh hit (or an untracked, purely conceptual entry) counts as a real
|
|
1617
1747
|
// avoided re-investigation — this is the only case worth crediting as savings.
|
|
@@ -1659,11 +1789,18 @@ async function handleStoreMemory(args) {
|
|
|
1659
1789
|
const claims = Array.isArray(args?.claims)
|
|
1660
1790
|
? args.claims.filter((c) => c?.text)
|
|
1661
1791
|
: undefined;
|
|
1792
|
+
const explicitDomain = typeof args?.domain === "string" && args.domain.trim() ? args.domain.trim() : undefined;
|
|
1793
|
+
// Auto-derive from the first tracked file when the caller didn't say — see
|
|
1794
|
+
// deriveDomainForFile for why this is a best-effort tag, not a hard classification.
|
|
1795
|
+
const domain = explicitDomain || (filePaths && filePaths.length > 0 ? deriveDomainForFile(filePaths[0]) : undefined);
|
|
1796
|
+
const derivedFrom = Array.isArray(args?.derivedFrom)
|
|
1797
|
+
? args.derivedFrom.filter((id) => typeof id === "string" && id)
|
|
1798
|
+
: undefined;
|
|
1662
1799
|
if (!query || !responseText)
|
|
1663
1800
|
throw new Error("Query and response are required");
|
|
1664
1801
|
try {
|
|
1665
1802
|
const brain = getBrain();
|
|
1666
|
-
const storeRes = brain.store(query, responseText, provider, 0.92, filePaths, undefined, outcome, symbols, claims);
|
|
1803
|
+
const storeRes = brain.store(query, responseText, provider, 0.92, filePaths, undefined, outcome, symbols, claims, domain, derivedFrom);
|
|
1667
1804
|
// Storing a memory saves nothing — it only creates the chance of a saving later.
|
|
1668
1805
|
// Crediting tokens here inflated the ledger on write and then credited the same
|
|
1669
1806
|
// answer again on every read. The saving is booked by search_memory on a fresh hit.
|
|
@@ -1676,13 +1813,63 @@ async function handleStoreMemory(args) {
|
|
|
1676
1813
|
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.` : "";
|
|
1677
1814
|
const outcomeNote = outcome === "failed" ? " Tagged as a FAILED attempt — future searches will surface it as a warning, not a suggestion." : "";
|
|
1678
1815
|
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.` : "";
|
|
1679
|
-
|
|
1816
|
+
const domainNote = domain
|
|
1817
|
+
? ` Tagged with domain '${domain}'${explicitDomain ? "" : " (auto-derived from filePaths)"} — domain-scoped searches will prefer it.`
|
|
1818
|
+
: "";
|
|
1819
|
+
const derivedFromNote = derivedFrom && derivedFrom.length > 0
|
|
1820
|
+
? ` Derived from ${derivedFrom.length} prior memor${derivedFrom.length === 1 ? "y" : "ies"} — this one goes stale too if any of those do.`
|
|
1821
|
+
: "";
|
|
1822
|
+
const conflictsNote = storeRes.conflicts && storeRes.conflicts.length > 0
|
|
1823
|
+
? `\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` +
|
|
1824
|
+
storeRes.conflicts.map((c) => ` - [${c.outcome}] ${c.id}: ${c.query.substring(0, 120)}`).join("\n")
|
|
1825
|
+
: "";
|
|
1826
|
+
return { content: [{ type: "text", text: `Success: Memory stored. ${storeRes.reason}${trackingNote}${symbolNote}${outcomeNote}${claimsNote}${domainNote}${derivedFromNote}${conflictsNote}` }] };
|
|
1680
1827
|
}
|
|
1681
1828
|
catch (e) {
|
|
1682
1829
|
logError("store_memory", e);
|
|
1683
1830
|
return { content: [{ type: "text", text: `Failed to store memory in local Brain: ${e.message}` }] };
|
|
1684
1831
|
}
|
|
1685
1832
|
}
|
|
1833
|
+
async function handleDownvoteMemory(args) {
|
|
1834
|
+
const id = args?.id;
|
|
1835
|
+
if (!id)
|
|
1836
|
+
throw new Error("id is required");
|
|
1837
|
+
try {
|
|
1838
|
+
const result = getBrain().downvote(id);
|
|
1839
|
+
return { content: [{ type: "text", text: result.message }] };
|
|
1840
|
+
}
|
|
1841
|
+
catch (e) {
|
|
1842
|
+
logError("downvote_memory", e);
|
|
1843
|
+
return { content: [{ type: "text", text: `Failed to downvote memory: ${e.message}` }] };
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
async function handleVerifyMemory(args) {
|
|
1847
|
+
const ids = Array.isArray(args?.ids) ? args.ids.filter((id) => typeof id === "string" && id) : [];
|
|
1848
|
+
const semanticDiff = args?.semanticDiff === true;
|
|
1849
|
+
if (ids.length === 0)
|
|
1850
|
+
throw new Error("ids is required and must be a non-empty array");
|
|
1851
|
+
try {
|
|
1852
|
+
const results = getBrain().verifyByIds(ids, { semanticDiff });
|
|
1853
|
+
const fresh = results.filter((r) => r.status === "fresh");
|
|
1854
|
+
const stale = results.filter((r) => r.status === "stale");
|
|
1855
|
+
const unknown = results.filter((r) => r.status === "unknown");
|
|
1856
|
+
const formatOne = (r) => {
|
|
1857
|
+
const staleNote = r.staleFiles && r.staleFiles.length > 0 ? ` (changed: ${r.staleFiles.join(", ")})` : "";
|
|
1858
|
+
const cosmeticNote = r.cosmeticChanges && r.cosmeticChanges.length > 0 ? ` (cosmetic only, ignored: ${r.cosmeticChanges.join(", ")})` : "";
|
|
1859
|
+
const claimsNote = r.claimBreakdown && r.claimBreakdown.length > 0
|
|
1860
|
+
? `\n Claims: ${r.claimBreakdown.map((c) => `${c.fresh ? "✓" : "✗"} ${c.text}`).join(" | ")}`
|
|
1861
|
+
: "";
|
|
1862
|
+
return `${r.id}: ${r.status}${staleNote}${cosmeticNote}${claimsNote}`;
|
|
1863
|
+
};
|
|
1864
|
+
const summary = `${fresh.length} fresh, ${stale.length} stale, ${unknown.length} unknown (of ${results.length} checked).`;
|
|
1865
|
+
const lines = results.map(formatOne).join("\n");
|
|
1866
|
+
return { content: [{ type: "text", text: `${summary}\n${lines}` }] };
|
|
1867
|
+
}
|
|
1868
|
+
catch (e) {
|
|
1869
|
+
logError("verify_memory", e);
|
|
1870
|
+
return { content: [{ type: "text", text: `Failed to verify memory: ${e.message}` }] };
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1686
1873
|
async function handleGetRoutingAdvice(args) {
|
|
1687
1874
|
const prompt = args?.prompt;
|
|
1688
1875
|
const intendedModel = args?.intended_model;
|
|
@@ -1921,6 +2108,30 @@ function stripLineNumberGutter(text) {
|
|
|
1921
2108
|
return text;
|
|
1922
2109
|
return lines.map((l) => l.replace(GUTTER, "")).join("\n");
|
|
1923
2110
|
}
|
|
2111
|
+
// Splices one resolved match into the file and writes it. Shared by the normal
|
|
2112
|
+
// single-match path and the ambiguous-match-resolved-via-elicitation path below.
|
|
2113
|
+
async function applyMatch(match, filePath, resolved, originalContent, searchContent, replaceContent) {
|
|
2114
|
+
let updatedContent;
|
|
2115
|
+
if (match.strategy === "exact" && match.charStart !== undefined && match.charEnd !== undefined) {
|
|
2116
|
+
// Splice by character offset — the search block may be a mid-line fragment, and
|
|
2117
|
+
// a whole-line splice would discard everything else sharing that line.
|
|
2118
|
+
updatedContent = originalContent.slice(0, match.charStart) + replaceContent + originalContent.slice(match.charEnd);
|
|
2119
|
+
}
|
|
2120
|
+
else {
|
|
2121
|
+
const contentLines = originalContent.split("\n");
|
|
2122
|
+
const finalReplace = reindentReplacement(replaceContent, searchContent, contentLines[match.startLine]);
|
|
2123
|
+
const updatedLines = [...contentLines.slice(0, match.startLine), ...finalReplace.split("\n"), ...contentLines.slice(match.endLine + 1)];
|
|
2124
|
+
updatedContent = updatedLines.join("\n");
|
|
2125
|
+
}
|
|
2126
|
+
const elicited = await tryElicitConfirmation(`Apply a patch to ${filePath}? This replaces ${searchContent.length} char(s) with ${replaceContent.length} char(s) ` +
|
|
2127
|
+
`starting at line ${match.startLine + 1}.`);
|
|
2128
|
+
if (elicited.supported && !elicited.confirmed) {
|
|
2129
|
+
return { content: [{ type: "text", text: `Patch to ${filePath} was not applied: declined during confirmation.` }] };
|
|
2130
|
+
}
|
|
2131
|
+
fs.writeFileSync(resolved, updatedContent, "utf8");
|
|
2132
|
+
const note = match.strategy === "exact" ? "" : ` (matched via ${match.strategy}, score ${match.score.toFixed(2)})`;
|
|
2133
|
+
return { content: [{ type: "text", text: `Success: Patch successfully applied to ${filePath}${note}` }] };
|
|
2134
|
+
}
|
|
1924
2135
|
async function handleApplyWorkspacePatch(args) {
|
|
1925
2136
|
const filePath = args?.filePath;
|
|
1926
2137
|
const rawSearch = args?.searchContent;
|
|
@@ -1943,6 +2154,15 @@ async function handleApplyWorkspacePatch(args) {
|
|
|
1943
2154
|
const matchResult = findMatch(originalContent, searchContent);
|
|
1944
2155
|
if (!Array.isArray(matchResult)) {
|
|
1945
2156
|
if (matchResult.reason === "ambiguous") {
|
|
2157
|
+
const candidates = matchResult.candidates ?? [];
|
|
2158
|
+
if (candidates.length > 0) {
|
|
2159
|
+
const chosen = await tryElicitChoice(`${candidates.length} equally good matches found for the search content in ${filePath}. Which one should be patched?`, candidates.map((c) => ({
|
|
2160
|
+
title: `line ${c.startLine + 1}: ${c.matchedText.split("\n")[0].slice(0, 80)}`,
|
|
2161
|
+
})));
|
|
2162
|
+
if (chosen.index !== null) {
|
|
2163
|
+
return applyMatch(candidates[chosen.index], filePath, resolved, originalContent, searchContent, replaceContent);
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
1946
2166
|
return {
|
|
1947
2167
|
content: [
|
|
1948
2168
|
{
|
|
@@ -1971,33 +2191,71 @@ async function handleApplyWorkspacePatch(args) {
|
|
|
1971
2191
|
],
|
|
1972
2192
|
};
|
|
1973
2193
|
}
|
|
1974
|
-
|
|
1975
|
-
let updatedContent;
|
|
1976
|
-
if (match.strategy === "exact" && match.charStart !== undefined && match.charEnd !== undefined) {
|
|
1977
|
-
// Splice by character offset — the search block may be a mid-line fragment, and
|
|
1978
|
-
// a whole-line splice would discard everything else sharing that line.
|
|
1979
|
-
updatedContent = originalContent.slice(0, match.charStart) + replaceContent + originalContent.slice(match.charEnd);
|
|
1980
|
-
}
|
|
1981
|
-
else {
|
|
1982
|
-
const contentLines = originalContent.split("\n");
|
|
1983
|
-
const finalReplace = reindentReplacement(replaceContent, searchContent, contentLines[match.startLine]);
|
|
1984
|
-
const updatedLines = [...contentLines.slice(0, match.startLine), ...finalReplace.split("\n"), ...contentLines.slice(match.endLine + 1)];
|
|
1985
|
-
updatedContent = updatedLines.join("\n");
|
|
1986
|
-
}
|
|
1987
|
-
const elicited = await tryElicitConfirmation(`Apply a patch to ${filePath}? This replaces ${searchContent.length} char(s) with ${replaceContent.length} char(s) ` +
|
|
1988
|
-
`starting at line ${match.startLine + 1}.`);
|
|
1989
|
-
if (elicited.supported && !elicited.confirmed) {
|
|
1990
|
-
return { content: [{ type: "text", text: `Patch to ${filePath} was not applied: declined during confirmation.` }] };
|
|
1991
|
-
}
|
|
1992
|
-
fs.writeFileSync(resolved, updatedContent, "utf8");
|
|
1993
|
-
const note = match.strategy === "exact" ? "" : ` (matched via ${match.strategy}, score ${match.score.toFixed(2)})`;
|
|
1994
|
-
return { content: [{ type: "text", text: `Success: Patch successfully applied to ${filePath}${note}` }] };
|
|
2194
|
+
return applyMatch(matchResult[0], filePath, resolved, originalContent, searchContent, replaceContent);
|
|
1995
2195
|
}
|
|
1996
2196
|
catch (err) {
|
|
1997
2197
|
logError("apply_workspace_patch", err);
|
|
1998
2198
|
return { content: [{ type: "text", text: `Error applying patch: ${err.message}` }] };
|
|
1999
2199
|
}
|
|
2000
2200
|
}
|
|
2201
|
+
// Async replacement for spawnSync: a blocking syscall inside a `tools/call` handler
|
|
2202
|
+
// freezes the whole (single-threaded) MCP server for the run's entire duration — including
|
|
2203
|
+
// task-status polls for THIS run once it's task-augmented (see runAsTask in tasks.ts), which
|
|
2204
|
+
// would otherwise defeat the point of making the tool pollable. `spawn` keeps the event loop
|
|
2205
|
+
// free while the command runs. Shape of the resolved value mirrors spawnSync's return value
|
|
2206
|
+
// on the fields the caller actually reads, so the rest of handleRunWorkspaceCommand is unchanged.
|
|
2207
|
+
function spawnCollect(command, cwd, timeoutMs) {
|
|
2208
|
+
return new Promise((resolve) => {
|
|
2209
|
+
const child = spawn(command, {
|
|
2210
|
+
cwd,
|
|
2211
|
+
shell: true,
|
|
2212
|
+
env: { ...process.env },
|
|
2213
|
+
// spawnSync closed stdin immediately (no `input` option given), so a command reading
|
|
2214
|
+
// from stdin (e.g. `sed` with no file argument) saw EOF right away. Plain `spawn`
|
|
2215
|
+
// leaves stdin as an open pipe by default — nothing ever writes to or closes it — so
|
|
2216
|
+
// the same command hangs until the timeout kills it. "ignore" reproduces the old,
|
|
2217
|
+
// immediate-EOF behavior.
|
|
2218
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2219
|
+
});
|
|
2220
|
+
let stdout = "";
|
|
2221
|
+
let stderr = "";
|
|
2222
|
+
let stdoutBytes = 0;
|
|
2223
|
+
let stderrBytes = 0;
|
|
2224
|
+
const MAX_BUFFER = 20 * 1024 * 1024;
|
|
2225
|
+
let timedOut = false;
|
|
2226
|
+
let settled = false;
|
|
2227
|
+
const timer = setTimeout(() => {
|
|
2228
|
+
timedOut = true;
|
|
2229
|
+
child.kill("SIGTERM");
|
|
2230
|
+
}, timeoutMs);
|
|
2231
|
+
child.stdout?.on("data", (chunk) => {
|
|
2232
|
+
if (stdoutBytes >= MAX_BUFFER)
|
|
2233
|
+
return;
|
|
2234
|
+
stdoutBytes += chunk.length;
|
|
2235
|
+
stdout += chunk.toString("utf8");
|
|
2236
|
+
});
|
|
2237
|
+
child.stderr?.on("data", (chunk) => {
|
|
2238
|
+
if (stderrBytes >= MAX_BUFFER)
|
|
2239
|
+
return;
|
|
2240
|
+
stderrBytes += chunk.length;
|
|
2241
|
+
stderr += chunk.toString("utf8");
|
|
2242
|
+
});
|
|
2243
|
+
child.on("error", (err) => {
|
|
2244
|
+
if (settled)
|
|
2245
|
+
return;
|
|
2246
|
+
settled = true;
|
|
2247
|
+
clearTimeout(timer);
|
|
2248
|
+
resolve({ stdout, stderr, status: null, timedOut, error: err });
|
|
2249
|
+
});
|
|
2250
|
+
child.on("close", (code) => {
|
|
2251
|
+
if (settled)
|
|
2252
|
+
return;
|
|
2253
|
+
settled = true;
|
|
2254
|
+
clearTimeout(timer);
|
|
2255
|
+
resolve({ stdout, stderr, status: code, timedOut });
|
|
2256
|
+
});
|
|
2257
|
+
});
|
|
2258
|
+
}
|
|
2001
2259
|
async function handleRunWorkspaceCommand(args) {
|
|
2002
2260
|
const command = args?.command;
|
|
2003
2261
|
if (!command)
|
|
@@ -2027,10 +2285,10 @@ async function handleRunWorkspaceCommand(args) {
|
|
|
2027
2285
|
// ── End allowlist check ────────────────────────────────────────────────
|
|
2028
2286
|
const workspaceRoot = process.cwd();
|
|
2029
2287
|
try {
|
|
2030
|
-
//
|
|
2031
|
-
// stdio is overridden, which silently dropped the output of every tool
|
|
2032
|
-
// stderr (jest and tsc both do) on a successful run. It also gives us
|
|
2033
|
-
// the exit code through one code path instead of a throw-on-failure split.
|
|
2288
|
+
// spawn (via spawnCollect above) rather than execSync: execSync leaves stderr attached to
|
|
2289
|
+
// the parent unless stdio is overridden, which silently dropped the output of every tool
|
|
2290
|
+
// that reports on stderr (jest and tsc both do) on a successful run. It also gives us
|
|
2291
|
+
// stdout, stderr and the exit code through one code path instead of a throw-on-failure split.
|
|
2034
2292
|
// 15s was below the runtime of the commands this tool exists for: a real `npm test` or
|
|
2035
2293
|
// `tsc --noEmit` always tripped it, so the distillation pipeline never ran on the output
|
|
2036
2294
|
// it was built for and the call was pure overhead before falling back to a plain shell.
|
|
@@ -2042,15 +2300,8 @@ async function handleRunWorkspaceCommand(args) {
|
|
|
2042
2300
|
if (elicited.supported && !elicited.confirmed) {
|
|
2043
2301
|
return { content: [{ type: "text", text: `Command was not run: declined during confirmation.\n\nCommand: ${command}` }] };
|
|
2044
2302
|
}
|
|
2045
|
-
const result =
|
|
2046
|
-
|
|
2047
|
-
shell: true,
|
|
2048
|
-
encoding: "utf8",
|
|
2049
|
-
timeout: timeoutMs,
|
|
2050
|
-
maxBuffer: 20 * 1024 * 1024,
|
|
2051
|
-
env: { ...process.env },
|
|
2052
|
-
});
|
|
2053
|
-
const timedOut = !!result.error && result.error.code === "ETIMEDOUT";
|
|
2303
|
+
const result = await spawnCollect(command, workspaceRoot, timeoutMs);
|
|
2304
|
+
const timedOut = result.timedOut;
|
|
2054
2305
|
if (result.error && !timedOut) {
|
|
2055
2306
|
return {
|
|
2056
2307
|
content: [{ type: "text", text: `Command could not be executed: ${result.error.message}` }],
|
|
@@ -4963,6 +5214,37 @@ async function handleSmartFileSlice(args) {
|
|
|
4963
5214
|
}
|
|
4964
5215
|
}
|
|
4965
5216
|
// ── Test Oracle ──────────────────────────────────────────────────────
|
|
5217
|
+
/**
|
|
5218
|
+
* Auto-capture a green test run into the Brain, tagged `outcome: 'confirmed'` and tracked
|
|
5219
|
+
* against the files actually changed. This is the auto-store hook: today store_memory only
|
|
5220
|
+
* fires when the agent remembers to call it, so working fixes routinely go uncaptured. A
|
|
5221
|
+
* passing suite over an uncommitted diff is real, cheap evidence something worked — reusing
|
|
5222
|
+
* it costs nothing store_memory wouldn't have cost anyway, and store()'s own dedup guard
|
|
5223
|
+
* means re-running the same green suite repeatedly just no-ops instead of piling up entries.
|
|
5224
|
+
*
|
|
5225
|
+
* Silent on a clean tree (nothing changed, nothing to attribute the pass to) or when the
|
|
5226
|
+
* repo isn't git-tracked. Returns a short suffix for resultText — this must never happen
|
|
5227
|
+
* invisibly, or a future search_memory hit would look reasoned-from-scratch instead of
|
|
5228
|
+
* traced back to an automated test run.
|
|
5229
|
+
*/
|
|
5230
|
+
function autoStoreGreenTests(command, workspaceRoot) {
|
|
5231
|
+
try {
|
|
5232
|
+
const changed = changedFilesFromGit(workspaceRoot);
|
|
5233
|
+
if (changed.length === 0)
|
|
5234
|
+
return "";
|
|
5235
|
+
const domain = deriveDomainForFile(changed[0]);
|
|
5236
|
+
const query = `tests passing after changes to ${changed.slice(0, 5).join(", ")}${changed.length > 5 ? ` and ${changed.length - 5} more file(s)` : ""}`;
|
|
5237
|
+
const response = `\`${command}\` passed with these files changed (uncommitted at capture time):\n${changed.join("\n")}`;
|
|
5238
|
+
const storeRes = getBrain().store(query, response, "test_oracle_auto", 0.92, changed, undefined, "confirmed", undefined, undefined, domain);
|
|
5239
|
+
if (!storeRes.stored)
|
|
5240
|
+
return "";
|
|
5241
|
+
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.`;
|
|
5242
|
+
}
|
|
5243
|
+
catch {
|
|
5244
|
+
// Never let auto-capture break the actual test result the caller asked for.
|
|
5245
|
+
return "";
|
|
5246
|
+
}
|
|
5247
|
+
}
|
|
4966
5248
|
async function handleTestOracle(args) {
|
|
4967
5249
|
const command = args.command || "npm test";
|
|
4968
5250
|
const workspaceRoot = process.cwd();
|
|
@@ -4973,6 +5255,7 @@ async function handleTestOracle(args) {
|
|
|
4973
5255
|
try {
|
|
4974
5256
|
const out = execSync(command, { cwd: workspaceRoot, encoding: "utf8", timeout: 30000 });
|
|
4975
5257
|
resultText = `All tests passed!\n\nOutput:\n${out.substring(0, 1000)}`;
|
|
5258
|
+
resultText += autoStoreGreenTests(command, workspaceRoot);
|
|
4976
5259
|
}
|
|
4977
5260
|
catch (e) {
|
|
4978
5261
|
const stdout = e.stdout || "";
|
|
@@ -5288,6 +5571,41 @@ async function handleFileIntentIndex(args) {
|
|
|
5288
5571
|
}
|
|
5289
5572
|
}
|
|
5290
5573
|
const COG_MAP_FILE = path.join(os.homedir(), ".lemma-cache", "cog_map.json");
|
|
5574
|
+
/**
|
|
5575
|
+
* Best-effort domain for a repo-relative file path, so store_memory and the test-oracle
|
|
5576
|
+
* auto-store below don't require a human to type `domain` by hand every time.
|
|
5577
|
+
*
|
|
5578
|
+
* Prefers the persisted Cognitive Map (built via `cognitive_map` action "build") when it
|
|
5579
|
+
* has this exact file listed — that reflects an actual workspace scan. Falls back to the
|
|
5580
|
+
* same lightweight substring heuristic "build" uses, so a project that never ran "build"
|
|
5581
|
+
* still gets a reasonable tag instead of none at all. Returns undefined rather than
|
|
5582
|
+
* "general" when nothing matches — an absent domain stays visible to every domain-scoped
|
|
5583
|
+
* search (see BrainEntry.domain), while a wrong guess would actively mislead one.
|
|
5584
|
+
*/
|
|
5585
|
+
function deriveDomainForFile(relPath) {
|
|
5586
|
+
const norm = relPath.replace(/^\//, "");
|
|
5587
|
+
if (fs.existsSync(COG_MAP_FILE)) {
|
|
5588
|
+
try {
|
|
5589
|
+
const map = JSON.parse(fs.readFileSync(COG_MAP_FILE, "utf8"));
|
|
5590
|
+
for (const node of Object.values(map.nodes || {})) {
|
|
5591
|
+
if (node.files.includes(norm))
|
|
5592
|
+
return node.domain;
|
|
5593
|
+
}
|
|
5594
|
+
}
|
|
5595
|
+
catch { /* fall through to heuristic */ }
|
|
5596
|
+
}
|
|
5597
|
+
if (norm.includes("mcp"))
|
|
5598
|
+
return "mcp";
|
|
5599
|
+
if (norm.includes("subconscious") || norm.includes("brain"))
|
|
5600
|
+
return "memory";
|
|
5601
|
+
if (norm.includes("security") || norm.includes("scrubber"))
|
|
5602
|
+
return "security";
|
|
5603
|
+
if (norm.includes("pr-review"))
|
|
5604
|
+
return "pr-review";
|
|
5605
|
+
if (norm.includes("utils"))
|
|
5606
|
+
return "utils";
|
|
5607
|
+
return undefined;
|
|
5608
|
+
}
|
|
5291
5609
|
async function handleCognitiveMap(args) {
|
|
5292
5610
|
const action = args.action;
|
|
5293
5611
|
const domain = args.domain;
|