Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions packages/core/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from './vectordb';
import { SemanticSearchResult } from './types';
import { envManager } from './utils/env-manager';
import { canonicalCodebasePath, normalizeRelativePath } from './utils/path';
import * as fs from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';
Expand Down Expand Up @@ -233,8 +234,8 @@ export class Context {
*/
public getCollectionName(codebasePath: string): string {
const isHybrid = this.getIsHybrid();
const normalizedPath = path.resolve(codebasePath);
const hash = crypto.createHash('md5').update(normalizedPath).digest('hex');
const canonicalPath = canonicalCodebasePath(codebasePath);
const hash = crypto.createHash('md5').update(canonicalPath).digest('hex');
const prefix = isHybrid === true ? 'hybrid_code_chunks' : 'code_chunks';
return `${prefix}_${hash.substring(0, 8)}`;
}
Expand Down Expand Up @@ -382,8 +383,9 @@ export class Context {
}

private async deleteFileChunks(collectionName: string, relativePath: string): Promise<void> {
const normalized = normalizeRelativePath(relativePath);
// Escape backslashes for Milvus query expression (Windows path compatibility)
const escapedPath = relativePath.replace(/\\/g, '\\\\');
const escapedPath = normalized.replace(/\\/g, '\\\\');
const results = await this.vectorDatabase.query(
collectionName,
`relativePath == "${escapedPath}"`,
Expand Down Expand Up @@ -822,7 +824,7 @@ export class Context {
throw new Error(`Missing filePath in chunk metadata at index ${index}`);
}

const relativePath = path.relative(codebasePath, chunk.metadata.filePath);
const relativePath = normalizeRelativePath(path.relative(codebasePath, chunk.metadata.filePath));
const fileExtension = path.extname(chunk.metadata.filePath);
const { filePath, startLine, endLine, ...restMetadata } = chunk.metadata;

Expand Down Expand Up @@ -852,7 +854,7 @@ export class Context {
throw new Error(`Missing filePath in chunk metadata at index ${index}`);
}

const relativePath = path.relative(codebasePath, chunk.metadata.filePath);
const relativePath = normalizeRelativePath(path.relative(codebasePath, chunk.metadata.filePath));
const fileExtension = path.extname(chunk.metadata.filePath);
const { filePath, startLine, endLine, ...restMetadata } = chunk.metadata;

Expand Down
10 changes: 5 additions & 5 deletions packages/core/src/sync/synchronizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as path from 'path';
import * as crypto from 'crypto';
import { MerkleDAG } from './merkle';
import * as os from 'os';
import { canonicalCodebasePath } from '../utils/path';

export class FileSynchronizer {
private fileHashes: Map<string, string>;
Expand All @@ -22,9 +23,8 @@ export class FileSynchronizer {
private getSnapshotPath(codebasePath: string): string {
const homeDir = os.homedir();
const merkleDir = path.join(homeDir, '.context', 'merkle');

const normalizedPath = path.resolve(codebasePath);
const hash = crypto.createHash('md5').update(normalizedPath).digest('hex');
const canonicalPath = canonicalCodebasePath(codebasePath);
const hash = crypto.createHash('md5').update(canonicalPath).digest('hex');

return path.join(merkleDir, `${hash}.json`);
}
Expand Down Expand Up @@ -329,8 +329,8 @@ export class FileSynchronizer {
static async deleteSnapshot(codebasePath: string): Promise<void> {
const homeDir = os.homedir();
const merkleDir = path.join(homeDir, '.context', 'merkle');
const normalizedPath = path.resolve(codebasePath);
const hash = crypto.createHash('md5').update(normalizedPath).digest('hex');
const canonicalPath = canonicalCodebasePath(codebasePath);
const hash = crypto.createHash('md5').update(canonicalPath).digest('hex');
const snapshotPath = path.join(merkleDir, `${hash}.json`);

try {
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { EnvManager, envManager } from './env-manager';
export { EnvManager, envManager } from './env-manager';
export { normalizeCodebasePath, canonicalCodebasePath, normalizeRelativePath } from './path';
56 changes: 56 additions & 0 deletions packages/core/src/utils/path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import * as path from 'path';

/**
* Normalize a codebase path for file system operations.
* Handles: MSYS/Git Bash (/c/Users/...), forward slashes, mixed,
* trailing slashes. Preserves original case.
*/
export function normalizeCodebasePath(inputPath: string): string {
let p = inputPath;

// 1. Convert MSYS/Git Bash paths: /c/Users/... -> C:\Users\...
if (process.platform === 'win32') {
const msysMatch = p.match(/^\/([a-zA-Z])(\/.*)?$/);
if (msysMatch) {
const drive = msysMatch[1].toUpperCase();
const rest = msysMatch[2] || '';
p = `${drive}:${rest}`;
}
}

// 2. Resolve to absolute (handles relative paths, .., etc.)
p = path.resolve(p);

// 3. Windows-specific slash normalization
if (process.platform === 'win32') {
p = p.replace(/\//g, '\\'); // forward -> backslash
p = p.replace(/\\{2,}/g, '\\'); // collapse multiple backslashes
}

// 4. Remove trailing separator
if (p.length > 1 && (p.endsWith('/') || p.endsWith('\\'))) {
p = p.slice(0, -1);
}

return p;
}

/**
* Canonical codebase path for hashing/identity comparison.
* Same as normalizeCodebasePath + lowercase on Windows (case-insensitive FS).
* Use this ONLY for hash inputs, not for FS operations or display.
*/
export function canonicalCodebasePath(inputPath: string): string {
let p = normalizeCodebasePath(inputPath);
if (process.platform === 'win32') {
p = p.toLowerCase();
}
return p;
}

/**
* Normalize a relative file path to use forward slashes (for DB storage/queries).
*/
export function normalizeRelativePath(relativePath: string): string {
return relativePath.replace(/\\/g, '/');
}
14 changes: 4 additions & 10 deletions packages/mcp/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as path from "path";
import { normalizeCodebasePath } from "@zilliz/claude-context-core";

/**
* Truncate content to specified length
Expand All @@ -11,17 +11,11 @@ export function truncateContent(content: string, maxLength: number): string {
}

/**
* Ensure path is absolute. If relative path is provided, resolve it properly.
* Ensure path is absolute and normalized for consistent cross-client usage.
* Handles MSYS/Git Bash paths, forward slashes, mixed separators on Windows.
*/
export function ensureAbsolutePath(inputPath: string): string {
// If already absolute, return as is
if (path.isAbsolute(inputPath)) {
return inputPath;
}

// For relative paths, resolve to absolute path
const resolved = path.resolve(inputPath);
return resolved;
return normalizeCodebasePath(inputPath);
}

export function trackCodebasePath(codebasePath: string): void {
Expand Down