@herbertgao/pi-extensions 2026.8.7 → 2026.8.9

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 (161) hide show
  1. package/README.md +8 -6
  2. package/node_modules/@czottmann/pi-automode/CHANGELOG.md +24 -0
  3. package/node_modules/@czottmann/pi-automode/README.md +78 -112
  4. package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +18 -14
  5. package/node_modules/@czottmann/pi-automode/docs/adr/ADR-001-permission-precedence-and-trust-boundaries.md +44 -0
  6. package/node_modules/@czottmann/pi-automode/docs/adr/INDEX.md +5 -0
  7. package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +198 -104
  8. package/node_modules/@czottmann/pi-automode/docs/configuration.md +155 -0
  9. package/node_modules/@czottmann/pi-automode/docs/defaults.md +55 -14
  10. package/node_modules/@czottmann/pi-automode/docs/diagnostics.md +90 -0
  11. package/node_modules/@czottmann/pi-automode/docs/observability-logging.md +61 -26
  12. package/node_modules/@czottmann/pi-automode/examples/automode.local.json +5 -0
  13. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +172 -18
  14. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +127 -30
  15. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +3 -0
  16. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +273 -31
  17. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/log.ts +60 -5
  18. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/paths.ts +92 -8
  19. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/permissions.ts +203 -13
  20. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/state.ts +1 -0
  21. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/types.ts +11 -0
  22. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/utils.ts +9 -1
  23. package/node_modules/@czottmann/pi-automode/package.json +9 -2
  24. package/node_modules/@czottmann/pi-automode/skills/automode-diagnostics/SKILL.md +63 -0
  25. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +3 -3
  26. package/node_modules/@herbertgao/pi-cc-extensions/README.md +3 -3
  27. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +8 -3
  28. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +63 -15
  29. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +56 -29
  30. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/message-display.ts +78 -16
  31. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +13 -0
  32. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +30 -17
  33. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  34. package/node_modules/@herbertgao/pi-handoff/CHANGELOG.md +29 -0
  35. package/node_modules/@herbertgao/pi-handoff/README.md +15 -18
  36. package/node_modules/@herbertgao/pi-handoff/package.json +11 -4
  37. package/node_modules/@herbertgao/pi-handoff/src/complete-text.ts +33 -0
  38. package/node_modules/@herbertgao/pi-handoff/src/index.ts +127 -232
  39. package/node_modules/@herbertgao/pi-handoff/src/session-query.ts +15 -30
  40. package/node_modules/@herbertgao/pi-mermaid-open/CHANGELOG.md +14 -0
  41. package/node_modules/@herbertgao/pi-mermaid-open/README.md +21 -4
  42. package/node_modules/@herbertgao/pi-mermaid-open/herdr-plugin/herdr-plugin.toml +11 -0
  43. package/node_modules/@herbertgao/pi-mermaid-open/herdr-plugin/viewer.mjs +305 -0
  44. package/node_modules/@herbertgao/pi-mermaid-open/package.json +15 -4
  45. package/node_modules/@herbertgao/pi-mermaid-open/src/index.ts +351 -101
  46. package/node_modules/@herbertgao/pi-preferred-thinking/CHANGELOG.md +12 -0
  47. package/node_modules/@herbertgao/pi-preferred-thinking/README.md +30 -14
  48. package/node_modules/@herbertgao/pi-preferred-thinking/package.json +7 -4
  49. package/node_modules/@herbertgao/pi-preferred-thinking/src/index.ts +140 -85
  50. package/node_modules/@herbertgao/pi-recap/CHANGELOG.md +12 -0
  51. package/node_modules/@herbertgao/pi-recap/README.md +5 -3
  52. package/node_modules/@herbertgao/pi-recap/package.json +9 -3
  53. package/node_modules/@herbertgao/pi-recap/src/index.ts +3 -7
  54. package/node_modules/@herbertgao/pi-recap/src/model-picker.ts +6 -19
  55. package/node_modules/@herbertgao/pi-recap/src/models.ts +9 -21
  56. package/node_modules/@herbertgao/pi-recap/src/tui.ts +1 -1
  57. package/node_modules/@herbertgao/pi-rename/CHANGELOG.md +49 -0
  58. package/node_modules/@herbertgao/pi-rename/README.md +13 -11
  59. package/node_modules/@herbertgao/pi-rename/package.json +13 -3
  60. package/node_modules/@herbertgao/pi-rename/src/index.ts +135 -59
  61. package/node_modules/@herbertgao/pi-rename/src/models.ts +5 -13
  62. package/node_modules/@herbertgao/pi-rename/src/naming.ts +2 -6
  63. package/node_modules/@herbertgao/pi-stash/package.json +3 -3
  64. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
  65. package/node_modules/@herbertgao/pi-subagents/README.md +16 -3
  66. package/node_modules/@herbertgao/pi-subagents/package.json +1 -1
  67. package/node_modules/@herbertgao/pi-subagents/src/cross-extension-rpc.ts +15 -4
  68. package/node_modules/@herbertgao/pi-subagents/src/index.ts +16 -0
  69. package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +5 -1
  70. package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +4 -4
  71. package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +154 -96
  72. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +3 -0
  73. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +10 -2
  74. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/localization.md +7 -5
  75. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/tool-schema.md +11 -5
  76. package/node_modules/@juicesharp/rpiv-ask-user-question/locales/en.json +3 -0
  77. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  78. package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +137 -96
  79. package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +44 -32
  80. package/node_modules/@juicesharp/rpiv-ask-user-question/state/selectors/projections.ts +22 -15
  81. package/node_modules/@juicesharp/rpiv-ask-user-question/state/state-reducer.ts +11 -1
  82. package/node_modules/@juicesharp/rpiv-ask-user-question/tool/response-envelope.ts +11 -0
  83. package/node_modules/@juicesharp/rpiv-ask-user-question/tool/types.ts +11 -0
  84. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/multi-select-view.ts +31 -17
  85. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/preview/preview-block-renderer.ts +15 -6
  86. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/wrapping-select.ts +15 -6
  87. package/node_modules/@juicesharp/rpiv-ask-user-question/view/dialog-builder.ts +56 -34
  88. package/node_modules/@juicesharp/rpiv-ask-user-question/view/tab-content-strategy.ts +50 -4
  89. package/node_modules/@narumitw/pi-btw/package.json +2 -2
  90. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +44 -1856
  91. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
  92. package/node_modules/@pi-plugins/fast-mode/package.json +4 -6
  93. package/node_modules/pi-lens/CHANGELOG.md +90 -0
  94. package/node_modules/pi-lens/README.md +106 -0
  95. package/node_modules/pi-lens/dist/clients/bash-file-access.js +97 -6
  96. package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +16 -6
  97. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +14 -0
  98. package/node_modules/pi-lens/dist/clients/cache-observability.js +353 -76
  99. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +22 -1
  100. package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +155 -0
  101. package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +8 -2
  102. package/node_modules/pi-lens/dist/clients/deps/typebox.js +8 -3
  103. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +9 -6
  104. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +9 -7
  105. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +8 -6
  106. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +8 -10
  107. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +10 -10
  108. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +20 -5
  109. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +44 -11
  110. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +10 -15
  111. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +19 -11
  112. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +35 -13
  113. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +8 -8
  114. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +110 -23
  115. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +12 -7
  116. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +117 -1
  117. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +16 -14
  118. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +8 -9
  119. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +22 -0
  120. package/node_modules/pi-lens/dist/clients/jscpd-client.js +5 -2
  121. package/node_modules/pi-lens/dist/clients/language-profile.js +7 -2
  122. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +27 -2
  123. package/node_modules/pi-lens/dist/clients/lsp/client.js +144 -5
  124. package/node_modules/pi-lens/dist/clients/lsp/index.js +151 -6
  125. package/node_modules/pi-lens/dist/clients/module-report.js +14 -1
  126. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +114 -23
  127. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +18 -9
  128. package/node_modules/pi-lens/dist/clients/project-report.js +62 -27
  129. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +7 -2
  130. package/node_modules/pi-lens/dist/clients/read-guard.js +88 -19
  131. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +194 -31
  132. package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +21 -0
  133. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +6 -3
  134. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +44 -0
  135. package/node_modules/pi-lens/dist/clients/runtime-session.js +41 -0
  136. package/node_modules/pi-lens/dist/clients/runtime-turn.js +47 -2
  137. package/node_modules/pi-lens/dist/clients/session-event-guard.js +184 -0
  138. package/node_modules/pi-lens/dist/clients/source-filter.js +6 -2
  139. package/node_modules/pi-lens/dist/clients/startup-scan.js +5 -2
  140. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +1 -0
  141. package/node_modules/pi-lens/dist/index.js +1224 -393
  142. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +10 -3
  143. package/node_modules/pi-lens/package.json +18 -5
  144. package/node_modules/pi-lens/scripts/install-selftest.mjs +99 -11
  145. package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +75 -0
  146. package/node_modules/pi-lens/scripts/lib/warm-loader-cache.mjs +285 -0
  147. package/node_modules/pi-lens/scripts/warm-loader-cache.mjs +233 -0
  148. package/node_modules/pi-web-access/CHANGELOG.md +27 -0
  149. package/node_modules/pi-web-access/README.md +8 -7
  150. package/node_modules/pi-web-access/chrome-cookies.ts +96 -20
  151. package/node_modules/pi-web-access/data-uri-sanitize.ts +406 -0
  152. package/node_modules/pi-web-access/extract.ts +12 -2
  153. package/node_modules/pi-web-access/firecrawl.ts +18 -2
  154. package/node_modules/pi-web-access/gemini-search.ts +28 -10
  155. package/node_modules/pi-web-access/github-extract.ts +65 -36
  156. package/node_modules/pi-web-access/index.ts +43 -14
  157. package/node_modules/pi-web-access/openai-search.ts +17 -6
  158. package/node_modules/pi-web-access/package.json +1 -1
  159. package/node_modules/pi-web-access/pdf-extract.ts +18 -5
  160. package/node_modules/pi-web-access/ssrf-protection.ts +12 -2
  161. package/package.json +19 -18
