gsd-pi 2.20.0 → 2.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (279) hide show
  1. package/README.md +12 -0
  2. package/dist/cli.js +21 -18
  3. package/dist/help-text.d.ts +2 -0
  4. package/dist/help-text.js +47 -0
  5. package/dist/loader.js +2 -16
  6. package/dist/mcp-server.d.ts +18 -0
  7. package/dist/mcp-server.js +53 -0
  8. package/dist/onboarding.d.ts +0 -6
  9. package/dist/onboarding.js +0 -28
  10. package/dist/resources/agents/javascript-pro.md +280 -0
  11. package/dist/resources/agents/typescript-pro.md +255 -0
  12. package/dist/resources/extensions/bg-shell/index.ts +14 -2
  13. package/dist/resources/extensions/bg-shell/utilities.ts +14 -0
  14. package/dist/resources/extensions/browser-tools/{core.js → core.ts} +329 -190
  15. package/dist/resources/extensions/gsd/auto-dashboard.ts +2 -1
  16. package/dist/resources/extensions/gsd/auto-prompts.ts +42 -1
  17. package/dist/resources/extensions/gsd/auto-worktree.ts +35 -2
  18. package/dist/resources/extensions/gsd/auto.ts +116 -10
  19. package/dist/resources/extensions/gsd/claude-import.ts +656 -0
  20. package/dist/resources/extensions/gsd/collision-diagnostics.ts +332 -0
  21. package/dist/resources/extensions/gsd/commands.ts +60 -7
  22. package/dist/resources/extensions/gsd/diff-context.ts +220 -0
  23. package/dist/resources/extensions/gsd/docs/claude-marketplace-import.md +214 -0
  24. package/dist/resources/extensions/gsd/docs/preferences-reference.md +2 -1
  25. package/dist/resources/extensions/gsd/doctor.ts +12 -4
  26. package/dist/resources/extensions/gsd/file-watcher.ts +97 -0
  27. package/dist/resources/extensions/gsd/files.ts +1 -1
  28. package/dist/resources/extensions/gsd/forensics.ts +596 -0
  29. package/dist/resources/extensions/gsd/git-service.ts +2 -1
  30. package/dist/resources/extensions/gsd/github-client.ts +235 -0
  31. package/dist/resources/extensions/gsd/gitignore.ts +1 -0
  32. package/dist/resources/extensions/gsd/marketplace-discovery.ts +507 -0
  33. package/dist/resources/extensions/gsd/mcp-server.ts +87 -0
  34. package/dist/resources/extensions/gsd/namespaced-registry.ts +467 -0
  35. package/dist/resources/extensions/gsd/namespaced-resolver.ts +307 -0
  36. package/dist/resources/extensions/gsd/plugin-importer.ts +410 -0
  37. package/dist/resources/extensions/gsd/preferences.ts +14 -3
  38. package/dist/resources/extensions/gsd/prompts/execute-task.md +1 -1
  39. package/dist/resources/extensions/gsd/prompts/forensics.md +71 -0
  40. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  41. package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
  42. package/dist/resources/extensions/gsd/prompts/system.md +12 -3
  43. package/dist/resources/extensions/gsd/roadmap-slices.ts +1 -1
  44. package/dist/resources/extensions/gsd/session-forensics.ts +5 -5
  45. package/dist/resources/extensions/gsd/tests/claude-import-tui.test.ts +351 -0
  46. package/dist/resources/extensions/gsd/tests/collision-diagnostics.test.ts +705 -0
  47. package/dist/resources/extensions/gsd/tests/context-compression.test.ts +13 -0
  48. package/dist/resources/extensions/gsd/tests/diff-context.test.ts +136 -0
  49. package/dist/resources/extensions/gsd/tests/doctor-git.test.ts +118 -0
  50. package/dist/resources/extensions/gsd/tests/marketplace-discovery.test.ts +202 -0
  51. package/dist/resources/extensions/gsd/tests/marketplace-test-fixtures.ts +91 -0
  52. package/dist/resources/extensions/gsd/tests/namespaced-registry.test.ts +1027 -0
  53. package/dist/resources/extensions/gsd/tests/namespaced-resolver.test.ts +671 -0
  54. package/dist/resources/extensions/gsd/tests/none-mode-gates.test.ts +105 -0
  55. package/dist/resources/extensions/gsd/tests/plugin-importer-live.test.ts +481 -0
  56. package/dist/resources/extensions/gsd/tests/plugin-importer.test.ts +1383 -0
  57. package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +21 -2
  58. package/dist/resources/extensions/gsd/tests/preferences-schema-validation.test.ts +8 -1
  59. package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +11 -0
  60. package/dist/resources/extensions/gsd/tests/worktree-bugfix.test.ts +120 -0
  61. package/dist/resources/extensions/gsd/token-counter.ts +45 -0
  62. package/dist/resources/extensions/gsd/visualizer-data.ts +2 -2
  63. package/dist/resources/extensions/gsd/worktree-manager.ts +29 -1
  64. package/dist/resources/extensions/gsd/worktree.ts +3 -0
  65. package/dist/resources/extensions/mcporter/index.ts +90 -7
  66. package/dist/resources/extensions/search-the-web/native-search.ts +2 -0
  67. package/dist/resources/extensions/search-the-web/tool-fetch-page.ts +9 -1
  68. package/dist/resources/extensions/search-the-web/url-utils.ts +35 -1
  69. package/dist/resources/extensions/shared/terminal.ts +1 -1
  70. package/dist/resources/extensions/universal-config/discovery.ts +4 -0
  71. package/dist/resources/extensions/universal-config/format.ts +35 -4
  72. package/dist/resources/extensions/universal-config/index.ts +5 -3
  73. package/dist/resources/extensions/universal-config/scanners.ts +65 -2
  74. package/dist/resources/extensions/universal-config/tests/discovery.test.ts +9 -1
  75. package/dist/resources/extensions/universal-config/tests/format.test.ts +22 -6
  76. package/dist/resources/extensions/universal-config/tests/scanners.test.ts +18 -0
  77. package/dist/resources/extensions/universal-config/types.ts +20 -1
  78. package/dist/resources/skills/lint/SKILL.md +141 -0
  79. package/dist/resources/skills/review/SKILL.md +214 -0
  80. package/dist/resources/skills/test/SKILL.md +201 -0
  81. package/dist/tool-bootstrap.js +2 -1
  82. package/dist/wizard.js +2 -0
  83. package/package.json +12 -7
  84. package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts.map +1 -1
  85. package/packages/pi-ai/dist/providers/azure-openai-responses.js +12 -1
  86. package/packages/pi-ai/dist/providers/azure-openai-responses.js.map +1 -1
  87. package/packages/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
  88. package/packages/pi-ai/dist/providers/openai-responses.js +12 -1
  89. package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
  90. package/packages/pi-ai/node_modules/@smithy/node-http-handler/LICENSE +201 -0
  91. package/packages/pi-ai/node_modules/@smithy/node-http-handler/README.md +9 -0
  92. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-cjs/index.js +762 -0
  93. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-es/build-abort-error.js +19 -0
  94. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-es/constants.js +1 -0
  95. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-es/get-transformed-headers.js +9 -0
  96. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-es/index.js +3 -0
  97. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-es/node-http-handler.js +230 -0
  98. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-es/node-http2-connection-manager.js +87 -0
  99. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-es/node-http2-connection-pool.js +32 -0
  100. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-es/node-http2-handler.js +169 -0
  101. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-es/readable.mock.js +21 -0
  102. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-es/server.mock.js +88 -0
  103. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-es/set-connection-timeout.js +36 -0
  104. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-es/set-request-timeout.js +21 -0
  105. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-es/set-socket-keep-alive.js +22 -0
  106. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-es/set-socket-timeout.js +23 -0
  107. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-es/stream-collector/collector.js +8 -0
  108. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-es/stream-collector/index.js +41 -0
  109. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-es/stream-collector/readable.mock.js +21 -0
  110. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-es/timing.js +4 -0
  111. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-es/write-request-body.js +63 -0
  112. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/build-abort-error.d.ts +10 -0
  113. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/constants.d.ts +5 -0
  114. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/get-transformed-headers.d.ts +4 -0
  115. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/index.d.ts +3 -0
  116. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/node-http-handler.d.ts +46 -0
  117. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/node-http2-connection-manager.d.ts +24 -0
  118. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/node-http2-connection-pool.d.ts +12 -0
  119. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/node-http2-handler.d.ts +63 -0
  120. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/readable.mock.d.ts +13 -0
  121. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/server.mock.d.ts +12 -0
  122. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/set-connection-timeout.d.ts +2 -0
  123. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/set-request-timeout.d.ts +6 -0
  124. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/set-socket-keep-alive.d.ts +6 -0
  125. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/set-socket-timeout.d.ts +2 -0
  126. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/stream-collector/collector.d.ts +5 -0
  127. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/stream-collector/index.d.ts +6 -0
  128. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/stream-collector/readable.mock.d.ts +13 -0
  129. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/timing.d.ts +8 -0
  130. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/ts3.4/build-abort-error.d.ts +10 -0
  131. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/ts3.4/constants.d.ts +5 -0
  132. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/ts3.4/get-transformed-headers.d.ts +4 -0
  133. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/ts3.4/index.d.ts +3 -0
  134. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/ts3.4/node-http-handler.d.ts +46 -0
  135. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/ts3.4/node-http2-connection-manager.d.ts +24 -0
  136. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/ts3.4/node-http2-connection-pool.d.ts +12 -0
  137. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/ts3.4/node-http2-handler.d.ts +63 -0
  138. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/ts3.4/readable.mock.d.ts +13 -0
  139. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/ts3.4/server.mock.d.ts +12 -0
  140. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/ts3.4/set-connection-timeout.d.ts +2 -0
  141. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/ts3.4/set-request-timeout.d.ts +6 -0
  142. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/ts3.4/set-socket-keep-alive.d.ts +6 -0
  143. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/ts3.4/set-socket-timeout.d.ts +2 -0
  144. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/ts3.4/stream-collector/collector.d.ts +5 -0
  145. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/ts3.4/stream-collector/index.d.ts +6 -0
  146. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/ts3.4/stream-collector/readable.mock.d.ts +13 -0
  147. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/ts3.4/timing.d.ts +8 -0
  148. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/ts3.4/write-request-body.d.ts +12 -0
  149. package/packages/pi-ai/node_modules/@smithy/node-http-handler/dist-types/write-request-body.d.ts +12 -0
  150. package/packages/pi-ai/node_modules/@smithy/node-http-handler/package.json +68 -0
  151. package/packages/pi-ai/package.json +3 -0
  152. package/packages/pi-ai/pnpm-lock.yaml +2022 -0
  153. package/packages/pi-ai/src/providers/azure-openai-responses.ts +12 -1
  154. package/packages/pi-ai/src/providers/openai-responses.ts +12 -1
  155. package/packages/pi-coding-agent/dist/core/extensions/index.d.ts +1 -1
  156. package/packages/pi-coding-agent/dist/core/extensions/index.d.ts.map +1 -1
  157. package/packages/pi-coding-agent/dist/core/extensions/index.js +1 -1
  158. package/packages/pi-coding-agent/dist/core/extensions/index.js.map +1 -1
  159. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts +1 -0
  160. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  161. package/packages/pi-coding-agent/dist/core/extensions/loader.js +12 -1
  162. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  163. package/packages/pi-coding-agent/dist/core/extensions/loader.test.d.ts +2 -0
  164. package/packages/pi-coding-agent/dist/core/extensions/loader.test.d.ts.map +1 -0
  165. package/packages/pi-coding-agent/dist/core/extensions/loader.test.js +113 -0
  166. package/packages/pi-coding-agent/dist/core/extensions/loader.test.js.map +1 -0
  167. package/packages/pi-coding-agent/dist/core/extensions/project-trust.d.ts +4 -0
  168. package/packages/pi-coding-agent/dist/core/extensions/project-trust.d.ts.map +1 -0
  169. package/packages/pi-coding-agent/dist/core/extensions/project-trust.js +42 -0
  170. package/packages/pi-coding-agent/dist/core/extensions/project-trust.js.map +1 -0
  171. package/packages/pi-coding-agent/dist/core/resolve-config-value.d.ts +1 -0
  172. package/packages/pi-coding-agent/dist/core/resolve-config-value.d.ts.map +1 -1
  173. package/packages/pi-coding-agent/dist/core/resolve-config-value.js +18 -0
  174. package/packages/pi-coding-agent/dist/core/resolve-config-value.js.map +1 -1
  175. package/packages/pi-coding-agent/dist/core/resolve-config-value.test.d.ts +2 -0
  176. package/packages/pi-coding-agent/dist/core/resolve-config-value.test.d.ts.map +1 -0
  177. package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js +114 -0
  178. package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js.map +1 -0
  179. package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  180. package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.js +8 -2
  181. package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.js.map +1 -1
  182. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.d.ts +2 -0
  183. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.d.ts.map +1 -1
  184. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.js +23 -9
  185. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.js.map +1 -1
  186. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.test.d.ts +2 -0
  187. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.test.d.ts.map +1 -0
  188. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.test.js +67 -0
  189. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.test.js.map +1 -0
  190. package/packages/pi-coding-agent/package.json +1 -1
  191. package/packages/pi-coding-agent/pnpm-lock.yaml +454 -0
  192. package/packages/pi-coding-agent/scripts/copy-assets.cjs +24 -0
  193. package/packages/pi-coding-agent/src/core/extensions/index.ts +3 -0
  194. package/packages/pi-coding-agent/src/core/extensions/loader.test.ts +141 -0
  195. package/packages/pi-coding-agent/src/core/extensions/loader.ts +14 -1
  196. package/packages/pi-coding-agent/src/core/extensions/project-trust.ts +51 -0
  197. package/packages/pi-coding-agent/src/core/resolve-config-value.test.ts +132 -0
  198. package/packages/pi-coding-agent/src/core/resolve-config-value.ts +20 -0
  199. package/packages/pi-coding-agent/src/modes/interactive/components/login-dialog.ts +7 -2
  200. package/packages/pi-coding-agent/src/resources/extensions/memory/storage.test.ts +98 -0
  201. package/packages/pi-coding-agent/src/resources/extensions/memory/storage.ts +24 -9
  202. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  203. package/packages/pi-tui/dist/tui.js +2 -2
  204. package/packages/pi-tui/dist/tui.js.map +1 -1
  205. package/packages/pi-tui/package.json +3 -1
  206. package/packages/pi-tui/src/tui.ts +2 -2
  207. package/src/resources/agents/javascript-pro.md +280 -0
  208. package/src/resources/agents/typescript-pro.md +255 -0
  209. package/src/resources/extensions/bg-shell/index.ts +14 -2
  210. package/src/resources/extensions/bg-shell/utilities.ts +14 -0
  211. package/src/resources/extensions/browser-tools/{core.js → core.ts} +329 -190
  212. package/src/resources/extensions/gsd/auto-dashboard.ts +2 -1
  213. package/src/resources/extensions/gsd/auto-prompts.ts +42 -1
  214. package/src/resources/extensions/gsd/auto-worktree.ts +35 -2
  215. package/src/resources/extensions/gsd/auto.ts +116 -10
  216. package/src/resources/extensions/gsd/claude-import.ts +656 -0
  217. package/src/resources/extensions/gsd/collision-diagnostics.ts +332 -0
  218. package/src/resources/extensions/gsd/commands.ts +60 -7
  219. package/src/resources/extensions/gsd/diff-context.ts +220 -0
  220. package/src/resources/extensions/gsd/docs/claude-marketplace-import.md +214 -0
  221. package/src/resources/extensions/gsd/docs/preferences-reference.md +2 -1
  222. package/src/resources/extensions/gsd/doctor.ts +12 -4
  223. package/src/resources/extensions/gsd/file-watcher.ts +97 -0
  224. package/src/resources/extensions/gsd/files.ts +1 -1
  225. package/src/resources/extensions/gsd/forensics.ts +596 -0
  226. package/src/resources/extensions/gsd/git-service.ts +2 -1
  227. package/src/resources/extensions/gsd/github-client.ts +235 -0
  228. package/src/resources/extensions/gsd/gitignore.ts +1 -0
  229. package/src/resources/extensions/gsd/marketplace-discovery.ts +507 -0
  230. package/src/resources/extensions/gsd/mcp-server.ts +87 -0
  231. package/src/resources/extensions/gsd/namespaced-registry.ts +467 -0
  232. package/src/resources/extensions/gsd/namespaced-resolver.ts +307 -0
  233. package/src/resources/extensions/gsd/plugin-importer.ts +410 -0
  234. package/src/resources/extensions/gsd/preferences.ts +14 -3
  235. package/src/resources/extensions/gsd/prompts/execute-task.md +1 -1
  236. package/src/resources/extensions/gsd/prompts/forensics.md +71 -0
  237. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  238. package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
  239. package/src/resources/extensions/gsd/prompts/system.md +12 -3
  240. package/src/resources/extensions/gsd/roadmap-slices.ts +1 -1
  241. package/src/resources/extensions/gsd/session-forensics.ts +5 -5
  242. package/src/resources/extensions/gsd/tests/claude-import-tui.test.ts +351 -0
  243. package/src/resources/extensions/gsd/tests/collision-diagnostics.test.ts +705 -0
  244. package/src/resources/extensions/gsd/tests/context-compression.test.ts +13 -0
  245. package/src/resources/extensions/gsd/tests/diff-context.test.ts +136 -0
  246. package/src/resources/extensions/gsd/tests/doctor-git.test.ts +118 -0
  247. package/src/resources/extensions/gsd/tests/marketplace-discovery.test.ts +202 -0
  248. package/src/resources/extensions/gsd/tests/marketplace-test-fixtures.ts +91 -0
  249. package/src/resources/extensions/gsd/tests/namespaced-registry.test.ts +1027 -0
  250. package/src/resources/extensions/gsd/tests/namespaced-resolver.test.ts +671 -0
  251. package/src/resources/extensions/gsd/tests/none-mode-gates.test.ts +105 -0
  252. package/src/resources/extensions/gsd/tests/plugin-importer-live.test.ts +481 -0
  253. package/src/resources/extensions/gsd/tests/plugin-importer.test.ts +1383 -0
  254. package/src/resources/extensions/gsd/tests/preferences-git.test.ts +21 -2
  255. package/src/resources/extensions/gsd/tests/preferences-schema-validation.test.ts +8 -1
  256. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +11 -0
  257. package/src/resources/extensions/gsd/tests/worktree-bugfix.test.ts +120 -0
  258. package/src/resources/extensions/gsd/token-counter.ts +45 -0
  259. package/src/resources/extensions/gsd/visualizer-data.ts +2 -2
  260. package/src/resources/extensions/gsd/worktree-manager.ts +29 -1
  261. package/src/resources/extensions/gsd/worktree.ts +3 -0
  262. package/src/resources/extensions/mcporter/index.ts +90 -7
  263. package/src/resources/extensions/search-the-web/native-search.ts +2 -0
  264. package/src/resources/extensions/search-the-web/tool-fetch-page.ts +9 -1
  265. package/src/resources/extensions/search-the-web/url-utils.ts +35 -1
  266. package/src/resources/extensions/shared/terminal.ts +1 -1
  267. package/src/resources/extensions/universal-config/discovery.ts +4 -0
  268. package/src/resources/extensions/universal-config/format.ts +35 -4
  269. package/src/resources/extensions/universal-config/index.ts +5 -3
  270. package/src/resources/extensions/universal-config/scanners.ts +65 -2
  271. package/src/resources/extensions/universal-config/tests/discovery.test.ts +9 -1
  272. package/src/resources/extensions/universal-config/tests/format.test.ts +22 -6
  273. package/src/resources/extensions/universal-config/tests/scanners.test.ts +18 -0
  274. package/src/resources/extensions/universal-config/types.ts +20 -1
  275. package/src/resources/skills/lint/SKILL.md +141 -0
  276. package/src/resources/skills/review/SKILL.md +214 -0
  277. package/src/resources/skills/test/SKILL.md +201 -0
  278. package/dist/resources/extensions/browser-tools/core.d.ts +0 -205
  279. package/src/resources/extensions/browser-tools/core.d.ts +0 -205
