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,97 @@
1
+ /** One indexable word and where it sits in the string it came from. */
2
+ export type SearchToken = {
3
+ /** Lowercased: the corpus itself is never lowered, only its tokens. */
4
+ readonly text: string;
5
+ readonly start: number;
6
+ readonly end: number;
7
+ };
8
+
9
+ const RUN = /[\p{L}\p{N}_]+/gu;
10
+
11
+ function scan(text: string): readonly SearchToken[] {
12
+ const tokens: SearchToken[] = [];
13
+ for (const run of text.matchAll(RUN)) {
14
+ const at = run.index;
15
+ const word = run[0];
16
+ const pieces = piecesOf(word);
17
+ for (const [from, to] of pieces) {
18
+ tokens.push({
19
+ text: word.slice(from, to).toLowerCase(),
20
+ start: at + from,
21
+ end: at + to,
22
+ });
23
+ }
24
+ if (pieces.length > 1) {
25
+ tokens.push({
26
+ text: word.toLowerCase(),
27
+ start: at,
28
+ end: at + word.length,
29
+ });
30
+ }
31
+ }
32
+ return tokens;
33
+ }
34
+
35
+ /**
36
+ * The words a query asks for: its pieces alone, never the identifier they
37
+ * spell. `SessionLease` asks for `session` and `lease`, which the index holds
38
+ * for `SessionLease` and for `session lease` alike; asking for `sessionlease`
39
+ * too would AND away the second.
40
+ */
41
+ function words(query: string): readonly string[] {
42
+ const found: string[] = [];
43
+ for (const run of query.matchAll(RUN)) {
44
+ for (const [from, to] of piecesOf(run[0])) {
45
+ const word = run[0].slice(from, to).toLowerCase();
46
+ if (!found.includes(word)) {
47
+ found.push(word);
48
+ }
49
+ }
50
+ }
51
+ return found;
52
+ }
53
+
54
+ /** Half-open slices of one run: `WsGateway` is `ws` and `gateway`, `read_file` is `read` and `file`. */
55
+ function piecesOf(run: string): readonly (readonly [number, number])[] {
56
+ const pieces: (readonly [number, number])[] = [];
57
+ let start = -1;
58
+ for (let at = 0; at < run.length; at++) {
59
+ if (run[at] === "_") {
60
+ if (start >= 0) {
61
+ pieces.push([start, at]);
62
+ start = -1;
63
+ }
64
+ continue;
65
+ }
66
+ if (start < 0) {
67
+ start = at;
68
+ } else if (breaksAt(run, at)) {
69
+ pieces.push([start, at]);
70
+ start = at;
71
+ }
72
+ }
73
+ if (start >= 0) {
74
+ pieces.push([start, run.length]);
75
+ }
76
+ return pieces;
77
+ }
78
+
79
+ function breaksAt(run: string, at: number): boolean {
80
+ const here = run[at]!;
81
+ if (!isUpper(here)) {
82
+ return false;
83
+ }
84
+ const before = run[at - 1]!;
85
+ const after = run[at + 1];
86
+ return !isUpper(before) || (after !== undefined && isLower(after));
87
+ }
88
+
89
+ function isUpper(char: string): boolean {
90
+ return char !== char.toLowerCase() && char === char.toUpperCase();
91
+ }
92
+
93
+ function isLower(char: string): boolean {
94
+ return char !== char.toUpperCase() && char === char.toLowerCase();
95
+ }
96
+
97
+ export const SearchTokens = { scan, words };
@@ -0,0 +1,68 @@
1
+ /** What a cache entry has to offer: a recency stamp and a way to let go. */
2
+ export type CachedSession = {
3
+ lastUsed: number;
4
+ dispose(): Promise<void>;
5
+ };
6
+
7
+ const CAPACITY = 16;
8
+
9
+ /** Live sessions, keyed however the frontend keys them, capped by least-recently-used. */
10
+ export class SessionCache<T extends CachedSession> {
11
+ private readonly capacity: number;
12
+ private readonly entries = new Map<string, T>();
13
+
14
+ public constructor(capacity: number = CAPACITY) {
15
+ this.capacity = capacity;
16
+ }
17
+
18
+ public get size(): number {
19
+ return this.entries.size;
20
+ }
21
+
22
+ /** The live entry, if there is one, without counting as a use. */
23
+ public peek(key: string): T | undefined {
24
+ return this.entries.get(key);
25
+ }
26
+
27
+ /** The live entry, marked used now so eviction passes over it. */
28
+ public touch(key: string): T | undefined {
29
+ const entry = this.entries.get(key);
30
+ if (entry) {
31
+ entry.lastUsed = Date.now();
32
+ }
33
+ return entry;
34
+ }
35
+
36
+ /** Store `value`, disposing the least recently used entry first when full. */
37
+ public adopt(key: string, value: T): T {
38
+ this.evictIfNeeded();
39
+ this.entries.set(key, value);
40
+ return value;
41
+ }
42
+
43
+ public async disposeAll(): Promise<void> {
44
+ const live = [...this.entries.values()];
45
+ this.entries.clear();
46
+ await Promise.all(live.map((entry) => entry.dispose()));
47
+ }
48
+
49
+ private evictIfNeeded(): void {
50
+ if (this.entries.size < this.capacity) {
51
+ return;
52
+ }
53
+ let oldestKey: string | undefined;
54
+ let oldest = Infinity;
55
+ for (const [key, entry] of this.entries) {
56
+ if (entry.lastUsed < oldest) {
57
+ oldest = entry.lastUsed;
58
+ oldestKey = key;
59
+ }
60
+ }
61
+ if (oldestKey === undefined) {
62
+ return;
63
+ }
64
+ const evicted = this.entries.get(oldestKey)!;
65
+ this.entries.delete(oldestKey);
66
+ void evicted.dispose();
67
+ }
68
+ }
@@ -0,0 +1,161 @@
1
+ import { parseSessionEntries } from "@earendil-works/pi-coding-agent";
2
+
3
+ import { Attachments } from "../attachments/Attachments";
4
+ import { MessageText } from "./MessageText";
5
+
6
+ /** What the session catalogue shows for one file without opening a session. */
7
+ export type SessionDigest = {
8
+ /** The session's name if it has one, else its first user message; absent when it has neither. */
9
+ readonly title?: string;
10
+ /** True when `title` is a name somebody wrote rather than the opening message. */
11
+ readonly named?: true;
12
+ /** When the agent last wrote; it moves with an in-flight turn, so read it before starting one. */
13
+ readonly settledAt?: number;
14
+ };
15
+
16
+ /**
17
+ * The digest before it is clamped, so a reader that tails an appended region
18
+ * can fold that region into what an earlier read of the same file found.
19
+ */
20
+ export type DigestParts = {
21
+ readonly name?: string;
22
+ /** The opening ask, unclamped. */
23
+ readonly opening?: string;
24
+ readonly settledAt?: number;
25
+ };
26
+
27
+ /**
28
+ * A session file's durable bytes, undecoded: a whole session is megabytes of
29
+ * tool output and the digest wants a handful of lines of it, so the search
30
+ * runs over the bytes and only the lines it keeps become strings.
31
+ */
32
+ export type Durable = {
33
+ readonly bytes: Buffer;
34
+ /** Past the last newline; a trailing fragment is a write in progress and is not a line. */
35
+ readonly end: number;
36
+ };
37
+
38
+ const PROBE_LINES = 16;
39
+
40
+ const TAIL_LINES = 10;
41
+
42
+ const TITLE_LIMIT = 120;
43
+
44
+ /** Cheap reject before decoding a line: pi writes the discriminant verbatim. */
45
+ const NAMED = Buffer.from('"session_info"');
46
+
47
+ export const NEWLINE = 0x0a;
48
+
49
+ function durable(read: Uint8Array): Durable | undefined {
50
+ const bytes = Buffer.from(read.buffer, read.byteOffset, read.byteLength);
51
+ const end = bytes.lastIndexOf(NEWLINE) + 1;
52
+ return end === 0 ? undefined : { bytes, end };
53
+ }
54
+
55
+ function partsOf(body: Durable): DigestParts {
56
+ return {
57
+ name: nameOf(body),
58
+ opening: firstUserMessage(headLinesOf(body, PROBE_LINES)),
59
+ settledAt: lastAgentTime(tailLinesOf(body, TAIL_LINES).reverse()),
60
+ };
61
+ }
62
+
63
+ /** An append only adds, so the opening ask stands and a later name or agent line wins. */
64
+ function merge(before: DigestParts, appended: DigestParts): DigestParts {
65
+ return {
66
+ name: appended.name ?? before.name,
67
+ opening: before.opening ?? appended.opening,
68
+ settledAt: appended.settledAt ?? before.settledAt,
69
+ };
70
+ }
71
+
72
+ function of({ name, opening, settledAt }: DigestParts): SessionDigest {
73
+ const title = name ?? opening;
74
+ return {
75
+ ...(title === undefined ? {} : { title: clamp(title) }),
76
+ ...(name === undefined ? {} : { named: true as const }),
77
+ ...(settledAt === undefined ? {} : { settledAt }),
78
+ };
79
+ }
80
+
81
+ /** The last `session_info` line, found by walking the marker's occurrences backwards; a line that merely quotes the marker parses as something else, and the walk carries on past it. */
82
+ function nameOf({ bytes, end }: Durable): string | undefined {
83
+ let at = end - 1;
84
+ while (at >= 0) {
85
+ const found = bytes.lastIndexOf(NAMED, at);
86
+ if (found === -1) {
87
+ return undefined;
88
+ }
89
+ const from = bytes.lastIndexOf(NEWLINE, found) + 1;
90
+ const to = bytes.indexOf(NEWLINE, found);
91
+ const entry = parseSessionEntries(bytes.toString("utf8", from, to))[0];
92
+ if (entry?.type === "session_info") {
93
+ return entry.name?.trim() || undefined;
94
+ }
95
+ at = from - 1;
96
+ }
97
+ return undefined;
98
+ }
99
+
100
+ function headLinesOf({ bytes, end }: Durable, count: number): string[] {
101
+ let at = 0;
102
+ for (let taken = 0; taken < count && at < end; taken++) {
103
+ at = bytes.indexOf(NEWLINE, at) + 1;
104
+ }
105
+ return split(bytes.toString("utf8", 0, at));
106
+ }
107
+
108
+ function tailLinesOf({ bytes, end }: Durable, count: number): string[] {
109
+ let at = end - 1;
110
+ for (let taken = 0; taken < count && at > 0; taken++) {
111
+ at = bytes.lastIndexOf(NEWLINE, at - 1);
112
+ }
113
+ return split(bytes.toString("utf8", at + 1, end));
114
+ }
115
+
116
+ function split(text: string): string[] {
117
+ const lines = text.split("\n");
118
+ lines.pop();
119
+ return lines;
120
+ }
121
+
122
+ function lastAgentTime(lines: readonly string[]): number | undefined {
123
+ for (const line of lines) {
124
+ const entry = parseSessionEntries(line)[0];
125
+ if (entry?.type !== "message") {
126
+ continue;
127
+ }
128
+ const role = entry.message.role;
129
+ if (role === "assistant" || role === "toolResult") {
130
+ const at = Date.parse(entry.timestamp);
131
+ return Number.isNaN(at) ? undefined : at;
132
+ }
133
+ }
134
+ return undefined;
135
+ }
136
+
137
+ function clamp(text: string): string {
138
+ return text.length > TITLE_LIMIT
139
+ ? `${text.slice(0, TITLE_LIMIT).trimEnd()}…`
140
+ : text;
141
+ }
142
+
143
+ /** Over the head window the caller has already cut, not the whole file. */
144
+ function firstUserMessage(lines: readonly string[]): string | undefined {
145
+ for (const line of lines) {
146
+ const entry = parseSessionEntries(line)[0];
147
+ if (entry?.type !== "message" || entry.message.role !== "user") {
148
+ continue;
149
+ }
150
+ const said = Attachments.parse(MessageText.textOf(entry.message.content));
151
+ const text =
152
+ said.text.trim() ||
153
+ said.files.map((file) => Attachments.nameOf(file.path)).join(", ");
154
+ if (text !== "") {
155
+ return text;
156
+ }
157
+ }
158
+ return undefined;
159
+ }
160
+
161
+ export const SessionDigest = { durable, partsOf, merge, of, nameOf };