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,244 @@
1
+ import {
2
+ SessionManager,
3
+ type ToolDefinition,
4
+ } from "@earendil-works/pi-coding-agent";
5
+ import { stat } from "node:fs/promises";
6
+ import { join } from "node:path";
7
+
8
+ import { Directories } from "../shared/Directories";
9
+ import { Pool } from "../shared/Pool";
10
+ import type { Surface } from "../shared/Surface";
11
+ import { AgentRuntime, type ModelChoice } from "./AgentRuntime";
12
+ import { EventLog } from "./EventLog";
13
+ import { SessionCache } from "./SessionCache";
14
+ import {
15
+ SessionHost,
16
+ type CustomToolContext,
17
+ type HostSettings,
18
+ } from "./SessionHost";
19
+ import { SessionLease } from "./SessionLease";
20
+ import { SessionName } from "./SessionName";
21
+
22
+ /** A session as pi stores it: one JSONL file under a cwd-encoded directory. */
23
+ export type SessionSummary = {
24
+ readonly sessionId: string;
25
+ readonly cwd: string;
26
+ readonly path: string;
27
+ readonly createdAt: number;
28
+ readonly modifiedAt: number;
29
+ };
30
+
31
+ export type SessionRegistryDeps = {
32
+ readonly defaults: { readonly cwd: string; readonly model?: string };
33
+ /** The process-wide pi installation; one is built here when a caller has none to share. */
34
+ readonly runtime?: AgentRuntime;
35
+ /** Where auth, models, settings and sessions live; must agree with pi's own `getAgentDir()`. */
36
+ readonly agentDir?: string;
37
+ readonly capacity?: number;
38
+ readonly customTools?: (
39
+ context: CustomToolContext
40
+ ) => readonly ToolDefinition[];
41
+ /** Appended to every session's system prompt. */
42
+ readonly systemInstruction?: () => Promise<string | undefined>;
43
+ /** Where this frontend's human is reading. */
44
+ readonly surface?: Surface;
45
+ };
46
+
47
+ export type SessionCreateOptions = {
48
+ readonly cwd?: string;
49
+ /** Open the new session like this one: model, chosen thinking level, and cwd when none is given. */
50
+ readonly like?: SessionHost;
51
+ };
52
+
53
+ /** A session tree is thousands of files and each header is one open; fanning out over all of them at once is how a listing runs the process out of descriptors. */
54
+ const HEADER_READS = 32;
55
+
56
+ /** Live sessions keyed on pi's session UUID; the catalogue is pi's sessions directory, read on demand. */
57
+ export class SessionRegistry {
58
+ private readonly deps: SessionRegistryDeps;
59
+ private readonly runtime: AgentRuntime;
60
+ private readonly hosts: SessionCache<SessionHost>;
61
+
62
+ public constructor(deps: SessionRegistryDeps) {
63
+ this.deps = deps;
64
+ this.runtime = deps.runtime ?? new AgentRuntime(deps.agentDir);
65
+ this.hosts = new SessionCache(deps.capacity);
66
+ }
67
+
68
+ public get sessionsRoot(): string {
69
+ return join(this.runtime.agentDir, "sessions");
70
+ }
71
+
72
+ public async init(): Promise<void> {
73
+ await this.runtime.init();
74
+ }
75
+
76
+ /** Sessions on disk, newest first; reads only each file's header line. */
77
+ public async list(cwd?: string): Promise<readonly SessionSummary[]> {
78
+ const summaries = (
79
+ await Pool.mapPooled(
80
+ await this.paths("*/*.jsonl"),
81
+ HEADER_READS,
82
+ readSummary
83
+ )
84
+ ).filter(
85
+ (summary): summary is SessionSummary =>
86
+ summary !== undefined && (cwd === undefined || summary.cwd === cwd)
87
+ );
88
+ return summaries.sort((a, b) => b.modifiedAt - a.modifiedAt);
89
+ }
90
+
91
+ /** The live host for `sessionId`, if one is currently loaded. */
92
+ public peek(sessionId: string): SessionHost | undefined {
93
+ return this.hosts.peek(sessionId);
94
+ }
95
+
96
+ /**
97
+ * Rename a session through pi's own session name, so its `/resume` picker shows
98
+ * it too; `null` clears it. A live session is renamed by its agent, a closed one
99
+ * by appending to its file under the turn lease. Answers with the name pi kept.
100
+ */
101
+ public async setName(
102
+ sessionId: string,
103
+ name: string | null
104
+ ): Promise<string | undefined> {
105
+ const host = this.peek(sessionId);
106
+ if (host?.agentSession) {
107
+ return await host.setName(name);
108
+ }
109
+ const summary = await this.find(sessionId);
110
+ if (summary === undefined) {
111
+ throw new Error(`unknown session: ${sessionId}`);
112
+ }
113
+ const next = SessionName.normalise(name);
114
+ // Reopening is a plain read today, but a pi that migrates the file rewrites it.
115
+ // The lease is only ever held for a whole turn, so a rename refuses rather than queues.
116
+ return await SessionLease.hold(
117
+ summary.path,
118
+ "daemon",
119
+ async () => {
120
+ const manager = SessionManager.open(summary.path);
121
+ manager.appendSessionInfo(next);
122
+ return manager.getSessionName();
123
+ },
124
+ { timeoutMs: 0 }
125
+ );
126
+ }
127
+
128
+ /** Every model this machine has credentials for, qualified as `SessionHost.setModel` takes them. */
129
+ public models(): readonly ModelChoice[] {
130
+ return this.runtime.models();
131
+ }
132
+
133
+ public async open(sessionId: string): Promise<SessionHost> {
134
+ const cached = this.hosts.touch(sessionId);
135
+ if (cached) {
136
+ return cached;
137
+ }
138
+ const summary = await this.find(sessionId);
139
+ if (!summary) {
140
+ throw new Error(`unknown session: ${sessionId}`);
141
+ }
142
+ return this.hosts.adopt(
143
+ sessionId,
144
+ this.buildHost(sessionId, {
145
+ cwd: summary.cwd,
146
+ sessionPath: summary.path,
147
+ })
148
+ );
149
+ }
150
+
151
+ /** Start a new session; the agent is built eagerly because pi assigns the UUID and file. */
152
+ public async create(
153
+ options: SessionCreateOptions = {}
154
+ ): Promise<SessionHost> {
155
+ const like = options.like;
156
+ const cwd = options.cwd ?? like?.cwd ?? this.deps.defaults.cwd;
157
+ const reason = await Directories.check(cwd);
158
+ if (reason !== undefined) {
159
+ throw new Error(reason);
160
+ }
161
+ const model = like?.currentModelId;
162
+ const thinkingLevel = like?.chosenThinkingLevel;
163
+ const host = this.buildHost("pending", {
164
+ cwd,
165
+ ...(model === undefined ? {} : { model }),
166
+ ...(thinkingLevel === undefined ? {} : { thinkingLevel }),
167
+ });
168
+ const agent = await host.ensureAgent();
169
+ return this.hosts.adopt(agent.sessionId, host);
170
+ }
171
+
172
+ public async disposeAll(): Promise<void> {
173
+ await this.hosts.disposeAll();
174
+ }
175
+
176
+ /**
177
+ * One session by id. Pi names a file for the id it carries, so the usual
178
+ * answer is one glob and one header; resolving it by listing would read the
179
+ * header of every session on disk to throw all but one away.
180
+ */
181
+ private async find(sessionId: string): Promise<SessionSummary | undefined> {
182
+ for (const path of await this.paths(`*/*${sessionId}.jsonl`)) {
183
+ const summary = await readSummary(path);
184
+ if (summary?.sessionId === sessionId) {
185
+ return summary;
186
+ }
187
+ }
188
+ // A file named something else entirely: the id is the header's, not the name's.
189
+ return (await this.list()).find(
190
+ (summary) => summary.sessionId === sessionId
191
+ );
192
+ }
193
+
194
+ private async paths(pattern: string): Promise<readonly string[]> {
195
+ const root = this.sessionsRoot;
196
+ if (!(await stat(root).catch(() => undefined))?.isDirectory()) {
197
+ return [];
198
+ }
199
+ const found: string[] = [];
200
+ for await (const relative of new Bun.Glob(pattern).scan({
201
+ cwd: root,
202
+ onlyFiles: true,
203
+ })) {
204
+ found.push(join(root, relative));
205
+ }
206
+ return found;
207
+ }
208
+
209
+ private buildHost(label: string, settings: HostSettings): SessionHost {
210
+ return new SessionHost({
211
+ label: `session ${label}`,
212
+ settings,
213
+ defaults: this.deps.defaults,
214
+ agentDir: this.runtime.agentDir,
215
+ modelRuntime: this.runtime.modelRuntime,
216
+ modelRegistry: this.runtime.modelRegistry,
217
+ settingsManagerFor: (cwd) => this.runtime.settingsManagerFor(cwd),
218
+ persistSettings: async () => {},
219
+ // The terminal can hold the same file open, so every mutation goes through the turn lease.
220
+ lease: "daemon",
221
+ customTools: this.deps.customTools,
222
+ ...(this.deps.systemInstruction === undefined
223
+ ? {}
224
+ : { systemInstruction: this.deps.systemInstruction }),
225
+ ...(this.deps.surface === undefined
226
+ ? {}
227
+ : { surface: this.deps.surface }),
228
+ });
229
+ }
230
+ }
231
+
232
+ async function readSummary(path: string): Promise<SessionSummary | undefined> {
233
+ const header = await new EventLog(path).header();
234
+ if (!header) {
235
+ return undefined;
236
+ }
237
+ return {
238
+ sessionId: header.id,
239
+ cwd: header.cwd,
240
+ path,
241
+ createdAt: Date.parse(header.timestamp),
242
+ modifiedAt: Math.floor((await Bun.file(path).stat()).mtimeMs),
243
+ };
244
+ }
@@ -0,0 +1,51 @@
1
+ import type { EventLog } from "./EventLog";
2
+
3
+ /** How much of a session file one process can account for: durable lines, and pi's own entries. */
4
+ export type WriteMark = {
5
+ readonly head: number;
6
+ readonly entries: number;
7
+ };
8
+
9
+ /** Pi's in-memory entry list: an `AgentSession`'s `sessionManager`, or one held directly. */
10
+ type EntrySource = {
11
+ getEntries(): readonly unknown[];
12
+ };
13
+
14
+ /** Nothing accounted for yet, which `foreignSince` reads as "the file is pi's to write". */
15
+ const UNREAD: WriteMark = { head: 0, entries: 0 };
16
+
17
+ /** Head first: an append between the two reads must under-count the file, never over. */
18
+ async function of(log: EventLog, source: EntrySource): Promise<WriteMark> {
19
+ return { head: await log.head(), entries: source.getEntries().length };
20
+ }
21
+
22
+ /**
23
+ * Whether the file gained lines beyond the ones pi appended from memory. A turn, a
24
+ * model change, a compaction, a `/name` and an idle `!bash` all move both counts
25
+ * together, so only a writer that is not this process makes the difference positive.
26
+ *
27
+ * pi names a session file before it writes one, buffering entries until the first
28
+ * assistant reply, so a mark taken before the file exists is answered by the whole of
29
+ * it: header plus every entry pi was holding.
30
+ */
31
+ function foreignSince(mark: WriteMark, next: WriteMark): boolean {
32
+ const base = mark.head === 0 ? { head: 1, entries: 0 } : mark;
33
+ return next.head - base.head - (next.entries - base.entries) > 0;
34
+ }
35
+
36
+ /**
37
+ * Whether every line the file gained since `mark` is one no reader has to replay.
38
+ * A rename appends `session_info` and moves no history, so a surface that absorbs
39
+ * it stays on the session it is already holding. Only ask between turns: a window
40
+ * that also holds our own appends is not benign, and a running turn's is.
41
+ */
42
+ async function benignSince(log: EventLog, mark: WriteMark): Promise<boolean> {
43
+ const added = await log.read(mark.head);
44
+ return (
45
+ added.length > 0 &&
46
+ added.every(({ entry }) => entry.type === "session_info")
47
+ );
48
+ }
49
+
50
+ /** Write accounting over one session file: whose lines are these? */
51
+ export const WriteMark = { UNREAD, of, foreignSince, benignSince };
@@ -0,0 +1,25 @@
1
+ // Never throw on an unknown flag or positional: the same argv reaches every mode.
2
+ function scan(
3
+ args: ReadonlyArray<string>,
4
+ visit: (key: string, take: () => string | undefined) => void
5
+ ): void {
6
+ for (let i = 0; i < args.length; i++) {
7
+ const arg = args[i]!;
8
+ if (!arg.startsWith("--")) {
9
+ continue;
10
+ }
11
+ const eqIdx = arg.indexOf("=");
12
+ const key = eqIdx >= 0 ? arg.slice(0, eqIdx) : arg;
13
+ const inline = eqIdx >= 0 ? arg.slice(eqIdx + 1) : undefined;
14
+
15
+ visit(key, () => {
16
+ if (inline !== undefined) {
17
+ return inline;
18
+ }
19
+ i += 1;
20
+ return args[i];
21
+ });
22
+ }
23
+ }
24
+
25
+ export const Cli = { scan };
@@ -0,0 +1,13 @@
1
+ import type { Unit } from "./Supervisor";
2
+
3
+ /** The single unit every pim surface runs inside; `--surfaces` picks which of them come up. */
4
+ export const DaemonUnit = {
5
+ mode: "daemon",
6
+ description: "Pim daemon",
7
+ } satisfies Unit;
8
+
9
+ /** One unit per surface, which is how pim shipped until the surfaces shared a process. */
10
+ export const SupersededUnits: ReadonlyArray<Unit> = [
11
+ { mode: "web", description: "Pim web daemon" },
12
+ { mode: "telegram", description: "Pim Telegram daemon" },
13
+ ];
@@ -0,0 +1,304 @@
1
+ import * as Diff from "diff";
2
+
3
+ export type ToolDiffLineKind = "context" | "added" | "removed";
4
+
5
+ export type IntraLineRange = {
6
+ readonly start: number;
7
+ readonly end: number;
8
+ };
9
+
10
+ export type ToolDiffLine = {
11
+ readonly kind: ToolDiffLineKind;
12
+ readonly oldLine?: number;
13
+ readonly newLine?: number;
14
+ readonly text: string;
15
+ readonly emphasis?: readonly IntraLineRange[];
16
+ };
17
+
18
+ export type ToolDiffHunk = {
19
+ readonly oldStart: number;
20
+ readonly oldLines: number;
21
+ readonly newStart: number;
22
+ readonly newLines: number;
23
+ readonly lines: readonly ToolDiffLine[];
24
+ };
25
+
26
+ export type ToolDiff = {
27
+ readonly path: string;
28
+ readonly hunks: readonly ToolDiffHunk[];
29
+ };
30
+
31
+ // Keep the EOF-newline flag out of `lines`, or `joinComparable` emits a phantom blank line.
32
+ export type ToolDiffSide = {
33
+ readonly lines: readonly string[];
34
+ readonly hasTrailingNewline: boolean;
35
+ };
36
+
37
+ function buildToolDiff(
38
+ path: string,
39
+ oldSide: ToolDiffSide,
40
+ newSide: ToolDiffSide,
41
+ contextSize: number
42
+ ): ToolDiff | undefined {
43
+ if (contextSize < 0 || !Number.isFinite(contextSize)) {
44
+ throw new Error(
45
+ `contextSize must be a non-negative finite number, got ${contextSize}.`
46
+ );
47
+ }
48
+
49
+ const lines = build(oldSide.lines, newSide.lines);
50
+
51
+ if (!lines.some((line) => line.kind !== "context")) {
52
+ return undefined;
53
+ }
54
+
55
+ const emphasized = attachEmphasis(lines);
56
+
57
+ return {
58
+ path,
59
+ hunks: buildHunks(emphasized, contextSize),
60
+ };
61
+ }
62
+
63
+ function fromText(text: string): ToolDiffSide {
64
+ if (text.length === 0) {
65
+ return emptySide;
66
+ }
67
+
68
+ const hasTrailingNewline = text.endsWith("\n");
69
+ const parts = text.split("\n");
70
+
71
+ if (hasTrailingNewline) {
72
+ parts.pop();
73
+ }
74
+
75
+ return { lines: parts, hasTrailingNewline };
76
+ }
77
+
78
+ function attachEmphasis(
79
+ lines: readonly ToolDiffLine[]
80
+ ): readonly ToolDiffLine[] {
81
+ const result: ToolDiffLine[] = [...lines];
82
+ let i = 0;
83
+
84
+ while (i < result.length) {
85
+ if (result[i]?.kind !== "removed") {
86
+ i += 1;
87
+ continue;
88
+ }
89
+
90
+ let removedEnd = i;
91
+ while (
92
+ removedEnd < result.length &&
93
+ result[removedEnd]?.kind === "removed"
94
+ ) {
95
+ removedEnd += 1;
96
+ }
97
+
98
+ let addedEnd = removedEnd;
99
+ while (addedEnd < result.length && result[addedEnd]?.kind === "added") {
100
+ addedEnd += 1;
101
+ }
102
+
103
+ const removedCount = removedEnd - i;
104
+ const addedCount = addedEnd - removedEnd;
105
+
106
+ if (removedCount > 0 && removedCount === addedCount) {
107
+ for (let k = 0; k < removedCount; k += 1) {
108
+ const removed = result[i + k];
109
+ const added = result[removedEnd + k];
110
+
111
+ if (removed === undefined || added === undefined) {
112
+ continue;
113
+ }
114
+
115
+ const ranges = computeIntraLineRanges(removed.text, added.text);
116
+
117
+ if (ranges === undefined) {
118
+ continue;
119
+ }
120
+
121
+ result[i + k] = { ...removed, emphasis: ranges.removed };
122
+ result[removedEnd + k] = { ...added, emphasis: ranges.added };
123
+ }
124
+ }
125
+
126
+ i = addedEnd > i ? addedEnd : i + 1;
127
+ }
128
+
129
+ return result;
130
+ }
131
+
132
+ function computeIntraLineRanges(
133
+ oldText: string,
134
+ newText: string
135
+ ):
136
+ | {
137
+ readonly removed: readonly IntraLineRange[];
138
+ readonly added: readonly IntraLineRange[];
139
+ }
140
+ | undefined {
141
+ const parts = Diff.diffWords(oldText, newText);
142
+ const removedRanges: IntraLineRange[] = [];
143
+ const addedRanges: IntraLineRange[] = [];
144
+ let oldPos = 0;
145
+ let newPos = 0;
146
+ let sharedLen = 0;
147
+ let firstRemoved = true;
148
+ let firstAdded = true;
149
+
150
+ for (const part of parts) {
151
+ const len = part.value.length;
152
+
153
+ if (part.added === true) {
154
+ const leading = firstAdded ? leadingWsLen(part.value) : 0;
155
+ firstAdded = false;
156
+ if (len - leading > 0) {
157
+ addedRanges.push({ start: newPos + leading, end: newPos + len });
158
+ }
159
+ newPos += len;
160
+ continue;
161
+ }
162
+
163
+ if (part.removed === true) {
164
+ const leading = firstRemoved ? leadingWsLen(part.value) : 0;
165
+ firstRemoved = false;
166
+ if (len - leading > 0) {
167
+ removedRanges.push({ start: oldPos + leading, end: oldPos + len });
168
+ }
169
+ oldPos += len;
170
+ continue;
171
+ }
172
+
173
+ sharedLen += len;
174
+ oldPos += len;
175
+ newPos += len;
176
+ }
177
+
178
+ if (sharedLen === 0) {
179
+ return undefined;
180
+ }
181
+
182
+ return { removed: removedRanges, added: addedRanges };
183
+ }
184
+
185
+ function build(
186
+ oldLines: readonly string[],
187
+ newLines: readonly string[]
188
+ ): readonly ToolDiffLine[] {
189
+ const parts = Diff.diffLines(
190
+ joinComparable(oldLines),
191
+ joinComparable(newLines)
192
+ );
193
+ const lines: ToolDiffLine[] = [];
194
+ let oldLine = 1;
195
+ let newLine = 1;
196
+
197
+ for (const part of parts) {
198
+ const values = partLines(part.value);
199
+
200
+ for (const text of values) {
201
+ if (part.added === true) {
202
+ lines.push({ kind: "added", newLine, text });
203
+ newLine += 1;
204
+ continue;
205
+ }
206
+
207
+ if (part.removed === true) {
208
+ lines.push({ kind: "removed", oldLine, text });
209
+ oldLine += 1;
210
+ continue;
211
+ }
212
+
213
+ lines.push({ kind: "context", oldLine, newLine, text });
214
+ oldLine += 1;
215
+ newLine += 1;
216
+ }
217
+ }
218
+
219
+ return lines;
220
+ }
221
+
222
+ function buildHunks(
223
+ lines: readonly ToolDiffLine[],
224
+ contextSize: number
225
+ ): readonly ToolDiffHunk[] {
226
+ const changeIndexes = lines
227
+ .map((line, index) => (line.kind === "context" ? -1 : index))
228
+ .filter((index) => index >= 0);
229
+ const hunks: ToolDiffHunk[] = [];
230
+ let firstChange = changeIndexes[0];
231
+ let lastChange = changeIndexes[0];
232
+
233
+ if (firstChange === undefined || lastChange === undefined) {
234
+ return hunks;
235
+ }
236
+
237
+ const pushHunk = (startChange: number, endChange: number): void => {
238
+ const hunkLines = lines.slice(
239
+ Math.max(0, startChange - contextSize),
240
+ Math.min(lines.length, endChange + contextSize + 1)
241
+ );
242
+ const oldNumbers = hunkLines.flatMap((line) =>
243
+ line.oldLine === undefined ? [] : [line.oldLine]
244
+ );
245
+ const newNumbers = hunkLines.flatMap((line) =>
246
+ line.newLine === undefined ? [] : [line.newLine]
247
+ );
248
+ const oldStart =
249
+ oldNumbers.length === 0
250
+ ? Math.max(0, (newNumbers[0] ?? 1) - 1)
251
+ : Math.min(...oldNumbers);
252
+ const newStart =
253
+ newNumbers.length === 0
254
+ ? Math.max(0, (oldNumbers[0] ?? 1) - 1)
255
+ : Math.min(...newNumbers);
256
+
257
+ hunks.push({
258
+ oldStart,
259
+ oldLines:
260
+ oldNumbers.length === 0 ? 0 : Math.max(...oldNumbers) - oldStart + 1,
261
+ newStart,
262
+ newLines:
263
+ newNumbers.length === 0 ? 0 : Math.max(...newNumbers) - newStart + 1,
264
+ lines: hunkLines,
265
+ });
266
+ };
267
+
268
+ for (const changeIndex of changeIndexes.slice(1)) {
269
+ if (changeIndex - lastChange <= contextSize * 2 + 1) {
270
+ lastChange = changeIndex;
271
+ continue;
272
+ }
273
+
274
+ pushHunk(firstChange, lastChange);
275
+ firstChange = changeIndex;
276
+ lastChange = changeIndex;
277
+ }
278
+
279
+ pushHunk(firstChange, lastChange);
280
+
281
+ return hunks;
282
+ }
283
+
284
+ function leadingWsLen(value: string): number {
285
+ return value.match(/^\s*/)?.[0].length ?? 0;
286
+ }
287
+
288
+ function joinComparable(lines: readonly string[]): string {
289
+ return lines.length === 0 ? "" : `${lines.join("\n")}\n`;
290
+ }
291
+
292
+ function partLines(value: string): readonly string[] {
293
+ const lines = value.split("\n");
294
+
295
+ if (lines.at(-1) === "") {
296
+ lines.pop();
297
+ }
298
+
299
+ return lines;
300
+ }
301
+
302
+ const emptySide: ToolDiffSide = { lines: [], hasTrailingNewline: false };
303
+
304
+ export const DiffLines = { buildToolDiff, fromText, emptySide, attachEmphasis };
@@ -0,0 +1,70 @@
1
+ import * as Diff from "diff";
2
+ import {
3
+ DiffLines,
4
+ type ToolDiff,
5
+ type ToolDiffHunk,
6
+ type ToolDiffLine,
7
+ type ToolDiffLineKind,
8
+ } from "./DiffLines";
9
+
10
+ function fromUnified(path: string, patch: string): ToolDiff | undefined {
11
+ const hunks = Diff.parsePatch(patch)
12
+ .flatMap((file) => file.hunks)
13
+ .map(toHunk)
14
+ .filter((hunk) => hunk.lines.length > 0);
15
+
16
+ return hunks.length === 0 ? undefined : { path, hunks };
17
+ }
18
+
19
+ function toHunk(hunk: Diff.StructuredPatchHunk): ToolDiffHunk {
20
+ const lines: ToolDiffLine[] = [];
21
+ let oldLine = hunk.oldStart;
22
+ let newLine = hunk.newStart;
23
+
24
+ for (const raw of hunk.lines) {
25
+ if (raw.startsWith("\\")) {
26
+ continue;
27
+ }
28
+
29
+ const kind = kindOf(raw);
30
+ const text = raw.slice(1);
31
+
32
+ if (kind === "added") {
33
+ lines.push({ kind, newLine, text });
34
+ newLine += 1;
35
+ continue;
36
+ }
37
+
38
+ if (kind === "removed") {
39
+ lines.push({ kind, oldLine, text });
40
+ oldLine += 1;
41
+ continue;
42
+ }
43
+
44
+ lines.push({ kind, oldLine, newLine, text });
45
+ oldLine += 1;
46
+ newLine += 1;
47
+ }
48
+
49
+ return {
50
+ oldStart: hunk.oldStart,
51
+ oldLines: hunk.oldLines,
52
+ newStart: hunk.newStart,
53
+ newLines: hunk.newLines,
54
+ lines: DiffLines.attachEmphasis(lines),
55
+ };
56
+ }
57
+
58
+ function kindOf(line: string): ToolDiffLineKind {
59
+ if (line.startsWith("+")) {
60
+ return "added";
61
+ }
62
+
63
+ if (line.startsWith("-")) {
64
+ return "removed";
65
+ }
66
+
67
+ return "context";
68
+ }
69
+
70
+ export const DiffPatch = { fromUnified };