@herbertgao/pi-extensions 2026.9.11 → 2026.9.13
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/README.md +4 -4
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +1 -1
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
- package/node_modules/@herbertgao/pi-subagents/package.json +1 -1
- package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +5 -4
- package/node_modules/@herbertgao/pi-subagents/src/mention-clone.ts +39 -16
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
- package/node_modules/@narumitw/pi-btw/README.md +21 -4
- package/node_modules/@narumitw/pi-btw/dist/index.ts +1668 -958
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +4 -4
- package/node_modules/@narumitw/pi-btw/docs/workflows.md +10 -2
- package/node_modules/@narumitw/pi-btw/package.json +1 -1
- package/node_modules/@narumitw/pi-btw/src/btw.ts +17 -79
- package/node_modules/@narumitw/pi-btw/src/conversation-context.ts +74 -0
- package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +196 -7
- package/node_modules/@narumitw/pi-btw/src/main-thread-updates.ts +40 -0
- package/node_modules/@narumitw/pi-btw/src/menu.ts +34 -2
- package/node_modules/@narumitw/pi-btw/src/settings.ts +49 -0
- package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +9 -1
- package/node_modules/@narumitw/pi-btw/src/workspace-layout.ts +559 -0
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +62 -0
- package/node_modules/pi-mcp-adapter/README.md +75 -7
- package/node_modules/pi-mcp-adapter/bearer-command-resolver.ts +193 -0
- package/node_modules/pi-mcp-adapter/cli.js +56 -5
- package/node_modules/pi-mcp-adapter/commands.ts +96 -5
- package/node_modules/pi-mcp-adapter/config.ts +73 -6
- package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +22 -28
- package/node_modules/pi-mcp-adapter/direct-tools.ts +13 -5
- package/node_modules/pi-mcp-adapter/dist/abort.d.ts +2 -0
- package/node_modules/pi-mcp-adapter/dist/abort.js +36 -0
- package/node_modules/pi-mcp-adapter/dist/abort.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/bearer-command-resolver.d.ts +6 -0
- package/node_modules/pi-mcp-adapter/dist/bearer-command-resolver.js +194 -0
- package/node_modules/pi-mcp-adapter/dist/bearer-command-resolver.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/config.d.ts +5 -0
- package/node_modules/pi-mcp-adapter/dist/config.js +75 -6
- package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/consent-manager.d.ts +18 -0
- package/node_modules/pi-mcp-adapter/dist/consent-manager.js +88 -0
- package/node_modules/pi-mcp-adapter/dist/consent-manager.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/elicitation-handler.d.ts +17 -0
- package/node_modules/pi-mcp-adapter/dist/elicitation-handler.js +316 -0
- package/node_modules/pi-mcp-adapter/dist/elicitation-handler.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/errors.d.ts +131 -0
- package/node_modules/pi-mcp-adapter/dist/errors.js +278 -0
- package/node_modules/pi-mcp-adapter/dist/errors.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/http-ca.d.ts +8 -0
- package/node_modules/pi-mcp-adapter/dist/http-ca.js +88 -0
- package/node_modules/pi-mcp-adapter/dist/http-ca.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/jev-client.d.ts +15 -0
- package/node_modules/pi-mcp-adapter/dist/jev-client.js +340 -0
- package/node_modules/pi-mcp-adapter/dist/jev-client.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/jev-contracts.d.ts +77 -0
- package/node_modules/pi-mcp-adapter/dist/jev-contracts.js +2 -0
- package/node_modules/pi-mcp-adapter/dist/jev-contracts.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/jev-key-store.d.ts +22 -0
- package/node_modules/pi-mcp-adapter/dist/jev-key-store.js +85 -0
- package/node_modules/pi-mcp-adapter/dist/jev-key-store.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/json-schema-validator.d.ts +2 -0
- package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js +55 -0
- package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/lifecycle.d.ts +56 -0
- package/node_modules/pi-mcp-adapter/dist/lifecycle.js +410 -0
- package/node_modules/pi-mcp-adapter/dist/lifecycle.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/logger.d.ts +51 -0
- package/node_modules/pi-mcp-adapter/dist/logger.js +131 -0
- package/node_modules/pi-mcp-adapter/dist/logger.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth-fetch.d.ts +26 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth-fetch.js +103 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth-fetch.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.d.ts +118 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.js +1098 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth.d.ts +168 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth.js +1117 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js +7 -140
- package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/mcp-callback-server.d.ts +53 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-callback-server.js +441 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-callback-server.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-oauth-provider.d.ts +148 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-oauth-provider.js +689 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-oauth-provider.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-probe.d.ts +6 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-probe.js +166 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-probe.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-tasks.d.ts +102 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-tasks.js +369 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-tasks.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-trace.d.ts +95 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-trace.js +242 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-trace.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +2 -11
- package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/npx-resolver.d.ts +6 -0
- package/node_modules/pi-mcp-adapter/dist/npx-resolver.js +505 -0
- package/node_modules/pi-mcp-adapter/dist/npx-resolver.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/request-headers-command.d.ts +10 -0
- package/node_modules/pi-mcp-adapter/dist/request-headers-command.js +312 -0
- package/node_modules/pi-mcp-adapter/dist/request-headers-command.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/runtime-owner.d.ts +13 -0
- package/node_modules/pi-mcp-adapter/dist/runtime-owner.js +97 -0
- package/node_modules/pi-mcp-adapter/dist/runtime-owner.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/sampling-handler.d.ts +17 -0
- package/node_modules/pi-mcp-adapter/dist/sampling-handler.js +203 -0
- package/node_modules/pi-mcp-adapter/dist/sampling-handler.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/secure-keyring.d.ts +11 -0
- package/node_modules/pi-mcp-adapter/dist/secure-keyring.js +172 -0
- package/node_modules/pi-mcp-adapter/dist/secure-keyring.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/server-manager.d.ts +157 -0
- package/node_modules/pi-mcp-adapter/dist/server-manager.js +1743 -0
- package/node_modules/pi-mcp-adapter/dist/server-manager.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/session-approvals.d.ts +34 -0
- package/node_modules/pi-mcp-adapter/dist/session-approvals.js +140 -0
- package/node_modules/pi-mcp-adapter/dist/session-approvals.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/session-recovery.d.ts +26 -0
- package/node_modules/pi-mcp-adapter/dist/session-recovery.js +142 -0
- package/node_modules/pi-mcp-adapter/dist/session-recovery.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/state.d.ts +73 -0
- package/node_modules/pi-mcp-adapter/dist/state.js +2 -0
- package/node_modules/pi-mcp-adapter/dist/state.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/types.d.ts +36 -0
- package/node_modules/pi-mcp-adapter/dist/types.js +18 -0
- package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/ui-resource-handler.d.ts +17 -0
- package/node_modules/pi-mcp-adapter/dist/ui-resource-handler.js +219 -0
- package/node_modules/pi-mcp-adapter/dist/ui-resource-handler.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/unix-socket-transport.d.ts +15 -0
- package/node_modules/pi-mcp-adapter/dist/unix-socket-transport.js +85 -0
- package/node_modules/pi-mcp-adapter/dist/unix-socket-transport.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/utils.d.ts +1 -0
- package/node_modules/pi-mcp-adapter/dist/utils.js +13 -0
- package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -1
- package/node_modules/pi-mcp-adapter/elicitation-handler.ts +29 -19
- package/node_modules/pi-mcp-adapter/examples/jev-accessibility-loop.mjs +85 -0
- package/node_modules/pi-mcp-adapter/examples/jev-semantic-filter.mjs +34 -0
- package/node_modules/pi-mcp-adapter/index.ts +129 -6
- package/node_modules/pi-mcp-adapter/init.ts +6 -19
- package/node_modules/pi-mcp-adapter/jev-client.ts +273 -0
- package/node_modules/pi-mcp-adapter/jev-contracts.ts +53 -0
- package/node_modules/pi-mcp-adapter/jev-key-store.ts +79 -0
- package/node_modules/pi-mcp-adapter/lifecycle.ts +14 -3
- package/node_modules/pi-mcp-adapter/mcp-bearer-store.ts +7 -142
- package/node_modules/pi-mcp-adapter/mcp-code.ts +119 -11
- package/node_modules/pi-mcp-adapter/mcp-output-guard.ts +4 -0
- package/node_modules/pi-mcp-adapter/mcp-references.ts +5 -3
- package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +5 -0
- package/node_modules/pi-mcp-adapter/mcp-tasks.ts +467 -0
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +2 -13
- package/node_modules/pi-mcp-adapter/namespace-tools.ts +1 -1
- package/node_modules/pi-mcp-adapter/package.json +17 -9
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +319 -166
- package/node_modules/pi-mcp-adapter/request-headers-command.ts +6 -3
- package/node_modules/pi-mcp-adapter/search-ranking.ts +38 -7
- package/node_modules/pi-mcp-adapter/secure-keyring.ts +170 -0
- package/node_modules/pi-mcp-adapter/semantic-search.ts +186 -0
- package/node_modules/pi-mcp-adapter/server-manager.ts +293 -50
- package/node_modules/pi-mcp-adapter/session-approvals.ts +14 -0
- package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +2 -0
- package/node_modules/pi-mcp-adapter/state.ts +3 -1
- package/node_modules/pi-mcp-adapter/tool-approval.ts +26 -4
- package/node_modules/pi-mcp-adapter/tool-metadata.ts +6 -14
- package/node_modules/pi-mcp-adapter/tool-registrar.ts +8 -6
- package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +4 -1
- package/node_modules/pi-mcp-adapter/types.ts +53 -0
- package/node_modules/pi-mcp-adapter/ui-server.ts +17 -9
- package/node_modules/pi-mcp-adapter/utils.ts +16 -0
- package/node_modules/pi-multi-account/CHANGELOG.md +23 -0
- package/node_modules/pi-multi-account/README.md +38 -11
- package/node_modules/pi-multi-account/index.ts +371 -96
- package/node_modules/pi-multi-account/package.json +5 -4
- package/node_modules/pi-multi-account/provider-payload-stream.ts +36 -28
- package/node_modules/pi-multi-account/usage.ts +31 -2
- package/node_modules/pi-typesafe/README.md +3 -1
- package/node_modules/pi-typesafe/dist/auth.d.ts +10 -3
- package/node_modules/pi-typesafe/dist/auth.js +13 -7
- package/node_modules/pi-typesafe/dist/backends.d.ts +31 -0
- package/node_modules/pi-typesafe/dist/backends.js +33 -0
- package/node_modules/pi-typesafe/dist/client.d.ts +3 -9
- package/node_modules/pi-typesafe/dist/client.js +63 -39
- package/node_modules/pi-typesafe/dist/credentials.d.ts +11 -5
- package/node_modules/pi-typesafe/dist/credentials.js +15 -8
- package/node_modules/pi-typesafe/dist/extension.js +4 -1
- package/node_modules/pi-typesafe/dist/index.d.ts +1 -1
- package/node_modules/pi-typesafe/dist/index.js +1 -1
- package/node_modules/pi-typesafe/dist/login.d.ts +13 -7
- package/node_modules/pi-typesafe/dist/login.js +17 -8
- package/node_modules/pi-typesafe/dist/schema.js +19 -13
- package/node_modules/pi-typesafe/package.json +1 -1
- package/package.json +11 -10
|
@@ -298,25 +298,28 @@ async function invokeRequestHeadersCommand(
|
|
|
298
298
|
export function createRequestHeadersCommandFetch(
|
|
299
299
|
config: HttpRequestHeadersCommand,
|
|
300
300
|
delegate: FetchLike = globalThis.fetch,
|
|
301
|
-
)
|
|
301
|
+
) {
|
|
302
302
|
// Validate static configuration before the first request.
|
|
303
303
|
resolvedCommand(config);
|
|
304
304
|
return async (input: URL | RequestInfo, init?: RequestInit): Promise<Response> => {
|
|
305
305
|
const request = new Request(input, init);
|
|
306
|
+
const signal = init?.signal === null
|
|
307
|
+
? request.signal
|
|
308
|
+
: init?.signal ?? (input instanceof Request ? input.signal : request.signal);
|
|
306
309
|
const body = Buffer.from(await request.clone().arrayBuffer());
|
|
307
310
|
const derived = await invokeRequestHeadersCommand(config, {
|
|
308
311
|
version: 1,
|
|
309
312
|
method: request.method.toUpperCase(),
|
|
310
313
|
url: request.url,
|
|
311
314
|
bodyBase64: body.toString("base64"),
|
|
312
|
-
},
|
|
315
|
+
}, signal);
|
|
313
316
|
const headers = new Headers(request.headers);
|
|
314
317
|
derived.forEach((value, name) => headers.set(name, value));
|
|
315
318
|
return delegate(new URL(request.url), {
|
|
316
319
|
method: request.method,
|
|
317
320
|
headers,
|
|
318
321
|
...(request.method === "GET" || request.method === "HEAD" ? {} : { body }),
|
|
319
|
-
signal
|
|
322
|
+
signal,
|
|
320
323
|
cache: request.cache,
|
|
321
324
|
credentials: request.credentials,
|
|
322
325
|
integrity: request.integrity,
|
|
@@ -10,6 +10,12 @@ import { isServerInActiveFailureBackoff } from "./failure-backoff.ts";
|
|
|
10
10
|
*/
|
|
11
11
|
const MIN_STEM_LENGTH = 4;
|
|
12
12
|
|
|
13
|
+
// Keep ASCII tokens unchanged; bound adjacent bigrams for non-ASCII Unicode word runs.
|
|
14
|
+
// Whole-run fallbacks preserve single characters and prevent truncated queries from matching only a prefix.
|
|
15
|
+
const MAX_UNICODE_BIGRAMS_PER_RUN = 64;
|
|
16
|
+
const SEARCH_RUN = /[a-z0-9]+|(?:(?![a-z0-9])[\p{L}\p{N}\p{M}])+/gu;
|
|
17
|
+
const ASCII_RUN = /^[a-z0-9]+$/;
|
|
18
|
+
|
|
13
19
|
const FIELD_WEIGHTS = {
|
|
14
20
|
name: 12,
|
|
15
21
|
originalName: 10,
|
|
@@ -82,7 +88,32 @@ export function normalizeSearchText(value: string): string {
|
|
|
82
88
|
}
|
|
83
89
|
|
|
84
90
|
export function tokenize(value: string): string[] {
|
|
85
|
-
|
|
91
|
+
const tokens: string[] = [];
|
|
92
|
+
for (const run of normalizeSearchText(value).match(SEARCH_RUN) ?? []) {
|
|
93
|
+
if (ASCII_RUN.test(run)) {
|
|
94
|
+
tokens.push(run);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const characters = [...run];
|
|
98
|
+
if (characters.length === 1 || characters.length > MAX_UNICODE_BIGRAMS_PER_RUN + 1) {
|
|
99
|
+
tokens.push(run);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
let previous = "";
|
|
103
|
+
for (const character of characters) {
|
|
104
|
+
if (previous) {
|
|
105
|
+
tokens.push(previous + character);
|
|
106
|
+
}
|
|
107
|
+
previous = character;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return tokens;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function matchesAsciiStem(fieldToken: string, queryToken: string): boolean {
|
|
114
|
+
return ASCII_RUN.test(fieldToken)
|
|
115
|
+
&& ASCII_RUN.test(queryToken)
|
|
116
|
+
&& (fieldToken.startsWith(queryToken) || (fieldToken.length >= MIN_STEM_LENGTH && queryToken.startsWith(fieldToken)));
|
|
86
117
|
}
|
|
87
118
|
|
|
88
119
|
function prepareToolSearch(tool: ToolMetadata, server: string, keywords?: string[]): PreparedToolSearch {
|
|
@@ -132,7 +163,7 @@ function scorePreparedToolMatch(
|
|
|
132
163
|
if (fieldTokens.includes(token)) {
|
|
133
164
|
score += weight * 4;
|
|
134
165
|
matchedTokens.add(token);
|
|
135
|
-
} else if (fieldTokens.some(fieldToken =>
|
|
166
|
+
} else if (fieldTokens.some(fieldToken => matchesAsciiStem(fieldToken, token))) {
|
|
136
167
|
score += weight * 2;
|
|
137
168
|
matchedTokens.add(token);
|
|
138
169
|
} else if (value.includes(token)) {
|
|
@@ -167,7 +198,7 @@ function scorePreparedToolMatch(
|
|
|
167
198
|
if (prepared.keywordTokens.includes(token)) {
|
|
168
199
|
score += weight * 4;
|
|
169
200
|
matchedTokens.add(token);
|
|
170
|
-
} else if (prepared.keywordTokens.some(keywordToken =>
|
|
201
|
+
} else if (prepared.keywordTokens.some(keywordToken => matchesAsciiStem(keywordToken, token))) {
|
|
171
202
|
score += weight * 2;
|
|
172
203
|
matchedTokens.add(token);
|
|
173
204
|
} else if (prepared.keywordPhrases.some(phrase => phrase.includes(token))) {
|
|
@@ -266,13 +297,13 @@ export function paginate<T>(items: T[], offset: number, limit: number): { items:
|
|
|
266
297
|
};
|
|
267
298
|
}
|
|
268
299
|
|
|
269
|
-
export function rankSuggestions(state: McpExtensionState, name: string, limit: number): string[] {
|
|
300
|
+
export function rankSuggestions(state: McpExtensionState, name: string, limit: number, server?: string): string[] {
|
|
270
301
|
const stripped = Object.keys(state.config.mcpServers)
|
|
271
|
-
.flatMap(
|
|
272
|
-
.map(prefix => getServerPrefix(
|
|
302
|
+
.flatMap(serverName => (["server", "short", "mcp"] as const)
|
|
303
|
+
.map(prefix => getServerPrefix(serverName, prefix)))
|
|
273
304
|
.filter((candidate): candidate is string => Boolean(candidate) && name.startsWith(`${candidate}_`))
|
|
274
305
|
.sort((a, b) => b.length - a.length)
|
|
275
306
|
.map(candidate => name.slice(candidate.length + 1));
|
|
276
307
|
const query = stripped[0] ?? name;
|
|
277
|
-
return rankToolMatches(state, query,
|
|
308
|
+
return rankToolMatches(state, query, server, false).slice(0, limit).map(match => match.tool.name);
|
|
278
309
|
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
export interface SecureKeyringStore {
|
|
8
|
+
read(account: string): string | undefined;
|
|
9
|
+
write(account: string, payload: string): void;
|
|
10
|
+
remove(account: string): void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type KeyringEntry = {
|
|
14
|
+
getPassword(): string | null;
|
|
15
|
+
setPassword(password: string): void;
|
|
16
|
+
deleteCredential(): boolean;
|
|
17
|
+
};
|
|
18
|
+
type KeyringEntryConstructor = new (service: string, account: string) => KeyringEntry;
|
|
19
|
+
type KeyringModule = { Entry: KeyringEntryConstructor };
|
|
20
|
+
type KeyringRequire = ((id: string) => unknown) & { resolve(id: string): string };
|
|
21
|
+
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
const TEST_STORE_ENV = "PI_MCP_ADAPTER_TEST_AUTH_STORE";
|
|
24
|
+
const memoryEntries = new Map<string, string>();
|
|
25
|
+
let memoryReadCount = 0;
|
|
26
|
+
let KeyringEntryClass: KeyringEntryConstructor | undefined;
|
|
27
|
+
|
|
28
|
+
function key(service: string, account: string): string {
|
|
29
|
+
return `${service}\0${account}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function nativeStore(service: string): SecureKeyringStore {
|
|
33
|
+
const entry = (account: string): KeyringEntry => {
|
|
34
|
+
try {
|
|
35
|
+
KeyringEntryClass ??= loadKeyringEntryClass();
|
|
36
|
+
return new KeyringEntryClass(service, account);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
throw new Error("OS secure credential storage is unavailable. Configure or unlock the OS credential store and retry.", { cause: error });
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const run = (operation: "read" | "write" | "remove", account: string, payload?: string): string | undefined => {
|
|
42
|
+
try {
|
|
43
|
+
const credential = entry(account);
|
|
44
|
+
if (operation === "read") return credential.getPassword() ?? undefined;
|
|
45
|
+
if (operation === "write") credential.setPassword(payload!);
|
|
46
|
+
else credential.deleteCredential();
|
|
47
|
+
return undefined;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (!shouldRecoverKeyring(error)) throw error;
|
|
50
|
+
return recoverKeyring(operation, service, account, payload);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
return {
|
|
54
|
+
read: account => run("read", account),
|
|
55
|
+
write: (account, payload) => { run("write", account, payload); },
|
|
56
|
+
remove: account => { run("remove", account); },
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function shouldRecoverKeyring(error: unknown): boolean {
|
|
61
|
+
if (process.platform !== "linux" || process.env.PI_MCP_ADAPTER_DISABLE_KEYRING_RECOVERY === "1") return false;
|
|
62
|
+
const seen = new Set<unknown>();
|
|
63
|
+
for (let depth = 0; depth < 16 && error !== undefined && !seen.has(error); depth++) {
|
|
64
|
+
seen.add(error);
|
|
65
|
+
const candidate = typeof error === "object" && error !== null
|
|
66
|
+
? error as { name?: unknown; message?: unknown; code?: unknown; cause?: unknown }
|
|
67
|
+
: { message: error };
|
|
68
|
+
if ([candidate.name, candidate.message, candidate.code].some(value =>
|
|
69
|
+
typeof value === "string" && /\b(?:KeyRevoked|key\s+(?:has been\s+)?revoked)\b/i.test(value))) return true;
|
|
70
|
+
error = candidate.cause;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function recoverKeyring(operation: "read" | "write" | "remove", service: string, account: string, payload?: string): string | undefined {
|
|
76
|
+
try {
|
|
77
|
+
const adjacentHelper = new URL("./mcp-keyring-helper.cjs", import.meta.url);
|
|
78
|
+
const helper = existsSync(adjacentHelper) ? adjacentHelper : new URL("../mcp-keyring-helper.cjs", import.meta.url);
|
|
79
|
+
const result = spawnSync("keyctl", ["session", "-", process.execPath, fileURLToPath(helper)], {
|
|
80
|
+
input: `${JSON.stringify({ operation, service, account, payload })}\n`,
|
|
81
|
+
encoding: "utf8",
|
|
82
|
+
stdio: "pipe",
|
|
83
|
+
shell: false,
|
|
84
|
+
timeout: 10_000,
|
|
85
|
+
killSignal: "SIGKILL",
|
|
86
|
+
maxBuffer: 1024 * 1024,
|
|
87
|
+
windowsHide: true,
|
|
88
|
+
});
|
|
89
|
+
if (result.error || result.status !== 0 || result.signal) throw new Error();
|
|
90
|
+
const response: unknown = JSON.parse(result.stdout);
|
|
91
|
+
if (typeof response !== "object" || response === null || Array.isArray(response)) throw new Error();
|
|
92
|
+
const value = response as { ok?: unknown; found?: unknown; value?: unknown };
|
|
93
|
+
if (value.ok !== true) throw new Error();
|
|
94
|
+
const allowedKeys = operation === "read" ? ["ok", "found", "value"] : ["ok"];
|
|
95
|
+
if (Object.keys(response).some(key => !allowedKeys.includes(key))) throw new Error();
|
|
96
|
+
if (operation === "read") {
|
|
97
|
+
if (value.found === true && typeof value.value === "string") return value.value;
|
|
98
|
+
if (value.found === false && value.value === undefined) return undefined;
|
|
99
|
+
throw new Error();
|
|
100
|
+
}
|
|
101
|
+
return undefined;
|
|
102
|
+
} catch {
|
|
103
|
+
throw new Error("Linux keyring recovery failed. Configure or unlock the OS credential store and retry.");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function createSecureKeyringStore(service: string): SecureKeyringStore {
|
|
108
|
+
const mode = process.env[TEST_STORE_ENV];
|
|
109
|
+
if (mode === "memory" || mode === "sizelimited") {
|
|
110
|
+
return {
|
|
111
|
+
read(account) { memoryReadCount++; return memoryEntries.get(key(service, account)); },
|
|
112
|
+
write(account, payload) {
|
|
113
|
+
if (mode === "sizelimited" && payload.length > 1280) throw new Error("secure credential value exceeds platform limit");
|
|
114
|
+
memoryEntries.set(key(service, account), payload);
|
|
115
|
+
},
|
|
116
|
+
remove(account) { memoryEntries.delete(key(service, account)); },
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (mode === "unavailable") {
|
|
120
|
+
return {
|
|
121
|
+
read() { memoryReadCount++; throw new Error("simulated secure credential store unavailable"); },
|
|
122
|
+
write() { throw new Error("simulated secure credential store unavailable"); },
|
|
123
|
+
remove() { throw new Error("simulated secure credential store unavailable"); },
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return nativeStore(service);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function loadKeyringEntryClass(keyringRequire: KeyringRequire = require, platform: NodeJS.Platform = process.platform, arch: NodeJS.Architecture = process.arch): KeyringEntryConstructor {
|
|
130
|
+
try {
|
|
131
|
+
return (keyringRequire("@napi-rs/keyring") as KeyringModule).Entry;
|
|
132
|
+
} catch (loaderError) {
|
|
133
|
+
try {
|
|
134
|
+
return loadKeyringNativeBindingFallback(keyringRequire, platform, arch).Entry;
|
|
135
|
+
} catch (fallbackError) {
|
|
136
|
+
const message = fallbackError instanceof Error ? fallbackError.message : String(fallbackError);
|
|
137
|
+
throw new Error(`Failed to load @napi-rs/keyring; absolute-path native binding fallback also failed: ${message}`, { cause: loaderError });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function loadKeyringNativeBindingFallback(keyringRequire: KeyringRequire, platform: NodeJS.Platform, arch: NodeJS.Architecture): KeyringModule {
|
|
143
|
+
let lastError: unknown;
|
|
144
|
+
for (const suffix of getKeyringNativeBindingSuffixes(platform, arch)) {
|
|
145
|
+
try {
|
|
146
|
+
const packageName = `@napi-rs/keyring-${suffix}`;
|
|
147
|
+
const packageJsonPath = keyringRequire.resolve(`${packageName}/package.json`);
|
|
148
|
+
return keyringRequire(join(dirname(packageJsonPath), `keyring.${suffix}.node`)) as KeyringModule;
|
|
149
|
+
} catch (error) { lastError = error; }
|
|
150
|
+
}
|
|
151
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function getKeyringNativeBindingSuffixes(platform: NodeJS.Platform, arch: NodeJS.Architecture): string[] {
|
|
155
|
+
if (platform === "darwin") return arch === "arm64" ? ["darwin-arm64"] : arch === "x64" ? ["darwin-x64"] : [];
|
|
156
|
+
if (platform === "win32") return arch === "arm64" ? ["win32-arm64-msvc"] : arch === "x64" ? ["win32-x64-msvc"] : arch === "ia32" ? ["win32-ia32-msvc"] : [];
|
|
157
|
+
if (platform === "linux") {
|
|
158
|
+
if (arch === "arm64") return ["linux-arm64-gnu", "linux-arm64-musl"];
|
|
159
|
+
if (arch === "arm") return ["linux-arm-gnueabihf"];
|
|
160
|
+
if (arch === "riscv64") return ["linux-riscv64-gnu"];
|
|
161
|
+
if (arch === "x64") return ["linux-x64-gnu", "linux-x64-musl"];
|
|
162
|
+
}
|
|
163
|
+
return platform === "freebsd" && arch === "x64" ? ["freebsd-x64"] : [];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function resetTestSecureKeyring(): void { memoryEntries.clear(); memoryReadCount = 0; }
|
|
167
|
+
export function getTestSecureKeyringReadCount(): number { return memoryReadCount; }
|
|
168
|
+
export function getTestSecureKeyringEntries(): [string, string][] { return [...memoryEntries.entries()]; }
|
|
169
|
+
export function setTestSecureKeyringEntry(service: string, account: string, payload: string): void { memoryEntries.set(key(service, account), payload); }
|
|
170
|
+
export function removeTestSecureKeyringEntry(service: string, account: string): void { memoryEntries.delete(key(service, account)); }
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { areJevSourcesAllowed, evaluateJev, resolveSemanticJevSettings } from "./jev-client.ts";
|
|
3
|
+
import type { JevEvaluateInput, JevEvaluationEnvelope } from "./jev-contracts.ts";
|
|
4
|
+
import { isServerInActiveFailureBackoff } from "./failure-backoff.ts";
|
|
5
|
+
import { rankToolMatches, type RankedToolMatch } from "./search-ranking.ts";
|
|
6
|
+
import type { McpExtensionState } from "./state.ts";
|
|
7
|
+
import type { ToolMetadata } from "./types.ts";
|
|
8
|
+
import { isServerDisabled } from "./types.ts";
|
|
9
|
+
|
|
10
|
+
export type SemanticSearchEvaluator = (
|
|
11
|
+
state: McpExtensionState,
|
|
12
|
+
input: JevEvaluateInput,
|
|
13
|
+
options: { purpose: "semantic-search"; signal?: AbortSignal; observedSources?: readonly string[] },
|
|
14
|
+
) => Promise<JevEvaluationEnvelope>;
|
|
15
|
+
|
|
16
|
+
export type SemanticSearchBackend =
|
|
17
|
+
| { requested: "semantic"; used: "semantic"; degraded: false; model: string; usage: { inputTokens: number; outputTokens: number }; abstained: boolean }
|
|
18
|
+
| { requested: "semantic"; used: "lexical"; degraded: true; reason: "timeout" | "rate_limited" | "service_unavailable" };
|
|
19
|
+
|
|
20
|
+
export type SemanticSearchResult =
|
|
21
|
+
| { ok: true; matches: RankedToolMatch[]; backend: SemanticSearchBackend }
|
|
22
|
+
| { ok: false; error: { code: string; message: string } };
|
|
23
|
+
|
|
24
|
+
interface Candidate {
|
|
25
|
+
id: string;
|
|
26
|
+
server: string;
|
|
27
|
+
description: string;
|
|
28
|
+
tool: ToolMetadata;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function truncateUtf8(value: string, maxBytes: number): string {
|
|
32
|
+
const bytes = Buffer.from(value, "utf8");
|
|
33
|
+
if (bytes.length <= maxBytes) return value;
|
|
34
|
+
let end = maxBytes;
|
|
35
|
+
while (end > 0 && ((bytes[end] ?? 0) & 0xc0) === 0x80) end -= 1;
|
|
36
|
+
return bytes.subarray(0, end).toString("utf8");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function stableHash(value: string): string {
|
|
40
|
+
return createHash("sha256").update(value).digest("hex");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function eligibleMatches(state: McpExtensionState, server: string | undefined, allowed: Set<string>): RankedToolMatch[] {
|
|
44
|
+
const matches: RankedToolMatch[] = [];
|
|
45
|
+
for (const [serverName, metadata] of state.toolMetadata) {
|
|
46
|
+
if ((server && serverName !== server) || !allowed.has(serverName)
|
|
47
|
+
|| isServerDisabled(state.config.mcpServers[serverName]) || isServerInActiveFailureBackoff(state, serverName)) continue;
|
|
48
|
+
for (const tool of metadata) matches.push({ server: serverName, tool, score: 0 });
|
|
49
|
+
}
|
|
50
|
+
return matches;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function roundRobinNonLexical(query: string, matches: RankedToolMatch[]): RankedToolMatch[] {
|
|
54
|
+
const buckets = new Map<string, RankedToolMatch[]>();
|
|
55
|
+
for (const match of matches) {
|
|
56
|
+
const bucket = buckets.get(match.server) ?? [];
|
|
57
|
+
bucket.push(match);
|
|
58
|
+
buckets.set(match.server, bucket);
|
|
59
|
+
}
|
|
60
|
+
const servers = [...buckets.keys()].sort((a, b) => a.localeCompare(b));
|
|
61
|
+
for (const bucket of buckets.values()) {
|
|
62
|
+
bucket.sort((a, b) => stableHash(query + a.tool.name).localeCompare(stableHash(query + b.tool.name)) || a.tool.name.localeCompare(b.tool.name));
|
|
63
|
+
}
|
|
64
|
+
if (servers.length > 1) {
|
|
65
|
+
const start = Number.parseInt(stableHash(query).slice(0, 8), 16) % servers.length;
|
|
66
|
+
servers.push(...servers.splice(0, start));
|
|
67
|
+
}
|
|
68
|
+
const result: RankedToolMatch[] = [];
|
|
69
|
+
let index = 0;
|
|
70
|
+
while (result.length < matches.length) {
|
|
71
|
+
let added = false;
|
|
72
|
+
for (const server of servers) {
|
|
73
|
+
const match = buckets.get(server)?.[index];
|
|
74
|
+
if (match) { result.push(match); added = true; }
|
|
75
|
+
}
|
|
76
|
+
if (!added) break;
|
|
77
|
+
index += 1;
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function selectSemanticCandidates(
|
|
83
|
+
state: McpExtensionState,
|
|
84
|
+
query: string,
|
|
85
|
+
server: string | undefined,
|
|
86
|
+
allowedServers: string[],
|
|
87
|
+
limit: number,
|
|
88
|
+
): Candidate[] {
|
|
89
|
+
const eligible = eligibleMatches(state, server, new Set(allowedServers));
|
|
90
|
+
let selected = eligible;
|
|
91
|
+
if (eligible.length > limit) {
|
|
92
|
+
const eligiblePaths = new Set(eligible.map(match => `${match.server}\0${match.tool.name}`));
|
|
93
|
+
const lexical = rankToolMatches(state, query, server)
|
|
94
|
+
.filter(match => eligiblePaths.has(`${match.server}\0${match.tool.name}`));
|
|
95
|
+
const lexicalCount = Math.floor(limit / 2);
|
|
96
|
+
const first = lexical.slice(0, lexicalCount);
|
|
97
|
+
const lexicalPaths = new Set(lexical.map(match => `${match.server}\0${match.tool.name}`));
|
|
98
|
+
const broad = roundRobinNonLexical(query, eligible.filter(match => !lexicalPaths.has(`${match.server}\0${match.tool.name}`)));
|
|
99
|
+
selected = [...first, ...broad.slice(0, limit - first.length)];
|
|
100
|
+
}
|
|
101
|
+
return selected.map((match, index) => ({ id: `c${index}`, server: match.server,
|
|
102
|
+
description: truncateUtf8(match.tool.description ?? "", 512), tool: match.tool }));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function semanticSearch(
|
|
106
|
+
state: McpExtensionState,
|
|
107
|
+
query: string,
|
|
108
|
+
server?: string,
|
|
109
|
+
signal?: AbortSignal,
|
|
110
|
+
evaluator: SemanticSearchEvaluator = evaluateJev,
|
|
111
|
+
observedSources: readonly string[] = [],
|
|
112
|
+
): Promise<SemanticSearchResult> {
|
|
113
|
+
let settings;
|
|
114
|
+
try {
|
|
115
|
+
settings = resolveSemanticJevSettings(state);
|
|
116
|
+
} catch {
|
|
117
|
+
return { ok: false, error: { code: "invalid_request", message: "Invalid TypeSafe semantic search settings." } };
|
|
118
|
+
}
|
|
119
|
+
if (!settings.semanticSearch) {
|
|
120
|
+
return { ok: false, error: { code: "disabled", message: "TypeSafe semantic search is disabled." } };
|
|
121
|
+
}
|
|
122
|
+
if (settings.allowedServers.length === 0) {
|
|
123
|
+
return { ok: false, error: { code: "data_policy_denied", message: "Semantic search is enabled, but settings.jev.allowedServers is empty. Run /mcp jev setup or allow specific MCP servers." } };
|
|
124
|
+
}
|
|
125
|
+
if (server && !settings.allowedServers.includes(server)) {
|
|
126
|
+
return { ok: false, error: { code: "data_policy_denied", message: `Server "${server}" is not allowed by settings.jev.allowedServers.` } };
|
|
127
|
+
}
|
|
128
|
+
if (!areJevSourcesAllowed(state, settings, observedSources)) {
|
|
129
|
+
return { ok: false, error: { code: "data_policy_denied", message: "Evaluation sources are not allowed by policy." } };
|
|
130
|
+
}
|
|
131
|
+
if (query.trim().length === 0) {
|
|
132
|
+
return { ok: false, error: { code: "empty_query", message: "Semantic search query cannot be empty." } };
|
|
133
|
+
}
|
|
134
|
+
const candidates = selectSemanticCandidates(state, query, server, settings.allowedServers, settings.semanticCandidateLimit);
|
|
135
|
+
if (candidates.length === 0) {
|
|
136
|
+
return { ok: false, error: { code: "no_eligible_tools", message: "Semantic search has no eligible cached tools from the allowed servers. Connect an allowed server or update settings.jev.allowedServers." } };
|
|
137
|
+
}
|
|
138
|
+
const input: JevEvaluateInput = {
|
|
139
|
+
state: {
|
|
140
|
+
query,
|
|
141
|
+
candidates: candidates.map(candidate => ({ id: candidate.id, path: candidate.tool.name,
|
|
142
|
+
name: candidate.tool.originalName, server: candidate.server, description: candidate.description })),
|
|
143
|
+
},
|
|
144
|
+
questions: {
|
|
145
|
+
match: {
|
|
146
|
+
type: "choice",
|
|
147
|
+
instructions: "Rank which tool best matches the query. Choose none when no tool is suitable.",
|
|
148
|
+
criteria: Object.fromEntries([
|
|
149
|
+
...candidates.map(candidate => [candidate.id, { path: candidate.tool.name }]),
|
|
150
|
+
["none", { noSuitableTool: true }],
|
|
151
|
+
]),
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
sources: [...new Set(candidates.map(candidate => candidate.server))],
|
|
155
|
+
};
|
|
156
|
+
const envelope = await evaluator(state, input, { purpose: "semantic-search", ...(signal ? { signal } : {}), ...(observedSources.length > 0 ? { observedSources } : {}) });
|
|
157
|
+
if (!envelope.ok) {
|
|
158
|
+
if (envelope.error.code === "timeout" || envelope.error.code === "rate_limited" || envelope.error.code === "service_unavailable") {
|
|
159
|
+
return {
|
|
160
|
+
ok: true,
|
|
161
|
+
matches: rankToolMatches(state, query, server),
|
|
162
|
+
backend: { requested: "semantic", used: "lexical", degraded: true, reason: envelope.error.code },
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return { ok: false, error: { code: envelope.error.code, message: envelope.error.message } };
|
|
166
|
+
}
|
|
167
|
+
const answer = envelope.data.answers.match;
|
|
168
|
+
if (!answer || answer.type !== "choice") {
|
|
169
|
+
return { ok: false, error: { code: "invalid_response", message: "TypeSafe returned an invalid semantic search response." } };
|
|
170
|
+
}
|
|
171
|
+
const ranked = candidates
|
|
172
|
+
.map(candidate => ({ candidate, probability: answer.probabilities[candidate.id] ?? 0 }))
|
|
173
|
+
.sort((a, b) => b.probability - a.probability || a.candidate.id.localeCompare(b.candidate.id));
|
|
174
|
+
const top = ranked[0];
|
|
175
|
+
const noneProbability = answer.probabilities.none ?? 0;
|
|
176
|
+
const abstained = answer.choice === "none" || !top || noneProbability >= top.probability || top.probability < settings.semanticMinProbability;
|
|
177
|
+
const matches = abstained ? [] : ranked.map(({ candidate, probability }) => ({ server: candidate.server, tool: candidate.tool, score: probability }));
|
|
178
|
+
return {
|
|
179
|
+
ok: true,
|
|
180
|
+
matches,
|
|
181
|
+
backend: {
|
|
182
|
+
requested: "semantic", used: "semantic", degraded: false, model: envelope.data.model,
|
|
183
|
+
usage: envelope.data.usage, abstained,
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|