@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.
@@ -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;AA2hCnE,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;AAED,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,EACd,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,GAC1C,IAAI,CA+DN"}
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"}
@@ -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, 0, { projectId: (0, TheBrainV2_1.deriveProjectId)() });
1337
+ const results = brain.search(query, limit, minSimilarity, { projectId: (0, TheBrainV2_1.deriveProjectId)() });
1279
1338
  if (results.length === 0) {
1280
- return { content: [{ type: "text", text: "No relevant memories found in Lemma's Brain." }] };
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
- const tokensSaved = Math.max(100, Math.floor(responseText.length / 4));
1320
- (0, reportSavings_1.reportSavings)({
1321
- source: "cache",
1322
- tokens: tokensSaved,
1323
- toolName: "store_memory",
1324
- query: query.substring(0, 100),
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
- if (isExport || ts.isSourceFile(node.parent)) {
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
- allSymbols = allSymbols.concat(extractSymbolsWithTsCompiler(fullPath, relPath));
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 symbols = walkDirForHologram(resolved, dirPath, extensions);
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 : 0.75;
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;