@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,313 @@
1
+ /**
2
+ * Channel adapter registry.
3
+ *
4
+ * Channels self-register on import. The host calls initChannelAdapters() at startup
5
+ * to instantiate and set up all registered adapters.
6
+ */
7
+ import type { ChannelAdapter, ChannelRegistration, ChannelSetup } from './adapter.js';
8
+ import { getDb } from '../db/connection.js';
9
+ import { log } from '../log.js';
10
+ import { decodePlatformIdAs } from '../platform-id.js';
11
+ import { listSecrets, getSecret } from '../secrets/index.js';
12
+
13
+ const SETUP_RETRY_DELAYS_MS = [2000, 5000, 10000];
14
+
15
+ /** Duck-type check — adapters that throw an Error with `name === 'NetworkError'`
16
+ * (Chat SDK's `@chat-adapter/shared.NetworkError` and similar) get a retry on
17
+ * setup. Avoids depending on `@chat-adapter/shared` at trunk level. */
18
+ function isNetworkError(err: unknown): err is Error {
19
+ return err instanceof Error && err.name === 'NetworkError';
20
+ }
21
+
22
+ const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
23
+
24
+ const registry = new Map<string, ChannelRegistration>();
25
+ /**
26
+ * Active adapters keyed by `<channelType>\0<botId>` so two adapters on the
27
+ * same channel type but different bots (e.g. two Telegram bots) can coexist.
28
+ * Adapters without a bot dimension (CLI admin transport) key under empty
29
+ * botId.
30
+ */
31
+ const activeAdapters = new Map<string, ChannelAdapter>();
32
+
33
+ /**
34
+ * Cached host-callbacks builder, set by {@link initChannelAdapters} the
35
+ * first time it runs. Reused by {@link spawnSecretsBackedBots} (boot scan)
36
+ * and {@link registerBotAdapter} (dynamic per-bot adds via the HTTP
37
+ * register-bot endpoint) so every adapter — primary or dynamic — wires
38
+ * inbound through the same router callbacks.
39
+ */
40
+ let cachedSetupFn: ((adapter: ChannelAdapter) => ChannelSetup) | null = null;
41
+
42
+ function adapterKey(channelType: string, botId: string | null | undefined): string {
43
+ return `${channelType}\0${botId ?? ''}`;
44
+ }
45
+
46
+ /** Register a channel adapter factory. Called by channel modules on import. */
47
+ export function registerChannelAdapter(name: string, registration: ChannelRegistration): void {
48
+ registry.set(name, registration);
49
+ }
50
+
51
+ /**
52
+ * Get a live adapter by channel type. Returns the first adapter registered
53
+ * under that type — meaningful only in single-bot-per-platform installs
54
+ * (the current state through PR A). Multi-bot callers must use
55
+ * {@link getChannelAdapterForPlatformId} so the right bot's adapter is
56
+ * selected by the v2 platform_id's bot dimension.
57
+ */
58
+ export function getChannelAdapter(channelType: string): ChannelAdapter | undefined {
59
+ for (const [key, adapter] of activeAdapters) {
60
+ if (key.startsWith(`${channelType}\0`)) return adapter;
61
+ }
62
+ return undefined;
63
+ }
64
+
65
+ /** Resolve a live adapter by exact `(channelType, botId)`. Returns undefined if no adapter for that bot is active. */
66
+ export function getChannelAdapterByBotId(channelType: string, botId: string): ChannelAdapter | undefined {
67
+ return activeAdapters.get(adapterKey(channelType, botId));
68
+ }
69
+
70
+ /**
71
+ * Resolve the adapter responsible for a given v2 platform_id by decoding
72
+ * its bot segment. Falls back to the channel-type-only lookup for legacy
73
+ * v1 ids (botId === null) so deliveries against not-yet-backfilled rows
74
+ * still go somewhere sensible during the rollout window.
75
+ */
76
+ export function getChannelAdapterForPlatformId(channelType: string, platformId: string): ChannelAdapter | undefined {
77
+ const decoded = decodePlatformIdAs(platformId, 'v2');
78
+ if (decoded.botId !== null) {
79
+ const exact = activeAdapters.get(adapterKey(channelType, decoded.botId));
80
+ if (exact) return exact;
81
+ }
82
+ return getChannelAdapter(channelType);
83
+ }
84
+
85
+ /** Get all active adapters. */
86
+ export function getActiveAdapters(): ChannelAdapter[] {
87
+ return [...activeAdapters.values()];
88
+ }
89
+
90
+ /** Get all registered channel names. */
91
+ export function getRegisteredChannelNames(): string[] {
92
+ return [...registry.keys()];
93
+ }
94
+
95
+ /** Get container config for a channel (used by container-runner for additional mounts/env). */
96
+ export function getChannelContainerConfig(name: string): ChannelRegistration['containerConfig'] {
97
+ return registry.get(name)?.containerConfig;
98
+ }
99
+
100
+ /**
101
+ * Run an adapter's setup with NetworkError retry — the same retry loop both
102
+ * {@link initChannelAdapters} and the dynamic register helpers share so
103
+ * boot-time and runtime adds get identical resilience semantics.
104
+ */
105
+ async function setupAdapterWithRetry(adapter: ChannelAdapter, setup: ChannelSetup, label: string): Promise<void> {
106
+ let attempt = 0;
107
+ while (true) {
108
+ try {
109
+ await adapter.setup(setup);
110
+ return;
111
+ } catch (err) {
112
+ if (isNetworkError(err) && attempt < SETUP_RETRY_DELAYS_MS.length) {
113
+ const delay = SETUP_RETRY_DELAYS_MS[attempt]!;
114
+ log.warn('Channel adapter setup failed with network error, retrying', {
115
+ channel: label,
116
+ attempt: attempt + 1,
117
+ delayMs: delay,
118
+ err: err.message,
119
+ });
120
+ await sleep(delay);
121
+ attempt += 1;
122
+ continue;
123
+ }
124
+ throw err;
125
+ }
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Instantiate and set up all registered channel adapters.
131
+ * Skips adapters that return null (missing credentials).
132
+ */
133
+ export async function initChannelAdapters(setupFn: (adapter: ChannelAdapter) => ChannelSetup): Promise<void> {
134
+ cachedSetupFn = setupFn;
135
+ for (const [name, registration] of registry) {
136
+ try {
137
+ const adapter = await registration.factory();
138
+ if (!adapter) {
139
+ log.warn('Channel credentials missing, skipping', { channel: name });
140
+ continue;
141
+ }
142
+
143
+ // Transient network failures during adapter init (e.g. Telegram deleteWebhook
144
+ // hitting a DNS hiccup at boot) would otherwise leave the channel permanently
145
+ // dead until manual restart. Retry only on NetworkError so misconfigs (bad
146
+ // tokens, etc.) still fail fast.
147
+ await setupAdapterWithRetry(adapter, setupFn(adapter), name);
148
+ activeAdapters.set(adapterKey(adapter.channelType, adapter.botId), adapter);
149
+ log.info('Channel adapter started', {
150
+ channel: name,
151
+ type: adapter.channelType,
152
+ botId: adapter.botId ?? null,
153
+ });
154
+ } catch (err) {
155
+ log.error('Failed to start channel adapter', { channel: name, err });
156
+ }
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Register an adapter for a specific bot at runtime — used by the
162
+ * `POST /api/channels/{channel}/register-bot` endpoint after the operator
163
+ * pastes a token via the wire-channel UI.
164
+ *
165
+ * Resolves the channel's `spawnFromSecret` hook to build the adapter, runs
166
+ * setup with the same callbacks the primary adapter uses, and slots it into
167
+ * `activeAdapters` keyed by `(channelType, botId)`. Idempotent: if an
168
+ * adapter is already active for this `(channelType, botId)` it returns the
169
+ * existing one instead of double-registering.
170
+ *
171
+ * Throws if the channel doesn't expose `spawnFromSecret` (single-bot
172
+ * channel) or if `initChannelAdapters` hasn't run yet (no cached setup
173
+ * callbacks). Returns null if the spawn hook itself returns null (token
174
+ * rejected at the platform).
175
+ */
176
+ export async function registerBotAdapter(
177
+ channelType: string,
178
+ secretName: string,
179
+ secretValue: string,
180
+ ): Promise<ChannelAdapter | null> {
181
+ const registration = registry.get(channelType);
182
+ if (!registration) throw new Error(`unknown channel: ${channelType}`);
183
+ if (!registration.spawnFromSecret) {
184
+ throw new Error(`channel does not support multi-bot operation: ${channelType}`);
185
+ }
186
+ if (!cachedSetupFn) {
187
+ throw new Error('initChannelAdapters has not run yet — cannot register dynamic bot');
188
+ }
189
+ const adapter = await registration.spawnFromSecret(secretName, secretValue);
190
+ if (!adapter) return null;
191
+ const key = adapterKey(adapter.channelType, adapter.botId);
192
+ const existing = activeAdapters.get(key);
193
+ if (existing) {
194
+ log.info('Channel adapter already active for bot, skipping re-setup', {
195
+ channel: channelType,
196
+ botId: adapter.botId ?? null,
197
+ });
198
+ return existing;
199
+ }
200
+ await setupAdapterWithRetry(adapter, cachedSetupFn(adapter), channelType);
201
+ activeAdapters.set(key, adapter);
202
+ log.info('Channel adapter registered dynamically', {
203
+ channel: channelType,
204
+ botId: adapter.botId ?? null,
205
+ });
206
+ return adapter;
207
+ }
208
+
209
+ /**
210
+ * Boot-time scan that brings up adapters for every persisted
211
+ * `CHANNEL_BOT_TOKEN:<channel>:<botId>` secret that (a) has at least one
212
+ * wired messaging_group_agents row and (b) isn't already covered by the
213
+ * `.env`-seeded primary adapter. Run AFTER `initChannelAdapters` and
214
+ * AFTER `runStartupBootstrap`.
215
+ *
216
+ * Orphan rule (paraclaw#67 Proposal A): a secret with no MGA wiring is
217
+ * "registered but inert" — the operator validated a token but never
218
+ * committed it to a group. We deliberately don't spawn its adapter at
219
+ * boot, because doing so would re-introduce the validate-then-poll race
220
+ * that pre-A behavior had: an adapter polling without a wire feeds
221
+ * inbounds straight into the unwired-channel approval cascade. Operators
222
+ * recover by completing the wire from `/agent/channels/new` (the wire
223
+ * endpoint spawns the adapter atomically with the MGA insert).
224
+ *
225
+ * Skips channels with no `spawnFromSecret` hook. Logs but doesn't throw
226
+ * on individual spawn failures — one bad token shouldn't keep the rest
227
+ * offline.
228
+ */
229
+ export async function spawnSecretsBackedBots(): Promise<void> {
230
+ const tokens = listSecrets(null).filter((s) => s.kind === 'channel-token' && s.name.startsWith('CHANNEL_BOT_TOKEN:'));
231
+ for (const row of tokens) {
232
+ const parts = row.name.split(':');
233
+ if (parts.length < 3) continue;
234
+ const channelType = parts[1]!;
235
+ const botId = parts.slice(2).join(':');
236
+ if (!channelType || !botId) continue;
237
+ const registration = registry.get(channelType);
238
+ if (!registration?.spawnFromSecret) continue;
239
+ if (activeAdapters.has(adapterKey(channelType, botId))) continue;
240
+ if (!hasWiringForBot(channelType, botId)) {
241
+ log.info('Skipping orphan channel bot secret (no wiring)', { channel: channelType, botId });
242
+ continue;
243
+ }
244
+ const value = getSecret(row.name);
245
+ if (!value) {
246
+ log.warn('Channel bot secret has no value, skipping', { secret: row.name });
247
+ continue;
248
+ }
249
+ try {
250
+ const adapter = await registerBotAdapter(channelType, row.name, value);
251
+ if (!adapter) {
252
+ log.warn('Secrets-backed bot spawn returned null', { channel: channelType, botId });
253
+ }
254
+ } catch (err) {
255
+ log.error('Failed to spawn secrets-backed bot', { channel: channelType, botId, err });
256
+ }
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Returns true iff at least one messaging_group_agents row exists wired
262
+ * through a messaging_groups row whose platform_id encodes the given
263
+ * `(channelType, botId)` pair (v2 format `<channel>:<botId>:<native>`).
264
+ *
265
+ * v1 rows (no bot dimension) for the same channel match nothing here —
266
+ * the secrets-backed scan only runs for bots that have a botId in their
267
+ * secret name, and v1 wires don't carry one. That's the correct
268
+ * conservative answer: a v1 wire could belong to *any* bot on that
269
+ * channel, so we can't safely auto-attribute it to this secret.
270
+ */
271
+ function hasWiringForBot(channelType: string, botId: string): boolean {
272
+ const row = getDb()
273
+ .prepare(
274
+ `SELECT 1
275
+ FROM messaging_groups mg
276
+ JOIN messaging_group_agents mga ON mga.messaging_group_id = mg.id
277
+ WHERE mg.channel_type = ?
278
+ AND mg.platform_id LIKE ? ESCAPE '\\'
279
+ LIMIT 1`,
280
+ )
281
+ .get(channelType, `${channelType}:${escapeLike(botId)}:%`);
282
+ return row !== undefined;
283
+ }
284
+
285
+ function escapeLike(s: string): string {
286
+ return s.replace(/\\/g, '\\\\').replace(/%/g, '\\%').replace(/_/g, '\\_');
287
+ }
288
+
289
+ /**
290
+ * Test-only: inject a fake active adapter so functions that read the
291
+ * registry (e.g. startup-bootstrap) can run without a full setup. Caller
292
+ * is responsible for calling {@link _resetActiveAdaptersForTest} after.
293
+ */
294
+ export function _setActiveAdapterForTest(adapter: ChannelAdapter): void {
295
+ activeAdapters.set(adapterKey(adapter.channelType, adapter.botId), adapter);
296
+ }
297
+
298
+ export function _resetActiveAdaptersForTest(): void {
299
+ activeAdapters.clear();
300
+ }
301
+
302
+ /** Tear down all active adapters. */
303
+ export async function teardownChannelAdapters(): Promise<void> {
304
+ for (const [key, adapter] of activeAdapters) {
305
+ try {
306
+ await adapter.teardown();
307
+ log.info('Channel adapter stopped', { key, type: adapter.channelType });
308
+ } catch (err) {
309
+ log.error('Failed to stop channel adapter', { key, type: adapter.channelType, err });
310
+ }
311
+ }
312
+ activeAdapters.clear();
313
+ }
@@ -0,0 +1,84 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import type { Adapter } from 'chat';
4
+
5
+ import { createChatSdkBridge, splitForLimit } from './chat-sdk-bridge.js';
6
+
7
+ function stubAdapter(partial: Partial<Adapter>): Adapter {
8
+ return { name: 'stub', ...partial } as unknown as Adapter;
9
+ }
10
+
11
+ describe('splitForLimit', () => {
12
+ it('returns a single chunk when text fits', () => {
13
+ expect(splitForLimit('short text', 100)).toEqual(['short text']);
14
+ });
15
+
16
+ it('splits on paragraph boundaries when available', () => {
17
+ const text = 'para one line one\npara one line two\n\npara two line one\npara two line two';
18
+ const chunks = splitForLimit(text, 40);
19
+ expect(chunks.length).toBeGreaterThan(1);
20
+ for (const c of chunks) expect(c.length).toBeLessThanOrEqual(40);
21
+ });
22
+
23
+ it('falls back to line boundaries when no paragraph fits', () => {
24
+ const text = 'alpha\nbravo\ncharlie\ndelta\necho\nfoxtrot';
25
+ const chunks = splitForLimit(text, 15);
26
+ expect(chunks.length).toBeGreaterThan(1);
27
+ for (const c of chunks) expect(c.length).toBeLessThanOrEqual(15);
28
+ });
29
+
30
+ it('hard-cuts when no whitespace is available', () => {
31
+ const text = 'a'.repeat(100);
32
+ const chunks = splitForLimit(text, 30);
33
+ expect(chunks.length).toBe(Math.ceil(100 / 30));
34
+ for (const c of chunks) expect(c.length).toBeLessThanOrEqual(30);
35
+ expect(chunks.join('')).toBe(text);
36
+ });
37
+ });
38
+
39
+ describe('createChatSdkBridge', () => {
40
+ // The bridge is now transport-only: forward inbound events, relay outbound
41
+ // ops. All per-wiring engage / accumulate / drop / subscribe decisions live
42
+ // in the router (src/router.ts routeInbound / evaluateEngage) and are
43
+ // exercised by host-core.test.ts end-to-end. These tests only cover the
44
+ // bridge's narrow, platform-adjacent surface.
45
+
46
+ it('omits openDM when the underlying Chat SDK adapter has none', () => {
47
+ const bridge = createChatSdkBridge({
48
+ adapter: stubAdapter({}),
49
+ botId: 'bot-1',
50
+ supportsThreads: false,
51
+ });
52
+ expect(bridge.openDM).toBeUndefined();
53
+ });
54
+
55
+ it('exposes openDM when the underlying adapter has one, and encodes the bot dim', async () => {
56
+ const openDMCalls: string[] = [];
57
+ const bridge = createChatSdkBridge({
58
+ adapter: stubAdapter({
59
+ openDM: async (userId: string) => {
60
+ openDMCalls.push(userId);
61
+ return `thread::${userId}`;
62
+ },
63
+ channelIdFromThreadId: (threadId: string) => `stub:${threadId.replace(/^thread::/, '')}`,
64
+ }),
65
+ botId: 'bot-1',
66
+ supportsThreads: false,
67
+ });
68
+ expect(bridge.openDM).toBeDefined();
69
+ const platformId = await bridge.openDM!('user-42');
70
+ // Delegation: adapter.openDM → adapter.channelIdFromThreadId, then bridge
71
+ // wraps with the bot id so messaging_groups gets the v2 form.
72
+ expect(openDMCalls).toEqual(['user-42']);
73
+ expect(platformId).toBe('stub:bot-1:user-42');
74
+ });
75
+
76
+ it('exposes subscribe (lets the router initiate thread subscription on mention-sticky engage)', () => {
77
+ const bridge = createChatSdkBridge({
78
+ adapter: stubAdapter({}),
79
+ botId: 'bot-1',
80
+ supportsThreads: true,
81
+ });
82
+ expect(typeof bridge.subscribe).toBe('function');
83
+ });
84
+ });