lmgrep 0.1.18 → 0.2.1
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.
- package/README.md +253 -62
- package/completions/_lmgrep +49 -40
- package/dist/application/Lmgrep.d.ts +107 -0
- package/dist/application/Lmgrep.js +99 -0
- package/dist/application/Lmgrep.js.map +1 -0
- package/dist/application/LmgrepFactory.d.ts +40 -0
- package/dist/application/LmgrepFactory.js +155 -0
- package/dist/application/LmgrepFactory.js.map +1 -0
- package/dist/application/indexing/BranchBootstrapper.d.ts +21 -0
- package/dist/application/indexing/BranchBootstrapper.js +52 -0
- package/dist/application/indexing/BranchBootstrapper.js.map +1 -0
- package/dist/application/indexing/BranchManifestSweeper.d.ts +17 -0
- package/dist/application/indexing/BranchManifestSweeper.js +35 -0
- package/dist/application/indexing/BranchManifestSweeper.js.map +1 -0
- package/dist/application/indexing/Duration.d.ts +9 -0
- package/dist/application/indexing/Duration.js +25 -0
- package/dist/application/indexing/Duration.js.map +1 -0
- package/dist/application/indexing/EmbeddingAbortError.d.ts +11 -0
- package/dist/application/indexing/EmbeddingAbortError.js +20 -0
- package/dist/application/indexing/EmbeddingAbortError.js.map +1 -0
- package/dist/application/indexing/EmbeddingPipeline.d.ts +59 -0
- package/dist/application/indexing/EmbeddingPipeline.js +140 -0
- package/dist/application/indexing/EmbeddingPipeline.js.map +1 -0
- package/dist/application/indexing/IndexBuilder.d.ts +112 -0
- package/dist/application/indexing/IndexBuilder.js +370 -0
- package/dist/application/indexing/IndexBuilder.js.map +1 -0
- package/dist/application/indexing/IndexingProgress.d.ts +34 -0
- package/dist/application/indexing/IndexingProgress.js +2 -0
- package/dist/application/indexing/IndexingProgress.js.map +1 -0
- package/dist/application/operations/Deadline.d.ts +13 -0
- package/dist/application/operations/Deadline.js +29 -0
- package/dist/application/operations/Deadline.js.map +1 -0
- package/dist/application/operations/HealthMonitor.d.ts +58 -0
- package/dist/application/operations/HealthMonitor.js +128 -0
- package/dist/application/operations/HealthMonitor.js.map +1 -0
- package/dist/application/operations/IndexAlternatives.d.ts +35 -0
- package/dist/application/operations/IndexAlternatives.js +61 -0
- package/dist/application/operations/IndexAlternatives.js.map +1 -0
- package/dist/application/operations/IndexInventory.d.ts +69 -0
- package/dist/application/operations/IndexInventory.js +99 -0
- package/dist/application/operations/IndexInventory.js.map +1 -0
- package/dist/application/operations/StatusService.d.ts +90 -0
- package/dist/application/operations/StatusService.js +171 -0
- package/dist/application/operations/StatusService.js.map +1 -0
- package/dist/application/operations/WatchService.d.ts +67 -0
- package/dist/application/operations/WatchService.js +145 -0
- package/dist/application/operations/WatchService.js.map +1 -0
- package/dist/application/operations/WorkingTreeDiff.d.ts +19 -0
- package/dist/application/operations/WorkingTreeDiff.js +36 -0
- package/dist/application/operations/WorkingTreeDiff.js.map +1 -0
- package/dist/application/research/ResearchAgent.d.ts +68 -0
- package/dist/application/research/ResearchAgent.js +201 -0
- package/dist/application/research/ResearchAgent.js.map +1 -0
- package/dist/application/research/ResearchPrompts.d.ts +16 -0
- package/dist/application/research/ResearchPrompts.js +34 -0
- package/dist/application/research/ResearchPrompts.js.map +1 -0
- package/dist/application/search/SearchCriteria.d.ts +37 -0
- package/dist/application/search/SearchCriteria.js +48 -0
- package/dist/application/search/SearchCriteria.js.map +1 -0
- package/dist/application/search/SearchService.d.ts +36 -0
- package/dist/application/search/SearchService.js +108 -0
- package/dist/application/search/SearchService.js.map +1 -0
- package/dist/application/search/SearchTargetResolver.d.ts +56 -0
- package/dist/application/search/SearchTargetResolver.js +104 -0
- package/dist/application/search/SearchTargetResolver.js.map +1 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +4 -953
- package/dist/cli.js.map +1 -1
- package/dist/domain/config/ConfigSource.d.ts +16 -0
- package/dist/domain/config/ConfigSource.js +2 -0
- package/dist/domain/config/ConfigSource.js.map +1 -0
- package/dist/domain/config/LmgrepConfig.d.ts +53 -0
- package/dist/domain/config/LmgrepConfig.js +2 -0
- package/dist/domain/config/LmgrepConfig.js.map +1 -0
- package/dist/domain/corpus/Chunk.d.ts +49 -0
- package/dist/domain/corpus/Chunk.js +60 -0
- package/dist/domain/corpus/Chunk.js.map +1 -0
- package/dist/domain/corpus/CodeLocation.d.ts +18 -0
- package/dist/domain/corpus/CodeLocation.js +34 -0
- package/dist/domain/corpus/CodeLocation.js.map +1 -0
- package/dist/domain/corpus/ContentHash.d.ts +15 -0
- package/dist/domain/corpus/ContentHash.js +30 -0
- package/dist/domain/corpus/ContentHash.js.map +1 -0
- package/dist/domain/corpus/FileVersion.d.ts +31 -0
- package/dist/domain/corpus/FileVersion.js +47 -0
- package/dist/domain/corpus/FileVersion.js.map +1 -0
- package/dist/domain/corpus/SourceFile.d.ts +26 -0
- package/dist/domain/corpus/SourceFile.js +47 -0
- package/dist/domain/corpus/SourceFile.js.map +1 -0
- package/dist/domain/corpus/Vector.d.ts +37 -0
- package/dist/domain/corpus/Vector.js +96 -0
- package/dist/domain/corpus/Vector.js.map +1 -0
- package/dist/domain/ports/ChatModelPort.d.ts +57 -0
- package/dist/domain/ports/ChatModelPort.js +2 -0
- package/dist/domain/ports/ChatModelPort.js.map +1 -0
- package/dist/domain/ports/ChunkRepositoryPort.d.ts +42 -0
- package/dist/domain/ports/ChunkRepositoryPort.js +2 -0
- package/dist/domain/ports/ChunkRepositoryPort.js.map +1 -0
- package/dist/domain/ports/ChunkerPort.d.ts +5 -0
- package/dist/domain/ports/ChunkerPort.js +2 -0
- package/dist/domain/ports/ChunkerPort.js.map +1 -0
- package/dist/domain/ports/DatabaseSessionPort.d.ts +10 -0
- package/dist/domain/ports/DatabaseSessionPort.js +2 -0
- package/dist/domain/ports/DatabaseSessionPort.js.map +1 -0
- package/dist/domain/ports/EmbedderPort.d.ts +14 -0
- package/dist/domain/ports/EmbedderPort.js +2 -0
- package/dist/domain/ports/EmbedderPort.js.map +1 -0
- package/dist/domain/ports/FileManifestRepositoryPort.d.ts +32 -0
- package/dist/domain/ports/FileManifestRepositoryPort.js +2 -0
- package/dist/domain/ports/FileManifestRepositoryPort.js.map +1 -0
- package/dist/domain/ports/GitPort.d.ts +24 -0
- package/dist/domain/ports/GitPort.js +2 -0
- package/dist/domain/ports/GitPort.js.map +1 -0
- package/dist/domain/ports/IndexMaintenancePort.d.ts +58 -0
- package/dist/domain/ports/IndexMaintenancePort.js +2 -0
- package/dist/domain/ports/IndexMaintenancePort.js.map +1 -0
- package/dist/domain/ports/IndexMetadataPort.d.ts +23 -0
- package/dist/domain/ports/IndexMetadataPort.js +2 -0
- package/dist/domain/ports/IndexMetadataPort.js.map +1 -0
- package/dist/domain/ports/LockPort.d.ts +17 -0
- package/dist/domain/ports/LockPort.js +2 -0
- package/dist/domain/ports/LockPort.js.map +1 -0
- package/dist/domain/ports/LoggerPort.d.ts +5 -0
- package/dist/domain/ports/LoggerPort.js +2 -0
- package/dist/domain/ports/LoggerPort.js.map +1 -0
- package/dist/domain/ports/ProjectIndexesPort.d.ts +20 -0
- package/dist/domain/ports/ProjectIndexesPort.js +2 -0
- package/dist/domain/ports/ProjectIndexesPort.js.map +1 -0
- package/dist/domain/ports/ProjectRegistryPort.d.ts +42 -0
- package/dist/domain/ports/ProjectRegistryPort.js +2 -0
- package/dist/domain/ports/ProjectRegistryPort.js.map +1 -0
- package/dist/domain/ports/StateDirectoryPort.d.ts +29 -0
- package/dist/domain/ports/StateDirectoryPort.js +2 -0
- package/dist/domain/ports/StateDirectoryPort.js.map +1 -0
- package/dist/domain/ports/WorkspacePort.d.ts +38 -0
- package/dist/domain/ports/WorkspacePort.js +2 -0
- package/dist/domain/ports/WorkspacePort.js.map +1 -0
- package/dist/domain/project/Branch.d.ts +20 -0
- package/dist/domain/project/Branch.js +33 -0
- package/dist/domain/project/Branch.js.map +1 -0
- package/dist/domain/project/DatabaseLocation.d.ts +32 -0
- package/dist/domain/project/DatabaseLocation.js +35 -0
- package/dist/domain/project/DatabaseLocation.js.map +1 -0
- package/dist/domain/project/IndexMetadata.d.ts +17 -0
- package/dist/domain/project/IndexMetadata.js +2 -0
- package/dist/domain/project/IndexMetadata.js.map +1 -0
- package/dist/domain/project/ModelIdentity.d.ts +58 -0
- package/dist/domain/project/ModelIdentity.js +94 -0
- package/dist/domain/project/ModelIdentity.js.map +1 -0
- package/dist/domain/project/Project.d.ts +17 -0
- package/dist/domain/project/Project.js +21 -0
- package/dist/domain/project/Project.js.map +1 -0
- package/dist/domain/project/ProjectId.d.ts +28 -0
- package/dist/domain/project/ProjectId.js +58 -0
- package/dist/domain/project/ProjectId.js.map +1 -0
- package/dist/domain/project/ProjectLocator.d.ts +112 -0
- package/dist/domain/project/ProjectLocator.js +178 -0
- package/dist/domain/project/ProjectLocator.js.map +1 -0
- package/dist/domain/research/Citation.d.ts +27 -0
- package/dist/domain/research/Citation.js +43 -0
- package/dist/domain/research/Citation.js.map +1 -0
- package/dist/domain/research/EvidenceLedger.d.ts +47 -0
- package/dist/domain/research/EvidenceLedger.js +111 -0
- package/dist/domain/research/EvidenceLedger.js.map +1 -0
- package/dist/domain/research/ResearchTrace.d.ts +19 -0
- package/dist/domain/research/ResearchTrace.js +22 -0
- package/dist/domain/research/ResearchTrace.js.map +1 -0
- package/dist/domain/retrieval/Hit.d.ts +19 -0
- package/dist/domain/retrieval/Hit.js +28 -0
- package/dist/domain/retrieval/Hit.js.map +1 -0
- package/dist/domain/retrieval/HitList.d.ts +35 -0
- package/dist/domain/retrieval/HitList.js +81 -0
- package/dist/domain/retrieval/HitList.js.map +1 -0
- package/dist/domain/retrieval/MissingIndexError.d.ts +11 -0
- package/dist/domain/retrieval/MissingIndexError.js +15 -0
- package/dist/domain/retrieval/MissingIndexError.js.map +1 -0
- package/dist/index.d.ts +43 -45
- package/dist/index.js +30 -567
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/ai/AiSdkChatModel.d.ts +31 -0
- package/dist/infrastructure/ai/AiSdkChatModel.js +137 -0
- package/dist/infrastructure/ai/AiSdkChatModel.js.map +1 -0
- package/dist/infrastructure/ai/AiSdkEmbedder.d.ts +28 -0
- package/dist/infrastructure/ai/AiSdkEmbedder.js +79 -0
- package/dist/infrastructure/ai/AiSdkEmbedder.js.map +1 -0
- package/dist/infrastructure/ai/DockerModelRunnerProbe.d.ts +16 -0
- package/dist/infrastructure/ai/DockerModelRunnerProbe.js +52 -0
- package/dist/infrastructure/ai/DockerModelRunnerProbe.js.map +1 -0
- package/dist/infrastructure/ai/EmbeddingPrefixes.d.ts +21 -0
- package/dist/infrastructure/ai/EmbeddingPrefixes.js +40 -0
- package/dist/infrastructure/ai/EmbeddingPrefixes.js.map +1 -0
- package/dist/infrastructure/ai/LmStudioProbe.d.ts +18 -0
- package/dist/infrastructure/ai/LmStudioProbe.js +49 -0
- package/dist/infrastructure/ai/LmStudioProbe.js.map +1 -0
- package/dist/infrastructure/ai/LocalModelReloader.d.ts +21 -0
- package/dist/infrastructure/ai/LocalModelReloader.js +61 -0
- package/dist/infrastructure/ai/LocalModelReloader.js.map +1 -0
- package/dist/infrastructure/ai/LocalRuntimeDetector.d.ts +36 -0
- package/dist/infrastructure/ai/LocalRuntimeDetector.js +63 -0
- package/dist/infrastructure/ai/LocalRuntimeDetector.js.map +1 -0
- package/dist/infrastructure/ai/ModelRuntime.d.ts +32 -0
- package/dist/infrastructure/ai/ModelRuntime.js +2 -0
- package/dist/infrastructure/ai/ModelRuntime.js.map +1 -0
- package/dist/infrastructure/ai/OllamaProbe.d.ts +22 -0
- package/dist/infrastructure/ai/OllamaProbe.js +71 -0
- package/dist/infrastructure/ai/OllamaProbe.js.map +1 -0
- package/dist/infrastructure/ai/ProviderFailure.d.ts +16 -0
- package/dist/infrastructure/ai/ProviderFailure.js +35 -0
- package/dist/infrastructure/ai/ProviderFailure.js.map +1 -0
- package/dist/infrastructure/ai/ProviderModuleLoader.d.ts +14 -0
- package/dist/infrastructure/ai/ProviderModuleLoader.js +56 -0
- package/dist/infrastructure/ai/ProviderModuleLoader.js.map +1 -0
- package/dist/infrastructure/ai/ProviderRegistry.d.ts +38 -0
- package/dist/infrastructure/ai/ProviderRegistry.js +63 -0
- package/dist/infrastructure/ai/ProviderRegistry.js.map +1 -0
- package/dist/infrastructure/fs/ConfigLoader.d.ts +59 -0
- package/dist/infrastructure/fs/ConfigLoader.js +194 -0
- package/dist/infrastructure/fs/ConfigLoader.js.map +1 -0
- package/dist/infrastructure/fs/DatabaseLocks.d.ts +52 -0
- package/dist/infrastructure/fs/DatabaseLocks.js +92 -0
- package/dist/infrastructure/fs/DatabaseLocks.js.map +1 -0
- package/dist/infrastructure/fs/DiskUsage.d.ts +6 -0
- package/dist/infrastructure/fs/DiskUsage.js +41 -0
- package/dist/infrastructure/fs/DiskUsage.js.map +1 -0
- package/dist/infrastructure/fs/IndexableFileRules.d.ts +33 -0
- package/dist/infrastructure/fs/IndexableFileRules.js +177 -0
- package/dist/infrastructure/fs/IndexableFileRules.js.map +1 -0
- package/dist/infrastructure/fs/Loggers.d.ts +14 -0
- package/dist/infrastructure/fs/Loggers.js +18 -0
- package/dist/infrastructure/fs/Loggers.js.map +1 -0
- package/dist/infrastructure/fs/PidFileLock.d.ts +40 -0
- package/dist/infrastructure/fs/PidFileLock.js +97 -0
- package/dist/infrastructure/fs/PidFileLock.js.map +1 -0
- package/dist/infrastructure/fs/ProjectIndexes.d.ts +8 -0
- package/dist/infrastructure/fs/ProjectIndexes.js +35 -0
- package/dist/infrastructure/fs/ProjectIndexes.js.map +1 -0
- package/dist/infrastructure/fs/ProjectMetadataStore.d.ts +42 -0
- package/dist/infrastructure/fs/ProjectMetadataStore.js +86 -0
- package/dist/infrastructure/fs/ProjectMetadataStore.js.map +1 -0
- package/dist/infrastructure/fs/ProjectRegistry.d.ts +25 -0
- package/dist/infrastructure/fs/ProjectRegistry.js +83 -0
- package/dist/infrastructure/fs/ProjectRegistry.js.map +1 -0
- package/dist/infrastructure/fs/StateDirectory.d.ts +28 -0
- package/dist/infrastructure/fs/StateDirectory.js +71 -0
- package/dist/infrastructure/fs/StateDirectory.js.map +1 -0
- package/dist/infrastructure/fs/Workspace.d.ts +25 -0
- package/dist/infrastructure/fs/Workspace.js +90 -0
- package/dist/infrastructure/fs/Workspace.js.map +1 -0
- package/dist/infrastructure/git/GitClient.d.ts +34 -0
- package/dist/infrastructure/git/GitClient.js +74 -0
- package/dist/infrastructure/git/GitClient.js.map +1 -0
- package/dist/infrastructure/lancedb/ChunkRepository.d.ts +38 -0
- package/dist/infrastructure/lancedb/ChunkRepository.js +166 -0
- package/dist/infrastructure/lancedb/ChunkRepository.js.map +1 -0
- package/dist/infrastructure/lancedb/DatabaseImporter.d.ts +22 -0
- package/dist/infrastructure/lancedb/DatabaseImporter.js +74 -0
- package/dist/infrastructure/lancedb/DatabaseImporter.js.map +1 -0
- package/dist/infrastructure/lancedb/FileManifestRepository.d.ts +34 -0
- package/dist/infrastructure/lancedb/FileManifestRepository.js +137 -0
- package/dist/infrastructure/lancedb/FileManifestRepository.js.map +1 -0
- package/dist/infrastructure/lancedb/IndexMaintenance.d.ts +53 -0
- package/dist/infrastructure/lancedb/IndexMaintenance.js +227 -0
- package/dist/infrastructure/lancedb/IndexMaintenance.js.map +1 -0
- package/dist/infrastructure/lancedb/LanceTables.d.ts +67 -0
- package/dist/infrastructure/lancedb/LanceTables.js +159 -0
- package/dist/infrastructure/lancedb/LanceTables.js.map +1 -0
- package/dist/{lib/native-tuning.js → infrastructure/lancedb/NativeTuning.js} +1 -1
- package/dist/infrastructure/lancedb/NativeTuning.js.map +1 -0
- package/dist/infrastructure/lancedb/RowReplication.d.ts +27 -0
- package/dist/infrastructure/lancedb/RowReplication.js +82 -0
- package/dist/infrastructure/lancedb/RowReplication.js.map +1 -0
- package/dist/infrastructure/lancedb/VectorIndexPolicy.d.ts +60 -0
- package/dist/infrastructure/lancedb/VectorIndexPolicy.js +72 -0
- package/dist/infrastructure/lancedb/VectorIndexPolicy.js.map +1 -0
- package/dist/infrastructure/p2p/IndexShare.d.ts +45 -0
- package/dist/infrastructure/p2p/IndexShare.js +160 -0
- package/dist/infrastructure/p2p/IndexShare.js.map +1 -0
- package/dist/infrastructure/p2p/PeerSwarm.d.ts +16 -0
- package/dist/infrastructure/p2p/PeerSwarm.js +41 -0
- package/dist/infrastructure/p2p/PeerSwarm.js.map +1 -0
- package/dist/infrastructure/p2p/SecureChannel.d.ts +60 -0
- package/dist/infrastructure/p2p/SecureChannel.js +71 -0
- package/dist/infrastructure/p2p/SecureChannel.js.map +1 -0
- package/dist/infrastructure/p2p/ShareCode.d.ts +25 -0
- package/dist/infrastructure/p2p/ShareCode.js +304 -0
- package/dist/infrastructure/p2p/ShareCode.js.map +1 -0
- package/dist/infrastructure/process/ProcessRegistry.d.ts +50 -0
- package/dist/infrastructure/process/ProcessRegistry.js +109 -0
- package/dist/infrastructure/process/ProcessRegistry.js.map +1 -0
- package/dist/infrastructure/treesitter/ChunkContextBuilder.d.ts +45 -0
- package/dist/infrastructure/treesitter/ChunkContextBuilder.js +159 -0
- package/dist/infrastructure/treesitter/ChunkContextBuilder.js.map +1 -0
- package/dist/infrastructure/treesitter/EmbeddedTreeSitterAssets.d.ts +8 -0
- package/dist/infrastructure/treesitter/EmbeddedTreeSitterAssets.js +12 -0
- package/dist/infrastructure/treesitter/EmbeddedTreeSitterAssets.js.map +1 -0
- package/dist/infrastructure/treesitter/LanguageCatalog.d.ts +30 -0
- package/dist/{lib/chunker/languages.js → infrastructure/treesitter/LanguageCatalog.js} +38 -29
- package/dist/infrastructure/treesitter/LanguageCatalog.js.map +1 -0
- package/dist/infrastructure/treesitter/SlidingWindowChunker.d.ts +15 -0
- package/dist/infrastructure/treesitter/SlidingWindowChunker.js +42 -0
- package/dist/infrastructure/treesitter/SlidingWindowChunker.js.map +1 -0
- package/dist/infrastructure/treesitter/TreeSitterChunker.d.ts +42 -0
- package/dist/infrastructure/treesitter/TreeSitterChunker.js +165 -0
- package/dist/infrastructure/treesitter/TreeSitterChunker.js.map +1 -0
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +14 -72
- package/dist/mcp.js.map +1 -1
- package/dist/presentation/cli/Cli.d.ts +31 -0
- package/dist/presentation/cli/Cli.js +120 -0
- package/dist/presentation/cli/Cli.js.map +1 -0
- package/dist/presentation/cli/CliOptions.d.ts +18 -0
- package/dist/presentation/cli/CliOptions.js +25 -0
- package/dist/presentation/cli/CliOptions.js.map +1 -0
- package/dist/presentation/cli/CommandContext.d.ts +50 -0
- package/dist/presentation/cli/CommandContext.js +88 -0
- package/dist/presentation/cli/CommandContext.js.map +1 -0
- package/dist/presentation/cli/ConfigTemplate.d.ts +31 -0
- package/dist/presentation/cli/ConfigTemplate.js +83 -0
- package/dist/presentation/cli/ConfigTemplate.js.map +1 -0
- package/dist/presentation/cli/Renderer.d.ts +90 -0
- package/dist/presentation/cli/Renderer.js +290 -0
- package/dist/presentation/cli/Renderer.js.map +1 -0
- package/dist/presentation/cli/commands/AskCommand.d.ts +14 -0
- package/dist/presentation/cli/commands/AskCommand.js +48 -0
- package/dist/presentation/cli/commands/AskCommand.js.map +1 -0
- package/dist/presentation/cli/commands/CompletionsCommand.d.ts +18 -0
- package/dist/presentation/cli/commands/CompletionsCommand.js +96 -0
- package/dist/presentation/cli/commands/CompletionsCommand.js.map +1 -0
- package/dist/presentation/cli/commands/ConfigCommands.d.ts +56 -0
- package/dist/presentation/cli/commands/ConfigCommands.js +220 -0
- package/dist/presentation/cli/commands/ConfigCommands.js.map +1 -0
- package/dist/presentation/cli/commands/IndexCommand.d.ts +18 -0
- package/dist/presentation/cli/commands/IndexCommand.js +54 -0
- package/dist/presentation/cli/commands/IndexCommand.js.map +1 -0
- package/dist/presentation/cli/commands/ProjectsCommand.d.ts +48 -0
- package/dist/presentation/cli/commands/ProjectsCommand.js +240 -0
- package/dist/presentation/cli/commands/ProjectsCommand.js.map +1 -0
- package/dist/presentation/cli/commands/SearchCommand.d.ts +19 -0
- package/dist/presentation/cli/commands/SearchCommand.js +55 -0
- package/dist/presentation/cli/commands/SearchCommand.js.map +1 -0
- package/dist/presentation/cli/commands/ServerCommands.d.ts +13 -0
- package/dist/presentation/cli/commands/ServerCommands.js +40 -0
- package/dist/presentation/cli/commands/ServerCommands.js.map +1 -0
- package/dist/presentation/cli/commands/ShareCommands.d.ts +22 -0
- package/dist/presentation/cli/commands/ShareCommands.js +134 -0
- package/dist/presentation/cli/commands/ShareCommands.js.map +1 -0
- package/dist/presentation/cli/commands/StatusCommand.d.ts +15 -0
- package/dist/presentation/cli/commands/StatusCommand.js +53 -0
- package/dist/presentation/cli/commands/StatusCommand.js.map +1 -0
- package/dist/presentation/mcp/HitFormatter.d.ts +17 -0
- package/dist/presentation/mcp/HitFormatter.js +42 -0
- package/dist/presentation/mcp/HitFormatter.js.map +1 -0
- package/dist/presentation/mcp/IndexWatchController.d.ts +17 -0
- package/dist/presentation/mcp/IndexWatchController.js +31 -0
- package/dist/presentation/mcp/IndexWatchController.js.map +1 -0
- package/dist/presentation/mcp/LmgrepCore.d.ts +66 -0
- package/dist/presentation/mcp/LmgrepCore.js +178 -0
- package/dist/presentation/mcp/LmgrepCore.js.map +1 -0
- package/dist/presentation/mcp/McpServer.d.ts +18 -0
- package/dist/presentation/mcp/McpServer.js +63 -0
- package/dist/presentation/mcp/McpServer.js.map +1 -0
- package/dist/presentation/mcp/ToolDescriptions.d.ts +40 -0
- package/dist/presentation/mcp/ToolDescriptions.js +85 -0
- package/dist/presentation/mcp/ToolDescriptions.js.map +1 -0
- package/package.json +93 -89
- package/dist/lib/build.d.ts +0 -21
- package/dist/lib/build.js +0 -374
- package/dist/lib/build.js.map +0 -1
- package/dist/lib/chunker/context.d.ts +0 -21
- package/dist/lib/chunker/context.js +0 -131
- package/dist/lib/chunker/context.js.map +0 -1
- package/dist/lib/chunker/index.d.ts +0 -7
- package/dist/lib/chunker/index.js +0 -209
- package/dist/lib/chunker/index.js.map +0 -1
- package/dist/lib/chunker/languages.d.ts +0 -15
- package/dist/lib/chunker/languages.js.map +0 -1
- package/dist/lib/config.d.ts +0 -12
- package/dist/lib/config.js +0 -83
- package/dist/lib/config.js.map +0 -1
- package/dist/lib/embedder.d.ts +0 -65
- package/dist/lib/embedder.js +0 -236
- package/dist/lib/embedder.js.map +0 -1
- package/dist/lib/facet-session.d.ts +0 -44
- package/dist/lib/facet-session.js +0 -125
- package/dist/lib/facet-session.js.map +0 -1
- package/dist/lib/native-tuning.js.map +0 -1
- package/dist/lib/p2p.d.ts +0 -32
- package/dist/lib/p2p.js +0 -281
- package/dist/lib/p2p.js.map +0 -1
- package/dist/lib/providers.d.ts +0 -1
- package/dist/lib/providers.js +0 -41
- package/dist/lib/providers.js.map +0 -1
- package/dist/lib/repair.d.ts +0 -10
- package/dist/lib/repair.js +0 -57
- package/dist/lib/repair.js.map +0 -1
- package/dist/lib/scanner.d.ts +0 -34
- package/dist/lib/scanner.js +0 -242
- package/dist/lib/scanner.js.map +0 -1
- package/dist/lib/search-tool.d.ts +0 -55
- package/dist/lib/search-tool.js +0 -319
- package/dist/lib/search-tool.js.map +0 -1
- package/dist/lib/serve.d.ts +0 -11
- package/dist/lib/serve.js +0 -144
- package/dist/lib/serve.js.map +0 -1
- package/dist/lib/store.d.ts +0 -252
- package/dist/lib/store.js +0 -1216
- package/dist/lib/store.js.map +0 -1
- package/dist/lib/types.d.ts +0 -194
- package/dist/lib/types.js +0 -13
- package/dist/lib/types.js.map +0 -1
- package/dist/lib/vocab.d.ts +0 -20
- package/dist/lib/vocab.js +0 -275
- package/dist/lib/vocab.js.map +0 -1
- /package/dist/{lib/native-tuning.d.ts → infrastructure/lancedb/NativeTuning.d.ts} +0 -0
package/dist/lib/store.js
DELETED
|
@@ -1,1216 +0,0 @@
|
|
|
1
|
-
import { connect } from "@lancedb/lancedb";
|
|
2
|
-
import { execSync } from "node:child_process";
|
|
3
|
-
import { createHash } from "node:crypto";
|
|
4
|
-
import { mkdirSync, statSync, writeFileSync, readFileSync, readdirSync, existsSync, unlinkSync, } from "node:fs";
|
|
5
|
-
import { homedir } from "node:os";
|
|
6
|
-
import { join, resolve } from "node:path";
|
|
7
|
-
const CHUNKS_TABLE = "chunks";
|
|
8
|
-
const FILES_TABLE = "files";
|
|
9
|
-
const VOCAB_TABLE = "vocab";
|
|
10
|
-
const DELETE_BATCH_SIZE = 50;
|
|
11
|
-
function buildInFilter(column, values) {
|
|
12
|
-
const escaped = values.map((v) => `'${v.replace(/'/g, "''")}'`);
|
|
13
|
-
return `${column} IN (${escaped.join(", ")})`;
|
|
14
|
-
}
|
|
15
|
-
async function batchDelete(table, column, values) {
|
|
16
|
-
for (let i = 0; i < values.length; i += DELETE_BATCH_SIZE) {
|
|
17
|
-
const batch = values.slice(i, i + DELETE_BATCH_SIZE);
|
|
18
|
-
await table.delete(buildInFilter(column, batch));
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Remove redundant search rows. Two passes, assuming `rows` is already in
|
|
23
|
-
* best-first order (ANN returns ascending distance = descending score):
|
|
24
|
-
*
|
|
25
|
-
* 1. Exact duplicates by chunk id — collapses identical rows produced by
|
|
26
|
-
* concurrent unlocked indexing (same filePath:row:contentHash).
|
|
27
|
-
* 2. Overlapping line ranges within the same file — collapses the
|
|
28
|
-
* fallback chunker's sliding-window overlap (and any parent/child or
|
|
29
|
-
* near-duplicate spans), keeping the highest-scoring chunk of each
|
|
30
|
-
* overlapping cluster. Tree-sitter chunks are node-bounded and don't
|
|
31
|
-
* overlap, so this only ever drops genuine near-duplicates.
|
|
32
|
-
*/
|
|
33
|
-
function dedupeRows(rows) {
|
|
34
|
-
const seenIds = new Set();
|
|
35
|
-
const keptRanges = new Map();
|
|
36
|
-
const out = [];
|
|
37
|
-
for (const r of rows) {
|
|
38
|
-
if (seenIds.has(r.id))
|
|
39
|
-
continue;
|
|
40
|
-
seenIds.add(r.id);
|
|
41
|
-
const ranges = keptRanges.get(r.filePath);
|
|
42
|
-
if (ranges) {
|
|
43
|
-
const overlaps = ranges.some(([s, e]) => r.startLine <= e && s <= r.endLine);
|
|
44
|
-
if (overlaps)
|
|
45
|
-
continue;
|
|
46
|
-
ranges.push([r.startLine, r.endLine]);
|
|
47
|
-
}
|
|
48
|
-
else {
|
|
49
|
-
keptRanges.set(r.filePath, [[r.startLine, r.endLine]]);
|
|
50
|
-
}
|
|
51
|
-
out.push(r);
|
|
52
|
-
}
|
|
53
|
-
return out;
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Legacy indexes (pre branch-scoping) have a `files` table without a `branch`
|
|
57
|
-
* column. Detect and backfill it with the current branch so queries that filter
|
|
58
|
-
* by branch keep working.
|
|
59
|
-
*/
|
|
60
|
-
async function migrateBranchColumn(table, currentBranch) {
|
|
61
|
-
const schema = await table.schema();
|
|
62
|
-
if (schema.fields.some((f) => f.name === "branch"))
|
|
63
|
-
return;
|
|
64
|
-
const escaped = currentBranch.replace(/'/g, "''");
|
|
65
|
-
await table.addColumns([
|
|
66
|
-
{ name: "branch", valueSql: `CAST('${escaped}' AS STRING)` },
|
|
67
|
-
]);
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Legacy chunk tables (pre version-scoping) have no `fileHash` column. Backfill
|
|
71
|
-
* it with "" — the empty string is treated as a wildcard at search time so
|
|
72
|
-
* legacy chunks keep appearing until the file is re-indexed and gets a real
|
|
73
|
-
* file-version hash.
|
|
74
|
-
*/
|
|
75
|
-
async function migrateFileHashColumn(table) {
|
|
76
|
-
const schema = await table.schema();
|
|
77
|
-
if (schema.fields.some((f) => f.name === "fileHash"))
|
|
78
|
-
return;
|
|
79
|
-
await table.addColumns([
|
|
80
|
-
{ name: "fileHash", valueSql: `CAST('' AS STRING)` },
|
|
81
|
-
]);
|
|
82
|
-
}
|
|
83
|
-
function git(cwd, ...args) {
|
|
84
|
-
try {
|
|
85
|
-
return execSync(`git ${args.join(" ")}`, {
|
|
86
|
-
cwd,
|
|
87
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
88
|
-
timeout: 5000,
|
|
89
|
-
})
|
|
90
|
-
.toString()
|
|
91
|
-
.trim();
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
94
|
-
return undefined;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Resolve the project identity for a directory.
|
|
99
|
-
*
|
|
100
|
-
* For git repos: uses the remote origin URL as identity so that multiple
|
|
101
|
-
* worktrees of the same repo share one index.
|
|
102
|
-
*
|
|
103
|
-
* For non-git directories: falls back to hashing the absolute path.
|
|
104
|
-
*
|
|
105
|
-
* Returns { id, root } where id is the string to hash for the DB path,
|
|
106
|
-
* and root is the project root directory (git toplevel or cwd).
|
|
107
|
-
*/
|
|
108
|
-
export function resolveProject(cwd) {
|
|
109
|
-
const absolute = resolve(cwd);
|
|
110
|
-
const gitRoot = git(absolute, "rev-parse", "--show-toplevel");
|
|
111
|
-
if (gitRoot) {
|
|
112
|
-
const branch = git(gitRoot, "rev-parse", "--abbrev-ref", "HEAD") ?? "HEAD";
|
|
113
|
-
const remoteUrl = git(gitRoot, "remote", "get-url", "origin");
|
|
114
|
-
if (remoteUrl) {
|
|
115
|
-
return { id: remoteUrl, root: gitRoot, branch };
|
|
116
|
-
}
|
|
117
|
-
// Git repo with no remote — use the git root path
|
|
118
|
-
return { id: gitRoot, root: gitRoot, branch };
|
|
119
|
-
}
|
|
120
|
-
return { id: absolute, root: absolute, branch: "_default" };
|
|
121
|
-
}
|
|
122
|
-
function slugifyId(id) {
|
|
123
|
-
// Strip git URL scheme and trailing .git so worktrees of the same repo
|
|
124
|
-
// produce the same human-readable prefix regardless of the worktree path.
|
|
125
|
-
// git@host:user/repo.git → user/repo
|
|
126
|
-
// https://host/user/repo.git → host/user/repo (we'll take the last two)
|
|
127
|
-
// /abs/path/to/project → path/to/project (we'll take the last two)
|
|
128
|
-
let s = id.replace(/\.git$/, "");
|
|
129
|
-
const scpMatch = s.match(/^[^@]+@[^:]+:(.+)$/);
|
|
130
|
-
if (scpMatch) {
|
|
131
|
-
s = scpMatch[1];
|
|
132
|
-
}
|
|
133
|
-
else {
|
|
134
|
-
s = s.replace(/^[a-z]+:\/\/[^/]+\//, "");
|
|
135
|
-
}
|
|
136
|
-
const parts = s.split("/").filter(Boolean);
|
|
137
|
-
return parts.slice(-2).join("-").replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
138
|
-
}
|
|
139
|
-
export function buildSlug(id) {
|
|
140
|
-
const hash = createHash("sha256").update(id).digest("hex").slice(0, 8);
|
|
141
|
-
return `${slugifyId(id)}-${hash}`;
|
|
142
|
-
}
|
|
143
|
-
export function getDbPath(cwd) {
|
|
144
|
-
const { id } = resolveProject(cwd);
|
|
145
|
-
return join(homedir(), ".local", "state", "lmgrep", buildSlug(id));
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Compute the DB path using the pre-git-aware scheme (absolute path hash).
|
|
149
|
-
* Used by `lmgrep import` to find legacy indexes.
|
|
150
|
-
*/
|
|
151
|
-
export function getLegacyDbPath(cwd) {
|
|
152
|
-
const absolute = resolve(cwd);
|
|
153
|
-
const hash = createHash("sha256").update(absolute).digest("hex").slice(0, 6);
|
|
154
|
-
const parts = absolute.split("/").filter(Boolean);
|
|
155
|
-
const slug = parts.slice(-2).join("-").replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
156
|
-
return join(homedir(), ".local", "state", "lmgrep", `${slug}-${hash}`);
|
|
157
|
-
}
|
|
158
|
-
/**
|
|
159
|
-
* Find the project root and compute the prefix (subdirectory offset).
|
|
160
|
-
* For git repos, the root is the git toplevel. For non-git dirs, walks up
|
|
161
|
-
* looking for an existing index.
|
|
162
|
-
*/
|
|
163
|
-
export function findIndexedAncestor(cwd) {
|
|
164
|
-
const absolute = resolve(cwd);
|
|
165
|
-
const { root } = resolveProject(cwd);
|
|
166
|
-
// For git repos, the root is always the git toplevel
|
|
167
|
-
const dbPath = getDbPath(root);
|
|
168
|
-
try {
|
|
169
|
-
if (statSync(dbPath).isDirectory()) {
|
|
170
|
-
const prefix = root === absolute ? "" : absolute.slice(root.length + 1);
|
|
171
|
-
return { root, prefix };
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
catch {
|
|
175
|
-
// no index yet
|
|
176
|
-
}
|
|
177
|
-
// For non-git dirs, walk up looking for an ancestor with an index
|
|
178
|
-
if (root === absolute) {
|
|
179
|
-
let current = resolve(absolute, "..");
|
|
180
|
-
while (true) {
|
|
181
|
-
const ancestorDb = getDbPath(current);
|
|
182
|
-
try {
|
|
183
|
-
if (statSync(ancestorDb).isDirectory()) {
|
|
184
|
-
const prefix = absolute.slice(current.length + 1);
|
|
185
|
-
return { root: current, prefix };
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
catch {
|
|
189
|
-
// keep climbing
|
|
190
|
-
}
|
|
191
|
-
const parent = resolve(current, "..");
|
|
192
|
-
if (parent === current)
|
|
193
|
-
break;
|
|
194
|
-
current = parent;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
return undefined;
|
|
198
|
-
}
|
|
199
|
-
// --- Project metadata ---
|
|
200
|
-
const METADATA_FILE = "lmgrep.json";
|
|
201
|
-
/**
|
|
202
|
-
* Extract the base model family name from a full model string.
|
|
203
|
-
* Strips provider prefix (e.g. "openai:", "ollama:") and quantization/tag
|
|
204
|
-
* suffixes (e.g. ":Q4_K_M", ":latest", ":fp16").
|
|
205
|
-
*
|
|
206
|
-
* Examples:
|
|
207
|
-
* "openai:nomic-embed-text" → "nomic-embed-text"
|
|
208
|
-
* "ollama:nomic-embed-text:Q4_K_M" → "nomic-embed-text"
|
|
209
|
-
* "lmstudio:bge-large-en:fp16" → "bge-large-en"
|
|
210
|
-
* "openai:text-embedding-3-small" → "text-embedding-3-small"
|
|
211
|
-
*/
|
|
212
|
-
export function extractModelFamily(model) {
|
|
213
|
-
// Strip provider prefix (first colon-separated segment)
|
|
214
|
-
const colonIdx = model.indexOf(":");
|
|
215
|
-
if (colonIdx === -1)
|
|
216
|
-
return model;
|
|
217
|
-
const rest = model.slice(colonIdx + 1);
|
|
218
|
-
// Strip quant/tag suffix: known patterns like Q4_K_M, Q8_0, fp16, latest, etc.
|
|
219
|
-
// These appear as the last colon-separated segment
|
|
220
|
-
const lastColon = rest.lastIndexOf(":");
|
|
221
|
-
if (lastColon === -1)
|
|
222
|
-
return rest;
|
|
223
|
-
const suffix = rest.slice(lastColon + 1);
|
|
224
|
-
// Match common quantization and tag patterns
|
|
225
|
-
if (/^(Q\d|q\d|fp\d|f\d|latest|gguf|ggml)/i.test(suffix)) {
|
|
226
|
-
return rest.slice(0, lastColon);
|
|
227
|
-
}
|
|
228
|
-
// Not a recognized suffix — keep the whole thing (could be part of model name)
|
|
229
|
-
return rest;
|
|
230
|
-
}
|
|
231
|
-
export function writeProjectMetadata(cwd, extra) {
|
|
232
|
-
const dbPath = getDbPath(cwd);
|
|
233
|
-
const { id, root, branch } = resolveProject(cwd);
|
|
234
|
-
const gitRoot = git(resolve(cwd), "rev-parse", "--show-toplevel");
|
|
235
|
-
const remote = gitRoot
|
|
236
|
-
? git(gitRoot, "remote", "get-url", "origin") ?? undefined
|
|
237
|
-
: undefined;
|
|
238
|
-
mkdirSync(dbPath, { recursive: true });
|
|
239
|
-
// Preserve existing model/dimensions if not provided (don't overwrite baseline)
|
|
240
|
-
const existing = readProjectMetadata(dbPath);
|
|
241
|
-
const metadata = {
|
|
242
|
-
root,
|
|
243
|
-
remote,
|
|
244
|
-
branch,
|
|
245
|
-
indexedAt: new Date().toISOString(),
|
|
246
|
-
model: existing?.model ?? extra?.model,
|
|
247
|
-
dimensions: existing?.dimensions ?? extra?.dimensions,
|
|
248
|
-
};
|
|
249
|
-
writeFileSync(join(dbPath, METADATA_FILE), JSON.stringify(metadata, null, 2));
|
|
250
|
-
}
|
|
251
|
-
export function readProjectMetadata(dbPath) {
|
|
252
|
-
const metaPath = join(dbPath, METADATA_FILE);
|
|
253
|
-
try {
|
|
254
|
-
return JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
255
|
-
}
|
|
256
|
-
catch {
|
|
257
|
-
return undefined;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
/**
|
|
261
|
-
* Scan all lmgrep indexes and return their metadata.
|
|
262
|
-
*/
|
|
263
|
-
export function discoverIndexedProjects() {
|
|
264
|
-
const baseDir = join(homedir(), ".local", "state", "lmgrep");
|
|
265
|
-
if (!existsSync(baseDir))
|
|
266
|
-
return [];
|
|
267
|
-
const results = [];
|
|
268
|
-
for (const entry of readdirSync(baseDir, { withFileTypes: true })) {
|
|
269
|
-
if (!entry.isDirectory())
|
|
270
|
-
continue;
|
|
271
|
-
const dbPath = join(baseDir, entry.name);
|
|
272
|
-
const metadata = readProjectMetadata(dbPath);
|
|
273
|
-
if (metadata) {
|
|
274
|
-
results.push({ dbPath, metadata });
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
return results;
|
|
278
|
-
}
|
|
279
|
-
// --- DB-level write lock ---
|
|
280
|
-
function isProcessAlive(pid) {
|
|
281
|
-
try {
|
|
282
|
-
process.kill(pid, 0);
|
|
283
|
-
return true;
|
|
284
|
-
}
|
|
285
|
-
catch {
|
|
286
|
-
return false;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
/**
|
|
290
|
-
* Acquire an exclusive write lock for a project's DB.
|
|
291
|
-
* Returns true if the lock was acquired, false if another process holds it.
|
|
292
|
-
*/
|
|
293
|
-
export function acquireDbLock(cwd) {
|
|
294
|
-
const lockPath = `${getDbPath(cwd)}.lock`;
|
|
295
|
-
if (existsSync(lockPath)) {
|
|
296
|
-
try {
|
|
297
|
-
const pid = Number.parseInt(readFileSync(lockPath, "utf-8").trim(), 10);
|
|
298
|
-
if (isProcessAlive(pid))
|
|
299
|
-
return false;
|
|
300
|
-
}
|
|
301
|
-
catch {
|
|
302
|
-
// stale lock, take over
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
const dbPath = getDbPath(cwd);
|
|
306
|
-
mkdirSync(dbPath, { recursive: true });
|
|
307
|
-
writeFileSync(lockPath, `${process.pid}\n`);
|
|
308
|
-
return true;
|
|
309
|
-
}
|
|
310
|
-
/**
|
|
311
|
-
* Release the write lock for a project's DB.
|
|
312
|
-
*/
|
|
313
|
-
export function releaseDbLock(cwd) {
|
|
314
|
-
try {
|
|
315
|
-
unlinkSync(`${getDbPath(cwd)}.lock`);
|
|
316
|
-
}
|
|
317
|
-
catch { }
|
|
318
|
-
}
|
|
319
|
-
// --- Per-build write mutex ---
|
|
320
|
-
//
|
|
321
|
-
// The `.lock` maintainer lock (above) is held for a watcher/serve process's
|
|
322
|
-
// whole lifetime and doubles as a liveness registry for `lmgrep status`. It
|
|
323
|
-
// cannot also serve as a write mutex, because then a one-shot `lmgrep index`
|
|
324
|
-
// could never run while a watcher is up. This separate `.writelock` is a
|
|
325
|
-
// short-lived mutex acquired around each build() so that the watcher and an
|
|
326
|
-
// ad-hoc `lmgrep index` serialize their writes instead of racing into
|
|
327
|
-
// duplicate rows. Named `.writelock` (not `.write.lock`) so it does not match
|
|
328
|
-
// the `.lock` suffix scan in discoverRunningProcesses.
|
|
329
|
-
function writeLockPath(cwd) {
|
|
330
|
-
return `${getDbPath(cwd)}.writelock`;
|
|
331
|
-
}
|
|
332
|
-
function tryAcquireWriteLock(cwd) {
|
|
333
|
-
const lockPath = writeLockPath(cwd);
|
|
334
|
-
if (existsSync(lockPath)) {
|
|
335
|
-
try {
|
|
336
|
-
const pid = Number.parseInt(readFileSync(lockPath, "utf-8").trim(), 10);
|
|
337
|
-
if (isProcessAlive(pid))
|
|
338
|
-
return false;
|
|
339
|
-
}
|
|
340
|
-
catch {
|
|
341
|
-
// stale/corrupt lock, take over
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
mkdirSync(getDbPath(cwd), { recursive: true });
|
|
345
|
-
writeFileSync(lockPath, `${process.pid}\n`);
|
|
346
|
-
return true;
|
|
347
|
-
}
|
|
348
|
-
function releaseWriteLock(cwd) {
|
|
349
|
-
const lockPath = writeLockPath(cwd);
|
|
350
|
-
try {
|
|
351
|
-
// Only remove the lock if we still own it.
|
|
352
|
-
const pid = Number.parseInt(readFileSync(lockPath, "utf-8").trim(), 10);
|
|
353
|
-
if (pid === process.pid)
|
|
354
|
-
unlinkSync(lockPath);
|
|
355
|
-
}
|
|
356
|
-
catch { }
|
|
357
|
-
}
|
|
358
|
-
/**
|
|
359
|
-
* Run `fn` while holding the project's write mutex, so concurrent indexers
|
|
360
|
-
* (a watcher plus an ad-hoc `lmgrep index`) can't write at the same time and
|
|
361
|
-
* produce duplicate chunk rows. Waits up to `waitMs` for a busy lock, taking
|
|
362
|
-
* over a lock held by a dead process. Throws if the lock can't be acquired in
|
|
363
|
-
* time.
|
|
364
|
-
*/
|
|
365
|
-
export async function withWriteLock(cwd, fn, opts = {}) {
|
|
366
|
-
const waitMs = opts.waitMs ?? 120_000;
|
|
367
|
-
const pollMs = opts.pollMs ?? 200;
|
|
368
|
-
let waited = 0;
|
|
369
|
-
while (!tryAcquireWriteLock(cwd)) {
|
|
370
|
-
if (waited >= waitMs) {
|
|
371
|
-
throw new Error("Could not acquire the index write lock — another indexer is busy. " +
|
|
372
|
-
"Try again once it finishes.");
|
|
373
|
-
}
|
|
374
|
-
await new Promise((r) => setTimeout(r, pollMs));
|
|
375
|
-
waited += pollMs;
|
|
376
|
-
}
|
|
377
|
-
try {
|
|
378
|
-
return await fn();
|
|
379
|
-
}
|
|
380
|
-
finally {
|
|
381
|
-
releaseWriteLock(cwd);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
/**
|
|
385
|
-
* Check if a write lock is held by a live process.
|
|
386
|
-
*/
|
|
387
|
-
export function isDbLocked(cwd) {
|
|
388
|
-
const lockPath = `${getDbPath(cwd)}.lock`;
|
|
389
|
-
if (!existsSync(lockPath))
|
|
390
|
-
return false;
|
|
391
|
-
try {
|
|
392
|
-
const pid = Number.parseInt(readFileSync(lockPath, "utf-8").trim(), 10);
|
|
393
|
-
return isProcessAlive(pid);
|
|
394
|
-
}
|
|
395
|
-
catch {
|
|
396
|
-
return false;
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
function getProcessInfo(pid) {
|
|
400
|
-
try {
|
|
401
|
-
const name = readFileSync(`/proc/${pid}/comm`, "utf-8").trim();
|
|
402
|
-
const cmdline = readFileSync(`/proc/${pid}/cmdline`, "utf-8")
|
|
403
|
-
.replace(/\0/g, " ")
|
|
404
|
-
.trim();
|
|
405
|
-
return { name, cmdline };
|
|
406
|
-
}
|
|
407
|
-
catch {
|
|
408
|
-
return undefined;
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
function classifyProcess(info) {
|
|
412
|
-
if (info.name === "lmgrep-mcp" || info.cmdline.includes("mcp"))
|
|
413
|
-
return "mcp";
|
|
414
|
-
if (info.cmdline.includes("serve"))
|
|
415
|
-
return "serve";
|
|
416
|
-
return "cli";
|
|
417
|
-
}
|
|
418
|
-
/**
|
|
419
|
-
* Scan all lock files to find running lmgrep processes,
|
|
420
|
-
* which indexes they hold, and whether they are watching for changes.
|
|
421
|
-
*/
|
|
422
|
-
export function discoverRunningProcesses() {
|
|
423
|
-
const baseDir = join(homedir(), ".local", "state", "lmgrep");
|
|
424
|
-
if (!existsSync(baseDir))
|
|
425
|
-
return [];
|
|
426
|
-
const results = [];
|
|
427
|
-
const seen = new Set();
|
|
428
|
-
for (const entry of readdirSync(baseDir)) {
|
|
429
|
-
if (!entry.endsWith(".lock"))
|
|
430
|
-
continue;
|
|
431
|
-
const lockPath = join(baseDir, entry);
|
|
432
|
-
let pid;
|
|
433
|
-
try {
|
|
434
|
-
pid = Number.parseInt(readFileSync(lockPath, "utf-8").trim(), 10);
|
|
435
|
-
}
|
|
436
|
-
catch {
|
|
437
|
-
continue;
|
|
438
|
-
}
|
|
439
|
-
if (!isProcessAlive(pid) || seen.has(pid))
|
|
440
|
-
continue;
|
|
441
|
-
seen.add(pid);
|
|
442
|
-
const info = getProcessInfo(pid);
|
|
443
|
-
if (!info)
|
|
444
|
-
continue;
|
|
445
|
-
const kind = classifyProcess(info);
|
|
446
|
-
// Resolve which project this lock belongs to
|
|
447
|
-
const dbDir = entry.slice(0, -".lock".length);
|
|
448
|
-
const dbPath = join(baseDir, dbDir);
|
|
449
|
-
const metadata = readProjectMetadata(dbPath);
|
|
450
|
-
results.push({
|
|
451
|
-
pid,
|
|
452
|
-
processName: info.name,
|
|
453
|
-
cmdline: info.cmdline,
|
|
454
|
-
kind,
|
|
455
|
-
projectRoot: metadata?.root,
|
|
456
|
-
// MCP and serve processes watch; plain CLI invocations don't
|
|
457
|
-
watching: kind === "mcp" || kind === "serve",
|
|
458
|
-
});
|
|
459
|
-
}
|
|
460
|
-
return results;
|
|
461
|
-
}
|
|
462
|
-
export class Store {
|
|
463
|
-
dbPath;
|
|
464
|
-
branch;
|
|
465
|
-
db;
|
|
466
|
-
chunksTable;
|
|
467
|
-
filesTable;
|
|
468
|
-
vocabTable;
|
|
469
|
-
constructor(dbPath, branch = "_default") {
|
|
470
|
-
this.dbPath = dbPath;
|
|
471
|
-
this.branch = branch;
|
|
472
|
-
}
|
|
473
|
-
static forProject(cwd) {
|
|
474
|
-
const { branch } = resolveProject(cwd);
|
|
475
|
-
return new Store(getDbPath(cwd), branch);
|
|
476
|
-
}
|
|
477
|
-
// --- Connection ---
|
|
478
|
-
async connection() {
|
|
479
|
-
if (this.db)
|
|
480
|
-
return this.db;
|
|
481
|
-
mkdirSync(this.dbPath, { recursive: true });
|
|
482
|
-
this.db = await connect(this.dbPath);
|
|
483
|
-
return this.db;
|
|
484
|
-
}
|
|
485
|
-
async openChunks() {
|
|
486
|
-
if (this.chunksTable)
|
|
487
|
-
return this.chunksTable;
|
|
488
|
-
const conn = await this.connection();
|
|
489
|
-
const tables = await conn.tableNames();
|
|
490
|
-
if (tables.includes(CHUNKS_TABLE)) {
|
|
491
|
-
const t = await conn.openTable(CHUNKS_TABLE);
|
|
492
|
-
await migrateFileHashColumn(t);
|
|
493
|
-
this.chunksTable = t;
|
|
494
|
-
return this.chunksTable;
|
|
495
|
-
}
|
|
496
|
-
return undefined;
|
|
497
|
-
}
|
|
498
|
-
async openFiles() {
|
|
499
|
-
if (this.filesTable)
|
|
500
|
-
return this.filesTable;
|
|
501
|
-
const conn = await this.connection();
|
|
502
|
-
const tables = await conn.tableNames();
|
|
503
|
-
if (tables.includes(FILES_TABLE)) {
|
|
504
|
-
const t = await conn.openTable(FILES_TABLE);
|
|
505
|
-
await migrateBranchColumn(t, this.branch);
|
|
506
|
-
this.filesTable = t;
|
|
507
|
-
return this.filesTable;
|
|
508
|
-
}
|
|
509
|
-
return undefined;
|
|
510
|
-
}
|
|
511
|
-
// --- Vocab ---
|
|
512
|
-
async openVocab() {
|
|
513
|
-
if (this.vocabTable)
|
|
514
|
-
return this.vocabTable;
|
|
515
|
-
const conn = await this.connection();
|
|
516
|
-
const tables = await conn.tableNames();
|
|
517
|
-
if (tables.includes(VOCAB_TABLE)) {
|
|
518
|
-
this.vocabTable = await conn.openTable(VOCAB_TABLE);
|
|
519
|
-
return this.vocabTable;
|
|
520
|
-
}
|
|
521
|
-
return undefined;
|
|
522
|
-
}
|
|
523
|
-
async hasVocab() {
|
|
524
|
-
return (await this.openVocab()) !== undefined;
|
|
525
|
-
}
|
|
526
|
-
/**
|
|
527
|
-
* Return the set of vocab terms already embedded in the vocab table.
|
|
528
|
-
* Used at index time to skip re-embedding.
|
|
529
|
-
*/
|
|
530
|
-
async getVocabTerms() {
|
|
531
|
-
const t = await this.openVocab();
|
|
532
|
-
if (!t)
|
|
533
|
-
return new Set();
|
|
534
|
-
const rows = await t.query().select(["term"]).toArray();
|
|
535
|
-
return new Set(rows.map((r) => r.term));
|
|
536
|
-
}
|
|
537
|
-
async addVocab(entries) {
|
|
538
|
-
if (entries.length === 0)
|
|
539
|
-
return;
|
|
540
|
-
// Dedup within the batch
|
|
541
|
-
const seen = new Set();
|
|
542
|
-
const batchUnique = [];
|
|
543
|
-
for (const e of entries) {
|
|
544
|
-
if (seen.has(e.term))
|
|
545
|
-
continue;
|
|
546
|
-
seen.add(e.term);
|
|
547
|
-
batchUnique.push(e);
|
|
548
|
-
}
|
|
549
|
-
// Skip terms already in the table
|
|
550
|
-
const known = await this.getVocabTerms();
|
|
551
|
-
const records = batchUnique
|
|
552
|
-
.filter((e) => !known.has(e.term))
|
|
553
|
-
.map((e) => ({ term: e.term, vector: e.vector }));
|
|
554
|
-
if (records.length === 0)
|
|
555
|
-
return;
|
|
556
|
-
const conn = await this.connection();
|
|
557
|
-
const tables = await conn.tableNames();
|
|
558
|
-
if (tables.includes(VOCAB_TABLE)) {
|
|
559
|
-
const t = await conn.openTable(VOCAB_TABLE);
|
|
560
|
-
this.vocabTable = t;
|
|
561
|
-
await t.add(records);
|
|
562
|
-
}
|
|
563
|
-
else {
|
|
564
|
-
this.vocabTable = await conn.createTable(VOCAB_TABLE, records);
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
/**
|
|
568
|
-
* ANN search against the vocab table. Returns top-N terms closest to the
|
|
569
|
-
* given vector by cosine distance.
|
|
570
|
-
*/
|
|
571
|
-
async searchVocab(vector, limit, excludeTerms) {
|
|
572
|
-
const t = await this.openVocab();
|
|
573
|
-
if (!t)
|
|
574
|
-
return [];
|
|
575
|
-
const fetch = excludeTerms ? limit + excludeTerms.size : limit;
|
|
576
|
-
const rows = await t.search(vector).limit(fetch).toArray();
|
|
577
|
-
const out = [];
|
|
578
|
-
for (const r of rows) {
|
|
579
|
-
const term = r.term;
|
|
580
|
-
if (excludeTerms?.has(term))
|
|
581
|
-
continue;
|
|
582
|
-
out.push({
|
|
583
|
-
term,
|
|
584
|
-
score: r._distance != null ? 1 - r._distance : 0,
|
|
585
|
-
});
|
|
586
|
-
if (out.length >= limit)
|
|
587
|
-
break;
|
|
588
|
-
}
|
|
589
|
-
return out;
|
|
590
|
-
}
|
|
591
|
-
async vocabCount() {
|
|
592
|
-
const t = await this.openVocab();
|
|
593
|
-
if (!t)
|
|
594
|
-
return 0;
|
|
595
|
-
return t.countRows();
|
|
596
|
-
}
|
|
597
|
-
async dropVocab() {
|
|
598
|
-
const conn = await this.connection();
|
|
599
|
-
const tables = await conn.tableNames();
|
|
600
|
-
if (tables.includes(VOCAB_TABLE)) {
|
|
601
|
-
await conn.dropTable(VOCAB_TABLE);
|
|
602
|
-
}
|
|
603
|
-
this.vocabTable = undefined;
|
|
604
|
-
}
|
|
605
|
-
// --- Chunks ---
|
|
606
|
-
async addChunks(chunks) {
|
|
607
|
-
if (chunks.length === 0)
|
|
608
|
-
return;
|
|
609
|
-
const conn = await this.connection();
|
|
610
|
-
const records = chunks.map((c) => ({
|
|
611
|
-
id: c.id,
|
|
612
|
-
filePath: c.filePath,
|
|
613
|
-
startLine: c.startLine,
|
|
614
|
-
endLine: c.endLine,
|
|
615
|
-
type: c.type,
|
|
616
|
-
name: c.name,
|
|
617
|
-
content: c.content,
|
|
618
|
-
context: c.context,
|
|
619
|
-
hash: c.hash,
|
|
620
|
-
fileHash: c.fileHash ?? "",
|
|
621
|
-
vector: c.vector,
|
|
622
|
-
}));
|
|
623
|
-
const tables = await conn.tableNames();
|
|
624
|
-
if (tables.includes(CHUNKS_TABLE)) {
|
|
625
|
-
const t = await conn.openTable(CHUNKS_TABLE);
|
|
626
|
-
this.chunksTable = t;
|
|
627
|
-
await t.add(records);
|
|
628
|
-
}
|
|
629
|
-
else {
|
|
630
|
-
this.chunksTable = await conn.createTable(CHUNKS_TABLE, records);
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
/**
|
|
634
|
-
* Delete chunks for files that are no longer referenced by ANY branch.
|
|
635
|
-
* If another branch still has a file hash entry for a given path,
|
|
636
|
-
* the chunks are kept (they're shared via content-addressing).
|
|
637
|
-
*/
|
|
638
|
-
async deleteChunksByFiles(filePaths) {
|
|
639
|
-
const t = await this.openChunks();
|
|
640
|
-
if (!t || filePaths.length === 0)
|
|
641
|
-
return;
|
|
642
|
-
const filesTable = await this.openFiles();
|
|
643
|
-
if (!filesTable) {
|
|
644
|
-
// No files table means no other branches — safe to delete all
|
|
645
|
-
await batchDelete(t, "filePath", filePaths);
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
648
|
-
// Find which files are still referenced by other branches
|
|
649
|
-
const escaped = this.branch.replace(/'/g, "''");
|
|
650
|
-
const stillReferenced = new Set();
|
|
651
|
-
for (let i = 0; i < filePaths.length; i += DELETE_BATCH_SIZE) {
|
|
652
|
-
const batch = filePaths.slice(i, i + DELETE_BATCH_SIZE);
|
|
653
|
-
const pathFilter = buildInFilter("filePath", batch);
|
|
654
|
-
const refs = await filesTable
|
|
655
|
-
.query()
|
|
656
|
-
.where(`branch != '${escaped}' AND ${pathFilter}`)
|
|
657
|
-
.select(["filePath"])
|
|
658
|
-
.toArray();
|
|
659
|
-
for (const r of refs) {
|
|
660
|
-
stillReferenced.add(r.filePath);
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
// Only delete chunks for files not referenced by other branches
|
|
664
|
-
const toDelete = filePaths.filter((fp) => !stillReferenced.has(fp));
|
|
665
|
-
if (toDelete.length > 0) {
|
|
666
|
-
await batchDelete(t, "filePath", toDelete);
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
branchVersionsCache;
|
|
670
|
-
/**
|
|
671
|
-
* Map of filePath -> file-version hash for the current branch (cached).
|
|
672
|
-
* Used to scope search to the exact file versions this branch references,
|
|
673
|
-
* so stale chunks from another version of the same path are excluded.
|
|
674
|
-
*/
|
|
675
|
-
async getBranchFileVersions() {
|
|
676
|
-
if (this.branchVersionsCache)
|
|
677
|
-
return this.branchVersionsCache;
|
|
678
|
-
const t = await this.openFiles();
|
|
679
|
-
if (!t)
|
|
680
|
-
return undefined;
|
|
681
|
-
const escaped = this.branch.replace(/'/g, "''");
|
|
682
|
-
const rows = await t
|
|
683
|
-
.query()
|
|
684
|
-
.where(`branch = '${escaped}'`)
|
|
685
|
-
.select(["filePath", "fileHash"])
|
|
686
|
-
.toArray();
|
|
687
|
-
const map = new Map();
|
|
688
|
-
for (const r of rows) {
|
|
689
|
-
map.set(r.filePath, r.fileHash);
|
|
690
|
-
}
|
|
691
|
-
this.branchVersionsCache = map;
|
|
692
|
-
return this.branchVersionsCache;
|
|
693
|
-
}
|
|
694
|
-
/** Invalidate the branch files cache (call after index/import). */
|
|
695
|
-
invalidateBranchFilesCache() {
|
|
696
|
-
this.branchVersionsCache = undefined;
|
|
697
|
-
}
|
|
698
|
-
async search(queryVector, limit = 25, filePrefix, typeFilter,
|
|
699
|
-
/** Pass false to skip branch scoping (e.g. for cross-project search). */
|
|
700
|
-
scopeToBranch = true) {
|
|
701
|
-
const t = await this.openChunks();
|
|
702
|
-
if (!t) {
|
|
703
|
-
throw new Error("No index found. Run `lmgrep index` first.");
|
|
704
|
-
}
|
|
705
|
-
const branchVersions = scopeToBranch
|
|
706
|
-
? await this.getBranchFileVersions()
|
|
707
|
-
: undefined;
|
|
708
|
-
// Over-fetch: branch/version filtering and dedup both discard rows, so
|
|
709
|
-
// pull extra to still return `limit` distinct results.
|
|
710
|
-
const fetchLimit = branchVersions ? limit * 3 : limit * 2;
|
|
711
|
-
let query = t.search(queryVector).limit(fetchLimit);
|
|
712
|
-
const conditions = [];
|
|
713
|
-
if (filePrefix) {
|
|
714
|
-
conditions.push(`filePath LIKE '${filePrefix.replace(/'/g, "''")}%'`);
|
|
715
|
-
}
|
|
716
|
-
if (typeFilter && typeFilter.length > 0) {
|
|
717
|
-
const escaped = typeFilter.map((t) => `'${t.replace(/'/g, "''")}'`);
|
|
718
|
-
conditions.push(`type IN (${escaped.join(", ")})`);
|
|
719
|
-
}
|
|
720
|
-
if (conditions.length > 0) {
|
|
721
|
-
query = query.where(conditions.join(" AND "));
|
|
722
|
-
}
|
|
723
|
-
const results = await query.toArray();
|
|
724
|
-
let mapped = results.map((r) => ({
|
|
725
|
-
id: r.id,
|
|
726
|
-
filePath: r.filePath,
|
|
727
|
-
startLine: r.startLine,
|
|
728
|
-
endLine: r.endLine,
|
|
729
|
-
type: r.type,
|
|
730
|
-
name: r.name,
|
|
731
|
-
content: r.content,
|
|
732
|
-
context: r.context,
|
|
733
|
-
score: r._distance != null ? 1 - r._distance : 0,
|
|
734
|
-
fileHash: r.fileHash ?? "",
|
|
735
|
-
}));
|
|
736
|
-
if (branchVersions) {
|
|
737
|
-
mapped = mapped.filter((r) => {
|
|
738
|
-
const want = branchVersions.get(r.filePath);
|
|
739
|
-
// "" fileHash = legacy chunk, treated as wildcard.
|
|
740
|
-
return want !== undefined && (r.fileHash === "" || r.fileHash === want);
|
|
741
|
-
});
|
|
742
|
-
}
|
|
743
|
-
return dedupeRows(mapped)
|
|
744
|
-
.slice(0, limit)
|
|
745
|
-
.map(({ id, fileHash, ...rest }) => rest);
|
|
746
|
-
}
|
|
747
|
-
/**
|
|
748
|
-
* Same as search() but also returns each chunk's vector — needed for
|
|
749
|
-
* client-side clustering (facet command).
|
|
750
|
-
*/
|
|
751
|
-
async searchWithVectors(queryVector, limit = 25, filePrefix, scopeToBranch = true) {
|
|
752
|
-
const t = await this.openChunks();
|
|
753
|
-
if (!t) {
|
|
754
|
-
throw new Error("No index found. Run `lmgrep index` first.");
|
|
755
|
-
}
|
|
756
|
-
const branchVersions = scopeToBranch
|
|
757
|
-
? await this.getBranchFileVersions()
|
|
758
|
-
: undefined;
|
|
759
|
-
const fetchLimit = branchVersions ? limit * 3 : limit * 2;
|
|
760
|
-
let query = t.search(queryVector).limit(fetchLimit);
|
|
761
|
-
if (filePrefix) {
|
|
762
|
-
query = query.where(`filePath LIKE '${filePrefix.replace(/'/g, "''")}%'`);
|
|
763
|
-
}
|
|
764
|
-
const results = await query.toArray();
|
|
765
|
-
let mapped = results.map((r) => ({
|
|
766
|
-
id: r.id,
|
|
767
|
-
filePath: r.filePath,
|
|
768
|
-
startLine: r.startLine,
|
|
769
|
-
endLine: r.endLine,
|
|
770
|
-
type: r.type,
|
|
771
|
-
name: r.name,
|
|
772
|
-
content: r.content,
|
|
773
|
-
context: r.context,
|
|
774
|
-
score: r._distance != null ? 1 - r._distance : 0,
|
|
775
|
-
fileHash: r.fileHash ?? "",
|
|
776
|
-
vector: Array.from(r.vector),
|
|
777
|
-
}));
|
|
778
|
-
if (branchVersions) {
|
|
779
|
-
mapped = mapped.filter((r) => {
|
|
780
|
-
const want = branchVersions.get(r.filePath);
|
|
781
|
-
return want !== undefined && (r.fileHash === "" || r.fileHash === want);
|
|
782
|
-
});
|
|
783
|
-
}
|
|
784
|
-
return dedupeRows(mapped)
|
|
785
|
-
.slice(0, limit)
|
|
786
|
-
.map(({ fileHash, ...rest }) => rest);
|
|
787
|
-
}
|
|
788
|
-
/**
|
|
789
|
-
* Fetch chunks (with vectors) by id. Used to rehydrate faceting sessions.
|
|
790
|
-
* Missing ids are silently dropped.
|
|
791
|
-
*/
|
|
792
|
-
async getChunksByIds(ids) {
|
|
793
|
-
if (ids.length === 0)
|
|
794
|
-
return [];
|
|
795
|
-
const t = await this.openChunks();
|
|
796
|
-
if (!t)
|
|
797
|
-
return [];
|
|
798
|
-
const escaped = ids.map((i) => `'${i.replace(/'/g, "''")}'`).join(",");
|
|
799
|
-
const rows = await t.query().where(`id IN (${escaped})`).toArray();
|
|
800
|
-
return rows.map((r) => ({
|
|
801
|
-
id: r.id,
|
|
802
|
-
filePath: r.filePath,
|
|
803
|
-
startLine: r.startLine,
|
|
804
|
-
endLine: r.endLine,
|
|
805
|
-
type: r.type,
|
|
806
|
-
name: r.name,
|
|
807
|
-
content: r.content,
|
|
808
|
-
context: r.context,
|
|
809
|
-
score: 0,
|
|
810
|
-
vector: Array.from(r.vector),
|
|
811
|
-
}));
|
|
812
|
-
}
|
|
813
|
-
async getIndexedFiles() {
|
|
814
|
-
const t = await this.openChunks();
|
|
815
|
-
if (!t)
|
|
816
|
-
return new Map();
|
|
817
|
-
const rows = await t.query().select(["filePath", "hash"]).toArray();
|
|
818
|
-
const map = new Map();
|
|
819
|
-
for (const row of rows) {
|
|
820
|
-
const fp = row.filePath;
|
|
821
|
-
const hash = row.hash;
|
|
822
|
-
const existing = map.get(fp) ?? [];
|
|
823
|
-
existing.push(hash);
|
|
824
|
-
map.set(fp, existing);
|
|
825
|
-
}
|
|
826
|
-
return map;
|
|
827
|
-
}
|
|
828
|
-
async getIndexedHashes() {
|
|
829
|
-
const t = await this.openChunks();
|
|
830
|
-
if (!t)
|
|
831
|
-
return new Set();
|
|
832
|
-
const rows = await t.query().select(["hash"]).toArray();
|
|
833
|
-
return new Set(rows.map((r) => r.hash));
|
|
834
|
-
}
|
|
835
|
-
/**
|
|
836
|
-
* Given a set of chunk hashes, return those that already exist in the
|
|
837
|
-
* chunks table. Runs as batched IN() queries in the DB.
|
|
838
|
-
*/
|
|
839
|
-
async filterExistingChunkHashes(hashes) {
|
|
840
|
-
const t = await this.openChunks();
|
|
841
|
-
if (!t || hashes.length === 0)
|
|
842
|
-
return new Set();
|
|
843
|
-
const existing = new Set();
|
|
844
|
-
const unique = [...new Set(hashes)];
|
|
845
|
-
for (let i = 0; i < unique.length; i += DELETE_BATCH_SIZE) {
|
|
846
|
-
const batch = unique.slice(i, i + DELETE_BATCH_SIZE);
|
|
847
|
-
const filter = buildInFilter("hash", batch);
|
|
848
|
-
const rows = await t
|
|
849
|
-
.query()
|
|
850
|
-
.where(filter)
|
|
851
|
-
.select(["hash"])
|
|
852
|
-
.toArray();
|
|
853
|
-
for (const r of rows) {
|
|
854
|
-
existing.add(r.hash);
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
return existing;
|
|
858
|
-
}
|
|
859
|
-
async chunkCount() {
|
|
860
|
-
const t = await this.openChunks();
|
|
861
|
-
if (!t)
|
|
862
|
-
return 0;
|
|
863
|
-
return await t.countRows();
|
|
864
|
-
}
|
|
865
|
-
/**
|
|
866
|
-
* Stream chunk texts (name + content only) in batches. Used by vocab
|
|
867
|
-
* backfill to avoid loading all chunks into memory at once.
|
|
868
|
-
*/
|
|
869
|
-
async *streamChunkTexts(batchSize = 1000) {
|
|
870
|
-
const t = await this.openChunks();
|
|
871
|
-
if (!t)
|
|
872
|
-
return;
|
|
873
|
-
const stream = await t
|
|
874
|
-
.query()
|
|
875
|
-
.select(["name", "content"])
|
|
876
|
-
.toArray();
|
|
877
|
-
for (let i = 0; i < stream.length; i += batchSize) {
|
|
878
|
-
yield stream.slice(i, i + batchSize).map((r) => ({
|
|
879
|
-
name: r.name ?? "",
|
|
880
|
-
content: r.content ?? "",
|
|
881
|
-
}));
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
// --- File hashes (change detection) ---
|
|
885
|
-
async getFileHashes() {
|
|
886
|
-
const t = await this.openFiles();
|
|
887
|
-
if (!t)
|
|
888
|
-
return new Map();
|
|
889
|
-
const escaped = this.branch.replace(/'/g, "''");
|
|
890
|
-
const rows = await t
|
|
891
|
-
.query()
|
|
892
|
-
.where(`branch = '${escaped}'`)
|
|
893
|
-
.select(["filePath", "fileHash"])
|
|
894
|
-
.toArray();
|
|
895
|
-
const map = new Map();
|
|
896
|
-
for (const row of rows) {
|
|
897
|
-
map.set(row.filePath, row.fileHash);
|
|
898
|
-
}
|
|
899
|
-
return map;
|
|
900
|
-
}
|
|
901
|
-
/**
|
|
902
|
-
* Given a set of file hashes, return those that already exist in the
|
|
903
|
-
* files table on ANY branch. The query runs in the DB, not in JS.
|
|
904
|
-
*/
|
|
905
|
-
async filterKnownFileHashes(hashes) {
|
|
906
|
-
const t = await this.openFiles();
|
|
907
|
-
if (!t || hashes.length === 0)
|
|
908
|
-
return new Set();
|
|
909
|
-
const known = new Set();
|
|
910
|
-
for (let i = 0; i < hashes.length; i += DELETE_BATCH_SIZE) {
|
|
911
|
-
const batch = hashes.slice(i, i + DELETE_BATCH_SIZE);
|
|
912
|
-
const filter = buildInFilter("fileHash", batch);
|
|
913
|
-
const rows = await t
|
|
914
|
-
.query()
|
|
915
|
-
.where(filter)
|
|
916
|
-
.select(["fileHash"])
|
|
917
|
-
.toArray();
|
|
918
|
-
for (const r of rows) {
|
|
919
|
-
known.add(r.fileHash);
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
return known;
|
|
923
|
-
}
|
|
924
|
-
async upsertFileHashes(entries) {
|
|
925
|
-
if (entries.length === 0)
|
|
926
|
-
return;
|
|
927
|
-
const records = entries.map((e) => ({
|
|
928
|
-
filePath: e.filePath,
|
|
929
|
-
fileHash: e.fileHash,
|
|
930
|
-
branch: e.branch ?? this.branch,
|
|
931
|
-
}));
|
|
932
|
-
const conn = await this.connection();
|
|
933
|
-
const tables = await conn.tableNames();
|
|
934
|
-
if (tables.includes(FILES_TABLE)) {
|
|
935
|
-
const t = await conn.openTable(FILES_TABLE);
|
|
936
|
-
this.filesTable = t;
|
|
937
|
-
// Delete existing entries for the same branch + filePath pairs.
|
|
938
|
-
// Group by branch so we don't accidentally clobber other branches'
|
|
939
|
-
// rows for the same path.
|
|
940
|
-
const byBranch = new Map();
|
|
941
|
-
for (const r of records) {
|
|
942
|
-
const list = byBranch.get(r.branch) ?? [];
|
|
943
|
-
list.push(r.filePath);
|
|
944
|
-
byBranch.set(r.branch, list);
|
|
945
|
-
}
|
|
946
|
-
for (const [branch, paths] of byBranch) {
|
|
947
|
-
const escaped = branch.replace(/'/g, "''");
|
|
948
|
-
for (let i = 0; i < paths.length; i += DELETE_BATCH_SIZE) {
|
|
949
|
-
const batch = paths.slice(i, i + DELETE_BATCH_SIZE);
|
|
950
|
-
const pathFilter = buildInFilter("filePath", batch);
|
|
951
|
-
await t.delete(`branch = '${escaped}' AND ${pathFilter}`);
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
await t.add(records);
|
|
955
|
-
}
|
|
956
|
-
else {
|
|
957
|
-
this.filesTable = await conn.createTable(FILES_TABLE, records);
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
async deleteFileHashes(filePaths) {
|
|
961
|
-
const t = await this.openFiles();
|
|
962
|
-
if (!t || filePaths.length === 0)
|
|
963
|
-
return;
|
|
964
|
-
const escaped = this.branch.replace(/'/g, "''");
|
|
965
|
-
for (let i = 0; i < filePaths.length; i += DELETE_BATCH_SIZE) {
|
|
966
|
-
const batch = filePaths.slice(i, i + DELETE_BATCH_SIZE);
|
|
967
|
-
const pathFilter = buildInFilter("filePath", batch);
|
|
968
|
-
await t.delete(`branch = '${escaped}' AND ${pathFilter}`);
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
// --- Admin ---
|
|
972
|
-
async *streamAllChunks(batchSize) {
|
|
973
|
-
const t = await this.openChunks();
|
|
974
|
-
if (!t)
|
|
975
|
-
return;
|
|
976
|
-
const total = await t.countRows();
|
|
977
|
-
for (let offset = 0; offset < total; offset += batchSize) {
|
|
978
|
-
const rows = await t.query().limit(batchSize).offset(offset).toArray();
|
|
979
|
-
if (rows.length === 0)
|
|
980
|
-
break;
|
|
981
|
-
yield rows.map((r) => ({
|
|
982
|
-
id: r.id,
|
|
983
|
-
filePath: r.filePath,
|
|
984
|
-
startLine: r.startLine,
|
|
985
|
-
endLine: r.endLine,
|
|
986
|
-
type: r.type,
|
|
987
|
-
name: r.name,
|
|
988
|
-
content: r.content,
|
|
989
|
-
context: r.context,
|
|
990
|
-
hash: r.hash,
|
|
991
|
-
fileHash: r.fileHash ?? "",
|
|
992
|
-
vector: Array.from(r.vector),
|
|
993
|
-
}));
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
/**
|
|
997
|
-
* Remove redundant rows from the chunks table:
|
|
998
|
-
* - exact duplicate ids (identical rows from concurrent unlocked indexing)
|
|
999
|
-
* - orphaned versions: chunks whose fileHash is no longer referenced by any
|
|
1000
|
-
* branch's manifest (left behind when a file was edited on one branch
|
|
1001
|
-
* while another branch still pointed at the old path). Legacy chunks with
|
|
1002
|
-
* an empty fileHash are kept.
|
|
1003
|
-
* Rewrites the table from the surviving rows. Loads chunks into memory in
|
|
1004
|
-
* batches, so this is a maintenance operation, not a hot path.
|
|
1005
|
-
*/
|
|
1006
|
-
async dedupeChunks() {
|
|
1007
|
-
const t = await this.openChunks();
|
|
1008
|
-
if (!t)
|
|
1009
|
-
return { before: 0, after: 0, duplicateIds: 0, staleVersions: 0 };
|
|
1010
|
-
const before = await t.countRows();
|
|
1011
|
-
// Which (filePath, fileHash) pairs any branch still references.
|
|
1012
|
-
const refs = new Map();
|
|
1013
|
-
for (const e of await this.getAllFileEntries()) {
|
|
1014
|
-
const set = refs.get(e.filePath) ?? new Set();
|
|
1015
|
-
set.add(e.fileHash);
|
|
1016
|
-
refs.set(e.filePath, set);
|
|
1017
|
-
}
|
|
1018
|
-
const kept = [];
|
|
1019
|
-
const seenIds = new Set();
|
|
1020
|
-
let duplicateIds = 0;
|
|
1021
|
-
let staleVersions = 0;
|
|
1022
|
-
for await (const batch of this.streamAllChunks(2000)) {
|
|
1023
|
-
for (const r of batch) {
|
|
1024
|
-
const id = r.id;
|
|
1025
|
-
if (seenIds.has(id)) {
|
|
1026
|
-
duplicateIds++;
|
|
1027
|
-
continue;
|
|
1028
|
-
}
|
|
1029
|
-
seenIds.add(id);
|
|
1030
|
-
const filePath = r.filePath;
|
|
1031
|
-
const fileHash = r.fileHash ?? "";
|
|
1032
|
-
if (fileHash !== "" && !refs.get(filePath)?.has(fileHash)) {
|
|
1033
|
-
staleVersions++;
|
|
1034
|
-
continue;
|
|
1035
|
-
}
|
|
1036
|
-
kept.push({
|
|
1037
|
-
id,
|
|
1038
|
-
filePath,
|
|
1039
|
-
startLine: r.startLine,
|
|
1040
|
-
endLine: r.endLine,
|
|
1041
|
-
type: r.type,
|
|
1042
|
-
name: r.name,
|
|
1043
|
-
content: r.content,
|
|
1044
|
-
context: r.context,
|
|
1045
|
-
hash: r.hash,
|
|
1046
|
-
fileHash,
|
|
1047
|
-
vector: r.vector,
|
|
1048
|
-
});
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
if (duplicateIds + staleVersions === 0) {
|
|
1052
|
-
return { before, after: before, duplicateIds, staleVersions };
|
|
1053
|
-
}
|
|
1054
|
-
// Rewrite the table from the survivors.
|
|
1055
|
-
const conn = await this.connection();
|
|
1056
|
-
if ((await conn.tableNames()).includes(CHUNKS_TABLE)) {
|
|
1057
|
-
await conn.dropTable(CHUNKS_TABLE);
|
|
1058
|
-
}
|
|
1059
|
-
this.chunksTable = undefined;
|
|
1060
|
-
if (kept.length > 0)
|
|
1061
|
-
await this.addChunks(kept);
|
|
1062
|
-
return { before, after: kept.length, duplicateIds, staleVersions };
|
|
1063
|
-
}
|
|
1064
|
-
async getAllFileEntries() {
|
|
1065
|
-
const t = await this.openFiles();
|
|
1066
|
-
if (!t)
|
|
1067
|
-
return [];
|
|
1068
|
-
const rows = await t.query().toArray();
|
|
1069
|
-
return rows.map((r) => ({
|
|
1070
|
-
filePath: r.filePath,
|
|
1071
|
-
fileHash: r.fileHash,
|
|
1072
|
-
branch: r.branch ?? this.branch,
|
|
1073
|
-
}));
|
|
1074
|
-
}
|
|
1075
|
-
async reset() {
|
|
1076
|
-
const conn = await this.connection();
|
|
1077
|
-
const tables = await conn.tableNames();
|
|
1078
|
-
if (tables.includes(CHUNKS_TABLE))
|
|
1079
|
-
await conn.dropTable(CHUNKS_TABLE);
|
|
1080
|
-
if (tables.includes(FILES_TABLE))
|
|
1081
|
-
await conn.dropTable(FILES_TABLE);
|
|
1082
|
-
if (tables.includes(VOCAB_TABLE))
|
|
1083
|
-
await conn.dropTable(VOCAB_TABLE);
|
|
1084
|
-
this.chunksTable = undefined;
|
|
1085
|
-
this.filesTable = undefined;
|
|
1086
|
-
this.vocabTable = undefined;
|
|
1087
|
-
}
|
|
1088
|
-
async compact() {
|
|
1089
|
-
const t = await this.openChunks();
|
|
1090
|
-
if (t)
|
|
1091
|
-
await t.optimize();
|
|
1092
|
-
const f = await this.openFiles();
|
|
1093
|
-
if (f)
|
|
1094
|
-
await f.optimize();
|
|
1095
|
-
const v = await this.openVocab();
|
|
1096
|
-
if (v)
|
|
1097
|
-
await v.optimize();
|
|
1098
|
-
}
|
|
1099
|
-
/**
|
|
1100
|
-
* Import all chunks and file hashes from another Store's database.
|
|
1101
|
-
* Returns { chunks, files } counts of imported records.
|
|
1102
|
-
*/
|
|
1103
|
-
async importFrom(sourcePath) {
|
|
1104
|
-
const sourceConn = await connect(sourcePath);
|
|
1105
|
-
const sourceTables = await sourceConn.tableNames();
|
|
1106
|
-
let chunks = 0;
|
|
1107
|
-
let files = 0;
|
|
1108
|
-
if (sourceTables.includes(CHUNKS_TABLE)) {
|
|
1109
|
-
const sourceChunks = await sourceConn.openTable(CHUNKS_TABLE);
|
|
1110
|
-
const rawChunkRows = await sourceChunks.query().toArray();
|
|
1111
|
-
if (rawChunkRows.length > 0) {
|
|
1112
|
-
// Convert Arrow typed arrays to plain JS objects so LanceDB can
|
|
1113
|
-
// infer the schema when creating a new table.
|
|
1114
|
-
const rows = rawChunkRows.map((r) => ({
|
|
1115
|
-
id: r.id,
|
|
1116
|
-
filePath: r.filePath,
|
|
1117
|
-
startLine: r.startLine,
|
|
1118
|
-
endLine: r.endLine,
|
|
1119
|
-
type: r.type,
|
|
1120
|
-
name: r.name,
|
|
1121
|
-
content: r.content,
|
|
1122
|
-
context: r.context,
|
|
1123
|
-
hash: r.hash,
|
|
1124
|
-
fileHash: r.fileHash ?? "",
|
|
1125
|
-
vector: Array.from(r.vector),
|
|
1126
|
-
}));
|
|
1127
|
-
const conn = await this.connection();
|
|
1128
|
-
const destTables = await conn.tableNames();
|
|
1129
|
-
if (destTables.includes(CHUNKS_TABLE)) {
|
|
1130
|
-
const t = await conn.openTable(CHUNKS_TABLE);
|
|
1131
|
-
this.chunksTable = t;
|
|
1132
|
-
await t.add(rows);
|
|
1133
|
-
}
|
|
1134
|
-
else {
|
|
1135
|
-
this.chunksTable = await conn.createTable(CHUNKS_TABLE, rows);
|
|
1136
|
-
}
|
|
1137
|
-
chunks = rows.length;
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
if (sourceTables.includes(FILES_TABLE)) {
|
|
1141
|
-
const sourceFiles = await sourceConn.openTable(FILES_TABLE);
|
|
1142
|
-
const rawRows = await sourceFiles.query().toArray();
|
|
1143
|
-
if (rawRows.length > 0) {
|
|
1144
|
-
// Ensure branch column exists (legacy DBs won't have it)
|
|
1145
|
-
const rows = rawRows.map((r) => ({
|
|
1146
|
-
filePath: r.filePath,
|
|
1147
|
-
fileHash: r.fileHash,
|
|
1148
|
-
branch: r.branch ?? this.branch,
|
|
1149
|
-
}));
|
|
1150
|
-
const conn = await this.connection();
|
|
1151
|
-
const destTables = await conn.tableNames();
|
|
1152
|
-
if (destTables.includes(FILES_TABLE)) {
|
|
1153
|
-
const t = await conn.openTable(FILES_TABLE);
|
|
1154
|
-
this.filesTable = t;
|
|
1155
|
-
await t.add(rows);
|
|
1156
|
-
}
|
|
1157
|
-
else {
|
|
1158
|
-
this.filesTable = await conn.createTable(FILES_TABLE, rows);
|
|
1159
|
-
}
|
|
1160
|
-
files = rows.length;
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
return { chunks, files };
|
|
1164
|
-
}
|
|
1165
|
-
/**
|
|
1166
|
-
* Return all distinct branch names present in the files table.
|
|
1167
|
-
*/
|
|
1168
|
-
async getStoredBranches() {
|
|
1169
|
-
const t = await this.openFiles();
|
|
1170
|
-
if (!t)
|
|
1171
|
-
return [];
|
|
1172
|
-
const rows = await t.query().select(["branch"]).toArray();
|
|
1173
|
-
return [...new Set(rows.map((r) => r.branch))];
|
|
1174
|
-
}
|
|
1175
|
-
/**
|
|
1176
|
-
* Delete all file manifest rows for a given branch.
|
|
1177
|
-
*/
|
|
1178
|
-
async deleteBranchManifest(branch) {
|
|
1179
|
-
const t = await this.openFiles();
|
|
1180
|
-
if (!t)
|
|
1181
|
-
return;
|
|
1182
|
-
const escaped = branch.replace(/'/g, "''");
|
|
1183
|
-
await t.delete(`branch = '${escaped}'`);
|
|
1184
|
-
}
|
|
1185
|
-
/**
|
|
1186
|
-
* Copy another branch's file manifest to this store's branch.
|
|
1187
|
-
* Used to bootstrap a new branch from a merge base.
|
|
1188
|
-
*/
|
|
1189
|
-
async copyBranchManifest(sourceBranch) {
|
|
1190
|
-
const t = await this.openFiles();
|
|
1191
|
-
if (!t)
|
|
1192
|
-
return 0;
|
|
1193
|
-
const escaped = sourceBranch.replace(/'/g, "''");
|
|
1194
|
-
const rows = await t
|
|
1195
|
-
.query()
|
|
1196
|
-
.where(`branch = '${escaped}'`)
|
|
1197
|
-
.select(["filePath", "fileHash"])
|
|
1198
|
-
.toArray();
|
|
1199
|
-
if (rows.length === 0)
|
|
1200
|
-
return 0;
|
|
1201
|
-
const records = rows.map((r) => ({
|
|
1202
|
-
filePath: r.filePath,
|
|
1203
|
-
fileHash: r.fileHash,
|
|
1204
|
-
branch: this.branch,
|
|
1205
|
-
}));
|
|
1206
|
-
await t.add(records);
|
|
1207
|
-
return records.length;
|
|
1208
|
-
}
|
|
1209
|
-
async close() {
|
|
1210
|
-
this.chunksTable = undefined;
|
|
1211
|
-
this.filesTable = undefined;
|
|
1212
|
-
this.vocabTable = undefined;
|
|
1213
|
-
this.db = undefined;
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1216
|
-
//# sourceMappingURL=store.js.map
|