@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.
Files changed (192) hide show
  1. package/README.md +4 -4
  2. package/THIRD_PARTY_NOTICES.md +1 -1
  3. package/node_modules/@herbertgao/pi-cc-extensions/package.json +1 -1
  4. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
  5. package/node_modules/@herbertgao/pi-subagents/package.json +1 -1
  6. package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +5 -4
  7. package/node_modules/@herbertgao/pi-subagents/src/mention-clone.ts +39 -16
  8. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  9. package/node_modules/@narumitw/pi-btw/README.md +21 -4
  10. package/node_modules/@narumitw/pi-btw/dist/index.ts +1668 -958
  11. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +4 -4
  12. package/node_modules/@narumitw/pi-btw/docs/workflows.md +10 -2
  13. package/node_modules/@narumitw/pi-btw/package.json +1 -1
  14. package/node_modules/@narumitw/pi-btw/src/btw.ts +17 -79
  15. package/node_modules/@narumitw/pi-btw/src/conversation-context.ts +74 -0
  16. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +196 -7
  17. package/node_modules/@narumitw/pi-btw/src/main-thread-updates.ts +40 -0
  18. package/node_modules/@narumitw/pi-btw/src/menu.ts +34 -2
  19. package/node_modules/@narumitw/pi-btw/src/settings.ts +49 -0
  20. package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +9 -1
  21. package/node_modules/@narumitw/pi-btw/src/workspace-layout.ts +559 -0
  22. package/node_modules/pi-mcp-adapter/CHANGELOG.md +62 -0
  23. package/node_modules/pi-mcp-adapter/README.md +75 -7
  24. package/node_modules/pi-mcp-adapter/bearer-command-resolver.ts +193 -0
  25. package/node_modules/pi-mcp-adapter/cli.js +56 -5
  26. package/node_modules/pi-mcp-adapter/commands.ts +96 -5
  27. package/node_modules/pi-mcp-adapter/config.ts +73 -6
  28. package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +22 -28
  29. package/node_modules/pi-mcp-adapter/direct-tools.ts +13 -5
  30. package/node_modules/pi-mcp-adapter/dist/abort.d.ts +2 -0
  31. package/node_modules/pi-mcp-adapter/dist/abort.js +36 -0
  32. package/node_modules/pi-mcp-adapter/dist/abort.js.map +1 -0
  33. package/node_modules/pi-mcp-adapter/dist/bearer-command-resolver.d.ts +6 -0
  34. package/node_modules/pi-mcp-adapter/dist/bearer-command-resolver.js +194 -0
  35. package/node_modules/pi-mcp-adapter/dist/bearer-command-resolver.js.map +1 -0
  36. package/node_modules/pi-mcp-adapter/dist/config.d.ts +5 -0
  37. package/node_modules/pi-mcp-adapter/dist/config.js +75 -6
  38. package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
  39. package/node_modules/pi-mcp-adapter/dist/consent-manager.d.ts +18 -0
  40. package/node_modules/pi-mcp-adapter/dist/consent-manager.js +88 -0
  41. package/node_modules/pi-mcp-adapter/dist/consent-manager.js.map +1 -0
  42. package/node_modules/pi-mcp-adapter/dist/elicitation-handler.d.ts +17 -0
  43. package/node_modules/pi-mcp-adapter/dist/elicitation-handler.js +316 -0
  44. package/node_modules/pi-mcp-adapter/dist/elicitation-handler.js.map +1 -0
  45. package/node_modules/pi-mcp-adapter/dist/errors.d.ts +131 -0
  46. package/node_modules/pi-mcp-adapter/dist/errors.js +278 -0
  47. package/node_modules/pi-mcp-adapter/dist/errors.js.map +1 -0
  48. package/node_modules/pi-mcp-adapter/dist/http-ca.d.ts +8 -0
  49. package/node_modules/pi-mcp-adapter/dist/http-ca.js +88 -0
  50. package/node_modules/pi-mcp-adapter/dist/http-ca.js.map +1 -0
  51. package/node_modules/pi-mcp-adapter/dist/jev-client.d.ts +15 -0
  52. package/node_modules/pi-mcp-adapter/dist/jev-client.js +340 -0
  53. package/node_modules/pi-mcp-adapter/dist/jev-client.js.map +1 -0
  54. package/node_modules/pi-mcp-adapter/dist/jev-contracts.d.ts +77 -0
  55. package/node_modules/pi-mcp-adapter/dist/jev-contracts.js +2 -0
  56. package/node_modules/pi-mcp-adapter/dist/jev-contracts.js.map +1 -0
  57. package/node_modules/pi-mcp-adapter/dist/jev-key-store.d.ts +22 -0
  58. package/node_modules/pi-mcp-adapter/dist/jev-key-store.js +85 -0
  59. package/node_modules/pi-mcp-adapter/dist/jev-key-store.js.map +1 -0
  60. package/node_modules/pi-mcp-adapter/dist/json-schema-validator.d.ts +2 -0
  61. package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js +55 -0
  62. package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js.map +1 -0
  63. package/node_modules/pi-mcp-adapter/dist/lifecycle.d.ts +56 -0
  64. package/node_modules/pi-mcp-adapter/dist/lifecycle.js +410 -0
  65. package/node_modules/pi-mcp-adapter/dist/lifecycle.js.map +1 -0
  66. package/node_modules/pi-mcp-adapter/dist/logger.d.ts +51 -0
  67. package/node_modules/pi-mcp-adapter/dist/logger.js +131 -0
  68. package/node_modules/pi-mcp-adapter/dist/logger.js.map +1 -0
  69. package/node_modules/pi-mcp-adapter/dist/mcp-auth-fetch.d.ts +26 -0
  70. package/node_modules/pi-mcp-adapter/dist/mcp-auth-fetch.js +103 -0
  71. package/node_modules/pi-mcp-adapter/dist/mcp-auth-fetch.js.map +1 -0
  72. package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.d.ts +118 -0
  73. package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.js +1098 -0
  74. package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.js.map +1 -0
  75. package/node_modules/pi-mcp-adapter/dist/mcp-auth.d.ts +168 -0
  76. package/node_modules/pi-mcp-adapter/dist/mcp-auth.js +1117 -0
  77. package/node_modules/pi-mcp-adapter/dist/mcp-auth.js.map +1 -0
  78. package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js +7 -140
  79. package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js.map +1 -1
  80. package/node_modules/pi-mcp-adapter/dist/mcp-callback-server.d.ts +53 -0
  81. package/node_modules/pi-mcp-adapter/dist/mcp-callback-server.js +441 -0
  82. package/node_modules/pi-mcp-adapter/dist/mcp-callback-server.js.map +1 -0
  83. package/node_modules/pi-mcp-adapter/dist/mcp-oauth-provider.d.ts +148 -0
  84. package/node_modules/pi-mcp-adapter/dist/mcp-oauth-provider.js +689 -0
  85. package/node_modules/pi-mcp-adapter/dist/mcp-oauth-provider.js.map +1 -0
  86. package/node_modules/pi-mcp-adapter/dist/mcp-probe.d.ts +6 -0
  87. package/node_modules/pi-mcp-adapter/dist/mcp-probe.js +166 -0
  88. package/node_modules/pi-mcp-adapter/dist/mcp-probe.js.map +1 -0
  89. package/node_modules/pi-mcp-adapter/dist/mcp-tasks.d.ts +102 -0
  90. package/node_modules/pi-mcp-adapter/dist/mcp-tasks.js +369 -0
  91. package/node_modules/pi-mcp-adapter/dist/mcp-tasks.js.map +1 -0
  92. package/node_modules/pi-mcp-adapter/dist/mcp-trace.d.ts +95 -0
  93. package/node_modules/pi-mcp-adapter/dist/mcp-trace.js +242 -0
  94. package/node_modules/pi-mcp-adapter/dist/mcp-trace.js.map +1 -0
  95. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +2 -11
  96. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -1
  97. package/node_modules/pi-mcp-adapter/dist/npx-resolver.d.ts +6 -0
  98. package/node_modules/pi-mcp-adapter/dist/npx-resolver.js +505 -0
  99. package/node_modules/pi-mcp-adapter/dist/npx-resolver.js.map +1 -0
  100. package/node_modules/pi-mcp-adapter/dist/request-headers-command.d.ts +10 -0
  101. package/node_modules/pi-mcp-adapter/dist/request-headers-command.js +312 -0
  102. package/node_modules/pi-mcp-adapter/dist/request-headers-command.js.map +1 -0
  103. package/node_modules/pi-mcp-adapter/dist/runtime-owner.d.ts +13 -0
  104. package/node_modules/pi-mcp-adapter/dist/runtime-owner.js +97 -0
  105. package/node_modules/pi-mcp-adapter/dist/runtime-owner.js.map +1 -0
  106. package/node_modules/pi-mcp-adapter/dist/sampling-handler.d.ts +17 -0
  107. package/node_modules/pi-mcp-adapter/dist/sampling-handler.js +203 -0
  108. package/node_modules/pi-mcp-adapter/dist/sampling-handler.js.map +1 -0
  109. package/node_modules/pi-mcp-adapter/dist/secure-keyring.d.ts +11 -0
  110. package/node_modules/pi-mcp-adapter/dist/secure-keyring.js +172 -0
  111. package/node_modules/pi-mcp-adapter/dist/secure-keyring.js.map +1 -0
  112. package/node_modules/pi-mcp-adapter/dist/server-manager.d.ts +157 -0
  113. package/node_modules/pi-mcp-adapter/dist/server-manager.js +1743 -0
  114. package/node_modules/pi-mcp-adapter/dist/server-manager.js.map +1 -0
  115. package/node_modules/pi-mcp-adapter/dist/session-approvals.d.ts +34 -0
  116. package/node_modules/pi-mcp-adapter/dist/session-approvals.js +140 -0
  117. package/node_modules/pi-mcp-adapter/dist/session-approvals.js.map +1 -0
  118. package/node_modules/pi-mcp-adapter/dist/session-recovery.d.ts +26 -0
  119. package/node_modules/pi-mcp-adapter/dist/session-recovery.js +142 -0
  120. package/node_modules/pi-mcp-adapter/dist/session-recovery.js.map +1 -0
  121. package/node_modules/pi-mcp-adapter/dist/state.d.ts +73 -0
  122. package/node_modules/pi-mcp-adapter/dist/state.js +2 -0
  123. package/node_modules/pi-mcp-adapter/dist/state.js.map +1 -0
  124. package/node_modules/pi-mcp-adapter/dist/types.d.ts +36 -0
  125. package/node_modules/pi-mcp-adapter/dist/types.js +18 -0
  126. package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
  127. package/node_modules/pi-mcp-adapter/dist/ui-resource-handler.d.ts +17 -0
  128. package/node_modules/pi-mcp-adapter/dist/ui-resource-handler.js +219 -0
  129. package/node_modules/pi-mcp-adapter/dist/ui-resource-handler.js.map +1 -0
  130. package/node_modules/pi-mcp-adapter/dist/unix-socket-transport.d.ts +15 -0
  131. package/node_modules/pi-mcp-adapter/dist/unix-socket-transport.js +85 -0
  132. package/node_modules/pi-mcp-adapter/dist/unix-socket-transport.js.map +1 -0
  133. package/node_modules/pi-mcp-adapter/dist/utils.d.ts +1 -0
  134. package/node_modules/pi-mcp-adapter/dist/utils.js +13 -0
  135. package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -1
  136. package/node_modules/pi-mcp-adapter/elicitation-handler.ts +29 -19
  137. package/node_modules/pi-mcp-adapter/examples/jev-accessibility-loop.mjs +85 -0
  138. package/node_modules/pi-mcp-adapter/examples/jev-semantic-filter.mjs +34 -0
  139. package/node_modules/pi-mcp-adapter/index.ts +129 -6
  140. package/node_modules/pi-mcp-adapter/init.ts +6 -19
  141. package/node_modules/pi-mcp-adapter/jev-client.ts +273 -0
  142. package/node_modules/pi-mcp-adapter/jev-contracts.ts +53 -0
  143. package/node_modules/pi-mcp-adapter/jev-key-store.ts +79 -0
  144. package/node_modules/pi-mcp-adapter/lifecycle.ts +14 -3
  145. package/node_modules/pi-mcp-adapter/mcp-bearer-store.ts +7 -142
  146. package/node_modules/pi-mcp-adapter/mcp-code.ts +119 -11
  147. package/node_modules/pi-mcp-adapter/mcp-output-guard.ts +4 -0
  148. package/node_modules/pi-mcp-adapter/mcp-references.ts +5 -3
  149. package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +5 -0
  150. package/node_modules/pi-mcp-adapter/mcp-tasks.ts +467 -0
  151. package/node_modules/pi-mcp-adapter/metadata-cache.ts +2 -13
  152. package/node_modules/pi-mcp-adapter/namespace-tools.ts +1 -1
  153. package/node_modules/pi-mcp-adapter/package.json +17 -9
  154. package/node_modules/pi-mcp-adapter/proxy-modes.ts +319 -166
  155. package/node_modules/pi-mcp-adapter/request-headers-command.ts +6 -3
  156. package/node_modules/pi-mcp-adapter/search-ranking.ts +38 -7
  157. package/node_modules/pi-mcp-adapter/secure-keyring.ts +170 -0
  158. package/node_modules/pi-mcp-adapter/semantic-search.ts +186 -0
  159. package/node_modules/pi-mcp-adapter/server-manager.ts +293 -50
  160. package/node_modules/pi-mcp-adapter/session-approvals.ts +14 -0
  161. package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +2 -0
  162. package/node_modules/pi-mcp-adapter/state.ts +3 -1
  163. package/node_modules/pi-mcp-adapter/tool-approval.ts +26 -4
  164. package/node_modules/pi-mcp-adapter/tool-metadata.ts +6 -14
  165. package/node_modules/pi-mcp-adapter/tool-registrar.ts +8 -6
  166. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +4 -1
  167. package/node_modules/pi-mcp-adapter/types.ts +53 -0
  168. package/node_modules/pi-mcp-adapter/ui-server.ts +17 -9
  169. package/node_modules/pi-mcp-adapter/utils.ts +16 -0
  170. package/node_modules/pi-multi-account/CHANGELOG.md +23 -0
  171. package/node_modules/pi-multi-account/README.md +38 -11
  172. package/node_modules/pi-multi-account/index.ts +371 -96
  173. package/node_modules/pi-multi-account/package.json +5 -4
  174. package/node_modules/pi-multi-account/provider-payload-stream.ts +36 -28
  175. package/node_modules/pi-multi-account/usage.ts +31 -2
  176. package/node_modules/pi-typesafe/README.md +3 -1
  177. package/node_modules/pi-typesafe/dist/auth.d.ts +10 -3
  178. package/node_modules/pi-typesafe/dist/auth.js +13 -7
  179. package/node_modules/pi-typesafe/dist/backends.d.ts +31 -0
  180. package/node_modules/pi-typesafe/dist/backends.js +33 -0
  181. package/node_modules/pi-typesafe/dist/client.d.ts +3 -9
  182. package/node_modules/pi-typesafe/dist/client.js +63 -39
  183. package/node_modules/pi-typesafe/dist/credentials.d.ts +11 -5
  184. package/node_modules/pi-typesafe/dist/credentials.js +15 -8
  185. package/node_modules/pi-typesafe/dist/extension.js +4 -1
  186. package/node_modules/pi-typesafe/dist/index.d.ts +1 -1
  187. package/node_modules/pi-typesafe/dist/index.js +1 -1
  188. package/node_modules/pi-typesafe/dist/login.d.ts +13 -7
  189. package/node_modules/pi-typesafe/dist/login.js +17 -8
  190. package/node_modules/pi-typesafe/dist/schema.js +19 -13
  191. package/node_modules/pi-typesafe/package.json +1 -1
  192. 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
- ): FetchLike {
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
- }, request.signal);
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: request.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
- return normalizeSearchText(value).split(/[^a-z0-9]+/).filter(Boolean);
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 => fieldToken.startsWith(token) || (fieldToken.length >= MIN_STEM_LENGTH && token.startsWith(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 => keywordToken.startsWith(token) || (keywordToken.length >= MIN_STEM_LENGTH && token.startsWith(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(server => (["server", "short", "mcp"] as const)
272
- .map(prefix => getServerPrefix(server, prefix)))
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, undefined, false).slice(0, limit).map(match => match.tool.name);
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
+ }