@geminixiang/mikan 0.5.0-beta.0 → 0.5.1

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 (263) hide show
  1. package/CHANGELOG.md +72 -2
  2. package/README.md +13 -13
  3. package/dist/adapter.d.ts +1 -1
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/adapter.js.map +1 -1
  6. package/dist/adapters/discord/bot.d.ts +9 -6
  7. package/dist/adapters/discord/bot.d.ts.map +1 -1
  8. package/dist/adapters/discord/bot.js +22 -22
  9. package/dist/adapters/discord/bot.js.map +1 -1
  10. package/dist/adapters/discord/context.d.ts +6 -6
  11. package/dist/adapters/discord/context.d.ts.map +1 -1
  12. package/dist/adapters/discord/context.js +3 -3
  13. package/dist/adapters/discord/context.js.map +1 -1
  14. package/dist/adapters/discord/types.d.ts +2 -2
  15. package/dist/adapters/discord/types.d.ts.map +1 -1
  16. package/dist/adapters/discord/types.js.map +1 -1
  17. package/dist/adapters/intake.d.ts +2 -2
  18. package/dist/adapters/intake.d.ts.map +1 -1
  19. package/dist/adapters/intake.js +4 -4
  20. package/dist/adapters/intake.js.map +1 -1
  21. package/dist/adapters/shared.d.ts +3 -3
  22. package/dist/adapters/shared.d.ts.map +1 -1
  23. package/dist/adapters/shared.js +2 -2
  24. package/dist/adapters/shared.js.map +1 -1
  25. package/dist/adapters/slack/blocks.d.ts +6 -0
  26. package/dist/adapters/slack/blocks.d.ts.map +1 -0
  27. package/dist/adapters/slack/blocks.js +176 -0
  28. package/dist/adapters/slack/blocks.js.map +1 -0
  29. package/dist/adapters/slack/bot.d.ts +10 -8
  30. package/dist/adapters/slack/bot.d.ts.map +1 -1
  31. package/dist/adapters/slack/bot.js +58 -80
  32. package/dist/adapters/slack/bot.js.map +1 -1
  33. package/dist/adapters/slack/context.d.ts +6 -6
  34. package/dist/adapters/slack/context.d.ts.map +1 -1
  35. package/dist/adapters/slack/context.js +2 -2
  36. package/dist/adapters/slack/context.js.map +1 -1
  37. package/dist/adapters/slack/response-lifecycle.d.ts +6 -6
  38. package/dist/adapters/slack/response-lifecycle.d.ts.map +1 -1
  39. package/dist/adapters/slack/response-lifecycle.js +8 -33
  40. package/dist/adapters/slack/response-lifecycle.js.map +1 -1
  41. package/dist/adapters/slack/session.d.ts.map +1 -1
  42. package/dist/adapters/slack/tools/attach.d.ts.map +1 -1
  43. package/dist/adapters/slack/types.d.ts +1 -1
  44. package/dist/adapters/slack/types.d.ts.map +1 -1
  45. package/dist/adapters/slack/types.js.map +1 -1
  46. package/dist/adapters/streaming.d.ts.map +1 -1
  47. package/dist/adapters/telegram/bot.d.ts +10 -7
  48. package/dist/adapters/telegram/bot.d.ts.map +1 -1
  49. package/dist/adapters/telegram/bot.js +21 -21
  50. package/dist/adapters/telegram/bot.js.map +1 -1
  51. package/dist/adapters/telegram/context.d.ts +6 -6
  52. package/dist/adapters/telegram/context.d.ts.map +1 -1
  53. package/dist/adapters/telegram/context.js +3 -3
  54. package/dist/adapters/telegram/context.js.map +1 -1
  55. package/dist/adapters/telegram/html.d.ts.map +1 -1
  56. package/dist/adapters/telegram/types.d.ts +2 -2
  57. package/dist/adapters/telegram/types.d.ts.map +1 -1
  58. package/dist/adapters/telegram/types.js.map +1 -1
  59. package/dist/adapters/types.d.ts +6 -6
  60. package/dist/adapters/types.d.ts.map +1 -1
  61. package/dist/adapters/types.js.map +1 -1
  62. package/dist/agent-events.d.ts +5 -0
  63. package/dist/agent-events.d.ts.map +1 -0
  64. package/dist/agent-events.js +26 -0
  65. package/dist/agent-events.js.map +1 -0
  66. package/dist/agent.d.ts +7 -7
  67. package/dist/agent.d.ts.map +1 -1
  68. package/dist/agent.js +117 -82
  69. package/dist/agent.js.map +1 -1
  70. package/dist/commands/admin.d.ts.map +1 -1
  71. package/dist/commands/admin.js +1 -1
  72. package/dist/commands/admin.js.map +1 -1
  73. package/dist/commands/auto-reply.d.ts.map +1 -1
  74. package/dist/commands/auto-reply.js +3 -3
  75. package/dist/commands/auto-reply.js.map +1 -1
  76. package/dist/commands/login.d.ts.map +1 -1
  77. package/dist/commands/login.js +1 -1
  78. package/dist/commands/login.js.map +1 -1
  79. package/dist/commands/model.d.ts.map +1 -1
  80. package/dist/commands/model.js +5 -5
  81. package/dist/commands/model.js.map +1 -1
  82. package/dist/commands/new.d.ts.map +1 -1
  83. package/dist/commands/new.js +2 -2
  84. package/dist/commands/new.js.map +1 -1
  85. package/dist/commands/parse.d.ts.map +1 -1
  86. package/dist/commands/registry.d.ts.map +1 -1
  87. package/dist/commands/sandbox.d.ts.map +1 -1
  88. package/dist/commands/sandbox.js +5 -5
  89. package/dist/commands/sandbox.js.map +1 -1
  90. package/dist/commands/session-view.d.ts.map +1 -1
  91. package/dist/commands/session-view.js +1 -1
  92. package/dist/commands/session-view.js.map +1 -1
  93. package/dist/commands/types.d.ts +5 -5
  94. package/dist/commands/types.d.ts.map +1 -1
  95. package/dist/commands/types.js.map +1 -1
  96. package/dist/commands/utils.d.ts +3 -3
  97. package/dist/commands/utils.d.ts.map +1 -1
  98. package/dist/commands/utils.js +6 -6
  99. package/dist/commands/utils.js.map +1 -1
  100. package/dist/config.d.ts.map +1 -1
  101. package/dist/context.d.ts.map +1 -1
  102. package/dist/download.d.ts.map +1 -1
  103. package/dist/events.d.ts +2 -2
  104. package/dist/events.d.ts.map +1 -1
  105. package/dist/events.js.map +1 -1
  106. package/dist/execution-resolver.d.ts.map +1 -1
  107. package/dist/index.d.ts +3 -3
  108. package/dist/index.d.ts.map +1 -1
  109. package/dist/index.js +2 -2
  110. package/dist/index.js.map +1 -1
  111. package/dist/log.d.ts.map +1 -1
  112. package/dist/main.d.ts.map +1 -1
  113. package/dist/main.js +35 -27
  114. package/dist/main.js.map +1 -1
  115. package/dist/model-registry.d.ts.map +1 -1
  116. package/dist/observability/instrument.d.ts.map +1 -1
  117. package/dist/observability/sentry.d.ts.map +1 -1
  118. package/dist/observability/sentry.js +27 -9
  119. package/dist/observability/sentry.js.map +1 -1
  120. package/dist/observability/types.d.ts.map +1 -1
  121. package/dist/platform-messages.d.ts +2 -2
  122. package/dist/platform-messages.d.ts.map +1 -1
  123. package/dist/platform-messages.js +2 -2
  124. package/dist/platform-messages.js.map +1 -1
  125. package/dist/portal-shell.d.ts.map +1 -1
  126. package/dist/provisioner.d.ts.map +1 -1
  127. package/dist/provisioner.js +6 -0
  128. package/dist/provisioner.js.map +1 -1
  129. package/dist/runtime/{conversation-orchestrator.d.ts → agent-run-controller.d.ts} +7 -5
  130. package/dist/runtime/agent-run-controller.d.ts.map +1 -0
  131. package/dist/runtime/{conversation-orchestrator.js → agent-run-controller.js} +18 -17
  132. package/dist/runtime/agent-run-controller.js.map +1 -0
  133. package/dist/runtime/conversation-runtime.d.ts +4 -0
  134. package/dist/runtime/conversation-runtime.d.ts.map +1 -0
  135. package/dist/runtime/{session-runtime.js → conversation-runtime.js} +19 -15
  136. package/dist/runtime/conversation-runtime.js.map +1 -0
  137. package/dist/runtime/types.d.ts +11 -9
  138. package/dist/runtime/types.d.ts.map +1 -1
  139. package/dist/runtime/types.js.map +1 -1
  140. package/dist/sandbox/cloudflare.d.ts.map +1 -1
  141. package/dist/sandbox/cloudflare.js +2 -4
  142. package/dist/sandbox/cloudflare.js.map +1 -1
  143. package/dist/sandbox/container.d.ts.map +1 -1
  144. package/dist/sandbox/container.js +43 -10
  145. package/dist/sandbox/container.js.map +1 -1
  146. package/dist/sandbox/errors.d.ts.map +1 -1
  147. package/dist/sandbox/firecracker.d.ts.map +1 -1
  148. package/dist/sandbox/firecracker.js +2 -4
  149. package/dist/sandbox/firecracker.js.map +1 -1
  150. package/dist/sandbox/host.d.ts.map +1 -1
  151. package/dist/sandbox/image.d.ts.map +1 -1
  152. package/dist/sandbox/index.d.ts.map +1 -1
  153. package/dist/sandbox/path-context.d.ts.map +1 -1
  154. package/dist/sandbox/types.d.ts.map +1 -1
  155. package/dist/sandbox/utils.d.ts.map +1 -1
  156. package/dist/sessions/agent-memory-file-manager.d.ts +19 -0
  157. package/dist/sessions/agent-memory-file-manager.d.ts.map +1 -0
  158. package/dist/sessions/{chat-session-manager.js → agent-memory-file-manager.js} +107 -34
  159. package/dist/sessions/agent-memory-file-manager.js.map +1 -0
  160. package/dist/sessions/metadata.d.ts.map +1 -1
  161. package/dist/sessions/policy.d.ts.map +1 -1
  162. package/dist/sessions/store.d.ts.map +1 -1
  163. package/dist/sessions/store.js +4 -4
  164. package/dist/sessions/store.js.map +1 -1
  165. package/dist/sessions/types.d.ts +5 -3
  166. package/dist/sessions/types.d.ts.map +1 -1
  167. package/dist/sessions/types.js.map +1 -1
  168. package/dist/store.d.ts +3 -0
  169. package/dist/store.d.ts.map +1 -1
  170. package/dist/store.js +1 -1
  171. package/dist/store.js.map +1 -1
  172. package/dist/tools/bash.d.ts.map +1 -1
  173. package/dist/tools/edit.d.ts.map +1 -1
  174. package/dist/tools/event.d.ts.map +1 -1
  175. package/dist/tools/index.d.ts +1 -2
  176. package/dist/tools/index.d.ts.map +1 -1
  177. package/dist/tools/index.js +0 -4
  178. package/dist/tools/index.js.map +1 -1
  179. package/dist/tools/read.d.ts.map +1 -1
  180. package/dist/tools/sandbox.d.ts.map +1 -1
  181. package/dist/tools/truncate.d.ts.map +1 -1
  182. package/dist/tools/types.d.ts.map +1 -1
  183. package/dist/tools/write.d.ts.map +1 -1
  184. package/dist/trigger.d.ts +2 -2
  185. package/dist/trigger.d.ts.map +1 -1
  186. package/dist/trigger.js +1 -1
  187. package/dist/trigger.js.map +1 -1
  188. package/dist/types.d.ts +47 -29
  189. package/dist/types.d.ts.map +1 -1
  190. package/dist/types.js +1 -1
  191. package/dist/types.js.map +1 -1
  192. package/dist/utils/date.d.ts +1 -1
  193. package/dist/utils/date.d.ts.map +1 -1
  194. package/dist/utils/date.js +1 -1
  195. package/dist/utils/date.js.map +1 -1
  196. package/dist/utils/env.d.ts.map +1 -1
  197. package/dist/utils/file-guards.d.ts.map +1 -1
  198. package/dist/utils/fs-atomic.d.ts.map +1 -1
  199. package/dist/utils/html.d.ts.map +1 -1
  200. package/dist/utils/http-body.d.ts.map +1 -1
  201. package/dist/vault/index.d.ts.map +1 -1
  202. package/dist/vault/routing.d.ts.map +1 -1
  203. package/dist/vault/types.d.ts.map +1 -1
  204. package/dist/web/admin/portal.d.ts.map +1 -1
  205. package/dist/web/admin/portal.js +1 -1
  206. package/dist/web/admin/portal.js.map +1 -1
  207. package/dist/web/admin/provider-models.d.ts.map +1 -1
  208. package/dist/web/admin/store.d.ts.map +1 -1
  209. package/dist/web/admin/types.d.ts +1 -1
  210. package/dist/web/admin/types.d.ts.map +1 -1
  211. package/dist/web/admin/types.js.map +1 -1
  212. package/dist/web/agent-events/portal.d.ts +3 -0
  213. package/dist/web/agent-events/portal.d.ts.map +1 -0
  214. package/dist/web/agent-events/portal.js +18 -0
  215. package/dist/web/agent-events/portal.js.map +1 -0
  216. package/dist/web/login/oauth.d.ts.map +1 -1
  217. package/dist/web/login/oauth.js.map +1 -1
  218. package/dist/web/login/portal.d.ts +2 -14
  219. package/dist/web/login/portal.d.ts.map +1 -1
  220. package/dist/web/login/portal.js +53 -130
  221. package/dist/web/login/portal.js.map +1 -1
  222. package/dist/web/login/store.d.ts.map +1 -1
  223. package/dist/web/login/types.d.ts.map +1 -1
  224. package/dist/web/request.d.ts +3 -0
  225. package/dist/web/request.d.ts.map +1 -0
  226. package/dist/web/request.js +13 -0
  227. package/dist/web/request.js.map +1 -0
  228. package/dist/web/server.d.ts +28 -0
  229. package/dist/web/server.d.ts.map +1 -0
  230. package/dist/web/server.js +66 -0
  231. package/dist/web/server.js.map +1 -0
  232. package/dist/web/session-view/command.d.ts.map +1 -1
  233. package/dist/web/session-view/portal.d.ts.map +1 -1
  234. package/dist/web/session-view/portal.js +8 -8
  235. package/dist/web/session-view/portal.js.map +1 -1
  236. package/dist/web/session-view/service.d.ts.map +1 -1
  237. package/dist/web/session-view/store.d.ts.map +1 -1
  238. package/dist/web/session-view/types.d.ts +4 -4
  239. package/dist/web/session-view/types.d.ts.map +1 -1
  240. package/dist/web/session-view/types.js.map +1 -1
  241. package/dist/web/token-store.d.ts.map +1 -1
  242. package/dist/web/types.d.ts.map +1 -1
  243. package/package.json +44 -20
  244. package/dist/adapters/slack/tools/block-kit.d.ts +0 -16
  245. package/dist/adapters/slack/tools/block-kit.d.ts.map +0 -1
  246. package/dist/adapters/slack/tools/block-kit.js +0 -105
  247. package/dist/adapters/slack/tools/block-kit.js.map +0 -1
  248. package/dist/runtime/conversation-orchestrator.d.ts.map +0 -1
  249. package/dist/runtime/conversation-orchestrator.js.map +0 -1
  250. package/dist/runtime/session-runtime.d.ts +0 -4
  251. package/dist/runtime/session-runtime.d.ts.map +0 -1
  252. package/dist/runtime/session-runtime.js.map +0 -1
  253. package/dist/sandbox/credential-policy.d.ts +0 -2
  254. package/dist/sandbox/credential-policy.d.ts.map +0 -1
  255. package/dist/sandbox/credential-policy.js +0 -77
  256. package/dist/sandbox/credential-policy.js.map +0 -1
  257. package/dist/sessions/chat-session-manager.d.ts +0 -18
  258. package/dist/sessions/chat-session-manager.d.ts.map +0 -1
  259. package/dist/sessions/chat-session-manager.js.map +0 -1
  260. package/dist/tool-diagnostics.d.ts +0 -2
  261. package/dist/tool-diagnostics.d.ts.map +0 -1
  262. package/dist/tool-diagnostics.js +0 -7
  263. package/dist/tool-diagnostics.js.map +0 -1
