import { EventEmitter } from "node:events"; export declare const CLANGD_MISSING = "CLANGD_MISSING"; export declare const NO_COMPILE_COMMANDS = "NO_COMPILE_COMMANDS"; export declare const CLANGD_TIMEOUT = "CLANGD_TIMEOUT"; export declare const CLANGD_DIED = "CLANGD_DIED"; export declare const LSP_ERROR = "LSP_ERROR"; export declare const CLANGD_CANCELLED = "CANCELLED"; /** Same `{code, message, hint, details}` shape the daemon errors carry, so the * tool layer can render both through one envelope. */ export declare class ClangdError extends Error { code: string; hint?: string; details: Record; constructor(code: string, message: string, hint?: string, details?: Record); } export declare const CLANGD_INSTALL_HINT: string; /** * `$CROSSPAD_CLANGD` → the first `clangd*` on `$PATH`. Returns null when there * is none; the tool turns that into a typed ENV error rather than spawning a * command that does not exist and waiting for a reply that never comes. */ export declare function findClangd(env?: NodeJS.ProcessEnv): string | null; export type ProjectId = "pc" | "idf"; export interface CompileDb { project: ProjectId; /** Repo root — clangd's workspace root, and what file paths are relative to. */ root: string; /** Directory holding compile_commands.json (clangd's --compile-commands-dir). */ dir: string; file: string; mtimeMs: number; } /** The `crosspad_build` invocation that would produce the missing database. */ export declare const BUILD_HINT: Record; /** * Candidate build directories per project. crosspad-pc has one `build`; * platform-idf has a `build`-prefixed one per hardware revision and per feature * profile (`build_v1`, `build_v2`, `build_lite`, …), so the newest one wins — * it is the one whose flags match what the developer last compiled. */ export declare function compileDbCandidates(project: ProjectId): CompileDb[]; /** * The compilation database to index. With no `project`, whichever exists — * and if both do, the one built most recently, because that is the tree the * caller is working in. */ export declare function findCompileDb(project?: ProjectId): CompileDb | null; /** Typed "there is no index" error naming the build that would create one. */ export declare function noCompileDbError(project?: ProjectId): ClangdError; /** * A translation unit to open purely to get clangd working. * * Measured, not guessed: `--background-index` does nothing until the client * opens a document — a `workspace/symbol` query on a freshly started clangd * answers instantly with an empty list, indefinitely. Opening one TU starts the * index, and the query then works within seconds. The first entry of * compile_commands.json is as good a seed as any, and reading the head of the * file avoids parsing a database that runs to tens of megabytes. */ export declare function seedFile(db: CompileDb): string | null; export interface ChildLike extends EventEmitter { stdin: NodeJS.WritableStream | null; stdout: NodeJS.ReadableStream | null; stderr: NodeJS.ReadableStream | null; pid?: number; kill(signal?: NodeJS.Signals | number): boolean; } export type SpawnFn = (cmd: string, args: string[], opts: { cwd?: string; }) => ChildLike; export declare const FIRST_REQUEST_TIMEOUT_MS = 120000; export declare const REQUEST_TIMEOUT_MS = 20000; export declare const INDEX_WAIT_MS = 60000; export declare const INDEX_POLL_MS = 1000; export declare const INDEX_GRACE_MS = 3000; export interface ClangdOpts { binary: string; db: CompileDb; spawnFn?: SpawnFn; firstRequestTimeoutMs?: number; requestTimeoutMs?: number; /** Index-wait knobs; overridden by tests so they need no real waiting. */ indexWaitMs?: number; indexPollMs?: number; indexGraceMs?: number; } export interface Position { line: number; character: number; } export declare class ClangdClient { private readonly opts; private proc; private pending; private nextId; private buf; private stderrLines; private stderrBuf; private starting; private opened; private warm; private indexingTitles; private sawIndex; private seeded; private termTimer; private killTimer; private readonly spawnFn; constructor(opts: ClangdOpts); get alive(): boolean; /** True once a request has completed — i.e. the index is loaded. */ get warmed(): boolean; /** True while clangd reports a background-indexing progress token. */ get indexing(): boolean; /** True once clangd has started indexing at least once this session. */ get sawIndexing(): boolean; get root(): string; stderrTail(n?: number): string; /** Spawn clangd and complete the `initialize` handshake. Idempotent; a * second call while starting shares the same promise. */ start(): Promise; private doStart; /** * One LSP request. The first one gets the generous index warm-up budget; * after that a timeout means clangd is stuck, so the process is killed and * the next call gets a fresh one — a hung language server must never become * a hung MCP server. * * `abortSignal` is the client's cancellation. Its budget is minutes on a cold * tree, so "the caller gave up" has to be a thing this can hear: without it * a cancelled `crosspad_symbol` keeps the MCP server waiting out the full * warm-up for an answer nobody will read. */ request(method: string, params: unknown, timeoutMs?: number, abortSignal?: AbortSignal): Promise; notify(method: string, params: unknown): void; /** * clangd answers position requests only for documents the client has opened, * so every file is pushed once per session. Sending its text (rather than * relying on the file on disk) is also what lets the answer reflect an edit * that has not been saved yet. */ openFile(file: string): void; /** Open one TU so `--background-index` has something to start from. Cheap * and idempotent; a no-op once any file has been opened. */ warmup(): void; /** * Name → index hits, waiting out the background index. * * A cold clangd answers `workspace/symbol` immediately with an empty list, * which is indistinguishable from "no such symbol" unless you wait: hence * the seed open above, then a poll while indexing is in flight. Once nothing * is indexing and the grace period has passed, an empty answer is the truth. */ workspaceSymbol(query: string, abortSignal?: AbortSignal): Promise; /** `shutdown` + `exit`, then SIGTERM/SIGKILL if it lingers. Idempotent. */ stop(): Promise; /** Hard kill — used on timeout, where the process is presumed wedged. */ kill(): void; private settle; private lastStderr; /** `Content-Length: N\r\n\r\n` framing, with a carried partial tail. * Lengths are byte counts, not characters, so the buffer stays a Buffer. */ private ingest; private dispatch; private replyOk; private onNotification; private ingestStderr; private onExit; } /** clangd wants a languageId; headers are compiled as C++ everywhere here. */ export declare function languageIdOf(file: string): string; /** Absolute path out of an LSP `file://` uri, or the string unchanged. */ export declare function uriToPath(uri: string): string; /** * Lazily start (and then reuse) the client for a compilation database. One * server per index root: clangd's index is per-workspace, and a second process * over the same tree would rebuild the whole thing for nothing. */ export declare function getClangdClient(db: CompileDb, opts?: Partial, abortSignal?: AbortSignal): Promise; /** Stop every running server, gracefully. Idempotent. */ export declare function stopAllClangd(): Promise; /** SIGKILL every running server. The graceful path is `stopAllClangd()`; this * is the one that works from an `exit` handler, where there is no turn of the * event loop left in which to await anything. */ export declare function killAllClangd(): void; /** * Make the process clean up after itself. Nothing else owns these children: * clangd is spawned detached from any tool call that outlives it, and an MCP * server that just stops reading stdin would otherwise leave a language server * indexing a 1950-file tree with no parent left to ask it to stop. * * `beforeExit` gets the graceful `shutdown`/`exit` handshake; `exit` is the * backstop that can only kill. Signals are deliberately not hooked here — * adding a SIGINT listener suppresses Node's default termination, which is a * bigger behaviour change than this is allowed to make. * * Returns a remover, so a test can install it without leaking listeners. */ export declare function installClangdShutdownHook(target?: NodeJS.Process): () => void; /** @internal test-only */ export declare function _resetClangdForTest(): void; /** @internal test-only — how many servers the module is holding. */ export declare function _clangdClientCount(): number;