@nxuss/lemma 1.5.0 → 1.7.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.
Files changed (94) hide show
  1. package/README.md +182 -6
  2. package/bin/checkpoint.js +49 -0
  3. package/bin/init.js +21 -2
  4. package/dist/cjs/cli/lemma-proxy.d.ts.map +1 -1
  5. package/dist/cjs/cli/lemma-proxy.js +50 -6
  6. package/dist/cjs/cli/lemma-proxy.js.map +1 -1
  7. package/dist/cjs/mcp/index.js +23 -0
  8. package/dist/cjs/mcp/index.js.map +1 -1
  9. package/dist/cjs/mcp/prompts.d.ts +1 -2
  10. package/dist/cjs/mcp/prompts.d.ts.map +1 -1
  11. package/dist/cjs/mcp/prompts.js +34 -129
  12. package/dist/cjs/mcp/prompts.js.map +1 -1
  13. package/dist/cjs/mcp/resources.d.ts.map +1 -1
  14. package/dist/cjs/mcp/resources.js +4 -4
  15. package/dist/cjs/mcp/resources.js.map +1 -1
  16. package/dist/cjs/mcp/tool-profiles.d.ts +55 -0
  17. package/dist/cjs/mcp/tool-profiles.d.ts.map +1 -0
  18. package/dist/cjs/mcp/tool-profiles.js +204 -0
  19. package/dist/cjs/mcp/tool-profiles.js.map +1 -0
  20. package/dist/cjs/mcp/tools.d.ts +14 -0
  21. package/dist/cjs/mcp/tools.d.ts.map +1 -1
  22. package/dist/cjs/mcp/tools.js +190 -27
  23. package/dist/cjs/mcp/tools.js.map +1 -1
  24. package/dist/cjs/subconscious/TheBrainV2.d.ts +36 -2
  25. package/dist/cjs/subconscious/TheBrainV2.d.ts.map +1 -1
  26. package/dist/cjs/subconscious/TheBrainV2.js +104 -23
  27. package/dist/cjs/subconscious/TheBrainV2.js.map +1 -1
  28. package/dist/cjs/utils/AutoContextBundle.d.ts.map +1 -1
  29. package/dist/cjs/utils/AutoContextBundle.js +17 -20
  30. package/dist/cjs/utils/AutoContextBundle.js.map +1 -1
  31. package/dist/cjs/utils/ContextSqueezer.d.ts.map +1 -1
  32. package/dist/cjs/utils/ContextSqueezer.js +5 -3
  33. package/dist/cjs/utils/ContextSqueezer.js.map +1 -1
  34. package/dist/cjs/utils/ConversationCheckpoint.d.ts +13 -0
  35. package/dist/cjs/utils/ConversationCheckpoint.d.ts.map +1 -1
  36. package/dist/cjs/utils/ConversationCheckpoint.js +58 -0
  37. package/dist/cjs/utils/ConversationCheckpoint.js.map +1 -1
  38. package/dist/cjs/utils/SavingsLedger.d.ts +38 -1
  39. package/dist/cjs/utils/SavingsLedger.d.ts.map +1 -1
  40. package/dist/cjs/utils/SavingsLedger.js +89 -13
  41. package/dist/cjs/utils/SavingsLedger.js.map +1 -1
  42. package/dist/cjs/utils/TestAutofixInterceptor.d.ts.map +1 -1
  43. package/dist/cjs/utils/TestAutofixInterceptor.js +15 -26
  44. package/dist/cjs/utils/TestAutofixInterceptor.js.map +1 -1
  45. package/dist/cjs/utils/reportSavings.d.ts +15 -0
  46. package/dist/cjs/utils/reportSavings.d.ts.map +1 -1
  47. package/dist/cjs/utils/reportSavings.js +13 -1
  48. package/dist/cjs/utils/reportSavings.js.map +1 -1
  49. package/dist/esm/cli/lemma-proxy.d.ts.map +1 -1
  50. package/dist/esm/cli/lemma-proxy.js +50 -6
  51. package/dist/esm/cli/lemma-proxy.js.map +1 -1
  52. package/dist/esm/mcp/index.js +24 -1
  53. package/dist/esm/mcp/index.js.map +1 -1
  54. package/dist/esm/mcp/prompts.d.ts +1 -2
  55. package/dist/esm/mcp/prompts.d.ts.map +1 -1
  56. package/dist/esm/mcp/prompts.js +33 -128
  57. package/dist/esm/mcp/prompts.js.map +1 -1
  58. package/dist/esm/mcp/resources.d.ts.map +1 -1
  59. package/dist/esm/mcp/resources.js +4 -4
  60. package/dist/esm/mcp/resources.js.map +1 -1
  61. package/dist/esm/mcp/tool-profiles.d.ts +55 -0
  62. package/dist/esm/mcp/tool-profiles.d.ts.map +1 -0
  63. package/dist/esm/mcp/tool-profiles.js +196 -0
  64. package/dist/esm/mcp/tool-profiles.js.map +1 -0
  65. package/dist/esm/mcp/tools.d.ts +14 -0
  66. package/dist/esm/mcp/tools.d.ts.map +1 -1
  67. package/dist/esm/mcp/tools.js +188 -29
  68. package/dist/esm/mcp/tools.js.map +1 -1
  69. package/dist/esm/subconscious/TheBrainV2.d.ts +36 -2
  70. package/dist/esm/subconscious/TheBrainV2.d.ts.map +1 -1
  71. package/dist/esm/subconscious/TheBrainV2.js +103 -23
  72. package/dist/esm/subconscious/TheBrainV2.js.map +1 -1
  73. package/dist/esm/utils/AutoContextBundle.d.ts.map +1 -1
  74. package/dist/esm/utils/AutoContextBundle.js +17 -20
  75. package/dist/esm/utils/AutoContextBundle.js.map +1 -1
  76. package/dist/esm/utils/ContextSqueezer.d.ts.map +1 -1
  77. package/dist/esm/utils/ContextSqueezer.js +5 -3
  78. package/dist/esm/utils/ContextSqueezer.js.map +1 -1
  79. package/dist/esm/utils/ConversationCheckpoint.d.ts +13 -0
  80. package/dist/esm/utils/ConversationCheckpoint.d.ts.map +1 -1
  81. package/dist/esm/utils/ConversationCheckpoint.js +57 -0
  82. package/dist/esm/utils/ConversationCheckpoint.js.map +1 -1
  83. package/dist/esm/utils/SavingsLedger.d.ts +38 -1
  84. package/dist/esm/utils/SavingsLedger.d.ts.map +1 -1
  85. package/dist/esm/utils/SavingsLedger.js +89 -13
  86. package/dist/esm/utils/SavingsLedger.js.map +1 -1
  87. package/dist/esm/utils/TestAutofixInterceptor.d.ts.map +1 -1
  88. package/dist/esm/utils/TestAutofixInterceptor.js +15 -26
  89. package/dist/esm/utils/TestAutofixInterceptor.js.map +1 -1
  90. package/dist/esm/utils/reportSavings.d.ts +15 -0
  91. package/dist/esm/utils/reportSavings.d.ts.map +1 -1
  92. package/dist/esm/utils/reportSavings.js +12 -1
  93. package/dist/esm/utils/reportSavings.js.map +1 -1
  94. package/package.json +4 -2
