pim-agent 0.0.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +293 -7
  3. package/bin/pim.ts +151 -0
  4. package/package.json +82 -4
  5. package/packages/core/src/attachments/AttachmentStore.ts +99 -0
  6. package/packages/core/src/attachments/Attachments.ts +69 -0
  7. package/packages/core/src/extensions/CoreExtensions.ts +51 -0
  8. package/packages/core/src/extensions/apply-patch/coordinator.ts +53 -0
  9. package/packages/core/src/extensions/apply-patch/executor.ts +550 -0
  10. package/packages/core/src/extensions/apply-patch/index.ts +69 -0
  11. package/packages/core/src/extensions/apply-patch/matcher.ts +47 -0
  12. package/packages/core/src/extensions/apply-patch/model.ts +39 -0
  13. package/packages/core/src/extensions/apply-patch/parser.ts +379 -0
  14. package/packages/core/src/extensions/apply-patch/render.ts +216 -0
  15. package/packages/core/src/extensions/apply-patch/schema.ts +37 -0
  16. package/packages/core/src/extensions/apply-patch/types.ts +30 -0
  17. package/packages/core/src/extensions/bash/capture.ts +86 -0
  18. package/packages/core/src/extensions/bash/format.ts +110 -0
  19. package/packages/core/src/extensions/bash/image.ts +31 -0
  20. package/packages/core/src/extensions/bash/index.ts +88 -0
  21. package/packages/core/src/extensions/bash/render.ts +31 -0
  22. package/packages/core/src/extensions/bash/run.ts +221 -0
  23. package/packages/core/src/extensions/bash/schema.ts +64 -0
  24. package/packages/core/src/extensions/edit/edit.ts +366 -0
  25. package/packages/core/src/extensions/edit/index.ts +36 -0
  26. package/packages/core/src/extensions/edit/render.ts +16 -0
  27. package/packages/core/src/extensions/edit/schema.ts +37 -0
  28. package/packages/core/src/extensions/glob/glob.ts +34 -0
  29. package/packages/core/src/extensions/glob/index.ts +83 -0
  30. package/packages/core/src/extensions/glob/render.ts +72 -0
  31. package/packages/core/src/extensions/glob/schema.ts +66 -0
  32. package/packages/core/src/extensions/grep/grep.ts +256 -0
  33. package/packages/core/src/extensions/grep/index.ts +114 -0
  34. package/packages/core/src/extensions/grep/render.ts +271 -0
  35. package/packages/core/src/extensions/grep/schema.ts +92 -0
  36. package/packages/core/src/extensions/read/ImageMemory.ts +42 -0
  37. package/packages/core/src/extensions/read/image.ts +57 -0
  38. package/packages/core/src/extensions/read/index.ts +88 -0
  39. package/packages/core/src/extensions/read/read.ts +191 -0
  40. package/packages/core/src/extensions/read/render.ts +58 -0
  41. package/packages/core/src/extensions/read/schema.ts +49 -0
  42. package/packages/core/src/extensions/subagent/index.ts +33 -0
  43. package/packages/core/src/extensions/subagent/render.ts +127 -0
  44. package/packages/core/src/extensions/subagent/schema.ts +9 -0
  45. package/packages/core/src/extensions/subagent/subagent.ts +419 -0
  46. package/packages/core/src/extensions/system-prompt/index.ts +28 -0
  47. package/packages/core/src/extensions/system-prompt/prompt.ts +228 -0
  48. package/packages/core/src/extensions/todo/index.ts +123 -0
  49. package/packages/core/src/extensions/todo/render.ts +156 -0
  50. package/packages/core/src/extensions/todo/schema.ts +24 -0
  51. package/packages/core/src/extensions/todo/todo.ts +185 -0
  52. package/packages/core/src/extensions/web-fetch/JinaReaderClient.ts +187 -0
  53. package/packages/core/src/extensions/web-fetch/WebViewFetchClient.ts +183 -0
  54. package/packages/core/src/extensions/web-fetch/WebViewMarkdownSnapshot.ts +512 -0
  55. package/packages/core/src/extensions/web-fetch/fetch.ts +265 -0
  56. package/packages/core/src/extensions/web-fetch/image.ts +165 -0
  57. package/packages/core/src/extensions/web-fetch/index.ts +83 -0
  58. package/packages/core/src/extensions/web-fetch/render.ts +100 -0
  59. package/packages/core/src/extensions/web-fetch/schema.ts +46 -0
  60. package/packages/core/src/extensions/web-search/ExaMcpClient.ts +251 -0
  61. package/packages/core/src/extensions/web-search/SearchBreaker.ts +147 -0
  62. package/packages/core/src/extensions/web-search/SearchChain.ts +77 -0
  63. package/packages/core/src/extensions/web-search/index.ts +90 -0
  64. package/packages/core/src/extensions/web-search/providers/DuckDuckGoProvider.ts +195 -0
  65. package/packages/core/src/extensions/web-search/providers/ExaProvider.ts +54 -0
  66. package/packages/core/src/extensions/web-search/providers/FirecrawlProvider.ts +144 -0
  67. package/packages/core/src/extensions/web-search/providers/SearchProvider.ts +118 -0
  68. package/packages/core/src/extensions/web-search/render.ts +49 -0
  69. package/packages/core/src/extensions/web-search/schema.ts +29 -0
  70. package/packages/core/src/extensions/web-search/search.ts +23 -0
  71. package/packages/core/src/extensions/write/index.ts +58 -0
  72. package/packages/core/src/extensions/write/render.ts +16 -0
  73. package/packages/core/src/extensions/write/schema.ts +14 -0
  74. package/packages/core/src/extensions/write/write.ts +87 -0
  75. package/packages/core/src/picker/FilePickerSuggestionEngine.ts +14 -0
  76. package/packages/core/src/picker/InProcessFilePickerSuggestionEngine.ts +52 -0
  77. package/packages/core/src/picker/PickerItem.ts +6 -0
  78. package/packages/core/src/picker/PickerService.ts +110 -0
  79. package/packages/core/src/picker/RemoteFilePickerSuggestionEngine.ts +56 -0
  80. package/packages/core/src/picker/WorkerFilePickerSuggestionEngine.ts +268 -0
  81. package/packages/core/src/picker/catalog.ts +220 -0
  82. package/packages/core/src/picker/commandRanker.ts +17 -0
  83. package/packages/core/src/picker/filePickerWorker.ts +66 -0
  84. package/packages/core/src/picker/filePickerWorkerMessages.ts +39 -0
  85. package/packages/core/src/picker/ranker.ts +245 -0
  86. package/packages/core/src/picker/token.ts +70 -0
  87. package/packages/core/src/session/AgentRuntime.ts +85 -0
  88. package/packages/core/src/session/EventLog.ts +213 -0
  89. package/packages/core/src/session/MessageText.ts +21 -0
  90. package/packages/core/src/session/ReadCursors.ts +94 -0
  91. package/packages/core/src/session/SessionCache.ts +68 -0
  92. package/packages/core/src/session/SessionHost.ts +876 -0
  93. package/packages/core/src/session/SessionLease.ts +279 -0
  94. package/packages/core/src/session/SessionRegistry.ts +169 -0
  95. package/packages/core/src/session/WriteMark.ts +37 -0
  96. package/packages/core/src/shared/Cli.ts +25 -0
  97. package/packages/core/src/shared/DaemonUnit.ts +13 -0
  98. package/packages/core/src/shared/DiffLines.ts +304 -0
  99. package/packages/core/src/shared/DiffRenderer.ts +283 -0
  100. package/packages/core/src/shared/DiffView.ts +29 -0
  101. package/packages/core/src/shared/Directories.ts +61 -0
  102. package/packages/core/src/shared/EditMatcher.ts +767 -0
  103. package/packages/core/src/shared/Errors.ts +9 -0
  104. package/packages/core/src/shared/ExtensionToggles.ts +130 -0
  105. package/packages/core/src/shared/FileEnumerator.ts +361 -0
  106. package/packages/core/src/shared/FileScanner.ts +57 -0
  107. package/packages/core/src/shared/FileWatch.ts +107 -0
  108. package/packages/core/src/shared/Format.ts +74 -0
  109. package/packages/core/src/shared/Fs.ts +91 -0
  110. package/packages/core/src/shared/FsErrors.ts +67 -0
  111. package/packages/core/src/shared/FuzzyMatcher.ts +71 -0
  112. package/packages/core/src/shared/Git.ts +89 -0
  113. package/packages/core/src/shared/GlobExclusions.ts +13 -0
  114. package/packages/core/src/shared/Http.ts +14 -0
  115. package/packages/core/src/shared/ImageMime.ts +36 -0
  116. package/packages/core/src/shared/Images.ts +357 -0
  117. package/packages/core/src/shared/Json.ts +19 -0
  118. package/packages/core/src/shared/Languages.ts +95 -0
  119. package/packages/core/src/shared/Levenshtein.ts +30 -0
  120. package/packages/core/src/shared/Lines.ts +79 -0
  121. package/packages/core/src/shared/McpClient.ts +444 -0
  122. package/packages/core/src/shared/OutputBudget.ts +84 -0
  123. package/packages/core/src/shared/PatchSummary.ts +40 -0
  124. package/packages/core/src/shared/Paths.ts +80 -0
  125. package/packages/core/src/shared/PimSettings.ts +128 -0
  126. package/packages/core/src/shared/PimVersion.ts +93 -0
  127. package/packages/core/src/shared/Pool.ts +26 -0
  128. package/packages/core/src/shared/Proc.ts +30 -0
  129. package/packages/core/src/shared/RateLimiter.ts +50 -0
  130. package/packages/core/src/shared/Renderer.ts +371 -0
  131. package/packages/core/src/shared/SafePath.ts +17 -0
  132. package/packages/core/src/shared/SearchRender.ts +82 -0
  133. package/packages/core/src/shared/SendFile.ts +23 -0
  134. package/packages/core/src/shared/SpillCache.ts +78 -0
  135. package/packages/core/src/shared/SubagentLogs.ts +102 -0
  136. package/packages/core/src/shared/Supervisor.ts +460 -0
  137. package/packages/core/src/shared/Sweeper.ts +32 -0
  138. package/packages/core/src/shared/Tools.ts +801 -0
  139. package/packages/core/src/shared/Updater.ts +193 -0
  140. package/packages/core/src/view/AnsiPainter.ts +227 -0
  141. package/packages/core/src/view/BodyRenderer.ts +102 -0
  142. package/packages/core/src/view/DiffBlocks.ts +52 -0
  143. package/packages/core/src/view/DiffLayout.ts +69 -0
  144. package/packages/core/src/view/MarkdownPainter.ts +314 -0
  145. package/packages/core/src/view/Painting.ts +182 -0
  146. package/packages/core/src/view/ViewBlock.ts +124 -0
  147. package/packages/daemon/src/Daemon.ts +63 -0
  148. package/packages/daemon/src/DaemonInstall.ts +115 -0
  149. package/packages/daemon/src/Surfaces.ts +66 -0
  150. package/packages/daemon/src/TelegramSurface.ts +30 -0
  151. package/packages/daemon/src/WebSurface.ts +57 -0
  152. package/packages/daemon/src/index.ts +37 -0
  153. package/packages/protocol/src/Command.ts +92 -0
  154. package/packages/protocol/src/ImageRoute.ts +23 -0
  155. package/packages/protocol/src/Protocol.ts +6 -0
  156. package/packages/protocol/src/ServerEvent.ts +251 -0
  157. package/packages/server/src/AttachmentEndpoint.ts +184 -0
  158. package/packages/server/src/ClientConnection.ts +265 -0
  159. package/packages/server/src/ImageEndpoint.ts +36 -0
  160. package/packages/server/src/ProbeClient.ts +312 -0
  161. package/packages/server/src/Reloader.ts +93 -0
  162. package/packages/server/src/SendFileTool.ts +88 -0
  163. package/packages/server/src/SessionCatalogue.ts +202 -0
  164. package/packages/server/src/SessionProjection.ts +194 -0
  165. package/packages/server/src/SessionStream.ts +601 -0
  166. package/packages/server/src/StaticClient.ts +95 -0
  167. package/packages/server/src/WebOptions.ts +72 -0
  168. package/packages/server/src/WsGateway.ts +563 -0
  169. package/packages/server/src/probe.ts +146 -0
  170. package/packages/telegram/src/Bot.ts +229 -0
  171. package/packages/telegram/src/Commands.ts +768 -0
  172. package/packages/telegram/src/Config.ts +148 -0
  173. package/packages/telegram/src/Markdown.ts +235 -0
  174. package/packages/telegram/src/Message.ts +152 -0
  175. package/packages/telegram/src/Renderer.ts +456 -0
  176. package/packages/telegram/src/SendFileSchema.ts +18 -0
  177. package/packages/telegram/src/SendFileTool.ts +65 -0
  178. package/packages/telegram/src/Session.ts +252 -0
  179. package/packages/telegram/src/SessionRegistry.ts +126 -0
  180. package/packages/telegram/src/TaskScheduler.ts +296 -0
  181. package/packages/telegram/src/TaskSchema.ts +88 -0
  182. package/packages/telegram/src/TaskStore.ts +67 -0
  183. package/packages/telegram/src/TaskTool.ts +208 -0
  184. package/packages/telegram/src/TelegramHtml.ts +288 -0
  185. package/packages/telegram/src/TypingIndicator.ts +43 -0
  186. package/packages/telegram/src/UpdateConfirm.ts +61 -0
  187. package/packages/tui/src/extensions/_init/index.ts +144 -0
  188. package/packages/tui/src/extensions/command-picker/index.ts +43 -0
  189. package/packages/tui/src/extensions/file-picker/index.ts +156 -0
  190. package/packages/tui/src/extensions/footer/index.ts +117 -0
  191. package/packages/tui/src/extensions/footer/powerline.ts +87 -0
  192. package/packages/tui/src/extensions/footer/segments.ts +202 -0
  193. package/packages/tui/src/extensions/pim/index.ts +89 -0
  194. package/packages/tui/src/extensions/session-lease/index.ts +248 -0
  195. package/packages/tui/src/extensions/tps/index.ts +118 -0
  196. package/packages/tui/src/extensions/working-indicator/index.ts +75 -0
  197. package/packages/tui/src/extensions/wrapProvider.ts +18 -0
  198. package/packages/tui/src/themes/pim-dark.json +84 -0
  199. package/packages/tui/src/themes/pim-light.json +84 -0
  200. package/packages/tui/src/themes/themeCliArgs.ts +20 -0
  201. package/packages/web/dist/client/apple-touch-icon.png +0 -0
  202. package/packages/web/dist/client/assets/bash-LmRjUKlR.js +1 -0
  203. package/packages/web/dist/client/assets/c-DBVbJFpC.js +1 -0
  204. package/packages/web/dist/client/assets/clojure-CR7dvLJ4.js +1 -0
  205. package/packages/web/dist/client/assets/cmake-CgP6gB4W.js +1 -0
  206. package/packages/web/dist/client/assets/commit-mono-latin-300-italic-B48RJMeK.woff +0 -0
  207. package/packages/web/dist/client/assets/commit-mono-latin-300-italic-DBq5bJCE.woff2 +0 -0
  208. package/packages/web/dist/client/assets/commit-mono-latin-400-italic-BXinMwCi.woff +0 -0
  209. package/packages/web/dist/client/assets/commit-mono-latin-400-italic-DjSHLl2N.woff2 +0 -0
  210. package/packages/web/dist/client/assets/commit-mono-latin-400-normal-s0S3qwFW.woff +0 -0
  211. package/packages/web/dist/client/assets/commit-mono-latin-400-normal-wzhe4RuD.woff2 +0 -0
  212. package/packages/web/dist/client/assets/commit-mono-latin-600-normal-BuQDXT0M.woff +0 -0
  213. package/packages/web/dist/client/assets/commit-mono-latin-600-normal-SwgSWHaV.woff2 +0 -0
  214. package/packages/web/dist/client/assets/commit-mono-latin-700-normal-1-GU0IUE.woff +0 -0
  215. package/packages/web/dist/client/assets/commit-mono-latin-700-normal-DU8mrtj2.woff2 +0 -0
  216. package/packages/web/dist/client/assets/core-CGdQx9la.js +3 -0
  217. package/packages/web/dist/client/assets/cpp-N2d8M7Nc.js +1 -0
  218. package/packages/web/dist/client/assets/csharp-yYt0Ikks.js +1 -0
  219. package/packages/web/dist/client/assets/css-D8M8GYaw.js +1 -0
  220. package/packages/web/dist/client/assets/diff-BhdZizse.js +1 -0
  221. package/packages/web/dist/client/assets/dockerfile-C0zufGsU.js +1 -0
  222. package/packages/web/dist/client/assets/elixir-DXIHlGsR.js +1 -0
  223. package/packages/web/dist/client/assets/erlang-BjYI8dhz.js +1 -0
  224. package/packages/web/dist/client/assets/go-CYstVVJV.js +1 -0
  225. package/packages/web/dist/client/assets/graphql-BHHhacaR.js +1 -0
  226. package/packages/web/dist/client/assets/haskell-DUkhXIda.js +1 -0
  227. package/packages/web/dist/client/assets/index-BMNwleWO.js +40 -0
  228. package/packages/web/dist/client/assets/index-CqjB2RPr.css +1 -0
  229. package/packages/web/dist/client/assets/ini-B1CroMsf.js +1 -0
  230. package/packages/web/dist/client/assets/java-Dde-Wnb4.js +1 -0
  231. package/packages/web/dist/client/assets/javascript-8AmHuH5O.js +1 -0
  232. package/packages/web/dist/client/assets/json-PmyKWHmm.js +1 -0
  233. package/packages/web/dist/client/assets/kotlin-DgSK8bu-.js +2 -0
  234. package/packages/web/dist/client/assets/less-Bp0mgmpG.js +1 -0
  235. package/packages/web/dist/client/assets/lua-1dXYaL-H.js +1 -0
  236. package/packages/web/dist/client/assets/makefile-BX-o3EIz.js +1 -0
  237. package/packages/web/dist/client/assets/markdown-D6FrnlqJ.js +1 -0
  238. package/packages/web/dist/client/assets/ocaml-B-P1Uyq8.js +1 -0
  239. package/packages/web/dist/client/assets/perl-CvqMZYuf.js +1 -0
  240. package/packages/web/dist/client/assets/php-DuZl5e-L.js +2 -0
  241. package/packages/web/dist/client/assets/powershell-BUe16zE4.js +1 -0
  242. package/packages/web/dist/client/assets/protobuf-DGOW1qT8.js +1 -0
  243. package/packages/web/dist/client/assets/python-gHmu6VPG.js +1 -0
  244. package/packages/web/dist/client/assets/r-DNWPCnjT.js +1 -0
  245. package/packages/web/dist/client/assets/ruby-BGpZ3p4_.js +1 -0
  246. package/packages/web/dist/client/assets/rust-BIKPVoi6.js +1 -0
  247. package/packages/web/dist/client/assets/scala-C9lP4FWY.js +1 -0
  248. package/packages/web/dist/client/assets/scss-gOyOKiXd.js +1 -0
  249. package/packages/web/dist/client/assets/sql-DWRKyvJ-.js +1 -0
  250. package/packages/web/dist/client/assets/swift-BTn9__hL.js +1 -0
  251. package/packages/web/dist/client/assets/typescript-CxkPLmSc.js +1 -0
  252. package/packages/web/dist/client/assets/vim-Dhhcn_CA.js +1 -0
  253. package/packages/web/dist/client/assets/xml-COl5yFSC.js +1 -0
  254. package/packages/web/dist/client/assets/yaml-DjOhhVLG.js +1 -0
  255. package/packages/web/dist/client/favicon.svg +1 -0
  256. package/packages/web/dist/client/icon-192.png +0 -0
  257. package/packages/web/dist/client/icon-512.png +0 -0
  258. package/packages/web/dist/client/icon-maskable.png +0 -0
  259. package/packages/web/dist/client/index.html +24 -0
  260. package/packages/web/dist/client/manifest.webmanifest +21 -0
  261. package/packages/web/dist/client/wordmark.svg +1 -0
