piclaw 0.0.19 → 0.0.21

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 (270) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/defult-D5RLDUrI.js +1 -0
  3. package/.output/public/assets/{dist-CMBqBOCp.js → dist-BH_oa-kv.js} +1 -1
  4. package/.output/public/assets/index-7JvURuHy.js +204 -0
  5. package/.output/public/assets/index-K43slwjJ.css +1 -0
  6. package/.output/public/index.html +11 -2
  7. package/.output/server/_...path_.get.mjs +16 -0
  8. package/.output/server/_chunks/app.mjs +261 -181
  9. package/.output/server/_chunks/browser.mjs +4 -1
  10. package/.output/server/_chunks/config.mjs +4 -0
  11. package/.output/server/_chunks/db.mjs +32 -28
  12. package/.output/server/_chunks/device-bus.mjs +123 -0
  13. package/.output/server/_chunks/dummy.mjs +1 -1
  14. package/.output/server/_chunks/logger.mjs +23 -0
  15. package/.output/server/_chunks/login.mjs +1 -1
  16. package/.output/server/_chunks/notes.mjs +1 -3
  17. package/.output/server/_chunks/renderer-template.mjs +1 -1
  18. package/.output/server/_chunks/sandbox.mjs +217 -0
  19. package/.output/server/_chunks/server.mjs +2302 -122
  20. package/.output/server/_chunks/terminal.mjs +63 -8
  21. package/.output/server/_chunks/uploads.mjs +60 -0
  22. package/.output/server/_chunks/virtual.mjs +192 -54
  23. package/.output/server/_id_.delete.mjs +5 -2
  24. package/.output/server/_id_.patch.mjs +2 -0
  25. package/.output/server/_id_2.delete.mjs +8 -0
  26. package/.output/server/_jid_.delete.mjs +5 -2
  27. package/.output/server/_jid_.patch.mjs +37 -4
  28. package/.output/server/_jid_2.delete.mjs +5 -2
  29. package/.output/server/_libs/@acemir/cssom+[...].mjs +2269 -1137
  30. package/.output/server/_libs/@google/genai.mjs +337 -273
  31. package/.output/server/_libs/@mariozechner/pi-agent-core+[...].mjs +381 -2073
  32. package/.output/server/_libs/@mariozechner/pi-coding-agent+[...].mjs +231 -131
  33. package/.output/server/_libs/_.mjs +3 -2
  34. package/.output/server/_libs/_10.mjs +2 -4
  35. package/.output/server/_libs/_11.mjs +2 -4
  36. package/.output/server/_libs/_12.mjs +2 -3
  37. package/.output/server/_libs/_13.mjs +2 -3
  38. package/.output/server/_libs/_14.mjs +2 -4
  39. package/.output/server/_libs/_15.mjs +2 -4
  40. package/.output/server/_libs/_16.mjs +2 -3
  41. package/.output/server/_libs/_17.mjs +2 -4
  42. package/.output/server/_libs/_18.mjs +2 -2
  43. package/.output/server/_libs/_19.mjs +2 -2
  44. package/.output/server/_libs/_2.mjs +3 -3
  45. package/.output/server/_libs/_20.mjs +2 -2
  46. package/.output/server/_libs/_21.mjs +2 -2
  47. package/.output/server/_libs/_22.mjs +2 -2
  48. package/.output/server/_libs/_23.mjs +2 -2
  49. package/.output/server/_libs/_24.mjs +2 -2
  50. package/.output/server/_libs/_25.mjs +2 -2
  51. package/.output/server/_libs/_26.mjs +2 -2
  52. package/.output/server/_libs/_27.mjs +2 -2
  53. package/.output/server/_libs/_28.mjs +2 -2
  54. package/.output/server/_libs/_29.mjs +2 -2
  55. package/.output/server/_libs/_3.mjs +3 -3
  56. package/.output/server/_libs/_30.mjs +2 -2
  57. package/.output/server/_libs/_31.mjs +2 -2
  58. package/.output/server/_libs/_32.mjs +2 -2
  59. package/.output/server/_libs/_33.mjs +2 -2
  60. package/.output/server/_libs/_34.mjs +2 -2
  61. package/.output/server/_libs/_35.mjs +2 -2
  62. package/.output/server/_libs/_36.mjs +2 -2
  63. package/.output/server/_libs/_37.mjs +2 -2
  64. package/.output/server/_libs/_38.mjs +2 -2
  65. package/.output/server/_libs/_39.mjs +2 -2
  66. package/.output/server/_libs/_4.mjs +4 -3
  67. package/.output/server/_libs/_40.mjs +2 -2
  68. package/.output/server/_libs/_41.mjs +2 -2
  69. package/.output/server/_libs/_42.mjs +2 -2
  70. package/.output/server/_libs/_43.mjs +2 -2
  71. package/.output/server/_libs/_44.mjs +2 -2
  72. package/.output/server/_libs/_45.mjs +2 -2
  73. package/.output/server/_libs/_46.mjs +2 -2
  74. package/.output/server/_libs/_47.mjs +2 -2
  75. package/.output/server/_libs/_48.mjs +2 -2
  76. package/.output/server/_libs/_49.mjs +2 -2
  77. package/.output/server/_libs/_5.mjs +2 -3
  78. package/.output/server/_libs/_50.mjs +2 -2
  79. package/.output/server/_libs/_51.mjs +2 -2
  80. package/.output/server/_libs/_52.mjs +2 -2
  81. package/.output/server/_libs/_53.mjs +2 -2
  82. package/.output/server/_libs/_54.mjs +2 -2
  83. package/.output/server/_libs/_55.mjs +2 -2
  84. package/.output/server/_libs/_56.mjs +2 -2
  85. package/.output/server/_libs/_57.mjs +2 -2
  86. package/.output/server/_libs/_58.mjs +2 -2
  87. package/.output/server/_libs/_59.mjs +2 -2
  88. package/.output/server/_libs/_6.mjs +2 -3
  89. package/.output/server/_libs/_60.mjs +2 -2
  90. package/.output/server/_libs/_61.mjs +2 -2
  91. package/.output/server/_libs/_62.mjs +2 -2
  92. package/.output/server/_libs/_63.mjs +2 -2
  93. package/.output/server/_libs/_64.mjs +2 -2
  94. package/.output/server/_libs/_65.mjs +2 -2
  95. package/.output/server/_libs/_66.mjs +2 -2
  96. package/.output/server/_libs/_67.mjs +2 -2
  97. package/.output/server/_libs/_68.mjs +2 -2
  98. package/.output/server/_libs/_69.mjs +2 -2
  99. package/.output/server/_libs/_7.mjs +2 -5
  100. package/.output/server/_libs/_70.mjs +2 -2
  101. package/.output/server/_libs/_71.mjs +2 -2
  102. package/.output/server/_libs/_72.mjs +2 -2
  103. package/.output/server/_libs/_73.mjs +2 -2
  104. package/.output/server/_libs/_74.mjs +2 -2
  105. package/.output/server/_libs/_75.mjs +2 -2
  106. package/.output/server/_libs/_76.mjs +2 -2
  107. package/.output/server/_libs/_77.mjs +2 -2
  108. package/.output/server/_libs/_78.mjs +2 -2
  109. package/.output/server/_libs/_79.mjs +2 -2
  110. package/.output/server/_libs/_8.mjs +2 -3
  111. package/.output/server/_libs/_80.mjs +2 -2
  112. package/.output/server/_libs/_81.mjs +2 -2
  113. package/.output/server/_libs/_82.mjs +2 -2
  114. package/.output/server/_libs/_83.mjs +2 -2
  115. package/.output/server/_libs/_84.mjs +2 -2
  116. package/.output/server/_libs/_85.mjs +2 -2
  117. package/.output/server/_libs/_86.mjs +2 -2
  118. package/.output/server/_libs/_87.mjs +2 -2
  119. package/.output/server/_libs/_88.mjs +2 -2
  120. package/.output/server/_libs/_89.mjs +2 -2
  121. package/.output/server/_libs/_9.mjs +2 -4
  122. package/.output/server/_libs/_90.mjs +5 -2
  123. package/.output/server/_libs/_91.mjs +3 -2
  124. package/.output/server/_libs/_92.mjs +2 -2
  125. package/.output/server/_libs/_93.mjs +2 -2
  126. package/.output/server/_libs/_94.mjs +2 -2
  127. package/.output/server/_libs/agent-base.mjs +1 -1
  128. package/.output/server/_libs/cheerio+[...].mjs +1 -1
  129. package/.output/server/_libs/data-uri-to-buffer.mjs +2 -67
  130. package/.output/server/_libs/data-urls+[...].mjs +1 -1
  131. package/.output/server/_libs/diff.mjs +1 -1
  132. package/.output/server/_libs/exodus__bytes.mjs +99 -81
  133. package/.output/server/_libs/fetch-blob+node-domexception.mjs +1 -1
  134. package/.output/server/_libs/h3+rou3+srvx.mjs +34 -4
  135. package/.output/server/_libs/html-encoding-sniffer.mjs +1 -1
  136. package/.output/server/_libs/https-proxy-agent.mjs +2 -2
  137. package/.output/server/_libs/jsdom.mjs +1 -1
  138. package/.output/server/_libs/just-bash+[...].mjs +4676 -3916
  139. package/.output/server/_libs/mariozechner__jiti.mjs +1 -1
  140. package/.output/server/_libs/mariozechner__pi-ai.mjs +1472 -0
  141. package/.output/server/_libs/md4x.mjs +1 -1
  142. package/.output/server/_libs/mime.mjs +838 -1
  143. package/.output/server/_libs/node-fetch.mjs +4 -4
  144. package/.output/server/_libs/node-liblzma.mjs +1 -1
  145. package/.output/server/_libs/silvia-odwyer__photon-node.mjs +1 -1
  146. package/.output/server/_routes/api/auth/approve.mjs +2 -0
  147. package/.output/server/_routes/api/auth/revoke.mjs +2 -0
  148. package/.output/server/_routes/api/auth/status.mjs +25 -6
  149. package/.output/server/_routes/api/browser2.mjs +1 -1
  150. package/.output/server/_routes/api/config2.mjs +2 -0
  151. package/.output/server/_routes/api/device_events.mjs +36 -0
  152. package/.output/server/_routes/api/files/groups.mjs +1 -2
  153. package/.output/server/_routes/api/files/raw.mjs +1 -1
  154. package/.output/server/_routes/api/groups.mjs +5 -3
  155. package/.output/server/_routes/api/groups2.mjs +18 -6
  156. package/.output/server/_routes/api/health.mjs +1 -2
  157. package/.output/server/_routes/api/messages.mjs +7 -1
  158. package/.output/server/_routes/api/notes/delete.mjs +4 -1
  159. package/.output/server/_routes/api/notes/write.mjs +2 -0
  160. package/.output/server/_routes/api/ntfy/setup.mjs +8 -0
  161. package/.output/server/_routes/api/pi/apikey.mjs +3 -2
  162. package/.output/server/_routes/api/pi/apikey_providers.mjs +1 -2
  163. package/.output/server/_routes/api/pi/commands.mjs +13 -3
  164. package/.output/server/_routes/api/pi/login/events.mjs +0 -1
  165. package/.output/server/_routes/api/pi/login/respond.mjs +2 -1
  166. package/.output/server/_routes/api/pi/login.mjs +1 -2
  167. package/.output/server/_routes/api/pi/logout.mjs +2 -1
  168. package/.output/server/_routes/api/pi/models.mjs +1 -2
  169. package/.output/server/_routes/api/pi/models_config2.mjs +2 -0
  170. package/.output/server/_routes/api/pi/settings2.mjs +2 -0
  171. package/.output/server/_routes/api/pi/status.mjs +1 -2
  172. package/.output/server/_routes/api/proxy.mjs +19 -1
  173. package/.output/server/_routes/api/sandbox.mjs +26 -0
  174. package/.output/server/_routes/api/sandbox2.mjs +17 -0
  175. package/.output/server/_routes/api/send.mjs +26 -18
  176. package/.output/server/_routes/api/status.mjs +1 -3
  177. package/.output/server/_routes/api/stop.mjs +11 -0
  178. package/.output/server/_routes/api/store/plugins.mjs +75 -0
  179. package/.output/server/_routes/api/store/skills.mjs +11 -0
  180. package/.output/server/_routes/api/tasks2.mjs +3 -2
  181. package/.output/server/_routes/api/telegram/setup.mjs +5 -2
  182. package/.output/server/_routes/api/telegram/status.mjs +1 -2
  183. package/.output/server/_routes/api/terminal2.mjs +2 -1
  184. package/.output/server/_routes/api/tunnel/setup.mjs +4 -2
  185. package/.output/server/_runtime.mjs +1 -2
  186. package/.output/server/_utils.mjs +10 -2
  187. package/.output/server/index.mjs +1 -1
  188. package/.output/server/node_modules/amdefine/amdefine.js +301 -0
  189. package/.output/server/node_modules/amdefine/package.json +16 -0
  190. package/.output/server/node_modules/compressjs/lib/BWT.js +420 -0
  191. package/.output/server/node_modules/compressjs/lib/BWTC.js +234 -0
  192. package/.output/server/node_modules/compressjs/lib/BitStream.js +108 -0
  193. package/.output/server/node_modules/compressjs/lib/Bzip2.js +936 -0
  194. package/.output/server/node_modules/compressjs/lib/CRC32.js +105 -0
  195. package/.output/server/node_modules/compressjs/lib/Context1Model.js +56 -0
  196. package/.output/server/node_modules/compressjs/lib/DefSumModel.js +152 -0
  197. package/.output/server/node_modules/compressjs/lib/DeflateDistanceModel.js +55 -0
  198. package/.output/server/node_modules/compressjs/lib/Dmc.js +197 -0
  199. package/.output/server/node_modules/compressjs/lib/DummyRangeCoder.js +81 -0
  200. package/.output/server/node_modules/compressjs/lib/FenwickModel.js +194 -0
  201. package/.output/server/node_modules/compressjs/lib/Huffman.js +514 -0
  202. package/.output/server/node_modules/compressjs/lib/HuffmanAllocator.js +227 -0
  203. package/.output/server/node_modules/compressjs/lib/LogDistanceModel.js +46 -0
  204. package/.output/server/node_modules/compressjs/lib/Lzjb.js +300 -0
  205. package/.output/server/node_modules/compressjs/lib/LzjbR.js +241 -0
  206. package/.output/server/node_modules/compressjs/lib/Lzp3.js +273 -0
  207. package/.output/server/node_modules/compressjs/lib/MTFModel.js +208 -0
  208. package/.output/server/node_modules/compressjs/lib/NoModel.js +46 -0
  209. package/.output/server/node_modules/compressjs/lib/PPM.js +343 -0
  210. package/.output/server/node_modules/compressjs/lib/RangeCoder.js +238 -0
  211. package/.output/server/node_modules/compressjs/lib/Simple.js +111 -0
  212. package/.output/server/node_modules/compressjs/lib/Stream.js +53 -0
  213. package/.output/server/node_modules/compressjs/lib/Util.js +324 -0
  214. package/.output/server/node_modules/compressjs/lib/freeze.js +14 -0
  215. package/.output/server/node_modules/compressjs/main.js +29 -0
  216. package/.output/server/node_modules/compressjs/package.json +35 -0
  217. package/.output/server/package.json +2 -1
  218. package/README.md +10 -1
  219. package/lib/index.d.mts +1 -0
  220. package/lib/index.mjs +1 -0
  221. package/lib/piclaw.mjs +100 -0
  222. package/lib/utils.mjs +96 -0
  223. package/package.json +16 -11
  224. package/.output/public/assets/defult-CMO6TZ5a.js +0 -1
  225. package/.output/public/assets/index-jdnbJw-M.js +0 -204
  226. package/.output/public/assets/index-ooXrRwgl.css +0 -1
  227. package/.output/server/_chunks/commands.mjs +0 -282
  228. package/.output/server/_chunks/pi.mjs +0 -202
  229. package/.output/server/_chunks/session.mjs +0 -1114
  230. package/.output/server/_libs/@aws-crypto/crc32+[...].mjs +0 -299
  231. package/.output/server/_libs/@aws-sdk/client-bedrock-runtime+[...].mjs +0 -17828
  232. package/.output/server/_libs/@aws-sdk/credential-provider-http+[...].mjs +0 -122
  233. package/.output/server/_libs/@aws-sdk/credential-provider-ini+[...].mjs +0 -417
  234. package/.output/server/_libs/@aws-sdk/credential-provider-process+[...].mjs +0 -54
  235. package/.output/server/_libs/@aws-sdk/credential-provider-sso+[...].mjs +0 -1151
  236. package/.output/server/_libs/@aws-sdk/credential-provider-web-identity+[...].mjs +0 -50
  237. package/.output/server/_libs/@smithy/credential-provider-imds+[...].mjs +0 -369
  238. package/.output/server/_libs/@tootallnate/quickjs-emscripten+[...].mjs +0 -3011
  239. package/.output/server/_libs/_100.mjs +0 -2
  240. package/.output/server/_libs/_101.mjs +0 -2
  241. package/.output/server/_libs/_102.mjs +0 -5
  242. package/.output/server/_libs/_103.mjs +0 -3
  243. package/.output/server/_libs/_104.mjs +0 -2
  244. package/.output/server/_libs/_105.mjs +0 -3
  245. package/.output/server/_libs/_106.mjs +0 -2
  246. package/.output/server/_libs/_107.mjs +0 -2
  247. package/.output/server/_libs/_95.mjs +0 -2
  248. package/.output/server/_libs/_96.mjs +0 -2
  249. package/.output/server/_libs/_97.mjs +0 -2
  250. package/.output/server/_libs/_98.mjs +0 -2
  251. package/.output/server/_libs/_99.mjs +0 -2
  252. package/.output/server/_libs/amdefine.mjs +0 -188
  253. package/.output/server/_libs/ast-types.mjs +0 -2270
  254. package/.output/server/_libs/aws-sdk__nested-clients.mjs +0 -3141
  255. package/.output/server/_libs/basic-ftp.mjs +0 -1906
  256. package/.output/server/_libs/compressjs.mjs +0 -50
  257. package/.output/server/_libs/degenerator+[...].mjs +0 -9964
  258. package/.output/server/_libs/get-uri.mjs +0 -413
  259. package/.output/server/_libs/http-proxy-agent.mjs +0 -123
  260. package/.output/server/_libs/ip-address.mjs +0 -1423
  261. package/.output/server/_libs/lru-cache.mjs +0 -732
  262. package/.output/server/_libs/netmask.mjs +0 -139
  263. package/.output/server/_libs/pac-proxy-agent+[...].mjs +0 -3104
  264. package/.output/server/_libs/proxy-agent+proxy-from-env.mjs +0 -204
  265. package/.output/server/_libs/smithy__core.mjs +0 -192
  266. package/.output/server/node_modules/tslib/modules/index.js +0 -70
  267. package/.output/server/node_modules/tslib/modules/package.json +0 -3
  268. package/.output/server/node_modules/tslib/package.json +0 -47
  269. package/.output/server/node_modules/tslib/tslib.js +0 -484
  270. package/bin/piclaw.mjs +0 -195
