availsync 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 (460) hide show
  1. package/.adal/skills/stripe-best-practices/SKILL.md +42 -0
  2. package/.adal/skills/stripe-best-practices/references/billing.md +36 -0
  3. package/.adal/skills/stripe-best-practices/references/connect.md +48 -0
  4. package/.adal/skills/stripe-best-practices/references/payments.md +79 -0
  5. package/.adal/skills/stripe-best-practices/references/security.md +109 -0
  6. package/.adal/skills/stripe-best-practices/references/treasury.md +16 -0
  7. package/.adal/skills/stripe-projects/SKILL.md +139 -0
  8. package/.adal/skills/upgrade-stripe/SKILL.md +185 -0
  9. package/.agents/skills/stripe-best-practices/SKILL.md +42 -0
  10. package/.agents/skills/stripe-best-practices/references/billing.md +36 -0
  11. package/.agents/skills/stripe-best-practices/references/connect.md +48 -0
  12. package/.agents/skills/stripe-best-practices/references/payments.md +79 -0
  13. package/.agents/skills/stripe-best-practices/references/security.md +109 -0
  14. package/.agents/skills/stripe-best-practices/references/treasury.md +16 -0
  15. package/.agents/skills/stripe-projects/SKILL.md +139 -0
  16. package/.agents/skills/upgrade-stripe/SKILL.md +185 -0
  17. package/.augment/skills/stripe-best-practices/SKILL.md +42 -0
  18. package/.augment/skills/stripe-best-practices/references/billing.md +36 -0
  19. package/.augment/skills/stripe-best-practices/references/connect.md +48 -0
  20. package/.augment/skills/stripe-best-practices/references/payments.md +79 -0
  21. package/.augment/skills/stripe-best-practices/references/security.md +109 -0
  22. package/.augment/skills/stripe-best-practices/references/treasury.md +16 -0
  23. package/.augment/skills/stripe-projects/SKILL.md +139 -0
  24. package/.augment/skills/upgrade-stripe/SKILL.md +185 -0
  25. package/.bob/skills/stripe-best-practices/SKILL.md +42 -0
  26. package/.bob/skills/stripe-best-practices/references/billing.md +36 -0
  27. package/.bob/skills/stripe-best-practices/references/connect.md +48 -0
  28. package/.bob/skills/stripe-best-practices/references/payments.md +79 -0
  29. package/.bob/skills/stripe-best-practices/references/security.md +109 -0
  30. package/.bob/skills/stripe-best-practices/references/treasury.md +16 -0
  31. package/.bob/skills/stripe-projects/SKILL.md +139 -0
  32. package/.bob/skills/upgrade-stripe/SKILL.md +185 -0
  33. package/.claude/settings.local.json +7 -0
  34. package/.claude/skills/stripe-best-practices/SKILL.md +42 -0
  35. package/.claude/skills/stripe-best-practices/references/billing.md +36 -0
  36. package/.claude/skills/stripe-best-practices/references/connect.md +48 -0
  37. package/.claude/skills/stripe-best-practices/references/payments.md +79 -0
  38. package/.claude/skills/stripe-best-practices/references/security.md +109 -0
  39. package/.claude/skills/stripe-best-practices/references/treasury.md +16 -0
  40. package/.claude/skills/stripe-projects/SKILL.md +139 -0
  41. package/.claude/skills/upgrade-stripe/SKILL.md +185 -0
  42. package/.codebuddy/skills/stripe-best-practices/SKILL.md +42 -0
  43. package/.codebuddy/skills/stripe-best-practices/references/billing.md +36 -0
  44. package/.codebuddy/skills/stripe-best-practices/references/connect.md +48 -0
  45. package/.codebuddy/skills/stripe-best-practices/references/payments.md +79 -0
  46. package/.codebuddy/skills/stripe-best-practices/references/security.md +109 -0
  47. package/.codebuddy/skills/stripe-best-practices/references/treasury.md +16 -0
  48. package/.codebuddy/skills/stripe-projects/SKILL.md +139 -0
  49. package/.codebuddy/skills/upgrade-stripe/SKILL.md +185 -0
  50. package/.commandcode/skills/stripe-best-practices/SKILL.md +42 -0
  51. package/.commandcode/skills/stripe-best-practices/references/billing.md +36 -0
  52. package/.commandcode/skills/stripe-best-practices/references/connect.md +48 -0
  53. package/.commandcode/skills/stripe-best-practices/references/payments.md +79 -0
  54. package/.commandcode/skills/stripe-best-practices/references/security.md +109 -0
  55. package/.commandcode/skills/stripe-best-practices/references/treasury.md +16 -0
  56. package/.commandcode/skills/stripe-projects/SKILL.md +139 -0
  57. package/.commandcode/skills/upgrade-stripe/SKILL.md +185 -0
  58. package/.continue/skills/stripe-best-practices/SKILL.md +42 -0
  59. package/.continue/skills/stripe-best-practices/references/billing.md +36 -0
  60. package/.continue/skills/stripe-best-practices/references/connect.md +48 -0
  61. package/.continue/skills/stripe-best-practices/references/payments.md +79 -0
  62. package/.continue/skills/stripe-best-practices/references/security.md +109 -0
  63. package/.continue/skills/stripe-best-practices/references/treasury.md +16 -0
  64. package/.continue/skills/stripe-projects/SKILL.md +139 -0
  65. package/.continue/skills/upgrade-stripe/SKILL.md +185 -0
  66. package/.cortex/skills/stripe-best-practices/SKILL.md +42 -0
  67. package/.cortex/skills/stripe-best-practices/references/billing.md +36 -0
  68. package/.cortex/skills/stripe-best-practices/references/connect.md +48 -0
  69. package/.cortex/skills/stripe-best-practices/references/payments.md +79 -0
  70. package/.cortex/skills/stripe-best-practices/references/security.md +109 -0
  71. package/.cortex/skills/stripe-best-practices/references/treasury.md +16 -0
  72. package/.cortex/skills/stripe-projects/SKILL.md +139 -0
  73. package/.cortex/skills/upgrade-stripe/SKILL.md +185 -0
  74. package/.crush/skills/stripe-best-practices/SKILL.md +42 -0
  75. package/.crush/skills/stripe-best-practices/references/billing.md +36 -0
  76. package/.crush/skills/stripe-best-practices/references/connect.md +48 -0
  77. package/.crush/skills/stripe-best-practices/references/payments.md +79 -0
  78. package/.crush/skills/stripe-best-practices/references/security.md +109 -0
  79. package/.crush/skills/stripe-best-practices/references/treasury.md +16 -0
  80. package/.crush/skills/stripe-projects/SKILL.md +139 -0
  81. package/.crush/skills/upgrade-stripe/SKILL.md +185 -0
  82. package/.env.example +20 -0
  83. package/.factory/skills/stripe-best-practices/SKILL.md +42 -0
  84. package/.factory/skills/stripe-best-practices/references/billing.md +36 -0
  85. package/.factory/skills/stripe-best-practices/references/connect.md +48 -0
  86. package/.factory/skills/stripe-best-practices/references/payments.md +79 -0
  87. package/.factory/skills/stripe-best-practices/references/security.md +109 -0
  88. package/.factory/skills/stripe-best-practices/references/treasury.md +16 -0
  89. package/.factory/skills/stripe-projects/SKILL.md +139 -0
  90. package/.factory/skills/upgrade-stripe/SKILL.md +185 -0
  91. package/.goose/skills/stripe-best-practices/SKILL.md +42 -0
  92. package/.goose/skills/stripe-best-practices/references/billing.md +36 -0
  93. package/.goose/skills/stripe-best-practices/references/connect.md +48 -0
  94. package/.goose/skills/stripe-best-practices/references/payments.md +79 -0
  95. package/.goose/skills/stripe-best-practices/references/security.md +109 -0
  96. package/.goose/skills/stripe-best-practices/references/treasury.md +16 -0
  97. package/.goose/skills/stripe-projects/SKILL.md +139 -0
  98. package/.goose/skills/upgrade-stripe/SKILL.md +185 -0
  99. package/.iflow/skills/stripe-best-practices/SKILL.md +42 -0
  100. package/.iflow/skills/stripe-best-practices/references/billing.md +36 -0
  101. package/.iflow/skills/stripe-best-practices/references/connect.md +48 -0
  102. package/.iflow/skills/stripe-best-practices/references/payments.md +79 -0
  103. package/.iflow/skills/stripe-best-practices/references/security.md +109 -0
  104. package/.iflow/skills/stripe-best-practices/references/treasury.md +16 -0
  105. package/.iflow/skills/stripe-projects/SKILL.md +139 -0
  106. package/.iflow/skills/upgrade-stripe/SKILL.md +185 -0
  107. package/.junie/skills/stripe-best-practices/SKILL.md +42 -0
  108. package/.junie/skills/stripe-best-practices/references/billing.md +36 -0
  109. package/.junie/skills/stripe-best-practices/references/connect.md +48 -0
  110. package/.junie/skills/stripe-best-practices/references/payments.md +79 -0
  111. package/.junie/skills/stripe-best-practices/references/security.md +109 -0
  112. package/.junie/skills/stripe-best-practices/references/treasury.md +16 -0
  113. package/.junie/skills/stripe-projects/SKILL.md +139 -0
  114. package/.junie/skills/upgrade-stripe/SKILL.md +185 -0
  115. package/.kilocode/skills/stripe-best-practices/SKILL.md +42 -0
  116. package/.kilocode/skills/stripe-best-practices/references/billing.md +36 -0
  117. package/.kilocode/skills/stripe-best-practices/references/connect.md +48 -0
  118. package/.kilocode/skills/stripe-best-practices/references/payments.md +79 -0
  119. package/.kilocode/skills/stripe-best-practices/references/security.md +109 -0
  120. package/.kilocode/skills/stripe-best-practices/references/treasury.md +16 -0
  121. package/.kilocode/skills/stripe-projects/SKILL.md +139 -0
  122. package/.kilocode/skills/upgrade-stripe/SKILL.md +185 -0
  123. package/.kiro/skills/stripe-best-practices/SKILL.md +42 -0
  124. package/.kiro/skills/stripe-best-practices/references/billing.md +36 -0
  125. package/.kiro/skills/stripe-best-practices/references/connect.md +48 -0
  126. package/.kiro/skills/stripe-best-practices/references/payments.md +79 -0
  127. package/.kiro/skills/stripe-best-practices/references/security.md +109 -0
  128. package/.kiro/skills/stripe-best-practices/references/treasury.md +16 -0
  129. package/.kiro/skills/stripe-projects/SKILL.md +139 -0
  130. package/.kiro/skills/upgrade-stripe/SKILL.md +185 -0
  131. package/.kode/skills/stripe-best-practices/SKILL.md +42 -0
  132. package/.kode/skills/stripe-best-practices/references/billing.md +36 -0
  133. package/.kode/skills/stripe-best-practices/references/connect.md +48 -0
  134. package/.kode/skills/stripe-best-practices/references/payments.md +79 -0
  135. package/.kode/skills/stripe-best-practices/references/security.md +109 -0
  136. package/.kode/skills/stripe-best-practices/references/treasury.md +16 -0
  137. package/.kode/skills/stripe-projects/SKILL.md +139 -0
  138. package/.kode/skills/upgrade-stripe/SKILL.md +185 -0
  139. package/.mcpjam/skills/stripe-best-practices/SKILL.md +42 -0
  140. package/.mcpjam/skills/stripe-best-practices/references/billing.md +36 -0
  141. package/.mcpjam/skills/stripe-best-practices/references/connect.md +48 -0
  142. package/.mcpjam/skills/stripe-best-practices/references/payments.md +79 -0
  143. package/.mcpjam/skills/stripe-best-practices/references/security.md +109 -0
  144. package/.mcpjam/skills/stripe-best-practices/references/treasury.md +16 -0
  145. package/.mcpjam/skills/stripe-projects/SKILL.md +139 -0
  146. package/.mcpjam/skills/upgrade-stripe/SKILL.md +185 -0
  147. package/.mux/skills/stripe-best-practices/SKILL.md +42 -0
  148. package/.mux/skills/stripe-best-practices/references/billing.md +36 -0
  149. package/.mux/skills/stripe-best-practices/references/connect.md +48 -0
  150. package/.mux/skills/stripe-best-practices/references/payments.md +79 -0
  151. package/.mux/skills/stripe-best-practices/references/security.md +109 -0
  152. package/.mux/skills/stripe-best-practices/references/treasury.md +16 -0
  153. package/.mux/skills/stripe-projects/SKILL.md +139 -0
  154. package/.mux/skills/upgrade-stripe/SKILL.md +185 -0
  155. package/.neovate/skills/stripe-best-practices/SKILL.md +42 -0
  156. package/.neovate/skills/stripe-best-practices/references/billing.md +36 -0
  157. package/.neovate/skills/stripe-best-practices/references/connect.md +48 -0
  158. package/.neovate/skills/stripe-best-practices/references/payments.md +79 -0
  159. package/.neovate/skills/stripe-best-practices/references/security.md +109 -0
  160. package/.neovate/skills/stripe-best-practices/references/treasury.md +16 -0
  161. package/.neovate/skills/stripe-projects/SKILL.md +139 -0
  162. package/.neovate/skills/upgrade-stripe/SKILL.md +185 -0
  163. package/.nixpacksignore +14 -0
  164. package/.openhands/skills/stripe-best-practices/SKILL.md +42 -0
  165. package/.openhands/skills/stripe-best-practices/references/billing.md +36 -0
  166. package/.openhands/skills/stripe-best-practices/references/connect.md +48 -0
  167. package/.openhands/skills/stripe-best-practices/references/payments.md +79 -0
  168. package/.openhands/skills/stripe-best-practices/references/security.md +109 -0
  169. package/.openhands/skills/stripe-best-practices/references/treasury.md +16 -0
  170. package/.openhands/skills/stripe-projects/SKILL.md +139 -0
  171. package/.openhands/skills/upgrade-stripe/SKILL.md +185 -0
  172. package/.pi/skills/stripe-best-practices/SKILL.md +42 -0
  173. package/.pi/skills/stripe-best-practices/references/billing.md +36 -0
  174. package/.pi/skills/stripe-best-practices/references/connect.md +48 -0
  175. package/.pi/skills/stripe-best-practices/references/payments.md +79 -0
  176. package/.pi/skills/stripe-best-practices/references/security.md +109 -0
  177. package/.pi/skills/stripe-best-practices/references/treasury.md +16 -0
  178. package/.pi/skills/stripe-projects/SKILL.md +139 -0
  179. package/.pi/skills/upgrade-stripe/SKILL.md +185 -0
  180. package/.pochi/skills/stripe-best-practices/SKILL.md +42 -0
  181. package/.pochi/skills/stripe-best-practices/references/billing.md +36 -0
  182. package/.pochi/skills/stripe-best-practices/references/connect.md +48 -0
  183. package/.pochi/skills/stripe-best-practices/references/payments.md +79 -0
  184. package/.pochi/skills/stripe-best-practices/references/security.md +109 -0
  185. package/.pochi/skills/stripe-best-practices/references/treasury.md +16 -0
  186. package/.pochi/skills/stripe-projects/SKILL.md +139 -0
  187. package/.pochi/skills/upgrade-stripe/SKILL.md +185 -0
  188. package/.qoder/skills/stripe-best-practices/SKILL.md +42 -0
  189. package/.qoder/skills/stripe-best-practices/references/billing.md +36 -0
  190. package/.qoder/skills/stripe-best-practices/references/connect.md +48 -0
  191. package/.qoder/skills/stripe-best-practices/references/payments.md +79 -0
  192. package/.qoder/skills/stripe-best-practices/references/security.md +109 -0
  193. package/.qoder/skills/stripe-best-practices/references/treasury.md +16 -0
  194. package/.qoder/skills/stripe-projects/SKILL.md +139 -0
  195. package/.qoder/skills/upgrade-stripe/SKILL.md +185 -0
  196. package/.qwen/skills/stripe-best-practices/SKILL.md +42 -0
  197. package/.qwen/skills/stripe-best-practices/references/billing.md +36 -0
  198. package/.qwen/skills/stripe-best-practices/references/connect.md +48 -0
  199. package/.qwen/skills/stripe-best-practices/references/payments.md +79 -0
  200. package/.qwen/skills/stripe-best-practices/references/security.md +109 -0
  201. package/.qwen/skills/stripe-best-practices/references/treasury.md +16 -0
  202. package/.qwen/skills/stripe-projects/SKILL.md +139 -0
  203. package/.qwen/skills/upgrade-stripe/SKILL.md +185 -0
  204. package/.roo/skills/stripe-best-practices/SKILL.md +42 -0
  205. package/.roo/skills/stripe-best-practices/references/billing.md +36 -0
  206. package/.roo/skills/stripe-best-practices/references/connect.md +48 -0
  207. package/.roo/skills/stripe-best-practices/references/payments.md +79 -0
  208. package/.roo/skills/stripe-best-practices/references/security.md +109 -0
  209. package/.roo/skills/stripe-best-practices/references/treasury.md +16 -0
  210. package/.roo/skills/stripe-projects/SKILL.md +139 -0
  211. package/.roo/skills/upgrade-stripe/SKILL.md +185 -0
  212. package/.trae/skills/stripe-best-practices/SKILL.md +42 -0
  213. package/.trae/skills/stripe-best-practices/references/billing.md +36 -0
  214. package/.trae/skills/stripe-best-practices/references/connect.md +48 -0
  215. package/.trae/skills/stripe-best-practices/references/payments.md +79 -0
  216. package/.trae/skills/stripe-best-practices/references/security.md +109 -0
  217. package/.trae/skills/stripe-best-practices/references/treasury.md +16 -0
  218. package/.trae/skills/stripe-projects/SKILL.md +139 -0
  219. package/.trae/skills/upgrade-stripe/SKILL.md +185 -0
  220. package/.vibe/skills/stripe-best-practices/SKILL.md +42 -0
  221. package/.vibe/skills/stripe-best-practices/references/billing.md +36 -0
  222. package/.vibe/skills/stripe-best-practices/references/connect.md +48 -0
  223. package/.vibe/skills/stripe-best-practices/references/payments.md +79 -0
  224. package/.vibe/skills/stripe-best-practices/references/security.md +109 -0
  225. package/.vibe/skills/stripe-best-practices/references/treasury.md +16 -0
  226. package/.vibe/skills/stripe-projects/SKILL.md +139 -0
  227. package/.vibe/skills/upgrade-stripe/SKILL.md +185 -0
  228. package/.windsurf/skills/stripe-best-practices/SKILL.md +42 -0
  229. package/.windsurf/skills/stripe-best-practices/references/billing.md +36 -0
  230. package/.windsurf/skills/stripe-best-practices/references/connect.md +48 -0
  231. package/.windsurf/skills/stripe-best-practices/references/payments.md +79 -0
  232. package/.windsurf/skills/stripe-best-practices/references/security.md +109 -0
  233. package/.windsurf/skills/stripe-best-practices/references/treasury.md +16 -0
  234. package/.windsurf/skills/stripe-projects/SKILL.md +139 -0
  235. package/.windsurf/skills/upgrade-stripe/SKILL.md +185 -0
  236. package/.zencoder/skills/stripe-best-practices/SKILL.md +42 -0
  237. package/.zencoder/skills/stripe-best-practices/references/billing.md +36 -0
  238. package/.zencoder/skills/stripe-best-practices/references/connect.md +48 -0
  239. package/.zencoder/skills/stripe-best-practices/references/payments.md +79 -0
  240. package/.zencoder/skills/stripe-best-practices/references/security.md +109 -0
  241. package/.zencoder/skills/stripe-best-practices/references/treasury.md +16 -0
  242. package/.zencoder/skills/stripe-projects/SKILL.md +139 -0
  243. package/.zencoder/skills/upgrade-stripe/SKILL.md +185 -0
  244. package/AUDIT.md +95 -0
  245. package/BLOCKERS.md +0 -0
  246. package/COOLIFY.md +51 -0
  247. package/MCP_SETUP.md +23 -0
  248. package/PRODUCTION_CHECKLIST.md +246 -0
  249. package/README.md +47 -0
  250. package/ROADMAP.md +91 -0
  251. package/docs/superpowers/plans/2026-05-11-availsync-frontend-sales-flow.md +2445 -0
  252. package/frontend/.env.example +2 -0
  253. package/frontend/app/admin/layout.tsx +13 -0
  254. package/frontend/app/admin/page.tsx +747 -0
  255. package/frontend/app/app/activity/page.tsx +257 -0
  256. package/frontend/app/app/agents/[agentId]/page.tsx +21 -0
  257. package/frontend/app/app/agents/page.tsx +1155 -0
  258. package/frontend/app/app/audit/page.tsx +225 -0
  259. package/frontend/app/app/availability/page.tsx +840 -0
  260. package/frontend/app/app/holds/page.tsx +262 -0
  261. package/frontend/app/app/layout.tsx +19 -0
  262. package/frontend/app/app/onboarding/page.tsx +10 -0
  263. package/frontend/app/app/onboarding/verify/page.tsx +309 -0
  264. package/frontend/app/app/page.tsx +508 -0
  265. package/frontend/app/app/settings/page.tsx +399 -0
  266. package/frontend/app/app/work/page.tsx +426 -0
  267. package/frontend/app/changelog/page.tsx +93 -0
  268. package/frontend/app/checkout/page.tsx +25 -0
  269. package/frontend/app/docs/api/page.tsx +157 -0
  270. package/frontend/app/docs/page.tsx +296 -0
  271. package/frontend/app/docs/pilot/page.tsx +127 -0
  272. package/frontend/app/docs/quickstart/page.tsx +318 -0
  273. package/frontend/app/docs/reliability/page.tsx +78 -0
  274. package/frontend/app/docs/sdk/node/page.tsx +166 -0
  275. package/frontend/app/globals.css +57 -0
  276. package/frontend/app/icon.png +0 -0
  277. package/frontend/app/layout.tsx +87 -0
  278. package/frontend/app/login/page.tsx +14 -0
  279. package/frontend/app/page.tsx +47 -0
  280. package/frontend/app/pricing/page.tsx +66 -0
  281. package/frontend/app/privacy/page.tsx +52 -0
  282. package/frontend/app/robots.ts +26 -0
  283. package/frontend/app/security/page.tsx +74 -0
  284. package/frontend/app/signup/page.tsx +14 -0
  285. package/frontend/app/sitemap.ts +14 -0
  286. package/frontend/app/terms/page.tsx +51 -0
  287. package/frontend/components/brand/AvailsyncLogo.tsx +56 -0
  288. package/frontend/components/checkout/CheckoutClient.tsx +100 -0
  289. package/frontend/components/dashboard/AgentForm.tsx +59 -0
  290. package/frontend/components/dashboard/AppShell.tsx +291 -0
  291. package/frontend/components/dashboard/AvailabilityChecker.tsx +117 -0
  292. package/frontend/components/dashboard/AvailabilityWindowForm.tsx +40 -0
  293. package/frontend/components/dashboard/HoldForm.tsx +133 -0
  294. package/frontend/components/dashboard/MetricCard.tsx +10 -0
  295. package/frontend/components/login/LoginForm.tsx +95 -0
  296. package/frontend/components/marketing/AgentCoordinationStory.tsx +1530 -0
  297. package/frontend/components/marketing/Faq.tsx +41 -0
  298. package/frontend/components/marketing/Hero.tsx +73 -0
  299. package/frontend/components/marketing/HowItWorks.tsx +28 -0
  300. package/frontend/components/marketing/ObserveModeTeaser.tsx +41 -0
  301. package/frontend/components/marketing/PricingTeaser.tsx +23 -0
  302. package/frontend/components/marketing/ProblemSolution.tsx +36 -0
  303. package/frontend/components/marketing/SiteFooter.tsx +59 -0
  304. package/frontend/components/marketing/SiteHeader.tsx +45 -0
  305. package/frontend/components/marketing/UseCases.tsx +27 -0
  306. package/frontend/components/onboarding/OnboardingClient.tsx +278 -0
  307. package/frontend/components/pricing/PricingCards.tsx +65 -0
  308. package/frontend/components/privacy/CookieConsent.tsx +230 -0
  309. package/frontend/components/privacy/CookieSettingsButton.tsx +15 -0
  310. package/frontend/components/seo/JsonLd.tsx +10 -0
  311. package/frontend/components/signup/SignupForm.tsx +55 -0
  312. package/frontend/components/ui/Badge.tsx +23 -0
  313. package/frontend/components/ui/Button.tsx +37 -0
  314. package/frontend/components/ui/Card.tsx +11 -0
  315. package/frontend/components/ui/ConfirmDialog.tsx +77 -0
  316. package/frontend/components/ui/EmptyState.tsx +24 -0
  317. package/frontend/components/ui/Input.tsx +14 -0
  318. package/frontend/components/ui/KeyDisplay.tsx +49 -0
  319. package/frontend/components/ui/Select.tsx +14 -0
  320. package/frontend/components/ui/Skeleton.tsx +24 -0
  321. package/frontend/components/ui/Tabs.tsx +19 -0
  322. package/frontend/components/ui/Textarea.tsx +14 -0
  323. package/frontend/components/ui/Toast.tsx +78 -0
  324. package/frontend/components/waitlist/WaitlistDialog.tsx +128 -0
  325. package/frontend/lib/api.ts +1282 -0
  326. package/frontend/lib/billing.ts +6 -0
  327. package/frontend/lib/cookieConsent.ts +113 -0
  328. package/frontend/lib/format.ts +16 -0
  329. package/frontend/lib/plans.ts +62 -0
  330. package/frontend/lib/schemas.ts +108 -0
  331. package/frontend/lib/seo.ts +376 -0
  332. package/frontend/lib/setupGuides.ts +630 -0
  333. package/frontend/lib/storage.ts +30 -0
  334. package/frontend/next-env.d.ts +6 -0
  335. package/frontend/next.config.mjs +13 -0
  336. package/frontend/package-lock.json +14409 -0
  337. package/frontend/package.json +41 -0
  338. package/frontend/playwright.config.ts +20 -0
  339. package/frontend/postcss.config.mjs +8 -0
  340. package/frontend/public/.gitkeep +0 -0
  341. package/frontend/public/brand/availsync-logo-board.png +0 -0
  342. package/frontend/public/brand/availsync-logo-dark.png +0 -0
  343. package/frontend/public/brand/availsync-mark-dark.png +0 -0
  344. package/frontend/public/brand/availsync-wordmark-dark.png +0 -0
  345. package/frontend/public/marketing/hero-agent-coordination.png +0 -0
  346. package/frontend/tailwind.config.ts +53 -0
  347. package/frontend/tests/smoke.spec.ts +89 -0
  348. package/frontend/tsconfig.json +23 -0
  349. package/jest.config.js +7 -0
  350. package/nixpacks.toml +11 -0
  351. package/package.json +53 -0
  352. package/packages/mcp/LICENSE +21 -0
  353. package/packages/mcp/README.md +60 -0
  354. package/packages/mcp/jest.config.cjs +8 -0
  355. package/packages/mcp/package.json +54 -0
  356. package/packages/mcp/src/helpers.ts +38 -0
  357. package/packages/mcp/src/index.test.ts +60 -0
  358. package/packages/mcp/src/index.ts +387 -0
  359. package/packages/mcp/tsconfig.json +20 -0
  360. package/packages/mcp/tsconfig.test.json +12 -0
  361. package/packages/node/LICENSE +21 -0
  362. package/packages/node/README.md +120 -0
  363. package/packages/node/jest.config.cjs +8 -0
  364. package/packages/node/package.json +46 -0
  365. package/packages/node/src/index.test.ts +360 -0
  366. package/packages/node/src/index.ts +402 -0
  367. package/packages/node/tsconfig.json +20 -0
  368. package/packages/node/tsconfig.test.json +12 -0
  369. package/plan.md +923 -0
  370. package/skills/stripe-best-practices/SKILL.md +42 -0
  371. package/skills/stripe-best-practices/references/billing.md +36 -0
  372. package/skills/stripe-best-practices/references/connect.md +48 -0
  373. package/skills/stripe-best-practices/references/payments.md +79 -0
  374. package/skills/stripe-best-practices/references/security.md +109 -0
  375. package/skills/stripe-best-practices/references/treasury.md +16 -0
  376. package/skills/stripe-projects/SKILL.md +139 -0
  377. package/skills/upgrade-stripe/SKILL.md +185 -0
  378. package/skills-lock.json +20 -0
  379. package/src/core/availability.ts +178 -0
  380. package/src/core/conflict.ts +209 -0
  381. package/src/core/work.ts +490 -0
  382. package/src/db/client.ts +17 -0
  383. package/src/db/migrations/001_init.sql +88 -0
  384. package/src/db/migrations/002_stripe.sql +2 -0
  385. package/src/db/migrations/003_workspace_auth.sql +19 -0
  386. package/src/db/migrations/004_agent_mcp_status.sql +2 -0
  387. package/src/db/migrations/005_hold_event_actor.sql +4 -0
  388. package/src/db/migrations/006_agent_activity.sql +35 -0
  389. package/src/db/migrations/007_work_coordination.sql +60 -0
  390. package/src/db/migrations/008_work_claim_leases.sql +20 -0
  391. package/src/db/migrations/009_billing_subscription_state.sql +23 -0
  392. package/src/db/migrations/010_agent_api_key_prefix.sql +10 -0
  393. package/src/db/migrations/011_org_verified_and_work_event_retention.sql +11 -0
  394. package/src/db/migrations/012_agent_enforcement_mode.sql +12 -0
  395. package/src/db/migrations/013_support_tickets.sql +21 -0
  396. package/src/db/migrations/014_paid_plan_waitlist.sql +23 -0
  397. package/src/db/migrations/015_agent_last_seen.sql +2 -0
  398. package/src/db/migrations.ts +164 -0
  399. package/src/db/run-migrations.ts +13 -0
  400. package/src/index.ts +183 -0
  401. package/src/lib/activity.ts +137 -0
  402. package/src/lib/apiKeys.ts +32 -0
  403. package/src/lib/appInfo.ts +26 -0
  404. package/src/lib/billingConfig.ts +3 -0
  405. package/src/lib/env.ts +75 -0
  406. package/src/lib/logger.ts +8 -0
  407. package/src/lib/plans.ts +204 -0
  408. package/src/mcp/server.js +5 -0
  409. package/src/mcp/server.ts +350 -0
  410. package/src/middleware/auth.ts +342 -0
  411. package/src/middleware/requestId.ts +16 -0
  412. package/src/routes/account.ts +168 -0
  413. package/src/routes/activity.ts +126 -0
  414. package/src/routes/admin.ts +514 -0
  415. package/src/routes/audit.ts +68 -0
  416. package/src/routes/auth.ts +203 -0
  417. package/src/routes/availability.ts +325 -0
  418. package/src/routes/billing.ts +406 -0
  419. package/src/routes/conflicts.ts +131 -0
  420. package/src/routes/holds.ts +437 -0
  421. package/src/routes/mcp.ts +57 -0
  422. package/src/routes/metrics.ts +39 -0
  423. package/src/routes/onboarding.ts +273 -0
  424. package/src/routes/orgs.ts +981 -0
  425. package/src/routes/preferences.ts +132 -0
  426. package/src/routes/session.ts +16 -0
  427. package/src/routes/support.ts +77 -0
  428. package/src/routes/value.ts +186 -0
  429. package/src/routes/waitlist.ts +63 -0
  430. package/src/routes/work.ts +1578 -0
  431. package/src/server.ts +36 -0
  432. package/src/types/index.ts +109 -0
  433. package/tests/integration/activity.route.test.ts +103 -0
  434. package/tests/integration/admin.route.test.ts +143 -0
  435. package/tests/integration/agent-keys.route.test.ts +237 -0
  436. package/tests/integration/availability.route.test.ts +125 -0
  437. package/tests/integration/billing.route.test.ts +393 -0
  438. package/tests/integration/conflicts.route.test.ts +131 -0
  439. package/tests/integration/flows.test.ts +154 -0
  440. package/tests/integration/helpers.ts +134 -0
  441. package/tests/integration/holds.route.test.ts +185 -0
  442. package/tests/integration/metrics.route.test.ts +100 -0
  443. package/tests/integration/onboarding.verify.route.test.ts +163 -0
  444. package/tests/integration/preferences.route.test.ts +53 -0
  445. package/tests/integration/session.route.test.ts +97 -0
  446. package/tests/integration/system.route.test.ts +92 -0
  447. package/tests/integration/value.route.test.ts +235 -0
  448. package/tests/integration/work.route.test.ts +745 -0
  449. package/tests/setup.ts +4 -0
  450. package/tests/smoke.sh +62 -0
  451. package/tests/unit/auth.test.ts +114 -0
  452. package/tests/unit/availability.test.ts +149 -0
  453. package/tests/unit/conflict.test.ts +118 -0
  454. package/tests/unit/env.test.ts +69 -0
  455. package/tests/unit/migrations.test.ts +135 -0
  456. package/tests/unit/request-id.test.ts +37 -0
  457. package/tmp-mobile-agents.png +0 -0
  458. package/tmp-next-mobile.err.log +10 -0
  459. package/tmp-next-mobile.log +5 -0
  460. package/tsconfig.json +16 -0
