piclaw 0.0.20 → 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 (245) 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-D-Hc5HbQ.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/_chunks/app.mjs +138 -104
  8. package/.output/server/_chunks/config.mjs +4 -0
  9. package/.output/server/_chunks/dummy.mjs +1 -1
  10. package/.output/server/_chunks/logger.mjs +1 -1
  11. package/.output/server/_chunks/notes.mjs +1 -3
  12. package/.output/server/_chunks/renderer-template.mjs +1 -1
  13. package/.output/server/_chunks/sandbox.mjs +217 -0
  14. package/.output/server/_chunks/server.mjs +411 -291
  15. package/.output/server/_chunks/terminal.mjs +47 -8
  16. package/.output/server/_chunks/virtual.mjs +192 -54
  17. package/.output/server/_id_.delete.mjs +5 -2
  18. package/.output/server/_id_2.delete.mjs +8 -0
  19. package/.output/server/_jid_.delete.mjs +0 -1
  20. package/.output/server/_jid_.patch.mjs +21 -3
  21. package/.output/server/_jid_2.delete.mjs +0 -1
  22. package/.output/server/_libs/@acemir/cssom+[...].mjs +2269 -1137
  23. package/.output/server/_libs/@google/genai.mjs +348 -284
  24. package/.output/server/_libs/@mariozechner/pi-agent-core+[...].mjs +381 -2073
  25. package/.output/server/_libs/@mariozechner/pi-coding-agent+[...].mjs +236 -136
  26. package/.output/server/_libs/_.mjs +3 -2
  27. package/.output/server/_libs/_10.mjs +2 -4
  28. package/.output/server/_libs/_11.mjs +2 -4
  29. package/.output/server/_libs/_12.mjs +2 -3
  30. package/.output/server/_libs/_13.mjs +2 -3
  31. package/.output/server/_libs/_14.mjs +2 -4
  32. package/.output/server/_libs/_15.mjs +2 -4
  33. package/.output/server/_libs/_16.mjs +2 -3
  34. package/.output/server/_libs/_17.mjs +2 -4
  35. package/.output/server/_libs/_18.mjs +2 -2
  36. package/.output/server/_libs/_19.mjs +2 -2
  37. package/.output/server/_libs/_2.mjs +3 -3
  38. package/.output/server/_libs/_20.mjs +2 -2
  39. package/.output/server/_libs/_21.mjs +2 -2
  40. package/.output/server/_libs/_22.mjs +2 -2
  41. package/.output/server/_libs/_23.mjs +2 -2
  42. package/.output/server/_libs/_24.mjs +2 -2
  43. package/.output/server/_libs/_25.mjs +2 -2
  44. package/.output/server/_libs/_26.mjs +2 -2
  45. package/.output/server/_libs/_27.mjs +2 -2
  46. package/.output/server/_libs/_28.mjs +2 -2
  47. package/.output/server/_libs/_29.mjs +2 -2
  48. package/.output/server/_libs/_3.mjs +3 -3
  49. package/.output/server/_libs/_30.mjs +2 -2
  50. package/.output/server/_libs/_31.mjs +2 -2
  51. package/.output/server/_libs/_32.mjs +2 -2
  52. package/.output/server/_libs/_33.mjs +2 -2
  53. package/.output/server/_libs/_34.mjs +2 -2
  54. package/.output/server/_libs/_35.mjs +2 -2
  55. package/.output/server/_libs/_36.mjs +2 -2
  56. package/.output/server/_libs/_37.mjs +2 -2
  57. package/.output/server/_libs/_38.mjs +2 -2
  58. package/.output/server/_libs/_39.mjs +2 -2
  59. package/.output/server/_libs/_4.mjs +4 -3
  60. package/.output/server/_libs/_40.mjs +2 -2
  61. package/.output/server/_libs/_41.mjs +2 -2
  62. package/.output/server/_libs/_42.mjs +2 -2
  63. package/.output/server/_libs/_43.mjs +2 -2
  64. package/.output/server/_libs/_44.mjs +2 -2
  65. package/.output/server/_libs/_45.mjs +2 -2
  66. package/.output/server/_libs/_46.mjs +2 -2
  67. package/.output/server/_libs/_47.mjs +2 -2
  68. package/.output/server/_libs/_48.mjs +2 -2
  69. package/.output/server/_libs/_49.mjs +2 -2
  70. package/.output/server/_libs/_5.mjs +2 -3
  71. package/.output/server/_libs/_50.mjs +2 -2
  72. package/.output/server/_libs/_51.mjs +2 -2
  73. package/.output/server/_libs/_52.mjs +2 -2
  74. package/.output/server/_libs/_53.mjs +2 -2
  75. package/.output/server/_libs/_54.mjs +2 -2
  76. package/.output/server/_libs/_55.mjs +2 -2
  77. package/.output/server/_libs/_56.mjs +2 -2
  78. package/.output/server/_libs/_57.mjs +2 -2
  79. package/.output/server/_libs/_58.mjs +2 -2
  80. package/.output/server/_libs/_59.mjs +2 -2
  81. package/.output/server/_libs/_6.mjs +2 -3
  82. package/.output/server/_libs/_60.mjs +2 -2
  83. package/.output/server/_libs/_61.mjs +2 -2
  84. package/.output/server/_libs/_62.mjs +2 -2
  85. package/.output/server/_libs/_63.mjs +2 -2
  86. package/.output/server/_libs/_64.mjs +2 -2
  87. package/.output/server/_libs/_65.mjs +2 -2
  88. package/.output/server/_libs/_66.mjs +2 -2
  89. package/.output/server/_libs/_67.mjs +2 -2
  90. package/.output/server/_libs/_68.mjs +2 -2
  91. package/.output/server/_libs/_69.mjs +2 -2
  92. package/.output/server/_libs/_7.mjs +2 -5
  93. package/.output/server/_libs/_70.mjs +2 -2
  94. package/.output/server/_libs/_71.mjs +2 -2
  95. package/.output/server/_libs/_72.mjs +2 -2
  96. package/.output/server/_libs/_73.mjs +2 -2
  97. package/.output/server/_libs/_74.mjs +2 -2
  98. package/.output/server/_libs/_75.mjs +2 -2
  99. package/.output/server/_libs/_76.mjs +2 -2
  100. package/.output/server/_libs/_77.mjs +2 -2
  101. package/.output/server/_libs/_78.mjs +2 -2
  102. package/.output/server/_libs/_79.mjs +2 -2
  103. package/.output/server/_libs/_8.mjs +2 -3
  104. package/.output/server/_libs/_80.mjs +2 -2
  105. package/.output/server/_libs/_81.mjs +2 -2
  106. package/.output/server/_libs/_82.mjs +2 -2
  107. package/.output/server/_libs/_83.mjs +2 -2
  108. package/.output/server/_libs/_84.mjs +2 -2
  109. package/.output/server/_libs/_85.mjs +2 -2
  110. package/.output/server/_libs/_86.mjs +2 -2
  111. package/.output/server/_libs/_87.mjs +2 -2
  112. package/.output/server/_libs/_88.mjs +2 -2
  113. package/.output/server/_libs/_89.mjs +2 -2
  114. package/.output/server/_libs/_9.mjs +2 -4
  115. package/.output/server/_libs/_90.mjs +5 -2
  116. package/.output/server/_libs/_91.mjs +3 -2
  117. package/.output/server/_libs/_92.mjs +2 -2
  118. package/.output/server/_libs/_93.mjs +2 -2
  119. package/.output/server/_libs/_94.mjs +2 -2
  120. package/.output/server/_libs/agent-base.mjs +1 -1
  121. package/.output/server/_libs/cheerio+[...].mjs +1 -1
  122. package/.output/server/_libs/data-uri-to-buffer.mjs +2 -67
  123. package/.output/server/_libs/data-urls+[...].mjs +1 -1
  124. package/.output/server/_libs/diff.mjs +1 -1
  125. package/.output/server/_libs/exodus__bytes.mjs +99 -81
  126. package/.output/server/_libs/fetch-blob+node-domexception.mjs +1 -1
  127. package/.output/server/_libs/h3+rou3+srvx.mjs +34 -4
  128. package/.output/server/_libs/html-encoding-sniffer.mjs +1 -1
  129. package/.output/server/_libs/https-proxy-agent.mjs +2 -2
  130. package/.output/server/_libs/jsdom.mjs +1 -1
  131. package/.output/server/_libs/just-bash+[...].mjs +4676 -3916
  132. package/.output/server/_libs/mariozechner__jiti.mjs +1 -1
  133. package/.output/server/_libs/mariozechner__pi-ai.mjs +1472 -0
  134. package/.output/server/_libs/md4x.mjs +1 -1
  135. package/.output/server/_libs/node-fetch.mjs +14 -14
  136. package/.output/server/_libs/node-liblzma.mjs +1 -1
  137. package/.output/server/_libs/silvia-odwyer__photon-node.mjs +1 -1
  138. package/.output/server/_routes/api/auth/status.mjs +25 -6
  139. package/.output/server/_routes/api/config2.mjs +2 -0
  140. package/.output/server/_routes/api/files/groups.mjs +0 -1
  141. package/.output/server/_routes/api/groups.mjs +4 -2
  142. package/.output/server/_routes/api/groups2.mjs +14 -5
  143. package/.output/server/_routes/api/health.mjs +0 -1
  144. package/.output/server/_routes/api/pi/apikey.mjs +0 -1
  145. package/.output/server/_routes/api/pi/apikey_providers.mjs +0 -1
  146. package/.output/server/_routes/api/pi/commands.mjs +1 -2
  147. package/.output/server/_routes/api/pi/login/events.mjs +0 -1
  148. package/.output/server/_routes/api/pi/login/respond.mjs +0 -1
  149. package/.output/server/_routes/api/pi/login.mjs +0 -1
  150. package/.output/server/_routes/api/pi/logout.mjs +0 -1
  151. package/.output/server/_routes/api/pi/models.mjs +0 -1
  152. package/.output/server/_routes/api/pi/status.mjs +0 -1
  153. package/.output/server/_routes/api/sandbox.mjs +26 -0
  154. package/.output/server/_routes/api/sandbox2.mjs +17 -0
  155. package/.output/server/_routes/api/send.mjs +12 -12
  156. package/.output/server/_routes/api/status.mjs +0 -1
  157. package/.output/server/_routes/api/stop.mjs +0 -1
  158. package/.output/server/_routes/api/tasks2.mjs +0 -1
  159. package/.output/server/_routes/api/telegram/setup.mjs +0 -1
  160. package/.output/server/_routes/api/telegram/status.mjs +0 -1
  161. package/.output/server/_routes/api/terminal2.mjs +2 -1
  162. package/.output/server/_routes/api/tunnel/setup.mjs +0 -1
  163. package/.output/server/_runtime.mjs +1 -2
  164. package/.output/server/index.mjs +1 -1
  165. package/.output/server/node_modules/amdefine/amdefine.js +301 -0
  166. package/.output/server/node_modules/amdefine/package.json +16 -0
  167. package/.output/server/node_modules/compressjs/lib/BWT.js +420 -0
  168. package/.output/server/node_modules/compressjs/lib/BWTC.js +234 -0
  169. package/.output/server/node_modules/compressjs/lib/BitStream.js +108 -0
  170. package/.output/server/node_modules/compressjs/lib/Bzip2.js +936 -0
  171. package/.output/server/node_modules/compressjs/lib/CRC32.js +105 -0
  172. package/.output/server/node_modules/compressjs/lib/Context1Model.js +56 -0
  173. package/.output/server/node_modules/compressjs/lib/DefSumModel.js +152 -0
  174. package/.output/server/node_modules/compressjs/lib/DeflateDistanceModel.js +55 -0
  175. package/.output/server/node_modules/compressjs/lib/Dmc.js +197 -0
  176. package/.output/server/node_modules/compressjs/lib/DummyRangeCoder.js +81 -0
  177. package/.output/server/node_modules/compressjs/lib/FenwickModel.js +194 -0
  178. package/.output/server/node_modules/compressjs/lib/Huffman.js +514 -0
  179. package/.output/server/node_modules/compressjs/lib/HuffmanAllocator.js +227 -0
  180. package/.output/server/node_modules/compressjs/lib/LogDistanceModel.js +46 -0
  181. package/.output/server/node_modules/compressjs/lib/Lzjb.js +300 -0
  182. package/.output/server/node_modules/compressjs/lib/LzjbR.js +241 -0
  183. package/.output/server/node_modules/compressjs/lib/Lzp3.js +273 -0
  184. package/.output/server/node_modules/compressjs/lib/MTFModel.js +208 -0
  185. package/.output/server/node_modules/compressjs/lib/NoModel.js +46 -0
  186. package/.output/server/node_modules/compressjs/lib/PPM.js +343 -0
  187. package/.output/server/node_modules/compressjs/lib/RangeCoder.js +238 -0
  188. package/.output/server/node_modules/compressjs/lib/Simple.js +111 -0
  189. package/.output/server/node_modules/compressjs/lib/Stream.js +53 -0
  190. package/.output/server/node_modules/compressjs/lib/Util.js +324 -0
  191. package/.output/server/node_modules/compressjs/lib/freeze.js +14 -0
  192. package/.output/server/node_modules/compressjs/main.js +29 -0
  193. package/.output/server/node_modules/compressjs/package.json +35 -0
  194. package/.output/server/package.json +2 -1
  195. package/README.md +10 -1
  196. package/lib/index.d.mts +1 -0
  197. package/lib/index.mjs +1 -0
  198. package/lib/piclaw.mjs +100 -0
  199. package/lib/utils.mjs +96 -0
  200. package/package.json +16 -11
  201. package/.output/public/assets/defult-DtwgaiMA.js +0 -1
  202. package/.output/public/assets/index-B5n0eraW.css +0 -1
  203. package/.output/public/assets/index-DUbn6fuj.js +0 -205
  204. package/.output/server/_libs/@aws-crypto/crc32+[...].mjs +0 -299
  205. package/.output/server/_libs/@aws-sdk/client-bedrock-runtime+[...].mjs +0 -17828
  206. package/.output/server/_libs/@aws-sdk/credential-provider-http+[...].mjs +0 -122
  207. package/.output/server/_libs/@aws-sdk/credential-provider-ini+[...].mjs +0 -417
  208. package/.output/server/_libs/@aws-sdk/credential-provider-process+[...].mjs +0 -54
  209. package/.output/server/_libs/@aws-sdk/credential-provider-sso+[...].mjs +0 -1151
  210. package/.output/server/_libs/@aws-sdk/credential-provider-web-identity+[...].mjs +0 -50
  211. package/.output/server/_libs/@smithy/credential-provider-imds+[...].mjs +0 -369
  212. package/.output/server/_libs/@tootallnate/quickjs-emscripten+[...].mjs +0 -3011
  213. package/.output/server/_libs/_100.mjs +0 -2
  214. package/.output/server/_libs/_101.mjs +0 -2
  215. package/.output/server/_libs/_102.mjs +0 -2
  216. package/.output/server/_libs/_103.mjs +0 -5
  217. package/.output/server/_libs/_104.mjs +0 -3
  218. package/.output/server/_libs/_105.mjs +0 -2
  219. package/.output/server/_libs/_106.mjs +0 -3
  220. package/.output/server/_libs/_107.mjs +0 -2
  221. package/.output/server/_libs/_108.mjs +0 -2
  222. package/.output/server/_libs/_95.mjs +0 -2
  223. package/.output/server/_libs/_96.mjs +0 -2
  224. package/.output/server/_libs/_97.mjs +0 -2
  225. package/.output/server/_libs/_98.mjs +0 -2
  226. package/.output/server/_libs/_99.mjs +0 -2
  227. package/.output/server/_libs/amdefine.mjs +0 -188
  228. package/.output/server/_libs/ast-types.mjs +0 -2270
  229. package/.output/server/_libs/aws-sdk__nested-clients.mjs +0 -3141
  230. package/.output/server/_libs/basic-ftp.mjs +0 -1906
  231. package/.output/server/_libs/compressjs.mjs +0 -50
  232. package/.output/server/_libs/degenerator+[...].mjs +0 -9964
  233. package/.output/server/_libs/get-uri.mjs +0 -413
  234. package/.output/server/_libs/http-proxy-agent.mjs +0 -123
  235. package/.output/server/_libs/ip-address.mjs +0 -1423
  236. package/.output/server/_libs/lru-cache.mjs +0 -732
  237. package/.output/server/_libs/netmask.mjs +0 -139
  238. package/.output/server/_libs/pac-proxy-agent+[...].mjs +0 -3104
  239. package/.output/server/_libs/proxy-agent+proxy-from-env.mjs +0 -204
  240. package/.output/server/_libs/smithy__core.mjs +0 -192
  241. package/.output/server/node_modules/tslib/modules/index.js +0 -70
  242. package/.output/server/node_modules/tslib/modules/package.json +0 -3
  243. package/.output/server/node_modules/tslib/package.json +0 -47
  244. package/.output/server/node_modules/tslib/tslib.js +0 -484
  245. package/bin/piclaw.mjs +0 -195
