/** * `QodoClient` — the SDK 2.0 entry point. * * One typed wrapper layer above QAR's wire vocabulary. Sub-clients (`tasks`, * `tools`, `info`, `pipeline`) mirror QAR's `kind` taxonomy; consumers `for await` * the iterator returned by `tasks.start` and switch on `event.kind`. * * Construction is cheap and synchronous: validate options, hold the bearer, hand * back sub-clients. `connect()` opens the WebSocket lazily; `disconnect()` tears * it down. */ import type { Envelope } from '../qar/envelopes.js'; import type { ClientEvent } from '../qar/clientEvents.js'; import { TaskClient } from './TaskClient.js'; import { ToolClient } from './ToolClient.js'; import { InfoClient } from './InfoClient.js'; import { SpecsClient } from './SpecsClient.js'; import { ManagedToolsClient } from './ManagedToolsClient.js'; import { PipelineClient } from './PipelineClient.js'; import { BulletinClient } from './BulletinClient.js'; import { ArtifactsClient } from './ArtifactsClient.js'; import { StateClient } from './StateClient.js'; import { AgentsClient } from './AgentsClient.js'; import { QarRegistryClient } from './QarRegistryClient.js'; import { SkillsManager } from '../skills/manager.js'; import { QodoSkillsMcpServer } from '../skills/mcp/index.js'; import type { ClientOptions } from './options.js'; import { TransportMetrics } from '../observability/transportMetrics.js'; /** * Read-only observer sub-clients, reached via `client.observers`. Each * observes one server-emitted envelope kind; none of them mutate server * state. */ export interface ClientObservers { /** Read-only observer for QAR's `bulletin.post` envelopes (multi-agent coordination). */ readonly bulletin: BulletinClient; /** Read-only observer for QAR's `artifact.add` envelopes (durable artifacts). */ readonly artifacts: ArtifactsClient; /** Read-only observer for QAR's `state.update` envelopes (per-skill state mutations). */ readonly state: StateClient; /** Read-only observer for QAR's `agent.spawn` envelopes (sub-agent topology). */ readonly agents: AgentsClient; } export declare class QodoClient { /** Task lifecycle surface — `task.start` / `task.continue` / `task.cancel` / `task.resubscribe`. */ readonly tasks: TaskClient; /** Tool tunneling surface — inbound `tool.request`, outbound `tool.response`. */ readonly tools: ToolClient; /** Bootstrap surface — `GET /v1/info`, `GET /v1/auth/me`. */ readonly info: InfoClient; /** Pre-flight surface — `POST /v1/specs/validate` for inline specs. */ readonly specs: SpecsClient; /** * Managed-tools surface — QAR's HTTP tool catalog (`GET /v1/tools`) and * direct invocation (`POST /v1/tools/{name}:invoke`). `invoke()` hides the * long-running-operation protocol: fast results resolve directly, escalated * ones are polled to terminal, and failures throw a typed `QodoToolError`. * * Distinct from {@link tools} (the WebSocket tool-tunneling / handler * surface) — `managedTools` reaches server-side tools the consumer does not * implement. */ readonly managedTools: ManagedToolsClient; /** Pipeline DSL — chained over the task primitive. */ readonly pipeline: PipelineClient; /** * Read-only observer sub-clients, grouped under one namespace so the * top-level surface stays focused on the active task/tool/registry * clients. Each observes one server-emitted envelope kind: * * - `observers.bulletin` — `bulletin.post` (multi-agent coordination) * - `observers.artifacts` — `artifact.add` (durable artifacts) * - `observers.state` — `state.update` (per-skill state mutations) * - `observers.agents` — `agent.spawn` (sub-agent topology) * * They observe, they don't manage — `observers.agents` watches spawns, * it doesn't register agents (that's `client.qar.registerAgent`). */ readonly observers: ClientObservers; /** * SDK-process-scoped local registries for consumer-built agents and MCP * servers. See {@link QarRegistryClient} for the surface — `registerAgent`, * `registerMcp`, plus unregister / lookup helpers. * * The SDK ships with zero built-in MCPs; everything a consumer wants * their agents to call must be registered here (or be a server-side * managed tool via `AgentSpec.use_managed_tools`). */ readonly qar: QarRegistryClient; /** * Skills foundation surface. Present iff `ClientOptions.skills` was * provided at construction. `undefined` when skills are disabled — * consumers branch on this before reaching for `.list()` / `.get()`. * * Discovery is lazy: the manager walks sources on the first * `tasks.startWithAgent` / `tasks.startWithGraph` invocation that * needs the catalog (or when the consumer calls `await * client.skills.discover()` explicitly). Subsequent uses share the * cached snapshot for the SDK lifetime. */ readonly skills?: SkillsManager; /** * `qodo-skills` MCP server. Present iff `ClientOptions.skills` was * provided at construction. Auto-registered via * `client.qar.registerMcp('qodo-skills', { transport: 'sdk', handler })` — * the SDK ships zero built-in MCPs by default, with `qodo-skills` as * the per-config exception. The handler owns the three tools * (`list_skills`, `get_skill`, `get_skill_file`) and the in-memory * body / file caches. * * `undefined` when `skills` is not configured — the server isn't * registered either, so the no-MCPs invariant holds for default-config * consumers. */ readonly skillsMcpServer?: QodoSkillsMcpServer; /** Frozen copy of the construction options for runtime introspection. */ readonly options: ClientOptions; /** * Wire-level transport counters covering the reconnect + replay path. * Three monotonic counters: * * - `replay_envelopes_received_total` — envelopes received as descendants * of a `task.resubscribe` anchor. * - `resubscribes_sent_total` — `task.resubscribe` envelopes the SDK * emitted (auto-replay on reconnect AND manual `tasks.resubscribe`). * - `replay_anchor_missing_total` — inbound `error { code: * 'replay_anchor_missing' }` envelopes from QAR. Defensive wiring: no * released QAR emits that code, so this stays at zero. A stale anchor * replays the whole buffer rather than erroring. * * Counters survive across reconnects and `disconnect()`. Read via * {@link TransportMetrics.snapshot}. */ readonly transportMetrics: TransportMetrics; private readonly authProvider; private readonly transportFactory; /** * OTel API the SDK emits spans against. Resolved once at construction time * via the explicit `options.otel` first, then a best-effort dynamic * `import('@opentelemetry/api')`. `undefined` means no OTel — every span * call is a no-op and `traceparent` falls back to a fresh random. */ private readonly otel; private readonly traceContext; private connection; private connectInFlight; private readonly clientHeaders; constructor(opts: ClientOptions); /** * Open the underlying WebSocket. Idempotent — calling twice without an * intervening `disconnect()` returns the same live connection. * * On a 401/403 from the upgrade, throws `QodoAuthError` with the auth source * named so the caller can fix the right surface (env var, file, or explicit). */ connect(): Promise; /** * Subscription token returned by `attachSkillsMcpBridge`. Kept so that * `disconnect()` can clean up the bridge alongside the tool dispatcher. * Initialized to `null` until skills are wired. */ private skillsMcpBridge; /** * Auto-wire a `tool.request` handler that intercepts qodo-skills tool * calls and dispatches them through the registered MCP handler. Skipped * when `skillsMcpServer` is undefined (consumer didn't opt into skills). * * **Tool-name matching.** The qodo-skills MCP exposes three tools: * `list_skills`, `get_skill`, `get_skill_file`. QAR deployments may * surface them either bare or under the `qodo-skills.` server prefix * (the open MCP convention). The bridge accepts both shapes — anything * else falls through to consumer-registered handlers via the normal * `client.tools.onRequest(...)` stack. * * **Why the bridge lives here.** The qodo-skills tools are intrinsic * to the Skills feature, not a generic MCP we registered for the * consumer's convenience — the SDK owns their dispatch end to end. * Auto-wiring the bridge here keeps the consumer's `onRequest` stack * free of skills routing concerns. * * **Symmetric projection auto-include.** The dispatch end * (this bridge) is only half the contract. The other half is the * wire-side surface: `TaskClient.projectMcpToolsForWire` / * `mergeQodoSkillsTools` auto-includes the three * `qodo-skills.list_skills` / `qodo-skills.get_skill` / * `qodo-skills.get_skill_file` entries in every dispatched agent's * `function_tools` list when `ClientOptions.skills` is configured, * regardless of the consumer's `mcpTools.allowlist`. The bridge * matches what the projection advertises; the projection ships what * the slim-index preamble references. Both ends move together — if * you change the tool names here, mirror the change in * `qodoSkillsFunctionToolDefs()` (skills/mcp/server.ts) and the * `BLOCK_PREAMBLE` text (skills/render.ts). * * **Precedence vs. remote MCPs.** The skills bridge is attached * BEFORE the generic remote-MCP bridge in `QodoClient.connect()`, so * its bare-name match on `list_skills` / `get_skill` / * `get_skill_file` wins over a registered remote MCP that happens to * advertise the same name. This is intentional — the qodo-skills * tools are intrinsic to the Skills feature surface and consumers * shouldn't be able to shadow them by registering a same-named remote * MCP. A consumer who legitimately needs to reach a remote MCP's * same-named tool uses the prefix form: `mcp-name.list_skills` (or * `mcp-name:list_skills` / `mcp-name/list_skills`) — the remote-MCP * bridge resolves the prefix form against the registered MCP set, not * against the skills bridge's hardcoded names. */ private attachSkillsMcpBridge; /** * Subscription token returned by `attachRemoteMcpBridge`. Mirrors * {@link skillsMcpBridge} — kept so `disconnect()` can detach the bridge * alongside the skills variant. */ private remoteMcpBridge; /** * Auto-wire a `tool.request` handler that resolves each call's * `name` against `qar.resolveMcpForTool` and dispatches the resolved set * through {@link QarRegistryClient.callMcpTool} (which round-trips via the * canonical `@modelcontextprotocol/sdk` `Client.callTool`). * * **Claim semantics.** Three batch shapes: * * - **All-resolved** — every call's name routes to a registered * remote / stdio MCP. Bridge claims; each call dispatches via * {@link QarRegistryClient.callMcpTool}; per-call success / * error responses are returned positionally. * * - **All-unresolved** — no call routes to any registered MCP. * Bridge returns `undefined` so consumer-registered * `client.tools.onRequest(...)` handlers can intercept the * batch (consumers may legitimately wire unrelated custom tools * alongside remote MCPs). Consumers who register remote MCPs * and want unknown-tool calls handled MUST register their own * `onRequest` handler; otherwise the call waits for QAR's * per-task tool-response timeout (the documented wire-side safety * net for the "all handlers returned undefined" case). * * - **Mixed batch** — some resolved, some not. Returns `undefined` * so consumer-registered `client.tools.onRequest(...)` handlers * can intercept. QAR's single-server-per-batch convention means * this path is unreachable in normal operation; the defer posture * is the safety hatch for a future QAR routing change that might * produce mixed batches. * * **Bare-name catalog race.** First-dispatch bare-name resolution * scans the cached `entry.tools` populated by `listTools()` at * `commitEntry` time. A `tool.request` arriving immediately after * `registerMcp` (before connect+listTools resolved) would otherwise * see an empty catalog and fall into the all-unresolved defer path. * The bridge awaits {@link QarRegistryClient._waitForMcpCatalogsToSettle} * before deciding "unresolved" so first-call timing doesn't * mis-classify a legitimate bare-name lookup as unknown. */ private attachRemoteMcpBridge; /** * Detach the remote-MCP bridge. Idempotent — safe to call when the * bridge is already detached. Fired both from {@link disconnect} and * from the registry hook when the last remote / stdio MCP is * unregistered (preserves `ToolClient`'s deny fallback for subsequent * `tool.request` envelopes). */ private detachRemoteMcpBridge; /** * Close the underlying WebSocket and abort any in-flight task iterators. * Idempotent. */ disconnect(): Promise; /** * Send a raw envelope verbatim. Escape hatch for advanced consumers — most * callers use `tasks.start` / `tasks.continue` / `tools.respond` instead. * * The envelope must already be fully populated: * - `message_id`, `ts`, `envelope_version`, `trace_context` on every kind * - `session_id` on every kind EXCEPT `task.start` * * The `task.start` envelope is a `_StartEnvelopeBase` (no `session_id` * on the wire); the type narrowing on {@link TaskStartEnvelope} omits * the field so raw senders cannot compile an invalid `task.start`. * All other kinds inherit `_OngoingEnvelopeBase` and require * `session_id`. * * The connection writes the envelope through the underlying transport * unchanged. */ send(envelope: Envelope): void; /** * Subscribe to every inbound envelope plus the SDK-internal * `qar.client.*` lifecycle events. The same envelopes that drive * `tasks.start()` iterators flow here too; useful for telemetry / logging / * advanced consumers. * * Yielded type is the union `Envelope | ClientEvent`. `Envelope` arms carry * the wire `kind` discriminators; `ClientEvent` arms carry `qar.client.*` * keys that can never collide with QAR envelope kinds. `switch (ev.kind)` * narrows cleanly between the two. */ receive(): AsyncIterable; /** * Whether the underlying connection is currently in `flow.pause`. * `false` when not connected — disconnect always resets the flag. * * The SDK obeys backpressure transparently; consumers don't normally need * to check this. It's exposed for richer surfaces (CLIs, UIs) that want to * show a "server overloaded; waiting" indicator. Reading this is cheap and * synchronous. */ get isPaused(): boolean; /** * Synchronous validation of `ClientOptions`. Runs at construction time so * misconfigured callers fail fast instead of at the first `tasks.start`. * * Treats the construction call as a system boundary — JavaScript callers can * pass `null` even though TypeScript bars it, so we check both `undefined` * and `null` and refuse with a typed error rather than letting a subsequent * property access throw an unhelpful `TypeError`. */ private static validateOptions; private static buildAuthProvider; /** * Map the consumer-supplied `baseUrl` (typically `https://qar.qodo.ai`) onto * the WS endpoint. `https://` → `wss://`; existing `wss://` is left as-is. * Trailing slashes on `baseUrl` are stripped before joining. * * Insecure schemes (`http://` / `ws://`) are refused unless * `allowInsecureTransport` is `true`. Rationale: `connect()` always sends the * bearer token in the upgrade `Authorization` header — over `ws://` that's * plaintext on the wire. The opt-in keeps local-dev workflows possible * without making credential leakage a default footgun. */ private static deriveWsUrl; } //# sourceMappingURL=QodoClient.d.ts.map