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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AaronCQL
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,10 +1,296 @@
1
- # pim-agent
1
+ <!-- omit in toc -->
2
+ # PIM - Pi IMproved
2
3
 
3
- Placeholder. This version exists only to register the package name so that npm
4
- trusted publishing (OIDC) can be configured for it; npm cannot publish a
5
- package's first version over OIDC.
4
+ [![npm version](https://img.shields.io/npm/v/pim-agent?style=flat-square)](https://www.npmjs.com/package/pim-agent)
5
+ [![npm downloads](https://img.shields.io/npm/dm/pim-agent?style=flat-square)](https://www.npmjs.com/package/pim-agent)
6
+ [![license](https://img.shields.io/npm/l/pim-agent?style=flat-square)](./LICENSE)
7
+ [![Bun](https://img.shields.io/badge/runtime-Bun-black?logo=bun&style=flat-square)](https://bun.com)
6
8
 
7
- The real package — a batteries-included distro of Pi, in your terminal, browser
8
- and Telegram — lands as `0.9.0`, published from CI with provenance.
9
+ A batteries-included distro of Pi, accessible in your terminal, browser and Telegram.
9
10
 
10
- <https://github.com/AaronCQL/pim-agent>
11
+ - **For the agent**: model-aware editing, web search/fetch, subagents, and more, behind a system prompt under 3K tokens.
12
+ - **For you**: shared terminal/browser sessions, a mobile-friendly web UI, and a Telegram bot.
13
+ - **Still Pi**: your Pi extensions, CLI, sessions and config keep working, while vanilla `pi` stays untouched.
14
+
15
+ ![Pim running in a terminal, a browser and Telegram, with a live session shared between them](https://raw.githubusercontent.com/AaronCQL/pim-agent/refs/heads/main/assets/hero.webp)
16
+
17
+ - [Quick Start](#quick-start)
18
+ - [Web \& Telegram](#web--telegram)
19
+ - [Agent Tools](#agent-tools)
20
+ - [Terminal UI](#terminal-ui)
21
+ - [Web UI](#web-ui)
22
+ - [Setup](#setup)
23
+ - [Remote Access](#remote-access)
24
+ - [Telegram Bot](#telegram-bot)
25
+ - [Setup](#setup-1)
26
+ - [Commands](#commands)
27
+ - [Features](#features)
28
+ - [Configuration](#configuration)
29
+ - [Toggling Features](#toggling-features)
30
+ - [API Keys (Optional)](#api-keys-optional)
31
+ - [Recommended TUI Settings (Optional)](#recommended-tui-settings-optional)
32
+ - [Why Pim?](#why-pim)
33
+ - [Pi Core](#pi-core)
34
+ - [Lean System Prompt](#lean-system-prompt)
35
+ - [Model-Aware Tools](#model-aware-tools)
36
+ - [Benchmarks](#benchmarks)
37
+ - [Terminal-Bench 2.0](#terminal-bench-20)
38
+ - [Changelog](#changelog)
39
+ - [Developing](#developing)
40
+
41
+ ## Quick Start
42
+
43
+ Ensure that you have [Bun](https://bun.com/docs/installation) already installed:
44
+
45
+ ```sh
46
+ # Install pim:
47
+ bun install -g pim-agent
48
+
49
+ # Launch pim TUI:
50
+ pim
51
+
52
+ # Update pim:
53
+ pim update
54
+ ```
55
+
56
+ ### Web & Telegram
57
+
58
+ To use Pim via your browser or Telegram, the recommended approach is to install the servers as a persistent daemon:
59
+
60
+ ```sh
61
+ # Supports Linux (systemd) and macOS (launchd)
62
+ pim --mode daemon --install
63
+
64
+ # Tear down
65
+ pim --mode daemon --uninstall
66
+ ```
67
+
68
+ Supported arguments:
69
+
70
+ | Flag | Default |
71
+ | --- | --- |
72
+ | `--surfaces` | `web,telegram` |
73
+ | `--port` (web only) | `4319` |
74
+ | `--hostname` (web only) | `127.0.0.1` |
75
+ | `--cwd` | Current working directory |
76
+
77
+ These are frozen into the daemon's unit file at install time, so re-run `--install` to change them. Flags you omit keep their installed values, and an older per-surface install is stopped and replaced for you. The daemon auto-restarts on failure, and updates in place from **Settings → Update & Restart** on the web UI, or `/update` in Telegram.
78
+
79
+ After installing with the default settings, the web UI is at <http://localhost:4319>. Telegram needs a bot token first (see [Telegram Bot](#telegram-bot)). Surfaces start in isolation, so an unconfigured bot still leaves the browser served.
80
+
81
+ ## Agent Tools
82
+
83
+ Pim revamps Pi's default tools (`bash`, `read`, `write`, `edit`) so they produce consistent behaviour and output, cross-reference each other where useful, and render uniformly in your UIs. It also adds:
84
+
85
+ - **`apply_patch`** - V4A patch editing, dynamically exposed instead of `edit` for OpenAI and select Claude models
86
+ - **`glob`** - file enumeration by glob pattern, sorted newest-first, respects `.gitignore`
87
+ - **`grep`** - regex search across files with context lines, multiline matching, respects `.gitignore`
88
+ - **`web_search`** - search the web via [Exa](https://exa.ai)/[Firecrawl](https://www.firecrawl.dev/)/[DuckDuckGo](https://duckduckgo.com/) with ranked results and snippets
89
+ - **`web_fetch`** - fetch websites as Markdown via [Jina](https://jina.ai/reader/), with browser-rendered fallback via [`Bun.WebView`](https://bun.com/docs/runtime/webview)
90
+ - **`subagent`** - delegate complex work to isolated sub-sessions with full tool access
91
+
92
+ ## Terminal UI
93
+
94
+ Pim also ships with quality of life improvements for the TUI:
95
+
96
+ - **ANSI-compatible themes** - `pim-light` and `pim-dark` themes which adapt to your terminal's colour scheme
97
+ - **fzf-style autocomplete** - `@path` file picker and `/command` picker with fuzzy search
98
+ - **Git-aware powerline footer** - cwd, git branch and states, context usage, model and session cost (run `/pim` to disable)
99
+ - **TPS reporting** - per-cycle decode/prefill rate, TTFT, and cache read tokens (disabled by default; run `/pim` to enable)
100
+ - **Concise tool UI** - minimal one-liner title across all tool calls, `Ctrl+O` to toggle full details
101
+
102
+ ## Web UI
103
+
104
+ Run Pim in the browser, on any device. Hosted on a machine you can reach remotely, this lets you start work at your desk and carry on from your phone.
105
+
106
+ ### Setup
107
+
108
+ See [Web & Telegram](#web--telegram).
109
+
110
+ ### Remote Access
111
+
112
+ > [!WARNING]
113
+ > Web mode has no built-in authentication, and the agent runs shell commands as you: anything that can reach the port has full access to the host. Bind it to a private network, never to `0.0.0.0`.
114
+
115
+ [Tailscale](https://tailscale.com/) is the recommended way to do this. Binding to your tailnet IP keeps the server off your LAN and off the public internet, while every device on your tailnet can still reach it:
116
+
117
+ ```sh
118
+ # Look up this machine's tailnet IP:
119
+ tailscale ip -4
120
+
121
+ # Install with --hostname set to it:
122
+ pim --mode daemon --install --hostname=100.115.46.15 # Replace with your actual tailscale IP
123
+ ```
124
+
125
+ The web UI is then at `http://<tailnet-ip>:4319` and `localhost` no longer serves it.
126
+
127
+ ## Telegram Bot
128
+
129
+ Run Pim as a Telegram bot with full agent capabilities in your DMs or group chats (supports threads).
130
+
131
+ ### Setup
132
+
133
+ Create `~/.pim/telegram/config.json` with your bot token (from [@BotFather](https://t.me/BotFather)) and an allowlist of chat IDs the bot will respond to:
134
+
135
+ ```json
136
+ {
137
+ "token": "YOUR_TELEGRAM_BOT_TOKEN",
138
+ "allow": [123456789, 987654321]
139
+ }
140
+ ```
141
+
142
+ Then [install the daemon](#run-as-a-daemon), which brings the bot up with it.
143
+
144
+ ### Commands
145
+
146
+ > [!TIP]
147
+ > Use `/commands` on your bot for all commands to show up on your Telegram UI.
148
+
149
+ | Command | Description |
150
+ | ------------ | -------------------------------------------------- |
151
+ | `/cancel` | Cancel the current turn |
152
+ | `/cd` | Show or change the working directory |
153
+ | `/chatid` | Show this chat's numeric ID |
154
+ | `/clear` | Reset chat history and context window |
155
+ | `/commands` | Register all commands with Telegram |
156
+ | `/compact` | Compact the current session context |
157
+ | `/effort` | Show or change thinking effort level |
158
+ | `/logs` | Show or change log verbosity |
159
+ | `/model` | Show or change the AI model |
160
+ | `/temporary` | Toggle temporary chat (fresh session each message) |
161
+ | `/update` | Update the bot to the latest version |
162
+ | `/usage` | Show context window and session cost |
163
+
164
+ ### Features
165
+
166
+ - ⏰ **Scheduled tasks** - your bot can create one-time, interval, or cron-based tasks that fire automatically; ask your bot to schedule something.
167
+ - 👀 **Live progress logs** - use `/logs` to choose what you see while the agent works: final replies, tool use, intermediate text, or thinking.
168
+ - 📝 **Rich Markdown** - supports Telegram's [rich text formatting](https://telegram.org/blog/watch-apps-and-more#obscenely-rich-text-formatting-for-bots) with full markdown and LaTeX math support.
169
+ - 📎 **Rich media** - send photos, documents, videos, audio, and voice messages directly in chat; your bot can also send files back to you.
170
+ - 🧵 **Thread-specific prompts** - each chat (or thread) gets its own session and optional instructions; ask your bot to modify its instructions.
171
+
172
+ ## Configuration
173
+
174
+ ### Toggling Features
175
+
176
+ Pim ships a collection of features, nearly all enabled by default. To enable or disable specific features, run `/pim` in the TUI and toggle from the list. The changes apply to the running session immediately.
177
+
178
+ ### API Keys (Optional)
179
+
180
+ The `web_search` tool tries [Exa](https://exa.ai) → [Firecrawl](https://www.firecrawl.dev/) → DuckDuckGo (via [Jina](https://jina.ai/reader/) reader), and the `web_fetch` tool uses Jina with a `Bun.WebView` fallback. These tools still work without API keys, but are subject to the following keyless rate limits (as of Sept 2026):
181
+
182
+ - Exa - 1,000 requests per month
183
+ - Firecrawl - 1,000 requests per month
184
+ - Jina - 20 requests per minute
185
+
186
+ For heavier usage, add API keys to `~/.pim/settings.json`:
187
+
188
+ ```json
189
+ {
190
+ "exa": {
191
+ "apiKey": "api_key_here"
192
+ },
193
+ "firecrawl": {
194
+ "apiKey": "api_key_here"
195
+ },
196
+ "jina": {
197
+ "apiKey": "api_key_here"
198
+ }
199
+ }
200
+ ```
201
+
202
+ Environment variables take precedence over `settings.json` when set:
203
+
204
+ ```sh
205
+ EXA_API_KEY='api_key_here' FIRECRAWL_API_KEY='api_key_here' JINA_API_KEY='api_key_here' pim
206
+ ```
207
+
208
+ ### Recommended TUI Settings (Optional)
209
+
210
+ Add the following settings to your `~/.pi/agent/settings.json` for the best experience with Pim:
211
+
212
+ ```json
213
+ {
214
+ "quietStartup": true,
215
+ "editorPaddingX": 1,
216
+ "markdown": {
217
+ "codeBlockIndent": ""
218
+ }
219
+ }
220
+ ```
221
+
222
+ ## Why Pim?
223
+
224
+ Pim's philosophy is **opinionated but minimal**. Its goal is to improve the out-of-the-box experience for both users and agents, without sacrificing composability with other Pi extensions.
225
+
226
+ ### Pi Core
227
+
228
+ Think of Pim as an opinionated, batteries-included distro of Pi (like what Ubuntu is to Linux). Pim uses Pi in its core, and everything Pi does, it continues to do:
229
+
230
+ - **Every Pi extension still works.** Pim registers its own extensions in-process, so third-party extensions from your Pi settings load right alongside them, exactly as before.
231
+ - **Pi's CLI, sessions and config are unchanged.** Pim reads and writes pi's own session files, so a conversation started in pi resumes in pim and back again. Only Pim's own settings live separately in `~/.pim/settings.json`.
232
+ - **Your vanilla `pi` keeps working.** Pim never registers itself with Pi and never touches your Pi settings, so `pi` and `pim` can both be used on the same machine.
233
+
234
+ ### Lean System Prompt
235
+
236
+ Pim's system prompt is **under 3K tokens** despite exposing 10+ tools, far leaner than alternatives like OpenCode (~10K), Hermes (~16K), or Claude Code (~30K).
237
+
238
+ This is achieved by having tool descriptions focus on _how_ to use each tool instead of prescribing _when_, since models already appear to internally encode when tools are needed, and prompting them to call tools can [suppress both necessary and unnecessary calls](https://arxiv.org/abs/2605.09252).
239
+
240
+ ### Model-Aware Tools
241
+
242
+ LLMs are [increasingly post-trained](https://openai.com/index/introducing-codex) for specific agent harnesses, making tool schemas part of the model's learned interface. For text-file editing, Anthropic models are trained to use [string replacement operations](https://platform.claude.com/docs/en/agents-and-tools/tool-use/text-editor-tool), while OpenAI models use [V4A patch operations](https://developers.openai.com/api/docs/guides/tools-apply-patch).
243
+
244
+ Pim keeps the active toolset model-aware instead of assuming one tool fits every LLM. It dynamically exposes the tools best suited to the selected model, giving each model the interface that best matches its learned behaviour while keeping the prompt lean.
245
+
246
+ ## Benchmarks
247
+
248
+ ### Terminal-Bench 2.0
249
+
250
+ | ID | Pim Version | LLM / Model | Results |
251
+ | --- | --- | --- | --- |
252
+ | [r1](./benchmarks/terminal_bench_2/results/r1/) | [`21d084d1`](https://github.com/AaronCQL/pim-agent/tree/21d084d1) | `Qwen3.6-35B-A3B-UD-Q6_K_XL.gguf` | **41.6%** (37/89) |
253
+ | [r2](./benchmarks/terminal_bench_2/results/r2/) | [`bfd792cf`](https://github.com/AaronCQL/pim-agent/tree/bfd792cf) | `Qwen3.6-35B-A3B-UD-Q6_K_XL.gguf` | **36.0%** (32/89) |
254
+ | [r3](./benchmarks/terminal_bench_2/results/r3/) | [`cd52f3a4`](https://github.com/AaronCQL/pim-agent/tree/cd52f3a4) | `Qwen3.6-35B-A3B-UD-Q6_K_XL.gguf` | **36.0%** (32/89) |
255
+
256
+ Preliminary aggregate score of **37.8%** from 3 independent runs. Each ran on an incremental build of Pim, though changes between runs were minor and none were tuned to the benchmark. Pim's `subagent` tool was disabled for all runs to keep each trial single-agent.
257
+
258
+ On average, Pim solves **~54% more tasks** than [little-coder](https://github.com/itayinbarr/little-coder) with the same Qwen3.6-35B model (37.8% vs 24.6%). This also places Pim in a similar tier to Claude Code + Sonnet 4.5 (40.1%), and above Codex + GPT-5-Mini (31.9%).
259
+
260
+ The Qwen3.6-35B model is hosted via llama.cpp on an M4 Pro 48GB MacBook, with the following config:
261
+
262
+ ```sh
263
+ llama-server \
264
+ -c 131072 \
265
+ -ngl 99 \
266
+ --slot-save-path /tmp/llama-slots \
267
+ --flash-attn on \
268
+ --cache-type-k q8_0 \
269
+ --cache-type-v q8_0 \
270
+ --jinja \
271
+ --temp 0.6 \
272
+ --top-p 0.95 \
273
+ --top-k 20 \
274
+ --min-p 0.0 \
275
+ --presence-penalty 0.0 \
276
+ --repeat-penalty 1.0 \
277
+ --reasoning-budget 16384 \
278
+ --reasoning-budget-message "Alright, I've thought enough. Let me take the next concrete step now — either a tool call or a final answer — and refine based on what I learn." \
279
+ -np 1
280
+ ```
281
+
282
+ _Note 1_: results are preliminary as only 3 independent full runs were conducted; Terminal-Bench 2.0 requires 5 independent full runs under a fixed configuration for an official score.
283
+
284
+ _Note 2_: the gap with little-coder may be partly explained by different inference configs (128K context vs 32K, Q6_K_XL vs Q4_K_M, higher thinking budget, etc.).
285
+
286
+ _Note 3_: in r1 and r3, the `code-from-image` trial was counted as non-passing because Qwen autonomously searched for the answer online after legitimately trying for a while.
287
+
288
+ _Note 4_: see the [`benchmarks/terminal_bench_2`](./benchmarks/terminal_bench_2/) dir for breakdown of results and reproduction steps.
289
+
290
+ ## Changelog
291
+
292
+ See [CHANGELOG.md](./CHANGELOG.md) for release notes.
293
+
294
+ ## Developing
295
+
296
+ See [AGENTS.md](./AGENTS.md) for the developer guide.
package/bin/pim.ts ADDED
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env bun
2
+ import { Readable } from "node:stream";
3
+
4
+ import { main } from "@earendil-works/pi-coding-agent";
5
+
6
+ import {
7
+ CoreExtensions,
8
+ type PimInlineExtension,
9
+ } from "#core/extensions/CoreExtensions";
10
+ import { ExtensionToggles } from "#core/shared/ExtensionToggles";
11
+ import { PimVersion } from "#core/shared/PimVersion";
12
+ import { Supervisor } from "#core/shared/Supervisor";
13
+ import { Updater } from "#core/shared/Updater";
14
+ import init from "#tui/extensions/_init/index";
15
+ import commandPicker from "#tui/extensions/command-picker/index";
16
+ import filePicker from "#tui/extensions/file-picker/index";
17
+ import footer from "#tui/extensions/footer/index";
18
+ import pim from "#tui/extensions/pim/index";
19
+ import sessionLease from "#tui/extensions/session-lease/index";
20
+ import tps from "#tui/extensions/tps/index";
21
+ import workingIndicator from "#tui/extensions/working-indicator/index";
22
+ import { themeCliArgs } from "#tui/themes/themeCliArgs";
23
+
24
+ // `_init` must stay first: it installs the runtime guard.
25
+ const extensionFactories: readonly PimInlineExtension[] = [
26
+ { name: "_init", factory: init },
27
+ ...CoreExtensions.list,
28
+ { name: "command-picker", factory: commandPicker },
29
+ { name: "file-picker", factory: filePicker },
30
+ { name: "footer", factory: footer },
31
+ { name: "pim", factory: pim },
32
+ { name: "session-lease", factory: sessionLease },
33
+ { name: "tps", factory: tps },
34
+ { name: "working-indicator", factory: workingIndicator },
35
+ ];
36
+
37
+ async function runUpdate(force: boolean): Promise<number> {
38
+ const at = await Supervisor.detectInstall();
39
+ if (!force && at.kind === "prod") {
40
+ const [installed, available] = await Promise.all([
41
+ PimVersion.current(),
42
+ PimVersion.latest({ timeoutMs: 10_000 }),
43
+ ]);
44
+ if (available === undefined) {
45
+ console.error("Could not determine the latest pim version.");
46
+ return 1;
47
+ }
48
+ if (!PimVersion.isNewer(available, installed)) {
49
+ console.log(`pim is already up to date (v${installed})`);
50
+ return 0;
51
+ }
52
+ }
53
+ const outcome = await Updater.run({
54
+ onStep: (label) => console.log(`[update] ${label}`),
55
+ });
56
+ for (const skip of outcome.skipped) {
57
+ console.log(`[update] skipped ${skip.label}: ${skip.reason}`);
58
+ }
59
+ if (!outcome.ok) {
60
+ console.error(outcome.error);
61
+ return 1;
62
+ }
63
+ console.log(
64
+ outcome.to === outcome.from
65
+ ? `pim is on v${outcome.to}`
66
+ : `Updated pim from v${outcome.from} to v${outcome.to}`
67
+ );
68
+ return 0;
69
+ }
70
+
71
+ const cliArgs = process.argv.slice(2);
72
+
73
+ // Pi's argparse rejects prompts starting with `-` and ignores `--`; route them through stdin.
74
+ const dashDashIdx = cliArgs.indexOf("--");
75
+ let promptViaStdin: string | undefined;
76
+ if (dashDashIdx >= 0) {
77
+ promptViaStdin = cliArgs.slice(dashDashIdx + 1).join(" ");
78
+ cliArgs.length = dashDashIdx;
79
+ }
80
+
81
+ if (cliArgs.includes("--version") || cliArgs.includes("-v")) {
82
+ console.log(
83
+ `pim ${await PimVersion.current()} (pi ${await PimVersion.pi()})`
84
+ );
85
+ process.exit(0);
86
+ }
87
+
88
+ // Pi's `update` cannot move the pi inside pim's install tree; only reinstalling pim can.
89
+ if (cliArgs[0] === "update") {
90
+ const rest = cliArgs.slice(1);
91
+ const isSelf = rest.every(
92
+ (arg) => arg === "self" || arg === "pim" || arg === "--force"
93
+ );
94
+ if (isSelf) {
95
+ process.exit(await runUpdate(rest.includes("--force")));
96
+ }
97
+ }
98
+
99
+ // Pi's banner points at `pi update`, which cannot reach the bundled copy.
100
+ process.env["PI_SKIP_VERSION_CHECK"] = "1";
101
+
102
+ const modeIdx = cliArgs.findIndex(
103
+ (a) => a === "--mode" || a.startsWith("--mode=")
104
+ );
105
+ const mode =
106
+ modeIdx >= 0
107
+ ? cliArgs[modeIdx]!.includes("=")
108
+ ? cliArgs[modeIdx]!.split("=")[1]
109
+ : cliArgs[modeIdx + 1]
110
+ : undefined;
111
+ // Writing a unit file must not load the frontend it describes; keep these imports lazy.
112
+ const daemonAction = cliArgs.includes("--install")
113
+ ? "install"
114
+ : cliArgs.includes("--uninstall")
115
+ ? "uninstall"
116
+ : undefined;
117
+ // `--mode web` and `--mode telegram` are the single-surface spellings of the daemon.
118
+ if (mode === "daemon" || mode === "web" || mode === "telegram") {
119
+ if (daemonAction !== undefined) {
120
+ const { DaemonInstall } = await import("#daemon/DaemonInstall");
121
+ await (daemonAction === "install"
122
+ ? DaemonInstall.install(cliArgs)
123
+ : DaemonInstall.uninstall());
124
+ process.exit(0);
125
+ }
126
+ const { start } = await import("#daemon/index");
127
+ await start(cliArgs);
128
+ process.exit(0);
129
+ }
130
+
131
+ if (promptViaStdin !== undefined) {
132
+ // Pi's `readPipedStdin()` only checks `isTTY` and drains, so an in-memory Readable passes as a pipe.
133
+ Object.defineProperty(process, "stdin", {
134
+ value: Readable.from([promptViaStdin]),
135
+ configurable: true,
136
+ });
137
+ }
138
+
139
+ // Pi's `cli.js` preamble, minus `configureHttpDispatcher()`, which `main` calls itself.
140
+ process.title = "pi";
141
+ process.env["PI_CODING_AGENT"] = "true";
142
+ process.env["AI_AGENT"] = "pi";
143
+ process.emitWarning = () => {};
144
+
145
+ // Pi never applies its `enabled` filter to inline factories, so the toggle must gate inside the factory.
146
+ await main([...themeCliArgs(cliArgs), ...cliArgs], {
147
+ extensionFactories: extensionFactories.map(({ name, factory }) => ({
148
+ name,
149
+ factory: ExtensionToggles.gate(name, factory),
150
+ })),
151
+ });
package/package.json CHANGED
@@ -1,9 +1,87 @@
1
1
  {
2
2
  "name": "pim-agent",
3
- "version": "0.0.1",
4
- "description": "Placeholder — the real release lands as 0.9.0.",
3
+ "version": "0.9.0",
4
+ "description": "A batteries-included distro of Pi, accessible in your terminal, browser and Telegram.",
5
5
  "license": "MIT",
6
+ "type": "module",
7
+ "keywords": [
8
+ "agent"
9
+ ],
6
10
  "homepage": "https://github.com/AaronCQL/pim-agent#readme",
7
- "repository": { "type": "git", "url": "git+https://github.com/AaronCQL/pim-agent.git" },
8
- "files": ["README.md"]
11
+ "bugs": {
12
+ "url": "https://github.com/AaronCQL/pim-agent/issues"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/AaronCQL/pim-agent.git"
17
+ },
18
+ "bin": {
19
+ "pim": "bin/pim.ts"
20
+ },
21
+ "imports": {
22
+ "#core/*": "./packages/core/src/*.ts",
23
+ "#daemon/*": "./packages/daemon/src/*.ts",
24
+ "#tui/*": "./packages/tui/src/*.ts",
25
+ "#telegram/*": "./packages/telegram/src/*.ts",
26
+ "#protocol/*": "./packages/protocol/src/*.ts",
27
+ "#server/*": "./packages/server/src/*.ts",
28
+ "#web/*": "./packages/web/src/*.ts"
29
+ },
30
+ "files": [
31
+ "bin/",
32
+ "packages/core/src/",
33
+ "packages/daemon/src/",
34
+ "packages/tui/src/",
35
+ "packages/telegram/src/",
36
+ "packages/protocol/src/",
37
+ "packages/server/src/",
38
+ "packages/web/dist/client/",
39
+ "!packages/core/src/**/fixtures/",
40
+ "!packages/**/*.test.ts"
41
+ ],
42
+ "engines": {
43
+ "bun": ">=1.2.7"
44
+ },
45
+ "pi": {
46
+ "image": "https://raw.githubusercontent.com/AaronCQL/pim-agent/refs/heads/main/assets/hero.webp"
47
+ },
48
+ "scripts": {
49
+ "dev": "bun link && pim",
50
+ "prepack": "bun run web:build",
51
+ "probe": "bun packages/server/src/probe.ts",
52
+ "check": "bun scripts/check.ts",
53
+ "gen:brand": "bun scripts/brand/generate.ts",
54
+ "gen:hero": "bun scripts/hero/generate.ts",
55
+ "web:build": "cd packages/web && vite build",
56
+ "web:dev": "cd packages/web && vite"
57
+ },
58
+ "devDependencies": {
59
+ "@fontsource/commit-mono": "^5.3.0",
60
+ "@happy-dom/global-registrator": "^20.14.0",
61
+ "@iconify-json/griddy-icons": "^1.2.0",
62
+ "@iconify-json/solar": "^1.2.10",
63
+ "@jsquash/webp": "^1.5.0",
64
+ "@resvg/resvg-js": "^2.6.2",
65
+ "@solidjs/vite-plugin": "3.0.0-next.40",
66
+ "@solidjs/web": "^2.0.0-rc.7",
67
+ "@types/bun": "latest",
68
+ "happy-dom": "^20.14.0",
69
+ "highlight.js": "^11.12.0",
70
+ "oxfmt": "0.67.0",
71
+ "oxlint": "^1.82.0",
72
+ "solid-js": "^2.0.0-rc.7",
73
+ "streaming-markdown": "^0.2.15",
74
+ "typescript": "^7.0.2",
75
+ "unocss": "^66.10.0",
76
+ "vite": "^8.2.2"
77
+ },
78
+ "dependencies": {
79
+ "@earendil-works/pi-coding-agent": "0.85.1",
80
+ "@earendil-works/pi-server": "0.85.1",
81
+ "diff": "^9.0.0",
82
+ "fzf": "^0.5.2",
83
+ "grammy": "^1.46.0",
84
+ "ignore": "^7.0.9",
85
+ "ky": "^2.1.0"
86
+ }
9
87
  }
@@ -0,0 +1,99 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import { basename, extname } from "node:path";
3
+
4
+ import { SafePath } from "../shared/SafePath";
5
+
6
+ /** A file the store has taken a copy of, under a name it chose. */
7
+ export type StoredFile = {
8
+ /** Names the file within its scope; what a client sends back on a prompt. */
9
+ readonly id: string;
10
+ /** Absolute path on the server. The only path the agent is ever told. */
11
+ readonly path: string;
12
+ readonly mimeType: string;
13
+ };
14
+
15
+ /** A file that now lives on the machine the agent runs on. */
16
+ export type StoredAttachment = StoredFile & {
17
+ /** Present only for images small enough to inline into the prompt. */
18
+ readonly imageBase64: string | undefined;
19
+ };
20
+
21
+ export type AttachmentInput = {
22
+ readonly bytes: ArrayBuffer;
23
+ readonly mimeType: string;
24
+ /** Client-supplied filename. Only its basename is ever used. */
25
+ readonly name?: string;
26
+ /** Stem of the stored filename; a random one when omitted. */
27
+ readonly stem?: string;
28
+ /** Extension to fall back on when `name` carries none. */
29
+ readonly ext?: string;
30
+ };
31
+
32
+ /** Above this, an image is referenced by path instead of inlined. */
33
+ const IMAGE_BYTES_LIMIT = 4 * 1024 * 1024;
34
+
35
+ /** Materialises client bytes onto the server's filesystem; the agent is only ever told a server path. */
36
+ export class AttachmentStore {
37
+ public constructor(private readonly root: string) {}
38
+
39
+ public async store(
40
+ scope: string,
41
+ input: AttachmentInput
42
+ ): Promise<StoredAttachment> {
43
+ const { id, path } = await this.place(
44
+ scope,
45
+ input.stem,
46
+ extensionOf(input)
47
+ );
48
+ await Bun.write(path, input.bytes);
49
+
50
+ const isImage = input.mimeType.startsWith("image/");
51
+ return {
52
+ id,
53
+ path,
54
+ mimeType: input.mimeType,
55
+ imageBase64:
56
+ isImage && input.bytes.byteLength <= IMAGE_BYTES_LIMIT
57
+ ? Buffer.from(input.bytes).toString("base64")
58
+ : undefined,
59
+ };
60
+ }
61
+
62
+ /** Copies a file already on the agent's disk into the store so it can be served. */
63
+ public async storeFile(scope: string, source: string): Promise<StoredFile> {
64
+ const file = Bun.file(source);
65
+ const name = basename(source);
66
+ const ext = extname(name);
67
+ // Keep `basename(name, ext)`: a regex strip leaves a dotfile like `.bashrc` with no name.
68
+ const { id, path } = await this.place(scope, basename(name, ext), ext);
69
+ await Bun.write(path, file);
70
+ return { id, path, mimeType: file.type || "application/octet-stream" };
71
+ }
72
+
73
+ private async place(
74
+ scope: string,
75
+ stem: string | undefined,
76
+ ext: string
77
+ ): Promise<{ readonly id: string; readonly path: string }> {
78
+ const dir = this.scopeDir(scope);
79
+ await mkdir(dir, { recursive: true });
80
+ const id = SafePath.safeName(
81
+ `${stem || Bun.randomUUIDv7()}-${Date.now()}${ext}`
82
+ );
83
+ return { id, path: SafePath.contain(dir, id) };
84
+ }
85
+
86
+ private scopeDir(scope: string): string {
87
+ return SafePath.contain(this.root, SafePath.safeName(scope));
88
+ }
89
+
90
+ /** Where `store` put this id; the id is re-sanitised, so it can never name a file outside its scope. */
91
+ public locate(scope: string, id: string): string {
92
+ return SafePath.contain(this.scopeDir(scope), SafePath.safeName(id));
93
+ }
94
+ }
95
+
96
+ function extensionOf(input: AttachmentInput): string {
97
+ const fromName = extname(basename(input.name ?? ""));
98
+ return fromName || input.ext || "";
99
+ }