@@ -1,16 +1,17 @@
1
- import { r as __exportAll, s as __toESM } from "../_runtime.mjs";
1
+ import { o as __toESM, r as __exportAll } from "../_runtime.mjs";
2
2
  import { i as SESSIONS_DIR, n as GROUPS_DIR, r as MAIN_GROUP_FOLDER, s as config } from "./config.mjs";
3
3
  import { t as createLogger } from "./logger.mjs";
4
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 createCodingTools, i as AuthStorage, n as DefaultResourceLoader, o as SessionManager, r as ModelRegistry, s as SettingsManager, t as createAgentSession } from "../_libs/@mariozechner/pi-coding-agent+[...].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";
6
6
  import { n as parseAST, t as init } from "../_libs/md4x.mjs";
7
7
  import { t as streamBus } from "./stream.mjs";
8
- import { b as Type } from "../_libs/@mariozechner/pi-agent-core+[...].mjs";
8
+ import { h as Type } from "../_libs/@mariozechner/pi-agent-core+[...].mjs";
9
9
  import { t as deviceBus } from "./device-bus.mjs";
10
10
  import { n as setBrowserState, t as getBrowserState } from "./browser.mjs";
11
11
  import { i as writeNote, n as listNotes, r as readNote, t as deleteNote } from "./notes.mjs";
