@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
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @gtrabanco/pi-nan-provider — NaN Builders provider + MCP
|
|
2
|
+
* @gtrabanco/pi-nan-provider — NaN Builders provider + native MCP servers for pi.
|
|
3
3
|
*
|
|
4
4
|
* What this extension registers:
|
|
5
5
|
*
|
|
@@ -15,14 +15,17 @@
|
|
|
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
|
-
* - `
|
|
20
|
-
* (https://api.nan.builders/mcp)
|
|
21
|
-
* disable.
|
|
22
|
-
* -
|
|
23
|
-
* (
|
|
24
|
-
*
|
|
25
|
-
*
|
|
18
|
+
* 2. MCP servers via pi's native registerMcpServer (pi >=0.99):
|
|
19
|
+
* - `nan-search` — the official NaN remote MCP server
|
|
20
|
+
* (https://api.nan.builders/mcp), exposing `mcp__nan-search__web_search`.
|
|
21
|
+
* Default: enabled. NAN_MCP_TOOLS=0 to disable.
|
|
22
|
+
* - `nan-media` — the community stdio media server
|
|
23
|
+
* (flux-2-klein / kokoro / whisper), exposing
|
|
24
|
+
* `mcp__nan-media__generate_image` etc. Default: enabled,
|
|
25
|
+
* NAN_MEDIA_MCP=0 to disable.
|
|
26
|
+
*
|
|
27
|
+
* Both are session-scoped (visible in /mcp with source "extension").
|
|
28
|
+
* Exposure: `direct` — tools are declared to the model like built-ins.
|
|
26
29
|
*/
|
|
27
30
|
|
|
28
31
|
import type { ContextEvent, ExtensionAPI, ProviderConfig } from "@earendil-works/pi-coding-agent";
|
|
@@ -34,10 +37,10 @@ import {
|
|
|
34
37
|
stripCrossModelThinking,
|
|
35
38
|
} from "./cross-model-thinking-guard.ts";
|
|
36
39
|
import { baselineModels } from "./fetch-models.ts";
|
|
37
|
-
import { createNanWebSearchTool, webSearchBridgeEnabled, NAN_API_KEY_ENV } from "./mcp/nan-search.ts";
|
|
38
|
-
import { createNanMediaTools, mediaMcpEnabled } from "./mcp/nan-media.ts";
|
|
39
40
|
import { createNanCompatibleProvider, type OpenAICompatibleProviderConfig } from "./provider-factory.ts";
|
|
40
41
|
import { PROVIDERS } from "./providers.ts";
|
|
42
|
+
import { NAN_API_KEY_ENV, mcpToolsDisabled, mcpToolsEnvExplicit, resolveNanApiKey, tryResolveNanApiKeyViaRegistry, NAN_MCP_TOOLS_ENV, webSearchBridgeEnabled } from "./mcp/api-key.ts";
|
|
43
|
+
import { mediaMcpCommand, mediaMcpEnabled, mediaMcpEnvExplicit, mediaMcpEnvTruthy, mediaMcpTimeoutSec } from "./mcp/media-server.ts";
|
|
41
44
|
|
|
42
45
|
/**
|
|
43
46
|
* Register a provider on any pi version: the native full-Provider overload
|
|
@@ -80,45 +83,78 @@ async function registerProviderCompat(
|
|
|
80
83
|
pi.registerProvider(config.id, legacy);
|
|
81
84
|
}
|
|
82
85
|
|
|
86
|
+
// ── Native MCP registration ──────────────────────────────────────────────
|
|
87
|
+
|
|
83
88
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* `/nan-mcp` (and the NAN_MCP_TOOLS / NAN_MEDIA_MCP env vars) toggle them.
|
|
87
|
-
* Old pi versions without registerTool/registerCommand skip gracefully.
|
|
88
|
-
*
|
|
89
|
-
* Registration is tracked in `registeredToolNames` so `/nan-mcp enable` can
|
|
90
|
-
* add tools mid-session without double-registering.
|
|
89
|
+
* Guard message shown when pi lacks native MCP support (pi <0.99 despite peers).
|
|
90
|
+
* Matches the loud-skip style of existing guards (pi-ai, registerTool).
|
|
91
91
|
*/
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const registeredToolNames = new Set<string>();
|
|
95
|
-
const registerSearchTool = () => {
|
|
96
|
-
const search = createNanWebSearchTool();
|
|
97
|
-
if (registeredToolNames.has(search.name)) return;
|
|
98
|
-
registeredToolNames.add(search.name);
|
|
99
|
-
pi.registerTool(search);
|
|
100
|
-
};
|
|
101
|
-
const registerMediaTools = () => {
|
|
102
|
-
for (const tool of createNanMediaTools()) {
|
|
103
|
-
if (registeredToolNames.has(tool.name)) continue;
|
|
104
|
-
registeredToolNames.add(tool.name);
|
|
105
|
-
pi.registerTool(tool);
|
|
106
|
-
}
|
|
107
|
-
};
|
|
92
|
+
const NO_NATIVE_MCP_GUARD_MSG =
|
|
93
|
+
"pi-nan-provider 0.10+ requires pi >= 0.99 for MCP tools (native MCP); upgrade pi or stay on package 0.9.x";
|
|
108
94
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
|
|
95
|
+
/** Register the official NaN web-search MCP server (session-scoped). */
|
|
96
|
+
async function registerWebSearchMcpServer(pi: ExtensionAPI): Promise<boolean> {
|
|
97
|
+
if (!webSearchBridgeEnabled()) return false;
|
|
98
|
+
const apiKey = process.env[NAN_API_KEY_ENV];
|
|
99
|
+
if (!apiKey) {
|
|
100
|
+
return false;
|
|
114
101
|
}
|
|
102
|
+
pi.registerMcpServer("nan-search", {
|
|
103
|
+
type: "http",
|
|
104
|
+
url: "https://api.nan.builders/mcp",
|
|
105
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
106
|
+
exposure: "direct",
|
|
107
|
+
});
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Register the community media stdio MCP server (session-scoped). */
|
|
112
|
+
function registerMediaMcpServer(pi: ExtensionAPI): boolean {
|
|
113
|
+
if (!mediaMcpEnabled()) return false;
|
|
114
|
+
const mediaArgs = mediaMcpCommand();
|
|
115
|
+
pi.registerMcpServer("nan-media", {
|
|
116
|
+
type: "stdio",
|
|
117
|
+
command: mediaArgs[0]!,
|
|
118
|
+
args: mediaArgs.slice(1),
|
|
119
|
+
env: { [NAN_API_KEY_ENV]: process.env[NAN_API_KEY_ENV] ?? "" },
|
|
120
|
+
exposure: "direct",
|
|
121
|
+
timeout: mediaMcpTimeoutSec(),
|
|
122
|
+
});
|
|
123
|
+
return true;
|
|
115
124
|
}
|
|
116
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Register MCP servers via pi's native registerMcpServer.
|
|
128
|
+
*
|
|
129
|
+
* Session-scoped: servers appear in /mcp with source "extension", are
|
|
130
|
+
* visible to the model (exposure: "direct"), and user mcp.json entries
|
|
131
|
+
* with the same name take precedence.
|
|
132
|
+
*
|
|
133
|
+
* On pi <0.99 (registerMcpServer absent) this does NOT bridge — it
|
|
134
|
+
* notifies once and skips (same loud-skip style as existing guards).
|
|
135
|
+
* Hard cutover: users on older pi stay on package 0.9.x.
|
|
136
|
+
*/
|
|
137
|
+
async function registerMcpServersNative(pi: ExtensionAPI): Promise<void> {
|
|
138
|
+
// Guard: pi >= 0.99 required for native MCP.
|
|
139
|
+
if (typeof pi.registerMcpServer !== "function") {
|
|
140
|
+
console.warn(NO_NATIVE_MCP_GUARD_MSG);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
const didWebSearch = await registerWebSearchMcpServer(pi);
|
|
146
|
+
registerMediaMcpServer(pi);
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Extension entrypoint ─────────────────────────────────────────────────
|
|
152
|
+
|
|
117
153
|
/**
|
|
118
154
|
* Drop the reasoning pi-ai replays across a model switch.
|
|
119
155
|
*
|
|
120
156
|
* pi-ai's `transformMessages` downgrades a previous model's `thinking` blocks
|
|
121
|
-
* to plain text with no size bound (
|
|
157
|
+
* to plain text with no size bound (verified on 0.87.1), and nothing
|
|
122
158
|
* bounds the sum across messages — measured at 30–60% of the whole context on
|
|
123
159
|
* real sessions. Switching from a 1M-context model to a 262K one (`qwen3.6`)
|
|
124
160
|
* then overflows the window, and NaN's gateway answers a generic
|
|
@@ -146,7 +182,11 @@ export default async function nanProviderExtension(pi: ExtensionAPI): Promise<vo
|
|
|
146
182
|
for (const config of PROVIDERS) {
|
|
147
183
|
await registerProviderCompat(pi, config);
|
|
148
184
|
}
|
|
149
|
-
|
|
185
|
+
await registerMcpServersNative(pi);
|
|
186
|
+
if (typeof pi.registerCommand === "function") {
|
|
187
|
+
registerNanMcpCommand(pi);
|
|
188
|
+
registerNanUsageCommand(pi);
|
|
189
|
+
}
|
|
150
190
|
}
|
|
151
191
|
|
|
152
192
|
/** Exposed for tests: the env var this package uses for every NaN surface. */
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { bridgeSource, resolveBridgeEnabled } from "./state.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* NaN API key resolution — shared across every surface (web search, media, usage).
|
|
5
|
+
*
|
|
6
|
+
* Strategy: pi's stored credential (modelRegistry) wins → env var fallback.
|
|
7
|
+
* Never logs or embeds the key beyond the Authorization header.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Env var that overrides the official web_search bridge. */
|
|
11
|
+
export const NAN_MCP_TOOLS_ENV = "NAN_MCP_TOOLS";
|
|
12
|
+
/** Env var that overrides the media bridge. */
|
|
13
|
+
export const NAN_MEDIA_MCP_ENV = "NAN_MEDIA_MCP";
|
|
14
|
+
/** The NaN API key env var used everywhere. */
|
|
15
|
+
export const NAN_API_KEY_ENV = "NAN_API_KEY";
|
|
16
|
+
/** NaN provider id used for stored-credential lookup. */
|
|
17
|
+
const NAN_PROVIDER_ID = "nan";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the NaN API key: pi's registry first (covers stored auth.json),
|
|
21
|
+
* then env fallback. `registryKey` is the result of an attempted registry
|
|
22
|
+
* lookup (already caught and converted to string | undefined by the caller).
|
|
23
|
+
*/
|
|
24
|
+
export function resolveNanApiKey(registryKey: string | undefined): string | undefined {
|
|
25
|
+
if (registryKey) return registryKey;
|
|
26
|
+
return process.env[NAN_API_KEY_ENV] || undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Attempt to resolve the API key through pi's model registry.
|
|
31
|
+
* Catches any error and returns undefined so the caller can fall back to env.
|
|
32
|
+
*/
|
|
33
|
+
export async function tryResolveNanApiKeyViaRegistry(
|
|
34
|
+
modelRegistry: { getApiKeyForProvider(provider: string): Promise<string | undefined> } | undefined,
|
|
35
|
+
): Promise<string | undefined> {
|
|
36
|
+
try {
|
|
37
|
+
return await modelRegistry?.getApiKeyForProvider(NAN_PROVIDER_ID);
|
|
38
|
+
} catch {
|
|
39
|
+
// Registry unavailable — caller falls back to env.
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Whether MCP tool registration is disabled via env (NAN_MCP_TOOLS=0|false|off). */
|
|
45
|
+
export function mcpToolsDisabled(): boolean {
|
|
46
|
+
const value = process.env[NAN_MCP_TOOLS_ENV]?.trim().toLowerCase();
|
|
47
|
+
return value === "0" || value === "false" || value === "off";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Whether NAN_MCP_TOOLS is explicitly set (any value). */
|
|
51
|
+
export function mcpToolsEnvExplicit(): boolean {
|
|
52
|
+
const value = process.env[NAN_MCP_TOOLS_ENV];
|
|
53
|
+
return value !== undefined && value.trim() !== "";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Whether NAN_MEDIA_MCP is explicitly set (any value). */
|
|
57
|
+
export function mediaMcpEnvExplicit(): boolean {
|
|
58
|
+
const value = process.env[NAN_MEDIA_MCP_ENV];
|
|
59
|
+
return value !== undefined && value.trim() !== "";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Whether NAN_MEDIA_MCP is explicitly set and truthy. */
|
|
63
|
+
export function mediaMcpEnvTruthy(): boolean {
|
|
64
|
+
const value = process.env[NAN_MEDIA_MCP_ENV]?.trim().toLowerCase();
|
|
65
|
+
return value === "1" || value === "true" || value === "on";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Source of the effective web-search enablement (env / persisted / default). */
|
|
69
|
+
export function webSearchBridgeSource(): import("./state.ts").BridgeSource {
|
|
70
|
+
return bridgeSource("webSearch", mcpToolsEnvExplicit());
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Whether the web-search bridge is enabled by gate (env → persisted → default). */
|
|
74
|
+
export function webSearchBridgeEnabled(): boolean {
|
|
75
|
+
return resolveBridgeEnabled("webSearch", mcpToolsEnvExplicit(), !mcpToolsDisabled(), true);
|
|
76
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media server construction helpers — version pin, command, timeout.
|
|
3
|
+
* Also exports bridge gate resolution (mediaMcpEnabled / mediaMcpSource)
|
|
4
|
+
* to keep the native MCP registration in index.ts minimal.
|
|
5
|
+
*/
|
|
6
|
+
import { bridgeSource, resolveBridgeEnabled, type BridgeSource } from "./state.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Default spawn: `npx -y nan-mcp-server@<pinned version>` (npx caches after first use).
|
|
10
|
+
*/
|
|
11
|
+
export function mediaMcpCommand(version = process.env[NAN_MEDIA_MCP_VERSION_ENV] || DEFAULT_NAN_MEDIA_MCP_VERSION): string[] {
|
|
12
|
+
const custom = process.env[NAN_MEDIA_MCP_COMMAND_ENV]?.trim();
|
|
13
|
+
if (custom) return custom.split(/\s+/);
|
|
14
|
+
return ["npx", "-y", `nan-mcp-server@${version}`];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Per-request timeout in milliseconds. Native MCP uses seconds. */
|
|
18
|
+
export function mediaMcpTimeoutMs(): number {
|
|
19
|
+
const parsed = Number.parseInt(process.env[NAN_MEDIA_MCP_TIMEOUT_ENV] ?? "", 10);
|
|
20
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_MEDIA_MCP_TIMEOUT_MS;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Per-request timeout in **seconds** for the native MCP config. */
|
|
24
|
+
export function mediaMcpTimeoutSec(): number {
|
|
25
|
+
return Math.ceil(mediaMcpTimeoutMs() / 1000);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const NAN_MEDIA_MCP_ENV = "NAN_MEDIA_MCP";
|
|
29
|
+
|
|
30
|
+
export const DEFAULT_NAN_MEDIA_MCP_VERSION = "1.1.2";
|
|
31
|
+
export const DEFAULT_MEDIA_MCP_TIMEOUT_MS = 120_000;
|
|
32
|
+
export const NAN_MEDIA_MCP_VERSION_ENV = "NAN_MEDIA_MCP_VERSION";
|
|
33
|
+
export const NAN_MEDIA_MCP_COMMAND_ENV = "NAN_MEDIA_MCP_COMMAND";
|
|
34
|
+
export const NAN_MEDIA_MCP_TIMEOUT_ENV = "NAN_MEDIA_MCP_TIMEOUT_MS";
|
|
35
|
+
|
|
36
|
+
/** Whether NAN_MEDIA_MCP is explicitly set (any value). */
|
|
37
|
+
export function mediaMcpEnvExplicit(): boolean {
|
|
38
|
+
const value = process.env[NAN_MEDIA_MCP_ENV];
|
|
39
|
+
return value !== undefined && value.trim() !== "";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Whether NAN_MEDIA_MCP is explicitly set and truthy. */
|
|
43
|
+
export function mediaMcpEnvTruthy(): boolean {
|
|
44
|
+
const value = process.env[NAN_MEDIA_MCP_ENV]?.trim().toLowerCase();
|
|
45
|
+
return value === "1" || value === "true" || value === "on";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Whether the media bridge is enabled by gate (env → persisted → default). */
|
|
49
|
+
export function mediaMcpEnabled(): boolean {
|
|
50
|
+
return resolveBridgeEnabled("mediaMcp", mediaMcpEnvExplicit(), mediaMcpEnvTruthy(), true);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Source of the effective media enablement. */
|
|
54
|
+
export function mediaMcpSource(): BridgeSource {
|
|
55
|
+
return bridgeSource("mediaMcp", mediaMcpEnvExplicit());
|
|
56
|
+
}
|
package/src/provider-factory.ts
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* statically import ONLY the bare `@earendil-works/pi-ai` root. pi's
|
|
15
15
|
* extension loader maps that specifier to the compat entrypoint on every
|
|
16
16
|
* supported runtime (bundled CLI, Node-mode aliases, compiled-binary
|
|
17
|
-
* virtualModules; pi 0.83
|
|
18
|
-
* API factory. Subpath specifiers (`@earendil-works/pi-ai/api/...`) get the
|
|
17
|
+
* virtualModules; pi 0.83 through 0.87.1 alike), and compat re-exports every
|
|
18
|
+
* lazy API factory. Subpath specifiers (`@earendil-works/pi-ai/api/...`) get the
|
|
19
19
|
* alias applied as a prefix and resolve to `<compat.js>/api/...`, which does
|
|
20
20
|
* not exist — the extension then fails to load entirely.
|
|
21
21
|
*
|
|
@@ -79,6 +79,11 @@ export interface NanCompatibleProviderOptions {
|
|
|
79
79
|
* Any caller-supplied `onPayload` (e.g. pi's own debug/passthrough hook) is
|
|
80
80
|
* preserved and chained AFTER sanitization, so the final payload is always
|
|
81
81
|
* schema-valid.
|
|
82
|
+
*
|
|
83
|
+
* Note (pi-ai ≥ 0.86): this wrapper forwards the `TranscriptContext` parameter
|
|
84
|
+
* unchanged and only rewrites `options.onPayload` — the `TranscriptContext`
|
|
85
|
+
* migration (branded `TranscriptContext = { messages: Message[] }` replacing
|
|
86
|
+
* `Context`) does not affect this function's behavior.
|
|
82
87
|
*/
|
|
83
88
|
function isObject(value: unknown): value is Record<string, unknown> {
|
|
84
89
|
return typeof value === "object" && value !== null;
|