@asterxsk/kiln 0.1.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 (263) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +170 -0
  3. package/agent/AGENTS.md +67 -0
  4. package/agent/README.md +5 -0
  5. package/agent/extensions/AGENTS.md +68 -0
  6. package/agent/extensions/ask-user/index.ts +418 -0
  7. package/agent/extensions/ask-user/install.ps1 +23 -0
  8. package/agent/extensions/ask-user/install.sh +21 -0
  9. package/agent/extensions/ask-user/package-lock.json +769 -0
  10. package/agent/extensions/ask-user/package.json +19 -0
  11. package/agent/extensions/ask-user/prompt.ts +45 -0
  12. package/agent/extensions/ask-user/tsconfig.json +7 -0
  13. package/agent/extensions/background-terminals/docs/implementation-guide.md +942 -0
  14. package/agent/extensions/background-terminals/index.ts +627 -0
  15. package/agent/extensions/background-terminals/install.ps1 +23 -0
  16. package/agent/extensions/background-terminals/install.sh +21 -0
  17. package/agent/extensions/background-terminals/manager.test.ts +735 -0
  18. package/agent/extensions/background-terminals/output.test.ts +109 -0
  19. package/agent/extensions/background-terminals/package-lock.json +769 -0
  20. package/agent/extensions/background-terminals/package.json +17 -0
  21. package/agent/extensions/background-terminals/prompt.test.ts +125 -0
  22. package/agent/extensions/background-terminals/ps.test.ts +82 -0
  23. package/agent/extensions/background-terminals/result-delivery.test.ts +44 -0
  24. package/agent/extensions/background-terminals/src/domain.ts +87 -0
  25. package/agent/extensions/background-terminals/src/manager.ts +907 -0
  26. package/agent/extensions/background-terminals/src/output.ts +84 -0
  27. package/agent/extensions/background-terminals/src/prompt.ts +142 -0
  28. package/agent/extensions/background-terminals/src/result-delivery.ts +27 -0
  29. package/agent/extensions/background-terminals/src/runtime.ts +36 -0
  30. package/agent/extensions/background-terminals/src/ui/output-view.ts +79 -0
  31. package/agent/extensions/background-terminals/src/ui/ps.ts +621 -0
  32. package/agent/extensions/background-terminals/tsconfig.json +7 -0
  33. package/agent/extensions/destructive/README.md +31 -0
  34. package/agent/extensions/destructive/index.ts +88 -0
  35. package/agent/extensions/destructive/install.ps1 +23 -0
  36. package/agent/extensions/destructive/install.sh +21 -0
  37. package/agent/extensions/file-search/index.spec.ts +443 -0
  38. package/agent/extensions/file-search/index.ts +459 -0
  39. package/agent/extensions/file-search/install.ps1 +23 -0
  40. package/agent/extensions/file-search/install.sh +21 -0
  41. package/agent/extensions/file-search/package-lock.json +2253 -0
  42. package/agent/extensions/file-search/package.json +23 -0
  43. package/agent/extensions/file-search/src/args.ts +122 -0
  44. package/agent/extensions/file-search/src/binaries.ts +422 -0
  45. package/agent/extensions/file-search/src/output.ts +126 -0
  46. package/agent/extensions/file-search/src/process.ts +146 -0
  47. package/agent/extensions/file-search/src/prompt.ts +52 -0
  48. package/agent/extensions/file-search/tsconfig.json +7 -0
  49. package/agent/extensions/goal/README.md +50 -0
  50. package/agent/extensions/goal/index.ts +155 -0
  51. package/agent/extensions/goal/install.ps1 +23 -0
  52. package/agent/extensions/goal/install.sh +21 -0
  53. package/agent/extensions/modelconf/PLAN.md +915 -0
  54. package/agent/extensions/modelconf/README.md +66 -0
  55. package/agent/extensions/modelconf/index.ts +296 -0
  56. package/agent/extensions/modelconf/install.ps1 +23 -0
  57. package/agent/extensions/modelconf/install.sh +21 -0
  58. package/agent/extensions/modelconf/package-lock.json +1809 -0
  59. package/agent/extensions/modelconf/package.json +13 -0
  60. package/agent/extensions/modelconf/src/fuzzy.ts +26 -0
  61. package/agent/extensions/modelconf/src/glob.ts +13 -0
  62. package/agent/extensions/modelconf/src/persistence.test.ts +46 -0
  63. package/agent/extensions/modelconf/src/persistence.ts +164 -0
  64. package/agent/extensions/modelconf/src/ui/ModelConfView.test.ts +75 -0
  65. package/agent/extensions/modelconf/src/ui/ModelConfView.ts +1101 -0
  66. package/agent/extensions/modelconf/tsconfig.json +15 -0
  67. package/agent/extensions/pi-web-access/CHANGELOG.md +690 -0
  68. package/agent/extensions/pi-web-access/LICENSE +21 -0
  69. package/agent/extensions/pi-web-access/README.md +470 -0
  70. package/agent/extensions/pi-web-access/SECURITY.md +5 -0
  71. package/agent/extensions/pi-web-access/activity.ts +101 -0
  72. package/agent/extensions/pi-web-access/auth-fetch.ts +148 -0
  73. package/agent/extensions/pi-web-access/banner.png +0 -0
  74. package/agent/extensions/pi-web-access/brightdata-unlocker.ts +272 -0
  75. package/agent/extensions/pi-web-access/chrome-cookies.ts +669 -0
  76. package/agent/extensions/pi-web-access/content-find.ts +139 -0
  77. package/agent/extensions/pi-web-access/credential-source.ts +191 -0
  78. package/agent/extensions/pi-web-access/data-uri-sanitize.ts +406 -0
  79. package/agent/extensions/pi-web-access/datalab-pdf-extract.ts +568 -0
  80. package/agent/extensions/pi-web-access/declared-web-links.ts +173 -0
  81. package/agent/extensions/pi-web-access/evidence/CONTRACT-EVIDENCE.md +496 -0
  82. package/agent/extensions/pi-web-access/evidence/contract-probe.mjs +140 -0
  83. package/agent/extensions/pi-web-access/exa.ts +526 -0
  84. package/agent/extensions/pi-web-access/extract.ts +1196 -0
  85. package/agent/extensions/pi-web-access/feature-config.ts +29 -0
  86. package/agent/extensions/pi-web-access/fetch-params.ts +111 -0
  87. package/agent/extensions/pi-web-access/gemini-adc.ts +298 -0
  88. package/agent/extensions/pi-web-access/gemini-api.ts +353 -0
  89. package/agent/extensions/pi-web-access/gemini-pdf-extract.ts +108 -0
  90. package/agent/extensions/pi-web-access/gemini-search.ts +21 -0
  91. package/agent/extensions/pi-web-access/gemini-url-context.ts +128 -0
  92. package/agent/extensions/pi-web-access/gemini-web-config.ts +101 -0
  93. package/agent/extensions/pi-web-access/gemini-web.ts +487 -0
  94. package/agent/extensions/pi-web-access/github-api.ts +197 -0
  95. package/agent/extensions/pi-web-access/github-extract.ts +746 -0
  96. package/agent/extensions/pi-web-access/github-issue-pr.ts +700 -0
  97. package/agent/extensions/pi-web-access/index.ts +1737 -0
  98. package/agent/extensions/pi-web-access/package-lock.json +5808 -0
  99. package/agent/extensions/pi-web-access/package.json +64 -0
  100. package/agent/extensions/pi-web-access/page-query.ts +96 -0
  101. package/agent/extensions/pi-web-access/pdf-extract.ts +409 -0
  102. package/agent/extensions/pi-web-access/pi-web-fetch-demo.mp4 +0 -0
  103. package/agent/extensions/pi-web-access/promise-try.d.ts +7 -0
  104. package/agent/extensions/pi-web-access/query-rewrite.ts +51 -0
  105. package/agent/extensions/pi-web-access/render-search-error.ts +170 -0
  106. package/agent/extensions/pi-web-access/rsc-extract.ts +338 -0
  107. package/agent/extensions/pi-web-access/search-types.ts +20 -0
  108. package/agent/extensions/pi-web-access/source-check.ts +282 -0
  109. package/agent/extensions/pi-web-access/ssrf-protection.ts +526 -0
  110. package/agent/extensions/pi-web-access/storage.ts +521 -0
  111. package/agent/extensions/pi-web-access/summary-model-scope.ts +125 -0
  112. package/agent/extensions/pi-web-access/test/auth-fetch.test.mjs +208 -0
  113. package/agent/extensions/pi-web-access/test/brightdata-unlocker.test.mjs +840 -0
  114. package/agent/extensions/pi-web-access/test/chrome-cookie-extraction.test.mjs +441 -0
  115. package/agent/extensions/pi-web-access/test/config-path.test.mjs +283 -0
  116. package/agent/extensions/pi-web-access/test/content-find.test.mjs +25 -0
  117. package/agent/extensions/pi-web-access/test/credential-source.test.mjs +118 -0
  118. package/agent/extensions/pi-web-access/test/data-uri-sanitize.test.mjs +210 -0
  119. package/agent/extensions/pi-web-access/test/datalab-pdf-extract.test.mjs +552 -0
  120. package/agent/extensions/pi-web-access/test/declared-web-links.test.mjs +212 -0
  121. package/agent/extensions/pi-web-access/test/fetch-answer-storage.test.mjs +40 -0
  122. package/agent/extensions/pi-web-access/test/fetch-cache-storage.test.mjs +334 -0
  123. package/agent/extensions/pi-web-access/test/fetch-content-domain-policy.test.mjs +95 -0
  124. package/agent/extensions/pi-web-access/test/fetch-modes.test.mjs +53 -0
  125. package/agent/extensions/pi-web-access/test/fetch-not-found-guidance.test.mjs +92 -0
  126. package/agent/extensions/pi-web-access/test/fetch-params.test.mjs +86 -0
  127. package/agent/extensions/pi-web-access/test/fetch-render-call.test.mjs +34 -0
  128. package/agent/extensions/pi-web-access/test/fetch-routing.test.mjs +173 -0
  129. package/agent/extensions/pi-web-access/test/gemini-adc-auth.test.mjs +257 -0
  130. package/agent/extensions/pi-web-access/test/gemini-api-transport.test.mjs +170 -0
  131. package/agent/extensions/pi-web-access/test/gemini-pdf-extract.test.mjs +133 -0
  132. package/agent/extensions/pi-web-access/test/gemini-web-cookie-opt-in.test.mjs +178 -0
  133. package/agent/extensions/pi-web-access/test/gemini-web-header-overflow.test.mjs +148 -0
  134. package/agent/extensions/pi-web-access/test/get-search-content.test.mjs +223 -0
  135. package/agent/extensions/pi-web-access/test/github-extract.test.mjs +378 -0
  136. package/agent/extensions/pi-web-access/test/github-issue-pr.test.mjs +565 -0
  137. package/agent/extensions/pi-web-access/test/inline-content-config.test.mjs +99 -0
  138. package/agent/extensions/pi-web-access/test/lazy-extract-load.test.mjs +118 -0
  139. package/agent/extensions/pi-web-access/test/local-video-oversize.test.mjs +52 -0
  140. package/agent/extensions/pi-web-access/test/package-typebox-dependency.test.mjs +50 -0
  141. package/agent/extensions/pi-web-access/test/page-query.test.mjs +51 -0
  142. package/agent/extensions/pi-web-access/test/pdf-config.test.mjs +140 -0
  143. package/agent/extensions/pi-web-access/test/pdf-extract.test.mjs +500 -0
  144. package/agent/extensions/pi-web-access/test/proxy-transport.test.mjs +286 -0
  145. package/agent/extensions/pi-web-access/test/query-rewrite.test.mjs +52 -0
  146. package/agent/extensions/pi-web-access/test/rsc-fallback.test.mjs +102 -0
  147. package/agent/extensions/pi-web-access/test/search-error-render.test.mjs +152 -0
  148. package/agent/extensions/pi-web-access/test/search-providers.test.mjs +274 -0
  149. package/agent/extensions/pi-web-access/test/source-check.test.mjs +179 -0
  150. package/agent/extensions/pi-web-access/test/ssrf-allow-ranges-config.test.mjs +205 -0
  151. package/agent/extensions/pi-web-access/test/ssrf-protection.test.mjs +456 -0
  152. package/agent/extensions/pi-web-access/test/summary-model-scope.test.mjs +106 -0
  153. package/agent/extensions/pi-web-access/test/tool-registration-config.test.mjs +182 -0
  154. package/agent/extensions/pi-web-access/test/web-search-answer-render.test.mjs +66 -0
  155. package/agent/extensions/pi-web-access/test/youtube-extract-errors.test.mjs +64 -0
  156. package/agent/extensions/pi-web-access/tsconfig.json +11 -0
  157. package/agent/extensions/pi-web-access/utils.ts +451 -0
  158. package/agent/extensions/pi-web-access/video-extract.ts +392 -0
  159. package/agent/extensions/pi-web-access/youtube-extract.ts +328 -0
  160. package/agent/extensions/shared/activity-status.ts +31 -0
  161. package/agent/extensions/shared/child-session.test.ts +270 -0
  162. package/agent/extensions/shared/child-session.ts +148 -0
  163. package/agent/extensions/shared/context-utilization.test.ts +48 -0
  164. package/agent/extensions/shared/context-utilization.ts +47 -0
  165. package/agent/extensions/shared/dashboard-state.ts +99 -0
  166. package/agent/extensions/shared/install.ps1 +23 -0
  167. package/agent/extensions/shared/install.sh +21 -0
  168. package/agent/extensions/shared/tool-call-timeout.test.ts +117 -0
  169. package/agent/extensions/shared/tool-call-timeout.ts +104 -0
  170. package/agent/extensions/skillsconf/README.md +74 -0
  171. package/agent/extensions/skillsconf/index.ts +92 -0
  172. package/agent/extensions/skillsconf/install.ps1 +23 -0
  173. package/agent/extensions/skillsconf/install.sh +21 -0
  174. package/agent/extensions/skillsconf/package-lock.json +1809 -0
  175. package/agent/extensions/skillsconf/package.json +18 -0
  176. package/agent/extensions/skillsconf/src/delete-skill.test.ts +64 -0
  177. package/agent/extensions/skillsconf/src/delete-skill.ts +45 -0
  178. package/agent/extensions/skillsconf/src/filter.test.ts +80 -0
  179. package/agent/extensions/skillsconf/src/filter.ts +74 -0
  180. package/agent/extensions/skillsconf/src/fuzzy.ts +26 -0
  181. package/agent/extensions/skillsconf/src/persistence.test.ts +72 -0
  182. package/agent/extensions/skillsconf/src/persistence.ts +169 -0
  183. package/agent/extensions/skillsconf/src/ui/SkillConfView.test.ts +337 -0
  184. package/agent/extensions/skillsconf/src/ui/SkillConfView.ts +758 -0
  185. package/agent/extensions/skillsconf/src/ui/text-input.ts +91 -0
  186. package/agent/extensions/skillsconf/src/ui/tui-helpers.ts +144 -0
  187. package/agent/extensions/skillsconf/tsconfig.json +15 -0
  188. package/agent/extensions/status line/index.ts +282 -0
  189. package/agent/extensions/status line/install.ps1 +23 -0
  190. package/agent/extensions/status line/install.sh +21 -0
  191. package/agent/extensions/subagents/by-the-way.test.ts +29 -0
  192. package/agent/extensions/subagents/claude.test.ts +119 -0
  193. package/agent/extensions/subagents/codex.test.ts +102 -0
  194. package/agent/extensions/subagents/context-usage.test.ts +107 -0
  195. package/agent/extensions/subagents/docs/design-plan.md +568 -0
  196. package/agent/extensions/subagents/docs/effect-v4-extension-guide.md +354 -0
  197. package/agent/extensions/subagents/docs/effect-v4-notes.md +571 -0
  198. package/agent/extensions/subagents/index.ts +779 -0
  199. package/agent/extensions/subagents/install.ps1 +23 -0
  200. package/agent/extensions/subagents/install.sh +21 -0
  201. package/agent/extensions/subagents/manager.test.ts +276 -0
  202. package/agent/extensions/subagents/package-lock.json +2244 -0
  203. package/agent/extensions/subagents/package.json +19 -0
  204. package/agent/extensions/subagents/result-delivery.test.ts +27 -0
  205. package/agent/extensions/subagents/src/backend.ts +73 -0
  206. package/agent/extensions/subagents/src/backends/claude.ts +701 -0
  207. package/agent/extensions/subagents/src/backends/codex.ts +1060 -0
  208. package/agent/extensions/subagents/src/backends/pi.ts +575 -0
  209. package/agent/extensions/subagents/src/backends/stub.ts +300 -0
  210. package/agent/extensions/subagents/src/by-the-way.ts +21 -0
  211. package/agent/extensions/subagents/src/domain.ts +253 -0
  212. package/agent/extensions/subagents/src/format.ts +74 -0
  213. package/agent/extensions/subagents/src/manager.ts +736 -0
  214. package/agent/extensions/subagents/src/prompt.ts +92 -0
  215. package/agent/extensions/subagents/src/result-delivery.ts +20 -0
  216. package/agent/extensions/subagents/src/runtime.ts +53 -0
  217. package/agent/extensions/subagents/src/ui/takeover.ts +583 -0
  218. package/agent/extensions/subagents/src/ui/transcript.ts +201 -0
  219. package/agent/extensions/subagents/takeover.test.ts +29 -0
  220. package/agent/extensions/subagents/tsconfig.json +7 -0
  221. package/agent/extensions/taste/index.ts +443 -0
  222. package/agent/extensions/taste/install.ps1 +23 -0
  223. package/agent/extensions/taste/install.sh +21 -0
  224. package/agent/extensions/todo/AGENTS.md +38 -0
  225. package/agent/extensions/todo/LICENSE +21 -0
  226. package/agent/extensions/todo/config.ts +55 -0
  227. package/agent/extensions/todo/index.ts +151 -0
  228. package/agent/extensions/todo/install.ps1 +23 -0
  229. package/agent/extensions/todo/install.sh +21 -0
  230. package/agent/extensions/todo/locales/de.json +17 -0
  231. package/agent/extensions/todo/locales/en.json +15 -0
  232. package/agent/extensions/todo/locales/es.json +17 -0
  233. package/agent/extensions/todo/locales/fr.json +17 -0
  234. package/agent/extensions/todo/locales/pt-BR.json +17 -0
  235. package/agent/extensions/todo/locales/pt.json +17 -0
  236. package/agent/extensions/todo/locales/ru.json +17 -0
  237. package/agent/extensions/todo/locales/uk.json +17 -0
  238. package/agent/extensions/todo/locales/zh.json +17 -0
  239. package/agent/extensions/todo/package-lock.json +3358 -0
  240. package/agent/extensions/todo/package.json +67 -0
  241. package/agent/extensions/todo/state/i18n-bridge.ts +64 -0
  242. package/agent/extensions/todo/state/invariants.ts +20 -0
  243. package/agent/extensions/todo/state/replay.ts +38 -0
  244. package/agent/extensions/todo/state/selectors.ts +107 -0
  245. package/agent/extensions/todo/state/state-reducer.ts +326 -0
  246. package/agent/extensions/todo/state/state.ts +18 -0
  247. package/agent/extensions/todo/state/store.ts +82 -0
  248. package/agent/extensions/todo/state/task-graph.ts +57 -0
  249. package/agent/extensions/todo/todo-overlay.ts +200 -0
  250. package/agent/extensions/todo/todo.ts +155 -0
  251. package/agent/extensions/todo/tool/response-envelope.ts +109 -0
  252. package/agent/extensions/todo/tool/types.ts +206 -0
  253. package/agent/extensions/todo/verify-ref-system.js +0 -0
  254. package/agent/extensions/todo/view/format.ts +177 -0
  255. package/agent/extensions/trim-context/README.md +54 -0
  256. package/agent/extensions/trim-context/index.ts +487 -0
  257. package/agent/extensions/trim-context/install.ps1 +23 -0
  258. package/agent/extensions/trim-context/install.sh +21 -0
  259. package/agent/install.ps1 +527 -0
  260. package/agent/install.sh +511 -0
  261. package/agent/keybindings.json +7 -0
  262. package/bin/kiln.js +359 -0
  263. package/package.json +21 -0