@@ -1,7 +1,7 @@
1
1
  // GSD Git Preferences Tests — validates git.isolation and git.merge_to_main handling
2
2
 
3
3
  import { createTestContext } from "./test-helpers.ts";
4
- import { validatePreferences } from "../preferences.ts";
4
+ import { validatePreferences, getIsolationMode } from "../preferences.ts";
5
5
 
6
6
  const { assertEq, assertTrue, report } = createTestContext();
7
7
 
@@ -21,12 +21,18 @@ async function main(): Promise<void> {
21
21
  assertEq(warnings.length, 0, "isolation: branch — no warnings");
22
22
  assertEq(preferences.git?.isolation, "branch", "isolation: branch — value preserved");
23
23
  }
24
+ {
25
+ const { preferences, warnings, errors } = validatePreferences({ git: { isolation: "none" } });
26
+ assertEq(errors.length, 0, "isolation: none — no errors");
27
+ assertEq(warnings.length, 0, "isolation: none — no warnings");
28
+ assertEq(preferences.git?.isolation, "none", "isolation: none — value preserved");
29
+ }
24
30
 
25
31
  // Invalid values produce errors
26
32
  {
27
33
  const { errors } = validatePreferences({ git: { isolation: "invalid" as any } });
28
34
  assertTrue(errors.length > 0, "isolation: invalid — produces error");
29
- assertTrue(errors[0].includes("worktree, branch"), "isolation: invalid — error mentions valid values");
35
+ assertTrue(errors[0].includes("worktree, branch, none"), "isolation: invalid — error mentions valid values");
30
36
  }
