veryfront 0.1.1237 → 0.1.1239

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 (493) hide show
  1. package/esm/cli/auth/login.d.ts +24 -2
  2. package/esm/cli/auth/login.d.ts.map +1 -1
  3. package/esm/cli/auth/login.js +444 -35
  4. package/esm/cli/commands/build/error-handler.d.ts.map +1 -1
  5. package/esm/cli/commands/build/error-handler.js +41 -3
  6. package/esm/cli/commands/build/handler.d.ts.map +1 -1
  7. package/esm/cli/commands/build/handler.js +1 -1
  8. package/esm/cli/commands/dev/command.d.ts.map +1 -1
  9. package/esm/cli/commands/dev/command.js +14 -1
  10. package/esm/cli/commands/dev/inference-status.d.ts +6 -0
  11. package/esm/cli/commands/dev/inference-status.d.ts.map +1 -1
  12. package/esm/cli/commands/dev/inference-status.js +14 -2
  13. package/esm/cli/commands/generate/command.d.ts.map +1 -1
  14. package/esm/cli/commands/generate/command.js +61 -0
  15. package/esm/cli/commands/init/init-command.d.ts.map +1 -1
  16. package/esm/cli/commands/init/init-command.js +23 -31
  17. package/esm/cli/commands/install/registry.d.ts.map +1 -1
  18. package/esm/cli/commands/install/registry.js +6 -9
  19. package/esm/cli/commands/login/command-help.d.ts.map +1 -1
  20. package/esm/cli/commands/login/command-help.js +3 -1
  21. package/esm/cli/commands/pull/command.d.ts +1 -0
  22. package/esm/cli/commands/pull/command.d.ts.map +1 -1
  23. package/esm/cli/commands/pull/command.js +49 -6
  24. package/esm/cli/commands/push/command-help.d.ts.map +1 -1
  25. package/esm/cli/commands/push/command-help.js +7 -0
  26. package/esm/cli/commands/push/command.d.ts +23 -12
  27. package/esm/cli/commands/push/command.d.ts.map +1 -1
  28. package/esm/cli/commands/push/command.js +461 -44
  29. package/esm/cli/commands/push/plan.d.ts +35 -0
  30. package/esm/cli/commands/push/plan.d.ts.map +1 -0
  31. package/esm/cli/commands/push/plan.js +96 -0
  32. package/esm/cli/commands/schedule/handler.d.ts +21 -2
  33. package/esm/cli/commands/schedule/handler.d.ts.map +1 -1
  34. package/esm/cli/commands/schedule/handler.js +74 -27
  35. package/esm/cli/commands/schema/command-help.js +1 -1
  36. package/esm/cli/commands/up/command.js +1 -1
  37. package/esm/cli/commands/webhook/handler.d.ts.map +1 -1
  38. package/esm/cli/commands/webhook/handler.js +3 -1
  39. package/esm/cli/router.d.ts.map +1 -1
  40. package/esm/cli/router.js +27 -5
  41. package/esm/cli/shared/args.d.ts +15 -1
  42. package/esm/cli/shared/args.d.ts.map +1 -1
  43. package/esm/cli/shared/args.js +56 -3
  44. package/esm/cli/shared/config.d.ts +11 -2
  45. package/esm/cli/shared/config.d.ts.map +1 -1
  46. package/esm/cli/shared/config.js +72 -27
  47. package/esm/cli/shared/project-creation.js +1 -1
  48. package/esm/cli/sync/state.d.ts +20 -0
  49. package/esm/cli/sync/state.d.ts.map +1 -0
  50. package/esm/cli/sync/state.js +193 -0
  51. package/esm/deno.js +2 -2
  52. package/esm/extensions/ext-llm-anthropic/src/anthropic-request-builder.d.ts.map +1 -1
  53. package/esm/extensions/ext-llm-anthropic/src/anthropic-request-builder.js +852 -21
  54. package/esm/extensions/ext-observability-sentry/src/policy.d.ts +2 -0
  55. package/esm/extensions/ext-observability-sentry/src/policy.d.ts.map +1 -1
  56. package/esm/extensions/ext-observability-sentry/src/policy.js +214 -3
  57. package/esm/src/agent/conversation/delegation-policy.d.ts +2 -0
  58. package/esm/src/agent/conversation/delegation-policy.d.ts.map +1 -1
  59. package/esm/src/agent/conversation/delegation-policy.js +13 -3
  60. package/esm/src/agent/conversation/hosted-terminal.d.ts.map +1 -1
  61. package/esm/src/agent/conversation/hosted-terminal.js +6 -2
  62. package/esm/src/agent/conversation/run-chunk-mirror.d.ts.map +1 -1
  63. package/esm/src/agent/conversation/run-chunk-mirror.js +14 -2
  64. package/esm/src/agent/factory.d.ts.map +1 -1
  65. package/esm/src/agent/factory.js +22 -11
  66. package/esm/src/agent/hosted/chat-runtime-tool-assembly.d.ts +2 -0
  67. package/esm/src/agent/hosted/chat-runtime-tool-assembly.d.ts.map +1 -1
  68. package/esm/src/agent/hosted/chat-runtime-tool-assembly.js +6 -1
  69. package/esm/src/agent/hosted/child-fork-execution-runner.d.ts +2 -1
  70. package/esm/src/agent/hosted/child-fork-execution-runner.d.ts.map +1 -1
  71. package/esm/src/agent/hosted/child-fork-execution-runner.js +6 -3
  72. package/esm/src/agent/hosted/child-fork-step-message-preparation.d.ts +12 -3
  73. package/esm/src/agent/hosted/child-fork-step-message-preparation.d.ts.map +1 -1
  74. package/esm/src/agent/hosted/child-fork-step-message-preparation.js +16 -3
  75. package/esm/src/agent/hosted/cloud-agent-chat-execution.d.ts +1 -1
  76. package/esm/src/agent/hosted/cloud-agent-chat-execution.d.ts.map +1 -1
  77. package/esm/src/agent/hosted/cloud-agent-chat-execution.js +3 -3
  78. package/esm/src/agent/hosted/cloud-agent-child-tools.d.ts +11 -0
  79. package/esm/src/agent/hosted/cloud-agent-child-tools.d.ts.map +1 -1
  80. package/esm/src/agent/hosted/cloud-agent-child-tools.js +2 -3
  81. package/esm/src/agent/hosted/cloud-agent-config.d.ts +1 -0
  82. package/esm/src/agent/hosted/cloud-agent-config.d.ts.map +1 -1
  83. package/esm/src/agent/hosted/cloud-runtime-system-messages.d.ts +16 -1
  84. package/esm/src/agent/hosted/cloud-runtime-system-messages.d.ts.map +1 -1
  85. package/esm/src/agent/hosted/cloud-runtime-system-messages.js +13 -3
  86. package/esm/src/agent/hosted/default-chat-runtime.d.ts +2 -1
  87. package/esm/src/agent/hosted/default-chat-runtime.d.ts.map +1 -1
  88. package/esm/src/agent/hosted/default-chat-runtime.js +18 -7
  89. package/esm/src/agent/hosted/default-invoke-agent-tool.d.ts +2 -1
  90. package/esm/src/agent/hosted/default-invoke-agent-tool.d.ts.map +1 -1
  91. package/esm/src/agent/hosted/default-invoke-agent-tool.js +11 -3
  92. package/esm/src/agent/hosted/default-project-steering-refresh.d.ts +2 -1
  93. package/esm/src/agent/hosted/default-project-steering-refresh.d.ts.map +1 -1
  94. package/esm/src/agent/hosted/default-project-steering-refresh.js +4 -1
  95. package/esm/src/agent/hosted/runtime-state-resolver.d.ts +6 -4
  96. package/esm/src/agent/hosted/runtime-state-resolver.d.ts.map +1 -1
  97. package/esm/src/agent/hosted/runtime-state-resolver.js +8 -11
  98. package/esm/src/agent/index.d.ts +3 -3
  99. package/esm/src/agent/index.d.ts.map +1 -1
  100. package/esm/src/agent/project/agent-runtime.d.ts.map +1 -1
  101. package/esm/src/agent/project/agent-runtime.js +6 -3
  102. package/esm/src/agent/runtime/agent-definition.d.ts +68 -66
  103. package/esm/src/agent/runtime/agent-definition.d.ts.map +1 -1
  104. package/esm/src/agent/runtime/agent-definition.js +15 -2
  105. package/esm/src/agent/runtime/agent-invocation-contract.d.ts +2 -0
  106. package/esm/src/agent/runtime/agent-invocation-contract.d.ts.map +1 -1
  107. package/esm/src/agent/runtime/agent-markdown-adapter.js +1 -1
  108. package/esm/src/agent/runtime/agent-runtime-step.d.ts +11 -5
  109. package/esm/src/agent/runtime/agent-runtime-step.d.ts.map +1 -1
  110. package/esm/src/agent/runtime/agent-runtime-step.js +23 -6
  111. package/esm/src/agent/runtime/call-context.d.ts +42 -11
  112. package/esm/src/agent/runtime/call-context.d.ts.map +1 -1
  113. package/esm/src/agent/runtime/call-context.js +480 -42
  114. package/esm/src/agent/runtime/chat-stream-handler.d.ts +18 -0
  115. package/esm/src/agent/runtime/chat-stream-handler.d.ts.map +1 -1
  116. package/esm/src/agent/runtime/chat-stream-handler.js +38 -18
  117. package/esm/src/agent/runtime/effective-agent-system.d.ts +13 -4
  118. package/esm/src/agent/runtime/effective-agent-system.d.ts.map +1 -1
  119. package/esm/src/agent/runtime/effective-agent-system.js +29 -0
  120. package/esm/src/agent/runtime/index.d.ts +1 -0
  121. package/esm/src/agent/runtime/index.d.ts.map +1 -1
  122. package/esm/src/agent/runtime/index.js +638 -53
  123. package/esm/src/agent/runtime/load-skill-tool.d.ts +33 -3
  124. package/esm/src/agent/runtime/load-skill-tool.d.ts.map +1 -1
  125. package/esm/src/agent/runtime/load-skill-tool.js +152 -21
  126. package/esm/src/agent/runtime/model-resolution.d.ts +3 -0
  127. package/esm/src/agent/runtime/model-resolution.d.ts.map +1 -1
  128. package/esm/src/agent/runtime/model-resolution.js +14 -0
  129. package/esm/src/agent/runtime/model-transport.d.ts +1 -0
  130. package/esm/src/agent/runtime/model-transport.d.ts.map +1 -1
  131. package/esm/src/agent/runtime/model-transport.js +5 -2
  132. package/esm/src/agent/runtime/run-runtime-context.d.ts +4 -0
  133. package/esm/src/agent/runtime/run-runtime-context.d.ts.map +1 -1
  134. package/esm/src/agent/runtime/run-runtime-context.js +9 -3
  135. package/esm/src/agent/runtime/runtime-tool-config.d.ts +1 -0
  136. package/esm/src/agent/runtime/runtime-tool-config.d.ts.map +1 -1
  137. package/esm/src/agent/runtime/skill-prompt.d.ts +3 -0
  138. package/esm/src/agent/runtime/skill-prompt.d.ts.map +1 -1
  139. package/esm/src/agent/runtime/skill-prompt.js +113 -25
  140. package/esm/src/agent/runtime/streamed-assistant-message.d.ts +22 -2
  141. package/esm/src/agent/runtime/streamed-assistant-message.d.ts.map +1 -1
  142. package/esm/src/agent/runtime/streamed-assistant-message.js +26 -5
  143. package/esm/src/agent/runtime/tool-inventory.d.ts.map +1 -1
  144. package/esm/src/agent/runtime/tool-inventory.js +13 -9
  145. package/esm/src/agent/runtime/tool-result-continuation.d.ts +4 -2
  146. package/esm/src/agent/runtime/tool-result-continuation.d.ts.map +1 -1
  147. package/esm/src/agent/runtime/tool-result-continuation.js +24 -20
  148. package/esm/src/agent/schemas/agent.schema.d.ts +2 -2
  149. package/esm/src/agent/service/runtime.js +1 -1
  150. package/esm/src/agent/streaming/fork-runtime-stream.d.ts +5 -4
  151. package/esm/src/agent/streaming/fork-runtime-stream.d.ts.map +1 -1
  152. package/esm/src/agent/types.d.ts +14 -1
  153. package/esm/src/agent/types.d.ts.map +1 -1
  154. package/esm/src/chat/chat-ui-message-helpers.d.ts.map +1 -1
  155. package/esm/src/chat/chat-ui-message-helpers.js +75 -16
  156. package/esm/src/config/loader.d.ts.map +1 -1
  157. package/esm/src/config/loader.js +37 -1
  158. package/esm/src/errors/catalog/build-errors.d.ts.map +1 -1
  159. package/esm/src/errors/catalog/build-errors.js +14 -0
  160. package/esm/src/errors/error-registry/build.d.ts +2 -0
  161. package/esm/src/errors/error-registry/build.d.ts.map +1 -1
  162. package/esm/src/errors/error-registry/build.js +8 -0
  163. package/esm/src/errors/error-registry/deploy.d.ts +4 -0
  164. package/esm/src/errors/error-registry/deploy.d.ts.map +1 -1
  165. package/esm/src/errors/error-registry/deploy.js +16 -0
  166. package/esm/src/errors/error-registry.d.ts +3 -0
  167. package/esm/src/errors/error-registry.d.ts.map +1 -1
  168. package/esm/src/errors/index.d.ts +2 -1
  169. package/esm/src/errors/index.d.ts.map +1 -1
  170. package/esm/src/errors/index.js +4 -3
  171. package/esm/src/errors/tenant-classification.d.ts +25 -0
  172. package/esm/src/errors/tenant-classification.d.ts.map +1 -0
  173. package/esm/src/errors/tenant-classification.js +61 -0
  174. package/esm/src/extensions/builtin-extensions.d.ts +10 -15
  175. package/esm/src/extensions/builtin-extensions.d.ts.map +1 -1
  176. package/esm/src/extensions/builtin-extensions.js +32 -26
  177. package/esm/src/extensions/deferred-extension.d.ts +4 -4
  178. package/esm/src/extensions/deferred-extension.js +4 -4
  179. package/esm/src/extensions/index.d.ts +1 -0
  180. package/esm/src/extensions/index.d.ts.map +1 -1
  181. package/esm/src/extensions/index.js +1 -0
  182. package/esm/src/html/hydration-script-builder/hydration-runtime.generated.js +1 -1
  183. package/esm/src/integrations/_data.d.ts.map +1 -1
  184. package/esm/src/integrations/_data.js +6 -0
  185. package/esm/src/integrations/schema.d.ts +8 -0
  186. package/esm/src/integrations/schema.d.ts.map +1 -1
  187. package/esm/src/integrations/schema.js +3 -0
  188. package/esm/src/integrations/types.d.ts +2 -0
  189. package/esm/src/integrations/types.d.ts.map +1 -1
  190. package/esm/src/internal-agents/run-stream.d.ts.map +1 -1
  191. package/esm/src/internal-agents/run-stream.js +35 -20
  192. package/esm/src/internal-agents/run-system-prompt.d.ts +7 -2
  193. package/esm/src/internal-agents/run-system-prompt.d.ts.map +1 -1
  194. package/esm/src/internal-agents/run-system-prompt.js +19 -13
  195. package/esm/src/internal-agents/schema.d.ts +2 -0
  196. package/esm/src/internal-agents/schema.d.ts.map +1 -1
  197. package/esm/src/modules/react-loader/ssr-module-loader/loader.d.ts.map +1 -1
  198. package/esm/src/modules/react-loader/ssr-module-loader/loader.js +2 -18
  199. package/esm/src/modules/react-loader/ssr-module-loader/ssr-dependency-validator.d.ts +11 -0
  200. package/esm/src/modules/react-loader/ssr-module-loader/ssr-dependency-validator.d.ts.map +1 -1
  201. package/esm/src/modules/react-loader/ssr-module-loader/ssr-dependency-validator.js +53 -6
  202. package/esm/src/observability/application-error-contract.d.ts +4 -0
  203. package/esm/src/observability/application-error-contract.d.ts.map +1 -1
  204. package/esm/src/observability/application-errors.d.ts.map +1 -1
  205. package/esm/src/observability/application-errors.js +64 -3
  206. package/esm/src/platform/adapters/runtime/deno/http-server.d.ts.map +1 -1
  207. package/esm/src/platform/adapters/runtime/deno/http-server.js +2 -10
  208. package/esm/src/platform/adapters/runtime/shared/request-peer.d.ts +14 -0
  209. package/esm/src/platform/adapters/runtime/shared/request-peer.d.ts.map +1 -1
  210. package/esm/src/platform/adapters/runtime/shared/request-peer.js +82 -0
  211. package/esm/src/platform/compat/http/pinned-fetch.d.ts +1 -1
  212. package/esm/src/provider/runtime-loader/provider-http.d.ts +5 -1
  213. package/esm/src/provider/runtime-loader/provider-http.d.ts.map +1 -1
  214. package/esm/src/provider/runtime-loader/provider-http.js +55 -17
  215. package/esm/src/provider/shared/index.d.ts +1 -0
  216. package/esm/src/provider/shared/index.d.ts.map +1 -1
  217. package/esm/src/provider/shared/index.js +2 -0
  218. package/esm/src/provider/types.d.ts +1 -0
  219. package/esm/src/provider/types.d.ts.map +1 -1
  220. package/esm/src/react/components/ui/adapter/builtin/combobox.d.ts +3 -0
  221. package/esm/src/react/components/ui/adapter/builtin/combobox.d.ts.map +1 -0
  222. package/esm/src/react/components/ui/adapter/builtin/combobox.js +204 -0
  223. package/esm/src/react/components/ui/adapter/builtin/dialog.d.ts +3 -0
  224. package/esm/src/react/components/ui/adapter/builtin/dialog.d.ts.map +1 -0
  225. package/esm/src/react/components/ui/adapter/builtin/dialog.js +20 -0
  226. package/esm/src/react/components/ui/adapter/builtin/drawer.d.ts +3 -0
  227. package/esm/src/react/components/ui/adapter/builtin/drawer.d.ts.map +1 -0
  228. package/esm/src/react/components/ui/adapter/builtin/drawer.js +21 -0
  229. package/esm/src/react/components/ui/adapter/builtin/index.d.ts.map +1 -1
  230. package/esm/src/react/components/ui/adapter/builtin/index.js +10 -0
  231. package/esm/src/react/components/ui/adapter/builtin/popover.d.ts +3 -0
  232. package/esm/src/react/components/ui/adapter/builtin/popover.d.ts.map +1 -0
  233. package/esm/src/react/components/ui/adapter/builtin/popover.js +28 -0
  234. package/esm/src/react/components/ui/adapter/builtin/tabs.d.ts +3 -0
  235. package/esm/src/react/components/ui/adapter/builtin/tabs.d.ts.map +1 -0
  236. package/esm/src/react/components/ui/adapter/builtin/tabs.js +73 -0
  237. package/esm/src/react/components/ui/adapter/context.d.ts.map +1 -1
  238. package/esm/src/react/components/ui/adapter/context.js +10 -0
  239. package/esm/src/react/components/ui/adapter/contract.d.ts +212 -0
  240. package/esm/src/react/components/ui/adapter/contract.d.ts.map +1 -1
  241. package/esm/src/react/components/ui/adapter/token-scope.d.ts +24 -0
  242. package/esm/src/react/components/ui/adapter/token-scope.d.ts.map +1 -0
  243. package/esm/src/react/components/ui/adapter/token-scope.js +26 -0
  244. package/esm/src/react/components/ui/alert-dialog.d.ts +101 -0
  245. package/esm/src/react/components/ui/alert-dialog.d.ts.map +1 -0
  246. package/esm/src/react/components/ui/alert-dialog.js +181 -0
  247. package/esm/src/react/components/ui/aspect-ratio.d.ts +18 -0
  248. package/esm/src/react/components/ui/aspect-ratio.d.ts.map +1 -0
  249. package/esm/src/react/components/ui/aspect-ratio.js +13 -0
  250. package/esm/src/react/components/ui/autocomplete.d.ts +67 -0
  251. package/esm/src/react/components/ui/autocomplete.d.ts.map +1 -0
  252. package/esm/src/react/components/ui/autocomplete.js +59 -0
  253. package/esm/src/react/components/ui/breadcrumb.d.ts +86 -0
  254. package/esm/src/react/components/ui/breadcrumb.d.ts.map +1 -0
  255. package/esm/src/react/components/ui/breadcrumb.js +67 -0
  256. package/esm/src/react/components/ui/calendar.d.ts +40 -0
  257. package/esm/src/react/components/ui/calendar.d.ts.map +1 -0
  258. package/esm/src/react/components/ui/calendar.js +170 -0
  259. package/esm/src/react/components/ui/combobox.d.ts +74 -0
  260. package/esm/src/react/components/ui/combobox.d.ts.map +1 -0
  261. package/esm/src/react/components/ui/combobox.js +100 -0
  262. package/esm/src/react/components/ui/context-menu.d.ts +116 -0
  263. package/esm/src/react/components/ui/context-menu.d.ts.map +1 -0
  264. package/esm/src/react/components/ui/context-menu.js +168 -0
  265. package/esm/src/react/components/ui/date-picker.d.ts +69 -0
  266. package/esm/src/react/components/ui/date-picker.d.ts.map +1 -0
  267. package/esm/src/react/components/ui/date-picker.js +96 -0
  268. package/esm/src/react/components/ui/dialog.d.ts +41 -11
  269. package/esm/src/react/components/ui/dialog.d.ts.map +1 -1
  270. package/esm/src/react/components/ui/dialog.js +47 -28
  271. package/esm/src/react/components/ui/drawer.d.ts +31 -8
  272. package/esm/src/react/components/ui/drawer.d.ts.map +1 -1
  273. package/esm/src/react/components/ui/drawer.js +33 -22
  274. package/esm/src/react/components/ui/dropdown-menu.d.ts.map +1 -1
  275. package/esm/src/react/components/ui/dropdown-menu.js +7 -75
  276. package/esm/src/react/components/ui/field.d.ts +70 -0
  277. package/esm/src/react/components/ui/field.d.ts.map +1 -0
  278. package/esm/src/react/components/ui/field.js +161 -0
  279. package/esm/src/react/components/ui/file-type.d.ts +9 -8
  280. package/esm/src/react/components/ui/file-type.d.ts.map +1 -1
  281. package/esm/src/react/components/ui/file-type.js +8 -8
  282. package/esm/src/react/components/ui/hover-card.d.ts +82 -0
  283. package/esm/src/react/components/ui/hover-card.d.ts.map +1 -0
  284. package/esm/src/react/components/ui/hover-card.js +140 -0
  285. package/esm/src/react/components/ui/index.d.ts +23 -2
  286. package/esm/src/react/components/ui/index.d.ts.map +1 -1
  287. package/esm/src/react/components/ui/index.js +21 -0
  288. package/esm/src/react/components/ui/input-otp.d.ts +37 -0
  289. package/esm/src/react/components/ui/input-otp.d.ts.map +1 -0
  290. package/esm/src/react/components/ui/input-otp.js +42 -0
  291. package/esm/src/react/components/ui/menu-keyboard.d.ts +8 -0
  292. package/esm/src/react/components/ui/menu-keyboard.d.ts.map +1 -0
  293. package/esm/src/react/components/ui/menu-keyboard.js +76 -0
  294. package/esm/src/react/components/ui/menubar.d.ts +98 -0
  295. package/esm/src/react/components/ui/menubar.d.ts.map +1 -0
  296. package/esm/src/react/components/ui/menubar.js +173 -0
  297. package/esm/src/react/components/ui/meter.d.ts +45 -0
  298. package/esm/src/react/components/ui/meter.d.ts.map +1 -0
  299. package/esm/src/react/components/ui/meter.js +59 -0
  300. package/esm/src/react/components/ui/modal-surface.d.ts +7 -1
  301. package/esm/src/react/components/ui/modal-surface.d.ts.map +1 -1
  302. package/esm/src/react/components/ui/modal-surface.js +23 -6
  303. package/esm/src/react/components/ui/navigation-menu.d.ts +101 -0
  304. package/esm/src/react/components/ui/navigation-menu.d.ts.map +1 -0
  305. package/esm/src/react/components/ui/navigation-menu.js +195 -0
  306. package/esm/src/react/components/ui/number-field.d.ts +46 -0
  307. package/esm/src/react/components/ui/number-field.d.ts.map +1 -0
  308. package/esm/src/react/components/ui/number-field.js +121 -0
  309. package/esm/src/react/components/ui/pagination.d.ts +77 -0
  310. package/esm/src/react/components/ui/pagination.d.ts.map +1 -0
  311. package/esm/src/react/components/ui/pagination.js +62 -0
  312. package/esm/src/react/components/ui/popover.d.ts +42 -13
  313. package/esm/src/react/components/ui/popover.d.ts.map +1 -1
  314. package/esm/src/react/components/ui/popover.js +25 -12
  315. package/esm/src/react/components/ui/scroll-area.d.ts +38 -0
  316. package/esm/src/react/components/ui/scroll-area.d.ts.map +1 -0
  317. package/esm/src/react/components/ui/scroll-area.js +35 -0
  318. package/esm/src/react/components/ui/separator.d.ts +20 -0
  319. package/esm/src/react/components/ui/separator.d.ts.map +1 -0
  320. package/esm/src/react/components/ui/separator.js +13 -0
  321. package/esm/src/react/components/ui/slider.d.ts +44 -0
  322. package/esm/src/react/components/ui/slider.d.ts.map +1 -0
  323. package/esm/src/react/components/ui/slider.js +46 -0
  324. package/esm/src/react/components/ui/status.d.ts +6 -5
  325. package/esm/src/react/components/ui/status.d.ts.map +1 -1
  326. package/esm/src/react/components/ui/status.js +4 -4
  327. package/esm/src/react/components/ui/tabs.d.ts +32 -9
  328. package/esm/src/react/components/ui/tabs.d.ts.map +1 -1
  329. package/esm/src/react/components/ui/tabs.js +46 -22
  330. package/esm/src/react/components/ui/toggle.d.ts +26 -0
  331. package/esm/src/react/components/ui/toggle.d.ts.map +1 -0
  332. package/esm/src/react/components/ui/toggle.js +29 -0
  333. package/esm/src/react/components/ui/use-isomorphic-layout-effect.d.ts +2 -1
  334. package/esm/src/react/components/ui/use-isomorphic-layout-effect.d.ts.map +1 -1
  335. package/esm/src/react/components/ui/use-isomorphic-layout-effect.js +6 -3
  336. package/esm/src/rendering/chunk-optimizer/source-imports.d.ts.map +1 -1
  337. package/esm/src/rendering/chunk-optimizer/source-imports.js +4 -114
  338. package/esm/src/rendering/orchestrator/css-candidate-manifest.d.ts.map +1 -1
  339. package/esm/src/rendering/orchestrator/css-candidate-manifest.js +30 -1
  340. package/esm/src/rendering/orchestrator/module-loader/build-failure.d.ts +10 -0
  341. package/esm/src/rendering/orchestrator/module-loader/build-failure.d.ts.map +1 -1
  342. package/esm/src/rendering/orchestrator/module-loader/build-failure.js +50 -3
  343. package/esm/src/rendering/orchestrator/module-loader/dependency-resolver.d.ts +2 -0
  344. package/esm/src/rendering/orchestrator/module-loader/dependency-resolver.d.ts.map +1 -1
  345. package/esm/src/rendering/orchestrator/module-loader/dependency-resolver.js +21 -6
  346. package/esm/src/rendering/orchestrator/module-loader/index.d.ts +2 -1
  347. package/esm/src/rendering/orchestrator/module-loader/index.d.ts.map +1 -1
  348. package/esm/src/rendering/orchestrator/module-loader/index.js +190 -7
  349. package/esm/src/rendering/orchestrator/module-loader/module-persistence.d.ts +4 -0
  350. package/esm/src/rendering/orchestrator/module-loader/module-persistence.d.ts.map +1 -1
  351. package/esm/src/rendering/orchestrator/module-loader/module-persistence.js +374 -7
  352. package/esm/src/rendering/orchestrator/pipeline.d.ts.map +1 -1
  353. package/esm/src/rendering/orchestrator/pipeline.js +12 -1
  354. package/esm/src/runs/runs-client.d.ts +2 -4
  355. package/esm/src/runs/runs-client.d.ts.map +1 -1
  356. package/esm/src/runs/schemas.d.ts +18 -8
  357. package/esm/src/runs/schemas.d.ts.map +1 -1
  358. package/esm/src/runs/schemas.js +26 -0
  359. package/esm/src/runtime/model-call-context.d.ts +6 -1
  360. package/esm/src/runtime/model-call-context.d.ts.map +1 -1
  361. package/esm/src/runtime/runtime-bridge.d.ts.map +1 -1
  362. package/esm/src/runtime/runtime-bridge.js +133 -15
  363. package/esm/src/schedule/factory.d.ts +8 -2
  364. package/esm/src/schedule/factory.d.ts.map +1 -1
  365. package/esm/src/schedule/factory.js +0 -8
  366. package/esm/src/schedule/index.d.ts +10 -1
  367. package/esm/src/schedule/index.d.ts.map +1 -1
  368. package/esm/src/schedule/index.js +9 -0
  369. package/esm/src/schedule/types.d.ts +30 -11
  370. package/esm/src/schedule/types.d.ts.map +1 -1
  371. package/esm/src/schedule/validation.d.ts +17 -0
  372. package/esm/src/schedule/validation.d.ts.map +1 -1
  373. package/esm/src/schedule/validation.js +93 -5
  374. package/esm/src/security/sandbox/worker-pool.d.ts.map +1 -1
  375. package/esm/src/security/sandbox/worker-pool.js +6 -1
  376. package/esm/src/server/bootstrap.d.ts.map +1 -1
  377. package/esm/src/server/bootstrap.js +6 -0
  378. package/esm/src/server/dev-server/index.d.ts +1 -1
  379. package/esm/src/server/dev-server/index.d.ts.map +1 -1
  380. package/esm/src/server/dev-server/server.d.ts +7 -3
  381. package/esm/src/server/dev-server/server.d.ts.map +1 -1
  382. package/esm/src/server/dev-server/server.js +12 -3
  383. package/esm/src/server/dev-server/types.d.ts +7 -1
  384. package/esm/src/server/dev-server/types.d.ts.map +1 -1
  385. package/esm/src/server/dev-server.d.ts +1 -1
  386. package/esm/src/server/dev-server.d.ts.map +1 -1
  387. package/esm/src/server/handlers/dev/framework-candidates.generated.d.ts.map +1 -1
  388. package/esm/src/server/handlers/dev/framework-candidates.generated.js +2509 -34
  389. package/esm/src/server/handlers/dev/projects/index.d.ts.map +1 -1
  390. package/esm/src/server/handlers/dev/projects/index.js +3 -0
  391. package/esm/src/server/handlers/request/agent-stream.handler.d.ts.map +1 -1
  392. package/esm/src/server/handlers/request/agent-stream.handler.js +29 -24
  393. package/esm/src/server/index.d.ts +3 -3
  394. package/esm/src/server/index.d.ts.map +1 -1
  395. package/esm/src/server/index.js +10 -5
  396. package/esm/src/server/node-handler.d.ts.map +1 -1
  397. package/esm/src/server/node-handler.js +4 -1
  398. package/esm/src/server/production-server.d.ts.map +1 -1
  399. package/esm/src/server/production-server.js +2 -1
  400. package/esm/src/server/project-env/cache.d.ts +17 -0
  401. package/esm/src/server/project-env/cache.d.ts.map +1 -1
  402. package/esm/src/server/project-env/cache.js +78 -2
  403. package/esm/src/server/project-env/fetcher.d.ts.map +1 -1
  404. package/esm/src/server/project-env/fetcher.js +24 -14
  405. package/esm/src/server/project-env/index.d.ts +1 -1
  406. package/esm/src/server/project-env/index.d.ts.map +1 -1
  407. package/esm/src/server/project-env/index.js +1 -1
  408. package/esm/src/server/project-env/internal-authorization.d.ts +7 -0
  409. package/esm/src/server/project-env/internal-authorization.d.ts.map +1 -0
  410. package/esm/src/server/project-env/internal-authorization.js +32 -0
  411. package/esm/src/server/runtime-handler/index.d.ts.map +1 -1
  412. package/esm/src/server/runtime-handler/index.js +1 -1
  413. package/esm/src/server/runtime-handler/projects-handler.d.ts +1 -1
  414. package/esm/src/server/runtime-handler/projects-handler.d.ts.map +1 -1
  415. package/esm/src/server/runtime-handler/projects-handler.js +4 -2
  416. package/esm/src/transforms/esm/http-bundler.d.ts.map +1 -1
  417. package/esm/src/transforms/esm/http-bundler.js +6 -5
  418. package/esm/src/transforms/esm/http-cache-helpers.d.ts +11 -0
  419. package/esm/src/transforms/esm/http-cache-helpers.d.ts.map +1 -1
  420. package/esm/src/transforms/esm/http-cache-helpers.js +35 -0
  421. package/esm/src/transforms/esm/http-cache.d.ts.map +1 -1
  422. package/esm/src/transforms/esm/http-cache.js +16 -10
  423. package/esm/src/transforms/esm/specifier-resolver.d.ts.map +1 -1
  424. package/esm/src/transforms/esm/specifier-resolver.js +87 -7
  425. package/esm/src/transforms/import-rewriter/strategies/alias-strategy.d.ts +2 -0
  426. package/esm/src/transforms/import-rewriter/strategies/alias-strategy.d.ts.map +1 -1
  427. package/esm/src/transforms/import-rewriter/strategies/alias-strategy.js +11 -6
  428. package/esm/src/transforms/md/compiler/md-compiler.d.ts.map +1 -1
  429. package/esm/src/transforms/md/compiler/md-compiler.js +12 -5
  430. package/esm/src/transforms/mdx/compiler/frontmatter-extractor.d.ts +2 -0
  431. package/esm/src/transforms/mdx/compiler/frontmatter-extractor.d.ts.map +1 -1
  432. package/esm/src/transforms/mdx/compiler/frontmatter-extractor.js +33 -1
  433. package/esm/src/transforms/mdx/compiler/mdx-compiler.d.ts.map +1 -1
  434. package/esm/src/transforms/mdx/compiler/mdx-compiler.js +46 -7
  435. package/esm/src/transforms/mdx/esm-module-loader/cache/index.d.ts.map +1 -1
  436. package/esm/src/transforms/mdx/esm-module-loader/cache/index.js +8 -2
  437. package/esm/src/transforms/mdx/esm-module-loader/cache-format.d.ts +1 -0
  438. package/esm/src/transforms/mdx/esm-module-loader/cache-format.d.ts.map +1 -1
  439. package/esm/src/transforms/mdx/esm-module-loader/cache-format.js +3 -0
  440. package/esm/src/transforms/mdx/esm-module-loader/loader-helpers.d.ts +4 -0
  441. package/esm/src/transforms/mdx/esm-module-loader/loader-helpers.d.ts.map +1 -1
  442. package/esm/src/transforms/mdx/esm-module-loader/loader-helpers.js +44 -8
  443. package/esm/src/transforms/mdx/esm-module-loader/missing-module.d.ts +2 -0
  444. package/esm/src/transforms/mdx/esm-module-loader/missing-module.d.ts.map +1 -1
  445. package/esm/src/transforms/mdx/esm-module-loader/missing-module.js +4 -0
  446. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/http-fallback.d.ts.map +1 -1
  447. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/http-fallback.js +5 -1
  448. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/http-fetcher.d.ts +2 -0
  449. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/http-fetcher.d.ts.map +1 -1
  450. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/http-fetcher.js +50 -11
  451. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/nested-imports.d.ts +24 -12
  452. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/nested-imports.d.ts.map +1 -1
  453. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/nested-imports.js +201 -18
  454. package/esm/src/transforms/mdx/esm-module-loader/types.d.ts +5 -0
  455. package/esm/src/transforms/mdx/esm-module-loader/types.d.ts.map +1 -1
  456. package/esm/src/transforms/mdx/esm-module-loader/utils/source-spans.d.ts +22 -2
  457. package/esm/src/transforms/mdx/esm-module-loader/utils/source-spans.d.ts.map +1 -1
  458. package/esm/src/transforms/mdx/esm-module-loader/utils/source-spans.js +2080 -129
  459. package/esm/src/transforms/mdx/esm-module-loader/utils/stub-module.d.ts +11 -1
  460. package/esm/src/transforms/mdx/esm-module-loader/utils/stub-module.d.ts.map +1 -1
  461. package/esm/src/transforms/mdx/esm-module-loader/utils/stub-module.js +32 -15
  462. package/esm/src/transforms/pipeline/stages/compile.d.ts.map +1 -1
  463. package/esm/src/transforms/pipeline/stages/compile.js +44 -0
  464. package/esm/src/transforms/shared/specifier-suffix.d.ts +25 -0
  465. package/esm/src/transforms/shared/specifier-suffix.d.ts.map +1 -0
  466. package/esm/src/transforms/shared/specifier-suffix.js +43 -0
  467. package/esm/src/trigger/index.d.ts +2 -2
  468. package/esm/src/trigger/index.d.ts.map +1 -1
  469. package/esm/src/trigger/index.js +1 -1
  470. package/esm/src/trigger/local-runner.d.ts +2 -2
  471. package/esm/src/trigger/local-runner.d.ts.map +1 -1
  472. package/esm/src/trigger/local-runner.js +11 -4
  473. package/esm/src/trigger/target.d.ts +97 -5
  474. package/esm/src/trigger/target.d.ts.map +1 -1
  475. package/esm/src/trigger/target.js +175 -5
  476. package/esm/src/utils/version-constant.d.ts +1 -1
  477. package/esm/src/utils/version-constant.js +1 -1
  478. package/esm/src/webhook/types.d.ts +21 -6
  479. package/esm/src/webhook/types.d.ts.map +1 -1
  480. package/esm/src/webhook/validation.d.ts.map +1 -1
  481. package/esm/src/webhook/validation.js +27 -39
  482. package/esm/templates/index.d.ts +1 -1
  483. package/esm/templates/index.d.ts.map +1 -1
  484. package/esm/templates/loader.d.ts +6 -6
  485. package/esm/templates/loader.d.ts.map +1 -1
  486. package/esm/templates/loader.js +34 -18
  487. package/esm/templates/manifest.generated.d.ts +3 -0
  488. package/esm/templates/manifest.generated.d.ts.map +1 -0
  489. package/esm/templates/manifest.generated.js +2 -0
  490. package/package.json +8 -8
  491. package/esm/templates/manifest.d.ts +0 -664
  492. package/esm/templates/manifest.d.ts.map +0 -1
  493. package/esm/templates/manifest.js +0 -662
@@ -1,662 +0,0 @@
1
- export default {
2
- "version": 1,
3
- "templates": {
4
- "agentic-workflow": {
5
- "files": {
6
- "agents/researcher.ts": "import { agent } from \"veryfront/agent\";\n\nexport default agent({\n id: \"researcher\",\n system:\n \"You research topics thoroughly and return structured findings. \" +\n \"Present results as clear bullet points with key facts, data, and sources.\",\n maxSteps: 3,\n});\n",
7
- "agents/writer.ts": "import { agent } from \"veryfront/agent\";\n\nexport default agent({\n id: \"writer\",\n system:\n \"You transform research notes into polished, publication-ready content. \" +\n \"Use a professional but approachable tone.\",\n maxSteps: 3,\n});\n",
8
- "app/api/workflows/[workflowId]/start/route.ts": "import { startDemoWorkflowRun } from \"../../sample-runs.ts\";\n\nexport async function POST(\n request: Request,\n context: { params: Record<string, string> },\n): Promise<Response> {\n const body = await request.json().catch(() => ({})) as {\n input?: { topic?: string };\n };\n const run = startDemoWorkflowRun(context.params.workflowId, body.input);\n\n return Response.json({\n success: true,\n runId: run.id,\n id: run.id,\n workflowId: run.workflowId,\n status: run.status,\n input: run.input,\n createdAt: run.createdAt,\n });\n}\n",
9
- "app/api/workflows/runs/[id]/approvals/[approvalId]/route.ts": "export function POST(): Response {\n return Response.json({ success: true });\n}\n",
10
- "app/api/workflows/runs/[id]/route.ts": "import { getDemoWorkflowRun } from \"../../sample-runs.ts\";\n\nexport function GET(\n _request: Request,\n context: { params: Record<string, string> },\n): Response {\n return Response.json(getDemoWorkflowRun(context.params.id));\n}\n",
11
- "app/api/workflows/runs/route.ts": "import { listDemoWorkflowRuns } from \"../sample-runs.ts\";\n\nexport function GET(request: Request): Response {\n const url = new URL(request.url);\n const workflowId = url.searchParams.get(\"workflowId\");\n const limit = Number(url.searchParams.get(\"limit\") ?? \"20\");\n\n const runs = listDemoWorkflowRuns({ workflowId, limit });\n\n return Response.json({\n runs,\n totalCount: runs.length,\n });\n}\n",
12
- "app/api/workflows/sample-runs.ts": "/** Node statuses the framework reports; the UI keys its icons off these. */\nexport type DemoNodeStatus = \"pending\" | \"running\" | \"completed\" | \"failed\" | \"skipped\";\n\nexport interface DemoNodeState {\n nodeId: string;\n status: DemoNodeStatus;\n attempt: number;\n output?: string | Record<string, unknown>;\n}\n\nexport interface DemoWorkflowRun {\n id: string;\n workflowId: string;\n status: \"pending\" | \"running\" | \"completed\" | \"waiting_for_approval\" | \"failed\";\n input: { topic: string };\n createdAt: string;\n currentNodes: string[];\n nodeStates: Record<string, DemoNodeState>;\n pendingApprovals: Array<{ id: string; status: \"pending\" | \"approved\" | \"rejected\" }>;\n}\n\nconst globalStore = globalThis as typeof globalThis & {\n __veryfrontAgenticWorkflowDemoRuns?: Map<string, DemoWorkflowRun>;\n};\nconst demoRuns = globalStore.__veryfrontAgenticWorkflowDemoRuns ??= new Map<\n string,\n DemoWorkflowRun\n>();\n\nexport function createDemoWorkflowRun(\n id = \"test-run\",\n topic = \"Example content pipeline\",\n workflowId = \"content-pipeline\",\n): DemoWorkflowRun {\n return {\n id,\n workflowId,\n status: \"completed\",\n input: { topic },\n createdAt: new Date().toISOString(),\n currentNodes: [],\n nodeStates: {\n research: {\n nodeId: \"research\",\n status: \"completed\",\n attempt: 1,\n output: \"Found key points and source material.\",\n },\n \"write-article\": {\n nodeId: \"write-article\",\n status: \"completed\",\n attempt: 1,\n output: \"Drafted a concise article from the research notes.\",\n },\n \"editorial-review\": {\n nodeId: \"editorial-review\",\n status: \"completed\",\n attempt: 1,\n },\n publish: {\n nodeId: \"publish\",\n status: \"completed\",\n attempt: 1,\n output: { published: true },\n },\n },\n pendingApprovals: [],\n };\n}\n\nexport function getDemoWorkflowRun(id: string): DemoWorkflowRun {\n return demoRuns.get(id) ?? createDemoWorkflowRun(id);\n}\n\nexport function listDemoWorkflowRuns(options: {\n workflowId?: string | null;\n limit?: number;\n} = {}): DemoWorkflowRun[] {\n if (!demoRuns.has(\"test-run\")) {\n demoRuns.set(\"test-run\", createDemoWorkflowRun());\n }\n\n const limit = Number.isFinite(options.limit) && options.limit && options.limit > 0\n ? options.limit\n : 20;\n\n return Array.from(demoRuns.values())\n .filter((run) => !options.workflowId || run.workflowId === options.workflowId)\n .sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt))\n .slice(0, limit);\n}\n\nexport function startDemoWorkflowRun(\n workflowId: string,\n input: { topic?: string } = {},\n): DemoWorkflowRun {\n const runId = `run-${Date.now()}`;\n const topic = input.topic?.trim() || \"Untitled workflow\";\n const run = createDemoWorkflowRun(runId, topic, workflowId);\n\n demoRuns.set(run.id, run);\n return run;\n}\n",
13
- "app/layout.tsx": "import \"../globals.css\";\nimport { Head } from \"veryfront/head\";\n\nexport default function RootLayout({\n children,\n}: {\n children: React.ReactNode;\n}): React.ReactNode {\n return (\n <>\n <Head>\n <title>AI Workflows</title>\n <link rel=\"icon\" href=\"/favicon.svg\" type=\"image/svg+xml\" />\n </Head>\n {children}\n </>\n );\n}\n",
14
- "app/page.tsx": "'use client'\n\nimport { useState } from 'react'\nimport { useWorkflowStart, useWorkflowList } from 'veryfront/workflow'\n\nconst STATUS_STYLES: Record<string, string> = {\n running: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400',\n completed: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400',\n waiting_for_approval: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400',\n failed: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400',\n pending: 'bg-neutral-100 text-neutral-600 dark:bg-neutral-800 dark:text-neutral-400',\n}\n\n/** A run's input is workflow-defined, so narrow it before reading `topic`. */\nfunction topicOf(input: unknown): string {\n return typeof input === 'object' && input !== null && 'topic' in input\n ? String((input as { topic: unknown }).topic)\n : ''\n}\n\nexport default function WorkflowDashboard(): React.JSX.Element {\n const [topic, setTopic] = useState('')\n const { start, isStarting } = useWorkflowStart({ workflowId: 'content-pipeline' })\n const { runs, isLoading, refresh } = useWorkflowList()\n\n async function handleStart(e: React.FormEvent) {\n e.preventDefault()\n if (!topic.trim()) return\n await start({ topic: topic.trim() })\n setTopic('')\n await refresh()\n }\n\n return (\n <div className=\"min-h-screen bg-neutral-50 dark:bg-neutral-950\">\n <div className=\"max-w-2xl mx-auto px-4 py-12\">\n <div className=\"mb-10\">\n <h1 className=\"text-2xl font-bold text-neutral-900 dark:text-white\">Content Pipeline</h1>\n <p className=\"mt-1 text-neutral-500 dark:text-neutral-400\">Research &rarr; Write &rarr; Review &rarr; Publish</p>\n </div>\n\n {/* Start new workflow */}\n <form onSubmit={handleStart} className=\"mb-10\">\n <div className=\"flex gap-3\">\n <input\n type=\"text\"\n value={topic}\n onChange={(e) => setTopic(e.target.value)}\n placeholder=\"Enter a topic to research and write about...\"\n className=\"flex-1 px-4 py-2.5 bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl text-neutral-900 dark:text-white placeholder-neutral-400 focus:outline-none focus:ring-2 focus:ring-blue-500/30 focus:border-blue-500\"\n />\n <button\n type=\"submit\"\n disabled={isStarting || !topic.trim()}\n className=\"px-5 py-2.5 bg-blue-500 text-white font-medium rounded-xl hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors\"\n >\n {isStarting ? 'Starting...' : 'Start'}\n </button>\n </div>\n </form>\n\n {/* Workflow runs */}\n <div>\n <h2 className=\"text-sm font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wider mb-4\">Recent Runs</h2>\n\n {isLoading ? (\n <p className=\"text-neutral-400 text-sm py-8 text-center\">Loading...</p>\n ) : runs.length === 0 ? (\n <div className=\"text-center py-12 bg-white dark:bg-neutral-900 rounded-2xl border border-neutral-200 dark:border-neutral-800\">\n <p className=\"text-neutral-500 dark:text-neutral-400\">No workflows yet. Start one above.</p>\n </div>\n ) : (\n <div className=\"space-y-3\">\n {runs.map((wf) => (\n <a\n key={wf.id}\n href={`/workflows/${wf.id}`}\n className=\"block bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl p-4 hover:border-neutral-300 dark:hover:border-neutral-700 transition-colors\"\n >\n <div className=\"flex items-center justify-between\">\n <div>\n <p className=\"font-medium text-neutral-900 dark:text-white text-sm\">{topicOf(wf.input) || 'Untitled'}</p>\n <p className=\"text-xs text-neutral-500 mt-1\">{new Date(wf.createdAt).toLocaleString()}</p>\n </div>\n <span className={`px-2.5 py-1 rounded-full text-xs font-medium ${STATUS_STYLES[wf.status] || STATUS_STYLES.pending}`}>\n {wf.status.replace(/_/g, ' ')}\n </span>\n </div>\n </a>\n ))}\n </div>\n )}\n </div>\n </div>\n </div>\n )\n}\n",
15
- "app/workflows/[id]/page.tsx": "'use client'\n\nimport { useState } from 'react'\nimport { usePageContext } from 'veryfront/context'\nimport { useWorkflow } from 'veryfront/workflow'\n\nconst STEP_ICONS: Record<string, string> = {\n completed: '\\u2713',\n running: '\\u25C9',\n pending: '\\u25CB',\n skipped: '\\u23F8',\n failed: '\\u2717',\n}\n\n/** A run's input is workflow-defined, so narrow it before reading `topic`. */\nfunction topicOf(input: unknown): string {\n return typeof input === 'object' && input !== null && 'topic' in input\n ? String((input as { topic: unknown }).topic)\n : ''\n}\n\nexport default function WorkflowDetail(): React.JSX.Element {\n const { params } = usePageContext()\n const { run, pendingApprovals, isLoading, refresh } = useWorkflow({ runId: params.id })\n const [isSubmitting, setIsSubmitting] = useState(false)\n\n async function handleApproval(approvalId: string, approved: boolean) {\n setIsSubmitting(true)\n try {\n await fetch(`/api/workflows/runs/${params.id}/approvals/${approvalId}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ approved, approver: 'user' }),\n })\n await refresh()\n } finally {\n setIsSubmitting(false)\n }\n }\n\n if (isLoading) {\n return (\n <div className=\"min-h-screen flex items-center justify-center bg-neutral-50 dark:bg-neutral-950\">\n <p className=\"text-neutral-400\">Loading workflow...</p>\n </div>\n )\n }\n\n if (!run) {\n return (\n <div className=\"min-h-screen flex items-center justify-center bg-neutral-50 dark:bg-neutral-950\">\n <p className=\"text-neutral-400\">Workflow not found</p>\n </div>\n )\n }\n\n return (\n <div className=\"min-h-screen bg-neutral-50 dark:bg-neutral-950\">\n <div className=\"max-w-2xl mx-auto px-4 py-12\">\n <a href=\"/\" className=\"text-sm text-neutral-500 hover:text-neutral-700 dark:hover:text-neutral-300 mb-6 inline-block\">&larr; Back</a>\n\n <h1 className=\"text-2xl font-bold text-neutral-900 dark:text-white mb-1\">{topicOf(run.input) || 'Workflow'}</h1>\n <p className=\"text-sm text-neutral-500 dark:text-neutral-400 mb-8\">Started {new Date(run.createdAt).toLocaleString()}</p>\n\n {/* One card per workflow node, in definition order */}\n <div className=\"space-y-4 mb-8\">\n {Object.entries(run.nodeStates).map(([nodeId, node]) => (\n <div key={nodeId} className=\"flex items-start gap-3 bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl p-4\">\n <span className=\"text-lg mt-0.5\">{STEP_ICONS[node.status] || '\\u25CB'}</span>\n <div className=\"flex-1\">\n <p className=\"font-medium text-neutral-900 dark:text-white text-sm\">{nodeId}</p>\n {node.output !== undefined && (\n <p className=\"text-xs text-neutral-500 mt-1 line-clamp-2\">{typeof node.output === 'string' ? node.output : JSON.stringify(node.output)}</p>\n )}\n </div>\n <span className=\"text-xs text-neutral-400\">{node.status}</span>\n </div>\n ))}\n </div>\n\n {/* Approval */}\n {pendingApprovals.length > 0 && (\n <div className=\"bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-xl p-6\">\n <h2 className=\"font-medium text-amber-900 dark:text-amber-200 mb-2\">Approval Required</h2>\n <p className=\"text-sm text-amber-700 dark:text-amber-300 mb-4\">Review the draft before publishing.</p>\n <div className=\"flex gap-3\">\n <button\n type=\"button\"\n onClick={() => handleApproval(pendingApprovals[0].id, true)}\n disabled={isSubmitting}\n className=\"px-4 py-2 bg-emerald-500 text-white font-medium rounded-lg hover:bg-emerald-600 disabled:opacity-50 transition-colors text-sm\"\n >\n Approve\n </button>\n <button\n type=\"button\"\n onClick={() => handleApproval(pendingApprovals[0].id, false)}\n disabled={isSubmitting}\n className=\"px-4 py-2 bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 text-neutral-700 dark:text-neutral-300 font-medium rounded-lg hover:bg-neutral-50 dark:hover:bg-neutral-700 disabled:opacity-50 transition-colors text-sm\"\n >\n Reject\n </button>\n </div>\n </div>\n )}\n </div>\n </div>\n )\n}\n",
16
- "globals.css": "@import \"tailwindcss\";\n",
17
- "globals.d.ts": "declare module \"*.css\";\n",
18
- "public/favicon.svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 64 64\">\n <rect width=\"64\" height=\"64\" fill=\"#fff\"/>\n <circle cx=\"32\" cy=\"32\" r=\"20\" fill=\"#000\"/>\n</svg>\n",
19
- "README.md": "# Agentic Workflow\n\nOrchestrated multi-step processes with human approval gates.\n\n## What's included\n\n- Content pipeline workflow (research, write, review, publish)\n- Parallel step execution\n- Human-in-the-loop approval gates\n- Dashboard to start, monitor, and approve workflow runs\n\n## Structure\n\n```\nagents/\n researcher.ts Research agent\n writer.ts Writing agent\nworkflows/content-pipeline.ts Workflow definition\napp/\n api/workflows/ Demo workflow API routes\n page.tsx Workflow dashboard\n workflows/[id]/page.tsx Run detail and approval UI\n```\n\nThis starter is not production-ready.\n",
20
- "tools/publish.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\n\n/**\n * Final step of `workflows/content-pipeline.ts`.\n *\n * Replace the body with a call to your CMS, static site build, or storage\n * bucket. The workflow reaches this step only after the approval gate passes.\n */\nexport default tool({\n id: \"publish\",\n description: \"Publish an approved draft\",\n inputSchema: defineSchema((v) =>\n v.object({\n title: v.string().default(\"Untitled\").describe(\"Headline of the article\"),\n })\n )(),\n execute: ({ title }) => ({\n published: true,\n title,\n url: `/articles/${Date.now()}`,\n }),\n});\n",
21
- "tsconfig.json": "{\n \"compilerOptions\": {\n \"target\": \"ES2022\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"bundler\",\n \"strict\": true,\n \"jsx\": \"react-jsx\",\n \"skipLibCheck\": true,\n \"esModuleInterop\": true,\n \"noEmit\": true,\n \"allowImportingTsExtensions\": true,\n \"paths\": {\n \"@/*\": [\"./*\"]\n }\n },\n \"include\": [\"**/*.ts\", \"**/*.tsx\"],\n \"exclude\": [\"node_modules\"]\n}\n",
22
- "workflows/content-pipeline.ts": "import { workflow, step, parallel, waitForApproval } from \"veryfront/workflow\";\n\n/** Typing the input makes `input.topic` available to every step below. */\ninterface ContentPipelineInput {\n topic: string;\n}\n\nexport default workflow<ContentPipelineInput>({\n id: \"content-pipeline\",\n description: \"Research, write, review, and publish content\",\n steps: ({ input }) => [\n step(\"research\", {\n agent: \"researcher\",\n input: { topic: input.topic },\n }),\n\n parallel(\"draft\", [\n step(\"write-article\", { agent: \"writer\" }),\n step(\"write-summary\", { agent: \"writer\", input: { format: \"summary\" } }),\n ]),\n\n waitForApproval(\"editorial-review\", {\n message: \"Review the draft before publishing\",\n timeout: \"24h\",\n }),\n\n // Every step runs an agent or a tool. `tools/publish.ts` is where the\n // publishing logic lives.\n step(\"publish\", { tool: \"publish\" }),\n ],\n});\n"
23
- }
24
- },
25
- "ai-agent": {
26
- "files": {
27
- "agents/assistant.ts": "import { agent } from \"veryfront/agent\";\n\nexport default agent({\n id: \"assistant\",\n name: \"Assistant\",\n description: \"Turn a rough idea into a clear next move.\",\n system:\n \"Be direct and practical. Use the calculator tool for arithmetic instead of calculating mentally, and answer as soon as you have the result. For currency splits use the calculator's split operation, then state every share it returns. Write numbers in plain text, using x and / for operators, never in LaTeX or MathJax.\",\n tools: { calculator: true },\n maxSteps: 20,\n suggestions: [\n {\n type: \"prompt\",\n title: \"Shape an idea\",\n prompt: \"Turn this rough idea into a focused plan with the first three steps: \",\n },\n {\n type: \"prompt\",\n title: \"Run the numbers\",\n prompt:\n \"Calculate an 18% tip on $84.50, split the total among three people, and explain the result briefly.\",\n },\n ],\n});\n",
28
- "app/api/ag-ui/route.ts": "import { createAgUiHandler } from \"veryfront/agent\";\n\nexport const POST = createAgUiHandler(\"assistant\");\n",
29
- "app/layout.tsx": "import \"../globals.css\";\nimport { Head } from \"veryfront/head\";\n\nexport default function RootLayout({\n children,\n}: {\n children: React.ReactNode;\n}): React.ReactNode {\n return (\n <>\n <Head>\n <title>Assistant</title>\n <link rel=\"icon\" href=\"/favicon.svg\" type=\"image/svg+xml\" />\n </Head>\n {children}\n </>\n );\n}\n",
30
- "app/markdown-renderer.tsx": "\"use client\";\n\nimport ReactMarkdown from \"react-markdown@9.0.3\";\nimport remarkGfm from \"remark-gfm@4.0.1\";\nimport type { MarkdownRendererProps } from \"veryfront/markdown\";\n\n/**\n * Rich Markdown for assistant answers.\n *\n * `veryfront/markdown` presents plain source until a renderer is installed, so\n * this component supplies one. Swap in any renderer that accepts\n * `MarkdownRendererProps` to change how answers are parsed and rendered.\n */\nexport function MarkdownRenderer({ source }: MarkdownRendererProps): React.JSX.Element {\n return <ReactMarkdown remarkPlugins={[remarkGfm]}>{source}</ReactMarkdown>;\n}\n",
31
- "app/page.tsx": "\"use client\";\n\nimport { Chat } from \"veryfront/chat\";\nimport { MarkdownRendererProvider } from \"veryfront/markdown\";\nimport { MarkdownRenderer } from \"./markdown-renderer.tsx\";\n\nexport default function ChatPage(): React.JSX.Element {\n return (\n <MarkdownRendererProvider renderer={MarkdownRenderer}>\n <Chat agentId=\"assistant\" className=\"h-screen\" />\n </MarkdownRendererProvider>\n );\n}\n",
32
- "evals/assistant.eval.ts": "import { datasets, evalAgent, judges, metrics } from \"veryfront/eval\";\n\nexport default evalAgent({\n name: \"Assistant smoke test\",\n target: \"agent:assistant\",\n\n dataset: datasets.inline([\n {\n id: \"calculator\",\n input:\n \"Calculate an 18% tip on $84.50, split the total among three people, and explain the result briefly.\",\n reference:\n \"The tip is $15.21 and the total is $99.71. Two people pay $33.24 and one pays $33.23.\",\n },\n ]),\n\n metrics: [\n metrics.agent.calledTool(\"calculator\").gate(),\n\n metrics.agent.noFailedTools().gate(),\n\n metrics.judge.rubric({\n rubric: [\n \"The answer must state a tip of $15.21 and a total of $99.71.\",\n \"It must split the total into $33.24, $33.24, and $33.23.\",\n ].join(\" \"),\n judge: judges.llm.rubric(),\n }).gate({ min: 0.8 }),\n ],\n});\n",
33
- "globals.css": "@import \"tailwindcss\";\n",
34
- "globals.d.ts": "declare module \"*.css\";\n",
35
- "public/favicon.svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 64 64\">\n <rect width=\"64\" height=\"64\" fill=\"#fff\"/>\n <circle cx=\"32\" cy=\"32\" r=\"20\" fill=\"#000\"/>\n</svg>\n",
36
- "README.md": "# AI Agent\n\nA small, customizable agent with a streaming chat UI and tool support.\n\n## What's included\n\n- Single assistant agent with streaming chat UI\n- Example calculator tool\n- Smoke eval for the agent and calculator\n- App-mode `Chat` component for real-time responses\n\n## Structure\n\n```\nagents/assistant.ts Agent definition\ntools/calculator.ts Example tool\nevals/assistant.eval.ts Agent smoke eval\napp/\n api/ag-ui/route.ts AG-UI endpoint\n page.tsx Chat interface\n```\n\n## Customize\n\n- Edit `agents/assistant.ts` to change the agent's identity, instructions, and suggestions.\n- Add or replace files in `tools/` to give the agent new capabilities.\n- Update `evals/assistant.eval.ts`, then run `npm run eval -- assistant`.\n- Edit `app/page.tsx` when you need to customize the chat UI.\n",
37
- "tools/calculator.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\n\nexport default tool({\n id: \"calculator\",\n description:\n \"Perform one arithmetic operation on two numbers. Use split to divide a money amount a into b shares that add up to it exactly.\",\n inputSchema: defineSchema((v) =>\n v.object({\n operation: v.enum([\"add\", \"subtract\", \"multiply\", \"divide\", \"split\"]),\n a: v.number(),\n b: v.number(),\n })\n )(),\n execute: ({ operation, a, b }) => {\n if ((operation === \"divide\" || operation === \"split\") && b === 0) {\n throw new Error(\"Cannot divide by zero\");\n }\n\n if (operation === \"split\") {\n const parts = Math.max(1, Math.trunc(Math.abs(b)));\n if (parts > 1000) throw new Error(\"Cannot split into more than 1000 shares\");\n\n const cents = Math.round(a * 100);\n const base = Math.trunc(cents / parts);\n const remainder = Math.abs(cents - base * parts);\n return {\n result: Array.from(\n { length: parts },\n (_, index) => (base + (index < remainder ? Math.sign(cents) : 0)) / 100,\n ),\n };\n }\n\n if (operation === \"add\") return { result: a + b };\n if (operation === \"subtract\") return { result: a - b };\n if (operation === \"multiply\") return { result: a * b };\n return { result: a / b };\n },\n});\n",
38
- "tsconfig.json": "{\n \"compilerOptions\": {\n \"target\": \"ES2022\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"bundler\",\n \"strict\": true,\n \"jsx\": \"react-jsx\",\n \"skipLibCheck\": true,\n \"esModuleInterop\": true,\n \"noEmit\": true,\n \"allowImportingTsExtensions\": true,\n \"paths\": {\n \"@/*\": [\n \"./*\"\n ],\n \"react-markdown@*\": [\n \"./node_modules/react-markdown\"\n ],\n \"remark-gfm@*\": [\n \"./node_modules/remark-gfm\"\n ]\n }\n },\n \"include\": [\n \"**/*.ts\",\n \"**/*.tsx\"\n ],\n \"exclude\": [\n \"node_modules\"\n ]\n}\n"
39
- }
40
- },
41
- "coding-agent": {
42
- "files": {
43
- "agents/coder.ts": "import { agent } from \"veryfront/agent\";\n\nexport default agent({\n id: \"coder\",\n name: \"Code Agent\",\n description: \"Read, search, and edit project files.\",\n system: `You are an expert coding assistant. You can read, search, and modify code files in the project.\n\nWhen asked to make changes:\n1. First read the relevant files to understand the codebase\n2. Explain what you'll change and why\n3. Make the changes\n4. Verify the result\n\nAlways explain your reasoning before making edits.`,\n tools: true,\n maxSteps: 15,\n suggestions: [\n {\n type: \"prompt\",\n title: \"Explain the codebase\",\n prompt: \"Explain how this project is organized.\",\n },\n {\n type: \"prompt\",\n title: \"Make a change\",\n prompt: \"Make this code change: \",\n },\n ],\n});\n",
44
- "app/api/ag-ui/route.ts": "import { createAgUiHandler } from \"veryfront/agent\";\n\nexport const POST = createAgUiHandler(\"coder\");\n",
45
- "app/layout.tsx": "import \"../globals.css\";\nimport { Head } from \"veryfront/head\";\n\nexport default function RootLayout({\n children,\n}: {\n children: React.ReactNode;\n}): React.ReactNode {\n return (\n <>\n <Head>\n <title>Code Agent</title>\n <link rel=\"icon\" href=\"/favicon.svg\" type=\"image/svg+xml\" />\n </Head>\n <div className=\"dark\">\n <div className=\"flex flex-col h-screen bg-neutral-950\">\n <header className=\"flex-shrink-0 border-b border-neutral-800\">\n <div className=\"max-w-4xl mx-auto flex items-center gap-3 px-4 py-3\">\n <div className=\"w-8 h-8 rounded-lg bg-emerald-500/10 flex items-center justify-center\">\n <svg\n className=\"w-4 h-4 text-emerald-400\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n strokeWidth={2}\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n d=\"M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5\"\n />\n </svg>\n </div>\n <div>\n <h1 className=\"font-medium text-white text-sm font-mono\">\n code-agent\n </h1>\n <p className=\"text-xs text-neutral-500\">\n read, search, edit project files\n </p>\n </div>\n <div className=\"ml-auto flex items-center gap-1.5\">\n <span className=\"w-2 h-2 rounded-full bg-emerald-400 animate-pulse\" />\n <span className=\"text-xs text-neutral-500 font-mono\">ready</span>\n </div>\n </div>\n </header>\n {children}\n </div>\n </div>\n </>\n );\n}\n",
46
- "app/markdown-renderer.tsx": "'use client'\n\nimport ReactMarkdown from 'react-markdown@9.0.3'\nimport remarkGfm from 'remark-gfm@4.0.1'\nimport type { MarkdownRendererProps } from 'veryfront/markdown'\n\n/**\n * Rich Markdown for assistant answers.\n *\n * `veryfront/markdown` presents plain source until a renderer is installed, so\n * this component supplies one. Swap in any renderer that accepts\n * `MarkdownRendererProps` to change how answers are parsed and rendered.\n */\nexport function MarkdownRenderer({ source }: MarkdownRendererProps): React.JSX.Element {\n return <ReactMarkdown remarkPlugins={[remarkGfm]}>{source}</ReactMarkdown>\n}\n",
47
- "app/page.tsx": "'use client'\n\nimport { Chat } from 'veryfront/chat'\nimport { MarkdownRendererProvider } from 'veryfront/markdown'\nimport { MarkdownRenderer } from './markdown-renderer.tsx'\n\nexport default function CodeAgent(): React.JSX.Element {\n return (\n <MarkdownRendererProvider renderer={MarkdownRenderer}>\n <Chat\n agentId=\"coder\"\n api=\"/api/ag-ui\"\n className=\"flex-1 min-h-0\"\n placeholder=\"Describe what you want to build or fix...\"\n />\n </MarkdownRendererProvider>\n )\n}\n",
48
- "globals.css": "@import \"tailwindcss\";\n",
49
- "globals.d.ts": "declare module \"*.css\";\n",
50
- "public/favicon.svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 64 64\">\n <rect width=\"64\" height=\"64\" fill=\"#fff\"/>\n <circle cx=\"32\" cy=\"32\" r=\"20\" fill=\"#000\"/>\n</svg>\n",
51
- "README.md": "# Coding Agent\n\nAn AI assistant that can read, understand, and modify project files.\n\n## What's included\n\n- Coder agent with file system tools\n- Read, list, and edit files through conversation\n- Safe search/replace editing pattern\n\n## Structure\n\n```\nagents/coder.ts Agent with coding instructions\ntools/\n read-file.ts Read file contents\n list-files.ts List directory contents\n edit-file.ts Search and replace in files\napp/\n api/ag-ui/route.ts AG-UI endpoint\n page.tsx Chat interface\n```\n\nThis starter is not production-ready.\n",
52
- "tools/edit-file.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { cwd, readTextFile, realPath, resolve, writeTextFile } from \"veryfront/fs\";\n\n/** True when `target` is the same as, or nested under, `root` (both canonical). */\nfunction isWithin(root: string, target: string): boolean {\n const r = root.replace(/\\\\/g, \"/\");\n const t = target.replace(/\\\\/g, \"/\");\n return t === r || t.startsWith(`${r}/`);\n}\n\nexport default tool({\n id: \"edit-file\",\n description: \"Edit a file by replacing a specific string with new content\",\n inputSchema: defineSchema((v) => v.object({\n path: v.string().describe(\"File path relative to the project root\"),\n search: v.string().describe(\"Exact string to find in the file\"),\n replace: v.string().describe(\"String to replace it with\"),\n }))(),\n execute: async ({ path, search, replace }) => {\n let projectDir: string;\n let absolute: string;\n try {\n // Canonicalize both sides so a symlink that points outside the project\n // is resolved to its real target before the containment check.\n projectDir = await realPath(cwd());\n absolute = await realPath(resolve(projectDir, path));\n } catch {\n return { error: `File not found: ${path}` };\n }\n if (!isWithin(projectDir, absolute)) {\n return { error: `Path escapes project directory: ${path}` };\n }\n\n const content = await readTextFile(absolute);\n if (!content.includes(search)) {\n return { error: \"Search string not found in file\" };\n }\n\n const updated = content.replace(search, replace);\n await writeTextFile(absolute, updated);\n return { path, success: true };\n },\n});\n",
53
- "tools/list-files.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { cwd, readDir, realPath, resolve } from \"veryfront/fs\";\n\n/** True when `target` is the same as, or nested under, `root` (both canonical). */\nfunction isWithin(root: string, target: string): boolean {\n const r = root.replace(/\\\\/g, \"/\");\n const t = target.replace(/\\\\/g, \"/\");\n return t === r || t.startsWith(`${r}/`);\n}\n\nexport default tool({\n id: \"list-files\",\n description: \"List files in a project directory\",\n inputSchema: defineSchema((v) => v.object({\n directory: v\n .string()\n .default(\".\")\n .describe(\"Directory path relative to project root\"),\n extensions: v\n .array(v.string())\n .optional()\n .describe(\"Filter by file extensions (e.g. ['.ts', '.tsx'])\"),\n }))(),\n execute: async ({ directory, extensions }) => {\n let projectDir: string;\n let absolute: string;\n try {\n // Canonicalize both sides so a symlink that points outside the project\n // is resolved to its real target before the containment check.\n projectDir = await realPath(cwd());\n absolute = await realPath(resolve(projectDir, directory));\n } catch {\n return { error: `Directory not found: ${directory}` };\n }\n if (!isWithin(projectDir, absolute)) {\n return { error: `Path escapes project directory: ${directory}` };\n }\n\n // `readDir` streams entries, so collect the files before filtering.\n let files: string[] = [];\n for await (const entry of readDir(absolute)) {\n if (entry.isFile) files.push(entry.name);\n }\n\n if (extensions?.length) {\n files = files.filter((file) => extensions.some((ext) => file.endsWith(ext)));\n }\n\n return { directory, files, count: files.length };\n },\n});\n",
54
- "tools/read-file.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { cwd, readTextFile, realPath, resolve } from \"veryfront/fs\";\n\n/** True when `target` is the same as, or nested under, `root` (both canonical). */\nfunction isWithin(root: string, target: string): boolean {\n const r = root.replace(/\\\\/g, \"/\");\n const t = target.replace(/\\\\/g, \"/\");\n return t === r || t.startsWith(`${r}/`);\n}\n\nexport default tool({\n id: \"read-file\",\n description: \"Read the contents of a file in the project\",\n inputSchema: defineSchema((v) => v.object({\n path: v.string().describe(\"File path relative to the project root\"),\n }))(),\n execute: async ({ path }) => {\n let projectDir: string;\n let absolute: string;\n try {\n // Canonicalize both sides so a symlink that points outside the project\n // is resolved to its real target before the containment check.\n projectDir = await realPath(cwd());\n absolute = await realPath(resolve(projectDir, path));\n } catch {\n return { error: `File not found: ${path}` };\n }\n if (!isWithin(projectDir, absolute)) {\n return { error: `Path escapes project directory: ${path}` };\n }\n const content = await readTextFile(absolute);\n return { path, content };\n },\n});\n",
55
- "tsconfig.json": "{\n \"compilerOptions\": {\n \"target\": \"ES2022\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"bundler\",\n \"strict\": true,\n \"jsx\": \"react-jsx\",\n \"skipLibCheck\": true,\n \"esModuleInterop\": true,\n \"noEmit\": true,\n \"allowImportingTsExtensions\": true,\n \"paths\": {\n \"@/*\": [\n \"./*\"\n ],\n \"react-markdown@*\": [\n \"./node_modules/react-markdown\"\n ],\n \"remark-gfm@*\": [\n \"./node_modules/remark-gfm\"\n ]\n }\n },\n \"include\": [\n \"**/*.ts\",\n \"**/*.tsx\"\n ],\n \"exclude\": [\n \"node_modules\"\n ]\n}\n"
56
- }
57
- },
58
- "docs-agent": {
59
- "files": {
60
- "agents/rag.ts": "import { agent } from \"veryfront/agent\";\n\nexport default agent({\n id: \"rag\",\n name: \"Docs Agent\",\n description: \"Upload files and ask questions.\",\n system:\n `You answer questions using the provided documents. ` +\n `Always cite your sources by referencing the document title. ` +\n `If the search results don't contain a clear answer, say so honestly.`,\n suggestions: [\n {\n type: \"prompt\",\n title: \"Ask Question\",\n prompt: \"I have a question about this document: \",\n },\n {\n type: \"prompt\",\n title: \"Extract Insights\",\n prompt: \"Extract the key insights from the uploaded documents.\",\n },\n {\n type: \"prompt\",\n title: \"Find Sources\",\n prompt: \"Find relevant sources and references in the documents for: \",\n },\n ],\n});\n",
61
- "app/api/ag-ui/route.ts": "import { createAgUiHandler } from \"veryfront/agent\";\nimport { store } from \"../../../store.ts\";\n\nexport const POST = createAgUiHandler(\"rag\", {\n beforeStream: async ({ lastUserText }) => {\n const query = lastUserText.trim();\n if (!query) return;\n\n try {\n const results = await store.search(query, { topK: 5 });\n if (results.length === 0) return;\n\n const contextBlock = results\n .map(\n (result) =>\n `[${result.title}] (score: ${\n result.score.toFixed(2)\n })\\n${result.text}`,\n )\n .join(\"\\n\\n---\\n\\n\");\n\n return {\n prepend: [\n {\n role: \"system\",\n parts: [\n {\n type: \"text\",\n text:\n `Here are relevant documents retrieved for your question:\\n\\n${contextBlock}\\n\\n` +\n \"Use these documents to answer. Cite the document title when referencing information.\",\n },\n ],\n },\n ],\n };\n } catch (e) {\n console.error(\"[RAG] Retrieval failed:\", e);\n return;\n }\n },\n});\n",
62
- "app/api/ingest/route.ts": "import { store } from \"../../../store.ts\";\n\nexport async function POST() {\n await store.indexContentDir();\n return Response.json({ ok: true });\n}\n",
63
- "app/api/uploads/route.ts": "import { createUploadHandler } from \"veryfront/embedding\";\nimport { store } from \"../../../store.ts\";\n\nexport const { POST, GET, DELETE } = createUploadHandler(store, {\n auth: { type: \"none\", allowUnauthenticated: true },\n});\n",
64
- "app/layout.tsx": "'use client'\n\nimport \"../globals.css\";\nimport {\n AppShell,\n ChatSidebar,\n ChatThemeScope,\n ConversationsProvider,\n Tabs,\n TabsItem,\n} from \"veryfront/chat\";\nimport { Head } from \"veryfront/head\";\nimport { useRouter } from \"veryfront/router\";\n\nexport default function RootLayout({ children }: { children: React.ReactNode }): React.ReactNode {\n const router = useRouter();\n const activeTab = router.pathname.startsWith(\"/uploads\") ? \"uploads\" : \"chat\";\n\n return (\n <>\n <Head>\n <title>Docs Agent</title>\n <link rel=\"icon\" href=\"/favicon.svg\" type=\"image/svg+xml\" />\n </Head>\n <ChatThemeScope className=\"flex flex-col h-screen\">\n <ConversationsProvider storageKey=\"rag-conversations\">\n <AppShell className=\"flex-1 min-h-0\">\n <AppShell.Sidebar side=\"left\" className=\"border-r border-[var(--outline-border)]\">\n <AppShell.SidebarContent className=\"p-0\">\n <ChatSidebar.Root>\n <ChatSidebar.NewButton />\n <ChatSidebar.List />\n </ChatSidebar.Root>\n </AppShell.SidebarContent>\n </AppShell.Sidebar>\n <AppShell.Main>\n <AppShell.Header border className=\"h-16 gap-3 px-3\">\n <AppShell.Trigger side=\"left\" />\n <div className=\"flex flex-1 justify-center\">\n <Tabs\n value={activeTab}\n onValueChange={(value: string) =>\n router.push(value === \"uploads\" ? \"/uploads\" : \"/\")}\n >\n <TabsItem value=\"chat\">Chat</TabsItem>\n <TabsItem value=\"uploads\">Uploads</TabsItem>\n </Tabs>\n </div>\n </AppShell.Header>\n <AppShell.Content className=\"flex flex-col min-h-0 pt-3\">\n {children}\n </AppShell.Content>\n </AppShell.Main>\n </AppShell>\n </ConversationsProvider>\n </ChatThemeScope>\n </>\n );\n}\n",
65
- "app/markdown-renderer.tsx": "'use client'\n\nimport ReactMarkdown from 'react-markdown@9.0.3'\nimport remarkGfm from 'remark-gfm@4.0.1'\nimport type { MarkdownRendererProps } from 'veryfront/markdown'\n\n/**\n * Rich Markdown for assistant answers.\n *\n * `veryfront/markdown` presents plain source until a renderer is installed, so\n * this component supplies one. Swap in any renderer that accepts\n * `MarkdownRendererProps` to change how answers are parsed and rendered.\n */\nexport function MarkdownRenderer({ source }: MarkdownRendererProps): React.JSX.Element {\n return <ReactMarkdown remarkPlugins={[remarkGfm]}>{source}</ReactMarkdown>\n}\n",
66
- "app/page.tsx": "'use client'\n\nimport { Chat } from 'veryfront/chat'\nimport { MarkdownRendererProvider } from 'veryfront/markdown'\nimport { MarkdownRenderer } from './markdown-renderer.tsx'\n\nexport default function DocsChat(): React.JSX.Element {\n return (\n <MarkdownRendererProvider renderer={MarkdownRenderer}>\n <Chat\n agentId=\"rag\"\n api=\"/api/ag-ui\"\n className=\"flex-1 min-h-0\"\n placeholder=\"Ask anything about your documents...\"\n />\n </MarkdownRendererProvider>\n )\n}\n",
67
- "app/uploads/page.tsx": "'use client'\n\nimport { AttachmentsPanel, useUploadsRegistry } from 'veryfront/chat'\n\nconst UPLOAD_API = '/api/uploads'\nconst ACCEPT = '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.csv,.txt,.md,.mdx,.html,.rtf,.epub,.json,.xml'\n\nexport default function UploadsPage(): React.JSX.Element {\n const uploads = useUploadsRegistry({ url: UPLOAD_API, storageKey: 'rag-uploads' })\n const error = uploads.uploadError\n ?? uploads.refreshError\n ?? uploads.removeError\n ?? uploads.storageError\n\n return (\n <>\n {error && (\n <p role=\"alert\" className=\"m-4 rounded-md bg-red-50 p-3 text-sm text-red-800\">\n {error.message}\n </p>\n )}\n <AttachmentsPanel\n uploads={uploads.items}\n loading={uploads.isLoading}\n onAttach={uploads.upload}\n onRemoveUpload={uploads.remove}\n attachAccept={ACCEPT}\n className=\"flex-1 min-h-0\"\n />\n </>\n )\n}\n",
68
- "content/architecture.md": "# Architecture\n\nAcme Platform uses a modular, event-driven architecture.\n\n## Core Components\n\n### API Gateway\nRoutes incoming requests to the appropriate microservice. Handles authentication, rate limiting, and request validation.\n\n### Event Bus\nAsynchronous message broker connecting all services. Supports pub/sub and point-to-point messaging patterns.\n\n### Data Layer\nMulti-tenant data storage with automatic sharding. Supports PostgreSQL for relational data and Redis for caching.\n\n## Request Flow\n\n1. Client sends request to API Gateway\n2. Gateway validates authentication token\n3. Request is routed to the target service\n4. Service processes request and publishes events\n5. Response is returned through the gateway\n\n## Scaling\n\nEach component scales independently. The API Gateway uses horizontal scaling with load balancing. Services auto-scale based on queue depth and CPU utilization.\n\n## Security\n\n- All inter-service communication uses mTLS\n- API tokens are rotated every 24 hours\n- Data at rest is encrypted with AES-256\n- Audit logs are retained for 90 days\n",
69
- "content/getting-started.md": "# Getting Started\n\nWelcome to Acme Platform. This guide covers initial setup and core concepts.\n\n## Installation\n\nInstall the CLI globally:\n\n```bash\nnpm install -g @acme/cli\n```\n\n## Creating a Project\n\nRun the init command to scaffold a new project:\n\n```bash\nacme init my-project\ncd my-project\n```\n\n## Project Structure\n\n- `src/`: Application source code\n- `config/`: Configuration files\n- `tests/`: Test suite\n- `docs/`: Documentation\n\n## Configuration\n\nCreate an `acme.config.ts` file in your project root:\n\n```ts\nexport default {\n name: \"my-project\",\n region: \"us-east-1\",\n features: [\"auth\", \"storage\"],\n};\n```\n\n## Next Steps\n\n- Read the [Architecture Guide](./architecture) to understand the system design\n- Check [API Reference](./api-reference) for available endpoints\n- Join our Discord community for support\n",
70
- "globals.css": "@import \"tailwindcss\";\n",
71
- "globals.d.ts": "declare module \"*.css\";\n",
72
- "public/favicon.svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 64 64\">\n <rect width=\"64\" height=\"64\" fill=\"#fff\"/>\n <circle cx=\"32\" cy=\"32\" r=\"20\" fill=\"#000\"/>\n</svg>\n",
73
- "README.md": "# Docs Agent\n\nA chatbot that answers questions from your own documents using Retrieval-Augmented Generation (RAG).\n\n## What's included\n\n- Q&A agent with source citation\n- Embedding-based semantic search with convention-based model selection\n- Document upload supporting PDF, DOCX, XLSX, PPTX, CSV, HTML, RTF, EPUB, TXT, and Markdown\n- `ragStore()` with local JSON storage by default, and Veryfront Cloud RAG when bootstrap is present\n- Veryfront Cloud AI Gateway routing for generation and embeddings when bootstrap is present\n- Original uploaded files stored in Veryfront Cloud project uploads when cloud bootstrap is present\n- Sample content in `/content` indexed through a dedicated ingestion route\n\n## Getting started\n\n1. Set your Veryfront Cloud bootstrap vars:\n\n ```bash\n export VERYFRONT_API_TOKEN=vf_...\n export VERYFRONT_PROJECT_SLUG=my-project\n ```\n\n2. Start the dev server:\n\n ```bash\n npx veryfront@latest dev\n ```\n\n3. Index the sample docs in `content/`:\n\n ```bash\n curl -X POST http://localhost:3000/api/ingest\n ```\n\n4. Open the app and upload a document or ask a question.\n\nIf you are using a self-hosted Veryfront API, also set `VERYFRONT_API_URL`.\n\n## Cloud mode\n\nWith `VERYFRONT_API_TOKEN` and `VERYFRONT_PROJECT_SLUG` set, the template uses\nVeryfront Cloud automatically:\n\n- Agent generation routes through Veryfront Cloud model routing.\n- Query and document embeddings route through Veryfront Cloud embedding routing.\n- `veryfront-cloud/openai/...` and `veryfront-cloud/google/...` models use AI Gateway.\n- RAG documents, chunks, and embeddings are stored in the target project.\n\nThe default cloud embedding model is\n`veryfront-cloud/openai/text-embedding-3-small`. Set\n`VERYFRONT_DEFAULT_EMBEDDING_MODEL` only when you need a different embedding\nmodel.\n\n## Architecture\n\nRAG grounds LLM responses in your documents through three pipelines: **Ingestion**, **Query**, and **RAG**. These pipelines are orchestrated around a shared vector store.\n\n```mermaid\nflowchart LR\n ChatUI_L[\"Chat UI\"]\n\n subgraph IngestionFlow[\"Ingestion Pipeline\"]\n D[\"Documents\"] --> EXT[\"Extraction\"] --> DC[\"Chunking\"] --> DE[\"Document\\nEmbedding\"] --> ING[\"Storage\"]\n end\n\n subgraph QueryFlow[\"Query Pipeline\"]\n Q[\"Query\"] --> QE[\"Query\\nEmbedding\"] --> SS[\"Similarity\\nSearch\"]\n end\n\n subgraph RAGFlow[\"RAG Pipeline\"]\n BF[\"beforeStream\\nHook\"] --> RET[\"Retrieval\"] --> AUG[\"Augmentation\"] --> AG[\"Agent\"] --> GEN[\"Generation\"]\n end\n\n EMB((\"Embedding\\nModel\"))\n GEN_LLM((\"Generative\\nModel\"))\n VS[(\"Vector\\nStore\")]\n ChatUI_R[\"Chat UI\"]\n\n ChatUI_L --> D\n ChatUI_L --> Q\n\n QE -.- EMB\n DE -.- EMB\n\n SS --> VS\n ING --> VS\n\n Q --> BF\n VS --> RET\n AG -.- GEN_LLM\n GEN -.- GEN_LLM\n GEN --> ChatUI_R\n```\n\n### Pipelines\n\n**Ingestion**: Documents are parsed into plain text via the built-in kreuzberg extraction engine (supporting PDF, DOCX, XLSX, PPTX, HTML, RTF, EPUB, and 76+ formats), split into overlapping chunks (~1000 chars, 200 char overlap), and stored in the default `ragStore()`. In local mode that means `data/index.json`; with Veryfront Cloud bootstrap it upgrades to the cloud RAG backend automatically. The original uploaded binary is also stored in the project's Veryfront Cloud uploads store so remote projects retain the source file, not just the extracted text. Uploaded files are ingested by the upload route. Bundled files in `content/` are ingested by the `/api/ingest` route.\n\n**Query**: The user's query is embedded into the same vector space as the documents, then compared against all stored chunks using cosine similarity to find the top-*k* most relevant results.\n\n**RAG**: The `beforeStream` hook in the AG-UI route intercepts each message before it reaches the agent. It searches the document store for relevant chunks, assembles them into context, and prepends them as retrieved reference data. The agent then generates a cited response streamed back to the user.\n\n## Structure\n\n```\nstore.ts RAG store config\nagents/rag.ts Q&A agent with citation instructions\ncontent/\n getting-started.md Sample document\n architecture.md Sample document\napp/\n api/ag-ui/route.ts AG-UI endpoint\n api/ingest/route.ts Bundled content ingestion\n api/uploads/route.ts Upload, list, and delete uploads\n uploads/page.tsx Upload management panel\n page.tsx Chat UI\n layout.tsx Chat shell with sidebar and tabs\n```\n\n## Framework usage\n\n| What | Framework | Template code |\n|------|-----------|---------------|\n| Chat UI + streaming | app-mode `Chat` | `page.tsx` |\n| Upload management | `AttachmentsPanel`, `useUploadsRegistry` | `uploads/page.tsx` |\n| Source display | Built into the `Chat` preset | `page.tsx` |\n| Upload API route | `createUploadHandler` | 1 line in `route.ts` |\n| AG-UI route | `createAgUiHandler` | 1 line in `route.ts` |\n| Agent definition | `agent()` | Config object in `agents/rag.ts` |\n| RAG retrieval | `beforeStream` hook | Context injection in `api/ag-ui/route.ts` |\n| Vector store | `ragStore()` | Config in `store.ts` |\n\n## Adding documents\n\n- Drop files into `content/`, then run `curl -X POST http://localhost:3000/api/ingest`\n- Or use the upload panel in the UI for PDF, DOCX, XLSX, PPTX, CSV, HTML, RTF, EPUB, TXT, and MD files\n\n## Production notes\n\nThis is a starter template, not a production-ready setup. For production, consider:\n\n- **Vector store**: Replace the default store with pgvector, Pinecone, or Qdrant for datasets beyond ~10k chunks\n- **Reranking**: Add a cross-encoder reranker (e.g. Cohere Rerank) after retrieval to improve precision\n- **Hybrid search**: Combine dense vectors with BM25 keyword matching for better recall\n",
74
- "store.ts": "import { ragStore } from \"veryfront/embedding\";\n\nexport const store = ragStore({\n storagePath: \"data/index.json\",\n contentDir: \"content\",\n});\n",
75
- "tsconfig.json": "{\n \"compilerOptions\": {\n \"target\": \"ES2022\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"bundler\",\n \"strict\": true,\n \"jsx\": \"react-jsx\",\n \"skipLibCheck\": true,\n \"esModuleInterop\": true,\n \"noEmit\": true,\n \"allowImportingTsExtensions\": true,\n \"paths\": {\n \"@/*\": [\"./*\"],\n \"react-markdown@*\": [\"./node_modules/react-markdown\"],\n \"remark-gfm@*\": [\"./node_modules/remark-gfm\"]\n }\n },\n \"include\": [\"**/*.ts\", \"**/*.tsx\"],\n \"exclude\": [\"node_modules\"]\n}\n"
76
- }
77
- },
78
- "minimal": {
79
- "files": {
80
- "app/about/page.mdx": "<div className=\"prose dark:prose-invert\">\n\n# About\n\nThis is a minimal starter template.\n\n## Features\n\n- HMR for local development\n- MDX support\n- Tailwind CSS\n- Minimal defaults\n\n## Getting Started\n\n1. Edit pages in the `app` directory\n2. Add components in `components`\n3. Customize with `veryfront.config.ts` when needed\n\nHappy coding!\n\n</div>\n",
81
- "app/layout.tsx": "import { Head } from \"veryfront/head\";\n\nexport default function RootLayout({\n children,\n}: {\n children: React.ReactNode;\n}): React.ReactNode {\n return (\n <>\n <Head>\n <link rel=\"icon\" href=\"/favicon.svg\" type=\"image/svg+xml\" />\n </Head>\n <div className=\"min-h-screen bg-white text-neutral-900 dark:bg-neutral-900 dark:text-neutral-100\">\n <main className=\"mx-auto max-w-2xl px-6 py-16\">{children}</main>\n </div>\n </>\n );\n}\n",
82
- "app/page.tsx": "export default function HomePage(): React.JSX.Element {\n return (\n <div>\n <h1 className=\"mb-4 text-4xl font-bold text-neutral-900 dark:text-white\">\n Welcome to Veryfront\n </h1>\n <p className=\"mb-8 text-neutral-600 dark:text-neutral-400\">\n Edit{\" \"}\n <code className=\"rounded bg-neutral-100 px-1.5 py-0.5 text-sm dark:bg-neutral-800\">\n app/page.tsx\n </code>{\" \"}\n to get started.\n </p>\n <div className=\"flex gap-3\">\n <a\n href=\"/about\"\n className=\"rounded-full bg-blue-500 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-600\"\n >\n About\n </a>\n <a\n href=\"https://veryfront.com/docs\"\n className=\"rounded-full bg-neutral-100 px-4 py-2 text-sm font-medium text-neutral-900 transition-colors hover:bg-neutral-200 dark:bg-neutral-800 dark:text-white dark:hover:bg-neutral-700\"\n >\n Documentation\n </a>\n </div>\n </div>\n );\n}\n",
83
- "public/favicon.svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 64 64\">\n <rect width=\"64\" height=\"64\" fill=\"#fff\"/>\n <circle cx=\"32\" cy=\"32\" r=\"20\" fill=\"#000\"/>\n</svg>\n",
84
- "README.md": "# Minimal\n\nA barebones starter with just pages and routing, no agents, no tools.\n\n## What's included\n\n- Home page with welcome message\n- About page using MDX\n- Tailwind CSS styling with dark mode\n\n## Structure\n\n```\napp/\n layout.tsx Root layout\n page.tsx Home page\n about/page.mdx Markdown content page\n```\n\nThis starter is not production-ready.\n",
85
- "tsconfig.json": "{\n \"compilerOptions\": {\n \"target\": \"ES2022\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"bundler\",\n \"strict\": true,\n \"jsx\": \"react-jsx\",\n \"skipLibCheck\": true,\n \"esModuleInterop\": true,\n \"noEmit\": true,\n \"allowImportingTsExtensions\": true,\n \"paths\": {\n \"@/*\": [\n \"./*\"\n ]\n }\n },\n \"include\": [\n \"**/*.ts\",\n \"**/*.tsx\"\n ],\n \"exclude\": [\n \"node_modules\"\n ]\n}\n"
86
- }
87
- },
88
- "multi-agent-system": {
89
- "files": {
90
- "agents/orchestrator.ts": "import { agent } from \"veryfront/agent\";\n\nexport default agent({\n id: \"orchestrator\",\n name: \"Agent Team\",\n description: \"Coordinate research and writing agents.\",\n system:\n \"You coordinate a team of AI agents. \" +\n \"Delegate research tasks to the researcher and writing tasks to the writer. \" +\n \"Combine their outputs into a polished response.\",\n // Each id becomes an `agent_<id>` tool. The specialists are looked up when\n // the run happens, so it does not matter which agent file loads first.\n delegates: [\"researcher\", \"writer\"],\n maxSteps: 10,\n suggestions: [\n {\n type: \"prompt\",\n title: \"Research a topic\",\n prompt: \"Research this topic and summarize the key findings: \",\n },\n {\n type: \"prompt\",\n title: \"Write a brief\",\n prompt: \"Research and write a concise brief about \",\n },\n ],\n});\n",
91
- "agents/researcher.ts": "import { agent } from \"veryfront/agent\";\n\nexport default agent({\n id: \"researcher\",\n system:\n \"You are a research specialist. \" +\n \"Gather comprehensive information on the given topic. \" +\n \"Present findings as structured bullet points with key facts and data.\",\n tools: true,\n maxSteps: 5,\n});\n",
92
- "agents/writer.ts": "import { agent } from \"veryfront/agent\";\n\nexport default agent({\n id: \"writer\",\n system:\n \"You are a writing specialist. \" +\n \"Take research notes and transform them into clear, engaging prose. \" +\n \"Use a professional but approachable tone.\",\n maxSteps: 3,\n});\n",
93
- "app/api/ag-ui/route.ts": "import { createAgUiHandler } from \"veryfront/agent\";\n\nexport const POST = createAgUiHandler(\"orchestrator\");\n",
94
- "app/layout.tsx": "import \"../globals.css\";\nimport { Head } from \"veryfront/head\";\n\nexport default function RootLayout({\n children,\n}: {\n children: React.ReactNode;\n}): React.ReactNode {\n return (\n <>\n <Head>\n <title>Multi-Agent System</title>\n <link rel=\"icon\" href=\"/favicon.svg\" type=\"image/svg+xml\" />\n </Head>\n <div className=\"flex flex-col h-screen bg-white dark:bg-neutral-950\">\n <header className=\"flex-shrink-0 border-b border-neutral-200 dark:border-neutral-800\">\n <div className=\"max-w-3xl mx-auto flex items-center gap-3 px-4 py-3\">\n <div className=\"flex -space-x-2\">\n <div className=\"w-7 h-7 rounded-full bg-blue-500 ring-2 ring-white dark:ring-neutral-950 flex items-center justify-center text-[10px] font-bold text-white\">\n O\n </div>\n <div className=\"w-7 h-7 rounded-full bg-amber-500 ring-2 ring-white dark:ring-neutral-950 flex items-center justify-center text-[10px] font-bold text-white\">\n R\n </div>\n <div className=\"w-7 h-7 rounded-full bg-violet-500 ring-2 ring-white dark:ring-neutral-950 flex items-center justify-center text-[10px] font-bold text-white\">\n W\n </div>\n </div>\n <div>\n <h1 className=\"font-medium text-neutral-900 dark:text-white text-sm\">\n Agent Team\n </h1>\n <p className=\"text-xs text-neutral-500 dark:text-neutral-400\">\n Orchestrator, Researcher, Writer\n </p>\n </div>\n </div>\n </header>\n {children}\n </div>\n </>\n );\n}\n",
95
- "app/markdown-renderer.tsx": "'use client'\n\nimport ReactMarkdown from 'react-markdown@9.0.3'\nimport remarkGfm from 'remark-gfm@4.0.1'\nimport type { MarkdownRendererProps } from 'veryfront/markdown'\n\n/**\n * Rich Markdown for assistant answers.\n *\n * `veryfront/markdown` presents plain source until a renderer is installed, so\n * this component supplies one. Swap in any renderer that accepts\n * `MarkdownRendererProps` to change how answers are parsed and rendered.\n */\nexport function MarkdownRenderer({ source }: MarkdownRendererProps): React.JSX.Element {\n return <ReactMarkdown remarkPlugins={[remarkGfm]}>{source}</ReactMarkdown>\n}\n",
96
- "app/page.tsx": "'use client'\n\nimport { Chat } from 'veryfront/chat'\nimport { MarkdownRendererProvider } from 'veryfront/markdown'\nimport { MarkdownRenderer } from './markdown-renderer.tsx'\n\nexport default function MultiAgentChat(): React.JSX.Element {\n return (\n <MarkdownRendererProvider renderer={MarkdownRenderer}>\n <Chat\n agentId=\"orchestrator\"\n api=\"/api/ag-ui\"\n className=\"flex-1 min-h-0\"\n placeholder=\"Give the team a task...\"\n />\n </MarkdownRendererProvider>\n )\n}\n",
97
- "globals.css": "@import \"tailwindcss\";\n",
98
- "globals.d.ts": "declare module \"*.css\";\n",
99
- "public/favicon.svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 64 64\">\n <rect width=\"64\" height=\"64\" fill=\"#fff\"/>\n <circle cx=\"32\" cy=\"32\" r=\"20\" fill=\"#000\"/>\n</svg>\n",
100
- "README.md": "# Multi-Agent System\n\nA team of specialized agents that collaborate on tasks.\n\n## What's included\n\n- Orchestrator that delegates to researcher and writer agents\n- Delegation via `delegates: [\"researcher\", \"writer\"]`, which the runtime\n resolves into `agent_researcher` and `agent_writer` tools when a run starts\n- Web search tool (placeholder, configure your own API)\n\n## Structure\n\n```\nagents/\n orchestrator.ts Coordinates the team\n researcher.ts Gathers information\n writer.ts Produces polished content\ntools/web-search.ts Placeholder search tool\napp/\n api/ag-ui/route.ts AG-UI endpoint\n page.tsx Chat interface\n```\n\nThis starter is not production-ready.\n",
101
- "tools/web-search.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\n\nexport default tool({\n id: \"web-search\",\n description: \"Search the web for information on a topic\",\n inputSchema: defineSchema((v) => v.object({\n query: v.string().describe(\"Search query\"),\n }))(),\n execute: ({ query: _query }) => {\n // Connect a real search API to use this tool.\n // Popular options: Tavily, SerpAPI, Brave Search\n throw new Error(\n \"No search API configured. \" +\n \"See https://veryfront.com/docs/code/guides/tools for setup instructions.\",\n );\n },\n});\n",
102
- "tsconfig.json": "{\n \"compilerOptions\": {\n \"target\": \"ES2022\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"bundler\",\n \"strict\": true,\n \"jsx\": \"react-jsx\",\n \"skipLibCheck\": true,\n \"esModuleInterop\": true,\n \"noEmit\": true,\n \"allowImportingTsExtensions\": true,\n \"paths\": {\n \"@/*\": [\n \"./*\"\n ],\n \"react-markdown@*\": [\n \"./node_modules/react-markdown\"\n ],\n \"remark-gfm@*\": [\n \"./node_modules/remark-gfm\"\n ]\n }\n },\n \"include\": [\n \"**/*.ts\",\n \"**/*.tsx\"\n ],\n \"exclude\": [\n \"node_modules\"\n ]\n}\n"
103
- }
104
- },
105
- "saas-starter": {
106
- "files": {
107
- "agents/assistant.ts": "import { agent } from \"veryfront/agent\";\n\nexport default agent({\n id: \"assistant\",\n name: \"SaaS Assistant\",\n description: \"Answer product and customer questions.\",\n system: \"You are a helpful AI assistant. Be concise and direct.\",\n tools: true,\n memory: { type: \"conversation\", maxMessages: 50 },\n maxSteps: 10,\n suggestions: [\n {\n type: \"prompt\",\n title: \"Summarize account\",\n prompt: \"Summarize the latest account activity.\",\n },\n {\n type: \"prompt\",\n title: \"Find customers\",\n prompt: \"Find customers who need attention.\",\n },\n ],\n});\n",
108
- "app/api/ag-ui/route.ts": "import { createAgUiHandler } from \"veryfront/agent\";\n\nexport const POST = createAgUiHandler(\"assistant\");\n",
109
- "app/dashboard/page.tsx": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Chat } from \"veryfront/chat\";\nimport { MarkdownRendererProvider } from \"veryfront/markdown\";\nimport { MarkdownRenderer } from \"../markdown-renderer.tsx\";\n\ninterface Conversation {\n id: string;\n title: string;\n updatedAt: string;\n}\n\nconst INITIAL_CONVERSATIONS: Conversation[] = [\n { id: \"1\", title: \"Getting started\", updatedAt: \"Just now\" },\n];\n\nexport default function Dashboard(): React.JSX.Element {\n const [conversations] = useState<Conversation[]>(INITIAL_CONVERSATIONS);\n const [activeId, setActiveId] = useState(\"1\");\n\n return (\n <div className=\"flex h-screen bg-white dark:bg-neutral-950\">\n {/* Sidebar */}\n <aside className=\"w-64 border-r border-neutral-200 dark:border-neutral-800 flex flex-col bg-neutral-50 dark:bg-neutral-900\">\n <div className=\"p-4 border-b border-neutral-200 dark:border-neutral-800\">\n <button type=\"button\" className=\"w-full flex items-center gap-2 px-3 py-2 text-sm font-medium text-neutral-700 dark:text-neutral-300 bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg hover:bg-neutral-50 dark:hover:bg-neutral-700 transition-colors\">\n <svg\n className=\"w-4 h-4\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n strokeWidth={2}\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n d=\"M12 4.5v15m7.5-7.5h-15\"\n />\n </svg>\n New chat\n </button>\n </div>\n\n <nav className=\"flex-1 overflow-y-auto p-2 space-y-0.5\">\n {conversations.map((conv) => (\n <button\n key={conv.id}\n type=\"button\"\n onClick={() => setActiveId(conv.id)}\n className={`w-full text-left px-3 py-2 rounded-lg text-sm transition-colors ${\n activeId === conv.id\n ? \"bg-neutral-200 dark:bg-neutral-800 text-neutral-900 dark:text-white\"\n : \"text-neutral-600 dark:text-neutral-400 hover:bg-neutral-100 dark:hover:bg-neutral-800/50\"\n }`}\n >\n <p className=\"truncate\">{conv.title}</p>\n <p className=\"text-xs text-neutral-400 mt-0.5\">\n {conv.updatedAt}\n </p>\n </button>\n ))}\n </nav>\n\n <div className=\"p-4 border-t border-neutral-200 dark:border-neutral-800\">\n <div className=\"flex items-center gap-2\">\n <div className=\"w-8 h-8 rounded-full bg-neutral-200 dark:bg-neutral-700 flex items-center justify-center text-xs font-medium text-neutral-600 dark:text-neutral-300\">\n U\n </div>\n <div className=\"flex-1 min-w-0\">\n <p className=\"text-sm font-medium text-neutral-900 dark:text-white truncate\">\n User\n </p>\n <p className=\"text-xs text-neutral-500 truncate\">\n user@example.com\n </p>\n </div>\n </div>\n </div>\n </aside>\n\n {/* Chat */}\n <main className=\"flex-1 flex flex-col\">\n <MarkdownRendererProvider renderer={MarkdownRenderer}>\n <Chat\n agentId=\"assistant\"\n api=\"/api/ag-ui\"\n className=\"flex-1 min-h-0\"\n placeholder=\"Message...\"\n />\n </MarkdownRendererProvider>\n </main>\n </div>\n );\n}\n",
110
- "app/layout.tsx": "import \"../globals.css\";\nimport { Head } from \"veryfront/head\";\n\nexport default function RootLayout({\n children,\n}: {\n children: React.ReactNode;\n}): React.ReactNode {\n return (\n <>\n <Head>\n <title>AI SaaS</title>\n <link rel=\"icon\" href=\"/favicon.svg\" type=\"image/svg+xml\" />\n </Head>\n <div className=\"antialiased\">\n {children}\n </div>\n </>\n );\n}\n",
111
- "app/login/page.tsx": "\"use client\";\n\n// Demo sign-in: the buttons below pass straight through to /dashboard so the\n// starter is usable out of the box. To wire up real OAuth, scaffold provider\n// routes at app/api/auth/google/route.ts and app/api/auth/github/route.ts and\n// point the hrefs there. See https://veryfront.com/docs/code/guides/oauth.\nexport default function LoginPage(): React.JSX.Element {\n return (\n <div className=\"min-h-screen flex items-center justify-center bg-neutral-50 dark:bg-neutral-950 px-4\">\n <div className=\"w-full max-w-sm\">\n <div className=\"text-center mb-8\">\n <h1 className=\"text-xl font-bold text-neutral-900 dark:text-white\">\n Welcome back\n </h1>\n <p className=\"text-sm text-neutral-500 dark:text-neutral-400 mt-1\">\n Sign in to continue\n </p>\n </div>\n\n <div className=\"bg-white dark:bg-neutral-900 rounded-2xl border border-neutral-200 dark:border-neutral-800 p-6 space-y-3\">\n <a\n href=\"/dashboard\"\n className=\"flex items-center justify-center gap-2 w-full px-4 py-2.5 border border-neutral-200 dark:border-neutral-800 rounded-xl text-sm font-medium text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-800 transition-colors\"\n >\n <svg className=\"w-4 h-4\" viewBox=\"0 0 24 24\">\n <path\n fill=\"#4285F4\"\n d=\"M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 01-2.2 3.32v2.76h3.57c2.08-1.92 3.28-4.74 3.28-8.09z\"\n />\n <path\n fill=\"#34A853\"\n d=\"M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z\"\n />\n <path\n fill=\"#FBBC05\"\n d=\"M5.84 14.09a7.12 7.12 0 010-4.18V7.07H2.18A11.99 11.99 0 001 12c0 1.94.46 3.77 1.18 5.43l3.66-3.34z\"\n />\n <path\n fill=\"#EA4335\"\n d=\"M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z\"\n />\n </svg>\n Continue with Google\n </a>\n <a\n href=\"/dashboard\"\n className=\"flex items-center justify-center gap-2 w-full px-4 py-2.5 border border-neutral-200 dark:border-neutral-800 rounded-xl text-sm font-medium text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-800 transition-colors\"\n >\n <svg className=\"w-4 h-4\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <path d=\"M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.51 11.51 0 0112 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12z\" />\n </svg>\n Continue with GitHub\n </a>\n </div>\n\n <p className=\"mt-6 text-center text-xs text-neutral-400\">\n <a\n href=\"/\"\n className=\"hover:text-neutral-600 dark:hover:text-neutral-300\"\n >\n &larr; Back to home\n </a>\n </p>\n </div>\n </div>\n );\n}\n",
112
- "app/markdown-renderer.tsx": "\"use client\";\n\nimport ReactMarkdown from \"react-markdown@9.0.3\";\nimport remarkGfm from \"remark-gfm@4.0.1\";\nimport type { MarkdownRendererProps } from \"veryfront/markdown\";\n\n/**\n * Rich Markdown for assistant answers.\n *\n * `veryfront/markdown` presents plain source until a renderer is installed, so\n * this component supplies one. Swap in any renderer that accepts\n * `MarkdownRendererProps` to change how answers are parsed and rendered.\n */\nexport function MarkdownRenderer({ source }: MarkdownRendererProps): React.JSX.Element {\n return <ReactMarkdown remarkPlugins={[remarkGfm]}>{source}</ReactMarkdown>;\n}\n",
113
- "app/page.tsx": "export default function LandingPage(): React.JSX.Element {\n return (\n <div className=\"min-h-screen bg-white dark:bg-neutral-950\">\n {/* Nav */}\n <nav className=\"border-b border-neutral-100 dark:border-neutral-900\">\n <div className=\"max-w-5xl mx-auto flex items-center justify-between px-6 h-14\">\n <span className=\"font-semibold text-neutral-900 dark:text-white\">\n AI SaaS\n </span>\n <div className=\"flex items-center gap-4\">\n <a\n href=\"/login\"\n className=\"text-sm text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white transition-colors\"\n >\n Sign in\n </a>\n <a\n href=\"/login\"\n className=\"text-sm px-4 py-1.5 bg-neutral-900 dark:bg-white text-white dark:text-neutral-900 rounded-full font-medium hover:opacity-90 transition-opacity\"\n >\n Get started\n </a>\n </div>\n </div>\n </nav>\n\n {/* Hero */}\n <main className=\"max-w-5xl mx-auto px-6\">\n <div className=\"pt-24 pb-16 text-center\">\n <h1 className=\"text-4xl md:text-5xl font-bold tracking-tight text-neutral-900 dark:text-white\">\n Your AI-powered platform\n </h1>\n <p className=\"mt-4 text-lg text-neutral-500 dark:text-neutral-400 max-w-lg mx-auto\">\n Built with Veryfront. Agents, tools, and memory are ready for production.\n </p>\n <div className=\"mt-8 flex gap-3 justify-center\">\n <a\n href=\"/login\"\n className=\"px-6 py-2.5 bg-neutral-900 dark:bg-white text-white dark:text-neutral-900 rounded-full font-medium hover:opacity-90 transition-opacity\"\n >\n Start free\n </a>\n <a\n href=\"https://veryfront.com/docs/code/guides\"\n className=\"px-6 py-2.5 border border-neutral-200 dark:border-neutral-800 text-neutral-700 dark:text-neutral-300 rounded-full font-medium hover:bg-neutral-50 dark:hover:bg-neutral-900 transition-colors\"\n >\n Documentation\n </a>\n </div>\n </div>\n\n {/* Features */}\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-6 py-16 border-t border-neutral-100 dark:border-neutral-900\">\n {[\n {\n title: \"AI Agents\",\n desc:\n \"Define agents with tools, memory, and streaming. Veryfront auto-discovers them from your project.\",\n },\n {\n title: \"Per-User Memory\",\n desc: \"Each user gets their own conversation history, persisted across sessions.\",\n },\n {\n title: \"Production Ready\",\n desc:\n \"Use auth, rate limiting, and deployment to ship to production with one command.\",\n },\n ].map(({ title, desc }) => (\n <div key={title}>\n <h3 className=\"font-medium text-neutral-900 dark:text-white\">\n {title}\n </h3>\n <p className=\"mt-1 text-sm text-neutral-500 dark:text-neutral-400\">\n {desc}\n </p>\n </div>\n ))}\n </div>\n </main>\n </div>\n );\n}\n",
114
- "globals.css": "@import \"tailwindcss\";\n",
115
- "globals.d.ts": "declare module \"*.css\";\n",
116
- "public/favicon.svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 64 64\">\n <rect width=\"64\" height=\"64\" fill=\"#fff\"/>\n <circle cx=\"32\" cy=\"32\" r=\"20\" fill=\"#000\"/>\n</svg>\n",
117
- "README.md": "# SaaS Starter\n\nA SaaS-shaped starter with authentication, conversation memory, and a full UI.\n\n## What's included\n\n- Landing page with feature highlights\n- OAuth login (Google and GitHub)\n- Dashboard with conversation sidebar\n- Per-user conversation memory persisted across sessions\n\n## Structure\n\n```\nagents/assistant.ts Agent with conversation memory\ntools/search.ts Placeholder domain search\napp/\n api/ag-ui/route.ts AG-UI endpoint\n page.tsx Landing page\n login/page.tsx OAuth login\n dashboard/page.tsx Chat with sidebar\n```\n\nThis starter is not production-ready.\n",
118
- "tools/search.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\n\nexport default tool({\n id: \"search\",\n description: \"Search your knowledge base\",\n inputSchema: defineSchema((v) =>\n v.object({\n query: v.string().describe(\"Search query\"),\n })\n )(),\n execute: ({ query }) => {\n // Replace with your domain-specific search logic\n return {\n results: [],\n query,\n message: \"Connect your data source for real results.\",\n };\n },\n});\n",
119
- "tsconfig.json": "{\n \"compilerOptions\": {\n \"target\": \"ES2022\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"bundler\",\n \"strict\": true,\n \"jsx\": \"react-jsx\",\n \"skipLibCheck\": true,\n \"esModuleInterop\": true,\n \"noEmit\": true,\n \"allowImportingTsExtensions\": true,\n \"paths\": {\n \"@/*\": [\n \"./*\"\n ],\n \"react-markdown@*\": [\n \"./node_modules/react-markdown\"\n ],\n \"remark-gfm@*\": [\n \"./node_modules/remark-gfm\"\n ]\n }\n },\n \"include\": [\n \"**/*.ts\",\n \"**/*.tsx\"\n ],\n \"exclude\": [\n \"node_modules\"\n ]\n}\n"
120
- }
121
- },
122
- "integration:_base": {
123
- "files": {
124
- "app/api/integrations/status/route.ts": "import { tokenStore } from \"../../../../lib/token-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../lib/user-id.ts\";\n\nconst INTEGRATIONS = [\n { id: \"gmail\", name: \"Gmail\", icon: \"mail\" },\n { id: \"slack\", name: \"Slack\", icon: \"slack\" },\n { id: \"calendar\", name: \"Calendar\", icon: \"calendar\" },\n { id: \"github\", name: \"GitHub\", icon: \"github\" },\n { id: \"jira\", name: \"Jira\", icon: \"jira\" },\n { id: \"notion\", name: \"Notion\", icon: \"notion\" },\n];\n\nexport async function GET(req: Request): Promise<Response> {\n const userId = await requireUserIdFromRequest(req);\n if (!userId) {\n return Response.json({ error: \"Unauthorized\" }, { status: 401 });\n }\n\n const integrations = await Promise.all(\n INTEGRATIONS.map(async (integration) => {\n const { id, name, icon } = integration;\n\n return {\n id,\n name,\n icon,\n connected: await tokenStore.isConnected(userId, id),\n connectUrl: `/api/auth/${id}`,\n };\n }),\n );\n\n return Response.json({ integrations });\n}\n",
125
- "app/api/integrations/token-storage/route.ts": "/**\n * Token Storage Status API\n *\n * Returns the current token storage mode and encryption status.\n * This endpoint is self-contained to work with any version of token-store.\n */\nexport async function GET(): Promise<Response> {\n const env = process.env;\n\n let mode: \"memory\" | \"database\" | \"kv\" | \"redis\" = \"memory\";\n if (env.DATABASE_URL) {\n mode = \"database\";\n } else if (env.KV_REST_API_URL) {\n mode = \"kv\";\n } else if (env.REDIS_URL) {\n mode = \"redis\";\n }\n\n const hasExplicitKey = env.TOKEN_ENCRYPTION_KEY?.length === 64;\n\n return Response.json({\n mode,\n encrypted: true,\n autoGenerated: !hasExplicitKey,\n });\n}\n",
126
- "app/components/ServiceConnections.tsx": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\ninterface Service {\n id: string;\n name: string;\n connected: boolean;\n authUrl: string;\n}\n\ninterface ServiceConnectionsProps {\n services: Array<{\n id: string;\n name: string;\n authUrl: string;\n }>;\n className?: string;\n}\n\nfunction useIntegrationStatus(): { status: Record<string, boolean>; loading: boolean } {\n const [status, setStatus] = useState<Record<string, boolean>>({});\n const [loading, setLoading] = useState<boolean>(true);\n\n useEffect(() => {\n async function checkStatus(): Promise<void> {\n try {\n const res = await fetch(\"/api/integrations/status\");\n if (!res.ok) return;\n\n const data = await res.json();\n const integrations = data?.integrations ?? [];\n\n const statusMap: Record<string, boolean> = {};\n for (const integration of integrations) {\n statusMap[integration.id] = integration.connected;\n }\n\n setStatus(statusMap);\n } catch (error) {\n console.error(\"Failed to check service status:\", error);\n } finally {\n setLoading(false);\n }\n }\n\n checkStatus();\n }, []);\n\n return { status, loading };\n}\n\nfunction withStatus(\n services: ServiceConnectionsProps[\"services\"],\n status: Record<string, boolean>,\n): Service[] {\n return services.map((service) => ({\n ...service,\n connected: status[service.id] ?? false,\n }));\n}\n\nexport function ServiceConnections({\n services,\n className = \"\",\n}: ServiceConnectionsProps): React.ReactElement {\n const { status, loading } = useIntegrationStatus();\n\n if (loading) {\n return (\n <div className={`flex items-center gap-2 ${className}`}>\n <div className=\"animate-pulse h-6 w-32 bg-neutral-200 dark:bg-neutral-700 rounded\" />\n </div>\n );\n }\n\n const servicesWithStatus = withStatus(services, status);\n const connectedCount = servicesWithStatus.reduce(\n (count, service) => count + (service.connected ? 1 : 0),\n 0,\n );\n\n return (\n <div className={`flex items-center gap-2 ${className}`}>\n {servicesWithStatus.map((service) => (\n <ServiceBadge key={service.id} service={service} />\n ))}\n {connectedCount < services.length && (\n <span className=\"text-xs text-neutral-500 dark:text-neutral-400 ml-1\">\n {connectedCount}/{services.length} connected\n </span>\n )}\n </div>\n );\n}\n\nfunction ServiceBadge({ service }: { service: Service }): React.ReactElement {\n if (service.connected) {\n return (\n <span\n className=\"inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400\"\n title={`${service.name} connected`}\n >\n <span className=\"w-1.5 h-1.5 rounded-full bg-green-500\" />\n {service.name}\n </span>\n );\n }\n\n const handleConnect = (): void => {\n globalThis.location.href = service.authUrl;\n };\n\n return (\n <button\n type=\"button\"\n onClick={handleConnect}\n className=\"inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium bg-neutral-100 text-neutral-600 hover:bg-neutral-200 dark:bg-neutral-800 dark:text-neutral-400 dark:hover:bg-neutral-700 transition-colors\"\n title={`Connect ${service.name}`}\n >\n <span className=\"w-1.5 h-1.5 rounded-full bg-neutral-400\" />\n {service.name}\n </button>\n );\n}\n\nexport function ServiceConnectionsCard({\n services,\n className = \"\",\n}: ServiceConnectionsProps): React.ReactElement | null {\n const { status, loading } = useIntegrationStatus();\n\n if (loading) return null;\n\n const disconnectedServices = withStatus(services, status).filter((service) => !service.connected);\n if (disconnectedServices.length === 0) return null;\n\n return (\n <div\n className={`rounded-lg border border-amber-200 dark:border-amber-900/50 bg-amber-50 dark:bg-amber-900/20 p-4 ${className}`}\n >\n <h3 className=\"font-medium text-amber-900 dark:text-amber-200 mb-2\">\n Connect your services\n </h3>\n <p className=\"text-sm text-amber-700 dark:text-amber-300/80 mb-3\">\n Connect the following services to unlock all features:\n </p>\n <div className=\"flex flex-wrap gap-2\">\n {disconnectedServices.map((service) => (\n <a\n key={service.id}\n href={service.authUrl}\n className=\"inline-flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium bg-amber-100 text-amber-800 hover:bg-amber-200 dark:bg-amber-900/40 dark:text-amber-200 dark:hover:bg-amber-900/60 transition-colors\"\n >\n Connect {service.name}\n </a>\n ))}\n </div>\n </div>\n );\n}\n",
127
- "app/page.tsx": "'use client'\n\nimport { useEffect, useState } from 'react'\nimport { Chat, useChat } from 'veryfront/chat'\n\ninterface Integration {\n id: string\n name: string\n connected: boolean\n connectUrl: string\n}\n\nexport default function ChatPage(): React.ReactElement {\n const chat = useChat({ api: '/api/ag-ui' })\n\n return (\n <div className=\"flex flex-col h-screen bg-white dark:bg-neutral-900\">\n <header className=\"sticky top-0 z-10 flex-shrink-0 border-b border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900\">\n <div className=\"px-4 py-3 flex items-center justify-between\">\n <h1 className=\"font-medium text-neutral-900 dark:text-white\">AI Agent</h1>\n <div className=\"flex items-center gap-4\">\n <ServiceStatusFromAPI />\n <a\n href=\"/setup\"\n className=\"text-sm text-neutral-500 hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-200\"\n >\n Setup\n </a>\n </div>\n </div>\n </header>\n\n <Chat chat={chat} className=\"flex-1 min-h-0\" placeholder=\"Message\" />\n </div>\n )\n}\n\nfunction ServiceStatusFromAPI(): React.ReactElement | null {\n const [integrations, setIntegrations] = useState<Integration[]>([])\n const [loading, setLoading] = useState<boolean>(true)\n\n useEffect((): void => {\n async function fetchStatus(): Promise<void> {\n try {\n const res = await fetch('/api/integrations/status')\n if (!res.ok) return\n\n const data = await res.json()\n setIntegrations(data.integrations ?? [])\n } catch (error) {\n console.error('Failed to fetch integration status:', error)\n } finally {\n setLoading(false)\n }\n }\n\n void fetchStatus()\n }, [])\n\n if (loading) {\n return (\n <div className=\"flex items-center gap-2\">\n <div className=\"animate-pulse h-6 w-24 bg-neutral-200 dark:bg-neutral-700 rounded-full\" />\n </div>\n )\n }\n\n if (integrations.length === 0) return null\n\n const connected: Integration[] = []\n const disconnected: Integration[] = []\n\n for (const integration of integrations) {\n if (integration.connected) connected.push(integration)\n else disconnected.push(integration)\n }\n\n return (\n <div className=\"flex items-center gap-2\">\n {connected.map(service => (\n <span\n key={service.id}\n className=\"inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400\"\n title={`${service.name} connected`}\n >\n <span className=\"w-1.5 h-1.5 rounded-full bg-green-500\" />\n {service.name}\n </span>\n ))}\n\n {disconnected.map(service => (\n <a\n key={service.id}\n href={service.connectUrl}\n className=\"inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium bg-neutral-100 text-neutral-600 hover:bg-neutral-200 dark:bg-neutral-800 dark:text-neutral-400 dark:hover:bg-neutral-700 transition-colors\"\n title={`Connect ${service.name}`}\n >\n <span className=\"w-1.5 h-1.5 rounded-full bg-neutral-400\" />\n {service.name}\n </a>\n ))}\n\n {disconnected.length > 0 && (\n <span className=\"text-xs text-neutral-500 dark:text-neutral-400\">\n {connected.length}/{integrations.length}\n </span>\n )}\n </div>\n )\n}\n",
128
- "app/setup/page-helpers.tsx": "import type { JSX } from \"react\";\n\nexport interface Integration {\n id: string;\n name: string;\n icon: string;\n connected: boolean;\n connectUrl: string;\n}\n\nexport interface SetupStep {\n id: string;\n title: string;\n description: string;\n completed: boolean;\n action?: () => void;\n link?: string;\n}\n\ninterface SetupGuide {\n title: string;\n steps: string[];\n link: string;\n envVars: string[];\n category: string;\n}\n\nexport interface TokenStorageStatus {\n mode: \"memory\" | \"database\" | \"kv\" | \"redis\" | \"custom\";\n encrypted: boolean;\n autoGenerated?: boolean;\n}\n\nexport type TokenStorageStyles = {\n container: string;\n iconWrapper: string;\n title: string;\n text: string;\n isMemory: boolean;\n};\n\nexport const CATEGORIES = [\n { id: \"google\", name: \"Google Services\", icon: \"google\" },\n { id: \"microsoft\", name: \"Microsoft Services\", icon: \"microsoft\" },\n { id: \"atlassian\", name: \"Atlassian\", icon: \"atlassian\" },\n { id: \"communication\", name: \"Communication\", icon: \"chat\" },\n { id: \"development\", name: \"Development\", icon: \"code\" },\n { id: \"productivity\", name: \"Productivity\", icon: \"tasks\" },\n { id: \"storage\", name: \"Storage\", icon: \"folder\" },\n { id: \"infrastructure\", name: \"Infrastructure\", icon: \"server\" },\n { id: \"sales\", name: \"Sales & CRM\", icon: \"users\" },\n { id: \"support\", name: \"Support\", icon: \"headset\" },\n { id: \"finance\", name: \"Finance\", icon: \"dollar\" },\n { id: \"marketing\", name: \"Marketing\", icon: \"megaphone\" },\n { id: \"design\", name: \"Design\", icon: \"palette\" },\n { id: \"ai\", name: \"AI Providers\", icon: \"brain\" },\n] as const;\n\nexport const OAUTH_SETUP_GUIDES: Record<string, SetupGuide> = {\n gmail: {\n title: \"Google OAuth Setup (Gmail)\",\n category: \"google\",\n steps: [\n \"Go to Google Cloud Console\",\n \"Create a new project or select existing one\",\n \"Enable Gmail API in APIs & Services > Library\",\n \"Go to APIs & Services > Credentials\",\n \"Create OAuth 2.0 credentials (Web application)\",\n \"Add redirect URI: http://localhost:3000/api/auth/gmail/callback\",\n \"Copy Client ID and Secret to your .env file\",\n ],\n link: \"https://console.cloud.google.com/apis/credentials\",\n envVars: [\"GOOGLE_CLIENT_ID\", \"GOOGLE_CLIENT_SECRET\"],\n },\n calendar: {\n title: \"Google Calendar Setup\",\n category: \"google\",\n steps: [\n \"Uses same Google OAuth credentials as Gmail\",\n \"Enable Calendar API in Google Cloud Console\",\n \"Add redirect URI: http://localhost:3000/api/auth/calendar/callback\",\n ],\n link: \"https://console.cloud.google.com/apis/library/calendar-json.googleapis.com\",\n envVars: [\"GOOGLE_CLIENT_ID\", \"GOOGLE_CLIENT_SECRET\"],\n },\n drive: {\n title: \"Google Drive Setup\",\n category: \"google\",\n steps: [\n \"Uses same Google OAuth credentials\",\n \"Enable Drive API in Google Cloud Console\",\n \"Add redirect URI: http://localhost:3000/api/auth/drive/callback\",\n ],\n link: \"https://console.cloud.google.com/apis/library/drive.googleapis.com\",\n envVars: [\"GOOGLE_CLIENT_ID\", \"GOOGLE_CLIENT_SECRET\"],\n },\n sheets: {\n title: \"Google Sheets Setup\",\n category: \"google\",\n steps: [\n \"Uses same Google OAuth credentials\",\n \"Enable Sheets API in Google Cloud Console\",\n \"Add redirect URI: http://localhost:3000/api/auth/sheets/callback\",\n ],\n link: \"https://console.cloud.google.com/apis/library/sheets.googleapis.com\",\n envVars: [\"GOOGLE_CLIENT_ID\", \"GOOGLE_CLIENT_SECRET\"],\n },\n \"docs-google\": {\n title: \"Google Docs Setup\",\n category: \"google\",\n steps: [\n \"Uses same Google OAuth credentials\",\n \"Enable Docs API in Google Cloud Console\",\n \"Add redirect URI: http://localhost:3000/api/auth/docs-google/callback\",\n ],\n link: \"https://console.cloud.google.com/apis/library/docs.googleapis.com\",\n envVars: [\"GOOGLE_CLIENT_ID\", \"GOOGLE_CLIENT_SECRET\"],\n },\n outlook: {\n title: \"Microsoft Outlook Setup\",\n category: \"microsoft\",\n steps: [\n \"Go to Azure Portal > Azure Active Directory\",\n \"Click App registrations > New registration\",\n \"Set redirect URI: http://localhost:3000/api/auth/outlook/callback\",\n \"Go to API permissions > Add Microsoft Graph permissions\",\n \"Add: Mail.Read, Mail.Send, Mail.ReadWrite\",\n \"Go to Certificates & secrets > New client secret\",\n \"Copy Application ID and Secret to .env\",\n ],\n link: \"https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade\",\n envVars: [\"MICROSOFT_CLIENT_ID\", \"MICROSOFT_CLIENT_SECRET\"],\n },\n teams: {\n title: \"Microsoft Teams Setup\",\n category: \"microsoft\",\n steps: [\n \"Uses same Microsoft OAuth credentials as Outlook\",\n \"Add Teams permissions: Chat.Read, Chat.ReadWrite, Channel.ReadBasic.All\",\n \"Add redirect URI: http://localhost:3000/api/auth/teams/callback\",\n ],\n link: \"https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade\",\n envVars: [\"MICROSOFT_CLIENT_ID\", \"MICROSOFT_CLIENT_SECRET\"],\n },\n onedrive: {\n title: \"Microsoft OneDrive Setup\",\n category: \"microsoft\",\n steps: [\n \"Uses same Microsoft OAuth credentials\",\n \"Add permissions: Files.Read, Files.ReadWrite\",\n \"Add redirect URI: http://localhost:3000/api/auth/onedrive/callback\",\n ],\n link: \"https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade\",\n envVars: [\"MICROSOFT_CLIENT_ID\", \"MICROSOFT_CLIENT_SECRET\"],\n },\n sharepoint: {\n title: \"Microsoft SharePoint Setup\",\n category: \"microsoft\",\n steps: [\n \"Uses same Microsoft OAuth credentials\",\n \"Add permissions: Sites.Read.All, Sites.ReadWrite.All\",\n \"Add redirect URI: http://localhost:3000/api/auth/sharepoint/callback\",\n ],\n link: \"https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade\",\n envVars: [\"MICROSOFT_CLIENT_ID\", \"MICROSOFT_CLIENT_SECRET\"],\n },\n jira: {\n title: \"Atlassian Jira Setup\",\n category: \"atlassian\",\n steps: [\n \"Go to Atlassian Developer Console\",\n \"Click Create > OAuth 2.0 integration\",\n \"Add Jira API scopes: read:jira-work, write:jira-work\",\n \"Set callback URL: http://localhost:3000/api/auth/jira/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://developer.atlassian.com/console/myapps/\",\n envVars: [\"ATLASSIAN_CLIENT_ID\", \"ATLASSIAN_CLIENT_SECRET\"],\n },\n confluence: {\n title: \"Atlassian Confluence Setup\",\n category: \"atlassian\",\n steps: [\n \"Uses same Atlassian OAuth credentials as Jira\",\n \"Add Confluence scopes: read:confluence-content.all, write:confluence-content\",\n \"Add callback URL: http://localhost:3000/api/auth/confluence/callback\",\n ],\n link: \"https://developer.atlassian.com/console/myapps/\",\n envVars: [\"ATLASSIAN_CLIENT_ID\", \"ATLASSIAN_CLIENT_SECRET\"],\n },\n bitbucket: {\n title: \"Atlassian Bitbucket Setup\",\n category: \"atlassian\",\n steps: [\n \"Go to Bitbucket Settings > OAuth consumers\",\n \"Click Add consumer\",\n \"Set callback URL: http://localhost:3000/api/auth/bitbucket/callback\",\n \"Add permissions: repository:read, repository:write\",\n \"Copy Key and Secret to .env\",\n ],\n link: \"https://bitbucket.org/account/settings/app-passwords/\",\n envVars: [\"BITBUCKET_CLIENT_ID\", \"BITBUCKET_CLIENT_SECRET\"],\n },\n slack: {\n title: \"Slack App Setup\",\n category: \"communication\",\n steps: [\n \"Go to Slack API Apps page\",\n \"Click Create New App > From scratch\",\n \"Go to OAuth & Permissions\",\n \"Add scopes: channels:history, channels:read, chat:write, groups:history, groups:read, im:history, im:read, mpim:history, mpim:read, users:read\",\n \"Add redirect URL: http://localhost:3000/api/auth/slack/callback\",\n \"Install to Workspace\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://api.slack.com/apps\",\n envVars: [\"SLACK_CLIENT_ID\", \"SLACK_CLIENT_SECRET\"],\n },\n zoom: {\n title: \"Zoom App Setup\",\n category: \"communication\",\n steps: [\n \"Go to Zoom App Marketplace\",\n \"Click Develop > Build App\",\n \"Choose OAuth app type\",\n \"Add redirect URL: http://localhost:3000/api/auth/zoom/callback\",\n \"Add scopes: meeting:read, meeting:write, user:read\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://marketplace.zoom.us/develop/create\",\n envVars: [\"ZOOM_CLIENT_ID\", \"ZOOM_CLIENT_SECRET\"],\n },\n webex: {\n title: \"Webex Integration Setup\",\n category: \"communication\",\n steps: [\n \"Go to Webex Developer Portal\",\n \"Create a new integration\",\n \"Add redirect URI: http://localhost:3000/api/auth/webex/callback\",\n \"Select required scopes\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://developer.webex.com/my-apps\",\n envVars: [\"WEBEX_CLIENT_ID\", \"WEBEX_CLIENT_SECRET\"],\n },\n twilio: {\n title: \"Twilio Setup\",\n category: \"communication\",\n steps: [\n \"Go to Twilio Console\",\n \"Copy Account SID and Auth Token\",\n \"Get a phone number for SMS\",\n \"Add credentials to .env\",\n ],\n link: \"https://console.twilio.com/\",\n envVars: [\"TWILIO_ACCOUNT_SID\", \"TWILIO_AUTH_TOKEN\", \"TWILIO_PHONE_NUMBER\"],\n },\n github: {\n title: \"GitHub OAuth App Setup\",\n category: \"development\",\n steps: [\n \"Go to GitHub Developer Settings\",\n \"Click OAuth Apps > New OAuth App\",\n \"Set Homepage URL: http://localhost:3000\",\n \"Set callback URL: http://localhost:3000/api/auth/github/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://github.com/settings/developers\",\n envVars: [\"GITHUB_CLIENT_ID\", \"GITHUB_CLIENT_SECRET\"],\n },\n gitlab: {\n title: \"GitLab OAuth Setup\",\n category: \"development\",\n steps: [\n \"Go to GitLab User Settings > Applications\",\n \"Create new application\",\n \"Add redirect URI: http://localhost:3000/api/auth/gitlab/callback\",\n \"Select scopes: api, read_user, read_repository\",\n \"Copy Application ID and Secret to .env\",\n ],\n link: \"https://gitlab.com/-/profile/applications\",\n envVars: [\"GITLAB_CLIENT_ID\", \"GITLAB_CLIENT_SECRET\"],\n },\n sentry: {\n title: \"Sentry Setup\",\n category: \"development\",\n steps: [\n \"Go to Sentry Settings > Developer Settings\",\n \"Create new integration\",\n \"Add redirect URL: http://localhost:3000/api/auth/sentry/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://sentry.io/settings/developer-settings/\",\n envVars: [\"SENTRY_CLIENT_ID\", \"SENTRY_CLIENT_SECRET\"],\n },\n posthog: {\n title: \"PostHog Setup\",\n category: \"development\",\n steps: [\"Go to PostHog Project Settings\", \"Copy your Project API Key\", \"Add to .env file\"],\n link: \"https://app.posthog.com/project/settings\",\n envVars: [\"POSTHOG_API_KEY\", \"POSTHOG_HOST\"],\n },\n mixpanel: {\n title: \"Mixpanel Setup\",\n category: \"development\",\n steps: [\n \"Go to Mixpanel Project Settings\",\n \"Copy your Project Token\",\n \"For API access, create a Service Account\",\n \"Add credentials to .env\",\n ],\n link: \"https://mixpanel.com/settings/project\",\n envVars: [\"MIXPANEL_TOKEN\", \"MIXPANEL_API_SECRET\"],\n },\n notion: {\n title: \"Notion Integration Setup\",\n category: \"productivity\",\n steps: [\n \"Go to Notion Integrations page\",\n \"Click New integration\",\n \"Name your integration and select workspace\",\n \"Copy the Internal Integration Token\",\n \"Share desired pages/databases with your integration\",\n \"Add token to .env\",\n ],\n link: \"https://www.notion.so/my-integrations\",\n envVars: [\"NOTION_API_KEY\"],\n },\n linear: {\n title: \"Linear OAuth Setup\",\n category: \"productivity\",\n steps: [\n \"Go to Linear Settings > API\",\n \"Create new OAuth application\",\n \"Add redirect URI: http://localhost:3000/api/auth/linear/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://linear.app/settings/api\",\n envVars: [\"LINEAR_CLIENT_ID\", \"LINEAR_CLIENT_SECRET\"],\n },\n asana: {\n title: \"Asana OAuth Setup\",\n category: \"productivity\",\n steps: [\n \"Go to Asana Developer Console\",\n \"Create new app\",\n \"Add redirect URI: http://localhost:3000/api/auth/asana/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://app.asana.com/0/developer-console\",\n envVars: [\"ASANA_CLIENT_ID\", \"ASANA_CLIENT_SECRET\"],\n },\n trello: {\n title: \"Trello Power-Up Setup\",\n category: \"productivity\",\n steps: [\n \"Go to Trello Power-Ups Admin\",\n \"Create new Power-Up\",\n \"Add redirect URI: http://localhost:3000/api/auth/trello/callback\",\n \"Copy API Key and Secret to .env\",\n ],\n link: \"https://trello.com/power-ups/admin\",\n envVars: [\"TRELLO_API_KEY\", \"TRELLO_API_SECRET\"],\n },\n monday: {\n title: \"Monday.com App Setup\",\n category: \"productivity\",\n steps: [\n \"Go to monday.com Developers\",\n \"Create new app\",\n \"Add OAuth redirect: http://localhost:3000/api/auth/monday/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://monday.com/developers/apps\",\n envVars: [\"MONDAY_CLIENT_ID\", \"MONDAY_CLIENT_SECRET\"],\n },\n clickup: {\n title: \"ClickUp OAuth Setup\",\n category: \"productivity\",\n steps: [\n \"Go to ClickUp Settings > Apps\",\n \"Create new app\",\n \"Add redirect URI: http://localhost:3000/api/auth/clickup/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://app.clickup.com/settings/apps\",\n envVars: [\"CLICKUP_CLIENT_ID\", \"CLICKUP_CLIENT_SECRET\"],\n },\n box: {\n title: \"Box App Setup\",\n category: \"storage\",\n steps: [\n \"Go to Box Developer Console\",\n \"Create new app with OAuth 2.0\",\n \"Add redirect URI: http://localhost:3000/api/auth/box/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://app.box.com/developers/console\",\n envVars: [\"BOX_CLIENT_ID\", \"BOX_CLIENT_SECRET\"],\n },\n airtable: {\n title: \"Airtable OAuth Setup\",\n category: \"storage\",\n steps: [\n \"Go to Airtable Developer Hub\",\n \"Create new OAuth integration\",\n \"Add redirect URI: http://localhost:3000/api/auth/airtable/callback\",\n \"Select required scopes\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://airtable.com/create/oauth\",\n envVars: [\"AIRTABLE_CLIENT_ID\", \"AIRTABLE_CLIENT_SECRET\"],\n },\n supabase: {\n title: \"Supabase Setup\",\n category: \"infrastructure\",\n steps: [\n \"Go to Supabase Dashboard\",\n \"Create new project or select existing\",\n \"Go to Settings > API\",\n \"Copy Project URL and anon/service_role keys\",\n \"Add to .env file\",\n ],\n link: \"https://supabase.com/dashboard\",\n envVars: [\"SUPABASE_URL\", \"SUPABASE_ANON_KEY\", \"SUPABASE_SERVICE_ROLE_KEY\"],\n },\n neon: {\n title: \"Neon Database Setup\",\n category: \"infrastructure\",\n steps: [\n \"Go to Neon Console\",\n \"Create new project\",\n \"Copy connection string from Dashboard\",\n \"Add to .env file\",\n ],\n link: \"https://console.neon.tech/\",\n envVars: [\"DATABASE_URL\"],\n },\n snowflake: {\n title: \"Snowflake Setup\",\n category: \"infrastructure\",\n steps: [\n \"Go to Snowflake Console\",\n \"Create a service account or use existing credentials\",\n \"Note your account identifier, warehouse, database\",\n \"Add credentials to .env\",\n ],\n link: \"https://app.snowflake.com/\",\n envVars: [\"SNOWFLAKE_ACCOUNT\", \"SNOWFLAKE_USER\", \"SNOWFLAKE_PASSWORD\", \"SNOWFLAKE_WAREHOUSE\"],\n },\n aws: {\n title: \"AWS Setup\",\n category: \"infrastructure\",\n steps: [\n \"Go to AWS IAM Console\",\n \"Create new IAM user with programmatic access\",\n \"Attach required policies (S3, Lambda, DynamoDB)\",\n \"Copy Access Key ID and Secret\",\n \"Add to .env file\",\n ],\n link: \"https://console.aws.amazon.com/iam/\",\n envVars: [\"AWS_ACCESS_KEY_ID\", \"AWS_SECRET_ACCESS_KEY\", \"AWS_REGION\"],\n },\n salesforce: {\n title: \"Salesforce Connected App Setup\",\n category: \"sales\",\n steps: [\n \"Go to Salesforce Setup > App Manager\",\n \"Create new Connected App\",\n \"Enable OAuth Settings\",\n \"Add callback URL: http://localhost:3000/api/auth/salesforce/callback\",\n \"Select OAuth scopes: api, refresh_token\",\n \"Copy Consumer Key and Secret to .env\",\n ],\n link: \"https://login.salesforce.com/\",\n envVars: [\"SALESFORCE_CLIENT_ID\", \"SALESFORCE_CLIENT_SECRET\"],\n },\n pipedrive: {\n title: \"Pipedrive OAuth Setup\",\n category: \"sales\",\n steps: [\n \"Go to Pipedrive Developer Hub\",\n \"Create new app\",\n \"Add redirect URI: http://localhost:3000/api/auth/pipedrive/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://developers.pipedrive.com/\",\n envVars: [\"PIPEDRIVE_CLIENT_ID\", \"PIPEDRIVE_CLIENT_SECRET\"],\n },\n zendesk: {\n title: \"Zendesk OAuth Setup\",\n category: \"support\",\n steps: [\n \"Go to Zendesk Admin > API > OAuth Clients\",\n \"Add new OAuth client\",\n \"Set redirect URI: http://localhost:3000/api/auth/zendesk/callback\",\n \"Copy Client ID and Secret to .env\",\n \"Add your Zendesk subdomain\",\n ],\n link: \"https://support.zendesk.com/hc/en-us/articles/4408845965210\",\n envVars: [\"ZENDESK_CLIENT_ID\", \"ZENDESK_CLIENT_SECRET\", \"ZENDESK_SUBDOMAIN\"],\n },\n intercom: {\n title: \"Intercom OAuth Setup\",\n category: \"support\",\n steps: [\n \"Go to Intercom Developer Hub\",\n \"Create new app\",\n \"Add redirect URI: http://localhost:3000/api/auth/intercom/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://developers.intercom.com/\",\n envVars: [\"INTERCOM_CLIENT_ID\", \"INTERCOM_CLIENT_SECRET\"],\n },\n freshdesk: {\n title: \"Freshdesk OAuth Setup\",\n category: \"support\",\n steps: [\n \"Go to Freshdesk Admin > Apps > Custom Apps\",\n \"Create new OAuth application\",\n \"Add redirect URI: http://localhost:3000/api/auth/freshdesk/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://developers.freshdesk.com/\",\n envVars: [\"FRESHDESK_CLIENT_ID\", \"FRESHDESK_CLIENT_SECRET\", \"FRESHDESK_DOMAIN\"],\n },\n servicenow: {\n title: \"ServiceNow OAuth Setup\",\n category: \"support\",\n steps: [\n \"Go to ServiceNow System OAuth > Application Registry\",\n \"Create OAuth API endpoint for external clients\",\n \"Add redirect URL: http://localhost:3000/api/auth/servicenow/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://docs.servicenow.com/\",\n envVars: [\"SERVICENOW_CLIENT_ID\", \"SERVICENOW_CLIENT_SECRET\", \"SERVICENOW_INSTANCE\"],\n },\n stripe: {\n title: \"Stripe Setup\",\n category: \"finance\",\n steps: [\n \"Go to Stripe Dashboard\",\n \"Go to Developers > API keys\",\n \"Copy Publishable and Secret keys\",\n \"For Connect, set up OAuth in Connect settings\",\n \"Add to .env file\",\n ],\n link: \"https://dashboard.stripe.com/apikeys\",\n envVars: [\"STRIPE_SECRET_KEY\", \"STRIPE_PUBLISHABLE_KEY\"],\n },\n quickbooks: {\n title: \"QuickBooks OAuth Setup\",\n category: \"finance\",\n steps: [\n \"Go to Intuit Developer Portal\",\n \"Create new app\",\n \"Add redirect URI: http://localhost:3000/api/auth/quickbooks/callback\",\n \"Select Accounting scope\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://developer.intuit.com/app/developer/dashboard\",\n envVars: [\"QUICKBOOKS_CLIENT_ID\", \"QUICKBOOKS_CLIENT_SECRET\"],\n },\n xero: {\n title: \"Xero OAuth Setup\",\n category: \"finance\",\n steps: [\n \"Go to Xero Developer Portal\",\n \"Create new app\",\n \"Add redirect URI: http://localhost:3000/api/auth/xero/callback\",\n \"Select required scopes\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://developer.xero.com/app/manage\",\n envVars: [\"XERO_CLIENT_ID\", \"XERO_CLIENT_SECRET\"],\n },\n mailchimp: {\n title: \"Mailchimp OAuth Setup\",\n category: \"marketing\",\n steps: [\n \"Go to Mailchimp Developer Portal\",\n \"Register new app\",\n \"Add redirect URI: http://localhost:3000/api/auth/mailchimp/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://admin.mailchimp.com/account/oauth2/\",\n envVars: [\"MAILCHIMP_CLIENT_ID\", \"MAILCHIMP_CLIENT_SECRET\"],\n },\n shopify: {\n title: \"Shopify App Setup\",\n category: \"marketing\",\n steps: [\n \"Go to Shopify Partners Dashboard\",\n \"Create new app\",\n \"Add redirect URI: http://localhost:3000/api/auth/shopify/callback\",\n \"Copy API Key and Secret to .env\",\n ],\n link: \"https://partners.shopify.com/\",\n envVars: [\"SHOPIFY_API_KEY\", \"SHOPIFY_API_SECRET\"],\n },\n twitter: {\n title: \"Twitter/X OAuth Setup\",\n category: \"marketing\",\n steps: [\n \"Go to Twitter Developer Portal\",\n \"Create new project and app\",\n \"Enable OAuth 2.0\",\n \"Add redirect URI: http://localhost:3000/api/auth/twitter/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://developer.twitter.com/en/portal/dashboard\",\n envVars: [\"TWITTER_CLIENT_ID\", \"TWITTER_CLIENT_SECRET\"],\n },\n figma: {\n title: \"Figma OAuth Setup\",\n category: \"design\",\n steps: [\n \"Go to Figma Developer Settings\",\n \"Create new app\",\n \"Add redirect URI: http://localhost:3000/api/auth/figma/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://www.figma.com/developers/apps\",\n envVars: [\"FIGMA_CLIENT_ID\", \"FIGMA_CLIENT_SECRET\"],\n },\n anthropic: {\n title: \"Anthropic API Setup\",\n category: \"ai\",\n steps: [\"Go to Anthropic Console\", \"Create new API key\", \"Copy API key to .env\"],\n link: \"https://console.anthropic.com/\",\n envVars: [\"ANTHROPIC_API_KEY\"],\n },\n};\n\nexport function filterIntegrations(\n integrations: Integration[],\n searchQuery: string,\n selectedCategory: string | null,\n): Integration[] {\n const query = searchQuery.toLowerCase();\n\n return integrations.filter((integration) => {\n const guide = OAUTH_SETUP_GUIDES[integration.id];\n\n const matchesSearch =\n query === \"\" ||\n integration.name.toLowerCase().includes(query) ||\n integration.id.toLowerCase().includes(query);\n\n const matchesCategory = selectedCategory === null || guide?.category === selectedCategory;\n\n return matchesSearch && matchesCategory;\n });\n}\n\nexport function groupIntegrationsByCategory(\n integrations: Integration[],\n): Record<string, Integration[]> {\n const groups: Record<string, Integration[]> = {};\n\n for (const integration of integrations) {\n const category = OAUTH_SETUP_GUIDES[integration.id]?.category ?? \"other\";\n (groups[category] ??= []).push(integration);\n }\n\n return groups;\n}\n\nexport function buildSetupSteps(\n envChecked: boolean,\n allConnected: boolean,\n markEnvChecked: () => void,\n): SetupStep[] {\n return [\n {\n id: \"env\",\n title: \"Configure Environment Variables\",\n description: \"Add your OAuth credentials to the .env file\",\n completed: envChecked,\n action: markEnvChecked,\n },\n {\n id: \"oauth\",\n title: \"Create OAuth Apps\",\n description: \"Set up OAuth applications for each service\",\n completed: false,\n },\n {\n id: \"connect\",\n title: \"Connect Services\",\n description: \"Authorize your app to access each service\",\n completed: allConnected,\n },\n ];\n}\n\nexport function getTokenStorageStyles(\n tokenStorage: TokenStorageStatus | null,\n): TokenStorageStyles | null {\n if (!tokenStorage) return null;\n\n const isMemory = tokenStorage.mode === \"memory\";\n\n return {\n container: `rounded-2xl p-6 shadow-sm border mb-8 ${\n isMemory\n ? \"bg-amber-50 dark:bg-amber-900/20 border-amber-200 dark:border-amber-800\"\n : \"bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800\"\n }`,\n iconWrapper: `w-10 h-10 rounded-full flex items-center justify-center ${\n isMemory ? \"bg-amber-100 dark:bg-amber-900\" : \"bg-green-100 dark:bg-green-900\"\n }`,\n title: `font-semibold ${\n isMemory ? \"text-amber-800 dark:text-amber-200\" : \"text-green-800 dark:text-green-200\"\n }`,\n text: `text-sm mt-1 ${\n isMemory ? \"text-amber-700 dark:text-amber-300\" : \"text-green-700 dark:text-green-300\"\n }`,\n isMemory,\n };\n}\n\nexport function ServiceIcon({ name }: { name: string }): React.JSX.Element {\n const iconMap: Record<string, React.JSX.Element> = {\n mail: (\n <svg className=\"w-6 h-6 text-red-500\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n d=\"M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n fill=\"none\"\n />\n </svg>\n ),\n slack: (\n <svg className=\"w-6 h-6\" viewBox=\"0 0 24 24\" fill=\"none\">\n <path\n d=\"M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313z\"\n fill=\"#E01E5A\"\n />\n <path\n d=\"M8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312z\"\n fill=\"#36C5F0\"\n />\n <path\n d=\"M18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312z\"\n fill=\"#2EB67D\"\n />\n <path\n d=\"M15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z\"\n fill=\"#ECB22E\"\n />\n </svg>\n ),\n calendar: (\n <svg\n className=\"w-6 h-6 text-blue-500\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z\"\n />\n </svg>\n ),\n github: (\n <svg className=\"w-6 h-6\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n fillRule=\"evenodd\"\n d=\"M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z\"\n clipRule=\"evenodd\"\n />\n </svg>\n ),\n jira: (\n <svg className=\"w-6 h-6\" viewBox=\"0 0 24 24\">\n <defs>\n <linearGradient id=\"jira-gradient\" x1=\"98.031%\" x2=\"58.888%\" y1=\".161%\" y2=\"40.766%\">\n <stop offset=\"0%\" stopColor=\"#0052CC\" />\n <stop offset=\"100%\" stopColor=\"#2684FF\" />\n </linearGradient>\n </defs>\n <path\n fill=\"url(#jira-gradient)\"\n d=\"M11.571 11.513H0a5.218 5.218 0 005.232 5.215h2.13v2.057A5.215 5.215 0 0012.575 24V12.518a1.005 1.005 0 00-1.005-1.005z\"\n />\n <path\n fill=\"#2684FF\"\n d=\"M17.151 5.97H5.58a5.215 5.215 0 005.215 5.214h2.129v2.058a5.218 5.218 0 005.232 5.215V6.975a1.005 1.005 0 00-1.005-1.005z\"\n />\n <path\n fill=\"#2684FF\"\n d=\"M22.723.426H11.152a5.215 5.215 0 005.215 5.215h2.129v2.057a5.218 5.218 0 005.232 5.215V1.431a1.005 1.005 0 00-1.005-1.005z\"\n />\n </svg>\n ),\n notion: (\n <svg className=\"w-6 h-6\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M4.459 4.208c.746.606 1.026.56 2.428.466l13.215-.793c.28 0 .047-.28-.046-.326L17.86 1.968c-.42-.326-.98-.7-2.055-.607L3.01 2.295c-.466.046-.56.28-.374.466l1.823 1.447zm.793 3.08v13.904c0 .747.373 1.027 1.214.98l14.523-.84c.84-.046.933-.56.933-1.167V6.354c0-.606-.233-.933-.746-.886l-15.177.887c-.56.046-.747.326-.747.933zm14.337.745c.093.42 0 .84-.42.888l-.7.14v10.264c-.608.327-1.168.514-1.635.514-.746 0-.933-.234-1.495-.933l-4.577-7.186v6.952L12.21 19s0 .84-1.168.84l-3.222.186c-.093-.186 0-.653.327-.746l.84-.233V9.854L7.822 9.76c-.094-.42.14-1.026.793-1.073l3.456-.233 4.764 7.279v-6.44l-1.215-.14c-.093-.514.28-.886.747-.933l3.222-.186zM1.936 1.035l13.31-.98c1.634-.14 2.055-.047 3.082.7l4.249 2.986c.7.513.933.653.933 1.213v16.378c0 1.026-.373 1.634-1.68 1.726l-15.458.934c-.98.047-1.448-.093-1.962-.747l-3.129-4.06c-.56-.747-.793-1.306-.793-1.96V2.667c0-.839.374-1.54 1.448-1.632z\" />\n </svg>\n ),\n default: (\n <svg\n className=\"w-6 h-6 text-neutral-400\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M13 10V3L4 14h7v7l9-11h-7z\"\n />\n </svg>\n ),\n };\n\n return iconMap[name] ?? iconMap.default;\n}\n",
129
- "app/setup/page.tsx": "\"use client\";\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport {\n buildSetupSteps,\n CATEGORIES,\n filterIntegrations,\n getTokenStorageStyles,\n groupIntegrationsByCategory,\n type Integration,\n OAUTH_SETUP_GUIDES,\n ServiceIcon,\n type TokenStorageStatus,\n} from \"./page-helpers\";\n\nexport default function SetupPage(): React.JSX.Element {\n const [integrations, setIntegrations] = useState<Integration[]>([]);\n const [loading, setLoading] = useState(true);\n const [expandedGuide, setExpandedGuide] = useState<string | null>(null);\n const [envChecked, setEnvChecked] = useState(false);\n const [searchQuery, setSearchQuery] = useState(\"\");\n const [selectedCategory, setSelectedCategory] = useState<string | null>(null);\n const [tokenStorage, setTokenStorage] = useState<TokenStorageStatus | null>(null);\n\n useEffect(() => {\n void fetchStatus();\n void fetchTokenStorage();\n }, []);\n\n async function fetchStatus(): Promise<void> {\n try {\n const res = await fetch(\"/api/integrations/status\");\n if (!res.ok) {\n console.error(\"Failed to fetch integration status:\", res.status);\n setIntegrations([]);\n return;\n }\n\n const data = await res.json();\n setIntegrations(data.integrations ?? []);\n } catch (error) {\n console.error(\"Failed to fetch integration status:\", error);\n setIntegrations([]);\n } finally {\n setLoading(false);\n }\n }\n\n async function fetchTokenStorage(): Promise<void> {\n const fallback: TokenStorageStatus = { mode: \"memory\", encrypted: false };\n\n try {\n const res = await fetch(\"/api/integrations/token-storage\");\n if (!res.ok) {\n setTokenStorage(fallback);\n return;\n }\n const data = await res.json();\n setTokenStorage(data);\n } catch {\n setTokenStorage(fallback);\n }\n }\n\n const filteredIntegrations = useMemo(\n () => filterIntegrations(integrations, searchQuery, selectedCategory),\n [integrations, searchQuery, selectedCategory],\n );\n\n const groupedIntegrations = useMemo(\n () => groupIntegrationsByCategory(filteredIntegrations),\n [filteredIntegrations],\n );\n\n const connectedCount = integrations.filter((i) => i.connected).length;\n const totalCount = integrations.length;\n const progress = totalCount > 0 ? (connectedCount / totalCount) * 100 : 0;\n\n const allConnected = connectedCount === totalCount && totalCount > 0;\n\n const setupSteps = useMemo(\n () => buildSetupSteps(envChecked, allConnected, () => setEnvChecked(true)),\n [allConnected, envChecked],\n );\n\n const tokenStorageStyles = useMemo(() => getTokenStorageStyles(tokenStorage), [tokenStorage]);\n\n return (\n <div className=\"min-h-screen bg-neutral-50 dark:bg-neutral-900\">\n <div className=\"max-w-4xl mx-auto px-4 py-12\">\n <div className=\"text-center mb-12\">\n <h1 className=\"text-4xl font-bold text-neutral-900 dark:text-white mb-4\">\n Setup Your AI Agent\n </h1>\n <p className=\"text-lg text-neutral-600 dark:text-neutral-400\">\n Connect your services to enable AI-powered automation\n </p>\n </div>\n\n <div className=\"bg-white dark:bg-neutral-800 rounded-2xl p-6 shadow-sm border border-neutral-200 dark:border-neutral-700 mb-8\">\n <div className=\"flex items-center justify-between mb-2\">\n <span className=\"text-sm font-medium text-neutral-600 dark:text-neutral-400\">\n Setup Progress\n </span>\n <span className=\"text-sm font-medium text-neutral-900 dark:text-white\">\n {connectedCount} / {totalCount} services connected\n </span>\n </div>\n <div className=\"w-full bg-neutral-200 dark:bg-neutral-700 rounded-full h-3\">\n <div\n className=\"bg-gradient-to-r from-green-500 to-emerald-500 h-3 rounded-full transition-all duration-500\"\n style={{ width: `${progress}%` }}\n />\n </div>\n </div>\n\n {tokenStorage && tokenStorageStyles && (\n <div className={tokenStorageStyles.container}>\n <div className=\"flex items-start gap-4\">\n <div className={tokenStorageStyles.iconWrapper}>\n {tokenStorageStyles.isMemory ? (\n <svg\n className=\"w-5 h-5 text-amber-600 dark:text-amber-400\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\"\n />\n </svg>\n ) : (\n <svg\n className=\"w-5 h-5 text-green-600 dark:text-green-400\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z\"\n />\n </svg>\n )}\n </div>\n\n <div className=\"flex-1\">\n <h3 className={tokenStorageStyles.title}>\n Token Storage:{\" \"}\n {tokenStorageStyles.isMemory\n ? \"Development Mode\"\n : `${tokenStorage.mode.charAt(0).toUpperCase()}${tokenStorage.mode.slice(\n 1,\n )} Storage`}\n </h3>\n\n <p className={tokenStorageStyles.text}>\n {tokenStorageStyles.isMemory ? (\n <>Tokens are stored in memory and will be lost on restart.</>\n ) : (\n <>Tokens are persisted to {tokenStorage.mode} storage.</>\n )}\n </p>\n\n <div className=\"mt-2 flex items-center gap-1.5 text-sm text-green-600 dark:text-green-400\">\n <svg className=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z\"\n />\n </svg>\n <span>Encryption enabled {tokenStorage.autoGenerated && \"(auto-generated key)\"}</span>\n </div>\n\n {tokenStorageStyles.isMemory && (\n <div className=\"mt-4 pt-4 border-t border-amber-200 dark:border-amber-800\">\n <p className=\"text-sm font-medium text-amber-800 dark:text-amber-200 mb-3\">\n For production, add one of these to your{\" \"}\n <code className=\"px-1 py-0.5 bg-amber-100 dark:bg-amber-900 rounded text-xs\">\n .env\n </code>\n :\n </p>\n <div className=\"grid gap-2\">\n <a\n href=\"https://upstash.com/docs/redis/overall/getstarted\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center justify-between p-3 bg-white dark:bg-neutral-800 rounded-lg border border-green-200 dark:border-green-700 hover:border-green-400 dark:hover:border-green-500 transition-colors group\"\n >\n <div>\n <span className=\"font-medium text-neutral-900 dark:text-white\">\n Upstash\n </span>\n <span className=\"text-green-600 dark:text-green-400 text-xs ml-2 font-medium\">\n Recommended\n </span>\n <span className=\"text-neutral-500 dark:text-neutral-400 text-sm ml-2\">\n Serverless Redis, scales horizontally\n </span>\n </div>\n <code className=\"text-xs bg-neutral-100 dark:bg-neutral-700 px-2 py-1 rounded text-neutral-600 dark:text-neutral-300\">\n REDIS_URL\n </code>\n </a>\n\n <a\n href=\"https://docs.turso.tech/quickstart\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center justify-between p-3 bg-white dark:bg-neutral-800 rounded-lg border border-amber-200 dark:border-amber-700 hover:border-amber-400 dark:hover:border-amber-500 transition-colors group\"\n >\n <div>\n <span className=\"font-medium text-neutral-900 dark:text-white\">\n Turso / libSQL\n </span>\n <span className=\"text-neutral-500 dark:text-neutral-400 text-sm ml-2\">\n Edge SQLite, fast reads globally\n </span>\n </div>\n <code className=\"text-xs bg-neutral-100 dark:bg-neutral-700 px-2 py-1 rounded text-neutral-600 dark:text-neutral-300\">\n DATABASE_URL\n </code>\n </a>\n\n <a\n href=\"https://vercel.com/docs/storage/vercel-kv/quickstart\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center justify-between p-3 bg-white dark:bg-neutral-800 rounded-lg border border-amber-200 dark:border-amber-700 hover:border-amber-400 dark:hover:border-amber-500 transition-colors group\"\n >\n <div>\n <span className=\"font-medium text-neutral-900 dark:text-white\">\n Vercel KV\n </span>\n <span className=\"text-neutral-500 dark:text-neutral-400 text-sm ml-2\">\n Built-in if using Vercel\n </span>\n </div>\n <code className=\"text-xs bg-neutral-100 dark:bg-neutral-700 px-2 py-1 rounded text-neutral-600 dark:text-neutral-300\">\n KV_REST_API_URL\n </code>\n </a>\n\n <a\n href=\"https://neon.tech/docs/get-started-with-neon/connect-neon\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center justify-between p-3 bg-white dark:bg-neutral-800 rounded-lg border border-amber-200 dark:border-amber-700 hover:border-amber-400 dark:hover:border-amber-500 transition-colors group\"\n >\n <div>\n <span className=\"font-medium text-neutral-900 dark:text-white\">Neon</span>\n <span className=\"text-neutral-500 dark:text-neutral-400 text-sm ml-2\">\n Serverless Postgres\n </span>\n </div>\n <code className=\"text-xs bg-neutral-100 dark:bg-neutral-700 px-2 py-1 rounded text-neutral-600 dark:text-neutral-300\">\n DATABASE_URL\n </code>\n </a>\n\n <a\n href=\"https://www.sqlite.org/index.html\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center justify-between p-3 bg-white dark:bg-neutral-800 rounded-lg border border-amber-200 dark:border-amber-700 hover:border-amber-400 dark:hover:border-amber-500 transition-colors group\"\n >\n <div>\n <span className=\"font-medium text-neutral-900 dark:text-white\">\n SQLite\n </span>\n <span className=\"text-neutral-500 dark:text-neutral-400 text-sm ml-2\">\n Local file, single instance only\n </span>\n </div>\n <code className=\"text-xs bg-neutral-100 dark:bg-neutral-700 px-2 py-1 rounded text-neutral-600 dark:text-neutral-300\">\n DATABASE_URL=file:./data.db\n </code>\n </a>\n </div>\n </div>\n )}\n </div>\n </div>\n </div>\n )}\n\n <div className=\"bg-white dark:bg-neutral-800 rounded-2xl shadow-sm border border-neutral-200 dark:border-neutral-700 mb-8 overflow-hidden\">\n <div className=\"p-6 border-b border-neutral-200 dark:border-neutral-700\">\n <h2 className=\"text-xl font-semibold text-neutral-900 dark:text-white\">\n Quick Start Guide\n </h2>\n </div>\n <div className=\"divide-y divide-neutral-200 dark:divide-neutral-700\">\n {setupSteps.map((step, index) => (\n <div key={step.id} className=\"p-6 flex items-start gap-4\">\n <div\n className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${\n step.completed\n ? \"bg-green-500 text-white\"\n : \"bg-neutral-200 dark:bg-neutral-700 text-neutral-600 dark:text-neutral-400\"\n }`}\n >\n {step.completed ? (\n <svg className=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M5 13l4 4L19 7\"\n />\n </svg>\n ) : (\n <span className=\"font-semibold\">{index + 1}</span>\n )}\n </div>\n <div className=\"flex-1\">\n <h3 className=\"font-semibold text-neutral-900 dark:text-white\">{step.title}</h3>\n <p className=\"text-sm text-neutral-600 dark:text-neutral-400 mt-1\">\n {step.description}\n </p>\n </div>\n </div>\n ))}\n </div>\n </div>\n\n <div className=\"bg-white dark:bg-neutral-800 rounded-2xl shadow-sm border border-neutral-200 dark:border-neutral-700 overflow-hidden\">\n <div className=\"p-6 border-b border-neutral-200 dark:border-neutral-700\">\n <h2 className=\"text-xl font-semibold text-neutral-900 dark:text-white\">\n Service Connections\n </h2>\n <p className=\"text-sm text-neutral-600 dark:text-neutral-400 mt-1\">\n Click on a service to see setup instructions or connect\n </p>\n\n <div className=\"mt-4\">\n <input\n type=\"text\"\n placeholder=\"Search services...\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n className=\"w-full px-4 py-2 bg-neutral-100 dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-700 rounded-xl text-neutral-900 dark:text-white placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-blue-500\"\n />\n </div>\n\n <div className=\"mt-4 flex flex-wrap gap-2\">\n <button\n type=\"button\"\n onClick={() => setSelectedCategory(null)}\n className={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors ${\n selectedCategory === null\n ? \"bg-neutral-900 dark:bg-white text-white dark:text-neutral-900\"\n : \"bg-neutral-100 dark:bg-neutral-700 text-neutral-600 dark:text-neutral-300 hover:bg-neutral-200 dark:hover:bg-neutral-600\"\n }`}\n >\n All\n </button>\n\n {CATEGORIES.map((category) => (\n <button\n key={category.id}\n type=\"button\"\n onClick={() =>\n setSelectedCategory(selectedCategory === category.id ? null : category.id)\n }\n className={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors ${\n selectedCategory === category.id\n ? \"bg-neutral-900 dark:bg-white text-white dark:text-neutral-900\"\n : \"bg-neutral-100 dark:bg-neutral-700 text-neutral-600 dark:text-neutral-300 hover:bg-neutral-200 dark:hover:bg-neutral-600\"\n }`}\n >\n {category.name}\n </button>\n ))}\n </div>\n </div>\n\n {loading ? (\n <div className=\"p-12 text-center text-neutral-500\">Loading...</div>\n ) : filteredIntegrations.length === 0 ? (\n <div className=\"p-12 text-center text-neutral-500\">\n No services found matching your search\n </div>\n ) : (\n <div>\n {CATEGORIES.filter((cat) => groupedIntegrations[cat.id]?.length > 0).map(\n (category) => (\n <div key={category.id}>\n <div className=\"px-6 py-3 bg-neutral-50 dark:bg-neutral-900 border-b border-neutral-200 dark:border-neutral-700\">\n <h3 className=\"text-sm font-semibold text-neutral-500 dark:text-neutral-400 uppercase tracking-wider\">\n {category.name}\n </h3>\n </div>\n\n <div className=\"divide-y divide-neutral-200 dark:divide-neutral-700\">\n {groupedIntegrations[category.id]?.map((integration) => {\n const guide = OAUTH_SETUP_GUIDES[integration.id];\n const isExpanded = expandedGuide === integration.id;\n\n return (\n <div key={integration.id}>\n <div className=\"p-6 flex items-center justify-between\">\n <div className=\"flex items-center gap-4\">\n <div className=\"w-12 h-12 bg-neutral-100 dark:bg-neutral-700 rounded-xl flex items-center justify-center\">\n <ServiceIcon name={integration.icon} />\n </div>\n <div>\n <h3 className=\"font-semibold text-neutral-900 dark:text-white\">\n {integration.name}\n </h3>\n <p\n className={`text-sm ${\n integration.connected\n ? \"text-green-600 dark:text-green-400\"\n : \"text-neutral-500\"\n }`}\n >\n {integration.connected ? \"Connected\" : \"Not connected\"}\n </p>\n </div>\n </div>\n\n <div className=\"flex items-center gap-3\">\n {guide && (\n <button\n type=\"button\"\n onClick={() =>\n setExpandedGuide(isExpanded ? null : integration.id)\n }\n className=\"px-4 py-2 text-sm font-medium text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white\"\n >\n {isExpanded ? \"Hide Guide\" : \"Setup Guide\"}\n </button>\n )}\n\n {integration.connected ? (\n <span className=\"inline-flex items-center gap-1.5 px-4 py-2 bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-400 rounded-xl text-sm font-medium\">\n <span className=\"w-2 h-2 bg-green-500 rounded-full\" />\n Connected\n </span>\n ) : (\n <a\n href={integration.connectUrl}\n className=\"px-4 py-2 bg-neutral-900 dark:bg-white text-white dark:text-neutral-900 rounded-xl text-sm font-medium hover:opacity-90 transition-opacity\"\n >\n Connect\n </a>\n )}\n </div>\n </div>\n\n {isExpanded && guide && (\n <div className=\"px-6 pb-6\">\n <div className=\"bg-neutral-50 dark:bg-neutral-900 rounded-xl p-6 border border-neutral-200 dark:border-neutral-700\">\n <h4 className=\"font-semibold text-neutral-900 dark:text-white mb-4\">\n {guide.title}\n </h4>\n\n <ol className=\"space-y-3 mb-6\">\n {guide.steps.map((step, i) => (\n <li key={i} className=\"flex items-start gap-3\">\n <span className=\"w-6 h-6 bg-neutral-200 dark:bg-neutral-700 rounded-full flex items-center justify-center text-sm font-medium text-neutral-600 dark:text-neutral-400 flex-shrink-0\">\n {i + 1}\n </span>\n <span className=\"text-neutral-700 dark:text-neutral-300\">\n {step}\n </span>\n </li>\n ))}\n </ol>\n\n <div className=\"mb-4 p-4 bg-neutral-100 dark:bg-neutral-800 rounded-lg\">\n <h5 className=\"text-sm font-semibold text-neutral-700 dark:text-neutral-300 mb-2\">\n Required Environment Variables:\n </h5>\n <pre className=\"text-sm text-neutral-600 dark:text-neutral-400 font-mono whitespace-pre-wrap\">\n {guide.envVars.map((v) => `${v}=your_value`).join(\"\\n\")}\n </pre>\n </div>\n\n <a\n href={guide.link}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex items-center gap-2 text-blue-600 dark:text-blue-400 text-sm font-medium hover:underline\"\n >\n Open Developer Console\n <svg\n className=\"w-4 h-4\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14\"\n />\n </svg>\n </a>\n </div>\n </div>\n )}\n </div>\n );\n })}\n </div>\n </div>\n ),\n )}\n </div>\n )}\n </div>\n\n {allConnected && (\n <div className=\"mt-8 bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 rounded-2xl p-6 border border-green-200 dark:border-green-800 text-center\">\n <div className=\"text-4xl mb-4\">🎉</div>\n <h3 className=\"text-xl font-semibold text-green-800 dark:text-green-200 mb-2\">\n All Services Connected!\n </h3>\n <p className=\"text-green-700 dark:text-green-300 mb-4\">\n Your AI agent is ready to use. Start chatting to automate your workflows.\n </p>\n <a\n href=\"/\"\n className=\"inline-flex items-center gap-2 px-6 py-3 bg-green-600 text-white rounded-xl font-medium hover:bg-green-700 transition-colors\"\n >\n Start Using Your Agent\n <svg className=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M13 7l5 5m0 0l-5 5m5-5H6\"\n />\n </svg>\n </a>\n </div>\n )}\n </div>\n </div>\n );\n}\n",
130
- "lib/encrypted-token-store.ts": "/**\n * Encrypted key-value OAuth token store for generated integrations.\n *\n * Wraps any durable key-value service (Redis, Postgres, Deno KV, a cloud KV\n * API) in the `RefreshCapableTokenStore` contract that `configureTokenStore`\n * in `token-store.ts` expects. Every value is encrypted at rest with\n * AES-256-GCM via the Web Crypto API before it reaches the backend:\n *\n * - A fresh random 96-bit IV is generated for every encryption.\n * - The storage key is bound as AES-GCM additional authenticated data, so a\n * ciphertext copied between storage slots fails authentication.\n * - The key comes from the `TOKEN_ENCRYPTION_KEY` environment variable\n * (64 hex characters = 256 bits). There is NO plaintext fallback: creating\n * the store without a valid key throws, and values that are not in the\n * expected encrypted envelope are refused on read.\n *\n * Key rotation: set `TOKEN_ENCRYPTION_KEY` to the new key and move the old\n * key to `TOKEN_ENCRYPTION_KEY_PREVIOUS`. New writes are sealed with the new\n * key (the v2 envelope records a key id derived from the key), while rows\n * sealed with the previous key stay readable. Every token row decrypted with\n * a non-current key is transparently re-sealed with the current key: always\n * on the next write, and best-effort on every read, so rotation converges\n * even for rows that are read but never rewritten. Use\n * `checkEncryptedTokenStoreRotation` to confirm no rows still need the\n * previous key, then remove `TOKEN_ENCRYPTION_KEY_PREVIOUS`; stragglers\n * degrade to \"disconnected\" and recover on reconnect.\n *\n * Legacy v1 envelopes (no key id) were written only by earlier revisions of\n * this template; they are decrypted by trying every configured key and are\n * upgraded to v2 by the same re-seal-on-read path, so the compatibility\n * branch retires itself as rows are read.\n *\n * Undecryptable token rows (unknown key, tampering, legacy plaintext) never\n * fail a whole request: the token read paths log a warning and report the\n * integration as disconnected, so the recovery is simply reconnecting (a\n * fresh `setTokens` overwrites the row; `clearTokens` removes it).\n *\n * Generate a key once per deployment and set it before startup:\n *\n * ```sh\n * openssl rand -hex 32\n * ```\n *\n * Concurrency (compare-and-swap, refresh locking) is delegated to the\n * backend so the guarantees hold across workers; this module never emulates\n * distributed behavior in process memory. See `token-store-examples.ts` for\n * reference backends and wiring.\n */\n\nimport type {\n OAuthTokens,\n OAuthTokenSnapshot,\n RefreshCapableTokenStore,\n StoredOAuthState,\n} from \"veryfront/oauth\";\n\n/**\n * Minimal durable backend contract. All five operations are required; the\n * atomic ones are what make token refresh and one-shot OAuth state safe\n * across workers.\n */\nexport interface EncryptedKvBackend {\n /** Read the raw stored value for a key, or null when absent. */\n get(key: string): Promise<string | null>;\n /**\n * Durably write a value, replacing any existing one. `expiresInMs`, when\n * provided, is a TTL after which the backend may drop the row.\n */\n set(key: string, value: string, options?: { expiresInMs?: number }): Promise<void>;\n /** Remove a key. Deleting an absent key is not an error. */\n delete(key: string): Promise<void>;\n /**\n * Atomically replace `expected` with `next`. `expected === null` requires\n * the key to be absent; `next === null` deletes the key. Returns false\n * (without writing) when the current value does not match `expected`.\n */\n compareAndSwap(\n key: string,\n expected: string | null,\n next: string | null,\n options?: { expiresInMs?: number },\n ): Promise<boolean>;\n /**\n * Run `operation` while holding a mutual-exclusion lease for `key` that is\n * visible to every worker (for example a Redis lock or an advisory lock).\n * The lease must be bounded so a crashed holder cannot block refresh\n * forever.\n */\n withLock<T>(key: string, operation: () => Promise<T>): Promise<T>;\n}\n\nexport interface EncryptedKvRotationScanBackend extends EncryptedKvBackend {\n /**\n * Iterate stored rows whose key starts with `prefix`. Use a backend-native\n * bounded cursor or paginated scan; do not load an unbounded keyspace into\n * memory before yielding.\n */\n scan(prefix: string): AsyncIterable<{ key: string; value: string }>;\n}\n\nexport interface EncryptedTokenStoreRotationReport {\n scannedRows: number;\n currentKeyRows: number;\n previousKeyRows: number;\n unreadableRows: number;\n complete: boolean;\n}\n\nconst ENCRYPTION_KEY_ENV_VAR = \"TOKEN_ENCRYPTION_KEY\";\nconst PREVIOUS_ENCRYPTION_KEY_ENV_VAR = \"TOKEN_ENCRYPTION_KEY_PREVIOUS\";\nconst ENVELOPE_PREFIX = \"vf-aes-gcm.v2:\";\nconst LEGACY_ENVELOPE_PREFIX = \"vf-aes-gcm.v1:\";\nconst KEY_ID_HEX_LENGTH = 16;\nconst KEY_ID_PATTERN = /^[0-9a-f]{16}$/;\nconst AES_GCM_IV_BYTES = 12;\nconst AES_GCM_TAG_BYTES = 16;\nconst AES_KEY_BYTES = 32;\nconst MAX_PLAINTEXT_BYTES = 64 * 1024;\nconst MAX_ENCRYPTED_BYTES = MAX_PLAINTEXT_BYTES + AES_GCM_IV_BYTES + AES_GCM_TAG_BYTES;\nconst MAX_ENCODED_LENGTH = Math.ceil(MAX_ENCRYPTED_BYTES / 3) * 4 + ENVELOPE_PREFIX.length +\n KEY_ID_HEX_LENGTH + 1;\nconst BASE64_CHUNK_BYTES = 0x8000;\nconst MAX_KEY_COMPONENT_LENGTH = 1_024;\nconst MAX_STATE_KEY_LENGTH = 1_024;\nconst STATE_TTL_MS = 10 * 60 * 1_000;\nconst STATE_CLOCK_SKEW_MS = 60 * 1_000;\nconst MAX_SERVICE_ID_LENGTH = 128;\nconst MAX_SCOPE_COUNT = 100;\nconst MAX_REDIRECT_URI_LENGTH = 8_192;\nconst MAX_TOKEN_VALUE_LENGTH = 65_536;\nconst MAX_TOKEN_TYPE_LENGTH = 256;\nconst MAX_SCOPE_WIRE_LENGTH = 4_096;\n// A JSON array containing one-character values needs two bytes per value once\n// separators are included. Bounding the traversal before cloning therefore\n// prevents sparse arrays or deeply nested metadata from consuming memory\n// before the final plaintext-size check can run.\nconst MAX_JSON_VALUE_COUNT = Math.floor((MAX_PLAINTEXT_BYTES + 1) / 2);\nconst MAX_JSON_NESTING_DEPTH = 64;\n\nconst SERVICE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;\nconst SCOPE_TOKEN_PATTERN = /^[\\x21\\x23-\\x5B\\x5D-\\x7E]+$/;\nconst PKCE_VERIFIER_PATTERN = /^[A-Za-z0-9._~-]{43,128}$/;\n\nconst TOKENS_KEY_PREFIX = \"veryfront:oauth:v1:tokens:\";\nconst STATE_KEY_PREFIX = \"veryfront:oauth:v1:state:\";\nconst REFRESH_LOCK_KEY_PREFIX = \"veryfront:oauth:v1:refresh-lock:\";\n\nconst REQUIRED_BACKEND_METHODS = [\n \"get\",\n \"set\",\n \"delete\",\n \"compareAndSwap\",\n \"withLock\",\n] as const;\n\nfunction readEnvironmentVariable(name: string): string | undefined {\n try {\n if (typeof process !== \"undefined\" && process.env) return process.env[name];\n } catch {\n // Deno exposes the Node-compatible `process` global even when env access\n // is denied. Treat that denial as an unavailable value; never bypass it\n // through a second environment API.\n return undefined;\n }\n try {\n return (globalThis as { Deno?: { env?: { get?: (name: string) => string | undefined } } })\n .Deno?.env?.get?.(name);\n } catch {\n return undefined;\n }\n}\n\n/** Generate a fresh 256-bit key encoded as 64 hex characters. */\nexport function generateEncryptionKey(): string {\n return Array.from(crypto.getRandomValues(new Uint8Array(AES_KEY_BYTES)))\n .map((byte) => byte.toString(16).padStart(2, \"0\"))\n .join(\"\");\n}\n\nfunction parseEncryptionKeyHex(keyHex: string, envVar: string): Uint8Array<ArrayBuffer> {\n if (!/^[0-9a-fA-F]{64}$/.test(keyHex)) {\n throw new TypeError(\n `${envVar} must be exactly 64 hexadecimal characters ` +\n \"(a 256-bit AES key). Generate one with `openssl rand -hex 32`.\",\n );\n }\n const key = new Uint8Array(AES_KEY_BYTES);\n for (let index = 0; index < key.length; index++) {\n key[index] = Number.parseInt(keyHex.slice(index * 2, index * 2 + 2), 16);\n }\n return key;\n}\n\n/**\n * Resolve the configured encryption key or fail closed. This store never\n * writes plaintext credentials, so a missing key is a hard error rather than\n * a downgrade.\n */\nfunction requireEncryptionKeyBytes(): Uint8Array<ArrayBuffer> {\n const configured = readEnvironmentVariable(ENCRYPTION_KEY_ENV_VAR);\n if (configured === undefined || configured === \"\") {\n throw new Error(\n `${ENCRYPTION_KEY_ENV_VAR} is not set. The encrypted token store refuses ` +\n \"to persist plaintext OAuth credentials. Generate a key with \" +\n \"`openssl rand -hex 32` (or generateEncryptionKey()) and set \" +\n `${ENCRYPTION_KEY_ENV_VAR} before starting the app.`,\n );\n }\n return parseEncryptionKeyHex(configured, ENCRYPTION_KEY_ENV_VAR);\n}\n\n/**\n * Resolve the decryption key ring: the current key first (used for every\n * new write), then the optional previous key kept readable during rotation.\n */\nfunction resolveEncryptionKeyRing(): Uint8Array<ArrayBuffer>[] {\n const ring = [requireEncryptionKeyBytes()];\n const previous = readEnvironmentVariable(PREVIOUS_ENCRYPTION_KEY_ENV_VAR);\n if (previous !== undefined && previous !== \"\") {\n ring.push(parseEncryptionKeyHex(previous, PREVIOUS_ENCRYPTION_KEY_ENV_VAR));\n }\n return ring;\n}\n\nfunction bytesToBase64(bytes: Uint8Array): string {\n let binary = \"\";\n for (let offset = 0; offset < bytes.byteLength; offset += BASE64_CHUNK_BYTES) {\n const chunk = bytes.subarray(offset, offset + BASE64_CHUNK_BYTES);\n binary += String.fromCharCode(...chunk);\n }\n return btoa(binary);\n}\n\nfunction base64ToBytes(encoded: string): Uint8Array<ArrayBuffer> {\n if (\n encoded.length === 0 || encoded.length % 4 !== 0 ||\n !/^[A-Za-z0-9+/]+={0,2}$/.test(encoded)\n ) {\n throw new TypeError(\"Encrypted OAuth value has invalid base64 encoding\");\n }\n const binary = atob(encoded);\n if (\n binary.length < AES_GCM_IV_BYTES + AES_GCM_TAG_BYTES ||\n binary.length > MAX_ENCRYPTED_BYTES\n ) {\n throw new RangeError(\"Encrypted OAuth value has an invalid size\");\n }\n const bytes = new Uint8Array(binary.length);\n for (let index = 0; index < binary.length; index++) {\n bytes[index] = binary.charCodeAt(index);\n }\n return bytes;\n}\n\nfunction requireKeyComponent(value: string, label: string): string {\n if (\n typeof value !== \"string\" || value.length === 0 ||\n value.length > MAX_KEY_COMPONENT_LENGTH || value.trim() !== value ||\n hasAsciiControlCharacter(value)\n ) {\n throw new TypeError(\n `${label} must be a trimmed, non-empty string of at most ${MAX_KEY_COMPONENT_LENGTH} characters without control characters`,\n );\n }\n return value;\n}\n\nfunction quoteJsonString(value: string): string {\n let quoted = '\"';\n for (let index = 0; index < value.length; index++) {\n const code = value.charCodeAt(index);\n switch (code) {\n case 0x08:\n quoted += \"\\\\b\";\n break;\n case 0x09:\n quoted += \"\\\\t\";\n break;\n case 0x0a:\n quoted += \"\\\\n\";\n break;\n case 0x0c:\n quoted += \"\\\\f\";\n break;\n case 0x0d:\n quoted += \"\\\\r\";\n break;\n case 0x22:\n quoted += '\\\\\"';\n break;\n case 0x5c:\n quoted += \"\\\\\\\\\";\n break;\n default:\n if (code <= 0x1f) {\n quoted += \"\\\\u\" + code.toString(16).padStart(4, \"0\");\n } else if (code >= 0xd800 && code <= 0xdbff) {\n const next = value.charCodeAt(index + 1);\n if (next >= 0xdc00 && next <= 0xdfff) {\n quoted += value[index]! + value[index + 1]!;\n index++;\n } else {\n quoted += \"\\\\u\" + code.toString(16).padStart(4, \"0\");\n }\n } else if (code >= 0xdc00 && code <= 0xdfff) {\n quoted += \"\\\\u\" + code.toString(16).padStart(4, \"0\");\n } else {\n quoted += value[index];\n }\n }\n }\n return quoted + '\"';\n}\n\nfunction jsonArrayFrame(values: readonly string[]): string {\n return \"[\" + values.map(quoteJsonString).join(\",\") + \"]\";\n}\n\nfunction tokensStorageKey(serviceId: string, userId: string): string {\n return TOKENS_KEY_PREFIX + jsonArrayFrame([\n requireKeyComponent(serviceId, \"serviceId\"),\n requireKeyComponent(userId, \"userId\"),\n ]);\n}\n\nfunction refreshLockKey(serviceId: string, userId: string): string {\n return REFRESH_LOCK_KEY_PREFIX + jsonArrayFrame([\n requireKeyComponent(serviceId, \"serviceId\"),\n requireKeyComponent(userId, \"userId\"),\n ]);\n}\n\nfunction stateStorageKey(state: string): string {\n if (typeof state !== \"string\") {\n throw new TypeError(\"state must be a string\");\n }\n if (state.length === 0 || state.length > MAX_STATE_KEY_LENGTH) {\n throw new RangeError(\n `state must contain between 1 and ${MAX_STATE_KEY_LENGTH} characters`,\n );\n }\n if (state.trim() !== state || hasAsciiControlCharacter(state)) {\n throw new TypeError(\"state must not contain surrounding whitespace or control characters\");\n }\n return STATE_KEY_PREFIX + jsonArrayFrame([state]);\n}\n\ninterface StoredTokenEntry {\n revision: string;\n tokens: OAuthTokens;\n}\n\nfunction ownDataValue(record: object, key: string): unknown {\n const descriptor = Object.getOwnPropertyDescriptor(record, key);\n return descriptor && \"value\" in descriptor ? descriptor.value : undefined;\n}\n\nfunction hasAsciiControlCharacter(value: string): boolean {\n for (let index = 0; index < value.length; index++) {\n const code = value.charCodeAt(index);\n if (code <= 0x1f || code === 0x7f) return true;\n }\n return false;\n}\n\ninterface JsonTraversalState {\n ancestors: WeakSet<object>;\n depth: number;\n remainingStringCodeUnits: number;\n remainingValues: number;\n}\n\nfunction createJsonTraversalState(): JsonTraversalState {\n return {\n ancestors: new WeakSet(),\n depth: 0,\n remainingStringCodeUnits: MAX_PLAINTEXT_BYTES,\n remainingValues: MAX_JSON_VALUE_COUNT,\n };\n}\n\nfunction consumeJsonStringBudget(\n state: JsonTraversalState,\n value: string,\n label: string,\n): void {\n if (value.length > state.remainingStringCodeUnits) {\n throw new RangeError(`${label} contains too much JSON string data`);\n }\n state.remainingStringCodeUnits -= value.length;\n}\n\nfunction snapshotJsonData(\n value: unknown,\n label: string,\n state = createJsonTraversalState(),\n): unknown {\n if (state.remainingValues === 0) {\n throw new RangeError(`${label} contains too many JSON values`);\n }\n state.remainingValues--;\n\n if (value === null || typeof value === \"boolean\") {\n return value;\n }\n if (typeof value === \"string\") {\n consumeJsonStringBudget(state, value, label);\n return value;\n }\n if (typeof value === \"number\") {\n if (!Number.isFinite(value)) {\n throw new TypeError(`${label} must contain only finite JSON numbers`);\n }\n return value;\n }\n if (!value || typeof value !== \"object\") {\n throw new TypeError(`${label} must contain only JSON data values`);\n }\n if (state.depth >= MAX_JSON_NESTING_DEPTH) {\n throw new RangeError(\n `${label} exceeds the maximum JSON nesting depth of ${MAX_JSON_NESTING_DEPTH}`,\n );\n }\n if (state.ancestors.has(value)) {\n throw new TypeError(`${label} must not contain cyclic JSON data`);\n }\n state.ancestors.add(value);\n state.depth++;\n\n try {\n if (Array.isArray(value)) {\n if (value.length > state.remainingValues) {\n throw new RangeError(`${label} contains too many JSON values`);\n }\n const snapshot: unknown[] = [];\n snapshot.length = value.length;\n for (let index = 0; index < value.length; index++) {\n const descriptor = Object.getOwnPropertyDescriptor(value, String(index));\n if (!descriptor || !(\"value\" in descriptor)) {\n throw new TypeError(`${label} must contain only own data values`);\n }\n Object.defineProperty(snapshot, String(index), {\n configurable: true,\n enumerable: true,\n value: snapshotJsonData(descriptor.value, label, state),\n writable: true,\n });\n }\n Object.defineProperty(snapshot, \"toJSON\", {\n configurable: true,\n enumerable: false,\n value: undefined,\n });\n return snapshot;\n }\n\n const prototype = Object.getPrototypeOf(value);\n if (prototype !== Object.prototype && prototype !== null) {\n throw new TypeError(`${label} must contain only plain JSON data objects`);\n }\n const keys = Object.keys(value);\n if (keys.length > state.remainingValues) {\n throw new RangeError(`${label} contains too many JSON values`);\n }\n const snapshot: Record<string, unknown> = Object.create(null);\n for (const key of keys) {\n consumeJsonStringBudget(state, key, label);\n const descriptor = Object.getOwnPropertyDescriptor(value, key);\n if (!descriptor || !(\"value\" in descriptor)) {\n throw new TypeError(`${label} must contain only own data values`);\n }\n snapshot[key] = snapshotJsonData(descriptor.value, label, state);\n }\n return snapshot;\n } finally {\n state.depth--;\n state.ancestors.delete(value);\n }\n}\n\nfunction stringifyJsonData(value: unknown): string {\n return JSON.stringify(snapshotJsonData(value, \"Stored OAuth value\"));\n}\n\nfunction requireMetadata(value: unknown): Record<string, unknown> | undefined {\n if (value === undefined) return undefined;\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n throw new TypeError(\"Stored OAuth state metadata must be a plain object\");\n }\n return snapshotJsonData(value, \"Stored OAuth state metadata\") as Record<string, unknown>;\n}\n\nfunction requireOptionalTokenString(\n record: object,\n key: string,\n maxLength: number,\n): string | undefined {\n const value = ownDataValue(record, key);\n if (value === undefined) return undefined;\n if (\n typeof value !== \"string\" || value.length === 0 || value.length > maxLength ||\n value.trim() !== value || hasAsciiControlCharacter(value)\n ) {\n throw new TypeError(`OAuth token row ${key} must be a safe bounded string`);\n }\n return value;\n}\n\nfunction requireTokenRow(value: unknown): OAuthTokens {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n throw new TypeError(\"OAuth token row must be an object\");\n }\n const accessToken = requireOptionalTokenString(value, \"accessToken\", MAX_TOKEN_VALUE_LENGTH);\n if (accessToken === undefined) {\n throw new TypeError(\"OAuth token row must contain a non-empty accessToken\");\n }\n const refreshToken = requireOptionalTokenString(value, \"refreshToken\", MAX_TOKEN_VALUE_LENGTH);\n const tokenType = requireOptionalTokenString(value, \"tokenType\", MAX_TOKEN_TYPE_LENGTH);\n const scopeValue = ownDataValue(value, \"scope\");\n let scope: string | undefined;\n if (scopeValue !== undefined) {\n if (\n typeof scopeValue !== \"string\" || hasAsciiControlCharacter(scopeValue) ||\n scopeValue.length > MAX_SCOPE_WIRE_LENGTH\n ) {\n throw new TypeError(\"OAuth token row scope must be a safe bounded string\");\n }\n scope = scopeValue.trim();\n if (scope.length === 0) {\n throw new TypeError(\"OAuth token row scope must be a safe bounded string\");\n }\n }\n const idToken = requireOptionalTokenString(value, \"idToken\", MAX_TOKEN_VALUE_LENGTH);\n const expiresAt = ownDataValue(value, \"expiresAt\");\n if (\n expiresAt !== undefined &&\n (typeof expiresAt !== \"number\" || !Number.isSafeInteger(expiresAt) || expiresAt < 0)\n ) {\n throw new TypeError(\"OAuth token expiresAt must be a non-negative safe integer\");\n }\n return {\n accessToken,\n ...(refreshToken === undefined ? {} : { refreshToken }),\n ...(expiresAt === undefined ? {} : { expiresAt }),\n ...(tokenType === undefined ? {} : { tokenType }),\n ...(scope === undefined ? {} : { scope }),\n ...(idToken === undefined ? {} : { idToken }),\n };\n}\n\nfunction requireTokenEntry(value: unknown): StoredTokenEntry {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n throw new TypeError(\"Stored OAuth token entry must be an object\");\n }\n const revision = ownDataValue(value, \"revision\");\n if (typeof revision !== \"string\" || revision.length === 0) {\n throw new TypeError(\"Stored OAuth token entry must contain a revision\");\n }\n return { revision, tokens: requireTokenRow(ownDataValue(value, \"tokens\")) };\n}\n\nfunction requireStateRow(value: unknown): StoredOAuthState {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n throw new TypeError(\"Stored OAuth state row must be an object\");\n }\n const userId = ownDataValue(value, \"userId\");\n const serviceId = ownDataValue(value, \"serviceId\");\n const redirectUri = ownDataValue(value, \"redirectUri\");\n const scopes = ownDataValue(value, \"scopes\");\n const createdAt = ownDataValue(value, \"createdAt\");\n const codeVerifier = ownDataValue(value, \"codeVerifier\");\n const metadata = requireMetadata(ownDataValue(value, \"metadata\"));\n if (\n typeof userId !== \"string\" || userId.length === 0 ||\n userId.length > MAX_KEY_COMPONENT_LENGTH || userId.trim() !== userId ||\n hasAsciiControlCharacter(userId)\n ) {\n throw new TypeError(\"Stored OAuth state row must contain a userId\");\n }\n if (\n typeof serviceId !== \"string\" || serviceId.length > MAX_SERVICE_ID_LENGTH ||\n !SERVICE_ID_PATTERN.test(serviceId)\n ) {\n throw new TypeError(\"Stored OAuth state row must contain a serviceId\");\n }\n let parsedRedirectUri: URL;\n try {\n if (\n typeof redirectUri !== \"string\" || redirectUri.length > MAX_REDIRECT_URI_LENGTH ||\n redirectUri.trim() !== redirectUri || hasAsciiControlCharacter(redirectUri) ||\n redirectUri.includes(\"\\\\\")\n ) {\n throw new TypeError();\n }\n parsedRedirectUri = new URL(redirectUri);\n } catch {\n throw new TypeError(\"Stored OAuth state row must contain a valid redirectUri\");\n }\n const isLoopback = parsedRedirectUri.hostname === \"localhost\" ||\n parsedRedirectUri.hostname === \"127.0.0.1\" ||\n parsedRedirectUri.hostname === \"[::1]\" || parsedRedirectUri.hostname === \"::1\";\n if (\n parsedRedirectUri.username || parsedRedirectUri.password || parsedRedirectUri.hash ||\n (parsedRedirectUri.protocol !== \"https:\" &&\n !(parsedRedirectUri.protocol === \"http:\" && isLoopback))\n ) {\n throw new TypeError(\"Stored OAuth state row must contain a valid redirectUri\");\n }\n if (!Array.isArray(scopes) || scopes.length > MAX_SCOPE_COUNT) {\n throw new TypeError(\"Stored OAuth state row must contain valid scopes\");\n }\n const scopeSnapshot: string[] = [];\n for (let index = 0; index < scopes.length; index++) {\n const descriptor = Object.getOwnPropertyDescriptor(scopes, String(index));\n if (\n !descriptor || !(\"value\" in descriptor) || typeof descriptor.value !== \"string\" ||\n !SCOPE_TOKEN_PATTERN.test(descriptor.value)\n ) {\n throw new TypeError(\"Stored OAuth state row must contain valid scopes\");\n }\n scopeSnapshot.push(descriptor.value);\n }\n if (scopeSnapshot.join(\" \").length > MAX_SCOPE_WIRE_LENGTH) {\n throw new TypeError(\"Stored OAuth state row must contain valid scopes\");\n }\n if (typeof createdAt !== \"number\" || !Number.isSafeInteger(createdAt) || createdAt <= 0) {\n throw new TypeError(\"Stored OAuth state row must contain a createdAt timestamp\");\n }\n if (\n codeVerifier !== undefined &&\n (typeof codeVerifier !== \"string\" || !PKCE_VERIFIER_PATTERN.test(codeVerifier))\n ) {\n throw new TypeError(\"Stored OAuth state row has an invalid codeVerifier\");\n }\n return {\n userId,\n serviceId,\n redirectUri,\n scopes: scopeSnapshot,\n createdAt,\n ...(codeVerifier === undefined ? {} : { codeVerifier }),\n ...(metadata === undefined ? {} : { metadata }),\n };\n}\n\nfunction isFreshState(createdAt: number, now: number): boolean {\n if (createdAt > now) {\n return createdAt - now <= STATE_CLOCK_SKEW_MS;\n }\n return now - createdAt <= STATE_TTL_MS;\n}\n\nfunction assertBackend(backend: EncryptedKvBackend): void {\n if (!backend || typeof backend !== \"object\") {\n throw new TypeError(\"Encrypted token store backend must be an object\");\n }\n for (const method of REQUIRED_BACKEND_METHODS) {\n if (typeof backend[method] !== \"function\") {\n throw new TypeError(`Encrypted token store backend must implement ${method}()`);\n }\n }\n}\n\ninterface EnvelopeKey {\n /** First 8 bytes of SHA-256 over the raw key, hex-encoded. */\n keyId: string;\n key: CryptoKey;\n}\n\nasync function importEnvelopeKey(keyBytes: Uint8Array<ArrayBuffer>): Promise<EnvelopeKey> {\n const digest = new Uint8Array(await crypto.subtle.digest(\"SHA-256\", keyBytes));\n const key = await crypto.subtle.importKey(\"raw\", keyBytes, \"AES-GCM\", false, [\n \"encrypt\",\n \"decrypt\",\n ]);\n keyBytes.fill(0);\n const keyId = Array.from(digest.subarray(0, KEY_ID_HEX_LENGTH / 2))\n .map((byte) => byte.toString(16).padStart(2, \"0\"))\n .join(\"\");\n return { keyId, key };\n}\n\ninterface OpenedEnvelope {\n value: unknown;\n /**\n * False when the row was decrypted with a retiring key or arrived in a\n * legacy v1 envelope, i.e. re-sealing it with the current key lets\n * `TOKEN_ENCRYPTION_KEY_PREVIOUS` be dropped sooner.\n */\n sealedWithCurrentKey: boolean;\n}\n\nclass EnvelopeCipher {\n /** The first entry is the current key; every entry may decrypt. */\n readonly #keys: Promise<EnvelopeKey[]>;\n\n constructor(keyRing: readonly Uint8Array<ArrayBuffer>[]) {\n this.#keys = Promise.all(keyRing.map(importEnvelopeKey));\n }\n\n async seal(storageKey: string, value: unknown): Promise<string> {\n const plaintext = new TextEncoder().encode(stringifyJsonData(value));\n if (plaintext.byteLength > MAX_PLAINTEXT_BYTES) {\n throw new RangeError(`Stored OAuth value exceeds ${MAX_PLAINTEXT_BYTES} bytes`);\n }\n const [current] = await this.#keys;\n if (!current) {\n throw new Error(\"Encrypted token store has no encryption key configured\");\n }\n const iv = crypto.getRandomValues(new Uint8Array(AES_GCM_IV_BYTES));\n const ciphertext = new Uint8Array(\n await crypto.subtle.encrypt(\n { name: \"AES-GCM\", iv, additionalData: new TextEncoder().encode(storageKey) },\n current.key,\n plaintext,\n ),\n );\n const combined = new Uint8Array(iv.byteLength + ciphertext.byteLength);\n combined.set(iv);\n combined.set(ciphertext, iv.byteLength);\n return ENVELOPE_PREFIX + current.keyId + \":\" + bytesToBase64(combined);\n }\n\n async open(storageKey: string, stored: string): Promise<OpenedEnvelope> {\n if (typeof stored !== \"string\" || stored.length > MAX_ENCODED_LENGTH) {\n throw new TypeError(\"Stored OAuth value must be a bounded string\");\n }\n const keys = await this.#keys;\n if (stored.startsWith(ENVELOPE_PREFIX)) {\n const body = stored.slice(ENVELOPE_PREFIX.length);\n const keyId = body.slice(0, KEY_ID_HEX_LENGTH);\n if (!KEY_ID_PATTERN.test(keyId) || body[KEY_ID_HEX_LENGTH] !== \":\") {\n throw new TypeError(\"Encrypted OAuth value has a malformed key id\");\n }\n const match = keys.find((entry) => entry.keyId === keyId);\n if (!match) {\n throw new Error(\n `Encrypted OAuth value was sealed with an unknown encryption key (id ${keyId}). ` +\n `Set ${PREVIOUS_ENCRYPTION_KEY_ENV_VAR} to the retiring key during rotation, ` +\n \"or re-authenticate affected users.\",\n );\n }\n return {\n value: await this.#decrypt(\n storageKey,\n base64ToBytes(body.slice(KEY_ID_HEX_LENGTH + 1)),\n match.key,\n ),\n sealedWithCurrentKey: match === keys[0],\n };\n }\n if (stored.startsWith(LEGACY_ENVELOPE_PREFIX)) {\n // v1 envelopes carry no key id, so try every configured key. They are\n // never reported as current: re-sealing upgrades them to v2.\n const combined = base64ToBytes(stored.slice(LEGACY_ENVELOPE_PREFIX.length));\n let lastFailure: unknown;\n for (const entry of keys) {\n try {\n return {\n value: await this.#decrypt(storageKey, combined, entry.key),\n sealedWithCurrentKey: false,\n };\n } catch (failure) {\n lastFailure = failure;\n }\n }\n throw lastFailure;\n }\n throw new Error(\n \"Stored OAuth value is not in a vf-aes-gcm envelope format. This store \" +\n \"never reads plaintext credentials; re-authenticate affected users to \" +\n \"replace legacy rows.\",\n );\n }\n\n async #decrypt(\n storageKey: string,\n combined: Uint8Array<ArrayBuffer>,\n key: CryptoKey,\n ): Promise<unknown> {\n const iv = combined.subarray(0, AES_GCM_IV_BYTES);\n const ciphertext = combined.subarray(AES_GCM_IV_BYTES);\n let plaintext: ArrayBuffer;\n try {\n plaintext = await crypto.subtle.decrypt(\n { name: \"AES-GCM\", iv, additionalData: new TextEncoder().encode(storageKey) },\n key,\n ciphertext,\n );\n } catch (cause) {\n throw new Error(\n \"Encrypted OAuth value failed authentication (wrong key, corrupted \" +\n \"data, or a value moved between storage slots)\",\n { cause },\n );\n }\n try {\n return JSON.parse(new TextDecoder().decode(plaintext));\n } catch (cause) {\n throw new Error(\"Encrypted OAuth value contains invalid JSON\", { cause });\n }\n }\n}\n\nfunction unreadableTokenRowReason(failure: unknown): string {\n if (!(failure instanceof Error)) return \"malformed encrypted row\";\n if (failure.message.includes(\"unknown encryption key\")) return \"unknown encryption key\";\n if (failure.message.includes(\"failed authentication\")) return \"failed authentication\";\n return \"malformed encrypted row\";\n}\n\nfunction unreadableStateRowReason(failure: unknown): string {\n if (!(failure instanceof Error)) return \"malformed encrypted state row\";\n if (failure.message.includes(\"unknown encryption key\")) return \"unknown encryption key\";\n if (failure.message.includes(\"failed authentication\")) return \"failed authentication\";\n return \"malformed encrypted state row\";\n}\n\nfunction assertRotationScanBackend(\n backend: EncryptedKvRotationScanBackend,\n): asserts backend is EncryptedKvRotationScanBackend {\n assertBackend(backend);\n if (typeof backend.scan !== \"function\") {\n throw new TypeError(\"Encrypted token store rotation checks require backend.scan()\");\n }\n}\n\n/**\n * Count token and OAuth state rows that still require\n * `TOKEN_ENCRYPTION_KEY_PREVIOUS`.\n *\n * Run this after rotating keys and after normal reconnect/refresh traffic has\n * had a chance to rewrite rows. When `complete` is true, the scanned rows no\n * longer require the previous key. Unreadable rows are counted separately and\n * should be cleared or replaced before removing the previous key.\n * Expired OAuth state rows are ignored after authenticated decrypt and schema\n * validation because they can no longer be consumed.\n * `complete` describes only rows yielded by the backend; an empty scan reports\n * `complete: true` with `scannedRows: 0`. Confirm the scan covered the expected\n * rows before removing the previous key.\n */\nexport async function checkEncryptedTokenStoreRotation(\n backend: EncryptedKvRotationScanBackend,\n): Promise<EncryptedTokenStoreRotationReport> {\n assertRotationScanBackend(backend);\n const cipher = new EnvelopeCipher(resolveEncryptionKeyRing());\n const report: EncryptedTokenStoreRotationReport = {\n scannedRows: 0,\n currentKeyRows: 0,\n previousKeyRows: 0,\n unreadableRows: 0,\n complete: false,\n };\n const scanNow = Date.now();\n\n for (const prefix of [TOKENS_KEY_PREFIX, STATE_KEY_PREFIX]) {\n for await (const row of backend.scan(prefix)) {\n report.scannedRows++;\n if (\n !row || typeof row !== \"object\" || typeof row.key !== \"string\" ||\n typeof row.value !== \"string\" || !row.key.startsWith(prefix)\n ) {\n report.unreadableRows++;\n continue;\n }\n try {\n const opened = await cipher.open(row.key, row.value);\n if (row.key.startsWith(TOKENS_KEY_PREFIX)) {\n requireTokenEntry(opened.value);\n } else {\n const state = requireStateRow(opened.value);\n if (!isFreshState(state.createdAt, scanNow)) continue;\n }\n if (opened.sealedWithCurrentKey) report.currentKeyRows++;\n else report.previousKeyRows++;\n } catch {\n report.unreadableRows++;\n }\n }\n }\n\n report.complete = report.previousKeyRows === 0 && report.unreadableRows === 0;\n return report;\n}\n\n/**\n * Build a `RefreshCapableTokenStore` over a durable key-value backend with\n * AES-256-GCM encryption at rest.\n *\n * Fails closed at creation time when `TOKEN_ENCRYPTION_KEY` is missing or\n * malformed, and when the backend does not provide the atomic operations\n * that safe multi-worker refresh requires.\n *\n * Wire it once during startup through an explicit configuration boundary:\n *\n * ```ts\n * import { configureTokenStore } from \"./token-store.ts\";\n * import {\n * createEncryptedTokenStore,\n * type EncryptedKvBackend,\n * } from \"./encrypted-token-store.ts\";\n *\n * export function configureOAuthStorage(backend: EncryptedKvBackend): void {\n * configureTokenStore(createEncryptedTokenStore(backend));\n * }\n * ```\n */\nexport function createEncryptedTokenStore(\n backend: EncryptedKvBackend,\n): RefreshCapableTokenStore {\n assertBackend(backend);\n const cipher = new EnvelopeCipher(resolveEncryptionKeyRing());\n\n // Undecryptable or malformed rows degrade to \"absent\" instead of failing\n // the caller: a single bad row must not take down an integrations page.\n // The integration shows as disconnected and reconnecting (setTokens)\n // overwrites the row; clearTokens removes it explicitly. The warning never\n // includes token material.\n async function readTokenEntry(\n serviceId: string,\n userId: string,\n ): Promise<{ key: string; raw: string; entry: StoredTokenEntry } | null> {\n const key = tokensStorageKey(serviceId, userId);\n const raw = await backend.get(key);\n if (raw === null) return null;\n try {\n const opened = await cipher.open(key, raw);\n const entry = requireTokenEntry(opened.value);\n if (opened.sealedWithCurrentKey) return { key, raw, entry };\n return { key, raw: (await resealTokenRow(key, raw, entry)) ?? raw, entry };\n } catch (failure) {\n console.warn(\n \"[Encrypted Token Store] Ignoring unreadable OAuth token row \" +\n `(${unreadableTokenRowReason(failure)}). ` +\n \"The integration is reported as disconnected; reconnecting overwrites the row.\",\n );\n return null;\n }\n }\n\n // Best-effort transparent re-seal so rotation also converges for rows that\n // are read but never rewritten. The revision is preserved (this is a\n // re-encryption, not a logical write), the swap is ABA-safe because every\n // seal uses a fresh IV, and any failure is ignored: the next read simply\n // tries again, and losing the swap to a concurrent writer is fine because\n // that writer already sealed with the current key.\n async function resealTokenRow(\n key: string,\n raw: string,\n entry: StoredTokenEntry,\n ): Promise<string | null> {\n try {\n const resealed = await cipher.seal(key, entry);\n return (await backend.compareAndSwap(key, raw, resealed)) ? resealed : null;\n } catch {\n return null;\n }\n }\n\n return {\n async getTokens(serviceId: string, userId: string): Promise<OAuthTokens | null> {\n return (await readTokenEntry(serviceId, userId))?.entry.tokens ?? null;\n },\n\n async getTokenSnapshot(\n serviceId: string,\n userId: string,\n ): Promise<OAuthTokenSnapshot | null> {\n return (await readTokenEntry(serviceId, userId))?.entry ?? null;\n },\n\n async setTokens(serviceId: string, userId: string, tokens: OAuthTokens): Promise<void> {\n const key = tokensStorageKey(serviceId, userId);\n const entry: StoredTokenEntry = {\n revision: crypto.randomUUID(),\n tokens: requireTokenRow(tokens),\n };\n await backend.set(key, await cipher.seal(key, entry));\n },\n\n async compareAndSetTokens(\n serviceId: string,\n userId: string,\n expectedRevision: string,\n tokens: OAuthTokens,\n ): Promise<boolean> {\n if (typeof expectedRevision !== \"string\" || expectedRevision.length === 0) {\n throw new TypeError(\"Expected OAuth token revision must be a non-empty string\");\n }\n const current = await readTokenEntry(serviceId, userId);\n if (!current || current.entry.revision !== expectedRevision) return false;\n const next: StoredTokenEntry = {\n revision: crypto.randomUUID(),\n tokens: requireTokenRow(tokens),\n };\n return backend.compareAndSwap(\n current.key,\n current.raw,\n await cipher.seal(current.key, next),\n );\n },\n\n withTokenRefreshLock<T>(\n serviceId: string,\n userId: string,\n operation: () => Promise<T>,\n ): Promise<T> {\n return backend.withLock(refreshLockKey(serviceId, userId), operation);\n },\n\n async clearTokens(serviceId: string, userId: string): Promise<void> {\n await backend.delete(tokensStorageKey(serviceId, userId));\n },\n\n async setState(state: string, metadata: StoredOAuthState): Promise<void> {\n const key = stateStorageKey(state);\n const row = requireStateRow(metadata);\n if (!isFreshState(row.createdAt, Date.now())) {\n throw new RangeError(\"OAuth state createdAt is outside the acceptance window\");\n }\n const inserted = await backend.compareAndSwap(\n key,\n null,\n await cipher.seal(key, row),\n { expiresInMs: STATE_TTL_MS + STATE_CLOCK_SKEW_MS },\n );\n if (!inserted) throw new Error(\"OAuth state already exists\");\n },\n\n async consumeState(state: string): Promise<StoredOAuthState | null> {\n const key = stateStorageKey(state);\n const raw = await backend.get(key);\n if (raw === null) return null;\n // One-shot semantics: only the caller that atomically removes the row\n // may redeem it, so a replayed callback cannot reuse the state.\n const consumed = await backend.compareAndSwap(key, raw, null);\n if (!consumed) return null;\n try {\n const row = requireStateRow((await cipher.open(key, raw)).value);\n return isFreshState(row.createdAt, Date.now()) ? row : null;\n } catch (failure) {\n console.warn(\n \"[Encrypted Token Store] Ignoring unreadable OAuth state row \" +\n `(${unreadableStateRowReason(failure)}). ` +\n \"The OAuth callback state is rejected.\",\n );\n return null;\n }\n },\n };\n}\n",
131
- "lib/oauth.ts": "import {\n getRefreshableAccessToken,\n type OAuthToken,\n tokenStore,\n} from \"./token-store.ts\";\n\nexport interface OAuthProvider {\n name: string;\n authorizationUrl: string;\n tokenUrl: string;\n clientId: string;\n clientSecret: string;\n scopes: string[];\n callbackPath: string;\n}\n\nfunction getExpiresAt(expiresIn: unknown): number | undefined {\n if (typeof expiresIn !== \"number\" || expiresIn <= 0) return undefined;\n return Date.now() + expiresIn * 1000;\n}\n\nasync function postTokenRequest(\n provider: OAuthProvider,\n body: Record<string, string>,\n errorPrefix: string,\n): Promise<any> {\n const response = await fetch(provider.tokenUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams(body),\n });\n\n if (response.ok) return response.json();\n\n const error = await response.text();\n throw new Error(`${errorPrefix}: ${response.status} - ${error}`);\n}\n\nexport function getAuthorizationUrl(\n provider: OAuthProvider,\n state: string,\n redirectUri: string,\n): string {\n const params = new URLSearchParams({\n client_id: provider.clientId,\n redirect_uri: redirectUri,\n response_type: \"code\",\n scope: provider.scopes.join(\" \"),\n state,\n access_type: \"offline\",\n prompt: \"consent\",\n });\n\n return `${provider.authorizationUrl}?${params.toString()}`;\n}\n\nexport async function exchangeCodeForTokens(\n provider: OAuthProvider,\n code: string,\n redirectUri: string,\n): Promise<OAuthToken> {\n const data = await postTokenRequest(\n provider,\n {\n client_id: provider.clientId,\n client_secret: provider.clientSecret,\n code,\n grant_type: \"authorization_code\",\n redirect_uri: redirectUri,\n },\n \"Token exchange failed\",\n );\n\n return {\n accessToken: data.access_token,\n refreshToken: data.refresh_token,\n expiresAt: getExpiresAt(data.expires_in),\n tokenType: data.token_type ?? \"Bearer\",\n scope: data.scope,\n };\n}\n\nexport async function refreshAccessToken(\n provider: OAuthProvider,\n refreshToken: string,\n): Promise<OAuthToken> {\n const data = await postTokenRequest(\n provider,\n {\n client_id: provider.clientId,\n client_secret: provider.clientSecret,\n refresh_token: refreshToken,\n grant_type: \"refresh_token\",\n },\n \"Token refresh failed\",\n );\n\n return {\n accessToken: data.access_token,\n refreshToken: data.refresh_token ?? refreshToken,\n expiresAt: getExpiresAt(data.expires_in),\n tokenType: data.token_type ?? \"Bearer\",\n scope: data.scope,\n };\n}\n\nexport async function getValidToken(\n provider: OAuthProvider,\n userId: string,\n service: string,\n): Promise<string | null> {\n return await getRefreshableAccessToken(\n tokenStore,\n service,\n userId,\n (refreshToken) => refreshAccessToken(provider, refreshToken),\n );\n}\n",
132
- "lib/token-store-examples.ts": "/**\n * Reference backends for `createEncryptedTokenStore` in\n * `encrypted-token-store.ts`.\n *\n * The in-memory backend below is for local development and tests only: it is\n * process-local, so tokens vanish on restart and are not shared across\n * workers. For production, implement `EncryptedKvBackend` over a durable\n * service and pass it into startup through an explicit configuration\n * boundary. This example is complete and does not rely on module globals:\n *\n * ```ts\n * import { configureTokenStore } from \"./token-store.ts\";\n * import {\n * createEncryptedTokenStore,\n * type EncryptedKvBackend,\n * } from \"./encrypted-token-store.ts\";\n *\n * export function configureOAuthStorage(backend: EncryptedKvBackend): void {\n * configureTokenStore(createEncryptedTokenStore(backend));\n * }\n * ```\n *\n * Redis adapter sketch (pseudocode, not a paste-ready client): replace every\n * angle-bracketed operation with the equivalent atomic operation from your\n * initialized Redis client.\n *\n * ```text\n * const redisBackend: EncryptedKvBackend = {\n * get: (key) => <redis client get>(key),\n * set: async (key, value, options) => {\n * await <redis client set>(key, value, options?.expiresInMs);\n * },\n * delete: (key) => <redis client delete>(key),\n * compareAndSwap: (key, expected, next, options) =>\n * <atomic WATCH/MULTI or Lua CAS>(key, expected, next, options?.expiresInMs),\n * withLock: (key, operation) =>\n * <bounded, renewable, fenced Redis lease>(key, operation),\n * };\n * ```\n */\n\nimport type { EncryptedKvBackend } from \"./encrypted-token-store.ts\";\n\nfunction runtimeMode(): string | undefined {\n try {\n if (typeof process !== \"undefined\" && process.env) return process.env.NODE_ENV;\n } catch {\n // Deno exposes the Node-compatible `process` global even when env access\n // is denied. Preserve the fail-closed mode decision in that runtime.\n return undefined;\n }\n try {\n return (globalThis as { Deno?: { env?: { get?: (name: string) => string | undefined } } })\n .Deno?.env?.get?.(\"NODE_ENV\");\n } catch {\n return undefined;\n }\n}\n\ninterface MemoryRow {\n value: string;\n expiresAt: number | null;\n}\n\n/**\n * Development/test in-memory backend. Values are still encrypted (the store\n * requires `TOKEN_ENCRYPTION_KEY` in every mode) but nothing is durable and\n * nothing is shared across workers, so creation is refused in production.\n */\nexport function createMemoryKvBackend(): EncryptedKvBackend {\n const mode = runtimeMode();\n if (mode !== \"development\" && mode !== \"test\") {\n throw new Error(\n mode === \"production\"\n ? \"The in-memory example backend is not allowed in production. Implement \" +\n \"EncryptedKvBackend over a durable service (Redis, Postgres, Deno KV).\"\n : \"The in-memory example backend requires an explicit development or test \" +\n \"runtime. Set NODE_ENV accordingly, or implement EncryptedKvBackend over \" +\n \"a durable service (Redis, Postgres, Deno KV).\",\n );\n }\n\n const rows = new Map<string, MemoryRow>();\n const lockTails = new Map<string, Promise<void>>();\n\n function readRow(key: string): string | null {\n const row = rows.get(key);\n if (!row) return null;\n if (row.expiresAt !== null && Date.now() >= row.expiresAt) {\n rows.delete(key);\n return null;\n }\n return row.value;\n }\n\n function writeRow(key: string, value: string, expiresInMs?: number): void {\n rows.set(key, {\n value,\n expiresAt: expiresInMs === undefined ? null : Date.now() + expiresInMs,\n });\n }\n\n return {\n get(key) {\n return Promise.resolve(readRow(key));\n },\n set(key, value, options) {\n writeRow(key, value, options?.expiresInMs);\n return Promise.resolve();\n },\n delete(key) {\n rows.delete(key);\n return Promise.resolve();\n },\n compareAndSwap(key, expected, next, options) {\n // No await between comparison and write: within one process this block\n // is indivisible, which is exactly the guarantee the contract asks a\n // distributed backend to provide server-side.\n if (readRow(key) !== expected) return Promise.resolve(false);\n if (next === null) rows.delete(key);\n else writeRow(key, next, options?.expiresInMs);\n return Promise.resolve(true);\n },\n async withLock(key, operation) {\n const prior = lockTails.get(key) ?? Promise.resolve();\n let release!: () => void;\n const current = new Promise<void>((resolve) => {\n release = resolve;\n });\n const tail = prior.catch(() => undefined).then(() => current);\n lockTails.set(key, tail);\n\n await prior.catch(() => undefined);\n try {\n return await operation();\n } finally {\n release();\n if (lockTails.get(key) === tail) lockTails.delete(key);\n }\n },\n };\n}\n",
133
- "lib/token-store.ts": "/**\n * Shared OAuth token store for generated integrations.\n *\n * The same store owns authorization state and tokens. This is required for\n * callbacks and token refresh to work across production workers. Configure a\n * durable, extension-owned RefreshCapableTokenStore before the first OAuth\n * request in production. The built-in memory store is for development and test.\n *\n * To build that durable store on top of a plain key-value service with\n * AES-256-GCM encryption at rest, see `encrypted-token-store.ts` (reference\n * backends live in `token-store-examples.ts`).\n */\n\nimport {\n MemoryTokenStore,\n type OAuthTokens,\n type OAuthTokenSnapshot,\n type RefreshCapableTokenStore,\n type StoredOAuthState,\n} from \"veryfront/oauth\";\n\nexport type OAuthToken = OAuthTokens;\n\n/**\n * Application-facing store used by both Veryfront OAuth handlers and the\n * generated integration clients.\n */\nexport interface TokenStore extends RefreshCapableTokenStore {\n getToken(userId: string, serviceId: string): Promise<OAuthToken | null>;\n setToken(userId: string, serviceId: string, token: OAuthToken): Promise<void>;\n revokeToken(userId: string, serviceId: string): Promise<void>;\n isConnected(userId: string, serviceId: string): Promise<boolean>;\n}\n\nconst REQUIRED_STORE_METHODS = [\n \"getTokens\",\n \"getTokenSnapshot\",\n \"setTokens\",\n \"compareAndSetTokens\",\n \"withTokenRefreshLock\",\n \"clearTokens\",\n \"setState\",\n \"consumeState\",\n] as const;\nconst TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1_000;\n\nfunction runtimeMode(): string | undefined {\n try {\n if (typeof process !== \"undefined\" && process.env) return process.env.NODE_ENV;\n } catch {\n // Deno exposes the Node-compatible `process` global even when env access\n // is denied. Preserve the fail-closed mode decision in that runtime.\n return undefined;\n }\n try {\n return (globalThis as { Deno?: { env?: { get?: (name: string) => string | undefined } } })\n .Deno?.env?.get?.(\"NODE_ENV\");\n } catch {\n return undefined;\n }\n}\n\nfunction allowsProcessLocalStorage(): boolean {\n const mode = runtimeMode();\n return mode === \"development\" || mode === \"test\";\n}\n\nfunction assertRefreshCapableStore(\n store: RefreshCapableTokenStore,\n): asserts store is RefreshCapableTokenStore {\n if (!store || typeof store !== \"object\") {\n throw new TypeError(\"OAuth token store must be an object\");\n }\n\n for (const method of REQUIRED_STORE_METHODS) {\n if (typeof store[method] !== \"function\") {\n throw new TypeError(`OAuth token store must implement ${method}()`);\n }\n }\n}\n\n/**\n * Add the generated client aliases to a production-grade Veryfront OAuth\n * store. The adapter delegates every concurrency and state operation to the\n * supplied store; it never emulates distributed behavior in process memory.\n */\nexport function createTokenStore(store: RefreshCapableTokenStore): TokenStore {\n assertRefreshCapableStore(store);\n\n return {\n getTokens(serviceId: string, userId: string): Promise<OAuthTokens | null> {\n return store.getTokens(serviceId, userId);\n },\n\n getTokenSnapshot(\n serviceId: string,\n userId: string,\n ): Promise<OAuthTokenSnapshot | null> {\n return store.getTokenSnapshot(serviceId, userId);\n },\n\n setTokens(serviceId: string, userId: string, tokens: OAuthTokens): Promise<void> {\n return store.setTokens(serviceId, userId, tokens);\n },\n\n compareAndSetTokens(\n serviceId: string,\n userId: string,\n expectedRevision: string,\n tokens: OAuthTokens,\n ): Promise<boolean> {\n return store.compareAndSetTokens(serviceId, userId, expectedRevision, tokens);\n },\n\n withTokenRefreshLock<T>(\n serviceId: string,\n userId: string,\n operation: () => Promise<T>,\n ): Promise<T> {\n return store.withTokenRefreshLock(serviceId, userId, operation);\n },\n\n clearTokens(serviceId: string, userId: string): Promise<void> {\n return store.clearTokens(serviceId, userId);\n },\n\n setState(state: string, metadata: StoredOAuthState): Promise<void> {\n return store.setState(state, metadata);\n },\n\n consumeState(state: string): Promise<StoredOAuthState | null> {\n return store.consumeState(state);\n },\n\n getToken(userId: string, serviceId: string): Promise<OAuthToken | null> {\n return store.getTokens(serviceId, userId);\n },\n\n setToken(userId: string, serviceId: string, token: OAuthToken): Promise<void> {\n return store.setTokens(serviceId, userId, token);\n },\n\n revokeToken(userId: string, serviceId: string): Promise<void> {\n return store.clearTokens(serviceId, userId);\n },\n\n async isConnected(userId: string, serviceId: string): Promise<boolean> {\n const token = await store.getTokens(serviceId, userId);\n return !!token && (token.expiresAt === undefined || token.expiresAt > Date.now());\n },\n };\n}\n\nfunction unexpiredAccessToken(token: OAuthToken, now = Date.now()): string | null {\n return token.expiresAt === undefined || now < token.expiresAt ? token.accessToken : null;\n}\n\n/**\n * Resolve an access token, refreshing it under the store's distributed lock.\n * Revisioned compare-and-set prevents a refresh from overwriting a concurrent\n * reconnect or revocation that did not participate in the refresh lock.\n */\nexport async function getRefreshableAccessToken(\n store: TokenStore,\n serviceId: string,\n userId: string,\n refresh: (refreshToken: string) => Promise<OAuthToken>,\n): Promise<string | null> {\n const initial = await store.getTokenSnapshot(serviceId, userId);\n if (!initial) return null;\n\n const initialToken = initial.tokens;\n const now = Date.now();\n if (\n initialToken.expiresAt === undefined ||\n now < initialToken.expiresAt - TOKEN_REFRESH_BUFFER_MS\n ) {\n return initialToken.accessToken;\n }\n if (!initialToken.refreshToken) return unexpiredAccessToken(initialToken, now);\n\n return await store.withTokenRefreshLock(serviceId, userId, async () => {\n // Another worker may have refreshed this slot while this caller waited.\n const current = await store.getTokenSnapshot(serviceId, userId);\n if (!current) return null;\n\n const token = current.tokens;\n const lockedNow = Date.now();\n if (\n token.expiresAt === undefined ||\n lockedNow < token.expiresAt - TOKEN_REFRESH_BUFFER_MS\n ) {\n return token.accessToken;\n }\n if (!token.refreshToken) return unexpiredAccessToken(token, lockedNow);\n\n let refreshed: OAuthToken;\n try {\n refreshed = await refresh(token.refreshToken);\n } catch {\n // A provider failure must not unconditionally delete a row that may\n // have been replaced by a concurrent reconnect outside the lock.\n const latest = await store.getTokens(serviceId, userId);\n return latest ? unexpiredAccessToken(latest) : null;\n }\n\n if (refreshed.refreshToken === undefined) {\n refreshed = { ...refreshed, refreshToken: token.refreshToken };\n }\n\n const replaced = await store.compareAndSetTokens(\n serviceId,\n userId,\n current.revision,\n refreshed,\n );\n if (replaced) return refreshed.accessToken;\n\n const latest = await store.getTokens(serviceId, userId);\n return latest ? unexpiredAccessToken(latest) : null;\n });\n}\n\nlet configuredTokenStore: TokenStore | null = null;\nlet defaultTokenStore: TokenStore | null = null;\n\n/**\n * Configure the shared production store before the first OAuth request.\n *\n * Production stores must persist state and tokens across workers, implement\n * atomic compare-and-set, and use a bounded, crash-recoverable distributed\n * lease for withTokenRefreshLock(). Storage extensions are responsible for\n * encryption and backend-specific concurrency guarantees.\n */\nexport function configureTokenStore(store: RefreshCapableTokenStore): void {\n if (configuredTokenStore || defaultTokenStore) {\n throw new Error(\"OAuth token store must be configured exactly once before first use\");\n }\n if (!allowsProcessLocalStorage() && store instanceof MemoryTokenStore) {\n throw new Error(\n \"MemoryTokenStore is allowed only when NODE_ENV is explicitly development or test\",\n );\n }\n configuredTokenStore = createTokenStore(store);\n}\n\n/** Resolve the development default without doing work during module import. */\nexport function createDefaultTokenStore(): TokenStore {\n if (!allowsProcessLocalStorage()) {\n throw new Error(\n \"OAuth token storage is not configured. The in-memory default is allowed \" +\n \"only when NODE_ENV is explicitly development or test. \" +\n \"Configure an extension-owned RefreshCapableTokenStore with \" +\n \"configureTokenStore() before the first OAuth request.\",\n );\n }\n\n console.warn(\n \"[Token Store] Using development-only in-memory OAuth storage. \" +\n \"State and tokens will be lost on restart.\",\n );\n return createTokenStore(new MemoryTokenStore());\n}\n\nfunction getDefaultTokenStore(): TokenStore {\n defaultTokenStore ??= configuredTokenStore ?? createDefaultTokenStore();\n return defaultTokenStore;\n}\n\n/**\n * Lazy proxy shared by every generated OAuth route and integration client.\n * Importing a route never initializes storage or throws.\n */\nexport const tokenStore: TokenStore = {\n getTokens(serviceId, userId) {\n return getDefaultTokenStore().getTokens(serviceId, userId);\n },\n getTokenSnapshot(serviceId, userId) {\n return getDefaultTokenStore().getTokenSnapshot(serviceId, userId);\n },\n setTokens(serviceId, userId, tokens) {\n return getDefaultTokenStore().setTokens(serviceId, userId, tokens);\n },\n compareAndSetTokens(serviceId, userId, expectedRevision, tokens) {\n return getDefaultTokenStore().compareAndSetTokens(\n serviceId,\n userId,\n expectedRevision,\n tokens,\n );\n },\n withTokenRefreshLock(serviceId, userId, operation) {\n return getDefaultTokenStore().withTokenRefreshLock(serviceId, userId, operation);\n },\n clearTokens(serviceId, userId) {\n return getDefaultTokenStore().clearTokens(serviceId, userId);\n },\n setState(state, metadata) {\n return getDefaultTokenStore().setState(state, metadata);\n },\n consumeState(state) {\n return getDefaultTokenStore().consumeState(state);\n },\n getToken(userId, serviceId) {\n return getDefaultTokenStore().getToken(userId, serviceId);\n },\n setToken(userId, serviceId, token) {\n return getDefaultTokenStore().setToken(userId, serviceId, token);\n },\n revokeToken(userId, serviceId) {\n return getDefaultTokenStore().revokeToken(userId, serviceId);\n },\n isConnected(userId, serviceId) {\n return getDefaultTokenStore().isConnected(userId, serviceId);\n },\n};\n",
134
- "lib/user-id.ts": "import type { ToolExecutionContext } from \"veryfront/tool\";\n\nfunction normalizeUserId(value: unknown): string | null {\n if (typeof value !== \"string\" || value.length === 0 || value.length > 1_024) {\n return null;\n }\n return value.trim() === value ? value : null;\n}\n\n/**\n * Application-owned session/JWT lookup.\n *\n * Replace this implementation with a server-side session lookup or verified\n * JWT check. There is deliberately no environment-gated default, because an\n * ambient identity collapses every visitor onto a single OAuth token owner.\n */\nexport async function resolveAuthenticatedUserId(\n _request: Request,\n): Promise<string | null> {\n throw new Error(\n \"Authenticated request identity is not configured. Implement \" +\n \"resolveAuthenticatedUserId in lib/user-id.ts using a verified session or JWT.\",\n );\n}\n\n/** Resolve and validate the authenticated user for an OAuth request. */\nexport async function requireUserIdFromRequest(\n request: Request,\n): Promise<string | null> {\n return normalizeUserId(await resolveAuthenticatedUserId(request));\n}\n\nexport function requireUserIdFromContext(\n context?: ToolExecutionContext,\n): string {\n const userId = normalizeUserId(context?.userId);\n if (userId) return userId;\n throw new Error(\"Authenticated tool context userId is required\");\n}\n",
135
- "SETUP.md": "# Integration Setup Guide\n\nThis guide helps you set up credentials for all 50+ service integrations available in Veryfront.\n\n## Quick Start\n\n```bash\n# Create a new project with integrations\nveryfront init my-app --with ai --integrations slack,github,notion\n\n# Start development\ncd my-app\nveryfront dev\n```\n\nVisit `http://localhost:3000/api/auth/{service}` to connect each service.\n\n---\n\n## Table of Contents\n\n- [Google Services](#google-services) (Gmail, Calendar, Drive, Docs, Sheets)\n- [Microsoft Services](#microsoft-services) (Outlook, Teams, SharePoint, OneDrive)\n- [Atlassian Services](#atlassian-services) (Jira, Confluence)\n- [Communication](#communication) (Slack, Twilio, Zoom, Webex)\n- [Project Management](#project-management) (Asana, Monday, Trello, ClickUp, Linear, Notion)\n- [Developer Tools](#developer-tools) (GitHub, GitLab, Bitbucket, Figma, Sentry, PostHog)\n- [CRM & Sales](#crm--sales) (Salesforce, Pipedrive, Intercom, Zendesk, Freshdesk)\n- [Databases](#databases) (Supabase, Neon, Airtable, Snowflake)\n- [Cloud & Storage](#cloud--storage) (AWS, Box)\n- [Finance](#finance) (Stripe, QuickBooks, Xero)\n- [Marketing](#marketing) (Mailchimp, Twitter)\n- [E-commerce](#e-commerce) (Shopify)\n- [AI & Analytics](#ai--analytics) (Anthropic, Mixpanel)\n\n---\n\n## Google Services\n\n**Gmail, Calendar, Drive, Docs, Sheets** all use the same Google OAuth credentials.\n\n### Setup Steps\n\n1. Go to [Google Cloud Console](https://console.cloud.google.com/apis/credentials)\n2. Create a new project or select existing\n3. Enable required APIs:\n - Gmail API\n - Google Calendar API\n - Google Drive API\n - Google Docs API\n - Google Sheets API\n4. Go to **OAuth consent screen**:\n - User Type: External (or Internal for Workspace)\n - Add scopes for each API you need\n5. Go to **Credentials** > **Create Credentials** > **OAuth client ID**:\n - Application type: Web application\n - Authorized redirect URIs:\n ```\n http://localhost:3000/api/auth/gmail/callback\n http://localhost:3000/api/auth/calendar/callback\n http://localhost:3000/api/auth/drive/callback\n http://localhost:3000/api/auth/docs-google/callback\n http://localhost:3000/api/auth/sheets/callback\n ```\n\n### Environment Variables\n\n```env\nGOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com\nGOOGLE_CLIENT_SECRET=your-client-secret\n```\n\n### Required Scopes by Service\n\n| Service | Scopes |\n| -------- | -------------------------------------------------------------------------------------------------------------------------------- |\n| Gmail | `gmail.readonly`, `gmail.send`, `gmail.modify`, `gmail.labels`, `gmail.compose`, `https://mail.google.com/` for permanent delete |\n| Calendar | `calendar.readonly`, `calendar.events` |\n| Drive | `drive.readonly`, `drive.file` |\n| Docs | `documents.readonly`, `documents` |\n| Sheets | `spreadsheets.readonly`, `spreadsheets` |\n\n---\n\n## Microsoft Services\n\n**Outlook, Teams, SharePoint, OneDrive** use Microsoft OAuth (Azure AD).\n\n### Setup Steps\n\n1. Go to [Azure Portal](https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade)\n2. Click **New registration**:\n - Name: Your app name\n - Supported account types: Accounts in any organizational directory\n - Redirect URI: Web, `http://localhost:3000/api/auth/outlook/callback`\n3. After creation, go to **Certificates & secrets**:\n - Create a new client secret\n4. Go to **API permissions**:\n - Add Microsoft Graph permissions\n\n### Environment Variables\n\n```env\nMICROSOFT_CLIENT_ID=your-application-client-id\nMICROSOFT_CLIENT_SECRET=your-client-secret\nMICROSOFT_TENANT_ID=common\n```\n\n### Required Scopes by Service\n\n| Service | Scopes |\n| ---------- | ------------------------------------------------------------- |\n| Outlook | `Mail.Read`, `Mail.Send`, `Calendars.ReadWrite` |\n| Teams | `Team.ReadBasic.All`, `Chat.ReadWrite`, `ChannelMessage.Send` |\n| SharePoint | `Sites.Read.All`, `Files.ReadWrite.All` |\n| OneDrive | `Files.Read`, `Files.ReadWrite` |\n\n---\n\n## Atlassian Services\n\n**Jira and Confluence** use Atlassian OAuth 2.0 (3LO).\n\n### Setup Steps\n\n1. Go to [Atlassian Developer Console](https://developer.atlassian.com/console/myapps/)\n2. Click **Create** > **OAuth 2.0 integration**\n3. Configure:\n - Name: Your app name\n - Callback URL: `http://localhost:3000/api/auth/jira/callback`\n4. Add required scopes in **Permissions**\n5. Get your Cloud ID: Visit `https://your-domain.atlassian.net/_edge/tenant_info`\n\n### Environment Variables\n\n```env\nATLASSIAN_CLIENT_ID=your-client-id\nATLASSIAN_CLIENT_SECRET=your-client-secret\nATLASSIAN_CLOUD_ID=your-cloud-id\n```\n\n### Required Scopes\n\n| Service | Scopes |\n| ---------- | --------------------------------------------------------- |\n| Jira | `read:jira-work`, `write:jira-work`, `read:jira-user` |\n| Confluence | `read:confluence-content.all`, `write:confluence-content` |\n\n---\n\n## Communication\n\n### Slack\n\n1. Go to [Slack API Apps](https://api.slack.com/apps)\n2. Click **Create New App** > **From scratch**\n3. Go to **OAuth & Permissions**:\n - Add redirect URL: `http://localhost:3000/api/auth/slack/callback`\n - Add scopes: `channels:history`, `channels:read`, `chat:write`, `groups:history`, `groups:read`, `im:history`, `im:read`, `mpim:history`, `mpim:read`, `users:read`\n4. **Install to Workspace**\n\n```env\nSLACK_CLIENT_ID=your-client-id\nSLACK_CLIENT_SECRET=your-client-secret\n```\n\n### Twilio (SMS/WhatsApp)\n\n1. Go to [Twilio Console](https://console.twilio.com/)\n2. Get Account SID and Auth Token from dashboard\n3. Get or buy a phone number for sending\n\n```env\nTWILIO_ACCOUNT_SID=your-account-sid\nTWILIO_AUTH_TOKEN=your-auth-token\nTWILIO_PHONE_NUMBER=+1234567890\n```\n\n### Zoom\n\n1. Go to [Zoom App Marketplace](https://marketplace.zoom.us/develop/create)\n2. Create **OAuth App**\n3. Configure redirect: `http://localhost:3000/api/auth/zoom/callback`\n4. Add scopes: `meeting:read`, `meeting:write`, `user:read`\n\n```env\nZOOM_CLIENT_ID=your-client-id\nZOOM_CLIENT_SECRET=your-client-secret\n```\n\n### Webex\n\n1. Go to [Webex for Developers](https://developer.webex.com/my-apps)\n2. Create new integration\n3. Redirect URI: `http://localhost:3000/api/auth/webex/callback`\n4. Scopes: `spark:messages_read`, `spark:messages_write`, `spark:rooms_read`\n\n```env\nWEBEX_CLIENT_ID=your-client-id\nWEBEX_CLIENT_SECRET=your-client-secret\n```\n\n---\n\n## Project Management\n\n### Asana\n\n1. Go to [Asana Developer Console](https://app.asana.com/0/developer-console)\n2. Create new app\n3. Set redirect URL: `http://localhost:3000/api/auth/asana/callback`\n\n```env\nASANA_CLIENT_ID=your-client-id\nASANA_CLIENT_SECRET=your-client-secret\n```\n\n### Monday.com\n\n1. Go to [Monday Apps](https://auth.monday.com/oauth2/authorize)\n2. Create new app in your account's Developer section\n3. Configure OAuth with redirect: `http://localhost:3000/api/auth/monday/callback`\n\n```env\nMONDAY_CLIENT_ID=your-client-id\nMONDAY_CLIENT_SECRET=your-client-secret\n```\n\n### Trello\n\n1. Go to [Trello Power-Ups Admin](https://trello.com/power-ups/admin)\n2. Create new Power-Up\n3. Configure OAuth redirect: `http://localhost:3000/api/auth/trello/callback`\n\n```env\nTRELLO_API_KEY=your-api-key\nTRELLO_API_SECRET=your-api-secret\n```\n\n### ClickUp\n\n1. Go to [ClickUp API Settings](https://app.clickup.com/settings/apps)\n2. Create new app\n3. Redirect URL: `http://localhost:3000/api/auth/clickup/callback`\n\n```env\nCLICKUP_CLIENT_ID=your-client-id\nCLICKUP_CLIENT_SECRET=your-client-secret\n```\n\n### Linear\n\n1. Go to [Linear Settings > API](https://linear.app/settings/api)\n2. Create OAuth application\n3. Callback URL: `http://localhost:3000/api/auth/linear/callback`\n\n```env\nLINEAR_CLIENT_ID=your-client-id\nLINEAR_CLIENT_SECRET=your-client-secret\n```\n\n### Notion\n\n1. Go to [Notion Integrations](https://www.notion.so/my-integrations)\n2. Create new **public** integration (for OAuth)\n3. Set redirect URI: `http://localhost:3000/api/auth/notion/callback`\n4. **Important**: Share pages with your integration\n\n```env\nNOTION_CLIENT_ID=your-oauth-client-id\nNOTION_CLIENT_SECRET=your-oauth-client-secret\n```\n\n---\n\n## Developer Tools\n\n### GitHub\n\n1. Go to [GitHub Developer Settings](https://github.com/settings/developers)\n2. Create **New OAuth App**\n3. Authorization callback: `http://localhost:3000/api/auth/github/callback`\n\n```env\nGITHUB_CLIENT_ID=your-client-id\nGITHUB_CLIENT_SECRET=your-client-secret\n```\n\n### GitLab\n\n1. Go to [GitLab Applications](https://gitlab.com/-/profile/applications)\n2. Create new application\n3. Redirect URI: `http://localhost:3000/api/auth/gitlab/callback`\n4. Scopes: `read_user`, `read_api`, `read_repository`\n\n```env\nGITLAB_CLIENT_ID=your-application-id\nGITLAB_CLIENT_SECRET=your-secret\n```\n\n### Bitbucket\n\n1. Go to [Bitbucket App Passwords](https://bitbucket.org/account/settings/app-passwords/) or create OAuth consumer\n2. For OAuth: Workspace settings > OAuth consumers\n3. Callback URL: `http://localhost:3000/api/auth/bitbucket/callback`\n\n```env\nBITBUCKET_CLIENT_ID=your-client-id\nBITBUCKET_CLIENT_SECRET=your-client-secret\n```\n\n### Figma\n\n1. Go to [Figma Developers](https://www.figma.com/developers/apps)\n2. Create new app\n3. Callback URL: `http://localhost:3000/api/auth/figma/callback`\n\n```env\nFIGMA_CLIENT_ID=your-client-id\nFIGMA_CLIENT_SECRET=your-client-secret\n```\n\n### Sentry\n\n1. Go to [Sentry Developer Settings](https://sentry.io/settings/developer-settings/)\n2. Create new public integration\n3. Redirect URL: `http://localhost:3000/api/auth/sentry/callback`\n\n```env\nSENTRY_CLIENT_ID=your-client-id\nSENTRY_CLIENT_SECRET=your-client-secret\n```\n\n### PostHog\n\nUses API key authentication (no OAuth).\n\n1. Go to your PostHog project settings\n2. Create a personal API key\n\n```env\nPOSTHOG_API_KEY=phx_your-api-key\nPOSTHOG_HOST=https://app.posthog.com\n```\n\n---\n\n## CRM & Sales\n\n### Salesforce\n\n1. Go to [Salesforce Setup](https://login.salesforce.com/) > App Manager\n2. Create **New Connected App**\n3. Enable OAuth, add callback: `http://localhost:3000/api/auth/salesforce/callback`\n4. Required scopes: `api`, `refresh_token`\n\n```env\nSALESFORCE_CLIENT_ID=your-consumer-key\nSALESFORCE_CLIENT_SECRET=your-consumer-secret\n```\n\n### Pipedrive\n\n1. Go to [Pipedrive Marketplace Manager](https://developers.pipedrive.com/)\n2. Create new app\n3. OAuth redirect: `http://localhost:3000/api/auth/pipedrive/callback`\n\n```env\nPIPEDRIVE_CLIENT_ID=your-client-id\nPIPEDRIVE_CLIENT_SECRET=your-client-secret\n```\n\n### Intercom\n\n1. Go to [Intercom Developer Hub](https://developers.intercom.com/)\n2. Create new app\n3. Configure OAuth: `http://localhost:3000/api/auth/intercom/callback`\n\n```env\nINTERCOM_CLIENT_ID=your-client-id\nINTERCOM_CLIENT_SECRET=your-client-secret\n```\n\n### Zendesk\n\n1. Go to Admin Center > Apps and integrations > APIs > Zendesk API\n2. Create OAuth client\n3. Redirect URL: `http://localhost:3000/api/auth/zendesk/callback`\n\n```env\nZENDESK_CLIENT_ID=your-client-id\nZENDESK_CLIENT_SECRET=your-client-secret\nZENDESK_SUBDOMAIN=your-subdomain\n```\n\n### Freshdesk\n\nUses API key authentication.\n\n1. Go to Profile Settings in Freshdesk\n2. Find your API Key\n\n```env\nFRESHDESK_API_KEY=your-api-key\nFRESHDESK_DOMAIN=your-domain.freshdesk.com\n```\n\n---\n\n## Databases\n\n### Supabase\n\nUses API key (no OAuth needed).\n\n1. Go to your Supabase project dashboard\n2. Go to Settings > API\n3. Copy the `anon` or `service_role` key\n\n```env\nSUPABASE_URL=https://your-project.supabase.co\nSUPABASE_ANON_KEY=your-anon-key\nSUPABASE_SERVICE_ROLE_KEY=your-service-role-key\n```\n\n### Neon\n\nUses API key authentication.\n\n1. Go to [Neon Console](https://console.neon.tech/)\n2. Create API key in Account Settings\n\n```env\nNEON_API_KEY=your-api-key\nNEON_PROJECT_ID=your-project-id\n```\n\n### Airtable\n\n1. Go to [Airtable Account](https://airtable.com/account)\n2. Create personal access token or OAuth app\n3. For OAuth: [Airtable OAuth](https://airtable.com/create/oauth)\n\n```env\nAIRTABLE_API_KEY=your-api-key\n# Or for OAuth:\nAIRTABLE_CLIENT_ID=your-client-id\nAIRTABLE_CLIENT_SECRET=your-client-secret\n```\n\n### Snowflake\n\nUses account credentials (key-pair or password).\n\n1. Get your Snowflake account identifier\n2. Create a user with appropriate permissions\n3. (Optional) Set up key-pair authentication\n\n```env\nSNOWFLAKE_ACCOUNT=your-account-identifier\nSNOWFLAKE_USERNAME=your-username\nSNOWFLAKE_PASSWORD=your-password\nSNOWFLAKE_WAREHOUSE=your-warehouse\nSNOWFLAKE_DATABASE=your-database\n```\n\n---\n\n## Cloud & Storage\n\n### AWS\n\nUses IAM credentials.\n\n1. Go to [AWS IAM Console](https://console.aws.amazon.com/iam/)\n2. Create a new IAM user with programmatic access\n3. Attach policies for services you need (S3, EC2, Lambda, etc.)\n\n```env\nAWS_ACCESS_KEY_ID=your-access-key\nAWS_SECRET_ACCESS_KEY=your-secret-key\nAWS_REGION=us-east-1\n```\n\n### Box\n\n1. Go to [Box Developer Console](https://app.box.com/developers/console)\n2. Create new app with OAuth 2.0\n3. Redirect URI: `http://localhost:3000/api/auth/box/callback`\n\n```env\nBOX_CLIENT_ID=your-client-id\nBOX_CLIENT_SECRET=your-client-secret\n```\n\n---\n\n## Finance\n\n### Stripe\n\nUses API key (no OAuth for basic usage).\n\n1. Go to [Stripe Dashboard](https://dashboard.stripe.com/apikeys)\n2. Get your secret key (use test key for development)\n\n```env\nSTRIPE_SECRET_KEY=sk_test_your-secret-key\nSTRIPE_PUBLISHABLE_KEY=pk_test_your-publishable-key\n```\n\n### QuickBooks\n\n1. Go to [Intuit Developer](https://developer.intuit.com/)\n2. Create app and get OAuth credentials\n3. Redirect URI: `http://localhost:3000/api/auth/quickbooks/callback`\n\n```env\nQUICKBOOKS_CLIENT_ID=your-client-id\nQUICKBOOKS_CLIENT_SECRET=your-client-secret\n```\n\n### Xero\n\n1. Go to [Xero Developer](https://developer.xero.com/app/manage)\n2. Create app\n3. Redirect URI: `http://localhost:3000/api/auth/xero/callback`\n\n```env\nXERO_CLIENT_ID=your-client-id\nXERO_CLIENT_SECRET=your-client-secret\n```\n\n---\n\n## Marketing\n\n### Mailchimp\n\n1. Go to [Mailchimp Account API Keys](https://us1.admin.mailchimp.com/account/api/)\n2. For OAuth: Register app at [Mailchimp OAuth](https://admin.mailchimp.com/account/oauth2/)\n3. Redirect: `http://localhost:3000/api/auth/mailchimp/callback`\n\n```env\nMAILCHIMP_CLIENT_ID=your-client-id\nMAILCHIMP_CLIENT_SECRET=your-client-secret\n# Or API key:\nMAILCHIMP_API_KEY=your-api-key-us1\n```\n\n### Twitter/X\n\n1. Go to [Twitter Developer Portal](https://developer.twitter.com/en/portal/dashboard)\n2. Create project and app\n3. Enable OAuth 2.0\n4. Callback URL: `http://localhost:3000/api/auth/twitter/callback`\n\n```env\nTWITTER_CLIENT_ID=your-client-id\nTWITTER_CLIENT_SECRET=your-client-secret\n```\n\n---\n\n## E-commerce\n\n### Shopify\n\n1. Go to [Shopify Partners](https://partners.shopify.com/)\n2. Create new app\n3. App URL and redirect: `http://localhost:3000/api/auth/shopify/callback`\n\n```env\nSHOPIFY_CLIENT_ID=your-api-key\nSHOPIFY_CLIENT_SECRET=your-api-secret\nSHOPIFY_SHOP_NAME=your-store.myshopify.com\n```\n\n---\n\n## AI & Analytics\n\n### Anthropic (Admin API)\n\nFor organization management and usage tracking.\n\n1. Go to [Anthropic Console](https://console.anthropic.com/)\n2. Create Admin API key (requires admin access)\n\n```env\nANTHROPIC_ADMIN_API_KEY=your-admin-api-key\n```\n\n### Mixpanel\n\nUses API key/secret for data export.\n\n1. Go to [Mixpanel Project Settings](https://mixpanel.com/settings/project)\n2. Get Project Token for tracking\n3. Get API Secret for data export\n\n```env\nMIXPANEL_PROJECT_TOKEN=your-project-token\nMIXPANEL_API_SECRET=your-api-secret\n```\n\n---\n\n## Testing Your Setup\n\nAfter configuring credentials:\n\n```bash\n# Start the dev server\nveryfront dev\n\n# Test each integration by visiting:\n# http://localhost:3000/api/auth/{service}\n\n# Check connection status\ncurl http://localhost:3000/api/connections\n```\n\n## Troubleshooting\n\n### Common Issues\n\n| Error | Solution |\n| ---------------------- | -------------------------------------------------------------- |\n| \"Invalid redirect URI\" | Ensure callback URL matches exactly (including trailing slash) |\n| \"Invalid client\" | Check CLIENT_ID is correct and app is published |\n| \"Access denied\" | Verify all required scopes are added |\n| \"Token expired\" | Implement refresh token flow or re-authenticate |\n\n### Debug Mode\n\nEnable debug logging:\n\n```bash\nDEBUG=veryfront:oauth veryfront dev\n```\n\n### Token storage\n\nThe generated token-store proxy uses in-memory storage only in an explicit\ndevelopment or test environment. For production:\n\n1. Select a storage extension that provides a durable\n `RefreshCapableTokenStore`.\n2. Configure it with `configureTokenStore()` before the first OAuth request.\n3. Ensure the extension encrypts tokens, atomically consumes OAuth state,\n implements compare-and-set, and uses a bounded, crash-recoverable\n distributed lease for refresh locking.\n\n```typescript\n// lib/configure-oauth-storage.ts (import once during application startup)\nimport type { RefreshCapableTokenStore } from \"veryfront/oauth\";\nimport { configureTokenStore } from \"./token-store.ts\";\nimport { createApplicationOAuthTokenStore } from \"./storage/oauth.ts\";\n\nconst oauthStore: RefreshCapableTokenStore = createApplicationOAuthTokenStore();\nconfigureTokenStore(oauthStore);\n```\n\n`createApplicationOAuthTokenStore` is the factory exported by your selected\nstorage extension. Veryfront core does not select or import the backend.\n\n### Authenticated OAuth routes\n\nGenerated OAuth routes call `requireUserIdFromRequest` in `lib/user-id.ts`.\nThe generated `resolveAuthenticatedUserId` seam deliberately throws in every\nruntime mode until you replace it with a resolver backed by your server-side\nsession or verified JWT. The surrounding request boundary validates the\nreturned id. There is no development escape hatch because an ambient default\nidentity makes all visitors share one token owner.\n\n```ts\n// lib/user-id.ts\nexport async function resolveAuthenticatedUserId(request: Request) {\n const session = await verifySession(request); // your session/JWT check\n return session?.userId ?? null; // null => anonymous => routes answer 401\n}\n```\n\nRaw request headers are not an authentication boundary. Never return a value\ncopied from an untrusted header such as `x-user-id`.\n\n### Authenticated tool context\n\nGenerated integration tools resolve the token owner from `context.userId`.\nPass the authenticated user through `ToolExecutionContext` whenever an agent or\nworkflow invokes a tool. Execution fails closed when the context has no\nauthenticated user id.\n\n## Production Checklist\n\n- [ ] Update all redirect URIs to production domain\n- [ ] Configure an extension-owned `RefreshCapableTokenStore`\n- [ ] Implement `resolveAuthenticatedUserId` using a verified session or JWT\n- [ ] Verify token encryption and distributed refresh locking\n- [ ] Pass authenticated user ids through every tool execution context\n- [ ] Configure rate limiting\n- [ ] Add error monitoring (Sentry)\n- [ ] Test OAuth flows end-to-end\n- [ ] Review and minimize required scopes\n\n## Need Help?\n\n- Run `veryfront doctor` to diagnose issues\n- Check the [Veryfront Documentation](https://veryfront.com/docs)\n- Join our [Discord community](https://discord.gg/xWuRjafrtV)\n"
136
- }
137
- },
138
- "integration:airtable": {
139
- "files": {
140
- "app/api/auth/airtable/callback/route.ts": "import { createOAuthCallbackHandler, airtableConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\n\nexport const GET = createOAuthCallbackHandler(airtableConfig, { tokenStore });\n",
141
- "app/api/auth/airtable/route.ts": "import { airtableConfig, createOAuthInitHandler } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../lib/token-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../lib/user-id.ts\";\n\nexport const GET = createOAuthInitHandler(airtableConfig, {\n tokenStore,\n getUserId: requireUserIdFromRequest,\n});\n",
142
- "lib/airtable-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst AIRTABLE_BASE_URL = \"https://api.airtable.com/v0\";\nconst AIRTABLE_META_BASE_URL = \"https://api.airtable.com/v0/meta\";\n\ninterface AirtableResponse<T> {\n records?: T[];\n offset?: string;\n}\n\ninterface AirtableBase {\n id: string;\n name: string;\n permissionLevel: string;\n}\n\ninterface AirtableBaseSchema {\n tables: Array<{\n id: string;\n name: string;\n primaryFieldId: string;\n fields: Array<{\n id: string;\n name: string;\n type: string;\n options?: Record<string, unknown>;\n }>;\n views: Array<{\n id: string;\n name: string;\n type: string;\n }>;\n }>;\n}\n\nexport interface AirtableRecord {\n id: string;\n createdTime: string;\n fields: Record<string, unknown>;\n}\n\nexport interface AirtableFieldDefinition {\n name: string;\n type: string;\n description?: string;\n options?: Record<string, unknown>;\n}\n\nexport interface AirtableTableDefinition {\n id: string;\n name: string;\n primaryFieldId: string;\n fields: AirtableFieldDefinition[];\n views: Array<{\n id: string;\n name: string;\n type: string;\n }>;\n}\n\nfunction getTokenOrThrow(): string {\n const token = getAccessToken();\n if (token) return token;\n throw new Error(\"Not authenticated with Airtable. Please connect your account.\");\n}\n\nasync function apiFetch<T>(\n baseUrl: string,\n endpoint: string,\n options: RequestInit,\n errorPrefix: string,\n): Promise<T> {\n const token = getTokenOrThrow();\n\n const response = await fetch(`${baseUrl}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}));\n throw new Error(\n `${errorPrefix}: ${response.status} ${error?.error?.message ?? response.statusText}`,\n );\n }\n\n return response.json() as Promise<T>;\n}\n\nfunction airtableFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n return apiFetch<T>(AIRTABLE_BASE_URL, endpoint, options, \"Airtable API error\");\n}\n\nfunction metaFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n return apiFetch<T>(AIRTABLE_META_BASE_URL, endpoint, options, \"Airtable Meta API error\");\n}\n\nexport async function listBases(): Promise<AirtableBase[]> {\n const response = await metaFetch<{ bases: AirtableBase[] }>(\"/bases\");\n return response.bases ?? [];\n}\n\nexport function getBase(baseId: string): Promise<AirtableBaseSchema> {\n return metaFetch<AirtableBaseSchema>(`/bases/${baseId}/tables`);\n}\n\nexport async function listRecords(\n baseId: string,\n tableIdOrName: string,\n options?: {\n fields?: string[];\n filterByFormula?: string;\n maxRecords?: number;\n pageSize?: number;\n sort?: Array<{ field: string; direction: \"asc\" | \"desc\" }>;\n view?: string;\n offset?: string;\n },\n): Promise<{ records: AirtableRecord[]; offset?: string }> {\n const params = new URLSearchParams();\n\n options?.fields?.forEach((field) => params.append(\"fields[]\", field));\n if (options?.filterByFormula) params.append(\"filterByFormula\", options.filterByFormula);\n if (options?.maxRecords) params.append(\"maxRecords\", String(options.maxRecords));\n if (options?.pageSize) params.append(\"pageSize\", String(options.pageSize));\n options?.sort?.forEach((s, i) => {\n params.append(`sort[${i}][field]`, s.field);\n params.append(`sort[${i}][direction]`, s.direction);\n });\n if (options?.view) params.append(\"view\", options.view);\n if (options?.offset) params.append(\"offset\", options.offset);\n\n const queryString = params.toString();\n const endpoint = `/${baseId}/${encodeURIComponent(tableIdOrName)}${\n queryString ? `?${queryString}` : \"\"\n }`;\n\n const response = await airtableFetch<AirtableResponse<AirtableRecord>>(endpoint);\n\n return { records: response.records ?? [], offset: response.offset };\n}\n\nexport function getRecord(\n baseId: string,\n tableIdOrName: string,\n recordId: string,\n): Promise<AirtableRecord> {\n return airtableFetch<AirtableRecord>(\n `/${baseId}/${encodeURIComponent(tableIdOrName)}/${recordId}`,\n );\n}\n\nexport function createRecord(\n baseId: string,\n tableIdOrName: string,\n fields: Record<string, unknown>,\n options?: { typecast?: boolean },\n): Promise<AirtableRecord> {\n return airtableFetch<AirtableRecord>(`/${baseId}/${encodeURIComponent(tableIdOrName)}`, {\n method: \"POST\",\n body: JSON.stringify({ fields, typecast: options?.typecast }),\n });\n}\n\nexport async function createRecords(\n baseId: string,\n tableIdOrName: string,\n records: Array<{ fields: Record<string, unknown> }>,\n options?: { typecast?: boolean },\n): Promise<AirtableRecord[]> {\n const response = await airtableFetch<{ records: AirtableRecord[] }>(\n `/${baseId}/${encodeURIComponent(tableIdOrName)}`,\n {\n method: \"POST\",\n body: JSON.stringify({ records, typecast: options?.typecast }),\n },\n );\n\n return response.records;\n}\n\nexport function updateRecord(\n baseId: string,\n tableIdOrName: string,\n recordId: string,\n fields: Record<string, unknown>,\n options?: { destructive?: boolean; typecast?: boolean },\n): Promise<AirtableRecord> {\n return airtableFetch<AirtableRecord>(\n `/${baseId}/${encodeURIComponent(tableIdOrName)}/${recordId}`,\n {\n method: options?.destructive ? \"PUT\" : \"PATCH\",\n body: JSON.stringify({ fields, typecast: options?.typecast }),\n },\n );\n}\n\nexport function deleteRecord(\n baseId: string,\n tableIdOrName: string,\n recordId: string,\n): Promise<{ id: string; deleted: boolean }> {\n return airtableFetch<{ id: string; deleted: boolean }>(\n `/${baseId}/${encodeURIComponent(tableIdOrName)}/${recordId}`,\n { method: \"DELETE\" },\n );\n}\n\nexport function createTable(\n baseId: string,\n name: string,\n fields: AirtableFieldDefinition[],\n options?: { description?: string },\n): Promise<AirtableTableDefinition> {\n return metaFetch<AirtableTableDefinition>(`/bases/${baseId}/tables`, {\n method: \"POST\",\n body: JSON.stringify({ name, description: options?.description, fields }),\n });\n}\n\nexport function updateTable(\n baseId: string,\n tableId: string,\n updates: { name?: string; description?: string },\n): Promise<AirtableTableDefinition> {\n return metaFetch<AirtableTableDefinition>(`/bases/${baseId}/tables/${tableId}`, {\n method: \"PATCH\",\n body: JSON.stringify(updates),\n });\n}\n\nexport function createField(\n baseId: string,\n tableId: string,\n field: AirtableFieldDefinition,\n): Promise<AirtableFieldDefinition & { id: string }> {\n return metaFetch<AirtableFieldDefinition & { id: string }>(\n `/bases/${baseId}/tables/${tableId}/fields`,\n {\n method: \"POST\",\n body: JSON.stringify(field),\n },\n );\n}\n\nexport function formatFieldValue(value: unknown): string {\n if (value == null) return \"\";\n if (Array.isArray(value)) return value.map((v) => formatFieldValue(v)).join(\", \");\n if (typeof value === \"object\") return JSON.stringify(value);\n return String(value);\n}\n",
143
- "tools/create-field.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createField } from \"../lib/airtable-client.ts\";\n\nexport default tool({\n id: \"airtable-create-field\",\n description:\n \"Create a new field in an Airtable table. Requires schema write permissions.\",\n inputSchema: defineSchema((v) =>\n v.object({\n baseId: v.string().describe(\n 'The ID of the Airtable base (starts with \"app\")',\n ),\n tableId: v.string().describe(\n 'The ID of the Airtable table (starts with \"tbl\")',\n ),\n name: v.string().describe(\"Field name\"),\n type: v.string().describe(\n 'Airtable field type, such as \"singleLineText\"',\n ),\n description: v.string().optional().describe(\"Optional field description\"),\n options: v.record(v.string(), v.unknown()).optional().describe(\n \"Field type-specific options\",\n ),\n })\n )(),\n execute: async ({ baseId, tableId, name, type, description, options }) =>\n createField(baseId, tableId, { name, type, description, options }),\n});\n",
144
- "tools/create-record.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createRecord } from \"../lib/airtable-client.ts\";\n\nexport default tool({\n id: \"airtable-create-record\",\n description:\n \"Create a new record in an Airtable table. Provide field names and values as an object. Returns the created record with its ID.\",\n inputSchema: defineSchema((v) => v.object({\n baseId: v.string().describe('The ID of the Airtable base (starts with \"app\")'),\n tableIdOrName: v.string().describe(\"The ID or name of the table\"),\n fields: v\n .record(v.string(), v.unknown())\n .describe(\n 'Object with field names as keys and their values. Field names must match exactly. Example: { \"Name\": \"John Doe\", \"Email\": \"john@example.com\", \"Status\": \"Active\" }',\n ),\n typecast: v.boolean().optional().describe(\"Allow Airtable to typecast field values\"),\n }))(),\n async execute({ baseId, tableIdOrName, fields, typecast }) {\n const record = await createRecord(baseId, tableIdOrName, fields, { typecast });\n\n return { id: record.id, createdTime: record.createdTime, fields: record.fields };\n },\n});\n",
145
- "tools/create-records.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createRecords } from \"../lib/airtable-client.ts\";\n\nexport default tool({\n id: \"airtable-create-records\",\n description:\n \"Create multiple records in an Airtable table. Provide an array of record objects with field values. Returns created records with IDs.\",\n inputSchema: defineSchema((v) =>\n v.object({\n baseId: v.string().describe(\n 'The ID of the Airtable base (starts with \"app\")',\n ),\n tableIdOrName: v.string().describe(\"The ID or name of the table\"),\n records: v\n .array(v.object({ fields: v.record(v.string(), v.unknown()) }))\n .min(1)\n .max(10)\n .describe(\n 'Array of 1-10 records to create. Example: [{ fields: { \"Name\": \"Jane\" } }]',\n ),\n typecast: v.boolean().optional().describe(\n \"Allow Airtable to typecast field values\",\n ),\n })\n )(),\n async execute({ baseId, tableIdOrName, records, typecast }) {\n const createdRecords = await createRecords(baseId, tableIdOrName, records, {\n typecast,\n });\n\n return createdRecords.map((record) => ({\n id: record.id,\n createdTime: record.createdTime,\n fields: record.fields,\n }));\n },\n});\n",
146
- "tools/create-table.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createTable } from \"../lib/airtable-client.ts\";\n\nexport default tool({\n id: \"airtable-create-table\",\n description:\n \"Create a new table in an Airtable base. Requires schema write permissions and at least one field definition.\",\n inputSchema: defineSchema((v) =>\n v.object({\n baseId: v.string().describe(\n 'The ID of the Airtable base (starts with \"app\")',\n ),\n name: v.string().describe(\"Name for the new table\"),\n description: v.string().optional().describe(\"Optional table description\"),\n fields: v\n .array(v.object({\n name: v.string().describe(\"Field name\"),\n type: v.string().describe(\n 'Airtable field type, such as \"singleLineText\"',\n ),\n description: v.string().optional().describe(\n \"Optional field description\",\n ),\n options: v.record(v.string(), v.unknown()).optional().describe(\n \"Field type-specific options\",\n ),\n }))\n .min(1)\n .describe(\n 'At least one initial field definition. Example: [{ \"name\": \"Name\", \"type\": \"singleLineText\" }]',\n ),\n })\n )(),\n execute: async ({ baseId, name, description, fields }) =>\n createTable(baseId, name, fields, { description }),\n});\n",
147
- "tools/delete-record.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { deleteRecord } from \"../lib/airtable-client.ts\";\n\nexport default tool({\n id: \"airtable-delete-record\",\n description:\n \"Delete an Airtable record from a table. Returns Airtable's deletion confirmation.\",\n inputSchema: defineSchema((v) =>\n v.object({\n baseId: v.string().describe(\n 'The ID of the Airtable base (starts with \"app\")',\n ),\n tableIdOrName: v.string().describe(\"The ID or name of the table\"),\n recordId: v.string().describe(\n 'The ID of the record to delete (starts with \"rec\")',\n ),\n })\n )(),\n execute: async ({ baseId, tableIdOrName, recordId }) =>\n deleteRecord(baseId, tableIdOrName, recordId),\n});\n",
148
- "tools/get-base.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { getBase } from \"../lib/airtable-client.ts\";\n\nexport default tool({\n id: \"airtable-get-base\",\n description:\n \"Get the schema and structure of an Airtable base, including all tables, fields, and views. Useful for understanding the data model before querying or creating records.\",\n inputSchema: defineSchema((v) => v.object({\n baseId: v.string().describe('The ID of the Airtable base (starts with \"app\")'),\n }))(),\n async execute({ baseId }) {\n const { tables } = await getBase(baseId);\n\n return {\n tables: tables.map((table) => ({\n id: table.id,\n name: table.name,\n primaryFieldId: table.primaryFieldId,\n fields: table.fields.map((field) => ({\n id: field.id,\n name: field.name,\n type: field.type,\n options: field.options,\n })),\n views: table.views.map((view) => ({\n id: view.id,\n name: view.name,\n type: view.type,\n })),\n })),\n };\n },\n});\n",
149
- "tools/get-record.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { getRecord } from \"../lib/airtable-client.ts\";\n\nexport default tool({\n id: \"airtable-get-record\",\n description:\n \"Get a specific record from an Airtable table by its ID. Returns the full record with all field values.\",\n inputSchema: defineSchema((v) => v.object({\n baseId: v.string().describe('The ID of the Airtable base (starts with \"app\")'),\n tableIdOrName: v.string().describe(\"The ID or name of the table\"),\n recordId: v.string().describe('The ID of the record to retrieve (starts with \"rec\")'),\n }))(),\n execute: async ({ baseId, tableIdOrName, recordId }) =>\n getRecord(baseId, tableIdOrName, recordId),\n});\n",
150
- "tools/list-bases.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { listBases } from \"../lib/airtable-client.ts\";\n\nexport default tool({\n id: \"airtable-list-bases\",\n description:\n \"List all accessible Airtable bases in the connected account. Returns base IDs, names, and permission levels.\",\n inputSchema: defineSchema((v) => v.object({}))(),\n async execute() {\n return listBases();\n },\n});\n",
151
- "tools/list-records.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { listRecords } from \"../lib/airtable-client.ts\";\n\nexport default tool({\n id: \"airtable-list-records\",\n description:\n \"List records from an Airtable table. Supports filtering with formulas, sorting, and limiting results. Returns record IDs, creation times, and all field values.\",\n inputSchema: defineSchema((v) => v.object({\n baseId: v.string().describe('The ID of the Airtable base (starts with \"app\")'),\n tableIdOrName: v.string().describe(\"The ID or name of the table\"),\n fields: v\n .array(v.string())\n .optional()\n .describe(\"Specific field names to return (returns all fields if not specified)\"),\n filterByFormula: v\n .string()\n .optional()\n .describe('Airtable formula to filter records (e.g., \"{Status} = \\'Done\\'\")'),\n maxRecords: v.number().min(1).max(100).optional().describe(\"Maximum number of records to return\"),\n sort: v\n .array(\n v.object({\n field: v.string().describe(\"Field name to sort by\"),\n direction: v.enum([\"asc\", \"desc\"]).describe(\"Sort direction\"),\n }),\n )\n .optional()\n .describe(\"Array of sort specifications\"),\n view: v.string().optional().describe(\"Name of a view to use for filtering and sorting\"),\n }))(),\n async execute({ baseId, tableIdOrName, fields, filterByFormula, maxRecords, sort, view }) {\n const { records, offset } = await listRecords(baseId, tableIdOrName, {\n fields,\n filterByFormula,\n maxRecords,\n pageSize: maxRecords,\n sort,\n view,\n });\n\n return {\n records: records.map((record) => ({\n id: record.id,\n createdTime: record.createdTime,\n fields: record.fields,\n })),\n count: records.length,\n hasMore: Boolean(offset),\n };\n },\n});\n",
152
- "tools/update-record.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { updateRecord } from \"../lib/airtable-client.ts\";\n\nexport default tool({\n id: \"airtable-update-record\",\n description:\n \"Update fields on an existing Airtable record. Returns the updated record with all visible fields.\",\n inputSchema: defineSchema((v) =>\n v.object({\n baseId: v.string().describe(\n 'The ID of the Airtable base (starts with \"app\")',\n ),\n tableIdOrName: v.string().describe(\"The ID or name of the table\"),\n recordId: v.string().describe(\n 'The ID of the record to update (starts with \"rec\")',\n ),\n fields: v\n .record(v.string(), v.unknown())\n .describe(\n 'Field values to update. Field names must match exactly. Example: { \"Status\": \"Done\" }',\n ),\n typecast: v.boolean().optional().describe(\n \"Allow Airtable to typecast field values\",\n ),\n })\n )(),\n async execute({ baseId, tableIdOrName, recordId, fields, typecast }) {\n const record = await updateRecord(baseId, tableIdOrName, recordId, fields, {\n typecast,\n });\n\n return {\n id: record.id,\n createdTime: record.createdTime,\n fields: record.fields,\n };\n },\n});\n",
153
- "tools/update-table.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { updateTable } from \"../lib/airtable-client.ts\";\n\nexport default tool({\n id: \"airtable-update-table\",\n description:\n \"Update Airtable table metadata, such as name or description. Uses the table ID for stable updates.\",\n inputSchema: defineSchema((v) =>\n v.object({\n baseId: v.string().describe(\n 'The ID of the Airtable base (starts with \"app\")',\n ),\n tableId: v.string().describe(\n 'The ID of the Airtable table (starts with \"tbl\")',\n ),\n name: v.string().optional().describe(\"New table name\"),\n description: v.string().optional().describe(\"New table description\"),\n })\n )(),\n execute: async ({ baseId, tableId, name, description }) =>\n updateTable(baseId, tableId, { name, description }),\n});\n"
154
- }
155
- },
156
- "integration:anthropic": {
157
- "files": {
158
- ".env.example": "# Anthropic Admin API Configuration\n# Get your admin API key from https://console.anthropic.com\n# Admin keys provide full access to organization management features\nANTHROPIC_ADMIN_API_KEY=sk-ant-admin-your-api-key-here\n",
159
- "lib/anthropic-admin-client.ts": "/**\n * Anthropic Admin API Client\n *\n * Provides methods to interact with the Anthropic Admin API for organization management.\n * Requires an admin API key with appropriate permissions.\n *\n * @see https://docs.anthropic.com/en/api/admin-api\n */\n\nconst ANTHROPIC_ADMIN_API_BASE_URL = 'https://api.anthropic.com/v1/admin';\n\nexport interface AnthropicWorkspace {\n id: string;\n name: string;\n display_name: string;\n created_at: string;\n}\n\nexport interface AnthropicUsageRecord {\n workspace_id: string;\n date: string;\n model: string;\n input_tokens: number;\n output_tokens: number;\n cache_creation_tokens?: number;\n cache_read_tokens?: number;\n total_cost_usd: number;\n}\n\nexport interface AnthropicAPIKey {\n id: string;\n name: string;\n workspace_id?: string;\n created_at: string;\n last_used_at?: string;\n status: 'active' | 'revoked';\n key_type: 'admin' | 'workspace' | 'service';\n}\n\nexport interface AnthropicMember {\n id: string;\n email: string;\n role: 'owner' | 'admin' | 'member' | 'developer';\n status: 'active' | 'pending' | 'inactive';\n created_at: string;\n last_active_at?: string;\n}\n\nexport interface AnthropicOrganization {\n id: string;\n name: string;\n display_name: string;\n created_at: string;\n settings: {\n default_model?: string;\n rate_limit_tier?: string;\n billing_email?: string;\n };\n}\n\nexport interface AnthropicUsageOptions {\n startDate: string;\n endDate: string;\n workspaceId?: string;\n model?: string;\n granularity?: 'day' | 'hour';\n}\n\nexport class AnthropicAdminError extends Error {\n constructor(\n message: string,\n public statusCode?: number,\n public response?: unknown\n ) {\n super(message);\n this.name = 'AnthropicAdminError';\n }\n}\n\n/**\n * Client for interacting with the Anthropic Admin API\n */\nexport class AnthropicAdminClient {\n private apiKey: string;\n private baseUrl: string;\n\n constructor(apiKey?: string, baseUrl?: string) {\n this.apiKey = apiKey ?? process.env.ANTHROPIC_ADMIN_API_KEY ?? '';\n this.baseUrl = baseUrl ?? ANTHROPIC_ADMIN_API_BASE_URL;\n\n if (!this.apiKey) {\n throw new AnthropicAdminError(\n 'ANTHROPIC_ADMIN_API_KEY is required. Please set it in your environment variables.'\n );\n }\n\n if (!this.apiKey.startsWith('sk-ant-')) {\n throw new AnthropicAdminError(\n 'Invalid Anthropic API key format. Admin keys should start with \"sk-ant-\"'\n );\n }\n }\n\n private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const response = await fetch(`${this.baseUrl}${endpoint}`, {\n ...options,\n headers: {\n 'x-api-key': this.apiKey,\n 'anthropic-version': '2023-06-01',\n 'Content-Type': 'application/json',\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n let errorData: any = {};\n try {\n errorData = await response.json();\n } catch {\n // ignore\n }\n\n throw new AnthropicAdminError(\n errorData?.error?.message ?? `API request failed: ${response.statusText}`,\n response.status,\n errorData\n );\n }\n\n return response.json();\n }\n\n async listWorkspaces(): Promise<{ workspaces: AnthropicWorkspace[] }> {\n return this.request('/workspaces');\n }\n\n async getWorkspace(workspaceId: string): Promise<AnthropicWorkspace> {\n if (!workspaceId) throw new AnthropicAdminError('workspaceId is required');\n return this.request(`/workspaces/${workspaceId}`);\n }\n\n async getUsage(options: AnthropicUsageOptions): Promise<{\n usage: AnthropicUsageRecord[];\n total_cost_usd: number;\n }> {\n const { startDate, endDate, workspaceId, model, granularity = 'day' } = options;\n\n if (!startDate || !endDate) {\n throw new AnthropicAdminError('startDate and endDate are required');\n }\n\n const dateRegex = /^\\d{4}-\\d{2}-\\d{2}$/;\n if (!dateRegex.test(startDate) || !dateRegex.test(endDate)) {\n throw new AnthropicAdminError('Dates must be in YYYY-MM-DD format');\n }\n\n const params = new URLSearchParams({\n start_date: startDate,\n end_date: endDate,\n granularity,\n });\n\n if (workspaceId) params.append('workspace_id', workspaceId);\n if (model) params.append('model', model);\n\n return this.request(`/usage?${params.toString()}`);\n }\n\n async listAPIKeys(workspaceId?: string): Promise<{ api_keys: AnthropicAPIKey[] }> {\n const endpoint = workspaceId ? `/workspaces/${workspaceId}/api-keys` : '/api-keys';\n return this.request(endpoint);\n }\n\n async listMembers(): Promise<{ members: AnthropicMember[] }> {\n return this.request('/members');\n }\n\n async getOrganization(): Promise<AnthropicOrganization> {\n return this.request('/organization');\n }\n\n async createAPIKey(data: {\n name: string;\n workspace_id?: string;\n key_type?: 'workspace' | 'service';\n }): Promise<{ api_key: AnthropicAPIKey & { key: string } }> {\n return this.request('/api-keys', {\n method: 'POST',\n body: JSON.stringify(data),\n });\n }\n\n async revokeAPIKey(keyId: string): Promise<{ success: boolean }> {\n if (!keyId) throw new AnthropicAdminError('keyId is required');\n\n return this.request(`/api-keys/${keyId}/revoke`, {\n method: 'POST',\n });\n }\n}\n\nlet client: AnthropicAdminClient | null = null;\n\nexport function getAnthropicAdminClient(): AnthropicAdminClient {\n client ??= new AnthropicAdminClient();\n return client;\n}\n\nexport default AnthropicAdminClient;\n",
160
- "tools/get-organization.ts": "import { tool } from 'veryfront/tool';\nimport { defineSchema } from 'veryfront/schemas';\nimport { getAnthropicAdminClient } from '../lib/anthropic-admin-client.ts';\n\nexport const getOrganization = tool({\n id: 'anthropic-get-organization',\n description:\n 'Get detailed information about the Anthropic organization including name, settings, default configurations, and billing information.',\n inputSchema: defineSchema((v) => v.object({}))(),\n execute: async () => {\n try {\n const client = getAnthropicAdminClient();\n const organization = await client.getOrganization();\n\n return {\n success: true,\n organization,\n message: `Retrieved organization details for ${organization.display_name}`,\n };\n } catch (error) {\n return {\n success: false,\n error:\n error instanceof Error\n ? error.message\n : 'Failed to retrieve organization details',\n organization: null,\n };\n }\n },\n});\n\nexport default getOrganization;\n",
161
- "tools/get-usage.ts": "import { tool } from 'veryfront/tool';\nimport { defineSchema } from 'veryfront/schemas';\nimport { getAnthropicAdminClient } from '../lib/anthropic-admin-client.ts';\n\nexport const getUsage = tool({\n id: 'anthropic-get-usage',\n description:\n 'Get API usage statistics for a specific date range. Returns token usage and costs broken down by date, workspace, and model. Dates must be in YYYY-MM-DD format.',\n inputSchema: defineSchema((v) =>\n v.object({\n startDate: v\n .string()\n .regex(/^\\d{4}-\\d{2}-\\d{2}$/, 'Date must be in YYYY-MM-DD format')\n .describe('Start date for usage query (YYYY-MM-DD format, e.g., 2025-01-01)'),\n endDate: v\n .string()\n .regex(/^\\d{4}-\\d{2}-\\d{2}$/, 'Date must be in YYYY-MM-DD format')\n .describe('End date for usage query (YYYY-MM-DD format, e.g., 2025-01-31)'),\n workspaceId: v.string().optional().describe('Optional workspace ID to filter usage by specific workspace'),\n model: v\n .string()\n .optional()\n .describe('Optional model name to filter usage (e.g., claude-3-opus-20240229, claude-3-sonnet-20240229)'),\n granularity: v.enum(['day', 'hour']).default('day').describe('Time granularity for usage aggregation (day or hour)'),\n })\n )(),\n execute: async ({ startDate, endDate, workspaceId, model, granularity }) => {\n try {\n const client = getAnthropicAdminClient();\n const result = await client.getUsage({ startDate, endDate, workspaceId, model, granularity });\n\n const totals = result.usage.reduce(\n (acc, record) => ({\n input: acc.input + record.input_tokens,\n output: acc.output + record.output_tokens,\n cacheCreation: acc.cacheCreation + (record.cache_creation_tokens ?? 0),\n cacheRead: acc.cacheRead + (record.cache_read_tokens ?? 0),\n }),\n { input: 0, output: 0, cacheCreation: 0, cacheRead: 0 },\n );\n\n return {\n success: true,\n usage: result.usage,\n summary: {\n total_cost_usd: result.total_cost_usd,\n total_input_tokens: totals.input,\n total_output_tokens: totals.output,\n total_cache_creation_tokens: totals.cacheCreation,\n total_cache_read_tokens: totals.cacheRead,\n record_count: result.usage.length,\n date_range: { start: startDate, end: endDate },\n filters: { workspace_id: workspaceId, model, granularity },\n },\n message: `Retrieved ${result.usage.length} usage record(s) totaling $${result.total_cost_usd.toFixed(4)} USD`,\n };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Failed to retrieve usage data',\n usage: [],\n };\n }\n },\n});\n\nexport default getUsage;\n",
162
- "tools/list-api-keys.ts": "import { tool } from 'veryfront/tool';\nimport { defineSchema } from 'veryfront/schemas';\nimport { getAnthropicAdminClient } from '../lib/anthropic-admin-client.ts';\n\nexport const listAPIKeys = tool({\n id: 'anthropic-list-api-keys',\n description:\n 'List all API keys for the organization or a specific workspace. Returns key metadata including name, status, type, and usage information. The actual key values are not returned for security reasons.',\n inputSchema: defineSchema((v) =>\n v.object({\n workspaceId: v\n .string()\n .optional()\n .describe(\n 'Optional workspace ID to filter API keys by workspace. If not provided, lists all organization API keys',\n ),\n })\n )(),\n execute: async ({ workspaceId }) => {\n try {\n const client = getAnthropicAdminClient();\n const { api_keys } = await client.listAPIKeys(workspaceId);\n\n let active = 0;\n let revoked = 0;\n const by_type: Record<string, number> = {};\n\n for (const key of api_keys) {\n if (key.status === 'active') active += 1;\n if (key.status === 'revoked') revoked += 1;\n\n by_type[key.key_type] = (by_type[key.key_type] ?? 0) + 1;\n }\n\n return {\n success: true,\n api_keys,\n summary: {\n total: api_keys.length,\n active,\n revoked,\n by_type,\n workspace_id: workspaceId,\n },\n message: workspaceId\n ? `Found ${api_keys.length} API key(s) for workspace ${workspaceId}`\n : `Found ${api_keys.length} API key(s) in the organization`,\n };\n } catch (error) {\n return {\n success: false,\n error:\n error instanceof Error ? error.message : 'Failed to list API keys',\n api_keys: [],\n };\n }\n },\n});\n\nexport default listAPIKeys;\n",
163
- "tools/list-members.ts": "import { tool } from 'veryfront/tool';\nimport { defineSchema } from 'veryfront/schemas';\nimport { getAnthropicAdminClient } from '../lib/anthropic-admin-client.ts';\n\nexport const listMembers = tool({\n id: 'anthropic-list-members',\n description:\n 'List all members in the Anthropic organization. Returns member details including email, role, status, and activity information.',\n inputSchema: defineSchema((v) => v.object({}))(),\n execute: async () => {\n try {\n const client = getAnthropicAdminClient();\n const { members } = await client.listMembers();\n\n const membersByRole: Record<string, number> = {};\n const membersByStatus: Record<string, number> = {};\n let active = 0;\n let pending = 0;\n\n for (const member of members) {\n membersByRole[member.role] = (membersByRole[member.role] ?? 0) + 1;\n membersByStatus[member.status] = (membersByStatus[member.status] ?? 0) + 1;\n\n if (member.status === 'active') active += 1;\n if (member.status === 'pending') pending += 1;\n }\n\n return {\n success: true,\n members,\n summary: {\n total: members.length,\n active,\n pending,\n by_role: membersByRole,\n by_status: membersByStatus,\n },\n message: `Found ${members.length} member(s) in the organization`,\n };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Failed to list members',\n members: [],\n };\n }\n },\n});\n\nexport default listMembers;\n",
164
- "tools/list-workspaces.ts": "import { tool } from 'veryfront/tool';\nimport { defineSchema } from 'veryfront/schemas';\nimport { getAnthropicAdminClient } from '../lib/anthropic-admin-client.ts';\n\nexport const listWorkspaces = tool({\n id: 'anthropic-list-workspaces',\n description:\n 'List all workspaces in the Anthropic organization. Workspaces allow you to organize API keys, usage, and permissions for different teams or projects.',\n inputSchema: defineSchema((v) => v.object({}))(),\n execute: async () => {\n try {\n const client = getAnthropicAdminClient();\n const { workspaces } = await client.listWorkspaces();\n const count = workspaces.length;\n\n return {\n success: true,\n workspaces,\n count,\n message: `Found ${count} workspace(s)`,\n };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Failed to list workspaces',\n workspaces: [],\n };\n }\n },\n});\n\nexport default listWorkspaces;\n"
165
- }
166
- },
167
- "integration:asana": {
168
- "files": {
169
- ".env.example": "# Asana OAuth Configuration\n# Get your credentials from https://app.asana.com/0/developer-console\nASANA_CLIENT_ID=your-client-id\nASANA_CLIENT_SECRET=your-client-secret\n",
170
- "app/api/auth/asana/callback/route.ts": "import { createOAuthCallbackHandler, asanaConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\n\nexport const GET = createOAuthCallbackHandler(asanaConfig, { tokenStore });\n",
171
- "app/api/auth/asana/route.ts": "import { asanaConfig, createOAuthInitHandler } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../lib/token-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../lib/user-id.ts\";\n\nexport const GET = createOAuthInitHandler(asanaConfig, {\n tokenStore,\n getUserId: requireUserIdFromRequest,\n});\n",
172
- "lib/asana-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst ASANA_BASE_URL = \"https://app.asana.com/api/1.0\";\n\ninterface AsanaResponse<T> {\n data: T;\n next_page?: { offset: string } | null;\n}\n\ninterface AsanaTask {\n gid: string;\n name: string;\n notes: string;\n completed: boolean;\n due_on: string | null;\n assignee: { gid: string; name: string } | null;\n projects: Array<{ gid: string; name: string }>;\n created_at: string;\n modified_at: string;\n}\n\ninterface AsanaProject {\n gid: string;\n name: string;\n notes: string;\n workspace: { gid: string; name: string };\n created_at: string;\n modified_at: string;\n}\n\ninterface AsanaWorkspace {\n gid: string;\n name: string;\n}\n\nasync function asanaFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with Asana. Please connect your account.\");\n }\n\n const response = await fetch(`${ASANA_BASE_URL}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (response.ok) {\n return response.json();\n }\n\n let error: unknown = {};\n try {\n error = await response.json();\n } catch {\n // ignore JSON parse errors\n }\n\n const message =\n (error as { errors?: Array<{ message?: string }> })?.errors?.[0]?.message ?? response.statusText;\n\n throw new Error(`Asana API error: ${response.status} ${message}`);\n}\n\nexport async function listWorkspaces(): Promise<AsanaWorkspace[]> {\n const { data } = await asanaFetch<AsanaResponse<AsanaWorkspace[]>>(\"/workspaces\");\n return data;\n}\n\nexport async function listProjects(workspaceGid: string): Promise<AsanaProject[]> {\n const { data } = await asanaFetch<AsanaResponse<AsanaProject[]>>(\n `/workspaces/${workspaceGid}/projects?opt_fields=name,notes,created_at,modified_at`,\n );\n return data;\n}\n\nexport async function listTasks(options: {\n projectGid?: string;\n assigneeGid?: string;\n workspaceGid?: string;\n completedSince?: string;\n}): Promise<AsanaTask[]> {\n const params = new URLSearchParams({\n opt_fields: \"name,notes,completed,due_on,assignee.name,projects.name,created_at,modified_at\",\n });\n\n if (options.completedSince) {\n params.set(\"completed_since\", options.completedSince);\n }\n\n let endpoint = \"/tasks\";\n if (options.projectGid) {\n endpoint = `/projects/${options.projectGid}/tasks`;\n } else if (options.assigneeGid && options.workspaceGid) {\n params.set(\"assignee\", options.assigneeGid);\n params.set(\"workspace\", options.workspaceGid);\n }\n\n const { data } = await asanaFetch<AsanaResponse<AsanaTask[]>>(`${endpoint}?${params}`);\n return data;\n}\n\nexport async function getTask(taskGid: string): Promise<AsanaTask> {\n const { data } = await asanaFetch<AsanaResponse<AsanaTask>>(\n `/tasks/${taskGid}?opt_fields=name,notes,completed,due_on,assignee.name,projects.name,created_at,modified_at`,\n );\n return data;\n}\n\nexport async function createTask(options: {\n projectGid: string;\n name: string;\n notes?: string;\n dueOn?: string;\n assigneeGid?: string;\n}): Promise<AsanaTask> {\n const body: Record<string, unknown> = {\n name: options.name,\n projects: [options.projectGid],\n };\n\n if (options.notes) body.notes = options.notes;\n if (options.dueOn) body.due_on = options.dueOn;\n if (options.assigneeGid) body.assignee = options.assigneeGid;\n\n const { data } = await asanaFetch<AsanaResponse<AsanaTask>>(\"/tasks\", {\n method: \"POST\",\n body: JSON.stringify({ data: body }),\n });\n\n return data;\n}\n\nexport async function updateTask(\n taskGid: string,\n updates: {\n name?: string;\n notes?: string;\n completed?: boolean;\n dueOn?: string;\n assigneeGid?: string;\n },\n): Promise<AsanaTask> {\n const body: Record<string, unknown> = {};\n\n if (updates.name !== undefined) body.name = updates.name;\n if (updates.notes !== undefined) body.notes = updates.notes;\n if (updates.completed !== undefined) body.completed = updates.completed;\n if (updates.dueOn !== undefined) body.due_on = updates.dueOn;\n if (updates.assigneeGid !== undefined) body.assignee = updates.assigneeGid;\n\n const { data } = await asanaFetch<AsanaResponse<AsanaTask>>(`/tasks/${taskGid}`, {\n method: \"PUT\",\n body: JSON.stringify({ data: body }),\n });\n\n return data;\n}\n\nexport async function getMe(): Promise<{ gid: string; name: string; email: string }> {\n const { data } = await asanaFetch<AsanaResponse<{ gid: string; name: string; email: string }>>(\n \"/users/me\",\n );\n return data;\n}\n\ninterface AsanaUser {\n gid: string;\n name: string;\n email?: string;\n}\n\ninterface AsanaTeam {\n gid: string;\n name: string;\n description?: string;\n}\n\ninterface AsanaStory {\n gid: string;\n type: string;\n text?: string;\n created_at: string;\n created_by?: { gid: string; name: string };\n}\n\nexport async function listUsers(options: {\n workspaceGid: string;\n teamGid?: string;\n}): Promise<AsanaUser[]> {\n const params = new URLSearchParams({\n workspace: options.workspaceGid,\n opt_fields: \"gid,name,email\",\n });\n\n if (options.teamGid) params.set(\"team\", options.teamGid);\n\n const { data } = await asanaFetch<AsanaResponse<AsanaUser[]>>(`/users?${params}`);\n return data;\n}\n\nexport async function listTeams(workspaceGid: string): Promise<AsanaTeam[]> {\n const { data } = await asanaFetch<AsanaResponse<AsanaTeam[]>>(\n `/workspaces/${workspaceGid}/teams?opt_fields=gid,name,description`,\n );\n return data;\n}\n\nexport async function addTaskComment(options: {\n taskGid: string;\n text: string;\n}): Promise<AsanaStory> {\n const { data } = await asanaFetch<AsanaResponse<AsanaStory>>(\n `/tasks/${options.taskGid}/stories`,\n {\n method: \"POST\",\n body: JSON.stringify({ data: { text: options.text } }),\n },\n );\n return data;\n}\n\nexport async function listTaskComments(taskGid: string): Promise<AsanaStory[]> {\n const params = new URLSearchParams({\n opt_fields: \"gid,type,text,created_at,created_by.name\",\n });\n const { data } = await asanaFetch<AsanaResponse<AsanaStory[]>>(\n `/tasks/${taskGid}/stories?${params}`,\n );\n return data.filter((story) => story.type === \"comment\");\n}\n",
173
- "tools/add-task-comment.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { addTaskComment } from \"../lib/asana-client.ts\";\n\nexport default tool({\n id: \"asana-add-task-comment\",\n description: \"Add a comment to an Asana task.\",\n inputSchema: defineSchema((v) => v.object({\n taskGid: v.string().describe(\"Asana task GID\"),\n text: v.string().min(1).describe(\"Comment text\"),\n }))(),\n async execute({ taskGid, text }) {\n const story = await addTaskComment({ taskGid, text });\n return {\n gid: story.gid,\n type: story.type,\n text: story.text,\n createdAt: story.created_at,\n createdBy: story.created_by?.name,\n };\n },\n});\n",
174
- "tools/create-task.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createTask } from \"../lib/asana-client.ts\";\n\nexport default tool({\n id: \"asana-create-task\",\n description: \"Create a new task in an Asana project.\",\n inputSchema: defineSchema((v) => v.object({\n projectGid: v.string().describe(\"The GID of the project to create the task in\"),\n name: v.string().describe(\"The name/title of the task\"),\n notes: v.string().optional().describe(\"Description or notes for the task\"),\n dueOn: v.string().optional().describe(\"Due date in YYYY-MM-DD format\"),\n assigneeGid: v.string().optional().describe(\"GID of the user to assign the task to\"),\n }))(),\n async execute({ projectGid, name, notes, dueOn, assigneeGid }) {\n const task = await createTask({\n projectGid,\n name,\n notes,\n dueOn,\n assigneeGid,\n });\n\n return {\n success: true,\n task: {\n gid: task.gid,\n name: task.name,\n dueOn: task.due_on,\n assignee: task.assignee?.name,\n },\n };\n },\n});\n",
175
- "tools/get-task.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { getTask } from \"../lib/asana-client.ts\";\n\nexport default tool({\n id: \"asana-get-task\",\n description: \"Get details of a specific Asana task by its GID.\",\n inputSchema: defineSchema((v) => v.object({\n taskGid: v.string().describe(\"The GID of the task to retrieve\"),\n }))(),\n async execute({ taskGid }) {\n const task = await getTask(taskGid);\n\n return {\n gid: task.gid,\n name: task.name,\n notes: task.notes,\n completed: task.completed,\n dueOn: task.due_on,\n assignee: task.assignee?.name,\n projects: task.projects.map(({ gid, name }) => ({ gid, name })),\n createdAt: task.created_at,\n modifiedAt: task.modified_at,\n };\n },\n});\n",
176
- "tools/list-projects.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { listProjects, listWorkspaces } from \"../lib/asana-client.ts\";\n\nexport default tool({\n id: \"asana-list-projects\",\n description: \"List all projects in the Asana workspace.\",\n inputSchema: defineSchema((v) => v.object({\n limit: v\n .number()\n .min(1)\n .max(50)\n .default(20)\n .describe(\"Maximum number of projects to return\"),\n }))(),\n async execute({ limit }) {\n const [workspace] = await listWorkspaces();\n\n if (!workspace) {\n return { projects: [], message: \"No workspaces found\" };\n }\n\n const projects = await listProjects(workspace.gid);\n\n return projects.slice(0, limit).map(({ gid, name, notes, created_at }) => ({\n gid,\n name,\n notes,\n createdAt: created_at,\n }));\n },\n});\n",
177
- "tools/list-task-comments.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { listTaskComments } from \"../lib/asana-client.ts\";\n\nexport default tool({\n id: \"asana-list-task-comments\",\n description: \"List comment stories for an Asana task.\",\n inputSchema: defineSchema((v) => v.object({\n taskGid: v.string().describe(\"Asana task GID\"),\n }))(),\n async execute({ taskGid }) {\n const comments = await listTaskComments(taskGid);\n return comments.map((story) => ({\n gid: story.gid,\n text: story.text,\n createdAt: story.created_at,\n createdBy: story.created_by?.name,\n }));\n },\n});\n",
178
- "tools/list-tasks.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { getMe, listTasks, listWorkspaces } from \"../lib/asana-client.ts\";\n\nexport default tool({\n id: \"asana-list-tasks\",\n description:\n \"List tasks from Asana. Can filter by project or get tasks assigned to the current user.\",\n inputSchema: defineSchema((v) => v.object({\n projectGid: v.string().optional().describe(\"Project GID to list tasks from\"),\n assignedToMe: v\n .boolean()\n .default(false)\n .describe(\"List tasks assigned to the current user\"),\n includeCompleted: v.boolean().default(false).describe(\"Include completed tasks\"),\n limit: v.number().min(1).max(50).default(20).describe(\"Maximum number of tasks to return\"),\n }))(),\n async execute({ projectGid, assignedToMe, includeCompleted, limit }) {\n const completedSince = includeCompleted ? undefined : \"now\";\n\n if (!assignedToMe && !projectGid) {\n return {\n tasks: [],\n message: \"Please specify either a projectGid or set assignedToMe to true\",\n };\n }\n\n let tasks;\n\n if (assignedToMe) {\n const me = await getMe();\n const workspaces = await listWorkspaces();\n const workspaceGid = workspaces[0]?.gid;\n\n if (!workspaceGid) {\n return { tasks: [], message: \"No workspaces found\" };\n }\n\n tasks = await listTasks({\n assigneeGid: me.gid,\n workspaceGid,\n completedSince,\n });\n } else {\n tasks = await listTasks({\n projectGid,\n completedSince,\n });\n }\n\n return tasks.slice(0, limit).map((task) => ({\n gid: task.gid,\n name: task.name,\n completed: task.completed,\n dueOn: task.due_on,\n assignee: task.assignee?.name,\n projects: task.projects.map((p) => p.name),\n }));\n },\n});\n",
179
- "tools/list-teams.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { listTeams } from \"../lib/asana-client.ts\";\n\nexport default tool({\n id: \"asana-list-teams\",\n description: \"List teams in an Asana workspace.\",\n inputSchema: defineSchema((v) => v.object({\n workspaceGid: v.string().describe(\"Asana workspace GID\"),\n }))(),\n async execute({ workspaceGid }) {\n const teams = await listTeams(workspaceGid);\n return teams.map(({ gid, name, description }) => ({ gid, name, description }));\n },\n});\n",
180
- "tools/list-users.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { listUsers } from \"../lib/asana-client.ts\";\n\nexport default tool({\n id: \"asana-list-users\",\n description: \"List users in an Asana workspace.\",\n inputSchema: defineSchema((v) => v.object({\n workspaceGid: v.string().describe(\"Asana workspace GID\"),\n teamGid: v.string().optional().describe(\"Optional Asana team GID\"),\n }))(),\n async execute({ workspaceGid, teamGid }) {\n const users = await listUsers({ workspaceGid, teamGid });\n return users.map(({ gid, name, email }) => ({ gid, name, email }));\n },\n});\n",
181
- "tools/list-workspaces.ts": "import { tool } from \"veryfront/tool\";\nimport { listWorkspaces } from \"../lib/asana-client.ts\";\n\nexport default tool({\n id: \"asana-list-workspaces\",\n description: \"List Asana workspaces accessible to the authenticated user.\",\n async execute() {\n const workspaces = await listWorkspaces();\n return workspaces.map(({ gid, name }) => ({ gid, name }));\n },\n});\n",
182
- "tools/update-task.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { updateTask } from \"../lib/asana-client.ts\";\n\nexport default tool({\n id: \"asana-update-task\",\n description: \"Update an existing Asana task.\",\n inputSchema: defineSchema((v) => v.object({\n taskGid: v.string().describe(\"The GID of the task to update\"),\n name: v.string().optional().describe(\"New name/title for the task\"),\n notes: v.string().optional().describe(\"New description or notes\"),\n completed: v.boolean().optional().describe(\"Mark the task as completed or not\"),\n dueOn: v.string().optional().describe(\"New due date in YYYY-MM-DD format\"),\n assigneeGid: v.string().optional().describe(\"GID of the user to reassign the task to\"),\n }))(),\n async execute({ taskGid, ...updates }) {\n const task = await updateTask(taskGid, updates);\n\n return {\n success: true,\n task: {\n gid: task.gid,\n name: task.name,\n completed: task.completed,\n dueOn: task.due_on,\n assignee: task.assignee?.name,\n },\n };\n },\n});\n"
183
- }
184
- },
185
- "integration:aws": {
186
- "files": {
187
- ".env.example": "# AWS Integration Configuration\n\n# AWS Access Key ID (from IAM user)\nAWS_ACCESS_KEY_ID=your_access_key_id_here\n\n# AWS Secret Access Key (from IAM user)\nAWS_SECRET_ACCESS_KEY=your_secret_access_key_here\n\n# AWS Region (e.g., us-east-1, us-west-2, eu-west-1)\nAWS_REGION=us-east-1\n",
188
- "lib/aws-client.ts": "import { EC2Client, DescribeInstancesCommand } from '@aws-sdk/client-ec2';\nimport { LambdaClient, ListFunctionsCommand } from '@aws-sdk/client-lambda';\nimport { GetObjectCommand, ListBucketsCommand, ListObjectsV2Command, S3Client } from '@aws-sdk/client-s3';\n\ninterface AWSClientConfig {\n region?: string;\n accessKeyId?: string;\n secretAccessKey?: string;\n}\n\nexport interface S3Bucket {\n name: string;\n creationDate?: Date;\n}\n\nexport interface S3Object {\n key: string;\n size: number;\n lastModified?: Date;\n etag?: string;\n storageClass?: string;\n}\n\nexport interface EC2Instance {\n instanceId: string;\n instanceType: string;\n state: string;\n publicIpAddress?: string;\n privateIpAddress?: string;\n launchTime?: Date;\n name?: string;\n availabilityZone?: string;\n}\n\nexport interface LambdaFunction {\n functionName: string;\n functionArn: string;\n runtime?: string;\n handler?: string;\n codeSize: number;\n lastModified: string;\n memorySize?: number;\n timeout?: number;\n description?: string;\n}\n\nexport class AWSClient {\n private region: string;\n private credentials: { accessKeyId: string; secretAccessKey: string };\n\n constructor(config?: AWSClientConfig) {\n this.region = config?.region ?? process.env.AWS_REGION ?? 'us-east-1';\n this.credentials = {\n accessKeyId: config?.accessKeyId ?? process.env.AWS_ACCESS_KEY_ID ?? '',\n secretAccessKey: config?.secretAccessKey ?? process.env.AWS_SECRET_ACCESS_KEY ?? '',\n };\n\n if (!this.credentials.accessKeyId || !this.credentials.secretAccessKey) {\n throw new Error(\n 'AWS credentials are required. Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables.',\n );\n }\n }\n\n private getS3Client(): S3Client {\n return new S3Client({ region: this.region, credentials: this.credentials });\n }\n\n private getEC2Client(region?: string): EC2Client {\n return new EC2Client({ region: region ?? this.region, credentials: this.credentials });\n }\n\n private getLambdaClient(region?: string): LambdaClient {\n return new LambdaClient({ region: region ?? this.region, credentials: this.credentials });\n }\n\n private formatErrorMessage(error: unknown): string {\n return error instanceof Error ? error.message : 'Unknown error';\n }\n\n async listS3Buckets(): Promise<S3Bucket[]> {\n const client = this.getS3Client();\n\n try {\n const response = await client.send(new ListBucketsCommand({}));\n return (response.Buckets ?? []).map(bucket => ({\n name: bucket.Name ?? '',\n creationDate: bucket.CreationDate,\n }));\n } catch (error) {\n throw new Error(`Failed to list S3 buckets: ${this.formatErrorMessage(error)}`);\n }\n }\n\n async listS3Objects(bucket: string, prefix?: string, maxKeys?: number): Promise<S3Object[]> {\n const client = this.getS3Client();\n\n try {\n const response = await client.send(\n new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: prefix,\n MaxKeys: maxKeys ?? 1000,\n }),\n );\n\n return (response.Contents ?? []).map(object => ({\n key: object.Key ?? '',\n size: object.Size ?? 0,\n lastModified: object.LastModified,\n etag: object.ETag,\n storageClass: object.StorageClass,\n }));\n } catch (error) {\n throw new Error(`Failed to list S3 objects in bucket ${bucket}: ${this.formatErrorMessage(error)}`);\n }\n }\n\n async getS3Object(bucket: string, key: string): Promise<string> {\n const client = this.getS3Client();\n\n try {\n const response = await client.send(\n new GetObjectCommand({\n Bucket: bucket,\n Key: key,\n }),\n );\n\n if (!response.Body) throw new Error('Object body is empty');\n\n return response.Body.transformToString();\n } catch (error) {\n throw new Error(`Failed to get S3 object ${key} from bucket ${bucket}: ${this.formatErrorMessage(error)}`);\n }\n }\n\n async listEC2Instances(region?: string): Promise<EC2Instance[]> {\n const client = this.getEC2Client(region);\n\n try {\n const response = await client.send(new DescribeInstancesCommand({}));\n\n return (response.Reservations ?? []).flatMap(reservation =>\n (reservation.Instances ?? []).map(instance => {\n const nameTag = instance.Tags?.find(tag => tag.Key === 'Name');\n\n return {\n instanceId: instance.InstanceId ?? '',\n instanceType: instance.InstanceType ?? '',\n state: instance.State?.Name ?? 'unknown',\n publicIpAddress: instance.PublicIpAddress,\n privateIpAddress: instance.PrivateIpAddress,\n launchTime: instance.LaunchTime,\n name: nameTag?.Value,\n availabilityZone: instance.Placement?.AvailabilityZone,\n };\n }),\n );\n } catch (error) {\n throw new Error(`Failed to list EC2 instances: ${this.formatErrorMessage(error)}`);\n }\n }\n\n async listLambdaFunctions(region?: string): Promise<LambdaFunction[]> {\n const client = this.getLambdaClient(region);\n\n try {\n const response = await client.send(new ListFunctionsCommand({}));\n\n return (response.Functions ?? []).map(func => ({\n functionName: func.FunctionName ?? '',\n functionArn: func.FunctionArn ?? '',\n runtime: func.Runtime,\n handler: func.Handler,\n codeSize: func.CodeSize ?? 0,\n lastModified: func.LastModified ?? '',\n memorySize: func.MemorySize,\n timeout: func.Timeout,\n description: func.Description,\n }));\n } catch (error) {\n throw new Error(`Failed to list Lambda functions: ${this.formatErrorMessage(error)}`);\n }\n }\n}\n\nlet awsClient: AWSClient | null = null;\n\nexport function getAWSClient(config?: AWSClientConfig): AWSClient {\n if (awsClient) return awsClient;\n\n awsClient = new AWSClient(config);\n return awsClient;\n}\n\nexport default AWSClient;\n",
189
- "tools/get-s3-object.ts": "import { tool } from 'veryfront/tool';\nimport { defineSchema } from 'veryfront/schemas';\nimport { getAWSClient } from '../lib/aws-client.ts';\n\nexport const getS3ObjectTool = tool({\n id: 'aws-get-s3-object',\n description: 'Get the contents of an object from an S3 bucket. Returns the object content as a string.',\n inputSchema: defineSchema((v) =>\n v.object({\n bucket: v.string().describe('The name of the S3 bucket'),\n key: v.string().describe('The key (path) of the object to retrieve'),\n })\n )(),\n execute: async ({ bucket, key }) => {\n try {\n const client = getAWSClient();\n const content = await client.getS3Object(bucket, key);\n\n const isBinary = /[\\x00-\\x08\\x0E-\\x1F]/.test(content.substring(0, 8000));\n if (isBinary) {\n return {\n success: true,\n message: `Retrieved object \"${key}\" from bucket \"${bucket}\". Content appears to be binary.`,\n bucket,\n key,\n contentType: 'binary',\n contentLength: content.length,\n contentPreview: '[Binary content - not displayed]',\n };\n }\n\n const maxPreviewLength = 10000;\n const truncated = content.length > maxPreviewLength;\n\n return {\n success: true,\n message: `Retrieved object \"${key}\" from bucket \"${bucket}\".`,\n bucket,\n key,\n contentType: 'text',\n contentLength: content.length,\n content: truncated ? `${content.substring(0, maxPreviewLength)}\\n... [truncated]` : content,\n truncated,\n };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Failed to get S3 object',\n bucket,\n key,\n };\n }\n },\n});\n\nexport default getS3ObjectTool;\n",
190
- "tools/list-ec2-instances.ts": "import { tool } from 'veryfront/tool';\nimport { defineSchema } from 'veryfront/schemas';\nimport { getAWSClient } from '../lib/aws-client.ts';\n\nexport const listEC2InstancesTool = tool({\n id: 'aws-list-ec2-instances',\n description:\n 'List all EC2 instances in your AWS account. Returns instance details including ID, type, state, and IP addresses.',\n inputSchema: defineSchema((v) =>\n v.object({\n region: v\n .string()\n .optional()\n .describe(\n 'AWS region to list instances from (e.g., \"us-east-1\", \"eu-west-1\"). Defaults to configured region.',\n ),\n })\n )(),\n execute: async ({ region }) => {\n try {\n const client = getAWSClient();\n const instances = await client.listEC2Instances(region);\n const regionMessage = region ? ` in region \"${region}\"` : '';\n\n if (instances.length === 0) {\n return {\n success: true,\n message: `No EC2 instances found${regionMessage}.`,\n instances: [],\n region,\n };\n }\n\n const count = instances.length;\n\n return {\n success: true,\n message: `Found ${count} EC2 instance${count === 1 ? '' : 's'}${regionMessage}.`,\n instances: instances.map((instance) => ({\n instanceId: instance.instanceId,\n instanceType: instance.instanceType,\n state: instance.state,\n name: instance.name ?? 'N/A',\n publicIpAddress: instance.publicIpAddress ?? 'N/A',\n privateIpAddress: instance.privateIpAddress ?? 'N/A',\n availabilityZone: instance.availabilityZone ?? 'N/A',\n launchTime: instance.launchTime?.toISOString(),\n })),\n count,\n region,\n };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Failed to list EC2 instances',\n instances: [],\n region,\n };\n }\n },\n});\n\nexport default listEC2InstancesTool;\n",
191
- "tools/list-lambda-functions.ts": "import { tool } from 'veryfront/tool';\nimport { defineSchema } from 'veryfront/schemas';\nimport { getAWSClient } from '../lib/aws-client.ts';\n\nexport const listLambdaFunctionsTool = tool({\n id: 'aws-list-lambda-functions',\n description:\n 'List all Lambda functions in your AWS account. Returns function details including name, ARN, runtime, and configuration.',\n inputSchema: defineSchema((v) =>\n v.object({\n region: v\n .string()\n .optional()\n .describe(\n 'AWS region to list Lambda functions from (e.g., \"us-east-1\", \"eu-west-1\"). Defaults to configured region.',\n ),\n })\n )(),\n execute: async ({ region }) => {\n try {\n const client = getAWSClient();\n const functions = await client.listLambdaFunctions(region);\n const regionMessage = region ? ` in region \"${region}\"` : '';\n\n if (!functions.length) {\n return {\n success: true,\n message: `No Lambda functions found${regionMessage}.`,\n functions: [],\n region,\n };\n }\n\n return {\n success: true,\n message: `Found ${functions.length} Lambda function${functions.length === 1 ? '' : 's'}${regionMessage}.`,\n functions: functions.map((func) => ({\n functionName: func.functionName,\n functionArn: func.functionArn,\n runtime: func.runtime ?? 'N/A',\n handler: func.handler ?? 'N/A',\n codeSize: func.codeSize,\n lastModified: func.lastModified,\n memorySize: func.memorySize ?? 'N/A',\n timeout: func.timeout ?? 'N/A',\n description: func.description ?? 'No description',\n })),\n count: functions.length,\n region,\n };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Failed to list Lambda functions',\n functions: [],\n region,\n };\n }\n },\n});\n\nexport default listLambdaFunctionsTool;\n",
192
- "tools/list-s3-buckets.ts": "import { tool } from 'veryfront/tool';\nimport { defineSchema } from 'veryfront/schemas';\nimport { getAWSClient } from '../lib/aws-client.ts';\n\nexport const listS3BucketsTool = tool({\n id: 'aws-list-s3-buckets',\n description: 'List all S3 buckets in your AWS account. Returns bucket names and creation dates.',\n inputSchema: defineSchema((v) => v.object({}))(),\n execute: async () => {\n try {\n const client = getAWSClient();\n const buckets = await client.listS3Buckets();\n const count = buckets.length;\n\n if (count === 0) {\n return {\n success: true,\n message: 'No S3 buckets found in your AWS account.',\n buckets: [],\n };\n }\n\n return {\n success: true,\n message: `Found ${count} S3 bucket${count === 1 ? '' : 's'}.`,\n buckets: buckets.map((bucket) => ({\n name: bucket.name,\n creationDate: bucket.creationDate?.toISOString(),\n })),\n count,\n };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Failed to list S3 buckets',\n buckets: [],\n };\n }\n },\n});\n\nexport default listS3BucketsTool;\n",
193
- "tools/list-s3-objects.ts": "import { tool } from 'veryfront/tool';\nimport { defineSchema } from 'veryfront/schemas';\nimport { getAWSClient } from '../lib/aws-client.ts';\n\nexport const listS3ObjectsTool = tool({\n id: 'aws-list-s3-objects',\n description: 'List objects in a specific S3 bucket. Optionally filter by prefix and limit the number of results.',\n inputSchema: defineSchema((v) =>\n v.object({\n bucket: v.string().describe('The name of the S3 bucket to list objects from'),\n prefix: v.string().optional().describe('Optional prefix to filter objects (e.g., \"folder/\" or \"images/\")'),\n maxKeys: v.number().min(1).max(1000).optional().describe('Maximum number of objects to return (default: 1000)'),\n })\n )(),\n execute: async ({ bucket, prefix, maxKeys }) => {\n try {\n const client = getAWSClient();\n const objects = await client.listS3Objects(bucket, prefix, maxKeys);\n const prefixMessage = prefix ? ` with prefix \"${prefix}\"` : '';\n\n if (objects.length === 0) {\n return {\n success: true,\n message: `No objects found in bucket \"${bucket}\"${prefixMessage}.`,\n objects: [],\n bucket,\n prefix,\n };\n }\n\n const count = objects.length;\n\n return {\n success: true,\n message: `Found ${count} object${count === 1 ? '' : 's'} in bucket \"${bucket}\"${prefixMessage}.`,\n objects: objects.map(({ key, size, lastModified, etag, storageClass }) => ({\n key,\n size,\n lastModified: lastModified?.toISOString(),\n etag,\n storageClass,\n })),\n count,\n bucket,\n prefix,\n };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Failed to list S3 objects',\n objects: [],\n bucket,\n prefix,\n };\n }\n },\n});\n\nexport default listS3ObjectsTool;\n"
194
- }
195
- },
196
- "integration:bitbucket": {
197
- "files": {
198
- ".env.example": "# Bitbucket OAuth Configuration\n# Create a new OAuth consumer at: https://bitbucket.org/account/settings/app-passwords/\n# Or create an OAuth consumer at: https://bitbucket.org/{workspace}/workspace/settings/oauth-consumers/new\n# Set the callback URL to: http://localhost:3000/api/auth/bitbucket/callback\n# (Update the URL for production)\n# Required permissions: repository, pullrequest, issue, account\n\nBITBUCKET_CLIENT_ID=your_bitbucket_client_id\nBITBUCKET_CLIENT_SECRET=your_bitbucket_client_secret\n",
199
- "app/api/auth/bitbucket/callback/route.ts": "import { createOAuthCallbackHandler, bitbucketConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\n\nexport const GET = createOAuthCallbackHandler(bitbucketConfig, { tokenStore });\n",
200
- "app/api/auth/bitbucket/route.ts": "import { bitbucketConfig, createOAuthInitHandler } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../lib/token-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../lib/user-id.ts\";\n\nexport const GET = createOAuthInitHandler(bitbucketConfig, {\n tokenStore,\n getUserId: requireUserIdFromRequest,\n});\n",
201
- "lib/bitbucket-client.ts": "import { getValidToken } from \"./oauth.ts\";\n\nfunction getEnv(key: string): string | undefined {\n // @ts-ignore - Deno global\n if (typeof Deno !== \"undefined\") return Deno.env.get(key);\n\n // @ts-ignore - process global\n if (typeof process !== \"undefined\" && process.env) return process.env[key];\n\n return undefined;\n}\n\nconst BITBUCKET_API_BASE = \"https://api.bitbucket.org/2.0\";\n\nexport interface BitbucketUser {\n uuid: string;\n username: string;\n display_name: string;\n account_id: string;\n links: {\n avatar: { href: string };\n html: { href: string };\n };\n}\n\nexport interface Repository {\n uuid: string;\n name: string;\n full_name: string;\n description: string | null;\n is_private: boolean;\n mainbranch: { name: string } | null;\n language: string;\n size: number;\n updated_on: string;\n created_on: string;\n links: {\n html: { href: string };\n clone: Array<{ href: string; name: string }>;\n };\n owner: {\n username: string;\n display_name: string;\n };\n}\n\nexport interface PullRequest {\n id: number;\n title: string;\n description: string;\n state: \"OPEN\" | \"MERGED\" | \"DECLINED\" | \"SUPERSEDED\";\n author: {\n username: string;\n display_name: string;\n };\n created_on: string;\n updated_on: string;\n source: {\n branch: { name: string };\n repository: { full_name: string };\n };\n destination: {\n branch: { name: string };\n repository: { full_name: string };\n };\n links: {\n html: { href: string };\n diff: { href: string };\n };\n comment_count: number;\n task_count: number;\n}\n\nexport interface Issue {\n id: number;\n title: string;\n content: {\n raw: string;\n } | null;\n state: \"new\" | \"open\" | \"resolved\" | \"on hold\" | \"invalid\" | \"duplicate\" | \"wontfix\" | \"closed\";\n kind: \"bug\" | \"enhancement\" | \"proposal\" | \"task\";\n priority: \"trivial\" | \"minor\" | \"major\" | \"critical\" | \"blocker\";\n created_on: string;\n updated_on: string;\n reporter: {\n username: string;\n display_name: string;\n };\n assignee: {\n username: string;\n display_name: string;\n } | null;\n links: {\n html: { href: string };\n };\n}\n\nexport const bitbucketOAuthProvider = {\n name: \"bitbucket\",\n authorizationUrl: \"https://bitbucket.org/site/oauth2/authorize\",\n tokenUrl: \"https://bitbucket.org/site/oauth2/access_token\",\n clientId: getEnv(\"BITBUCKET_CLIENT_ID\") ?? \"\",\n clientSecret: getEnv(\"BITBUCKET_CLIENT_SECRET\") ?? \"\",\n scopes: [\"repository\", \"pullrequest\", \"issue\", \"account\"],\n callbackPath: \"/api/auth/bitbucket/callback\",\n};\n\nfunction buildQuery(params: URLSearchParams): string {\n const query = params.toString();\n return query ? `?${query}` : \"\";\n}\n\nexport function createBitbucketClient(userId: string): {\n getCurrentUser(): Promise<BitbucketUser>;\n listRepositories(options?: { role?: \"owner\" | \"contributor\" | \"member\"; perPage?: number }): Promise<Repository[]>;\n getRepository(workspace: string, repoSlug: string): Promise<Repository>;\n listPullRequests(\n workspace: string,\n repoSlug: string,\n options?: { state?: \"OPEN\" | \"MERGED\" | \"DECLINED\" | \"SUPERSEDED\"; perPage?: number },\n ): Promise<PullRequest[]>;\n getPullRequest(workspace: string, repoSlug: string, pullRequestId: number): Promise<PullRequest>;\n createPullRequest(\n workspace: string,\n repoSlug: string,\n options: {\n title: string;\n description?: string;\n sourceBranch: string;\n destinationBranch: string;\n closeSourceBranch?: boolean;\n },\n ): Promise<PullRequest>;\n listIssues(\n workspace: string,\n repoSlug: string,\n options?: {\n state?:\n | \"new\"\n | \"open\"\n | \"resolved\"\n | \"on hold\"\n | \"invalid\"\n | \"duplicate\"\n | \"wontfix\"\n | \"closed\";\n kind?: \"bug\" | \"enhancement\" | \"proposal\" | \"task\";\n priority?: \"trivial\" | \"minor\" | \"major\" | \"critical\" | \"blocker\";\n perPage?: number;\n },\n ): Promise<Issue[]>;\n createIssue(\n workspace: string,\n repoSlug: string,\n options: {\n title: string;\n description?: string;\n kind?: \"bug\" | \"enhancement\" | \"proposal\" | \"task\";\n priority?: \"trivial\" | \"minor\" | \"major\" | \"critical\" | \"blocker\";\n },\n ): Promise<Issue>;\n} {\n async function getAccessToken(): Promise<string> {\n const token = await getValidToken(bitbucketOAuthProvider, userId, \"bitbucket\");\n if (!token) throw new Error(\"Bitbucket not connected. Please connect your Bitbucket account first.\");\n return token;\n }\n\n async function apiRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const accessToken = await getAccessToken();\n\n const response = await fetch(`${BITBUCKET_API_BASE}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Bitbucket API error: ${response.status} - ${error}`);\n }\n\n return response.json();\n }\n\n return {\n getCurrentUser(): Promise<BitbucketUser> {\n return apiRequest(\"/user\");\n },\n\n async listRepositories(\n options: {\n role?: \"owner\" | \"contributor\" | \"member\";\n perPage?: number;\n } = {},\n ): Promise<Repository[]> {\n const params = new URLSearchParams();\n if (options.role) params.set(\"role\", options.role);\n if (options.perPage) params.set(\"pagelen\", String(options.perPage));\n\n const { values } = await apiRequest<{ values: Repository[] }>(`/repositories${buildQuery(params)}`);\n return values;\n },\n\n getRepository(workspace: string, repoSlug: string): Promise<Repository> {\n return apiRequest(`/repositories/${workspace}/${repoSlug}`);\n },\n\n async listPullRequests(\n workspace: string,\n repoSlug: string,\n options: {\n state?: \"OPEN\" | \"MERGED\" | \"DECLINED\" | \"SUPERSEDED\";\n perPage?: number;\n } = {},\n ): Promise<PullRequest[]> {\n const params = new URLSearchParams();\n if (options.state) params.set(\"state\", options.state);\n if (options.perPage) params.set(\"pagelen\", String(options.perPage));\n\n const { values } = await apiRequest<{ values: PullRequest[] }>(\n `/repositories/${workspace}/${repoSlug}/pullrequests${buildQuery(params)}`,\n );\n return values;\n },\n\n getPullRequest(workspace: string, repoSlug: string, pullRequestId: number): Promise<PullRequest> {\n return apiRequest(`/repositories/${workspace}/${repoSlug}/pullrequests/${pullRequestId}`);\n },\n\n createPullRequest(\n workspace: string,\n repoSlug: string,\n options: {\n title: string;\n description?: string;\n sourceBranch: string;\n destinationBranch: string;\n closeSourceBranch?: boolean;\n },\n ): Promise<PullRequest> {\n return apiRequest(`/repositories/${workspace}/${repoSlug}/pullrequests`, {\n method: \"POST\",\n body: JSON.stringify({\n title: options.title,\n description: options.description,\n source: { branch: { name: options.sourceBranch } },\n destination: { branch: { name: options.destinationBranch } },\n close_source_branch: options.closeSourceBranch,\n }),\n });\n },\n\n async listIssues(\n workspace: string,\n repoSlug: string,\n options: {\n state?:\n | \"new\"\n | \"open\"\n | \"resolved\"\n | \"on hold\"\n | \"invalid\"\n | \"duplicate\"\n | \"wontfix\"\n | \"closed\";\n kind?: \"bug\" | \"enhancement\" | \"proposal\" | \"task\";\n priority?: \"trivial\" | \"minor\" | \"major\" | \"critical\" | \"blocker\";\n perPage?: number;\n } = {},\n ): Promise<Issue[]> {\n const params = new URLSearchParams();\n if (options.state) params.set(\"q\", `state=\"${options.state}\"`);\n if (options.kind) params.set(\"kind\", options.kind);\n if (options.priority) params.set(\"priority\", options.priority);\n if (options.perPage) params.set(\"pagelen\", String(options.perPage));\n\n const { values } = await apiRequest<{ values: Issue[] }>(\n `/repositories/${workspace}/${repoSlug}/issues${buildQuery(params)}`,\n );\n return values;\n },\n\n createIssue(\n workspace: string,\n repoSlug: string,\n options: {\n title: string;\n description?: string;\n kind?: \"bug\" | \"enhancement\" | \"proposal\" | \"task\";\n priority?: \"trivial\" | \"minor\" | \"major\" | \"critical\" | \"blocker\";\n },\n ): Promise<Issue> {\n return apiRequest(`/repositories/${workspace}/${repoSlug}/issues`, {\n method: \"POST\",\n body: JSON.stringify({\n title: options.title,\n content: options.description ? { raw: options.description } : undefined,\n kind: options.kind ?? \"bug\",\n priority: options.priority ?? \"major\",\n }),\n });\n },\n };\n}\n\nexport type BitbucketClient = ReturnType<typeof createBitbucketClient>;\n",
202
- "tools/create-pull-request.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createBitbucketClient } from \"../lib/bitbucket-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"bitbucket-create-pull-request\",\n description: \"Create a new pull request in a Bitbucket repository\",\n inputSchema: defineSchema((v) => v.object({\n workspace: v.string().describe(\"Workspace name or UUID\"),\n repoSlug: v.string().describe(\"Repository slug (e.g., 'my-repo')\"),\n title: v.string().min(1).describe(\"Pull request title\"),\n description: v\n .string()\n .optional()\n .describe(\"Pull request description (supports Markdown)\"),\n sourceBranch: v.string().describe(\"Source branch name\"),\n destinationBranch: v.string().describe(\"Destination branch name\"),\n closeSourceBranch: v\n .boolean()\n .optional()\n .default(false)\n .describe(\"Close source branch after merge\"),\n }))(),\n execute: async (\n {\n workspace,\n repoSlug,\n title,\n description,\n sourceBranch,\n destinationBranch,\n closeSourceBranch,\n },\n context,\n ) => {\n const userId = requireUserIdFromContext(context);\n\n try {\n const bitbucket = createBitbucketClient(userId);\n const pr = await bitbucket.createPullRequest(workspace, repoSlug, {\n title,\n description,\n sourceBranch,\n destinationBranch,\n closeSourceBranch,\n });\n\n return {\n success: true,\n pullRequest: {\n id: pr.id,\n title: pr.title,\n url: pr.links.html.href,\n state: pr.state,\n sourceBranch: pr.source.branch.name,\n destinationBranch: pr.destination.branch.name,\n author: {\n username: pr.author.username,\n displayName: pr.author.display_name,\n },\n },\n message: `Pull request #${pr.id} created successfully in ${workspace}/${repoSlug}.`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Bitbucket not connected. Please connect your Bitbucket account.\",\n connectUrl: \"/api/auth/bitbucket\",\n };\n }\n\n throw error;\n }\n },\n});\n",
203
- "tools/list-issues.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createBitbucketClient } from \"../lib/bitbucket-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\ntype BitbucketIssue = {\n id: number;\n title: string;\n state: string;\n kind: string;\n priority: string;\n created_on: string;\n updated_on: string;\n reporter: {\n username: string;\n display_name: string;\n };\n assignee: {\n username: string;\n display_name: string;\n } | null;\n links: {\n html: { href: string };\n };\n content: {\n raw: string;\n } | null;\n};\n\nexport default tool({\n id: \"bitbucket-list-issues\",\n description: \"List issues for a Bitbucket repository\",\n inputSchema: defineSchema((v) => v.object({\n workspace: v.string().describe(\"Workspace name or UUID\"),\n repoSlug: v.string().describe(\"Repository slug (e.g., 'my-repo')\"),\n state: v\n .enum([\n \"new\",\n \"open\",\n \"resolved\",\n \"on hold\",\n \"invalid\",\n \"duplicate\",\n \"wontfix\",\n \"closed\",\n ])\n .optional()\n .describe(\"Filter by issue state\"),\n kind: v\n .enum([\"bug\", \"enhancement\", \"proposal\", \"task\"])\n .optional()\n .describe(\"Filter by issue kind\"),\n priority: v\n .enum([\"trivial\", \"minor\", \"major\", \"critical\", \"blocker\"])\n .optional()\n .describe(\"Filter by priority level\"),\n limit: v\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of issues to return\"),\n }))(),\n execute: async (\n { workspace, repoSlug, state, kind, priority, limit },\n context,\n ) => {\n const userId = requireUserIdFromContext(context);\n\n try {\n const bitbucket = createBitbucketClient(userId);\n const issues = await bitbucket.listIssues(workspace, repoSlug, {\n state,\n kind,\n priority,\n perPage: limit,\n });\n\n const repository = `${workspace}/${repoSlug}`;\n\n return {\n issues: issues.map((issue: BitbucketIssue) => ({\n id: issue.id,\n title: issue.title,\n state: issue.state,\n kind: issue.kind,\n priority: issue.priority,\n description: issue.content?.raw ?? null,\n reporter: {\n username: issue.reporter.username,\n displayName: issue.reporter.display_name,\n },\n assignee: issue.assignee\n ? {\n username: issue.assignee.username,\n displayName: issue.assignee.display_name,\n }\n : null,\n url: issue.links.html.href,\n createdOn: issue.created_on,\n updatedOn: issue.updated_on,\n })),\n count: issues.length,\n repository,\n filters: {\n state: state ?? \"all\",\n kind: kind ?? \"all\",\n priority: priority ?? \"all\",\n },\n message: `Found ${issues.length} issue(s) in ${repository}.`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Bitbucket not connected. Please connect your Bitbucket account.\",\n connectUrl: \"/api/auth/bitbucket\",\n };\n }\n throw error;\n }\n },\n});\n",
204
- "tools/list-pull-requests.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createBitbucketClient } from \"../lib/bitbucket-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\ntype PullRequest = {\n id: number;\n title: string;\n state: string;\n author: {\n username: string;\n display_name: string;\n };\n created_on: string;\n updated_on: string;\n source: {\n branch: { name: string };\n };\n destination: {\n branch: { name: string };\n };\n links: {\n html: { href: string };\n };\n comment_count: number;\n task_count: number;\n};\n\nexport default tool({\n id: \"bitbucket-list-pull-requests\",\n description: \"List pull requests for a Bitbucket repository\",\n inputSchema: defineSchema((v) => v.object({\n workspace: v.string().describe(\"Workspace name or UUID\"),\n repoSlug: v.string().describe(\"Repository slug (e.g., 'my-repo')\"),\n state: v\n .enum([\"OPEN\", \"MERGED\", \"DECLINED\", \"SUPERSEDED\"])\n .default(\"OPEN\")\n .describe(\"State of pull requests to list\"),\n limit: v\n .number()\n .min(1)\n .max(100)\n .default(10)\n .describe(\"Maximum number of pull requests to return\"),\n }))(),\n execute: async ({ workspace, repoSlug, state, limit }, context) => {\n const userId = requireUserIdFromContext(context);\n\n try {\n const bitbucket = createBitbucketClient(userId);\n const prs = await bitbucket.listPullRequests(workspace, repoSlug, {\n state,\n perPage: limit,\n });\n\n const repository = `${workspace}/${repoSlug}`;\n\n return {\n pullRequests: prs.map((pr: PullRequest) => ({\n id: pr.id,\n title: pr.title,\n state: pr.state,\n author: {\n username: pr.author.username,\n displayName: pr.author.display_name,\n },\n url: pr.links.html.href,\n sourceBranch: pr.source.branch.name,\n destinationBranch: pr.destination.branch.name,\n commentCount: pr.comment_count,\n taskCount: pr.task_count,\n createdOn: pr.created_on,\n updatedOn: pr.updated_on,\n })),\n count: prs.length,\n repository,\n message: `Found ${prs.length} ${state} pull request(s) in ${repository}.`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Bitbucket not connected. Please connect your Bitbucket account.\",\n connectUrl: \"/api/auth/bitbucket\",\n };\n }\n throw error;\n }\n },\n});\n",
205
- "tools/list-repositories.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createBitbucketClient } from \"../lib/bitbucket-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\ntype BitbucketRepo = {\n name: string;\n full_name: string;\n description: string | null;\n is_private: boolean;\n mainbranch: { name: string } | null;\n language: string;\n updated_on: string;\n created_on: string;\n links: { html: { href: string } };\n owner: { username: string; display_name: string };\n};\n\nexport default tool({\n id: \"bitbucket-list-repositories\",\n description: \"List Bitbucket repositories for the authenticated user\",\n inputSchema: defineSchema((v) => v.object({\n role: v\n .enum([\"owner\", \"contributor\", \"member\"])\n .optional()\n .describe(\"Filter repositories by role\"),\n limit: v\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of repositories to return\"),\n }))(),\n execute: async ({ role, limit }, context) => {\n const userId = requireUserIdFromContext(context);\n\n try {\n const bitbucket = createBitbucketClient(userId);\n const repos = await bitbucket.listRepositories({ role, perPage: limit });\n\n return {\n repositories: repos.map((repo: BitbucketRepo) => ({\n name: repo.name,\n fullName: repo.full_name,\n description: repo.description ?? null,\n isPrivate: repo.is_private,\n mainBranch: repo.mainbranch?.name ?? null,\n language: repo.language,\n url: repo.links.html.href,\n owner: {\n username: repo.owner.username,\n displayName: repo.owner.display_name,\n },\n updatedOn: repo.updated_on,\n createdOn: repo.created_on,\n })),\n count: repos.length,\n message: `Found ${repos.length} repository(s).`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Bitbucket not connected. Please connect your Bitbucket account.\",\n connectUrl: \"/api/auth/bitbucket\",\n };\n }\n throw error;\n }\n },\n});\n"
206
- }
207
- },
208
- "integration:calendar": {
209
- "files": {
210
- ".env.example": "# =============================================================================\n# Google Calendar Integration Setup\n# =============================================================================\n#\n# STEP 1: Create a Google Cloud Project\n# Visit: https://console.cloud.google.com/projectcreate\n#\n# STEP 2: Enable the Google Calendar API\n# Visit: https://console.cloud.google.com/apis/library/calendar-json.googleapis.com\n# Click \"Enable\" to activate the Calendar API for your project\n#\n# STEP 3: Configure OAuth Consent Screen\n# Visit: https://console.cloud.google.com/apis/credentials/consent\n# - Choose \"External\" user type (or \"Internal\" for Workspace)\n# - Fill in app name, support email\n# - Add scopes: calendar.readonly, calendar.events\n# - Add your email as a test user (required for development)\n#\n# STEP 4: Create OAuth Credentials\n# Visit: https://console.cloud.google.com/apis/credentials\n# - Click \"Create Credentials\" > \"OAuth client ID\"\n# - Application type: \"Web application\"\n# - Add Authorized redirect URI: http://localhost:3000/api/auth/calendar/callback\n# - Copy the Client ID and Client Secret below\n#\n# =============================================================================\n\nGOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com\nGOOGLE_CLIENT_SECRET=your-client-secret\n",
211
- "app/api/auth/calendar/callback/route.ts": "import { createOAuthCallbackHandler, calendarConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\n\nexport const GET = createOAuthCallbackHandler(calendarConfig, { tokenStore });\n",
212
- "app/api/auth/calendar/route.ts": "import { calendarConfig, createOAuthInitHandler } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../lib/token-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../lib/user-id.ts\";\n\nexport const GET = createOAuthInitHandler(calendarConfig, {\n tokenStore,\n getUserId: requireUserIdFromRequest,\n});\n",
213
- "lib/calendar-client.ts": "/**\n * Google Calendar API Client\n *\n * Provides a type-safe interface to Google Calendar API operations.\n */\n\nimport { getValidToken } from \"./oauth.ts\";\n\nfunction getEnv(key: string): string | undefined {\n // @ts-ignore - Deno global\n if (typeof Deno !== \"undefined\") {\n // @ts-ignore - Deno global\n return Deno.env.get(key);\n }\n\n // @ts-ignore - process global\n if (typeof process !== \"undefined\" && process.env) {\n // @ts-ignore - process global\n return process.env[key];\n }\n\n return undefined;\n}\n\nconst CALENDAR_API_BASE = \"https://www.googleapis.com/calendar/v3\";\n\nexport interface CalendarEvent {\n id: string;\n summary: string;\n description?: string;\n location?: string;\n start: {\n dateTime?: string;\n date?: string;\n timeZone?: string;\n };\n end: {\n dateTime?: string;\n date?: string;\n timeZone?: string;\n };\n attendees?: Array<{\n email: string;\n responseStatus: \"needsAction\" | \"declined\" | \"tentative\" | \"accepted\";\n displayName?: string;\n }>;\n htmlLink: string;\n status: \"confirmed\" | \"tentative\" | \"cancelled\";\n organizer?: { email: string; displayName?: string };\n}\n\nexport interface CreateEventOptions {\n summary: string;\n description?: string;\n location?: string;\n start: Date | string;\n end: Date | string;\n attendees?: string[];\n timeZone?: string;\n}\n\nexport interface UpdateEventOptions {\n summary?: string;\n description?: string;\n location?: string;\n start?: Date | string;\n end?: Date | string;\n attendees?: string[];\n timeZone?: string;\n}\n\nexport interface FreeBusySlot {\n start: string;\n end: string;\n}\n\n/**\n * Google Calendar OAuth provider configuration\n */\nexport const calendarOAuthProvider = {\n name: \"calendar\",\n authorizationUrl: \"https://accounts.google.com/o/oauth2/v2/auth\",\n tokenUrl: \"https://oauth2.googleapis.com/token\",\n clientId: getEnv(\"GOOGLE_CLIENT_ID\") ?? \"\",\n clientSecret: getEnv(\"GOOGLE_CLIENT_SECRET\") ?? \"\",\n scopes: [\n \"https://www.googleapis.com/auth/calendar.readonly\",\n \"https://www.googleapis.com/auth/calendar.events\",\n ],\n callbackPath: \"/api/auth/calendar/callback\",\n};\n\ntype ListEventsOptions = {\n maxResults?: number;\n timeMin?: Date | string;\n timeMax?: Date | string;\n calendarId?: string;\n};\n\ntype FreeBusyOptions = {\n timeMin: Date | string;\n timeMax: Date | string;\n calendarId?: string;\n};\n\ntype FindFreeSlotsOptions = FreeBusyOptions & {\n durationMinutes: number;\n};\n\ntype CalendarClientShape = {\n listEvents(options?: ListEventsOptions): Promise<CalendarEvent[]>;\n getTodayEvents(): Promise<CalendarEvent[]>;\n createEvent(\n options: CreateEventOptions,\n calendarId?: string,\n ): Promise<CalendarEvent>;\n updateEvent(\n eventId: string,\n options: UpdateEventOptions,\n calendarId?: string,\n ): Promise<CalendarEvent>;\n getFreeBusy(options: FreeBusyOptions): Promise<FreeBusySlot[]>;\n findFreeSlots(\n options: FindFreeSlotsOptions,\n ): Promise<Array<{ start: Date; end: Date }>>;\n deleteEvent(eventId: string, calendarId?: string): Promise<void>;\n};\n\n/**\n * Create a Calendar client for a specific user\n */\nexport function createCalendarClient(userId: string): CalendarClientShape {\n async function getAccessToken(): Promise<string> {\n const token = await getValidToken(\n calendarOAuthProvider,\n userId,\n \"calendar\",\n );\n if (!token) {\n throw new Error(\n \"Calendar not connected. Please connect your Google Calendar first.\",\n );\n }\n return token;\n }\n\n async function apiRequest<T>(\n endpoint: string,\n options: RequestInit = {},\n ): Promise<T> {\n const accessToken = await getAccessToken();\n\n const response = await fetch(`${CALENDAR_API_BASE}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${accessToken}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Calendar API error: ${response.status} - ${error}`);\n }\n\n return response.json();\n }\n\n async function listEvents(\n options: ListEventsOptions = {},\n ): Promise<CalendarEvent[]> {\n const params = new URLSearchParams();\n\n const timeMin = options.timeMin ? new Date(options.timeMin) : new Date();\n params.set(\"timeMin\", timeMin.toISOString());\n\n if (options.timeMax) {\n params.set(\"timeMax\", new Date(options.timeMax).toISOString());\n }\n\n params.set(\"maxResults\", String(options.maxResults ?? 10));\n params.set(\"singleEvents\", \"true\");\n params.set(\"orderBy\", \"startTime\");\n\n const calendarId = encodeURIComponent(options.calendarId ?? \"primary\");\n const result = await apiRequest<{ items: CalendarEvent[] }>(\n `/calendars/${calendarId}/events?${params.toString()}`,\n );\n\n return result.items ?? [];\n }\n\n function getTodayEvents(): Promise<CalendarEvent[]> {\n const today = new Date();\n today.setHours(0, 0, 0, 0);\n\n const tomorrow = new Date(today);\n tomorrow.setDate(tomorrow.getDate() + 1);\n\n return listEvents({ timeMin: today, timeMax: tomorrow, maxResults: 50 });\n }\n\n function createEvent(\n options: CreateEventOptions,\n calendarId = \"primary\",\n ): Promise<CalendarEvent> {\n const startDate = typeof options.start === \"string\"\n ? options.start\n : options.start.toISOString();\n const endDate = typeof options.end === \"string\"\n ? options.end\n : options.end.toISOString();\n const timeZone = options.timeZone ?? \"UTC\";\n\n const event = {\n summary: options.summary,\n description: options.description,\n location: options.location,\n start: { dateTime: startDate, timeZone },\n end: { dateTime: endDate, timeZone },\n attendees: options.attendees?.map((email) => ({ email })),\n };\n\n return apiRequest<CalendarEvent>(\n `/calendars/${encodeURIComponent(calendarId)}/events`,\n {\n method: \"POST\",\n body: JSON.stringify(event),\n },\n );\n }\n\n function updateEvent(\n eventId: string,\n options: UpdateEventOptions,\n calendarId = \"primary\",\n ): Promise<CalendarEvent> {\n const timeZone = options.timeZone ?? \"UTC\";\n const event: Record<string, unknown> = {};\n\n if (options.summary !== undefined) event.summary = options.summary;\n if (options.description !== undefined) {\n event.description = options.description;\n }\n if (options.location !== undefined) event.location = options.location;\n if (options.start !== undefined) {\n const startDate = typeof options.start === \"string\"\n ? options.start\n : options.start.toISOString();\n event.start = { dateTime: startDate, timeZone };\n }\n if (options.end !== undefined) {\n const endDate = typeof options.end === \"string\"\n ? options.end\n : options.end.toISOString();\n event.end = { dateTime: endDate, timeZone };\n }\n if (options.attendees !== undefined) {\n event.attendees = options.attendees.map((email) => ({ email }));\n }\n\n return apiRequest<CalendarEvent>(\n `/calendars/${encodeURIComponent(calendarId)}/events/${\n encodeURIComponent(eventId)\n }?sendUpdates=none`,\n {\n method: \"PATCH\",\n body: JSON.stringify(event),\n },\n );\n }\n\n async function getFreeBusy(\n options: FreeBusyOptions,\n ): Promise<FreeBusySlot[]> {\n const calendarId = options.calendarId ?? \"primary\";\n\n const result = await apiRequest<{\n calendars: Record<string, { busy: FreeBusySlot[] }>;\n }>(\"/freeBusy\", {\n method: \"POST\",\n body: JSON.stringify({\n timeMin: new Date(options.timeMin).toISOString(),\n timeMax: new Date(options.timeMax).toISOString(),\n items: [{ id: calendarId }],\n }),\n });\n\n return result.calendars[calendarId]?.busy ?? [];\n }\n\n async function findFreeSlots(\n options: FindFreeSlotsOptions,\n ): Promise<Array<{ start: Date; end: Date }>> {\n const busySlots = await getFreeBusy(options);\n\n const freeSlots: Array<{ start: Date; end: Date }> = [];\n const rangeStart = new Date(options.timeMin);\n const rangeEnd = new Date(options.timeMax);\n const durationMs = options.durationMinutes * 60 * 1000;\n\n let currentStart = rangeStart;\n\n const sortedBusy = busySlots\n .map((s) => ({ start: new Date(s.start), end: new Date(s.end) }))\n .sort((a, b) => a.start.getTime() - b.start.getTime());\n\n for (const busy of sortedBusy) {\n if (busy.start.getTime() - currentStart.getTime() >= durationMs) {\n freeSlots.push({\n start: new Date(currentStart),\n end: new Date(busy.start),\n });\n }\n\n if (busy.end > currentStart) {\n currentStart = busy.end;\n }\n }\n\n if (rangeEnd.getTime() - currentStart.getTime() >= durationMs) {\n freeSlots.push({ start: new Date(currentStart), end: rangeEnd });\n }\n\n return freeSlots;\n }\n\n async function deleteEvent(\n eventId: string,\n calendarId = \"primary\",\n ): Promise<void> {\n const accessToken = await getAccessToken();\n\n const response = await fetch(\n `${CALENDAR_API_BASE}/calendars/${\n encodeURIComponent(calendarId)\n }/events/${eventId}`,\n {\n method: \"DELETE\",\n headers: { Authorization: `Bearer ${accessToken}` },\n },\n );\n\n if (!response.ok && response.status !== 204) {\n throw new Error(`Failed to delete event: ${response.status}`);\n }\n }\n\n return {\n listEvents,\n getTodayEvents,\n createEvent,\n updateEvent,\n getFreeBusy,\n findFreeSlots,\n deleteEvent,\n };\n}\n\nexport type CalendarClient = ReturnType<typeof createCalendarClient>;\n",
214
- "tools/create-event.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createCalendarClient } from \"../lib/calendar-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"calendar-create-event\",\n description: \"Create a new event in Google Calendar\",\n inputSchema: defineSchema((v) => v.object({\n title: v.string().min(1).describe(\"Event title\"),\n startTime: v\n .string()\n .describe(\"Start time in ISO 8601 format (e.g., '2024-01-15T09:00:00')\"),\n endTime: v\n .string()\n .describe(\"End time in ISO 8601 format (e.g., '2024-01-15T10:00:00')\"),\n description: v.string().optional().describe(\"Event description\"),\n location: v.string().optional().describe(\"Event location\"),\n attendees: v\n .array(v.string().email())\n .optional()\n .describe(\"Email addresses of attendees to invite\"),\n timeZone: v\n .string()\n .default(\"UTC\")\n .describe(\"Time zone for the event (e.g., 'America/New_York')\"),\n }))(),\n execute: async (\n { title, startTime, endTime, description, location, attendees, timeZone },\n context,\n ) => {\n const userId = requireUserIdFromContext(context);\n\n try {\n const calendar = createCalendarClient(userId);\n const event = await calendar.createEvent({\n summary: title,\n start: startTime,\n end: endTime,\n description,\n location,\n attendees,\n timeZone,\n });\n\n return {\n success: true,\n event: {\n id: event.id,\n title: event.summary,\n start: event.start.dateTime ?? event.start.date,\n end: event.end.dateTime ?? event.end.date,\n url: event.htmlLink,\n location: event.location,\n attendees: event.attendees?.map((a: { email: string }) => a.email) ?? [],\n },\n message: `Event \"${title}\" created successfully.`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Calendar not connected. Please connect your Google Calendar.\",\n connectUrl: \"/api/auth/calendar\",\n };\n }\n throw error;\n }\n },\n});\n",
215
- "tools/delete-event.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createCalendarClient } from \"../lib/calendar-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"calendar-delete-event\",\n description: \"Delete a Google Calendar event by ID\",\n inputSchema: defineSchema((v) =>\n v.object({\n eventId: v.string().min(1).describe(\"Event ID to delete\"),\n calendarId: v.string().default(\"primary\").describe(\"Calendar ID\"),\n })\n )(),\n execute: async ({ eventId, calendarId }, context) => {\n const userId = requireUserIdFromContext(context);\n const calendar = createCalendarClient(userId);\n await calendar.deleteEvent(eventId, calendarId);\n\n return {\n success: true,\n eventId,\n message: `Event ${eventId} deleted successfully.`,\n };\n },\n});\n",
216
- "tools/find-free-time.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createCalendarClient } from \"../lib/calendar-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\ntype FreeSlot = { start: Date; end: Date };\n\nexport default tool({\n id: \"calendar-find-free-time\",\n description: \"Find available time slots in the calendar for scheduling\",\n inputSchema: defineSchema((v) => v.object({\n durationMinutes: v\n .number()\n .min(15)\n .max(480)\n .default(60)\n .describe(\"Duration needed in minutes\"),\n daysToSearch: v\n .number()\n .min(1)\n .max(14)\n .default(7)\n .describe(\"Number of days to search ahead\"),\n workingHoursOnly: v\n .boolean()\n .default(true)\n .describe(\"Only show slots during working hours (9 AM - 6 PM)\"),\n }))(),\n execute: async (\n { durationMinutes, daysToSearch, workingHoursOnly },\n context,\n ): Promise<unknown> => {\n const userId = requireUserIdFromContext(context);\n\n try {\n const calendar = createCalendarClient(userId);\n\n const now = new Date();\n const searchEnd = new Date();\n searchEnd.setDate(searchEnd.getDate() + daysToSearch);\n\n const freeSlots = (await calendar.findFreeSlots({\n timeMin: now,\n timeMax: searchEnd,\n durationMinutes,\n })) as FreeSlot[];\n\n const slots = workingHoursOnly\n ? freeSlots.filter(({ start, end }) => {\n const startHour = start.getHours();\n const endHour = end.getHours();\n return startHour >= 9 && endHour <= 18;\n })\n : freeSlots;\n\n const formattedSlots = slots.slice(0, 10).map(({ start, end }) => {\n const duration = Math.round((end.getTime() - start.getTime()) / 60000);\n\n return {\n start: start.toISOString(),\n end: end.toISOString(),\n durationMinutes: duration,\n date: start.toLocaleDateString(\"en-US\", {\n weekday: \"long\",\n month: \"short\",\n day: \"numeric\",\n }),\n timeRange: `${start.toLocaleTimeString(\"en-US\", {\n hour: \"numeric\",\n minute: \"2-digit\",\n })} - ${end.toLocaleTimeString(\"en-US\", {\n hour: \"numeric\",\n minute: \"2-digit\",\n })}`,\n };\n });\n\n const count = formattedSlots.length;\n\n return {\n freeSlots: formattedSlots,\n count,\n searchCriteria: {\n durationMinutes,\n daysToSearch,\n workingHoursOnly,\n },\n message:\n count > 0\n ? `Found ${count} available slot(s) of ${durationMinutes} minutes or more.`\n : `No free slots of ${durationMinutes} minutes found in the next ${daysToSearch} days.`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Calendar not connected. Please connect your Google Calendar.\",\n connectUrl: \"/api/auth/calendar\",\n };\n }\n throw error;\n }\n },\n});\n",
217
- "tools/list-events.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createCalendarClient } from \"../lib/calendar-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\ntype CalendarEvent = {\n id: string;\n summary: string;\n description?: string;\n location?: string;\n start: { dateTime?: string; date?: string };\n end: { dateTime?: string; date?: string };\n status: string;\n htmlLink: string;\n attendees?: Array<{ email: string; displayName?: string; responseStatus?: string }>;\n};\n\nexport default tool({\n id: \"calendar-list-events\",\n description: \"List upcoming calendar events. By default shows events from now onwards.\",\n inputSchema: defineSchema((v) => v.object({\n maxResults: v\n .number()\n .min(1)\n .max(100)\n .default(10)\n .describe(\"Maximum number of events to return\"),\n daysAhead: v.number().min(1).max(30).default(7).describe(\"Number of days to look ahead\"),\n todayOnly: v.boolean().default(false).describe(\"Only show events for today\"),\n }))(),\n execute: async ({ maxResults, daysAhead, todayOnly }, context) => {\n const userId = requireUserIdFromContext(context);\n\n try {\n const calendar = createCalendarClient(userId);\n\n const events = todayOnly\n ? ((await calendar.getTodayEvents()) as CalendarEvent[])\n : ((await calendar.listEvents({\n maxResults,\n timeMin: new Date(),\n timeMax: new Date(Date.now() + daysAhead * 24 * 60 * 60 * 1000),\n })) as CalendarEvent[]);\n\n return {\n events: events.map((event) => ({\n id: event.id,\n title: event.summary,\n description: event.description ?? null,\n location: event.location ?? null,\n start: event.start.dateTime || event.start.date,\n end: event.end.dateTime || event.end.date,\n isAllDay: !event.start.dateTime,\n status: event.status,\n url: event.htmlLink,\n attendees:\n event.attendees?.map((a) => ({\n email: a.email,\n name: a.displayName,\n status: a.responseStatus,\n })) ?? [],\n })),\n count: events.length,\n message: todayOnly\n ? `Found ${events.length} event(s) for today.`\n : `Found ${events.length} event(s) in the next ${daysAhead} days.`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Calendar not connected. Please connect your Google Calendar.\",\n connectUrl: \"/api/auth/calendar\",\n };\n }\n throw error;\n }\n },\n});\n",
218
- "tools/update-event.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createCalendarClient } from \"../lib/calendar-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"calendar-update-event\",\n description: \"Update an existing Google Calendar event by ID\",\n inputSchema: defineSchema((v) =>\n v.object({\n eventId: v.string().min(1).describe(\"Event ID to update\"),\n calendarId: v.string().default(\"primary\").describe(\"Calendar ID\"),\n title: v.string().optional().describe(\"Updated event title\"),\n startTime: v.string().optional().describe(\n \"Updated start time in ISO 8601 format\",\n ),\n endTime: v.string().optional().describe(\n \"Updated end time in ISO 8601 format\",\n ),\n description: v.string().optional().describe(\"Updated event description\"),\n location: v.string().optional().describe(\"Updated event location\"),\n attendees: v.array(v.string().email()).optional().describe(\n \"Updated attendee email addresses\",\n ),\n timeZone: v.string().default(\"UTC\").describe(\n \"Time zone for updated start/end values\",\n ),\n })\n )(),\n execute: async (\n {\n eventId,\n calendarId,\n title,\n startTime,\n endTime,\n description,\n location,\n attendees,\n timeZone,\n },\n context,\n ) => {\n const userId = requireUserIdFromContext(context);\n const calendar = createCalendarClient(userId);\n const event = await calendar.updateEvent(\n eventId,\n {\n summary: title,\n start: startTime,\n end: endTime,\n description,\n location,\n attendees,\n timeZone,\n },\n calendarId,\n );\n\n return {\n success: true,\n event: {\n id: event.id,\n title: event.summary,\n start: event.start.dateTime ?? event.start.date,\n end: event.end.dateTime ?? event.end.date,\n url: event.htmlLink,\n location: event.location,\n },\n message: `Event \"${event.summary}\" updated successfully.`,\n };\n },\n});\n"
219
- }
220
- },
221
- "integration:confluence": {
222
- "files": {
223
- ".env.example": "# Atlassian OAuth credentials\n# Get these from: https://developer.atlassian.com/console/myapps/\nATLASSIAN_CLIENT_ID=your_client_id_here\nATLASSIAN_CLIENT_SECRET=your_client_secret_here\n",
224
- "app/api/auth/confluence/callback/route.ts": "import { createOAuthCallbackHandler, confluenceConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\n\nexport const GET = createOAuthCallbackHandler(confluenceConfig, { tokenStore });\n",
225
- "app/api/auth/confluence/route.ts": "import { confluenceConfig, createOAuthInitHandler } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../lib/token-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../lib/user-id.ts\";\n\nexport const GET = createOAuthInitHandler(confluenceConfig, {\n tokenStore,\n getUserId: requireUserIdFromRequest,\n});\n",
226
- "lib/confluence-client.ts": "import { getAccessToken, getCloudId } from \"./token-store.ts\";\n\nconst CONFLUENCE_API_BASE = \"https://api.atlassian.com/ex/confluence\";\n\ninterface ConfluenceResponse<T> {\n results: T[];\n size?: number;\n start?: number;\n limit?: number;\n _links?: {\n next?: string;\n base?: string;\n };\n}\n\nexport interface ConfluenceSpace {\n id: string;\n key: string;\n name: string;\n type: string;\n status: string;\n _links: {\n webui: string;\n };\n}\n\nexport type ConfluencePageType = \"page\" | \"blogpost\";\n\nexport interface ConfluencePage {\n id: string;\n type?: ConfluencePageType;\n status: string;\n title: string;\n spaceId?: string;\n parentId?: string;\n version: {\n number: number;\n message?: string;\n };\n body?: {\n storage?: {\n value: string;\n representation: \"storage\";\n };\n view?: {\n value: string;\n representation: \"view\";\n };\n };\n _links: {\n webui: string;\n tinyui?: string;\n };\n}\n\nexport interface ConfluenceSearchResult {\n content: {\n id: string;\n type: string;\n status: string;\n title: string;\n space?: {\n id: string;\n key: string;\n name: string;\n };\n history?: {\n lastUpdated: {\n when: string;\n };\n };\n _links: {\n webui: string;\n };\n };\n excerpt?: string;\n url: string;\n resultGlobalContainer?: {\n title: string;\n };\n}\n\nexport class ConfluenceApiError extends Error {\n constructor(public readonly status: number, message: string) {\n super(message);\n this.name = \"ConfluenceApiError\";\n }\n}\n\nasync function confluenceFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const [token, cloudId] = await Promise.all([getAccessToken(), getCloudId()]);\n\n if (!token || !cloudId) {\n throw new Error(\"Not authenticated with Confluence. Please connect your Atlassian account.\");\n }\n\n const url = `${CONFLUENCE_API_BASE}/${cloudId}${endpoint}`;\n\n const response = await fetch(url, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({}))) as { message?: string };\n throw new ConfluenceApiError(\n response.status,\n `Confluence API error: ${response.status} ${error.message ?? response.statusText}`,\n );\n }\n\n return response.json() as Promise<T>;\n}\n\nfunction buildEndpoint(path: string, params?: URLSearchParams): string {\n const query = params?.toString();\n return `${path}${query ? `?${query}` : \"\"}`;\n}\n\n// Uses Confluence v2 — v1 /wiki/rest/api/space is deprecated alongside /content.\nexport async function listSpaces(options?: {\n limit?: number;\n type?: \"global\" | \"personal\";\n}): Promise<ConfluenceSpace[]> {\n const params = new URLSearchParams();\n\n if (options?.limit) params.set(\"limit\", options.limit.toString());\n if (options?.type) params.set(\"type\", options.type);\n\n const response = await confluenceFetch<ConfluenceResponse<ConfluenceSpace>>(\n buildEndpoint(\"/wiki/api/v2/spaces\", params),\n );\n\n return response.results ?? [];\n}\n\n// Direct key lookup via v2 — avoids the v1 enumeration trap that capped at 250 spaces\n// and silently failed on enterprise tenancies with hundreds of spaces.\nasync function getSpaceIdByKey(spaceKey: string): Promise<string> {\n const params = new URLSearchParams();\n params.set(\"keys\", spaceKey);\n params.set(\"limit\", \"1\");\n\n const response = await confluenceFetch<ConfluenceResponse<ConfluenceSpace>>(\n buildEndpoint(\"/wiki/api/v2/spaces\", params),\n );\n\n const space = response.results?.[0];\n if (!space) {\n throw new Error(`Confluence space not found: ${spaceKey}`);\n }\n return space.id;\n}\n\nexport async function searchContent(\n query: string,\n options?: {\n cql?: string;\n limit?: number;\n spaceKey?: string;\n },\n): Promise<ConfluenceSearchResult[]> {\n const params = new URLSearchParams();\n\n let cqlQuery = options?.cql ?? `title ~ \"${query}\" OR text ~ \"${query}\"`;\n if (options?.spaceKey) cqlQuery += ` AND space = \"${options.spaceKey}\"`;\n\n params.set(\"cql\", cqlQuery);\n if (options?.limit) params.set(\"limit\", options.limit.toString());\n\n const response = await confluenceFetch<ConfluenceResponse<ConfluenceSearchResult>>(\n buildEndpoint(\"/wiki/rest/api/search\", params),\n );\n\n return response.results ?? [];\n}\n\n// v2 splits pages and blogposts into separate resources. Try /pages first;\n// fall back to /blogposts on 404 so search-content → get-page works for both\n// (searchContent returns mixed results and tools/get-page.ts has no type discriminator).\nexport async function getPage(pageId: string): Promise<ConfluencePage> {\n try {\n return await confluenceFetch<ConfluencePage>(\n `/wiki/api/v2/pages/${pageId}?body-format=storage`,\n );\n } catch (error) {\n if (error instanceof ConfluenceApiError && error.status === 404) {\n return await confluenceFetch<ConfluencePage>(\n `/wiki/api/v2/blogposts/${pageId}?body-format=storage`,\n );\n }\n throw error;\n }\n}\n\nexport function getPageContent(pageId: string): Promise<ConfluencePage> {\n return getPage(pageId);\n}\n\nexport async function createPage(options: {\n spaceKey: string;\n title: string;\n content: string;\n parentId?: string;\n type?: ConfluencePageType;\n}): Promise<ConfluencePage> {\n const spaceId = await getSpaceIdByKey(options.spaceKey);\n const type: ConfluencePageType = options.type ?? \"page\";\n\n const body: Record<string, unknown> = {\n spaceId,\n title: options.title,\n status: \"current\",\n body: {\n representation: \"storage\",\n value: options.content,\n },\n };\n\n if (type === \"blogpost\") {\n // v2 blogposts cannot have a parent — surface the user error instead of dropping it silently.\n if (options.parentId) {\n throw new Error(\"Confluence blogposts cannot have a parentId\");\n }\n return confluenceFetch<ConfluencePage>(\"/wiki/api/v2/blogposts\", {\n method: \"POST\",\n body: JSON.stringify(body),\n });\n }\n\n if (options.parentId) body.parentId = options.parentId;\n\n return confluenceFetch<ConfluencePage>(\"/wiki/api/v2/pages\", {\n method: \"POST\",\n body: JSON.stringify(body),\n });\n}\n\n// v2 PUT /pages/{id} is a full replace, not PATCH — title and body are both required.\n// Callers must resolve fallbacks (e.g. from a prior getPage) before invoking this.\n// Mirrors getPage's pages-then-blogposts fallback so update-page works on either resource\n// (createPage with type='blogpost' returns a blogpost id that this must accept).\nexport async function updatePage(\n pageId: string,\n options: {\n title: string;\n content: string;\n version: number;\n versionMessage?: string;\n },\n): Promise<ConfluencePage> {\n const body = {\n id: pageId,\n status: \"current\",\n title: options.title,\n body: {\n representation: \"storage\",\n value: options.content,\n },\n version: {\n number: options.version,\n message: options.versionMessage,\n },\n };\n\n const init: RequestInit = {\n method: \"PUT\",\n body: JSON.stringify(body),\n };\n\n try {\n return await confluenceFetch<ConfluencePage>(`/wiki/api/v2/pages/${pageId}`, init);\n } catch (error) {\n if (error instanceof ConfluenceApiError && error.status === 404) {\n return await confluenceFetch<ConfluencePage>(`/wiki/api/v2/blogposts/${pageId}`, init);\n }\n throw error;\n }\n}\n\nexport function extractPlainText(storageHtml: string): string {\n return storageHtml\n .replace(/<[^>]*>/g, \" \")\n .replace(/&nbsp;/g, \" \")\n .replace(/&amp;/g, \"&\")\n .replace(/&lt;/g, \"<\")\n .replace(/&gt;/g, \">\")\n .replace(/&quot;/g, '\"')\n .replace(/&#39;/g, \"'\")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nexport function formatAsStorage(text: string): string {\n const paragraphs = text.split(\"\\n\\n\").filter((p) => p.trim());\n return paragraphs.map((p) => `<p>${escapeHtml(p.trim())}</p>`).join(\"\\n\");\n}\n\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#39;\");\n}\n",
227
- "tools/create-page.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createPage, formatAsStorage } from \"../lib/confluence-client.ts\";\n\nexport default tool({\n id: \"confluence-create-page\",\n description:\n \"Create a new page in a Confluence space. Can optionally be created as a child of an existing page.\",\n inputSchema: defineSchema((v) => v.object({\n spaceKey: v\n .string()\n .describe('The key of the space to create the page in (e.g., \"TEAM\", \"DEV\")'),\n title: v.string().describe(\"Title of the new page\"),\n content: v\n .string()\n .describe(\n \"Content for the page (can be plain text or Confluence storage format HTML)\",\n ),\n parentId: v\n .string()\n .optional()\n .describe(\"Optional ID of the parent page to create this as a child page\"),\n type: v\n .enum([\"page\", \"blogpost\"])\n .default(\"page\")\n .describe(\"Type of content to create\"),\n }))(),\n async execute({ spaceKey, title, content, parentId, type }) {\n const trimmedContent = content.trim();\n const storageContent = trimmedContent.startsWith(\"<\")\n ? trimmedContent\n : formatAsStorage(trimmedContent);\n\n const page = await createPage({\n spaceKey,\n title,\n content: storageContent,\n parentId,\n type,\n });\n\n return {\n id: page.id,\n title: page.title,\n type: page.type ?? \"page\",\n url: page._links.webui,\n version: page.version.number,\n spaceId: page.spaceId,\n };\n },\n});\n",
228
- "tools/get-page.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { extractPlainText, getPageContent } from \"../lib/confluence-client.ts\";\n\nexport default tool({\n id: \"confluence-get-page\",\n description:\n \"Get the content of a specific Confluence page. Returns the page title, content, and metadata.\",\n inputSchema: defineSchema((v) => v.object({\n pageId: v.string().describe(\"The ID of the Confluence page to retrieve\"),\n }))(),\n async execute({ pageId }) {\n const page = await getPageContent(pageId);\n\n const htmlContent = page.body?.storage?.value ?? page.body?.view?.value ?? \"\";\n const content = extractPlainText(htmlContent);\n\n return {\n id: page.id,\n type: page.type ?? \"page\",\n title: page.title,\n content,\n htmlContent,\n version: page.version.number,\n url: page._links.webui,\n spaceId: page.spaceId,\n parentId: page.parentId,\n };\n },\n});\n",
229
- "tools/list-spaces.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { listSpaces } from \"../lib/confluence-client.ts\";\n\nexport default tool({\n id: \"confluence-list-spaces\",\n description: \"List all accessible Confluence spaces. Returns space keys, names, and links.\",\n inputSchema: defineSchema((v) => v.object({\n type: v\n .enum([\"global\", \"personal\", \"all\"])\n .default(\"all\")\n .describe(\"Type of spaces to list (global, personal, or all)\"),\n limit: v\n .number()\n .min(1)\n .max(100)\n .default(25)\n .describe(\"Maximum number of spaces to return\"),\n }))(),\n async execute({ type, limit }) {\n const spaces = await listSpaces({\n type: type === \"all\" ? undefined : type,\n limit,\n });\n\n return spaces.map(({ id, key, name, type, status, _links }) => ({\n id,\n key,\n name,\n type,\n status,\n url: _links.webui,\n }));\n },\n});\n",
230
- "tools/search-content.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { searchContent } from \"../lib/confluence-client.ts\";\n\nexport default tool({\n id: \"confluence-search-content\",\n description:\n \"Search for pages and blog posts in Confluence. Returns matching content with titles, excerpts, and links.\",\n inputSchema: defineSchema((v) => v.object({\n query: v.string().describe(\"Search query to find pages or blog posts\"),\n spaceKey: v\n .string()\n .optional()\n .describe(\"Optional space key to limit search to a specific space\"),\n limit: v\n .number()\n .min(1)\n .max(50)\n .default(10)\n .describe(\"Maximum number of results to return\"),\n }))(),\n async execute({ query, spaceKey, limit }) {\n const results = await searchContent(query, { spaceKey, limit });\n\n return results.map((result) => {\n const { content, excerpt, url } = result;\n const space = content.space;\n\n return {\n id: content.id,\n type: content.type,\n title: content.title,\n excerpt,\n url,\n space: space\n ? {\n id: space.id,\n key: space.key,\n name: space.name,\n }\n : undefined,\n lastUpdated: content.history?.lastUpdated.when,\n };\n });\n },\n});\n",
231
- "tools/update-page.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { formatAsStorage, getPage, updatePage } from \"../lib/confluence-client.ts\";\n\nfunction toStorageContent(content?: string): string | undefined {\n if (!content || !content.trim()) return undefined;\n\n const trimmed = content.trim();\n if (trimmed.startsWith(\"<\")) return content;\n\n return formatAsStorage(content);\n}\n\nfunction nonEmpty(value: string | undefined): string | undefined {\n return value && value.trim() ? value : undefined;\n}\n\nexport default tool({\n id: \"confluence-update-page\",\n description:\n \"Update the content or title of an existing Confluence page. Requires the current version number.\",\n inputSchema: defineSchema((v) => v.object({\n pageId: v.string().describe(\"The ID of the page to update\"),\n title: v\n .string()\n .optional()\n .describe(\"New title for the page (leave empty to keep current title)\"),\n content: v\n .string()\n .optional()\n .describe(\"New content for the page (can be plain text or Confluence storage format HTML)\"),\n versionMessage: v\n .string()\n .optional()\n .describe(\"Optional message describing the changes made\"),\n }))(),\n async execute({ pageId, title, content, versionMessage }) {\n // v2 PUT /pages/{id} is a full replace — both title and body must be sent on every\n // update. Resolve missing fields from the current page so partial updates work.\n // The schema describes empty values as \"keep current\", so treat empty/whitespace\n // strings as unset (??-fallback would otherwise let \"\" overwrite a real title).\n const currentPage = await getPage(pageId);\n const storageContent = toStorageContent(content);\n const currentBody = currentPage.body?.storage?.value ?? \"\";\n\n const updatedPage = await updatePage(pageId, {\n title: nonEmpty(title) ?? currentPage.title,\n content: storageContent ?? currentBody,\n version: currentPage.version.number + 1,\n versionMessage,\n });\n\n return {\n id: updatedPage.id,\n title: updatedPage.title,\n type: updatedPage.type ?? \"page\",\n url: updatedPage._links.webui,\n version: updatedPage.version.number,\n versionMessage: updatedPage.version.message,\n };\n },\n});\n"
232
- }
233
- },
234
- "integration:docs-google": {
235
- "files": {
236
- ".env.example": "# Google Docs Integration\n# Create OAuth credentials at https://console.cloud.google.com/apis/credentials\n# Make sure to enable:\n# - Google Docs API: https://console.cloud.google.com/apis/library/docs.googleapis.com\n# - Google Drive API: https://console.cloud.google.com/apis/library/drive.googleapis.com\n\nGOOGLE_CLIENT_ID=your_client_id_here\nGOOGLE_CLIENT_SECRET=your_client_secret_here\n",
237
- "app/api/auth/docs-google/callback/route.ts": "import { createOAuthCallbackHandler, docsGoogleConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\n\nexport const GET = createOAuthCallbackHandler(docsGoogleConfig, { tokenStore });\n",
238
- "app/api/auth/docs-google/route.ts": "import { createOAuthInitHandler, docsGoogleConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../lib/token-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../lib/user-id.ts\";\n\nexport const GET = createOAuthInitHandler(docsGoogleConfig, {\n tokenStore,\n getUserId: requireUserIdFromRequest,\n});\n",
239
- "lib/docs-client.ts": "/**\n * Google Docs API Client\n *\n * Provides a type-safe interface to Google Docs API operations.\n */\n\nimport { getValidToken } from \"./docs-google-oauth.ts\";\n\nfunction getEnv(key: string): string | undefined {\n // @ts-ignore - Deno global\n if (typeof Deno !== \"undefined\") return Deno.env.get(key);\n // @ts-ignore - process global\n if (typeof process !== \"undefined\" && process.env) return process.env[key];\n return undefined;\n}\n\nconst DOCS_API_BASE = \"https://docs.googleapis.com/v1\";\nconst DRIVE_API_BASE = \"https://www.googleapis.com/drive/v3\";\n\nexport interface Document {\n documentId: string;\n title: string;\n body: {\n content: StructuralElement[];\n };\n revisionId: string;\n suggestionsViewMode: string;\n documentStyle: DocumentStyle;\n}\n\nexport interface StructuralElement {\n startIndex: number;\n endIndex: number;\n paragraph?: Paragraph;\n table?: Table;\n sectionBreak?: SectionBreak;\n}\n\nexport interface Paragraph {\n elements: ParagraphElement[];\n paragraphStyle?: ParagraphStyle;\n bullet?: Bullet;\n}\n\nexport interface ParagraphElement {\n startIndex: number;\n endIndex: number;\n textRun?: TextRun;\n inlineObjectElement?: InlineObjectElement;\n}\n\nexport interface TextRun {\n content: string;\n textStyle?: TextStyle;\n}\n\nexport interface TextStyle {\n bold?: boolean;\n italic?: boolean;\n underline?: boolean;\n strikethrough?: boolean;\n fontSize?: Dimension;\n foregroundColor?: Color;\n backgroundColor?: Color;\n fontFamily?: string;\n link?: Link;\n}\n\nexport interface Link {\n url?: string;\n bookmarkId?: string;\n headingId?: string;\n}\n\nexport interface Dimension {\n magnitude: number;\n unit: string;\n}\n\nexport interface Color {\n rgbColor?: RgbColor;\n}\n\nexport interface RgbColor {\n red: number;\n green: number;\n blue: number;\n}\n\nexport interface ParagraphStyle {\n headingId?: string;\n namedStyleType?: string;\n alignment?: string;\n lineSpacing?: number;\n direction?: string;\n spacingMode?: string;\n spaceAbove?: Dimension;\n spaceBelow?: Dimension;\n indentFirstLine?: Dimension;\n indentStart?: Dimension;\n indentEnd?: Dimension;\n}\n\nexport interface Bullet {\n listId: string;\n nestingLevel?: number;\n textStyle?: TextStyle;\n}\n\nexport interface Table {\n rows: number;\n columns: number;\n tableRows: TableRow[];\n tableStyle?: TableStyle;\n}\n\nexport interface TableRow {\n startIndex: number;\n endIndex: number;\n tableCells: TableCell[];\n}\n\nexport interface TableCell {\n startIndex: number;\n endIndex: number;\n content: StructuralElement[];\n tableCellStyle?: TableCellStyle;\n}\n\nexport interface TableCellStyle {\n rowSpan?: number;\n columnSpan?: number;\n backgroundColor?: Color;\n borderLeft?: TableCellBorder;\n borderRight?: TableCellBorder;\n borderTop?: TableCellBorder;\n borderBottom?: TableCellBorder;\n paddingLeft?: Dimension;\n paddingRight?: Dimension;\n paddingTop?: Dimension;\n paddingBottom?: Dimension;\n}\n\nexport interface TableCellBorder {\n color?: Color;\n width?: Dimension;\n dashStyle?: string;\n}\n\nexport interface TableStyle {\n tableColumnProperties?: TableColumnProperties[];\n}\n\nexport interface TableColumnProperties {\n width?: Dimension;\n widthType?: string;\n}\n\nexport interface SectionBreak {\n sectionStyle?: SectionStyle;\n}\n\nexport interface SectionStyle {\n columnSeparatorStyle?: string;\n contentDirection?: string;\n marginTop?: Dimension;\n marginBottom?: Dimension;\n marginRight?: Dimension;\n marginLeft?: Dimension;\n pageNumberStart?: number;\n}\n\nexport interface DocumentStyle {\n background?: Background;\n pageNumberStart?: number;\n marginTop?: Dimension;\n marginBottom?: Dimension;\n marginRight?: Dimension;\n marginLeft?: Dimension;\n pageSize?: Size;\n marginHeader?: Dimension;\n marginFooter?: Dimension;\n useFirstPageHeaderFooter?: boolean;\n}\n\nexport interface Background {\n color?: Color;\n}\n\nexport interface Size {\n height?: Dimension;\n width?: Dimension;\n}\n\nexport interface InlineObjectElement {\n inlineObjectId: string;\n textStyle?: TextStyle;\n}\n\nexport interface DocumentFile {\n id: string;\n name: string;\n mimeType: string;\n createdTime: string;\n modifiedTime: string;\n webViewLink: string;\n iconLink?: string;\n thumbnailLink?: string;\n}\n\nexport interface CreateDocumentOptions {\n title: string;\n}\n\nexport interface BatchUpdateRequest {\n requests: Request[];\n}\n\nexport interface Request {\n insertText?: InsertTextRequest;\n deleteContentRange?: DeleteContentRangeRequest;\n replaceAllText?: ReplaceAllTextRequest;\n updateTextStyle?: UpdateTextStyleRequest;\n updateParagraphStyle?: UpdateParagraphStyleRequest;\n insertPageBreak?: InsertPageBreakRequest;\n insertTable?: InsertTableRequest;\n deleteTableRow?: DeleteTableRowRequest;\n deleteTableColumn?: DeleteTableColumnRequest;\n createParagraphBullets?: CreateParagraphBulletsRequest;\n deleteParagraphBullets?: DeleteParagraphBulletsRequest;\n}\n\nexport interface InsertTextRequest {\n text: string;\n location: Location;\n}\n\nexport interface DeleteContentRangeRequest {\n range: Range;\n}\n\nexport interface ReplaceAllTextRequest {\n containsText: ContainsText;\n replaceText: string;\n}\n\nexport interface UpdateTextStyleRequest {\n range: Range;\n textStyle: TextStyle;\n fields: string;\n}\n\nexport interface UpdateParagraphStyleRequest {\n range: Range;\n paragraphStyle: ParagraphStyle;\n fields: string;\n}\n\nexport interface InsertPageBreakRequest {\n location: Location;\n}\n\nexport interface InsertTableRequest {\n rows: number;\n columns: number;\n location: Location;\n}\n\nexport interface DeleteTableRowRequest {\n tableCellLocation: TableCellLocation;\n}\n\nexport interface DeleteTableColumnRequest {\n tableCellLocation: TableCellLocation;\n}\n\nexport interface CreateParagraphBulletsRequest {\n range: Range;\n bulletPreset: string;\n}\n\nexport interface DeleteParagraphBulletsRequest {\n range: Range;\n}\n\nexport interface Location {\n index: number;\n segmentId?: string;\n}\n\nexport interface Range {\n startIndex: number;\n endIndex: number;\n segmentId?: string;\n}\n\nexport interface ContainsText {\n text: string;\n matchCase: boolean;\n}\n\nexport interface TableCellLocation {\n tableStartLocation: Location;\n rowIndex: number;\n columnIndex: number;\n}\n\nexport interface BatchUpdateResponse {\n documentId: string;\n replies: Reply[];\n writeControl?: WriteControl;\n}\n\nexport interface Reply {\n [key: string]: unknown;\n}\n\nexport interface WriteControl {\n requiredRevisionId: string;\n targetRevisionId: string;\n}\n\n/**\n * Google Docs OAuth provider configuration\n */\nexport const docsOAuthProvider = {\n name: \"docs-google\",\n authorizationUrl: \"https://accounts.google.com/o/oauth2/v2/auth\",\n tokenUrl: \"https://oauth2.googleapis.com/token\",\n clientId: getEnv(\"GOOGLE_CLIENT_ID\") ?? \"\",\n clientSecret: getEnv(\"GOOGLE_CLIENT_SECRET\") ?? \"\",\n scopes: [\n \"https://www.googleapis.com/auth/documents.readonly\",\n \"https://www.googleapis.com/auth/documents\",\n \"https://www.googleapis.com/auth/docs\",\n \"https://www.googleapis.com/auth/drive.readonly\",\n ],\n callbackPath: \"/api/auth/docs-google/callback\",\n};\n\nexport function createDocsClient(userId: string): {\n listDocuments(options?: {\n maxResults?: number;\n orderBy?: \"createdTime\" | \"modifiedTime\" | \"name\";\n }): Promise<DocumentFile[]>;\n getDocument(documentId: string): Promise<Document>;\n createDocument(options: CreateDocumentOptions): Promise<Document>;\n updateDocument(documentId: string, requests: Request[]): Promise<BatchUpdateResponse>;\n insertText(documentId: string, text: string, index: number): Promise<BatchUpdateResponse>;\n deleteContent(documentId: string, startIndex: number, endIndex: number): Promise<BatchUpdateResponse>;\n replaceAllText(\n documentId: string,\n searchText: string,\n replaceText: string,\n matchCase?: boolean,\n ): Promise<BatchUpdateResponse>;\n searchDocuments(query: string, maxResults?: number): Promise<DocumentFile[]>;\n extractText(document: Document): string;\n createDocumentWithContent(title: string, content: string): Promise<Document>;\n} {\n async function getAccessToken(): Promise<string> {\n const token = await getValidToken(docsOAuthProvider, userId, \"docs-google\");\n if (!token) throw new Error(\"Google Docs not connected. Please connect your Google account first.\");\n return token;\n }\n\n async function apiRequest<T>(\n baseUrl: string,\n label: string,\n endpoint: string,\n options: RequestInit = {},\n ): Promise<T> {\n const accessToken = await getAccessToken();\n\n const response = await fetch(`${baseUrl}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${accessToken}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`${label} API error: ${response.status} - ${error}`);\n }\n\n return response.json();\n }\n\n function docsApiRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n return apiRequest<T>(DOCS_API_BASE, \"Docs\", endpoint, options);\n }\n\n function driveApiRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n return apiRequest<T>(DRIVE_API_BASE, \"Drive\", endpoint, options);\n }\n\n function extractText(document: Document): string {\n const textParts: string[] = [];\n\n function processElement(element: StructuralElement): void {\n if (element.paragraph) {\n for (const el of element.paragraph.elements) {\n if (el.textRun) textParts.push(el.textRun.content);\n }\n return;\n }\n\n if (!element.table) return;\n\n for (const row of element.table.tableRows) {\n for (const cell of row.tableCells) {\n for (const child of cell.content) processElement(child);\n }\n }\n }\n\n for (const element of document.body.content) processElement(element);\n return textParts.join(\"\");\n }\n\n async function listDocuments(options: {\n maxResults?: number;\n orderBy?: \"createdTime\" | \"modifiedTime\" | \"name\";\n } = {}): Promise<DocumentFile[]> {\n const params = new URLSearchParams({\n q: \"mimeType='application/vnd.google-apps.document' and trashed=false\",\n fields: \"files(id,name,mimeType,createdTime,modifiedTime,webViewLink,iconLink,thumbnailLink)\",\n pageSize: String(options.maxResults ?? 20),\n orderBy: `${options.orderBy ?? \"modifiedTime\"} desc`,\n });\n\n const result = await driveApiRequest<{ files: DocumentFile[] }>(`/files?${params.toString()}`);\n return result.files ?? [];\n }\n\n async function searchDocuments(query: string, maxResults = 20): Promise<DocumentFile[]> {\n const params = new URLSearchParams({\n q: `mimeType='application/vnd.google-apps.document' and trashed=false and fullText contains '${query}'`,\n fields: \"files(id,name,mimeType,createdTime,modifiedTime,webViewLink,iconLink,thumbnailLink)\",\n pageSize: String(maxResults),\n orderBy: \"modifiedTime desc\",\n });\n\n const result = await driveApiRequest<{ files: DocumentFile[] }>(`/files?${params.toString()}`);\n return result.files ?? [];\n }\n\n function getDocument(documentId: string): Promise<Document> {\n return docsApiRequest<Document>(`/documents/${documentId}`);\n }\n\n function createDocument(options: CreateDocumentOptions): Promise<Document> {\n return docsApiRequest<Document>(\"/documents\", {\n method: \"POST\",\n body: JSON.stringify({ title: options.title }),\n });\n }\n\n function updateDocument(documentId: string, requests: Request[]): Promise<BatchUpdateResponse> {\n return docsApiRequest<BatchUpdateResponse>(`/documents/${documentId}:batchUpdate`, {\n method: \"POST\",\n body: JSON.stringify({ requests }),\n });\n }\n\n function insertText(documentId: string, text: string, index: number): Promise<BatchUpdateResponse> {\n return updateDocument(documentId, [\n {\n insertText: {\n text,\n location: { index },\n },\n },\n ]);\n }\n\n function deleteContent(documentId: string, startIndex: number, endIndex: number): Promise<BatchUpdateResponse> {\n return updateDocument(documentId, [\n {\n deleteContentRange: {\n range: { startIndex, endIndex },\n },\n },\n ]);\n }\n\n function replaceAllText(\n documentId: string,\n searchText: string,\n replaceText: string,\n matchCase = false,\n ): Promise<BatchUpdateResponse> {\n return updateDocument(documentId, [\n {\n replaceAllText: {\n containsText: {\n text: searchText,\n matchCase,\n },\n replaceText,\n },\n },\n ]);\n }\n\n async function createDocumentWithContent(title: string, content: string): Promise<Document> {\n const doc = await createDocument({ title });\n await insertText(doc.documentId, content, 1);\n return getDocument(doc.documentId);\n }\n\n return {\n listDocuments,\n getDocument,\n createDocument,\n updateDocument,\n insertText,\n deleteContent,\n replaceAllText,\n searchDocuments,\n extractText,\n createDocumentWithContent,\n };\n}\n\nexport type DocsClient = ReturnType<typeof createDocsClient>;\n",
240
- "lib/docs-google-oauth.ts": "import {\n getRefreshableAccessToken,\n type OAuthToken,\n tokenStore,\n} from \"./token-store.ts\";\n\nexport interface OAuthProvider {\n name: string;\n authorizationUrl: string;\n tokenUrl: string;\n clientId: string;\n clientSecret: string;\n scopes: string[];\n callbackPath: string;\n}\n\nfunction getExpiresAt(expiresIn: unknown): number | undefined {\n if (typeof expiresIn !== \"number\") return undefined;\n return Date.now() + expiresIn * 1000;\n}\n\nasync function postForm(url: string, body: Record<string, string>): Promise<any> {\n const response = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams(body),\n });\n\n if (response.ok) return response.json();\n\n throw new Error(\n `Token request failed: ${response.status} - ${await response.text()}`,\n );\n}\n\nexport function getAuthorizationUrl(\n provider: OAuthProvider,\n state: string,\n redirectUri: string,\n): string {\n const params = new URLSearchParams({\n client_id: provider.clientId,\n redirect_uri: redirectUri,\n response_type: \"code\",\n scope: provider.scopes.join(\" \"),\n state,\n access_type: \"offline\",\n prompt: \"consent\",\n });\n\n return `${provider.authorizationUrl}?${params.toString()}`;\n}\n\nexport async function exchangeCodeForTokens(\n provider: OAuthProvider,\n code: string,\n redirectUri: string,\n): Promise<OAuthToken> {\n const data = await postForm(provider.tokenUrl, {\n client_id: provider.clientId,\n client_secret: provider.clientSecret,\n code,\n grant_type: \"authorization_code\",\n redirect_uri: redirectUri,\n });\n\n return {\n accessToken: data.access_token,\n refreshToken: data.refresh_token,\n expiresAt: getExpiresAt(data.expires_in),\n tokenType: data.token_type ?? \"Bearer\",\n scope: data.scope,\n };\n}\n\nexport async function refreshAccessToken(\n provider: OAuthProvider,\n refreshToken: string,\n): Promise<OAuthToken> {\n const data = await postForm(provider.tokenUrl, {\n client_id: provider.clientId,\n client_secret: provider.clientSecret,\n refresh_token: refreshToken,\n grant_type: \"refresh_token\",\n });\n\n return {\n accessToken: data.access_token,\n refreshToken: data.refresh_token ?? refreshToken,\n expiresAt: getExpiresAt(data.expires_in),\n tokenType: data.token_type ?? \"Bearer\",\n scope: data.scope,\n };\n}\n\nexport async function getValidToken(\n provider: OAuthProvider,\n userId: string,\n service: string,\n): Promise<string | null> {\n return await getRefreshableAccessToken(\n tokenStore,\n service,\n userId,\n (refreshToken) => refreshAccessToken(provider, refreshToken),\n );\n}\n",
241
- "tools/create-document.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createDocsClient } from \"../lib/docs-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"docs-google-create-document\",\n description:\n \"Create a new Google Docs document with optional initial content. Returns the new document ID and URL.\",\n inputSchema: defineSchema((v) =>\n v.object({\n title: v.string().describe(\"Title of the new document\"),\n content: v\n .string()\n .optional()\n .describe(\"Optional initial text content to insert into the document\"),\n })\n )(),\n async execute({ title, content }, context) {\n const userId = requireUserIdFromContext(context);\n const client = createDocsClient(userId);\n\n const document = content\n ? await client.createDocumentWithContent(title, content)\n : await client.createDocument({ title });\n\n const [docMeta] = await client.listDocuments({ maxResults: 1 });\n const webViewLink = docMeta?.id === document.documentId ? docMeta.webViewLink : undefined;\n\n return {\n documentId: document.documentId,\n title: document.title,\n url: webViewLink ?? `https://docs.google.com/document/d/${document.documentId}/edit`,\n revisionId: document.revisionId,\n };\n },\n});\n",
242
- "tools/get-document.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createDocsClient } from \"../lib/docs-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"docs-google-get-document\",\n description:\n \"Get a Google Docs document's content and metadata. Returns the full document structure including text, formatting, and styles.\",\n inputSchema: defineSchema((v) =>\n v.object({\n documentId: v.string().describe(\"The ID of the document to retrieve\"),\n extractTextOnly: v\n .boolean()\n .default(false)\n .describe(\"If true, only return plain text content without formatting\"),\n })\n )(),\n async execute({ documentId, extractTextOnly }, context) {\n const userId = requireUserIdFromContext(context);\n const client = createDocsClient(userId);\n const document = await client.getDocument(documentId);\n\n const { documentId: id, title, revisionId } = document;\n\n if (extractTextOnly) {\n return {\n documentId: id,\n title,\n revisionId,\n text: client.extractText(document),\n };\n }\n\n return {\n documentId: id,\n title,\n revisionId,\n body: document.body,\n documentStyle: document.documentStyle,\n };\n },\n});\n",
243
- "tools/list-documents.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createDocsClient } from \"../lib/docs-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"docs-google-list-documents\",\n description:\n \"List recent Google Docs documents from Google Drive. Returns document names, IDs, and metadata.\",\n inputSchema: defineSchema((v) =>\n v.object({\n maxResults: v\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of documents to return\"),\n orderBy: v\n .enum([\"createdTime\", \"modifiedTime\", \"name\"] as const)\n .default(\"modifiedTime\")\n .describe(\"Sort order for results\"),\n })\n )(),\n async execute({ maxResults, orderBy }, context) {\n const userId = requireUserIdFromContext(context);\n const client = createDocsClient(userId);\n const documents = await client.listDocuments({ maxResults, orderBy });\n\n return documents.map((doc) => ({\n id: doc.id,\n name: doc.name,\n url: doc.webViewLink,\n createdTime: doc.createdTime,\n modifiedTime: doc.modifiedTime,\n thumbnail: doc.thumbnailLink,\n }));\n },\n});\n",
244
- "tools/search-documents.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createDocsClient } from \"../lib/docs-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"docs-google-search-documents\",\n description:\n \"Search for Google Docs documents by query string. Searches document names and content. Returns matching document IDs, names, and metadata.\",\n inputSchema: defineSchema((v) =>\n v.object({\n query: v\n .string()\n .describe(\"Search query to find documents. Searches in document names and content.\"),\n maxResults: v\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of results to return\"),\n })\n )(),\n async execute({ query, maxResults }, context) {\n const userId = requireUserIdFromContext(context);\n const client = createDocsClient(userId);\n const documents = await client.searchDocuments(query, maxResults);\n\n return documents.map((document) => ({\n id: document.id,\n name: document.name,\n url: document.webViewLink,\n createdTime: document.createdTime,\n modifiedTime: document.modifiedTime,\n thumbnail: document.thumbnailLink,\n }));\n },\n});\n",
245
- "tools/update-document.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createDocsClient, type Request } from \"../lib/docs-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"docs-google-update-document\",\n description:\n \"Update a Google Docs document using batch requests. Supports inserting text, deleting content, replacing text, and more.\",\n inputSchema: defineSchema((v) =>\n v.union([\n v.object({\n documentId: v.string().describe(\"The ID of the document to update\"),\n requests: v\n .array(v.any())\n .describe(\n \"Array of batch update requests. See Google Docs API documentation for request types: insertText, deleteContentRange, replaceAllText, etc.\",\n ),\n }),\n v.object({\n documentId: v.string().describe(\"The ID of the document to update\"),\n operation: v\n .object({\n type: v\n .enum([\"insertText\", \"deleteContent\", \"replaceAllText\"] as const)\n .describe(\"Type of operation to perform\"),\n insertText: v\n .object({\n text: v.string().describe(\"Text to insert\"),\n index: v.number().describe(\"Position to insert at (1 = start of document)\"),\n })\n .optional()\n .describe(\"Parameters for insertText operation\"),\n deleteContent: v\n .object({\n startIndex: v.number().describe(\"Start position of content to delete\"),\n endIndex: v.number().describe(\"End position of content to delete\"),\n })\n .optional()\n .describe(\"Parameters for deleteContent operation\"),\n replaceAllText: v\n .object({\n searchText: v.string().describe(\"Text to search for\"),\n replaceText: v.string().describe(\"Text to replace with\"),\n matchCase: v.boolean().default(false).describe(\"Whether to match case\"),\n })\n .optional()\n .describe(\"Parameters for replaceAllText operation\"),\n })\n .describe(\"Simple operation to perform\"),\n }),\n ])\n )(),\n async execute(input, context): Promise<{\n documentId: string;\n success: true;\n replies: unknown;\n writeControl?: unknown;\n }> {\n const userId = requireUserIdFromContext(context);\n const client = createDocsClient(userId);\n\n if (!(\"operation\" in input)) {\n const { documentId, requests } = input;\n const result = await client.updateDocument(documentId, requests as Request[]);\n\n return {\n documentId: result.documentId,\n success: true,\n replies: result.replies,\n writeControl: result.writeControl,\n };\n }\n\n const { documentId, operation } = input;\n\n switch (operation.type) {\n case \"insertText\": {\n const params = operation.insertText;\n if (!params) throw new Error(\"insertText parameters required\");\n\n const result = await client.insertText(documentId, params.text, params.index);\n return { documentId: result.documentId, success: true, replies: result.replies };\n }\n\n case \"deleteContent\": {\n const params = operation.deleteContent;\n if (!params) throw new Error(\"deleteContent parameters required\");\n\n const result = await client.deleteContent(documentId, params.startIndex, params.endIndex);\n return { documentId: result.documentId, success: true, replies: result.replies };\n }\n\n case \"replaceAllText\": {\n const params = operation.replaceAllText;\n if (!params) throw new Error(\"replaceAllText parameters required\");\n\n const result = await client.replaceAllText(\n documentId,\n params.searchText,\n params.replaceText,\n params.matchCase,\n );\n return { documentId: result.documentId, success: true, replies: result.replies };\n }\n }\n },\n});\n"
246
- }
247
- },
248
- "integration:drive": {
249
- "files": {
250
- ".env.example": "# Google Drive Integration\n# Create OAuth credentials at https://console.cloud.google.com/apis/credentials\n# Make sure to enable:\n# - Google Drive API: https://console.cloud.google.com/apis/library/drive.googleapis.com\n#\n# Note: These credentials are shared across all Google integrations (Gmail, Calendar, Sheets, Drive)\n\nGOOGLE_CLIENT_ID=your_client_id_here\nGOOGLE_CLIENT_SECRET=your_client_secret_here\n",
251
- "app/api/auth/drive/callback/route.ts": "import { createOAuthCallbackHandler, driveConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\n\nexport const GET = createOAuthCallbackHandler(driveConfig, { tokenStore });\n",
252
- "app/api/auth/drive/route.ts": "import { createOAuthInitHandler, driveConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../lib/token-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../lib/user-id.ts\";\n\nexport const GET = createOAuthInitHandler(driveConfig, {\n tokenStore,\n getUserId: requireUserIdFromRequest,\n});\n",
253
- "lib/drive-client.ts": "import { getValidToken } from \"./drive-oauth.ts\";\n\nfunction getEnv(key: string): string | undefined {\n // @ts-ignore - Deno global\n if (typeof Deno !== \"undefined\") return Deno.env.get(key);\n // @ts-ignore - process global\n if (typeof process !== \"undefined\" && process.env) return process.env[key];\n return undefined;\n}\n\nconst DRIVE_API_BASE = \"https://www.googleapis.com/drive/v3\";\n\nexport interface DriveFile {\n id: string;\n name: string;\n mimeType: string;\n kind: string;\n createdTime: string;\n modifiedTime: string;\n size?: string;\n webViewLink?: string;\n webContentLink?: string;\n iconLink?: string;\n thumbnailLink?: string;\n parents?: string[];\n starred?: boolean;\n trashed?: boolean;\n shared?: boolean;\n owners?: Array<{\n displayName: string;\n emailAddress: string;\n photoLink?: string;\n }>;\n lastModifyingUser?: {\n displayName: string;\n emailAddress: string;\n photoLink?: string;\n };\n capabilities?: {\n canEdit?: boolean;\n canComment?: boolean;\n canShare?: boolean;\n canDelete?: boolean;\n canDownload?: boolean;\n };\n}\n\nexport interface DriveFileList {\n files: DriveFile[];\n nextPageToken?: string;\n incompleteSearch?: boolean;\n}\n\nexport interface CreateFolderOptions {\n name: string;\n parentId?: string;\n description?: string;\n}\n\nexport interface UploadFileOptions {\n name: string;\n content: string;\n mimeType: string;\n parentId?: string;\n description?: string;\n}\n\nexport interface ListFilesOptions {\n folderId?: string;\n pageSize?: number;\n pageToken?: string;\n orderBy?: string;\n query?: string;\n}\n\nexport interface SearchFilesOptions {\n query: string;\n pageSize?: number;\n pageToken?: string;\n orderBy?: string;\n}\n\nexport const driveOAuthProvider = {\n name: \"drive\",\n authorizationUrl: \"https://accounts.google.com/o/oauth2/v2/auth\",\n tokenUrl: \"https://oauth2.googleapis.com/token\",\n clientId: getEnv(\"GOOGLE_CLIENT_ID\") ?? \"\",\n clientSecret: getEnv(\"GOOGLE_CLIENT_SECRET\") ?? \"\",\n scopes: [\n \"https://www.googleapis.com/auth/drive\",\n ],\n callbackPath: \"/api/auth/drive/callback\",\n};\n\nexport function createDriveClient(userId: string): {\n listFiles(options?: ListFilesOptions): Promise<DriveFileList>;\n getFile(fileId: string): Promise<DriveFile>;\n searchFiles(options: SearchFilesOptions): Promise<DriveFileList>;\n createFolder(options: CreateFolderOptions): Promise<DriveFile>;\n uploadFile(options: UploadFileOptions): Promise<DriveFile>;\n downloadFile(fileId: string): Promise<string>;\n deleteFile(fileId: string): Promise<void>;\n copyFile(fileId: string, name: string, parentId?: string): Promise<DriveFile>;\n updateFile(\n fileId: string,\n updates: { name?: string; description?: string; starred?: boolean },\n ): Promise<DriveFile>;\n} {\n async function getAccessToken(): Promise<string> {\n const token = await getValidToken(driveOAuthProvider, userId, \"drive\");\n if (!token) {\n throw new Error(\"Google Drive not connected. Please connect your Google account first.\");\n }\n return token;\n }\n\n async function driveApiRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const accessToken = await getAccessToken();\n\n const response = await fetch(`${DRIVE_API_BASE}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${accessToken}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Drive API error: ${response.status} - ${error}`);\n }\n\n if (response.status === 204) return undefined as T;\n return response.json();\n }\n\n function buildMetadata(options: {\n name: string;\n mimeType: string;\n parentId?: string;\n description?: string;\n }): Record<string, unknown> {\n const metadata: Record<string, unknown> = {\n name: options.name,\n mimeType: options.mimeType,\n };\n\n if (options.parentId) metadata.parents = [options.parentId];\n if (options.description) metadata.description = options.description;\n\n return metadata;\n }\n\n const fileFields =\n \"id,name,mimeType,kind,createdTime,modifiedTime,size,webViewLink,webContentLink,iconLink,thumbnailLink,parents,starred,trashed,shared,owners,lastModifyingUser,capabilities\";\n\n return {\n async listFiles(options: ListFilesOptions = {}): Promise<DriveFileList> {\n const params = new URLSearchParams({\n fields: `nextPageToken,incompleteSearch,files(${fileFields})`,\n pageSize: String(options.pageSize ?? 100),\n orderBy: options.orderBy ?? \"modifiedTime desc\",\n });\n\n let query = \"trashed=false\";\n if (options.folderId) query += ` and '${options.folderId}' in parents`;\n if (options.query) query += ` and ${options.query}`;\n\n params.append(\"q\", query);\n if (options.pageToken) params.append(\"pageToken\", options.pageToken);\n\n return driveApiRequest<DriveFileList>(`/files?${params.toString()}`);\n },\n\n async getFile(fileId: string): Promise<DriveFile> {\n const params = new URLSearchParams({ fields: fileFields });\n return driveApiRequest<DriveFile>(`/files/${fileId}?${params.toString()}`);\n },\n\n async searchFiles(options: SearchFilesOptions): Promise<DriveFileList> {\n const params = new URLSearchParams({\n fields:\n \"nextPageToken,incompleteSearch,files(id,name,mimeType,kind,createdTime,modifiedTime,size,webViewLink,webContentLink,iconLink,thumbnailLink,parents,starred,trashed)\",\n pageSize: String(options.pageSize ?? 100),\n q: `${options.query} and trashed=false`,\n orderBy: options.orderBy ?? \"modifiedTime desc\",\n });\n\n if (options.pageToken) params.append(\"pageToken\", options.pageToken);\n\n return driveApiRequest<DriveFileList>(`/files?${params.toString()}`);\n },\n\n async createFolder(options: CreateFolderOptions): Promise<DriveFile> {\n const metadata = buildMetadata({\n name: options.name,\n mimeType: \"application/vnd.google-apps.folder\",\n parentId: options.parentId,\n description: options.description,\n });\n\n return driveApiRequest<DriveFile>(\"/files\", {\n method: \"POST\",\n body: JSON.stringify(metadata),\n });\n },\n\n async uploadFile(options: UploadFileOptions): Promise<DriveFile> {\n const accessToken = await getAccessToken();\n\n const boundary = \"-------314159265358979323846\";\n const delimiter = `\\r\\n--${boundary}\\r\\n`;\n const closeDelimiter = `\\r\\n--${boundary}--`;\n\n const metadata = buildMetadata({\n name: options.name,\n mimeType: options.mimeType,\n parentId: options.parentId,\n description: options.description,\n });\n\n const multipartRequestBody =\n delimiter +\n \"Content-Type: application/json\\r\\n\\r\\n\" +\n JSON.stringify(metadata) +\n delimiter +\n `Content-Type: ${options.mimeType}\\r\\n\\r\\n` +\n options.content +\n closeDelimiter;\n\n const response = await fetch(\n \"https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id,name,mimeType,kind,createdTime,modifiedTime,size,webViewLink,webContentLink\",\n {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${accessToken}`,\n \"Content-Type\": `multipart/related; boundary=${boundary}`,\n },\n body: multipartRequestBody,\n },\n );\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Drive upload error: ${response.status} - ${error}`);\n }\n\n return response.json();\n },\n\n async downloadFile(fileId: string): Promise<string> {\n const accessToken = await getAccessToken();\n\n const response = await fetch(`${DRIVE_API_BASE}/files/${fileId}?alt=media`, {\n headers: { Authorization: `Bearer ${accessToken}` },\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Drive download error: ${response.status} - ${error}`);\n }\n\n return response.text();\n },\n\n async deleteFile(fileId: string): Promise<void> {\n await driveApiRequest(`/files/${fileId}`, { method: \"DELETE\" });\n },\n\n async copyFile(fileId: string, name: string, parentId?: string): Promise<DriveFile> {\n const metadata: Record<string, unknown> = { name };\n if (parentId) metadata.parents = [parentId];\n\n return driveApiRequest<DriveFile>(`/files/${fileId}/copy`, {\n method: \"POST\",\n body: JSON.stringify(metadata),\n });\n },\n\n async updateFile(\n fileId: string,\n updates: { name?: string; description?: string; starred?: boolean },\n ): Promise<DriveFile> {\n return driveApiRequest<DriveFile>(`/files/${fileId}`, {\n method: \"PATCH\",\n body: JSON.stringify(updates),\n });\n },\n };\n}\n\nexport type DriveClient = ReturnType<typeof createDriveClient>;\n",
254
- "lib/drive-oauth.ts": "import {\n getRefreshableAccessToken,\n type OAuthToken,\n tokenStore,\n} from \"./token-store.ts\";\n\nexport interface OAuthProvider {\n name: string;\n authorizationUrl: string;\n tokenUrl: string;\n clientId: string;\n clientSecret: string;\n scopes: string[];\n callbackPath: string;\n}\n\nfunction buildTokenRequest(\n provider: OAuthProvider,\n body: Record<string, string>,\n): RequestInit {\n return {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams({\n client_id: provider.clientId,\n client_secret: provider.clientSecret,\n ...body,\n }),\n };\n}\n\nasync function fetchToken(\n provider: OAuthProvider,\n body: Record<string, string>,\n errorPrefix: string,\n): Promise<any> {\n const response = await fetch(\n provider.tokenUrl,\n buildTokenRequest(provider, body),\n );\n\n if (response.ok) return response.json();\n\n const error = await response.text();\n throw new Error(`${errorPrefix}: ${response.status} - ${error}`);\n}\n\nfunction toOAuthToken(data: any, fallbackRefreshToken?: string): OAuthToken {\n const expiresIn = data.expires_in;\n\n return {\n accessToken: data.access_token,\n refreshToken: data.refresh_token ?? fallbackRefreshToken,\n expiresAt: expiresIn ? Date.now() + expiresIn * 1000 : undefined,\n tokenType: data.token_type ?? \"Bearer\",\n scope: data.scope,\n };\n}\n\nexport function getAuthorizationUrl(\n provider: OAuthProvider,\n state: string,\n redirectUri: string,\n): string {\n const params = new URLSearchParams({\n client_id: provider.clientId,\n redirect_uri: redirectUri,\n response_type: \"code\",\n scope: provider.scopes.join(\" \"),\n state,\n access_type: \"offline\",\n prompt: \"consent\",\n });\n\n return `${provider.authorizationUrl}?${params.toString()}`;\n}\n\nexport async function exchangeCodeForTokens(\n provider: OAuthProvider,\n code: string,\n redirectUri: string,\n): Promise<OAuthToken> {\n const data = await fetchToken(\n provider,\n {\n code,\n grant_type: \"authorization_code\",\n redirect_uri: redirectUri,\n },\n \"Token exchange failed\",\n );\n\n return toOAuthToken(data);\n}\n\nexport async function refreshAccessToken(\n provider: OAuthProvider,\n refreshToken: string,\n): Promise<OAuthToken> {\n const data = await fetchToken(\n provider,\n {\n refresh_token: refreshToken,\n grant_type: \"refresh_token\",\n },\n \"Token refresh failed\",\n );\n\n return toOAuthToken(data, refreshToken);\n}\n\nexport async function getValidToken(\n provider: OAuthProvider,\n userId: string,\n service: string,\n): Promise<string | null> {\n return await getRefreshableAccessToken(\n tokenStore,\n service,\n userId,\n (refreshToken) => refreshAccessToken(provider, refreshToken),\n );\n}\n",
255
- "tools/create-folder.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createDriveClient } from \"../lib/drive-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"drive-create-folder\",\n description:\n \"Create a new folder in Google Drive. Can optionally specify a parent folder. Returns the new folder ID and details.\",\n inputSchema: defineSchema((v) =>\n v.object({\n name: v.string().describe(\"Name of the folder to create\"),\n parentId: v\n .string()\n .optional()\n .describe(\"ID of the parent folder. If not provided, creates in root.\"),\n description: v\n .string()\n .optional()\n .describe(\"Optional description for the folder\"),\n })\n )(),\n async execute({ name, parentId, description }, context) {\n const userId = requireUserIdFromContext(context);\n const client = createDriveClient(userId);\n const folder = await client.createFolder({ name, parentId, description });\n\n return {\n id: folder.id,\n name: folder.name,\n mimeType: folder.mimeType,\n createdTime: folder.createdTime,\n modifiedTime: folder.modifiedTime,\n webViewLink: folder.webViewLink,\n parents: folder.parents,\n };\n },\n});\n",
256
- "tools/get-file.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createDriveClient } from \"../lib/drive-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\nconst FOLDER_MIME_TYPE = \"application/vnd.google-apps.folder\";\n\nexport default tool({\n id: \"drive-get-file\",\n description:\n \"Get detailed metadata about a specific file or folder in Google Drive. Returns detailed information including sharing settings, owners, and capabilities.\",\n inputSchema: defineSchema((v) =>\n v.object({\n fileId: v.string().describe(\"The ID of the file or folder to retrieve\"),\n })\n )(),\n async execute({ fileId }, context) {\n const userId = requireUserIdFromContext(context);\n const client = createDriveClient(userId);\n const file = await client.getFile(fileId);\n\n const lastModifyingUser = file.lastModifyingUser\n ? {\n name: file.lastModifyingUser.displayName,\n email: file.lastModifyingUser.emailAddress,\n photoLink: file.lastModifyingUser.photoLink,\n }\n : undefined;\n\n return {\n id: file.id,\n name: file.name,\n mimeType: file.mimeType,\n isFolder: file.mimeType === FOLDER_MIME_TYPE,\n size: file.size,\n createdTime: file.createdTime,\n modifiedTime: file.modifiedTime,\n webViewLink: file.webViewLink,\n webContentLink: file.webContentLink,\n iconLink: file.iconLink,\n thumbnailLink: file.thumbnailLink,\n parents: file.parents,\n starred: file.starred,\n trashed: file.trashed,\n shared: file.shared,\n owners: file.owners?.map((owner) => ({\n name: owner.displayName,\n email: owner.emailAddress,\n photoLink: owner.photoLink,\n })),\n lastModifyingUser,\n capabilities: file.capabilities,\n };\n },\n});\n",
257
- "tools/list-files.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createDriveClient } from \"../lib/drive-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\nconst FOLDER_MIME_TYPE = \"application/vnd.google-apps.folder\";\n\nexport default tool({\n id: \"drive-list-files\",\n description:\n \"List files and folders in Google Drive. Can list from a specific folder or root. Returns file names, IDs, types, and metadata.\",\n inputSchema: defineSchema((v) =>\n v.object({\n folderId: v\n .string()\n .optional()\n .describe(\n \"ID of the folder to list files from. If not provided, lists from root.\",\n ),\n pageSize: v\n .number()\n .min(1)\n .max(1000)\n .default(100)\n .describe(\"Maximum number of files to return\"),\n pageToken: v\n .string()\n .optional()\n .describe(\"Token for pagination to get next page of results\"),\n orderBy: v\n .enum([\n \"createdTime\",\n \"folder\",\n \"modifiedByMeTime\",\n \"modifiedTime\",\n \"name\",\n \"quotaBytesUsed\",\n \"recency\",\n \"sharedWithMeTime\",\n \"starred\",\n \"viewedByMeTime\",\n ])\n .optional()\n .describe(\"Field to sort results by\"),\n })\n )(),\n async execute({ folderId, pageSize, pageToken, orderBy }, context) {\n const userId = requireUserIdFromContext(context);\n const client = createDriveClient(userId);\n\n const result = await client.listFiles({\n folderId,\n pageSize,\n pageToken,\n orderBy: orderBy ? `${orderBy} desc` : undefined,\n });\n\n const nextPageToken = result.nextPageToken;\n\n return {\n files: result.files.map((file) => ({\n id: file.id,\n name: file.name,\n mimeType: file.mimeType,\n isFolder: file.mimeType === FOLDER_MIME_TYPE,\n size: file.size,\n createdTime: file.createdTime,\n modifiedTime: file.modifiedTime,\n webViewLink: file.webViewLink,\n iconLink: file.iconLink,\n thumbnailLink: file.thumbnailLink,\n starred: file.starred,\n shared: file.shared,\n })),\n nextPageToken,\n hasMore: Boolean(nextPageToken),\n };\n },\n});\n",
258
- "tools/search-files.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createDriveClient } from \"../lib/drive-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\nconst FOLDER_MIME_TYPE = \"application/vnd.google-apps.folder\";\n\nexport default tool({\n id: \"drive-search-files\",\n description:\n \"Search for files and folders in Google Drive using queries. Supports searching by name, content, type, and more. Use Drive query syntax (e.g., \\\"name contains 'report'\\\", \\\"mimeType='application/pdf'\\\").\",\n inputSchema: defineSchema((v) =>\n v.object({\n query: v\n .string()\n .describe(\n \"Search query using Drive query syntax. Examples: \\\"name contains 'report'\\\", \\\"mimeType='application/pdf'\\\", \\\"fullText contains 'budget'\\\"\",\n ),\n pageSize: v\n .number()\n .min(1)\n .max(1000)\n .default(100)\n .describe(\"Maximum number of files to return\"),\n pageToken: v\n .string()\n .optional()\n .describe(\"Token for pagination to get next page of results\"),\n orderBy: v\n .enum([\n \"createdTime\",\n \"folder\",\n \"modifiedByMeTime\",\n \"modifiedTime\",\n \"name\",\n \"quotaBytesUsed\",\n \"recency\",\n \"sharedWithMeTime\",\n \"starred\",\n \"viewedByMeTime\",\n ])\n .optional()\n .describe(\"Field to sort results by\"),\n })\n )(),\n async execute({ query, pageSize, pageToken, orderBy }, context) {\n const userId = requireUserIdFromContext(context);\n const client = createDriveClient(userId);\n\n const result = await client.searchFiles({\n query,\n pageSize,\n pageToken,\n orderBy: orderBy ? `${orderBy} desc` : undefined,\n });\n\n const files = result.files.map((file) => ({\n id: file.id,\n name: file.name,\n mimeType: file.mimeType,\n isFolder: file.mimeType === FOLDER_MIME_TYPE,\n size: file.size,\n createdTime: file.createdTime,\n modifiedTime: file.modifiedTime,\n webViewLink: file.webViewLink,\n iconLink: file.iconLink,\n thumbnailLink: file.thumbnailLink,\n starred: file.starred,\n shared: file.shared,\n parents: file.parents,\n }));\n\n const nextPageToken = result.nextPageToken;\n\n return {\n files,\n nextPageToken,\n hasMore: Boolean(nextPageToken),\n incompleteSearch: result.incompleteSearch,\n };\n },\n});\n",
259
- "tools/upload-file.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createDriveClient } from \"../lib/drive-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"drive-upload-file\",\n description:\n \"Upload or create a text file in Google Drive. Supports plain text, JSON, CSV, Markdown, and other text formats. Returns the new file ID and details.\",\n inputSchema: defineSchema((v) =>\n v.object({\n name: v\n .string()\n .describe(\"Name of the file including extension (e.g., 'report.txt', 'data.json')\"),\n content: v.string().describe(\"Text content of the file\"),\n mimeType: v\n .string()\n .default(\"text/plain\")\n .describe(\n \"MIME type of the file. Examples: 'text/plain', 'application/json', 'text/csv', 'text/markdown'\",\n ),\n parentId: v\n .string()\n .optional()\n .describe(\"ID of the parent folder. If not provided, creates in root.\"),\n description: v.string().optional().describe(\"Optional description for the file\"),\n })\n )(),\n async execute({ name, content, mimeType, parentId, description }, context) {\n const userId = requireUserIdFromContext(context);\n const client = createDriveClient(userId);\n const file = await client.uploadFile({ name, content, mimeType, parentId, description });\n\n return {\n id: file.id,\n name: file.name,\n mimeType: file.mimeType,\n size: file.size,\n createdTime: file.createdTime,\n modifiedTime: file.modifiedTime,\n webViewLink: file.webViewLink,\n webContentLink: file.webContentLink,\n };\n },\n});\n"
260
- }
261
- },
262
- "integration:figma": {
263
- "files": {
264
- ".env.example": "# Figma OAuth Integration\n# Get these credentials from https://www.figma.com/developers/apps\n\nFIGMA_CLIENT_ID=your_figma_client_id\nFIGMA_CLIENT_SECRET=your_figma_client_secret\n",
265
- "app/api/auth/figma/callback/route.ts": "import { createOAuthCallbackHandler, figmaConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\n\nexport const GET = createOAuthCallbackHandler(figmaConfig, { tokenStore });\n",
266
- "app/api/auth/figma/route.ts": "import { createOAuthInitHandler, figmaConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../lib/token-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../lib/user-id.ts\";\n\nexport const GET = createOAuthInitHandler(figmaConfig, {\n tokenStore,\n getUserId: requireUserIdFromRequest,\n});\n",
267
- "lib/figma-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst FIGMA_BASE_URL = \"https://api.figma.com/v1\";\n\nexport interface FigmaFile {\n document: FigmaNode;\n components: Record<string, FigmaComponent>;\n componentSets: Record<string, FigmaComponentSet>;\n schemaVersion: number;\n styles: Record<string, FigmaStyle>;\n name: string;\n lastModified: string;\n thumbnailUrl: string;\n version: string;\n role: string;\n editorType: string;\n linkAccess: string;\n}\n\nexport interface FigmaNode {\n id: string;\n name: string;\n type: string;\n children?: FigmaNode[];\n visible?: boolean;\n locked?: boolean;\n absoluteBoundingBox?: {\n x: number;\n y: number;\n width: number;\n height: number;\n };\n fills?: Array<{\n type: string;\n color?: {\n r: number;\n g: number;\n b: number;\n a: number;\n };\n }>;\n strokes?: unknown[];\n strokeWeight?: number;\n effects?: unknown[];\n cornerRadius?: number;\n rectangleCornerRadii?: number[];\n characters?: string;\n style?: {\n fontFamily?: string;\n fontSize?: number;\n fontWeight?: number;\n lineHeightPx?: number;\n };\n}\n\nexport interface FigmaComponent {\n key: string;\n name: string;\n description: string;\n componentSetId?: string;\n documentationLinks: unknown[];\n}\n\nexport interface FigmaComponentSet {\n key: string;\n name: string;\n description: string;\n documentationLinks: unknown[];\n}\n\nexport interface FigmaStyle {\n key: string;\n name: string;\n description: string;\n styleType: \"FILL\" | \"TEXT\" | \"EFFECT\" | \"GRID\";\n}\n\nexport interface FigmaComment {\n id: string;\n file_key: string;\n parent_id?: string;\n user: {\n id: string;\n handle: string;\n img_url: string;\n };\n created_at: string;\n resolved_at?: string;\n message: string;\n client_meta: {\n x?: number;\n y?: number;\n node_id?: string[];\n node_offset?: { x: number; y: number };\n };\n order_id: string;\n}\n\nexport interface FigmaUser {\n id: string;\n handle: string;\n img_url: string;\n email?: string;\n}\n\nasync function figmaFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with Figma. Please connect your account.\");\n }\n\n const response = await fetch(`${FIGMA_BASE_URL}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (response.ok) {\n return response.json() as Promise<T>;\n }\n\n const error = (await response.json().catch(() => ({}))) as { message?: string; err?: string };\n throw new Error(\n `Figma API error: ${response.status} ${error.message ?? error.err ?? response.statusText}`,\n );\n}\n\nexport function getMe(): Promise<FigmaUser> {\n return figmaFetch<FigmaUser>(\"/me\");\n}\n\nexport function getFile(\n fileKey: string,\n options?: {\n version?: string;\n ids?: string[];\n depth?: number;\n geometry?: \"paths\" | \"bounds\";\n plugin_data?: string;\n branch_data?: boolean;\n },\n): Promise<FigmaFile> {\n const params = new URLSearchParams();\n\n if (options?.version) params.set(\"version\", options.version);\n if (options?.ids?.length) params.set(\"ids\", options.ids.join(\",\"));\n if (options?.depth) params.set(\"depth\", String(options.depth));\n if (options?.geometry) params.set(\"geometry\", options.geometry);\n if (options?.plugin_data) params.set(\"plugin_data\", options.plugin_data);\n if (options?.branch_data) params.set(\"branch_data\", \"true\");\n\n const query = params.toString();\n const url = query ? `/files/${fileKey}?${query}` : `/files/${fileKey}`;\n\n return figmaFetch<FigmaFile>(url);\n}\n\nexport function getFileNodes(\n fileKey: string,\n nodeIds: string[],\n): Promise<{\n name: string;\n lastModified: string;\n thumbnailUrl: string;\n version: string;\n nodes: Record<string, { document: FigmaNode; components: Record<string, FigmaComponent> }>;\n}> {\n const params = new URLSearchParams({ ids: nodeIds.join(\",\") });\n return figmaFetch(`/files/${fileKey}/nodes?${params.toString()}`);\n}\n\nexport function getFileImages(\n fileKey: string,\n nodeIds: string[],\n options?: {\n format?: \"jpg\" | \"png\" | \"svg\" | \"pdf\";\n scale?: number;\n svg_include_id?: boolean;\n svg_simplify_stroke?: boolean;\n use_absolute_bounds?: boolean;\n version?: string;\n },\n): Promise<{\n err?: string;\n images: Record<string, string | null>;\n status?: number;\n}> {\n const params = new URLSearchParams({\n ids: nodeIds.join(\",\"),\n format: options?.format ?? \"png\",\n });\n\n if (options?.scale) params.set(\"scale\", String(options.scale));\n if (options?.svg_include_id) params.set(\"svg_include_id\", \"true\");\n if (options?.svg_simplify_stroke) params.set(\"svg_simplify_stroke\", \"true\");\n if (options?.use_absolute_bounds) params.set(\"use_absolute_bounds\", \"true\");\n if (options?.version) params.set(\"version\", options.version);\n\n return figmaFetch(`/images/${fileKey}?${params.toString()}`);\n}\n\nexport function getComments(fileKey: string): Promise<{ comments: FigmaComment[] }> {\n return figmaFetch<{ comments: FigmaComment[] }>(`/files/${fileKey}/comments`);\n}\n\nexport function postComment(\n fileKey: string,\n message: string,\n options?: {\n client_meta?: { x?: number; y?: number; node_id?: string[] };\n parent_id?: string;\n },\n): Promise<FigmaComment> {\n return figmaFetch<FigmaComment>(`/files/${fileKey}/comments`, {\n method: \"POST\",\n body: JSON.stringify({\n message,\n client_meta: options?.client_meta ?? {},\n ...(options?.parent_id ? { parent_id: options.parent_id } : {}),\n }),\n });\n}\n\nexport function extractComponents(file: FigmaFile): Array<{\n key: string;\n name: string;\n description: string;\n type: \"component\" | \"component_set\";\n}> {\n const components = Object.entries(file.components).map(([key, component]) => ({\n key,\n name: component.name,\n description: component.description,\n type: \"component\" as const,\n }));\n\n const componentSets = Object.entries(file.componentSets).map(([key, componentSet]) => ({\n key,\n name: componentSet.name,\n description: componentSet.description,\n type: \"component_set\" as const,\n }));\n\n return [...components, ...componentSets];\n}\n\nexport function extractStyles(file: FigmaFile): Array<{\n key: string;\n name: string;\n description: string;\n type: string;\n}> {\n return Object.entries(file.styles).map(([key, style]) => ({\n key,\n name: style.name,\n description: style.description,\n type: style.styleType,\n }));\n}\n\nexport function findNodesByType(node: FigmaNode, type: string): FigmaNode[] {\n const results: FigmaNode[] = [];\n\n if (node.type === type) {\n results.push(node);\n }\n\n for (const child of node.children ?? []) {\n results.push(...findNodesByType(child, type));\n }\n\n return results;\n}\n\nexport function getFileSummary(file: FigmaFile): {\n name: string;\n lastModified: string;\n componentCount: number;\n componentSetCount: number;\n styleCount: number;\n pageCount: number;\n} {\n return {\n name: file.name,\n lastModified: file.lastModified,\n componentCount: Object.keys(file.components).length,\n componentSetCount: Object.keys(file.componentSets).length,\n styleCount: Object.keys(file.styles).length,\n pageCount: file.document.children?.length ?? 0,\n };\n}\n",
268
- "lib/types.ts": "export type NodeType =\n | \"DOCUMENT\"\n | \"CANVAS\"\n | \"FRAME\"\n | \"GROUP\"\n | \"VECTOR\"\n | \"BOOLEAN_OPERATION\"\n | \"STAR\"\n | \"LINE\"\n | \"ELLIPSE\"\n | \"REGULAR_POLYGON\"\n | \"RECTANGLE\"\n | \"TEXT\"\n | \"SLICE\"\n | \"COMPONENT\"\n | \"COMPONENT_SET\"\n | \"INSTANCE\";\n\nexport type BlendMode =\n | \"NORMAL\"\n | \"DARKEN\"\n | \"MULTIPLY\"\n | \"LINEAR_BURN\"\n | \"COLOR_BURN\"\n | \"LIGHTEN\"\n | \"SCREEN\"\n | \"LINEAR_DODGE\"\n | \"COLOR_DODGE\"\n | \"OVERLAY\"\n | \"SOFT_LIGHT\"\n | \"HARD_LIGHT\"\n | \"DIFFERENCE\"\n | \"EXCLUSION\"\n | \"HUE\"\n | \"SATURATION\"\n | \"COLOR\"\n | \"LUMINOSITY\";\n\nexport type EasingType = \"EASE_IN\" | \"EASE_OUT\" | \"EASE_IN_AND_OUT\" | \"LINEAR\";\n\nexport interface Vector2D {\n x: number;\n y: number;\n}\n\nexport interface Rectangle {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\nexport interface Transform {\n /** 2D transformation matrix [[a, b, tx], [c, d, ty]] */\n matrix: [[number, number, number], [number, number, number]];\n}\n\nexport type PaintType =\n | \"SOLID\"\n | \"GRADIENT_LINEAR\"\n | \"GRADIENT_RADIAL\"\n | \"GRADIENT_ANGULAR\"\n | \"GRADIENT_DIAMOND\"\n | \"IMAGE\"\n | \"EMOJI\";\n\nexport interface Color {\n r: number;\n g: number;\n b: number;\n a: number;\n}\n\nexport interface ColorStop {\n position: number;\n color: Color;\n}\n\nexport interface Paint {\n type: PaintType;\n visible?: boolean;\n opacity?: number;\n color?: Color;\n blendMode?: BlendMode;\n gradientHandlePositions?: Vector2D[];\n gradientStops?: ColorStop[];\n scaleMode?: \"FILL\" | \"FIT\" | \"TILE\" | \"STRETCH\";\n imageTransform?: Transform;\n scalingFactor?: number;\n imageRef?: string;\n gifRef?: string;\n}\n\nexport type EffectType = \"INNER_SHADOW\" | \"DROP_SHADOW\" | \"LAYER_BLUR\" | \"BACKGROUND_BLUR\";\n\nexport interface Effect {\n type: EffectType;\n visible?: boolean;\n radius?: number;\n color?: Color;\n blendMode?: BlendMode;\n offset?: Vector2D;\n spread?: number;\n}\n\nexport type LayoutConstraintVertical = \"TOP\" | \"BOTTOM\" | \"CENTER\" | \"TOP_BOTTOM\" | \"SCALE\";\nexport type LayoutConstraintHorizontal = \"LEFT\" | \"RIGHT\" | \"CENTER\" | \"LEFT_RIGHT\" | \"SCALE\";\n\nexport interface LayoutConstraint {\n vertical: LayoutConstraintVertical;\n horizontal: LayoutConstraintHorizontal;\n}\n\nexport type LayoutAlign = \"MIN\" | \"CENTER\" | \"MAX\" | \"STRETCH\" | \"INHERIT\";\nexport type LayoutMode = \"NONE\" | \"HORIZONTAL\" | \"VERTICAL\";\n\nexport interface LayoutGrid {\n pattern: \"COLUMNS\" | \"ROWS\" | \"GRID\";\n sectionSize?: number;\n visible?: boolean;\n color?: Color;\n alignment?: \"MIN\" | \"MAX\" | \"CENTER\" | \"STRETCH\";\n gutterSize?: number;\n offset?: number;\n count?: number;\n}\n\nexport type TextAlignHorizontal = \"LEFT\" | \"CENTER\" | \"RIGHT\" | \"JUSTIFIED\";\nexport type TextAlignVertical = \"TOP\" | \"CENTER\" | \"BOTTOM\";\nexport type TextCase = \"ORIGINAL\" | \"UPPER\" | \"LOWER\" | \"TITLE\";\nexport type TextDecoration = \"NONE\" | \"STRIKETHROUGH\" | \"UNDERLINE\";\n\nexport interface TypeStyle {\n fontFamily: string;\n fontPostScriptName?: string;\n paragraphSpacing?: number;\n paragraphIndent?: number;\n italic?: boolean;\n fontWeight: number;\n fontSize: number;\n textAlignHorizontal?: TextAlignHorizontal;\n textAlignVertical?: TextAlignVertical;\n letterSpacing?: number;\n fills?: Paint[];\n lineHeightPx?: number;\n lineHeightPercent?: number;\n lineHeightPercentFontSize?: number;\n lineHeightUnit?: \"PIXELS\" | \"FONT_SIZE_%\" | \"INTRINSIC_%\";\n}\n\nexport interface Component {\n key: string;\n name: string;\n description: string;\n componentSetId?: string;\n documentationLinks: string[];\n remote?: boolean;\n}\n\nexport interface ComponentSet {\n key: string;\n name: string;\n description: string;\n documentationLinks: string[];\n remote?: boolean;\n}\n\nexport type StyleType = \"FILL\" | \"TEXT\" | \"EFFECT\" | \"GRID\";\n\nexport interface Style {\n key: string;\n name: string;\n description: string;\n styleType: StyleType;\n remote?: boolean;\n}\n\nexport type ExportFormat = \"JPG\" | \"PNG\" | \"SVG\" | \"PDF\";\n\nexport interface ExportSettings {\n suffix: string;\n format: ExportFormat;\n constraint?: {\n type: \"SCALE\" | \"WIDTH\" | \"HEIGHT\";\n value: number;\n };\n}\n\nexport interface Comment {\n id: string;\n file_key: string;\n parent_id?: string;\n user: User;\n created_at: string;\n resolved_at?: string;\n message: string;\n client_meta: CommentClientMeta;\n order_id: string;\n}\n\nexport interface CommentClientMeta {\n x?: number;\n y?: number;\n node_id?: string[];\n node_offset?: Vector2D;\n}\n\nexport interface User {\n id: string;\n handle: string;\n img_url: string;\n email?: string;\n}\n\nexport interface FileResponse {\n document: Node;\n components: Record<string, Component>;\n componentSets: Record<string, ComponentSet>;\n schemaVersion: number;\n styles: Record<string, Style>;\n name: string;\n lastModified: string;\n thumbnailUrl: string;\n version: string;\n role: \"owner\" | \"editor\" | \"viewer\";\n editorType: \"figma\" | \"figjam\";\n linkAccess: \"view\" | \"edit\" | \"org_view\" | \"org_edit\";\n}\n\nexport interface NodeBase {\n id: string;\n name: string;\n visible?: boolean;\n type: NodeType;\n pluginData?: unknown;\n sharedPluginData?: unknown;\n locked?: boolean;\n}\n\nexport interface NodeWithChildren extends NodeBase {\n children: Node[];\n}\n\nexport interface DocumentNode extends NodeWithChildren {\n type: \"DOCUMENT\";\n}\n\nexport interface CanvasNode extends NodeWithChildren {\n type: \"CANVAS\";\n backgroundColor: Color;\n prototypeStartNodeID?: string;\n prototypeDevice?: {\n type: string;\n rotation: \"NONE\" | \"CCW_90\";\n };\n exportSettings?: ExportSettings[];\n}\n\nexport interface FrameNode extends NodeWithChildren {\n type: \"FRAME\";\n absoluteBoundingBox?: Rectangle;\n absoluteRenderBounds?: Rectangle;\n constraints?: LayoutConstraint;\n clipsContent?: boolean;\n background: Paint[];\n backgroundColor?: Color;\n fills?: Paint[];\n strokes?: Paint[];\n strokeWeight?: number;\n strokeAlign?: \"INSIDE\" | \"OUTSIDE\" | \"CENTER\";\n strokeDashes?: number[];\n cornerRadius?: number;\n rectangleCornerRadii?: [number, number, number, number];\n exportSettings?: ExportSettings[];\n blendMode?: BlendMode;\n preserveRatio?: boolean;\n layoutAlign?: LayoutAlign;\n layoutGrow?: number;\n layoutMode?: LayoutMode;\n primaryAxisSizingMode?: \"FIXED\" | \"AUTO\";\n counterAxisSizingMode?: \"FIXED\" | \"AUTO\";\n primaryAxisAlignItems?: LayoutAlign;\n counterAxisAlignItems?: LayoutAlign;\n paddingLeft?: number;\n paddingRight?: number;\n paddingTop?: number;\n paddingBottom?: number;\n itemSpacing?: number;\n layoutGrids?: LayoutGrid[];\n effects?: Effect[];\n isMask?: boolean;\n isMaskOutline?: boolean;\n transitionNodeID?: string;\n transitionDuration?: number;\n transitionEasing?: EasingType;\n opacity?: number;\n}\n\nexport interface GroupNode extends NodeWithChildren {\n type: \"GROUP\";\n absoluteBoundingBox?: Rectangle;\n absoluteRenderBounds?: Rectangle;\n constraints?: LayoutConstraint;\n clipsContent?: boolean;\n blendMode?: BlendMode;\n effects?: Effect[];\n opacity?: number;\n}\n\nexport type VectorNodeType =\n | \"VECTOR\"\n | \"BOOLEAN_OPERATION\"\n | \"STAR\"\n | \"LINE\"\n | \"ELLIPSE\"\n | \"REGULAR_POLYGON\"\n | \"RECTANGLE\";\n\nexport interface VectorNode extends NodeBase {\n type: VectorNodeType;\n absoluteBoundingBox?: Rectangle;\n absoluteRenderBounds?: Rectangle;\n constraints?: LayoutConstraint;\n fills?: Paint[];\n fillGeometry?: unknown[];\n strokes?: Paint[];\n strokeWeight?: number;\n strokeCap?: \"NONE\" | \"ROUND\" | \"SQUARE\" | \"LINE_ARROW\" | \"TRIANGLE_ARROW\";\n strokeJoin?: \"MITER\" | \"BEVEL\" | \"ROUND\";\n strokeDashes?: number[];\n strokeAlign?: \"INSIDE\" | \"OUTSIDE\" | \"CENTER\";\n strokeGeometry?: unknown[];\n cornerRadius?: number;\n rectangleCornerRadii?: [number, number, number, number];\n exportSettings?: ExportSettings[];\n blendMode?: BlendMode;\n preserveRatio?: boolean;\n layoutAlign?: LayoutAlign;\n layoutGrow?: number;\n effects?: Effect[];\n isMask?: boolean;\n opacity?: number;\n}\n\nexport interface TextNode extends NodeBase {\n type: \"TEXT\";\n absoluteBoundingBox?: Rectangle;\n absoluteRenderBounds?: Rectangle;\n constraints?: LayoutConstraint;\n fills?: Paint[];\n strokes?: Paint[];\n strokeWeight?: number;\n strokeAlign?: \"INSIDE\" | \"OUTSIDE\" | \"CENTER\";\n strokeDashes?: number[];\n exportSettings?: ExportSettings[];\n blendMode?: BlendMode;\n preserveRatio?: boolean;\n layoutAlign?: LayoutAlign;\n layoutGrow?: number;\n effects?: Effect[];\n characters: string;\n style: TypeStyle;\n characterStyleOverrides?: number[];\n styleOverrideTable?: Record<number, TypeStyle>;\n opacity?: number;\n}\n\nexport interface ComponentNode extends FrameNode {\n type: \"COMPONENT\";\n}\n\nexport interface ComponentSetNode extends FrameNode {\n type: \"COMPONENT_SET\";\n}\n\nexport interface InstanceNode extends FrameNode {\n type: \"INSTANCE\";\n componentId: string;\n overrides?: unknown[];\n}\n\nexport type Node =\n | DocumentNode\n | CanvasNode\n | FrameNode\n | GroupNode\n | VectorNode\n | TextNode\n | ComponentNode\n | ComponentSetNode\n | InstanceNode;\n\nexport interface Project {\n id: string;\n name: string;\n}\n\nexport interface FileReference {\n key: string;\n name: string;\n thumbnail_url: string;\n last_modified: string;\n}\n\nexport interface ProjectFilesResponse {\n files: FileReference[];\n}\n\nexport interface TeamProjectsResponse {\n projects: Project[];\n}\n\nexport interface Version {\n id: string;\n created_at: string;\n label?: string;\n description?: string;\n user: User;\n thumbnail_url?: string;\n}\n\nexport interface VersionsResponse {\n versions: Version[];\n pagination?: {\n next_page?: number;\n };\n}\n",
269
- "tools/get-comments.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { getComments } from \"../lib/figma-client.ts\";\n\ntype FormattedComment = {\n id: string;\n message: string;\n author: { handle: string; avatar: string };\n createdAt: string;\n resolvedAt: string | null;\n isResolved: boolean;\n parentId: string | null;\n isReply: boolean;\n location: { nodeIds: string; x: number; y: number } | null;\n};\n\ntype Output = {\n totalComments: number;\n unresolvedCount: number;\n resolvedCount: number;\n threads: Array<{\n rootComment: FormattedComment;\n replies: FormattedComment[];\n }>;\n fileUrl: string;\n};\n\nexport default tool({\n id: \"figma-get-comments\",\n description:\n \"Get all comments on a Figma file. Returns comment threads with messages, authors, timestamps, and resolution status.\",\n inputSchema: defineSchema((v) => v.object({\n fileKey: v.string().describe(\"The file key (from the Figma URL)\"),\n includeResolved: v.boolean().default(false).describe(\"Include resolved comments\"),\n limit: v\n .number()\n .min(1)\n .max(100)\n .default(50)\n .describe(\"Maximum number of comments to return\"),\n }))(),\n async execute({ fileKey, includeResolved, limit }): Promise<Output> {\n const response = await getComments(fileKey);\n\n const filteredComments = includeResolved\n ? response.comments\n : response.comments.filter((comment) => !comment.resolved_at);\n\n const comments = filteredComments.slice(0, limit);\n\n const formattedComments: FormattedComment[] = comments.map((comment) => ({\n id: comment.id,\n message: comment.message,\n author: {\n handle: comment.user.handle,\n avatar: comment.user.img_url,\n },\n createdAt: comment.created_at,\n resolvedAt: comment.resolved_at,\n isResolved: Boolean(comment.resolved_at),\n parentId: comment.parent_id,\n isReply: Boolean(comment.parent_id),\n location: comment.client_meta.node_id\n ? {\n nodeIds: comment.client_meta.node_id,\n x: comment.client_meta.x,\n y: comment.client_meta.y,\n }\n : null,\n }));\n\n const rootComments = formattedComments.filter((comment) => !comment.isReply);\n\n const repliesByParentId = new Map<string, FormattedComment[]>();\n for (const comment of formattedComments) {\n if (!comment.parentId) continue;\n const replies = repliesByParentId.get(comment.parentId);\n if (replies) replies.push(comment);\n else repliesByParentId.set(comment.parentId, [comment]);\n }\n\n const threads = rootComments.map((root) => ({\n rootComment: root,\n replies: repliesByParentId.get(root.id) ?? [],\n }));\n\n let unresolvedCount = 0;\n let resolvedCount = 0;\n\n for (const comment of comments) {\n if (comment.resolved_at) resolvedCount += 1;\n else unresolvedCount += 1;\n }\n\n return {\n totalComments: comments.length,\n unresolvedCount,\n resolvedCount,\n threads,\n fileUrl: `https://www.figma.com/file/${fileKey}`,\n };\n },\n});\n",
270
- "tools/get-file.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport {\n extractComponents,\n extractStyles,\n getFile,\n getFileSummary,\n} from \"../lib/figma-client.ts\";\n\nexport default tool({\n id: \"figma-get-file\",\n description:\n \"Get detailed information about a Figma file including components, styles, and structure. Returns file metadata, component list, and style information.\",\n inputSchema: defineSchema((v) => v.object({\n fileKey: v.string().describe(\"The file key (from the Figma URL)\"),\n includeComponents: v\n .boolean()\n .default(true)\n .describe(\"Include component information\"),\n includeStyles: v.boolean().default(true).describe(\"Include style information\"),\n depth: v\n .number()\n .min(1)\n .max(10)\n .optional()\n .describe(\"Depth of nodes to traverse (default: all)\"),\n }))(),\n async execute({ fileKey, includeComponents, includeStyles, depth }) {\n const file = await getFile(fileKey, { depth });\n\n return {\n summary: getFileSummary(file),\n url: `https://www.figma.com/file/${fileKey}`,\n thumbnailUrl: file.thumbnailUrl,\n pages: file.document.children?.map((page) => ({\n id: page.id,\n name: page.name,\n type: page.type,\n })) ?? [],\n ...(includeComponents ? { components: extractComponents(file) } : {}),\n ...(includeStyles ? { styles: extractStyles(file) } : {}),\n };\n },\n});\n",
271
- "tools/post-comment.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { postComment } from \"../lib/figma-client.ts\";\n\nexport default tool({\n id: \"figma-post-comment\",\n description:\n \"Post a comment on a Figma file. Can be a new comment or a reply to an existing comment thread.\",\n inputSchema: defineSchema((v) => v.object({\n fileKey: v.string().describe(\"The file key (from the Figma URL)\"),\n message: v.string().min(1).describe(\"The comment message to post\"),\n parentId: v\n .string()\n .optional()\n .describe(\"ID of parent comment to reply to (for threaded replies)\"),\n nodeId: v.string().optional().describe(\"ID of the Figma node to attach the comment to\"),\n x: v.number().optional().describe(\"X coordinate for comment placement (0-1, relative to canvas)\"),\n y: v.number().optional().describe(\"Y coordinate for comment placement (0-1, relative to canvas)\"),\n }))(),\n async execute({ fileKey, message, parentId, nodeId, x, y }) {\n const clientMeta: { x?: number; y?: number; node_id?: string[] } = {};\n\n if (x !== undefined) clientMeta.x = x;\n if (y !== undefined) clientMeta.y = y;\n if (nodeId) clientMeta.node_id = [nodeId];\n\n const comment = await postComment(fileKey, message, {\n client_meta: Object.keys(clientMeta).length ? clientMeta : undefined,\n parent_id: parentId,\n });\n\n return {\n success: true,\n comment: {\n id: comment.id,\n message: comment.message,\n author: {\n handle: comment.user.handle,\n avatar: comment.user.img_url,\n },\n createdAt: comment.created_at,\n isReply: Boolean(comment.parent_id),\n fileUrl: `https://www.figma.com/file/${fileKey}`,\n },\n };\n },\n});\n"
272
- }
273
- },
274
- "integration:github": {
275
- "files": {
276
- "app/api/auth/github/callback/route.ts": "import { createOAuthCallbackHandler, githubConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\n\nexport const GET = createOAuthCallbackHandler(githubConfig, { tokenStore });\n",
277
- "app/api/auth/github/route.ts": "import { createOAuthInitHandler, githubConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../lib/token-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../lib/user-id.ts\";\n\nexport const GET = createOAuthInitHandler(githubConfig, {\n tokenStore,\n getUserId: requireUserIdFromRequest,\n});\n",
278
- "lib/github-client.ts": "/**\n * GitHub API Client\n *\n * Provides a type-safe interface to GitHub API operations.\n */\n\nimport { getValidToken } from \"./oauth.ts\";\n\nfunction getEnv(key: string): string | undefined {\n // @ts-ignore - Deno global\n if (typeof Deno !== \"undefined\") return Deno.env.get(key);\n\n // @ts-ignore - process global\n if (typeof process !== \"undefined\" && process.env) return process.env[key];\n\n return undefined;\n}\n\nconst GITHUB_API_BASE = \"https://api.github.com\";\n\nexport interface GitHubUser {\n id: number;\n login: string;\n name: string | null;\n type: string;\n html_url: string;\n avatar_url: string;\n company: string | null;\n blog: string;\n location: string | null;\n email: string | null;\n bio: string | null;\n twitter_username: string | null;\n public_repos: number;\n followers: number;\n following: number;\n created_at: string;\n updated_at: string;\n}\n\nexport interface GitHubRepo {\n id: number;\n name: string;\n full_name: string;\n description: string | null;\n private: boolean;\n html_url: string;\n default_branch: string;\n language: string | null;\n stargazers_count: number;\n forks_count: number;\n open_issues_count: number;\n updated_at: string;\n}\n\nexport interface GitHubPullRequest {\n id: number;\n number: number;\n title: string;\n body: string | null;\n state: \"open\" | \"closed\";\n html_url: string;\n user: { login: string; avatar_url: string };\n created_at: string;\n updated_at: string;\n head: { ref: string; sha: string };\n base: { ref: string };\n mergeable: boolean | null;\n additions: number;\n deletions: number;\n changed_files: number;\n draft: boolean;\n labels: Array<{ name: string; color: string }>;\n}\n\nexport interface GitHubIssue {\n id: number;\n number: number;\n title: string;\n body: string | null;\n state: \"open\" | \"closed\";\n html_url: string;\n user: { login: string };\n created_at: string;\n updated_at: string;\n labels: Array<{ name: string; color: string }>;\n assignees: Array<{ login: string }>;\n}\n\nexport interface GitHubCommit {\n sha: string;\n commit: {\n message: string;\n author: { name: string; date: string };\n };\n html_url: string;\n author: { login: string; avatar_url: string } | null;\n}\n\nexport interface GitHubMergeResult {\n sha: string;\n merged: boolean;\n message: string;\n}\n\nexport interface GitHubUser {\n id: number;\n node_id: string;\n login: string;\n name: string | null;\n email: string | null;\n html_url: string;\n avatar_url: string;\n type: string;\n}\n\n/**\n * GitHub OAuth provider configuration\n */\nexport const githubOAuthProvider = {\n name: \"github\",\n authorizationUrl: \"https://github.com/login/oauth/authorize\",\n tokenUrl: \"https://github.com/login/oauth/access_token\",\n clientId: getEnv(\"GITHUB_CLIENT_ID\") ?? \"\",\n clientSecret: getEnv(\"GITHUB_CLIENT_SECRET\") ?? \"\",\n scopes: [\"repo\", \"read:user\", \"read:org\"],\n callbackPath: \"/api/auth/github/callback\",\n};\n\nexport function createGitHubClient(userId: string): {\n listRepos(options?: {\n sort?: \"created\" | \"updated\" | \"pushed\" | \"full_name\";\n perPage?: number;\n type?: \"all\" | \"owner\" | \"public\" | \"private\" | \"member\";\n }): Promise<GitHubRepo[]>;\n getRepo(owner: string, repo: string): Promise<GitHubRepo>;\n listPullRequests(\n owner: string,\n repo: string,\n options?: { state?: \"open\" | \"closed\" | \"all\"; perPage?: number },\n ): Promise<GitHubPullRequest[]>;\n getPullRequest(\n owner: string,\n repo: string,\n pullNumber: number,\n ): Promise<GitHubPullRequest>;\n getPullRequestDiff(\n owner: string,\n repo: string,\n pullNumber: number,\n ): Promise<string>;\n createIssue(\n owner: string,\n repo: string,\n options: {\n title: string;\n body?: string;\n labels?: string[];\n assignees?: string[];\n },\n ): Promise<GitHubIssue>;\n getIssue(\n owner: string,\n repo: string,\n issueNumber: number,\n ): Promise<GitHubIssue>;\n updateIssue(\n owner: string,\n repo: string,\n issueNumber: number,\n options: {\n title?: string;\n body?: string;\n state?: \"open\" | \"closed\";\n labels?: string[];\n assignees?: string[];\n },\n ): Promise<GitHubIssue>;\n addIssueComment(\n owner: string,\n repo: string,\n issueNumber: number,\n body: string,\n ): Promise<\n {\n id: number;\n html_url: string;\n body: string;\n user: { login: string };\n created_at: string;\n }\n >;\n listIssues(\n owner: string,\n repo: string,\n options?: { state?: \"open\" | \"closed\" | \"all\"; perPage?: number },\n ): Promise<GitHubIssue[]>;\n listCommits(\n owner: string,\n repo: string,\n options?: { sha?: string; path?: string; perPage?: number },\n ): Promise<GitHubCommit[]>;\n createPullRequest(\n owner: string,\n repo: string,\n options: {\n title: string;\n body?: string;\n head: string;\n base: string;\n draft?: boolean;\n },\n ): Promise<GitHubPullRequest>;\n mergePullRequest(\n owner: string,\n repo: string,\n pullNumber: number,\n options?: {\n commit_title?: string;\n commit_message?: string;\n merge_method?: \"merge\" | \"squash\" | \"rebase\";\n },\n ): Promise<GitHubMergeResult>;\n getUser(): Promise<GitHubUser>;\n getUserByUsername(username: string): Promise<GitHubUser>;\n} {\n async function getAccessToken(): Promise<string> {\n const token = await getValidToken(githubOAuthProvider, userId, \"github\");\n if (!token) {\n throw new Error(\n \"GitHub not connected. Please connect your GitHub account first.\",\n );\n }\n return token;\n }\n\n async function apiRequest<T>(\n endpoint: string,\n options: RequestInit = {},\n ): Promise<T> {\n const accessToken = await getAccessToken();\n\n const response = await fetch(`${GITHUB_API_BASE}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: \"application/vnd.github+json\",\n \"X-GitHub-Api-Version\": \"2022-11-28\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`GitHub API error: ${response.status} - ${error}`);\n }\n\n return response.json() as Promise<T>;\n }\n\n async function apiTextRequest(\n endpoint: string,\n accept: string,\n ): Promise<string> {\n const accessToken = await getAccessToken();\n\n const response = await fetch(`${GITHUB_API_BASE}${endpoint}`, {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: accept,\n \"X-GitHub-Api-Version\": \"2022-11-28\",\n },\n });\n\n if (!response.ok) throw new Error(`GitHub API error: ${response.status}`);\n\n return response.text();\n }\n\n function toQueryString(params: URLSearchParams): string {\n const query = params.toString();\n return query ? `?${query}` : \"\";\n }\n\n return {\n listRepos(options = {}): Promise<GitHubRepo[]> {\n const params = new URLSearchParams();\n if (options.sort) params.set(\"sort\", options.sort);\n if (options.perPage) params.set(\"per_page\", String(options.perPage));\n if (options.type) params.set(\"type\", options.type);\n\n return apiRequest<GitHubRepo[]>(`/user/repos${toQueryString(params)}`);\n },\n\n getRepo(owner, repo): Promise<GitHubRepo> {\n return apiRequest<GitHubRepo>(`/repos/${owner}/${repo}`);\n },\n\n listPullRequests(owner, repo, options = {}): Promise<GitHubPullRequest[]> {\n const params = new URLSearchParams();\n params.set(\"state\", options.state ?? \"open\");\n if (options.perPage) params.set(\"per_page\", String(options.perPage));\n\n return apiRequest<GitHubPullRequest[]>(\n `/repos/${owner}/${repo}/pulls${toQueryString(params)}`,\n );\n },\n\n getPullRequest(owner, repo, pullNumber): Promise<GitHubPullRequest> {\n return apiRequest<GitHubPullRequest>(\n `/repos/${owner}/${repo}/pulls/${pullNumber}`,\n );\n },\n\n getPullRequestDiff(owner, repo, pullNumber): Promise<string> {\n return apiTextRequest(\n `/repos/${owner}/${repo}/pulls/${pullNumber}`,\n \"application/vnd.github.diff\",\n );\n },\n\n createIssue(owner, repo, options): Promise<GitHubIssue> {\n return apiRequest<GitHubIssue>(`/repos/${owner}/${repo}/issues`, {\n method: \"POST\",\n body: JSON.stringify(options),\n });\n },\n\n getIssue(owner, repo, issueNumber): Promise<GitHubIssue> {\n return apiRequest<GitHubIssue>(\n `/repos/${owner}/${repo}/issues/${issueNumber}`,\n );\n },\n\n updateIssue(owner, repo, issueNumber, options): Promise<GitHubIssue> {\n return apiRequest<GitHubIssue>(\n `/repos/${owner}/${repo}/issues/${issueNumber}`,\n {\n method: \"PATCH\",\n body: JSON.stringify(options),\n },\n );\n },\n\n addIssueComment(owner, repo, issueNumber, body) {\n return apiRequest(\n `/repos/${owner}/${repo}/issues/${issueNumber}/comments`,\n {\n method: \"POST\",\n body: JSON.stringify({ body }),\n },\n );\n },\n\n listIssues(owner, repo, options = {}): Promise<GitHubIssue[]> {\n const params = new URLSearchParams();\n params.set(\"state\", options.state ?? \"open\");\n if (options.perPage) params.set(\"per_page\", String(options.perPage));\n\n return apiRequest<GitHubIssue[]>(\n `/repos/${owner}/${repo}/issues${toQueryString(params)}`,\n );\n },\n\n listCommits(owner, repo, options = {}): Promise<GitHubCommit[]> {\n const params = new URLSearchParams();\n if (options.sha) params.set(\"sha\", options.sha);\n if (options.path) params.set(\"path\", options.path);\n if (options.perPage) params.set(\"per_page\", String(options.perPage));\n\n return apiRequest<GitHubCommit[]>(\n `/repos/${owner}/${repo}/commits${toQueryString(params)}`,\n );\n },\n\n createPullRequest(owner, repo, options): Promise<GitHubPullRequest> {\n return apiRequest<GitHubPullRequest>(`/repos/${owner}/${repo}/pulls`, {\n method: \"POST\",\n body: JSON.stringify(options),\n });\n },\n\n mergePullRequest(owner, repo, pullNumber, options = {}): Promise<GitHubMergeResult> {\n return apiRequest<GitHubMergeResult>(\n `/repos/${owner}/${repo}/pulls/${pullNumber}/merge`,\n {\n method: \"PUT\",\n body: JSON.stringify(options),\n },\n );\n },\n\n getUser(): Promise<GitHubUser> {\n return apiRequest(\"/user\");\n },\n\n getUserByUsername(username): Promise<GitHubUser> {\n return apiRequest<GitHubUser>(`/users/${encodeURIComponent(username)}`);\n },\n };\n}\n\nexport type GitHubClient = ReturnType<typeof createGitHubClient>;\n",
279
- "tools/add-issue-comment.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGitHubClient } from \"../lib/github-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"github-add-issue-comment\",\n description: \"Add a comment to a GitHub issue or pull request\",\n inputSchema: defineSchema((v) =>\n v.object({\n repo: v.string().describe(\"Repository in format 'owner/repo'\"),\n issueNumber: v.number().int().positive().describe(\n \"Issue or pull request number\",\n ),\n body: v.string().min(1).describe(\"Comment body (supports Markdown)\"),\n })\n )(),\n execute: async ({ repo, issueNumber, body }, context) => {\n const userId = requireUserIdFromContext(context);\n const [owner, repoName] = repo.split(\"/\");\n if (!owner || !repoName) {\n return { error: \"Invalid repository format. Use 'owner/repo' format.\" };\n }\n\n try {\n const github = createGitHubClient(userId);\n const comment = await github.addIssueComment(\n owner,\n repoName,\n issueNumber,\n body,\n );\n return {\n success: true,\n comment: {\n id: comment.id,\n url: comment.html_url,\n body: comment.body,\n author: comment.user.login,\n createdAt: comment.created_at,\n },\n message: `Comment added to issue #${issueNumber} in ${repo}.`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"GitHub not connected. Please connect your GitHub account.\",\n connectUrl: \"/api/auth/github\",\n };\n }\n throw error;\n }\n },\n});\n",
280
- "tools/create-issue.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGitHubClient } from \"../lib/github-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"github-create-issue\",\n description: \"Create a new issue in a GitHub repository\",\n inputSchema: defineSchema((v) =>\n v.object({\n repo: v\n .string()\n .describe(\"Repository in format 'owner/repo' (e.g., 'facebook/react')\"),\n title: v.string().min(1).describe(\"Issue title\"),\n body: v\n .string()\n .optional()\n .describe(\"Issue body/description (supports Markdown)\"),\n labels: v.array(v.string()).optional().describe(\n \"Labels to add to the issue\",\n ),\n assignees: v\n .array(v.string())\n .optional()\n .describe(\"GitHub usernames to assign to the issue\"),\n })\n )(),\n execute: async ({ repo, title, body, labels, assignees }, context) => {\n const userId = requireUserIdFromContext(context);\n\n const [owner, repoName] = repo.split(\"/\");\n if (!owner || !repoName) {\n return { error: \"Invalid repository format. Use 'owner/repo' format.\" };\n }\n\n try {\n const github = createGitHubClient(userId);\n const issue = await github.createIssue(owner, repoName, {\n title,\n body,\n labels,\n assignees,\n });\n\n return {\n success: true,\n issue: {\n number: issue.number,\n title: issue.title,\n url: issue.html_url,\n state: issue.state,\n labels: issue.labels.map((l: { name: string }) => l.name),\n assignees: issue.assignees.map((a: { login: string }) => a.login),\n },\n message: `Issue #${issue.number} created successfully in ${repo}.`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"GitHub not connected. Please connect your GitHub account.\",\n connectUrl: \"/api/auth/github\",\n };\n }\n throw error;\n }\n },\n});\n",
281
- "tools/create-pr.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGitHubClient } from \"../lib/github-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"github-create-pr\",\n description: \"Create a new pull request in a GitHub repository\",\n inputSchema: defineSchema((v) =>\n v.object({\n repo: v\n .string()\n .describe(\"Repository in format 'owner/repo' (e.g., 'facebook/react')\"),\n title: v.string().min(1).describe(\"Pull request title\"),\n head: v\n .string()\n .describe(\n \"Branch to merge from (e.g., 'feature-branch' or 'owner:feature-branch')\",\n ),\n base: v.string().describe(\"Branch to merge into (e.g., 'main')\"),\n body: v\n .string()\n .optional()\n .describe(\"Pull request description (supports Markdown)\"),\n draft: v\n .boolean()\n .default(false)\n .optional()\n .describe(\"Create as a draft pull request\"),\n })\n )(),\n execute: async ({ repo, title, head, base, body, draft }, context) => {\n const userId = requireUserIdFromContext(context);\n const [owner, repoName] = repo.split(\"/\");\n\n if (!owner || !repoName) {\n return { error: \"Invalid repository format. Use 'owner/repo' format.\" };\n }\n\n try {\n const github = createGitHubClient(userId);\n const pr = await github.createPullRequest(owner, repoName, {\n title,\n head,\n base,\n body,\n draft,\n });\n\n return {\n success: true,\n number: pr.number,\n title: pr.title,\n url: pr.html_url,\n state: pr.state,\n isDraft: pr.draft,\n sourceBranch: pr.head.ref,\n targetBranch: pr.base.ref,\n message: `Pull request #${pr.number} created successfully in ${repo}.`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"GitHub not connected. Please connect your GitHub account.\",\n connectUrl: \"/api/auth/github\",\n };\n }\n throw error;\n }\n },\n});\n",
282
- "tools/get-current-user.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGitHubClient } from \"../lib/github-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"github-get-current-user\",\n description: \"Get the authenticated GitHub user identity\",\n inputSchema: defineSchema((v) => v.object({}))(),\n execute: async (_input, context) => {\n const userId = requireUserIdFromContext(context);\n\n try {\n const github = createGitHubClient(userId);\n const user = await github.getUser();\n\n return {\n user: {\n id: user.id,\n nodeId: user.node_id,\n login: user.login,\n name: user.name ?? null,\n email: user.email ?? null,\n url: user.html_url,\n avatarUrl: user.avatar_url,\n type: user.type,\n },\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"GitHub not connected. Please connect your GitHub account.\",\n connectUrl: \"/api/auth/github\",\n };\n }\n throw error;\n }\n },\n});\n",
283
- "tools/get-issue.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGitHubClient } from \"../lib/github-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"github-get-issue\",\n description: \"Get details of a GitHub issue\",\n inputSchema: defineSchema((v) =>\n v.object({\n repo: v.string().describe(\"Repository in format 'owner/repo'\"),\n issueNumber: v.number().int().positive().describe(\"Issue number\"),\n })\n )(),\n execute: async ({ repo, issueNumber }, context) => {\n const userId = requireUserIdFromContext(context);\n const [owner, repoName] = repo.split(\"/\");\n if (!owner || !repoName) {\n return { error: \"Invalid repository format. Use 'owner/repo' format.\" };\n }\n\n try {\n const github = createGitHubClient(userId);\n const issue = await github.getIssue(owner, repoName, issueNumber);\n return {\n issue: {\n number: issue.number,\n title: issue.title,\n body: issue.body,\n state: issue.state,\n url: issue.html_url,\n author: issue.user.login,\n labels: issue.labels.map((label: { name: string }) => label.name),\n assignees: issue.assignees.map((assignee: { login: string }) =>\n assignee.login\n ),\n updatedAt: issue.updated_at,\n },\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"GitHub not connected. Please connect your GitHub account.\",\n connectUrl: \"/api/auth/github\",\n };\n }\n throw error;\n }\n },\n});\n",
284
- "tools/get-pr-diff.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGitHubClient } from \"../lib/github-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"github-get-pr-diff\",\n description: \"Get the diff for a pull request to review code changes\",\n inputSchema: defineSchema((v) =>\n v.object({\n repo: v\n .string()\n .describe(\"Repository in format 'owner/repo' (e.g., 'facebook/react')\"),\n prNumber: v.number().int().positive().describe(\"Pull request number\"),\n })\n )(),\n execute: async ({ repo, prNumber }, context) => {\n const userId = requireUserIdFromContext(context);\n\n const [owner, repoName] = repo.split(\"/\");\n if (!owner || !repoName) {\n return { error: \"Invalid repository format. Use 'owner/repo' format.\" };\n }\n\n try {\n const github = createGitHubClient(userId);\n\n const pr = await github.getPullRequest(owner, repoName, prNumber);\n const diff = await github.getPullRequestDiff(owner, repoName, prNumber);\n\n return {\n pullRequest: {\n number: pr.number,\n title: pr.title,\n author: pr.user.login,\n url: pr.html_url,\n sourceBranch: pr.head.ref,\n targetBranch: pr.base.ref,\n additions: pr.additions,\n deletions: pr.deletions,\n changedFiles: pr.changed_files,\n isDraft: pr.draft,\n state: pr.state,\n },\n diff,\n stats: {\n additions: pr.additions,\n deletions: pr.deletions,\n changedFiles: pr.changed_files,\n },\n message:\n `Retrieved diff for PR #${prNumber} (${pr.additions} additions, ${pr.deletions} deletions across ${pr.changed_files} files).`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"GitHub not connected. Please connect your GitHub account.\",\n connectUrl: \"/api/auth/github\",\n };\n }\n throw error;\n }\n },\n});\n",
285
- "tools/get-pr.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGitHubClient } from \"../lib/github-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"github-get-pr\",\n description: \"Get details of a specific GitHub pull request\",\n inputSchema: defineSchema((v) =>\n v.object({\n repo: v\n .string()\n .describe(\"Repository in format 'owner/repo' (e.g., 'facebook/react')\"),\n pull_number: v.number().int().positive().describe(\"Pull request number\"),\n })\n )(),\n execute: async ({ repo, pull_number }, context) => {\n const userId = requireUserIdFromContext(context);\n const [owner, repoName] = repo.split(\"/\");\n\n if (!owner || !repoName) {\n return { error: \"Invalid repository format. Use 'owner/repo' format.\" };\n }\n\n try {\n const github = createGitHubClient(userId);\n const pr = await github.getPullRequest(owner, repoName, pull_number);\n\n return {\n number: pr.number,\n title: pr.title,\n body: pr.body,\n state: pr.state,\n isDraft: pr.draft,\n url: pr.html_url,\n author: pr.user.login,\n createdAt: pr.created_at,\n updatedAt: pr.updated_at,\n sourceBranch: pr.head.ref,\n targetBranch: pr.base.ref,\n mergeable: pr.mergeable,\n additions: pr.additions,\n deletions: pr.deletions,\n changedFiles: pr.changed_files,\n labels: pr.labels.map(({ name }) => name),\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"GitHub not connected. Please connect your GitHub account.\",\n connectUrl: \"/api/auth/github\",\n };\n }\n throw error;\n }\n },\n});\n",
286
- "tools/get-repo.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGitHubClient } from \"../lib/github-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"github-get-repo\",\n description: \"Get details of a GitHub repository\",\n inputSchema: defineSchema((v) =>\n v.object({\n repo: v\n .string()\n .describe(\"Repository in format 'owner/repo' (e.g., 'facebook/react')\"),\n })\n )(),\n execute: async ({ repo }, context) => {\n const userId = requireUserIdFromContext(context);\n const [owner, repoName] = repo.split(\"/\");\n\n if (!owner || !repoName) {\n return { error: \"Invalid repository format. Use 'owner/repo' format.\" };\n }\n\n try {\n const github = createGitHubClient(userId);\n const result = await github.getRepo(owner, repoName);\n\n return {\n repository: {\n name: result.name,\n fullName: result.full_name,\n description: result.description ?? null,\n isPrivate: result.private,\n url: result.html_url,\n defaultBranch: result.default_branch,\n language: result.language,\n stars: result.stargazers_count,\n forks: result.forks_count,\n openIssues: result.open_issues_count,\n updatedAt: result.updated_at,\n },\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"GitHub not connected. Please connect your GitHub account.\",\n connectUrl: \"/api/auth/github\",\n };\n }\n throw error;\n }\n },\n});\n",
287
- "tools/get-user.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGitHubClient } from \"../lib/github-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"github-get-user\",\n description: \"Get a GitHub user profile by username\",\n inputSchema: defineSchema((v) =>\n v.object({\n username: v.string().describe(\"GitHub username/login to look up\"),\n })\n )(),\n execute: async ({ username }, context) => {\n const userId = requireUserIdFromContext(context);\n\n try {\n const github = createGitHubClient(userId);\n const result = await github.getUserByUsername(username);\n\n return {\n user: {\n login: result.login,\n id: result.id,\n name: result.name ?? null,\n type: result.type,\n url: result.html_url,\n avatarUrl: result.avatar_url,\n company: result.company ?? null,\n blog: result.blog || null,\n location: result.location ?? null,\n email: result.email ?? null,\n bio: result.bio ?? null,\n twitterUsername: result.twitter_username ?? null,\n publicRepos: result.public_repos,\n followers: result.followers,\n following: result.following,\n createdAt: result.created_at,\n updatedAt: result.updated_at,\n },\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"GitHub not connected. Please connect your GitHub account.\",\n connectUrl: \"/api/auth/github\",\n };\n }\n throw error;\n }\n },\n});\n",
288
- "tools/list-commits.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGitHubClient, type GitHubCommit } from \"../lib/github-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"github-list-commits\",\n description: \"List commits for a repository, branch, or file path\",\n inputSchema: defineSchema((v) =>\n v.object({\n repo: v\n .string()\n .describe(\"Repository in format 'owner/repo' (e.g., 'facebook/react')\"),\n sha: v\n .string()\n .optional()\n .describe(\"Branch name or commit SHA to list commits from\"),\n path: v\n .string()\n .optional()\n .describe(\"Only include commits that touch this file path\"),\n limit: v\n .number()\n .min(1)\n .max(100)\n .default(30)\n .describe(\"Maximum number of commits to return\"),\n })\n )(),\n execute: async ({ repo, sha, path, limit }, context) => {\n const userId = requireUserIdFromContext(context);\n const [owner, repoName] = repo.split(\"/\");\n\n if (!owner || !repoName) {\n return { error: \"Invalid repository format. Use 'owner/repo' format.\" };\n }\n\n try {\n const github = createGitHubClient(userId);\n const commits = await github.listCommits(owner, repoName, {\n sha,\n path,\n perPage: limit,\n });\n\n return {\n commits: commits.map((c: GitHubCommit) => ({\n sha: c.sha.slice(0, 7),\n message: c.commit.message.split(\"\\n\")[0],\n author: c.author?.login ?? c.commit.author.name,\n date: c.commit.author.date,\n url: c.html_url,\n })),\n count: commits.length,\n repository: repo,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"GitHub not connected. Please connect your GitHub account.\",\n connectUrl: \"/api/auth/github\",\n };\n }\n throw error;\n }\n },\n});\n",
289
- "tools/list-issues.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGitHubClient } from \"../lib/github-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\ntype GitHubIssueListItem = {\n number: number;\n title: string;\n body: string | null;\n state: string;\n html_url: string;\n user: { login: string };\n created_at: string;\n updated_at: string;\n labels: Array<{ name: string }>;\n assignees: Array<{ login: string }>;\n};\n\nexport default tool({\n id: \"github-list-issues\",\n description: \"List issues for a GitHub repository\",\n inputSchema: defineSchema((v) =>\n v.object({\n repo: v\n .string()\n .describe(\"Repository in format 'owner/repo' (e.g., 'facebook/react')\"),\n state: v\n .enum([\"open\", \"closed\", \"all\"])\n .default(\"open\")\n .describe(\"State of issues to list\"),\n limit: v\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of issues to return\"),\n })\n )(),\n execute: async ({ repo, state, limit }, context) => {\n const userId = requireUserIdFromContext(context);\n const [owner, repoName] = repo.split(\"/\");\n\n if (!owner || !repoName) {\n return { error: \"Invalid repository format. Use 'owner/repo' format.\" };\n }\n\n try {\n const github = createGitHubClient(userId);\n const issues = await github.listIssues(owner, repoName, {\n state,\n perPage: limit,\n });\n\n return {\n issues: issues.map((issue: GitHubIssueListItem) => ({\n number: issue.number,\n title: issue.title,\n body: issue.body,\n state: issue.state,\n url: issue.html_url,\n author: issue.user.login,\n labels: issue.labels.map((label) => label.name),\n assignees: issue.assignees.map((assignee) => assignee.login),\n createdAt: issue.created_at,\n updatedAt: issue.updated_at,\n })),\n count: issues.length,\n repository: repo,\n message: `Found ${issues.length} ${state} issue(s) in ${repo}.`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"GitHub not connected. Please connect your GitHub account.\",\n connectUrl: \"/api/auth/github\",\n };\n }\n throw error;\n }\n },\n});\n",
290
- "tools/list-prs.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGitHubClient } from \"../lib/github-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\ntype PullRequest = {\n number: number;\n title: string;\n state: string;\n draft: boolean;\n html_url: string;\n user: { login: string };\n created_at: string;\n updated_at: string;\n head: { ref: string };\n base: { ref: string };\n additions: number;\n deletions: number;\n changed_files: number;\n labels: Array<{ name: string }>;\n};\n\nexport default tool({\n id: \"github-list-prs\",\n description: \"List pull requests for a GitHub repository\",\n inputSchema: defineSchema((v) =>\n v.object({\n repo: v\n .string()\n .describe(\"Repository in format 'owner/repo' (e.g., 'facebook/react')\"),\n state: v\n .enum([\"open\", \"closed\", \"all\"])\n .default(\"open\")\n .describe(\"State of pull requests to list\"),\n limit: v\n .number()\n .min(1)\n .max(100)\n .default(10)\n .describe(\"Maximum number of pull requests to return\"),\n })\n )(),\n execute: async ({ repo, state, limit }, context) => {\n const userId = requireUserIdFromContext(context);\n const [owner, repoName] = repo.split(\"/\");\n\n if (!owner || !repoName) {\n return { error: \"Invalid repository format. Use 'owner/repo' format.\" };\n }\n\n try {\n const github = createGitHubClient(userId);\n const prs = await github.listPullRequests(owner, repoName, {\n state,\n perPage: limit,\n });\n\n return {\n pullRequests: prs.map((pr: PullRequest) => ({\n number: pr.number,\n title: pr.title,\n state: pr.state,\n isDraft: pr.draft,\n url: pr.html_url,\n author: pr.user.login,\n createdAt: pr.created_at,\n updatedAt: pr.updated_at,\n sourceBranch: pr.head.ref,\n targetBranch: pr.base.ref,\n additions: pr.additions,\n deletions: pr.deletions,\n changedFiles: pr.changed_files,\n labels: pr.labels.map(({ name }) => name),\n })),\n count: prs.length,\n repository: repo,\n message: `Found ${prs.length} ${state} pull request(s) in ${repo}.`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"GitHub not connected. Please connect your GitHub account.\",\n connectUrl: \"/api/auth/github\",\n };\n }\n throw error;\n }\n },\n});\n",
291
- "tools/list-repos.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGitHubClient } from \"../lib/github-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\ntype GitHubRepo = {\n name: string;\n full_name: string;\n description: string | null;\n private: boolean;\n html_url: string;\n default_branch: string;\n language: string | null;\n stargazers_count: number;\n forks_count: number;\n open_issues_count: number;\n updated_at: string;\n};\n\nexport default tool({\n id: \"github-list-repos\",\n description: \"List GitHub repositories for the authenticated user\",\n inputSchema: defineSchema((v) =>\n v.object({\n type: v\n .enum([\"all\", \"owner\", \"public\", \"private\", \"member\"])\n .default(\"all\")\n .describe(\"Type of repositories to list\"),\n sort: v\n .enum([\"created\", \"updated\", \"pushed\", \"full_name\"])\n .default(\"updated\")\n .describe(\"How to sort the repositories\"),\n limit: v\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of repositories to return\"),\n })\n )(),\n execute: async ({ type, sort, limit }, context) => {\n const userId = requireUserIdFromContext(context);\n\n try {\n const github = createGitHubClient(userId);\n const repos = await github.listRepos({ type, sort, perPage: limit });\n\n return {\n repositories: repos.map((repo: GitHubRepo) => ({\n name: repo.name,\n fullName: repo.full_name,\n description: repo.description ?? null,\n isPrivate: repo.private,\n url: repo.html_url,\n defaultBranch: repo.default_branch,\n language: repo.language,\n stars: repo.stargazers_count,\n forks: repo.forks_count,\n openIssues: repo.open_issues_count,\n updatedAt: repo.updated_at,\n })),\n count: repos.length,\n message: `Found ${repos.length} repository(s).`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"GitHub not connected. Please connect your GitHub account.\",\n connectUrl: \"/api/auth/github\",\n };\n }\n throw error;\n }\n },\n});\n",
292
- "tools/merge-pr.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGitHubClient } from \"../lib/github-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"github-merge-pr\",\n description: \"Merge an open GitHub pull request\",\n inputSchema: defineSchema((v) =>\n v.object({\n repo: v\n .string()\n .describe(\"Repository in format 'owner/repo' (e.g., 'facebook/react')\"),\n pull_number: v\n .number()\n .int()\n .positive()\n .describe(\"Pull request number to merge\"),\n merge_method: v\n .enum([\"merge\", \"squash\", \"rebase\"])\n .default(\"merge\")\n .optional()\n .describe(\"Merge method: merge, squash, or rebase\"),\n commit_title: v\n .string()\n .optional()\n .describe(\"Title for the merge commit\"),\n commit_message: v\n .string()\n .optional()\n .describe(\"Extra detail for the merge commit message\"),\n })\n )(),\n execute: async (\n { repo, pull_number, merge_method, commit_title, commit_message },\n context,\n ) => {\n const userId = requireUserIdFromContext(context);\n const [owner, repoName] = repo.split(\"/\");\n\n if (!owner || !repoName) {\n return { error: \"Invalid repository format. Use 'owner/repo' format.\" };\n }\n\n try {\n const github = createGitHubClient(userId);\n const result = await github.mergePullRequest(owner, repoName, pull_number, {\n merge_method,\n commit_title,\n commit_message,\n });\n\n return {\n success: result.merged,\n sha: result.sha,\n message: result.message,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"GitHub not connected. Please connect your GitHub account.\",\n connectUrl: \"/api/auth/github\",\n };\n }\n throw error;\n }\n },\n});\n",
293
- "tools/update-issue.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGitHubClient } from \"../lib/github-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"github-update-issue\",\n description: \"Update, close, or reopen a GitHub issue\",\n inputSchema: defineSchema((v) =>\n v.object({\n repo: v.string().describe(\"Repository in format 'owner/repo'\"),\n issueNumber: v.number().int().positive().describe(\"Issue number\"),\n title: v.string().optional().describe(\"Updated issue title\"),\n body: v.string().optional().describe(\"Updated issue body\"),\n state: v.enum([\"open\", \"closed\"]).optional().describe(\"Issue state\"),\n labels: v.array(v.string()).optional().describe(\n \"Replacement label names\",\n ),\n assignees: v.array(v.string()).optional().describe(\n \"Replacement assignee usernames\",\n ),\n })\n )(),\n execute: async (\n { repo, issueNumber, title, body, state, labels, assignees },\n context,\n ) => {\n const userId = requireUserIdFromContext(context);\n const [owner, repoName] = repo.split(\"/\");\n if (!owner || !repoName) {\n return { error: \"Invalid repository format. Use 'owner/repo' format.\" };\n }\n\n try {\n const github = createGitHubClient(userId);\n const issue = await github.updateIssue(owner, repoName, issueNumber, {\n title,\n body,\n state,\n labels,\n assignees,\n });\n return {\n success: true,\n issue: {\n number: issue.number,\n title: issue.title,\n state: issue.state,\n url: issue.html_url,\n labels: issue.labels.map((label: { name: string }) => label.name),\n assignees: issue.assignees.map((assignee: { login: string }) =>\n assignee.login\n ),\n },\n message: `Issue #${issue.number} updated successfully in ${repo}.`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"GitHub not connected. Please connect your GitHub account.\",\n connectUrl: \"/api/auth/github\",\n };\n }\n throw error;\n }\n },\n});\n"
294
- }
295
- },
296
- "integration:gitlab": {
297
- "files": {
298
- ".env.example": "# GitLab OAuth Configuration\n# Create a new application at: https://gitlab.com/-/profile/applications\n# Set the redirect URI to: http://localhost:3000/api/auth/gitlab/callback\n# (Update the URL for production)\n\nGITLAB_CLIENT_ID=your_gitlab_application_id\nGITLAB_CLIENT_SECRET=your_gitlab_application_secret\n",
299
- "app/api/auth/gitlab/callback/route.ts": "import { createOAuthCallbackHandler, gitlabConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\n\nexport const GET = createOAuthCallbackHandler(gitlabConfig, { tokenStore });\n",
300
- "app/api/auth/gitlab/route.ts": "import { createOAuthInitHandler, gitlabConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../lib/token-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../lib/user-id.ts\";\n\nexport const GET = createOAuthInitHandler(gitlabConfig, {\n tokenStore,\n getUserId: requireUserIdFromRequest,\n});\n",
301
- "lib/gitlab-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst GITLAB_BASE_URL = \"https://gitlab.com/api/v4\";\n\nexport interface GitLabProject {\n id: number;\n name: string;\n name_with_namespace: string;\n description: string | null;\n web_url: string;\n path_with_namespace: string;\n default_branch: string;\n visibility: \"private\" | \"internal\" | \"public\";\n created_at: string;\n last_activity_at: string;\n}\n\nexport interface GitLabIssue {\n id: number;\n iid: number;\n project_id: number;\n title: string;\n description: string | null;\n state: \"opened\" | \"closed\";\n created_at: string;\n updated_at: string;\n closed_at: string | null;\n labels: string[];\n milestone: {\n id: number;\n title: string;\n } | null;\n assignees: Array<{\n id: number;\n username: string;\n name: string;\n avatar_url: string;\n }>;\n author: {\n id: number;\n username: string;\n name: string;\n avatar_url: string;\n };\n web_url: string;\n time_stats: {\n time_estimate: number;\n total_time_spent: number;\n };\n}\n\nexport interface GitLabMergeRequest {\n id: number;\n iid: number;\n project_id: number;\n title: string;\n description: string | null;\n state: \"opened\" | \"closed\" | \"merged\";\n created_at: string;\n updated_at: string;\n merged_at: string | null;\n closed_at: string | null;\n target_branch: string;\n source_branch: string;\n author: {\n id: number;\n username: string;\n name: string;\n avatar_url: string;\n };\n assignees: Array<{\n id: number;\n username: string;\n name: string;\n avatar_url: string;\n }>;\n reviewers: Array<{\n id: number;\n username: string;\n name: string;\n avatar_url: string;\n }>;\n labels: string[];\n draft: boolean;\n web_url: string;\n changes_count: string;\n diff_refs: {\n base_sha: string;\n head_sha: string;\n start_sha: string;\n };\n}\n\nexport interface GitLabUser {\n id: number;\n username: string;\n name: string;\n email: string;\n avatar_url: string;\n web_url: string;\n}\n\nexport interface GitLabNote {\n id: number;\n body: string;\n author: { id: number; username: string; name: string; avatar_url: string };\n created_at: string;\n updated_at: string;\n system: boolean;\n confidential?: boolean;\n internal?: boolean;\n}\n\nfunction encodeProjectId(projectId: number | string): number | string {\n return typeof projectId === \"string\"\n ? encodeURIComponent(projectId)\n : projectId;\n}\n\nfunction buildQuery(params: URLSearchParams): string {\n const query = params.toString();\n return query ? `?${query}` : \"\";\n}\n\nasync function gitlabFetch<T>(\n endpoint: string,\n options: RequestInit = {},\n): Promise<T> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\n \"Not authenticated with GitLab. Please connect your account.\",\n );\n }\n\n const response = await fetch(`${GITLAB_BASE_URL}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({}))) as {\n message?: string;\n error?: string;\n };\n\n const message = error.message ?? error.error ?? response.statusText;\n throw new Error(`GitLab API error: ${response.status} ${message}`);\n }\n\n return (await response.json()) as T;\n}\n\nexport function getCurrentUser(): Promise<GitLabUser> {\n return gitlabFetch<GitLabUser>(\"/user\");\n}\n\nexport function listProjects(options?: {\n membership?: boolean;\n search?: string;\n orderBy?: \"id\" | \"name\" | \"created_at\" | \"updated_at\" | \"last_activity_at\";\n sort?: \"asc\" | \"desc\";\n perPage?: number;\n}): Promise<GitLabProject[]> {\n const params = new URLSearchParams();\n\n if (options?.membership !== false) params.set(\"membership\", \"true\");\n if (options?.search) params.set(\"search\", options.search);\n if (options?.orderBy) params.set(\"order_by\", options.orderBy);\n if (options?.sort) params.set(\"sort\", options.sort);\n if (options?.perPage) params.set(\"per_page\", options.perPage.toString());\n\n return gitlabFetch<GitLabProject[]>(`/projects${buildQuery(params)}`);\n}\n\nexport function getProject(projectId: number | string): Promise<GitLabProject> {\n return gitlabFetch<GitLabProject>(`/projects/${encodeProjectId(projectId)}`);\n}\n\nexport function searchIssues(options: {\n scope?: \"created_by_me\" | \"assigned_to_me\" | \"all\";\n state?: \"opened\" | \"closed\" | \"all\";\n labels?: string[];\n search?: string;\n projectId?: number | string;\n perPage?: number;\n}): Promise<GitLabIssue[]> {\n const params = new URLSearchParams();\n\n if (options.scope) params.set(\"scope\", options.scope);\n if (options.state) params.set(\"state\", options.state);\n if (options.labels?.length) params.set(\"labels\", options.labels.join(\",\"));\n if (options.search) params.set(\"search\", options.search);\n if (options.perPage) params.set(\"per_page\", options.perPage.toString());\n\n const base = options.projectId\n ? `/projects/${encodeProjectId(options.projectId)}/issues`\n : \"/issues\";\n\n return gitlabFetch<GitLabIssue[]>(`${base}${buildQuery(params)}`);\n}\n\nexport function getIssue(\n projectId: number | string,\n issueIid: number,\n): Promise<GitLabIssue> {\n return gitlabFetch<GitLabIssue>(\n `/projects/${encodeProjectId(projectId)}/issues/${issueIid}`,\n );\n}\n\nexport function createIssue(\n projectId: number | string,\n options: {\n title: string;\n description?: string;\n labels?: string[];\n assigneeIds?: number[];\n milestoneId?: number;\n dueDate?: string;\n },\n): Promise<GitLabIssue> {\n const body: Record<string, unknown> = { title: options.title };\n\n if (options.description) body.description = options.description;\n if (options.labels?.length) body.labels = options.labels.join(\",\");\n if (options.assigneeIds?.length) body.assignee_ids = options.assigneeIds;\n if (options.milestoneId) body.milestone_id = options.milestoneId;\n if (options.dueDate) body.due_date = options.dueDate;\n\n return gitlabFetch<GitLabIssue>(\n `/projects/${encodeProjectId(projectId)}/issues`,\n {\n method: \"POST\",\n body: JSON.stringify(body),\n },\n );\n}\n\nexport function updateIssue(\n projectId: number | string,\n issueIid: number,\n options: {\n title?: string;\n description?: string;\n state?: \"opened\" | \"closed\";\n labels?: string[];\n assigneeIds?: number[];\n },\n): Promise<GitLabIssue> {\n const body: Record<string, unknown> = {};\n\n if (options.title) body.title = options.title;\n if (options.description !== undefined) body.description = options.description;\n if (options.state) {\n body.state_event = options.state === \"closed\" ? \"close\" : \"reopen\";\n }\n if (options.labels) body.labels = options.labels.join(\",\");\n if (options.assigneeIds) body.assignee_ids = options.assigneeIds;\n\n return gitlabFetch<GitLabIssue>(\n `/projects/${encodeProjectId(projectId)}/issues/${issueIid}`,\n {\n method: \"PUT\",\n body: JSON.stringify(body),\n },\n );\n}\n\nexport function addIssueComment(\n projectId: number | string,\n issueIid: number,\n options: { body: string; confidential?: boolean },\n): Promise<GitLabNote> {\n const body: Record<string, unknown> = { body: options.body };\n if (options.confidential !== undefined) {\n body.confidential = options.confidential;\n }\n\n return gitlabFetch<GitLabNote>(\n `/projects/${encodeProjectId(projectId)}/issues/${issueIid}/notes`,\n {\n method: \"POST\",\n body: JSON.stringify(body),\n },\n );\n}\n\nexport function listMergeRequests(options?: {\n scope?: \"created_by_me\" | \"assigned_to_me\" | \"all\";\n state?: \"opened\" | \"closed\" | \"merged\" | \"all\";\n labels?: string[];\n projectId?: number | string;\n perPage?: number;\n}): Promise<GitLabMergeRequest[]> {\n const params = new URLSearchParams();\n\n if (options?.scope) params.set(\"scope\", options.scope);\n if (options?.state) params.set(\"state\", options.state);\n if (options?.labels?.length) params.set(\"labels\", options.labels.join(\",\"));\n if (options?.perPage) params.set(\"per_page\", options.perPage.toString());\n\n const base = options?.projectId\n ? `/projects/${encodeProjectId(options.projectId)}/merge_requests`\n : \"/merge_requests\";\n\n return gitlabFetch<GitLabMergeRequest[]>(`${base}${buildQuery(params)}`);\n}\n\nexport function getMergeRequest(\n projectId: number | string,\n mrIid: number,\n): Promise<GitLabMergeRequest> {\n return gitlabFetch<GitLabMergeRequest>(\n `/projects/${encodeProjectId(projectId)}/merge_requests/${mrIid}`,\n );\n}\n\nexport function addMergeRequestComment(\n projectId: number | string,\n mrIid: number,\n options: { body: string; internal?: boolean },\n): Promise<GitLabNote> {\n const body: Record<string, unknown> = { body: options.body };\n if (options.internal !== undefined) body.internal = options.internal;\n\n return gitlabFetch<GitLabNote>(\n `/projects/${encodeProjectId(projectId)}/merge_requests/${mrIid}/notes`,\n {\n method: \"POST\",\n body: JSON.stringify(body),\n },\n );\n}\n\nexport function formatIssueForDisplay(issue: GitLabIssue): string {\n const assignees = issue.assignees.map((a) => `@${a.username}`).join(\", \");\n const labels = issue.labels.length ? `[${issue.labels.join(\", \")}]` : \"\";\n\n return `#${issue.iid}: ${issue.title} ${labels}\nState: ${issue.state}\nAssignees: ${assignees || \"None\"}\nCreated: ${new Date(issue.created_at).toLocaleDateString()}\nURL: ${issue.web_url}`;\n}\n\nexport function formatMergeRequestForDisplay(mr: GitLabMergeRequest): string {\n const assignees = mr.assignees.map((a) => `@${a.username}`).join(\", \");\n const reviewers = mr.reviewers.map((r) => `@${r.username}`).join(\", \");\n const labels = mr.labels.length ? `[${mr.labels.join(\", \")}]` : \"\";\n\n return `!${mr.iid}: ${mr.title} ${labels}\nState: ${mr.state}${mr.draft ? \" (Draft)\" : \"\"}\nSource: ${mr.source_branch} → Target: ${mr.target_branch}\nAuthor: @${mr.author.username}\nAssignees: ${assignees || \"None\"}\nReviewers: ${reviewers || \"None\"}\nCreated: ${new Date(mr.created_at).toLocaleDateString()}\nURL: ${mr.web_url}`;\n}\n",
302
- "tools/add-issue-comment.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { addIssueComment } from \"../lib/gitlab-client.ts\";\n\nexport default tool({\n id: \"gitlab-add-issue-comment\",\n description: \"Add a Markdown comment/note to a GitLab issue.\",\n inputSchema: defineSchema((v) =>\n v.object({\n projectId: v\n .union([v.number(), v.string()])\n .describe('Project ID or path (e.g., \"gitlab-org/gitlab\" or 278964)'),\n issueIid: v\n .number()\n .describe(\n \"Issue IID (the project-local number shown in the issue URL)\",\n ),\n body: v.string().min(1).describe(\"Comment body in Markdown\"),\n confidential: v.boolean().optional().describe(\n \"Make the note confidential\",\n ),\n })\n )(),\n async execute({ projectId, issueIid, body, confidential }) {\n const note = await addIssueComment(projectId, issueIid, {\n body,\n confidential,\n });\n\n return {\n success: true,\n message: `Comment added to issue #${issueIid}.`,\n note,\n };\n },\n});\n",
303
- "tools/add-merge-request-comment.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { addMergeRequestComment } from \"../lib/gitlab-client.ts\";\n\nexport default tool({\n id: \"gitlab-add-merge-request-comment\",\n description: \"Add a Markdown comment/note to a GitLab merge request.\",\n inputSchema: defineSchema((v) =>\n v.object({\n projectId: v\n .union([v.number(), v.string()])\n .describe('Project ID or path (e.g., \"gitlab-org/gitlab\" or 278964)'),\n mergeRequestIid: v\n .number()\n .describe(\n \"Merge request IID (the project-local number shown in the MR URL)\",\n ),\n body: v.string().min(1).describe(\"Comment body in Markdown\"),\n internal: v.boolean().optional().describe(\n \"Make the note internal when supported\",\n ),\n })\n )(),\n async execute({ projectId, mergeRequestIid, body, internal }) {\n const note = await addMergeRequestComment(projectId, mergeRequestIid, {\n body,\n internal,\n });\n\n return {\n success: true,\n message: `Comment added to merge request !${mergeRequestIid}.`,\n note,\n };\n },\n});\n",
304
- "tools/create-issue.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createIssue } from \"../lib/gitlab-client.ts\";\n\nexport default tool({\n id: \"gitlab-create-issue\",\n description:\n \"Create a new issue in a GitLab project. Can set title, description, labels, assignees, milestone, and due date.\",\n inputSchema: defineSchema((v) =>\n v.object({\n projectId: v\n .union([v.number(), v.string()])\n .describe('Project ID or path (e.g., \"gitlab-org/gitlab\" or 278964)'),\n title: v.string().min(1).describe(\"Issue title\"),\n description: v.string().optional().describe(\n \"Issue description in Markdown format\",\n ),\n labels: v.array(v.string()).optional().describe(\n 'Labels to apply (e.g., [\"bug\", \"urgent\"])',\n ),\n assigneeIds: v.array(v.number()).optional().describe(\n \"User IDs to assign the issue to\",\n ),\n milestoneId: v.number().optional().describe(\n \"Milestone ID to associate with the issue\",\n ),\n dueDate: v.string().optional().describe(\"Due date in YYYY-MM-DD format\"),\n })\n )(),\n async execute(\n {\n projectId,\n title,\n description,\n labels,\n assigneeIds,\n milestoneId,\n dueDate,\n },\n ) {\n const issue = await createIssue(projectId, {\n title,\n description,\n labels,\n assigneeIds,\n milestoneId,\n dueDate,\n });\n\n return {\n success: true,\n message: `Issue created successfully: #${issue.iid}`,\n issue: {\n id: issue.id,\n iid: issue.iid,\n projectId: issue.project_id,\n title: issue.title,\n state: issue.state,\n labels: issue.labels,\n assignees: issue.assignees.map(({ username, name }) => ({\n username,\n name,\n })),\n webUrl: issue.web_url,\n createdAt: issue.created_at,\n },\n };\n },\n});\n",
305
- "tools/get-issue.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { getIssue } from \"../lib/gitlab-client.ts\";\n\nexport default tool({\n id: \"gitlab-get-issue\",\n description:\n \"Get detailed information about a specific GitLab issue including full description, comments, time tracking, and metadata.\",\n inputSchema: defineSchema((v) =>\n v.object({\n projectId: v\n .union([v.number(), v.string()])\n .describe('Project ID or path (e.g., \"gitlab-org/gitlab\" or 278964)'),\n issueIid: v\n .number()\n .describe(\n \"Issue IID (internal ID, the number shown in the issue URL like #123)\",\n ),\n })\n )(),\n async execute({ projectId, issueIid }) {\n const issue = await getIssue(projectId, issueIid);\n\n return {\n id: issue.id,\n iid: issue.iid,\n projectId: issue.project_id,\n title: issue.title,\n description: issue.description ?? \"No description provided\",\n state: issue.state,\n labels: issue.labels,\n milestone: issue.milestone\n ? { id: issue.milestone.id, title: issue.milestone.title }\n : null,\n assignees: issue.assignees.map(({ id, username, name, avatar_url }) => ({\n id,\n username,\n name,\n avatarUrl: avatar_url,\n })),\n author: {\n id: issue.author.id,\n username: issue.author.username,\n name: issue.author.name,\n avatarUrl: issue.author.avatar_url,\n },\n timeStats: {\n timeEstimate: issue.time_stats.time_estimate,\n totalTimeSpent: issue.time_stats.total_time_spent,\n },\n createdAt: issue.created_at,\n updatedAt: issue.updated_at,\n closedAt: issue.closed_at,\n webUrl: issue.web_url,\n };\n },\n});\n",
306
- "tools/get-merge-request.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport {\n formatMergeRequestForDisplay,\n getMergeRequest,\n} from \"../lib/gitlab-client.ts\";\n\nexport default tool({\n id: \"gitlab-get-merge-request\",\n description:\n \"Get detailed information about a specific GitLab merge request.\",\n inputSchema: defineSchema((v) =>\n v.object({\n projectId: v\n .union([v.number(), v.string()])\n .describe('Project ID or path (e.g., \"gitlab-org/gitlab\" or 278964)'),\n mergeRequestIid: v\n .number()\n .describe(\n \"Merge request IID (the project-local number shown in the MR URL)\",\n ),\n })\n )(),\n async execute({ projectId, mergeRequestIid }) {\n const mr = await getMergeRequest(projectId, mergeRequestIid);\n\n return {\n id: mr.id,\n iid: mr.iid,\n projectId: mr.project_id,\n title: mr.title,\n description: mr.description ?? \"No description provided\",\n state: mr.state,\n draft: mr.draft,\n sourceBranch: mr.source_branch,\n targetBranch: mr.target_branch,\n labels: mr.labels,\n author: { username: mr.author.username, name: mr.author.name },\n assignees: mr.assignees.map(({ username, name }) => ({ username, name })),\n reviewers: mr.reviewers.map(({ username, name }) => ({ username, name })),\n createdAt: mr.created_at,\n updatedAt: mr.updated_at,\n mergedAt: mr.merged_at,\n closedAt: mr.closed_at,\n webUrl: mr.web_url,\n summary: formatMergeRequestForDisplay(mr),\n };\n },\n});\n",
307
- "tools/get-project.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { getProject } from \"../lib/gitlab-client.ts\";\n\nexport default tool({\n id: \"gitlab-get-project\",\n description: \"Get detailed information about a GitLab project.\",\n inputSchema: defineSchema((v) =>\n v.object({\n projectId: v\n .union([v.number(), v.string()])\n .describe('Project ID or path (e.g., \"gitlab-org/gitlab\" or 278964)'),\n })\n )(),\n async execute({ projectId }) {\n const project = await getProject(projectId);\n\n return {\n id: project.id,\n name: project.name,\n nameWithNamespace: project.name_with_namespace,\n description: project.description ?? \"No description\",\n path: project.path_with_namespace,\n visibility: project.visibility,\n defaultBranch: project.default_branch,\n webUrl: project.web_url,\n createdAt: project.created_at,\n lastActivityAt: project.last_activity_at,\n };\n },\n});\n",
308
- "tools/list-merge-requests.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport {\n formatMergeRequestForDisplay,\n listMergeRequests,\n} from \"../lib/gitlab-client.ts\";\n\nexport default tool({\n id: \"gitlab-list-merge-requests\",\n description:\n \"List merge requests in GitLab. Can filter by scope, state, labels, and specific project. Returns MR titles, states, branches, assignees, and reviewers.\",\n inputSchema: defineSchema((v) =>\n v.object({\n scope: v\n .enum([\"created_by_me\", \"assigned_to_me\", \"all\"])\n .default(\"all\")\n .describe(\"Scope of merge requests to list\"),\n state: v\n .enum([\"opened\", \"closed\", \"merged\", \"all\"])\n .default(\"opened\")\n .describe(\"State of merge requests to list\"),\n labels: v\n .array(v.string())\n .optional()\n .describe('Filter by labels (e.g., [\"feature\", \"review-needed\"])'),\n projectId: v\n .union([v.number(), v.string()])\n .optional()\n .describe('Project ID or path (e.g., \"gitlab-org/gitlab\" or 278964)'),\n limit: v.number().min(1).max(100).default(20).describe(\n \"Maximum number of results to return\",\n ),\n })\n )(),\n async execute({ scope, state, labels, projectId, limit }) {\n const mergeRequests = await listMergeRequests({\n scope,\n state,\n labels,\n projectId,\n perPage: limit,\n });\n\n if (mergeRequests.length === 0) {\n return {\n message: \"No merge requests found matching the criteria.\",\n count: 0,\n mergeRequests: [],\n };\n }\n\n return {\n count: mergeRequests.length,\n mergeRequests: mergeRequests.map((mr) => {\n const description = mr.description ?? \"\";\n const truncatedDescription = description.length > 200\n ? `${description.substring(0, 200)}...`\n : description;\n\n return {\n id: mr.id,\n iid: mr.iid,\n projectId: mr.project_id,\n title: mr.title,\n state: mr.state,\n draft: mr.draft,\n sourceBranch: mr.source_branch,\n targetBranch: mr.target_branch,\n labels: mr.labels,\n author: {\n username: mr.author.username,\n name: mr.author.name,\n },\n assignees: mr.assignees.map((a) => ({\n username: a.username,\n name: a.name,\n })),\n reviewers: mr.reviewers.map((r) => ({\n username: r.username,\n name: r.name,\n })),\n createdAt: mr.created_at,\n updatedAt: mr.updated_at,\n mergedAt: mr.merged_at,\n webUrl: mr.web_url,\n description: truncatedDescription,\n };\n }),\n summary: mergeRequests.map(formatMergeRequestForDisplay).join(\"\\n\\n\"),\n };\n },\n});\n",
309
- "tools/list-projects.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { listProjects } from \"../lib/gitlab-client.ts\";\n\nexport default tool({\n id: \"gitlab-list-projects\",\n description:\n \"List GitLab projects accessible to the authenticated user. Can search, filter by membership, and sort results.\",\n inputSchema: defineSchema((v) =>\n v.object({\n search: v.string().optional().describe(\n \"Search query to filter projects by name or path\",\n ),\n membership: v.boolean().default(true).describe(\n \"Only show projects where user is a member\",\n ),\n orderBy: v\n .enum([\"id\", \"name\", \"created_at\", \"updated_at\", \"last_activity_at\"])\n .default(\"last_activity_at\")\n .describe(\"Field to order results by\"),\n sort: v.enum([\"asc\", \"desc\"]).default(\"desc\").describe(\"Sort direction\"),\n limit: v.number().min(1).max(100).default(20).describe(\n \"Maximum number of results to return\",\n ),\n })\n )(),\n async execute({ search, membership, orderBy, sort, limit }) {\n const projects = await listProjects({\n search,\n membership,\n orderBy,\n sort,\n perPage: limit,\n });\n\n const mappedProjects = projects.map((project) => ({\n id: project.id,\n name: project.name,\n nameWithNamespace: project.name_with_namespace,\n path: project.path_with_namespace,\n description: project.description ?? \"No description\",\n visibility: project.visibility,\n defaultBranch: project.default_branch,\n webUrl: project.web_url,\n createdAt: project.created_at,\n lastActivityAt: project.last_activity_at,\n }));\n\n if (!mappedProjects.length) {\n return {\n message: \"No projects found matching the criteria.\",\n count: 0,\n projects: [],\n };\n }\n\n return {\n count: mappedProjects.length,\n projects: mappedProjects,\n };\n },\n});\n",
310
- "tools/search-issues.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport {\n formatIssueForDisplay,\n searchIssues,\n} from \"../lib/gitlab-client.ts\";\n\nexport default tool({\n id: \"gitlab-search-issues\",\n description:\n \"Search for issues in GitLab projects. Can search across all accessible projects or within a specific project. Returns issue titles, states, assignees, and labels.\",\n inputSchema: defineSchema((v) =>\n v.object({\n scope: v\n .enum([\"created_by_me\", \"assigned_to_me\", \"all\"])\n .default(\"all\")\n .describe(\"Scope of issues to search\"),\n state: v\n .enum([\"opened\", \"closed\", \"all\"])\n .default(\"opened\")\n .describe(\"State of issues to search for\"),\n search: v.string().optional().describe(\n \"Search query to filter issues by title or description\",\n ),\n labels: v.array(v.string()).optional().describe(\n 'Filter by labels (e.g., [\"bug\", \"urgent\"])',\n ),\n projectId: v\n .union([v.number(), v.string()])\n .optional()\n .describe('Project ID or path (e.g., \"gitlab-org/gitlab\" or 278964)'),\n limit: v.number().min(1).max(100).default(20).describe(\n \"Maximum number of results to return\",\n ),\n })\n )(),\n async execute({ scope, state, search, labels, projectId, limit }) {\n const issues = await searchIssues({\n scope,\n state,\n search,\n labels,\n projectId,\n perPage: limit,\n });\n\n if (issues.length === 0) {\n return {\n message: \"No issues found matching the criteria.\",\n count: 0,\n issues: [],\n };\n }\n\n return {\n count: issues.length,\n issues: issues.map((issue) => {\n const description = issue.description ?? \"\";\n const truncatedDescription = description.length > 200\n ? `${description.substring(0, 200)}...`\n : description;\n\n return {\n id: issue.id,\n iid: issue.iid,\n projectId: issue.project_id,\n title: issue.title,\n state: issue.state,\n labels: issue.labels,\n assignees: issue.assignees.map(({ username, name }) => ({\n username,\n name,\n })),\n author: {\n username: issue.author.username,\n name: issue.author.name,\n },\n createdAt: issue.created_at,\n updatedAt: issue.updated_at,\n webUrl: issue.web_url,\n description: truncatedDescription,\n };\n }),\n summary: issues.map(formatIssueForDisplay).join(\"\\n\\n\"),\n };\n },\n});\n",
311
- "tools/update-issue.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { updateIssue } from \"../lib/gitlab-client.ts\";\n\nexport default tool({\n id: \"gitlab-update-issue\",\n description: \"Update, close, or reopen a GitLab issue.\",\n inputSchema: defineSchema((v) =>\n v.object({\n projectId: v\n .union([v.number(), v.string()])\n .describe('Project ID or path (e.g., \"gitlab-org/gitlab\" or 278964)'),\n issueIid: v\n .number()\n .describe(\n \"Issue IID (the project-local number shown in the issue URL)\",\n ),\n title: v.string().optional().describe(\"Updated issue title\"),\n description: v.string().optional().describe(\"Updated issue description\"),\n state: v.enum([\"opened\", \"closed\"]).optional().describe(\"Issue state\"),\n labels: v.array(v.string()).optional().describe(\"Replacement labels\"),\n assigneeIds: v.array(v.number()).optional().describe(\n \"GitLab user IDs to assign\",\n ),\n })\n )(),\n async execute(\n { projectId, issueIid, title, description, state, labels, assigneeIds },\n ) {\n const issue = await updateIssue(projectId, issueIid, {\n title,\n description,\n state,\n labels,\n assigneeIds,\n });\n\n return {\n success: true,\n message: `Issue #${issue.iid} updated successfully.`,\n issue: {\n id: issue.id,\n iid: issue.iid,\n projectId: issue.project_id,\n title: issue.title,\n state: issue.state,\n labels: issue.labels,\n assignees: issue.assignees.map(({ username, name }) => ({\n username,\n name,\n })),\n webUrl: issue.web_url,\n updatedAt: issue.updated_at,\n },\n };\n },\n});\n"
312
- }
313
- },
314
- "integration:gmail": {
315
- "files": {
316
- ".env.example": "# =============================================================================\n# Gmail Integration Setup\n# =============================================================================\n#\n# STEP 1: Create a Google Cloud Project\n# Visit: https://console.cloud.google.com/projectcreate\n#\n# STEP 2: Enable the Gmail API\n# Visit: https://console.cloud.google.com/apis/library/gmail.googleapis.com\n# Select \"Enable\" to activate the Gmail API for your project\n#\n# STEP 3: Configure OAuth Consent Screen\n# Visit: https://console.cloud.google.com/apis/credentials/consent\n# - Choose \"External\" user type (or \"Internal\" for Workspace)\n# - Fill in app name, support email\n# - Add scopes: gmail.readonly, gmail.send, gmail.modify, gmail.labels, gmail.compose\n# - Add https://mail.google.com/ if you use permanent delete tools\n# - Add your email as a test user (required for development)\n#\n# STEP 4: Create OAuth Credentials\n# Visit: https://console.cloud.google.com/apis/credentials\n# - Select \"Create Credentials\" > \"OAuth client ID\"\n# - Application type: \"Web application\"\n# - Add Authorized redirect URI: http://localhost:3000/api/auth/gmail/callback\n# - Copy the Client ID and Client Secret below\n#\n# Optional: mailbox watch tools require a Cloud Pub/Sub topic. Grant publish\n# access to the Gmail API service account before calling watch-mailbox.\n#\n# =============================================================================\n\nGOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com\nGOOGLE_CLIENT_SECRET=your-client-secret\n",
317
- "app/api/auth/gmail/callback/route.ts": "import { createOAuthCallbackHandler, gmailConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\n\nexport const GET = createOAuthCallbackHandler(gmailConfig, { tokenStore });\n",
318
- "app/api/auth/gmail/route.ts": "import { createOAuthInitHandler, gmailConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../lib/token-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../lib/user-id.ts\";\n\nexport const GET = createOAuthInitHandler(gmailConfig, {\n tokenStore,\n getUserId: requireUserIdFromRequest,\n});\n",
319
- "lib/context.ts": "import type { ToolExecutionContext } from \"veryfront/tool\";\nimport { requireUserIdFromContext } from \"./user-id.ts\";\n\nexport function resolveUserId(context?: ToolExecutionContext): string {\n return requireUserIdFromContext(context);\n}\n",
320
- "lib/gmail-client.ts": "/**\n * Gmail API Client\n *\n * Provides a type-safe interface to Gmail API operations\n * using the veryfront/oauth module for authentication.\n */\n\nimport { gmailConfig, OAuthService } from \"veryfront/oauth\";\nimport { tokenStore } from \"./token-store.ts\";\n\nexport type GmailMessageFormat = \"full\" | \"metadata\" | \"minimal\" | \"raw\";\nexport type GmailThreadFormat = Exclude<GmailMessageFormat, \"raw\">;\nexport type GmailLabelVisibility = \"labelShow\" | \"labelShowIfUnread\" | \"labelHide\";\nexport type GmailMessageListVisibility = \"show\" | \"hide\";\nexport type GmailHistoryType = \"messageAdded\" | \"messageDeleted\" | \"labelAdded\" | \"labelRemoved\";\n\nexport interface GmailMessagePartBody {\n attachmentId?: string;\n data?: string;\n size: number;\n}\n\nexport interface GmailMessagePart {\n partId?: string;\n mimeType: string;\n filename?: string;\n headers?: Array<{ name: string; value: string }>;\n body?: GmailMessagePartBody;\n parts?: GmailMessagePart[];\n}\n\nexport interface GmailMessage {\n id: string;\n threadId: string;\n labelIds?: string[];\n snippet?: string;\n payload?: GmailMessagePart;\n internalDate?: string;\n historyId?: string;\n sizeEstimate?: number;\n raw?: string;\n}\n\nexport interface GmailMessageList {\n messages?: Array<{ id: string; threadId: string }>;\n nextPageToken?: string;\n resultSizeEstimate: number;\n}\n\nexport interface GmailLabel {\n id: string;\n name: string;\n messageListVisibility?: GmailMessageListVisibility;\n labelListVisibility?: GmailLabelVisibility;\n type?: \"system\" | \"user\";\n messagesTotal?: number;\n messagesUnread?: number;\n threadsTotal?: number;\n threadsUnread?: number;\n color?: {\n textColor: string;\n backgroundColor: string;\n };\n}\n\nexport interface GmailLabelList {\n labels: GmailLabel[];\n}\n\nexport interface GmailThread {\n id: string;\n snippet?: string;\n historyId?: string;\n messages?: GmailMessage[];\n}\n\nexport interface GmailThreadList {\n threads?: Array<{ id: string; historyId?: string; snippet?: string }>;\n nextPageToken?: string;\n resultSizeEstimate: number;\n}\n\nexport interface GmailDraft {\n id: string;\n message: GmailMessage;\n}\n\nexport interface GmailDraftList {\n drafts?: Array<{ id: string; message: { id: string; threadId: string } }>;\n nextPageToken?: string;\n resultSizeEstimate: number;\n}\n\nexport interface GmailAttachment {\n attachmentId?: string;\n size: number;\n data: string;\n}\n\nexport interface GmailProfile {\n emailAddress: string;\n messagesTotal: number;\n threadsTotal: number;\n historyId: string;\n}\n\nexport interface GmailHistoryList {\n history?: Array<{\n id: string;\n messages?: GmailMessage[];\n messagesAdded?: Array<{ message: GmailMessage }>;\n messagesDeleted?: Array<{ message: GmailMessage }>;\n labelsAdded?: Array<{ message: GmailMessage; labelIds: string[] }>;\n labelsRemoved?: Array<{ message: GmailMessage; labelIds: string[] }>;\n }>;\n nextPageToken?: string;\n historyId: string;\n}\n\nexport interface GmailWatchResponse {\n historyId: string;\n expiration: string;\n}\n\nexport interface SendEmailOptions {\n to: string | string[];\n subject: string;\n body: string;\n cc?: string | string[];\n bcc?: string | string[];\n replyTo?: string;\n isHtml?: boolean;\n threadId?: string;\n}\n\nexport type DraftEmailOptions = SendEmailOptions;\n\nexport interface ModifyLabelsOptions {\n addLabelIds?: string[];\n removeLabelIds?: string[];\n}\n\nexport interface ListOptions {\n maxResults?: number;\n pageToken?: string;\n}\n\nexport interface ListMessagesOptions extends ListOptions {\n query?: string;\n labelIds?: string[];\n}\n\nexport interface ListHistoryOptions extends ListOptions {\n startHistoryId: string;\n labelId?: string;\n historyTypes?: GmailHistoryType[];\n}\n\nexport interface WatchMailboxOptions {\n topicName: string;\n labelIds?: string[];\n labelFilterBehavior?: \"include\" | \"exclude\";\n}\n\nexport interface GmailClient {\n isConnected(): Promise<boolean>;\n listMessages(options?: ListMessagesOptions): Promise<GmailMessageList>;\n getMessage(messageId: string, format?: GmailMessageFormat): Promise<GmailMessage>;\n sendEmail(options: SendEmailOptions): Promise<{ id: string; threadId: string }>;\n searchEmails(query: string, maxResults?: number): Promise<GmailMessage[]>;\n getUnreadEmails(maxResults?: number): Promise<GmailMessage[]>;\n markAsRead(messageId: string): Promise<void>;\n archiveEmail(messageId: string): Promise<void>;\n listLabels(): Promise<GmailLabelList>;\n getLabel(labelId: string): Promise<GmailLabel>;\n createLabel(label: Partial<GmailLabel> & { name: string }): Promise<GmailLabel>;\n updateLabel(labelId: string, label: Partial<GmailLabel> & { name: string }): Promise<GmailLabel>;\n patchLabel(labelId: string, label: Partial<GmailLabel>): Promise<GmailLabel>;\n deleteLabel(labelId: string): Promise<void>;\n modifyMessageLabels(messageId: string, labels: ModifyLabelsOptions): Promise<GmailMessage>;\n trashMessage(messageId: string): Promise<GmailMessage>;\n untrashMessage(messageId: string): Promise<GmailMessage>;\n deleteMessage(messageId: string): Promise<void>;\n batchModifyMessages(messageIds: string[], labels: ModifyLabelsOptions): Promise<void>;\n batchDeleteMessages(messageIds: string[]): Promise<void>;\n listThreads(options?: ListMessagesOptions): Promise<GmailThreadList>;\n getThread(threadId: string, format?: GmailThreadFormat): Promise<GmailThread>;\n modifyThreadLabels(threadId: string, labels: ModifyLabelsOptions): Promise<GmailThread>;\n trashThread(threadId: string): Promise<GmailThread>;\n untrashThread(threadId: string): Promise<GmailThread>;\n deleteThread(threadId: string): Promise<void>;\n createDraft(options: DraftEmailOptions): Promise<GmailDraft>;\n listDrafts(options?: ListMessagesOptions): Promise<GmailDraftList>;\n getDraft(draftId: string, format?: GmailMessageFormat): Promise<GmailDraft>;\n updateDraft(draftId: string, options: DraftEmailOptions): Promise<GmailDraft>;\n sendDraft(draftId: string): Promise<{ id: string; threadId: string }>;\n deleteDraft(draftId: string): Promise<void>;\n getAttachment(messageId: string, attachmentId: string): Promise<GmailAttachment>;\n getProfile(): Promise<GmailProfile>;\n listHistory(options: ListHistoryOptions): Promise<GmailHistoryList>;\n watchMailbox(options: WatchMailboxOptions): Promise<GmailWatchResponse>;\n stopMailboxWatch(): Promise<void>;\n}\n\n// Keep the full refresh-capable contract: OAuthService uses the store's\n// distributed lock and revisioned compare-and-set when access tokens expire.\nconst gmailService = new OAuthService(gmailConfig, tokenStore);\n\nfunction formatAddresses(addresses: string | string[] | undefined): string {\n if (!addresses) return \"\";\n return Array.isArray(addresses) ? addresses.join(\", \") : addresses;\n}\n\nfunction encodeEmail(options: SendEmailOptions): string {\n const toAddresses = formatAddresses(options.to);\n const ccAddresses = formatAddresses(options.cc);\n const bccAddresses = formatAddresses(options.bcc);\n\n const headers = [\n `To: ${toAddresses}`,\n `Subject: ${options.subject}`,\n options.isHtml\n ? \"Content-Type: text/html; charset=utf-8\"\n : \"Content-Type: text/plain; charset=utf-8\",\n ];\n\n if (ccAddresses) headers.push(`Cc: ${ccAddresses}`);\n if (bccAddresses) headers.push(`Bcc: ${bccAddresses}`);\n if (options.replyTo) headers.push(`Reply-To: ${options.replyTo}`);\n\n const email = `${headers.join(\"\\r\\n\")}\\r\\n\\r\\n${options.body}`;\n return btoa(email).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nfunction addListParams(params: URLSearchParams, options: ListMessagesOptions = {}): void {\n if (options.maxResults != null) params.set(\"maxResults\", String(options.maxResults));\n if (options.query) params.set(\"q\", options.query);\n if (options.labelIds?.length) {\n for (const labelId of options.labelIds) params.append(\"labelIds\", labelId);\n }\n if (options.pageToken) params.set(\"pageToken\", options.pageToken);\n}\n\nfunction withQuery(path: string, params: URLSearchParams): string {\n const query = params.toString();\n return query ? `${path}?${query}` : path;\n}\n\nfunction encodedMessage(options: SendEmailOptions): { raw: string; threadId?: string } {\n return {\n raw: encodeEmail(options),\n ...(options.threadId ? { threadId: options.threadId } : {}),\n };\n}\n\n/**\n * Create a Gmail client scoped to a specific user. Pass the authenticated\n * user's id (from your session). Tokens are looked up and stored per-user.\n */\nexport function createGmailClient(userId: string): GmailClient {\n async function apiRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await gmailService.getAccessToken(userId);\n if (!token) {\n throw new Error(\"Gmail not connected\");\n }\n\n const url = endpoint.startsWith(\"http\") ? endpoint : `${gmailConfig.apiBaseUrl}${endpoint}`;\n const response = await fetch(url, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const detail = await response.text();\n throw new Error(`Gmail API error: ${response.status} ${detail}`);\n }\n\n const text = await response.text();\n return (text ? JSON.parse(text) : undefined) as T;\n }\n\n return {\n async isConnected(): Promise<boolean> {\n const token = await gmailService.getAccessToken(userId);\n return token !== null;\n },\n\n listMessages(options: ListMessagesOptions = {}): Promise<GmailMessageList> {\n const params = new URLSearchParams();\n addListParams(params, options);\n return apiRequest<GmailMessageList>(withQuery(\"/users/me/messages\", params));\n },\n\n getMessage(messageId: string, format: GmailMessageFormat = \"full\"): Promise<GmailMessage> {\n return apiRequest<GmailMessage>(`/users/me/messages/${messageId}?format=${format}`);\n },\n\n sendEmail(options: SendEmailOptions): Promise<{ id: string; threadId: string }> {\n return apiRequest<{ id: string; threadId: string }>(\"/users/me/messages/send\", {\n method: \"POST\",\n body: JSON.stringify(encodedMessage(options)),\n });\n },\n\n async searchEmails(query: string, maxResults = 10): Promise<GmailMessage[]> {\n const list = await this.listMessages({ query, maxResults });\n if (!list.messages?.length) return [];\n return Promise.all(list.messages.map((m) => this.getMessage(m.id, \"metadata\")));\n },\n\n getUnreadEmails(maxResults = 10): Promise<GmailMessage[]> {\n return this.searchEmails(\"is:unread\", maxResults);\n },\n\n async markAsRead(messageId: string): Promise<void> {\n await this.modifyMessageLabels(messageId, { removeLabelIds: [\"UNREAD\"] });\n },\n\n async archiveEmail(messageId: string): Promise<void> {\n await this.modifyMessageLabels(messageId, { removeLabelIds: [\"INBOX\"] });\n },\n\n listLabels(): Promise<GmailLabelList> {\n return apiRequest<GmailLabelList>(\"/users/me/labels\");\n },\n\n getLabel(labelId: string): Promise<GmailLabel> {\n return apiRequest<GmailLabel>(`/users/me/labels/${labelId}`);\n },\n\n createLabel(label: Partial<GmailLabel> & { name: string }): Promise<GmailLabel> {\n return apiRequest<GmailLabel>(\"/users/me/labels\", {\n method: \"POST\",\n body: JSON.stringify(label),\n });\n },\n\n updateLabel(\n labelId: string,\n label: Partial<GmailLabel> & { name: string },\n ): Promise<GmailLabel> {\n return apiRequest<GmailLabel>(`/users/me/labels/${labelId}`, {\n method: \"PUT\",\n body: JSON.stringify(label),\n });\n },\n\n patchLabel(labelId: string, label: Partial<GmailLabel>): Promise<GmailLabel> {\n return apiRequest<GmailLabel>(`/users/me/labels/${labelId}`, {\n method: \"PATCH\",\n body: JSON.stringify(label),\n });\n },\n\n async deleteLabel(labelId: string): Promise<void> {\n await apiRequest<void>(`/users/me/labels/${labelId}`, { method: \"DELETE\" });\n },\n\n modifyMessageLabels(messageId: string, labels: ModifyLabelsOptions): Promise<GmailMessage> {\n return apiRequest<GmailMessage>(`/users/me/messages/${messageId}/modify`, {\n method: \"POST\",\n body: JSON.stringify(labels),\n });\n },\n\n trashMessage(messageId: string): Promise<GmailMessage> {\n return apiRequest<GmailMessage>(`/users/me/messages/${messageId}/trash`, { method: \"POST\" });\n },\n\n untrashMessage(messageId: string): Promise<GmailMessage> {\n return apiRequest<GmailMessage>(`/users/me/messages/${messageId}/untrash`, {\n method: \"POST\",\n });\n },\n\n async deleteMessage(messageId: string): Promise<void> {\n await apiRequest<void>(`/users/me/messages/${messageId}`, { method: \"DELETE\" });\n },\n\n async batchModifyMessages(messageIds: string[], labels: ModifyLabelsOptions): Promise<void> {\n await apiRequest<void>(\"/users/me/messages/batchModify\", {\n method: \"POST\",\n body: JSON.stringify({ ids: messageIds, ...labels }),\n });\n },\n\n async batchDeleteMessages(messageIds: string[]): Promise<void> {\n await apiRequest<void>(\"/users/me/messages/batchDelete\", {\n method: \"POST\",\n body: JSON.stringify({ ids: messageIds }),\n });\n },\n\n listThreads(options: ListMessagesOptions = {}): Promise<GmailThreadList> {\n const params = new URLSearchParams();\n addListParams(params, options);\n return apiRequest<GmailThreadList>(withQuery(\"/users/me/threads\", params));\n },\n\n getThread(threadId: string, format: GmailThreadFormat = \"full\"): Promise<GmailThread> {\n return apiRequest<GmailThread>(`/users/me/threads/${threadId}?format=${format}`);\n },\n\n modifyThreadLabels(threadId: string, labels: ModifyLabelsOptions): Promise<GmailThread> {\n return apiRequest<GmailThread>(`/users/me/threads/${threadId}/modify`, {\n method: \"POST\",\n body: JSON.stringify(labels),\n });\n },\n\n trashThread(threadId: string): Promise<GmailThread> {\n return apiRequest<GmailThread>(`/users/me/threads/${threadId}/trash`, { method: \"POST\" });\n },\n\n untrashThread(threadId: string): Promise<GmailThread> {\n return apiRequest<GmailThread>(`/users/me/threads/${threadId}/untrash`, { method: \"POST\" });\n },\n\n async deleteThread(threadId: string): Promise<void> {\n await apiRequest<void>(`/users/me/threads/${threadId}`, { method: \"DELETE\" });\n },\n\n createDraft(options: DraftEmailOptions): Promise<GmailDraft> {\n return apiRequest<GmailDraft>(\"/users/me/drafts\", {\n method: \"POST\",\n body: JSON.stringify({ message: encodedMessage(options) }),\n });\n },\n\n listDrafts(options: ListMessagesOptions = {}): Promise<GmailDraftList> {\n const params = new URLSearchParams();\n addListParams(params, options);\n return apiRequest<GmailDraftList>(withQuery(\"/users/me/drafts\", params));\n },\n\n getDraft(draftId: string, format: GmailMessageFormat = \"full\"): Promise<GmailDraft> {\n return apiRequest<GmailDraft>(`/users/me/drafts/${draftId}?format=${format}`);\n },\n\n updateDraft(draftId: string, options: DraftEmailOptions): Promise<GmailDraft> {\n return apiRequest<GmailDraft>(`/users/me/drafts/${draftId}`, {\n method: \"PUT\",\n body: JSON.stringify({ id: draftId, message: encodedMessage(options) }),\n });\n },\n\n sendDraft(draftId: string): Promise<{ id: string; threadId: string }> {\n return apiRequest<{ id: string; threadId: string }>(\"/users/me/drafts/send\", {\n method: \"POST\",\n body: JSON.stringify({ id: draftId }),\n });\n },\n\n async deleteDraft(draftId: string): Promise<void> {\n await apiRequest<void>(`/users/me/drafts/${draftId}`, { method: \"DELETE\" });\n },\n\n getAttachment(messageId: string, attachmentId: string): Promise<GmailAttachment> {\n return apiRequest<GmailAttachment>(\n `/users/me/messages/${messageId}/attachments/${attachmentId}`,\n );\n },\n\n getProfile(): Promise<GmailProfile> {\n return apiRequest<GmailProfile>(\"/users/me/profile\");\n },\n\n listHistory(options: ListHistoryOptions): Promise<GmailHistoryList> {\n const params = new URLSearchParams();\n params.set(\"startHistoryId\", options.startHistoryId);\n if (options.maxResults != null) params.set(\"maxResults\", String(options.maxResults));\n if (options.pageToken) params.set(\"pageToken\", options.pageToken);\n if (options.labelId) params.set(\"labelId\", options.labelId);\n if (options.historyTypes?.length) {\n for (const historyType of options.historyTypes) params.append(\"historyTypes\", historyType);\n }\n return apiRequest<GmailHistoryList>(withQuery(\"/users/me/history\", params));\n },\n\n watchMailbox(options: WatchMailboxOptions): Promise<GmailWatchResponse> {\n return apiRequest<GmailWatchResponse>(\"/users/me/watch\", {\n method: \"POST\",\n body: JSON.stringify(options),\n });\n },\n\n async stopMailboxWatch(): Promise<void> {\n await apiRequest<void>(\"/users/me/stop\", { method: \"POST\" });\n },\n };\n}\n\nexport function parseEmailHeaders(\n headers: Array<{ name: string; value: string }>,\n): { from: string; to: string; subject: string; date: string } {\n function getHeader(name: string): string {\n return headers.find((h) => h.name.toLowerCase() === name.toLowerCase())?.value ?? \"\";\n }\n\n return {\n from: getHeader(\"From\"),\n to: getHeader(\"To\"),\n subject: getHeader(\"Subject\"),\n date: getHeader(\"Date\"),\n };\n}\n",
321
- "tools/apply-labels.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGmailClient } from \"../lib/gmail-client.ts\";\nimport { resolveUserId } from \"../lib/context.ts\";\n\nconst getLabelChangeInput = defineSchema((v) => v\n .object({\n messageId: v.string().min(1).describe(\"Gmail message ID\"),\n addLabelIds: v.array(v.string().min(1)).optional().describe(\"Label IDs to add\"),\n removeLabelIds: v.array(v.string().min(1)).optional().describe(\"Label IDs to remove\"),\n })\n .refine((value) => value.addLabelIds?.length || value.removeLabelIds?.length, {\n message: \"At least one label must be added or removed\",\n }));\n\nexport default tool({\n id: \"gmail-apply-labels\",\n description: \"Apply or remove Gmail labels on a message.\",\n inputSchema: getLabelChangeInput(),\n execute: async ({ messageId, addLabelIds, removeLabelIds }, context) => {\n const userId = resolveUserId(context);\n\n try {\n const gmail = createGmailClient(userId);\n const message = await gmail.modifyMessageLabels(messageId, { addLabelIds, removeLabelIds });\n\n return {\n success: true,\n message,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Gmail not connected. Please connect your Gmail account.\",\n connectUrl: \"/api/auth/gmail\",\n };\n }\n throw error;\n }\n },\n});\n",
322
- "tools/archive-email.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGmailClient } from \"../lib/gmail-client.ts\";\nimport { resolveUserId } from \"../lib/context.ts\";\n\nexport default tool({\n id: \"gmail-archive-email\",\n description: \"Archive a Gmail message by removing the INBOX label.\",\n inputSchema: defineSchema((v) => v.object({\n messageId: v.string().min(1).describe(\"Gmail message ID\"),\n }))(),\n execute: async ({ messageId }, context) => {\n const userId = resolveUserId(context);\n\n try {\n const gmail = createGmailClient(userId);\n await gmail.archiveEmail(messageId);\n\n return {\n success: true,\n messageId,\n message: \"Email archived.\",\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Gmail not connected. Please connect your Gmail account.\",\n connectUrl: \"/api/auth/gmail\",\n };\n }\n throw error;\n }\n },\n});\n",
323
- "tools/batch-delete-emails.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGmailClient } from \"../lib/gmail-client.ts\";\nimport { resolveUserId } from \"../lib/context.ts\";\n\nexport default tool({\n id: \"gmail-batch-delete-emails\",\n description: \"Permanently delete multiple Gmail messages.\",\n inputSchema: defineSchema((v) => v.object({\n messageIds: v.array(v.string().min(1)).min(1).describe(\"Gmail message IDs\"),\n }))(),\n execute: async ({ messageIds }, context) => {\n const userId = resolveUserId(context);\n\n try {\n const gmail = createGmailClient(userId);\n await gmail.batchDeleteMessages(messageIds);\n\n return {\n success: true,\n count: messageIds.length,\n message: `Permanently deleted ${messageIds.length} email(s).`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Gmail not connected. Please connect your Gmail account.\",\n connectUrl: \"/api/auth/gmail\",\n };\n }\n throw error;\n }\n },\n});\n",
324
- "tools/batch-modify-emails.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGmailClient } from \"../lib/gmail-client.ts\";\nimport { resolveUserId } from \"../lib/context.ts\";\n\nconst getBatchModifyInput = defineSchema((v) => v\n .object({\n messageIds: v.array(v.string().min(1)).min(1).describe(\"Gmail message IDs\"),\n addLabelIds: v.array(v.string().min(1)).optional().describe(\"Label IDs to add\"),\n removeLabelIds: v.array(v.string().min(1)).optional().describe(\"Label IDs to remove\"),\n })\n .refine((value) => value.addLabelIds?.length || value.removeLabelIds?.length, {\n message: \"At least one label must be added or removed\",\n }));\n\nexport default tool({\n id: \"gmail-batch-modify-emails\",\n description: \"Modify labels on multiple Gmail messages.\",\n inputSchema: getBatchModifyInput(),\n execute: async ({ messageIds, addLabelIds, removeLabelIds }, context) => {\n const userId = resolveUserId(context);\n\n try {\n const gmail = createGmailClient(userId);\n await gmail.batchModifyMessages(messageIds, { addLabelIds, removeLabelIds });\n\n return {\n success: true,\n count: messageIds.length,\n message: `Modified ${messageIds.length} email(s).`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Gmail not connected. Please connect your Gmail account.\",\n connectUrl: \"/api/auth/gmail\",\n };\n }\n throw error;\n }\n },\n});\n",
325
- "tools/create-draft.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGmailClient } from \"../lib/gmail-client.ts\";\nimport { resolveUserId } from \"../lib/context.ts\";\n\nconst getDraftInput = defineSchema((v) => v.object({\n to: v.union([v.string().email(), v.array(v.string().email())]).describe(\"Email recipient(s)\"),\n subject: v.string().min(1).describe(\"Email subject line\"),\n body: v.string().min(1).describe(\"Email body content\"),\n cc: v.union([v.string().email(), v.array(v.string().email())]).optional().describe(\n \"CC recipient(s)\",\n ),\n bcc: v\n .union([v.string().email(), v.array(v.string().email())])\n .optional()\n .describe(\"BCC recipient(s)\"),\n replyTo: v.string().email().optional().describe(\"Reply-To address\"),\n isHtml: v.boolean().default(false).describe(\"Whether the body contains HTML\"),\n threadId: v.string().optional().describe(\"Thread ID to draft a reply in\"),\n}));\n\nexport default tool({\n id: \"gmail-create-draft\",\n description: \"Create a Gmail draft message.\",\n inputSchema: getDraftInput(),\n execute: async (input, context) => {\n const userId = resolveUserId(context);\n\n try {\n const gmail = createGmailClient(userId);\n const draft = await gmail.createDraft(input);\n\n return {\n success: true,\n draft,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Gmail not connected. Please connect your Gmail account.\",\n connectUrl: \"/api/auth/gmail\",\n };\n }\n throw error;\n }\n },\n});\n",
326
- "tools/create-label.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGmailClient } from \"../lib/gmail-client.ts\";\nimport { resolveUserId } from \"../lib/context.ts\";\n\nconst getLabelInput = defineSchema((v) => v.object({\n name: v.string().min(1).describe(\"Label display name\"),\n messageListVisibility: v.enum([\"show\", \"hide\"]).optional().describe(\"Message list visibility\"),\n labelListVisibility: v\n .enum([\"labelShow\", \"labelShowIfUnread\", \"labelHide\"])\n .optional()\n .describe(\"Label list visibility\"),\n textColor: v.string().optional().describe(\"Label text color hex value\"),\n backgroundColor: v.string().optional().describe(\"Label background color hex value\"),\n}));\n\nexport default tool({\n id: \"gmail-create-label\",\n description: \"Create a Gmail user label.\",\n inputSchema: getLabelInput(),\n execute: async ({ textColor, backgroundColor, ...input }, context) => {\n const userId = resolveUserId(context);\n\n try {\n const gmail = createGmailClient(userId);\n const label = await gmail.createLabel({\n ...input,\n ...(textColor && backgroundColor ? { color: { textColor, backgroundColor } } : {}),\n });\n\n return {\n success: true,\n label,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Gmail not connected. Please connect your Gmail account.\",\n connectUrl: \"/api/auth/gmail\",\n };\n }\n throw error;\n }\n },\n});\n",
327
- "tools/delete-draft.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGmailClient } from \"../lib/gmail-client.ts\";\nimport { resolveUserId } from \"../lib/context.ts\";\n\nexport default tool({\n id: \"gmail-delete-draft\",\n description: \"Permanently delete a Gmail draft.\",\n inputSchema: defineSchema((v) => v.object({\n draftId: v.string().min(1).describe(\"Gmail draft ID\"),\n }))(),\n execute: async ({ draftId }, context) => {\n const userId = resolveUserId(context);\n\n try {\n const gmail = createGmailClient(userId);\n await gmail.deleteDraft(draftId);\n\n return {\n success: true,\n draftId,\n message: \"Draft deleted.\",\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Gmail not connected. Please connect your Gmail account.\",\n connectUrl: \"/api/auth/gmail\",\n };\n }\n throw error;\n }\n },\n});\n",
328
- "tools/delete-email.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGmailClient } from \"../lib/gmail-client.ts\";\nimport { resolveUserId } from \"../lib/context.ts\";\n\nexport default tool({\n id: \"gmail-delete-email\",\n description: \"Permanently delete a Gmail message.\",\n inputSchema: defineSchema((v) => v.object({\n messageId: v.string().min(1).describe(\"Gmail message ID\"),\n }))(),\n execute: async ({ messageId }, context) => {\n const userId = resolveUserId(context);\n\n try {\n const gmail = createGmailClient(userId);\n await gmail.deleteMessage(messageId);\n\n return {\n success: true,\n messageId,\n message: \"Email permanently deleted.\",\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Gmail not connected. Please connect your Gmail account.\",\n connectUrl: \"/api/auth/gmail\",\n };\n }\n throw error;\n }\n },\n});\n",
329
- "tools/delete-label.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGmailClient } from \"../lib/gmail-client.ts\";\nimport { resolveUserId } from \"../lib/context.ts\";\n\nexport default tool({\n id: \"gmail-delete-label\",\n description: \"Delete a Gmail user label.\",\n inputSchema: defineSchema((v) => v.object({\n labelId: v.string().min(1).describe(\"Gmail label ID\"),\n }))(),\n execute: async ({ labelId }, context) => {\n const userId = resolveUserId(context);\n\n try {\n const gmail = createGmailClient(userId);\n await gmail.deleteLabel(labelId);\n\n return {\n success: true,\n labelId,\n message: \"Label deleted.\",\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Gmail not connected. Please connect your Gmail account.\",\n connectUrl: \"/api/auth/gmail\",\n };\n }\n throw error;\n }\n },\n});\n",
330
- "tools/delete-thread.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGmailClient } from \"../lib/gmail-client.ts\";\nimport { resolveUserId } from \"../lib/context.ts\";\n\nexport default tool({\n id: \"gmail-delete-thread\",\n description: \"Permanently delete a Gmail thread.\",\n inputSchema: defineSchema((v) => v.object({\n threadId: v.string().min(1).describe(\"Gmail thread ID\"),\n }))(),\n execute: async ({ threadId }, context) => {\n const userId = resolveUserId(context);\n\n try {\n const gmail = createGmailClient(userId);\n await gmail.deleteThread(threadId);\n\n return {\n success: true,\n threadId,\n message: \"Thread permanently deleted.\",\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Gmail not connected. Please connect your Gmail account.\",\n connectUrl: \"/api/auth/gmail\",\n };\n }\n throw error;\n }\n },\n});\n",
331
- "tools/get-attachment.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGmailClient } from \"../lib/gmail-client.ts\";\nimport { resolveUserId } from \"../lib/context.ts\";\n\nexport default tool({\n id: \"gmail-get-attachment\",\n description: \"Get a Gmail message attachment by message ID and attachment ID.\",\n inputSchema: defineSchema((v) => v.object({\n messageId: v.string().min(1).describe(\"Gmail message ID\"),\n attachmentId: v.string().min(1).describe(\"Gmail attachment ID\"),\n }))(),\n execute: async ({ messageId, attachmentId }, context) => {\n const userId = resolveUserId(context);\n\n try {\n const gmail = createGmailClient(userId);\n return await gmail.getAttachment(messageId, attachmentId);\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Gmail not connected. Please connect your Gmail account.\",\n connectUrl: \"/api/auth/gmail\",\n };\n }\n throw error;\n }\n },\n});\n",
332
- "tools/get-draft.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGmailClient } from \"../lib/gmail-client.ts\";\nimport { resolveUserId } from \"../lib/context.ts\";\n\nexport default tool({\n id: \"gmail-get-draft\",\n description: \"Get a Gmail draft by ID.\",\n inputSchema: defineSchema((v) => v.object({\n draftId: v.string().min(1).describe(\"Gmail draft ID\"),\n format: v.enum([\"full\", \"metadata\", \"minimal\", \"raw\"]).default(\"full\").describe(\n \"Draft message format\",\n ),\n }))(),\n execute: async ({ draftId, format }, context) => {\n const userId = resolveUserId(context);\n\n try {\n const gmail = createGmailClient(userId);\n return await gmail.getDraft(draftId, format);\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Gmail not connected. Please connect your Gmail account.\",\n connectUrl: \"/api/auth/gmail\",\n };\n }\n throw error;\n }\n },\n});\n",
333
- "tools/get-email.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGmailClient, parseEmailHeaders } from \"../lib/gmail-client.ts\";\nimport { resolveUserId } from \"../lib/context.ts\";\n\nexport default tool({\n id: \"gmail-get-email\",\n description: \"Get a Gmail message by ID, including headers, labels, snippet, and payload data.\",\n inputSchema: defineSchema((v) => v.object({\n messageId: v.string().min(1).describe(\"Gmail message ID\"),\n format: v.enum([\"full\", \"metadata\", \"minimal\", \"raw\"]).default(\"full\").describe(\n \"Message format\",\n ),\n }))(),\n execute: async ({ messageId, format }, context) => {\n const userId = resolveUserId(context);\n\n try {\n const gmail = createGmailClient(userId);\n const message = await gmail.getMessage(messageId, format);\n const headers = parseEmailHeaders(message.payload?.headers ?? []);\n\n return {\n id: message.id,\n threadId: message.threadId,\n labelIds: message.labelIds ?? [],\n snippet: message.snippet,\n headers,\n payload: message.payload,\n raw: message.raw,\n internalDate: message.internalDate,\n historyId: message.historyId,\n sizeEstimate: message.sizeEstimate,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Gmail not connected. Please connect your Gmail account.\",\n connectUrl: \"/api/auth/gmail\",\n };\n }\n throw error;\n }\n },\n});\n",
334
- "tools/get-label.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGmailClient } from \"../lib/gmail-client.ts\";\nimport { resolveUserId } from \"../lib/context.ts\";\n\nexport default tool({\n id: \"gmail-get-label\",\n description: \"Get a Gmail label by ID.\",\n inputSchema: defineSchema((v) => v.object({\n labelId: v.string().min(1).describe(\"Gmail label ID\"),\n }))(),\n execute: async ({ labelId }, context) => {\n const userId = resolveUserId(context);\n\n try {\n const gmail = createGmailClient(userId);\n return await gmail.getLabel(labelId);\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Gmail not connected. Please connect your Gmail account.\",\n connectUrl: \"/api/auth/gmail\",\n };\n }\n throw error;\n }\n },\n});\n",
335
- "tools/get-profile.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGmailClient } from \"../lib/gmail-client.ts\";\nimport { resolveUserId } from \"../lib/context.ts\";\n\nexport default tool({\n id: \"gmail-get-profile\",\n description: \"Get the Gmail profile for the connected account.\",\n inputSchema: defineSchema((v) => v.object({}))(),\n execute: async (_input, context) => {\n const userId = resolveUserId(context);\n\n try {\n const gmail = createGmailClient(userId);\n return await gmail.getProfile();\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Gmail not connected. Please connect your Gmail account.\",\n connectUrl: \"/api/auth/gmail\",\n };\n }\n throw error;\n }\n },\n});\n",
336
- "tools/get-thread.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGmailClient } from \"../lib/gmail-client.ts\";\nimport { resolveUserId } from \"../lib/context.ts\";\n\nexport default tool({\n id: \"gmail-get-thread\",\n description: \"Get a Gmail thread by ID.\",\n inputSchema: defineSchema((v) => v.object({\n threadId: v.string().min(1).describe(\"Gmail thread ID\"),\n format: v.enum([\"full\", \"metadata\", \"minimal\"]).default(\"full\").describe(\n \"Thread message format\",\n ),\n }))(),\n execute: async ({ threadId, format }, context) => {\n const userId = resolveUserId(context);\n\n try {\n const gmail = createGmailClient(userId);\n return await gmail.getThread(threadId, format);\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Gmail not connected. Please connect your Gmail account.\",\n connectUrl: \"/api/auth/gmail\",\n };\n }\n throw error;\n }\n },\n});\n",
337
- "tools/list-drafts.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGmailClient } from \"../lib/gmail-client.ts\";\nimport { resolveUserId } from \"../lib/context.ts\";\n\nexport default tool({\n id: \"gmail-list-drafts\",\n description: \"List Gmail draft messages.\",\n inputSchema: defineSchema((v) => v.object({\n maxResults: v.number().min(1).max(500).default(10).describe(\"Maximum number of drafts\"),\n query: v.string().optional().describe(\"Gmail search query\"),\n pageToken: v.string().optional().describe(\"Page token for pagination\"),\n }))(),\n execute: async ({ maxResults, query, pageToken }, context) => {\n const userId = resolveUserId(context);\n\n try {\n const gmail = createGmailClient(userId);\n const result = await gmail.listDrafts({ maxResults, query, pageToken });\n\n return {\n drafts: result.drafts ?? [],\n nextPageToken: result.nextPageToken,\n resultSizeEstimate: result.resultSizeEstimate,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Gmail not connected. Please connect your Gmail account.\",\n connectUrl: \"/api/auth/gmail\",\n };\n }\n throw error;\n }\n },\n});\n",
338
- "tools/list-emails.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGmailClient, parseEmailHeaders } from \"../lib/gmail-client.ts\";\nimport { resolveUserId } from \"../lib/context.ts\";\n\nexport default tool({\n id: \"gmail-list-emails\",\n description:\n \"List recent emails from Gmail inbox. Returns email subjects, senders, and snippets.\",\n inputSchema: defineSchema((v) => v.object({\n maxResults: v\n .number()\n .min(1)\n .max(50)\n .default(10)\n .describe(\"Maximum number of emails to return\"),\n unreadOnly: v.boolean().default(false).describe(\"Only return unread emails\"),\n label: v\n .string()\n .optional()\n .describe(\"Filter by Gmail label (e.g., 'INBOX', 'IMPORTANT', 'STARRED')\"),\n }))(),\n execute: async ({ maxResults, unreadOnly, label }, context) => {\n const userId = resolveUserId(context);\n\n try {\n const gmail = createGmailClient(userId);\n\n const list = await gmail.listMessages({\n maxResults,\n query: unreadOnly ? \"is:unread\" : undefined,\n labelIds: label ? [label] : undefined,\n });\n\n if (!list.messages?.length) {\n return { emails: [], message: \"No emails found matching your criteria.\" };\n }\n\n const emails = await Promise.all(\n list.messages.map(async ({ id }) => {\n const message = await gmail.getMessage(id, \"metadata\");\n const headers = parseEmailHeaders(message.payload?.headers ?? []);\n const labelIds = message.labelIds ?? [];\n\n return {\n id: message.id,\n threadId: message.threadId,\n from: headers.from,\n to: headers.to,\n subject: headers.subject,\n date: headers.date,\n snippet: message.snippet,\n isUnread: labelIds.includes(\"UNREAD\"),\n isStarred: labelIds.includes(\"STARRED\"),\n isImportant: labelIds.includes(\"IMPORTANT\"),\n };\n }),\n );\n\n return {\n emails,\n count: emails.length,\n message: `Found ${emails.length} email(s).`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Gmail not connected. Please connect your Gmail account.\",\n connectUrl: \"/api/auth/gmail\",\n };\n }\n throw error;\n }\n },\n});\n",
339
- "tools/list-history.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGmailClient } from \"../lib/gmail-client.ts\";\nimport { resolveUserId } from \"../lib/context.ts\";\n\nexport default tool({\n id: \"gmail-list-history\",\n description: \"List Gmail mailbox history changes after a start history ID.\",\n inputSchema: defineSchema((v) => v.object({\n startHistoryId: v.string().min(1).describe(\"History ID to start after\"),\n maxResults: v.number().min(1).max(500).optional().describe(\"Maximum history records\"),\n pageToken: v.string().optional().describe(\"Page token for pagination\"),\n labelId: v.string().optional().describe(\"Only return history for this label\"),\n historyTypes: v\n .array(v.enum([\"messageAdded\", \"messageDeleted\", \"labelAdded\", \"labelRemoved\"]))\n .optional()\n .describe(\"History event types to return\"),\n }))(),\n execute: async (input, context) => {\n const userId = resolveUserId(context);\n\n try {\n const gmail = createGmailClient(userId);\n return await gmail.listHistory(input);\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Gmail not connected. Please connect your Gmail account.\",\n connectUrl: \"/api/auth/gmail\",\n };\n }\n throw error;\n }\n },\n});\n",
340
- "tools/list-labels.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGmailClient } from \"../lib/gmail-client.ts\";\nimport { resolveUserId } from \"../lib/context.ts\";\n\nexport default tool({\n id: \"gmail-list-labels\",\n description: \"List Gmail labels for the connected account.\",\n inputSchema: defineSchema((v) => v.object({}))(),\n execute: async (_input, context) => {\n const userId = resolveUserId(context);\n\n try {\n const gmail = createGmailClient(userId);\n const result = await gmail.listLabels();\n\n return {\n labels: result.labels ?? [],\n count: result.labels?.length ?? 0,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Gmail not connected. Please connect your Gmail account.\",\n connectUrl: \"/api/auth/gmail\",\n };\n }\n throw error;\n }\n },\n});\n",
341
- "tools/list-threads.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGmailClient } from \"../lib/gmail-client.ts\";\nimport { resolveUserId } from \"../lib/context.ts\";\n\nexport default tool({\n id: \"gmail-list-threads\",\n description: \"List Gmail threads with optional search and label filters.\",\n inputSchema: defineSchema((v) => v.object({\n maxResults: v.number().min(1).max(500).default(10).describe(\"Maximum number of threads\"),\n query: v.string().optional().describe(\"Gmail search query\"),\n labelIds: v.array(v.string().min(1)).optional().describe(\n \"Only return threads with these labels\",\n ),\n pageToken: v.string().optional().describe(\"Page token for pagination\"),\n }))(),\n execute: async ({ maxResults, query, labelIds, pageToken }, context) => {\n const userId = resolveUserId(context);\n\n try {\n const gmail = createGmailClient(userId);\n const result = await gmail.listThreads({ maxResults, query, labelIds, pageToken });\n\n return {\n threads: result.threads ?? [],\n nextPageToken: result.nextPageToken,\n resultSizeEstimate: result.resultSizeEstimate,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Gmail not connected. Please connect your Gmail account.\",\n connectUrl: \"/api/auth/gmail\",\n };\n }\n throw error;\n }\n },\n});\n",
342
- "tools/mark-email-read.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGmailClient } from \"../lib/gmail-client.ts\";\nimport { resolveUserId } from \"../lib/context.ts\";\n\nexport default tool({\n id: \"gmail-mark-email-read\",\n description: \"Mark a Gmail message as read by removing the UNREAD label.\",\n inputSchema: defineSchema((v) => v.object({\n messageId: v.string().min(1).describe(\"Gmail message ID\"),\n }))(),\n execute: async ({ messageId }, context) => {\n const userId = resolveUserId(context);\n\n try {\n const gmail = createGmailClient(userId);\n await gmail.markAsRead(messageId);\n\n return {\n success: true,\n messageId,\n message: \"Email marked as read.\",\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Gmail not connected. Please connect your Gmail account.\",\n connectUrl: \"/api/auth/gmail\",\n };\n }\n throw error;\n }\n },\n});\n",
343
- "tools/modify-email-labels.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGmailClient } from \"../lib/gmail-client.ts\";\nimport { resolveUserId } from \"../lib/context.ts\";\n\nconst getModifyLabelsInput = defineSchema((v) => v\n .object({\n messageId: v.string().min(1).describe(\"Gmail message ID\"),\n addLabelIds: v.array(v.string().min(1)).optional().describe(\"Label IDs to add\"),\n removeLabelIds: v.array(v.string().min(1)).optional().describe(\"Label IDs to remove\"),\n })\n .refine((value) => value.addLabelIds?.length || value.removeLabelIds?.length, {\n message: \"At least one label must be added or removed\",\n }));\n\nexport default tool({\n id: \"gmail-modify-email-labels\",\n description: \"Modify labels on a Gmail message.\",\n inputSchema: getModifyLabelsInput(),\n execute: async ({ messageId, addLabelIds, removeLabelIds }, context) => {\n const userId = resolveUserId(context);\n\n try {\n const gmail = createGmailClient(userId);\n const message = await gmail.modifyMessageLabels(messageId, { addLabelIds, removeLabelIds });\n\n return {\n success: true,\n message,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Gmail not connected. Please connect your Gmail account.\",\n connectUrl: \"/api/auth/gmail\",\n };\n }\n throw error;\n }\n },\n});\n",
344
- "tools/modify-thread-labels.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGmailClient } from \"../lib/gmail-client.ts\";\nimport { resolveUserId } from \"../lib/context.ts\";\n\nconst getModifyThreadLabelsInput = defineSchema((v) => v\n .object({\n threadId: v.string().min(1).describe(\"Gmail thread ID\"),\n addLabelIds: v.array(v.string().min(1)).optional().describe(\"Label IDs to add\"),\n removeLabelIds: v.array(v.string().min(1)).optional().describe(\"Label IDs to remove\"),\n })\n .refine((value) => value.addLabelIds?.length || value.removeLabelIds?.length, {\n message: \"At least one label must be added or removed\",\n }));\n\nexport default tool({\n id: \"gmail-modify-thread-labels\",\n description: \"Modify labels on a Gmail thread.\",\n inputSchema: getModifyThreadLabelsInput(),\n execute: async ({ threadId, addLabelIds, removeLabelIds }, context) => {\n const userId = resolveUserId(context);\n\n try {\n const gmail = createGmailClient(userId);\n const thread = await gmail.modifyThreadLabels(threadId, { addLabelIds, removeLabelIds });\n\n return {\n success: true,\n thread,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Gmail not connected. Please connect your Gmail account.\",\n connectUrl: \"/api/auth/gmail\",\n };\n }\n throw error;\n }\n },\n});\n",
345
- "tools/search-emails.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGmailClient, parseEmailHeaders } from \"../lib/gmail-client.ts\";\nimport { resolveUserId } from \"../lib/context.ts\";\n\nexport default tool({\n id: \"gmail-search-emails\",\n description:\n \"Search emails using Gmail's search syntax. Supports queries like 'from:person@email.com', 'subject:meeting', 'after:2024/01/01', etc.\",\n inputSchema: defineSchema((v) => v.object({\n query: v\n .string()\n .min(1)\n .describe(\n \"Search query using Gmail search syntax (e.g., 'from:boss@company.com subject:urgent')\",\n ),\n maxResults: v\n .number()\n .min(1)\n .max(50)\n .default(10)\n .describe(\"Maximum number of results to return\"),\n }))(),\n execute: async ({ query, maxResults }, context) => {\n const userId = resolveUserId(context);\n const gmail = createGmailClient(userId);\n\n try {\n const list = await gmail.listMessages({ query, maxResults });\n\n if (!list.messages?.length) {\n return {\n emails: [],\n query,\n message: `No emails found matching: \"${query}\"`,\n searchTips: [\n \"from:email@example.com - Search by sender\",\n \"to:email@example.com - Search by recipient\",\n \"subject:keywords - Search in subject\",\n \"after:YYYY/MM/DD - Emails after date\",\n \"before:YYYY/MM/DD - Emails before date\",\n \"is:unread - Unread emails only\",\n \"has:attachment - Emails with attachments\",\n ],\n };\n }\n\n const emails = await Promise.all(\n list.messages.map(async ({ id }) => {\n const message = await gmail.getMessage(id, \"metadata\");\n const headers = parseEmailHeaders(message.payload?.headers ?? []);\n\n return {\n id: message.id,\n threadId: message.threadId,\n from: headers.from,\n to: headers.to,\n subject: headers.subject,\n date: headers.date,\n snippet: message.snippet,\n isUnread: message.labelIds?.includes(\"UNREAD\") ?? false,\n labels: message.labelIds,\n };\n }),\n );\n\n return {\n emails,\n query,\n count: emails.length,\n message: `Found ${emails.length} email(s) matching: \"${query}\"`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Gmail not connected. Please connect your Gmail account.\",\n connectUrl: \"/api/auth/gmail\",\n };\n }\n throw error;\n }\n },\n});\n",
346
- "tools/send-draft.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGmailClient } from \"../lib/gmail-client.ts\";\nimport { resolveUserId } from \"../lib/context.ts\";\n\nexport default tool({\n id: \"gmail-send-draft\",\n description: \"Send an existing Gmail draft.\",\n inputSchema: defineSchema((v) => v.object({\n draftId: v.string().min(1).describe(\"Gmail draft ID\"),\n }))(),\n execute: async ({ draftId }, context) => {\n const userId = resolveUserId(context);\n\n try {\n const gmail = createGmailClient(userId);\n const message = await gmail.sendDraft(draftId);\n\n return {\n success: true,\n messageId: message.id,\n threadId: message.threadId,\n message: \"Draft sent.\",\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Gmail not connected. Please connect your Gmail account.\",\n connectUrl: \"/api/auth/gmail\",\n };\n }\n throw error;\n }\n },\n});\n",
347
- "tools/send-email.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGmailClient } from \"../lib/gmail-client.ts\";\nimport { resolveUserId } from \"../lib/context.ts\";\n\nfunction formatRecipients(value?: string | string[]): string | undefined {\n if (!value) return undefined;\n return Array.isArray(value) ? value.join(\", \") : value;\n}\n\nexport default tool({\n id: \"gmail-send-email\",\n description: \"Send an email via Gmail. Can send to multiple recipients with CC and BCC support.\",\n inputSchema: defineSchema((v) => v.object({\n to: v.union([v.string().email(), v.array(v.string().email())]).describe(\"Email recipient(s)\"),\n subject: v.string().min(1).describe(\"Email subject line\"),\n body: v.string().min(1).describe(\"Email body content\"),\n cc: v\n .union([v.string().email(), v.array(v.string().email())])\n .optional()\n .describe(\"CC recipient(s)\"),\n bcc: v\n .union([v.string().email(), v.array(v.string().email())])\n .optional()\n .describe(\"BCC recipient(s)\"),\n isHtml: v.boolean().default(false).describe(\"Whether the body contains HTML\"),\n }))(),\n execute: async ({ to, subject, body, cc, bcc, isHtml }, context) => {\n const userId = resolveUserId(context);\n\n try {\n const gmail = createGmailClient(userId);\n\n const result = await gmail.sendEmail({ to, subject, body, cc, bcc, isHtml });\n\n const toFormatted = formatRecipients(to) ?? \"\";\n\n return {\n success: true,\n messageId: result.id,\n threadId: result.threadId,\n message: `Email sent successfully to ${toFormatted}.`,\n details: {\n to: toFormatted,\n subject,\n cc: formatRecipients(cc),\n bcc: formatRecipients(bcc),\n },\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Gmail not connected. Please connect your Gmail account.\",\n connectUrl: \"/api/auth/gmail\",\n };\n }\n throw error;\n }\n },\n});\n",
348
- "tools/trash-email.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGmailClient } from \"../lib/gmail-client.ts\";\nimport { resolveUserId } from \"../lib/context.ts\";\n\nexport default tool({\n id: \"gmail-trash-email\",\n description: \"Move a Gmail message to trash.\",\n inputSchema: defineSchema((v) => v.object({\n messageId: v.string().min(1).describe(\"Gmail message ID\"),\n }))(),\n execute: async ({ messageId }, context) => {\n const userId = resolveUserId(context);\n\n try {\n const gmail = createGmailClient(userId);\n const message = await gmail.trashMessage(messageId);\n\n return {\n success: true,\n message,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Gmail not connected. Please connect your Gmail account.\",\n connectUrl: \"/api/auth/gmail\",\n };\n }\n throw error;\n }\n },\n});\n",
349
- "tools/trash-thread.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGmailClient } from \"../lib/gmail-client.ts\";\nimport { resolveUserId } from \"../lib/context.ts\";\n\nexport default tool({\n id: \"gmail-trash-thread\",\n description: \"Move a Gmail thread to trash.\",\n inputSchema: defineSchema((v) => v.object({\n threadId: v.string().min(1).describe(\"Gmail thread ID\"),\n }))(),\n execute: async ({ threadId }, context) => {\n const userId = resolveUserId(context);\n\n try {\n const gmail = createGmailClient(userId);\n const thread = await gmail.trashThread(threadId);\n\n return {\n success: true,\n thread,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Gmail not connected. Please connect your Gmail account.\",\n connectUrl: \"/api/auth/gmail\",\n };\n }\n throw error;\n }\n },\n});\n",
350
- "tools/untrash-email.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGmailClient } from \"../lib/gmail-client.ts\";\nimport { resolveUserId } from \"../lib/context.ts\";\n\nexport default tool({\n id: \"gmail-untrash-email\",\n description: \"Remove a Gmail message from trash.\",\n inputSchema: defineSchema((v) => v.object({\n messageId: v.string().min(1).describe(\"Gmail message ID\"),\n }))(),\n execute: async ({ messageId }, context) => {\n const userId = resolveUserId(context);\n\n try {\n const gmail = createGmailClient(userId);\n const message = await gmail.untrashMessage(messageId);\n\n return {\n success: true,\n message,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Gmail not connected. Please connect your Gmail account.\",\n connectUrl: \"/api/auth/gmail\",\n };\n }\n throw error;\n }\n },\n});\n",
351
- "tools/untrash-thread.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGmailClient } from \"../lib/gmail-client.ts\";\nimport { resolveUserId } from \"../lib/context.ts\";\n\nexport default tool({\n id: \"gmail-untrash-thread\",\n description: \"Remove a Gmail thread from trash.\",\n inputSchema: defineSchema((v) => v.object({\n threadId: v.string().min(1).describe(\"Gmail thread ID\"),\n }))(),\n execute: async ({ threadId }, context) => {\n const userId = resolveUserId(context);\n\n try {\n const gmail = createGmailClient(userId);\n const thread = await gmail.untrashThread(threadId);\n\n return {\n success: true,\n thread,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Gmail not connected. Please connect your Gmail account.\",\n connectUrl: \"/api/auth/gmail\",\n };\n }\n throw error;\n }\n },\n});\n",
352
- "tools/update-draft.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGmailClient } from \"../lib/gmail-client.ts\";\nimport { resolveUserId } from \"../lib/context.ts\";\n\nexport default tool({\n id: \"gmail-update-draft\",\n description: \"Replace the content of a Gmail draft.\",\n inputSchema: defineSchema((v) => v.object({\n draftId: v.string().min(1).describe(\"Gmail draft ID\"),\n to: v.union([v.string().email(), v.array(v.string().email())]).describe(\"Email recipient(s)\"),\n subject: v.string().min(1).describe(\"Email subject line\"),\n body: v.string().min(1).describe(\"Email body content\"),\n cc: v\n .union([v.string().email(), v.array(v.string().email())])\n .optional()\n .describe(\"CC recipient(s)\"),\n bcc: v\n .union([v.string().email(), v.array(v.string().email())])\n .optional()\n .describe(\"BCC recipient(s)\"),\n replyTo: v.string().email().optional().describe(\"Reply-To address\"),\n isHtml: v.boolean().default(false).describe(\"Whether the body contains HTML\"),\n threadId: v.string().optional().describe(\"Thread ID to keep the draft in\"),\n }))(),\n execute: async ({ draftId, ...input }, context) => {\n const userId = resolveUserId(context);\n\n try {\n const gmail = createGmailClient(userId);\n const draft = await gmail.updateDraft(draftId, input);\n\n return {\n success: true,\n draft,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Gmail not connected. Please connect your Gmail account.\",\n connectUrl: \"/api/auth/gmail\",\n };\n }\n throw error;\n }\n },\n});\n",
353
- "tools/update-label.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGmailClient } from \"../lib/gmail-client.ts\";\nimport { resolveUserId } from \"../lib/context.ts\";\n\nexport default tool({\n id: \"gmail-update-label\",\n description: \"Update a Gmail user label.\",\n inputSchema: defineSchema((v) => v.object({\n labelId: v.string().min(1).describe(\"Gmail label ID\"),\n name: v.string().min(1).describe(\"Label display name\"),\n messageListVisibility: v.enum([\"show\", \"hide\"]).optional().describe(\"Message list visibility\"),\n labelListVisibility: v\n .enum([\"labelShow\", \"labelShowIfUnread\", \"labelHide\"])\n .optional()\n .describe(\"Label list visibility\"),\n textColor: v.string().optional().describe(\"Label text color hex value\"),\n backgroundColor: v.string().optional().describe(\"Label background color hex value\"),\n }))(),\n execute: async ({ labelId, textColor, backgroundColor, ...input }, context) => {\n const userId = resolveUserId(context);\n\n try {\n const gmail = createGmailClient(userId);\n const label = await gmail.updateLabel(labelId, {\n ...input,\n ...(textColor && backgroundColor ? { color: { textColor, backgroundColor } } : {}),\n });\n\n return {\n success: true,\n label,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Gmail not connected. Please connect your Gmail account.\",\n connectUrl: \"/api/auth/gmail\",\n };\n }\n throw error;\n }\n },\n});\n"
354
- }
355
- },
356
- "integration:jira": {
357
- "files": {
358
- "app/api/auth/jira/callback/route.ts": "import { createOAuthCallbackHandler, jiraConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\n\nexport const GET = createOAuthCallbackHandler(jiraConfig, { tokenStore });\n",
359
- "app/api/auth/jira/route.ts": "import { createOAuthInitHandler, jiraConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../lib/token-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../lib/user-id.ts\";\n\nexport const GET = createOAuthInitHandler(jiraConfig, {\n tokenStore,\n getUserId: requireUserIdFromRequest,\n});\n",
360
- "lib/jira-client.ts": "import { getAccessToken, getCloudId } from \"./token-store.ts\";\n\nconst JIRA_API_VERSION = \"3\";\n\ninterface JiraResponse<T> {\n expand?: string;\n startAt?: number;\n maxResults?: number;\n total?: number;\n issues?: T[];\n values?: T[];\n}\n\nexport interface JiraIssue {\n id: string;\n key: string;\n self: string;\n fields: {\n summary: string;\n description?:\n | {\n type: string;\n content: unknown[];\n }\n | string;\n status: {\n name: string;\n statusCategory: {\n key: string;\n name: string;\n };\n };\n issuetype: {\n id: string;\n name: string;\n iconUrl: string;\n };\n priority?: {\n name: string;\n iconUrl: string;\n };\n assignee?: {\n displayName: string;\n emailAddress: string;\n accountId: string;\n };\n reporter?: {\n displayName: string;\n emailAddress: string;\n accountId: string;\n };\n created: string;\n updated: string;\n project: {\n id: string;\n key: string;\n name: string;\n };\n labels?: string[];\n [key: string]: unknown;\n };\n}\n\nexport interface JiraProject {\n id: string;\n key: string;\n name: string;\n projectTypeKey: string;\n self: string;\n avatarUrls?: Record<string, string>;\n lead?: {\n displayName: string;\n accountId: string;\n };\n}\n\nexport interface JiraIssueType {\n id: string;\n name: string;\n description: string;\n iconUrl: string;\n subtask: boolean;\n}\n\nexport interface JiraTransition {\n id: string;\n name: string;\n to: {\n id: string;\n name: string;\n };\n}\n\nexport interface JiraComment {\n id: string;\n body: unknown;\n author?: {\n displayName: string;\n accountId: string;\n };\n created: string;\n updated: string;\n}\n\nfunction buildAdfDescription(text: string): Record<string, unknown> {\n return {\n type: \"doc\",\n version: 1,\n content: [\n {\n type: \"paragraph\",\n content: [\n {\n type: \"text\",\n text,\n },\n ],\n },\n ],\n };\n}\n\nasync function jiraFetch<T>(\n endpoint: string,\n options: RequestInit = {},\n): Promise<T> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\n \"Not authenticated with Jira. Please connect your account.\",\n );\n }\n\n const cloudId = await getCloudId();\n if (!cloudId) {\n throw new Error(\"Jira cloud ID not found. Please reconnect your account.\");\n }\n\n const baseUrl =\n `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/${JIRA_API_VERSION}`;\n const url = endpoint.startsWith(\"http\") ? endpoint : `${baseUrl}${endpoint}`;\n\n const response = await fetch(url, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({} as unknown));\n const message = (error as any)?.errorMessages?.join(\", \") ||\n (error as any)?.message ||\n response.statusText;\n\n throw new Error(`Jira API error: ${response.status} ${message}`);\n }\n\n if (response.status === 204) {\n return {} as T;\n }\n\n return response.json();\n}\n\nexport async function searchIssues(\n jql: string,\n options?: {\n fields?: string[];\n maxResults?: number;\n startAt?: number;\n },\n): Promise<{ issues: JiraIssue[]; total: number }> {\n const params = new URLSearchParams({\n jql,\n maxResults: String(options?.maxResults ?? 50),\n startAt: String(options?.startAt ?? 0),\n });\n\n if (options?.fields?.length) {\n params.set(\"fields\", options.fields.join(\",\"));\n }\n\n const response = await jiraFetch<JiraResponse<JiraIssue>>(\n `/search?${params.toString()}`,\n );\n\n return {\n issues: response.issues ?? [],\n total: response.total ?? 0,\n };\n}\n\nexport function getIssue(issueIdOrKey: string): Promise<JiraIssue> {\n return jiraFetch<JiraIssue>(`/issue/${issueIdOrKey}`);\n}\n\nexport async function createIssue(options: {\n projectKey: string;\n summary: string;\n description?: string;\n issueType: string;\n priority?: string;\n assigneeId?: string;\n labels?: string[];\n}): Promise<JiraIssue> {\n const fields: Record<string, unknown> = {\n project: { key: options.projectKey },\n summary: options.summary,\n issuetype: { name: options.issueType },\n };\n\n if (options.description) {\n fields.description = buildAdfDescription(options.description);\n }\n\n if (options.priority) {\n fields.priority = { name: options.priority };\n }\n\n if (options.assigneeId) {\n fields.assignee = { id: options.assigneeId };\n }\n\n if (options.labels?.length) {\n fields.labels = options.labels;\n }\n\n const response = await jiraFetch<{ id: string; key: string; self: string }>(\n \"/issue\",\n {\n method: \"POST\",\n body: JSON.stringify({ fields }),\n },\n );\n\n return getIssue(response.key);\n}\n\nexport async function listComments(\n issueIdOrKey: string,\n options?: { startAt?: number; maxResults?: number },\n): Promise<\n {\n comments: JiraComment[];\n total: number;\n startAt: number;\n maxResults: number;\n }\n> {\n const params = new URLSearchParams({\n startAt: String(options?.startAt ?? 0),\n maxResults: String(options?.maxResults ?? 50),\n });\n\n const response = await jiraFetch<{\n comments?: JiraComment[];\n total?: number;\n startAt?: number;\n maxResults?: number;\n }>(`/issue/${issueIdOrKey}/comment?${params.toString()}`);\n\n return {\n comments: response.comments ?? [],\n total: response.total ?? 0,\n startAt: response.startAt ?? 0,\n maxResults: response.maxResults ?? 0,\n };\n}\n\nexport function addComment(\n issueIdOrKey: string,\n body: string,\n): Promise<JiraComment> {\n return jiraFetch<JiraComment>(`/issue/${issueIdOrKey}/comment`, {\n method: \"POST\",\n body: JSON.stringify({ body: buildAdfDescription(body) }),\n });\n}\n\nexport function updateIssue(\n issueIdOrKey: string,\n updates: {\n summary?: string;\n description?: string;\n priority?: string;\n assigneeId?: string;\n labels?: string[];\n },\n): Promise<void> {\n const fields: Record<string, unknown> = {};\n\n if (updates.summary) {\n fields.summary = updates.summary;\n }\n\n if (updates.description) {\n fields.description = buildAdfDescription(updates.description);\n }\n\n if (updates.priority) {\n fields.priority = { name: updates.priority };\n }\n\n if (updates.assigneeId) {\n fields.assignee = { id: updates.assigneeId };\n }\n\n if (updates.labels) {\n fields.labels = updates.labels;\n }\n\n return jiraFetch<void>(`/issue/${issueIdOrKey}`, {\n method: \"PUT\",\n body: JSON.stringify({ fields }),\n });\n}\n\nexport async function transitionIssue(\n issueIdOrKey: string,\n transitionId: string,\n): Promise<void> {\n await jiraFetch<void>(`/issue/${issueIdOrKey}/transitions`, {\n method: \"POST\",\n body: JSON.stringify({ transition: { id: transitionId } }),\n });\n}\n\nexport async function getIssueTransitions(\n issueIdOrKey: string,\n): Promise<JiraTransition[]> {\n const response = await jiraFetch<{ transitions: JiraTransition[] }>(\n `/issue/${issueIdOrKey}/transitions`,\n );\n return response.transitions ?? [];\n}\n\nexport async function listProjects(): Promise<JiraProject[]> {\n return jiraFetch<JiraProject[]>(\"/project\");\n}\n\nexport function getProject(projectIdOrKey: string): Promise<JiraProject> {\n return jiraFetch<JiraProject>(`/project/${projectIdOrKey}`);\n}\n\nexport async function getProjectIssueTypes(\n projectIdOrKey: string,\n): Promise<JiraIssueType[]> {\n return jiraFetch<JiraIssueType[]>(`/project/${projectIdOrKey}/statuses`);\n}\n\nexport function extractDescriptionText(description: unknown): string {\n if (typeof description === \"string\") {\n return description;\n }\n\n if (!description || typeof description !== \"object\") {\n return \"\";\n }\n\n const content = (description as { content?: unknown[] }).content;\n if (!Array.isArray(content)) {\n return \"\";\n }\n\n const texts: string[] = [];\n\n function extractText(node: any): void {\n if (node?.type === \"text\" && node.text) {\n texts.push(node.text);\n }\n\n if (Array.isArray(node?.content)) {\n node.content.forEach(extractText);\n }\n }\n\n content.forEach(extractText);\n return texts.join(\" \");\n}\n",
361
- "tools/add-comment.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { addComment, extractDescriptionText } from \"../lib/jira-client.ts\";\n\nexport default tool({\n id: \"jira-add-comment\",\n description: \"Add a comment to a Jira issue.\",\n inputSchema: defineSchema((v) =>\n v.object({\n issueKey: v.string().describe('The issue key (e.g., \"PROJ-123\") or ID'),\n body: v.string().min(1).describe(\"Comment body text\"),\n })\n )(),\n async execute({ issueKey, body }) {\n const comment = await addComment(issueKey, body);\n\n return {\n success: true,\n id: comment.id,\n author: comment.author?.displayName,\n body: extractDescriptionText(comment.body),\n created: comment.created,\n updated: comment.updated,\n message: `Comment added to ${issueKey}`,\n };\n },\n});\n",
362
- "tools/create-issue.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createIssue } from \"../lib/jira-client.ts\";\n\nexport default tool({\n id: \"jira-create-issue\",\n description:\n \"Create a new Jira issue in a project. Requires project key, summary, and issue type. Optionally set description, priority, assignee, and labels.\",\n inputSchema: defineSchema((v) =>\n v.object({\n projectKey: v.string().describe('The project key (e.g., \"PROJ\", \"DEV\")'),\n summary: v.string().describe(\"Brief summary/title of the issue\"),\n issueType: v.string().describe(\n 'Type of issue: \"Task\", \"Bug\", \"Story\", \"Epic\", etc.',\n ),\n description: v.string().optional().describe(\n \"Detailed description of the issue\",\n ),\n priority: v\n .string()\n .optional()\n .describe('Priority: \"Highest\", \"High\", \"Medium\", \"Low\", \"Lowest\"'),\n assigneeId: v.string().optional().describe(\n \"Atlassian account ID of the assignee (optional)\",\n ),\n labels: v.array(v.string()).optional().describe(\n \"Array of labels to add to the issue\",\n ),\n })\n )(),\n async execute(\n {\n projectKey,\n summary,\n issueType,\n description,\n priority,\n assigneeId,\n labels,\n },\n ) {\n const { key, id, fields } = await createIssue({\n projectKey,\n summary,\n issueType,\n description,\n priority,\n assigneeId,\n labels,\n });\n\n return {\n key,\n id,\n summary: fields.summary,\n status: fields.status.name,\n type: fields.issuetype.name,\n priority: fields.priority?.name,\n assignee: fields.assignee?.displayName,\n project: {\n key: fields.project.key,\n name: fields.project.name,\n },\n created: fields.created,\n message: `Issue ${key} created successfully`,\n };\n },\n});\n",
363
- "tools/get-issue.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { extractDescriptionText, getIssue } from \"../lib/jira-client.ts\";\n\nexport default tool({\n id: \"jira-get-issue\",\n description:\n \"Get detailed information about a specific Jira issue by its key (e.g., PROJ-123) or ID. Returns all fields including description, comments, history, etc.\",\n inputSchema: defineSchema((v) =>\n v.object({\n issueKey: v.string().describe('The issue key (e.g., \"PROJ-123\") or ID'),\n })\n )(),\n async execute({ issueKey }) {\n const issue = await getIssue(issueKey);\n const { fields } = issue;\n\n const priority = fields.priority\n ? { name: fields.priority.name, iconUrl: fields.priority.iconUrl }\n : null;\n\n const assignee = fields.assignee\n ? {\n displayName: fields.assignee.displayName,\n email: fields.assignee.emailAddress,\n accountId: fields.assignee.accountId,\n }\n : null;\n\n const reporter = fields.reporter\n ? {\n displayName: fields.reporter.displayName,\n email: fields.reporter.emailAddress,\n accountId: fields.reporter.accountId,\n }\n : null;\n\n return {\n key: issue.key,\n id: issue.id,\n summary: fields.summary,\n description: extractDescriptionText(fields.description),\n status: fields.status.name,\n statusCategory: fields.status.statusCategory.name,\n type: {\n name: fields.issuetype.name,\n iconUrl: fields.issuetype.iconUrl,\n },\n priority,\n assignee,\n reporter,\n project: {\n key: fields.project.key,\n name: fields.project.name,\n id: fields.project.id,\n },\n created: fields.created,\n updated: fields.updated,\n labels: fields.labels ?? [],\n url: issue.self,\n };\n },\n});\n",
364
- "tools/get-project.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { getProject } from \"../lib/jira-client.ts\";\n\nexport default tool({\n id: \"jira-get-project\",\n description: \"Get detailed information about a Jira project by key or ID.\",\n inputSchema: defineSchema((v) =>\n v.object({\n projectIdOrKey: v.string().describe('Project key or ID (e.g., \"PROJ\")'),\n })\n )(),\n async execute({ projectIdOrKey }) {\n const project = await getProject(projectIdOrKey);\n\n return {\n key: project.key,\n id: project.id,\n name: project.name,\n projectType: project.projectTypeKey,\n lead: project.lead\n ? {\n displayName: project.lead.displayName,\n accountId: project.lead.accountId,\n }\n : null,\n avatarUrl: project.avatarUrls?.[\"48x48\"],\n self: project.self,\n };\n },\n});\n",
365
- "tools/get-transitions.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { getIssueTransitions } from \"../lib/jira-client.ts\";\n\nexport default tool({\n id: \"jira-get-transitions\",\n description: \"List available workflow transitions for a Jira issue.\",\n inputSchema: defineSchema((v) =>\n v.object({\n issueKey: v.string().describe('The issue key (e.g., \"PROJ-123\") or ID'),\n })\n )(),\n async execute({ issueKey }) {\n const transitions = await getIssueTransitions(issueKey);\n\n return {\n issueKey,\n transitions: transitions.map((transition) => ({\n id: transition.id,\n name: transition.name,\n to: transition.to.name,\n })),\n };\n },\n});\n",
366
- "tools/list-comments.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { extractDescriptionText, listComments } from \"../lib/jira-client.ts\";\n\nexport default tool({\n id: \"jira-list-comments\",\n description: \"List comments on a Jira issue.\",\n inputSchema: defineSchema((v) =>\n v.object({\n issueKey: v.string().describe('The issue key (e.g., \"PROJ-123\") or ID'),\n startAt: v.number().min(0).default(0).describe(\"Pagination offset\"),\n maxResults: v.number().min(1).max(100).default(50).describe(\n \"Maximum comments to return\",\n ),\n })\n )(),\n async execute({ issueKey, startAt, maxResults }) {\n const result = await listComments(issueKey, { startAt, maxResults });\n\n return {\n total: result.total,\n startAt: result.startAt,\n maxResults: result.maxResults,\n comments: result.comments.map((comment) => ({\n id: comment.id,\n author: comment.author?.displayName,\n body: extractDescriptionText(comment.body),\n created: comment.created,\n updated: comment.updated,\n })),\n };\n },\n});\n",
367
- "tools/list-projects.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { listProjects } from \"../lib/jira-client.ts\";\n\nexport default tool({\n id: \"jira-list-projects\",\n description:\n \"List all accessible Jira projects in the connected site. Returns project keys, names, and basic information.\",\n inputSchema: defineSchema((v) => v.object({}))(),\n async execute() {\n const projects = await listProjects();\n\n return {\n total: projects.length,\n projects: projects.map((project) => {\n const lead = project.lead\n ? {\n displayName: project.lead.displayName,\n accountId: project.lead.accountId,\n }\n : null;\n\n return {\n key: project.key,\n id: project.id,\n name: project.name,\n projectType: project.projectTypeKey,\n lead,\n avatarUrl: project.avatarUrls?.[\"48x48\"],\n };\n }),\n };\n },\n});\n",
368
- "tools/search-issues.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { extractDescriptionText, searchIssues } from \"../lib/jira-client.ts\";\n\nexport default tool({\n id: \"jira-search-issues\",\n description:\n 'Search for Jira issues using JQL (Jira Query Language). Returns matching issues with key details. Common JQL examples: \"assignee = currentUser() AND status != Done\", \"project = PROJ AND type = Bug\", \"created >= -7d\".',\n inputSchema: defineSchema((v) =>\n v.object({\n jql: v\n .string()\n .describe(\n 'JQL query string to search issues. Examples: \"assignee = currentUser()\", \"project = PROJ\", \"status = Open\"',\n ),\n maxResults: v\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of results to return\"),\n fields: v\n .array(v.string())\n .optional()\n .describe(\n 'Specific fields to include (e.g., [\"summary\", \"status\", \"assignee\"])',\n ),\n })\n )(),\n async execute({ jql, maxResults, fields }) {\n const result = await searchIssues(jql, { maxResults, fields });\n\n return {\n total: result.total,\n issues: result.issues.map((issue) => {\n const issueFields = issue.fields;\n\n return {\n key: issue.key,\n id: issue.id,\n summary: issueFields.summary,\n description: extractDescriptionText(issueFields.description),\n status: issueFields.status.name,\n statusCategory: issueFields.status.statusCategory.name,\n type: issueFields.issuetype.name,\n priority: issueFields.priority?.name,\n assignee: issueFields.assignee?.displayName,\n reporter: issueFields.reporter?.displayName,\n project: {\n key: issueFields.project.key,\n name: issueFields.project.name,\n },\n created: issueFields.created,\n updated: issueFields.updated,\n labels: issueFields.labels ?? [],\n };\n }),\n };\n },\n});\n",
369
- "tools/update-issue.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport {\n getIssue,\n getIssueTransitions,\n transitionIssue,\n updateIssue,\n} from \"../lib/jira-client.ts\";\n\nexport default tool({\n id: \"jira-update-issue\",\n description:\n 'Update an existing Jira issue. Can update fields like summary, description, priority, assignee, labels, or transition the status (e.g., move to \"In Progress\", \"Done\").',\n inputSchema: defineSchema((v) =>\n v.object({\n issueKey: v.string().describe(\n 'The issue key (e.g., \"PROJ-123\") to update',\n ),\n summary: v.string().optional().describe(\n \"New summary/title for the issue\",\n ),\n description: v.string().optional().describe(\n \"New description for the issue\",\n ),\n priority: v\n .string()\n .optional()\n .describe('New priority: \"Highest\", \"High\", \"Medium\", \"Low\", \"Lowest\"'),\n assigneeId: v\n .string()\n .optional()\n .describe(\"Atlassian account ID of the new assignee\"),\n labels: v\n .array(v.string())\n .optional()\n .describe(\"New array of labels (replaces existing labels)\"),\n status: v\n .string()\n .optional()\n .describe(\n 'New status to transition to (e.g., \"In Progress\", \"Done\", \"To Do\")',\n ),\n })\n )(),\n async execute({\n issueKey,\n summary,\n description,\n priority,\n assigneeId,\n labels,\n status,\n }) {\n if (\n summary !== undefined ||\n description !== undefined ||\n priority !== undefined ||\n assigneeId !== undefined ||\n labels !== undefined\n ) {\n await updateIssue(issueKey, {\n summary,\n description,\n priority,\n assigneeId,\n labels,\n });\n }\n\n if (status) {\n const transitions = await getIssueTransitions(issueKey);\n const normalizedStatus = status.toLowerCase();\n\n const targetTransition = transitions.find((t) => {\n const transitionName = t.name.toLowerCase();\n const toName = t.to.name.toLowerCase();\n return transitionName === normalizedStatus ||\n toName === normalizedStatus;\n });\n\n if (!targetTransition) {\n const available = transitions.map((t) => t.to.name).join(\", \");\n throw new Error(\n `Status \"${status}\" not found. Available transitions: ${available}`,\n );\n }\n\n await transitionIssue(issueKey, targetTransition.id);\n }\n\n const updatedIssue = await getIssue(issueKey);\n\n return {\n key: updatedIssue.key,\n id: updatedIssue.id,\n summary: updatedIssue.fields.summary,\n status: updatedIssue.fields.status.name,\n type: updatedIssue.fields.issuetype.name,\n priority: updatedIssue.fields.priority?.name,\n assignee: updatedIssue.fields.assignee?.displayName,\n project: {\n key: updatedIssue.fields.project.key,\n name: updatedIssue.fields.project.name,\n },\n updated: updatedIssue.fields.updated,\n labels: updatedIssue.fields.labels ?? [],\n message: `Issue ${issueKey} updated successfully`,\n };\n },\n});\n"
370
- }
371
- },
372
- "integration:linear": {
373
- "files": {
374
- ".env.example": "# Linear Integration\n# Create an OAuth application at https://linear.app/settings/api\n# Set the callback URL to: http://localhost:3000/api/auth/linear/callback (or your production URL)\n\nLINEAR_CLIENT_ID=your_client_id_here\nLINEAR_CLIENT_SECRET=your_client_secret_here\n",
375
- "app/api/auth/linear/callback/route.ts": "import { createOAuthCallbackHandler, linearConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\n\nexport const GET = createOAuthCallbackHandler(linearConfig, { tokenStore });\n",
376
- "app/api/auth/linear/route.ts": "import { createOAuthInitHandler, linearConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../lib/token-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../lib/user-id.ts\";\n\nexport const GET = createOAuthInitHandler(linearConfig, {\n tokenStore,\n getUserId: requireUserIdFromRequest,\n});\n",
377
- "lib/linear-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst LINEAR_API_URL = \"https://api.linear.app/graphql\";\n\nexport interface LinearIssue {\n id: string;\n identifier: string;\n title: string;\n description?: string;\n priority: number;\n priorityLabel: string;\n state: {\n id: string;\n name: string;\n type: string;\n };\n assignee?: {\n id: string;\n name: string;\n email: string;\n };\n team: {\n id: string;\n name: string;\n key: string;\n };\n project?: {\n id: string;\n name: string;\n };\n labels: {\n nodes: Array<{\n id: string;\n name: string;\n color: string;\n }>;\n };\n createdAt: string;\n updatedAt: string;\n url: string;\n}\n\nexport interface LinearProject {\n id: string;\n name: string;\n description?: string;\n state: string;\n progress: number;\n url: string;\n lead?: {\n id: string;\n name: string;\n };\n teams: {\n nodes: Array<{\n id: string;\n name: string;\n key: string;\n }>;\n };\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface LinearTeam {\n id: string;\n name: string;\n key: string;\n}\n\nexport interface LinearWorkflowState {\n id: string;\n name: string;\n type: string;\n}\n\nexport interface LinearUser {\n id: string;\n name: string;\n displayName?: string;\n email: string;\n active: boolean;\n avatarUrl?: string;\n}\n\nexport interface LinearComment {\n id: string;\n body: string;\n createdAt: string;\n user?: {\n id: string;\n name: string;\n };\n issue?: {\n id: string;\n identifier: string;\n title: string;\n };\n}\n\ninterface GraphQLResponse<T> {\n data?: T;\n errors?: Array<{\n message: string;\n path?: string[];\n }>;\n}\n\nasync function linearFetch<T>(query: string, variables?: Record<string, unknown>): Promise<T> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with Linear. Please connect your account.\");\n }\n\n const response = await fetch(LINEAR_API_URL, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({ query, variables }),\n });\n\n if (!response.ok) {\n throw new Error(`Linear API error: ${response.status} ${response.statusText}`);\n }\n\n const json: GraphQLResponse<T> = await response.json();\n\n const errorMessage = json.errors?.[0]?.message;\n if (errorMessage) {\n throw new Error(`Linear GraphQL error: ${errorMessage}`);\n }\n\n if (!json.data) {\n throw new Error(\"Linear API returned no data\");\n }\n\n return json.data;\n}\n\nexport async function searchIssues(\n query: string,\n options?: {\n limit?: number;\n includeArchived?: boolean;\n },\n): Promise<LinearIssue[]> {\n const gqlQuery = `\n query SearchIssues($query: String!, $first: Int, $includeArchived: Boolean) {\n issueSearch(query: $query, first: $first, includeArchived: $includeArchived) {\n nodes {\n id\n identifier\n title\n description\n priority\n priorityLabel\n state {\n id\n name\n type\n }\n assignee {\n id\n name\n email\n }\n team {\n id\n name\n key\n }\n project {\n id\n name\n }\n labels {\n nodes {\n id\n name\n color\n }\n }\n createdAt\n updatedAt\n url\n }\n }\n }\n `;\n\n const data = await linearFetch<{ issueSearch: { nodes: LinearIssue[] } }>(gqlQuery, {\n query,\n first: options?.limit ?? 10,\n includeArchived: options?.includeArchived ?? false,\n });\n\n return data.issueSearch.nodes;\n}\n\nexport async function getIssue(issueId: string): Promise<LinearIssue> {\n const query = `\n query GetIssue($id: String!) {\n issue(id: $id) {\n id\n identifier\n title\n description\n priority\n priorityLabel\n state {\n id\n name\n type\n }\n assignee {\n id\n name\n email\n }\n team {\n id\n name\n key\n }\n project {\n id\n name\n }\n labels {\n nodes {\n id\n name\n color\n }\n }\n createdAt\n updatedAt\n url\n }\n }\n `;\n\n const data = await linearFetch<{ issue: LinearIssue }>(query, { id: issueId });\n return data.issue;\n}\n\nexport async function createIssue(options: {\n teamId: string;\n title: string;\n description?: string;\n priority?: number;\n stateId?: string;\n assigneeId?: string;\n projectId?: string;\n labelIds?: string[];\n}): Promise<LinearIssue> {\n const mutation = `\n mutation CreateIssue($input: IssueCreateInput!) {\n issueCreate(input: $input) {\n success\n issue {\n id\n identifier\n title\n description\n priority\n priorityLabel\n state {\n id\n name\n type\n }\n assignee {\n id\n name\n email\n }\n team {\n id\n name\n key\n }\n project {\n id\n name\n }\n labels {\n nodes {\n id\n name\n color\n }\n }\n createdAt\n updatedAt\n url\n }\n }\n }\n `;\n\n const input: Record<string, unknown> = {\n teamId: options.teamId,\n title: options.title,\n };\n\n if (options.description) input.description = options.description;\n if (options.priority !== undefined) input.priority = options.priority;\n if (options.stateId) input.stateId = options.stateId;\n if (options.assigneeId) input.assigneeId = options.assigneeId;\n if (options.projectId) input.projectId = options.projectId;\n if (options.labelIds?.length) input.labelIds = options.labelIds;\n\n const data = await linearFetch<{ issueCreate: { success: boolean; issue: LinearIssue } }>(mutation, {\n input,\n });\n\n if (!data.issueCreate.success) {\n throw new Error(\"Failed to create issue\");\n }\n\n return data.issueCreate.issue;\n}\n\nexport async function updateIssue(\n issueId: string,\n options: {\n title?: string;\n description?: string;\n priority?: number;\n stateId?: string;\n assigneeId?: string;\n projectId?: string;\n labelIds?: string[];\n },\n): Promise<LinearIssue> {\n const mutation = `\n mutation UpdateIssue($id: String!, $input: IssueUpdateInput!) {\n issueUpdate(id: $id, input: $input) {\n success\n issue {\n id\n identifier\n title\n description\n priority\n priorityLabel\n state {\n id\n name\n type\n }\n assignee {\n id\n name\n email\n }\n team {\n id\n name\n key\n }\n project {\n id\n name\n }\n labels {\n nodes {\n id\n name\n color\n }\n }\n createdAt\n updatedAt\n url\n }\n }\n }\n `;\n\n const input: Record<string, unknown> = {};\n\n if (options.title) input.title = options.title;\n if (options.description !== undefined) input.description = options.description;\n if (options.priority !== undefined) input.priority = options.priority;\n if (options.stateId) input.stateId = options.stateId;\n if (options.assigneeId) input.assigneeId = options.assigneeId;\n if (options.projectId) input.projectId = options.projectId;\n if (options.labelIds) input.labelIds = options.labelIds;\n\n const data = await linearFetch<{ issueUpdate: { success: boolean; issue: LinearIssue } }>(mutation, {\n id: issueId,\n input,\n });\n\n if (!data.issueUpdate.success) {\n throw new Error(\"Failed to update issue\");\n }\n\n return data.issueUpdate.issue;\n}\n\nexport async function listProjects(options?: {\n limit?: number;\n includeArchived?: boolean;\n}): Promise<LinearProject[]> {\n const query = `\n query ListProjects($first: Int, $includeArchived: Boolean) {\n projects(first: $first, includeArchived: $includeArchived) {\n nodes {\n id\n name\n description\n state\n progress\n url\n lead {\n id\n name\n }\n teams {\n nodes {\n id\n name\n key\n }\n }\n createdAt\n updatedAt\n }\n }\n }\n `;\n\n const data = await linearFetch<{ projects: { nodes: LinearProject[] } }>(query, {\n first: options?.limit ?? 20,\n includeArchived: options?.includeArchived ?? false,\n });\n\n return data.projects.nodes;\n}\n\nexport async function getTeams(): Promise<LinearTeam[]> {\n const query = `\n query GetTeams {\n teams {\n nodes {\n id\n name\n key\n }\n }\n }\n `;\n\n const data = await linearFetch<{ teams: { nodes: LinearTeam[] } }>(query);\n return data.teams.nodes;\n}\n\nexport async function getWorkflowStates(teamId: string): Promise<LinearWorkflowState[]> {\n const query = `\n query GetWorkflowStates($teamId: String!) {\n team(id: $teamId) {\n states {\n nodes {\n id\n name\n type\n }\n }\n }\n }\n `;\n\n const data = await linearFetch<{ team: { states: { nodes: LinearWorkflowState[] } } }>(query, {\n teamId,\n });\n\n return data.team.states.nodes;\n}\n\nexport async function listUsers(options?: {\n limit?: number;\n}): Promise<LinearUser[]> {\n const query = `\n query ListUsers($first: Int) {\n users(first: $first) {\n nodes {\n id\n name\n displayName\n email\n active\n avatarUrl\n }\n }\n }\n `;\n\n const data = await linearFetch<{ users: { nodes: LinearUser[] } }>(query, {\n first: options?.limit ?? 50,\n });\n\n return data.users.nodes;\n}\n\nexport async function addComment(options: {\n issueId: string;\n body: string;\n}): Promise<LinearComment> {\n const mutation = `\n mutation AddComment($issueId: String!, $body: String!) {\n commentCreate(input: { issueId: $issueId, body: $body }) {\n success\n comment {\n id\n body\n createdAt\n user {\n id\n name\n }\n issue {\n id\n identifier\n title\n }\n }\n }\n }\n `;\n\n const data = await linearFetch<{ commentCreate: { success: boolean; comment: LinearComment } }>(\n mutation,\n options,\n );\n\n if (!data.commentCreate.success) {\n throw new Error(\"Failed to add comment\");\n }\n\n return data.commentCreate.comment;\n}\n",
378
- "tools/add-comment.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { addComment } from \"../lib/linear-client.ts\";\n\nexport default tool({\n id: \"linear-add-comment\",\n description: \"Add a comment to a Linear issue.\",\n inputSchema: defineSchema((v) => v.object({\n issueId: v.string().describe(\"Linear issue ID\"),\n body: v.string().min(1).describe(\"Comment body in markdown\"),\n }))(),\n async execute({ issueId, body }) {\n const comment = await addComment({ issueId, body });\n\n return {\n id: comment.id,\n body: comment.body,\n createdAt: comment.createdAt,\n user: comment.user\n ? { id: comment.user.id, name: comment.user.name }\n : null,\n issue: comment.issue\n ? {\n id: comment.issue.id,\n identifier: comment.issue.identifier,\n title: comment.issue.title,\n }\n : null,\n };\n },\n});\n",
379
- "tools/create-issue.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createIssue } from \"../lib/linear-client.ts\";\n\nexport default tool({\n id: \"linear-create-issue\",\n description:\n \"Create a new Linear issue in a specified team. You can optionally set priority, assign to someone, add to a project, and attach labels.\",\n inputSchema: defineSchema((v) => v.object({\n teamId: v\n .string()\n .describe(\n \"The ID of the team to create the issue in. Use list-projects tool first if you need to find team IDs.\",\n ),\n title: v.string().describe(\"Title of the issue\"),\n description: v\n .string()\n .optional()\n .describe(\"Detailed description of the issue (supports markdown)\"),\n priority: v\n .number()\n .min(0)\n .max(4)\n .optional()\n .describe(\"Priority level: 0=No priority, 1=Urgent, 2=High, 3=Medium, 4=Low\"),\n stateId: v\n .string()\n .optional()\n .describe('Workflow state ID (e.g., \"Todo\", \"In Progress\", \"Done\")'),\n assigneeId: v.string().optional().describe(\"User ID to assign the issue to\"),\n projectId: v.string().optional().describe(\"Project ID to add the issue to\"),\n labelIds: v\n .array(v.string())\n .optional()\n .describe(\"Array of label IDs to attach to the issue\"),\n }))(),\n async execute(\n { teamId, title, description, priority, stateId, assigneeId, projectId, labelIds },\n ) {\n const issue = await createIssue({\n teamId,\n title,\n description,\n priority,\n stateId,\n assigneeId,\n projectId,\n labelIds,\n });\n\n const assignee = issue.assignee\n ? { name: issue.assignee.name, email: issue.assignee.email }\n : null;\n\n const project = issue.project ? { name: issue.project.name } : null;\n\n const labels = issue.labels.nodes.map((label) => ({\n name: label.name,\n color: label.color,\n }));\n\n return {\n id: issue.id,\n identifier: issue.identifier,\n title: issue.title,\n description: issue.description,\n priority: issue.priorityLabel,\n status: issue.state.name,\n assignee,\n team: {\n name: issue.team.name,\n key: issue.team.key,\n },\n project,\n labels,\n url: issue.url,\n createdAt: issue.createdAt,\n };\n },\n});\n",
380
- "tools/get-issue.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { getIssue } from \"../lib/linear-client.ts\";\n\nexport default tool({\n id: \"linear-get-issue\",\n description:\n \"Get detailed information about a specific Linear issue by its ID or identifier (e.g., ENG-123). Returns complete issue details including description, status, assignee, labels, and project.\",\n inputSchema: defineSchema((v) => v.object({\n issueId: v\n .string()\n .describe(\n 'The ID or identifier of the issue (e.g., \"ENG-123\" or full UUID)',\n ),\n }))(),\n async execute({ issueId }) {\n const issue = await getIssue(issueId);\n\n return {\n id: issue.id,\n identifier: issue.identifier,\n title: issue.title,\n description: issue.description,\n priority: issue.priorityLabel,\n priorityNumber: issue.priority,\n status: issue.state.name,\n statusType: issue.state.type,\n stateId: issue.state.id,\n assignee: issue.assignee\n ? {\n id: issue.assignee.id,\n name: issue.assignee.name,\n email: issue.assignee.email,\n }\n : null,\n team: {\n id: issue.team.id,\n name: issue.team.name,\n key: issue.team.key,\n },\n project: issue.project\n ? {\n id: issue.project.id,\n name: issue.project.name,\n }\n : null,\n labels: issue.labels.nodes.map(({ id, name, color }) => ({\n id,\n name,\n color,\n })),\n url: issue.url,\n createdAt: issue.createdAt,\n updatedAt: issue.updatedAt,\n };\n },\n});\n",
381
- "tools/list-projects.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { listProjects } from \"../lib/linear-client.ts\";\n\nexport default tool({\n id: \"linear-list-projects\",\n description:\n \"List all projects in the Linear workspace. Returns project details including name, state, progress, and associated teams.\",\n inputSchema: defineSchema((v) => v.object({\n limit: v\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of projects to return\"),\n includeArchived: v\n .boolean()\n .default(false)\n .describe(\"Whether to include archived projects in results\"),\n }))(),\n async execute({ limit, includeArchived }) {\n const projects = await listProjects({ limit, includeArchived });\n\n return projects.map((project) => ({\n id: project.id,\n name: project.name,\n description: project.description,\n state: project.state,\n progress: Math.round(project.progress * 100),\n url: project.url,\n lead: project.lead\n ? { id: project.lead.id, name: project.lead.name }\n : null,\n teams: project.teams.nodes.map((team) => ({\n id: team.id,\n name: team.name,\n key: team.key,\n })),\n createdAt: project.createdAt,\n updatedAt: project.updatedAt,\n }));\n },\n});\n",
382
- "tools/list-teams.ts": "import { tool } from \"veryfront/tool\";\nimport { getTeams } from \"../lib/linear-client.ts\";\n\nexport default tool({\n id: \"linear-list-teams\",\n description:\n \"List teams in the Linear workspace. Use this to find the team ID required when creating issues.\",\n async execute() {\n const teams = await getTeams();\n\n return teams.map((team) => ({\n id: team.id,\n name: team.name,\n key: team.key,\n }));\n },\n});\n",
383
- "tools/list-users.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { listUsers } from \"../lib/linear-client.ts\";\n\nexport default tool({\n id: \"linear-list-users\",\n description:\n \"List users in the Linear workspace. Use this to find assignee user IDs before assigning issues.\",\n inputSchema: defineSchema((v) => v.object({\n limit: v\n .number()\n .min(1)\n .max(100)\n .default(50)\n .describe(\"Maximum number of users to return\"),\n }))(),\n async execute({ limit }) {\n const users = await listUsers({ limit });\n\n return users.map((user) => ({\n id: user.id,\n name: user.name,\n displayName: user.displayName,\n email: user.email,\n active: user.active,\n avatarUrl: user.avatarUrl,\n }));\n },\n});\n",
384
- "tools/list-workflow-states.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { getWorkflowStates } from \"../lib/linear-client.ts\";\n\nexport default tool({\n id: \"linear-list-workflow-states\",\n description:\n \"List workflow states for a Linear team. Use this to find a state ID before updating an issue status.\",\n inputSchema: defineSchema((v) => v.object({\n teamId: v.string().describe(\"Linear team ID\"),\n }))(),\n async execute({ teamId }) {\n const states = await getWorkflowStates(teamId);\n\n return states.map((state) => ({\n id: state.id,\n name: state.name,\n type: state.type,\n }));\n },\n});\n",
385
- "tools/search-issues.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { searchIssues } from \"../lib/linear-client.ts\";\n\nexport default tool({\n id: \"linear-search-issues\",\n description:\n \"Search for Linear issues by title or description. Returns matching issues with their details including status, assignee, and team.\",\n inputSchema: defineSchema((v) => v.object({\n query: v.string().describe(\"Search query to find issues (searches in title and description)\"),\n limit: v.number().min(1).max(50).default(10).describe(\"Maximum number of results to return\"),\n includeArchived: v\n .boolean()\n .default(false)\n .describe(\"Whether to include archived issues in results\"),\n }))(),\n async execute({ query, limit, includeArchived }) {\n const issues = await searchIssues(query, { limit, includeArchived });\n\n return issues.map((issue) => {\n const assignee = issue.assignee\n ? { name: issue.assignee.name, email: issue.assignee.email }\n : null;\n\n const project = issue.project ? { name: issue.project.name } : null;\n\n return {\n id: issue.id,\n identifier: issue.identifier,\n title: issue.title,\n description: issue.description,\n priority: issue.priorityLabel,\n status: issue.state.name,\n statusType: issue.state.type,\n assignee,\n team: { name: issue.team.name, key: issue.team.key },\n project,\n labels: issue.labels.nodes.map((label) => ({\n name: label.name,\n color: label.color,\n })),\n url: issue.url,\n createdAt: issue.createdAt,\n updatedAt: issue.updatedAt,\n };\n });\n },\n});\n",
386
- "tools/update-issue.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { updateIssue } from \"../lib/linear-client.ts\";\n\nexport default tool({\n id: \"linear-update-issue\",\n description:\n \"Update an existing Linear issue. You can change the title, description, status, priority, assignee, project, or labels.\",\n inputSchema: defineSchema((v) => v.object({\n issueId: v.string().describe(\"The ID of the issue to update\"),\n title: v.string().optional().describe(\"New title for the issue\"),\n description: v\n .string()\n .optional()\n .describe(\"New description for the issue (supports markdown)\"),\n priority: v\n .number()\n .min(0)\n .max(4)\n .optional()\n .describe(\n \"New priority level: 0=No priority, 1=Urgent, 2=High, 3=Medium, 4=Low\",\n ),\n stateId: v\n .string()\n .optional()\n .describe(\"New workflow state ID to move the issue to\"),\n assigneeId: v\n .string()\n .optional()\n .describe(\"User ID to assign the issue to (or null to unassign)\"),\n projectId: v.string().optional().describe(\"Project ID to move the issue to\"),\n labelIds: v\n .array(v.string())\n .optional()\n .describe(\"New array of label IDs (replaces existing labels)\"),\n }))(),\n async execute({\n issueId,\n title,\n description,\n priority,\n stateId,\n assigneeId,\n projectId,\n labelIds,\n }) {\n const issue = await updateIssue(issueId, {\n title,\n description,\n priority,\n stateId,\n assigneeId,\n projectId,\n labelIds,\n });\n\n return {\n id: issue.id,\n identifier: issue.identifier,\n title: issue.title,\n description: issue.description,\n priority: issue.priorityLabel,\n status: issue.state.name,\n statusType: issue.state.type,\n assignee: issue.assignee\n ? { name: issue.assignee.name, email: issue.assignee.email }\n : null,\n team: {\n name: issue.team.name,\n key: issue.team.key,\n },\n project: issue.project ? { name: issue.project.name } : null,\n labels: issue.labels.nodes.map(({ name, color }) => ({ name, color })),\n url: issue.url,\n updatedAt: issue.updatedAt,\n };\n },\n});\n"
387
- }
388
- },
389
- "integration:mixpanel": {
390
- "files": {
391
- ".env.example": "# Mixpanel Integration\n# Get your credentials at https://mixpanel.com/settings/project\n\n# Project Token - used for tracking events (ingestion)\nMIXPANEL_PROJECT_TOKEN=your_project_token_here\n\n# API Secret - used for querying and exporting data\nMIXPANEL_API_SECRET=your_api_secret_here\n\n# Project ID - found in your project settings URL\nMIXPANEL_PROJECT_ID=your_project_id_here\n",
392
- "lib/mixpanel-client.ts": "import { getApiSecret, getProjectId, getProjectToken } from \"./token-store.ts\";\n\nconst MIXPANEL_API_BASE = \"https://mixpanel.com/api\";\nconst MIXPANEL_TRACK_BASE = \"https://api.mixpanel.com\";\nconst MIXPANEL_DATA_BASE = \"https://data.mixpanel.com/api/2.0\";\n\nexport interface MixpanelEvent {\n event: string;\n properties: Record<string, unknown>;\n}\n\nexport interface MixpanelEventQuery {\n event?: string;\n from_date: string;\n to_date: string;\n where?: string;\n limit?: number;\n}\n\nexport interface MixpanelEventResult {\n event: string;\n properties: Record<string, unknown>;\n}\n\nexport interface MixpanelFunnel {\n funnel_id: number;\n name: string;\n steps: Array<{\n event: string;\n count: number;\n avg_time: number | null;\n overall_conv_ratio: number;\n step_conv_ratio: number;\n }>;\n data: {\n series: string[];\n values: Record<string, number[]>;\n };\n}\n\nexport interface MixpanelRetention {\n date: string;\n count: number;\n retention: Array<{\n day: number;\n count: number;\n rate: number;\n }>;\n}\n\nexport interface MixpanelCohort {\n id: number;\n name: string;\n description: string;\n count: number;\n created: string;\n is_visible: boolean;\n project_id: number;\n}\n\ninterface MixpanelError {\n error: string;\n request: string;\n}\n\nfunction getAuthHeader(): string {\n const apiSecret = getApiSecret();\n if (!apiSecret) {\n throw new Error(\n \"Not authenticated with Mixpanel. Please set MIXPANEL_API_SECRET.\",\n );\n }\n\n // Mixpanel uses Basic auth with API secret as username and empty password\n return `Basic ${btoa(`${apiSecret}:`)}`;\n}\n\nasync function mixpanelFetch<T>(\n baseUrl: string,\n endpoint: string,\n options: RequestInit & { params?: Record<string, string | number | boolean> } = {},\n): Promise<T> {\n const url = new URL(`${baseUrl}${endpoint}`);\n\n for (const [key, value] of Object.entries(options.params ?? {})) {\n url.searchParams.append(key, String(value));\n }\n\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n ...(options.headers as Record<string, string> | undefined),\n };\n\n if (baseUrl === MIXPANEL_DATA_BASE || baseUrl === MIXPANEL_API_BASE) {\n headers.Authorization = getAuthHeader();\n }\n\n const response = await fetch(url.toString(), { ...options, headers });\n\n if (response.ok) return (await response.json()) as T;\n\n let errorMessage = `Mixpanel API error: ${response.status} ${response.statusText}`;\n\n try {\n const errorData = (await response.json()) as MixpanelError;\n if (errorData.error) errorMessage = `Mixpanel API error: ${errorData.error}`;\n } catch {\n // If parsing JSON fails, use default error message\n }\n\n throw new Error(errorMessage);\n}\n\nexport async function trackEvent(\n event: string,\n properties: Record<string, unknown>,\n distinctId: string,\n): Promise<{ status: number; error?: string }> {\n const projectToken = getProjectToken();\n if (!projectToken) {\n throw new Error(\n \"Not authenticated with Mixpanel. Please set MIXPANEL_PROJECT_TOKEN.\",\n );\n }\n\n const payload = {\n event,\n properties: {\n ...properties,\n token: projectToken,\n distinct_id: distinctId,\n time: Date.now(),\n },\n };\n\n const response = await fetch(`${MIXPANEL_TRACK_BASE}/track`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify([payload]),\n });\n\n if (response.ok) {\n return (await response.json()) as { status: number; error?: string };\n }\n\n const text = await response.text();\n return {\n status: 0,\n error: `Failed to track event: ${response.status} ${text}`,\n };\n}\n\nexport async function queryEvents(\n from: string,\n to: string,\n event?: string,\n): Promise<MixpanelEventResult[]> {\n const projectId = getProjectId();\n if (!projectId) {\n throw new Error(\"Project ID not set. Please set MIXPANEL_PROJECT_ID.\");\n }\n\n const params: Record<string, string> = { from_date: from, to_date: to };\n if (event) params.event = JSON.stringify([event]);\n\n const response = await mixpanelFetch<string[]>(\n MIXPANEL_DATA_BASE,\n \"/export\",\n { params },\n );\n\n if (!Array.isArray(response)) return [];\n\n const events: MixpanelEventResult[] = [];\n\n for (const line of response) {\n if (typeof line !== \"string\" || !line.trim()) continue;\n\n try {\n const parsed = JSON.parse(line) as MixpanelEventResult;\n events.push({ event: parsed.event, properties: parsed.properties });\n } catch {\n // Skip malformed lines\n }\n }\n\n return events;\n}\n\nexport async function getFunnel(\n funnelId: number,\n from: string,\n to: string,\n): Promise<MixpanelFunnel> {\n const params: Record<string, string | number> = {\n funnel_id: funnelId,\n from_date: from,\n to_date: to,\n unit: \"day\",\n };\n\n return mixpanelFetch<MixpanelFunnel>(MIXPANEL_DATA_BASE, \"/funnels\", {\n params,\n });\n}\n\nexport async function getRetention(\n from: string,\n to: string,\n event: string,\n retentionType: \"birth\" | \"compounded\" = \"birth\",\n): Promise<MixpanelRetention[]> {\n const params: Record<string, string> = {\n from_date: from,\n to_date: to,\n retention_type: retentionType,\n born_event: event,\n event,\n unit: \"day\",\n };\n\n const response = await mixpanelFetch<Record<string, MixpanelRetention>>(\n MIXPANEL_DATA_BASE,\n \"/retention\",\n { params },\n );\n\n return Object.entries(response).map(([date, data]) => ({ date, ...data }));\n}\n\nexport async function listCohorts(): Promise<MixpanelCohort[]> {\n const projectId = getProjectId();\n if (!projectId) {\n throw new Error(\"Project ID not set. Please set MIXPANEL_PROJECT_ID.\");\n }\n\n return mixpanelFetch<MixpanelCohort[]>(\n MIXPANEL_API_BASE,\n \"/2.0/cohorts/list\",\n { params: { project_id: projectId } },\n );\n}\n\nexport function formatDate(date: Date): string {\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, \"0\");\n const day = String(date.getDate()).padStart(2, \"0\");\n return `${year}-${month}-${day}`;\n}\n\nexport function getDateRange(days: number): { from: string; to: string } {\n const to = new Date();\n const from = new Date();\n from.setDate(from.getDate() - days);\n\n return { from: formatDate(from), to: formatDate(to) };\n}\n\nexport function calculateFunnelConversionRate(funnel: MixpanelFunnel): number {\n if (!funnel.steps || funnel.steps.length < 2) return 0;\n\n const firstStep = funnel.steps[0];\n const lastStep = funnel.steps[funnel.steps.length - 1];\n\n if (!firstStep || !lastStep || firstStep.count === 0) return 0;\n\n return (lastStep.count / firstStep.count) * 100;\n}\n",
393
- "tools/get-funnel.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { calculateFunnelConversionRate, getFunnel } from \"../lib/mixpanel-client.ts\";\n\nexport default tool({\n id: \"mixpanel-get-funnel\",\n description:\n \"Retrieve funnel analysis data from Mixpanel. Analyze conversion rates and user drop-off at each step of a funnel.\",\n inputSchema: defineSchema((v) => v.object({\n funnelId: v\n .number()\n .describe(\"The numeric ID of the funnel (found in Mixpanel funnel URL or settings)\"),\n from: v.string().describe(\"Start date in YYYY-MM-DD format (e.g., '2024-01-01')\"),\n to: v.string().describe(\"End date in YYYY-MM-DD format (e.g., '2024-01-31')\"),\n }))(),\n async execute({ funnelId, from, to }) {\n const funnel = await getFunnel(funnelId, from, to);\n const overallConversionRate = calculateFunnelConversionRate(funnel);\n\n return {\n funnelId: funnel.funnel_id,\n name: funnel.name,\n dateRange: { from, to },\n overallConversionRate: `${overallConversionRate.toFixed(2)}%`,\n steps: funnel.steps.map((step, index) => {\n const averageTime = step.avg_time\n ? `${(step.avg_time / 60).toFixed(1)} minutes`\n : \"N/A\";\n\n return {\n stepNumber: index + 1,\n event: step.event,\n count: step.count,\n overallConversionRate: `${(step.overall_conv_ratio * 100).toFixed(2)}%`,\n stepConversionRate: `${(step.step_conv_ratio * 100).toFixed(2)}%`,\n averageTime,\n };\n }),\n data: funnel.data,\n };\n },\n});\n",
394
- "tools/get-retention.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { getRetention } from \"../lib/mixpanel-client.ts\";\n\nfunction formatRate(rate: number): string {\n return `${(rate * 100).toFixed(2)}%`;\n}\n\ntype RetentionCohort = { retention: Array<{ day: number; rate: number }> };\n\nfunction averageRetention(retention: RetentionCohort[], day: number): string {\n if (retention.length === 0) return \"N/A\";\n\n const total = retention.reduce((sum, cohort) => {\n const rate = cohort.retention.find((r) => r.day === day)?.rate ?? 0;\n return sum + rate;\n }, 0);\n\n return formatRate(total / retention.length);\n}\n\nexport default tool({\n id: \"mixpanel-get-retention\",\n description:\n \"Analyze user retention cohorts in Mixpanel. Understand how many users return after performing an initial event.\",\n inputSchema: defineSchema((v) => v.object({\n from: v.string().describe(\"Start date in YYYY-MM-DD format (e.g., '2024-01-01')\"),\n to: v.string().describe(\"End date in YYYY-MM-DD format (e.g., '2024-01-31')\"),\n event: v.string().describe(\"The event to analyze retention for (e.g., 'App Opened', 'Sign Up')\"),\n retentionType: v\n .enum([\"birth\", \"compounded\"])\n .optional()\n .default(\"birth\")\n .describe(\"Retention type: 'birth' (first time users) or 'compounded' (all users who did the event)\"),\n }))(),\n async execute({ from, to, event, retentionType }): Promise<{\n event: string;\n retentionType: \"birth\" | \"compounded\";\n dateRange: { from: string; to: string };\n cohorts: Array<{\n date: string;\n initialCount: number;\n retention: Array<{ day: number; count: number; rate: string }>;\n }>;\n summary: {\n totalCohorts: number;\n averageDay1Retention: string;\n averageDay7Retention: string;\n };\n }> {\n const retention = await getRetention(from, to, event, retentionType);\n\n const cohorts = retention.map((cohort) => ({\n date: cohort.date,\n initialCount: cohort.count,\n retention: cohort.retention.map((r) => ({\n day: r.day,\n count: r.count,\n rate: formatRate(r.rate),\n })),\n }));\n\n return {\n event,\n retentionType,\n dateRange: { from, to },\n cohorts,\n summary: {\n totalCohorts: retention.length,\n averageDay1Retention: averageRetention(retention, 1),\n averageDay7Retention: averageRetention(retention, 7),\n },\n };\n },\n});\n",
395
- "tools/list-cohorts.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { listCohorts } from \"../lib/mixpanel-client.ts\";\n\nexport default tool({\n id: \"mixpanel-list-cohorts\",\n description:\n \"List all user cohorts defined in your Mixpanel project. Cohorts are saved user segments based on properties or behaviors.\",\n inputSchema: defineSchema((v) => v.object({\n includeHidden: v\n .boolean()\n .optional()\n .default(false)\n .describe(\"Include hidden cohorts in the results (defaults to false)\"),\n }))(),\n async execute({ includeHidden }) {\n const allCohorts = await listCohorts();\n const cohorts = includeHidden\n ? allCohorts\n : allCohorts.filter((c) => c.is_visible);\n\n const totalUsers = cohorts.reduce((sum, c) => sum + c.count, 0);\n\n if (cohorts.length === 0) {\n return {\n total: 0,\n cohorts: [],\n summary: {\n totalUsers: 0,\n largestCohort: \"N/A\",\n smallestCohort: \"N/A\",\n },\n };\n }\n\n let largest = cohorts[0];\n let smallest = cohorts[0];\n\n for (const c of cohorts) {\n if (c.count > largest.count) largest = c;\n if (c.count < smallest.count) smallest = c;\n }\n\n return {\n total: cohorts.length,\n cohorts: cohorts.map((cohort) => ({\n id: cohort.id,\n name: cohort.name,\n description: cohort.description,\n count: cohort.count,\n created: cohort.created,\n isVisible: cohort.is_visible,\n projectId: cohort.project_id,\n })),\n summary: {\n totalUsers,\n largestCohort: largest.name,\n smallestCohort: smallest.name,\n },\n };\n },\n});\n",
396
- "tools/query-events.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { queryEvents } from \"../lib/mixpanel-client.ts\";\n\nexport default tool({\n id: \"mixpanel-query-events\",\n description:\n \"Query and export event data from Mixpanel. Retrieve events within a date range, optionally filtered by event name.\",\n inputSchema: defineSchema((v) => v.object({\n from: v\n .string()\n .describe(\"Start date in YYYY-MM-DD format (e.g., '2024-01-01')\"),\n to: v\n .string()\n .describe(\"End date in YYYY-MM-DD format (e.g., '2024-01-31')\"),\n event: v\n .string()\n .optional()\n .describe(\n \"Optional: Filter by specific event name (e.g., 'Page Viewed'). If not provided, returns all events.\",\n ),\n limit: v\n .number()\n .optional()\n .default(100)\n .describe(\"Maximum number of events to return (defaults to 100)\"),\n }))(),\n async execute({ from, to, event, limit }) {\n const events = await queryEvents(from, to, event);\n const limitedEvents = events.slice(0, limit);\n\n return {\n total: events.length,\n returned: limitedEvents.length,\n dateRange: { from, to },\n eventFilter: event ?? \"all\",\n events: limitedEvents.map(({ event, properties }) => ({ event, properties })),\n };\n },\n});\n",
397
- "tools/track-event.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { trackEvent } from \"../lib/mixpanel-client.ts\";\n\nexport default tool({\n id: \"mixpanel-track-event\",\n description:\n \"Track a custom event in Mixpanel. Capture user actions, page views, or any custom analytics event with properties.\",\n inputSchema: defineSchema((v) => v.object({\n event: v\n .string()\n .describe(\n \"Event name (e.g., 'Button Clicked', 'Page Viewed', 'Purchase Completed')\",\n ),\n distinctId: v\n .string()\n .describe(\n \"Unique identifier for the user or session (e.g., user ID, email, or anonymous ID)\",\n ),\n properties: v\n .record(v.string(), v.unknown())\n .optional()\n .describe(\n \"Additional properties to attach to the event (e.g., {product_id: '123', price: 29.99, category: 'electronics'})\",\n ),\n }))(),\n async execute({ event, distinctId, properties }) {\n const eventProperties = properties ?? {};\n const result = await trackEvent(event, eventProperties, distinctId);\n\n if (result.status !== 1) {\n return { success: false, error: result.error ?? \"Failed to track event\" };\n }\n\n return {\n success: true,\n event: {\n name: event,\n distinctId,\n properties: eventProperties,\n timestamp: new Date().toISOString(),\n },\n message: \"Event tracked successfully\",\n };\n },\n});\n"
398
- }
399
- },
400
- "integration:neon": {
401
- "files": {
402
- ".env.example": "# Neon Integration\n# Create an API key at https://console.neon.tech/app/settings/api-keys\n# Get your connection string from your Neon project dashboard\n\nNEON_API_KEY=your_api_key_here\nDATABASE_URL=postgres://user:password@ep-xxxx.region.neon.tech/dbname?sslmode=require\n",
403
- "app/api/auth/neon/route.ts": "import { setApiKey } from \"../../../../lib/token-store.ts\";\n\nexport async function POST(request: Request): Promise<Response> {\n try {\n const { apiKey, databaseUrl } = await request.json();\n\n if (!apiKey) {\n return Response.json({ error: \"API key is required\" }, { status: 400 });\n }\n\n const response = await fetch(\"https://console.neon.tech/api/v2/projects\", {\n headers: {\n Authorization: `Bearer ${apiKey}`,\n Accept: \"application/json\",\n },\n });\n\n if (!response.ok) {\n return Response.json({ error: \"Invalid API key\" }, { status: 401 });\n }\n\n setApiKey(apiKey, databaseUrl);\n\n return Response.json({\n success: true,\n message: \"Successfully authenticated with Neon\",\n });\n } catch (error) {\n console.error(\"Neon auth error:\", error);\n return Response.json({ error: \"Authentication failed\" }, { status: 500 });\n }\n}\n\nexport function GET(): Response {\n return Response.json({\n authenticated: false,\n message: \"Use POST to authenticate with API key\",\n });\n}\n",
404
- "lib/neon-client.ts": "import { getApiKey, getDatabaseUrl } from \"./token-store.ts\";\nimport { Client } from \"pg\";\n\nconst NEON_API_BASE_URL = \"https://console.neon.tech/api/v2\";\n\ninterface NeonProject {\n id: string;\n platform_id: string;\n region_id: string;\n name: string;\n provisioner: string;\n default_endpoint_settings?: {\n autoscaling_limit_min_cu: number;\n autoscaling_limit_max_cu: number;\n suspend_timeout_seconds: number;\n };\n settings?: {\n quota?: {\n active_time_seconds?: number;\n compute_time_seconds?: number;\n written_data_bytes?: number;\n data_transfer_bytes?: number;\n };\n };\n pg_version: number;\n store_passwords: boolean;\n creation_source: string;\n created_at: string;\n updated_at: string;\n proxy_host: string;\n branch_logical_size_limit: number;\n branch_logical_size_limit_bytes: number;\n cpu_used_sec: number;\n maintenance_starts_at?: string;\n}\n\ninterface NeonBranch {\n id: string;\n project_id: string;\n parent_id?: string;\n parent_lsn?: string;\n parent_timestamp?: string;\n name: string;\n current_state: string;\n pending_state?: string;\n logical_size?: number;\n creation_source: string;\n primary?: boolean;\n default?: boolean;\n protected?: boolean;\n cpu_used_sec: number;\n compute_time_sec?: number;\n active_time_sec?: number;\n written_data_bytes?: number;\n data_transfer_bytes?: number;\n created_at: string;\n updated_at: string;\n}\n\ninterface NeonProjectsResponse {\n projects: NeonProject[];\n}\n\ninterface NeonBranchesResponse {\n branches: NeonBranch[];\n}\n\ninterface NeonEndpoint {\n host: string;\n id: string;\n project_id: string;\n branch_id: string;\n autoscaling_limit_min_cu: number;\n autoscaling_limit_max_cu: number;\n region_id: string;\n type: string;\n current_state: string;\n settings: {\n pg_settings?: Record<string, string>;\n };\n pooler_enabled: boolean;\n pooler_mode?: string;\n disabled: boolean;\n passwordless_access: boolean;\n creation_source: string;\n created_at: string;\n updated_at: string;\n proxy_host: string;\n suspend_timeout_seconds: number;\n provisioner: string;\n}\n\ninterface NeonEndpointsResponse {\n endpoints: NeonEndpoint[];\n}\n\ninterface TableInfo {\n tablename: string;\n schemaname: string;\n tableowner: string;\n}\n\ninterface ColumnInfo {\n column_name: string;\n data_type: string;\n is_nullable: string;\n column_default: string | null;\n character_maximum_length: number | null;\n}\n\nasync function neonFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const apiKey = getApiKey() ?? process.env.NEON_API_KEY;\n if (!apiKey) {\n throw new Error(\"Not authenticated with Neon. Please set NEON_API_KEY.\");\n }\n\n const response = await fetch(`${NEON_API_BASE_URL}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${apiKey}`,\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({}))) as { message?: string };\n throw new Error(\n `Neon API error: ${response.status} ${error.message ?? response.statusText}`,\n );\n }\n\n return response.json() as Promise<T>;\n}\n\nexport async function listProjects(): Promise<NeonProject[]> {\n const { projects } = await neonFetch<NeonProjectsResponse>(\"/projects\");\n return projects;\n}\n\nexport function getProject(projectId: string): Promise<NeonProject> {\n return neonFetch<NeonProject>(`/projects/${projectId}`);\n}\n\nexport async function listBranches(projectId: string): Promise<NeonBranch[]> {\n const { branches } = await neonFetch<NeonBranchesResponse>(\n `/projects/${projectId}/branches`,\n );\n return branches;\n}\n\nexport async function createBranch(\n projectId: string,\n options: {\n name?: string;\n parentId?: string;\n parentLsn?: string;\n parentTimestamp?: string;\n },\n): Promise<NeonBranch> {\n const branch: Record<string, unknown> = {\n name: options.name,\n ...(options.parentId ? { parent_id: options.parentId } : {}),\n ...(options.parentLsn ? { parent_lsn: options.parentLsn } : {}),\n ...(options.parentTimestamp ? { parent_timestamp: options.parentTimestamp } : {}),\n };\n\n const { branch: createdBranch } = await neonFetch<{ branch: NeonBranch }>(\n `/projects/${projectId}/branches`,\n {\n method: \"POST\",\n body: JSON.stringify({ branch }),\n },\n );\n\n return createdBranch;\n}\n\nexport async function listEndpoints(projectId: string): Promise<NeonEndpoint[]> {\n const { endpoints } = await neonFetch<NeonEndpointsResponse>(\n `/projects/${projectId}/endpoints`,\n );\n return endpoints;\n}\n\nasync function getDbClient(): Promise<Client> {\n const databaseUrl = getDatabaseUrl();\n if (!databaseUrl) {\n throw new Error(\n \"DATABASE_URL not configured. Please set DATABASE_URL environment variable.\",\n );\n }\n\n const client = new Client({\n connectionString: databaseUrl,\n ssl: { rejectUnauthorized: false },\n });\n\n await client.connect();\n return client;\n}\n\nexport async function query<T = Record<string, unknown>>(\n sql: string,\n params?: unknown[],\n): Promise<{ rows: T[]; rowCount: number }> {\n const client = await getDbClient();\n\n try {\n const result = await client.query(sql, params);\n return { rows: result.rows as T[], rowCount: result.rowCount ?? 0 };\n } finally {\n await client.end();\n }\n}\n\nexport async function listTables(schema: string = \"public\"): Promise<TableInfo[]> {\n const result = await query<TableInfo>(\n `SELECT tablename, schemaname, tableowner\n FROM pg_tables\n WHERE schemaname = $1\n ORDER BY tablename`,\n [schema],\n );\n\n return result.rows;\n}\n\nexport async function describeTable(\n tableName: string,\n schema: string = \"public\",\n): Promise<{ tableName: string; schema: string; columns: ColumnInfo[] }> {\n const result = await query<ColumnInfo>(\n `SELECT\n column_name,\n data_type,\n is_nullable,\n column_default,\n character_maximum_length\n FROM information_schema.columns\n WHERE table_schema = $1 AND table_name = $2\n ORDER BY ordinal_position`,\n [schema, tableName],\n );\n\n return { tableName, schema, columns: result.rows };\n}\n\nexport async function getTableRowCount(\n tableName: string,\n schema: string = \"public\",\n): Promise<number> {\n if (!/^[a-zA-Z0-9_]+$/.test(schema)) {\n throw new Error('Invalid schema name: must contain only letters, numbers, and underscores');\n }\n if (!/^[a-zA-Z0-9_]+$/.test(tableName)) {\n throw new Error('Invalid table name: must contain only letters, numbers, and underscores');\n }\n const result = await query<{ count: string }>(\n `SELECT COUNT(*) as count FROM \"${schema}\".\"${tableName}\"`,\n );\n\n return parseInt(result.rows[0]?.count ?? \"0\", 10);\n}\n",
405
- "tools/describe-table.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { describeTable, getTableRowCount } from \"../lib/neon-client.ts\";\n\nexport default tool({\n id: \"neon-describe-table\",\n description:\n \"Get detailed schema information for a specific table including column names, data types, nullability, defaults, and constraints.\",\n inputSchema: defineSchema((v) => v.object({\n tableName: v.string().describe(\"Name of the table to describe\"),\n schema: v\n .string()\n .default(\"public\")\n .describe(\"Schema name where the table is located\"),\n }))(),\n async execute({ tableName, schema }): Promise<{\n tableName: string;\n schema: string;\n rowCount: number | undefined;\n columnCount: number;\n columns: Array<{\n name: string;\n type: string;\n nullable: boolean;\n default: unknown;\n maxLength: number | null;\n }>;\n }> {\n const tableInfo = await describeTable(tableName, schema);\n const rowCount = await getTableRowCount(tableName, schema).catch(\n () => undefined,\n );\n\n return {\n tableName: tableInfo.tableName,\n schema: tableInfo.schema,\n rowCount,\n columnCount: tableInfo.columns.length,\n columns: tableInfo.columns.map((col) => ({\n name: col.column_name,\n type: col.data_type,\n nullable: col.is_nullable === \"YES\",\n default: col.column_default,\n maxLength: col.character_maximum_length,\n })),\n };\n },\n});\n",
406
- "tools/list-branches.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { listBranches } from \"../lib/neon-client.ts\";\n\nexport default tool({\n id: \"neon-list-branches\",\n description:\n \"List all branches for a specific Neon project. Branches are isolated database environments that can be created from any point in time.\",\n inputSchema: defineSchema((v) => v.object({\n projectId: v.string().describe(\"The ID of the Neon project\"),\n }))(),\n async execute({ projectId }) {\n const branches = await listBranches(projectId);\n\n return branches.map((branch) => ({\n id: branch.id,\n projectId: branch.project_id,\n name: branch.name,\n currentState: branch.current_state,\n pendingState: branch.pending_state,\n primary: branch.primary,\n default: branch.default,\n protected: branch.protected,\n parentId: branch.parent_id,\n parentLsn: branch.parent_lsn,\n parentTimestamp: branch.parent_timestamp,\n logicalSize: branch.logical_size,\n createdAt: branch.created_at,\n updatedAt: branch.updated_at,\n cpuUsedSec: branch.cpu_used_sec,\n computeTimeSec: branch.compute_time_sec,\n activeTimeSec: branch.active_time_sec,\n }));\n },\n});\n",
407
- "tools/list-projects.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { listProjects } from \"../lib/neon-client.ts\";\n\nexport default tool({\n id: \"neon-list-projects\",\n description:\n \"List all Neon projects in your account. Returns project details including name, region, PostgreSQL version, and creation date.\",\n inputSchema: defineSchema((v) => v.object({}))(),\n async execute() {\n const projects = await listProjects();\n\n return projects.map((project) => {\n const settings = project.default_endpoint_settings;\n\n return {\n id: project.id,\n name: project.name,\n region: project.region_id,\n pgVersion: project.pg_version,\n proxyHost: project.proxy_host,\n createdAt: project.created_at,\n updatedAt: project.updated_at,\n cpuUsedSec: project.cpu_used_sec,\n autoscaling: settings\n ? {\n minCu: settings.autoscaling_limit_min_cu,\n maxCu: settings.autoscaling_limit_max_cu,\n suspendTimeout: settings.suspend_timeout_seconds,\n }\n : undefined,\n };\n });\n },\n});\n",
408
- "tools/list-tables.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { getTableRowCount, listTables } from \"../lib/neon-client.ts\";\n\nexport default tool({\n id: \"neon-list-tables\",\n description:\n \"List all tables in the connected database. Returns table names, schemas, and row counts to help understand the database structure.\",\n inputSchema: defineSchema((v) => v.object({\n schema: v.string().default(\"public\").describe(\"Schema name to list tables from\"),\n includeRowCounts: v\n .boolean()\n .default(false)\n .describe(\"Whether to include row counts for each table (slower but more informative)\"),\n }))(),\n async execute({ schema, includeRowCounts }) {\n const tables = await listTables(schema);\n\n const results = await Promise.all(\n tables.map(async (table) => {\n const result: {\n tablename: string;\n schemaname: string;\n tableowner: string;\n rowCount?: number;\n } = {\n tablename: table.tablename,\n schemaname: table.schemaname,\n tableowner: table.tableowner,\n };\n\n if (!includeRowCounts) return result;\n\n try {\n result.rowCount = await getTableRowCount(table.tablename, schema);\n } catch {\n result.rowCount = undefined;\n }\n\n return result;\n }),\n );\n\n return {\n schema,\n tableCount: results.length,\n tables: results,\n };\n },\n});\n",
409
- "tools/query-database.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { query } from \"../lib/neon-client.ts\";\n\nexport default tool({\n id: \"neon-query-database\",\n description:\n \"Execute SQL queries against the connected Neon database. Supports parameterized queries for safety. Use this to retrieve, analyze, or search data.\",\n inputSchema: defineSchema((v) => v.object({\n sql: v.string().describe(\"SQL query to execute. Use $1, $2, etc. for parameters\"),\n params: v\n .array(v.union([v.string(), v.number(), v.boolean(), v.null()]))\n .optional()\n .describe(\"Optional array of parameter values for the query\"),\n limit: v.number().min(1).max(1000).default(100).describe(\"Maximum number of rows to return\"),\n }))(),\n async execute({ sql, params, limit }) {\n const trimmedSql = sql.trim();\n const isSelectQuery = /^SELECT/i.test(trimmedSql);\n const hasLimit = /LIMIT\\s+\\d+/i.test(trimmedSql);\n\n const finalSql = isSelectQuery && !hasLimit ? `${trimmedSql} LIMIT ${limit}` : trimmedSql;\n const result = await query(finalSql, params);\n\n return {\n rows: result.rows,\n rowCount: result.rowCount,\n limited: isSelectQuery && result.rowCount >= limit,\n };\n },\n});\n"
410
- }
411
- },
412
- "integration:notion": {
413
- "files": {
414
- ".env.example": "# Notion Integration\n# Create an integration at https://www.notion.so/my-integrations\n# Make sure to enable \"Public Integration\" for OAuth\n\nNOTION_CLIENT_ID=your_client_id_here\nNOTION_CLIENT_SECRET=your_client_secret_here\n",
415
- "app/api/auth/notion/callback/route.ts": "import { createOAuthCallbackHandler, notionConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\n\nexport const GET = createOAuthCallbackHandler(notionConfig, { tokenStore });\n",
416
- "app/api/auth/notion/route.ts": "import { createOAuthInitHandler, notionConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../lib/token-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../lib/user-id.ts\";\n\nexport const GET = createOAuthInitHandler(notionConfig, {\n tokenStore,\n getUserId: requireUserIdFromRequest,\n});\n",
417
- "lib/notion-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst NOTION_API_VERSION = \"2022-06-28\";\nconst NOTION_BASE_URL = \"https://api.notion.com/v1\";\n\ninterface NotionResponse<T> {\n object: string;\n results?: T[];\n next_cursor?: string | null;\n has_more?: boolean;\n}\n\ninterface NotionPage {\n id: string;\n object: \"page\";\n created_time: string;\n last_edited_time: string;\n parent: { type: string; database_id?: string; page_id?: string };\n properties: Record<string, NotionProperty>;\n url: string;\n}\n\ninterface NotionDatabase {\n id: string;\n object: \"database\";\n title: Array<{ plain_text: string }>;\n properties: Record<string, { type: string }>;\n}\n\ninterface NotionBlock {\n id: string;\n type: string;\n [key: string]: unknown;\n}\n\ninterface NotionProperty {\n type: string;\n title?: Array<{ plain_text: string }>;\n rich_text?: Array<{ plain_text: string }>;\n [key: string]: unknown;\n}\n\nasync function notionFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with Notion. Please connect your account.\");\n }\n\n const response = await fetch(`${NOTION_BASE_URL}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n \"Notion-Version\": NOTION_API_VERSION,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({} as { message?: string }))) as {\n message?: string;\n };\n throw new Error(\n `Notion API error: ${response.status} ${error.message ?? response.statusText}`,\n );\n }\n\n return response.json();\n}\n\nexport async function searchNotion(\n query: string,\n options?: {\n filter?: { property: \"object\"; value: \"page\" | \"database\" };\n pageSize?: number;\n },\n): Promise<Array<NotionPage | NotionDatabase>> {\n const body: Record<string, unknown> = {\n query,\n ...(options?.filter ? { filter: options.filter } : {}),\n ...(options?.pageSize ? { page_size: options.pageSize } : {}),\n };\n\n const response = await notionFetch<NotionResponse<NotionPage | NotionDatabase>>(\"/search\", {\n method: \"POST\",\n body: JSON.stringify(body),\n });\n\n return response.results ?? [];\n}\n\nexport function getPage(pageId: string): Promise<NotionPage> {\n return notionFetch<NotionPage>(`/pages/${pageId}`);\n}\n\nexport async function getPageContent(pageId: string): Promise<NotionBlock[]> {\n const response = await notionFetch<NotionResponse<NotionBlock>>(`/blocks/${pageId}/children`);\n return response.results ?? [];\n}\n\nexport async function queryDatabase(\n databaseId: string,\n options?: {\n filter?: Record<string, unknown>;\n sorts?: Array<{ property: string; direction: \"ascending\" | \"descending\" }>;\n pageSize?: number;\n },\n): Promise<NotionPage[]> {\n const body: Record<string, unknown> = {\n ...(options?.filter ? { filter: options.filter } : {}),\n ...(options?.sorts ? { sorts: options.sorts } : {}),\n ...(options?.pageSize ? { page_size: options.pageSize } : {}),\n };\n\n const response = await notionFetch<NotionResponse<NotionPage>>(\n `/databases/${databaseId}/query`,\n { method: \"POST\", body: JSON.stringify(body) },\n );\n\n return response.results ?? [];\n}\n\nexport function createPage(options: {\n parentId: string;\n parentType: \"database\" | \"page\";\n title: string;\n content?: string;\n properties?: Record<string, unknown>;\n}): Promise<NotionPage> {\n const parent =\n options.parentType === \"database\"\n ? { database_id: options.parentId }\n : { page_id: options.parentId };\n\n const properties: Record<string, unknown> = options.properties ?? {};\n\n if (options.parentType === \"database\") {\n properties.title ??= { title: [{ text: { content: options.title } }] };\n }\n\n const children: Array<Record<string, unknown>> = [];\n\n if (options.parentType === \"page\") {\n children.push({\n object: \"block\",\n type: \"heading_1\",\n heading_1: {\n rich_text: [{ type: \"text\", text: { content: options.title } }],\n },\n });\n }\n\n for (const paragraph of options.content?.split(\"\\n\\n\") ?? []) {\n const trimmed = paragraph.trim();\n if (!trimmed) continue;\n\n children.push({\n object: \"block\",\n type: \"paragraph\",\n paragraph: {\n rich_text: [{ type: \"text\", text: { content: trimmed } }],\n },\n });\n }\n\n return notionFetch<NotionPage>(\"/pages\", {\n method: \"POST\",\n body: JSON.stringify({\n parent,\n properties,\n children: children.length ? children : undefined,\n }),\n });\n}\n\nexport function extractPlainText(blocks: NotionBlock[]): string {\n const texts: string[] = [];\n\n for (const block of blocks) {\n const content = block[block.type] as { rich_text?: Array<{ plain_text: string }> } | undefined;\n const text = content?.rich_text?.map((t) => t.plain_text).join(\"\");\n if (text) texts.push(text);\n }\n\n return texts.join(\"\\n\\n\");\n}\n\nexport function getPageTitle(page: NotionPage): string {\n for (const prop of Object.values(page.properties)) {\n if (prop.type === \"title\" && prop.title) {\n return prop.title.map((t) => t.plain_text).join(\"\");\n }\n }\n\n return \"Untitled\";\n}\n\nexport function getDatabase(databaseId: string): Promise<NotionDatabase> {\n return notionFetch<NotionDatabase>(`/databases/${databaseId}`);\n}\n\nexport async function appendBlocks(options: {\n blockId: string;\n children: Array<Record<string, unknown>>;\n after?: string;\n}): Promise<NotionBlock[]> {\n const response = await notionFetch<NotionResponse<NotionBlock>>(\n `/blocks/${options.blockId}/children`,\n {\n method: \"PATCH\",\n body: JSON.stringify({\n children: options.children,\n after: options.after,\n }),\n },\n );\n\n return response.results ?? [];\n}\n\nexport function updatePage(options: {\n pageId: string;\n properties?: Record<string, unknown>;\n archived?: boolean;\n icon?: Record<string, unknown>;\n cover?: Record<string, unknown>;\n}): Promise<NotionPage> {\n return notionFetch<NotionPage>(`/pages/${options.pageId}`, {\n method: \"PATCH\",\n body: JSON.stringify({\n properties: options.properties,\n archived: options.archived,\n icon: options.icon,\n cover: options.cover,\n }),\n });\n}\n",
418
- "tools/append-blocks.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { appendBlocks } from \"../lib/notion-client.ts\";\n\nexport default tool({\n id: \"notion-append-blocks\",\n description: \"Append child blocks to a Notion page or block.\",\n inputSchema: defineSchema((v) => v.object({\n blockId: v.string().describe(\"The page or block ID to append children to\"),\n children: v.array(v.record(v.string(), v.unknown())).describe(\"Notion block objects to append\"),\n after: v.string().optional().describe(\"Optional existing child block ID after which to append\"),\n }))(),\n async execute({ blockId, children, after }) {\n const blocks = await appendBlocks({ blockId, children, after });\n\n return blocks.map((block) => ({\n id: block.id,\n type: block.type,\n block,\n }));\n },\n});\n",
419
- "tools/create-page.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createPage, getPageTitle } from \"../lib/notion-client.ts\";\n\nexport default tool({\n id: \"notion-create-page\",\n description:\n \"Create a new page in Notion. Can create as a subpage of an existing page or as a new entry in a database.\",\n inputSchema: defineSchema((v) => v.object({\n parentId: v.string().describe(\"The ID of the parent page or database\"),\n parentType: v.enum([\"page\", \"database\"]).describe(\"Whether the parent is a page or database\"),\n title: v.string().describe(\"Title of the new page\"),\n content: v\n .string()\n .optional()\n .describe(\n \"Initial content for the page (plain text, paragraphs separated by double newlines)\",\n ),\n }))(),\n async execute({ parentId, parentType, title, content }) {\n const page = await createPage({ parentId, parentType, title, content });\n\n return {\n id: page.id,\n title: getPageTitle(page),\n url: page.url,\n createdAt: page.created_time,\n };\n },\n});\n",
420
- "tools/get-database.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { getDatabase } from \"../lib/notion-client.ts\";\n\nexport default tool({\n id: \"notion-get-database\",\n description: \"Get Notion database metadata, title, and property schema.\",\n inputSchema: defineSchema((v) => v.object({\n databaseId: v.string().describe(\"The ID of the Notion database to retrieve\"),\n }))(),\n async execute({ databaseId }) {\n const database = await getDatabase(databaseId);\n\n return {\n id: database.id,\n title: database.title?.map((item) => item.plain_text).join(\"\") ?? \"\",\n properties: database.properties,\n };\n },\n});\n",
421
- "tools/get-page.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { getPage, getPageTitle } from \"../lib/notion-client.ts\";\n\nexport default tool({\n id: \"notion-get-page\",\n description: \"Get Notion page metadata and properties without reading child block content.\",\n inputSchema: defineSchema((v) => v.object({\n pageId: v.string().describe(\"The ID of the Notion page to retrieve\"),\n }))(),\n async execute({ pageId }) {\n const page = await getPage(pageId);\n\n return {\n id: page.id,\n title: getPageTitle(page),\n url: page.url,\n parent: page.parent,\n properties: page.properties,\n lastEdited: page.last_edited_time,\n createdAt: page.created_time,\n };\n },\n});\n",
422
- "tools/query-database.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { getPageTitle, queryDatabase } from \"../lib/notion-client.ts\";\n\nexport default tool({\n id: \"notion-query-database\",\n description: \"Query a Notion database to retrieve entries. Supports filtering and sorting.\",\n inputSchema: defineSchema((v) => v.object({\n databaseId: v.string().describe(\"The ID of the Notion database to query\"),\n sortProperty: v.string().optional().describe(\"Property name to sort by\"),\n sortDirection: v\n .enum([\"ascending\", \"descending\"])\n .default(\"descending\")\n .describe(\"Sort direction\"),\n limit: v.number().min(1).max(50).default(20).describe(\"Maximum number of results\"),\n }))(),\n async execute({ databaseId, sortProperty, sortDirection, limit }) {\n const results = await queryDatabase(databaseId, {\n sorts: sortProperty ? [{ property: sortProperty, direction: sortDirection }] : undefined,\n pageSize: limit,\n });\n\n return results.map((page) => {\n const properties: Record<string, string> = {};\n\n for (const [key, prop] of Object.entries(page.properties)) {\n if (prop.type !== \"title\" && prop.type !== \"rich_text\") continue;\n\n const text =\n prop.type === \"title\"\n ? prop.title?.map((t) => t.plain_text).join(\"\") ?? \"\"\n : prop.rich_text?.map((t) => t.plain_text).join(\"\") ?? \"\";\n\n properties[key] = text;\n }\n\n return {\n id: page.id,\n title: getPageTitle(page),\n url: page.url,\n properties,\n lastEdited: page.last_edited_time,\n };\n });\n },\n});\n",
423
- "tools/read-page.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { extractPlainText, getPage, getPageContent, getPageTitle } from \"../lib/notion-client.ts\";\n\nexport default tool({\n id: \"notion-read-page\",\n description: \"Read the content of a Notion page. Returns the page title and text content.\",\n inputSchema: defineSchema((v) => v.object({\n pageId: v.string().describe(\"The ID of the Notion page to read\"),\n }))(),\n async execute({ pageId }): Promise<{\n id: string;\n title: string;\n url: string;\n content: string;\n lastEdited: string;\n createdAt: string;\n }> {\n const [page, blocks] = await Promise.all([getPage(pageId), getPageContent(pageId)]);\n\n return {\n id: page.id,\n title: getPageTitle(page),\n url: page.url,\n content: extractPlainText(blocks),\n lastEdited: page.last_edited_time,\n createdAt: page.created_time,\n };\n },\n});\n",
424
- "tools/search-notion.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { getPageTitle, searchNotion } from \"../lib/notion-client.ts\";\n\nexport default tool({\n id: \"notion-search-notion\",\n description:\n \"Search for pages and databases in the connected Notion workspace. Returns matching pages with their titles and IDs.\",\n inputSchema: defineSchema((v) => v.object({\n query: v.string().describe(\"Search query to find pages or databases\"),\n type: v\n .enum([\"page\", \"database\", \"all\"])\n .default(\"all\")\n .describe(\"Type of objects to search for\"),\n limit: v\n .number()\n .min(1)\n .max(20)\n .default(10)\n .describe(\"Maximum number of results to return\"),\n }))(),\n async execute({ query, type, limit }) {\n const filter = type === \"all\" ? undefined : { property: \"object\", value: type };\n const results = await searchNotion(query, { filter, pageSize: limit });\n\n return results.map((item) => {\n if (item.object === \"page\") {\n return {\n id: item.id,\n type: \"page\",\n title: getPageTitle(item),\n url: item.url,\n lastEdited: item.last_edited_time,\n };\n }\n\n return {\n id: item.id,\n type: \"database\",\n title: item.title?.map((t) => t.plain_text).join(\"\") ?? \"\",\n url: item.url,\n };\n });\n },\n});\n",
425
- "tools/update-page.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { getPageTitle, updatePage } from \"../lib/notion-client.ts\";\n\nexport default tool({\n id: \"notion-update-page\",\n description: \"Update Notion page properties or archive/unarchive a page.\",\n inputSchema: defineSchema((v) => v.object({\n pageId: v.string().describe(\"The ID of the Notion page to update\"),\n properties: v.record(v.string(), v.unknown()).optional().describe(\"Page properties to update\"),\n archived: v.boolean().optional().describe(\"Whether the page should be archived\"),\n icon: v.record(v.string(), v.unknown()).optional().describe(\"Optional page icon object\"),\n cover: v.record(v.string(), v.unknown()).optional().describe(\"Optional page cover object\"),\n }))(),\n async execute({ pageId, properties, archived, icon, cover }) {\n const page = await updatePage({ pageId, properties, archived, icon, cover });\n\n return {\n id: page.id,\n title: getPageTitle(page),\n url: page.url,\n properties: page.properties,\n archived,\n lastEdited: page.last_edited_time,\n };\n },\n});\n"
426
- }
427
- },
428
- "integration:onedrive": {
429
- "files": {
430
- ".env.example": "# OneDrive Integration Environment Variables\n\n# Microsoft Azure App Client ID\n# Get this from https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade\nMICROSOFT_CLIENT_ID=your_client_id_here\n\n# Microsoft Azure App Client Secret\n# Get this from https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade\nMICROSOFT_CLIENT_SECRET=your_client_secret_here\n\n# Setup Instructions:\n# 1. Go to https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade\n# 2. Create a new app registration or select an existing one\n# 3. Note the Application (client) ID\n# 4. Create a new client secret under \"Certificates & secrets\"\n# 5. Add the OAuth2 redirect URI under \"Authentication\": http://localhost:3000/api/auth/onedrive/callback\n# 6. Grant the following Microsoft Graph API permissions under \"API permissions\":\n# - Files.Read\n# - Files.ReadWrite\n# - Files.Read.All\n# - Files.ReadWrite.All\n# - offline_access\n# 7. Grant admin consent if required by your organization\n",
431
- "app/api/auth/onedrive/callback/route.ts": "import { createOAuthCallbackHandler, oneDriveConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\n\nexport const GET = createOAuthCallbackHandler(oneDriveConfig, { tokenStore });\n",
432
- "app/api/auth/onedrive/route.ts": "import { createOAuthInitHandler, oneDriveConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../lib/token-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../lib/user-id.ts\";\n\nexport const GET = createOAuthInitHandler(oneDriveConfig, {\n tokenStore,\n getUserId: requireUserIdFromRequest,\n});\n",
433
- "lib/onedrive-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst GRAPH_API_URL = \"https://graph.microsoft.com/v1.0\";\n\nexport interface DriveItem {\n id: string;\n name: string;\n size?: number;\n createdDateTime: string;\n lastModifiedDateTime: string;\n webUrl: string;\n parentReference?: {\n driveId: string;\n id: string;\n path: string;\n };\n file?: {\n mimeType: string;\n hashes?: {\n quickXorHash?: string;\n sha1Hash?: string;\n sha256Hash?: string;\n };\n };\n folder?: {\n childCount: number;\n };\n \"@microsoft.graph.downloadUrl\"?: string;\n}\n\nexport interface FileMetadata {\n id: string;\n name: string;\n size: number;\n mimeType: string;\n createdDateTime: string;\n lastModifiedDateTime: string;\n webUrl: string;\n downloadUrl?: string;\n}\n\nexport interface FolderMetadata {\n id: string;\n name: string;\n childCount: number;\n createdDateTime: string;\n lastModifiedDateTime: string;\n webUrl: string;\n}\n\nexport interface ListFilesResult {\n value: DriveItem[];\n \"@odata.nextLink\"?: string;\n}\n\nexport interface SearchResult {\n value: DriveItem[];\n \"@odata.nextLink\"?: string;\n}\n\nasync function getTokenOrThrow(): Promise<string> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with OneDrive. Please connect your account.\");\n }\n return token;\n}\n\nasync function onedriveFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await getTokenOrThrow();\n const url = endpoint.startsWith(\"http\") ? endpoint : `${GRAPH_API_URL}${endpoint}`;\n\n const response = await fetch(url, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}));\n throw new Error(\n `OneDrive API error: ${response.status} ${error.error?.message ?? response.statusText}`,\n );\n }\n\n return response.json();\n}\n\nexport function listFiles(\n folderId: string = \"root\",\n options?: {\n orderBy?: string;\n top?: number;\n select?: string[];\n },\n): Promise<ListFilesResult> {\n const params = new URLSearchParams();\n\n if (options?.orderBy) params.set(\"$orderby\", options.orderBy);\n if (options?.top) params.set(\"$top\", options.top.toString());\n if (options?.select?.length) params.set(\"$select\", options.select.join(\",\"));\n\n const queryString = params.toString();\n const endpoint = `/me/drive/items/${folderId}/children${queryString ? `?${queryString}` : \"\"}`;\n\n return onedriveFetch<ListFilesResult>(endpoint);\n}\n\nexport function getFile(itemId: string): Promise<DriveItem> {\n return onedriveFetch<DriveItem>(`/me/drive/items/${itemId}`);\n}\n\nexport async function downloadFile(itemId: string): Promise<{\n content: string;\n metadata: FileMetadata;\n}> {\n const item = await getFile(itemId);\n\n if (!item.file) throw new Error(\"Item is not a file\");\n\n const downloadUrl = item[\"@microsoft.graph.downloadUrl\"];\n if (!downloadUrl) throw new Error(\"Download URL not available\");\n\n const response = await fetch(downloadUrl);\n if (!response.ok) throw new Error(`Failed to download file: ${response.statusText}`);\n\n const content = await response.text();\n\n return {\n content,\n metadata: {\n id: item.id,\n name: item.name,\n size: item.size ?? 0,\n mimeType: item.file.mimeType,\n createdDateTime: item.createdDateTime,\n lastModifiedDateTime: item.lastModifiedDateTime,\n webUrl: item.webUrl,\n downloadUrl,\n },\n };\n}\n\nexport async function uploadFile(\n fileName: string,\n content: string,\n parentFolderId: string = \"root\",\n): Promise<DriveItem> {\n const token = await getTokenOrThrow();\n const endpoint = `${GRAPH_API_URL}/me/drive/items/${parentFolderId}:/${fileName}:/content`;\n\n const response = await fetch(endpoint, {\n method: \"PUT\",\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/octet-stream\",\n },\n body: content,\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}));\n throw new Error(`Failed to upload file: ${error.error?.message ?? response.statusText}`);\n }\n\n return response.json();\n}\n\nexport function createFolder(\n folderName: string,\n parentFolderId: string = \"root\",\n): Promise<DriveItem> {\n return onedriveFetch<DriveItem>(`/me/drive/items/${parentFolderId}/children`, {\n method: \"POST\",\n body: JSON.stringify({\n name: folderName,\n folder: {},\n \"@microsoft.graph.conflictBehavior\": \"rename\",\n }),\n });\n}\n\nexport function searchFiles(\n query: string,\n options?: {\n top?: number;\n },\n): Promise<SearchResult> {\n const params = new URLSearchParams({ q: query });\n if (options?.top) params.set(\"$top\", options.top.toString());\n\n return onedriveFetch<SearchResult>(\n `/me/drive/root/search(q='${encodeURIComponent(query)}')?${params.toString()}`,\n );\n}\n\nexport async function deleteFile(itemId: string): Promise<void> {\n const token = await getTokenOrThrow();\n\n const response = await fetch(`${GRAPH_API_URL}/me/drive/items/${itemId}`, {\n method: \"DELETE\",\n headers: { Authorization: `Bearer ${token}` },\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}));\n throw new Error(`Failed to delete item: ${error.error?.message ?? response.statusText}`);\n }\n}\n\nexport function moveFile(\n itemId: string,\n newParentId: string,\n newName?: string,\n): Promise<DriveItem> {\n const body: Record<string, unknown> = {\n parentReference: { id: newParentId },\n ...(newName ? { name: newName } : {}),\n };\n\n return onedriveFetch<DriveItem>(`/me/drive/items/${itemId}`, {\n method: \"PATCH\",\n body: JSON.stringify(body),\n });\n}\n\nexport function formatFileSize(bytes: number): string {\n const units = [\"B\", \"KB\", \"MB\", \"GB\", \"TB\"];\n let size = bytes;\n let unitIndex = 0;\n\n while (size >= 1024 && unitIndex < units.length - 1) {\n size /= 1024;\n unitIndex++;\n }\n\n return `${size.toFixed(2)} ${units[unitIndex]}`;\n}\n\nexport function isFile(item: DriveItem): boolean {\n return item.file !== undefined;\n}\n\nexport function isFolder(item: DriveItem): boolean {\n return item.folder !== undefined;\n}\n",
434
- "tools/download-file.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { downloadFile, formatFileSize } from \"../lib/onedrive-client.ts\";\n\nexport default tool({\n id: \"onedrive-download-file\",\n description: \"Download file content from OneDrive. Returns the file content and metadata.\",\n inputSchema: defineSchema((v) => v.object({\n itemId: v.string().describe(\"The ID of the file to download\"),\n preview: v\n .boolean()\n .default(false)\n .describe(\"If true, return only first 1000 characters as preview\"),\n }))(),\n async execute({ itemId, preview }) {\n const { content, metadata } = await downloadFile(itemId);\n\n const shouldTruncate = preview && content.length > 1000;\n\n return {\n content: preview ? content.substring(0, 1000) : content,\n isTruncated: shouldTruncate,\n metadata: {\n id: metadata.id,\n name: metadata.name,\n size: metadata.size,\n sizeFormatted: formatFileSize(metadata.size),\n mimeType: metadata.mimeType,\n createdDateTime: metadata.createdDateTime,\n lastModifiedDateTime: metadata.lastModifiedDateTime,\n webUrl: metadata.webUrl,\n },\n message: shouldTruncate\n ? `Retrieved preview (first 1000 characters) of ${metadata.name}`\n : `Retrieved full content of ${metadata.name}`,\n };\n },\n});\n",
435
- "tools/list-files.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { formatFileSize, isFile, isFolder, listFiles } from \"../lib/onedrive-client.ts\";\n\nexport default tool({\n id: \"onedrive-list-files\",\n description:\n \"List files and folders in a OneDrive folder. Returns file/folder names, types, sizes, and modification dates.\",\n inputSchema: defineSchema((v) => v.object({\n folderId: v\n .string()\n .default(\"root\")\n .describe('Folder ID or \"root\" for the root folder'),\n orderBy: v\n .string()\n .optional()\n .describe('Order by field (e.g., \"name\", \"lastModifiedDateTime desc\")'),\n limit: v\n .number()\n .min(1)\n .max(200)\n .default(100)\n .describe(\"Maximum number of items to return\"),\n }))(),\n async execute({ folderId, orderBy, limit }) {\n const result = await listFiles(folderId, { orderBy, top: limit });\n\n const items = result.value.map((item) => {\n const baseInfo = {\n id: item.id,\n name: item.name,\n webUrl: item.webUrl,\n createdDateTime: item.createdDateTime,\n lastModifiedDateTime: item.lastModifiedDateTime,\n };\n\n if (isFile(item)) {\n const size = item.size ?? 0;\n\n return {\n ...baseInfo,\n type: \"file\" as const,\n size,\n sizeFormatted: formatFileSize(size),\n mimeType: item.file?.mimeType,\n };\n }\n\n if (isFolder(item)) {\n return {\n ...baseInfo,\n type: \"folder\" as const,\n childCount: item.folder?.childCount ?? 0,\n };\n }\n\n return { ...baseInfo, type: \"unknown\" as const };\n });\n\n return {\n items,\n count: items.length,\n hasMore: Boolean(result[\"@odata.nextLink\"]),\n };\n },\n});\n",
436
- "tools/search-files.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { formatFileSize, isFile, isFolder, searchFiles } from \"../lib/onedrive-client.ts\";\n\nexport default tool({\n id: \"onedrive-search-files\",\n description:\n \"Search for files and folders in OneDrive by name or content. Returns matching items with their paths and metadata.\",\n inputSchema: defineSchema((v) => v.object({\n query: v.string().describe(\"Search query to find files or folders\"),\n maxResults: v\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of results to return\"),\n }))(),\n async execute({ query, maxResults }) {\n const result = await searchFiles(query, { top: maxResults });\n\n const matches = result.value.map((item) => {\n const baseInfo = {\n id: item.id,\n name: item.name,\n webUrl: item.webUrl,\n createdDateTime: item.createdDateTime,\n lastModifiedDateTime: item.lastModifiedDateTime,\n parentPath: item.parentReference?.path,\n };\n\n if (isFile(item)) {\n const size = item.size ?? 0;\n\n return {\n ...baseInfo,\n type: \"file\" as const,\n size,\n sizeFormatted: formatFileSize(size),\n mimeType: item.file?.mimeType,\n };\n }\n\n if (isFolder(item)) {\n return {\n ...baseInfo,\n type: \"folder\" as const,\n childCount: item.folder?.childCount ?? 0,\n };\n }\n\n return { ...baseInfo, type: \"unknown\" as const };\n });\n\n return {\n matches,\n count: matches.length,\n hasMore: Boolean(result[\"@odata.nextLink\"]),\n query,\n };\n },\n});\n",
437
- "tools/upload-file.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { formatFileSize, uploadFile } from \"../lib/onedrive-client.ts\";\n\nexport default tool({\n id: \"onedrive-upload-file\",\n description:\n \"Upload or update a file in OneDrive. Can create new files or overwrite existing ones.\",\n inputSchema: defineSchema((v) => v.object({\n fileName: v\n .string()\n .describe(\"Name of the file to upload (e.g., 'notes.txt', 'document.pdf')\"),\n content: v.string().describe(\"The content to write to the file\"),\n parentFolderId: v\n .string()\n .default(\"root\")\n .describe('Parent folder ID where the file should be uploaded (default: \"root\")'),\n }))(),\n async execute({ fileName, content, parentFolderId }) {\n const name = fileName.trim();\n\n if (!name) throw new Error(\"Filename cannot be empty\");\n if (name.includes(\"/\") || name.includes(\"\\\\\")) {\n throw new Error(\"Filename cannot contain path separators\");\n }\n\n const result = await uploadFile(name, content, parentFolderId);\n const size = result.size ?? 0;\n\n return {\n success: true,\n id: result.id,\n name: result.name,\n webUrl: result.webUrl,\n size,\n sizeFormatted: formatFileSize(size),\n createdDateTime: result.createdDateTime,\n lastModifiedDateTime: result.lastModifiedDateTime,\n message: `File uploaded successfully: ${result.name}`,\n };\n },\n});\n"
438
- }
439
- },
440
- "integration:outlook": {
441
- "files": {
442
- ".env.example": "# Microsoft Outlook Integration\n# Get these from: https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade\n\n# Your Microsoft Azure App Client ID (Application ID)\nMICROSOFT_CLIENT_ID=your_client_id_here\n\n# Your Microsoft Azure App Client Secret\nMICROSOFT_CLIENT_SECRET=your_client_secret_here\n",
443
- "app/api/auth/outlook/callback/route.ts": "import { createOAuthCallbackHandler, outlookConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\n\nexport const GET = createOAuthCallbackHandler(outlookConfig, { tokenStore });\n",
444
- "app/api/auth/outlook/route.ts": "import { createOAuthInitHandler, outlookConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../lib/token-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../lib/user-id.ts\";\n\nexport const GET = createOAuthInitHandler(outlookConfig, {\n tokenStore,\n getUserId: requireUserIdFromRequest,\n});\n",
445
- "lib/outlook-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst GRAPH_BASE_URL = \"https://graph.microsoft.com/v1.0\";\n\ninterface GraphResponse<T> {\n value?: T[];\n \"@odata.nextLink\"?: string;\n}\n\ninterface OutlookEmailAddress {\n name?: string;\n address?: string;\n}\n\nexport interface OutlookContact {\n emailAddress?: OutlookEmailAddress | null;\n}\n\nexport interface OutlookMessage {\n id: string;\n subject: string;\n bodyPreview: string;\n body: {\n contentType: \"text\" | \"html\";\n content: string;\n };\n from?: OutlookContact | null;\n toRecipients: OutlookContact[];\n ccRecipients?: OutlookContact[] | null;\n receivedDateTime: string;\n sentDateTime: string;\n isRead: boolean;\n hasAttachments: boolean;\n importance: \"low\" | \"normal\" | \"high\";\n conversationId: string;\n webLink: string;\n}\n\nexport interface OutlookFolder {\n id: string;\n displayName: string;\n parentFolderId: string;\n childFolderCount: number;\n unreadItemCount: number;\n totalItemCount: number;\n}\n\nexport interface SendEmailOptions {\n to: string[];\n subject: string;\n body: string;\n cc?: string[];\n bcc?: string[];\n importance?: \"low\" | \"normal\" | \"high\";\n bodyType?: \"text\" | \"html\";\n}\n\nexport interface CreateDraftOptions extends SendEmailOptions {\n replyTo?: string[];\n categories?: string[];\n}\n\nasync function graphFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with Microsoft. Please connect your account.\");\n }\n\n const response = await fetch(`${GRAPH_BASE_URL}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}));\n throw new Error(\n `Microsoft Graph API error: ${response.status} ${error.error?.message ?? response.statusText}`,\n );\n }\n\n return response.json();\n}\n\nexport async function listEmails(options?: {\n folderId?: string;\n top?: number;\n skip?: number;\n filter?: string;\n orderBy?: string;\n}): Promise<OutlookMessage[]> {\n const params = new URLSearchParams();\n\n if (options?.top != null) params.set(\"$top\", options.top.toString());\n if (options?.skip != null) params.set(\"$skip\", options.skip.toString());\n if (options?.filter) params.set(\"$filter\", options.filter);\n if (options?.orderBy) params.set(\"$orderby\", options.orderBy);\n\n const folderPath = options?.folderId\n ? `/mailFolders/${options.folderId}/messages`\n : \"/messages\";\n\n const queryString = params.toString();\n const endpoint = queryString ? `${folderPath}?${queryString}` : folderPath;\n\n const response = await graphFetch<GraphResponse<OutlookMessage>>(endpoint);\n return response.value ?? [];\n}\n\nexport function getEmail(messageId: string): Promise<OutlookMessage> {\n return graphFetch<OutlookMessage>(`/messages/${messageId}`);\n}\n\nexport async function sendEmail(options: SendEmailOptions): Promise<void> {\n const message = buildMessage(options);\n\n await graphFetch(\"/sendMail\", {\n method: \"POST\",\n body: JSON.stringify({ message }),\n });\n}\n\nfunction buildMessage(options: CreateDraftOptions) {\n return {\n subject: options.subject,\n body: {\n contentType: options.bodyType ?? \"text\",\n content: options.body,\n },\n toRecipients: options.to.map((email) => ({\n emailAddress: { address: email },\n })),\n ccRecipients: options.cc?.map((email) => ({\n emailAddress: { address: email },\n })),\n bccRecipients: options.bcc?.map((email) => ({\n emailAddress: { address: email },\n })),\n replyTo: options.replyTo?.map((email) => ({\n emailAddress: { address: email },\n })),\n importance: options.importance ?? \"normal\",\n categories: options.categories,\n };\n}\n\nexport async function createDraft(options: CreateDraftOptions): Promise<OutlookMessage> {\n return graphFetch<OutlookMessage>(\"/messages\", {\n method: \"POST\",\n body: JSON.stringify(buildMessage(options)),\n });\n}\n\nexport async function searchEmails(options: {\n query: string;\n top?: number;\n skip?: number;\n}): Promise<OutlookMessage[]> {\n const params = new URLSearchParams({ $search: `\"${options.query}\"` });\n\n if (options.top != null) params.set(\"$top\", options.top.toString());\n if (options.skip != null) params.set(\"$skip\", options.skip.toString());\n\n const response = await graphFetch<GraphResponse<OutlookMessage>>(`/messages?${params.toString()}`);\n return response.value ?? [];\n}\n\nexport async function listFolders(): Promise<OutlookFolder[]> {\n const response = await graphFetch<GraphResponse<OutlookFolder>>(\"/mailFolders\");\n return response.value ?? [];\n}\n\nexport async function listThreads(options?: {\n folderId?: string;\n top?: number;\n filter?: string;\n orderBy?: string;\n}): Promise<OutlookMessage[]> {\n const messages = await listEmails({\n folderId: options?.folderId ?? \"inbox\",\n top: options?.top,\n filter: options?.filter,\n orderBy: options?.orderBy ?? \"receivedDateTime desc\",\n });\n\n const seenConversationIds = new Set<string>();\n return messages.filter((message) => {\n const conversationId = message.conversationId || message.id;\n if (seenConversationIds.has(conversationId)) return false;\n seenConversationIds.add(conversationId);\n return true;\n });\n}\n\nexport async function getThread(threadId: string, limit = 25): Promise<OutlookMessage[]> {\n const safeThreadId = threadId.replaceAll(\"'\", \"''\");\n const params = new URLSearchParams({\n $filter: `conversationId eq '${safeThreadId}'`,\n $top: String(limit),\n $select:\n \"id,conversationId,internetMessageId,subject,body,bodyPreview,from,sender,toRecipients,ccRecipients,bccRecipients,replyTo,receivedDateTime,sentDateTime,categories,isRead,importance,hasAttachments,webLink,flag\",\n });\n\n const response = await graphFetch<GraphResponse<OutlookMessage>>(`/messages?${params}`);\n return response.value ?? [];\n}\n\nasync function setReadState(messageId: string, isRead: boolean): Promise<void> {\n await graphFetch(`/messages/${messageId}`, {\n method: \"PATCH\",\n body: JSON.stringify({ isRead }),\n });\n}\n\nexport async function markAsRead(messageId: string): Promise<void> {\n await setReadState(messageId, true);\n}\n\nexport async function markAsUnread(messageId: string): Promise<void> {\n await setReadState(messageId, false);\n}\n\nexport async function deleteEmail(messageId: string): Promise<void> {\n await graphFetch(`/messages/${messageId}`, { method: \"DELETE\" });\n}\n\nexport async function moveEmail(messageId: string, destinationFolderId: string): Promise<void> {\n await graphFetch(`/messages/${messageId}/move`, {\n method: \"POST\",\n body: JSON.stringify({ destinationId: destinationFolderId }),\n });\n}\n\nexport function formatEmail(message: OutlookMessage): string {\n const fromContact = summarizeContact(message.from);\n const from = fromContact.name || fromContact.email || \"Unknown sender\";\n const to = summarizeContacts(message.toRecipients).map((r) => r.email).filter(Boolean).join(\", \");\n const date = new Date(message.receivedDateTime).toLocaleString();\n const read = message.isRead ? \"Yes\" : \"No\";\n\n return `From: ${from}\nTo: ${to}\nSubject: ${message.subject}\nDate: ${date}\nRead: ${read}\n\n${message.bodyPreview}`;\n}\n\nexport function summarizeContact(contact?: OutlookContact | null): { name: string; email: string } {\n const emailAddress = contact?.emailAddress;\n const email = emailAddress?.address ?? \"\";\n return {\n name: emailAddress?.name ?? email,\n email,\n };\n}\n\nexport function summarizeContacts(contacts?: OutlookContact[] | null): Array<{\n name: string;\n email: string;\n}> {\n return (contacts ?? []).map(summarizeContact);\n}\n",
446
- "tools/create-draft.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createDraft, summarizeContacts } from \"../lib/outlook-client.ts\";\n\nexport default tool({\n id: \"outlook-create-draft\",\n description:\n \"Create an Outlook email draft for human approval. This does not send the message.\",\n inputSchema: defineSchema((v) => v.object({\n to: v.array(v.string().email()).min(1).describe(\"Email addresses of recipients\"),\n subject: v.string().min(1).describe(\"Email subject line\"),\n body: v.string().min(1).describe(\"Email body content\"),\n cc: v.array(v.string().email()).optional().describe(\"Email addresses to CC\"),\n bcc: v.array(v.string().email()).optional().describe(\"Email addresses to BCC\"),\n replyTo: v.array(v.string().email()).optional().describe(\"Reply-to addresses\"),\n importance: v\n .enum([\"low\", \"normal\", \"high\"])\n .default(\"normal\")\n .describe(\"Email importance level\"),\n bodyType: v\n .enum([\"text\", \"html\"])\n .default(\"text\")\n .describe(\"Body content type\"),\n categories: v.array(v.string()).optional().describe(\"Outlook category names\"),\n }))(),\n async execute({ to, subject, body, cc, bcc, replyTo, importance, bodyType, categories }) {\n const draft = await createDraft({\n to,\n subject,\n body,\n cc,\n bcc,\n replyTo,\n importance,\n bodyType,\n categories,\n });\n\n return {\n draft: {\n id: draft.id,\n thread_id: draft.conversationId,\n subject: draft.subject,\n to: summarizeContacts(draft.toRecipients),\n preview: draft.bodyPreview,\n webLink: draft.webLink,\n isDraft: true,\n },\n };\n },\n});\n",
447
- "tools/get-email.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { getEmail, summarizeContact, summarizeContacts } from \"../lib/outlook-client.ts\";\n\nexport default tool({\n id: \"outlook-get-email\",\n description:\n \"Get detailed information about a specific email, including full body content, recipients, and metadata.\",\n inputSchema: defineSchema((v) => v.object({\n messageId: v.string().describe(\"The ID of the email message to retrieve\"),\n includeBody: v\n .boolean()\n .default(true)\n .describe(\"Include full email body content\"),\n }))(),\n async execute({ messageId, includeBody }) {\n const message = await getEmail(messageId);\n\n const body = includeBody\n ? {\n contentType: message.body.contentType,\n content: message.body.content,\n }\n : undefined;\n\n return {\n id: message.id,\n subject: message.subject,\n from: summarizeContact(message.from),\n to: summarizeContacts(message.toRecipients),\n cc: summarizeContacts(message.ccRecipients),\n body,\n bodyPreview: message.bodyPreview,\n receivedAt: message.receivedDateTime,\n sentAt: message.sentDateTime,\n isRead: message.isRead,\n hasAttachments: message.hasAttachments,\n importance: message.importance,\n conversationId: message.conversationId,\n webLink: message.webLink,\n };\n },\n});\n",
448
- "tools/get-thread.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { getThread, summarizeContact, summarizeContacts } from \"../lib/outlook-client.ts\";\n\nexport default tool({\n id: \"outlook-get-thread\",\n description:\n \"Get all Outlook messages in a conversation thread by thread_id. thread_id is the conversationId returned by list-threads.\",\n inputSchema: defineSchema((v) => v.object({\n thread_id: v\n .string()\n .min(1)\n .describe(\"Outlook conversationId returned by list-threads\"),\n limit: v\n .number()\n .min(1)\n .max(50)\n .default(25)\n .describe(\"Maximum messages to return for the thread\"),\n }))(),\n async execute({ thread_id, limit }) {\n const messages = await getThread(thread_id, limit);\n const firstMessage = messages[0];\n\n return {\n thread: {\n thread_id,\n subject: firstMessage?.subject ?? \"\",\n messages: messages.map((message) => ({\n id: message.id,\n subject: message.subject,\n from: summarizeContact(message.from),\n to: summarizeContacts(message.toRecipients),\n cc: summarizeContacts(message.ccRecipients),\n body: message.body,\n bodyPreview: message.bodyPreview,\n receivedAt: message.receivedDateTime,\n sentAt: message.sentDateTime,\n isRead: message.isRead,\n hasAttachments: message.hasAttachments,\n importance: message.importance,\n webLink: message.webLink,\n })),\n },\n };\n },\n});\n",
449
- "tools/list-emails.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { listEmails, summarizeContact, summarizeContacts } from \"../lib/outlook-client.ts\";\n\nexport default tool({\n id: \"outlook-list-emails\",\n description:\n \"List recent emails from inbox or a specific folder. Returns email metadata including subject, sender, date, and preview.\",\n inputSchema: defineSchema((v) => v.object({\n folderId: v\n .string()\n .optional()\n .describe(\"Folder ID to list emails from (default: inbox)\"),\n limit: v\n .number()\n .min(1)\n .max(50)\n .default(10)\n .describe(\"Maximum number of emails to return\"),\n unreadOnly: v.boolean().default(false).describe(\"Only return unread emails\"),\n orderBy: v\n .enum([\"receivedDateTime desc\", \"receivedDateTime asc\", \"subject\"])\n .default(\"receivedDateTime desc\")\n .describe(\"Sort order for emails\"),\n }))(),\n async execute({ folderId, limit, unreadOnly, orderBy }) {\n const messages = await listEmails({\n folderId,\n top: limit,\n filter: unreadOnly ? \"isRead eq false\" : undefined,\n orderBy,\n });\n\n return messages.map((msg) => ({\n id: msg.id,\n subject: msg.subject,\n from: summarizeContact(msg.from),\n to: summarizeContacts(msg.toRecipients),\n preview: msg.bodyPreview,\n receivedAt: msg.receivedDateTime,\n isRead: msg.isRead,\n hasAttachments: msg.hasAttachments,\n importance: msg.importance,\n webLink: msg.webLink,\n }));\n },\n});\n",
450
- "tools/list-folders.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { listFolders } from \"../lib/outlook-client.ts\";\n\nexport default tool({\n id: \"outlook-list-folders\",\n description:\n \"List all mail folders in the mailbox, including inbox, sent items, drafts, and custom folders.\",\n inputSchema: defineSchema((v) => v.object({}))(),\n async execute() {\n const folders = await listFolders();\n\n return folders.map((folder) => ({\n id: folder.id,\n name: folder.displayName,\n parentFolderId: folder.parentFolderId,\n childFolderCount: folder.childFolderCount,\n unreadItemCount: folder.unreadItemCount,\n totalItemCount: folder.totalItemCount,\n }));\n },\n});\n",
451
- "tools/list-threads.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { listThreads, summarizeContact } from \"../lib/outlook-client.ts\";\n\nexport default tool({\n id: \"outlook-list-threads\",\n description:\n \"List recent Outlook conversation threads for request-desk triage. Returns representative messages with thread_id values that can be passed to get-thread.\",\n inputSchema: defineSchema((v) => v.object({\n folderId: v\n .string()\n .default(\"inbox\")\n .describe(\"Folder ID or well-known folder name to inspect\"),\n limit: v\n .number()\n .min(1)\n .max(50)\n .default(10)\n .describe(\"Maximum recent messages to inspect as thread representatives\"),\n unreadOnly: v.boolean().default(false).describe(\"Only return unread messages\"),\n orderBy: v\n .enum([\"receivedDateTime desc\", \"receivedDateTime asc\", \"subject\"])\n .default(\"receivedDateTime desc\")\n .describe(\"Sort order for representative messages\"),\n }))(),\n async execute({ folderId, limit, unreadOnly, orderBy }) {\n const messages = await listThreads({\n folderId,\n top: limit,\n filter: unreadOnly ? \"isRead eq false\" : undefined,\n orderBy,\n });\n\n return {\n threads: messages.map((message) => ({\n thread_id: message.conversationId,\n messageId: message.id,\n subject: message.subject,\n from: summarizeContact(message.from),\n preview: message.bodyPreview,\n receivedAt: message.receivedDateTime,\n isRead: message.isRead,\n hasAttachments: message.hasAttachments,\n importance: message.importance,\n webLink: message.webLink,\n })),\n };\n },\n});\n",
452
- "tools/search-emails.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { searchEmails, summarizeContact, summarizeContacts } from \"../lib/outlook-client.ts\";\n\nexport default tool({\n id: \"outlook-search-emails\",\n description:\n \"Search emails by query string. Searches across subject, body, sender, and recipients. Supports advanced search syntax.\",\n inputSchema: defineSchema((v) => v.object({\n query: v\n .string()\n .min(1)\n .describe(\"Search query (searches subject, body, from, to fields)\"),\n limit: v\n .number()\n .min(1)\n .max(50)\n .default(10)\n .describe(\"Maximum number of results to return\"),\n }))(),\n async execute({ query, limit }) {\n const messages = await searchEmails({ query, top: limit });\n\n return {\n totalResults: messages.length,\n emails: messages.map((msg) => ({\n id: msg.id,\n subject: msg.subject,\n from: summarizeContact(msg.from),\n to: summarizeContacts(msg.toRecipients),\n preview: msg.bodyPreview,\n receivedAt: msg.receivedDateTime,\n isRead: msg.isRead,\n hasAttachments: msg.hasAttachments,\n importance: msg.importance,\n webLink: msg.webLink,\n })),\n };\n },\n});\n",
453
- "tools/send-email.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { sendEmail } from \"../lib/outlook-client.ts\";\n\nexport default tool({\n id: \"outlook-send-email\",\n description:\n \"Send a new email message. Supports multiple recipients, CC, BCC, and importance levels.\",\n inputSchema: defineSchema((v) => v.object({\n to: v.array(v.string().email()).min(1).describe(\"Email addresses of recipients\"),\n subject: v.string().min(1).describe(\"Email subject line\"),\n body: v.string().min(1).describe(\"Email body content\"),\n cc: v.array(v.string().email()).optional().describe(\"Email addresses to CC\"),\n bcc: v.array(v.string().email()).optional().describe(\"Email addresses to BCC\"),\n importance: v\n .enum([\"low\", \"normal\", \"high\"])\n .default(\"normal\")\n .describe(\"Email importance level\"),\n bodyType: v\n .enum([\"text\", \"html\"])\n .default(\"text\")\n .describe(\"Body content type (text or html)\"),\n }))(),\n async execute({ to, subject, body, cc, bcc, importance, bodyType }) {\n await sendEmail({ to, subject, body, cc, bcc, importance, bodyType });\n\n return {\n success: true,\n message: `Email sent successfully to ${to.join(\", \")}`,\n recipients: { to, cc, bcc },\n };\n },\n});\n"
454
- }
455
- },
456
- "integration:posthog": {
457
- "files": {
458
- ".env.example": "# PostHog Integration\n# Get your API key at https://app.posthog.com/project/settings\n# Create a Personal API Key for server-side access\n\nPOSTHOG_API_KEY=phx_your_api_key_here\nPOSTHOG_HOST=https://app.posthog.com\n",
459
- "lib/posthog-client.ts": "import { getApiKey } from \"./token-store.ts\";\n\nconst DEFAULT_POSTHOG_HOST = \"https://app.posthog.com\";\n\nexport interface PostHogInsight {\n id: number;\n name: string;\n derived_name: string | null;\n description: string;\n filters: Record<string, unknown>;\n result: unknown;\n created_at: string;\n created_by: {\n id: number;\n uuid: string;\n distinct_id: string;\n first_name: string;\n email: string;\n } | null;\n}\n\nexport interface PostHogTrend {\n action: {\n id: string;\n name: string;\n type: string;\n };\n label: string;\n count: number;\n data: number[];\n labels: string[];\n days: string[];\n}\n\nexport interface PostHogFunnel {\n id: number;\n name: string;\n steps: Array<{\n action_id: string;\n name: string;\n order: number;\n count: number;\n average_conversion_time: number | null;\n }>;\n filters: Record<string, unknown>;\n}\n\nexport interface PostHogFeatureFlag {\n id: number;\n name: string;\n key: string;\n filters: {\n groups: Array<{\n properties: unknown[];\n rollout_percentage: number | null;\n }>;\n };\n deleted: boolean;\n active: boolean;\n created_at: string;\n created_by: {\n id: number;\n uuid: string;\n distinct_id: string;\n first_name: string;\n email: string;\n } | null;\n is_simple_flag: boolean;\n rollout_percentage: number | null;\n ensure_experience_continuity: boolean;\n}\n\nexport interface PostHogPerson {\n id: string;\n name: string;\n distinct_ids: string[];\n properties: Record<string, unknown>;\n created_at: string;\n uuid: string;\n}\n\nexport interface PostHogEvent {\n event: string;\n distinct_id: string;\n properties?: Record<string, unknown>;\n timestamp?: string;\n}\n\ninterface PostHogListResponse<T> {\n next: string | null;\n previous: string | null;\n results: T[];\n}\n\ninterface PostHogError {\n detail?: string;\n}\n\nfunction getPostHogHost(): string {\n return process.env.POSTHOG_HOST ?? DEFAULT_POSTHOG_HOST;\n}\n\nfunction buildParams(\n options?: Record<string, string | number | boolean | undefined>,\n): Record<string, string | number | boolean> | undefined {\n if (!options) return undefined;\n\n const params: Record<string, string | number | boolean> = {};\n for (const [key, value] of Object.entries(options)) {\n if (value !== undefined) params[key] = value;\n }\n\n return Object.keys(params).length ? params : undefined;\n}\n\nasync function posthogFetch<T>(\n endpoint: string,\n options: RequestInit & { params?: Record<string, string | number | boolean> } = {},\n): Promise<T> {\n const apiKey = getApiKey();\n if (!apiKey) {\n throw new Error(\"Not authenticated with PostHog. Please set POSTHOG_API_KEY.\");\n }\n\n const url = new URL(`${getPostHogHost()}/api${endpoint}`);\n for (const [key, value] of Object.entries(options.params ?? {})) {\n url.searchParams.append(key, String(value));\n }\n\n const headers: Record<string, string> = {\n Authorization: `Bearer ${apiKey}`,\n \"Content-Type\": \"application/json\",\n ...(options.headers as Record<string, string> | undefined),\n };\n\n const response = await fetch(url.toString(), { ...options, headers });\n const data: unknown = await response.json();\n\n if (!response.ok) {\n const error = data as PostHogError;\n throw new Error(`PostHog API error: ${response.status} ${error.detail ?? response.statusText}`);\n }\n\n return data as T;\n}\n\nexport function getInsights(options?: {\n limit?: number;\n}): Promise<PostHogListResponse<PostHogInsight>> {\n return posthogFetch<PostHogListResponse<PostHogInsight>>(\"/projects/@current/insights/\", {\n params: buildParams({ limit: options?.limit }),\n });\n}\n\nexport function getTrends(options: {\n events?: Array<{ id: string; name?: string; type?: string }>;\n date_from?: string;\n date_to?: string;\n interval?: \"hour\" | \"day\" | \"week\" | \"month\";\n properties?: Record<string, unknown>[];\n}): Promise<PostHogTrend[]> {\n const body = {\n events: options.events ?? [{ id: \"$pageview\", name: \"$pageview\", type: \"events\" }],\n date_from: options.date_from ?? \"-7d\",\n date_to: options.date_to ?? \"now\",\n interval: options.interval ?? \"day\",\n properties: options.properties ?? [],\n };\n\n return posthogFetch<PostHogTrend[]>(\"/projects/@current/insights/trend/\", {\n method: \"POST\",\n body: JSON.stringify(body),\n });\n}\n\nexport function getFunnels(options: {\n events?: Array<{ id: string; name?: string; order: number }>;\n date_from?: string;\n date_to?: string;\n}): Promise<PostHogFunnel> {\n const body = {\n events: options.events ?? [],\n date_from: options.date_from ?? \"-7d\",\n date_to: options.date_to ?? \"now\",\n };\n\n return posthogFetch<PostHogFunnel>(\"/projects/@current/insights/funnel/\", {\n method: \"POST\",\n body: JSON.stringify(body),\n });\n}\n\nexport function getFeatureFlags(options?: {\n limit?: number;\n}): Promise<PostHogListResponse<PostHogFeatureFlag>> {\n return posthogFetch<PostHogListResponse<PostHogFeatureFlag>>(\n \"/projects/@current/feature_flags/\",\n { params: buildParams({ limit: options?.limit }) },\n );\n}\n\nexport function getFeatureFlag(flagId: number): Promise<PostHogFeatureFlag> {\n return posthogFetch<PostHogFeatureFlag>(`/projects/@current/feature_flags/${flagId}/`);\n}\n\nexport function listPersons(options?: {\n limit?: number;\n search?: string;\n}): Promise<PostHogListResponse<PostHogPerson>> {\n return posthogFetch<PostHogListResponse<PostHogPerson>>(\"/projects/@current/persons/\", {\n params: buildParams({ limit: options?.limit, search: options?.search }),\n });\n}\n\nexport function getPerson(personId: string): Promise<PostHogPerson> {\n return posthogFetch<PostHogPerson>(`/projects/@current/persons/${personId}/`);\n}\n\nexport function captureEvent(event: PostHogEvent): Promise<{ status: number }> {\n const body = {\n api_key: getApiKey(),\n event: event.event,\n distinct_id: event.distinct_id,\n properties: event.properties ?? {},\n timestamp: event.timestamp ?? new Date().toISOString(),\n };\n\n return posthogFetch<{ status: number }>(\"/capture/\", {\n method: \"POST\",\n body: JSON.stringify(body),\n });\n}\n\nexport function formatDate(dateString: string): string {\n return new Date(dateString).toISOString();\n}\n\nexport function calculateConversionRate(funnel: PostHogFunnel): number {\n if (funnel.steps.length < 2) return 0;\n\n const firstStep = funnel.steps[0];\n if (firstStep.count === 0) return 0;\n\n const lastStep = funnel.steps[funnel.steps.length - 1];\n return (lastStep.count / firstStep.count) * 100;\n}\n",
460
- "tools/capture-event.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { captureEvent } from \"../lib/posthog-client.ts\";\n\nexport default tool({\n id: \"posthog-capture-event\",\n description:\n \"Track a custom event in PostHog. Capture user actions, page views, or any custom analytics event.\",\n inputSchema: defineSchema((v) => v.object({\n event: v.string().describe(\"Event name (e.g., 'button_clicked', 'page_viewed')\"),\n distinctId: v.string().describe(\"Unique identifier for the user or session\"),\n properties: v\n .record(v.string(), v.unknown())\n .optional()\n .describe(\"Additional properties to attach to the event\"),\n timestamp: v\n .string()\n .optional()\n .describe(\"Event timestamp in ISO format (defaults to current time)\"),\n }))(),\n async execute({ event, distinctId, properties, timestamp }) {\n const result = await captureEvent({\n event,\n distinct_id: distinctId,\n properties,\n timestamp,\n });\n\n const success = result.status === 1 || result.status === 200;\n\n return {\n success,\n event: {\n name: event,\n distinctId,\n properties,\n timestamp: timestamp ?? new Date().toISOString(),\n },\n };\n },\n});\n",
461
- "tools/get-trends.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { getTrends } from \"../lib/posthog-client.ts\";\n\nexport default tool({\n id: \"posthog-get-trends\",\n description:\n \"Retrieve event trends and analytics data from PostHog. Analyze how events are trending over time.\",\n inputSchema: defineSchema((v) => v.object({\n events: v\n .array(\n v.object({\n id: v\n .string()\n .describe(\"Event ID or name (e.g., '$pageview', 'button_clicked')\"),\n name: v.string().optional().describe(\"Display name for the event\"),\n type: v.string().optional().default(\"events\").describe(\"Event type\"),\n }),\n )\n .optional()\n .describe(\"List of events to analyze (defaults to $pageview)\"),\n dateFrom: v\n .string()\n .optional()\n .default(\"-7d\")\n .describe(\"Start date in ISO format or relative (e.g., '-7d', '-30d')\"),\n dateTo: v\n .string()\n .optional()\n .default(\"now\")\n .describe(\"End date in ISO format or relative (e.g., 'now', '-1d')\"),\n interval: v\n .enum([\"hour\", \"day\", \"week\", \"month\"])\n .optional()\n .default(\"day\")\n .describe(\"Time interval for aggregation\"),\n }))(),\n async execute({ events, dateFrom, dateTo, interval }) {\n return getTrends({\n events,\n date_from: dateFrom,\n date_to: dateTo,\n interval,\n });\n },\n});\n",
462
- "tools/list-feature-flags.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { formatDate, getFeatureFlags } from \"../lib/posthog-client.ts\";\n\nexport default tool({\n id: \"posthog-list-feature-flags\",\n description:\n \"List all feature flags in your PostHog project. View flag status, rollout percentages, and configuration.\",\n inputSchema: defineSchema((v) => v.object({\n limit: v\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of feature flags to retrieve\"),\n }))(),\n async execute({ limit }) {\n const { results } = await getFeatureFlags({ limit });\n\n return {\n count: results.length,\n flags: results.map((flag) => {\n const createdBy = flag.created_by\n ? {\n name: flag.created_by.first_name,\n email: flag.created_by.email,\n }\n : null;\n\n return {\n id: flag.id,\n name: flag.name,\n key: flag.key,\n active: flag.active,\n deleted: flag.deleted,\n isSimpleFlag: flag.is_simple_flag,\n rolloutPercentage: flag.rollout_percentage,\n createdAt: formatDate(flag.created_at),\n createdBy,\n filters: flag.filters,\n };\n }),\n };\n },\n});\n",
463
- "tools/list-persons.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { formatDate, listPersons } from \"../lib/posthog-client.ts\";\n\nexport default tool({\n id: \"posthog-list-persons\",\n description:\n \"List persons/users tracked in PostHog. View user properties, distinct IDs, and activity.\",\n inputSchema: defineSchema((v) => v.object({\n limit: v\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of persons to retrieve\"),\n search: v\n .string()\n .optional()\n .describe(\"Search query to filter persons by properties or distinct ID\"),\n }))(),\n async execute({ limit, search }) {\n const { results } = await listPersons({ limit, search });\n\n return {\n count: results.length,\n persons: results.map((person) => ({\n id: person.id,\n uuid: person.uuid,\n name: person.name,\n distinctIds: person.distinct_ids,\n properties: person.properties,\n createdAt: formatDate(person.created_at),\n })),\n };\n },\n});\n"
464
- }
465
- },
466
- "integration:salesforce": {
467
- "files": {
468
- "app/api/auth/salesforce/callback/route.ts": "import { createOAuthCallbackHandler, salesforceConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\n\nexport const GET = createOAuthCallbackHandler(salesforceConfig, { tokenStore });\n",
469
- "app/api/auth/salesforce/route.ts": "import { createOAuthInitHandler, salesforceConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../lib/token-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../lib/user-id.ts\";\n\nexport const GET = createOAuthInitHandler(salesforceConfig, {\n tokenStore,\n getUserId: requireUserIdFromRequest,\n});\n",
470
- "lib/salesforce-client.ts": "import { getAccessToken, getInstanceUrl } from \"./token-store.ts\";\n\nconst API_VERSION = \"v59.0\";\n\ninterface SalesforceQueryResponse<T> {\n totalSize: number;\n done: boolean;\n records: T[];\n nextRecordsUrl?: string;\n}\n\ninterface SalesforceAccount {\n Id: string;\n Name: string;\n Type?: string;\n Industry?: string;\n Website?: string;\n Phone?: string;\n BillingStreet?: string;\n BillingCity?: string;\n BillingState?: string;\n BillingPostalCode?: string;\n BillingCountry?: string;\n NumberOfEmployees?: number;\n AnnualRevenue?: number;\n Description?: string;\n CreatedDate: string;\n LastModifiedDate: string;\n [key: string]: any;\n}\n\ninterface SalesforceContact {\n Id: string;\n FirstName?: string;\n LastName: string;\n Email?: string;\n Phone?: string;\n MobilePhone?: string;\n Title?: string;\n Department?: string;\n AccountId?: string;\n MailingStreet?: string;\n MailingCity?: string;\n MailingState?: string;\n MailingPostalCode?: string;\n MailingCountry?: string;\n Description?: string;\n CreatedDate: string;\n LastModifiedDate: string;\n [key: string]: any;\n}\n\ninterface SalesforceOpportunity {\n Id: string;\n Name: string;\n AccountId?: string;\n Amount?: number;\n StageName: string;\n Probability?: number;\n CloseDate: string;\n Type?: string;\n LeadSource?: string;\n Description?: string;\n NextStep?: string;\n IsClosed: boolean;\n IsWon: boolean;\n ForecastCategory?: string;\n CreatedDate: string;\n LastModifiedDate: string;\n [key: string]: any;\n}\n\ninterface SalesforceLead {\n Id: string;\n FirstName?: string;\n LastName: string;\n Company: string;\n Email?: string;\n Phone?: string;\n MobilePhone?: string;\n Title?: string;\n Status: string;\n LeadSource?: string;\n Industry?: string;\n Street?: string;\n City?: string;\n State?: string;\n PostalCode?: string;\n Country?: string;\n Website?: string;\n Description?: string;\n Rating?: string;\n CreatedDate: string;\n LastModifiedDate: string;\n [key: string]: any;\n}\n\n/** Validate a Salesforce record ID (15 or 18 character alphanumeric). */\nfunction validateSalesforceId(id: string, label: string): string {\n if (!/^[a-zA-Z0-9]{15,18}$/.test(id)) {\n throw new Error(`Invalid ${label}: must be a 15 or 18 character Salesforce ID`);\n }\n return id;\n}\n\n/** Escape a string value for use in SOQL single-quoted literals. */\nfunction escapeSoql(value: string): string {\n return value.replace(/\\\\/g, \"\\\\\\\\\").replace(/'/g, \"\\\\'\");\n}\n\n/** Validate a SOQL field name. */\nfunction validateFieldName(field: string): string {\n if (!/^[a-zA-Z][a-zA-Z0-9_.]*$/.test(field)) {\n throw new Error(`Invalid SOQL field name: ${field}`);\n }\n return field;\n}\n\nasync function salesforceFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with Salesforce. Please connect your account.\");\n }\n\n const instanceUrl = getInstanceUrl();\n if (!instanceUrl) {\n throw new Error(\"Salesforce instance URL not found. Please reconnect your account.\");\n }\n\n const url = endpoint.startsWith(\"http\")\n ? endpoint\n : `${instanceUrl}/services/data/${API_VERSION}${endpoint}`;\n\n const response = await fetch(url, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}));\n const message = error?.[0]?.message ?? error?.message ?? response.statusText;\n throw new Error(`Salesforce API error: ${response.status} ${message}`);\n }\n\n return response.json();\n}\n\nexport function query<T = any>(soql: string): Promise<SalesforceQueryResponse<T>> {\n return salesforceFetch<SalesforceQueryResponse<T>>(`/query?q=${encodeURIComponent(soql)}`);\n}\n\nfunction buildListSoql(params: {\n object: string;\n fields: string[];\n where?: string;\n limit: number;\n offset: number;\n}): string {\n const { object, fields, where, limit, offset } = params;\n\n fields.forEach((f) => validateFieldName(f));\n let soql = `SELECT ${fields.join(\", \")} FROM ${object}`;\n if (where) soql += ` WHERE ${where}`;\n soql += ` ORDER BY LastModifiedDate DESC LIMIT ${limit} OFFSET ${offset}`;\n\n return soql;\n}\n\nasync function getSingleRecord<T>(params: {\n object: string;\n id: string;\n fields: string[];\n notFoundMessage: string;\n}): Promise<T> {\n const { object, id, fields, notFoundMessage } = params;\n fields.forEach((f) => validateFieldName(f));\n validateSalesforceId(id, `${object} ID`);\n const soql = `SELECT ${fields.join(\", \")} FROM ${object} WHERE Id = '${id}'`;\n const result = await query<T>(soql);\n\n if (result.totalSize === 0) throw new Error(notFoundMessage);\n return result.records[0];\n}\n\n// ============================================================================\n// ACCOUNTS\n// ============================================================================\n\nexport function listAccounts(options?: {\n limit?: number;\n offset?: number;\n fields?: string[];\n}): Promise<SalesforceQueryResponse<SalesforceAccount>> {\n const limit = options?.limit ?? 10;\n const offset = options?.offset ?? 0;\n const fields = options?.fields ?? [\n \"Id\",\n \"Name\",\n \"Type\",\n \"Industry\",\n \"Website\",\n \"Phone\",\n \"BillingCity\",\n \"BillingState\",\n \"BillingCountry\",\n \"NumberOfEmployees\",\n \"AnnualRevenue\",\n \"CreatedDate\",\n \"LastModifiedDate\",\n ];\n\n return query<SalesforceAccount>(buildListSoql({ object: \"Account\", fields, limit, offset }));\n}\n\nexport function getAccount(accountId: string, fields?: string[]): Promise<SalesforceAccount> {\n const selectedFields = fields ?? [\n \"Id\",\n \"Name\",\n \"Type\",\n \"Industry\",\n \"Website\",\n \"Phone\",\n \"BillingStreet\",\n \"BillingCity\",\n \"BillingState\",\n \"BillingPostalCode\",\n \"BillingCountry\",\n \"NumberOfEmployees\",\n \"AnnualRevenue\",\n \"Description\",\n \"CreatedDate\",\n \"LastModifiedDate\",\n ];\n\n return getSingleRecord<SalesforceAccount>({\n object: \"Account\",\n id: accountId,\n fields: selectedFields,\n notFoundMessage: `Account with ID ${accountId} not found`,\n });\n}\n\nexport function createAccount(data: {\n Name: string;\n Type?: string;\n Industry?: string;\n Website?: string;\n Phone?: string;\n BillingStreet?: string;\n BillingCity?: string;\n BillingState?: string;\n BillingPostalCode?: string;\n BillingCountry?: string;\n NumberOfEmployees?: number;\n AnnualRevenue?: number;\n Description?: string;\n [key: string]: any;\n}): Promise<{ id: string; success: boolean; errors: any[] }> {\n return salesforceFetch(\"/sobjects/Account\", {\n method: \"POST\",\n body: JSON.stringify(data),\n });\n}\n\n// ============================================================================\n// CONTACTS\n// ============================================================================\n\nexport function listContacts(options?: {\n limit?: number;\n offset?: number;\n fields?: string[];\n accountId?: string;\n}): Promise<SalesforceQueryResponse<SalesforceContact>> {\n const limit = options?.limit ?? 10;\n const offset = options?.offset ?? 0;\n const fields = options?.fields ?? [\n \"Id\",\n \"FirstName\",\n \"LastName\",\n \"Email\",\n \"Phone\",\n \"Title\",\n \"Department\",\n \"AccountId\",\n \"MailingCity\",\n \"MailingState\",\n \"MailingCountry\",\n \"CreatedDate\",\n \"LastModifiedDate\",\n ];\n\n const where = options?.accountId\n ? (validateSalesforceId(options.accountId, \"accountId\"), `AccountId = '${options.accountId}'`)\n : undefined;\n\n return query<SalesforceContact>(buildListSoql({ object: \"Contact\", fields, where, limit, offset }));\n}\n\nexport function getContact(contactId: string, fields?: string[]): Promise<SalesforceContact> {\n const selectedFields = fields ?? [\n \"Id\",\n \"FirstName\",\n \"LastName\",\n \"Email\",\n \"Phone\",\n \"MobilePhone\",\n \"Title\",\n \"Department\",\n \"AccountId\",\n \"MailingStreet\",\n \"MailingCity\",\n \"MailingState\",\n \"MailingPostalCode\",\n \"MailingCountry\",\n \"Description\",\n \"CreatedDate\",\n \"LastModifiedDate\",\n ];\n\n return getSingleRecord<SalesforceContact>({\n object: \"Contact\",\n id: contactId,\n fields: selectedFields,\n notFoundMessage: `Contact with ID ${contactId} not found`,\n });\n}\n\nexport function createContact(data: {\n LastName: string;\n FirstName?: string;\n Email?: string;\n Phone?: string;\n MobilePhone?: string;\n Title?: string;\n Department?: string;\n AccountId?: string;\n MailingStreet?: string;\n MailingCity?: string;\n MailingState?: string;\n MailingPostalCode?: string;\n MailingCountry?: string;\n Description?: string;\n [key: string]: any;\n}): Promise<{ id: string; success: boolean; errors: any[] }> {\n return salesforceFetch(\"/sobjects/Contact\", {\n method: \"POST\",\n body: JSON.stringify(data),\n });\n}\n\n// ============================================================================\n// OPPORTUNITIES\n// ============================================================================\n\nexport function listOpportunities(options?: {\n limit?: number;\n offset?: number;\n fields?: string[];\n accountId?: string;\n}): Promise<SalesforceQueryResponse<SalesforceOpportunity>> {\n const limit = options?.limit ?? 10;\n const offset = options?.offset ?? 0;\n const fields = options?.fields ?? [\n \"Id\",\n \"Name\",\n \"AccountId\",\n \"Amount\",\n \"StageName\",\n \"Probability\",\n \"CloseDate\",\n \"Type\",\n \"LeadSource\",\n \"IsClosed\",\n \"IsWon\",\n \"ForecastCategory\",\n \"CreatedDate\",\n \"LastModifiedDate\",\n ];\n\n const where = options?.accountId\n ? (validateSalesforceId(options.accountId, \"accountId\"), `AccountId = '${options.accountId}'`)\n : undefined;\n\n return query<SalesforceOpportunity>(\n buildListSoql({ object: \"Opportunity\", fields, where, limit, offset }),\n );\n}\n\nexport function getOpportunity(opportunityId: string, fields?: string[]): Promise<SalesforceOpportunity> {\n const selectedFields = fields ?? [\n \"Id\",\n \"Name\",\n \"AccountId\",\n \"Amount\",\n \"StageName\",\n \"Probability\",\n \"CloseDate\",\n \"Type\",\n \"LeadSource\",\n \"Description\",\n \"NextStep\",\n \"IsClosed\",\n \"IsWon\",\n \"ForecastCategory\",\n \"CreatedDate\",\n \"LastModifiedDate\",\n ];\n\n return getSingleRecord<SalesforceOpportunity>({\n object: \"Opportunity\",\n id: opportunityId,\n fields: selectedFields,\n notFoundMessage: `Opportunity with ID ${opportunityId} not found`,\n });\n}\n\nexport function createOpportunity(data: {\n Name: string;\n StageName: string;\n CloseDate: string;\n AccountId?: string;\n Amount?: number;\n Probability?: number;\n Type?: string;\n LeadSource?: string;\n Description?: string;\n NextStep?: string;\n [key: string]: any;\n}): Promise<{ id: string; success: boolean; errors: any[] }> {\n return salesforceFetch(\"/sobjects/Opportunity\", {\n method: \"POST\",\n body: JSON.stringify(data),\n });\n}\n\n// ============================================================================\n// LEADS\n// ============================================================================\n\nexport function listLeads(options?: {\n limit?: number;\n offset?: number;\n fields?: string[];\n status?: string;\n}): Promise<SalesforceQueryResponse<SalesforceLead>> {\n const limit = options?.limit ?? 10;\n const offset = options?.offset ?? 0;\n const fields = options?.fields ?? [\n \"Id\",\n \"FirstName\",\n \"LastName\",\n \"Company\",\n \"Email\",\n \"Phone\",\n \"Title\",\n \"Status\",\n \"LeadSource\",\n \"Industry\",\n \"City\",\n \"State\",\n \"Country\",\n \"Rating\",\n \"CreatedDate\",\n \"LastModifiedDate\",\n ];\n\n const where = options?.status ? `Status = '${escapeSoql(options.status)}'` : undefined;\n\n return query<SalesforceLead>(buildListSoql({ object: \"Lead\", fields, where, limit, offset }));\n}\n\nexport function createLead(data: {\n LastName: string;\n Company: string;\n FirstName?: string;\n Email?: string;\n Phone?: string;\n MobilePhone?: string;\n Title?: string;\n Status?: string;\n LeadSource?: string;\n Industry?: string;\n Street?: string;\n City?: string;\n State?: string;\n PostalCode?: string;\n Country?: string;\n Website?: string;\n Description?: string;\n Rating?: string;\n [key: string]: any;\n}): Promise<{ id: string; success: boolean; errors: any[] }> {\n return salesforceFetch(\"/sobjects/Lead\", {\n method: \"POST\",\n body: JSON.stringify({ ...data, Status: data.Status ?? \"Open - Not Contacted\" }),\n });\n}\n\n// ============================================================================\n// HELPER FUNCTIONS\n// ============================================================================\n\nfunction formatPersonName(firstName?: string, lastName?: string, email?: string, fallback = \"Unnamed\"): string {\n const parts = [firstName, lastName].filter(Boolean);\n if (parts.length) return parts.join(\" \");\n return email ?? fallback;\n}\n\nexport function formatContactName(contact: SalesforceContact): string {\n return formatPersonName(contact.FirstName, contact.LastName, contact.Email, \"Unnamed Contact\");\n}\n\nexport function formatLeadName(lead: SalesforceLead): string {\n return formatPersonName(lead.FirstName, lead.LastName, lead.Email, \"Unnamed Lead\");\n}\n\nexport function formatAddress(\n street?: string,\n city?: string,\n state?: string,\n postalCode?: string,\n country?: string,\n): string {\n return [street, city, state, postalCode, country].filter(Boolean).join(\", \");\n}\n\nexport type {\n SalesforceAccount,\n SalesforceContact,\n SalesforceLead,\n SalesforceOpportunity,\n SalesforceQueryResponse,\n};\n",
471
- "tools/create-lead.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createLead, formatLeadName } from \"../lib/salesforce-client.ts\";\n\ntype Output = {\n id: string;\n name: string;\n lastName: string;\n firstName?: string;\n company: string;\n email?: string;\n phone?: string;\n title?: string;\n status: string;\n message: string;\n};\n\nexport default tool({\n id: \"salesforce-create-lead\",\n description:\n \"Create a new lead in Salesforce CRM. LastName and Company are required, other fields are optional.\",\n inputSchema: defineSchema((v) => v.object({\n lastName: v.string().describe(\"Last name (required)\"),\n company: v.string().describe(\"Company name (required)\"),\n firstName: v.string().optional().describe(\"First name\"),\n email: v.string().email().optional().describe(\"Email address\"),\n phone: v.string().optional().describe(\"Phone number\"),\n mobilePhone: v.string().optional().describe(\"Mobile phone number\"),\n title: v.string().optional().describe(\"Job title\"),\n status: v\n .string()\n .optional()\n .describe(\n 'Lead status (e.g., \"Open - Not Contacted\", \"Working - Contacted\", \"Closed - Converted\")',\n ),\n leadSource: v\n .string()\n .optional()\n .describe('Lead source (e.g., \"Web\", \"Phone Inquiry\", \"Partner Referral\")'),\n industry: v.string().optional().describe(\"Industry\"),\n street: v.string().optional().describe(\"Street address\"),\n city: v.string().optional().describe(\"City\"),\n state: v.string().optional().describe(\"State/Province\"),\n postalCode: v.string().optional().describe(\"Postal code\"),\n country: v.string().optional().describe(\"Country\"),\n website: v.string().optional().describe(\"Website URL\"),\n description: v.string().optional().describe(\"Description or notes about the lead\"),\n rating: v.string().optional().describe('Lead rating (e.g., \"Hot\", \"Warm\", \"Cold\")'),\n }))(),\n async execute(input): Promise<Output> {\n const leadData: Record<string, unknown> = {\n LastName: input.lastName,\n Company: input.company,\n };\n\n const optionalFields: Array<[keyof typeof input, string]> = [\n [\"firstName\", \"FirstName\"],\n [\"email\", \"Email\"],\n [\"phone\", \"Phone\"],\n [\"mobilePhone\", \"MobilePhone\"],\n [\"title\", \"Title\"],\n [\"status\", \"Status\"],\n [\"leadSource\", \"LeadSource\"],\n [\"industry\", \"Industry\"],\n [\"street\", \"Street\"],\n [\"city\", \"City\"],\n [\"state\", \"State\"],\n [\"postalCode\", \"PostalCode\"],\n [\"country\", \"Country\"],\n [\"website\", \"Website\"],\n [\"description\", \"Description\"],\n [\"rating\", \"Rating\"],\n ];\n\n for (const [inputKey, sfKey] of optionalFields) {\n const value = input[inputKey];\n if (value) leadData[sfKey] = value;\n }\n\n const result = await createLead(leadData);\n\n if (!result.success) {\n throw new Error(`Failed to create lead: ${JSON.stringify(result.errors)}`);\n }\n\n const name = formatLeadName({\n FirstName: input.firstName,\n LastName: input.lastName,\n Email: input.email,\n });\n\n return {\n id: result.id,\n name,\n lastName: input.lastName,\n firstName: input.firstName,\n company: input.company,\n email: input.email,\n phone: input.phone,\n title: input.title,\n status: input.status || \"Open - Not Contacted\",\n message: `Successfully created lead: ${name} at ${input.company}`,\n };\n },\n});\n",
472
- "tools/get-account.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { formatAddress, getAccount } from \"../lib/salesforce-client.ts\";\n\nexport default tool({\n id: \"salesforce-get-account\",\n description:\n \"Get detailed information about a specific account in Salesforce CRM by their account ID.\",\n inputSchema: defineSchema((v) => v.object({\n accountId: v\n .string()\n .describe(\"The Salesforce account ID (e.g., 001XXXXXXXXXXXXXXX)\"),\n fields: v\n .array(v.string())\n .optional()\n .describe(\n \"Additional fields to retrieve (e.g., Description, Owner.Name, ParentId)\",\n ),\n }))(),\n async execute({ accountId, fields }) {\n const account = await getAccount(accountId, fields);\n\n const billingAddress =\n formatAddress(\n account.BillingStreet,\n account.BillingCity,\n account.BillingState,\n account.BillingPostalCode,\n account.BillingCountry,\n ) || undefined;\n\n const additionalFields = fields?.length\n ? Object.fromEntries(\n fields\n .filter((field) => account[field] !== undefined)\n .map((field) => [field, account[field]]),\n )\n : undefined;\n\n return {\n id: account.Id,\n name: account.Name,\n type: account.Type,\n industry: account.Industry,\n website: account.Website,\n phone: account.Phone,\n billingAddress,\n billingStreet: account.BillingStreet,\n billingCity: account.BillingCity,\n billingState: account.BillingState,\n billingPostalCode: account.BillingPostalCode,\n billingCountry: account.BillingCountry,\n numberOfEmployees: account.NumberOfEmployees,\n annualRevenue: account.AnnualRevenue,\n description: account.Description,\n createdDate: account.CreatedDate,\n lastModifiedDate: account.LastModifiedDate,\n additionalFields,\n };\n },\n});\n",
473
- "tools/list-accounts.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { listAccounts } from \"../lib/salesforce-client.ts\";\n\nexport default tool({\n id: \"salesforce-list-accounts\",\n description:\n \"List accounts from your Salesforce CRM. Returns account information including name, type, industry, website, and billing details.\",\n inputSchema: defineSchema((v) => v.object({\n limit: v\n .number()\n .min(1)\n .max(100)\n .default(10)\n .describe(\"Maximum number of accounts to return\"),\n offset: v\n .number()\n .min(0)\n .default(0)\n .describe(\"Number of records to skip for pagination\"),\n fields: v\n .array(v.string())\n .optional()\n .describe(\n \"Additional fields to retrieve (e.g., Description, Owner.Name, ParentId)\",\n ),\n }))(),\n async execute({ limit, offset, fields }) {\n const response = await listAccounts({ limit, offset, fields });\n\n return {\n accounts: response.records.map((account) => {\n if (!fields?.length) {\n return {\n id: account.Id,\n name: account.Name,\n type: account.Type,\n industry: account.Industry,\n website: account.Website,\n phone: account.Phone,\n billingCity: account.BillingCity,\n billingState: account.BillingState,\n billingCountry: account.BillingCountry,\n numberOfEmployees: account.NumberOfEmployees,\n annualRevenue: account.AnnualRevenue,\n createdDate: account.CreatedDate,\n lastModifiedDate: account.LastModifiedDate,\n additionalFields: undefined,\n };\n }\n\n const additionalFields = Object.fromEntries(\n fields\n .filter((field) => account[field] !== undefined)\n .map((field) => [field, account[field]]),\n );\n\n return {\n id: account.Id,\n name: account.Name,\n type: account.Type,\n industry: account.Industry,\n website: account.Website,\n phone: account.Phone,\n billingCity: account.BillingCity,\n billingState: account.BillingState,\n billingCountry: account.BillingCountry,\n numberOfEmployees: account.NumberOfEmployees,\n annualRevenue: account.AnnualRevenue,\n createdDate: account.CreatedDate,\n lastModifiedDate: account.LastModifiedDate,\n additionalFields,\n };\n }),\n totalSize: response.totalSize,\n hasMore: !response.done,\n };\n },\n});\n",
474
- "tools/list-contacts.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { formatContactName, listContacts } from \"../lib/salesforce-client.ts\";\n\nexport default tool({\n id: \"salesforce-list-contacts\",\n description:\n \"List contacts from your Salesforce CRM. Returns contact information including name, email, phone, title, and account association.\",\n inputSchema: defineSchema((v) => v.object({\n limit: v.number().min(1).max(100).default(10).describe(\"Maximum number of contacts to return\"),\n offset: v.number().min(0).default(0).describe(\"Number of records to skip for pagination\"),\n accountId: v.string().optional().describe(\"Filter contacts by Account ID\"),\n fields: v\n .array(v.string())\n .optional()\n .describe(\"Additional fields to retrieve (e.g., Account.Name, Owner.Name, LeadSource)\"),\n }))(),\n async execute({ limit, offset, accountId, fields }) {\n const response = await listContacts({ limit, offset, accountId, fields });\n\n return {\n contacts: response.records.map((contact) => {\n const additionalFields = fields\n ? Object.fromEntries(\n fields.flatMap((field) => {\n const value = contact[field];\n return value === undefined ? [] : [[field, value]];\n }),\n )\n : undefined;\n\n return {\n id: contact.Id,\n name: formatContactName(contact),\n firstName: contact.FirstName,\n lastName: contact.LastName,\n email: contact.Email,\n phone: contact.Phone,\n mobilePhone: contact.MobilePhone,\n title: contact.Title,\n department: contact.Department,\n accountId: contact.AccountId,\n mailingCity: contact.MailingCity,\n mailingState: contact.MailingState,\n mailingCountry: contact.MailingCountry,\n createdDate: contact.CreatedDate,\n lastModifiedDate: contact.LastModifiedDate,\n additionalFields,\n };\n }),\n totalSize: response.totalSize,\n hasMore: !response.done,\n };\n },\n});\n",
475
- "tools/list-opportunities.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { listOpportunities } from \"../lib/salesforce-client.ts\";\n\nexport default tool({\n id: \"salesforce-list-opportunities\",\n description:\n \"List sales opportunities from your Salesforce CRM. Returns opportunity information including name, amount, stage, close date, and account association.\",\n inputSchema: defineSchema((v) => v.object({\n limit: v\n .number()\n .min(1)\n .max(100)\n .default(10)\n .describe(\"Maximum number of opportunities to return\"),\n offset: v\n .number()\n .min(0)\n .default(0)\n .describe(\"Number of records to skip for pagination\"),\n accountId: v.string().optional().describe(\"Filter opportunities by Account ID\"),\n fields: v\n .array(v.string())\n .optional()\n .describe(\"Additional fields to retrieve (e.g., Account.Name, Owner.Name, Description)\"),\n }))(),\n async execute({ limit, offset, accountId, fields }) {\n const response = await listOpportunities({ limit, offset, accountId, fields });\n\n return {\n opportunities: response.records.map((opportunity) => {\n let additionalFields: Record<string, unknown> | undefined;\n\n if (fields) {\n additionalFields = Object.fromEntries(\n fields\n .filter((field) => opportunity[field] !== undefined)\n .map((field) => [field, opportunity[field]]),\n );\n }\n\n return {\n id: opportunity.Id,\n name: opportunity.Name,\n accountId: opportunity.AccountId,\n amount: opportunity.Amount,\n stageName: opportunity.StageName,\n probability: opportunity.Probability,\n closeDate: opportunity.CloseDate,\n type: opportunity.Type,\n leadSource: opportunity.LeadSource,\n isClosed: opportunity.IsClosed,\n isWon: opportunity.IsWon,\n forecastCategory: opportunity.ForecastCategory,\n createdDate: opportunity.CreatedDate,\n lastModifiedDate: opportunity.LastModifiedDate,\n additionalFields,\n };\n }),\n totalSize: response.totalSize,\n hasMore: !response.done,\n };\n },\n});\n"
476
- }
477
- },
478
- "integration:sentry": {
479
- "files": {
480
- ".env.example": "# Sentry Integration\n# Create an Auth Token at https://sentry.io/settings/account/api/auth-tokens/\n# Find your organization slug in your Sentry URL: https://sentry.io/organizations/YOUR_ORG_SLUG/\n\nSENTRY_AUTH_TOKEN=your_auth_token_here\nSENTRY_ORG=your_organization_slug\n",
481
- "lib/sentry-client.ts": "import { getApiKey, getOrg } from \"./token-store.ts\";\n\nconst SENTRY_API_BASE_URL = \"https://sentry.io/api/0\";\n\nexport interface Organization {\n id: string;\n slug: string;\n name: string;\n dateCreated: string;\n status: {\n id: string;\n name: string;\n };\n avatar?: {\n avatarType: string;\n avatarUuid: string | null;\n };\n features: string[];\n}\n\nexport interface Project {\n id: string;\n slug: string;\n name: string;\n platform?: string;\n dateCreated: string;\n isBookmarked: boolean;\n isMember: boolean;\n features: string[];\n firstEvent: string | null;\n firstTransactionEvent: boolean;\n access: string[];\n hasAccess: boolean;\n hasCustomMetrics: boolean;\n hasMinifiedStackTrace: boolean;\n hasMonitors: boolean;\n hasProfiles: boolean;\n hasReplays: boolean;\n hasSessions: boolean;\n team?: {\n id: string;\n name: string;\n slug: string;\n };\n teams: Array<{\n id: string;\n name: string;\n slug: string;\n }>;\n eventProcessing: {\n symbolicationDegraded: boolean;\n };\n status: string;\n}\n\nexport interface Issue {\n id: string;\n shareId: string | null;\n shortId: string;\n title: string;\n culprit: string;\n permalink: string;\n logger: string | null;\n level: string;\n status: string;\n statusDetails: Record<string, unknown>;\n substatus: string | null;\n isPublic: boolean;\n platform: string;\n project: {\n id: string;\n name: string;\n slug: string;\n platform: string;\n };\n type: string;\n metadata: {\n value?: string;\n type?: string;\n filename?: string;\n function?: string;\n title?: string;\n };\n numComments: number;\n assignedTo: {\n id: string;\n name: string;\n type: string;\n } | null;\n isBookmarked: boolean;\n isSubscribed: boolean;\n subscriptionDetails: {\n reason?: string;\n } | null;\n hasSeen: boolean;\n annotations: string[];\n isUnhandled: boolean;\n count: string;\n userCount: number;\n firstSeen: string;\n lastSeen: string;\n stats?: {\n \"24h\": Array<[number, number]>;\n };\n}\n\nexport interface Event {\n id: string;\n groupID: string;\n eventID: string;\n projectID: string;\n size: number;\n platform: string;\n message: string;\n dateCreated: string;\n dateReceived: string;\n user: {\n id?: string;\n email?: string;\n username?: string;\n ip_address?: string;\n } | null;\n entries: Array<{\n type: string;\n data: unknown;\n }>;\n contexts: Record<string, unknown>;\n tags: Array<{\n key: string;\n value: string;\n }>;\n errors: Array<{\n type: string;\n message: string;\n }>;\n}\n\nfunction getRequiredOrg(): string {\n const org = getOrg();\n if (org) return org;\n\n throw new Error(\n \"Sentry organization not configured. Please set SENTRY_ORG environment variable.\",\n );\n}\n\nasync function sentryFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const authToken = getApiKey() ?? process.env.SENTRY_AUTH_TOKEN;\n if (!authToken) {\n throw new Error(\"Not authenticated with Sentry. Please set SENTRY_AUTH_TOKEN.\");\n }\n\n const response = await fetch(`${SENTRY_API_BASE_URL}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${authToken}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error: { detail?: string } = await response.json().catch(() => ({}));\n throw new Error(\n error.detail ?? `Sentry API error: ${response.status} ${response.statusText}`,\n );\n }\n\n return response.json();\n}\n\nexport function listOrganizations(): Promise<Organization[]> {\n return sentryFetch<Organization[]>(\"/organizations/\");\n}\n\nexport function listProjects(): Promise<Project[]> {\n const org = getRequiredOrg();\n return sentryFetch<Project[]>(`/organizations/${org}/projects/`);\n}\n\nexport function getProject(projectSlug: string): Promise<Project> {\n const org = getRequiredOrg();\n return sentryFetch<Project>(`/projects/${org}/${projectSlug}/`);\n}\n\nexport function listIssues(\n projectSlug: string,\n options: {\n query?: string;\n status?: \"resolved\" | \"unresolved\" | \"ignored\";\n sort?: \"date\" | \"new\" | \"freq\" | \"priority\" | \"user\";\n limit?: number;\n } = {},\n): Promise<Issue[]> {\n const org = getRequiredOrg();\n const params = new URLSearchParams({ project: projectSlug });\n\n if (options.query) params.append(\"query\", options.query);\n if (options.status) params.append(\"query\", `is:${options.status}`);\n if (options.sort) params.append(\"sort\", options.sort);\n if (options.limit) params.append(\"limit\", options.limit.toString());\n\n return sentryFetch<Issue[]>(`/organizations/${org}/issues/?${params.toString()}`);\n}\n\nexport function getIssue(issueId: string): Promise<Issue> {\n return sentryFetch<Issue>(`/issues/${issueId}/`);\n}\n\nexport function resolveIssue(issueId: string): Promise<Issue> {\n return sentryFetch<Issue>(`/issues/${issueId}/`, {\n method: \"PUT\",\n body: JSON.stringify({ status: \"resolved\" }),\n });\n}\n\nexport function listEvents(issueId: string, limit: number = 10): Promise<Event[]> {\n const params = new URLSearchParams({ limit: limit.toString() });\n return sentryFetch<Event[]>(`/issues/${issueId}/events/?${params.toString()}`);\n}\n",
482
- "tools/get-issue.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { getIssue, listEvents } from \"../lib/sentry-client.ts\";\n\nexport default tool({\n id: \"sentry-get-issue\",\n description:\n \"Get detailed information about a specific Sentry issue including error details, stack traces, and recent events. Use this to investigate and debug specific errors.\",\n inputSchema: defineSchema((v) => v.object({\n issueId: v.string().describe(\"The ID of the issue to retrieve\"),\n includeEvents: v\n .boolean()\n .default(true)\n .describe(\"Whether to include recent events for this issue\"),\n eventLimit: v\n .number()\n .min(1)\n .max(50)\n .default(5)\n .describe(\"Number of recent events to include (1-50)\"),\n }))(),\n async execute({ issueId, includeEvents, eventLimit }) {\n const issue = await getIssue(issueId);\n\n const events = includeEvents ? await listEvents(issueId, eventLimit) : [];\n\n return {\n issue: {\n id: issue.id,\n shortId: issue.shortId,\n title: issue.title,\n culprit: issue.culprit,\n permalink: issue.permalink,\n logger: issue.logger,\n level: issue.level,\n status: issue.status,\n substatus: issue.substatus,\n platform: issue.platform,\n project: {\n id: issue.project.id,\n name: issue.project.name,\n slug: issue.project.slug,\n platform: issue.project.platform,\n },\n type: issue.type,\n metadata: issue.metadata,\n count: issue.count,\n userCount: issue.userCount,\n firstSeen: issue.firstSeen,\n lastSeen: issue.lastSeen,\n numComments: issue.numComments,\n isBookmarked: issue.isBookmarked,\n isSubscribed: issue.isSubscribed,\n assignedTo: issue.assignedTo,\n stats: issue.stats,\n },\n events: events.map((event) => ({\n id: event.id,\n eventID: event.eventID,\n message: event.message,\n platform: event.platform,\n dateCreated: event.dateCreated,\n user: event.user,\n tags: event.tags,\n contexts: event.contexts,\n entries: event.entries,\n })),\n };\n },\n});\n",
483
- "tools/list-issues.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { listIssues } from \"../lib/sentry-client.ts\";\n\nexport default tool({\n id: \"sentry-list-issues\",\n description:\n \"List issues/errors in a Sentry project with optional filters. Returns issue details including title, status, error count, and last seen date.\",\n inputSchema: defineSchema((v) => v.object({\n projectSlug: v.string().describe(\"The slug of the project to list issues from\"),\n query: v.string().optional().describe(\"Search query to filter issues (e.g., 'is:unresolved')\"),\n status: v.enum([\"resolved\", \"unresolved\", \"ignored\"]).optional().describe(\"Filter by issue status\"),\n sort: v\n .enum([\"date\", \"new\", \"freq\", \"priority\", \"user\"])\n .optional()\n .describe(\n \"Sort order: date (most recent), new (newest), freq (most frequent), priority, user (most users affected)\",\n ),\n limit: v\n .number()\n .min(1)\n .max(100)\n .default(25)\n .describe(\"Maximum number of issues to return (1-100)\"),\n }))(),\n async execute({ projectSlug, query, status, sort, limit }) {\n const issues = await listIssues(projectSlug, { query, status, sort, limit });\n\n return issues.map((issue) => ({\n ...issue,\n status: issue.status,\n project: {\n id: issue.project.id,\n name: issue.project.name,\n slug: issue.project.slug,\n },\n }));\n },\n});\n",
484
- "tools/list-projects.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { listProjects } from \"../lib/sentry-client.ts\";\n\nexport default tool({\n id: \"sentry-list-projects\",\n description:\n \"List all projects in your Sentry organization. Returns project details including name, platform, status, and team information.\",\n inputSchema: defineSchema((v) => v.object({}))(),\n async execute() {\n const projects = await listProjects();\n\n return projects.map((project) => ({\n id: project.id,\n slug: project.slug,\n name: project.name,\n platform: project.platform,\n status: project.status,\n dateCreated: project.dateCreated,\n firstEvent: project.firstEvent,\n isBookmarked: project.isBookmarked,\n isMember: project.isMember,\n hasAccess: project.hasAccess,\n teams: project.teams.map((team) => ({\n id: team.id,\n name: team.name,\n slug: team.slug,\n })),\n features: project.features,\n }));\n },\n});\n",
485
- "tools/resolve-issue.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { resolveIssue } from \"../lib/sentry-client.ts\";\n\nexport default tool({\n id: \"sentry-resolve-issue\",\n description:\n \"Mark a Sentry issue as resolved. Use this after you've fixed a bug or determined an issue is no longer relevant.\",\n inputSchema: defineSchema((v) => v.object({\n issueId: v.string().describe(\"The ID of the issue to resolve\"),\n }))(),\n async execute({ issueId }) {\n const issue = await resolveIssue(issueId);\n\n return {\n success: true,\n issue,\n message: `Issue ${issue.shortId} has been marked as resolved.`,\n };\n },\n});\n"
486
- }
487
- },
488
- "integration:servicenow": {
489
- "files": {
490
- ".env.example": "# ServiceNow OAuth Configuration\n# Get these from your ServiceNow instance: System OAuth > Application Registry\nSERVICENOW_INSTANCE=your-instance.service-now.com\nSERVICENOW_CLIENT_ID=your_client_id\nSERVICENOW_CLIENT_SECRET=your_client_secret\n",
491
- "app/api/auth/servicenow/callback/route.ts": "import { setServiceNowTokens } from \"../../../../../lib/token-store.ts\";\n\nfunction getEnv(name: string): string | undefined {\n if (typeof Deno !== \"undefined\") {\n // @ts-ignore: Deno global\n return Deno.env.get(name);\n }\n // @ts-ignore: Node process\n return globalThis.process?.env?.[name];\n}\n\nexport async function GET(request: Request): Promise<Response> {\n const url = new URL(request.url);\n const code = url.searchParams.get(\"code\");\n const error = url.searchParams.get(\"error\");\n const errorDescription = url.searchParams.get(\"error_description\");\n\n const configuredUrl = getEnv(\"NEXT_PUBLIC_APP_URL\");\n if (!configuredUrl) {\n return Response.json(\n { error: \"NEXT_PUBLIC_APP_URL environment variable is required\" },\n { status: 500 },\n );\n }\n const baseUrl = configuredUrl;\n\n if (error) {\n console.error(\"ServiceNow OAuth error:\", error, errorDescription);\n const description = encodeURIComponent(errorDescription ?? error);\n return Response.redirect(\n `${baseUrl}/?error=servicenow_oauth_failed&description=${description}`,\n 302,\n );\n }\n\n if (!code) return Response.redirect(`${baseUrl}/?error=no_code`, 302);\n\n const instance = getEnv(\"SERVICENOW_INSTANCE\");\n const clientId = getEnv(\"SERVICENOW_CLIENT_ID\");\n const clientSecret = getEnv(\"SERVICENOW_CLIENT_SECRET\");\n\n if (!instance || !clientId || !clientSecret) {\n return Response.redirect(`${baseUrl}/?error=servicenow_not_configured`, 302);\n }\n\n const instanceUrl = instance.includes(\"://\") ? instance : `https://${instance}`;\n const redirectUri = `${baseUrl}/api/auth/servicenow/callback`;\n\n try {\n const tokenResponse = await fetch(`${instanceUrl}/oauth_token.do`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams({\n grant_type: \"authorization_code\",\n code,\n redirect_uri: redirectUri,\n client_id: clientId,\n client_secret: clientSecret,\n }),\n });\n\n if (!tokenResponse.ok) {\n console.error(\"ServiceNow token exchange failed:\", await tokenResponse.text());\n return Response.redirect(`${baseUrl}/?error=token_exchange_failed`, 302);\n }\n\n const tokens = await tokenResponse.json();\n\n await setServiceNowTokens({\n accessToken: tokens.access_token,\n refreshToken: tokens.refresh_token,\n expiresAt: tokens.expires_in ? Date.now() + tokens.expires_in * 1000 : undefined,\n instanceUrl,\n });\n\n return Response.redirect(`${baseUrl}/?connected=servicenow`, 302);\n } catch (error) {\n console.error(\"ServiceNow OAuth error:\", error);\n return Response.redirect(`${baseUrl}/?error=servicenow_oauth_failed`, 302);\n }\n}\n",
492
- "app/api/auth/servicenow/route.ts": "function getEnv(name: string): string | undefined {\n if (typeof Deno !== \"undefined\") {\n // @ts-ignore: Deno global\n return Deno.env.get(name);\n }\n\n // @ts-ignore: Node process\n return globalThis.process?.env?.[name];\n}\n\nexport function GET(request: Request): Response {\n const instance = getEnv(\"SERVICENOW_INSTANCE\");\n const clientId = getEnv(\"SERVICENOW_CLIENT_ID\");\n\n if (!instance || !clientId) {\n return Response.json(\n { error: \"ServiceNow OAuth not configured\" },\n { status: 500 },\n );\n }\n\n const instanceUrl = instance.includes(\"://\") ? instance : `https://${instance}`;\n\n const baseUrl = getEnv(\"NEXT_PUBLIC_APP_URL\");\n if (!baseUrl) {\n return Response.json(\n { error: \"NEXT_PUBLIC_APP_URL environment variable is required\" },\n { status: 500 },\n );\n }\n const redirectUri = `${baseUrl}/api/auth/servicenow/callback`;\n\n const authUrl = new URL(`${instanceUrl}/oauth_auth.do`);\n authUrl.searchParams.set(\"response_type\", \"code\");\n authUrl.searchParams.set(\"client_id\", clientId);\n authUrl.searchParams.set(\"redirect_uri\", redirectUri);\n authUrl.searchParams.set(\"state\", crypto.randomUUID());\n\n return Response.redirect(authUrl.toString(), 302);\n}\n",
493
- "lib/servicenow-client.ts": "import { getServiceNowTokens } from \"./token-store.ts\";\n\nfunction getEnv(name: string): string | undefined {\n if (typeof Deno !== \"undefined\") {\n // @ts-ignore: Deno global\n return Deno.env.get(name);\n }\n // @ts-ignore: Node process\n return globalThis.process?.env?.[name];\n}\n\nexport interface ServiceNowIncident {\n sys_id: string;\n number: string;\n short_description: string;\n description: string;\n state: string;\n priority: string;\n urgency: string;\n impact: string;\n category: string;\n subcategory: string;\n assigned_to: { display_value: string; link: string } | string;\n caller_id: { display_value: string; link: string } | string;\n opened_at: string;\n resolved_at: string | null;\n closed_at: string | null;\n sys_created_on: string;\n sys_updated_on: string;\n}\n\nexport interface ServiceNowKnowledgeArticle {\n sys_id: string;\n number: string;\n short_description: string;\n text: string;\n kb_category: string;\n published: string;\n sys_created_on: string;\n}\n\nexport interface ServiceNowResponse<T> {\n result: T;\n}\n\nexport interface ServiceNowListResponse<T> {\n result: T[];\n}\n\nexport class ServiceNowClient {\n private instance: string;\n private accessToken: string | null = null;\n\n constructor() {\n const instance = getEnv(\"SERVICENOW_INSTANCE\");\n if (!instance) throw new Error(\"SERVICENOW_INSTANCE not configured\");\n\n this.instance = instance.replace(/^https?:\\/\\//, \"\").replace(/\\/$/, \"\");\n }\n\n private get baseUrl(): string {\n return `https://${this.instance}/api/now`;\n }\n\n async ensureAuthenticated(): Promise<void> {\n const tokens = await getServiceNowTokens();\n if (!tokens) {\n throw new Error(\n \"ServiceNow not connected. Please connect via /api/auth/servicenow\",\n );\n }\n this.accessToken = tokens.accessToken;\n }\n\n private async request<T>(\n endpoint: string,\n options: RequestInit = {},\n ): Promise<T> {\n await this.ensureAuthenticated();\n\n const response = await fetch(`${this.baseUrl}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${this.accessToken}`,\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`ServiceNow API error: ${response.status} ${errorText}`);\n }\n\n return response.json();\n }\n\n async listIncidents(\n options: {\n limit?: number;\n offset?: number;\n state?: string;\n priority?: string;\n assignedTo?: string;\n query?: string;\n } = {},\n ): Promise<ServiceNowIncident[]> {\n const params = new URLSearchParams({\n sysparm_limit: String(options.limit ?? 20),\n sysparm_offset: String(options.offset ?? 0),\n sysparm_display_value: \"all\",\n });\n\n const queryParts: string[] = [];\n if (options.state) queryParts.push(`state=${options.state}`);\n if (options.priority) queryParts.push(`priority=${options.priority}`);\n if (options.assignedTo) {\n queryParts.push(`assigned_to.name=${options.assignedTo}`);\n }\n if (options.query) queryParts.push(`short_descriptionLIKE${options.query}`);\n\n if (queryParts.length) params.set(\"sysparm_query\", queryParts.join(\"^\"));\n\n const response = await this.request<ServiceNowListResponse<ServiceNowIncident>>(\n `/table/incident?${params}`,\n );\n return response.result;\n }\n\n async getIncident(idOrNumber: string): Promise<ServiceNowIncident> {\n const params = new URLSearchParams({ sysparm_display_value: \"all\" });\n\n if (!idOrNumber.toUpperCase().startsWith(\"INC\")) {\n const response = await this.request<ServiceNowResponse<ServiceNowIncident>>(\n `/table/incident/${idOrNumber}?${params}`,\n );\n return response.result;\n }\n\n params.set(\"sysparm_query\", `number=${idOrNumber}`);\n const response = await this.request<ServiceNowListResponse<ServiceNowIncident>>(\n `/table/incident?${params}`,\n );\n\n const incident = response.result[0];\n if (!incident) throw new Error(`Incident ${idOrNumber} not found`);\n return incident;\n }\n\n async createIncident(data: {\n short_description: string;\n description?: string;\n urgency?: string;\n impact?: string;\n category?: string;\n subcategory?: string;\n caller_id?: string;\n }): Promise<ServiceNowIncident> {\n const response = await this.request<ServiceNowResponse<ServiceNowIncident>>(\n \"/table/incident\",\n {\n method: \"POST\",\n body: JSON.stringify(data),\n },\n );\n return response.result;\n }\n\n async updateIncident(\n sysId: string,\n data: Partial<{\n short_description: string;\n description: string;\n state: string;\n urgency: string;\n impact: string;\n assigned_to: string;\n work_notes: string;\n close_notes: string;\n }>,\n ): Promise<ServiceNowIncident> {\n const response = await this.request<ServiceNowResponse<ServiceNowIncident>>(\n `/table/incident/${sysId}`,\n {\n method: \"PATCH\",\n body: JSON.stringify(data),\n },\n );\n return response.result;\n }\n\n async searchKnowledge(\n query: string,\n limit = 10,\n ): Promise<ServiceNowKnowledgeArticle[]> {\n const params = new URLSearchParams({\n sysparm_limit: String(limit),\n sysparm_query: `short_descriptionLIKE${query}^ORtextLIKE${query}^workflow_state=published`,\n });\n\n const response = await this.request<\n ServiceNowListResponse<ServiceNowKnowledgeArticle>\n >(`/table/kb_knowledge?${params}`);\n return response.result;\n }\n}\n\nlet client: ServiceNowClient | null = null;\n\nexport function getServiceNowClient(): ServiceNowClient {\n client ??= new ServiceNowClient();\n return client;\n}\n",
494
- "tools/create-incident.ts": "import { defineSchema } from \"veryfront/schemas\";\nimport { getServiceNowClient } from \"../lib/servicenow-client.ts\";\nimport { isServiceNowConnected } from \"../lib/token-store.ts\";\n\nexport default defineTool({\n id: \"servicenow-create-incident\",\n description: \"Create a new incident in ServiceNow\",\n inputSchema: defineSchema((v) => v.object({\n short_description: v.string().describe(\"Brief description of the incident\"),\n description: v.string().optional().describe(\"Detailed description of the incident\"),\n urgency: v\n .enum([\"1\", \"2\", \"3\"])\n .optional()\n .describe(\"Urgency level (1=High, 2=Medium, 3=Low)\"),\n impact: v\n .enum([\"1\", \"2\", \"3\"])\n .optional()\n .describe(\"Impact level (1=High, 2=Medium, 3=Low)\"),\n category: v.string().optional().describe(\"Incident category\"),\n subcategory: v.string().optional().describe(\"Incident subcategory\"),\n }))(),\n async execute(input) {\n const connected = await isServiceNowConnected();\n if (!connected) {\n return {\n error: \"ServiceNow not connected\",\n action: \"Please connect ServiceNow via /api/auth/servicenow\",\n };\n }\n\n try {\n const client = getServiceNowClient();\n const incident = await client.createIncident(input);\n\n return {\n success: true,\n number: incident.number,\n sys_id: incident.sys_id,\n short_description: incident.short_description,\n state: incident.state,\n priority: incident.priority,\n message: `Incident ${incident.number} created successfully`,\n };\n } catch (error) {\n return {\n error: error instanceof Error ? error.message : \"Failed to create incident\",\n };\n }\n },\n});\n",
495
- "tools/get-incident.ts": "/**\n * Get ServiceNow Incident Tool\n */\n\nimport { defineSchema } from \"veryfront/schemas\";\nimport { getServiceNowClient } from \"../lib/servicenow-client.ts\";\nimport { isServiceNowConnected } from \"../lib/token-store.ts\";\n\nfunction getDisplayValue(value: unknown): unknown {\n if (!value || typeof value !== \"object\") return value;\n if (!(\"display_value\" in value)) return value;\n return (value as { display_value: unknown }).display_value;\n}\n\nexport default defineTool({\n id: \"servicenow-get-incident\",\n description:\n \"Get details of a specific ServiceNow incident by number (e.g., INC0010001) or sys_id\",\n inputSchema: defineSchema((v) => v.object({\n id: v.string().describe(\"Incident number (INC0010001) or sys_id\"),\n }))(),\n async execute(input) {\n if (!(await isServiceNowConnected())) {\n return {\n error: \"ServiceNow not connected\",\n action: \"Please connect ServiceNow via /api/auth/servicenow\",\n };\n }\n\n try {\n const client = getServiceNowClient();\n const incident = await client.getIncident(input.id);\n\n return {\n number: incident.number,\n sys_id: incident.sys_id,\n short_description: incident.short_description,\n description: incident.description,\n state: incident.state,\n priority: incident.priority,\n urgency: incident.urgency,\n impact: incident.impact,\n category: incident.category,\n subcategory: incident.subcategory,\n assigned_to: getDisplayValue(incident.assigned_to),\n caller_id: getDisplayValue(incident.caller_id),\n opened_at: incident.opened_at,\n resolved_at: incident.resolved_at,\n closed_at: incident.closed_at,\n created: incident.sys_created_on,\n updated: incident.sys_updated_on,\n };\n } catch (error) {\n return {\n error: error instanceof Error ? error.message : \"Failed to get incident\",\n };\n }\n },\n});\n",
496
- "tools/list-incidents.ts": "import { defineSchema } from \"veryfront/schemas\";\nimport { getServiceNowClient } from \"../lib/servicenow-client.ts\";\nimport { isServiceNowConnected } from \"../lib/token-store.ts\";\n\nconst stateMap: Record<string, string> = {\n new: \"1\",\n in_progress: \"2\",\n on_hold: \"3\",\n resolved: \"6\",\n closed: \"7\",\n};\n\nexport default defineTool({\n id: \"servicenow-list-incidents\",\n description:\n \"List incidents from ServiceNow with optional filters for state, priority, or search query\",\n inputSchema: defineSchema((v) => v.object({\n limit: v.number().optional().describe(\"Maximum number of incidents to return (default: 20)\"),\n state: v\n .enum([\"new\", \"in_progress\", \"on_hold\", \"resolved\", \"closed\"])\n .optional()\n .describe(\"Filter by incident state\"),\n priority: v\n .enum([\"1\", \"2\", \"3\", \"4\", \"5\"])\n .optional()\n .describe(\"Filter by priority (1=Critical, 2=High, 3=Moderate, 4=Low, 5=Planning)\"),\n query: v.string().optional().describe(\"Search query for incident short description\"),\n }))(),\n async execute(input) {\n if (!(await isServiceNowConnected())) {\n return {\n error: \"ServiceNow not connected\",\n action: \"Please connect ServiceNow via /api/auth/servicenow\",\n };\n }\n\n try {\n const client = getServiceNowClient();\n const incidents = await client.listIncidents({\n limit: input.limit,\n state: input.state ? stateMap[input.state] : undefined,\n priority: input.priority,\n query: input.query,\n });\n\n return {\n count: incidents.length,\n incidents: incidents.map((inc) => ({\n number: inc.number,\n short_description: inc.short_description,\n state: inc.state,\n priority: inc.priority,\n urgency: inc.urgency,\n impact: inc.impact,\n assigned_to:\n typeof inc.assigned_to === \"object\" ? inc.assigned_to.display_value : inc.assigned_to,\n opened_at: inc.opened_at,\n sys_id: inc.sys_id,\n })),\n };\n } catch (error) {\n return {\n error: error instanceof Error ? error.message : \"Failed to list incidents\",\n };\n }\n },\n});\n",
497
- "tools/search-knowledge.ts": "import { defineSchema } from \"veryfront/schemas\";\nimport { getServiceNowClient } from \"../lib/servicenow-client.ts\";\nimport { isServiceNowConnected } from \"../lib/token-store.ts\";\n\nexport default defineTool({\n id: \"servicenow-search-knowledge\",\n description: \"Search the ServiceNow knowledge base for articles matching a query\",\n inputSchema: defineSchema((v) => v.object({\n query: v.string().describe(\"Search query for knowledge articles\"),\n limit: v.number().optional().describe(\"Maximum number of articles to return (default: 10)\"),\n }))(),\n async execute(input) {\n if (!(await isServiceNowConnected())) {\n return {\n error: \"ServiceNow not connected\",\n action: \"Please connect ServiceNow via /api/auth/servicenow\",\n };\n }\n\n try {\n const client = getServiceNowClient();\n const articles = await client.searchKnowledge(input.query, input.limit ?? 10);\n\n return {\n count: articles.length,\n articles: articles.map((article) => {\n const text = article.text ?? \"\";\n const summary = `${text.substring(0, 500)}${text.length > 500 ? \"...\" : \"\"}`;\n\n return {\n number: article.number,\n title: article.short_description,\n category: article.kb_category,\n published: article.published,\n sys_id: article.sys_id,\n summary,\n };\n }),\n };\n } catch (error) {\n return {\n error: error instanceof Error ? error.message : \"Failed to search knowledge base\",\n };\n }\n },\n});\n",
498
- "tools/update-incident.ts": "import { defineSchema } from \"veryfront/schemas\";\nimport { getServiceNowClient } from \"../lib/servicenow-client.ts\";\nimport { isServiceNowConnected } from \"../lib/token-store.ts\";\n\nexport default defineTool({\n id: \"servicenow-update-incident\",\n description: \"Update an existing incident in ServiceNow\",\n inputSchema: defineSchema((v) => v.object({\n sys_id: v.string().describe(\"The sys_id of the incident to update\"),\n state: v\n .enum([\"1\", \"2\", \"3\", \"6\", \"7\"])\n .optional()\n .describe(\"New state (1=New, 2=In Progress, 3=On Hold, 6=Resolved, 7=Closed)\"),\n short_description: v.string().optional().describe(\"Updated short description\"),\n description: v.string().optional().describe(\"Updated description\"),\n urgency: v\n .enum([\"1\", \"2\", \"3\"])\n .optional()\n .describe(\"Updated urgency (1=High, 2=Medium, 3=Low)\"),\n impact: v\n .enum([\"1\", \"2\", \"3\"])\n .optional()\n .describe(\"Updated impact (1=High, 2=Medium, 3=Low)\"),\n work_notes: v.string().optional().describe(\"Add work notes to the incident\"),\n close_notes: v.string().optional().describe(\"Close notes (required when closing)\"),\n }))(),\n async execute(input) {\n const connected = await isServiceNowConnected();\n if (!connected) {\n return {\n error: \"ServiceNow not connected\",\n action: \"Please connect ServiceNow via /api/auth/servicenow\",\n };\n }\n\n try {\n const client = getServiceNowClient();\n const { sys_id, ...updateData } = input;\n\n const cleanData = Object.fromEntries(\n Object.entries(updateData).filter(([, value]) => value !== undefined),\n );\n\n const incident = await client.updateIncident(sys_id, cleanData);\n\n return {\n success: true,\n number: incident.number,\n sys_id: incident.sys_id,\n state: incident.state,\n updated: incident.sys_updated_on,\n message: `Incident ${incident.number} updated successfully`,\n };\n } catch (error) {\n return {\n error: error instanceof Error ? error.message : \"Failed to update incident\",\n };\n }\n },\n});\n"
499
- }
500
- },
501
- "integration:sharepoint": {
502
- "files": {
503
- "app/api/auth/sharepoint/callback/route.ts": "import { createOAuthCallbackHandler, sharePointConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\n\nexport const GET = createOAuthCallbackHandler(sharePointConfig, { tokenStore });\n",
504
- "app/api/auth/sharepoint/route.ts": "import { createOAuthInitHandler, sharePointConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../lib/token-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../lib/user-id.ts\";\n\nexport const GET = createOAuthInitHandler(sharePointConfig, {\n tokenStore,\n getUserId: requireUserIdFromRequest,\n});\n",
505
- "lib/sharepoint-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst GRAPH_BASE_URL = \"https://graph.microsoft.com/v1.0\";\n\nexport interface SharePointSite {\n id: string;\n name: string;\n displayName: string;\n description?: string;\n webUrl: string;\n createdDateTime: string;\n lastModifiedDateTime: string;\n siteCollection?: {\n hostname: string;\n };\n}\n\nexport interface SharePointDrive {\n id: string;\n name: string;\n description?: string;\n driveType: string;\n createdDateTime: string;\n lastModifiedDateTime: string;\n webUrl: string;\n quota?: {\n total: number;\n used: number;\n remaining: number;\n };\n}\n\nexport interface SharePointFile {\n id: string;\n name: string;\n size: number;\n createdDateTime: string;\n lastModifiedDateTime: string;\n webUrl: string;\n file?: {\n mimeType: string;\n hashes?: {\n sha1Hash?: string;\n quickXorHash?: string;\n };\n };\n folder?: {\n childCount: number;\n };\n parentReference?: {\n driveId: string;\n id: string;\n path: string;\n };\n createdBy?: {\n user?: {\n displayName: string;\n email?: string;\n };\n };\n lastModifiedBy?: {\n user?: {\n displayName: string;\n email?: string;\n };\n };\n}\n\ninterface GraphResponse<T> {\n value: T[];\n \"@odata.nextLink\"?: string;\n}\n\nasync function requireAccessToken(): Promise<string> {\n const token = await getAccessToken();\n if (!token) throw new Error(\"Not authenticated with Microsoft. Please connect your account.\");\n return token;\n}\n\nasync function graphFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await requireAccessToken();\n\n const response = await fetch(`${GRAPH_BASE_URL}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n ...(options.headers ?? {}),\n },\n });\n\n if (response.ok) return response.json();\n\n const error = await response.json().catch(() => ({}));\n throw new Error(\n `Microsoft Graph API error: ${response.status} ${error.error?.message ?? response.statusText}`,\n );\n}\n\nexport async function listSites(options?: {\n search?: string;\n limit?: number;\n}): Promise<SharePointSite[]> {\n const endpoint = options?.search\n ? `/sites?search=${encodeURIComponent(options.search)}`\n : \"/sites?search=*\";\n\n const { value = [] } = await graphFetch<GraphResponse<SharePointSite>>(endpoint);\n return options?.limit ? value.slice(0, options.limit) : value;\n}\n\nexport function getSite(siteId: string): Promise<SharePointSite> {\n return graphFetch<SharePointSite>(`/sites/${siteId}`);\n}\n\nexport function getSiteByPath(hostname: string, sitePath: string): Promise<SharePointSite> {\n return graphFetch<SharePointSite>(`/sites/${hostname}:${sitePath}`);\n}\n\nexport async function listDrives(siteId: string): Promise<SharePointDrive[]> {\n const { value = [] } = await graphFetch<GraphResponse<SharePointDrive>>(`/sites/${siteId}/drives`);\n return value;\n}\n\nexport function getDefaultDrive(siteId: string): Promise<SharePointDrive> {\n return graphFetch<SharePointDrive>(`/sites/${siteId}/drive`);\n}\n\nexport async function listFiles(\n siteId: string,\n driveId: string,\n folderId?: string,\n options?: {\n limit?: number;\n orderBy?: string;\n },\n): Promise<SharePointFile[]> {\n const baseEndpoint = folderId\n ? `/sites/${siteId}/drives/${driveId}/items/${folderId}/children`\n : `/sites/${siteId}/drives/${driveId}/root/children`;\n\n const params = new URLSearchParams();\n if (options?.orderBy) params.set(\"$orderby\", options.orderBy);\n if (options?.limit) params.set(\"$top\", String(options.limit));\n\n const endpoint = params.size ? `${baseEndpoint}?${params.toString()}` : baseEndpoint;\n\n const { value = [] } = await graphFetch<GraphResponse<SharePointFile>>(endpoint);\n return value;\n}\n\nexport function getFile(siteId: string, driveId: string, itemId: string): Promise<SharePointFile> {\n return graphFetch<SharePointFile>(`/sites/${siteId}/drives/${driveId}/items/${itemId}`);\n}\n\nexport function getFileByPath(\n siteId: string,\n driveId: string,\n path: string,\n): Promise<SharePointFile> {\n const encodedPath = encodeURIComponent(path);\n return graphFetch<SharePointFile>(`/sites/${siteId}/drives/${driveId}/root:/${encodedPath}`);\n}\n\nexport async function downloadFile(\n siteId: string,\n driveId: string,\n itemId: string,\n): Promise<ArrayBuffer> {\n const token = await requireAccessToken();\n\n await getFile(siteId, driveId, itemId);\n\n const response = await fetch(\n `${GRAPH_BASE_URL}/sites/${siteId}/drives/${driveId}/items/${itemId}/content`,\n { headers: { Authorization: `Bearer ${token}` } },\n );\n\n if (!response.ok) throw new Error(`Failed to download file: ${response.statusText}`);\n\n return response.arrayBuffer();\n}\n\nexport async function downloadFileAsText(\n siteId: string,\n driveId: string,\n itemId: string,\n): Promise<string> {\n const buffer = await downloadFile(siteId, driveId, itemId);\n return new TextDecoder().decode(buffer);\n}\n\nexport async function uploadFile(\n siteId: string,\n driveId: string,\n fileName: string,\n content: string | ArrayBuffer | Blob,\n folderId?: string,\n): Promise<SharePointFile> {\n const token = await requireAccessToken();\n\n const encodedFileName = encodeURIComponent(fileName);\n const endpoint = folderId\n ? `/sites/${siteId}/drives/${driveId}/items/${folderId}:/${encodedFileName}:/content`\n : `/sites/${siteId}/drives/${driveId}/root:/${encodedFileName}:/content`;\n\n let body: ArrayBuffer;\n if (typeof content === \"string\") {\n body = new TextEncoder().encode(content);\n } else if (content instanceof Blob) {\n body = await content.arrayBuffer();\n } else {\n body = content;\n }\n\n const response = await fetch(`${GRAPH_BASE_URL}${endpoint}`, {\n method: \"PUT\",\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/octet-stream\",\n },\n body,\n });\n\n if (response.ok) return response.json();\n\n const error = await response.json().catch(() => ({}));\n throw new Error(`Failed to upload file: ${error.error?.message ?? response.statusText}`);\n}\n\nexport function createFolder(\n siteId: string,\n driveId: string,\n folderName: string,\n parentFolderId?: string,\n): Promise<SharePointFile> {\n const endpoint = parentFolderId\n ? `/sites/${siteId}/drives/${driveId}/items/${parentFolderId}/children`\n : `/sites/${siteId}/drives/${driveId}/root/children`;\n\n return graphFetch<SharePointFile>(endpoint, {\n method: \"POST\",\n body: JSON.stringify({\n name: folderName,\n folder: {},\n \"@microsoft.graph.conflictBehavior\": \"rename\",\n }),\n });\n}\n\nexport async function searchFiles(\n siteId: string,\n query: string,\n options?: {\n limit?: number;\n },\n): Promise<SharePointFile[]> {\n const baseEndpoint = `/sites/${siteId}/drive/root/search(q='${encodeURIComponent(query)}')`;\n const endpoint = options?.limit ? `${baseEndpoint}?$top=${options.limit}` : baseEndpoint;\n\n const { value = [] } = await graphFetch<GraphResponse<SharePointFile>>(endpoint);\n return value;\n}\n\nexport async function deleteItem(siteId: string, driveId: string, itemId: string): Promise<void> {\n await graphFetch<void>(`/sites/${siteId}/drives/${driveId}/items/${itemId}`, { method: \"DELETE\" });\n}\n\nexport function moveItem(\n siteId: string,\n driveId: string,\n itemId: string,\n newParentId: string,\n newName?: string,\n): Promise<SharePointFile> {\n const body: { parentReference: { id: string }; name?: string } = {\n parentReference: { id: newParentId },\n ...(newName ? { name: newName } : {}),\n };\n\n return graphFetch<SharePointFile>(`/sites/${siteId}/drives/${driveId}/items/${itemId}`, {\n method: \"PATCH\",\n body: JSON.stringify(body),\n });\n}\n\nexport async function copyItem(\n siteId: string,\n driveId: string,\n itemId: string,\n newParentId: string,\n newName?: string,\n): Promise<void> {\n const body: { parentReference: { driveId: string; id: string }; name?: string } = {\n parentReference: { driveId, id: newParentId },\n ...(newName ? { name: newName } : {}),\n };\n\n await graphFetch<void>(`/sites/${siteId}/drives/${driveId}/items/${itemId}/copy`, {\n method: \"POST\",\n body: JSON.stringify(body),\n });\n}\n",
506
- "tools/get-file.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { downloadFileAsText, getFile } from \"../lib/sharepoint-client.ts\";\n\nexport default tool({\n id: \"sharepoint-get-file\",\n description:\n \"Get detailed metadata and optionally download content of a file from SharePoint. Can retrieve text content for text-based files.\",\n inputSchema: defineSchema((v) => v.object({\n siteId: v.string().describe(\"The ID of the SharePoint site\"),\n driveId: v.string().describe(\"The ID of the document library (drive)\"),\n itemId: v.string().describe(\"The ID of the file to retrieve\"),\n includeContent: v\n .boolean()\n .default(false)\n .describe(\n \"Whether to download and include the file content (only works for text-based files)\",\n ),\n }))(),\n async execute({ siteId, driveId, itemId, includeContent }) {\n const file = await getFile(siteId, driveId, itemId);\n\n const result: Record<string, unknown> = {\n id: file.id,\n name: file.name,\n size: file.size,\n sizeFormatted: formatBytes(file.size),\n mimeType: file.file?.mimeType,\n url: file.webUrl,\n created: file.createdDateTime,\n lastModified: file.lastModifiedDateTime,\n createdBy: {\n name: file.createdBy?.user?.displayName,\n email: file.createdBy?.user?.email,\n },\n lastModifiedBy: {\n name: file.lastModifiedBy?.user?.displayName,\n email: file.lastModifiedBy?.user?.email,\n },\n parentReference: {\n driveId: file.parentReference?.driveId,\n id: file.parentReference?.id,\n path: file.parentReference?.path,\n },\n hashes: file.file?.hashes,\n };\n\n if (!includeContent) return result;\n\n const mimeType = file.file?.mimeType;\n if (!mimeType) return result;\n\n const isTextFile = [\n \"text/\",\n \"application/json\",\n \"application/xml\",\n \"application/javascript\",\n \"application/typescript\",\n ].some((type) => mimeType.startsWith(type));\n\n if (!isTextFile) {\n result.contentError = \"File is not a text-based file type\";\n return result;\n }\n\n try {\n const content = await downloadFileAsText(siteId, driveId, itemId);\n result.content = content;\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Unknown error\";\n result.contentError = `Failed to download content: ${message}`;\n }\n\n return result;\n },\n});\n\nfunction formatBytes(bytes: number): string {\n if (bytes === 0) return \"0 Bytes\";\n\n const k = 1024;\n const sizes = [\"Bytes\", \"KB\", \"MB\", \"GB\", \"TB\"];\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n\n return `${Math.round((bytes / Math.pow(k, i)) * 100) / 100} ${sizes[i]}`;\n}\n",
507
- "tools/get-site.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { getSite, listDrives } from \"../lib/sharepoint-client.ts\";\n\nexport default tool({\n id: \"sharepoint-get-site\",\n description:\n \"Get detailed information about a specific SharePoint site including its document libraries (drives).\",\n inputSchema: defineSchema((v) => v.object({\n siteId: v.string().describe(\"The ID of the SharePoint site to retrieve\"),\n includeDrives: v\n .boolean()\n .default(true)\n .describe(\"Whether to include the list of document libraries in the response\"),\n }))(),\n async execute({ siteId, includeDrives }): Promise<Record<string, unknown>> {\n const site = await getSite(siteId);\n\n const result: Record<string, unknown> = {\n id: site.id,\n name: site.displayName ?? site.name,\n description: site.description,\n url: site.webUrl,\n hostname: site.siteCollection?.hostname,\n created: site.createdDateTime,\n lastModified: site.lastModifiedDateTime,\n };\n\n if (!includeDrives) return result;\n\n const drives = await listDrives(siteId);\n result.documentLibraries = drives.map((drive) => {\n const quota = drive.quota;\n const percentUsed =\n quota && quota.total > 0 ? Math.round((quota.used / quota.total) * 100) : 0;\n\n return {\n id: drive.id,\n name: drive.name,\n description: drive.description,\n type: drive.driveType,\n url: drive.webUrl,\n quota: quota\n ? {\n total: quota.total,\n used: quota.used,\n remaining: quota.remaining,\n percentUsed,\n }\n : undefined,\n };\n });\n\n return result;\n },\n});\n",
508
- "tools/list-files.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { listFiles, searchFiles } from \"../lib/sharepoint-client.ts\";\n\nexport default tool({\n id: \"sharepoint-list-files\",\n description:\n \"List files and folders in a SharePoint document library. Can list root level or a specific folder, or search across the entire library.\",\n inputSchema: defineSchema((v) => v.object({\n siteId: v.string().describe(\"The ID of the SharePoint site\"),\n driveId: v.string().describe(\"The ID of the document library (drive)\"),\n folderId: v\n .string()\n .optional()\n .describe(\n \"Optional folder ID to list contents from. If not provided, lists root level.\",\n ),\n search: v\n .string()\n .optional()\n .describe(\n \"Optional search query to find files by name or content instead of listing\",\n ),\n orderBy: v\n .enum([\"name\", \"lastModifiedDateTime\", \"size\"])\n .optional()\n .describe(\"Sort order for results\"),\n limit: v\n .number()\n .min(1)\n .max(100)\n .default(50)\n .describe(\"Maximum number of items to return\"),\n }))(),\n async execute({ siteId, driveId, folderId, search, orderBy, limit }) {\n const files = search\n ? await searchFiles(siteId, search, { limit })\n : await listFiles(siteId, driveId, folderId, { limit, orderBy });\n\n return files.map((file) => ({\n id: file.id,\n name: file.name,\n type: file.folder ? \"folder\" : \"file\",\n size: file.size,\n sizeFormatted: formatBytes(file.size),\n mimeType: file.file?.mimeType,\n url: file.webUrl,\n created: file.createdDateTime,\n lastModified: file.lastModifiedDateTime,\n createdBy: file.createdBy?.user?.displayName,\n lastModifiedBy: file.lastModifiedBy?.user?.displayName,\n parentPath: file.parentReference?.path,\n childCount: file.folder?.childCount,\n }));\n },\n});\n\nfunction formatBytes(bytes: number): string {\n if (bytes === 0) return \"0 Bytes\";\n\n const k = 1024;\n const sizes = [\"Bytes\", \"KB\", \"MB\", \"GB\", \"TB\"];\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n\n return `${Math.round((bytes / Math.pow(k, i)) * 100) / 100} ${sizes[i]}`;\n}\n",
509
- "tools/list-sites.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { listSites } from \"../lib/sharepoint-client.ts\";\n\nexport default tool({\n id: \"sharepoint-list-sites\",\n description:\n \"List all SharePoint sites the user has access to. Returns site names, URLs, and IDs.\",\n inputSchema: defineSchema((v) => v.object({\n search: v\n .string()\n .optional()\n .describe(\"Optional search query to filter sites by name or description\"),\n limit: v\n .number()\n .min(1)\n .max(50)\n .default(20)\n .describe(\"Maximum number of sites to return\"),\n }))(),\n async execute({ search, limit }) {\n const sites = await listSites({ search, limit });\n\n return sites.map((site) => ({\n id: site.id,\n name: site.displayName ?? site.name,\n description: site.description,\n url: site.webUrl,\n hostname: site.siteCollection?.hostname,\n created: site.createdDateTime,\n lastModified: site.lastModifiedDateTime,\n }));\n },\n});\n",
510
- "tools/upload-file.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createFolder, uploadFile } from \"../lib/sharepoint-client.ts\";\n\nexport default tool({\n id: \"sharepoint-upload-file\",\n description:\n \"Upload a file to a SharePoint document library. Can upload to root or a specific folder.\",\n inputSchema: defineSchema((v) => v.object({\n siteId: v.string().describe(\"The ID of the SharePoint site\"),\n driveId: v.string().describe(\"The ID of the document library (drive) to upload to\"),\n fileName: v.string().describe(\"The name of the file to create (including extension)\"),\n content: v.string().describe(\"The content of the file to upload\"),\n folderId: v\n .string()\n .optional()\n .describe(\"Optional folder ID to upload into. If not provided, uploads to root.\"),\n createFolderIfNeeded: v\n .boolean()\n .default(false)\n .describe(\"If true and folderPath is provided, creates the folder if it does not exist\"),\n folderPath: v\n .string()\n .optional()\n .describe(\n 'Optional folder path (e.g., \"Documents/Projects\") to create if createFolderIfNeeded is true',\n ),\n }))(),\n async execute({\n siteId,\n driveId,\n fileName,\n content,\n folderId,\n createFolderIfNeeded,\n folderPath,\n }) {\n const targetFolderId = await resolveTargetFolderId({\n siteId,\n driveId,\n folderId,\n createFolderIfNeeded,\n folderPath,\n });\n\n const file = await uploadFile(siteId, driveId, fileName, content, targetFolderId);\n\n return {\n id: file.id,\n name: file.name,\n size: file.size,\n sizeFormatted: formatBytes(file.size),\n mimeType: file.file?.mimeType,\n url: file.webUrl,\n created: file.createdDateTime,\n lastModified: file.lastModifiedDateTime,\n parentPath: file.parentReference?.path,\n message: `Successfully uploaded \"${fileName}\" to SharePoint`,\n };\n },\n});\n\nasync function resolveTargetFolderId({\n siteId,\n driveId,\n folderId,\n createFolderIfNeeded,\n folderPath,\n}: {\n siteId: string;\n driveId: string;\n folderId?: string;\n createFolderIfNeeded: boolean;\n folderPath?: string;\n}): Promise<string | undefined> {\n if (!createFolderIfNeeded || !folderPath || folderId) return folderId;\n\n const folders = folderPath.split(\"/\").filter(Boolean);\n let currentFolderId: string | undefined;\n\n for (const folderName of folders) {\n try {\n const folder = await createFolder(siteId, driveId, folderName, currentFolderId);\n currentFolderId = folder.id;\n } catch (error) {\n console.warn(`Note: Could not create folder \"${folderName}\":`, error);\n }\n }\n\n return currentFolderId;\n}\n\nfunction formatBytes(bytes: number): string {\n if (bytes === 0) return \"0 Bytes\";\n\n const k = 1024;\n const sizes = [\"Bytes\", \"KB\", \"MB\", \"GB\", \"TB\"];\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n\n return `${Math.round((bytes / Math.pow(k, i)) * 100) / 100} ${sizes[i]}`;\n}\n"
511
- }
512
- },
513
- "integration:sheets": {
514
- "files": {
515
- ".env.example": "# Google Sheets Integration\n# Create OAuth credentials at https://console.cloud.google.com/apis/credentials\n# Make sure to enable:\n# - Google Sheets API: https://console.cloud.google.com/apis/library/sheets.googleapis.com\n# - Google Drive API: https://console.cloud.google.com/apis/library/drive.googleapis.com\n\nGOOGLE_CLIENT_ID=your_client_id_here\nGOOGLE_CLIENT_SECRET=your_client_secret_here\n",
516
- "app/api/auth/sheets/callback/route.ts": "import { createOAuthCallbackHandler, sheetsConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\n\nexport const GET = createOAuthCallbackHandler(sheetsConfig, { tokenStore });\n",
517
- "app/api/auth/sheets/route.ts": "import { createOAuthInitHandler, sheetsConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../lib/token-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../lib/user-id.ts\";\n\nexport const GET = createOAuthInitHandler(sheetsConfig, {\n tokenStore,\n getUserId: requireUserIdFromRequest,\n});\n",
518
- "lib/sheets-client.ts": "/**\n * Google Sheets API Client\n *\n * Provides a type-safe interface to Google Sheets API operations.\n */\n\nimport { getValidToken } from \"./oauth.ts\";\n\nfunction getEnv(key: string): string | undefined {\n // @ts-ignore - Deno global\n if (typeof Deno !== \"undefined\") return Deno.env.get(key);\n // @ts-ignore - process global\n if (typeof process !== \"undefined\" && process.env) return process.env[key];\n return undefined;\n}\n\nconst SHEETS_API_BASE = \"https://sheets.googleapis.com/v4\";\nconst DRIVE_API_BASE = \"https://www.googleapis.com/drive/v3\";\n\nexport interface Spreadsheet {\n spreadsheetId: string;\n properties: {\n title: string;\n locale: string;\n autoRecalc: string;\n timeZone: string;\n };\n sheets: Sheet[];\n spreadsheetUrl: string;\n}\n\nexport interface Sheet {\n properties: {\n sheetId: number;\n title: string;\n index: number;\n sheetType: \"GRID\" | \"OBJECT\";\n gridProperties?: {\n rowCount: number;\n columnCount: number;\n };\n };\n}\n\nexport interface SpreadsheetFile {\n id: string;\n name: string;\n mimeType: string;\n createdTime: string;\n modifiedTime: string;\n webViewLink: string;\n}\n\nexport interface CellData {\n values: unknown[][];\n range: string;\n}\n\nexport interface CreateSpreadsheetOptions {\n title: string;\n sheets?: Array<{\n title: string;\n rowCount?: number;\n columnCount?: number;\n }>;\n}\n\nexport interface WriteRangeOptions {\n spreadsheetId: string;\n range: string;\n values: unknown[][];\n valueInputOption?: \"RAW\" | \"USER_ENTERED\";\n}\n\nexport interface AppendRangeOptions extends WriteRangeOptions {\n insertDataOption?: \"OVERWRITE\" | \"INSERT_ROWS\";\n}\n\nexport interface BatchUpdateOptions {\n spreadsheetId: string;\n requests: Array<Record<string, unknown>>;\n includeSpreadsheetInResponse?: boolean;\n responseRanges?: string[];\n}\n\nexport const sheetsOAuthProvider = {\n name: \"sheets\",\n authorizationUrl: \"https://accounts.google.com/o/oauth2/v2/auth\",\n tokenUrl: \"https://oauth2.googleapis.com/token\",\n clientId: getEnv(\"GOOGLE_CLIENT_ID\") ?? \"\",\n clientSecret: getEnv(\"GOOGLE_CLIENT_SECRET\") ?? \"\",\n scopes: [\n \"https://www.googleapis.com/auth/spreadsheets\",\n \"https://www.googleapis.com/auth/drive.readonly\",\n \"https://www.googleapis.com/auth/drive.file\",\n ],\n callbackPath: \"/api/auth/sheets/callback\",\n};\n\nexport function createSheetsClient(userId: string): {\n listSpreadsheets(options?: {\n maxResults?: number;\n orderBy?: \"createdTime\" | \"modifiedTime\" | \"name\";\n }): Promise<SpreadsheetFile[]>;\n getSpreadsheet(spreadsheetId: string): Promise<Spreadsheet>;\n readRange(spreadsheetId: string, range: string): Promise<CellData>;\n readRanges(spreadsheetId: string, ranges: string[]): Promise<CellData[]>;\n writeRange(options: WriteRangeOptions): Promise<{\n updatedRange: string;\n updatedRows: number;\n updatedColumns: number;\n updatedCells: number;\n }>;\n appendRange(options: AppendRangeOptions): Promise<{\n updates: {\n updatedRange: string;\n updatedRows: number;\n updatedColumns: number;\n updatedCells: number;\n };\n }>;\n clearRange(\n spreadsheetId: string,\n range: string,\n ): Promise<{ clearedRange: string }>;\n batchUpdate(options: BatchUpdateOptions): Promise<unknown>;\n createSpreadsheet(options: CreateSpreadsheetOptions): Promise<Spreadsheet>;\n addSheet(\n spreadsheetId: string,\n title: string,\n options?: { rowCount?: number; columnCount?: number },\n ): Promise<Sheet>;\n deleteSheet(spreadsheetId: string, sheetId: number): Promise<void>;\n renameSheet(\n spreadsheetId: string,\n sheetId: number,\n title: string,\n ): Promise<unknown>;\n deleteSpreadsheet(\n spreadsheetId: string,\n options?: { permanentlyDelete?: boolean },\n ): Promise<\n { deleted: true; spreadsheetId: string; permanentlyDeleted: boolean }\n >;\n findReplace(options: {\n spreadsheetId: string;\n find: string;\n replacement: string;\n sheetId?: number;\n matchCase?: boolean;\n matchEntireCell?: boolean;\n searchByRegex?: boolean;\n }): Promise<unknown>;\n copySheet(options: {\n spreadsheetId: string;\n sheetId: number;\n destinationSpreadsheetId: string;\n }): Promise<unknown>;\n createChart(\n spreadsheetId: string,\n chart: Record<string, unknown>,\n ): Promise<unknown>;\n setDataValidation(options: {\n spreadsheetId: string;\n range: Record<string, unknown>;\n rule: Record<string, unknown>;\n }): Promise<unknown>;\n} {\n async function getAccessToken(): Promise<string> {\n const token = await getValidToken(sheetsOAuthProvider, userId, \"sheets\");\n if (token) return token;\n throw new Error(\n \"Google Sheets not connected. Please connect your Google account first.\",\n );\n }\n\n async function apiRequest<T>(\n baseUrl: string,\n serviceName: \"Sheets\" | \"Drive\",\n endpoint: string,\n options: RequestInit = {},\n ): Promise<T> {\n const accessToken = await getAccessToken();\n\n const response = await fetch(`${baseUrl}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${accessToken}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(\n `${serviceName} API error: ${response.status} - ${error}`,\n );\n }\n\n if (response.status === 204) return undefined as T;\n return response.json();\n }\n\n function sheetsApiRequest<T>(\n endpoint: string,\n options: RequestInit = {},\n ): Promise<T> {\n return apiRequest<T>(SHEETS_API_BASE, \"Sheets\", endpoint, options);\n }\n\n function driveApiRequest<T>(\n endpoint: string,\n options: RequestInit = {},\n ): Promise<T> {\n return apiRequest<T>(DRIVE_API_BASE, \"Drive\", endpoint, options);\n }\n\n return {\n async listSpreadsheets(options: {\n maxResults?: number;\n orderBy?: \"createdTime\" | \"modifiedTime\" | \"name\";\n } = {}): Promise<SpreadsheetFile[]> {\n const params = new URLSearchParams({\n q: \"mimeType='application/vnd.google-apps.spreadsheet' and trashed=false\",\n fields: \"files(id,name,mimeType,createdTime,modifiedTime,webViewLink)\",\n pageSize: String(options.maxResults ?? 20),\n orderBy: `${options.orderBy ?? \"modifiedTime\"} desc`,\n });\n\n const result = await driveApiRequest<{ files?: SpreadsheetFile[] }>(\n `/files?${params.toString()}`,\n );\n return result.files ?? [];\n },\n\n getSpreadsheet(spreadsheetId: string): Promise<Spreadsheet> {\n return sheetsApiRequest<Spreadsheet>(`/spreadsheets/${spreadsheetId}`);\n },\n\n async readRange(spreadsheetId: string, range: string): Promise<CellData> {\n const result = await sheetsApiRequest<\n { values?: unknown[][]; range: string }\n >(\n `/spreadsheets/${spreadsheetId}/values/${encodeURIComponent(range)}`,\n );\n\n return { values: result.values ?? [], range: result.range };\n },\n\n async readRanges(\n spreadsheetId: string,\n ranges: string[],\n ): Promise<CellData[]> {\n const params = new URLSearchParams();\n ranges.forEach((range) => params.append(\"ranges\", range));\n\n const result = await sheetsApiRequest<{\n valueRanges: Array<{ values?: unknown[][]; range: string }>;\n }>(`/spreadsheets/${spreadsheetId}/values:batchGet?${params.toString()}`);\n\n return result.valueRanges.map((vr) => ({\n values: vr.values ?? [],\n range: vr.range,\n }));\n },\n\n writeRange(options: WriteRangeOptions): Promise<{\n updatedRange: string;\n updatedRows: number;\n updatedColumns: number;\n updatedCells: number;\n }> {\n const valueInputOption = options.valueInputOption ?? \"USER_ENTERED\";\n\n return sheetsApiRequest(\n `/spreadsheets/${options.spreadsheetId}/values/${\n encodeURIComponent(options.range)\n }?valueInputOption=${valueInputOption}`,\n {\n method: \"PUT\",\n body: JSON.stringify({ values: options.values }),\n },\n );\n },\n\n appendRange(options: AppendRangeOptions): Promise<{\n updates: {\n updatedRange: string;\n updatedRows: number;\n updatedColumns: number;\n updatedCells: number;\n };\n }> {\n const params = new URLSearchParams({\n valueInputOption: options.valueInputOption ?? \"USER_ENTERED\",\n insertDataOption: options.insertDataOption ?? \"INSERT_ROWS\",\n });\n\n return sheetsApiRequest(\n `/spreadsheets/${options.spreadsheetId}/values/${\n encodeURIComponent(options.range)\n }:append?${params.toString()}`,\n {\n method: \"POST\",\n body: JSON.stringify({ values: options.values }),\n },\n );\n },\n\n clearRange(\n spreadsheetId: string,\n range: string,\n ): Promise<{ clearedRange: string }> {\n return sheetsApiRequest(\n `/spreadsheets/${spreadsheetId}/values/${\n encodeURIComponent(range)\n }:clear`,\n {\n method: \"POST\",\n },\n );\n },\n\n batchUpdate(options: BatchUpdateOptions): Promise<unknown> {\n return sheetsApiRequest(\n `/spreadsheets/${options.spreadsheetId}:batchUpdate`,\n {\n method: \"POST\",\n body: JSON.stringify({\n requests: options.requests,\n includeSpreadsheetInResponse: options.includeSpreadsheetInResponse,\n responseRanges: options.responseRanges,\n }),\n },\n );\n },\n\n createSpreadsheet(options: CreateSpreadsheetOptions): Promise<Spreadsheet> {\n const body: {\n properties: { title: string };\n sheets?: Array<{\n properties: {\n title: string;\n gridProperties?: { rowCount: number; columnCount: number };\n };\n }>;\n } = { properties: { title: options.title } };\n\n if (options.sheets?.length) {\n body.sheets = options.sheets.map((sheet) => ({\n properties: {\n title: sheet.title,\n gridProperties: {\n rowCount: sheet.rowCount ?? 1000,\n columnCount: sheet.columnCount ?? 26,\n },\n },\n }));\n }\n\n return sheetsApiRequest(\"/spreadsheets\", {\n method: \"POST\",\n body: JSON.stringify(body),\n });\n },\n\n async addSheet(\n spreadsheetId: string,\n title: string,\n options?: { rowCount?: number; columnCount?: number },\n ): Promise<Sheet> {\n const result = await sheetsApiRequest<{\n replies: Array<{ addSheet?: { properties: Sheet[\"properties\"] } }>;\n }>(`/spreadsheets/${spreadsheetId}:batchUpdate`, {\n method: \"POST\",\n body: JSON.stringify({\n requests: [\n {\n addSheet: {\n properties: {\n title,\n gridProperties: {\n rowCount: options?.rowCount ?? 1000,\n columnCount: options?.columnCount ?? 26,\n },\n },\n },\n },\n ],\n }),\n });\n\n const properties = result.replies[0]?.addSheet?.properties;\n if (!properties) throw new Error(\"Failed to add sheet\");\n\n return { properties };\n },\n\n async deleteSheet(spreadsheetId: string, sheetId: number): Promise<void> {\n await sheetsApiRequest(`/spreadsheets/${spreadsheetId}:batchUpdate`, {\n method: \"POST\",\n body: JSON.stringify({\n requests: [{ deleteSheet: { sheetId } }],\n }),\n });\n },\n\n renameSheet(\n spreadsheetId: string,\n sheetId: number,\n title: string,\n ): Promise<unknown> {\n return sheetsApiRequest(`/spreadsheets/${spreadsheetId}:batchUpdate`, {\n method: \"POST\",\n body: JSON.stringify({\n requests: [{\n updateSheetProperties: {\n properties: { sheetId, title },\n fields: \"title\",\n },\n }],\n }),\n });\n },\n\n async deleteSpreadsheet(\n spreadsheetId: string,\n options: { permanentlyDelete?: boolean } = {},\n ): Promise<\n { deleted: true; spreadsheetId: string; permanentlyDeleted: boolean }\n > {\n if (options.permanentlyDelete) {\n await driveApiRequest(`/files/${spreadsheetId}`, { method: \"DELETE\" });\n } else {\n await driveApiRequest(`/files/${spreadsheetId}`, {\n method: \"PATCH\",\n body: JSON.stringify({ trashed: true }),\n });\n }\n\n return {\n deleted: true,\n spreadsheetId,\n permanentlyDeleted: Boolean(options.permanentlyDelete),\n };\n },\n\n findReplace(options: {\n spreadsheetId: string;\n find: string;\n replacement: string;\n sheetId?: number;\n matchCase?: boolean;\n matchEntireCell?: boolean;\n searchByRegex?: boolean;\n }): Promise<unknown> {\n return sheetsApiRequest(\n `/spreadsheets/${options.spreadsheetId}:batchUpdate`,\n {\n method: \"POST\",\n body: JSON.stringify({\n requests: [{\n findReplace: {\n find: options.find,\n replacement: options.replacement,\n sheetId: options.sheetId,\n matchCase: options.matchCase,\n matchEntireCell: options.matchEntireCell,\n searchByRegex: options.searchByRegex,\n allSheets: options.sheetId === undefined ? true : undefined,\n },\n }],\n }),\n },\n );\n },\n\n copySheet(options: {\n spreadsheetId: string;\n sheetId: number;\n destinationSpreadsheetId: string;\n }): Promise<unknown> {\n return sheetsApiRequest(\n `/spreadsheets/${options.spreadsheetId}/sheets/${options.sheetId}:copyTo`,\n {\n method: \"POST\",\n body: JSON.stringify({\n destinationSpreadsheetId: options.destinationSpreadsheetId,\n }),\n },\n );\n },\n\n createChart(\n spreadsheetId: string,\n chart: Record<string, unknown>,\n ): Promise<unknown> {\n return sheetsApiRequest(`/spreadsheets/${spreadsheetId}:batchUpdate`, {\n method: \"POST\",\n body: JSON.stringify({\n requests: [{ addChart: { chart } }],\n }),\n });\n },\n\n setDataValidation(options: {\n spreadsheetId: string;\n range: Record<string, unknown>;\n rule: Record<string, unknown>;\n }): Promise<unknown> {\n return sheetsApiRequest(\n `/spreadsheets/${options.spreadsheetId}:batchUpdate`,\n {\n method: \"POST\",\n body: JSON.stringify({\n requests: [{\n repeatCell: {\n range: options.range,\n cell: { dataValidation: options.rule },\n fields: \"dataValidation\",\n },\n }],\n }),\n },\n );\n },\n };\n}\n\nexport type SheetsClient = ReturnType<typeof createSheetsClient>;\n",
519
- "tools/add-sheet.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createSheetsClient } from \"../lib/sheets-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"sheets-add-sheet\",\n description: \"Add a new sheet/tab to an existing spreadsheet.\",\n inputSchema: defineSchema((v) =>\n v.object({\n spreadsheetId: v.string().describe(\"The ID of the spreadsheet\"),\n title: v.string().describe(\"Title for the new sheet/tab\"),\n rowCount: v.number().min(1).max(10000).optional(),\n columnCount: v.number().min(1).max(18278).optional(),\n })\n )(),\n execute({ spreadsheetId, title, rowCount, columnCount }, context) {\n const userId = requireUserIdFromContext(context);\n return createSheetsClient(userId).addSheet(spreadsheetId, title, {\n rowCount,\n columnCount,\n });\n },\n});\n",
520
- "tools/append-rows.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createSheetsClient } from \"../lib/sheets-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"sheets-append-rows\",\n description:\n \"Append rows to a Google Sheets range. Use for trackers, logs, and adding records without overwriting existing rows.\",\n inputSchema: defineSchema((v) =>\n v.object({\n spreadsheetId: v.string().describe(\"The ID of the spreadsheet\"),\n range: v.string().describe(\n \"A1 notation range/table to append to (e.g., 'Sheet1!A:C')\",\n ),\n values: v.array(v.array(v.any())).describe(\"2D array of rows to append\"),\n valueInputOption: v.enum([\"RAW\", \"USER_ENTERED\"] as const).default(\"USER_ENTERED\"),\n insertDataOption: v.enum([\"OVERWRITE\", \"INSERT_ROWS\"] as const).default(\n \"INSERT_ROWS\",\n ),\n })\n )(),\n execute(\n { spreadsheetId, range, values, valueInputOption, insertDataOption },\n context,\n ) {\n const userId = requireUserIdFromContext(context);\n return createSheetsClient(userId).appendRange({\n spreadsheetId,\n range,\n values,\n valueInputOption,\n insertDataOption,\n });\n },\n});\n",
521
- "tools/batch-update.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createSheetsClient } from \"../lib/sheets-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"sheets-batch-update\",\n description:\n \"Run Google Sheets batchUpdate requests for formatting, filters, dimensions, protected ranges, charts, and other advanced spreadsheet changes.\",\n inputSchema: defineSchema((v) =>\n v.object({\n spreadsheetId: v.string().describe(\"The ID of the spreadsheet\"),\n requests: v.array(v.record(v.string(), v.any())).describe(\n \"Google Sheets API batchUpdate requests\",\n ),\n includeSpreadsheetInResponse: v.boolean().optional(),\n responseRanges: v.array(v.string()).optional(),\n })\n )(),\n execute(\n { spreadsheetId, requests, includeSpreadsheetInResponse, responseRanges },\n context,\n ) {\n const userId = requireUserIdFromContext(context);\n return createSheetsClient(userId).batchUpdate({\n spreadsheetId,\n requests,\n includeSpreadsheetInResponse,\n responseRanges,\n });\n },\n});\n",
522
- "tools/clear-range.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createSheetsClient } from \"../lib/sheets-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"sheets-clear-range\",\n description: \"Clear cell values from a Google Sheets range while preserving formatting.\",\n inputSchema: defineSchema((v) =>\n v.object({\n spreadsheetId: v.string().describe(\"The ID of the spreadsheet\"),\n range: v.string().describe(\n \"A1 notation range to clear (e.g., 'Sheet1!A2:D100')\",\n ),\n })\n )(),\n execute({ spreadsheetId, range }, context) {\n const userId = requireUserIdFromContext(context);\n return createSheetsClient(userId).clearRange(spreadsheetId, range);\n },\n});\n",
523
- "tools/copy-sheet.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createSheetsClient } from \"../lib/sheets-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"sheets-copy-sheet\",\n description: \"Copy a sheet/tab to another spreadsheet.\",\n inputSchema: defineSchema((v) =>\n v.object({\n spreadsheetId: v.string().describe(\"Source spreadsheet ID\"),\n sheetId: v.number().describe(\"Numeric source sheet ID\"),\n destinationSpreadsheetId: v.string().describe(\n \"Destination spreadsheet ID\",\n ),\n })\n )(),\n execute({ spreadsheetId, sheetId, destinationSpreadsheetId }, context) {\n const userId = requireUserIdFromContext(context);\n return createSheetsClient(userId).copySheet({\n spreadsheetId,\n sheetId,\n destinationSpreadsheetId,\n });\n },\n});\n",
524
- "tools/create-chart.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createSheetsClient } from \"../lib/sheets-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"sheets-create-chart\",\n description:\n \"Create an embedded chart. Provide a Google Sheets API EmbeddedChart spec without chartId.\",\n inputSchema: defineSchema((v) =>\n v.object({\n spreadsheetId: v.string().describe(\"The ID of the spreadsheet\"),\n chart: v.record(v.string(), v.any()).describe(\n \"Google Sheets API EmbeddedChart spec\",\n ),\n })\n )(),\n execute({ spreadsheetId, chart }, context) {\n const userId = requireUserIdFromContext(context);\n return createSheetsClient(userId).createChart(\n spreadsheetId,\n chart,\n );\n },\n});\n",
525
- "tools/create-spreadsheet.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createSheetsClient } from \"../lib/sheets-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"sheets-create-spreadsheet\",\n description:\n \"Create a new Google Sheets spreadsheet with optional sheet configurations. Returns the new spreadsheet ID and URL.\",\n inputSchema: defineSchema((v) =>\n v.object({\n title: v.string().describe(\"Title of the new spreadsheet\"),\n sheets: v\n .array(\n v.object({\n title: v.string().describe(\"Name of the sheet/tab\"),\n rowCount: v\n .number()\n .min(1)\n .max(10000)\n .optional()\n .describe(\"Number of rows (default: 1000)\"),\n columnCount: v\n .number()\n .min(1)\n .max(26)\n .optional()\n .describe(\"Number of columns (default: 26)\"),\n }),\n )\n .optional()\n .describe(\n \"Optional array of sheet configurations. If not provided, a single default sheet is created.\",\n ),\n initialData: v\n .object({\n sheetTitle: v.string().describe(\"Name of the sheet to write data to\"),\n range: v\n .string()\n .describe(\"Range in A1 notation (e.g., 'A1', 'A1:D10')\"),\n values: v\n .array(v.array(v.any()))\n .describe(\n \"2D array of values to write. Example: [['Name', 'Age'], ['John', 30]]\",\n ),\n })\n .optional()\n .describe(\"Optional initial data to populate the spreadsheet\"),\n })\n )(),\n async execute({ title, sheets, initialData }, context) {\n const userId = requireUserIdFromContext(context);\n const client = createSheetsClient(userId);\n const spreadsheet = await client.createSpreadsheet({ title, sheets });\n\n if (!initialData) {\n return {\n id: spreadsheet.spreadsheetId,\n title: spreadsheet.properties.title,\n url: spreadsheet.spreadsheetUrl,\n sheets: spreadsheet.sheets.map(({ properties }) => ({\n id: properties.sheetId,\n title: properties.title,\n index: properties.index,\n })),\n };\n }\n\n await client.writeRange({\n spreadsheetId: spreadsheet.spreadsheetId,\n range: `${initialData.sheetTitle}!${initialData.range}`,\n values: initialData.values,\n valueInputOption: \"USER_ENTERED\",\n });\n\n return {\n id: spreadsheet.spreadsheetId,\n title: spreadsheet.properties.title,\n url: spreadsheet.spreadsheetUrl,\n sheets: spreadsheet.sheets.map(({ properties }) => ({\n id: properties.sheetId,\n title: properties.title,\n index: properties.index,\n })),\n };\n },\n});\n",
526
- "tools/delete-sheet.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createSheetsClient } from \"../lib/sheets-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"sheets-delete-sheet\",\n description: \"Delete a sheet/tab from a spreadsheet by numeric sheet ID.\",\n inputSchema: defineSchema((v) =>\n v.object({\n spreadsheetId: v.string().describe(\"The ID of the spreadsheet\"),\n sheetId: v.number().describe(\n \"Numeric sheet ID to delete, from get-spreadsheet\",\n ),\n })\n )(),\n execute({ spreadsheetId, sheetId }, context) {\n const userId = requireUserIdFromContext(context);\n return createSheetsClient(userId).deleteSheet(\n spreadsheetId,\n sheetId,\n );\n },\n});\n",
527
- "tools/delete-spreadsheet.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createSheetsClient } from \"../lib/sheets-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"sheets-delete-spreadsheet\",\n description:\n \"Delete an app-accessible spreadsheet file. Defaults to moving it to trash for safer cleanup.\",\n inputSchema: defineSchema((v) =>\n v.object({\n spreadsheetId: v.string().describe(\"The spreadsheet/Drive file ID\"),\n permanentlyDelete: v.boolean().default(false).describe(\n \"If true, permanently deletes instead of moving to trash\",\n ),\n })\n )(),\n execute({ spreadsheetId, permanentlyDelete }, context) {\n const userId = requireUserIdFromContext(context);\n return createSheetsClient(userId).deleteSpreadsheet(\n spreadsheetId,\n { permanentlyDelete },\n );\n },\n});\n",
528
- "tools/find-replace.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createSheetsClient } from \"../lib/sheets-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"sheets-find-replace\",\n description: \"Find and replace text in a spreadsheet, optionally limited to a single sheet ID.\",\n inputSchema: defineSchema((v) =>\n v.object({\n spreadsheetId: v.string().describe(\"The ID of the spreadsheet\"),\n find: v.string().describe(\"Text or regex pattern to find\"),\n replacement: v.string().describe(\"Replacement text\"),\n sheetId: v.number().optional().describe(\n \"Optional numeric sheet ID to limit replacement\",\n ),\n matchCase: v.boolean().optional(),\n matchEntireCell: v.boolean().optional(),\n searchByRegex: v.boolean().optional(),\n })\n )(),\n execute(\n {\n spreadsheetId,\n find,\n replacement,\n sheetId,\n matchCase,\n matchEntireCell,\n searchByRegex,\n },\n context,\n ) {\n const userId = requireUserIdFromContext(context);\n return createSheetsClient(userId).findReplace({\n spreadsheetId,\n find,\n replacement,\n sheetId,\n matchCase,\n matchEntireCell,\n searchByRegex,\n });\n },\n});\n",
529
- "tools/get-spreadsheet.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createSheetsClient } from \"../lib/sheets-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"sheets-get-spreadsheet\",\n description:\n \"Get metadata about a Google Sheets spreadsheet including all sheet names, properties, and structure. Use this to discover available sheets and their dimensions.\",\n inputSchema: defineSchema((v) =>\n v.object({\n spreadsheetId: v\n .string()\n .describe(\"The ID of the spreadsheet (from URL or list-spreadsheets)\"),\n })\n )(),\n async execute({ spreadsheetId }, context) {\n const userId = requireUserIdFromContext(context);\n const client = createSheetsClient(userId);\n const spreadsheet = await client.getSpreadsheet(spreadsheetId);\n\n return {\n id: spreadsheet.spreadsheetId,\n title: spreadsheet.properties.title,\n url: spreadsheet.spreadsheetUrl,\n locale: spreadsheet.properties.locale,\n timeZone: spreadsheet.properties.timeZone,\n sheets: spreadsheet.sheets.map(({ properties }) => ({\n id: properties.sheetId,\n title: properties.title,\n index: properties.index,\n type: properties.sheetType,\n rowCount: properties.gridProperties?.rowCount,\n columnCount: properties.gridProperties?.columnCount,\n })),\n };\n },\n});\n",
530
- "tools/list-spreadsheets.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createSheetsClient } from \"../lib/sheets-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"sheets-list-spreadsheets\",\n description:\n \"List recent Google Sheets spreadsheets from Google Drive. Returns spreadsheet names, IDs, and metadata.\",\n inputSchema: defineSchema((v) =>\n v.object({\n maxResults: v\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of spreadsheets to return\"),\n orderBy: v\n .enum([\"createdTime\", \"modifiedTime\", \"name\"] as const)\n .default(\"modifiedTime\")\n .describe(\"Sort order for results\"),\n })\n )(),\n async execute({ maxResults, orderBy }, context) {\n const userId = requireUserIdFromContext(context);\n const client = createSheetsClient(userId);\n const spreadsheets = await client.listSpreadsheets({ maxResults, orderBy });\n\n return spreadsheets.map((spreadsheet) => ({\n id: spreadsheet.id,\n name: spreadsheet.name,\n url: spreadsheet.webViewLink,\n createdTime: spreadsheet.createdTime,\n modifiedTime: spreadsheet.modifiedTime,\n }));\n },\n});\n",
531
- "tools/read-range.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createSheetsClient } from \"../lib/sheets-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"sheets-read-range\",\n description:\n \"Read cell data from a Google Sheets range. Returns a 2D array of values. Use A1 notation (e.g., 'Sheet1!A1:D10', 'A1:B', or just 'Sheet1' for entire sheet).\",\n inputSchema: defineSchema((v) =>\n v.object({\n spreadsheetId: v.string().describe(\"The ID of the spreadsheet\"),\n range: v\n .string()\n .describe(\n \"Range in A1 notation (e.g., 'Sheet1!A1:D10', 'A1:B5', or 'Sheet1' for entire sheet)\",\n ),\n })\n )(),\n async execute({ spreadsheetId, range }, context) {\n const userId = requireUserIdFromContext(context);\n const client = createSheetsClient(userId);\n const { range: resultRange, values } = await client.readRange(\n spreadsheetId,\n range,\n );\n\n return {\n range: resultRange,\n values,\n rowCount: values.length,\n columnCount: values[0]?.length ?? 0,\n };\n },\n});\n",
532
- "tools/rename-sheet.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createSheetsClient } from \"../lib/sheets-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"sheets-rename-sheet\",\n description: \"Rename an existing sheet/tab by numeric sheet ID.\",\n inputSchema: defineSchema((v) =>\n v.object({\n spreadsheetId: v.string().describe(\"The ID of the spreadsheet\"),\n sheetId: v.number().describe(\"Numeric sheet ID to rename\"),\n title: v.string().describe(\"New sheet/tab title\"),\n })\n )(),\n execute({ spreadsheetId, sheetId, title }, context) {\n const userId = requireUserIdFromContext(context);\n return createSheetsClient(userId).renameSheet(\n spreadsheetId,\n sheetId,\n title,\n );\n },\n});\n",
533
- "tools/set-data-validation.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createSheetsClient } from \"../lib/sheets-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"sheets-set-data-validation\",\n description: \"Set a Google Sheets data validation rule on a grid range.\",\n inputSchema: defineSchema((v) =>\n v.object({\n spreadsheetId: v.string().describe(\"The ID of the spreadsheet\"),\n range: v.record(v.string(), v.any()).describe(\n \"Google Sheets API GridRange\",\n ),\n rule: v.record(v.string(), v.any()).describe(\n \"Google Sheets API DataValidationRule\",\n ),\n })\n )(),\n execute({ spreadsheetId, range, rule }, context) {\n const userId = requireUserIdFromContext(context);\n return createSheetsClient(userId).setDataValidation({\n spreadsheetId,\n range,\n rule,\n });\n },\n});\n",
534
- "tools/write-range.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createSheetsClient } from \"../lib/sheets-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"sheets-write-range\",\n description:\n \"Write data to a Google Sheets range. Overwrites existing content in the specified range. Provide data as a 2D array where each inner array is a row.\",\n inputSchema: defineSchema((v) =>\n v.object({\n spreadsheetId: v.string().describe(\"The ID of the spreadsheet\"),\n range: v\n .string()\n .describe(\n \"Range in A1 notation where to write data (e.g., 'Sheet1!A1', 'Sheet1!A1:D5')\",\n ),\n values: v\n .array(v.array(v.any()))\n .describe(\n \"2D array of values to write. Each inner array represents a row. Example: [['Name', 'Age'], ['John', 30], ['Jane', 25]]\",\n ),\n valueInputOption: v\n .enum([\"RAW\", \"USER_ENTERED\"] as const)\n .default(\"USER_ENTERED\")\n .describe(\n \"RAW: Values are stored as-is. USER_ENTERED: Values are parsed as if typed by user (formulas, numbers, dates)\",\n ),\n })\n )(),\n async execute({ spreadsheetId, range, values, valueInputOption }, context) {\n const userId = requireUserIdFromContext(context);\n const client = createSheetsClient(userId);\n return client.writeRange({ spreadsheetId, range, values, valueInputOption });\n },\n});\n"
535
- }
536
- },
537
- "integration:shopify": {
538
- "files": {
539
- ".env.example": "# Shopify OAuth Configuration\n# Get your credentials from https://partners.shopify.com\nSHOPIFY_CLIENT_ID=your-client-id\nSHOPIFY_CLIENT_SECRET=your-client-secret\nSHOPIFY_SHOP_DOMAIN=mystore.myshopify.com\n",
540
- "app/api/auth/shopify/callback/route.ts": "import { createOAuthCallbackHandler, shopifyConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\n\nexport const GET = createOAuthCallbackHandler(shopifyConfig, { tokenStore });\n",
541
- "app/api/auth/shopify/route.ts": "import { createOAuthInitHandler, shopifyConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../lib/token-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../lib/user-id.ts\";\n\nexport const GET = createOAuthInitHandler(shopifyConfig, {\n tokenStore,\n getUserId: requireUserIdFromRequest,\n});\n",
542
- "lib/shopify-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst SHOPIFY_SHOP_DOMAIN = process.env.SHOPIFY_SHOP_DOMAIN ?? \"shop.myshopify.com\";\nconst SHOPIFY_API_VERSION = \"2024-01\";\nconst SHOPIFY_BASE_URL = `https://${SHOPIFY_SHOP_DOMAIN}/admin/api/${SHOPIFY_API_VERSION}`;\n\ninterface ShopifyProduct {\n id: number;\n title: string;\n body_html: string;\n vendor: string;\n product_type: string;\n created_at: string;\n updated_at: string;\n published_at: string | null;\n status: string;\n tags: string;\n variants: Array<{\n id: number;\n title: string;\n price: string;\n sku: string;\n inventory_quantity: number;\n }>;\n images: Array<{\n id: number;\n src: string;\n alt: string | null;\n }>;\n}\n\ninterface ShopifyOrder {\n id: number;\n order_number: number;\n email: string;\n created_at: string;\n updated_at: string;\n total_price: string;\n subtotal_price: string;\n total_tax: string;\n currency: string;\n financial_status: string;\n fulfillment_status: string | null;\n customer: {\n id: number;\n email: string;\n first_name: string;\n last_name: string;\n } | null;\n line_items: Array<{\n id: number;\n title: string;\n quantity: number;\n price: string;\n sku: string;\n variant_title: string;\n }>;\n shipping_address: {\n address1: string;\n city: string;\n province: string;\n country: string;\n zip: string;\n } | null;\n}\n\ninterface ShopifyCustomer {\n id: number;\n email: string;\n first_name: string;\n last_name: string;\n phone: string | null;\n created_at: string;\n updated_at: string;\n orders_count: number;\n total_spent: string;\n tags: string;\n state: string;\n verified_email: boolean;\n addresses: Array<{\n id: number;\n address1: string;\n city: string;\n province: string;\n country: string;\n zip: string;\n default: boolean;\n }>;\n}\n\nfunction buildQuery(params: URLSearchParams): string {\n const query = params.toString();\n return query ? `?${query}` : \"\";\n}\n\nasync function shopifyFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with Shopify. Please connect your account.\");\n }\n\n const response = await fetch(`${SHOPIFY_BASE_URL}${endpoint}`, {\n ...options,\n headers: {\n \"X-Shopify-Access-Token\": token,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n let errors: string | undefined;\n try {\n const body = (await response.json()) as { errors?: string };\n errors = body.errors;\n } catch {\n // ignore JSON parse errors\n }\n\n throw new Error(`Shopify API error: ${response.status} ${errors ?? response.statusText}`);\n }\n\n return response.json();\n}\n\nexport async function listProducts(options?: {\n limit?: number;\n status?: \"active\" | \"archived\" | \"draft\";\n productType?: string;\n}): Promise<ShopifyProduct[]> {\n const params = new URLSearchParams();\n if (options?.limit) params.set(\"limit\", options.limit.toString());\n if (options?.status) params.set(\"status\", options.status);\n if (options?.productType) params.set(\"product_type\", options.productType);\n\n const { products } = await shopifyFetch<{ products: ShopifyProduct[] }>(\n `/products.json${buildQuery(params)}`,\n );\n return products;\n}\n\nexport async function getProduct(productId: number | string): Promise<ShopifyProduct> {\n const { product } = await shopifyFetch<{ product: ShopifyProduct }>(`/products/${productId}.json`);\n return product;\n}\n\nexport async function listOrders(options?: {\n limit?: number;\n status?: \"open\" | \"closed\" | \"cancelled\" | \"any\";\n financialStatus?: \"pending\" | \"authorized\" | \"paid\" | \"refunded\" | \"voided\";\n fulfillmentStatus?: \"shipped\" | \"partial\" | \"unshipped\" | \"any\" | \"unfulfilled\";\n}): Promise<ShopifyOrder[]> {\n const params = new URLSearchParams();\n if (options?.limit) params.set(\"limit\", options.limit.toString());\n if (options?.status) params.set(\"status\", options.status);\n if (options?.financialStatus) params.set(\"financial_status\", options.financialStatus);\n if (options?.fulfillmentStatus) params.set(\"fulfillment_status\", options.fulfillmentStatus);\n\n const { orders } = await shopifyFetch<{ orders: ShopifyOrder[] }>(\n `/orders.json${buildQuery(params)}`,\n );\n return orders;\n}\n\nexport async function getOrder(orderId: number | string): Promise<ShopifyOrder> {\n const { order } = await shopifyFetch<{ order: ShopifyOrder }>(`/orders/${orderId}.json`);\n return order;\n}\n\nexport async function listCustomers(options?: {\n limit?: number;\n query?: string;\n}): Promise<ShopifyCustomer[]> {\n const params = new URLSearchParams();\n if (options?.limit) params.set(\"limit\", options.limit.toString());\n if (options?.query) params.set(\"query\", options.query);\n\n const { customers } = await shopifyFetch<{ customers: ShopifyCustomer[] }>(\n `/customers.json${buildQuery(params)}`,\n );\n return customers;\n}\n\nexport async function getCustomer(customerId: number | string): Promise<ShopifyCustomer> {\n const { customer } = await shopifyFetch<{ customer: ShopifyCustomer }>(\n `/customers/${customerId}.json`,\n );\n return customer;\n}\n\nexport async function getShopInfo(): Promise<{\n id: number;\n name: string;\n email: string;\n domain: string;\n currency: string;\n timezone: string;\n}> {\n const { shop } = await shopifyFetch<{\n shop: {\n id: number;\n name: string;\n email: string;\n domain: string;\n currency: string;\n timezone: string;\n };\n }>(\"/shop.json\");\n\n return shop;\n}\n",
543
- "tools/get-order.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { getOrder } from \"../lib/shopify-client.ts\";\n\nexport default tool({\n id: \"shopify-get-order\",\n description: \"Get details of a specific Shopify order by its ID.\",\n inputSchema: defineSchema((v) => v.object({\n orderId: v.union([v.number(), v.string()]).describe(\"The ID of the order to retrieve\"),\n }))(),\n async execute({ orderId }) {\n const order = await getOrder(orderId);\n\n return {\n id: order.id,\n orderNumber: order.order_number,\n email: order.email,\n createdAt: order.created_at,\n updatedAt: order.updated_at,\n totalPrice: order.total_price,\n subtotalPrice: order.subtotal_price,\n totalTax: order.total_tax,\n currency: order.currency,\n financialStatus: order.financial_status,\n fulfillmentStatus: order.fulfillment_status,\n customer: order.customer\n ? {\n id: order.customer.id,\n email: order.customer.email,\n firstName: order.customer.first_name,\n lastName: order.customer.last_name,\n }\n : null,\n lineItems: order.line_items.map((item) => ({\n id: item.id,\n title: item.title,\n quantity: item.quantity,\n price: item.price,\n sku: item.sku,\n variantTitle: item.variant_title,\n })),\n shippingAddress: order.shipping_address\n ? {\n address1: order.shipping_address.address1,\n city: order.shipping_address.city,\n province: order.shipping_address.province,\n country: order.shipping_address.country,\n zip: order.shipping_address.zip,\n }\n : null,\n };\n },\n});\n",
544
- "tools/get-product.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { getProduct } from \"../lib/shopify-client.ts\";\n\nexport default tool({\n id: \"shopify-get-product\",\n description: \"Get details of a specific Shopify product by its ID.\",\n inputSchema: defineSchema((v) => v.object({\n productId: v.union([v.number(), v.string()]).describe(\"The ID of the product to retrieve\"),\n }))(),\n async execute({ productId }) {\n const product = await getProduct(productId);\n\n return {\n id: product.id,\n title: product.title,\n bodyHtml: product.body_html,\n vendor: product.vendor,\n productType: product.product_type,\n status: product.status,\n tags: product.tags,\n createdAt: product.created_at,\n updatedAt: product.updated_at,\n publishedAt: product.published_at,\n variants: product.variants.map((variant) => ({\n id: variant.id,\n title: variant.title,\n price: variant.price,\n sku: variant.sku,\n inventoryQuantity: variant.inventory_quantity,\n })),\n images: product.images.map((image) => ({\n id: image.id,\n src: image.src,\n alt: image.alt,\n })),\n };\n },\n});\n",
545
- "tools/list-customers.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { listCustomers } from \"../lib/shopify-client.ts\";\n\nexport default tool({\n id: \"shopify-list-customers\",\n description: \"List customers from your Shopify store. Can search by query string.\",\n inputSchema: defineSchema((v) => v.object({\n limit: v\n .number()\n .min(1)\n .max(250)\n .default(20)\n .describe(\"Maximum number of customers to return\"),\n query: v\n .string()\n .optional()\n .describe(\"Search query to filter customers (e.g., email, name)\"),\n }))(),\n async execute({ limit, query }) {\n const customers = await listCustomers({ limit, query });\n\n return customers.map((customer) => ({\n id: customer.id,\n email: customer.email,\n firstName: customer.first_name,\n lastName: customer.last_name,\n phone: customer.phone,\n createdAt: customer.created_at,\n updatedAt: customer.updated_at,\n ordersCount: customer.orders_count,\n totalSpent: customer.total_spent,\n tags: customer.tags,\n state: customer.state,\n verifiedEmail: customer.verified_email,\n addresses: customer.addresses.map((address) => ({\n id: address.id,\n address1: address.address1,\n city: address.city,\n province: address.province,\n country: address.country,\n zip: address.zip,\n default: address.default,\n })),\n }));\n },\n});\n",
546
- "tools/list-orders.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { listOrders } from \"../lib/shopify-client.ts\";\n\nexport default tool({\n id: \"shopify-list-orders\",\n description:\n \"List orders from your Shopify store. Can filter by status, financial status, and fulfillment status.\",\n inputSchema: defineSchema((v) => v.object({\n limit: v\n .number()\n .min(1)\n .max(250)\n .default(20)\n .describe(\"Maximum number of orders to return\"),\n status: v\n .enum([\"open\", \"closed\", \"cancelled\", \"any\"])\n .optional()\n .describe(\"Filter by order status\"),\n financialStatus: v\n .enum([\"pending\", \"authorized\", \"paid\", \"refunded\", \"voided\"])\n .optional()\n .describe(\"Filter by financial status\"),\n fulfillmentStatus: v\n .enum([\"shipped\", \"partial\", \"unshipped\", \"any\", \"unfulfilled\"])\n .optional()\n .describe(\"Filter by fulfillment status\"),\n }))(),\n async execute({ limit, status, financialStatus, fulfillmentStatus }) {\n const orders = await listOrders({\n limit,\n status,\n financialStatus,\n fulfillmentStatus,\n });\n\n return orders.map((order) => ({\n id: order.id,\n orderNumber: order.order_number,\n email: order.email,\n createdAt: order.created_at,\n totalPrice: order.total_price,\n subtotalPrice: order.subtotal_price,\n totalTax: order.total_tax,\n currency: order.currency,\n financialStatus: order.financial_status,\n fulfillmentStatus: order.fulfillment_status,\n customer: order.customer\n ? {\n id: order.customer.id,\n email: order.customer.email,\n firstName: order.customer.first_name,\n lastName: order.customer.last_name,\n }\n : null,\n lineItems: order.line_items.map((item) => ({\n id: item.id,\n title: item.title,\n quantity: item.quantity,\n price: item.price,\n sku: item.sku,\n variantTitle: item.variant_title,\n })),\n shippingAddress: order.shipping_address\n ? {\n address1: order.shipping_address.address1,\n city: order.shipping_address.city,\n province: order.shipping_address.province,\n country: order.shipping_address.country,\n zip: order.shipping_address.zip,\n }\n : null,\n }));\n },\n});\n",
547
- "tools/list-products.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { listProducts } from \"../lib/shopify-client.ts\";\n\nexport default tool({\n id: \"shopify-list-products\",\n description:\n \"List products from your Shopify store. Can filter by status and product type.\",\n inputSchema: defineSchema((v) => v.object({\n limit: v\n .number()\n .min(1)\n .max(250)\n .default(20)\n .describe(\"Maximum number of products to return\"),\n status: v\n .enum([\"active\", \"archived\", \"draft\"])\n .optional()\n .describe(\"Filter by product status\"),\n productType: v.string().optional().describe(\"Filter by product type\"),\n }))(),\n async execute({ limit, status, productType }) {\n const products = await listProducts({ limit, status, productType });\n\n return products.map(\n ({\n id,\n title,\n vendor,\n product_type,\n status: productStatus,\n tags,\n created_at,\n variants,\n images,\n }) => ({\n id,\n title,\n vendor,\n productType: product_type,\n status: productStatus,\n tags,\n createdAt: created_at,\n variants: variants.map(\n ({ id, title, price, sku, inventory_quantity }) => ({\n id,\n title,\n price,\n sku,\n inventoryQuantity: inventory_quantity,\n }),\n ),\n images: images.map(({ id, src, alt }) => ({ id, src, alt })),\n }),\n );\n },\n});\n"
548
- }
549
- },
550
- "integration:slack": {
551
- "files": {
552
- "app/api/auth/slack/callback/route.ts": "import { createOAuthCallbackHandler, slackConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\n\nexport const GET = createOAuthCallbackHandler(slackConfig, { tokenStore });\n",
553
- "app/api/auth/slack/route.ts": "import { createOAuthInitHandler, slackConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../lib/token-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../lib/user-id.ts\";\n\nexport const GET = createOAuthInitHandler(slackConfig, {\n tokenStore,\n getUserId: requireUserIdFromRequest,\n});\n",
554
- "lib/slack-client.ts": "/**\n * Slack API Client\n *\n * Provides a type-safe interface to Slack API operations.\n */\n\nimport { getValidToken } from \"./oauth.ts\";\n\nfunction getEnv(key: string): string | undefined {\n // @ts-ignore - Deno global\n if (typeof Deno !== \"undefined\") {\n // @ts-ignore - Deno global\n return Deno.env.get(key);\n }\n\n // @ts-ignore - process global\n if (typeof process !== \"undefined\" && process.env) {\n // @ts-ignore - process global\n return process.env[key];\n }\n\n return undefined;\n}\n\nconst SLACK_API_BASE = \"https://slack.com/api\";\n\nexport interface SlackChannel {\n id: string;\n name: string;\n is_channel: boolean;\n is_private: boolean;\n is_member: boolean;\n topic?: { value: string };\n purpose?: { value: string };\n}\n\nexport interface SlackMessage {\n type: string;\n user?: string;\n text: string;\n ts: string;\n thread_ts?: string;\n reply_count?: number;\n reactions?: Array<{ name: string; count: number }>;\n}\n\nexport interface SlackUser {\n id: string;\n name: string;\n real_name: string;\n profile: {\n display_name: string;\n email?: string;\n image_48?: string;\n };\n}\n\n/**\n * Slack OAuth provider configuration\n */\nexport const slackOAuthProvider = {\n name: \"slack\",\n authorizationUrl: \"https://slack.com/oauth/v2/authorize\",\n tokenUrl: \"https://slack.com/api/oauth.v2.access\",\n clientId: getEnv(\"SLACK_CLIENT_ID\") ?? \"\",\n clientSecret: getEnv(\"SLACK_CLIENT_SECRET\") ?? \"\",\n scopes: [\n \"channels:history\",\n \"channels:read\",\n \"chat:write\",\n \"groups:history\",\n \"groups:read\",\n \"im:history\",\n \"im:read\",\n \"mpim:history\",\n \"mpim:read\",\n \"users:read\",\n ],\n callbackPath: \"/api/auth/slack/callback\",\n};\n\nexport interface SlackClient {\n listChannels(options?: {\n limit?: number;\n excludeArchived?: boolean;\n }): Promise<SlackChannel[]>;\n getMessages(\n channelId: string,\n options?: { limit?: number; oldest?: string },\n ): Promise<SlackMessage[]>;\n sendMessage(\n channelId: string,\n text: string,\n options?: { threadTs?: string; unfurlLinks?: boolean },\n ): Promise<{ ts: string; channel: string }>;\n getUser(userId: string): Promise<SlackUser>;\n getThread(channelId: string, threadTs: string): Promise<SlackMessage[]>;\n searchMessages(\n query: string,\n options?: { count?: number },\n ): Promise<SlackMessage[]>;\n}\n\n/**\n * Create a Slack client for a specific user\n */\nexport function createSlackClient(userId: string): SlackClient {\n async function getAccessToken(): Promise<string> {\n const token = await getValidToken(slackOAuthProvider, userId, \"slack\");\n if (!token) {\n throw new Error(\n \"Slack not connected. Please connect your Slack account first.\",\n );\n }\n return token;\n }\n\n async function apiRequest<T>(\n method: string,\n params: Record<string, unknown> = {},\n ): Promise<T> {\n const accessToken = await getAccessToken();\n\n const response = await fetch(`${SLACK_API_BASE}/${method}`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${accessToken}`,\n \"Content-Type\": \"application/json; charset=utf-8\",\n },\n body: JSON.stringify(params),\n });\n\n const data = await response.json();\n\n if (!data.ok) {\n throw new Error(`Slack API error: ${data.error}`);\n }\n\n return data as T;\n }\n\n return {\n /**\n * List channels the user is a member of\n */\n async listChannels(options = {}): Promise<SlackChannel[]> {\n const result = await apiRequest<{ channels: SlackChannel[] }>(\n \"conversations.list\",\n {\n limit: options.limit ?? 100,\n exclude_archived: options.excludeArchived ?? true,\n types: \"public_channel,private_channel\",\n },\n );\n return result.channels;\n },\n\n /**\n * Get messages from a channel\n */\n async getMessages(\n channelId: string,\n options = {},\n ): Promise<SlackMessage[]> {\n const result = await apiRequest<{ messages: SlackMessage[] }>(\n \"conversations.history\",\n {\n channel: channelId,\n limit: options.limit ?? 20,\n oldest: options.oldest,\n },\n );\n return result.messages;\n },\n\n /**\n * Send a message to a channel\n */\n async sendMessage(\n channelId: string,\n text: string,\n options = {},\n ): Promise<{ ts: string; channel: string }> {\n return apiRequest<{ ts: string; channel: string }>(\"chat.postMessage\", {\n channel: channelId,\n text,\n thread_ts: options.threadTs,\n unfurl_links: options.unfurlLinks ?? true,\n });\n },\n\n /**\n * Get user info\n */\n async getUser(userId: string): Promise<SlackUser> {\n const result = await apiRequest<{ user: SlackUser }>(\"users.info\", {\n user: userId,\n });\n return result.user;\n },\n\n /**\n * Get thread replies\n */\n async getThread(\n channelId: string,\n threadTs: string,\n ): Promise<SlackMessage[]> {\n const result = await apiRequest<{ messages: SlackMessage[] }>(\n \"conversations.replies\",\n {\n channel: channelId,\n ts: threadTs,\n },\n );\n return result.messages;\n },\n\n /**\n * Search messages\n */\n async searchMessages(\n query: string,\n options = {},\n ): Promise<SlackMessage[]> {\n const result = await apiRequest<{\n messages: { matches: SlackMessage[] };\n }>(\"search.messages\", {\n query,\n count: options.count ?? 20,\n });\n return result.messages.matches;\n },\n };\n}\n",
555
- "tools/get-messages.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createSlackClient } from \"../lib/slack-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\ntype SlackMessage = {\n text?: string;\n user?: string;\n ts: string;\n thread_ts?: string;\n reply_count?: number;\n reactions?: Array<{ name: string; count: number }>;\n};\n\nexport default tool({\n id: \"slack-get-messages\",\n description: \"Get recent messages from a Slack channel\",\n inputSchema: defineSchema((v) => v.object({\n channel: v.string().describe(\"Channel ID (e.g., 'C1234567890')\"),\n limit: v\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of messages to return\"),\n }))(),\n execute: async ({ channel, limit }, context) => {\n const userId = requireUserIdFromContext(context);\n\n try {\n const slack = createSlackClient(userId);\n const messages = await slack.getMessages(channel, { limit });\n\n return {\n messages: messages.map((msg: SlackMessage) => ({\n text: msg.text ?? \"\",\n user: msg.user ?? \"unknown\",\n timestamp: msg.ts,\n threadTs: msg.thread_ts,\n replyCount: msg.reply_count ?? 0,\n reactions: msg.reactions?.map((r) => `${r.name} (${r.count})`) ?? [],\n })),\n count: messages.length,\n channel,\n message: `Retrieved ${messages.length} message(s) from channel.`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Slack not connected. Please connect your Slack account.\",\n connectUrl: \"/api/auth/slack\",\n };\n }\n throw error;\n }\n },\n});\n",
556
- "tools/list-channels.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createSlackClient } from \"../lib/slack-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\ntype SlackChannel = {\n id: string;\n name: string;\n is_private: boolean;\n is_member: boolean;\n topic?: { value: string };\n purpose?: { value: string };\n};\n\nexport default tool({\n id: \"slack-list-channels\",\n description: \"List Slack channels the user is a member of\",\n inputSchema: defineSchema((v) => v.object({\n limit: v\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of channels to return\"),\n excludeArchived: v\n .boolean()\n .default(true)\n .describe(\"Exclude archived channels\"),\n }))(),\n execute: async ({ limit, excludeArchived }, context) => {\n const userId = requireUserIdFromContext(context);\n\n try {\n const slack = createSlackClient(userId);\n const channels = await slack.listChannels({ limit, excludeArchived });\n const count = channels.length;\n\n return {\n channels: channels.map((ch: SlackChannel) => ({\n id: ch.id,\n name: ch.name,\n isPrivate: ch.is_private,\n isMember: ch.is_member,\n topic: ch.topic?.value ?? null,\n purpose: ch.purpose?.value ?? null,\n })),\n count,\n message: `Found ${count} channel(s).`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Slack not connected. Please connect your Slack account.\",\n connectUrl: \"/api/auth/slack\",\n };\n }\n\n throw error;\n }\n },\n});\n",
557
- "tools/send-message.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createSlackClient } from \"../lib/slack-client.ts\";\nimport { requireUserIdFromContext } from \"../lib/user-id.ts\";\n\nexport default tool({\n id: \"slack-send-message\",\n description: \"Send a message to a Slack channel\",\n inputSchema: defineSchema((v) => v.object({\n channel: v\n .string()\n .describe(\"Channel ID or name (e.g., 'C1234567890' or '#general')\"),\n text: v.string().min(1).describe(\"Message text to send\"),\n threadTs: v\n .string()\n .optional()\n .describe(\"Thread timestamp to reply to (for threaded messages)\"),\n }))(),\n execute: async ({ channel, text, threadTs }, context) => {\n const userId = requireUserIdFromContext(context);\n\n try {\n const slack = createSlackClient(userId);\n const result = await slack.sendMessage(channel, text, { threadTs });\n\n return {\n success: true,\n messageTs: result.ts,\n channel: result.channel,\n message: threadTs\n ? `Reply sent to thread in ${channel}.`\n : `Message sent to ${channel}.`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Slack not connected. Please connect your Slack account.\",\n connectUrl: \"/api/auth/slack\",\n };\n }\n throw error;\n }\n },\n});\n"
558
- }
559
- },
560
- "integration:snowflake": {
561
- "files": {
562
- ".env.example": "# Snowflake Integration\n# Get your account details from https://app.snowflake.com/\n\n# Your Snowflake account identifier (e.g., xy12345.us-east-1)\nSNOWFLAKE_ACCOUNT=xy12345.us-east-1\n\n# Authentication credentials\nSNOWFLAKE_USERNAME=your_username\nSNOWFLAKE_PASSWORD=your_password\n\n# Default warehouse for compute resources\nSNOWFLAKE_WAREHOUSE=COMPUTE_WH\n\n# Optional: Default database and schema\nSNOWFLAKE_DATABASE=your_database\nSNOWFLAKE_SCHEMA=PUBLIC\n",
563
- "lib/snowflake-client.ts": "import {\n getSnowflakeAccount,\n getSnowflakeDatabase,\n getSnowflakePassword,\n getSnowflakeSchema,\n getSnowflakeUsername,\n getSnowflakeWarehouse,\n} from \"./token-store.ts\";\n\ninterface SnowflakeStatementResponse {\n statementHandle: string;\n statementStatusUrl: string;\n message?: string;\n code?: string;\n}\n\ninterface SnowflakeQueryResult {\n resultSetMetaData: {\n rowType: Array<{\n name: string;\n type: string;\n nullable: boolean;\n scale?: number;\n precision?: number;\n length?: number;\n }>;\n numRows: number;\n format?: string;\n partitionInfo?: Array<{\n rowCount: number;\n uncompressedSize: number;\n }>;\n };\n data: unknown[][];\n code?: string;\n message?: string;\n statementHandle?: string;\n statementStatusUrl?: string;\n}\n\ninterface SnowflakeQueryStatusResponse {\n message: string;\n code: string;\n statementHandle: string;\n statementStatusUrl: string;\n sqlText?: string;\n resultSetMetaData?: SnowflakeQueryResult[\"resultSetMetaData\"];\n data?: unknown[][];\n stats?: {\n numRowsInserted?: number;\n numRowsUpdated?: number;\n numRowsDeleted?: number;\n numDuplicateRowsUpdated?: number;\n };\n}\n\ninterface DatabaseInfo {\n name: string;\n created_on: string;\n owner: string;\n comment?: string;\n}\n\ninterface SchemaInfo {\n name: string;\n database_name: string;\n created_on: string;\n owner: string;\n comment?: string;\n}\n\ninterface TableInfo {\n name: string;\n database_name: string;\n schema_name: string;\n kind: string;\n created_on: string;\n row_count?: number;\n bytes?: number;\n owner: string;\n comment?: string;\n}\n\ninterface ColumnInfo {\n name: string;\n type: string;\n kind: string;\n null?: string;\n default?: string;\n primary_key?: string;\n unique_key?: string;\n check?: string;\n expression?: string;\n comment?: string;\n}\n\ninterface SnowflakeError extends Error {\n code?: string;\n sqlState?: string;\n}\n\n/** Validate a Snowflake identifier (database, schema, or table name). */\nfunction validateIdentifier(value: string, label: string): string {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {\n throw new Error(\n `Invalid ${label}: must start with a letter or underscore and contain only letters, numbers, and underscores`,\n );\n }\n return value;\n}\n\nasync function snowflakeFetch<T>(\n endpoint: string,\n options: RequestInit = {},\n): Promise<T> {\n const account = getSnowflakeAccount();\n const username = getSnowflakeUsername();\n const password = getSnowflakePassword();\n\n const baseUrl = `https://${account}.snowflakecomputing.com/api/v2`;\n const authHeader = `Basic ${btoa(`${username}:${password}`)}`;\n\n const response = await fetch(`${baseUrl}${endpoint}`, {\n ...options,\n headers: {\n Authorization: authHeader,\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n \"X-Snowflake-Authorization-Token-Type\": \"KEYPAIR_JWT\",\n ...options.headers,\n },\n });\n\n if (response.ok) return await response.json();\n\n const errorData = (await response.json().catch(() => ({}))) as Partial<SnowflakeError>;\n const errorMessage =\n errorData.message ??\n `Snowflake API error: ${response.status} ${response.statusText}`;\n\n const err: SnowflakeError = new Error(errorMessage);\n err.code = errorData.code;\n err.sqlState = errorData.sqlState;\n throw err;\n}\n\nasync function submitStatement(\n sqlText: string,\n database?: string,\n schema?: string,\n timeout?: number,\n async_exec = false,\n): Promise<SnowflakeStatementResponse | SnowflakeQueryResult> {\n const warehouse = getSnowflakeWarehouse();\n\n const requestBody = {\n statement: sqlText,\n warehouse,\n database: database ?? getSnowflakeDatabase(),\n schema: schema ?? getSnowflakeSchema(),\n timeout: timeout ?? 60,\n resultSetMetaData: { format: \"json\" },\n parameters: {},\n };\n\n const endpoint = async_exec ? \"/statements?async=true\" : \"/statements\";\n\n return await snowflakeFetch<SnowflakeStatementResponse | SnowflakeQueryResult>(\n endpoint,\n {\n method: \"POST\",\n body: JSON.stringify(requestBody),\n },\n );\n}\n\nexport async function getQueryStatus(\n statementHandle: string,\n): Promise<SnowflakeQueryStatusResponse> {\n return await snowflakeFetch<SnowflakeQueryStatusResponse>(\n `/statements/${statementHandle}`,\n );\n}\n\nexport async function cancelQuery(statementHandle: string): Promise<void> {\n await snowflakeFetch(`/statements/${statementHandle}/cancel`, {\n method: \"POST\",\n });\n}\n\nfunction transformResults(result: SnowflakeQueryResult): Record<string, unknown>[] {\n if (result.data.length === 0) return [];\n\n const columns = result.resultSetMetaData.rowType.map((col) => col.name);\n\n return result.data.map((row) => {\n const obj: Record<string, unknown> = {};\n for (let i = 0; i < columns.length; i++) obj[columns[i]] = row[i];\n return obj;\n });\n}\n\nexport async function runQuery(\n sql: string,\n database?: string,\n schema?: string,\n options: {\n timeout?: number;\n async?: boolean;\n } = {},\n): Promise<{\n columns: Array<{ name: string; type: string; nullable: boolean }>;\n rows: Record<string, unknown>[];\n rowCount: number;\n statementHandle?: string;\n}> {\n const result = await submitStatement(\n sql,\n database,\n schema,\n options.timeout,\n options.async,\n );\n\n if (\"statementHandle\" in result && !(\"data\" in result)) {\n return {\n columns: [],\n rows: [],\n rowCount: 0,\n statementHandle: result.statementHandle,\n };\n }\n\n const queryResult = result as SnowflakeQueryResult;\n\n return {\n columns: queryResult.resultSetMetaData.rowType.map((col) => ({\n name: col.name,\n type: col.type,\n nullable: col.nullable,\n })),\n rows: transformResults(queryResult),\n rowCount: queryResult.resultSetMetaData.numRows,\n statementHandle: queryResult.statementHandle,\n };\n}\n\nexport async function listDatabases(): Promise<DatabaseInfo[]> {\n const result = await runQuery(\"SHOW DATABASES\");\n return result.rows as DatabaseInfo[];\n}\n\nexport async function listSchemas(database: string): Promise<SchemaInfo[]> {\n validateIdentifier(database, \"database name\");\n const result = await runQuery(`SHOW SCHEMAS IN DATABASE ${database}`);\n return result.rows as SchemaInfo[];\n}\n\nexport async function listTables(\n database: string,\n schema: string,\n): Promise<TableInfo[]> {\n validateIdentifier(database, \"database name\");\n validateIdentifier(schema, \"schema name\");\n const result = await runQuery(`SHOW TABLES IN ${database}.${schema}`);\n return result.rows as TableInfo[];\n}\n\nexport async function describeTable(\n database: string,\n schema: string,\n table: string,\n): Promise<{\n columns: ColumnInfo[];\n primaryKeys: string[];\n}> {\n validateIdentifier(database, \"database name\");\n validateIdentifier(schema, \"schema name\");\n validateIdentifier(table, \"table name\");\n const result = await runQuery(`DESCRIBE TABLE ${database}.${schema}.${table}`);\n\n const columns = result.rows as ColumnInfo[];\n const primaryKeys = columns\n .filter((col) => col.primary_key === \"Y\")\n .map((col) => col.name);\n\n return { columns, primaryKeys };\n}\n\nexport async function getTableRowCount(\n database: string,\n schema: string,\n table: string,\n): Promise<number> {\n validateIdentifier(database, \"database name\");\n validateIdentifier(schema, \"schema name\");\n validateIdentifier(table, \"table name\");\n const result = await runQuery(\n `SELECT COUNT(*) as count FROM ${database}.${schema}.${table}`,\n );\n\n const count = result.rows[0]?.count;\n return count == null ? 0 : Number(count);\n}\n\nexport async function getSessionInfo(): Promise<{\n version: string;\n warehouse: string;\n database?: string;\n schema?: string;\n user: string;\n role?: string;\n}> {\n const result = await runQuery(`\n SELECT\n CURRENT_VERSION() as version,\n CURRENT_WAREHOUSE() as warehouse,\n CURRENT_DATABASE() as database,\n CURRENT_SCHEMA() as schema,\n CURRENT_USER() as user,\n CURRENT_ROLE() as role\n `);\n\n const row = result.rows[0];\n if (!row) throw new Error(\"Failed to get session info\");\n\n return row as {\n version: string;\n warehouse: string;\n database?: string;\n schema?: string;\n user: string;\n role?: string;\n };\n}\n",
564
- "tools/describe-table.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { describeTable, getTableRowCount } from \"../lib/snowflake-client.ts\";\n\nexport default tool({\n id: \"snowflake-describe-table\",\n description:\n \"Get detailed schema information about a specific table in Snowflake. Returns column names, data types, constraints, and table statistics.\",\n inputSchema: defineSchema((v) => v.object({\n database: v.string().describe(\"The name of the database containing the table\"),\n schema: v\n .string()\n .default(\"PUBLIC\")\n .describe(\"The name of the schema containing the table. Defaults to PUBLIC.\"),\n table: v.string().describe(\"The name of the table to describe\"),\n includeRowCount: v\n .boolean()\n .default(false)\n .describe(\n \"Include the current row count for the table (may be slow for large tables)\",\n ),\n }))(),\n async execute({ database, schema, table, includeRowCount }) {\n const description = await describeTable(database, schema, table);\n\n const rowCount = includeRowCount\n ? await getTableRowCount(database, schema, table).catch(() => null)\n : null;\n\n return {\n database,\n schema,\n table,\n rowCount,\n primaryKeys: description.primaryKeys,\n columnCount: description.columns.length,\n columns: description.columns.map((col) => ({\n name: col.name,\n type: col.type,\n kind: col.kind,\n nullable: col.null === \"Y\",\n default: col.default || null,\n primaryKey: col.primary_key === \"Y\",\n uniqueKey: col.unique_key === \"Y\",\n check: col.check || null,\n expression: col.expression || null,\n comment: col.comment || null,\n })),\n };\n },\n});\n",
565
- "tools/list-databases.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { listDatabases } from \"../lib/snowflake-client.ts\";\n\nexport default tool({\n id: \"snowflake-list-databases\",\n description:\n \"List all databases in your Snowflake account. Returns database names, creation dates, owners, and comments.\",\n inputSchema: defineSchema((v) => v.object({\n includeDetails: v\n .boolean()\n .default(true)\n .describe(\"Include detailed information like creation date, owner, and comments\"),\n }))(),\n async execute({ includeDetails }) {\n const databases = await listDatabases();\n const count = databases.length;\n\n if (!includeDetails) {\n return { count, databases: databases.map((db) => db.name) };\n }\n\n return {\n count,\n databases: databases.map((db) => ({\n name: db.name,\n createdOn: db.created_on,\n owner: db.owner,\n comment: db.comment ?? null,\n })),\n };\n },\n});\n",
566
- "tools/list-schemas.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { listSchemas } from \"../lib/snowflake-client.ts\";\n\nexport default tool({\n id: \"snowflake-list-schemas\",\n description:\n \"List all schemas in a Snowflake database. Returns schema names, database names, creation dates, and owners.\",\n inputSchema: defineSchema((v) => v.object({\n database: v\n .string()\n .describe(\"The name of the database to list schemas from\"),\n includeDetails: v\n .boolean()\n .default(true)\n .describe(\n \"Include detailed information like creation date, owner, and comments\",\n ),\n }))(),\n async execute({ database, includeDetails }) {\n const schemas = await listSchemas(database);\n const count = schemas.length;\n\n if (!includeDetails) {\n return {\n database,\n count,\n schemas: schemas.map(({ name }) => name),\n };\n }\n\n return {\n database,\n count,\n schemas: schemas.map(({ name, database_name, created_on, owner, comment }) => ({\n name,\n databaseName: database_name,\n createdOn: created_on,\n owner,\n comment: comment ?? null,\n })),\n };\n },\n});\n",
567
- "tools/list-tables.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { listTables } from \"../lib/snowflake-client.ts\";\n\nexport default tool({\n id: \"snowflake-list-tables\",\n description:\n \"List all tables in a Snowflake database schema. Returns table names, types, creation dates, row counts, and sizes.\",\n inputSchema: defineSchema((v) => v.object({\n database: v.string().describe(\"The name of the database containing the schema\"),\n schema: v\n .string()\n .default(\"PUBLIC\")\n .describe(\"The name of the schema to list tables from. Defaults to PUBLIC.\"),\n includeDetails: v\n .boolean()\n .default(true)\n .describe(\n \"Include detailed information like creation date, row count, size, and owner\",\n ),\n }))(),\n async execute({ database, schema, includeDetails }) {\n const tables = await listTables(database, schema);\n\n const base = { database, schema, count: tables.length };\n\n if (!includeDetails) {\n return { ...base, tables: tables.map((t) => t.name) };\n }\n\n return {\n ...base,\n tables: tables.map((t) => ({\n name: t.name,\n databaseName: t.database_name,\n schemaName: t.schema_name,\n kind: t.kind,\n createdOn: t.created_on,\n rowCount: t.row_count ?? null,\n bytes: t.bytes ?? null,\n owner: t.owner,\n comment: t.comment ?? null,\n })),\n };\n },\n});\n",
568
- "tools/run-query.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { getQueryStatus, runQuery } from \"../lib/snowflake-client.ts\";\n\nexport default tool({\n id: \"snowflake-run-query\",\n description:\n \"Execute a SQL query against your Snowflake data warehouse. Supports SELECT, INSERT, UPDATE, DELETE, and other SQL operations.\",\n inputSchema: defineSchema((v) => v.object({\n sql: v\n .string()\n .describe(\n \"The SQL query to execute. Can be SELECT, INSERT, UPDATE, DELETE, or DDL statements.\",\n ),\n database: v\n .string()\n .optional()\n .describe(\n \"The database to use for this query. If not specified, uses the default database.\",\n ),\n schema: v\n .string()\n .optional()\n .describe(\n \"The schema to use for this query. If not specified, uses the default schema.\",\n ),\n timeout: v\n .number()\n .min(1)\n .max(300)\n .default(60)\n .describe(\"Query timeout in seconds (1-300). Default is 60 seconds.\"),\n async: v\n .boolean()\n .default(false)\n .describe(\n \"Execute query asynchronously. If true, returns immediately with a statement handle to check status later.\",\n ),\n }))(),\n async execute({ sql, database, schema, timeout, async: asyncExec }) {\n const result = await runQuery(sql, database, schema, {\n timeout,\n async: asyncExec,\n });\n\n if (asyncExec && result.statementHandle) {\n return {\n status: \"submitted\",\n statementHandle: result.statementHandle,\n message:\n \"Query submitted for async execution. Use the statement handle to check status.\",\n };\n }\n\n return {\n status: \"completed\",\n sql,\n database: database ?? \"default\",\n schema: schema ?? \"PUBLIC\",\n columns: result.columns,\n rowCount: result.rowCount,\n rows: result.rows,\n statementHandle: result.statementHandle,\n };\n },\n});\n\nexport const checkQueryStatus = tool({\n id: \"snowflake-check-query-status\",\n description:\n \"Check the status and retrieve results of an asynchronously executed query.\",\n inputSchema: defineSchema((v) => v.object({\n statementHandle: v\n .string()\n .describe(\"The statement handle returned from an async query execution.\"),\n }))(),\n async execute({ statementHandle }) {\n const status = await getQueryStatus(statementHandle);\n\n if (status.code === \"090001\") {\n return {\n status: \"running\",\n message: status.message,\n statementHandle,\n };\n }\n\n if (status.code && status.code !== \"000000\") {\n return {\n status: \"failed\",\n code: status.code,\n message: status.message,\n statementHandle,\n };\n }\n\n const rowType = status.resultSetMetaData?.rowType ?? [];\n const columns = rowType.map((col) => ({\n name: col.name,\n type: col.type,\n nullable: col.nullable,\n }));\n\n if (!status.data || !status.resultSetMetaData) {\n return {\n status: \"completed\",\n columns,\n rowCount: status.resultSetMetaData?.numRows ?? 0,\n rows: [],\n stats: status.stats,\n statementHandle,\n };\n }\n\n const rows = status.data.map((row) => {\n const obj: Record<string, unknown> = {};\n rowType.forEach((col, index) => {\n obj[col.name] = row[index];\n });\n return obj;\n });\n\n return {\n status: \"completed\",\n columns,\n rowCount: status.resultSetMetaData?.numRows ?? 0,\n rows,\n stats: status.stats,\n statementHandle,\n };\n },\n});\n"
569
- }
570
- },
571
- "integration:stripe": {
572
- "files": {
573
- ".env.example": "# Stripe Integration\n# Get your API keys at https://dashboard.stripe.com/apikeys\n# Use test keys (sk_test_...) for development, live keys (sk_live_...) for production\n\nSTRIPE_SECRET_KEY=sk_test_your_secret_key_here\nSTRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret_here\n",
574
- "app/api/auth/stripe/route.ts": "import { setApiKey } from \"../../../../lib/token-store.ts\";\n\nexport async function POST(request: Request): Promise<Response> {\n let body: unknown;\n\n try {\n body = await request.json();\n } catch {\n return Response.json({ error: \"Internal server error\" }, { status: 500 });\n }\n\n const apiKey = (body as { apiKey?: unknown })?.apiKey;\n\n if (typeof apiKey !== \"string\" || apiKey.length === 0) {\n return Response.json({ error: \"API key is required\" }, { status: 400 });\n }\n\n const isValidPrefix = apiKey.startsWith(\"sk_test_\") || apiKey.startsWith(\"sk_live_\");\n if (!isValidPrefix) {\n return Response.json(\n {\n error:\n \"Invalid Stripe API key format. Key should start with sk_test_ or sk_live_\",\n },\n { status: 400 },\n );\n }\n\n try {\n const response = await fetch(\"https://api.stripe.com/v1/balance\", {\n headers: {\n Authorization: `Bearer ${apiKey}`,\n \"Stripe-Version\": \"2024-12-18.acacia\",\n },\n });\n\n if (!response.ok) {\n const error = await response.json();\n return Response.json(\n {\n error: `Invalid API key: ${error.error?.message ?? \"Authentication failed\"}`,\n },\n { status: 401 },\n );\n }\n } catch {\n return Response.json(\n { error: \"Failed to validate API key with Stripe\" },\n { status: 500 },\n );\n }\n\n setApiKey(apiKey);\n\n return Response.json({\n success: true,\n message: \"Stripe API key validated and stored successfully\",\n });\n}\n\nexport function GET(): Response {\n const apiKey = process.env.STRIPE_SECRET_KEY;\n\n return Response.json({\n authenticated: !!apiKey,\n hasEnvKey: !!apiKey,\n });\n}\n",
575
- "lib/stripe-client.ts": "import { getApiKey } from \"./token-store.ts\";\n\nconst STRIPE_API_VERSION = \"2024-12-18.acacia\";\nconst STRIPE_BASE_URL = \"https://api.stripe.com/v1\";\n\nexport interface StripeCustomer {\n id: string;\n object: \"customer\";\n email: string | null;\n name: string | null;\n description: string | null;\n created: number;\n metadata: Record<string, string>;\n balance: number;\n currency: string | null;\n default_source: string | null;\n}\n\nexport interface StripePaymentIntent {\n id: string;\n object: \"payment_intent\";\n amount: number;\n currency: string;\n status:\n | \"requires_payment_method\"\n | \"requires_confirmation\"\n | \"requires_action\"\n | \"processing\"\n | \"requires_capture\"\n | \"canceled\"\n | \"succeeded\";\n customer: string | null;\n description: string | null;\n created: number;\n metadata: Record<string, string>;\n receipt_email: string | null;\n}\n\nexport interface StripeSubscription {\n id: string;\n object: \"subscription\";\n customer: string;\n status:\n | \"incomplete\"\n | \"incomplete_expired\"\n | \"trialing\"\n | \"active\"\n | \"past_due\"\n | \"canceled\"\n | \"unpaid\"\n | \"paused\";\n current_period_start: number;\n current_period_end: number;\n created: number;\n canceled_at: number | null;\n metadata: Record<string, string>;\n items: {\n data: Array<{\n id: string;\n price: {\n id: string;\n unit_amount: number;\n currency: string;\n recurring: { interval: string; interval_count: number };\n };\n }>;\n };\n}\n\nexport interface StripeBalance {\n object: \"balance\";\n available: Array<{ amount: number; currency: string; source_types?: Record<string, number> }>;\n pending: Array<{ amount: number; currency: string; source_types?: Record<string, number> }>;\n livemode: boolean;\n}\n\nexport interface StripeBalanceTransaction {\n id: string;\n object: \"balance_transaction\";\n amount: number;\n currency: string;\n description: string | null;\n fee: number;\n net: number;\n status: \"available\" | \"pending\";\n type: string;\n created: number;\n}\n\ninterface StripeListResponse<T> {\n object: \"list\";\n data: T[];\n has_more: boolean;\n url: string;\n}\n\ninterface StripeError {\n error: {\n message: string;\n type: string;\n code?: string;\n param?: string;\n };\n}\n\nfunction addCreatedParams(\n params: Record<string, string | number>,\n created?: { gt?: number; gte?: number; lt?: number; lte?: number },\n): void {\n if (!created) return;\n\n if (created.gt != null) params[\"created[gt]\"] = created.gt;\n if (created.gte != null) params[\"created[gte]\"] = created.gte;\n if (created.lt != null) params[\"created[lt]\"] = created.lt;\n if (created.lte != null) params[\"created[lte]\"] = created.lte;\n}\n\nfunction flattenToFormData(formData: URLSearchParams, obj: Record<string, unknown>, prefix = \"\"): void {\n for (const [key, value] of Object.entries(obj)) {\n const formKey = prefix ? `${prefix}[${key}]` : key;\n\n if (value != null && typeof value === \"object\" && !Array.isArray(value)) {\n flattenToFormData(formData, value as Record<string, unknown>, formKey);\n continue;\n }\n\n if (value != null) formData.append(formKey, String(value));\n }\n}\n\nasync function stripeFetch<T>(\n endpoint: string,\n options: RequestInit & { params?: Record<string, string | number | boolean> } = {},\n): Promise<T> {\n const apiKey = getApiKey();\n if (!apiKey) throw new Error(\"Not authenticated with Stripe. Please set STRIPE_SECRET_KEY.\");\n\n const url = new URL(`${STRIPE_BASE_URL}${endpoint}`);\n if (options.params) {\n for (const [key, value] of Object.entries(options.params)) {\n url.searchParams.append(key, String(value));\n }\n }\n\n const headers: Record<string, string> = {\n Authorization: `Bearer ${apiKey}`,\n \"Stripe-Version\": STRIPE_API_VERSION,\n ...(options.headers as Record<string, string> | undefined),\n };\n\n let body = options.body;\n if (options.method === \"POST\" && typeof body === \"string\") {\n try {\n const jsonBody = JSON.parse(body) as Record<string, unknown>;\n const formData = new URLSearchParams();\n flattenToFormData(formData, jsonBody);\n body = formData.toString();\n headers[\"Content-Type\"] = \"application/x-www-form-urlencoded\";\n } catch {\n // If not JSON, use as-is\n }\n }\n\n const response = await fetch(url.toString(), { ...options, headers, body });\n const data = await response.json();\n\n if (response.ok) return data as T;\n\n const error = data as StripeError;\n throw new Error(`Stripe API error: ${response.status} ${error.error?.message ?? response.statusText}`);\n}\n\nfunction buildListParams(options?: { limit?: number }): Record<string, string | number> {\n return { limit: options?.limit ?? 10 };\n}\n\nexport async function listCustomers(options?: {\n limit?: number;\n email?: string;\n created?: { gt?: number; gte?: number; lt?: number; lte?: number };\n}): Promise<StripeCustomer[]> {\n const params = buildListParams(options);\n\n if (options?.email) params.email = options.email;\n addCreatedParams(params, options?.created);\n\n const response = await stripeFetch<StripeListResponse<StripeCustomer>>(\"/customers\", { params });\n return response.data;\n}\n\nexport function getCustomer(customerId: string): Promise<StripeCustomer> {\n return stripeFetch<StripeCustomer>(`/customers/${customerId}`);\n}\n\nexport function createCustomer(data: {\n email?: string;\n name?: string;\n description?: string;\n metadata?: Record<string, string>;\n}): Promise<StripeCustomer> {\n return stripeFetch<StripeCustomer>(\"/customers\", { method: \"POST\", body: JSON.stringify(data) });\n}\n\nexport function updateCustomer(\n customerId: string,\n data: {\n email?: string;\n name?: string;\n description?: string;\n metadata?: Record<string, string>;\n },\n): Promise<StripeCustomer> {\n return stripeFetch<StripeCustomer>(`/customers/${customerId}`, { method: \"POST\", body: JSON.stringify(data) });\n}\n\nexport async function listPaymentIntents(options?: {\n limit?: number;\n customer?: string;\n created?: { gt?: number; gte?: number; lt?: number; lte?: number };\n}): Promise<StripePaymentIntent[]> {\n const params = buildListParams(options);\n\n if (options?.customer) params.customer = options.customer;\n addCreatedParams(params, options?.created);\n\n const response = await stripeFetch<StripeListResponse<StripePaymentIntent>>(\"/payment_intents\", { params });\n return response.data;\n}\n\nexport function getPaymentIntent(paymentIntentId: string): Promise<StripePaymentIntent> {\n return stripeFetch<StripePaymentIntent>(`/payment_intents/${paymentIntentId}`);\n}\n\nexport function createPaymentIntent(data: {\n amount: number;\n currency: string;\n customer?: string;\n description?: string;\n metadata?: Record<string, string>;\n payment_method?: string;\n confirm?: boolean;\n}): Promise<StripePaymentIntent> {\n return stripeFetch<StripePaymentIntent>(\"/payment_intents\", { method: \"POST\", body: JSON.stringify(data) });\n}\n\nexport async function listSubscriptions(options?: {\n limit?: number;\n customer?: string;\n status?:\n | \"incomplete\"\n | \"incomplete_expired\"\n | \"trialing\"\n | \"active\"\n | \"past_due\"\n | \"canceled\"\n | \"unpaid\"\n | \"paused\";\n created?: { gt?: number; gte?: number; lt?: number; lte?: number };\n}): Promise<StripeSubscription[]> {\n const params = buildListParams(options);\n\n if (options?.customer) params.customer = options.customer;\n if (options?.status) params.status = options.status;\n addCreatedParams(params, options?.created);\n\n const response = await stripeFetch<StripeListResponse<StripeSubscription>>(\"/subscriptions\", { params });\n return response.data;\n}\n\nexport function getSubscription(subscriptionId: string): Promise<StripeSubscription> {\n return stripeFetch<StripeSubscription>(`/subscriptions/${subscriptionId}`);\n}\n\nexport function getBalance(): Promise<StripeBalance> {\n return stripeFetch<StripeBalance>(\"/balance\");\n}\n\nexport async function listBalanceTransactions(options?: {\n limit?: number;\n created?: { gt?: number; gte?: number; lt?: number; lte?: number };\n type?: string;\n}): Promise<StripeBalanceTransaction[]> {\n const params = buildListParams(options);\n\n addCreatedParams(params, options?.created);\n if (options?.type) params.type = options.type;\n\n const response = await stripeFetch<StripeListResponse<StripeBalanceTransaction>>(\"/balance_transactions\", { params });\n return response.data;\n}\n\nexport function formatAmount(amount: number, currency: string): string {\n return new Intl.NumberFormat(\"en-US\", { style: \"currency\", currency: currency.toUpperCase() }).format(amount / 100);\n}\n\nexport function formatDate(timestamp: number): string {\n return new Date(timestamp * 1000).toISOString();\n}\n",
576
- "tools/get-balance.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { formatAmount, getBalance } from \"../lib/stripe-client.ts\";\n\ntype BalanceItem = {\n amount: number;\n currency: string;\n source_types: unknown;\n};\n\nfunction mapBalanceItem(bal: BalanceItem): {\n amount: string;\n amountRaw: number;\n currency: string;\n sourceTypes: unknown;\n} {\n return {\n amount: formatAmount(bal.amount, bal.currency),\n amountRaw: bal.amount,\n currency: bal.currency,\n sourceTypes: bal.source_types,\n };\n}\n\nexport default tool({\n id: \"stripe-get-balance\",\n description: \"Retrieve the current Stripe account balance including available and pending funds.\",\n inputSchema: defineSchema((v) => v.object({}))(),\n async execute() {\n const balance = await getBalance();\n\n return {\n livemode: balance.livemode,\n available: balance.available.map(mapBalanceItem),\n pending: balance.pending.map(mapBalanceItem),\n };\n },\n});\n",
577
- "tools/get-customer.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { formatDate, getCustomer } from \"../lib/stripe-client.ts\";\n\nexport default tool({\n id: \"stripe-get-customer\",\n description: \"Retrieve detailed information about a specific Stripe customer by their ID.\",\n inputSchema: defineSchema((v) => v.object({\n customerId: v.string().describe(\"The Stripe customer ID (starts with cus_)\"),\n }))(),\n async execute({ customerId }) {\n const customer = await getCustomer(customerId);\n\n return {\n id: customer.id,\n email: customer.email,\n name: customer.name,\n description: customer.description,\n created: formatDate(customer.created),\n balance: customer.balance,\n currency: customer.currency,\n defaultSource: customer.default_source,\n metadata: customer.metadata,\n };\n },\n});\n",
578
- "tools/list-customers.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { formatDate, listCustomers } from \"../lib/stripe-client.ts\";\n\nexport default tool({\n id: \"stripe-list-customers\",\n description: \"List Stripe customers. Supports filtering by email and creation date range.\",\n inputSchema: defineSchema((v) => v.object({\n limit: v\n .number()\n .min(1)\n .max(100)\n .default(10)\n .describe(\"Maximum number of customers to retrieve\"),\n email: v.string().email().optional().describe(\"Filter by customer email address\"),\n createdAfter: v.number().optional().describe(\"Filter customers created after this Unix timestamp\"),\n createdBefore: v\n .number()\n .optional()\n .describe(\"Filter customers created before this Unix timestamp\"),\n }))(),\n async execute({ limit, email, createdAfter, createdBefore }) {\n const created =\n createdAfter != null || createdBefore != null\n ? { gte: createdAfter, lte: createdBefore }\n : undefined;\n\n const customers = await listCustomers({ limit, email, created });\n\n return customers.map((customer) => ({\n id: customer.id,\n email: customer.email,\n name: customer.name,\n description: customer.description,\n created: formatDate(customer.created),\n balance: customer.balance,\n currency: customer.currency,\n metadata: customer.metadata,\n }));\n },\n});\n",
579
- "tools/list-payments.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { formatAmount, formatDate, listPaymentIntents } from \"../lib/stripe-client.ts\";\n\nexport default tool({\n id: \"stripe-list-payments\",\n description: \"List Stripe payment intents. Supports filtering by customer and creation date range.\",\n inputSchema: defineSchema((v) => v.object({\n limit: v\n .number()\n .min(1)\n .max(100)\n .default(10)\n .describe(\"Maximum number of payment intents to retrieve\"),\n customerId: v.string().optional().describe(\"Filter by customer ID (starts with cus_)\"),\n createdAfter: v.number().optional().describe(\"Filter payments created after this Unix timestamp\"),\n createdBefore: v.number().optional().describe(\"Filter payments created before this Unix timestamp\"),\n }))(),\n async execute({ limit, customerId, createdAfter, createdBefore }) {\n const created =\n createdAfter || createdBefore ? { gte: createdAfter, lte: createdBefore } : undefined;\n\n const payments = await listPaymentIntents({\n limit,\n customer: customerId,\n created,\n });\n\n return payments.map((payment) => ({\n id: payment.id,\n amount: formatAmount(payment.amount, payment.currency),\n amountRaw: payment.amount,\n currency: payment.currency,\n status: payment.status,\n customer: payment.customer,\n description: payment.description,\n receiptEmail: payment.receipt_email,\n created: formatDate(payment.created),\n metadata: payment.metadata,\n }));\n },\n});\n",
580
- "tools/list-subscriptions.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { formatAmount, formatDate, listSubscriptions } from \"../lib/stripe-client.ts\";\n\nexport default tool({\n id: \"stripe-list-subscriptions\",\n description:\n \"List Stripe subscriptions. Supports filtering by customer, status, and creation date range.\",\n inputSchema: defineSchema((v) => v.object({\n limit: v\n .number()\n .min(1)\n .max(100)\n .default(10)\n .describe(\"Maximum number of subscriptions to retrieve\"),\n customerId: v.string().optional().describe(\"Filter by customer ID (starts with cus_)\"),\n status: v\n .enum([\n \"incomplete\",\n \"incomplete_expired\",\n \"trialing\",\n \"active\",\n \"past_due\",\n \"canceled\",\n \"unpaid\",\n \"paused\",\n ])\n .optional()\n .describe(\"Filter by subscription status\"),\n createdAfter: v\n .number()\n .optional()\n .describe(\"Filter subscriptions created after this Unix timestamp\"),\n createdBefore: v\n .number()\n .optional()\n .describe(\"Filter subscriptions created before this Unix timestamp\"),\n }))(),\n async execute({ limit, customerId, status, createdAfter, createdBefore }) {\n const created =\n createdAfter || createdBefore ? { gte: createdAfter, lte: createdBefore } : undefined;\n\n const subscriptions = await listSubscriptions({\n limit,\n customer: customerId,\n status,\n created,\n });\n\n return subscriptions.map((subscription) => ({\n id: subscription.id,\n customer: subscription.customer,\n status: subscription.status,\n currentPeriodStart: formatDate(subscription.current_period_start),\n currentPeriodEnd: formatDate(subscription.current_period_end),\n created: formatDate(subscription.created),\n canceledAt: subscription.canceled_at ? formatDate(subscription.canceled_at) : null,\n items: subscription.items.data.map((item) => ({\n id: item.id,\n priceId: item.price.id,\n amount: formatAmount(item.price.unit_amount, item.price.currency),\n amountRaw: item.price.unit_amount,\n currency: item.price.currency,\n interval: item.price.recurring.interval,\n intervalCount: item.price.recurring.interval_count,\n })),\n metadata: subscription.metadata,\n }));\n },\n});\n"
581
- }
582
- },
583
- "integration:supabase": {
584
- "files": {
585
- ".env.example": "# Supabase Integration\n# Get your API keys from https://supabase.com/dashboard/project/_/settings/api\n\nSUPABASE_URL=https://xxxxx.supabase.co\nSUPABASE_ANON_KEY=your_anon_key_here\nSUPABASE_SERVICE_KEY=your_service_role_key_here\n",
586
- "app/api/auth/supabase/route.ts": "import { clearConfig, isConfigured, setSupabaseConfig } from \"../../../../lib/token-store.ts\";\n\nexport async function POST(request: Request): Promise<Response> {\n try {\n const { url, anonKey, serviceKey } = await request.json();\n\n if (!url || !anonKey || !serviceKey) {\n return Response.json(\n { error: \"Missing required fields: url, anonKey, serviceKey\" },\n { status: 400 },\n );\n }\n\n try {\n new URL(url);\n } catch {\n return Response.json({ error: \"Invalid Supabase URL format\" }, { status: 400 });\n }\n\n setSupabaseConfig({ url, anonKey, serviceKey });\n\n return Response.json({\n success: true,\n message: \"Supabase configuration saved successfully\",\n });\n } catch (error) {\n console.error(\"Supabase config error:\", error);\n return Response.json({ error: \"Failed to configure Supabase\" }, { status: 500 });\n }\n}\n\nexport function GET(): Response {\n try {\n const configured = isConfigured();\n\n return Response.json({\n configured,\n message: configured ? \"Supabase is configured\" : \"Supabase is not configured\",\n });\n } catch (error) {\n console.error(\"Supabase status check error:\", error);\n return Response.json({ error: \"Failed to check Supabase status\" }, { status: 500 });\n }\n}\n\nexport async function DELETE(): Promise<Response> {\n try {\n clearConfig();\n\n return Response.json({\n success: true,\n message: \"Supabase configuration cleared\",\n });\n } catch (error) {\n console.error(\"Supabase clear config error:\", error);\n return Response.json(\n { error: \"Failed to clear Supabase configuration\" },\n { status: 500 },\n );\n }\n}\n",
587
- "lib/supabase-client.ts": "import { getAnonKey, getServiceKey, getSupabaseUrl } from \"./token-store.ts\";\n\ninterface TableInfo {\n table_name: string;\n table_schema: string;\n table_type: string;\n}\n\ninterface ColumnInfo {\n column_name: string;\n data_type: string;\n is_nullable: string;\n column_default: string | null;\n}\n\ninterface QueryOptions {\n select?: string;\n filter?: Record<string, unknown>;\n order?: { column: string; ascending?: boolean };\n limit?: number;\n offset?: number;\n}\n\ninterface SupabaseError extends Error {\n code?: string;\n details?: string;\n hint?: string;\n}\n\nasync function supabaseFetch<T>(\n endpoint: string,\n options: RequestInit = {},\n useServiceRole = true,\n): Promise<T> {\n const url = getSupabaseUrl();\n const apiKey = useServiceRole ? getServiceKey() : getAnonKey();\n\n const response = await fetch(`${url}/rest/v1${endpoint}`, {\n ...options,\n headers: {\n apikey: apiKey,\n Authorization: `Bearer ${apiKey}`,\n \"Content-Type\": \"application/json\",\n Prefer: \"return=representation\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const payload = (await response.json().catch(() => ({}))) as Partial<SupabaseError>;\n const message =\n payload.message ?? `Supabase API error: ${response.status} ${response.statusText}`;\n\n const err = new Error(message) as SupabaseError;\n err.code = payload.code;\n err.details = payload.details;\n err.hint = payload.hint;\n throw err;\n }\n\n const text = await response.text();\n return (text ? JSON.parse(text) : null) as T;\n}\n\nfunction toEqFilterValue(value: unknown): string | null {\n if (value === null) return \"is.null\";\n if (typeof value === \"string\" || typeof value === \"number\" || typeof value === \"boolean\") {\n return `eq.${value}`;\n }\n return null;\n}\n\nfunction buildFilterParams(filter: Record<string, unknown>): URLSearchParams {\n const params = new URLSearchParams();\n for (const [key, value] of Object.entries(filter)) {\n const filterValue = toEqFilterValue(value);\n if (filterValue !== null) params.append(key, filterValue);\n }\n return params;\n}\n\n/**\n * List all tables in the public schema\n */\nexport async function listTables(): Promise<TableInfo[]> {\n try {\n const tables = await supabaseFetch<TableInfo[]>(\n \"/rpc/get_tables\",\n {\n method: \"POST\",\n body: JSON.stringify({}),\n },\n );\n return tables ?? [];\n } catch {\n const query =\n \"?select=table_name,table_schema,table_type&table_schema=eq.public&table_type=eq.BASE TABLE\";\n const tables = await supabaseFetch<TableInfo[]>(`/information_schema.tables${query}`);\n return tables ?? [];\n }\n}\n\n/**\n * Get columns for a specific table\n */\nexport async function getTableColumns(tableName: string): Promise<ColumnInfo[]> {\n const query =\n `?select=column_name,data_type,is_nullable,column_default&table_name=eq.${tableName}&table_schema=eq.public`;\n const columns = await supabaseFetch<ColumnInfo[]>(`/information_schema.columns${query}`);\n return columns ?? [];\n}\n\n/**\n * Query a table with filters, sorting, and pagination\n */\nexport async function queryTable<T = Record<string, unknown>>(\n tableName: string,\n options: QueryOptions = {},\n): Promise<T[]> {\n const params = new URLSearchParams();\n params.append(\"select\", options.select ?? \"*\");\n\n if (options.filter) {\n for (const [key, value] of buildFilterParams(options.filter).entries()) {\n params.append(key, value);\n }\n }\n\n if (options.order) {\n const direction = options.order.ascending === false ? \".desc\" : \".asc\";\n params.append(\"order\", `${options.order.column}${direction}`);\n }\n\n if (options.limit) params.append(\"limit\", options.limit.toString());\n if (options.offset) params.append(\"offset\", options.offset.toString());\n\n const results = await supabaseFetch<T[]>(`/${tableName}?${params.toString()}`);\n return results ?? [];\n}\n\n/**\n * Insert a new row into a table\n */\nexport async function insertRow<T = Record<string, unknown>>(\n tableName: string,\n data: Record<string, unknown>,\n): Promise<T> {\n const result = await supabaseFetch<T[]>(\n `/${tableName}`,\n {\n method: \"POST\",\n body: JSON.stringify(data),\n },\n );\n\n if (!result?.length) throw new Error(\"Insert operation did not return data\");\n return result[0];\n}\n\n/**\n * Update a row in a table by ID\n */\nexport async function updateRow<T = Record<string, unknown>>(\n tableName: string,\n id: string | number,\n data: Record<string, unknown>,\n): Promise<T> {\n const result = await supabaseFetch<T[]>(\n `/${tableName}?id=eq.${id}`,\n {\n method: \"PATCH\",\n body: JSON.stringify(data),\n },\n );\n\n if (!result?.length) throw new Error(`No row found with id ${id}`);\n return result[0];\n}\n\n/**\n * Update rows in a table with custom filter\n */\nexport async function updateRows<T = Record<string, unknown>>(\n tableName: string,\n filter: Record<string, unknown>,\n data: Record<string, unknown>,\n): Promise<T[]> {\n const params = buildFilterParams(filter);\n\n const result = await supabaseFetch<T[]>(\n `/${tableName}?${params.toString()}`,\n {\n method: \"PATCH\",\n body: JSON.stringify(data),\n },\n );\n\n return result ?? [];\n}\n\n/**\n * Delete a row from a table by ID\n */\nexport async function deleteRow<T = Record<string, unknown>>(\n tableName: string,\n id: string | number,\n): Promise<T> {\n const result = await supabaseFetch<T[]>(\n `/${tableName}?id=eq.${id}`,\n {\n method: \"DELETE\",\n },\n );\n\n if (!result?.length) throw new Error(`No row found with id ${id}`);\n return result[0];\n}\n\n/**\n * Delete rows from a table with custom filter\n */\nexport async function deleteRows<T = Record<string, unknown>>(\n tableName: string,\n filter: Record<string, unknown>,\n): Promise<T[]> {\n const params = buildFilterParams(filter);\n\n const result = await supabaseFetch<T[]>(\n `/${tableName}?${params.toString()}`,\n {\n method: \"DELETE\",\n },\n );\n\n return result ?? [];\n}\n\n/**\n * Execute a raw SQL query using RPC\n * Note: This requires a stored procedure to be created in your Supabase database\n */\nexport function runRawQuery<T = unknown>(query: string): Promise<T> {\n return supabaseFetch<T>(\n \"/rpc/execute_sql\",\n {\n method: \"POST\",\n body: JSON.stringify({ query }),\n },\n );\n}\n\n/**\n * Get client instance (for use with @supabase/supabase-js if needed)\n */\nexport function getClient(): { url: string; anonKey: string; serviceKey: string } {\n return {\n url: getSupabaseUrl(),\n anonKey: getAnonKey(),\n serviceKey: getServiceKey(),\n };\n}\n",
588
- "tools/delete-row.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { deleteRow, deleteRows } from \"../lib/supabase-client.ts\";\n\nexport default tool({\n id: \"supabase-delete-row\",\n description:\n \"Delete rows from a Supabase table. Can delete by ID or by custom filter conditions. Returns the deleted rows.\",\n inputSchema: defineSchema((v) => v.object({\n tableName: v.string().describe(\"The name of the table to delete from\"),\n id: v\n .union([v.string(), v.number()])\n .optional()\n .describe(\"The ID of the row to delete (if deleting a single row by ID)\"),\n filter: v\n .record(v.string(), v.unknown())\n .optional()\n .describe('Filter conditions to match rows to delete (e.g., {\"status\": \"archived\"})'),\n confirm: v\n .boolean()\n .default(false)\n .describe(\"Confirm deletion (must be true to proceed with delete operation)\"),\n }))(),\n async execute({ tableName, id, filter, confirm }) {\n if (!confirm) {\n return {\n success: false,\n tableName,\n error: \"Deletion not confirmed\",\n message: \"You must set confirm: true to delete rows. This is a safety measure.\",\n };\n }\n\n if (id == null && filter == null) {\n return {\n success: false,\n tableName,\n error: \"Either id or filter must be provided\",\n message: \"You must specify either an id or filter conditions to delete rows\",\n };\n }\n\n try {\n if (id != null) {\n const result = await deleteRow(tableName, id);\n return {\n success: true,\n tableName,\n rowsDeleted: 1,\n row: result,\n message: `Successfully deleted row with id ${id} from ${tableName}`,\n };\n }\n\n const results = await deleteRows(tableName, filter as Record<string, unknown>);\n return {\n success: true,\n tableName,\n rowsDeleted: results.length,\n rows: results,\n message: `Successfully deleted ${results.length} row(s) from ${tableName}`,\n };\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : \"Unknown error\";\n\n return {\n success: false,\n tableName,\n error: errorMessage,\n message: `Failed to delete row(s) from ${tableName}: ${errorMessage}`,\n };\n }\n },\n});\n",
589
- "tools/insert-row.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { insertRow } from \"../lib/supabase-client.ts\";\n\nexport default tool({\n id: \"supabase-insert-row\",\n description: \"Insert a new row into a Supabase table. Returns the created row.\",\n inputSchema: defineSchema((v) => v.object({\n tableName: v.string().describe(\"The name of the table to insert into\"),\n data: v\n .record(v.string(), v.unknown())\n .describe(\"The data to insert as key-value pairs matching the table schema\"),\n }))(),\n async execute({ tableName, data }) {\n try {\n const row = await insertRow(tableName, data);\n\n return {\n success: true,\n tableName,\n row,\n message: `Successfully inserted row into ${tableName}`,\n };\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : \"Unknown error\";\n\n return {\n success: false,\n tableName,\n error: errorMessage,\n message: `Failed to insert row into ${tableName}: ${errorMessage}`,\n };\n }\n },\n});\n",
590
- "tools/list-tables.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { getTableColumns, listTables } from \"../lib/supabase-client.ts\";\n\nexport default tool({\n id: \"supabase-list-tables\",\n description: \"List all tables in your Supabase database with their schema information.\",\n inputSchema: defineSchema((v) => v.object({\n includeColumns: v\n .boolean()\n .default(false)\n .describe(\"Include column information for each table\"),\n }))(),\n async execute({ includeColumns }): Promise<{\n count: number;\n tables: Array<{\n name: string;\n schema: string;\n type: string;\n columns?: Array<{\n name: string;\n type: string;\n nullable: boolean;\n default: unknown;\n }>;\n error?: string;\n }>;\n }> {\n const tables = await listTables();\n\n if (!includeColumns) {\n const baseTables = tables.map((t) => ({\n name: t.table_name,\n schema: t.table_schema,\n type: t.table_type,\n }));\n\n return { count: baseTables.length, tables: baseTables };\n }\n\n const tablesWithColumns = await Promise.all(\n tables.map(async (table) => {\n const base = {\n name: table.table_name,\n schema: table.table_schema,\n type: table.table_type,\n };\n\n try {\n const columns = await getTableColumns(table.table_name);\n\n return {\n ...base,\n columns: columns.map((c) => ({\n name: c.column_name,\n type: c.data_type,\n nullable: c.is_nullable === \"YES\",\n default: c.column_default,\n })),\n };\n } catch (error) {\n return {\n ...base,\n columns: [],\n error: error instanceof Error ? error.message : \"Failed to fetch columns\",\n };\n }\n }),\n );\n\n return { count: tablesWithColumns.length, tables: tablesWithColumns };\n },\n});\n",
591
- "tools/query-table.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { queryTable } from \"../lib/supabase-client.ts\";\n\nexport default tool({\n id: \"supabase-query-table\",\n description:\n \"Query a table in your Supabase database with optional filters, sorting, and pagination.\",\n inputSchema: defineSchema((v) => v.object({\n tableName: v.string().describe(\"The name of the table to query\"),\n select: v\n .string()\n .optional()\n .describe(\n 'Columns to select (comma-separated, e.g., \"id,name,email\"). Default is all columns (*)',\n ),\n filter: v\n .record(v.string(), v.unknown())\n .optional()\n .describe(\n 'Filter conditions as key-value pairs (e.g., {\"status\": \"active\", \"age\": 25})',\n ),\n orderBy: v.string().optional().describe(\"Column to order by\"),\n ascending: v\n .boolean()\n .default(true)\n .describe(\"Sort in ascending order (true) or descending (false)\"),\n limit: v\n .number()\n .min(1)\n .max(1000)\n .default(100)\n .describe(\"Maximum number of rows to return (1-1000)\"),\n offset: v\n .number()\n .min(0)\n .default(0)\n .describe(\"Number of rows to skip (for pagination)\"),\n }))(),\n async execute({\n tableName,\n select,\n filter,\n orderBy,\n ascending,\n limit,\n offset,\n }): Promise<{\n tableName: string;\n count: number;\n rows: unknown[];\n pagination: { limit: number; offset: number; hasMore: boolean };\n }> {\n const rows = await queryTable(tableName, {\n select,\n filter,\n order: orderBy ? { column: orderBy, ascending } : undefined,\n limit,\n offset,\n });\n\n return {\n tableName,\n count: rows.length,\n rows,\n pagination: {\n limit,\n offset,\n hasMore: rows.length === limit,\n },\n };\n },\n});\n",
592
- "tools/update-row.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { updateRow, updateRows } from \"../lib/supabase-client.ts\";\n\nexport default tool({\n id: \"supabase-update-row\",\n description: \"Update rows in a Supabase table. Can update by ID or by custom filter conditions.\",\n inputSchema: defineSchema((v) => v.object({\n tableName: v.string().describe(\"The name of the table to update\"),\n id: v\n .union([v.string(), v.number()])\n .optional()\n .describe(\"The ID of the row to update (if updating a single row by ID)\"),\n filter: v\n .record(v.string(), v.unknown())\n .optional()\n .describe('Filter conditions to match rows to update (e.g., {\"status\": \"pending\"})'),\n data: v.record(v.string(), v.unknown()).describe(\"The data to update as key-value pairs\"),\n }))(),\n async execute({ tableName, id, filter, data }) {\n if (id == null && filter == null) {\n return {\n success: false,\n tableName,\n error: \"Either id or filter must be provided\",\n message: \"You must specify either an id or filter conditions to update rows\",\n };\n }\n\n try {\n if (id != null) {\n const row = await updateRow(tableName, id, data);\n return {\n success: true,\n tableName,\n rowsUpdated: 1,\n row,\n message: `Successfully updated row with id ${id} in ${tableName}`,\n };\n }\n\n const rows = await updateRows(tableName, filter, data);\n return {\n success: true,\n tableName,\n rowsUpdated: rows.length,\n rows,\n message: `Successfully updated ${rows.length} row(s) in ${tableName}`,\n };\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : \"Unknown error\";\n\n return {\n success: false,\n tableName,\n error: errorMessage,\n message: `Failed to update row(s) in ${tableName}: ${errorMessage}`,\n };\n }\n },\n});\n"
593
- }
594
- },
595
- "integration:teams": {
596
- "files": {
597
- "app/api/auth/teams/callback/route.ts": "import { createOAuthCallbackHandler, teamsConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\n\nexport const GET = createOAuthCallbackHandler(teamsConfig, { tokenStore });\n",
598
- "app/api/auth/teams/route.ts": "import { createOAuthInitHandler, teamsConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../lib/token-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../lib/user-id.ts\";\n\nexport const GET = createOAuthInitHandler(teamsConfig, {\n tokenStore,\n getUserId: requireUserIdFromRequest,\n});\n",
599
- "lib/teams-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst GRAPH_API_BASE = \"https://graph.microsoft.com/v1.0\";\n\ninterface GraphResponse<T> {\n \"@odata.context\"?: string;\n \"@odata.nextLink\"?: string;\n value?: T[];\n}\n\nexport interface TeamsChat {\n id: string;\n topic: string | null;\n createdDateTime: string;\n lastUpdatedDateTime: string;\n chatType: \"oneOnOne\" | \"group\" | \"meeting\";\n webUrl?: string;\n members?: ChatMember[];\n}\n\nexport interface ChatMember {\n \"@odata.type\": string;\n id: string;\n displayName?: string;\n userId?: string;\n email?: string;\n}\n\nexport interface ChatMessage {\n id: string;\n messageType: \"message\" | \"chatEvent\" | \"typing\";\n createdDateTime: string;\n lastModifiedDateTime?: string;\n deletedDateTime?: string;\n subject?: string | null;\n summary?: string | null;\n importance: \"normal\" | \"high\" | \"urgent\";\n locale?: string;\n from: {\n user?: {\n id: string;\n displayName?: string;\n userIdentityType?: string;\n };\n };\n body: {\n contentType: \"text\" | \"html\";\n content: string;\n };\n attachments?: Array<{\n id: string;\n contentType: string;\n contentUrl?: string;\n content?: string;\n name?: string;\n }>;\n mentions?: Array<{\n id: number;\n mentionText: string;\n mentioned: {\n user: {\n id: string;\n displayName?: string;\n };\n };\n }>;\n reactions?: Array<{\n reactionType: string;\n createdDateTime: string;\n user: {\n id: string;\n displayName?: string;\n };\n }>;\n}\n\nexport interface Team {\n id: string;\n displayName: string;\n description?: string;\n createdDateTime?: string;\n webUrl?: string;\n isArchived?: boolean;\n visibility?: \"private\" | \"public\";\n}\n\nexport interface Channel {\n id: string;\n displayName: string;\n description?: string;\n email?: string;\n webUrl?: string;\n membershipType?: \"standard\" | \"private\" | \"shared\";\n createdDateTime?: string;\n}\n\nfunction buildEndpoint(path: string, params?: URLSearchParams): string {\n const queryString = params?.toString();\n return queryString ? `${path}?${queryString}` : path;\n}\n\nasync function graphFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with Microsoft Teams. Please connect your account.\");\n }\n\n const url = endpoint.startsWith(\"http\") ? endpoint : `${GRAPH_API_BASE}${endpoint}`;\n\n const response = await fetch(url, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}));\n throw new Error(\n `Microsoft Graph API error: ${response.status} ${error?.error?.message ?? response.statusText}`,\n );\n }\n\n return response.json();\n}\n\nexport async function listChats(options?: { limit?: number; expand?: string[] }): Promise<TeamsChat[]> {\n const params = new URLSearchParams();\n if (options?.limit) params.set(\"$top\", options.limit.toString());\n if (options?.expand?.length) params.set(\"$expand\", options.expand.join(\",\"));\n\n const response = await graphFetch<GraphResponse<TeamsChat>>(buildEndpoint(\"/me/chats\", params));\n return response.value ?? [];\n}\n\nexport async function getChatMessages(\n chatId: string,\n options?: { limit?: number; orderBy?: string },\n): Promise<ChatMessage[]> {\n const params = new URLSearchParams();\n if (options?.limit) params.set(\"$top\", options.limit.toString());\n params.set(\"$orderby\", options?.orderBy ?? \"createdDateTime desc\");\n\n const response = await graphFetch<GraphResponse<ChatMessage>>(\n buildEndpoint(`/me/chats/${chatId}/messages`, params),\n );\n return response.value ?? [];\n}\n\nexport function sendChatMessage(\n chatId: string,\n content: string,\n contentType: \"text\" | \"html\" = \"text\",\n): Promise<ChatMessage> {\n return graphFetch<ChatMessage>(`/me/chats/${chatId}/messages`, {\n method: \"POST\",\n body: JSON.stringify({ body: { contentType, content } }),\n });\n}\n\nexport async function listTeams(options?: { limit?: number }): Promise<Team[]> {\n const params = new URLSearchParams();\n if (options?.limit) params.set(\"$top\", options.limit.toString());\n\n const response = await graphFetch<GraphResponse<Team>>(buildEndpoint(\"/me/joinedTeams\", params));\n return response.value ?? [];\n}\n\nexport async function listChannels(teamId: string, options?: { limit?: number }): Promise<Channel[]> {\n const params = new URLSearchParams();\n if (options?.limit) params.set(\"$top\", options.limit.toString());\n\n const response = await graphFetch<GraphResponse<Channel>>(\n buildEndpoint(`/teams/${teamId}/channels`, params),\n );\n return response.value ?? [];\n}\n\nexport function sendChannelMessage(\n teamId: string,\n channelId: string,\n content: string,\n contentType: \"text\" | \"html\" = \"text\",\n subject?: string,\n): Promise<ChatMessage> {\n const body: Record<string, unknown> = { body: { contentType, content } };\n if (subject) body.subject = subject;\n\n return graphFetch<ChatMessage>(`/teams/${teamId}/channels/${channelId}/messages`, {\n method: \"POST\",\n body: JSON.stringify(body),\n });\n}\n\nexport async function getChannelMessages(\n teamId: string,\n channelId: string,\n options?: { limit?: number; orderBy?: string },\n): Promise<ChatMessage[]> {\n const params = new URLSearchParams();\n if (options?.limit) params.set(\"$top\", options.limit.toString());\n params.set(\"$orderby\", options?.orderBy ?? \"createdDateTime desc\");\n\n const response = await graphFetch<GraphResponse<ChatMessage>>(\n buildEndpoint(`/teams/${teamId}/channels/${channelId}/messages`, params),\n );\n return response.value ?? [];\n}\n\nexport function getCurrentUser(): Promise<{\n id: string;\n displayName: string;\n mail?: string;\n userPrincipalName?: string;\n}> {\n return graphFetch(\"/me\");\n}\n\nexport function getChatDisplayName(chat: TeamsChat): string {\n if (chat.topic) return chat.topic;\n\n const memberNames = chat.members?.flatMap((m) => (m.displayName ? [m.displayName] : [])).join(\", \");\n if (memberNames) return memberNames;\n\n return chat.chatType === \"oneOnOne\" ? \"Direct Chat\" : \"Group Chat\";\n}\n\nexport function getPlainTextContent(message: ChatMessage): string {\n if (message.body.contentType === \"text\") return message.body.content;\n\n return message.body.content\n .replace(/<[^>]*>/g, \"\")\n .replace(/&nbsp;/g, \" \")\n .replace(/&amp;/g, \"&\")\n .replace(/&lt;/g, \"<\")\n .replace(/&gt;/g, \">\")\n .replace(/&quot;/g, '\"')\n .trim();\n}\n",
600
- "tools/get-messages.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { getChatMessages, getPlainTextContent } from \"../lib/teams-client.ts\";\n\nexport default tool({\n id: \"teams-get-messages\",\n description:\n \"Get messages from a specific Microsoft Teams chat. Returns message content, sender information, and timestamps. Use list-chats first to get chat IDs.\",\n inputSchema: defineSchema((v) => v.object({\n chatId: v.string().describe(\"The ID of the chat to get messages from\"),\n limit: v\n .number()\n .min(1)\n .max(50)\n .default(20)\n .describe(\"Maximum number of messages to return (1-50)\"),\n includeHtml: v\n .boolean()\n .default(false)\n .describe(\"Include HTML formatted content in addition to plain text\"),\n }))(),\n async execute({ chatId, limit, includeHtml }) {\n const messages = await getChatMessages(chatId, {\n limit,\n orderBy: \"createdDateTime desc\",\n });\n\n return messages\n .filter((msg) => msg.messageType === \"message\")\n .map((msg) => {\n const attachments = msg.attachments ?? [];\n const mentions = msg.mentions ?? [];\n const reactions = msg.reactions ?? [];\n\n return {\n id: msg.id,\n content: getPlainTextContent(msg),\n htmlContent: includeHtml ? msg.body.content : undefined,\n contentType: msg.body.contentType,\n sender: {\n id: msg.from.user?.id,\n displayName: msg.from.user?.displayName,\n },\n createdAt: msg.createdDateTime,\n lastModified: msg.lastModifiedDateTime,\n importance: msg.importance,\n subject: msg.subject,\n hasAttachments: attachments.length > 0,\n attachmentCount: attachments.length,\n attachments: attachments.map((att) => ({\n id: att.id,\n name: att.name,\n contentType: att.contentType,\n contentUrl: att.contentUrl,\n })),\n mentions: mentions.map((mention) => ({\n text: mention.mentionText,\n userId: mention.mentioned.user.id,\n displayName: mention.mentioned.user.displayName,\n })),\n reactionCount: reactions.length,\n };\n });\n },\n});\n",
601
- "tools/list-channels.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { listChannels } from \"../lib/teams-client.ts\";\n\nexport default tool({\n id: \"teams-list-channels\",\n description:\n \"List all channels in a specific Microsoft Team. Use list-teams first to get team IDs. Returns channel IDs, names, descriptions, and types.\",\n inputSchema: defineSchema((v) => v.object({\n teamId: v.string().describe(\"The ID of the team to list channels from\"),\n limit: v\n .number()\n .min(1)\n .max(50)\n .default(25)\n .describe(\"Maximum number of channels to return (1-50)\"),\n }))(),\n async execute({ teamId, limit }) {\n const channels = await listChannels(teamId, { limit });\n\n return channels.map((channel) => ({\n id: channel.id,\n name: channel.displayName,\n description: channel.description,\n email: channel.email,\n webUrl: channel.webUrl,\n membershipType: channel.membershipType,\n createdAt: channel.createdDateTime,\n }));\n },\n});\n",
602
- "tools/list-chats.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { getChatDisplayName, listChats } from \"../lib/teams-client.ts\";\n\nexport default tool({\n id: \"teams-list-chats\",\n description:\n \"List recent Microsoft Teams chats for the authenticated user. Returns chat IDs, names, types, and last updated timestamps.\",\n inputSchema: defineSchema((v) => v.object({\n limit: v\n .number()\n .min(1)\n .max(50)\n .default(20)\n .describe(\"Maximum number of chats to return (1-50)\"),\n expandMembers: v\n .boolean()\n .default(false)\n .describe(\"Include chat member information\"),\n }))(),\n async execute({ limit, expandMembers }) {\n const chats = await listChats({\n limit,\n expand: expandMembers ? [\"members\"] : undefined,\n });\n\n return chats.map((chat) => {\n const members = expandMembers\n ? chat.members?.map(({ id, displayName, email }) => ({\n id,\n displayName,\n email,\n }))\n : undefined;\n\n return {\n id: chat.id,\n name: getChatDisplayName(chat),\n type: chat.chatType,\n topic: chat.topic,\n lastUpdated: chat.lastUpdatedDateTime,\n created: chat.createdDateTime,\n webUrl: chat.webUrl,\n memberCount: chat.members?.length,\n members,\n };\n });\n },\n});\n",
603
- "tools/list-teams.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { listTeams } from \"../lib/teams-client.ts\";\n\nexport default tool({\n id: \"teams-list-teams\",\n description:\n \"List all Microsoft Teams that the authenticated user is a member of. Returns team IDs, names, descriptions, and metadata.\",\n inputSchema: defineSchema((v) => v.object({\n limit: v\n .number()\n .min(1)\n .max(50)\n .default(25)\n .describe(\"Maximum number of teams to return (1-50)\"),\n }))(),\n async execute({ limit }) {\n const teams = await listTeams({ limit });\n\n return teams.map((team) => ({\n id: team.id,\n name: team.displayName,\n description: team.description,\n visibility: team.visibility,\n isArchived: team.isArchived,\n createdAt: team.createdDateTime,\n webUrl: team.webUrl,\n }));\n },\n});\n",
604
- "tools/send-message.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { sendChannelMessage, sendChatMessage } from \"../lib/teams-client.ts\";\n\nexport default tool({\n id: \"teams-send-message\",\n description:\n \"Send a message to a Microsoft Teams chat or channel. For chats, use the chatId. For channels, use both teamId and channelId.\",\n inputSchema: defineSchema((v) => v\n .object({\n chatId: v\n .string()\n .optional()\n .describe(\"The ID of the chat to send the message to (use this for direct/group chats)\"),\n teamId: v\n .string()\n .optional()\n .describe(\"The ID of the team (use with channelId for channel messages)\"),\n channelId: v\n .string()\n .optional()\n .describe(\"The ID of the channel (use with teamId for channel messages)\"),\n content: v.string().min(1).describe(\"The message content to send\"),\n contentType: v.enum([\"text\", \"html\"]).default(\"text\").describe(\"Content format: text or html\"),\n subject: v.string().optional().describe(\"Subject line (only for channel messages)\"),\n })\n .refine(\n (data) =>\n (data.chatId && !data.teamId && !data.channelId) ||\n (!data.chatId && data.teamId && data.channelId),\n { message: \"Either provide chatId OR both teamId and channelId\" },\n ))(),\n async execute({ chatId, teamId, channelId, content, contentType, subject }) {\n if (chatId) {\n const message = await sendChatMessage(chatId, content, contentType);\n return {\n success: true,\n messageId: message.id,\n type: \"chat\",\n chatId,\n createdAt: message.createdDateTime,\n content: message.body.content,\n };\n }\n\n if (!teamId || !channelId) {\n throw new Error(\"Invalid parameters: provide either chatId or both teamId and channelId\");\n }\n\n const message = await sendChannelMessage(teamId, channelId, content, contentType, subject);\n return {\n success: true,\n messageId: message.id,\n type: \"channel\",\n teamId,\n channelId,\n subject,\n createdAt: message.createdDateTime,\n content: message.body.content,\n };\n },\n});\n"
605
- }
606
- },
607
- "integration:trello": {
608
- "files": {
609
- ".env.example": "# Trello OAuth Configuration\n# Get your credentials from https://trello.com/app-key\nTRELLO_CLIENT_ID=your-api-key\nTRELLO_CLIENT_SECRET=your-oauth-secret\n",
610
- "app/api/auth/trello/callback/route.ts": "import { createOAuthCallbackHandler, trelloConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\n\nexport const GET = createOAuthCallbackHandler(trelloConfig, { tokenStore });\n",
611
- "app/api/auth/trello/route.ts": "import { createOAuthInitHandler, trelloConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../lib/token-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../lib/user-id.ts\";\n\nexport const GET = createOAuthInitHandler(trelloConfig, {\n tokenStore,\n getUserId: requireUserIdFromRequest,\n});\n",
612
- "lib/trello-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst TRELLO_BASE_URL = \"https://api.trello.com/1\";\n\ninterface TrelloBoard {\n id: string;\n name: string;\n desc: string;\n closed: boolean;\n url: string;\n prefs: {\n background: string;\n backgroundColor: string;\n };\n dateLastActivity: string;\n}\n\ninterface TrelloList {\n id: string;\n name: string;\n closed: boolean;\n idBoard: string;\n pos: number;\n}\n\ninterface TrelloCard {\n id: string;\n name: string;\n desc: string;\n closed: boolean;\n idBoard: string;\n idList: string;\n idMembers: string[];\n labels: Array<{\n id: string;\n name: string;\n color: string;\n }>;\n due: string | null;\n dueComplete: boolean;\n url: string;\n dateLastActivity: string;\n}\n\ninterface TrelloMember {\n id: string;\n fullName: string;\n username: string;\n avatarUrl: string;\n}\n\nasync function trelloFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with Trello. Please connect your account.\");\n }\n\n const clientId = process.env.TRELLO_CLIENT_ID;\n if (!clientId) {\n throw new Error(\"TRELLO_CLIENT_ID environment variable is not set.\");\n }\n\n const url = new URL(`${TRELLO_BASE_URL}${endpoint}`);\n // SECURITY: Trello's REST API requires key and token as query parameters.\n // Tokens in query params may be recorded in browser history, server/proxy\n // access logs, and leaked via the Referer header. The Referrer-Policy\n // header (set by Veryfront's security middleware) mitigates the Referer leak.\n // This is an API design limitation. There is no Authorization header alternative.\n url.searchParams.set(\"key\", clientId);\n url.searchParams.set(\"token\", token);\n\n const response = await fetch(url.toString(), {\n ...options,\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => \"\");\n const message = errorText || response.statusText;\n throw new Error(`Trello API error: ${response.status} ${message}`);\n }\n\n return response.json();\n}\n\nexport async function listBoards(): Promise<TrelloBoard[]> {\n return trelloFetch<TrelloBoard[]>(\n \"/members/me/boards?fields=name,desc,closed,url,prefs,dateLastActivity\",\n );\n}\n\nexport async function getBoard(boardId: string): Promise<TrelloBoard> {\n return trelloFetch<TrelloBoard>(\n `/boards/${boardId}?fields=name,desc,closed,url,prefs,dateLastActivity`,\n );\n}\n\nexport async function listLists(boardId: string): Promise<TrelloList[]> {\n return trelloFetch<TrelloList[]>(\n `/boards/${boardId}/lists?fields=name,closed,idBoard,pos`,\n );\n}\n\nexport async function listCards(options: {\n boardId?: string;\n listId?: string;\n limit?: number;\n}): Promise<TrelloCard[]> {\n const { boardId, listId, limit = 50 } = options;\n\n const fields =\n \"name,desc,closed,idBoard,idList,idMembers,labels,due,dueComplete,url,dateLastActivity\";\n\n if (listId) {\n return trelloFetch<TrelloCard[]>(\n `/lists/${listId}/cards?fields=${fields}&limit=${limit}`,\n );\n }\n\n if (boardId) {\n return trelloFetch<TrelloCard[]>(\n `/boards/${boardId}/cards?fields=${fields}&limit=${limit}`,\n );\n }\n\n throw new Error(\"Either boardId or listId must be provided\");\n}\n\nexport async function getCard(cardId: string): Promise<TrelloCard> {\n return trelloFetch<TrelloCard>(\n \"/cards/\" +\n `${cardId}?fields=name,desc,closed,idBoard,idList,idMembers,labels,due,dueComplete,url,dateLastActivity`,\n );\n}\n\nexport async function createCard(options: {\n listId: string;\n name: string;\n desc?: string;\n due?: string;\n pos?: string | number;\n idMembers?: string[];\n idLabels?: string[];\n}): Promise<TrelloCard> {\n const params = new URLSearchParams({\n idList: options.listId,\n name: options.name,\n });\n\n if (options.desc) params.set(\"desc\", options.desc);\n if (options.due) params.set(\"due\", options.due);\n if (options.pos !== undefined) params.set(\"pos\", String(options.pos));\n if (options.idMembers) params.set(\"idMembers\", options.idMembers.join(\",\"));\n if (options.idLabels) params.set(\"idLabels\", options.idLabels.join(\",\"));\n\n return trelloFetch<TrelloCard>(`/cards?${params}`, { method: \"POST\" });\n}\n\nexport async function updateCard(\n cardId: string,\n updates: {\n name?: string;\n desc?: string;\n closed?: boolean;\n idList?: string;\n due?: string | null;\n dueComplete?: boolean;\n idMembers?: string[];\n idLabels?: string[];\n pos?: string | number;\n },\n): Promise<TrelloCard> {\n const params = new URLSearchParams();\n\n if (updates.name !== undefined) params.set(\"name\", updates.name);\n if (updates.desc !== undefined) params.set(\"desc\", updates.desc);\n if (updates.closed !== undefined) params.set(\"closed\", String(updates.closed));\n if (updates.idList !== undefined) params.set(\"idList\", updates.idList);\n if (updates.due !== undefined) params.set(\"due\", updates.due ?? \"\");\n if (updates.dueComplete !== undefined) params.set(\"dueComplete\", String(updates.dueComplete));\n if (updates.idMembers !== undefined) params.set(\"idMembers\", updates.idMembers.join(\",\"));\n if (updates.idLabels !== undefined) params.set(\"idLabels\", updates.idLabels.join(\",\"));\n if (updates.pos !== undefined) params.set(\"pos\", String(updates.pos));\n\n return trelloFetch<TrelloCard>(`/cards/${cardId}?${params}`, { method: \"PUT\" });\n}\n\nexport async function getMe(): Promise<TrelloMember> {\n return trelloFetch<TrelloMember>(\"/members/me?fields=fullName,username,avatarUrl\");\n}\n",
613
- "tools/create-card.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createCard } from \"../lib/trello-client.ts\";\n\nexport default tool({\n id: \"trello-create-card\",\n description: \"Create a new card in a Trello list.\",\n inputSchema: defineSchema((v) => v.object({\n listId: v.string().describe(\"The ID of the list to create the card in\"),\n name: v.string().describe(\"The name/title of the card\"),\n desc: v.string().optional().describe(\"Description or details for the card\"),\n due: v\n .string()\n .optional()\n .describe(\"Due date in ISO 8601 format (e.g., 2024-12-31T23:59:59.000Z)\"),\n pos: v\n .union([v.string(), v.number()])\n .optional()\n .describe('Position of the card: \"top\", \"bottom\", or a positive number'),\n idMembers: v\n .array(v.string())\n .optional()\n .describe(\"Array of member IDs to assign to the card\"),\n idLabels: v\n .array(v.string())\n .optional()\n .describe(\"Array of label IDs to add to the card\"),\n }))(),\n async execute({ listId, name, desc, due, pos, idMembers, idLabels }) {\n const card = await createCard({\n listId,\n name,\n desc,\n due,\n pos,\n idMembers,\n idLabels,\n });\n\n return {\n success: true,\n card: {\n id: card.id,\n name: card.name,\n desc: card.desc,\n url: card.url,\n idList: card.idList,\n due: card.due,\n labels: card.labels.map((label) => ({\n id: label.id,\n name: label.name,\n color: label.color,\n })),\n },\n };\n },\n});\n",
614
- "tools/get-card.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { getCard } from \"../lib/trello-client.ts\";\n\nexport default tool({\n id: \"trello-get-card\",\n description: \"Get details of a specific Trello card by its ID.\",\n inputSchema: defineSchema((v) => v.object({\n cardId: v.string().describe(\"The ID of the card to retrieve\"),\n }))(),\n async execute({ cardId }) {\n const card = await getCard(cardId);\n\n return {\n id: card.id,\n name: card.name,\n desc: card.desc,\n url: card.url,\n closed: card.closed,\n idList: card.idList,\n idBoard: card.idBoard,\n due: card.due,\n dueComplete: card.dueComplete,\n labels: card.labels.map(({ id, name, color }) => ({ id, name, color })),\n memberIds: card.idMembers,\n lastActivity: card.dateLastActivity,\n };\n },\n});\n",
615
- "tools/list-boards.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { listBoards } from \"../lib/trello-client.ts\";\n\nexport default tool({\n id: \"trello-list-boards\",\n description: \"List all Trello boards accessible to the current user.\",\n inputSchema: defineSchema((v) => v.object({\n includeArchived: v\n .boolean()\n .default(false)\n .describe(\"Include archived/closed boards\"),\n limit: v\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of boards to return\"),\n }))(),\n async execute({ includeArchived, limit }) {\n const boards = await listBoards();\n\n const visibleBoards = includeArchived\n ? boards\n : boards.filter((board) => !board.closed);\n\n return visibleBoards.slice(0, limit).map((board) => ({\n id: board.id,\n name: board.name,\n desc: board.desc,\n url: board.url,\n closed: board.closed,\n backgroundColor: board.prefs?.backgroundColor,\n lastActivity: board.dateLastActivity,\n }));\n },\n});\n",
616
- "tools/list-cards.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { listCards } from \"../lib/trello-client.ts\";\n\nexport default tool({\n id: \"trello-list-cards\",\n description:\n \"List cards from Trello. Can filter by board or list. Provide either boardId or listId.\",\n inputSchema: defineSchema((v) => v.object({\n boardId: v.string().optional().describe(\"Board ID to list cards from\"),\n listId: v.string().optional().describe(\"List ID to list cards from\"),\n includeArchived: v\n .boolean()\n .default(false)\n .describe(\"Include archived/closed cards\"),\n limit: v\n .number()\n .min(1)\n .max(100)\n .default(50)\n .describe(\"Maximum number of cards to return\"),\n }))(),\n async execute({ boardId, listId, includeArchived, limit }) {\n if (!boardId && !listId) {\n return { cards: [], message: \"Please specify either a boardId or listId\" };\n }\n\n const cards = await listCards({ boardId, listId, limit });\n const visibleCards = includeArchived\n ? cards\n : cards.filter((card) => !card.closed);\n\n return visibleCards.map(\n ({\n id,\n name,\n desc,\n url,\n closed,\n idList,\n idBoard,\n due,\n dueComplete,\n labels,\n idMembers,\n dateLastActivity,\n }) => ({\n id,\n name,\n desc,\n url,\n closed,\n idList,\n idBoard,\n due,\n dueComplete,\n labels: labels.map(({ id, name, color }) => ({ id, name, color })),\n memberIds: idMembers,\n lastActivity: dateLastActivity,\n }),\n );\n },\n});\n",
617
- "tools/update-card.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { updateCard } from \"../lib/trello-client.ts\";\n\nexport default tool({\n id: \"trello-update-card\",\n description: \"Update an existing Trello card.\",\n inputSchema: defineSchema((v) => v.object({\n cardId: v.string().describe(\"The ID of the card to update\"),\n name: v.string().optional().describe(\"New name/title for the card\"),\n desc: v.string().optional().describe(\"New description or details\"),\n closed: v.boolean().optional().describe(\"Archive or unarchive the card\"),\n idList: v.string().optional().describe(\"Move the card to a different list by list ID\"),\n due: v\n .string()\n .nullable()\n .optional()\n .describe(\"New due date in ISO 8601 format, or null to remove due date\"),\n dueComplete: v.boolean().optional().describe(\"Mark the due date as complete or incomplete\"),\n pos: v\n .union([v.string(), v.number()])\n .optional()\n .describe('New position: \"top\", \"bottom\", or a positive number'),\n idMembers: v\n .array(v.string())\n .optional()\n .describe(\"Array of member IDs to assign to the card (replaces existing)\"),\n idLabels: v\n .array(v.string())\n .optional()\n .describe(\"Array of label IDs for the card (replaces existing)\"),\n }))(),\n async execute({ cardId, ...updates }) {\n const {\n id,\n name,\n desc,\n url,\n closed,\n idList,\n due,\n dueComplete,\n labels,\n } = await updateCard(cardId, updates);\n\n return {\n success: true,\n card: {\n id,\n name,\n desc,\n url,\n closed,\n idList,\n due,\n dueComplete,\n labels: labels.map(({ id, name, color }) => ({ id, name, color })),\n },\n };\n },\n});\n"
618
- }
619
- },
620
- "integration:twilio": {
621
- "files": {
622
- ".env.example": "# Twilio Integration\n# Get your credentials at https://console.twilio.com/\n# Trial account: Can only send to verified numbers, messages prefixed with trial notice\n# Production account: Remove all restrictions by upgrading at https://console.twilio.com/billing\n\n# Your Twilio Account SID (starts with AC)\nTWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\n# Your Twilio Auth Token (keep this secret!)\nTWILIO_AUTH_TOKEN=your_auth_token_here\n\n# Your Twilio phone number in E.164 format (e.g., +14155552671)\n# Get one at: https://console.twilio.com/us1/develop/phone-numbers/manage/search\nTWILIO_PHONE_NUMBER=+1234567890\n",
623
- "lib/twilio-client.ts": "import { getTwilioCredentials } from \"./token-store.ts\";\n\nconst TWILIO_API_VERSION = \"2010-04-01\";\n\nexport interface TwilioMessage {\n sid: string;\n account_sid: string;\n from: string;\n to: string;\n body: string;\n status: \"queued\" | \"sending\" | \"sent\" | \"failed\" | \"delivered\" | \"undelivered\" | \"receiving\" | \"received\";\n direction: \"inbound\" | \"outbound-api\" | \"outbound-call\" | \"outbound-reply\";\n date_created: string;\n date_updated: string;\n date_sent: string | null;\n price: string | null;\n price_unit: string | null;\n error_code: number | null;\n error_message: string | null;\n uri: string;\n num_segments: string;\n num_media: string;\n messaging_service_sid: string | null;\n}\n\nexport interface TwilioCall {\n sid: string;\n account_sid: string;\n from: string;\n to: string;\n status: \"queued\" | \"ringing\" | \"in-progress\" | \"completed\" | \"busy\" | \"failed\" | \"no-answer\" | \"canceled\";\n direction: \"inbound\" | \"outbound-api\" | \"outbound-dial\";\n date_created: string;\n date_updated: string;\n start_time: string | null;\n end_time: string | null;\n duration: string | null;\n price: string | null;\n price_unit: string | null;\n uri: string;\n answered_by: string | null;\n}\n\nexport interface TwilioListResponse<T> {\n messages?: T[];\n calls?: T[];\n first_page_uri: string;\n next_page_uri: string | null;\n previous_page_uri: string | null;\n uri: string;\n page: number;\n page_size: number;\n}\n\ninterface TwilioErrorResponse {\n code: number;\n message: string;\n more_info: string;\n status: number;\n}\n\nfunction buildParams(params: Record<string, string | number>): string {\n return new URLSearchParams(\n Object.entries(params).map(([key, value]) => [key, String(value)]),\n ).toString();\n}\n\nfunction addMediaUrls(params: Record<string, string>, mediaUrl?: string[]): void {\n if (!mediaUrl?.length) return;\n\n for (const [index, url] of mediaUrl.entries()) {\n params[`MediaUrl[${index}]`] = url;\n }\n}\n\nfunction ensureTwilioCredentials(): NonNullable<ReturnType<typeof getTwilioCredentials>> {\n const credentials = getTwilioCredentials();\n if (!credentials) throw new Error(\"Twilio credentials not configured\");\n return credentials;\n}\n\nasync function twilioFetch<T>(\n endpoint: string,\n options: RequestInit & { params?: Record<string, string | number> } = {},\n): Promise<T> {\n const credentials = getTwilioCredentials();\n if (!credentials) {\n throw new Error(\n \"Twilio not configured. Please set TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_PHONE_NUMBER environment variables.\",\n );\n }\n\n const { accountSid, authToken } = credentials;\n const baseUrl = `https://api.twilio.com/${TWILIO_API_VERSION}/Accounts/${accountSid}`;\n let url = `${baseUrl}${endpoint}`;\n\n const headers: Record<string, string> = {\n Authorization: `Basic ${btoa(`${accountSid}:${authToken}`)}`,\n };\n\n let body: string | undefined;\n const encodedParams = options.params ? buildParams(options.params) : undefined;\n\n if (encodedParams) {\n if (options.method === \"POST\") {\n body = encodedParams;\n headers[\"Content-Type\"] = \"application/x-www-form-urlencoded\";\n } else {\n url += `?${encodedParams}`;\n }\n }\n\n const response = await fetch(url, { ...options, headers, body });\n const data: unknown = await response.json();\n\n if (!response.ok) {\n const error = data as TwilioErrorResponse;\n throw new Error(`Twilio API error (${error.code}): ${error.message}\\nMore info: ${error.more_info}`);\n }\n\n return data as T;\n}\n\nexport async function sendSMS(\n to: string,\n body: string,\n options?: {\n mediaUrl?: string[];\n statusCallback?: string;\n },\n): Promise<TwilioMessage> {\n const { phoneNumber } = ensureTwilioCredentials();\n\n const params: Record<string, string> = {\n To: to,\n From: phoneNumber,\n Body: body,\n };\n\n addMediaUrls(params, options?.mediaUrl);\n\n if (options?.statusCallback) params.StatusCallback = options.statusCallback;\n\n return twilioFetch<TwilioMessage>(\"/Messages.json\", { method: \"POST\", params });\n}\n\nexport async function sendWhatsApp(\n to: string,\n body: string,\n options?: {\n mediaUrl?: string[];\n statusCallback?: string;\n },\n): Promise<TwilioMessage> {\n const { phoneNumber } = ensureTwilioCredentials();\n\n const whatsappTo = to.startsWith(\"whatsapp:\") ? to : `whatsapp:${to}`;\n const whatsappFrom = phoneNumber.startsWith(\"whatsapp:\") ? phoneNumber : `whatsapp:${phoneNumber}`;\n\n const params: Record<string, string> = {\n To: whatsappTo,\n From: whatsappFrom,\n Body: body,\n };\n\n addMediaUrls(params, options?.mediaUrl);\n\n if (options?.statusCallback) params.StatusCallback = options.statusCallback;\n\n return twilioFetch<TwilioMessage>(\"/Messages.json\", { method: \"POST\", params });\n}\n\nexport async function listMessages(options?: {\n to?: string;\n from?: string;\n dateSent?: string;\n limit?: number;\n}): Promise<TwilioMessage[]> {\n const params: Record<string, string | number> = {};\n\n if (options?.to) params.To = options.to;\n if (options?.from) params.From = options.from;\n if (options?.dateSent) params.DateSent = options.dateSent;\n if (options?.limit) params.PageSize = options.limit;\n\n const response = await twilioFetch<TwilioListResponse<TwilioMessage>>(\"/Messages.json\", { params });\n return response.messages ?? [];\n}\n\nexport function getMessage(messageSid: string): Promise<TwilioMessage> {\n return twilioFetch<TwilioMessage>(`/Messages/${messageSid}.json`);\n}\n\nexport async function listCalls(options?: {\n to?: string;\n from?: string;\n status?: \"queued\" | \"ringing\" | \"in-progress\" | \"completed\" | \"busy\" | \"failed\" | \"no-answer\" | \"canceled\";\n startTime?: string;\n limit?: number;\n}): Promise<TwilioCall[]> {\n const params: Record<string, string | number> = {};\n\n if (options?.to) params.To = options.to;\n if (options?.from) params.From = options.from;\n if (options?.status) params.Status = options.status;\n if (options?.startTime) params.StartTime = options.startTime;\n if (options?.limit) params.PageSize = options.limit;\n\n const response = await twilioFetch<TwilioListResponse<TwilioCall>>(\"/Calls.json\", { params });\n return response.calls ?? [];\n}\n\nexport function getCall(callSid: string): Promise<TwilioCall> {\n return twilioFetch<TwilioCall>(`/Calls/${callSid}.json`);\n}\n\nexport async function makeCall(\n to: string,\n twiml: string,\n options?: {\n twimlUrl?: string;\n statusCallback?: string;\n statusCallbackMethod?: \"GET\" | \"POST\";\n timeout?: number;\n },\n): Promise<TwilioCall> {\n const { phoneNumber } = ensureTwilioCredentials();\n\n const params: Record<string, string | number> = {\n To: to,\n From: phoneNumber,\n };\n\n if (options?.twimlUrl) {\n params.Url = options.twimlUrl;\n } else {\n params.Twiml = twiml;\n }\n\n if (options?.statusCallback) params.StatusCallback = options.statusCallback;\n if (options?.statusCallbackMethod) params.StatusCallbackMethod = options.statusCallbackMethod;\n if (options?.timeout) params.Timeout = options.timeout;\n\n return twilioFetch<TwilioCall>(\"/Calls.json\", { method: \"POST\", params });\n}\n\nexport function formatPhoneNumber(phone: string, defaultCountryCode = \"+1\"): string {\n const digits = phone.replace(/\\D/g, \"\");\n\n if (phone.startsWith(\"+\")) return phone;\n if (digits.length === 10) return `${defaultCountryCode}${digits}`;\n if (digits.length === 11 && digits.startsWith(\"1\")) return `+${digits}`;\n return `+${digits}`;\n}\n\nexport function formatDate(date: Date): string {\n return date.toISOString().split(\"T\")[0];\n}\n\nexport function parseDate(dateString: string): Date {\n return new Date(dateString);\n}\n",
624
- "tools/get-message.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { getMessage } from \"../lib/twilio-client.ts\";\n\nexport default tool({\n id: \"twilio-get-message\",\n description:\n \"Get detailed information about a specific SMS or WhatsApp message by its SID (Message ID)\",\n inputSchema: defineSchema((v) => v.object({\n messageSid: v\n .string()\n .describe(\n \"The unique Twilio Message SID (starts with 'MM' or 'SM', e.g., MM1234567890abcdef)\",\n ),\n }))(),\n execute: async ({ messageSid }) => {\n try {\n const message = await getMessage(messageSid);\n\n return {\n success: true,\n message: {\n sid: message.sid,\n accountSid: message.account_sid,\n direction: message.direction,\n from: message.from,\n to: message.to,\n body: message.body,\n status: message.status,\n dateSent: message.date_sent,\n dateCreated: message.date_created,\n dateUpdated: message.date_updated,\n numSegments: message.num_segments,\n numMedia: message.num_media,\n price: message.price ? `${message.price} ${message.price_unit}` : null,\n errorCode: message.error_code,\n errorMessage: message.error_message,\n uri: message.uri,\n messagingServiceSid: message.messaging_service_sid,\n },\n summary: `Message ${message.sid}: ${message.direction} ${message.status} message from ${message.from} to ${message.to}`,\n };\n } catch (error) {\n if (!(error instanceof Error)) throw error;\n\n const errorMessage = error.message;\n\n if (errorMessage.includes(\"not configured\")) {\n return {\n error:\n \"Twilio not configured. Please set TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_PHONE_NUMBER.\",\n setupUrl: \"https://console.twilio.com/\",\n };\n }\n\n if (errorMessage.includes(\"20404\")) {\n return {\n error: `Message not found. The SID '${messageSid}' does not exist in your account.`,\n };\n }\n\n throw error;\n }\n },\n});\n",
625
- "tools/list-calls.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { formatPhoneNumber, listCalls } from \"../lib/twilio-client.ts\";\n\ntype CallStatus =\n | \"queued\"\n | \"ringing\"\n | \"in-progress\"\n | \"completed\"\n | \"busy\"\n | \"failed\"\n | \"no-answer\"\n | \"canceled\";\n\ntype ListCallsOptions = {\n to?: string;\n from?: string;\n status?: CallStatus;\n startTime?: string;\n limit?: number;\n};\n\nexport default tool({\n id: \"twilio-list-calls\",\n description:\n \"List recent phone calls from your Twilio account. Supports filtering by recipient, sender, status, and date.\",\n inputSchema: defineSchema((v) => v.object({\n to: v\n .string()\n .optional()\n .describe(\"Filter by recipient phone number in E.164 format (e.g., +14155552671)\"),\n from: v\n .string()\n .optional()\n .describe(\"Filter by sender phone number in E.164 format (e.g., +14155552671)\"),\n status: v\n .enum([\n \"queued\",\n \"ringing\",\n \"in-progress\",\n \"completed\",\n \"busy\",\n \"failed\",\n \"no-answer\",\n \"canceled\",\n ])\n .optional()\n .describe(\"Filter by call status\"),\n startTime: v\n .string()\n .optional()\n .describe(\"Filter by start time in YYYY-MM-DD format (e.g., 2024-01-15)\"),\n limit: v\n .number()\n .min(1)\n .max(100)\n .optional()\n .describe(\"Maximum number of calls to return (default: 20, max: 100)\"),\n }))(),\n execute: async ({ to, from, status, startTime, limit }) => {\n try {\n const options: ListCallsOptions = {\n ...(to ? { to: formatPhoneNumber(to) } : {}),\n ...(from ? { from: formatPhoneNumber(from) } : {}),\n ...(status ? { status } : {}),\n ...(startTime ? { startTime } : {}),\n ...(limit ? { limit } : {}),\n };\n\n const calls = await listCalls(options);\n\n if (calls.length === 0) {\n return {\n success: true,\n count: 0,\n calls: [],\n message: \"No calls found matching the criteria.\",\n };\n }\n\n const formattedCalls = calls.map((call) => ({\n sid: call.sid,\n direction: call.direction,\n from: call.from,\n to: call.to,\n status: call.status,\n startTime: call.start_time,\n endTime: call.end_time,\n duration: call.duration ? `${call.duration} seconds` : null,\n dateCreated: call.date_created,\n dateUpdated: call.date_updated,\n price: call.price ? `${call.price} ${call.price_unit}` : null,\n answeredBy: call.answered_by,\n }));\n\n const totalDuration = calls.reduce((sum, call) => {\n if (!call.duration) return sum;\n return sum + parseInt(call.duration, 10);\n }, 0);\n\n const statusCounts = calls.reduce<Record<string, number>>((acc, call) => {\n acc[call.status] = (acc[call.status] ?? 0) + 1;\n return acc;\n }, {});\n\n const callCount = calls.length;\n\n return {\n success: true,\n count: callCount,\n calls: formattedCalls,\n statistics: {\n totalCalls: callCount,\n totalDuration: `${totalDuration} seconds (${Math.round(totalDuration / 60)} minutes)`,\n statusBreakdown: statusCounts,\n },\n message: `Found ${callCount} call${callCount === 1 ? \"\" : \"s\"}.`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not configured\")) {\n return {\n error: \"Twilio not configured. Please set TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_PHONE_NUMBER.\",\n setupUrl: \"https://console.twilio.com/\",\n };\n }\n throw error;\n }\n },\n});\n",
626
- "tools/list-messages.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { formatPhoneNumber, listMessages } from \"../lib/twilio-client.ts\";\n\ntype ListMessagesOptions = {\n to?: string;\n from?: string;\n dateSent?: string;\n limit?: number;\n};\n\nexport default tool({\n id: \"twilio-list-messages\",\n description:\n \"List recent SMS and WhatsApp messages from your Twilio account. Supports filtering by recipient, sender, and date.\",\n inputSchema: defineSchema((v) => v.object({\n to: v\n .string()\n .optional()\n .describe(\"Filter by recipient phone number in E.164 format (e.g., +14155552671)\"),\n from: v\n .string()\n .optional()\n .describe(\"Filter by sender phone number in E.164 format (e.g., +14155552671)\"),\n dateSent: v\n .string()\n .optional()\n .describe(\"Filter by date sent in YYYY-MM-DD format (e.g., 2024-01-15)\"),\n limit: v\n .number()\n .min(1)\n .max(100)\n .optional()\n .describe(\"Maximum number of messages to return (default: 20, max: 100)\"),\n }))(),\n execute: async ({ to, from, dateSent, limit }) => {\n try {\n const options: ListMessagesOptions = {\n to: to ? formatPhoneNumber(to) : undefined,\n from: from ? formatPhoneNumber(from) : undefined,\n dateSent,\n limit,\n };\n\n const messages = await listMessages(options);\n\n if (messages.length === 0) {\n return {\n success: true,\n count: 0,\n messages: [],\n message: \"No messages found matching the criteria.\",\n };\n }\n\n const formattedMessages = messages.map((msg) => ({\n sid: msg.sid,\n direction: msg.direction,\n from: msg.from,\n to: msg.to,\n body: msg.body,\n status: msg.status,\n dateSent: msg.date_sent,\n dateCreated: msg.date_created,\n numSegments: msg.num_segments,\n numMedia: msg.num_media,\n price: msg.price ? `${msg.price} ${msg.price_unit}` : null,\n errorCode: msg.error_code,\n errorMessage: msg.error_message,\n }));\n\n const messageCount = messages.length;\n\n return {\n success: true,\n count: messageCount,\n messages: formattedMessages,\n message: `Found ${messageCount} message${messageCount === 1 ? \"\" : \"s\"}.`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not configured\")) {\n return {\n error:\n \"Twilio not configured. Please set TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_PHONE_NUMBER.\",\n setupUrl: \"https://console.twilio.com/\",\n };\n }\n throw error;\n }\n },\n});\n",
627
- "tools/send-sms.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { formatPhoneNumber, sendSMS } from \"../lib/twilio-client.ts\";\n\nexport default tool({\n id: \"twilio-send-sms\",\n description: \"Send an SMS text message to a phone number using Twilio\",\n inputSchema: defineSchema((v) => v.object({\n to: v\n .string()\n .describe(\n \"Recipient phone number in E.164 format (e.g., +14155552671) or 10-digit US format\",\n ),\n body: v\n .string()\n .min(1)\n .max(1600)\n .describe(\"Message text to send (max 1600 characters)\"),\n mediaUrl: v\n .array(v.string().url())\n .optional()\n .describe(\"Optional array of media URLs to send as MMS (images, videos, etc.)\"),\n }))(),\n execute: async ({ to, body, mediaUrl }) => {\n try {\n const formattedPhone = formatPhoneNumber(to);\n const message = await sendSMS(formattedPhone, body, { mediaUrl });\n\n return {\n success: true,\n messageSid: message.sid,\n status: message.status,\n to: message.to,\n from: message.from,\n body: message.body,\n numSegments: message.num_segments,\n price: message.price,\n priceUnit: message.price_unit,\n message: `SMS sent successfully to ${message.to}. Status: ${message.status}`,\n };\n } catch (error) {\n if (!(error instanceof Error)) throw error;\n\n const msg = error.message;\n\n if (msg.includes(\"not configured\")) {\n return {\n error:\n \"Twilio not configured. Please set TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_PHONE_NUMBER.\",\n setupUrl: \"https://console.twilio.com/\",\n };\n }\n\n if (msg.includes(\"21211\")) {\n return {\n error: `Invalid phone number: ${to}. Please use E.164 format (e.g., +14155552671).`,\n };\n }\n\n if (msg.includes(\"21608\")) {\n return {\n error:\n \"Unverified number. Trial accounts can only send to verified numbers. Verify at: https://console.twilio.com/us1/develop/phone-numbers/manage/verified\",\n };\n }\n\n if (msg.includes(\"21610\")) {\n return {\n error:\n \"Unverified 'To' number. This number must be verified before you can send messages to it during trial.\",\n };\n }\n\n throw error;\n }\n },\n});\n",
628
- "tools/send-whatsapp.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { formatPhoneNumber, sendWhatsApp } from \"../lib/twilio-client.ts\";\n\nexport default tool({\n id: \"twilio-send-whatsapp\",\n description:\n \"Send a WhatsApp message to a phone number using Twilio. Note: Recipients must have opted in to receive messages.\",\n inputSchema: defineSchema((v) => v.object({\n to: v\n .string()\n .describe(\n \"Recipient phone number in E.164 format (e.g., +14155552671). The 'whatsapp:' prefix is optional.\",\n ),\n body: v.string().min(1).describe(\"Message text to send\"),\n mediaUrl: v\n .array(v.string().url())\n .optional()\n .describe(\"Optional array of media URLs to send (images, videos, PDFs, etc.)\"),\n }))(),\n execute: async ({ to, body, mediaUrl }) => {\n try {\n const formattedPhone = formatPhoneNumber(to);\n const message = await sendWhatsApp(formattedPhone, body, { mediaUrl });\n\n return {\n success: true,\n messageSid: message.sid,\n status: message.status,\n to: message.to,\n from: message.from,\n body: message.body,\n numSegments: message.num_segments,\n price: message.price,\n priceUnit: message.price_unit,\n message: `WhatsApp message sent successfully to ${message.to}. Status: ${message.status}`,\n };\n } catch (error) {\n if (!(error instanceof Error)) throw error;\n\n const message = error.message;\n\n if (message.includes(\"not configured\")) {\n return {\n error:\n \"Twilio not configured. Please set TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_PHONE_NUMBER.\",\n setupUrl: \"https://console.twilio.com/\",\n };\n }\n\n const errorMap: Array<[code: string, result: Record<string, unknown>]> = [\n [\n \"63007\",\n {\n error:\n \"Recipient has not opted in to receive WhatsApp messages. They must send a message to your WhatsApp sandbox first.\",\n sandboxUrl: \"https://console.twilio.com/us1/develop/sms/try-it-out/whatsapp-learn\",\n },\n ],\n [\"63016\", { error: \"WhatsApp message failed: Recipient phone number is not a WhatsApp user.\" }],\n [\"63030\", { error: \"Message body is required for WhatsApp messages unless media is included.\" }],\n [\"63003\", { error: \"Message exceeds maximum allowed length for WhatsApp.\" }],\n ];\n\n for (const [code, result] of errorMap) {\n if (message.includes(code)) return result;\n }\n\n throw error;\n }\n },\n});\n"
629
- }
630
- },
631
- "ai-rules:agents.md": {
632
- "files": {
633
- "agents.md": "# Veryfront project guide\n\nThis is a Veryfront project. Veryfront is a framework for building and running AI apps and agents in TypeScript and React.\n\n## Project conventions\n\nUse these folders as runtime boundaries. Create folders only when the feature needs them.\n\n- `app/`: pages, layouts, route handlers, and user-facing API routes.\n- `agents/`: model reasoning and tool use.\n- `tools/`: callable capabilities for agents and workflows.\n- `workflows/`: multi-step coordination.\n- `skills/`: reusable agent instructions in `skills/<id>/SKILL.md`.\n- `veryfront.config.ts`: project metadata and router configuration.\n\n## Developer loop\n\n1. Start local development with `veryfront dev`.\n2. Generate new files with `veryfront generate <type> <name>`.\n3. Inspect current CLI commands with `veryfront schema --json`.\n4. Verify discovered routes with `veryfront routes`.\n5. Run focused tests and builds before shipping.\n6. Use https://veryfront.com/docs when local files and CLI schema do not answer a Veryfront API or convention question.\n\n## Coding agent loop\n\nPrefer Veryfront scaffold tools over hand-written boilerplate. Keep app routes, agents, tools, workflows, and skills in their expected folders.\n\n## Inference\n\nAgent routes need model inference. Use a provider API key such as `OPENAI_API_KEY` or `ANTHROPIC_API_KEY`, configure an OpenAI-compatible local server, or run `veryfront login` to use the optional Veryfront Cloud gateway.\n"
634
- }
635
- },
636
- "ai-rules:claude-code.md": {
637
- "files": {
638
- "claude-code.md": "# Veryfront project guide\n\nFollow `AGENTS.md` when it exists. If it does not exist, use this guide.\n\nThis is a Veryfront project. Veryfront is a framework for building and running AI apps and agents in TypeScript and React.\n\n## Project conventions\n\nUse these folders as runtime boundaries. Create folders only when the feature needs them.\n\n- `app/`: pages, layouts, route handlers, and user-facing API routes.\n- `agents/`: model reasoning and tool use.\n- `tools/`: callable capabilities for agents and workflows.\n- `workflows/`: multi-step coordination.\n- `skills/`: reusable agent instructions in `skills/<id>/SKILL.md`.\n- `veryfront.config.ts`: project metadata and router configuration.\n\n## Developer loop\n\n1. Start local development with `veryfront dev`.\n2. Generate new files with `veryfront generate <type> <name>`.\n3. Inspect current CLI commands with `veryfront schema --json`.\n4. Verify discovered routes with `veryfront routes`.\n5. Run focused tests and builds before shipping.\n6. Use https://veryfront.com/docs when local files and CLI schema do not answer a Veryfront API or convention question.\n\n## Coding agent loop\n\nPrefer Veryfront scaffold tools over hand-written boilerplate. Keep app routes, agents, tools, workflows, and skills in their expected folders.\n\n## Inference\n\nAgent routes need model inference. Use a provider API key such as `OPENAI_API_KEY` or `ANTHROPIC_API_KEY`, configure an OpenAI-compatible local server, or run `veryfront login` to use the optional Veryfront Cloud gateway.\n"
639
- }
640
- },
641
- "ai-rules:copilot.md": {
642
- "files": {
643
- "copilot.md": "# Veryfront project guide\n\nFollow `AGENTS.md` when it exists. If it does not exist, use this guide.\n\nThis is a Veryfront project. Veryfront is a framework for building and running AI apps and agents in TypeScript and React.\n\n## Project conventions\n\nUse these folders as runtime boundaries. Create folders only when the feature needs them.\n\n- `app/`: pages, layouts, route handlers, and user-facing API routes.\n- `agents/`: model reasoning and tool use.\n- `tools/`: callable capabilities for agents and workflows.\n- `workflows/`: multi-step coordination.\n- `skills/`: reusable agent instructions in `skills/<id>/SKILL.md`.\n- `veryfront.config.ts`: project metadata and router configuration.\n\n## Developer loop\n\n1. Start local development with `veryfront dev`.\n2. Generate new files with `veryfront generate <type> <name>`.\n3. Inspect current CLI commands with `veryfront schema --json`.\n4. Verify discovered routes with `veryfront routes`.\n5. Run focused tests and builds before shipping.\n6. Use https://veryfront.com/docs when local files and CLI schema do not answer a Veryfront API or convention question.\n\n## Coding agent loop\n\nPrefer Veryfront scaffold tools over hand-written boilerplate. Keep app routes, agents, tools, workflows, and skills in their expected folders.\n\n## Inference\n\nAgent routes need model inference. Use a provider API key such as `OPENAI_API_KEY` or `ANTHROPIC_API_KEY`, configure an OpenAI-compatible local server, or run `veryfront login` to use the optional Veryfront Cloud gateway.\n"
644
- }
645
- },
646
- "ai-rules:cursor.md": {
647
- "files": {
648
- "cursor.md": "# Veryfront project guide\n\nFollow `AGENTS.md` when it exists. If it does not exist, use this guide.\n\nThis is a Veryfront project. Veryfront is a framework for building and running AI apps and agents in TypeScript and React.\n\n## Project conventions\n\nUse these folders as runtime boundaries. Create folders only when the feature needs them.\n\n- `app/`: pages, layouts, route handlers, and user-facing API routes.\n- `agents/`: model reasoning and tool use.\n- `tools/`: callable capabilities for agents and workflows.\n- `workflows/`: multi-step coordination.\n- `skills/`: reusable agent instructions in `skills/<id>/SKILL.md`.\n- `veryfront.config.ts`: project metadata and router configuration.\n\n## Developer loop\n\n1. Start local development with `veryfront dev`.\n2. Generate new files with `veryfront generate <type> <name>`.\n3. Inspect current CLI commands with `veryfront schema --json`.\n4. Verify discovered routes with `veryfront routes`.\n5. Run focused tests and builds before shipping.\n6. Use https://veryfront.com/docs when local files and CLI schema do not answer a Veryfront API or convention question.\n\n## Coding agent loop\n\nPrefer Veryfront scaffold tools over hand-written boilerplate. Keep app routes, agents, tools, workflows, and skills in their expected folders.\n\n## Inference\n\nAgent routes need model inference. Use a provider API key such as `OPENAI_API_KEY` or `ANTHROPIC_API_KEY`, configure an OpenAI-compatible local server, or run `veryfront login` to use the optional Veryfront Cloud gateway.\n"
649
- }
650
- },
651
- "ai-rules:skill.md": {
652
- "files": {
653
- "skill.md": "---\nname: veryfront\ndescription: Build and run AI apps and agents with Veryfront CLI\nlicense: Apache-2.0\ncompatibility: Claude Code, Cursor, VS Code, Codex, Gemini CLI\nmetadata:\n author: veryfront\n version: \"1.0\"\n---\n\n# Veryfront\n\nVeryfront is a framework for building and running AI apps and agents in TypeScript and React.\n\n## Project conventions\n\nUse these folders as runtime boundaries. Create folders only when the feature needs them.\n\n- `app/`: pages, layouts, route handlers, and user-facing API routes.\n- `agents/`: model reasoning and tool use.\n- `tools/`: callable capabilities for agents and workflows.\n- `workflows/`: multi-step coordination.\n- `skills/`: reusable agent instructions in `skills/<id>/SKILL.md`.\n- `veryfront.config.ts`: project metadata and router configuration.\n\n## Developer loop\n\n1. Start local development with `veryfront dev`.\n2. Generate new files with `veryfront generate <type> <name>`.\n3. Inspect current CLI commands with `veryfront schema --json`.\n4. Verify discovered routes with `veryfront routes`.\n5. Run focused tests and builds before shipping.\n6. Use https://veryfront.com/docs when local files and CLI schema do not answer a Veryfront API or convention question.\n\n## Coding agent loop\n\nPrefer Veryfront scaffold tools over hand-written boilerplate. Keep app routes, agents, tools, workflows, and skills in their expected folders.\n\n## Inference\n\nAgent routes need model inference. Use a provider API key such as `OPENAI_API_KEY` or `ANTHROPIC_API_KEY`, configure an OpenAI-compatible local server, or run `veryfront login` to use the optional Veryfront Cloud gateway.\n"
654
- }
655
- },
656
- "ai-rules:windsurf.md": {
657
- "files": {
658
- "windsurf.md": "# Veryfront project guide\n\nFollow `AGENTS.md` when it exists. If it does not exist, use this guide.\n\nThis is a Veryfront project. Veryfront is a framework for building and running AI apps and agents in TypeScript and React.\n\n## Project conventions\n\nUse these folders as runtime boundaries. Create folders only when the feature needs them.\n\n- `app/`: pages, layouts, route handlers, and user-facing API routes.\n- `agents/`: model reasoning and tool use.\n- `tools/`: callable capabilities for agents and workflows.\n- `workflows/`: multi-step coordination.\n- `skills/`: reusable agent instructions in `skills/<id>/SKILL.md`.\n- `veryfront.config.ts`: project metadata and router configuration.\n\n## Developer loop\n\n1. Start local development with `veryfront dev`.\n2. Generate new files with `veryfront generate <type> <name>`.\n3. Inspect current CLI commands with `veryfront schema --json`.\n4. Verify discovered routes with `veryfront routes`.\n5. Run focused tests and builds before shipping.\n6. Use https://veryfront.com/docs when local files and CLI schema do not answer a Veryfront API or convention question.\n\n## Coding agent loop\n\nPrefer Veryfront scaffold tools over hand-written boilerplate. Keep app routes, agents, tools, workflows, and skills in their expected folders.\n\n## Inference\n\nAgent routes need model inference. Use a provider API key such as `OPENAI_API_KEY` or `ANTHROPIC_API_KEY`, configure an OpenAI-compatible local server, or run `veryfront login` to use the optional Veryfront Cloud gateway.\n"
659
- }
660
- }
661
- }
662
- };