@@ -0,0 +1,88 @@
1
+ /**
2
+ * destructive - Asks before running destructive (deletion) commands.
3
+ *
4
+ * Intercepts `bash` / `powershell` tool calls and prompts:
5
+ *
6
+ * Do you want to allow pi to run:
7
+ * {full-command}
8
+ *
9
+ * 1. Allow
10
+ * 2. Deny
11
+ *
12
+ * Anything else (including Deny, or dismissing with Esc) blocks the command.
13
+ * In non-interactive mode (no UI) deletion commands are blocked by default.
14
+ */
15
+
16
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
17
+
18
+ const FILE_DELETERS = new Set([
19
+ "rm",
20
+ "rmdir",
21
+ "unlink",
22
+ "shred",
23
+ "del",
24
+ "erase",
25
+ "rd",
26
+ "remove-item",
27
+ "ri",
28
+ ]);
29
+
30
+ function isDestructiveGit(rest: string): boolean {
31
+ const args = rest.trim();
32
+ if (/^rm\b/.test(args)) return true;
33
+ if (/^branch\b.*(^|\s)(-[dD]|--delete)\b/.test(args)) return true;
34
+ if (/^tag\b.*(^|\s)(-d|--delete)\b/.test(args)) return true;
35
+ if (/^push\b.*(^|\s)(-[dD]|--delete)\b/.test(args)) return true;
36
+ if (/^push\b.*(^|\s)\S+:\S*(\s|$)/.test(args)) return true; // `git push origin :branch`
37
+ if (/^stash\b\s+(drop|clear)\b/.test(args)) return true;
38
+ if (/^clean\b/.test(args) && /(^|\s)-[a-zA-Z]*f/.test(args)) return true;
39
+ if (/^worktree\b\s+remove\b/.test(args)) return true;
40
+ if (/^notes\b.*\b(remove|prune)\b/.test(args)) return true;
41
+ return false;
42
+ }
43
+
44
+ function isDestructiveSegment(segment: string): boolean {
45
+ let s = segment.trim().replace(/^[({]+|[)}]+$/g, "").trim();
46
+ if (!s) return false;
47
+ s = s.replace(/^(sudo|doas)\s+/i, "");
48
+ const m = s.match(/^([^\s]+)\s*(.*)$/s);
49
+ if (!m) return false;
50
+ const bin = m[1].split("/").pop()!.toLowerCase();
51
+ const rest = m[2] ?? "";
52
+ if (FILE_DELETERS.has(bin)) return true;
53
+ if (bin === "git") return isDestructiveGit(rest);
54
+ return false;
55
+ }
56
+
57
+ export function isDestructiveCommand(command: string): boolean {
58
+ return command.split(/&&|\|\||;|\||\n/).some(isDestructiveSegment);
59
+ }
60
+
61
+ export default function (pi: ExtensionAPI) {
62
+ pi.on("tool_call", async (event, ctx) => {
63
+ if (event.toolName !== "bash" && event.toolName !== "powershell") {
64
+ return undefined;
65
+ }
66
+
67
+ const command = event.input.command as string | undefined;
68
+ if (!command || !isDestructiveCommand(command)) return undefined;
69
+
70
+ if (!ctx.hasUI) {
71
+ return {
72
+ block: true,
73
+ reason: `Blocked deletion command (no UI to confirm): ${command}`,
74
+ };
75
+ }
76
+
77
+ const choice = await ctx.ui.select(
78
+ `Do you want to allow pi to run:\n${command}`,
79
+ ["1. Allow", "2. Deny"],
80
+ );
81
+
82
+ if (choice !== "1. Allow") {
83
+ return { block: true, reason: `User denied deletion command: ${command}` };
84
+ }
85
+
86
+ return undefined;
87
+ });
88
+ }
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env pwsh
2
+ # Per-extension installer — Windows / PowerShell
3
+ # Installs npm dependencies for this extension. No-op if no package.json.
4
+ $ErrorActionPreference = "Stop"
5
+ $extDir = $PSScriptRoot
6
+ if (-not $extDir) { $extDir = Split-Path -Parent $MyInvocation.MyCommand.Path }
7
+ if (-not (Test-Path "$extDir/package.json")) {
8
+ Write-Host " - no package.json — skipping" -ForegroundColor DarkGray
9
+ exit 0
10
+ }
11
+ if (-not (Get-Command npm -ErrorAction SilentlyContinue)) {
12
+ Write-Host " x npm not found — install Node.js 22+" -ForegroundColor Red
13
+ exit 1
14
+ }
15
+ if (Test-Path "$extDir/package-lock.json") {
16
+ Write-Host " - npm ci" -ForegroundColor DarkGray
17
+ npm --prefix $extDir ci --no-audit --no-fund
18
+ } else {
19
+ Write-Host " - npm install" -ForegroundColor DarkGray
20
+ npm --prefix $extDir install --no-audit --no-fund
21
+ }
22
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
23
+ Write-Host " + done" -ForegroundColor Green
@@ -0,0 +1,21 @@
1
+ #!/bin/sh
2
+ # Per-extension installer — macOS / Linux
3
+ # Installs npm dependencies for this extension. No-op if no package.json.
4
+ set -eu
5
+ EXT_DIR="$(cd "$(dirname "$0")" && pwd)"
6
+ if [ ! -f "$EXT_DIR/package.json" ]; then
7
+ echo " · no package.json — skipping"
8
+ exit 0
9
+ fi
10
+ if ! command -v npm >/dev/null 2>&1; then
11
+ echo " ✖ npm not found — install Node.js 22+" >&2
12
+ exit 1
13
+ fi
14
+ if [ -f "$EXT_DIR/package-lock.json" ]; then
15
+ echo " · npm ci"
16
+ npm --prefix "$EXT_DIR" ci --no-audit --no-fund
17
+ else
18
+ echo " · npm install"
19
+ npm --prefix "$EXT_DIR" install --no-audit --no-fund
20
+ fi
21
+ echo " ✓ done"
@@ -0,0 +1,443 @@
1
+ import * as NodeServices from "@effect/platform-node/NodeServices";
2
+ import { assert, it } from "@effect/vitest";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { Effect, FileSystem } from "effect";
6
+ import { HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
7
+ import {
8
+ buildFdArgs,
9
+ buildRgArgs,
10
+ FD_DEFAULT_LIMIT,
11
+ normalizeSearchPath,
12
+ } from "./src/args.ts";
13
+ import {
14
+ FD_INTEL_DARWIN_VERSION,
15
+ InstallError,
16
+ readBoundedResponse,
17
+ releaseAsset,
18
+ resolveBinary,
19
+ TOOL_SPECS,
20
+ UnsupportedPlatformError,
21
+ type BinaryEnv,
22
+ type ReleaseAsset,
23
+ type ResolvedBinary,
24
+ } from "./src/binaries.ts";
25
+ import { formatCapturedOutput, formatOutput } from "./src/output.ts";
26
+ import { executeSearchProcess } from "./src/process.ts";
27
+ import { installNotifications, makeBinaryInitializers } from "./index.ts";
28
+
29
+ // --- argument construction -------------------------------------------------
30
+
31
+ it("fd args: defaults list everything with the default limit", () => {
32
+ assert.deepEqual(buildFdArgs({}), [
33
+ "--color=never",
34
+ "--max-results",
35
+ String(FD_DEFAULT_LIMIT),
36
+ "--",
37
+ "",
38
+ ]);
39
+ });
40
+
41
+ it("fd args: all options are translated and pattern stays behind --", () => {
42
+ const args = buildFdArgs({
43
+ pattern: "-rf",
44
+ path: "@src",
45
+ type: "file",
46
+ extension: ".ts",
47
+ glob: true,
48
+ hidden: true,
49
+ max_depth: 3,
50
+ limit: 50,
51
+ });
52
+ assert.deepEqual(args, [
53
+ "--color=never",
54
+ "--hidden",
55
+ "--glob",
56
+ "--type",
57
+ "f",
58
+ "--extension",
59
+ "ts",
60
+ "--max-depth",
61
+ "3",
62
+ "--max-results",
63
+ "50",
64
+ "--",
65
+ "-rf",
66
+ "src",
67
+ ]);
68
+ });
69
+
70
+ it("fd args: out-of-range values are clamped", () => {
71
+ const args = buildFdArgs({ max_depth: 500, limit: 1_000_000 });
72
+ assert.deepEqual(args, [
73
+ "--color=never",
74
+ "--max-depth",
75
+ "64",
76
+ "--max-results",
77
+ "10000",
78
+ "--",
79
+ "",
80
+ ]);
81
+ });
82
+
83
+ it("rg args: defaults use smart-case and safe separators", () => {
84
+ assert.deepEqual(buildRgArgs({ pattern: "--help" }), [
85
+ "--line-number",
86
+ "--color=never",
87
+ "--no-heading",
88
+ "--with-filename",
89
+ "--smart-case",
90
+ "--max-count",
91
+ "100",
92
+ "--",
93
+ "--help",
94
+ ]);
95
+ });
96
+
97
+ it("rg args: all options are translated", () => {
98
+ const args = buildRgArgs({
99
+ pattern: "TODO",
100
+ path: "@lib",
101
+ glob: "*.ts",
102
+ file_type: "ts",
103
+ case_sensitive: true,
104
+ fixed_strings: true,
105
+ hidden: true,
106
+ context: 2,
107
+ limit: 10,
108
+ });
109
+ assert.deepEqual(args, [
110
+ "--line-number",
111
+ "--color=never",
112
+ "--no-heading",
113
+ "--with-filename",
114
+ "--case-sensitive",
115
+ "--fixed-strings",
116
+ "--hidden",
117
+ "--context",
118
+ "2",
119
+ "--glob",
120
+ "*.ts",
121
+ "--type",
122
+ "ts",
123
+ "--max-count",
124
+ "10",
125
+ "--",
126
+ "TODO",
127
+ "lib",
128
+ ]);
129
+ });
130
+
131
+ it("rg args: case_sensitive false forces ignore-case", () => {
132
+ const args = buildRgArgs({ pattern: "x", case_sensitive: false });
133
+ assert.isTrue(args.includes("--ignore-case"));
134
+ assert.isFalse(args.includes("--smart-case"));
135
+ });
136
+
137
+ it("path normalization strips leading @ and expands ~", () => {
138
+ assert.equal(normalizeSearchPath("@src/lib"), "src/lib");
139
+ assert.equal(normalizeSearchPath("~"), homedir());
140
+ assert.equal(normalizeSearchPath("~/projects"), join(homedir(), "projects"));
141
+ assert.equal(normalizeSearchPath(" plain "), "plain");
142
+ });
143
+
144
+ // --- binary resolution -----------------------------------------------------
145
+
146
+ function makeEnv(options: {
147
+ available?: string[];
148
+ installShouldFail?: boolean;
149
+ }): BinaryEnv & { installs: ReleaseAsset[]; probes: string[] } {
150
+ const installs: ReleaseAsset[] = [];
151
+ const probes: string[] = [];
152
+ const installed = new Set<string>();
153
+ return {
154
+ installs,
155
+ probes,
156
+ probe: (command) =>
157
+ Effect.sync(() => {
158
+ probes.push(command);
159
+ return (
160
+ (options.available ?? []).includes(command) || installed.has(command)
161
+ );
162
+ }),
163
+ install: (asset, destination) => {
164
+ if (options.installShouldFail) {
165
+ return Effect.fail(new InstallError({ message: "network down" }));
166
+ }
167
+ return Effect.sync(() => {
168
+ installs.push(asset);
169
+ installed.add(destination);
170
+ });
171
+ },
172
+ };
173
+ }
174
+
175
+ const darwinArm = { os: "darwin", arch: "arm64" } as const;
176
+
177
+ it.effect("binary resolution: system fd wins and nothing is installed", () =>
178
+ Effect.gen(function* () {
179
+ const env = makeEnv({ available: ["fd"] });
180
+ const resolved = yield* resolveBinary(
181
+ TOOL_SPECS.fd,
182
+ "/repo/bin",
183
+ darwinArm,
184
+ env,
185
+ );
186
+
187
+ assert.deepEqual(resolved, {
188
+ tool: "fd",
189
+ command: "fd",
190
+ source: "system",
191
+ });
192
+ assert.equal(env.installs.length, 0);
193
+ }),
194
+ );
195
+
196
+ it.effect("binary resolution: fdfind is accepted as a system fd", () =>
197
+ Effect.gen(function* () {
198
+ const env = makeEnv({ available: ["fdfind"] });
199
+ const resolved = yield* resolveBinary(
200
+ TOOL_SPECS.fd,
201
+ "/repo/bin",
202
+ darwinArm,
203
+ env,
204
+ );
205
+
206
+ assert.deepEqual(resolved, {
207
+ tool: "fd",
208
+ command: "fdfind",
209
+ source: "system",
210
+ });
211
+ assert.equal(env.installs.length, 0);
212
+ }),
213
+ );
214
+
215
+ it.effect("binary resolution: existing bin fallback is used silently", () =>
216
+ Effect.gen(function* () {
217
+ const env = makeEnv({ available: ["/repo/bin/rg"] });
218
+ const resolved = yield* resolveBinary(
219
+ TOOL_SPECS.rg,
220
+ "/repo/bin",
221
+ darwinArm,
222
+ env,
223
+ );
224
+
225
+ assert.deepEqual(resolved, {
226
+ tool: "rg",
227
+ command: "/repo/bin/rg",
228
+ source: "bundled",
229
+ });
230
+ assert.equal(env.installs.length, 0);
231
+ }),
232
+ );
233
+
234
+ it.effect(
235
+ "binary resolution: missing everywhere triggers exactly one install",
236
+ () =>
237
+ Effect.gen(function* () {
238
+ const env = makeEnv({ available: [] });
239
+ const resolved = yield* resolveBinary(
240
+ TOOL_SPECS.rg,
241
+ "/repo/bin",
242
+ darwinArm,
243
+ env,
244
+ );
245
+
246
+ assert.equal(resolved.source, "installed");
247
+ assert.equal(resolved.command, "/repo/bin/rg");
248
+ assert.equal(env.installs.length, 1);
249
+ assert.match(
250
+ env.installs[0].url,
251
+ /^https:\/\/github\.com\/BurntSushi\/ripgrep\//,
252
+ );
253
+ }),
254
+ );
255
+
256
+ it.effect("binary resolution: install failure surfaces a typed error", () =>
257
+ Effect.gen(function* () {
258
+ const env = makeEnv({ available: [], installShouldFail: true });
259
+ const error = yield* Effect.flip(
260
+ resolveBinary(TOOL_SPECS.fd, "/repo/bin", darwinArm, env),
261
+ );
262
+
263
+ assert.instanceOf(error, InstallError);
264
+ assert.equal(error.message, "network down");
265
+ }),
266
+ );
267
+
268
+ it.effect(
269
+ "binary resolution: unsupported platform fails without installing",
270
+ () =>
271
+ Effect.gen(function* () {
272
+ const env = makeEnv({ available: [] });
273
+ const error = yield* Effect.flip(
274
+ resolveBinary(
275
+ TOOL_SPECS.fd,
276
+ "/repo/bin",
277
+ { os: "linux", arch: "s390x" },
278
+ env,
279
+ ),
280
+ );
281
+
282
+ assert.instanceOf(error, UnsupportedPlatformError);
283
+ assert.equal(env.installs.length, 0);
284
+ }),
285
+ );
286
+
287
+ it.effect("binary resolution: one failed tool does not disable the other", () =>
288
+ Effect.gen(function* () {
289
+ const env = makeEnv({
290
+ available: ["rg"],
291
+ installShouldFail: true,
292
+ });
293
+ const initializers = makeBinaryInitializers("/repo/bin", darwinArm, env);
294
+
295
+ const fdError = yield* Effect.flip(initializers.fd);
296
+ const rg = yield* initializers.rg;
297
+
298
+ assert.instanceOf(fdError, InstallError);
299
+ assert.deepEqual(rg, { tool: "rg", command: "rg", source: "system" });
300
+ }),
301
+ );
302
+
303
+ it("release assets cover macOS and Linux on arm64 and x64 over HTTPS", () => {
304
+ for (const os of ["darwin", "linux"] as const) {
305
+ for (const arch of ["arm64", "x64"] as const) {
306
+ for (const tool of ["fd", "rg"] as const) {
307
+ const asset = releaseAsset(tool, { os, arch });
308
+ assert.isDefined(asset, `${tool} ${os}/${arch}`);
309
+ assert.match(asset.url, /^https:\/\//);
310
+ assert.isTrue(asset.url.endsWith(asset.fileName));
311
+ assert.match(asset.sha256, /^[a-f0-9]{64}$/);
312
+ }
313
+ }
314
+ }
315
+ });
316
+
317
+ it("linux assets use statically linked musl builds", () => {
318
+ const asset = releaseAsset("fd", { os: "linux", arch: "x64" });
319
+ assert.isTrue(asset?.url.includes("unknown-linux-musl"));
320
+ });
321
+
322
+ it("Intel macOS uses the latest fd release that publishes that target", () => {
323
+ const asset = releaseAsset("fd", { os: "darwin", arch: "x64" });
324
+ assert.equal(asset?.version, FD_INTEL_DARWIN_VERSION);
325
+ });
326
+
327
+ it.effect(
328
+ "bounded downloads reject oversized declared and streamed bodies",
329
+ () =>
330
+ Effect.gen(function* () {
331
+ const request = HttpClientRequest.get(
332
+ "https://example.com/archive.tar.gz",
333
+ );
334
+ const declared = HttpClientResponse.fromWeb(
335
+ request,
336
+ new Response("small", {
337
+ headers: { "content-length": "100" },
338
+ }),
339
+ );
340
+ const declaredError = yield* Effect.flip(
341
+ readBoundedResponse(declared, 10),
342
+ );
343
+ assert.match(declaredError.message, /size limit/);
344
+
345
+ const streamed = HttpClientResponse.fromWeb(
346
+ request,
347
+ new Response("this body is too large"),
348
+ );
349
+ const streamedError = yield* Effect.flip(
350
+ readBoundedResponse(streamed, 5),
351
+ );
352
+ assert.match(streamedError.message, /size limit/);
353
+ }),
354
+ );
355
+
356
+ // --- notification policy ----------------------------------------------------
357
+
358
+ it("notifications: only fresh installs notify", () => {
359
+ const system: ResolvedBinary = {
360
+ tool: "fd",
361
+ command: "fd",
362
+ source: "system",
363
+ };
364
+ const bundled: ResolvedBinary = {
365
+ tool: "rg",
366
+ command: "/repo/bin/rg",
367
+ source: "bundled",
368
+ };
369
+ const installed: ResolvedBinary = {
370
+ tool: "rg",
371
+ command: "/repo/bin/rg",
372
+ source: "installed",
373
+ version: "15.2.0",
374
+ };
375
+
376
+ assert.deepEqual(installNotifications([system, bundled]), []);
377
+ const messages = installNotifications([system, installed]);
378
+ assert.equal(messages.length, 1);
379
+ assert.match(messages[0], /downloaded rg 15\.2\.0/);
380
+ });
381
+
382
+ // --- output truncation -------------------------------------------------------
383
+
384
+ it.effect("process output is streamed to a complete spill file", () =>
385
+ Effect.gen(function* () {
386
+ const result = yield* executeSearchProcess({
387
+ command: process.execPath,
388
+ args: ["-e", 'process.stdout.write("line\\n".repeat(3000))'],
389
+ cwd: process.cwd(),
390
+ tempPrefix: "pi-search-test-",
391
+ });
392
+ const formatted = formatCapturedOutput(result.output);
393
+
394
+ assert.equal(result.code, 0);
395
+ assert.isTrue(formatted.truncated);
396
+ assert.equal(formatted.lineCount, 3000);
397
+ assert.match(formatted.text, /2000 of 3000 lines/);
398
+ assert.isDefined(formatted.fullOutputPath);
399
+
400
+ const fs = yield* FileSystem.FileSystem;
401
+ const fullOutput = yield* fs.readFileString(formatted.fullOutputPath);
402
+ assert.equal(fullOutput, "line\n".repeat(3000));
403
+ yield* fs.remove(dirname(formatted.fullOutputPath), {
404
+ recursive: true,
405
+ force: true,
406
+ });
407
+ }).pipe(Effect.provide(NodeServices.layer)),
408
+ );
409
+
410
+ it("output: small results pass through untouched", async () => {
411
+ const formatted = await formatOutput("a.ts\nb.ts\n", {
412
+ tempPrefix: "pi-fd-",
413
+ persistFullOutput: () => Promise.reject(new Error("should not persist")),
414
+ });
415
+ assert.equal(formatted.text, "a.ts\nb.ts");
416
+ assert.equal(formatted.lineCount, 2);
417
+ assert.isFalse(formatted.truncated);
418
+ assert.isUndefined(formatted.fullOutputPath);
419
+ });
420
+
421
+ it("output: oversized results are truncated and persisted", async () => {
422
+ const bigOutput = Array.from({ length: 3000 }, (_, i) => `file-${i}.ts`).join(
423
+ "\n",
424
+ );
425
+ let persisted: string | undefined;
426
+ const formatted = await formatOutput(bigOutput, {
427
+ tempPrefix: "pi-fd-",
428
+ persistFullOutput: async (full) => {
429
+ persisted = full;
430
+ return "/tmp/fake/output.txt";
431
+ },
432
+ });
433
+ assert.isTrue(formatted.truncated);
434
+ assert.equal(formatted.fullOutputPath, "/tmp/fake/output.txt");
435
+ assert.equal(persisted, bigOutput);
436
+ assert.match(formatted.text, /\[Output truncated: 2000 of 3000 lines/);
437
+ assert.match(
438
+ formatted.text,
439
+ /Full output saved to: \/tmp\/fake\/output\.txt\]/,
440
+ );
441
+ const shownLines = formatted.text.split("\n");
442
+ assert.equal(shownLines[0], "file-0.ts");
443
+ });