# 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/<id>/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 <platform> <event> --connector <id>`; a
  wrapped MCP entry runs `agent-connector serve --connector <id> -- <real cmd>`).
  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 <path>`. 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<Record<PlatformId, PlatformOverride>>` | `{}` | 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<string, string>;     // values support ${env:VAR} / ${env:VAR:-default}
  cwd?: string;
  // remote (http | sse | ws) transport:
  url?: string;                     // required for remote
  headers?: Record<string, string>;
  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<E> {
  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<HookResponse | void>;
}
```

`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<string, unknown>` |
| `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<string, unknown>;   // 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 <platform> <event> --connector <id>`)
  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.<id>.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<string, NativeHookDef>;  // key = HOST event name, VERBATIM

interface NativeHookDef {
  matcher?: string;   // host-native matcher, written verbatim — the HOST filters
  handler(evt: NativeHookEvent): unknown | Promise<unknown>;
}

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 <id>`), 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<string, unknown>; // 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<string, string>;      // relpath → contents, bundled beside SKILL.md
  extra?: Record<string, unknown>;
}

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<string, unknown>;
}

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 (<connectorId>/<name>) — 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/<n>.md` (md+fm) | `.claude/skills/<n>/SKILL.md` | `.claude/agents/<n>.md` (md+fm) |
| gemini-cli | `.gemini/commands/<n>.toml` | `.gemini/skills/<n>/SKILL.md` | `.gemini/agents/<n>.md` (md+fm) |
| qwen-code | `.qwen/commands/<n>.toml` | — | `.qwen/agents/<n>.md` (md+fm) |
| vscode-copilot (+ jetbrains alias) | `.github/prompts/<n>.prompt.md` | `.github/skills/<n>/SKILL.md` | `.github/agents/<n>.agent.md` (vscode only — jetbrains skips+warns) |
| copilot-cli | — | `.github/skills/<n>/SKILL.md` | `~/.copilot/agents/<n>.agent.md` |
| cursor | `.cursor/commands/<n>.md` (body-only, no fm) | `.cursor/skills/<n>/SKILL.md` | `.cursor/agents/<n>.md` (md+fm) |
| codex | `~/.codex/prompts/<n>.md` (user-only) | `.codex/skills/<n>/SKILL.md` | `.codex/agents/<n>.toml` (TOML) |
| opencode | `.opencode/commands/<n>.md` (md+fm) | `.opencode/skills/<n>/SKILL.md` | `.opencode/agent/<n>.md` (singular dir) |
| kilo | `.kilocode/commands/<n>.md` (md+fm) | — | `.kilocode/agents/<n>.md` (md+fm) |
| pi | — | `.pi/skills/<n>/SKILL.md` | — |
| antigravity (+ antigravity-cli) | `.agent/workflows/<n>.md` (project; user → `~/.gemini/antigravity/global_workflows/<n>.md`) | `.agents/skills/<n>/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**:

```
<!-- agent-connector:begin <connectorId>/<name> hash=<sha256-12> -->
<!-- Managed by agent-connector for "<connectorId>". Do not edit between these markers: run your connector package's upgrade/sync command to rewrite this block; run its uninstall command to remove it (framework fallback: `agent-connector uninstall <connectorId>`). -->
…content…
<!-- agent-connector:end <connectorId>/<name> -->
```

Marker + edit-detection semantics:
- The blockId (`<connectorId>/<name>`) 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/<name>` 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 →
`<projectDir>/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 `<projectDir>/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 `<projectDir>/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 `<projectDir>/.clinerules/agent-connector.md`,
amazon-q `<projectDir>/.amazonq/rules/agent-connector.md` (plain Markdown
auto-applied as context — AWS context-project-rules docs), continue
`<projectDir>/.continue/rules/agent-connector.md` leading with `alwaysApply: true`
frontmatter ("always included" — Continue rules docs), and windsurf
`<projectDir>/.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.<id>.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
`<connectorId>/` 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<string>;
  hosts?: Partial<Record<PlatformId, {
    render?: (ctx: StatuslineContext) => string | Promise<string>;
    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": "<homeBin> statusline claude-code --connector <id>", "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 <platform> --connector <id>` (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<ActionResult | void>;
  hosts?: Partial<Record<PlatformId, {
    run?: (ctx: HostCtx) => ActionResult | void | Promise<ActionResult | void>;
    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 <platform> <actionId> --connector <id>` — 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.<id>` 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<HooksConfig>;   // false → no NORMALIZED hooks here; object → merge/replace
  nativeHooks?: Record<string, NativeHookDef>; // 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<ServerDef> | 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<string, unknown>;          // 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.<id>.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 <key>: <absent> → <value> (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 `<dataRoot>/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 <ids>` (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) / `<project>/.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 <id>; 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.<id>.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<Record<PlatformId, PlatformOverride>>;
  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 <package>`), local Node/process MCP (`node
<server-file>`), Python MCP (`uv run --with mcp <server.py>` by default, direct
`python <server.py>` only when the environment is managed elsewhere), CLI-based
MCP (`<mcp-cli> 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 ... -- <real
cmd>`) 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 <command> [flags]`. Run `agent-connector <command> --help` for
command-specific flags. `--help`/`-h`/`help` print usage; `--version`/`-v` prints
`<name> <version>`.

Shared flags: `--scope user|project` (default `user`); `--targets a,b,c`
(comma-separated `PlatformId` list); `--connector <path>` (explicit config);
`--project <dir>` (defaults to cwd); `--dry-run`; `--json` (where noted).

### detect
`agent-connector detect [--project <dir>] [--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 <path>] [--project <dir>] [--dry-run] [--force]`
`--method marketplace` drives the HOST's own plugin flow instead of writing
config directly. Drivable hosts (11, a `MarketplaceDriver` per host, 3 shapes):
`claude-code`, `codex`, `copilot-cli`, `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, copilot-cli, droid:
stage under `<dataRoot>/marketplace/<host>/`, regenerate the shared local
catalog, register once by reference (`<host> plugin marketplace add`), then the
install verb (`claude|copilot plugin install` / `codex|droid plugin add|install`
`<id>@agent-connector`). (2) DIRECT — agy, gemini-cli, qwen-code: install-by-path
(`agy plugin install <dir>` / `gemini extensions install <dir> --consent` /
`qwen extensions install <dir>`) + the host's uninstall verb, no marketplace
registration. (3) NPM-LOCAL — opencode, kilo, kilo-cli: `<host> plugin --global
file://<stagedDir>` 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: `<dataRoot>/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@<dist>` 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 <id>] [--connector <path>] [--scope …] [--targets …] [--project <dir>] [--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/<id>/` under the data-root) and,
when no connectors remain, the shared home-bin launcher.

### doctor
`agent-connector doctor [--targets …] [--connector <path>] [--scope …] [--project <dir>] [--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 <path>] [--scope user|project] [--project <dir>] [--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 <path>] [--format <fmt>|all] [--out <dir>] [--project <dir>] [--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
`<cwd>/dist-plugin`), printing the emitted file tree + per-format install
instructions. Host formats (default `agent-plugin`; `--format all` emits all 9):
agent-plugin · claude-plugin · factory-plugin · gemini-extension ·
qwen-extension · agy-plugin · cursor-plugin · kimi-plugin · npm-plugin.
`agent-plugin` is the portable Agent Plugins 1.0.0 package
(https://agent-plugins.org — Vercel-led, co-maintained with AWS, Cursor,
GitHub, Microsoft, OpenAI) and the SINGLE SOURCE OF TRUTH for every host that
speaks the spec — Codex, GitHub Copilot CLI, VS Code / JetBrains Copilot, Kiro,
Hermes, Cursor, OpenClaw: root `plugin.json` (+ `$schema`, `extensions`),
`mcp.json` (stdio and remote streamable-http/sse servers), `skills/<n>/SKILL.md`,
plus hooks / commands / subagents under per-client extension namespaces —
`com.github.copilot/` (Copilot CLI + VS Code + JetBrains) and `com.openai/`
(Codex, pointed at by `extensions["com.openai"].hooks`); clients ignore
namespaces they do not own. It embeds NO absolute path in its portable
surfaces: the MCP serve-wrapper and Copilot hooks run `node
${PLUGIN_ROOT}/bin/agent-connector.mjs`, a bundled launcher that resolves the
runtime at run time (home binary → PATH → `npx @ken-jo/agent-connector`), so no
global install is required. The codex and copilot-cli marketplace drivers stage
this exact bundle (`install --method marketplace --targets codex,copilot-cli`;
live-verified on codex 0.149 + GitHub Copilot CLI 1.0.80). The retired
`codex-plugin` / `copilot-plugin` names still parse and resolve to agent-plugin
with a deprecation line. 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 <report|export|leaderboard> [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 <Nd|Nh|Nm|Ns> --connector <id> [--json]`
  → ranked footprint table; `--json` emits the rows.
- `export --format csv|json (default json) --out <file> --since … --connector <id>`
  → raw aggregate records (stdout, or to `--out`).
- `leaderboard --by mcp|tool|surface (default mcp) --since … --connector <id> --scope <slice> [--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 <report|export|leaderboard> [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 <id> [--json]`
  → aggregated table; prints skip notes for platforms requiring a sync.
- `export --format csv|json --out <file> --since … --platform <id>` → deduped records.
- `leaderboard --by platform|model (default platform) --since … --platform <id> [--json]`
  → the host/user leaderboard ("which CLI/host spent the most").

### leaderboard (unified)
`agent-connector leaderboard [--since <window>] [--scope <slice>] [--connector <id>] [--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 <platform> <event> --connector <id>` — 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 <id> [--scope user|project] [--host <platformId>] -- <command> [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 <platform> --connector <id>` — 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 <platform> <actionId> --connector <id>` — 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=<id>` (override install-time host detection) ·
`AGENT_CONNECTOR_HOST=<id>` (override runtime host detection in the hook entrypoint) ·
`AGENT_CONNECTOR_SESSION=<id>` (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/<name>.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.<name>]`) |

### `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/<id>.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/<id>/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.<id>.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<PlatformCapabilities \| undefined>` | Capability flags for the host; `undefined` for an unknown id. |
| `hostsSupporting` | `(surface: SurfaceName) => Promise<PlatformId[]>` | All registered hosts that honor the surface. |
| `surfaceSupport` | `(host: PlatformId, surface: SurfaceName) => Promise<boolean>` | Convenience: single host × surface check. |
| `SURFACE_PREDICATES` | `Record<SurfaceName, (caps: PlatformCapabilities) => 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<ExplainRow[]>

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].<surface> = 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).
