diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index c28d60d..4907f8a 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -13,7 +13,7 @@ "name": "kensai", "description": "Multi-agent code review pipeline with adversarial falsification, orchestrated by a stateful MCP server", "source": "./claude/kensai", - "version": "0.1.3", + "version": "0.1.4", "license": "MIT", "keywords": [ "code-review", diff --git a/.github/workflows/kensai-review-mcp.yml b/.github/workflows/kensai-review-mcp.yml index c22389c..5ec38d9 100644 --- a/.github/workflows/kensai-review-mcp.yml +++ b/.github/workflows/kensai-review-mcp.yml @@ -19,40 +19,35 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - - uses: pnpm/action-setup@v4 + - uses: actions/checkout@v6 + - uses: pnpm/action-setup@v6 with: - package_json_file: claude/kensai/mcp/review/package.json - - - uses: actions/setup-node@v4 + version: "latest" + - uses: actions/setup-node@v6 with: node-version: "26" cache: pnpm cache-dependency-path: claude/kensai/mcp/review/pnpm-lock.yaml - run: pnpm install --frozen-lockfile - - run: pnpm lint test: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} strategy: matrix: - node-version: ["lts/*", "26"] + os: ["ubuntu-latest", "windows-latest"] + node-version: ["24", "26"] steps: - - uses: actions/checkout@v4 - - - uses: pnpm/action-setup@v4 + - uses: actions/checkout@v6 + - uses: pnpm/action-setup@v6 with: - package_json_file: claude/kensai/mcp/review/package.json - - - uses: actions/setup-node@v4 + version: "latest" + - uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} cache: pnpm cache-dependency-path: claude/kensai/mcp/review/pnpm-lock.yaml - run: pnpm install --frozen-lockfile - - run: pnpm test diff --git a/claude/kensai/.claude-plugin/plugin.json b/claude/kensai/.claude-plugin/plugin.json index 72e247a..0a66b52 100644 --- a/claude/kensai/.claude-plugin/plugin.json +++ b/claude/kensai/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "kensai", - "version": "0.1.3", + "version": "0.1.4", "description": "Multi-agent code review pipeline with adversarial falsification, orchestrated by a stateful MCP server", "author": { "name": "xobotyi", diff --git a/claude/kensai/mcp/review/package.json b/claude/kensai/mcp/review/package.json index aa51472..0c30297 100644 --- a/claude/kensai/mcp/review/package.json +++ b/claude/kensai/mcp/review/package.json @@ -1,6 +1,6 @@ { "name": "@gaijin/kensai-review-mcp", - "version": "0.1.3", + "version": "0.1.4", "description": "Stateful MCP server for the kensai multi-agent code review pipeline", "license": "MIT", "author": { diff --git a/claude/kensai/mcp/review/src/repofs/pathindex/pathindex.ts b/claude/kensai/mcp/review/src/repofs/pathindex/pathindex.ts index 64d5c44..f663757 100644 --- a/claude/kensai/mcp/review/src/repofs/pathindex/pathindex.ts +++ b/claude/kensai/mcp/review/src/repofs/pathindex/pathindex.ts @@ -1,5 +1,6 @@ import { createReadStream } from "node:fs"; import fs from "node:fs/promises"; +import nativePath from "node:path"; import path from "node:path/posix"; import fuzzysort from "fuzzysort"; @@ -58,7 +59,7 @@ export class PathIndex { readonly #fdPool = new Pool(256); private constructor(rootPath: string) { - this.absRoot = path.resolve(rootPath); + this.absRoot = nativePath.resolve(rootPath); } /** Paths ending with "/" are directories; without are files. */ @@ -162,13 +163,13 @@ export class PathIndex { async #walkDir(dir: string, signal?: AbortSignal): Promise { signal?.throwIfAborted(); - const dirEntries = await fs.readdir(path.resolve(this.absRoot, dir), { withFileTypes: true }); + const dirEntries = await fs.readdir(nativePath.resolve(this.absRoot, dir), { withFileTypes: true }); const dirEntry: IndexEntryDir = { type: "dir", name: path.basename(dir), children: [] }; const work: Promise[] = []; for (const dirent of dirEntries) { const relPath = path.join(dir, dirent.name); - const absPath = path.join(this.absRoot, relPath); + const absPath = nativePath.join(this.absRoot, relPath); if (dirent.isSymbolicLink()) { work.push( diff --git a/claude/kensai/mcp/review/src/repofs/repofs.test.ts b/claude/kensai/mcp/review/src/repofs/repofs.test.ts index 48e7017..8af4e3b 100644 --- a/claude/kensai/mcp/review/src/repofs/repofs.test.ts +++ b/claude/kensai/mcp/review/src/repofs/repofs.test.ts @@ -30,7 +30,7 @@ beforeAll(async () => { await writeFile(join(rootDir, "empty.txt"), ""); await writeFile(join(rootDir, "image.png"), Buffer.from([0x89, 0x50, 0x4e, 0x47])); - await symlink("/tmp", join(rootDir, "escape-link")); + await symlink(tmpdir(), join(rootDir, "escape-link")); await symlink(join(rootDir, "src/main.ts"), join(rootDir, "link-to-file")); await git(["init"], rootDir); @@ -74,6 +74,14 @@ describe("resolve", () => { expect(rfs.resolve("src/../README.md")).toBe("README.md"); }); + it("normalizes backslash separators", () => { + expect(rfs.resolve("src\\main.ts")).toBe("src/main.ts"); + }); + + it("normalizes mixed separators", () => { + expect(rfs.resolve("src\\nested/deep.ts")).toBe("src/nested/deep.ts"); + }); + it("accepts absolute path inside root", () => { expect(rfs.resolve(join(resolve(rootDir), "src/main.ts"))).toBe("src/main.ts"); }); diff --git a/claude/kensai/mcp/review/src/repofs/repofs.ts b/claude/kensai/mcp/review/src/repofs/repofs.ts index 25e560e..dbd3045 100644 --- a/claude/kensai/mcp/review/src/repofs/repofs.ts +++ b/claude/kensai/mcp/review/src/repofs/repofs.ts @@ -64,7 +64,7 @@ export class RepoFs { /** Normalizes any path (absolute or relative) to POSIX relative from root. Throws on escape. */ resolve(p: string): string { const abs = path.resolve(this.#root, path.posix.normalize(p.replaceAll("\\", "/"))); - const rel = path.relative(this.#root, abs); + const rel = path.relative(this.#root, abs).replaceAll("\\", "/"); if (rel.startsWith("..")) { throw new RepoFsError(`path escapes root: ${p}`);