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,88 @@
1
+ import { type Static, Type } from "typebox";
2
+
3
+ export type ScheduleSpec =
4
+ | { readonly type: "once"; readonly at: string }
5
+ | { readonly type: "interval"; readonly every: string }
6
+ | { readonly type: "cron"; readonly expr: string };
7
+
8
+ export type ScheduledTask = {
9
+ readonly id: string;
10
+ readonly prompt: string;
11
+ readonly chatId: number;
12
+ readonly threadId: number | undefined;
13
+ readonly schedule: ScheduleSpec;
14
+ readonly status: "active" | "paused";
15
+ readonly nextRun: string;
16
+ readonly expires: string | null;
17
+ readonly isolatedSession: boolean;
18
+ readonly createdAt: string;
19
+ };
20
+
21
+ const scheduleSchema = Type.Union([
22
+ Type.Object({
23
+ type: Type.Literal("once"),
24
+ at: Type.String({
25
+ description:
26
+ "RFC3339 timestamp in the future, UTC. Example: '2026-05-14T15:30:00Z'.",
27
+ }),
28
+ }),
29
+ Type.Object({
30
+ type: Type.Literal("interval"),
31
+ every: Type.String({
32
+ description:
33
+ "Duration like '30m', '2h', '1h30m'. Units: s, m, h, d. Minimum 1 minute.",
34
+ }),
35
+ }),
36
+ Type.Object({
37
+ type: Type.Literal("cron"),
38
+ expr: Type.String({
39
+ description:
40
+ "Standard 5-field cron expression in UTC. Macros @hourly, @daily, @weekly, @monthly, @yearly supported.",
41
+ }),
42
+ }),
43
+ ]);
44
+
45
+ export const taskToolSchema = Type.Object({
46
+ action: Type.Union(
47
+ [
48
+ Type.Literal("create"),
49
+ Type.Literal("list"),
50
+ Type.Literal("delete"),
51
+ Type.Literal("pause"),
52
+ Type.Literal("resume"),
53
+ Type.Literal("update_prompt"),
54
+ ],
55
+ {
56
+ description:
57
+ "create: schedule a new task. list: see this thread's tasks. delete/pause/resume: by id. update_prompt: change a task's prompt.",
58
+ }
59
+ ),
60
+ prompt: Type.Optional(
61
+ Type.String({
62
+ description:
63
+ "Required for 'create' and 'update_prompt'. The instruction the agent will receive when the task fires.",
64
+ minLength: 1,
65
+ })
66
+ ),
67
+ schedule: Type.Optional(scheduleSchema),
68
+ expires: Type.Optional(
69
+ Type.String({
70
+ description:
71
+ "Optional RFC3339 timestamp after which the task auto-deletes without firing again.",
72
+ })
73
+ ),
74
+ isolatedSession: Type.Optional(
75
+ Type.Boolean({
76
+ description:
77
+ "If true, run in a fresh/isolated session with no chat history. Default false.",
78
+ })
79
+ ),
80
+ id: Type.Optional(
81
+ Type.String({
82
+ description:
83
+ "Task id. Required for delete, pause, resume, update_prompt.",
84
+ })
85
+ ),
86
+ });
87
+
88
+ export type TaskToolInput = Static<typeof taskToolSchema>;
@@ -0,0 +1,67 @@
1
+ import { readdir, unlink } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ import { Fs } from "#core/shared/Fs";
5
+ import type { ScheduledTask } from "./TaskSchema";
6
+
7
+ async function loadAll(
8
+ configDir: string
9
+ ): Promise<ReadonlyArray<ScheduledTask>> {
10
+ let entries: string[];
11
+ try {
12
+ entries = await readdir(dir(configDir));
13
+ } catch (err) {
14
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
15
+ return [];
16
+ }
17
+ throw err;
18
+ }
19
+ const loaded = await Promise.all(
20
+ entries
21
+ .filter((name) => name.endsWith(".json"))
22
+ .map((name) =>
23
+ Fs.readJsonOrEmpty<ScheduledTask | undefined>(
24
+ join(dir(configDir), name),
25
+ undefined
26
+ )
27
+ )
28
+ );
29
+ return loaded.filter(
30
+ (data): data is ScheduledTask =>
31
+ !!data && typeof data === "object" && "id" in data
32
+ );
33
+ }
34
+
35
+ async function save(configDir: string, task: ScheduledTask): Promise<void> {
36
+ await Fs.writeAtomic(path(configDir, task.id), JSON.stringify(task, null, 2));
37
+ }
38
+
39
+ async function remove(configDir: string, id: string): Promise<void> {
40
+ try {
41
+ await unlink(path(configDir, id));
42
+ } catch (err) {
43
+ if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
44
+ throw err;
45
+ }
46
+ }
47
+ }
48
+
49
+ function makeId(prompt: string): string {
50
+ const slug = prompt
51
+ .toLowerCase()
52
+ .replace(/[^a-z0-9]+/g, "-")
53
+ .replace(/^-+|-+$/g, "")
54
+ .slice(0, 40);
55
+ const rand = Math.random().toString(36).slice(2, 8);
56
+ return slug ? `${slug}-${rand}` : rand;
57
+ }
58
+
59
+ function dir(configDir: string): string {
60
+ return join(configDir, "tasks");
61
+ }
62
+
63
+ function path(configDir: string, id: string): string {
64
+ return join(dir(configDir), `${id}.json`);
65
+ }
66
+
67
+ export const TaskStore = { loadAll, save, remove, makeId };
@@ -0,0 +1,208 @@
1
+ import {
2
+ defineTool,
3
+ type AgentToolResult,
4
+ } from "@earendil-works/pi-coding-agent";
5
+
6
+ import type { PimToolDefinition } from "#core/shared/Tools";
7
+ import type { Span, ToolView } from "#core/view/ViewBlock";
8
+ import type { SessionId } from "./Session";
9
+ import type { TaskScheduler } from "./TaskScheduler";
10
+ import {
11
+ taskToolSchema,
12
+ type ScheduledTask,
13
+ type TaskToolInput,
14
+ } from "./TaskSchema";
15
+
16
+ export type TaskToolDeps = {
17
+ readonly scheduler: TaskScheduler;
18
+ readonly sessionId: SessionId;
19
+ };
20
+
21
+ function build(deps: TaskToolDeps): PimToolDefinition<typeof taskToolSchema> {
22
+ return {
23
+ ...defineTool({
24
+ name: "task",
25
+ label: "task",
26
+ description:
27
+ "Manage scheduled/recurring tasks for this Telegram chat/thread.",
28
+ parameters: taskToolSchema,
29
+ async execute(_id, params) {
30
+ const input = params as TaskToolInput;
31
+ switch (input.action) {
32
+ case "create":
33
+ return await create(deps, input);
34
+ case "list":
35
+ return await list(deps);
36
+ case "delete":
37
+ return await deleteTask(deps, input);
38
+ case "pause":
39
+ case "resume":
40
+ return await setStatus(deps, input);
41
+ case "update_prompt":
42
+ return await updatePrompt(deps, input);
43
+ }
44
+ },
45
+ }),
46
+ toViewModel: ({ args }): ToolView => ({
47
+ label: "Task",
48
+ icon: "clock",
49
+ title: [
50
+ {
51
+ kind: "spans",
52
+ spans: taskSpans((args ?? {}) as Partial<TaskToolInput>),
53
+ },
54
+ ],
55
+ }),
56
+ };
57
+ }
58
+
59
+ async function create(
60
+ deps: TaskToolDeps,
61
+ input: TaskToolInput
62
+ ): Promise<AgentToolResult<unknown>> {
63
+ const prompt = required(input, "prompt");
64
+ const schedule = required(input, "schedule");
65
+ const task = await deps.scheduler.create(deps.sessionId, {
66
+ prompt,
67
+ schedule,
68
+ expires: input.expires,
69
+ isolatedSession: input.isolatedSession,
70
+ });
71
+ return text(`Created task ${task.id}. Next run: ${task.nextRun}.`, task);
72
+ }
73
+
74
+ async function list(deps: TaskToolDeps): Promise<AgentToolResult<unknown>> {
75
+ const tasks = await deps.scheduler.list(deps.sessionId);
76
+ return tasks.length === 0
77
+ ? text("No tasks scheduled for this thread.", { tasks: [] })
78
+ : text(JSON.stringify(tasks), { tasks });
79
+ }
80
+
81
+ async function deleteTask(
82
+ deps: TaskToolDeps,
83
+ input: TaskToolInput
84
+ ): Promise<AgentToolResult<unknown>> {
85
+ const id = required(input, "id");
86
+ const ok = await deps.scheduler.delete(deps.sessionId, id);
87
+ if (!ok) {
88
+ throw notFound(id);
89
+ }
90
+ return text(`Deleted task ${id}.`, { id });
91
+ }
92
+
93
+ async function setStatus(
94
+ deps: TaskToolDeps,
95
+ input: TaskToolInput
96
+ ): Promise<AgentToolResult<unknown>> {
97
+ const id = required(input, "id");
98
+ const paused = input.action === "pause";
99
+ const task = found(
100
+ await deps.scheduler.setStatus(
101
+ deps.sessionId,
102
+ id,
103
+ paused ? "paused" : "active"
104
+ ),
105
+ id
106
+ );
107
+ return text(
108
+ paused
109
+ ? `Paused task ${task.id}. Will not fire until resumed.`
110
+ : `Resumed task ${task.id}. Next run: ${task.nextRun}.`,
111
+ task
112
+ );
113
+ }
114
+
115
+ async function updatePrompt(
116
+ deps: TaskToolDeps,
117
+ input: TaskToolInput
118
+ ): Promise<AgentToolResult<unknown>> {
119
+ const id = required(input, "id");
120
+ const prompt = required(input, "prompt");
121
+ const task = found(
122
+ await deps.scheduler.updatePrompt(deps.sessionId, id, prompt),
123
+ id
124
+ );
125
+ return text(`Updated prompt for task ${task.id}.`, task);
126
+ }
127
+
128
+ function text(message: string, details: unknown): AgentToolResult<unknown> {
129
+ return {
130
+ content: [{ type: "text" as const, text: message }],
131
+ details,
132
+ };
133
+ }
134
+
135
+ function required<K extends "id" | "prompt" | "schedule">(
136
+ input: TaskToolInput,
137
+ field: K
138
+ ): NonNullable<TaskToolInput[K]> {
139
+ const value = input[field];
140
+ if (!value) {
141
+ throw new Error(`'${field}' is required for action=${input.action}`);
142
+ }
143
+ return value;
144
+ }
145
+
146
+ function found(task: ScheduledTask | undefined, id: string): ScheduledTask {
147
+ if (!task) {
148
+ throw notFound(id);
149
+ }
150
+ return task;
151
+ }
152
+
153
+ function notFound(id: string): Error {
154
+ return new Error(`no task with id=${id} in this thread`);
155
+ }
156
+
157
+ function taskSpans(input: Partial<TaskToolInput>): readonly Span[] {
158
+ const action = input.action;
159
+ if (!action) {
160
+ return [{ text: "..." }];
161
+ }
162
+ if (action === "list") {
163
+ return [{ text: "List tasks" }];
164
+ }
165
+ if (action === "create") {
166
+ const schedule = scheduleSummary(input);
167
+ return [
168
+ { text: "Schedule task" },
169
+ ...(input.prompt ? [{ text: ": " }, code(input.prompt)] : []),
170
+ ...(schedule ? [{ text: ` (${schedule})` }] : []),
171
+ ];
172
+ }
173
+ if (action === "update_prompt") {
174
+ return input.prompt
175
+ ? [{ text: "Update task: " }, code(input.prompt)]
176
+ : [{ text: "Update task" }];
177
+ }
178
+ const verb = VERBS[action] ?? action;
179
+ return input.id
180
+ ? [{ text: `${verb} task: ` }, code(input.id)]
181
+ : [{ text: `${verb} task` }];
182
+ }
183
+
184
+ const VERBS: Readonly<Record<string, string>> = {
185
+ delete: "Delete",
186
+ pause: "Pause",
187
+ resume: "Resume",
188
+ };
189
+
190
+ function code(text: string): Span {
191
+ return { text, code: true };
192
+ }
193
+
194
+ function scheduleSummary(input: Partial<TaskToolInput>): string | undefined {
195
+ const schedule = input.schedule;
196
+ if (!schedule) {
197
+ return undefined;
198
+ }
199
+ if (schedule.type === "once") {
200
+ return `once @ ${schedule.at}`;
201
+ }
202
+ if (schedule.type === "interval") {
203
+ return `every ${schedule.every}`;
204
+ }
205
+ return `cron ${schedule.expr}`;
206
+ }
207
+
208
+ export const TaskTool = { build };
@@ -0,0 +1,288 @@
1
+ import { Markdown } from "./Markdown";
2
+
3
+ export const BR = "<br>";
4
+
5
+ const MESSAGE_LIMIT = 32000;
6
+ const BLOCK_TAGS = new Set([
7
+ "blockquote",
8
+ "h1",
9
+ "h2",
10
+ "h3",
11
+ "h4",
12
+ "h5",
13
+ "h6",
14
+ "ol",
15
+ "p",
16
+ "pre",
17
+ "table",
18
+ "tg-math-block",
19
+ "ul",
20
+ ]);
21
+ const VOID_BLOCK_TAGS = new Set(["br", "hr"]);
22
+
23
+ type HtmlTag = {
24
+ readonly start: number;
25
+ readonly end: number;
26
+ readonly name: string;
27
+ readonly closing: boolean;
28
+ readonly selfClosing: boolean;
29
+ };
30
+
31
+ function capStatus(text: string): string {
32
+ if (text.length <= MESSAGE_LIMIT) {
33
+ return text;
34
+ }
35
+ const blocks = splitStatusBlocks(text);
36
+ let dropped = 0;
37
+ while (blocks.length > 1) {
38
+ blocks.shift();
39
+ dropped += 1;
40
+ const rest = trimLeadingBreaks(blocks.join("").trimStart());
41
+ const candidate = `<p>… ${dropped} earlier entries</p>${rest}`;
42
+ if (candidate.length <= MESSAGE_LIMIT) {
43
+ return candidate;
44
+ }
45
+ }
46
+ return capPlainStatus(blocks);
47
+ }
48
+
49
+ function splitStatusBlocks(html: string): string[] {
50
+ const blocks: string[] = [];
51
+ let cursor = 0;
52
+ while (cursor < html.length) {
53
+ const tag = nextStatusBlockTag(html, cursor);
54
+ if (!tag) {
55
+ pushStatusBlock(blocks, html.slice(cursor));
56
+ break;
57
+ }
58
+ if (VOID_BLOCK_TAGS.has(tag.name)) {
59
+ pushStatusBlock(blocks, html.slice(cursor, tag.end));
60
+ cursor = tag.end;
61
+ continue;
62
+ }
63
+ pushStatusBlock(blocks, html.slice(cursor, tag.start));
64
+ const end = statusBlockEnd(html, tag);
65
+ pushStatusBlock(blocks, html.slice(tag.start, end));
66
+ cursor = end;
67
+ }
68
+ return blocks;
69
+ }
70
+
71
+ function nextStatusBlockTag(html: string, start: number): HtmlTag | undefined {
72
+ const tags = htmlTags(html, start);
73
+ for (const tag of tags) {
74
+ if (tag.closing) {
75
+ continue;
76
+ }
77
+ if (BLOCK_TAGS.has(tag.name) || VOID_BLOCK_TAGS.has(tag.name)) {
78
+ return tag;
79
+ }
80
+ }
81
+ return undefined;
82
+ }
83
+
84
+ function statusBlockEnd(html: string, opener: HtmlTag): number {
85
+ if (opener.selfClosing) {
86
+ return opener.end;
87
+ }
88
+ const stack = [opener.name];
89
+ const tags = htmlTags(html, opener.end);
90
+ for (const tag of tags) {
91
+ if (VOID_BLOCK_TAGS.has(tag.name)) {
92
+ continue;
93
+ }
94
+ if (!BLOCK_TAGS.has(tag.name)) {
95
+ continue;
96
+ }
97
+ if (tag.closing) {
98
+ if (stack.at(-1) === tag.name) {
99
+ stack.pop();
100
+ }
101
+ } else if (!tag.selfClosing) {
102
+ stack.push(tag.name);
103
+ }
104
+ if (stack.length === 0) {
105
+ return tag.end;
106
+ }
107
+ }
108
+ return html.length;
109
+ }
110
+
111
+ function* htmlTags(html: string, start: number): Generator<HtmlTag> {
112
+ const re = /<\s*(\/)?\s*([a-z][\w:-]*)(?:\s[^>]*)?\/?\s*>/gi;
113
+ re.lastIndex = start;
114
+ for (let match = re.exec(html); match; match = re.exec(html)) {
115
+ const raw = match[0]!;
116
+ yield {
117
+ start: match.index,
118
+ end: re.lastIndex,
119
+ name: match[2]!.toLowerCase(),
120
+ closing: match[1] !== undefined,
121
+ selfClosing: /\/\s*>$/.test(raw),
122
+ };
123
+ }
124
+ }
125
+
126
+ function pushStatusBlock(blocks: string[], block: string): void {
127
+ if (block) {
128
+ blocks.push(block);
129
+ }
130
+ }
131
+
132
+ function trimLeadingBreaks(text: string): string {
133
+ return text.replace(/^(?:<br\s*\/?>)+/i, "").trimStart();
134
+ }
135
+
136
+ function capPlainStatus(blocks: readonly string[]): string {
137
+ let head = "";
138
+ for (let i = 0; i < blocks.length; i++) {
139
+ const block = blocks[i]!;
140
+ const candidate = `${head}${block}`;
141
+ if (candidate.length <= MESSAGE_LIMIT) {
142
+ head = candidate;
143
+ continue;
144
+ }
145
+ const remaining = MESSAGE_LIMIT - head.length;
146
+ const truncated = truncateHtmlHead(block, remaining);
147
+ if (truncated) {
148
+ head = `${head}${truncated}`;
149
+ }
150
+ break;
151
+ }
152
+ return head.trimEnd();
153
+ }
154
+
155
+ function truncateHtmlHead(html: string, limit: number): string {
156
+ if (html.length <= limit) {
157
+ return html;
158
+ }
159
+ if (limit <= 0) {
160
+ return "";
161
+ }
162
+ const wrapper = outerHtmlWrapper(html);
163
+ if (wrapper) {
164
+ const innerLimit = limit - wrapper.open.length - wrapper.close.length;
165
+ if (innerLimit > 0) {
166
+ const inner = truncateHtmlHead(wrapper.inner, innerLimit);
167
+ if (inner) {
168
+ return `${wrapper.open}${inner}${wrapper.close}`;
169
+ }
170
+ }
171
+ }
172
+ return escapePlainHead(stripHtml(html), limit);
173
+ }
174
+
175
+ function outerHtmlWrapper(html: string):
176
+ | {
177
+ readonly open: string;
178
+ readonly inner: string;
179
+ readonly close: string;
180
+ }
181
+ | undefined {
182
+ const opener = /^<\s*([a-z][\w:-]*)(?:\s[^>]*)?\/?\s*>/i.exec(html);
183
+ if (!opener) {
184
+ return undefined;
185
+ }
186
+ const open = opener[0]!;
187
+ if (/\/\s*>$/.test(open)) {
188
+ return undefined;
189
+ }
190
+ const name = opener[1]!.toLowerCase();
191
+ const close = matchingHtmlCloseTag(html, name, open.length);
192
+ if (!close || close.end !== html.length) {
193
+ return undefined;
194
+ }
195
+ return {
196
+ open,
197
+ inner: html.slice(open.length, close.start),
198
+ close: html.slice(close.start, close.end),
199
+ };
200
+ }
201
+
202
+ function matchingHtmlCloseTag(
203
+ html: string,
204
+ name: string,
205
+ start: number
206
+ ): HtmlTag | undefined {
207
+ let depth = 1;
208
+ const tags = htmlTags(html, start);
209
+ for (const tag of tags) {
210
+ if (tag.name !== name) {
211
+ continue;
212
+ }
213
+ if (tag.closing) {
214
+ depth -= 1;
215
+ if (depth === 0) {
216
+ return tag;
217
+ }
218
+ } else if (!tag.selfClosing && !VOID_BLOCK_TAGS.has(tag.name)) {
219
+ depth += 1;
220
+ }
221
+ }
222
+ return undefined;
223
+ }
224
+
225
+ function escapePlainHead(text: string, limit: number): string {
226
+ const marker = "…";
227
+ if (limit < marker.length) {
228
+ return "";
229
+ }
230
+ const budget = limit - marker.length;
231
+ const escaped: string[] = [];
232
+ let length = 0;
233
+ for (const char of text) {
234
+ const next = char === "\n" ? BR : Markdown.escape(char);
235
+ if (length + next.length > budget) {
236
+ break;
237
+ }
238
+ escaped.push(next);
239
+ length += next.length;
240
+ }
241
+ return `${escaped.join("").trimEnd()}${marker}`;
242
+ }
243
+
244
+ function chunk(html: string): readonly string[] {
245
+ if (html.length <= MESSAGE_LIMIT) {
246
+ return [html];
247
+ }
248
+ const chunks: string[] = [];
249
+ let rest = html;
250
+ while (rest.length > MESSAGE_LIMIT) {
251
+ const idx = rest.lastIndexOf(BR, MESSAGE_LIMIT);
252
+ if (idx > 0) {
253
+ chunks.push(rest.slice(0, idx).trim());
254
+ rest = rest.slice(idx + BR.length).trim();
255
+ } else {
256
+ chunks.push(rest.slice(0, MESSAGE_LIMIT).trim());
257
+ rest = rest.slice(MESSAGE_LIMIT).trim();
258
+ }
259
+ }
260
+ if (rest) {
261
+ chunks.push(rest);
262
+ }
263
+ return chunks;
264
+ }
265
+
266
+ function sanitize(text: string): string {
267
+ return text.replace(
268
+ /\b(api[_-]?key|token|secret)\b\s*[:=]\s*\S+/gi,
269
+ "$1=[redacted]"
270
+ );
271
+ }
272
+
273
+ function stripHtml(html: string): string {
274
+ return html
275
+ .replace(/<br\s*\/?>/gi, "\n")
276
+ .replace(/<[^>]+>/g, "")
277
+ .replace(/&lt;/g, "<")
278
+ .replace(/&gt;/g, ">")
279
+ .replace(/&quot;/g, '"')
280
+ .replace(/&amp;/g, "&");
281
+ }
282
+
283
+ export const TelegramHtml = {
284
+ cap: capStatus,
285
+ chunk,
286
+ sanitize,
287
+ strip: stripHtml,
288
+ };
@@ -0,0 +1,43 @@
1
+ import type { Api } from "grammy";
2
+
3
+ import type { SessionId } from "./Session";
4
+
5
+ export class TypingIndicator {
6
+ private readonly api: Api;
7
+ private readonly sessionId: SessionId;
8
+ private timer: Timer | undefined;
9
+ private stopped = true;
10
+
11
+ public constructor(api: Api, sessionId: SessionId) {
12
+ this.api = api;
13
+ this.sessionId = sessionId;
14
+ }
15
+
16
+ public start(): void {
17
+ if (this.timer) {
18
+ return;
19
+ }
20
+ this.stopped = false;
21
+ void this.send();
22
+ this.timer = setInterval(() => void this.send(), 4_000);
23
+ }
24
+
25
+ public stop(): void {
26
+ this.stopped = true;
27
+ if (this.timer) {
28
+ clearInterval(this.timer);
29
+ this.timer = undefined;
30
+ }
31
+ }
32
+
33
+ private async send(): Promise<void> {
34
+ if (this.stopped) {
35
+ return;
36
+ }
37
+ await this.api
38
+ .sendChatAction(this.sessionId.chatId, "typing", {
39
+ message_thread_id: this.sessionId.threadId,
40
+ })
41
+ .catch(() => {});
42
+ }
43
+ }