@@ -1,27 +1,1943 @@
1
+ import { o as __toESM, r as __exportAll } from "../_runtime.mjs";
1
2
  import { i as SESSIONS_DIR, n as GROUPS_DIR, r as MAIN_GROUP_FOLDER, s as config } from "./config.mjs";
2
3
  import { t as createLogger } from "./logger.mjs";
3
- import { C as setSession, D as updateTaskAfterRun, S as setRouterState, T as storeMessage, _ as removeRegisteredGroup, b as setConfig, d as getMessagesSince, f as getNewMessages, g as logTaskRun, h as initDatabase, i as deleteConfig, l as getConfig, m as getTaskById, o as getAllRegisteredGroups, p as getRouterState, r as deleteChat, s as getAllSessions, u as getDueTasks, v as removeSession, w as storeChatMetadata, x as setRegisteredGroup, y as removeTasksByChat } from "./db.mjs";
4
+ import { C as setSession, D as updateTaskAfterRun, S as setRouterState, T as storeMessage, _ as removeRegisteredGroup, b as setConfig, c as getAllTasks, d as getMessagesSince, f as getNewMessages, g as logTaskRun, h as initDatabase, i as deleteConfig, l as getConfig, m as getTaskById, o as getAllRegisteredGroups, p as getRouterState, r as deleteChat, s as getAllSessions, t as clearMessages, u as getDueTasks, v as removeSession, w as storeChatMetadata, x as setRegisteredGroup, y as removeTasksByChat } from "./db.mjs";
5
+ import { a as createWriteTool, c as createBashTool, d as stripAnsi$1, i as AuthStorage, l as SessionManager, n as DefaultResourceLoader, o as createReadTool, r as ModelRegistry, s as createEditTool, t as createAgentSession, u as SettingsManager } from "../_libs/@mariozechner/pi-coding-agent+[...].mjs";
4
6
  import { n as parseAST, t as init } from "../_libs/md4x.mjs";
5
7
  import { t as streamBus } from "./stream.mjs";
6
- import { a as resolveGroupModel } from "./session.mjs";
8
+ import { h as Type } from "../_libs/@mariozechner/pi-agent-core+[...].mjs";
9
+ import { t as deviceBus } from "./device-bus.mjs";
10
+ import { n as setBrowserState, t as getBrowserState } from "./browser.mjs";
11
+ import { i as writeNote, n as listNotes, r as readNote, t as deleteNote } from "./notes.mjs";
12
+ import { n as getNtfyConfig, r as sendNtfyNotification } from "./ntfy.mjs";
7
13
  import { t as terminalManager } from "./terminal.mjs";
8
- import { n as pi_exports } from "./pi.mjs";
9
- import { i as tryPriorityCommand, n as tryBashCommand, r as tryCommand, t as commandDescriptions } from "./commands.mjs";
14
+ import { t as sandboxManager } from "./sandbox.mjs";
15
+ import { a as saveAttachment, i as readAttachmentBase64, n as getMimeType, r as isImageMimeType } from "./uploads.mjs";
10
16
  import { i as tunnelManager, n as getTunnelConfig, r as setTunnelConfig, t as clearTunnelConfig } from "./tunnel.mjs";
11
17
  import fs from "fs";
12
18
  import path from "path";
13
19
  import fs$1 from "node:fs";
14
20
  import path$1 from "node:path";
15
21
  import crypto from "node:crypto";
