@nxuss/lemma 1.3.3 → 1.5.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 +33 -3
- package/bin/init.js +1 -1
- package/dist/cjs/mcp/prompts.d.ts +1 -1
- package/dist/cjs/mcp/prompts.d.ts.map +1 -1
- package/dist/cjs/mcp/prompts.js +4 -1
- package/dist/cjs/mcp/prompts.js.map +1 -1
- package/dist/cjs/mcp/tools.d.ts.map +1 -1
- package/dist/cjs/mcp/tools.js +208 -75
- package/dist/cjs/mcp/tools.js.map +1 -1
- package/dist/cjs/subconscious/TheBrainV2.d.ts +4 -1
- package/dist/cjs/subconscious/TheBrainV2.d.ts.map +1 -1
- package/dist/cjs/subconscious/TheBrainV2.js +49 -9
- package/dist/cjs/subconscious/TheBrainV2.js.map +1 -1
- package/dist/cjs/utils/CommandOutputDistiller.d.ts +75 -0
- package/dist/cjs/utils/CommandOutputDistiller.d.ts.map +1 -0
- package/dist/cjs/utils/CommandOutputDistiller.js +598 -0
- package/dist/cjs/utils/CommandOutputDistiller.js.map +1 -0
- package/dist/cjs/utils/PatchMatcher.d.ts +17 -0
- package/dist/cjs/utils/PatchMatcher.d.ts.map +1 -0
- package/dist/cjs/utils/PatchMatcher.js +210 -0
- package/dist/cjs/utils/PatchMatcher.js.map +1 -0
- package/dist/cjs/utils/ReadWorkspaceCache.d.ts +9 -0
- package/dist/cjs/utils/ReadWorkspaceCache.d.ts.map +1 -0
- package/dist/cjs/utils/ReadWorkspaceCache.js +31 -0
- package/dist/cjs/utils/ReadWorkspaceCache.js.map +1 -0
- package/dist/cjs/utils/SearchCache.d.ts +7 -0
- package/dist/cjs/utils/SearchCache.d.ts.map +1 -0
- package/dist/cjs/utils/SearchCache.js +75 -0
- package/dist/cjs/utils/SearchCache.js.map +1 -0
- package/dist/cjs/utils/StructuralSearch.d.ts +18 -0
- package/dist/cjs/utils/StructuralSearch.d.ts.map +1 -0
- package/dist/cjs/utils/StructuralSearch.js +190 -0
- package/dist/cjs/utils/StructuralSearch.js.map +1 -0
- package/dist/cjs/utils/WorkspaceSearch.d.ts +20 -0
- package/dist/cjs/utils/WorkspaceSearch.d.ts.map +1 -0
- package/dist/cjs/utils/WorkspaceSearch.js +0 -0
- package/dist/cjs/utils/WorkspaceSearch.js.map +1 -0
- package/dist/esm/mcp/prompts.d.ts +1 -1
- package/dist/esm/mcp/prompts.d.ts.map +1 -1
- package/dist/esm/mcp/prompts.js +4 -1
- package/dist/esm/mcp/prompts.js.map +1 -1
- package/dist/esm/mcp/tools.d.ts.map +1 -1
- package/dist/esm/mcp/tools.js +209 -76
- package/dist/esm/mcp/tools.js.map +1 -1
- package/dist/esm/subconscious/TheBrainV2.d.ts +4 -1
- package/dist/esm/subconscious/TheBrainV2.d.ts.map +1 -1
- package/dist/esm/subconscious/TheBrainV2.js +49 -9
- package/dist/esm/subconscious/TheBrainV2.js.map +1 -1
- package/dist/esm/utils/CommandOutputDistiller.d.ts +75 -0
- package/dist/esm/utils/CommandOutputDistiller.d.ts.map +1 -0
- package/dist/esm/utils/CommandOutputDistiller.js +588 -0
- package/dist/esm/utils/CommandOutputDistiller.js.map +1 -0
- package/dist/esm/utils/PatchMatcher.d.ts +17 -0
- package/dist/esm/utils/PatchMatcher.d.ts.map +1 -0
- package/dist/esm/utils/PatchMatcher.js +205 -0
- package/dist/esm/utils/PatchMatcher.js.map +1 -0
- package/dist/esm/utils/ReadWorkspaceCache.d.ts +9 -0
- package/dist/esm/utils/ReadWorkspaceCache.d.ts.map +1 -0
- package/dist/esm/utils/ReadWorkspaceCache.js +24 -0
- package/dist/esm/utils/ReadWorkspaceCache.js.map +1 -0
- package/dist/esm/utils/SearchCache.d.ts +7 -0
- package/dist/esm/utils/SearchCache.d.ts.map +1 -0
- package/dist/esm/utils/SearchCache.js +67 -0
- package/dist/esm/utils/SearchCache.js.map +1 -0
- package/dist/esm/utils/StructuralSearch.d.ts +18 -0
- package/dist/esm/utils/StructuralSearch.d.ts.map +1 -0
- package/dist/esm/utils/StructuralSearch.js +150 -0
- package/dist/esm/utils/StructuralSearch.js.map +1 -0
- package/dist/esm/utils/WorkspaceSearch.d.ts +20 -0
- package/dist/esm/utils/WorkspaceSearch.d.ts.map +1 -0
- package/dist/esm/utils/WorkspaceSearch.js +0 -0
- package/dist/esm/utils/WorkspaceSearch.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# 🧠 Lemma v1.
|
|
1
|
+
# 🧠 Lemma v1.4.0
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@nxuss/lemma)
|
|
4
4
|
[](https://github.com/Nxusbets/lemma/blob/main/LICENSE)
|
|
@@ -23,7 +23,36 @@ Lemma is a high-performance orchestration layer that sits between your developme
|
|
|
23
23
|
|
|
24
24
|
## 📋 Changelog
|
|
25
25
|
|
|
26
|
-
### v1.
|
|
26
|
+
### v1.4.0 — Correctness-first patching, search, and memory freshness *(NEW)*
|
|
27
|
+
|
|
28
|
+
An audit of the everyday MCP path — read a file, search the workspace, patch it, remember the
|
|
29
|
+
answer — found the same failure mode repeating: a tool that looked done but silently degraded
|
|
30
|
+
correctness or wasted tokens the moment reality diverged from the happy path. This release fixes
|
|
31
|
+
that path end to end.
|
|
32
|
+
|
|
33
|
+
| Tool | What changed | Why |
|
|
34
|
+
| :--- | :--- | :--- |
|
|
35
|
+
| `apply_workspace_patch` | Cascading match: exact → whitespace-normalized → indent-anchored → fuzzy (similarity-scored, with an anti-tie margin). On total failure, the error now includes a line-diff against the closest candidate block instead of a generic "not found". | A patch built from a `read_workspace_file(compact:true)` view (comments/imports stripped, whitespace collapsed) could never byte-match the file on disk — a guaranteed failure on the most common read→edit flow, forcing an expensive full re-read every time. |
|
|
36
|
+
| `search_workspace` | Respects `.gitignore`, skips binaries and files over 500KB, supports comma-separated multi-extension filters, caps results with `maxResults`, and groups matches by file (capped per file) instead of one repeated-path line per hit. Code files (`.ts/.tsx/.js/.jsx`) get symbol-level grouping — matches inside the same function/class collapse into one entry with its signature and full line list instead of raw duplicate lines. Repeating an identical query while the workspace is unchanged (tracked via a cheap mtime/size fingerprint, no content re-reads) returns the cached result instantly. | A single broad query could read 300+ files (2.6MB+) unfiltered and return 15,000+ tokens of flat, path-repeated text — often more expensive than just reading the files directly. |
|
|
37
|
+
| `read_workspace_file` | Tracks the content hash last sent for each `(file, compact)` pair for the life of the MCP session. Re-reading an unchanged file returns a short `UNCHANGED` marker instead of the full content again; `force:true` bypasses it. | Re-reading the same file to verify an edit — one of the most common steps in a multi-turn coding session — was resending the full file every time, even when nothing had changed since the last read. |
|
|
38
|
+
| `store_memory` / `search_memory` | `store_memory` accepts an optional `filePaths` array and records a sha256 per file. `search_memory` now splits results into **fresh** (safe to reuse) and **stale** (a tracked file changed since it was stored) — a stale hit is never returned as if it were current. | The Brain's semantic match (BM25 + Jaccard) is intentionally fuzzy so differently-phrased questions still hit — but with no invalidation, a fuzzy hit could resurface a stale answer about code that had since changed, with no signal that it was stale. |
|
|
39
|
+
|
|
40
|
+
**Net effect**: the four tools an agent reaches for by default — read, search, patch, remember —
|
|
41
|
+
now fail closed (a diff instead of a dead-end error) and skip redundant work automatically instead
|
|
42
|
+
of requiring the calling model to remember to check first.
|
|
43
|
+
|
|
44
|
+
### Files changed
|
|
45
|
+
- `src/utils/PatchMatcher.ts` *(new)* — cascading match strategies + line-diff generator
|
|
46
|
+
- `src/utils/WorkspaceSearch.ts` *(new)* — gitignore-aware, capped, grouped search
|
|
47
|
+
- `src/utils/StructuralSearch.ts` *(new)* — AST-based symbol grouping for code file matches
|
|
48
|
+
- `src/utils/SearchCache.ts` *(new)* — session-scoped, fingerprint-invalidated search cache
|
|
49
|
+
- `src/utils/ReadWorkspaceCache.ts` *(new)* — session-scoped read-dedup
|
|
50
|
+
- `src/subconscious/TheBrainV2.ts` — `fileHashes` on entries, `checkEntryFreshness`, `fresh`/`staleFiles` on search results
|
|
51
|
+
- `src/mcp/tools.ts` — wired all of the above into `apply_workspace_patch`, `search_workspace`, `read_workspace_file`, `store_memory`, `search_memory`
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
### v1.3.0 — Verifiable Caching
|
|
27
56
|
|
|
28
57
|
Two new tools built around a single rule: don't ask the model to trust a similarity score or a
|
|
29
58
|
savings estimate — give it something it can actually verify.
|
|
@@ -496,7 +525,8 @@ Once connected, your IDE gains access to **62 MCP tools** — 11 free + 51 Pro.
|
|
|
496
525
|
| `list_workspace_dir` | Recursively list the project structure up to a configurable depth. |
|
|
497
526
|
| `search_workspace` | Fast text search (grep) across all project files. |
|
|
498
527
|
| `semantic_grep` | Natural language concept search using BM25. |
|
|
499
|
-
| `run_workspace_command` | Execute bash commands in the workspace root. Secured by allowlist. |
|
|
528
|
+
| `run_workspace_command` | Execute bash commands in the workspace root. Secured by allowlist. Distills long jest/vitest/tsc/eslint output down to the failures and their locations; full output kept retrievable. |
|
|
529
|
+
| `output_region` | Retrieve any part of a stored command output verbatim (whole thing, one failing test, or a line range). |
|
|
500
530
|
| `file_intent_index` | Group files by domain intent (auth, routing, config). |
|
|
501
531
|
|
|
502
532
|
### 🔬 Code Intelligence Tools
|
package/bin/init.js
CHANGED
|
@@ -199,7 +199,7 @@ function configureClaudeDesktop() {
|
|
|
199
199
|
// ─── Lemma MCP tool names (server prefix: "lemma") ────────────────────────────
|
|
200
200
|
const FALLBACK_LEMMA_TOOL_NAMES = [
|
|
201
201
|
'read_workspace_file', 'write_workspace_file', 'create_workspace_file', 'list_workspace_dir',
|
|
202
|
-
'search_workspace', 'apply_workspace_patch', 'run_workspace_command',
|
|
202
|
+
'search_workspace', 'apply_workspace_patch', 'run_workspace_command', 'output_region',
|
|
203
203
|
'search_memory', 'store_memory', 'smarter_cache',
|
|
204
204
|
'get_project_onboarding', 'get_telepathic_hints', 'get_ast_hologram',
|
|
205
205
|
'get_symbol_surgical_context', 'compress_context',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
-
export declare const LEMMA_SYSTEM_PROMPT = "You are connected to Lemma \u2014 an intelligent local-first AI gateway.\nLemma gives you a set of tools you MUST use proactively and automatically. NEVER ask the user\nto call a tool manually. YOU invoke the tools. The user should experience zero friction.\n\n## MANDATORY AUTOMATIC BEHAVIORS\n\n### 1. Before reading ANY file \u2192 call get_ast_hologram first\nWhen you need to understand the structure of a codebase or navigate files, ALWAYS call\n`get_ast_hologram` first (with the relevant dirPath, e.g. \"src\"). Use the returned symbol\nmap to pinpoint exactly which files to read. NEVER blindly read every file one by one.\n\n### 2. Before answering ANY technical question \u2192 call query_hybrid_consensus first\nBEFORE making any reasoning or cloud LLM call, ALWAYS call `query_hybrid_consensus` with\nthe user's question. If similarity \u2265 80%, return the cached answer immediately \u2014 no cloud\ncall needed. Only proceed with your own reasoning if the Brain returns a MISS.\n\n### 3. When you start working on ANY file \u2192 call get_telepathic_hints automatically\nWhenever the user mentions a file or you are about to read/edit a file, IMMEDIATELY call\n`get_telepathic_hints` with that file's path. Surface the results to the user as relevant\ncontext BEFORE doing any work. Do not ask permission \u2014 just do it.\n\n### 4. Before applying ANY code patch \u2192 call validate_patch_sandbox first\nNEVER call `write_workspace_file` or `apply_workspace_patch` without FIRST calling\n`validate_patch_sandbox` with the proposed content. Only proceed with the write if the\nverdict is \"\u2705 SAFE TO APPLY\". If it fails, fix the issues and re-validate.\n\n### 5. After solving any technical problem \u2192 call store_memory automatically\nAfter resolving a bug, implementing a feature, or answering a significant technical question,\nALWAYS call `store_memory` to persist the solution. Use a descriptive query key so it can\nbe retrieved in future sessions across ALL projects.\n\n### 6. Workspace navigation \u2192 always use Lemma tools, NEVER guess file contents\n- Use `list_workspace_dir` to navigate structure\n- Use `read_workspace_file` (auto-compresses + scrubs secrets) instead of asking the user\n- Use `search_workspace` for text search across files\n- Use `run_workspace_command` for builds, tests, linters\n\n### 7. Privacy is automatic\n`read_workspace_file` and `search_workspace` automatically scrub API keys and PII.\nYou never need to manually sanitize file content.\n\n## LEMMA RESOURCES (auto-subscribe when available)\n- `lemma://runtime/context` \u2014 live crash logs. Read this FIRST when the user reports a bug.\n- `lemma://multiverse/timeline` \u2014 AST history. Use when the user says \"it was working before\".\n- `lemma://project/onboarding` \u2014 architecture map. Read at session start in new repos.\n- `lemma://stats/usage` \u2014 token savings report.\n\nRemember: the user hired YOU to be autonomous. Act like it.";
|
|
2
|
+
export declare const LEMMA_SYSTEM_PROMPT = "You are connected to Lemma \u2014 an intelligent local-first AI gateway.\nLemma gives you a set of tools you MUST use proactively and automatically. NEVER ask the user\nto call a tool manually. YOU invoke the tools. The user should experience zero friction.\n\n## MANDATORY AUTOMATIC BEHAVIORS\n\n### 1. Before reading ANY file \u2192 call get_ast_hologram first\nWhen you need to understand the structure of a codebase or navigate files, ALWAYS call\n`get_ast_hologram` first (with the relevant dirPath, e.g. \"src\"). Use the returned symbol\nmap to pinpoint exactly which files to read. NEVER blindly read every file one by one.\n\n### 2. Before answering ANY technical question \u2192 call query_hybrid_consensus first\nBEFORE making any reasoning or cloud LLM call, ALWAYS call `query_hybrid_consensus` with\nthe user's question. If similarity \u2265 80%, return the cached answer immediately \u2014 no cloud\ncall needed. Only proceed with your own reasoning if the Brain returns a MISS.\n\n### 3. When you start working on ANY file \u2192 call get_telepathic_hints automatically\nWhenever the user mentions a file or you are about to read/edit a file, IMMEDIATELY call\n`get_telepathic_hints` with that file's path. Surface the results to the user as relevant\ncontext BEFORE doing any work. Do not ask permission \u2014 just do it.\n\n### 4. Before applying ANY code patch \u2192 call validate_patch_sandbox first\nNEVER call `write_workspace_file` or `apply_workspace_patch` without FIRST calling\n`validate_patch_sandbox` with the proposed content. Only proceed with the write if the\nverdict is \"\u2705 SAFE TO APPLY\". If it fails, fix the issues and re-validate.\n\n### 5. After solving any technical problem \u2192 call store_memory automatically\nAfter resolving a bug, implementing a feature, or answering a significant technical question,\nALWAYS call `store_memory` to persist the solution. Use a descriptive query key so it can\nbe retrieved in future sessions across ALL projects.\n\n### 6. Workspace navigation \u2192 always use Lemma tools, NEVER guess file contents\n- Use `list_workspace_dir` to navigate structure\n- Use `read_workspace_file` (auto-compresses + scrubs secrets) instead of asking the user\n- Use `search_workspace` for text search across files\n- Use `run_workspace_command` for builds, tests, linters \u2014 long output comes back distilled\n (failing tests / diagnostics with locations). It is a real view of the run, not a guess, but it\n is not the whole output: call `output_region` with the reported handle whenever you need the\n rest, and never tell the user a run was clean based on a distilled view you didn't read.\n\n### 7. Privacy is automatic\n`read_workspace_file` and `search_workspace` automatically scrub API keys and PII.\nYou never need to manually sanitize file content.\n\n## LEMMA RESOURCES (auto-subscribe when available)\n- `lemma://runtime/context` \u2014 live crash logs. Read this FIRST when the user reports a bug.\n- `lemma://multiverse/timeline` \u2014 AST history. Use when the user says \"it was working before\".\n- `lemma://project/onboarding` \u2014 architecture map. Read at session start in new repos.\n- `lemma://stats/usage` \u2014 token savings report.\n\nRemember: the user hired YOU to be autonomous. Act like it.";
|
|
3
3
|
export declare const LEMMA_TURBOMODE_PROMPT = "You are operating in LEMMA TURBOMODE. Your goal: multiply the user's LLM membership 50x by spending 50x fewer tokens per task. Every token counts. Every cache hit is a membership extension.\n\n## MANDATORY TURBOMODE RULES \u2014 VIOLATE THESE AND YOU WASTE THE USER'S MONEY\n\n### 0. Before ANYTHING \u2192 check token_budget\nEvery 5 turns, call `token_budget` to check your consumption. If multiplier drops below 10x, optimize harder.\n\n### 1. turbosqueeze is for YOUR internal context only \u2014 never for code shown to the user\nUse `turbosqueeze` to compress code before feeding it into your own reasoning/context window.\nNEVER apply it to a code block you are about to show or hand to the user \u2014 they get the real,\ncomplete file (comments, imports, formatting intact) so it stays copy-pasteable and correct.\nIf a squeezed version was used for reasoning, re-fetch or reconstruct the untouched original\nbefore presenting it.\n\n### 2. smarter_cache (threshold 0.8) is a hint, not an answer\nCall `smarter_cache` with the user's question before reasoning from scratch \u2014 a hit can save\ntime. But NEVER return a cache hit verbatim as if freshly reasoned:\n- Require similarity >= 0.8 (raised from 0.75 \u2014 0.75 was matching questions that weren't\n actually the same question).\n- Always tell the user the answer came from cache (e.g. \"from a cached earlier answer:\").\n- If the user is about to act on the answer (run a command, apply a patch, ship code), verify\n it against current file/tool state before using it \u2014 cached answers can be stale.\n- On a MISS, or on any doubt, reason normally.\n\n### 3. Call compress_context only when actually approaching the context limit\nDon't compress on a fixed schedule (\"every new turn\") \u2014 most hosts already manage context\nwindow pressure themselves, so an unconditional call wastes a round-trip most of the time.\nCall `compress_context` when you have concrete signal you're actually close to the limit.\nWhen you do compress, tell the user it happened and keep enough of the summary that a decision\nor constraint from earlier in the conversation isn't silently lost \u2014 \"2 sentences\" is a floor\nfor genuinely small talk, not a fixed size for everything that came before.\n\n### 4. Before reading files \u2192 call get_ast_hologram\nNEVER read files blindly. Always call `get_ast_hologram` first to locate the exact symbols you need. Then read only those specific files. Each unnecessary file read wastes 1000+ tokens.\n\n### 4b. Before re-reading a file \u2192 call file_fingerprint\nBefore calling read_workspace_file, check if the file hash has changed since last read. If status is 'unchanged', skip the read entirely. Only read when hash differs.\n\n### 4c. Before deep file analysis \u2192 use import_tree_context + type_coverage_report\nInstead of reading full files to understand architecture, call import_tree_context (import graph) and type_coverage_report (type safety). Both are zero-token tools.\n\n### 4d. Before investigating code ownership \u2192 call git_blame_heat\nFor questions about who wrote code, why it's risky, or change history, call git_blame_heat instead of reasoning. Zero LLM cost.\n\n### 5. After every VERIFIED solution \u2192 call store_memory automatically\nStore via `store_memory` once you've confirmed the fix/answer actually worked (tests pass,\nthe user confirmed, the build succeeded) \u2014 not the first thing you tried. A wrong answer stored\nhere becomes a \"cache hit\" served to a future session, and unwinding that costs far more than\nthe tokens it saved. Verified answers are still free to cache aggressively.\n\n### 6. Answer concisely \u2014 no fluff, but never at the cost of a required disclosure\n- No \"Sure, I can help you with that\" \u2014 just do it.\n- No unnecessary explanation of what you did \u2014 unless asked.\n- No markdown formatting in internal responses.\n- No repeating the user's question back to them.\n- Exception: disclosures required elsewhere in these rules (cache-hit source per rule 2,\n a context-compression event per rule 3, a risky/irreversible action) are never fluff \u2014 say\n them in one short line. Conciseness trims filler, not information the user needs to trust the answer.\n\n### 7. Use batch_tool_calls aggressively\nAny set of independent operations (read 3 files, search 2 patterns, check cache for 2 questions) \u2014 use `batch_tool_calls` to run them in parallel. Single round-trip > multiple sequential calls.\n\n### 8. Before multi-file operations \u2192 use auto_context_bundle or bulk_file_digest\nInstead of reading files one by one, use `auto_context_bundle` (one file, everything included) or `bulk_file_digest` (multiple files, one call). Both save 75-80% round-trips.\n\n### 9. Before outputting long responses \u2192 check repeat_guard\nCall `repeat_guard` with your draft output to avoid repeating yourself. Saves 10-15% of output tokens.\n\n### 9b. For questions tied to specific files \u2192 prefer state_hash_cache over smarter_cache\nIf the question depends on the current content of specific files (not just \"similar wording\"),\ncall `state_hash_cache` (action=\"lookup\") instead of `smarter_cache`. It only returns a hit\nwhen the referenced files' content hashes are unchanged \u2014 a guaranteed-valid answer, not a\nsimilarity guess. After answering, call it again with action=\"store\" to cache it. Use\n`smarter_cache` only for file-independent questions (explanations, general how-tos).\n\n### 9c. Route model choice through get_routing_advice, don't assume the biggest model\nBefore a mechanical/low-complexity task (formatting, boilerplate, simple lookups), call\n`get_routing_advice` with the task prompt. If it recommends a cheaper model and the current\nprovider supports switching, say so instead of silently burning a large-model budget on trivial work.\n\n### 9d. Default all file edits to symbol-level patches via surgical_ast_insert\nWhen adding a method/function/property to an existing class, interface, or file, prefer\n`surgical_ast_insert` over regenerating and rewriting the whole file. Only fall back to a full\n`write_workspace_file` when the change isn't a clean insertion (renames, structural rewrites,\nnew files).\n\n### 9e. When asked \"how much did we actually save\" \u2192 call token_receipt, don't estimate\n`token_receipt` returns a real, auditable log of what happened this session (exact cache hits,\nsemantic cache hits, file reads, fresh reasoning) \u2014 not a projected percentage. Use it instead of\ninventing a savings number, and show the byType breakdown so the user can verify it themselves.\n\n### 10. When resuming work \u2192 load conversation_checkpoint\nAt session start or when user says \"continuemos\", call `conversation_checkpoint` with action \"load\" to restore context without re-explaining.\n\n### 11. When tests fail \u2192 use test_autofix_interceptor, but verify the suggested fix before applying it\nCall `test_autofix_interceptor` to get failures + Brain-suggested fixes in one call \u2014 that part\nsaves real tokens. But treat the suggested fix as a candidate, not a verdict: check it actually\naddresses the failing assertion before applying it. Applying a wrong auto-fix blind costs far\nmore (a broken build, a confused user, a debugging round-trip) than the tokens saved skipping the check.\n\n### 12. For common prompt patterns \u2192 use prompt_pattern_cache\nBefore generating repetitive prompts (\"explain this\", \"review this\", \"write tests\"), check `prompt_pattern_cache` action \"find\" for cached templates.\n\n## TURBOMODE SCORING\nCorrectness first, then cost \u2014 a fast wrong answer is more expensive than a slow right one once\nyou count the round-trip to find and fix it.\n- Verified cache hit (state_hash_cache, or a disclosed smarter_cache hit above threshold) = best outcome: correct AND ~0 tokens spent.\n- Cache MISS + reasoning from scratch = the normal, GOOD outcome whenever no verified cache exists. Reasoning is not a failure mode \u2014 it's what you do when you don't already have a trustworthy answer.\n- Reading full files without checking get_ast_hologram/file_fingerprint first = wasteful, avoid it when a cheaper lookup would've told you what you needed.\n- Any answer delivered without required disclosure (rule 6's exception), or a cached/auto-fixed result used without verification = FAIL, regardless of tokens saved \u2014 it can cost the user far more than tokens once it's wrong.\n\nRemember: the goal is spending fewer tokens on the SAME quality of answer, not a lower-quality answer for fewer tokens.";
|
|
4
4
|
export declare function setupPromptsHandlers(server: Server): void;
|
|
5
5
|
//# sourceMappingURL=prompts.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prompts.d.ts","sourceRoot":"","sources":["../../../src/mcp/prompts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAGnE,eAAO,MAAM,mBAAmB,
|
|
1
|
+
{"version":3,"file":"prompts.d.ts","sourceRoot":"","sources":["../../../src/mcp/prompts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAGnE,eAAO,MAAM,mBAAmB,0uGAkD4B,CAAC;AAE7D,eAAO,MAAM,sBAAsB,27QA+GqF,CAAC;AAEzH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAiDzD"}
|
package/dist/cjs/mcp/prompts.js
CHANGED
|
@@ -38,7 +38,10 @@ be retrieved in future sessions across ALL projects.
|
|
|
38
38
|
- Use \`list_workspace_dir\` to navigate structure
|
|
39
39
|
- Use \`read_workspace_file\` (auto-compresses + scrubs secrets) instead of asking the user
|
|
40
40
|
- Use \`search_workspace\` for text search across files
|
|
41
|
-
- Use \`run_workspace_command\` for builds, tests, linters
|
|
41
|
+
- Use \`run_workspace_command\` for builds, tests, linters — long output comes back distilled
|
|
42
|
+
(failing tests / diagnostics with locations). It is a real view of the run, not a guess, but it
|
|
43
|
+
is not the whole output: call \`output_region\` with the reported handle whenever you need the
|
|
44
|
+
rest, and never tell the user a run was clean based on a distilled view you didn't read.
|
|
42
45
|
|
|
43
46
|
### 7. Privacy is automatic
|
|
44
47
|
\`read_workspace_file\` and \`search_workspace\` automatically scrub API keys and PII.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prompts.js","sourceRoot":"","sources":["../../../src/mcp/prompts.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"prompts.js","sourceRoot":"","sources":["../../../src/mcp/prompts.ts"],"names":[],"mappings":";;;AAwKA,oDAiDC;AAxND,iEAAsG;AAEzF,QAAA,mBAAmB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4DAkDyB,CAAC;AAEhD,QAAA,sBAAsB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wHA+GkF,CAAC;AAEzH,SAAgB,oBAAoB,CAAC,MAAc;IACjD,MAAM,CAAC,iBAAiB,CAAC,mCAAwB,EAAE,KAAK,IAAI,EAAE;QAC5D,OAAO;YACL,OAAO,EAAE;gBACP;oBACE,IAAI,EAAE,iBAAiB;oBACvB,WAAW,EAAE,+JAA+J;oBAC5K,SAAS,EAAE,EAAE;iBACd;gBACD;oBACE,IAAI,EAAE,iBAAiB;oBACvB,WAAW,EAAE,wOAAwO;oBACrP,SAAS,EAAE,EAAE;iBACd;aACF;SACF,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,MAAM,CAAC,iBAAiB,CAAC,iCAAsB,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;QACjE,IAAI,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,iBAAiB,EAAE,CAAC;YAC9C,OAAO;gBACL,WAAW,EAAE,qCAAqC;gBAClD,QAAQ,EAAE;oBACR;wBACE,IAAI,EAAE,MAAM;wBACZ,OAAO,EAAE;4BACP,IAAI,EAAE,MAAM;4BACZ,IAAI,EAAE,2BAAmB;yBAC1B;qBACF;iBACF;aACF,CAAC;QACJ,CAAC;QACD,IAAI,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,iBAAiB,EAAE,CAAC;YAC9C,OAAO;gBACL,WAAW,EAAE,8CAA8C;gBAC3D,QAAQ,EAAE;oBACR;wBACE,IAAI,EAAE,MAAM;wBACZ,OAAO,EAAE;4BACP,IAAI,EAAE,MAAM;4BACZ,IAAI,EAAE,8BAAsB;yBAC7B;qBACF;iBACF;aACF,CAAC;QACJ,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,mBAAmB,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;IAC5D,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -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;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"}
|
package/dist/cjs/mcp/tools.js
CHANGED
|
@@ -72,6 +72,11 @@ const SurgicalASTInsert_1 = require("../utils/SurgicalASTInsert");
|
|
|
72
72
|
const mcp_tools_1 = require("../infra/mcp-tools");
|
|
73
73
|
const StateHashCache_1 = require("../utils/StateHashCache");
|
|
74
74
|
const TokenReceipt_1 = require("../utils/TokenReceipt");
|
|
75
|
+
const CommandOutputDistiller_1 = require("../utils/CommandOutputDistiller");
|
|
76
|
+
const PatchMatcher_1 = require("../utils/PatchMatcher");
|
|
77
|
+
const WorkspaceSearch_1 = require("../utils/WorkspaceSearch");
|
|
78
|
+
const ReadWorkspaceCache_1 = require("../utils/ReadWorkspaceCache");
|
|
79
|
+
const SearchCache_1 = require("../utils/SearchCache");
|
|
75
80
|
// ── Pro License Gate ──────────────────────────────────────────────────────────
|
|
76
81
|
const LICENSE_FILE = path_1.default.join(os_1.default.homedir(), '.lemma-cache', 'license.json');
|
|
77
82
|
let _proStatusCache = null;
|
|
@@ -128,7 +133,7 @@ const toolDefinitions = [
|
|
|
128
133
|
},
|
|
129
134
|
{
|
|
130
135
|
name: "search_memory",
|
|
131
|
-
description: "Search Lemma's semantic memory (The Brain) before
|
|
136
|
+
description: "Search Lemma's semantic memory (The Brain) before investigating something from scratch — retrieves past solutions, fixes, and context from ALL your projects globally. Results are split into 'fresh' (safe to reuse — either untracked general knowledge, or every file it depended on still hashes the same) and 'stale' (a similar question was answered before, but a tracked file changed since — re-verify against current state before reusing). Never treat a stale result as current.",
|
|
132
137
|
inputSchema: {
|
|
133
138
|
type: "object",
|
|
134
139
|
properties: {
|
|
@@ -140,13 +145,14 @@ const toolDefinitions = [
|
|
|
140
145
|
},
|
|
141
146
|
{
|
|
142
147
|
name: "store_memory",
|
|
143
|
-
description: "Persist a technical solution, bug fix, architecture decision, or key fact into Lemma's Brain.",
|
|
148
|
+
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.",
|
|
144
149
|
inputSchema: {
|
|
145
150
|
type: "object",
|
|
146
151
|
properties: {
|
|
147
152
|
query: { type: "string", description: "The natural language query key" },
|
|
148
153
|
response: { type: "string", description: "The complete technical content to memorize" },
|
|
149
154
|
provider: { type: "string", description: "Optional model provider name", default: "generic" },
|
|
155
|
+
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." },
|
|
150
156
|
},
|
|
151
157
|
required: ["query", "response"],
|
|
152
158
|
},
|
|
@@ -175,12 +181,13 @@ const toolDefinitions = [
|
|
|
175
181
|
},
|
|
176
182
|
{
|
|
177
183
|
name: "read_workspace_file",
|
|
178
|
-
description: "Read a file inside the workspace. Automatically compresses comments/whitespace and scrubs API keys.",
|
|
184
|
+
description: "Read a file inside the workspace. Automatically compresses comments/whitespace and scrubs API keys. If this exact file (same compact mode) was already read this session and hasn't changed on disk, returns a short UNCHANGED marker instead of resending the full content — reuse your existing copy. Pass force:true to always get the full content.",
|
|
179
185
|
inputSchema: {
|
|
180
186
|
type: "object",
|
|
181
187
|
properties: {
|
|
182
188
|
filePath: { type: "string", description: "Path relative to the project root" },
|
|
183
189
|
compact: { type: "boolean", description: "Whether to compress comments and whitespace", default: true },
|
|
190
|
+
force: { type: "boolean", description: "Bypass the unchanged-file short-circuit and always return full content", default: false },
|
|
184
191
|
},
|
|
185
192
|
required: ["filePath"],
|
|
186
193
|
},
|
|
@@ -211,7 +218,7 @@ const toolDefinitions = [
|
|
|
211
218
|
},
|
|
212
219
|
{
|
|
213
220
|
name: "apply_workspace_patch",
|
|
214
|
-
description: "Apply a smart search-and-replace patch to an existing file. Safe against duplicate matches.",
|
|
221
|
+
description: "Apply a smart search-and-replace patch to an existing file. Falls back through whitespace-normalized, indent-anchored, and fuzzy matching if the exact block isn't found — so a searchContent built from a compacted read still applies. Safe against duplicate matches. On total failure, the error includes a diff against the closest candidate block.",
|
|
215
222
|
inputSchema: {
|
|
216
223
|
type: "object",
|
|
217
224
|
properties: {
|
|
@@ -224,15 +231,35 @@ const toolDefinitions = [
|
|
|
224
231
|
},
|
|
225
232
|
{
|
|
226
233
|
name: "run_workspace_command",
|
|
227
|
-
description: "Execute a bash command in the workspace root.
|
|
234
|
+
description: "Execute a bash command in the workspace root (15s timeout). Captures both stdout and stderr. Long test/build/lint output is distilled deterministically — for jest, vitest and tsc it returns the failing tests or diagnostics with their locations and reasons, dropping code frames and node_modules stack frames; anything else falls back to head/tail plus error-matching lines. The complete output is always stored first and the reply carries a handle: nothing is lost, use output_region to retrieve any part verbatim. Pass raw:true to skip distillation.",
|
|
228
235
|
inputSchema: {
|
|
229
236
|
type: "object",
|
|
230
237
|
properties: {
|
|
231
238
|
command: { type: "string", description: "The bash command to run" },
|
|
239
|
+
raw: {
|
|
240
|
+
type: "boolean",
|
|
241
|
+
description: "Return the complete untouched output instead of the distilled view",
|
|
242
|
+
default: false,
|
|
243
|
+
},
|
|
232
244
|
},
|
|
233
245
|
required: ["command"],
|
|
234
246
|
},
|
|
235
247
|
},
|
|
248
|
+
{
|
|
249
|
+
name: "output_region",
|
|
250
|
+
description: "Retrieve any part of a command output previously stored by run_workspace_command, verbatim. This is what makes distillation lossless — use it when the distilled view isn't enough. Pass all:true for the whole output, section for one named block (a failing test title or a file path, as listed in the distilled view), or startLine/endLine for a range.",
|
|
251
|
+
inputSchema: {
|
|
252
|
+
type: "object",
|
|
253
|
+
properties: {
|
|
254
|
+
handle: { type: "string", description: "The handle reported by run_workspace_command" },
|
|
255
|
+
section: { type: "string", description: "Name of a section to retrieve (failing test title or file path)" },
|
|
256
|
+
startLine: { type: "number", description: "First line to return (1-indexed, inclusive)" },
|
|
257
|
+
endLine: { type: "number", description: "Last line to return (1-indexed, inclusive)" },
|
|
258
|
+
all: { type: "boolean", description: "Return the complete stored output", default: false },
|
|
259
|
+
},
|
|
260
|
+
required: ["handle"],
|
|
261
|
+
},
|
|
262
|
+
},
|
|
236
263
|
{
|
|
237
264
|
name: "list_workspace_dir",
|
|
238
265
|
description: "List files and subdirectories recursively to navigate the repository structure.",
|
|
@@ -246,12 +273,13 @@ const toolDefinitions = [
|
|
|
246
273
|
},
|
|
247
274
|
{
|
|
248
275
|
name: "search_workspace",
|
|
249
|
-
description: "Perform a fast local text search (grep) across all files in the project workspace.",
|
|
276
|
+
description: "Perform a fast local text search (grep) across all files in the project workspace. Respects .gitignore, skips binaries and files over 500KB, groups results by file (capped per file), and stops at maxResults. For code files (.ts/.tsx/.js/.jsx), matches inside the same function/class/method are collapsed into one entry with its signature and line range instead of raw duplicate lines. Repeating an identical query while the workspace is unchanged returns the cached result instantly instead of re-scanning.",
|
|
250
277
|
inputSchema: {
|
|
251
278
|
type: "object",
|
|
252
279
|
properties: {
|
|
253
280
|
query: { type: "string", description: "The keyword or text pattern to search for" },
|
|
254
|
-
extension: { type: "string", description: "Optional file extension filter (e.g., 'ts
|
|
281
|
+
extension: { type: "string", description: "Optional file extension filter, comma-separated for multiple (e.g., 'ts,tsx')" },
|
|
282
|
+
maxResults: { type: "number", description: "Maximum total matches to return before truncating", default: 200 },
|
|
255
283
|
},
|
|
256
284
|
required: ["query"],
|
|
257
285
|
},
|
|
@@ -1014,6 +1042,7 @@ const toolHandlers = {
|
|
|
1014
1042
|
create_workspace_file: handleCreateWorkspaceFile,
|
|
1015
1043
|
apply_workspace_patch: handleApplyWorkspacePatch,
|
|
1016
1044
|
run_workspace_command: handleRunWorkspaceCommand,
|
|
1045
|
+
output_region: handleOutputRegion,
|
|
1017
1046
|
list_workspace_dir: handleListWorkspaceDir,
|
|
1018
1047
|
search_workspace: handleSearchWorkspace,
|
|
1019
1048
|
squeeze_prompt: handleSqueezePrompt,
|
|
@@ -1152,12 +1181,27 @@ async function handleSearchMemory(args) {
|
|
|
1152
1181
|
if (results.length === 0) {
|
|
1153
1182
|
return { content: [{ type: "text", text: "No relevant memories found in Lemma's Brain." }] };
|
|
1154
1183
|
}
|
|
1155
|
-
const
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1184
|
+
const fresh = results.filter((r) => r.fresh);
|
|
1185
|
+
const stale = results.filter((r) => !r.fresh);
|
|
1186
|
+
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)}`;
|
|
1187
|
+
if (fresh.length > 0) {
|
|
1188
|
+
// Only a fresh hit (or an untracked, purely conceptual entry) counts as a real
|
|
1189
|
+
// avoided re-investigation — this is the only case worth crediting as savings.
|
|
1190
|
+
const bestFresh = fresh[0];
|
|
1191
|
+
const tokensSaved = Math.max(100, Math.floor(String(bestFresh.response).length / 4));
|
|
1192
|
+
(0, reportSavings_1.reportSavings)({ source: "cache", tokens: tokensSaved, toolName: "search_memory", query: query.substring(0, 100) });
|
|
1193
|
+
}
|
|
1194
|
+
const parts = [];
|
|
1195
|
+
if (fresh.length > 0) {
|
|
1196
|
+
parts.push(`Lemma found ${fresh.length} fresh ${fresh.length === 1 ? "memory" : "memories"} (from cache — say so if you use this):\n\n${fresh.map(formatResult).join("\n\n---\n\n")}`);
|
|
1197
|
+
}
|
|
1198
|
+
if (stale.length > 0) {
|
|
1199
|
+
const staleText = stale
|
|
1200
|
+
.map((r, i) => `${formatResult(r, i)}\nSTALE — changed since stored: ${r.staleFiles.join(", ")}`)
|
|
1201
|
+
.join("\n\n---\n\n");
|
|
1202
|
+
parts.push(`${stale.length} more ${stale.length === 1 ? "memory is" : "memories are"} similar but STALE (underlying file(s) changed) — do not reuse verbatim, re-verify against current file state:\n\n${staleText}`);
|
|
1203
|
+
}
|
|
1204
|
+
return { content: [{ type: "text", text: parts.join("\n\n===\n\n") }] };
|
|
1161
1205
|
}
|
|
1162
1206
|
catch (e) {
|
|
1163
1207
|
(0, utils_1.logError)("search_memory", e);
|
|
@@ -1168,11 +1212,12 @@ async function handleStoreMemory(args) {
|
|
|
1168
1212
|
const query = args?.query;
|
|
1169
1213
|
const responseText = args?.response;
|
|
1170
1214
|
const provider = args?.provider || "generic";
|
|
1215
|
+
const filePaths = Array.isArray(args?.filePaths) ? args.filePaths : undefined;
|
|
1171
1216
|
if (!query || !responseText)
|
|
1172
1217
|
throw new Error("Query and response are required");
|
|
1173
1218
|
try {
|
|
1174
1219
|
const brain = (0, TheBrainV2_1.getBrain)();
|
|
1175
|
-
const storeRes = brain.store(query, responseText, provider);
|
|
1220
|
+
const storeRes = brain.store(query, responseText, provider, 0.92, filePaths);
|
|
1176
1221
|
const tokensSaved = Math.max(100, Math.floor(responseText.length / 4));
|
|
1177
1222
|
(0, reportSavings_1.reportSavings)({
|
|
1178
1223
|
source: "cache",
|
|
@@ -1180,7 +1225,8 @@ async function handleStoreMemory(args) {
|
|
|
1180
1225
|
toolName: "store_memory",
|
|
1181
1226
|
query: query.substring(0, 100),
|
|
1182
1227
|
});
|
|
1183
|
-
|
|
1228
|
+
const trackingNote = filePaths && filePaths.length > 0 ? ` Tracking freshness against ${filePaths.length} file(s) — this memory auto-invalidates if they change.` : "";
|
|
1229
|
+
return { content: [{ type: "text", text: `Success: Memory stored. ${storeRes.reason}${trackingNote}` }] };
|
|
1184
1230
|
}
|
|
1185
1231
|
catch (e) {
|
|
1186
1232
|
(0, utils_1.logError)("store_memory", e);
|
|
@@ -1225,6 +1271,7 @@ async function handleAutoHeal(args) {
|
|
|
1225
1271
|
async function handleReadWorkspaceFile(args) {
|
|
1226
1272
|
const filePath = args?.filePath;
|
|
1227
1273
|
const compact = args?.compact !== false;
|
|
1274
|
+
const force = !!args?.force;
|
|
1228
1275
|
if (!filePath)
|
|
1229
1276
|
throw new Error("filePath is required");
|
|
1230
1277
|
const workspaceRoot = process.cwd();
|
|
@@ -1232,6 +1279,24 @@ async function handleReadWorkspaceFile(args) {
|
|
|
1232
1279
|
const { resolved } = (0, utils_1.safeResolvePath)(workspaceRoot, filePath);
|
|
1233
1280
|
let content = fs_1.default.readFileSync(resolved, "utf8");
|
|
1234
1281
|
const originalSize = content.length;
|
|
1282
|
+
const dedup = (0, ReadWorkspaceCache_1.checkAlreadySent)(resolved, compact, content);
|
|
1283
|
+
if (!force && dedup.unchanged) {
|
|
1284
|
+
(0, reportSavings_1.reportSavings)({
|
|
1285
|
+
source: "contextSqueeze",
|
|
1286
|
+
charsBefore: originalSize,
|
|
1287
|
+
charsAfter: 0,
|
|
1288
|
+
toolName: "read_workspace_file",
|
|
1289
|
+
filePath,
|
|
1290
|
+
});
|
|
1291
|
+
return {
|
|
1292
|
+
content: [
|
|
1293
|
+
{
|
|
1294
|
+
type: "text",
|
|
1295
|
+
text: `UNCHANGED: ${filePath} is identical to what you already read this session (hash ${dedup.hash.slice(0, 12)}) — reuse your existing copy. Pass force:true to re-fetch anyway.`,
|
|
1296
|
+
},
|
|
1297
|
+
],
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1235
1300
|
if (compact) {
|
|
1236
1301
|
content = (0, ContextSqueezer_1.squeezeCode)(content);
|
|
1237
1302
|
if (content.length < originalSize) {
|
|
@@ -1252,6 +1317,7 @@ async function handleReadWorkspaceFile(args) {
|
|
|
1252
1317
|
catch (err) {
|
|
1253
1318
|
(0, utils_1.logWarn)("read_workspace_file", "SemanticScrubber failed, returning raw content");
|
|
1254
1319
|
}
|
|
1320
|
+
(0, ReadWorkspaceCache_1.recordSent)(resolved, compact, dedup.hash);
|
|
1255
1321
|
return { content: [{ type: "text", text: content }] };
|
|
1256
1322
|
}
|
|
1257
1323
|
catch (err) {
|
|
@@ -1318,30 +1384,44 @@ async function handleApplyWorkspacePatch(args) {
|
|
|
1318
1384
|
return { content: [{ type: "text", text: `Error: File ${filePath} does not exist.` }] };
|
|
1319
1385
|
}
|
|
1320
1386
|
const originalContent = fs_1.default.readFileSync(resolved, "utf8");
|
|
1321
|
-
const
|
|
1322
|
-
if (
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1387
|
+
const matchResult = (0, PatchMatcher_1.findMatch)(originalContent, searchContent);
|
|
1388
|
+
if (!Array.isArray(matchResult)) {
|
|
1389
|
+
if (matchResult.reason === "ambiguous") {
|
|
1390
|
+
return {
|
|
1391
|
+
content: [
|
|
1392
|
+
{
|
|
1393
|
+
type: "text",
|
|
1394
|
+
text: `Error: Multiple matches found for the search content. Please provide a larger search block with unique surrounding context lines to make it unambiguous.`,
|
|
1395
|
+
},
|
|
1396
|
+
],
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
if (matchResult.candidateDiff) {
|
|
1400
|
+
return {
|
|
1401
|
+
content: [
|
|
1402
|
+
{
|
|
1403
|
+
type: "text",
|
|
1404
|
+
text: `Error: No match found for the search content. Closest candidate starts at line ${matchResult.candidateStartLine} (- = your searchContent, + = actual file content):\n\n${matchResult.candidateDiff}`,
|
|
1405
|
+
},
|
|
1406
|
+
],
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1333
1409
|
return {
|
|
1334
1410
|
content: [
|
|
1335
1411
|
{
|
|
1336
1412
|
type: "text",
|
|
1337
|
-
text: `Error:
|
|
1413
|
+
text: `Error: Target search content not found in file, and no similar block was found to diff against.`,
|
|
1338
1414
|
},
|
|
1339
1415
|
],
|
|
1340
1416
|
};
|
|
1341
1417
|
}
|
|
1342
|
-
const
|
|
1343
|
-
|
|
1344
|
-
|
|
1418
|
+
const match = matchResult[0];
|
|
1419
|
+
const contentLines = originalContent.split("\n");
|
|
1420
|
+
const finalReplace = match.strategy === "exact" ? replaceContent : (0, PatchMatcher_1.reindentReplacement)(replaceContent, searchContent, contentLines[match.startLine]);
|
|
1421
|
+
const updatedLines = [...contentLines.slice(0, match.startLine), ...finalReplace.split("\n"), ...contentLines.slice(match.endLine + 1)];
|
|
1422
|
+
fs_1.default.writeFileSync(resolved, updatedLines.join("\n"), "utf8");
|
|
1423
|
+
const note = match.strategy === "exact" ? "" : ` (matched via ${match.strategy}, score ${match.score.toFixed(2)})`;
|
|
1424
|
+
return { content: [{ type: "text", text: `Success: Patch successfully applied to ${filePath}${note}` }] };
|
|
1345
1425
|
}
|
|
1346
1426
|
catch (err) {
|
|
1347
1427
|
(0, utils_1.logError)("apply_workspace_patch", err);
|
|
@@ -1377,25 +1457,95 @@ async function handleRunWorkspaceCommand(args) {
|
|
|
1377
1457
|
// ── End allowlist check ────────────────────────────────────────────────
|
|
1378
1458
|
const workspaceRoot = process.cwd();
|
|
1379
1459
|
try {
|
|
1380
|
-
|
|
1460
|
+
// spawnSync rather than execSync: execSync leaves stderr attached to the parent unless
|
|
1461
|
+
// stdio is overridden, which silently dropped the output of every tool that reports on
|
|
1462
|
+
// stderr (jest and tsc both do) on a successful run. It also gives us stdout, stderr and
|
|
1463
|
+
// the exit code through one code path instead of a throw-on-failure split.
|
|
1464
|
+
const result = (0, child_process_1.spawnSync)(command, {
|
|
1381
1465
|
cwd: workspaceRoot,
|
|
1466
|
+
shell: true,
|
|
1382
1467
|
encoding: "utf8",
|
|
1383
1468
|
timeout: 15000,
|
|
1469
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
1384
1470
|
env: { ...process.env },
|
|
1385
1471
|
});
|
|
1386
|
-
|
|
1472
|
+
if (result.error) {
|
|
1473
|
+
const timedOut = result.error.code === "ETIMEDOUT";
|
|
1474
|
+
return {
|
|
1475
|
+
content: [
|
|
1476
|
+
{
|
|
1477
|
+
type: "text",
|
|
1478
|
+
text: timedOut
|
|
1479
|
+
? `Command timed out after 15s: ${command}`
|
|
1480
|
+
: `Command could not be executed: ${result.error.message}`,
|
|
1481
|
+
},
|
|
1482
|
+
],
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
const stdout = result.stdout || "";
|
|
1486
|
+
const stderr = result.stderr || "";
|
|
1487
|
+
const combined = [stdout, stderr].filter((s) => s.trim().length > 0).join("\n");
|
|
1488
|
+
const status = result.status ?? 0;
|
|
1489
|
+
const header = status === 0 ? "Command completed successfully." : `Command failed with exit code ${status}.`;
|
|
1490
|
+
if (!combined.trim()) {
|
|
1491
|
+
return { content: [{ type: "text", text: `${header}\n\n(no output)` }] };
|
|
1492
|
+
}
|
|
1493
|
+
const distilled = (0, CommandOutputDistiller_1.distillCommandOutput)(combined, { command, raw: args?.raw === true });
|
|
1494
|
+
if (distilled.handle) {
|
|
1495
|
+
(0, TokenReceipt_1.recordReceiptEvent)("tool_call", `run_workspace_command distilled (${distilled.format})`, {
|
|
1496
|
+
command,
|
|
1497
|
+
originalChars: distilled.originalChars,
|
|
1498
|
+
distilledChars: distilled.distilledChars,
|
|
1499
|
+
handle: distilled.handle,
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
return {
|
|
1503
|
+
content: [
|
|
1504
|
+
{ type: "text", text: `${header}\n\nOutput:\n${distilled.text}${(0, CommandOutputDistiller_1.buildDistillFooter)(distilled)}` },
|
|
1505
|
+
],
|
|
1506
|
+
};
|
|
1387
1507
|
}
|
|
1388
1508
|
catch (err) {
|
|
1389
1509
|
return {
|
|
1390
1510
|
content: [
|
|
1391
1511
|
{
|
|
1392
1512
|
type: "text",
|
|
1393
|
-
text: `Command
|
|
1513
|
+
text: `Command could not be executed: ${err?.message || String(err)}`,
|
|
1394
1514
|
},
|
|
1395
1515
|
],
|
|
1396
1516
|
};
|
|
1397
1517
|
}
|
|
1398
1518
|
}
|
|
1519
|
+
async function handleOutputRegion(args) {
|
|
1520
|
+
const handle = args?.handle;
|
|
1521
|
+
if (!handle)
|
|
1522
|
+
throw new Error("handle is required");
|
|
1523
|
+
const region = (0, CommandOutputDistiller_1.readRegion)({
|
|
1524
|
+
handle,
|
|
1525
|
+
section: args?.section,
|
|
1526
|
+
startLine: args?.startLine,
|
|
1527
|
+
endLine: args?.endLine,
|
|
1528
|
+
all: args?.all === true,
|
|
1529
|
+
});
|
|
1530
|
+
if (!region.found) {
|
|
1531
|
+
const parts = [region.error ?? `Could not read region for handle "${handle}".`];
|
|
1532
|
+
if (region.availableSections?.length) {
|
|
1533
|
+
parts.push("", "Available sections:", ...region.availableSections.map((s) => ` ${s}`));
|
|
1534
|
+
}
|
|
1535
|
+
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
1536
|
+
}
|
|
1537
|
+
const scope = region.startLine === 1 && region.endLine === region.totalLines
|
|
1538
|
+
? `complete output (${region.totalLines} lines)`
|
|
1539
|
+
: `lines ${region.startLine}-${region.endLine} of ${region.totalLines}`;
|
|
1540
|
+
return {
|
|
1541
|
+
content: [
|
|
1542
|
+
{
|
|
1543
|
+
type: "text",
|
|
1544
|
+
text: `Raw output for \`${region.command ?? "?"}\` — ${scope}, verbatim:\n\n${region.text}`,
|
|
1545
|
+
},
|
|
1546
|
+
],
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1399
1549
|
function listDirRecursive(currentDir, relativePath, depth, maxDepth) {
|
|
1400
1550
|
if (depth > maxDepth)
|
|
1401
1551
|
return [];
|
|
@@ -1442,53 +1592,28 @@ async function handleListWorkspaceDir(args) {
|
|
|
1442
1592
|
return { content: [{ type: "text", text: `Error listing directory: ${err.message}` }] };
|
|
1443
1593
|
}
|
|
1444
1594
|
}
|
|
1445
|
-
function searchDirRecursive(currentDir, relativePath, query, extFilter) {
|
|
1446
|
-
try {
|
|
1447
|
-
const entries = fs_1.default.readdirSync(currentDir, { withFileTypes: true });
|
|
1448
|
-
let result = [];
|
|
1449
|
-
for (const entry of entries) {
|
|
1450
|
-
if (["node_modules", ".git", "dist", "chroma_data", ".lemma"].includes(entry.name))
|
|
1451
|
-
continue;
|
|
1452
|
-
const rel = relativePath ? path_1.default.join(relativePath, entry.name) : entry.name;
|
|
1453
|
-
const full = path_1.default.join(currentDir, entry.name);
|
|
1454
|
-
if (entry.isDirectory()) {
|
|
1455
|
-
result.push(...searchDirRecursive(full, rel, query, extFilter));
|
|
1456
|
-
}
|
|
1457
|
-
else {
|
|
1458
|
-
if (extFilter && !entry.name.endsWith(`.${extFilter}`))
|
|
1459
|
-
continue;
|
|
1460
|
-
try {
|
|
1461
|
-
const content = fs_1.default.readFileSync(full, "utf8");
|
|
1462
|
-
if (content.toLowerCase().includes(query.toLowerCase())) {
|
|
1463
|
-
const lines = content.split("\n");
|
|
1464
|
-
lines.forEach((lineText, idx) => {
|
|
1465
|
-
if (lineText.toLowerCase().includes(query.toLowerCase())) {
|
|
1466
|
-
result.push({ filePath: rel, line: idx + 1, text: lineText.trim() });
|
|
1467
|
-
}
|
|
1468
|
-
});
|
|
1469
|
-
}
|
|
1470
|
-
}
|
|
1471
|
-
catch { }
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
return result;
|
|
1475
|
-
}
|
|
1476
|
-
catch {
|
|
1477
|
-
return [];
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
1595
|
async function handleSearchWorkspace(args) {
|
|
1481
1596
|
const query = args?.query;
|
|
1482
1597
|
const extension = args?.extension;
|
|
1598
|
+
const maxResults = args?.maxResults || 200;
|
|
1483
1599
|
if (!query)
|
|
1484
1600
|
throw new Error("query is required");
|
|
1485
1601
|
const workspaceRoot = process.cwd();
|
|
1486
1602
|
try {
|
|
1487
|
-
const
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1603
|
+
const extensions = (0, WorkspaceSearch_1.parseExtensionFilter)(extension);
|
|
1604
|
+
const cached = (0, SearchCache_1.getCachedSearch)(workspaceRoot, query, extensions, maxResults);
|
|
1605
|
+
if (cached.text !== null) {
|
|
1606
|
+
(0, reportSavings_1.reportSavings)({ source: "cache", tokens: Math.floor(cached.text.length / 4), toolName: "search_workspace", query: query.substring(0, 100) });
|
|
1607
|
+
return { content: [{ type: "text", text: cached.text }] };
|
|
1608
|
+
}
|
|
1609
|
+
const { matches, truncated } = (0, WorkspaceSearch_1.searchWorkspace)(workspaceRoot, query, extensions, maxResults);
|
|
1610
|
+
if (matches.length === 0) {
|
|
1611
|
+
const emptyText = `No matches found in the workspace for "${query}".`;
|
|
1612
|
+
(0, SearchCache_1.setCachedSearch)(query, extensions, maxResults, cached.fingerprint, emptyText);
|
|
1613
|
+
return { content: [{ type: "text", text: emptyText }] };
|
|
1614
|
+
}
|
|
1615
|
+
const grouped = (0, WorkspaceSearch_1.groupSearchResults)(matches, truncated, undefined, workspaceRoot);
|
|
1616
|
+
let formatted = grouped.formatted;
|
|
1492
1617
|
try {
|
|
1493
1618
|
const scrubber = new SemanticScrubber_1.SemanticScrubber();
|
|
1494
1619
|
const { maskedPrompt } = scrubber.mask(formatted);
|
|
@@ -1497,8 +1622,16 @@ async function handleSearchWorkspace(args) {
|
|
|
1497
1622
|
catch (err) {
|
|
1498
1623
|
(0, utils_1.logWarn)("search_workspace", "SemanticScrubber failed, returning raw results");
|
|
1499
1624
|
}
|
|
1625
|
+
const truncationNote = grouped.truncated ? ` (truncated at ${maxResults} matches — narrow your query or extension filter for the rest)` : "";
|
|
1626
|
+
const resultText = `Found ${grouped.totalMatches} matches across ${grouped.filesMatched} files${truncationNote}:\n\n${formatted}`;
|
|
1627
|
+
(0, SearchCache_1.setCachedSearch)(query, extensions, maxResults, cached.fingerprint, resultText);
|
|
1500
1628
|
return {
|
|
1501
|
-
content: [
|
|
1629
|
+
content: [
|
|
1630
|
+
{
|
|
1631
|
+
type: "text",
|
|
1632
|
+
text: resultText,
|
|
1633
|
+
},
|
|
1634
|
+
],
|
|
1502
1635
|
};
|
|
1503
1636
|
}
|
|
1504
1637
|
catch (err) {
|