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,148 @@
1
+ import { join } from "node:path";
2
+
3
+ import { Cli as Argv } from "#core/shared/Cli";
4
+ import { Fs } from "#core/shared/Fs";
5
+ import { Paths } from "#core/shared/Paths";
6
+
7
+ export type Cli = {
8
+ readonly token?: string;
9
+ readonly allow?: string;
10
+ readonly cwd?: string;
11
+ readonly model?: string;
12
+ readonly configDir?: string;
13
+ readonly printConfig: boolean;
14
+ };
15
+
16
+ export type TelegramConfig = {
17
+ readonly token: string;
18
+ readonly allow: ReadonlyArray<number>;
19
+ readonly cwd: string;
20
+ readonly model?: string;
21
+ readonly configDir: string;
22
+ };
23
+
24
+ export const THINKING_LEVELS = [
25
+ "off",
26
+ "minimal",
27
+ "low",
28
+ "medium",
29
+ "high",
30
+ "xhigh",
31
+ ] as const;
32
+ export type ThinkingLevelOpt = (typeof THINKING_LEVELS)[number];
33
+
34
+ export const LOGS_MODES = ["off", "tool", "text", "verbose"] as const;
35
+ export type LogsMode = (typeof LOGS_MODES)[number];
36
+
37
+ function parseArgs(args: ReadonlyArray<string>): Cli {
38
+ let token: string | undefined;
39
+ let allow: string | undefined;
40
+ let cwd: string | undefined;
41
+ let model: string | undefined;
42
+ let configDir: string | undefined;
43
+ let printConfig = false;
44
+
45
+ Argv.scan(args, (key, take) => {
46
+ switch (key) {
47
+ case "--token":
48
+ token = take();
49
+ break;
50
+ case "--allow":
51
+ allow = take();
52
+ break;
53
+ case "--cwd":
54
+ cwd = take();
55
+ break;
56
+ case "--model":
57
+ model = take();
58
+ break;
59
+ case "--config-dir":
60
+ configDir = take();
61
+ break;
62
+ case "--print-config":
63
+ printConfig = true;
64
+ break;
65
+ case "--mode":
66
+ take();
67
+ break;
68
+ }
69
+ });
70
+ return { token, allow, cwd, model, configDir, printConfig };
71
+ }
72
+
73
+ async function load(cli: Cli): Promise<TelegramConfig> {
74
+ const configDir = cli.configDir ?? join(Paths.pimHomeDir(), "telegram");
75
+
76
+ const filePath = join(configDir, "config.json");
77
+ const fileConfig = await Fs.readJsonOrEmpty<Partial<TelegramConfig>>(
78
+ filePath,
79
+ {}
80
+ );
81
+
82
+ const token =
83
+ cli.token ?? process.env.PIM_TELEGRAM_BOT_TOKEN ?? fileConfig.token;
84
+ if (!token) {
85
+ throw new Error(
86
+ "Bot token required (set --token, PIM_TELEGRAM_BOT_TOKEN, or 'token' in config.json)"
87
+ );
88
+ }
89
+
90
+ const allowSrc = cli.allow ?? process.env.PIM_TELEGRAM_ALLOW;
91
+ const allow = allowSrc
92
+ ? parseAllow(allowSrc)
93
+ : normalizeAllow(fileConfig.allow);
94
+
95
+ const cwd = cli.cwd ?? fileConfig.cwd ?? process.cwd();
96
+ const model = cli.model ?? fileConfig.model;
97
+
98
+ return { token, allow, cwd, model, configDir };
99
+ }
100
+
101
+ async function save(config: TelegramConfig): Promise<void> {
102
+ const filePath = join(config.configDir, "config.json");
103
+ const data = JSON.stringify(
104
+ {
105
+ token: config.token,
106
+ allow: config.allow,
107
+ cwd: config.cwd,
108
+ model: config.model,
109
+ },
110
+ null,
111
+ 2
112
+ );
113
+ await Fs.writeAtomic(filePath, data, 0o600);
114
+ }
115
+
116
+ function parseAllow(s: string): ReadonlyArray<number> {
117
+ return s
118
+ .split(",")
119
+ .map((x) => x.trim())
120
+ .filter((x) => x.length > 0)
121
+ .map((x) => {
122
+ const n = Number(x);
123
+ if (!Number.isFinite(n)) {
124
+ throw new Error(`Invalid chat ID in allow list: ${x}`);
125
+ }
126
+ return n;
127
+ });
128
+ }
129
+
130
+ function normalizeAllow(value: unknown): ReadonlyArray<number> {
131
+ if (value === undefined || value === null) {
132
+ return [];
133
+ }
134
+ if (typeof value === "string") {
135
+ return parseAllow(value);
136
+ }
137
+ if (typeof value === "number") {
138
+ return parseAllow(String(value));
139
+ }
140
+ if (Array.isArray(value)) {
141
+ return parseAllow(value.join(","));
142
+ }
143
+ throw new Error(
144
+ `Invalid 'allow' in config.json: expected number, string, or array, got ${typeof value}`
145
+ );
146
+ }
147
+
148
+ export const Config = { parseArgs, load, save };
@@ -0,0 +1,235 @@
1
+ import { MarkdownPainter } from "#core/view/MarkdownPainter";
2
+
3
+ type Align = "left" | "center" | "right";
4
+
5
+ type TableRow = ReadonlyArray<string>;
6
+
7
+ type Segment =
8
+ | { readonly kind: "md"; readonly text: string }
9
+ | {
10
+ readonly kind: "table";
11
+ readonly rows: ReadonlyArray<TableRow>;
12
+ readonly aligns: ReadonlyArray<Align | undefined>;
13
+ };
14
+
15
+ type RenderOptions = {
16
+ readonly italics?: boolean;
17
+ };
18
+
19
+ const SAFE_LINK = /^(https?:|tg:|mailto:)/i;
20
+
21
+ // Bun's GFM strikes on a lone `~`; Telegram and CommonMark require `~~`.
22
+ const STRIKETHROUGH = /(?<!~)~~(?!~)((?:[^~]|~(?!~))+?)~~(?!~)/g;
23
+
24
+ function toHtml(md: string, options: RenderOptions = {}): string {
25
+ const segments = split(md);
26
+ let out = "";
27
+ for (const seg of segments) {
28
+ out +=
29
+ seg.kind === "md"
30
+ ? renderMd(seg.text, options)
31
+ : renderTable(seg.rows, seg.aligns);
32
+ }
33
+ return out.trim();
34
+ }
35
+
36
+ function escape(s: string): string {
37
+ return MarkdownPainter.escape(s);
38
+ }
39
+
40
+ const RENDERERS = {
41
+ text: (c: string): string => escape(c).replace(STRIKETHROUGH, "<s>$1</s>"),
42
+ paragraph: (c: string): string => `<p>${c}</p>`,
43
+ heading: (c: string, meta?: { level?: number }): string => {
44
+ const level = Math.min(6, Math.max(1, meta?.level ?? 1));
45
+ return `<h${level}>${c}</h${level}>`;
46
+ },
47
+ strong: (c: string): string => `<b>${c}</b>`,
48
+ emphasis: (c: string): string => `<i>${c}</i>`,
49
+ codespan: (c: string): string => `<code>${c}</code>`,
50
+ code: (c: string, meta?: { language?: string }): string => {
51
+ const body = c.replace(/\n+$/, "");
52
+ const lang = meta?.language;
53
+ if (lang === "math") {
54
+ return `<tg-math-block>${body}</tg-math-block>`;
55
+ }
56
+ const open = lang
57
+ ? `<pre><code class="language-${escape(lang)}">`
58
+ : "<pre>";
59
+ const close = lang ? "</code></pre>" : "</pre>";
60
+ return `${open}${body}${close}`;
61
+ },
62
+ link: (c: string, meta?: { href?: string }): string => {
63
+ const href = meta?.href ?? "";
64
+ return SAFE_LINK.test(href) ? `<a href="${escape(href)}">${c}</a>` : c;
65
+ },
66
+ image: (c: string, meta?: { src?: string }): string => {
67
+ const src = meta?.src ?? "";
68
+ const alt = c || src;
69
+ return SAFE_LINK.test(src) ? `<a href="${escape(src)}">${alt}</a>` : alt;
70
+ },
71
+ blockquote: (c: string): string => `<blockquote>${c}</blockquote>`,
72
+ list: (c: string, meta?: { ordered?: boolean; start?: number }): string => {
73
+ if (meta?.ordered) {
74
+ const start = meta.start ?? 1;
75
+ const startAttr = start > 1 ? ` start="${start}"` : "";
76
+ return `<ol${startAttr}>${c}</ol>`;
77
+ }
78
+ return `<ul>${c}</ul>`;
79
+ },
80
+ listItem: (c: string, meta?: { checked?: boolean }): string =>
81
+ listItemHtml(c.replace(/\n+$/, ""), meta?.checked),
82
+ hr: (): string => "<hr/>",
83
+ br: (): string => "<br>",
84
+ table: (c: string): string => c,
85
+ };
86
+
87
+ const ITALIC_RENDERERS = {
88
+ ...RENDERERS,
89
+ paragraph: (c: string): string => `<p>${italic(c)}</p>`,
90
+ heading: (c: string, meta?: { level?: number }): string => {
91
+ const level = Math.min(6, Math.max(1, meta?.level ?? 1));
92
+ return `<h${level}>${italic(c)}</h${level}>`;
93
+ },
94
+ listItem: (c: string, meta?: { checked?: boolean }): string =>
95
+ listItemHtml(italicListItemBody(c.replace(/\n+$/, "")), meta?.checked),
96
+ };
97
+
98
+ function listItemHtml(body: string, checked?: boolean): string {
99
+ if (checked === true) {
100
+ return `<li><input type="checkbox" checked> ${body}</li>`;
101
+ }
102
+ if (checked === false) {
103
+ return `<li><input type="checkbox"> ${body}</li>`;
104
+ }
105
+ return `<li>${body}</li>`;
106
+ }
107
+
108
+ function renderMd(md: string, options: RenderOptions = {}): string {
109
+ if (!md.trim()) {
110
+ return "";
111
+ }
112
+ return Bun.markdown.render(md, renderers(options), {
113
+ strikethrough: false,
114
+ });
115
+ }
116
+
117
+ function renderers(options: RenderOptions): typeof RENDERERS {
118
+ return options.italics ? ITALIC_RENDERERS : RENDERERS;
119
+ }
120
+
121
+ function italic(text: string): string {
122
+ return text ? `<i>${text}</i>` : "";
123
+ }
124
+
125
+ // Italicize only the leading text: a child <ul>/<ol> inside <i> is invalid HTML.
126
+ function italicListItemBody(body: string): string {
127
+ const nestedListIndex = body.search(/<(?:ul|ol)\b/);
128
+ if (nestedListIndex < 0) {
129
+ return italic(body);
130
+ }
131
+ const before = body.slice(0, nestedListIndex);
132
+ return `${italic(before)}${body.slice(nestedListIndex)}`;
133
+ }
134
+
135
+ function renderInline(md: string): string {
136
+ return renderMd(md)
137
+ .replace(/^<p>/, "")
138
+ .replace(/<\/p>$/, "")
139
+ .trim();
140
+ }
141
+
142
+ function split(md: string): ReadonlyArray<Segment> {
143
+ const lines = md.split("\n");
144
+ const segments: Segment[] = [];
145
+ let buf: string[] = [];
146
+
147
+ const flushMd = (): void => {
148
+ if (buf.length > 0) {
149
+ segments.push({ kind: "md", text: buf.join("\n") });
150
+ buf = [];
151
+ }
152
+ };
153
+
154
+ for (let i = 0; i < lines.length; i++) {
155
+ const line = lines[i]!;
156
+ const next = lines[i + 1];
157
+ if (isPipeLine(line) && next !== undefined && isTableSeparator(next)) {
158
+ flushMd();
159
+ const rows: string[][] = [parseRow(line)];
160
+ const aligns = parseAligns(next);
161
+ i += 1;
162
+ while (i + 1 < lines.length && isPipeLine(lines[i + 1]!)) {
163
+ i += 1;
164
+ rows.push(parseRow(lines[i]!));
165
+ }
166
+ segments.push({ kind: "table", rows, aligns });
167
+ continue;
168
+ }
169
+ buf.push(line);
170
+ }
171
+ flushMd();
172
+ return segments;
173
+ }
174
+
175
+ function isPipeLine(line: string): boolean {
176
+ return /^\s*\|.*\|\s*$/.test(line);
177
+ }
178
+
179
+ function isTableSeparator(line: string): boolean {
180
+ return /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)+\|?\s*$/.test(line);
181
+ }
182
+
183
+ function parseRow(line: string): string[] {
184
+ const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
185
+ return trimmed.split("|").map((cell) => cell.trim());
186
+ }
187
+
188
+ function parseAligns(sep: string): (Align | undefined)[] {
189
+ return parseRow(sep).map((cell) => {
190
+ const left = cell.startsWith(":");
191
+ const right = cell.endsWith(":");
192
+ if (left && right) {
193
+ return "center";
194
+ }
195
+ if (right) {
196
+ return "right";
197
+ }
198
+ if (left) {
199
+ return "left";
200
+ }
201
+ return undefined;
202
+ });
203
+ }
204
+
205
+ function renderTable(
206
+ rows: ReadonlyArray<TableRow>,
207
+ aligns: ReadonlyArray<Align | undefined>
208
+ ): string {
209
+ if (rows.length < 2) {
210
+ return "";
211
+ }
212
+ const header = rows[0]!;
213
+ const dataRows = rows.slice(1);
214
+ if (dataRows.length === 0) {
215
+ return "";
216
+ }
217
+ const attr = (col: number): string => {
218
+ const align = aligns[col];
219
+ return align ? ` align="${align}"` : "";
220
+ };
221
+ const cells = (row: TableRow, tag: "th" | "td"): string =>
222
+ header
223
+ .map((_, c) => `<${tag}${attr(c)}>${renderInline(row[c] ?? "")}</${tag}>`)
224
+ .join("");
225
+
226
+ let out = "<table>";
227
+ out += `<tr>${cells(header, "th")}</tr>`;
228
+ for (const row of dataRows) {
229
+ out += `<tr>${cells(row, "td")}</tr>`;
230
+ }
231
+ out += "</table>";
232
+ return out;
233
+ }
234
+
235
+ export const Markdown = { toHtml, escape };
@@ -0,0 +1,152 @@
1
+ import type { PromptOptions } from "@earendil-works/pi-coding-agent";
2
+ import type { Context, Filter } from "grammy";
3
+ import { extname, join } from "node:path";
4
+
5
+ import {
6
+ AttachmentStore,
7
+ type StoredAttachment,
8
+ } from "#core/attachments/AttachmentStore";
9
+ import { Attachments } from "#core/attachments/Attachments";
10
+ import type { SessionId } from "./Session";
11
+
12
+ type FileRef = {
13
+ readonly fileId: string;
14
+ readonly uniqueId?: string;
15
+ readonly name?: string;
16
+ readonly mimeType: string;
17
+ readonly ext: string;
18
+ };
19
+
20
+ export type Prompt = {
21
+ readonly text: string;
22
+ readonly options: Pick<PromptOptions, "images">;
23
+ };
24
+
25
+ const REPLY_QUOTE_HEAD = 128;
26
+ const REPLY_QUOTE_TAIL = 128;
27
+
28
+ async function toPrompt(
29
+ ctx: Filter<Context, "message">,
30
+ token: string,
31
+ configDir: string,
32
+ sessionId: SessionId
33
+ ): Promise<Prompt | undefined> {
34
+ const message = ctx.message;
35
+ const text = ("text" in message ? message.text : undefined) ?? "";
36
+ const caption = ("caption" in message ? message.caption : undefined) ?? "";
37
+ const files = await download(ctx, token, configDir, sessionId);
38
+ const { lines: attachments, images } = Attachments.render(files);
39
+
40
+ const body = (text || caption || "").trim();
41
+ if (!body && images.length === 0 && attachments.length === 0) {
42
+ return undefined;
43
+ }
44
+ const replyContext = buildReplyContext(ctx);
45
+ const promptText = [replyContext, body, ...attachments]
46
+ .filter(Boolean)
47
+ .join("\n\n")
48
+ .trim();
49
+ return {
50
+ text: promptText,
51
+ options: images.length > 0 ? { images } : {},
52
+ };
53
+ }
54
+
55
+ function buildReplyContext(
56
+ ctx: Filter<Context, "message">
57
+ ): string | undefined {
58
+ const reply = ctx.message.reply_to_message;
59
+ if (!reply) {
60
+ return undefined;
61
+ }
62
+ const raw = ctx.message.quote?.text ?? reply.text ?? reply.caption ?? "";
63
+ const trimmed = raw.trim();
64
+ if (!trimmed) {
65
+ return undefined;
66
+ }
67
+ const truncated =
68
+ trimmed.length > REPLY_QUOTE_HEAD + REPLY_QUOTE_TAIL + 1
69
+ ? `${trimmed.slice(0, REPLY_QUOTE_HEAD)}…${trimmed.slice(-REPLY_QUOTE_TAIL)}`
70
+ : trimmed;
71
+ const quoted = truncated
72
+ .split("\n")
73
+ .map((line) => `> ${line}`)
74
+ .join("\n");
75
+ const isFromBot = reply.from?.id === ctx.me.id;
76
+ const label = isFromBot
77
+ ? "Replying to your earlier message:"
78
+ : "Replying to my earlier message:";
79
+ return `${label}\n${quoted}`;
80
+ }
81
+
82
+ async function download(
83
+ ctx: Filter<Context, "message">,
84
+ token: string,
85
+ configDir: string,
86
+ sessionId: SessionId
87
+ ): Promise<ReadonlyArray<StoredAttachment>> {
88
+ const ref = refOf(ctx);
89
+ if (!ref) {
90
+ return [];
91
+ }
92
+ const telegramFile = await ctx.api.getFile(ref.fileId);
93
+ if (!telegramFile.file_path) {
94
+ return [];
95
+ }
96
+ const url = `https://api.telegram.org/file/bot${token}/${telegramFile.file_path}`;
97
+ const response = await fetch(url);
98
+ if (!response.ok) {
99
+ throw new Error(`Telegram file download failed: ${response.status}`);
100
+ }
101
+ const store = new AttachmentStore(join(configDir, "attachments"));
102
+ return [
103
+ await store.store(String(sessionId.chatId), {
104
+ bytes: await response.arrayBuffer(),
105
+ mimeType: ref.mimeType,
106
+ stem: ref.uniqueId ?? ref.fileId,
107
+ ext:
108
+ extname(telegramFile.file_path) || extname(ref.name ?? "") || ref.ext,
109
+ }),
110
+ ];
111
+ }
112
+
113
+ const MEDIA_KINDS = [
114
+ {
115
+ key: "document",
116
+ defaultMime: "application/octet-stream",
117
+ defaultExt: ".bin",
118
+ },
119
+ { key: "video", defaultMime: "video/mp4", defaultExt: ".mp4" },
120
+ { key: "audio", defaultMime: "audio/mpeg", defaultExt: ".mp3" },
121
+ { key: "voice", defaultMime: "audio/ogg", defaultExt: ".ogg" },
122
+ ] as const;
123
+
124
+ function refOf(ctx: Filter<Context, "message">): FileRef | undefined {
125
+ const message = ctx.message;
126
+ if ("photo" in message && message.photo) {
127
+ const photo = message.photo.at(-1)!;
128
+ return {
129
+ fileId: photo.file_id,
130
+ uniqueId: photo.file_unique_id,
131
+ mimeType: "image/jpeg",
132
+ ext: ".jpg",
133
+ };
134
+ }
135
+ for (const { key, defaultMime, defaultExt } of MEDIA_KINDS) {
136
+ const file = key in message ? message[key] : undefined;
137
+ if (!file) {
138
+ continue;
139
+ }
140
+ const name = "file_name" in file ? file.file_name : undefined;
141
+ return {
142
+ fileId: file.file_id,
143
+ uniqueId: file.file_unique_id,
144
+ name,
145
+ mimeType: file.mime_type ?? defaultMime,
146
+ ext: extname(name ?? "") || defaultExt,
147
+ };
148
+ }
149
+ return undefined;
150
+ }
151
+
152
+ export const Message = { toPrompt };