pim-agent 0.0.1 → 0.9.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 (261) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +293 -7
  3. package/bin/pim.ts +151 -0
  4. package/package.json +82 -4
  5. package/packages/core/src/attachments/AttachmentStore.ts +99 -0
  6. package/packages/core/src/attachments/Attachments.ts +69 -0
  7. package/packages/core/src/extensions/CoreExtensions.ts +51 -0
  8. package/packages/core/src/extensions/apply-patch/coordinator.ts +53 -0
  9. package/packages/core/src/extensions/apply-patch/executor.ts +550 -0
  10. package/packages/core/src/extensions/apply-patch/index.ts +69 -0
  11. package/packages/core/src/extensions/apply-patch/matcher.ts +47 -0
  12. package/packages/core/src/extensions/apply-patch/model.ts +39 -0
  13. package/packages/core/src/extensions/apply-patch/parser.ts +379 -0
  14. package/packages/core/src/extensions/apply-patch/render.ts +216 -0
  15. package/packages/core/src/extensions/apply-patch/schema.ts +37 -0
  16. package/packages/core/src/extensions/apply-patch/types.ts +30 -0
  17. package/packages/core/src/extensions/bash/capture.ts +86 -0
  18. package/packages/core/src/extensions/bash/format.ts +110 -0
  19. package/packages/core/src/extensions/bash/image.ts +31 -0
  20. package/packages/core/src/extensions/bash/index.ts +88 -0
  21. package/packages/core/src/extensions/bash/render.ts +31 -0
  22. package/packages/core/src/extensions/bash/run.ts +221 -0
  23. package/packages/core/src/extensions/bash/schema.ts +64 -0
  24. package/packages/core/src/extensions/edit/edit.ts +366 -0
  25. package/packages/core/src/extensions/edit/index.ts +36 -0
  26. package/packages/core/src/extensions/edit/render.ts +16 -0
  27. package/packages/core/src/extensions/edit/schema.ts +37 -0
  28. package/packages/core/src/extensions/glob/glob.ts +34 -0
  29. package/packages/core/src/extensions/glob/index.ts +83 -0
  30. package/packages/core/src/extensions/glob/render.ts +72 -0
  31. package/packages/core/src/extensions/glob/schema.ts +66 -0
  32. package/packages/core/src/extensions/grep/grep.ts +256 -0
  33. package/packages/core/src/extensions/grep/index.ts +114 -0
  34. package/packages/core/src/extensions/grep/render.ts +271 -0
  35. package/packages/core/src/extensions/grep/schema.ts +92 -0
  36. package/packages/core/src/extensions/read/ImageMemory.ts +42 -0
  37. package/packages/core/src/extensions/read/image.ts +57 -0
  38. package/packages/core/src/extensions/read/index.ts +88 -0
  39. package/packages/core/src/extensions/read/read.ts +191 -0
  40. package/packages/core/src/extensions/read/render.ts +58 -0
  41. package/packages/core/src/extensions/read/schema.ts +49 -0
  42. package/packages/core/src/extensions/subagent/index.ts +33 -0
  43. package/packages/core/src/extensions/subagent/render.ts +127 -0
  44. package/packages/core/src/extensions/subagent/schema.ts +9 -0
  45. package/packages/core/src/extensions/subagent/subagent.ts +419 -0
  46. package/packages/core/src/extensions/system-prompt/index.ts +28 -0
  47. package/packages/core/src/extensions/system-prompt/prompt.ts +228 -0
  48. package/packages/core/src/extensions/todo/index.ts +123 -0
  49. package/packages/core/src/extensions/todo/render.ts +156 -0
  50. package/packages/core/src/extensions/todo/schema.ts +24 -0
  51. package/packages/core/src/extensions/todo/todo.ts +185 -0
  52. package/packages/core/src/extensions/web-fetch/JinaReaderClient.ts +187 -0
  53. package/packages/core/src/extensions/web-fetch/WebViewFetchClient.ts +183 -0
  54. package/packages/core/src/extensions/web-fetch/WebViewMarkdownSnapshot.ts +512 -0
  55. package/packages/core/src/extensions/web-fetch/fetch.ts +265 -0
  56. package/packages/core/src/extensions/web-fetch/image.ts +165 -0
  57. package/packages/core/src/extensions/web-fetch/index.ts +83 -0
  58. package/packages/core/src/extensions/web-fetch/render.ts +100 -0
  59. package/packages/core/src/extensions/web-fetch/schema.ts +46 -0
  60. package/packages/core/src/extensions/web-search/ExaMcpClient.ts +251 -0
  61. package/packages/core/src/extensions/web-search/SearchBreaker.ts +147 -0
  62. package/packages/core/src/extensions/web-search/SearchChain.ts +77 -0
  63. package/packages/core/src/extensions/web-search/index.ts +90 -0
  64. package/packages/core/src/extensions/web-search/providers/DuckDuckGoProvider.ts +195 -0
  65. package/packages/core/src/extensions/web-search/providers/ExaProvider.ts +54 -0
  66. package/packages/core/src/extensions/web-search/providers/FirecrawlProvider.ts +144 -0
  67. package/packages/core/src/extensions/web-search/providers/SearchProvider.ts +118 -0
  68. package/packages/core/src/extensions/web-search/render.ts +49 -0
  69. package/packages/core/src/extensions/web-search/schema.ts +29 -0
  70. package/packages/core/src/extensions/web-search/search.ts +23 -0
  71. package/packages/core/src/extensions/write/index.ts +58 -0
  72. package/packages/core/src/extensions/write/render.ts +16 -0
  73. package/packages/core/src/extensions/write/schema.ts +14 -0
  74. package/packages/core/src/extensions/write/write.ts +87 -0
  75. package/packages/core/src/picker/FilePickerSuggestionEngine.ts +14 -0
  76. package/packages/core/src/picker/InProcessFilePickerSuggestionEngine.ts +52 -0
  77. package/packages/core/src/picker/PickerItem.ts +6 -0
  78. package/packages/core/src/picker/PickerService.ts +110 -0
  79. package/packages/core/src/picker/RemoteFilePickerSuggestionEngine.ts +56 -0
  80. package/packages/core/src/picker/WorkerFilePickerSuggestionEngine.ts +268 -0
  81. package/packages/core/src/picker/catalog.ts +220 -0
  82. package/packages/core/src/picker/commandRanker.ts +17 -0
  83. package/packages/core/src/picker/filePickerWorker.ts +66 -0
  84. package/packages/core/src/picker/filePickerWorkerMessages.ts +39 -0
  85. package/packages/core/src/picker/ranker.ts +245 -0
  86. package/packages/core/src/picker/token.ts +70 -0
  87. package/packages/core/src/session/AgentRuntime.ts +85 -0
  88. package/packages/core/src/session/EventLog.ts +213 -0
  89. package/packages/core/src/session/MessageText.ts +21 -0
  90. package/packages/core/src/session/ReadCursors.ts +94 -0
  91. package/packages/core/src/session/SessionCache.ts +68 -0
  92. package/packages/core/src/session/SessionHost.ts +876 -0
  93. package/packages/core/src/session/SessionLease.ts +279 -0
  94. package/packages/core/src/session/SessionRegistry.ts +169 -0
  95. package/packages/core/src/session/WriteMark.ts +37 -0
  96. package/packages/core/src/shared/Cli.ts +25 -0
  97. package/packages/core/src/shared/DaemonUnit.ts +13 -0
  98. package/packages/core/src/shared/DiffLines.ts +304 -0
  99. package/packages/core/src/shared/DiffRenderer.ts +283 -0
  100. package/packages/core/src/shared/DiffView.ts +29 -0
  101. package/packages/core/src/shared/Directories.ts +61 -0
  102. package/packages/core/src/shared/EditMatcher.ts +767 -0
  103. package/packages/core/src/shared/Errors.ts +9 -0
  104. package/packages/core/src/shared/ExtensionToggles.ts +130 -0
  105. package/packages/core/src/shared/FileEnumerator.ts +361 -0
  106. package/packages/core/src/shared/FileScanner.ts +57 -0
  107. package/packages/core/src/shared/FileWatch.ts +107 -0
  108. package/packages/core/src/shared/Format.ts +74 -0
  109. package/packages/core/src/shared/Fs.ts +91 -0
  110. package/packages/core/src/shared/FsErrors.ts +67 -0
  111. package/packages/core/src/shared/FuzzyMatcher.ts +71 -0
  112. package/packages/core/src/shared/Git.ts +89 -0
  113. package/packages/core/src/shared/GlobExclusions.ts +13 -0
  114. package/packages/core/src/shared/Http.ts +14 -0
  115. package/packages/core/src/shared/ImageMime.ts +36 -0
  116. package/packages/core/src/shared/Images.ts +357 -0
  117. package/packages/core/src/shared/Json.ts +19 -0
  118. package/packages/core/src/shared/Languages.ts +95 -0
  119. package/packages/core/src/shared/Levenshtein.ts +30 -0
  120. package/packages/core/src/shared/Lines.ts +79 -0
  121. package/packages/core/src/shared/McpClient.ts +444 -0
  122. package/packages/core/src/shared/OutputBudget.ts +84 -0
  123. package/packages/core/src/shared/PatchSummary.ts +40 -0
  124. package/packages/core/src/shared/Paths.ts +80 -0
  125. package/packages/core/src/shared/PimSettings.ts +128 -0
  126. package/packages/core/src/shared/PimVersion.ts +93 -0
  127. package/packages/core/src/shared/Pool.ts +26 -0
  128. package/packages/core/src/shared/Proc.ts +30 -0
  129. package/packages/core/src/shared/RateLimiter.ts +50 -0
  130. package/packages/core/src/shared/Renderer.ts +371 -0
  131. package/packages/core/src/shared/SafePath.ts +17 -0
  132. package/packages/core/src/shared/SearchRender.ts +82 -0
  133. package/packages/core/src/shared/SendFile.ts +23 -0
  134. package/packages/core/src/shared/SpillCache.ts +78 -0
  135. package/packages/core/src/shared/SubagentLogs.ts +102 -0
  136. package/packages/core/src/shared/Supervisor.ts +460 -0
  137. package/packages/core/src/shared/Sweeper.ts +32 -0
  138. package/packages/core/src/shared/Tools.ts +801 -0
  139. package/packages/core/src/shared/Updater.ts +193 -0
  140. package/packages/core/src/view/AnsiPainter.ts +227 -0
  141. package/packages/core/src/view/BodyRenderer.ts +102 -0
  142. package/packages/core/src/view/DiffBlocks.ts +52 -0
  143. package/packages/core/src/view/DiffLayout.ts +69 -0
  144. package/packages/core/src/view/MarkdownPainter.ts +314 -0
  145. package/packages/core/src/view/Painting.ts +182 -0
  146. package/packages/core/src/view/ViewBlock.ts +124 -0
  147. package/packages/daemon/src/Daemon.ts +63 -0
  148. package/packages/daemon/src/DaemonInstall.ts +115 -0
  149. package/packages/daemon/src/Surfaces.ts +66 -0
  150. package/packages/daemon/src/TelegramSurface.ts +30 -0
  151. package/packages/daemon/src/WebSurface.ts +57 -0
  152. package/packages/daemon/src/index.ts +37 -0
  153. package/packages/protocol/src/Command.ts +92 -0
  154. package/packages/protocol/src/ImageRoute.ts +23 -0
  155. package/packages/protocol/src/Protocol.ts +6 -0
  156. package/packages/protocol/src/ServerEvent.ts +251 -0
  157. package/packages/server/src/AttachmentEndpoint.ts +184 -0
  158. package/packages/server/src/ClientConnection.ts +265 -0
  159. package/packages/server/src/ImageEndpoint.ts +36 -0
  160. package/packages/server/src/ProbeClient.ts +312 -0
  161. package/packages/server/src/Reloader.ts +93 -0
  162. package/packages/server/src/SendFileTool.ts +88 -0
  163. package/packages/server/src/SessionCatalogue.ts +202 -0
  164. package/packages/server/src/SessionProjection.ts +194 -0
  165. package/packages/server/src/SessionStream.ts +601 -0
  166. package/packages/server/src/StaticClient.ts +95 -0
  167. package/packages/server/src/WebOptions.ts +72 -0
  168. package/packages/server/src/WsGateway.ts +563 -0
  169. package/packages/server/src/probe.ts +146 -0
  170. package/packages/telegram/src/Bot.ts +229 -0
  171. package/packages/telegram/src/Commands.ts +768 -0
  172. package/packages/telegram/src/Config.ts +148 -0
  173. package/packages/telegram/src/Markdown.ts +235 -0
  174. package/packages/telegram/src/Message.ts +152 -0
  175. package/packages/telegram/src/Renderer.ts +456 -0
  176. package/packages/telegram/src/SendFileSchema.ts +18 -0
  177. package/packages/telegram/src/SendFileTool.ts +65 -0
  178. package/packages/telegram/src/Session.ts +252 -0
  179. package/packages/telegram/src/SessionRegistry.ts +126 -0
  180. package/packages/telegram/src/TaskScheduler.ts +296 -0
  181. package/packages/telegram/src/TaskSchema.ts +88 -0
  182. package/packages/telegram/src/TaskStore.ts +67 -0
  183. package/packages/telegram/src/TaskTool.ts +208 -0
  184. package/packages/telegram/src/TelegramHtml.ts +288 -0
  185. package/packages/telegram/src/TypingIndicator.ts +43 -0
  186. package/packages/telegram/src/UpdateConfirm.ts +61 -0
  187. package/packages/tui/src/extensions/_init/index.ts +144 -0
  188. package/packages/tui/src/extensions/command-picker/index.ts +43 -0
  189. package/packages/tui/src/extensions/file-picker/index.ts +156 -0
  190. package/packages/tui/src/extensions/footer/index.ts +117 -0
  191. package/packages/tui/src/extensions/footer/powerline.ts +87 -0
  192. package/packages/tui/src/extensions/footer/segments.ts +202 -0
  193. package/packages/tui/src/extensions/pim/index.ts +89 -0
  194. package/packages/tui/src/extensions/session-lease/index.ts +248 -0
  195. package/packages/tui/src/extensions/tps/index.ts +118 -0
  196. package/packages/tui/src/extensions/working-indicator/index.ts +75 -0
  197. package/packages/tui/src/extensions/wrapProvider.ts +18 -0
  198. package/packages/tui/src/themes/pim-dark.json +84 -0
  199. package/packages/tui/src/themes/pim-light.json +84 -0
  200. package/packages/tui/src/themes/themeCliArgs.ts +20 -0
  201. package/packages/web/dist/client/apple-touch-icon.png +0 -0
  202. package/packages/web/dist/client/assets/bash-LmRjUKlR.js +1 -0
  203. package/packages/web/dist/client/assets/c-DBVbJFpC.js +1 -0
  204. package/packages/web/dist/client/assets/clojure-CR7dvLJ4.js +1 -0
  205. package/packages/web/dist/client/assets/cmake-CgP6gB4W.js +1 -0
  206. package/packages/web/dist/client/assets/commit-mono-latin-300-italic-B48RJMeK.woff +0 -0
  207. package/packages/web/dist/client/assets/commit-mono-latin-300-italic-DBq5bJCE.woff2 +0 -0
  208. package/packages/web/dist/client/assets/commit-mono-latin-400-italic-BXinMwCi.woff +0 -0
  209. package/packages/web/dist/client/assets/commit-mono-latin-400-italic-DjSHLl2N.woff2 +0 -0
  210. package/packages/web/dist/client/assets/commit-mono-latin-400-normal-s0S3qwFW.woff +0 -0
  211. package/packages/web/dist/client/assets/commit-mono-latin-400-normal-wzhe4RuD.woff2 +0 -0
  212. package/packages/web/dist/client/assets/commit-mono-latin-600-normal-BuQDXT0M.woff +0 -0
  213. package/packages/web/dist/client/assets/commit-mono-latin-600-normal-SwgSWHaV.woff2 +0 -0
  214. package/packages/web/dist/client/assets/commit-mono-latin-700-normal-1-GU0IUE.woff +0 -0
  215. package/packages/web/dist/client/assets/commit-mono-latin-700-normal-DU8mrtj2.woff2 +0 -0
  216. package/packages/web/dist/client/assets/core-CGdQx9la.js +3 -0
  217. package/packages/web/dist/client/assets/cpp-N2d8M7Nc.js +1 -0
  218. package/packages/web/dist/client/assets/csharp-yYt0Ikks.js +1 -0
  219. package/packages/web/dist/client/assets/css-D8M8GYaw.js +1 -0
  220. package/packages/web/dist/client/assets/diff-BhdZizse.js +1 -0
  221. package/packages/web/dist/client/assets/dockerfile-C0zufGsU.js +1 -0
  222. package/packages/web/dist/client/assets/elixir-DXIHlGsR.js +1 -0
  223. package/packages/web/dist/client/assets/erlang-BjYI8dhz.js +1 -0
  224. package/packages/web/dist/client/assets/go-CYstVVJV.js +1 -0
  225. package/packages/web/dist/client/assets/graphql-BHHhacaR.js +1 -0
  226. package/packages/web/dist/client/assets/haskell-DUkhXIda.js +1 -0
  227. package/packages/web/dist/client/assets/index-BMNwleWO.js +40 -0
  228. package/packages/web/dist/client/assets/index-CqjB2RPr.css +1 -0
  229. package/packages/web/dist/client/assets/ini-B1CroMsf.js +1 -0
  230. package/packages/web/dist/client/assets/java-Dde-Wnb4.js +1 -0
  231. package/packages/web/dist/client/assets/javascript-8AmHuH5O.js +1 -0
  232. package/packages/web/dist/client/assets/json-PmyKWHmm.js +1 -0
  233. package/packages/web/dist/client/assets/kotlin-DgSK8bu-.js +2 -0
  234. package/packages/web/dist/client/assets/less-Bp0mgmpG.js +1 -0
  235. package/packages/web/dist/client/assets/lua-1dXYaL-H.js +1 -0
  236. package/packages/web/dist/client/assets/makefile-BX-o3EIz.js +1 -0
  237. package/packages/web/dist/client/assets/markdown-D6FrnlqJ.js +1 -0
  238. package/packages/web/dist/client/assets/ocaml-B-P1Uyq8.js +1 -0
  239. package/packages/web/dist/client/assets/perl-CvqMZYuf.js +1 -0
  240. package/packages/web/dist/client/assets/php-DuZl5e-L.js +2 -0
  241. package/packages/web/dist/client/assets/powershell-BUe16zE4.js +1 -0
  242. package/packages/web/dist/client/assets/protobuf-DGOW1qT8.js +1 -0
  243. package/packages/web/dist/client/assets/python-gHmu6VPG.js +1 -0
  244. package/packages/web/dist/client/assets/r-DNWPCnjT.js +1 -0
  245. package/packages/web/dist/client/assets/ruby-BGpZ3p4_.js +1 -0
  246. package/packages/web/dist/client/assets/rust-BIKPVoi6.js +1 -0
  247. package/packages/web/dist/client/assets/scala-C9lP4FWY.js +1 -0
  248. package/packages/web/dist/client/assets/scss-gOyOKiXd.js +1 -0
  249. package/packages/web/dist/client/assets/sql-DWRKyvJ-.js +1 -0
  250. package/packages/web/dist/client/assets/swift-BTn9__hL.js +1 -0
  251. package/packages/web/dist/client/assets/typescript-CxkPLmSc.js +1 -0
  252. package/packages/web/dist/client/assets/vim-Dhhcn_CA.js +1 -0
  253. package/packages/web/dist/client/assets/xml-COl5yFSC.js +1 -0
  254. package/packages/web/dist/client/assets/yaml-DjOhhVLG.js +1 -0
  255. package/packages/web/dist/client/favicon.svg +1 -0
  256. package/packages/web/dist/client/icon-192.png +0 -0
  257. package/packages/web/dist/client/icon-512.png +0 -0
  258. package/packages/web/dist/client/icon-maskable.png +0 -0
  259. package/packages/web/dist/client/index.html +24 -0
  260. package/packages/web/dist/client/manifest.webmanifest +21 -0
  261. package/packages/web/dist/client/wordmark.svg +1 -0