12
12
  import { n as getNtfyConfig, r as sendNtfyNotification } from "./ntfy.mjs";
13
13
  import { t as terminalManager } from "./terminal.mjs";
14
+ import { t as sandboxManager } from "./sandbox.mjs";
14
15
  import { a as saveAttachment, i as readAttachmentBase64, n as getMimeType, r as isImageMimeType } from "./uploads.mjs";
15
16
  import { i as tunnelManager, n as getTunnelConfig, r as setTunnelConfig, t as clearTunnelConfig } from "./tunnel.mjs";
16
17
  import fs from "fs";
@@ -20,7 +21,7 @@ import path$1 from "node:path";
20
21
  import crypto from "node:crypto";
21
22
  import fs$2 from "node:fs/promises";
22
23
  import os from "node:os";
23
- import { exec } from "node:child_process";
24
+ import "node:child_process";
24
25
  function appendGroupEvent(folder, event) {
25
26
  try {
26
27
  const logsDir = path$1.join(GROUPS_DIR, folder, "logs");
@@ -32,6 +33,32 @@ function appendGroupEvent(folder, event) {
32
33
  fs$1.appendFileSync(path$1.join(logsDir, "events.jsonl"), line, "utf-8");
33
34
  } catch {}
34
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
+ }
35
62
  var availableCommands;
36
63
  async function getSystemInfo() {
37
64
  const info = {
@@ -130,7 +157,8 @@ function sendPrompt(managed, group, input, onOutput) {
130
157
  let thinkingText = "";
131
158
  let resolved = false;
132
159
  let flushedTextLength = 0;
133
- const toolsList = [];
160
+ /** Chronologically ordered raw items (thinking text + tool markers) flushed as single toggle */
161
+ const pendingItems = [];
134
162
  let outputChain = Promise.resolve();
135
163
  const timeout = setTimeout(() => {
136
164
  if (resolved) return;
@@ -150,8 +178,7 @@ function sendPrompt(managed, group, input, onOutput) {
150
178
  status: "error",
151
179
  result: fullText || null,
152
180
  newSessionId: session.sessionId,
153
- error: "Agent timeout",
154
- tools: toolsList.length ? toolsList : void 0
181
+ error: "Agent timeout"
155
182
  });
156
183
  }, config.get("agentTimeout"));
157
184
  const finish = (status, error) => {
@@ -165,8 +192,7 @@ function sendPrompt(managed, group, input, onOutput) {
165
192
  status,
166
193
  result: fullText || null,
167
194
  newSessionId: session.sessionId,
168
- error,
169
- tools: toolsList.length ? toolsList : void 0
195
+ error
170
196
  });
171
197
  });
172
198
  };
