/** * Repository Manager * * Manages CodraGraph index storage in .codragraph/ at repo root. * Also maintains a global registry at ~/.codragraph/registry.json * so the MCP server can discover indexed repos from any cwd. */ /** * Normalise a repo path for registry comparison across platforms * (#664 review feedback from @evander-wang). * * Why this exists: `path.resolve` alone is NOT enough for * cross-platform registry stability. * - **macOS**: tmpdirs and `/var` are symlinks to `/private/var`. * A child process that stored `/private/var/folders/.../repo` in * the registry cannot later be matched by an outer caller that * supplies the symlink form `/var/folders/.../repo`. `path.resolve` * does not follow symlinks; `realpathSync.native` does. * - **Windows**: GitHub runners surface tmpdirs in 8.3 short-name * form (`RUNNERA~1\...`), but `process.cwd()` often returns the * long form (`runneradmin\...`). `realpathSync.native` normalises * both sides to the long-name canonical path. * * Fallback behaviour: if the path does not exist on disk (e.g. a user * passed `codragraph remove some-alias` and the alias misses every * registry entry, or the caller is resolving a path that was deleted * after registration), we return `path.resolve(p)` rather than * throwing. This preserves the idempotent-on-missing semantics of * `resolveRegistryEntry` / `remove`. * * Backwards compatibility: this function is applied to BOTH the * caller-supplied input AND each stored `entry.path` at compare time * inside `resolveRegistryEntry`, so registries written by older * versions (where `registerRepo` only ran `path.resolve`) still match * correctly. Newly-written entries are canonicalised at write time too * so the registry stabilises over analyze/re-analyze cycles. */ export declare const canonicalizePath: (p: string) => string; /** * On-disk schema version for `.codragraph/cgdb` and `.codragraph/meta.json`. * * 1 — pre-RFC-0001-Phase-2 layout. Node tables have `content STRING` * but no `contentEncoding` column. Implicit/missing on existing * 1.6.x and 1.7.x indexes (RepoMeta.schemaVersion was undefined). * 2 — RFC 0001 Phase 2: every node table that has `content` also has * a `contentEncoding STRING DEFAULT 'none'` column. Writers may * opt into compression via `--compress brotli|zstd` (compression * is OFF by default, so existing readers keep working). Readers * decode based on the per-row encoding tag. * 3 - FeatureCluster layer: adds FeatureCluster nodes plus * FEATURE_MEMBER_OF / FEATURE_DEPENDS_ON relation rows. * 4 - FeatureCluster context-pack metadata: summary, repo/service, * routes, tools, test coverage hints, and indexed commit fields. * * Bumping this is the migration trigger: `runFullAnalysis` forces a * full re-analyze when an existing index has a missing or older * `schemaVersion` field, because adding a column to an existing * LadybugDB table via ALTER is not validated end-to-end yet — fresh * `CREATE NODE TABLE` is the supported path. */ export declare const INDEX_SCHEMA_VERSION: 4; export interface RepoMeta { repoPath: string; lastCommit: string; indexedAt: string; /** * On-disk schema version (see {@link INDEX_SCHEMA_VERSION}). Absent on * indexes written by 1.7.x or earlier; `runFullAnalysis` treats those * as needing a full re-analyze when they're loaded by a 1.8+ CLI. */ schemaVersion?: number; /** * RFC 0001 Phase 2 — the per-row content encoding chosen at the last * `analyze --compress` invocation. `'none'` (or absent) means rows * carry plain text; `'brotli'` / `'zstd'` means rows are compressed * and consumers must decode. Persisted so query-time tooling can * detect the compressed mode without sampling rows. * * Phase 2.5 hooks: `core/search/bm25-index.ts` reads this field at * FTS-create time and drops `content` from the FTS property list * when set to a non-`'none'` value (full-text search falls back to * symbol-name matches). Embeddings and graph queries are unaffected * — they decode at the read boundary. */ compress?: 'none' | 'brotli' | 'zstd'; /** * Query-time index capabilities written by analyze after optional search * indexes are successfully created. Read-only MCP/CLI query paths use this * to avoid expensive probes for indexes that older analyses never wrote. */ searchIndexes?: { fts?: boolean; }; /** * Runtime policy chosen by `analyze --profile auto|lean|balanced|power`. * This is diagnostic metadata only; readers must not require it because * older indexes do not have a policy record. */ adaptiveProfile?: { requested?: 'auto' | 'lean' | 'balanced' | 'power'; resolved?: 'lean' | 'balanced' | 'power'; platform?: NodeJS.Platform; arch?: NodeJS.Architecture; cpuCount?: number; totalMemoryBytes?: number; heapLimitBytes?: number; compression?: 'none' | 'brotli' | 'zstd'; embeddingMode?: 'auto' | 'off' | 'on'; embeddingNodeLimit?: number; embeddingDecision?: 'enabled' | 'skipped'; embeddingReason?: string; workerPoolSize?: number; workerSubBatchSize?: number; }; /** * Canonical `origin` remote URL captured at index time. Used to * fingerprint the same logical repo across multiple on-disk clones * (worktrees, agent workspaces, "clean clone for indexing"). When * absent (no remote configured, git unavailable, etc.) the repo is * treated as path-only and sibling-clone detection is skipped. */ remoteUrl?: string; /** * Phase 4 — versioned graph storage. `currentBranch` is the * graphstore branch the most recent analyze advanced; `headCommit` * is the commit id that branch now points at. Both `undefined` when * the graphstore hook didn't produce a snapshot (best-effort path — * see `core/graphstore/`). Read by the dashboard and the upcoming * `codragraph log` / `diff` CLI commands; not consulted by the live * query path which still goes straight to LadybugDB. */ currentBranch?: string; headCommit?: string; stats?: { files?: number; nodes?: number; edges?: number; communities?: number; featureClusters?: number; processes?: number; embeddings?: number; }; } export interface IndexedRepo { repoPath: string; storagePath: string; cgdbPath: string; metaPath: string; meta: RepoMeta; } /** * Shape of an entry in the global registry (~/.codragraph/registry.json) */ export interface RegistryEntry { name: string; path: string; storagePath: string; indexedAt: string; lastCommit: string; /** See {@link RepoMeta.remoteUrl}. Mirrored from meta at register time. */ remoteUrl?: string; /** Mirrored from {@link RepoMeta.currentBranch} when available. */ currentBranch?: string; /** Mirrored from {@link RepoMeta.headCommit} when available. */ headCommit?: string; stats?: RepoMeta['stats']; } /** * Get the .codragraph storage path for a repository */ export declare const getStoragePath: (repoPath: string) => string; /** * Get paths to key storage files */ export declare const getStoragePaths: (repoPath: string) => { storagePath: string; cgdbPath: string; metaPath: string; }; /** * Check whether a KuzuDB index exists in the given storage path. * Non-destructive — safe to call from status commands. */ export declare const hasKuzuIndex: (storagePath: string) => Promise; /** * Clean up stale KuzuDB files after migration to LadybugDB. * * Returns: * found — true if .codragraph/kuzu existed and was deleted * needsReindex — true if kuzu existed but cgdb does not (re-analyze required) * * Callers own the user-facing messaging; this function only deletes files. */ export declare const cleanupOldKuzuFiles: (storagePath: string) => Promise<{ found: boolean; needsReindex: boolean; }>; /** * Load metadata from an indexed repo */ export declare const loadMeta: (storagePath: string) => Promise; /** * Save metadata to storage */ export declare const saveMeta: (storagePath: string, meta: RepoMeta) => Promise; /** * Check if a path has a CodraGraph index */ export declare const hasIndex: (repoPath: string) => Promise; /** * Load an indexed repo from a path */ export declare const loadRepo: (repoPath: string) => Promise; /** * Find .codragraph by walking up from a starting path */ export declare const findRepo: (startPath: string) => Promise; /** * Add .codragraph to .gitignore if not already present */ export declare const addToGitignore: (repoPath: string) => Promise; /** * Get the path to the global CodraGraph directory */ export declare const getGlobalDir: () => string; /** * Get the path to the global registry file */ export declare const getGlobalRegistryPath: () => string; /** * Read the global registry. Returns empty array if not found. */ export declare const readRegistry: () => Promise; /** * Options for {@link registerRepo}. All optional — callers without any * disambiguation requirement can keep calling `registerRepo(path, meta)` * unchanged. */ export interface RegisterRepoOptions { /** * User-provided alias from `analyze --name ` (#829). Overrides * the default basename-derived registry `name`. Persisted — subsequent * re-analyses of the same path without `--name` preserve the alias. */ name?: string; /** * Allow two DIFFERENT repo paths to register under the same alias * (#829). Mapped from the `--allow-duplicate-name` CLI flag. * * Scope: this flag governs cross-path alias sharing only — one repo * path always has exactly one registry entry (and therefore exactly * one alias). Re-analyzing the same path with `--name Y` overwrites * a previous `--name X`; it does NOT create a second entry or a * second alias for the same path (see the upsert-by-resolved-path * logic in {@link registerRepo} and the * `re-registerRepo with a different name overrides the previous * alias` test in `test/unit/repo-manager.test.ts`). * * Distinct from `--force` (which only triggers pipeline re-index); * a user accepting a duplicate alias should not be forced to also * re-run the full pipeline. */ allowDuplicateName?: boolean; } /** * Thrown by {@link registerRepo} when a requested name is already in * use by a DIFFERENT path. The CLI layer surfaces this as an actionable * error instead of relying on `.message` string-matching. * * The colliding alias is exposed as `err.registryName` (not `err.name`). * `err.name` keeps its inherited `Error.prototype.name` semantics (the * class name) so downstream code can do the usual `err.name === * 'RegistryNameCollisionError'` checks; use the `kind` discriminant or * `instanceof RegistryNameCollisionError` for type-safe narrowing. */ export declare class RegistryNameCollisionError extends Error { readonly registryName: string; readonly existingPath: string; readonly requestedPath: string; readonly kind: "RegistryNameCollisionError"; constructor(registryName: string, existingPath: string, requestedPath: string); } /** * Register (add or update) a repo in the global registry. * Called after `codragraph analyze` completes. * * Name resolution precedence (#829, #979): * 1. explicit `opts.name` (from `analyze --name `) * 2. preserved alias on an existing entry for this path * 3. `git config --get remote.origin.url` repo name (#979 — recovers * a meaningful name for monorepo subprojects, git worktrees, and * Gas-Town-style `/refinery/rig/` layouts where the basename * is generic) * 4. `path.basename(repoPath)` (the original default) * * Duplicate-name guard: if another path already uses the resolved * `name`, throw {@link RegistryNameCollisionError} unless * `opts.allowDuplicateName` is set. The guard ONLY fires when the user explicitly passed a * `name`; un-aliased basename collisions continue to register silently * so existing users who don't know about `--name` see no behaviour * change. * * Returns the `name` that was actually written to the registry — the * caller can re-use it to keep AGENTS.md / skill files aligned with the * MCP-visible repo name (#979). */ export declare const registerRepo: (repoPath: string, meta: RepoMeta, opts?: RegisterRepoOptions) => Promise; /** * Remove a repo from the global registry. * Called after `codragraph clean`. */ export declare const unregisterRepo: (repoPath: string) => Promise; /** * Thrown by {@link resolveRegistryEntry} when no registered repo matches * the caller's target string (by alias, basename, remote-inferred name, * or resolved path). CLI callers that want idempotent "remove" semantics * should catch this and exit 0 with a warning; non-idempotent callers * (e.g. MCP tools) can surface the error directly. */ export declare class RegistryNotFoundError extends Error { readonly target: string; readonly availableNames: string[]; readonly kind: "RegistryNotFoundError"; constructor(target: string, availableNames: string[]); } /** * Thrown by {@link resolveRegistryEntry} when the target string matches * the `name` of two or more entries — only possible when the user * previously registered duplicates via `analyze --name X * --allow-duplicate-name` (#829). The error carries enough information * for the caller to render an actionable disambiguation hint without * string-matching on `.message`. * * `kind` is a string literal discriminant (same pattern as * {@link RegistryNameCollisionError}) so callers can narrow via * `err.kind === 'RegistryAmbiguousTargetError'` without importing the * class. */ export declare class RegistryAmbiguousTargetError extends Error { readonly target: string; readonly matches: RegistryEntry[]; readonly kind: "RegistryAmbiguousTargetError"; constructor(target: string, matches: RegistryEntry[]); } /** * Thrown by {@link assertSafeStoragePath} when a registry entry's * `storagePath` does NOT point at the expected `/.codragraph` * subfolder. CLI destructive commands (`remove`, `clean --all`) should * catch this and exit non-zero without deleting anything — the usual * cause is a corrupted or hand-edited `~/.codragraph/registry.json`, and * proceeding would mean `fs.rm(recursive: true)` on whatever odd path * the entry is pointing at. */ export declare class UnsafeStoragePathError extends Error { readonly entry: RegistryEntry; readonly expectedStoragePath: string; readonly actualStoragePath: string; readonly kind: "UnsafeStoragePathError"; constructor(entry: RegistryEntry, expectedStoragePath: string, actualStoragePath: string); } /** * Guard rail for destructive CLI paths (`remove` #664, * `clean --all` #258, future MCP `remove` tool): verify that a * registry entry's `storagePath` is the canonical `/.codragraph` * subfolder of its `path`. If not, throw {@link UnsafeStoragePathError} * so the caller exits without touching disk. * * Why this exists (#1003 review — @magyargergo): * - `~/.codragraph/registry.json` is a plain-text user-writable file. * A corrupted, hand-edited, or downgrade/upgrade-racing entry * could plausibly end up with `storagePath === ""` (resolves to * cwd), `storagePath === path` (the repo root!), `storagePath` * equal to a parent/sibling of the repo, or simply any arbitrary * filesystem path. * - `fs.rm(recursive: true, force: true)` on ANY of those would be * a runtime disaster — at best delete the user's working tree, at * worst nuke an unrelated directory tree they happen to own. * - `clean` (default, cwd-scoped) is safe by construction — it * re-derives storagePath from `findRepo(cwd)` and never trusts * the registry field. But `clean --all` DOES iterate the registry * and trust each entry's stored storagePath (same shape as * `remove`), so this helper must be wired into that loop too. * - `server/api.ts` recomputes storagePath from `getStoragePath(entry.path)` * and so is likewise safe-by-construction. * * Pure string check — does NOT require the paths to exist on disk. * Windows: case-insensitive; POSIX: case-sensitive. Matches the * comparison shape used elsewhere in this module. */ export declare const assertSafeStoragePath: (entry: RegistryEntry) => void; /** * Resolve a user-supplied target string (from `codragraph remove ` * or equivalent MCP tool argument) to a single registry entry. * * Match precedence (first hit wins, subsequent tiers are only tried if * the prior tier produces zero matches): * 1. Exact resolved-path match (Windows: case-insensitive). * Paths are unique by registry construction, so a path match can * never be ambiguous. * 2. Exact `name` match (case-insensitive). If ≥ 2 entries share the * name — only possible via `--allow-duplicate-name` (#829) — * throws {@link RegistryAmbiguousTargetError}. * * No fuzzy / partial matching — unambiguous, scriptable behaviour is * more important than convenience for destructive commands. * * Throws {@link RegistryNotFoundError} if no entry matches. * * `entries` is passed in (rather than re-read) so callers that already * hold the registry snapshot (e.g. to print a "before" state) can avoid * a second disk read, and so tests can inject fixtures without touching * `CODRAGRAPH_HOME`. */ export declare const resolveRegistryEntry: (entries: RegistryEntry[], target: string) => RegistryEntry; /** * List all registered repos from the global registry. * Optionally validates that each entry's .codragraph/ still exists. */ export declare const listRegisteredRepos: (opts?: { validate?: boolean; }) => Promise; /** * Identifiers for inference / API-key providers known to CodraGraph. * Adding a new one is a one-line union extension; tooling that reads * the config switches on this name. */ export type ProviderName = 'claude' | 'openai' | 'opencode' | 'openrouter' | 'azure' | 'cursor' | 'custom'; export interface ProviderConfig { /** API key (BYO). When absent, fallback is the matching env var. */ apiKey?: string; /** Optional override of the provider's default base URL. */ baseUrl?: string; /** Default model for this provider when one isn't specified per-call. */ model?: string; /** Azure-only: api-version query param (e.g. '2024-10-21'). */ apiVersion?: string; /** Set true when the deployment is a reasoning model (Azure only — auto-detected for OpenAI). */ isReasoningModel?: boolean; /** Free-form metadata users can attach (cursorModel, deployment notes, etc.). */ meta?: Record; } export interface CLIConfig { apiKey?: string; model?: string; baseUrl?: string; provider?: 'openai' | 'openrouter' | 'azure' | 'custom' | 'cursor'; cursorModel?: string; apiVersion?: string; isReasoningModel?: boolean; /** * Per-provider config keyed by {@link ProviderName}. Read by the * harness's makeInferenceProvider, the web settings panel, and * future MCP tools that need credentials. */ providers?: Partial>; /** * Reserved for the future hosted/org plane. When set, codragraph * tools that talk to a hosted CodraGraph instance authenticate with * this key. v0.1 ships the slot only — no server validates it yet. */ codragraphApiKey?: string; /** Hosted org instance URL, when applicable. */ codragraphServerUrl?: string; } /** * Get the path to the global CLI config file */ export declare const getGlobalConfigPath: () => string; /** * Load CLI config from ~/.codragraph/config.json */ export declare const loadCLIConfig: () => Promise; /** * Save CLI config to ~/.codragraph/config.json */ export declare const saveCLIConfig: (config: CLIConfig) => Promise; /** * Resolve a provider's effective config. Resolution order: * 1. config.providers[name] (canonical, multi-provider) * 2. config.{apiKey,model,...} when the legacy provider field matches * 3. process.env[PROVIDER_ENV_VARS[name]] (BYO via env) * Returns `null` when nothing is configured for the provider. */ export declare const getProviderConfig: (config: CLIConfig, name: ProviderName) => ProviderConfig | null; /** * Update a single provider's config. Persists to disk. Pass `patch` * with only the fields you want to change — others are merged. */ export declare const setProviderConfig: (name: ProviderName, patch: Partial) => Promise; /** Delete a provider's config. Idempotent. */ export declare const removeProviderConfig: (name: ProviderName) => Promise; /** List every configured provider with non-secret fields. Used by `codragraph config list`. */ export declare const listProviderConfigs: () => Promise>; /** * Find other registered entries whose `remoteUrl` matches the given * one, excluding `selfPath` (case-insensitive on Windows). Entries * without a `remoteUrl` are ignored — we cannot prove sibling-ness * without a fingerprint. */ export declare const findSiblingClones: (remoteUrl: string | undefined, selfPath: string) => Promise; /** * Description of how a working directory relates to a registered index. * * `match` semantics: * - `path` — `cwd` is inside the registered entry's path. * - `sibling-by-remote` — `cwd` is in a different on-disk clone of the * same repo (same `remoteUrl`). * - `none` — no relationship found. */ export interface CwdMatch { match: 'path' | 'sibling-by-remote' | 'none'; entry?: RegistryEntry; /** The git toplevel of `cwd`, when `cwd` is inside a git work tree. */ cwdGitRoot?: string; /** HEAD of the cwd's clone, when resolvable. */ cwdHead?: string; /** * Number of commits the registered `lastCommit` is behind the * sibling-clone HEAD, when both refs are known to the cwd's clone. * `undefined` when the comparison cannot be performed (e.g. the * indexed commit isn't reachable from cwd). */ drift?: number; /** Human-readable hint, set whenever the situation warrants warning. */ hint?: string; }