package/dist/store.d.ts CHANGED
@@ -36,6 +36,9 @@ export declare class ChannelStore {
36
36
  * Returns null if no log exists
37
37
  */
38
38
  getLastTimestamp(channelId: string): string | null;
39
+ /**
40
+ * Download a single attachment
41
+ */
39
42
  private downloadAttachmentWithRetry;
40
43
  private downloadAttachment;
41
44
  }
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAUA,YAAY,EAAE,UAAU,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChF,OAAO,KAAK,EAAE,UAAU,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAgBhF,qBAAa,YAAY;IACvB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,QAAQ,CAAS;IAGzB,OAAO,CAAC,cAAc,CAA6B;IAEnD,YAAY,MAAM,EAAE,kBAAkB,EAMrC;IAED;;OAEG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAIvC;IAED;;OAEG;IACH,qBAAqB,CAAC,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAMrE;IAED;;;OAGG;IACG,kBAAkB,CACtB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,EACpF,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,UAAU,EAAE,CAAC,CA8BvB;IAED;;;OAGG;IACG,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,CA+B5E;IAED;;OAEG;IACG,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAS/E;IAED;;;OAGG;IACH,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAsBjD;YAKa,2BAA2B;YAQ3B,kBAAkB;CAuBjC","sourcesContent":["import { appendFile, writeFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport {\n ensureDirExists,\n isRecord,\n parseJsonValue,\n readTextFileIfExists,\n} from \"./utils/file-guards.js\";\nimport { withRetry } from \"./adapters/shared.js\";\n\nexport type { Attachment, ChannelStoreConfig, LoggedMessage } from \"./types.js\";\nimport type { Attachment, ChannelStoreConfig, LoggedMessage } from \"./types.js\";\n\nclass AttachmentDownloadHttpError extends Error {\n constructor(\n message: string,\n readonly status: number,\n ) {\n super(message);\n }\n}\n\nfunction isRetryableAttachmentDownloadError(error: unknown): boolean {\n if (!(error instanceof AttachmentDownloadHttpError)) return true;\n return error.status === 408 || error.status === 429 || error.status >= 500;\n}\n\nexport class ChannelStore {\n private workingDir: string;\n private botToken: string;\n // Track recently logged message timestamps to prevent duplicates\n // Key: \"channelId:ts\", automatically cleaned up after 60 seconds\n private recentlyLogged = new Map<string, number>();\n\n constructor(config: ChannelStoreConfig) {\n this.workingDir = config.workingDir;\n this.botToken = config.botToken;\n\n // Ensure working directory exists\n ensureDirExists(this.workingDir);\n }\n\n /**\n * Get or create the directory for a channel/DM\n */\n getChannelDir(channelId: string): string {\n const channelDir = join(this.workingDir, channelId);\n ensureDirExists(channelDir);\n return channelDir;\n }\n\n /**\n * Generate a unique local filename for an attachment\n */\n generateLocalFilename(originalName: string, timestamp: string): string {\n // Convert slack timestamp (1234567890.123456) to milliseconds\n const ts = Math.floor(parseFloat(timestamp) * 1000);\n // Sanitize original name (remove problematic characters)\n const sanitized = originalName.replace(/[^a-zA-Z0-9._-]/g, \"_\");\n return `${ts}_${sanitized}`;\n }\n\n /**\n * Process attachments from a Slack message event.\n * Downloads files before returning so callers only receive readable paths.\n */\n async processAttachments(\n channelId: string,\n files: Array<{ name?: string; url_private_download?: string; url_private?: string }>,\n timestamp: string,\n ): Promise<Attachment[]> {\n const downloads: Array<Promise<Attachment>> = [];\n\n for (const file of files) {\n const url = file.url_private_download || file.url_private;\n if (!url) continue;\n if (!file.name) {\n throw new Error(`Attachment missing name for URL: ${url}`);\n }\n\n const filename = this.generateLocalFilename(file.name, timestamp);\n const localPath = `${channelId}/attachments/${filename}`;\n const attachment: Attachment = {\n original: file.name,\n localPath,\n };\n\n downloads.push(\n this.downloadAttachmentWithRetry(localPath, url)\n .then(() => attachment)\n .catch((error) => {\n const errorMsg = error instanceof Error ? error.message : String(error);\n throw new Error(`Failed to download attachment ${localPath}: ${errorMsg}`, {\n cause: error,\n });\n }),\n );\n }\n\n return Promise.all(downloads);\n }\n\n /**\n * Log a message to the channel's log.jsonl\n * Returns false if message was already logged (duplicate)\n */\n async logMessage(channelId: string, message: LoggedMessage): Promise<boolean> {\n // Check for duplicate (same channel + timestamp)\n const dedupeKey = `${channelId}:${message.ts}`;\n if (this.recentlyLogged.has(dedupeKey)) {\n return false; // Already logged\n }\n\n const logPath = join(this.getChannelDir(channelId), \"log.jsonl\");\n\n // Ensure message has a date field\n if (!message.date) {\n // Parse timestamp to get date\n let date: Date;\n if (message.ts.includes(\".\")) {\n // Slack timestamp format (1234567890.123456)\n date = new Date(parseFloat(message.ts) * 1000);\n } else {\n // Epoch milliseconds\n date = new Date(parseInt(message.ts, 10));\n }\n message.date = date.toISOString();\n }\n\n const line = `${JSON.stringify(message)}\\n`;\n await appendFile(logPath, line, \"utf-8\");\n\n // Mark as logged only after the append succeeds. Otherwise a transient\n // write failure can make retries look like duplicates and drop messages.\n this.recentlyLogged.set(dedupeKey, Date.now());\n setTimeout(() => this.recentlyLogged.delete(dedupeKey), 60000);\n return true;\n }\n\n /**\n * Log a bot response\n */\n async logBotResponse(channelId: string, text: string, ts: string): Promise<void> {\n await this.logMessage(channelId, {\n date: new Date().toISOString(),\n ts,\n user: \"bot\",\n text,\n attachments: [],\n isBot: true,\n });\n }\n\n /**\n * Get the timestamp of the last logged message for a channel\n * Returns null if no log exists\n */\n getLastTimestamp(channelId: string): string | null {\n const logPath = join(this.workingDir, channelId, \"log.jsonl\");\n const content = readTextFileIfExists(logPath);\n if (content === undefined) {\n return null;\n }\n\n try {\n const lines = content.trim().split(\"\\n\");\n if (lines.length === 0 || lines[0] === \"\") {\n return null;\n }\n const lastLine = lines[lines.length - 1];\n const message = parseJsonValue(\n lastLine,\n (value): value is LoggedMessage => isRecord(value) && typeof value.ts === \"string\",\n (detail) => (detail === \"unexpected JSON shape\" ? \"log entry missing timestamp\" : detail),\n );\n return message.ts;\n } catch {\n return null;\n }\n }\n\n /**\n * Download a single attachment\n */\n private async downloadAttachmentWithRetry(localPath: string, url: string): Promise<void> {\n await withRetry(() => this.downloadAttachment(localPath, url), {\n maxAttempts: 3,\n baseDelayMs: 250,\n isRateLimited: isRetryableAttachmentDownloadError,\n });\n }\n\n private async downloadAttachment(localPath: string, url: string): Promise<void> {\n const filePath = join(this.workingDir, localPath);\n\n // Ensure directory exists\n const parentDir = join(this.workingDir, localPath.substring(0, localPath.lastIndexOf(\"/\")));\n ensureDirExists(parentDir);\n\n const response = await fetch(url, {\n headers: {\n Authorization: `Bearer ${this.botToken}`,\n },\n });\n\n if (!response.ok) {\n throw new AttachmentDownloadHttpError(\n `HTTP ${response.status}: ${response.statusText}`,\n response.status,\n );\n }\n\n const buffer = await response.arrayBuffer();\n await writeFile(filePath, Buffer.from(buffer));\n }\n}\n"]}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAUA,YAAY,EAAE,UAAU,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChF,OAAO,KAAK,EAAE,UAAU,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAgBhF,qBAAa,YAAY;IACvB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,QAAQ,CAAS;IAGzB,OAAO,CAAC,cAAc,CAA6B;IAEnD,YAAY,MAAM,EAAE,kBAAkB,EAMrC;IAED;;OAEG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAIvC;IAED;;OAEG;IACH,qBAAqB,CAAC,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAMrE;IAED;;;OAGG;IACG,kBAAkB,CACtB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,EACpF,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,UAAU,EAAE,CAAC,CA8BvB;IAED;;;OAGG;IACG,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,CA+B5E;IAED;;OAEG;IACG,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAS/E;IAED;;;OAGG;IACH,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAsBjD;IAED;;OAEG;YACW,2BAA2B;YAQ3B,kBAAkB;CAuBjC"}
package/dist/store.js CHANGED
@@ -114,7 +114,7 @@ export class ChannelStore {
114
114
  user: "bot",
115
115
  text,
116
116
  attachments: [],
117
- isBot: true,
117
+ isMessagingBot: true,
118
118
  });
119
119
  }