22
+ import fs$2 from "node:fs/promises";
23
+ import os from "node:os";
24
+ import "node:child_process";
25
+ function appendGroupEvent(folder, event) {
26
+ try {
27
+ const logsDir = path$1.join(GROUPS_DIR, folder, "logs");
28
+ fs$1.mkdirSync(logsDir, { recursive: true });
29
+ const line = JSON.stringify({
30
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
31
+ ...event
32
+ }) + "\n";
33
+ fs$1.appendFileSync(path$1.join(logsDir, "events.jsonl"), line, "utf-8");
34
+ } catch {}
35
+ }
36
+ /**
37
+ * Extract readable text from a Pi SDK tool result.
38
+ * Handles `{ content: [{ type: "text", text }] }` objects, JSON strings, and plain strings.
39
+ * Strips ANSI/control chars and truncates.
40
+ */
41
+ function extractToolResultText(value, maxLength = 200) {
42
+ let text = "";
43
+ if (typeof value === "string") try {
44
+ text = _textFromParsed(JSON.parse(value)) ?? value;
45
+ } catch {
46
+ text = value.match(/"text"\s*:\s*"((?:[^"\\]|\\.)*)(?:"|$)/)?.[1]?.replaceAll("\\n", "\n").replaceAll("\\t", " ").replaceAll("\\\"", "\"") ?? value;
47
+ }
48
+ else if (value && typeof value === "object") text = _textFromParsed(value) ?? JSON.stringify(value);
49
+ if (!text) return "";
50
+ text = stripAnsi$1(text).trim();
51
+ if (text.length > maxLength) return text.slice(0, maxLength) + "…";
52
+ return text;
53
+ }
54
+ function _textFromParsed(obj) {
55
+ if (!obj || typeof obj !== "object") return void 0;
56
+ const rec = obj;
57
+ if (Array.isArray(rec.content)) {
58
+ const first = rec.content[0];
59
+ if (first?.text && typeof first.text === "string") return first.text;
60
+ }
61
+ }
62
+ var availableCommands;
63
+ async function getSystemInfo() {
64
+ const info = {
65
+ os: process.platform,
66
+ arch: process.arch,
67
+ nodeVersion: process.versions.node,
68
+ runtime: _getRuntime(),
69
+ freeMemory: os.freemem(),
70
+ totalMemory: os.totalmem(),
71
+ uptime: os.uptime()
72
+ };
73
+ if (process.platform === "linux") {
74
+ info.linuxDistro = await _getLinuxDistro();
75
+ info.isDocker = await _isDocker();
76
+ }
77
+ info.availableDiskSpace = await _getAvailableDiskSpace();
78
+ info.availableCommands = availableCommands ??= await _getAvailableCommands();
79
+ info.processMemory = process.memoryUsage().rss;
80
+ return info;
81
+ }
82
+ async function getSystemInfoText() {
83
+ const info = await getSystemInfo();
84
+ const lines = [];
85
+ let osLine = `${info.os} ${info.arch}`;
86
+ if (info.linuxDistro) osLine += ` (${info.linuxDistro})`;
87
+ if (info.isDocker) osLine += ` [Docker]`;
88
+ lines.push(`OS: ${osLine}`);
89
+ if (info.runtime) lines.push(`Runtime: ${info.runtime}`);
90
+ if (info.freeMemory != null && info.totalMemory != null) {
91
+ const fmt = (b) => `${(b / 1024 / 1024 / 1024).toFixed(1)}GB`;
92
+ let memLine = `Memory: ${fmt(info.freeMemory)} free / ${fmt(info.totalMemory)} total`;
93
+ if (info.processMemory != null) {
94
+ const mb = (info.processMemory / 1024 / 1024).toFixed(0);
95
+ memLine += ` (process: ${mb}MB)`;
96
+ }
97
+ lines.push(memLine);
98
+ }
99
+ if (info.availableDiskSpace != null) lines.push(`Disk: ${(info.availableDiskSpace / 1024 / 1024 / 1024).toFixed(1)}GB available`);
100
+ return lines.map((l) => `- ${l}`).join("\n");
101
+ }
102
+ function _getRuntime() {
103
+ if ("Bun" in globalThis) return `bun v${globalThis.Bun.version}`;
104
+ if ("Deno" in globalThis) return `deno v${globalThis.Deno.version.deno}`;
105
+ return `node v${process.versions.node}`;
106
+ }
107
+ async function _getLinuxDistro() {
108
+ try {
109
+ return (await fs$2.readFile("/etc/os-release", "utf8")).match(/^PRETTY_NAME="?(.+?)"?\s*$/m)?.[1] ?? null;
110
+ } catch {
111
+ return null;
112
+ }
113
+ }
114
+ async function _isDocker() {
115
+ try {
116
+ await fs$2.access("/.dockerenv");
117
+ return true;
118
+ } catch {
119
+ try {
120
+ const cgroup = await fs$2.readFile("/proc/1/cgroup", "utf8");
121
+ return cgroup.includes("docker") || cgroup.includes("containerd");
122
+ } catch {
123
+ return false;
124
+ }
125
+ }
126
+ }
127
+ async function _getAvailableCommands() {
128
+ try {
129
+ const pathEnv = process.env.PATH;
130
+ if (!pathEnv) return void 0;
131
+ const dirs = [...new Set(pathEnv.split(":"))];
132
+ const commands = /* @__PURE__ */ new Set();
133
+ await Promise.all(dirs.map(async (dir) => {
134
+ try {
135
+ const entries = await fs$2.readdir(dir);
136
+ for (const entry of entries) commands.add(entry);
137
+ } catch {}
138
+ }));
139
+ return [...commands].sort();
140
+ } catch {
141
+ return;
142
+ }
143
+ }
144
+ async function _getAvailableDiskSpace() {
145
+ try {
146
+ const stats = await fs$2.statfs("/");
147
+ return stats.bsize * stats.bavail;
148
+ } catch {
149
+ return null;
150
+ }
151
+ }
152
+ var logger$6 = createLogger("pi");
153
+ function sendPrompt(managed, group, input, onOutput) {
154
+ return new Promise((resolve) => {
155
+ const { session } = managed;
156
+ let fullText = "";
157
+ let thinkingText = "";
158
+ let resolved = false;
159
+ let flushedTextLength = 0;
160
+ /** Chronologically ordered raw items (thinking text + tool markers) flushed as single toggle */
161
+ const pendingItems = [];
162
+ let outputChain = Promise.resolve();
163
+ const timeout = setTimeout(() => {
164
+ if (resolved) return;
165
+ resolved = true;
166
+ logger$6.error({ group: group.name }, "Agent timeout");
167
+ appendGroupEvent(group.folder, {
168
+ type: "error",
169
+ reason: "timeout"
170
+ });
171
+ streamBus.emit({
172
+ type: "error",
173
+ chatJid: input.chatJid,
174
+ error: "Agent timeout"
175
+ });
176
+ session.abort().catch(() => {});
177
+ resolve({
178
+ status: "error",
179
+ result: fullText || null,
180
+ newSessionId: session.sessionId,
181
+ error: "Agent timeout"
182
+ });
183
+ }, config.get("agentTimeout"));
184
+ const finish = (status, error) => {
185
+ if (resolved) return;
186
+ resolved = true;
187
+ clearTimeout(timeout);
188
+ clearInterval(abortCheck);
189
+ outputChain.then(() => {
190
+ resolveAbortDone?.();
191
+ resolve({
192
+ status,
193
+ result: fullText || null,
194
+ newSessionId: session.sessionId,
195
+ error
196
+ });
197
+ });
198
+ };
199
+ let unsubscribe;
200
+ let resolveAbortDone;
201
+ managed.abortDone = new Promise((r) => {
202
+ resolveAbortDone = r;
203
+ });
204
+ const abortCheck = setInterval(() => {
205
+ if (managed.aborted && !resolved) {
206
+ clearInterval(abortCheck);
207
+ unsubscribe?.();
208
+ const unflushed = fullText.slice(flushedTextLength);
209
+ const jid = input.chatJid;
210
+ if (onOutput) {
211
+ const parts = [];
212
+ if (thinkingText) {
213
+ pendingItems.push(thinkingText);
214
+ thinkingText = "";
215
+ }
216
+ if (pendingItems.length) parts.push(flushPendingItems(pendingItems));
217
+ if (unflushed) parts.push(unflushed);
218
+ const partial = parts.length ? parts.join("\n\n") : null;
219
+ outputChain = outputChain.then(async () => {
220
+ await onOutput({
221
+ status: "success",
222
+ result: partial ? partial + "\n\n_(stopped)_" : "_(stopped)_",
223
+ newSessionId: session.sessionId
224
+ });
225
+ if (partial) streamBus.emit({
226
+ type: "text_end",
227
+ chatJid: jid,
228
+ content: partial
229
+ });
230
+ });
231
+ }
232
+ outputChain = outputChain.then(() => {
233
+ streamBus.emit({
234
+ type: "agent_end",
235
+ chatJid: jid
236
+ });
237
+ resolveAbortDone?.();
238
+ });
239
+ finish("stopped", "Session stopped by user");
240
+ }
241
+ }, 200);
242
+ unsubscribe = session.subscribe((event) => {
243
+ if (resolved) {
244
+ clearInterval(abortCheck);
245
+ return;
246
+ }
247
+ const jid = input.chatJid;
248
+ switch (event.type) {
249
+ case "message_update": {
250
+ const ae = event.assistantMessageEvent;
251
+ if (ae.type === "text_delta" && "delta" in ae) {
252
+ fullText += ae.delta;
253
+ streamBus.emit({
254
+ type: "text_delta",
255
+ chatJid: jid,
256
+ delta: ae.delta
257
+ });
258
+ }
259
+ if (ae.type === "text_end" && "content" in ae && onOutput) {
260
+ const text = ae.content;
261
+ const parts = [];
262
+ if (thinkingText) {
263
+ pendingItems.push(thinkingText);
264
+ thinkingText = "";
265
+ }
266
+ if (pendingItems.length) parts.push(flushPendingItems(pendingItems));
267
+ parts.push(text);
268
+ const stored = parts.join("\n\n");
269
+ appendGroupEvent(group.folder, {
270
+ type: "text_end",
271
+ length: stored.length,
272
+ preview: stored.slice(0, 100)
273
+ });
274
+ outputChain = outputChain.then(async () => {
275
+ await onOutput({
276
+ status: "success",
277
+ result: stored,
278
+ newSessionId: session.sessionId
279
+ });
280
+ flushedTextLength = fullText.length;
281
+ streamBus.emit({
282
+ type: "text_end",
283
+ chatJid: jid,
284
+ content: stored
285
+ });
286
+ });
287
+ }
288
+ if (ae.type === "thinking_delta" && "delta" in ae) {
289
+ thinkingText += ae.delta;
290
+ streamBus.emit({
291
+ type: "thinking_delta",
292
+ chatJid: jid,
293
+ delta: ae.delta
294
+ });
295
+ }
296
+ if (ae.type === "thinking_end" && "content" in ae) {
297
+ if (thinkingText) {
298
+ pendingItems.push(thinkingText);
299
+ thinkingText = "";
300
+ }
301
+ streamBus.emit({
302
+ type: "thinking_end",
303
+ chatJid: jid,
304
+ content: ae.content
305
+ });
306
+ }
307
+ break;
308
+ }
309
+ case "agent_end":
310
+ unsubscribe();
311
+ appendGroupEvent(group.folder, { type: "agent_end" });
312
+ streamBus.emit({
313
+ type: "agent_end",
314
+ chatJid: jid
315
+ });
316
+ finish("success");
317
+ break;
318
+ case "tool_execution_start":
319
+ logger$6.debug({
320
+ group: group.name,
321
+ tool: event.toolName,
322
+ toolCallId: event.toolCallId,
323
+ argsPreview: previewForLog(event.args)
324
+ }, "Tool execution started");
325
+ appendGroupEvent(group.folder, {
326
+ type: "tool_start",
327
+ tool: event.toolName,
328
+ toolCallId: event.toolCallId,
329
+ argsPreview: previewForLog(event.args, 200)
330
+ });
331
+ streamBus.emit({
332
+ type: "tool_start",
333
+ chatJid: jid,
334
+ toolName: event.toolName
335
+ });
336
+ break;
337
+ case "tool_execution_update":
338
+ logger$6.debug({
339
+ group: group.name,
340
+ tool: event.toolName,
341
+ toolCallId: event.toolCallId,
342
+ partialResultPreview: previewForLog(event.partialResult)
343
+ }, "Tool execution update");
344
+ break;
345
+ case "tool_execution_end": {
346
+ const payload = {
347
+ group: group.name,
348
+ tool: event.toolName,
349
+ toolCallId: event.toolCallId,
350
+ isError: event.isError,
351
+ resultPreview: previewForLog(event.result)
352
+ };
353
+ if (event.isError) logger$6.debug(payload, "Tool execution error");
354
+ else logger$6.debug(payload, "Tool execution finished");
355
+ appendGroupEvent(group.folder, {
356
+ type: "tool_end",
357
+ tool: event.toolName,
358
+ toolCallId: event.toolCallId,
359
+ isError: event.isError,
360
+ resultPreview: previewForLog(event.result, 200)
361
+ });
362
+ const cleanPreview = extractToolResultText(event.result) || void 0;
363
+ pendingItems.push(toToolMarker(event.toolName, event.isError, cleanPreview));
364
+ streamBus.emit({
365
+ type: "tool_end",
366
+ chatJid: jid,
367
+ toolName: event.toolName,
368
+ isError: event.isError,
369
+ resultPreview: cleanPreview
370
+ });
371
+ break;
372
+ }
373
+ }
374
+ });
375
+ session.prompt(input.prompt, { images: input.images?.length ? input.images : void 0 }).catch((err) => {
376
+ unsubscribe();
377
+ const reason = err instanceof Error ? err.message : String(err);
378
+ logger$6.error({
379
+ group: group.name,
380
+ err
381
+ }, "Prompt error");
382
+ appendGroupEvent(group.folder, {
383
+ type: "error",
384
+ reason
385
+ });
386
+ streamBus.emit({
387
+ type: "error",
388
+ chatJid: input.chatJid,
389
+ error: reason
390
+ });
391
+ finish("error", reason);
392
+ });
393
+ });
394
+ }
395
+ var baseSystemPrompt = `
396
+ You live in your own OS and help your human with tasks.
397
+
398
+ Available tools:
399
+ - read: Read file contents
400
+ - bash: Execute shell commands (ls, rg, find, curl, etc.)
401
+ - edit: Make precise text replacements in existing files
402
+ - write: Create or fully overwrite files
403
+ - notes: Read/write/list/delete markdown notes by category — shared across all agents
404
+ - notify: Send notifications to user
405
+ - terminal: Manage persistent terminal sessions with pty support
406
+ - browser: Open, navigate, switch, and close browser tabs in the admin UI
407
+ - message: Send a message to another group's agent by folder name
408
+
409
+ Execution rules:
410
+ - You operate directly in your own OS/workspace environment autonomously; you do not need to ask for permission to run bash commands; users do not know how to run commands you do it for them.
411
+ - Use tools proactively to do the work; do not just describe what should be done.
412
+ - Use bash for operations (searching, building, testing, git-aware inspection).
413
+ - Use terminal for long running processes such as dev server or tmux.
414
+ - Do not use cat/sed/awk to read files use read tool instead.
415
+ - IMPORTANT: Always read a file before editing it. The edit tool requires an exact match of existing text — if you guess the content, it will fail. Read first, then copy the exact text to replace.
416
+ - Open browser tabs only when explicitly asked.
417
+ - Prefer edit for targeted changes; use write for new files or full rewrites.
418
+ - Use notify tool as a secondary channel to send important info to the user and for anything you can't send in chat. Do not repeat chat messages in notifications.
419
+ - Summarize results in plain text; do not print summaries via bash commands.
420
+ - Be concise, explicit, and include file paths when referencing changes.
421
+ - For thinking/planning notes in your response, use blockquote syntax (> ...) not bold (**...**). Example: "> Planning server conversion steps" instead of "**Planning server conversion steps**".
422
+ - Ask a short clarification question when requirements are ambiguous or risky.
423
+ - never say "Use this command", you can just execute commands using bash tool.
424
+ - always write necessary info to AGENTS.md, it is your own private memory (do not ask user to do it).
425
+ - Keep AGENTS.md concise: when it gets long, compact older entries into brief summaries while preserving key decisions, current state, and open tasks.
426
+ - Do not mention AGENTS.md updates in normal replies unless the user explicitly asks about memory/log updates.
427
+ - Use notes tool for persistent, non-task-specific information that should be shared across all agents (e.g. user preferences, reference material, reusable snippets). AGENTS.md is for your private context; notes are global memory.
428
+ `;
429
+ async function buildSystemPrompt(ctx) {
430
+ let prompt = `${ctx.soul ? ctx.soul.trim() : `You are ${ctx.assistantName}, an expert assistant.`}
431
+
432
+ ${baseSystemPrompt}
433
+
434
+ Current model name: ${ctx.modelName}
435
+ `;
436
+ const envInfo = await getSystemInfoText().catch((err) => {
437
+ logger$6.debug({ err }, "Failed to get system info");
438
+ return null;
439
+ });
440
+ if (envInfo) prompt += `\n\nEnvironment:\n${envInfo}`;
441
+ return prompt;
442
+ }
443
+ function previewForLog(value, maxLength = 1200) {
444
+ try {
445
+ const text = typeof value === "string" ? value : JSON.stringify(value);
446
+ if (!text) return "";
447
+ if (text.length <= maxLength) return text;
448
+ return `${text.slice(0, maxLength)}…(truncated ${text.length - maxLength} chars)`;
449
+ } catch {
450
+ return "[unserializable value]";
451
+ }
452
+ }
453
+ /** Flush all pending items (thinking text + tool markers) into a single collapsible block */
454
+ function flushPendingItems(items) {
455
+ return `<details><summary>thinking</summary>\n\n${items.splice(0).join("\n\n")}\n\n</details>`;
456
+ }
457
+ /** Build an inline tool marker tag */
458
+ function toToolMarker(name, isError, preview) {
459
+ return `<tool name="${name}" status="${isError ? "error" : "ok"}"${preview ? ` preview="${preview.replaceAll("\"", "&quot;")}"` : ""}/>`;
460
+ }
461
+ const browserTool = {
462
+ name: "browser",
463
+ label: "Browser",
464
+ description: [
465
+ "Control the browser window tabs.",
466
+ "Actions: list (show open tabs and active tab),",
467
+ "open (open a new tab, optionally with a URL),",
468
+ "navigate (change URL of a tab), switch (activate a tab), close (close a tab)."
469
+ ].join(" "),
470
+ promptSnippet: "browser: Open, navigate, switch, and close browser tabs in the admin UI",
471
+ promptGuidelines: [
472
+ "Use the browser tool to open web pages in the admin browser window.",
473
+ "Use list to see current tabs before operating on them.",
474
+ "Tab IDs are integers — get them from list output."
475
+ ],
476
+ parameters: Type.Object({
477
+ action: Type.Union([
478
+ Type.Literal("list"),
479
+ Type.Literal("open"),
480
+ Type.Literal("close"),
481
+ Type.Literal("navigate"),
482
+ Type.Literal("switch")
483
+ ], { description: "Action to perform" }),
484
+ url: Type.Optional(Type.String({ description: "URL for open/navigate actions (http/https)" })),
485
+ tabId: Type.Optional(Type.Number({ description: "Tab ID for close/navigate/switch actions" }))
486
+ }),
487
+ async execute(_toolCallId, params) {
488
+ const p = params;
489
+ switch (p.action) {
490
+ case "list": return handleList$2();
491
+ case "open": return handleOpen(p);
492
+ case "close": return handleClose(p);
493
+ case "navigate": return handleNavigate(p);
494
+ case "switch": return handleSwitch(p);
495
+ default: return err$4(`Unknown action: ${p.action}`);
496
+ }
497
+ }
498
+ };
499
+ function handleList$2() {
500
+ const state = getBrowserState();
501
+ if (state.tabs.length === 0) return ok$3("No browser tabs open.");
502
+ return ok$3(`Browser tabs:\n${state.tabs.map((t) => `- [${t.id}]${t.id === state.activeTabId ? " (active)" : ""} ${t.url || "(empty)"}`).join("\n")}`, {
503
+ tabs: state.tabs,
504
+ activeTabId: state.activeTabId
505
+ });
506
+ }
507
+ function handleOpen(p) {
508
+ const state = getBrowserState();
509
+ const id = Math.max(0, ...state.tabs.map((t) => t.id)) + 1;
510
+ setBrowserState({
511
+ tabs: [...state.tabs, {
512
+ id,
513
+ url: p.url || ""
514
+ }],
515
+ activeTabId: id
516
+ });
517
+ return ok$3(`Opened tab ${id}${p.url ? ` at ${p.url}` : ""}.`, { tabId: id });
518
+ }
519
+ function handleClose(p) {
520
+ if (p.tabId == null) return err$4("tabId is required for close");
521
+ const state = getBrowserState();
522
+ const idx = state.tabs.findIndex((t) => t.id === p.tabId);
523
+ if (idx < 0) return err$4(`Tab ${p.tabId} not found`);
524
+ const tabs = state.tabs.filter((t) => t.id !== p.tabId);
525
+ let activeTabId = state.activeTabId;
526
+ if (activeTabId === p.tabId) activeTabId = tabs[Math.min(idx, tabs.length - 1)]?.id ?? null;
527
+ setBrowserState({
528
+ tabs,
529
+ activeTabId
530
+ });
531
+ return ok$3(`Closed tab ${p.tabId}.`, { tabId: p.tabId });
532
+ }
533
+ function handleNavigate(p) {
534
+ if (p.tabId == null) return err$4("tabId is required for navigate");
535
+ if (!p.url) return err$4("url is required for navigate");
536
+ const state = getBrowserState();
537
+ if (!state.tabs.find((t) => t.id === p.tabId)) return err$4(`Tab ${p.tabId} not found`);
538
+ setBrowserState({
539
+ tabs: state.tabs.map((t) => t.id === p.tabId ? {
540
+ ...t,
541
+ url: p.url
542
+ } : t),
543
+ activeTabId: state.activeTabId
544
+ });
545
+ return ok$3(`Navigated tab ${p.tabId} to ${p.url}.`, {
546
+ tabId: p.tabId,
547
+ url: p.url
548
+ });
549
+ }
550
+ function handleSwitch(p) {
551
+ if (p.tabId == null) return err$4("tabId is required for switch");
552
+ const state = getBrowserState();
553
+ if (!state.tabs.some((t) => t.id === p.tabId)) return err$4(`Tab ${p.tabId} not found`);
554
+ setBrowserState({
555
+ tabs: state.tabs,
556
+ activeTabId: p.tabId
557
+ });
558
+ return ok$3(`Switched to tab ${p.tabId}.`, { tabId: p.tabId });
559
+ }
560
+ function ok$3(text, details) {
561
+ return {
562
+ content: [{
563
+ type: "text",
564
+ text
565
+ }],
566
+ details: {
567
+ ok: true,
568
+ ...details
569
+ }
570
+ };
571
+ }
572
+ function err$4(text) {
573
+ return {
574
+ content: [{
575
+ type: "text",
576
+ text
577
+ }],
578
+ details: {
579
+ ok: false,
580
+ isError: true
581
+ }
582
+ };
583
+ }
584
+ function createMessageTool(callerGroupFolder) {
585
+ return {
586
+ name: "message",
587
+ label: "Message",
588
+ description: [
589
+ "Send a message to another registered group's agent.",
590
+ "The message appears in the target group's chat and triggers the target agent to process it.",
591
+ "Use 'folder' to identify the target group."
592
+ ].join(" "),
593
+ promptSnippet: "message: Send a message to another group's agent by folder name",
594
+ promptGuidelines: [
595
+ "Use 'message' to delegate work or coordinate with another group's agent.",
596
+ "Specify the 'folder' (not display name) of the target group.",
597
+ "The receiving agent sees the message as if it came from you."
598
+ ],
599
+ parameters: Type.Object({
600
+ folder: Type.String({ description: "Target group folder name" }),
601
+ content: Type.String({ description: "Message content to send" })
602
+ }),
603
+ async execute(_toolCallId, params) {
604
+ const p = params;
605
+ if (!server) return err$3("Server not available");
606
+ const groups = server.getRegisteredGroups();
607
+ const targetEntry = Object.entries(groups).find(([, g]) => g.folder === p.folder);
608
+ if (!targetEntry) {
609
+ const available = Object.values(groups).map((g) => `${g.name} (${g.folder})`).join(", ");
610
+ return err$3(`Group folder "${p.folder}" not found. Available: ${available}`);
611
+ }
612
+ const [targetJid, targetGroup] = targetEntry;
613
+ const senderLabel = `agent:${callerGroupFolder}`;
614
+ const now = (/* @__PURE__ */ new Date()).toISOString();
615
+ await storeChatMetadata(targetJid, now, targetGroup.name);
616
+ await storeMessage({
617
+ id: crypto.randomUUID(),
618
+ chat_jid: targetJid,
619
+ sender: senderLabel,
620
+ sender_name: senderLabel,
621
+ content: p.content,
622
+ timestamp: now,
623
+ is_from_me: false,
624
+ is_bot_message: false
625
+ });
626
+ server.queue.enqueueMessageCheck(targetJid);
627
+ return {
628
+ content: [{
629
+ type: "text",
630
+ text: `Message sent to "${targetGroup.name}" (${p.folder})`
631
+ }],
632
+ details: {
633
+ ok: true,
634
+ targetJid,
635
+ folder: p.folder
636
+ }
637
+ };
638
+ }
639
+ };
640
+ }
641
+ function err$3(text) {
642
+ return {
643
+ content: [{
644
+ type: "text",
645
+ text
646
+ }],
647
+ details: {
648
+ ok: false,
649
+ isError: true
650
+ }
651
+ };
652
+ }
653
+ const notesTool = {
654
+ name: "notes",
655
+ label: "Notes",
656
+ description: [
657
+ "Manage markdown notes organized by category.",
658
+ "Actions: list (show notes, optionally filtered by category),",
659
+ "read (get note content), write (create/overwrite a note), delete (remove a note).",
660
+ "Notes are referenced as \"category/id\" (e.g. \"ideas/project-plan\").",
661
+ "Virtual ~agents notes map to per-session AGENTS.md files."
662
+ ].join(" "),
663
+ promptSnippet: "notes: Read, write, list, and delete markdown notes organized by category",
664
+ promptGuidelines: [
665
+ "Use notes to persist structured information across sessions — plans, references, logs, etc.",
666
+ "Note refs use \"category/id\" format, e.g. \"tasks/backlog\" or \"docs/api-spec\".",
667
+ "List notes first to see what exists before reading or writing.",
668
+ "Virtual ~agents/ notes map to per-session AGENTS.md files."
669
+ ],
670
+ parameters: Type.Object({
671
+ action: Type.Union([
672
+ Type.Literal("list"),
673
+ Type.Literal("read"),
674
+ Type.Literal("write"),
675
+ Type.Literal("delete")
676
+ ], { description: "Action to perform" }),
677
+ ref: Type.Optional(Type.String({ description: "Note reference as \"category/id\" (required for read/write/delete)" })),
678
+ category: Type.Optional(Type.String({ description: "Filter by category (list only)" })),
679
+ content: Type.Optional(Type.String({ description: "Markdown content to write (write only)" }))
680
+ }),
681
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
682
+ const p = params;
683
+ switch (p.action) {
684
+ case "list": return handleList$1(p);
685
+ case "read": return handleRead$1(p);
686
+ case "write": return handleWrite$1(p);
687
+ case "delete": return handleDelete(p);
688
+ default: return err$2(`Unknown action: ${p.action}`);
689
+ }
690
+ }
691
+ };
692
+ function handleList$1(p) {
693
+ const entries = listNotes(p.category);
694
+ if (entries.length === 0) return ok$2(p.category ? `No notes in category "${p.category}".` : "No notes found.");
695
+ const lines = entries.map((e) => `- ${e.category}/${e.id}`);
696
+ return ok$2(`Notes (${entries.length}):\n${lines.join("\n")}`, { entries });
697
+ }
698
+ function handleRead$1(p) {
699
+ if (!p.ref) return err$2("ref is required for read");
700
+ const content = readNote(p.ref);
701
+ if (content === void 0) return err$2(`Note "${p.ref}" not found`);
702
+ return ok$2(content, { ref: p.ref });
703
+ }
704
+ function handleWrite$1(p) {
705
+ if (!p.ref) return err$2("ref is required for write");
706
+ if (p.content == null) return err$2("content is required for write");
707
+ try {
708
+ writeNote(p.ref, p.content);
709
+ deviceBus.emitDeviceEvent("notes");
710
+ return ok$2(`Note "${p.ref}" saved.`, { ref: p.ref });
711
+ } catch (e) {
712
+ return err$2(`Failed to write note: ${e instanceof Error ? e.message : String(e)}`);
713
+ }
714
+ }
715
+ function handleDelete(p) {
716
+ if (!p.ref) return err$2("ref is required for delete");
717
+ if (!deleteNote(p.ref)) return err$2(`Note "${p.ref}" not found or cannot be deleted`);
718
+ deviceBus.emitDeviceEvent("notes");
719
+ return ok$2(`Note "${p.ref}" deleted.`, { ref: p.ref });
720
+ }
721
+ function ok$2(text, details) {
722
+ return {
723
+ content: [{
724
+ type: "text",
725
+ text
726
+ }],
727
+ details: {
728
+ ok: true,
729
+ ...details
730
+ }
731
+ };
732
+ }
733
+ function err$2(text) {
734
+ return {
735
+ content: [{
736
+ type: "text",
737
+ text
738
+ }],
739
+ details: {
740
+ ok: false,
741
+ isError: true
742
+ }
743
+ };
744
+ }
745
+ const notifyTool = {
746
+ name: "notify",
747
+ label: "Notify",
748
+ description: [
749
+ "Send a notification to the user. Always sends an in-app toast notification to connected browsers.",
750
+ "If ntfy is configured, also sends a push notification via ntfy with full ntfy features",
751
+ "(priority, emoji tags, clickable URLs, markdown, scheduled delivery, action buttons, attachments)."
752
+ ].join(" "),
753
+ parameters: Type.Object({
754
+ message: Type.String({ description: "Notification body text" }),
755
+ title: Type.Optional(Type.String({ description: "Notification title" })),
756
+ priority: Type.Optional(Type.Union([
757
+ Type.Literal(1),
758
+ Type.Literal(2),
759
+ Type.Literal(3),
760
+ Type.Literal(4),
761
+ Type.Literal(5)
762
+ ], { description: "1=min, 2=low, 3=default, 4=high, 5=urgent" })),
763
+ tags: Type.Optional(Type.Array(Type.String(), { description: "Emoji shortcodes or labels, e.g. [\"white_check_mark\",\"robot\"]. See https://docs.ntfy.sh/emojis/" })),
764
+ click: Type.Optional(Type.String({ description: "URL opened when notification is tapped" })),
765
+ markdown: Type.Optional(Type.Boolean({ description: "Enable markdown formatting in body (enabled by default)" })),
766
+ delay: Type.Optional(Type.String({ description: "Schedule delivery: duration (\"30m\",\"2h\"), timestamp, or \"tomorrow 9am\" (cannot be combined with email)" })),
767
+ actions: Type.Optional(Type.Array(Type.String(), { description: "Action buttons (max 3). Format: \"view, Label, https://...\" or \"http, Label, https://...\"" })),
768
+ attach: Type.Optional(Type.String({ description: "URL to an external file to attach" })),
769
+ filename: Type.Optional(Type.String({ description: "Override attachment filename" })),
770
+ email: Type.Optional(Type.String({ description: "Also forward notification to this email" }))
771
+ }),
772
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
773
+ const p = params;
774
+ deviceBus.notify({
775
+ title: p.title ?? p.message,
776
+ description: p.title ? p.message : void 0,
777
+ type: p.priority ? {
778
+ 5: "alert",
779
+ 4: "alert",
780
+ 3: "info",
781
+ 2: "info",
782
+ 1: "info"
783
+ }[p.priority] : "info"
784
+ });
785
+ const config = await getNtfyConfig();
786
+ let ntfySent = false;
787
+ if (config) {
788
+ await sendNtfyNotification(config, {
789
+ message: p.message,
790
+ title: p.title,
791
+ priority: p.priority,
792
+ tags: p.tags,
793
+ click: p.click,
794
+ markdown: p.markdown ?? true,
795
+ delay: p.delay,
796
+ actions: p.actions,
797
+ attach: p.attach,
798
+ filename: p.filename,
799
+ email: p.email
800
+ });
801
+ ntfySent = true;
802
+ }
803
+ const channels = ["in-app", ...ntfySent ? ["ntfy"] : []].join(", ");
804
+ return {
805
+ content: [{
806
+ type: "text",
807
+ text: `Notification sent (${channels}): ${p.title ?? p.message}`
808
+ }],
809
+ details: {
810
+ ok: true,
811
+ channels
812
+ }
813
+ };
814
+ }
815
+ };
816
+ const terminalTool = {
817
+ name: "terminal",
818
+ label: "Terminal",
819
+ description: [
820
+ "Manage persistent terminal/PTY sessions.",
821
+ "Actions: list (show sessions), create (spawn new session),",
822
+ "exec (send command and capture output), write (send raw input),",
823
+ "read (capture recent output), kill (destroy session), resize (change dimensions).",
824
+ "Use exec for running commands and getting results.",
825
+ "Use write for interactive/raw input (arrow keys, ctrl sequences, escape codes).",
826
+ "write supports optional wait param (ms) to capture output after sending input.",
827
+ "For escape key use \\x1b, for enter use \\n, for ctrl+c use \\x03."
828
+ ].join(" "),
829
+ promptSnippet: "terminal: Manage persistent terminal sessions — create, exec commands, read output, kill",
830
+ promptGuidelines: [
831
+ "Use terminal exec to run commands in persistent shell sessions when you need statefulness (virtualenvs, long-running processes, interactive workflows).",
832
+ "For simple one-off commands, prefer bash. Use terminal for multi-step workflows needing a persistent shell.",
833
+ "Always list sessions before creating new ones to reuse existing sessions.",
834
+ "Remember session IDs for reuse across tool calls — sessions persist until killed or timed out (30min)."
835
+ ],
836
+ parameters: Type.Object({
837
+ action: Type.Union([
838
+ Type.Literal("list"),
839
+ Type.Literal("create"),
840
+ Type.Literal("exec"),
841
+ Type.Literal("write"),
842
+ Type.Literal("read"),
843
+ Type.Literal("kill"),
844
+ Type.Literal("resize")
845
+ ], { description: "Action to perform on terminal sessions" }),
846
+ id: Type.Optional(Type.String({ description: "Session ID (required for exec/write/read/kill/resize)" })),
847
+ input: Type.Optional(Type.String({ description: "Command or input text (exec appends newline automatically)" })),
848
+ cwd: Type.Optional(Type.String({ description: "Working directory for new session (create)" })),
849
+ label: Type.Optional(Type.String({ description: "Session label (create)" })),
850
+ sandbox: Type.Optional(Type.Boolean({ description: "Use sandboxed virtual TTY (create, default: false)" })),
851
+ cols: Type.Optional(Type.Number({ description: "Terminal columns (create/resize, default: 120)" })),
852
+ rows: Type.Optional(Type.Number({ description: "Terminal rows (create/resize, default: 40)" })),
853
+ timeout: Type.Optional(Type.Number({ description: "Max ms to wait for output (exec only, default: 10000, max: 30000)" }))
854
+ }),
855
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
856
+ const p = params;
857
+ switch (p.action) {
858
+ case "list": return handleList();
859
+ case "create": return handleCreate(p);
860
+ case "exec": return handleExec(p);
861
+ case "write": return handleWrite(p);
862
+ case "read": return handleRead(p);
863
+ case "kill": return handleKill(p);
864
+ case "resize": return handleResize(p);
865
+ default: return err$1(`Unknown action: ${p.action}`);
866
+ }
867
+ }
868
+ };
869
+ function handleList() {
870
+ const sessions = terminalManager.list();
871
+ if (sessions.length === 0) return ok$1("No active terminal sessions.");
872
+ const lines = sessions.map((s) => `- ${s.id} [${s.label}] ${s.cols}x${s.rows} cwd=${s.cwd}`);
873
+ return ok$1(`Active sessions (${sessions.length}):\n${lines.join("\n")}`, { sessions });
874
+ }
875
+ async function handleCreate(p) {
876
+ try {
877
+ const session = await terminalManager.create({
878
+ cols: p.cols ?? 120,
879
+ rows: p.rows ?? 40,
880
+ cwd: p.cwd,
881
+ label: p.label,
882
+ sandbox: p.sandbox
883
+ });
884
+ return ok$1(`Terminal session created: ${session.id} [${session.label}] ${session.cols}x${session.rows}`, {
885
+ id: session.id,
886
+ label: session.label
887
+ });
888
+ } catch (e) {
889
+ return err$1(`Failed to create session: ${e instanceof Error ? e.message : String(e)}`);
890
+ }
891
+ }
892
+ function handleExec(p) {
893
+ if (!p.id) return err$1("id is required for exec");
894
+ if (!p.input) return err$1("input is required for exec");
895
+ if (!terminalManager.get(p.id)) return err$1("Session not found");
896
+ const timeoutMs = Math.min(p.timeout ?? 1e4, 3e4);
897
+ const quietMs = 300;
898
+ const chunks = [];
899
+ let settled = false;
900
+ let quietTimer;
901
+ return new Promise((resolve) => {
902
+ const unsub = terminalManager.subscribe(p.id, (data) => {
903
+ chunks.push(data);
904
+ if (quietTimer) clearTimeout(quietTimer);
905
+ quietTimer = setTimeout(() => finish(false), quietMs);
906
+ });
907
+ const overallTimer = setTimeout(() => finish(true), timeoutMs);
908
+ function finish(timedOut) {
909
+ if (settled) return;
910
+ settled = true;
911
+ if (quietTimer) clearTimeout(quietTimer);
912
+ clearTimeout(overallTimer);
913
+ unsub();
914
+ resolve(ok$1((stripAnsi(chunks.join("")).trim() || "(no output)") + (timedOut ? "\n(timed out — output may be partial)" : ""), {
915
+ sessionId: p.id,
916
+ timedOut
917
+ }));
918
+ }
919
+ terminalManager.write(p.id, interpretEscapes(p.input) + "\n");
920
+ quietTimer = setTimeout(() => finish(false), quietMs);
921
+ });
922
+ }
923
+ async function handleWrite(p) {
924
+ if (!p.id) return err$1("id is required for write");
925
+ if (p.input == null) return err$1("input is required for write");
926
+ if (!terminalManager.get(p.id)) return err$1("Session not found");
927
+ terminalManager.write(p.id, interpretEscapes(p.input));
928
+ if (p.wait && p.wait > 0) {
929
+ const waitMs = Math.min(p.wait, 5e3);
930
+ const chunks = [];
931
+ const unsub = terminalManager.subscribe(p.id, (data) => chunks.push(data));
932
+ await new Promise((r) => setTimeout(r, waitMs));
933
+ unsub();
934
+ return ok$1(stripAnsi(chunks.join("")).trim() || `Input sent to ${p.id} (no output after ${waitMs}ms)`, { sessionId: p.id });
935
+ }
936
+ return ok$1(`Input sent to ${p.id}`);
937
+ }
938
+ async function handleRead(p) {
939
+ if (!p.id) return err$1("id is required for read");
940
+ if (!terminalManager.get(p.id)) return err$1("Session not found");
941
+ const chunks = [];
942
+ const unsub = terminalManager.subscribe(p.id, (data) => chunks.push(data));
943
+ terminalManager.nudge(p.id);
944
+ await new Promise((r) => setTimeout(r, 200));
945
+ unsub();
946
+ return ok$1(stripAnsi(chunks.join("")).trim() || "(no output)", { sessionId: p.id });
947
+ }
948
+ function handleKill(p) {
949
+ if (!p.id) return err$1("id is required for kill");
950
+ terminalManager.kill(p.id);
951
+ return ok$1(`Session ${p.id} killed`);
952
+ }
953
+ function handleResize(p) {
954
+ if (!p.id) return err$1("id is required for resize");
955
+ const cols = p.cols ?? 120;
956
+ const rows = p.rows ?? 40;
957
+ if (!terminalManager.get(p.id)) return err$1("Session not found");
958
+ terminalManager.resize(p.id, cols, rows);
959
+ return ok$1(`Session ${p.id} resized to ${cols}x${rows}`);
960
+ }
961
+ var stripAnsiRe$1 = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*[A-Za-z]`, "g");
962
+ function interpretEscapes(text) {
963
+ return text.replace(/\\x([0-9a-fA-F]{2})/g, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16)));
964
+ }
965
+ function stripAnsi(text) {
966
+ return text.replace(stripAnsiRe$1, "");
967
+ }
968
+ function ok$1(text, details) {
969
+ return {
970
+ content: [{
971
+ type: "text",
972
+ text
973
+ }],
974
+ details: {
975
+ ok: true,
976
+ ...details
977
+ }
978
+ };
979
+ }
980
+ function err$1(text) {
981
+ return {
982
+ content: [{
983
+ type: "text",
984
+ text
985
+ }],
986
+ details: {
987
+ ok: false,
988
+ isError: true
989
+ }
990
+ };
991
+ }
992
+ /**
993
+ * Sandboxed coding tools — real pi SDK tools backed by an in-memory filesystem.
994
+ * Uses the centralized sandbox manager so the filesystem is shared with virtual PTY.
995
+ */
996
+ function createSandboxedCodingTools(opts) {
997
+ const sb = sandboxManager.get(opts.sandboxId, { ...opts.realCwd && { realCwd: opts.realCwd } });
998
+ const { fs } = sb;
999
+ const read = createReadTool(sb.cwd, { operations: {
1000
+ readFile: async (p) => {
1001
+ const content = await fs.readFile(p);
1002
+ return Buffer.from(content);
1003
+ },
1004
+ access: async (p) => {
1005
+ if (!await fs.exists(p)) throw new Error(`ENOENT: no such file or directory, access '${p}'`);
1006
+ }
1007
+ } });
1008
+ const write = createWriteTool(sb.cwd, { operations: {
1009
+ writeFile: async (p, content) => {
1010
+ await fs.writeFile(p, content);
1011
+ await sb.writeBack();
1012
+ },
1013
+ mkdir: (dir) => {
1014
+ return fs.mkdir(dir, { recursive: true });
1015
+ }
1016
+ } });
1017
+ const edit = createEditTool(sb.cwd, { operations: {
1018
+ readFile: async (p) => {
1019
+ const content = await fs.readFile(p);
1020
+ return Buffer.from(content);
1021
+ },
1022
+ writeFile: async (p, content) => {
1023
+ await fs.writeFile(p, content);
1024
+ await sb.writeBack();
1025
+ },
1026
+ access: async (p) => {
1027
+ if (!await fs.exists(p)) throw new Error(`ENOENT: no such file or directory, access '${p}'`);
1028
+ }
1029
+ } });
1030
+ return [
1031
+ read,
1032
+ createBashTool(sb.cwd, { operations: { exec: async (command, execCwd, options) => {
1033
+ try {
1034
+ const result = await sb.exec(command, { cwd: execCwd || void 0 });
1035
+ if (result.stdout) options.onData(Buffer.from(result.stdout));
1036
+ if (result.stderr) options.onData(Buffer.from(result.stderr));
1037
+ return { exitCode: result.exitCode };
1038
+ } catch (err) {
1039
+ options.onData(Buffer.from(err.message + "\n"));
1040
+ return { exitCode: 1 };
1041
+ }
1042
+ } } }),
1043
+ edit,
1044
+ write
1045
+ ];
1046
+ }
1047
+ /**
1048
+ * MIT License
1049
+ * Copyright (c) 2024 Mario Zechner
1050
+ * Based on https://github.com/badlogic/pi-skills/tree/main/brave-search
1051
+ */
1052
+ const webSearchTool = {
1053
+ name: "web_search",
1054
+ label: "Web Search",
1055
+ description: [
1056
+ "Search the web using the Brave Search API.",
1057
+ "Returns titles, links, snippets, and optionally full page content as markdown.",
1058
+ "Requires BRAVE_API_KEY environment variable."
1059
+ ].join(" "),
1060
+ promptSnippet: "web_search: Search the web and optionally fetch page content as markdown",
1061
+ promptGuidelines: [
1062
+ "Use web_search to look up current information, documentation, error messages, or anything that benefits from live web results.",
1063
+ "Enable content=true to fetch and read full page content as markdown (slower but more detailed).",
1064
+ "Use freshness to filter by time: pd (past day), pw (past week), pm (past month), py (past year).",
1065
+ "Keep count reasonable (3-5) when fetching content to avoid slow responses."
1066
+ ],
1067
+ parameters: Type.Object({
1068
+ query: Type.String({ description: "Search query" }),
1069
+ count: Type.Optional(Type.Number({ description: "Number of results (default: 5, max: 20)" })),
1070
+ content: Type.Optional(Type.Boolean({ description: "Fetch full page content as markdown (default: false, slower)" })),
1071
+ country: Type.Optional(Type.String({ description: "Country code for results (default: \"US\")" })),
1072
+ freshness: Type.Optional(Type.String({ description: "Filter by time: pd (past day), pw (past week), pm (past month), py (past year)" }))
1073
+ }),
1074
+ async execute(_toolCallId, params, signal) {
1075
+ const p = params;
1076
+ const apiKey = config.get("braveApiKey");
1077
+ if (!apiKey) return err("BRAVE_API_KEY is not configured. Set it via Settings => Config => braveApiKey. Get a key at https://api-dashboard.search.brave.com/app/keys");
1078
+ const count = Math.min(Math.max(p.count ?? 5, 1), 20);
1079
+ try {
1080
+ const results = await fetchBraveResults(apiKey, p.query, count, p.country, p.freshness, signal);
1081
+ if (results.length === 0) return ok("No results found.", {
1082
+ query: p.query,
1083
+ count: 0
1084
+ });
1085
+ if (p.content) await Promise.all(results.map(async (r) => {
1086
+ r.content = await fetchPageContent(r.link, signal);
1087
+ }));
1088
+ const lines = [];
1089
+ for (const [i, r] of results.entries()) {
1090
+ lines.push(`--- Result ${i + 1} ---`);
1091
+ lines.push(`Title: ${r.title}`);
1092
+ lines.push(`Link: ${r.link}`);
1093
+ if (r.age) lines.push(`Age: ${r.age}`);
1094
+ lines.push(`Snippet: ${r.snippet}`);
1095
+ if (r.content) lines.push(`Content:\n${r.content}`);
1096
+ lines.push("");
1097
+ }
1098
+ return ok(lines.join("\n"), {
1099
+ query: p.query,
1100
+ count: results.length
1101
+ });
1102
+ } catch (e) {
1103
+ return err(`Search failed: ${e instanceof Error ? e.message : String(e)}`);
1104
+ }
1105
+ }
1106
+ };
1107
+ async function fetchBraveResults(apiKey, query, count, country, freshness, signal) {
1108
+ const params = new URLSearchParams({
1109
+ q: query,
1110
+ count: String(count),
1111
+ country: country?.toUpperCase() || "US"
1112
+ });
1113
+ if (freshness) params.append("freshness", freshness);
1114
+ const res = await fetch(`https://api.search.brave.com/res/v1/web/search?${params}`, {
1115
+ headers: {
1116
+ Accept: "application/json",
1117
+ "Accept-Encoding": "gzip",
1118
+ "X-Subscription-Token": apiKey
1119
+ },
1120
+ signal
1121
+ });
1122
+ if (!res.ok) {
1123
+ const text = await res.text();
1124
+ throw new Error(`HTTP ${res.status}: ${res.statusText}\n${text}`);
1125
+ }
1126
+ const data = await res.json();
1127
+ const results = [];
1128
+ if (data.web?.results) for (const r of data.web.results) {
1129
+ if (results.length >= count) break;
1130
+ results.push({
1131
+ title: r.title || "",
1132
+ link: r.url || "",
1133
+ snippet: r.description || "",
1134
+ age: r.age || r.page_age || ""
1135
+ });
1136
+ }
1137
+ return results;
1138
+ }
1139
+ async function fetchPageContent(url, signal) {
1140
+ try {
1141
+ const res = await fetch(url, {
1142
+ headers: {
1143
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
1144
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
1145
+ },
1146
+ signal: signal ?? AbortSignal.timeout(1e4)
1147
+ });
1148
+ if (!res.ok) return `(HTTP ${res.status})`;
1149
+ return extractReadableText(await res.text(), url);
1150
+ } catch (e) {
1151
+ return `(Error: ${e instanceof Error ? e.message : String(e)})`;
1152
+ }
1153
+ }
1154
+ async function extractReadableText(html, url) {
1155
+ try {
1156
+ const { Readability } = await import("../_libs/_.mjs").then((m) => /* @__PURE__ */ __toESM(m.default, 1));
1157
+ const { JSDOM } = await import("../_libs/_4.mjs").then((m) => /* @__PURE__ */ __toESM(m.default, 1));
1158
+ const article = new Readability(new JSDOM(html, { url }).window.document).parse();
1159
+ if (article?.content) return (await htmlToMarkdown(article.content)).slice(0, 5e3);
1160
+ const doc = new JSDOM(html, { url }).window.document;
1161
+ doc.querySelectorAll("script, style, noscript, nav, header, footer, aside").forEach((el) => el.remove());
1162
+ const text = (doc.querySelector("main, article, [role='main'], .content, #content") || doc.body)?.textContent || "";
1163
+ if (text.trim().length > 100) return text.trim().slice(0, 5e3);
1164
+ return "(Could not extract content)";
1165
+ } catch {
1166
+ return stripHtmlTags$1(html).slice(0, 5e3) || "(Could not extract content)";
1167
+ }
1168
+ }
1169
+ async function htmlToMarkdown(html) {
1170
+ try {
1171
+ const TurndownService = (await import("../_libs/_29.mjs")).default;
1172
+ const { gfm } = await import("../_libs/_93.mjs");
1173
+ const turndown = new TurndownService({
1174
+ headingStyle: "atx",
1175
+ codeBlockStyle: "fenced"
1176
+ });
1177
+ turndown.use(gfm);
1178
+ turndown.addRule("removeEmptyLinks", {
1179
+ filter: (node) => node.nodeName === "A" && !node.textContent?.trim(),
1180
+ replacement: () => ""
1181
+ });
1182
+ return turndown.turndown(html).replace(/\[\\?\[\s*\\?\]\]\([^)]*\)/g, "").replace(/ +/g, " ").replace(/\s+,/g, ",").replace(/\s+\./g, ".").replace(/\n{3,}/g, "\n\n").trim();
1183
+ } catch {
1184
+ return stripHtmlTags$1(html);
1185
+ }
1186
+ }
1187
+ function stripHtmlTags$1(html) {
1188
+ return html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
1189
+ }
1190
+ function ok(text, details) {
1191
+ return {
1192
+ content: [{
1193
+ type: "text",
1194
+ text
1195
+ }],
1196
+ details: {
1197
+ ok: true,
1198
+ ...details
1199
+ }
1200
+ };
1201
+ }
1202
+ function err(text) {
1203
+ return {
1204
+ content: [{
1205
+ type: "text",
1206
+ text
1207
+ }],
1208
+ details: {
1209
+ ok: false,
1210
+ isError: true
1211
+ }
1212
+ };
1213
+ }
1214
+ var logger$5 = createLogger("pi");
1215
+ /**
1216
+ * Model preference order for fallback resolution.
1217
+ * Entries with `model` try a specific model; without tries any available from that provider.
1218
+ */
1219
+ var MODEL_PREFERENCE = [
1220
+ {
1221
+ provider: "openai-codex",
1222
+ model: "gpt-5.3-codex"
1223
+ },
1224
+ { provider: "openai-codex" },
1225
+ { provider: "anthropic" },
1226
+ { provider: "google" }
1227
+ ];
1228
+ /** Provider search order derived from MODEL_PREFERENCE */
1229
+ var PROVIDER_ORDER = [...new Set(MODEL_PREFERENCE.map((e) => e.provider))];
1230
+ const sessions = /* @__PURE__ */ new Map();
1231
+ const authStorage = AuthStorage.create();
1232
+ const modelRegistry = new ModelRegistry(authStorage);
1233
+ async function getOrCreateSession(group, input) {
1234
+ const sessionDir = path.join(SESSIONS_DIR, group.folder);
1235
+ fs.mkdirSync(sessionDir, { recursive: true });
1236
+ let managed = sessions.get(input.chatJid);
1237
+ if (!managed) {
1238
+ streamBus.emit({
1239
+ type: "session_init",
1240
+ chatJid: input.chatJid
1241
+ });
1242
+ managed = await createManagedSession(group, input, sessionDir);
1243
+ sessions.set(input.chatJid, managed);
1244
+ }
1245
+ managed.lastActivity = Date.now();
1246
+ return managed;
1247
+ }
1248
+ function resolveModel() {
1249
+ const available = getFilteredModels();
1250
+ const piProvider = config.get("piProvider");
1251
+ const piModel = config.get("piModel");
1252
+ if (piProvider && piModel) {
1253
+ const model = findAvailable(available, piProvider, piModel);
1254
+ if (model) return model;
1255
+ }
1256
+ if (piModel) {
1257
+ const slashIdx = piModel.indexOf("/");
1258
+ if (slashIdx > 0) {
1259
+ const model = findAvailable(available, piModel.slice(0, slashIdx), piModel.slice(slashIdx + 1));
1260
+ if (model) return model;
1261
+ }
1262
+ for (const provider of PROVIDER_ORDER) {
1263
+ const model = findAvailable(available, provider, piModel);
1264
+ if (model) return model;
1265
+ }
1266
+ }
1267
+ return resolveDefaultModel();
1268
+ }
1269
+ function resolveGroupModel(group) {
1270
+ if (!group.model) {
1271
+ const model = resolveModel();
1272
+ logger$5.debug({
1273
+ group: group.name,
1274
+ resolved: fmtModel(model),
1275
+ source: "global"
1276
+ }, "Model resolved");
1277
+ return model;
1278
+ }
1279
+ const available = getFilteredModels();
1280
+ const modelStr = group.model;
1281
+ const slashIdx = modelStr.indexOf("/");
1282
+ if (slashIdx > 0) {
1283
+ const model = findAvailable(available, modelStr.slice(0, slashIdx), modelStr.slice(slashIdx + 1));
1284
+ if (model) {
1285
+ logger$5.debug({
1286
+ group: group.name,
1287
+ resolved: fmtModel(model),
1288
+ source: "group"
1289
+ }, "Model resolved");
1290
+ return model;
1291
+ }
1292
+ }
1293
+ for (const provider of PROVIDER_ORDER) {
1294
+ const model = findAvailable(available, provider, modelStr);
1295
+ if (model) {
1296
+ logger$5.debug({
1297
+ group: group.name,
1298
+ resolved: fmtModel(model),
1299
+ source: "group-search"
1300
+ }, "Model resolved");
1301
+ return model;
1302
+ }
1303
+ }
1304
+ logger$5.warn({
1305
+ group: group.name,
1306
+ wanted: modelStr,
1307
+ available: available.map(fmtModel)
1308
+ }, "Group model not found in available models, falling back to global");
1309
+ const fallback = resolveModel();
1310
+ logger$5.debug({
1311
+ group: group.name,
1312
+ resolved: fmtModel(fallback),
1313
+ source: "fallback"
1314
+ }, "Model resolved");
1315
+ return fallback;
1316
+ }
1317
+ function resolveDefaultModel() {
1318
+ const available = getFilteredModels();
1319
+ for (const pref of MODEL_PREFERENCE) if (pref.model) {
1320
+ const model = findAvailable(available, pref.provider, pref.model);
1321
+ if (model) return model;
1322
+ } else {
1323
+ const model = available.find((m) => m.provider === pref.provider);
1324
+ if (model) return model;
1325
+ }
1326
+ }
1327
+ async function createManagedSession(group, input, sessionDir) {
1328
+ const groupDir = path.join(GROUPS_DIR, group.folder);
1329
+ const sandboxEnforced = config.get("sandbox") === "enforced";
1330
+ const sandbox = sandboxEnforced || !!group.sandbox;
1331
+ logger$5.info({
1332
+ group: group.name,
1333
+ folder: group.folder,
1334
+ isMain: input.isMain,
1335
+ hasSession: !!input.sessionId,
1336
+ sandbox,
1337
+ ...sandboxEnforced && { sandboxSource: "enforced" }
1338
+ }, "Creating pi SDK session");
1339
+ const model = resolveGroupModel(group);
1340
+ const settingsManager = SettingsManager.inMemory({
1341
+ compaction: { enabled: true },
1342
+ retry: {
1343
+ enabled: true,
1344
+ maxRetries: 3
1345
+ }
1346
+ });
1347
+ const systemPrompt = await buildSystemPrompt({
1348
+ assistantName: input.assistantName,
1349
+ dir: groupDir,
1350
+ modelName: `${model?.provider}/${model?.id}`,
1351
+ soul: group.soul
1352
+ });
1353
+ const resourceLoader = new DefaultResourceLoader({
1354
+ cwd: groupDir,
1355
+ settingsManager,
1356
+ systemPromptOverride: () => systemPrompt,
1357
+ ...sandbox && {
1358
+ agentDir: groupDir,
1359
+ noExtensions: true,
1360
+ noSkills: true,
1361
+ noPromptTemplates: true,
1362
+ noThemes: true,
1363
+ agentsFilesOverride: (_base) => ({ agentsFiles: [] })
1364
+ }
1365
+ });
1366
+ await resourceLoader.reload();
1367
+ const sessionManager = input.sessionId ? SessionManager.continueRecent(groupDir, sessionDir) : SessionManager.create(groupDir, sessionDir);
1368
+ const thinkingLevel = group.thinkingLevel || "off";
1369
+ if (sandbox) logger$5.info({ group: group.name }, "Using sandboxed coding tools (in-memory filesystem)");
1370
+ const sandboxedTools = sandbox ? Object.fromEntries(createSandboxedCodingTools({
1371
+ sandboxId: `~${group.folder}`,
1372
+ realCwd: groupDir
1373
+ }).map((t) => [t.name, t])) : void 0;
1374
+ const { session, modelFallbackMessage } = await createAgentSession({
1375
+ cwd: groupDir,
1376
+ model,
1377
+ thinkingLevel,
1378
+ authStorage,
1379
+ modelRegistry,
1380
+ customTools: sandbox ? [notifyTool, webSearchTool] : [
1381
+ browserTool,
1382
+ createMessageTool(group.folder),
1383
+ notesTool,
1384
+ notifyTool,
1385
+ terminalTool,
1386
+ webSearchTool
1387
+ ],
1388
+ resourceLoader,
1389
+ sessionManager,
1390
+ settingsManager
1391
+ });
1392
+ if (sandboxedTools) {
1393
+ session._baseToolsOverride = sandboxedTools;
1394
+ await session.reload();
1395
+ }
1396
+ if (modelFallbackMessage) logger$5.warn({
1397
+ group: group.name,
1398
+ message: modelFallbackMessage
1399
+ }, "Model fallback");
1400
+ const actualModel = session.model;
1401
+ if (model && actualModel && (actualModel.provider !== model.provider || actualModel.id !== model.id)) {
1402
+ logger$5.warn({
1403
+ group: group.name,
1404
+ requested: fmtModel(model),
1405
+ actual: fmtModel(actualModel)
1406
+ }, "SDK used different model than requested — forcing requested model");
1407
+ await session.setModel(model);
1408
+ }
1409
+ logger$5.info({
1410
+ group: group.name,
1411
+ sessionId: session.sessionId,
1412
+ model: fmtModel(session.model),
1413
+ groupModel: group.model || "default"
1414
+ }, "Pi SDK session created");
1415
+ return {
1416
+ session,
1417
+ groupFolder: group.folder,
1418
+ sessionDir,
1419
+ lastActivity: Date.now()
1420
+ };
1421
+ }
1422
+ /** Date suffix pattern (e.g. `-20241022`, `-20250514`) */
1423
+ var DATE_SUFFIX_RE = /-\d{8}$/;
1424
+ /**
1425
+ * Returns available models filtered and sorted:
1426
+ * - Removes models with date suffixes in their ID (prefer canonical short names)
1427
+ * - Removes all haiku models (too weak for coding tasks)
1428
+ * - Sorts by MODEL_PREFERENCE provider order (preferred providers first, unknown last)
1429
+ */
1430
+ function getFilteredModels() {
1431
+ return modelRegistry.getAvailable().filter((m) => {
1432
+ if (DATE_SUFFIX_RE.test(m.id)) return false;
1433
+ if (/haiku/i.test(m.id)) return false;
1434
+ return true;
1435
+ }).sort((a, b) => {
1436
+ const ai = PROVIDER_ORDER.indexOf(a.provider);
1437
+ const bi = PROVIDER_ORDER.indexOf(b.provider);
1438
+ return (ai === -1 ? PROVIDER_ORDER.length : ai) - (bi === -1 ? PROVIDER_ORDER.length : bi);
1439
+ });
1440
+ }
1441
+ function findAvailable(available, provider, modelId) {
1442
+ return available.find((m) => m.provider === provider && m.id === modelId);
1443
+ }
1444
+ function fmtModel(m) {
1445
+ return m ? `${m.provider}/${m.id}` : "none";
1446
+ }
1447
+ function getPiSdkStatus$1(options) {
1448
+ const { sessions, modelRegistry, authStorage, resolvedModel, configuredProvider, configuredModel } = options;
1449
+ const now = Date.now();
1450
+ const allModels = modelRegistry.getAll();
1451
+ const availableModels = modelRegistry.getAvailable();
1452
+ const availableModelKeys = new Set(availableModels.map((model) => getModelKey(model.provider, model.id)));
1453
+ const sessionList = Array.from(sessions.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([chatJid, managed]) => {
1454
+ const contextUsage = managed.session.getContextUsage();
1455
+ return {
1456
+ chatJid,
1457
+ groupFolder: managed.groupFolder,
1458
+ sessionDir: managed.sessionDir,
1459
+ lastActivity: managed.lastActivity,
1460
+ idleMs: Math.max(0, now - managed.lastActivity),
1461
+ session: {
1462
+ id: managed.session.sessionId,
1463
+ file: managed.session.sessionFile ?? null,
1464
+ name: managed.session.sessionName ?? null,
1465
+ messageCount: managed.session.messages.length,
1466
+ model: managed.session.model ? serializeModel(managed.session.model) : null,
1467
+ thinkingLevel: managed.session.thinkingLevel,
1468
+ contextUsage: contextUsage ? {
1469
+ tokens: contextUsage.tokens,
1470
+ contextWindow: contextUsage.contextWindow,
1471
+ percent: contextUsage.percent
1472
+ } : null
1473
+ },
1474
+ runtime: {
1475
+ isStreaming: managed.session.isStreaming,
1476
+ isCompacting: managed.session.isCompacting,
1477
+ isRetrying: managed.session.isRetrying,
1478
+ retryAttempt: managed.session.retryAttempt,
1479
+ autoRetryEnabled: managed.session.autoRetryEnabled,
1480
+ autoCompactionEnabled: managed.session.autoCompactionEnabled,
1481
+ pendingMessageCount: managed.session.pendingMessageCount,
1482
+ isBashRunning: managed.session.isBashRunning,
1483
+ hasPendingBashMessages: managed.session.hasPendingBashMessages,
1484
+ steeringMode: managed.session.steeringMode,
1485
+ followUpMode: managed.session.followUpMode
1486
+ },
1487
+ settings: {
1488
+ retry: managed.session.settingsManager.getRetrySettings(),
1489
+ compaction: managed.session.settingsManager.getCompactionSettings()
1490
+ }
1491
+ };
1492
+ });
1493
+ const authProviders = new Set([...allModels.map((model) => model.provider), ...authStorage.list()]);
1494
+ return {
1495
+ sdk: {
1496
+ activeSessions: sessions.size,
1497
+ streamingSessions: sessionList.filter((session) => session.runtime.isStreaming).length,
1498
+ compactingSessions: sessionList.filter((session) => session.runtime.isCompacting).length,
1499
+ retryingSessions: sessionList.filter((session) => session.runtime.isRetrying).length,
1500
+ bashRunningSessions: sessionList.filter((session) => session.runtime.isBashRunning).length,
1501
+ timestamp: now
1502
+ },
1503
+ model: {
1504
+ configured: {
1505
+ provider: configuredProvider ?? null,
1506
+ model: configuredModel ?? null
1507
+ },
1508
+ resolved: resolvedModel ? {
1509
+ ...serializeModel(resolvedModel),
1510
+ isAvailable: availableModelKeys.has(getModelKey(resolvedModel.provider, resolvedModel.id)),
1511
+ usesOAuth: modelRegistry.isUsingOAuth(resolvedModel)
1512
+ } : null,
1513
+ registry: {
1514
+ totalModels: allModels.length,
1515
+ availableModels: availableModels.length,
1516
+ loadError: modelRegistry.getError() ?? null
1517
+ }
1518
+ },
1519
+ auth: {
1520
+ configuredProviders: authStorage.list().sort(),
1521
+ oauthProviders: authStorage.getOAuthProviders().map((provider) => ({
1522
+ id: provider.id,
1523
+ name: provider.name
1524
+ })).sort((a, b) => a.id.localeCompare(b.id)),
1525
+ providers: Array.from(authProviders).sort().map((provider) => ({
1526
+ provider,
1527
+ hasAuth: authStorage.hasAuth(provider)
1528
+ }))
1529
+ },
1530
+ sessions: sessionList
1531
+ };
1532
+ }
1533
+ function serializeModel(model) {
1534
+ return {
1535
+ provider: model.provider,
1536
+ id: model.id,
1537
+ name: model.name,
1538
+ api: model.api,
1539
+ reasoning: model.reasoning ?? false,
1540
+ input: model.input ?? ["text"],
1541
+ contextWindow: model.contextWindow ?? null,
1542
+ maxTokens: model.maxTokens ?? null,
1543
+ cost: model.cost ?? null
1544
+ };
1545
+ }
1546
+ function getModelKey(provider, modelId) {
1547
+ return `${provider}/${modelId}`;
1548
+ }
1549
+ /**
1550
+ * Pi agent — runs pi coding agent in-process via the SDK.
1551
+ *
1552
+ * Uses `createAgentSession` from @mariozechner/pi-coding-agent directly.
1553
+ * Sessions are persisted per-group and automatically continued across restarts.
1554
+ */
1555
+ var pi_exports = /* @__PURE__ */ __exportAll({
1556
+ getFilteredModels: () => getFilteredModels,
1557
+ getPiSdkStatus: () => getPiSdkStatus,
1558
+ kill: () => kill,
1559
+ modelRegistry: () => modelRegistry,
1560
+ run: () => run,
1561
+ sendMessage: () => sendMessage,
1562
+ shutdown: () => shutdown,
1563
+ warmUp: () => warmUp
1564
+ });
1565
+ var logger$4 = createLogger("pi");
1566
+ async function run(group, input, onOutput) {
1567
+ return sendPrompt(await getOrCreateSession(group, input), group, input, onOutput);
1568
+ }
1569
+ /**
1570
+ * Deliver a message to an active streaming session.
1571
+ * If the message ends with `!`, uses `steer` (interrupts agent after current tool).
1572
+ * Otherwise uses `followUp` (delivered after agent finishes current work).
1573
+ * Returns the delivery method, or `false` if no active streaming session.
1574
+ */
1575
+ function sendMessage(groupJid, message) {
1576
+ const managed = sessions.get(groupJid);
1577
+ if (!managed || !managed.session.isStreaming) return false;
1578
+ const { session } = managed;
1579
+ if (message.includes("!")) {
1580
+ session.steer(message).catch((err) => {
1581
+ logger$4.error({
1582
+ group: managed.groupFolder,
1583
+ err
1584
+ }, "Failed to steer message");
1585
+ });
1586
+ return "steer";
1587
+ } else {
1588
+ session.followUp(message).catch((err) => {
1589
+ logger$4.error({
1590
+ group: managed.groupFolder,
1591
+ err
1592
+ }, "Failed to queue follow-up");
1593
+ });
1594
+ return "followup";
1595
+ }
1596
+ }
1597
+ async function kill(groupJid) {
1598
+ const managed = sessions.get(groupJid);
1599
+ if (managed) {
1600
+ managed.aborted = true;
1601
+ try {
1602
+ await managed.session.abort();
1603
+ } catch {}
1604
+ if (managed.abortDone) await managed.abortDone;
1605
+ managed.session.dispose();
1606
+ sessions.delete(groupJid);
1607
+ }
1608
+ }
1609
+ /**
1610
+ * Eagerly restore in-memory sessions for groups that had persisted sessions.
1611
+ * Called on server boot so sessions are warm before message recovery.
1612
+ */
1613
+ async function warmUp(groups, storedSessions, assistantName) {
1614
+ const folderToJid = /* @__PURE__ */ new Map();
1615
+ for (const [jid, group] of Object.entries(groups)) folderToJid.set(group.folder, {
1616
+ jid,
1617
+ group
1618
+ });
1619
+ const toRestore = Object.entries(storedSessions).filter(([folder]) => folderToJid.has(folder) && !sessions.has(folderToJid.get(folder).jid));
1620
+ if (toRestore.length === 0) return;
1621
+ logger$4.info({ count: toRestore.length }, "Warming up persisted sessions");
1622
+ const results = await Promise.allSettled(toRestore.map(async ([folder, sessionId]) => {
1623
+ const { jid, group } = folderToJid.get(folder);
1624
+ await getOrCreateSession(group, {
1625
+ prompt: "",
1626
+ sessionId,
1627
+ groupFolder: folder,
1628
+ chatJid: jid,
1629
+ isMain: folder === "main",
1630
+ assistantName
1631
+ });
1632
+ return {
1633
+ jid,
1634
+ group: group.name
1635
+ };
1636
+ }));
1637
+ for (const r of results) if (r.status === "fulfilled") logger$4.info({ group: r.value.group }, "Session restored");
1638
+ else logger$4.warn({ err: r.reason }, "Failed to restore session");
1639
+ }
1640
+ async function shutdown(timeoutMs) {
1641
+ for (const [jid, managed] of sessions) {
1642
+ try {
1643
+ managed.session.dispose();
1644
+ } catch {}
1645
+ sessions.delete(jid);
1646
+ }
1647
+ await new Promise((resolve) => setTimeout(resolve, Math.min(timeoutMs, 1e3)));
1648
+ }
1649
+ function getPiSdkStatus() {
1650
+ return getPiSdkStatus$1({
1651
+ sessions,
1652
+ modelRegistry,
1653
+ authStorage,
1654
+ resolvedModel: resolveModel(),
1655
+ configuredProvider: config.get("piProvider"),
1656
+ configuredModel: config.get("piModel")
1657
+ });
1658
+ }
1659
+ var commands = {
1660
+ help: handleHelp,
1661
+ info: handleInfo,
1662
+ new: handleNew,
1663
+ stop: handleStop,
1664
+ model: handleModel,
1665
+ thinking: handleThinking,
1666
+ compact: handleCompact
1667
+ };
1668
+ var VALID_THINKING_LEVELS = [
1669
+ "off",
1670
+ "minimal",
1671
+ "low",
1672
+ "medium",
1673
+ "high",
1674
+ "xhigh"
1675
+ ];
1676
+ /** Descriptions for registering commands with external channels (e.g. Telegram). */
1677
+ const commandDescriptions = [
1678
+ {
1679
+ command: "help",
1680
+ description: "Show available commands"
1681
+ },
1682
+ {
1683
+ command: "info",
1684
+ description: "Show session info and workspace overview"
1685
+ },
1686
+ {
1687
+ command: "new",
1688
+ description: "Reset session and start fresh"
1689
+ },
1690
+ {
1691
+ command: "stop",
1692
+ description: "Stop the current agent session"
1693
+ },
1694
+ {
1695
+ command: "model",
1696
+ description: "List or set model",
1697
+ hasParams: true
1698
+ },
1699
+ {
1700
+ command: "thinking",
1701
+ description: "Show or set thinking level",
1702
+ hasParams: true,
1703
+ paramOptions: VALID_THINKING_LEVELS
1704
+ },
1705
+ {
1706
+ command: "compact",
1707
+ description: "Compact current session context"
1708
+ }
1709
+ ];
1710
+ /** Commands that bypass the queue and execute immediately (even during streaming). */
1711
+ var priorityCommands = {
1712
+ stop: handleStop,
1713
+ new: handleNew,
1714
+ info: handleInfo
1715
+ };
1716
+ /**
1717
+ * Try to handle a priority command that must bypass the queue.
1718
+ * Called from the message loop and send API before enqueueing.
1719
+ * Returns true if the message was a priority command (consumed).
1720
+ */
1721
+ function tryPriorityCommand(ctx, text) {
1722
+ if (!text.startsWith("/")) return false;
1723
+ const spaceIdx = text.indexOf(" ");
1724
+ const name = (spaceIdx > 0 ? text.slice(1, spaceIdx) : text.slice(1)).toLowerCase();
1725
+ const handler = priorityCommands[name];
1726
+ if (!handler) return false;
1727
+ Promise.resolve(handler(ctx, "")).catch((err) => {
1728
+ ctx.server.sendBotMessage(ctx.chatJid, `Command /${name} failed: ${String(err)}`).catch(() => {});
1729
+ });
1730
+ return true;
1731
+ }
1732
+ /**
1733
+ * Try to handle a message as a `/command`. Returns true if handled.
1734
+ */
1735
+ async function tryCommand(ctx, text) {
1736
+ if (!text.startsWith("/")) return false;
1737
+ const spaceIdx = text.indexOf(" ");
1738
+ const name = (spaceIdx > 0 ? text.slice(1, spaceIdx) : text.slice(1)).toLowerCase();
1739
+ const arg = spaceIdx > 0 ? text.slice(spaceIdx + 1).trim() : "";
1740
+ const handler = commands[name];
1741
+ if (handler) {
1742
+ try {
1743
+ await handler(ctx, arg);
1744
+ } catch (err) {
1745
+ await ctx.server.sendBotMessage(ctx.chatJid, `Command /${name} failed: ${String(err)}`);
1746
+ }
1747
+ return true;
1748
+ }
1749
+ await ctx.server.sendBotMessage(ctx.chatJid, `Unknown command \`/${name}\`. Use \`/help\` for available commands.`);
1750
+ return true;
1751
+ }
1752
+ async function handleHelp(ctx) {
1753
+ const lines = ["**Available commands:**", ...commandDescriptions.map((c) => `- \`/${c.command}\` — ${c.description}`)];
1754
+ await ctx.server.sendBotMessage(ctx.chatJid, lines.join("\n"));
1755
+ }
1756
+ async function handleInfo(ctx) {
1757
+ const resolved = resolveGroupModel(ctx.group);
1758
+ const modelLabel = resolved ? `${resolved.provider}/${resolved.id}` : "none";
1759
+ const modelSource = ctx.group.model ? `group (${ctx.group.model})` : "default";
1760
+ const managed = sessions.get(ctx.chatJid);
1761
+ const lines = [
1762
+ `**Group:** ${ctx.group.name} (\`${ctx.group.folder}\`)`,
1763
+ `**Model:** \`${modelLabel}\` [${modelSource}]`,
1764
+ `**Trigger:** ${ctx.group.requiresTrigger ? "required" : "not required"}`
1765
+ ];
1766
+ if (managed) {
1767
+ const s = managed.session;
1768
+ const actualModel = s.model;
1769
+ const actualLabel = actualModel ? `${actualModel.provider}/${actualModel.id}` : "none";
1770
+ const ctx$ = s.getContextUsage();
1771
+ lines.push("", `**Session:** \`${s.sessionId}\``, actualLabel !== modelLabel ? `**Session model:** \`${actualLabel}\` ⚠️ mismatch` : `**Session model:** \`${actualLabel}\``, `**Messages:** ${s.messages.length}`, `**Streaming:** ${s.isStreaming ? "yes" : "no"}`, `**Idle:** ${formatDuration(Date.now() - managed.lastActivity)}`);
1772
+ if (ctx$?.tokens != null && ctx$.percent != null) {
1773
+ const pct = Math.round(ctx$.percent);
1774
+ lines.push(`**Context:** ${ctx$.tokens.toLocaleString()} / ${ctx$.contextWindow.toLocaleString()} tokens (${pct}%)`);
1775
+ }
1776
+ } else lines.push("", "*No active session*");
1777
+ const allGroups = ctx.server.getRegisteredGroups();
1778
+ const allTasks = await getAllTasks();
1779
+ lines.push("", "---", "", "**All Groups:**");
1780
+ for (const [jid, g] of Object.entries(allGroups)) {
1781
+ const gResolved = resolveGroupModel(g);
1782
+ const gModelLabel = gResolved ? `${gResolved.provider}/${gResolved.id}` : "none";
1783
+ const gManaged = sessions.get(jid);
1784
+ const sessionPart = gManaged ? `active, idle ${formatDuration(Date.now() - gManaged.lastActivity)}` : "no session";
1785
+ const soulPart = g.soul ? " 🧬" : "";
1786
+ lines.push(`- **${g.name}** (\`${g.folder}\`) — \`${gModelLabel}\` — ${sessionPart}${soulPart}`);
1787
+ }
1788
+ const cutoff = (/* @__PURE__ */ new Date(Date.now() - 1440 * 60 * 1e3)).toISOString();
1789
+ const recentTasks = allTasks.filter((t) => t.status === "active" || t.last_run && t.last_run > cutoff);
1790
+ if (recentTasks.length > 0) {
1791
+ lines.push("", `**Tasks** (active or run in last 24h): ${recentTasks.length}`);
1792
+ for (const t of recentTasks.slice(0, 10)) {
1793
+ const groupName = Object.values(allGroups).find((g) => g.folder === t.group_folder)?.name || t.group_folder;
1794
+ lines.push(`- [${t.status}] \`${t.schedule_type}:${t.schedule_value}\` in **${groupName}** — ${t.prompt.slice(0, 60)}`);
1795
+ }
1796
+ if (recentTasks.length > 10) lines.push(` … and ${recentTasks.length - 10} more`);
1797
+ }
1798
+ lines.push("", `*${Object.keys(allGroups).length} groups, ${allTasks.filter((t) => t.status === "active").length} active tasks*`);
1799
+ await ctx.server.sendBotMessage(ctx.chatJid, lines.join("\n"));
1800
+ }
1801
+ function formatDuration(ms) {
1802
+ const s = Math.floor(ms / 1e3);
1803
+ if (s < 60) return `${s}s`;
1804
+ const m = Math.floor(s / 60);
1805
+ if (m < 60) return `${m}m ${s % 60}s`;
1806
+ return `${Math.floor(m / 60)}h ${m % 60}m`;
1807
+ }
1808
+ async function handleNew(ctx) {
1809
+ ctx.server.pi.kill(ctx.chatJid);
1810
+ await removeSession(ctx.group.folder);
1811
+ const sessionDir = path$1.join(SESSIONS_DIR, ctx.group.folder);
1812
+ fs$1.rmSync(sessionDir, {
1813
+ recursive: true,
1814
+ force: true
1815
+ });
1816
+ delete ctx.server.sessions[ctx.group.folder];
1817
+ await clearMessages(ctx.chatJid);
1818
+ await ctx.server.sendBotMessage(ctx.chatJid, "Session reset. Next message starts a fresh conversation.");
1819
+ }
1820
+ async function handleStop(ctx) {
1821
+ if (!sessions.get(ctx.chatJid)) return;
1822
+ await ctx.server.pi.kill(ctx.chatJid);
1823
+ }
1824
+ async function handleModel(ctx, arg) {
1825
+ if (!arg) return listModels(ctx);
1826
+ if (arg === "default" || arg === "reset") return resetModel(ctx);
1827
+ return setModel(ctx, arg);
1828
+ }
1829
+ async function listModels(ctx) {
1830
+ const available = getFilteredModels();
1831
+ const resolved = resolveGroupModel(ctx.group);
1832
+ const lines = [
1833
+ `**Current:** \`${ctx.group.model || "default (global)"}\` → \`${resolved ? `${resolved.provider}/${resolved.id}` : "none"}\``,
1834
+ "",
1835
+ "**Available models:**"
1836
+ ];
1837
+ const byProvider = /* @__PURE__ */ new Map();
1838
+ for (const m of available) {
1839
+ let list = byProvider.get(m.provider);
1840
+ if (!list) {
1841
+ list = [];
1842
+ byProvider.set(m.provider, list);
1843
+ }
1844
+ const marker = resolved && m.provider === resolved.provider && m.id === resolved.id ? " ←" : "";
1845
+ list.push(`- \`${m.provider}/${m.id}\`${marker}`);
1846
+ }
1847
+ for (const [provider, models] of byProvider) {
1848
+ lines.push("", `**${provider}**`);
1849
+ lines.push(...models);
1850
+ }
1851
+ lines.push("", "`/model <query>` | `/model default`");
1852
+ await ctx.server.sendBotMessage(ctx.chatJid, lines.join("\n"));
1853
+ }
1854
+ async function resetModel(ctx) {
1855
+ const had = ctx.group.model;
1856
+ await updateGroupModel(ctx, void 0);
1857
+ await ctx.server.sendBotMessage(ctx.chatJid, had ? `Model reset to default (was: \`${had}\`).` : "Already using default model.");
1858
+ }
1859
+ async function setModel(ctx, query) {
1860
+ const match = findModel(query);
1861
+ if (!match) {
1862
+ await ctx.server.sendBotMessage(ctx.chatJid, `No model matching "${query}". Use \`/model\` to list available models.`);
1863
+ return;
1864
+ }
1865
+ const modelStr = `${match.provider}/${match.id}`;
1866
+ await updateGroupModel(ctx, modelStr);
1867
+ await ctx.server.sendBotMessage(ctx.chatJid, `Model set to \`${modelStr}\``);
1868
+ }
1869
+ async function updateGroupModel(ctx, model) {
1870
+ const old = ctx.group.model;
1871
+ ctx.group.model = model;
1872
+ await ctx.server.setRegisteredGroup(ctx.chatJid, ctx.group);
1873
+ if (old !== model) ctx.server.pi.kill(ctx.chatJid);
1874
+ }
1875
+ async function handleThinking(ctx, arg) {
1876
+ if (!arg) {
1877
+ const current = ctx.group.thinkingLevel || "off";
1878
+ await ctx.server.sendBotMessage(ctx.chatJid, `**Thinking level:** \`${current}\`\n\n\`/thinking <level>\`\nLevels: ${VALID_THINKING_LEVELS.map((l) => `\`${l}\``).join(", ")}`);
1879
+ return;
1880
+ }
1881
+ const level = arg.toLowerCase();
1882
+ if (!VALID_THINKING_LEVELS.includes(level)) {
1883
+ await ctx.server.sendBotMessage(ctx.chatJid, `Invalid thinking level "${arg}". Must be one of: ${VALID_THINKING_LEVELS.map((l) => `\`${l}\``).join(", ")}`);
1884
+ return;
1885
+ }
1886
+ const old = ctx.group.thinkingLevel;
1887
+ ctx.group.thinkingLevel = level === "off" ? void 0 : level;
1888
+ await ctx.server.setRegisteredGroup(ctx.chatJid, ctx.group);
1889
+ if (old !== ctx.group.thinkingLevel) ctx.server.pi.kill(ctx.chatJid);
1890
+ await ctx.server.sendBotMessage(ctx.chatJid, `Thinking level set to \`${level}\``);
1891
+ }
1892
+ async function handleCompact(ctx, arg) {
1893
+ const managed = sessions.get(ctx.chatJid);
1894
+ if (!managed) {
1895
+ await ctx.server.sendBotMessage(ctx.chatJid, "No active session to compact. Send a message first, then run `/compact`.");
1896
+ return;
1897
+ }
1898
+ const session = managed.session;
1899
+ if (session.isCompacting) {
1900
+ await ctx.server.sendBotMessage(ctx.chatJid, "Compaction is already in progress.");
1901
+ return;
1902
+ }
1903
+ const before = session.getContextUsage();
1904
+ await ctx.server.sendBotMessage(ctx.chatJid, "Compacting session context…");
1905
+ const result = await session.compact(arg || void 0);
1906
+ const after = session.getContextUsage();
1907
+ const lines = ["**Compaction complete.**"];
1908
+ if (typeof result?.tokensBefore === "number") lines.push(`**Tokens before:** ${result.tokensBefore.toLocaleString()}`);
1909
+ if (before?.tokens != null) lines.push(`**Est. before:** ${before.tokens.toLocaleString()}`);
1910
+ if (after?.tokens != null) lines.push(`**Est. after:** ${after.tokens.toLocaleString()}`);
1911
+ await ctx.server.sendBotMessage(ctx.chatJid, lines.join("\n"));
1912
+ }
1913
+ function findModel(query) {
1914
+ const available = getFilteredModels();
1915
+ if (available.length === 0) return void 0;
1916
+ const q = query.toLowerCase();
1917
+ const exact = available.filter((m) => `${m.provider}/${m.id}`.toLowerCase() === q);
1918
+ if (exact.length === 1) return exact[0];
1919
+ const byId = available.filter((m) => m.id.toLowerCase() === q);
1920
+ if (byId.length === 1) return byId[0];
1921
+ const tokens = q.split(/[\s/]+/).filter(Boolean);
1922
+ const fuzzy = available.filter((m) => {
1923
+ const label = `${m.provider}/${m.id}`.toLowerCase();
1924
+ return tokens.every((t) => label.includes(t));
1925
+ });
1926
+ const matches = exact.length > 1 ? exact : byId.length > 1 ? byId : fuzzy;
1927
+ if (matches.length === 0) return void 0;
1928
+ matches.sort((a, b) => a.id.length - b.id.length || b.id.localeCompare(a.id, void 0, { numeric: true }));
1929
+ return matches[0];
1930
+ }
1931
+ new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*[A-Za-z]`, "g");
16
1932
  await init();
17
1933
  var logger$3 = createLogger("telegram");
18
1934
  var JID_PREFIX = "tg:";
19
1935
  var POLL_INTERVAL = 1e3;
20
1936
  var POLL_TIMEOUT = 30;
21
1937
  var API_BASE = "https://api.telegram.org/bot";
22
- var STREAM_THROTTLE_MIN = 700;
23
- var STREAM_THROTTLE_MAX = 1500;
24
- var STREAM_THROTTLE_STEP = 200;
1938
+ var STREAM_THROTTLE_MIN = 1e3;
1939
+ var STREAM_THROTTLE_MAX = 1e4;
1940
+ var STREAM_THROTTLE_STEP = 1e3;
25
1941
  var STREAM_THROTTLE_JITTER = 120;
26
1942
  var STREAM_INITIAL_DELAY = 100;
27
1943
  var TG_MAX_TEXT = 4096;
@@ -61,6 +1977,7 @@ var TelegramChannel = class {
61
1977
  const state = this.streamState.get(jid);
62
1978
  const { text: formatted, parse_mode } = formatForTelegram(text);
63
1979
  const parseMode = { parse_mode };
1980
+ const plainText = stripThinkingTags(text);
64
1981
  if (state) {
65
1982
  this.streamState.delete(jid);
66
1983
  if (state.editTimer) clearTimeout(state.editTimer);
@@ -73,13 +1990,43 @@ var TelegramChannel = class {
73
1990
  ...parseMode
74
1991
  });
75
1992
  return;
76
- } catch {}
1993
+ } catch (editErr) {
1994
+ if (isTgParseError(editErr)) try {
1995
+ logger$3.debug({
1996
+ err: editErr,
1997
+ formattedText: formatted.slice(0, 500),
1998
+ originalText: text.slice(0, 500)
1999
+ }, "HTML edit failed, retrying as plain text");
2000
+ await this.api("editMessageText", {
2001
+ chat_id: chatId,
2002
+ message_id: state.messageId,
2003
+ text: plainText.slice(0, TG_MAX_TEXT)
2004
+ });
2005
+ return;
2006
+ } catch (plainErr) {
2007
+ logger$3.debug({ err: plainErr }, "Plain-text edit also failed, sending new message");
2008
+ }
2009
+ }
2010
+ }
2011
+ try {
2012
+ await this.api("sendMessage", {
2013
+ chat_id: chatId,
2014
+ text: formatted,
2015
+ ...parseMode
2016
+ });
2017
+ } catch (err) {
2018
+ if (isTgParseError(err)) {
2019
+ logger$3.debug({
2020
+ err,
2021
+ formattedText: formatted.slice(0, 500),
2022
+ originalText: text.slice(0, 500)
2023
+ }, "HTML send failed, retrying as plain text");
2024
+ await this.api("sendMessage", {
2025
+ chat_id: chatId,
2026
+ text: plainText
2027
+ });
2028
+ } else throw err;
77
2029
  }
78
- await this.api("sendMessage", {
79
- chat_id: chatId,
80
- text: formatted,
81
- ...parseMode
82
- });
83
2030
  }
84
2031
  isConnected() {
85
2032
  return this.connected;
@@ -113,16 +2060,69 @@ var TelegramChannel = class {
113
2060
  switch (event.type) {
114
2061
  case "thinking_delta": {
115
2062
  const state = this.getOrCreateStreamState(jid);
116
- state.thinkingText += event.delta || "";
2063
+ const last = state.details[state.details.length - 1];
2064
+ if (last && last.type === "thinking") last.text += event.delta || "";
2065
+ else state.details.push({
2066
+ type: "thinking",
2067
+ text: event.delta || ""
2068
+ });
117
2069
  this.scheduleStreamEdit(jid, state);
118
2070
  break;
119
2071
  }
120
2072
  case "text_delta": {
121
2073
  const state = this.getOrCreateStreamState(jid);
122
- state.text += event.delta || "";
2074
+ const last = state.details[state.details.length - 1];
2075
+ if (last && last.type === "text") last.text += event.delta || "";
2076
+ else state.details.push({
2077
+ type: "text",
2078
+ text: event.delta || ""
2079
+ });
2080
+ this.scheduleStreamEdit(jid, state);
2081
+ break;
2082
+ }
2083
+ case "tool_start": {
2084
+ const state = this.getOrCreateStreamState(jid);
2085
+ const tool = {
2086
+ name: event.toolName || "tool",
2087
+ done: false
2088
+ };
2089
+ state.tools.push(tool);
2090
+ state.details.push({
2091
+ type: "tool",
2092
+ tool
2093
+ });
123
2094
  this.scheduleStreamEdit(jid, state);
124
2095
  break;
125
2096
  }
2097
+ case "tool_end": {
2098
+ const state = this.getOrCreateStreamState(jid);
2099
+ for (let i = state.tools.length - 1; i >= 0; i--) {
2100
+ const t = state.tools[i];
2101
+ if (t.name === event.toolName && !t.done) {
2102
+ t.done = true;
2103
+ t.isError = event.isError;
2104
+ t.resultPreview = event.resultPreview;
2105
+ break;
2106
+ }
2107
+ }
2108
+ this.scheduleStreamEdit(jid, state);
2109
+ break;
2110
+ }
2111
+ case "error": {
2112
+ const state = this.getOrCreateStreamState(jid);
2113
+ if (state.editTimer) {
2114
+ clearTimeout(state.editTimer);
2115
+ state.editTimer = null;
2116
+ }
2117
+ const errorMsg = event.error || "Unknown error";
2118
+ const chatId = jid.slice(3);
2119
+ this.api("sendMessage", {
2120
+ chat_id: chatId,
2121
+ text: `⚠️ ${errorMsg}`
2122
+ }).catch(() => {});
2123
+ this.streamState.delete(jid);
2124
+ break;
2125
+ }
126
2126
  case "text_end":
127
2127
  case "agent_end": {
128
2128
  const state = this.streamState.get(jid);
@@ -139,8 +2139,8 @@ var TelegramChannel = class {
139
2139
  if (!state) {
140
2140
  state = {
141
2141
  chatId: jid.slice(3),
142
- text: "",
143
- thinkingText: "",
2142
+ details: [],
2143
+ tools: [],
144
2144
  messageId: null,
145
2145
  sendPromise: null,
146
2146
  lastEdit: 0,
@@ -162,42 +2162,83 @@ var TelegramChannel = class {
162
2162
  }, delay);
163
2163
  }
164
2164
  async flushStreamEdit(jid, state) {
2165
+ if (!this.streamState.has(jid)) return;
165
2166
  if (state.sendPromise) {
166
2167
  await state.sendPromise.catch(() => {});
167
2168
  state.sendPromise = null;
168
2169
  }
169
- const display = state.text.trim() || (state.thinkingText.trim() ? `<thinking>${state.thinkingText.trim()}</thinking>` : "");
170
- if (!display) return;
171
- const { text: formatted, parse_mode } = formatForTelegram(display.length > TG_MAX_TEXT ? display.slice(0, TG_MAX_TEXT - 1) + "…" : display);
172
- const snapshotLen = display.length;
173
- try {
2170
+ if (!this.streamState.has(jid)) return;
2171
+ const htmlParts = [];
2172
+ let toolBuf = [];
2173
+ const flushTools = () => {
2174
+ if (!toolBuf.length) return;
2175
+ htmlParts.push(formatToolLines(toolBuf));
2176
+ toolBuf = [];
2177
+ };
2178
+ for (const item of state.details) if (item.type === "tool") toolBuf.push(item.tool);
2179
+ else if (item.type === "thinking") {
2180
+ flushTools();
2181
+ const trimmed = item.text.trim().replace(/\*\*/g, "");
2182
+ if (trimmed) htmlParts.push(`💭 <b>${escHtml(trimmed)}</b>`);
2183
+ } else {
2184
+ flushTools();
2185
+ const clean = stripInternalTags(item.text).trim();
2186
+ if (clean) {
2187
+ const truncated = clean.length > TG_MAX_TEXT ? clean.slice(0, TG_MAX_TEXT - 1) + "…" : clean;
2188
+ htmlParts.push(formatForTelegram(truncated).text);
2189
+ }
2190
+ }
2191
+ flushTools();
2192
+ if (!htmlParts.length) return;
2193
+ htmlParts.push("⏳");
2194
+ const formatted = htmlParts.join("\n\n");
2195
+ const parse_mode = "HTML";
2196
+ const snapshotLen = detailsTextLen(state.details);
2197
+ const streamApiOpts = { maxRetryWait: 5 };
2198
+ const sendOrEdit = async (text, parseMode) => {
2199
+ if (!this.streamState.has(jid)) return;
2200
+ const body = {
2201
+ chat_id: state.chatId,
2202
+ text
2203
+ };
2204
+ if (parseMode) body.parse_mode = parseMode;
174
2205
  if (state.messageId === null) {
175
- state.sendPromise = this.api("sendMessage", {
176
- chat_id: state.chatId,
177
- text: formatted,
178
- parse_mode
179
- }).then((msg) => {
2206
+ state.sendPromise = this.api("sendMessage", body, streamApiOpts).then((msg) => {
180
2207
  state.messageId = msg.message_id;
181
2208
  state.sendPromise = null;
182
2209
  });
183
2210
  await state.sendPromise;
184
- } else await this.api("editMessageText", {
185
- chat_id: state.chatId,
186
- message_id: state.messageId,
187
- text: formatted,
188
- parse_mode
189
- });
2211
+ } else {
2212
+ body.message_id = state.messageId;
2213
+ await this.api("editMessageText", body, streamApiOpts);
2214
+ }
2215
+ };
2216
+ try {
2217
+ await sendOrEdit(formatted, parse_mode);
190
2218
  state.lastEdit = Date.now();
191
2219
  state.throttleMs = Math.max(STREAM_THROTTLE_MIN, state.throttleMs - 50);
192
2220
  } catch (err) {
193
- const retryAfterMs = getRetryAfterMs(err);
194
- if (retryAfterMs > 0) state.throttleMs = Math.min(STREAM_THROTTLE_MAX, Math.max(state.throttleMs + STREAM_THROTTLE_STEP, retryAfterMs));
195
- logger$3.debug({
196
- err,
197
- throttleMs: state.throttleMs
198
- }, "Stream edit failed");
2221
+ if (isTgParseError(err)) {
2222
+ logger$3.debug({
2223
+ err,
2224
+ formattedText: formatted.slice(0, 500)
2225
+ }, "Stream HTML failed, retrying as plain text");
2226
+ try {
2227
+ await sendOrEdit(stripHtmlTags(formatted).slice(0, TG_MAX_TEXT));
2228
+ state.lastEdit = Date.now();
2229
+ } catch (retryErr) {
2230
+ logger$3.debug({ err: retryErr }, "Stream plain-text fallback also failed");
2231
+ }
2232
+ } else if (isTgMessageNotModified(err)) {} else {
2233
+ const retryAfterMs = getRetryAfterMs(err);
2234
+ if (retryAfterMs > 0) state.throttleMs = Math.min(STREAM_THROTTLE_MAX, Math.max(state.throttleMs + STREAM_THROTTLE_STEP, retryAfterMs));
2235
+ logger$3.debug({
2236
+ err,
2237
+ throttleMs: state.throttleMs
2238
+ }, "Stream edit failed");
2239
+ }
199
2240
  }
200
- if ((state.text.trim() || (state.thinkingText.trim() ? `<thinking>${state.thinkingText.trim()}</thinking>` : "")).length > snapshotLen && this.streamState.has(jid)) this.scheduleStreamEdit(jid, state);
2241
+ if (detailsTextLen(state.details) > snapshotLen && this.streamState.has(jid)) this.scheduleStreamEdit(jid, state);
201
2242
  }
202
2243
  startPolling() {
203
2244
  this.stopPolling();
@@ -219,17 +2260,17 @@ var TelegramChannel = class {
219
2260
  const res = await fetch(`${API_BASE}${this.config.botToken}/getUpdates?${params}`, { signal });
220
2261
  if (!res.ok) {
221
2262
  if (res.status === 409) {
222
- logger$3.warn("Telegram 409 conflict: another polling instance is active, retrying in 30s");
2263
+ logger$3.debug("Telegram 409 conflict: another polling instance is active, retrying in 30s");
223
2264
  await sleep(3e4, signal);
224
2265
  continue;
225
2266
  }
226
- logger$3.warn({ status: res.status }, "Telegram getUpdates HTTP error");
2267
+ logger$3.debug({ status: res.status }, "Telegram getUpdates HTTP error");
227
2268
  await sleep(POLL_INTERVAL, signal);
228
2269
  continue;
229
2270
  }
230
2271
  const data = await res.json();
231
2272
  if (!data.ok) {
232
- logger$3.warn({ description: data.description }, "Telegram getUpdates API error");
2273
+ logger$3.debug({ description: data.description }, "Telegram getUpdates API error");
233
2274
  await sleep(POLL_INTERVAL, signal);
234
2275
  continue;
235
2276
  }
@@ -240,7 +2281,7 @@ var TelegramChannel = class {
240
2281
  }
241
2282
  } catch (err) {
242
2283
  if (signal.aborted) break;
243
- logger$3.warn({ err }, "Telegram polling error");
2284
+ logger$3.debug({ err }, "Telegram polling error");
244
2285
  await sleep(POLL_INTERVAL, signal);
245
2286
  }
246
2287
  };
@@ -250,37 +2291,120 @@ var TelegramChannel = class {
250
2291
  this.pollAbort?.abort();
251
2292
  this.pollAbort = null;
252
2293
  }
253
- async api(method, body) {
2294
+ async api(method, body, opts, _retries = 0) {
254
2295
  const res = await fetch(`${API_BASE}${this.config.botToken}/${method}`, {
255
2296
  method: "POST",
256
2297
  headers: { "content-type": "application/json" },
257
2298
  body: body ? JSON.stringify(body) : void 0
258
2299
  });
259
2300
  const data = await res.json();
260
- if (!data.ok) throw new TelegramApiError(method, data.description || "unknown error", res.status, data);
2301
+ if (!data.ok) {
2302
+ if (res.status === 429 && _retries < 2) {
2303
+ const retryAfter = Number(data.parameters?.retry_after || 5);
2304
+ const maxWait = opts?.maxRetryWait ?? 30;
2305
+ if (retryAfter > maxWait) {
2306
+ logger$3.debug({
2307
+ method,
2308
+ retryAfter,
2309
+ maxWait
2310
+ }, "Telegram 429 retry_after exceeds max wait, skipping retry");
2311
+ throw new TelegramApiError(method, data.description || "unknown error", res.status, data);
2312
+ }
2313
+ logger$3.debug({
2314
+ method,
2315
+ retryAfter,
2316
+ attempt: _retries + 1
2317
+ }, "Telegram 429 rate limit, waiting");
2318
+ await sleep(retryAfter * 1e3);
2319
+ return this.api(method, body, opts, _retries + 1);
2320
+ }
2321
+ throw new TelegramApiError(method, data.description || "unknown error", res.status, data);
2322
+ }
261
2323
  return data.result;
262
2324
  }
263
2325
  handleUpdate(update) {
264
2326
  const message = update.message || update.edited_message || update.channel_post;
265
- if (!message?.text) return;
2327
+ if (!message) return;
2328
+ if (!(message.text || message.caption || message.photo || message.document || message.voice || message.audio || message.video || message.video_note || message.sticker)) return;
266
2329
  if (message.from?.id === this.botId) return;
267
2330
  const chatId = String(message.chat.id);
268
2331
  const jid = `${JID_PREFIX}${chatId}`;
269
2332
  const isGroup = message.chat.type === "group" || message.chat.type === "supergroup";
270
2333
  const senderName = message.from ? [message.from.first_name, message.from.last_name].filter(Boolean).join(" ") : message.chat.title || chatId;
271
2334
  const now = (/* @__PURE__ */ new Date(message.date * 1e3)).toISOString();
2335
+ const msgId = crypto.randomUUID();
272
2336
  const msg = {
273
- id: crypto.randomUUID(),
2337
+ id: msgId,
274
2338
  chat_jid: jid,
275
2339
  sender: String(message.from?.id || chatId),
276
2340
  sender_name: senderName,
277
- content: message.text,
2341
+ content: message.text || message.caption || "",
278
2342
  timestamp: now,
279
2343
  is_from_me: false,
280
2344
  is_bot_message: false
281
2345
  };
282
- this.opts.onMessage(jid, msg);
283
- this.opts.onChatMetadata(jid, now, senderName, "telegram", isGroup);
2346
+ const group = this.opts.registeredGroups()[jid];
2347
+ (group ? this.downloadMedia(message, msgId, group.folder) : Promise.resolve([])).then((attachments) => {
2348
+ if (attachments.length) msg.meta = { attachments };
2349
+ this.opts.onMessage(jid, msg);
2350
+ this.opts.onChatMetadata(jid, now, senderName, "telegram", isGroup);
2351
+ }).catch((err) => {
2352
+ logger$3.debug({ err }, "Failed to download Telegram media, sending text-only");
2353
+ this.opts.onMessage(jid, msg);
2354
+ this.opts.onChatMetadata(jid, now, senderName, "telegram", isGroup);
2355
+ });
2356
+ }
2357
+ async downloadMedia(message, msgId, groupFolder) {
2358
+ const attachments = [];
2359
+ if (message.photo?.length) {
2360
+ const photo = message.photo[message.photo.length - 1];
2361
+ const att = await this.downloadTelegramFile(photo.file_id, groupFolder, msgId, "photo.jpg", "image/jpeg");
2362
+ if (att) attachments.push(att);
2363
+ }
2364
+ if (message.document) {
2365
+ const att = await this.downloadTelegramFile(message.document.file_id, groupFolder, msgId, message.document.file_name || "document", message.document.mime_type || getMimeType(message.document.file_name || "document"));
2366
+ if (att) attachments.push(att);
2367
+ }
2368
+ if (message.voice) {
2369
+ const att = await this.downloadTelegramFile(message.voice.file_id, groupFolder, msgId, "voice.ogg", message.voice.mime_type || "audio/ogg");
2370
+ if (att) attachments.push(att);
2371
+ }
2372
+ if (message.audio) {
2373
+ const att = await this.downloadTelegramFile(message.audio.file_id, groupFolder, msgId, message.audio.file_name || "audio.mp3", message.audio.mime_type || "audio/mpeg");
2374
+ if (att) attachments.push(att);
2375
+ }
2376
+ if (message.video) {
2377
+ const att = await this.downloadTelegramFile(message.video.file_id, groupFolder, msgId, message.video.file_name || "video.mp4", message.video.mime_type || "video/mp4");
2378
+ if (att) attachments.push(att);
2379
+ }
2380
+ if (message.video_note) {
2381
+ const att = await this.downloadTelegramFile(message.video_note.file_id, groupFolder, msgId, "video_note.mp4", "video/mp4");
2382
+ if (att) attachments.push(att);
2383
+ }
2384
+ if (message.sticker && !message.sticker.is_animated) {
2385
+ const att = await this.downloadTelegramFile(message.sticker.file_id, groupFolder, msgId, "sticker.webp", "image/webp");
2386
+ if (att) attachments.push(att);
2387
+ }
2388
+ return attachments;
2389
+ }
2390
+ async downloadTelegramFile(fileId, groupFolder, msgId, name, mimeType) {
2391
+ try {
2392
+ const fileInfo = await this.api("getFile", { file_id: fileId });
2393
+ const url = `https://api.telegram.org/file/bot${this.config.botToken}/${fileInfo.file_path}`;
2394
+ const res = await fetch(url);
2395
+ if (!res.ok) return null;
2396
+ return saveAttachment(groupFolder, msgId, {
2397
+ data: Buffer.from(await res.arrayBuffer()).toString("base64"),
2398
+ mimeType,
2399
+ name
2400
+ });
2401
+ } catch (err) {
2402
+ logger$3.debug({
2403
+ err,
2404
+ fileId
2405
+ }, "Failed to download Telegram file");
2406
+ return null;
2407
+ }
284
2408
  }
