@gtrabanco/pi-nan-provider 0.7.0 → 0.10.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 +70 -23
- package/README.es.md +67 -37
- package/README.md +66 -40
- package/package.json +5 -5
- package/scripts/check-nan-mcp-server.ts +11 -3
- package/scripts/check-pi-sdk-versions.mjs +392 -0
- package/scripts/generate-models.ts +64 -12
- package/scripts/manual-overrides.ts +20 -8
- package/scripts/models.generated.ts +55 -52
- package/src/commands.ts +132 -52
- package/src/context-overflow-classifier.ts +1 -1
- package/src/cross-model-thinking-guard.ts +1 -1
- package/src/index.ts +81 -41
- package/src/mcp/api-key.ts +76 -0
- package/src/mcp/media-server.ts +56 -0
- package/src/provider-factory.ts +7 -2
- package/src/usage.ts +352 -175
- package/src/mcp/nan-media.ts +0 -236
- package/src/mcp/nan-search.ts +0 -241
- package/src/mcp/stdio-client.ts +0 -222
|
@@ -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-09-
|
|
4
|
+
// Source: https://models.dev/api.json (provider "nan"), fetched 2026-09-29T22:08:21.449Z
|
|
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.
|
|
@@ -71,7 +71,8 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
|
|
|
71
71
|
"cost": {
|
|
72
72
|
"input": 0,
|
|
73
73
|
"output": 0
|
|
74
|
-
}
|
|
74
|
+
},
|
|
75
|
+
"canonical_model_id": "deepseek/deepseek-v4.1-flash"
|
|
75
76
|
}
|
|
76
77
|
},
|
|
77
78
|
{
|
|
@@ -117,7 +118,14 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
|
|
|
117
118
|
"reasoning": true,
|
|
118
119
|
"reasoning_options": [
|
|
119
120
|
{
|
|
120
|
-
"type": "
|
|
121
|
+
"type": "effort",
|
|
122
|
+
"values": [
|
|
123
|
+
"none",
|
|
124
|
+
"low",
|
|
125
|
+
"medium",
|
|
126
|
+
"high",
|
|
127
|
+
"max"
|
|
128
|
+
]
|
|
121
129
|
}
|
|
122
130
|
],
|
|
123
131
|
"tool_call": true,
|
|
@@ -142,7 +150,8 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
|
|
|
142
150
|
"cost": {
|
|
143
151
|
"input": 0,
|
|
144
152
|
"output": 0
|
|
145
|
-
}
|
|
153
|
+
},
|
|
154
|
+
"canonical_model_id": "google/gemma-4-26b-a4b-it"
|
|
146
155
|
}
|
|
147
156
|
},
|
|
148
157
|
{
|
|
@@ -184,7 +193,17 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
|
|
|
184
193
|
"family": "glm-flash",
|
|
185
194
|
"attachment": true,
|
|
186
195
|
"reasoning": true,
|
|
187
|
-
"reasoning_options": [
|
|
196
|
+
"reasoning_options": [
|
|
197
|
+
{
|
|
198
|
+
"type": "effort",
|
|
199
|
+
"values": [
|
|
200
|
+
"low",
|
|
201
|
+
"medium",
|
|
202
|
+
"high",
|
|
203
|
+
"max"
|
|
204
|
+
]
|
|
205
|
+
}
|
|
206
|
+
],
|
|
188
207
|
"tool_call": true,
|
|
189
208
|
"structured_output": true,
|
|
190
209
|
"temperature": true,
|
|
@@ -207,12 +226,13 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
|
|
|
207
226
|
"cost": {
|
|
208
227
|
"input": 0,
|
|
209
228
|
"output": 0
|
|
210
|
-
}
|
|
229
|
+
},
|
|
230
|
+
"canonical_model_id": "zhipuai/glm-5.3-flash"
|
|
211
231
|
}
|
|
212
232
|
},
|
|
213
233
|
{
|
|
214
|
-
"id": "mimo-v2.
|
|
215
|
-
"name": "MiMo-V2.
|
|
234
|
+
"id": "mimo-v2.6-flash",
|
|
235
|
+
"name": "MiMo-V2.6-Flash",
|
|
216
236
|
"reasoning": true,
|
|
217
237
|
"input": [
|
|
218
238
|
"text",
|
|
@@ -235,21 +255,21 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
|
|
|
235
255
|
"maxTokensField": "max_tokens"
|
|
236
256
|
},
|
|
237
257
|
"notes": [
|
|
238
|
-
"compat matches the maintainer's working ~/.pi/agent/models.json LiteLLM config for api.nan.builders (2026-09-04): supportsDeveloperRole false, supportsReasoningEffort true, maxTokensField max_tokens. NaN's docs example sets only supportsDeveloperRole: true and is not battle-tested. supportsFinishReason true (2026-09-13, issue #2): the LiteLLM gateway intermittently closes SSE streams before emitting finish_reason; with true pi-ai raises 'Stream ended without finish_reason', which matches pi-ai's retryable-provider pattern ('ended without') and is retried automatically, whereas false silently synthesized stop/toolUse and stalled the turn mid-answer. supportsUsageInStreaming true (2026-09-16, issue #7): NaN's published schema is silent about stream_options, but the live gateway honors it — two identical streaming calls per model, differing only in stream_options: { include_usage: true }, returned 0 usage chunks without it and exactly 1 with it (prompt/completion/reasoning/cached token counts) on deepseek-v4-flash, glm5.3-flash, qwen3.6, mimo-v2.5 and gemma4, and a real pi session then recorded token counts where it recorded zeros. pi-ai only sends stream_options when this is not false, and the sanitizer forwards it when the model declares true, so chat models opt in by default and usage is reported (issue #4). A model that does not report streaming usage can still opt out per model with a models.json compat override (supportsUsageInStreaming: false); the sanitizer then strips stream_options and the payload stays strict."
|
|
258
|
+
"compat matches the maintainer's working ~/.pi/agent/models.json LiteLLM config for api.nan.builders (2026-09-04): supportsDeveloperRole false, supportsReasoningEffort true, maxTokensField max_tokens. NaN's docs example sets only supportsDeveloperRole: true and is not battle-tested. supportsFinishReason true (2026-09-13, issue #2): the LiteLLM gateway intermittently closes SSE streams before emitting finish_reason; with true pi-ai raises 'Stream ended without finish_reason', which matches pi-ai's retryable-provider pattern ('ended without') and is retried automatically, whereas false silently synthesized stop/toolUse and stalled the turn mid-answer. supportsUsageInStreaming true (2026-09-16, issue #7): NaN's published schema is silent about stream_options, but the live gateway honors it — two identical streaming calls per model, differing only in stream_options: { include_usage: true }, returned 0 usage chunks without it and exactly 1 with it (prompt/completion/reasoning/cached token counts) on deepseek-v4-flash, glm5.3-flash, qwen3.6, mimo-v2.5 and gemma4, and a real pi session then recorded token counts where it recorded zeros. pi-ai only sends stream_options when this is not false, and the sanitizer forwards it when the model declares true, so chat models opt in by default and usage is reported (issue #4). A model that does not report streaming usage can still opt out per model with a models.json compat override (supportsUsageInStreaming: false); the sanitizer then strips stream_options and the payload stays strict.",
|
|
259
|
+
"manual-only: \"mimo-v2.6-flash\" skipped: models.dev provider \"nan\" now lists it, so the manual-only entry is not emitted and the models.dev values are used (omnimodal model (text, image, audio input) served by NaN; limits and modalities from https://nan.builders/docs/models#mimo-v2-6-flash (checked 2026-09-25, quotas re-checked 2026-09-29): 1,048,576 context / 131,072 max output / 1.0B monthly quota per member. Reasoning: accepted, depth not adjustable (docs table, checked 2026-09-29). Tool calling: yes. Streaming: yes. Input modalities (pi-representable): text, image (audio not representable in pi's Model type). Fallback entry: emitted only while models.dev provider nan does not list the model — models.dev started listing it on 2026-09-29, so the generator then keeps the models.dev values and attaches this note to that entry instead of emitting a second one.)"
|
|
239
260
|
],
|
|
240
261
|
"extras": {
|
|
241
|
-
"id": "mimo-v2.
|
|
242
|
-
"name": "MiMo-V2.
|
|
243
|
-
"description": "
|
|
262
|
+
"id": "mimo-v2.6-flash",
|
|
263
|
+
"name": "MiMo-V2.6-Flash",
|
|
264
|
+
"description": "MiMo Flash model for multimodal coding agents and long-context automation",
|
|
244
265
|
"family": "mimo",
|
|
245
266
|
"attachment": true,
|
|
246
267
|
"reasoning": true,
|
|
247
268
|
"reasoning_options": [],
|
|
248
269
|
"tool_call": true,
|
|
249
270
|
"temperature": true,
|
|
250
|
-
"
|
|
251
|
-
"
|
|
252
|
-
"last_updated": "2026-04-22",
|
|
271
|
+
"release_date": "2026-09-22",
|
|
272
|
+
"last_updated": "2026-09-22",
|
|
253
273
|
"modalities": {
|
|
254
274
|
"input": [
|
|
255
275
|
"text",
|
|
@@ -268,38 +288,10 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
|
|
|
268
288
|
"cost": {
|
|
269
289
|
"input": 0,
|
|
270
290
|
"output": 0
|
|
271
|
-
}
|
|
291
|
+
},
|
|
292
|
+
"canonical_model_id": "xiaomi/mimo-v2.6-flash"
|
|
272
293
|
}
|
|
273
294
|
},
|
|
274
|
-
{
|
|
275
|
-
"id": "mimo-v2.6-flash",
|
|
276
|
-
"name": "mimo-v2.6-flash",
|
|
277
|
-
"reasoning": true,
|
|
278
|
-
"input": [
|
|
279
|
-
"text",
|
|
280
|
-
"image"
|
|
281
|
-
],
|
|
282
|
-
"cost": {
|
|
283
|
-
"input": 0,
|
|
284
|
-
"output": 0,
|
|
285
|
-
"cacheRead": 0,
|
|
286
|
-
"cacheWrite": 0
|
|
287
|
-
},
|
|
288
|
-
"contextWindow": 1048576,
|
|
289
|
-
"maxTokens": 131072,
|
|
290
|
-
"compat": {
|
|
291
|
-
"supportsDeveloperRole": false,
|
|
292
|
-
"supportsReasoningEffort": true,
|
|
293
|
-
"supportsUsageInStreaming": true,
|
|
294
|
-
"supportsFinishReason": true,
|
|
295
|
-
"maxTokensField": "max_tokens"
|
|
296
|
-
},
|
|
297
|
-
"notes": [
|
|
298
|
-
"compat matches the maintainer's working ~/.pi/agent/models.json LiteLLM config for api.nan.builders (2026-09-04): supportsDeveloperRole false, supportsReasoningEffort true, maxTokensField max_tokens. NaN's docs example sets only supportsDeveloperRole: true and is not battle-tested. supportsFinishReason true (2026-09-13, issue #2): the LiteLLM gateway intermittently closes SSE streams before emitting finish_reason; with true pi-ai raises 'Stream ended without finish_reason', which matches pi-ai's retryable-provider pattern ('ended without') and is retried automatically, whereas false silently synthesized stop/toolUse and stalled the turn mid-answer. supportsUsageInStreaming true (2026-09-16, issue #7): NaN's published schema is silent about stream_options, but the live gateway honors it — two identical streaming calls per model, differing only in stream_options: { include_usage: true }, returned 0 usage chunks without it and exactly 1 with it (prompt/completion/reasoning/cached token counts) on deepseek-v4-flash, glm5.3-flash, qwen3.6, mimo-v2.5 and gemma4, and a real pi session then recorded token counts where it recorded zeros. pi-ai only sends stream_options when this is not false, and the sanitizer forwards it when the model declares true, so chat models opt in by default and usage is reported (issue #4). A model that does not report streaming usage can still opt out per model with a models.json compat override (supportsUsageInStreaming: false); the sanitizer then strips stream_options and the payload stays strict.",
|
|
299
|
-
"manual-only: \"mimo-v2.6-flash\" not yet on models.dev provider nan (omnimodal model (text, image, audio input) served by NaN, not yet listed on models.dev provider nan (checked 2026-09-25); included so the live /models refresh does not hand it UNKNOWN_MODEL_LIMITS. Same limits as mimo-v2.5: 1,048,576 / 131,072 / 1.0B monthly quota per member. Reasoning: accepted, depth not adjustable. Tool calling: yes. Streaming: yes. Input modalities (pi-representable): text, image (audio not representable in pi's Model type).)."
|
|
300
|
-
],
|
|
301
|
-
"extras": {}
|
|
302
|
-
},
|
|
303
295
|
{
|
|
304
296
|
"id": "qwen3.6",
|
|
305
297
|
"name": "Qwen3.6 35B-A3B",
|
|
@@ -343,7 +335,14 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
|
|
|
343
335
|
"reasoning": true,
|
|
344
336
|
"reasoning_options": [
|
|
345
337
|
{
|
|
346
|
-
"type": "
|
|
338
|
+
"type": "effort",
|
|
339
|
+
"values": [
|
|
340
|
+
"none",
|
|
341
|
+
"low",
|
|
342
|
+
"medium",
|
|
343
|
+
"high",
|
|
344
|
+
"max"
|
|
345
|
+
]
|
|
347
346
|
}
|
|
348
347
|
],
|
|
349
348
|
"tool_call": true,
|
|
@@ -368,7 +367,8 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
|
|
|
368
367
|
"cost": {
|
|
369
368
|
"input": 0,
|
|
370
369
|
"output": 0
|
|
371
|
-
}
|
|
370
|
+
},
|
|
371
|
+
"canonical_model_id": "alibaba/qwen3.6-35b-a3b"
|
|
372
372
|
}
|
|
373
373
|
},
|
|
374
374
|
{
|
|
@@ -428,7 +428,8 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
|
|
|
428
428
|
"cost": {
|
|
429
429
|
"input": 0,
|
|
430
430
|
"output": 0
|
|
431
|
-
}
|
|
431
|
+
},
|
|
432
|
+
"canonical_model_id": "alibaba/qwen3.8-flash"
|
|
432
433
|
}
|
|
433
434
|
}
|
|
434
435
|
];
|
|
@@ -436,15 +437,17 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
|
|
|
436
437
|
export const GENERATED_CATALOG_META = {
|
|
437
438
|
source: "https://models.dev/api.json",
|
|
438
439
|
modelsDevProvider: "nan",
|
|
439
|
-
fetchedAt: "2026-09-
|
|
440
|
-
modelCount:
|
|
441
|
-
models: ["deepseek-v4-flash","gemma4","glm5.3-flash","mimo-v2.
|
|
440
|
+
fetchedAt: "2026-09-29T22:08:21.449Z",
|
|
441
|
+
modelCount: 6,
|
|
442
|
+
models: ["deepseek-v4-flash","gemma4","glm5.3-flash","mimo-v2.6-flash","qwen3.6","qwen3.8-flash"],
|
|
442
443
|
notes: [
|
|
444
|
+
"non-chat: \"qwen-image-2.1\" excluded from the chat catalog (image-generation model (text→image; output modality image) documented in https://nan.builders/docs/models and https://nan.builders/openapi.json (checked 2026-09-29) — not a chat model, out of the static chat catalog (models.dev reports limit.output 0))",
|
|
443
445
|
"live-only: \"glm5.3\" kept out of the static catalog (premium-tier model (models.dev now documents it with 1M context / 131,072 max output; NaN docs https://nan.builders/docs/models + https://nan.builders/openapi.json, checked 2026-09-13) kept live-only so a non-premium key never sees a model it cannot call when the live /models fetch is unavailable; premium keys still get it via the /models refresh with conservative placeholder limits)",
|
|
444
446
|
"provider-removed: \"glm5.2\" excluded from the catalog (removed by NaN (2026-09-05); absent from the official chat model list in https://nan.builders/openapi.json and https://nan.builders/docs/models (checked 2026-09-07) while models.dev provider nan still listed it — excluded so regeneration does not resurrect it)",
|
|
447
|
+
"provider-removed: \"mimo-v2.5\" excluded from the catalog (removed by NaN (checked 2026-09-29): zero mentions in https://nan.builders/docs/models and in the model param description of https://nan.builders/openapi.json, and models.dev provider nan dropped it as well — superseded by mimo-v2.6-flash; excluded so regeneration does not resurrect it)",
|
|
445
448
|
"compat matches the maintainer's working ~/.pi/agent/models.json LiteLLM config for api.nan.builders (2026-09-04): supportsDeveloperRole false, supportsReasoningEffort true, maxTokensField max_tokens. NaN's docs example sets only supportsDeveloperRole: true and is not battle-tested. supportsFinishReason true (2026-09-13, issue #2): the LiteLLM gateway intermittently closes SSE streams before emitting finish_reason; with true pi-ai raises 'Stream ended without finish_reason', which matches pi-ai's retryable-provider pattern ('ended without') and is retried automatically, whereas false silently synthesized stop/toolUse and stalled the turn mid-answer. supportsUsageInStreaming true (2026-09-16, issue #7): NaN's published schema is silent about stream_options, but the live gateway honors it — two identical streaming calls per model, differing only in stream_options: { include_usage: true }, returned 0 usage chunks without it and exactly 1 with it (prompt/completion/reasoning/cached token counts) on deepseek-v4-flash, glm5.3-flash, qwen3.6, mimo-v2.5 and gemma4, and a real pi session then recorded token counts where it recorded zeros. pi-ai only sends stream_options when this is not false, and the sanitizer forwards it when the model declares true, so chat models opt in by default and usage is reported (issue #4). A model that does not report streaming usage can still opt out per model with a models.json compat override (supportsUsageInStreaming: false); the sanitizer then strips stream_options and the payload stays strict.",
|
|
446
449
|
"input includes image: NaN serves the Vision-Exp variant ('takes images as input', https://nan.builders/docs/models; the image_url content-parts in https://nan.builders/openapi.json list deepseek-v4-flash among the vision models). models.dev provider nan also lists text+image now (DeepSeek V4.1 Flash entry, checked 2026-09-13; its 2026-09-07 snapshot listed text only), so this override is kept as a pin for the vision capability rather than as a divergence. reasoning_effort_values=[]: the model decides per-request how much to reason (https://nan.builders/docs/models, checked 2026-09-25).",
|
|
447
|
-
"manual-only: \"mimo-v2.6-flash\" not
|
|
450
|
+
"manual-only: \"mimo-v2.6-flash\" skipped: models.dev provider \"nan\" now lists it, so the manual-only entry is not emitted and the models.dev values are used (omnimodal model (text, image, audio input) served by NaN; limits and modalities from https://nan.builders/docs/models#mimo-v2-6-flash (checked 2026-09-25, quotas re-checked 2026-09-29): 1,048,576 context / 131,072 max output / 1.0B monthly quota per member. Reasoning: accepted, depth not adjustable (docs table, checked 2026-09-29). Tool calling: yes. Streaming: yes. Input modalities (pi-representable): text, image (audio not representable in pi's Model type). Fallback entry: emitted only while models.dev provider nan does not list the model — models.dev started listing it on 2026-09-29, so the generator then keeps the models.dev values and attaches this note to that entry instead of emitting a second one.)",
|
|
448
451
|
"contextWindow 262,144: the earlier 1,000,000 override (maintainer-confirmed 2026-09-05) was withdrawn 2026-09-07 — the updated https://nan.builders/docs/models still states '262K token context, the model's native window' and models.dev agrees at 262,144; NaN docs are treated as the most reliable source (maintainer instruction, 2026-09-07)."
|
|
449
452
|
],
|
|
450
453
|
} as const;
|
package/src/commands.ts
CHANGED
|
@@ -1,45 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `/nan-mcp` — slash command configuring
|
|
3
|
-
* registers
|
|
2
|
+
* `/nan-mcp` — slash command configuring both native MCP servers this
|
|
3
|
+
* package registers:
|
|
4
4
|
*
|
|
5
|
-
* - `
|
|
6
|
-
* exposing `
|
|
7
|
-
* - `nan-
|
|
8
|
-
*
|
|
5
|
+
* - `nan-search` — the official NaN remote MCP server (api.nan.builders/mcp),
|
|
6
|
+
* exposing `mcp__nan-search__web_search`. Default: enabled.
|
|
7
|
+
* - `nan-media` — the community stdio media server (flux-2-klein / kokoro /
|
|
8
|
+
* whisper), exposing `mcp__nan-media__generate_image` etc. Default: enabled.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* - `/nan-mcp` or `/nan-mcp status` — state of both bridges and their source.
|
|
14
|
-
* - `/nan-mcp enable [target]` — enable a bridge (or both, when no target is
|
|
15
|
-
* given) and persist it in `<agentDir>/nan-provider.json`; enabled bridges
|
|
16
|
-
* register their tools immediately for the current session.
|
|
17
|
-
* - `/nan-mcp disable [target]` — disable persistently. pi has no
|
|
18
|
-
* unregisterTool, so already-registered tools remain until the next
|
|
19
|
-
* session/restart; the persisted toggle governs future sessions.
|
|
10
|
+
* Both are session-scoped native servers (visible in /mcp, source "extension").
|
|
11
|
+
* /nan-mcp enables/disables them via pi.registerMcpServer / pi.unregisterMcpServer,
|
|
12
|
+
* persisting the toggle in nan-provider.json.
|
|
20
13
|
*
|
|
21
14
|
* Targets accept aliases: `web-search` (`search`, `web_search`, `official`,
|
|
22
15
|
* `nan-web-search`) and `nan-mcp-server` (`media`, `media-mcp`, `nan-media`).
|
|
23
16
|
* Explicit env vars (NAN_MCP_TOOLS, NAN_MEDIA_MCP) override the persisted
|
|
24
17
|
* toggles for the session.
|
|
18
|
+
*
|
|
19
|
+
* Native behavior changes from the old bridge:
|
|
20
|
+
* - `disable` IMMEDIATELY hides tools (unregisterMcpServer disconnects the
|
|
21
|
+
* server; the MCP extension hides tools when disconnected).
|
|
22
|
+
* - `enable` IMMEDIATELY registers the server for the current session.
|
|
23
|
+
* - `status` reads the server state from pi.getMcpServers().
|
|
24
|
+
* - Missing API key at load: server is NOT registered; guidance notification
|
|
25
|
+
* replaces the old "errors at call time" behavior.
|
|
25
26
|
*/
|
|
26
27
|
|
|
27
28
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
28
|
-
import { mediaMcpCommand, mediaMcpEnabled, mediaMcpSource } from "./mcp/
|
|
29
|
-
import { webSearchBridgeEnabled, webSearchBridgeSource } from "./mcp/
|
|
30
|
-
import {
|
|
31
|
-
|
|
32
|
-
export interface NanMcpCommandOptions {
|
|
33
|
-
/** Register web-search bridge tools now; must skip already-registered tools. */
|
|
34
|
-
registerWebSearchTools: () => void;
|
|
35
|
-
/** Register media bridge tools now; must skip already-registered tools. */
|
|
36
|
-
registerMediaTools: () => void;
|
|
37
|
-
}
|
|
29
|
+
import { mediaMcpCommand, mediaMcpEnabled, mediaMcpSource, mediaMcpEnvExplicit, mediaMcpEnvTruthy, mediaMcpTimeoutSec } from "./mcp/media-server.ts";
|
|
30
|
+
import { webSearchBridgeEnabled, webSearchBridgeSource, mcpToolsEnvExplicit, mcpToolsDisabled } from "./mcp/api-key.ts";
|
|
31
|
+
import { NAN_API_KEY_ENV, tryResolveNanApiKeyViaRegistry } from "./mcp/api-key.ts";
|
|
32
|
+
import { NAN_STATE_FILE, readBridgeState, writeBridgeState, type BridgeKey, readState, writeState } from "./mcp/state.ts";
|
|
38
33
|
|
|
39
34
|
const USAGE =
|
|
40
35
|
"Usage: /nan-mcp [status] · /nan-mcp enable [web-search|nan-mcp-server] · /nan-mcp disable [web-search|nan-mcp-server]";
|
|
41
36
|
|
|
42
|
-
/** Canonical bridge targets with aliases
|
|
37
|
+
/** Canonical bridge targets with aliases. */
|
|
43
38
|
const TARGETS: Record<string, BridgeKey> = {
|
|
44
39
|
"web-search": "webSearch",
|
|
45
40
|
search: "webSearch",
|
|
@@ -67,19 +62,64 @@ function sourceLabel(bridge: BridgeKey, source: "env" | "persisted" | "default")
|
|
|
67
62
|
if (source === "persisted") {
|
|
68
63
|
return `persisted in <agentDir>/${NAN_STATE_FILE} (${targetName(bridge)}: ${readBridgeState(bridge)})`;
|
|
69
64
|
}
|
|
70
|
-
return "default (both bridges are enabled
|
|
65
|
+
return "default (both bridges are enabled by default)";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Resolve a bridge key to its native server name. */
|
|
69
|
+
function serverName(bridge: BridgeKey): string {
|
|
70
|
+
return bridge === "webSearch" ? "nan-search" : "nan-media";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Describe the bridge in the status message. */
|
|
74
|
+
function bridgeDescription(bridge: BridgeKey): string {
|
|
75
|
+
return bridge === "webSearch"
|
|
76
|
+
? "web-search bridge (official NaN MCP → mcp__nan-search__web_search)"
|
|
77
|
+
: "nan-mcp-server bridge (community media MCP → mcp__nan-media__generate_image/edit_image/text_to_speech/list_voices/speech_to_text)";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Check if a native server of this name is currently registered. */
|
|
81
|
+
function nativeServerRegistered(pi: ExtensionAPI, name: string): boolean {
|
|
82
|
+
try {
|
|
83
|
+
return pi.getMcpServers().some((s) => s.name === name);
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
71
87
|
}
|
|
72
88
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
89
|
+
/** Status message listing both bridges' gate state and native registration. */
|
|
90
|
+
function statusMessage(pi: ExtensionAPI): string {
|
|
91
|
+
const lines: string[] = [];
|
|
92
|
+
|
|
93
|
+
for (const bridge of ["webSearch" as BridgeKey, "mediaMcp" as BridgeKey]) {
|
|
94
|
+
const enabled = bridge === "webSearch" ? webSearchBridgeEnabled() : mediaMcpEnabled();
|
|
95
|
+
const source = bridge === "webSearch" ? webSearchBridgeSource() : mediaMcpSource();
|
|
96
|
+
const regStatus = nativeServerRegistered(pi, serverName(bridge)) ? "registered (native)" : "not registered";
|
|
97
|
+
lines.push(`${bridgeDescription(bridge)}: ${enabled ? "enabled" : "disabled"} — ${sourceLabel(bridge, source)}, ${regStatus}.`);
|
|
98
|
+
|
|
99
|
+
if (bridge === "webSearch") {
|
|
100
|
+
// Key status for web-search
|
|
101
|
+
const keyStatus = resolveKeyStatus(pi);
|
|
102
|
+
lines.push(` API key: ${keyStatus}.`);
|
|
103
|
+
}
|
|
104
|
+
if (bridge === "mediaMcp") {
|
|
105
|
+
lines.push(` Media spawn command: ${mediaMcpCommand().join(" ")}.`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
lines.push("Configure with /nan-mcp enable|disable [web-search|nan-mcp-server]; no target = both.");
|
|
110
|
+
return lines.join("\n");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Check API key resolution status for web-search. */
|
|
114
|
+
async function resolveKeyStatus(pi: ExtensionAPI): Promise<string> {
|
|
115
|
+
// @ts-expect-error — modelRegistry is a runtime surface.
|
|
116
|
+
const registryKey = await tryResolveNanApiKeyViaRegistry(pi.modelRegistry);
|
|
117
|
+
const envKey = process.env[NAN_API_KEY_ENV];
|
|
118
|
+
if (registryKey || envKey) return "resolved";
|
|
119
|
+
return "NOT SET — run /login nan or export NAN_API_KEY";
|
|
80
120
|
}
|
|
81
121
|
|
|
82
|
-
/** Parse an optional target argument; no target = both bridges.
|
|
122
|
+
/** Parse an optional target argument; no target = both bridges. */
|
|
83
123
|
function parseTarget(token: string | undefined): { bridges: BridgeKey[]; target?: string } | undefined {
|
|
84
124
|
if (token === undefined) return { bridges: ["webSearch", "mediaMcp"] };
|
|
85
125
|
const key = TARGETS[token.toLowerCase()];
|
|
@@ -92,18 +132,25 @@ function describeBridges(bridges: BridgeKey[]): string {
|
|
|
92
132
|
return names.length === 2 ? "both bridges" : names.join(" and ");
|
|
93
133
|
}
|
|
94
134
|
|
|
95
|
-
|
|
135
|
+
/**
|
|
136
|
+
* Check if a bridge is effectively enabled (gate resolved).
|
|
137
|
+
*/
|
|
138
|
+
function bridgeEffectivelyEnabled(bridge: BridgeKey): boolean {
|
|
139
|
+
return bridge === "webSearch" ? webSearchBridgeEnabled() : mediaMcpEnabled();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function registerNanMcpCommand(pi: ExtensionAPI): void {
|
|
96
143
|
pi.registerCommand("nan-mcp", {
|
|
97
144
|
description:
|
|
98
|
-
"NaN MCP configuration: enable/disable/status for both
|
|
145
|
+
"NaN MCP configuration: enable/disable/status for both native servers (official web-search + community media)",
|
|
99
146
|
getArgumentCompletions: (argumentPrefix: string) => {
|
|
100
147
|
const prefix = argumentPrefix.trim().toLowerCase();
|
|
101
148
|
const items = [
|
|
102
|
-
{ value: "enable", label: "enable", description: "Enable a
|
|
103
|
-
{ value: "disable", label: "disable", description: "Disable a
|
|
104
|
-
{ value: "status", label: "status", description: "Show current
|
|
105
|
-
{ value: "enable web-search", label: "enable web-search", description: "Enable the official NaN web-search
|
|
106
|
-
{ value: "enable nan-mcp-server", label: "enable nan-mcp-server", description: "Enable the community media
|
|
149
|
+
{ value: "enable", label: "enable", description: "Enable a native server (or both) and persist it" },
|
|
150
|
+
{ value: "disable", label: "disable", description: "Disable a native server (or both) persistently" },
|
|
151
|
+
{ value: "status", label: "status", description: "Show current server status and registration" },
|
|
152
|
+
{ value: "enable web-search", label: "enable web-search", description: "Enable the official NaN web-search server" },
|
|
153
|
+
{ value: "enable nan-mcp-server", label: "enable nan-mcp-server", description: "Enable the community media server" },
|
|
107
154
|
];
|
|
108
155
|
const filtered = items.filter((item) => item.value.startsWith(prefix));
|
|
109
156
|
return filtered.length > 0 ? filtered : null;
|
|
@@ -114,12 +161,11 @@ export function registerNanMcpCommand(pi: ExtensionAPI, options: NanMcpCommandOp
|
|
|
114
161
|
const subcommand = rawSubcommand.toLowerCase();
|
|
115
162
|
|
|
116
163
|
if (subcommand === "status") {
|
|
117
|
-
ctx.ui.notify(statusMessage(), "info");
|
|
164
|
+
ctx.ui.notify(statusMessage(pi), "info");
|
|
118
165
|
return;
|
|
119
166
|
}
|
|
120
167
|
|
|
121
168
|
if (subcommand === "enable" || subcommand === "disable") {
|
|
122
|
-
// Trailing tokens beyond the target (e.g. duplicate target names) are ignored.
|
|
123
169
|
const parsed = parseTarget(rawTarget);
|
|
124
170
|
if (parsed === undefined || rest.length > 0) {
|
|
125
171
|
ctx.ui.notify(
|
|
@@ -136,23 +182,57 @@ export function registerNanMcpCommand(pi: ExtensionAPI, options: NanMcpCommandOp
|
|
|
136
182
|
for (const bridge of bridges) {
|
|
137
183
|
writeBridgeState(bridge, enabled);
|
|
138
184
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
185
|
+
|
|
186
|
+
// Immediate native registration/unregistration.
|
|
187
|
+
for (const bridge of bridges) {
|
|
188
|
+
const name = serverName(bridge);
|
|
189
|
+
if (enabled) {
|
|
190
|
+
// Only register if gate allows it.
|
|
191
|
+
if (bridgeEffectivelyEnabled(bridge)) {
|
|
192
|
+
if (bridge === "webSearch") {
|
|
193
|
+
// @ts-expect-error — modelRegistry is a runtime surface.
|
|
194
|
+
const registryKey = await tryResolveNanApiKeyViaRegistry(pi.modelRegistry);
|
|
195
|
+
const apiKey = registryKey ?? process.env[NAN_API_KEY_ENV];
|
|
196
|
+
if (apiKey) {
|
|
197
|
+
pi.registerMcpServer(name, {
|
|
198
|
+
type: "http",
|
|
199
|
+
url: "https://api.nan.builders/mcp",
|
|
200
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
201
|
+
exposure: "direct",
|
|
202
|
+
});
|
|
203
|
+
} else {
|
|
204
|
+
ctx.ui.notify(
|
|
205
|
+
`${NAN_API_KEY_ENV} is not set. Export it or run /login nan.`,
|
|
206
|
+
"warning",
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
const mediaArgs = mediaMcpCommand();
|
|
211
|
+
pi.registerMcpServer(name, {
|
|
212
|
+
type: "stdio",
|
|
213
|
+
command: mediaArgs[0]!,
|
|
214
|
+
args: mediaArgs.slice(1),
|
|
215
|
+
env: { [NAN_API_KEY_ENV]: process.env[NAN_API_KEY_ENV] ?? "" },
|
|
216
|
+
exposure: "direct",
|
|
217
|
+
timeout: mediaMcpTimeoutSec(),
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
pi.unregisterMcpServer(name);
|
|
223
|
+
}
|
|
142
224
|
}
|
|
143
225
|
|
|
144
226
|
const where = target ? `for "${target}"` : "for both bridges";
|
|
145
227
|
const persistence = `persisted in <agentDir>/${NAN_STATE_FILE}`;
|
|
146
228
|
if (enabled) {
|
|
147
229
|
ctx.ui.notify(
|
|
148
|
-
`Enabled ${describeBridges(bridges)} ${where}, ${persistence}.
|
|
149
|
-
" An explicit NAN_MCP_TOOLS / NAN_MEDIA_MCP env var would override this toggle.",
|
|
230
|
+
`Enabled ${describeBridges(bridges)} ${where}, ${persistence}. Native servers registered for this session.`,
|
|
150
231
|
"info",
|
|
151
232
|
);
|
|
152
233
|
} else {
|
|
153
234
|
ctx.ui.notify(
|
|
154
|
-
`Disabled ${describeBridges(bridges)} ${where}, ${persistence}.
|
|
155
|
-
" An explicit NAN_MCP_TOOLS / NAN_MEDIA_MCP env var overrides this toggle.",
|
|
235
|
+
`Disabled ${describeBridges(bridges)} ${where}, ${persistence}. Native servers unregistered — tools hidden immediately.`,
|
|
156
236
|
"warning",
|
|
157
237
|
);
|
|
158
238
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* NaN-aware context-overflow classification.
|
|
3
3
|
*
|
|
4
|
-
* Root cause (pi-ai,
|
|
4
|
+
* Root cause (pi-ai, verified on 0.87.1): when history is replayed
|
|
5
5
|
* into a DIFFERENT model, `transformMessages` downgrades every non-redacted
|
|
6
6
|
* `thinking` block to plain `text` verbatim, with no size bound — and nothing
|
|
7
7
|
* bounds the SUM across messages. Switching from a 1M-context model
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cross-model thinking guard.
|
|
3
3
|
*
|
|
4
|
-
* Root cause this guards against (pi-ai,
|
|
4
|
+
* Root cause this guards against (pi-ai, verified on 0.87.1): when
|
|
5
5
|
* history is replayed into a DIFFERENT model, `transformMessages` downgrades
|
|
6
6
|
* every non-redacted `thinking` block to a plain `text` block verbatim
|
|
7
7
|
* (`packages/ai/src/api/transform-messages.ts`), and `openai-completions`
|