diff --git a/.gitignore b/.gitignore index 81340bf..3fbde00 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .env dist/ -node_modules/ \ No newline at end of file +node_modules/ +.DS_Store \ No newline at end of file diff --git a/commands/security/analyze.toml b/commands/security/analyze.toml index d8f1eab..97053ad 100644 --- a/commands/security/analyze.toml +++ b/commands/security/analyze.toml @@ -1,66 +1,51 @@ -description = "Analyzes code changes on your current branch for common security vulnerabilities and privacy violations." +description = "Analyzes code changes on your current branch for common security vulnerabilities and privacy violations using a diff-based approach" prompt = """You are a highly skilled senior security and privacy analyst. Your primary task is to conduct a security and privacy audit of the current pull request. Utilizing your skillset, you must operate by strictly following the operating principles defined in your context. -## Skillset: Taint Analysis & The Two-Pass Investigation Model +## Skillset: Taint Analysis & The Diff-Based Investigation Model -This is your primary technique for identifying injection-style vulnerabilities (`SQLi`, `XSS`, `Command Injection`, etc.) and other data-flow-related issues. You **MUST** apply this technique within the **Two-Pass "Recon & Investigate" Workflow**. +This is your primary technique for identifying injection-style vulnerabilities (`SQLi`, `XSS`, `Command Injection`, etc.) and other data-flow-related issues. You **MUST** apply this technique within the **Diff-Based Analysis Workflow**. -The core principle is to trace untrusted or sensitive data from its entry point (**Source**) to a location where it is executed, rendered, or stored (**Sink**). A vulnerability exists if the data is not properly sanitized or validated on its path from the Source to the Sink. +The core principle is to trace untrusted data from its entry point (**Source**) to a location where it is executed or rendered (**Sink**). A vulnerability exists if the data is not properly sanitized or validated on its path from the Source to the Sink. -## Core Operational Loop: The Two-Pass "Recon & Investigate" Workflow +## Core Operational Loop: The Diff-Based Two-Pass Workflow -#### Role in the **Reconnaissance Pass** +This workflow builds on the standard Two-Pass model to be diff-centric for maximum efficiency. -Your primary objective during the **"SAST Recon on [file]"** task is to identify and flag **every potential Source of untrusted or sensitive input**. +#### Pass 1: Diff Reconnaissance +Your primary objective during the **Reconnaissance Pass** is to identify and flag potential "taint sources" (untrusted input) by scanning the pull request diff. -* **Action:** Scan the entire file for code that brings external or sensitive data into the application. -* **Trigger:** The moment you identify a `Source`, you **MUST** immediately rewrite the `SECURITY_ANALYSIS_TODO.md` file and add a new, indented sub-task: +* **Action:** Execute the **"Smart Context Retrieval"** procedure mentioned below to efficiently map changed lines to their enclosing function or class context. +* **Trigger:** If a diff chunk contains a `Source`, you **MUST** immediately rewrite the `SECURITY_ANALYSIS_TODO.md` file and add a new, indented sub-task: * `- [ ] Investigate data flow from [variable_name] on line [line_number]`. -* You are not tracing or analyzing the flow yet. You are only planting flags for later investigation. This ensures you scan the entire file and identify all potential starting points before diving deep. ---- +#### Pass 2: Deep Investigation +Your objective during the **Investigation Pass** is to perform the full, deep-dive analysis, but *only* on the specific files and variables identified during the Reconnaissance Pass. -#### Role in the **Investigation Pass** +* **Action:** For each flagged item, trace the variable from its Source to a potential Sink within the full file content. You can read other files if needed. +* **Procedure:** Follow the standard Taint Analysis procedure: trace the data flow through assignments and function calls to verify if it reaches a sensitive execution point without proper sanitization. -Your objective during an **"Investigate data flow from..."** sub-task is to perform the actual trace. - -* **Action:** Start with the variable and line number identified in your task. -* **Procedure:** - 1. Trace this variable through the code. Follow it through function calls, reassignments, and object properties. - 2. Search for a `Sink` where this variable (or a derivative of it) is used. - 3. Analyze the code path between the `Source` and the `Sink`. If there is no evidence of proper sanitization, validation, or escaping, you have confirmed a vulnerability. For PII data, sanitization includes masking or redaction before it reaches a logging or third-party sink. - 4. If a vulnerability is confirmed, append a full finding to your `DRAFT_SECURITY_REPORT.md`. - -For EVERY task, you MUST follow this procedure. This loop separates high-level scanning from deep-dive investigation to ensure full coverage. +For EVERY task, you MUST follow this procedure. 1. **Phase 0: Initial Planning** * **Action:** First, understand the high-level task from the user's prompt. - * **Action:** If it does not already exist, create a new folder named `.gemini_security` in the user's workspace. - * **Action:** Create a new file named `SECURITY_ANALYSIS_TODO.md` in `.gemini_security`, and write the initial, high-level objectives from the prompt into it. - * **Action:** Create a new, empty file named `DRAFT_SECURITY_REPORT.md` in `.gemini_security`. - * **Action"** Prep yourself using the following possible notes files under `.gemini_security/`. If they do not exist, skip them. - * `vuln_allowlist.txt`: The allowlist file has vulnerabilities to ignore during your scan. If you match a vulernability to this file, notify the user and skip it in your scan. + * **Action:** If it does not already exist, create a new folder named `.gemini_security/` in the user's workspace. + * **Action:** Create a new file named `SECURITY_ANALYSIS_TODO.md` in `.gemini_security/`, and write the initial, high-level objectives from the prompt into it. + * **Action:** Create a new, empty file named `DRAFT_SECURITY_REPORT.md` in `.gemini_security/`. + * **Action:** Prep yourself using the following possible notes files under `.gemini_security/`. If they do not exist, skip them. + * `vuln_allowlist.txt`: The allowlist file has vulnerabilities to ignore during your scan. If you match a vulnerability to this file, notify the user and skip it in your scan. 2. **Phase 1: Dynamic Execution & Planning** - * **Action:** Read the `SECURITY_ANALYSIS_TODO.md` file and execute the first task about determinig the scope of the analysis. - * **Action (Plan Refinement):** After identifying the scope, rewrite `SECURITY_ANALYSIS_TODO.md` to replace the generic "analyze files" task with a specific **Reconnaissance Task** for each file (e.g., `- [ ] SAST Recon on fileA.js`). - -3. **Phase 2: The Two-Pass Analysis Loop** - * This is the core execution loop for analyzing a single file. - * **Step A: Reconnaissance Pass** - * When executing a **"SAST Recon on [file]"** task, your goal is to perform a fast but complete scan of the entire file against your SAST Skillset. - * **DO NOT** perform deep investigations during this pass. - * If you identify a suspicious pattern that requires a deeper look (e.g., a source-to-sink flow), you **MUST immediately rewrite `SECURITY_ANALYSIS_TODO.md`** to **add a new, indented "Investigate" sub-task** below the current Recon task. - * Continue the Recon scan of the rest of the file until you reach the end. You may add multiple "Investigate" sub-tasks during a single Recon pass. - * Once the Recon pass for the file is complete, mark the Recon task as done (`[x]`). - * **Step B: Investigation Pass** - * The workflow will now naturally move to the first "Investigate" sub-task you created. - * Execute each investigation sub-task, performing the deep-dive analysis (e.g., tracing the variable, checking for sanitization). - * If an investigation confirms a vulnerability, **append the finding to `DRAFT_SECURITY_REPORT.md`**. - * Mark the investigation sub-task as done (`[x]`). - * **Action:** Repeat this Recon -> Investigate loop until all tasks and sub-tasks are complete. + * **Action:** Read the `SECURITY_ANALYSIS_TODO.md` file and execute the first task about determining the scope of the analysis (getting the diff). + * **Action (Plan Refinement):** After identifying the scope, analyze the diff for taint sources. Rewrite `SECURITY_ANALYSIS_TODO.md` to replace the generic "analyze files" task with specific **Investigation Tasks** for each file that contains a potential taint source (e.g., `- [ ] Investigate data flow from [variable] in fileA.js`). + +3. **Phase 2: The Investigation Loop** + * This is the core execution loop for analyzing the identified files. + * Execute each investigation task, performing the deep-dive analysis (e.g., tracing the variable, checking for sanitization). + * If an investigation confirms a vulnerability, **append the finding to `DRAFT_SECURITY_REPORT.md`**. + * Mark the investigation task as done (`[x]`). + * **Action:** Repeat this loop until all investigation tasks are complete. 4. **Phase 3: Final Review & Refinement** * **Action:** This phase begins when all analysis tasks in `SECURITY_ANALYSIS_TODO.md` are complete. @@ -72,26 +57,20 @@ For EVERY task, you MUST follow this procedure. This loop separates high-level s 5. **Phase 4: Final Reporting & Cleanup** * **Action:** Output the final, reviewed report as your response to the user. * **Action:** If, after the review, no vulnerabilities remain, your final output **MUST** be the standard "clean report" message specified by the task prompt. - * **Action:** Remove the temporary files (`SECURITY_ANALYSIS_TODO.md` and `DRAFT_SECURITY_REPORT.md`) from the `.gemini_security/` directory. Only remove these files and do not remove any other user files under any circumstances. - + * **Action:** Remove the temporary files (`SECURITY_ANALYSIS_TODO.md` and `DRAFT_SECURITY_REPORT.md`) from the `.gemini_security/` directory unless instructed otherwise. ### Example of the Workflow in `SECURITY_ANALYSIS_TODO.md` 1. **Initial State:** ```markdown - - [ ] SAST Recon on `userController.js`. + - [ ] Define the audit scope, analyze diff for taint sources and create investigation plan. ``` -2. **During Recon Pass:** The model finds `const userId = req.query.id;` on line 15. It immediately rewrites the `SECURITY_ANALYSIS_TODO.md`: +2. **After Scope Definition (Diff Analysis):** You will get the diff and find `+ const userId = req.query.id;` in `userController.js`. You will then rewrite `SECURITY_ANALYSIS_TODO.md`: ```markdown - - [ ] SAST Recon on `userController.js`. - - [ ] Investigate data flow from `userId` on line 15. + - [x] Define the audit scope, analyze diff for taint sources and create investigation plan. + - [ ] Investigate data flow from `userId` in `userController.js`. ``` -3. The model continues scanning the rest of the file. When the Recon pass is done, it marks the parent task complete: - ```markdown - - [x] SAST Recon on `userController.js`. - - [ ] Investigate data flow from `userId` on line 15. - ``` -4. **Investigation Pass Begins:** The model now executes the sub-task. It traces `userId` and finds it is used on line 32 in `db.run("SELECT * FROM users WHERE id = " + userId);`. It confirms this is an SQL Injection vulnerability, adds the finding to `DRAFT_SECURITY_REPORT.md`, and marks the final task as complete. +3. **Investigation Pass Begins:** You will now execute the sub-task. You will trace `userId` and find it is used in `db.run("SELECT * FROM users WHERE id = " + userId);`. You will confirm this is an SQL Injection vulnerability, add the finding to `DRAFT_SECURITY_REPORT.md`, and mark the task as complete. ## Analysis Instructions @@ -99,24 +78,39 @@ For EVERY task, you MUST follow this procedure. This loop separates high-level s Your first action is to create a `SECURITY_ANALYSIS_TODO.md` file with the following exact, high-level plan. This initial plan is fixed and must not be altered. When writing files always use absolute paths (e.g., `/path/to/file`). -- [ ] Define the audit scope. -- [ ] Conduct a two-pass SAST analysis on all files within scope. +- [ ] Define the audit scope, analyze diff for taint sources and create investigation plan. +- [ ] Conduct deep-dive SAST analysis on identified files. - [ ] Conduct the final review of all findings as per your **Minimizing False Positives** operating principle and generate the final report. **Step 2: Execution Directives** You will now begin executing the plan. The following are your precise instructions to start with. -1. **To complete the 'Define the audit scope' task:** - * Identify if the user specified specific branches to compare (e.g. "compare main and dev"). - * You **MUST** use the `get_audit_scope` tool and nothing else to get a list of changed files to perform a security scan on. Pass the branch names as arguments if the user provided them; otherwise call it with no arguments to scan the current changes. - * After using the tool, provide the user a list of changed files. If the list of files is empty, ask the user to provide files to be scanned. - -2. **Immediately after defining the scope, you must refine your plan:** - * You will rewrite the `SECURITY_ANALYSIS_TODO.md` file. - * Out of Scope Files: Files that are primarily used for managing dependencies like lockfiles (e.g., `package-lock.json`, `package.json` `yarn.lock`, `go.sum`) should be considered out of scope and **must be omitted from the plan entirely**, as they contain no actionable code to review. - * You **MUST** replace the line `- [ ] Conduct a two-pass SAST analysis on all files within scope.` with a specific **"SAST Recon on [file]"** task for each file you discovered in the previous step. - -After completing these two initial tasks, continue executing the dynamically generated plan according to your **Core Operational Loop**. - -Proceed with the Initial Planning Phase now.""" +1. **To complete the 'Define the audit scope, analyze diff for taint sources and create investigation plan' task:** + * First, identify if the user explicitly requested to compare specific branches (e.g., "compare main and dev"). + * You **MUST** use the `get_audit_scope` tool to retrieve the diff for the current changes. Pass the branch names as arguments if the user provided them; otherwise, call it without arguments. + * **If the tool returns a diff:** You will proceed to execute the **"Procedure: Smart Context Retrieval"** (detailed below) to identify the precise function or class context for every changed line. + * **If the tool returns no diff:** You will prompt the user to provide a list of specific files to scan. + + **Procedure: Smart Context Retrieval** + * *Goal:* To efficiently determine the context of changed lines while minimizing expensive tool calls. + * *Methodology:* You will iterate through each added or modified line in the retrieved diff: + 1. First, call the `find_line_numbers` tool to obtain the precise line number for the change. + 2. **Check Cache:** Before making further calls, verify if this line number is already covered by a function or class you have previously retrieved for this file. If it is covered, you **MUST SKIP** to the next line to avoid redundancy. + 3. **Contextual Analysis and Optimization:** If the line is not covered, and if the changed lines within the diff *do not* visibly contain the entire relevant enclosing entity (e.g., the full function or class definition), you **STRICTLY MUST** call `get_enclosing_entity` to retrieve the function details (eg: name, content and line range). This ensures a complete understanding of the surrounding code. However, if the changed lines are small and *do* visibly contain the entire relevant enclosing entity within the diff itself, you may optimize by *skipping* the `get_enclosing_entity` call and use the diff's context directly. Record the determined context (whether from diff or tool) in your local cache for future checks. + * *Fallback Mechanism:* If the primary tools fail to locate the context for any specific block (e.g., due to syntax errors or tool limitations), you **MUST** fall back to using the raw diff hunk itself as the scope for your analysis. + +2. **Immediately after defining the scope, you must refine your plan (Pass 1: Recon):** + * You will rewrite the `SECURITY_ANALYSIS_TODO.md` file to reflect your findings from the scope definition. + * **Analyze Findings:** + * If the scope has valid diffs, identify taint sources in the added/modified lines. + * If the scope has no valid diffs, you will prompt the user to provide a list of specific files to scan. Based on the user's response, you will read the files and identify taint sources in the **full file content**. + * **Create Investigation Tasks:** For each file where the diff introduces a potential taint source, you must add a specific **Investigation Task** to the plan: + * `- [ ] Investigate data flow from [variable] in [file]`. + * **Filter Benign Changes:** You **MUST** ignore files with only benign changes (e.g., documentation updates, lockfile changes, or simple comment fixes) to ensure you focus only on security-relevant code. + * **Out of Scope Files:** Files primarily used for dependency management (e.g., `package-lock.json`, `go.sum`) are strictly out of scope and must be omitted from the plan. + * You **MUST** replace the generic placeholder line `- [ ] Analyze diff for taint sources...` with these specific, targeted investigation tasks. + +After completing these initial tasks, continue executing the dynamically generated plan according to your **Core Operational Loop**. + +Proceed with the Initial Planning Phase now.""" \ No newline at end of file diff --git a/mcp-server/package-lock.json b/mcp-server/package-lock.json index edacc10..4d4f47e 100644 --- a/mcp-server/package-lock.json +++ b/mcp-server/package-lock.json @@ -7,6 +7,11 @@ "name": "gemini-cli-security-mcp-server", "dependencies": { "@modelcontextprotocol/sdk": "^1.24.0", + "tree-sitter": "^0.21.0", + "tree-sitter-go": "^0.21.0", + "tree-sitter-javascript": "^0.21.0", + "tree-sitter-python": "^0.21.0", + "tree-sitter-typescript": "^0.21.0", "zod": "^3.24.2" }, "devDependencies": { @@ -15,6 +20,74 @@ "vitest": "^3.2.4" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", + "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/android-arm/-/android-arm-0.25.11.tgz", + "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", + "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/android-x64/-/android-x64-0.25.11.tgz", + "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/darwin-arm64": { "version": "0.25.11", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", @@ -26,7 +99,364 @@ "license": "MIT", "optional": true, "os": [ - "darwin" + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", + "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", + "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", + "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", + "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", + "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", + "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", + "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", + "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", + "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", + "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", + "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", + "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", + "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", + "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", + "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", + "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", + "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", + "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", + "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", + "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", + "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" ], "engines": { "node": ">=18" @@ -76,6 +506,34 @@ } } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz", + "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz", + "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.52.5", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz", @@ -90,6 +548,272 @@ "darwin" ] }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz", + "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz", + "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz", + "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz", + "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz", + "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz", + "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz", + "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz", + "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz", + "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz", + "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz", + "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz", + "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz", + "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz", + "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz", + "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz", + "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz", + "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz", + "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz", + "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -1074,6 +1798,26 @@ "node": ">= 0.6" } }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1639,6 +2383,99 @@ "node": ">=0.6" } }, + "node_modules/tree-sitter": { + "version": "0.21.1", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/tree-sitter/-/tree-sitter-0.21.1.tgz", + "integrity": "sha512-7dxoA6kYvtgWw80265MyqJlkRl4yawIjO7S5MigytjELkX43fV2WsAXzsNfO7sBpPPCF5Gp0+XzHk0DwLCq3xQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.0.0", + "node-gyp-build": "^4.8.0" + } + }, + "node_modules/tree-sitter-go": { + "version": "0.21.2", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/tree-sitter-go/-/tree-sitter-go-0.21.2.tgz", + "integrity": "sha512-aMFwjsB948nWhURiIxExK8QX29JYKs96P/IfXVvluVMRJZpL04SREHsdOZHYqJr1whkb7zr3/gWHqqvlkczmvw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.1.0", + "node-gyp-build": "^4.8.1" + }, + "peerDependencies": { + "tree-sitter": "^0.21.0" + }, + "peerDependenciesMeta": { + "tree_sitter": { + "optional": true + } + } + }, + "node_modules/tree-sitter-javascript": { + "version": "0.21.4", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/tree-sitter-javascript/-/tree-sitter-javascript-0.21.4.tgz", + "integrity": "sha512-Lrk8yahebwrwc1sWJE9xPcz1OnnqiEV7Dh5fbN6EN3wNAdu9r06HpTqLqDwUUbnG4EB46Sfk+FJFAOldfoKLOw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.0.0", + "node-gyp-build": "^4.8.1" + }, + "peerDependencies": { + "tree-sitter": "^0.21.1" + }, + "peerDependenciesMeta": { + "tree_sitter": { + "optional": true + } + } + }, + "node_modules/tree-sitter-python": { + "version": "0.21.0", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/tree-sitter-python/-/tree-sitter-python-0.21.0.tgz", + "integrity": "sha512-IUKx7JcTVbByUx1iHGFS/QsIjx7pqwTMHL9bl/NGyhyyydbfNrpruo2C7W6V4KZrbkkCOlX8QVrCoGOFW5qecg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^7.1.0", + "node-gyp-build": "^4.8.0" + }, + "peerDependencies": { + "tree-sitter": "^0.21.0" + }, + "peerDependenciesMeta": { + "tree_sitter": { + "optional": true + } + } + }, + "node_modules/tree-sitter-python/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/tree-sitter-typescript": { + "version": "0.21.2", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/tree-sitter-typescript/-/tree-sitter-typescript-0.21.2.tgz", + "integrity": "sha512-/RyNK41ZpkA8PuPZimR6pGLvNR1p0ibRUJwwQn4qAjyyLEIQD/BNlwS3NSxWtGsAWZe9gZ44VK1mWx2+eQVldg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.0.0", + "node-gyp-build": "^4.8.1" + }, + "peerDependencies": { + "tree-sitter": "^0.21.0" + }, + "peerDependenciesMeta": { + "tree_sitter": { + "optional": true + } + } + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", diff --git a/mcp-server/package.json b/mcp-server/package.json index 3369633..355fb33 100644 --- a/mcp-server/package.json +++ b/mcp-server/package.json @@ -17,6 +17,11 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1.24.0", - "zod": "^3.24.2" + "zod": "^3.24.2", + "tree-sitter": "^0.21.0", + "tree-sitter-python": "^0.21.0", + "tree-sitter-javascript": "^0.21.0", + "tree-sitter-go": "^0.21.0", + "tree-sitter-typescript": "^0.21.0" } } \ No newline at end of file diff --git a/mcp-server/src/codemaps/graph_builder.test.ts b/mcp-server/src/codemaps/graph_builder.test.ts new file mode 100644 index 0000000..d8b0404 --- /dev/null +++ b/mcp-server/src/codemaps/graph_builder.test.ts @@ -0,0 +1,115 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { GraphBuilder } from './graph_builder'; +import { GraphService } from './graph_service'; +import { promises as fs } from 'fs'; + +vi.mock('fs', () => ({ + promises: { + readFile: vi.fn(), + }, +})); + +describe('GraphBuilder', () => { + let graphService: GraphService; + let graphBuilder: GraphBuilder; + + beforeEach(() => { + graphService = new GraphService(); + graphBuilder = new GraphBuilder(graphService); + }); + + it('should build a graph for a Python file', async () => { + const filePath = 'test.py'; + const fileContent = ` +def my_function(): + pass + +class MyClass: + def my_method(self): + pass +`; + vi.mocked(fs.readFile).mockResolvedValue(fileContent); + + await graphBuilder.buildGraph(filePath); + + expect(graphService.graph.nodes.has('test.py')).toBe(true); + expect(graphService.graph.nodes.has('test.py:my_function')).toBe(true); + expect(graphService.graph.nodes.has('test.py:MyClass')).toBe(true); + expect(graphService.graph.nodes.has('test.py:MyClass:my_method')).toBe(true); + }); + + it('should build a graph for a JavaScript file', async () => { + const filePath = 'test.js'; + const fileContent = ` +function myFunction() { +} + +class MyClass { + myMethod() { + } +} +`; + vi.mocked(fs.readFile).mockResolvedValue(fileContent); + + await graphBuilder.buildGraph(filePath); + + expect(graphService.graph.nodes.has('test.js')).toBe(true); + expect(graphService.graph.nodes.has('test.js:myFunction')).toBe(true); + expect(graphService.graph.nodes.has('test.js:MyClass')).toBe(true); + expect(graphService.graph.nodes.has('test.js:MyClass:myMethod')).toBe(true); + }); + + it('should build a graph for a TypeScript file', async () => { + const filePath = 'test.ts'; + const fileContent = ` +function myFunction(): void { +} + +class MyClass { + myMethod(): void { + } +} +`; + vi.mocked(fs.readFile).mockResolvedValue(fileContent); + + await graphBuilder.buildGraph(filePath); + + expect(graphService.graph.nodes.has('test.ts')).toBe(true); + expect(graphService.graph.nodes.has('test.ts:myFunction')).toBe(true); + expect(graphService.graph.nodes.has('test.ts:MyClass')).toBe(true); + expect(graphService.graph.nodes.has('test.ts:MyClass:myMethod')).toBe(true); + }); + + it('should build a graph for a Go file', async () => { + const filePath = 'test.go'; + const fileContent = ` +func myFunction() { +} + +type MyStruct struct { +} + +func (s *MyStruct) myMethod() { +} +`; + vi.mocked(fs.readFile).mockResolvedValue(fileContent); + + await graphBuilder.buildGraph(filePath); + + expect(graphService.graph.nodes.has('test.go')).toBe(true); + expect(graphService.graph.nodes.has('test.go:myFunction')).toBe(true); + expect(graphService.graph.nodes.has('test.go:MyStruct')).toBe(true); + expect(graphService.graph.nodes.has('test.go:MyStruct:myMethod')).toBe(true); + }); + + it('should throw an error for an unsupported file extension', async () => { + const filePath = 'test.txt'; + await expect(graphBuilder.buildGraph(filePath)).rejects.toThrow('Unsupported file extension: test.txt'); + }); +}); diff --git a/mcp-server/src/codemaps/graph_builder.ts b/mcp-server/src/codemaps/graph_builder.ts new file mode 100644 index 0000000..b2b91e7 --- /dev/null +++ b/mcp-server/src/codemaps/graph_builder.ts @@ -0,0 +1,87 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import Parser from 'tree-sitter'; +import { GraphService } from './graph_service.js'; +import { GraphNode } from './models.js'; +import { PythonParser } from './parsers/python_parser.js'; +import { JavaScriptParser } from './parsers/javascript_parser.js'; +import { GoParser } from './parsers/go_parser.js'; +import { TypeScriptParser } from './parsers/typescript_parser.js'; +import { LanguageParser } from './parsers/base_parser.js'; +import { promises as fs } from 'fs'; +import Python from 'tree-sitter-python'; +import JavaScript from 'tree-sitter-javascript'; +import Go from 'tree-sitter-go'; +import TypeScript from 'tree-sitter-typescript'; + +export class GraphBuilder { + private parser!: Parser; + private languageParsers: { [key: string]: LanguageParser }; + private languages: { [key: string]: object }; + constructor(private graphService: GraphService) { + this.languageParsers = { + python: new PythonParser(graphService), + javascript: new JavaScriptParser(graphService), + go: new GoParser(graphService), + typescript: new TypeScriptParser(graphService), + }; + this.languages = { + python: Python, + javascript: JavaScript, + go: Go, + typescript: TypeScript.typescript, + }; + } + + public async buildGraph(filePath: string) { + const language = this._getLanguageFromFileExtension(filePath); + const languageMapping = this.languages[language]; + if (!languageMapping) { + throw new Error(`Unsupported language: ${language}`); + } + + this.parser = new Parser(); + this.parser.setLanguage(languageMapping); + + const fileContent = await fs.readFile(filePath, 'utf8'); + const tree = this.parser.parse(fileContent); + const fileNode: GraphNode = { + id: filePath, + type: 'file', + name: filePath, + startLine: 0, + endLine: 0, + documentation: '', + codeSnippet: '', + }; + this.graphService.addNode(fileNode); + const languageParser = this.languageParsers[language]; + this._traverseTree(tree.rootNode, languageParser, filePath, filePath); + return this.graphService.graph; + } + + private _traverseTree(node: Parser.SyntaxNode, languageParser: LanguageParser, filePath: string, scope: string) { + const newScope = languageParser.parse(node, filePath, scope); + for (const child of node.children) { + this._traverseTree(child, languageParser, filePath, newScope); + } + } + + private _getLanguageFromFileExtension(filePath: string): string { + if (filePath.endsWith('.py')) { + return 'python'; + } else if (filePath.endsWith('.js')) { + return 'javascript'; + } else if (filePath.endsWith('.ts') || filePath.endsWith('.tsx')) { + return 'typescript'; + } else if (filePath.endsWith('.go')) { + return 'go'; + } else { + throw new Error(`Unsupported file extension: ${filePath}`); + } + } +} diff --git a/mcp-server/src/codemaps/graph_service.test.ts b/mcp-server/src/codemaps/graph_service.test.ts new file mode 100644 index 0000000..5a6be1d --- /dev/null +++ b/mcp-server/src/codemaps/graph_service.test.ts @@ -0,0 +1,94 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, beforeEach } from 'vitest'; +import { GraphService } from './graph_service'; +import { GraphNode } from './models'; + +describe('GraphService', () => { + let graphService: GraphService; + + beforeEach(() => { + graphService = new GraphService(); + }); + + it('should add a node to the graph', () => { + const node: GraphNode = { + id: 'file.py:my_function', + type: 'function', + name: 'my_function', + startLine: 1, + endLine: 10, + documentation: '', + codeSnippet: '', + }; + graphService.addNode(node); + expect(graphService.graph.nodes.get('file.py:my_function')).toEqual(node); + }); + + it('should add an edge to the graph', () => { + const edge = { + source: 'file.py:my_function', + target: 'file.py:another_function', + type: 'calls', + }; + graphService.addEdge(edge); + expect(graphService.graph.edges.get('file.py:my_function')).toEqual([edge]); + expect(graphService.graph.inEdges.get('file.py:another_function')).toEqual([edge]); + }); + + it('should find the enclosing entity', () => { + const fileNode: GraphNode = { + id: 'file.py', + type: 'file', + name: 'file.py', + startLine: 0, + endLine: 0, + documentation: '', + codeSnippet: '', + }; + const functionNode: GraphNode = { + id: 'file.py:my_function', + type: 'function', + name: 'my_function', + startLine: 1, + endLine: 10, + documentation: '', + codeSnippet: '', + }; + const innerFunctionNode: GraphNode = { + id: 'file.py:my_function:inner', + type: 'function', + name: 'inner', + startLine: 2, + endLine: 5, + documentation: '', + codeSnippet: '', + }; + graphService.addNode(fileNode); + graphService.addNode(functionNode); + graphService.addNode(innerFunctionNode); + + const enclosingEntity = graphService.findEnclosingEntity('file.py', 3); + expect(enclosingEntity).toEqual(innerFunctionNode); + }); + + it('should query a symbol', () => { + const node: GraphNode = { + id: 'file.py:my_function', + type: 'function', + name: 'my_function', + startLine: 1, + endLine: 10, + documentation: '', + codeSnippet: '', + }; + graphService.addNode(node); + + const foundNode = graphService.querySymbol('my_function', 'file.py'); + expect(foundNode).toEqual(node); + }); +}); diff --git a/mcp-server/src/codemaps/graph_service.ts b/mcp-server/src/codemaps/graph_service.ts new file mode 100644 index 0000000..76a9a09 --- /dev/null +++ b/mcp-server/src/codemaps/graph_service.ts @@ -0,0 +1,160 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { GraphNode, GraphEdge } from './models.js'; +import { promises as fs } from 'fs'; +import path from 'path'; + +export class GraphService { + public graph: { + nodes: Map; + edges: Map; // Adjacency list for outgoing edges + inEdges: Map; // For incoming edges + }; + private _byName: Map>; + private _byFileAndName: Map; + public _pendingCalls: [string, string, string][]; + + constructor() { + this.graph = { + nodes: new Map(), + edges: new Map(), + inEdges: new Map(), + }; + this._byName = new Map(); + this._byFileAndName = new Map(); + this._pendingCalls = []; + } + + private _indexNode(nodeId: string, nodeData: GraphNode) { + const name = nodeData.name; + if (name) { + if (!this._byName.has(name)) { + this._byName.set(name, new Set()); + } + this._byName.get(name)!.add(nodeId); + + const filePath = nodeId.split(':', 1)[0]; + this._byFileAndName.set(`${filePath}:${name}`, nodeId); + } + } + + public addNode(node: GraphNode) { + this.graph.nodes.set(node.id, node); + this._indexNode(node.id, node); + } + + public addEdge(edge: GraphEdge) { + if (!this.graph.edges.has(edge.source)) { + this.graph.edges.set(edge.source, []); + } + this.graph.edges.get(edge.source)!.push(edge); + + if (!this.graph.inEdges.has(edge.target)) { + this.graph.inEdges.set(edge.target, []); + } + this.graph.inEdges.get(edge.target)!.push(edge); + } + + public findEnclosingEntity(filePath: string, lineNumber: number): GraphNode | null { + const enclosingNodes: GraphNode[] = []; + for (const node of this.graph.nodes.values()) { + if (node.id.startsWith(filePath) && node.type !== 'file') { + if (node.startLine <= lineNumber && node.endLine >= lineNumber) { + enclosingNodes.push(node); + } + } + } + + if (enclosingNodes.length === 0) { + return null; + } + + // Find the most specific entity (smallest line range) + return enclosingNodes.reduce((mostSpecific, current) => { + const specificRange = mostSpecific.endLine - mostSpecific.startLine; + const currentRange = current.endLine - current.startLine; + return currentRange < specificRange ? current : mostSpecific; + }); + } + + public querySymbol(name: string, filePath?: string): GraphNode | null { + if (filePath) { + const key = `${filePath}:${name}`; + if (this._byFileAndName.has(key)) { + const nodeId = this._byFileAndName.get(key); + if (nodeId) { + return this.graph.nodes.get(nodeId) || null; + } + } + } + const ids = this._byName.get(name); + if (ids && ids.size === 1) { + const nodeId = ids.values().next().value; + if (nodeId) { + return this.graph.nodes.get(nodeId) || null; + } + } + // Ambiguous or not found + return null; + } + + public ensureModuleNode(moduleName: string): string { + const nodeId = `module:${moduleName}`; + if (!this.graph.nodes.has(nodeId)) { + const node: GraphNode = { + id: nodeId, + type: 'module', + name: moduleName, + startLine: 0, + endLine: 0, + documentation: '', + codeSnippet: '', + }; + this.addNode(node); + } + return nodeId; + } + + public addPendingCall(filePath: string, sourceId: string, calleeName: string) { + this._pendingCalls.push([filePath, sourceId, calleeName]); + } + + public async saveGraph(outputDir: string) { + const filePath = path.join(outputDir, 'codemap.json'); + const graphJson = { + nodes: Array.from(this.graph.nodes.values()), + edges: Array.from(this.graph.edges.values()).flat(), + }; + await fs.mkdir(outputDir, { recursive: true }); + await fs.writeFile(filePath, JSON.stringify(graphJson, null, 2)); + } + public async loadGraph(outputDir: string): Promise { + const filePath = path.join(outputDir, 'codemap.json'); + try { + const data = await fs.readFile(filePath, 'utf8'); + const graphJson = JSON.parse(data); + + this.graph.nodes.clear(); + this.graph.edges.clear(); + this.graph.inEdges.clear(); + this._byName.clear(); + this._byFileAndName.clear(); + this._pendingCalls = []; + + for (const node of graphJson.nodes) { + this.addNode(node); + } + + for (const edge of graphJson.edges) { + this.addEdge(edge); + } + return true; + } catch (error) { + return false; + } + } +} diff --git a/mcp-server/src/codemaps/index.ts b/mcp-server/src/codemaps/index.ts new file mode 100644 index 0000000..fa00e46 --- /dev/null +++ b/mcp-server/src/codemaps/index.ts @@ -0,0 +1,9 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './models.js'; +export * from './graph_service.js'; +export * from './graph_builder.js'; diff --git a/mcp-server/src/codemaps/models.ts b/mcp-server/src/codemaps/models.ts new file mode 100644 index 0000000..2afff19 --- /dev/null +++ b/mcp-server/src/codemaps/models.ts @@ -0,0 +1,21 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +export interface GraphNode { + id: string; + type: string; + name: string; + startLine: number; + endLine: number; + documentation: string; + codeSnippet: string; +} + +export interface GraphEdge { + source: string; + target: string; + type: string; +} diff --git a/mcp-server/src/codemaps/parsers/base_parser.ts b/mcp-server/src/codemaps/parsers/base_parser.ts new file mode 100644 index 0000000..e81521c --- /dev/null +++ b/mcp-server/src/codemaps/parsers/base_parser.ts @@ -0,0 +1,13 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SyntaxNode } from 'tree-sitter'; +import { GraphService } from '../graph_service.js'; + +export interface LanguageParser { + graphService: GraphService; + parse(node: SyntaxNode, filePath: string, scope: string): string; +} diff --git a/mcp-server/src/codemaps/parsers/go_parser.ts b/mcp-server/src/codemaps/parsers/go_parser.ts new file mode 100644 index 0000000..47630a7 --- /dev/null +++ b/mcp-server/src/codemaps/parsers/go_parser.ts @@ -0,0 +1,271 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SyntaxNode } from 'tree-sitter'; +import { GraphService } from '../graph_service.js'; +import { GraphNode, GraphEdge } from '../models.js'; +import { LanguageParser } from './base_parser.js'; + +export class GoParser implements LanguageParser { + constructor(public graphService: GraphService) {} + + parse(node: SyntaxNode, filePath: string, scope: string): string { + let newScope = scope; + + if (node.type === 'function_declaration') { + const nameNode = node.childForFieldName('name'); + if (nameNode) { + const name = nameNode.text; + const startLine = node.startPosition.row + 1; + const endLine = node.endPosition.row + 1; + const documentation = this._getDocstring(node); + const codeSnippet = node.text; + const nodeId = `${scope}:${name}`; + + this.graphService.addNode({ + id: nodeId, + type: 'function', + name, + startLine, + endLine, + documentation, + codeSnippet, + }); + newScope = nodeId; + if (scope) { + this.graphService.addEdge({ source: scope, target: nodeId, type: 'contains' }); + } + } + } else if (node.type === 'type_declaration') { + for (const spec of this._findAll(node, 'type_spec')) { + this._handleTypeSpec(spec, filePath, scope); + } + } else if (node.type === 'method_declaration') { + const receiverNode = node.childForFieldName('receiver'); + const nameNode = node.childForFieldName('name'); + if (receiverNode && nameNode) { + const receiverType = this._getReceiverType(receiverNode); + const methodName = nameNode.text; + if (receiverType) { + const parentNode = this.graphService.querySymbol(receiverType, filePath); + if (parentNode) { + const startLine = node.startPosition.row + 1; + const endLine = node.endPosition.row + 1; + const documentation = this._getDocstring(node); + const codeSnippet = node.text; + const nodeId = `${parentNode.id}:${methodName}`; + + this.graphService.addNode({ + id: nodeId, + type: 'function', + name: methodName, + startLine, + endLine, + documentation, + codeSnippet, + }); + newScope = nodeId; + this.graphService.addEdge({ source: parentNode.id, target: nodeId, type: 'contains' }); + } + } + } + } else if (node.type === 'call_expression') { + const callee = this._extractCalleeName(node); + if (callee && scope) { + const calleeNode = this.graphService.querySymbol(callee); + if (calleeNode) { + this.graphService.addEdge({ source: scope, target: calleeNode.id, type: 'calls' }); + } else { + this.graphService.addPendingCall(filePath, scope, callee); + } + } + } else if (node.type === 'import_declaration') { + for (const spec of this._findAll(node, 'import_spec')) { + const pathNode = spec.childForFieldName('path'); + let moduleName: string | null = null; + if (pathNode) { + moduleName = this._stripQuotes(pathNode.text); + } else { + const lit = this._firstStringLiteral(spec); + if (lit) { + moduleName = this._stripQuotes(lit); + } + } + + if (moduleName) { + const targetId = this.graphService.ensureModuleNode(moduleName); + this.graphService.addEdge({ source: filePath, target: targetId, type: 'imports' }); + } + } + } + + return newScope; + } + + private _handleTypeSpec(specNode: SyntaxNode, filePath: string, scope: string) { + const nameNode = specNode.childForFieldName('name'); + const typeNode = specNode.childForFieldName('type'); + if (!nameNode || !typeNode) { + return; + } + + if (typeNode.type !== 'struct_type') { + return; + } + + const name = nameNode.text; + const startLine = specNode.startPosition.row + 1; + const endLine = specNode.endPosition.row + 1; + const codeSnippet = specNode.text; + const nodeId = `${scope}:${name}`; + + this.graphService.addNode({ + id: nodeId, + type: 'struct', + name, + startLine, + endLine, + documentation: '', + codeSnippet, + }); + this.graphService.addEdge({ source: filePath, target: nodeId, type: 'contains' }); + + for (const fieldDecl of this._findAll(typeNode, 'field_declaration')) { + if (this._hasChildType(fieldDecl, 'field_identifier')) { + continue; + } + + const typeChild = fieldDecl.childForFieldName('type'); + let parentName: string | null = null; + if (!typeChild) { + parentName = this._rightmostIdentifier(fieldDecl); + } else { + parentName = this._rightmostIdentifier(typeChild); + } + + if (!parentName) { + continue; + } + + const parentNode = this.graphService.querySymbol(parentName, filePath); if (parentNode) { + this.graphService.addEdge({ source: nodeId, target: parentNode.id, type: 'inherits' }); + } + } + } + + private _extractCalleeName(callNode: SyntaxNode): string | null { + if (callNode.type !== 'call_expression') { + return null; + } + const fn = callNode.childForFieldName('function'); + if (!fn) { + return null; + } + + if (fn.type === 'identifier') { + return fn.text; + } + + if (fn.type === 'selector_expression') { + const field = fn.childForFieldName('field'); + if (field) { + return field.text; + } + return this._rightmostIdentifier(fn); + } + + return null; + } + + private _rightmostIdentifier(node: SyntaxNode): string | null { + if ( + node.type === 'identifier' || + node.type === 'type_identifier' || + node.type === 'field_identifier' + ) { + return node.text; + } + + if (node.type === 'selector_expression') { + const fld = node.childForFieldName('field'); + if (fld) { + return fld.text; + } + } + + if (node.namedChildCount > 0) { + for (let i = node.namedChildCount - 1; i >= 0; i--) { + const name = this._rightmostIdentifier(node.namedChildren[i]); + if (name) { + return name; + } + } + } + return null; + } + + private _findAll(node: SyntaxNode, typeName: string): SyntaxNode[] { + const out: SyntaxNode[] = []; + const stack: SyntaxNode[] = [node]; + while (stack.length > 0) { + const cur = stack.pop()!; + for (const ch of cur.children) { + if (ch.type === typeName) { + out.push(ch); + } + stack.push(ch); + } + } + return out; + } + + private _hasChildType(node: SyntaxNode, typeName: string): boolean { + return node.children.some((ch) => ch.type === typeName); + } + + private _firstStringLiteral(node: SyntaxNode): string | null { + const stack: SyntaxNode[] = [node]; + while (stack.length > 0) { + const cur = stack.pop()!; + for (const ch of cur.children) { + if ( + ch.type === 'interpreted_string_literal' || + ch.type === 'raw_string_literal' || + ch.type === 'string_literal' + ) { + return ch.text; + } + stack.push(ch); + } + } + return null; + } + + private _stripQuotes(s: string): string { + return s.length >= 2 && (s.startsWith("'") || s.startsWith('"')) ? s.slice(1, -1) : s; + } + + private _getDocstring(node: SyntaxNode): string { + const prev = node.previousSibling; + if (prev && prev.type === 'comment') { + return prev.text; + } + return ''; + } + + private _getReceiverType(node: SyntaxNode): string | null { + if (node.type === 'type_identifier') { + return node.text; + } + for (const child of node.children) { + const found = this._getReceiverType(child); + if (found) { + return found; + } + } + return null; + } +} diff --git a/mcp-server/src/codemaps/parsers/javascript_parser.ts b/mcp-server/src/codemaps/parsers/javascript_parser.ts new file mode 100644 index 0000000..5e7c692 --- /dev/null +++ b/mcp-server/src/codemaps/parsers/javascript_parser.ts @@ -0,0 +1,296 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SyntaxNode } from 'tree-sitter'; +import { GraphService } from '../graph_service.js'; +import { GraphNode, GraphEdge } from '../models.js'; +import { LanguageParser } from './base_parser.js'; + +export class JavaScriptParser implements LanguageParser { + constructor(public graphService: GraphService) {} + + parse(node: SyntaxNode, filePath: string, scope: string): string { + let newScope = scope; + + if (node.type === 'function_declaration') { + const nameNode = node.childForFieldName('name'); + if (nameNode) { + const name = nameNode.text; + const startLine = node.startPosition.row + 1; + const endLine = node.endPosition.row + 1; + const documentation = this._getDocstring(node); + const codeSnippet = node.text; + const nodeId = `${scope}:${name}`; + + this.graphService.addNode({ + id: nodeId, + type: 'function', + name, + startLine, + endLine, + documentation, + codeSnippet, + }); + newScope = nodeId; + + if (scope) { + this.graphService.addEdge({ source: scope, target: nodeId, type: 'contains' }); + } + } + } else if (node.type === 'class_declaration') { + const nameNode = node.childForFieldName('name'); + if (nameNode) { + const name = nameNode.text; + const startLine = node.startPosition.row + 1; + const endLine = node.endPosition.row + 1; + const documentation = this._getDocstring(node); + const codeSnippet = node.text; + const nodeId = `${scope}:${name}`; + + this.graphService.addNode({ + id: nodeId, + type: 'class', + name, + startLine, + endLine, + documentation, + codeSnippet, + }); + newScope = nodeId; + if (scope) { + this.graphService.addEdge({ source: scope, target: nodeId, type: 'contains' }); + } + + const parentName = this._getParentJavaScriptClassDeclaration(node); + if (parentName) { + const parentNode = this.graphService.querySymbol(parentName); + if (parentNode) { + this.graphService.addEdge({ source: nodeId, target: parentNode.id, type: 'inherits' }); + } + } + } + } else if (node.type === 'method_definition') { + const nameNode = node.children.find( + (child) => child.type === 'property_identifier' || child.type === 'private_property_identifier' + ); + if (nameNode) { + const methodName = nameNode.text; + const startLine = node.startPosition.row + 1; + const endLine = node.endPosition.row + 1; + const documentation = this._getDocstring(node); + const codeSnippet = node.text; + const nodeId = `${scope}:${methodName}`; + + this.graphService.addNode({ + id: nodeId, + type: 'function', + name: methodName, + startLine, + endLine, + documentation, + codeSnippet, + }); + + if (scope) { + this.graphService.addEdge({ source: scope, target: nodeId, type: 'contains' }); + } + + newScope = nodeId; + } + } else if (node.type === 'call_expression') { + if (this._maybeAddCommonJsImport(node, filePath)) { + return newScope; + } + + const calleeName = this._getCalleeName(node); + if (calleeName && scope) { + const calleeNode = this.graphService.querySymbol(calleeName); + if (calleeNode) { + this.graphService.addEdge({ source: scope, target: calleeNode.id, type: 'calls' }); + } else { + this.graphService.addPendingCall(filePath, scope, calleeName); + } + } + } else if (node.type === 'variable_declarator') { + const nameNode = node.childForFieldName('name'); + const valueNode = node.childForFieldName('value'); + + if (nameNode && nameNode.type === 'identifier') { + const varName = nameNode.text; + const startLine = node.startPosition.row + 1; + const endLine = node.endPosition.row + 1; + const codeSnippet = node.text; + const varId = `${scope}:${varName}`; + this.graphService.addNode({ + id: varId, + type: 'variable', + name: varName, + startLine, + endLine, + documentation: '', + codeSnippet, + }); + if (scope) { + this.graphService.addEdge({ source: scope, target: varId, type: 'contains' }); + } + } + + if ( + nameNode && + nameNode.type === 'identifier' && + valueNode && + (valueNode.type === 'function_expression' || valueNode.type === 'arrow_function') + ) { + const funcName = nameNode.text; + const startLine = valueNode.startPosition.row + 1; + const endLine = valueNode.endPosition.row + 1; + const documentation = this._getDocstring(node); + const codeSnippet = valueNode.text; + const funcId = `${scope}:${funcName}`; + this.graphService.addNode({ + id: funcId, + type: 'function', + name: funcName, + startLine, + endLine, + documentation, + codeSnippet, + }); + if (scope) { + this.graphService.addEdge({ source: scope, target: funcId, type: 'contains' }); + } + } + } else if (node.type === 'import_statement') { + const sourceNode = node.childForFieldName('source'); + if (sourceNode) { + let moduleName = sourceNode.text; + if (moduleName.length >= 2 && (moduleName.startsWith("'") || moduleName.startsWith('"'))) { + moduleName = moduleName.slice(1, -1); + } + const targetId = this.graphService.ensureModuleNode(moduleName); + this.graphService.addEdge({ source: filePath, target: targetId, type: 'imports' }); + } + } + + return newScope; + } + + private _getDocstring(node: SyntaxNode): string { + let prev = node.previousSibling; + while (prev && prev.type === 'comment') { + if (prev.text.startsWith('/**')) { + return prev.text; + } + prev = prev.previousSibling; + } + return ''; + } + + private _getParentJavaScriptClassDeclaration(node: SyntaxNode): string | null { + const classHeritageNode = node.children.find((child) => child.type === 'class_heritage'); + if (classHeritageNode && classHeritageNode.namedChildCount > 0) { + const base = classHeritageNode.namedChildren[0]; + if (base.type === 'identifier') { + return base.text; + } else if (base.type === 'member_expression') { + const prop = base.childForFieldName('property'); + if (prop) { + return prop.text; + } + } + } + return null; + } + + private _getCalleeName(callNode: SyntaxNode): string | null { + const fn = callNode.childForFieldName('function'); + if (!fn) { + return null; + } + + if (fn.type === 'identifier') { + return fn.text; + } + + if (fn.type === 'member_expression') { + const prop = fn.childForFieldName('property'); + if (prop && prop.type === 'property_identifier') { + return prop.text; + } + if (prop && prop.type === 'identifier') { + return prop.text; + } + return null; + } + + if (fn.type === 'optional_chain') { + let inner = fn; + while (inner && inner.namedChildCount > 0) { + const last = inner.namedChildren[inner.namedChildCount - 1]; + if (last.type === 'member_expression' || last.type === 'identifier') { + inner = last; + break; + } + inner = last; + } + if (inner.type === 'identifier') { + return inner.text; + } + if (inner.type === 'member_expression') { + const prop = inner.childForFieldName('property'); + if (prop && (prop.type === 'property_identifier' || prop.type === 'identifier')) { + return prop.text; + } + } + return null; + } + + return null; + } + + private _maybeAddCommonJsImport(callNode: SyntaxNode, filePath: string): boolean { + if (callNode.type !== 'call_expression') { + return false; + } + + const fn = callNode.childForFieldName('function'); + if (!fn || fn.type !== 'identifier' || fn.text !== 'require') { + return false; + } + + const args = callNode.childForFieldName('arguments'); + if (!args || args.namedChildCount === 0) { + return false; + } + + const first = args.namedChildren[0]; + + if (first.type === 'string') { + let moduleName = first.text; + if (moduleName.length >= 2 && (moduleName.startsWith("'") || moduleName.startsWith('"'))) { + moduleName = moduleName.slice(1, -1); + } + const targetId = this.graphService.ensureModuleNode(moduleName); + this.graphService.addEdge({ source: filePath, target: targetId, type: 'imports' }); + return true; + } + + if (first.type === 'template_string') { + const raw = first.text; + if (!raw.includes('${')) { + let moduleName = raw; + if (moduleName.length >= 2 && moduleName.startsWith('`') && moduleName.endsWith('`')) { + moduleName = moduleName.slice(1, -1); + } + const targetId = this.graphService.ensureModuleNode(moduleName); + this.graphService.addEdge({ source: filePath, target: targetId, type: 'imports' }); + return true; + } + } + + return false; + } +} diff --git a/mcp-server/src/codemaps/parsers/python_parser.ts b/mcp-server/src/codemaps/parsers/python_parser.ts new file mode 100644 index 0000000..904a92c --- /dev/null +++ b/mcp-server/src/codemaps/parsers/python_parser.ts @@ -0,0 +1,153 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SyntaxNode } from 'tree-sitter'; +import { GraphService } from '../graph_service.js'; +import { GraphNode, GraphEdge } from '../models.js'; +import { LanguageParser } from './base_parser.js'; + +export class PythonParser implements LanguageParser { + constructor(public graphService: GraphService) {} + + parse(node: SyntaxNode, filePath: string, scope: string): string { + let newScope = scope; + if (node.type === 'function_definition') { + const nameNode = node.childForFieldName('name'); + if (nameNode) { + const name = nameNode.text; + const startLine = node.startPosition.row + 1; + const endLine = node.endPosition.row + 1; + const documentation = this._getDocstring(node); + const codeSnippet = node.text; + const nodeId = `${scope}:${name}`; + + const newNode: GraphNode = { + id: nodeId, + type: 'function', + name, + startLine, + endLine, + documentation, + codeSnippet, + }; + this.graphService.addNode(newNode); + newScope = nodeId; + if (scope) { + this.graphService.addEdge({ source: scope, target: nodeId, type: 'contains' }); + } + } + } else if (node.type === 'class_definition') { + const nameNode = node.childForFieldName('name'); + if (nameNode) { + const name = nameNode.text; + const startLine = node.startPosition.row + 1; + const endLine = node.endPosition.row + 1; + const documentation = this._getDocstring(node); + const codeSnippet = node.text; + const nodeId = `${scope}:${name}`; + + const newNode: GraphNode = { + id: nodeId, + type: 'class', + name, + startLine, + endLine, + documentation, + codeSnippet, + }; + this.graphService.addNode(newNode); + newScope = nodeId; + if (scope) { + this.graphService.addEdge({ source: scope, target: nodeId, type: 'contains' }); + } + + const superclassesNode = node.childForFieldName('superclasses'); + if (superclassesNode) { + for (const superclass of superclassesNode.children) { + if (superclass.type === 'identifier') { + const parentName = superclass.text; + const parentNode = this.graphService.querySymbol(parentName); + if (parentNode) { + this.graphService.addEdge({ source: nodeId, target: parentNode.id, type: 'inherits' }); + } + } + } + } + } + } else if (node.type === 'call') { + const calleeName = this._pyCalleeName(node); + if (calleeName && scope) { + const calleeNode = this.graphService.querySymbol(calleeName); + if (calleeNode) { + this.graphService.addEdge({ source: scope, target: calleeNode.id, type: 'calls' }); + } else { + this.graphService.addPendingCall(filePath, scope, calleeName); + } + } + } else if (node.type === 'import_statement') { + for (const alias of node.namedChildren) { + if (alias.type === 'dotted_name') { + const moduleName = alias.text; + const targetId = this.graphService.ensureModuleNode(moduleName); + this.graphService.addEdge({ source: filePath, target: targetId, type: 'imports' }); + } + } + } else if (node.type === 'import_from_statement') { + const moduleNameNode = node.childForFieldName('module_name'); + if (moduleNameNode) { + const moduleName = moduleNameNode.text; + const targetId = this.graphService.ensureModuleNode(moduleName); + this.graphService.addEdge({ source: filePath, target: targetId, type: 'imports' }); + } + } + + return newScope; + } + + private _getDocstring(node: SyntaxNode): string { + if (node.type === 'function_definition' || node.type === 'class_definition') { + const body = node.childForFieldName('body'); + if (body && body.namedChildCount > 0) { + const firstChild = body.namedChildren[0]; + if (firstChild.type === 'expression_statement' && firstChild.firstChild?.type === 'string') { + return firstChild.firstChild.text; + } + } + } + return ''; + } + + private _pyCalleeName(callNode: SyntaxNode): string | null { + const fn = callNode.childForFieldName('function'); + if (!fn) { + return null; + } + return this._pyRightmostName(fn); + } + + private _pyRightmostName(node: SyntaxNode): string | null { + if (node.type === 'identifier') { + return node.text; + } + + if (node.type === 'attribute') { + const attr = node.childForFieldName('attribute'); + if (attr) { + return attr.text; + } + } + + if (node.namedChildCount > 0) { + for (let i = node.namedChildCount - 1; i >= 0; i--) { + const name = this._pyRightmostName(node.namedChildren[i]); + if (name) { + return name; + } + } + } + return null; + } +} diff --git a/mcp-server/src/codemaps/parsers/typescript_parser.ts b/mcp-server/src/codemaps/parsers/typescript_parser.ts new file mode 100644 index 0000000..dd3527c --- /dev/null +++ b/mcp-server/src/codemaps/parsers/typescript_parser.ts @@ -0,0 +1,383 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SyntaxNode } from 'tree-sitter'; +import { GraphService } from '../graph_service.js'; +import { GraphNode, GraphEdge } from '../models.js'; +import { LanguageParser } from './base_parser.js'; + +export class TypeScriptParser implements LanguageParser { + constructor(public graphService: GraphService) {} + + parse(node: SyntaxNode, filePath: string, scope: string): string { + let newScope = scope; + + if (node.type === 'function_declaration') { + const nameNode = node.childForFieldName('name'); + if (nameNode) { + const name = nameNode.text; + const startLine = node.startPosition.row + 1; + const endLine = node.endPosition.row + 1; + const documentation = this._getDocstring(node); + const codeSnippet = node.text; + const nodeId = `${scope}:${name}`; + this.graphService.addNode({ + id: nodeId, + type: 'function', + name, + startLine, + endLine, + documentation, + codeSnippet, + }); + newScope = nodeId; + if (scope) { + this.graphService.addEdge({ source: scope, target: nodeId, type: 'contains' }); + } + } + } else if (node.type === 'class_declaration') { + const nameNode = node.childForFieldName('name'); + if (nameNode) { + const name = nameNode.text; + const startLine = node.startPosition.row + 1; + const endLine = node.endPosition.row + 1; + const documentation = this._getDocstring(node); + const codeSnippet = node.text; + const nodeId = `${scope}:${name}`; + + this.graphService.addNode({ + id: nodeId, + type: 'class', + name, + startLine, + endLine, + documentation, + codeSnippet, + }); + newScope = nodeId; + if (scope) { + this.graphService.addEdge({ source: scope, target: nodeId, type: 'contains' }); + } + + const superclass = node.childForFieldName('superclass'); + let baseName: string | null = null; + if (superclass) { + baseName = this._tsRightmostIdentifier(superclass); + } + + if (!baseName) { + for (const ch of node.children) { + if ( + ch.type === 'extends_clause' || + ch.type === 'class_heritage' || + ch.type === 'heritage_clause' + ) { + const cand = this._tsRightmostIdentifier(ch); + if (cand) { + baseName = cand; + break; + } + } + } + } + + if (baseName) { + const parentNode = this.graphService.querySymbol(baseName, filePath); + if (parentNode) { + this.graphService.addEdge({ source: nodeId, target: parentNode.id, type: 'inherits' }); + } + } + + for (const ch of node.children) { + if (ch.type === 'implements_clause') { + for (const t of ch.namedChildren) { + const iface = this._tsRightmostIdentifier(t); + if (iface) { + const ifaceNode = this.graphService.querySymbol(iface, filePath); + if (ifaceNode) { + this.graphService.addEdge({ + source: nodeId, + target: ifaceNode.id, + type: 'implements', + }); + } + } + } + } + } + } + } else if (node.type === 'interface_declaration') { + const nameNode = node.childForFieldName('name'); + if (nameNode) { + const name = nameNode.text; + const startLine = node.startPosition.row + 1; + const endLine = node.endPosition.row + 1; + const documentation = this._getDocstring(node); + const codeSnippet = node.text; + const nodeId = `${scope}:${name}`; + this.graphService.addNode({ + id: nodeId, + type: 'interface', + name, + startLine, + endLine, + documentation, + codeSnippet, + }); + newScope = nodeId; + if (scope) { + this.graphService.addEdge({ source: scope, target: nodeId, type: 'contains' }); + } + } + } else if (node.type === 'enum_declaration') { + const nameNode = node.childForFieldName('name'); + if (nameNode) { + const name = nameNode.text; + const startLine = node.startPosition.row + 1; + const endLine = node.endPosition.row + 1; + const codeSnippet = node.text; + const nodeId = `${scope}:${name}`; + this.graphService.addNode({ + id: nodeId, + type: 'enum', + name, + startLine, + endLine, + documentation: '', + codeSnippet, + }); + if (scope) { + this.graphService.addEdge({ source: scope, target: nodeId, type: 'contains' }); + } + } + } else if (node.type === 'type_alias_declaration') { + const nameNode = node.childForFieldName('name'); + if (nameNode) { + const name = nameNode.text; + const startLine = node.startPosition.row + 1; + const endLine = node.endPosition.row + 1; + const codeSnippet = node.text; + const nodeId = `${scope}:${name}`; + this.graphService.addNode({ + id: nodeId, + type: 'type_alias', + name, + startLine, + endLine, + documentation: '', + codeSnippet, + }); + if (scope) { + this.graphService.addEdge({ source: scope, target: nodeId, type: 'contains' }); + } + } + } else if (node.type === 'method_definition') { + const nameNode = node.children.find( + (ch) => ch.type === 'property_identifier' || ch.type === 'private_property_identifier' + ); + if (nameNode) { + const name = nameNode.text; + const startLine = node.startPosition.row + 1; + const endLine = node.endPosition.row + 1; + const documentation = this._getDocstring(node); + const codeSnippet = node.text; + const nodeId = `${scope}:${name}`; + this.graphService.addNode({ + id: nodeId, + type: 'function', + name, + startLine, + endLine, + documentation, + codeSnippet, + }); + if (scope) { + this.graphService.addEdge({ source: scope, target: nodeId, type: 'contains' }); + } + newScope = nodeId; + } + } else if (node.type === 'method_signature') { + const nameNode = node.children.find( + (ch) => ch.type === 'property_identifier' || ch.type === 'private_property_identifier' + ); + if (nameNode) { + const name = nameNode.text; + const startLine = node.startPosition.row + 1; + const endLine = node.endPosition.row + 1; + const codeSnippet = node.text; + const nodeId = `${scope}:${name}`; + this.graphService.addNode({ + id: nodeId, + type: 'method', + name, + startLine, + endLine, + documentation: '', + codeSnippet, + }); + if (scope) { + this.graphService.addEdge({ source: scope, target: nodeId, type: 'contains' }); + } + } + } else if (node.type === 'variable_declarator') { + const nameNode = node.childForFieldName('name'); + const valueNode = node.childForFieldName('value'); + + if (nameNode && nameNode.type === 'identifier') { + const varName = nameNode.text; + const startLine = node.startPosition.row + 1; + const endLine = node.endPosition.row + 1; + const codeSnippet = node.text; + const varId = `${scope}:${varName}`; + this.graphService.addNode({ + id: varId, + type: 'variable', + name: varName, + startLine, + endLine, + documentation: '', + codeSnippet, + }); + if (scope) { + this.graphService.addEdge({ source: scope, target: varId, type: 'contains' }); + } + } + + if ( + nameNode && + nameNode.type === 'identifier' && + valueNode && + (valueNode.type === 'function_expression' || valueNode.type === 'arrow_function') + ) { + const funcName = nameNode.text; + const startLine = valueNode.startPosition.row + 1; + const endLine = valueNode.endPosition.row + 1; + const documentation = this._getDocstring(node); + const codeSnippet = valueNode.text; + const funcId = `${scope}:${funcName}`; + this.graphService.addNode({ + id: funcId, + type: 'function', + name: funcName, + startLine, + endLine, + documentation, + codeSnippet, + }); + if (scope) { + this.graphService.addEdge({ source: scope, target: funcId, type: 'contains' }); + } + } + } else if (node.type === 'call_expression') { + if (this._maybeAddCommonJsImport(node, filePath)) { + return newScope; + } + + const calleeName = this._getCalleeName(node); + if (calleeName && scope) { + const calleeNode = this.graphService.querySymbol(calleeName, filePath); + if (calleeNode) { + this.graphService.addEdge({ source: scope, target: calleeNode.id, type: 'calls' }); + } else { + this.graphService.addPendingCall(filePath, scope, calleeName); + } + } + } else if (node.type === 'import_statement') { + const sourceNode = node.childForFieldName('source'); + if (sourceNode) { + let moduleName = sourceNode.text; + if (moduleName.length >= 2 && (moduleName.startsWith("'") || moduleName.startsWith('"'))) { + moduleName = moduleName.slice(1, -1); + } + const targetId = this.graphService.ensureModuleNode(moduleName); + this.graphService.addEdge({ source: filePath, target: targetId, type: 'imports' }); + } + } + + return newScope; + } + + private _getDocstring(node: SyntaxNode): string { + const prev = node.previousSibling; + if (prev && prev.type === 'comment' && prev.text.startsWith('/**')) { + return prev.text; + } + return ''; + } + + private _getCalleeName(callNode: SyntaxNode): string | null { + const fn = callNode.childForFieldName('function'); + if (!fn) { + return null; + } + if (fn.type === 'identifier') { + return fn.text; + } + if (fn.type === 'member_expression') { + const prop = fn.childForFieldName('property'); + if (prop) { + return prop.text; + } + return this._tsRightmostIdentifier(fn); + } + return null; + } + + private _tsRightmostIdentifier(node: SyntaxNode): string | null { + if ( + node.type === 'identifier' || + node.type === 'type_identifier' || + node.type === 'property_identifier' || + node.type === 'private_property_identifier' + ) { + return node.text; + } + if (node.type === 'member_expression') { + const prop = node.childForFieldName('property'); + if (prop) { + return prop.text; + } + } + if (node.namedChildCount > 0) { + for (let i = node.namedChildCount - 1; i >= 0; i--) { + const name = this._tsRightmostIdentifier(node.namedChildren[i]); + if (name) { + return name; + } + } + } + return null; + } + + private _maybeAddCommonJsImport(callNode: SyntaxNode, filePath: string): boolean { + const fn = callNode.childForFieldName('function'); + if (!fn || fn.type !== 'identifier' || fn.text !== 'require') { + return false; + } + const args = callNode.childForFieldName('arguments'); + if (!args || args.namedChildCount === 0) { + return false; + } + const first = args.namedChildren[0]; + let mod: string | null = null; + if (first.type === 'string') { + const raw = first.text; + mod = + raw.length >= 2 && (raw.startsWith("'") || raw.startsWith('"')) ? raw.slice(1, -1) : raw; + } else if (first.type === 'template_string') { + const raw = first.text; + if (!raw.includes('${')) { + mod = raw.length >= 2 && raw.startsWith('`') && raw.endsWith('`') ? raw.slice(1, -1) : raw; + } + } + if (mod === null) { + return false; + } + const targetId = this.graphService.ensureModuleNode(mod); + this.graphService.addEdge({ source: filePath, target: targetId, type: 'imports' }); + return true; + } +} diff --git a/mcp-server/src/filesystem.test.ts b/mcp-server/src/filesystem.test.ts index c3cd1e1..0571f12 100644 --- a/mcp-server/src/filesystem.test.ts +++ b/mcp-server/src/filesystem.test.ts @@ -15,6 +15,7 @@ describe('filesystem', () => { fs.writeFileSync('test.txt', 'hello'); execSync('git add .'); execSync('git commit -m "initial commit"'); + execSync('git update-ref refs/remotes/origin/HEAD HEAD'); }); afterAll(() => { @@ -35,10 +36,12 @@ describe('filesystem', () => { }); it('should return a diff of the current changes when no branches or commits are specified', () => { - fs.writeFileSync('test.txt', 'hello world'); - // Defaults to 'git diff' with remote removed + // Modify a file but do not commit it + fs.writeFileSync('test.txt', 'uncommitted change'); const diff = getAuditScope(); - expect(diff).toContain('hello world'); + expect(diff).toContain('diff --git a/test.txt b/test.txt'); + expect(diff).toContain('-hello'); + expect(diff).toContain('+uncommitted change'); }); it('should return a diff between two specific branches', () => { diff --git a/mcp-server/src/index.ts b/mcp-server/src/index.ts index 2ebd816..72f4159 100644 --- a/mcp-server/src/index.ts +++ b/mcp-server/src/index.ts @@ -11,9 +11,10 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { z } from 'zod'; import { promises as fs } from 'fs'; import path from 'path'; +import os from 'os'; import { getAuditScope } from './filesystem.js'; import { findLineNumbers } from './security.js'; - +import { GraphBuilder, GraphService } from './codemaps/index.js'; import { runPoc } from './poc.js'; const server = new McpServer({ @@ -21,6 +22,110 @@ const server = new McpServer({ version: '0.1.0', }); +const SUPPORTED_EXTS = ['.py', '.js', '.ts', 'go']; +const DEFAULT_EXCLUDES = ['.git', 'node_modules', 'dist', 'build', 'venv', '__pycache__']; + +async function scan_dir(dir_path: string, excludes = DEFAULT_EXCLUDES, exts = SUPPORTED_EXTS) { + const files: string[] = []; + + async function scan(currentPath: string) { + const entries = await fs.readdir(currentPath, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(currentPath, entry.name); + if (excludes.includes(entry.name)) { + continue; + } + if (entry.isDirectory()) { + await scan(fullPath); + } else if (exts.some(ext => entry.name.endsWith(ext))) { + files.push(fullPath); + } + } + } + + await scan(dir_path); + return files; +} + +const graphService = new GraphService(); +const graphBuilder = new GraphBuilder(graphService); +let graphBuilt = false; + +server.tool( + 'get_enclosing_entity', + 'Get the nearest enclosing node (function/class) details (name, type, range).', + { + file_path: z.string().describe('The path to the file.'), + line: z.number().describe('The line number.'), + } as any, + async (input: any) => { + + // The first call can be empty, so we guard against it. + if (!input.file_path) { + return { + content: [{ type: 'text', text: 'Invalid argument: file_path is missing.' }], + }; + } + + const { file_path, line } = input as { file_path: string; line: number }; + + // Sanitize and resolve the file path to be absolute + let sanitizedFilePath = file_path.trim(); + if (sanitizedFilePath.startsWith('"') && sanitizedFilePath.endsWith('"')) { + sanitizedFilePath = sanitizedFilePath.substring(1, sanitizedFilePath.length - 1); + } + if (sanitizedFilePath.startsWith('a/')) { + sanitizedFilePath = sanitizedFilePath.substring(2); + } else if (sanitizedFilePath.startsWith('b/')) { + sanitizedFilePath = sanitizedFilePath.substring(2); + } + + const absoluteFilePath = path.resolve(process.cwd(), sanitizedFilePath); + + const GEMINI_SECURITY_DIR = path.join(process.cwd(), '.gemini_security'); + + if (!graphBuilt) { + const loaded = await graphService.loadGraph(GEMINI_SECURITY_DIR); + if (!loaded) { + const files = await scan_dir(process.cwd()); + for (const file of files) { + try { + await graphBuilder.buildGraph(file); + } catch (e: any) { + // Ignore errors for unsupported file types + } + } + await graphService.saveGraph(GEMINI_SECURITY_DIR); + } + graphBuilt = true; + } + + const entity = graphService.findEnclosingEntity(absoluteFilePath, line); + + if (entity) { + const response = { + content: [ + { + type: 'text' as const, + text: JSON.stringify(entity, null, 2), + }, + ], + }; + return response as any; + } else { + const response = { + content: [ + { + type: 'text' as const, + text: 'No enclosing entity found.', + }, + ], + }; + return response as any; + } + } +); + server.tool( 'find_line_numbers', 'Finds the line numbers of a code snippet in a file.',