@herbertgao/pi-extensions 2026.9.18 → 2026.9.20

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 (226) hide show
  1. package/README.md +20 -28
  2. package/THIRD_PARTY_NOTICES.md +1 -1
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +76 -59
  4. package/node_modules/@herbertgao/pi-cc-extensions/README.md +76 -59
  5. package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/preview.webp +0 -0
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +93 -34
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +346 -117
  8. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/agent-summary/core.ts +6 -36
  9. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/agent-summary/index.ts +2 -2
  10. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +24 -29
  11. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +330 -360
  12. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/index.ts +56 -108
  13. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/session.ts +97 -15
  14. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/subagent.ts +80 -45
  15. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/flush-docked-bash.ts +36 -0
  16. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer-layout.ts +229 -0
  17. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer.ts +479 -0
  18. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/startup-header.ts +92 -61
  19. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/working-message.ts +75 -40
  20. package/node_modules/@herbertgao/pi-cc-extensions/extensions/index.ts +19 -3
  21. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +1080 -229
  22. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +232 -259
  23. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/index.ts +42 -41
  24. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +8 -2
  25. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +2 -7
  26. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +277 -113
  27. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/layout.ts +15 -6
  28. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/packets.ts +18 -72
  29. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/scroll.ts +61 -26
  30. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/ansi-utils.ts +25 -57
  31. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-component.ts +125 -0
  32. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-edit-render.ts +227 -0
  33. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-header.ts +136 -0
  34. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-highlight.ts +115 -0
  35. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-inline.ts +324 -0
  36. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-layout.ts +762 -0
  37. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-limits.ts +152 -0
  38. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-palette.ts +410 -0
  39. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-parse.ts +535 -0
  40. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-presentation.ts +2 -2
  41. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +21 -3219
  42. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-text.ts +44 -0
  43. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-write-render.ts +568 -0
  44. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/index.ts +47 -10
  45. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/line-width-safety.ts +2 -2
  46. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/write-execution.ts +1 -0
  47. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +145 -190
  48. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/mcp-title.ts +39 -0
  49. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/{message-display.ts → tool/message-display.ts} +24 -90
  50. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/names.ts +426 -0
  51. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +90 -132
  52. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/{show-more-hint.ts → tool/show-more-hint.ts} +10 -0
  53. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/transcript-refresh.ts +8 -26
  54. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/ansi-color.ts +44 -0
  55. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/ansi-text.ts +76 -0
  56. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/component-tree.ts +34 -0
  57. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/format.ts +28 -0
  58. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/fullscreen-detect.ts +51 -0
  59. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/patch-keys.ts +124 -0
  60. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/sgr-mouse.ts +70 -0
  61. package/node_modules/@herbertgao/pi-cc-extensions/package.json +8 -8
  62. package/node_modules/pi-antigravity/README.md +29 -6
  63. package/node_modules/pi-antigravity/package.json +12 -7
  64. package/node_modules/pi-antigravity/src/auth/accounts.ts +327 -0
  65. package/node_modules/pi-antigravity/src/auth/index.ts +9 -0
  66. package/node_modules/pi-antigravity/src/client/client.ts +1 -1
  67. package/node_modules/pi-antigravity/src/image/image.ts +4 -4
  68. package/node_modules/pi-antigravity/src/index.ts +203 -13
  69. package/node_modules/pi-antigravity/src/search/index.ts +1 -0
  70. package/node_modules/pi-antigravity/src/search/search.ts +325 -0
  71. package/node_modules/pi-antigravity/src/stream/stream.ts +191 -14
  72. package/node_modules/pi-antigravity/src/types/enums.ts +45 -36
  73. package/node_modules/pi-antigravity/src/types/types.ts +5 -12
  74. package/node_modules/pi-antigravity/src/utils/http.ts +7 -2
  75. package/node_modules/pi-antigravity/src/utils/util.ts +31 -3
  76. package/node_modules/pi-typesafe/README.md +2 -0
  77. package/node_modules/pi-typesafe/package.json +2 -1
  78. package/node_modules/pi-web-access/CHANGELOG.md +72 -0
  79. package/node_modules/pi-web-access/README.md +34 -13
  80. package/node_modules/pi-web-access/chrome-cookies.ts +79 -25
  81. package/node_modules/pi-web-access/curator-page.ts +5 -1
  82. package/node_modules/pi-web-access/curator-server.ts +2 -0
  83. package/node_modules/pi-web-access/dist/index.js +1337 -1009
  84. package/node_modules/pi-web-access/exa.ts +20 -52
  85. package/node_modules/pi-web-access/extract.ts +41 -4
  86. package/node_modules/pi-web-access/gemini-search.ts +12 -4
  87. package/node_modules/pi-web-access/index.ts +29 -37
  88. package/node_modules/pi-web-access/openai-search.ts +35 -7
  89. package/node_modules/pi-web-access/package.json +6 -5
  90. package/node_modules/pi-web-access/page-query.ts +2 -2
  91. package/node_modules/pi-web-access/query-rewrite.ts +2 -3
  92. package/node_modules/pi-web-access/ssrf-protection.ts +27 -10
  93. package/node_modules/pi-web-access/storage.ts +48 -77
  94. package/node_modules/pi-web-access/summary-model-scope.ts +6 -77
  95. package/node_modules/pi-web-access/summary-review.ts +3 -4
  96. package/node_modules/pi-web-access/tavily.ts +67 -20
  97. package/node_modules/pi-web-access/tool-activation.ts +53 -33
  98. package/node_modules/pi-web-access/utils.ts +1 -1
  99. package/node_modules/pi-web-access/you.ts +164 -0
  100. package/node_modules/pi-web-access/zai.ts +161 -0
  101. package/package.json +7 -11
  102. package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/hero.en.svg +0 -101
  103. package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/hero.svg +0 -101
  104. package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/welcome.webp +0 -0
  105. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/render-utils.ts +0 -5
  106. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/types.ts +0 -30
  107. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/write-display-utils.ts +0 -12
  108. package/node_modules/pi-footer/CHANGELOG.md +0 -145
  109. package/node_modules/pi-footer/LICENSE +0 -21
  110. package/node_modules/pi-footer/README.md +0 -354
  111. package/node_modules/pi-footer/package.json +0 -68
  112. package/node_modules/pi-footer/src/cache.ts +0 -106
  113. package/node_modules/pi-footer/src/colors.ts +0 -237
  114. package/node_modules/pi-footer/src/config.ts +0 -202
  115. package/node_modules/pi-footer/src/event-widgets.ts +0 -41
  116. package/node_modules/pi-footer/src/extension-statuses.ts +0 -102
  117. package/node_modules/pi-footer/src/git.ts +0 -135
  118. package/node_modules/pi-footer/src/index.ts +0 -268
  119. package/node_modules/pi-footer/src/metrics.ts +0 -141
  120. package/node_modules/pi-footer/src/presets.ts +0 -383
  121. package/node_modules/pi-footer/src/render.ts +0 -114
  122. package/node_modules/pi-footer/src/separators.ts +0 -52
  123. package/node_modules/pi-footer/src/types.ts +0 -122
  124. package/node_modules/pi-footer/src/ui/color-level-confirm.ts +0 -22
  125. package/node_modules/pi-footer/src/ui/color-options.ts +0 -195
  126. package/node_modules/pi-footer/src/ui/config-lifecycle.ts +0 -61
  127. package/node_modules/pi-footer/src/ui/edit-colors.ts +0 -13
  128. package/node_modules/pi-footer/src/ui/events.ts +0 -35
  129. package/node_modules/pi-footer/src/ui/extension-status-picker.ts +0 -60
  130. package/node_modules/pi-footer/src/ui/extension-statuses.ts +0 -73
  131. package/node_modules/pi-footer/src/ui/fields.ts +0 -252
  132. package/node_modules/pi-footer/src/ui/global-menu.ts +0 -126
  133. package/node_modules/pi-footer/src/ui/helpers.ts +0 -44
  134. package/node_modules/pi-footer/src/ui/layout.ts +0 -17
  135. package/node_modules/pi-footer/src/ui/line-list.ts +0 -19
  136. package/node_modules/pi-footer/src/ui/model.ts +0 -64
  137. package/node_modules/pi-footer/src/ui/navigation.ts +0 -29
  138. package/node_modules/pi-footer/src/ui/option-edit.ts +0 -93
  139. package/node_modules/pi-footer/src/ui/overlay-render.ts +0 -103
  140. package/node_modules/pi-footer/src/ui/screen-context.ts +0 -18
  141. package/node_modules/pi-footer/src/ui/screen-controller.ts +0 -27
  142. package/node_modules/pi-footer/src/ui/screen-render.ts +0 -32
  143. package/node_modules/pi-footer/src/ui/screen-state.ts +0 -20
  144. package/node_modules/pi-footer/src/ui/screen.ts +0 -243
  145. package/node_modules/pi-footer/src/ui/screens/add-widget.ts +0 -92
  146. package/node_modules/pi-footer/src/ui/screens/confirm-exit.ts +0 -52
  147. package/node_modules/pi-footer/src/ui/screens/controller.ts +0 -12
  148. package/node_modules/pi-footer/src/ui/screens/edit-colors.ts +0 -68
  149. package/node_modules/pi-footer/src/ui/screens/edit-widget.ts +0 -115
  150. package/node_modules/pi-footer/src/ui/screens/extension-status-row.ts +0 -61
  151. package/node_modules/pi-footer/src/ui/screens/global.ts +0 -75
  152. package/node_modules/pi-footer/src/ui/screens/line-list.ts +0 -108
  153. package/node_modules/pi-footer/src/ui/screens/main.ts +0 -99
  154. package/node_modules/pi-footer/src/ui/screens/terminal.ts +0 -121
  155. package/node_modules/pi-footer/src/ui/screens/widget-list.ts +0 -154
  156. package/node_modules/pi-footer/src/ui/terminal-menu.ts +0 -32
  157. package/node_modules/pi-footer/src/ui/theme.ts +0 -48
  158. package/node_modules/pi-footer/src/ui/title-bar.ts +0 -100
  159. package/node_modules/pi-footer/src/ui/widget-actions.ts +0 -76
  160. package/node_modules/pi-footer/src/ui.ts +0 -66
  161. package/node_modules/pi-footer/src/widgets/context.ts +0 -28
  162. package/node_modules/pi-footer/src/widgets/core/active-tools.ts +0 -17
  163. package/node_modules/pi-footer/src/widgets/core/context-window.ts +0 -24
  164. package/node_modules/pi-footer/src/widgets/core/cwd-basename.ts +0 -19
  165. package/node_modules/pi-footer/src/widgets/core/cwd.ts +0 -108
  166. package/node_modules/pi-footer/src/widgets/core/event.ts +0 -34
  167. package/node_modules/pi-footer/src/widgets/core/external-status.ts +0 -102
  168. package/node_modules/pi-footer/src/widgets/core/model-provider.ts +0 -20
  169. package/node_modules/pi-footer/src/widgets/core/model.ts +0 -35
  170. package/node_modules/pi-footer/src/widgets/core/provider.ts +0 -17
  171. package/node_modules/pi-footer/src/widgets/core/session-name.ts +0 -17
  172. package/node_modules/pi-footer/src/widgets/core/text-verbosity.ts +0 -17
  173. package/node_modules/pi-footer/src/widgets/core/thinking-level.ts +0 -17
  174. package/node_modules/pi-footer/src/widgets/git/ahead-behind.ts +0 -17
  175. package/node_modules/pi-footer/src/widgets/git/branch.ts +0 -81
  176. package/node_modules/pi-footer/src/widgets/git/clean.ts +0 -19
  177. package/node_modules/pi-footer/src/widgets/git/deletions.ts +0 -17
  178. package/node_modules/pi-footer/src/widgets/git/diff.ts +0 -38
  179. package/node_modules/pi-footer/src/widgets/git/insertions.ts +0 -17
  180. package/node_modules/pi-footer/src/widgets/git/remote.ts +0 -17
  181. package/node_modules/pi-footer/src/widgets/git/root.ts +0 -17
  182. package/node_modules/pi-footer/src/widgets/git/sha.ts +0 -17
  183. package/node_modules/pi-footer/src/widgets/git/staged.ts +0 -17
  184. package/node_modules/pi-footer/src/widgets/git/status.ts +0 -19
  185. package/node_modules/pi-footer/src/widgets/git/unstaged.ts +0 -17
  186. package/node_modules/pi-footer/src/widgets/git/untracked.ts +0 -17
  187. package/node_modules/pi-footer/src/widgets/instance.ts +0 -118
  188. package/node_modules/pi-footer/src/widgets/layout/custom-text.ts +0 -18
  189. package/node_modules/pi-footer/src/widgets/layout/flex-separator.ts +0 -32
  190. package/node_modules/pi-footer/src/widgets/layout/separator.ts +0 -57
  191. package/node_modules/pi-footer/src/widgets/layout/spacer.ts +0 -31
  192. package/node_modules/pi-footer/src/widgets/options.ts +0 -122
  193. package/node_modules/pi-footer/src/widgets/project/runtime.ts +0 -361
  194. package/node_modules/pi-footer/src/widgets/registry.ts +0 -223
  195. package/node_modules/pi-footer/src/widgets/session/assistant-messages.ts +0 -17
  196. package/node_modules/pi-footer/src/widgets/session/compactions.ts +0 -17
  197. package/node_modules/pi-footer/src/widgets/session/elapsed.ts +0 -18
  198. package/node_modules/pi-footer/src/widgets/session/last-activity.ts +0 -18
  199. package/node_modules/pi-footer/src/widgets/session/messages.ts +0 -19
  200. package/node_modules/pi-footer/src/widgets/session/session-id.ts +0 -17
  201. package/node_modules/pi-footer/src/widgets/session/session-start.ts +0 -18
  202. package/node_modules/pi-footer/src/widgets/session/tool-results.ts +0 -17
  203. package/node_modules/pi-footer/src/widgets/session/total-messages.ts +0 -19
  204. package/node_modules/pi-footer/src/widgets/session/total-time.ts +0 -18
  205. package/node_modules/pi-footer/src/widgets/session/user-messages.ts +0 -17
  206. package/node_modules/pi-footer/src/widgets/store.ts +0 -26
  207. package/node_modules/pi-footer/src/widgets/tokens/cache-hit-rate.ts +0 -55
  208. package/node_modules/pi-footer/src/widgets/tokens/cache-read.ts +0 -18
  209. package/node_modules/pi-footer/src/widgets/tokens/cache-write.ts +0 -18
  210. package/node_modules/pi-footer/src/widgets/tokens/context-bar.ts +0 -108
  211. package/node_modules/pi-footer/src/widgets/tokens/context-length.ts +0 -26
  212. package/node_modules/pi-footer/src/widgets/tokens/context-remaining.ts +0 -29
  213. package/node_modules/pi-footer/src/widgets/tokens/context.ts +0 -29
  214. package/node_modules/pi-footer/src/widgets/tokens/cost.ts +0 -51
  215. package/node_modules/pi-footer/src/widgets/tokens/input-speed.ts +0 -25
  216. package/node_modules/pi-footer/src/widgets/tokens/input-tokens.ts +0 -18
  217. package/node_modules/pi-footer/src/widgets/tokens/output-speed.ts +0 -25
  218. package/node_modules/pi-footer/src/widgets/tokens/output-tokens.ts +0 -18
  219. package/node_modules/pi-footer/src/widgets/tokens/tokens.ts +0 -20
  220. package/node_modules/pi-footer/src/widgets/tokens/total-speed.ts +0 -25
  221. package/node_modules/pi-footer/src/widgets/tokens/total-tokens.ts +0 -18
  222. package/node_modules/pi-footer/src/widgets/types.ts +0 -181
  223. package/node_modules/pi-footer/src/widgets/utils/colors.ts +0 -11
  224. package/node_modules/pi-footer/src/widgets/utils/context.ts +0 -112
  225. package/node_modules/pi-footer/src/widgets/utils/session.ts +0 -19
  226. package/node_modules/pi-footer/src/widgets/utils/token-format.ts +0 -78
