pim-agent 0.0.1 → 0.10.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 (275) 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 +54 -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 +177 -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 +36 -0
  47. package/packages/core/src/extensions/system-prompt/prompt.ts +232 -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 +132 -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 +77 -0
  87. package/packages/core/src/session/AgentRuntime.ts +85 -0
  88. package/packages/core/src/session/EventLog.ts +156 -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/SearchIndex.ts +752 -0
  92. package/packages/core/src/session/SearchTokens.ts +97 -0
  93. package/packages/core/src/session/SessionCache.ts +68 -0
  94. package/packages/core/src/session/SessionDigest.ts +161 -0
  95. package/packages/core/src/session/SessionHost.ts +892 -0
  96. package/packages/core/src/session/SessionLease.ts +279 -0
  97. package/packages/core/src/session/SessionMeta.ts +267 -0
  98. package/packages/core/src/session/SessionName.ts +18 -0
  99. package/packages/core/src/session/SessionRegistry.ts +244 -0
  100. package/packages/core/src/session/WriteMark.ts +51 -0
  101. package/packages/core/src/shared/Cli.ts +25 -0
  102. package/packages/core/src/shared/DaemonUnit.ts +13 -0
  103. package/packages/core/src/shared/DiffLines.ts +304 -0
  104. package/packages/core/src/shared/DiffPatch.ts +70 -0
  105. package/packages/core/src/shared/DiffRenderer.ts +283 -0
  106. package/packages/core/src/shared/DiffView.ts +29 -0
  107. package/packages/core/src/shared/Directories.ts +61 -0
  108. package/packages/core/src/shared/EditMatcher.ts +767 -0
  109. package/packages/core/src/shared/Errors.ts +9 -0
  110. package/packages/core/src/shared/ExtensionToggles.ts +130 -0
  111. package/packages/core/src/shared/FileEnumerator.ts +361 -0
  112. package/packages/core/src/shared/FileScanner.ts +57 -0
  113. package/packages/core/src/shared/FileWatch.ts +121 -0
  114. package/packages/core/src/shared/Format.ts +74 -0
  115. package/packages/core/src/shared/Fs.ts +91 -0
  116. package/packages/core/src/shared/FsErrors.ts +67 -0
  117. package/packages/core/src/shared/FuzzyMatcher.ts +71 -0
  118. package/packages/core/src/shared/Git.ts +530 -0
  119. package/packages/core/src/shared/GitMonitor.ts +221 -0
  120. package/packages/core/src/shared/GlobExclusions.ts +13 -0
  121. package/packages/core/src/shared/Http.ts +14 -0
  122. package/packages/core/src/shared/ImageMime.ts +36 -0
  123. package/packages/core/src/shared/Images.ts +357 -0
  124. package/packages/core/src/shared/Json.ts +19 -0
  125. package/packages/core/src/shared/Languages.ts +95 -0
  126. package/packages/core/src/shared/Levenshtein.ts +83 -0
  127. package/packages/core/src/shared/Lines.ts +79 -0
  128. package/packages/core/src/shared/McpClient.ts +444 -0
  129. package/packages/core/src/shared/OutputBudget.ts +84 -0
  130. package/packages/core/src/shared/PatchSummary.ts +40 -0
  131. package/packages/core/src/shared/Paths.ts +80 -0
  132. package/packages/core/src/shared/PimSettings.ts +128 -0
  133. package/packages/core/src/shared/PimVersion.ts +93 -0
  134. package/packages/core/src/shared/Pool.ts +26 -0
  135. package/packages/core/src/shared/Proc.ts +50 -0
  136. package/packages/core/src/shared/RateLimiter.ts +50 -0
  137. package/packages/core/src/shared/Renderer.ts +371 -0
  138. package/packages/core/src/shared/RepoDiff.ts +741 -0
  139. package/packages/core/src/shared/SafePath.ts +17 -0
  140. package/packages/core/src/shared/SearchRender.ts +82 -0
  141. package/packages/core/src/shared/SendFile.ts +23 -0
  142. package/packages/core/src/shared/SpillCache.ts +78 -0
  143. package/packages/core/src/shared/SubagentLogs.ts +102 -0
  144. package/packages/core/src/shared/Supervisor.ts +460 -0
  145. package/packages/core/src/shared/Surface.ts +2 -0
  146. package/packages/core/src/shared/Sweeper.ts +32 -0
  147. package/packages/core/src/shared/Tools.ts +801 -0
  148. package/packages/core/src/shared/Updater.ts +209 -0
  149. package/packages/core/src/view/AnsiPainter.ts +227 -0
  150. package/packages/core/src/view/BodyRenderer.ts +102 -0
  151. package/packages/core/src/view/DiffBlocks.ts +52 -0
  152. package/packages/core/src/view/DiffExpand.ts +214 -0
  153. package/packages/core/src/view/DiffLayout.ts +69 -0
  154. package/packages/core/src/view/DiffPairs.ts +47 -0
  155. package/packages/core/src/view/MarkdownPainter.ts +314 -0
  156. package/packages/core/src/view/MovePath.ts +70 -0
  157. package/packages/core/src/view/Painting.ts +182 -0
  158. package/packages/core/src/view/ViewBlock.ts +124 -0
  159. package/packages/daemon/src/Daemon.ts +63 -0
  160. package/packages/daemon/src/DaemonInstall.ts +115 -0
  161. package/packages/daemon/src/Surfaces.ts +66 -0
  162. package/packages/daemon/src/TelegramSurface.ts +30 -0
  163. package/packages/daemon/src/WebSurface.ts +56 -0
  164. package/packages/daemon/src/index.ts +37 -0
  165. package/packages/protocol/src/Command.ts +219 -0
  166. package/packages/protocol/src/Diff.ts +15 -0
  167. package/packages/protocol/src/ImageRoute.ts +23 -0
  168. package/packages/protocol/src/ServerEvent.ts +406 -0
  169. package/packages/server/src/AttachmentEndpoint.ts +184 -0
  170. package/packages/server/src/ClientConnection.ts +277 -0
  171. package/packages/server/src/ImageEndpoint.ts +36 -0
  172. package/packages/server/src/ProbeClient.ts +374 -0
  173. package/packages/server/src/Reloader.ts +93 -0
  174. package/packages/server/src/SendFileTool.ts +88 -0
  175. package/packages/server/src/SessionCatalogue.ts +456 -0
  176. package/packages/server/src/SessionProjection.ts +194 -0
  177. package/packages/server/src/SessionStream.ts +627 -0
  178. package/packages/server/src/StaticClient.ts +178 -0
  179. package/packages/server/src/WebOptions.ts +72 -0
  180. package/packages/server/src/WsGateway.ts +798 -0
  181. package/packages/server/src/probe.ts +200 -0
  182. package/packages/telegram/src/Bot.ts +229 -0
  183. package/packages/telegram/src/Commands.ts +768 -0
  184. package/packages/telegram/src/Config.ts +148 -0
  185. package/packages/telegram/src/Markdown.ts +235 -0
  186. package/packages/telegram/src/Message.ts +152 -0
  187. package/packages/telegram/src/Renderer.ts +456 -0
  188. package/packages/telegram/src/SendFileSchema.ts +18 -0
  189. package/packages/telegram/src/SendFileTool.ts +65 -0
  190. package/packages/telegram/src/Session.ts +253 -0
  191. package/packages/telegram/src/SessionRegistry.ts +126 -0
  192. package/packages/telegram/src/TaskScheduler.ts +296 -0
  193. package/packages/telegram/src/TaskSchema.ts +88 -0
  194. package/packages/telegram/src/TaskStore.ts +67 -0
  195. package/packages/telegram/src/TaskTool.ts +208 -0
  196. package/packages/telegram/src/TelegramHtml.ts +288 -0
  197. package/packages/telegram/src/TypingIndicator.ts +43 -0
  198. package/packages/telegram/src/UpdateConfirm.ts +61 -0
  199. package/packages/tui/src/extensions/_init/index.ts +144 -0
  200. package/packages/tui/src/extensions/command-picker/index.ts +75 -0
  201. package/packages/tui/src/extensions/file-picker/index.ts +156 -0
  202. package/packages/tui/src/extensions/footer/index.ts +93 -0
  203. package/packages/tui/src/extensions/footer/powerline.ts +87 -0
  204. package/packages/tui/src/extensions/footer/segments.ts +202 -0
  205. package/packages/tui/src/extensions/pim/index.ts +89 -0
  206. package/packages/tui/src/extensions/session-lease/index.ts +253 -0
  207. package/packages/tui/src/extensions/tps/index.ts +118 -0
  208. package/packages/tui/src/extensions/working-indicator/index.ts +75 -0
  209. package/packages/tui/src/extensions/wrapProvider.ts +18 -0
  210. package/packages/tui/src/themes/pim-dark.json +84 -0
  211. package/packages/tui/src/themes/pim-light.json +84 -0
  212. package/packages/tui/src/themes/themeCliArgs.ts +20 -0
  213. package/packages/web/dist/client/apple-touch-icon.png +0 -0
  214. package/packages/web/dist/client/assets/bash-LmRjUKlR.js +1 -0
  215. package/packages/web/dist/client/assets/c-DBVbJFpC.js +1 -0
  216. package/packages/web/dist/client/assets/clojure-CR7dvLJ4.js +1 -0
  217. package/packages/web/dist/client/assets/cmake-CgP6gB4W.js +1 -0
  218. package/packages/web/dist/client/assets/commit-mono-latin-300-italic-B48RJMeK.woff +0 -0
  219. package/packages/web/dist/client/assets/commit-mono-latin-300-italic-DBq5bJCE.woff2 +0 -0
  220. package/packages/web/dist/client/assets/commit-mono-latin-300-normal-B-iV2FbL.woff2 +0 -0
  221. package/packages/web/dist/client/assets/commit-mono-latin-300-normal-kM0OTZCv.woff +0 -0
  222. package/packages/web/dist/client/assets/commit-mono-latin-400-italic-BXinMwCi.woff +0 -0
  223. package/packages/web/dist/client/assets/commit-mono-latin-400-italic-DjSHLl2N.woff2 +0 -0
  224. package/packages/web/dist/client/assets/commit-mono-latin-400-normal-s0S3qwFW.woff +0 -0
  225. package/packages/web/dist/client/assets/commit-mono-latin-400-normal-wzhe4RuD.woff2 +0 -0
  226. package/packages/web/dist/client/assets/commit-mono-latin-600-normal-BuQDXT0M.woff +0 -0
  227. package/packages/web/dist/client/assets/commit-mono-latin-600-normal-SwgSWHaV.woff2 +0 -0
  228. package/packages/web/dist/client/assets/commit-mono-latin-700-normal-1-GU0IUE.woff +0 -0
  229. package/packages/web/dist/client/assets/commit-mono-latin-700-normal-DU8mrtj2.woff2 +0 -0
  230. package/packages/web/dist/client/assets/core-CA0aSzPu.js +3 -0
  231. package/packages/web/dist/client/assets/cpp-N2d8M7Nc.js +1 -0
  232. package/packages/web/dist/client/assets/csharp-yYt0Ikks.js +1 -0
  233. package/packages/web/dist/client/assets/css-D8M8GYaw.js +1 -0
  234. package/packages/web/dist/client/assets/diff-BhdZizse.js +1 -0
  235. package/packages/web/dist/client/assets/dockerfile-C0zufGsU.js +1 -0
  236. package/packages/web/dist/client/assets/elixir-DXIHlGsR.js +1 -0
  237. package/packages/web/dist/client/assets/erlang-BjYI8dhz.js +1 -0
  238. package/packages/web/dist/client/assets/go-CYstVVJV.js +1 -0
  239. package/packages/web/dist/client/assets/graphql-BHHhacaR.js +1 -0
  240. package/packages/web/dist/client/assets/haskell-DUkhXIda.js +1 -0
  241. package/packages/web/dist/client/assets/index-B9be-i9d.css +1 -0
  242. package/packages/web/dist/client/assets/index-CpinM5NK.js +45 -0
  243. package/packages/web/dist/client/assets/ini-B1CroMsf.js +1 -0
  244. package/packages/web/dist/client/assets/java-Dde-Wnb4.js +1 -0
  245. package/packages/web/dist/client/assets/javascript-8AmHuH5O.js +1 -0
  246. package/packages/web/dist/client/assets/json-PmyKWHmm.js +1 -0
  247. package/packages/web/dist/client/assets/kotlin-DgSK8bu-.js +2 -0
  248. package/packages/web/dist/client/assets/less-Bp0mgmpG.js +1 -0
  249. package/packages/web/dist/client/assets/lua-1dXYaL-H.js +1 -0
  250. package/packages/web/dist/client/assets/makefile-BX-o3EIz.js +1 -0
  251. package/packages/web/dist/client/assets/markdown-D6FrnlqJ.js +1 -0
  252. package/packages/web/dist/client/assets/ocaml-B-P1Uyq8.js +1 -0
  253. package/packages/web/dist/client/assets/perl-CvqMZYuf.js +1 -0
  254. package/packages/web/dist/client/assets/php-DuZl5e-L.js +2 -0
  255. package/packages/web/dist/client/assets/powershell-BUe16zE4.js +1 -0
  256. package/packages/web/dist/client/assets/protobuf-DGOW1qT8.js +1 -0
  257. package/packages/web/dist/client/assets/python-gHmu6VPG.js +1 -0
  258. package/packages/web/dist/client/assets/r-DNWPCnjT.js +1 -0
  259. package/packages/web/dist/client/assets/ruby-BGpZ3p4_.js +1 -0
  260. package/packages/web/dist/client/assets/rust-BIKPVoi6.js +1 -0
  261. package/packages/web/dist/client/assets/scala-C9lP4FWY.js +1 -0
  262. package/packages/web/dist/client/assets/scss-gOyOKiXd.js +1 -0
  263. package/packages/web/dist/client/assets/sql-DWRKyvJ-.js +1 -0
  264. package/packages/web/dist/client/assets/swift-BTn9__hL.js +1 -0
  265. package/packages/web/dist/client/assets/typescript-CxkPLmSc.js +1 -0
  266. package/packages/web/dist/client/assets/vim-Dhhcn_CA.js +1 -0
  267. package/packages/web/dist/client/assets/xml-COl5yFSC.js +1 -0
  268. package/packages/web/dist/client/assets/yaml-DjOhhVLG.js +1 -0
  269. package/packages/web/dist/client/favicon.svg +1 -0
  270. package/packages/web/dist/client/icon-192.png +0 -0
  271. package/packages/web/dist/client/icon-512.png +0 -0
  272. package/packages/web/dist/client/icon-maskable.png +0 -0
  273. package/packages/web/dist/client/index.html +24 -0
  274. package/packages/web/dist/client/manifest.webmanifest +21 -0
  275. package/packages/web/dist/client/wordmark.svg +1 -0