285
2409
  };
286
2410
  var TelegramApiError = class extends Error {
@@ -291,39 +2415,28 @@ var TelegramApiError = class extends Error {
291
2415
  this.response = response;
292
2416
  }
293
2417
  };
294
- /** Format message text as Telegram MarkdownV2 — parse with md4x, render AST to TG format */
2418
+ /** Format message text as Telegram HTML — parse markdown with md4x, render AST to HTML tags */
295
2419
  function formatForTelegram(text) {
296
2420
  let remaining = text.replace(/<\/?internal>/g, (t) => t.replace("internal", "thinking"));
297
- const parts = [];
298
- const thinkingRe = /<thinking>([\s\S]*?)<\/thinking>/g;
299
- let lastIdx = 0;
300
- let m;
301
- while ((m = thinkingRe.exec(remaining)) !== null) {
302
- if (m.index > lastIdx) parts.push(mdToTgV2(remaining.slice(lastIdx, m.index)));
303
- const trimmed = (m[1] || "").trim();
304
- if (trimmed) {
305
- const quoted = escTgV2(trimmed).split("\n").map((l) => `>${l}`).join("\n");
306
- parts.push(quoted);
307
- }
308
- lastIdx = m.index + m[0].length;
309
- }
310
- if (lastIdx < remaining.length) parts.push(mdToTgV2(remaining.slice(lastIdx)));
2421
+ remaining = remaining.replace(/<tool\s+name="[^"]*"\s+status="[^"]*"(?:\s+preview="[^"]*")?\s*\/>/g, "");
2422
+ remaining = remaining.replace(/<details><summary>thinking<\/summary>[\s\S]*?<\/details>/g, "");
2423
+ remaining = remaining.replace(/<thinking>[\s\S]*?<\/thinking>/g, "");
311
2424
  return {
312
- text: parts.join("").replace(/\n{3,}/g, "\n\n").trim(),
313
- parse_mode: "MarkdownV2"
2425
+ text: [mdToTgHtml(remaining)].join("").replace(/\n{3,}/g, "\n\n").trim(),
2426
+ parse_mode: "HTML"
314
2427
  };
315
2428
  }
316
- /** Parse markdown with md4x and render AST nodes to Telegram MarkdownV2 */
317
- function mdToTgV2(text) {
318
- return parseAST(text).nodes.map((node) => renderNode(node, true)).join("\n\n");
2429
+ /** Parse markdown with md4x and render AST nodes to Telegram HTML */
2430
+ function mdToTgHtml(text) {
2431
+ return parseAST(text).nodes.map((node) => renderNode(node)).join("\n\n");
319
2432
  }
320
- /** Collect raw text from children (for code content that must not be escaped) */
2433
+ /** Collect raw text from children recursively (for code content that must not be escaped) */
321
2434
  function collectText(children) {
322
- return children.map((c) => typeof c === "string" ? c : "").join("");
2435
+ return children.map((c) => typeof c === "string" ? c : collectText(c.slice(2))).join("");
323
2436
  }
324
- /** Render a single AST node to Telegram MarkdownV2 */
325
- function renderNode(node, topLevel = false) {
326
- if (typeof node === "string") return escTgV2(node);
2437
+ /** Render a single AST node to Telegram HTML */
2438
+ function renderNode(node) {
2439
+ if (typeof node === "string") return escHtml(node);
327
2440
  const [tag, attrs, ...children] = node;
328
2441
  const inner = () => children.map((c) => renderNode(c)).join("");
329
2442
  switch (tag) {
@@ -333,42 +2446,44 @@ function renderNode(node, topLevel = false) {
333
2446
  case "h3":
334
2447
  case "h4":
335
2448
  case "h5":
336
- case "h6": return `*${inner()}*`;
337
- case "strong": return `*${inner()}*`;
338
- case "em": return `_${inner()}_`;
339
- case "u": return `__${inner()}__`;
340
- case "del": return `~${inner()}~`;
341
- case "code":
342
- if (!topLevel) return collectText(children);
343
- return `\`${collectText(children)}\``;
2449
+ case "h6": return `<b>${inner()}</b>`;
2450
+ case "strong": return `<b>${inner()}</b>`;
2451
+ case "em": return `<i>${inner()}</i>`;
2452
+ case "u": return `<u>${inner()}</u>`;
2453
+ case "del": return `<s>${inner()}</s>`;
2454
+ case "code": return `<code>${escHtml(collectText(children))}</code>`;
344
2455
  case "pre": {
345
2456
  const codeNode = children.find((c) => typeof c !== "string" && c[0] === "code");
346
- return `\`\`\`${String(attrs.language || "")}\n${(codeNode ? collectText(codeNode.slice(2)) : inner()).replace(/\n$/, "")}\n\`\`\``;
2457
+ const lang = String(attrs.language || "");
2458
+ const raw = codeNode ? collectText(codeNode.slice(2)) : collectText(children);
2459
+ if (!raw.trim() && !lang) return "";
2460
+ return `<pre><code${lang ? ` class="language-${escHtml(lang)}"` : ""}>${escHtml(raw.replace(/\n$/, ""))}</code></pre>`;
347
2461
  }
348
- case "a": {
349
- const href = String(attrs.href || "");
350
- return `[${inner()}](${escTgUrl(href)})`;
351
- }
352
- case "img": return escTgV2(String(attrs.alt || "image"));
353
- case "blockquote": return inner().split("\n").map((l) => `>${l}`).join("\n");
2462
+ case "a": return `<a href="${escHtml(String(attrs.href || ""))}">${inner()}</a>`;
2463
+ case "img": return escHtml(String(attrs.alt || "image"));
2464
+ case "blockquote": return `<blockquote>${inner()}</blockquote>`;
354
2465
  case "ul":
355
2466
  case "ol": return children.map((c, i) => {
356
- if (typeof c === "string") return escTgV2(c);
357
- return (tag === "ol" ? `${escTgV2(`${i + 1}.`)} ` : `${escTgV2("•")} `) + renderNode(c);
2467
+ if (typeof c === "string") return escHtml(c);
2468
+ return (tag === "ol" ? `${i + 1}. ` : "• ") + renderNode(c);
358
2469
  }).filter((l) => l.trim()).join("\n");
359
2470
  case "li": return inner();
360
- case "hr": return escTgV2("---");
2471
+ case "hr": return "---";
361
2472
  case "br": return "\n";
362
2473
  default: return inner();
363
2474
  }
364
2475
  }
365
- /** Escape special characters for Telegram MarkdownV2 outside code spans */
366
- function escTgV2(s) {
367
- return s.replace(/([_*[\]()~`>#+=|{}.!\\-])/g, "\\$1");
2476
+ /** Escape HTML entities for Telegram HTML mode */
2477
+ function escHtml(s) {
2478
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2479
+ }
2480
+ /** Check if a Telegram API error is an HTML parse failure */
2481
+ function isTgParseError(err) {
2482
+ return err instanceof TelegramApiError && err.status === 400 && typeof err.response?.description === "string" && err.response.description.includes("can't parse entities");
368
2483
  }
369
- /** Escape only ) and \ inside MarkdownV2 link URLs */
370
- function escTgUrl(s) {
371
- return s.replace(/([)\\])/g, "\\$1");
2484
+ /** Check if a Telegram API error is "message is not modified" (identical content edit) */
2485
+ function isTgMessageNotModified(err) {
2486
+ return err instanceof TelegramApiError && err.status === 400 && typeof err.response?.description === "string" && err.response.description.includes("message is not modified");
372
2487
  }
373
2488
  function getRetryAfterMs(err) {
374
2489
  if (!(err instanceof TelegramApiError)) return 0;
@@ -376,6 +2491,40 @@ function getRetryAfterMs(err) {
376
2491
  if (!Number.isFinite(retryAfterSec) || retryAfterSec <= 0) return 0;
377
2492
  return retryAfterSec * 1e3;
378
2493
  }
2494
+ /** Verbose tool lines with result previews — used during streaming */
2495
+ function formatToolLines(tools) {
2496
+ return tools.map((t) => {
2497
+ const icon = t.done ? t.isError ? "❌" : "✅" : "🔧";
2498
+ const preview = t.done && t.resultPreview ? ` ${tgToolPreview(t.resultPreview)}` : "";
2499
+ return `${icon} ${escHtml(t.name)}${preview}`;
2500
+ }).join("\n");
2501
+ }
2502
+ function detailsTextLen(details) {
2503
+ let len = 0;
2504
+ for (const d of details) len += d.type === "tool" ? 1 : d.text.length;
2505
+ return len;
2506
+ }
2507
+ function tgToolPreview(raw) {
2508
+ const firstLine = extractToolResultText(raw, 200).split("\n")[0] || "";
2509
+ return escHtml(firstLine.length > 80 ? firstLine.slice(0, 79) + "…" : firstLine);
2510
+ }
2511
+ /** Strip <internal>/<thinking> tags from streamed text (handles partial/unclosed tags mid-stream) */
2512
+ function stripInternalTags(text) {
2513
+ let s = text.replace(/<(internal|thinking)>[\s\S]*?<\/\1>/g, "");
2514
+ s = s.replace(/<(internal|thinking)>[\s\S]*$/, "");
2515
+ s = s.replace(/<(?:internal|thinking|intern|inter|inte|int|in|i|thinkin|thinki|think|thin|thi|th|t)?$/i, "");
2516
+ return s;
2517
+ }
2518
+ /** Strip <internal>/<thinking>/tool tags for plain-text fallback — keeps inner content */
2519
+ function stripHtmlTags(html) {
2520
+ return html.replace(/<[^>]*>/g, "");
2521
+ }
2522
+ function stripThinkingTags(text) {
2523
+ let s = text.replace(/<\/?(internal|thinking)>/g, "");
2524
+ s = s.replace(/<details><summary>thinking<\/summary>([\s\S]*?)<\/details>/g, "$1");
2525
+ s = s.replace(/<tool\s+name="([^"]+)"\s+status="[^"]*"(?:\s+preview="[^"]*")?\s*\/>/g, "[$1]");
2526
+ return s;
2527
+ }
379
2528
  function randomJitter(maxAbs) {
380
2529
  return Math.round((Math.random() * 2 - 1) * maxAbs);
381
2530
  }
@@ -449,16 +2598,18 @@ var GroupQueue = class {
449
2598
  this.drain();
450
2599
  }
451
2600
  drain() {
2601
+ const deferred = [];
452
2602
  while (this.active.size < config.get("maxConcurrentAgents") && this.queue.length > 0) {
453
2603
  const entry = this.queue.shift();
454
2604
  if (!entry) break;
455
2605
  if (this.active.has(entry.chatJid)) {
456
- this.queue.push(entry);
457
- break;
2606
+ deferred.push(entry);
2607
+ continue;
458
2608
  }
459
2609
  this.active.add(entry.chatJid);
460
2610
  this.processEntry(entry);
461
2611
  }
2612
+ this.queue.push(...deferred);
462
2613
  }
463
2614
  async processEntry(entry) {
464
2615
  try {
@@ -500,7 +2651,14 @@ function escapeXml(s) {
500
2651
  }
501
2652
  /** Format messages into XML for the agent prompt */
502
2653
  function formatMessages(messages) {
503
- return `<messages>\n${messages.map((m) => `<message sender="${escapeXml(m.sender_name)}" time="${m.timestamp}">${escapeXml(m.content)}</message>`).join("\n")}\n</messages>`;
2654
+ return `<messages>\n${messages.map((m) => {
2655
+ let body = escapeXml(m.content);
2656
+ if (m.meta?.attachments?.length) body += m.meta.attachments.map((a) => {
2657
+ const fileAttr = a.mimeType.startsWith("image/") ? "" : ` file="downloads/${a.path.split("/").slice(1).join("/")}"`;
2658
+ return `\n<attachment name="${escapeXml(a.name)}" type="${a.mimeType}"${fileAttr}/>`;
2659
+ }).join("");
2660
+ return `<message sender="${escapeXml(m.sender_name)}" time="${m.timestamp}">${body}</message>`;
2661
+ }).join("\n")}\n</messages>`;
504
2662
  }
505
2663
  /** Format outbound text */
506
2664
  function formatOutbound(rawText) {
@@ -510,7 +2668,9 @@ function formatOutbound(rawText) {
510
2668
  function needsProcessing(group, messages) {
511
2669
  if (group.folder === "main") return true;
512
2670
  if (group.requiresTrigger === false) return true;
513
- return messages.some((m) => config.triggerPattern().test(m.content.trim()));
2671
+ const trigger = group.trigger || `@${config.get("assistantName")}`;
2672
+ const pattern = new RegExp(`^${trigger.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i");
2673
+ return messages.some((m) => pattern.test(m.content.trim()));
514
2674
  }
515
2675
  /**
516
2676
  * Task scheduler — runs scheduled tasks (cron/interval/once) using the pi agent.
@@ -722,7 +2882,7 @@ var PiClawServer = class {
722
2882
  folder: group.folder
723
2883
  }, "Group unregistered");
724
2884
  }
725
- async sendBotMessage(chatJid, text) {
2885
+ async sendBotMessage(chatJid, text, meta) {
726
2886
  const now = (/* @__PURE__ */ new Date()).toISOString();
727
2887
  await storeChatMetadata(chatJid, now);
728
2888
  await storeMessage({
@@ -733,8 +2893,10 @@ var PiClawServer = class {
733
2893
  content: text,
734
2894
  timestamp: now,
735
2895
  is_from_me: true,
736
- is_bot_message: true
2896
+ is_bot_message: true,
2897
+ meta
737
2898
  });
2899
+ deviceBus.emitDeviceEvent("chats");
738
2900
  for (const ch of this.channels) if (ch.ownsJid(chatJid)) {
739
2901
  ch.sendMessage(chatJid, text).catch((err) => {
740
2902
  logger.error({
@@ -749,7 +2911,7 @@ var PiClawServer = class {
749
2911
  /** Full cleanup: kill agent, remove session/tasks/chat/group data */
750
2912
  async cleanupChat(jid) {
751
2913
  const group = this.registeredGroups[jid];
752
- this.pi.kill(jid);
2914
+ await this.pi.kill(jid);
753
2915
  if (group) {
754
2916
  await removeSession(group.folder);
755
2917
  const sessionDir = path$1.join(SESSIONS_DIR, group.folder);
@@ -785,8 +2947,18 @@ var PiClawServer = class {
785
2947
  return new TelegramChannel({
786
2948
  onMessage: async (jid, msg) => {
787
2949
  await storeChatMetadata(jid, msg.timestamp, msg.sender_name, "telegram");
2950
+ const group = this.registeredGroups[jid];
2951
+ if (group) {
2952
+ const trimmed = msg.content.trim();
2953
+ if (tryPriorityCommand({
2954
+ chatJid: jid,
2955
+ group,
2956
+ server: this
2957
+ }, trimmed)) return;
2958
+ }
788
2959
  await storeMessage(msg);
789
- if (this.registeredGroups[jid]) this.queue.enqueueMessageCheck(jid);
2960
+ if (group) if (this.pi.sendMessage(jid, formatMessages([msg]))) await this.advanceCursor(jid, msg.timestamp);
2961
+ else this.queue.enqueueMessageCheck(jid);
790
2962
  else await this.notifyUnregistered(jid);
791
2963
  },
792
2964
  onChatMetadata: async (jid, ts, name, channel, isGroup) => {
@@ -870,18 +3042,22 @@ var PiClawServer = class {
870
3042
  await setRouterState("last_timestamp", this.lastTimestamp);
871
3043
  await setRouterState("last_agent_timestamp", JSON.stringify(this.lastAgentTimestamp));
872
3044
  }
3045
+ /** Advance the agent cursor for a chat — marks messages up to `timestamp` as processed. */
3046
+ async advanceCursor(chatJid, timestamp) {
3047
+ this.lastAgentTimestamp[chatJid] = timestamp;
3048
+ await this.saveState();
3049
+ }
873
3050
  async handleCommands(chatJid, messages) {
874
3051
  const group = this.registeredGroups[chatJid];
875
3052
  if (!group) return messages;
876
3053
  const remaining = [];
877
3054
  for (const msg of messages) {
878
3055
  const text = msg.content.trim();
879
- const ctx = {
3056
+ if (!await tryCommand({
880
3057
  chatJid,
881
3058
  group,
882
3059
  server: this
883
- };
884
- if (!(tryBashCommand(ctx, text) || await tryCommand(ctx, text))) remaining.push(msg);
3060
+ }, text)) remaining.push(msg);
885
3061
  }
886
3062
  return remaining;
887
3063
  }
@@ -900,6 +3076,18 @@ var PiClawServer = class {
900
3076
  if (!needsProcessing(group, agentMessages)) return true;
901
3077
  const prompt = formatMessages(agentMessages);
902
3078
  const lastMessageTimestamp = missedMessages[missedMessages.length - 1].timestamp;
3079
+ const images = [];
3080
+ for (const msg of agentMessages) if (msg.meta?.attachments) {
3081
+ for (const att of msg.meta.attachments) if (isImageMimeType(att.mimeType)) try {
3082
+ images.push({
3083
+ type: "image",
3084
+ data: readAttachmentBase64(group.folder, att),
3085
+ mimeType: att.mimeType
3086
+ });
3087
+ } catch {
3088
+ logger.warn({ attachment: att.path }, "Failed to read attachment");
3089
+ }
3090
+ }
903
3091
  if (!resolveGroupModel(group)) {
904
3092
  logger.warn({ group: group.name }, "No model available, notifying user");
905
3093
  await this.sendBotMessage(chatJid, "No AI model is currently available. Please configure a provider API key or log in via the admin panel.");
@@ -917,6 +3105,7 @@ var PiClawServer = class {
917
3105
  try {
918
3106
  await this.pi.run(group, {
919
3107
  prompt,
3108
+ images: images.length ? images : void 0,
920
3109
  sessionId: this.sessions[group.folder],
921
3110
  groupFolder: group.folder,
922
3111
  chatJid,
@@ -980,21 +3169,12 @@ var PiClawServer = class {
980
3169
  for (const [chatJid, groupMessages] of byGroup) {
981
3170
  const group = this.registeredGroups[chatJid];
982
3171
  if (!group) continue;
983
- for (const msg of groupMessages) {
984
- const ctx = {
985
- chatJid,
986
- group,
987
- server: this
988
- };
989
- tryBashCommand(ctx, msg.content.trim());
990
- tryPriorityCommand(ctx, msg.content.trim());
991
- }
992
3172
  if (!needsProcessing(group, groupMessages)) continue;
993
3173
  this.queue.enqueueMessageCheck(chatJid);
994
3174
  }
995
3175
  }
996
3176
  } catch (err) {
997
- logger.error({ err }, "Error in message loop");
3177
+ logger.error({ err: err?.stack || err?.message || err }, "Error in message loop");
998
3178
  }
999
3179
  await new Promise((resolve) => {
1000
3180
  const timer = setTimeout(resolve, config.get("pollInterval"));
@@ -1028,4 +3208,4 @@ const startTime = globalThis.__piclaw_start_time__ || performance.now();
1028
3208
  const server = new PiClawServer();
1029
3209
  await server.start();
1030
3210
  var server_default = () => {};
1031
- export { server_default as n, startTime as r, server as t };
3211
+ export { commandDescriptions as a, authStorage as c, getSystemInfo as d, formatMessages as i, getFilteredModels as l, server_default as n, tryPriorityCommand as o, startTime as r, getPiSdkStatus as s, server as t, modelRegistry as u };