brainbank 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +149 -16
  2. package/dist/{types-Da_zLLOl.d.ts → base-9vfWRHCV.d.ts} +131 -31
  3. package/dist/{chunk-YGSEUWLV.js → chunk-6MFTQV3O.js} +911 -674
  4. package/dist/chunk-6MFTQV3O.js.map +1 -0
  5. package/dist/chunk-7JCEW7LT.js +266 -0
  6. package/dist/chunk-7JCEW7LT.js.map +1 -0
  7. package/dist/{chunk-GOUBW7UA.js → chunk-F6SJ3U4H.js} +98 -34
  8. package/dist/chunk-F6SJ3U4H.js.map +1 -0
  9. package/dist/{chunk-MJ3Y24H6.js → chunk-FJJY4H2Y.js} +11 -11
  10. package/dist/chunk-FJJY4H2Y.js.map +1 -0
  11. package/dist/{chunk-3GAIDXRW.js → chunk-GUT5MSJT.js} +5 -11
  12. package/dist/chunk-GUT5MSJT.js.map +1 -0
  13. package/dist/{chunk-2P3EGY6S.js → chunk-QNHBCOKB.js} +2 -2
  14. package/dist/chunk-QNHBCOKB.js.map +1 -0
  15. package/dist/{chunk-4ZKBQ33J.js → chunk-V4UJKXPK.js} +23 -5
  16. package/dist/chunk-V4UJKXPK.js.map +1 -0
  17. package/dist/chunk-WR4WXKJT.js +723 -0
  18. package/dist/chunk-WR4WXKJT.js.map +1 -0
  19. package/dist/{chunk-Z5SU54HP.js → chunk-X6645UVR.js} +3 -3
  20. package/dist/chunk-X6645UVR.js.map +1 -0
  21. package/dist/cli.js +150 -100
  22. package/dist/cli.js.map +1 -1
  23. package/dist/code.d.ts +5 -5
  24. package/dist/code.js +1 -1
  25. package/dist/docs.d.ts +4 -6
  26. package/dist/docs.js +1 -1
  27. package/dist/git.d.ts +5 -5
  28. package/dist/git.js +1 -1
  29. package/dist/index.d.ts +95 -104
  30. package/dist/index.js +13 -13
  31. package/dist/memory.d.ts +5 -7
  32. package/dist/memory.js +9 -12
  33. package/dist/memory.js.map +1 -1
  34. package/dist/notes.d.ts +4 -6
  35. package/dist/notes.js +7 -10
  36. package/dist/notes.js.map +1 -1
  37. package/dist/{openai-PCTYLOWI.js → openai-CYDMYX7X.js} +2 -2
  38. package/package.json +24 -4
  39. package/dist/chunk-2P3EGY6S.js.map +0 -1
  40. package/dist/chunk-3GAIDXRW.js.map +0 -1
  41. package/dist/chunk-4ZKBQ33J.js.map +0 -1
  42. package/dist/chunk-EDKSKLX4.js +0 -490
  43. package/dist/chunk-EDKSKLX4.js.map +0 -1
  44. package/dist/chunk-GOUBW7UA.js.map +0 -1
  45. package/dist/chunk-MJ3Y24H6.js.map +0 -1
  46. package/dist/chunk-N6ZMBFDE.js +0 -224
  47. package/dist/chunk-N6ZMBFDE.js.map +0 -1
  48. package/dist/chunk-YGSEUWLV.js.map +0 -1
  49. package/dist/chunk-Z5SU54HP.js.map +0 -1
  50. /package/dist/{openai-PCTYLOWI.js.map → openai-CYDMYX7X.js.map} +0 -0
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/indexers/code-indexer.ts","../src/indexers/chunker.ts","../src/indexers/languages.ts","../src/plugins/code.ts"],"sourcesContent":["/**\n * BrainBank — Code Indexer\n * \n * Walks a repository, chunks source files semantically,\n * embeds each chunk, and stores in SQLite + HNSW.\n * Incremental: only re-indexes files that changed (by content hash).\n */\n\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { CodeChunker } from './chunker.ts';\nimport { SUPPORTED_EXTENSIONS, IGNORE_DIRS, isIgnoredDir, isIgnoredFile } from './languages.ts';\nimport type { Database } from '../storage/database.ts';\nimport type { EmbeddingProvider, ProgressCallback, IndexResult } from '../types.ts';\nimport type { HNSWIndex } from '../vector/hnsw.ts';\n\nexport interface CodeIndexerDeps {\n db: Database;\n hnsw: HNSWIndex;\n vectorCache: Map<number, Float32Array>;\n embedding: EmbeddingProvider;\n}\n\nexport interface CodeIndexOptions {\n forceReindex?: boolean;\n onProgress?: ProgressCallback;\n}\n\nexport class CodeIndexer {\n private _chunker = new CodeChunker();\n private _deps: CodeIndexerDeps;\n private _repoPath: string;\n private _maxFileSize: number;\n\n constructor(repoPath: string, deps: CodeIndexerDeps, maxFileSize: number = 512_000) {\n this._deps = deps;\n this._repoPath = repoPath;\n this._maxFileSize = maxFileSize;\n }\n\n /**\n * Index all supported files in the repository.\n * Skips unchanged files (same content hash).\n */\n async index(options: CodeIndexOptions = {}): Promise<IndexResult> {\n const { forceReindex = false, onProgress } = options;\n const files = this._walkRepo(this._repoPath);\n let indexed = 0, skipped = 0, totalChunks = 0;\n\n for (let i = 0; i < files.length; i++) {\n const filePath = files[i];\n const rel = path.relative(this._repoPath, filePath);\n onProgress?.(rel, i + 1, files.length);\n\n let content: string;\n try { content = fs.readFileSync(filePath, 'utf-8'); }\n catch { continue; }\n\n const hash = this._hash(content);\n const existing = this._deps.db.prepare(\n 'SELECT file_hash FROM indexed_files WHERE file_path = ?'\n ).get(rel) as any;\n\n if (!forceReindex && existing?.file_hash === hash) {\n skipped++;\n continue;\n }\n\n // Delete old chunks if re-indexing\n if (existing) {\n this._deps.db.prepare('DELETE FROM code_chunks WHERE file_path = ?').run(rel);\n }\n\n const ext = path.extname(filePath).toLowerCase();\n const language = SUPPORTED_EXTENSIONS[ext] ?? 'text';\n const chunks = this._chunker.chunk(rel, content, language);\n\n for (const chunk of chunks) {\n // Build embedding text with file context\n const text = [\n `File: ${rel}`,\n chunk.name ? `${chunk.chunkType}: ${chunk.name}` : chunk.chunkType,\n chunk.content,\n ].join('\\n');\n\n const vec = await this._deps.embedding.embed(text);\n\n const result = this._deps.db.prepare(\n `INSERT INTO code_chunks (file_path, chunk_type, name, start_line, end_line, content, language, file_hash)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)`\n ).run(rel, chunk.chunkType, chunk.name ?? null, chunk.startLine, chunk.endLine, chunk.content, language, hash);\n\n const id = Number(result.lastInsertRowid);\n this._deps.db.prepare(\n 'INSERT INTO code_vectors (chunk_id, embedding) VALUES (?, ?)'\n ).run(id, Buffer.from(vec.buffer));\n\n this._deps.hnsw.add(vec, id);\n this._deps.vectorCache.set(id, vec);\n totalChunks++;\n }\n\n // Mark file as indexed\n this._deps.db.prepare(\n 'INSERT OR REPLACE INTO indexed_files (file_path, file_hash) VALUES (?, ?)'\n ).run(rel, hash);\n indexed++;\n }\n\n return { indexed, skipped, chunks: totalChunks };\n }\n\n // ── File Walker ─────────────────────────────────\n\n private _walkRepo(dir: string, files: string[] = []): string[] {\n let entries: fs.Dirent[];\n try { entries = fs.readdirSync(dir, { withFileTypes: true }); }\n catch { return files; }\n\n for (const entry of entries) {\n if (entry.isDirectory()) {\n if (isIgnoredDir(entry.name)) continue;\n this._walkRepo(path.join(dir, entry.name), files);\n } else if (entry.isFile()) {\n if (isIgnoredFile(entry.name)) continue;\n const ext = path.extname(entry.name).toLowerCase();\n if (!(ext in SUPPORTED_EXTENSIONS)) continue;\n\n const full = path.join(dir, entry.name);\n try {\n if (fs.statSync(full).size <= this._maxFileSize) {\n files.push(full);\n }\n } catch {}\n }\n }\n return files;\n }\n\n // ── FNV-1a Hash ─────────────────────────────────\n\n private _hash(content: string): string {\n let h = 2166136261;\n for (let i = 0; i < content.length; i++) {\n h ^= content.charCodeAt(i);\n h = (h * 16777619) >>> 0;\n }\n return h.toString(16);\n }\n}\n","/**\n * BrainBank — Code Chunker\n * \n * Language-aware code splitting into semantic blocks.\n * Detects functions, classes, and methods using regex + brace balancing.\n * Falls back to sliding window for unsupported languages.\n */\n\nimport type { CodeChunk } from '../types.ts';\n\n// ── Configuration ───────────────────────────────────\n\nexport interface ChunkerConfig {\n /** Max lines per chunk. Default: 80 */\n maxLines?: number;\n /** Min lines for a detected block to be a chunk. Default: 3 */\n minLines?: number;\n /** Overlap between adjacent generic chunks. Default: 5 */\n overlap?: number;\n}\n\n// ── CodeChunker ─────────────────────────────────────\n\nexport class CodeChunker {\n private MAX: number;\n private MIN: number;\n private OVERLAP: number;\n\n constructor(config: ChunkerConfig = {}) {\n this.MAX = config.maxLines ?? 80;\n this.MIN = config.minLines ?? 3;\n this.OVERLAP = config.overlap ?? 5;\n }\n\n /**\n * Split file content into semantic chunks.\n * Small files (< maxLines) become a single chunk.\n * For JS/TS/Python: detects functions and classes.\n * For other languages: sliding window with overlap.\n */\n chunk(filePath: string, content: string, language: string): CodeChunk[] {\n const lines = content.split('\\n');\n\n // Small file → single chunk\n if (lines.length <= this.MAX) {\n return [{\n filePath,\n chunkType: 'file',\n startLine: 1,\n endLine: lines.length,\n content: content.trim(),\n language,\n }];\n }\n\n switch (language) {\n case 'typescript':\n case 'javascript':\n return this._chunkJS(filePath, lines, language);\n case 'python':\n return this._chunkPython(filePath, lines, language);\n default:\n return this._chunkGeneric(filePath, lines, language);\n }\n }\n\n // ── JS / TS Strategy ────────────────────────────\n\n private _chunkJS(filePath: string, lines: string[], language: string): CodeChunk[] {\n const chunks: CodeChunk[] = [];\n const funcRe = /^(?:export\\s+)?(?:async\\s+)?function\\s+(\\w+)/;\n const constFuncRe = /^(?:export\\s+)?(?:const|let|var)\\s+(\\w+)\\s*=\\s*(?:async\\s+)?\\(/;\n const classRe = /^(?:export\\s+)?(?:abstract\\s+)?class\\s+(\\w+)/;\n const arrowRe = /^(?:export\\s+)?(?:const|let)\\s+(\\w+)\\s*=\\s*(?:async\\s+)?\\([^)]*\\)\\s*(?::\\s*\\S+)?\\s*=>/;\n const interfaceRe = /^(?:export\\s+)?(?:interface|type)\\s+(\\w+)/;\n\n let i = 0;\n while (i < lines.length) {\n const line = lines[i].trim();\n const fm = line.match(funcRe) || line.match(constFuncRe) || line.match(arrowRe);\n const cm = line.match(classRe);\n const im = line.match(interfaceRe);\n\n if (fm || cm || im) {\n const name = fm?.[1] || cm?.[1] || im?.[1] || 'default';\n const type = cm ? 'class' : im ? 'interface' : 'function';\n const start = i;\n const end = this._findBlockEnd(lines, i);\n\n if (end - start >= this.MIN) {\n if (end - start > this.MAX) {\n chunks.push(...this._splitLarge(filePath, lines, start, end, name, type, language));\n } else {\n const content = lines.slice(start, end + 1).join('\\n').trim();\n chunks.push({\n filePath,\n chunkType: type,\n name,\n startLine: start + 1,\n endLine: end + 1,\n content,\n language,\n });\n }\n i = end + 1;\n continue;\n }\n }\n i++;\n }\n\n // If we found semantic chunks, filter noise and return\n if (chunks.length > 0) {\n return chunks.filter(c => c.content.length > 20);\n }\n\n // Fallback to generic\n return this._chunkGeneric(filePath, lines, language);\n }\n\n // ── Python Strategy ─────────────────────────────\n\n private _chunkPython(filePath: string, lines: string[], language: string): CodeChunk[] {\n const chunks: CodeChunk[] = [];\n let i = 0;\n\n while (i < lines.length) {\n const funcMatch = lines[i].match(/^(?:async\\s+)?def\\s+(\\w+)/);\n const classMatch = lines[i].match(/^class\\s+(\\w+)/);\n\n if (funcMatch || classMatch) {\n const name = funcMatch?.[1] || classMatch?.[1]!;\n const type = classMatch ? 'class' : 'function';\n const start = i;\n const baseIndent = (lines[i].match(/^(\\s*)/) ?? ['', ''])[1].length;\n\n let end = i + 1;\n while (end < lines.length) {\n const line = lines[end];\n if (line.trim() !== '') {\n const indent = (line.match(/^(\\s*)/) ?? ['', ''])[1].length;\n if (indent <= baseIndent) break;\n }\n end++;\n }\n end = Math.min(end - 1, lines.length - 1);\n\n if (end - start >= this.MIN) {\n const content = lines.slice(start, end + 1).join('\\n').trim();\n chunks.push({\n filePath,\n chunkType: type,\n name,\n startLine: start + 1,\n endLine: end + 1,\n content,\n language,\n });\n }\n i = end + 1;\n continue;\n }\n i++;\n }\n\n return chunks.length > 0 ? chunks : this._chunkGeneric(filePath, lines, language);\n }\n\n // ── Generic Strategy (sliding window) ───────────\n\n private _chunkGeneric(filePath: string, lines: string[], language: string): CodeChunk[] {\n const chunks: CodeChunk[] = [];\n const step = this.MAX - this.OVERLAP;\n\n for (let s = 0; s < lines.length; s += step) {\n const e = Math.min(s + this.MAX, lines.length);\n const content = lines.slice(s, e).join('\\n').trim();\n if (content.length > 20) {\n chunks.push({\n filePath,\n chunkType: 'block',\n startLine: s + 1,\n endLine: e,\n content,\n language,\n });\n }\n if (e >= lines.length) break;\n }\n\n return chunks;\n }\n\n // ── Block End Detection (brace balance) ─────────\n\n private _findBlockEnd(lines: string[], start: number): number {\n let depth = 0;\n let foundOpen = false;\n\n for (let i = start; i < lines.length; i++) {\n for (const c of lines[i]) {\n if (c === '{') { depth++; foundOpen = true; }\n if (c === '}') depth--;\n }\n if (foundOpen && depth === 0) return i;\n }\n\n // No brace-balanced end found — take up to MAX lines\n return Math.min(start + this.MAX, lines.length - 1);\n }\n\n // ── Split Large Blocks ──────────────────────────\n\n private _splitLarge(\n filePath: string,\n lines: string[],\n start: number,\n end: number,\n name: string,\n type: string,\n language: string,\n ): CodeChunk[] {\n const chunks: CodeChunk[] = [];\n const step = this.MAX - this.OVERLAP;\n let part = 1;\n\n for (let s = start; s <= end; s += step) {\n const e = Math.min(s + this.MAX, end + 1);\n const content = lines.slice(s, e).join('\\n').trim();\n chunks.push({\n filePath,\n chunkType: type,\n name: `${name} (part ${part++})`,\n startLine: s + 1,\n endLine: e,\n content,\n language,\n });\n if (e > end) break;\n }\n\n return chunks;\n }\n}\n","/**\n * BrainBank — Language Registry\n * \n * Supported file extensions, language mappings, and ignore lists.\n * Controls which files get indexed and how they're chunked.\n */\n\n// ── Supported Extensions ────────────────────────────\n\nexport const SUPPORTED_EXTENSIONS: Record<string, string> = {\n // TypeScript / JavaScript\n '.ts': 'typescript',\n '.tsx': 'typescript',\n '.js': 'javascript',\n '.jsx': 'javascript',\n '.mjs': 'javascript',\n '.cjs': 'javascript',\n\n // Systems\n '.go': 'go',\n '.rs': 'rust',\n '.cpp': 'cpp',\n '.cc': 'cpp',\n '.c': 'c',\n '.h': 'c',\n '.hpp': 'cpp',\n\n // JVM\n '.java': 'java',\n '.kt': 'kotlin',\n '.scala': 'scala',\n\n // Scripting\n '.py': 'python',\n '.rb': 'ruby',\n '.php': 'php',\n '.lua': 'lua',\n '.sh': 'bash',\n '.bash': 'bash',\n '.zsh': 'bash',\n\n // Web\n '.html': 'html',\n '.css': 'css',\n '.scss': 'scss',\n '.less': 'less',\n '.svelte': 'svelte',\n '.vue': 'vue',\n\n // Data / Config\n '.json': 'json',\n '.yaml': 'yaml',\n '.yml': 'yaml',\n '.toml': 'toml',\n '.xml': 'xml',\n '.graphql': 'graphql',\n '.gql': 'graphql',\n\n // Docs\n '.md': 'markdown',\n '.mdx': 'markdown',\n\n // Database\n '.sql': 'sql',\n '.prisma': 'prisma',\n\n // Other\n '.swift': 'swift',\n '.dart': 'dart',\n '.r': 'r',\n '.ex': 'elixir',\n '.exs': 'elixir',\n '.erl': 'erlang',\n '.zig': 'zig',\n};\n\n// ── Ignore Directories ──────────────────────────────\n\nexport const IGNORE_DIRS = new Set([\n // Package managers\n 'node_modules',\n 'bower_components',\n '.pnpm',\n\n // Build output\n 'dist',\n 'build',\n 'out',\n '.next',\n '.nuxt',\n '.output',\n '.svelte-kit',\n\n // Version control\n '.git',\n '.hg',\n '.svn',\n\n // IDE / Editor\n '.idea',\n '.vscode',\n\n // Runtime / Cache\n '__pycache__',\n '.pytest_cache',\n 'venv',\n '.venv',\n '.env',\n '.tox',\n\n // Coverage / Test artifacts\n 'coverage',\n '.nyc_output',\n 'htmlcov',\n\n // Compiled\n 'target', // Rust, Java\n '.cargo',\n 'vendor', // Go, PHP\n\n // AI / Model cache\n '.model-cache',\n '.brainbank',\n\n // OS\n '.DS_Store',\n]);\n\n// ── Ignore Files ────────────────────────────────────\n\nexport const IGNORE_FILES = new Set([\n 'package-lock.json',\n 'yarn.lock',\n 'pnpm-lock.yaml',\n 'bun.lockb',\n 'Cargo.lock',\n 'Gemfile.lock',\n 'poetry.lock',\n 'composer.lock',\n 'go.sum',\n]);\n\n// ── Helpers ─────────────────────────────────────────\n\nimport path from 'node:path';\n\n/** Check if a file extension is supported for indexing. */\nexport function isSupported(filePath: string): boolean {\n const ext = path.extname(filePath).toLowerCase();\n return ext in SUPPORTED_EXTENSIONS;\n}\n\n/** Get the language name for a file. Returns undefined if not supported. */\nexport function getLanguage(filePath: string): string | undefined {\n const ext = path.extname(filePath).toLowerCase();\n return SUPPORTED_EXTENSIONS[ext];\n}\n\n/** Check if a directory name should be ignored. */\nexport function isIgnoredDir(dirName: string): boolean {\n return IGNORE_DIRS.has(dirName) || dirName.startsWith('.');\n}\n\n/** Check if a filename should be ignored. */\nexport function isIgnoredFile(fileName: string): boolean {\n return IGNORE_FILES.has(fileName);\n}\n","/**\n * BrainBank — Code Module\n * \n * Language-aware code indexing for 30+ languages.\n * \n * import { BrainBank } from 'brainbank';\n * import { code } from 'brainbank/code';\n * \n * const brain = new BrainBank().use(code({ repoPath: '.' }));\n * \n * // Multi-repo: namespace to avoid key collisions\n * brain\n * .use(code({ repoPath: './frontend', name: 'code:frontend' }))\n * .use(code({ repoPath: './backend', name: 'code:backend' }));\n */\n\nimport type { BrainBankModule, ModuleContext } from './types.ts';\nimport type { HNSWIndex } from '../vector/hnsw.ts';\nimport { CodeIndexer } from '../indexers/code-indexer.ts';\nimport type { IndexResult, ProgressCallback } from '../types.ts';\n\nexport interface CodeModuleOptions {\n /** Repository path to index. Default: '.' */\n repoPath?: string;\n /** Maximum file size in bytes. Default: from config */\n maxFileSize?: number;\n /** Custom indexer name for multi-repo (e.g. 'code:frontend'). Default: 'code' */\n name?: string;\n}\n\nclass CodeModuleImpl implements BrainBankModule {\n readonly name: string;\n hnsw!: HNSWIndex;\n indexer!: CodeIndexer;\n vecCache = new Map<number, Float32Array>();\n\n constructor(private opts: CodeModuleOptions = {}) {\n this.name = opts.name ?? 'code';\n }\n\n async initialize(ctx: ModuleContext): Promise<void> {\n // Use shared HNSW so all code indexers (code, code:frontend, etc.) share one index\n const shared = await ctx.getOrCreateSharedHnsw('code');\n this.hnsw = shared.hnsw;\n this.vecCache = shared.vecCache;\n\n // Only load vectors once (first code indexer to initialize)\n if (shared.isNew) {\n ctx.loadVectors('code_vectors', 'chunk_id', this.hnsw, this.vecCache);\n }\n\n const repoPath = this.opts.repoPath ?? ctx.config.repoPath;\n this.indexer = new CodeIndexer(repoPath, {\n db: ctx.db,\n hnsw: this.hnsw,\n vectorCache: this.vecCache,\n embedding: ctx.embedding,\n }, this.opts.maxFileSize ?? ctx.config.maxFileSize);\n }\n\n async index(options: {\n forceReindex?: boolean;\n onProgress?: ProgressCallback;\n } = {}): Promise<IndexResult> {\n return this.indexer.index(options);\n }\n\n stats(): Record<string, any> {\n return { hnswSize: this.hnsw.size };\n }\n}\n\n/** Create a code indexing module. */\nexport function code(opts?: CodeModuleOptions): BrainBankModule {\n return new CodeModuleImpl(opts);\n}\n"],"mappings":"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","names":["path","path"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/indexers/doc-indexer.ts","../src/plugins/docs.ts"],"sourcesContent":["/**\n * BrainBank — Document Indexer\n * \n * Indexes generic document collections (markdown, text, etc.)\n * with heading-aware smart chunking, inspired by qmd.\n * \n * const indexer = new DocIndexer(db, embedding, hnsw, vecCache);\n * await indexer.indexCollection('notes', '/path/to/notes', '**\\/*.md');\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { createHash } from 'node:crypto';\nimport { glob } from 'node:fs/promises';\nimport type { Database } from '../storage/database.ts';\nimport type { EmbeddingProvider, VectorIndex } from '../types.ts';\nimport type { HNSWIndex } from '../vector/hnsw.ts';\n\n// ── Break Point Scoring (qmd-inspired) ──────────────\n\ninterface BreakPoint {\n pos: number; // character position\n score: number; // break quality (higher = better)\n}\n\nconst BREAK_SCORES: [RegExp, number][] = [\n [/^# /, 100], // H1\n [/^## /, 90], // H2\n [/^### /, 80], // H3\n [/^#### /, 70], // H4\n [/^##### /, 60], // H5\n [/^###### /, 50], // H6\n [/^```/, 80], // Code fence\n [/^---$/, 60], // Horizontal rule\n [/^\\*\\*\\*$/, 60], // Horizontal rule alt\n [/^$/, 20], // Blank line (paragraph break)\n [/^[-*+] /, 5], // List item\n];\n\n// ── Chunk Target ────────────────────────────────────\n\nconst TARGET_CHARS = 3000; // ~900 tokens\nconst WINDOW_CHARS = 600; // search window before cutoff\nconst MIN_CHUNK_CHARS = 200; // don't create tiny chunks\n\n// ── DocIndexer ──────────────────────────────────────\n\nexport class DocIndexer {\n constructor(\n private _db: Database,\n private _embedding: EmbeddingProvider,\n private _hnsw: HNSWIndex,\n private _vecCache: Map<number, Float32Array>,\n ) {}\n\n /**\n * Index all documents in a collection.\n * Incremental — skips unchanged files (by content hash).\n */\n async indexCollection(\n collection: string,\n dirPath: string,\n pattern: string = '**/*.md',\n options: {\n ignore?: string[];\n onProgress?: (file: string, current: number, total: number) => void;\n } = {},\n ): Promise<{ indexed: number; skipped: number; chunks: number }> {\n // Resolve absolute path\n const absDir = path.resolve(dirPath);\n if (!fs.existsSync(absDir)) {\n throw new Error(`Collection path does not exist: ${absDir}`);\n }\n\n // Find files matching pattern\n const files: string[] = [];\n for await (const entry of glob(pattern, { cwd: absDir })) {\n const fullPath = path.join(absDir, entry);\n const stat = fs.statSync(fullPath);\n if (stat.isFile()) {\n // Check ignore patterns\n const shouldIgnore = options.ignore?.some(ig => {\n const igRegex = new RegExp(ig.replace(/\\*\\*/g, '.*').replace(/\\*/g, '[^/]*'));\n return igRegex.test(entry);\n });\n if (!shouldIgnore) {\n files.push(entry); // relative path\n }\n }\n }\n\n let indexed = 0;\n let skipped = 0;\n let totalChunks = 0;\n\n for (let i = 0; i < files.length; i++) {\n const relPath = files[i];\n const absPath = path.join(absDir, relPath);\n\n options.onProgress?.(relPath, i + 1, files.length);\n\n // Read content and hash\n const content = fs.readFileSync(absPath, 'utf-8');\n const hash = createHash('sha256').update(content).digest('hex').slice(0, 16);\n\n // Check if already indexed with same hash\n const existing = this._db.prepare(\n 'SELECT id FROM doc_chunks WHERE collection = ? AND file_path = ? AND content_hash = ? LIMIT 1'\n ).get(collection, relPath, hash) as any;\n\n if (existing) {\n skipped++;\n continue;\n }\n\n // Remove old chunks for this file\n this._db.prepare(\n 'DELETE FROM doc_chunks WHERE collection = ? AND file_path = ?'\n ).run(collection, relPath);\n\n // Extract title and chunk\n const title = this._extractTitle(content, relPath);\n const chunks = this._smartChunk(content);\n\n // Insert chunks\n const insertChunk = this._db.prepare(`\n INSERT INTO doc_chunks (collection, file_path, title, content, seq, pos, content_hash)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n `);\n\n const chunkIds: number[] = [];\n\n this._db.transaction(() => {\n for (let seq = 0; seq < chunks.length; seq++) {\n const chunk = chunks[seq];\n const result = insertChunk.run(\n collection, relPath, title, chunk.text, seq, chunk.pos, hash,\n );\n chunkIds.push(Number(result.lastInsertRowid));\n }\n });\n\n // Generate embeddings\n const texts = chunks.map(c => `title: ${title} | text: ${c.text}`);\n const embeddings = await this._embedding.embedBatch(texts);\n\n // Store vectors\n const insertVec = this._db.prepare(\n 'INSERT OR REPLACE INTO doc_vectors (chunk_id, embedding) VALUES (?, ?)'\n );\n\n this._db.transaction(() => {\n for (let j = 0; j < chunkIds.length; j++) {\n const buf = Buffer.from(embeddings[j].buffer);\n insertVec.run(chunkIds[j], buf);\n this._hnsw.add(embeddings[j], chunkIds[j]);\n this._vecCache.set(chunkIds[j], embeddings[j]);\n }\n });\n\n indexed++;\n totalChunks += chunks.length;\n }\n\n return { indexed, skipped, chunks: totalChunks };\n }\n\n /**\n * Remove all indexed data for a collection.\n */\n removeCollection(collection: string): void {\n this._db.prepare('DELETE FROM doc_chunks WHERE collection = ?').run(collection);\n this._db.prepare('DELETE FROM collections WHERE name = ?').run(collection);\n this._db.prepare('DELETE FROM path_contexts WHERE collection = ?').run(collection);\n }\n\n // ── Smart Chunking ──────────────────────────────\n\n /**\n * Split document into chunks at natural markdown boundaries.\n * Uses heading-aware scoring like qmd.\n */\n private _smartChunk(text: string): { text: string; pos: number }[] {\n if (text.length <= TARGET_CHARS) {\n return [{ text, pos: 0 }];\n }\n\n const lines = text.split('\\n');\n const breakPoints = this._findBreakPoints(lines);\n const chunks: { text: string; pos: number }[] = [];\n\n let chunkStart = 0; // char position\n let lineStart = 0; // line index\n\n while (chunkStart < text.length) {\n const remaining = text.length - chunkStart;\n if (remaining <= TARGET_CHARS + WINDOW_CHARS) {\n // Last chunk — take everything\n const lastText = text.slice(chunkStart).trim();\n if (lastText.length >= MIN_CHUNK_CHARS) {\n chunks.push({ text: lastText, pos: chunkStart });\n } else if (chunks.length > 0) {\n // Merge with previous chunk\n chunks[chunks.length - 1].text += '\\n' + lastText;\n } else {\n chunks.push({ text: lastText, pos: chunkStart });\n }\n break;\n }\n\n // Find best break point in window\n const targetEnd = chunkStart + TARGET_CHARS;\n const windowStart = targetEnd - WINDOW_CHARS;\n\n let bestBreak = targetEnd;\n let bestScore = 0;\n\n for (const bp of breakPoints) {\n if (bp.pos <= chunkStart) continue;\n if (bp.pos > targetEnd + WINDOW_CHARS / 2) break;\n if (bp.pos < windowStart) continue;\n\n // Score decay: prefer closer break points\n const distance = Math.abs(bp.pos - targetEnd);\n const decay = 1 - (distance / WINDOW_CHARS) ** 2 * 0.7;\n const finalScore = bp.score * decay;\n\n if (finalScore > bestScore) {\n bestScore = finalScore;\n bestBreak = bp.pos;\n }\n }\n\n const chunkText = text.slice(chunkStart, bestBreak).trim();\n if (chunkText.length >= MIN_CHUNK_CHARS) {\n chunks.push({ text: chunkText, pos: chunkStart });\n }\n\n chunkStart = bestBreak;\n }\n\n return chunks;\n }\n\n /**\n * Find all potential break points in the document with scores.\n */\n private _findBreakPoints(lines: string[]): BreakPoint[] {\n const points: BreakPoint[] = [];\n let charPos = 0;\n let inCodeBlock = false;\n\n for (const line of lines) {\n // Track code fences\n if (line.trimStart().startsWith('```')) {\n inCodeBlock = !inCodeBlock;\n if (!inCodeBlock) {\n // End of code block is a good break point\n points.push({ pos: charPos + line.length + 1, score: 80 });\n }\n charPos += line.length + 1;\n continue;\n }\n\n // Skip break points inside code blocks\n if (inCodeBlock) {\n charPos += line.length + 1;\n continue;\n }\n\n // Score this line as a potential break point\n for (const [pattern, score] of BREAK_SCORES) {\n if (pattern.test(line.trim())) {\n points.push({ pos: charPos, score });\n break;\n }\n }\n\n charPos += line.length + 1;\n }\n\n return points;\n }\n\n /**\n * Extract document title from first heading or filename.\n */\n private _extractTitle(content: string, filePath: string): string {\n const match = content.match(/^#{1,3}\\s+(.+)$/m);\n if (match) return match[1].trim();\n return path.basename(filePath, path.extname(filePath));\n }\n}\n","/**\n * BrainBank — Docs Module\n * \n * Index any folder of markdown/text files (notes, docs, wikis).\n * Heading-aware smart chunking inspired by qmd.\n * \n * import { docs } from 'brainbank/docs';\n * brain.use(docs());\n */\n\nimport type { BrainBankModule, ModuleContext } from './types.ts';\nimport type { HNSWIndex } from '../vector/hnsw.ts';\nimport type { Database } from '../storage/database.ts';\nimport type { EmbeddingProvider, DocumentCollection, SearchResult } from '../types.ts';\nimport { DocIndexer } from '../indexers/doc-indexer.ts';\n\nexport interface DocsModuleOptions {}\n\nclass DocsModuleImpl implements BrainBankModule {\n readonly name = 'docs';\n hnsw!: HNSWIndex;\n indexer!: DocIndexer;\n vecCache = new Map<number, Float32Array>();\n private _db!: Database;\n private _embedding!: EmbeddingProvider;\n\n constructor(private opts: DocsModuleOptions = {}) {}\n\n async initialize(ctx: ModuleContext): Promise<void> {\n this._db = ctx.db;\n this._embedding = ctx.embedding;\n this.hnsw = await ctx.createHnsw();\n ctx.loadVectors('doc_vectors', 'chunk_id', this.hnsw, this.vecCache);\n this.indexer = new DocIndexer(ctx.db, ctx.embedding, this.hnsw, this.vecCache);\n }\n\n /** Register a document collection. */\n addCollection(collection: DocumentCollection): void {\n this._db.prepare(`\n INSERT OR REPLACE INTO collections (name, path, pattern, ignore_json, context)\n VALUES (?, ?, ?, ?, ?)\n `).run(\n collection.name,\n collection.path,\n collection.pattern ?? '**/*.md',\n JSON.stringify(collection.ignore ?? []),\n collection.context ?? null,\n );\n }\n\n /** Remove a collection and its indexed data. */\n removeCollection(name: string): void {\n this.indexer.removeCollection(name);\n }\n\n /** List all registered collections. */\n listCollections(): DocumentCollection[] {\n return (this._db.prepare('SELECT * FROM collections').all() as any[]).map(row => ({\n name: row.name,\n path: row.path,\n pattern: row.pattern,\n ignore: JSON.parse(row.ignore_json),\n context: row.context,\n }));\n }\n\n /** Index all (or specific) collections. Incremental. */\n async indexCollections(options: {\n collections?: string[];\n onProgress?: (collection: string, file: string, current: number, total: number) => void;\n } = {}): Promise<Record<string, { indexed: number; skipped: number; chunks: number }>> {\n const allCollections = this.listCollections();\n const toIndex = options.collections\n ? allCollections.filter(c => options.collections!.includes(c.name))\n : allCollections;\n\n const results: Record<string, { indexed: number; skipped: number; chunks: number }> = {};\n\n for (const coll of toIndex) {\n results[coll.name] = await this.indexer.indexCollection(\n coll.name,\n coll.path,\n coll.pattern,\n {\n ignore: coll.ignore,\n onProgress: (file, cur, total) => options.onProgress?.(coll.name, file, cur, total),\n },\n );\n }\n\n return results;\n }\n\n /** Search documents only. */\n async search(query: string, options?: {\n collection?: string;\n k?: number;\n minScore?: number;\n }): Promise<SearchResult[]> {\n const k = options?.k ?? 8;\n const queryVec = await this._embedding.embed(query);\n const hits = this.hnsw.search(queryVec, k);\n\n const results: SearchResult[] = [];\n for (const hit of hits) {\n if (options?.minScore && hit.score < options.minScore) continue;\n\n const chunk = this._db.prepare(\n 'SELECT * FROM doc_chunks WHERE id = ?'\n ).get(hit.id) as any;\n\n if (!chunk) continue;\n if (options?.collection && chunk.collection !== options.collection) continue;\n\n const ctx = this._getDocContext(chunk.collection, chunk.file_path);\n\n results.push({\n type: 'document',\n score: hit.score,\n filePath: chunk.file_path,\n content: chunk.content,\n context: ctx,\n metadata: {\n collection: chunk.collection,\n title: chunk.title,\n seq: chunk.seq,\n },\n });\n }\n\n return results;\n }\n\n /** Add context description for a document path. */\n addContext(collection: string, path: string, context: string): void {\n this._db.prepare(`\n INSERT OR REPLACE INTO path_contexts (collection, path, context)\n VALUES (?, ?, ?)\n `).run(collection, path, context);\n }\n\n /** Remove context for a path. */\n removeContext(collection: string, path: string): void {\n this._db.prepare(\n 'DELETE FROM path_contexts WHERE collection = ? AND path = ?'\n ).run(collection, path);\n }\n\n /** List all context entries. */\n listContexts(): { collection: string; path: string; context: string }[] {\n return this._db.prepare('SELECT * FROM path_contexts').all() as any[];\n }\n\n stats(): Record<string, any> {\n return {\n collections: (this._db.prepare('SELECT COUNT(*) as c FROM collections').get() as any).c,\n documents: (this._db.prepare('SELECT COUNT(DISTINCT file_path) as c FROM doc_chunks').get() as any).c,\n chunks: (this._db.prepare('SELECT COUNT(*) as c FROM doc_chunks').get() as any).c,\n hnswSize: this.hnsw.size,\n };\n }\n\n /** Resolve context for a document (checks path_contexts tree → collection context). */\n private _getDocContext(collection: string, filePath: string): string | undefined {\n const parts = filePath.split('/');\n for (let i = parts.length; i >= 0; i--) {\n const checkPath = i === 0 ? '/' : '/' + parts.slice(0, i).join('/');\n const ctx = this._db.prepare(\n 'SELECT context FROM path_contexts WHERE collection = ? AND path = ?'\n ).get(collection, checkPath) as any;\n if (ctx) return ctx.context;\n }\n\n const coll = this._db.prepare(\n 'SELECT context FROM collections WHERE name = ?'\n ).get(collection) as any;\n return coll?.context ?? undefined;\n }\n}\n\n/** Create a document collections module. */\nexport function docs(opts?: DocsModuleOptions): BrainBankModule {\n return new DocsModuleImpl(opts);\n}\n"],"mappings":"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","names":["path"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/memory/note-store.ts"],"sourcesContent":["/**\n * BrainBank — Note Memory Store\n * \n * Stores structured note digests for long-term agent memory.\n * Each digest captures decisions, files changed, patterns, and open questions.\n * Supports vector + BM25 hybrid retrieval via HNSW + FTS5.\n * \n * Memory tiers:\n * - \"short\" (default): Full digest, last ~20 notes\n * - \"long\": Compressed to patterns + decisions only\n */\n\nimport type { Database } from '../storage/database.ts';\nimport type { EmbeddingProvider, SearchResult } from '../types.ts';\nimport type { HNSWIndex } from '../vector/hnsw.ts';\nimport { BM25Search } from '../query/bm25.ts';\nimport { reciprocalRankFusion } from '../query/rrf.ts';\n\nexport interface NoteDigest {\n title: string;\n summary: string;\n decisions?: string[];\n filesChanged?: string[];\n patterns?: string[];\n openQuestions?: string[];\n tags?: string[];\n}\n\nexport interface StoredNote extends NoteDigest {\n id: number;\n tier: 'short' | 'long';\n createdAt: number;\n score?: number;\n}\n\nexport interface RecallOptions {\n /** Max results. Default: 5 */\n k?: number;\n /** Search mode. Default: 'hybrid' */\n mode?: 'hybrid' | 'vector' | 'keyword';\n /** Minimum score threshold. Default: 0.15 */\n minScore?: number;\n /** Filter by tier. Default: all */\n tier?: 'short' | 'long';\n}\n\nexport class NoteStore {\n private _db: Database;\n private _embedding: EmbeddingProvider;\n private _hnsw: HNSWIndex;\n private _vecs: Map<number, Float32Array>;\n\n constructor(\n db: Database,\n embedding: EmbeddingProvider,\n hnsw: HNSWIndex,\n vecs: Map<number, Float32Array>,\n ) {\n this._db = db;\n this._embedding = embedding;\n this._hnsw = hnsw;\n this._vecs = vecs;\n }\n\n /**\n * Store a note digest.\n * Embeds title + summary for vector search, auto-indexed in FTS5.\n */\n async remember(digest: NoteDigest): Promise<number> {\n const { title, summary, decisions = [], filesChanged = [], patterns = [], openQuestions = [], tags = [] } = digest;\n\n // Store in SQLite\n const result = this._db.prepare(`\n INSERT INTO note_memories (title, summary, decisions_json, files_json, patterns_json, open_json, tags_json)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n `).run(\n title,\n summary,\n JSON.stringify(decisions),\n JSON.stringify(filesChanged),\n JSON.stringify(patterns),\n JSON.stringify(openQuestions),\n JSON.stringify(tags),\n );\n\n const id = Number(result.lastInsertRowid);\n\n // Embed and index\n const text = `${title}\\n${summary}\\n${decisions.join('. ')}\\n${patterns.join('. ')}`;\n const vec = await this._embedding.embed(text);\n\n this._db.prepare('INSERT INTO note_vectors (note_id, embedding) VALUES (?, ?)').run(\n id, Buffer.from(vec.buffer),\n );\n\n this._hnsw.add(vec, id);\n this._vecs.set(id, vec);\n\n return id;\n }\n\n /**\n * Recall relevant notes.\n * Supports vector, keyword, or hybrid (default) retrieval.\n */\n async recall(query: string, options: RecallOptions = {}): Promise<StoredNote[]> {\n const { k = 5, mode = 'hybrid', minScore = 0.15, tier } = options;\n\n let results: StoredNote[];\n\n if (mode === 'keyword') {\n results = this._searchBM25(query, k);\n } else if (mode === 'vector') {\n results = await this._searchVector(query, k);\n } else {\n // Hybrid: vector + BM25 → RRF\n const [vectorHits, bm25Hits] = await Promise.all([\n this._searchVector(query, k),\n Promise.resolve(this._searchBM25(query, k)),\n ]);\n\n const fusedResults = reciprocalRankFusion(\n [\n vectorHits.map(m => ({ type: 'pattern' as const, score: m.score ?? 0, content: m.summary, metadata: { id: m.id } })),\n bm25Hits.map(m => ({ type: 'pattern' as const, score: m.score ?? 0, content: m.summary, metadata: { id: m.id } })),\n ],\n );\n\n // Map back to full StoredNote objects\n const allById = new Map<number, StoredNote>();\n for (const m of [...vectorHits, ...bm25Hits]) allById.set(m.id, m);\n\n results = fusedResults\n .map(r => {\n const mem = allById.get(r.metadata.id);\n if (!mem) return null;\n return { ...mem, score: r.score };\n })\n .filter(Boolean) as StoredNote[];\n }\n\n // Apply filters\n return results\n .filter(m => (m.score ?? 0) >= minScore)\n .filter(m => !tier || m.tier === tier)\n .slice(0, k);\n }\n\n /**\n * List recent notes.\n */\n list(limit: number = 20, tier?: 'short' | 'long'): StoredNote[] {\n const sql = tier\n ? 'SELECT * FROM note_memories WHERE tier = ? ORDER BY id DESC LIMIT ?'\n : 'SELECT * FROM note_memories ORDER BY id DESC LIMIT ?';\n\n const rows = tier\n ? this._db.prepare(sql).all(tier, limit) as any[]\n : this._db.prepare(sql).all(limit) as any[];\n\n return rows.map(r => this._rowToNote(r));\n }\n\n /**\n * Get total count of notes.\n */\n count(): { total: number; short: number; long: number } {\n const total = (this._db.prepare('SELECT COUNT(*) as n FROM note_memories').get() as any).n;\n const short = (this._db.prepare(\"SELECT COUNT(*) as n FROM note_memories WHERE tier = 'short'\").get() as any).n;\n const long = (this._db.prepare(\"SELECT COUNT(*) as n FROM note_memories WHERE tier = 'long'\").get() as any).n;\n return { total, short, long };\n }\n\n /**\n * Consolidate old short-term notes into long-term.\n * Keeps the most recent `keepRecent` as short-term, compresses the rest.\n */\n consolidate(keepRecent: number = 20): { promoted: number } {\n // Find short-term notes beyond the keep window\n const old = this._db.prepare(`\n SELECT id FROM note_memories \n WHERE tier = 'short' \n ORDER BY created_at DESC \n LIMIT -1 OFFSET ?\n `).all(keepRecent) as any[];\n\n if (old.length === 0) return { promoted: 0 };\n\n const ids = old.map((r: any) => r.id);\n const placeholders = ids.map(() => '?').join(',');\n\n // Promote to long-term: clear verbose fields, keep patterns + decisions\n this._db.prepare(`\n UPDATE note_memories \n SET tier = 'long',\n open_json = '[]',\n files_json = '[]'\n WHERE id IN (${placeholders})\n `).run(...ids);\n\n return { promoted: ids.length };\n }\n\n // ── Private helpers ────────────────────────────\n\n private async _searchVector(query: string, k: number): Promise<StoredNote[]> {\n if (this._hnsw.size === 0) return [];\n\n const queryVec = await this._embedding.embed(query);\n const hits = this._hnsw.search(queryVec, k);\n\n if (hits.length === 0) return [];\n\n const ids = hits.map(h => h.id);\n const scoreMap = new Map(hits.map(h => [h.id, h.score]));\n const placeholders = ids.map(() => '?').join(',');\n\n const rows = this._db.prepare(\n `SELECT * FROM note_memories WHERE id IN (${placeholders})`\n ).all(...ids) as any[];\n\n return rows.map(r => ({\n ...this._rowToNote(r),\n score: scoreMap.get(r.id) ?? 0,\n }));\n }\n\n private _searchBM25(query: string, k: number): StoredNote[] {\n // Sanitize for FTS5\n const clean = query\n .replace(/[{}[\\]()^~*:]/g, ' ')\n .replace(/\\bAND\\b|\\bOR\\b|\\bNOT\\b|\\bNEAR\\b/gi, '')\n .trim();\n\n const words = clean.split(/\\s+/).filter(w => w.length > 1);\n if (words.length === 0) return [];\n\n const ftsQuery = words.map(w => `\"${w}\"`).join(' ');\n\n try {\n const rows = this._db.prepare(`\n SELECT m.*, bm25(fts_notes, 5.0, 3.0, 2.0, 2.0, 1.0) AS score\n FROM fts_notes f\n JOIN note_memories m ON m.id = f.rowid\n WHERE fts_notes MATCH ?\n ORDER BY score ASC\n LIMIT ?\n `).all(ftsQuery, k) as any[];\n\n return rows.map(r => ({\n ...this._rowToNote(r),\n score: 1.0 / (1.0 + Math.exp(-0.3 * (Math.abs(r.score) - 5))),\n }));\n } catch {\n return [];\n }\n }\n\n private _rowToNote(r: any): StoredNote {\n return {\n id: r.id,\n title: r.title,\n summary: r.summary,\n decisions: JSON.parse(r.decisions_json || '[]'),\n filesChanged: JSON.parse(r.files_json || '[]'),\n patterns: JSON.parse(r.patterns_json || '[]'),\n openQuestions: JSON.parse(r.open_json || '[]'),\n tags: JSON.parse(r.tags_json || '[]'),\n tier: r.tier,\n createdAt: r.created_at,\n };\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,224 +0,0 @@
1
- import {
2
- __name
3
- } from "./chunk-7QVYU63E.js";
4
-
5
- // src/indexers/git-indexer.ts
6
- var GitIndexer = class {
7
- static {
8
- __name(this, "GitIndexer");
9
- }
10
- _deps;
11
- _repoPath;
12
- _maxDiffBytes;
13
- constructor(repoPath, deps, maxDiffBytes = 8192) {
14
- this._deps = deps;
15
- this._repoPath = repoPath;
16
- this._maxDiffBytes = maxDiffBytes;
17
- }
18
- /**
19
- * Index git history.
20
- * Only processes commits not already in the database.
21
- */
22
- async index(options = {}) {
23
- const { depth = 500, onProgress } = options;
24
- let git2;
25
- try {
26
- const simpleGit = (await import("simple-git")).default;
27
- git2 = simpleGit(this._repoPath);
28
- } catch {
29
- return { indexed: 0, skipped: 0 };
30
- }
31
- let log;
32
- try {
33
- log = await git2.log({ maxCount: depth });
34
- } catch {
35
- return { indexed: 0, skipped: 0 };
36
- }
37
- const commits = log.all;
38
- let indexed = 0, skipped = 0;
39
- for (let i = 0; i < commits.length; i++) {
40
- const c = commits[i];
41
- onProgress?.(`[${c.hash.slice(0, 7)}] ${c.message.slice(0, 50)}`, i + 1, commits.length);
42
- const exists = this._deps.db.prepare(
43
- "SELECT id FROM git_commits WHERE hash = ?"
44
- ).get(c.hash);
45
- if (exists) {
46
- skipped++;
47
- continue;
48
- }
49
- let diff = "";
50
- let additions = 0, deletions = 0;
51
- const filesChanged = [];
52
- try {
53
- const stat = await git2.raw(["show", "--stat", "--format=", c.hash]);
54
- for (const line of stat.trim().split("\n")) {
55
- const m = line.match(/^\s+(.+?)\s+\|\s+(\d+)\s*([\+\-]*)/);
56
- if (m) {
57
- filesChanged.push(m[1].trim());
58
- additions += (m[3].match(/\+/g) ?? []).length;
59
- deletions += (m[3].match(/-/g) ?? []).length;
60
- }
61
- }
62
- const rawDiff = await git2.raw(["show", "--format=", "--unified=3", "--no-color", c.hash]);
63
- diff = rawDiff.length > this._maxDiffBytes ? rawDiff.slice(0, this._maxDiffBytes) + "\n... [truncated]" : rawDiff;
64
- } catch {
65
- }
66
- const isMerge = c.message.startsWith("Merge") || c.message.startsWith("merge");
67
- const ts = Math.floor(new Date(c.date).getTime() / 1e3);
68
- const result = this._deps.db.prepare(`
69
- INSERT OR IGNORE INTO git_commits (hash, short_hash, message, author, date, timestamp, files_json, diff, additions, deletions, is_merge)
70
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
71
- `).run(
72
- c.hash,
73
- c.hash.slice(0, 7),
74
- c.message,
75
- c.author_name,
76
- c.date,
77
- ts,
78
- JSON.stringify(filesChanged),
79
- diff || null,
80
- additions,
81
- deletions,
82
- isMerge ? 1 : 0
83
- );
84
- if (result.changes === 0) {
85
- skipped++;
86
- continue;
87
- }
88
- const commitId = Number(result.lastInsertRowid);
89
- for (const f of filesChanged) {
90
- this._deps.db.prepare(
91
- "INSERT INTO commit_files (commit_id, file_path) VALUES (?, ?)"
92
- ).run(commitId, f);
93
- }
94
- const text = [
95
- `Commit: ${c.message}`,
96
- `Author: ${c.author_name}`,
97
- `Date: ${c.date}`,
98
- filesChanged.length > 0 ? `Files: ${filesChanged.join(", ")}` : "",
99
- diff ? `Changes:
100
- ${diff.slice(0, 2e3)}` : ""
101
- ].filter(Boolean).join("\n");
102
- const vec = await this._deps.embedding.embed(text);
103
- this._deps.db.prepare(
104
- "INSERT OR IGNORE INTO git_vectors (commit_id, embedding) VALUES (?, ?)"
105
- ).run(commitId, Buffer.from(vec.buffer));
106
- this._deps.hnsw.add(vec, commitId);
107
- this._deps.vectorCache.set(commitId, vec);
108
- indexed++;
109
- }
110
- this._computeCoEdits();
111
- return { indexed, skipped };
112
- }
113
- /**
114
- * Compute which files tend to be edited together.
115
- * Stored in the co_edits table for later suggestion.
116
- */
117
- _computeCoEdits() {
118
- const rows = this._deps.db.prepare(
119
- "SELECT commit_id, file_path FROM commit_files ORDER BY commit_id"
120
- ).all();
121
- const byCommit = /* @__PURE__ */ new Map();
122
- for (const r of rows) {
123
- if (!byCommit.has(r.commit_id)) byCommit.set(r.commit_id, []);
124
- byCommit.get(r.commit_id).push(r.file_path);
125
- }
126
- const counts = /* @__PURE__ */ new Map();
127
- for (const files of byCommit.values()) {
128
- if (files.length < 2 || files.length > 20) continue;
129
- for (let i = 0; i < files.length; i++) {
130
- for (let j = i + 1; j < files.length; j++) {
131
- const key = [files[i], files[j]].sort().join("|||");
132
- counts.set(key, (counts.get(key) ?? 0) + 1);
133
- }
134
- }
135
- }
136
- for (const [key, count] of counts) {
137
- if (count < 2) continue;
138
- const [a, b] = key.split("|||");
139
- this._deps.db.prepare(
140
- `INSERT INTO co_edits (file_a, file_b, count)
141
- VALUES (?, ?, ?)
142
- ON CONFLICT(file_a, file_b) DO UPDATE SET count = excluded.count`
143
- ).run(a, b, count);
144
- }
145
- }
146
- };
147
-
148
- // src/query/co-edits.ts
149
- var CoEditAnalyzer = class {
150
- constructor(_db) {
151
- this._db = _db;
152
- }
153
- static {
154
- __name(this, "CoEditAnalyzer");
155
- }
156
- /**
157
- * Get files that frequently change alongside the given file.
158
- * Returns sorted by co-edit count (highest first).
159
- */
160
- suggest(filePath, limit = 5) {
161
- const rows = this._db.prepare(`
162
- SELECT
163
- CASE WHEN file_a = ? THEN file_b ELSE file_a END AS file,
164
- count
165
- FROM co_edits
166
- WHERE file_a = ? OR file_b = ?
167
- ORDER BY count DESC
168
- LIMIT ?
169
- `).all(filePath, filePath, filePath, limit);
170
- return rows.map((r) => ({ file: r.file, count: r.count }));
171
- }
172
- };
173
-
174
- // src/plugins/git.ts
175
- var GitModuleImpl = class {
176
- constructor(opts = {}) {
177
- this.opts = opts;
178
- this.name = opts.name ?? "git";
179
- }
180
- static {
181
- __name(this, "GitModuleImpl");
182
- }
183
- name;
184
- hnsw;
185
- indexer;
186
- coEdits;
187
- vecCache = /* @__PURE__ */ new Map();
188
- async initialize(ctx) {
189
- const shared = await ctx.getOrCreateSharedHnsw("git", 5e5);
190
- this.hnsw = shared.hnsw;
191
- this.vecCache = shared.vecCache;
192
- if (shared.isNew) {
193
- ctx.loadVectors("git_vectors", "commit_id", this.hnsw, this.vecCache);
194
- }
195
- const repoPath = this.opts.repoPath ?? ctx.config.repoPath;
196
- this.indexer = new GitIndexer(repoPath, {
197
- db: ctx.db,
198
- hnsw: this.hnsw,
199
- vectorCache: this.vecCache,
200
- embedding: ctx.embedding
201
- }, this.opts.maxDiffBytes ?? ctx.config.maxDiffBytes);
202
- this.coEdits = new CoEditAnalyzer(ctx.db);
203
- }
204
- async index(options = {}) {
205
- return this.indexer.index(options);
206
- }
207
- suggest(filePath, limit = 5) {
208
- return this.coEdits.suggest(filePath, limit);
209
- }
210
- stats() {
211
- return { hnswSize: this.hnsw.size };
212
- }
213
- };
214
- function git(opts) {
215
- return new GitModuleImpl(opts);
216
- }
217
- __name(git, "git");
218
-
219
- export {
220
- GitIndexer,
221
- CoEditAnalyzer,
222
- git
223
- };
224
- //# sourceMappingURL=chunk-N6ZMBFDE.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/indexers/git-indexer.ts","../src/query/co-edits.ts","../src/plugins/git.ts"],"sourcesContent":["/**\n * BrainBank — Git Indexer\n * \n * Reads git history, embeds commit messages + diffs,\n * and computes file co-edit relationships.\n * Incremental: only processes new commits.\n */\n\nimport type { Database } from '../storage/database.ts';\nimport type { EmbeddingProvider, ProgressCallback, IndexResult } from '../types.ts';\nimport type { HNSWIndex } from '../vector/hnsw.ts';\n\nexport interface GitIndexerDeps {\n db: Database;\n hnsw: HNSWIndex;\n vectorCache: Map<number, Float32Array>;\n embedding: EmbeddingProvider;\n}\n\nexport interface GitIndexOptions {\n depth?: number;\n onProgress?: ProgressCallback;\n}\n\nexport class GitIndexer {\n private _deps: GitIndexerDeps;\n private _repoPath: string;\n private _maxDiffBytes: number;\n\n constructor(repoPath: string, deps: GitIndexerDeps, maxDiffBytes: number = 8192) {\n this._deps = deps;\n this._repoPath = repoPath;\n this._maxDiffBytes = maxDiffBytes;\n }\n\n /**\n * Index git history.\n * Only processes commits not already in the database.\n */\n async index(options: GitIndexOptions = {}): Promise<IndexResult> {\n const { depth = 500, onProgress } = options;\n\n let git: any;\n try {\n const simpleGit = (await import('simple-git')).default;\n git = simpleGit(this._repoPath);\n } catch {\n return { indexed: 0, skipped: 0 };\n }\n\n let log: any;\n try { log = await git.log({ maxCount: depth }); }\n catch { return { indexed: 0, skipped: 0 }; }\n\n const commits = log.all;\n let indexed = 0, skipped = 0;\n\n for (let i = 0; i < commits.length; i++) {\n const c = commits[i];\n onProgress?.(`[${c.hash.slice(0, 7)}] ${c.message.slice(0, 50)}`, i + 1, commits.length);\n\n // Skip if already indexed\n const exists = this._deps.db.prepare(\n 'SELECT id FROM git_commits WHERE hash = ?'\n ).get(c.hash);\n if (exists) { skipped++; continue; }\n\n // Get diff and stat\n let diff = '';\n let additions = 0, deletions = 0;\n const filesChanged: string[] = [];\n\n try {\n const stat = await git.raw(['show', '--stat', '--format=', c.hash]);\n for (const line of stat.trim().split('\\n')) {\n const m = line.match(/^\\s+(.+?)\\s+\\|\\s+(\\d+)\\s*([\\+\\-]*)/);\n if (m) {\n filesChanged.push(m[1].trim());\n additions += (m[3].match(/\\+/g) ?? []).length;\n deletions += (m[3].match(/-/g) ?? []).length;\n }\n }\n\n const rawDiff = await git.raw(['show', '--format=', '--unified=3', '--no-color', c.hash]);\n diff = rawDiff.length > this._maxDiffBytes\n ? rawDiff.slice(0, this._maxDiffBytes) + '\\n... [truncated]'\n : rawDiff;\n } catch {}\n\n const isMerge = c.message.startsWith('Merge') || c.message.startsWith('merge');\n const ts = Math.floor(new Date(c.date).getTime() / 1000);\n\n const result = this._deps.db.prepare(`\n INSERT OR IGNORE INTO git_commits (hash, short_hash, message, author, date, timestamp, files_json, diff, additions, deletions, is_merge)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n `).run(\n c.hash, c.hash.slice(0, 7), c.message, c.author_name, c.date,\n ts, JSON.stringify(filesChanged), diff || null,\n additions, deletions, isMerge ? 1 : 0,\n );\n\n if (result.changes === 0) { skipped++; continue; }\n const commitId = Number(result.lastInsertRowid);\n\n // Insert commit files\n for (const f of filesChanged) {\n this._deps.db.prepare(\n 'INSERT INTO commit_files (commit_id, file_path) VALUES (?, ?)'\n ).run(commitId, f);\n }\n\n // Embed: message + files + diff snippet\n const text = [\n `Commit: ${c.message}`,\n `Author: ${c.author_name}`,\n `Date: ${c.date}`,\n filesChanged.length > 0 ? `Files: ${filesChanged.join(', ')}` : '',\n diff ? `Changes:\\n${diff.slice(0, 2000)}` : '',\n ].filter(Boolean).join('\\n');\n\n const vec = await this._deps.embedding.embed(text);\n this._deps.db.prepare(\n 'INSERT OR IGNORE INTO git_vectors (commit_id, embedding) VALUES (?, ?)'\n ).run(commitId, Buffer.from(vec.buffer));\n\n this._deps.hnsw.add(vec, commitId);\n this._deps.vectorCache.set(commitId, vec);\n indexed++;\n }\n\n // Compute co-edits\n this._computeCoEdits();\n\n return { indexed, skipped };\n }\n\n /**\n * Compute which files tend to be edited together.\n * Stored in the co_edits table for later suggestion.\n */\n private _computeCoEdits(): void {\n const rows = this._deps.db.prepare(\n 'SELECT commit_id, file_path FROM commit_files ORDER BY commit_id'\n ).all() as any[];\n\n const byCommit = new Map<number, string[]>();\n for (const r of rows) {\n if (!byCommit.has(r.commit_id)) byCommit.set(r.commit_id, []);\n byCommit.get(r.commit_id)!.push(r.file_path);\n }\n\n const counts = new Map<string, number>();\n for (const files of byCommit.values()) {\n // Skip very small or very large changesets\n if (files.length < 2 || files.length > 20) continue;\n for (let i = 0; i < files.length; i++) {\n for (let j = i + 1; j < files.length; j++) {\n const key = [files[i], files[j]].sort().join('|||');\n counts.set(key, (counts.get(key) ?? 0) + 1);\n }\n }\n }\n\n for (const [key, count] of counts) {\n if (count < 2) continue;\n const [a, b] = key.split('|||');\n this._deps.db.prepare(\n `INSERT INTO co_edits (file_a, file_b, count)\n VALUES (?, ?, ?)\n ON CONFLICT(file_a, file_b) DO UPDATE SET count = excluded.count`\n ).run(a, b, count);\n }\n }\n}\n","/**\n * BrainBank — Co-Edit Analyzer\n * \n * Suggests files that historically change together.\n * Based on git commit co-occurrence analysis.\n */\n\nimport type { Database } from '../storage/database.ts';\nimport type { CoEditSuggestion } from '../types.ts';\n\nexport class CoEditAnalyzer {\n constructor(private _db: Database) {}\n\n /**\n * Get files that frequently change alongside the given file.\n * Returns sorted by co-edit count (highest first).\n */\n suggest(filePath: string, limit: number = 5): CoEditSuggestion[] {\n const rows = this._db.prepare(`\n SELECT\n CASE WHEN file_a = ? THEN file_b ELSE file_a END AS file,\n count\n FROM co_edits\n WHERE file_a = ? OR file_b = ?\n ORDER BY count DESC\n LIMIT ?\n `).all(filePath, filePath, filePath, limit) as any[];\n\n return rows.map(r => ({ file: r.file, count: r.count }));\n }\n}\n","/**\n * BrainBank — Git Module\n * \n * Git history indexing with co-edit relationships.\n * \n * import { git } from 'brainbank/git';\n * brain.use(git({ depth: 500 }));\n * \n * // Multi-repo: namespace to avoid key collisions\n * brain\n * .use(git({ repoPath: './frontend', name: 'git:frontend' }))\n * .use(git({ repoPath: './backend', name: 'git:backend' }));\n */\n\nimport type { BrainBankModule, ModuleContext } from './types.ts';\nimport type { HNSWIndex } from '../vector/hnsw.ts';\nimport { GitIndexer } from '../indexers/git-indexer.ts';\nimport { CoEditAnalyzer } from '../query/co-edits.ts';\nimport type { IndexResult, ProgressCallback, CoEditSuggestion } from '../types.ts';\n\nexport interface GitModuleOptions {\n /** Repository path. Default: from config */\n repoPath?: string;\n /** Max commits to index. Default: from config */\n depth?: number;\n /** Max diff bytes. Default: from config */\n maxDiffBytes?: number;\n /** Custom indexer name for multi-repo (e.g. 'git:frontend'). Default: 'git' */\n name?: string;\n}\n\nclass GitModuleImpl implements BrainBankModule {\n readonly name: string;\n hnsw!: HNSWIndex;\n indexer!: GitIndexer;\n coEdits!: CoEditAnalyzer;\n vecCache = new Map<number, Float32Array>();\n\n constructor(private opts: GitModuleOptions = {}) {\n this.name = opts.name ?? 'git';\n }\n\n async initialize(ctx: ModuleContext): Promise<void> {\n // Use shared HNSW so all git indexers share one index\n const shared = await ctx.getOrCreateSharedHnsw('git', 500_000);\n this.hnsw = shared.hnsw;\n this.vecCache = shared.vecCache;\n\n if (shared.isNew) {\n ctx.loadVectors('git_vectors', 'commit_id', this.hnsw, this.vecCache);\n }\n\n const repoPath = this.opts.repoPath ?? ctx.config.repoPath;\n this.indexer = new GitIndexer(repoPath, {\n db: ctx.db,\n hnsw: this.hnsw,\n vectorCache: this.vecCache,\n embedding: ctx.embedding,\n }, this.opts.maxDiffBytes ?? ctx.config.maxDiffBytes);\n\n this.coEdits = new CoEditAnalyzer(ctx.db);\n }\n\n async index(options: {\n depth?: number;\n onProgress?: ProgressCallback;\n } = {}): Promise<IndexResult> {\n return this.indexer.index(options);\n }\n\n suggest(filePath: string, limit: number = 5): CoEditSuggestion[] {\n return this.coEdits.suggest(filePath, limit);\n }\n\n stats(): Record<string, any> {\n return { hnswSize: this.hnsw.size };\n }\n}\n\n/** Create a git history module. */\nexport function git(opts?: GitModuleOptions): BrainBankModule {\n return new GitModuleImpl(opts);\n}\n"],"mappings":";;;;;AAwBO,IAAM,aAAN,MAAiB;AAAA,EAxBxB,OAwBwB;AAAA;AAAA;AAAA,EACZ;AAAA,EACA;AAAA,EACA;AAAA,EAER,YAAY,UAAkB,MAAsB,eAAuB,MAAM;AAC7E,SAAK,QAAQ;AACb,SAAK,YAAY;AACjB,SAAK,gBAAgB;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,MAAM,UAA2B,CAAC,GAAyB;AAC7D,UAAM,EAAE,QAAQ,KAAK,WAAW,IAAI;AAEpC,QAAIA;AACJ,QAAI;AACA,YAAM,aAAa,MAAM,OAAO,YAAY,GAAG;AAC/C,MAAAA,OAAM,UAAU,KAAK,SAAS;AAAA,IAClC,QAAQ;AACJ,aAAO,EAAE,SAAS,GAAG,SAAS,EAAE;AAAA,IACpC;AAEA,QAAI;AACJ,QAAI;AAAE,YAAM,MAAMA,KAAI,IAAI,EAAE,UAAU,MAAM,CAAC;AAAA,IAAG,QAC1C;AAAE,aAAO,EAAE,SAAS,GAAG,SAAS,EAAE;AAAA,IAAG;AAE3C,UAAM,UAAU,IAAI;AACpB,QAAI,UAAU,GAAG,UAAU;AAE3B,aAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACrC,YAAM,IAAI,QAAQ,CAAC;AACnB,mBAAa,IAAI,EAAE,KAAK,MAAM,GAAG,CAAC,CAAC,KAAK,EAAE,QAAQ,MAAM,GAAG,EAAE,CAAC,IAAI,IAAI,GAAG,QAAQ,MAAM;AAGvF,YAAM,SAAS,KAAK,MAAM,GAAG;AAAA,QACzB;AAAA,MACJ,EAAE,IAAI,EAAE,IAAI;AACZ,UAAI,QAAQ;AAAE;AAAW;AAAA,MAAU;AAGnC,UAAI,OAAO;AACX,UAAI,YAAY,GAAG,YAAY;AAC/B,YAAM,eAAyB,CAAC;AAEhC,UAAI;AACA,cAAM,OAAO,MAAMA,KAAI,IAAI,CAAC,QAAQ,UAAU,aAAa,EAAE,IAAI,CAAC;AAClE,mBAAW,QAAQ,KAAK,KAAK,EAAE,MAAM,IAAI,GAAG;AACxC,gBAAM,IAAI,KAAK,MAAM,oCAAoC;AACzD,cAAI,GAAG;AACH,yBAAa,KAAK,EAAE,CAAC,EAAE,KAAK,CAAC;AAC7B,0BAAc,EAAE,CAAC,EAAE,MAAM,KAAK,KAAK,CAAC,GAAG;AACvC,0BAAc,EAAE,CAAC,EAAE,MAAM,IAAI,KAAK,CAAC,GAAG;AAAA,UAC1C;AAAA,QACJ;AAEA,cAAM,UAAU,MAAMA,KAAI,IAAI,CAAC,QAAQ,aAAa,eAAe,cAAc,EAAE,IAAI,CAAC;AACxF,eAAO,QAAQ,SAAS,KAAK,gBACvB,QAAQ,MAAM,GAAG,KAAK,aAAa,IAAI,sBACvC;AAAA,MACV,QAAQ;AAAA,MAAC;AAET,YAAM,UAAU,EAAE,QAAQ,WAAW,OAAO,KAAK,EAAE,QAAQ,WAAW,OAAO;AAC7E,YAAM,KAAK,KAAK,MAAM,IAAI,KAAK,EAAE,IAAI,EAAE,QAAQ,IAAI,GAAI;AAEvD,YAAM,SAAS,KAAK,MAAM,GAAG,QAAQ;AAAA;AAAA;AAAA,aAGpC,EAAE;AAAA,QACC,EAAE;AAAA,QAAM,EAAE,KAAK,MAAM,GAAG,CAAC;AAAA,QAAG,EAAE;AAAA,QAAS,EAAE;AAAA,QAAa,EAAE;AAAA,QACxD;AAAA,QAAI,KAAK,UAAU,YAAY;AAAA,QAAG,QAAQ;AAAA,QAC1C;AAAA,QAAW;AAAA,QAAW,UAAU,IAAI;AAAA,MACxC;AAEA,UAAI,OAAO,YAAY,GAAG;AAAE;AAAW;AAAA,MAAU;AACjD,YAAM,WAAW,OAAO,OAAO,eAAe;AAG9C,iBAAW,KAAK,cAAc;AAC1B,aAAK,MAAM,GAAG;AAAA,UACV;AAAA,QACJ,EAAE,IAAI,UAAU,CAAC;AAAA,MACrB;AAGA,YAAM,OAAO;AAAA,QACT,WAAW,EAAE,OAAO;AAAA,QACpB,WAAW,EAAE,WAAW;AAAA,QACxB,SAAS,EAAE,IAAI;AAAA,QACf,aAAa,SAAS,IAAI,UAAU,aAAa,KAAK,IAAI,CAAC,KAAK;AAAA,QAChE,OAAO;AAAA,EAAa,KAAK,MAAM,GAAG,GAAI,CAAC,KAAK;AAAA,MAChD,EAAE,OAAO,OAAO,EAAE,KAAK,IAAI;AAE3B,YAAM,MAAM,MAAM,KAAK,MAAM,UAAU,MAAM,IAAI;AACjD,WAAK,MAAM,GAAG;AAAA,QACV;AAAA,MACJ,EAAE,IAAI,UAAU,OAAO,KAAK,IAAI,MAAM,CAAC;AAEvC,WAAK,MAAM,KAAK,IAAI,KAAK,QAAQ;AACjC,WAAK,MAAM,YAAY,IAAI,UAAU,GAAG;AACxC;AAAA,IACJ;AAGA,SAAK,gBAAgB;AAErB,WAAO,EAAE,SAAS,QAAQ;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,kBAAwB;AAC5B,UAAM,OAAO,KAAK,MAAM,GAAG;AAAA,MACvB;AAAA,IACJ,EAAE,IAAI;AAEN,UAAM,WAAW,oBAAI,IAAsB;AAC3C,eAAW,KAAK,MAAM;AAClB,UAAI,CAAC,SAAS,IAAI,EAAE,SAAS,EAAG,UAAS,IAAI,EAAE,WAAW,CAAC,CAAC;AAC5D,eAAS,IAAI,EAAE,SAAS,EAAG,KAAK,EAAE,SAAS;AAAA,IAC/C;AAEA,UAAM,SAAS,oBAAI,IAAoB;AACvC,eAAW,SAAS,SAAS,OAAO,GAAG;AAEnC,UAAI,MAAM,SAAS,KAAK,MAAM,SAAS,GAAI;AAC3C,eAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACnC,iBAAS,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACvC,gBAAM,MAAM,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,KAAK;AAClD,iBAAO,IAAI,MAAM,OAAO,IAAI,GAAG,KAAK,KAAK,CAAC;AAAA,QAC9C;AAAA,MACJ;AAAA,IACJ;AAEA,eAAW,CAAC,KAAK,KAAK,KAAK,QAAQ;AAC/B,UAAI,QAAQ,EAAG;AACf,YAAM,CAAC,GAAG,CAAC,IAAI,IAAI,MAAM,KAAK;AAC9B,WAAK,MAAM,GAAG;AAAA,QACV;AAAA;AAAA;AAAA,MAGJ,EAAE,IAAI,GAAG,GAAG,KAAK;AAAA,IACrB;AAAA,EACJ;AACJ;;;ACnKO,IAAM,iBAAN,MAAqB;AAAA,EACxB,YAAoB,KAAe;AAAf;AAAA,EAAgB;AAAA,EAXxC,OAU4B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOxB,QAAQ,UAAkB,QAAgB,GAAuB;AAC7D,UAAM,OAAO,KAAK,IAAI,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,SAQ7B,EAAE,IAAI,UAAU,UAAU,UAAU,KAAK;AAE1C,WAAO,KAAK,IAAI,QAAM,EAAE,MAAM,EAAE,MAAM,OAAO,EAAE,MAAM,EAAE;AAAA,EAC3D;AACJ;;;ACCA,IAAM,gBAAN,MAA+C;AAAA,EAO3C,YAAoB,OAAyB,CAAC,GAAG;AAA7B;AAChB,SAAK,OAAO,KAAK,QAAQ;AAAA,EAC7B;AAAA,EAxCJ,OA+B+C;AAAA;AAAA;AAAA,EAClC;AAAA,EACT;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW,oBAAI,IAA0B;AAAA,EAMzC,MAAM,WAAW,KAAmC;AAEhD,UAAM,SAAS,MAAM,IAAI,sBAAsB,OAAO,GAAO;AAC7D,SAAK,OAAO,OAAO;AACnB,SAAK,WAAW,OAAO;AAEvB,QAAI,OAAO,OAAO;AACd,UAAI,YAAY,eAAe,aAAa,KAAK,MAAM,KAAK,QAAQ;AAAA,IACxE;AAEA,UAAM,WAAW,KAAK,KAAK,YAAY,IAAI,OAAO;AAClD,SAAK,UAAU,IAAI,WAAW,UAAU;AAAA,MACpC,IAAI,IAAI;AAAA,MACR,MAAM,KAAK;AAAA,MACX,aAAa,KAAK;AAAA,MAClB,WAAW,IAAI;AAAA,IACnB,GAAG,KAAK,KAAK,gBAAgB,IAAI,OAAO,YAAY;AAEpD,SAAK,UAAU,IAAI,eAAe,IAAI,EAAE;AAAA,EAC5C;AAAA,EAEA,MAAM,MAAM,UAGR,CAAC,GAAyB;AAC1B,WAAO,KAAK,QAAQ,MAAM,OAAO;AAAA,EACrC;AAAA,EAEA,QAAQ,UAAkB,QAAgB,GAAuB;AAC7D,WAAO,KAAK,QAAQ,QAAQ,UAAU,KAAK;AAAA,EAC/C;AAAA,EAEA,QAA6B;AACzB,WAAO,EAAE,UAAU,KAAK,KAAK,KAAK;AAAA,EACtC;AACJ;AAGO,SAAS,IAAI,MAA0C;AAC1D,SAAO,IAAI,cAAc,IAAI;AACjC;AAFgB;","names":["git"]}