@openparachute/agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (501) hide show
  1. package/.claude/scheduled_tasks.lock +1 -0
  2. package/.claude/settings.json +5 -0
  3. package/.claude/skills/add-atomic-chat-tool/SKILL.md +243 -0
  4. package/.claude/skills/add-atomic-chat-tool/atomic-chat-mcp-stdio.ts +229 -0
  5. package/.claude/skills/add-codex/SKILL.md +161 -0
  6. package/.claude/skills/add-dashboard/SKILL.md +138 -0
  7. package/.claude/skills/add-dashboard/resources/dashboard-pusher.ts +495 -0
  8. package/.claude/skills/add-emacs/SKILL.md +296 -0
  9. package/.claude/skills/add-gcal-tool/SKILL.md +210 -0
  10. package/.claude/skills/add-gchat/REMOVE.md +6 -0
  11. package/.claude/skills/add-gchat/SKILL.md +92 -0
  12. package/.claude/skills/add-gchat/VERIFY.md +3 -0
  13. package/.claude/skills/add-github/REMOVE.md +6 -0
  14. package/.claude/skills/add-github/SKILL.md +148 -0
  15. package/.claude/skills/add-github/VERIFY.md +3 -0
  16. package/.claude/skills/add-gmail-tool/SKILL.md +229 -0
  17. package/.claude/skills/add-imessage/REMOVE.md +6 -0
  18. package/.claude/skills/add-imessage/SKILL.md +113 -0
  19. package/.claude/skills/add-imessage/VERIFY.md +3 -0
  20. package/.claude/skills/add-karpathy-llm-wiki/SKILL.md +110 -0
  21. package/.claude/skills/add-karpathy-llm-wiki/llm-wiki.md +75 -0
  22. package/.claude/skills/add-linear/REMOVE.md +6 -0
  23. package/.claude/skills/add-linear/SKILL.md +168 -0
  24. package/.claude/skills/add-linear/VERIFY.md +3 -0
  25. package/.claude/skills/add-macos-statusbar/SKILL.md +133 -0
  26. package/.claude/skills/add-macos-statusbar/add/src/statusbar.swift +147 -0
  27. package/.claude/skills/add-matrix/REMOVE.md +6 -0
  28. package/.claude/skills/add-matrix/SKILL.md +148 -0
  29. package/.claude/skills/add-matrix/VERIFY.md +3 -0
  30. package/.claude/skills/add-ollama-provider/SKILL.md +179 -0
  31. package/.claude/skills/add-ollama-tool/SKILL.md +193 -0
  32. package/.claude/skills/add-opencode/SKILL.md +229 -0
  33. package/.claude/skills/add-parallel/SKILL.md +290 -0
  34. package/.claude/skills/add-resend/REMOVE.md +6 -0
  35. package/.claude/skills/add-resend/SKILL.md +93 -0
  36. package/.claude/skills/add-resend/VERIFY.md +3 -0
  37. package/.claude/skills/add-signal/REMOVE.md +13 -0
  38. package/.claude/skills/add-signal/SKILL.md +318 -0
  39. package/.claude/skills/add-signal/VERIFY.md +5 -0
  40. package/.claude/skills/add-slack/REMOVE.md +6 -0
  41. package/.claude/skills/add-slack/SKILL.md +112 -0
  42. package/.claude/skills/add-slack/VERIFY.md +3 -0
  43. package/.claude/skills/add-teams/REMOVE.md +6 -0
  44. package/.claude/skills/add-teams/SKILL.md +207 -0
  45. package/.claude/skills/add-teams/VERIFY.md +3 -0
  46. package/.claude/skills/add-vercel/SKILL.md +147 -0
  47. package/.claude/skills/add-vercel/container-skills/vercel-cli/SKILL.md +103 -0
  48. package/.claude/skills/add-webex/REMOVE.md +6 -0
  49. package/.claude/skills/add-webex/SKILL.md +88 -0
  50. package/.claude/skills/add-webex/VERIFY.md +3 -0
  51. package/.claude/skills/add-wechat/REMOVE.md +49 -0
  52. package/.claude/skills/add-wechat/SKILL.md +170 -0
  53. package/.claude/skills/add-wechat/scripts/wire-dm.ts +172 -0
  54. package/.claude/skills/add-whatsapp/SKILL.md +264 -0
  55. package/.claude/skills/add-whatsapp-cloud/REMOVE.md +6 -0
  56. package/.claude/skills/add-whatsapp-cloud/SKILL.md +95 -0
  57. package/.claude/skills/add-whatsapp-cloud/VERIFY.md +3 -0
  58. package/.claude/skills/claw/SKILL.md +131 -0
  59. package/.claude/skills/claw/scripts/claw +374 -0
  60. package/.claude/skills/convert-to-apple-container/SKILL.md +212 -0
  61. package/.claude/skills/customize/SKILL.md +110 -0
  62. package/.claude/skills/debug/SKILL.md +349 -0
  63. package/.claude/skills/get-qodo-rules/SKILL.md +122 -0
  64. package/.claude/skills/get-qodo-rules/references/output-format.md +41 -0
  65. package/.claude/skills/get-qodo-rules/references/pagination.md +33 -0
  66. package/.claude/skills/get-qodo-rules/references/repository-scope.md +26 -0
  67. package/.claude/skills/init-first-agent/SKILL.md +120 -0
  68. package/.claude/skills/init-onecli/SKILL.md +270 -0
  69. package/.claude/skills/manage-channels/SKILL.md +87 -0
  70. package/.claude/skills/manage-mounts/SKILL.md +47 -0
  71. package/.claude/skills/migrate-from-openclaw/MIGRATE_CRONS.md +100 -0
  72. package/.claude/skills/migrate-from-openclaw/SKILL.md +447 -0
  73. package/.claude/skills/migrate-from-openclaw/scripts/discover-openclaw.ts +734 -0
  74. package/.claude/skills/migrate-from-openclaw/scripts/extract-channel-credentials.ts +476 -0
  75. package/.claude/skills/migrate-nanoclaw/SKILL.md +484 -0
  76. package/.claude/skills/migrate-nanoclaw/diagnostics.md +51 -0
  77. package/.claude/skills/qodo-pr-resolver/SKILL.md +326 -0
  78. package/.claude/skills/qodo-pr-resolver/resources/providers.md +329 -0
  79. package/.claude/skills/update-nanoclaw/SKILL.md +243 -0
  80. package/.claude/skills/update-nanoclaw/diagnostics.md +48 -0
  81. package/.claude/skills/update-skills/SKILL.md +130 -0
  82. package/.claude/skills/use-native-credential-proxy/SKILL.md +167 -0
  83. package/.claude/skills/x-integration/SKILL.md +417 -0
  84. package/.claude/skills/x-integration/agent.ts +243 -0
  85. package/.claude/skills/x-integration/host.ts +155 -0
  86. package/.claude/skills/x-integration/lib/browser.ts +148 -0
  87. package/.claude/skills/x-integration/lib/config.ts +62 -0
  88. package/.claude/skills/x-integration/scripts/like.ts +56 -0
  89. package/.claude/skills/x-integration/scripts/post.ts +66 -0
  90. package/.claude/skills/x-integration/scripts/quote.ts +80 -0
  91. package/.claude/skills/x-integration/scripts/reply.ts +74 -0
  92. package/.claude/skills/x-integration/scripts/retweet.ts +62 -0
  93. package/.claude/skills/x-integration/scripts/setup.ts +87 -0
  94. package/.github/CODEOWNERS +10 -0
  95. package/.github/PULL_REQUEST_TEMPLATE.md +18 -0
  96. package/.github/workflows/bump-version.yml +35 -0
  97. package/.github/workflows/ci.yml +39 -0
  98. package/.github/workflows/label-pr.yml +40 -0
  99. package/.github/workflows/update-tokens.yml +43 -0
  100. package/.husky/pre-commit +1 -0
  101. package/.mcp.json +3 -0
  102. package/.nvmrc +1 -0
  103. package/.parachute/module.json +14 -0
  104. package/.prettierrc +4 -0
  105. package/CHANGELOG.md +215 -0
  106. package/CLAUDE.md +307 -0
  107. package/CODE_OF_CONDUCT.md +128 -0
  108. package/CONTRIBUTING.md +159 -0
  109. package/CONTRIBUTORS.md +26 -0
  110. package/LICENSE +21 -0
  111. package/README.md +190 -0
  112. package/README_ja.md +194 -0
  113. package/README_zh.md +194 -0
  114. package/assets/nanoclaw-favicon.png +0 -0
  115. package/assets/nanoclaw-icon.png +0 -0
  116. package/assets/nanoclaw-logo-dark.png +0 -0
  117. package/assets/nanoclaw-logo.png +0 -0
  118. package/assets/nanoclaw-profile.jpeg +0 -0
  119. package/assets/nanoclaw-sales.png +0 -0
  120. package/assets/social-preview.jpg +0 -0
  121. package/config-examples/mount-allowlist.json +25 -0
  122. package/container/.dockerignore +2 -0
  123. package/container/CLAUDE.md +21 -0
  124. package/container/Dockerfile +121 -0
  125. package/container/agent-runner/bun.lock +243 -0
  126. package/container/agent-runner/package.json +22 -0
  127. package/container/agent-runner/scripts/sdk-signal-probe.ts +169 -0
  128. package/container/agent-runner/src/config.ts +55 -0
  129. package/container/agent-runner/src/db/connection.ts +267 -0
  130. package/container/agent-runner/src/db/index.ts +20 -0
  131. package/container/agent-runner/src/db/messages-in.ts +138 -0
  132. package/container/agent-runner/src/db/messages-out.ts +143 -0
  133. package/container/agent-runner/src/db/session-routing.ts +30 -0
  134. package/container/agent-runner/src/db/session-state.test.ts +100 -0
  135. package/container/agent-runner/src/db/session-state.ts +79 -0
  136. package/container/agent-runner/src/destinations.ts +135 -0
  137. package/container/agent-runner/src/formatter.test.ts +167 -0
  138. package/container/agent-runner/src/formatter.ts +260 -0
  139. package/container/agent-runner/src/index.ts +110 -0
  140. package/container/agent-runner/src/integration.test.ts +121 -0
  141. package/container/agent-runner/src/mcp-tools/agents.instructions.md +26 -0
  142. package/container/agent-runner/src/mcp-tools/agents.ts +66 -0
  143. package/container/agent-runner/src/mcp-tools/core.instructions.md +27 -0
  144. package/container/agent-runner/src/mcp-tools/core.ts +262 -0
  145. package/container/agent-runner/src/mcp-tools/index.ts +22 -0
  146. package/container/agent-runner/src/mcp-tools/interactive.instructions.md +22 -0
  147. package/container/agent-runner/src/mcp-tools/interactive.ts +169 -0
  148. package/container/agent-runner/src/mcp-tools/scheduling.instructions.md +40 -0
  149. package/container/agent-runner/src/mcp-tools/scheduling.ts +299 -0
  150. package/container/agent-runner/src/mcp-tools/self-mod.instructions.md +25 -0
  151. package/container/agent-runner/src/mcp-tools/self-mod.ts +120 -0
  152. package/container/agent-runner/src/mcp-tools/server.ts +54 -0
  153. package/container/agent-runner/src/mcp-tools/types.ts +6 -0
  154. package/container/agent-runner/src/poll-loop.test.ts +248 -0
  155. package/container/agent-runner/src/poll-loop.ts +437 -0
  156. package/container/agent-runner/src/providers/claude.ts +379 -0
  157. package/container/agent-runner/src/providers/factory.test.ts +19 -0
  158. package/container/agent-runner/src/providers/factory.ts +13 -0
  159. package/container/agent-runner/src/providers/index.ts +6 -0
  160. package/container/agent-runner/src/providers/mock.ts +77 -0
  161. package/container/agent-runner/src/providers/provider-registry.ts +33 -0
  162. package/container/agent-runner/src/providers/types.ts +82 -0
  163. package/container/agent-runner/src/scheduling/task-script.ts +121 -0
  164. package/container/agent-runner/src/timezone.test.ts +93 -0
  165. package/container/agent-runner/src/timezone.ts +107 -0
  166. package/container/agent-runner/tsconfig.json +14 -0
  167. package/container/build.sh +48 -0
  168. package/container/entrypoint.sh +16 -0
  169. package/container/skills/agent-browser/SKILL.md +159 -0
  170. package/container/skills/frontend-engineer/SKILL.md +157 -0
  171. package/container/skills/self-customize/SKILL.md +87 -0
  172. package/container/skills/slack-formatting/SKILL.md +94 -0
  173. package/container/skills/vercel-cli/SKILL.md +111 -0
  174. package/container/skills/welcome/SKILL.md +85 -0
  175. package/docs/APPLE-CONTAINER-NETWORKING.md +90 -0
  176. package/docs/BRANCH-FORK-MAINTENANCE.md +81 -0
  177. package/docs/README.md +25 -0
  178. package/docs/SDK_DEEP_DIVE.md +643 -0
  179. package/docs/SECURITY.md +162 -0
  180. package/docs/agent-runner-details.md +749 -0
  181. package/docs/api-details.md +365 -0
  182. package/docs/architecture-diagram.html +422 -0
  183. package/docs/architecture-diagram.md +215 -0
  184. package/docs/architecture.md +751 -0
  185. package/docs/audit/2026-04-30-channel-endpoint-audit.md +36 -0
  186. package/docs/build-and-runtime.md +80 -0
  187. package/docs/cross-mount-stress/README.md +112 -0
  188. package/docs/cross-mount-stress/container-writer-retry.mjs +55 -0
  189. package/docs/cross-mount-stress/container-writer-slow.mjs +42 -0
  190. package/docs/cross-mount-stress/container-writer.mjs +47 -0
  191. package/docs/cross-mount-stress/host-writer-retry.mjs +55 -0
  192. package/docs/cross-mount-stress/host-writer-slow.mjs +43 -0
  193. package/docs/cross-mount-stress/host-writer.mjs +47 -0
  194. package/docs/db-central.md +316 -0
  195. package/docs/db-session.md +183 -0
  196. package/docs/db.md +119 -0
  197. package/docs/design/2026-04-29-vault-management-ui.md +231 -0
  198. package/docs/design/2026-04-30-channel-wiring-rework.md +234 -0
  199. package/docs/design/2026-05-01-channel-wiring-approvals-deep-dive.md +272 -0
  200. package/docs/design/2026-05-02-channel-policy-and-approval-routing.md +250 -0
  201. package/docs/docker-sandboxes.md +359 -0
  202. package/docs/isolation-model.md +88 -0
  203. package/docs/ollama.md +79 -0
  204. package/docs/parachute-integration.md +109 -0
  205. package/docs/post-night-rebirth-reflections.md +151 -0
  206. package/eslint.config.js +32 -0
  207. package/package.json +54 -0
  208. package/pnpm-workspace.yaml +8 -0
  209. package/repo-tokens/README.md +113 -0
  210. package/repo-tokens/action.yml +186 -0
  211. package/repo-tokens/badge.svg +23 -0
  212. package/repo-tokens/examples/green.svg +14 -0
  213. package/repo-tokens/examples/red.svg +14 -0
  214. package/repo-tokens/examples/yellow-green.svg +14 -0
  215. package/repo-tokens/examples/yellow.svg +14 -0
  216. package/scripts/chat.ts +101 -0
  217. package/scripts/cleanup-sessions.sh +150 -0
  218. package/scripts/init-cli-agent.ts +171 -0
  219. package/scripts/init-first-agent.ts +377 -0
  220. package/scripts/parachute.ts +158 -0
  221. package/scripts/run-migrations.ts +105 -0
  222. package/scripts/sanity-live-poll.ts +95 -0
  223. package/scripts/seed-discord.ts +79 -0
  224. package/scripts/test-v2-agent.ts +106 -0
  225. package/scripts/test-v2-channel-e2e.ts +265 -0
  226. package/scripts/test-v2-host.ts +184 -0
  227. package/src/channels/adapter.ts +214 -0
  228. package/src/channels/ask-question.ts +46 -0
  229. package/src/channels/channel-registry.test.ts +421 -0
  230. package/src/channels/channel-registry.ts +313 -0
  231. package/src/channels/chat-sdk-bridge.test.ts +84 -0
  232. package/src/channels/chat-sdk-bridge.ts +652 -0
  233. package/src/channels/cli.ts +276 -0
  234. package/src/channels/discord.ts +90 -0
  235. package/src/channels/index.ts +17 -0
  236. package/src/channels/telegram-markdown-sanitize.test.ts +78 -0
  237. package/src/channels/telegram-markdown-sanitize.ts +55 -0
  238. package/src/channels/telegram-pairing.test.ts +254 -0
  239. package/src/channels/telegram-pairing.ts +339 -0
  240. package/src/channels/telegram.ts +279 -0
  241. package/src/channels/trust-hint.test.ts +48 -0
  242. package/src/channels/trust-hint.ts +75 -0
  243. package/src/claude-md-compose.migrate.test.ts +64 -0
  244. package/src/claude-md-compose.ts +205 -0
  245. package/src/command-gate.ts +63 -0
  246. package/src/config.test.ts +93 -0
  247. package/src/config.ts +108 -0
  248. package/src/container-config.ts +167 -0
  249. package/src/container-runner.test.ts +32 -0
  250. package/src/container-runner.ts +576 -0
  251. package/src/container-runtime.test.ts +169 -0
  252. package/src/container-runtime.ts +92 -0
  253. package/src/db/_bun-sqlite-shim.ts +88 -0
  254. package/src/db/agent-activity.test.ts +155 -0
  255. package/src/db/agent-activity.ts +121 -0
  256. package/src/db/agent-groups.ts +77 -0
  257. package/src/db/connection.migrate.test.ts +143 -0
  258. package/src/db/connection.ts +224 -0
  259. package/src/db/db-v2.test.ts +440 -0
  260. package/src/db/dropped-messages.ts +44 -0
  261. package/src/db/index.ts +40 -0
  262. package/src/db/messaging-groups.ts +252 -0
  263. package/src/db/migrations/001-initial.ts +112 -0
  264. package/src/db/migrations/002-chat-sdk-state.ts +36 -0
  265. package/src/db/migrations/008-dropped-messages.ts +27 -0
  266. package/src/db/migrations/009-drop-pending-credentials.ts +13 -0
  267. package/src/db/migrations/010-engage-modes.ts +103 -0
  268. package/src/db/migrations/011-pending-sender-approvals.ts +40 -0
  269. package/src/db/migrations/012-channel-registration.ts +48 -0
  270. package/src/db/migrations/013-approval-render-metadata.ts +27 -0
  271. package/src/db/migrations/014-secrets.ts +44 -0
  272. package/src/db/migrations/015-secrets-drop-host-pattern.ts +18 -0
  273. package/src/db/migrations/016-secret-assignments.ts +30 -0
  274. package/src/db/migrations/017-agent-activity.ts +40 -0
  275. package/src/db/migrations/018-oauth-app-configs.ts +34 -0
  276. package/src/db/migrations/019-oauth-app-connections.ts +48 -0
  277. package/src/db/migrations/020-agent-app-connections.ts +28 -0
  278. package/src/db/migrations/021-pending-oauth-states.ts +35 -0
  279. package/src/db/migrations/022-app-connections-provider.ts +25 -0
  280. package/src/db/migrations/023-agent-group-secret-mode.test.ts +124 -0
  281. package/src/db/migrations/023-agent-group-secret-mode.ts +65 -0
  282. package/src/db/migrations/024-collapse-approvals.test.ts +249 -0
  283. package/src/db/migrations/024-collapse-approvals.ts +182 -0
  284. package/src/db/migrations/025-secret-mode-check.test.ts +155 -0
  285. package/src/db/migrations/025-secret-mode-check.ts +49 -0
  286. package/src/db/migrations/026-user-dms-bot-id.test.ts +116 -0
  287. package/src/db/migrations/026-user-dms-bot-id.ts +54 -0
  288. package/src/db/migrations/027-provider-credentials.ts +41 -0
  289. package/src/db/migrations/_test-helpers.ts +41 -0
  290. package/src/db/migrations/index.ts +127 -0
  291. package/src/db/migrations/module-agent-to-agent-destinations.ts +84 -0
  292. package/src/db/migrations/module-approvals-pending-approvals.ts +42 -0
  293. package/src/db/migrations/module-approvals-title-options.ts +40 -0
  294. package/src/db/schema.ts +258 -0
  295. package/src/db/session-db.test.ts +93 -0
  296. package/src/db/session-db.ts +325 -0
  297. package/src/db/sessions.ts +241 -0
  298. package/src/delivery.test.ts +148 -0
  299. package/src/delivery.ts +445 -0
  300. package/src/env.ts +74 -0
  301. package/src/group-folder.test.ts +35 -0
  302. package/src/group-folder.ts +44 -0
  303. package/src/group-init.ts +92 -0
  304. package/src/host-core.test.ts +456 -0
  305. package/src/host-sweep.test.ts +146 -0
  306. package/src/host-sweep.ts +287 -0
  307. package/src/index.ts +227 -0
  308. package/src/install-slug.ts +33 -0
  309. package/src/log.test.ts +81 -0
  310. package/src/log.ts +117 -0
  311. package/src/mcp/http.ts +72 -0
  312. package/src/mcp/server.ts +92 -0
  313. package/src/mcp/stdio.ts +51 -0
  314. package/src/mcp/tools/activity.ts +88 -0
  315. package/src/mcp/tools/agent-groups.ts +183 -0
  316. package/src/mcp/tools/approvals.ts +122 -0
  317. package/src/mcp/tools/channels.ts +199 -0
  318. package/src/mcp/tools/index.ts +27 -0
  319. package/src/mcp/tools/oauth.ts +48 -0
  320. package/src/mcp/tools/secrets.ts +169 -0
  321. package/src/mcp/tools/sessions.ts +135 -0
  322. package/src/mcp/types.ts +51 -0
  323. package/src/modules/agent-to-agent/agent-route.test.ts +46 -0
  324. package/src/modules/agent-to-agent/agent-route.ts +223 -0
  325. package/src/modules/agent-to-agent/create-agent.ts +127 -0
  326. package/src/modules/agent-to-agent/db/agent-destinations.ts +135 -0
  327. package/src/modules/agent-to-agent/index.ts +22 -0
  328. package/src/modules/agent-to-agent/write-destinations.ts +59 -0
  329. package/src/modules/approvals/agent.md +45 -0
  330. package/src/modules/approvals/index.ts +21 -0
  331. package/src/modules/approvals/picks.test.ts +291 -0
  332. package/src/modules/approvals/primitive.ts +279 -0
  333. package/src/modules/approvals/project.md +27 -0
  334. package/src/modules/approvals/response-handler.ts +87 -0
  335. package/src/modules/index.ts +24 -0
  336. package/src/modules/interactive/agent.md +21 -0
  337. package/src/modules/interactive/index.ts +69 -0
  338. package/src/modules/interactive/project.md +12 -0
  339. package/src/modules/mount-security/index.ts +448 -0
  340. package/src/modules/mount-security/migrate.test.ts +91 -0
  341. package/src/modules/permissions/access.ts +28 -0
  342. package/src/modules/permissions/channel-approval.test.ts +389 -0
  343. package/src/modules/permissions/channel-approval.ts +188 -0
  344. package/src/modules/permissions/db/agent-group-members.ts +44 -0
  345. package/src/modules/permissions/db/pending-channel-approvals.test.ts +86 -0
  346. package/src/modules/permissions/db/pending-channel-approvals.ts +66 -0
  347. package/src/modules/permissions/db/pending-sender-approvals.ts +60 -0
  348. package/src/modules/permissions/db/user-dms.ts +58 -0
  349. package/src/modules/permissions/db/user-roles.ts +85 -0
  350. package/src/modules/permissions/db/users.ts +38 -0
  351. package/src/modules/permissions/index.ts +421 -0
  352. package/src/modules/permissions/permissions.test.ts +358 -0
  353. package/src/modules/permissions/sender-approval.test.ts +470 -0
  354. package/src/modules/permissions/sender-approval.ts +165 -0
  355. package/src/modules/permissions/user-dm.ts +200 -0
  356. package/src/modules/provider-credentials/db.ts +121 -0
  357. package/src/modules/provider-credentials/index.ts +12 -0
  358. package/src/modules/provider-credentials/spawn.test.ts +206 -0
  359. package/src/modules/provider-credentials/spawn.ts +114 -0
  360. package/src/modules/scheduling/actions.ts +113 -0
  361. package/src/modules/scheduling/db.test.ts +282 -0
  362. package/src/modules/scheduling/db.ts +148 -0
  363. package/src/modules/scheduling/index.ts +34 -0
  364. package/src/modules/scheduling/recurrence.test.ts +98 -0
  365. package/src/modules/scheduling/recurrence.ts +54 -0
  366. package/src/modules/self-mod/agent.md +30 -0
  367. package/src/modules/self-mod/apply.ts +85 -0
  368. package/src/modules/self-mod/index.ts +30 -0
  369. package/src/modules/self-mod/project.md +39 -0
  370. package/src/modules/self-mod/request.ts +91 -0
  371. package/src/modules/typing/index.ts +165 -0
  372. package/src/oauth/agent-app-connections.ts +103 -0
  373. package/src/oauth/app-configs.test.ts +64 -0
  374. package/src/oauth/app-configs.ts +114 -0
  375. package/src/oauth/app-connections.test.ts +109 -0
  376. package/src/oauth/app-connections.ts +178 -0
  377. package/src/oauth/crypto.ts +56 -0
  378. package/src/oauth/flow.ts +104 -0
  379. package/src/oauth/providers/google.test.ts +38 -0
  380. package/src/oauth/providers/google.ts +46 -0
  381. package/src/oauth/providers/index.ts +48 -0
  382. package/src/oauth/state-store.test.ts +54 -0
  383. package/src/oauth/state-store.ts +93 -0
  384. package/src/parachute/README.md +27 -0
  385. package/src/parachute/create-agent.test.ts +83 -0
  386. package/src/parachute/create-agent.ts +122 -0
  387. package/src/parachute/group-status.test.ts +165 -0
  388. package/src/parachute/group-status.ts +136 -0
  389. package/src/parachute/types.ts +41 -0
  390. package/src/parachute/vault-mcp.test.ts +251 -0
  391. package/src/parachute/vault-mcp.ts +232 -0
  392. package/src/platform-id.test.ts +104 -0
  393. package/src/platform-id.ts +109 -0
  394. package/src/providers/index.ts +6 -0
  395. package/src/providers/provider-container-registry.ts +58 -0
  396. package/src/response-registry.ts +45 -0
  397. package/src/router.ts +530 -0
  398. package/src/secrets/crypto.test.ts +45 -0
  399. package/src/secrets/crypto.ts +55 -0
  400. package/src/secrets/index.ts +355 -0
  401. package/src/secrets/master-key.ts +70 -0
  402. package/src/secrets/secrets.test.ts +354 -0
  403. package/src/session-manager.migrate.test.ts +59 -0
  404. package/src/session-manager.ts +433 -0
  405. package/src/startup-bootstrap.test.ts +226 -0
  406. package/src/startup-bootstrap.ts +207 -0
  407. package/src/state-sqlite.ts +182 -0
  408. package/src/timezone.test.ts +64 -0
  409. package/src/timezone.ts +37 -0
  410. package/src/types.ts +230 -0
  411. package/src/web/auth.test.ts +335 -0
  412. package/src/web/auth.ts +214 -0
  413. package/src/web/discord-validate.test.ts +77 -0
  414. package/src/web/discord-validate.ts +88 -0
  415. package/src/web/hub-discovery.test.ts +98 -0
  416. package/src/web/hub-discovery.ts +69 -0
  417. package/src/web/routes/activity.ts +106 -0
  418. package/src/web/routes/agent-provider.test.ts +282 -0
  419. package/src/web/routes/agent-provider.ts +309 -0
  420. package/src/web/routes/approvals.ts +185 -0
  421. package/src/web/routes/apps.ts +434 -0
  422. package/src/web/routes/channels-mg-detail.test.ts +324 -0
  423. package/src/web/routes/channels-mga-detail.test.ts +425 -0
  424. package/src/web/routes/channels.ts +489 -0
  425. package/src/web/routes/oauth-providers.ts +42 -0
  426. package/src/web/routes/secrets.test.ts +175 -0
  427. package/src/web/routes/secrets.ts +282 -0
  428. package/src/web/routes/sessions.ts +123 -0
  429. package/src/web/routes/settings.test.ts +106 -0
  430. package/src/web/routes/settings.ts +247 -0
  431. package/src/web/routes/setup-status.ts +205 -0
  432. package/src/web/routes/vaults.test.ts +389 -0
  433. package/src/web/routes/vaults.ts +225 -0
  434. package/src/web/server-version.test.ts +16 -0
  435. package/src/web/server.ts +1003 -0
  436. package/src/web/services-manifest.test.ts +120 -0
  437. package/src/web/services-manifest.ts +61 -0
  438. package/src/web/static-serve.test.ts +255 -0
  439. package/src/web/static-serve.ts +104 -0
  440. package/src/web/telegram-validate.test.ts +116 -0
  441. package/src/web/telegram-validate.ts +107 -0
  442. package/src/web/vault-proxy.test.ts +214 -0
  443. package/src/web/vault-proxy.ts +120 -0
  444. package/src/web/wire-channel.ts +181 -0
  445. package/src/webhook-server.ts +134 -0
  446. package/tsconfig.json +21 -0
  447. package/vitest.config.ts +18 -0
  448. package/web/README.md +63 -0
  449. package/web/ui/index.html +13 -0
  450. package/web/ui/package.json +35 -0
  451. package/web/ui/pnpm-lock.yaml +2164 -0
  452. package/web/ui/scripts/verify-base.mjs +31 -0
  453. package/web/ui/src/App.tsx +88 -0
  454. package/web/ui/src/components/ActivityFeed.tsx +444 -0
  455. package/web/ui/src/components/AgentGroupPicker.tsx +263 -0
  456. package/web/ui/src/components/AgentProviderCards.tsx +220 -0
  457. package/web/ui/src/components/CredentialForm.tsx +214 -0
  458. package/web/ui/src/components/ScopeGrants.tsx +74 -0
  459. package/web/ui/src/components/StatusDot.tsx +43 -0
  460. package/web/ui/src/components/VaultPicker.tsx +127 -0
  461. package/web/ui/src/components/setup/AdapterInstallStep.tsx +178 -0
  462. package/web/ui/src/components/setup/AgentGroupStep.tsx +43 -0
  463. package/web/ui/src/components/setup/ChannelPickStep.tsx +74 -0
  464. package/web/ui/src/components/setup/DoneStep.tsx +49 -0
  465. package/web/ui/src/components/setup/PrereqStep.tsx +129 -0
  466. package/web/ui/src/components/setup/TestConnectionStep.tsx +108 -0
  467. package/web/ui/src/components/setup/TestMessageStep.tsx +104 -0
  468. package/web/ui/src/components/setup/WireChannelStep.tsx +166 -0
  469. package/web/ui/src/components/setup/types.ts +105 -0
  470. package/web/ui/src/lib/api.test.ts +410 -0
  471. package/web/ui/src/lib/api.ts +1210 -0
  472. package/web/ui/src/lib/auth.test.ts +139 -0
  473. package/web/ui/src/lib/auth.ts +348 -0
  474. package/web/ui/src/lib/channel-adapters.ts +136 -0
  475. package/web/ui/src/main.tsx +19 -0
  476. package/web/ui/src/routes/ApprovalsList.tsx +294 -0
  477. package/web/ui/src/routes/Apps.tsx +613 -0
  478. package/web/ui/src/routes/ChannelWireDetail.test.tsx +233 -0
  479. package/web/ui/src/routes/ChannelWireDetail.tsx +403 -0
  480. package/web/ui/src/routes/ChannelsList.tsx +158 -0
  481. package/web/ui/src/routes/GroupDetail.tsx +755 -0
  482. package/web/ui/src/routes/GroupList.tsx +187 -0
  483. package/web/ui/src/routes/MessagingGroupDetail.test.tsx +233 -0
  484. package/web/ui/src/routes/MessagingGroupDetail.tsx +306 -0
  485. package/web/ui/src/routes/NewGroupWizard.tsx +390 -0
  486. package/web/ui/src/routes/OAuthCallback.tsx +56 -0
  487. package/web/ui/src/routes/SecretsList.tsx +921 -0
  488. package/web/ui/src/routes/SessionsList.tsx +220 -0
  489. package/web/ui/src/routes/SettingsAgentProvider.tsx +109 -0
  490. package/web/ui/src/routes/SettingsApprovals.tsx +234 -0
  491. package/web/ui/src/routes/SetupWizard.tsx +219 -0
  492. package/web/ui/src/routes/VaultDetail.test.tsx +361 -0
  493. package/web/ui/src/routes/VaultDetail.tsx +960 -0
  494. package/web/ui/src/routes/VaultsList.tsx +295 -0
  495. package/web/ui/src/routes/WireChannelPage.tsx +413 -0
  496. package/web/ui/src/styles.css +608 -0
  497. package/web/ui/src/test/setup.ts +23 -0
  498. package/web/ui/src/vite-env.d.ts +10 -0
  499. package/web/ui/tsconfig.json +20 -0
  500. package/web/ui/vite.config.ts +34 -0
  501. package/web/ui/vitest.config.ts +25 -0