@@ -60,6 +60,19 @@ async function getApiKey(signal?: AbortSignal): Promise<string | null> {
60
60
  });
61
61
  }
62
62
 
63
+ const TAVILY_KEY_POOL_RETRIES = new Set([401, 402, 403, 429, 432]);
64
+
65
+ function tavilyKeyPool(): string[] {
66
+ const slots: Array<[number, string]> = [];
67
+ for (let slot = 1; slot <= 20; slot++) {
68
+ const key = process.env[`TAVILY_API_KEY_${slot}`]?.trim();
69
+ if (key) slots.push([slot, key]);
70
+ }
71
+ const requestedSlot = Number.parseInt(process.env.TAVILY_API_KEY_INDEX ?? "", 10) || 1;
72
+ const start = Math.max(0, slots.findIndex(([slot]) => slot >= requestedSlot));
73
+ return [...new Set([...slots.slice(start), ...slots.slice(0, start)].map(([, key]) => key))];
74
+ }
75
+
63
76
  function getApiUrl(): string {
64
77
  return `${resolveApiBaseUrl({
65
78
  configKey: "tavilyBaseUrl",
@@ -141,12 +154,11 @@ export function isTavilyAvailable(): boolean {
141
154
  provider: "Tavily",
142
155
  configuredValue: loadConfig().tavilyApiKey,
143
156
  environmentValue: process.env.TAVILY_API_KEY,
144
- });
157
+ }) || tavilyKeyPool().length > 0;
145
158
  }
146
159
 
147
160
  export async function searchWithTavily(query: string, options: TavilySearchOptions = {}): Promise<SearchResponse> {
148
161
  const apiUrl = getApiUrl();
149
- const apiKey = await requireApiKey(options.signal);
150
162
  const numResults = normalizeSearchResultCount(options.numResults);
151
163
  const body: Record<string, unknown> = {
152
164
  query,
@@ -158,7 +170,58 @@ export async function searchWithTavily(query: string, options: TavilySearchOptio
158
170
  ...mapDomainFilter(options.domainFilter),
159
171
  };
160
172
 
173
+ const signal = requestSignal(options.signal);
161
174
  const activityId = activityMonitor.logStart({ type: "api", query });
175
+ let data: TavilyResponse;
176
+ try {
177
+ data = await requestWithKeyFailover(apiUrl, body, signal);
178
+ } catch (err) {
179
+ if (options.signal?.aborted || (err instanceof Error && err.name === "AbortError")) activityMonitor.logComplete(activityId, 0);
180
+ else {
181
+ const status = tavilyErrorStatus(err);
182
+ if (status > 0) activityMonitor.logComplete(activityId, status);
183
+ else activityMonitor.logError(activityId, errorMessage(err));
184
+ }
185
+ throw err;
186
+ }
187
+
188
+ activityMonitor.logComplete(activityId, 200);
189
+ const result: SearchResponse = {
190
+ answer: typeof data.answer === "string" ? data.answer : "",
191
+ results: mapResults(data.results, numResults),
192
+ };
193
+ if (options.includeContent) {
194
+ const inlineContent = mapInlineContent(data.results);
195
+ if (inlineContent.length > 0) result.inlineContent = inlineContent;
196
+ }
197
+ return result;
198
+ }
199
+
200
+ function tavilyErrorStatus(err: unknown): number {
201
+ return Number(errorMessage(err).match(/^Tavily API error (\d{3}):/)?.[1] ?? 0);
202
+ }
203
+
204
+ async function requestWithKeyFailover(apiUrl: string, body: Record<string, unknown>, signal: AbortSignal): Promise<TavilyResponse> {
205
+ const pool = tavilyKeyPool();
206
+ const keys = pool.length > 0 ? pool : [await requireApiKey(signal)];
207
+ let fallbackChecked = pool.length === 0;
208
+ for (let i = 0; ; i++) {
209
+ try {
210
+ return await tavilyRequest(apiUrl, keys[i], body, signal);
211
+ } catch (err) {
212
+ if (!TAVILY_KEY_POOL_RETRIES.has(tavilyErrorStatus(err))) throw err;
213
+ if (i + 1 < keys.length) continue;
214
+ if (fallbackChecked) throw err;
215
+ // Resolve the standalone credential only after every numbered pool key failed.
216
+ fallbackChecked = true;
217
+ const fallback = await getApiKey(signal);
218
+ if (!fallback || keys.includes(fallback)) throw err;
219
+ keys.push(fallback);
220
+ }
221
+ }
222
+ }
223
+
224
+ async function tavilyRequest(apiUrl: string, apiKey: string, body: Record<string, unknown>, signal: AbortSignal): Promise<TavilyResponse> {
162
225
  let response: Response;
163
226
  try {
164
227
  response = await fetchWithCredentialRedirects(apiUrl, {
@@ -168,13 +231,11 @@ export async function searchWithTavily(query: string, options: TavilySearchOptio
168
231
  "Content-Type": "application/json",
169
232
  },
170
233
  body: JSON.stringify(body),
171
- signal: requestSignal(options.signal),
234
+ signal,
172
235
  }, ["Authorization"]);
173
236
  } catch (err) {
174
237
  const message = errorMessage(err);
175
238
  const redactedMessage = redactCredential(message, apiKey);
176
- if (redactedMessage.toLowerCase().includes("abort")) activityMonitor.logComplete(activityId, 0);
177
- else activityMonitor.logError(activityId, redactedMessage);
178
239
  if (redactedMessage === message) throw err;
179
240
  const redactedError = new Error(redactedMessage);
180
241
  if (err instanceof Error) redactedError.name = err.name;
@@ -182,27 +243,13 @@ export async function searchWithTavily(query: string, options: TavilySearchOptio
182
243
  }
183
244
 
184
245
  if (!response.ok) {
185
- activityMonitor.logComplete(activityId, response.status);
186
246
  const errorText = redactCredential(await response.text(), apiKey);
187
247
  throw new Error(`Tavily API error ${response.status}: ${errorText.slice(0, 300)}`);
188
248
  }
189
249
 
190
- let data: TavilyResponse;
191
250
  try {
192
- data = await response.json() as TavilyResponse;
251
+ return await response.json() as TavilyResponse;
193
252
  } catch (err) {
194
- activityMonitor.logComplete(activityId, response.status);
195
253
  throw new Error(`Tavily API returned invalid JSON: ${errorMessage(err)}`);
196
254
  }
197
-
198
- activityMonitor.logComplete(activityId, response.status);
199
- const result: SearchResponse = {
200
- answer: typeof data.answer === "string" ? data.answer : "",
201
- results: mapResults(data.results, numResults),
202
- };
203
- if (options.includeContent) {
204
- const inlineContent = mapInlineContent(data.results);
205
- if (inlineContent.length > 0) result.inlineContent = inlineContent;
206
- }
207
- return result;
208
255
  }
@@ -1,7 +1,4 @@
1
- import { buildSessionContext, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { dirname, join } from "node:path";
3
- import { readFileSync } from "node:fs";
4
- import { fileURLToPath } from "node:url";
1
+ import { buildSessionContext, type ExtensionAPI, type ExtensionContext, type SessionContext } from "@earendil-works/pi-coding-agent";
5
2
  import { Type } from "typebox";
6
3
 
7
4
  export type WebCapability = "search" | "source-check" | "fetch" | "stored-content";
@@ -21,31 +18,52 @@ const CAPABILITY_LABELS: Record<WebCapability, string> = {
21
18
 
22
19
  function supportsDynamicTools(pi: ExtensionAPI): boolean {
23
20
  if (typeof pi.getAllTools !== "function" || typeof pi.getActiveTools !== "function" || typeof pi.setActiveTools !== "function") return false;
24
- try {
25
- const packagePath = join(dirname(fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"))), "..", "package.json");
26
- const [major, minor, patch] = JSON.parse(readFileSync(packagePath, "utf8")).version.split(".").map(Number);
27
- return major > 0 || minor > 86 || minor === 86 && patch >= 1;
28
- } catch {
29
- return false;
30
- }
21
+ // Pi 0.86.0 shipped transcript-backed tool changes in the same release that made pi.on() return
22
+ // an unsubscribe function. Probe the host API object: an imported VERSION can come from a stale
23
+ // Pi package installed beside this extension instead of the running host.
24
+ const unsubscribe: unknown = pi.on("session_start", () => {});
25
+ if (typeof unsubscribe !== "function") return false;
26
+ unsubscribe();
27
+ return true;
31
28
  }
32
29
 
33
- function hasToolDeclarations(messages: unknown[]): boolean {
34
- return messages.some(message => message && typeof message === "object" && (
35
- "toolsAdded" in message || "toolsRemoved" in message
36
- ));
30
+ // Mirrors pi-ai's per-API transcript handling: without these flags, Pi resends the whole transcript
31
+ // as a checkpoint when web_enable changes the tools and prompt, which can miss the prompt cache.
32
+ function addsToolsWithoutCheckpoint(model: ExtensionContext["model"]): boolean {
33
+ const compat = model?.compat as {
34
+ supportsMidConvoSystemMessages?: boolean;
35
+ supportsMidConvoToolChanges?: boolean;
36
+ supportsMidConvoToolAdditions?: boolean;
37
+ supportsAdditionalTools?: boolean;
38
+ supportsToolSearch?: boolean;
39
+ } | undefined;
40
+ if (!model || compat?.supportsMidConvoSystemMessages !== true) return false;
41
+ switch (model.api) {
42
+ case "anthropic-messages": return compat.supportsMidConvoToolChanges === true;
43
+ case "openai-completions": return compat.supportsMidConvoToolAdditions === true;
44
+ case "openai-responses":
45
+ case "openai-codex-responses":
46
+ case "azure-openai-responses": return compat.supportsAdditionalTools === true || compat.supportsToolSearch === true;
47
+ default: return false;
48
+ }
37
49
  }
38
50
 
39
- async function currentTranscriptToolNames(messages: unknown[]): Promise<string[]> {
40
- const moduleName = "@earendil-works/pi-ai/utils/transcript";
41
- const { getCurrentTools } = await import(moduleName);
42
- return getCurrentTools(messages).map((tool: { name: string }) => tool.name);
51
+ // Undefined when the transcript never declared tool changes.
52
+ function transcriptToolNames(messages: SessionContext["messages"]): Set<string> | undefined {
53
+ let tools: Set<string> | undefined;
54
+ for (const message of messages) {
55
+ if (message.role !== "system" || !("toolsAdded" in message || "toolsRemoved" in message)) continue;
56
+ tools ??= new Set();
57
+ for (const tool of message.toolsRemoved ?? []) tools.delete(tool.name);
58
+ for (const tool of message.toolsAdded ?? []) tools.add(tool.name);
59
+ }
60
+ return tools;
43
61
  }
44
62
 
45
- export function registerWebToolActivation(pi: ExtensionAPI, tools: ReadonlyArray<WebActivationTool>): void {
63
+ export function registerWebToolActivation(pi: ExtensionAPI, tools: ReadonlyArray<WebActivationTool>, mode: "auto" | "dynamic"): void {
46
64
  if (tools.length === 0) return;
47
65
  if (!supportsDynamicTools(pi)) {
48
- console.warn("[pi-web-access] Dynamic tool activation requires Pi 0.86.1 or newer; web tools remain eagerly available.");
66
+ console.warn("[pi-web-access] Dynamic tool activation requires Pi 0.86.0 or newer; web tools remain eagerly available.");
49
67
  return;
50
68
  }
51
69
  const names = tools.map(tool => tool.name);
@@ -56,7 +74,7 @@ export function registerWebToolActivation(pi: ExtensionAPI, tools: ReadonlyArray
56
74
  name: LOADER_NAME,
57
75
  label: "Enable Web Access",
58
76
  description: "Enable configured pi-web-access tools for web research and content retrieval. Does not search or fetch. Enabled tools are available on the next model request; disabled capabilities remain unavailable.",
59
- promptSnippet: `pi-web-access is configured for ${capabilities}. Call web_enable to activate these tools; use them on the next model request.`,
77
+ promptSnippet: `If tools for ${capabilities} are not already available, call web_enable first whenever current, external, or linked information could help; the tools appear on the next model request.`,
60
78
  parameters,
61
79
  async execute() {
62
80
  const registered = new Set(pi.getAllTools().map(tool => tool.name));
@@ -100,19 +118,21 @@ export function registerWebToolActivation(pi: ExtensionAPI, tools: ReadonlyArray
100
118
  }
101
119
 
102
120
  let warned = false;
103
- async function selectFromSession(ctx: ExtensionContext): Promise<void> {
121
+ // A resumed transcript that declared its tools without the loader keeps that set; adding the
122
+ // loader there would record a mid-conversation tool change the user never asked for (#462).
123
+ let loaderSelected = true;
124
+ function selectFromSession(ctx: ExtensionContext): void {
104
125
  if (!loaderAvailable()) return;
105
126
  try {
106
127
  const messages = buildSessionContext(ctx.sessionManager.getBranch()).messages;
107
- const recorded = hasToolDeclarations(messages)
108
- ? new Set(await currentTranscriptToolNames(messages))
109
- : messages.length > 0
110
- ? new Set(names)
111
- : new Set<string>();
112
- const heavy = new Set(names);
113
- const active = pi.getActiveTools().filter(name => !heavy.has(name));
114
- for (const name of names) if (recorded.has(name)) active.push(name);
115
- pi.setActiveTools([...new Set([...active, LOADER_NAME])]);
128
+ const declared = transcriptToolNames(messages);
129
+ // "auto" decides once, for a fresh session; recorded sessions keep their tools.
130
+ const eager = mode === "auto" && !declared && messages.length === 0 && !addsToolsWithoutCheckpoint(ctx.model);
131
+ // Sessions from before transcript tool declarations keep every web tool; fresh ones start with none.
132
+ const recorded = declared ?? new Set(messages.length > 0 || eager ? names : []);
133
+ loaderSelected = !eager && (!declared || declared.has(LOADER_NAME));
134
+ const others = pi.getActiveTools().filter(name => name !== LOADER_NAME && !names.includes(name));
135
+ pi.setActiveTools([...new Set([...others, ...names.filter(name => recorded.has(name)), ...(loaderSelected ? [LOADER_NAME] : [])])]);
116
136
  } catch (error) {
117
137
  if (!warned) {
118
138
  warned = true;
@@ -124,7 +144,7 @@ export function registerWebToolActivation(pi: ExtensionAPI, tools: ReadonlyArray
124
144
  pi.on("session_start", (_event, ctx) => selectFromSession(ctx));
125
145
  pi.on("session_tree", (_event, ctx) => selectFromSession(ctx));
126
146
  pi.on("before_agent_start", () => {
127
- if (!loaderAvailable() || pi.getActiveTools().includes(LOADER_NAME)) return;
147
+ if (!loaderSelected || !loaderAvailable() || pi.getActiveTools().includes(LOADER_NAME)) return;
128
148
  try {
129
149
  pi.setActiveTools([...pi.getActiveTools(), LOADER_NAME]);
130
150
  } catch {
@@ -249,7 +249,7 @@ function redactProxyUrl(value: string): string {
249
249
  return parsed.toString();
250
250
  }
251
251
 
252
- function loadConfiguredProxy(): string | null {
252
+ export function loadConfiguredProxy(): string | null {
253
253
  let configured: unknown;
254
254
  const path = getWebSearchConfigPath();
255
255
  if (existsSync(path)) {
@@ -0,0 +1,164 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { activityMonitor } from "./activity.ts";
3
+ import { normalizeDomain } from "./domain-filter-normalization.ts";
4
+ import type { SearchOptions, SearchResponse } from "./perplexity.ts";
5
+ import { formatSearchResultsAsAnswer } from "./search-answer-formatting.ts";
6
+ import { normalizeSearchResultCount } from "./search-result-count-normalization.ts";
7
+ import { hasCredentialSource, redactCredential, resolveCredential } from "./credential-source.ts";
8
+ import { fetchWithCredentialRedirects, getWebSearchConfigPath } from "./utils.ts";
9
+
10
+ const YOU_SEARCH_URL = "https://ydc-index.io/v1/search";
11
+ const CONFIG_PATH = getWebSearchConfigPath();
12
+ const SEARCH_TIMEOUT_MS = 60_000;
13
+
14
+ interface WebSearchConfig {
15
+ youApiKey?: unknown;
16
+ }
17
+
18
+ let cachedConfig: WebSearchConfig | null = null;
19
+
20
+ function loadConfig(): WebSearchConfig {
21
+ if (cachedConfig) return cachedConfig;
22
+ if (!existsSync(CONFIG_PATH)) {
23
+ cachedConfig = {};
24
+ return cachedConfig;
25
+ }
26
+ const raw = readFileSync(CONFIG_PATH, "utf-8");
27
+ let parsed: unknown;
28
+ try {
29
+ parsed = JSON.parse(raw);
30
+ } catch (err) {
31
+ const message = err instanceof Error ? err.message : String(err);
32
+ throw new Error(`Failed to parse ${CONFIG_PATH}: ${message}`);
33
+ }
34
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
35
+ throw new Error(`Invalid config in ${CONFIG_PATH}: expected a JSON object`);
36
+ }
37
+ cachedConfig = parsed as WebSearchConfig;
38
+ return cachedConfig;
39
+ }
40
+
41
+ async function requireApiKey(signal?: AbortSignal): Promise<string> {
42
+ const apiKey = await resolveCredential({
43
+ provider: "You.com",
44
+ configuredValue: loadConfig().youApiKey,
45
+ environmentValue: process.env.YDC_API_KEY,
46
+ signal,
47
+ });
48
+ if (!apiKey) {
49
+ throw new Error(
50
+ "You.com API key not found. Either:\n" +
51
+ ` 1. Create ${CONFIG_PATH} with { "youApiKey": "your-key" }\n` +
52
+ " 2. Set YDC_API_KEY environment variable\n" +
53
+ "Get a key at https://you.com/platform/api-keys",
54
+ );
55
+ }
56
+ return apiKey;
57
+ }
58
+
59
+ function mapDomainFilter(domainFilter: string[] | undefined): { include_domains?: string[]; exclude_domains?: string[] } {
60
+ const include_domains: string[] = [];
61
+ const exclude_domains: string[] = [];
62
+ for (const raw of domainFilter ?? []) {
63
+ const domain = normalizeDomain(raw);
64
+ if (!domain) continue;
65
+ const target = raw.trim().startsWith("-") ? exclude_domains : include_domains;
66
+ if (!target.includes(domain)) target.push(domain);
67
+ }
68
+ return {
69
+ ...(include_domains.length > 0 ? { include_domains } : {}),
70
+ ...(exclude_domains.length > 0 ? { exclude_domains } : {}),
71
+ };
72
+ }
73
+
74
+ function errorMessage(err: unknown): string {
75
+ return err instanceof Error ? err.message : String(err);
76
+ }
77
+
78
+ function parseWebResults(value: unknown): unknown[] {
79
+ const results = value && typeof value === "object" ? (value as { results?: unknown }).results : undefined;
80
+ const web = results && typeof results === "object" ? (results as { web?: unknown }).web : undefined;
81
+ if (!Array.isArray(web)) throw new Error("You.com returned invalid response: expected results.web array");
82
+ return web;
83
+ }
84
+
85
+ export function isYouAvailable(): boolean {
86
+ return hasCredentialSource({ provider: "You.com", configuredValue: loadConfig().youApiKey, environmentValue: process.env.YDC_API_KEY });
87
+ }
88
+
89
+ export async function searchWithYou(query: string, options: SearchOptions = {}): Promise<SearchResponse> {
90
+ const apiKey = await requireApiKey(options.signal);
91
+ const numResults = normalizeSearchResultCount(options.numResults);
92
+ const body = {
93
+ query,
94
+ count: numResults,
95
+ ...(options.recencyFilter ? { freshness: options.recencyFilter } : {}),
96
+ ...mapDomainFilter(options.domainFilter),
97
+ };
98
+ const activityId = activityMonitor.logStart({ type: "api", query });
99
+ const timeoutSignal = AbortSignal.timeout(SEARCH_TIMEOUT_MS);
100
+ let response: Response;
101
+ let entries: unknown[];
102
+ try {
103
+ response = await fetchWithCredentialRedirects(YOU_SEARCH_URL, {
104
+ method: "POST",
105
+ headers: { Accept: "application/json", "Content-Type": "application/json", "X-API-Key": apiKey },
106
+ body: JSON.stringify(body),
107
+ signal: options.signal ? AbortSignal.any([timeoutSignal, options.signal]) : timeoutSignal,
108
+ }, ["X-API-Key"]);
109
+ if (!response.ok) {
110
+ const errorText = await response.text();
111
+ throw new Error(`You.com error ${response.status}: ${redactCredential(errorText, apiKey).slice(0, 300)}`);
112
+ }
113
+ let rawData: unknown;
114
+ try {
115
+ rawData = await response.json();
116
+ } catch (err) {
117
+ if (err instanceof Error && err.name === "TimeoutError") throw err;
118
+ throw new Error(`You.com returned invalid JSON: ${errorMessage(err)}`);
119
+ }
120
+ entries = parseWebResults(rawData);
121
+ } catch (err) {
122
+ if (options.signal?.aborted) {
123
+ activityMonitor.logComplete(activityId, 0);
124
+ throw new Error("Aborted");
125
+ }
126
+ const message = errorMessage(err);
127
+ const providerTimeout = timeoutSignal.aborted || (err instanceof Error && err.name === "TimeoutError");
128
+ const outgoing = providerTimeout
129
+ ? new Error(`You.com request timed out after ${Math.round(SEARCH_TIMEOUT_MS / 1000)}s`)
130
+ : (() => {
131
+ const redactedMessage = redactCredential(message, apiKey);
132
+ if (redactedMessage === message && err instanceof Error) return err;
133
+ const redactedError = new Error(redactedMessage);
134
+ if (err instanceof Error) redactedError.name = err.name;
135
+ return redactedError;
136
+ })();
137
+ activityMonitor.logError(activityId, redactCredential(errorMessage(outgoing), apiKey));
138
+ throw outgoing;
139
+ }
140
+ activityMonitor.logComplete(activityId, response.status);
141
+ const results: SearchResponse["results"] = [];
142
+ for (const entry of entries) {
143
+ if (!entry || typeof entry !== "object") continue;
144
+ const { url, title, description, snippets } = entry as Record<string, unknown>;
145
+ if (typeof url !== "string" || !url) continue;
146
+ let resultUrl: URL;
147
+ try {
148
+ resultUrl = new URL(url);
149
+ } catch {
150
+ continue;
151
+ }
152
+ if (resultUrl.protocol !== "http:" && resultUrl.protocol !== "https:") continue;
153
+ const snippet = typeof description === "string" && description.trim()
154
+ ? description
155
+ : Array.isArray(snippets) && typeof snippets[0] === "string" ? snippets[0] : "";
156
+ results.push({
157
+ title: typeof title === "string" && title.trim() ? title.trim() : `Source ${results.length + 1}`,
158
+ url: resultUrl.href,
159
+ snippet,
160
+ });
161
+ if (results.length >= numResults) break;
162
+ }
163
+ return { answer: formatSearchResultsAsAnswer(results), results };
164
+ }
@@ -0,0 +1,161 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { activityMonitor } from "./activity.ts";
3
+ import { hasCredentialSource, redactCredential, resolveCredential } from "./credential-source.ts";
4
+ import { normalizeDomain } from "./domain-filter-normalization.ts";
5
+ import type { SearchOptions, SearchResponse } from "./perplexity.ts";
6
+ import { formatSearchResultsAsAnswer } from "./search-answer-formatting.ts";
7
+ import { normalizeSearchResultCount } from "./search-result-count-normalization.ts";
8
+ import { getWebSearchConfigPath } from "./utils.ts";
9
+
10
+ // GLM Coding Plan keys are issued per site, so the user picks the matching endpoint.
11
+ const MCP_URLS = {
12
+ global: "https://api.z.ai/api/mcp/web_search_prime/mcp",
13
+ china: "https://open.bigmodel.cn/api/mcp/web_search_prime/mcp",
14
+ } as const;
15
+ const RECENCY = { day: "oneDay", week: "oneWeek", month: "oneMonth", year: "oneYear" } as const;
16
+ const SEARCH_TIMEOUT_MS = 60_000;
17
+ const CONFIG_PATH = getWebSearchConfigPath();
18
+
19
+ function loadConfig(): Record<string, unknown> {
20
+ if (!existsSync(CONFIG_PATH)) return {};
21
+ try {
22
+ const parsed: unknown = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
23
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error();
24
+ return parsed as Record<string, unknown>;
25
+ } catch {
26
+ // JSON parser errors may include a fragment containing a literal key.
27
+ throw new Error(`Invalid configuration in ${CONFIG_PATH}: expected a JSON object`);
28
+ }
29
+ }
30
+
31
+ function credentialOptions(config: Record<string, unknown>) {
32
+ return { provider: "Z.ai", configuredValue: config.zaiApiKey, environmentValue: process.env.ZAI_API_KEY };
33
+ }
34
+
35
+ function endpointUrl(config: Record<string, unknown>): string {
36
+ const endpoint = config.zaiEndpoint ?? "global";
37
+ if (endpoint !== "global" && endpoint !== "china") {
38
+ throw new Error(`zaiEndpoint in ${CONFIG_PATH} must be "global" or "china"`);
39
+ }
40
+ return MCP_URLS[endpoint];
41
+ }
42
+
43
+ export function isZaiAvailable(): boolean {
44
+ return hasCredentialSource(credentialOptions(loadConfig()));
45
+ }
46
+
47
+ function searchArguments(query: string, options: SearchOptions): Record<string, unknown> {
48
+ if (!query.trim()) throw new Error("Z.ai search query must not be empty");
49
+ const domains = (options.domainFilter ?? []).map((raw) => raw.trim()).filter(Boolean);
50
+ if (domains.length > 1 || domains.some((raw) => raw.startsWith("-"))) {
51
+ throw new Error("Z.ai domainFilter supports a single included domain");
52
+ }
53
+ const domain = domains[0] === undefined ? undefined : normalizeDomain(domains[0]);
54
+ if (domain === null) throw new Error("Z.ai domain filter must contain a valid domain");
55
+ return {
56
+ search_query: query.trim(),
57
+ ...(options.recencyFilter ? { search_recency_filter: RECENCY[options.recencyFilter] } : {}),
58
+ ...(domain ? { search_domain_filter: domain } : {}),
59
+ };
60
+ }
61
+
62
+ /** The tool returns its result list as JSON text, sometimes encoded twice. */
63
+ function parseResults(text: string, limit: number, domain: string | undefined): SearchResponse["results"] {
64
+ let value: unknown = JSON.parse(text);
65
+ if (typeof value === "string") value = JSON.parse(value);
66
+ if (!Array.isArray(value)) throw new Error("Z.ai returned invalid response: expected a result list");
67
+ const results: SearchResponse["results"] = [];
68
+ for (const entry of value) {
69
+ if (!entry || typeof entry !== "object") continue;
70
+ const { link, title, content } = entry as Record<string, unknown>;
71
+ if (typeof link !== "string") continue;
72
+ let url: URL;
73
+ try {
74
+ url = new URL(link);
75
+ } catch {
76
+ continue;
77
+ }
78
+ if (url.protocol !== "http:" && url.protocol !== "https:") continue;
79
+ // The remote filter is undocumented, so enforce it locally too.
80
+ const host = url.hostname.toLowerCase().replace(/\.+$/, "");
81
+ if (domain && host !== domain && !host.endsWith(`.${domain}`)) continue;
82
+ results.push({
83
+ title: typeof title === "string" && title.trim() ? title.trim() : `Source ${results.length + 1}`,
84
+ url: url.href,
85
+ snippet: typeof content === "string" ? content : "",
86
+ });
87
+ if (results.length >= limit) break;
88
+ }
89
+ return results;
90
+ }
91
+
92
+ export async function searchWithZai(query: string, options: SearchOptions = {}): Promise<SearchResponse> {
93
+ const config = loadConfig();
94
+ const mcpUrl = endpointUrl(config);
95
+ const args = searchArguments(query, options);
96
+ const limit = normalizeSearchResultCount(options.numResults);
97
+ const timeoutSignal = AbortSignal.timeout(SEARCH_TIMEOUT_MS);
98
+ const signal = options.signal ? AbortSignal.any([timeoutSignal, options.signal]) : timeoutSignal;
99
+ if (signal.aborted) throw new Error("Aborted");
100
+ const apiKey = await resolveCredential({ ...credentialOptions(config), signal });
101
+ if (!apiKey) throw new Error("Z.ai API key not found. Set ZAI_API_KEY or zaiApiKey in web-search.json with a GLM Coding Plan key.");
102
+ let headers: Headers;
103
+ try { headers = new Headers({ Authorization: `Bearer ${apiKey}` }); }
104
+ catch { throw new Error("Z.ai credential resolution failed: invalid-header-value"); }
105
+ // Load the protocol client only when this explicit provider is selected.
106
+ const [{ Client }, { StreamableHTTPClientTransport }] = await Promise.all([
107
+ import("@modelcontextprotocol/sdk/client/index.js"),
108
+ import("@modelcontextprotocol/sdk/client/streamableHttp.js"),
109
+ ]);
110
+ let cleaningUp = false;
111
+ const transport = new StreamableHTTPClientTransport(new URL(mcpUrl), {
112
+ requestInit: { headers },
113
+ fetch: async (url, init) => {
114
+ // Fixed endpoint, no redirects: neither credentials nor session IDs can
115
+ // be forwarded to another origin (including MCP discovery redirects).
116
+ if (String(url) !== mcpUrl) throw new Error("Z.ai endpoint changed unexpectedly");
117
+ const requestSignal = cleaningUp ? AbortSignal.timeout(1_500) : signal;
118
+ return fetch(url, { ...init, redirect: "error", signal: !cleaningUp && init?.signal ? AbortSignal.any([init.signal, requestSignal]) : requestSignal });
119
+ },
120
+ reconnectionOptions: { maxRetries: 0, initialReconnectionDelay: 1_000, maxReconnectionDelay: 1_000, reconnectionDelayGrowFactor: 1 },
121
+ });
122
+ const client = new Client({ name: "pi-web-access-zai", version: "1.0.0" });
123
+ const activityId = activityMonitor.logStart({ type: "api", query });
124
+ try {
125
+ await client.connect(transport, { signal, timeout: SEARCH_TIMEOUT_MS });
126
+ // The live China server lists `web_search_prime` while both sites document
127
+ // `webSearchPrime`, so call whichever name this server exposes.
128
+ const { tools } = await client.listTools(undefined, { signal, timeout: SEARCH_TIMEOUT_MS });
129
+ const toolName = tools.find((tool) => tool.name === "web_search_prime" || tool.name === "webSearchPrime")?.name;
130
+ if (!toolName) throw new Error("Z.ai returned invalid response: web search tool not listed");
131
+ const result = await client.callTool({ name: toolName, arguments: args }, undefined, { signal, timeout: SEARCH_TIMEOUT_MS });
132
+ if (result.isError) throw new Error("Z.ai search tool returned an error");
133
+ const text = (Array.isArray(result.content) ? result.content : []).find((item) => item.type === "text" && typeof item.text === "string");
134
+ if (!text) throw new Error("Z.ai returned invalid response: no search text");
135
+ const results = parseResults(text.text as string, limit, args.search_domain_filter as string | undefined);
136
+ activityMonitor.logComplete(activityId, 200);
137
+ return { answer: formatSearchResultsAsAnswer(results), results };
138
+ } catch (err) {
139
+ let message: string;
140
+ if (options.signal?.aborted) message = "Aborted";
141
+ else if (timeoutSignal.aborted) message = "Z.ai request timed out after 60s";
142
+ else {
143
+ const code = err && typeof err === "object" && "code" in err ? err.code : undefined;
144
+ // Never expose remote HTTP/JSON-RPC bodies or nested SDK errors: they
145
+ // can echo credentials in plain, encoded or transformed form.
146
+ message = typeof code === "number" && Number.isInteger(code) && code >= 400 && code <= 599
147
+ ? `Z.ai HTTP ${code} request failed; check your GLM Coding Plan key, zaiEndpoint and plan quota`
148
+ : err instanceof TypeError ? "Z.ai network request failed"
149
+ : err instanceof SyntaxError || code === -32700 || code === -32602 ? "Z.ai returned invalid response"
150
+ : err instanceof Error && /^Z\.ai (search tool returned an error|returned invalid response:)/.test(err.message) ? err.message
151
+ : "Z.ai MCP request failed; check your connection and GLM Coding Plan key";
152
+ }
153
+ activityMonitor.logError(activityId, redactCredential(message, apiKey));
154
+ throw new Error(message);
155
+ } finally {
156
+ cleaningUp = true;
157
+ // Best effort, separately bounded DELETE; always close the local streams.
158
+ try { await transport.terminateSession(); } catch { /* Server may not support DELETE. */ }
159
+ await client.close();
160
+ }
161
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@herbertgao/pi-extensions",
3
- "version": "2026.9.18",
3
+ "version": "2026.9.20",
4
4
  "description": "Aggregate installer for HerbertGao-maintained Pi extensions.",
5
5
  "keywords": [
6
6
  "extensions",
@@ -39,7 +39,7 @@
39
39
  "@bufbuild/protobuf": "^2.14.0",
40
40
  "@dietrichgebert/ponytail": "4.10.0",
41
41
  "@herbertgao/pi-bark": "0.1.4",
42
- "@herbertgao/pi-cc-extensions": "0.9.6",
42
+ "@herbertgao/pi-cc-extensions": "0.10.0",
43
43
  "@herbertgao/pi-subagents": "0.18.4",
44
44
  "@herbertgao/resume-from": "0.2.4",
45
45
  "@herbertgao/sol-pi": "0.3.4",
@@ -62,7 +62,6 @@
62
62
  "@tifan/pi-recap": "0.4.7",
63
63
  "@tifan/pi-rename": "0.6.0",
64
64
  "@typesafe-ai/sdk": "0.6.0",
65
- "chalk": "^6.0.0",
66
65
  "croner": "^10.0.1",
67
66
  "dbus-native": "^0.15.2",
68
67
  "defuddle": "0.19.3",
@@ -74,14 +73,13 @@
74
73
  "minimatch": "^10.2.6",
75
74
  "nanoid": "^5.0.0",
76
75
  "p-limit": "^6.1.0",
77
- "pi-antigravity": "0.7.2",
78
- "pi-footer": "0.5.1",
76
+ "pi-antigravity": "0.8.1",
79
77
  "pi-jev-auto-mode": "0.5.0",
80
78
  "pi-lens": "4.3.0",
81
79
  "pi-multi-account": "1.23.2",
82
80
  "pi-next-cue": "1.0.7",
83
- "pi-typesafe": "0.8.0",
84
- "pi-web-access": "0.31.0",
81
+ "pi-typesafe": "0.8.1",
82
+ "pi-web-access": "0.34.0",
85
83
  "pidusage": "^4.0.1",
86
84
  "promise.try": "^2.0.1",
87
85
  "proper-lockfile": "4.1.2",
@@ -89,7 +87,7 @@
89
87
  "remote-pi": "0.7.0",
90
88
  "turndown": "^7.2.0",
91
89
  "unbash": "4.0.10",
92
- "undici": "^8.9.0",
90
+ "undici": "^8.11.2",
93
91
  "unpdf": "^1.6.2",
94
92
  "vscode-jsonrpc": "^9.0.0",
95
93
  "ws": "^8.21.0",
@@ -126,7 +124,6 @@
126
124
  "@tifan/pi-rename",
127
125
  "grok-mermaid",
128
126
  "pi-antigravity",
129
- "pi-footer",
130
127
  "pi-jev-auto-mode",
131
128
  "pi-lens",
132
129
  "pi-multi-account",
@@ -165,8 +162,7 @@
165
162
  "./node_modules/pi-web-access/dist",
166
163
  "./node_modules/pi-antigravity/src/index.ts",
167
164
  "./node_modules/pi-typesafe/extensions/index.js",
168
- "./node_modules/remote-pi/dist/index.js",
169
- "./node_modules/pi-footer/src/index.ts"
165
+ "./node_modules/remote-pi/dist/index.js"
170
166
  ],
171
167
  "themes": [
172
168
  "./node_modules/@herbertgao/pi-cc-extensions/themes/cc-dark.json",