@@ -0,0 +1,9 @@
1
+ function describe(error: unknown): string {
2
+ return error instanceof Error ? error.message : String(error);
3
+ }
4
+
5
+ function isAbort(error: unknown): boolean {
6
+ return error instanceof Error && error.name === "AbortError";
7
+ }
8
+
9
+ export const Errors = { describe, isAbort };
@@ -0,0 +1,130 @@
1
+ import type { ExtensionFactory } from "@earendil-works/pi-coding-agent";
2
+
3
+ import { PimSettings } from "./PimSettings";
4
+
5
+ // Pi's `enabled` filter covers disk paths only, never inline factories: pim owns this roster.
6
+ const EXTENSIONS = {
7
+ _init: "Splash screen, runtime guard, /clear",
8
+ "apply-patch": "apply_patch tool",
9
+ bash: "bash tool",
10
+ "command-picker": "Slash command and skill autocomplete",
11
+ edit: "edit tool",
12
+ "file-picker": "@-path autocomplete",
13
+ footer: "Powerline footer",
14
+ glob: "glob tool",
15
+ grep: "grep tool",
16
+ pim: "This menu",
17
+ read: "read tool",
18
+ "session-lease": "Cross-surface turn lease and /sync",
19
+ subagent: "subagent tool",
20
+ "system-prompt": "Pim system prompt",
21
+ todo: "todo tool",
22
+ tps: "Per-turn decode/prefill tps report",
23
+ "web-fetch": "web_fetch tool",
24
+ "web-search": "web_search tool",
25
+ "working-indicator": "Animated working indicator",
26
+ write: "write tool",
27
+ } as const;
28
+
29
+ export type PimExtensionName = keyof typeof EXTENSIONS;
30
+
31
+ const NAMES = Object.keys(EXTENSIONS) as readonly PimExtensionName[];
32
+
33
+ const DEFAULT_DISABLED: readonly PimExtensionName[] = ["todo", "tps"];
34
+
35
+ const REQUIRED: readonly PimExtensionName[] = ["_init", "pim", "session-lease"];
36
+
37
+ function isRequired(name: string): boolean {
38
+ return (REQUIRED as readonly string[]).includes(name);
39
+ }
40
+
41
+ function isKnown(name: string): name is PimExtensionName {
42
+ return name in EXTENSIONS;
43
+ }
44
+
45
+ function describe(name: PimExtensionName): string {
46
+ return EXTENSIONS[name];
47
+ }
48
+
49
+ // Gate inside the factory, not around the roster: pi re-invokes factories on `ctx.reload()`.
50
+ function gate(
51
+ name: PimExtensionName,
52
+ factory: ExtensionFactory
53
+ ): ExtensionFactory {
54
+ return async (pi) => {
55
+ if (await isDisabled(name)) {
56
+ return;
57
+ }
58
+ await factory(pi);
59
+ };
60
+ }
61
+
62
+ async function disabled(): Promise<readonly PimExtensionName[]> {
63
+ const { toggles } = await PimSettings.get("extensions");
64
+ return NAMES.filter((name) => !enabled(name, toggles));
65
+ }
66
+
67
+ async function isDisabled(name: string): Promise<boolean> {
68
+ if (!isKnown(name)) {
69
+ return false;
70
+ }
71
+ const { toggles } = await PimSettings.get("extensions");
72
+ return !enabled(name, toggles);
73
+ }
74
+
75
+ // Serialized: each call is a read-modify-write of the same record.
76
+ async function setDisabled(name: string, isOff: boolean): Promise<void> {
77
+ if (!isKnown(name)) {
78
+ throw new Error(`Unknown pim extension "${name}"`);
79
+ }
80
+ if (isOff && isRequired(name)) {
81
+ throw new Error(`"${name}" is required by pim and cannot be disabled`);
82
+ }
83
+ await PimSettings.update("extensions", ({ toggles }) => {
84
+ const next = { ...toggles };
85
+ if (!isOff === defaultEnabled(name)) {
86
+ delete next[name];
87
+ } else {
88
+ next[name] = !isOff;
89
+ }
90
+ return { toggles: next };
91
+ });
92
+ }
93
+
94
+ async function toggle(
95
+ name: string
96
+ ): Promise<{ readonly name: PimExtensionName; readonly disabled: boolean }> {
97
+ if (!isKnown(name)) {
98
+ throw new Error(`Unknown pim extension "${name}"`);
99
+ }
100
+ const isOff = !(await isDisabled(name));
101
+ await setDisabled(name, isOff);
102
+ return { name, disabled: isOff };
103
+ }
104
+
105
+ function defaultEnabled(name: PimExtensionName): boolean {
106
+ return !DEFAULT_DISABLED.includes(name);
107
+ }
108
+
109
+ function enabled(
110
+ name: PimExtensionName,
111
+ toggles: Readonly<Record<string, boolean>>
112
+ ): boolean {
113
+ if (isRequired(name)) {
114
+ return true;
115
+ }
116
+ return toggles[name] ?? defaultEnabled(name);
117
+ }
118
+
119
+ export const ExtensionToggles = {
120
+ REQUIRED,
121
+ NAMES,
122
+ isRequired,
123
+ isKnown,
124
+ describe,
125
+ gate,
126
+ disabled,
127
+ isDisabled,
128
+ setDisabled,
129
+ toggle,
130
+ };
@@ -0,0 +1,361 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import ignore, { type Ignore } from "ignore";
4
+
5
+ const CONCURRENCY = 32;
6
+
7
+ export type EnumerateOptions = {
8
+ readonly includeDotfiles?: boolean;
9
+ readonly includeIgnored?: boolean;
10
+ };
11
+
12
+ type StackEntry = {
13
+ readonly abs: string;
14
+ readonly rel: string;
15
+ readonly inRepo: boolean;
16
+ readonly repoRootAbs: string;
17
+ readonly ignoreRules: string[];
18
+ readonly matcher: Ignore;
19
+ };
20
+
21
+ const EMPTY_MATCHER = ignore();
22
+
23
+ type WalkContext = {
24
+ includeDotfiles: boolean;
25
+ useIgnore: boolean;
26
+ globalGitIgnore: string | undefined;
27
+ stack: StackEntry[];
28
+ result: string[];
29
+ };
30
+
31
+ async function readIgnoreFile(path: string): Promise<string | undefined> {
32
+ try {
33
+ return await Bun.file(path).text();
34
+ } catch {
35
+ return undefined;
36
+ }
37
+ }
38
+
39
+ function globalGitIgnorePath(): string | undefined {
40
+ const xdgConfigHome = process.env.XDG_CONFIG_HOME;
41
+ if (xdgConfigHome !== undefined && xdgConfigHome !== "") {
42
+ return join(xdgConfigHome, "git", "ignore");
43
+ }
44
+
45
+ const home = process.env.HOME;
46
+ if (home !== undefined && home !== "") {
47
+ return join(home, ".config", "git", "ignore");
48
+ }
49
+
50
+ return undefined;
51
+ }
52
+
53
+ function relFromBase(absPath: string, baseAbs: string): string | undefined {
54
+ if (absPath === baseAbs) {
55
+ return "";
56
+ }
57
+
58
+ const prefix = baseAbs.endsWith("/") ? baseAbs : `${baseAbs}/`;
59
+ if (!absPath.startsWith(prefix)) {
60
+ return undefined;
61
+ }
62
+
63
+ return absPath.slice(prefix.length);
64
+ }
65
+
66
+ function ignoreVerdict(
67
+ matcher: Ignore,
68
+ repoRootAbs: string,
69
+ absPath: string,
70
+ isDirectory: boolean
71
+ ): { ignored: boolean; unignored: boolean } {
72
+ const path = relFromBase(absPath, repoRootAbs);
73
+ if (path === undefined || path === "") {
74
+ return { ignored: false, unignored: false };
75
+ }
76
+ const result = matcher.test(isDirectory ? `${path}/` : path);
77
+ return { ignored: result.ignored, unignored: result.unignored };
78
+ }
79
+
80
+ function pushRules(out: string[], content: string): void {
81
+ for (const line of content.split(/\r?\n/)) {
82
+ if (line.trim() === "" || line.startsWith("#")) {
83
+ continue;
84
+ }
85
+ out.push(line);
86
+ }
87
+ }
88
+
89
+ // Gitignore anchoring: a pattern containing a non-trailing slash only gains the prefix; one
90
+ // without also gains `**/`, or it stops matching below its own directory.
91
+ function reanchorRules(
92
+ content: string,
93
+ basePrefix: string,
94
+ out: string[]
95
+ ): void {
96
+ for (const line of content.split(/\r?\n/)) {
97
+ if (line.trim() === "" || line.startsWith("#")) {
98
+ continue;
99
+ }
100
+
101
+ let negated = false;
102
+ let pattern = line;
103
+ if (pattern.startsWith("!")) {
104
+ negated = true;
105
+ pattern = pattern.slice(1);
106
+ }
107
+ if (pattern === "") {
108
+ continue;
109
+ }
110
+
111
+ let body: string;
112
+ if (pattern.startsWith("/")) {
113
+ body = basePrefix + pattern.slice(1);
114
+ } else {
115
+ const core = pattern.endsWith("/") ? pattern.slice(0, -1) : pattern;
116
+ body = core.includes("/")
117
+ ? basePrefix + pattern
118
+ : basePrefix + "**/" + pattern;
119
+ }
120
+
121
+ out.push(negated ? `!${body}` : body);
122
+ }
123
+ }
124
+
125
+ async function findRepoRoot(start: string): Promise<string | undefined> {
126
+ let dir = start;
127
+ for (;;) {
128
+ if (await Bun.file(join(dir, ".git")).exists()) {
129
+ return dir;
130
+ }
131
+ const parent = dirname(dir);
132
+ if (parent === dir) {
133
+ return undefined;
134
+ }
135
+ dir = parent;
136
+ }
137
+ }
138
+
139
+ // Precedence order: global excludes, then .git/info/exclude, then the repo's own .gitignore.
140
+ async function repoBaseRules(
141
+ repoAbs: string,
142
+ globalGitIgnore: string | undefined
143
+ ): Promise<string[]> {
144
+ const rules: string[] = [];
145
+ if (globalGitIgnore !== undefined) {
146
+ pushRules(rules, globalGitIgnore);
147
+ }
148
+ const [infoExclude, repoGitIgnore] = await Promise.all([
149
+ readIgnoreFile(join(repoAbs, ".git", "info", "exclude")),
150
+ readIgnoreFile(join(repoAbs, ".gitignore")),
151
+ ]);
152
+ if (infoExclude !== undefined) {
153
+ pushRules(rules, infoExclude);
154
+ }
155
+ if (repoGitIgnore !== undefined) {
156
+ pushRules(rules, repoGitIgnore);
157
+ }
158
+ return rules;
159
+ }
160
+
161
+ async function processDir(
162
+ ctx: WalkContext,
163
+ currentDir: StackEntry
164
+ ): Promise<void> {
165
+ let entries;
166
+ try {
167
+ entries = await readdir(currentDir.abs, { withFileTypes: true });
168
+ } catch {
169
+ return;
170
+ }
171
+
172
+ let inRepo = currentDir.inRepo;
173
+ let repoRootAbs = currentDir.repoRootAbs;
174
+ let rules = currentDir.ignoreRules;
175
+ let matcher = currentDir.matcher;
176
+
177
+ if (ctx.useIgnore) {
178
+ const hasDotGit = entries.some((e) => e.name === ".git");
179
+ if (hasDotGit) {
180
+ // A .git is a repo boundary: a child repo must not inherit its parent's rules.
181
+ inRepo = true;
182
+ repoRootAbs = currentDir.abs;
183
+ rules = await repoBaseRules(currentDir.abs, ctx.globalGitIgnore);
184
+ matcher = ignore().add(rules);
185
+ } else if (inRepo) {
186
+ const hasGitIgnore = entries.some(
187
+ (e) => e.isFile() && e.name === ".gitignore"
188
+ );
189
+ if (hasGitIgnore) {
190
+ const content = await readIgnoreFile(
191
+ join(currentDir.abs, ".gitignore")
192
+ );
193
+ if (content !== undefined) {
194
+ const dirRel = relFromBase(currentDir.abs, repoRootAbs);
195
+ const next = rules.slice();
196
+ if (dirRel === undefined || dirRel === "") {
197
+ pushRules(next, content);
198
+ } else {
199
+ reanchorRules(content, `${dirRel}/`, next);
200
+ }
201
+ rules = next;
202
+ matcher = ignore().add(rules);
203
+ }
204
+ }
205
+ }
206
+ }
207
+
208
+ for (const entry of entries) {
209
+ const name = entry.name;
210
+ const relPath = currentDir.rel === "" ? name : `${currentDir.rel}/${name}`;
211
+ const isDir = entry.isDirectory();
212
+ const isSymlink = entry.isSymbolicLink();
213
+
214
+ if (name === ".git") {
215
+ continue;
216
+ }
217
+
218
+ const needsAbs = isDir || (ctx.useIgnore && inRepo);
219
+ const childAbs = needsAbs ? join(currentDir.abs, name) : "";
220
+
221
+ const verdict =
222
+ ctx.useIgnore && inRepo
223
+ ? ignoreVerdict(matcher, repoRootAbs, childAbs, isDir)
224
+ : { ignored: false, unignored: false };
225
+
226
+ // A `!` negation re-includes a dotfile, as in git/fd.
227
+ if (
228
+ !ctx.includeDotfiles &&
229
+ name.charCodeAt(0) === 0x2e /* "." */ &&
230
+ !verdict.unignored
231
+ ) {
232
+ continue;
233
+ }
234
+
235
+ if (verdict.ignored) {
236
+ continue;
237
+ }
238
+
239
+ if (isDir) {
240
+ // Never follow symlinked dirs: cycles.
241
+ if (isSymlink) {
242
+ continue;
243
+ }
244
+
245
+ ctx.stack.push({
246
+ abs: childAbs,
247
+ rel: relPath,
248
+ inRepo,
249
+ repoRootAbs,
250
+ ignoreRules: rules,
251
+ matcher,
252
+ });
253
+ continue;
254
+ }
255
+
256
+ if (entry.isFile() || isSymlink) {
257
+ ctx.result.push(relPath);
258
+ continue;
259
+ }
260
+ }
261
+ }
262
+
263
+ // Refill from the shared stack on each completion; N worker loops would exit on the root-only
264
+ // stack before any child was pushed and silently return a partial listing.
265
+ function drain(ctx: WalkContext): Promise<void> {
266
+ let inFlight = 0;
267
+ return new Promise<void>((resolve, reject) => {
268
+ const pump = (): void => {
269
+ while (inFlight < CONCURRENCY && ctx.stack.length > 0) {
270
+ const currentDir = ctx.stack.pop()!;
271
+ inFlight++;
272
+ processDir(ctx, currentDir).then(() => {
273
+ inFlight--;
274
+ pump();
275
+ }, reject);
276
+ }
277
+ if (inFlight === 0 && ctx.stack.length === 0) {
278
+ resolve();
279
+ }
280
+ };
281
+ pump();
282
+ });
283
+ }
284
+
285
+ /** All files under `root` as root-relative POSIX paths; `.gitignore` is honored only inside a repo. */
286
+ async function enumerate(
287
+ root: string,
288
+ opts?: EnumerateOptions
289
+ ): Promise<string[]> {
290
+ const includeDotfiles = opts?.includeDotfiles ?? false;
291
+ const includeIgnored = opts?.includeIgnored ?? false;
292
+ const useIgnore = !includeIgnored;
293
+
294
+ let globalGitIgnore: string | undefined;
295
+ if (useIgnore) {
296
+ const pathname = globalGitIgnorePath();
297
+ globalGitIgnore =
298
+ pathname === undefined ? undefined : await readIgnoreFile(pathname);
299
+ }
300
+
301
+ // Seed rules from a repo enclosing `root`; `root`'s own .gitignore is added by processDir.
302
+ let initialInRepo = false;
303
+ let initialRepoRootAbs = root;
304
+ let initialRules: string[] = [];
305
+ if (useIgnore) {
306
+ const repoRoot = await findRepoRoot(root);
307
+ if (repoRoot !== undefined && repoRoot !== root) {
308
+ initialInRepo = true;
309
+ initialRepoRootAbs = repoRoot;
310
+ initialRules = await repoBaseRules(repoRoot, globalGitIgnore);
311
+
312
+ // Shallowest first: gitignore precedence is by depth.
313
+ const intermediates: string[] = [];
314
+ let dir = dirname(root);
315
+ while (dir !== repoRoot && dir.length > repoRoot.length) {
316
+ intermediates.push(dir);
317
+ const parent = dirname(dir);
318
+ if (parent === dir) {
319
+ break;
320
+ }
321
+ dir = parent;
322
+ }
323
+ intermediates.reverse();
324
+
325
+ for (const dirAbs of intermediates) {
326
+ const content = await readIgnoreFile(join(dirAbs, ".gitignore"));
327
+ if (content !== undefined) {
328
+ const dirRel = relFromBase(dirAbs, repoRoot);
329
+ if (dirRel === undefined || dirRel === "") {
330
+ pushRules(initialRules, content);
331
+ } else {
332
+ reanchorRules(content, `${dirRel}/`, initialRules);
333
+ }
334
+ }
335
+ }
336
+ }
337
+ }
338
+
339
+ const ctx: WalkContext = {
340
+ includeDotfiles,
341
+ useIgnore,
342
+ globalGitIgnore,
343
+ stack: [
344
+ {
345
+ abs: root,
346
+ rel: "",
347
+ inRepo: initialInRepo,
348
+ repoRootAbs: initialRepoRootAbs,
349
+ ignoreRules: initialRules,
350
+ matcher: initialInRepo ? ignore().add(initialRules) : EMPTY_MATCHER,
351
+ },
352
+ ],
353
+ result: [],
354
+ };
355
+
356
+ await drain(ctx);
357
+
358
+ return ctx.result;
359
+ }
360
+
361
+ export const FileEnumerator = { enumerate };
@@ -0,0 +1,57 @@
1
+ import { stat } from "node:fs/promises";
2
+ import { join, resolve } from "node:path";
3
+ import { FileEnumerator } from "./FileEnumerator";
4
+ import { GlobExclusions } from "./GlobExclusions";
5
+
6
+ export type FileScanOptions = {
7
+ readonly exclude?: readonly string[];
8
+ readonly includeDotfiles: boolean;
9
+ readonly includeIgnored: boolean;
10
+ };
11
+
12
+ async function scan(
13
+ root: string,
14
+ pattern: string,
15
+ options: FileScanOptions
16
+ ): Promise<readonly string[]> {
17
+ const absoluteRoot = resolve(root);
18
+ const relativePaths = await FileEnumerator.enumerate(absoluteRoot, {
19
+ includeDotfiles: options.includeDotfiles,
20
+ includeIgnored: options.includeIgnored,
21
+ });
22
+ const matcher = new Bun.Glob(await expandDirectory(absoluteRoot, pattern));
23
+ const excludes = GlobExclusions.compile(options.exclude);
24
+ const files: string[] = [];
25
+
26
+ for (const relativePath of relativePaths) {
27
+ if (!matcher.match(relativePath)) {
28
+ continue;
29
+ }
30
+ if (GlobExclusions.ignores(excludes, relativePath)) {
31
+ continue;
32
+ }
33
+ files.push(join(absoluteRoot, relativePath));
34
+ }
35
+
36
+ return files;
37
+ }
38
+
39
+ async function expandDirectory(
40
+ absoluteRoot: string,
41
+ pattern: string
42
+ ): Promise<string> {
43
+ const trimmed = pattern.replace(/^(?:\.\/)+/, "").replace(/\/+$/, "");
44
+
45
+ if (trimmed.length === 0) {
46
+ return "**/*";
47
+ }
48
+
49
+ try {
50
+ const metadata = await stat(resolve(absoluteRoot, trimmed));
51
+ return metadata.isDirectory() ? `${trimmed}/**/*` : pattern;
52
+ } catch {
53
+ return pattern;
54
+ }
55
+ }
56
+
57
+ export const FileScanner = { scan };
@@ -0,0 +1,107 @@
1
+ import {
2
+ type FSWatcher,
3
+ readdirSync,
4
+ statSync,
5
+ watch as watchFs,
6
+ } from "node:fs";
7
+ import { basename, dirname, join } from "node:path";
8
+
9
+ const POLL_MS = 1_000;
10
+
11
+ function signatureOf(path: string): string {
12
+ const stats = statSync(path, { throwIfNoEntry: false });
13
+ return stats === undefined
14
+ ? "gone"
15
+ : `${stats.mtimeMs}:${stats.size}:${stats.ino}`;
16
+ }
17
+
18
+ /** Reports a change only once the path itself moved, so a watcher firing for its neighbours costs nothing. */
19
+ function tracker(path: string, onChange: () => void): () => void {
20
+ let signature = signatureOf(path);
21
+ return (): void => {
22
+ const next = signatureOf(path);
23
+ if (next === signature) {
24
+ return;
25
+ }
26
+ signature = next;
27
+ onChange();
28
+ };
29
+ }
30
+
31
+ /**
32
+ * `fs.watch` reports nothing at all on some filesystems and network mounts, so
33
+ * the poll beside it is not optional. The poll also re-arms the watcher, which
34
+ * a directory that does not exist yet refuses to give.
35
+ */
36
+ function follow(
37
+ watched: string,
38
+ entry: string | undefined,
39
+ onEvent: () => void,
40
+ onPoll: () => void,
41
+ pollMs: number
42
+ ): () => void {
43
+ let watcher: FSWatcher | undefined;
44
+ const arm = (): void => {
45
+ if (watcher !== undefined) {
46
+ return;
47
+ }
48
+ try {
49
+ watcher = watchFs(watched, { persistent: false }, (_event, changed) => {
50
+ if (entry === undefined || changed === null || changed === entry) {
51
+ onEvent();
52
+ }
53
+ });
54
+ watcher.on("error", () => {
55
+ watcher = undefined;
56
+ });
57
+ } catch {
58
+ watcher = undefined;
59
+ }
60
+ };
61
+ arm();
62
+ const timer = setInterval(() => {
63
+ arm();
64
+ onPoll();
65
+ }, pollMs);
66
+ timer.unref?.();
67
+ return (): void => {
68
+ clearInterval(timer);
69
+ try {
70
+ watcher?.close();
71
+ } catch {}
72
+ };
73
+ }
74
+
75
+ /** Fires when one file appears, grows, or goes away; the directory is watched, so the file need not exist yet. */
76
+ function file(
77
+ path: string,
78
+ onChange: () => void,
79
+ pollMs: number = POLL_MS
80
+ ): () => void {
81
+ const check = tracker(path, onChange);
82
+ return follow(dirname(path), basename(path), check, check, pollMs);
83
+ }
84
+
85
+ /** Fires when a directory gains or loses an entry, or when anything inside it is written. */
86
+ function directory(
87
+ path: string,
88
+ onChange: () => void,
89
+ pollMs: number = POLL_MS
90
+ ): () => void {
91
+ // Writing a file moves nothing on the directory holding it, so the poll answers only for entries coming and going.
92
+ return follow(path, undefined, onChange, tracker(path, onChange), pollMs);
93
+ }
94
+
95
+ /** The directories directly inside `path`, or none when it does not exist. */
96
+ function subdirectories(path: string): readonly string[] {
97
+ try {
98
+ return readdirSync(path, { withFileTypes: true })
99
+ .filter((entry) => entry.isDirectory())
100
+ .map((entry) => join(path, entry.name));
101
+ } catch {
102
+ return [];
103
+ }
104
+ }
105
+
106
+ /** Watching paths other processes write: an `fs.watch` that may do nothing, backed by a poll that always does. */
107
+ export const FileWatch = { file, directory, subdirectories };