@@ -0,0 +1,115 @@
1
+ import { join } from "node:path";
2
+
3
+ import { DaemonUnit, SupersededUnits } from "#core/shared/DaemonUnit";
4
+ import { Supervisor, type Unit } from "#core/shared/Supervisor";
5
+ import { DEFAULT_CLIENT_DIR } from "#server/StaticClient";
6
+ import { WebOptions } from "#server/WebOptions";
7
+ import { Surfaces, type SurfaceName } from "./Surfaces";
8
+
9
+ type FrozenUnit = Unit & { readonly args: ReadonlyArray<string> };
10
+
11
+ /** What this machine serves today: the unit being replaced, or the ones it supersedes. */
12
+ export type Installed = {
13
+ readonly surfaces: ReadonlyArray<SurfaceName>;
14
+ readonly args: ReadonlyArray<string>;
15
+ };
16
+
17
+ const NOTHING: Installed = { surfaces: [], args: [] };
18
+
19
+ /**
20
+ * A supervisor starts the daemon with no argv: every flag it needs must be
21
+ * frozen in here. What is installed already is the base the new argv overrides
22
+ * flag by flag, so installing one surface keeps the other one — and its
23
+ * settings. Naming `--surfaces` outright is how a daemon is shrunk again.
24
+ */
25
+ function unit(
26
+ argv: ReadonlyArray<string>,
27
+ installed: Installed = NOTHING
28
+ ): FrozenUnit {
29
+ const surfaces = Surfaces.named(argv)
30
+ ? Surfaces.parse(argv)
31
+ : Surfaces.union(installed.surfaces, Surfaces.parse(argv));
32
+ const merged = [...installed.args, ...argv];
33
+ return {
34
+ ...DaemonUnit,
35
+ description: `${DaemonUnit.description} (${surfaces.join(", ")})`,
36
+ args: [
37
+ "--surfaces",
38
+ Surfaces.format(surfaces),
39
+ ...(surfaces.includes("web")
40
+ ? WebOptions.freeze(WebOptions.parse(merged))
41
+ : []),
42
+ ],
43
+ };
44
+ }
45
+
46
+ /**
47
+ * The merged unit's own argv, or — before the cutover — whatever the per-surface
48
+ * units were installed with, so a tailnet hostname frozen months ago survives.
49
+ */
50
+ async function installed(): Promise<Installed> {
51
+ const current = await Supervisor.installedArgs(DaemonUnit);
52
+ if (current.length > 0) {
53
+ return { surfaces: Surfaces.parse(current), args: carriedOver(current) };
54
+ }
55
+ const superseded = (
56
+ await Promise.all(
57
+ SupersededUnits.map((unit) => Supervisor.installedArgs(unit))
58
+ )
59
+ ).filter((args) => args.length > 0);
60
+ return {
61
+ surfaces: Surfaces.union(...superseded.map(Surfaces.parse)),
62
+ args: superseded.flatMap(carriedOver),
63
+ };
64
+ }
65
+
66
+ // `--web-cwd` becomes `--cwd` on the way in, or it would outrank a new `--cwd`.
67
+ function carriedOver(args: ReadonlyArray<string>): ReadonlyArray<string> {
68
+ return args.map((arg) => (arg === "--web-cwd" ? "--cwd" : arg));
69
+ }
70
+
71
+ async function install(argv: ReadonlyArray<string>): Promise<void> {
72
+ const descriptor = unit(argv, await installed());
73
+ const surfaces = Surfaces.parse(descriptor.args);
74
+ if (surfaces.includes("web")) {
75
+ await buildClient();
76
+ }
77
+ const kept = surfaces.filter((name) => !Surfaces.parse(argv).includes(name));
78
+ if (kept.length > 0) {
79
+ console.log(
80
+ `[install] keeping the ${kept.join(", ")} surface this machine already serves; pass --surfaces to change that`
81
+ );
82
+ }
83
+ console.log(
84
+ `[install] unit starts: pim --mode ${descriptor.mode} ${descriptor.args.join(" ")}`
85
+ );
86
+ await Supervisor.install(descriptor, { replaces: SupersededUnits });
87
+ }
88
+
89
+ async function uninstall(): Promise<void> {
90
+ await Supervisor.uninstall(DaemonUnit);
91
+ await Supervisor.supersede(SupersededUnits);
92
+ }
93
+
94
+ async function buildClient(): Promise<void> {
95
+ const at = await Supervisor.detectInstall();
96
+ // Probe `index.html`, not the directory: a half-emptied `dist/client` is still a broken bundle.
97
+ const bundled = await Bun.file(
98
+ join(DEFAULT_CLIENT_DIR, "index.html")
99
+ ).exists();
100
+ if (at.kind !== "dev" || bundled) {
101
+ return;
102
+ }
103
+ console.log("[install] building the web client");
104
+ const proc = Bun.spawn(["bun", "run", "web:build"], {
105
+ cwd: at.packageRoot,
106
+ stdout: "inherit",
107
+ stderr: "inherit",
108
+ });
109
+ const code = await proc.exited;
110
+ if (code !== 0) {
111
+ throw new Error(`bun run web:build exited ${code}`);
112
+ }
113
+ }
114
+
115
+ export const DaemonInstall = { unit, installed, install, uninstall };
@@ -0,0 +1,66 @@
1
+ import { Cli as Argv } from "#core/shared/Cli";
2
+
3
+ export const SURFACE_NAMES = ["web", "telegram"] as const;
4
+
5
+ export type SurfaceName = (typeof SURFACE_NAMES)[number];
6
+
7
+ function isSurfaceName(value: string | undefined): value is SurfaceName {
8
+ return SURFACE_NAMES.includes(value as SurfaceName);
9
+ }
10
+
11
+ /**
12
+ * Which surfaces this process should bring up. `--mode web` and `--mode
13
+ * telegram` are the single-surface spellings of `--surfaces`, so a Telegram-only
14
+ * install needs no port and a web-only one needs no token.
15
+ */
16
+ function parse(args: ReadonlyArray<string>): ReadonlyArray<SurfaceName> {
17
+ let requested: string | undefined;
18
+ let mode: string | undefined;
19
+ Argv.scan(args, (key, take) => {
20
+ switch (key) {
21
+ case "--surfaces":
22
+ requested = take() ?? requested;
23
+ break;
24
+ case "--mode":
25
+ mode = take() ?? mode;
26
+ break;
27
+ default:
28
+ break;
29
+ }
30
+ });
31
+ if (requested === undefined) {
32
+ return isSurfaceName(mode) ? [mode] : SURFACE_NAMES;
33
+ }
34
+ const names = requested
35
+ .split(",")
36
+ .map((name) => name.trim())
37
+ .filter((name) => name.length > 0);
38
+ const unknown = names.filter((name) => !isSurfaceName(name));
39
+ if (names.length === 0 || unknown.length > 0) {
40
+ throw new Error(
41
+ `--surfaces takes a comma-separated list of ${SURFACE_NAMES.join(", ")}, got "${requested}"`
42
+ );
43
+ }
44
+ return SURFACE_NAMES.filter((name) => names.includes(name));
45
+ }
46
+
47
+ function format(names: ReadonlyArray<SurfaceName>): string {
48
+ return names.join(",");
49
+ }
50
+
51
+ /** Whether the caller named the surfaces outright, rather than implying them by mode. */
52
+ function named(args: ReadonlyArray<string>): boolean {
53
+ return args.some(
54
+ (arg) => arg === "--surfaces" || arg.startsWith("--surfaces=")
55
+ );
56
+ }
57
+
58
+ function union(
59
+ ...groups: ReadonlyArray<ReadonlyArray<SurfaceName>>
60
+ ): ReadonlyArray<SurfaceName> {
61
+ return SURFACE_NAMES.filter((name) =>
62
+ groups.some((group) => group.includes(name))
63
+ );
64
+ }
65
+
66
+ export const Surfaces = { parse, format, named, union };
@@ -0,0 +1,30 @@
1
+ import type { AgentRuntime } from "#core/session/AgentRuntime";
2
+ import { Bot } from "#telegram/Bot";
3
+ import { Config } from "#telegram/Config";
4
+ import type { Surface } from "./Daemon";
5
+
6
+ function create(args: ReadonlyArray<string>, runtime: AgentRuntime): Surface {
7
+ return {
8
+ name: "telegram",
9
+ start: async () => {
10
+ const config = await Config.load(Config.parseArgs(args));
11
+ const bot = new Bot(config, runtime);
12
+ await bot.start();
13
+ return { stop: () => bot.stop() };
14
+ },
15
+ };
16
+ }
17
+
18
+ /** `--print-config`, which resolves the bot's config the way a run would and stops there. */
19
+ async function printConfig(args: ReadonlyArray<string>): Promise<void> {
20
+ const config = await Config.load(Config.parseArgs(args));
21
+ console.log(
22
+ JSON.stringify(
23
+ { ...config, token: config.token ? "***" : undefined },
24
+ null,
25
+ 2
26
+ )
27
+ );
28
+ }
29
+
30
+ export const TelegramSurface = { create, printConfig };
@@ -0,0 +1,57 @@
1
+ import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
2
+
3
+ import { AttachmentStore } from "#core/attachments/AttachmentStore";
4
+ import type { AgentRuntime } from "#core/session/AgentRuntime";
5
+ import { SessionRegistry } from "#core/session/SessionRegistry";
6
+ import { Tools } from "#core/shared/Tools";
7
+ import { defaultAttachmentsRoot } from "#server/AttachmentEndpoint";
8
+ import { SendFileTool } from "#server/SendFileTool";
9
+ import { WebOptions } from "#server/WebOptions";
10
+ import { WsGateway } from "#server/WsGateway";
11
+ import type { Surface } from "./Daemon";
12
+
13
+ function create(args: ReadonlyArray<string>, runtime: AgentRuntime): Surface {
14
+ return {
15
+ name: "web",
16
+ start: async () => {
17
+ const options = WebOptions.parse(args);
18
+ const attachmentsRoot = defaultAttachmentsRoot();
19
+ const store = new AttachmentStore(attachmentsRoot);
20
+ const registry = new SessionRegistry({
21
+ runtime,
22
+ defaults: { cwd: options.cwd },
23
+ customTools: ({ cwd, sessionId }) => [
24
+ Tools.wrap(
25
+ SendFileTool.build({ store, cwd, sessionId })
26
+ ) as ToolDefinition,
27
+ ],
28
+ systemInstruction: async () =>
29
+ "The user is interacting with you via a web browser.",
30
+ });
31
+ await registry.init();
32
+
33
+ const gateway = new WsGateway({
34
+ registry,
35
+ hostname: options.hostname,
36
+ port: WebOptions.port(options),
37
+ attachmentsRoot,
38
+ ...(options.clientDir === undefined
39
+ ? {}
40
+ : { clientDir: options.clientDir }),
41
+ });
42
+ gateway.start();
43
+ process.stderr.write(
44
+ `pim-server listening on ${gateway.url} (web UI: http://${options.hostname}:${gateway.port})\n`
45
+ );
46
+
47
+ return {
48
+ stop: async () => {
49
+ await gateway.stop();
50
+ await registry.disposeAll();
51
+ },
52
+ };
53
+ },
54
+ };
55
+ }
56
+
57
+ export const WebSurface = { create };
@@ -0,0 +1,37 @@
1
+ import { AgentRuntime } from "#core/session/AgentRuntime";
2
+ import { Daemon, type Surface } from "./Daemon";
3
+ import { Surfaces } from "./Surfaces";
4
+ import { TelegramSurface } from "./TelegramSurface";
5
+ import { WebSurface } from "./WebSurface";
6
+
7
+ /** Every surface in this process shares one pi installation, and so one `auth.json`. */
8
+ export function build(args: ReadonlyArray<string>): ReadonlyArray<Surface> {
9
+ const runtime = new AgentRuntime();
10
+ return Surfaces.parse(args).map((name) =>
11
+ name === "web"
12
+ ? WebSurface.create(args, runtime)
13
+ : TelegramSurface.create(args, runtime)
14
+ );
15
+ }
16
+
17
+ export async function start(args: ReadonlyArray<string>): Promise<void> {
18
+ if (
19
+ Surfaces.parse(args).includes("telegram") &&
20
+ args.includes("--print-config")
21
+ ) {
22
+ await TelegramSurface.printConfig(args);
23
+ return;
24
+ }
25
+
26
+ const daemon = new Daemon(build(args));
27
+ const running = await daemon.start();
28
+ console.log(`[daemon] serving ${running.join(", ")}`);
29
+
30
+ await new Promise<void>((resolve) => {
31
+ for (const signal of ["SIGINT", "SIGTERM"] as const) {
32
+ process.once(signal, () => {
33
+ void daemon.stop().then(resolve);
34
+ });
35
+ }
36
+ });
37
+ }
@@ -0,0 +1,92 @@
1
+ import type { ProtocolVersion } from "./Protocol";
2
+
3
+ /** A file already uploaded via `POST /upload`, by the id that endpoint answered with; never a client-local path. */
4
+ export type AttachmentRef = {
5
+ readonly id: string;
6
+ };
7
+
8
+ /** Client → server. Every command carries an `id` so its response correlates. */
9
+ export type Command =
10
+ /** Opening frame of every connection; omit `sessionId` to start a new session in `cwd`. */
11
+ | {
12
+ readonly id: string;
13
+ readonly type: "attach";
14
+ readonly protocolVersion: ProtocolVersion;
15
+ readonly sessionId?: string;
16
+ readonly cwd?: string;
17
+ /** Copy model, thinking level and cwd from this session; ignored when `sessionId` is set or the session is not held open. */
18
+ readonly like?: string;
19
+ readonly fromSeq: number;
20
+ }
21
+ /** Sent into a running turn it steers that turn, landing before the next model call. */
22
+ | {
23
+ readonly id: string;
24
+ readonly type: "user_message";
25
+ readonly sessionId: string;
26
+ readonly text: string;
27
+ readonly attachments?: readonly AttachmentRef[];
28
+ }
29
+ | { readonly id: string; readonly type: "cancel"; readonly sessionId: string }
30
+ /** Take back the queued messages without stopping the turn; they come back on `restored`. */
31
+ | {
32
+ readonly id: string;
33
+ readonly type: "dequeue";
34
+ readonly sessionId: string;
35
+ }
36
+ | {
37
+ readonly id: string;
38
+ readonly type: "pick_files";
39
+ readonly sessionId: string;
40
+ readonly query: string;
41
+ readonly limit: number;
42
+ }
43
+ | {
44
+ readonly id: string;
45
+ readonly type: "pick_commands";
46
+ readonly sessionId: string;
47
+ readonly query: string;
48
+ readonly limit?: number;
49
+ }
50
+ | {
51
+ readonly id: string;
52
+ readonly type: "set_cwd" | "set_model" | "set_thinking";
53
+ readonly sessionId: string;
54
+ readonly value: string;
55
+ }
56
+ /** The session catalogue; answers before any `attach`. */
57
+ | {
58
+ readonly id: string;
59
+ readonly type: "list_sessions";
60
+ /** Restrict to one working directory; omit for every session on disk. */
61
+ readonly cwd?: string;
62
+ readonly limit?: number;
63
+ }
64
+ /** The models this server can switch to, plus the current model's thinking levels; answers without a session. */
65
+ | { readonly id: string; readonly type: "list_models" }
66
+ /** Subdirectories of `path` on the server's filesystem; errors rather than answering empty when it is not a readable directory. */
67
+ | { readonly id: string; readonly type: "list_dirs"; readonly path: string }
68
+ /** Read-only view of a subagent's transcript; `callId` is the parent's tool call and `sessionId` must be this connection's session. */
69
+ | {
70
+ readonly id: string;
71
+ readonly type: "watch_subagent";
72
+ readonly sessionId: string;
73
+ readonly callId: string;
74
+ readonly fromSeq: number;
75
+ }
76
+ /** Stop watching; succeeds whether or not this connection holds the watch. */
77
+ | {
78
+ readonly id: string;
79
+ readonly type: "unwatch_subagent";
80
+ readonly callId: string;
81
+ }
82
+ /** Update this install and restart it unconditionally; refused while any session is mid-turn unless `force`. */
83
+ | { readonly id: string; readonly type: "reload"; readonly force?: boolean };
84
+
85
+ export type CommandType = Command["type"];
86
+
87
+ /** A command before the transport stamps its correlation id. */
88
+ export type CommandDraft = Command extends infer T
89
+ ? T extends Command
90
+ ? Omit<T, "id">
91
+ : never
92
+ : never;
@@ -0,0 +1,23 @@
1
+ const PREFIX = "/image/";
2
+
3
+ /** Only the names a tool writes: a hex digest and one of the formats a provider takes. */
4
+ const NAME = /^[0-9a-f]{64}\.(?:png|jpg|gif|webp)$/;
5
+
6
+ /** Server-relative address of a picture a tool view names, the way an attachment's `url` is. */
7
+ function url(sha256: string, extension: string): string {
8
+ return `${PREFIX}${sha256}.${extension}`;
9
+ }
10
+
11
+ /** Whether the gateway should route this path to its picture handler. */
12
+ function owns(pathname: string): boolean {
13
+ return pathname.startsWith(PREFIX);
14
+ }
15
+
16
+ /** The `<sha256>.<ext>` a request addresses, or null when it is not a name we ever served. */
17
+ function nameOf(pathname: string): string | null {
18
+ const name = pathname.slice(PREFIX.length);
19
+ return owns(pathname) && NAME.test(name) ? name : null;
20
+ }
21
+
22
+ /** `GET /image/<sha256>.<ext>`: the one spelling the server answers and the client asks for. */
23
+ export const ImageRoute = { url, owns, nameOf };
@@ -0,0 +1,6 @@
1
+ export const PROTOCOL_VERSION = 1;
2
+
3
+ export type ProtocolVersion = typeof PROTOCOL_VERSION;
4
+
5
+ /** Close code for a client speaking a protocol version the server does not. */
6
+ export const CLOSE_PROTOCOL_MISMATCH = 4001;
@@ -0,0 +1,251 @@
1
+ import type { DirectoryListing } from "#core/shared/Directories";
2
+ import type { PickerItem } from "#core/picker/PickerItem";
3
+ import type { LeaseFrontend } from "#core/session/SessionLease";
4
+ import type { UpdateSkip } from "#core/shared/Updater";
5
+ import type { NoticeSeverity, ToolView } from "#core/view/ViewBlock";
6
+ import type { ProtocolVersion } from "./Protocol";
7
+
8
+ export type SessionStatus = "idle" | "thinking" | "streaming" | "tool";
9
+
10
+ export type TurnStats = {
11
+ readonly inputTokens: number;
12
+ readonly outputTokens: number;
13
+ readonly costUsd: number;
14
+ readonly durationMs: number;
15
+ };
16
+
17
+ export type ToolCallView = {
18
+ readonly callId: string;
19
+ readonly name: string;
20
+ readonly view: ToolView;
21
+ };
22
+
23
+ /** A file a user message carried; `url` is server-relative. */
24
+ export type AttachmentView = {
25
+ /** What to call it on screen; never a path. */
26
+ readonly name: string;
27
+ readonly url: string;
28
+ readonly isImage: boolean;
29
+ };
30
+
31
+ /** Projected from pi's session JSONL; `seq` is the line ordinal, so one line must emit at most one durable event and a client resumes at `seq > n`. */
32
+ export type DurableEvent =
33
+ | {
34
+ readonly seq: number;
35
+ readonly type: "message";
36
+ readonly messageId: string;
37
+ readonly role: "user" | "assistant";
38
+ readonly text: string;
39
+ /** When pi appended the entry, in epoch ms. */
40
+ readonly timestamp: number;
41
+ readonly thinking?: string;
42
+ /** Only ever set on a user message, which may carry nothing else. */
43
+ readonly attachments?: readonly AttachmentView[];
44
+ readonly toolCalls?: readonly ToolCallView[];
45
+ /** The model call failed, with what it said; only ever set on an assistant message. */
46
+ readonly error?: string;
47
+ }
48
+ | {
49
+ readonly seq: number;
50
+ readonly type: "tool_result";
51
+ readonly callId: string;
52
+ readonly name: string;
53
+ readonly view: ToolView;
54
+ readonly isError: boolean;
55
+ }
56
+ | {
57
+ readonly seq: number;
58
+ readonly type: "notice";
59
+ readonly severity: NoticeSeverity;
60
+ readonly text: string;
61
+ };
62
+
63
+ /** Progress of the one `reload` this server can have in flight. */
64
+ export type UpdateStateEvent =
65
+ | {
66
+ readonly type: "update_state";
67
+ readonly phase: "step";
68
+ readonly label: string;
69
+ }
70
+ | {
71
+ readonly type: "update_state";
72
+ readonly phase: "restarting";
73
+ readonly from: string;
74
+ readonly to: string;
75
+ readonly skipped: readonly UpdateSkip[];
76
+ }
77
+ /** New code is on disk but nothing will restart this process: no supervisor owns it. */
78
+ | {
79
+ readonly type: "update_state";
80
+ readonly phase: "stranded";
81
+ readonly from: string;
82
+ readonly to: string;
83
+ readonly skipped: readonly UpdateSkip[];
84
+ }
85
+ | {
86
+ readonly type: "update_state";
87
+ readonly phase: "failed";
88
+ readonly error: string;
89
+ };
90
+
91
+ /** Unsequenced live state, never replayed by ordinal; live `tool_call`s reappear in a durable message's `toolCalls`, so dedupe on `callId`. */
92
+ export type EphemeralEvent =
93
+ | {
94
+ readonly type: "attached";
95
+ readonly protocolVersion: ProtocolVersion;
96
+ readonly sessionId: string;
97
+ readonly cwd: string;
98
+ /** Highest durable `seq` at attach time; replay follows immediately. */
99
+ readonly head: number;
100
+ readonly pimVersion: string;
101
+ readonly piVersion: string;
102
+ }
103
+ /** Several events in one frame; apply them in order, each as if it had arrived alone. */
104
+ | { readonly type: "replay"; readonly events: readonly StreamEvent[] }
105
+ | {
106
+ readonly type: "message_start";
107
+ readonly role: "assistant";
108
+ readonly messageId: string;
109
+ }
110
+ /** Drop the live message with this id; the durable `message` superseding it was sent immediately before. */
111
+ | { readonly type: "message_retire"; readonly messageId: string }
112
+ | {
113
+ readonly type: "text_delta";
114
+ readonly messageId: string;
115
+ readonly delta: string;
116
+ }
117
+ | {
118
+ readonly type: "thinking_delta";
119
+ readonly messageId: string;
120
+ readonly delta: string;
121
+ }
122
+ | {
123
+ readonly type: "tool_call";
124
+ readonly callId: string;
125
+ readonly name: string;
126
+ readonly messageId: string;
127
+ readonly view: ToolView;
128
+ }
129
+ | {
130
+ readonly type: "tool_update";
131
+ readonly callId: string;
132
+ readonly view: ToolView;
133
+ }
134
+ /** Settled view until pi appends the `tool_result` for the same `callId`, which supersedes it. */
135
+ | {
136
+ readonly type: "tool_end";
137
+ readonly callId: string;
138
+ readonly view: ToolView;
139
+ readonly isError: boolean;
140
+ }
141
+ /** Every picker answer a client holds for this cwd is stale; drop the cache and re-query. */
142
+ | {
143
+ readonly type: "picker_invalidate";
144
+ readonly scope: "files" | "commands" | "all";
145
+ readonly cwd: string;
146
+ }
147
+ /** A child's events, applied against the child's transcript; sent only to the connection that asked, and never resumed. */
148
+ | {
149
+ readonly type: "subagent_events";
150
+ /** The parent tool call the watch was opened on. */
151
+ readonly callId: string;
152
+ readonly events: readonly StreamEvent[];
153
+ }
154
+ | { readonly type: "turn_end"; readonly stats: TurnStats }
155
+ /** Sent to every connection, not just those attached; only sessions this server holds open are reported. */
156
+ | {
157
+ readonly type: "session_activity";
158
+ readonly sessionId: string;
159
+ readonly status: SessionStatus;
160
+ }
161
+ /** Sent to every connection; the read cursor is one per session, not one per client. */
162
+ | { readonly type: "session_read"; readonly sessionId: string }
163
+ /** Sent to every connection: the sessions on disk changed, so any listing a client holds is stale. */
164
+ | { readonly type: "sessions_changed" }
165
+ /** Sent to every connection; the restart it ends in drops every socket. */
166
+ | UpdateStateEvent
167
+ | {
168
+ readonly type: "session_state";
169
+ readonly cwd: string;
170
+ readonly model: string;
171
+ /** Display name for `model`; absent until one resolves. */
172
+ readonly modelLabel?: string;
173
+ readonly thinking: string;
174
+ readonly cost: number;
175
+ readonly status: SessionStatus;
176
+ /** False while another process holds this session's turn lease. */
177
+ readonly writable: boolean;
178
+ /** Who holds it; absent when nothing does, or when their record is torn. */
179
+ readonly heldBy?: {
180
+ readonly frontend: LeaseFrontend;
181
+ readonly pid: number;
182
+ };
183
+ readonly tps?: number;
184
+ /** Elapsed run time of the turn in flight, by the server's clock; absent when idle. */
185
+ readonly turnElapsedMs?: number;
186
+ /** Context filled, 0–100. Absent until a turn has reported usage. */
187
+ readonly contextPercent?: number;
188
+ readonly contextWindow?: number;
189
+ /** The cwd's git branch, absent outside a repository. */
190
+ readonly branch?: string;
191
+ /** Paths git reports as changed; zero is a clean tree. */
192
+ readonly dirtyCount?: number;
193
+ readonly ahead?: number;
194
+ readonly behind?: number;
195
+ }
196
+ /** A frame the server could not attribute to any command. */
197
+ | { readonly type: "error"; readonly message: string };
198
+
199
+ /** One row of the session catalogue. */
200
+ export type SessionSummaryView = {
201
+ readonly sessionId: string;
202
+ readonly cwd: string;
203
+ readonly createdAt: number;
204
+ /** End of the last completed turn and the catalogue's sort key, never the file mtime; falls back to `createdAt`. */
205
+ readonly settledAt: number;
206
+ /** The session's first user message, trimmed; absent when it has none. */
207
+ readonly title?: string;
208
+ /** Has answered since anything last read it; absent means it has not. */
209
+ readonly unread?: boolean;
210
+ /** Absent when idle, and for a session this server does not hold open. */
211
+ readonly status?: SessionStatus;
212
+ };
213
+
214
+ /** One model the server can be switched to, for the composer's model menu. */
215
+ export type ModelView = {
216
+ readonly id: string;
217
+ readonly label: string;
218
+ /** The provider half of `id`, so a client can tag a row without parsing it. */
219
+ readonly provider: string;
220
+ };
221
+
222
+ /** Answer to one `Command`, correlated by its `id`. Never sequenced. */
223
+ export type ResponseEvent = {
224
+ readonly type: "response";
225
+ readonly id: string;
226
+ readonly success: boolean;
227
+ readonly error?: string;
228
+ /** Ranked rows, for the commands that answer with data (`pick_*`). */
229
+ readonly items?: readonly PickerItem[];
230
+ /** The catalogue, for `list_sessions`. */
231
+ readonly sessions?: readonly SessionSummaryView[];
232
+ /** The model catalogue, for `list_models`. */
233
+ readonly models?: readonly ModelView[];
234
+ /** What the *current* model supports, on the same answer. */
235
+ readonly thinkingLevels?: readonly string[];
236
+ /** One directory's subdirectories, for `list_dirs`. */
237
+ readonly directory?: DirectoryListing;
238
+ /** For `cancel` and `dequeue`: queued messages pi gave back, now owned by the client that asked. */
239
+ readonly restored?: readonly string[];
240
+ };
241
+
242
+ export type ServerEvent = DurableEvent | EphemeralEvent | ResponseEvent;
243
+
244
+ /** Anything a session emits: everything on the wire but an answer to a command. */
245
+ export type StreamEvent = DurableEvent | EphemeralEvent;
246
+
247
+ export type ServerEventType = ServerEvent["type"];
248
+
249
+ export function isDurableEvent(event: ServerEvent): event is DurableEvent {
250
+ return "seq" in event;
251
+ }