agentinbox-cli 0.1.0-nightly.3 → 0.1.0-nightly.30

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 (242) hide show
  1. package/README.md +10 -294
  2. package/dist/admin-8MQLrhsj.js +200 -0
  3. package/dist/admin-8MQLrhsj.js.map +1 -0
  4. package/dist/audit-BVpTUgLt.js +2 -0
  5. package/dist/{audit-CNYWHcik.js → audit-J2PdyQ1T.js} +12 -3
  6. package/dist/audit-J2PdyQ1T.js.map +1 -0
  7. package/dist/{auth-B3ukRgpB.js → auth-DANSxhaG.js} +120 -17
  8. package/dist/auth-DANSxhaG.js.map +1 -0
  9. package/dist/auth-D_FWbtRd.js +2 -0
  10. package/dist/bus-Cz0flS37.js.map +1 -1
  11. package/dist/cli.js +136 -33
  12. package/dist/cli.js.map +1 -1
  13. package/dist/commands-DxKT4tGy.js +126 -0
  14. package/dist/commands-DxKT4tGy.js.map +1 -0
  15. package/dist/{config-v975gxv4.js → config-7aAYcVSK.js} +17 -6
  16. package/dist/config-7aAYcVSK.js.map +1 -0
  17. package/dist/config-DS2ClYL5.js +2 -0
  18. package/dist/connect-CfxNALy5.js +335 -0
  19. package/dist/connect-CfxNALy5.js.map +1 -0
  20. package/dist/content-BXblC_Qs.js +1162 -0
  21. package/dist/content-BXblC_Qs.js.map +1 -0
  22. package/dist/content-worker.js +28 -0
  23. package/dist/content-worker.js.map +1 -0
  24. package/dist/{db-B2pnfQW5.js → db-CTUkHFMr.js} +74 -3
  25. package/dist/db-CTUkHFMr.js.map +1 -0
  26. package/dist/db-Ddb-yVQt.js +2 -0
  27. package/dist/filter-Dl9GOkNe.js +140 -0
  28. package/dist/filter-Dl9GOkNe.js.map +1 -0
  29. package/dist/instance-CkiEOmWa.js +2 -0
  30. package/dist/{instance-BnbMB2EK.js → instance-b-zilpyH.js} +28 -18
  31. package/dist/instance-b-zilpyH.js.map +1 -0
  32. package/dist/listen-BOHWwm_N.js +2 -0
  33. package/dist/listen-cDUHSBkN.js +1991 -0
  34. package/dist/listen-cDUHSBkN.js.map +1 -0
  35. package/dist/mcpheader-CiJWpThH.js +2 -0
  36. package/dist/mcpheader-Du6e5MGj.js +66 -0
  37. package/dist/mcpheader-Du6e5MGj.js.map +1 -0
  38. package/dist/mcpsetup-B9EH5659.js +2 -0
  39. package/dist/mcpsetup-CHQ9_Rbb.js +342 -0
  40. package/dist/mcpsetup-CHQ9_Rbb.js.map +1 -0
  41. package/dist/paths-BMavi0Uq.js +2 -0
  42. package/dist/paths-DckASYtc.js +42 -0
  43. package/dist/paths-DckASYtc.js.map +1 -0
  44. package/dist/profile-K2QIOwX4.js +2 -0
  45. package/dist/profile-uRinkfdm.js +181 -0
  46. package/dist/profile-uRinkfdm.js.map +1 -0
  47. package/dist/recipes-BQpXA6p1.js +104 -0
  48. package/dist/recipes-BQpXA6p1.js.map +1 -0
  49. package/dist/recipes-QSyMVBiz.js +2 -0
  50. package/dist/send-Dk-t8ia-.js +108 -0
  51. package/dist/send-Dk-t8ia-.js.map +1 -0
  52. package/dist/server-CHIVVc1E.js +9205 -0
  53. package/dist/server-CHIVVc1E.js.map +1 -0
  54. package/dist/service-Bk4bGONc.js +2 -0
  55. package/dist/service-ChvWjnOR.js +508 -0
  56. package/dist/service-ChvWjnOR.js.map +1 -0
  57. package/dist/service-D2IUVTXR.js +2 -0
  58. package/dist/service-DXiBV61B.js +295 -0
  59. package/dist/service-DXiBV61B.js.map +1 -0
  60. package/dist/{settings-DkA2sgzt.js → settings-BfFrufSv.js} +17 -6
  61. package/dist/settings-BfFrufSv.js.map +1 -0
  62. package/dist/settings-CqouaxRg.js +2 -0
  63. package/dist/{setup-token-BO89xnpb.js → setup-token-1Z0Bh1MW.js} +2 -2
  64. package/dist/setup-token-1Z0Bh1MW.js.map +1 -0
  65. package/dist/skill/SKILL.md +104 -31
  66. package/dist/skill-BJFn2bHK.js +2 -0
  67. package/dist/types-wY3IqC2D.js +44 -0
  68. package/dist/types-wY3IqC2D.js.map +1 -0
  69. package/dist/ui-B8Sd4Y7Y.js +81 -0
  70. package/dist/ui-B8Sd4Y7Y.js.map +1 -0
  71. package/dist/urls-Dp8pBGMU.js +2 -0
  72. package/dist/{urls-DmnonkJo.js → urls-U_LfYV61.js} +7 -2
  73. package/dist/urls-U_LfYV61.js.map +1 -0
  74. package/dist/web/assets/_agentId-BqzTDcZD.js +1 -0
  75. package/dist/web/assets/_app-C7fPyDq2.js +1 -0
  76. package/dist/web/assets/_app-DgwuYgnY.js +65 -0
  77. package/dist/web/assets/_webhookId-CgpQo4vc.js +1 -0
  78. package/dist/web/assets/account-status-DJ0qWvWa.js +1 -0
  79. package/dist/web/assets/accounts-CbOWoMdP.js +1 -0
  80. package/dist/web/assets/accounts-list-DZevJmCK.js +1 -0
  81. package/dist/web/assets/{activity-zrckFhPk.js → activity-CC5uxf8W.js} +1 -1
  82. package/dist/web/assets/agent-create-form-BvCIE72U.js +1 -0
  83. package/dist/web/assets/agent-status-BbmZYAXl.js +1 -0
  84. package/dist/web/assets/agents-CceZM3x2.js +1 -0
  85. package/dist/web/assets/approvals-CAx6vOgX.js +2 -0
  86. package/dist/web/assets/{arrow-left-DVJV4Zx7.js → arrow-left-CeZ4xY5K.js} +1 -1
  87. package/dist/web/assets/attention-CNzVKIFK.js +1 -0
  88. package/dist/web/assets/audit-B7aEP3m0.js +1 -0
  89. package/dist/web/assets/audit-bits-BDWvLS1q.js +1 -0
  90. package/dist/web/assets/bell-ring-Z_8yUI-f.js +1 -0
  91. package/dist/web/assets/{bot-DVw15lBn.js → bot-u651IEFE.js} +1 -1
  92. package/dist/web/assets/card-tVRfUZPy.js +1 -0
  93. package/dist/web/assets/chevron-right-CKgM8xnT.js +1 -0
  94. package/dist/web/assets/chips-input-CqlJWxP5.js +1 -0
  95. package/dist/web/assets/choice-cards-BleSo0tf.js +1 -0
  96. package/dist/web/assets/{circle-x-Bi4IGwky.js → circle-x-C82EyMgx.js} +1 -1
  97. package/dist/web/assets/cli-q1RqZyaZ.js +1 -0
  98. package/dist/web/assets/command-AXQYlyUG.js +1 -0
  99. package/dist/web/assets/common-CYHl5rtl.js +1 -0
  100. package/dist/web/assets/connect-agent-guide-Dls13EN1.js +1 -0
  101. package/dist/web/assets/connect-host-CJIdv0FX.js +6 -0
  102. package/dist/web/assets/data-table-azFvp6rv.js +1 -0
  103. package/dist/web/assets/{empty-CE_WZGEC.js → empty-Dk1avvSS.js} +1 -1
  104. package/dist/web/assets/events-DIFNutrL.js +1 -0
  105. package/dist/web/assets/eye-Csacg7_Z.js +1 -0
  106. package/dist/web/assets/{eye-off-DjpaihHf.js → eye-off-IxMg5-q3.js} +1 -1
  107. package/dist/web/assets/field-DpcNMSMK.js +1 -0
  108. package/dist/web/assets/get-started-eioZ7d2-.js +1 -0
  109. package/dist/web/assets/hidden-chars-weGLIJEI.js +1 -0
  110. package/dist/web/assets/index-D9cLH_cC.css +2 -0
  111. package/dist/web/assets/index-Db_9jgoe.js +11 -0
  112. package/dist/web/assets/input-group-CsGuxxud.js +1 -0
  113. package/dist/web/assets/input-rv35ipQA.js +1 -0
  114. package/dist/web/assets/label-multi-select-BZsswbkd.js +1 -0
  115. package/dist/web/assets/{labels-C_Pwium9.js → labels-NC5qakj0.js} +1 -1
  116. package/dist/web/assets/listener-handled-Dx-VxyFq.js +1 -0
  117. package/dist/web/assets/login-CADN0nRx.js +1 -0
  118. package/dist/web/assets/{mail-CAVmuESo.js → mail-DutPVENi.js} +1 -1
  119. package/dist/web/assets/mutations-v-10xhba.js +1 -0
  120. package/dist/web/assets/pairing-B5noxLZT.js +1 -0
  121. package/dist/web/assets/pairing-notifier-CYHyrHzu.js +1 -0
  122. package/dist/web/assets/{paperclip-QFjM4y6n.js → paperclip-B6l-1SOY.js} +1 -1
  123. package/dist/web/assets/password-input-Bc7BmjrN.js +1 -0
  124. package/dist/web/assets/{pencil-Bf1JhsG0.js → pencil-CppiUFww.js} +1 -1
  125. package/dist/web/assets/{play-DiPk49JH.js → play-DL5txgI8.js} +1 -1
  126. package/dist/web/assets/plug-zap-Dz98w9GO.js +1 -0
  127. package/dist/web/assets/plus-DemtsZGW.js +1 -0
  128. package/dist/web/assets/preload-helper-CeFTMjI3.js +1 -0
  129. package/dist/web/assets/public-url-C5JBh_4V.js +1 -0
  130. package/dist/web/assets/queries-Br89Bi_U.js +42 -0
  131. package/dist/web/assets/revealed-keys-fGHhLjBX.js +1 -0
  132. package/dist/web/assets/rolldown-runtime-XKyaHtl9.js +1 -0
  133. package/dist/web/assets/{search-gcYjitzV.js → search-BLMGiu4b.js} +1 -1
  134. package/dist/web/assets/select-COKkOCUQ.js +1 -0
  135. package/dist/web/assets/send-CMXht_PA.js +1 -0
  136. package/dist/web/assets/settings-BtMBK51Q.js +1 -0
  137. package/dist/web/assets/setup-Bp-PaJW5.js +1 -0
  138. package/dist/web/assets/sheet-Dgp4Yt1H.js +1 -0
  139. package/dist/web/assets/{shield-alert-CbFgd06X.js → shield-alert-CmFu7WcO.js} +1 -1
  140. package/dist/web/assets/sparkles-BfbM1rm2.js +1 -0
  141. package/dist/web/assets/{stamp-CH2OvjH2.js → stamp-l0UKjjIJ.js} +1 -1
  142. package/dist/web/assets/step-url-CQX7MVh1.js +1 -0
  143. package/dist/web/assets/stream-CqIjoBES.js +1 -0
  144. package/dist/web/assets/{tag-CP72Kk6Y.js → tag-jRccVtVN.js} +1 -1
  145. package/dist/web/assets/terminal-xpHUW224.js +1 -0
  146. package/dist/web/assets/test-email-button-CzFAj2qJ.js +1 -0
  147. package/dist/web/assets/toggle-group-Cf-1Z6pn.js +1 -0
  148. package/dist/web/assets/{trash-DxdMNg8B.js → trash-CTz8sLO4.js} +1 -1
  149. package/dist/web/assets/use-delivery-speed-CKR1fpiI.js +1 -0
  150. package/dist/web/assets/use-label-names-CTa_WQiV.js +1 -0
  151. package/dist/web/assets/use-sign-out-ClRWmewz.js +1 -0
  152. package/dist/web/assets/{useInfiniteQuery-DE6WChlb.js → useInfiniteQuery-B57FcJxK.js} +1 -1
  153. package/dist/web/assets/useQuery-B9NBDFCr.js +1 -0
  154. package/dist/web/assets/useRouterState-BCPVsIUY.js +1 -0
  155. package/dist/web/assets/{webhook-BWT75M78.js → webhook-By-4DSM8.js} +1 -1
  156. package/dist/web/assets/webhook-bits-C1Czotz-.js +1 -0
  157. package/dist/web/assets/webhook-sheet-BJmnyFT9.js +34 -0
  158. package/dist/web/assets/webhooks-CrCs3Tr1.js +1 -0
  159. package/dist/web/index.html +7 -10
  160. package/package.json +1 -1
  161. package/dist/audit-Bf3TvAAN.js +0 -2
  162. package/dist/audit-CNYWHcik.js.map +0 -1
  163. package/dist/auth-B3ukRgpB.js.map +0 -1
  164. package/dist/auth-B9dqCyaj.js +0 -2
  165. package/dist/config-CIvgMqLx.js +0 -2
  166. package/dist/config-v975gxv4.js.map +0 -1
  167. package/dist/db-B2pnfQW5.js.map +0 -1
  168. package/dist/db-CXhlUuVP.js +0 -2
  169. package/dist/instance-BEEO0AHU.js +0 -2
  170. package/dist/instance-BnbMB2EK.js.map +0 -1
  171. package/dist/server-BvX0c9MQ.js +0 -4682
  172. package/dist/server-BvX0c9MQ.js.map +0 -1
  173. package/dist/service-lFSuvmgq.js +0 -332
  174. package/dist/service-lFSuvmgq.js.map +0 -1
  175. package/dist/settings-DkA2sgzt.js.map +0 -1
  176. package/dist/settings-DoPS659W.js +0 -2
  177. package/dist/setup-token-BO89xnpb.js.map +0 -1
  178. package/dist/skill-BWWtinsE.js +0 -2
  179. package/dist/urls-CNYhbeUQ.js +0 -2
  180. package/dist/urls-DmnonkJo.js.map +0 -1
  181. package/dist/web/assets/_agentId-Cq2tty9r.js +0 -1
  182. package/dist/web/assets/_app-Cb1baRA0.js +0 -63
  183. package/dist/web/assets/_app-DU_bk7n8.js +0 -1
  184. package/dist/web/assets/_webhookId-B55gK0Lb.js +0 -1
  185. package/dist/web/assets/account-status-C08BBEsU.js +0 -1
  186. package/dist/web/assets/accounts-Bk1s8Uzv.js +0 -1
  187. package/dist/web/assets/accounts-list-F-kO3jiT.js +0 -1
  188. package/dist/web/assets/agent-create-form-BxTkdezg.js +0 -1
  189. package/dist/web/assets/agents-CmbZ2lwf.js +0 -1
  190. package/dist/web/assets/approvals-3DYaHsIJ.js +0 -2
  191. package/dist/web/assets/arrow-right-4PFX6MSS.js +0 -1
  192. package/dist/web/assets/audit-CDXxi4Hk.js +0 -1
  193. package/dist/web/assets/audit-bits-D8AkB5ZU.js +0 -1
  194. package/dist/web/assets/card-EQTpqntn.js +0 -1
  195. package/dist/web/assets/checkbox-DDZZurgK.js +0 -1
  196. package/dist/web/assets/chevron-down-892ylB4R.js +0 -1
  197. package/dist/web/assets/chevron-right-CbBKS6-T.js +0 -1
  198. package/dist/web/assets/chips-input-D_rUs_lc.js +0 -1
  199. package/dist/web/assets/common-Cc3JNp4n.js +0 -1
  200. package/dist/web/assets/connect-host-BV4t499t.js +0 -1
  201. package/dist/web/assets/data-table-CyhIs-ZM.js +0 -1
  202. package/dist/web/assets/dialog-Da2XSU3Y.js +0 -1
  203. package/dist/web/assets/dist-B0e3Ju3r.js +0 -1
  204. package/dist/web/assets/es2015-DPaldoxG.js +0 -41
  205. package/dist/web/assets/event-row-BsPXPNKL.js +0 -1
  206. package/dist/web/assets/events-eSr5JR5h.js +0 -1
  207. package/dist/web/assets/field-CcyMn3ey.js +0 -1
  208. package/dist/web/assets/get-started-CGYjo7e7.js +0 -1
  209. package/dist/web/assets/grants-BOBZxwBG.js +0 -1
  210. package/dist/web/assets/index-C0ZXKtiq.css +0 -2
  211. package/dist/web/assets/index-MbDFbrbR.js +0 -10
  212. package/dist/web/assets/input-CMhmmHiH.js +0 -1
  213. package/dist/web/assets/input-group-DRs3TNaT.js +0 -1
  214. package/dist/web/assets/key-round-CVIZRCLM.js +0 -1
  215. package/dist/web/assets/login-DTteGF6c.js +0 -1
  216. package/dist/web/assets/mutations-C9NgE3M4.js +0 -1
  217. package/dist/web/assets/password-input-_m9Ycphv.js +0 -1
  218. package/dist/web/assets/plus-CETmK0SB.js +0 -1
  219. package/dist/web/assets/preload-helper-BFuRUfdt.js +0 -1
  220. package/dist/web/assets/progress-3ZOsbLSK.js +0 -6
  221. package/dist/web/assets/queries-a3M5_loa.js +0 -2
  222. package/dist/web/assets/refresh-cw-DOn9kD7g.js +0 -1
  223. package/dist/web/assets/select-B7o4J-XI.js +0 -1
  224. package/dist/web/assets/send-BhQwhuJo.js +0 -1
  225. package/dist/web/assets/settings-CundYHSp.js +0 -1
  226. package/dist/web/assets/settings-form-BDGVJnAI.js +0 -1
  227. package/dist/web/assets/setup-BHRb8np_.js +0 -1
  228. package/dist/web/assets/sheet-CbWxq7yd.js +0 -1
  229. package/dist/web/assets/stream-6OwrgVZw.js +0 -1
  230. package/dist/web/assets/tabs-ChnOxkjp.js +0 -1
  231. package/dist/web/assets/test-connection-AUx4zhwY.js +0 -1
  232. package/dist/web/assets/textarea-CCokBZIw.js +0 -1
  233. package/dist/web/assets/toggle-group-BjRrXeJH.js +0 -1
  234. package/dist/web/assets/use-sign-out-BuzlTb0Q.js +0 -1
  235. package/dist/web/assets/useMatch-BKUnm54_.js +0 -1
  236. package/dist/web/assets/useMutation-BGhPR1Vu.js +0 -1
  237. package/dist/web/assets/useQuery-DAhf1YDH.js +0 -1
  238. package/dist/web/assets/useSelector-D70eKNYs.js +0 -1
  239. package/dist/web/assets/webhook-sheet-DQJqHwl0.js +0 -34
  240. package/dist/web/assets/webhooks-DS8Xc6Gc.js +0 -1
  241. package/dist/web/assets/with-selector-CkNyXYMW.js +0 -1
  242. package/dist/web/assets/x-BHjDLXNO.js +0 -1
