@hypabolic/crossbar 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,168 @@
1
+ # Crossbar — Architecture & Frozen Contract (Phase 1)
2
+
3
+ **Status:** Interface frozen pending sign-off · **Date:** 2026-06-21 · Companion to `RESEARCH.md`,
4
+ `CAPABILITY-MATRIX.md`. The contract under `src/core/` is the **single source of truth**. Phase 2
5
+ fan-out targets it and nothing else. Typechecked against Pi `0.79.9` (`tsc --noEmit` → clean).
6
+
7
+ ```
8
+ discovery engine ──┐
9
+ ├─► server registry ──► provider shim ──► pi.registerProvider(...)
10
+ adapters[kind] ────┘ │ │
11
+ (BackendAdapter) └─► crossbar.json (metadata) └─► models appear in /model & /login
12
+ secrets ─► Pi authStorage (auth.json,0600)
13
+ onboarding overlay ─────────────────────────────────────────────────► /crossbar (alias /local)
14
+ loaded-model widget ─────────────────────────────────────────────────► ctx.ui.setStatus / setWidget
15
+ ```
16
+
17
+ ## 1. Module layout (Phase 2 fills these)
18
+
19
+ ```
20
+ src/
21
+ index.ts # extension entry: registers /crossbar, wires session_start (STUB today)
22
+ core/ # FROZEN CONTRACT — do not change without bumping CONTRACT_VERSION
23
+ capability.ts # Capability enum, AuthMode, BackendKind
24
+ types.ts # Probe, DiscoveredServer, ModelDescriptor, LoadedState, ServerRecord, PiModelEntry
25
+ backend-adapter.ts # BackendAdapter interface + capability guards
26
+ index.ts # re-exports
27
+ adapters/ # one file per kind, each `implements BackendAdapter`
28
+ ollama.ts lmstudio.ts llamacpp.ts llamaswap.ts vllm.ts
29
+ openai.ts anthropic.ts generic.ts # generic = openai-generic fallback
30
+ index.ts # ADAPTERS registry (kind → instance), probe-order export
31
+ discovery/
32
+ probe.ts # Probe impl: fetch + timeout + auth-header injection + redaction
33
+ engine.ts # port sweep (localhost default), fingerprint dispatch, dedupe by origin
34
+ registry/
35
+ registry.ts # ServerRecord CRUD, health poll, model cache, in-memory state
36
+ persistence.ts # crossbar.json read/write via getAgentDir(); secrets via authStorage
37
+ ids.ts # stable provider-id generation (kind + host + port)
38
+ shim/
39
+ provider-shim.ts # ServerRecord + ModelDescriptor[] → pi.registerProvider / unregisterProvider
40
+ ui/
41
+ onboarding.ts # ctx.ui.custom overlay: discovered list + manual add + test + pick + save
42
+ loaded-widget.ts # setStatus/setWidget live "currently loaded" indicator
43
+ theme.ts # getSelectListTheme wrapper + token helpers
44
+ tests/
45
+ conformance/ # runs EVERY adapter against the contract using fixtures
46
+ fixtures/<kind>/ # captured HTTP responses per backend (+ edge cases)
47
+ ```
48
+
49
+ ## 2. BackendAdapter contract (frozen)
50
+
51
+ See `src/core/backend-adapter.ts`. Invariants every adapter MUST honour — the conformance suite checks
52
+ each one:
53
+
54
+ 1. **Stateless & I/O-free.** No instance state; never call `fetch` directly — only the injected `Probe`.
55
+ 2. **Honest capabilities.** An optional method (`introspectLoaded`/`switchModel`/`loadUnload`/`health`)
56
+ is defined **iff** the matching `Capability` is in `capabilities`. Orchestrator uses the `canX`
57
+ guards, never feature-sniffs.
58
+ 3. **`fingerprint` is unauth & cheap.** Uses only public metadata endpoints; returns `null` fast for
59
+ non-matches; sets a calibrated `confidence` (exact unique-path match → ~1.0; generic `/v1/models`
60
+ shape → ~0.3). Cloud adapters return `null`.
61
+ 4. **`listModels` filters embeddings** out of chat registration (keeps them only if flagged).
62
+ 5. **`toPiModel` owns Pi mapping.** Sets `api`, `compat` quirk flags, `cost` zeros for local, and
63
+ conservative defaults: `contextWindow` from backend or a safe fallback, `input` defaults `["text"]`.
64
+ 6. **`switchModel` confirms or throws.** Must surface server-down-mid-switch and model-not-available as
65
+ rejections, not silent success.
66
+ 7. **No secret leakage.** Adapters receive `ServerCredential` but must never log/serialize `apiKey`.
67
+
68
+ `CONTRACT_VERSION = 1`. Any breaking change bumps it; registry asserts adapters match.
69
+
70
+ ## 3. Provider-registration shim (`shim/provider-shim.ts`)
71
+
72
+ The ONLY bridge to Pi. For each enabled, healthy server:
73
+
74
+ ```
75
+ register(record):
76
+ adapter = ADAPTERS[record.kind]
77
+ cred = resolveCredential(record) # apiKey from authStorage, or {mode:"none"}
78
+ models = registry.cachedModels(record) # from listModels, refreshed on poll
79
+ pi.registerProvider(record.id, {
80
+ name: record.label,
81
+ baseUrl: adapter.inferenceBaseUrl(server),
82
+ apiKey: cred.mode === "apiKey" ? `$${ENV_FOR(record.id)}` : undefined, # see secret note
83
+ api: adapter.piApi,
84
+ models: models.map(m => adapter.toPiModel(server, m)),
85
+ })
86
+ ```
87
+
88
+ - Re-registration on model-list change = `unregisterProvider(id)` then `registerProvider(id, ...)`.
89
+ - Switching the served model updates the registered `models[]` (and the loaded widget), then
90
+ optionally `pi.setModel(...)` to point the agent at it.
91
+ - **Secret handling:** keys are stored via `authStorage.set(record.id, {type:"api_key", key})`. The shim
92
+ passes the key to Pi using Pi's own indirection (env/`!cmd`/literal) — never inlining the plaintext
93
+ into long-lived structures. Exact mechanism (env handoff vs literal at register time) is the first
94
+ thing Phase 2 verifies against `auth-storage.ts`; default to letting Pi resolve from `auth.json`.
95
+
96
+ ## 4. Server registry & persistence
97
+
98
+ - **State:** `ServerRecord[]` in memory; mirror to `getAgentDir()/crossbar.json` (`CrossbarConfigFile`,
99
+ `version:1`). Non-secret only — never write `apiKey` here.
100
+ - **Secrets:** `ctx.modelRegistry.authStorage` (`auth.json`, `0600`), keyed by `record.id`.
101
+ - **IDs:** stable, derived from `kind + host + port` (e.g. `crossbar-ollama-localhost-11434`) so a server
102
+ keeps its id and key across restarts. (`registry/ids.ts`)
103
+ - **Health poll:** interval loop started in `session_start`, stopped in `session_shutdown` (per Pi's
104
+ long-lived-resource rule). Updates health, refreshes model cache, drives the loaded widget. On
105
+ `unreachable`, keep the registration but mark degraded; show last-known models/loaded.
106
+ - **Lifecycle:** `session_start` → load `crossbar.json` → (if enabled) run auto-discovery →
107
+ register all enabled+reachable servers → start poll. `session_shutdown` → stop poll.
108
+
109
+ ## 5. Discovery engine (`discovery/`)
110
+
111
+ - **Default scope: localhost only.** Probe ports `[11434,1234,8080,8000,5000,5001,1337]` on `127.0.0.1`.
112
+ LAN host-range probing is opt-in via `CrossbarSettings.lanDiscovery` (no mDNS exists for any backend).
113
+ - **Per origin:** run the probe-order fingerprint chain (CAPABILITY-MATRIX §"probe order"); pick the
114
+ highest-confidence adapter; fall back to `openai-generic` when only `/v1/models` matches.
115
+ - **Short timeouts** (e.g. 600ms) and bounded concurrency; a refused port returns `status:0` fast.
116
+ - **Key-vs-no-auth:** probe public metadata first; a `401` on `/v1/chat/completions` with `200` on
117
+ `/v1/models` ⇒ "running but keyed" → onboarding prompts for a key.
118
+
119
+ ## 6. Onboarding flow (`ui/onboarding.ts`) — `/crossbar`
120
+
121
+ Overlay via `ctx.ui.custom<T>(factory, { overlay:true, overlayOptions })` using `SelectList` +
122
+ `Container`/`Text`/`DynamicBorder`, themed with `getSelectListTheme` / `theme.fg`:
123
+
124
+ ```
125
+ /crossbar →
126
+ [Discovered] Ollama (localhost:11434) ✓ healthy
127
+ LM Studio (localhost:1234) ✓ healthy
128
+ [Manual add] + Add server… → input URL → optional API key (no-auth toggle) → Test connection
129
+ → on select/add: fingerprint → health → listModels → pick default model → save (registry + auth.json)
130
+ ```
131
+
132
+ - Capability-driven: hide "switch model" when `!supports(SwitchModel)`; show load/unload only when
133
+ `LoadUnload`; show a no-auth toggle always, key entry only when needed.
134
+ - Test-connection uses the same `Probe` + adapter `health`/`listModels` as production.
135
+ - Cloud keys (OpenAI/Anthropic) can still be entered via stock `/login`; Crossbar's registered models
136
+ surface in `/model` regardless.
137
+
138
+ ## 7. Loaded-model widget (`ui/loaded-widget.ts`)
139
+
140
+ - `ctx.ui.setStatus("crossbar-loaded", theme.fg("accent", "● <server>:<model>"))`, refreshed from the
141
+ health poll's `introspectLoaded` and from the `model_select` event.
142
+ - When `!supports(IntrospectLoaded)` (OpenAI, Anthropic, vLLM, llamafile) → show **last-known**
143
+ selection from `ServerRecord.lastKnownLoaded`; never claim live state we can't read (`source` field).
144
+
145
+ ## 8. Conformance suite (`tests/conformance/`)
146
+
147
+ One parameterized suite runs against **every** adapter using a fake `Probe` backed by per-backend
148
+ fixtures. Required cases:
149
+
150
+ - fingerprint: positive, negative (other backend's response), ambiguous-port disambiguation.
151
+ - listModels: normal, empty, embeddings filtered, missing-caps defaults applied.
152
+ - introspectLoaded / switchModel / loadUnload: present ⇔ capability; success + edge cases —
153
+ **auth failure (401), server-down-mid-switch, model-not-loaded, streaming cutoff**.
154
+ - toPiModel: output validates against Pi's `ProviderConfig["models"]` element shape (compile + runtime).
155
+ - capability honesty: every optional method present ⇔ its `Capability` is declared.
156
+
157
+ Discovery validated on **zero / one / many** servers (Phase 3 hardening).
158
+
159
+ ## 9. Phase 2 waves (contract-driven fan-out — nothing starts before sign-off)
160
+
161
+ - **Wave A (parallel):** conformance harness + fixtures · discovery engine + probe · registry +
162
+ persistence + id gen.
163
+ - **Wave B (parallel, one adapter per agent):** ollama · lmstudio · llamacpp+llamaswap · vllm ·
164
+ openai+anthropic · generic. Each lands with its fixtures and passes conformance before merge.
165
+ - **Wave C:** provider shim + `/crossbar` onboarding overlay + loaded widget; full integration; green
166
+ conformance across all adapters.
167
+
168
+ After each wave: reconvene, integrate, run conformance against every adapter.
@@ -0,0 +1,49 @@
1
+ # Crossbar — Capability Matrix (draft, Phase 0)
2
+
3
+ **Date:** 2026-06-21 · companion to `RESEARCH.md`. Values: ✅ yes · ◐ partial/conditional · ❌ no.
4
+ Backend endpoints are **[WEB]** — confirm live per adapter. `pi api` = which built-in Pi API type the
5
+ adapter registers under (`oai` = `openai-completions`, `ant` = `anthropic-messages`).
6
+
7
+ | Backend | port | pi api | listModels | introspectLoaded | switchModel | loadUnload | auth | health | perModelCaps | streaming | discovery fingerprint |
8
+ |---|---|---|---|---|---|---|---|---|---|---|---|
9
+ | **Ollama** | 11434 | oai | ✅ `/api/tags`,`/v1/models` | ✅ `/api/ps` | ✅ implicit (request id) | ✅ `keep_alive:0` | ◐ none local | ✅ `GET /` text | ✅ `/api/show` caps + ctx | ✅ | `GET /` → `Ollama is running` |
10
+ | **LM Studio** | 1234 | oai | ✅ `/api/v0/models`,`/v1/models` | ✅ `state` field | ✅ JIT + `/api/v1/models/load` | ✅ load/unload + `lms` | ◐ Bearer, none default | ◐ infer 200 | ✅ type+`max_context_length` | ✅ | `/api/v0/models` w/ `state`,`compatibility_type` |
11
+ | **llama-server** | 8080 | oai | ✅ `/v1/models` | ◐ `/props`,`/slots` (single) | ❌ (1/instance) | ❌ classic | ◐ none / `--api-key` | ✅ `/health` | ◐ ctx via `/props`,`meta` | ✅ | `/props` w/ `default_generation_settings`+`build_info` |
12
+ | **llama-swap** | 8080 | oai/ant | ✅ `/v1/models` (all config) | ✅ `/running` | ✅ via `model` → restart upstream | ✅ `/api/models/unload`, ttl | ◐ optional multi-scheme | ✅ `/health`→OK | ◐ via upstream | ✅ | `/` → `/ui/`; `/running`,`/upstream/{model}` |
13
+ | **vLLM** | 8000 | oai | ✅ `/v1/models` | ◐ `/is_sleeping` (dev) | ❌ base · ◐ LoRA | ◐ sleep/wake + LoRA | ◐ none / `--api-key` | ✅ `/health` | ◐ `max_model_len` only | ✅ | `/version` + `/metrics` `vllm:` + `owned_by:"vllm"` |
14
+ | **OpenAI** | cloud | oai | ✅ `/v1/models` | ❌ | ✅ (pick id) | ❌ managed | ✅ Bearer | ❌ (status page) | ❌ (static table needed) | ✅ | n/a (configured, not probed) |
15
+ | **Anthropic** | cloud | ant | ✅ `/v1/models` | ❌ | ✅ (pick id) | ❌ managed | ✅ x-api-key+version | ❌ | ✅ caps + `max_input_tokens` | ✅ | n/a |
16
+ | **TabbyAPI** | 5000 | oai | ✅ `/v1/model/list` | ✅ `/v1/model` | ✅ load | ✅ `/v1/model/{load,unload}` | ✅ x-api-key/x-admin-key | ◐ | ◐ | ✅ | `/v1/model/*` + `x-admin-key` |
17
+ | **KoboldCpp** | 5001 | oai | ✅ `/v1/models` | ✅ `/api/v1/model` | ❌ (1 GGUF) | ❌ | ◐ `--password` | ✅ `/api/extra/version` | ◐ | ✅ | `/api/extra/version`→`{"result":"KoboldCpp"}` |
18
+ | **oobabooga** | 5000 | oai | ✅ `/v1/models` | ✅ `/v1/internal/model/info` | ✅ load | ✅ `/v1/internal/model/{load,unload}` | ◐ `--api-key` | ◐ | ◐ | ✅ | `/v1/internal/*` namespace |
19
+ | **Jan** | 1337 | oai | ✅ `/v1/models` | ◐ | ◐ engine | ◐ engine | ◐ Bearer | ❌ | ◐ | ✅ | weak (log line) |
20
+ | **llamafile** | 8080 | oai | ✅ `/v1/models` | ◐ `/props` | ❌ | ❌ | ◐ `--api-key` | ✅ `/health` | ◐ via `/props` | ✅ | `/props` w/ non-`bNNNN` build_info |
21
+ | **generic OpenAI-compat** | varies | oai | ✅ `/v1/models` | ❌ | ❌ | ❌ | ◐ optional Bearer | ◐ | ◐ | ✅ | anything serving `/v1/models` (fallback) |
22
+
23
+ ## Capability-driven UX rules (derived)
24
+
25
+ - **switchModel ❌** (llama-server, vLLM-base, KoboldCpp, llamafile) → hide/disable the "switch model"
26
+ action; offer "restart server with model X" guidance only where applicable. Suggest llama-swap when a
27
+ bare llama-server is detected (it unlocks switching).
28
+ - **introspectLoaded ❌/◐** (OpenAI, Anthropic, vLLM, llamafile) → show **last-known** selected model
29
+ rather than a live "loaded" indicator; never claim live state we can't read.
30
+ - **perModelCaps ❌** (OpenAI) / ◐ (most local) → fall back to a maintained static capability table and
31
+ conservative defaults (`contextWindow` from `/props`/`max_*` when present, else a safe default).
32
+ - **auth ◐** → onboarding offers a no-auth toggle; probe public metadata endpoints first, only require a
33
+ key for inference (a `401` on `/v1/chat/completions` but `200` on `/v1/models` ⇒ "running but keyed").
34
+ - **loadUnload ✅** (Ollama, LM Studio, TabbyAPI, oobabooga, llama-swap) → expose explicit load/unload;
35
+ elsewhere degrade to implicit-on-use (Ollama) or nothing.
36
+
37
+ ## Discovery probe order (cheapest/most-specific first)
38
+
39
+ 1. `GET /` → `Ollama is running` ⇒ Ollama · redirect `/ui/` ⇒ llama-swap
40
+ 2. `GET /api/extra/version` → `{"result":"KoboldCpp"}` ⇒ KoboldCpp
41
+ 3. `GET /api/v0/models` 200 w/ `state`/`compatibility_type` ⇒ LM Studio
42
+ 4. `GET /props` w/ `default_generation_settings`+`build_info` ⇒ llama-server / llamafile
43
+ 5. `GET /version` + `/metrics` `vllm:` ⇒ vLLM
44
+ 6. `GET /v1/models` shape: `owned_by:"vllm"`⇒vLLM · `meta.n_ctx_train`⇒llama.cpp ·
45
+ multiple models on :8080⇒llama-swap · `/v1/internal/*`⇒oobabooga · `/v1/model/*`⇒TabbyAPI ·
46
+ else ⇒ generic OpenAI-compat
47
+
48
+ Default probe ports (localhost): `11434, 1234, 8080, 8000, 5000, 5001, 1337`. **No mDNS exists for any
49
+ backend** → LAN discovery, if enabled, is active port-probing across host IPs (opt-in).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hypabolic
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # Crossbar
2
+
3
+ [![CI](https://github.com/Hypabolic/Crossbar/actions/workflows/ci.yml/badge.svg)](https://github.com/Hypabolic/Crossbar/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/@hypabolic/crossbar)](https://www.npmjs.com/package/@hypabolic/crossbar)
5
+
6
+ **The local/self-hosted inference connector Pi should have shipped with.**
7
+
8
+ Crossbar is an extension for the [Pi coding agent](https://github.com/earendil-works/pi) that makes
9
+ wiring Pi to *any* local or self-hosted model backend effortless — zero hand-edited JSON, all setup
10
+ inside Pi's TUI, with auto-discovery, multi-server support, live "currently loaded" indicators, and
11
+ in-place model switching.
12
+
13
+ > Built by [Hypabolic](https://github.com/hypabolic).
14
+
15
+ ---
16
+
17
+ ## Why Crossbar
18
+
19
+ Existing connectors stop short: manual config files, a single server at a time, Ollama-only discovery,
20
+ or hardcoded model lists. Crossbar beats them on three axes:
21
+
22
+ 1. **Widest backend support** — Ollama, LM Studio, llama.cpp, llama-swap, vLLM, OpenAI, Anthropic, plus
23
+ a generic adapter for the OpenAI-compatible long tail (TabbyAPI, KoboldCpp, text-generation-webui,
24
+ Jan, llamafile, …).
25
+ 2. **Easiest onboarding** — `/crossbar` auto-discovers running servers on localhost and registers them.
26
+ No JSON. API-key *and* no-auth endpoints, with a connection test before you commit.
27
+ 3. **Highest-fidelity UX** — multiple simultaneous servers, a live loaded-model widget, and capability-
28
+ driven model switching that gracefully hides what a backend can't do.
29
+
30
+ ## Supported backends
31
+
32
+ | Backend | Discover | Loaded indicator | Switch model | Load/Unload | Auth |
33
+ |---|:--:|:--:|:--:|:--:|---|
34
+ | **Ollama** | ✅ | ✅ live | ✅ implicit | ✅ | none (local) |
35
+ | **LM Studio** | ✅ | ✅ live | ✅ JIT | ✅ | optional key |
36
+ | **llama.cpp** (`llama-server`) | ✅ | ✅ (single) | ❌¹ | ❌ | optional key |
37
+ | **llama-swap** | ✅ | ✅ live | ✅ proxy swap | ✅ | optional key |
38
+ | **vLLM** | ✅ | last-known | ❌¹ | ❌ | optional key |
39
+ | **OpenAI** | configured | — | pick model | — | API key |
40
+ | **Anthropic** | configured | last-known | pick model | — | API key |
41
+ | **Generic OpenAI-compatible** | ✅ (fallback) | last-known | ❌ | ❌ | optional key |
42
+
43
+ ¹ Single model per instance. Run **llama-swap** in front of `llama-server`/vLLM to unlock switching —
44
+ Crossbar detects it automatically and prefers it.
45
+
46
+ Full endpoint-level detail is in [`CAPABILITY-MATRIX.md`](./CAPABILITY-MATRIX.md); research notes and
47
+ Pi-integration citations are in [`RESEARCH.md`](./RESEARCH.md) and [`ARCHITECTURE.md`](./ARCHITECTURE.md).
48
+
49
+ ## Install
50
+
51
+ ```bash
52
+ # from npm
53
+ pi install npm:@hypabolic/crossbar
54
+
55
+ # or from git
56
+ pi install git:github.com/hypabolic/crossbar
57
+
58
+ # or a local checkout
59
+ pi install /path/to/crossbar
60
+ ```
61
+
62
+ Then start Pi and run `/crossbar`.
63
+
64
+ ## Usage
65
+
66
+ - **`/crossbar`** (alias **`/local`**) — open the onboarding overlay: pick from auto-discovered servers
67
+ or add one manually (URL + optional API key / no-auth toggle), test the connection, choose a model,
68
+ and save.
69
+ - **Auto-discovery** runs on session start: reachable no-auth servers on localhost are registered
70
+ automatically; keyed servers are surfaced with a prompt to add them.
71
+ - Registered models appear in Pi's standard **`/model`** picker and `/login` provider list.
72
+ - The **loaded-model widget** shows what's currently resident (`●` live via introspection, `◷` with a
73
+ `(last-known)` suffix where a backend can't report live state).
74
+
75
+ LAN discovery (beyond localhost) is **off by default** — no backend advertises over mDNS, so Crossbar
76
+ never scans your network unless you opt in.
77
+
78
+ ## How it works
79
+
80
+ - **Discovery** probes localhost ports and fingerprints each server by response shape (e.g. Ollama's
81
+ `GET /` banner, LM Studio's `/api/v0/models` state, vLLM's `/version` + `owned_by`), preferring the
82
+ most specific match. The generic adapter is the low-confidence fallback.
83
+ - **Registration** maps discovered models onto Pi's built-in `openai-completions` / `anthropic-messages`
84
+ providers via `pi.registerProvider` — Crossbar writes no streaming code for OpenAI-compatible servers.
85
+ - **Persistence** keeps non-secret server metadata in `~/.pi/agent/crossbar.json`; **API keys live only
86
+ in Pi's `auth.json`** (mode `0600`), keyed by the server's provider id, exactly like Pi's own creds.
87
+
88
+ ## Security
89
+
90
+ - API keys are **never** written to `crossbar.json`, never logged, and never inlined into a provider
91
+ config — Pi resolves them from `auth.json` at request time.
92
+ - Discovery is localhost-only by default.
93
+ - Crossbar adds no telemetry.
94
+
95
+ ## Development
96
+
97
+ ```bash
98
+ npm install
99
+ npm run check # tsc --noEmit
100
+ npm test # vitest (conformance + unit + live-socket integration)
101
+ ```
102
+
103
+ The `BackendAdapter` contract (`src/core/`) is the frozen boundary every adapter implements; the
104
+ conformance suite (`tests/conformance/`) validates every adapter against it, and
105
+ `tests/integration/` exercises the real discovery path over live sockets.
106
+
107
+ ### CI / releasing
108
+
109
+ - **CI** (`.github/workflows/ci.yml`) runs `tsc --noEmit` + the full test suite on every push and PR
110
+ (Node 22 & 24).
111
+ - **Releases** (`.github/workflows/release.yml`) publish to npm via **GitHub→npm OIDC trusted
112
+ publishing** — no tokens or secrets. [Provenance](https://docs.npmjs.com/generating-provenance-statements)
113
+ is attached automatically. Two ways:
114
+ 1. **Manual** — GitHub → *Actions → Release → Run workflow* → choose `patch` / `minor` / `major`.
115
+ It bumps `package.json`, commits, tags `vX.Y.Z`, and publishes.
116
+ 2. **Tag push** — `npm version patch && git push --follow-tags` locally.
117
+
118
+ **One-time setup:** on npmjs.com, add a **Trusted Publisher** for `@hypabolic/crossbar`
119
+ (*Package settings → Trusted Publisher → GitHub Actions*) pointing at repo **`Hypabolic/Crossbar`**
120
+ and workflow **`release.yml`**. The workflow authenticates through the OIDC `id-token` it already
121
+ requests — no `NPM_TOKEN` needed.
122
+
123
+ <!-- TODO: add an onboarding demo GIF (docs/onboarding.gif) recorded against a live Ollama + LM Studio. -->
124
+
125
+ ## License
126
+
127
+ [MIT](./LICENSE) © Hypabolic