@@ -0,0 +1,1155 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState, useCallback, useRef } from 'react';
4
+ import {
5
+ listAgents,
6
+ createAgent,
7
+ updateAgent,
8
+ deleteAgent,
9
+ getValueHealth,
10
+ rotateAgentKey,
11
+ runAgentSetupTest,
12
+ testAgentConnection,
13
+ type SetupTestRun,
14
+ type ValueHealth,
15
+ } from '@/lib/api';
16
+ import { loadSession, type StoredSession } from '@/lib/storage';
17
+ import { Badge } from '@/components/ui/Badge';
18
+ import { EmptyState } from '@/components/ui/EmptyState';
19
+ import { SkeletonRow } from '@/components/ui/Skeleton';
20
+ import { useToast } from '@/components/ui/Toast';
21
+ import { Check, KeyRound, Pencil, Plug, Trash2, Users, Plus, Copy, X, Eye, EyeOff } from 'lucide-react';
22
+ import type { Agent } from '@/lib/schemas';
23
+ import {
24
+ automationGuardrailSnippets,
25
+ claudeCursorSnippet,
26
+ codexMultiAgentMcpSnippet,
27
+ codexMcpSetupSnippet,
28
+ codexPromptSnippet,
29
+ mcpEnvSnippet,
30
+ nodeSdkSnippets,
31
+ openClawSetupSnippet,
32
+ openClawSkillSnippet,
33
+ schedulingSnippets,
34
+ serverCronSnippet,
35
+ workGuardrailSnippets,
36
+ type SetupSnippet,
37
+ } from '@/lib/setupGuides';
38
+ import { billingUpgradesEnabled } from '@/lib/billing';
39
+
40
+ const typeLabels: Record<string, string> = {
41
+ external_meeting: 'External',
42
+ internal: 'Internal',
43
+ focus: 'Focus',
44
+ generic: 'Generic',
45
+ };
46
+
47
+ const typeBadgeVariant: Record<string, 'accent' | 'success' | 'warning' | 'neutral'> = {
48
+ external_meeting: 'accent',
49
+ internal: 'success',
50
+ focus: 'warning',
51
+ generic: 'neutral',
52
+ };
53
+
54
+ const healthBadge: Record<ValueHealth['agent_health'][number]['status'], { label: string; variant: 'success' | 'warning' | 'neutral' | 'error' }> = {
55
+ healthy: { label: 'Healthy', variant: 'success' },
56
+ quiet: { label: 'Quiet', variant: 'warning' },
57
+ never_connected: { label: 'Never connected', variant: 'neutral' },
58
+ error: { label: 'Error', variant: 'error' },
59
+ };
60
+
61
+ const enforcementBadge: Record<Agent['enforcement_mode'], { label: string; variant: 'success' | 'neutral' }> = {
62
+ enforce: { label: 'Enforce', variant: 'success' },
63
+ observe: { label: 'Observe', variant: 'neutral' },
64
+ };
65
+
66
+ function formatAgentTimestamp(value: string | null | undefined) {
67
+ if (!value) return '-';
68
+ const date = new Date(value);
69
+ if (Number.isNaN(date.getTime())) return '-';
70
+ const pad = (number: number) => String(number).padStart(2, '0');
71
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
72
+ }
73
+
74
+ function mcpStatus(agent: Agent): { label: string; variant: 'success' | 'warning' | 'neutral'; detail: string } {
75
+ if (!agent.mcp_last_seen_at) {
76
+ return { label: 'Offline', variant: 'neutral', detail: 'No heartbeat yet' };
77
+ }
78
+
79
+ const diffMs = Date.now() - new Date(agent.mcp_last_seen_at).getTime();
80
+ if (diffMs < 2 * 60 * 1000) {
81
+ return { label: 'Online', variant: 'success', detail: 'Seen just now' };
82
+ }
83
+ if (diffMs < 15 * 60 * 1000) {
84
+ return { label: 'Recent', variant: 'warning', detail: `${Math.floor(diffMs / 60000)}m ago` };
85
+ }
86
+
87
+ return { label: 'Offline', variant: 'neutral', detail: `${Math.floor(diffMs / 60000)}m ago` };
88
+ }
89
+
90
+ export default function AgentsPage() {
91
+ const { toast } = useToast();
92
+ const [agents, setAgents] = useState<Agent[]>([]);
93
+ const [valueHealth, setValueHealth] = useState<ValueHealth | null>(null);
94
+ const [loading, setLoading] = useState(true);
95
+ const [session, setSession] = useState<StoredSession | null>(null);
96
+ const [showNewPanel, setShowNewPanel] = useState(false);
97
+ const [newAgentKey, setNewAgentKey] = useState<string | null>(null);
98
+ const [newAgentId, setNewAgentId] = useState<string | null>(null);
99
+ const [newAgentKeySaved, setNewAgentKeySaved] = useState(false);
100
+ const [form, setForm] = useState({ name: '', agent_type: 'generic', priority: 0 });
101
+ const [editingAgentId, setEditingAgentId] = useState<string | null>(null);
102
+ const [editForm, setEditForm] = useState({ name: '', agent_type: 'generic', priority: 0, enforcement_mode: 'enforce' as Agent['enforcement_mode'] });
103
+ const [creating, setCreating] = useState(false);
104
+ const [connectAgent, setConnectAgent] = useState<Agent | null>(null);
105
+ const [connection, setConnection] = useState<Awaited<ReturnType<typeof testAgentConnection>> | null>(null);
106
+ const [connectionLoading, setConnectionLoading] = useState(false);
107
+ const [rotatedKey, setRotatedKey] = useState<string | null>(null);
108
+ const [rotating, setRotating] = useState(false);
109
+ const [planLimit, setPlanLimit] = useState<{ message: string; upgrade_plan: string | null } | null>(null);
110
+ const [connectTab, setConnectTab] = useState<'status' | 'sdk' | 'mcp' | 'automation' | 'openclaw' | 'rest' | 'codex' | 'activity'>('status');
111
+ const [connectResource, setConnectResource] = useState({ resource_type: 'repo' as 'project' | 'repo', resource_key: 'owner/repo', duration_minutes: 45 });
112
+ const [setupTest, setSetupTest] = useState<SetupTestRun | null>(null);
113
+ const [setupTestRunning, setSetupTestRunning] = useState(false);
114
+ const autoOpenedAgent = useRef<string | null>(null);
115
+
116
+ const loadAgents = useCallback(() => {
117
+ const s = loadSession();
118
+ setSession(s);
119
+ if (s) {
120
+ Promise.all([
121
+ listAgents(s.orgId),
122
+ getValueHealth().catch(() => null),
123
+ ])
124
+ .then(([data, health]) => {
125
+ setAgents(data);
126
+ setValueHealth(health);
127
+ setLoading(false);
128
+ })
129
+ .catch(() => setLoading(false));
130
+ } else {
131
+ setLoading(false);
132
+ }
133
+ }, []);
134
+
135
+ useEffect(() => {
136
+ loadAgents();
137
+ }, [loadAgents]);
138
+
139
+ useEffect(() => {
140
+ if (typeof window === 'undefined' || !session || agents.length === 0) return;
141
+ const agentId = new URLSearchParams(window.location.search).get('connect');
142
+ if (!agentId || autoOpenedAgent.current === agentId) return;
143
+ const agent = agents.find((row) => row.id === agentId);
144
+ if (!agent) return;
145
+ autoOpenedAgent.current = agentId;
146
+ openConnect(agent);
147
+ // eslint-disable-next-line react-hooks/exhaustive-deps
148
+ }, [agents, session]);
149
+
150
+ // Keyboard shortcut: N to open new agent panel
151
+ useEffect(() => {
152
+ function handleKey(e: KeyboardEvent) {
153
+ if (e.key === 'n' && !showNewPanel && !(e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement)) {
154
+ e.preventDefault();
155
+ setShowNewPanel(true);
156
+ }
157
+ if (e.key === 'Escape') {
158
+ setShowNewPanel(false);
159
+ setNewAgentKey(null);
160
+ setNewAgentId(null);
161
+ setNewAgentKeySaved(false);
162
+ }
163
+ }
164
+ window.addEventListener('keydown', handleKey);
165
+ return () => window.removeEventListener('keydown', handleKey);
166
+ }, [showNewPanel]);
167
+
168
+ const handleCreate = async () => {
169
+ if (!session || !form.name.trim()) return;
170
+ setCreating(true);
171
+ try {
172
+ setPlanLimit(null);
173
+ const result = await createAgent({
174
+ orgId: session.orgId,
175
+ name: form.name.trim(),
176
+ agent_type: form.agent_type as Agent['agent_type'],
177
+ priority: form.priority,
178
+ });
179
+ setNewAgentKey(result.apiKey);
180
+ setNewAgentId(result.agent.id);
181
+ setNewAgentKeySaved(false);
182
+ setForm({ name: '', agent_type: 'generic', priority: 0 });
183
+ loadAgents();
184
+ toast('Agent created', 'success');
185
+ } catch (err) {
186
+ const body = (err as { body?: { error?: string; message?: string; upgrade_plan?: string | null } }).body;
187
+ if (body?.error === 'plan_limit_reached') {
188
+ setPlanLimit({ message: body.message || (err as Error).message, upgrade_plan: body.upgrade_plan ?? null });
189
+ }
190
+ toast((err as Error).message, 'error');
191
+ } finally {
192
+ setCreating(false);
193
+ }
194
+ };
195
+
196
+ const startEdit = (agent: Agent) => {
197
+ setEditingAgentId(agent.id);
198
+ setEditForm({
199
+ name: agent.name,
200
+ agent_type: agent.agent_type,
201
+ priority: agent.priority,
202
+ enforcement_mode: agent.enforcement_mode,
203
+ });
204
+ };
205
+
206
+ const handleUpdate = async () => {
207
+ if (!session || !editingAgentId || !editForm.name.trim()) return;
208
+ try {
209
+ await updateAgent({
210
+ orgId: session.orgId,
211
+ agentId: editingAgentId,
212
+ name: editForm.name.trim(),
213
+ agent_type: editForm.agent_type as Agent['agent_type'],
214
+ priority: editForm.priority,
215
+ enforcement_mode: editForm.enforcement_mode,
216
+ });
217
+ toast('Agent updated', 'success');
218
+ setEditingAgentId(null);
219
+ loadAgents();
220
+ } catch (err) {
221
+ toast((err as Error).message, 'error');
222
+ }
223
+ };
224
+
225
+ const handleDelete = async (agent: Agent) => {
226
+ if (!session) return;
227
+ if (!window.confirm(`Delete ${agent.name}? Holds and windows for this agent will also be removed.`)) return;
228
+ try {
229
+ await deleteAgent(session.orgId, agent.id);
230
+ toast('Agent deleted', 'success');
231
+ loadAgents();
232
+ } catch (err) {
233
+ toast((err as Error).message, 'error');
234
+ }
235
+ };
236
+
237
+ const openConnect = async (agent: Agent) => {
238
+ if (!session) return;
239
+ setConnectAgent(agent);
240
+ setConnectTab('status');
241
+ setConnection(null);
242
+ setRotatedKey(null);
243
+ setSetupTest(null);
244
+ setConnectionLoading(true);
245
+ try {
246
+ setConnection(await testAgentConnection(session.orgId, agent.id));
247
+ } catch (err) {
248
+ toast((err as Error).message, 'error');
249
+ } finally {
250
+ setConnectionLoading(false);
251
+ }
252
+ };
253
+
254
+ const handleRotateKey = async () => {
255
+ if (!session || !connectAgent) return;
256
+ if (!window.confirm('Rotate this API key? The old key will stop working immediately.')) return;
257
+ setRotating(true);
258
+ try {
259
+ const result = await rotateAgentKey(session.orgId, connectAgent.id);
260
+ setRotatedKey(result.apiKey);
261
+ toast('API key rotated', 'success');
262
+ loadAgents();
263
+ } catch (err) {
264
+ toast((err as Error).message, 'error');
265
+ } finally {
266
+ setRotating(false);
267
+ }
268
+ };
269
+
270
+ const handleRunSetupTest = async () => {
271
+ if (!session || !connectAgent) return;
272
+ setSetupTestRunning(true);
273
+ try {
274
+ const result = await runAgentSetupTest(session.orgId, connectAgent.id);
275
+ setSetupTest(result);
276
+ setConnection(await testAgentConnection(session.orgId, connectAgent.id));
277
+ setValueHealth(await getValueHealth().catch(() => null));
278
+ toast(result.status === 'success' ? 'Setup test passed' : `Setup test ${result.status}`, result.status === 'error' ? 'error' : 'success');
279
+ } catch (err) {
280
+ const body = (err as { body?: SetupTestRun }).body;
281
+ if (body?.steps) setSetupTest(body);
282
+ toast((err as Error).message, 'error');
283
+ } finally {
284
+ setSetupTestRunning(false);
285
+ }
286
+ };
287
+
288
+ const copyText = (text: string, label = 'Copied') => {
289
+ navigator.clipboard.writeText(text);
290
+ toast(label, 'success');
291
+ };
292
+
293
+ const appUrl = typeof window !== 'undefined' ? window.location.origin : 'https://availsync.dev';
294
+ const guideInput = {
295
+ appUrl,
296
+ agentId: connectAgent?.id,
297
+ resourceType: connectResource.resource_type,
298
+ resourceKey: connectResource.resource_key || 'owner/repo',
299
+ durationMinutes: connectResource.duration_minutes || 45,
300
+ };
301
+ const mcpSnippet = mcpEnvSnippet(guideInput);
302
+ const sdkSnippets = nodeSdkSnippets(guideInput);
303
+ const cursorSnippet = claudeCursorSnippet(guideInput);
304
+ const codexMcpSnippet = codexMcpSetupSnippet(guideInput);
305
+ const codexMultiAgentSnippet = codexMultiAgentMcpSnippet(guideInput);
306
+ const automationSnippets = automationGuardrailSnippets(guideInput);
307
+ const openClawSkill = openClawSkillSnippet(guideInput);
308
+ const openClawSetup = openClawSetupSnippet(guideInput);
309
+ const workSnippets = workGuardrailSnippets(guideInput);
310
+ const scheduleSnippets = schedulingSnippets(guideInput);
311
+ const healthByAgent = new Map(valueHealth?.agent_health.map((health) => [health.agent_id, health]) ?? []);
312
+ const connectAgentHealth = connectAgent ? healthByAgent.get(connectAgent.id) : undefined;
313
+ const codexSnippet = codexPromptSnippet(guideInput);
314
+ const cronSnippet = serverCronSnippet(guideInput);
315
+ const readinessItems = connection
316
+ ? [
317
+ ['Agent created', connection.setup_readiness.agent_created],
318
+ ['API key copied or rotated', Boolean(rotatedKey || connection.last_successful_api_call || connection.last_seen_at || connection.mcp_last_seen_at)],
319
+ ['MCP heartbeat seen', connection.setup_readiness.mcp_heartbeat_seen],
320
+ ['First availability check', connection.setup_readiness.first_availability_check],
321
+ ['First conflict preview', connection.setup_readiness.first_conflict_preview],
322
+ ['First work check', connection.setup_readiness.first_work_check],
323
+ ['First work claim', connection.setup_readiness.first_work_claim],
324
+ ['First work extend', connection.setup_readiness.first_work_extend],
325
+ ['First work release', connection.setup_readiness.first_work_release],
326
+ ] as Array<[string, boolean]>
327
+ : [];
328
+ const missingReadinessItems = readinessItems.filter(([, value]) => !value).map(([label]) => label);
329
+
330
+ return (
331
+ <div className="w-full max-w-none p-4 sm:p-6">
332
+ <div className="mb-6 flex flex-wrap items-center justify-between gap-3">
333
+ <h1 className="text-title font-semibold text-text-primary">Agents</h1>
334
+ <button
335
+ onClick={() => setShowNewPanel(true)}
336
+ className="flex items-center gap-1.5 rounded bg-accent text-white px-3 py-1.5 text-body font-medium hover:bg-accent-hover transition-colors active:scale-[0.98]"
337
+ >
338
+ <Plus className="h-3.5 w-3.5" />
339
+ New agent
340
+ </button>
341
+ </div>
342
+
343
+ {planLimit && (
344
+ <div className="mb-4 rounded border border-warning/40 bg-warning/10 p-3 text-body text-warning">
345
+ <div>{planLimit.message}</div>
346
+ {planLimit.upgrade_plan && (
347
+ <a
348
+ className="mt-2 inline-block text-accent hover:text-accent-hover"
349
+ href={billingUpgradesEnabled() ? `/checkout?plan=${planLimit.upgrade_plan}` : '/app/settings?tab=billing'}
350
+ >
351
+ {billingUpgradesEnabled() ? `Upgrade to ${planLimit.upgrade_plan}` : 'Join paid plan waitlist'}
352
+ </a>
353
+ )}
354
+ </div>
355
+ )}
356
+
357
+ {/* New agent slide-over */}
358
+ {showNewPanel && (
359
+ <div className="fixed inset-0 z-50 flex justify-end">
360
+ <div className="absolute inset-0 bg-black/50" onClick={() => { setShowNewPanel(false); setNewAgentKey(null); setNewAgentId(null); setNewAgentKeySaved(false); }} />
361
+ <div className="relative w-full max-w-sm overflow-y-auto border-l border-border bg-surface p-4 sm:p-6">
362
+ <div className="flex items-center justify-between mb-6">
363
+ <h2 className="text-heading font-medium text-text-primary">New agent</h2>
364
+ <button onClick={() => { setShowNewPanel(false); setNewAgentKey(null); setNewAgentId(null); setNewAgentKeySaved(false); }} className="text-text-tertiary hover:text-text-secondary">
365
+ <X className="h-4 w-4" />
366
+ </button>
367
+ </div>
368
+
369
+ {newAgentKey ? (
370
+ <div>
371
+ <OneTimeKeyPanel apiKey={newAgentKey} onCopy={copyText} saved={newAgentKeySaved} onSavedChange={setNewAgentKeySaved} />
372
+ {newAgentId && (
373
+ <div className="rounded border border-border bg-bg p-4 mb-4">
374
+ <label className="block text-label uppercase text-text-tertiary mb-1">Agent ID</label>
375
+ <div className="flex items-center gap-2">
376
+ <span className="font-mono text-[12px] text-text-secondary break-all">{newAgentId}</span>
377
+ <button
378
+ onClick={() => {
379
+ navigator.clipboard.writeText(newAgentId);
380
+ toast('Agent ID copied', 'success');
381
+ }}
382
+ className="text-text-tertiary hover:text-text-secondary"
383
+ title="Copy agent ID"
384
+ >
385
+ <Copy className="h-3.5 w-3.5" />
386
+ </button>
387
+ </div>
388
+ </div>
389
+ )}
390
+ <button
391
+ onClick={() => { setShowNewPanel(false); setNewAgentKey(null); setNewAgentId(null); setNewAgentKeySaved(false); }}
392
+ disabled={!newAgentKeySaved}
393
+ className="w-full rounded bg-surface-raised text-text-secondary py-2 text-body hover:bg-border transition-colors disabled:cursor-not-allowed disabled:opacity-40"
394
+ >
395
+ Done
396
+ </button>
397
+ </div>
398
+ ) : (
399
+ <div className="space-y-4">
400
+ <div>
401
+ <label className="block text-label uppercase text-text-tertiary mb-1">Name</label>
402
+ <input
403
+ value={form.name}
404
+ onChange={(e) => setForm({ ...form, name: e.target.value })}
405
+ className="w-full rounded bg-bg border border-border px-3 py-2 text-body text-text-primary outline-none focus:border-border-focus"
406
+ placeholder="e.g. deploy_agent"
407
+ autoFocus
408
+ />
409
+ </div>
410
+ <div>
411
+ <label className="block text-label uppercase text-text-tertiary mb-1">Type</label>
412
+ <select
413
+ value={form.agent_type}
414
+ onChange={(e) => setForm({ ...form, agent_type: e.target.value })}
415
+ className="w-full rounded bg-bg border border-border px-3 py-2 text-body text-text-primary outline-none focus:border-border-focus"
416
+ >
417
+ <option value="external_meeting">External meeting</option>
418
+ <option value="internal">Internal</option>
419
+ <option value="focus">Focus</option>
420
+ <option value="generic">Coding / generic</option>
421
+ </select>
422
+ </div>
423
+ <div>
424
+ <label className="block text-label uppercase text-text-tertiary mb-1">Priority</label>
425
+ <input
426
+ type="number"
427
+ value={form.priority}
428
+ onChange={(e) => setForm({ ...form, priority: Number(e.target.value) })}
429
+ className="w-full rounded bg-bg border border-border px-3 py-2 text-body text-text-primary outline-none focus:border-border-focus"
430
+ />
431
+ </div>
432
+ <button
433
+ onClick={handleCreate}
434
+ disabled={creating || !form.name.trim()}
435
+ className="w-full rounded bg-accent text-white py-2 text-body font-medium hover:bg-accent-hover transition-colors disabled:opacity-40 active:scale-[0.98]"
436
+ >
437
+ {creating ? 'Creating...' : 'Create agent'}
438
+ </button>
439
+ </div>
440
+ )}
441
+ </div>
442
+ </div>
443
+ )}
444
+
445
+ {connectAgent && (
446
+ <div className="fixed inset-0 z-50 flex justify-end">
447
+ <div className="absolute inset-0 bg-black/50" onClick={() => setConnectAgent(null)} />
448
+ <div className="relative w-full max-w-2xl overflow-y-auto border-l border-border bg-surface p-4 sm:p-6">
449
+ <div className="mb-6 flex items-start justify-between gap-4">
450
+ <div>
451
+ <h2 className="text-heading font-medium text-text-primary">Connect {connectAgent.name}</h2>
452
+ <div className="mt-1 flex items-center gap-2">
453
+ <p className="break-all font-mono text-[12px] text-text-tertiary">{connectAgent.id}</p>
454
+ <button
455
+ onClick={() => copyText(connectAgent.id, 'Agent ID copied')}
456
+ className="shrink-0 text-text-tertiary hover:text-text-secondary"
457
+ title="Copy agent ID"
458
+ >
459
+ <Copy className="h-3.5 w-3.5" />
460
+ </button>
461
+ </div>
462
+ </div>
463
+ <button onClick={() => setConnectAgent(null)} className="text-text-tertiary hover:text-text-secondary">
464
+ <X className="h-4 w-4" />
465
+ </button>
466
+ </div>
467
+
468
+ <div className="mb-5 flex flex-wrap gap-2 border-b border-border pb-3">
469
+ {[
470
+ ['status', 'Status'],
471
+ ['sdk', 'Node SDK'],
472
+ ['mcp', 'MCP'],
473
+ ['automation', 'Automation'],
474
+ ['openclaw', 'OpenClaw'],
475
+ ['rest', 'REST'],
476
+ ['codex', 'Codex/Cursor'],
477
+ ['activity', 'Activity'],
478
+ ].map(([id, label]) => (
479
+ <button
480
+ key={id}
481
+ onClick={() => setConnectTab(id as typeof connectTab)}
482
+ className={`rounded border px-3 py-1.5 text-body transition-colors ${
483
+ connectTab === id
484
+ ? 'border-accent bg-accent/10 text-accent'
485
+ : 'border-border bg-bg text-text-secondary hover:text-text-primary'
486
+ }`}
487
+ >
488
+ {label}
489
+ </button>
490
+ ))}
491
+ </div>
492
+
493
+ {connectTab === 'status' && (
494
+ <>
495
+ <div className="mb-5 rounded border border-border bg-bg p-4">
496
+ <div className="mb-3 flex items-center justify-between">
497
+ <h3 className="text-body font-medium text-text-primary">Connection status</h3>
498
+ <button
499
+ onClick={() => openConnect(connectAgent)}
500
+ className="text-body text-accent hover:text-accent-hover"
501
+ >
502
+ Test again
503
+ </button>
504
+ </div>
505
+ {connectionLoading ? (
506
+ <p className="text-body text-text-tertiary">Testing connection...</p>
507
+ ) : connection ? (
508
+ <>
509
+ <div className="grid gap-2 sm:grid-cols-2">
510
+ <StatusLine label="MCP" value={connection.mcp_status} success={connection.mcp_status === 'online'} />
511
+ <StatusLine label="Health" value={connectAgentHealth ? healthBadge[connectAgentHealth.status].label : 'unknown'} success={connectAgentHealth?.status === 'healthy'} />
512
+ <StatusLine label="Enforcement" value={enforcementBadge[connectAgent.enforcement_mode].label} success={connectAgent.enforcement_mode === 'enforce'} />
513
+ <StatusLine label="Last seen" value={connection.last_seen_at ? new Date(connection.last_seen_at).toLocaleString() : 'none'} />
514
+ <StatusLine label="Last real activity" value={connectAgentHealth?.last_activity_at ? new Date(connectAgentHealth.last_activity_at).toLocaleString() : 'none'} />
515
+ <StatusLine label="Last call" value={connection.last_successful_api_call?.activity_type || 'none'} />
516
+ <StatusLine label="Last error" value={connectAgentHealth?.last_error_code || connection.last_error?.error_code || 'none'} success={!connectAgentHealth?.last_error_at && !connection.last_error} />
517
+ <StatusLine
518
+ label="Latest activity"
519
+ value={connection.setup_readiness.latest_activity_type || 'none'}
520
+ />
521
+ </div>
522
+ {connection.mcp_status !== 'online' && (
523
+ <p className="mt-3 text-body text-text-secondary">
524
+ MCP is offline. Go to the MCP tab if this agent should be connected through Claude, Cursor, or another MCP client.
525
+ </p>
526
+ )}
527
+ {connectAgentHealth?.status === 'quiet' && (
528
+ <p className="mt-3 text-body text-warning">
529
+ No Availsync calls in 3 days. Check SDK/MCP env config.
530
+ </p>
531
+ )}
532
+ {connectAgent.enforcement_mode === 'observe' && (
533
+ <p className="mt-3 text-body text-text-secondary">
534
+ Observe-only is active. This agent will still run; Availsync records what it would have blocked.
535
+ </p>
536
+ )}
537
+ <div className="mt-4 rounded border border-border bg-surface px-3 py-2">
538
+ <p className="text-body text-text-secondary">
539
+ Recommended pilot path: run verification, switch this agent to Observe,
540
+ install <code>@availsync/node@alpha</code>, then check Activity for real traffic.
541
+ </p>
542
+ <div className="mt-2 flex flex-wrap gap-3">
543
+ <button onClick={() => setConnectTab('sdk')} className="text-body text-accent hover:text-accent-hover">
544
+ Open Node SDK setup
545
+ </button>
546
+ <a href="/docs/sdk/node" className="text-body text-accent hover:text-accent-hover">
547
+ Read SDK docs
548
+ </a>
549
+ </div>
550
+ </div>
551
+ </>
552
+ ) : (
553
+ <p className="text-body text-text-tertiary">Run a connection test to verify setup.</p>
554
+ )}
555
+ </div>
556
+
557
+ <div className="mb-5 rounded border border-border bg-bg p-4">
558
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
559
+ <div>
560
+ <h3 className="text-body font-medium text-text-primary">Dashboard setup test</h3>
561
+ <p className="mt-1 text-body text-text-secondary">
562
+ Run this safe dashboard test first, then connect your real MCP client.
563
+ </p>
564
+ </div>
565
+ <button
566
+ onClick={handleRunSetupTest}
567
+ disabled={setupTestRunning}
568
+ className="rounded bg-accent px-3 py-1.5 text-body font-medium text-white hover:bg-accent-hover disabled:opacity-50"
569
+ >
570
+ {setupTestRunning ? 'Running...' : 'Run test workflow'}
571
+ </button>
572
+ </div>
573
+ {setupTest && (
574
+ <div className="mt-4 space-y-2">
575
+ <div className="flex items-center justify-between">
576
+ <span className="font-mono text-[12px] text-text-tertiary">{setupTest.run_id}</span>
577
+ <Badge variant={setupTest.status === 'success' ? 'success' : setupTest.status === 'warning' ? 'warning' : 'error'}>
578
+ {setupTest.status}
579
+ </Badge>
580
+ </div>
581
+ <div className="space-y-2">
582
+ {setupTest.steps.map((step) => (
583
+ <SetupTestStepRow key={`${setupTest.run_id}-${step.name}`} step={step} />
584
+ ))}
585
+ </div>
586
+ </div>
587
+ )}
588
+ </div>
589
+
590
+ {connection && (
591
+ <div className="mb-5 rounded border border-border bg-bg p-4">
592
+ <h3 className="mb-3 text-body font-medium text-text-primary">Setup checklist</h3>
593
+ <div className="grid gap-2 sm:grid-cols-2">
594
+ {readinessItems.map(([label, value]) => (
595
+ <div key={label} className="flex items-center justify-between gap-2 rounded border border-border bg-surface px-3 py-2 text-body">
596
+ <span className="text-text-secondary">{label}</span>
597
+ <Badge variant={value ? 'success' : 'neutral'}>{value ? 'Done' : 'Missing'}</Badge>
598
+ </div>
599
+ ))}
600
+ </div>
601
+ {missingReadinessItems.length > 0 && (
602
+ <div className="mt-4 rounded border border-border bg-surface px-3 py-2">
603
+ <p className="text-body text-text-secondary">
604
+ Next: {missingReadinessItems.slice(0, 3).join(', ')}
605
+ {missingReadinessItems.length > 3 ? ` and ${missingReadinessItems.length - 3} more` : ''}.
606
+ </p>
607
+ </div>
608
+ )}
609
+ </div>
610
+ )}
611
+
612
+ <div className="rounded border border-border bg-bg p-4">
613
+ <div className="mb-3 flex items-center gap-2">
614
+ <KeyRound className="h-4 w-4 text-text-tertiary" />
615
+ <h3 className="text-body font-medium text-text-primary">API key</h3>
616
+ </div>
617
+ {rotatedKey ? (
618
+ <div>
619
+ <OneTimeKeyPanel apiKey={rotatedKey} onCopy={copyText} />
620
+ </div>
621
+ ) : (
622
+ <button
623
+ onClick={handleRotateKey}
624
+ disabled={rotating}
625
+ className="rounded border border-error/40 px-3 py-1.5 text-body font-medium text-error hover:border-error hover:bg-error/10 disabled:opacity-50"
626
+ >
627
+ {rotating ? 'Rotating...' : 'Rotate key'}
628
+ </button>
629
+ )}
630
+ </div>
631
+ </>
632
+ )}
633
+
634
+ {connectTab === 'sdk' && (
635
+ <div className="space-y-4">
636
+ <div className="rounded border border-border bg-bg p-4">
637
+ <h3 className="text-body font-medium text-text-primary">Node SDK pilot path</h3>
638
+ <p className="mt-1 text-body text-text-secondary">
639
+ Install <code>@availsync/node@alpha</code>, keep the API key in env, and wrap the repo task with withClaim. Use Observe first when you want proof without blocking real work.
640
+ </p>
641
+ <div className="mt-3 flex flex-wrap gap-3">
642
+ <a href="/docs/sdk/node" className="text-body text-accent hover:text-accent-hover">
643
+ Full SDK docs
644
+ </a>
645
+ <a href="/docs/pilot" className="text-body text-accent hover:text-accent-hover">
646
+ Pilot guide
647
+ </a>
648
+ </div>
649
+ </div>
650
+ {sdkSnippets.map((snippet) => <SnippetCard key={snippet.id} snippet={snippet} onCopy={copyText} />)}
651
+ </div>
652
+ )}
653
+
654
+ {connectTab === 'mcp' && (
655
+ <div className="space-y-4">
656
+ <div className="rounded border border-border bg-bg p-4">
657
+ <h3 className="text-body font-medium text-text-primary">Codex app setup</h3>
658
+ <p className="mt-1 text-body text-text-secondary">
659
+ Open Codex, go to Settings, then MCP servers. Add a new server and use the
660
+ field values below. The API key is the one-time secret from create or rotate key.
661
+ </p>
662
+ <div className="mt-3 grid gap-2 text-body text-text-secondary sm:grid-cols-2">
663
+ {[
664
+ 'Save the server.',
665
+ 'Restart Codex or reload MCP servers.',
666
+ 'Ask Codex to list available MCP tools.',
667
+ 'Return here and click Test again.',
668
+ ].map((step, index) => (
669
+ <div className="flex gap-2 rounded border border-border bg-surface px-3 py-2" key={step}>
670
+ <span className="font-mono text-text-tertiary">{index + 1}</span>
671
+ <span>{step}</span>
672
+ </div>
673
+ ))}
674
+ </div>
675
+ </div>
676
+ <SnippetCard snippet={codexMcpSnippet} onCopy={copyText} />
677
+ <div className="rounded border border-border bg-bg p-4">
678
+ <h3 className="text-body font-medium text-text-primary">Multiple Codex sessions</h3>
679
+ <p className="mt-1 text-body text-text-secondary">
680
+ Use one shared agent id when you only need a global Codex lock across sessions.
681
+ Use one Availsync agent per Codex role when you want different priorities or
682
+ conflict behavior.
683
+ </p>
684
+ <div className="mt-3 grid gap-2 text-body text-text-secondary sm:grid-cols-2">
685
+ <div className="rounded border border-border bg-surface px-3 py-2">
686
+ <span className="font-medium text-text-primary">Shared agent</span>
687
+ <p className="mt-1 text-text-tertiary">Best when all Codex sessions act as the same operator.</p>
688
+ </div>
689
+ <div className="rounded border border-border bg-surface px-3 py-2">
690
+ <span className="font-medium text-text-primary">Role agents</span>
691
+ <p className="mt-1 text-text-tertiary">Best for builder, reviewer, deploy, cleanup, or experiment priorities.</p>
692
+ </div>
693
+ </div>
694
+ </div>
695
+ <SnippetCard snippet={codexMultiAgentSnippet} onCopy={copyText} />
696
+ <SnippetCard snippet={mcpSnippet} onCopy={copyText} />
697
+ <SnippetCard snippet={cursorSnippet} onCopy={copyText} />
698
+ <div className="rounded border border-border bg-bg p-4">
699
+ <h3 className="text-body font-medium text-text-primary">MCP tool order</h3>
700
+ <p className="mt-1 text-body text-text-secondary">
701
+ heartbeat {'->'} check_work_slot {'->'} claim_work_slot {'->'} extend_work_slot while working {'->'} release_work_slot.
702
+ </p>
703
+ <p className="mt-2 text-body text-text-tertiary">
704
+ If Codex does not show these tools, check that Node.js and npx work locally,
705
+ confirm the agent id matches this agent, and rotate the key if the original API
706
+ key was lost.
707
+ </p>
708
+ </div>
709
+ </div>
710
+ )}
711
+
712
+ {connectTab === 'automation' && (
713
+ <div className="space-y-4">
714
+ <div className="rounded border border-border bg-bg p-4">
715
+ <h3 className="text-body font-medium text-text-primary">Automation guardrail</h3>
716
+ <p className="mt-1 text-body text-text-secondary">
717
+ Generate a wrapper for scheduled Codex, Claude, Cursor, cron, or server jobs. Blocked runs return skip_run and should exit cleanly.
718
+ </p>
719
+ <p className="mt-2 text-body text-text-secondary">
720
+ To pilot safely, switch the agent to Observe on the Agents table. Observe mode never creates a lease; it only reports what Availsync would have blocked.
721
+ </p>
722
+ <p className="mt-2 text-body text-text-tertiary">
723
+ Default to repo-level for maximum safety. Use project-level only for independent streams; Availsync does not yet infer dependencies between different resources.
724
+ </p>
725
+ <div className="mt-4 grid gap-3 sm:grid-cols-[140px_1fr_120px]">
726
+ <div>
727
+ <label className="mb-1 block text-label uppercase text-text-tertiary">Resource</label>
728
+ <select
729
+ value={connectResource.resource_type}
730
+ onChange={(event) => setConnectResource({ ...connectResource, resource_type: event.target.value as 'project' | 'repo' })}
731
+ className="w-full rounded border border-border bg-surface px-3 py-2 text-body text-text-primary outline-none focus:border-border-focus"
732
+ >
733
+ <option value="repo">Repo · safest</option>
734
+ <option value="project">Project · advanced</option>
735
+ </select>
736
+ </div>
737
+ <div>
738
+ <label className="mb-1 block text-label uppercase text-text-tertiary">Resource key</label>
739
+ <input
740
+ value={connectResource.resource_key}
741
+ onChange={(event) => setConnectResource({ ...connectResource, resource_key: event.target.value })}
742
+ className="w-full rounded border border-border bg-surface px-3 py-2 text-body text-text-primary outline-none focus:border-border-focus"
743
+ placeholder="owner/repo"
744
+ />
745
+ </div>
746
+ <div>
747
+ <label className="mb-1 block text-label uppercase text-text-tertiary">Minutes</label>
748
+ <input
749
+ type="number"
750
+ min={1}
751
+ max={240}
752
+ value={connectResource.duration_minutes}
753
+ onChange={(event) => setConnectResource({ ...connectResource, duration_minutes: Number(event.target.value) || 45 })}
754
+ className="w-full rounded border border-border bg-surface px-3 py-2 text-body text-text-primary outline-none focus:border-border-focus"
755
+ />
756
+ </div>
757
+ </div>
758
+ </div>
759
+ {automationSnippets.map((snippet) => <SnippetCard key={snippet.id} snippet={snippet} onCopy={copyText} />)}
760
+ </div>
761
+ )}
762
+
763
+ {connectTab === 'openclaw' && (
764
+ <div className="space-y-4">
765
+ <div className="rounded border border-border bg-bg p-4">
766
+ <h3 className="text-body font-medium text-text-primary">OpenClaw skill pack</h3>
767
+ <p className="mt-1 text-body text-text-secondary">
768
+ Install the SKILL.md in OpenClaw and keep API_KEY in env. The skill teaches OpenClaw to start, extend, and finish Availsync work runs before touching the repo.
769
+ </p>
770
+ </div>
771
+ <SnippetCard snippet={openClawSkill} onCopy={copyText} />
772
+ <SnippetCard snippet={openClawSetup} onCopy={copyText} />
773
+ </div>
774
+ )}
775
+
776
+ {connectTab === 'rest' && (
777
+ <div className="space-y-4">
778
+ <div className="rounded border border-border bg-bg p-4">
779
+ <h3 className="text-body font-medium text-text-primary">Work guardrail flow</h3>
780
+ <p className="mt-1 text-body text-text-secondary">
781
+ Use Availsync to stop agents from acting on the same selected resource at the same time. Repo-level is the safest default.
782
+ </p>
783
+ </div>
784
+ {workSnippets.map((snippet) => <SnippetCard key={snippet.id} snippet={snippet} onCopy={copyText} />)}
785
+ {scheduleSnippets.map((snippet) => <SnippetCard key={snippet.id} snippet={snippet} onCopy={copyText} />)}
786
+ </div>
787
+ )}
788
+
789
+ {connectTab === 'codex' && (
790
+ <div className="space-y-4">
791
+ <SnippetCard snippet={codexSnippet} onCopy={copyText} />
792
+ <SnippetCard snippet={cronSnippet} onCopy={copyText} />
793
+ <div className="rounded border border-border bg-bg p-4">
794
+ <h3 className="text-body font-medium text-text-primary">Plugin roadmap</h3>
795
+ <p className="mt-1 text-body text-text-secondary">
796
+ A dedicated Codex plugin is a future integration layer. For now, use the MCP server or REST flow above.
797
+ </p>
798
+ </div>
799
+ </div>
800
+ )}
801
+
802
+ {connectTab === 'activity' && (
803
+ <div className="space-y-4">
804
+ <div className="rounded border border-border bg-bg p-4">
805
+ <h3 className="text-body font-medium text-text-primary">Activity validation</h3>
806
+ <p className="mt-1 text-body text-text-secondary">
807
+ Activity shows MCP/API/runtime traffic. Audit stays focused on governance, work lifecycle decisions, holds, and conflicts.
808
+ </p>
809
+ <button
810
+ onClick={() => { window.location.href = `/app/activity?agent=${connectAgent.id}`; }}
811
+ className="mt-4 rounded bg-accent px-3 py-1.5 text-body font-medium text-white hover:bg-accent-hover"
812
+ >
813
+ Open Activity filtered to this agent
814
+ </button>
815
+ </div>
816
+ <div className="rounded border border-border bg-bg p-4">
817
+ <h3 className="text-body font-medium text-text-primary">Latest observed event</h3>
818
+ <p className="mt-1 font-mono text-[12px] text-text-tertiary">
819
+ {connection?.setup_readiness.latest_activity_type || 'none'} {connection?.setup_readiness.latest_activity_at || ''}
820
+ </p>
821
+ </div>
822
+ </div>
823
+ )}
824
+ </div>
825
+ </div>
826
+ )}
827
+
828
+ {/* Agent table */}
829
+ {loading ? (
830
+ <div className="rounded border border-border overflow-hidden">
831
+ {Array.from({ length: 3 }).map((_, i) => (
832
+ <SkeletonRow key={i} />
833
+ ))}
834
+ </div>
835
+ ) : agents.length === 0 ? (
836
+ <EmptyState
837
+ icon={Users}
838
+ title="No agents yet"
839
+ description="Create your first agent, copy its one-time API key, then install the Node SDK to send real guarded runs."
840
+ action={
841
+ <button
842
+ onClick={() => setShowNewPanel(true)}
843
+ className="rounded bg-accent text-white px-3 py-1.5 text-body font-medium hover:bg-accent-hover transition-colors"
844
+ >
845
+ Create agent
846
+ </button>
847
+ }
848
+ />
849
+ ) : (
850
+ <div className="overflow-x-auto rounded border border-border dark-scroll">
851
+ <table className="w-full min-w-[1180px]">
852
+ <colgroup>
853
+ <col className="w-[150px]" />
854
+ <col className="w-[130px]" />
855
+ <col className="w-[100px]" />
856
+ <col className="w-[95px]" />
857
+ <col className="w-[135px]" />
858
+ <col className="w-[150px]" />
859
+ <col className="w-[100px]" />
860
+ <col className="w-[85px]" />
861
+ <col className="w-[120px]" />
862
+ <col className="w-[115px]" />
863
+ </colgroup>
864
+ <thead>
865
+ <tr className="border-b border-border bg-surface text-label uppercase text-text-tertiary">
866
+ <th className="px-4 py-2 text-left font-medium">Name</th>
867
+ <th className="px-4 py-2 text-left font-medium">Agent ID</th>
868
+ <th className="px-4 py-2 text-left font-medium">Health</th>
869
+ <th className="px-4 py-2 text-left font-medium">Mode</th>
870
+ <th className="px-4 py-2 text-left font-medium">MCP</th>
871
+ <th className="px-4 py-2 text-left font-medium">Last seen</th>
872
+ <th className="px-4 py-2 text-left font-medium">Type</th>
873
+ <th className="px-4 py-2 text-left font-medium">Priority</th>
874
+ <th className="px-4 py-2 text-left font-medium">Created</th>
875
+ <th className="px-4 py-2 text-right font-medium">Actions</th>
876
+ </tr>
877
+ </thead>
878
+ <tbody>
879
+ {agents.map((agent) => {
880
+ const status = mcpStatus(agent);
881
+ const health = healthByAgent.get(agent.id);
882
+ return (
883
+ <tr
884
+ key={agent.id}
885
+ className="h-11 border-b border-border transition-colors duration-150 hover:bg-surface-raised"
886
+ >
887
+ <td className="px-4 py-2 text-body font-medium text-text-primary">
888
+ {editingAgentId === agent.id ? (
889
+ <input
890
+ value={editForm.name}
891
+ onChange={(e) => setEditForm({ ...editForm, name: e.target.value })}
892
+ className="w-full rounded bg-bg border border-border px-2 py-1 text-body text-text-primary outline-none focus:border-border-focus"
893
+ />
894
+ ) : (
895
+ <span className="block truncate" title={agent.name}>{agent.name}</span>
896
+ )}
897
+ </td>
898
+ <td className="px-4 py-2">
899
+ <div className="flex items-center gap-2 whitespace-nowrap">
900
+ <span className="font-mono text-[12px] text-text-secondary" title={agent.id}>
901
+ {agent.id.slice(0, 8)}...
902
+ </span>
903
+ <button
904
+ onClick={() => {
905
+ navigator.clipboard.writeText(agent.id);
906
+ toast('Agent ID copied', 'success');
907
+ }}
908
+ className="shrink-0 text-text-tertiary hover:text-text-secondary"
909
+ title="Copy full agent ID"
910
+ >
911
+ <Copy className="h-3.5 w-3.5" />
912
+ </button>
913
+ </div>
914
+ </td>
915
+ <td className="px-4 py-2">
916
+ {health ? (
917
+ <Badge variant={healthBadge[health.status].variant}>{healthBadge[health.status].label}</Badge>
918
+ ) : (
919
+ <Badge variant="neutral">Unknown</Badge>
920
+ )}
921
+ </td>
922
+ <td className="px-4 py-2">
923
+ {editingAgentId === agent.id ? (
924
+ <select
925
+ value={editForm.enforcement_mode}
926
+ onChange={(e) => setEditForm({ ...editForm, enforcement_mode: e.target.value as Agent['enforcement_mode'] })}
927
+ className="rounded bg-bg border border-border px-2 py-1 text-body text-text-primary outline-none focus:border-border-focus"
928
+ >
929
+ <option value="enforce">Enforce</option>
930
+ <option value="observe">Observe</option>
931
+ </select>
932
+ ) : (
933
+ <Badge variant={enforcementBadge[agent.enforcement_mode].variant}>{enforcementBadge[agent.enforcement_mode].label}</Badge>
934
+ )}
935
+ </td>
936
+ <td className="px-4 py-2">
937
+ <div className="flex items-center gap-2 whitespace-nowrap">
938
+ <Badge variant={status.variant}>{status.label}</Badge>
939
+ <span className="text-label text-text-tertiary">{status.detail}</span>
940
+ </div>
941
+ </td>
942
+ <td className="whitespace-nowrap px-4 py-2 font-mono text-body text-text-tertiary">
943
+ {formatAgentTimestamp(agent.last_seen_at ?? agent.last_used_at)}
944
+ </td>
945
+ <td className="px-4 py-2">
946
+ {editingAgentId === agent.id ? (
947
+ <select
948
+ value={editForm.agent_type}
949
+ onChange={(e) => setEditForm({ ...editForm, agent_type: e.target.value })}
950
+ className="rounded bg-bg border border-border px-2 py-1 text-body text-text-primary outline-none focus:border-border-focus"
951
+ >
952
+ <option value="external_meeting">External</option>
953
+ <option value="internal">Internal</option>
954
+ <option value="focus">Focus</option>
955
+ <option value="generic">Generic</option>
956
+ </select>
957
+ ) : (
958
+ <Badge variant={typeBadgeVariant[agent.agent_type] || 'neutral'}>
959
+ {typeLabels[agent.agent_type] || agent.agent_type}
960
+ </Badge>
961
+ )}
962
+ </td>
963
+ <td className="whitespace-nowrap px-4 py-2 font-mono text-body text-text-secondary">
964
+ {editingAgentId === agent.id ? (
965
+ <input
966
+ type="number"
967
+ value={editForm.priority}
968
+ onChange={(e) => setEditForm({ ...editForm, priority: Number(e.target.value) })}
969
+ className="w-20 rounded bg-bg border border-border px-2 py-1 text-body text-text-primary outline-none focus:border-border-focus"
970
+ />
971
+ ) : (
972
+ agent.priority
973
+ )}
974
+ </td>
975
+ <td className="whitespace-nowrap px-4 py-2 font-mono text-body text-text-tertiary">
976
+ {formatAgentTimestamp(agent.created_at).slice(0, 10)}
977
+ </td>
978
+ <td className="px-4 py-2">
979
+ <div className="flex items-center justify-end gap-3 whitespace-nowrap">
980
+ {editingAgentId === agent.id ? (
981
+ <>
982
+ <button
983
+ onClick={handleUpdate}
984
+ className="shrink-0 text-success hover:text-success/80"
985
+ title="Save changes"
986
+ >
987
+ <Check className="h-3.5 w-3.5" />
988
+ </button>
989
+ <button
990
+ onClick={() => setEditingAgentId(null)}
991
+ className="shrink-0 text-text-tertiary hover:text-text-secondary"
992
+ title="Cancel"
993
+ >
994
+ <X className="h-3.5 w-3.5" />
995
+ </button>
996
+ </>
997
+ ) : (
998
+ <>
999
+ <button
1000
+ onClick={() => openConnect(agent)}
1001
+ className="shrink-0 text-text-tertiary hover:text-text-secondary"
1002
+ title="Connect agent"
1003
+ >
1004
+ <Plug className="h-3.5 w-3.5" />
1005
+ </button>
1006
+ <button
1007
+ onClick={() => startEdit(agent)}
1008
+ className="shrink-0 text-text-tertiary hover:text-text-secondary"
1009
+ title="Edit agent"
1010
+ >
1011
+ <Pencil className="h-3.5 w-3.5" />
1012
+ </button>
1013
+ <button
1014
+ onClick={() => handleDelete(agent)}
1015
+ className="shrink-0 text-error hover:text-error/80"
1016
+ title="Delete agent"
1017
+ >
1018
+ <Trash2 className="h-3.5 w-3.5" />
1019
+ </button>
1020
+ </>
1021
+ )}
1022
+ </div>
1023
+ </td>
1024
+ </tr>
1025
+ );
1026
+ })}
1027
+ </tbody>
1028
+ </table>
1029
+ </div>
1030
+ )}
1031
+ </div>
1032
+ );
1033
+ }
1034
+
1035
+ function StatusLine({ label, value, success }: { label: string; value: string; success?: boolean }) {
1036
+ return (
1037
+ <div className="rounded border border-border bg-surface p-3">
1038
+ <div className="text-label uppercase text-text-tertiary">{label}</div>
1039
+ <div className="mt-1 flex items-center justify-between gap-2">
1040
+ <span className="truncate text-body text-text-secondary">{value}</span>
1041
+ {success != null && <Badge variant={success ? 'success' : 'neutral'}>{success ? 'OK' : 'Check'}</Badge>}
1042
+ </div>
1043
+ </div>
1044
+ );
1045
+ }
1046
+
1047
+ function SetupTestStepRow({ step }: { step: SetupTestRun['steps'][number] }) {
1048
+ const variant =
1049
+ step.status === 'success' ? 'success' : step.status === 'warning' ? 'warning' : step.status === 'blocked' || step.status === 'error' ? 'error' : 'neutral';
1050
+
1051
+ return (
1052
+ <div className="rounded border border-border bg-surface p-3">
1053
+ <div className="flex items-start justify-between gap-3">
1054
+ <div>
1055
+ <div className="font-mono text-[12px] text-text-primary">{step.name.replaceAll('_', ' ')}</div>
1056
+ <p className="mt-1 text-body text-text-secondary">{step.summary}</p>
1057
+ {step.next_action && (
1058
+ <p className="mt-1 text-body text-warning">{step.next_action}</p>
1059
+ )}
1060
+ </div>
1061
+ <div className="flex shrink-0 flex-col items-end gap-1">
1062
+ <Badge variant={variant}>{step.status}</Badge>
1063
+ <span className="font-mono text-[11px] text-text-tertiary">{step.latency_ms}ms</span>
1064
+ </div>
1065
+ </div>
1066
+ </div>
1067
+ );
1068
+ }
1069
+
1070
+ function OneTimeKeyPanel({
1071
+ apiKey,
1072
+ onCopy,
1073
+ saved,
1074
+ onSavedChange,
1075
+ }: {
1076
+ apiKey: string;
1077
+ onCopy: (text: string, label?: string) => void;
1078
+ saved?: boolean;
1079
+ onSavedChange?: (saved: boolean) => void;
1080
+ }) {
1081
+ const [revealed, setRevealed] = useState(false);
1082
+
1083
+ return (
1084
+ <div className="mb-4 rounded border border-warning/30 bg-bg p-4">
1085
+ <p className="mb-2 text-body font-medium text-warning">Copy now. This key will not be shown again.</p>
1086
+ <div className="mb-3 flex items-center gap-2 rounded bg-surface p-3">
1087
+ <input
1088
+ readOnly
1089
+ type={revealed ? 'text' : 'password'}
1090
+ value={apiKey}
1091
+ className="min-w-0 flex-1 bg-transparent font-mono text-[12px] text-text-secondary outline-none"
1092
+ />
1093
+ <button
1094
+ onClick={() => setRevealed((value) => !value)}
1095
+ className="shrink-0 text-text-tertiary hover:text-text-secondary"
1096
+ title={revealed ? 'Hide API key' : 'Reveal API key'}
1097
+ >
1098
+ {revealed ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
1099
+ </button>
1100
+ </div>
1101
+ <div className="flex flex-wrap items-center gap-3">
1102
+ <button
1103
+ onClick={() => {
1104
+ onCopy(apiKey, 'API key copied');
1105
+ onSavedChange?.(true);
1106
+ }}
1107
+ className="flex items-center gap-1.5 text-body text-accent hover:text-accent-hover"
1108
+ >
1109
+ <Copy className="h-3.5 w-3.5" />
1110
+ Copy to clipboard
1111
+ </button>
1112
+ {onSavedChange && (
1113
+ <label className="flex items-center gap-2 text-body text-text-secondary">
1114
+ <input
1115
+ type="checkbox"
1116
+ checked={Boolean(saved)}
1117
+ onChange={(event) => onSavedChange(event.target.checked)}
1118
+ className="h-4 w-4 accent-accent"
1119
+ />
1120
+ I saved this key
1121
+ </label>
1122
+ )}
1123
+ </div>
1124
+ </div>
1125
+ );
1126
+ }
1127
+
1128
+ function SnippetCard({
1129
+ snippet,
1130
+ onCopy,
1131
+ }: {
1132
+ snippet: SetupSnippet;
1133
+ onCopy: (text: string, label?: string) => void;
1134
+ }) {
1135
+ return (
1136
+ <div className="rounded border border-border bg-bg p-4">
1137
+ <div className="mb-3 flex items-start justify-between gap-3">
1138
+ <div>
1139
+ <h3 className="text-body font-medium text-text-primary">{snippet.title}</h3>
1140
+ <p className="mt-1 text-body text-text-secondary">{snippet.description}</p>
1141
+ </div>
1142
+ <button
1143
+ onClick={() => onCopy(snippet.code, snippet.copyLabel)}
1144
+ className="flex shrink-0 items-center gap-1.5 rounded border border-border bg-surface px-2 py-1 text-body text-text-secondary hover:text-text-primary"
1145
+ >
1146
+ <Copy className="h-3.5 w-3.5" />
1147
+ Copy
1148
+ </button>
1149
+ </div>
1150
+ <pre className="max-h-72 overflow-auto rounded border border-border bg-surface p-3 text-[11px] text-text-secondary whitespace-pre-wrap">
1151
+ {snippet.code}
1152
+ </pre>
1153
+ </div>
1154
+ );
1155
+ }