package/README.md CHANGED
@@ -4,308 +4,24 @@
4
4
  [![npm](https://img.shields.io/npm/v/agentinbox-cli)](https://www.npmjs.com/package/agentinbox-cli)
5
5
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
6
6
 
7
- A self-hosted Gmail broker for AI agents. One small daemon holds your Gmail credentials; every agent gets its own
8
- scoped API key and reaches mail through **MCP tools** (read, search, draft, send, organize) and **signed webhooks** or an
9
- MCP event feed for new mail. Guardrails decide what each agent may do per mailbox, every action lands in an audit log,
10
- and a web console makes setup and management painless. It runs anywhere Node does, against your own Google Cloud
11
- project, with no cloud service in between.
12
-
13
- ## Features
14
-
15
- - **One place for credentials.** Connect Gmail and Google Workspace accounts once. Refresh tokens are encrypted at rest.
16
- Agents only ever see an `aib_…` key.
17
- - **MCP for every agent.** Streamable HTTP at `/mcp`: `search_threads`, `get_thread`, `reply`, `send_message`,
18
- `modify_labels`, `wait_for_events` and more. Each agent sees only the tools its grants allow.
19
- - **Real-time events.** New mail, replies to threads you sent into, and label changes arrive within seconds through
20
- Gmail push via a Pub/Sub *pull* subscription (no public URL needed), or by polling with zero extra setup.
21
- - **Webhooks.** Filter by account, label, sender, recipient, subject or attachments. Payloads are signed per
22
- [Standard Webhooks](https://www.standardwebhooks.com), retried with backoff, logged, and replayable.
23
- - **Guardrails that don't degrade content.** Per agent and mailbox: capabilities, label scope, send mode (direct,
24
- draft-only or human approval), recipient allowlists, reply-only and send caps. Email content is cleaned, not censored.
25
- - **A console you'll actually enjoy.** Guided Google setup, connect snippets for Claude Code, Codex and Cursor, an
26
- approvals inbox, live events, delivery logs and the audit trail. Dark mode included.
27
- - **A ready-made agent skill.** `SKILL.md` teaches agents to use the tools well and to treat email as data.
28
-
29
- <!--
30
- ## Preview
31
-
32
- ![Overview](docs/screenshots/overview.png)
33
-
34
- <table>
35
- <tr>
36
- <td width="50%"><img src="docs/screenshots/setup.png" alt="Setup wizard"><br>Guided Google Cloud setup.</td>
37
- <td width="50%"><img src="docs/screenshots/agents.png" alt="Agents"><br>Agents, grants and connect snippets.</td>
38
- </tr>
39
- <tr>
40
- <td><img src="docs/screenshots/approvals.png" alt="Approvals"><br>Approve or reject sends.</td>
41
- <td><img src="docs/screenshots/webhooks.png" alt="Webhooks"><br>Webhooks with delivery log and replay.</td>
42
- </tr>
43
- </table>
44
- -->
45
-
46
- ## Quick start
47
-
48
- Requires Node.js 22.13 or newer.
7
+ A self-hosted Gmail broker for AI agents: one small server holds your Gmail credentials and gives each agent scoped,
8
+ audited access through MCP tools and webhooks.
49
9
 
50
10
  ```sh
51
11
  npx agentinbox-cli
52
12
  ```
53
13
 
54
- The npm package is `agentinbox-cli`; the command it provides is `agentinbox`. Use `npx agentinbox-cli …` for one-off
55
- runs, or `npm i -g agentinbox-cli` and then `agentinbox …`.
56
-
57
- agentinbox starts on port 8425 and prints a one-time setup link (and opens it if it can):
58
-
59
- ```
60
- agentinbox v0.1.0 · data in /home/you/.agentinbox
61
-
62
- First run: open this link to claim the instance and finish setup:
63
-
64
- http://localhost:8425/setup?token=…
65
- ```
66
-
67
- Open it, choose an admin password, and the setup wizard walks you through the rest in about ten minutes.
68
-
69
- ## Setting up Google
70
-
71
- agentinbox uses **your own** Google Cloud project and OAuth client. Gmail's read scopes are restricted, so a shared
72
- client isn't an option, and this way your mail never passes through anyone else's app. The wizard shows each step with
73
- the exact values to paste; in short:
74
-
75
- 1. **Create a Google Cloud project** and enable the **Gmail API** (and the **Cloud Pub/Sub API** if you want push).
76
- 2. **Configure the OAuth consent screen** (Google Auth Platform → Audience):
77
- - **Internal** if every mailbox is in your Google Workspace organization. No review, no expiry, nothing else to do.
78
- - **External** for personal `@gmail.com` accounts (or mailboxes outside your org). Then **publish the app to
79
- "In production"**. Left in *Testing*, Google expires refresh tokens after **7 days** and your accounts silently
80
- disconnect. You don't need Google's verification for your own use: on consent, click *Advanced → Go to
81
- (app name)* past the "unverified app" warning.
82
- 3. **Create an OAuth client** of type *Web application*, add the redirect URI the console shows
83
- (`<public URL>/oauth/callback`), and paste the client ID, client secret and project ID into the console.
84
- 4. **Connect accounts.** Each one goes through Google's consent screen. If the console is on plain HTTP at a non-localhost
85
- address, Google can't redirect back to it; the console then asks you to paste the URL you land on instead.
86
- 5. **Choose how new mail arrives:**
87
- - **Pub/Sub (recommended):** one click creates the topic, grants Gmail's push service
88
- (`gmail-api-push@system.gserviceaccount.com`) permission to publish, and creates a pull subscription, using a
89
- connected account that can manage Pub/Sub in the project. Events arrive in seconds, with no inbound connections.
90
- If a Workspace organization policy (domain-restricted sharing) blocks that grant, the console says so; use polling.
91
- - **Polling:** no extra setup; checks each mailbox every 30 seconds by default.
92
-
93
- Then create an agent and give it a grant on a mailbox.
94
-
95
- ## Connecting agents
96
-
97
- Create an agent in the console (**Agents → New agent**) to get its API key; the key is shown once. The console has
98
- copy-paste snippets with your URL and key filled in, and a **Test connection** button. By hand:
99
-
100
- **Claude Code**
101
-
102
- ```sh
103
- claude mcp add --transport http agentinbox http://your-host:8425/mcp --header "Authorization: Bearer aib_…"
104
- ```
105
-
106
- **Codex** (`~/.codex/config.toml`)
107
-
108
- ```toml
109
- [mcp_servers.agentinbox]
110
- url = "http://your-host:8425/mcp"
111
- bearer_token_env_var = "AGENTINBOX_KEY"
112
- ```
113
-
114
- **Cursor** (`~/.cursor/mcp.json`) and most other MCP clients
115
-
116
- ```json
117
- {
118
- "mcpServers": {
119
- "agentinbox": {
120
- "url": "http://your-host:8425/mcp",
121
- "headers": { "Authorization": "Bearer aib_…" }
122
- }
123
- }
124
- }
125
- ```
126
-
127
- Any MCP client that speaks streamable HTTP works: endpoint `/mcp`, header `Authorization: Bearer <key>`.
128
-
129
- ### The agent skill
130
-
131
- `SKILL.md` teaches an agent the tools, Gmail search syntax, reply etiquette, how sends and denials work, the event loop,
132
- and that email content is data, never instructions. Install it on the machine where the agent runs (no running
133
- agentinbox needed there):
134
-
135
- ```sh
136
- npx agentinbox-cli skill install --url https://mail.example.com # ~/.claude/skills/agentinbox/SKILL.md
137
- npx agentinbox-cli skill install --target codex --url https://mail.example.com
138
- npx agentinbox-cli skill install --target ./skills --url https://mail.example.com
139
- npx agentinbox-cli skill print --url https://mail.example.com # to stdout
140
- ```
141
-
142
- Every instance also serves it at `/skill.md` with its public URL filled in.
143
-
144
- ## Events and webhooks
145
-
146
- Event types: `message.received`, `message.sent`, `thread.reply_received` (a reply in a thread the account sent into),
147
- `label.added`, `label.removed`, `account.auth_failed` and `sync.gap`. Agents only receive events for mailboxes and
148
- labels their grants let them read.
149
-
150
- - **Agents without an HTTP endpoint** (most coding agents) use the MCP tools `wait_for_events` (long poll),
151
- `list_events` and `ack_events`. The server keeps each agent's position.
152
- - **Agents with an endpoint** get webhooks: create one under **Webhooks**, pick event types and filters, and optionally
153
- include the cleaned message body. Failed deliveries retry with exponential backoff before they're marked dead, and
154
- any delivery can be replayed from the console.
155
-
156
- Each webhook request carries `webhook-id`, `webhook-timestamp` and `webhook-signature` headers. Verify them with any
157
- [Standard Webhooks library](https://github.com/standard-webhooks/standard-webhooks) and the `whsec_…` secret shown when
158
- you create the webhook:
159
-
160
- ```js
161
- import { Webhook } from "standardwebhooks";
14
+ Open the link it prints: the console walks you through the rest (Google Cloud setup, connecting Gmail, your first
15
+ agent). Requires Node.js 22.13 or newer.
162
16
 
163
- const wh = new Webhook(process.env.AGENTINBOX_WEBHOOK_SECRET);
164
- const payload = wh.verify(rawBody, request.headers); // throws if the signature or timestamp is bad
165
- ```
166
-
167
- A dependency-free Node version is in the [skill](server/skill/SKILL.md#webhooks).
168
-
169
- By default webhooks may only target public addresses and loopback; other private addresses (for example a LAN host or
170
- `n8n.internal`) must be added to the webhook allowlist in **Settings**.
171
-
172
- ## Security model
173
-
174
- agentinbox assumes agents can be manipulated by the mail they read. So it keeps content intact and puts the hard limits
175
- on actions.
176
-
177
- **Content is cleaned, not censored.** Agents get full-fidelity mail converted to tidy Markdown, with links and tables.
178
- Only what a human reader couldn't see is removed: hidden CSS (`display:none`, zero-size, transparent or
179
- same-colour text), HTML comments, scripts and styles, zero-width characters, Unicode tag characters and bidi overrides.
180
- Each body is wrapped in one `<email_content … untrusted>` delimiter and comes with signals: SPF/DKIM/DMARC results, an
181
- external-sender flag, whether hidden content was removed, and phrases typical of prompt injection. Signals are
182
- informational; nothing is withheld because of them.
183
-
184
- **Actions are strictly controlled.** A new agent can do nothing. Each grant (agent × mailbox) sets:
185
-
186
- | Control | |
187
- |---|---|
188
- | Capabilities | `read` (search, read, attachments, events), `draft`, `send`, `organize` (labels, archive, read state), `trash` |
189
- | Label scope | Confine the agent to threads carrying certain labels. Searches are rewritten and fetches by id are checked. |
190
- | Send mode | `draft_only` (default: a Gmail draft for a human to send), `approval` (queued in the console; approving sends it) or `direct` |
191
- | Allowed recipients | Exact addresses or `*@domain.com` globs |
192
- | Reply only | Sending only inside threads the agent can already see |
193
- | Send caps | Per hour and per day |
194
-
195
- **Everything is audited.** Every tool call and every console change is recorded with its outcome, and denials name the
196
- guardrail that blocked them.
197
-
198
- **The console** is protected by an admin password (set through the one-time setup link), HttpOnly session cookies
199
- that expire after 30 days of inactivity (90 at most), a CSRF header on every change, request size limits, a strict
200
- Content Security Policy, and a 15-minute lockout after five wrong passwords (or 30 attempts) from one address. When
201
- failures pile up across many addresses, every sign-in attempt is slowed down instead. It never allows being framed.
202
- The data directory and database are readable only by their owner.
203
-
204
- Recommendations: split agents that read untrusted mail from agents that can send; prefer `draft_only` or `approval`;
205
- use allowlists for anything that sends directly.
206
-
207
- ## Running it
208
-
209
- ### As a service
210
-
211
- ```sh
212
- npx agentinbox-cli service install
213
- ```
214
-
215
- Installs a background service that starts at boot: systemd on Linux (a user service with lingering enabled, or a system
216
- service when run as root) and launchd on macOS. The service runs its own copy of agentinbox from the data directory,
217
- so it never depends on the npx cache; a failed install leaves the running service as it was. Re-run `service install`
218
- to update; `service status` and `service uninstall` do what you'd expect, and uninstalling keeps your data. Pass the same
219
- `--port`, `--host` and `--data` options each time. `AGENTINBOX_MASTER_KEY`, `TRUST_PROXY`, `TRUST_PROXY_HOPS` and
220
- `AGENTINBOX_LOG_LEVEL` are taken from your shell and remembered in `<data>/service.env`, so later installs keep them
221
- (set one to an empty value to remove it).
222
-
223
- Only one agentinbox runs per data directory; a second one refuses to start.
224
-
225
- When a newer version is published, the console shows **Update available** with the exact command to run.
226
-
227
- ### Behind a reverse proxy
228
-
229
- agentinbox serves plain HTTP. To expose it beyond your network, put it behind a TLS-terminating proxy (Caddy, nginx,
230
- Tailscale Funnel) and:
231
-
232
- - bind it to localhost: `--host 127.0.0.1`
233
- - set `TRUST_PROXY=1`, so lockouts apply per client and cookies are marked `Secure`. The client address is taken from
234
- the right end of `X-Forwarded-For`; with more than one proxy in front, set `TRUST_PROXY_HOPS` to their number
235
- - set **Settings → Public URL** to the https address, so MCP snippets, the skill and the OAuth redirect use it
236
-
237
- `GET /health` (or `/api/health`) needs no auth and suits uptime monitors: `200` when healthy, `503` when degraded, for
238
- example when an account needs to be reconnected or ingest is failing. It reports only a status, the version and
239
- whether setup is complete.
240
-
241
- ### Options
242
-
243
- With a global install (`npm i -g agentinbox-cli`); otherwise prefix `npx agentinbox-cli` instead of `agentinbox`.
244
-
245
- ```
246
- agentinbox [start] [--port 8425] [--host 0.0.0.0] [--data ~/.agentinbox] [--no-open] [--dev]
247
- agentinbox service install|uninstall|status [--port …] [--host …] [--data …]
248
- agentinbox skill install [--target claude|codex|agents|<dir>] [--url <public url>]
249
- agentinbox skill print [--url <public url>]
250
- agentinbox reset-password [--data …]
251
- ```
252
-
253
- | Flag | Env | Default | |
254
- |---|---|---|---|
255
- | `--port` | `PORT` | `8425` | Port to listen on |
256
- | `--host` | `HOST` | `0.0.0.0` | Bind address. Use `127.0.0.1` behind a reverse proxy or SSH tunnel |
257
- | `--data` | `AGENTINBOX_DATA` | `~/.agentinbox` | Data directory |
258
- | | `AGENTINBOX_MASTER_KEY` | | Base64 32-byte key for encrypting secrets, instead of `<data>/master.key` |
259
- | | `TRUST_PROXY` | | `1` behind a reverse proxy: client IPs from `X-Forwarded-For`, https from `X-Forwarded-Proto` |
260
- | | `TRUST_PROXY_HOPS` | `1` | Number of proxies in front of agentinbox (which `X-Forwarded-For` entry, from the right, is the client) |
261
- | | `AGENTINBOX_LOG_LEVEL` | `info` | `debug`, `info`, `warn` or `error` |
262
-
263
- Forgot the admin password? Run `npx agentinbox-cli reset-password` on the server, as the user agentinbox runs as (`sudo` for a
264
- system service) and with the same `--data`. It signs out all sessions and prints a new setup link; a running instance
265
- picks it up without a restart.
266
-
267
- ## Data and backups
268
-
269
- Everything lives in the data directory (`~/.agentinbox` by default):
270
-
271
- | File | |
272
- |---|---|
273
- | `agentinbox.db` | SQLite database: accounts, encrypted tokens, agents, grants, events, deliveries, approvals, audit log |
274
- | `master.key` | Encrypts refresh tokens, webhook secrets and the OAuth client secret (mode 0600) |
275
- | `setup-token` | The one-time setup token, until the instance is claimed |
276
- | `agentinbox.lock` | Marks the running instance (pid and port) |
277
- | `service.env` | Environment the service was installed with, possibly including `AGENTINBOX_MASTER_KEY` (mode 0600; `service install` only) |
278
- | `agentinbox.log` | Service output on macOS (launchd); on Linux use `journalctl --user -u agentinbox` (or without `--user` for a system service) |
279
- | `app/` | The service's own install (`service install` only) |
280
-
281
- Back up `agentinbox.db` and `master.key` together: without the key, stored secrets can't be decrypted, and agentinbox
282
- refuses to start rather than create a new one. Keep the key somewhere safer than the database backup, or supply it via
283
- `AGENTINBOX_MASTER_KEY`. The database runs in WAL mode, so don't just copy the file while agentinbox is running: stop
284
- it first (`systemctl --user stop agentinbox`), or take a consistent copy with
285
- `sqlite3 ~/.agentinbox/agentinbox.db ".backup /path/to/backup.db"`. Mail
286
- bodies are never stored; agentinbox keeps metadata (ids, headers, labels, snippets) and fetches content from Gmail on
287
- demand. Events, deliveries and audit entries are pruned after the retention period set in **Settings** (30 days by
288
- default).
289
-
290
- ## Development
291
-
292
- ```sh
293
- pnpm install
294
- AGENTINBOX_DATA=/tmp/agentinbox-dev pnpm dev
295
- ```
296
-
297
- `pnpm dev` runs the server on `:8425` in watch mode (API, MCP and OAuth only) and the console on Vite at `:5173`, which
298
- proxies to it. Open the Vite URL; the printed setup link already points there.
17
+ ![Overview](docs/screenshots/overview.png)
299
18
 
300
- ```sh
301
- pnpm typecheck && pnpm lint && pnpm test
302
- pnpm build # console → dist/web, server → dist/cli.js
303
- node dist/cli.js # run the build
304
- ```
19
+ ## More
305
20
 
306
- Stack: TypeScript on Node 22+, Hono on `@hono/node-server`, `node:sqlite`, the MCP TypeScript SDK, and thin fetch-based
307
- Google clients (no `googleapis`, so `npx` stays fast). The console is Vite, React 19, TanStack Router / Query / Table /
308
- Form and shadcn/ui on Tailwind v4.
21
+ - [Reference](docs/reference.md): features, configuration, running as a service or behind a proxy, security model,
22
+ webhooks, data and backups, development
23
+ - [Waking local agents with `agentinbox listen`](docs/listen.md)
24
+ - [Screenshots](docs/screenshots/README.md)
309
25
 
310
26
  ## License
311
27
 
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env node
2
+ import { S as ApiError, a as pidAlive, b as Dispatcher, c as writeDead, d as forwardSecret, f as resolveListen, g as checkTransport, h as TransportError, l as DEFAULT_LISTEN_TYPES, m as forwardEvent, o as readDead, s as readStatus, w as getSelf, x as describeGrants, y as runCommand } from "./listen-cDUHSBkN.js";
3
+ import { i as safeText, r as formatDuration } from "./ui-B8Sd4Y7Y.js";
4
+ import { d as NPX, i as saveProfile, n as loadProfile, s as ensurePrivateDir } from "./profile-uRinkfdm.js";
5
+ import { c as profilePath, i as deadPath, o as logPath, p as validProfileName } from "./paths-DckASYtc.js";
6
+ import { d as unitName, n as isServiceInstalled, r as launchdLabel } from "./service-DXiBV61B.js";
7
+ import pc from "picocolors";
8
+ //#region server/listen/admin.ts
9
+ const out = (s = "") => process.stdout.write(`${s}\n`);
10
+ const flagFor = (profile) => profile === "default" ? "" : ` --profile ${profile}`;
11
+ const ago = (iso) => iso ? `${formatDuration(Date.now() - Date.parse(iso))} ago` : "never";
12
+ /** Prints the forward-mode signing secret (generating and saving it on first use). */
13
+ function printSecret(profileName, opts) {
14
+ const name = validProfileName(profileName);
15
+ const profile = loadProfile(name);
16
+ let { secret, created } = forwardSecret(profile);
17
+ if (opts.rotate) {
18
+ secret = forwardSecret({
19
+ ...profile,
20
+ forward: void 0,
21
+ forwardSecret: void 0
22
+ }).secret;
23
+ created = true;
24
+ }
25
+ if (created) {
26
+ if (profile.forward) profile.forward = {
27
+ ...profile.forward,
28
+ secret
29
+ };
30
+ else profile.forwardSecret = secret;
31
+ saveProfile(name, profile);
32
+ process.stderr.write(pc.gray(`${opts.rotate ? "Rotated" : "Generated"} the forward signing secret for profile "${name}"${opts.rotate ? "; restart the listener" : ""}.\n`));
33
+ }
34
+ out(secret);
35
+ }
36
+ async function printStatus(profileName) {
37
+ const name = validProfileName(profileName);
38
+ const profile = loadProfile(name);
39
+ const st = readStatus(name);
40
+ const alive = st ? pidAlive(st.pid) && !st.stoppedAt : false;
41
+ const dead = readDead(name);
42
+ out(`${pc.bold("Profile")} ${name} ${pc.gray(profilePath(name))}`);
43
+ out(`${pc.bold("Agent")} ${safeText(profile.agentName)} ${pc.gray(`(${profile.agentId})`)}`);
44
+ out(`${pc.bold("Daemon")} ${profile.url}`);
45
+ out(`${pc.bold("Consumer")} ${st?.consumer ?? profile.consumer}`);
46
+ const exec = st && st.exec !== void 0 ? st.exec : profile.exec;
47
+ const forward = st && st.forward !== void 0 ? st.forward : profile.forward?.url;
48
+ const targets = [exec ? `exec ${safeText(exec, 120)}` : null, forward ? `forward ${forward}` : null].filter(Boolean);
49
+ out(`${pc.bold("Runs")} ${targets.length ? targets.join(" · ") : pc.gray("nothing saved (pass --exec/--forward with --save)")}`);
50
+ out(`${pc.bold("Events")} ${profile.types?.length ? profile.types.join(", ") : `${DEFAULT_LISTEN_TYPES.join(", ")} ${pc.gray("(default types; add --types all --save for every event)")}`}`);
51
+ let svc = "not installed";
52
+ if (process.platform === "linux" || process.platform === "darwin") {
53
+ if (isServiceInstalled(name)) svc = `installed (${process.platform === "linux" ? unitName(name) : launchdLabel(name)}; details: ${NPX} listen service status${flagFor(name)})`;
54
+ }
55
+ out(`${pc.bold("Service")} ${svc}`);
56
+ try {
57
+ await checkTransport(profile.url, { insecure: profile.insecure });
58
+ const self = await getSelf(profile.url, profile.apiKey, 5e3);
59
+ out(`${pc.bold("Key")} ${pc.green("valid")}`);
60
+ const lines = describeGrants(self.grants);
61
+ out(`${pc.bold("Access")} ${self.grants.length ? lines[0] : pc.yellow(lines[0])}`);
62
+ for (const l of lines.slice(1)) out(` ${l}`);
63
+ } catch (err) {
64
+ const revoked = err instanceof ApiError && (err.status === 401 || err.status === 403);
65
+ const why = err instanceof ApiError && err.status === 0 ? err.cause : err instanceof Error ? err.message.split("\n")[0] : String(err);
66
+ if (err instanceof TransportError) out(`${pc.bold("Key")} ${pc.red(`not checked: ${why}`)}`);
67
+ else out(`${pc.bold("Key")} ${revoked ? pc.red(`rejected: ${err.message}`) : pc.yellow(`not checked (${why})`)}`);
68
+ }
69
+ if (!st) out(`${pc.bold("Listener")} ${pc.gray("has not run yet")}`);
70
+ else {
71
+ const state = !alive ? pc.gray(`not running${st.stoppedAt ? ` (stopped ${ago(st.stoppedAt)}${st.exitReason ? `: ${safeText(st.exitReason, 160)}` : ""})` : " (exited unexpectedly)"}`) : st.connected ? pc.green(`running (pid ${st.pid}), connected ${ago(st.connectedSince)}`) : pc.yellow(`running (pid ${st.pid}), disconnected${st.lastDisconnect ? ` ${ago(st.lastDisconnect.at)}: ${safeText(st.lastDisconnect.reason, 160)}` : ""}`);
72
+ out(`${pc.bold("Listener")} ${state}`);
73
+ out(`${pc.bold("Cursor")} acked #${st.ackedSeq} · received #${st.receivedSeq}${alive && st.running ? ` · ${st.running} in progress` : ""} · last event ${ago(st.lastEventAt)}`);
74
+ if (st.recent.length) {
75
+ out(pc.bold("Recent"));
76
+ for (const r of st.recent.slice(0, 8)) {
77
+ const icon = r.outcome === "succeeded" ? pc.green("✓") : r.outcome === "dead" ? pc.red("✖") : r.outcome === "retrying" ? pc.yellow("↻") : pc.gray("·");
78
+ out(` ${icon} #${r.seq} ${r.type} ${pc.gray(`${ago(r.at)}${r.attempts > 1 ? `, ${r.attempts} attempts` : ""}${r.error && r.outcome !== "succeeded" ? `, ${safeText(r.error, 100)}` : ""}`)}`);
79
+ }
80
+ }
81
+ }
82
+ out(`${pc.bold("Dead")} ${dead.length ? pc.red(`${dead.length} event${dead.length > 1 ? "s" : ""}`) + pc.gray(` (inspect: ${NPX} listen dead${flagFor(name)})`) : "0"}`);
83
+ out(`${pc.bold("Log")} ${logPath(name)}`);
84
+ }
85
+ function printDead(profileName, opts) {
86
+ const name = validProfileName(profileName);
87
+ const entries = readDead(name);
88
+ if (opts.json) {
89
+ for (const e of entries) out(JSON.stringify(e));
90
+ return;
91
+ }
92
+ if (!entries.length) {
93
+ out(`No dead events for profile "${name}".`);
94
+ return;
95
+ }
96
+ for (const e of entries) {
97
+ const m = e.payload.data.message;
98
+ out(`${pc.red("✖")} #${e.seq} ${e.type} ${pc.gray(e.id)} ${pc.gray(`died ${ago(e.deadAt)} after ${e.attempts} attempts`)}`);
99
+ if (m) out(` from ${safeText(m.from?.email ?? "?", 80)} "${safeText(m.subject, 80)}"`);
100
+ out(pc.gray(` ${safeText(e.error, 300)}`));
101
+ }
102
+ out();
103
+ out(pc.gray(`${entries.length} dead event${entries.length > 1 ? "s" : ""} in ${deadPath(name)}.`));
104
+ out(pc.gray(`Replay with: ${NPX} listen dead --replay${flagFor(name)} (one: --id <evt_…>; drop all: --clear)`));
105
+ out(pc.gray(`Replay uses the profile's saved --exec/--forward; if you ran them as flags only, pass them again, e.g. --replay --exec '<command>'.`));
106
+ }
107
+ function clearDead(profileName, id) {
108
+ const name = validProfileName(profileName);
109
+ const entries = readDead(name);
110
+ const keep = id ? entries.filter((e) => e.id !== id) : [];
111
+ writeDead(name, keep);
112
+ out(`Removed ${entries.length - keep.length} dead event${entries.length - keep.length === 1 ? "" : "s"}.`);
113
+ }
114
+ /**
115
+ * Runs dead events again through the profile's targets (or the ones passed as flags). Successes are
116
+ * removed from dead.jsonl; failures stay. Returns how many still failed.
117
+ */
118
+ async function replayDead(flags, opts = {}) {
119
+ const name = validProfileName(flags.profile ?? "default");
120
+ const profile = loadProfile(name);
121
+ const { config } = resolveListen(name, profile, {
122
+ ...flags,
123
+ save: false
124
+ });
125
+ const chosen = readDead(name).filter((e) => !flags.id || e.id === flags.id);
126
+ if (!chosen.length) {
127
+ out(flags.id ? `No dead event with id ${flags.id}.` : `No dead events for profile "${name}".`);
128
+ return 0;
129
+ }
130
+ const available = [];
131
+ if (config.exec) available.push("exec");
132
+ if (config.forward) available.push("forward");
133
+ if (!available.length) throw new Error(`Nothing to replay to: pass --exec '<command>' and/or --forward <url> (or save them in the profile with \`${NPX} listen --save …\`).`);
134
+ if (config.exec && process.platform === "win32") throw new Error("--exec isn't supported on Windows; replay with --forward instead.");
135
+ const timeoutMs = config.timeoutSeconds * 1e3;
136
+ if (config.cwdIsDefault) ensurePrivateDir(config.cwd);
137
+ const failed = /* @__PURE__ */ new Map();
138
+ const bySeq = new Map(chosen.map((e) => [e.seq, e]));
139
+ const dispatcher = new Dispatcher({
140
+ targets: available,
141
+ concurrency: config.concurrency,
142
+ maxAttempts: config.maxAttempts,
143
+ retryDelayMs: opts.retryDelayMs,
144
+ run: async (job, signal) => {
145
+ const errors = [];
146
+ if (job.remaining.has("exec") && config.exec) {
147
+ const r = await runCommand(config.exec, job.payload, {
148
+ timeoutMs,
149
+ signal,
150
+ cwd: config.cwd
151
+ });
152
+ if (r.ok) job.remaining.delete("exec");
153
+ else errors.push(`command: ${r.error}`);
154
+ }
155
+ if (job.remaining.has("forward") && config.forward) {
156
+ const r = await forwardEvent(config.forward.url, config.forward.secret, job.payload, {
157
+ timeoutMs: Math.min(timeoutMs, 3e4),
158
+ signal
159
+ });
160
+ if (r.ok) job.remaining.delete("forward");
161
+ else errors.push(`forward: ${r.error}`);
162
+ }
163
+ return errors;
164
+ },
165
+ onAdvance: () => {},
166
+ onDead: (job) => {
167
+ const e = bySeq.get(job.seq);
168
+ failed.set(e.id, {
169
+ ...e,
170
+ deadAt: (/* @__PURE__ */ new Date()).toISOString(),
171
+ attempts: e.attempts + job.attempts,
172
+ error: job.lastError ?? "failed"
173
+ });
174
+ out(`${pc.red("✖")} #${job.seq} still failing: ${safeText(job.lastError ?? "failed", 200)}`);
175
+ },
176
+ onFinished: (job) => {
177
+ if (job.outcome === "succeeded") out(`${pc.green("✓")} #${job.seq} ${job.payload.type} replayed`);
178
+ }
179
+ });
180
+ out(`Replaying ${chosen.length} dead event${chosen.length > 1 ? "s" : ""} (up to ${config.maxAttempts} attempts each)…`);
181
+ for (const e of [...chosen].sort((a, b) => a.seq - b.seq)) {
182
+ const targets = e.targets?.length ? e.targets.filter((t) => available.includes(t)) : available;
183
+ if (!targets.length) {
184
+ out(`${pc.yellow("!")} #${e.seq} needs ${e.targets.join(" and ")}, which isn't configured now; pass --${e.targets[0]} to replay it.`);
185
+ failed.set(e.id, e);
186
+ continue;
187
+ }
188
+ dispatcher.push(e.payload, { targets });
189
+ }
190
+ await dispatcher.idle();
191
+ const replayed = new Set(chosen.map((e) => e.id));
192
+ const current = readDead(name);
193
+ writeDead(name, [...current.filter((e) => !replayed.has(e.id)), ...failed.values()]);
194
+ out(`${chosen.length - failed.size} replayed, ${failed.size} still dead.`);
195
+ return failed.size;
196
+ }
197
+ //#endregion
198
+ export { clearDead, printDead, printSecret, printStatus, replayDead };
199
+
200
+ //# sourceMappingURL=admin-8MQLrhsj.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin-8MQLrhsj.js","names":[],"sources":["../server/listen/admin.ts"],"sourcesContent":["import pc from \"picocolors\";\nimport { ApiError, getSelf } from \"./api.ts\";\nimport { describeGrants } from \"./grants.ts\";\nimport { Dispatcher, type Job, type Target } from \"./dispatcher.ts\";\nimport { runCommand } from \"./exec.ts\";\nimport { ensurePrivateDir } from \"./fsutil.ts\";\nimport { forwardEvent } from \"./forward.ts\";\nimport { type ListenFlags, resolveListen } from \"./options.ts\";\nimport { DEFAULT_PROFILE, deadPath, logPath, profilePath, validProfileName } from \"./paths.ts\";\nimport { loadProfile, saveProfile } from \"./profile.ts\";\nimport { DEFAULT_LISTEN_TYPES, forwardSecret } from \"./options.ts\";\nimport { isServiceInstalled, launchdLabel, unitName } from \"./service.ts\";\nimport { type DeadEntry, pidAlive, readDead, readStatus, writeDead } from \"./state.ts\";\nimport { checkTransport, TransportError } from \"./transport.ts\";\nimport { formatDuration, safeText } from \"./ui.ts\";\nimport { NPX } from \"./version.ts\";\n\nconst out = (s = \"\") => process.stdout.write(`${s}\\n`);\nconst flagFor = (profile: string) => (profile === DEFAULT_PROFILE ? \"\" : ` --profile ${profile}`);\nconst ago = (iso: string | null | undefined) => (iso ? `${formatDuration(Date.now() - Date.parse(iso))} ago` : \"never\");\n\n// ───────────── listen secret ─────────────\n\n/** Prints the forward-mode signing secret (generating and saving it on first use). */\nexport function printSecret(profileName: string, opts: { rotate?: boolean }): void {\n const name = validProfileName(profileName);\n const profile = loadProfile(name);\n let { secret, created } = forwardSecret(profile);\n if (opts.rotate) {\n secret = forwardSecret({ ...profile, forward: undefined, forwardSecret: undefined }).secret;\n created = true;\n }\n if (created) {\n if (profile.forward) profile.forward = { ...profile.forward, secret };\n else profile.forwardSecret = secret;\n saveProfile(name, profile);\n process.stderr.write(pc.gray(`${opts.rotate ? \"Rotated\" : \"Generated\"} the forward signing secret for profile \"${name}\"${opts.rotate ? \"; restart the listener\" : \"\"}.\\n`));\n }\n // The secret alone on stdout, so `export AGENTINBOX_WEBHOOK_SECRET=$(agentinbox listen secret)` works.\n out(secret);\n}\n\n// ───────────── listen status ─────────────\n\nexport async function printStatus(profileName: string): Promise<void> {\n const name = validProfileName(profileName);\n const profile = loadProfile(name);\n const st = readStatus(name);\n const alive = st ? pidAlive(st.pid) && !st.stoppedAt : false;\n const dead = readDead(name);\n\n out(`${pc.bold(\"Profile\")} ${name} ${pc.gray(profilePath(name))}`);\n out(`${pc.bold(\"Agent\")} ${safeText(profile.agentName)} ${pc.gray(`(${profile.agentId})`)}`);\n out(`${pc.bold(\"Daemon\")} ${profile.url}`);\n out(`${pc.bold(\"Consumer\")} ${st?.consumer ?? profile.consumer}`);\n // What the last run used (flags included), else what the profile has saved.\n const exec = st && st.exec !== undefined ? st.exec : profile.exec;\n const forward = st && st.forward !== undefined ? st.forward : profile.forward?.url;\n const targets = [exec ? `exec ${safeText(exec, 120)}` : null, forward ? `forward ${forward}` : null].filter(Boolean);\n out(`${pc.bold(\"Runs\")} ${targets.length ? targets.join(\" · \") : pc.gray(\"nothing saved (pass --exec/--forward with --save)\")}`);\n out(\n `${pc.bold(\"Events\")} ${\n profile.types?.length\n ? profile.types.join(\", \")\n : `${DEFAULT_LISTEN_TYPES.join(\", \")} ${pc.gray(\"(default types; add --types all --save for every event)\")}`\n }`,\n );\n\n let svc = \"not installed\";\n if (process.platform === \"linux\" || process.platform === \"darwin\") {\n if (isServiceInstalled(name)) svc = `installed (${process.platform === \"linux\" ? unitName(name) : launchdLabel(name)}; details: ${NPX} listen service status${flagFor(name)})`;\n }\n out(`${pc.bold(\"Service\")} ${svc}`);\n\n try {\n // Never send the key over a connection `listen` itself would refuse.\n await checkTransport(profile.url, { insecure: profile.insecure });\n const self = await getSelf(profile.url, profile.apiKey, 5000);\n out(`${pc.bold(\"Key\")} ${pc.green(\"valid\")}`);\n // The access the console actually granted, in plain words (what the agent should report to its human).\n const lines = describeGrants(self.grants);\n out(`${pc.bold(\"Access\")} ${self.grants.length ? lines[0] : pc.yellow(lines[0]!)}`);\n for (const l of lines.slice(1)) out(` ${l}`);\n } catch (err) {\n const revoked = err instanceof ApiError && (err.status === 401 || err.status === 403);\n const why = err instanceof ApiError && err.status === 0 ? (err.cause as string) : err instanceof Error ? err.message.split(\"\\n\")[0] : String(err);\n if (err instanceof TransportError) out(`${pc.bold(\"Key\")} ${pc.red(`not checked: ${why}`)}`);\n else out(`${pc.bold(\"Key\")} ${revoked ? pc.red(`rejected: ${(err as Error).message}`) : pc.yellow(`not checked (${why})`)}`);\n }\n\n if (!st) {\n out(`${pc.bold(\"Listener\")} ${pc.gray(\"has not run yet\")}`);\n } else {\n const state = !alive\n ? pc.gray(`not running${st.stoppedAt ? ` (stopped ${ago(st.stoppedAt)}${st.exitReason ? `: ${safeText(st.exitReason, 160)}` : \"\"})` : \" (exited unexpectedly)\"}`)\n : st.connected\n ? pc.green(`running (pid ${st.pid}), connected ${ago(st.connectedSince)}`)\n : pc.yellow(`running (pid ${st.pid}), disconnected${st.lastDisconnect ? ` ${ago(st.lastDisconnect.at)}: ${safeText(st.lastDisconnect.reason, 160)}` : \"\"}`);\n out(`${pc.bold(\"Listener\")} ${state}`);\n out(`${pc.bold(\"Cursor\")} acked #${st.ackedSeq} · received #${st.receivedSeq}${alive && st.running ? ` · ${st.running} in progress` : \"\"} · last event ${ago(st.lastEventAt)}`);\n if (st.recent.length) {\n out(pc.bold(\"Recent\"));\n for (const r of st.recent.slice(0, 8)) {\n const icon = r.outcome === \"succeeded\" ? pc.green(\"✓\") : r.outcome === \"dead\" ? pc.red(\"✖\") : r.outcome === \"retrying\" ? pc.yellow(\"↻\") : pc.gray(\"·\");\n out(` ${icon} #${r.seq} ${r.type} ${pc.gray(`${ago(r.at)}${r.attempts > 1 ? `, ${r.attempts} attempts` : \"\"}${r.error && r.outcome !== \"succeeded\" ? `, ${safeText(r.error, 100)}` : \"\"}`)}`);\n }\n }\n }\n out(`${pc.bold(\"Dead\")} ${dead.length ? pc.red(`${dead.length} event${dead.length > 1 ? \"s\" : \"\"}`) + pc.gray(` (inspect: ${NPX} listen dead${flagFor(name)})`) : \"0\"}`);\n out(`${pc.bold(\"Log\")} ${logPath(name)}`);\n}\n\n// ───────────── listen dead ─────────────\n\nexport function printDead(profileName: string, opts: { json?: boolean }): void {\n const name = validProfileName(profileName);\n const entries = readDead(name);\n if (opts.json) {\n for (const e of entries) out(JSON.stringify(e));\n return;\n }\n if (!entries.length) {\n out(`No dead events for profile \"${name}\".`);\n return;\n }\n for (const e of entries) {\n const m = e.payload.data.message;\n out(`${pc.red(\"✖\")} #${e.seq} ${e.type} ${pc.gray(e.id)} ${pc.gray(`died ${ago(e.deadAt)} after ${e.attempts} attempts`)}`);\n if (m) out(` from ${safeText(m.from?.email ?? \"?\", 80)} \"${safeText(m.subject, 80)}\"`);\n out(pc.gray(` ${safeText(e.error, 300)}`));\n }\n out();\n out(pc.gray(`${entries.length} dead event${entries.length > 1 ? \"s\" : \"\"} in ${deadPath(name)}.`));\n out(pc.gray(`Replay with: ${NPX} listen dead --replay${flagFor(name)} (one: --id <evt_…>; drop all: --clear)`));\n out(pc.gray(`Replay uses the profile's saved --exec/--forward; if you ran them as flags only, pass them again, e.g. --replay --exec '<command>'.`));\n}\n\nexport function clearDead(profileName: string, id?: string): void {\n const name = validProfileName(profileName);\n const entries = readDead(name);\n const keep = id ? entries.filter((e) => e.id !== id) : [];\n writeDead(name, keep);\n out(`Removed ${entries.length - keep.length} dead event${entries.length - keep.length === 1 ? \"\" : \"s\"}.`);\n}\n\n/**\n * Runs dead events again through the profile's targets (or the ones passed as flags). Successes are\n * removed from dead.jsonl; failures stay. Returns how many still failed.\n */\nexport async function replayDead(flags: ListenFlags & { id?: string }, opts: { retryDelayMs?: (n: number) => number } = {}): Promise<number> {\n const name = validProfileName(flags.profile ?? DEFAULT_PROFILE);\n const profile = loadProfile(name);\n const { config } = resolveListen(name, profile, { ...flags, save: false });\n const all = readDead(name);\n const chosen = all.filter((e) => !flags.id || e.id === flags.id);\n if (!chosen.length) {\n out(flags.id ? `No dead event with id ${flags.id}.` : `No dead events for profile \"${name}\".`);\n return 0;\n }\n const available: Target[] = [];\n if (config.exec) available.push(\"exec\");\n if (config.forward) available.push(\"forward\");\n if (!available.length) throw new Error(`Nothing to replay to: pass --exec '<command>' and/or --forward <url> (or save them in the profile with \\`${NPX} listen --save …\\`).`);\n if (config.exec && process.platform === \"win32\") throw new Error(\"--exec isn't supported on Windows; replay with --forward instead.\");\n\n const timeoutMs = config.timeoutSeconds * 1000;\n if (config.cwdIsDefault) ensurePrivateDir(config.cwd);\n const failed = new Map<string, DeadEntry>();\n const bySeq = new Map(chosen.map((e) => [e.seq, e]));\n const dispatcher = new Dispatcher({\n targets: available,\n concurrency: config.concurrency,\n maxAttempts: config.maxAttempts,\n retryDelayMs: opts.retryDelayMs,\n run: async (job: Job, signal) => {\n const errors: string[] = [];\n if (job.remaining.has(\"exec\") && config.exec) {\n const r = await runCommand(config.exec, job.payload, { timeoutMs, signal, cwd: config.cwd });\n if (r.ok) job.remaining.delete(\"exec\");\n else errors.push(`command: ${r.error}`);\n }\n if (job.remaining.has(\"forward\") && config.forward) {\n const r = await forwardEvent(config.forward.url, config.forward.secret, job.payload, { timeoutMs: Math.min(timeoutMs, 30_000), signal });\n if (r.ok) job.remaining.delete(\"forward\");\n else errors.push(`forward: ${r.error}`);\n }\n return errors;\n },\n onAdvance: () => {},\n onDead: (job) => {\n const e = bySeq.get(job.seq)!;\n failed.set(e.id, { ...e, deadAt: new Date().toISOString(), attempts: e.attempts + job.attempts, error: job.lastError ?? \"failed\" });\n out(`${pc.red(\"✖\")} #${job.seq} still failing: ${safeText(job.lastError ?? \"failed\", 200)}`);\n },\n onFinished: (job) => {\n if (job.outcome === \"succeeded\") out(`${pc.green(\"✓\")} #${job.seq} ${job.payload.type} replayed`);\n },\n });\n out(`Replaying ${chosen.length} dead event${chosen.length > 1 ? \"s\" : \"\"} (up to ${config.maxAttempts} attempts each)…`);\n // Only the targets that never succeeded (a forward that went through isn't POSTed again).\n for (const e of [...chosen].sort((a, b) => a.seq - b.seq)) {\n const targets = e.targets?.length ? e.targets.filter((t) => available.includes(t)) : available;\n if (!targets.length) {\n out(`${pc.yellow(\"!\")} #${e.seq} needs ${e.targets.join(\" and \")}, which isn't configured now; pass --${e.targets[0]} to replay it.`);\n failed.set(e.id, e);\n continue;\n }\n dispatcher.push(e.payload, { targets });\n }\n await dispatcher.idle();\n\n const replayed = new Set(chosen.map((e) => e.id));\n // Re-read so entries added by a running listener meanwhile are kept.\n const current = readDead(name);\n writeDead(name, [...current.filter((e) => !replayed.has(e.id)), ...failed.values()]);\n out(`${chosen.length - failed.size} replayed, ${failed.size} still dead.`);\n return failed.size;\n}\n"],"mappings":";;;;;;;;AAiBA,MAAM,OAAO,IAAI,OAAO,QAAQ,OAAO,MAAM,GAAG,EAAE,GAAG;AACrD,MAAM,WAAW,YAAqB,YAAA,YAA8B,KAAK,cAAc;AACvF,MAAM,OAAO,QAAoC,MAAM,GAAG,eAAe,KAAK,IAAI,IAAI,KAAK,MAAM,GAAG,CAAC,EAAE,QAAQ;;AAK/G,SAAgB,YAAY,aAAqB,MAAkC;CACjF,MAAM,OAAO,iBAAiB,WAAW;CACzC,MAAM,UAAU,YAAY,IAAI;CAChC,IAAI,EAAE,QAAQ,YAAY,cAAc,OAAO;CAC/C,IAAI,KAAK,QAAQ;EACf,SAAS,cAAc;GAAE,GAAG;GAAS,SAAS,KAAA;GAAW,eAAe,KAAA;EAAU,CAAC,CAAC,CAAC;EACrF,UAAU;CACZ;CACA,IAAI,SAAS;EACX,IAAI,QAAQ,SAAS,QAAQ,UAAU;GAAE,GAAG,QAAQ;GAAS;EAAO;OAC/D,QAAQ,gBAAgB;EAC7B,YAAY,MAAM,OAAO;EACzB,QAAQ,OAAO,MAAM,GAAG,KAAK,GAAG,KAAK,SAAS,YAAY,YAAY,2CAA2C,KAAK,GAAG,KAAK,SAAS,2BAA2B,GAAG,IAAI,CAAC;CAC5K;CAEA,IAAI,MAAM;AACZ;AAIA,eAAsB,YAAY,aAAoC;CACpE,MAAM,OAAO,iBAAiB,WAAW;CACzC,MAAM,UAAU,YAAY,IAAI;CAChC,MAAM,KAAK,WAAW,IAAI;CAC1B,MAAM,QAAQ,KAAK,SAAS,GAAG,GAAG,KAAK,CAAC,GAAG,YAAY;CACvD,MAAM,OAAO,SAAS,IAAI;CAE1B,IAAI,GAAG,GAAG,KAAK,SAAS,EAAE,KAAK,KAAK,IAAI,GAAG,KAAK,YAAY,IAAI,CAAC,GAAG;CACpE,IAAI,GAAG,GAAG,KAAK,OAAO,EAAE,OAAO,SAAS,QAAQ,SAAS,EAAE,GAAG,GAAG,KAAK,IAAI,QAAQ,QAAQ,EAAE,GAAG;CAC/F,IAAI,GAAG,GAAG,KAAK,QAAQ,EAAE,MAAM,QAAQ,KAAK;CAC5C,IAAI,GAAG,GAAG,KAAK,UAAU,EAAE,IAAI,IAAI,YAAY,QAAQ,UAAU;CAEjE,MAAM,OAAO,MAAM,GAAG,SAAS,KAAA,IAAY,GAAG,OAAO,QAAQ;CAC7D,MAAM,UAAU,MAAM,GAAG,YAAY,KAAA,IAAY,GAAG,UAAU,QAAQ,SAAS;CAC/E,MAAM,UAAU,CAAC,OAAO,QAAQ,SAAS,MAAM,GAAG,MAAM,MAAM,UAAU,WAAW,YAAY,IAAI,CAAC,CAAC,OAAO,OAAO;CACnH,IAAI,GAAG,GAAG,KAAK,MAAM,EAAE,QAAQ,QAAQ,SAAS,QAAQ,KAAK,KAAK,IAAI,GAAG,KAAK,mDAAmD,GAAG;CACpI,IACE,GAAG,GAAG,KAAK,QAAQ,EAAE,MACnB,QAAQ,OAAO,SACX,QAAQ,MAAM,KAAK,IAAI,IACvB,GAAG,qBAAqB,KAAK,IAAI,EAAE,GAAG,GAAG,KAAK,yDAAyD,KAE/G;CAEA,IAAI,MAAM;CACV,IAAI,QAAQ,aAAa,WAAW,QAAQ,aAAa,UACnD;MAAA,mBAAmB,IAAI,GAAG,MAAM,cAAc,QAAQ,aAAa,UAAU,SAAS,IAAI,IAAI,aAAa,IAAI,EAAE,aAAa,IAAI,wBAAwB,QAAQ,IAAI,EAAE;CAAA;CAE9K,IAAI,GAAG,GAAG,KAAK,SAAS,EAAE,KAAK,KAAK;CAEpC,IAAI;EAEF,MAAM,eAAe,QAAQ,KAAK,EAAE,UAAU,QAAQ,SAAS,CAAC;EAChE,MAAM,OAAO,MAAM,QAAQ,QAAQ,KAAK,QAAQ,QAAQ,GAAI;EAC5D,IAAI,GAAG,GAAG,KAAK,KAAK,EAAE,SAAS,GAAG,MAAM,OAAO,GAAG;EAElD,MAAM,QAAQ,eAAe,KAAK,MAAM;EACxC,IAAI,GAAG,GAAG,KAAK,QAAQ,EAAE,MAAM,KAAK,OAAO,SAAS,MAAM,KAAK,GAAG,OAAO,MAAM,EAAG,GAAG;EACrF,KAAK,MAAM,KAAK,MAAM,MAAM,CAAC,GAAG,IAAI,aAAa,GAAG;CACtD,SAAS,KAAK;EACZ,MAAM,UAAU,eAAe,aAAa,IAAI,WAAW,OAAO,IAAI,WAAW;EACjF,MAAM,MAAM,eAAe,YAAY,IAAI,WAAW,IAAK,IAAI,QAAmB,eAAe,QAAQ,IAAI,QAAQ,MAAM,IAAI,CAAC,CAAC,KAAK,OAAO,GAAG;EAChJ,IAAI,eAAe,gBAAgB,IAAI,GAAG,GAAG,KAAK,KAAK,EAAE,SAAS,GAAG,IAAI,gBAAgB,KAAK,GAAG;OAC5F,IAAI,GAAG,GAAG,KAAK,KAAK,EAAE,SAAS,UAAU,GAAG,IAAI,aAAc,IAAc,SAAS,IAAI,GAAG,OAAO,gBAAgB,IAAI,EAAE,GAAG;CACnI;CAEA,IAAI,CAAC,IACH,IAAI,GAAG,GAAG,KAAK,UAAU,EAAE,IAAI,GAAG,KAAK,iBAAiB,GAAG;MACtD;EACL,MAAM,QAAQ,CAAC,QACX,GAAG,KAAK,cAAc,GAAG,YAAY,aAAa,IAAI,GAAG,SAAS,IAAI,GAAG,aAAa,KAAK,SAAS,GAAG,YAAY,GAAG,MAAM,GAAG,KAAK,0BAA0B,IAC9J,GAAG,YACD,GAAG,MAAM,gBAAgB,GAAG,IAAI,eAAe,IAAI,GAAG,cAAc,GAAG,IACvE,GAAG,OAAO,gBAAgB,GAAG,IAAI,iBAAiB,GAAG,iBAAiB,IAAI,IAAI,GAAG,eAAe,EAAE,EAAE,IAAI,SAAS,GAAG,eAAe,QAAQ,GAAG,MAAM,IAAI;EAC9J,IAAI,GAAG,GAAG,KAAK,UAAU,EAAE,IAAI,OAAO;EACtC,IAAI,GAAG,GAAG,KAAK,QAAQ,EAAE,aAAa,GAAG,SAAS,eAAe,GAAG,cAAc,SAAS,GAAG,UAAU,MAAM,GAAG,QAAQ,gBAAgB,GAAG,gBAAgB,IAAI,GAAG,WAAW,GAAG;EACjL,IAAI,GAAG,OAAO,QAAQ;GACpB,IAAI,GAAG,KAAK,QAAQ,CAAC;GACrB,KAAK,MAAM,KAAK,GAAG,OAAO,MAAM,GAAG,CAAC,GAAG;IACrC,MAAM,OAAO,EAAE,YAAY,cAAc,GAAG,MAAM,GAAG,IAAI,EAAE,YAAY,SAAS,GAAG,IAAI,GAAG,IAAI,EAAE,YAAY,aAAa,GAAG,OAAO,GAAG,IAAI,GAAG,KAAK,GAAG;IACrJ,IAAI,KAAK,KAAK,IAAI,EAAE,IAAI,GAAG,EAAE,KAAK,GAAG,GAAG,KAAK,GAAG,IAAI,EAAE,EAAE,IAAI,EAAE,WAAW,IAAI,KAAK,EAAE,SAAS,aAAa,KAAK,EAAE,SAAS,EAAE,YAAY,cAAc,KAAK,SAAS,EAAE,OAAO,GAAG,MAAM,IAAI,GAAG;GAC/L;EACF;CACF;CACA,IAAI,GAAG,GAAG,KAAK,MAAM,EAAE,QAAQ,KAAK,SAAS,GAAG,IAAI,GAAG,KAAK,OAAO,QAAQ,KAAK,SAAS,IAAI,MAAM,IAAI,IAAI,GAAG,KAAK,cAAc,IAAI,cAAc,QAAQ,IAAI,EAAE,EAAE,IAAI,KAAK;CAC5K,IAAI,GAAG,GAAG,KAAK,KAAK,EAAE,SAAS,QAAQ,IAAI,GAAG;AAChD;AAIA,SAAgB,UAAU,aAAqB,MAAgC;CAC7E,MAAM,OAAO,iBAAiB,WAAW;CACzC,MAAM,UAAU,SAAS,IAAI;CAC7B,IAAI,KAAK,MAAM;EACb,KAAK,MAAM,KAAK,SAAS,IAAI,KAAK,UAAU,CAAC,CAAC;EAC9C;CACF;CACA,IAAI,CAAC,QAAQ,QAAQ;EACnB,IAAI,+BAA+B,KAAK,GAAG;EAC3C;CACF;CACA,KAAK,MAAM,KAAK,SAAS;EACvB,MAAM,IAAI,EAAE,QAAQ,KAAK;EACzB,IAAI,GAAG,GAAG,IAAI,GAAG,EAAE,IAAI,EAAE,IAAI,GAAG,EAAE,KAAK,GAAG,GAAG,KAAK,EAAE,EAAE,EAAE,IAAI,GAAG,KAAK,QAAQ,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,UAAU,GAAG;EAC3H,IAAI,GAAG,IAAI,YAAY,SAAS,EAAE,MAAM,SAAS,KAAK,EAAE,EAAE,KAAK,SAAS,EAAE,SAAS,EAAE,EAAE,EAAE;EACzF,IAAI,GAAG,KAAK,OAAO,SAAS,EAAE,OAAO,GAAG,GAAG,CAAC;CAC9C;CACA,IAAI;CACJ,IAAI,GAAG,KAAK,GAAG,QAAQ,OAAO,aAAa,QAAQ,SAAS,IAAI,MAAM,GAAG,MAAM,SAAS,IAAI,EAAE,EAAE,CAAC;CACjG,IAAI,GAAG,KAAK,gBAAgB,IAAI,uBAAuB,QAAQ,IAAI,EAAE,0CAA0C,CAAC;CAChH,IAAI,GAAG,KAAK,qIAAqI,CAAC;AACpJ;AAEA,SAAgB,UAAU,aAAqB,IAAmB;CAChE,MAAM,OAAO,iBAAiB,WAAW;CACzC,MAAM,UAAU,SAAS,IAAI;CAC7B,MAAM,OAAO,KAAK,QAAQ,QAAQ,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC;CACxD,UAAU,MAAM,IAAI;CACpB,IAAI,WAAW,QAAQ,SAAS,KAAK,OAAO,aAAa,QAAQ,SAAS,KAAK,WAAW,IAAI,KAAK,IAAI,EAAE;AAC3G;;;;;AAMA,eAAsB,WAAW,OAAsC,OAAiD,CAAC,GAAoB;CAC3I,MAAM,OAAO,iBAAiB,MAAM,WAAA,SAA0B;CAC9D,MAAM,UAAU,YAAY,IAAI;CAChC,MAAM,EAAE,WAAW,cAAc,MAAM,SAAS;EAAE,GAAG;EAAO,MAAM;CAAM,CAAC;CAEzE,MAAM,SADM,SAAS,IACJ,CAAC,CAAC,QAAQ,MAAM,CAAC,MAAM,MAAM,EAAE,OAAO,MAAM,EAAE;CAC/D,IAAI,CAAC,OAAO,QAAQ;EAClB,IAAI,MAAM,KAAK,yBAAyB,MAAM,GAAG,KAAK,+BAA+B,KAAK,GAAG;EAC7F,OAAO;CACT;CACA,MAAM,YAAsB,CAAC;CAC7B,IAAI,OAAO,MAAM,UAAU,KAAK,MAAM;CACtC,IAAI,OAAO,SAAS,UAAU,KAAK,SAAS;CAC5C,IAAI,CAAC,UAAU,QAAQ,MAAM,IAAI,MAAM,4GAA4G,IAAI,qBAAqB;CAC5K,IAAI,OAAO,QAAQ,QAAQ,aAAa,SAAS,MAAM,IAAI,MAAM,mEAAmE;CAEpI,MAAM,YAAY,OAAO,iBAAiB;CAC1C,IAAI,OAAO,cAAc,iBAAiB,OAAO,GAAG;CACpD,MAAM,yBAAS,IAAI,IAAuB;CAC1C,MAAM,QAAQ,IAAI,IAAI,OAAO,KAAK,MAAM,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;CACnD,MAAM,aAAa,IAAI,WAAW;EAChC,SAAS;EACT,aAAa,OAAO;EACpB,aAAa,OAAO;EACpB,cAAc,KAAK;EACnB,KAAK,OAAO,KAAU,WAAW;GAC/B,MAAM,SAAmB,CAAC;GAC1B,IAAI,IAAI,UAAU,IAAI,MAAM,KAAK,OAAO,MAAM;IAC5C,MAAM,IAAI,MAAM,WAAW,OAAO,MAAM,IAAI,SAAS;KAAE;KAAW;KAAQ,KAAK,OAAO;IAAI,CAAC;IAC3F,IAAI,EAAE,IAAI,IAAI,UAAU,OAAO,MAAM;SAChC,OAAO,KAAK,YAAY,EAAE,OAAO;GACxC;GACA,IAAI,IAAI,UAAU,IAAI,SAAS,KAAK,OAAO,SAAS;IAClD,MAAM,IAAI,MAAM,aAAa,OAAO,QAAQ,KAAK,OAAO,QAAQ,QAAQ,IAAI,SAAS;KAAE,WAAW,KAAK,IAAI,WAAW,GAAM;KAAG;IAAO,CAAC;IACvI,IAAI,EAAE,IAAI,IAAI,UAAU,OAAO,SAAS;SACnC,OAAO,KAAK,YAAY,EAAE,OAAO;GACxC;GACA,OAAO;EACT;EACA,iBAAiB,CAAC;EAClB,SAAS,QAAQ;GACf,MAAM,IAAI,MAAM,IAAI,IAAI,GAAG;GAC3B,OAAO,IAAI,EAAE,IAAI;IAAE,GAAG;IAAG,yBAAQ,IAAI,KAAK,EAAA,CAAE,YAAY;IAAG,UAAU,EAAE,WAAW,IAAI;IAAU,OAAO,IAAI,aAAa;GAAS,CAAC;GAClI,IAAI,GAAG,GAAG,IAAI,GAAG,EAAE,IAAI,IAAI,IAAI,kBAAkB,SAAS,IAAI,aAAa,UAAU,GAAG,GAAG;EAC7F;EACA,aAAa,QAAQ;GACnB,IAAI,IAAI,YAAY,aAAa,IAAI,GAAG,GAAG,MAAM,GAAG,EAAE,IAAI,IAAI,IAAI,GAAG,IAAI,QAAQ,KAAK,UAAU;EAClG;CACF,CAAC;CACD,IAAI,aAAa,OAAO,OAAO,aAAa,OAAO,SAAS,IAAI,MAAM,GAAG,UAAU,OAAO,YAAY,iBAAiB;CAEvH,KAAK,MAAM,KAAK,CAAC,GAAG,MAAM,CAAC,CAAC,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,GAAG,GAAG;EACzD,MAAM,UAAU,EAAE,SAAS,SAAS,EAAE,QAAQ,QAAQ,MAAM,UAAU,SAAS,CAAC,CAAC,IAAI;EACrF,IAAI,CAAC,QAAQ,QAAQ;GACnB,IAAI,GAAG,GAAG,OAAO,GAAG,EAAE,IAAI,EAAE,IAAI,SAAS,EAAE,QAAQ,KAAK,OAAO,EAAE,uCAAuC,EAAE,QAAQ,GAAG,eAAe;GACpI,OAAO,IAAI,EAAE,IAAI,CAAC;GAClB;EACF;EACA,WAAW,KAAK,EAAE,SAAS,EAAE,QAAQ,CAAC;CACxC;CACA,MAAM,WAAW,KAAK;CAEtB,MAAM,WAAW,IAAI,IAAI,OAAO,KAAK,MAAM,EAAE,EAAE,CAAC;CAEhD,MAAM,UAAU,SAAS,IAAI;CAC7B,UAAU,MAAM,CAAC,GAAG,QAAQ,QAAQ,MAAM,CAAC,SAAS,IAAI,EAAE,EAAE,CAAC,GAAG,GAAG,OAAO,OAAO,CAAC,CAAC;CACnF,IAAI,GAAG,OAAO,SAAS,OAAO,KAAK,aAAa,OAAO,KAAK,aAAa;CACzE,OAAO,OAAO;AAChB"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export { t as AuditLog } from "./audit-J2PdyQ1T.js";
@@ -1,6 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import { i as now, r as json } from "./db-B2pnfQW5.js";
2
+ import { i as now, r as json } from "./db-CTUkHFMr.js";
3
3
  //#region server/audit.ts
4
+ /**
5
+ * SQL condition for an MCP tool call in audit_log: an agent actor and an action without a "." (tool
6
+ * names are like "send_message"; "pairing.key_delivered" and console actions have a dot). The same
7
+ * rule as isMcpToolAction in shared/types.ts (used by the console). `t` is the table alias, if any.
8
+ */
9
+ function mcpToolCallSql(t) {
10
+ const c = t ? `${t}.` : "";
11
+ return `${c}actor_type = 'agent' AND instr(${c}action, '.') = 0`;
12
+ }
4
13
  const SELECT = `SELECT a.*, CASE a.actor_type WHEN 'agent' THEN ag.name WHEN 'admin' THEN 'admin' ELSE NULL END AS actor_name
5
14
  FROM audit_log a LEFT JOIN agents ag ON a.actor_type = 'agent' AND ag.id = a.actor_id`;
6
15
  const toEntry = (r) => ({
@@ -60,6 +69,6 @@ var AuditLog = class {
60
69
  }
61
70
  };
62
71
  //#endregion
63
- export { AuditLog as t };
72
+ export { mcpToolCallSql as n, AuditLog as t };
64
73
 
65
- //# sourceMappingURL=audit-CNYWHcik.js.map
74
+ //# sourceMappingURL=audit-J2PdyQ1T.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-J2PdyQ1T.js","names":[],"sources":["../server/audit.ts"],"sourcesContent":["import type { ActorType, AuditEntry, AuditOutcome, Page } from \"../shared/types.ts\";\nimport type { Bus } from \"./bus.ts\";\nimport { type Db, json, now } from \"./db/index.ts\";\n\nexport interface AuditInput {\n actorType: ActorType;\n actorId?: string | null;\n /** MCP tool name or console action, e.g. \"send_message\", \"agent.create\", \"account.connect\". */\n action: string;\n accountId?: string | null;\n target?: string | null;\n outcome: AuditOutcome;\n /** Required for denials: the guardrail that blocked it (\"allow_to\", \"label_scope\", \"rate_limit\", …). */\n reason?: string | null;\n durationMs?: number | null;\n /** Small, non-sensitive context. Never message bodies, keys or tokens. */\n detail?: Record<string, unknown> | null;\n}\n\n/**\n * SQL condition for an MCP tool call in audit_log: an agent actor and an action without a \".\" (tool\n * names are like \"send_message\"; \"pairing.key_delivered\" and console actions have a dot). The same\n * rule as isMcpToolAction in shared/types.ts (used by the console). `t` is the table alias, if any.\n */\nexport function mcpToolCallSql(t?: string): string {\n const c = t ? `${t}.` : \"\";\n return `${c}actor_type = 'agent' AND instr(${c}action, '.') = 0`;\n}\n\nexport interface AuditQuery {\n agentId?: string;\n accountId?: string;\n outcome?: AuditOutcome;\n action?: string;\n /** Opaque cursor from a previous page. */\n cursor?: string | null;\n limit?: number;\n}\n\ninterface AuditRow {\n id: number;\n ts: string;\n actor_type: ActorType;\n actor_id: string | null;\n actor_name: string | null;\n action: string;\n account_id: string | null;\n target: string | null;\n outcome: AuditOutcome;\n reason: string | null;\n duration_ms: number | null;\n detail: string | null;\n}\n\nconst SELECT = `SELECT a.*, CASE a.actor_type WHEN 'agent' THEN ag.name WHEN 'admin' THEN 'admin' ELSE NULL END AS actor_name\n FROM audit_log a LEFT JOIN agents ag ON a.actor_type = 'agent' AND ag.id = a.actor_id`;\n\nconst toEntry = (r: AuditRow): AuditEntry => ({\n id: r.id,\n ts: r.ts,\n actorType: r.actor_type,\n actorId: r.actor_id,\n actorName: r.actor_name,\n action: r.action,\n accountId: r.account_id,\n target: r.target,\n outcome: r.outcome,\n reason: r.reason,\n durationMs: r.duration_ms,\n detail: json.parse<Record<string, unknown> | null>(r.detail, null),\n});\n\n/** Append-only audit log. Every agent tool call and every admin mutation is recorded. */\nexport class AuditLog {\n constructor(\n private readonly db: Db,\n private readonly bus: Bus,\n ) {}\n\n record(input: AuditInput): AuditEntry {\n const { lastInsertRowid } = this.db.run(\n `INSERT INTO audit_log (ts, actor_type, actor_id, action, account_id, target, outcome, reason, duration_ms, detail)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n now(),\n input.actorType,\n input.actorId ?? null,\n input.action,\n input.accountId ?? null,\n input.target ?? null,\n input.outcome,\n input.reason ?? null,\n input.durationMs ?? null,\n input.detail ? json.str(input.detail) : null,\n );\n const entry = toEntry(this.db.get<AuditRow>(`${SELECT} WHERE a.id = ?`, lastInsertRowid)!);\n this.bus.emit(\"audit\", entry);\n return entry;\n }\n\n /** Newest first. */\n list(q: AuditQuery = {}): Page<AuditEntry> {\n const where: string[] = [];\n const params: (string | number)[] = [];\n const add = (clause: string, value: string | number) => {\n where.push(clause);\n params.push(value);\n };\n if (q.agentId) add(\"a.actor_id = ?\", q.agentId);\n if (q.accountId) add(\"a.account_id = ?\", q.accountId);\n if (q.outcome) add(\"a.outcome = ?\", q.outcome);\n if (q.action) add(\"a.action = ?\", q.action);\n const before = Number(q.cursor);\n if (q.cursor && Number.isInteger(before)) add(\"a.id < ?\", before);\n const limit = Math.min(Math.max(q.limit ?? 50, 1), 200);\n const rows = this.db.all<AuditRow>(\n `${SELECT} ${where.length ? \"WHERE \" + where.join(\" AND \") : \"\"} ORDER BY a.id DESC LIMIT ?`,\n ...params,\n limit + 1,\n );\n const items = rows.slice(0, limit).map(toEntry);\n return { items, nextCursor: rows.length > limit ? String(items.at(-1)!.id) : null };\n }\n\n /** Deletes entries older than `beforeIso`. Returns the number removed. */\n prune(beforeIso: string): number {\n return this.db.run(\"DELETE FROM audit_log WHERE ts < ?\", beforeIso).changes;\n }\n}\n"],"mappings":";;;;;;;;AAwBA,SAAgB,eAAe,GAAoB;CACjD,MAAM,IAAI,IAAI,GAAG,EAAE,KAAK;CACxB,OAAO,GAAG,EAAE,iCAAiC,EAAE;AACjD;AA2BA,MAAM,SAAS;;AAGf,MAAM,WAAW,OAA6B;CAC5C,IAAI,EAAE;CACN,IAAI,EAAE;CACN,WAAW,EAAE;CACb,SAAS,EAAE;CACX,WAAW,EAAE;CACb,QAAQ,EAAE;CACV,WAAW,EAAE;CACb,QAAQ,EAAE;CACV,SAAS,EAAE;CACX,QAAQ,EAAE;CACV,YAAY,EAAE;CACd,QAAQ,KAAK,MAAsC,EAAE,QAAQ,IAAI;AACnE;;AAGA,IAAa,WAAb,MAAsB;CAED;CACA;CAFnB,YACE,IACA,KACA;EAFiB,KAAA,KAAA;EACA,KAAA,MAAA;CAChB;CAEH,OAAO,OAA+B;EACpC,MAAM,EAAE,oBAAoB,KAAK,GAAG,IAClC;+CAEA,IAAI,GACJ,MAAM,WACN,MAAM,WAAW,MACjB,MAAM,QACN,MAAM,aAAa,MACnB,MAAM,UAAU,MAChB,MAAM,SACN,MAAM,UAAU,MAChB,MAAM,cAAc,MACpB,MAAM,SAAS,KAAK,IAAI,MAAM,MAAM,IAAI,IAC1C;EACA,MAAM,QAAQ,QAAQ,KAAK,GAAG,IAAc,GAAG,OAAO,kBAAkB,eAAe,CAAE;EACzF,KAAK,IAAI,KAAK,SAAS,KAAK;EAC5B,OAAO;CACT;;CAGA,KAAK,IAAgB,CAAC,GAAqB;EACzC,MAAM,QAAkB,CAAC;EACzB,MAAM,SAA8B,CAAC;EACrC,MAAM,OAAO,QAAgB,UAA2B;GACtD,MAAM,KAAK,MAAM;GACjB,OAAO,KAAK,KAAK;EACnB;EACA,IAAI,EAAE,SAAS,IAAI,kBAAkB,EAAE,OAAO;EAC9C,IAAI,EAAE,WAAW,IAAI,oBAAoB,EAAE,SAAS;EACpD,IAAI,EAAE,SAAS,IAAI,iBAAiB,EAAE,OAAO;EAC7C,IAAI,EAAE,QAAQ,IAAI,gBAAgB,EAAE,MAAM;EAC1C,MAAM,SAAS,OAAO,EAAE,MAAM;EAC9B,IAAI,EAAE,UAAU,OAAO,UAAU,MAAM,GAAG,IAAI,YAAY,MAAM;EAChE,MAAM,QAAQ,KAAK,IAAI,KAAK,IAAI,EAAE,SAAS,IAAI,CAAC,GAAG,GAAG;EACtD,MAAM,OAAO,KAAK,GAAG,IACnB,GAAG,OAAO,GAAG,MAAM,SAAS,WAAW,MAAM,KAAK,OAAO,IAAI,GAAG,8BAChE,GAAG,QACH,QAAQ,CACV;EACA,MAAM,QAAQ,KAAK,MAAM,GAAG,KAAK,CAAC,CAAC,IAAI,OAAO;EAC9C,OAAO;GAAE;GAAO,YAAY,KAAK,SAAS,QAAQ,OAAO,MAAM,GAAG,EAAE,CAAC,CAAE,EAAE,IAAI;EAAK;CACpF;;CAGA,MAAM,WAA2B;EAC/B,OAAO,KAAK,GAAG,IAAI,sCAAsC,SAAS,CAAC,CAAC;CACtE;AACF"}