@@ -0,0 +1,196 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ /**
4
+ * Tools that pay for their schema in an ordinary coding session: reading, searching,
5
+ * patching, AST context, exact-hash caching, and the ledger. Everything else is
6
+ * situational and reachable through `lemma_toolbox`.
7
+ */
8
+ export const CORE_TOOL_NAMES = [
9
+ // Workspace read & navigation
10
+ "read_workspace_file",
11
+ "list_workspace_dir",
12
+ "search_workspace",
13
+ "smart_file_slice",
14
+ "run_workspace_command",
15
+ "output_region",
16
+ // Write path — patch, then validate before it lands
17
+ "apply_workspace_patch",
18
+ "write_workspace_file",
19
+ "create_workspace_file",
20
+ "surgical_ast_insert",
21
+ "validate_patch_sandbox",
22
+ // Structure without reading whole files
23
+ "get_ast_hologram",
24
+ "get_symbol_surgical_context",
25
+ "get_project_onboarding",
26
+ // Avoid re-deriving what was already derived
27
+ "state_hash_cache",
28
+ "smarter_cache",
29
+ "search_memory",
30
+ "store_memory",
31
+ // Accounting & routing
32
+ "token_receipt",
33
+ "get_routing_advice",
34
+ ];
35
+ const exists = (cwd, ...rel) => rel.some((r) => {
36
+ try {
37
+ return fs.existsSync(path.join(cwd, r));
38
+ }
39
+ catch {
40
+ return false;
41
+ }
42
+ });
43
+ /** Shallow scan for an extension — deep walks at startup would cost more than they save. */
44
+ function hasExtension(cwd, ext, depth = 2) {
45
+ const walk = (dir, left) => {
46
+ let entries;
47
+ try {
48
+ entries = fs.readdirSync(dir, { withFileTypes: true });
49
+ }
50
+ catch {
51
+ return false;
52
+ }
53
+ for (const entry of entries) {
54
+ if (entry.name.startsWith(".") || entry.name === "node_modules")
55
+ continue;
56
+ if (entry.isFile() && entry.name.endsWith(ext))
57
+ return true;
58
+ if (entry.isDirectory() && left > 0 && walk(path.join(dir, entry.name), left - 1))
59
+ return true;
60
+ }
61
+ return false;
62
+ };
63
+ return walk(cwd, depth);
64
+ }
65
+ const TOOL_GROUPS = [
66
+ {
67
+ name: "infra",
68
+ tools: [
69
+ "infra_discover", "infra_connect", "infra_status", "infra_health_check",
70
+ "infra_diagnose", "infra_analyze_monolith", "infra_suggest_split",
71
+ "jenkins_generate_pipeline", "jenkins_trigger_build", "jenkins_build_status", "jenkins_logs",
72
+ "okd_generate_manifests", "okd_apply", "okd_status", "okd_logs", "okd_events",
73
+ "okd_describe", "okd_route_url",
74
+ "terraform_plan", "terraform_apply", "terraform_state_list", "terraform_drift",
75
+ ],
76
+ applies: (cwd) =>
77
+ // A provider already connected, or the repo actually contains infra to act on.
78
+ exists(process.env.HOME || "", ".lemma-cache/infra/config.json") ||
79
+ exists(cwd, "Jenkinsfile", "kustomization.yaml", ".openshift", "openshift") ||
80
+ hasExtension(cwd, ".tf", 2),
81
+ },
82
+ ];
83
+ /**
84
+ * Code review is a mode the user enters, not a property of the repo — and it cannot be
85
+ * detected at startup. Gating these on `.git` advertised them in every git repo, which is
86
+ * every repo: ~670 tokens per turn for tools most sessions never touch. They stay
87
+ * registered and `lemma_toolbox` finds them by query the moment a review actually starts.
88
+ */
89
+ export const REVIEW_TOOL_NAMES = ["review_diff", "review_pr", "pr_status", "generate_pr_workflow"];
90
+ function readConfiguredProfile(cwd) {
91
+ try {
92
+ const configPath = path.join(cwd, "lemma.config.json");
93
+ if (fs.existsSync(configPath)) {
94
+ const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
95
+ const profile = config?.mcp?.toolProfile;
96
+ if (profile === "core" || profile === "full" || profile === "auto")
97
+ return profile;
98
+ }
99
+ }
100
+ catch {
101
+ // Unreadable config must never change which tools work — fall through to the default.
102
+ }
103
+ const env = process.env.LEMMA_TOOL_PROFILE;
104
+ if (env === "core" || env === "full" || env === "auto")
105
+ return env;
106
+ return "auto";
107
+ }
108
+ export function resolveToolSurface(allDefinitions, cwd = process.cwd()) {
109
+ const profile = readConfiguredProfile(cwd);
110
+ if (profile === "full") {
111
+ return { profile, advertised: allDefinitions, hidden: [], activeGroups: ["*"] };
112
+ }
113
+ // Always advertised: it is the only route back to a withheld schema, so hiding it
114
+ // would turn a deferred tool into a lost one.
115
+ const allowed = new Set([...CORE_TOOL_NAMES, "lemma_toolbox"]);
116
+ const activeGroups = [];
117
+ if (profile === "auto") {
118
+ for (const group of TOOL_GROUPS) {
119
+ let matched = false;
120
+ try {
121
+ matched = group.applies(cwd);
122
+ }
123
+ catch {
124
+ // Detection must never be the reason a tool disappears; on error, include it.
125
+ matched = true;
126
+ }
127
+ if (matched) {
128
+ activeGroups.push(group.name);
129
+ group.tools.forEach((t) => allowed.add(t));
130
+ }
131
+ }
132
+ }
133
+ const advertised = [];
134
+ const hidden = [];
135
+ for (const def of allDefinitions) {
136
+ (allowed.has(def.name) ? advertised : hidden).push(def);
137
+ }
138
+ return { profile, advertised, hidden, activeGroups };
139
+ }
140
+ /**
141
+ * One-line-per-tool catalog of everything not advertised, so the model can find and
142
+ * invoke a hidden tool without its schema having been resident all session.
143
+ */
144
+ export function buildToolboxCatalog(hidden, query) {
145
+ const q = (query || "").trim().toLowerCase();
146
+ const terms = q ? q.split(/\s+/).filter(Boolean) : [];
147
+ let matches = hidden;
148
+ if (terms.length) {
149
+ // Plain substring matching floods the result: "pr" hits scrub_privacy, precrime_static
150
+ // and compress_context, so a 3-word query returns the whole catalog and costs more
151
+ // than the schemas it was meant to replace. Match on word boundaries and rank by how
152
+ // many distinct terms a tool hits, so the tools the user meant come first.
153
+ const scored = hidden
154
+ .map((t) => {
155
+ const haystack = `${t.name} ${t.description || ""}`.toLowerCase();
156
+ const words = new Set(haystack.split(/[^a-z0-9]+/).filter(Boolean));
157
+ let score = 0;
158
+ for (const term of terms) {
159
+ if (words.has(term))
160
+ score += 2; // whole word — the strong signal
161
+ else if (term.length >= 4 && haystack.includes(term))
162
+ score += 1; // prefix/partial
163
+ }
164
+ return { tool: t, score };
165
+ })
166
+ .filter((s) => s.score > 0)
167
+ .sort((a, b) => b.score - a.score);
168
+ // A whole-word hit is a real match; partials only get shown when nothing matched a
169
+ // whole word, so a weak substring never crowds out the tool the user asked for.
170
+ const hasWordHit = scored.some((s) => s.score >= 2);
171
+ matches = scored.filter((s) => (hasWordHit ? s.score >= 2 : true)).map((s) => s.tool);
172
+ }
173
+ const CATALOG_LIMIT = 15;
174
+ const truncated = Math.max(0, matches.length - CATALOG_LIMIT);
175
+ const shown = matches.slice(0, CATALOG_LIMIT);
176
+ if (matches.length === 0) {
177
+ return terms.length
178
+ ? `No hidden Lemma tool matches "${query}". ${hidden.length} tools available; call lemma_toolbox with no query to list all.`
179
+ : "No hidden tools — every Lemma tool is already advertised in this session.";
180
+ }
181
+ const lines = shown.map((t) => {
182
+ // One line per tool: enough to choose, not enough to be a second schema listing.
183
+ const desc = (t.description || "").split(/\.\s|\n/)[0].slice(0, 100);
184
+ return `- ${t.name}: ${desc}`;
185
+ });
186
+ return [
187
+ `${matches.length} Lemma tool(s) available but not advertised (schemas withheld to keep per-turn context small).`,
188
+ `Invoke one with: lemma_toolbox { action: "call", tool: "<name>", args: { ... } }`,
189
+ "",
190
+ ...lines,
191
+ ...(truncated > 0 ? [`… and ${truncated} more — narrow the query to see them.`] : []),
192
+ "",
193
+ `Call lemma_toolbox { action: "schema", tool: "<name>" } for a tool's full input schema before calling it.`,
194
+ ].join("\n");
195
+ }
196
+ //# sourceMappingURL=tool-profiles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-profiles.js","sourceRoot":"","sources":["../../../src/mcp/tool-profiles.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,18 @@
1
1
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ /** Serialized size of every tool schema sent to the client, for cost accounting. */
3
+ export declare function getToolSchemaChars(): number;
4
+ /** Every tool name with a registered handler, advertised or not. */
5
+ export declare function getRegisteredToolNames(): string[];
6
+ /** Tool names whose schemas this session sends to the client. */
7
+ export declare function getAdvertisedToolNames(): string[];
8
+ /** Which tools this session advertises, and why — surfaced by `token_budget`. */
9
+ export declare function getToolSurfaceSummary(): {
10
+ profile: string;
11
+ advertised: number;
12
+ hidden: number;
13
+ total: number;
14
+ activeGroups: string[];
15
+ };
2
16
  export interface ToolCallEvent {
3
17
  tool: string;
4
18
  args: Record<string, any>;
@@ -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;AAukCnE,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,CA2DN"}
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"}
@@ -8,7 +8,7 @@ import * as ts from "typescript";
8
8
  import { SemanticScrubber } from "../security/SemanticScrubber";
9
9
  import { ComplexityRouter } from "../proxy/ComplexityRouter";
10
10
  import { squeezeCode, squeezePrompt, pruneHistoryMessages, generateAstDiff } from "../utils/ContextSqueezer";
11
- import { reportSavings } from "../utils/reportSavings";
11
+ import { reportSavings, reportCost } from "../utils/reportSavings";
12
12
  import { getProxyPort, safeResolvePath, logError, logWarn, estimateTokensFromResult, isCommandAllowed, loadAllowedCommands } from "./utils";
13
13
  import { computeFingerprint } from "../utils/FileFingerprint";
14
14
  import { analyzeBlameHeat } from "../utils/GitBlameHeat";
@@ -24,13 +24,14 @@ import { findPattern, storePattern, getPatternStats } from "../utils/PromptPatte
24
24
  import { runAnalysis, parseDiff } from "../pr-review/PRReviewEngine";
25
25
  import { ReviewStore } from "../pr-review/ReviewStore";
26
26
  import { runPRReview } from "../pr-review/reviewRunner";
27
- import { getBrain } from "../subconscious/TheBrainV2";
27
+ import { getBrain, deriveProjectId } from "../subconscious/TheBrainV2";
28
28
  import { collapseContext } from "../utils/ContextCollapser";
29
29
  import { getSymbolSurgicalContext } from "../utils/SymbolSurgicalContext";
30
30
  import { compressWormhole } from "../utils/WormholeCompressor";
31
31
  import { generateExecutiveReport } from "../utils/ExecutiveReport";
32
32
  import { surgicalASTInsert } from "../utils/SurgicalASTInsert";
33
33
  import { getInfraToolDefinitions, getInfraToolHandlers, INFRA_TOOL_NAMES } from "../infra/mcp-tools";
34
+ import { resolveToolSurface, buildToolboxCatalog } from "./tool-profiles";
34
35
  import { lookupStateHash, storeStateHash } from "../utils/StateHashCache";
35
36
  import { recordReceiptEvent, getReceiptSummary } from "../utils/TokenReceipt";
36
37
  import { distillCommandOutput, buildDistillFooter, readRegion } from "../utils/CommandOutputDistiller";
@@ -62,6 +63,8 @@ const FREE_TOOLS = new Set([
62
63
  "token_budget", "squeeze_prompt", "turbosqueeze",
63
64
  // Utility — just enough to function
64
65
  "read_workspace_file", "scrub_privacy", "summarize_long_text",
66
+ // Discovery of withheld schemas — gating this would hide tools with no way back
67
+ "lemma_toolbox",
65
68
  // Phase 1 zero-cost analysis tools
66
69
  "file_fingerprint", "git_blame_heat", "type_coverage_report", "import_tree_context",
67
70
  // Phase 2 smart reading & dedup tools
@@ -990,8 +993,59 @@ const toolDefinitions = [
990
993
  ];
991
994
  // ── Infrastructure Tool Definitions ──────────────────────────────────────────
992
995
  const _infraToolDefs = getInfraToolDefinitions();
993
- const allToolDefinitions = [...toolDefinitions, ..._infraToolDefs];
994
- const toolDefinitionsArray = _infraToolDefs.length > 0 ? allToolDefinitions : toolDefinitions;
996
+ // `lemma_toolbox` keeps every non-advertised tool reachable. It is defined here rather
997
+ // than in the main array because it must never itself be hidden.
998
+ const TOOLBOX_TOOL_DEFINITION = {
999
+ name: "lemma_toolbox",
1000
+ description: "Discover and invoke Lemma tools whose schemas are not loaded this session. Use action='list' (optionally with a query) to see what exists, 'schema' to read one tool's inputs, and 'call' to run it. Every Lemma capability is reachable here.",
1001
+ inputSchema: {
1002
+ type: "object",
1003
+ properties: {
1004
+ action: { type: "string", enum: ["list", "schema", "call"], description: "list | schema | call", default: "list" },
1005
+ query: { type: "string", description: "Filter terms for action='list' (e.g. 'terraform drift')" },
1006
+ tool: { type: "string", description: "Tool name, for action='schema' or 'call'" },
1007
+ args: { type: "object", description: "Arguments passed through to the tool, for action='call'" },
1008
+ },
1009
+ required: ["action"],
1010
+ },
1011
+ };
1012
+ const allToolDefinitions = [
1013
+ ...toolDefinitions,
1014
+ ..._infraToolDefs,
1015
+ TOOLBOX_TOOL_DEFINITION,
1016
+ ];
1017
+ // Narrow what gets advertised to what this project can actually use. Hidden tools stay
1018
+ // registered in `toolHandlers` and callable — both directly and through `lemma_toolbox`.
1019
+ const _toolSurface = resolveToolSurface(allToolDefinitions, process.cwd());
1020
+ const toolDefinitionsArray = _toolSurface.advertised;
1021
+ const hiddenToolDefinitions = _toolSurface.hidden;
1022
+ /** Serialized size of every tool schema sent to the client, for cost accounting. */
1023
+ export function getToolSchemaChars() {
1024
+ try {
1025
+ return JSON.stringify(toolDefinitionsArray).length;
1026
+ }
1027
+ catch {
1028
+ return 0;
1029
+ }
1030
+ }
1031
+ /** Every tool name with a registered handler, advertised or not. */
1032
+ export function getRegisteredToolNames() {
1033
+ return Object.keys(toolHandlers);
1034
+ }
1035
+ /** Tool names whose schemas this session sends to the client. */
1036
+ export function getAdvertisedToolNames() {
1037
+ return toolDefinitionsArray.map((d) => d.name);
1038
+ }
1039
+ /** Which tools this session advertises, and why — surfaced by `token_budget`. */
1040
+ export function getToolSurfaceSummary() {
1041
+ return {
1042
+ profile: _toolSurface.profile,
1043
+ advertised: toolDefinitionsArray.length,
1044
+ hidden: hiddenToolDefinitions.length,
1045
+ total: allToolDefinitions.length,
1046
+ activeGroups: _toolSurface.activeGroups,
1047
+ };
1048
+ }
995
1049
  const toolHandlers = {
996
1050
  scrub_privacy: handleScrubPrivacy,
997
1051
  search_memory: handleSearchMemory,
@@ -1064,8 +1118,44 @@ const toolHandlers = {
1064
1118
  conversation_checkpoint: handleConversationCheckpoint,
1065
1119
  test_autofix_interceptor: handleTestAutofixInterceptor,
1066
1120
  prompt_pattern_cache: handlePromptPatternCache,
1121
+ lemma_toolbox: handleToolbox,
1067
1122
  ...getInfraToolHandlers(),
1068
1123
  };
1124
+ /**
1125
+ * Reaches the tools whose schemas were withheld from the session listing.
1126
+ *
1127
+ * `call` proxies through this server rather than relying on the client to emit a name it
1128
+ * never saw advertised — some clients validate outgoing tool names against tools/list, so
1129
+ * the proxy is what makes hiding a schema safe rather than a silent capability loss.
1130
+ */
1131
+ async function handleToolbox(args) {
1132
+ const action = args?.action || "list";
1133
+ if (action === "list") {
1134
+ return {
1135
+ content: [{ type: "text", text: buildToolboxCatalog(hiddenToolDefinitions, args?.query) }],
1136
+ };
1137
+ }
1138
+ const toolName = args?.tool;
1139
+ if (!toolName)
1140
+ throw new Error("`tool` is required for action '" + action + "'");
1141
+ if (action === "schema") {
1142
+ const def = allToolDefinitions.find((d) => d.name === toolName);
1143
+ if (!def)
1144
+ throw new Error(`Unknown tool: ${toolName}`);
1145
+ return { content: [{ type: "text", text: JSON.stringify(def, null, 2) }] };
1146
+ }
1147
+ if (action === "call") {
1148
+ const handler = toolHandlers[toolName];
1149
+ if (!handler)
1150
+ throw new Error(`Unknown tool: ${toolName}`);
1151
+ // The proxy must not become a way around the license gate the direct path enforces.
1152
+ if (!isProUser() && !FREE_TOOLS.has(toolName)) {
1153
+ return { content: [{ type: "text", text: PRO_GATE_MESSAGE }] };
1154
+ }
1155
+ return handler((args?.args || {}));
1156
+ }
1157
+ throw new Error(`Unknown action: ${action}. Use 'list', 'schema', or 'call'.`);
1158
+ }
1069
1159
  export function setupToolsHandlers(server, onToolCall) {
1070
1160
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
1071
1161
  tools: toolDefinitionsArray,
@@ -1101,13 +1191,17 @@ export function setupToolsHandlers(server, onToolCall) {
1101
1191
  }
1102
1192
  try {
1103
1193
  const result = await handler((args || {}));
1194
+ const tokensImpact = estimateTokensFromResult(result);
1104
1195
  onToolCall?.({
1105
1196
  tool: name,
1106
1197
  args: args || {},
1107
1198
  result: "OK",
1108
1199
  latency: Date.now() - startTime,
1109
- tokensImpact: estimateTokensFromResult(result),
1200
+ tokensImpact,
1110
1201
  });
1202
+ // Every tool result lands in the model's context — that is a real cost, and it
1203
+ // is charged even when the same call also reports a saving.
1204
+ reportCost({ source: "toolResult", tokens: tokensImpact, toolName: name });
1111
1205
  return result;
1112
1206
  }
1113
1207
  catch (error) {
@@ -1138,7 +1232,7 @@ async function handleSearchMemory(args) {
1138
1232
  throw new Error("Query is required");
1139
1233
  try {
1140
1234
  const brain = getBrain();
1141
- const results = brain.search(query, limit);
1235
+ const results = brain.search(query, limit, 0, { projectId: deriveProjectId() });
1142
1236
  if (results.length === 0) {
1143
1237
  return { content: [{ type: "text", text: "No relevant memories found in Lemma's Brain." }] };
1144
1238
  }
@@ -1623,30 +1717,88 @@ async function handleSqueezePrompt(args) {
1623
1717
  return { content: [{ type: "text", text: `Error squeezing prompt: ${err.message}` }] };
1624
1718
  }
1625
1719
  }
1720
+ /**
1721
+ * Onboarding built from the filesystem alone.
1722
+ *
1723
+ * The proxy produces a richer map, but this tool is in the core surface and CLAUDE.md tells
1724
+ * the model to call it first — so when the proxy is down, the first call of every session
1725
+ * used to return an error and the session started with no context at all. A local answer is
1726
+ * strictly better than that, and it never fails.
1727
+ */
1728
+ function buildLocalOnboarding(cwd) {
1729
+ const lines = [];
1730
+ const projectName = path.basename(cwd);
1731
+ lines.push(`# ${projectName}`, "");
1732
+ let pkg = null;
1733
+ try {
1734
+ pkg = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8"));
1735
+ }
1736
+ catch { /* not a node project */ }
1737
+ if (pkg) {
1738
+ if (pkg.description)
1739
+ lines.push(pkg.description, "");
1740
+ lines.push(`**Version:** ${pkg.version || "unknown"}`);
1741
+ const deps = Object.keys(pkg.dependencies || {});
1742
+ if (deps.length)
1743
+ lines.push(`**Dependencies:** ${deps.slice(0, 12).join(", ")}${deps.length > 12 ? ` (+${deps.length - 12})` : ""}`);
1744
+ const scripts = Object.keys(pkg.scripts || {});
1745
+ if (scripts.length)
1746
+ lines.push(`**Scripts:** ${scripts.slice(0, 12).join(", ")}`);
1747
+ }
1748
+ // Current branch straight from .git/HEAD — no subprocess, works on a detached HEAD too.
1749
+ try {
1750
+ const head = fs.readFileSync(path.join(cwd, ".git", "HEAD"), "utf8").trim();
1751
+ const branch = head.startsWith("ref: ") ? head.replace("ref: refs/heads/", "") : `detached @ ${head.slice(0, 8)}`;
1752
+ lines.push(`**Branch:** ${branch}`);
1753
+ }
1754
+ catch { /* not a git repo */ }
1755
+ try {
1756
+ const entries = fs
1757
+ .readdirSync(cwd, { withFileTypes: true })
1758
+ .filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules")
1759
+ .map((e) => e.name);
1760
+ if (entries.length)
1761
+ lines.push(`**Top-level directories:** ${entries.slice(0, 15).join(", ")}`);
1762
+ }
1763
+ catch { /* unreadable cwd */ }
1764
+ try {
1765
+ const memories = getBrain().getEntriesForProject(deriveProjectId(cwd));
1766
+ lines.push("", `**Brain memories for this project:** ${memories.length}`);
1767
+ if (memories.length) {
1768
+ const recent = memories
1769
+ .slice()
1770
+ .sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1))
1771
+ .slice(0, 5);
1772
+ lines.push("", "Most recent:");
1773
+ for (const m of recent)
1774
+ lines.push(`- ${m.query.slice(0, 100)}`);
1775
+ lines.push("", "Use `search_memory` to retrieve any of these in full.");
1776
+ }
1777
+ }
1778
+ catch { /* brain unavailable */ }
1779
+ return lines.join("\n");
1780
+ }
1626
1781
  async function handleGetProjectOnboarding(_args) {
1782
+ const cwd = process.cwd();
1627
1783
  try {
1628
1784
  const port = getProxyPort();
1629
- const response = await axios.get(`http://localhost:${port}/api/project/onboarding`);
1630
- return {
1631
- content: [
1632
- {
1633
- type: "text",
1634
- text: response.data.markdown || "# Onboarding mental model could not be generated.",
1635
- },
1636
- ],
1637
- };
1785
+ const response = await axios.get(`http://localhost:${port}/api/project/onboarding`, { timeout: 2000 });
1786
+ if (response.data?.markdown) {
1787
+ return { content: [{ type: "text", text: response.data.markdown }] };
1788
+ }
1638
1789
  }
1639
1790
  catch (error) {
1640
- logError("get_project_onboarding", error);
1641
- return {
1642
- content: [
1643
- {
1644
- type: "text",
1645
- text: `Failed to fetch codebase onboarding mental model: ${error.message}. Is Lemma proxy running?`,
1646
- },
1647
- ],
1648
- };
1791
+ // Proxy down or slow — fall through. Not worth surfacing as an error to the model.
1792
+ logError("get_project_onboarding (falling back to local)", error);
1649
1793
  }
1794
+ return {
1795
+ content: [
1796
+ {
1797
+ type: "text",
1798
+ text: `${buildLocalOnboarding(cwd)}\n\n---\n*Generated locally. Start the Lemma proxy (\`lemma start\`) for the full indexed architecture map.*`,
1799
+ },
1800
+ ],
1801
+ };
1650
1802
  }
1651
1803
  function extractSymbolsWithTsCompiler(filePath, relPath) {
1652
1804
  try {
@@ -1874,7 +2026,7 @@ async function handleQueryHybridConsensus(args) {
1874
2026
  const fullQuery = context ? `${query}\n\nContext: ${context}` : query;
1875
2027
  try {
1876
2028
  const brain = getBrain();
1877
- const results = brain.search(fullQuery, 3);
2029
+ const results = brain.search(fullQuery, 3, 0, { projectId: deriveProjectId() });
1878
2030
  const topHit = results[0];
1879
2031
  if (topHit && topHit.similarity >= threshold) {
1880
2032
  const savedTokens = Math.floor((topHit.response?.length || 500) / 4);
@@ -1932,7 +2084,7 @@ async function handleGetTelepathicHints(args) {
1932
2084
  const telepathicQuery = `${basename} ${dirContext} ${ext} patterns solutions architecture`;
1933
2085
  try {
1934
2086
  const brain = getBrain();
1935
- const results = brain.search(telepathicQuery, limit);
2087
+ const results = brain.search(telepathicQuery, limit, 0, { projectId: deriveProjectId() });
1936
2088
  if (results.length === 0) {
1937
2089
  return {
1938
2090
  content: [
@@ -2178,7 +2330,7 @@ async function handleSmarterCache(args) {
2178
2330
  const fullQuery = context ? `${query}\n\nContext: ${context}` : query;
2179
2331
  try {
2180
2332
  const brain = getBrain();
2181
- const results = brain.search(fullQuery, 5);
2333
+ const results = brain.search(fullQuery, 5, 0, { projectId: deriveProjectId() });
2182
2334
  const topHit = results[0];
2183
2335
  if (topHit && topHit.similarity >= threshold) {
2184
2336
  const responseText = topHit.response;
@@ -2331,6 +2483,12 @@ async function handleTokenBudget(_args) {
2331
2483
  astNavigation: true,
2332
2484
  privacyScrubbing: true,
2333
2485
  },
2486
+ // The fixed cost of being connected, re-sent on every request of the session.
2487
+ perTurnOverhead: {
2488
+ ...getToolSurfaceSummary(),
2489
+ schemaTokens: Math.round(getToolSchemaChars() / 4),
2490
+ note: "Hidden tools stay callable via lemma_toolbox. Set mcp.toolProfile='full' in lemma.config.json to advertise all of them.",
2491
+ },
2334
2492
  recommendations: toolCallCount === 0
2335
2493
  ? ["No se han usado tools de Lemma todavía. Asegúrate de que el agente esté usando el prompt lemma-turbomode."]
2336
2494
  : ["Sigue usando turbosqueeze antes de cada code block grande.", "Usa smarter_cache antes de razonar."],
@@ -2944,7 +3102,8 @@ async function handleSemanticDedupGuard(args) {
2944
3102
  throw new Error("query and response are required");
2945
3103
  try {
2946
3104
  const brain = getBrain();
2947
- const results = brain.search(query, 3);
3105
+ // A dedup check is not a question the user asked — it must not register as a cache hit.
3106
+ const results = brain.search(query, 3, 0, { countStats: false, projectId: deriveProjectId() });
2948
3107
  const topHit = results[0];
2949
3108
  if (!topHit) {
2950
3109
  return {
@@ -4050,7 +4209,7 @@ async function handleLocalSemanticAutofix(args) {
4050
4209
  throw new Error("errorMessage is required");
4051
4210
  try {
4052
4211
  const brain = getBrain();
4053
- const results = brain.search(errorMessage, 3, 0.70);
4212
+ const results = brain.search(errorMessage, 3, 0.70, { projectId: deriveProjectId() });
4054
4213
  const topHit = results[0];
4055
4214
  if (topHit) {
4056
4215
  const responseText = topHit.response;