@nxuss/lemma 1.7.1 → 1.7.2
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/dist/cjs/mcp/tools.d.ts.map +1 -1
- package/dist/cjs/mcp/tools.js +115 -15
- package/dist/cjs/mcp/tools.js.map +1 -1
- package/dist/cjs/proxy/ComplexityRouter.d.ts +7 -0
- package/dist/cjs/proxy/ComplexityRouter.d.ts.map +1 -1
- package/dist/cjs/proxy/ComplexityRouter.js +18 -5
- package/dist/cjs/proxy/ComplexityRouter.js.map +1 -1
- package/dist/cjs/utils/TokenReceipt.d.ts +6 -0
- package/dist/cjs/utils/TokenReceipt.d.ts.map +1 -1
- package/dist/cjs/utils/TokenReceipt.js +9 -0
- package/dist/cjs/utils/TokenReceipt.js.map +1 -1
- package/dist/esm/mcp/tools.d.ts.map +1 -1
- package/dist/esm/mcp/tools.js +116 -16
- package/dist/esm/mcp/tools.js.map +1 -1
- package/dist/esm/proxy/ComplexityRouter.d.ts +7 -0
- package/dist/esm/proxy/ComplexityRouter.d.ts.map +1 -1
- package/dist/esm/proxy/ComplexityRouter.js +18 -5
- package/dist/esm/proxy/ComplexityRouter.js.map +1 -1
- package/dist/esm/utils/TokenReceipt.d.ts +6 -0
- package/dist/esm/utils/TokenReceipt.d.ts.map +1 -1
- package/dist/esm/utils/TokenReceipt.js +8 -0
- package/dist/esm/utils/TokenReceipt.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAiiCnE,oFAAoF;AACpF,wBAAgB,kBAAkB,IAAI,MAAM,CAM3C;AAED,oEAAoE;AACpE,wBAAgB,sBAAsB,IAAI,MAAM,EAAE,CAEjD;AAED,iEAAiE;AACjE,wBAAgB,sBAAsB,IAAI,MAAM,EAAE,CAEjD;AAED,iFAAiF;AACjF,wBAAgB,qBAAqB,IAAI;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB,CAQA;AAoHD,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC1B,MAAM,EAAE,IAAI,GAAG,OAAO,GAAG,KAAK,GAAG,MAAM,CAAC;IACxC,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAqCD,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,EACd,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,GAC1C,IAAI,CAwEN"}
|
package/dist/cjs/mcp/tools.js
CHANGED
|
@@ -146,6 +146,11 @@ const toolDefinitions = [
|
|
|
146
146
|
properties: {
|
|
147
147
|
query: { type: "string", description: "The natural language query" },
|
|
148
148
|
limit: { type: "number", description: "Maximum results to return", default: 5 },
|
|
149
|
+
minSimilarity: {
|
|
150
|
+
type: "number",
|
|
151
|
+
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.",
|
|
152
|
+
default: 0.75,
|
|
153
|
+
},
|
|
149
154
|
},
|
|
150
155
|
required: ["query"],
|
|
151
156
|
},
|
|
@@ -1199,6 +1204,41 @@ async function handleToolbox(args) {
|
|
|
1199
1204
|
}
|
|
1200
1205
|
throw new Error(`Unknown action: ${action}. Use 'list', 'schema', or 'call'.`);
|
|
1201
1206
|
}
|
|
1207
|
+
/** Tools whose whole job is to put file contents into the model's context. */
|
|
1208
|
+
const FILE_READ_TOOLS = new Set([
|
|
1209
|
+
"read_workspace_file",
|
|
1210
|
+
"smart_file_slice",
|
|
1211
|
+
"get_symbol_surgical_context",
|
|
1212
|
+
"get_ast_hologram",
|
|
1213
|
+
"list_workspace_dir",
|
|
1214
|
+
"search_workspace",
|
|
1215
|
+
"read_token_budgeted",
|
|
1216
|
+
"bulk_file_digest",
|
|
1217
|
+
"import_tree_context",
|
|
1218
|
+
]);
|
|
1219
|
+
function receiptTypeForTool(name) {
|
|
1220
|
+
return FILE_READ_TOOLS.has(name) ? "file_read" : "tool_call";
|
|
1221
|
+
}
|
|
1222
|
+
/**
|
|
1223
|
+
* A receipt entry is only useful if it says what the call was about. Pull the most
|
|
1224
|
+
* identifying argument without dragging whole file contents into the ledger.
|
|
1225
|
+
*/
|
|
1226
|
+
function receiptLabelMeta(name, args) {
|
|
1227
|
+
if (!args)
|
|
1228
|
+
return {};
|
|
1229
|
+
const meta = {};
|
|
1230
|
+
if (typeof args.filePath === "string")
|
|
1231
|
+
meta.filePath = args.filePath;
|
|
1232
|
+
if (typeof args.dirPath === "string")
|
|
1233
|
+
meta.dirPath = args.dirPath;
|
|
1234
|
+
if (typeof args.query === "string")
|
|
1235
|
+
meta.query = args.query.substring(0, 100);
|
|
1236
|
+
if (typeof args.command === "string")
|
|
1237
|
+
meta.command = args.command.substring(0, 100);
|
|
1238
|
+
if (typeof args.symbolName === "string")
|
|
1239
|
+
meta.symbolName = args.symbolName;
|
|
1240
|
+
return meta;
|
|
1241
|
+
}
|
|
1202
1242
|
function setupToolsHandlers(server, onToolCall) {
|
|
1203
1243
|
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
1204
1244
|
tools: toolDefinitionsArray,
|
|
@@ -1233,7 +1273,16 @@ function setupToolsHandlers(server, onToolCall) {
|
|
|
1233
1273
|
throw new Error(`Unknown tool: ${name}`);
|
|
1234
1274
|
}
|
|
1235
1275
|
try {
|
|
1276
|
+
// Sampled around the handler so a tool that logs its own, more specific event
|
|
1277
|
+
// (a cache hit, a miss) isn't double-counted by the generic entry below.
|
|
1278
|
+
const ledgerBefore = (0, TokenReceipt_1.getLedgerLength)();
|
|
1236
1279
|
const result = await handler((args || {}));
|
|
1280
|
+
if ((0, TokenReceipt_1.getLedgerLength)() === ledgerBefore) {
|
|
1281
|
+
(0, TokenReceipt_1.recordReceiptEvent)(receiptTypeForTool(name), name, {
|
|
1282
|
+
tool: name,
|
|
1283
|
+
...receiptLabelMeta(name, args),
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1237
1286
|
const tokensImpact = (0, utils_1.estimateTokensFromResult)(result);
|
|
1238
1287
|
onToolCall?.({
|
|
1239
1288
|
tool: name,
|
|
@@ -1268,16 +1317,35 @@ async function handleScrubPrivacy(args) {
|
|
|
1268
1317
|
const { maskedPrompt } = scrubber.mask(text);
|
|
1269
1318
|
return { content: [{ type: "text", text: maskedPrompt }] };
|
|
1270
1319
|
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Minimum similarity for a stored memory to be offered for reuse. Shared by
|
|
1322
|
+
* search_memory and smarter_cache so the two never disagree about whether the same
|
|
1323
|
+
* query is a hit.
|
|
1324
|
+
*/
|
|
1325
|
+
const DEFAULT_MEMORY_SIMILARITY_FLOOR = 0.75;
|
|
1271
1326
|
async function handleSearchMemory(args) {
|
|
1272
1327
|
const query = args?.query;
|
|
1273
1328
|
const limit = args?.limit || 5;
|
|
1329
|
+
// A floor of 0 returns the nearest neighbour no matter how unrelated it is, and the
|
|
1330
|
+
// formatting below then presents it as a reusable memory. Match smarter_cache's
|
|
1331
|
+
// threshold so both paths agree on what counts as a hit.
|
|
1332
|
+
const minSimilarity = typeof args?.minSimilarity === "number" ? args.minSimilarity : DEFAULT_MEMORY_SIMILARITY_FLOOR;
|
|
1274
1333
|
if (!query)
|
|
1275
1334
|
throw new Error("Query is required");
|
|
1276
1335
|
try {
|
|
1277
1336
|
const brain = (0, TheBrainV2_1.getBrain)();
|
|
1278
|
-
const results = brain.search(query, limit,
|
|
1337
|
+
const results = brain.search(query, limit, minSimilarity, { projectId: (0, TheBrainV2_1.deriveProjectId)() });
|
|
1279
1338
|
if (results.length === 0) {
|
|
1280
|
-
|
|
1339
|
+
(0, TokenReceipt_1.recordReceiptEvent)("reasoning", query.substring(0, 100), {
|
|
1340
|
+
tool: "search_memory",
|
|
1341
|
+
reason: `no memory above the ${minSimilarity} similarity floor`,
|
|
1342
|
+
});
|
|
1343
|
+
return {
|
|
1344
|
+
content: [{
|
|
1345
|
+
type: "text",
|
|
1346
|
+
text: `No memory in Lemma's Brain scored at or above the ${(minSimilarity * 100).toFixed(0)}% similarity floor for this query. Investigate from scratch — there is nothing safe to reuse.`,
|
|
1347
|
+
}],
|
|
1348
|
+
};
|
|
1281
1349
|
}
|
|
1282
1350
|
const fresh = results.filter((r) => r.fresh);
|
|
1283
1351
|
const stale = results.filter((r) => !r.fresh);
|
|
@@ -1288,6 +1356,17 @@ async function handleSearchMemory(args) {
|
|
|
1288
1356
|
const bestFresh = fresh[0];
|
|
1289
1357
|
const tokensSaved = Math.max(100, Math.floor(String(bestFresh.response).length / 4));
|
|
1290
1358
|
(0, reportSavings_1.reportSavings)({ source: "cache", tokens: tokensSaved, toolName: "search_memory", query: query.substring(0, 100) });
|
|
1359
|
+
(0, TokenReceipt_1.recordReceiptEvent)("semantic_cache_hit", query.substring(0, 100), {
|
|
1360
|
+
tool: "search_memory",
|
|
1361
|
+
similarity: bestFresh.similarity,
|
|
1362
|
+
tokensSaved,
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
else {
|
|
1366
|
+
(0, TokenReceipt_1.recordReceiptEvent)("reasoning", query.substring(0, 100), {
|
|
1367
|
+
tool: "search_memory",
|
|
1368
|
+
reason: `${stale.length} similar memory/memories found but all stale — must re-verify`,
|
|
1369
|
+
});
|
|
1291
1370
|
}
|
|
1292
1371
|
const parts = [];
|
|
1293
1372
|
if (fresh.length > 0) {
|
|
@@ -1316,12 +1395,13 @@ async function handleStoreMemory(args) {
|
|
|
1316
1395
|
try {
|
|
1317
1396
|
const brain = (0, TheBrainV2_1.getBrain)();
|
|
1318
1397
|
const storeRes = brain.store(query, responseText, provider, 0.92, filePaths);
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1398
|
+
// Storing a memory saves nothing — it only creates the chance of a saving later.
|
|
1399
|
+
// Crediting tokens here inflated the ledger on write and then credited the same
|
|
1400
|
+
// answer again on every read. The saving is booked by search_memory on a fresh hit.
|
|
1401
|
+
(0, TokenReceipt_1.recordReceiptEvent)("tool_call", `store_memory: ${query.substring(0, 100)}`, {
|
|
1402
|
+
tool: "store_memory",
|
|
1403
|
+
filePaths,
|
|
1404
|
+
note: "memory written — no tokens saved yet",
|
|
1325
1405
|
});
|
|
1326
1406
|
const trackingNote = filePaths && filePaths.length > 0 ? ` Tracking freshness against ${filePaths.length} file(s) — this memory auto-invalidates if they change.` : "";
|
|
1327
1407
|
return { content: [{ type: "text", text: `Success: Memory stored. ${storeRes.reason}${trackingNote}` }] };
|
|
@@ -1843,7 +1923,7 @@ async function handleGetProjectOnboarding(_args) {
|
|
|
1843
1923
|
],
|
|
1844
1924
|
};
|
|
1845
1925
|
}
|
|
1846
|
-
function extractSymbolsWithTsCompiler(filePath, relPath) {
|
|
1926
|
+
function extractSymbolsWithTsCompiler(filePath, relPath, parseErrors) {
|
|
1847
1927
|
try {
|
|
1848
1928
|
const src = fs_1.default.readFileSync(filePath, "utf8");
|
|
1849
1929
|
const sourceFile = ts.createSourceFile(filePath, src, ts.ScriptTarget.Latest, true);
|
|
@@ -1851,7 +1931,10 @@ function extractSymbolsWithTsCompiler(filePath, relPath) {
|
|
|
1851
1931
|
function visit(node) {
|
|
1852
1932
|
const mods = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
|
|
1853
1933
|
const isExport = mods?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
1854
|
-
|
|
1934
|
+
// node.parent is undefined on the SourceFile root itself, and ts.isSourceFile()
|
|
1935
|
+
// dereferences .kind without a guard — calling it unguarded threw on the very
|
|
1936
|
+
// first visit(), so the catch below swallowed it and every file returned [].
|
|
1937
|
+
if (isExport || (node.parent && ts.isSourceFile(node.parent))) {
|
|
1855
1938
|
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
1856
1939
|
symbols.push({
|
|
1857
1940
|
kind: "function",
|
|
@@ -1920,10 +2003,11 @@ function extractSymbolsWithTsCompiler(filePath, relPath) {
|
|
|
1920
2003
|
}
|
|
1921
2004
|
catch (err) {
|
|
1922
2005
|
(0, utils_1.logWarn)("get_ast_hologram", `Could not parse ${relPath}: ${err}`);
|
|
2006
|
+
parseErrors?.push(`${relPath}: ${err}`);
|
|
1923
2007
|
return [];
|
|
1924
2008
|
}
|
|
1925
2009
|
}
|
|
1926
|
-
function walkDirForHologram(dir, rel, extensions) {
|
|
2010
|
+
function walkDirForHologram(dir, rel, extensions, stats) {
|
|
1927
2011
|
let allSymbols = [];
|
|
1928
2012
|
try {
|
|
1929
2013
|
const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
@@ -1933,18 +2017,20 @@ function walkDirForHologram(dir, rel, extensions) {
|
|
|
1933
2017
|
const fullPath = path_1.default.join(dir, entry.name);
|
|
1934
2018
|
const relPath = rel ? path_1.default.join(rel, entry.name) : entry.name;
|
|
1935
2019
|
if (entry.isDirectory()) {
|
|
1936
|
-
allSymbols = allSymbols.concat(walkDirForHologram(fullPath, relPath, extensions));
|
|
2020
|
+
allSymbols = allSymbols.concat(walkDirForHologram(fullPath, relPath, extensions, stats));
|
|
1937
2021
|
}
|
|
1938
2022
|
else {
|
|
1939
2023
|
const ext = entry.name.split(".").pop() || "";
|
|
1940
2024
|
if (extensions.includes(ext)) {
|
|
1941
|
-
|
|
2025
|
+
stats.filesScanned++;
|
|
2026
|
+
allSymbols = allSymbols.concat(extractSymbolsWithTsCompiler(fullPath, relPath, stats.parseErrors));
|
|
1942
2027
|
}
|
|
1943
2028
|
}
|
|
1944
2029
|
}
|
|
1945
2030
|
}
|
|
1946
2031
|
catch (err) {
|
|
1947
2032
|
(0, utils_1.logWarn)("get_ast_hologram", `Error walking dir ${dir}: ${err}`);
|
|
2033
|
+
stats.parseErrors.push(`walk ${rel || "."}: ${err}`);
|
|
1948
2034
|
}
|
|
1949
2035
|
return allSymbols;
|
|
1950
2036
|
}
|
|
@@ -1953,18 +2039,32 @@ async function handleGetAstHologram(args) {
|
|
|
1953
2039
|
const extensions = args?.extensions || ["ts", "tsx", "js", "jsx"];
|
|
1954
2040
|
const workspaceRoot = process.cwd();
|
|
1955
2041
|
const { resolved } = (0, utils_1.safeResolvePath)(workspaceRoot, dirPath);
|
|
1956
|
-
const
|
|
2042
|
+
const stats = { filesScanned: 0, parseErrors: [] };
|
|
2043
|
+
const symbols = walkDirForHologram(resolved, dirPath, extensions, stats);
|
|
1957
2044
|
const byFile = {};
|
|
1958
2045
|
symbols.forEach((s) => {
|
|
1959
2046
|
if (!byFile[s.file])
|
|
1960
2047
|
byFile[s.file] = [];
|
|
1961
2048
|
byFile[s.file].push({ kind: s.kind, name: s.name, line: s.line });
|
|
1962
2049
|
});
|
|
2050
|
+
// An empty map is reported as a failure, not as an answer: a silent "0 symbols" is
|
|
2051
|
+
// indistinguishable from a working scan of an empty dir, which is how a parse bug
|
|
2052
|
+
// stayed invisible here before. Say when nothing could be read.
|
|
2053
|
+
const warning = stats.filesScanned === 0
|
|
2054
|
+
? `No files matching [${extensions.join(", ")}] were found under "${dirPath || "."}" — check the path and extensions.`
|
|
2055
|
+
: symbols.length === 0
|
|
2056
|
+
? `Scanned ${stats.filesScanned} file(s) but extracted 0 symbols — this is very likely an extraction failure, not an empty codebase. Do not treat this as "the directory has no exports".`
|
|
2057
|
+
: undefined;
|
|
1963
2058
|
const hologram = {
|
|
1964
2059
|
workspace: path_1.default.basename(workspaceRoot),
|
|
1965
2060
|
scannedDir: dirPath || ".",
|
|
1966
2061
|
totalSymbols: symbols.length,
|
|
2062
|
+
filesScanned: stats.filesScanned,
|
|
1967
2063
|
totalFiles: Object.keys(byFile).length,
|
|
2064
|
+
...(warning ? { warning } : {}),
|
|
2065
|
+
...(stats.parseErrors.length > 0
|
|
2066
|
+
? { parseErrors: stats.parseErrors.slice(0, 10), parseErrorCount: stats.parseErrors.length }
|
|
2067
|
+
: {}),
|
|
1968
2068
|
map: byFile,
|
|
1969
2069
|
};
|
|
1970
2070
|
return { content: [{ type: "text", text: JSON.stringify(hologram, null, 2) }] };
|
|
@@ -2367,7 +2467,7 @@ async function handleCompressContext(args) {
|
|
|
2367
2467
|
async function handleSmarterCache(args) {
|
|
2368
2468
|
const query = args?.query;
|
|
2369
2469
|
const context = args?.context || "";
|
|
2370
|
-
const threshold = typeof args?.threshold === "number" ? args.threshold :
|
|
2470
|
+
const threshold = typeof args?.threshold === "number" ? args.threshold : DEFAULT_MEMORY_SIMILARITY_FLOOR;
|
|
2371
2471
|
if (!query)
|
|
2372
2472
|
throw new Error("query is required");
|
|
2373
2473
|
const fullQuery = context ? `${query}\n\nContext: ${context}` : query;
|