@gtrabanco/pi-nan-provider 1.2.0 → 1.3.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.
- package/AGENTS.md +34 -12
- package/README.es.md +51 -26
- package/README.md +51 -27
- package/package.json +4 -4
- package/scripts/models.generated.ts +2 -2
- package/scripts/prepare-sdk-update-pr.mjs +145 -0
- package/src/commands.ts +62 -149
- package/src/cross-model-thinking-guard.ts +42 -3
- package/src/index.ts +28 -38
- package/src/mcp/api-key.ts +1 -26
- package/src/mcp/state.ts +0 -3
- package/src/pi-ai-version.ts +92 -0
package/AGENTS.md
CHANGED
|
@@ -270,29 +270,33 @@ Since 1.0.0 the package version **aligns its MAJOR.MINOR with the pi release lin
|
|
|
270
270
|
the `SessionEntry` union, expanded `TurnEndEvent`, removed `shouldStopAfterTurn`,
|
|
271
271
|
`SessionManager` canonical for provider context. The package touches none of them.
|
|
272
272
|
- Since 0.10.0, this package uses native MCP via `pi.registerMcpServer()`.
|
|
273
|
-
|
|
273
|
+
The server is session-scoped, visible in `/mcp` with source "extension".
|
|
274
274
|
Server names and tool IDs:
|
|
275
|
-
- `nan-search` → tools: `mcp__nan_search__web_search`
|
|
276
|
-
(official remote, HTTP, `Authorization: Bearer <key>` header)
|
|
277
275
|
- `nan-media` → tools: `mcp__nan_media__generate_image/edit_image/text_to_speech/list_voices/speech_to_text/list_models/embed_text/rerank_documents/get_quota`
|
|
278
276
|
(community stdio, `npx -y nan-mcp-server@1.3.0`, 120s timeout; 9 tools as of 1.3.0)
|
|
279
|
-
- `exposure: "direct"`
|
|
277
|
+
- `exposure: "direct"` — tools are declared to the model.
|
|
280
278
|
- User `mcp.json` entries with the same name take precedence.
|
|
281
279
|
- `/nan-mcp enable/disable` calls `pi.registerMcpServer()` / `pi.unregisterMcpServer()` directly.
|
|
282
|
-
- Missing `NAN_API_KEY` at load:
|
|
280
|
+
- Missing `NAN_API_KEY` at load: media server NOT registered (console.warn).
|
|
281
|
+
- **`nan-search` was REMOVED in 1.2.1.** NaN's official remote MCP server
|
|
282
|
+
(`https://api.nan.builders/mcp`) now exposes ZERO tools: `initialize` returns an
|
|
283
|
+
empty `capabilities.tools` and `tools/list` returns `{"tools":[]}` (verified with a
|
|
284
|
+
valid key on 2026-10-15), and NaN deleted every MCP mention from its docs (the old
|
|
285
|
+
`/docs/mcp` redirects to `/docs/agent-setup`). The `web_search` tool no longer
|
|
286
|
+
exists anywhere in NaN's surface. Users must bring their own `web_search`/fetch MCP
|
|
287
|
+
server in `mcp.json` (README documents this). `NAN_MCP_TOOLS` is gone with it.
|
|
283
288
|
NaN integration facts remain:
|
|
284
289
|
- pi >=0.99.2 sanitizes MCP tool/namespace names by replacing every character except
|
|
285
|
-
`[A-Za-z0-9_]` with `"_"` (0.99.0–0.99.1 kept `-`). So `mcp__nan-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
(`nan-search` and `nan-media` do not collide, they differ in more than one character).
|
|
290
|
+
`[A-Za-z0-9_]` with `"_"` (0.99.0–0.99.1 kept `-`). So `mcp__nan-media__*` keeps its
|
|
291
|
+
underscore form from 0.99.2 onward. (The removed `nan-search` server used to
|
|
292
|
+
demonstrate the hyphen→underscore rename; do not rely on it.)
|
|
289
293
|
- pi 0.99.2+/1.0 accept `McpServerConfig.description` (system-prompt listing plus
|
|
290
294
|
tool-search ranking); `validateMcpServerConfig` in 0.99.0–0.99.1 ignores unknown
|
|
291
295
|
keys, so passing `description` stays compatible.
|
|
292
296
|
- NaN's official remote MCP server: `https://api.nan.builders/mcp` (host
|
|
293
297
|
root, NOT /v1; JSON-RPC 2.0 over streamable HTTP, stateless; same `sk-`
|
|
294
298
|
key, shared rate limit/quota). Spec: https://nan.builders/openapi.json
|
|
295
|
-
(tag "MCP"). Currently exposes
|
|
299
|
+
(tag "MCP"). **Currently exposes no tools** (see the removal note above).
|
|
296
300
|
- Community `nan-mcp-server` (https://github.com/luciferfran/nan-mcp-server):
|
|
297
301
|
stdio MCP server, version-pinned via NAN_MEDIA_MCP_VERSION (default 1.3.0)
|
|
298
302
|
or full command override via NAN_MEDIA_MCP_COMMAND. Tools (1.3.0): generate_image,
|
|
@@ -340,8 +344,26 @@ Since 1.0.0 the package version **aligns its MAJOR.MINOR with the pi release lin
|
|
|
340
344
|
`src/reasoning-only-stream-guard.ts` (issue #21) keys on that marker;
|
|
341
345
|
earlier comment/message text said "60,000 characters" and was corrected to
|
|
342
346
|
the documented figures.
|
|
343
|
-
- **pi 1.
|
|
344
|
-
baseline):
|
|
347
|
+
- **pi 1.3.0 / pi-ai 1.3.0 compat** (verified 2026-10-15, the current verified
|
|
348
|
+
baseline): devDependencies bumped 1.2.0 → 1.3.0; the peer range stays
|
|
349
|
+
`">=0.99.0 <2"`. The pi-ai 1.3.0 `compat.d.ts` and `api/openai-completions.d.ts`
|
|
350
|
+
surfaces are byte-identical to 1.2.0; `index.d.ts` only drops the
|
|
351
|
+
`utils/assistant-message-frame` re-export (unused here), and the
|
|
352
|
+
`thinkingLevelMap.off` branch in `buildRequest` still fires. **pi-ai 1.3.0 lands
|
|
353
|
+
the upstream fix for issue #3**: `transformMessages` now DROPS cross-model
|
|
354
|
+
`thinking` blocks (`if (!isSameModel) return []`) instead of downgrading them to
|
|
355
|
+
unbounded text — the cross-model-thinking guard is therefore VERSION-GATED
|
|
356
|
+
(`CROSS_MODEL_GUARD_PI_AI_CEILING = "1.3.0"`; registers only on hosts older than
|
|
357
|
+
that, unknown version keeps it on) and is DEPRECATED for removal in the next
|
|
358
|
+
package release once the peer floor reaches `>=1.3.0`. `RETRYABLE_PROVIDER_ERROR_PATTERN`
|
|
359
|
+
expands with gateway/upstream wording ("service is temporarily unavailable" #10965,
|
|
360
|
+
upstream wrapper text #10842) and #10926 fixes OpenAI-compatible streams where
|
|
361
|
+
LiteLLM sends reasoning and content in the same chunk — both relevant to NaN's
|
|
362
|
+
LiteLLM gateway, both upstream-side. Package tests (319) and typecheck pass against
|
|
363
|
+
1.3.0; the issue-3 acceptance tests were re-pointed at the new upstream reality
|
|
364
|
+
(thinking scenario pins pi-ai's drop; the opaque-400 reclassification contract is
|
|
365
|
+
exercised via oversized user content pi-ai cannot shrink).
|
|
366
|
+
- **pi 1.2.0 / pi-ai 1.2.0 compat** (verified 2026-10-14): the peer range stays `">=0.99.0 <2"` (1.2.0 is inside it). The pi-ai
|
|
345
367
|
1.2.0 index/compat type surface is byte-identical to 1.0.0 (`index.d.ts` diff is
|
|
346
368
|
empty); the only additions are `KnownProvider` gaining `"azure"` (renamed from
|
|
347
369
|
`"azure-openai-responses"` in pi 1.0.3 — not used here), a new
|
package/README.es.md
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# @gtrabanco/pi-nan-provider
|
|
2
2
|
|
|
3
3
|
[](https://opensource.org/licenses/MIT)
|
|
4
|
-
[](https://github.com/gtrabanco/pi-nan-provider/releases)
|
|
5
|
+
[](https://cloud.nan.builders/r/7GK06FX8)
|
|
5
6
|
|
|
6
|
-
[NaN Builders](https://nan.builders) model provider + MCP nativo para [pi](https://github.com/earendil-works/pi).
|
|
7
|
+
[NaN Builders](https://cloud.nan.builders/r/7GK06FX8) model provider + MCP nativo para [pi](https://github.com/earendil-works/pi).
|
|
7
8
|
|
|
8
|
-
Registra el proveedor `nan` vía `pi.registerProvider()` usando la API OpenAI-compatible de NaN (`https://api.nan.builders/v1`), y registra
|
|
9
|
+
Registra el proveedor `nan` vía `pi.registerProvider()` usando la API OpenAI-compatible de NaN (`https://api.nan.builders/v1`), y registra el servidor MCP de media de la comunidad con `pi.registerMcpServer()`.
|
|
9
10
|
|
|
10
11
|
---
|
|
11
12
|
|
|
@@ -51,6 +52,8 @@ Al cambiar de modelo, pi-ai reenvía el razonamiento del modelo anterior como te
|
|
|
51
52
|
|
|
52
53
|
Este paquete **elimina todos los bloques de razonamiento cross-model reenviados**, de modo que cambiar de un modelo de 1M de contexto a uno de 262K (`qwen3.6`) ya no desborda la ventana. Las respuestas y los tool results de los modelos no se tocan — solo se quitan sus trazas internas de razonamiento, así que `qwen3.6` puede seguir respondiendo sobre lo que hizo otro modelo. El razonamiento del mismo modelo no se toca nunca, y el guard solo actúa sobre peticiones dirigidas a los proveedores de este paquete. Pon `NAN_THINKING_GUARD=0` para desactivarlo.
|
|
53
54
|
|
|
55
|
+
> **pi 1.3.0+: este guard ya no se registra.** pi-ai 1.3.0 elimina el thinking cross-model por sí mismo (el fix upstream ya está publicado), así que el guard solo se activa en pi < 1.3.0 y está **deprecado — se eliminará en la próxima versión del paquete**. Si usas pi < 1.3.0, actualiza pi para mantener la mitigación; en pi ≥ 1.3.0 no hay que hacer nada.
|
|
56
|
+
|
|
54
57
|
Si una petición sigue desbordando — guard desactivado, inflado que no es un bloque de razonamiento (tool outputs grandes, imágenes) o una ventana de destino más pequeña — NaN responde con el mismo 400 genérico en lugar de nombrar el desbordamiento, y la auto-compactación de pi no lo reconoce, así que la sesión se queda atascada en el techo. Por eso el proveedor **vuelve a comprobar el tamaño de la petición a la salida**: cuando llega ese 400 genérico para una petición estimada por encima de la ventana del modelo, el error se reescribe como un mensaje de context overflow que pi reconoce, de modo que compacta y reintenta en vez de bloquearse. Un 400 genérico en una petición dentro de la ventana no se toca, así que nunca se etiquetan mal errores no relacionados.
|
|
55
58
|
|
|
56
59
|
### ⏱️ Streams truncados intermitentes (auto-retry, sin stall silencioso)
|
|
@@ -101,22 +104,48 @@ Consigue una clave en la [plataforma NaN](https://cloud.nan.builders/r/7GK06FX8)
|
|
|
101
104
|
|
|
102
105
|
## 🔌 MCP nativo
|
|
103
106
|
|
|
104
|
-
Dado que [pi 0.99.0 incluye un cliente MCP integrado](https://github.com/earendil-works/pi/blob/main/docs/usage.md), este paquete
|
|
107
|
+
Dado que [pi 0.99.0 incluye un cliente MCP integrado](https://github.com/earendil-works/pi/blob/main/docs/usage.md), este paquete usa MCP nativo (requiere `pi >=0.99, <2` — pi 0.99 hasta 1.2 son compatibles; el rango peer se amplió a `<2` en 0.11.0).
|
|
105
108
|
|
|
106
|
-
|
|
109
|
+
El puente de media está **activado por defecto** (por sesión, visible en `/mcp`). Usa `/nan-mcp` para gestionarlo.
|
|
107
110
|
|
|
108
|
-
> [!
|
|
109
|
-
>
|
|
111
|
+
> [!WARNING]
|
|
112
|
+
> **El servidor MCP remoto oficial de NaN ya no expone ninguna herramienta.** Hasta 1.2.0 este paquete
|
|
113
|
+
> incluía un puente `nan-search` apuntando a `https://api.nan.builders/mcp`, que exponía
|
|
114
|
+
> `web_search`. Ese servidor ahora responde a `initialize` con `capabilities.tools` vacío y a
|
|
115
|
+
> `tools/list` con `{"tools":[]}` — verificado con una clave válida el 2026-10-15 — y NaN eliminó toda
|
|
116
|
+
> mención a MCP de su documentación (la antigua página `/docs/mcp` ahora redirige a `/docs/agent-setup`).
|
|
117
|
+
> El puente se eliminó en **1.2.1** porque registrar un servidor que no expone nada solo produce un
|
|
118
|
+
> servidor sin herramientas en `/mcp`.
|
|
119
|
+
>
|
|
120
|
+
> **Trae tu propio `web_search` y fetch.** Instala un servidor MCP que los proporcione (p. ej. el par
|
|
121
|
+
> `fetch` + `brave-search` de los servidores MCP oficiales, o cualquier otro servidor MCP de
|
|
122
|
+
> búsqueda/fetch) y decláralo en tu `mcp.json` global:
|
|
123
|
+
>
|
|
124
|
+
> ```json
|
|
125
|
+
> {
|
|
126
|
+
> "mcpServers": {
|
|
127
|
+
> "fetch": { "command": "uvx", "args": ["mcp-server-fetch"] },
|
|
128
|
+
> "web-search": {
|
|
129
|
+
> "command": "npx",
|
|
130
|
+
> "args": ["-y", "@modelcontextprotocol/server-brave-search"],
|
|
131
|
+
> "env": { "BRAVE_API_KEY": "tu-clave" }
|
|
132
|
+
> }
|
|
133
|
+
> }
|
|
134
|
+
> }
|
|
135
|
+
> ```
|
|
136
|
+
>
|
|
137
|
+
> Vuelve a comprobar `https://api.nan.builders/mcp` periódicamente: si NaN publica herramientas otra
|
|
138
|
+
> vez, este paquete puede restaurar el puente.
|
|
110
139
|
|
|
111
140
|
### 🛠️ Comando de Gestión: `/nan-mcp`
|
|
112
141
|
|
|
113
142
|
| Comando | Efecto |
|
|
114
143
|
| :--- | :--- |
|
|
115
|
-
| `/nan-mcp status` | Muestra el estado
|
|
116
|
-
| `/nan-mcp enable [target]` | Activa `
|
|
117
|
-
| `/nan-mcp disable [target]` |
|
|
144
|
+
| `/nan-mcp status` | Muestra el estado del puente de media y la resolución de la clave API. |
|
|
145
|
+
| `/nan-mcp enable [target]` | Activa `nan-mcp-server` (persiste). |
|
|
146
|
+
| `/nan-mcp disable [target]` | Lo desactiva de forma persistente. |
|
|
118
147
|
|
|
119
|
-
|
|
148
|
+
El puente resuelve la credencial almacenada de `/login nan` (almacenada → env). Si ninguna se resuelve, el servidor no se registra y se muestra un aviso en la salida de estado.
|
|
120
149
|
|
|
121
150
|
> [!NOTE]
|
|
122
151
|
> El registro ocurre cuando se carga la extensión. Si ejecutas `/login nan` **después** de que la sesión haya empezado, la factoría ya se ejecutó — usa `/reload` (o `/nan-mcp enable`) para registrar los puentes sin reiniciar pi.
|
|
@@ -171,15 +200,7 @@ conector con `pi config` → Built-in extensions → `mcp`.
|
|
|
171
200
|
|
|
172
201
|
---
|
|
173
202
|
|
|
174
|
-
###
|
|
175
|
-
*Puente oficial para herramientas remotas vía [https://api.nan.builders/mcp](https://nan.builders/docs/api).*
|
|
176
|
-
|
|
177
|
-
- **`mcp__nan_search__web_search(query, ...)`**: Realiza búsquedas web a través del gateway de NaN.
|
|
178
|
-
> pi 0.99.0–0.99.1 usaba la forma con guiones (`mcp__nan-search__web_search`); pi 0.99.2+ sanitiza
|
|
179
|
-
> los nombres de herramientas a solo `[A-Za-z0-9_]`, así que los guiones se convirtieron en
|
|
180
|
-
> guiones bajos. La forma con guiones bajos aplica desde 0.99.2 hasta pi 1.0.
|
|
181
|
-
|
|
182
|
-
### 2. Servidor MCP de Media (Comunidad)
|
|
203
|
+
### Servidor MCP de Media (Comunidad)
|
|
183
204
|
*Conecta [`nan-mcp-server`](https://github.com/luciferfran/nan-mcp-server) mediante un cliente stdio local mínimo.*
|
|
184
205
|
|
|
185
206
|
- **Por sesión**: El servidor se conecta al registrarse (eager). Aparece en `/mcp` con fuente "extension".
|
|
@@ -197,10 +218,9 @@ conector con `pi config` → Built-in extensions → `mcp`.
|
|
|
197
218
|
| `mcp__nan_media__rerank_documents` | Reordenación de documentos (Qwen3-Reranker) |
|
|
198
219
|
| `mcp__nan_media__get_quota` | Cuota de tokens por modelo (usado / tope / restante / reinicio) |
|
|
199
220
|
|
|
200
|
-
> pi 0.99.0–0.99.1 usaba
|
|
201
|
-
> `
|
|
202
|
-
>
|
|
203
|
-
> con guiones bajos aplica desde 0.99.2 hasta pi 1.0.
|
|
221
|
+
> pi 0.99.0–0.99.1 usaba la forma con guiones (`mcp__nan-media__*`); pi 0.99.2+ sanitiza los
|
|
222
|
+
> nombres de herramientas a solo `[A-Za-z0-9_]`, así que los guiones se convirtieron en guiones
|
|
223
|
+
> bajos. La forma con guiones bajos aplica desde 0.99.2 hasta pi 1.2.
|
|
204
224
|
|
|
205
225
|
#### 🔧 Configuración del Puente de Media
|
|
206
226
|
|
|
@@ -210,7 +230,6 @@ conector con `pi config` → Built-in extensions → `mcp`.
|
|
|
210
230
|
| `NAN_MEDIA_MCP_VERSION` | `1.3.0` | Versión del servidor fijada (recomendado). |
|
|
211
231
|
| `NAN_MEDIA_MCP_COMMAND` | — | Override del comando personalizado. |
|
|
212
232
|
| `NAN_MEDIA_MCP_TIMEOUT_MS` | `120000` | Timeout por llamada. |
|
|
213
|
-
| `NAN_MCP_TOOLS` | — | Override para el puente oficial (`0` para desactivar). |
|
|
214
233
|
|
|
215
234
|
#### 🤖 Detección Automática de Actualizaciones
|
|
216
235
|
|
|
@@ -222,6 +241,12 @@ bun run check-nan-mcp-server --json # JSON para máquinas
|
|
|
222
241
|
bun run check-nan-mcp-server --issue # crea/refresca el issue
|
|
223
242
|
```
|
|
224
243
|
|
|
244
|
+
Las actualizaciones del SDK de pi (`@earendil-works/pi-ai` / `@earendil-works/pi-coding-agent`)
|
|
245
|
+
también están automatizadas: el workflow semanal `check-pi-sdk-versions` abre el issue
|
|
246
|
+
`sdk-monitor` **y** prepara un PR con el bump de devDependencies (`bun test` +
|
|
247
|
+
`bun run typecheck` + diff de superficie de tipos incluidos) para revisión humana — ver
|
|
248
|
+
`docs/sdk-version-monitoring.md`.
|
|
249
|
+
|
|
225
250
|
---
|
|
226
251
|
|
|
227
252
|
## 📊 Uso de Cuotas: `/nan-usage`
|
|
@@ -384,4 +409,4 @@ bun test # Ejecutar todos los tests
|
|
|
384
409
|
bun run typecheck # Ejecutar typechecking
|
|
385
410
|
```
|
|
386
411
|
|
|
387
|
-
*Las versiones siguen semver estricto. CI publica automáticamente al hacer merge a `main`.*
|
|
412
|
+
*Las versiones siguen semver estricto. CI publica automáticamente al hacer merge a `main`.*
|
package/README.md
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# @gtrabanco/pi-nan-provider
|
|
2
2
|
|
|
3
3
|
[](https://opensource.org/licenses/MIT)
|
|
4
|
-
[](https://github.com/gtrabanco/pi-nan-provider/releases)
|
|
5
|
+
[](https://cloud.nan.builders/r/7GK06FX8)
|
|
5
6
|
|
|
6
|
-
[NaN Builders](https://nan.builders) model provider + native MCP
|
|
7
|
+
[NaN Builders](https://cloud.nan.builders/r/7GK06FX8) model provider + a native MCP server for [pi](https://github.com/earendil-works/pi).
|
|
7
8
|
|
|
8
|
-
Registers the `nan` provider via `pi.registerProvider()` using NaN's OpenAI-compatible API (`https://api.nan.builders/v1`), and registers MCP
|
|
9
|
+
Registers the `nan` provider via `pi.registerProvider()` using NaN's OpenAI-compatible API (`https://api.nan.builders/v1`), and registers the community media MCP server via pi's native `pi.registerMcpServer()`.
|
|
9
10
|
|
|
10
11
|
---
|
|
11
12
|
|
|
@@ -51,6 +52,8 @@ When you switch models, pi-ai replays the previous model's reasoning as plain as
|
|
|
51
52
|
|
|
52
53
|
This package **drops every replayed cross-model reasoning block**, so switching from a 1M-context model to a 262K one (`qwen3.6`) no longer overflows the window. The models' answers and tool results are untouched — only their internal reasoning traces are removed, so `qwen3.6` can still answer about what another model did. Same-model reasoning is never altered, and the guard only acts on requests targeting this package's providers. Set `NAN_THINKING_GUARD=0` to disable it.
|
|
53
54
|
|
|
55
|
+
> **pi 1.3.0+: this guard no longer registers.** pi-ai 1.3.0 drops cross-model thinking itself (the upstream fix landed), so the guard only activates on pi < 1.3.0 and is **deprecated — it will be removed in the next package release**. If you run pi < 1.3.0, update pi to keep the mitigation; no action is needed on pi ≥ 1.3.0.
|
|
56
|
+
|
|
54
57
|
If a request still overflows — the guard is disabled, the inflation is not a reasoning block (large tool outputs, images), or the destination window is simply smaller — NaN answers the same generic 400 instead of naming the overflow, and pi's auto-compaction does not recognize it, so the session wedges at the ceiling. The provider therefore **re-checks the request size on the way out**: when that generic 400 arrives for a request estimated over the model's window, the error is rewritten into a context-overflow message pi recognizes, so it compacts and retries instead of stalling. A generic 400 on a within-window request is left untouched, so unrelated errors are never mislabelled.
|
|
55
58
|
|
|
56
59
|
### ⏱️ Intermittent truncated streams (auto-retry, no silent stall)
|
|
@@ -99,24 +102,50 @@ You can override any model's `compat` per-model in `~/.pi/agent/models.json` (pi
|
|
|
99
102
|
|
|
100
103
|
Get a key from the [NaN platform](https://cloud.nan.builders/r/7GK06FX8) (user settings → API Keys; referral link).
|
|
101
104
|
|
|
102
|
-
## 🔌 Native MCP
|
|
105
|
+
## 🔌 Native MCP Server
|
|
103
106
|
|
|
104
|
-
Since [pi 0.99.0 ships a built-in MCP client](https://github.com/earendil-works/pi/blob/main/docs/usage.md), this package
|
|
107
|
+
Since [pi 0.99.0 ships a built-in MCP client](https://github.com/earendil-works/pi/blob/main/docs/usage.md), this package bridges to native MCP (requires `pi >=0.99, <2` — pi 0.99 through 1.2 are supported; the peer range was widened to `<2` in 0.11.0).
|
|
105
108
|
|
|
106
|
-
|
|
109
|
+
The media bridge is **enabled by default** (session-scoped, visible in `/mcp`). Use `/nan-mcp` to manage it.
|
|
107
110
|
|
|
108
|
-
> [!
|
|
109
|
-
>
|
|
111
|
+
> [!WARNING]
|
|
112
|
+
> **NaN's official remote MCP server no longer exposes any tool.** Until 1.2.0 this package shipped a
|
|
113
|
+
> `nan-search` bridge pointing at `https://api.nan.builders/mcp`, which exposed
|
|
114
|
+
> `web_search`. That server now answers `initialize` with an empty `capabilities.tools` and
|
|
115
|
+
> `tools/list` with `{"tools":[]}` — verified with a valid key on 2026-10-15 — and NaN removed every
|
|
116
|
+
> MCP mention from its documentation (the old `/docs/mcp` page now redirects to `/docs/agent-setup`).
|
|
117
|
+
> The bridge was removed in **1.2.1** because registering a server that exposes nothing only produces
|
|
118
|
+
> a tool-less server in `/mcp`.
|
|
119
|
+
>
|
|
120
|
+
> **Bring your own `web_search` and fetch.** Install an MCP server that provides them (e.g. the
|
|
121
|
+
> `fetch` + `brave-search` pair for the official MCP servers, or any other search/fetch MCP server)
|
|
122
|
+
> and declare it in your global `mcp.json`:
|
|
123
|
+
>
|
|
124
|
+
> ```json
|
|
125
|
+
> {
|
|
126
|
+
> "mcpServers": {
|
|
127
|
+
> "fetch": { "command": "uvx", "args": ["mcp-server-fetch"] },
|
|
128
|
+
> "web-search": {
|
|
129
|
+
> "command": "npx",
|
|
130
|
+
> "args": ["-y", "@modelcontextprotocol/server-brave-search"],
|
|
131
|
+
> "env": { "BRAVE_API_KEY": "your-key" }
|
|
132
|
+
> }
|
|
133
|
+
> }
|
|
134
|
+
> }
|
|
135
|
+
> ```
|
|
136
|
+
>
|
|
137
|
+
> Re-check `https://api.nan.builders/mcp` periodically: if NaN publishes tools again, this package can
|
|
138
|
+
> restore the bridge.
|
|
110
139
|
|
|
111
140
|
### 🛠️ Management Command: `/nan-mcp`
|
|
112
141
|
|
|
113
142
|
| Command | Effect |
|
|
114
143
|
| :--- | :--- |
|
|
115
|
-
| `/nan-mcp status` | Shows
|
|
116
|
-
| `/nan-mcp enable [target]` | Enables `
|
|
117
|
-
| `/nan-mcp disable [target]` | Disables
|
|
144
|
+
| `/nan-mcp status` | Shows the media bridge state and API key resolution. |
|
|
145
|
+
| `/nan-mcp enable [target]` | Enables `nan-mcp-server` (persisted). |
|
|
146
|
+
| `/nan-mcp disable [target]` | Disables it persistently. |
|
|
118
147
|
|
|
119
|
-
|
|
148
|
+
The bridge resolves the stored `/login nan` credential (stored → env). If neither resolves, the server does not register and a warning is shown in the status output.
|
|
120
149
|
|
|
121
150
|
> [!NOTE]
|
|
122
151
|
> Registration happens when the extension loads. If you run `/login nan` **after** the session started, the factory has already run — run `/reload` (or `/nan-mcp enable`) to register the bridges without restarting pi.
|
|
@@ -168,15 +197,7 @@ registering a server that can never connect. To get MCP in the CLI, enable the c
|
|
|
168
197
|
|
|
169
198
|
---
|
|
170
199
|
|
|
171
|
-
###
|
|
172
|
-
*Official remote MCP server registered natively via `pi.registerMcpServer()`.*
|
|
173
|
-
|
|
174
|
-
- **`mcp__nan_search__web_search(query, ...)`**: Performs web searches through NaN's gateway.
|
|
175
|
-
> pi 0.99.0–0.99.1 used the hyphenated form (`mcp__nan-search__web_search`); pi 0.99.2+ sanitizes
|
|
176
|
-
> tool names to `[A-Za-z0-9_]` only, so hyphens became underscores. The underscore form applies
|
|
177
|
-
> from 0.99.2 onward through pi 1.0.
|
|
178
|
-
|
|
179
|
-
### 2. Community Media MCP Server
|
|
200
|
+
### Community Media MCP Server
|
|
180
201
|
*Bridges [`nan-mcp-server`](https://github.com/luciferfran/nan-mcp-server) via a minimal local stdio client.*
|
|
181
202
|
|
|
182
203
|
- **Session-scoped**: The server connects at registration time (eager). Appears in `/mcp` with source "extension".
|
|
@@ -194,10 +215,9 @@ registering a server that can never connect. To get MCP in the CLI, enable the c
|
|
|
194
215
|
| `mcp__nan_media__rerank_documents` | Document reranking (Qwen3-Reranker) |
|
|
195
216
|
| `mcp__nan_media__get_quota` | Per-model token quota (used / cap / remaining / reset) |
|
|
196
217
|
|
|
197
|
-
> pi 0.99.0–0.99.1 used the hyphenated
|
|
198
|
-
>
|
|
199
|
-
>
|
|
200
|
-
> through pi 1.0.
|
|
218
|
+
> pi 0.99.0–0.99.1 used the hyphenated form (`mcp__nan-media__*`); pi 0.99.2+ sanitizes tool
|
|
219
|
+
> names to `[A-Za-z0-9_]` only, so hyphens became underscores. The underscore form applies from
|
|
220
|
+
> 0.99.2 onward through pi 1.2.
|
|
201
221
|
|
|
202
222
|
#### 🔧 Media Bridge Configuration
|
|
203
223
|
|
|
@@ -207,7 +227,6 @@ registering a server that can never connect. To get MCP in the CLI, enable the c
|
|
|
207
227
|
| `NAN_MEDIA_MCP_VERSION` | `1.3.0` | Pinned server version (recommended). |
|
|
208
228
|
| `NAN_MEDIA_MCP_COMMAND` | — | Custom command override. |
|
|
209
229
|
| `NAN_MEDIA_MCP_TIMEOUT_MS` | `120000` | Per-call timeout. |
|
|
210
|
-
| `NAN_MCP_TOOLS` | — | Override for the official bridge (`0` to disable). |
|
|
211
230
|
|
|
212
231
|
#### 🤖 Automated Update Detection
|
|
213
232
|
|
|
@@ -219,6 +238,11 @@ bun run check-nan-mcp-server --json # machine-readable JSON
|
|
|
219
238
|
bun run check-nan-mcp-server --issue # create/refresh the issue
|
|
220
239
|
```
|
|
221
240
|
|
|
241
|
+
PI SDK updates (`@earendil-works/pi-ai` / `@earendil-works/pi-coding-agent`) are automated
|
|
242
|
+
too: the weekly `check-pi-sdk-versions` workflow files the `sdk-monitor` issue **and**
|
|
243
|
+
prepares a devDependencies-bump PR (`bun test` + `bun run typecheck` + type-surface diff
|
|
244
|
+
included) for human review — see `docs/sdk-version-monitoring.md`.
|
|
245
|
+
|
|
222
246
|
---
|
|
223
247
|
|
|
224
248
|
## 📊 Quota Usage: `/nan-usage`
|
|
@@ -381,4 +405,4 @@ bun test # Run all tests
|
|
|
381
405
|
bun run typecheck # Run typechecking
|
|
382
406
|
```
|
|
383
407
|
|
|
384
|
-
*Releases follow strict semver. CI publishes automatically on merge to `main`.*
|
|
408
|
+
*Releases follow strict semver. CI publishes automatically on merge to `main`.*
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gtrabanco/pi-nan-provider",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "NaN Builders (api.nan.builders) model provider for pi - OpenAI-compatible registration with a models.dev-generated fallback, tier-aware live catalog, and
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "NaN Builders (api.nan.builders) model provider for pi - OpenAI-compatible registration with a models.dev-generated fallback, tier-aware live catalog, and an optional community media MCP bridge",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi",
|
|
7
7
|
"pi-package",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"@earendil-works/pi-coding-agent": ">=0.99.0 <2"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@earendil-works/pi-ai": "1.
|
|
50
|
-
"@earendil-works/pi-coding-agent": "1.
|
|
49
|
+
"@earendil-works/pi-ai": "1.3.0",
|
|
50
|
+
"@earendil-works/pi-coding-agent": "1.3.0",
|
|
51
51
|
"@types/bun": "1.4",
|
|
52
52
|
"typescript": "7"
|
|
53
53
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// This file is auto-generated by scripts/generate-models.ts
|
|
2
2
|
// Do not edit manually — run `bun run generate-models` to update.
|
|
3
3
|
//
|
|
4
|
-
// Source: https://models.dev/api.json (provider "nan"), fetched 2026-10-
|
|
4
|
+
// Source: https://models.dev/api.json (provider "nan"), fetched 2026-10-19T22:52:49.128Z
|
|
5
5
|
// Provenance: every contextWindow/maxTokens/input/cost value traces to
|
|
6
6
|
// models.dev or to the per-entry notes below. Nothing is invented; entries
|
|
7
7
|
// models.dev documents incompletely are omitted and flagged instead.
|
|
@@ -459,7 +459,7 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
|
|
|
459
459
|
export const GENERATED_CATALOG_META = {
|
|
460
460
|
source: "https://models.dev/api.json",
|
|
461
461
|
modelsDevProvider: "nan",
|
|
462
|
-
fetchedAt: "2026-10-
|
|
462
|
+
fetchedAt: "2026-10-19T22:52:49.128Z",
|
|
463
463
|
modelCount: 6,
|
|
464
464
|
models: ["deepseek-v4-flash","gemma4","glm5.3-flash","mimo-v2.6-flash","qwen3.6","qwen3.8-flash"],
|
|
465
465
|
notes: [
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Prepare an SDK update PR for the packages monitored by
|
|
4
|
+
* scripts/check-pi-sdk-versions.mjs: bump their devDependencies in
|
|
5
|
+
* package.json and emit the branch/PR metadata the
|
|
6
|
+
* `.github/workflows/check-pi-sdk-versions.yml` workflow needs to open the PR.
|
|
7
|
+
*
|
|
8
|
+
* This script ONLY edits package.json. Installing, testing, typechecking, the
|
|
9
|
+
* type-surface diff, git and `gh pr create` stay in the workflow so a failure
|
|
10
|
+
* anywhere aborts the PR.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* node scripts/prepare-sdk-update-pr.mjs --body-file /tmp/pr-body.md
|
|
14
|
+
*
|
|
15
|
+
* Output (stdout, single JSON line):
|
|
16
|
+
* {"updates":false}
|
|
17
|
+
* {"updates":true,"branch":"chore/pi-sdk-1.3.0","title":"...","bodyFile":"...",
|
|
18
|
+
* "bumps":[{"name":"@earendil-works/pi-ai","from":"1.2.0","to":"1.3.0"}]}
|
|
19
|
+
*
|
|
20
|
+
* Exit codes: 0 = nothing to do or prepared; 2 = unexpected failure.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import https from "node:https";
|
|
24
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
26
|
+
|
|
27
|
+
const SDK_PACKAGES = ["@earendil-works/pi-ai", "@earendil-works/pi-coding-agent"];
|
|
28
|
+
|
|
29
|
+
function npmLatest(packageName) {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
const req = https.get(
|
|
32
|
+
`https://registry.npmjs.org/${encodeURIComponent(packageName)}`,
|
|
33
|
+
{ headers: { accept: "application/json" } },
|
|
34
|
+
(res) => {
|
|
35
|
+
if (res.statusCode !== 200) {
|
|
36
|
+
reject(new Error(`npm registry returned ${res.statusCode} for ${packageName}`));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
let data = "";
|
|
40
|
+
res.on("data", (chunk) => (data += chunk));
|
|
41
|
+
res.on("end", () => {
|
|
42
|
+
const latest = JSON.parse(data)["dist-tags"]?.["latest"];
|
|
43
|
+
if (!latest) reject(new Error(`No "latest" dist-tag for ${packageName}`));
|
|
44
|
+
else resolve(latest);
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
req.on("error", reject);
|
|
49
|
+
req.setTimeout(15_000, () => {
|
|
50
|
+
req.destroy();
|
|
51
|
+
reject(new Error(`Timeout fetching ${packageName} from npm`));
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function main() {
|
|
57
|
+
const bodyFileArg = process.argv.indexOf("--body-file");
|
|
58
|
+
const bodyFile =
|
|
59
|
+
bodyFileArg !== -1 ? process.argv[bodyFileArg + 1] : "/tmp/sdk-update-pr-body.md";
|
|
60
|
+
|
|
61
|
+
const packageUrl = new URL("../package.json", import.meta.url);
|
|
62
|
+
const raw = readFileSync(packageUrl, "utf-8");
|
|
63
|
+
const pkg = JSON.parse(raw);
|
|
64
|
+
|
|
65
|
+
const bumps = [];
|
|
66
|
+
for (const name of SDK_PACKAGES) {
|
|
67
|
+
const from = pkg.devDependencies?.[name];
|
|
68
|
+
if (!from) throw new Error(`${name} is not a devDependency of this package`);
|
|
69
|
+
const to = await npmLatest(name);
|
|
70
|
+
if (to !== from) bumps.push({ name, from, to });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (bumps.length === 0) {
|
|
74
|
+
console.log(JSON.stringify({ updates: false }));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Bump via exact-string replacement so the rest of package.json (ordering,
|
|
79
|
+
// indentation, other fields) is untouched.
|
|
80
|
+
let updated = raw;
|
|
81
|
+
for (const bump of bumps) {
|
|
82
|
+
const needle = `"${bump.name}": "${bump.from}"`;
|
|
83
|
+
if (!updated.includes(needle)) {
|
|
84
|
+
throw new Error(`Could not locate ${needle} in package.json to bump it`);
|
|
85
|
+
}
|
|
86
|
+
updated = updated.replace(needle, `"${bump.name}": "${bump.to}"`);
|
|
87
|
+
}
|
|
88
|
+
writeFileSync(packageUrl, updated);
|
|
89
|
+
|
|
90
|
+
const versionTag = bumps.map((b) => b.to).join("-");
|
|
91
|
+
const branch = `chore/pi-sdk-${versionTag}`;
|
|
92
|
+
const title = `chore: bump PI SDK to ${bumps.map((b) => `${b.name.split("/")[1]} ${b.to}`).join(" / ")}`;
|
|
93
|
+
const peer = pkg.peerDependencies ?? {};
|
|
94
|
+
|
|
95
|
+
const body = `## PI SDK update (automated)
|
|
96
|
+
|
|
97
|
+
Prepared from the weekly \`check-pi-sdk-versions\` run. Resolves the \`sdk-monitor\` issue
|
|
98
|
+
for this cycle. **A human must review the upstream changelog before merging** — a minor
|
|
99
|
+
pi release can change provider-relevant behavior (see \`AGENTS.md\` "Verified API facts").
|
|
100
|
+
|
|
101
|
+
### Version bumps
|
|
102
|
+
|
|
103
|
+
| Package | From | To | Peer range |
|
|
104
|
+
| --- | --- | --- | --- |
|
|
105
|
+
${bumps.map((b) => `| \`${b.name}\` | ${b.from} | **${b.to}** | \`${peer[b.name] ?? "n/a"}\` |`).join("\n")}
|
|
106
|
+
|
|
107
|
+
### Review checklist
|
|
108
|
+
|
|
109
|
+
- [ ] Upstream release notes reviewed (https://pi.dev/changelog)
|
|
110
|
+
- [ ] \`bun test\` + \`bun run typecheck\` green (executed by CI before this PR was opened)
|
|
111
|
+
- [ ] Type-surface diff (below) contains no removals this package uses
|
|
112
|
+
- [ ] Version policy applied: package MAJOR.MINOR realigns with the pi release line
|
|
113
|
+
(\`package.json\` version bump included in this PR when the pi MINOR moved)
|
|
114
|
+
|
|
115
|
+
The results of the two sections below were produced by CI while preparing this PR.
|
|
116
|
+
|
|
117
|
+
## Verification
|
|
118
|
+
|
|
119
|
+
<!-- VERIFICATION -->
|
|
120
|
+
|
|
121
|
+
## Type-surface diff (previous → new)
|
|
122
|
+
|
|
123
|
+
<details><summary>Click to expand</summary>
|
|
124
|
+
|
|
125
|
+
<!-- SURFACE_DIFF -->
|
|
126
|
+
|
|
127
|
+
</details>
|
|
128
|
+
`;
|
|
129
|
+
|
|
130
|
+
writeFileSync(bodyFile, body);
|
|
131
|
+
console.log(
|
|
132
|
+
JSON.stringify({
|
|
133
|
+
updates: true,
|
|
134
|
+
branch,
|
|
135
|
+
title,
|
|
136
|
+
bodyFile,
|
|
137
|
+
bumps,
|
|
138
|
+
}),
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
main().catch((err) => {
|
|
143
|
+
console.error("Fatal:", err.message);
|
|
144
|
+
process.exit(2);
|
|
145
|
+
});
|
package/src/commands.ts
CHANGED
|
@@ -1,24 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `/nan-mcp` — slash command configuring
|
|
2
|
+
* `/nan-mcp` — slash command configuring the native MCP server this
|
|
3
3
|
* package registers:
|
|
4
4
|
*
|
|
5
|
-
* - `nan-search` — the official NaN remote MCP server (api.nan.builders/mcp),
|
|
6
|
-
* exposing `mcp__nan_search__web_search` (pi >=0.99.2). Default: enabled.
|
|
7
|
-
* (pi 0.99.0–0.99.1 used the hyphenated form `mcp__nan-search__web_search`.)
|
|
8
5
|
* - `nan-media` — the community stdio media server (flux-2-klein / kokoro /
|
|
9
6
|
* whisper), exposing `mcp__nan_media__generate_image` etc. Default: enabled.
|
|
10
7
|
* (pi 0.99.0–0.99.1 used the hyphenated form `mcp__nan-media__*`.)
|
|
11
8
|
*
|
|
12
|
-
*
|
|
13
|
-
* /nan-mcp enables/disables
|
|
9
|
+
* The server is session-scoped (visible in /mcp, source "extension").
|
|
10
|
+
* /nan-mcp enables/disables it via pi.registerMcpServer / pi.unregisterMcpServer,
|
|
14
11
|
* persisting the toggle in nan-provider.json.
|
|
15
12
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* Explicit env vars (NAN_MCP_TOOLS, NAN_MEDIA_MCP) override the persisted
|
|
19
|
-
* toggles for the session.
|
|
13
|
+
* Target aliases: `nan-mcp-server` (`media`, `media-mcp`, `nan-media`).
|
|
14
|
+
* Explicit env var (NAN_MEDIA_MCP) overrides the persisted toggle for the session.
|
|
20
15
|
*
|
|
21
|
-
* Native behavior
|
|
16
|
+
* Native behavior:
|
|
22
17
|
* - `disable` IMMEDIATELY hides tools (unregisterMcpServer disconnects the
|
|
23
18
|
* server; the MCP extension hides tools when disconnected).
|
|
24
19
|
* - `enable` IMMEDIATELY registers the server for the current session.
|
|
@@ -29,55 +24,31 @@
|
|
|
29
24
|
|
|
30
25
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
31
26
|
import { mediaMcpCommand, mediaMcpEnabled, mediaMcpSource, mediaMcpEnvExplicit, mediaMcpEnvTruthy, mediaMcpTimeoutSec } from "./mcp/media-server.ts";
|
|
32
|
-
import { webSearchBridgeEnabled, webSearchBridgeSource, mcpToolsEnvExplicit, mcpToolsDisabled } from "./mcp/api-key.ts";
|
|
33
27
|
import { NAN_API_KEY_ENV, tryResolveNanApiKeyViaRegistry, resolveStoredNanApiKey } from "./mcp/api-key.ts";
|
|
34
28
|
import { NAN_STATE_FILE, MCP_SERVER_NAMES, readBridgeState, writeBridgeState, type BridgeKey, readState, writeState } from "./mcp/state.ts";
|
|
35
29
|
import { connectorMissingMessage, hasMcpConnector } from "./mcp/host-support.ts";
|
|
36
30
|
|
|
37
31
|
const USAGE =
|
|
38
|
-
"Usage: /nan-mcp [status] · /nan-mcp enable [
|
|
32
|
+
"Usage: /nan-mcp [status] · /nan-mcp enable [nan-mcp-server] · /nan-mcp disable [nan-mcp-server]";
|
|
39
33
|
|
|
40
34
|
/** Canonical bridge targets with aliases. */
|
|
41
35
|
const TARGETS: Record<string, BridgeKey> = {
|
|
42
|
-
"web-search": "webSearch",
|
|
43
|
-
search: "webSearch",
|
|
44
|
-
web_search: "webSearch",
|
|
45
|
-
official: "webSearch",
|
|
46
|
-
"nan-web-search": "webSearch",
|
|
47
36
|
"nan-mcp-server": "mediaMcp",
|
|
48
37
|
media: "mediaMcp",
|
|
49
38
|
"media-mcp": "mediaMcp",
|
|
50
39
|
"nan-media": "mediaMcp",
|
|
51
40
|
};
|
|
52
41
|
|
|
53
|
-
function
|
|
54
|
-
return
|
|
42
|
+
function envOverrideLabel(): string {
|
|
43
|
+
return `env NAN_MEDIA_MCP=${process.env.NAN_MEDIA_MCP ?? ""} (overrides persisted)`;
|
|
55
44
|
}
|
|
56
45
|
|
|
57
|
-
function
|
|
58
|
-
|
|
59
|
-
? `env NAN_MCP_TOOLS=${process.env.NAN_MCP_TOOLS ?? ""} (overrides persisted)`
|
|
60
|
-
: `env NAN_MEDIA_MCP=${process.env.NAN_MEDIA_MCP ?? ""} (overrides persisted)`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function sourceLabel(bridge: BridgeKey, source: "env" | "persisted" | "default"): string {
|
|
64
|
-
if (source === "env") return envOverrideLabel(bridge);
|
|
46
|
+
function sourceLabel(source: "env" | "persisted" | "default"): string {
|
|
47
|
+
if (source === "env") return envOverrideLabel();
|
|
65
48
|
if (source === "persisted") {
|
|
66
|
-
return `persisted in <agentDir>/${NAN_STATE_FILE} (
|
|
49
|
+
return `persisted in <agentDir>/${NAN_STATE_FILE} (nan-mcp-server: ${readBridgeState("mediaMcp")})`;
|
|
67
50
|
}
|
|
68
|
-
return "default (
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Resolve a bridge key to its native server name. */
|
|
72
|
-
function serverName(bridge: BridgeKey): string {
|
|
73
|
-
return MCP_SERVER_NAMES[bridge];
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Describe the bridge in the status message. */
|
|
77
|
-
function bridgeDescription(bridge: BridgeKey): string {
|
|
78
|
-
return bridge === "webSearch"
|
|
79
|
-
? "web-search bridge (official NaN MCP → mcp__nan_search__web_search)"
|
|
80
|
-
: "nan-mcp-server bridge (community media MCP → mcp__nan_media__generate_image/edit_image/text_to_speech/list_voices/speech_to_text)";
|
|
51
|
+
return "default (bridge is enabled by default)";
|
|
81
52
|
}
|
|
82
53
|
|
|
83
54
|
/** Check if a native server of this name is currently registered. */
|
|
@@ -89,39 +60,27 @@ function nativeServerRegistered(pi: ExtensionAPI, name: string): boolean {
|
|
|
89
60
|
}
|
|
90
61
|
}
|
|
91
62
|
|
|
92
|
-
/** Status message listing
|
|
63
|
+
/** Status message listing the media bridge gate state and native registration.
|
|
93
64
|
* `ctx` is the command context (provides modelRegistry for key lookup).
|
|
94
65
|
*/
|
|
95
66
|
async function statusMessage(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promise<string> {
|
|
96
67
|
const lines: string[] = [];
|
|
97
68
|
|
|
98
69
|
if (!hasMcpConnector(pi)) {
|
|
99
|
-
lines.push(connectorMissingMessage([MCP_SERVER_NAMES.
|
|
70
|
+
lines.push(connectorMissingMessage([MCP_SERVER_NAMES.mediaMcp]));
|
|
100
71
|
}
|
|
101
72
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
// Key status for web-search
|
|
110
|
-
const keyStatus = await resolveKeyStatus(ctx);
|
|
111
|
-
lines.push(` API key: ${keyStatus}.`);
|
|
112
|
-
}
|
|
113
|
-
if (bridge === "mediaMcp") {
|
|
114
|
-
lines.push(` Media spawn command: ${mediaMcpCommand().join(" ")}.`);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
lines.push("Configure with /nan-mcp enable|disable [web-search|nan-mcp-server]; no target = both.");
|
|
73
|
+
const enabled = mediaMcpEnabled();
|
|
74
|
+
const source = mediaMcpSource();
|
|
75
|
+
const regStatus = nativeServerRegistered(pi, MCP_SERVER_NAMES.mediaMcp) ? "registered (native)" : "not registered";
|
|
76
|
+
lines.push(`nan-mcp-server bridge: ${enabled ? "enabled" : "disabled"} — ${sourceLabel(source)}, ${regStatus}.`);
|
|
77
|
+
lines.push(` Media spawn command: ${mediaMcpCommand().join(" ")}.`);
|
|
78
|
+
lines.push(` API key: ${await resolveKeyStatus(ctx)}.`);
|
|
79
|
+
lines.push("Configure with /nan-mcp enable|disable [nan-mcp-server]; no target for the media server.");
|
|
119
80
|
return lines.join("\n");
|
|
120
81
|
}
|
|
121
82
|
|
|
122
|
-
/** Check API key resolution status
|
|
123
|
-
* `ctx` is the command context (provides modelRegistry).
|
|
124
|
-
*/
|
|
83
|
+
/** Check API key resolution status. */
|
|
125
84
|
async function resolveKeyStatus(ctx: ExtensionCommandContext): Promise<string> {
|
|
126
85
|
const registryKey = await tryResolveNanApiKeyViaRegistry(ctx.modelRegistry);
|
|
127
86
|
const storedKey = resolveStoredNanApiKey();
|
|
@@ -130,37 +89,22 @@ async function resolveKeyStatus(ctx: ExtensionCommandContext): Promise<string> {
|
|
|
130
89
|
return "NOT SET — run /login nan or export NAN_API_KEY";
|
|
131
90
|
}
|
|
132
91
|
|
|
133
|
-
/** Parse an optional target argument; no target =
|
|
134
|
-
function parseTarget(token: string | undefined):
|
|
135
|
-
if (token === undefined) return
|
|
136
|
-
|
|
137
|
-
if (!key) return undefined;
|
|
138
|
-
return { bridges: [key], target: token.toLowerCase() };
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function describeBridges(bridges: BridgeKey[]): string {
|
|
142
|
-
const names = bridges.map((bridge) => targetName(bridge));
|
|
143
|
-
return names.length === 2 ? "both bridges" : names.join(" and ");
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Check if a bridge is effectively enabled (gate resolved).
|
|
148
|
-
*/
|
|
149
|
-
function bridgeEffectivelyEnabled(bridge: BridgeKey): boolean {
|
|
150
|
-
return bridge === "webSearch" ? webSearchBridgeEnabled() : mediaMcpEnabled();
|
|
92
|
+
/** Parse an optional target argument; no target = media server. */
|
|
93
|
+
function parseTarget(token: string | undefined): string | undefined {
|
|
94
|
+
if (token === undefined) return "mediaMcp";
|
|
95
|
+
return TARGETS[token.toLowerCase()];
|
|
151
96
|
}
|
|
152
97
|
|
|
153
98
|
export function registerNanMcpCommand(pi: ExtensionAPI): void {
|
|
154
99
|
pi.registerCommand("nan-mcp", {
|
|
155
100
|
description:
|
|
156
|
-
"NaN MCP configuration: enable/disable/status for
|
|
101
|
+
"NaN MCP configuration: enable/disable/status for the community media server",
|
|
157
102
|
getArgumentCompletions: (argumentPrefix: string) => {
|
|
158
103
|
const prefix = argumentPrefix.trim().toLowerCase();
|
|
159
104
|
const items = [
|
|
160
|
-
{ value: "enable", label: "enable", description: "Enable
|
|
161
|
-
{ value: "disable", label: "disable", description: "Disable
|
|
105
|
+
{ value: "enable", label: "enable", description: "Enable the media server and persist it" },
|
|
106
|
+
{ value: "disable", label: "disable", description: "Disable the media server persistently" },
|
|
162
107
|
{ value: "status", label: "status", description: "Show current server status and registration" },
|
|
163
|
-
{ value: "enable web-search", label: "enable web-search", description: "Enable the official NaN web-search server" },
|
|
164
108
|
{ value: "enable nan-mcp-server", label: "enable nan-mcp-server", description: "Enable the community media server" },
|
|
165
109
|
];
|
|
166
110
|
const filtered = items.filter((item) => item.value.startsWith(prefix));
|
|
@@ -181,20 +125,17 @@ export function registerNanMcpCommand(pi: ExtensionAPI): void {
|
|
|
181
125
|
if (parsed === undefined || rest.length > 0) {
|
|
182
126
|
ctx.ui.notify(
|
|
183
127
|
parsed === undefined
|
|
184
|
-
? `Unknown target "${[rawTarget, ...rest].filter(Boolean).join(" ")}".
|
|
128
|
+
? `Unknown target "${[rawTarget, ...rest].filter(Boolean).join(" ")}". Target: nan-mcp-server (or omit).`
|
|
185
129
|
: USAGE,
|
|
186
130
|
"warning",
|
|
187
131
|
);
|
|
188
132
|
return;
|
|
189
133
|
}
|
|
190
|
-
const { bridges, target } = parsed;
|
|
191
134
|
const enabled = subcommand === "enable";
|
|
135
|
+
const where = rawTarget ? `for "${rawTarget.toLowerCase()}"` : "for the media server";
|
|
192
136
|
|
|
193
|
-
|
|
194
|
-
writeBridgeState(bridge, enabled);
|
|
195
|
-
}
|
|
137
|
+
writeBridgeState("mediaMcp", enabled);
|
|
196
138
|
|
|
197
|
-
const where = target ? `for "${target}"` : "for both bridges";
|
|
198
139
|
const persistence = `persisted in <agentDir>/${NAN_STATE_FILE}`;
|
|
199
140
|
|
|
200
141
|
// Without an extension that handles `mcp_servers_change` pi cannot connect the
|
|
@@ -203,84 +144,56 @@ export function registerNanMcpCommand(pi: ExtensionAPI): void {
|
|
|
203
144
|
// so it applies as soon as a connector (built-in `mcp`) is available.
|
|
204
145
|
if (enabled && !hasMcpConnector(pi)) {
|
|
205
146
|
ctx.ui.notify(
|
|
206
|
-
`Enabled ${
|
|
207
|
-
connectorMissingMessage(
|
|
147
|
+
`Enabled ${where}, ${persistence} — NOT registered. ` +
|
|
148
|
+
connectorMissingMessage([MCP_SERVER_NAMES.mediaMcp]),
|
|
208
149
|
"warning",
|
|
209
150
|
);
|
|
210
151
|
return;
|
|
211
152
|
}
|
|
212
153
|
|
|
213
154
|
// Immediate native registration/unregistration.
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if (
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
} else {
|
|
238
|
-
// Resolve key for media: stored credential → registry → env.
|
|
239
|
-
const registryKey = await tryResolveNanApiKeyViaRegistry(ctx.modelRegistry);
|
|
240
|
-
const storedKey = resolveStoredNanApiKey();
|
|
241
|
-
const apiKey = storedKey ?? registryKey ?? process.env[NAN_API_KEY_ENV];
|
|
242
|
-
if (apiKey) {
|
|
243
|
-
const mediaArgs = mediaMcpCommand();
|
|
244
|
-
pi.registerMcpServer(name, {
|
|
245
|
-
type: "stdio",
|
|
246
|
-
command: mediaArgs[0]!,
|
|
247
|
-
args: mediaArgs.slice(1),
|
|
248
|
-
env: { [NAN_API_KEY_ENV]: apiKey },
|
|
249
|
-
exposure: "direct",
|
|
250
|
-
timeout: mediaMcpTimeoutSec(),
|
|
251
|
-
});
|
|
252
|
-
} else {
|
|
253
|
-
ctx.ui.notify(
|
|
254
|
-
`${NAN_API_KEY_ENV} is not set. Export it or run /login nan.`,
|
|
255
|
-
"warning",
|
|
256
|
-
);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
155
|
+
const name = MCP_SERVER_NAMES.mediaMcp;
|
|
156
|
+
if (enabled) {
|
|
157
|
+
if (mediaMcpEnabled()) {
|
|
158
|
+
const registryKey = await tryResolveNanApiKeyViaRegistry(ctx.modelRegistry);
|
|
159
|
+
const storedKey = resolveStoredNanApiKey();
|
|
160
|
+
const apiKey = storedKey ?? registryKey ?? process.env[NAN_API_KEY_ENV];
|
|
161
|
+
if (apiKey) {
|
|
162
|
+
const mediaArgs = mediaMcpCommand();
|
|
163
|
+
pi.registerMcpServer(name, {
|
|
164
|
+
type: "stdio",
|
|
165
|
+
command: mediaArgs[0]!,
|
|
166
|
+
args: mediaArgs.slice(1),
|
|
167
|
+
env: { [NAN_API_KEY_ENV]: apiKey },
|
|
168
|
+
exposure: "direct",
|
|
169
|
+
timeout: mediaMcpTimeoutSec(),
|
|
170
|
+
});
|
|
171
|
+
} else {
|
|
172
|
+
ctx.ui.notify(
|
|
173
|
+
`${NAN_API_KEY_ENV} is not set. Export it or run /login nan.`,
|
|
174
|
+
"warning",
|
|
175
|
+
);
|
|
259
176
|
}
|
|
260
|
-
} else {
|
|
261
|
-
pi.unregisterMcpServer(name);
|
|
262
177
|
}
|
|
178
|
+
} else {
|
|
179
|
+
pi.unregisterMcpServer(name);
|
|
263
180
|
}
|
|
264
181
|
|
|
265
182
|
if (enabled) {
|
|
266
|
-
|
|
267
|
-
const allRegistered = bridges.every((b: BridgeKey) =>
|
|
268
|
-
nativeServerRegistered(pi, serverName(b)),
|
|
269
|
-
);
|
|
270
|
-
if (allRegistered) {
|
|
183
|
+
if (nativeServerRegistered(pi, name)) {
|
|
271
184
|
ctx.ui.notify(
|
|
272
|
-
`Enabled ${
|
|
185
|
+
`Enabled ${where}, ${persistence}. Native server registered for this session.`,
|
|
273
186
|
"info",
|
|
274
187
|
);
|
|
275
188
|
} else {
|
|
276
189
|
ctx.ui.notify(
|
|
277
|
-
`Enabled ${
|
|
190
|
+
`Enabled ${where}, ${persistence} — API key missing; server not registered.`,
|
|
278
191
|
"warning",
|
|
279
192
|
);
|
|
280
193
|
}
|
|
281
194
|
} else {
|
|
282
195
|
ctx.ui.notify(
|
|
283
|
-
`Disabled ${
|
|
196
|
+
`Disabled ${where}, ${persistence}. Native server unregistered — tools hidden immediately.`,
|
|
284
197
|
"warning",
|
|
285
198
|
);
|
|
286
199
|
}
|
|
@@ -27,14 +27,53 @@
|
|
|
27
27
|
* provider stream). It therefore sees the original `thinking` blocks and can
|
|
28
28
|
* remove them before they become text.
|
|
29
29
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* UPSTREAM UPDATE (pi-ai 1.3.0, verified 2026-10-15): `transformMessages` now
|
|
31
|
+
* drops cross-model `thinking` blocks itself (`if (!isSameModel) return []`),
|
|
32
|
+
* which is the upstream fix this guard was mitigating. The guard is therefore
|
|
33
|
+
* GATED BY VERSION: it only registers when the host's pi-ai is older than
|
|
34
|
+
* {@link CROSS_MODEL_GUARD_PI_AI_CEILING} (1.3.0). An unknown host version
|
|
35
|
+
* keeps the guard on — the conservative default, since the guard is harmless
|
|
36
|
+
* where pi-ai already drops the blocks and essential where it still replays
|
|
37
|
+
* them.
|
|
38
|
+
*
|
|
39
|
+
* DEPRECATED — REMOVE THIS MODULE when the peer range floor rises to
|
|
40
|
+
* `>=1.3.0`: at that point every supported pi ships the upstream drop and this
|
|
41
|
+
* file (plus the `NAN_THINKING_GUARD` opt-out) is dead code. Users on
|
|
42
|
+
* pi < 1.3.0 should update pi to keep getting the mitigation for free.
|
|
33
43
|
*/
|
|
34
44
|
|
|
45
|
+
import { parsePiAiVersion } from "./pi-ai-version.ts";
|
|
46
|
+
|
|
35
47
|
/** Env var that opts out of the guard (`0`, `false`, `no` or `off`). Default: enabled. */
|
|
36
48
|
export const NAN_THINKING_GUARD_ENV = "NAN_THINKING_GUARD";
|
|
37
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Exclusive pi-ai version ceiling for this guard: it registers only while the
|
|
52
|
+
* host's pi-ai is OLDER than this version. pi-ai 1.3.0 drops cross-model
|
|
53
|
+
* thinking in `transformMessages` (upstream fix for issue #3), so from 1.3.0
|
|
54
|
+
* on the guard is redundant. Remove this constant together with the module
|
|
55
|
+
* once the peer range floor reaches this version.
|
|
56
|
+
*/
|
|
57
|
+
export const CROSS_MODEL_GUARD_PI_AI_CEILING = "1.3.0";
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Whether the guard still applies to a host running the given pi-ai version.
|
|
61
|
+
* `undefined`/`null` (unknown host version) and unparseable versions return
|
|
62
|
+
* `true` — the conservative older-pi default. `false` means the host's pi-ai
|
|
63
|
+
* already drops cross-model thinking itself and the guard must not register.
|
|
64
|
+
*/
|
|
65
|
+
export function crossModelThinkingGuardCovers(piAiVersion: string | null | undefined): boolean {
|
|
66
|
+
if (piAiVersion === null || piAiVersion === undefined) return true;
|
|
67
|
+
const parsed = parsePiAiVersion(piAiVersion);
|
|
68
|
+
if (!parsed) return true;
|
|
69
|
+
const ceiling = parsePiAiVersion(CROSS_MODEL_GUARD_PI_AI_CEILING);
|
|
70
|
+
if (!ceiling) return true;
|
|
71
|
+
return (
|
|
72
|
+
parsed.major < ceiling.major ||
|
|
73
|
+
(parsed.major === ceiling.major && parsed.minor < ceiling.minor)
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
38
77
|
export interface CrossModelThinkingGuardOptions {
|
|
39
78
|
/** Provider ids this guard applies to (the caller's registered providers). */
|
|
40
79
|
providerIds: ReadonlySet<string>;
|
package/src/index.ts
CHANGED
|
@@ -15,17 +15,13 @@
|
|
|
15
15
|
* 0.84 alike), and the openai-completions streaming implementation is
|
|
16
16
|
* resolved dynamically — see provider-factory.ts for why.
|
|
17
17
|
*
|
|
18
|
-
* 2. MCP
|
|
19
|
-
* - `nan-search` — the official NaN remote MCP server
|
|
20
|
-
* (https://api.nan.builders/mcp), exposing `mcp__nan_search__web_search`
|
|
21
|
-
* (pi >=0.99.2; 0.99.0-0.99.1 kept the hyphen: `mcp__nan-search__web_search`).
|
|
22
|
-
* Default: enabled. NAN_MCP_TOOLS=0 to disable.
|
|
18
|
+
* 2. MCP server via pi's native registerMcpServer (pi >=0.99):
|
|
23
19
|
* - `nan-media` — the community stdio media server
|
|
24
20
|
* (flux-2-klein / kokoro / whisper), exposing
|
|
25
21
|
* `mcp__nan_media__generate_image` etc. Default: enabled,
|
|
26
22
|
* NAN_MEDIA_MCP=0 to disable.
|
|
27
23
|
*
|
|
28
|
-
*
|
|
24
|
+
* It is session-scoped (visible in /mcp with source "extension").
|
|
29
25
|
* Exposure: `direct` — tools are declared to the model like built-ins.
|
|
30
26
|
*/
|
|
31
27
|
|
|
@@ -34,13 +30,15 @@ import type { Provider } from "@earendil-works/pi-ai";
|
|
|
34
30
|
import { registerNanMcpCommand } from "./commands.ts";
|
|
35
31
|
import { registerNanUsageCommand } from "./usage.ts";
|
|
36
32
|
import {
|
|
33
|
+
crossModelThinkingGuardCovers,
|
|
37
34
|
crossModelThinkingGuardEnabled,
|
|
38
35
|
stripCrossModelThinking,
|
|
39
36
|
} from "./cross-model-thinking-guard.ts";
|
|
37
|
+
import { resolvePiAiVersion } from "./pi-ai-version.ts";
|
|
40
38
|
import { baselineModels } from "./fetch-models.ts";
|
|
41
39
|
import { createNanCompatibleProvider, type OpenAICompatibleProviderConfig } from "./provider-factory.ts";
|
|
42
40
|
import { PROVIDERS } from "./providers.ts";
|
|
43
|
-
import { NAN_API_KEY_ENV,
|
|
41
|
+
import { NAN_API_KEY_ENV, resolveNanApiKey, tryResolveNanApiKeyViaRegistry, resolveStoredNanApiKey } from "./mcp/api-key.ts";
|
|
44
42
|
import { mediaMcpCommand, mediaMcpEnabled, mediaMcpEnvExplicit, mediaMcpEnvTruthy, mediaMcpTimeoutSec } from "./mcp/media-server.ts";
|
|
45
43
|
import { registerMcpHostGuard } from "./mcp/host-support.ts";
|
|
46
44
|
|
|
@@ -95,26 +93,6 @@ async function registerProviderCompat(
|
|
|
95
93
|
const NO_NATIVE_MCP_GUARD_MSG =
|
|
96
94
|
"pi-nan-provider 0.10+ requires pi >= 0.99 for MCP tools (native MCP); upgrade pi or stay on package 0.9.x";
|
|
97
95
|
|
|
98
|
-
/** Register the official NaN web-search MCP server (session-scoped). */
|
|
99
|
-
async function registerWebSearchMcpServer(pi: ExtensionAPI, apiKey: string | undefined): Promise<boolean> {
|
|
100
|
-
if (!webSearchBridgeEnabled()) return false;
|
|
101
|
-
if (!apiKey) {
|
|
102
|
-
console.warn(
|
|
103
|
-
"[pi-nan-provider] NAN_API_KEY is not set — web-search server not registered. " +
|
|
104
|
-
"Run `/login nan` in pi or export `NAN_API_KEY=<your-key>`.",
|
|
105
|
-
);
|
|
106
|
-
return false;
|
|
107
|
-
}
|
|
108
|
-
pi.registerMcpServer("nan-search", {
|
|
109
|
-
type: "http",
|
|
110
|
-
url: "https://api.nan.builders/mcp",
|
|
111
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
112
|
-
exposure: "direct",
|
|
113
|
-
description: "Web search through NaN's gateway (official NaN MCP server)",
|
|
114
|
-
});
|
|
115
|
-
return true;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
96
|
/** Register the community media stdio MCP server (session-scoped). */
|
|
119
97
|
function registerMediaMcpServer(pi: ExtensionAPI, apiKey: string | undefined): boolean {
|
|
120
98
|
if (!mediaMcpEnabled()) return false;
|
|
@@ -139,11 +117,11 @@ function registerMediaMcpServer(pi: ExtensionAPI, apiKey: string | undefined): b
|
|
|
139
117
|
}
|
|
140
118
|
|
|
141
119
|
/**
|
|
142
|
-
* Register MCP
|
|
120
|
+
* Register the MCP server via pi's native registerMcpServer.
|
|
143
121
|
*
|
|
144
|
-
* Session-scoped:
|
|
145
|
-
* visible to the model (exposure: "direct"), and user mcp.json
|
|
146
|
-
* with the same name
|
|
122
|
+
* Session-scoped: the server appears in /mcp with source "extension", is
|
|
123
|
+
* visible to the model (exposure: "direct"), and a user mcp.json entry
|
|
124
|
+
* with the same name takes precedence.
|
|
147
125
|
*
|
|
148
126
|
* On pi <0.99 (registerMcpServer absent) this does NOT bridge — it
|
|
149
127
|
* notifies once and skips (same loud-skip style as existing guards).
|
|
@@ -162,7 +140,6 @@ async function registerMcpServersNative(pi: ExtensionAPI): Promise<void> {
|
|
|
162
140
|
const storedKey = resolveStoredNanApiKey();
|
|
163
141
|
const apiKey = storedKey ?? process.env[NAN_API_KEY_ENV];
|
|
164
142
|
|
|
165
|
-
await registerWebSearchMcpServer(pi, apiKey);
|
|
166
143
|
registerMediaMcpServer(pi, apiKey);
|
|
167
144
|
}
|
|
168
145
|
|
|
@@ -171,21 +148,34 @@ async function registerMcpServersNative(pi: ExtensionAPI): Promise<void> {
|
|
|
171
148
|
/**
|
|
172
149
|
* Drop the reasoning pi-ai replays across a model switch.
|
|
173
150
|
*
|
|
174
|
-
* pi-ai
|
|
175
|
-
* to plain text with no size bound (verified on 0.87.1), and
|
|
176
|
-
* bounds the sum across messages — measured at 30–60% of the whole
|
|
177
|
-
* real sessions. Switching from a 1M-context model to a 262K one
|
|
178
|
-
* then overflows the window, and NaN's gateway answers a generic
|
|
151
|
+
* On pi-ai < 1.3.0, `transformMessages` downgrades a previous model's
|
|
152
|
+
* `thinking` blocks to plain text with no size bound (verified on 0.87.1), and
|
|
153
|
+
* nothing bounds the sum across messages — measured at 30–60% of the whole
|
|
154
|
+
* context on real sessions. Switching from a 1M-context model to a 262K one
|
|
155
|
+
* (`qwen3.6`) then overflows the window, and NaN's gateway answers a generic
|
|
179
156
|
* `400 Invalid request. Check your request parameters.` This hook runs before
|
|
180
157
|
* pi-ai converts the blocks, so removing the cross-model reasoning here keeps
|
|
181
158
|
* the replayed context small. Answers and tool results are untouched. See
|
|
182
159
|
* src/cross-model-thinking-guard.ts.
|
|
183
160
|
*
|
|
161
|
+
* VERSION-GATED: pi-ai 1.3.0 drops cross-model thinking itself (upstream fix
|
|
162
|
+
* for issue #3), so the guard only registers when the host's resolved pi-ai is
|
|
163
|
+
* older than 1.3.0; an undeterminable host version keeps it on (conservative
|
|
164
|
+
* older-pi default). DEPRECATED — remove this whole guard once the peer range
|
|
165
|
+
* floor rises to `>=1.3.0`; users on pi < 1.3.0 should update pi.
|
|
166
|
+
*
|
|
184
167
|
* Scope: only requests targeting this package's providers are touched, and
|
|
185
168
|
* only messages from a DIFFERENT model — same-model reasoning is never removed.
|
|
186
169
|
*/
|
|
187
|
-
export function registerCrossModelThinkingGuard(
|
|
170
|
+
export function registerCrossModelThinkingGuard(
|
|
171
|
+
pi: ExtensionAPI,
|
|
172
|
+
options: { piAiVersion?: string | null } = {},
|
|
173
|
+
): void {
|
|
188
174
|
if (typeof pi.on !== "function") return; // old pi without the context hook
|
|
175
|
+
// `null` forces the unknown-version path (guard on); omitted resolves the
|
|
176
|
+
// host's pi-ai version at runtime; a string pins the version (tests).
|
|
177
|
+
const piAiVersion = options.piAiVersion !== undefined ? options.piAiVersion : resolvePiAiVersion();
|
|
178
|
+
if (!crossModelThinkingGuardCovers(piAiVersion)) return;
|
|
189
179
|
const providerIds = new Set(PROVIDERS.map((provider) => provider.id));
|
|
190
180
|
pi.on("context", (event, ctx) => {
|
|
191
181
|
if (!crossModelThinkingGuardEnabled()) return;
|
package/src/mcp/api-key.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { bridgeSource, resolveBridgeEnabled } from "./state.ts";
|
|
2
1
|
import { readStoredCredential } from "@earendil-works/pi-coding-agent";
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
|
-
* NaN API key resolution — shared across every surface (
|
|
4
|
+
* NaN API key resolution — shared across every surface (media, usage).
|
|
6
5
|
*
|
|
7
6
|
* Strategy: pi's stored credential (modelRegistry) wins → env var fallback.
|
|
8
7
|
* Never logs or embeds the key beyond the Authorization header.
|
|
@@ -40,8 +39,6 @@ export function resolveStoredNanApiKey(): string | undefined {
|
|
|
40
39
|
}
|
|
41
40
|
}
|
|
42
41
|
|
|
43
|
-
/** Env var that overrides the official web_search bridge. */
|
|
44
|
-
export const NAN_MCP_TOOLS_ENV = "NAN_MCP_TOOLS";
|
|
45
42
|
/** Env var that overrides the media bridge. */
|
|
46
43
|
export const NAN_MEDIA_MCP_ENV = "NAN_MEDIA_MCP";
|
|
47
44
|
/** The NaN API key env var used everywhere. */
|
|
@@ -74,18 +71,6 @@ export async function tryResolveNanApiKeyViaRegistry(
|
|
|
74
71
|
}
|
|
75
72
|
}
|
|
76
73
|
|
|
77
|
-
/** Whether MCP tool registration is disabled via env (NAN_MCP_TOOLS=0|false|off). */
|
|
78
|
-
export function mcpToolsDisabled(): boolean {
|
|
79
|
-
const value = process.env[NAN_MCP_TOOLS_ENV]?.trim().toLowerCase();
|
|
80
|
-
return value === "0" || value === "false" || value === "off";
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** Whether NAN_MCP_TOOLS is explicitly set (any value). */
|
|
84
|
-
export function mcpToolsEnvExplicit(): boolean {
|
|
85
|
-
const value = process.env[NAN_MCP_TOOLS_ENV];
|
|
86
|
-
return value !== undefined && value.trim() !== "";
|
|
87
|
-
}
|
|
88
|
-
|
|
89
74
|
/** Whether NAN_MEDIA_MCP is explicitly set (any value). */
|
|
90
75
|
export function mediaMcpEnvExplicit(): boolean {
|
|
91
76
|
const value = process.env[NAN_MEDIA_MCP_ENV];
|
|
@@ -97,13 +82,3 @@ export function mediaMcpEnvTruthy(): boolean {
|
|
|
97
82
|
const value = process.env[NAN_MEDIA_MCP_ENV]?.trim().toLowerCase();
|
|
98
83
|
return value === "1" || value === "true" || value === "on";
|
|
99
84
|
}
|
|
100
|
-
|
|
101
|
-
/** Source of the effective web-search enablement (env / persisted / default). */
|
|
102
|
-
export function webSearchBridgeSource(): import("./state.ts").BridgeSource {
|
|
103
|
-
return bridgeSource("webSearch", mcpToolsEnvExplicit());
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** Whether the web-search bridge is enabled by gate (env → persisted → default). */
|
|
107
|
-
export function webSearchBridgeEnabled(): boolean {
|
|
108
|
-
return resolveBridgeEnabled("webSearch", mcpToolsEnvExplicit(), !mcpToolsDisabled(), true);
|
|
109
|
-
}
|
package/src/mcp/state.ts
CHANGED
|
@@ -17,8 +17,6 @@ import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
|
17
17
|
export const NAN_STATE_FILE = "nan-provider.json";
|
|
18
18
|
|
|
19
19
|
export interface NanProviderState {
|
|
20
|
-
/** Persisted enablement of the official web_search MCP bridge. */
|
|
21
|
-
webSearch?: boolean;
|
|
22
20
|
/** Persisted enablement of the community nan-mcp-server media bridge. */
|
|
23
21
|
mediaMcp?: boolean;
|
|
24
22
|
}
|
|
@@ -27,7 +25,6 @@ export type BridgeKey = keyof NanProviderState;
|
|
|
27
25
|
|
|
28
26
|
/** Server name each bridge registers with `pi.registerMcpServer()`. Single source of truth. */
|
|
29
27
|
export const MCP_SERVER_NAMES: Record<BridgeKey, string> = {
|
|
30
|
-
webSearch: "nan-search",
|
|
31
28
|
mediaMcp: "nan-media",
|
|
32
29
|
};
|
|
33
30
|
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime pi-ai version detection.
|
|
3
|
+
*
|
|
4
|
+
* Resolves the `@earendil-works/pi-ai` package instance the HOST loaded — the
|
|
5
|
+
* same host-anchored strategy `src/pi-ai-loader.ts` uses, because an
|
|
6
|
+
* extension-relative resolve can return a stale copy from the extension's own
|
|
7
|
+
* npm tree (issue #8) — and reads that copy's package.json version. The result
|
|
8
|
+
* feeds version-gated behavior, currently the cross-model thinking guard
|
|
9
|
+
* (registered only while the host's pi-ai is older than 1.3.0, where pi-ai
|
|
10
|
+
* still replays cross-model thinking as unbounded text).
|
|
11
|
+
*
|
|
12
|
+
* Failure is NOT an error: an unresolvable package or an unreadable version
|
|
13
|
+
* returns `undefined`, and callers fall back to the conservative older-pi
|
|
14
|
+
* behavior (the guard stays on — harmless on modern hosts, essential on old
|
|
15
|
+
* ones). Guarded by `test/pi-ai-version.test.ts`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readFileSync } from "node:fs";
|
|
19
|
+
import { dirname, join } from "node:path";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
import { hostAnchorUrl, PI_AI_PACKAGE_SPECIFIER, resolvePiAiSpecifier } from "./pi-ai-loader.ts";
|
|
22
|
+
|
|
23
|
+
/** A parsed semver triple (`1.3.0-beta.1` → `{ major: 1, minor: 3, patch: 0 }`). */
|
|
24
|
+
export interface PiAiVersion {
|
|
25
|
+
major: number;
|
|
26
|
+
minor: number;
|
|
27
|
+
patch: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Parse a semver string; prerelease/build suffixes are ignored. */
|
|
31
|
+
export function parsePiAiVersion(version: string): PiAiVersion | undefined {
|
|
32
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(version.trim());
|
|
33
|
+
if (!match) return undefined;
|
|
34
|
+
return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]) };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Read the pi-ai version from a resolved specifier (path or `file://` URL) of
|
|
39
|
+
* any entry file inside the package. Walks up from the entry's directory until
|
|
40
|
+
* a package.json whose `name` is exactly `@earendil-works/pi-ai` is found —
|
|
41
|
+
* matching the name prevents picking up an unrelated package.json in a
|
|
42
|
+
* workspace layout — and returns its `version`. Bounded walk (8 levels);
|
|
43
|
+
* `undefined` when nothing matches.
|
|
44
|
+
*/
|
|
45
|
+
export function piAiVersionFromResolvedEntry(entry: string): string | undefined {
|
|
46
|
+
let dir: string;
|
|
47
|
+
try {
|
|
48
|
+
dir = dirname(entry.startsWith("file://") ? fileURLToPath(entry) : entry);
|
|
49
|
+
} catch {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
53
|
+
try {
|
|
54
|
+
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf-8")) as {
|
|
55
|
+
name?: unknown;
|
|
56
|
+
version?: unknown;
|
|
57
|
+
};
|
|
58
|
+
if (pkg.name === PI_AI_PACKAGE_SPECIFIER && typeof pkg.version === "string") {
|
|
59
|
+
return pkg.version;
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
// No readable package.json at this level — keep walking up.
|
|
63
|
+
}
|
|
64
|
+
const parent = dirname(dir);
|
|
65
|
+
if (parent === dir) return undefined;
|
|
66
|
+
dir = parent;
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Cache for {@link resolvePiAiVersion}; resolution runs once per process. */
|
|
72
|
+
let cachedVersion: string | undefined;
|
|
73
|
+
let resolved = false;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Version of the pi-ai instance the host loaded, or `undefined` when it cannot
|
|
77
|
+
* be determined (unresolvable specifier, unreadable package.json, odd runtime).
|
|
78
|
+
* Resolution happens once and is cached for the process lifetime.
|
|
79
|
+
*/
|
|
80
|
+
export function resolvePiAiVersion(): string | undefined {
|
|
81
|
+
if (resolved) return cachedVersion;
|
|
82
|
+
resolved = true;
|
|
83
|
+
try {
|
|
84
|
+
const entry = resolvePiAiSpecifier(PI_AI_PACKAGE_SPECIFIER, {
|
|
85
|
+
anchorUrl: hostAnchorUrl(process.argv[1]),
|
|
86
|
+
});
|
|
87
|
+
cachedVersion = piAiVersionFromResolvedEntry(entry);
|
|
88
|
+
} catch {
|
|
89
|
+
cachedVersion = undefined;
|
|
90
|
+
}
|
|
91
|
+
return cachedVersion;
|
|
92
|
+
}
|