@@ -0,0 +1,184 @@
1
+ import { basename, dirname, join } from "node:path";
2
+
3
+ import {
4
+ AttachmentStore,
5
+ type StoredAttachment,
6
+ } from "#core/attachments/AttachmentStore";
7
+ import { Paths } from "#core/shared/Paths";
8
+ import { IMMUTABLE, serveFile } from "./StaticClient";
9
+
10
+ export type AttachmentEndpointDeps = {
11
+ /** Defaults to `~/.pim/attachments`. */
12
+ readonly root?: string;
13
+ readonly maxBytes?: number;
14
+ };
15
+
16
+ const DEFAULT_MAX_BYTES = 25 * 1024 * 1024;
17
+
18
+ const PREFIX = "/attachment/";
19
+
20
+ /** Where stored bytes live when nothing says otherwise. */
21
+ export function defaultAttachmentsRoot(): string {
22
+ return join(Paths.pimHomeDir(), "attachments");
23
+ }
24
+
25
+ // Every origin is allowed: in dev the client is served from vite's own port and its upload is cross-origin.
26
+ function allow(response: Response): Response {
27
+ response.headers.set("access-control-allow-origin", "*");
28
+ response.headers.set("access-control-allow-headers", "content-type");
29
+ return response;
30
+ }
31
+
32
+ /** Where `serve` answers for a stored file: the last two path segments only, never the server's layout. */
33
+ export function attachmentUrl(path: string): string {
34
+ const scope = encodeURIComponent(basename(dirname(path)));
35
+ return `${PREFIX}${scope}/${encodeURIComponent(basename(path))}`;
36
+ }
37
+
38
+ /** `POST /upload?session=<id>` with a multipart `file` field, and `GET /attachment/<session>/<file>` to read it back. */
39
+ export class AttachmentEndpoint {
40
+ private readonly store: AttachmentStore;
41
+ private readonly maxBytes: number;
42
+ private readonly bySession = new Map<string, Map<string, StoredAttachment>>();
43
+
44
+ public constructor(deps: AttachmentEndpointDeps = {}) {
45
+ this.store = new AttachmentStore(deps.root ?? defaultAttachmentsRoot());
46
+ this.maxBytes = deps.maxBytes ?? DEFAULT_MAX_BYTES;
47
+ }
48
+
49
+ /** Whether this endpoint owns the path, so the gateway can route to it. */
50
+ public static owns(pathname: string): boolean {
51
+ return pathname === "/upload" || pathname.startsWith(PREFIX);
52
+ }
53
+
54
+ public async handle(request: Request): Promise<Response> {
55
+ if (request.method === "OPTIONS") {
56
+ return allow(new Response(null, { status: 204 }));
57
+ }
58
+ const { pathname } = new URL(request.url);
59
+ return allow(
60
+ pathname.startsWith(PREFIX)
61
+ ? await this.serve(pathname)
62
+ : await this.upload(request)
63
+ );
64
+ }
65
+
66
+ // Every segment goes back through the write's sanitising, so a crafted name cannot escape its scope.
67
+ private async serve(pathname: string): Promise<Response> {
68
+ const [scope, name, ...rest] = pathname
69
+ .slice(PREFIX.length)
70
+ .split("/")
71
+ .map(decodeURIComponentSafely);
72
+ if (!scope || !name || rest.length > 0) {
73
+ return new Response("not found", { status: 404 });
74
+ }
75
+ let path: string;
76
+ try {
77
+ path = this.store.locate(scope, name);
78
+ } catch {
79
+ return new Response("not found", { status: 404 });
80
+ }
81
+ return serveFile(path, IMMUTABLE);
82
+ }
83
+
84
+ private async upload(request: Request): Promise<Response> {
85
+ if (request.method !== "POST") {
86
+ return new Response("expected POST", { status: 405 });
87
+ }
88
+ const sessionId = new URL(request.url).searchParams.get("session");
89
+ if (!sessionId) {
90
+ return Response.json({ error: "missing ?session" }, { status: 400 });
91
+ }
92
+ const declared = Number(request.headers.get("content-length") ?? 0);
93
+ if (declared > this.maxBytes) {
94
+ return Response.json(
95
+ { error: `upload exceeds ${this.maxBytes} bytes` },
96
+ { status: 413 }
97
+ );
98
+ }
99
+
100
+ let file: File | undefined;
101
+ try {
102
+ const form = await request.formData();
103
+ const field = form.get("file");
104
+ file = field instanceof File ? field : undefined;
105
+ } catch {
106
+ return Response.json(
107
+ { error: "expected multipart/form-data" },
108
+ { status: 400 }
109
+ );
110
+ }
111
+ if (!file) {
112
+ return Response.json({ error: "missing `file` field" }, { status: 400 });
113
+ }
114
+ if (file.size > this.maxBytes) {
115
+ return Response.json(
116
+ { error: `upload exceeds ${this.maxBytes} bytes` },
117
+ { status: 413 }
118
+ );
119
+ }
120
+
121
+ let stored: StoredAttachment;
122
+ try {
123
+ stored = await this.store.store(sessionId, {
124
+ bytes: await file.arrayBuffer(),
125
+ mimeType: file.type || "application/octet-stream",
126
+ name: file.name,
127
+ stem: stemOf(file.name),
128
+ });
129
+ } catch (err) {
130
+ return Response.json({ error: (err as Error).message }, { status: 400 });
131
+ }
132
+ this.remember(sessionId, stored);
133
+
134
+ return Response.json({
135
+ id: stored.id,
136
+ path: stored.path,
137
+ url: attachmentUrl(stored.path),
138
+ mimeType: stored.mimeType,
139
+ isImage: stored.imageBase64 !== undefined,
140
+ });
141
+ }
142
+
143
+ /** Consume the uploads a prompt referenced; unknown ids are dropped, so a replayed command cannot re-attach. */
144
+ public take(
145
+ sessionId: string,
146
+ ids: readonly string[]
147
+ ): readonly StoredAttachment[] {
148
+ const pending = this.bySession.get(sessionId);
149
+ if (!pending) {
150
+ return [];
151
+ }
152
+ const taken: StoredAttachment[] = [];
153
+ for (const id of ids) {
154
+ const stored = pending.get(id);
155
+ if (stored) {
156
+ pending.delete(id);
157
+ taken.push(stored);
158
+ }
159
+ }
160
+ if (pending.size === 0) {
161
+ this.bySession.delete(sessionId);
162
+ }
163
+ return taken;
164
+ }
165
+
166
+ private remember(sessionId: string, stored: StoredAttachment): void {
167
+ const pending = this.bySession.get(sessionId) ?? new Map();
168
+ pending.set(stored.id, stored);
169
+ this.bySession.set(sessionId, pending);
170
+ }
171
+ }
172
+
173
+ function stemOf(name: string): string | undefined {
174
+ const stem = basename(name).replace(/\.[^.]*$/, "");
175
+ return stem === "" ? undefined : stem;
176
+ }
177
+
178
+ function decodeURIComponentSafely(segment: string): string {
179
+ try {
180
+ return decodeURIComponent(segment);
181
+ } catch {
182
+ return "";
183
+ }
184
+ }
@@ -0,0 +1,277 @@
1
+ import type { ServerWebSocket } from "bun";
2
+
3
+ import {
4
+ isDurableEvent,
5
+ type ServerEvent,
6
+ type StreamEvent,
7
+ } from "#protocol/ServerEvent";
8
+ import type { SessionProjection } from "./SessionProjection";
9
+
10
+ /** The half of `SessionStream` a connection needs: subscribe, and ask for everything missed. */
11
+ export type AttachableStream = {
12
+ readonly subscribe: (listener: (event: ServerEvent) => void) => () => void;
13
+ readonly replay: (fromSeq: number) => Promise<readonly StreamEvent[]>;
14
+ readonly sessionId: string;
15
+ };
16
+
17
+ type SubagentWatch = {
18
+ readonly callId: string;
19
+ readonly projection: SessionProjection;
20
+ cursor: number;
21
+ };
22
+
23
+ const HIGH_WATER_MARK = 1 << 20;
24
+
25
+ const FRAMES = new WeakMap<object, string>();
26
+
27
+ export function frame(event: object): string {
28
+ const cached = FRAMES.get(event);
29
+ if (cached !== undefined) {
30
+ return cached;
31
+ }
32
+ const text = JSON.stringify(event);
33
+ FRAMES.set(event, text);
34
+ return text;
35
+ }
36
+
37
+ /** One WebSocket client, attached to at most one session; a lagging client is paused and re-derived on `drain`. */
38
+ export class ClientConnection {
39
+ private readonly ws: ServerWebSocket<undefined>;
40
+ private stream: AttachableStream | undefined;
41
+ private unsubscribe: (() => void) | undefined;
42
+ private readonly gate: ServerEvent[] = [];
43
+ private gated = false;
44
+ private paused = false;
45
+ private cursor = 0;
46
+ private closed = false;
47
+ private reading = true;
48
+ private watch: SubagentWatch | undefined;
49
+
50
+ public constructor(ws: ServerWebSocket<undefined>) {
51
+ this.ws = ws;
52
+ }
53
+
54
+ public get sessionId(): string | undefined {
55
+ return this.stream?.sessionId;
56
+ }
57
+
58
+ /** Whether the reader behind this socket is looking at the session right now. */
59
+ public get attentive(): boolean {
60
+ return this.reading;
61
+ }
62
+
63
+ /** Highest durable `seq` this client has been handed. */
64
+ public get seq(): number {
65
+ return this.cursor;
66
+ }
67
+
68
+ /** The subagent this client is reading, if it is reading one. */
69
+ public get watchedCallId(): string | undefined {
70
+ return this.watch?.callId;
71
+ }
72
+
73
+ public async attach(
74
+ stream: AttachableStream,
75
+ fromSeq: number,
76
+ attentive = true
77
+ ): Promise<void> {
78
+ this.detach();
79
+ this.stream = stream;
80
+ this.cursor = fromSeq;
81
+ this.reading = attentive;
82
+ this.unsubscribe = stream.subscribe((event) => {
83
+ this.onStreamEvent(event);
84
+ });
85
+ await this.sync();
86
+ }
87
+
88
+ public setAttentive(attentive: boolean): void {
89
+ this.reading = attentive;
90
+ }
91
+
92
+ /** Read a subagent's log alongside the attached session; at most one watch at a time. */
93
+ public async watchSubagent(
94
+ callId: string,
95
+ projection: SessionProjection,
96
+ fromSeq: number
97
+ ): Promise<void> {
98
+ this.watch = { callId, projection, cursor: fromSeq };
99
+ await this.pumpWatch();
100
+ }
101
+
102
+ /** Closes only the named watch, so it cannot close the one that replaced it. */
103
+ public unwatchSubagent(callId: string): void {
104
+ if (this.watch?.callId === callId) {
105
+ this.watch = undefined;
106
+ }
107
+ }
108
+
109
+ /** Send something that belongs to no stream, e.g. a command response. */
110
+ public send(event: ServerEvent): void {
111
+ if (this.closed) {
112
+ return;
113
+ }
114
+ this.ws.send(frame(event), true);
115
+ }
116
+
117
+ public onDrain(): void {
118
+ if (!this.paused) {
119
+ return;
120
+ }
121
+ this.paused = false;
122
+ void this.sync();
123
+ this.flushWatch();
124
+ }
125
+
126
+ public detach(): void {
127
+ this.unsubscribe?.();
128
+ this.unsubscribe = undefined;
129
+ this.stream = undefined;
130
+ this.watch = undefined;
131
+ }
132
+
133
+ public close(): void {
134
+ this.closed = true;
135
+ this.detach();
136
+ }
137
+
138
+ // Gate live events during the read and reconcile by `seq`; ephemeral ones are already in the snapshot.
139
+ private async sync(): Promise<void> {
140
+ const stream = this.stream;
141
+ if (!stream || this.closed) {
142
+ return;
143
+ }
144
+ this.gated = true;
145
+ const replayed = await stream.replay(this.cursor);
146
+ if (this.stream !== stream || this.closed) {
147
+ return;
148
+ }
149
+ const queued = this.gate.filter(isDurableEvent);
150
+ this.gate.length = 0;
151
+ this.gated = false;
152
+ this.writeBatch([...replayed, ...queued]);
153
+ }
154
+
155
+ private onStreamEvent(event: ServerEvent): void {
156
+ if (this.touchesWatch(event)) {
157
+ void this.pumpWatch();
158
+ }
159
+ // Gate a batch unwrapped: the durable filter cannot see into an envelope.
160
+ if (event.type === "replay") {
161
+ if (this.gated) {
162
+ this.gate.push(...event.events);
163
+ return;
164
+ }
165
+ this.writeBatch(event.events);
166
+ return;
167
+ }
168
+ if (this.gated) {
169
+ this.gate.push(event);
170
+ return;
171
+ }
172
+ this.write(event);
173
+ }
174
+
175
+ // Sent whole or not at all: the cursor moves only once the frame is on the socket.
176
+ private writeBatch(events: readonly StreamEvent[]): void {
177
+ if (this.closed || this.paused) {
178
+ return;
179
+ }
180
+ // Filter against a cursor that moves inside the batch: replay and queue can name the same line.
181
+ let cursor = this.cursor;
182
+ const fresh: StreamEvent[] = [];
183
+ for (const event of events) {
184
+ if (isDurableEvent(event)) {
185
+ if (event.seq <= cursor) {
186
+ continue;
187
+ }
188
+ cursor = event.seq;
189
+ }
190
+ fresh.push(event);
191
+ }
192
+ if (fresh.length === 0) {
193
+ return;
194
+ }
195
+ if (this.deliver(JSON.stringify({ type: "replay", events: fresh }))) {
196
+ this.cursor = cursor;
197
+ }
198
+ }
199
+
200
+ private touchesWatch(event: ServerEvent): boolean {
201
+ const callId = this.watch?.callId;
202
+ if (callId === undefined) {
203
+ return false;
204
+ }
205
+ switch (event.type) {
206
+ case "tool_update":
207
+ case "tool_end":
208
+ case "tool_result":
209
+ return event.callId === callId;
210
+ case "replay":
211
+ return event.events.some((inner) => this.touchesWatch(inner));
212
+ default:
213
+ return false;
214
+ }
215
+ }
216
+
217
+ private async pumpWatch(): Promise<void> {
218
+ const watch = this.watch;
219
+ if (!watch || this.closed) {
220
+ return;
221
+ }
222
+ await watch.projection.drain();
223
+ // The watch can be closed or replaced during the read.
224
+ if (this.watch !== watch) {
225
+ return;
226
+ }
227
+ this.flushWatch();
228
+ }
229
+
230
+ private flushWatch(): void {
231
+ const watch = this.watch;
232
+ if (!watch || this.closed || this.paused) {
233
+ return;
234
+ }
235
+ const events = watch.projection.since(watch.cursor);
236
+ const last = events.at(-1);
237
+ if (!last) {
238
+ return;
239
+ }
240
+ const payload = JSON.stringify({
241
+ type: "subagent_events",
242
+ callId: watch.callId,
243
+ events,
244
+ });
245
+ if (this.deliver(payload)) {
246
+ watch.cursor = last.seq;
247
+ }
248
+ }
249
+
250
+ private deliver(payload: string): boolean {
251
+ if (this.closed || this.paused) {
252
+ return false;
253
+ }
254
+ const status = this.ws.send(payload, true);
255
+ if (status === 0) {
256
+ this.paused = true;
257
+ return false;
258
+ }
259
+ if (this.ws.getBufferedAmount() > HIGH_WATER_MARK) {
260
+ this.paused = true;
261
+ }
262
+ return true;
263
+ }
264
+
265
+ private write(event: ServerEvent): void {
266
+ if (this.closed || this.paused) {
267
+ return;
268
+ }
269
+ const durable = isDurableEvent(event);
270
+ if (durable && event.seq <= this.cursor) {
271
+ return;
272
+ }
273
+ if (this.deliver(frame(event)) && durable) {
274
+ this.cursor = event.seq;
275
+ }
276
+ }
277
+ }
@@ -0,0 +1,36 @@
1
+ import { join } from "node:path";
2
+
3
+ import { Images } from "#core/shared/Images";
4
+ import { SpillCache } from "#core/shared/SpillCache";
5
+ import { ImageRoute } from "#protocol/ImageRoute";
6
+ import { IMMUTABLE, serveFile } from "./StaticClient";
7
+
8
+ export type ImageEndpointDeps = {
9
+ /** Defaults to `~/.pim/cache`, where `read` spills the picture it showed the model. */
10
+ readonly root?: string;
11
+ };
12
+
13
+ /** `GET /image/<sha256>.<ext>`: the cached copy of a picture a tool view addresses, or 404 once the TTL sweep took it. */
14
+ export class ImageEndpoint {
15
+ private readonly root: string;
16
+
17
+ public constructor(deps: ImageEndpointDeps = {}) {
18
+ this.root = deps.root ?? SpillCache.dir();
19
+ }
20
+
21
+ /** Whether this endpoint owns the path, so the gateway can route to it. */
22
+ public static owns(pathname: string): boolean {
23
+ return ImageRoute.owns(pathname);
24
+ }
25
+
26
+ public async handle(request: Request): Promise<Response> {
27
+ if (request.method !== "GET" && request.method !== "HEAD") {
28
+ return new Response("expected GET", { status: 405 });
29
+ }
30
+ // The name is a digest and a format we wrote, which admits no separator to escape the cache with.
31
+ const name = ImageRoute.nameOf(new URL(request.url).pathname);
32
+ return name === null
33
+ ? new Response("not found", { status: 404 })
34
+ : serveFile(join(this.root, `${Images.CACHE_PREFIX}${name}`), IMMUTABLE);
35
+ }
36
+ }