120
120
  /**
package/dist/store.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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","sourcesContent":["import { appendFile, writeFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport {\n ensureDirExists,\n isRecord,\n parseJsonValue,\n readTextFileIfExists,\n} from \"./utils/file-guards.js\";\nimport { withRetry } from \"./adapters/shared.js\";\n\nexport type { Attachment, ChannelStoreConfig, LoggedMessage } from \"./types.js\";\nimport type { Attachment, ChannelStoreConfig, LoggedMessage } from \"./types.js\";\n\nclass AttachmentDownloadHttpError extends Error {\n constructor(\n message: string,\n readonly status: number,\n ) {\n super(message);\n }\n}\n\nfunction isRetryableAttachmentDownloadError(error: unknown): boolean {\n if (!(error instanceof AttachmentDownloadHttpError)) return true;\n return error.status === 408 || error.status === 429 || error.status >= 500;\n}\n\nexport class ChannelStore {\n private workingDir: string;\n private botToken: string;\n // Track recently logged message timestamps to prevent duplicates\n // Key: \"channelId:ts\", automatically cleaned up after 60 seconds\n private recentlyLogged = new Map<string, number>();\n\n constructor(config: ChannelStoreConfig) {\n this.workingDir = config.workingDir;\n this.botToken = config.botToken;\n\n // Ensure working directory exists\n ensureDirExists(this.workingDir);\n }\n\n /**\n * Get or create the directory for a channel/DM\n */\n getChannelDir(channelId: string): string {\n const channelDir = join(this.workingDir, channelId);\n ensureDirExists(channelDir);\n return channelDir;\n }\n\n /**\n * Generate a unique local filename for an attachment\n */\n generateLocalFilename(originalName: string, timestamp: string): string {\n // Convert slack timestamp (1234567890.123456) to milliseconds\n const ts = Math.floor(parseFloat(timestamp) * 1000);\n // Sanitize original name (remove problematic characters)\n const sanitized = originalName.replace(/[^a-zA-Z0-9._-]/g, \"_\");\n return `${ts}_${sanitized}`;\n }\n\n /**\n * Process attachments from a Slack message event.\n * Downloads files before returning so callers only receive readable paths.\n */\n async processAttachments(\n channelId: string,\n files: Array<{ name?: string; url_private_download?: string; url_private?: string }>,\n timestamp: string,\n ): Promise<Attachment[]> {\n const downloads: Array<Promise<Attachment>> = [];\n\n for (const file of files) {\n const url = file.url_private_download || file.url_private;\n if (!url) continue;\n if (!file.name) {\n throw new Error(`Attachment missing name for URL: ${url}`);\n }\n\n const filename = this.generateLocalFilename(file.name, timestamp);\n const localPath = `${channelId}/attachments/${filename}`;\n const attachment: Attachment = {\n original: file.name,\n localPath,\n };\n\n downloads.push(\n this.downloadAttachmentWithRetry(localPath, url)\n .then(() => attachment)\n .catch((error) => {\n const errorMsg = error instanceof Error ? error.message : String(error);\n throw new Error(`Failed to download attachment ${localPath}: ${errorMsg}`, {\n cause: error,\n });\n }),\n );\n }\n\n return Promise.all(downloads);\n }\n\n /**\n * Log a message to the channel's log.jsonl\n * Returns false if message was already logged (duplicate)\n */\n async logMessage(channelId: string, message: LoggedMessage): Promise<boolean> {\n // Check for duplicate (same channel + timestamp)\n const dedupeKey = `${channelId}:${message.ts}`;\n if (this.recentlyLogged.has(dedupeKey)) {\n return false; // Already logged\n }\n\n const logPath = join(this.getChannelDir(channelId), \"log.jsonl\");\n\n // Ensure message has a date field\n if (!message.date) {\n // Parse timestamp to get date\n let date: Date;\n if (message.ts.includes(\".\")) {\n // Slack timestamp format (1234567890.123456)\n date = new Date(parseFloat(message.ts) * 1000);\n } else {\n // Epoch milliseconds\n date = new Date(parseInt(message.ts, 10));\n }\n message.date = date.toISOString();\n }\n\n const line = `${JSON.stringify(message)}\\n`;\n await appendFile(logPath, line, \"utf-8\");\n\n // Mark as logged only after the append succeeds. Otherwise a transient\n // write failure can make retries look like duplicates and drop messages.\n this.recentlyLogged.set(dedupeKey, Date.now());\n setTimeout(() => this.recentlyLogged.delete(dedupeKey), 60000);\n return true;\n }\n\n /**\n * Log a bot response\n */\n async logBotResponse(channelId: string, text: string, ts: string): Promise<void> {\n await this.logMessage(channelId, {\n date: new Date().toISOString(),\n ts,\n user: \"bot\",\n text,\n attachments: [],\n isBot: true,\n });\n }\n\n /**\n * Get the timestamp of the last logged message for a channel\n * Returns null if no log exists\n */\n getLastTimestamp(channelId: string): string | null {\n const logPath = join(this.workingDir, channelId, \"log.jsonl\");\n const content = readTextFileIfExists(logPath);\n if (content === undefined) {\n return null;\n }\n\n try {\n const lines = content.trim().split(\"\\n\");\n if (lines.length === 0 || lines[0] === \"\") {\n return null;\n }\n const lastLine = lines[lines.length - 1];\n const message = parseJsonValue(\n lastLine,\n (value): value is LoggedMessage => isRecord(value) && typeof value.ts === \"string\",\n (detail) => (detail === \"unexpected JSON shape\" ? \"log entry missing timestamp\" : detail),\n );\n return message.ts;\n } catch {\n return null;\n }\n }\n\n /**\n * Download a single attachment\n */\n private async downloadAttachmentWithRetry(localPath: string, url: string): Promise<void> {\n await withRetry(() => this.downloadAttachment(localPath, url), {\n maxAttempts: 3,\n baseDelayMs: 250,\n isRateLimited: isRetryableAttachmentDownloadError,\n });\n }\n\n private async downloadAttachment(localPath: string, url: string): Promise<void> {\n const filePath = join(this.workingDir, localPath);\n\n // Ensure directory exists\n const parentDir = join(this.workingDir, localPath.substring(0, localPath.lastIndexOf(\"/\")));\n ensureDirExists(parentDir);\n\n const response = await fetch(url, {\n headers: {\n Authorization: `Bearer ${this.botToken}`,\n },\n });\n\n if (!response.ok) {\n throw new AttachmentDownloadHttpError(\n `HTTP ${response.status}: ${response.statusText}`,\n response.status,\n );\n }\n\n const buffer = await response.arrayBuffer();\n await writeFile(filePath, Buffer.from(buffer));\n }\n}\n"]}
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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","sourcesContent":["import { appendFile, writeFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport {\n ensureDirExists,\n isRecord,\n parseJsonValue,\n readTextFileIfExists,\n} from \"./utils/file-guards.js\";\nimport { withRetry } from \"./adapters/shared.js\";\n\nexport type { Attachment, ChannelStoreConfig, LoggedMessage } from \"./types.js\";\nimport type { Attachment, ChannelStoreConfig, LoggedMessage } from \"./types.js\";\n\nclass AttachmentDownloadHttpError extends Error {\n constructor(\n message: string,\n readonly status: number,\n ) {\n super(message);\n }\n}\n\nfunction isRetryableAttachmentDownloadError(error: unknown): boolean {\n if (!(error instanceof AttachmentDownloadHttpError)) return true;\n return error.status === 408 || error.status === 429 || error.status >= 500;\n}\n\nexport class ChannelStore {\n private workingDir: string;\n private botToken: string;\n // Track recently logged message timestamps to prevent duplicates\n // Key: \"channelId:ts\", automatically cleaned up after 60 seconds\n private recentlyLogged = new Map<string, number>();\n\n constructor(config: ChannelStoreConfig) {\n this.workingDir = config.workingDir;\n this.botToken = config.botToken;\n\n // Ensure working directory exists\n ensureDirExists(this.workingDir);\n }\n\n /**\n * Get or create the directory for a channel/DM\n */\n getChannelDir(channelId: string): string {\n const channelDir = join(this.workingDir, channelId);\n ensureDirExists(channelDir);\n return channelDir;\n }\n\n /**\n * Generate a unique local filename for an attachment\n */\n generateLocalFilename(originalName: string, timestamp: string): string {\n // Convert slack timestamp (1234567890.123456) to milliseconds\n const ts = Math.floor(parseFloat(timestamp) * 1000);\n // Sanitize original name (remove problematic characters)\n const sanitized = originalName.replace(/[^a-zA-Z0-9._-]/g, \"_\");\n return `${ts}_${sanitized}`;\n }\n\n /**\n * Process attachments from a Slack message event.\n * Downloads files before returning so callers only receive readable paths.\n */\n async processAttachments(\n channelId: string,\n files: Array<{ name?: string; url_private_download?: string; url_private?: string }>,\n timestamp: string,\n ): Promise<Attachment[]> {\n const downloads: Array<Promise<Attachment>> = [];\n\n for (const file of files) {\n const url = file.url_private_download || file.url_private;\n if (!url) continue;\n if (!file.name) {\n throw new Error(`Attachment missing name for URL: ${url}`);\n }\n\n const filename = this.generateLocalFilename(file.name, timestamp);\n const localPath = `${channelId}/attachments/${filename}`;\n const attachment: Attachment = {\n original: file.name,\n localPath,\n };\n\n downloads.push(\n this.downloadAttachmentWithRetry(localPath, url)\n .then(() => attachment)\n .catch((error) => {\n const errorMsg = error instanceof Error ? error.message : String(error);\n throw new Error(`Failed to download attachment ${localPath}: ${errorMsg}`, {\n cause: error,\n });\n }),\n );\n }\n\n return Promise.all(downloads);\n }\n\n /**\n * Log a message to the channel's log.jsonl\n * Returns false if message was already logged (duplicate)\n */\n async logMessage(channelId: string, message: LoggedMessage): Promise<boolean> {\n // Check for duplicate (same channel + timestamp)\n const dedupeKey = `${channelId}:${message.ts}`;\n if (this.recentlyLogged.has(dedupeKey)) {\n return false; // Already logged\n }\n\n const logPath = join(this.getChannelDir(channelId), \"log.jsonl\");\n\n // Ensure message has a date field\n if (!message.date) {\n // Parse timestamp to get date\n let date: Date;\n if (message.ts.includes(\".\")) {\n // Slack timestamp format (1234567890.123456)\n date = new Date(parseFloat(message.ts) * 1000);\n } else {\n // Epoch milliseconds\n date = new Date(parseInt(message.ts, 10));\n }\n message.date = date.toISOString();\n }\n\n const line = `${JSON.stringify(message)}\\n`;\n await appendFile(logPath, line, \"utf-8\");\n\n // Mark as logged only after the append succeeds. Otherwise a transient\n // write failure can make retries look like duplicates and drop messages.\n this.recentlyLogged.set(dedupeKey, Date.now());\n setTimeout(() => this.recentlyLogged.delete(dedupeKey), 60000);\n return true;\n }\n\n /**\n * Log a bot response\n */\n async logBotResponse(channelId: string, text: string, ts: string): Promise<void> {\n await this.logMessage(channelId, {\n date: new Date().toISOString(),\n ts,\n user: \"bot\",\n text,\n attachments: [],\n isMessagingBot: true,\n });\n }\n\n /**\n * Get the timestamp of the last logged message for a channel\n * Returns null if no log exists\n */\n getLastTimestamp(channelId: string): string | null {\n const logPath = join(this.workingDir, channelId, \"log.jsonl\");\n const content = readTextFileIfExists(logPath);\n if (content === undefined) {\n return null;\n }\n\n try {\n const lines = content.trim().split(\"\\n\");\n if (lines.length === 0 || lines[0] === \"\") {\n return null;\n }\n const lastLine = lines[lines.length - 1];\n const message = parseJsonValue(\n lastLine,\n (value): value is LoggedMessage => isRecord(value) && typeof value.ts === \"string\",\n (detail) => (detail === \"unexpected JSON shape\" ? \"log entry missing timestamp\" : detail),\n );\n return message.ts;\n } catch {\n return null;\n }\n }\n\n /**\n * Download a single attachment\n */\n private async downloadAttachmentWithRetry(localPath: string, url: string): Promise<void> {\n await withRetry(() => this.downloadAttachment(localPath, url), {\n maxAttempts: 3,\n baseDelayMs: 250,\n isRateLimited: isRetryableAttachmentDownloadError,\n });\n }\n\n private async downloadAttachment(localPath: string, url: string): Promise<void> {\n const filePath = join(this.workingDir, localPath);\n\n // Ensure directory exists\n const parentDir = join(this.workingDir, localPath.substring(0, localPath.lastIndexOf(\"/\")));\n ensureDirExists(parentDir);\n\n const response = await fetch(url, {\n headers: {\n Authorization: `Bearer ${this.botToken}`,\n },\n });\n\n if (!response.ok) {\n throw new AttachmentDownloadHttpError(\n `HTTP ${response.status}: ${response.statusText}`,\n response.status,\n );\n }\n\n const buffer = await response.arrayBuffer();\n await writeFile(filePath, Buffer.from(buffer));\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"bash.d.ts","sourceRoot":"","sources":["../../src/tools/bash.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAE/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAiBpD,QAAA,MAAM,UAAU;;;;EAQd,CAAC;AAOH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,QAAQ,GAAG,SAAS,CAAC,OAAO,UAAU,CAAC,CAsE/E","sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport { createWriteStream } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { AgentTool } from \"@earendil-works/pi-agent-core\";\nimport { Type } from \"@sinclair/typebox\";\nimport type { Executor } from \"../sandbox/index.js\";\nimport {\n DEFAULT_MAX_BYTES,\n DEFAULT_MAX_LINES,\n formatSize,\n type TruncationResult,\n truncateTail,\n} from \"./truncate.js\";\n\n/**\n * Generate a unique temp file path for bash output\n */\nfunction getTempFilePath(): string {\n const id = randomBytes(8).toString(\"hex\");\n return join(tmpdir(), `mikan-bash-${id}.log`);\n}\n\nconst bashSchema = Type.Object({\n label: Type.String({\n description: \"Brief description of what this command does (shown to user)\",\n }),\n command: Type.String({ description: \"Bash command to execute\" }),\n timeout: Type.Optional(\n Type.Number({ description: \"Timeout in seconds (optional, no default timeout)\" }),\n ),\n});\n\ninterface BashToolDetails {\n truncation?: TruncationResult;\n fullOutputPath?: string;\n}\n\nexport function createBashTool(executor: Executor): AgentTool<typeof bashSchema> {\n return {\n name: \"bash\",\n label: \"bash\",\n description: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`,\n parameters: bashSchema,\n execute: async (\n _toolCallId: string,\n { command, timeout }: { label: string; command: string; timeout?: number },\n signal?: AbortSignal,\n ) => {\n // Track output for potential temp file writing\n let tempFilePath: string | undefined;\n let tempFileStream: ReturnType<typeof createWriteStream> | undefined;\n\n const result = await executor.exec(command, { timeout, signal });\n let output = \"\";\n if (result.stdout) output += result.stdout;\n if (result.stderr) {\n if (output) output += \"\\n\";\n output += result.stderr;\n }\n\n const totalBytes = Buffer.byteLength(output, \"utf-8\");\n\n // Write to temp file if output exceeds limit\n if (totalBytes > DEFAULT_MAX_BYTES) {\n tempFilePath = getTempFilePath();\n tempFileStream = createWriteStream(tempFilePath);\n tempFileStream.write(output);\n tempFileStream.end();\n }\n\n // Apply tail truncation\n const truncation = truncateTail(output);\n let outputText = truncation.content || \"(no output)\";\n\n // Build details with truncation info\n let details: BashToolDetails | undefined;\n\n if (truncation.truncated) {\n details = {\n truncation,\n fullOutputPath: tempFilePath,\n };\n\n // Build actionable notice\n const startLine = truncation.totalLines - truncation.outputLines + 1;\n const endLine = truncation.totalLines;\n\n if (truncation.lastLinePartial) {\n // Edge case: last line alone > 50KB\n const lastLineSize = formatSize(\n Buffer.byteLength(output.split(\"\\n\").pop() || \"\", \"utf-8\"),\n );\n outputText += `\\n\\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${tempFilePath}]`;\n } else if (truncation.truncatedBy === \"lines\") {\n outputText += `\\n\\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${tempFilePath}]`;\n } else {\n outputText += `\\n\\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${tempFilePath}]`;\n }\n }\n\n if (result.code !== 0) {\n throw new Error(`${outputText}\\n\\nCommand exited with code ${result.code}`.trim());\n }\n\n return { content: [{ type: \"text\", text: outputText }], details };\n },\n };\n}\n"]}
1
+ {"version":3,"file":"bash.d.ts","sourceRoot":"","sources":["../../src/tools/bash.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAE/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAiBpD,QAAA,MAAM,UAAU;;;;EAQd,CAAC;AAOH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,QAAQ,GAAG,SAAS,CAAC,OAAO,UAAU,CAAC,CAsE/E"}
@@ -1 +1 @@
1
- {"version":3,"file":"edit.d.ts","sourceRoot":"","sources":["../../src/tools/edit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAG/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAqFpD,QAAA,MAAM,UAAU;;;;;EAOd,CAAC;AAEH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,QAAQ,GAAG,SAAS,CAAC,OAAO,UAAU,CAAC,CAqE/E","sourcesContent":["import type { AgentTool } from \"@earendil-works/pi-agent-core\";\nimport { Type } from \"@sinclair/typebox\";\nimport * as Diff from \"diff\";\nimport type { Executor } from \"../sandbox/index.js\";\n\n/**\n * Generate a unified diff string with line numbers and context\n */\nfunction generateDiffString(oldContent: string, newContent: string, contextLines = 4): string {\n const parts = Diff.diffLines(oldContent, newContent);\n const output: string[] = [];\n\n const oldLines = oldContent.split(\"\\n\");\n const newLines = newContent.split(\"\\n\");\n const maxLineNum = Math.max(oldLines.length, newLines.length);\n const lineNumWidth = String(maxLineNum).length;\n\n let oldLineNum = 1;\n let newLineNum = 1;\n let lastWasChange = false;\n\n for (let i = 0; i < parts.length; i++) {\n const part = parts[i];\n const raw = part.value.split(\"\\n\");\n if (raw[raw.length - 1] === \"\") {\n raw.pop();\n }\n\n if (part.added || part.removed) {\n for (const line of raw) {\n if (part.added) {\n const lineNum = String(newLineNum).padStart(lineNumWidth, \" \");\n output.push(`+${lineNum} ${line}`);\n newLineNum++;\n } else {\n const lineNum = String(oldLineNum).padStart(lineNumWidth, \" \");\n output.push(`-${lineNum} ${line}`);\n oldLineNum++;\n }\n }\n lastWasChange = true;\n } else {\n const nextPartIsChange = i < parts.length - 1 && (parts[i + 1].added || parts[i + 1].removed);\n\n if (lastWasChange || nextPartIsChange) {\n let linesToShow = raw;\n let skipStart = 0;\n let skipEnd = 0;\n\n if (!lastWasChange) {\n skipStart = Math.max(0, raw.length - contextLines);\n linesToShow = raw.slice(skipStart);\n }\n\n if (!nextPartIsChange && linesToShow.length > contextLines) {\n skipEnd = linesToShow.length - contextLines;\n linesToShow = linesToShow.slice(0, contextLines);\n }\n\n if (skipStart > 0) {\n output.push(` ${\"\".padStart(lineNumWidth, \" \")} ...`);\n }\n\n for (const line of linesToShow) {\n const lineNum = String(oldLineNum).padStart(lineNumWidth, \" \");\n output.push(` ${lineNum} ${line}`);\n oldLineNum++;\n newLineNum++;\n }\n\n if (skipEnd > 0) {\n output.push(` ${\"\".padStart(lineNumWidth, \" \")} ...`);\n }\n\n oldLineNum += skipStart + skipEnd;\n newLineNum += skipStart + skipEnd;\n } else {\n oldLineNum += raw.length;\n newLineNum += raw.length;\n }\n\n lastWasChange = false;\n }\n }\n\n return output.join(\"\\n\");\n}\n\nconst editSchema = Type.Object({\n label: Type.String({\n description: \"Brief description of the edit you're making (shown to user)\",\n }),\n path: Type.String({ description: \"Path to the file to edit (relative or absolute)\" }),\n oldText: Type.String({ description: \"Exact text to find and replace (must match exactly)\" }),\n newText: Type.String({ description: \"New text to replace the old text with\" }),\n});\n\nexport function createEditTool(executor: Executor): AgentTool<typeof editSchema> {\n return {\n name: \"edit\",\n label: \"edit\",\n description:\n \"Edit a file by replacing exact text. The oldText must match exactly (including whitespace). Use this for precise, surgical edits.\",\n parameters: editSchema,\n execute: async (\n _toolCallId: string,\n { path, oldText, newText }: { label: string; path: string; oldText: string; newText: string },\n signal?: AbortSignal,\n ) => {\n // Read the file\n const readResult = await executor.exec(`cat ${shellEscape(path)}`, { signal });\n if (readResult.code !== 0) {\n throw new Error(readResult.stderr || `File not found: ${path}`);\n }\n\n const content = readResult.stdout;\n\n // Check if old text exists\n if (!content.includes(oldText)) {\n throw new Error(\n `Could not find the exact text in ${path}. The old text must match exactly including all whitespace and newlines.`,\n );\n }\n\n // Count occurrences\n const occurrences = content.split(oldText).length - 1;\n\n if (occurrences > 1) {\n throw new Error(\n `Found ${occurrences} occurrences of the text in ${path}. The text must be unique. Please provide more context to make it unique.`,\n );\n }\n\n // Perform replacement\n const index = content.indexOf(oldText);\n const newContent =\n content.substring(0, index) + newText + content.substring(index + oldText.length);\n\n if (content === newContent) {\n throw new Error(\n `No changes made to ${path}. The replacement produced identical content. This might indicate an issue with special characters or the text not existing as expected.`,\n );\n }\n\n // Write the file back\n const writeResult = await executor.exec(\n `printf '%s' ${shellEscape(newContent)} > ${shellEscape(path)}`,\n {\n signal,\n },\n );\n if (writeResult.code !== 0) {\n throw new Error(writeResult.stderr || `Failed to write file: ${path}`);\n }\n\n return {\n content: [\n {\n type: \"text\",\n text: `Successfully replaced text in ${path}. Changed ${oldText.length} characters to ${newText.length} characters.`,\n },\n ],\n details: { diff: generateDiffString(content, newContent) },\n };\n },\n };\n}\n\nfunction shellEscape(s: string): string {\n return `'${s.replace(/'/g, \"'\\\\''\")}'`;\n}\n"]}
1
+ {"version":3,"file":"edit.d.ts","sourceRoot":"","sources":["../../src/tools/edit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAG/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAqFpD,QAAA,MAAM,UAAU;;;;;EAOd,CAAC;AAEH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,QAAQ,GAAG,SAAS,CAAC,OAAO,UAAU,CAAC,CAqE/E"}
@@ -1 +1 @@
1
- {"version":3,"file":"event.d.ts","sourceRoot":"","sources":["../../src/tools/event.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAE/D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAGtD,QAAA,MAAM,WAAW;;;;;;;;;;;EA0Df,CAAC;AAEH,UAAU,gBAAgB;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,MAAM,EAAE,MAAM,CAAC;CAChB;AAeD,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE3D,qBAAa,cAAe,YAAW,UAAU;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS;IAAtC,YAA6B,SAAS,EAAE,MAAM,EAAI;IAElD,MAAM,CAAC,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,cAAc,CAE5D;IAEK,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAE5F;IAEK,IAAI,IAAI,OAAO,CACnB,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,YAAY,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAClF,CASA;IAEK,IAAI,CACR,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,YAAY,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAWrF;IAEK,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAE7F;IAEK,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAI5D;YAEa,YAAY;CAiB3B;AAED,wBAAgB,eAAe,CAAC,UAAU,EAAE,UAAU,GAAG;IACvD,IAAI,EAAE,SAAS,CAAC,OAAO,WAAW,CAAC,CAAC;IACpC,eAAe,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,IAAI,CAAC;CACtD,CAuGA","sourcesContent":["import { constants } from \"node:fs\";\nimport { mkdir, open, readdir, readFile, rm, stat, writeFile } from \"node:fs/promises\";\nimport { basename, join } from \"node:path\";\nimport type { AgentTool } from \"@earendil-works/pi-agent-core\";\nimport { Type } from \"@sinclair/typebox\";\nimport type { ConversationKind } from \"../adapter.js\";\nimport * as log from \"../log.js\";\n\nconst eventSchema = Type.Object({\n action: Type.Optional(\n Type.Union(\n [\n Type.Literal(\"create\"),\n Type.Literal(\"list\"),\n Type.Literal(\"read\"),\n Type.Literal(\"update\"),\n Type.Literal(\"delete\"),\n ],\n { description: \"CRUD action. Defaults to create for backward compatibility.\" },\n ),\n ),\n filename: Type.Optional(\n Type.String({\n description: \"Event filename for read, update, or delete actions\",\n }),\n ),\n scope: Type.Optional(\n Type.Union([Type.Literal(\"conversation\"), Type.Literal(\"all\")], {\n description:\n \"List scope. Defaults to conversation, which only shows events for the current conversation. Use all only when the user explicitly asks for all events.\",\n }),\n ),\n label: Type.Optional(\n Type.String({\n description: \"Brief description of the event you're scheduling (shown to user)\",\n }),\n ),\n type: Type.Optional(\n Type.Union([Type.Literal(\"immediate\"), Type.Literal(\"one-shot\"), Type.Literal(\"periodic\")]),\n ),\n text: Type.Optional(\n Type.String({\n description:\n \"A self-contained task for the future run. Include the necessary context, tone, and constraints in the text itself because events do not inherit normal conversation history.\",\n }),\n ),\n at: Type.Optional(\n Type.String({\n description: \"ISO 8601 timestamp with offset, required for one-shot events\",\n }),\n ),\n schedule: Type.Optional(\n Type.String({\n description: \"Cron schedule, required for periodic events\",\n }),\n ),\n timezone: Type.Optional(\n Type.String({\n description: \"IANA timezone, required for periodic events\",\n }),\n ),\n filenamePrefix: Type.Optional(\n Type.String({\n description: \"Optional filename prefix for the event file\",\n }),\n ),\n});\n\ninterface EventToolContext {\n platform: string;\n conversationId: string;\n conversationKind: ConversationKind;\n userId: string;\n}\n\ntype EventToolParams = {\n action?: \"create\" | \"list\" | \"read\" | \"update\" | \"delete\";\n filename?: string;\n scope?: \"conversation\" | \"all\";\n label?: string;\n type?: \"immediate\" | \"one-shot\" | \"periodic\";\n text?: string;\n at?: string;\n schedule?: string;\n timezone?: string;\n filenamePrefix?: string;\n};\n\nexport type { EventPayload, EventStore } from \"./types.js\";\nimport type { EventPayload, EventStore } from \"./types.js\";\n\nexport class HostEventStore implements EventStore {\n constructor(private readonly eventsDir: string) {}\n\n static fromWorkspaceDir(workspaceDir: string): HostEventStore {\n return new HostEventStore(join(workspaceDir, \"events\"));\n }\n\n async write(filename: string, payload: EventPayload): Promise<{ path: string; size: number }> {\n return this.writePayload(filename, payload);\n }\n\n async list(): Promise<\n Array<{ filename: string; payload: EventPayload; size: number; mtimeMs: number }>\n > {\n await mkdir(this.eventsDir, { recursive: true });\n const entries = await readdir(this.eventsDir, { withFileTypes: true });\n const events = await Promise.all(\n entries\n .filter((entry) => entry.isFile() && entry.name.endsWith(\".json\"))\n .map(async (entry) => this.read(entry.name)),\n );\n return events.toSorted((a, b) => a.filename.localeCompare(b.filename));\n }\n\n async read(\n filename: string,\n ): Promise<{ filename: string; payload: EventPayload; size: number; mtimeMs: number }> {\n const safeFilename = validateEventFilename(filename);\n const filePath = join(this.eventsDir, safeFilename);\n const [raw, fileStat] = await Promise.all([readFile(filePath, \"utf-8\"), stat(filePath)]);\n const parsed = JSON.parse(raw) as EventPayload;\n return {\n filename: safeFilename,\n payload: parsed,\n size: fileStat.size,\n mtimeMs: fileStat.mtimeMs,\n };\n }\n\n async update(filename: string, payload: EventPayload): Promise<{ path: string; size: number }> {\n return this.writePayload(filename, payload, constants.O_WRONLY | constants.O_TRUNC);\n }\n\n async delete(filename: string): Promise<{ deleted: boolean }> {\n const safeFilename = validateEventFilename(filename);\n await rm(join(this.eventsDir, safeFilename), { force: true });\n return { deleted: true };\n }\n\n private async writePayload(\n filename: string,\n payload: EventPayload,\n flag?: string | number,\n ): Promise<{ path: string; size: number }> {\n await mkdir(this.eventsDir, { recursive: true });\n const safeFilename = validateEventFilename(filename);\n const filePath = join(this.eventsDir, safeFilename);\n if (flag === undefined) {\n await writeFile(filePath, JSON.stringify(payload) + \"\\n\", \"utf-8\");\n } else {\n await using file = await open(filePath, flag);\n await file.writeFile(JSON.stringify(payload) + \"\\n\", \"utf-8\");\n }\n const fileStat = await stat(filePath);\n return { path: filePath, size: fileStat.size };\n }\n}\n\nexport function createEventTool(eventStore: EventStore): {\n tool: AgentTool<typeof eventSchema>;\n setEventContext: (context: EventToolContext) => void;\n} {\n let eventContext: EventToolContext | null = null;\n\n const tool: AgentTool<typeof eventSchema> = {\n name: \"event\",\n label: \"event\",\n description:\n \"CRUD tool for scheduled events. Create immediate, one-shot, or periodic events for the current conversation. List defaults to events for the current conversation only; use scope=all only when the user explicitly asks to list all events. Event text must be self-contained because events do not inherit normal conversation history.\",\n parameters: eventSchema,\n execute: async (_toolCallId: string, params: EventToolParams, signal?: AbortSignal) => {\n if (signal?.aborted) {\n throw new Error(\"Operation aborted\");\n }\n\n if (!eventContext) {\n throw new Error(\"Event context not configured\");\n }\n\n const action = params.action ?? \"create\";\n\n if (action === \"list\") {\n const events = await eventStore.list();\n const conversationId = eventContext.conversationId;\n const scopedEvents =\n params.scope === \"all\"\n ? events\n : events.filter((event) => event.payload.conversationId === conversationId);\n return {\n content: [\n {\n type: \"text\",\n text: JSON.stringify(\n {\n scope: params.scope ?? \"conversation\",\n conversationId: params.scope === \"all\" ? undefined : conversationId,\n events: scopedEvents,\n },\n null,\n 2,\n ),\n },\n ],\n details: undefined,\n };\n }\n\n if (action === \"read\") {\n const filename = requireFilename(params);\n const event = await eventStore.read(filename);\n return {\n content: [{ type: \"text\", text: JSON.stringify(event, null, 2) }],\n details: undefined,\n };\n }\n\n if (action === \"delete\") {\n const filename = requireFilename(params);\n await eventStore.delete(filename);\n return {\n content: [{ type: \"text\", text: `Deleted event ${filename}` }],\n details: undefined,\n };\n }\n\n const payload = buildEventPayload(params, eventContext);\n const filename =\n action === \"update\"\n ? requireFilename(params)\n : `${sanitizeFileSegment(params.filenamePrefix || payload.type || \"event\")}-${Date.now()}.json`;\n\n log.logInfo(\n `${action === \"update\" ? \"Updating\" : \"Writing\"} event file via control plane store: ${filename} (type=${payload.type}, platform=${payload.platform}, conversation=${payload.conversationId})`,\n );\n\n try {\n const result =\n action === \"update\"\n ? await eventStore.update(filename, payload)\n : await eventStore.write(filename, payload);\n log.logInfo(\n `${action === \"update\" ? \"Updated\" : \"Wrote\"} event file via control plane store: ${result.path} (${result.size} bytes)`,\n );\n } catch (err) {\n log.logWarning(\n `Failed to ${action === \"update\" ? \"update\" : \"write\"} event file via control plane store: ${filename}`,\n String(err),\n );\n throw err;\n }\n\n return {\n content: [{ type: \"text\", text: formatEventWriteResult(action, filename, payload) }],\n details: undefined,\n };\n },\n };\n\n return {\n tool,\n setEventContext: (context: EventToolContext) => {\n eventContext = context;\n },\n };\n}\n\nfunction buildEventPayload(params: EventToolParams, context: EventToolContext): EventPayload {\n if (!params.type) {\n throw new Error(\"`type` is required for create and update actions\");\n }\n if (!params.text) {\n throw new Error(\"`text` is required for create and update actions\");\n }\n\n const base = {\n platform: context.platform,\n conversationId: context.conversationId,\n conversationKind: context.conversationKind,\n userId: context.userId,\n text: params.text,\n };\n\n if (params.type === \"immediate\") {\n return {\n ...base,\n type: \"immediate\",\n };\n }\n\n if (params.type === \"one-shot\") {\n if (!params.at) {\n throw new Error(\"`at` is required for one-shot events\");\n }\n\n const atTime = new Date(params.at).getTime();\n if (Number.isNaN(atTime)) {\n throw new Error(\"`at` must be a valid ISO 8601 timestamp with UTC offset\");\n }\n if (atTime <= Date.now()) {\n throw new Error(\n `\\`at\\` must be in the future; got ${params.at} (now=${new Date().toISOString()}). Check the timezone offset.`,\n );\n }\n\n // No sessionKey or threadTs: reminders should fire as top-level messages, not buried in old threads\n return { ...base, type: \"one-shot\", at: params.at };\n }\n\n if (!params.schedule) {\n throw new Error(\"`schedule` is required for periodic events\");\n }\n if (!params.timezone) {\n throw new Error(\"`timezone` is required for periodic events\");\n }\n return {\n ...base,\n type: \"periodic\",\n schedule: params.schedule,\n timezone: params.timezone,\n };\n}\n\nfunction formatEventWriteResult(\n action: \"create\" | \"update\",\n filename: string,\n payload: EventPayload,\n): string {\n const scheduledVerb = action === \"update\" ? \"Updated\" : \"Scheduled\";\n const immediateVerb = action === \"update\" ? \"Updated\" : \"Queued\";\n switch (payload.type) {\n case \"periodic\":\n return `${scheduledVerb} periodic event ${filename} for ${payload.platform}/${payload.conversationId} (${payload.schedule} ${payload.timezone})`;\n case \"one-shot\":\n return `${scheduledVerb} one-shot event ${filename} for ${payload.platform}/${payload.conversationId} at ${payload.at}`;\n case \"immediate\":\n return `${immediateVerb} immediate event ${filename} for ${payload.platform}/${payload.conversationId}`;\n }\n}\n\nfunction requireFilename(params: EventToolParams): string {\n if (!params.filename) {\n throw new Error(\"`filename` is required for read, update, and delete actions\");\n }\n return validateEventFilename(params.filename);\n}\n\nfunction validateEventFilename(filename: string): string {\n const trimmed = filename.trim();\n if (\n !trimmed ||\n trimmed !== basename(trimmed) ||\n trimmed.includes(\"..\") ||\n !trimmed.endsWith(\".json\")\n ) {\n throw new Error(\"Invalid event filename\");\n }\n return trimmed;\n}\n\nfunction sanitizeFileSegment(value: string): string {\n const sanitized = value\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9._-]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n return sanitized || \"event\";\n}\n"]}
1
+ {"version":3,"file":"event.d.ts","sourceRoot":"","sources":["../../src/tools/event.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAE/D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAGtD,QAAA,MAAM,WAAW;;;;;;;;;;;EA0Df,CAAC;AAEH,UAAU,gBAAgB;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,MAAM,EAAE,MAAM,CAAC;CAChB;AAeD,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE3D,qBAAa,cAAe,YAAW,UAAU;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS;IAAtC,YAA6B,SAAS,EAAE,MAAM,EAAI;IAElD,MAAM,CAAC,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,cAAc,CAE5D;IAEK,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAE5F;IAEK,IAAI,IAAI,OAAO,CACnB,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,YAAY,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAClF,CASA;IAEK,IAAI,CACR,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,YAAY,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAWrF;IAEK,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAE7F;IAEK,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAI5D;YAEa,YAAY;CAiB3B;AAED,wBAAgB,eAAe,CAAC,UAAU,EAAE,UAAU,GAAG;IACvD,IAAI,EAAE,SAAS,CAAC,OAAO,WAAW,CAAC,CAAC;IACpC,eAAe,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,IAAI,CAAC;CACtD,CAuGA"}
@@ -1,6 +1,6 @@
1
1
  import type { AgentTool } from "@earendil-works/pi-agent-core";
2
2
  import type { TSchema } from "@sinclair/typebox";
3
- import type { ChatResponseBlockKit, ConversationKind } from "../adapter.js";
3
+ import type { ConversationKind } from "../adapter.js";
4
4
  import type { DockerContainerManager } from "../provisioner.js";
5
5
  import type { Executor, SandboxConfig } from "../sandbox/index.js";
6
6
  export declare function createMikanTools(executor: Executor, workspaceDir: string, sandboxController?: {
@@ -9,7 +9,6 @@ export declare function createMikanTools(executor: Executor, workspaceDir: strin
9
9
  }): {
10
10
  tools: AgentTool<TSchema>[];
11
11
  setUploadFunction: (fn: (filePath: string, title?: string) => Promise<void>) => void;
12
- setBlockKitResponseFunction: (fn: (response: ChatResponseBlockKit) => Promise<void>) => void;
13
12
  setEventContext: (context: {
14
13
  platform: string;
15
14
  conversationId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,KAAK,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAG5E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAQnE,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,QAAQ,EAClB,YAAY,EAAE,MAAM,EACpB,iBAAiB,CAAC,EAAE;IAClB,OAAO,EAAE,aAAa,CAAC;IACvB,WAAW,CAAC,EAAE,IAAI,CAAC,sBAAsB,EAAE,gBAAgB,GAAG,WAAW,CAAC,CAAC;CAC5E,GACA;IACD,KAAK,EAAE,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC;IAC5B,iBAAiB,EAAE,CAAC,EAAE,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC;IACrF,2BAA2B,EAAE,CAAC,EAAE,EAAE,CAAC,QAAQ,EAAE,oBAAoB,KAAK,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC;IAC7F,eAAe,EAAE,CAAC,OAAO,EAAE;QACzB,QAAQ,EAAE,MAAM,CAAC;QACjB,cAAc,EAAE,MAAM,CAAC;QACvB,gBAAgB,EAAE,gBAAgB,CAAC;QACnC,MAAM,EAAE,MAAM,CAAC;KAChB,KAAK,IAAI,CAAC;IACX,iBAAiB,EAAE,CAAC,OAAO,EAAE;QAAE,cAAc,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CAClF,CAyBA","sourcesContent":["import type { AgentTool } from \"@earendil-works/pi-agent-core\";\nimport type { TSchema } from \"@sinclair/typebox\";\nimport type { ChatResponseBlockKit, ConversationKind } from \"../adapter.js\";\nimport { createAttachTool } from \"../adapters/slack/tools/attach.js\";\nimport { createSlackBlockKitTool } from \"../adapters/slack/tools/block-kit.js\";\nimport type { DockerContainerManager } from \"../provisioner.js\";\nimport type { Executor, SandboxConfig } from \"../sandbox/index.js\";\nimport { createBashTool } from \"./bash.js\";\nimport { createEditTool } from \"./edit.js\";\nimport { createEventTool, HostEventStore } from \"./event.js\";\nimport { createReadTool } from \"./read.js\";\nimport { createSandboxTool } from \"./sandbox.js\";\nimport { createWriteTool } from \"./write.js\";\n\nexport function createMikanTools(\n executor: Executor,\n workspaceDir: string,\n sandboxController?: {\n sandbox: SandboxConfig;\n provisioner?: Pick<DockerContainerManager, \"getLimitStatus\" | \"setLimits\">;\n },\n): {\n tools: AgentTool<TSchema>[];\n setUploadFunction: (fn: (filePath: string, title?: string) => Promise<void>) => void;\n setBlockKitResponseFunction: (fn: (response: ChatResponseBlockKit) => Promise<void>) => void;\n setEventContext: (context: {\n platform: string;\n conversationId: string;\n conversationKind: ConversationKind;\n userId: string;\n }) => void;\n setSandboxContext: (context: { conversationId: string; userId: string }) => void;\n} {\n const { tool: attachTool, setUploadFunction } = createAttachTool();\n const { tool: slackBlockKitTool, setBlockKitResponseFunction } = createSlackBlockKitTool();\n const { tool: eventTool, setEventContext } = createEventTool(\n HostEventStore.fromWorkspaceDir(workspaceDir),\n );\n const { tool: sandboxTool, setSandboxContext } = createSandboxTool(\n sandboxController ?? { sandbox: executor.getSandboxConfig() },\n );\n return {\n tools: [\n createReadTool(executor),\n createBashTool(executor),\n createEditTool(executor),\n createWriteTool(executor),\n eventTool,\n sandboxTool,\n attachTool,\n slackBlockKitTool,\n ],\n setUploadFunction,\n setBlockKitResponseFunction,\n setEventContext,\n setSandboxContext,\n };\n}\n"]}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAQnE,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,QAAQ,EAClB,YAAY,EAAE,MAAM,EACpB,iBAAiB,CAAC,EAAE;IAClB,OAAO,EAAE,aAAa,CAAC;IACvB,WAAW,CAAC,EAAE,IAAI,CAAC,sBAAsB,EAAE,gBAAgB,GAAG,WAAW,CAAC,CAAC;CAC5E,GACA;IACD,KAAK,EAAE,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC;IAC5B,iBAAiB,EAAE,CAAC,EAAE,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC;IACrF,eAAe,EAAE,CAAC,OAAO,EAAE;QACzB,QAAQ,EAAE,MAAM,CAAC;QACjB,cAAc,EAAE,MAAM,CAAC;QACvB,gBAAgB,EAAE,gBAAgB,CAAC;QACnC,MAAM,EAAE,MAAM,CAAC;KAChB,KAAK,IAAI,CAAC;IACX,iBAAiB,EAAE,CAAC,OAAO,EAAE;QAAE,cAAc,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CAClF,CAsBA"}
@@ -1,5 +1,4 @@
1
1
  import { createAttachTool } from "../adapters/slack/tools/attach.js";
2
- import { createSlackBlockKitTool } from "../adapters/slack/tools/block-kit.js";
3
2
  import { createBashTool } from "./bash.js";
4
3
  import { createEditTool } from "./edit.js";
5
4
  import { createEventTool, HostEventStore } from "./event.js";
@@ -8,7 +7,6 @@ import { createSandboxTool } from "./sandbox.js";
8
7
  import { createWriteTool } from "./write.js";
9
8
  export function createMikanTools(executor, workspaceDir, sandboxController) {
10
9
  const { tool: attachTool, setUploadFunction } = createAttachTool();
11
- const { tool: slackBlockKitTool, setBlockKitResponseFunction } = createSlackBlockKitTool();
12
10
  const { tool: eventTool, setEventContext } = createEventTool(HostEventStore.fromWorkspaceDir(workspaceDir));
13
11
  const { tool: sandboxTool, setSandboxContext } = createSandboxTool(sandboxController ?? { sandbox: executor.getSandboxConfig() });
14
12
  return {
@@ -20,10 +18,8 @@ export function createMikanTools(executor, workspaceDir, sandboxController) {
20
18
  eventTool,
21
19
  sandboxTool,
22
20
  attachTool,
23
- slackBlockKitTool,
24
21
  ],
25
22
  setUploadFunction,
26
- setBlockKitResponseFunction,
27
23
  setEventContext,
28
24
  setSandboxContext,
29
25
  };
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AACrE,OAAO,EAAE,uBAAuB,EAAE,MAAM,sCAAsC,CAAC;AAG/E,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC7D,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE7C,MAAM,UAAU,gBAAgB,CAC9B,QAAkB,EAClB,YAAoB,EACpB,iBAGC;IAaD,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,iBAAiB,EAAE,GAAG,gBAAgB,EAAE,CAAC;IACnE,MAAM,EAAE,IAAI,EAAE,iBAAiB,EAAE,2BAA2B,EAAE,GAAG,uBAAuB,EAAE,CAAC;IAC3F,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,eAAe,EAAE,GAAG,eAAe,CAC1D,cAAc,CAAC,gBAAgB,CAAC,YAAY,CAAC,CAC9C,CAAC;IACF,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,iBAAiB,EAAE,GAAG,iBAAiB,CAChE,iBAAiB,IAAI,EAAE,OAAO,EAAE,QAAQ,CAAC,gBAAgB,EAAE,EAAE,CAC9D,CAAC;IACF,OAAO;QACL,KAAK,EAAE;YACL,cAAc,CAAC,QAAQ,CAAC;YACxB,cAAc,CAAC,QAAQ,CAAC;YACxB,cAAc,CAAC,QAAQ,CAAC;YACxB,eAAe,CAAC,QAAQ,CAAC;YACzB,SAAS;YACT,WAAW;YACX,UAAU;YACV,iBAAiB;SAClB;QACD,iBAAiB;QACjB,2BAA2B;QAC3B,eAAe;QACf,iBAAiB;KAClB,CAAC;AACJ,CAAC","sourcesContent":["import type { AgentTool } from \"@earendil-works/pi-agent-core\";\nimport type { TSchema } from \"@sinclair/typebox\";\nimport type { ChatResponseBlockKit, ConversationKind } from \"../adapter.js\";\nimport { createAttachTool } from \"../adapters/slack/tools/attach.js\";\nimport { createSlackBlockKitTool } from \"../adapters/slack/tools/block-kit.js\";\nimport type { DockerContainerManager } from \"../provisioner.js\";\nimport type { Executor, SandboxConfig } from \"../sandbox/index.js\";\nimport { createBashTool } from \"./bash.js\";\nimport { createEditTool } from \"./edit.js\";\nimport { createEventTool, HostEventStore } from \"./event.js\";\nimport { createReadTool } from \"./read.js\";\nimport { createSandboxTool } from \"./sandbox.js\";\nimport { createWriteTool } from \"./write.js\";\n\nexport function createMikanTools(\n executor: Executor,\n workspaceDir: string,\n sandboxController?: {\n sandbox: SandboxConfig;\n provisioner?: Pick<DockerContainerManager, \"getLimitStatus\" | \"setLimits\">;\n },\n): {\n tools: AgentTool<TSchema>[];\n setUploadFunction: (fn: (filePath: string, title?: string) => Promise<void>) => void;\n setBlockKitResponseFunction: (fn: (response: ChatResponseBlockKit) => Promise<void>) => void;\n setEventContext: (context: {\n platform: string;\n conversationId: string;\n conversationKind: ConversationKind;\n userId: string;\n }) => void;\n setSandboxContext: (context: { conversationId: string; userId: string }) => void;\n} {\n const { tool: attachTool, setUploadFunction } = createAttachTool();\n const { tool: slackBlockKitTool, setBlockKitResponseFunction } = createSlackBlockKitTool();\n const { tool: eventTool, setEventContext } = createEventTool(\n HostEventStore.fromWorkspaceDir(workspaceDir),\n );\n const { tool: sandboxTool, setSandboxContext } = createSandboxTool(\n sandboxController ?? { sandbox: executor.getSandboxConfig() },\n );\n return {\n tools: [\n createReadTool(executor),\n createBashTool(executor),\n createEditTool(executor),\n createWriteTool(executor),\n eventTool,\n sandboxTool,\n attachTool,\n slackBlockKitTool,\n ],\n setUploadFunction,\n setBlockKitResponseFunction,\n setEventContext,\n setSandboxContext,\n };\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAGrE,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC7D,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE7C,MAAM,UAAU,gBAAgB,CAC9B,QAAkB,EAClB,YAAoB,EACpB,iBAGC;IAYD,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,iBAAiB,EAAE,GAAG,gBAAgB,EAAE,CAAC;IACnE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,eAAe,EAAE,GAAG,eAAe,CAC1D,cAAc,CAAC,gBAAgB,CAAC,YAAY,CAAC,CAC9C,CAAC;IACF,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,iBAAiB,EAAE,GAAG,iBAAiB,CAChE,iBAAiB,IAAI,EAAE,OAAO,EAAE,QAAQ,CAAC,gBAAgB,EAAE,EAAE,CAC9D,CAAC;IACF,OAAO;QACL,KAAK,EAAE;YACL,cAAc,CAAC,QAAQ,CAAC;YACxB,cAAc,CAAC,QAAQ,CAAC;YACxB,cAAc,CAAC,QAAQ,CAAC;YACxB,eAAe,CAAC,QAAQ,CAAC;YACzB,SAAS;YACT,WAAW;YACX,UAAU;SACX;QACD,iBAAiB;QACjB,eAAe;QACf,iBAAiB;KAClB,CAAC;AACJ,CAAC","sourcesContent":["import type { AgentTool } from \"@earendil-works/pi-agent-core\";\nimport type { TSchema } from \"@sinclair/typebox\";\nimport type { ConversationKind } from \"../adapter.js\";\nimport { createAttachTool } from \"../adapters/slack/tools/attach.js\";\nimport type { DockerContainerManager } from \"../provisioner.js\";\nimport type { Executor, SandboxConfig } from \"../sandbox/index.js\";\nimport { createBashTool } from \"./bash.js\";\nimport { createEditTool } from \"./edit.js\";\nimport { createEventTool, HostEventStore } from \"./event.js\";\nimport { createReadTool } from \"./read.js\";\nimport { createSandboxTool } from \"./sandbox.js\";\nimport { createWriteTool } from \"./write.js\";\n\nexport function createMikanTools(\n executor: Executor,\n workspaceDir: string,\n sandboxController?: {\n sandbox: SandboxConfig;\n provisioner?: Pick<DockerContainerManager, \"getLimitStatus\" | \"setLimits\">;\n },\n): {\n tools: AgentTool<TSchema>[];\n setUploadFunction: (fn: (filePath: string, title?: string) => Promise<void>) => void;\n setEventContext: (context: {\n platform: string;\n conversationId: string;\n conversationKind: ConversationKind;\n userId: string;\n }) => void;\n setSandboxContext: (context: { conversationId: string; userId: string }) => void;\n} {\n const { tool: attachTool, setUploadFunction } = createAttachTool();\n const { tool: eventTool, setEventContext } = createEventTool(\n HostEventStore.fromWorkspaceDir(workspaceDir),\n );\n const { tool: sandboxTool, setSandboxContext } = createSandboxTool(\n sandboxController ?? { sandbox: executor.getSandboxConfig() },\n );\n return {\n tools: [\n createReadTool(executor),\n createBashTool(executor),\n createEditTool(executor),\n createWriteTool(executor),\n eventTool,\n sandboxTool,\n attachTool,\n ],\n setUploadFunction,\n setEventContext,\n setSandboxContext,\n };\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../../src/tools/read.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAI/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AA4BpD,QAAA,MAAM,UAAU;;;;;EASd,CAAC;AAMH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,QAAQ,GAAG,SAAS,CAAC,OAAO,UAAU,CAAC,CA0H/E","sourcesContent":["import type { AgentTool } from \"@earendil-works/pi-agent-core\";\nimport type { ImageContent, TextContent } from \"@earendil-works/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport { extname } from \"path\";\nimport type { Executor } from \"../sandbox/index.js\";\nimport {\n DEFAULT_MAX_BYTES,\n DEFAULT_MAX_LINES,\n formatSize,\n type TruncationResult,\n truncateHead,\n} from \"./truncate.js\";\n\n/**\n * Map of file extensions to MIME types for common image formats\n */\nconst IMAGE_MIME_TYPES: Record<string, string> = {\n \".jpg\": \"image/jpeg\",\n \".jpeg\": \"image/jpeg\",\n \".png\": \"image/png\",\n \".gif\": \"image/gif\",\n \".webp\": \"image/webp\",\n};\n\n/**\n * Check if a file is an image based on its extension\n */\nfunction isImageFile(filePath: string): string | null {\n const ext = extname(filePath).toLowerCase();\n return IMAGE_MIME_TYPES[ext] || null;\n}\n\nconst readSchema = Type.Object({\n label: Type.String({\n description: \"Brief description of what you're reading and why (shown to user)\",\n }),\n path: Type.String({ description: \"Path to the file to read (relative or absolute)\" }),\n offset: Type.Optional(\n Type.Number({ description: \"Line number to start reading from (1-indexed)\" }),\n ),\n limit: Type.Optional(Type.Number({ description: \"Maximum number of lines to read\" })),\n});\n\ninterface ReadToolDetails {\n truncation?: TruncationResult;\n}\n\nexport function createReadTool(executor: Executor): AgentTool<typeof readSchema> {\n return {\n name: \"read\",\n label: \"read\",\n description: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files.`,\n parameters: readSchema,\n execute: async (\n _toolCallId: string,\n { path, offset, limit }: { label: string; path: string; offset?: number; limit?: number },\n signal?: AbortSignal,\n ): Promise<{\n content: (TextContent | ImageContent)[];\n details: ReadToolDetails | undefined;\n }> => {\n const mimeType = isImageFile(path);\n\n if (mimeType) {\n // Read as image (binary) - use base64\n const result = await executor.exec(`base64 < ${shellEscape(path)}`, { signal });\n if (result.code !== 0) {\n throw new Error(result.stderr || `Failed to read file: ${path}`);\n }\n const base64 = result.stdout.replace(/\\s/g, \"\"); // Remove whitespace from base64\n\n return {\n content: [\n { type: \"text\", text: `Read image file [${mimeType}]` },\n { type: \"image\", data: base64, mimeType },\n ],\n details: undefined,\n };\n }\n\n // Get total line count first\n const countResult = await executor.exec(`wc -l < ${shellEscape(path)}`, { signal });\n if (countResult.code !== 0) {\n throw new Error(countResult.stderr || `Failed to read file: ${path}`);\n }\n const totalFileLines = Number.parseInt(countResult.stdout.trim(), 10) + 1; // wc -l counts newlines, not lines\n\n // Apply offset if specified (1-indexed)\n const startLine = offset ? Math.max(1, offset) : 1;\n const startLineDisplay = startLine;\n\n // Check if offset is out of bounds\n if (startLine > totalFileLines) {\n throw new Error(`Offset ${offset} is beyond end of file (${totalFileLines} lines total)`);\n }\n\n // Read content with offset\n let cmd: string;\n if (startLine === 1) {\n cmd = `cat ${shellEscape(path)}`;\n } else {\n cmd = `tail -n +${startLine} ${shellEscape(path)}`;\n }\n\n const result = await executor.exec(cmd, { signal });\n if (result.code !== 0) {\n throw new Error(result.stderr || `Failed to read file: ${path}`);\n }\n\n let selectedContent = result.stdout;\n let userLimitedLines: number | undefined;\n\n // Apply user limit if specified\n if (limit !== undefined) {\n const lines = selectedContent.split(\"\\n\");\n const endLine = Math.min(limit, lines.length);\n selectedContent = lines.slice(0, endLine).join(\"\\n\");\n userLimitedLines = endLine;\n }\n\n // Apply truncation (respects both line and byte limits)\n const truncation = truncateHead(selectedContent);\n\n let outputText: string;\n let details: ReadToolDetails | undefined;\n\n if (truncation.firstLineExceedsLimit) {\n // First line at offset exceeds 50KB - tell model to use bash\n const firstLineSize = formatSize(\n Buffer.byteLength(selectedContent.split(\"\\n\")[0], \"utf-8\"),\n );\n outputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${startLineDisplay}p' ${path} | head -c ${DEFAULT_MAX_BYTES}]`;\n details = { truncation };\n } else if (truncation.truncated) {\n // Truncation occurred - build actionable notice\n const endLineDisplay = startLineDisplay + truncation.outputLines - 1;\n const nextOffset = endLineDisplay + 1;\n\n outputText = truncation.content;\n\n if (truncation.truncatedBy === \"lines\") {\n outputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue]`;\n } else {\n outputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Use offset=${nextOffset} to continue]`;\n }\n details = { truncation };\n } else if (userLimitedLines !== undefined) {\n // User specified limit, check if there's more content\n const linesFromStart = startLine - 1 + userLimitedLines;\n if (linesFromStart < totalFileLines) {\n const remaining = totalFileLines - linesFromStart;\n const nextOffset = startLine + userLimitedLines;\n\n outputText = truncation.content;\n outputText += `\\n\\n[${remaining} more lines in file. Use offset=${nextOffset} to continue]`;\n } else {\n outputText = truncation.content;\n }\n } else {\n // No truncation, no user limit exceeded\n outputText = truncation.content;\n }\n\n return {\n content: [{ type: \"text\", text: outputText }],\n details,\n };\n },\n };\n}\n\nfunction shellEscape(s: string): string {\n return `'${s.replace(/'/g, \"'\\\\''\")}'`;\n}\n"]}
1
+ {"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../../src/tools/read.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAI/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AA4BpD,QAAA,MAAM,UAAU;;;;;EASd,CAAC;AAMH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,QAAQ,GAAG,SAAS,CAAC,OAAO,UAAU,CAAC,CA0H/E"}
@@ -1 +1 @@
1
- {"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../../src/tools/sandbox.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAE/D,OAAO,KAAK,EAAE,sBAAsB,EAAkB,MAAM,mBAAmB,CAAC;AAChF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGzD,QAAA,MAAM,aAAa;;;;EAcjB,CAAC;AAQH,UAAU,kBAAkB;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,UAAU,qBAAqB;IAC7B,OAAO,EAAE,aAAa,CAAC;IACvB,WAAW,CAAC,EAAE,IAAI,CAAC,sBAAsB,EAAE,gBAAgB,GAAG,WAAW,CAAC,CAAC;CAC5E;AAED,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,qBAAqB,GAAG;IACpE,IAAI,EAAE,SAAS,CAAC,OAAO,aAAa,CAAC,CAAC;IACtC,iBAAiB,EAAE,CAAC,OAAO,EAAE,kBAAkB,KAAK,IAAI,CAAC;CAC1D,CA+CA","sourcesContent":["import type { AgentTool } from \"@earendil-works/pi-agent-core\";\nimport { Type } from \"@sinclair/typebox\";\nimport type { DockerContainerManager, ResourceLimits } from \"../provisioner.js\";\nimport type { SandboxConfig } from \"../sandbox/index.js\";\nimport { resolveActorVaultKey } from \"../vault/routing.js\";\n\nconst sandboxSchema = Type.Object({\n action: Type.Union([Type.Literal(\"status\"), Type.Literal(\"set\")], {\n description: \"Use status to inspect current limits, or set to apply temporary limits.\",\n }),\n cpus: Type.Optional(\n Type.String({\n description: \"Docker CPU limit for action=set, for example '0.5', '1', or '2'.\",\n }),\n ),\n memory: Type.Optional(\n Type.String({\n description: \"Docker memory limit for action=set, for example '512m', '1g', or '4g'.\",\n }),\n ),\n});\n\ntype SandboxToolParams = {\n action: \"status\" | \"set\";\n cpus?: string;\n memory?: string;\n};\n\ninterface SandboxToolContext {\n userId: string;\n conversationId: string;\n}\n\ninterface SandboxToolController {\n sandbox: SandboxConfig;\n provisioner?: Pick<DockerContainerManager, \"getLimitStatus\" | \"setLimits\">;\n}\n\nexport function createSandboxTool(controller: SandboxToolController): {\n tool: AgentTool<typeof sandboxSchema>;\n setSandboxContext: (context: SandboxToolContext) => void;\n} {\n let sandboxContext: SandboxToolContext | null = null;\n\n const tool: AgentTool<typeof sandboxSchema> = {\n name: \"sandbox\",\n label: \"sandbox\",\n description:\n \"Inspect or temporarily set CPU/memory limits for the current managed image sandbox. Limits apply to this conversation's sandbox container and are cleared when the container stops.\",\n parameters: sandboxSchema,\n execute: async (_toolCallId: string, params: SandboxToolParams, signal?: AbortSignal) => {\n if (signal?.aborted) {\n throw new Error(\"Operation aborted\");\n }\n if (!sandboxContext) {\n throw new Error(\"Sandbox context not configured\");\n }\n if (controller.sandbox.type !== \"image\" || !controller.provisioner) {\n throw new Error(\"The sandbox tool only supports image:* managed sandboxes\");\n }\n\n const containerKey = resolveActorVaultKey(\n controller.sandbox,\n sandboxContext.userId,\n sandboxContext.conversationId,\n );\n\n if (params.action === \"set\") {\n const limits = normalizeLimits(params);\n const status = await controller.provisioner.setLimits(containerKey, limits);\n return textResult(\n `Updated sandbox limits for ${containerKey}: ${formatLimits(status.limits)}. These temporary limits are cleared when the sandbox container stops.`,\n );\n }\n\n const status = controller.provisioner.getLimitStatus(containerKey);\n return textResult(\n `Sandbox limits for ${containerKey}: ${formatLimits(status.limits)}${status.boosted ? \" (boosted)\" : \"\"}.`,\n );\n },\n };\n\n return {\n tool,\n setSandboxContext: (context: SandboxToolContext) => {\n sandboxContext = context;\n },\n };\n}\n\nfunction normalizeLimits(params: SandboxToolParams): ResourceLimits {\n const cpus = normalizeLimitValue(\"cpus\", params.cpus);\n const memory = normalizeLimitValue(\"memory\", params.memory);\n if (!cpus && !memory) {\n throw new Error(\"action=set requires cpus and/or memory\");\n }\n return { ...(cpus ? { cpus } : {}), ...(memory ? { memory } : {}) };\n}\n\nfunction normalizeLimitValue(name: string, value: string | undefined): string | undefined {\n if (value === undefined) return undefined;\n const trimmed = value.trim();\n if (!trimmed) return undefined;\n if (!/^[A-Za-z0-9_.-]+$/.test(trimmed)) {\n throw new Error(`${name} must not contain whitespace or shell metacharacters`);\n }\n return trimmed;\n}\n\nfunction formatLimits(limits: ResourceLimits | undefined): string {\n return `CPU ${limits?.cpus ?? \"unlimited\"} / Memory ${limits?.memory ?? \"unlimited\"}`;\n}\n\nfunction textResult(text: string): {\n content: Array<{ type: \"text\"; text: string }>;\n details: undefined;\n} {\n return { content: [{ type: \"text\", text }], details: undefined };\n}\n"]}
1
+ {"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../../src/tools/sandbox.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAE/D,OAAO,KAAK,EAAE,sBAAsB,EAAkB,MAAM,mBAAmB,CAAC;AAChF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGzD,QAAA,MAAM,aAAa;;;;EAcjB,CAAC;AAQH,UAAU,kBAAkB;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,UAAU,qBAAqB;IAC7B,OAAO,EAAE,aAAa,CAAC;IACvB,WAAW,CAAC,EAAE,IAAI,CAAC,sBAAsB,EAAE,gBAAgB,GAAG,WAAW,CAAC,CAAC;CAC5E;AAED,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,qBAAqB,GAAG;IACpE,IAAI,EAAE,SAAS,CAAC,OAAO,aAAa,CAAC,CAAC;IACtC,iBAAiB,EAAE,CAAC,OAAO,EAAE,kBAAkB,KAAK,IAAI,CAAC;CAC1D,CA+CA"}
@@ -1 +1 @@
1
- {"version":3,"file":"truncate.d.ts","sourceRoot":"","sources":["../../src/tools/truncate.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,eAAO,MAAM,iBAAiB,OAAO,CAAC;AACtC,eAAO,MAAM,iBAAiB,QAAY,CAAC;AAE3C,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACtE,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEtE;;GAEG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQhD;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,gBAAgB,CA4E/F;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,gBAAgB,CAqE/F","sourcesContent":["/**\n * Shared truncation utilities for tool outputs.\n *\n * Truncation is based on two independent limits - whichever is hit first wins:\n * - Line limit (default: 2000 lines)\n * - Byte limit (default: 50KB)\n *\n * Never returns partial lines (except bash tail truncation edge case).\n */\n\nexport const DEFAULT_MAX_LINES = 2000;\nexport const DEFAULT_MAX_BYTES = 50 * 1024; // 50KB\n\nexport type { TruncationOptions, TruncationResult } from \"./types.js\";\nimport type { TruncationOptions, TruncationResult } from \"./types.js\";\n\n/**\n * Format bytes as human-readable size.\n */\nexport function formatSize(bytes: number): string {\n if (bytes < 1024) {\n return `${bytes}B`;\n } else if (bytes < 1024 * 1024) {\n return `${(bytes / 1024).toFixed(1)}KB`;\n } else {\n return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;\n }\n}\n\n/**\n * Truncate content from the head (keep first N lines/bytes).\n * Suitable for file reads where you want to see the beginning.\n *\n * Never returns partial lines. If first line exceeds byte limit,\n * returns empty content with firstLineExceedsLimit=true.\n */\nexport function truncateHead(content: string, options: TruncationOptions = {}): TruncationResult {\n const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;\n const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\n\n const totalBytes = Buffer.byteLength(content, \"utf-8\");\n const lines = content.split(\"\\n\");\n const totalLines = lines.length;\n\n // Check if no truncation needed\n if (totalLines <= maxLines && totalBytes <= maxBytes) {\n return {\n content,\n truncated: false,\n truncatedBy: null,\n totalLines,\n totalBytes,\n outputLines: totalLines,\n outputBytes: totalBytes,\n lastLinePartial: false,\n firstLineExceedsLimit: false,\n };\n }\n\n // Check if first line alone exceeds byte limit\n const firstLineBytes = Buffer.byteLength(lines[0], \"utf-8\");\n if (firstLineBytes > maxBytes) {\n return {\n content: \"\",\n truncated: true,\n truncatedBy: \"bytes\",\n totalLines,\n totalBytes,\n outputLines: 0,\n outputBytes: 0,\n lastLinePartial: false,\n firstLineExceedsLimit: true,\n };\n }\n\n // Collect complete lines that fit\n const outputLinesArr: string[] = [];\n let outputBytesCount = 0;\n let truncatedBy: \"lines\" | \"bytes\" = \"lines\";\n\n for (let i = 0; i < lines.length && i < maxLines; i++) {\n const line = lines[i];\n const lineBytes = Buffer.byteLength(line, \"utf-8\") + (i > 0 ? 1 : 0); // +1 for newline\n\n if (outputBytesCount + lineBytes > maxBytes) {\n truncatedBy = \"bytes\";\n break;\n }\n\n outputLinesArr.push(line);\n outputBytesCount += lineBytes;\n }\n\n // If we exited due to line limit\n if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {\n truncatedBy = \"lines\";\n }\n\n const outputContent = outputLinesArr.join(\"\\n\");\n const finalOutputBytes = Buffer.byteLength(outputContent, \"utf-8\");\n\n return {\n content: outputContent,\n truncated: true,\n truncatedBy,\n totalLines,\n totalBytes,\n outputLines: outputLinesArr.length,\n outputBytes: finalOutputBytes,\n lastLinePartial: false,\n firstLineExceedsLimit: false,\n };\n}\n\n/**\n * Truncate content from the tail (keep last N lines/bytes).\n * Suitable for bash output where you want to see the end (errors, final results).\n *\n * May return partial first line if the last line of original content exceeds byte limit.\n */\nexport function truncateTail(content: string, options: TruncationOptions = {}): TruncationResult {\n const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;\n const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\n\n const totalBytes = Buffer.byteLength(content, \"utf-8\");\n const lines = content.split(\"\\n\");\n const totalLines = lines.length;\n\n // Check if no truncation needed\n if (totalLines <= maxLines && totalBytes <= maxBytes) {\n return {\n content,\n truncated: false,\n truncatedBy: null,\n totalLines,\n totalBytes,\n outputLines: totalLines,\n outputBytes: totalBytes,\n lastLinePartial: false,\n firstLineExceedsLimit: false,\n };\n }\n\n // Work backwards from the end\n const outputLinesArr: string[] = [];\n let outputBytesCount = 0;\n let truncatedBy: \"lines\" | \"bytes\" = \"lines\";\n let lastLinePartial = false;\n\n for (let i = lines.length - 1; i >= 0 && outputLinesArr.length < maxLines; i--) {\n const line = lines[i];\n const lineBytes = Buffer.byteLength(line, \"utf-8\") + (outputLinesArr.length > 0 ? 1 : 0); // +1 for newline\n\n if (outputBytesCount + lineBytes > maxBytes) {\n truncatedBy = \"bytes\";\n // Edge case: if we haven't added ANY lines yet and this line exceeds maxBytes,\n // take the end of the line (partial)\n if (outputLinesArr.length === 0) {\n const truncatedLine = truncateStringToBytesFromEnd(line, maxBytes);\n outputLinesArr.unshift(truncatedLine);\n outputBytesCount = Buffer.byteLength(truncatedLine, \"utf-8\");\n lastLinePartial = true;\n }\n break;\n }\n\n outputLinesArr.unshift(line);\n outputBytesCount += lineBytes;\n }\n\n // If we exited due to line limit\n if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {\n truncatedBy = \"lines\";\n }\n\n const outputContent = outputLinesArr.join(\"\\n\");\n const finalOutputBytes = Buffer.byteLength(outputContent, \"utf-8\");\n\n return {\n content: outputContent,\n truncated: true,\n truncatedBy,\n totalLines,\n totalBytes,\n outputLines: outputLinesArr.length,\n outputBytes: finalOutputBytes,\n lastLinePartial,\n firstLineExceedsLimit: false,\n };\n}\n\n/**\n * Truncate a string to fit within a byte limit (from the end).\n * Handles multi-byte UTF-8 characters correctly.\n */\nfunction truncateStringToBytesFromEnd(str: string, maxBytes: number): string {\n const buf = Buffer.from(str, \"utf-8\");\n if (buf.length <= maxBytes) {\n return str;\n }\n\n // Start from the end, skip maxBytes back\n let start = buf.length - maxBytes;\n\n // Find a valid UTF-8 boundary (start of a character)\n while (start < buf.length && (buf[start] & 0xc0) === 0x80) {\n start++;\n }\n\n return buf.slice(start).toString(\"utf-8\");\n}\n"]}
1
+ {"version":3,"file":"truncate.d.ts","sourceRoot":"","sources":["../../src/tools/truncate.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,eAAO,MAAM,iBAAiB,OAAO,CAAC;AACtC,eAAO,MAAM,iBAAiB,QAAY,CAAC;AAE3C,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACtE,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEtE;;GAEG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQhD;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,gBAAgB,CA4E/F;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,gBAAgB,CAqE/F"}
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/tools/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAI9C,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC;AAEtC,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACtF,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,UAAU,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;IACjG,IAAI,CACF,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,UAAU,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACrF,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACvF,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CACzD;AAID,MAAM,WAAW,gBAAgB;IAC/B,4BAA4B;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,kCAAkC;IAClC,SAAS,EAAE,OAAO,CAAC;IACnB,sEAAsE;IACtE,WAAW,EAAE,OAAO,GAAG,OAAO,GAAG,IAAI,CAAC;IACtC,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,WAAW,EAAE,MAAM,CAAC;IACpB,8CAA8C;IAC9C,WAAW,EAAE,MAAM,CAAC;IACpB,yFAAyF;IACzF,eAAe,EAAE,OAAO,CAAC;IACzB,2EAA2E;IAC3E,qBAAqB,EAAE,OAAO,CAAC;CAChC;AAED,MAAM,WAAW,iBAAiB;IAChC,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB","sourcesContent":["import type { MikanEvent } from \"../types.js\";\n\n// ── event tool ───────────────────────────────────────────────────────────────\n\nexport type EventPayload = MikanEvent;\n\nexport interface EventStore {\n write(filename: string, payload: MikanEvent): Promise<{ path: string; size: number }>;\n list(): Promise<Array<{ filename: string; payload: MikanEvent; size: number; mtimeMs: number }>>;\n read(\n filename: string,\n ): Promise<{ filename: string; payload: MikanEvent; size: number; mtimeMs: number }>;\n update(filename: string, payload: MikanEvent): Promise<{ path: string; size: number }>;\n delete(filename: string): Promise<{ deleted: boolean }>;\n}\n\n// ── truncation ───────────────────────────────────────────────────────────────\n\nexport interface TruncationResult {\n /** The truncated content */\n content: string;\n /** Whether truncation occurred */\n truncated: boolean;\n /** Which limit was hit: \"lines\", \"bytes\", or null if not truncated */\n truncatedBy: \"lines\" | \"bytes\" | null;\n /** Total number of lines in the original content */\n totalLines: number;\n /** Total number of bytes in the original content */\n totalBytes: number;\n /** Number of complete lines in the truncated output */\n outputLines: number;\n /** Number of bytes in the truncated output */\n outputBytes: number;\n /** Whether the last line was partially truncated (only for tail truncation edge case) */\n lastLinePartial: boolean;\n /** Whether the first line exceeded the byte limit (for head truncation) */\n firstLineExceedsLimit: boolean;\n}\n\nexport interface TruncationOptions {\n /** Maximum number of lines (default: 2000) */\n maxLines?: number;\n /** Maximum number of bytes (default: 50KB) */\n maxBytes?: number;\n}\n"]}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/tools/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAI9C,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC;AAEtC,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACtF,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,UAAU,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;IACjG,IAAI,CACF,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,UAAU,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACrF,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACvF,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CACzD;AAID,MAAM,WAAW,gBAAgB;IAC/B,4BAA4B;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,kCAAkC;IAClC,SAAS,EAAE,OAAO,CAAC;IACnB,sEAAsE;IACtE,WAAW,EAAE,OAAO,GAAG,OAAO,GAAG,IAAI,CAAC;IACtC,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,WAAW,EAAE,MAAM,CAAC;IACpB,8CAA8C;IAC9C,WAAW,EAAE,MAAM,CAAC;IACpB,yFAAyF;IACzF,eAAe,EAAE,OAAO,CAAC;IACzB,2EAA2E;IAC3E,qBAAqB,EAAE,OAAO,CAAC;CAChC;AAED,MAAM,WAAW,iBAAiB;IAChC,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB"}
@@ -1 +1 @@
1
- {"version":3,"file":"write.d.ts","sourceRoot":"","sources":["../../src/tools/write.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAE/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAEpD,QAAA,MAAM,WAAW;;;;EAIf,CAAC;AAEH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,QAAQ,GAAG,SAAS,CAAC,OAAO,WAAW,CAAC,CA8BjF","sourcesContent":["import type { AgentTool } from \"@earendil-works/pi-agent-core\";\nimport { Type } from \"@sinclair/typebox\";\nimport type { Executor } from \"../sandbox/index.js\";\n\nconst writeSchema = Type.Object({\n label: Type.String({ description: \"Brief description of what you're writing (shown to user)\" }),\n path: Type.String({ description: \"Path to the file to write (relative or absolute)\" }),\n content: Type.String({ description: \"Content to write to the file\" }),\n});\n\nexport function createWriteTool(executor: Executor): AgentTool<typeof writeSchema> {\n return {\n name: \"write\",\n label: \"write\",\n description:\n \"Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.\",\n parameters: writeSchema,\n execute: async (\n _toolCallId: string,\n { path, content }: { label: string; path: string; content: string },\n signal?: AbortSignal,\n ) => {\n // Create parent directories and write file using heredoc\n const dir = path.includes(\"/\") ? path.substring(0, path.lastIndexOf(\"/\")) : \".\";\n\n // Use printf to handle content with special characters, pipe to file\n // This avoids issues with heredoc and special characters\n const cmd = `mkdir -p ${shellEscape(dir)} && printf '%s' ${shellEscape(content)} > ${shellEscape(path)}`;\n\n const result = await executor.exec(cmd, { signal });\n if (result.code !== 0) {\n throw new Error(result.stderr || `Failed to write file: ${path}`);\n }\n\n return {\n content: [{ type: \"text\", text: `Successfully wrote ${content.length} bytes to ${path}` }],\n details: undefined,\n };\n },\n };\n}\n\nfunction shellEscape(s: string): string {\n return `'${s.replace(/'/g, \"'\\\\''\")}'`;\n}\n"]}
1
+ {"version":3,"file":"write.d.ts","sourceRoot":"","sources":["../../src/tools/write.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAE/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAEpD,QAAA,MAAM,WAAW;;;;EAIf,CAAC;AAEH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,QAAQ,GAAG,SAAS,CAAC,OAAO,WAAW,CAAC,CA8BjF"}
package/dist/trigger.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { BotEvent } from "./adapter.js";
1
+ import type { ConversationEvent } from "./adapter.js";
2
2
  export type { AutoReplyJudge, TriggerIntent, TriggerResult } from "./types.js";
3
3
  import type { AutoReplyJudge, TriggerIntent, TriggerResult } from "./types.js";
4
4
  /**
@@ -12,7 +12,7 @@ export declare function decideTrigger(intent: Exclude<TriggerIntent, "auto-reply
12
12
  * can apply a single uniform "do not trigger, but still log" policy.
13
13
  */
14
14
  export declare function evaluateAutoReplyPolicy(input: {
15
- event: BotEvent;
15
+ event: ConversationEvent;
16
16
  workingDir: string | undefined;
17
17
  judge?: AutoReplyJudge;
18
18
  timeoutMs?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"trigger.d.ts","sourceRoot":"","sources":["../src/trigger.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAS7C,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC/E,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE/E;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,OAAO,CAAC,aAAa,EAAE,sBAAsB,CAAC,GACrD,aAAa,CAEf;AAED;;;;GAIG;AACH,wBAAsB,uBAAuB,CAAC,KAAK,EAAE;IACnD,KAAK,EAAE,QAAQ,CAAC;IAChB,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GAAG,OAAO,CAAC,aAAa,CAAC,CA4BzB","sourcesContent":["import { completeSimple } from \"@earendil-works/pi-ai\";\nimport { AuthStorage, ModelRegistry } from \"@earendil-works/pi-coding-agent\";\nimport type { BotEvent } from \"./adapter.js\";\nimport { loadAutoReplyJudgeModel, loadConversationAutoReplyConfig } from \"./config.js\";\nimport * as log from \"./log.js\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\nimport { resolveConfiguredModel } from \"./model-registry.js\";\n\nconst JUDGE_TIMEOUT_MS = 10_000;\n\nexport type { AutoReplyJudge, TriggerIntent, TriggerResult } from \"./types.js\";\nimport type { AutoReplyJudge, TriggerIntent, TriggerResult } from \"./types.js\";\n\n/**\n * Trivially decide non-auto-reply intents synchronously. For \"auto-reply-candidate\"\n * callers must use {@link evaluateAutoReplyPolicy}.\n */\nexport function decideTrigger(\n intent: Exclude<TriggerIntent, \"auto-reply-candidate\">,\n): TriggerResult {\n return { trigger: true, reason: intent };\n}\n\n/**\n * Decide whether to auto-reply. Never throws — judge errors and timeouts are\n * folded into a `trigger: false` result with a distinct reason, so adapters\n * can apply a single uniform \"do not trigger, but still log\" policy.\n */\nexport async function evaluateAutoReplyPolicy(input: {\n event: BotEvent;\n workingDir: string | undefined;\n judge?: AutoReplyJudge;\n timeoutMs?: number;\n}): Promise<TriggerResult> {\n const { event, workingDir, judge = judgeAutoReplyWithLlm, timeoutMs = JUDGE_TIMEOUT_MS } = input;\n if (!workingDir) return { trigger: false, reason: \"auto-reply-unconfigured\" };\n\n const conversationDir = join(workingDir, event.conversationId);\n\n try {\n const config = loadConversationAutoReplyConfig(conversationDir);\n if (!config.enabled) return { trigger: false, reason: \"auto-reply-disabled\" };\n if (config.rules.length === 0) {\n return { trigger: true, reason: \"auto-reply-enabled\" };\n }\n\n const shouldReply = await withTimeout(\n judge({ event, rules: config.rules, conversationDir }),\n timeoutMs,\n );\n return shouldReply\n ? { trigger: true, reason: \"auto-reply-rule-match\" }\n : { trigger: false, reason: \"auto-reply-rule-no-match\" };\n } catch (err) {\n if (err instanceof JudgeTimeoutError) {\n log.logWarning(\"Auto-reply judge timed out\", String(err));\n return { trigger: false, reason: \"auto-reply-judge-timeout\" };\n }\n log.logWarning(\"Auto-reply policy evaluation failed\", String(err));\n return { trigger: false, reason: \"auto-reply-judge-failed\" };\n }\n}\n\nclass JudgeTimeoutError extends Error {\n constructor(ms: number) {\n super(`auto-reply judge exceeded ${ms}ms`);\n this.name = \"JudgeTimeoutError\";\n }\n}\n\nfunction withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n const timer = setTimeout(() => reject(new JudgeTimeoutError(ms)), ms);\n promise.then(\n (value) => {\n clearTimeout(timer);\n resolve(value);\n },\n (err) => {\n clearTimeout(timer);\n reject(err);\n },\n );\n });\n}\n\nasync function judgeAutoReplyWithLlm(input: {\n event: BotEvent;\n rules: string[];\n conversationDir: string;\n}): Promise<boolean> {\n const judgeConfig = loadAutoReplyJudgeModel(input.conversationDir);\n const modelRegistry = ModelRegistry.create(\n AuthStorage.create(join(homedir(), \".pi\", \"mikan\", \"auth.json\")),\n );\n const model = resolveConfiguredModel(modelRegistry, judgeConfig.provider, judgeConfig.model);\n const auth = await modelRegistry.getApiKeyAndHeaders(model);\n if (!auth.ok) {\n throw new Error(auth.error);\n }\n\n const answer = await completeSimple(\n model,\n {\n systemPrompt:\n \"You decide whether a bot should reply to a group/channel message. \" +\n \"Use only the rules provided by the user. Answer exactly YES or NO.\",\n messages: [\n {\n role: \"user\",\n timestamp: Date.now(),\n content: [\n \"Rules:\",\n ...input.rules.map((rule, index) => `${index + 1}. ${rule}`),\n \"\",\n \"Message:\",\n input.event.text,\n \"\",\n \"Should the bot reply? Answer exactly YES or NO.\",\n ].join(\"\\n\"),\n },\n ],\n },\n {\n temperature: 0,\n maxTokens: 4,\n reasoning: \"minimal\",\n apiKey: auth.apiKey,\n headers: auth.headers,\n },\n );\n const text = answer.content\n .filter((part) => part.type === \"text\")\n .map((part) => part.text)\n .join(\"\")\n .trim()\n .toUpperCase();\n return /^YES\\b/.test(text);\n}\n"]}
1
+ {"version":3,"file":"trigger.d.ts","sourceRoot":"","sources":["../src/trigger.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAStD,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC/E,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE/E;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,OAAO,CAAC,aAAa,EAAE,sBAAsB,CAAC,GACrD,aAAa,CAEf;AAED;;;;GAIG;AACH,wBAAsB,uBAAuB,CAAC,KAAK,EAAE;IACnD,KAAK,EAAE,iBAAiB,CAAC;IACzB,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GAAG,OAAO,CAAC,aAAa,CAAC,CA4BzB"}
package/dist/trigger.js CHANGED
@@ -1,4 +1,4 @@
1
- import { completeSimple } from "@earendil-works/pi-ai";
1
+ import { completeSimple } from "@earendil-works/pi-ai/compat";
2
2
  import { AuthStorage, ModelRegistry } from "@earendil-works/pi-coding-agent";
3
3
  import { loadAutoReplyJudgeModel, loadConversationAutoReplyConfig } from "./config.js";
4
4
  import * as log from "./log.js";
@@ -1 +1 @@
1
- {"version":3,"file":"trigger.js","sourceRoot":"","sources":["../src/trigger.ts"],"names":[],"mappings":"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","sourcesContent":["import { completeSimple } from \"@earendil-works/pi-ai\";\nimport { AuthStorage, ModelRegistry } from \"@earendil-works/pi-coding-agent\";\nimport type { BotEvent } from \"./adapter.js\";\nimport { loadAutoReplyJudgeModel, loadConversationAutoReplyConfig } from \"./config.js\";\nimport * as log from \"./log.js\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\nimport { resolveConfiguredModel } from \"./model-registry.js\";\n\nconst JUDGE_TIMEOUT_MS = 10_000;\n\nexport type { AutoReplyJudge, TriggerIntent, TriggerResult } from \"./types.js\";\nimport type { AutoReplyJudge, TriggerIntent, TriggerResult } from \"./types.js\";\n\n/**\n * Trivially decide non-auto-reply intents synchronously. For \"auto-reply-candidate\"\n * callers must use {@link evaluateAutoReplyPolicy}.\n */\nexport function decideTrigger(\n intent: Exclude<TriggerIntent, \"auto-reply-candidate\">,\n): TriggerResult {\n return { trigger: true, reason: intent };\n}\n\n/**\n * Decide whether to auto-reply. Never throws — judge errors and timeouts are\n * folded into a `trigger: false` result with a distinct reason, so adapters\n * can apply a single uniform \"do not trigger, but still log\" policy.\n */\nexport async function evaluateAutoReplyPolicy(input: {\n event: BotEvent;\n workingDir: string | undefined;\n judge?: AutoReplyJudge;\n timeoutMs?: number;\n}): Promise<TriggerResult> {\n const { event, workingDir, judge = judgeAutoReplyWithLlm, timeoutMs = JUDGE_TIMEOUT_MS } = input;\n if (!workingDir) return { trigger: false, reason: \"auto-reply-unconfigured\" };\n\n const conversationDir = join(workingDir, event.conversationId);\n\n try {\n const config = loadConversationAutoReplyConfig(conversationDir);\n if (!config.enabled) return { trigger: false, reason: \"auto-reply-disabled\" };\n if (config.rules.length === 0) {\n return { trigger: true, reason: \"auto-reply-enabled\" };\n }\n\n const shouldReply = await withTimeout(\n judge({ event, rules: config.rules, conversationDir }),\n timeoutMs,\n );\n return shouldReply\n ? { trigger: true, reason: \"auto-reply-rule-match\" }\n : { trigger: false, reason: \"auto-reply-rule-no-match\" };\n } catch (err) {\n if (err instanceof JudgeTimeoutError) {\n log.logWarning(\"Auto-reply judge timed out\", String(err));\n return { trigger: false, reason: \"auto-reply-judge-timeout\" };\n }\n log.logWarning(\"Auto-reply policy evaluation failed\", String(err));\n return { trigger: false, reason: \"auto-reply-judge-failed\" };\n }\n}\n\nclass JudgeTimeoutError extends Error {\n constructor(ms: number) {\n super(`auto-reply judge exceeded ${ms}ms`);\n this.name = \"JudgeTimeoutError\";\n }\n}\n\nfunction withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n const timer = setTimeout(() => reject(new JudgeTimeoutError(ms)), ms);\n promise.then(\n (value) => {\n clearTimeout(timer);\n resolve(value);\n },\n (err) => {\n clearTimeout(timer);\n reject(err);\n },\n );\n });\n}\n\nasync function judgeAutoReplyWithLlm(input: {\n event: BotEvent;\n rules: string[];\n conversationDir: string;\n}): Promise<boolean> {\n const judgeConfig = loadAutoReplyJudgeModel(input.conversationDir);\n const modelRegistry = ModelRegistry.create(\n AuthStorage.create(join(homedir(), \".pi\", \"mikan\", \"auth.json\")),\n );\n const model = resolveConfiguredModel(modelRegistry, judgeConfig.provider, judgeConfig.model);\n const auth = await modelRegistry.getApiKeyAndHeaders(model);\n if (!auth.ok) {\n throw new Error(auth.error);\n }\n\n const answer = await completeSimple(\n model,\n {\n systemPrompt:\n \"You decide whether a bot should reply to a group/channel message. \" +\n \"Use only the rules provided by the user. Answer exactly YES or NO.\",\n messages: [\n {\n role: \"user\",\n timestamp: Date.now(),\n content: [\n \"Rules:\",\n ...input.rules.map((rule, index) => `${index + 1}. ${rule}`),\n \"\",\n \"Message:\",\n input.event.text,\n \"\",\n \"Should the bot reply? Answer exactly YES or NO.\",\n ].join(\"\\n\"),\n },\n ],\n },\n {\n temperature: 0,\n maxTokens: 4,\n reasoning: \"minimal\",\n apiKey: auth.apiKey,\n headers: auth.headers,\n },\n );\n const text = answer.content\n .filter((part) => part.type === \"text\")\n .map((part) => part.text)\n .join(\"\")\n .trim()\n .toUpperCase();\n return /^YES\\b/.test(text);\n}\n"]}
1
+ {"version":3,"file":"trigger.js","sourceRoot":"","sources":["../src/trigger.ts"],"names":[],"mappings":"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","sourcesContent":["import { completeSimple } from \"@earendil-works/pi-ai/compat\";\nimport { AuthStorage, ModelRegistry } from \"@earendil-works/pi-coding-agent\";\nimport type { ConversationEvent } from \"./adapter.js\";\nimport { loadAutoReplyJudgeModel, loadConversationAutoReplyConfig } from \"./config.js\";\nimport * as log from \"./log.js\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\nimport { resolveConfiguredModel } from \"./model-registry.js\";\n\nconst JUDGE_TIMEOUT_MS = 10_000;\n\nexport type { AutoReplyJudge, TriggerIntent, TriggerResult } from \"./types.js\";\nimport type { AutoReplyJudge, TriggerIntent, TriggerResult } from \"./types.js\";\n\n/**\n * Trivially decide non-auto-reply intents synchronously. For \"auto-reply-candidate\"\n * callers must use {@link evaluateAutoReplyPolicy}.\n */\nexport function decideTrigger(\n intent: Exclude<TriggerIntent, \"auto-reply-candidate\">,\n): TriggerResult {\n return { trigger: true, reason: intent };\n}\n\n/**\n * Decide whether to auto-reply. Never throws — judge errors and timeouts are\n * folded into a `trigger: false` result with a distinct reason, so adapters\n * can apply a single uniform \"do not trigger, but still log\" policy.\n */\nexport async function evaluateAutoReplyPolicy(input: {\n event: ConversationEvent;\n workingDir: string | undefined;\n judge?: AutoReplyJudge;\n timeoutMs?: number;\n}): Promise<TriggerResult> {\n const { event, workingDir, judge = judgeAutoReplyWithLlm, timeoutMs = JUDGE_TIMEOUT_MS } = input;\n if (!workingDir) return { trigger: false, reason: \"auto-reply-unconfigured\" };\n\n const conversationDir = join(workingDir, event.conversationId);\n\n try {\n const config = loadConversationAutoReplyConfig(conversationDir);\n if (!config.enabled) return { trigger: false, reason: \"auto-reply-disabled\" };\n if (config.rules.length === 0) {\n return { trigger: true, reason: \"auto-reply-enabled\" };\n }\n\n const shouldReply = await withTimeout(\n judge({ event, rules: config.rules, conversationDir }),\n timeoutMs,\n );\n return shouldReply\n ? { trigger: true, reason: \"auto-reply-rule-match\" }\n : { trigger: false, reason: \"auto-reply-rule-no-match\" };\n } catch (err) {\n if (err instanceof JudgeTimeoutError) {\n log.logWarning(\"Auto-reply judge timed out\", String(err));\n return { trigger: false, reason: \"auto-reply-judge-timeout\" };\n }\n log.logWarning(\"Auto-reply policy evaluation failed\", String(err));\n return { trigger: false, reason: \"auto-reply-judge-failed\" };\n }\n}\n\nclass JudgeTimeoutError extends Error {\n constructor(ms: number) {\n super(`auto-reply judge exceeded ${ms}ms`);\n this.name = \"JudgeTimeoutError\";\n }\n}\n\nfunction withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n const timer = setTimeout(() => reject(new JudgeTimeoutError(ms)), ms);\n promise.then(\n (value) => {\n clearTimeout(timer);\n resolve(value);\n },\n (err) => {\n clearTimeout(timer);\n reject(err);\n },\n );\n });\n}\n\nasync function judgeAutoReplyWithLlm(input: {\n event: ConversationEvent;\n rules: string[];\n conversationDir: string;\n}): Promise<boolean> {\n const judgeConfig = loadAutoReplyJudgeModel(input.conversationDir);\n const modelRegistry = ModelRegistry.create(\n AuthStorage.create(join(homedir(), \".pi\", \"mikan\", \"auth.json\")),\n );\n const model = resolveConfiguredModel(modelRegistry, judgeConfig.provider, judgeConfig.model);\n const auth = await modelRegistry.getApiKeyAndHeaders(model);\n if (!auth.ok) {\n throw new Error(auth.error);\n }\n\n const answer = await completeSimple(\n model,\n {\n systemPrompt:\n \"You decide whether a bot should reply to a group/channel message. \" +\n \"Use only the rules provided by the user. Answer exactly YES or NO.\",\n messages: [\n {\n role: \"user\",\n timestamp: Date.now(),\n content: [\n \"Rules:\",\n ...input.rules.map((rule, index) => `${index + 1}. ${rule}`),\n \"\",\n \"Message:\",\n input.event.text,\n \"\",\n \"Should the bot reply? Answer exactly YES or NO.\",\n ].join(\"\\n\"),\n },\n ],\n },\n {\n temperature: 0,\n maxTokens: 4,\n reasoning: \"minimal\",\n apiKey: auth.apiKey,\n headers: auth.headers,\n },\n );\n const text = answer.content\n .filter((part) => part.type === \"text\")\n .map((part) => part.text)\n .join(\"\")\n .trim()\n .toUpperCase();\n return /^YES\\b/.test(text);\n}\n"]}
package/dist/types.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
2
2
  import { execFile } from "child_process";
3
- declare const _execFileAsync: typeof execFile.__promisify__;
4
- type ExecFileAsync = typeof _execFileAsync;
3
+ declare const execFileAsync: typeof execFile.__promisify__;
4
+ type ExecFileAsync = typeof execFileAsync;
5
5
  export type ConversationKind = "direct" | "shared";
6
6
  export type PlatformName = "slack" | "discord" | "telegram";
7
- export interface ChatMessage {
7
+ export interface ConversationMessage {
8
8
  id: string;
9
9
  sessionKey: string;
10
10
  conversationKind: ConversationKind;
@@ -25,11 +25,7 @@ export interface ChatToolResult {
25
25
  isError: boolean;
26
26
  durationMs: number;
27
27
  }
28
- export interface ChatResponseBlockKit {
29
- text: string;
30
- blocks: object[];
31
- }
32
- export interface ChatResponseContext {
28
+ export interface ConversationResponder {
33
29
  respond(text: string): Promise<void>;
34
30
  appendResponseDelta?(delta: string): Promise<void>;
35
31
  finishResponse?(finalText?: string): Promise<void>;
@@ -40,13 +36,12 @@ export interface ChatResponseContext {
40
36
  style?: "muted" | "error";
41
37
  }): Promise<void>;
42
38
  respondToolResult(result: ChatToolResult): Promise<void>;
43
- respondBlockKit?(response: ChatResponseBlockKit): Promise<void>;
44
39
  setTyping(isTyping: boolean): Promise<void>;
45
40
  setWorking(working: boolean): Promise<void>;
46
41
  uploadFile(filePath: string, title?: string): Promise<void>;
47
42
  deleteResponse(): Promise<void>;
48
43
  }
49
- export interface PlatformInfo {
44
+ export interface MessagingInfo {
50
45
  name: string;
51
46
  formattingGuide: string;
52
47
  channels: {
@@ -65,12 +60,35 @@ export interface PlatformInfo {
65
60
  export interface ChatAdapter {
66
61
  start(): Promise<void>;
67
62
  stop(): Promise<void>;
68
- getPlatformInfo(): PlatformInfo;
63
+ getMessagingInfo(): MessagingInfo;
64
+ }
65
+ export type AgentEventPayload = {
66
+ kind: "sessionStart";
67
+ } | {
68
+ kind: "toolStart";
69
+ toolId: string;
70
+ toolName: string;
71
+ input?: unknown;
72
+ } | {
73
+ kind: "toolEnd";
74
+ toolId: string;
75
+ } | {
76
+ kind: "turnEnd";
77
+ awaitingInput?: boolean;
78
+ } | {
79
+ kind: "sessionEnd";
80
+ reason?: string;
81
+ };
82
+ export interface AgentEventEnvelope {
83
+ source: "mikan";
84
+ sessionId: string;
85
+ actorName: string;
86
+ event: AgentEventPayload;
69
87
  }
70
88
  /**
71
89
  * A platform-agnostic event (message/mention) that triggers the agent.
72
90
  */
73
- export interface BotEvent {
91
+ export interface ConversationEvent {
74
92
  type: string;
75
93
  /** Platform-specific raw conversation/channel/chat identifier */
76
94
  conversationId: string;
@@ -98,22 +116,22 @@ export interface BotEvent {
98
116
  * Minimum interface that every platform bot must implement,
99
117
  * used by the central handler in main.ts and by EventsWatcher.
100
118
  */
101
- export interface Bot {
119
+ export interface MessagingBot {
102
120
  start(): Promise<void>;
103
121
  postMessage(channel: string, text: string): Promise<string>;
104
122
  updateMessage(channel: string, ts: string, text: string): Promise<void>;
105
- enqueueEvent(event: BotEvent): boolean;
106
- getPlatformInfo(): PlatformInfo;
123
+ enqueueEvent(event: ConversationEvent): boolean;
124
+ getMessagingInfo(): MessagingInfo;
107
125
  postPrivate?(conversationId: string, userId: string, text: string): Promise<void>;
108
126
  postPrivateDiagnostic?(conversationId: string, userId: string, text: string, options?: {
109
127
  style?: "muted" | "error";
110
128
  }): Promise<void>;
111
129
  }
112
- /** Pre-created platform adapters passed to the handler */
113
- export interface BotAdapters {
114
- message: ChatMessage;
115
- responseCtx: ChatResponseContext;
116
- platform: PlatformInfo;
130
+ /** Normalized platform data and reply hook for one event. */
131
+ export interface ConversationContext {
132
+ message: ConversationMessage;
133
+ responder: ConversationResponder;
134
+ platform: MessagingInfo;
117
135
  }
118
136
  export interface RunningSession {
119
137
  sessionKey: string;
@@ -121,17 +139,17 @@ export interface RunningSession {
121
139
  lastActivityAt?: number;
122
140
  currentTool?: string;
123
141
  }
124
- export interface BotHandler {
142
+ export interface MessagingEventHandler {
125
143
  isRunning(sessionKey: string): boolean;
126
144
  getRunningSessions(): RunningSession[];
127
- handleEvent(event: BotEvent, bot: Bot, adapters: BotAdapters): Promise<void>;
128
- handleStop(sessionKey: string, conversationId: string, bot: Bot): Promise<void>;
145
+ handleEvent(event: ConversationEvent, bot: MessagingBot, context: ConversationContext): Promise<void>;
146
+ handleStop(sessionKey: string, conversationId: string, bot: MessagingBot): Promise<void>;
129
147
  forceStop(sessionKey: string): void;
130
- handleNewCommand(sessionKey: string, conversationId: string, bot: Bot): Promise<void>;
148
+ handleNewCommand(sessionKey: string, conversationId: string, bot: MessagingBot): Promise<void>;
131
149
  }
132
- export interface AgentRunner {
150
+ export interface PiAgentWrapper {
133
151
  syncChatHistory(currentMessageId?: string): void;
134
- run(message: ChatMessage, responseCtx: ChatResponseContext, platform: PlatformInfo): Promise<{
152
+ run(message: ConversationMessage, responder: ConversationResponder, platform: MessagingInfo): Promise<{
135
153
  stopReason: string;
136
154
  errorMessage?: string;
137
155
  }>;
@@ -174,7 +192,7 @@ export interface ConversationLogMessage {
174
192
  user?: string;
175
193
  userName?: string;
176
194
  text?: string;
177
- isBot?: boolean;
195
+ isMessagingBot?: boolean;
178
196
  }
179
197
  export interface ImmediateEvent {
180
198
  type: "immediate";
@@ -283,7 +301,7 @@ export interface LoggedMessage {
283
301
  displayName?: string;
284
302
  text: string;
285
303
  attachments: Attachment[];
286
- isBot: boolean;
304
+ isMessagingBot: boolean;
287
305
  threadTs?: string;
288
306
  }
289
307
  export interface ChannelStoreConfig {
@@ -299,7 +317,7 @@ export type TriggerResult = {
299
317
  reason: string;
300
318
  };
301
319
  export type AutoReplyJudge = (input: {
302
- event: BotEvent;
320
+ event: ConversationEvent;
303
321
  rules: string[];
304
322
  conversationDir: string;
305
323
  }) => Promise<boolean>;