@nxuss/lemma 1.7.1 → 1.8.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 +69 -8
- package/bin/brain-ingest.js +56 -0
- package/dist/cjs/mcp/tools.d.ts.map +1 -1
- package/dist/cjs/mcp/tools.js +247 -48
- package/dist/cjs/mcp/tools.js.map +1 -1
- package/dist/cjs/pr-review/bridge/BrainBridge.d.ts +11 -1
- package/dist/cjs/pr-review/bridge/BrainBridge.d.ts.map +1 -1
- package/dist/cjs/pr-review/bridge/BrainBridge.js +18 -23
- package/dist/cjs/pr-review/bridge/BrainBridge.js.map +1 -1
- package/dist/cjs/protocol/utils.d.ts.map +1 -1
- package/dist/cjs/protocol/utils.js +6 -5
- package/dist/cjs/protocol/utils.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/subconscious/GitIngest.d.ts +48 -0
- package/dist/cjs/subconscious/GitIngest.d.ts.map +1 -0
- package/dist/cjs/subconscious/GitIngest.js +175 -0
- package/dist/cjs/subconscious/GitIngest.js.map +1 -0
- package/dist/cjs/subconscious/TheBrainV2.d.ts +23 -1
- package/dist/cjs/subconscious/TheBrainV2.d.ts.map +1 -1
- package/dist/cjs/subconscious/TheBrainV2.js +124 -17
- package/dist/cjs/subconscious/TheBrainV2.js.map +1 -1
- package/dist/cjs/utils/ConversationCheckpoint.d.ts +2 -0
- package/dist/cjs/utils/ConversationCheckpoint.d.ts.map +1 -1
- package/dist/cjs/utils/ConversationCheckpoint.js +38 -0
- package/dist/cjs/utils/ConversationCheckpoint.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 +250 -51
- package/dist/esm/mcp/tools.js.map +1 -1
- package/dist/esm/pr-review/bridge/BrainBridge.d.ts +11 -1
- package/dist/esm/pr-review/bridge/BrainBridge.d.ts.map +1 -1
- package/dist/esm/pr-review/bridge/BrainBridge.js +18 -20
- package/dist/esm/pr-review/bridge/BrainBridge.js.map +1 -1
- package/dist/esm/protocol/utils.d.ts.map +1 -1
- package/dist/esm/protocol/utils.js +6 -5
- package/dist/esm/protocol/utils.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/subconscious/GitIngest.d.ts +48 -0
- package/dist/esm/subconscious/GitIngest.d.ts.map +1 -0
- package/dist/esm/subconscious/GitIngest.js +166 -0
- package/dist/esm/subconscious/GitIngest.js.map +1 -0
- package/dist/esm/subconscious/TheBrainV2.d.ts +23 -1
- package/dist/esm/subconscious/TheBrainV2.d.ts.map +1 -1
- package/dist/esm/subconscious/TheBrainV2.js +124 -17
- package/dist/esm/subconscious/TheBrainV2.js.map +1 -1
- package/dist/esm/utils/ConversationCheckpoint.d.ts +2 -0
- package/dist/esm/utils/ConversationCheckpoint.d.ts.map +1 -1
- package/dist/esm/utils/ConversationCheckpoint.js +37 -0
- package/dist/esm/utils/ConversationCheckpoint.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 +4 -2
package/dist/cjs/mcp/tools.js
CHANGED
|
@@ -101,7 +101,7 @@ function isProUser() {
|
|
|
101
101
|
}
|
|
102
102
|
const FREE_TOOLS = new Set([
|
|
103
103
|
// Cache & Memory — the hook that shows instant value
|
|
104
|
-
"smarter_cache", "state_hash_cache", "token_receipt", "search_memory", "store_memory",
|
|
104
|
+
"smarter_cache", "state_hash_cache", "token_receipt", "search_memory", "store_memory", "get_project_history",
|
|
105
105
|
// Token optimization — shows what they're saving
|
|
106
106
|
"token_budget", "squeeze_prompt", "turbosqueeze",
|
|
107
107
|
// Utility — just enough to function
|
|
@@ -146,13 +146,18 @@ 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
|
},
|
|
152
157
|
},
|
|
153
158
|
{
|
|
154
159
|
name: "store_memory",
|
|
155
|
-
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.",
|
|
160
|
+
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.",
|
|
156
161
|
inputSchema: {
|
|
157
162
|
type: "object",
|
|
158
163
|
properties: {
|
|
@@ -160,6 +165,19 @@ const toolDefinitions = [
|
|
|
160
165
|
response: { type: "string", description: "The complete technical content to memorize" },
|
|
161
166
|
provider: { type: "string", description: "Optional model provider name", default: "generic" },
|
|
162
167
|
filePaths: { type: "array", items: { type: "string" }, description: "Paths (relative to project root) this answer depends on. If any changes later, this memory is marked stale instead of being silently reused." },
|
|
168
|
+
symbols: {
|
|
169
|
+
type: "array",
|
|
170
|
+
items: {
|
|
171
|
+
type: "object",
|
|
172
|
+
properties: {
|
|
173
|
+
filePath: { type: "string", description: "Path (relative to project root) containing the symbol" },
|
|
174
|
+
symbolName: { type: "string", description: "Function/class/interface/type/enum/const name" },
|
|
175
|
+
},
|
|
176
|
+
required: ["filePath", "symbolName"],
|
|
177
|
+
},
|
|
178
|
+
description: "Symbols (not whole files) this answer depends on. Freshness is then judged by that symbol's own source, so edits elsewhere in the same file don't stale this memory out.",
|
|
179
|
+
},
|
|
180
|
+
outcome: { type: "string", enum: ["confirmed", "failed"], description: "Default 'confirmed' (a verified working solution). Set 'failed' to record a dead end — an approach ruled out, not one to recommend." },
|
|
163
181
|
},
|
|
164
182
|
required: ["query", "response"],
|
|
165
183
|
},
|
|
@@ -311,6 +329,16 @@ const toolDefinitions = [
|
|
|
311
329
|
properties: {},
|
|
312
330
|
},
|
|
313
331
|
},
|
|
332
|
+
{
|
|
333
|
+
name: "get_project_history",
|
|
334
|
+
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.",
|
|
335
|
+
inputSchema: {
|
|
336
|
+
type: "object",
|
|
337
|
+
properties: {
|
|
338
|
+
limit: { type: "number", description: "Max items per section (commits, checkpoints, Brain memories)", default: 10 },
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
},
|
|
314
342
|
{
|
|
315
343
|
name: "get_ast_hologram",
|
|
316
344
|
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.",
|
|
@@ -1159,6 +1187,7 @@ const toolHandlers = {
|
|
|
1159
1187
|
auto_context_bundle: handleAutoContextBundle,
|
|
1160
1188
|
repeat_guard: handleRepeatGuard,
|
|
1161
1189
|
conversation_checkpoint: handleConversationCheckpoint,
|
|
1190
|
+
get_project_history: handleGetProjectHistory,
|
|
1162
1191
|
test_autofix_interceptor: handleTestAutofixInterceptor,
|
|
1163
1192
|
prompt_pattern_cache: handlePromptPatternCache,
|
|
1164
1193
|
lemma_toolbox: handleToolbox,
|
|
@@ -1199,6 +1228,41 @@ async function handleToolbox(args) {
|
|
|
1199
1228
|
}
|
|
1200
1229
|
throw new Error(`Unknown action: ${action}. Use 'list', 'schema', or 'call'.`);
|
|
1201
1230
|
}
|
|
1231
|
+
/** Tools whose whole job is to put file contents into the model's context. */
|
|
1232
|
+
const FILE_READ_TOOLS = new Set([
|
|
1233
|
+
"read_workspace_file",
|
|
1234
|
+
"smart_file_slice",
|
|
1235
|
+
"get_symbol_surgical_context",
|
|
1236
|
+
"get_ast_hologram",
|
|
1237
|
+
"list_workspace_dir",
|
|
1238
|
+
"search_workspace",
|
|
1239
|
+
"read_token_budgeted",
|
|
1240
|
+
"bulk_file_digest",
|
|
1241
|
+
"import_tree_context",
|
|
1242
|
+
]);
|
|
1243
|
+
function receiptTypeForTool(name) {
|
|
1244
|
+
return FILE_READ_TOOLS.has(name) ? "file_read" : "tool_call";
|
|
1245
|
+
}
|
|
1246
|
+
/**
|
|
1247
|
+
* A receipt entry is only useful if it says what the call was about. Pull the most
|
|
1248
|
+
* identifying argument without dragging whole file contents into the ledger.
|
|
1249
|
+
*/
|
|
1250
|
+
function receiptLabelMeta(name, args) {
|
|
1251
|
+
if (!args)
|
|
1252
|
+
return {};
|
|
1253
|
+
const meta = {};
|
|
1254
|
+
if (typeof args.filePath === "string")
|
|
1255
|
+
meta.filePath = args.filePath;
|
|
1256
|
+
if (typeof args.dirPath === "string")
|
|
1257
|
+
meta.dirPath = args.dirPath;
|
|
1258
|
+
if (typeof args.query === "string")
|
|
1259
|
+
meta.query = args.query.substring(0, 100);
|
|
1260
|
+
if (typeof args.command === "string")
|
|
1261
|
+
meta.command = args.command.substring(0, 100);
|
|
1262
|
+
if (typeof args.symbolName === "string")
|
|
1263
|
+
meta.symbolName = args.symbolName;
|
|
1264
|
+
return meta;
|
|
1265
|
+
}
|
|
1202
1266
|
function setupToolsHandlers(server, onToolCall) {
|
|
1203
1267
|
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
1204
1268
|
tools: toolDefinitionsArray,
|
|
@@ -1233,7 +1297,16 @@ function setupToolsHandlers(server, onToolCall) {
|
|
|
1233
1297
|
throw new Error(`Unknown tool: ${name}`);
|
|
1234
1298
|
}
|
|
1235
1299
|
try {
|
|
1300
|
+
// Sampled around the handler so a tool that logs its own, more specific event
|
|
1301
|
+
// (a cache hit, a miss) isn't double-counted by the generic entry below.
|
|
1302
|
+
const ledgerBefore = (0, TokenReceipt_1.getLedgerLength)();
|
|
1236
1303
|
const result = await handler((args || {}));
|
|
1304
|
+
if ((0, TokenReceipt_1.getLedgerLength)() === ledgerBefore) {
|
|
1305
|
+
(0, TokenReceipt_1.recordReceiptEvent)(receiptTypeForTool(name), name, {
|
|
1306
|
+
tool: name,
|
|
1307
|
+
...receiptLabelMeta(name, args),
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1237
1310
|
const tokensImpact = (0, utils_1.estimateTokensFromResult)(result);
|
|
1238
1311
|
onToolCall?.({
|
|
1239
1312
|
tool: name,
|
|
@@ -1268,26 +1341,56 @@ async function handleScrubPrivacy(args) {
|
|
|
1268
1341
|
const { maskedPrompt } = scrubber.mask(text);
|
|
1269
1342
|
return { content: [{ type: "text", text: maskedPrompt }] };
|
|
1270
1343
|
}
|
|
1344
|
+
/**
|
|
1345
|
+
* Minimum similarity for a stored memory to be offered for reuse. Shared by
|
|
1346
|
+
* search_memory and smarter_cache so the two never disagree about whether the same
|
|
1347
|
+
* query is a hit.
|
|
1348
|
+
*/
|
|
1349
|
+
const DEFAULT_MEMORY_SIMILARITY_FLOOR = 0.75;
|
|
1271
1350
|
async function handleSearchMemory(args) {
|
|
1272
1351
|
const query = args?.query;
|
|
1273
1352
|
const limit = args?.limit || 5;
|
|
1353
|
+
// A floor of 0 returns the nearest neighbour no matter how unrelated it is, and the
|
|
1354
|
+
// formatting below then presents it as a reusable memory. Match smarter_cache's
|
|
1355
|
+
// threshold so both paths agree on what counts as a hit.
|
|
1356
|
+
const minSimilarity = typeof args?.minSimilarity === "number" ? args.minSimilarity : DEFAULT_MEMORY_SIMILARITY_FLOOR;
|
|
1274
1357
|
if (!query)
|
|
1275
1358
|
throw new Error("Query is required");
|
|
1276
1359
|
try {
|
|
1277
1360
|
const brain = (0, TheBrainV2_1.getBrain)();
|
|
1278
|
-
const results = brain.search(query, limit,
|
|
1361
|
+
const results = brain.search(query, limit, minSimilarity, { projectId: (0, TheBrainV2_1.deriveProjectId)() });
|
|
1279
1362
|
if (results.length === 0) {
|
|
1280
|
-
|
|
1363
|
+
(0, TokenReceipt_1.recordReceiptEvent)("reasoning", query.substring(0, 100), {
|
|
1364
|
+
tool: "search_memory",
|
|
1365
|
+
reason: `no memory above the ${minSimilarity} similarity floor`,
|
|
1366
|
+
});
|
|
1367
|
+
return {
|
|
1368
|
+
content: [{
|
|
1369
|
+
type: "text",
|
|
1370
|
+
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.`,
|
|
1371
|
+
}],
|
|
1372
|
+
};
|
|
1281
1373
|
}
|
|
1282
1374
|
const fresh = results.filter((r) => r.fresh);
|
|
1283
1375
|
const stale = results.filter((r) => !r.fresh);
|
|
1284
|
-
const formatResult = (r, i) => `Result ${i + 1} (Similarity: ${(r.similarity * 100).toFixed(1)}%)\nPrompt: ${r.query.substring(0, 300)}...\nResponse: ${typeof r.response === "string" ? r.response : JSON.stringify(r.response, null, 2)}`;
|
|
1376
|
+
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)}`;
|
|
1285
1377
|
if (fresh.length > 0) {
|
|
1286
1378
|
// Only a fresh hit (or an untracked, purely conceptual entry) counts as a real
|
|
1287
1379
|
// avoided re-investigation — this is the only case worth crediting as savings.
|
|
1288
1380
|
const bestFresh = fresh[0];
|
|
1289
1381
|
const tokensSaved = Math.max(100, Math.floor(String(bestFresh.response).length / 4));
|
|
1290
1382
|
(0, reportSavings_1.reportSavings)({ source: "cache", tokens: tokensSaved, toolName: "search_memory", query: query.substring(0, 100) });
|
|
1383
|
+
(0, TokenReceipt_1.recordReceiptEvent)("semantic_cache_hit", query.substring(0, 100), {
|
|
1384
|
+
tool: "search_memory",
|
|
1385
|
+
similarity: bestFresh.similarity,
|
|
1386
|
+
tokensSaved,
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
else {
|
|
1390
|
+
(0, TokenReceipt_1.recordReceiptEvent)("reasoning", query.substring(0, 100), {
|
|
1391
|
+
tool: "search_memory",
|
|
1392
|
+
reason: `${stale.length} similar memory/memories found but all stale — must re-verify`,
|
|
1393
|
+
});
|
|
1291
1394
|
}
|
|
1292
1395
|
const parts = [];
|
|
1293
1396
|
if (fresh.length > 0) {
|
|
@@ -1311,20 +1414,27 @@ async function handleStoreMemory(args) {
|
|
|
1311
1414
|
const responseText = args?.response;
|
|
1312
1415
|
const provider = args?.provider || "generic";
|
|
1313
1416
|
const filePaths = Array.isArray(args?.filePaths) ? args.filePaths : undefined;
|
|
1417
|
+
const outcome = args?.outcome === "failed" ? "failed" : undefined;
|
|
1418
|
+
const symbols = Array.isArray(args?.symbols)
|
|
1419
|
+
? args.symbols.filter((s) => s?.filePath && s?.symbolName)
|
|
1420
|
+
: undefined;
|
|
1314
1421
|
if (!query || !responseText)
|
|
1315
1422
|
throw new Error("Query and response are required");
|
|
1316
1423
|
try {
|
|
1317
1424
|
const brain = (0, TheBrainV2_1.getBrain)();
|
|
1318
|
-
const storeRes = brain.store(query, responseText, provider, 0.92, filePaths);
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1425
|
+
const storeRes = brain.store(query, responseText, provider, 0.92, filePaths, undefined, outcome, symbols);
|
|
1426
|
+
// Storing a memory saves nothing — it only creates the chance of a saving later.
|
|
1427
|
+
// Crediting tokens here inflated the ledger on write and then credited the same
|
|
1428
|
+
// answer again on every read. The saving is booked by search_memory on a fresh hit.
|
|
1429
|
+
(0, TokenReceipt_1.recordReceiptEvent)("tool_call", `store_memory: ${query.substring(0, 100)}`, {
|
|
1430
|
+
tool: "store_memory",
|
|
1431
|
+
filePaths,
|
|
1432
|
+
note: "memory written — no tokens saved yet",
|
|
1325
1433
|
});
|
|
1326
1434
|
const trackingNote = filePaths && filePaths.length > 0 ? ` Tracking freshness against ${filePaths.length} file(s) — this memory auto-invalidates if they change.` : "";
|
|
1327
|
-
|
|
1435
|
+
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.` : "";
|
|
1436
|
+
const outcomeNote = outcome === "failed" ? " Tagged as a FAILED attempt — future searches will surface it as a warning, not a suggestion." : "";
|
|
1437
|
+
return { content: [{ type: "text", text: `Success: Memory stored. ${storeRes.reason}${trackingNote}${symbolNote}${outcomeNote}` }] };
|
|
1328
1438
|
}
|
|
1329
1439
|
catch (e) {
|
|
1330
1440
|
(0, utils_1.logError)("store_memory", e);
|
|
@@ -1843,7 +1953,60 @@ async function handleGetProjectOnboarding(_args) {
|
|
|
1843
1953
|
],
|
|
1844
1954
|
};
|
|
1845
1955
|
}
|
|
1846
|
-
|
|
1956
|
+
// ── Project History (aggregate: git log + checkpoints + Brain) ─────
|
|
1957
|
+
/**
|
|
1958
|
+
* "What have we done in this project" today means piecing together search_memory, git log,
|
|
1959
|
+
* and manually reading .lemma/session/ checkpoint files by hand — three separate calls whose
|
|
1960
|
+
* results the model has to merge itself. This does that merge in one call.
|
|
1961
|
+
*/
|
|
1962
|
+
async function handleGetProjectHistory(args) {
|
|
1963
|
+
const cwd = process.cwd();
|
|
1964
|
+
const limit = typeof args?.limit === "number" ? args.limit : 10;
|
|
1965
|
+
try {
|
|
1966
|
+
const projectId = (0, TheBrainV2_1.deriveProjectId)(cwd);
|
|
1967
|
+
const brainEntries = (0, TheBrainV2_1.getBrain)()
|
|
1968
|
+
.getEntriesForProject(projectId)
|
|
1969
|
+
.sort((a, b) => (b.hits - a.hits) || (new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()))
|
|
1970
|
+
.slice(0, limit);
|
|
1971
|
+
const checkpoints = (0, ConversationCheckpoint_1.loadRecentCheckpoints)(cwd, Math.min(limit, 5));
|
|
1972
|
+
let recentCommits = [];
|
|
1973
|
+
try {
|
|
1974
|
+
const log = (0, child_process_1.execFileSync)("git", ["-C", cwd, "log", `-${limit}`, "--pretty=format:%h %s (%ar)"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
1975
|
+
recentCommits = log.split("\n").filter(Boolean);
|
|
1976
|
+
}
|
|
1977
|
+
catch {
|
|
1978
|
+
// Not a git repo, or no commits yet — sections below just won't include this one.
|
|
1979
|
+
}
|
|
1980
|
+
const parts = [`# Project history — ${path_1.default.basename(cwd)}`];
|
|
1981
|
+
if (recentCommits.length > 0) {
|
|
1982
|
+
parts.push(`## Recent commits\n${recentCommits.map((c) => `- ${c}`).join("\n")}`);
|
|
1983
|
+
}
|
|
1984
|
+
if (checkpoints.length > 0) {
|
|
1985
|
+
const checkpointLines = checkpoints.map((cp) => {
|
|
1986
|
+
const date = new Date(cp.timestamp).toISOString().replace("T", " ").slice(0, 16);
|
|
1987
|
+
return `- **${date}** — ${cp.summary.replace(/\n+/g, " ")}`;
|
|
1988
|
+
});
|
|
1989
|
+
parts.push(`## Session checkpoints\n${checkpointLines.join("\n")}`);
|
|
1990
|
+
}
|
|
1991
|
+
if (brainEntries.length > 0) {
|
|
1992
|
+
const brainLines = brainEntries.map((e) => {
|
|
1993
|
+
const tag = e.outcome === "failed" ? " ⚠️ FAILED ATTEMPT" : "";
|
|
1994
|
+
const snippet = e.response.length > 300 ? `${e.response.substring(0, 300)}...` : e.response;
|
|
1995
|
+
return `- **${e.query}**${tag} _(${e.provider}, ${e.hits} hit(s))_\n ${snippet}`;
|
|
1996
|
+
});
|
|
1997
|
+
parts.push(`## Brain memories (decisions, fixes, prior reviews)\n${brainLines.join("\n")}`);
|
|
1998
|
+
}
|
|
1999
|
+
if (parts.length === 1) {
|
|
2000
|
+
parts.push("Nothing recorded yet — no git history, checkpoints, or Brain memories found for this project.");
|
|
2001
|
+
}
|
|
2002
|
+
return { content: [{ type: "text", text: parts.join("\n\n") }] };
|
|
2003
|
+
}
|
|
2004
|
+
catch (e) {
|
|
2005
|
+
(0, utils_1.logError)("get_project_history", e);
|
|
2006
|
+
return { content: [{ type: "text", text: `get_project_history failed: ${e.message}` }] };
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
function extractSymbolsWithTsCompiler(filePath, relPath, parseErrors) {
|
|
1847
2010
|
try {
|
|
1848
2011
|
const src = fs_1.default.readFileSync(filePath, "utf8");
|
|
1849
2012
|
const sourceFile = ts.createSourceFile(filePath, src, ts.ScriptTarget.Latest, true);
|
|
@@ -1851,7 +2014,10 @@ function extractSymbolsWithTsCompiler(filePath, relPath) {
|
|
|
1851
2014
|
function visit(node) {
|
|
1852
2015
|
const mods = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
|
|
1853
2016
|
const isExport = mods?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
1854
|
-
|
|
2017
|
+
// node.parent is undefined on the SourceFile root itself, and ts.isSourceFile()
|
|
2018
|
+
// dereferences .kind without a guard — calling it unguarded threw on the very
|
|
2019
|
+
// first visit(), so the catch below swallowed it and every file returned [].
|
|
2020
|
+
if (isExport || (node.parent && ts.isSourceFile(node.parent))) {
|
|
1855
2021
|
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
1856
2022
|
symbols.push({
|
|
1857
2023
|
kind: "function",
|
|
@@ -1920,10 +2086,11 @@ function extractSymbolsWithTsCompiler(filePath, relPath) {
|
|
|
1920
2086
|
}
|
|
1921
2087
|
catch (err) {
|
|
1922
2088
|
(0, utils_1.logWarn)("get_ast_hologram", `Could not parse ${relPath}: ${err}`);
|
|
2089
|
+
parseErrors?.push(`${relPath}: ${err}`);
|
|
1923
2090
|
return [];
|
|
1924
2091
|
}
|
|
1925
2092
|
}
|
|
1926
|
-
function walkDirForHologram(dir, rel, extensions) {
|
|
2093
|
+
function walkDirForHologram(dir, rel, extensions, stats) {
|
|
1927
2094
|
let allSymbols = [];
|
|
1928
2095
|
try {
|
|
1929
2096
|
const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
@@ -1933,18 +2100,20 @@ function walkDirForHologram(dir, rel, extensions) {
|
|
|
1933
2100
|
const fullPath = path_1.default.join(dir, entry.name);
|
|
1934
2101
|
const relPath = rel ? path_1.default.join(rel, entry.name) : entry.name;
|
|
1935
2102
|
if (entry.isDirectory()) {
|
|
1936
|
-
allSymbols = allSymbols.concat(walkDirForHologram(fullPath, relPath, extensions));
|
|
2103
|
+
allSymbols = allSymbols.concat(walkDirForHologram(fullPath, relPath, extensions, stats));
|
|
1937
2104
|
}
|
|
1938
2105
|
else {
|
|
1939
2106
|
const ext = entry.name.split(".").pop() || "";
|
|
1940
2107
|
if (extensions.includes(ext)) {
|
|
1941
|
-
|
|
2108
|
+
stats.filesScanned++;
|
|
2109
|
+
allSymbols = allSymbols.concat(extractSymbolsWithTsCompiler(fullPath, relPath, stats.parseErrors));
|
|
1942
2110
|
}
|
|
1943
2111
|
}
|
|
1944
2112
|
}
|
|
1945
2113
|
}
|
|
1946
2114
|
catch (err) {
|
|
1947
2115
|
(0, utils_1.logWarn)("get_ast_hologram", `Error walking dir ${dir}: ${err}`);
|
|
2116
|
+
stats.parseErrors.push(`walk ${rel || "."}: ${err}`);
|
|
1948
2117
|
}
|
|
1949
2118
|
return allSymbols;
|
|
1950
2119
|
}
|
|
@@ -1953,18 +2122,32 @@ async function handleGetAstHologram(args) {
|
|
|
1953
2122
|
const extensions = args?.extensions || ["ts", "tsx", "js", "jsx"];
|
|
1954
2123
|
const workspaceRoot = process.cwd();
|
|
1955
2124
|
const { resolved } = (0, utils_1.safeResolvePath)(workspaceRoot, dirPath);
|
|
1956
|
-
const
|
|
2125
|
+
const stats = { filesScanned: 0, parseErrors: [] };
|
|
2126
|
+
const symbols = walkDirForHologram(resolved, dirPath, extensions, stats);
|
|
1957
2127
|
const byFile = {};
|
|
1958
2128
|
symbols.forEach((s) => {
|
|
1959
2129
|
if (!byFile[s.file])
|
|
1960
2130
|
byFile[s.file] = [];
|
|
1961
2131
|
byFile[s.file].push({ kind: s.kind, name: s.name, line: s.line });
|
|
1962
2132
|
});
|
|
2133
|
+
// An empty map is reported as a failure, not as an answer: a silent "0 symbols" is
|
|
2134
|
+
// indistinguishable from a working scan of an empty dir, which is how a parse bug
|
|
2135
|
+
// stayed invisible here before. Say when nothing could be read.
|
|
2136
|
+
const warning = stats.filesScanned === 0
|
|
2137
|
+
? `No files matching [${extensions.join(", ")}] were found under "${dirPath || "."}" — check the path and extensions.`
|
|
2138
|
+
: symbols.length === 0
|
|
2139
|
+
? `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".`
|
|
2140
|
+
: undefined;
|
|
1963
2141
|
const hologram = {
|
|
1964
2142
|
workspace: path_1.default.basename(workspaceRoot),
|
|
1965
2143
|
scannedDir: dirPath || ".",
|
|
1966
2144
|
totalSymbols: symbols.length,
|
|
2145
|
+
filesScanned: stats.filesScanned,
|
|
1967
2146
|
totalFiles: Object.keys(byFile).length,
|
|
2147
|
+
...(warning ? { warning } : {}),
|
|
2148
|
+
...(stats.parseErrors.length > 0
|
|
2149
|
+
? { parseErrors: stats.parseErrors.slice(0, 10), parseErrorCount: stats.parseErrors.length }
|
|
2150
|
+
: {}),
|
|
1968
2151
|
map: byFile,
|
|
1969
2152
|
};
|
|
1970
2153
|
return { content: [{ type: "text", text: JSON.stringify(hologram, null, 2) }] };
|
|
@@ -2143,7 +2326,7 @@ async function handleGetTelepathicHints(args) {
|
|
|
2143
2326
|
const similarity = (r.similarity * 100).toFixed(1);
|
|
2144
2327
|
const prompt = r.query || "Unknown";
|
|
2145
2328
|
const responseContent = r.response || "";
|
|
2146
|
-
hintsText += `### Hint ${i + 1} (${similarity}% match)\n`;
|
|
2329
|
+
hintsText += `### Hint ${i + 1} (${similarity}% match)${r.outcome === "failed" ? " — ⚠️ FAILED ATTEMPT, do not repeat" : ""}\n`;
|
|
2147
2330
|
hintsText += `**Memory:** ${prompt}\n\n`;
|
|
2148
2331
|
hintsText += `${responseContent.substring(0, 400)}${responseContent.length > 400 ? "..." : ""}\n\n---\n\n`;
|
|
2149
2332
|
});
|
|
@@ -2367,7 +2550,7 @@ async function handleCompressContext(args) {
|
|
|
2367
2550
|
async function handleSmarterCache(args) {
|
|
2368
2551
|
const query = args?.query;
|
|
2369
2552
|
const context = args?.context || "";
|
|
2370
|
-
const threshold = typeof args?.threshold === "number" ? args.threshold :
|
|
2553
|
+
const threshold = typeof args?.threshold === "number" ? args.threshold : DEFAULT_MEMORY_SIMILARITY_FLOOR;
|
|
2371
2554
|
if (!query)
|
|
2372
2555
|
throw new Error("query is required");
|
|
2373
2556
|
const fullQuery = context ? `${query}\n\nContext: ${context}` : query;
|
|
@@ -2492,48 +2675,64 @@ async function handleTokenReceipt(args) {
|
|
|
2492
2675
|
}
|
|
2493
2676
|
// ── Token Budget ─────────────────────────────────────────────────
|
|
2494
2677
|
async function handleTokenBudget(_args) {
|
|
2495
|
-
const statsFile = path_1.default.join(os_1.default.homedir(), '.lemma-cache/stats.json');
|
|
2496
2678
|
const port = (0, utils_1.getProxyPort)();
|
|
2497
|
-
|
|
2679
|
+
const schemaTokens = Math.round(getToolSchemaChars() / 4);
|
|
2680
|
+
// /api/savings-breakdown serves SavingsLedger.getSnapshot() — the same ledger that
|
|
2681
|
+
// already nets tokensSaved against what Lemma itself spent (MCP schema injection,
|
|
2682
|
+
// tool-result bytes, cache misses). There is no separate "membership multiplier" to
|
|
2683
|
+
// compute: reusing that one number keeps this tool from reporting a bigger win than
|
|
2684
|
+
// the ledger the user can audit via `token_receipt` actually shows.
|
|
2685
|
+
let snapshot = null;
|
|
2498
2686
|
try {
|
|
2499
|
-
|
|
2687
|
+
const resp = await axios_1.default.get(`http://localhost:${port}/api/savings-breakdown`);
|
|
2688
|
+
snapshot = resp.data;
|
|
2500
2689
|
}
|
|
2501
2690
|
catch { }
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2691
|
+
if (!snapshot) {
|
|
2692
|
+
return {
|
|
2693
|
+
content: [{
|
|
2694
|
+
type: "text",
|
|
2695
|
+
text: JSON.stringify({
|
|
2696
|
+
note: "El proxy de Lemma no está corriendo, así que no hay bitácora de ahorro/costo que leer — no se inventa un número aquí. Arranca el proxy para que este reporte sea real.",
|
|
2697
|
+
perTurnOverhead: { ...getToolSurfaceSummary(), schemaTokens },
|
|
2698
|
+
}, null, 2),
|
|
2699
|
+
}],
|
|
2700
|
+
};
|
|
2506
2701
|
}
|
|
2507
|
-
|
|
2508
|
-
const
|
|
2509
|
-
const monthlyTokens = stats.monthlyTokens || stats.totalTokens || 100000;
|
|
2510
|
-
const multiplier = monthlyTokens > 0 ? ((monthlyTokens + totalTokensSaved) / monthlyTokens).toFixed(1) : "N/A";
|
|
2511
|
-
const toolCallCount = ledger.length || stats.toolCalls || 0;
|
|
2702
|
+
const { total, totalCost, net } = snapshot;
|
|
2703
|
+
const ratioStr = net.ratio === Infinity ? "n/a (sin costo registrado todavía)" : `${net.ratio.toFixed(2)}x`;
|
|
2512
2704
|
return {
|
|
2513
2705
|
content: [{
|
|
2514
2706
|
type: "text",
|
|
2515
2707
|
text: JSON.stringify({
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2708
|
+
netRatio: ratioStr,
|
|
2709
|
+
note: "netRatio = tokensSaved / tokensSpent, ambos de la misma bitácora auditable que expone token_receipt. Por debajo de 1.0x, Lemma está costando más de lo que ahorra en esta sesión.",
|
|
2710
|
+
tokensSaved: total.tokensSaved.toLocaleString(),
|
|
2711
|
+
tokensSpent: totalCost.tokensSpent.toLocaleString(),
|
|
2712
|
+
netTokens: net.netTokens.toLocaleString(),
|
|
2713
|
+
netCostUSD: net.netCost.toFixed(4),
|
|
2714
|
+
breakdown: {
|
|
2715
|
+
saved: {
|
|
2716
|
+
cache: snapshot.cache?.tokensSaved ?? 0,
|
|
2717
|
+
contextSqueeze: snapshot.contextSqueeze?.tokensSaved ?? 0,
|
|
2718
|
+
historyPrune: snapshot.historyPrune?.tokensSaved ?? 0,
|
|
2719
|
+
complexityRouting: snapshot.complexityRouting?.tokensSaved ?? 0,
|
|
2720
|
+
clipboard: snapshot.clipboard?.tokensSaved ?? 0,
|
|
2721
|
+
},
|
|
2722
|
+
spent: {
|
|
2723
|
+
mcpInstructions: snapshot.costs?.mcpInstructions?.tokensSpent ?? 0,
|
|
2724
|
+
toolResult: snapshot.costs?.toolResult?.tokensSpent ?? 0,
|
|
2725
|
+
cacheMiss: snapshot.costs?.cacheMiss?.tokensSpent ?? 0,
|
|
2726
|
+
},
|
|
2528
2727
|
},
|
|
2529
2728
|
// The fixed cost of being connected, re-sent on every request of the session.
|
|
2530
2729
|
perTurnOverhead: {
|
|
2531
2730
|
...getToolSurfaceSummary(),
|
|
2532
|
-
schemaTokens
|
|
2731
|
+
schemaTokens,
|
|
2533
2732
|
note: "Hidden tools stay callable via lemma_toolbox. Set mcp.toolProfile='full' in lemma.config.json to advertise all of them.",
|
|
2534
2733
|
},
|
|
2535
|
-
recommendations:
|
|
2536
|
-
? ["
|
|
2734
|
+
recommendations: net.ratio !== Infinity && net.ratio < 1
|
|
2735
|
+
? ["netRatio < 1.0x: revisa si search_memory/smarter_cache se están llamando antes de razonar, o si el toolProfile anuncia más tools de las que este proyecto necesita — cada schema extra es costo fijo por turno."]
|
|
2537
2736
|
: ["Sigue usando turbosqueeze antes de cada code block grande.", "Usa smarter_cache antes de razonar."],
|
|
2538
2737
|
}, null, 2),
|
|
2539
2738
|
}],
|