- fix(mcp): use ListToolsRequestSchema/CallToolRequestSchema instead of string literals (Issue #9, PR #25) - fix(mcp): handleToolCall reads dry_run from params instead of hardcoding false (#22 Bug #11) - fix(search): keyword search returns best chunk per page via DISTINCT ON, not all chunks (#22 Bug #8) - fix(search): dedup layer 1 keeps top 3 chunks per page instead of collapsing to 1 (#22 Bug #12) - fix(engine): transaction uses scoped engine via Object.create, no shared state mutation (#22 Bug #2) - fix(engine): upsertChunks uses UPSERT instead of DELETE+INSERT, preserves existing embeddings (#22 Bug #1) - fix(slugs): validateSlug normalizes to lowercase, pathToSlug lowercases consistently (#22 Bug #4) - schema: add unique index on content_chunks(page_id, chunk_index) for UPSERT support - schema: add access_tokens and mcp_request_log tables via migration Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
118 lines
3.1 KiB
TypeScript
118 lines
3.1 KiB
TypeScript
/**
|
|
* Sync utilities — pure functions for git diff parsing, filtering, and slug management.
|
|
*
|
|
* SYNC DATA FLOW:
|
|
* git diff --name-status -M LAST..HEAD
|
|
* │
|
|
* buildSyncManifest() → parse A/M/D/R lines
|
|
* │
|
|
* isSyncable() → filter to .md pages only
|
|
* │
|
|
* pathToSlug() → convert file paths to page slugs
|
|
*/
|
|
|
|
export interface SyncManifest {
|
|
added: string[];
|
|
modified: string[];
|
|
deleted: string[];
|
|
renamed: Array<{ from: string; to: string }>;
|
|
}
|
|
|
|
export interface RawManifestEntry {
|
|
action: 'A' | 'M' | 'D' | 'R';
|
|
path: string;
|
|
oldPath?: string;
|
|
}
|
|
|
|
/**
|
|
* Parse the output of `git diff --name-status -M LAST..HEAD` into structured entries.
|
|
*
|
|
* Input format (tab-separated):
|
|
* A path/to/new-file.md
|
|
* M path/to/modified-file.md
|
|
* D path/to/deleted-file.md
|
|
* R100 old/path.md new/path.md
|
|
*/
|
|
export function buildSyncManifest(gitDiffOutput: string): SyncManifest {
|
|
const manifest: SyncManifest = {
|
|
added: [],
|
|
modified: [],
|
|
deleted: [],
|
|
renamed: [],
|
|
};
|
|
|
|
const lines = gitDiffOutput.split('\n');
|
|
|
|
for (const line of lines) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed) continue;
|
|
|
|
const parts = trimmed.split('\t');
|
|
if (parts.length < 2) continue;
|
|
|
|
const action = parts[0];
|
|
const path = parts[parts.length === 3 ? 2 : 1]; // For renames, new path is 3rd column
|
|
|
|
if (action === 'A') {
|
|
manifest.added.push(path);
|
|
} else if (action === 'M') {
|
|
manifest.modified.push(path);
|
|
} else if (action === 'D') {
|
|
manifest.deleted.push(parts[1]);
|
|
} else if (action.startsWith('R')) {
|
|
// Rename: R100\told-path\tnew-path
|
|
const oldPath = parts[1];
|
|
const newPath = parts[2];
|
|
if (oldPath && newPath) {
|
|
manifest.renamed.push({ from: oldPath, to: newPath });
|
|
}
|
|
}
|
|
}
|
|
|
|
return manifest;
|
|
}
|
|
|
|
/**
|
|
* Filter a file path to determine if it should be synced to GBrain.
|
|
*/
|
|
export function isSyncable(path: string): boolean {
|
|
// Must be .md
|
|
if (!path.endsWith('.md')) return false;
|
|
|
|
// Skip hidden directories
|
|
if (path.split('/').some(p => p.startsWith('.'))) return false;
|
|
|
|
// Skip .raw/ sidecar directories
|
|
if (path.includes('.raw/')) return false;
|
|
|
|
// Skip meta files that aren't pages
|
|
const skipFiles = ['schema.md', 'index.md', 'log.md', 'README.md'];
|
|
const basename = path.split('/').pop() || '';
|
|
if (skipFiles.includes(basename)) return false;
|
|
|
|
// Skip ops/ directory
|
|
if (path.startsWith('ops/')) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Convert a repo-relative file path to a GBrain page slug.
|
|
*
|
|
* Examples:
|
|
* people/pedro-franceschi.md → people/pedro-franceschi
|
|
* daily/2026-04-05.md → daily/2026-04-05
|
|
* notes.md → notes
|
|
*/
|
|
export function pathToSlug(filePath: string, repoPrefix?: string): string {
|
|
// Strip .md extension
|
|
let slug = filePath.replace(/\.md$/, '');
|
|
// Normalize separators
|
|
slug = slug.replace(/\\/g, '/');
|
|
// Strip leading slash
|
|
slug = slug.replace(/^\//, '');
|
|
// Add repo prefix for multi-repo setups
|
|
if (repoPrefix) slug = `${repoPrefix}/${slug}`;
|
|
return slug.toLowerCase();
|
|
}
|