@@ -1,6 +1,7 @@
1
- import { existsSync, readFileSync, rmSync, statSync, readdirSync, openSync, readSync, closeSync, realpathSync } from "node:fs";
1
+ import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, realpathSync, rmSync, statSync, unlinkSync } from "node:fs";
2
2
  import { execFile, spawn, type ChildProcess } from "node:child_process";
3
- import { extname, join, resolve as resolvePath, sep as pathSep } from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { basename, dirname, extname, join, resolve as resolvePath, sep as pathSep } from "node:path";
4
5
  import { activityMonitor } from "./activity.ts";
5
6
  import type { ExtractedContent } from "./extract.ts";
6
7
  import { checkGhAvailable, checkRepoSize, fetchViaApi, showGhHint } from "./github-api.ts";
@@ -39,10 +40,15 @@ export interface GitHubUrlInfo {
39
40
  }
40
41
 
41
42
  interface CachedClone {
42
- localPath: string;
43
+ destination: CloneDestination;
43
44
  clonePromise: Promise<string | null>;
44
45
  }
45
46
 
47
+ interface CloneDestination {
48
+ rootPath: string;
49
+ localPath: string;
50
+ }
51
+
46
52
  interface GitHubCloneConfig {
47
53
  enabled: boolean;
48
54
  maxRepoSizeMB: number;
@@ -137,20 +143,21 @@ export function parseGitHubUrl(url: string): GitHubUrlInfo | null {
137
143
  const host = parsed.hostname.toLowerCase();
138
144
  if (host !== "github.com" && host !== "www.github.com") return null;
139
145
 
140
- const segments = parsed.pathname
141
- .split("/")
142
- .filter(Boolean)
143
- .map((segment) => {
144
- try {
145
- return decodeURIComponent(segment);
146
- } catch {
147
- return segment;
148
- }
149
- });
146
+ const segments: string[] = [];
147
+ for (const segment of parsed.pathname.split("/").filter(Boolean)) {
148
+ try {
149
+ segments.push(decodeURIComponent(segment));
150
+ } catch {
151
+ return null;
152
+ }
153
+ }
150
154
  if (segments.length < 2) return null;
151
155
 
152
156
  const owner = segments[0];
153
157
  const repo = segments[1].replace(/\.git$/, "");
158
+ if (!/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/.test(owner)) return null;
159
+ if (owner.includes("--")) return null;
160
+ if (!/^[A-Za-z0-9._-]{1,100}$/.test(repo) || repo === "." || repo === "..") return null;
154
161
 
155
162
  if (NON_CODE_SEGMENTS.has(segments[2]?.toLowerCase())) return null;
156
163
 
@@ -163,6 +170,7 @@ export function parseGitHubUrl(url: string): GitHubUrlInfo | null {
163
170
  if (segments.length < 4) return null;
164
171
 
165
172
  const ref = segments[3];
173
+ if (ref.length === 0 || ref.length > 1024 || /[\0-\x1f\x7f]/.test(ref)) return null;
166
174
  const refIsFullSha = /^[0-9a-f]{40}$/.test(ref);
167
175
  const pathParts = segments.slice(4);
168
176
  const path = pathParts.length > 0 ? pathParts.join("/") : "";
@@ -181,9 +189,32 @@ function cacheKey(owner: string, repo: string, ref?: string): string {
181
189
  return ref ? `${owner}/${repo}@${ref}` : `${owner}/${repo}`;
182
190
  }
183
191
 
184
- function cloneDir(config: GitHubCloneConfig, owner: string, repo: string, ref?: string): string {
185
- const dirName = ref ? `${repo}@${ref}` : repo;
186
- return join(config.clonePath, owner, dirName);
192
+ function cloneDestination(config: GitHubCloneConfig, owner: string, repo: string, ref?: string): CloneDestination | null {
193
+ try {
194
+ mkdirSync(resolvePath(config.clonePath), { recursive: true });
195
+ const rootPath = realpathSync(resolvePath(config.clonePath));
196
+ const digest = createHash("sha256").update(JSON.stringify([owner, repo, ref ?? null])).digest("hex");
197
+ const localPath = resolvePath(rootPath, digest);
198
+ if (dirname(localPath) !== rootPath) return null;
199
+ return { rootPath, localPath };
200
+ } catch {
201
+ return null;
202
+ }
203
+ }
204
+
205
+ function removeCloneDestination(destination: CloneDestination): boolean {
206
+ const rootPath = resolvePath(destination.rootPath);
207
+ const localPath = resolvePath(destination.localPath);
208
+ if (dirname(localPath) !== rootPath || !/^[0-9a-f]{64}$/.test(basename(localPath))) return false;
209
+ try {
210
+ const entry = lstatSync(localPath);
211
+ if (entry.isSymbolicLink()) unlinkSync(localPath);
212
+ else rmSync(localPath, { recursive: true, force: true });
213
+ return true;
214
+ } catch (err) {
215
+ if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") return true;
216
+ return false;
217
+ }
187
218
  }
188
219
 
189
220
  const PROCESS_KILL_GRACE_MS = 3000;
@@ -225,8 +256,9 @@ function terminateProcessTree(child: ChildProcess): void {
225
256
  forceKill.unref();
226
257
  }
227
258
 
228
- function execClone(args: string[], localPath: string, timeoutMs: number, signal?: AbortSignal): Promise<string | null> {
259
+ function execClone(args: string[], destination: CloneDestination, timeoutMs: number, signal?: AbortSignal): Promise<string | null> {
229
260
  return new Promise((resolve) => {
261
+ const { localPath } = destination;
230
262
  let settled = false;
231
263
  let timeout: ReturnType<typeof setTimeout> | undefined;
232
264
  let onAbort: (() => void) | undefined;
@@ -238,10 +270,7 @@ function execClone(args: string[], localPath: string, timeoutMs: number, signal?
238
270
  if (signal && onAbort) signal.removeEventListener("abort", onAbort);
239
271
 
240
272
  if (!success) {
241
- try {
242
- rmSync(localPath, { recursive: true, force: true });
243
- } catch {
244
- }
273
+ removeCloneDestination(destination);
245
274
  resolve(null);
246
275
  return;
247
276
  }
@@ -282,14 +311,11 @@ async function cloneRepo(
282
311
  repo: string,
283
312
  ref: string | undefined,
284
313
  config: GitHubCloneConfig,
314
+ destination: CloneDestination,
285
315
  signal?: AbortSignal,
286
316
  ): Promise<string | null> {
287
- const localPath = cloneDir(config, owner, repo, ref);
288
-
289
- try {
290
- rmSync(localPath, { recursive: true, force: true });
291
- } catch {
292
- }
317
+ const { localPath } = destination;
318
+ if (!removeCloneDestination(destination)) return null;
293
319
 
294
320
  const timeoutMs = config.cloneTimeoutSeconds * 1000;
295
321
  const hasGh = await checkGhAvailable();
@@ -297,7 +323,7 @@ async function cloneRepo(
297
323
  if (hasGh) {
298
324
  const args = ["gh", "repo", "clone", `${owner}/${repo}`, localPath, "--", "--depth", "1", "--single-branch"];
299
325
  if (ref) args.push("--branch", ref);
300
- return execClone(args, localPath, timeoutMs, signal);
326
+ return execClone(args, destination, timeoutMs, signal);
301
327
  }
302
328
 
303
329
  showGhHint();
@@ -306,7 +332,7 @@ async function cloneRepo(
306
332
  const args = ["git", "clone", "--depth", "1", "--single-branch"];
307
333
  if (ref) args.push("--branch", ref);
308
334
  args.push(gitUrl, localPath);
309
- return execClone(args, localPath, timeoutMs, signal);
335
+ return execClone(args, destination, timeoutMs, signal);
310
336
  }
311
337
 
312
338
  function isBinaryFile(filePath: string): boolean {
@@ -671,9 +697,15 @@ export async function extractGitHub(
671
697
  return cachedResult;
672
698
  }
673
699
 
674
- const clonePromise = cloneRepo(owner, repo, info.ref, config, signal);
675
- const localPath = cloneDir(config, owner, repo, info.ref);
676
- cloneCache.set(key, { localPath, clonePromise });
700
+ const destination = cloneDestination(config, owner, repo, info.ref);
701
+ if (!destination) {
702
+ const apiFallback = await fetchViaApi(url, owner, repo, info);
703
+ if (apiFallback) activityMonitor.logComplete(activityId, 200);
704
+ else activityMonitor.logError(activityId, "invalid clone destination");
705
+ return apiFallback;
706
+ }
707
+ const clonePromise = cloneRepo(owner, repo, info.ref, config, destination, signal);
708
+ cloneCache.set(key, { destination, clonePromise });
677
709
 
678
710
  const result = await clonePromise;
679
711
  if (signal?.aborted) {
@@ -707,10 +739,7 @@ export async function extractGitHub(
707
739
 
708
740
  export function clearCloneCache(): void {
709
741
  for (const entry of cloneCache.values()) {
710
- try {
711
- rmSync(entry.localPath, { recursive: true, force: true });
712
- } catch {
713
- }
742
+ removeCloneDestination(entry.destination);
714
743
  }
715
744
  cloneCache.clear();
716
745
  cachedConfig = null;
@@ -34,7 +34,7 @@ import {
34
34
  type SummaryMeta,
35
35
  } from "./summary-review.ts";
36
36
  import { randomUUID } from "node:crypto";
37
- import { execFileSync } from "node:child_process";
37
+ import { execFileSync, spawn } from "node:child_process";
38
38
  import { createRequire } from "node:module";
39
39
  import { platform } from "node:os";
40
40
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
@@ -152,7 +152,7 @@ interface WebSearchConfig {
152
152
  };
153
153
  }
154
154
 
155
- interface ProviderAvailability {
155
+ export interface ProviderAvailability {
156
156
  all: boolean;
157
157
  openai: boolean;
158
158
  brave: boolean;
@@ -184,7 +184,7 @@ interface ProviderAvailability {
184
184
 
185
185
  type WebSearchWorkflow = "none" | "summary-review" | "auto-summary";
186
186
  type CuratorWorkflow = "summary-review";
187
- type CuratorProvider = Exclude<SearchProvider, "auto">;
187
+ export type CuratorProvider = Exclude<SearchProvider, "auto">;
188
188
  type SummaryWorkflow = "summary-review" | "auto-summary";
189
189
 
190
190
  interface CuratorBootstrap {
@@ -420,13 +420,16 @@ async function getProviderAvailability(ctx: ExtensionContext): Promise<ProviderA
420
420
  };
421
421
  }
422
422
 
423
- function shouldPreferOpenAI(options?: Pick<PendingCurate, "numResults" | "recencyFilter">): boolean {
424
- if (!options) return true;
425
- if (options.recencyFilter) return false;
426
- if (typeof options.numResults === "number" && Number.isFinite(options.numResults) && Math.floor(options.numResults) !== 5) {
423
+ function shouldUseOpenAICodexDefault(ctx?: Pick<ExtensionContext, "model">): boolean {
424
+ return ctx?.model?.provider === "openai-codex";
425
+ }
426
+
427
+ function shouldPreferOpenAI(options: Pick<PendingCurate, "numResults" | "recencyFilter"> | undefined, preferOpenAICodexDefault: boolean): boolean {
428
+ if (options?.recencyFilter) return false;
429
+ if (typeof options?.numResults === "number" && Number.isFinite(options.numResults) && Math.floor(options.numResults) !== 5) {
427
430
  return false;
428
431
  }
429
- return true;
432
+ return preferOpenAICodexDefault;
430
433
  }
431
434
 
432
435
  async function loadCuratorBootstrap(
@@ -439,15 +442,25 @@ async function loadCuratorBootstrap(
439
442
  if (Array.isArray(provider)) availableProviders.all = true;
440
443
  return {
441
444
  availableProviders,
442
- defaultProvider: resolveProvider(provider, availableProviders, options),
445
+ defaultProvider: resolveCuratorDefaultProvider(provider, availableProviders, ctx, options),
443
446
  timeoutSeconds: getCuratorTimeoutSeconds(),
444
447
  };
445
448
  }
446
449
 
450
+ export function resolveCuratorDefaultProvider(
451
+ provider: SearchProviderSelection,
452
+ available: ProviderAvailability,
453
+ ctx?: Pick<ExtensionContext, "model">,
454
+ options?: Pick<PendingCurate, "numResults" | "recencyFilter">,
455
+ ): CuratorProvider {
456
+ return resolveProvider(provider, available, options, shouldUseOpenAICodexDefault(ctx));
457
+ }
458
+
447
459
  function firstAvailableProvider(available: ProviderAvailability, preferOpenAI: boolean, fallback: ResolvedSearchProvider): ResolvedSearchProvider {
448
460
  if (available.searxng) return "searxng";
449
461
  if (preferOpenAI && available.openai) return "openai";
450
462
  if (available.exa) return "exa";
463
+ if (available.openai) return "openai";
451
464
  if (available.brave) return "brave";
452
465
  if (available.parallel) return "parallel";
453
466
  if (available.tinyfish) return "tinyfish";
@@ -470,9 +483,10 @@ function resolveProvider(
470
483
  provider: SearchProviderSelection,
471
484
  available: ProviderAvailability,
472
485
  options?: Pick<PendingCurate, "numResults" | "recencyFilter">,
486
+ preferOpenAICodexDefault = false,
473
487
  ): CuratorProvider {
474
488
  if (Array.isArray(provider)) return "all";
475
- const preferOpenAI = shouldPreferOpenAI(options);
489
+ const preferOpenAI = shouldPreferOpenAI(options, preferOpenAICodexDefault);
476
490
 
477
491
  if (provider === "auto") {
478
492
  const routing = getConfiguredSearchRouting();
@@ -744,11 +758,26 @@ function closeCurator(callId?: string): void {
744
758
 
745
759
  async function openInBrowser(pi: ExtensionAPI, url: string): Promise<void> {
746
760
  const plat = platform();
761
+ if (plat !== "darwin" && plat !== "win32") {
762
+ await new Promise<void>((resolve, reject) => {
763
+ const child = spawn("xdg-open", [url], { detached: true, stdio: "ignore" });
764
+ const timer = setTimeout(resolve, 100);
765
+ child.once("error", (err) => {
766
+ clearTimeout(timer);
767
+ reject(err);
768
+ });
769
+ child.once("exit", (code) => {
770
+ clearTimeout(timer);
771
+ if (code === 0) resolve();
772
+ else reject(new Error(`Failed to open browser (exit code ${code ?? "unknown"})`));
773
+ });
774
+ child.unref();
775
+ });
776
+ return;
777
+ }
747
778
  const result = plat === "darwin"
748
779
  ? await pi.exec("open", [url])
749
- : plat === "win32"
750
- ? await pi.exec("cmd", ["/c", "start", "", url])
751
- : await pi.exec("xdg-open", [url]);
780
+ : await pi.exec("cmd", ["/c", "start", "", url]);
752
781
  if (result.code !== 0) {
753
782
  throw new Error(result.stderr || `Failed to open browser (exit code ${result.code})`);
754
783
  }
@@ -1645,7 +1674,7 @@ export default function (pi: ExtensionAPI) {
1645
1674
  name: toolNames.webSearch,
1646
1675
  label: "Web Search",
1647
1676
  description:
1648
- `Search the web using OpenAI, Brave, Parallel, Parallel MCP, TinyFish, Search1API, Searchinfinity, Querit, Tavily, Firecrawl, Jina, SERPdive, Kagi, Bocha, Ollama, SearXNG, DuckDuckGo, Exa, Perplexity, Gemini, AnySearch, Valyu, xAI, Bright Data, SerpBase, or Serper. Pass a provider array to search only those providers simultaneously, or use provider "all" to search every eligible provider except Parallel MCP, DuckDuckGo, AnySearch, Valyu, xAI, Bright Data, SerpBase, and Serper. Returns an AI-synthesized answer with source citations. OpenAI search uses a Codex subscription or OpenAI API key; xAI search uses a SuperGrok/X Premium subscription or xAI API key. Parallel MCP, DuckDuckGo, AnySearch, Valyu, xAI, Bright Data, SerpBase, and Serper are available only when explicitly selected. For comprehensive research, prefer queries (plural) with 2-4 varied angles over a single query — each query gets its own synthesized answer, so varying phrasing and scope gives much broader coverage. When includeContent is true, full page content is fetched in the background. Searches auto-open the interactive browser curator and stream results live; set workflow to "none" to skip curation or "auto-summary" for a model-generated summary without the browser curator. The configured provider is used when provider is omitted or set to auto; omit provider unless explicitly overriding it. Without a configured provider, auto-selects OpenAI when suitable and available, then Exa, Brave, Parallel, TinyFish, Search1API, Searchinfinity, Querit, Tavily, Firecrawl, Jina, SERPdive, Kagi, Bocha, Ollama, Perplexity, Gemini API, or Gemini Web. When SearXNG is configured, it is preferred first for local/private search.`,
1677
+ `Search the web using OpenAI, Brave, Parallel, Parallel MCP, TinyFish, Search1API, Searchinfinity, Querit, Tavily, Firecrawl, Jina, SERPdive, Kagi, Bocha, Ollama, SearXNG, DuckDuckGo, Exa, Perplexity, Gemini, AnySearch, Valyu, xAI, Bright Data, SerpBase, or Serper. Pass a provider array to search only those providers simultaneously, or use provider "all" to search every eligible provider except Parallel MCP, DuckDuckGo, AnySearch, Valyu, xAI, Bright Data, SerpBase, and Serper. Returns an AI-synthesized answer with source citations. OpenAI search uses a Codex subscription or OpenAI API key; xAI search uses a SuperGrok/X Premium subscription or xAI API key. Parallel MCP, DuckDuckGo, AnySearch, Valyu, xAI, Bright Data, SerpBase, and Serper are available only when explicitly selected. For comprehensive research, prefer queries (plural) with 2-4 varied angles over a single query — each query gets its own synthesized answer, so varying phrasing and scope gives much broader coverage. When includeContent is true, full page content is fetched in the background. Searches auto-open the interactive browser curator and stream results live; set workflow to "none" to skip curation or "auto-summary" for a model-generated summary without the browser curator. The configured provider is used when provider is omitted or set to auto; omit provider unless explicitly overriding it. Without a configured provider, SearXNG is preferred first for local/private search. When the active Pi model is openai-codex, Codex-backed OpenAI search is preferred next. Otherwise Exa is preferred before OpenAI, then Brave, Parallel, TinyFish, Search1API, Searchinfinity, Querit, Tavily, Firecrawl, Jina, SERPdive, Kagi, Bocha, Ollama, Perplexity, Gemini API, or Gemini Web.`,
1649
1678
  promptSnippet:
1650
1679
  "Use for web research questions. Prefer {queries:[...]} with 2-4 varied angles over a single query for broader coverage. Omit provider unless explicitly overriding the configured default.",
1651
1680
  parameters: Type.Object({
@@ -19,7 +19,7 @@ const MODEL_PREFERENCE = [
19
19
  (id: string) => id.includes("terra"),
20
20
  (id: string) => /^gpt-\d+(\.\d+)?$/.test(id),
21
21
  ];
22
- const SEARCH_PROVIDERS = ["openai-codex", "openai"] as const;
22
+ const DEFAULT_SEARCH_PROVIDERS: readonly string[] = ["openai-codex", "openai"];
23
23
 
24
24
  function pickSearchModel<T extends { id: string }>(models: readonly T[]): T | undefined {
25
25
  const candidates = models
@@ -36,12 +36,13 @@ interface WebSearchConfig {
36
36
  openaiApiKey?: unknown;
37
37
  openaiResponsesUrl?: unknown;
38
38
  openaiSearchModel?: unknown;
39
+ openaiSearchProviders?: unknown;
39
40
  }
40
41
 
41
42
  type ProviderHeaders = Record<string, string | null>;
42
43
 
43
44
  interface OpenAIAuth {
44
- provider: "openai-codex" | "openai";
45
+ provider: string;
45
46
  apiKey: string;
46
47
  model: string;
47
48
  headers: ProviderHeaders;
@@ -149,6 +150,14 @@ function resolveConfiguredResponsesUrl(value: unknown): string {
149
150
  return url.toString();
150
151
  }
151
152
 
153
+ function resolveConfiguredSearchProviders(value: unknown): readonly string[] {
154
+ if (value === undefined) return DEFAULT_SEARCH_PROVIDERS;
155
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || entry.trim().length === 0)) {
156
+ throw new Error(`openaiSearchProviders in ${CONFIG_PATH} must be an array of non-empty Pi provider ids`);
157
+ }
158
+ return value.map((entry) => entry.trim());
159
+ }
160
+
152
161
  function resolveConfiguredSearchModel(value: unknown): string | undefined {
153
162
  if (value == null) return undefined;
154
163
  if (typeof value !== "string" || value.trim().length === 0) {
@@ -165,14 +174,14 @@ function toRequestHeaders(headers: ProviderHeaders): Record<string, string> {
165
174
  return requestHeaders;
166
175
  }
167
176
 
168
- async function resolvePiAuth(ctx: ExtensionContext, responsesUrl: string, modelOverride?: string): Promise<OpenAIAuth | undefined> {
177
+ async function resolvePiAuth(ctx: ExtensionContext, responsesUrl: string, providers: readonly string[], modelOverride?: string): Promise<OpenAIAuth | undefined> {
169
178
  let models: ReturnType<typeof ctx.modelRegistry.getAll>;
170
179
  try {
171
180
  models = ctx.modelRegistry.getAll();
172
181
  } catch {
173
182
  return undefined;
174
183
  }
175
- for (const provider of SEARCH_PROVIDERS) {
184
+ for (const provider of providers) {
176
185
  const preferred = pickSearchModel(models.filter((model) => model.provider === provider));
177
186
  if (!preferred) continue;
178
187
  try {
@@ -196,8 +205,9 @@ export async function resolveOpenAIAuth(ctx?: ExtensionContext, signal?: AbortSi
196
205
  const config = loadConfig();
197
206
  const responsesUrl = resolveConfiguredResponsesUrl(config.openaiResponsesUrl);
198
207
  const modelOverride = resolveConfiguredSearchModel(config.openaiSearchModel);
208
+ const providers = resolveConfiguredSearchProviders(config.openaiSearchProviders);
199
209
  if (ctx) {
200
- const auth = await resolvePiAuth(ctx, responsesUrl, modelOverride);
210
+ const auth = await resolvePiAuth(ctx, responsesUrl, providers, modelOverride);
201
211
  if (auth) return auth;
202
212
  }
203
213
 
@@ -221,7 +231,8 @@ export async function resolveOpenAIAuth(ctx?: ExtensionContext, signal?: AbortSi
221
231
  export async function isOpenAISearchAvailable(ctx?: ExtensionContext): Promise<boolean> {
222
232
  const config = loadConfig();
223
233
  const responsesUrl = resolveConfiguredResponsesUrl(config.openaiResponsesUrl);
224
- if (ctx && await resolvePiAuth(ctx, responsesUrl)) return true;
234
+ const providers = resolveConfiguredSearchProviders(config.openaiSearchProviders);
235
+ if (ctx && await resolvePiAuth(ctx, responsesUrl, providers)) return true;
225
236
  return hasCredentialSource({
226
237
  provider: "OpenAI",
227
238
  configuredValue: config.openaiApiKey,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-web-access",
3
- "version": "0.24.0",
3
+ "version": "0.24.2",
4
4
  "description": "Web search, URL fetching, GitHub repo cloning, PDF extraction, YouTube video understanding, and local video analysis for Pi coding agent. Supports OpenAI, Brave, Parallel, TinyFish, Search1API, Searchinfinity, Querit, Tavily, Firecrawl, Jina, SERPdive, Kagi, Ollama, AnySearch, Bright Data SERP, SerpBase, SearXNG, Exa, Perplexity, and Gemini.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -51,6 +51,7 @@ export const PDF_PROVIDER_VALUES = new Set<PDFProvider>([
51
51
  export interface PDFConfig {
52
52
  enabled: boolean;
53
53
  maxSizeMB: number;
54
+ maxPages: number;
54
55
  provider: PDFProvider;
55
56
  datalabMode: DatalabMode;
56
57
  datalabTimeoutMs: number;
@@ -69,6 +70,7 @@ export function loadPDFConfig(): PDFConfig {
69
70
  return {
70
71
  enabled: true,
71
72
  maxSizeMB: DEFAULT_PDF_MAX_SIZE_MB,
73
+ maxPages: DEFAULT_MAX_PAGES,
72
74
  provider: "auto",
73
75
  datalabMode: normalizeDatalabMode(process.env.DATALAB_MODE),
74
76
  datalabTimeoutMs: DEFAULT_DATALAB_TIMEOUT_MS,
@@ -98,6 +100,13 @@ export function loadPDFConfig(): PDFConfig {
98
100
  configured > 0
99
101
  ? Math.min(configured, MAX_PDF_MAX_SIZE_MB)
100
102
  : DEFAULT_PDF_MAX_SIZE_MB;
103
+ const configuredMaxPages = pdf.maxPages;
104
+ const maxPages =
105
+ typeof configuredMaxPages === "number" &&
106
+ Number.isFinite(configuredMaxPages) &&
107
+ configuredMaxPages > 0
108
+ ? Math.max(1, Math.floor(configuredMaxPages))
109
+ : DEFAULT_MAX_PAGES;
101
110
 
102
111
  const provider =
103
112
  typeof pdf.provider === "string" &&
@@ -120,6 +129,7 @@ export function loadPDFConfig(): PDFConfig {
120
129
  return {
121
130
  enabled,
122
131
  maxSizeMB: normalized,
132
+ maxPages,
123
133
  provider,
124
134
  datalabMode,
125
135
  datalabTimeoutMs,
@@ -155,18 +165,21 @@ export async function extractPDFToMarkdown(
155
165
  options: PDFExtractOptions = {},
156
166
  ): Promise<PDFExtractResult> {
157
167
  const {
158
- maxPages = DEFAULT_MAX_PAGES,
168
+ maxPages,
159
169
  outputDir = DEFAULT_OUTPUT_DIR,
160
170
  filename,
161
171
  signal,
162
172
  geminiTimeoutMs,
163
173
  } = options;
164
174
 
165
- const safeMaxPages = Number.isFinite(maxPages)
166
- ? Math.max(1, Math.floor(maxPages))
167
- : DEFAULT_MAX_PAGES;
168
- const urlTitle = extractTitleFromURL(url);
169
175
  const pdfConfig = loadPDFConfig();
176
+ const safeMaxPages =
177
+ maxPages === undefined
178
+ ? pdfConfig.maxPages
179
+ : Number.isFinite(maxPages)
180
+ ? Math.max(1, Math.floor(maxPages))
181
+ : DEFAULT_MAX_PAGES;
182
+ const urlTitle = extractTitleFromURL(url);
170
183
  const provider = pdfConfig.provider;
171
184
 
172
185
  if (provider === "auto" || provider === "datalab") {
@@ -5,6 +5,7 @@ import { getWebSearchConfigPath } from "./utils.ts";
5
5
 
6
6
  const DEFAULT_MAX_REDIRECTS = 5;
7
7
  const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
8
+ const LOOPBACK_ALLOW_RANGES = ["127.0.0.0/8", "::1", "::ffff:127.0.0.0/104"];
8
9
 
9
10
  export type LookupAddress = { address: string; family: number };
10
11
  export type Lookup = (hostname: string) => Promise<LookupAddress[]>;
@@ -152,6 +153,8 @@ interface ValidationOptions {
152
153
  * the local SSRF preflight. This does not configure proxy transport.
153
154
  */
154
155
  trustEnvProxy?: boolean;
156
+ /** Allow loopback URLs for explicit provider base endpoints, not fetched targets. */
157
+ allowLoopback?: boolean;
155
158
  }
156
159
 
157
160
  /** Parsed entry from `allowRanges`: a network address (4 or 16 bytes) + prefix length. */
@@ -185,7 +188,11 @@ export async function validateRemoteUrl(rawUrl: string | URL, options: Validatio
185
188
 
186
189
  const hostname = normalizeHostname(url.hostname);
187
190
  if (!hostname) throw new Error("URL must include a hostname");
188
- if (hostname === "localhost" || hostname.endsWith(".localhost")) {
191
+ if (hostname === "localhost") {
192
+ if (options.allowLoopback === true) return url;
193
+ throw new Error(`Blocked internal hostname: ${hostname}`);
194
+ }
195
+ if (hostname.endsWith(".localhost")) {
189
196
  throw new Error(`Blocked internal hostname: ${hostname}`);
190
197
  }
191
198
 
@@ -193,7 +200,10 @@ export async function validateRemoteUrl(rawUrl: string | URL, options: Validatio
193
200
  assertDomainPolicy(hostname, options.domainPolicy);
194
201
 
195
202
  if (net.isIP(hostname)) {
196
- assertPublicAddress(hostname, hostname, allowRanges);
203
+ const addressAllowRanges = options.allowLoopback === true
204
+ ? [...allowRanges, ...parseAllowRanges(LOOPBACK_ALLOW_RANGES)]
205
+ : allowRanges;
206
+ assertPublicAddress(hostname, hostname, addressAllowRanges);
197
207
  return url;
198
208
  }
199
209
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@herbertgao/pi-extensions",
3
- "version": "2026.8.7",
3
+ "version": "2026.8.9",
4
4
  "description": "Aggregate installer for HerbertGao-maintained Pi extensions.",
5
5
  "keywords": [
6
6
  "extensions",
@@ -36,35 +36,35 @@
36
36
  "dependencies": {
37
37
  "@ast-grep/cli": "^0.45.0",
38
38
  "@ast-grep/napi": "^0.45.0",
39
- "@czottmann/pi-automode": "1.11.0",
39
+ "@czottmann/pi-automode": "1.12.0",
40
40
  "@dietrichgebert/ponytail": "4.9.0",
41
41
  "@earendil-works/pi-coding-agent": "^0.84.2",
42
42
  "@earendil-works/pi-tui": "^0.84.2",
43
43
  "@effect/platform-node": "4.0.0-beta.103",
44
44
  "@effect/platform-node-shared": "4.0.0-beta.103",
45
- "@herbertgao/pi-cc-extensions": "0.8.56",
45
+ "@herbertgao/pi-cc-extensions": "0.8.58",
46
46
  "@herbertgao/pi-copy-response": "0.2.6",
47
- "@herbertgao/pi-handoff": "1.1.2",
47
+ "@herbertgao/pi-handoff": "2.0.0",
48
48
  "@herbertgao/pi-inline-skills": "1.0.5",
49
- "@herbertgao/pi-mermaid-open": "0.1.3",
50
- "@herbertgao/pi-preferred-thinking": "0.3.0",
51
- "@herbertgao/pi-recap": "0.4.4",
52
- "@herbertgao/pi-rename": "0.4.2",
53
- "@herbertgao/pi-stash": "0.1.1",
54
- "@herbertgao/pi-subagents": "0.16.0",
49
+ "@herbertgao/pi-mermaid-open": "0.2.0",
50
+ "@herbertgao/pi-preferred-thinking": "0.4.0",
51
+ "@herbertgao/pi-recap": "0.4.5",
52
+ "@herbertgao/pi-rename": "0.5.0",
53
+ "@herbertgao/pi-stash": "0.1.2",
54
+ "@herbertgao/pi-subagents": "0.16.1",
55
55
  "@herbertgao/pi-titlebar-spinner": "0.1.3",
56
- "@juicesharp/rpiv-ask-user-question": "2.6.4",
57
- "@juicesharp/rpiv-config": "^2.6.4",
56
+ "@juicesharp/rpiv-ask-user-question": "2.7.0",
57
+ "@juicesharp/rpiv-config": "^2.7.0",
58
58
  "@modelcontextprotocol/client": "2.0.0",
59
59
  "@modelcontextprotocol/core": "2.0.0",
60
60
  "@modelcontextprotocol/ext-apps": "^1.2.2",
61
61
  "@modelcontextprotocol/sdk": "^1.29.0",
62
62
  "@mozilla/readability": "^0.6.0",
63
63
  "@napi-rs/keyring": "^1.3.0",
64
- "@narumitw/pi-btw": "0.55.0",
65
- "@narumitw/pi-tui-kit": "^0.56.0",
64
+ "@narumitw/pi-btw": "0.55.1",
65
+ "@narumitw/pi-tui-kit": "^0.57.0",
66
66
  "@noble/ed25519": "^3.1.0",
67
- "@pi-plugins/fast-mode": "0.1.9",
67
+ "@pi-plugins/fast-mode": "0.1.10",
68
68
  "@shikijs/cli": "^4.0.2",
69
69
  "@sinclair/typebox": "^0.34.49",
70
70
  "ajv": "^8.17.1",
@@ -73,7 +73,7 @@
73
73
  "croner": "^10.0.1",
74
74
  "cross-spawn": "^7.0.6",
75
75
  "effect": "4.0.0-beta.103",
76
- "grok-mermaid": "^0.2.2",
76
+ "grok-mermaid": "0.2.2",
77
77
  "jiti": "^2.7.0",
78
78
  "js-yaml": "^5.2.2",
79
79
  "linkedom": "^0.16.0",
@@ -82,9 +82,9 @@
82
82
  "open": "^10.2.0",
83
83
  "p-limit": "^6.1.0",
84
84
  "pi-footer": "0.5.1",
85
- "pi-lens": "4.1.0",
85
+ "pi-lens": "4.1.1",
86
86
  "pi-mcp-adapter": "2.27.0",
87
- "pi-web-access": "0.24.0",
87
+ "pi-web-access": "0.24.2",
88
88
  "pidusage": "^4.0.1",
89
89
  "promise.try": "^2.0.1",
90
90
  "qrcode-terminal": "^0.12.0",
@@ -159,6 +159,7 @@
159
159
  "./node_modules/@herbertgao/pi-cc-extensions/themes/cc-light.json"
160
160
  ],
161
161
  "skills": [
162
+ "./node_modules/@czottmann/pi-automode/skills",
162
163
  "./node_modules/@dietrichgebert/ponytail/skills",
163
164
  "./node_modules/pi-mcp-adapter/skills",
164
165
  "./node_modules/pi-lens/skills"