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,86 @@
1
+ import { Lines } from "../../shared/Lines";
2
+ import {
3
+ type CapturedStream,
4
+ STREAM_HEAD_BYTES,
5
+ STREAM_TAIL_BYTES,
6
+ } from "./schema";
7
+
8
+ export class StreamCapture {
9
+ private chunks: Uint8Array[] = [];
10
+ private totalBytesAccum = 0;
11
+ private fullBytes: Uint8Array | null = null;
12
+
13
+ push(chunk: Uint8Array): void {
14
+ if (chunk.byteLength === 0) {
15
+ return;
16
+ }
17
+ this.chunks.push(chunk);
18
+ this.totalBytesAccum += chunk.byteLength;
19
+ }
20
+
21
+ get totalBytes(): number {
22
+ return this.totalBytesAccum;
23
+ }
24
+
25
+ /** The first `n` bytes, read across however many chunks the kernel split them into. */
26
+ lead(n: number): Uint8Array {
27
+ if (this.fullBytes) {
28
+ return this.fullBytes.subarray(0, n);
29
+ }
30
+ const lead = new Uint8Array(Math.min(n, this.totalBytesAccum));
31
+ let at = 0;
32
+ for (const chunk of this.chunks) {
33
+ if (at === lead.byteLength) {
34
+ break;
35
+ }
36
+ const take = chunk.subarray(0, lead.byteLength - at);
37
+ lead.set(take, at);
38
+ at += take.byteLength;
39
+ }
40
+ return lead;
41
+ }
42
+
43
+ full(): Uint8Array {
44
+ if (!this.fullBytes) {
45
+ this.fullBytes = Buffer.concat(this.chunks, this.totalBytesAccum);
46
+ this.chunks = [];
47
+ }
48
+ return this.fullBytes;
49
+ }
50
+
51
+ /** Opaque bytes are not text: a half-image is worthless, so they are neither cut nor decoded. */
52
+ snapshot(opaque = false): CapturedStream {
53
+ if (this.totalBytesAccum === 0 || opaque) {
54
+ return {
55
+ text: "",
56
+ totalBytes: this.totalBytesAccum,
57
+ truncated: false,
58
+ path: null,
59
+ nextStart: null,
60
+ };
61
+ }
62
+ const dec = new TextDecoder();
63
+ const all = this.full();
64
+ if (this.totalBytesAccum <= STREAM_HEAD_BYTES + STREAM_TAIL_BYTES) {
65
+ return {
66
+ text: dec.decode(all),
67
+ totalBytes: this.totalBytesAccum,
68
+ truncated: false,
69
+ path: null,
70
+ nextStart: null,
71
+ };
72
+ }
73
+ const headText = dec.decode(all.subarray(0, STREAM_HEAD_BYTES));
74
+ const tailText = dec.decode(
75
+ all.subarray(all.byteLength - STREAM_TAIL_BYTES)
76
+ );
77
+ const middle = this.totalBytesAccum - STREAM_HEAD_BYTES - STREAM_TAIL_BYTES;
78
+ return {
79
+ text: `${headText}\n... ${middle} bytes truncated ...\n${tailText}`,
80
+ totalBytes: this.totalBytesAccum,
81
+ truncated: true,
82
+ path: null,
83
+ nextStart: Lines.continuationLine(headText),
84
+ };
85
+ }
86
+ }
@@ -0,0 +1,110 @@
1
+ import { Format } from "../../shared/Format";
2
+ import { type ImageMimeType, Images } from "../../shared/Images";
3
+ import {
4
+ type BashCommandResult,
5
+ type BashDetails,
6
+ type CapturedStream,
7
+ STREAM_HEAD_BYTES,
8
+ STREAM_TAIL_BYTES,
9
+ } from "./schema";
10
+
11
+ const STREAMS = ["stdout", "stderr"] as const;
12
+
13
+ export function stripTrailingNewline(s: string): string {
14
+ return s.endsWith("\n") ? s.slice(0, -1) : s;
15
+ }
16
+
17
+ export function formatTruncationAffordance(
18
+ label: string,
19
+ s: CapturedStream
20
+ ): string {
21
+ const base = `[bash tool: ${label} showing first ${STREAM_HEAD_BYTES} bytes + last ${STREAM_TAIL_BYTES} bytes of ${s.totalBytes}`;
22
+ if (s.path) {
23
+ return `${base}; use read with path=${s.path} and start=${s.nextStart} for the rest.]`;
24
+ }
25
+ return `${base}; redirect to a file (e.g. \`cmd > /tmp/out.log\`) and use read for the full output.]`;
26
+ }
27
+
28
+ export function formatResult(
29
+ result: BashCommandResult,
30
+ timeoutMs: number
31
+ ): string {
32
+ const lines: string[] = [`Exit code: ${result.exitCode ?? "none"}`];
33
+ if (result.signal !== null) {
34
+ lines.push(`Signal: ${result.signal}`);
35
+ }
36
+ if (result.aborted) {
37
+ lines.push("Aborted.");
38
+ } else if (result.timedOut) {
39
+ lines.push(`Timed out after ${timeoutMs} ms.`);
40
+ }
41
+ for (const label of STREAMS) {
42
+ const stream = result[label];
43
+ if (stream.totalBytes === 0) {
44
+ continue;
45
+ }
46
+ if (label === "stdout" && result.stdoutSniffed !== null) {
47
+ const line = unshownImageLine(
48
+ result,
49
+ result.stdoutSniffed,
50
+ stream.totalBytes
51
+ );
52
+ if (line !== null) {
53
+ lines.push(line);
54
+ }
55
+ continue;
56
+ }
57
+ lines.push(`${label}:`);
58
+ lines.push(stripTrailingNewline(stream.text));
59
+ if (stream.truncated) {
60
+ lines.push(formatTruncationAffordance(label, stream));
61
+ }
62
+ }
63
+ return lines.join("\n");
64
+ }
65
+
66
+ /** The words for a picture the content array will not carry; null when it will. */
67
+ function unshownImageLine(
68
+ result: BashCommandResult,
69
+ sniffed: ImageMimeType,
70
+ totalBytes: number
71
+ ): string | null {
72
+ const subject = `stdout is ${Format.bytes(totalBytes)} of ${Images.extensionOf(sniffed)} data`;
73
+ if (isErrorResult(result)) {
74
+ return `[bash tool: ${subject}, not shown because the command failed.]`;
75
+ }
76
+ if (result.stdoutImage === null) {
77
+ return `[bash tool: ${subject} that could not be decoded as an image.]`;
78
+ }
79
+ return null;
80
+ }
81
+
82
+ export function isErrorResult(
83
+ result: Pick<BashCommandResult, "exitCode" | "timedOut" | "aborted">
84
+ ): boolean {
85
+ return result.aborted || result.timedOut || result.exitCode !== 0;
86
+ }
87
+
88
+ export function detailsOf(result: BashCommandResult): BashDetails {
89
+ return {
90
+ exitCode: result.exitCode,
91
+ signal: result.signal,
92
+ durationMs: result.durationMs,
93
+ timedOut: result.timedOut,
94
+ aborted: result.aborted,
95
+ stdout: streamDetails(result.stdout),
96
+ stderr: streamDetails(result.stderr),
97
+ image:
98
+ result.stdoutImage === null
99
+ ? undefined
100
+ : Images.detailsOf(result.stdoutImage),
101
+ };
102
+ }
103
+
104
+ function streamDetails(stream: CapturedStream): BashDetails["stdout"] {
105
+ return {
106
+ totalBytes: stream.totalBytes,
107
+ truncated: stream.truncated,
108
+ path: stream.path,
109
+ };
110
+ }
@@ -0,0 +1,31 @@
1
+ import { Format } from "../../shared/Format";
2
+ import { Images, type NormalisedImage } from "../../shared/Images";
3
+
4
+ /**
5
+ * The picture stdout printed, once a provider will take it. Bytes that sniffed
6
+ * as an image but cannot be decoded are not an error here: the command ran, and
7
+ * bash reports what it printed.
8
+ */
9
+ export async function normaliseStdoutImage(
10
+ bytes: Uint8Array
11
+ ): Promise<NormalisedImage | null> {
12
+ try {
13
+ return await Images.normalise(bytes, "stdout");
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+
19
+ /** `stdout is a 1200x800 png (240 KB)`, the subject every note about the picture shares. */
20
+ export function imageSubject(image: NormalisedImage): string {
21
+ return `stdout is a ${image.width}x${image.height} ${Images.extensionOf(image.mimeType)} (${Format.bytes(image.bytes)})`;
22
+ }
23
+
24
+ /** What the model is told next to the picture, or instead of it when it has no eyes. */
25
+ export function imageNote(image: NormalisedImage, vision: boolean): string {
26
+ if (!vision) {
27
+ return Images.noVisionNote("bash", imageSubject(image));
28
+ }
29
+ const note = Images.noteOf(image);
30
+ return `[bash tool: ${imageSubject(image)}, shown as an image.${note ? ` ${note}` : ""}]`;
31
+ }
@@ -0,0 +1,88 @@
1
+ import type {
2
+ AgentToolResult,
3
+ ExtensionAPI,
4
+ } from "@earendil-works/pi-coding-agent";
5
+ import { Images } from "../../shared/Images";
6
+ import { SpillCache } from "../../shared/SpillCache";
7
+ import { Tools } from "../../shared/Tools";
8
+ import { detailsOf, formatResult, isErrorResult } from "./format";
9
+ import { imageNote } from "./image";
10
+ import { bashView } from "./render";
11
+ import { killAllActiveBashGroups, runBashCommand } from "./run";
12
+ import {
13
+ type BashDetails,
14
+ type BashInput,
15
+ bashSchema,
16
+ DEFAULT_TIMEOUT_MS,
17
+ } from "./schema";
18
+
19
+ let lifecycleHandlersInstalled = false;
20
+
21
+ function installLifecycleHandlers(): void {
22
+ if (lifecycleHandlersInstalled) {
23
+ return;
24
+ }
25
+ lifecycleHandlersInstalled = true;
26
+
27
+ // Sweep escaped bash subtrees, then re-raise so the default handler still runs.
28
+ for (const sig of ["SIGTERM", "SIGINT", "SIGHUP"] as const) {
29
+ process.once(sig, () => {
30
+ try {
31
+ killAllActiveBashGroups(sig);
32
+ } catch {}
33
+ process.kill(process.pid, sig);
34
+ });
35
+ }
36
+ }
37
+
38
+ export default function (pi: ExtensionAPI): void {
39
+ SpillCache.installSweeper();
40
+ installLifecycleHandlers();
41
+ Tools.register<typeof bashSchema, BashDetails>(pi, {
42
+ name: "bash",
43
+ label: "bash",
44
+ description:
45
+ "Execute a bash command in the cwd. " +
46
+ "Returns exit code, signal (if any), and stdout/stderr captured separately. " +
47
+ "Stdout that is a png/jpeg/gif/webp is returned as a picture. " +
48
+ "Prefer commands that emit only what you need; keep output as small as possible.",
49
+ parameters: bashSchema,
50
+ renderShell: "self",
51
+ effect: { kind: "unbounded" },
52
+ executionMode: "sequential",
53
+ async execute(_id, params, signal, _onUpdate, ctx) {
54
+ const { command, timeoutMs: requestedTimeoutMs } = params as BashInput;
55
+ const timeoutMs = requestedTimeoutMs ?? DEFAULT_TIMEOUT_MS;
56
+
57
+ if (signal?.aborted) {
58
+ throw new Error("Command aborted before execution.");
59
+ }
60
+
61
+ const result = await runBashCommand(command, timeoutMs, signal, ctx.cwd);
62
+ const text = formatResult(result, timeoutMs);
63
+ if (isErrorResult(result)) {
64
+ throw new Error(text);
65
+ }
66
+
67
+ const content: AgentToolResult<BashDetails>["content"] = [
68
+ { type: "text", text },
69
+ ];
70
+ const image = result.stdoutImage;
71
+ if (image !== null) {
72
+ const vision = Images.canSee(ctx.model);
73
+ const note = imageNote(image, vision);
74
+ content.push(
75
+ ...(vision
76
+ ? Images.contentOf(image, note)
77
+ : [{ type: "text" as const, text: note }])
78
+ );
79
+ }
80
+
81
+ return {
82
+ content,
83
+ details: detailsOf(result),
84
+ };
85
+ },
86
+ toViewModel: bashView,
87
+ });
88
+ }
@@ -0,0 +1,31 @@
1
+ import { Renderer } from "../../shared/Renderer";
2
+ import type { ToolViewInput } from "../../shared/Tools";
3
+ import { Painting } from "../../view/Painting";
4
+ import type { ToolView } from "../../view/ViewBlock";
5
+ import type { BashDetails, bashSchema } from "./schema";
6
+
7
+ type BashViewInput = ToolViewInput<typeof bashSchema, BashDetails>;
8
+
9
+ export function bashView({ args, result }: BashViewInput): ToolView {
10
+ const command = commandTitle(args?.command);
11
+ const image = result?.details?.image;
12
+ return {
13
+ label: "Bash",
14
+ icon: "terminal",
15
+ // `spans`, not `text`: a `text` title is split on newlines and rejoined with spaces.
16
+ title: [
17
+ {
18
+ kind: "spans",
19
+ spans: [{ text: command, code: true }],
20
+ },
21
+ ],
22
+ body: [
23
+ ...(image ? Painting.imageBlocks(image, command) : []),
24
+ { kind: "text", text: Renderer.firstText(result) },
25
+ ],
26
+ };
27
+ }
28
+
29
+ function commandTitle(command: unknown): string {
30
+ return typeof command === "string" && command !== "" ? command : "...";
31
+ }
@@ -0,0 +1,221 @@
1
+ import { type ImageMimeType, Images } from "../../shared/Images";
2
+ import { SpillCache } from "../../shared/SpillCache";
3
+ import { StreamCapture } from "./capture";
4
+ import { isErrorResult } from "./format";
5
+ import { normaliseStdoutImage } from "./image";
6
+ import {
7
+ type BashCommandResult,
8
+ type CapturedStream,
9
+ DRAIN_GRACE_MS,
10
+ KILL_GRACE_MS,
11
+ } from "./schema";
12
+
13
+ type Reader = ReadableStreamDefaultReader<Uint8Array>;
14
+
15
+ const activePids = new Set<number>();
16
+
17
+ export function killAllActiveBashGroups(sig: NodeJS.Signals = "SIGTERM"): void {
18
+ for (const pid of activePids) {
19
+ killGroup(pid, sig);
20
+ }
21
+ activePids.clear();
22
+ }
23
+
24
+ async function drain(reader: Reader | null, cap: StreamCapture): Promise<void> {
25
+ if (!reader) {
26
+ return;
27
+ }
28
+ try {
29
+ while (true) {
30
+ const { value, done } = await reader.read();
31
+ if (done) {
32
+ break;
33
+ }
34
+ if (value) {
35
+ cap.push(value);
36
+ }
37
+ }
38
+ } catch {
39
+ } finally {
40
+ try {
41
+ reader.releaseLock();
42
+ } catch {}
43
+ }
44
+ }
45
+
46
+ async function spillIfTruncated(
47
+ stream: CapturedStream,
48
+ cap: StreamCapture,
49
+ ext: "out" | "err"
50
+ ): Promise<string | null> {
51
+ return stream.truncated ? SpillCache.write("bash", ext, cap.full()) : null;
52
+ }
53
+
54
+ /**
55
+ * The picture stdout printed, or null. The verdict is the leading bytes',
56
+ * before anything is decoded; the cap is the whole stream's, so an image too
57
+ * big to send is text again and truncates like any other output.
58
+ */
59
+ export function sniffStdoutImage(cap: StreamCapture): ImageMimeType | null {
60
+ if (
61
+ cap.totalBytes < Images.SNIFF_BYTES ||
62
+ cap.totalBytes > Images.MAX_SOURCE_BYTES
63
+ ) {
64
+ return null;
65
+ }
66
+ return Images.sniff(cap.lead(Images.SNIFF_BYTES));
67
+ }
68
+
69
+ function killGroup(pid: number | undefined, sig: NodeJS.Signals): void {
70
+ if (pid === undefined) {
71
+ return;
72
+ }
73
+ try {
74
+ process.kill(-pid, sig);
75
+ } catch {
76
+ try {
77
+ process.kill(pid, sig);
78
+ } catch {}
79
+ }
80
+ }
81
+
82
+ function getReader(
83
+ stream: ReadableStream<Uint8Array> | undefined
84
+ ): Reader | null {
85
+ return stream ? stream.getReader() : null;
86
+ }
87
+
88
+ export async function runBashCommand(
89
+ command: string,
90
+ timeoutMs: number,
91
+ signal: AbortSignal | undefined,
92
+ cwd: string
93
+ ): Promise<BashCommandResult> {
94
+ const startedAt = Date.now();
95
+ const stdoutCap = new StreamCapture();
96
+ const stderrCap = new StreamCapture();
97
+
98
+ // setsid gives the tree its own process group (pgid == proc.pid) so the whole tree can be signalled.
99
+ const proc = Bun.spawn({
100
+ cmd: ["setsid", "bash", "-lc", command],
101
+ cwd,
102
+ stdout: "pipe",
103
+ stderr: "pipe",
104
+ env: { ...process.env },
105
+ });
106
+ if (proc.pid !== undefined) {
107
+ activePids.add(proc.pid);
108
+ }
109
+
110
+ let timedOut = false;
111
+ let aborted = false;
112
+
113
+ // Hold the readers: cancelling a locked stream through the stream itself throws.
114
+ const stdoutReader = getReader(
115
+ proc.stdout as unknown as ReadableStream<Uint8Array>
116
+ );
117
+ const stderrReader = getReader(
118
+ proc.stderr as unknown as ReadableStream<Uint8Array>
119
+ );
120
+
121
+ // Never block on EOF: a backgrounded child can hold the pipes open after bash exits.
122
+ const stdoutDrain = drain(stdoutReader, stdoutCap);
123
+ const stderrDrain = drain(stderrReader, stderrCap);
124
+
125
+ let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
126
+ const exitedPromise = proc.exited.then(() => "exited" as const);
127
+ const timeoutPromise = new Promise<"timeout">((resolve) => {
128
+ timeoutHandle = setTimeout(() => resolve("timeout"), timeoutMs);
129
+ });
130
+
131
+ const abort = Promise.withResolvers<"aborted">();
132
+ const onAbort = () => {
133
+ aborted = true;
134
+ abort.resolve("aborted");
135
+ };
136
+ if (signal) {
137
+ if (signal.aborted) {
138
+ onAbort();
139
+ } else {
140
+ signal.addEventListener("abort", onAbort, { once: true });
141
+ }
142
+ }
143
+
144
+ let exitCode: number | null = null;
145
+ let signalCode: NodeJS.Signals | null = null;
146
+ try {
147
+ const result = await Promise.race([
148
+ exitedPromise,
149
+ timeoutPromise,
150
+ abort.promise,
151
+ ]);
152
+
153
+ if (result === "timeout") {
154
+ timedOut = true;
155
+ }
156
+
157
+ if (result !== "exited") {
158
+ killGroup(proc.pid, "SIGTERM");
159
+ const sigkillTimer = setTimeout(() => {
160
+ killGroup(proc.pid, "SIGKILL");
161
+ }, KILL_GRACE_MS);
162
+ try {
163
+ await proc.exited;
164
+ } finally {
165
+ clearTimeout(sigkillTimer);
166
+ }
167
+ }
168
+
169
+ exitCode = proc.exitCode ?? null;
170
+ signalCode = (proc.signalCode as NodeJS.Signals | null | undefined) ?? null;
171
+
172
+ // Bound the drain: a detached grandchild holding the pipe would outlive this call.
173
+ await Promise.race([
174
+ Promise.all([stdoutDrain, stderrDrain]),
175
+ Bun.sleep(DRAIN_GRACE_MS),
176
+ ]);
177
+ } finally {
178
+ if (timeoutHandle) {
179
+ clearTimeout(timeoutHandle);
180
+ }
181
+ if (signal) {
182
+ signal.removeEventListener("abort", onAbort);
183
+ }
184
+ for (const reader of [stdoutReader, stderrReader]) {
185
+ if (!reader) {
186
+ continue;
187
+ }
188
+ try {
189
+ void reader.cancel().catch(() => {});
190
+ } catch {}
191
+ }
192
+ if (proc.pid !== undefined) {
193
+ activePids.delete(proc.pid);
194
+ }
195
+ }
196
+
197
+ // A picture nothing will show is never decoded: a failed command reports its bytes instead.
198
+ const sniffed = sniffStdoutImage(stdoutCap);
199
+ const shows =
200
+ sniffed !== null && !isErrorResult({ exitCode, timedOut, aborted });
201
+ const stdout = stdoutCap.snapshot(sniffed !== null);
202
+ const stderr = stderrCap.snapshot();
203
+
204
+ const [stdoutPath, stderrPath, stdoutImage] = await Promise.all([
205
+ spillIfTruncated(stdout, stdoutCap, "out"),
206
+ spillIfTruncated(stderr, stderrCap, "err"),
207
+ shows ? normaliseStdoutImage(stdoutCap.full()) : null,
208
+ ]);
209
+
210
+ return {
211
+ exitCode,
212
+ signal: signalCode,
213
+ stdout: { ...stdout, path: stdoutPath },
214
+ stderr: { ...stderr, path: stderrPath },
215
+ stdoutSniffed: sniffed,
216
+ stdoutImage,
217
+ timedOut,
218
+ aborted,
219
+ durationMs: Date.now() - startedAt,
220
+ };
221
+ }
@@ -0,0 +1,64 @@
1
+ import { type Static, Type } from "typebox";
2
+ import type {
3
+ ImageDetails,
4
+ ImageMimeType,
5
+ NormalisedImage,
6
+ } from "../../shared/Images";
7
+
8
+ export const STREAM_HEAD_BYTES = 8192;
9
+ export const STREAM_TAIL_BYTES = 8192;
10
+ export const DEFAULT_TIMEOUT_MS = 30_000;
11
+ export const KILL_GRACE_MS = 2000;
12
+ export const DRAIN_GRACE_MS = 1000;
13
+
14
+ export const bashSchema = Type.Object({
15
+ command: Type.String({
16
+ description: "Runs via bash -lc, so login shell init applies.",
17
+ }),
18
+ timeoutMs: Type.Optional(
19
+ Type.Integer({
20
+ minimum: 1,
21
+ description: `Timeout in milliseconds. Default is ${DEFAULT_TIMEOUT_MS} (${DEFAULT_TIMEOUT_MS / 1000}s) — raise it for long-running commands like builds, test suites, training runs, or installs.`,
22
+ })
23
+ ),
24
+ });
25
+ export type BashInput = Static<typeof bashSchema>;
26
+
27
+ export type CapturedStream = {
28
+ readonly text: string;
29
+ readonly totalBytes: number;
30
+ readonly truncated: boolean;
31
+ readonly path: string | null;
32
+ readonly nextStart: number | null;
33
+ };
34
+
35
+ export type BashCommandResult = {
36
+ readonly exitCode: number | null;
37
+ readonly signal: NodeJS.Signals | null;
38
+ readonly stdout: CapturedStream;
39
+ readonly stderr: CapturedStream;
40
+ /** What stdout's leading bytes sniffed as, whether or not the picture could then be decoded. */
41
+ readonly stdoutSniffed: ImageMimeType | null;
42
+ /** Set only when stdout sniffed as a picture and a provider will take it. */
43
+ readonly stdoutImage: NormalisedImage | null;
44
+ readonly timedOut: boolean;
45
+ readonly aborted: boolean;
46
+ readonly durationMs: number;
47
+ };
48
+
49
+ export type BashStreamDetails = {
50
+ readonly totalBytes: number;
51
+ readonly truncated: boolean;
52
+ readonly path: string | null;
53
+ };
54
+
55
+ export type BashDetails = {
56
+ readonly exitCode: number | null;
57
+ readonly signal: NodeJS.Signals | null;
58
+ readonly durationMs: number;
59
+ readonly timedOut: boolean;
60
+ readonly aborted: boolean;
61
+ readonly stdout: BashStreamDetails;
62
+ readonly stderr: BashStreamDetails;
63
+ readonly image?: ImageDetails;
64
+ };