31
37
 
32
38
  // Undefined passes through without warning
@@ -95,6 +101,19 @@ async function main(): Promise<void> {
95
101
  assertEq(preferences.git?.commit_docs, undefined, "commit_docs: undefined — not set");
96
102
  }
97
103
 
104
+ console.log("\n=== getIsolationMode() ===");
105
+
106
+ // Returns "none" when set to "none"
107
+ // Note: getIsolationMode() reads from disk via loadEffectiveGSDPreferences,
108
+ // so we test it indirectly by verifying the function is exported and callable.
109
+ // The validation tests above prove the preference value is stored correctly.
110
+ // Direct mode tests require mocking the filesystem, so we test the function's
111
+ // default return value (no preferences file in test context).
112
+ {
113
+ const mode = getIsolationMode();
114
+ assertEq(mode, "worktree", "getIsolationMode: returns worktree as default when no prefs file");
115
+ }
116
+
98
117
  report();
99
118
  }
100
119
 
@@ -155,7 +155,7 @@ async function main(): Promise<void> {
155
155
 
156
156
  console.log("\n=== existing behavior preserved ===");
157
157
 
158
- // git.isolation is a valid active setting (worktree | branch) — no warnings or errors
158
+ // git.isolation is a valid active setting (worktree | branch | none) — no warnings or errors
159
159
  {
160
160
  const { warnings, errors, preferences } = validatePreferences({ git: { isolation: "worktree" } } as GSDPreferences);
161
161
  const unknownWarnings = warnings.filter(w => w.includes("unknown"));
@@ -163,6 +163,13 @@ async function main(): Promise<void> {
163
163
  assertEq(errors.length, 0, "valid git.isolation produces no errors");
164
164
  assertEq(preferences.git?.isolation, "worktree", "git.isolation value passes through");
165
165
  }
166
+ {
167
+ const { warnings, errors, preferences } = validatePreferences({ git: { isolation: "none" } } as GSDPreferences);
168
+ const unknownWarnings = warnings.filter(w => w.includes("unknown"));
169
+ assertEq(unknownWarnings.length, 0, "git.isolation none — no unknown-key warning");
170
+ assertEq(errors.length, 0, "git.isolation none produces no errors");
171
+ assertEq(preferences.git?.isolation, "none", "git.isolation none value passes through");
172
+ }
166
173
 
167
174
  // git.merge_to_main is deprecated — still produces deprecation warning
168
175
  {
@@ -165,6 +165,17 @@ assertTrue(
165
165
  "VisualizerData has changelog field",
166
166
  );
167
167
 
168
+ // completedAt must be coerced to String() to handle YAML Date objects (issue #644)
169
+ assertTrue(
170
+ dataSrc.includes("String(summary.frontmatter.completed_at"),
171
+ "completedAt assignment coerces to String() for YAML Date safety",
172
+ );
173
+
174
+ assertTrue(
175
+ dataSrc.includes("String(b.completedAt") && dataSrc.includes("String(a.completedAt"),
176
+ "changelog sort coerces completedAt to String() for YAML Date safety",
177
+ );
178
+
168
179
  // Verify overlay source exists and imports data module
169
180
  const overlayPath = join(__dirname, "..", "visualizer-overlay.ts");
170
181
  const overlaySrc = readFileSync(overlayPath, "utf-8");
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Tests for worktree edge-case bugfixes:
3
+ *
4
+ * 1. resolveGitDir() follows gitdir: pointer in worktrees
5
+ * 2. captureIntegrationBranch() is a no-op in worktrees
6
+ * 3. detectWorktreeName() correctly identifies worktree paths
7
+ */
8
+
9
+ import {
10
+ mkdtempSync, mkdirSync, writeFileSync, rmSync,
11
+ existsSync, readFileSync,
12
+ } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { tmpdir } from "node:os";
15
+ import { execSync } from "node:child_process";
16
+ import { describe, it, after } from "node:test";
17
+
18
+ import { resolveGitDir } from "../worktree-manager.ts";
19
+ import { detectWorktreeName, captureIntegrationBranch } from "../worktree.ts";
20
+ import { createTestContext } from "./test-helpers.ts";
21
+
22
+ const { assertEq, assertTrue, report } = createTestContext();
23
+
24
+ // ─── Helpers ──────────────────────────────────────────────────────────────
25
+
26
+ function run(cmd: string, cwd: string): void {
27
+ execSync(cmd, { cwd, stdio: "ignore" });
28
+ }
29
+
30
+ function initRepo(dir: string): void {
31
+ run("git init", dir);
32
+ run("git config user.email test@test.com", dir);
33
+ run("git config user.name Test", dir);
34
+ run("git commit --allow-empty -m init", dir);
35
+ }
36
+
37
+ // ─── Tests ────────────────────────────────────────────────────────────────
38
+
39
+ describe("worktree-bugfix", () => {
40
+ const dirs: string[] = [];
41
+ after(() => {
42
+ for (const d of dirs) rmSync(d, { recursive: true, force: true });
43
+ report();
44
+ });
45
+
46
+ it("resolveGitDir returns .git directory in normal repo", () => {
47
+ const repo = mkdtempSync(join(tmpdir(), "gsd-wt-fix-"));
48
+ dirs.push(repo);
49
+ initRepo(repo);
50
+ const gitDir = resolveGitDir(repo);
51
+ assertTrue(gitDir.endsWith(".git"), "ends with .git");
52
+ assertTrue(existsSync(gitDir), ".git dir exists");
53
+ });
54
+
55
+ it("resolveGitDir follows gitdir: pointer in worktree", () => {
56
+ const repo = mkdtempSync(join(tmpdir(), "gsd-wt-fix-"));
57
+ dirs.push(repo);
58
+ initRepo(repo);
59
+
60
+ // Simulate a worktree .git file (git worktree add creates these)
61
+ const wtDir = mkdtempSync(join(tmpdir(), "gsd-wt-fix-wt-"));
62
+ dirs.push(wtDir);
63
+ const realGitDir = join(repo, ".git", "worktrees", "test-wt");
64
+ mkdirSync(realGitDir, { recursive: true });
65
+ writeFileSync(join(wtDir, ".git"), `gitdir: ${realGitDir}\n`);
66
+
67
+ const resolved = resolveGitDir(wtDir);
68
+ assertEq(resolved, realGitDir, "resolves to real git dir");
69
+ });
70
+
71
+ it("resolveGitDir returns default when .git doesn't exist", () => {
72
+ const noGit = mkdtempSync(join(tmpdir(), "gsd-wt-fix-"));
73
+ dirs.push(noGit);
74
+ const gitDir = resolveGitDir(noGit);
75
+ assertTrue(gitDir.endsWith(".git"), "returns default .git path");
76
+ });
77
+
78
+ it("detectWorktreeName returns name for worktree path", () => {
79
+ assertEq(
80
+ detectWorktreeName("/project/.gsd/worktrees/M005"),
81
+ "M005",
82
+ "detects worktree name",
83
+ );
84
+ });
85
+
86
+ it("detectWorktreeName returns null for normal repo", () => {
87
+ assertEq(
88
+ detectWorktreeName("/project"),
89
+ null,
90
+ "null for non-worktree path",
91
+ );
92
+ });
93
+
94
+ it("captureIntegrationBranch is a no-op when in a worktree", () => {
95
+ const repo = mkdtempSync(join(tmpdir(), "gsd-wt-fix-"));
96
+ dirs.push(repo);
97
+ initRepo(repo);
98
+
99
+ // Create a fake worktree path structure
100
+ const wtPath = join(repo, ".gsd", "worktrees", "M005");
101
+ mkdirSync(wtPath, { recursive: true });
102
+ mkdirSync(join(wtPath, ".gsd", "milestones", "M005"), { recursive: true });
103
+ // Initialize git in the worktree so getService doesn't fail
104
+ initRepo(wtPath);
105
+
106
+ // captureIntegrationBranch should be a no-op — no META.json written
107
+ const metaPath = join(wtPath, ".gsd", "milestones", "M005", "M005-META.json");
108
+ captureIntegrationBranch(wtPath, "M005");
109
+ assertTrue(!existsSync(metaPath), "no META.json written in worktree");
110
+ });
111
+
112
+ it("detectWorktreeName prevents pull in worktree context", () => {
113
+ // Verifies the guard pattern: if detectWorktreeName returns non-null,
114
+ // the caller should skip pull/fetch operations
115
+ const inWorktree = detectWorktreeName("/project/.gsd/worktrees/M006");
116
+ const inNormal = detectWorktreeName("/project");
117
+ assertTrue(inWorktree !== null, "worktree detected → skip pull");
118
+ assertTrue(inNormal === null, "normal repo → allow pull");
119
+ });
120
+ });
@@ -0,0 +1,45 @@
1
+ interface TokenEncoder {
2
+ encode(text: string): Uint32Array | number[];
3
+ }
4
+
5
+ let encoder: TokenEncoder | null = null;
6
+ let encoderFailed = false;
7
+
8
+ async function getEncoder(): Promise<TokenEncoder | null> {
9
+ if (encoder) return encoder;
10
+ if (encoderFailed) return null;
11
+ try {
12
+ // @ts-ignore — tiktoken may not have type declarations in extensions tsconfig
13
+ const tiktoken = await import("tiktoken");
14
+ encoder = tiktoken.encoding_for_model("gpt-4o") as TokenEncoder;
15
+ return encoder;
16
+ } catch {
17
+ encoderFailed = true;
18
+ return null;
19
+ }
20
+ }
21
+
22
+ export async function countTokens(text: string): Promise<number> {
23
+ const enc = await getEncoder();
24
+ if (enc) {
25
+ const tokens = enc.encode(text);
26
+ return tokens.length;
27
+ }
28
+ return Math.ceil(text.length / 4);
29
+ }
30
+
31
+ export function countTokensSync(text: string): number {
32
+ if (encoder) {
33
+ return encoder.encode(text).length;
34
+ }
35
+ return Math.ceil(text.length / 4);
36
+ }
37
+
38
+ export async function initTokenCounter(): Promise<boolean> {
39
+ const enc = await getEncoder();
40
+ return enc !== null;
41
+ }
42
+
43
+ export function isAccurateCountingAvailable(): boolean {
44
+ return encoder !== null;
45
+ }
@@ -379,7 +379,7 @@ async function loadChangelog(basePath: string, milestones: VisualizerMilestone[]
379
379
  path: f.path,
380
380
  description: f.description,
381
381
  })),
382
- completedAt: summary.frontmatter.completed_at ?? '',
382
+ completedAt: String(summary.frontmatter.completed_at ?? ''),
383
383
  };