@@ -0,0 +1,410 @@
1
+ /**
2
+ * api.ts unit tests — focused on the auth-gate behavior of `request<T>`.
3
+ * The vault token-mgmt helpers (mint/revoke/detach) thread an
4
+ * `authExtraScopes` hint so that a 403 scope-mismatch from the back-end
5
+ * triggers re-auth with the narrow per-vault scope appended, not just the
6
+ * broad REQUESTED_SCOPES set (paraclaw#56).
7
+ *
8
+ * Strategy: mock `auth.ts` so we can assert the exact arguments to
9
+ * beginLogin / refreshAccessToken / clearTokens; mock `fetch` to shape the
10
+ * wire response (200 / 403 with scope-mismatch body / 403 with unrelated
11
+ * body). beginLogin in production never returns (it does
12
+ * window.location.replace), so we mock it to *reject* — that lets the
13
+ * `await beginLogin(...)` inside request<T> propagate, and we assert on
14
+ * what it was called with.
15
+ */
16
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
17
+
18
+ import * as auth from './auth.ts';
19
+
20
+ vi.mock('./auth.ts', () => ({
21
+ beginLogin: vi.fn(),
22
+ clearTokens: vi.fn(),
23
+ getAccessToken: vi.fn(() => 'cached-token'),
24
+ refreshAccessToken: vi.fn(),
25
+ }));
26
+
27
+ beforeEach(() => {
28
+ // Each test does `await import('./api.ts')` after stubbing fetch. Without
29
+ // resetModules, vitest hands back the already-evaluated module from the
30
+ // first test in the file, so later tests see the *first* test's stubbed
31
+ // fetch — leading to false greens or confusing failures when one body
32
+ // shape is tested while another should fire. Reset between tests so
33
+ // every dynamic import re-evaluates against the current global stub.
34
+ vi.resetModules();
35
+ vi.mocked(auth.getAccessToken).mockReturnValue('cached-token');
36
+ // Reject so the `await beginLogin(...)` in request<T> resolves the chain
37
+ // and the caller's promise settles — letting us await + assert.
38
+ vi.mocked(auth.beginLogin).mockRejectedValue(new Error('beginLogin called (test)'));
39
+ vi.mocked(auth.refreshAccessToken).mockResolvedValue(null);
40
+ });
41
+
42
+ afterEach(() => {
43
+ vi.clearAllMocks();
44
+ vi.unstubAllGlobals();
45
+ });
46
+
47
+ function jsonResponse(status: number, body: unknown): Response {
48
+ return new Response(JSON.stringify(body), {
49
+ status,
50
+ headers: { 'content-type': 'application/json' },
51
+ });
52
+ }
53
+
54
+ describe('mintVaultToken — auth gate on 403', () => {
55
+ it('passes vault:<name>:admin to beginLogin when the vault returns scope-mismatch', async () => {
56
+ // Factory-per-call: a Response body is single-consume, so a single
57
+ // mockResolvedValue would let isScopeMismatch.clone().text() drain the
58
+ // body for the first test and leave a stale Response with an empty
59
+ // body for any subsequent code path.
60
+ const fetchMock = vi.fn(async () =>
61
+ jsonResponse(403, { error: "This endpoint requires the 'vault:work:admin' scope" }),
62
+ );
63
+ vi.stubGlobal('fetch', fetchMock);
64
+
65
+ const api = await import('./api.ts');
66
+ await expect(api.mintVaultToken('work', { label: 'claw-x', scopes: ['vault:read'] })).rejects.toThrow(
67
+ /beginLogin called/,
68
+ );
69
+
70
+ expect(auth.clearTokens).toHaveBeenCalled();
71
+ expect(auth.beginLogin).toHaveBeenCalledWith(['vault:work:admin']);
72
+ });
73
+ });
74
+
75
+ describe('revokeVaultToken — auth gate on 403', () => {
76
+ it('passes vault:<name>:admin to beginLogin', async () => {
77
+ // Factory-per-call: a Response body is single-consume, so a single
78
+ // mockResolvedValue would let isScopeMismatch.clone().text() drain the
79
+ // body for the first test and leave a stale Response with an empty
80
+ // body for any subsequent code path.
81
+ const fetchMock = vi.fn(async () =>
82
+ jsonResponse(403, { error: "This endpoint requires the 'vault:work:admin' scope" }),
83
+ );
84
+ vi.stubGlobal('fetch', fetchMock);
85
+
86
+ const api = await import('./api.ts');
87
+ await expect(api.revokeVaultToken('work', 't_abc')).rejects.toThrow(/beginLogin called/);
88
+
89
+ expect(auth.beginLogin).toHaveBeenCalledWith(['vault:work:admin']);
90
+ });
91
+ });
92
+
93
+ describe('detachVault — auth gate on 403', () => {
94
+ it('threads authExtraScopes from caller through to beginLogin', async () => {
95
+ // Factory-per-call: a Response body is single-consume, so a single
96
+ // mockResolvedValue would let isScopeMismatch.clone().text() drain the
97
+ // body for the first test and leave a stale Response with an empty
98
+ // body for any subsequent code path.
99
+ const fetchMock = vi.fn(async () =>
100
+ jsonResponse(403, { error: "This endpoint requires the 'vault:work:admin' scope" }),
101
+ );
102
+ vi.stubGlobal('fetch', fetchMock);
103
+
104
+ const api = await import('./api.ts');
105
+ await expect(
106
+ api.detachVault('research', {
107
+ mcpName: 'parachute-vault',
108
+ revokeToken: true,
109
+ authExtraScopes: ['vault:work:admin'],
110
+ }),
111
+ ).rejects.toThrow(/beginLogin called/);
112
+
113
+ expect(auth.beginLogin).toHaveBeenCalledWith(['vault:work:admin']);
114
+ });
115
+
116
+ it('omits scope hint when caller did not supply one', async () => {
117
+ const fetchMock = vi
118
+ .fn()
119
+ .mockResolvedValue(jsonResponse(403, { error: 'This endpoint requires the agent:admin scope' }));
120
+ vi.stubGlobal('fetch', fetchMock);
121
+
122
+ const api = await import('./api.ts');
123
+ await expect(api.detachVault('research', { revokeToken: false })).rejects.toThrow(/beginLogin called/);
124
+
125
+ expect(auth.beginLogin).toHaveBeenCalledWith(undefined);
126
+ });
127
+ });
128
+
129
+ describe('attachVault — auth gate on 403', () => {
130
+ it('threads authExtraScopes from caller through to beginLogin (paraclaw#65)', async () => {
131
+ const fetchMock = vi.fn(async () =>
132
+ jsonResponse(403, {
133
+ error: "vault token mint failed: This endpoint requires the 'vault:techne:admin' scope",
134
+ }),
135
+ );
136
+ vi.stubGlobal('fetch', fetchMock);
137
+
138
+ const api = await import('./api.ts');
139
+ await expect(
140
+ api.attachVault(
141
+ 'techne',
142
+ { scope: 'vault:read', vaultBaseUrl: 'https://example/vault/techne' },
143
+ { authExtraScopes: ['vault:techne:admin'] },
144
+ ),
145
+ ).rejects.toThrow(/beginLogin called/);
146
+
147
+ expect(auth.beginLogin).toHaveBeenCalledWith(['vault:techne:admin']);
148
+ });
149
+
150
+ it('omits scope hint when caller did not supply one', async () => {
151
+ const fetchMock = vi.fn().mockResolvedValue(
152
+ jsonResponse(403, {
153
+ error: "vault token mint failed: This endpoint requires the 'vault:techne:admin' scope",
154
+ }),
155
+ );
156
+ vi.stubGlobal('fetch', fetchMock);
157
+
158
+ const api = await import('./api.ts');
159
+ await expect(
160
+ api.attachVault('techne', { scope: 'vault:read', vaultBaseUrl: 'https://example/vault/techne' }),
161
+ ).rejects.toThrow(/beginLogin called/);
162
+
163
+ expect(auth.beginLogin).toHaveBeenCalledWith(undefined);
164
+ });
165
+ });
166
+
167
+ describe('createGroup — auth gate on 403', () => {
168
+ it('threads authExtraScopes when create-with-attach 403s on vault scope (paraclaw#65)', async () => {
169
+ const fetchMock = vi.fn(async () =>
170
+ jsonResponse(403, {
171
+ error: "vault token mint failed: This endpoint requires the 'vault:techne:admin' scope",
172
+ }),
173
+ );
174
+ vi.stubGlobal('fetch', fetchMock);
175
+
176
+ const api = await import('./api.ts');
177
+ await expect(
178
+ api.createGroup(
179
+ {
180
+ name: 'Techne',
181
+ folder: 'techne',
182
+ vault: { scope: 'vault:read', vaultBaseUrl: 'https://example/vault/techne' },
183
+ },
184
+ { authExtraScopes: ['vault:techne:admin'] },
185
+ ),
186
+ ).rejects.toThrow(/beginLogin called/);
187
+
188
+ expect(auth.beginLogin).toHaveBeenCalledWith(['vault:techne:admin']);
189
+ });
190
+ });
191
+
192
+ describe('non-scope 403 does NOT trigger re-auth', () => {
193
+ it('throws HttpError(403) when body is unrelated', async () => {
194
+ const fetchMock = vi.fn().mockResolvedValue(jsonResponse(403, { error: 'forbidden' }));
195
+ vi.stubGlobal('fetch', fetchMock);
196
+
197
+ const api = await import('./api.ts');
198
+ await expect(api.mintVaultToken('work', { label: 'x', scopes: ['vault:read'] })).rejects.toMatchObject({
199
+ name: 'HttpError',
200
+ status: 403,
201
+ });
202
+ expect(auth.beginLogin).not.toHaveBeenCalled();
203
+ expect(auth.clearTokens).not.toHaveBeenCalled();
204
+ });
205
+ });
206
+
207
+ describe('happy path — 200 returns parsed body and skips auth', () => {
208
+ it('mintVaultToken returns the parsed MintedVaultToken', async () => {
209
+ const minted = {
210
+ token: 'pvt_secret',
211
+ id: 't_new',
212
+ label: 'claw-x',
213
+ scopes: ['vault:read'],
214
+ };
215
+ const fetchMock = vi.fn().mockResolvedValue(jsonResponse(200, minted));
216
+ vi.stubGlobal('fetch', fetchMock);
217
+
218
+ const api = await import('./api.ts');
219
+ const result = await api.mintVaultToken('work', { label: 'claw-x', scopes: ['vault:read'] });
220
+
221
+ expect(result).toEqual(minted);
222
+ expect(auth.beginLogin).not.toHaveBeenCalled();
223
+ });
224
+ });
225
+
226
+ describe('updateMessagingGroupPolicy — body shape and method', () => {
227
+ // Server-side validateMgPatchInput keys on `unknownSenderPolicy` exactly;
228
+ // pin the wire shape here so a future rename doesn't silently regress.
229
+ it('PATCHes /channels/mg/:id with unknownSenderPolicy', async () => {
230
+ const result = {
231
+ messagingGroup: {
232
+ id: 'mg_1',
233
+ channelType: 'telegram',
234
+ platformId: 'telegram:42:1',
235
+ displayName: null,
236
+ isGroup: false,
237
+ unknownSenderPolicy: 'public',
238
+ deniedAt: null,
239
+ createdAt: '2026-04-20T10:00:00Z',
240
+ wiredAgents: [],
241
+ },
242
+ };
243
+ const fetchMock = vi.fn().mockResolvedValue(jsonResponse(200, result));
244
+ vi.stubGlobal('fetch', fetchMock);
245
+
246
+ const api = await import('./api.ts');
247
+ const view = await api.updateMessagingGroupPolicy('mg_1', 'public');
248
+
249
+ expect(view.unknownSenderPolicy).toBe('public');
250
+ expect(fetchMock).toHaveBeenCalledTimes(1);
251
+ const [url, init] = fetchMock.mock.calls[0]!;
252
+ expect(String(url)).toMatch(/\/api\/channels\/mg\/mg_1$/);
253
+ expect((init as RequestInit).method).toBe('PATCH');
254
+ expect(JSON.parse((init as RequestInit).body as string)).toEqual({ unknownSenderPolicy: 'public' });
255
+ });
256
+
257
+ it('surfaces server 400 as HttpError(400) without re-auth', async () => {
258
+ const fetchMock = vi.fn().mockResolvedValue(jsonResponse(400, { error: 'invalid unknownSenderPolicy: open' }));
259
+ vi.stubGlobal('fetch', fetchMock);
260
+
261
+ const api = await import('./api.ts');
262
+ // Cast through unknown so we can drive the bad-input branch even though
263
+ // `open` isn't a valid UnknownSenderPolicy at the type level — the
264
+ // server is what we're pinning here, not the static check.
265
+ type UnknownSenderPolicy = import('./api.ts').UnknownSenderPolicy;
266
+ await expect(
267
+ api.updateMessagingGroupPolicy('mg_1', 'open' as unknown as UnknownSenderPolicy),
268
+ ).rejects.toMatchObject({ name: 'HttpError', status: 400 });
269
+ expect(auth.beginLogin).not.toHaveBeenCalled();
270
+ });
271
+ });
272
+
273
+ describe('getMessagingGroupDetail — happy path', () => {
274
+ it('parses { messagingGroup } envelope and returns the view', async () => {
275
+ const view = {
276
+ id: 'mg_x',
277
+ channelType: 'discord',
278
+ platformId: 'discord:@me:99',
279
+ displayName: 'Aaron DM',
280
+ isGroup: false,
281
+ unknownSenderPolicy: 'request_approval',
282
+ deniedAt: null,
283
+ createdAt: '2026-04-20T10:00:00Z',
284
+ wiredAgents: [
285
+ {
286
+ messagingGroupAgentId: 'mga_1',
287
+ agentGroupId: 'ag_1',
288
+ agentGroupFolder: 'main',
289
+ agentGroupName: 'Main',
290
+ engageMode: 'mention',
291
+ engagePattern: null,
292
+ senderScope: 'all',
293
+ ignoredMessagePolicy: 'drop',
294
+ priority: 0,
295
+ createdAt: '2026-04-20T10:00:00Z',
296
+ },
297
+ ],
298
+ };
299
+ const fetchMock = vi.fn().mockResolvedValue(jsonResponse(200, { messagingGroup: view }));
300
+ vi.stubGlobal('fetch', fetchMock);
301
+
302
+ const api = await import('./api.ts');
303
+ const result = await api.getMessagingGroupDetail('mg_x');
304
+ expect(result).toEqual(view);
305
+ });
306
+ });
307
+
308
+ describe('wireChannelToGroup — body contract with server', () => {
309
+ // Server keys on `channelType` (matches DB column + WireDmInput). Helper
310
+ // accepts `channel` for ergonomics; this test pins the wire boundary so
311
+ // a future rename can't silently regress to "channelType must be …" 400s.
312
+ it('serializes input.channel as channelType in the request body', async () => {
313
+ const result = {
314
+ messagingGroupId: 'mg_1',
315
+ messagingGroupAgentId: 'mga_1',
316
+ platformId: 'telegram:42',
317
+ created: { messagingGroup: true, wiring: true },
318
+ };
319
+ const fetchMock = vi.fn().mockResolvedValue(jsonResponse(200, result));
320
+ vi.stubGlobal('fetch', fetchMock);
321
+
322
+ const api = await import('./api.ts');
323
+ await api.wireChannelToGroup('forge', {
324
+ channel: 'telegram',
325
+ botId: '7654321',
326
+ botUserId: '42',
327
+ operatorUserId: '42',
328
+ displayName: 'Forge DM',
329
+ });
330
+
331
+ expect(fetchMock).toHaveBeenCalledTimes(1);
332
+ const [, init] = fetchMock.mock.calls[0]!;
333
+ const body = JSON.parse((init as RequestInit).body as string);
334
+ expect(body).toEqual({
335
+ channelType: 'telegram',
336
+ botId: '7654321',
337
+ botUserId: '42',
338
+ operatorUserId: '42',
339
+ displayName: 'Forge DM',
340
+ });
341
+ expect(body.channel).toBeUndefined();
342
+ });
343
+ });
344
+
345
+ describe('channel-wire helpers — pinned to /channels/mga/:id', () => {
346
+ // PR3 disambiguates per-MG and per-MGA detail under prefixed paths. Pin
347
+ // the helper paths so a future refactor can't silently fall back to the
348
+ // single-segment `/channels/:id` shape that PR3 deleted.
349
+ function wireView(over: Partial<import('./api.ts').ChannelWireView> = {}): import('./api.ts').ChannelWireView {
350
+ return {
351
+ id: 'mga_1',
352
+ channelType: 'telegram',
353
+ messagingGroupId: 'mg_1',
354
+ platformId: 'telegram:42:1',
355
+ displayName: null,
356
+ agentGroupId: 'ag_1',
357
+ agentGroupFolder: 'main',
358
+ agentGroupName: 'Main',
359
+ engageMode: 'mention',
360
+ engagePattern: null,
361
+ senderScope: 'all',
362
+ ignoredMessagePolicy: 'drop',
363
+ priority: 0,
364
+ createdAt: '2026-04-20T10:00:00Z',
365
+ ...over,
366
+ };
367
+ }
368
+
369
+ it('getChannelWireDetail GETs /channels/mga/:id and unwraps { wire }', async () => {
370
+ const view = wireView({ id: 'mga_x', engageMode: 'all' });
371
+ const fetchMock = vi.fn().mockResolvedValue(jsonResponse(200, { wire: view }));
372
+ vi.stubGlobal('fetch', fetchMock);
373
+
374
+ const api = await import('./api.ts');
375
+ const result = await api.getChannelWireDetail('mga_x');
376
+
377
+ expect(result).toEqual(view);
378
+ expect(fetchMock).toHaveBeenCalledTimes(1);
379
+ const [url, init] = fetchMock.mock.calls[0]!;
380
+ expect(String(url)).toMatch(/\/api\/channels\/mga\/mga_x$/);
381
+ expect((init as RequestInit).method ?? 'GET').toBe('GET');
382
+ });
383
+
384
+ it('updateChannelWire PATCHes /channels/mga/:id with the input body', async () => {
385
+ const fetchMock = vi.fn().mockResolvedValue(jsonResponse(200, { wire: wireView({ engageMode: 'all' }) }));
386
+ vi.stubGlobal('fetch', fetchMock);
387
+
388
+ const api = await import('./api.ts');
389
+ await api.updateChannelWire('mga_1', { engageMode: 'all', priority: 3 });
390
+
391
+ expect(fetchMock).toHaveBeenCalledTimes(1);
392
+ const [url, init] = fetchMock.mock.calls[0]!;
393
+ expect(String(url)).toMatch(/\/api\/channels\/mga\/mga_1$/);
394
+ expect((init as RequestInit).method).toBe('PATCH');
395
+ expect(JSON.parse((init as RequestInit).body as string)).toEqual({ engageMode: 'all', priority: 3 });
396
+ });
397
+
398
+ it('deleteChannelWire DELETEs /channels/mga/:id', async () => {
399
+ const fetchMock = vi.fn().mockResolvedValue(jsonResponse(200, { id: 'mga_1', deleted: true }));
400
+ vi.stubGlobal('fetch', fetchMock);
401
+
402
+ const api = await import('./api.ts');
403
+ await api.deleteChannelWire('mga_1');
404
+
405
+ expect(fetchMock).toHaveBeenCalledTimes(1);
406
+ const [url, init] = fetchMock.mock.calls[0]!;
407
+ expect(String(url)).toMatch(/\/api\/channels\/mga\/mga_1$/);
408
+ expect((init as RequestInit).method).toBe('DELETE');
409
+ });
410
+ });