UNPKG

socraticode

Version:

SocratiCode — MCP server for local codebase indexing, semantic search, and code dependency graphs. All private, all local via Docker.

327 lines • 17.9 kB
import { QdrantClient } from "@qdrant/js-client-rest"; import type { ArtifactIndexState, CodeGraph, FileChunk, SearchResult } from "../types.js"; import type { GraphInputRecord } from "./graph-inputs.js"; import { type EffectiveIndexProfile } from "./index-profile.js"; /** * Render a Qdrant client error including the reason the server actually gave. * * `@qdrant/js-client-rest` builds an `ApiError` whose `message` is only the HTTP * status text — a 400 reads as the bare, useless "Bad Request" — while the * server's explanation ("JSON payload (N bytes) is larger than allowed …") sits * in `err.data.status.error`. Logging `err.message` alone therefore throws away * the one part a user can act on, so every symbol-graph failure looked * identical. Appends that reason when present, and looks through a `cause` * chain so an error already wrapped by {@link wrapQdrantError} still resolves. * * Without a server reason, appends the innermost cause's message and `code` * instead, so a transport failure reads as * `fetch failed: other side closed [UND_ERR_SOCKET]` rather than `fetch failed`. */ export declare function describeQdrantError(err: unknown): string; export declare function getClient(): QdrantClient; /** * Ensure a code collection is ready, sharing one attempt between concurrent * callers in this process. Failed attempts are removed so a later call retries. */ export declare function ensureCollection(name: string): Promise<void>; /** True when an error means "someone else already created it" — safe to ignore. */ export declare function isAlreadyExistsError(err: unknown): boolean; /** Create a payload index on a collection (best effort — never throws) */ export declare function ensurePayloadIndex(collName: string, fieldName: string): Promise<void>; /** Delete a collection */ export declare function deleteCollection(name: string): Promise<void>; export type ProjectPathState = "present-on-this-host" | "absent-on-this-host" | "unknown/inaccessible"; /** One metadata point, projected to the fields that decide whether a preview is still current. */ export interface ProjectMetadataRecord { pointId: string | number; collectionName: string; projectPath: string | null; indexingStatus: string | null; lastIndexedAt: string | null; lastBuiltAt: string | null; builtByVersion: string | null; /** Absent in legacy records. Refused or malformed ownership remains report-only. */ localIndexOwnership?: unknown; } export interface ProjectReclamationEntry { identity: string; projectPath: string | null; canonicalPath: string | null; pathState: ProjectPathState; resourceCollections: string[]; metadataRecords: ProjectMetadataRecord[]; /** Some record says an indexer has this identity mid-write. */ inProgress: boolean; possibleSuperseded: boolean; requiresManualInspection: boolean; /** Why a person has to look before this identity can be deleted; empty when nothing holds it back. */ manualInspectionReasons: string[]; confirmationToken: string; } /** A metadata point the inventory could not attribute to an identity, kept for a person to look at. */ export interface UnrecognisedMetadataEntry { pointId: string | number; collectionName: string | null; projectPath: string | null; reason: string; } /** A collection under the configured prefix whose name fits two identities and nothing settles which. */ export interface UnattributedCollection { name: string; reason: string; /** Every identity the name fits; each is held back from deletion while the name stays unsettled. */ candidateIdentities: string[]; } export interface ProjectReclamationInventory { entries: ProjectReclamationEntry[]; unrecognisedMetadata: UnrecognisedMetadataEntry[]; unattributedCollections: UnattributedCollection[]; } export interface ProjectReclamationOutcome { resource: string; kind: "collection" | "metadata"; /** `skipped`: the caller withdrew consent before this write; nothing was attempted. */ outcome: "deleted" | "failed" | "skipped"; error?: string; } /** * Read the stored identities without deriving an identity from the local path. * The result is an inventory: an absent path is local observation, never proof * that a shared-store index may be deleted. */ export declare function getProjectReclamationInventory(): Promise<ProjectReclamationInventory>; /** Delete exactly the inventoried resources, one outcome each; a resource already gone counts as deleted. */ export declare function removeProjectReclamationEntry(entry: ProjectReclamationEntry, /** Asked before every write; once it answers false, nothing more is attempted. */ mayContinue?: () => boolean | Promise<boolean>, /** Automatic reclamation retains its proof until collection deletion is verified. Manual prune is unchanged. */ preserveMetadataUntilCollectionsGone?: boolean): Promise<ProjectReclamationOutcome[]>; /** List all codebase, codegraph, and context artifact entries. * Codebase and context entries are actual collections; codegraph entries come from metadata. * * The collection-name filters honour `QDRANT_COLLECTION_PREFIX` so when * sharing a Qdrant server with other applications or other SocratiCode * instances, only this instance's collections are listed. */ export declare function listCodebaseCollections(): Promise<string[]>; /** Upsert chunks into a collection */ export declare function upsertChunks(collectionName: string, chunks: FileChunk[], contentHash: string, profile: EffectiveIndexProfile): Promise<void>; /** Upsert pre-embedded points into a collection (no embedding generation). * bm25Text is forwarded to Qdrant's server-side BM25 inference (truncated if too long). * * A failing batch is retried point by point to isolate the bad point(s). If any * point still fails, this throws: a partial write must fail the whole indexing * operation. * * Callers must not treat a partial write as success. A re-indexed file has its * previous chunks deleted before this call, so recording it as indexed after a * partial failure strands it at zero chunks with a content hash that suppresses * every future re-index. Throwing leaves stored hashes and earlier checkpoints * untouched, so the next index or update retries the file naturally. */ export declare function upsertPreEmbeddedChunks(collectionName: string, points: Array<{ id: string; vector: number[]; bm25Text: string; payload: Record<string, unknown>; }>): Promise<void>; /** Delete a file's chunks, optionally waiting for application before reporting freshness. */ export declare function deleteFileChunks(collectionName: string, relativePath: string, waitForCompletion?: boolean): Promise<void>; /** Hybrid search: combines dense semantic search with BM25 lexical search via RRF fusion. * Dense vector is generated client-side; BM25 inference runs server-side in Qdrant (requires v1.15.2+). */ export declare function searchChunks(collectionName: string, query: string, limit?: number, fileFilter?: string, languageFilter?: string): Promise<SearchResult[]>; export declare function loadEffectiveIndexProfileForCollection(collectionName: string): Promise<EffectiveIndexProfile>; /** Merge results from multiple collection queries using client-side Reciprocal Rank Fusion. * Deduplicates by `label::relativePath` so that files with the same relative path * in different projects are kept as separate hits. Within a single project, * the first (higher-priority) occurrence wins on conflict. * Exported for unit testing. */ export declare function mergeMultiCollectionResults(collectionResults: Array<{ label: string; results: SearchResult[]; }>, limit: number): SearchResult[]; /** Search across multiple collections in parallel with client-side RRF fusion and deduplication. * Each collection's results are queried independently, then merged using Reciprocal Rank Fusion. * When the same relativePath appears in multiple collections, the result from the * earlier (higher-priority) collection wins. * * @param collections - Array of { name, label } where label identifies the source project * in results. Order defines priority for deduplication (first wins). * @param query - Natural language search query. * @param limit - Maximum total results to return after merge. * @param fileFilter - Optional relativePath filter applied to every collection. * @param languageFilter - Optional language filter applied to every collection. */ export declare function searchMultipleCollections(collections: Array<{ name: string; label: string; }>, query: string, limit?: number, fileFilter?: string, languageFilter?: string): Promise<SearchResult[]>; /** Hybrid search with arbitrary payload filters. * Used by context artifacts to filter by artifactName. */ export declare function searchChunksWithFilter(collectionName: string, query: string, limit: number, filters: Array<{ key: string; value: string; }>): Promise<SearchResult[]>; /** * Every `relativePath` that currently has at least one point in the collection. * * Exists so a caller can tell "this file is unchanged, skip it" apart from "this * file's chunks are gone". The stored hash map cannot make that distinction on * its own: it records what was hashed, not what survived. * * Pages through with `scroll` rather than `facet` deliberately — facet takes a * limit and offers no cursor, so it cannot enumerate a repository's worth of * paths reliably. Payload is narrowed to the one field and vectors are excluded, * so the cost is one small round trip per `pageSize` points. */ export declare function listIndexedFilePaths(collName: string, pageSize?: number): Promise<Set<string>>; /** Get collection info. * Returns the collection info if it exists, null if the collection does not exist, * or throws an error if the request fails for any other reason (network, timeout, etc.). * This distinction is critical: callers must NOT treat transient errors as "collection missing". */ export interface CollectionInfo { pointsCount: number; status: string; /** Stored dense-vector width, when Qdrant exposes a supported vector config. */ denseVectorSize?: number; } /** * Read a collection's point count, status and dense vector size. * * Returns `null` only when the collection does not exist; every other failure * propagates. * * A read of a collection this process is still initializing waits for that * initialization first: Qdrant can answer a read made during the create with a * 500 rather than 404 or the new collection. If the initialization fails, that * failure propagates. Reads with no local initialization in flight are * unaffected. */ export declare function getCollectionInfo(name: string): Promise<CollectionInfo | null>; /** Reset the metadata collection readiness cache (for testing only) */ export declare function resetMetadataCollectionCache(): void; /** Indexing status persisted in Qdrant metadata */ export type IndexingStatus = "in-progress" | "completed"; export interface ProjectMetadata { projectPath: string; lastIndexedAt: string; filesTotal: number; filesIndexed: number; indexingStatus: IndexingStatus; effectiveProfile: EffectiveIndexProfile | null; } /** Save project metadata and file hashes to Qdrant */ export declare function saveProjectMetadata(collName: string, projectPath: string, filesTotal: number, filesIndexed: number, fileHashes: Map<string, string>, indexingStatus: IndexingStatus, effectiveProfile: EffectiveIndexProfile): Promise<void>; /** Load only the stored code-index profile. Missing profile means legacy metadata. */ export declare function loadProjectEffectiveProfile(collName: string): Promise<EffectiveIndexProfile | null>; /** Load file hashes for a project from Qdrant. * Returns the hash map if found, null if the metadata point doesn't exist, * or throws on transient/unexpected errors so callers can distinguish * "no metadata" from "Qdrant unreachable". */ export declare function loadProjectHashes(collName: string): Promise<Map<string, string> | null>; /** Get project metadata (for list display). * Returns null if metadata doesn't exist or on any error (logged as warning). */ export declare function getProjectMetadata(collName: string): Promise<ProjectMetadata | null>; /** * Persisted indexing status, without the best-effort swallowing. * * `getProjectMetadata()` is display-oriented: it catches every read error and * answers `null`, which is right for showing a status line and wrong for * deciding one. A caller that asks "was the last run interrupted?" and takes * `null` for "no" will, on a transient metadata read failure, skip the recovery * that failure should have triggered — and then persist the still-damaged index * as `completed`, which is worse than not having run at all. * * Returns `null` only when there is genuinely no metadata for the collection. * Transport, parsing and validation failures propagate, so the caller aborts * rather than guessing. */ export declare function loadIndexingStatus(collName: string): Promise<IndexingStatus | null>; /** Delete project metadata. * Errors are logged but not propagated (best-effort deletion). */ export declare function deleteProjectMetadata(collName: string): Promise<void>; /** Save a code graph to Qdrant as a single metadata point */ export declare function saveGraphData(graphCollName: string, projectPath: string, graph: CodeGraph, graphInputs: GraphInputRecord): Promise<void>; /** Load a code graph from Qdrant. * Returns null if no graph exists or on any error (logged as warning). */ export declare function loadGraphData(graphCollName: string): Promise<CodeGraph | null>; /** Get graph metadata (for list/status display). * Returns null if no graph exists or on any error (logged as warning). */ export declare function getGraphMetadata(graphCollName: string): Promise<{ projectPath: string; lastBuiltAt: string; nodeCount: number; edgeCount: number; /** Absent on graphs persisted before this field was recorded. */ importCount?: number; /** SocratiCode version that built this graph. Absent on graphs persisted * before this field was recorded. */ builtByVersion?: string; } | null>; /** * What is stored about the graph's inputs, and whether anything is stored at * all — three outcomes the caller must not confuse. */ export type GraphInputsLoad = /** No metadata point: this project has no persisted graph. */ { status: "absent"; } /** A graph is there. `value` is its record, or undefined on one built before records existed. */ | { status: "stored"; value: unknown; } /** The read itself failed. Nothing is known, least of all that there is no record. */ | { status: "failed"; error: string; }; /** * The record of what the graph was built from, without the graph. * * Projected to that one field deliberately. The same point carries * `graphData`, the whole serialized graph — megabytes on a large repository — * and every incremental update asks this question. Pulling the graph across to * answer it would cost more than the rebuild the answer is meant to avoid — * the same amplification that made `listCodebaseCollections` project its * scroll rather than read whole payloads. * * A missing point and a failed read are reported apart, and neither is * reported as "no record". A graph with no record must rebuild once and write * one however quiet the update was; a graph that could not be read tells us * nothing and must be treated as the most conservative of the three; and only * a genuinely absent point means there is no graph here to refresh. */ export declare function loadGraphInputs(graphCollName: string): Promise<GraphInputsLoad>; /** Delete graph data from metadata. * Errors are logged but not propagated (best-effort deletion). */ export declare function deleteGraphData(graphCollName: string): Promise<void>; /** Save context artifact metadata to Qdrant */ export declare function saveContextMetadata(contextCollName: string, projectPath: string, artifacts: ArtifactIndexState[], effectiveProfile: EffectiveIndexProfile): Promise<void>; export interface ContextIndexMetadata { artifacts: ArtifactIndexState[]; effectiveProfile: EffectiveIndexProfile | null; } /** * Load context states and profile for mutation paths. Missing metadata returns * null; transport, JSON, and profile validation failures propagate. */ export declare function loadContextIndexMetadata(contextCollName: string): Promise<ContextIndexMetadata | null>; /** Load only the stored context-index profile. Missing profile means legacy metadata. */ export declare function loadContextEffectiveProfile(contextCollName: string): Promise<EffectiveIndexProfile | null>; /** Load context artifact metadata from Qdrant. * Returns null if no metadata exists or on any error (logged as warning). */ export declare function loadContextMetadata(contextCollName: string): Promise<ArtifactIndexState[] | null>; /** Get context collection metadata (for list/status display). * Returns null if no metadata exists or on any error (logged as warning). */ export declare function getContextMetadata(contextCollName: string): Promise<{ projectPath: string; lastIndexedAt: string; artifactCount: number; effectiveProfile: EffectiveIndexProfile | null; } | null>; /** Delete context artifact metadata. * Errors are logged but not propagated (best-effort deletion). */ export declare function deleteContextMetadata(contextCollName: string): Promise<void>; /** Delete all chunks for a specific artifact within a collection */ export declare function deleteArtifactChunks(collectionName: string, artifactName: string): Promise<void>; //# sourceMappingURL=qdrant.d.ts.map