384
384
 
385
385
  changelogCache.set(cacheKey, { mtime, entry });
@@ -388,7 +388,7 @@ async function loadChangelog(basePath: string, milestones: VisualizerMilestone[]
388
388
  }
389
389
 
390
390
  // Sort by completedAt descending
391
- entries.sort((a, b) => (b.completedAt || '').localeCompare(a.completedAt || ''));
391
+ entries.sort((a, b) => String(b.completedAt || '').localeCompare(String(a.completedAt || '')));
392
392
 
393
393
  return { entries };
394
394
  }
@@ -15,7 +15,7 @@
15
15
  * 4. remove() — git worktree remove + branch cleanup
16
16
  */
17
17
 
18
- import { existsSync, mkdirSync, realpathSync } from "node:fs";
18
+ import { existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
19
19
  import { join, resolve, sep } from "node:path";
20
20
  import {
21
21
  nativeBranchDelete,
@@ -74,6 +74,34 @@ export function getMainBranch(basePath: string): string {
74
74
  return nativeDetectMainBranch(basePath);
75
75
  }
76
76
 
77
+ // ─── resolveGitDir ─────────────────────────────────────────────────────────
78
+
79
+ /**
80
+ * Resolve the actual git directory for a given repository path.
81
+ *
82
+ * In a normal repo, .git is a directory → returns `<basePath>/.git`.
83
+ * In a worktree, .git is a file containing `gitdir: <path>` → resolves
84
+ * and returns that path.
85
+ *
86
+ * This is critical for operations that reference git metadata files like
87
+ * MERGE_HEAD, SQUASH_MSG, etc. — these live in the git directory, not
88
+ * in the working tree root. Without this, worktree merges fail because
89
+ * they look for MERGE_HEAD in the wrong location.
90
+ */
91
+ export function resolveGitDir(basePath: string): string {
92
+ const gitPath = join(basePath, ".git");
93
+ if (!existsSync(gitPath)) return join(basePath, ".git");
94
+ try {
95
+ const content = readFileSync(gitPath, "utf-8").trim();
96
+ if (content.startsWith("gitdir: ")) {
97
+ return resolve(basePath, content.slice(8));
98
+ }
99
+ } catch {
100
+ // Not a file or unreadable — fall through to default
101
+ }
102
+ return join(basePath, ".git");
103
+ }
104
+
77
105
  export function worktreesDir(basePath: string): string {
78
106
  return join(basePath, ".gsd", "worktrees");
79
107
  }
@@ -55,6 +55,9 @@ export function setActiveMilestoneId(basePath: string, milestoneId: string | nul
55
55
  * if on a GSD slice branch.
56
56
  */
57
57
  export function captureIntegrationBranch(basePath: string, milestoneId: string, options?: { commitDocs?: boolean }): void {
58
+ // In a worktree, the base branch is implicit (worktree/<name>).
59
+ // Writing it to META.json would leave stale metadata after merge back to main.
60
+ if (detectWorktreeName(basePath)) return;
58
61
  const svc = getService(basePath);
59
62
  const current = svc.getCurrentBranch();
60
63
  writeIntegrationBranch(basePath, milestoneId, current, options);
@@ -26,6 +26,8 @@ import { Text } from "@gsd/pi-tui";
26
26
  import { Type } from "@sinclair/typebox";
27
27
  import { execFile, exec } from "node:child_process";
28
28
  import { promisify } from "node:util";
29
+ import { readFileSync, existsSync } from "node:fs";
30
+ import { join } from "node:path";
29
31
 
30
32
  const execFileAsync = promisify(execFile);
31
33
  const execAsync = promisify(exec);
@@ -99,6 +101,37 @@ async function runMcporter(
99
101
  return stdout;
100
102
  }
101
103
 
104
+ /**
105
+ * Read .mcp.json from the project root (cwd) and return servers not already
106
+ * discovered by mcporter. This bridges the gap where mcporter doesn't scan
107
+ * the standard .mcp.json config used by Claude Code, Cursor, etc.
108
+ */
109
+ function readProjectMcpJson(knownNames: Set<string>): McpServer[] {
110
+ const servers: McpServer[] = [];
111
+ try {
112
+ const mcpJsonPath = join(process.cwd(), ".mcp.json");
113
+ if (!existsSync(mcpJsonPath)) return servers;
114
+ const raw = readFileSync(mcpJsonPath, "utf-8");
115
+ const data = JSON.parse(raw) as Record<string, unknown>;
116
+ const mcpServers = (data.mcpServers ?? data.servers) as Record<string, Record<string, unknown>> | undefined;
117
+ if (!mcpServers || typeof mcpServers !== "object") return servers;
118
+
119
+ for (const [name, config] of Object.entries(mcpServers)) {
120
+ if (knownNames.has(name)) continue; // Already discovered by mcporter
121
+ const transport = (config.type as string) ?? (config.command ? "stdio" : config.url ? "http" : "unknown");
122
+ servers.push({
123
+ name,
124
+ status: "ok",
125
+ transport,
126
+ tools: [], // Tools unknown until mcp_discover is called
127
+ });
128
+ }
129
+ } catch {
130
+ // Non-fatal — .mcp.json may not exist or be malformed
131
+ }
132
+ return servers;
133
+ }
134
+
102
135
  async function getServerList(signal?: AbortSignal): Promise<McpServer[]> {
103
136
  if (serverListCache) return serverListCache;
104
137
 
@@ -112,6 +145,14 @@ async function getServerList(signal?: AbortSignal): Promise<McpServer[]> {
112
145
  if (!Array.isArray(data.servers)) {
113
146
  throw new Error(`Unexpected mcporter response shape: ${JSON.stringify(Object.keys(data))}`);
114
147
  }
148
+
149
+ // Merge servers from project-root .mcp.json that mcporter didn't discover
150
+ const knownNames = new Set(data.servers.map((s) => s.name));
151
+ const projectServers = readProjectMcpJson(knownNames);
152
+ if (projectServers.length > 0) {
153
+ data.servers.push(...projectServers);
154
+ }
155
+
115
156
  serverListCache = data.servers;
116
157
  return serverListCache;
117
158
  }
@@ -122,12 +163,39 @@ async function getServerDetail(
122
163
  ): Promise<McpServerDetail> {
123
164
  if (serverDetailCache.has(serverName)) return serverDetailCache.get(serverName)!;
124
165
 
125
- const raw = await runMcporter(["list", serverName, "--schema", "--json"], signal);
166
+ // Check if this server came from .mcp.json (not known to mcporter natively)
167
+ const mcpJsonUrl = getMcpJsonServerUrl(serverName);
168
+ const args = mcpJsonUrl
169
+ ? ["list", mcpJsonUrl, "--schema", "--json"]
170
+ : ["list", serverName, "--schema", "--json"];
171
+
172
+ const raw = await runMcporter(args, signal);
126
173
  const data = JSON.parse(raw) as McpServerDetail;
174
+ // Preserve the user-facing name from .mcp.json
175
+ if (mcpJsonUrl) data.name = serverName;
127
176
  serverDetailCache.set(serverName, data);
128
177
  return data;
129
178
  }
130
179
 
180
+ /**
181
+ * Look up a server's URL from .mcp.json if it's an HTTP server not known to mcporter.
182
+ */
183
+ function getMcpJsonServerUrl(serverName: string): string | null {
184
+ try {
185
+ const mcpJsonPath = join(process.cwd(), ".mcp.json");
186
+ if (!existsSync(mcpJsonPath)) return null;
187
+ const raw = readFileSync(mcpJsonPath, "utf-8");
188
+ const data = JSON.parse(raw) as Record<string, unknown>;
189
+ const mcpServers = (data.mcpServers ?? data.servers) as Record<string, Record<string, unknown>> | undefined;
190
+ if (!mcpServers?.[serverName]) return null;
191
+ const config = mcpServers[serverName];
192
+ if (config.type === "http" && typeof config.url === "string") return config.url;
193
+ return null;
194
+ } catch {
195
+ return null;
196
+ }
197
+ }
198
+
131
199
  function formatServerList(servers: McpServer[]): string {
132
200
  if (servers.length === 0) return "No MCP servers found.";
133
201
 
@@ -328,7 +396,10 @@ export default function (pi: ExtensionAPI) {
328
396
 
329
397
  async execute(_id, params, signal) {
330
398
  // Build mcporter call command: mcporter call server.tool key:value ...
331
- const callTarget = `${params.server}.${params.tool}`;
399
+ // For HTTP servers from .mcp.json, use the URL directly as the server identifier
400
+ const mcpJsonUrl = getMcpJsonServerUrl(params.server);
401
+ const serverRef = mcpJsonUrl ?? params.server;
402
+ const callTarget = `${serverRef}.${params.tool}`;
332
403
  const cliArgs = ["call", callTarget, "--output", "raw"];
333
404
 
334
405
  if (params.args && Object.keys(params.args).length > 0) {
@@ -418,12 +489,24 @@ export default function (pi: ExtensionAPI) {
418
489
  pi.on("session_start", async (_event, ctx) => {
419
490
  try {
420
491
  const ver = (await runMcporter(["--version"], undefined, 5000)).trim();
421
- ctx.ui.notify(`MCPorter ${ver} ready`, "info");
492
+ ctx.ui.notify(`MCPorter ${ver} ready`, "info");
422
493
  } catch {
423
- ctx.ui.notify(
424
- "MCPorter not found. Install with: npm i -g mcporter",
425
- "error",
426
- );
494
+ ctx.ui.notify("MCPorter not found — attempting auto-install…", "warning");
495
+ try {
496
+ await new Promise<void>((resolve, reject) => {
497
+ exec("npm install -g mcporter", { timeout: 60000 }, (err) => {
498
+ if (err) reject(err);
499
+ else resolve();
500
+ });
501
+ });
502
+ const ver = (await runMcporter(["--version"], undefined, 5000)).trim();
503
+ ctx.ui.notify(`MCPorter ${ver} auto-installed ✓`, "info");
504
+ } catch {
505
+ ctx.ui.notify(
506
+ "MCPorter auto-install failed. Install manually: npm i -g mcporter",
507
+ "error",
508
+ );
509
+ }
427
510
  }
428
511
  });
429
512
  }
@@ -157,11 +157,13 @@ export function registerNativeSearchHooks(pi: NativeSearchPI): { getIsAnthropic:
157
157
  const hasBrave = !!process.env.BRAVE_API_KEY;
158
158
  const hasJina = !!process.env.JINA_API_KEY;
159
159
  const hasAnswers = !!process.env.BRAVE_ANSWERS_KEY;
160
+ const hasTavily = !!process.env.TAVILY_API_KEY;
160
161
 
161
162
  const parts: string[] = ["Web search v4 loaded"];
162
163
  if (hasBrave) parts.push("Brave ✓");
163
164
  if (hasAnswers) parts.push("Answers ✓");
164
165
  if (hasJina) parts.push("Jina ✓");
166
+ if (hasTavily) parts.push("Tavily ✓");
165
167
 
166
168
  ctx.ui.notify(parts.join(" · "), "info");
167
169
  });
@@ -15,7 +15,7 @@ import { Type } from "@sinclair/typebox";
15
15
 
16
16
  import { LRUTTLCache } from "./cache.js";
17
17
  import { fetchSimple, HttpError } from "./http.js";
18
- import { extractDomain } from "./url-utils.js";
18
+ import { extractDomain, isBlockedUrl } from "./url-utils.js";
19
19
  import { formatPageContent, type FormatPageOptions } from "./format.js";
20
20
  import { getOllamaApiKey } from "./provider.js";
21
21
 
@@ -416,6 +416,14 @@ export function registerFetchPageTool(pi: ExtensionAPI) {
416
416
  };
417
417
  }
418
418
 
419
+ if (isBlockedUrl(url)) {
420
+ return {
421
+ content: [{ type: "text", text: `Blocked URL: requests to private/internal addresses are not allowed.` }],
422
+ isError: true,
423
+ details: { error: "SSRF blocked", url } satisfies Partial<FetchPageDetails>,
424
+ };
425
+ }
426
+
419
427
  // ------------------------------------------------------------------
420
428
  // Cache lookup (full content cached, offset/truncation applied after)
421
429
  // ------------------------------------------------------------------
@@ -1,7 +1,41 @@
1
1
  /**
2
- * URL normalization and query utilities.
2
+ * URL normalization, query utilities, and SSRF protection.
3
3
  */
4
4
 
5
+ const BLOCKED_HOSTNAMES = new Set([
6
+ "localhost",
7
+ "metadata.google.internal",
8
+ "instance-data",
9
+ ]);
10
+
11
+ const PRIVATE_IP_PATTERNS = [
12
+ /^127\./,
13
+ /^10\./,
14
+ /^172\.(1[6-9]|2\d|3[01])\./,
15
+ /^192\.168\./,
16
+ /^169\.254\./,
17
+ /^0\./,
18
+ /^::1$/,
19
+ /^fc00:/i,
20
+ /^fd/i,
21
+ /^fe80:/i,
22
+ ];
23
+
24
+ export function isBlockedUrl(url: string): boolean {
25
+ try {
26
+ const parsed = new URL(url);
27
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return true;
28
+ const hostname = parsed.hostname.toLowerCase();
29
+ if (BLOCKED_HOSTNAMES.has(hostname)) return true;
30
+ for (const pattern of PRIVATE_IP_PATTERNS) {
31
+ if (pattern.test(hostname)) return true;
32
+ }
33
+ return false;
34
+ } catch {
35
+ return true;
36
+ }
37
+ }
38
+
5
39
  /** Normalize a search query into a stable cache key. */
6
40
  export function normalizeQuery(query: string): string {
7
41
  return query.trim().toLowerCase().replace(/\s+/g, " ").normalize("NFC");
@@ -5,7 +5,7 @@
5
5
  * Terminals that lack this support silently swallow the key combos.
6
6
  */
7
7
 
8
- const UNSUPPORTED_TERMS = ["apple_terminal"];
8
+ const UNSUPPORTED_TERMS = ["apple_terminal", "warpterm"];
9
9
 
10
10
  export function supportsCtrlAltShortcuts(): boolean {
11
11
  const term = (process.env.TERM_PROGRAM || "").toLowerCase();
@@ -58,6 +58,8 @@ export async function discoverAllConfigs(
58
58
  const rules = allItems.filter((i) => i.type === "rule").length;
59
59
  const contextFiles = allItems.filter((i) => i.type === "context-file").length;
60
60
  const settings = allItems.filter((i) => i.type === "settings").length;
61
+ const claudeSkills = allItems.filter((i) => i.type === "claude-skill").length;
62
+ const claudePlugins = allItems.filter((i) => i.type === "claude-plugin").length;
61
63
  const toolsWithConfig = toolResults.filter((r) => r.items.length > 0).length;
62
64
 
63
65
  return {
@@ -68,6 +70,8 @@ export async function discoverAllConfigs(
68
70
  rules,
69
71
  contextFiles,
70
72
  settings,
73
+ claudeSkills,
74
+ claudePlugins,
71
75
  totalItems: allItems.length,
72
76
  toolsScanned: TOOLS.length,
73
77
  toolsWithConfig,
@@ -23,7 +23,7 @@ export function formatDiscoveryForTool(result: DiscoveryResult): string {
23
23
  return lines.join("\n");
24
24
  }
25
25
 
26
- lines.push(`Found: ${summary.mcpServers} MCP server(s), ${summary.rules} rule(s), ${summary.contextFiles} context file(s), ${summary.settings} settings file(s)`);
26
+ lines.push(`Found: ${summary.mcpServers} MCP server(s), ${summary.rules} rule(s), ${summary.contextFiles} context file(s), ${summary.settings} settings file(s), ${summary.claudeSkills} Claude skill(s), ${summary.claudePlugins} Claude plugin(s)`);
27
27
  lines.push("");
28
28
 
29
29
  for (const toolResult of result.tools) {
@@ -70,9 +70,24 @@ export function formatDiscoveryForTool(result: DiscoveryResult): string {
70
70
  lines.push(` Settings (${byType.settings.length}):`);
71
71
  for (const item of byType.settings) {
72
72
  if (item.type !== "settings") continue;
73
- const keys = Object.keys(item.data).slice(0, 5);
74
- const suffix = Object.keys(item.data).length > 5 ? ` +${Object.keys(item.data).length - 5} more` : "";
75
- lines.push(` - ${item.source.path} (${item.source.level}): keys: ${keys.join(", ")}${suffix}`);
73
+ lines.push(` - ${item.source.path} (${item.source.level})`);
74
+ }
75
+ }
76
+
77
+ if (byType["claude-skill"]?.length) {
78
+ lines.push(` Claude Skills (${byType["claude-skill"].length}):`);
79
+ for (const item of byType["claude-skill"]) {
80
+ if (item.type !== "claude-skill") continue;
81
+ lines.push(` - ${item.name} (${item.source.level}) ${item.path}`);
82
+ }
83
+ }
84
+
85
+ if (byType["claude-plugin"]?.length) {
86
+ lines.push(` Claude Plugins (${byType["claude-plugin"].length}):`);
87
+ for (const item of byType["claude-plugin"]) {
88
+ if (item.type !== "claude-plugin") continue;
89
+ const label = item.packageName ? `${item.name} [${item.packageName}]` : item.name;
90
+ lines.push(` - ${label} (${item.source.level}) ${item.path}`);
76
91
  }
77
92
  }
78
93
 
@@ -111,6 +126,8 @@ export function formatDiscoveryForCommand(result: DiscoveryResult): string[] {
111
126
  lines.push(` Rules: ${summary.rules}`);
112
127
  lines.push(` Context: ${summary.contextFiles}`);
113
128
  lines.push(` Settings: ${summary.settings}`);
129
+ lines.push(` Claude skills: ${summary.claudeSkills}`);
130
+ lines.push(` Claude plugins: ${summary.claudePlugins}`);
114
131
  lines.push("");
115
132
 
116
133
  for (const toolResult of result.tools) {
@@ -122,6 +139,8 @@ export function formatDiscoveryForCommand(result: DiscoveryResult): string[] {
122
139
  if (counts.rule) parts.push(`${counts.rule} rules`);
123
140
  if (counts["context-file"]) parts.push(`${counts["context-file"]} context`);
124
141
  if (counts.settings) parts.push(`${counts.settings} settings`);
142
+ if (counts["claude-skill"]) parts.push(`${counts["claude-skill"]} Claude skills`);
143
+ if (counts["claude-plugin"]) parts.push(`${counts["claude-plugin"]} Claude plugins`);
125
144
 
126
145
  lines.push(` ${toolResult.tool.name}: ${parts.join(", ")}`);
127
146
 
@@ -131,6 +150,18 @@ export function formatDiscoveryForCommand(result: DiscoveryResult): string[] {
131
150
  if (server.type !== "mcp-server") continue;
132
151
  lines.push(` MCP: ${server.name} (${server.source.level})`);
133
152
  }
153
+
154
+ const claudeSkills = toolResult.items.filter((i) => i.type === "claude-skill");
155
+ for (const skill of claudeSkills) {
156
+ if (skill.type !== "claude-skill") continue;
157
+ lines.push(` Skill: ${skill.name} (${skill.source.level})`);
158
+ }
159
+
160
+ const claudePlugins = toolResult.items.filter((i) => i.type === "claude-plugin");
161
+ for (const plugin of claudePlugins) {
162
+ if (plugin.type !== "claude-plugin") continue;
163
+ lines.push(` Plugin: ${plugin.name} (${plugin.source.level})`);
164
+ }
134
165
  }
135
166
 
136
167
  if (result.warnings.length > 0) {