@@ -182,13 +208,19 @@ function sendPrompt(managed, group, input, onOutput) {
182
208
  const unflushed = fullText.slice(flushedTextLength);
183
209
  const jid = input.chatJid;
184
210
  if (onOutput) {
185
- const partial = unflushed ? thinkingText ? `<thinking>${thinkingText}</thinking>\n\n${unflushed}` : unflushed : null;
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;
186
219
  outputChain = outputChain.then(async () => {
187
220
  await onOutput({
188
221
  status: "success",
189
222
  result: partial ? partial + "\n\n_(stopped)_" : "_(stopped)_",
190
- newSessionId: session.sessionId,
191
- tools: toolsList.length ? toolsList : void 0
223
+ newSessionId: session.sessionId
192
224
  });
193
225
  if (partial) streamBus.emit({
194
226
  type: "text_end",
@@ -226,20 +258,24 @@ function sendPrompt(managed, group, input, onOutput) {
226
258
  }
227
259
  if (ae.type === "text_end" && "content" in ae && onOutput) {
228
260
  const text = ae.content;
229
- const stored = thinkingText ? `<thinking>${thinkingText}</thinking>\n\n${text}` : text;
230
- thinkingText = "";
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");
231
269
  appendGroupEvent(group.folder, {
232
270
  type: "text_end",
233
271
  length: stored.length,
234
272
  preview: stored.slice(0, 100)
235
273
  });
236
- const snapshotTools = toolsList.length ? [...toolsList] : void 0;
237
274
  outputChain = outputChain.then(async () => {
238
275
  await onOutput({
239
276
  status: "success",
240
277
  result: stored,
241
- newSessionId: session.sessionId,
242
- tools: snapshotTools
278
+ newSessionId: session.sessionId
243
279
  });
244
280
  flushedTextLength = fullText.length;
245
281
  streamBus.emit({
@@ -257,11 +293,17 @@ function sendPrompt(managed, group, input, onOutput) {
257
293
  delta: ae.delta
258
294
  });
259
295
  }
260
- if (ae.type === "thinking_end" && "content" in ae) streamBus.emit({
261
- type: "thinking_end",
262
- chatJid: jid,
263
- content: ae.content
264
- });
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
+ }
265
307
  break;
266
308
  }
267
309
  case "agent_end":
@@ -308,7 +350,7 @@ function sendPrompt(managed, group, input, onOutput) {
308
350
  isError: event.isError,
309
351
  resultPreview: previewForLog(event.result)
310
352
  };
311
- if (event.isError) logger$6.warn(payload, "Tool execution error");
353
+ if (event.isError) logger$6.debug(payload, "Tool execution error");
312
354
  else logger$6.debug(payload, "Tool execution finished");
313
355
  appendGroupEvent(group.folder, {
314
356
  type: "tool_end",
@@ -317,12 +359,8 @@ function sendPrompt(managed, group, input, onOutput) {
317
359
  isError: event.isError,
318
360
  resultPreview: previewForLog(event.result, 200)
319
361
  });
320
- const cleanPreview = cleanToolPreview(event.result) || void 0;
321
- toolsList.push({
322
- name: event.toolName,
323
- isError: event.isError || void 0,
324
- resultPreview: cleanPreview
325
- });
362
+ const cleanPreview = extractToolResultText(event.result) || void 0;
363
+ pendingItems.push(toToolMarker(event.toolName, event.isError, cleanPreview));
326
364
  streamBus.emit({
327
365
  type: "tool_end",
328
366
  chatJid: jid,
@@ -374,7 +412,7 @@ Execution rules:
374
412
  - Use bash for operations (searching, building, testing, git-aware inspection).
375
413
  - Use terminal for long running processes such as dev server or tmux.
376
414
  - Do not use cat/sed/awk to read files use read tool instead.
377
- - Use read to inspect files before editing them.
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.
378
416
  - Open browser tabs only when explicitly asked.
379
417
  - Prefer edit for targeted changes; use write for new files or full rewrites.
380
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.
@@ -393,34 +431,15 @@ async function buildSystemPrompt(ctx) {
393
431
 
394
432
  ${baseSystemPrompt}
395
433
 
396
- Current date and time: ${(/* @__PURE__ */ new Date()).toLocaleString()}
397
434
  Current model name: ${ctx.modelName}
398
- Current working directory: ${ctx.dir}`;
435
+ `;
399
436
  const envInfo = await getSystemInfoText().catch((err) => {
400
- logger$6.warn({ err }, "Failed to get system info");
437
+ logger$6.debug({ err }, "Failed to get system info");
401
438
  return null;
402
439
  });
403
440
  if (envInfo) prompt += `\n\nEnvironment:\n${envInfo}`;
404
441
  return prompt;
405
442
  }
406
- var RE_ANSI = /.\x08|\x1b\[[0-9;]*[A-Za-z]|\x1b[>?]?[0-9]*[A-Za-z]|[\x00-\x08\x0b\x0c\x0e-\x1f]/g;
407
- /** Extract readable text from a tool result and strip ANSI/control chars */
408
- function cleanToolPreview(value, maxLength = 200) {
409
- let text = "";
410
- if (typeof value === "string") text = value;
411
- else if (value && typeof value === "object") {
412
- const obj = value;
413
- if (Array.isArray(obj.content)) {
414
- const first = obj.content[0];
415
- if (first?.text && typeof first.text === "string") text = first.text;
416
- else text = JSON.stringify(value);
417
- } else text = JSON.stringify(value);
418
- }
419
- if (!text) return "";
420
- text = text.replace(RE_ANSI, "").trim();
421
- if (text.length > maxLength) return text.slice(0, maxLength) + "…";
422
- return text;
423
- }
424
443
  function previewForLog(value, maxLength = 1200) {
425
444
  try {
426
445
  const text = typeof value === "string" ? value : JSON.stringify(value);
@@ -431,6 +450,14 @@ function previewForLog(value, maxLength = 1200) {
431
450
  return "[unserializable value]";
432
451
  }
433
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
+ }
434
461
  const browserTool = {
435
462
  name: "browser",
436
463
  label: "Browser",
@@ -631,14 +658,14 @@ const notesTool = {
631
658
  "Actions: list (show notes, optionally filtered by category),",
632
659
  "read (get note content), write (create/overwrite a note), delete (remove a note).",
633
660
  "Notes are referenced as \"category/id\" (e.g. \"ideas/project-plan\").",
634
- "Virtual ~agents notes (agent memory per session) are read-only."
661
+ "Virtual ~agents notes map to per-session AGENTS.md files."
635
662
  ].join(" "),
636
663
  promptSnippet: "notes: Read, write, list, and delete markdown notes organized by category",
637
664
  promptGuidelines: [
638
665
  "Use notes to persist structured information across sessions — plans, references, logs, etc.",
639
666
  "Note refs use \"category/id\" format, e.g. \"tasks/backlog\" or \"docs/api-spec\".",
640
667
  "List notes first to see what exists before reading or writing.",
641
- "Virtual ~agents/ notes map to per-session AGENTS.md files and cannot be deleted."
668
+ "Virtual ~agents/ notes map to per-session AGENTS.md files."
642
669
  ],
643
670
  parameters: Type.Object({
644
671
  action: Type.Union([
@@ -679,6 +706,7 @@ function handleWrite$1(p) {
679
706
  if (p.content == null) return err$2("content is required for write");
680
707
  try {
681
708
  writeNote(p.ref, p.content);
709
+ deviceBus.emitDeviceEvent("notes");
682
710
  return ok$2(`Note "${p.ref}" saved.`, { ref: p.ref });
683
711
  } catch (e) {
684
712
  return err$2(`Failed to write note: ${e instanceof Error ? e.message : String(e)}`);
@@ -687,6 +715,7 @@ function handleWrite$1(p) {
687
715
  function handleDelete(p) {
688
716
  if (!p.ref) return err$2("ref is required for delete");
689
717
  if (!deleteNote(p.ref)) return err$2(`Note "${p.ref}" not found or cannot be deleted`);
718
+ deviceBus.emitDeviceEvent("notes");
690
719
  return ok$2(`Note "${p.ref}" deleted.`, { ref: p.ref });
691
720
  }
692
721
  function ok$2(text, details) {
@@ -818,7 +847,7 @@ const terminalTool = {
818
847
  input: Type.Optional(Type.String({ description: "Command or input text (exec appends newline automatically)" })),
819
848
  cwd: Type.Optional(Type.String({ description: "Working directory for new session (create)" })),
820
849
  label: Type.Optional(Type.String({ description: "Session label (create)" })),
821
- virtual: Type.Optional(Type.Boolean({ description: "Use sandboxed virtual TTY (create, default: false)" })),
850
+ sandbox: Type.Optional(Type.Boolean({ description: "Use sandboxed virtual TTY (create, default: false)" })),
822
851
  cols: Type.Optional(Type.Number({ description: "Terminal columns (create/resize, default: 120)" })),
823
852
  rows: Type.Optional(Type.Number({ description: "Terminal rows (create/resize, default: 40)" })),
824
853
  timeout: Type.Optional(Type.Number({ description: "Max ms to wait for output (exec only, default: 10000, max: 30000)" }))
@@ -850,7 +879,7 @@ async function handleCreate(p) {
850
879
  rows: p.rows ?? 40,
851
880
  cwd: p.cwd,
852
881
  label: p.label,
853
- virtual: p.virtual
882
+ sandbox: p.sandbox
854
883
  });
855
884
  return ok$1(`Terminal session created: ${session.id} [${session.label}] ${session.cols}x${session.rows}`, {
856
885
  id: session.id,
@@ -961,6 +990,61 @@ function err$1(text) {
961
990
  };
962
991
  }
963
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
+ /**
964
1048
  * MIT License
965
1049
  * Copyright (c) 2024 Mario Zechner
966
1050
  * Based on https://github.com/badlogic/pi-skills/tree/main/brave-search
@@ -1069,8 +1153,8 @@ async function fetchPageContent(url, signal) {
1069
1153
  }
1070
1154
  async function extractReadableText(html, url) {
1071
1155
  try {
1072
- const { Readability } = await import("../_libs/_12.mjs").then((m) => /* @__PURE__ */ __toESM(m.default, 1));
1073
- const { JSDOM } = await import("../_libs/_17.mjs").then((m) => /* @__PURE__ */ __toESM(m.default, 1));
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));
1074
1158
  const article = new Readability(new JSDOM(html, { url }).window.document).parse();
1075
1159
  if (article?.content) return (await htmlToMarkdown(article.content)).slice(0, 5e3);
1076
1160
  const doc = new JSDOM(html, { url }).window.document;
@@ -1079,13 +1163,13 @@ async function extractReadableText(html, url) {
1079
1163
  if (text.trim().length > 100) return text.trim().slice(0, 5e3);
1080
1164
  return "(Could not extract content)";
1081
1165
  } catch {
1082
- return stripHtmlTags(html).slice(0, 5e3) || "(Could not extract content)";
1166
+ return stripHtmlTags$1(html).slice(0, 5e3) || "(Could not extract content)";
1083
1167
  }
1084
1168
  }
1085
1169
  async function htmlToMarkdown(html) {
1086
1170
  try {
1087
- const TurndownService = (await import("../_libs/_42.mjs")).default;
1088
- const { gfm } = await import("../_libs/_107.mjs");
1171
+ const TurndownService = (await import("../_libs/_29.mjs")).default;
1172
+ const { gfm } = await import("../_libs/_93.mjs");
1089
1173
  const turndown = new TurndownService({
1090
1174
  headingStyle: "atx",
1091
1175
  codeBlockStyle: "fenced"
@@ -1097,10 +1181,10 @@ async function htmlToMarkdown(html) {
1097
1181
  });
1098
1182
  return turndown.turndown(html).replace(/\[\\?\[\s*\\?\]\]\([^)]*\)/g, "").replace(/ +/g, " ").replace(/\s+,/g, ",").replace(/\s+\./g, ".").replace(/\n{3,}/g, "\n\n").trim();
1099
1183
  } catch {
1100
- return stripHtmlTags(html);
1184
+ return stripHtmlTags$1(html);
1101
1185
  }
1102
1186
  }
1103
- function stripHtmlTags(html) {
1187
+ function stripHtmlTags$1(html) {
1104
1188
  return html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
1105
1189
  }
1106
1190
  function ok(text, details) {
@@ -1242,11 +1326,15 @@ function resolveDefaultModel() {
1242
1326
  }
1243
1327
  async function createManagedSession(group, input, sessionDir) {
1244
1328
  const groupDir = path.join(GROUPS_DIR, group.folder);
1329
+ const sandboxEnforced = config.get("sandbox") === "enforced";
1330
+ const sandbox = sandboxEnforced || !!group.sandbox;
1245
1331
  logger$5.info({
1246
1332
  group: group.name,
1247
1333
  folder: group.folder,
1248
1334
  isMain: input.isMain,
1249
- hasSession: !!input.sessionId
1335
+ hasSession: !!input.sessionId,
1336
+ sandbox,
1337
+ ...sandboxEnforced && { sandboxSource: "enforced" }
1250
1338
  }, "Creating pi SDK session");
1251
1339
  const model = resolveGroupModel(group);
1252
1340
  const settingsManager = SettingsManager.inMemory({
@@ -1265,18 +1353,31 @@ async function createManagedSession(group, input, sessionDir) {
1265
1353
  const resourceLoader = new DefaultResourceLoader({
1266
1354
  cwd: groupDir,
1267
1355
  settingsManager,
1268
- systemPromptOverride: () => systemPrompt
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
+ }
1269
1365
  });
1270
1366
  await resourceLoader.reload();
1271
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;
1272
1374
  const { session, modelFallbackMessage } = await createAgentSession({
1273
1375
  cwd: groupDir,
1274
1376
  model,
1275
- thinkingLevel: group.thinkingLevel || "off",
1377
+ thinkingLevel,
1276
1378
  authStorage,
1277
1379
  modelRegistry,
1278
- tools: createCodingTools(groupDir),
1279
- customTools: [
1380
+ customTools: sandbox ? [notifyTool, webSearchTool] : [
1280
1381
  browserTool,
1281
1382
  createMessageTool(group.folder),
1282
1383
  notesTool,
@@ -1288,6 +1389,10 @@ async function createManagedSession(group, input, sessionDir) {
1288
1389
  sessionManager,
1289
1390
  settingsManager
1290
1391
  });
1392
+ if (sandboxedTools) {
1393
+ session._baseToolsOverride = sandboxedTools;
1394
+ await session.reload();
1395
+ }
1291
1396
  if (modelFallbackMessage) logger$5.warn({
1292
1397
  group: group.name,
1293
1398
  message: modelFallbackMessage
@@ -1461,23 +1566,33 @@ var logger$4 = createLogger("pi");
1461
1566
  async function run(group, input, onOutput) {
1462
1567
  return sendPrompt(await getOrCreateSession(group, input), group, input, onOutput);
1463
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
+ */
1464
1575
  function sendMessage(groupJid, message) {
1465
1576
  const managed = sessions.get(groupJid);
1466
- if (!managed) return false;
1577
+ if (!managed || !managed.session.isStreaming) return false;
1467
1578
  const { session } = managed;
1468
- if (session.isStreaming) session.followUp(message).catch((err) => {
1469
- logger$4.error({
1470
- group: managed.groupFolder,
1471
- err
1472
- }, "Failed to queue follow-up");
1473
- });
1474
- else session.prompt(message).catch((err) => {
1475
- logger$4.error({
1476
- group: managed.groupFolder,
1477
- err
1478
- }, "Failed to send message");
1479
- });
1480
- return true;
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
+ }
1481
1596
  }
1482
1597
  async function kill(groupJid) {
1483
1598
  const managed = sessions.get(groupJid);
@@ -1548,8 +1663,7 @@ var commands = {
1548
1663
  stop: handleStop,
1549
1664
  model: handleModel,
1550
1665
  thinking: handleThinking,
1551
- compact: handleCompact,
1552
- brief: handleBrief
1666
+ compact: handleCompact
1553
1667
  };
1554
1668
  var VALID_THINKING_LEVELS = [
1555
1669
  "off",
@@ -1567,7 +1681,7 @@ const commandDescriptions = [
1567
1681
  },
1568
1682
  {
1569
1683
  command: "info",
1570
- description: "Show session info"
1684
+ description: "Show session info and workspace overview"
1571
1685
  },
1572
1686
  {
1573
1687
  command: "new",
@@ -1591,10 +1705,6 @@ const commandDescriptions = [
1591
1705
  {
1592
1706
  command: "compact",
1593
1707
  description: "Compact current session context"
1594
- },
1595
- {
1596
- command: "brief",
1597
- description: "Show workspace snapshot: groups, sessions, tasks"
1598
1708
  }
1599
1709
  ];
1600
1710
  /** Commands that bypass the queue and execute immediately (even during streaming). */
@@ -1639,31 +1749,6 @@ async function tryCommand(ctx, text) {
1639
1749
  await ctx.server.sendBotMessage(ctx.chatJid, `Unknown command \`/${name}\`. Use \`/help\` for available commands.`);
1640
1750
  return true;
1641
1751
  }
1642
- /**
1643
- * Try to handle a message as a `! <bash>` command. Returns true if handled.
1644
- * Runs the command directly in the group's working directory, bypassing the agent.
1645
- */
1646
- function tryBashCommand(ctx, text) {
1647
- if (!text.startsWith("!")) return false;
1648
- const cmd = text.slice(1).trim();
1649
- if (!cmd) return false;
1650
- const cwd = path$1.join(GROUPS_DIR, ctx.group.folder);
1651
- ctx.server.sendBotMessage(ctx.chatJid, `\`$ ${cmd}\``).catch(() => {});
1652
- exec(cmd, {
1653
- cwd,
1654
- timeout: 5e3
1655
- }, (err, stdout, stderr) => {
1656
- const parts = [];
1657
- if (stdout) parts.push(stdout.trimEnd());
1658
- if (stderr) parts.push(stderr.trimEnd());
1659
- if (err && !stderr) parts.push(String(err.message || err));
1660
- const timedOut = err && "killed" in err && err.killed;
1661
- const output = parts.join("\n").replace(stripAnsiRe, "") || "(no output)";
1662
- const suffix = timedOut ? "\n(timed out)" : "";
1663
- ctx.server.sendBotMessage(ctx.chatJid, `\`\`\`\n${output}${suffix}\n\`\`\``).catch(() => {});
1664
- });
1665
- return true;
1666
- }
1667
1752
  async function handleHelp(ctx) {
1668
1753
  const lines = ["**Available commands:**", ...commandDescriptions.map((c) => `- \`/${c.command}\` — ${c.description}`)];
1669
1754
  await ctx.server.sendBotMessage(ctx.chatJid, lines.join("\n"));
@@ -1689,6 +1774,28 @@ async function handleInfo(ctx) {
1689
1774
  lines.push(`**Context:** ${ctx$.tokens.toLocaleString()} / ${ctx$.contextWindow.toLocaleString()} tokens (${pct}%)`);
1690
1775
  }
1691
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*`);
1692
1799
  await ctx.server.sendBotMessage(ctx.chatJid, lines.join("\n"));
1693
1800
  }
1694
1801
  function formatDuration(ms) {
@@ -1803,32 +1910,6 @@ async function handleCompact(ctx, arg) {
1803
1910
  if (after?.tokens != null) lines.push(`**Est. after:** ${after.tokens.toLocaleString()}`);
1804
1911
  await ctx.server.sendBotMessage(ctx.chatJid, lines.join("\n"));
1805
1912
  }
1806
- async function handleBrief(ctx) {
1807
- const allGroups = ctx.server.getRegisteredGroups();
1808
- const allTasks = await getAllTasks();
1809
- const lines = ["**Workspace Brief**", ""];
1810
- lines.push("**Groups:**");
1811
- for (const [jid, g] of Object.entries(allGroups)) {
1812
- const resolved = resolveGroupModel(g);
1813
- const modelLabel = resolved ? `${resolved.provider}/${resolved.id}` : "none";
1814
- const managed = sessions.get(jid);
1815
- const sessionPart = managed ? `active, idle ${formatDuration(Date.now() - managed.lastActivity)}` : "no session";
1816
- const soulPart = g.soul ? " 🧬" : "";
1817
- lines.push(`- **${g.name}** (\`${g.folder}\`) — \`${modelLabel}\` — ${sessionPart}${soulPart}`);
1818
- }
1819
- lines.push("");
1820
- const cutoff = (/* @__PURE__ */ new Date(Date.now() - 1440 * 60 * 1e3)).toISOString();
1821
- const recentTasks = allTasks.filter((t) => t.status === "active" || t.last_run && t.last_run > cutoff);
1822
- lines.push(`**Tasks** (active or run in last 24h): ${recentTasks.length}`);
1823
- for (const t of recentTasks.slice(0, 10)) {
1824
- const groupName = Object.values(allGroups).find((g) => g.folder === t.group_folder)?.name || t.group_folder;
1825
- lines.push(`- [${t.status}] \`${t.schedule_type}:${t.schedule_value}\` in **${groupName}** — ${t.prompt.slice(0, 60)}`);
1826
- }
1827
- if (recentTasks.length > 10) lines.push(` … and ${recentTasks.length - 10} more`);
1828
- lines.push("");
1829
- lines.push(`*${Object.keys(allGroups).length} groups, ${allTasks.filter((t) => t.status === "active").length} active tasks*`);
1830
- await ctx.server.sendBotMessage(ctx.chatJid, lines.join("\n"));
1831
- }
1832
1913
  function findModel(query) {
1833
1914
  const available = getFilteredModels();
1834
1915
  if (available.length === 0) return void 0;
@@ -1847,16 +1928,16 @@ function findModel(query) {
1847
1928
  matches.sort((a, b) => a.id.length - b.id.length || b.id.localeCompare(a.id, void 0, { numeric: true }));
1848
1929
  return matches[0];
1849
1930
  }
1850
- var stripAnsiRe = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*[A-Za-z]`, "g");
1931
+ new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*[A-Za-z]`, "g");
1851
1932
  await init();
1852
1933
  var logger$3 = createLogger("telegram");
1853
1934
  var JID_PREFIX = "tg:";
1854
1935
  var POLL_INTERVAL = 1e3;
1855
1936
  var POLL_TIMEOUT = 30;
1856
1937
  var API_BASE = "https://api.telegram.org/bot";
1857
- var STREAM_THROTTLE_MIN = 700;
1858
- var STREAM_THROTTLE_MAX = 1500;
1859
- var STREAM_THROTTLE_STEP = 200;
1938
+ var STREAM_THROTTLE_MIN = 1e3;
1939
+ var STREAM_THROTTLE_MAX = 1e4;
1940
+ var STREAM_THROTTLE_STEP = 1e3;
1860
1941
  var STREAM_THROTTLE_JITTER = 120;
1861
1942
  var STREAM_INITIAL_DELAY = 100;
1862
1943
  var TG_MAX_TEXT = 4096;
@@ -1894,10 +1975,9 @@ var TelegramChannel = class {
1894
1975
  async sendMessage(jid, text) {
1895
1976
  const chatId = jid.slice(3);
1896
1977
  const state = this.streamState.get(jid);
1897
- const toolSummary = state?.tools.length ? formatToolSummary(state.tools) : "";
1898
- const fullText = toolSummary ? `${toolSummary}\n\n${text}` : text;
1899
- const { text: formatted, parse_mode } = formatForTelegram(fullText);
1978
+ const { text: formatted, parse_mode } = formatForTelegram(text);
1900
1979
  const parseMode = { parse_mode };
1980
+ const plainText = stripThinkingTags(text);
1901
1981
  if (state) {
1902
1982
  this.streamState.delete(jid);
1903
1983
  if (state.editTimer) clearTimeout(state.editTimer);
@@ -1912,19 +1992,19 @@ var TelegramChannel = class {
1912
1992
  return;
1913
1993
  } catch (editErr) {
1914
1994
  if (isTgParseError(editErr)) try {
1915
- logger$3.warn({
1995
+ logger$3.debug({
1916
1996
  err: editErr,
1917
1997
  formattedText: formatted.slice(0, 500),
1918
- originalText: fullText.slice(0, 500)
1919
- }, "MarkdownV2 edit failed, retrying as plain text");
1998
+ originalText: text.slice(0, 500)
1999
+ }, "HTML edit failed, retrying as plain text");
1920
2000
  await this.api("editMessageText", {
1921
2001
  chat_id: chatId,
1922
2002
  message_id: state.messageId,
1923
- text: fullText.slice(0, TG_MAX_TEXT)
2003
+ text: plainText.slice(0, TG_MAX_TEXT)
1924
2004
  });
1925
2005
  return;
1926
2006
  } catch (plainErr) {
1927
- logger$3.warn({ err: plainErr }, "Plain-text edit also failed, sending new message");
2007
+ logger$3.debug({ err: plainErr }, "Plain-text edit also failed, sending new message");
1928
2008
  }
1929
2009
  }
1930
2010
  }
@@ -1936,14 +2016,14 @@ var TelegramChannel = class {
1936
2016
  });
1937
2017
  } catch (err) {
1938
2018
  if (isTgParseError(err)) {
1939
- logger$3.warn({
2019
+ logger$3.debug({
1940
2020
  err,
1941
2021
  formattedText: formatted.slice(0, 500),
1942
- originalText: fullText.slice(0, 500)
1943
- }, "MarkdownV2 send failed, retrying as plain text");
2022
+ originalText: text.slice(0, 500)
2023
+ }, "HTML send failed, retrying as plain text");
1944
2024
  await this.api("sendMessage", {
1945
2025
  chat_id: chatId,
1946
- text: fullText
2026
+ text: plainText
1947
2027
  });
1948
2028
  } else throw err;
1949
2029
  }
@@ -1980,32 +2060,50 @@ var TelegramChannel = class {
1980
2060
  switch (event.type) {
1981
2061
  case "thinking_delta": {
1982
2062
  const state = this.getOrCreateStreamState(jid);
1983
- 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
+ });
1984
2069
  this.scheduleStreamEdit(jid, state);
1985
2070
  break;
1986
2071
  }
1987
2072
  case "text_delta": {
1988
2073
  const state = this.getOrCreateStreamState(jid);
1989
- 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
+ });
1990
2080
  this.scheduleStreamEdit(jid, state);
1991
2081
  break;
1992
2082
  }
1993
2083
  case "tool_start": {
1994
2084
  const state = this.getOrCreateStreamState(jid);
1995
- state.tools.push({
2085
+ const tool = {
1996
2086
  name: event.toolName || "tool",
1997
2087
  done: false
2088
+ };
2089
+ state.tools.push(tool);
2090
+ state.details.push({
2091
+ type: "tool",
2092
+ tool
1998
2093
  });
1999
2094
  this.scheduleStreamEdit(jid, state);
2000
2095
  break;
2001
2096
  }
2002
2097
  case "tool_end": {
2003
2098
  const state = this.getOrCreateStreamState(jid);
2004
- for (let i = state.tools.length - 1; i >= 0; i--) if (state.tools[i].name === event.toolName && !state.tools[i].done) {
2005
- state.tools[i].done = true;
2006
- state.tools[i].isError = event.isError;
2007
- state.tools[i].resultPreview = event.resultPreview;
2008
- break;
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
+ }
2009
2107
  }
2010
2108
  this.scheduleStreamEdit(jid, state);
2011
2109
  break;
@@ -2041,8 +2139,7 @@ var TelegramChannel = class {
2041
2139
  if (!state) {
2042
2140
  state = {
2043
2141
  chatId: jid.slice(3),
2044
- text: "",
2045
- thinkingText: "",
2142
+ details: [],
2046
2143
  tools: [],
2047
2144
  messageId: null,
2048
2145
  sendPromise: null,
@@ -2065,29 +2162,55 @@ var TelegramChannel = class {
2065
2162
  }, delay);
2066
2163
  }
2067
2164
  async flushStreamEdit(jid, state) {
2165
+ if (!this.streamState.has(jid)) return;
2068
2166
  if (state.sendPromise) {
2069
2167
  await state.sendPromise.catch(() => {});
2070
2168
  state.sendPromise = null;
2071
2169
  }
2072
- const display = [formatToolLines(state.tools), state.text.trim() || (state.thinkingText.trim() ? `<thinking>${state.thinkingText.trim()}</thinking>` : "")].filter(Boolean).join("\n\n");
2073
- if (!display) return;
2074
- const { text: formatted, parse_mode } = formatForTelegram(display.length > TG_MAX_TEXT ? display.slice(0, TG_MAX_TEXT - 1) + "…" : display);
2075
- const snapshotLen = display.length;
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 };
2076
2198
  const sendOrEdit = async (text, parseMode) => {
2077
- const opts = {
2199
+ if (!this.streamState.has(jid)) return;
2200
+ const body = {
2078
2201
  chat_id: state.chatId,
2079
2202
  text
2080
2203
  };
2081
- if (parseMode) opts.parse_mode = parseMode;
2204
+ if (parseMode) body.parse_mode = parseMode;
2082
2205
  if (state.messageId === null) {
2083
- state.sendPromise = this.api("sendMessage", opts).then((msg) => {
2206
+ state.sendPromise = this.api("sendMessage", body, streamApiOpts).then((msg) => {
2084
2207
  state.messageId = msg.message_id;
2085
2208
  state.sendPromise = null;
2086
2209
  });
2087
2210
  await state.sendPromise;
2088
2211
  } else {
2089
- opts.message_id = state.messageId;
2090
- await this.api("editMessageText", opts);
2212
+ body.message_id = state.messageId;
2213
+ await this.api("editMessageText", body, streamApiOpts);
2091
2214
  }
2092
2215
  };
2093
2216
  try {
@@ -2096,16 +2219,15 @@ var TelegramChannel = class {
2096
2219
  state.throttleMs = Math.max(STREAM_THROTTLE_MIN, state.throttleMs - 50);
2097
2220
  } catch (err) {
2098
2221
  if (isTgParseError(err)) {
2099
- logger$3.warn({
2222
+ logger$3.debug({
2100
2223
  err,
2101
- formattedText: formatted.slice(0, 500),
2102
- originalText: display.slice(0, 500)
2103
- }, "Stream MarkdownV2 failed, retrying as plain text");
2224
+ formattedText: formatted.slice(0, 500)
2225
+ }, "Stream HTML failed, retrying as plain text");
2104
2226
  try {
2105
- await sendOrEdit(display.length > TG_MAX_TEXT ? display.slice(0, TG_MAX_TEXT - 1) + "…" : display);
2227
+ await sendOrEdit(stripHtmlTags(formatted).slice(0, TG_MAX_TEXT));
2106
2228
  state.lastEdit = Date.now();
2107
2229
  } catch (retryErr) {
2108
- logger$3.warn({ err: retryErr }, "Stream plain-text fallback also failed");
2230
+ logger$3.debug({ err: retryErr }, "Stream plain-text fallback also failed");
2109
2231
  }
2110
2232
  } else if (isTgMessageNotModified(err)) {} else {
2111
2233
  const retryAfterMs = getRetryAfterMs(err);
@@ -2116,7 +2238,7 @@ var TelegramChannel = class {
2116
2238
  }, "Stream edit failed");
2117
2239
  }
2118
2240
  }
2119
- 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);
2120
2242
  }
2121
2243
  startPolling() {
2122
2244
  this.stopPolling();
@@ -2138,17 +2260,17 @@ var TelegramChannel = class {
2138
2260
  const res = await fetch(`${API_BASE}${this.config.botToken}/getUpdates?${params}`, { signal });
2139
2261
  if (!res.ok) {
2140
2262
  if (res.status === 409) {
2141
- 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");
2142
2264
  await sleep(3e4, signal);
2143
2265
  continue;
2144
2266
  }
2145
- logger$3.warn({ status: res.status }, "Telegram getUpdates HTTP error");
2267
+ logger$3.debug({ status: res.status }, "Telegram getUpdates HTTP error");
2146
2268
  await sleep(POLL_INTERVAL, signal);
2147
2269
  continue;
2148
2270
  }
2149
2271
  const data = await res.json();
2150
2272
  if (!data.ok) {
2151
- logger$3.warn({ description: data.description }, "Telegram getUpdates API error");
2273
+ logger$3.debug({ description: data.description }, "Telegram getUpdates API error");
2152
2274
  await sleep(POLL_INTERVAL, signal);
2153
2275
  continue;
2154
2276
  }
@@ -2159,7 +2281,7 @@ var TelegramChannel = class {
2159
2281
  }
2160
2282
  } catch (err) {
2161
2283
  if (signal.aborted) break;
2162
- logger$3.warn({ err }, "Telegram polling error");
2284
+ logger$3.debug({ err }, "Telegram polling error");
2163
2285
  await sleep(POLL_INTERVAL, signal);
2164
2286
  }
2165
2287
  };
@@ -2169,14 +2291,35 @@ var TelegramChannel = class {
2169
2291
  this.pollAbort?.abort();
2170
2292
  this.pollAbort = null;
2171
2293
  }
2172
- async api(method, body) {
2294
+ async api(method, body, opts, _retries = 0) {
2173
2295
  const res = await fetch(`${API_BASE}${this.config.botToken}/${method}`, {
2174
2296
  method: "POST",
2175
2297
  headers: { "content-type": "application/json" },
2176
2298
  body: body ? JSON.stringify(body) : void 0
2177
2299
  });
2178
2300
  const data = await res.json();
2179
- 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
+ }
2180
2323
  return data.result;
2181
2324
  }
2182
2325
  handleUpdate(update) {
@@ -2206,7 +2349,7 @@ var TelegramChannel = class {
2206
2349
  this.opts.onMessage(jid, msg);
2207
2350
  this.opts.onChatMetadata(jid, now, senderName, "telegram", isGroup);
2208
2351
  }).catch((err) => {
2209
- logger$3.warn({ err }, "Failed to download Telegram media, sending text-only");
2352
+ logger$3.debug({ err }, "Failed to download Telegram media, sending text-only");
2210
2353
  this.opts.onMessage(jid, msg);
2211
2354
  this.opts.onChatMetadata(jid, now, senderName, "telegram", isGroup);
2212
2355
  });
@@ -2256,7 +2399,7 @@ var TelegramChannel = class {
2256
2399
  name
2257
2400
  });
2258
2401
  } catch (err) {
2259
- logger$3.warn({
2402
+ logger$3.debug({
2260
2403
  err,
2261
2404
  fileId
2262
2405
  }, "Failed to download Telegram file");
@@ -2272,39 +2415,28 @@ var TelegramApiError = class extends Error {
2272
2415
  this.response = response;
2273
2416
  }
2274
2417
  };
2275
- /** 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 */
2276
2419
  function formatForTelegram(text) {
2277
2420
  let remaining = text.replace(/<\/?internal>/g, (t) => t.replace("internal", "thinking"));
2278
- const parts = [];
2279
- const thinkingRe = /<thinking>([\s\S]*?)<\/thinking>/g;
2280
- let lastIdx = 0;
2281
- let m;
2282
- while ((m = thinkingRe.exec(remaining)) !== null) {
2283
- if (m.index > lastIdx) parts.push(mdToTgV2(remaining.slice(lastIdx, m.index)));
2284
- const trimmed = (m[1] || "").trim();
2285
- if (trimmed) {
2286
- const quoted = escTgV2(trimmed).split("\n").map((l) => `>${l}`).join("\n");
2287
- parts.push(quoted);
2288
- }
2289
- lastIdx = m.index + m[0].length;
2290
- }
2291
- 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, "");
2292
2424
  return {
2293
- text: parts.join("").replace(/\n{3,}/g, "\n\n").trim(),
2294
- parse_mode: "MarkdownV2"
2425
+ text: [mdToTgHtml(remaining)].join("").replace(/\n{3,}/g, "\n\n").trim(),
2426
+ parse_mode: "HTML"
2295
2427
  };
2296
2428
  }
2297
- /** Parse markdown with md4x and render AST nodes to Telegram MarkdownV2 */
2298
- function mdToTgV2(text) {
2429
+ /** Parse markdown with md4x and render AST nodes to Telegram HTML */
2430
+ function mdToTgHtml(text) {
2299
2431
  return parseAST(text).nodes.map((node) => renderNode(node)).join("\n\n");
2300
2432
  }
2301
2433
  /** Collect raw text from children recursively (for code content that must not be escaped) */
2302
2434
  function collectText(children) {
2303
2435
  return children.map((c) => typeof c === "string" ? c : collectText(c.slice(2))).join("");
2304
2436
  }
2305
- /** Render a single AST node to Telegram MarkdownV2 */
2437
+ /** Render a single AST node to Telegram HTML */
2306
2438
  function renderNode(node) {
2307
- if (typeof node === "string") return escTgV2(node);
2439
+ if (typeof node === "string") return escHtml(node);
2308
2440
  const [tag, attrs, ...children] = node;
2309
2441
  const inner = () => children.map((c) => renderNode(c)).join("");
2310
2442
  switch (tag) {
@@ -2314,52 +2446,41 @@ function renderNode(node) {
2314
2446
  case "h3":
2315
2447
  case "h4":
2316
2448
  case "h5":
2317
- case "h6": return `*${inner()}*`;
2318
- case "strong": return `*${inner()}*`;
2319
- case "em": return `_${inner()}_`;
2320
- case "u": return `__${inner()}__`;
2321
- case "del": return `~${inner()}~`;
2322
- case "code": {
2323
- let codeText = collectText(children);
2324
- if (codeText.endsWith("\\")) codeText += " ";
2325
- return `\`${codeText}\``;
2326
- }
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>`;
2327
2455
  case "pre": {
2328
2456
  const codeNode = children.find((c) => typeof c !== "string" && c[0] === "code");
2329
2457
  const lang = String(attrs.language || "");
2330
2458
  const raw = codeNode ? collectText(codeNode.slice(2)) : collectText(children);
2331
2459
  if (!raw.trim() && !lang) return "";
2332
- return `\`\`\`${lang}\n${raw.replace(/`{3,}/g, "'''").replace(/\n$/, "")}\n\`\`\``;
2333
- }
2334
- case "a": {
2335
- const href = String(attrs.href || "");
2336
- return `[${inner()}](${escTgUrl(href)})`;
2460
+ return `<pre><code${lang ? ` class="language-${escHtml(lang)}"` : ""}>${escHtml(raw.replace(/\n$/, ""))}</code></pre>`;
2337
2461
  }
2338
- case "img": return escTgV2(String(attrs.alt || "image"));
2339
- 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>`;
2340
2465
  case "ul":
2341
2466
  case "ol": return children.map((c, i) => {
2342
- if (typeof c === "string") return escTgV2(c);
2343
- 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);
2344
2469
  }).filter((l) => l.trim()).join("\n");
2345
2470
  case "li": return inner();
2346
- case "hr": return escTgV2("---");
2471
+ case "hr": return "---";
2347
2472
  case "br": return "\n";
2348
2473
  default: return inner();
2349
2474
  }
2350
2475
  }
2351
- /** Escape special characters for Telegram MarkdownV2 outside code spans */
2352
- function escTgV2(s) {
2353
- 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;");
2354
2479
  }
2355
- /** Check if a Telegram API error is a MarkdownV2 parse failure */
2480
+ /** Check if a Telegram API error is an HTML parse failure */
2356
2481
  function isTgParseError(err) {
2357
2482
  return err instanceof TelegramApiError && err.status === 400 && typeof err.response?.description === "string" && err.response.description.includes("can't parse entities");
2358
2483
  }
2359
- /** Escape only ) and \ inside MarkdownV2 link URLs */
2360
- function escTgUrl(s) {
2361
- return s.replace(/([)\\])/g, "\\$1");
2362
- }
2363
2484
  /** Check if a Telegram API error is "message is not modified" (identical content edit) */
2364
2485
  function isTgMessageNotModified(err) {
2365
2486
  return err instanceof TelegramApiError && err.status === 400 && typeof err.response?.description === "string" && err.response.description.includes("message is not modified");
@@ -2373,45 +2494,36 @@ function getRetryAfterMs(err) {
2373
2494
  /** Verbose tool lines with result previews — used during streaming */
2374
2495
  function formatToolLines(tools) {
2375
2496
  return tools.map((t) => {
2376
- const icon = t.done ? t.isError ? "" : "" : "";
2497
+ const icon = t.done ? t.isError ? "" : "" : "🔧";
2377
2498
  const preview = t.done && t.resultPreview ? ` ${tgToolPreview(t.resultPreview)}` : "";
2378
- return `${icon} ${t.name}${preview}`;
2499
+ return `${icon} ${escHtml(t.name)}${preview}`;
2379
2500
  }).join("\n");
2380
2501
  }
2381
- /** Compact tool summary with result previews — used in final persisted message */
2382
- function formatToolSummary(tools) {
2383
- const lines = [];
2384
- let i = 0;
2385
- while (i < tools.length) {
2386
- const t = tools[i];
2387
- if (t.resultPreview) {
2388
- const icon = t.isError ? "✗" : "✓";
2389
- lines.push(`${icon} ${t.name} ${tgToolPreview(t.resultPreview)}`);
2390
- i++;
2391
- continue;
2392
- }
2393
- let count = 1;
2394
- let hasError = !!t.isError;
2395
- while (i + count < tools.length && tools[i + count].name === t.name && !tools[i + count].resultPreview) {
2396
- if (tools[i + count].isError) hasError = true;
2397
- count++;
2398
- }
2399
- const icon = hasError ? "✗" : "✓";
2400
- const label = count > 1 ? `${t.name} ×${count}` : t.name;
2401
- lines.push(`${icon} ${label}`);
2402
- i += count;
2403
- }
2404
- return lines.join("\n");
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;
2405
2506
  }
2406
2507
  function tgToolPreview(raw) {
2407
- let text;
2408
- try {
2409
- text = JSON.parse(raw)?.content?.[0]?.text ?? raw;
2410
- } catch {
2411
- text = raw.match(/"text"\s*:\s*"((?:[^"\\]|\\.)*)(?:"|$)/)?.[1]?.replaceAll("\\n", "\n").replaceAll("\\t", " ").replaceAll("\\\"", "\"") ?? raw;
2412
- }
2413
- const firstLine = text.split("\n")[0] || "";
2414
- return firstLine.length > 80 ? firstLine.slice(0, 79) + "" : firstLine;
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;
2415
2527
  }
2416
2528
  function randomJitter(maxAbs) {
2417
2529
  return Math.round((Math.random() * 2 - 1) * maxAbs);
@@ -2486,16 +2598,18 @@ var GroupQueue = class {
2486
2598
  this.drain();
2487
2599
  }
2488
2600
  drain() {
2601
+ const deferred = [];
2489
2602
  while (this.active.size < config.get("maxConcurrentAgents") && this.queue.length > 0) {
2490
2603
  const entry = this.queue.shift();
2491
2604
  if (!entry) break;
2492
2605
  if (this.active.has(entry.chatJid)) {
2493
- this.queue.push(entry);
2494
- break;
2606
+ deferred.push(entry);
2607
+ continue;
2495
2608
  }
2496
2609
  this.active.add(entry.chatJid);
2497
2610
  this.processEntry(entry);
2498
2611
  }
2612
+ this.queue.push(...deferred);
2499
2613
  }
2500
2614
  async processEntry(entry) {
2501
2615
  try {
@@ -2554,7 +2668,9 @@ function formatOutbound(rawText) {
2554
2668
  function needsProcessing(group, messages) {
2555
2669
  if (group.folder === "main") return true;
2556
2670
  if (group.requiresTrigger === false) return true;
2557
- 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()));
2558
2674
  }
2559
2675
  /**
2560
2676
  * Task scheduler — runs scheduled tasks (cron/interval/once) using the pi agent.
@@ -2831,8 +2947,18 @@ var PiClawServer = class {
2831
2947
  return new TelegramChannel({
2832
2948
  onMessage: async (jid, msg) => {
2833
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
+ }
2834
2959
  await storeMessage(msg);
2835
- 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);
2836
2962
  else await this.notifyUnregistered(jid);
2837
2963
  },
2838
2964
  onChatMetadata: async (jid, ts, name, channel, isGroup) => {
@@ -2916,18 +3042,22 @@ var PiClawServer = class {
2916
3042
  await setRouterState("last_timestamp", this.lastTimestamp);
2917
3043
  await setRouterState("last_agent_timestamp", JSON.stringify(this.lastAgentTimestamp));
2918
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
+ }
2919
3050
  async handleCommands(chatJid, messages) {
2920
3051
  const group = this.registeredGroups[chatJid];
2921
3052
  if (!group) return messages;
2922
3053
  const remaining = [];
2923
3054
  for (const msg of messages) {
2924
3055
  const text = msg.content.trim();
2925
- const ctx = {
3056
+ if (!await tryCommand({
2926
3057
  chatJid,
2927
3058
  group,
2928
3059
  server: this
2929
- };
2930
- if (!(tryBashCommand(ctx, text) || await tryCommand(ctx, text))) remaining.push(msg);
3060
+ }, text)) remaining.push(msg);
2931
3061
  }
2932
3062
  return remaining;
2933
3063
  }
@@ -2989,8 +3119,7 @@ var PiClawServer = class {
2989
3119
  if (result.result) {
2990
3120
  const text = formatOutbound(result.result);
2991
3121
  if (text) {
2992
- const meta = result.tools?.length ? { tools: result.tools } : void 0;
2993
- await this.sendBotMessage(chatJid, text, meta);
3122
+ await this.sendBotMessage(chatJid, text);
2994
3123
  outputSentToUser = true;
2995
3124
  }
2996
3125
  }
@@ -3040,15 +3169,6 @@ var PiClawServer = class {
3040
3169
  for (const [chatJid, groupMessages] of byGroup) {
3041
3170
  const group = this.registeredGroups[chatJid];
3042
3171
  if (!group) continue;
3043
- for (const msg of groupMessages) {
3044
- const ctx = {
3045
- chatJid,
3046
- group,
3047
- server: this
3048
- };
3049
- tryBashCommand(ctx, msg.content.trim());
3050
- tryPriorityCommand(ctx, msg.content.trim());
3051
- }
3052
3172
  if (!needsProcessing(group, groupMessages)) continue;
3053
3173
  this.queue.enqueueMessageCheck(chatJid);
3054
3174
  }
@@ -3088,4 +3208,4 @@ const startTime = globalThis.__piclaw_start_time__ || performance.now();
3088
3208
  const server = new PiClawServer();
3089
3209
  await server.start();
3090
3210
  var server_default = () => {};
3091
- export { tryBashCommand as a, authStorage as c, getSystemInfo as d, commandDescriptions as i, getFilteredModels as l, server_default as n, tryPriorityCommand as o, startTime as r, getPiSdkStatus as s, server as t, modelRegistry as u };
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 };