From 2a4d2a408e78ee2431bb59e244be54787d23c78f Mon Sep 17 00:00:00 2001 From: Mark Stuart Date: Mon, 10 Aug 2026 19:07:54 -0700 Subject: [PATCH] fix: complete code-memory packaging audit fixes --- .github/workflows/ci.yml | 15 + .github/workflows/ci.yml.bak | 40 -- .github/workflows/release.yml.bak | 108 ---- Cargo.lock | 2 +- Cargo.toml | 4 +- LICENSE | 21 + npm/LICENSE | 21 + npm/bin/code-memory | 39 +- npm/package-lock.json | 565 +++++++++++++++++++ npm/scripts/install.js | 33 +- src/cli.rs | 8 +- src/indexer/walker.rs | 20 +- src/indexer/walker.rs.bak | 200 ------- src/main.rs | 25 +- src/main.rs.bak | 366 ------------ src/mcp/server.rs | 6 +- src/mcp/tools.rs.bak | 898 ------------------------------ tests/approval_test.rs | 20 +- tests/workspace_test.rs | 29 +- 19 files changed, 742 insertions(+), 1678 deletions(-) delete mode 100644 .github/workflows/ci.yml.bak delete mode 100644 .github/workflows/release.yml.bak create mode 100644 LICENSE create mode 100644 npm/LICENSE create mode 100644 npm/package-lock.json delete mode 100644 src/indexer/walker.rs.bak delete mode 100644 src/main.rs.bak delete mode 100644 src/mcp/tools.rs.bak diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a48457..4d00832 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,3 +83,18 @@ jobs: - name: Ultracite check working-directory: npm run: npm run check + npm-package-smoke: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: actions/setup-node@v4 + with: + node-version: '20' + - run: cargo build --release + - run: | + cp target/release/code-memory npm/bin/code-memory-bin + chmod +x npm/bin/code-memory-bin + cd npm + npm pack --dry-run + node bin/code-memory --version diff --git a/.github/workflows/ci.yml.bak b/.github/workflows/ci.yml.bak deleted file mode 100644 index 4a3ab1e..0000000 --- a/.github/workflows/ci.yml.bak +++ /dev/null @@ -1,40 +0,0 @@ -name: CI - -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - run: cargo check - - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - run: cargo test - - clippy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - with: - components: clippy - - run: cargo clippy -- -D warnings - - format: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - with: - components: rustfmt - - run: cargo fmt --check diff --git a/.github/workflows/release.yml.bak b/.github/workflows/release.yml.bak deleted file mode 100644 index 30fd21a..0000000 --- a/.github/workflows/release.yml.bak +++ /dev/null @@ -1,108 +0,0 @@ -name: Release - -on: - push: - tags: - - 'v*' - -permissions: - contents: write - -jobs: - build: - strategy: - matrix: - include: - - os: macos-latest - target: x86_64-apple-darwin - artifact: claude-context - - os: macos-latest - target: aarch64-apple-darwin - artifact: claude-context - - os: ubuntu-latest - target: x86_64-unknown-linux-gnu - artifact: claude-context - - os: ubuntu-latest - target: aarch64-unknown-linux-gnu - artifact: claude-context - - os: windows-latest - target: x86_64-pc-windows-msvc - artifact: claude-context.exe - - runs-on: ${{ matrix.os }} - - steps: - - uses: actions/checkout@v4 - - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ matrix.target }} - - - name: Install cross-compilation tools (Linux ARM64) - if: matrix.target == 'aarch64-unknown-linux-gnu' - run: | - sudo apt-get update - sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu - echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV - - - name: Build - run: cargo build --release --target ${{ matrix.target }} - - - name: Package (Unix) - if: runner.os != 'Windows' - run: | - cd target/${{ matrix.target }}/release - tar czf ../../../claude-context-${{ github.ref_name }}-${{ matrix.target }}.tar.gz ${{ matrix.artifact }} - - - name: Package (Windows) - if: runner.os == 'Windows' - run: | - cd target/${{ matrix.target }}/release - 7z a ../../../claude-context-${{ github.ref_name }}-${{ matrix.target }}.zip ${{ matrix.artifact }} - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: claude-context-${{ matrix.target }} - path: claude-context-${{ github.ref_name }}-${{ matrix.target }}.* - - release: - needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Download all artifacts - uses: actions/download-artifact@v4 - with: - path: artifacts - - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 - with: - files: artifacts/**/* - generate_release_notes: true - - publish-npm: - needs: release - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: '20' - registry-url: 'https://registry.npmjs.org' - - - name: Update npm package version - run: | - cd npm - VERSION="${{ github.ref_name }}" - VERSION="${VERSION#v}" - node -e "const p=require('./package.json');p.version='${VERSION}';require('fs').writeFileSync('./package.json',JSON.stringify(p,null,2)+'\n')" - - - name: Publish to npm - run: cd npm && npm publish --access public - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/Cargo.lock b/Cargo.lock index 56d9499..4082e30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -448,7 +448,7 @@ checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "code-memory" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index f380768..d2debcb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "code-memory" -version = "0.1.0" +version = "0.1.1" edition = "2021" description = "Persistent memory for AI coding - semantic search, git history, and intelligent context preservation" license = "MIT" repository = "https://github.com/mstuart/code-memory" -keywords = ["ai-memory", "code-context", "mcp-server", "semantic-search", "git-history"] +keywords = ["ai-memory", "code-memory", "mcp-server", "semantic-search", "git-history"] categories = ["development-tools", "command-line-utilities"] [dependencies] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ed71d21 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Mark Stuart + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/npm/LICENSE b/npm/LICENSE new file mode 100644 index 0000000..ed71d21 --- /dev/null +++ b/npm/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Mark Stuart + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/npm/bin/code-memory b/npm/bin/code-memory index b7ef428..99ec764 100755 --- a/npm/bin/code-memory +++ b/npm/bin/code-memory @@ -1,24 +1,25 @@ -#!/bin/sh -# Wrapper script for code-memory binary -# The actual binary is downloaded during npm postinstall +#!/usr/bin/env node +"use strict"; -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -BINARY="$SCRIPT_DIR/code-memory-bin" +const { spawnSync } = require("child_process"); +const path = require("path"); -if [ -f "$BINARY" ]; then - exec "$BINARY" "$@" -fi +const binary = path.join( + __dirname, + process.platform === "win32" ? "code-memory-bin.exe" : "code-memory-bin" +); -# Check for binary with platform-specific name -case "$(uname -s)" in - MINGW*|MSYS*|CYGWIN*) BINARY="$SCRIPT_DIR/code-memory.exe" ;; -esac +const result = spawnSync(binary, process.argv.slice(2), { stdio: "inherit" }); -if [ -f "$BINARY" ]; then - exec "$BINARY" "$@" -fi +if (result.error) { + console.error("Error: code-memory native binary not found or not executable."); + console.error("Try reinstalling: npm install -g code-memory"); + console.error("Or build from source: cargo install --git https://github.com/mstuart/code-memory code-memory"); + process.exit(1); +} -echo "Error: code-memory binary not found." -echo "Try reinstalling: npm install -g @mstuart/code-memory" -echo "Or build from source: cargo install --path ." -exit 1 +if (result.signal) { + process.kill(process.pid, result.signal); +} + +process.exit(result.status ?? 1); diff --git a/npm/package-lock.json b/npm/package-lock.json new file mode 100644 index 0000000..9d8719a --- /dev/null +++ b/npm/package-lock.json @@ -0,0 +1,565 @@ +{ + "name": "code-memory", + "version": "0.1.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "code-memory", + "version": "0.1.1", + "cpu": [ + "x64", + "arm64" + ], + "hasInstallScript": true, + "license": "MIT", + "os": [ + "darwin", + "linux", + "win32" + ], + "bin": { + "code-memory": "bin/code-memory" + }, + "devDependencies": { + "@biomejs/biome": "2.5.6", + "ultracite": "7.10.2" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@biomejs/biome": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.6.tgz", + "integrity": "sha512-lxVNjv7UF6KfhMJfL9gaUHbWdJdHbsAj6OSmwSYNdhRuG67NxNQ4Xdvh3TUxsSK9sBzJBQhEJj3AopmmNJ5pSA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.5.6", + "@biomejs/cli-darwin-x64": "2.5.6", + "@biomejs/cli-linux-arm64": "2.5.6", + "@biomejs/cli-linux-arm64-musl": "2.5.6", + "@biomejs/cli-linux-x64": "2.5.6", + "@biomejs/cli-linux-x64-musl": "2.5.6", + "@biomejs/cli-win32-arm64": "2.5.6", + "@biomejs/cli-win32-x64": "2.5.6" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-zMOLZP4oMrjh6m1zcSj1ud2awUPgTuMVbmQhYYWL7J8HwCnbHHBvTm7VBTRuY7epT5bez76IpKYQ11ZAqHFlnw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.6.tgz", + "integrity": "sha512-JAC1VqzvO7Th5ZplU0G2uGfkZbxEe9uDDektPAhF0JLusoz1w+T4okp2bkykI0bbaO2vslKiRfj4gU43JaGreA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.6.tgz", + "integrity": "sha512-6XsYwCFkp5sMxl85ffhgeGpGgs6A7dRYFnkceZ7WVxvycuTnGdD5xa534Z3xfrBQ0JCMK/mujT6ZNPJoghedwg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-eUa3jeeYvfMt19LBeh6E5PUZpxnTC4JqNWo+EDjTtQjAr2xLGnWaxACtVU1DQqmHYbvThlJzLX+ZsYgrqh2qVw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.6.tgz", + "integrity": "sha512-Pop9VXCFUhFTMfFefZ39S+u2rOPyNp5iHlxbZRwXGACHLy2r0jjiRgJHmaEKJzL3SyxlVeGShXhvvElvWowonA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-2Vp13QdKysH3HIWLaYLhUUwbK+jbZonJD1K+Lr0d0RO4wH7mkYd43vJixEDm8cUWrowoRz4UUHF1nm9Ae7ym8A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.6.tgz", + "integrity": "sha512-tDGshcm6BdkZOCGnTDX0Y8/U4IfBSlnUU7T56nNDuPEfed+aHg+u8G36NB43fJVl0Os6+QURXIE1yuD7AaEofA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.6.tgz", + "integrity": "sha512-WN05KwXnTO/2J45RQPvzZMXf7tZUIofHoR35xIPfCo7pQ2RFidxI8sfb5mGsaTxdMmEOzHzOPRCdA5/fCpc7xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@clack/core": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.4.3.tgz", + "integrity": "sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@clack/prompts": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.7.0.tgz", + "integrity": "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@clack/core": "1.4.3", + "fast-string-width": "^3.0.2", + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/citty": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz", + "integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/nypm": { + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.9.tgz", + "integrity": "sha512-zxlE2yvSWZWmHcNdT3+5zV2lrCogeE9YOklHrR3dFjqutq5wO7GFDYLFDRXLsYnJzwvy/im9fYoxePvS0VTW0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "citty": "^0.2.2", + "pathe": "^2.0.3", + "tinyexec": "^1.2.4" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/ultracite": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/ultracite/-/ultracite-7.10.2.tgz", + "integrity": "sha512-WP1Hu/BKy/BDgntaiU33uq2Y8hKL2gEO5li2wfg7XFvmlyIkGDEy8qbWg7GHsxTEjEpWOAKpyFSF8aqc3JNEGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@clack/prompts": "^1.5.1", + "commander": "^15.0.0", + "cross-spawn": "^7.0.6", + "deepmerge": "^4.3.1", + "glob": "^13.0.6", + "jsonc-parser": "^3.3.1", + "nypm": "^0.6.6", + "yaml": "^2.9.0", + "zod": "^4.4.3" + }, + "bin": { + "ultracite": "dist/index.js" + }, + "peerDependencies": { + "oxfmt": ">=0.1.0", + "oxlint": "^1.0.0" + }, + "peerDependenciesMeta": { + "oxfmt": { + "optional": true + }, + "oxlint": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/npm/scripts/install.js b/npm/scripts/install.js index 9e3391a..7875c5c 100644 --- a/npm/scripts/install.js +++ b/npm/scripts/install.js @@ -110,9 +110,12 @@ async function install() { path.dirname(require.resolve("../package.json")), "bin" ); + const extractedName = + platform === "win32" ? `${BINARY_NAME}.exe` : BINARY_NAME; + const extractedPath = path.join(binDir, extractedName); const binPath = path.join( binDir, - platform === "win32" ? `${BINARY_NAME}.exe` : BINARY_NAME + platform === "win32" ? `${BINARY_NAME}-bin.exe` : `${BINARY_NAME}-bin` ); // Check if binary already exists @@ -138,7 +141,14 @@ async function install() { extractTarGz(data, binDir); } - // Make executable + if (!fs.existsSync(extractedPath)) { + throw new Error(`archive did not contain ${extractedName}`); + } + + if (extractedPath !== binPath) { + fs.renameSync(extractedPath, binPath); + } + if (platform !== "win32") { fs.chmodSync(binPath, 0o755); } @@ -146,22 +156,13 @@ async function install() { console.log(` Installed to ${binPath}`); } catch (error) { console.warn(`\nFailed to download pre-built binary: ${error.message}`); - console.warn("\nYou can build from source instead:"); - console.warn(" cargo install --path ."); + console.warn("\nTry reinstalling: npm install -g code-memory"); + console.warn("\nOr build from source instead:"); + console.warn( + " cargo install --git https://github.com/mstuart/code-memory code-memory" + ); console.warn("\nOr download manually from:"); console.warn(` https://github.com/${REPO}/releases`); - - // Create a stub script that tells the user to install manually - const stub = - platform === "win32" - ? "@echo off\necho code-memory binary not installed. Run: cargo install --path . in the code-memory repo\nexit /b 1\n" - : `#!/bin/sh\necho "code-memory binary not installed. Run: cargo install --path . in the code-memory repo"\nexit 1\n`; - - fs.mkdirSync(binDir, { recursive: true }); - fs.writeFileSync(binPath, stub); - if (platform !== "win32") { - fs.chmodSync(binPath, 0o755); - } } } diff --git a/src/cli.rs b/src/cli.rs index a0f134d..eb08c7b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -3,10 +3,10 @@ use std::path::PathBuf; #[derive(Parser)] #[command( - name = "claude-context", + name = "code-memory", version, about = "Intelligent code context for Claude — full-text search, semantic search, git history, and dependency analysis", - long_about = "claude-context is an MCP server that gives Claude deep understanding of your codebase.\n\n\ + long_about = "code-memory is an MCP server that gives Claude deep understanding of your codebase.\n\n\ It indexes your code, extracts symbols, analyzes git history, and builds dependency\n\ graphs so Claude can search, explain, trace decisions, and find related code." )] @@ -24,7 +24,7 @@ pub enum Command { root: PathBuf, }, - /// Initialize claude-context in a project directory + /// Initialize code-memory in a project directory Init { /// Directory to initialize (default: current directory) #[arg(default_value = ".")] @@ -70,7 +70,7 @@ pub enum Command { /// Export knowledge base Export { /// Output file path - #[arg(short, long, default_value = "claude-context-export.json")] + #[arg(short, long, default_value = "code-memory-export.json")] output: PathBuf, /// Project root directory diff --git a/src/indexer/walker.rs b/src/indexer/walker.rs index 7cae403..fe399ad 100644 --- a/src/indexer/walker.rs +++ b/src/indexer/walker.rs @@ -144,11 +144,20 @@ pub fn index_project(project_path: &Path, code_index: &CodeIndex) -> Result PathBuf { let home = dirs::home_dir().expect("cannot determine home directory"); - let context_dir = home.join(".code-memory").join("index"); + let current_dir = home.join(".code-memory").join("index"); + let legacy_dir = home.join(".claude-context").join("index"); - // Use a hash of the project path as the directory name to avoid conflicts + // Use a hash of the project path as the directory name to avoid conflicts. let project_hash = simple_hash(&project_path.to_string_lossy()); - context_dir.join(format!("{:016x}", project_hash)) + let leaf = format!("{:016x}", project_hash); + let current_path = current_dir.join(&leaf); + let legacy_path = legacy_dir.join(&leaf); + + if current_path.exists() || !legacy_path.exists() { + current_path + } else { + legacy_path + } } fn simple_hash(s: &str) -> u64 { @@ -198,7 +207,10 @@ mod tests { fn test_index_storage_path() { let path = Path::new("/Users/test/myproject"); let storage = index_storage_path(path); - assert!(storage.to_string_lossy().contains(".code-memory")); + assert!( + storage.to_string_lossy().contains(".code-memory") + || storage.to_string_lossy().contains(".claude-context") + ); assert!(storage.to_string_lossy().contains("index")); } } diff --git a/src/indexer/walker.rs.bak b/src/indexer/walker.rs.bak deleted file mode 100644 index d9d7dd0..0000000 --- a/src/indexer/walker.rs.bak +++ /dev/null @@ -1,200 +0,0 @@ -use anyhow::Result; -use ignore::WalkBuilder; -use std::path::{Path, PathBuf}; -use std::time::SystemTime; -use tracing::{debug, warn}; - -use super::code_index::{CodeIndex, CodeSchema}; -use super::parser; - -/// Stats from an indexing operation -#[derive(Debug)] -pub struct IndexStats { - pub files_indexed: usize, - pub files_skipped: usize, - pub symbols_found: usize, - pub elapsed_ms: u128, -} - -/// Walk a project directory and index all code files. -/// Respects .gitignore via the `ignore` crate. -pub fn index_project(project_path: &Path, code_index: &CodeIndex) -> Result { - let start = std::time::Instant::now(); - let mut writer = code_index.writer()?; - let schema = code_index.schema(); - - let mut files_indexed = 0usize; - let mut files_skipped = 0usize; - let mut symbols_found = 0usize; - - let walker = WalkBuilder::new(project_path) - .hidden(true) // skip hidden files - .git_ignore(true) // respect .gitignore - .git_global(true) // respect global gitignore - .git_exclude(true) // respect .git/info/exclude - .build(); - - for entry in walker { - let entry = match entry { - Ok(e) => e, - Err(e) => { - warn!("Walk error: {}", e); - files_skipped += 1; - continue; - } - }; - - // Only process files - if !entry.file_type().map_or(false, |ft| ft.is_file()) { - continue; - } - - let path = entry.path(); - - // Detect language - let language = match parser::detect_language(path) { - Some(lang) => lang, - None => { - files_skipped += 1; - continue; - } - }; - - // Skip binary / very large files - let metadata = match std::fs::metadata(path) { - Ok(m) => m, - Err(_) => { - files_skipped += 1; - continue; - } - }; - - if metadata.len() > 1_000_000 { - // Skip files > 1MB - files_skipped += 1; - continue; - } - - // Read content - let content = match std::fs::read_to_string(path) { - Ok(c) => c, - Err(_) => { - // Likely a binary file - files_skipped += 1; - continue; - } - }; - - // Extract symbols - let symbols = parser::extract_symbols(&content, language); - symbols_found += symbols.len(); - - let symbol_names: String = symbols - .iter() - .map(|s| s.name.as_str()) - .collect::>() - .join(" "); - - // Get modification time - let modified_time = metadata - .modified() - .unwrap_or(SystemTime::UNIX_EPOCH) - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap_or_default() - .as_secs() as i64; - - // Relative path for storage - let rel_path = path - .strip_prefix(project_path) - .unwrap_or(path) - .to_string_lossy(); - - debug!("Indexing: {} ({}, {} symbols)", rel_path, language, symbols.len()); - - CodeIndex::index_file( - &writer, - schema, - &rel_path, - &content, - &symbol_names, - language, - modified_time, - )?; - - files_indexed += 1; - } - - writer.commit()?; - - let elapsed_ms = start.elapsed().as_millis(); - - Ok(IndexStats { - files_indexed, - files_skipped, - symbols_found, - elapsed_ms, - }) -} - -/// Get the storage path for a project's index -pub fn index_storage_path(project_path: &Path) -> PathBuf { - let home = dirs::home_dir().expect("cannot determine home directory"); - let context_dir = home.join(".claude-context").join("index"); - - // Use a hash of the project path as the directory name to avoid conflicts - let project_hash = simple_hash(&project_path.to_string_lossy()); - context_dir.join(format!("{:016x}", project_hash)) -} - -fn simple_hash(s: &str) -> u64 { - let mut hash: u64 = 5381; - for byte in s.bytes() { - hash = hash.wrapping_mul(33).wrapping_add(byte as u64); - } - hash -} - -#[cfg(test)] -mod tests { - use super::*; - use std::fs; - use tempfile::TempDir; - - #[test] - fn test_index_project() { - let dir = TempDir::new().unwrap(); - let src_dir = dir.path().join("src"); - fs::create_dir_all(&src_dir).unwrap(); - - // Create some test files - fs::write( - src_dir.join("main.rs"), - "pub fn main() {}\nstruct App {}\n", - ).unwrap(); - fs::write( - src_dir.join("lib.rs"), - "pub fn helper() {}\npub enum Color { Red }\n", - ).unwrap(); - fs::write( - dir.path().join("script.py"), - "def hello():\n pass\nclass World:\n pass\n", - ).unwrap(); - - // Create index in temp dir - let index_dir = dir.path().join("index"); - let code_index = CodeIndex::open_or_create(&index_dir).unwrap(); - - let stats = index_project(dir.path(), &code_index).unwrap(); - - assert!(stats.files_indexed >= 3); // 3 code files + possibly config files - assert!(stats.symbols_found >= 5); // main, App, helper, Color, hello, World - } - - #[test] - fn test_index_storage_path() { - let path = Path::new("/Users/test/myproject"); - let storage = index_storage_path(path); - assert!(storage.to_string_lossy().contains(".claude-context")); - assert!(storage.to_string_lossy().contains("index")); - } -} diff --git a/src/main.rs b/src/main.rs index 5beb7b8..403f9d1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -61,9 +61,24 @@ fn canonicalize_root(root: PathBuf) -> Result { Ok(std::fs::canonicalize(&root).unwrap_or(root)) } +fn config_dir(root: &std::path::Path) -> PathBuf { + let current = root.join(".code-memory"); + let legacy = root.join(".claude-context"); + + if current.exists() || !legacy.exists() { + current + } else { + legacy + } +} + +fn writable_config_dir(root: &std::path::Path) -> PathBuf { + root.join(".code-memory") +} + async fn cmd_init(path: PathBuf) -> Result<()> { let path = canonicalize_root(path)?; - let config_dir = path.join(".code-memory"); + let config_dir = writable_config_dir(&path); if config_dir.exists() { eprintln!("Already initialized: {}", config_dir.display()); @@ -74,7 +89,7 @@ async fn cmd_init(path: PathBuf) -> Result<()> { // Write default config let config = r#"# code-memory configuration -# See https://github.com/user/code-memory for documentation +# See https://github.com/mstuart/code-memory for documentation [index] # Languages to index (empty = all detected) @@ -176,7 +191,7 @@ async fn cmd_search( async fn cmd_stats(root: PathBuf) -> Result<()> { let root = canonicalize_root(root)?; - let config_dir = root.join(".code-memory"); + let config_dir = config_dir(&root); let index_path = code_memory::indexer::walker::index_storage_path(&root); println!("code-memory v{}", env!("CARGO_PKG_VERSION")); @@ -224,7 +239,7 @@ async fn cmd_stats(root: PathBuf) -> Result<()> { async fn cmd_export(root: PathBuf, output: PathBuf) -> Result<()> { let root = canonicalize_root(root)?; - let knowledge_file = root.join(".code-memory").join("knowledge.json"); + let knowledge_file = config_dir(&root).join("knowledge.json"); if !knowledge_file.exists() { eprintln!("No knowledge entries to export."); @@ -245,7 +260,7 @@ async fn cmd_export(root: PathBuf, output: PathBuf) -> Result<()> { async fn cmd_import(root: PathBuf, input: PathBuf) -> Result<()> { let root = canonicalize_root(root)?; - let config_dir = root.join(".code-memory"); + let config_dir = config_dir(&root); let knowledge_file = config_dir.join("knowledge.json"); if !input.exists() { diff --git a/src/main.rs.bak b/src/main.rs.bak deleted file mode 100644 index 073aeb4..0000000 --- a/src/main.rs.bak +++ /dev/null @@ -1,366 +0,0 @@ -use std::path::PathBuf; - -use anyhow::Result; -use clap::Parser; -use tracing_subscriber::EnvFilter; - -use claude_context::cli::{Cli, Command}; -use claude_context::mcp::server::McpServer; -use claude_context::sessions::{SessionTracker, SessionEvent, SessionEventType}; - -#[tokio::main] -async fn main() -> Result<()> { - // Initialize logging to stderr (never stdout — that's for MCP JSON-RPC) - tracing_subscriber::fmt() - .with_env_filter( - EnvFilter::try_from_default_env() - .unwrap_or_else(|_| EnvFilter::new("info")), - ) - .with_writer(std::io::stderr) - .init(); - - let cli = Cli::parse(); - - match cli.command { - // Default (no subcommand) or explicit `serve`: run MCP server - None => { - let root = std::env::current_dir()?; - let server = McpServer::new(root); - server.run().await - } - Some(Command::Serve { root }) => { - let root = canonicalize_root(root)?; - let server = McpServer::new(root); - server.run().await - } - Some(Command::Init { path }) => cmd_init(path).await, - Some(Command::Reindex { root, force }) => cmd_reindex(root, force).await, - Some(Command::Search { query, limit, language, root }) => { - cmd_search(root, &query, limit, language.as_deref()).await - } - Some(Command::Stats { root }) => cmd_stats(root).await, - Some(Command::Export { output, root }) => cmd_export(root, output).await, - Some(Command::Import { input, root }) => cmd_import(root, input).await, - Some(Command::Sessions { top, min_confidence, format }) => { - cmd_sessions(top, min_confidence, &format).await - } - } -} - -fn canonicalize_root(root: PathBuf) -> Result { - let root = if root.is_relative() { - std::env::current_dir()?.join(root) - } else { - root - }; - Ok(std::fs::canonicalize(&root).unwrap_or(root)) -} - -async fn cmd_init(path: PathBuf) -> Result<()> { - let path = canonicalize_root(path)?; - let config_dir = path.join(".claude-context"); - - if config_dir.exists() { - eprintln!("Already initialized: {}", config_dir.display()); - return Ok(()); - } - - std::fs::create_dir_all(&config_dir)?; - - // Write default config - let config = r#"# claude-context configuration -# See https://github.com/user/claude-context for documentation - -[index] -# Languages to index (empty = all detected) -languages = [] - -# Patterns to exclude (in addition to .gitignore) -exclude = ["node_modules", "target", "dist", ".git", "*.min.js", "*.map"] - -# Maximum file size to index (bytes) -max_file_size = 1048576 # 1 MB - -[search] -# Default max results -default_limit = 10 - -# Enable semantic search (requires model download on first use) -semantic = true - -[git] -# Maximum commits to analyze per file -max_commits = 500 - -# Keywords that indicate architectural decisions -decision_keywords = ["decision:", "chose", "rationale:", "why:", "trade-off", "alternative:"] -"#; - - std::fs::write(config_dir.join("config.toml"), config)?; - eprintln!("Initialized claude-context in {}", path.display()); - eprintln!("Config: {}", config_dir.join("config.toml").display()); - eprintln!("\nNext steps:"); - eprintln!(" claude-context reindex # Build the code index"); - eprintln!(" claude-context serve # Start the MCP server"); - Ok(()) -} - -async fn cmd_reindex(root: PathBuf, force: bool) -> Result<()> { - let root = canonicalize_root(root)?; - eprintln!("Reindexing {} (force={})", root.display(), force); - - let index_path = claude_context::indexer::walker::index_storage_path(&root); - if force && index_path.exists() { - std::fs::remove_dir_all(&index_path)?; - } - - let code_index = claude_context::indexer::code_index::CodeIndex::open_or_create(&index_path)?; - let stats = claude_context::indexer::walker::index_project(&root, &code_index)?; - - eprintln!( - "Indexed {} files ({} skipped) with {} symbols in {}ms", - stats.files_indexed, stats.files_skipped, stats.symbols_found, stats.elapsed_ms - ); - eprintln!("Index stored at: {}", index_path.display()); - Ok(()) -} - -async fn cmd_search( - root: PathBuf, - query: &str, - limit: usize, - _language: Option<&str>, -) -> Result<()> { - let root = canonicalize_root(root)?; - - let index_path = claude_context::indexer::walker::index_storage_path(&root); - if !index_path.exists() { - eprintln!("No index found. Run `claude-context reindex` first."); - return Ok(()); - } - - let code_index = claude_context::indexer::code_index::CodeIndex::open_or_create(&index_path)?; - let search = claude_context::search::fulltext::FullTextSearch::new( - code_index.index(), - code_index.schema(), - )?; - - let results = search.search(query, limit)?; - if results.is_empty() { - println!("No results found for '{}'", query); - return Ok(()); - } - - println!("Found {} results for '{}':\n", results.len(), query); - for (i, result) in results.iter().enumerate() { - println!( - "{}. {} (score: {:.2})\n Language: {}\n Symbols: {}\n", - i + 1, - result.path, - result.score, - result.language, - if result.symbols.is_empty() { "(none)" } else { &result.symbols }, - ); - } - Ok(()) -} - -async fn cmd_stats(root: PathBuf) -> Result<()> { - let root = canonicalize_root(root)?; - let config_dir = root.join(".claude-context"); - let index_path = claude_context::indexer::walker::index_storage_path(&root); - - println!("claude-context v{}", env!("CARGO_PKG_VERSION")); - println!("Project root: {}", root.display()); - println!( - "Initialized: {}", - if config_dir.exists() { "yes" } else { "no" } - ); - println!( - "Index: {}", - if index_path.exists() { "built" } else { "not built (run `claude-context reindex`)" } - ); - println!("Index path: {}", index_path.display()); - - // Knowledge store - let knowledge_file = config_dir.join("knowledge.json"); - if knowledge_file.exists() { - if let Ok(data) = std::fs::read_to_string(&knowledge_file) { - let entries: Vec = serde_json::from_str(&data).unwrap_or_default(); - println!("Knowledge entries: {}", entries.len()); - } - } else { - println!("Knowledge entries: 0"); - } - - // Git info - match claude_context::git::history::GitHistory::discover(&root) { - Ok(git) => { - let commits = git.walk_commits(1).unwrap_or_default(); - if !commits.is_empty() { - println!("Git: available (latest commit: {})", &commits[0].id[..8]); - } else { - println!("Git: available (no commits)"); - } - } - Err(_) => println!("Git: not available"), - } - - Ok(()) -} - -async fn cmd_export(root: PathBuf, output: PathBuf) -> Result<()> { - let root = canonicalize_root(root)?; - let knowledge_file = root.join(".claude-context").join("knowledge.json"); - - if !knowledge_file.exists() { - eprintln!("No knowledge entries to export."); - return Ok(()); - } - - let data = std::fs::read_to_string(&knowledge_file)?; - std::fs::write(&output, &data)?; - - let entries: Vec = serde_json::from_str(&data).unwrap_or_default(); - eprintln!( - "Exported {} knowledge entries to {}", - entries.len(), - output.display() - ); - Ok(()) -} - -async fn cmd_import(root: PathBuf, input: PathBuf) -> Result<()> { - let root = canonicalize_root(root)?; - let config_dir = root.join(".claude-context"); - let knowledge_file = config_dir.join("knowledge.json"); - - if !input.exists() { - eprintln!("Input file not found: {}", input.display()); - return Ok(()); - } - - let import_data = std::fs::read_to_string(&input)?; - let import_entries: Vec = serde_json::from_str(&import_data)?; - - // Merge with existing - let mut entries: Vec = if knowledge_file.exists() { - let data = std::fs::read_to_string(&knowledge_file)?; - serde_json::from_str(&data).unwrap_or_default() - } else { - std::fs::create_dir_all(&config_dir)?; - Vec::new() - }; - - let before = entries.len(); - for entry in import_entries { - let topic = entry.get("topic").and_then(|v| v.as_str()).unwrap_or(""); - let exists = entries - .iter() - .any(|e| e.get("topic").and_then(|v| v.as_str()) == Some(topic)); - if !exists { - entries.push(entry); - } - } - - std::fs::write( - &knowledge_file, - serde_json::to_string_pretty(&entries)?, - )?; - - eprintln!( - "Imported {} new entries (total: {}) from {}", - entries.len() - before, - entries.len(), - input.display() - ); - Ok(()) -} - -async fn cmd_sessions(top: usize, min_confidence: f32, format: &str) -> Result<()> { - let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")); - let history_path = home.join(".claude"); - - if !history_path.exists() { - eprintln!("No Claude Code history found at {}", history_path.display()); - eprintln!("Claude Code stores session data in ~/.claude/"); - return Ok(()); - } - - eprintln!("Analyzing Claude Code sessions in {}...", history_path.display()); - - let mut tracker = SessionTracker::new(history_path); - let events = tracker.analyze_all_sessions(); - - if format == "json" { - let output = serde_json::json!({ - "events": events, - "event_count": events.len(), - "patterns": tracker.patterns().to_json(), - }); - println!("{}", serde_json::to_string_pretty(&output)?); - return Ok(()); - } - - // Text output - println!("claude-context: Session Analysis"); - println!("================================\n"); - - // Events summary - let decisions: Vec<&SessionEvent> = events.iter() - .filter(|e| e.event_type == SessionEventType::ArchitecturalDecision) - .collect(); - let errors: Vec<&SessionEvent> = events.iter() - .filter(|e| e.event_type == SessionEventType::ErrorAndFix) - .collect(); - let tests: Vec<&SessionEvent> = events.iter() - .filter(|e| e.event_type == SessionEventType::TestWritten) - .collect(); - let refactors: Vec<&SessionEvent> = events.iter() - .filter(|e| e.event_type == SessionEventType::Refactoring) - .collect(); - - println!("Events found: {}", events.len()); - println!(" Architectural decisions: {}", decisions.len()); - println!(" Error-and-fix pairs: {}", errors.len()); - println!(" Tests written: {}", tests.len()); - println!(" Refactorings: {}", refactors.len()); - println!(); - - // Show recent decisions - if !decisions.is_empty() { - println!("Recent Decisions:"); - for decision in decisions.iter().rev().take(5) { - let desc = if decision.description.len() > 80 { - format!("{}...", &decision.description[..80]) - } else { - decision.description.clone() - }; - println!(" - {}", desc); - } - println!(); - } - - // Show patterns - let patterns = if min_confidence > 0.0 { - tracker.patterns().confident_patterns(min_confidence) - } else { - tracker.patterns().top_patterns(top) - }; - - if !patterns.is_empty() { - println!("Learned Patterns:"); - for pattern in &patterns { - println!( - " [{:.0}%] {} (observed {} times)", - pattern.confidence * 100.0, - pattern.description, - pattern.observation_count - ); - } - } else { - println!("No patterns learned yet. More session data needed."); - } - - Ok(()) -} diff --git a/src/mcp/server.rs b/src/mcp/server.rs index 0dd5065..027d6e9 100644 --- a/src/mcp/server.rs +++ b/src/mcp/server.rs @@ -23,7 +23,7 @@ impl McpServer { /// Run the MCP server reading JSON-RPC from stdin, writing to stdout. pub async fn run(&self) -> Result<()> { info!( - "claude-context MCP server starting (root: {:?})", + "code-memory MCP server starting (root: {:?})", self.project_root ); @@ -55,7 +55,7 @@ impl McpServer { } } - info!("claude-context MCP server shutting down"); + info!("code-memory MCP server shutting down"); Ok(()) } @@ -108,7 +108,7 @@ impl McpServer { }, }, server_info: ServerInfo { - name: "claude-context".to_string(), + name: "code-memory".to_string(), version: env!("CARGO_PKG_VERSION").to_string(), }, }; diff --git a/src/mcp/tools.rs.bak b/src/mcp/tools.rs.bak deleted file mode 100644 index cf1768a..0000000 --- a/src/mcp/tools.rs.bak +++ /dev/null @@ -1,898 +0,0 @@ -use serde_json::{json, Value}; -use std::path::{Path, PathBuf}; -use std::sync::{Mutex, OnceLock}; -use tracing::{info, warn}; - -use crate::git::history::GitHistory; -use crate::indexer::code_index::CodeIndex; -use crate::indexer::walker; -use crate::mcp::protocol::*; -use crate::search::fulltext::FullTextSearch; - -/// Global index state (lazily initialized per project) -pub(crate) struct IndexState { - pub search: FullTextSearch, - pub project_path: PathBuf, - pub files_indexed: usize, - pub symbols_found: usize, -} - -static INDEX_STATE: OnceLock>> = OnceLock::new(); - -pub(crate) fn get_state_lock() -> &'static Mutex> { - INDEX_STATE.get_or_init(|| Mutex::new(None)) -} - -/// Ensure the project is indexed. Returns true if index is available. -pub(crate) fn ensure_indexed(project_root: &Path) -> bool { - let guard = get_state_lock().lock().unwrap(); - if guard.is_some() { - return true; - } - drop(guard); - - do_index(project_root).is_ok() -} - -/// Force reindex (drops existing state, re-walks project). -/// Returns (files_indexed, symbols_found, elapsed_ms). -pub(crate) fn force_reindex(project_root: &Path) -> Result<(usize, usize, u128), String> { - // Clear existing state - *get_state_lock().lock().unwrap() = None; - - // Remove the index directory and rebuild - let index_path = walker::index_storage_path(project_root); - if index_path.exists() { - std::fs::remove_dir_all(&index_path).map_err(|e| format!("Failed to remove old index: {}", e))?; - } - - let (files, symbols, elapsed) = do_index(project_root)?; - Ok((files, symbols, elapsed)) -} - -fn do_index(project_root: &Path) -> Result<(usize, usize, u128), String> { - let index_path = walker::index_storage_path(project_root); - let code_index = CodeIndex::open_or_create(&index_path) - .map_err(|e| format!("Failed to open index: {}", e))?; - - let stats = walker::index_project(project_root, &code_index) - .map_err(|e| format!("Failed to index project: {}", e))?; - - let search = FullTextSearch::new(code_index.index(), code_index.schema()) - .map_err(|e| format!("Failed to create search engine: {}", e))?; - - info!( - "Indexed {} files with {} symbols in {}ms", - stats.files_indexed, stats.symbols_found, stats.elapsed_ms - ); - - let state = IndexState { - search, - project_path: project_root.to_path_buf(), - files_indexed: stats.files_indexed, - symbols_found: stats.symbols_found, - }; - *get_state_lock().lock().unwrap() = Some(state); - - Ok((stats.files_indexed, stats.symbols_found, stats.elapsed_ms)) -} - -/// Return all tool definitions for the MCP server. -pub fn definitions() -> Vec { - vec![ - ToolDefinition { - name: "search_code".to_string(), - description: "Search code using full-text and semantic search. \ - Finds relevant code snippets by keyword or natural language meaning." - .to_string(), - input_schema: json!({ - "type": "object", - "properties": { - "query": { - "type": "string", - "description": "Search query — keywords or natural language" - }, - "max_results": { - "type": "integer", - "description": "Maximum results to return (default: 10)", - "default": 10 - }, - "include_git_history": { - "type": "boolean", - "description": "Include git commit history for matched files (default: false)", - "default": false - }, - "path": { - "type": "string", - "description": "Limit search to files under this path" - }, - "language": { - "type": "string", - "description": "Filter by language (rust, typescript, python, etc.)" - } - }, - "required": ["query"] - }), - }, - ToolDefinition { - name: "explain_code".to_string(), - description: "Get detailed explanation of a code symbol — its definition, \ - usage, dependencies, and git history." - .to_string(), - input_schema: json!({ - "type": "object", - "properties": { - "symbol": { - "type": "string", - "description": "Symbol name (function, class, type, struct, etc.)" - }, - "include_dependencies": { - "type": "boolean", - "description": "Include dependency graph for the symbol (default: true)", - "default": true - }, - "file": { - "type": "string", - "description": "Narrow to a specific file path" - } - }, - "required": ["symbol"] - }), - }, - ToolDefinition { - name: "trace_decision".to_string(), - description: "Trace architectural decisions from git history — find \ - why code exists, who changed it, and the rationale behind changes." - .to_string(), - input_schema: json!({ - "type": "object", - "properties": { - "topic": { - "type": "string", - "description": "Topic or keyword to trace (e.g. 'authentication', 'database migration')" - }, - "time_range": { - "type": "string", - "description": "Time range filter: '7d', '30d', '90d', '1y', or 'all' (default: 'all')", - "default": "all" - }, - "file": { - "type": "string", - "description": "Limit to decisions affecting a specific file" - } - }, - "required": ["topic"] - }), - }, - ToolDefinition { - name: "find_related".to_string(), - description: "Find code related to a given file via dependency graph traversal. \ - Shows what depends on this file and what it depends on." - .to_string(), - input_schema: json!({ - "type": "object", - "properties": { - "file": { - "type": "string", - "description": "File path to find related code for" - }, - "depth": { - "type": "integer", - "description": "Graph traversal depth (default: 2)", - "default": 2 - }, - "direction": { - "type": "string", - "description": "Traversal direction: 'dependents', 'dependencies', or 'both' (default: 'both')", - "default": "both", - "enum": ["dependents", "dependencies", "both"] - } - }, - "required": ["file"] - }), - }, - ToolDefinition { - name: "remember".to_string(), - description: "Store a piece of knowledge for future recall — architectural decisions, \ - patterns, conventions, or any context that should persist across sessions." - .to_string(), - input_schema: json!({ - "type": "object", - "properties": { - "topic": { - "type": "string", - "description": "Short topic label (e.g. 'auth-pattern', 'db-migration-strategy')" - }, - "content": { - "type": "string", - "description": "The knowledge to store" - }, - "tags": { - "type": "array", - "items": { "type": "string" }, - "description": "Tags for categorization (e.g. ['architecture', 'security'])" - } - }, - "required": ["topic", "content"] - }), - }, - ToolDefinition { - name: "index_project".to_string(), - description: "Manually trigger reindexing of the project. Use this when files \ - have changed and you want to refresh the search index." - .to_string(), - input_schema: json!({ - "type": "object", - "properties": { - "force": { - "type": "boolean", - "description": "Force full reindex, deleting existing index data (default: false)", - "default": false - } - } - }), - }, - ToolDefinition { - name: "get_session_patterns".to_string(), - description: "Retrieve learned patterns from Claude Code session history. \ - Shows coding conventions, error-fix pairs, architectural decisions, \ - and other patterns discovered from past sessions." - .to_string(), - input_schema: json!({ - "type": "object", - "properties": { - "min_confidence": { - "type": "number", - "description": "Minimum confidence threshold (0.0-1.0, default: 0.0)", - "default": 0.0 - }, - "top": { - "type": "integer", - "description": "Maximum number of patterns to return (default: 10)", - "default": 10 - }, - "pattern_type": { - "type": "string", - "description": "Filter by pattern type: 'naming', 'error', 'testing', 'organization', 'import', 'config'", - "enum": ["naming", "error", "testing", "organization", "import", "config"] - } - } - }), - }, - ] -} - -/// Dispatch a tool call to the appropriate handler. -pub async fn dispatch( - name: &str, - args: Option, - project_root: &Path, -) -> CallToolResult { - let args = args.unwrap_or(json!({})); - - match name { - "search_code" => handle_search_code(&args, project_root).await, - "explain_code" => handle_explain_code(&args, project_root).await, - "trace_decision" => handle_trace_decision(&args, project_root).await, - "find_related" => handle_find_related(&args, project_root).await, - "remember" => handle_remember(&args, project_root).await, - "index_project" => handle_index_project(&args, project_root).await, - "get_session_patterns" => handle_get_session_patterns(&args, project_root).await, - other => CallToolResult::error(format!("Unknown tool: {}", other)), - } -} - -// --------------------------------------------------------------------------- -// Tool handlers — stubs that validate params and will be wired to real -// implementations when the indexer, search, git, and graph modules are ready. -// --------------------------------------------------------------------------- - -async fn handle_search_code(args: &Value, project_root: &Path) -> CallToolResult { - let query = match args.get("query").and_then(|v| v.as_str()) { - Some(q) => q, - None => return CallToolResult::error("Missing required parameter: query".to_string()), - }; - let max_results = args - .get("max_results") - .and_then(|v| v.as_u64()) - .unwrap_or(10) as usize; - - info!( - "search_code: query={:?} max={} root={:?}", - query, max_results, project_root - ); - - // Auto-index if needed - if !ensure_indexed(project_root) { - return CallToolResult::error( - "Failed to auto-index project. Check that the project directory exists.".to_string(), - ); - } - - let guard = get_state_lock().lock().unwrap(); - let state = guard.as_ref().unwrap(); - - match state.search.search(query, max_results) { - Ok(results) => { - if results.is_empty() { - return CallToolResult::text(format!("No results found for '{}'", query)); - } - - let mut output = format!("Found {} results for '{}':\n\n", results.len(), query); - for (i, result) in results.iter().enumerate() { - output.push_str(&format!( - "{}. {} (score: {:.2})\n Language: {}\n Symbols: {}\n\n", - i + 1, - result.path, - result.score, - result.language, - if result.symbols.is_empty() { - "(none)" - } else { - &result.symbols - }, - )); - } - CallToolResult::text(output) - } - Err(e) => CallToolResult::error(format!("Search failed: {}", e)), - } -} - -async fn handle_explain_code(args: &Value, project_root: &Path) -> CallToolResult { - let symbol = match args.get("symbol").and_then(|v| v.as_str()) { - Some(s) => s, - None => return CallToolResult::error("Missing required parameter: symbol".to_string()), - }; - - info!("explain_code: symbol={:?} root={:?}", symbol, project_root); - - // Auto-index if needed - if !ensure_indexed(project_root) { - return CallToolResult::error( - "Failed to auto-index project.".to_string(), - ); - } - - let guard = get_state_lock().lock().unwrap(); - let state = guard.as_ref().unwrap(); - - // Search in the symbols field specifically - let symbol_query = format!("symbols:{}", symbol); - match state.search.search(&symbol_query, 20) { - Ok(results) => { - if results.is_empty() { - return CallToolResult::text(format!("No symbols found matching '{}'", symbol)); - } - - let mut output = format!("Symbol '{}' found in {} files:\n\n", symbol, results.len()); - for (i, result) in results.iter().enumerate() { - // Try to read the file for more context - let file_path = state.project_path.join(&result.path); - let context = if file_path.exists() { - if let Ok(content) = std::fs::read_to_string(&file_path) { - let lang = crate::indexer::parser::detect_language(&file_path) - .unwrap_or("unknown"); - let syms = crate::indexer::parser::extract_symbols(&content, lang); - let matching: Vec<_> = syms.iter() - .filter(|s| s.name.contains(symbol)) - .collect(); - if !matching.is_empty() { - let details: Vec = matching.iter() - .map(|s| format!(" {:?} '{}' at line {}", s.kind, s.name, s.line + 1)) - .collect(); - details.join("\n") - } else { - " (symbol details unavailable)".to_string() - } - } else { - " (file not readable)".to_string() - } - } else { - " (file not found)".to_string() - }; - - output.push_str(&format!( - "{}. {} [{}]\n{}\n\n", - i + 1, - result.path, - result.language, - context, - )); - } - CallToolResult::text(output) - } - Err(e) => CallToolResult::error(format!("Symbol search failed: {}", e)), - } -} - -async fn handle_trace_decision(args: &Value, project_root: &Path) -> CallToolResult { - let topic = match args.get("topic").and_then(|v| v.as_str()) { - Some(t) => t, - None => return CallToolResult::error("Missing required parameter: topic".to_string()), - }; - let time_range = args - .get("time_range") - .and_then(|v| v.as_str()) - .unwrap_or("all"); - - info!( - "trace_decision: topic={:?} range={} root={:?}", - topic, time_range, project_root - ); - - // Use git history module to find decisions - let git = match GitHistory::discover(project_root) { - Ok(g) => g, - Err(e) => { - return CallToolResult::error(format!( - "Could not open git repository at {}: {}", - project_root.display(), - e - )); - } - }; - - let max_commits = match time_range { - "7d" => 100, - "30d" => 500, - "90d" => 1000, - "1y" => 2000, - _ => 5000, // "all" - }; - - let timeline = match git.extract_all_decisions(max_commits) { - Ok(t) => t, - Err(e) => { - return CallToolResult::error(format!("Failed to analyze git history: {}", e)); - } - }; - - // Filter decisions by topic - let topic_lower = topic.to_lowercase(); - let matching: Vec<_> = timeline - .decisions - .iter() - .filter(|d| { - d.summary.to_lowercase().contains(&topic_lower) - || d.details.to_lowercase().contains(&topic_lower) - || d.files_affected.iter().any(|f| f.to_lowercase().contains(&topic_lower)) - }) - .collect(); - - if matching.is_empty() { - return CallToolResult::text(format!( - "No decisions found related to '{}' in git history ({} commits analyzed).\n\ - Try a broader topic or different time range.", - topic, max_commits - )); - } - - let mut output = format!( - "Found {} decisions related to '{}' (time range: {}):\n\n", - matching.len(), - topic, - time_range - ); - - for (i, decision) in matching.iter().enumerate() { - let date = chrono::DateTime::from_timestamp(decision.timestamp, 0) - .map(|d| d.format("%Y-%m-%d").to_string()) - .unwrap_or_else(|| "unknown".to_string()); - - output.push_str(&format!( - "{}. [{:?}] {} (confidence: {:.0}%)\n\ - \x20 Author: {} | Date: {} | Commit: {}\n\ - \x20 Details: {}\n\ - \x20 Files: {}\n\n", - i + 1, - decision.decision_type, - decision.summary, - decision.confidence * 100.0, - decision.author, - date, - &decision.commit_id[..8.min(decision.commit_id.len())], - decision.details, - decision.files_affected.join(", "), - )); - } - - CallToolResult::text(output) -} - -async fn handle_find_related(args: &Value, project_root: &Path) -> CallToolResult { - let file = match args.get("file").and_then(|v| v.as_str()) { - Some(f) => f, - None => return CallToolResult::error("Missing required parameter: file".to_string()), - }; - let depth = args - .get("depth") - .and_then(|v| v.as_u64()) - .unwrap_or(2) as usize; - let direction = args - .get("direction") - .and_then(|v| v.as_str()) - .unwrap_or("both"); - - info!( - "find_related: file={:?} depth={} dir={} root={:?}", - file, depth, direction, project_root - ); - - let mut output = format!( - "Related code for '{}' (depth={}, direction={}):\n\n", - file, depth, direction - ); - - // 1. Dependency graph analysis (import-based) - if ensure_indexed(project_root) { - use crate::graph::analyzer::DependencyGraph; - - let mut dep_graph = DependencyGraph::new(); - // Build graph from indexed files - let walker = ignore::WalkBuilder::new(project_root) - .hidden(true) - .git_ignore(true) - .build(); - - for entry in walker.flatten() { - if !entry.file_type().map_or(false, |ft| ft.is_file()) { - continue; - } - let path = entry.path(); - if let Ok(content) = std::fs::read_to_string(path) { - let rel_path = path - .strip_prefix(project_root) - .unwrap_or(path) - .to_string_lossy() - .to_string(); - dep_graph.add_file(&rel_path, &content); - } - } - - let deps = dep_graph.depends_on(file); - let dependents = dep_graph.depended_on_by(file); - - if !deps.is_empty() && (direction == "dependencies" || direction == "both") { - output.push_str(&format!("Dependencies ({} imports):\n", deps.len())); - for dep in &deps { - output.push_str(&format!(" -> {}\n", dep)); - } - output.push('\n'); - } - - if !dependents.is_empty() && (direction == "dependents" || direction == "both") { - output.push_str(&format!("Dependents ({} files import this):\n", dependents.len())); - for dep in &dependents { - output.push_str(&format!(" <- {}\n", dep)); - } - output.push('\n'); - } - - if deps.is_empty() && dependents.is_empty() { - output.push_str("No import relationships found in dependency graph.\n\n"); - } - } - - // 2. Git co-change analysis - let git = match GitHistory::discover(project_root) { - Ok(g) => g, - Err(_) => { - output.push_str("[Git repository not found for co-change analysis.]\n"); - return CallToolResult::text(output); - } - }; - - let max_commits = depth * 500; - let history = match git.file_history(file, max_commits) { - Ok(h) => h, - Err(_) => { - output.push_str("No git history found for this file.\n"); - return CallToolResult::text(output); - } - }; - - if !history.commits.is_empty() { - let mut cochange_freq: std::collections::HashMap = - std::collections::HashMap::new(); - for commit in &history.commits { - for changed_file in &commit.files_changed { - if changed_file != file { - *cochange_freq.entry(changed_file.clone()).or_insert(0) += 1; - } - } - } - - let mut related: Vec<(String, usize)> = cochange_freq.into_iter().collect(); - related.sort_by(|a, b| b.1.cmp(&a.1)); - related.truncate(15); - - if !related.is_empty() { - output.push_str(&format!( - "Git co-change analysis ({} commits):\n", - history.commits.len() - )); - for (i, (related_file, freq)) in related.iter().enumerate() { - output.push_str(&format!( - " {}. {} (co-changed {} times)\n", - i + 1, - related_file, - freq, - )); - } - output.push('\n'); - } - } - - if !history.decisions.is_empty() { - output.push_str(&format!( - "Decisions in file history ({}):\n", - history.decisions.len() - )); - for decision in history.decisions.iter().take(5) { - output.push_str(&format!( - " - [{:?}] {}\n", - decision.decision_type, decision.summary - )); - } - } - - CallToolResult::text(output) -} - -async fn handle_remember(args: &Value, project_root: &Path) -> CallToolResult { - let topic = match args.get("topic").and_then(|v| v.as_str()) { - Some(t) => t, - None => return CallToolResult::error("Missing required parameter: topic".to_string()), - }; - let content = match args.get("content").and_then(|v| v.as_str()) { - Some(c) => c, - None => return CallToolResult::error("Missing required parameter: content".to_string()), - }; - let tags: Vec<&str> = args - .get("tags") - .and_then(|v| v.as_array()) - .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect()) - .unwrap_or_default(); - - info!( - "remember: topic={:?} tags={:?} root={:?}", - topic, tags, project_root - ); - - // Persist to .claude-context/knowledge.json - let knowledge_dir = project_root.join(".claude-context"); - let knowledge_file = knowledge_dir.join("knowledge.json"); - - // Load existing knowledge - let mut entries: Vec = if knowledge_file.exists() { - match std::fs::read_to_string(&knowledge_file) { - Ok(data) => serde_json::from_str(&data).unwrap_or_default(), - Err(_) => Vec::new(), - } - } else { - if let Err(e) = std::fs::create_dir_all(&knowledge_dir) { - return CallToolResult::error(format!( - "Failed to create knowledge directory: {}", - e - )); - } - Vec::new() - }; - - let timestamp = chrono::Utc::now().to_rfc3339(); - - // Check for existing entry with same topic - let existing_idx = entries - .iter() - .position(|e| e.get("topic").and_then(|v| v.as_str()) == Some(topic)); - - let entry = json!({ - "topic": topic, - "content": content, - "tags": tags, - "updated_at": timestamp, - }); - - if let Some(idx) = existing_idx { - entries[idx] = entry; - info!("Updated existing knowledge entry: {}", topic); - } else { - entries.push(entry); - info!("Added new knowledge entry: {}", topic); - } - - // Write back - match std::fs::write( - &knowledge_file, - serde_json::to_string_pretty(&entries).unwrap(), - ) { - Ok(_) => CallToolResult::text(format!( - "Remembered '{}' ({} total entries stored)\n\n\ - Content: {}\n\ - Tags: {}\n\ - Saved to: {}", - topic, - entries.len(), - content, - if tags.is_empty() { - "(none)".to_string() - } else { - tags.join(", ") - }, - knowledge_file.display() - )), - Err(e) => CallToolResult::error(format!("Failed to persist knowledge: {}", e)), - } -} - -async fn handle_index_project(args: &Value, project_root: &Path) -> CallToolResult { - let force = args - .get("force") - .and_then(|v| v.as_bool()) - .unwrap_or(false); - - info!("index_project: force={} root={:?}", force, project_root); - - if force { - match force_reindex(project_root) { - Ok((files, symbols, elapsed)) => CallToolResult::text(format!( - "Reindexed project (force=true)\n\n\ - Files indexed: {}\n\ - Symbols found: {}\n\ - Elapsed: {}ms\n\ - Root: {}", - files, symbols, elapsed, project_root.display() - )), - Err(e) => CallToolResult::error(format!("Reindex failed: {}", e)), - } - } else { - // Incremental: just ensure indexed (will index if not already done) - if ensure_indexed(project_root) { - let guard = get_state_lock().lock().unwrap(); - let state = guard.as_ref().unwrap(); - CallToolResult::text(format!( - "Index is up to date.\n\n\ - Files indexed: {}\n\ - Symbols found: {}\n\ - Root: {}\n\n\ - Use force=true to rebuild from scratch.", - state.files_indexed, state.symbols_found, state.project_path.display() - )) - } else { - CallToolResult::error( - "Failed to index project. Check that the project directory exists.".to_string(), - ) - } - } -} - -async fn handle_get_session_patterns(args: &Value, _project_root: &Path) -> CallToolResult { - let min_confidence = args - .get("min_confidence") - .and_then(|v| v.as_f64()) - .unwrap_or(0.0) as f32; - let top = args - .get("top") - .and_then(|v| v.as_u64()) - .unwrap_or(10) as usize; - let pattern_type_filter = args - .get("pattern_type") - .and_then(|v| v.as_str()); - - info!( - "get_session_patterns: min_confidence={} top={} type={:?}", - min_confidence, top, pattern_type_filter - ); - - let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")); - let history_path = home.join(".claude"); - - if !history_path.exists() { - return CallToolResult::text( - "No Claude Code session history found.\n\ - Claude Code stores session data in ~/.claude/\n\ - Patterns will be learned as you use Claude Code." - .to_string(), - ); - } - - let mut tracker = crate::sessions::SessionTracker::new(history_path); - let events = tracker.analyze_all_sessions(); - - // Gather patterns based on filters - let patterns = if min_confidence > 0.0 { - tracker.patterns().confident_patterns(min_confidence) - } else { - tracker.patterns().top_patterns(top) - }; - - // Filter by pattern type if specified - let filtered_patterns: Vec<_> = if let Some(type_filter) = pattern_type_filter { - use crate::sessions::PatternType; - let target_type = match type_filter { - "naming" => Some(PatternType::NamingConvention), - "error" => Some(PatternType::ErrorHandling), - "testing" => Some(PatternType::TestingStyle), - "organization" => Some(PatternType::CodeOrganization), - "import" => Some(PatternType::ImportStyle), - "config" => Some(PatternType::ConfigPreference), - _ => None, - }; - match target_type { - Some(ref t) => patterns.into_iter().filter(|p| &p.pattern_type == t).collect(), - None => patterns, - } - } else { - patterns - }; - - // Build event summary - let decisions = events - .iter() - .filter(|e| e.event_type == crate::sessions::SessionEventType::ArchitecturalDecision) - .count(); - let errors = events - .iter() - .filter(|e| e.event_type == crate::sessions::SessionEventType::ErrorAndFix) - .count(); - let tests = events - .iter() - .filter(|e| e.event_type == crate::sessions::SessionEventType::TestWritten) - .count(); - let refactors = events - .iter() - .filter(|e| e.event_type == crate::sessions::SessionEventType::Refactoring) - .count(); - - let mut output = format!( - "Session Analysis Summary\n\ - ========================\n\ - Events found: {}\n\ - - Architectural decisions: {}\n\ - - Error-and-fix pairs: {}\n\ - - Tests written: {}\n\ - - Refactorings: {}\n\n", - events.len(), - decisions, - errors, - tests, - refactors, - ); - - if filtered_patterns.is_empty() { - output.push_str("No patterns learned yet. More session data needed.\n"); - } else { - output.push_str(&format!("Learned Patterns ({}):\n", filtered_patterns.len())); - for pattern in &filtered_patterns { - output.push_str(&format!( - " [{:.0}%] {} (observed {} times)\n", - pattern.confidence * 100.0, - pattern.description, - pattern.observation_count - )); - } - } - - // Include recent decisions - let recent_decisions: Vec<_> = events - .iter() - .filter(|e| e.event_type == crate::sessions::SessionEventType::ArchitecturalDecision) - .rev() - .take(5) - .collect(); - - if !recent_decisions.is_empty() { - output.push_str("\nRecent Decisions:\n"); - for d in &recent_decisions { - let desc = if d.description.len() > 100 { - format!("{}...", &d.description[..100]) - } else { - d.description.clone() - }; - output.push_str(&format!(" - {}\n", desc)); - } - } - - CallToolResult::text(output) -} diff --git a/tests/approval_test.rs b/tests/approval_test.rs index 8de5e14..19563ea 100644 --- a/tests/approval_test.rs +++ b/tests/approval_test.rs @@ -1,13 +1,15 @@ use code_memory::approval::workflow::{ApprovalStatus, ApprovalWorkflow, PendingKnowledge}; -fn test_database_url() -> String { - std::env::var("TEST_DATABASE_URL") - .unwrap_or_else(|_| "postgresql://localhost/test_db".to_string()) +fn test_database_url() -> Option { + std::env::var("TEST_DATABASE_URL").ok() } #[tokio::test] async fn test_submit_for_approval() { - let database_url = test_database_url(); + let Some(database_url) = test_database_url() else { + eprintln!("skipping Postgres integration test: TEST_DATABASE_URL is not set"); + return; + }; let workflow = ApprovalWorkflow::new(&database_url).await.unwrap(); let pending = PendingKnowledge { @@ -23,7 +25,10 @@ async fn test_submit_for_approval() { #[tokio::test] async fn test_approve_knowledge() { - let database_url = test_database_url(); + let Some(database_url) = test_database_url() else { + eprintln!("skipping Postgres integration test: TEST_DATABASE_URL is not set"); + return; + }; let workflow = ApprovalWorkflow::new(&database_url).await.unwrap(); let pending = PendingKnowledge { @@ -42,7 +47,10 @@ async fn test_approve_knowledge() { #[tokio::test] async fn test_reject_knowledge() { - let database_url = test_database_url(); + let Some(database_url) = test_database_url() else { + eprintln!("skipping Postgres integration test: TEST_DATABASE_URL is not set"); + return; + }; let workflow = ApprovalWorkflow::new(&database_url).await.unwrap(); let pending = PendingKnowledge { diff --git a/tests/workspace_test.rs b/tests/workspace_test.rs index a1af5f4..c822ce7 100644 --- a/tests/workspace_test.rs +++ b/tests/workspace_test.rs @@ -1,8 +1,7 @@ use code_memory::workspace::team_workspace::{TeamWorkspace, WorkspaceConfig}; -fn test_database_url() -> String { - std::env::var("TEST_DATABASE_URL") - .unwrap_or_else(|_| "postgresql://localhost/test_db".to_string()) +fn test_database_url() -> Option { + std::env::var("TEST_DATABASE_URL").ok() } #[tokio::test] @@ -10,7 +9,13 @@ async fn test_create_workspace() { let config = WorkspaceConfig { team_id: "team_123".to_string(), name: "Engineering".to_string(), - database_url: test_database_url(), + database_url: match test_database_url() { + Some(url) => url, + None => { + eprintln!("skipping Postgres integration test: TEST_DATABASE_URL is not set"); + return; + } + }, }; let workspace = TeamWorkspace::new(config).await.unwrap(); @@ -22,7 +27,13 @@ async fn test_add_member() { let config = WorkspaceConfig { team_id: "team_123".to_string(), name: "Engineering".to_string(), - database_url: test_database_url(), + database_url: match test_database_url() { + Some(url) => url, + None => { + eprintln!("skipping Postgres integration test: TEST_DATABASE_URL is not set"); + return; + } + }, }; let workspace = TeamWorkspace::new(config).await.unwrap(); @@ -38,7 +49,13 @@ async fn test_shared_knowledge() { let config = WorkspaceConfig { team_id: "team_123".to_string(), name: "Engineering".to_string(), - database_url: test_database_url(), + database_url: match test_database_url() { + Some(url) => url, + None => { + eprintln!("skipping Postgres integration test: TEST_DATABASE_URL is not set"); + return; + } + }, }; let workspace = TeamWorkspace::new(config).await.unwrap();