# agent-connector — Full LLM Reference > agent-connector serves TWO audiences. **(A) MCP developers** write their MCP server + > hooks (and slash commands / Agent Skills / subagents / memory / status line) ONCE with `defineConnector({...})`; > the CLI detects every installed AI-agent host, renders the right config in each one's > native dialect, installs/syncs/uninstalls them, and gives you default, platform- > independent, local-first per-tool token telemetry for YOUR OWN wrapped server. > **(B) Agent-CLI end users** who have NOT authored a connector run one connector-free > command — `agent-connector usage` — to read each agent CLI's own session logs read-only > and see per-CLI / per-model / per-session token totals. The one accuracy-critical line: > *if you BUILD an MCP integration, agent-connector deploys it everywhere and measures your > own server's per-tool tokens; if you just USE agent CLIs, agent-connector reads their logs > to show you per-CLI / per-model token totals* (whole-conversation, never per-MCP/per-tool). > See §0 Audiences. The host-usage layer plus three non-summed leaderboards are detailed below. This is the exhaustive, code-grounded reference. It is the territory; `llms.txt` is the map. Everything below reflects what the code actually supports (`src/core/types.ts`, `src/core/define-connector.ts`, `src/cli/`, `src/adapters/`, `src/telemetry/`, `src/usage/`). --- ## 0. Audiences — read this fork first agent-connector has **two distinct audiences**, and almost every doc surface forks between them. Decide which one you are before reading further. ### Audience A — the MCP developer (builds an integration) You are **writing** an MCP integration and want it deployed and measured everywhere. You author one `defineConnector({...})` config (server + hooks, optionally commands / skills / subagents / memory), then deploy it across the detected agent hosts with one CLI — either by shipping a branded CLI (`createConnectorCli`) so your users never type `--connector`, or by running `npx @ken-jo/agent-connector`. For **stdio** servers, agent-connector transparently wraps your server in the `serve` telemetry proxy, so you get **per-MCP and per-tool token counts for YOUR OWN server** (the MCP your connector declares and wraps), measured locally as aggregate counts only. Your track owns: `defineConnector` (§2), install / uninstall / upgrade / doctor / detect / status / package (§3), and the developer/surface telemetry axis — `telemetry report|export|leaderboard --by mcp|tool|surface` and the 🔌 board of the unified leaderboard (§5). Per-MCP / per-tool numbers **exist only for a server a connector wraps** — that is why they live entirely in this track. Start here: ```bash npm install @ken-jo/agent-connector # inside the package that holds your connector # author agent-connector.config.{mjs,js,json} with `export default defineConnector({...})` acme-db install --dry-run # preview create/update/skip/warn per host acme-db install # deploy to DETECTED hosts (or --targets) acme-db doctor --probe # live initialize → ping → tools/list acme-db telemetry report --by tool # YOUR server's per-tool footprint ``` ### Audience B — the agent-CLI end user (has NOT authored a connector) You already run Claude Code / Codex / Cursor / … and you just want to know **how many tokens your agent CLIs are burning**. You do **not** write a connector, run no install, and need no config file. One connector-free command reads each agent CLI's OWN native session logs (read-only, never writes host config): ```bash npx @ken-jo/agent-connector usage report # totals by platform (default) npx @ken-jo/agent-connector usage leaderboard --by platform # which CLI cost the most npx @ken-jo/agent-connector usage report --by model --since 7d npx @ken-jo/agent-connector usage export --format csv --out usage.csv ``` **The accuracy-critical boundary.** The connector-free `usage` path reports **whole-conversation totals** grouped by `platform | project | session | model | day` only. It does **NOT** and **cannot** itemize cost per individual MCP server or per tool: agent CLIs fold tool-result bytes into the session's input tokens and never log per-tool token attribution (`UsageRecord` carries no tool/mcp field; `UsageGroupBy` has no tool/mcp value — §5). So the literal end-user ask "how much does each MCP I use cost me?" is **not answerable** on this path. Per-MCP and per-tool token costs come **only** from the serve-proxy telemetry that an MCP developer's own connector produces — i.e. Audience A, measuring a server their connector wraps. The docs never blur "see what your tools cost" (developer's own wrapped server) with "see what the MCPs you use cost" (end user — available only as host totals, not per-MCP). > **One sentence to remember:** *If you BUILD an MCP integration, agent-connector > deploys it everywhere and measures your own server's per-tool tokens. If you just USE > agent CLIs, agent-connector reads their logs to show you per-CLI / per-model token > totals.* --- ## 0.1 Agent authoring contract When an AI agent is asked to create, modify, review, or document an agent-connector integration: 1. Read the target package's `package.json` first. 2. Treat `package.json` `name`, `mcpName`, `bin`, and `version` as the source of truth for public package identity, MCP identity, host alias/display label, branded command, and connector version. 3. Do **not** ask for a separate connector id, display name, bin name, or version unless package metadata is absent or the user explicitly needs a legacy/multi-instance override. 4. Use `@ken-jo/agent-connector/sdk` for new authoring code (`defineConnector`, typed `define*` helpers, `hostsSupporting`, `capabilitiesOf`, `surfaceSupport`). Use `@ken-jo/agent-connector/cli` only inside the developer package's bin via `createConnectorCli({ packageJson, connector })`: `packageJson` supplies public identity; `connector` supplies behavior, so these are two layers rather than duplicate id/display-name inputs. 5. Foreground the developer's branded MCP package/bin in generated docs and commands: `npx @acme/acme-db-mcp install`, `acme-db doctor --probe`, etc. Use `npx @ken-jo/agent-connector ... --connector` only as a local framework development/debug fallback. 6. Verify before claiming success: run typecheck/tests, use `@ken-jo/agent-connector/sdk/test` (`explain`, `simulate`, `explainHooks`) for offline host behavior when relevant, then run `doctor --probe` for a live stdio MCP handshake when a real host/server is available. The skill-form entry point is intentionally small and reference-driven: `skills/agent-connector/SKILL.md` routes agents to `references/package-first.md`, `references/authoring.md`, `references/cli-workflow.md`, `references/telemetry.md`, and `references/agent-readiness.md`. --- ## 1. Mental model Two pillars (see `docs/ARCHITECTURE.md`): 1. **Single-API multi-platform deployment.** One declarative + programmatic `defineConnector({...})` → per-platform adapters render it into each host's native MCP registration, hook config, and content files; one CLI installs/syncs/ uninstalls everywhere. 2. **Default per-MCP token telemetry.** Platform-independent, local-first, privacy-preserving (aggregate counts, never content). On by default. Operating model (home-dir-centric, single binary, per-project data): - **One home binary.** The runtime installs once under `~/.agent-connector` (override env `AGENT_CONNECTOR_DATA_DIR`): ``` ~/.agent-connector/ bin/agent-connector single binary: CLI + hook entrypoint + telemetry runtime connectors//connector.json each registered connector's resolved definition telemetry.ndjson (or .db) shared telemetry store, rows keyed by project backups/ timestamped settings backups before each mutation logs/ ``` Every host config we write is a **thin pointer** back to this one stable binary (a hook command is `agent-connector hook --connector `; a wrapped MCP entry runs `agent-connector serve --connector -- `). Updating that single binary updates behavior in every host. Updates are explicit (`agent-connector upgrade`), never silent auto-update — so one bad release can't break every project at once. - **Native config stays native.** `AGENT_CONNECTOR_DATA_DIR` relocates only framework-owned state; a host's own settings files (`.cursor/mcp.json`, `~/.codex/config.toml`, …) are never relocated. - **Per-project data.** Telemetry/state is keyed by a stable project identity (`gitRemote || normalizedAbsPath`, hashed), stored under the home data-root — survives `git clean`, isn't committed, shared by every host opening that project. - **Windows-first.** Resolves home per-OS; no symlinks, no POSIX-only assumptions. --- ## 2. The public API — `defineConnector(config: ConnectorConfig): ResolvedConnector` ```ts import { defineConnector } from "@ken-jo/agent-connector/sdk"; export default defineConnector({ /* ConnectorConfig */ }); ``` Place the config in `agent-connector.config.{mjs,js,json}` at the project root (found by walking up from the project dir), or pass `--connector `. The function validates eagerly and **throws `ConnectorConfigError`** on any violation, returning a fully-defaulted `ResolvedConnector` that adapters and the CLI consume. ### 2.1 `ConnectorConfig` (the write-once surface) | Field | Type | Default | Notes | |---|---|---|---| | `id` | `string` | package identity metadata | Optional explicit install/runtime alias. Must match `^[a-z0-9][a-z0-9-]*$` (kebab-case) when supplied; omit for normal packaged connectors. | | `mcp` | `McpPackageIdentity` | package.json metadata | Optional package identity override when package.json is absent or intentionally different. | | `displayName` | `string` | derived id | Optional host-facing label override; omit unless the host should show a different label. | | `version` | `string` | package.json version, else `"0.0.0"` | Optional connector version override. Prefer package.json version for packaged connectors. | | `server` | `ServerDef` | — | The MCP server to deploy. Omit for a hooks-only / content-only connector. | | `hooks` | `HooksConfig` | `{}` | Lifecycle hooks. Omit for a server-only connector. | | `telemetry` | `TelemetryConfig` | (defaults below) | Telemetry is ON even if omitted. | | `commands` | `CommandDef[]` | `[]` | Slash commands → native content files. | | `skills` | `SkillDef[]` | `[]` | Agent Skills → native content files. | | `subagents` | `SubagentDef[]` | `[]` | Named subagents → native content files. | | `memory` | `MemoryDef[]` | `[]` | Standing guidance → marker-fenced MANAGED BLOCKS in each host's memory/rules file (AGENTS.md-first; see §2.4). | | `statusline` | `StatuslineDef` | — | HUD / status-line handler (SINGULAR, not an array). A `render(ctx)` fallback plus optional host-specific render/options overrides; deploys on antigravity-cli, claude-code and qwen-code today; other hosts skip-warn. See §2.5. | | `actions` | `ActionDef[]` | `[]` | Named actions the connector exposes for user-triggered dispatch, with optional label/icon/placement/confirm metadata and per-host overrides. See §2.6. | | `platforms` | `Partial>` | `{}` | Per-platform overrides / escape hatch (`extra`, `nativeHooks`, `configPatch`, `memory` target/mode tuning, disable a surface, force scope). | | `targets` | `"auto" \| PlatformId[]` | `"auto"` | `"auto"` = all detected; or an explicit allow-list. | | `publish` | `PublishConfig` | — | Distribution metadata for the official MCP standard artifacts (`package --format mcp-server-json` / `mcpb`): `registryNamespace` (reverse-DNS namespace the dev owns), `packageName` (the REAL published package), `registryBaseUrl?` (default https://registry.npmjs.org), `author { name, email?, url? }` (MCPB requires author.name). Describes the dev's real upstream server, NOT the telemetry serve wrapper. Optional; each format errors only when its required field is missing. | **Top-level validation rules** (`define-connector.ts`): - `config` must be an object; if supplied, `id` must match the kebab-case regex. - A connector must declare **at least one** of `server`, `hooks`, `commands`, `skills`, `subagents`, `memory`, `statusline`, `actions` (or a per-platform `nativeHooks` / `configPatch` declaration) — else it throws. - If `server` is present: stdio transport requires a string `command`; any remote transport (`http`/`sse`/`ws`) requires a string `url`. - Every present hook entry's `handler` must be a function. ### 2.2 `ServerDef` (transport-polymorphic, declared once) ```ts interface ServerDef { transport: "stdio" | "http" | "sse" | "ws"; // stdio transport: command?: string; // required for stdio args?: string[]; env?: Record; // values support ${env:VAR} / ${env:VAR:-default} cwd?: string; // remote (http | sse | ws) transport: url?: string; // required for remote headers?: Record; auth?: AuthSpec; // { type: "oauth"|"bearerEnv"|"none", bearerEnvVar? } // common: tools?: ToolFilter; // { include?: string[]; exclude?: string[] } timeoutMs?: number; enabled?: boolean; // default true; written disabled where supported wrapForTelemetry?: boolean; // see default below } ``` | Field | Default (after normalize) | Notes | |---|---|---| | `enabled` | `true` | When `false`, the entry is written disabled where the host supports it. | | `tools` | `{ include: ["*"] }` | Glob/exact include/exclude tool names. | | `wrapForTelemetry` | `true` for stdio, `false` otherwise | Wrap with `agent-connector serve` so per-tool telemetry is captured transparently. Remote transports can't be intercepted, so it defaults off there. | `AuthSpec`: `{ type: "oauth" | "bearerEnv" | "none"; bearerEnvVar?: string }` — `bearerEnvVar` names the env var holding the token when `type === "bearerEnv"`. Each adapter renders `ServerDef` into the host's dialect. The **root key and field names differ per host** (constant per adapter): `mcpServers` (Claude Code, Cursor, Copilot CLI, Amp, Codebuff, Crush, Antigravity, …), `servers` (VS Code Copilot), `mcp_servers` (Codex TOML), `mcp` (Warp). Field renames like `cwd`↔`working_directory` and `env`↔`environment` are handled per adapter. An adapter that cannot honor a requested transport downgrades-or-skips and **reports** it — it never throws. `${env:VAR}` / `${env:VAR:-default}` interpolation is universal; where a host supports native interpolation the reference is translated rather than baked in. ### 2.3 `HooksConfig` + the hook model ```ts interface HooksConfig { SessionStart?: HookDefinition<"SessionStart">; SessionEnd?: HookDefinition<"SessionEnd">; UserPromptSubmit?: HookDefinition<"UserPromptSubmit">; PreToolUse?: HookDefinition<"PreToolUse">; PostToolUse?: HookDefinition<"PostToolUse">; PreCompact?: HookDefinition<"PreCompact">; Stop?: HookDefinition<"Stop">; Notification?: HookDefinition<"Notification">; PermissionRequest?: HookDefinition<"PermissionRequest">; PostToolUseFailure?: HookDefinition<"PostToolUseFailure">; SubagentStart?: HookDefinition<"SubagentStart">; SubagentStop?: HookDefinition<"SubagentStop">; PostCompact?: HookDefinition<"PostCompact">; } interface HookDefinition { matcher?: string; // regex on tool name (tool events, incl. PermissionRequest / // PostToolUseFailure) or on agent type (SubagentStart / // SubagentStop); empty/omitted = match all handler(event: EventPayloadMap[E]): HookResponse | void | Promise; } ``` `matcher` is rendered into each host's native matcher syntax where supported, else evaluated by the universal entrypoint at runtime. **Normalized event payloads** (every event extends a base `{ hostPlatform, connectorId, sessionId, projectDir?, raw }`; `sessionId` is `""` when the host provides none; `raw` is the verbatim host payload for escape-hatch use): | Event | Extra payload fields | |---|---| | `SessionStart` | `source: "startup" \| "compact" \| "resume" \| "clear"` | | `SessionEnd` | `reason?: string` | | `UserPromptSubmit` | `prompt: string` | | `PreToolUse` | `toolName: string`, `toolInput: Record` | | `PostToolUse` | `toolName`, `toolInput`, `toolOutput?: string`, `isError?: boolean` | | `PreCompact` | `trigger?: "auto" \| "manual"` | | `Stop` | `stopHookActive?: boolean` | | `Notification` | `message: string` | | `PermissionRequest` | `toolName`, `toolInput`, `permissionSuggestions?: unknown[]` (host dialog suggestions, passthrough) | | `PostToolUseFailure` | `toolName`, `toolInput`, `toolUseId?`, `error: string`, `isInterrupt?: boolean`, `durationMs?: number` | | `SubagentStart` | `agentId?: string`, `agentType?: string` | | `SubagentStop` | `agentId?`, `agentType?` (both optional — hosts don't reliably populate them on stop), `agentTranscriptPath?`, `lastAssistantMessage?`, `stopHookActive?` | | `PostCompact` | `trigger?: "auto" \| "manual"` (observational post-compaction sibling of `PreCompact`; cannot block/modify — codex is the verified firing host) | **Normalized `HookResponse`** (return a subset; the adapter formats it into the host's native reply — exit codes / JSON / control fields — and **drops fields the host can't honor**, reporting the degradation): ```ts interface HookResponse { decision?: "allow" | "deny" | "modify" | "context" | "ask"; reason?: string; // shown to model/user; expected for deny/ask updatedInput?: Record; // only with "modify" (PreToolUse / PermissionRequest) additionalContext?: string; // with "context" or on SessionStart updatedOutput?: string; // PostToolUse only, where the host supports it } ``` Decision semantics: - `allow` — pass through (default when the handler returns void). On `PermissionRequest` ONLY, an EXPLICIT `allow` is an ACTIVE grant that suppresses the host's permission dialog (a void/decision-less return falls through to the native dialog; an allow never overrides host deny rules). - `deny` — block the tool call / stop the action. On `SubagentStop` this keeps the subagent running with `reason` as its next instruction (Stop semantics); on the feedback-only events (`PostToolUseFailure`, `SubagentStart`) it degrades to context carrying the reason — nothing is blockable there. - `modify` — replace tool input with `updatedInput` (PreToolUse / PermissionRequest). - `context` — inject `additionalContext` as soft guidance (on `SubagentStart` it lands in the SUBAGENT's conversation, before its first prompt). - `ask` — prompt the user to confirm. On `PermissionRequest` this falls through to the native dialog (the dialog IS the ask). **Per-paradigm synthesis** (the framework picks the right one from the host's detected paradigm): - `json-stdio` — host pipes JSON to a command on stdin and reads JSON/exit-code back. One universal entrypoint (`agent-connector hook --connector `) reads the payload, normalizes it, runs your handler, and formats the reply. - `ts-plugin` — host loads a generated JS/TS module exporting lifecycle functions that import your handler. - `mcp-only` — no hook layer; only the MCP server is installed and hooks are reported unavailable for that host. Fail-open is the runtime contract: the hook entrypoint never rejects, so a framework or handler bug can't wedge a host's tool call. **Native hooks passthrough (`platforms..nativeHooks`)** — the platform-scoped escape hatch for host hook events OUTSIDE the normalized 13-event union. Hosts ship far more events than the union normalizes: Claude Code alone has **30** hook events, 17 of them with no normalized analog (Setup, UserPromptExpansion, PermissionDenied, PostToolBatch, MessageDisplay, TaskCreated, TaskCompleted, StopFailure, TeammateIdle, InstructionsLoaded, ConfigChange, CwdChanged, FileChanged, WorktreeCreate, WorktreeRemove, Elicitation, ElicitationResult). `nativeHooks` wires any of them — and any future event a host adds — with zero agent-connector releases: ```ts // PlatformOverride (§2.8) gains: nativeHooks?: Record; // key = HOST event name, VERBATIM interface NativeHookDef { matcher?: string; // host-native matcher, written verbatim — the HOST filters handler(evt: NativeHookEvent): unknown | Promise; } interface NativeHookEvent { event: string; // host-native event name, verbatim (e.g. "TaskCompleted") hostPlatform: PlatformId; sessionId: string; // "" when the host provides none projectDir?: string; // Claude Code: `cwd` raw: unknown; // the host's RAW stdin payload, UNTOUCHED } ``` Contract — **raw in, verbatim out** (full native fidelity, zero translation): - The handler receives the host's RAW stdin payload (`raw`) — no normalization, no field mapping; you read the host's own contract (snake_case and all). - Whatever the handler RETURNS is serialized VERBATIM as the stdout JSON reply with exit 0 — no `HookResponse` mapping; the return value must already be the host's native reply shape. Claude examples: `{continue: false, stopReason}` on TaskCreated / TaskCompleted / TeammateIdle stops the teammate; `{hookSpecificOutput: {hookEventName: "MessageDisplay", displayContent}}` rewrites the rendered text; `{hookSpecificOutput: {hookEventName: "Elicitation", action: "accept" | "decline" | "cancel", …}}` answers an MCP user-input request. - `void`/`undefined` → exit 0 with NO output (right for output-ignored events like StopFailure / InstructionsLoaded — logging/alerting only). - Fail-open: any throw degrades to exit 0 with no output. **LIMITATION (v1): exit-2 blocking is NOT modeled** — a native handler always exits 0. JSON-on-exit-0 decision control covers Claude Code's events, but a contract that REQUIRES a non-zero exit or bare (non-JSON) stdout — e.g. WorktreeCreate fails worktree creation on any non-zero exit and reads a bare path from stdout — may not be fully drivable. Validation (`define-connector.ts`): each handler must be a function; `matcher`, when present, must be a string; a key that collides with one of the 13 normalized `HookEventName` values throws `ConnectorConfigError` — use the normalized `hooks` API for those (it gets normalization, matcher evaluation, and HookResponse mapping). A connector whose ONLY payload is a `nativeHooks` declaration is valid. Support is opt-in per adapter via `PlatformCapabilities.supportsNativeHooks` (optional, read `?? false`) — **only claude-code sets it today** (settings.json hook keys are free-form event names, so entries install verbatim: event-name key, the def's matcher, the same home-bin command shape; uninstall removes them; doctor counts them only when declared). `nativeHooks` is keyed per platform, so a declaration only ever applies to the platform it sits under; an adapter without the capability reports the standard skip-warn ChangeRecord (never silent). At runtime the hook CLI accepts the non-union event name when the resolved connector declares it (`agent-connector hook claude-code TaskCompleted --connector `), bypassing normalized parse/format entirely: stdin JSON → `NativeHookEvent{raw}` → handler → verbatim JSON stdout. Telemetry records the dispatch as a normal `scope: hook` developer-axis row under the host-native event name. **Promotion criteria:** an event graduates from `nativeHooks` into the normalized union when ≥3 hosts ship a native analog (tracked in the living cross-host matrix); `TaskCreated` / `TaskCompleted` are the named first candidates. ### 2.4 Content surfaces — `CommandDef`, `SkillDef`, `SubagentDef`, `MemoryDef` These are **content-only** (markdown / TOML files): no runtime dispatch, no telemetry wrapping, no home-bin pointer — pure file writers. Each supporting adapter writes the native file(s); unsupporting adapters skip+warn (same as `mcp-only` hook handling). `memory` is the fourth content surface with the same contract, except it edits a **shared, user-authored** memory/rules file (AGENTS.md / CLAUDE.md / GEMINI.md) via marker-fenced managed blocks instead of writing files agent-connector wholly owns — full mechanics under "The `memory` surface" below. `SurfaceToolPolicy` (shared): `{ allow?: string[]; deny?: string[] }` — rendered to each host's allowed-tools / tools[] / readonly. ```ts interface CommandDef { // a slash command name: string; // kebab-case; slash name + filename stem (source of truth) description?: string; // one-line, for /help + model auto-selection prompt: string; // markdown prompt template body (required, non-empty) argumentHint?: string; // e.g. "[environment]" tools?: SurfaceToolPolicy; model?: string; // raw id or alias; adapters pass through or drop+warn subtask?: boolean; // force subagent/forked context where supported extra?: Record; // verbatim per-platform frontmatter additions } interface SkillDef { // an Agent Skill (folder + SKILL.md) name: string; // <=64 chars, [a-z0-9-]; MUST equal the skill dir name description: string; // <=1024 chars, 3rd-person "what + when" (required) body: string; // SKILL.md markdown body (required, non-empty) tools?: SurfaceToolPolicy; model?: string; disableModelInvocation?: boolean; // → disable-model-invocation resources?: Record; // relpath → contents, bundled beside SKILL.md extra?: Record; } interface SubagentDef { // a named subagent name: string; // kebab-case; filename stem on most platforms description: string; // delegation hint (required, non-empty) prompt: string; // system prompt / instructions (required, non-empty) tools?: SurfaceToolPolicy; model?: string; // alias | full-id | "inherit" readonly?: boolean; // coarse permission knob (Cursor readonly, opencode/kilo perms) extra?: Record; } interface MemoryDef { // standing guidance → a managed block in the host's memory file name?: string; // kebab-case; default "memory". Suffixes the connector id in the // block marker (/) — keep it STABLE across versions description?: string; // status/docs output only; never written to the host file content: string; // plain CommonMark, host-agnostic (no @imports, no frontmatter); // inlined VERBATIM into every targeted host's prompt context } ``` **Surface validation rules** (`define-connector.ts`): - Each `name` must be kebab-case `^[a-z0-9][a-z0-9-]*$`. - No duplicate `name` within a single surface array. - Required non-empty strings: command `prompt`; skill `description` + `body`; subagent `description` + `prompt`. - Skill `description` length must be `<= 1024` (throws otherwise). - Skill `resources` keys must be SAFE relative paths inside the skill dir — empty, `.`, absolute, or any `..`-traversal key is rejected (prevents arbitrary file write/delete via `join(skillDir, rel)`). - Memory `content` must be non-empty; **hard `ConnectorConfigError`** above 16 KiB (memory is injected into every prompt of every targeted host — keep it terse) or when it contains the literal marker tokens `agent-connector:begin` / `agent-connector:end` (they would corrupt marker scanning). A **soft 4 KiB budget** is reported at install time as a `warn` ChangeRecord, not a config error. **Per-platform surface support** (from `docs/research/surfaces-design.md`; adapters that don't support a surface skip with a warning): | Platform | command | skill | subagent | |---|---|---|---| | claude-code | `.claude/commands/.md` (md+fm) | `.claude/skills//SKILL.md` | `.claude/agents/.md` (md+fm) | | gemini-cli | `.gemini/commands/.toml` | `.gemini/skills//SKILL.md` | `.gemini/agents/.md` (md+fm) | | qwen-code | `.qwen/commands/.toml` | — | `.qwen/agents/.md` (md+fm) | | vscode-copilot (+ jetbrains alias) | `.github/prompts/.prompt.md` | `.github/skills//SKILL.md` | `.github/agents/.agent.md` (vscode only — jetbrains skips+warns) | | copilot-cli | — | `.github/skills//SKILL.md` | `~/.copilot/agents/.agent.md` | | cursor | `.cursor/commands/.md` (body-only, no fm) | `.cursor/skills//SKILL.md` | `.cursor/agents/.md` (md+fm) | | codex | `~/.codex/prompts/.md` (user-only) | `.codex/skills//SKILL.md` | `.codex/agents/.toml` (TOML) | | opencode | `.opencode/commands/.md` (md+fm) | `.opencode/skills//SKILL.md` | `.opencode/agent/.md` (singular dir) | | kilo | `.kilocode/commands/.md` (md+fm) | — | `.kilocode/agents/.md` (md+fm) | | pi | — | `.pi/skills//SKILL.md` | — | | antigravity (+ antigravity-cli) | `.agent/workflows/.md` (project; user → `~/.gemini/antigravity/global_workflows/.md`) | `.agents/skills//SKILL.md` | — | | all others | — | — | — | `md+fm` = YAML frontmatter + markdown body. Skills are uniformly folder-per-skill `SKILL.md` (`name`+`description` frontmatter) plus optional `resources` files; only the parent dir differs per platform. **The `memory` surface — managed blocks in the file each host actually reads.** Unlike commands/skills/subagents (files agent-connector wholly owns), memory edits a SHARED, user-authored memory/rules file. Every write goes through one dependency-free engine (`core/managed-block.ts`) implementing idempotent, hash-stamped, uninstall-reversible **managed blocks**: ``` …content… ``` Marker + edit-detection semantics: - The blockId (`/`) lives ON the marker line, so multiple connectors coexist in one file and each update path only ever touches its own pair; the shared `agent-connector` namespace token lets doctor/uninstall enumerate every block in a file with one scan. `_shared/` is a reserved blockId prefix for ref-counted bridge blocks (claude-code, below). - `hash=` is the first 12 hex chars of sha256 over the NORMALIZED inner region (CRLF→LF + trim — stable under prettier/EOL converters). It gives O(1) idempotence (unchanged → `skip` ChangeRecord, no mtime/git churn) and **edit detection**: actual inner hash ≠ recorded hash ⇒ the user edited inside the block ⇒ `warn` and leave the edit intact — overwrite only under `install --force`, after a timestamped backup. The END line carries no hash, so a content change can never strand an unmatched end marker. - Replacement is **in place**: zero bytes outside the marker pair ever change — no move-to-top, no blank-line reflow of user content. A new block appends at EOF separated by exactly one blank line; a missing file is created (and recorded as agent-connector-created). The scanner is line-anchored, CRLF-preserving (blocks are emitted with the file's detected EOL), BOM-safe, and fence-aware (marker text quoted inside backtick/tilde code fences never matches); stray lone markers are recovered (backup + strip the marker line only), and duplicate pairs from past bugs collapse into one on every upsert. **AGENTS.md-first policy (where the block goes).** Most supporting hosts read the open AGENTS.md standard (https://agents.md — plain-markdown "README for agents", stewarded by the Agentic AI Foundation under the Linux Foundation), so the default target is the standard file. Project scope → `/AGENTS.md` — one canonical copy shared by every adopter host (the hash-stamped upsert makes convergent writes dedupe: the first adapter creates/updates, every subsequent one reports `skip`). EXCLUSIVE / first-match readers are PROBED so the block lands in the file the host will ACTUALLY read (a block in a file the host never loads fails silently, and creating AGENTS.md beside a fallback file can shadow the user's own rules): zed targets the first existing of [.rules, .cursorrules, .windsurfrules, .clinerules, .github/copilot-instructions.md, AGENT.md, AGENTS.md, CLAUDE.md, GEMINI.md] (creating AGENTS.md only when none exists); warp targets WARP.md when present (it takes priority in the same dir); hermes targets .hermes.md / HERMES.md when present (first context category wins); opencode targets an existing CLAUDE.md when no AGENTS.md exists (the fallback it actually reads); codex targets AGENTS.override.md when present (it shadows AGENTS.md; ~28 KiB per-file budget warn under its 32 KiB project-doc cap); openclaw maps BOTH scopes to its agent workspace `~/.openclaw/workspace/AGENTS.md` (personal-assistant semantics, not a repo file). User scope → the host's documented user/global memory file: AGENTS.md where one exists — codex `$CODEX_HOME/AGENTS.md` (default `~/.codex/AGENTS.md`), zed `~/.config/zed/AGENTS.md` (`%APPDATA%\Zed\AGENTS.md` on Windows), amp `~/.config/amp/AGENTS.md`, mux `~/.mux/AGENTS.md`, pi `~/.pi/agent/AGENTS.md`, droid `~/.factory/AGENTS.md`, opencode `~/.config/opencode/AGENTS.md`, antigravity + antigravity-cli `~/.gemini/AGENTS.md` (a shared tree — the idempotent upsert dedupes), omp `~/.omp/agent/AGENTS.md` — and the host's own documented file otherwise: qwen-code `~/.qwen/QWEN.md`, goose `.goosehints` beside config.yaml, copilot-cli `~/.copilot/copilot-instructions.md`, kilo/kilo-cli `~/.kilocode/rules/agent-connector.md`, roo-code `~/.roo/rules/agent-connector.md`, kiro `~/.kiro/steering/agent-connector.md` (dedicated agent-connector-owned rules-dir files — deletable on uninstall). Hosts whose user-scope rules are app/UI/cloud-managed (cursor, warp, trae, jetbrains-copilot, …) have no writable user file → the standard skip-warn, never silent. The two documented exceptions do NOT read AGENTS.md: - **claude-code → CLAUDE.md.** The official memory docs state verbatim: *"Claude Code reads CLAUDE.md, not AGENTS.md"* (code.claude.com/docs/en/memory; no AGENTS.md support shipped through v2.1.172). Default mode `"block"` writes the managed block directly into `/CLAUDE.md` (project) / `~/.claude/CLAUDE.md` (user) — zero side effects beyond the block itself. Opt-in `platforms["claude-code"].memory.mode: "agents-import"` instead writes the canonical block into AGENTS.md and manages Anthropic's documented interop — the `@AGENTS.md` import line (`@~/.claude/AGENTS.md` at user scope) — as its own tiny `_shared/claude-agents-import` bridge block in CLAUDE.md. The import is opt-in, never default, because it makes Claude read the ENTIRE AGENTS.md (user content included). When CLAUDE.md ALREADY imports or symlinks AGENTS.md, the adapter auto-behaves as agents-import: the canonical block goes to AGENTS.md, CLAUDE.md is not touched, and the pre-existing user wiring is never claimed as managed. HTML-comment markers are CORRECT here: Claude Code strips HTML comments from CLAUDE.md before context injection, so the markers and the do-not-edit notice are invisible to the model while remaining fully parseable for sync/doctor/uninstall. On AGENTS.md hosts (which inline the whole file into the prompt) the one-line notice doubles as an in-prompt "do not edit" instruction to the host's own agent. - **gemini-cli → GEMINI.md** (project `/GEMINI.md`, user `~/.gemini/GEMINI.md`) — unless `context.fileName` in `.gemini/settings.json` (project probed first, then user) includes `"AGENTS.md"`, in which case the natively-read AGENTS.md is targeted instead. PROBE-AND-RESPECT: agent-connector never flips host settings to make AGENTS.md readable — the `context.fileName: ["AGENTS.md", "GEMINI.md"]` opt-in is documented as a user recipe only. The DEDICATED rules-dir hosts also do NOT read AGENTS.md — each gets an agent-connector-owned file in its native rules directory (project scope; install writes it, uninstall deletes it): cline `/.clinerules/agent-connector.md`, amazon-q `/.amazonq/rules/agent-connector.md` (plain Markdown auto-applied as context — AWS context-project-rules docs), continue `/.continue/rules/agent-connector.md` leading with `alwaysApply: true` frontmatter ("always included" — Continue rules docs), and windsurf `/.windsurf/rules/agent-connector.md` leading with `trigger: always_on` frontmatter (full content in the system prompt every message — Windsurf Cascade rules docs). amazon-q/continue/windsurf have no primary-verified user/global rules dir, so user scope skip-warns there. Per-host tuning via `platforms..memory`: `false` disables the surface on that host; `{ path }` overrides the target file (absolute, or resolved against the project dir / home dir per scope); `{ mode }` is claude-code-only (ignored elsewhere with a `warn`). Scopes: `project` and `user`; `system` / `profile` / `managed` skip-warn (admin-owned memory files are out of scope in v1). **Reversibility.** Memory installs LAST among the content surfaces and is removed FIRST on uninstall. Uninstall candidates are the union of the re-probed targets and the persisted ownership ledger (`connectorDir(id)/memory-state.json`: platform / scope / path / blockId / createdFile / hash rows); in each candidate file EVERY block under the `/` marker prefix is excised — prefix scan, not the declared entry list, so renamed or stale entries are reclaimed too — plus at most one adjacent blank separator line. The markers in the committed file remain the source of truth: a teammate's machine with no ledger still uninstalls cleanly. A user-edited block (hash drift) is backed up before removal; a file agent-connector itself created is deleted when only whitespace remains; the claude-code `@AGENTS.md` bridge block is removed only when the sibling AGENTS.md holds no remaining agent-connector blocks (ref-counted — connector B's import keeps working when connector A uninstalls), and a user-authored import line is never touched. Re-running uninstall yields only `skip` records, and every byte outside agent-connector's own markers is exactly as the user left it. `doctor` verifies each ledger row: file present, block present, recorded hash == actual inner hash (user-edited drift → warn, never auto-fixed). ### 2.5 `StatuslineDef` — the status-line handler surface `ConnectorConfig.statusline` is a **handler surface** (like hooks, not like the file-writing content surfaces). It is SINGULAR — one `StatuslineDef` per connector, not an array. The developer supplies a `render(ctx)` function; agent-connector owns how it gets called. ```ts interface StatuslineDef { name?: string; // kebab-case id; default "statusline" description?: string; options?: StatuslineOptions; render: (ctx: StatuslineContext) => string | Promise; hosts?: Partial string | Promise; options?: StatuslineOptions; }>>; } interface StatuslineOptions { refreshInterval?: number; // integer >= 1; written only by hosts that support it respectUserColors?: boolean; // written only by hosts that support it hideContextIndicator?: boolean; // written only by hosts that support it maxLines?: number; // integer >= 1; enforced by the framework before host output } interface StatuslineContext { host: string; connectorId?: string; sessionId?: string; cwd?: string; model?: { id?: string; displayName?: string }; cost?: { totalUsd?: number }; context?: { usedTokens?: number; maxTokens?: number; percent?: number }; transcriptPath?: string; raw: unknown; // host's verbatim payload; fields the host doesn't provide are undefined } ``` SDK helpers exported from the package root and `@ken-jo/agent-connector/sdk`: - `defineStatusline({ render })` — typed identity helper - Types: `StatuslineDef`, `StatuslineContext`, `StatuslineOptions`, `StatuslineHostOverride`, `StatuslineMode` **Deployment.** `PlatformCapabilities.supportsStatusline` gates it (read `?? false`). Command-driven statusline hosts also expose `statuslineMode: "command-stdin"` and option support flags such as `statuslineSupportsRefreshInterval`, `statuslineSupportsRespectUserColors`, and `statuslineSupportsHideContextIndicator`. Hosts whose only status UI is a built-in preset must not be treated as supporting `StatuslineDef.render`. On **claude-code**, install registers: ```json { "statusLine": { "type": "command", "command": " statusline claude-code --connector ", "refreshInterval": 5 } } ``` in `settings.json`. The host execs that command on every status refresh; the entrypoint re-imports the connector and runs `render(ctx)`, printing the line. The optional `refreshInterval` field is written only when declared and supported. On **qwen-code**, install writes the nested `settings.json.ui.statusLine` command shape and may pass `refreshInterval`, `respectUserColors`, and `hideContextIndicator`. Its documented row cap is modeled via `statuslineMaxLines`, while connector-declared `options.maxLines` is still enforced by the framework for all statusline hosts. On **antigravity-cli**, install writes the `agy` command statusline shape under `~/.gemini/antigravity-cli/settings.json`. Registration REUSES the configPatch ownership ledger: **set-if-absent, never clobber a `statusLine` agent-connector doesn't own** (skip-warn + manual-edit printed), refcounted, reversible (uninstall removes it only when last-owner ∧ value-unchanged ∧ prior-absent). If the user or another tool (e.g. a live plugin) already owns `statusLine`, install skip-warns and prints the manual edit. **Every other adapter** inherits the BaseAdapter skip-warn (never silent) — exactly like other surfaces. Do not infer support from an adjacent host setting unless the adapter advertises `supportsStatusline` and wires the home-bin command. **Runtime is FAIL-SAFE.** ANY error (throwing `render`, unknown connector, malformed stdin, …) → exit 0 with empty stdout. A HUD must never wedge the host or corrupt the status bar. **CLI verb:** `agent-connector statusline --connector ` (internal, like `hook`/`serve`; the host points at it). **doctor:** a dedicated `statusline wired` check (`ok` / `present-but-not-ours` / `missing`). No telemetry in v1. **`statusLine` is a RESERVED configPatch key.** A raw `configPatch` entry targeting `statusLine` or `statusLine.*` throws `ConnectorConfigError` at `defineConnector`, pointing at this surface. Use `ConnectorConfig.statusline` instead. ### 2.6 `ActionDef[]` — the actions surface `ConnectorConfig.actions` is a list of **named, user-triggered actions** the connector exposes for out-of-band dispatch. Unlike hooks (which respond to host lifecycle events) and the statusline (which renders on a cadence), actions are invoked explicitly by the user or an external orchestrator. ```ts interface ActionDef { id: string; // kebab-case action identifier (required) label?: string; // short display label; defaults to description/id description?: string; // one-line hint for help / introspection output icon?: string; // host-specific icon name/string where supported placement?: ActionPlacement | ActionPlacement[]; confirm?: boolean | { title?: string; message?: string }; run: (ctx: HostCtx) => ActionResult | void | Promise; hosts?: Partial ActionResult | void | Promise; label?: string; description?: string; icon?: string; placement?: ActionPlacement | ActionPlacement[]; confirm?: boolean | { title?: string; message?: string }; }>>; // per-host run and/or metadata override } interface ActionResult { message?: string; // human-readable outcome printed to stdout } ``` `defineAction({ id, run })` is the typed identity helper (exported from **both** the root `@ken-jo/agent-connector` and `/sdk`). Related public types: `ActionResult`, `ActionHandler`, `ActionConfirm`, `ActionPlacement`, `ActionHostOverride`, `ActionInvocationMode`, and `ActionAffordanceKind`. **CLI verb:** `agent-connector action --connector ` — loads the connector, resolves the action by id, and invokes `run(ctx)` (or the per-host override when present). This is an **internal entrypoint** like `hook`/`serve`/`statusline`; the host or the user points at it directly. **User-triggered error semantics** (intentionally stricter than hooks/statusline): - Unknown `actionId` → exit 1 + stderr message. - A `run` that throws → exit 1 + stderr (the exception message). - These are NOT fail-open/fail-safe-silent — an action the user explicitly triggered should report errors, not swallow them. **Affordance metadata.** `label`, `description`, `icon`, `placement`, and `confirm` are optional hints for host-native emitters. Adapters only render what their host can express; unsupported placement/confirmation semantics are ignored rather than invented. `hosts.` can override either `run` or the metadata for that host, while top-level `run` remains the mandatory fallback. Host ids in the map must be registered platform ids; invalid ids or non-function override handlers throw `ConnectorConfigError` at `defineConnector`. **Dispatch + verified emitters.** The stable dispatch verb works for every host when invoked directly, and adapters with a verified native trigger surface emit host affordances: droid, hermes, kiro, nemoclaw, omp, openclaw, pi, warp, and zed. Hosts without a verified emission target skip-warn instead of pretending to support a native trigger. Capability metadata distinguishes how support works: `actionInvocationMode` is one of `exec`, `exec-file`, `manual-hook`, `paste`, `plugin-command`, or `task`; `actionAffordanceKind` is one of `command-palette`, `extension-command`, `hook-panel`, `slash-command`, `task`, or `workflow`. **Introspection:** `explain()` emits one row per action per host (`"native"` only for verified emitter hosts; `"skip-warn"` elsewhere). `simulate()` does **NOT** cover actions: an action takes no host payload and has no host-honor verdict (the dispatch is unconditional — intentional). ### 2.7 `TelemetryConfig` ```ts interface TelemetryConfig { enabled?: boolean; // default true; AGENT_CONNECTOR_TELEMETRY=0 also kills it modelFamilyHint?: "auto" | "openai" | "anthropic" | "generic"; // default "auto" measureToolDefs?: boolean; // default true; tokenize tools/list once → per-turn overhead calibration?: { anthropicCountTokens?: boolean }; // default false; opt-in network calibration (sends content off-box) hostNativeUsage?: boolean; // default false; opt-in host-native turn capture (see §5) store?: "ndjson" | "sqlite"; // default "ndjson" (no native deps); sqlite is an upgrade } ``` `ResolvedConnector.telemetry` is fully resolved: `{ enabled, modelFamilyHint, measureToolDefs, hostNativeUsage, store, calibration: { anthropicCountTokens } }`. `hostNativeUsage` may also be forced on at install via env `AGENT_CONNECTOR_HOST_NATIVE=1`. ### 2.8 `PlatformOverride` (per-platform escape hatch) ```ts interface PlatformOverride { hooks?: boolean | Partial; // false → no NORMALIZED hooks here; object → merge/replace nativeHooks?: Record; // native-event passthrough (§2.3); honored where supportsNativeHooks is true configPatch?: ConfigPatchDef[]; // host-config key patches (below); claude-code only today (§2.8) server?: Partial | false; // false → don't register server here; object → shallow-merge scope?: InstallScope; // force a scope for this platform commands?: boolean; // false → skip command files here skills?: boolean; // false → skip skill files here subagents?: boolean; // false → skip subagent files here memory?: boolean | PlatformMemoryOverride; // false → no memory block here; object → per-host tuning extra?: Record; // verbatim fields merged into the native config } interface PlatformMemoryOverride { path?: string; // override the target memory file (absolute, or resolved // against projectDir / home per scope) mode?: "block" | "agents-import"; // claude-code ONLY (ignored elsewhere with a warn): // "block" (default) = managed block in CLAUDE.md; // "agents-import" = canonical block in AGENTS.md + managed // @AGENTS.md import bridge in CLAUDE.md (§2.4) } ``` Use `extra` to reach platform-exclusive features the core doesn't model (thin universal core + fat per-adapter tail). **Host-config key patches (`platforms..configPatch`)** — the third escape hatch beside `extra` and `nativeHooks`, for host-exclusive config KEYS that `extra` cannot reach: `extra` merges into the native MCP server ENTRY (or content frontmatter), not sibling top-level settings keys, so something like Claude Code's `env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS` is structurally unreachable without it. Connectors name a platform + key, NEVER a file path — the adapter owns the key→file mapping. ```ts type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue }; interface ConfigPatchDef { key: string; // dotted LEAF path, e.g. "env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS"; // segments match /^[A-Za-z0-9_-]+$/ — no dots-in-key, no array indices value: JsonValue; // written ONLY when the key is absent; ${env:VAR} refs resolve at install time; // may be an object/array but is written atomically as the leaf — never merged into reason: string; // REQUIRED why — printed in the install diff, every ChangeRecord, every skip-warn docsUrl?: string; // appended to the manual-edit fallback printed on skip/conflict/unsupported host } ``` SEMANTICS ARE FIXED, not configurable: **set-if-absent on a single leaf key, skip-warn on ANY conflict** — no overwrite, no delete, no deep merge, no array ops, no `onConflict`/`force` options. Intermediate objects are created only when absent. Every outcome is a `ChangeRecord` in the install diff (`--dry-run` shows the exact key + value + reason before anything is written; warns exit 1): | State at install | Outcome | |---|---| | Key absent | `create` — value written, ownership recorded: `configPatch : (reason)` | | Present, AC-owned, equals what AC wrote | `skip` — this connector registered as co-owner (refcount++); no file write | | Present, AC-owned, value drifted (user edited) | `warn` — left in place, never reverted; sync re-asserts only ABSENT keys | | Present, NOT AC-owned (even if values match) | `warn` — never adopted; prints current vs desired + the exact manual edit | | Owned by another connector with a different value | `warn` — first-writer-wins, names the owning connector(s) | | Intermediate path segment exists but is not an object | `warn` — manual edit printed | | Key in the AC namespace (`hooks*`, `mcpServers*`, `enable/enabled/disabledMcpjsonServers`, `statusLine`/`statusLine.*`) | `ConnectorConfigError` at defineConnector — use `hooks`/`nativeHooks`, `server`/`extra`, or the `statusline` surface | | Key on the sensitive denylist (below) | `warn` — hard refuse, no override flag in v1 | **Ownership ledger + uninstall.** Ownership lives in a persisted, refcounted ledger at `/state/config-patches.json` (atomic temp+rename writes; shared across connectors because refcounting needs a global view; survives `--purge` of a connector's dir). One row per (platform, file, key): `{ writtenValue, writtenValueHash (sha256 of canonical JSON; deep-equal is the authority), prior: { present: false }, owners: [{ connectorId, connectorVersion, installedAt }] }`. `prior` is ALWAYS `{ present: false }` — ownership only ever attaches to keys agent-connector itself created, so the blind-restore footgun is structurally impossible. configPatches are installed LAST (after server/hooks/content surfaces) and uninstalled FIRST. Uninstall is keyed off the LEDGER, not the declaration, and removes a key ONLY when this connector is the LAST owner AND the current value still deep-equals `writtenValue` AND the prior state was absent — after backing up the exact file. Any other state: earlier owners out → `skip: key retained, still owned by ` (A-installs/B-relies/A-uninstalls cannot break B); drifted value → `warn: value changed since install; left in place` (ownership still released). Declared patches with no ownership record are skipped explicitly — a key AC did not create is never deleted. **Safety gates** (validated at `defineConnector` where statically knowable, re-validated in the adapter): leaf-path grammar, JSON-serializable value, required non-empty `reason`, duplicate-key rejection, the AC-namespace guard above (`hooks*`, `mcpServers*`, `statusLine`/`statusLine.*` — use the `statusline` surface), and a per-adapter sensitive-key DENYLIST. The documented claude-code v1 list (hard refuse): `permissions`/`permissions.*`, `allowedTools*`/`disallowedTools*`, `apiKey*` (e.g. `apiKeyHelper`), `awsAuthRefresh`, `awsCredentialExport`, `forceLoginMethod`, `forceLoginOrgUUID`, `otelHeadersHelper`, `env.ANTHROPIC_*`, `env.AWS_*`, `env.*_PROXY`, `env.*TOKEN*`, `env.*KEY*`, `env.*SECRET*`. (The teams-flag env case passes; credential rerouting and self-granted permissions do not.) The list lives in the claude-code adapter (`claudeSensitiveKeyViolation`) and is reviewed whenever the host matrix updates. **v1 host scope: claude-code ONLY**, opt-in via `PlatformCapabilities.supportsConfigPatch` (optional, read `?? false` — the `supportsNativeHooks` precedent). The patchable file is exactly one per scope: `~/.claude/settings.json` (user) / `/.claude/settings.json` (project), and keys are free-form leaf paths within that one file, minus the denylist. Every other adapter reports the standard skip-warn ChangeRecord (`configPatch not supported on ; N skipped` — never silent) plus a per-patch manual-edit line built from `reason`/`docsUrl`, so one declaration doubles as its own documented manual step on unsupported hosts. **doctor** checks every ledger entry owned by the connector: `ok` / `drifted` (current ≠ written — prints the manual-edit hint, NEVER auto-fixes) / `missing` (key deleted by the user; the next install/upgrade re-asserts it) / `orphaned` (ledger row whose owning connector records are gone — GC hint). It also re-prints the manual edit for any declared patch that was skipped at install (conflict, denylist, or not yet installed). Example — enabling an experimental Claude Code teams feature: ```ts platforms: { "claude-code": { configPatch: [{ key: "env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS", value: "1", reason: "Enable experimental agent-teams support required by acme-db", docsUrl: "https://github.com/acme/acme-db#agent-teams", }], }, }, ``` > **Note:** `statusLine` / `statusLine.*` is a RESERVED key — a raw `configPatch` > targeting it throws `ConnectorConfigError` pointing at the `statusline` surface > (`declare it via statusline: { render } instead`). Use `ConnectorConfig.statusline` > (§2.5) rather than patching `statusLine` directly. **Explicitly OUT of scope in v1** (deferred with the trigger that un-defers each): VS Code `inputs` arrays and Zed `context_servers..settings` — same-file SIBLING structures coupled to the MCP entry's lifecycle, where set-if-absent semantics are simply wrong (adapter dialect / `extra` territory; VS Code `inputs` doubles as its secret-prompt mechanism); TOML hosts — `core/toml.ts`'s parse/stringify round-trip destroys comments and ordering in files like `~/.codex/config.toml` and is BANNED for configPatch (Codex's `experimental_use_rmcp_client` becomes codex-adapter-internal behavior via an anchored line edit when remote-MCP support lands — no connector API); secret sourcing (the denylist refuses token/key/secret env paths; secrets need an acquisition/prompt surface, not a config write); sidecar/side-state files (`~/.mcp-auth`, browser profiles, caches — an uninstall-manifest concern, not a config key); runtime prereq checks (Docker/Node — doctor roadmap); array/index paths and deep merges (no surveyed demand); sync-removal of patches dropped between connector versions (uninstall/reinstall covers it). Future JSONC hosts MUST use jsonc-parser modify/applyEdits — never whole-file re-serialization. **Promotion rules** (mirroring the nativeHooks ≥3-hosts bar): (a) a second host gains `supportsConfigPatch` only on demonstrated, genuine connector-facing key demand for that host; (b) a host-exclusive feature graduates from `configPatch` to a typed cross-host surface (e.g. a new `ConnectorConfig` field) only when ≥3 hosts ship an analog (the `statusline` surface is the first example of this promotion path). ### 2.9 `ResolvedConnector` (what `defineConnector` returns) ```ts interface ResolvedConnector { id: string; displayName: string; version: string; server?: ServerDef; // normalized hooks: HooksConfig; hookEvents: HookEventName[]; // events with a function handler telemetry: { enabled; modelFamilyHint; measureToolDefs; hostNativeUsage; store; calibration: { anthropicCountTokens } }; commands: CommandDef[]; // normalized; [] when none skills: SkillDef[]; // normalized; [] when none subagents: SubagentDef[]; // normalized; [] when none memory: MemoryDef[]; // normalized; names defaulted ("memory"); [] when none statusline?: StatuslineDef; // undefined when not declared actions: ActionDef[]; // normalized; [] when none platforms: Partial>; targets: "auto" | PlatformId[]; publish?: PublishConfig; // registry/MCPB metadata, passed through verbatim } ``` ### 2.10 Full example (server + hooks + commands + skills) The example below is database-shaped. Do not generalize that into "all MCPs are database/write-guard MCPs." Match the MCP launch shape to the product: package-runner MCP (`npx -y `), local Node/process MCP (`node `), Python MCP (`uv run --with mcp ` by default, direct `python ` only when the environment is managed elsewhere), CLI-based MCP (` mcp serve`), or remote server MCP (`transport: "http"` + URL). Keep package identity in `package.json`, then add memory/skills only when they reflect that MCP's actual behavior. Framework relationship: `package.json` is public identity, `bin.mjs` exposes the developer's branded command via `createConnectorCli({ packageJson, connector })`, and `defineConnector({ server })` points at the actual MCP process or remote URL. Install renders native host config from that one declaration. Stdio servers are launched through the stable home binary (`agent-connector serve ... -- `) so per-tool telemetry can be measured; remote HTTP servers are registered by URL where the host supports remote MCP and have no local stdio process to wrap. ```ts import { defineConnector } from "@ken-jo/agent-connector/sdk"; export default defineConnector({ // package.json / npm metadata is the source of truth: // name/mcpName/bin/version derive the install id, host label, bin, and version. // Host-native ids are generated during install; don't copy them back here as id/displayName. server: { transport: "stdio", command: "npx", args: ["-y", "@acme/acme-db-mcp"], env: { ACME_DB_DSN: "${env:ACME_DB_DSN}" }, tools: { include: ["*"] }, timeoutMs: 30_000, }, hooks: { PreToolUse: { matcher: "acme_write", async handler(evt) { if (evt.toolName === "acme_write") return { decision: "ask", reason: "Confirm Acme DB write" }; return { decision: "allow" }; }, }, SessionStart: { async handler() { return { decision: "context", additionalContext: "Acme DB schema v12 is loaded." }; }, }, }, telemetry: { enabled: true, modelFamilyHint: "auto", measureToolDefs: true }, platforms: { warp: { hooks: false } }, // Warp is mcp-only: skip hooks gracefully targets: "auto", }); ``` The normal foreground command is the developer package/bin: `npx @acme/acme-db-mcp install` or `acme-db install`. The framework fallback `agent-connector install --connector ...` renders the same artifacts during local development, CI, or debugging. Either path turns the server into, e.g.: | Host | What gets written | |---|---| | Claude Code | `~/.claude.json` → `mcpServers.acme-db` (+ hooks in `~/.claude/settings.json`) | | Codex CLI | `~/.codex/config.toml` → `[mcp_servers.acme-db]` (+ `~/.codex/hooks.json`) | | Cursor | `~/.cursor/mcp.json` → `mcpServers.acme-db` (+ `~/.cursor/hooks.json`) | …each pointing hooks at the single stable home binary. --- ## 3. CLI reference `agent-connector [flags]`. Run `agent-connector --help` for command-specific flags. `--help`/`-h`/`help` print usage; `--version`/`-v` prints ` `. Shared flags: `--scope user|project` (default `user`); `--targets a,b,c` (comma-separated `PlatformId` list); `--connector ` (explicit config); `--project ` (defaults to cwd); `--dry-run`; `--json` (where noted). ### detect `agent-connector detect [--project ] [--json]` Probes every registered adapter and prints, per installed host: name, id, hook paradigm, install scope, the native config path that would be written, confidence + reason, and a one-line capabilities summary (events, transports, modifyArgs / modifyOutput / injectContext). `--json` emits the raw `DetectedPlatform[]`. ### install `agent-connector install [--method direct|marketplace] [--scope user|project] [--targets …] [--connector ] [--project ] [--dry-run] [--force]` `--method marketplace` drives the HOST's own plugin flow instead of writing config directly. Drivable hosts (10, a `MarketplaceDriver` per host, 3 shapes): `claude-code`, `codex`, `gemini-cli`, `opencode`, `kilo`, `kilo-cli`, `antigravity`, `antigravity-cli` (live-verified) + `droid`, `qwen-code` (driver shipped, pending a live host). Three shapes: (1) CATALOG — claude, codex, droid: stage under `/marketplace//`, regenerate the shared local catalog, register once by reference (` plugin marketplace add`), then the install verb (`claude plugin install` / `codex|droid plugin add|install` `@agent-connector`). (2) DIRECT — agy, gemini-cli, qwen-code: install-by-path (`agy plugin install ` / `gemini extensions install --consent` / `qwen extensions install `) + the host's uninstall verb, no marketplace registration. (3) NPM-LOCAL — opencode, kilo, kilo-cli: ` plugin --global file://` writes a `file://` entry into the host config's `plugin` array (run from a neutral cwd); there is no host uninstall verb, so removal EDITS the config array back. Probe-first + headless; if the host CLI is absent it degrades to printing the exact commands; non-drivable marketplace-format hosts (cursor, pi, vscode-copilot, openclaw, omp, kimi) still print manual commands. Windows specifics handled: codex's `\\?\` source-path canonicalization and agy's win32 manifest location (claude/codex/agy native-Windows-verified). A guard refuses installing the same connector by BOTH methods (duplicated hooks + server); `uninstall --method auto` reverses whichever method is present, and removing the last staged plugin also removes the shared marketplace registration. State: `/marketplace/marketplace-installs.json`. The default `--method direct`: Resolves the connector, then per target: backup settings → render server config into the native file → if hooks & paradigm≠mcp-only: synthesize the entrypoint + write hook config + set the exec bit → write any command/skill/subagent files → upsert any `memory` managed blocks (last among the content surfaces, §2.4) → register in the plugin registry where applicable → apply any `configPatch` entries LAST (§2.8 — so the diff reflects final state; `--dry-run` shows the exact key + value + reason). `--force` overwrites USER-EDITED memory blocks (hash drift) after a timestamped backup; the default is warn-and-leave. Prints a readable diff (one line per `ChangeRecord`: `+` create, `~` update, `-` remove, `=` skip, `!` warn) plus warnings and a summary tally. Idempotent and reversible. **Exit code 1 if any change is a `warn`**, else 0. ### upgrade `agent-connector upgrade [--channel stable|latest] [same flags as install]` The single "bring everything current" verb (back-compat aliases: `update`, `sync`). Step 1: idempotent re-render of the connector into every target host (byte-identical entries report `skip` — also the self-heal path for a drifted install). Step 2: refreshes the stable home-bin pointer and prints managed-update guidance (the exact `npm i -g @ken-jo/agent-connector@` when the install looks npm-managed: `latest` for stable, `next` for latest). With no resolvable connector it still does step 2 (tool-only refresh from anywhere). **Never silently auto-updates** (architecture R1). Same diff output and exit semantics as install for the re-render; exit 1 if the pointer refresh fails. ### uninstall `agent-connector uninstall [--connector-id ] [--connector ] [--scope …] [--targets …] [--project ] [--purge] [--dry-run]` Full inverse — releases `configPatch` ownership FIRST (ledger-keyed; a patched key is deleted only when this connector is the last owner and the value is unchanged, §2.8), then excises `memory` managed blocks (first among the content surfaces — prefix scan over the connector's marker namespace + the memory ledger, so even an id-only synthetic uninstall reclaims blocks, §2.4), then removes the connector's MCP + hook registrations and content files from every resolved target, using registered metadata so it works even when the source module is gone. The connector id comes from `--connector-id`, else inferred from the local config. Same diff output and exit semantics. `--purge` additionally removes the connector's registered framework state (`connectors//` under the data-root) and, when no connectors remain, the shared home-bin launcher. ### doctor `agent-connector doctor [--targets …] [--connector ] [--scope …] [--project ] [--json] [--probe]` For each detected host (or `--targets`), loads its adapter, builds an `InstallContext`, and runs the adapter's doctor checks; prints `[pass] / [warn] / [FAIL]` with any suggested fix. Connector context comes from the local config, else every connector registered under the data-root, else a minimal id-only placeholder. Installed `memory` blocks are verified per ledger row — file present, block present, recorded hash == actual inner hash (user-edited drift → warn, never auto-fixed). With `--probe` it also spawns the connector's REAL stdio server and runs a live MCP handshake (initialize → ping → tools/list); probe FAILs fold into the exit code. **Non-zero exit if any check FAILs** (warns alone do not fail). ### status `agent-connector status [--connector ] [--scope user|project] [--project ] [--json]` Light, glanceable install-state summary: one line per detected host showing which connectors are present (server ✓ / hooks ✓) via a read-only config-presence check. **Always exits 0** — it describes, never gates (that contrast with doctor is why it exists). ### package `agent-connector package [--connector ] [--format |all] [--out ] [--project ] [--dry-run]` `package` is framework tooling, not a branded MCP lifecycle command: use `npx @ken-jo/agent-connector package --connector ./agent-connector.config.mjs` or a global `agent-connector package` CLI. Keep `install`, `doctor`, `upgrade`/`update`, `uninstall`, and connector telemetry under the branded MCP package/bin. The command emits a marketplace/extension-installable bundle into `--out` (default `/dist-plugin`), printing the emitted file tree + per-format install instructions. Host formats (default `claude-plugin`; `--format all` emits all 10): claude-plugin · codex-plugin · copilot-plugin · factory-plugin · gemini-extension · qwen-extension · agy-plugin · cursor-plugin · kimi-plugin · npm-plugin. Two OFFICIAL MCP standard artifacts are emitted only by explicit name (they require the connector's `publish` block and are excluded from `all`): `mcp-server-json` (an MCP Registry server.json, schema 2025-12-11, describing your REAL upstream server — not the serve wrapper) and `mcpb` (an MCPB manifest, manifest_version 0.3). Bundled hooks + MCP keep the home-bin pointer and the telemetry serve-wrapper. ### telemetry `agent-connector telemetry [flags]` — per-MCP token telemetry (the server's own bytes). Rows are aggregate counts only (never raw arguments/results). - `report --by tool|session|project (default tool) --since --connector [--json]` → ranked footprint table; `--json` emits the rows. - `export --format csv|json (default json) --out --since … --connector ` → raw aggregate records (stdout, or to `--out`). - `leaderboard --by mcp|tool|surface (default mcp) --since … --connector --scope [--json]` → ranks per-connector ("which MCP server costs the most"), per-tool, or per developer-axis surface (`--by surface` folds the STATIC command/skill/subagent context footprints in beside the runtime server + hook rows). ### usage `agent-connector usage [flags]` — host-native token usage parsed **read-only** from each agent CLI's own session logs/DBs (the complement to `telemetry`; the two are NOT summed). Rows are aggregate counts only. - `report --by platform|project|session|model|day (default platform) --since … --platform [--json]` → aggregated table; prints skip notes for platforms requiring a sync. - `export --format csv|json --out --since … --platform ` → deduped records. - `leaderboard --by platform|model (default platform) --since … --platform [--json]` → the host/user leaderboard ("which CLI/host spent the most"). ### leaderboard (unified) `agent-connector leaderboard [--since ] [--scope ] [--connector ] [--json]` Prints THREE origin-labeled leaderboards that measure DIFFERENT things and are **NEVER summed**: - 🔌 **MCP / Plugin** (origin `mcp-self`) — serve-proxy telemetry (per-MCP `call` + `tool_defs` rows; excludes host-native `model_turn` rows). - 🖥️ **Host / User** (origin `host-scan-logs`) — host usage from scanning CLI logs. - 🛰️ **Host-native turns (live, exact)** (origin `host-native-live`) — the opt-in AfterModel / PostInvocation usage hook (scope `model_turn`, confidence host-native). `--scope` slices only the MCP section; `--connector` restricts the 🔌 MCP and 🛰️ host-native sections to one connector (the 🖥️ host-scan section is connector-agnostic); `--json` emits `{ mcp, host, hostSkipped, hostNativeTurns }`. ### Internal entrypoints (hosts point at these; omitted/hidden from top-level help) - `agent-connector hook --connector ` — universal `json-stdio` hook entrypoint. Reads the whole host payload from stdin, dispatches `runHook`, writes stdout/stderr, exits with the adapter's exit code. Fail-open (never rejects). - `agent-connector serve --connector [--scope user|project] [--host ] -- [args…]` — telemetry-wrapping MCP stdio proxy. Splits argv at the first literal `--`; the real server invocation on the right is passed through verbatim. `--host` bakes the install-target platform id into the wrapper so telemetry rows carry the correct hostPlatform under headless spawns. Tolerant flag parsing (`strict:false`) so a future/older wrapper flag can't wedge a tool call. - `agent-connector usage-event --connector ` — HIDDEN opt-in host-native turn-usage hook (installed by Gemini / Antigravity adapters when host-native usage is enabled). Reads stdin, records a distinct `model_turn` row, ALWAYS exits 0 (fail-open). - `agent-connector action --connector ` — user-triggered action dispatch. Loads the connector, resolves the action by id, and runs `run(ctx)` (or the per-host override). Exit 1 on unknown id or a throwing handler (NOT fail-open — see §2.6). **`--since` syntax** (telemetry/usage/leaderboard): `Ns`, `Nm`, `Nh`, `Nd` (seconds/minutes/hours/days), e.g. `30s`, `15m`, `24h`, `7d`. Empty = no lower bound; malformed = error. --- ## 4. Detection & capabilities Two detection layers (from `src/adapters/detect.ts`): - **Install-time platform detection** — which hosts are installed (config-dir + marker files) → `DetectedPlatform[]`. - **Runtime host detection** — which host is executing this hook now (env-var markers → config-dir → `clientInfo`), with fork-before-parent ordering & foreign-env scrubbing → `DetectionSignal`. `PlatformCapabilities` flags the single-API layer queries to degrade gracefully: the required per-event booleans (`preToolUse`, `postToolUse`, `preCompact`, `sessionStart`, `sessionEnd`, `userPromptSubmit`, `stop`, `notification`), `canModifyArgs`, `canModifyOutput`, `canInjectSessionContext`, and `transports: Transport[]`; plus OPTIONAL flags (each read `?? false`): the 5 newer per-event flags `permissionRequest` / `postToolUseFailure` / `subagentStart` / `subagentStop` / `postCompact` (1:1 with the 13-member `HookEventName` union), the content-surface flags `supportsCommands` / `supportsSkills` / `supportsSubagents` / `supportsMemory`, and the handler/escape-hatch surface flags `supportsStatusline` / `supportsConfigPatch` / `supportsNativeHooks` / `supportsActions` (unsupported adapters skip-warn; current counts and per-host coverage are canonical on `/coverage`, not in this prose). `InstallScope` is the normalized, low→high-precedence enum `{ system, user, project, profile, managed }`; each adapter maps it to a concrete path and knows its precedence. The CLI accepts `user` (default) and `project`. Result types: `DetectedPlatform { id, name, installed, paradigm, capabilities, configPath, scope, reason, confidence }`; `ChangeRecord { platform, action: create|update|skip|remove|warn, path?, detail }`; `InstallResult { connectorId, dryRun, changes, warnings }`; `DiagnosticResult { check, status: pass|fail|warn, message, fix? }`. --- ## 5. Telemetry model **What is measured.** The only data identical across hosts: the server's own bytes. The `agent-connector serve` proxy (or in-proc middleware) intercepts every `tools/call` at the server boundary — input = `params.arguments`, output = `result.content[]` + `structuredContent` — and tokenizes them locally. With `measureToolDefs` (default on) it also tokenizes the `tools/list` schemas once → the fixed "cost of merely defining my tools" per-turn overhead. **Tokenizer.** Default `gpt-tokenizer` (pure-JS, no native build → Windows/single- binary safe): `o200k_base` for OpenAI/Codex-family (labeled `tokenizer-exact`), and the same `o200k_base` as a **documented approximation** for every other family (labeled `tokenizer-approx`; no offline Claude tokenizer ships). Family is auto-selected from `initialize.clientInfo` or `modelFamilyHint`. Fallback is a `chars/4` heuristic (with content-type multipliers; never tokenizing base64) — explicitly **labeled** so it's never mistaken for exact. **Confidence sources** (every row carries one, ranked heuristic < tokenizer-approx < tokenizer-calibrated < tokenizer-exact < host-native): `tokenizer-exact` | `tokenizer-calibrated` | `tokenizer-approx` | `heuristic` | `host-native`. Opt-in enrichers (never the hot path): an Anthropic `count_tokens` calibration sampler (sends content off-box → opt-in only; can surface a `tokenizer-calibrated` confidence) and host-native usage where it exists (e.g. Gemini `usageMetadata.totalTokenCount` via the opt-in AfterModel hook). **Store.** Local, under the data-root, **aggregate counts only — never raw args/results**. MVP is an append-atomic NDJSON event log + derived rollups behind a `TelemetryStore` interface (`store: "sqlite"` is a drop-in upgrade). Rows keyed by roughly `connectorId, toolName, scope (call | tool_defs | model_turn | hook), surfaceKind (server | hook | command | skill | subagent), hostPlatform, sessionId, projectKey, projectDir, inputTokens, outputTokens, confidenceSource, isError, ts`. **Host usage layer (`src/usage/`) — the end-user / Audience B view.** This is the subsystem behind the connector-free `agent-connector usage` command (§0 Audience B). It is the ONLY telemetry surface an end user can use with no `defineConnector`, no install, and no config file: a read-only reader set parses each agent CLI's native logs/DBs (JSONL / JSON / SQLite via pure-WASM `sql.js` / synced-cache artifacts) and reports usage aggregated by `platform | project | session | model | day`. Confidence is `host-reported` (real numbers) vs `host-estimated` (e.g. Kiro char/4, Crush cost-only). It never writes host config and never collides with the serve-proxy store. **Limitation — no per-MCP / per-tool attribution on this path.** The host-usage layer groups **only** by `platform | project | session | model | day` (`UsageGroupBy` has exactly those five values — no `tool` or `mcp` member), and a `UsageRecord` carries no `toolName` / `mcp` field at all (only `platformId`, `modelId`, `providerId`, `sessionId`, `projectKey`, `tokens`, `ts`, …). Agent CLIs fold tool-result bytes into the session's input tokens and never attribute them to a tool name, so this path can report **whole-conversation totals only** — it does NOT and cannot itemize cost per individual MCP server or per tool. **Per-MCP / per-tool token data exists ONLY in the serve-proxy telemetry store** (the developer/surface axis below), which requires a registered connector and a stdio server it wraps — i.e. an Audience-A capability for the developer's OWN server. An end user cannot get per-MCP numbers for an MCP they did not author and wrap. **Coverage caveats.** Telemetry **auto-wrapping is stdio-only**: remote (`http`/`sse`/ `ws`) servers are registered as plain URL entries and are never wrapped, so they yield no per-tool serve-proxy telemetry (and `doctor --probe` skips them). On the host-usage side, **5 "synced" platforms — `cursor`, `antigravity`, `antigravity-cli`, `trae`, `warp` — need an external sync agent-connector does not perform**, so their rows are reported as "requires sync — no local cache found" and skipped, unless a tokscale-style local cache already exists (agent-connector does not populate that cache). **Two axes, five surfaces.** The developer/surface axis measures what the CONNECTOR costs across all five surfaces: `server` (runtime serve-proxy rows, scopes `call` + `tool_defs`) and `hooks` (one runtime row per hook dispatch through the home-bin entrypoint, scope `hook`, fail-open) are measured live; `commands`, `skills`, `subagents` are computed on demand as STATIC context footprints (never stored as rows). The user/host axis measures whole-conversation usage (host-scan log readers + the opt-in live `model_turn` hook). **Three leaderboards, never summed — and each has a DIFFERENT prerequisite.** The unified `agent-connector leaderboard` (§3) prints three origin-labeled boards that measure different things and have different requirements; understanding the prerequisite makes the audience boundary unambiguous: - 🔌 **MCP / Plugin** (`mcp-self`) — per-MCP server bytes from the serve-proxy. **Needs a registered connector + serve traffic** through a stdio server it wraps (Audience A, measuring the developer's OWN server). Empty for a plain end user. - 🖥️ **Host / User** (`host-scan-logs`) — whole-conversation usage from scanning each CLI's own logs. **Needs no setup** — this is the only board populated for a connector- free end user (Audience B), and it shows per-CLI / per-model / per-session / per-day totals only, never per-MCP or per-tool (same boundary as the host-usage layer above). - 🛰️ **Host-native turns (live, exact)** (`host-native-live`) — exact per-turn `model_turn` rows. **Needs the opt-in AfterModel / PostInvocation usage hook, which only the Gemini CLI and Antigravity adapters install** (when `hostNativeUsage` is enabled) and which requires `--connector` at runtime. Empty for a plain end user. Because only the 🖥️ board's data source is connector-free, `agent-connector usage` — not the unified `leaderboard` — is the primary end-user (Audience B) entry point. Totals are never added across these origins. **Privacy / opt-out.** Local-first, **zero network egress by default**. Global kill switch `AGENT_CONNECTOR_TELEMETRY=0` (or `telemetry: { enabled: false }`); per-layer opt-in for measure / calibrate / host-native (`AGENT_CONNECTOR_HOST_NATIVE=1`). Reported numbers are estimates from the server's own I/O, not host-billed usage. **Env switches.** `AGENT_CONNECTOR_DATA_DIR` (relocate the framework data-root) · `AGENT_CONNECTOR_TELEMETRY=0` (global telemetry kill switch) · `AGENT_CONNECTOR_HOST_NATIVE=1` (force host-native turn capture on at install) · `AGENT_CONNECTOR_CALIBRATE=anthropic` **+** `ANTHROPIC_API_KEY` (BOTH required to enable the opt-in count_tokens calibration sampler; either missing → disabled — the `calibration` config field alone does not enable it) · `AGENT_CONNECTOR_LOG=silent|error|warn|info|debug` (stderr log level, default info) · `AGENT_CONNECTOR_PLATFORM=` (override install-time host detection) · `AGENT_CONNECTOR_HOST=` (override runtime host detection in the hook entrypoint) · `AGENT_CONNECTOR_SESSION=` (session id for the serve proxy when the host provides none). --- ## 6. Supported platforms by hook paradigm `PlatformId` is a closed union; the adapter registry has one entry per platform. Current counts and per-host coverage are canonical at https://agent-connector.ai/coverage. Paradigm taxonomy (the deepest cross-platform divergence): ### `json-stdio` — full hook dispatch (22) One universal hook entrypoint binary handles all of them. | Platform | id | MCP native target (root key / file) | |---|---|---| | Claude Code | `claude-code` | `~/.claude.json` / `.mcp.json` → `mcpServers` (hooks in `settings.json`) | | CodeBuddy (Tencent) | `codebuddy` | `~/.codebuddy.json` / `.mcp.json` → `mcpServers` (Claude Code fork: hooks in `.codebuddy/settings.json`, memory in `CODEBUDDY.md`) | | Codex CLI | `codex` | `~/.codex/config.toml` → `[mcp_servers.*]` (hooks in `hooks.json`) | | Cursor | `cursor` | `.cursor/mcp.json` → `mcpServers` (hooks in `hooks.json`) | | VS Code Copilot | `vscode-copilot` | `.vscode/mcp.json` → `servers` | | JetBrains Copilot | `jetbrains-copilot` | shares the GitHub Copilot `.github/` files | | GitHub Copilot CLI | `copilot-cli` | `mcp.json` → `mcpServers` | | Gemini CLI | `gemini-cli` | `.gemini/` → `mcpServers` (opt-in host-native `AfterModel` usage) | | Qwen CLI | `qwen-code` | `.qwen/` → `mcpServers` | | Kiro | `kiro` | `mcpServers` | | Kimi CLI | `kimi` | `mcpServers` | | Crush | `crush` | config → `mcp` (root key, not `mcpServers`) | | Goose | `goose` | host config | | Hermes | `hermes` | host config | | Droid (Factory) | `droid` | `~/.factory/mcp.json` → `mcpServers` (hooks in separate `~/.factory/hooks.json`) | | OpenHands | `openhands` | `~/.openhands/mcp.json` → `mcpServers` (FastMCP entry shape; `$OPENHANDS_PERSISTENCE_DIR`); hooks in a separate Claude-Code-plugin-compatible `.openhands/hooks.json` (6 events) | | Antigravity (IDE) | `antigravity` | `~/.gemini/antigravity/mcp_config.json` → `mcpServers` (hooks.json) | | Antigravity CLI (`agy`) | `antigravity-cli` | shares `~/.gemini/antigravity/` | | Continue | `continue` | `~/.continue/config.yaml` → `mcpServers` (YAML ARRAY, keyed by `name`); hooks in a separate `settings.json` (honors `CONTINUE_GLOBAL_DIR`) | | Amazon Q Developer CLI | `amazon-q` | `~/.aws/amazonq/mcp.json` (user) / `.amazonq/mcp.json` (project) → `mcpServers` (hooks in the built-in default agent file `cli-agents/q_cli_default.json` — a bare `default.json` would be an inactive custom agent: trigger-keyed `hooks` object, entries `{ command, matcher? }`; subagents as per-agent JSON `cli-agents/.json` where the filename is the agent name) | | Grok CLI | `grok-cli` | `~/.grok/user-settings.json` → `mcp.servers` (JSON ARRAY, keyed by `id`); hooks in the SAME file under top-level `hooks` (Claude nested-rule shape). USER-SCOPE ONLY — community superagent-ai/grok-cli (npm `grok-dev`, bin `grok`) | | Devin CLI (Cognition) | `devin` | `~/.config/devin/config.json` (user; `%APPDATA%\devin\config.json` on Windows) / `.devin/config.json` (project) → `mcpServers` (object map; native `${env:VAR}`); hooks under the SAME file's `hooks` key (Claude-compatible NESTED-rule shape; reply is the simple top-level `{decision:"approve"\|"block"\|"deny", reason}`, exit 2 blocks) | ### `mcp-only` — MCP registration only, no hook layer (12) Detection surfaces "hooks unavailable here." | Platform | id | MCP native target | |---|---|---| | Warp | `warp` | `.warp` → `mcp` | | Roo Code | `roo-code` | mcp config | | Cline | `cline` | globalStorage saoudrizwan.claude-dev/settings/cline_mcp_settings.json mcpServers | | Trae | `trae` | mcp config | | Zed | `zed` | host config | | Codebuff | `codebuff` | `mcp.json` → `mcpServers` | | Mux | `mux` | mcp config | | Pi | `pi` | (telemetry/skills surface; no writable MCP hook config) | | Windsurf | `windsurf` | `~/.codeium/windsurf/mcp_config.json` (user/global ONLY) → `mcpServers` (object map; stdio `{ command, args?, env? }`, remote `{ serverUrl, headers? }` — `serverUrl` not `url`) | | Open Interpreter | `open-interpreter` | `~/.openinterpreter/config.toml` (`$INTERPRETER_HOME`; NOT `$CODEX_HOME`) → `[mcp_servers.*]` TOML table (stdio `{ command, args, env }`, streamable-HTTP `{ url, bearer_token_env_var?, http_headers? }`). The new Rust `interpreter`/`i` CLI — a fork of OpenAI's Codex | | Junie | `junie` | JetBrains' OWN agent (distinct from jetbrains-copilot) — `~/.junie/mcp/mcp.json` (user) / `.junie/mcp/mcp.json` (project) → `mcpServers` (object map; stdio `{ command, args?, env? }`, remote `{ url, headers? }` — `url` not `serverUrl`; no type/disabled) | | Mistral Vibe | `mistral-vibe` | `.vibe/config.toml` (project, precedence) / `~/.vibe/config.toml` (user) → `[[mcp_servers]]` (TOML ARRAY-OF-TABLES, keyed by `name`; stdio `{ name, transport:"stdio", command, args?, env? }`, remote `{ name, transport:"http"\|"streamable-http", url, headers? }` — distinct from codex's table-keyed `[mcp_servers.]`) | ### `ts-plugin` — framework-generated bridge module (8) | Platform | id | Mechanism | |---|---|---| | OpenCode | `opencode` | generated exported plugin module importing your handler | | MiMoCode | `mimo-code` | OpenCode fork (Xiaomi @mimo-ai/cli, bin `mimo`); generated plugin module in `~/.config/mimocode/plugin/` (MCP root key `mcp` in `mimocode.json`) | | Kilo CLI | `kilo-cli` | generated `@kilocode/plugin` module registered in kilo.jsonc's `plugin` array | | Kilo Code | `kilo` | generated plugin module in `.kilo/plugin/` (7.x extension shares the Kilo CLI backend) | | OMP | `omp` | generated plugin module | | NemoClaw | `nemoclaw` | NVIDIA OpenClaw wrapper/fork; generated plugin module + DUAL REGISTRATION in the wrapped `~/.openclaw/openclaw.json` (detected via `~/.nemoclaw/`) | | OpenClaw | `openclaw` | generated plugin module | | Amp | `amp` | generated TS plugin module in `.amp/plugins/.ts` (project scope; MCP in `settings.json` → `amp.mcpServers`) | (`PlatformId` also includes `synthetic` and `unknown` sentinels used internally.) --- ## 7. Extensibility contract Adding a platform is **one registry entry + one adapter** (the README's design guarantee). Concretely: - **Registry** (`src/adapters/registry.ts`): one `{ id, load: () => import(...) }` entry, lazily loaded. Order is load-bearing for runtime host detection. - **Adapter** (`src/adapters//index.ts`): a class (typically extending `BaseAdapter`) declaring `id`, `name`, `readonly paradigm`, a `capabilities` literal, `detect`, the MCP `installServer`/`uninstallServer`, hook install per paradigm (or inherit the `mcp-only` skip), optional content-surface writers (`installCommands`/`installSkills`/`installSubagents` + uninstall inverses; absent → `BaseAdapter` skips+warns), and `doctor` health checks. The escape hatch keeps the core thin: every adapter accepts `platforms..extra` passthrough for platform-exclusive features the core doesn't model — thin universal core, fat per-adapter tail. --- ## 8. Quick start & development The quick start **forks by audience** (see §0). Pick your track. ### Track A — MCP developer: deploy MY MCP everywhere + measure my own per-tool tokens ```bash # agent-connector is an SDK you depend on — no global install required npm install @ken-jo/agent-connector # inside the package that holds your connector cd my-mcp-project # package.json exposes your bin, e.g. "acme-db" acme-db detect acme-db install --dry-run acme-db install # touches DETECTED hosts (or explicit --targets) acme-db doctor --probe acme-db telemetry report --by tool # YOUR wrapped server's per-tool bytes ``` Use `npx @ken-jo/agent-connector ... --connector ./agent-connector.config.mjs` only as a local framework-development/debug fallback. Published MCP packages should expose their own bin and let that branded command drive install, doctor, upgrade, telemetry, and uninstall. `install` targets the hosts actually detected on the machine (or your `--targets` / `connector.targets` list), intersected with the registered deploy adapters — there is no "install to all unconditionally" path. Per-tool telemetry is automatic for **stdio** servers only; remote servers are registered but not wrapped. ### Track B — agent-CLI end user: see the token usage of the agent CLIs I use No `defineConnector`, no install, no config file. Run it straight from npx — it reads your local agent-CLI logs read-only and never writes host config: ```bash npx @ken-jo/agent-connector usage report # totals by platform (default) npx @ken-jo/agent-connector usage leaderboard --by platform # which CLI cost the most npx @ken-jo/agent-connector usage report --by model --since 7d npx @ken-jo/agent-connector usage export --format csv --out usage.csv ``` `usage` reports **whole-conversation totals** grouped by `platform | project | session | model | day` — counts only, never your prompts or results. It does NOT break down cost per MCP server or per tool (agent CLIs don't log per-tool attribution — §5); per-MCP / per-tool numbers require an MCP to be deployed and wrapped via a connector (Track A). A few readers are host-estimated (shown in the `CONFIDENCE` column), and 5 "synced" platforms — `cursor`, `antigravity`, `antigravity-cli`, `trae`, `warp` — are skipped ("requires sync") unless a local cache already exists. ### From source ```bash npm install npm run typecheck npm test npm run build npm run dev -- detect # run the CLI from source via tsx ``` Or ship a branded CLI: `createConnectorCli({ packageJson, connector })` from `agent-connector/cli` keeps two layers separate: `packageJson` derives the public bin/version/name/mcpName, while `connector` points at the behavior file. It exposes every subcommand under your own bin and auto-scopes to your connector (see `examples/branded-cli`); explicit `--connector`/`--connector-id` always overrides. **Package subpaths:** `.` (root — full `define*` family: `defineConnector` + `defineStatusline` + `defineAction` + `defineHook` / `defineCommand` / `defineSkill` / `defineSubagent` / `defineMemory` / `defineConfigPatch` / `defineNativeHook`; public types including `TelemetryAccessor` / `TelemetryUsageSummary`; `ConnectorConfigError`) · `./runtime` (serve-proxy + hook entrypoint internals) · `./cli` (`createConnectorCli`) · `./sdk` (same full `define*` family + introspection helpers `capabilitiesOf` / `hostsSupporting` / `surfaceSupport` / `SURFACE_PREDICATES` + the test harness export; additive, does NOT replace the root export) · `./sdk/test` (offline harness — `explain` + `simulate`; see §9). Engines: Node `>=18.17`, ESM only. Runtime deps are pure-JS / WASM (`gpt-tokenizer`, `sql.js`, `fzstd`, `@iarna/toml`, `yaml`) — no native build. License: Apache-2.0 © KenJo. --- ## 9. Connector SDK — `@ken-jo/agent-connector/sdk` + `/sdk/test` > "Author with the typed `define*` family, then `explain`/`simulate` your connector > against every host's real capabilities — offline." The framework already normalizes surfaces and capability-gates per host. The SDK turns that knowledge into a **queryable, offline** answer to "does my handler / HUD actually work on host X?" — before you touch a real host. ### 9.1 `/sdk` — authoring surface ```ts import { defineConnector, // existing — validates + returns ResolvedConnector defineStatusline, // existing typed helper defineAction, // typed identity helper for ActionDef (see §2.6) defineCommand, defineSkill, defineSubagent, defineMemory, defineConfigPatch, defineNativeHook, defineHook, // event-parameterized (see below) capabilitiesOf, hostsSupporting, surfaceSupport, SURFACE_PREDICATES, ConnectorConfigError, } from "@ken-jo/agent-connector/sdk"; ``` All `define*` helpers are **typed identity functions** — each takes and returns its corresponding `*Def`, giving type inference and a single import site. Validation stays in `defineConnector`; the helpers add no runtime behavior. **`defineHook` is event-parameterized** so the handler payload narrows to the concrete event type rather than the full union: ```ts const onPre = defineHook("PreToolUse", { handler(evt) { // evt is typed as PreToolUseEvent — evt.toolName is known return { decision: "deny", reason: "no" }; }, }); // pass onPre as hooks.PreToolUse in defineConnector({ hooks: { PreToolUse: onPre } }) ``` The leading event string is used only for type inference; `defineHook` returns the def unchanged. ### 9.2 Introspection helpers (all async — adapters load lazily) | Helper | Signature | Returns | |---|---|---| | `capabilitiesOf` | `(host: PlatformId) => Promise` | Capability flags for the host; `undefined` for an unknown id. | | `hostsSupporting` | `(surface: SurfaceName) => Promise` | All registered hosts that honor the surface. | | `surfaceSupport` | `(host: PlatformId, surface: SurfaceName) => Promise` | Convenience: single host × surface check. | | `SURFACE_PREDICATES` | `Record boolean>` | The per-surface predicate map (exported for advanced use). | `SurfaceName` vocabulary: `server` · `hooks` · `commands` · `skills` · `subagents` · `memory` · `statusline` · `configPatch` · `nativeHooks` · `actions`. Examples: ```ts await hostsSupporting("statusline"); // → command-driven statusline hosts await hostsSupporting("memory"); // → the broad AGENTS.md set await surfaceSupport("codex", "hooks"); // → true ``` ### 9.3 `/sdk/test` — offline harness ```ts import { explain, simulate } from "@ken-jo/agent-connector/sdk/test"; ``` #### `explain(connector)` — per-host × per-surface support matrix ```ts explain(connector: ResolvedConnector): Promise interface ExplainRow { host: PlatformId; surface: SurfaceName; support: "native" | "skip-warn" | "disabled"; reason: string; } ``` Returns one row per `(host, surface)` pair for **every surface the connector actually declares** (un-declared surfaces are omitted). `configPatch` and `nativeHooks` rows are scoped to the host that declares them. `disabled` means the connector explicitly set `platforms[host]. = false`. The `server` row reason notes that registration may be host-managed on some IDEs (v1 capability-based check). Use this as the full readout before install: ```ts const rows = await explain(myConnector); rows.forEach(r => console.log(r.host, r.surface, r.support, r.reason)); ``` #### `simulate(connector, opts)` — behavioral check against the real adapter chain ```ts simulate( connector: ResolvedConnector, opts: { surface: "hooks" | "statusline"; host: PlatformId; event?: HookEventName; // required when surface === "hooks" input: unknown; // host-shaped raw payload } ): Promise<{ honored: boolean; hostReply?: unknown; reason: string }> ``` Runs the **real** adapter parse → handler → format chain offline and reports whether the host would actually honor the handler's decision. It mirrors the runtime exactly: tolerant stdin parsing, matcher filtering (a matcher-scoped handler that doesn't match is not run), and a verdict computed by **parsing the host reply and judging the actual `(event, decision)` contract** — not substring guessing. Concrete honored/dropped/degraded cases: | Scenario | `honored` | `reason` (summary) | |---|---|---| | codex `UserPromptSubmit` — context drop (no stdout path) | `false` | codex drops `context` on UserPromptSubmit | | `deny` on `Stop` / `SubagentStop` | `true` | continues subagent / session (persistence) | | `deny` on `SubagentStart` / `PostToolUseFailure` | `false` | degrades to context note — nothing is blockable here | | `PermissionRequest` `ask` | `true` | host's native dialog handles the ask (no stdout needed) | | matcher-scoped handler that doesn't match `input` | `false` | matcher excluded this input — handler not run | ```ts const { honored, reason } = await simulate(myConnector, { surface: "hooks", host: "codex", event: "UserPromptSubmit", input: { prompt: "hello", session_id: "s1" }, }); // honored: false — codex drops context on UserPromptSubmit ``` `simulate` is the behavioral complement to `explain`: `explain` is the whole support matrix; `simulate` is the per-invocation contract check that encodes each host's real honor / drop / degrade behavior. --- ## Links - README.md — overview + quick start. - docs/ARCHITECTURE.md — the authoritative design. - llms.txt — concise index. - examples/acme-db/agent-connector.config.mjs — runnable example. - skills/agent-connector/SKILL.md — agent-facing how-to (deployable via the skills surface).