veryfront 0.1.545 → 0.1.546
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.
- package/esm/deno.js +3 -3
- package/esm/extensions/ext-document-kreuzberg/src/kreuzberg.d.ts.map +1 -1
- package/esm/extensions/ext-document-kreuzberg/src/kreuzberg.js +28 -2
- package/esm/extensions/ext-sandbox-shell-tools/src/index.d.ts.map +1 -1
- package/esm/extensions/ext-sandbox-shell-tools/src/index.js +22 -2
- package/esm/src/agent/hosted/veryfront-cloud-agent-service.d.ts.map +1 -1
- package/esm/src/agent/hosted/veryfront-cloud-agent-service.js +21 -1
- package/esm/src/agent/index.d.ts +25 -4
- package/esm/src/agent/index.d.ts.map +1 -1
- package/esm/src/agent/index.js +25 -4
- package/esm/src/agent/react/use-chat/streaming/handler.d.ts +1 -0
- package/esm/src/agent/react/use-chat/streaming/handler.d.ts.map +1 -1
- package/esm/src/agent/react/use-chat/streaming/handler.js +129 -6
- package/esm/src/agent/react/use-chat/streaming/index.d.ts +1 -1
- package/esm/src/agent/react/use-chat/streaming/index.d.ts.map +1 -1
- package/esm/src/agent/react/use-chat/streaming/index.js +1 -1
- package/esm/src/agent/react/use-chat/types.d.ts +2 -0
- package/esm/src/agent/react/use-chat/types.d.ts.map +1 -1
- package/esm/src/agent/react/use-chat/use-chat.d.ts +2 -2
- package/esm/src/agent/react/use-chat/use-chat.d.ts.map +1 -1
- package/esm/src/agent/react/use-chat/use-chat.js +7 -4
- package/esm/src/chat/index.d.ts +2 -2
- package/esm/src/chat/index.js +2 -2
- package/esm/src/integrations/index.d.ts +8 -1
- package/esm/src/integrations/index.d.ts.map +1 -1
- package/esm/src/integrations/index.js +8 -1
- package/esm/src/jobs/index.d.ts +0 -1
- package/esm/src/jobs/index.d.ts.map +1 -1
- package/esm/src/jobs/index.js +0 -1
- package/esm/src/mcp/index.d.ts +2 -6
- package/esm/src/mcp/index.d.ts.map +1 -1
- package/esm/src/mcp/index.js +2 -6
- package/esm/src/prompt/index.d.ts +5 -0
- package/esm/src/prompt/index.d.ts.map +1 -1
- package/esm/src/prompt/index.js +5 -0
- package/esm/src/react/components/chat/chat/index.d.ts +1 -1
- package/esm/src/react/components/chat/chat/index.js +1 -1
- package/esm/src/resource/index.d.ts +10 -7
- package/esm/src/resource/index.d.ts.map +1 -1
- package/esm/src/resource/index.js +10 -7
- package/esm/src/sandbox/types.d.ts +1 -1
- package/esm/src/sandbox/types.d.ts.map +1 -1
- package/esm/src/server/handlers/dev/framework-candidates.generated.d.ts.map +1 -1
- package/esm/src/server/handlers/dev/framework-candidates.generated.js +2 -0
- package/esm/src/tool/index.d.ts +44 -24
- package/esm/src/tool/index.d.ts.map +1 -1
- package/esm/src/tool/index.js +44 -24
- package/esm/src/utils/version-constant.d.ts +1 -1
- package/esm/src/utils/version-constant.js +1 -1
- package/package.json +51 -16
- package/src/_dnt.polyfills.ts +0 -558
- package/src/_dnt.shims.ts +0 -71
- package/src/cli/app/actions.ts +0 -149
- package/src/cli/app/components/inline-input.ts +0 -243
- package/src/cli/app/components/list-select.ts +0 -197
- package/src/cli/app/data/slug-words.ts +0 -303
- package/src/cli/app/handlers/remote-navigation.ts +0 -65
- package/src/cli/app/handlers/view-handlers.ts +0 -173
- package/src/cli/app/index.ts +0 -19
- package/src/cli/app/logging/console-interceptor.ts +0 -97
- package/src/cli/app/operations/project-creation.ts +0 -71
- package/src/cli/app/shell.ts +0 -666
- package/src/cli/app/startup.ts +0 -48
- package/src/cli/app/state.ts +0 -383
- package/src/cli/app/types.ts +0 -40
- package/src/cli/app/utils.ts +0 -65
- package/src/cli/app/views/auth.ts +0 -30
- package/src/cli/app/views/dashboard.ts +0 -189
- package/src/cli/app/views/help.ts +0 -65
- package/src/cli/app/views/index.ts +0 -12
- package/src/cli/app/views/new-project.ts +0 -36
- package/src/cli/app/views/startup.ts +0 -112
- package/src/cli/app/views/templates.ts +0 -35
- package/src/cli/auth/browser.ts +0 -26
- package/src/cli/auth/callback-server.ts +0 -297
- package/src/cli/auth/index.ts +0 -16
- package/src/cli/auth/login.ts +0 -303
- package/src/cli/auth/provider-store.ts +0 -82
- package/src/cli/auth/providers/anthropic.ts +0 -45
- package/src/cli/auth/providers/openai.ts +0 -44
- package/src/cli/auth/token-store.ts +0 -63
- package/src/cli/auth/utils.ts +0 -31
- package/src/cli/commands/analyze-chunks/command-help.ts +0 -18
- package/src/cli/commands/analyze-chunks/command.ts +0 -89
- package/src/cli/commands/analyze-chunks/handler.ts +0 -33
- package/src/cli/commands/build/command-help.ts +0 -51
- package/src/cli/commands/build/command.ts +0 -93
- package/src/cli/commands/build/config-display.ts +0 -48
- package/src/cli/commands/build/error-handler.ts +0 -30
- package/src/cli/commands/build/handler.ts +0 -103
- package/src/cli/commands/build/stats-display.ts +0 -40
- package/src/cli/commands/build/types.ts +0 -1
- package/src/cli/commands/clean/command-help.ts +0 -33
- package/src/cli/commands/clean/command.ts +0 -151
- package/src/cli/commands/clean/handler.ts +0 -33
- package/src/cli/commands/completions/command-help.ts +0 -15
- package/src/cli/commands/completions/command.ts +0 -163
- package/src/cli/commands/completions/handler.ts +0 -29
- package/src/cli/commands/config/command-help.ts +0 -15
- package/src/cli/commands/config/handler.ts +0 -90
- package/src/cli/commands/demo/command-help.ts +0 -27
- package/src/cli/commands/demo/demo.ts +0 -630
- package/src/cli/commands/demo/handler.ts +0 -30
- package/src/cli/commands/demo/index.ts +0 -2
- package/src/cli/commands/demo/steps.ts +0 -68
- package/src/cli/commands/deploy/command-help.ts +0 -42
- package/src/cli/commands/deploy/command.ts +0 -287
- package/src/cli/commands/deploy/handler.ts +0 -13
- package/src/cli/commands/deploy/index.ts +0 -14
- package/src/cli/commands/dev/command-help.ts +0 -34
- package/src/cli/commands/dev/command.ts +0 -337
- package/src/cli/commands/dev/handler.ts +0 -76
- package/src/cli/commands/dev/index.ts +0 -7
- package/src/cli/commands/doctor/ai-checks.ts +0 -65
- package/src/cli/commands/doctor/command-help.ts +0 -15
- package/src/cli/commands/doctor/handler.ts +0 -27
- package/src/cli/commands/doctor/index.ts +0 -89
- package/src/cli/commands/doctor/project-structure.ts +0 -56
- package/src/cli/commands/doctor/server-checks.ts +0 -142
- package/src/cli/commands/doctor/types.ts +0 -6
- package/src/cli/commands/doctor/version-checks.ts +0 -67
- package/src/cli/commands/extension/command-help.ts +0 -18
- package/src/cli/commands/extension/handler.ts +0 -41
- package/src/cli/commands/extension/init-command.ts +0 -150
- package/src/cli/commands/extension/validate-command.ts +0 -78
- package/src/cli/commands/files/command-help.ts +0 -41
- package/src/cli/commands/files/command.ts +0 -343
- package/src/cli/commands/files/handler.ts +0 -6
- package/src/cli/commands/files/index.ts +0 -19
- package/src/cli/commands/generate/command-help.ts +0 -21
- package/src/cli/commands/generate/command.ts +0 -155
- package/src/cli/commands/generate/handler.ts +0 -55
- package/src/cli/commands/generate/index.ts +0 -6
- package/src/cli/commands/generate/integration-generator-helpers.ts +0 -185
- package/src/cli/commands/generate/integration-generator.ts +0 -782
- package/src/cli/commands/init/catalog.ts +0 -224
- package/src/cli/commands/init/command-help.ts +0 -54
- package/src/cli/commands/init/config-generator.ts +0 -84
- package/src/cli/commands/init/handler.ts +0 -79
- package/src/cli/commands/init/index.ts +0 -8
- package/src/cli/commands/init/init-command.ts +0 -566
- package/src/cli/commands/init/interactive-wizard.ts +0 -170
- package/src/cli/commands/init/path-utils.ts +0 -15
- package/src/cli/commands/init/types.ts +0 -28
- package/src/cli/commands/install/command-help.ts +0 -63
- package/src/cli/commands/install/detect.ts +0 -51
- package/src/cli/commands/install/handler.ts +0 -33
- package/src/cli/commands/install/install.ts +0 -112
- package/src/cli/commands/install/registry.ts +0 -79
- package/src/cli/commands/install/types.ts +0 -58
- package/src/cli/commands/install/uninstall.ts +0 -139
- package/src/cli/commands/issues/command-help.ts +0 -50
- package/src/cli/commands/issues/command.ts +0 -364
- package/src/cli/commands/issues/handler.ts +0 -10
- package/src/cli/commands/issues/index.ts +0 -6
- package/src/cli/commands/knowledge/command-help.ts +0 -44
- package/src/cli/commands/knowledge/command-helpers.ts +0 -295
- package/src/cli/commands/knowledge/command.ts +0 -802
- package/src/cli/commands/knowledge/handler.ts +0 -6
- package/src/cli/commands/knowledge/index.ts +0 -2
- package/src/cli/commands/knowledge/parser-source.ts +0 -641
- package/src/cli/commands/knowledge/result.ts +0 -88
- package/src/cli/commands/knowledge/source-policy.ts +0 -164
- package/src/cli/commands/lint/command-help.ts +0 -15
- package/src/cli/commands/lint/command.ts +0 -59
- package/src/cli/commands/lint/handler.ts +0 -36
- package/src/cli/commands/lock/command-help.ts +0 -42
- package/src/cli/commands/lock/command.ts +0 -193
- package/src/cli/commands/lock/handler.ts +0 -35
- package/src/cli/commands/login/command-help.ts +0 -38
- package/src/cli/commands/logout/command-help.ts +0 -14
- package/src/cli/commands/mcp/command-help.ts +0 -36
- package/src/cli/commands/mcp/handler.ts +0 -37
- package/src/cli/commands/merge/command-help.ts +0 -32
- package/src/cli/commands/merge/command.ts +0 -224
- package/src/cli/commands/merge/handler.ts +0 -13
- package/src/cli/commands/open/command-help.ts +0 -19
- package/src/cli/commands/open/command.ts +0 -33
- package/src/cli/commands/open/handler.ts +0 -38
- package/src/cli/commands/pull/command-help.ts +0 -56
- package/src/cli/commands/pull/command.ts +0 -491
- package/src/cli/commands/pull/handler.ts +0 -13
- package/src/cli/commands/pull/index.ts +0 -3
- package/src/cli/commands/push/command-help.ts +0 -39
- package/src/cli/commands/push/command.ts +0 -404
- package/src/cli/commands/push/handler.ts +0 -13
- package/src/cli/commands/push/index.ts +0 -3
- package/src/cli/commands/routes/command-help.ts +0 -15
- package/src/cli/commands/routes/command.ts +0 -88
- package/src/cli/commands/routes/handler.ts +0 -31
- package/src/cli/commands/schema/command-help.ts +0 -17
- package/src/cli/commands/schema/command.ts +0 -64
- package/src/cli/commands/schema/handler.ts +0 -36
- package/src/cli/commands/serve/command-help.ts +0 -25
- package/src/cli/commands/serve/command.ts +0 -120
- package/src/cli/commands/serve/handler.ts +0 -43
- package/src/cli/commands/serve/split-mode.ts +0 -193
- package/src/cli/commands/skills/command-help.ts +0 -23
- package/src/cli/commands/skills/command.ts +0 -23
- package/src/cli/commands/skills/create.ts +0 -87
- package/src/cli/commands/skills/handler.ts +0 -106
- package/src/cli/commands/skills/validate.ts +0 -110
- package/src/cli/commands/start/command-help.ts +0 -52
- package/src/cli/commands/start/command.ts +0 -258
- package/src/cli/commands/start/handler.ts +0 -35
- package/src/cli/commands/studio/command-help.ts +0 -27
- package/src/cli/commands/studio/command.ts +0 -96
- package/src/cli/commands/studio/handler.ts +0 -28
- package/src/cli/commands/studio/index.ts +0 -6
- package/src/cli/commands/styles/command-help.ts +0 -22
- package/src/cli/commands/styles/command.ts +0 -303
- package/src/cli/commands/styles/handler.ts +0 -28
- package/src/cli/commands/task/command-help.ts +0 -23
- package/src/cli/commands/task/command.ts +0 -112
- package/src/cli/commands/task/handler.ts +0 -28
- package/src/cli/commands/test/command-help.ts +0 -18
- package/src/cli/commands/test/command.ts +0 -90
- package/src/cli/commands/test/handler.ts +0 -74
- package/src/cli/commands/up/command-help.ts +0 -25
- package/src/cli/commands/up/command.ts +0 -218
- package/src/cli/commands/up/handler.ts +0 -11
- package/src/cli/commands/up/index.ts +0 -7
- package/src/cli/commands/uploads/command-help.ts +0 -46
- package/src/cli/commands/uploads/command.ts +0 -482
- package/src/cli/commands/uploads/handler.ts +0 -6
- package/src/cli/commands/uploads/index.ts +0 -23
- package/src/cli/commands/whoami/command-help.ts +0 -14
- package/src/cli/commands/worker/command-help.ts +0 -49
- package/src/cli/commands/worker/command.ts +0 -114
- package/src/cli/commands/worker/handler.ts +0 -36
- package/src/cli/commands/workflow/command-help.ts +0 -22
- package/src/cli/commands/workflow/command.ts +0 -157
- package/src/cli/commands/workflow/handler.ts +0 -30
- package/src/cli/help/command-definitions.ts +0 -96
- package/src/cli/help/command-help.ts +0 -73
- package/src/cli/help/formatters.ts +0 -102
- package/src/cli/help/index.ts +0 -6
- package/src/cli/help/logo.ts +0 -5
- package/src/cli/help/main-help.ts +0 -98
- package/src/cli/help/tips.ts +0 -54
- package/src/cli/help/types.ts +0 -25
- package/src/cli/main.ts +0 -74
- package/src/cli/mcp/advanced-tools.ts +0 -61
- package/src/cli/mcp/dev-server-client.ts +0 -82
- package/src/cli/mcp/index.ts +0 -2
- package/src/cli/mcp/jsonrpc.ts +0 -186
- package/src/cli/mcp/remote-file-tool-helpers.ts +0 -27
- package/src/cli/mcp/remote-file-tools.ts +0 -826
- package/src/cli/mcp/server.ts +0 -531
- package/src/cli/mcp/standalone.ts +0 -646
- package/src/cli/mcp/stdio.ts +0 -57
- package/src/cli/mcp/tools/bootstrap-tool.ts +0 -71
- package/src/cli/mcp/tools/build-tool.ts +0 -119
- package/src/cli/mcp/tools/catalog-tools.ts +0 -507
- package/src/cli/mcp/tools/cicd-tools.ts +0 -17
- package/src/cli/mcp/tools/context7-tools.ts +0 -114
- package/src/cli/mcp/tools/deploy-tool.ts +0 -136
- package/src/cli/mcp/tools/dev-tools.ts +0 -475
- package/src/cli/mcp/tools/helpers.ts +0 -141
- package/src/cli/mcp/tools/introspection-tools.ts +0 -53
- package/src/cli/mcp/tools/project-tools.ts +0 -412
- package/src/cli/mcp/tools/run-lint-tool.ts +0 -109
- package/src/cli/mcp/tools/run-tests-tool.ts +0 -117
- package/src/cli/mcp/tools/scaffold-tools.ts +0 -433
- package/src/cli/mcp/tools/skill-tools.ts +0 -194
- package/src/cli/mcp/tools.ts +0 -267
- package/src/cli/router.ts +0 -299
- package/src/cli/shared/animation.ts +0 -25
- package/src/cli/shared/args.ts +0 -269
- package/src/cli/shared/config.ts +0 -258
- package/src/cli/shared/constants.ts +0 -29
- package/src/cli/shared/ensure-content-processor.ts +0 -17
- package/src/cli/shared/handler-utils.ts +0 -17
- package/src/cli/shared/interactive.ts +0 -33
- package/src/cli/shared/json-output.ts +0 -96
- package/src/cli/shared/parsed-args.ts +0 -9
- package/src/cli/shared/project-source-context.ts +0 -96
- package/src/cli/shared/reserve-slug.ts +0 -114
- package/src/cli/shared/server-startup.ts +0 -119
- package/src/cli/shared/slug.ts +0 -12
- package/src/cli/shared/suggest.ts +0 -33
- package/src/cli/shared/types.ts +0 -36
- package/src/cli/shared/update-check.ts +0 -108
- package/src/cli/skills/core-skills.ts +0 -233
- package/src/cli/skills/loader.ts +0 -98
- package/src/cli/skills/types.ts +0 -44
- package/src/cli/sync/ignore.ts +0 -143
- package/src/cli/sync/index.ts +0 -21
- package/src/cli/sync/project-discovery.ts +0 -81
- package/src/cli/templates/feature-loader.ts +0 -201
- package/src/cli/templates/index.ts +0 -77
- package/src/cli/templates/integration-loader-helpers.ts +0 -49
- package/src/cli/templates/integration-loader.ts +0 -709
- package/src/cli/templates/loader.ts +0 -64
- package/src/cli/templates/manifest.js +0 -623
- package/src/cli/templates/types.ts +0 -78
- package/src/cli/ui/animated-text.ts +0 -89
- package/src/cli/ui/ansi.ts +0 -91
- package/src/cli/ui/box.ts +0 -1
- package/src/cli/ui/colors.ts +0 -240
- package/src/cli/ui/components/banner.ts +0 -170
- package/src/cli/ui/components/index.ts +0 -4
- package/src/cli/ui/components/multi-select.ts +0 -163
- package/src/cli/ui/components/shortcuts.ts +0 -30
- package/src/cli/ui/components/table.ts +0 -187
- package/src/cli/ui/constants.ts +0 -20
- package/src/cli/ui/dot-matrix.ts +0 -344
- package/src/cli/ui/index.ts +0 -7
- package/src/cli/ui/keyboard.ts +0 -138
- package/src/cli/ui/layout.ts +0 -142
- package/src/cli/ui/progress.ts +0 -246
- package/src/cli/ui/tui.ts +0 -311
- package/src/cli/utils/env-prompt.ts +0 -190
- package/src/cli/utils/fs.ts +0 -33
- package/src/cli/utils/git.ts +0 -58
- package/src/cli/utils/index.ts +0 -230
- package/src/cli/utils/package-manager.ts +0 -204
- package/src/cli/utils/project.ts +0 -16
- package/src/cli/utils/sanitize-job-output.ts +0 -23
- package/src/cli/utils/string.ts +0 -27
- package/src/cli/utils/terminal-select.ts +0 -371
- package/src/cli/utils/write-job-result.ts +0 -23
- package/src/deno.js +0 -429
- package/src/deps/esm.sh/@types/react-dom@19.2.3/client.d.ts +0 -105
- package/src/deps/esm.sh/@types/react-dom@19.2.3/server.d.ts +0 -183
- package/src/deps/esm.sh/@types/react-dom@19.2.3/static.d.ts +0 -153
- package/src/deps/esm.sh/@types/react@19.2.14/global.d.ts +0 -166
- package/src/deps/esm.sh/@types/react@19.2.14/index.d.ts +0 -4370
- package/src/deps/esm.sh/@types/react@19.2.14/jsx-dev-runtime.d.ts +0 -45
- package/src/deps/esm.sh/@types/react@19.2.14/jsx-runtime.d.ts +0 -36
- package/src/deps/esm.sh/@types/react@19.2.3/global.d.ts +0 -165
- package/src/deps/esm.sh/@types/react@19.2.3/index.d.ts +0 -4301
- package/src/deps/esm.sh/@types/scheduler@0.26.0/index.d.ts +0 -30
- package/src/deps/esm.sh/csstype@3.2.3/index.d.ts +0 -22569
- package/src/deps/esm.sh/react-dom@19.2.4/client.d.ts +0 -105
- package/src/deps/esm.sh/react-dom@19.2.4/client.js +0 -5
- package/src/deps/esm.sh/react-dom@19.2.4/server.d.ts +0 -183
- package/src/deps/esm.sh/react-dom@19.2.4/server.js +0 -5
- package/src/deps/esm.sh/react@19.2.4/jsx-dev-runtime.d.ts +0 -45
- package/src/deps/esm.sh/react@19.2.4/jsx-dev-runtime.js +0 -3
- package/src/deps/esm.sh/react@19.2.4/jsx-runtime.d.ts +0 -36
- package/src/deps/esm.sh/react@19.2.4/jsx-runtime.js +0 -3
- package/src/deps/esm.sh/react@19.2.4.d.ts +0 -4370
- package/src/deps/esm.sh/react@19.2.4.js +0 -3
- package/src/deps/esm.sh/scheduler@^0.27.0.d.ts +0 -30
- package/src/deps/esm.sh/scheduler@^0.27.0.js +0 -3
- package/src/deps/jsr.io/@std/assert/1.0.19/almost_equals.ts +0 -56
- package/src/deps/jsr.io/@std/assert/1.0.19/array_includes.ts +0 -76
- package/src/deps/jsr.io/@std/assert/1.0.19/assert.ts +0 -24
- package/src/deps/jsr.io/@std/assert/1.0.19/assertion_error.ts +0 -31
- package/src/deps/jsr.io/@std/assert/1.0.19/equal.ts +0 -226
- package/src/deps/jsr.io/@std/assert/1.0.19/equals.ts +0 -68
- package/src/deps/jsr.io/@std/assert/1.0.19/exists.ts +0 -31
- package/src/deps/jsr.io/@std/assert/1.0.19/fail.ts +0 -21
- package/src/deps/jsr.io/@std/assert/1.0.19/false.ts +0 -27
- package/src/deps/jsr.io/@std/assert/1.0.19/greater.ts +0 -30
- package/src/deps/jsr.io/@std/assert/1.0.19/greater_or_equal.ts +0 -36
- package/src/deps/jsr.io/@std/assert/1.0.19/instance_of.ts +0 -64
- package/src/deps/jsr.io/@std/assert/1.0.19/is_error.ts +0 -65
- package/src/deps/jsr.io/@std/assert/1.0.19/less.ts +0 -29
- package/src/deps/jsr.io/@std/assert/1.0.19/less_or_equal.ts +0 -36
- package/src/deps/jsr.io/@std/assert/1.0.19/match.ts +0 -30
- package/src/deps/jsr.io/@std/assert/1.0.19/mod.ts +0 -47
- package/src/deps/jsr.io/@std/assert/1.0.19/not_equals.ts +0 -43
- package/src/deps/jsr.io/@std/assert/1.0.19/not_instance_of.ts +0 -33
- package/src/deps/jsr.io/@std/assert/1.0.19/not_match.ts +0 -30
- package/src/deps/jsr.io/@std/assert/1.0.19/not_strict_equals.ts +0 -42
- package/src/deps/jsr.io/@std/assert/1.0.19/object_match.ts +0 -224
- package/src/deps/jsr.io/@std/assert/1.0.19/rejects.ts +0 -123
- package/src/deps/jsr.io/@std/assert/1.0.19/strict_equals.ts +0 -68
- package/src/deps/jsr.io/@std/assert/1.0.19/string_includes.ts +0 -30
- package/src/deps/jsr.io/@std/assert/1.0.19/throws.ts +0 -111
- package/src/deps/jsr.io/@std/assert/1.0.19/unimplemented.ts +0 -21
- package/src/deps/jsr.io/@std/assert/1.0.19/unreachable.ts +0 -21
- package/src/deps/jsr.io/@std/dotenv/0.225.6/mod.ts +0 -251
- package/src/deps/jsr.io/@std/dotenv/0.225.6/parse.ts +0 -110
- package/src/deps/jsr.io/@std/dotenv/0.225.6/stringify.ts +0 -48
- package/src/deps/jsr.io/@std/fmt/1.0.10/colors.ts +0 -1004
- package/src/deps/jsr.io/@std/fs/1.0.23/_create_walk_entry.ts +0 -47
- package/src/deps/jsr.io/@std/fs/1.0.23/_get_file_info_type.ts +0 -25
- package/src/deps/jsr.io/@std/fs/1.0.23/_is_same_path.ts +0 -23
- package/src/deps/jsr.io/@std/fs/1.0.23/_is_subdir.ts +0 -34
- package/src/deps/jsr.io/@std/fs/1.0.23/_to_path_string.ts +0 -17
- package/src/deps/jsr.io/@std/fs/1.0.23/copy.ts +0 -411
- package/src/deps/jsr.io/@std/fs/1.0.23/empty_dir.ts +0 -91
- package/src/deps/jsr.io/@std/fs/1.0.23/ensure_dir.ts +0 -110
- package/src/deps/jsr.io/@std/fs/1.0.23/ensure_file.ts +0 -100
- package/src/deps/jsr.io/@std/fs/1.0.23/ensure_link.ts +0 -66
- package/src/deps/jsr.io/@std/fs/1.0.23/ensure_symlink.ts +0 -181
- package/src/deps/jsr.io/@std/fs/1.0.23/eol.ts +0 -75
- package/src/deps/jsr.io/@std/fs/1.0.23/exists.ts +0 -316
- package/src/deps/jsr.io/@std/fs/1.0.23/expand_glob.ts +0 -564
- package/src/deps/jsr.io/@std/fs/1.0.23/mod.ts +0 -31
- package/src/deps/jsr.io/@std/fs/1.0.23/move.ts +0 -171
- package/src/deps/jsr.io/@std/fs/1.0.23/walk.ts +0 -956
- package/src/deps/jsr.io/@std/internal/1.0.13/_os.ts +0 -15
- package/src/deps/jsr.io/@std/internal/1.0.13/assertion_state.ts +0 -248
- package/src/deps/jsr.io/@std/internal/1.0.13/build_message.ts +0 -147
- package/src/deps/jsr.io/@std/internal/1.0.13/diff.ts +0 -317
- package/src/deps/jsr.io/@std/internal/1.0.13/diff_str.ts +0 -208
- package/src/deps/jsr.io/@std/internal/1.0.13/format.ts +0 -89
- package/src/deps/jsr.io/@std/internal/1.0.13/os.ts +0 -7
- package/src/deps/jsr.io/@std/internal/1.0.13/styles.ts +0 -233
- package/src/deps/jsr.io/@std/internal/1.0.13/types.ts +0 -30
- package/src/deps/jsr.io/@std/path/1.1.4/_common/assert_path.ts +0 -10
- package/src/deps/jsr.io/@std/path/1.1.4/_common/basename.ts +0 -53
- package/src/deps/jsr.io/@std/path/1.1.4/_common/common.ts +0 -26
- package/src/deps/jsr.io/@std/path/1.1.4/_common/constants.ts +0 -49
- package/src/deps/jsr.io/@std/path/1.1.4/_common/dirname.ts +0 -9
- package/src/deps/jsr.io/@std/path/1.1.4/_common/format.ts +0 -25
- package/src/deps/jsr.io/@std/path/1.1.4/_common/from_file_url.ts +0 -12
- package/src/deps/jsr.io/@std/path/1.1.4/_common/glob_to_reg_exp.ts +0 -295
- package/src/deps/jsr.io/@std/path/1.1.4/_common/normalize.ts +0 -9
- package/src/deps/jsr.io/@std/path/1.1.4/_common/normalize_string.ts +0 -74
- package/src/deps/jsr.io/@std/path/1.1.4/_common/relative.ts +0 -10
- package/src/deps/jsr.io/@std/path/1.1.4/_common/strip_trailing_separators.ts +0 -25
- package/src/deps/jsr.io/@std/path/1.1.4/_common/to_file_url.ts +0 -17
- package/src/deps/jsr.io/@std/path/1.1.4/basename.ts +0 -37
- package/src/deps/jsr.io/@std/path/1.1.4/common.ts +0 -35
- package/src/deps/jsr.io/@std/path/1.1.4/constants.ts +0 -18
- package/src/deps/jsr.io/@std/path/1.1.4/dirname.ts +0 -30
- package/src/deps/jsr.io/@std/path/1.1.4/extname.ts +0 -29
- package/src/deps/jsr.io/@std/path/1.1.4/format.ts +0 -30
- package/src/deps/jsr.io/@std/path/1.1.4/from_file_url.ts +0 -30
- package/src/deps/jsr.io/@std/path/1.1.4/glob_to_regexp.ts +0 -94
- package/src/deps/jsr.io/@std/path/1.1.4/is_absolute.ts +0 -30
- package/src/deps/jsr.io/@std/path/1.1.4/is_glob.ts +0 -49
- package/src/deps/jsr.io/@std/path/1.1.4/join.ts +0 -31
- package/src/deps/jsr.io/@std/path/1.1.4/join_globs.ts +0 -42
- package/src/deps/jsr.io/@std/path/1.1.4/mod.ts +0 -217
- package/src/deps/jsr.io/@std/path/1.1.4/normalize.ts +0 -33
- package/src/deps/jsr.io/@std/path/1.1.4/normalize_glob.ts +0 -45
- package/src/deps/jsr.io/@std/path/1.1.4/parse.ts +0 -44
- package/src/deps/jsr.io/@std/path/1.1.4/posix/_util.ts +0 -10
- package/src/deps/jsr.io/@std/path/1.1.4/posix/basename.ts +0 -62
- package/src/deps/jsr.io/@std/path/1.1.4/posix/common.ts +0 -26
- package/src/deps/jsr.io/@std/path/1.1.4/posix/constants.ts +0 -15
- package/src/deps/jsr.io/@std/path/1.1.4/posix/dirname.ts +0 -72
- package/src/deps/jsr.io/@std/path/1.1.4/posix/extname.ts +0 -96
- package/src/deps/jsr.io/@std/path/1.1.4/posix/format.ts +0 -31
- package/src/deps/jsr.io/@std/path/1.1.4/posix/from_file_url.ts +0 -25
- package/src/deps/jsr.io/@std/path/1.1.4/posix/glob_to_regexp.ts +0 -94
- package/src/deps/jsr.io/@std/path/1.1.4/posix/is_absolute.ts +0 -25
- package/src/deps/jsr.io/@std/path/1.1.4/posix/is_glob.ts +0 -4
- package/src/deps/jsr.io/@std/path/1.1.4/posix/join.ts +0 -46
- package/src/deps/jsr.io/@std/path/1.1.4/posix/join_globs.ts +0 -45
- package/src/deps/jsr.io/@std/path/1.1.4/posix/mod.ts +0 -44
- package/src/deps/jsr.io/@std/path/1.1.4/posix/normalize.ts +0 -63
- package/src/deps/jsr.io/@std/path/1.1.4/posix/normalize_glob.ts +0 -43
- package/src/deps/jsr.io/@std/path/1.1.4/posix/parse.ts +0 -121
- package/src/deps/jsr.io/@std/path/1.1.4/posix/relative.ts +0 -103
- package/src/deps/jsr.io/@std/path/1.1.4/posix/resolve.ts +0 -71
- package/src/deps/jsr.io/@std/path/1.1.4/posix/to_file_url.ts +0 -32
- package/src/deps/jsr.io/@std/path/1.1.4/posix/to_namespaced_path.ts +0 -21
- package/src/deps/jsr.io/@std/path/1.1.4/relative.ts +0 -32
- package/src/deps/jsr.io/@std/path/1.1.4/resolve.ts +0 -32
- package/src/deps/jsr.io/@std/path/1.1.4/to_file_url.ts +0 -30
- package/src/deps/jsr.io/@std/path/1.1.4/to_namespaced_path.ts +0 -31
- package/src/deps/jsr.io/@std/path/1.1.4/types.ts +0 -40
- package/src/deps/jsr.io/@std/path/1.1.4/windows/_util.ts +0 -28
- package/src/deps/jsr.io/@std/path/1.1.4/windows/basename.ts +0 -54
- package/src/deps/jsr.io/@std/path/1.1.4/windows/constants.ts +0 -15
- package/src/deps/jsr.io/@std/path/1.1.4/windows/dirname.ts +0 -118
- package/src/deps/jsr.io/@std/path/1.1.4/windows/extname.ts +0 -90
- package/src/deps/jsr.io/@std/path/1.1.4/windows/format.ts +0 -31
- package/src/deps/jsr.io/@std/path/1.1.4/windows/from_file_url.ts +0 -34
- package/src/deps/jsr.io/@std/path/1.1.4/windows/glob_to_regexp.ts +0 -92
- package/src/deps/jsr.io/@std/path/1.1.4/windows/is_absolute.ts +0 -40
- package/src/deps/jsr.io/@std/path/1.1.4/windows/join.ts +0 -78
- package/src/deps/jsr.io/@std/path/1.1.4/windows/join_globs.ts +0 -46
- package/src/deps/jsr.io/@std/path/1.1.4/windows/normalize.ts +0 -136
- package/src/deps/jsr.io/@std/path/1.1.4/windows/normalize_glob.ts +0 -43
- package/src/deps/jsr.io/@std/path/1.1.4/windows/parse.ts +0 -184
- package/src/deps/jsr.io/@std/path/1.1.4/windows/relative.ts +0 -128
- package/src/deps/jsr.io/@std/path/1.1.4/windows/resolve.ts +0 -178
- package/src/deps/jsr.io/@std/path/1.1.4/windows/to_file_url.ts +0 -38
- package/src/deps/jsr.io/@std/path/1.1.4/windows/to_namespaced_path.ts +0 -60
- package/src/deps/jsr.io/@std/testing/1.0.18/_test_suite.ts +0 -461
- package/src/deps/jsr.io/@std/testing/1.0.18/bdd.ts +0 -1294
- package/src/deps/jsr.io/@std/yaml/1.1.0/_chars.ts +0 -55
- package/src/deps/jsr.io/@std/yaml/1.1.0/_loader_state.ts +0 -1845
- package/src/deps/jsr.io/@std/yaml/1.1.0/_schema.ts +0 -183
- package/src/deps/jsr.io/@std/yaml/1.1.0/_type/binary.ts +0 -127
- package/src/deps/jsr.io/@std/yaml/1.1.0/_type/bool.ts +0 -37
- package/src/deps/jsr.io/@std/yaml/1.1.0/_type/float.ts +0 -112
- package/src/deps/jsr.io/@std/yaml/1.1.0/_type/int.ts +0 -174
- package/src/deps/jsr.io/@std/yaml/1.1.0/_type/map.ts +0 -17
- package/src/deps/jsr.io/@std/yaml/1.1.0/_type/merge.ts +0 -13
- package/src/deps/jsr.io/@std/yaml/1.1.0/_type/nil.ts +0 -27
- package/src/deps/jsr.io/@std/yaml/1.1.0/_type/omap.ts +0 -30
- package/src/deps/jsr.io/@std/yaml/1.1.0/_type/pairs.ts +0 -22
- package/src/deps/jsr.io/@std/yaml/1.1.0/_type/regexp.ts +0 -33
- package/src/deps/jsr.io/@std/yaml/1.1.0/_type/seq.ts +0 -13
- package/src/deps/jsr.io/@std/yaml/1.1.0/_type/set.ts +0 -17
- package/src/deps/jsr.io/@std/yaml/1.1.0/_type/str.ts +0 -12
- package/src/deps/jsr.io/@std/yaml/1.1.0/_type/timestamp.ts +0 -101
- package/src/deps/jsr.io/@std/yaml/1.1.0/_type/undefined.ts +0 -23
- package/src/deps/jsr.io/@std/yaml/1.1.0/_type.ts +0 -49
- package/src/deps/jsr.io/@std/yaml/1.1.0/_utils.ts +0 -16
- package/src/deps/jsr.io/@std/yaml/1.1.0/parse.ts +0 -125
- package/src/extensions/ext-auth-jwt/src/index.ts +0 -200
- package/src/extensions/ext-bundler-esbuild/src/binary.ts +0 -154
- package/src/extensions/ext-bundler-esbuild/src/es-module-lexer.ts +0 -34
- package/src/extensions/ext-bundler-esbuild/src/esbuild-bundler.ts +0 -122
- package/src/extensions/ext-bundler-esbuild/src/index.ts +0 -46
- package/src/extensions/ext-bundler-esbuild/src/plugin-adapter.ts +0 -66
- package/src/extensions/ext-bundler-esbuild/src/runtime.ts +0 -54
- package/src/extensions/ext-content-mdx/src/compiler/markdown-compile.ts +0 -127
- package/src/extensions/ext-content-mdx/src/compiler/mdx-compile.ts +0 -82
- package/src/extensions/ext-content-mdx/src/index.ts +0 -66
- package/src/extensions/ext-content-mdx/src/plugins/plugin-loader.ts +0 -21
- package/src/extensions/ext-content-mdx/src/plugins/rehype-node-positions.ts +0 -67
- package/src/extensions/ext-content-mdx/src/plugins/remark-headings.ts +0 -93
- package/src/extensions/ext-content-mdx/src/plugins/remark-mdx-utils.ts +0 -246
- package/src/extensions/ext-css-tailwind/src/index.ts +0 -82
- package/src/extensions/ext-db-sqlite/src/index.ts +0 -47
- package/src/extensions/ext-document-kreuzberg/src/index.ts +0 -97
- package/src/extensions/ext-document-kreuzberg/src/kreuzberg.ts +0 -56
- package/src/extensions/ext-llm-anthropic/src/anthropic-provider.ts +0 -336
- package/src/extensions/ext-llm-anthropic/src/anthropic-request-builder.ts +0 -540
- package/src/extensions/ext-llm-anthropic/src/anthropic-stream.ts +0 -310
- package/src/extensions/ext-llm-anthropic/src/index.ts +0 -38
- package/src/extensions/ext-llm-google/src/google-provider.ts +0 -305
- package/src/extensions/ext-llm-google/src/google-request-builder.ts +0 -394
- package/src/extensions/ext-llm-google/src/google-stream.ts +0 -188
- package/src/extensions/ext-llm-google/src/index.ts +0 -35
- package/src/extensions/ext-llm-openai/src/index.ts +0 -37
- package/src/extensions/ext-llm-openai/src/openai-chat-request-builder.ts +0 -220
- package/src/extensions/ext-llm-openai/src/openai-chat-stream.ts +0 -251
- package/src/extensions/ext-llm-openai/src/openai-provider.ts +0 -577
- package/src/extensions/ext-llm-openai/src/openai-responses-request-builder.ts +0 -298
- package/src/extensions/ext-llm-openai/src/openai-responses-stream.ts +0 -226
- package/src/extensions/ext-observability-opentelemetry/src/index.ts +0 -291
- package/src/extensions/ext-parser-babel/src/index.ts +0 -111
- package/src/extensions/ext-parser-babel/src/inject-node-positions.ts +0 -184
- package/src/extensions/ext-sandbox-shell-tools/src/index.ts +0 -43
- package/src/extensions/ext-schema-zod/src/adapter.ts +0 -238
- package/src/extensions/ext-schema-zod/src/index.ts +0 -39
- package/src/extensions/ext-schema-zod/src/json-schema.ts +0 -311
- package/src/react/jsx-dev-runtime.ts +0 -1
- package/src/react/jsx-runtime.ts +0 -1
- package/src/react/react-dom-client.ts +0 -2
- package/src/react/react-dom-server.ts +0 -2
- package/src/react/react.ts +0 -274
- package/src/src/agent/ag-ui/browser-chunk-encoder.ts +0 -58
- package/src/src/agent/ag-ui/browser-encoder.ts +0 -538
- package/src/src/agent/ag-ui/browser-finalize-tracker.ts +0 -65
- package/src/src/agent/ag-ui/browser-response-stream.ts +0 -175
- package/src/src/agent/ag-ui/chat-ui-chunk-browser-encoder.ts +0 -172
- package/src/src/agent/ag-ui/chunk-encoder-bridge.ts +0 -34
- package/src/src/agent/ag-ui/detached-start.ts +0 -425
- package/src/src/agent/ag-ui/forwarded-context.ts +0 -83
- package/src/src/agent/ag-ui/handler.ts +0 -388
- package/src/src/agent/ag-ui/host-support.ts +0 -244
- package/src/src/agent/ag-ui/request-shared.ts +0 -76
- package/src/src/agent/ag-ui/run-control.ts +0 -145
- package/src/src/agent/ag-ui/runtime-browser-response.ts +0 -41
- package/src/src/agent/ag-ui/runtime-chat-stream-encoder.ts +0 -359
- package/src/src/agent/ag-ui/runtime-event-encoder.ts +0 -71
- package/src/src/agent/ag-ui/runtime-handler.ts +0 -510
- package/src/src/agent/ag-ui/runtime-support.ts +0 -64
- package/src/src/agent/ag-ui/sse-parser.ts +0 -306
- package/src/src/agent/ag-ui/tool-shared.ts +0 -80
- package/src/src/agent/ag-ui/tracked-browser-response.ts +0 -42
- package/src/src/agent/artifacts/default-research-artifact-policy.ts +0 -171
- package/src/src/agent/artifacts/default-research-artifact-support.ts +0 -374
- package/src/src/agent/artifacts/slash-command-artifact-policy.ts +0 -212
- package/src/src/agent/child-run/execution-cleanup.ts +0 -33
- package/src/src/agent/child-run/execution-snapshot.ts +0 -154
- package/src/src/agent/child-run/execution-support.ts +0 -29
- package/src/src/agent/child-run/final-step-support.ts +0 -51
- package/src/src/agent/child-run/invoke-agent-child-runs.ts +0 -198
- package/src/src/agent/child-run/result-summary.ts +0 -135
- package/src/src/agent/composition/composition.ts +0 -176
- package/src/src/agent/composition/index.ts +0 -19
- package/src/src/agent/conversation/bootstrap.ts +0 -335
- package/src/src/agent/conversation/delegation-policy.ts +0 -299
- package/src/src/agent/conversation/durable.ts +0 -1387
- package/src/src/agent/conversation/hosted-lifecycle.ts +0 -215
- package/src/src/agent/conversation/hosted-terminal.ts +0 -235
- package/src/src/agent/conversation/root-run-context.ts +0 -143
- package/src/src/agent/conversation/root-run-lifecycle.ts +0 -184
- package/src/src/agent/conversation/run-chunk-mirror.ts +0 -385
- package/src/src/agent/conversation/run-context.ts +0 -22
- package/src/src/agent/conversation/run-event-normalization.ts +0 -228
- package/src/src/agent/conversation/run-event-preparation.ts +0 -76
- package/src/src/agent/conversation/run-events.ts +0 -242
- package/src/src/agent/conversation/run-mirror.ts +0 -271
- package/src/src/agent/conversation/run-stream-mirror.ts +0 -63
- package/src/src/agent/factory.ts +0 -303
- package/src/src/agent/hosted/ag-ui-chat-request.ts +0 -199
- package/src/src/agent/hosted/agent-project-steering.ts +0 -141
- package/src/src/agent/hosted/agent-run-lifecycle.ts +0 -161
- package/src/src/agent/hosted/chat-execution-lifecycle-types.ts +0 -11
- package/src/src/agent/hosted/chat-execution-runtime.ts +0 -694
- package/src/src/agent/hosted/chat-preparation.ts +0 -357
- package/src/src/agent/hosted/chat-request-parser.ts +0 -209
- package/src/src/agent/hosted/chat-request.ts +0 -114
- package/src/src/agent/hosted/chat-runtime-agent-adapter.ts +0 -88
- package/src/src/agent/hosted/chat-runtime-contract.ts +0 -101
- package/src/src/agent/hosted/chat-runtime-tool-assembly.ts +0 -185
- package/src/src/agent/hosted/child-artifact-support.ts +0 -201
- package/src/src/agent/hosted/child-bootstrap.ts +0 -75
- package/src/src/agent/hosted/child-execution-logging.ts +0 -114
- package/src/src/agent/hosted/child-fork-execution-runner.ts +0 -396
- package/src/src/agent/hosted/child-fork-instructions.ts +0 -91
- package/src/src/agent/hosted/child-fork-run-context.ts +0 -309
- package/src/src/agent/hosted/child-fork-runtime-start.ts +0 -84
- package/src/src/agent/hosted/child-fork-step-message-preparation.ts +0 -96
- package/src/src/agent/hosted/child-fork-stream-execution.ts +0 -626
- package/src/src/agent/hosted/child-fork-tool-sources.ts +0 -231
- package/src/src/agent/hosted/child-invoke-tool.ts +0 -90
- package/src/src/agent/hosted/child-lifecycle.ts +0 -352
- package/src/src/agent/hosted/child-mirror.ts +0 -272
- package/src/src/agent/hosted/child-pending-tool-lifecycle.ts +0 -191
- package/src/src/agent/hosted/child-requested-tools.ts +0 -411
- package/src/src/agent/hosted/child-status.ts +0 -167
- package/src/src/agent/hosted/child-steering-tools.ts +0 -92
- package/src/src/agent/hosted/child-stream-watchdog.ts +0 -146
- package/src/src/agent/hosted/child-tool-input.ts +0 -91
- package/src/src/agent/hosted/cloud-chat-execution-preparation.ts +0 -98
- package/src/src/agent/hosted/cloud-prepared-chat-execution-runtime.ts +0 -36
- package/src/src/agent/hosted/cloud-runtime-system-messages.ts +0 -79
- package/src/src/agent/hosted/default-chat-runtime.ts +0 -329
- package/src/src/agent/hosted/default-invoke-agent-tool.ts +0 -581
- package/src/src/agent/hosted/default-project-steering-refresh.ts +0 -227
- package/src/src/agent/hosted/durable-chat-run-start.ts +0 -246
- package/src/src/agent/hosted/durable-child-fork-execution.ts +0 -597
- package/src/src/agent/hosted/finalized-message.ts +0 -170
- package/src/src/agent/hosted/form-input-tool.ts +0 -128
- package/src/src/agent/hosted/lifecycle.ts +0 -159
- package/src/src/agent/hosted/prepared-chat-execution.ts +0 -188
- package/src/src/agent/hosted/project-remote-tool-source.ts +0 -324
- package/src/src/agent/hosted/project-steering-adapter.ts +0 -184
- package/src/src/agent/hosted/response-stream.ts +0 -87
- package/src/src/agent/hosted/runtime-request-config.ts +0 -112
- package/src/src/agent/hosted/runtime-state-resolver.ts +0 -141
- package/src/src/agent/hosted/stream-finalization.ts +0 -188
- package/src/src/agent/hosted/stream-terminal-error.ts +0 -81
- package/src/src/agent/hosted/trace-attributes.ts +0 -196
- package/src/src/agent/hosted/veryfront-api-tool-access.ts +0 -293
- package/src/src/agent/hosted/veryfront-cloud-agent-service.ts +0 -1050
- package/src/src/agent/index.ts +0 -1770
- package/src/src/agent/input/human-input.ts +0 -353
- package/src/src/agent/input/request-protocol.ts +0 -279
- package/src/src/agent/memory/index.ts +0 -21
- package/src/src/agent/memory/memory-interface.ts +0 -65
- package/src/src/agent/memory/memory.ts +0 -263
- package/src/src/agent/memory/redis.ts +0 -168
- package/src/src/agent/middleware/cache/cache.ts +0 -318
- package/src/src/agent/middleware/cache/index.ts +0 -7
- package/src/src/agent/middleware/chain.ts +0 -62
- package/src/src/agent/middleware/cost-tracking/index.ts +0 -13
- package/src/src/agent/middleware/cost-tracking/tracker.ts +0 -351
- package/src/src/agent/middleware/index.ts +0 -33
- package/src/src/agent/middleware/rate-limit/index.ts +0 -12
- package/src/src/agent/middleware/rate-limit/limiter.ts +0 -197
- package/src/src/agent/middleware/security/index.ts +0 -14
- package/src/src/agent/middleware/security/validator.ts +0 -288
- package/src/src/agent/project/agent-runtime.ts +0 -139
- package/src/src/agent/project/context.ts +0 -63
- package/src/src/agent/project/live-studio-mcp-tools.ts +0 -153
- package/src/src/agent/project/steering-mutation.ts +0 -126
- package/src/src/agent/react/index.ts +0 -39
- package/src/src/agent/react/use-agent.ts +0 -133
- package/src/src/agent/react/use-chat/browser-inference/browser-engine.ts +0 -89
- package/src/src/agent/react/use-chat/browser-inference/types.ts +0 -52
- package/src/src/agent/react/use-chat/browser-inference/worker-client.ts +0 -87
- package/src/src/agent/react/use-chat/browser-inference/worker-script.ts +0 -98
- package/src/src/agent/react/use-chat/index.ts +0 -27
- package/src/src/agent/react/use-chat/stream-protocol.ts +0 -1
- package/src/src/agent/react/use-chat/streaming/handler.ts +0 -429
- package/src/src/agent/react/use-chat/streaming/index.ts +0 -15
- package/src/src/agent/react/use-chat/streaming/parts-builder.ts +0 -91
- package/src/src/agent/react/use-chat/streaming/types.ts +0 -41
- package/src/src/agent/react/use-chat/types.ts +0 -115
- package/src/src/agent/react/use-chat/use-chat.ts +0 -576
- package/src/src/agent/react/use-chat/utils.ts +0 -13
- package/src/src/agent/react/use-completion.ts +0 -133
- package/src/src/agent/react/use-streaming.ts +0 -120
- package/src/src/agent/react/use-voice-input.ts +0 -249
- package/src/src/agent/runtime/ag-ui-contract.ts +0 -224
- package/src/src/agent/runtime/agent-definition-files.ts +0 -155
- package/src/src/agent/runtime/agent-definition.ts +0 -166
- package/src/src/agent/runtime/agent-invocation-contract.ts +0 -360
- package/src/src/agent/runtime/agent-markdown-adapter.ts +0 -31
- package/src/src/agent/runtime/builtin-skill-files.ts +0 -122
- package/src/src/agent/runtime/chat-stream-handler.ts +0 -577
- package/src/src/agent/runtime/client-profile.ts +0 -123
- package/src/src/agent/runtime/constants.ts +0 -29
- package/src/src/agent/runtime/default-provider-options.ts +0 -52
- package/src/src/agent/runtime/defaults.ts +0 -12
- package/src/src/agent/runtime/error-utils.ts +0 -32
- package/src/src/agent/runtime/index.ts +0 -1478
- package/src/src/agent/runtime/input-utils.ts +0 -48
- package/src/src/agent/runtime/load-skill-tool.ts +0 -239
- package/src/src/agent/runtime/message-adapter.ts +0 -480
- package/src/src/agent/runtime/message-file-url-refresh.ts +0 -120
- package/src/src/agent/runtime/message-preparation.ts +0 -58
- package/src/src/agent/runtime/model-resolution.ts +0 -113
- package/src/src/agent/runtime/model-tool-converter.ts +0 -97
- package/src/src/agent/runtime/project-files-client.ts +0 -256
- package/src/src/agent/runtime/project-skill-catalog.ts +0 -238
- package/src/src/agent/runtime/project-skill-loader.ts +0 -217
- package/src/src/agent/runtime/prompt-block.ts +0 -19
- package/src/src/agent/runtime/provider-native-tool-inventory.ts +0 -74
- package/src/src/agent/runtime/provider-native-tools.ts +0 -130
- package/src/src/agent/runtime/provider-tool-compat.ts +0 -304
- package/src/src/agent/runtime/repair-tool-call.ts +0 -50
- package/src/src/agent/runtime/resume-session.ts +0 -353
- package/src/src/agent/runtime/runtime-tool-builder.ts +0 -82
- package/src/src/agent/runtime/runtime-tool-errors.ts +0 -46
- package/src/src/agent/runtime/runtime-tool-types.ts +0 -121
- package/src/src/agent/runtime/skill-metadata.ts +0 -260
- package/src/src/agent/runtime/skill-prompt.ts +0 -61
- package/src/src/agent/runtime/sse-utils.ts +0 -51
- package/src/src/agent/runtime/text-generation-runtime-message-converter.ts +0 -269
- package/src/src/agent/runtime/text-generation-runtime-message-types.ts +0 -62
- package/src/src/agent/runtime/tool-helpers.ts +0 -428
- package/src/src/agent/runtime/tool-inventory.ts +0 -46
- package/src/src/agent/runtime/upload-url-client.ts +0 -95
- package/src/src/agent/schemas/agent.schema.ts +0 -181
- package/src/src/agent/schemas/index.ts +0 -38
- package/src/src/agent/schemas/tool.schema.ts +0 -11
- package/src/src/agent/service/abort-rejection-guard.ts +0 -162
- package/src/src/agent/service/auth.ts +0 -427
- package/src/src/agent/service/bootstrap.ts +0 -79
- package/src/src/agent/service/chat-handler.ts +0 -508
- package/src/src/agent/service/config.ts +0 -107
- package/src/src/agent/service/definition.ts +0 -386
- package/src/src/agent/service/detached-run-tracker.ts +0 -169
- package/src/src/agent/service/env-files.ts +0 -60
- package/src/src/agent/service/external-worker-client.ts +0 -401
- package/src/src/agent/service/mcp-server-config.ts +0 -112
- package/src/src/agent/service/node-runtime-infrastructure.ts +0 -71
- package/src/src/agent/service/node-telemetry.ts +0 -197
- package/src/src/agent/service/registration.ts +0 -395
- package/src/src/agent/service/request-auth-cache.ts +0 -36
- package/src/src/agent/service/response-like.ts +0 -28
- package/src/src/agent/service/routes.ts +0 -386
- package/src/src/agent/service/runtime.ts +0 -303
- package/src/src/agent/service/server.ts +0 -98
- package/src/src/agent/streaming/chat-ui-message-stream.ts +0 -596
- package/src/src/agent/streaming/data-stream.ts +0 -209
- package/src/src/agent/streaming/fork-runtime-stream.ts +0 -1187
- package/src/src/agent/streaming/mirrored-tool-chunk-state.ts +0 -276
- package/src/src/agent/streaming/tool-execution-data-event-bridge.ts +0 -79
- package/src/src/agent/testing/agent-tester.ts +0 -202
- package/src/src/agent/testing/durable-run-canaries/cli-runner.ts +0 -117
- package/src/src/agent/testing/durable-run-canaries/environment.ts +0 -27
- package/src/src/agent/testing/durable-run-canaries/index.ts +0 -36
- package/src/src/agent/testing/durable-run-canaries/runner.ts +0 -585
- package/src/src/agent/testing/durable-run-canaries/validation.ts +0 -87
- package/src/src/agent/testing/index.ts +0 -128
- package/src/src/agent/testing/live-evals/api-client.ts +0 -568
- package/src/src/agent/testing/live-evals/cli-runner.ts +0 -234
- package/src/src/agent/testing/live-evals/environment.ts +0 -31
- package/src/src/agent/testing/live-evals/formatting.ts +0 -106
- package/src/src/agent/testing/live-evals/index.ts +0 -99
- package/src/src/agent/testing/live-evals/metadata.ts +0 -167
- package/src/src/agent/testing/live-evals/performance.ts +0 -52
- package/src/src/agent/testing/live-evals/preflight.ts +0 -42
- package/src/src/agent/testing/live-evals/report.ts +0 -155
- package/src/src/agent/testing/live-evals/request.ts +0 -78
- package/src/src/agent/testing/live-evals/result.ts +0 -109
- package/src/src/agent/testing/live-evals/runner.ts +0 -629
- package/src/src/agent/types.ts +0 -275
- package/src/src/build/asset-pipeline/css-optimizer/types/index.ts +0 -92
- package/src/src/build/asset-pipeline/tailwind-processor/css-utils.ts +0 -13
- package/src/src/build/binary-plugin-includes.ts +0 -17
- package/src/src/build/bundler/code-splitter/build-context.ts +0 -97
- package/src/src/build/bundler/code-splitter/entry-points.ts +0 -31
- package/src/src/build/bundler/code-splitter/esbuild-plugin.ts +0 -38
- package/src/src/build/bundler/code-splitter/index.ts +0 -80
- package/src/src/build/bundler/code-splitter/manifest-builder.ts +0 -140
- package/src/src/build/bundler/code-splitter/splitter.ts +0 -98
- package/src/src/build/bundler/code-splitter/types.ts +0 -46
- package/src/src/build/bundler/index.ts +0 -39
- package/src/src/build/compiler/index.ts +0 -17
- package/src/src/build/compiler/mdx-compiler/code-generator.ts +0 -16
- package/src/src/build/compiler/mdx-compiler/compiler.ts +0 -53
- package/src/src/build/compiler/mdx-compiler/directory-compiler.ts +0 -39
- package/src/src/build/compiler/mdx-compiler/file-writer.ts +0 -25
- package/src/src/build/compiler/mdx-compiler/frontmatter-parser.ts +0 -94
- package/src/src/build/compiler/mdx-compiler/import-transformer.ts +0 -27
- package/src/src/build/compiler/mdx-compiler/index.ts +0 -10
- package/src/src/build/compiler/mdx-compiler/mdx-processor.ts +0 -32
- package/src/src/build/compiler/mdx-compiler/transpiler.ts +0 -15
- package/src/src/build/compiler/mdx-compiler/types.ts +0 -21
- package/src/src/build/compiler/mdx-compiler/validator.ts +0 -52
- package/src/src/build/compiler/mdx-compiler/watcher.ts +0 -57
- package/src/src/build/compiler/mdx-to-js.ts +0 -143
- package/src/src/build/embedded/preset.ts +0 -326
- package/src/src/build/index.ts +0 -11
- package/src/src/build/production-build/asset-generation.ts +0 -178
- package/src/src/build/production-build/build/build-cleanup.ts +0 -31
- package/src/src/build/production-build/build/build-executor.ts +0 -50
- package/src/src/build/production-build/build/build-initializer.ts +0 -58
- package/src/src/build/production-build/build/build-orchestrator.ts +0 -140
- package/src/src/build/production-build/build/build-setup.ts +0 -47
- package/src/src/build/production-build/build/code-splitter-orchestrator.ts +0 -43
- package/src/src/build/production-build/build/index.ts +0 -58
- package/src/src/build/production-build/build/output-generator.ts +0 -142
- package/src/src/build/production-build/build/route-collector.ts +0 -46
- package/src/src/build/production-build/client-runtime.ts +0 -376
- package/src/src/build/production-build/index.ts +0 -51
- package/src/src/build/production-build/manifest.ts +0 -126
- package/src/src/build/production-build/static-generation.ts +0 -256
- package/src/src/build/production-build/templates.ts +0 -17
- package/src/src/build/renderer/types/bundler-types.ts +0 -12
- package/src/src/cache/backend.ts +0 -32
- package/src/src/cache/backends/api.ts +0 -226
- package/src/src/cache/backends/disk.ts +0 -150
- package/src/src/cache/backends/factory.ts +0 -209
- package/src/src/cache/backends/helpers.ts +0 -7
- package/src/src/cache/backends/index.ts +0 -30
- package/src/src/cache/backends/memory.ts +0 -180
- package/src/src/cache/backends/redis.ts +0 -134
- package/src/src/cache/cache-key-builder.ts +0 -125
- package/src/src/cache/config-hash.ts +0 -64
- package/src/src/cache/dependency-graph.ts +0 -208
- package/src/src/cache/distributed-cache-init.ts +0 -144
- package/src/src/cache/index.ts +0 -37
- package/src/src/cache/keys/builders/file.ts +0 -70
- package/src/src/cache/keys/builders/github.ts +0 -34
- package/src/src/cache/keys/builders/module.ts +0 -94
- package/src/src/cache/keys/builders/render.ts +0 -125
- package/src/src/cache/keys/index.ts +0 -94
- package/src/src/cache/keys/prefixes.ts +0 -127
- package/src/src/cache/keys/utils.ts +0 -236
- package/src/src/cache/keys.ts +0 -7
- package/src/src/cache/module-cache.ts +0 -242
- package/src/src/cache/paths.ts +0 -140
- package/src/src/cache/registry.ts +0 -587
- package/src/src/cache/request-cache-batcher.ts +0 -144
- package/src/src/cache/schemas/cache-backend.schema.ts +0 -22
- package/src/src/cache/schemas/cache-key.schema.ts +0 -17
- package/src/src/cache/schemas/index.ts +0 -14
- package/src/src/cache/tokenizing-gateway.ts +0 -278
- package/src/src/cache/types.ts +0 -67
- package/src/src/channels/control-plane.ts +0 -455
- package/src/src/channels/invoke.ts +0 -447
- package/src/src/chat/ag-ui-helpers.ts +0 -139
- package/src/src/chat/ag-ui.ts +0 -882
- package/src/src/chat/chat-ui-message-helpers.ts +0 -232
- package/src/src/chat/conversation.ts +0 -941
- package/src/src/chat/final-step-fallback.ts +0 -957
- package/src/src/chat/hosted-ui-chunk-mapping.ts +0 -303
- package/src/src/chat/index.ts +0 -275
- package/src/src/chat/message-prep.ts +0 -873
- package/src/src/chat/protocol.ts +0 -358
- package/src/src/chat/provider-errors.ts +0 -218
- package/src/src/chat/stream-watchdog.ts +0 -229
- package/src/src/chat/types.ts +0 -632
- package/src/src/config/defaults.ts +0 -73
- package/src/src/config/define-config.ts +0 -68
- package/src/src/config/env.ts +0 -197
- package/src/src/config/environment-config.ts +0 -274
- package/src/src/config/index.ts +0 -69
- package/src/src/config/loader.ts +0 -528
- package/src/src/config/network-defaults.ts +0 -55
- package/src/src/config/runtime-config.ts +0 -216
- package/src/src/config/schemas/config.schema.ts +0 -695
- package/src/src/config/schemas/index.ts +0 -27
- package/src/src/data/data-fetcher.ts +0 -80
- package/src/src/data/data-fetching-cache.ts +0 -57
- package/src/src/data/helpers.ts +0 -9
- package/src/src/data/index.ts +0 -17
- package/src/src/data/schemas/data.schema.ts +0 -70
- package/src/src/data/schemas/index.ts +0 -20
- package/src/src/data/server-data-fetcher.ts +0 -185
- package/src/src/data/static-data-fetcher.ts +0 -310
- package/src/src/data/static-paths-fetcher.ts +0 -35
- package/src/src/data/types.ts +0 -28
- package/src/src/discovery/discovery-engine.ts +0 -229
- package/src/src/discovery/discovery-utils.ts +0 -44
- package/src/src/discovery/file-discovery.ts +0 -109
- package/src/src/discovery/handlers/agent-handler.ts +0 -30
- package/src/src/discovery/handlers/index.ts +0 -13
- package/src/src/discovery/handlers/prompt-handler.ts +0 -21
- package/src/src/discovery/handlers/resource-handler.ts +0 -22
- package/src/src/discovery/handlers/runtime-agent-markdown-handler.ts +0 -78
- package/src/src/discovery/handlers/skill-handler.ts +0 -123
- package/src/src/discovery/handlers/task-handler.ts +0 -19
- package/src/src/discovery/handlers/tool-handler.ts +0 -32
- package/src/src/discovery/handlers/workflow-handler.ts +0 -23
- package/src/src/discovery/import-rewriter.ts +0 -209
- package/src/src/discovery/index.ts +0 -34
- package/src/src/discovery/module-import.ts +0 -36
- package/src/src/discovery/project-discovery-config.ts +0 -89
- package/src/src/discovery/provider-config-validator.ts +0 -33
- package/src/src/discovery/transpiler.ts +0 -244
- package/src/src/discovery/types.ts +0 -69
- package/src/src/embedding/chunk.ts +0 -66
- package/src/src/embedding/embedding.ts +0 -67
- package/src/src/embedding/index.ts +0 -50
- package/src/src/embedding/model-resolution.ts +0 -46
- package/src/src/embedding/rag-store.ts +0 -432
- package/src/src/embedding/react/use-uploads.ts +0 -102
- package/src/src/embedding/resolve.ts +0 -157
- package/src/src/embedding/types.ts +0 -115
- package/src/src/embedding/upload-handler.ts +0 -287
- package/src/src/embedding/upload-loader.ts +0 -85
- package/src/src/embedding/vector-store.ts +0 -283
- package/src/src/embedding/veryfront-cloud/provider.ts +0 -55
- package/src/src/embedding/veryfront-cloud/rag-store.ts +0 -600
- package/src/src/errors/catalog/build-errors.ts +0 -98
- package/src/src/errors/catalog/config-errors.ts +0 -105
- package/src/src/errors/catalog/deployment-errors.ts +0 -44
- package/src/src/errors/catalog/dev-errors.ts +0 -40
- package/src/src/errors/catalog/factory.ts +0 -24
- package/src/src/errors/catalog/general-errors.ts +0 -71
- package/src/src/errors/catalog/index.ts +0 -64
- package/src/src/errors/catalog/module-errors.ts +0 -106
- package/src/src/errors/catalog/route-errors.ts +0 -69
- package/src/src/errors/catalog/rsc-errors.ts +0 -82
- package/src/src/errors/catalog/runtime-errors.ts +0 -96
- package/src/src/errors/catalog/server-errors.ts +0 -95
- package/src/src/errors/catalog/types.ts +0 -15
- package/src/src/errors/error-context.ts +0 -172
- package/src/src/errors/error-handlers.ts +0 -85
- package/src/src/errors/error-registry-helpers.ts +0 -23
- package/src/src/errors/error-registry.ts +0 -824
- package/src/src/errors/http-error.ts +0 -157
- package/src/src/errors/index.ts +0 -197
- package/src/src/errors/logging.ts +0 -109
- package/src/src/errors/middleware/cli-error-boundary.ts +0 -248
- package/src/src/errors/middleware/http-error-boundary.ts +0 -133
- package/src/src/errors/middleware/index.ts +0 -13
- package/src/src/errors/middleware/wrap-unknown.ts +0 -117
- package/src/src/errors/tracing.ts +0 -90
- package/src/src/errors/types.ts +0 -147
- package/src/src/errors/user-friendly/error-catalog.ts +0 -126
- package/src/src/errors/user-friendly/error-formatter.ts +0 -93
- package/src/src/errors/user-friendly/error-identifier.ts +0 -41
- package/src/src/errors/user-friendly/error-wrapper.ts +0 -26
- package/src/src/errors/user-friendly/index.ts +0 -10
- package/src/src/errors/veryfront-error.ts +0 -148
- package/src/src/extensions/auth/auth-provider.ts +0 -96
- package/src/src/extensions/auth/index.ts +0 -15
- package/src/src/extensions/builtin-extensions.ts +0 -161
- package/src/src/extensions/bundler/bundler.ts +0 -289
- package/src/src/extensions/bundler/helper.ts +0 -58
- package/src/src/extensions/bundler/index.ts +0 -49
- package/src/src/extensions/bundler/module-lexer.ts +0 -46
- package/src/src/extensions/cache/cache-store.ts +0 -27
- package/src/src/extensions/cache/index.ts +0 -10
- package/src/src/extensions/cache/token-cache-store.ts +0 -58
- package/src/src/extensions/capabilities.ts +0 -93
- package/src/src/extensions/compat/index.ts +0 -15
- package/src/src/extensions/compat/native-services.ts +0 -83
- package/src/src/extensions/content/content-processor.ts +0 -98
- package/src/src/extensions/content/index.ts +0 -17
- package/src/src/extensions/contracts.ts +0 -42
- package/src/src/extensions/css/css-processor.ts +0 -61
- package/src/src/extensions/css/index.ts +0 -15
- package/src/src/extensions/database/database-client.ts +0 -35
- package/src/src/extensions/database/index.ts +0 -9
- package/src/src/extensions/discovery.ts +0 -250
- package/src/src/extensions/errors.ts +0 -39
- package/src/src/extensions/factory-loader.ts +0 -76
- package/src/src/extensions/index.ts +0 -91
- package/src/src/extensions/llm/embedding-provider.ts +0 -39
- package/src/src/extensions/llm/index.ts +0 -17
- package/src/src/extensions/llm/llm-provider-registry.ts +0 -50
- package/src/src/extensions/llm/llm-provider.ts +0 -54
- package/src/src/extensions/loader.ts +0 -282
- package/src/src/extensions/observability/index.ts +0 -17
- package/src/src/extensions/observability/node-telemetry-provider.ts +0 -45
- package/src/src/extensions/observability/tracing-exporter.ts +0 -77
- package/src/src/extensions/orchestrate.ts +0 -194
- package/src/src/extensions/parser/code-parser.ts +0 -96
- package/src/src/extensions/parser/index.ts +0 -18
- package/src/src/extensions/recommendations.ts +0 -30
- package/src/src/extensions/sandbox/index.ts +0 -18
- package/src/src/extensions/sandbox/shell-tools.ts +0 -42
- package/src/src/extensions/schema/index.ts +0 -28
- package/src/src/extensions/schema/json-schema.ts +0 -23
- package/src/src/extensions/schema/schema-validator.ts +0 -257
- package/src/src/extensions/types.ts +0 -68
- package/src/src/extensions/validation.ts +0 -188
- package/src/src/fs/index.ts +0 -51
- package/src/src/html/dev-scripts.ts +0 -92
- package/src/src/html/html-detection.ts +0 -8
- package/src/src/html/html-escape.ts +0 -20
- package/src/src/html/html-injection.ts +0 -174
- package/src/src/html/html-shell-generator.ts +0 -427
- package/src/src/html/hydration-script-builder/dev-client-renderer.ts +0 -18
- package/src/src/html/hydration-script-builder/dev-component-manifest.ts +0 -15
- package/src/src/html/hydration-script-builder/dev-error-logger.ts +0 -58
- package/src/src/html/hydration-script-builder/dev-scripts.ts +0 -47
- package/src/src/html/hydration-script-builder/hydration-data-generator.ts +0 -79
- package/src/src/html/hydration-script-builder/index.ts +0 -21
- package/src/src/html/hydration-script-builder/prod-hydration.ts +0 -41
- package/src/src/html/hydration-script-builder/prod-scripts.ts +0 -25
- package/src/src/html/hydration-script-builder/templates/index.ts +0 -10
- package/src/src/html/hydration-script-builder/templates/loader.ts +0 -107
- package/src/src/html/hydration-script-builder/templates/renderer.ts +0 -166
- package/src/src/html/hydration-script-builder/templates/router.ts +0 -679
- package/src/src/html/hydration-script-builder/templates/spa-renderer.ts +0 -103
- package/src/src/html/hydration-script-builder/types.ts +0 -29
- package/src/src/html/index.ts +0 -35
- package/src/src/html/metadata-builder.ts +0 -46
- package/src/src/html/metadata-extraction.ts +0 -54
- package/src/src/html/nonce-injection.ts +0 -330
- package/src/src/html/schemas/html.schema.ts +0 -88
- package/src/src/html/schemas/index.ts +0 -15
- package/src/src/html/styles-builder/candidate-extractor.ts +0 -71
- package/src/src/html/styles-builder/content-version.ts +0 -20
- package/src/src/html/styles-builder/css-hash-cache.ts +0 -393
- package/src/src/html/styles-builder/css-pregeneration.ts +0 -331
- package/src/src/html/styles-builder/dev-styles.ts +0 -23
- package/src/src/html/styles-builder/index.ts +0 -21
- package/src/src/html/styles-builder/plugin-loader.ts +0 -252
- package/src/src/html/styles-builder/prepared-project-css-cache.ts +0 -228
- package/src/src/html/styles-builder/project-css-cache.ts +0 -321
- package/src/src/html/styles-builder/style-scope-profile.ts +0 -164
- package/src/src/html/styles-builder/tailwind-compiler-cache.ts +0 -231
- package/src/src/html/styles-builder/tailwind-compiler-utils.ts +0 -199
- package/src/src/html/styles-builder/tailwind-compiler.ts +0 -276
- package/src/src/html/styles-builder/tailwind-plugin-allowlist.ts +0 -51
- package/src/src/html/tag-generators.ts +0 -120
- package/src/src/html/types.ts +0 -5
- package/src/src/html/utils.ts +0 -327
- package/src/src/index.ts +0 -78
- package/src/src/integrations/_data.ts +0 -82
- package/src/src/integrations/index.ts +0 -72
- package/src/src/integrations/remote-tools.ts +0 -243
- package/src/src/integrations/schema.ts +0 -204
- package/src/src/integrations/types.ts +0 -62
- package/src/src/internal-agents/ag-ui-sse.ts +0 -141
- package/src/src/internal-agents/control-plane-auth.ts +0 -92
- package/src/src/internal-agents/request-body.ts +0 -43
- package/src/src/internal-agents/run-stream.ts +0 -422
- package/src/src/internal-agents/runtime-owner.ts +0 -189
- package/src/src/internal-agents/schema.ts +0 -347
- package/src/src/internal-agents/session-manager.ts +0 -206
- package/src/src/issues/core.ts +0 -385
- package/src/src/issues/index.ts +0 -46
- package/src/src/issues/mcp.ts +0 -289
- package/src/src/issues/schemas/index.ts +0 -45
- package/src/src/issues/schemas/issue.schema.ts +0 -152
- package/src/src/jobs/index.ts +0 -101
- package/src/src/jobs/jobs-client.ts +0 -503
- package/src/src/jobs/runtime-env.ts +0 -106
- package/src/src/jobs/schemas.ts +0 -429
- package/src/src/markdown/index.ts +0 -20
- package/src/src/mcp/annotations.ts +0 -10
- package/src/src/mcp/elicitation.ts +0 -42
- package/src/src/mcp/http-transport.ts +0 -162
- package/src/src/mcp/index.ts +0 -65
- package/src/src/mcp/registry.ts +0 -41
- package/src/src/mcp/schemas/index.ts +0 -16
- package/src/src/mcp/schemas/mcp.schema.ts +0 -59
- package/src/src/mcp/server.ts +0 -790
- package/src/src/mcp/session.ts +0 -31
- package/src/src/mcp/sse.ts +0 -19
- package/src/src/mcp/task-store.ts +0 -137
- package/src/src/mcp/types.ts +0 -39
- package/src/src/mdx/index.ts +0 -23
- package/src/src/middleware/builtin/logger.ts +0 -246
- package/src/src/middleware/builtin/security/rate-limit.ts +0 -123
- package/src/src/middleware/builtin/security/redis-rate-limit.ts +0 -109
- package/src/src/middleware/builtin/security/types.ts +0 -34
- package/src/src/middleware/builtin/timeout.ts +0 -104
- package/src/src/middleware/builtin/types.ts +0 -38
- package/src/src/middleware/core/context.ts +0 -60
- package/src/src/middleware/core/index.ts +0 -16
- package/src/src/middleware/core/pipeline/composer.ts +0 -36
- package/src/src/middleware/core/pipeline/executor.ts +0 -67
- package/src/src/middleware/core/pipeline/index.ts +0 -10
- package/src/src/middleware/core/pipeline/pipeline.ts +0 -97
- package/src/src/middleware/core/pipeline/types.ts +0 -1
- package/src/src/middleware/core/types.ts +0 -27
- package/src/src/middleware/index.ts +0 -67
- package/src/src/modules/component-registry/index.ts +0 -8
- package/src/src/modules/component-registry/registry.ts +0 -246
- package/src/src/modules/import-map/default-import-map.ts +0 -62
- package/src/src/modules/import-map/index.ts +0 -14
- package/src/src/modules/import-map/loader.ts +0 -173
- package/src/src/modules/import-map/merger.ts +0 -19
- package/src/src/modules/import-map/preloader.ts +0 -47
- package/src/src/modules/import-map/resolver.ts +0 -106
- package/src/src/modules/import-map/transformer.ts +0 -49
- package/src/src/modules/import-map/types.ts +0 -8
- package/src/src/modules/index.ts +0 -43
- package/src/src/modules/loader-shared/patterns.ts +0 -40
- package/src/src/modules/manifest/route-module-manifest.ts +0 -241
- package/src/src/modules/module-resolver.ts +0 -145
- package/src/src/modules/react-loader/component-loader.ts +0 -90
- package/src/src/modules/react-loader/css-import-collector.ts +0 -50
- package/src/src/modules/react-loader/extract-component.ts +0 -33
- package/src/src/modules/react-loader/index.ts +0 -14
- package/src/src/modules/react-loader/path-resolver.ts +0 -27
- package/src/src/modules/react-loader/ssr-module-loader/cache/index.ts +0 -27
- package/src/src/modules/react-loader/ssr-module-loader/cache/memory.ts +0 -278
- package/src/src/modules/react-loader/ssr-module-loader/cache/redis.ts +0 -65
- package/src/src/modules/react-loader/ssr-module-loader/concurrency/index.ts +0 -7
- package/src/src/modules/react-loader/ssr-module-loader/concurrency/semaphore.ts +0 -67
- package/src/src/modules/react-loader/ssr-module-loader/constants.ts +0 -64
- package/src/src/modules/react-loader/ssr-module-loader/cross-project-import-loader.ts +0 -152
- package/src/src/modules/react-loader/ssr-module-loader/http-bundle-helpers.ts +0 -196
- package/src/src/modules/react-loader/ssr-module-loader/import-rewriter.ts +0 -170
- package/src/src/modules/react-loader/ssr-module-loader/index.ts +0 -48
- package/src/src/modules/react-loader/ssr-module-loader/loader-helpers.ts +0 -37
- package/src/src/modules/react-loader/ssr-module-loader/loader.ts +0 -685
- package/src/src/modules/react-loader/ssr-module-loader/preflight-imports.ts +0 -45
- package/src/src/modules/react-loader/ssr-module-loader/ssr-cache-manager.ts +0 -318
- package/src/src/modules/react-loader/ssr-module-loader/ssr-circuit-breaker.ts +0 -75
- package/src/src/modules/react-loader/ssr-module-loader/ssr-dependency-validator.ts +0 -172
- package/src/src/modules/react-loader/ssr-module-loader/tmp-paths.ts +0 -44
- package/src/src/modules/react-loader/ssr-module-loader/types.ts +0 -42
- package/src/src/modules/react-loader/ssr-module-loader/vf-module-resolver.ts +0 -117
- package/src/src/modules/react-loader/temp-directory.ts +0 -58
- package/src/src/modules/react-loader/types.ts +0 -26
- package/src/src/modules/react-loader/unified-loader.ts +0 -134
- package/src/src/modules/server/api-server.ts +0 -57
- package/src/src/modules/server/index.ts +0 -15
- package/src/src/modules/server/module-batch-handler.ts +0 -450
- package/src/src/modules/server/module-server.ts +0 -697
- package/src/src/modules/server/rate-limiter.ts +0 -26
- package/src/src/modules/server/ssr-import-rewriter.ts +0 -116
- package/src/src/modules/server/websocket-handler.ts +0 -133
- package/src/src/oauth/handlers/callback-handler.ts +0 -176
- package/src/src/oauth/handlers/index.ts +0 -18
- package/src/src/oauth/handlers/init-handler.ts +0 -237
- package/src/src/oauth/index.ts +0 -83
- package/src/src/oauth/providers/atlassian.ts +0 -67
- package/src/src/oauth/providers/base.ts +0 -410
- package/src/src/oauth/providers/common.ts +0 -361
- package/src/src/oauth/providers/google.ts +0 -71
- package/src/src/oauth/providers/index.ts +0 -66
- package/src/src/oauth/providers/microsoft.ts +0 -85
- package/src/src/oauth/schemas/index.ts +0 -22
- package/src/src/oauth/schemas/oauth.schema.ts +0 -106
- package/src/src/oauth/token-store/index.ts +0 -8
- package/src/src/oauth/token-store/memory.ts +0 -92
- package/src/src/oauth/types.ts +0 -49
- package/src/src/observability/auto-instrument/configurator.ts +0 -12
- package/src/src/observability/auto-instrument/http-instrumentation.ts +0 -208
- package/src/src/observability/auto-instrument/index.ts +0 -19
- package/src/src/observability/auto-instrument/orchestrator.ts +0 -60
- package/src/src/observability/auto-instrument/react-instrumentation.ts +0 -78
- package/src/src/observability/auto-instrument/types.ts +0 -46
- package/src/src/observability/auto-instrument/wrappers.ts +0 -107
- package/src/src/observability/error-collector.ts +0 -390
- package/src/src/observability/file-log-subscriber.ts +0 -187
- package/src/src/observability/index.ts +0 -130
- package/src/src/observability/instruments/build-instruments.ts +0 -36
- package/src/src/observability/instruments/cache-instruments.ts +0 -65
- package/src/src/observability/instruments/data-instruments.ts +0 -32
- package/src/src/observability/instruments/error-instruments.ts +0 -54
- package/src/src/observability/instruments/http-instruments.ts +0 -34
- package/src/src/observability/instruments/index.ts +0 -16
- package/src/src/observability/instruments/instruments-factory.ts +0 -71
- package/src/src/observability/instruments/memory-instruments.ts +0 -72
- package/src/src/observability/instruments/render-instruments.ts +0 -32
- package/src/src/observability/instruments/rsc-instruments.ts +0 -65
- package/src/src/observability/log-buffer.ts +0 -217
- package/src/src/observability/metrics/config.ts +0 -96
- package/src/src/observability/metrics/index.ts +0 -142
- package/src/src/observability/metrics/manager.ts +0 -134
- package/src/src/observability/metrics/recorder.ts +0 -139
- package/src/src/observability/metrics/types.ts +0 -81
- package/src/src/observability/request-profiler.ts +0 -140
- package/src/src/observability/simple-metrics/index.ts +0 -88
- package/src/src/observability/simple-metrics/metrics-recorder.ts +0 -327
- package/src/src/observability/simple-metrics/metrics-state.ts +0 -135
- package/src/src/observability/simple-metrics/observability-loader.ts +0 -53
- package/src/src/observability/simple-metrics/otel-instruments.ts +0 -93
- package/src/src/observability/simple-metrics/types.ts +0 -81
- package/src/src/observability/tracing/api-shim.ts +0 -409
- package/src/src/observability/tracing/config.ts +0 -77
- package/src/src/observability/tracing/context-propagation.ts +0 -88
- package/src/src/observability/tracing/index.ts +0 -134
- package/src/src/observability/tracing/manager.ts +0 -129
- package/src/src/observability/tracing/otlp-setup.ts +0 -251
- package/src/src/observability/tracing/service-tracer.ts +0 -267
- package/src/src/observability/tracing/span-names.ts +0 -111
- package/src/src/observability/tracing/span-operations.ts +0 -102
- package/src/src/observability/tracing/types.ts +0 -47
- package/src/src/platform/adapters/base.ts +0 -186
- package/src/src/platform/adapters/bun.ts +0 -20
- package/src/src/platform/adapters/deno.ts +0 -1
- package/src/src/platform/adapters/detect.ts +0 -84
- package/src/src/platform/adapters/fallback-wrapper.ts +0 -146
- package/src/src/platform/adapters/fs/cache/file-cache.ts +0 -420
- package/src/src/platform/adapters/fs/cache/size-estimator.ts +0 -7
- package/src/src/platform/adapters/fs/cache/types.ts +0 -20
- package/src/src/platform/adapters/fs/factory.ts +0 -70
- package/src/src/platform/adapters/fs/github/adapter.ts +0 -152
- package/src/src/platform/adapters/fs/github/directory-operations.ts +0 -61
- package/src/src/platform/adapters/fs/github/github-api-client.ts +0 -230
- package/src/src/platform/adapters/fs/github/index.ts +0 -34
- package/src/src/platform/adapters/fs/github/path-utils.ts +0 -9
- package/src/src/platform/adapters/fs/github/read-operations.ts +0 -175
- package/src/src/platform/adapters/fs/github/schemas/github-api.schema.ts +0 -89
- package/src/src/platform/adapters/fs/github/schemas/index.ts +0 -19
- package/src/src/platform/adapters/fs/github/stat-operations.ts +0 -266
- package/src/src/platform/adapters/fs/github/types.ts +0 -109
- package/src/src/platform/adapters/fs/index.ts +0 -33
- package/src/src/platform/adapters/fs/integration.ts +0 -99
- package/src/src/platform/adapters/fs/shared-types.ts +0 -11
- package/src/src/platform/adapters/fs/veryfront/adapter-content-context.ts +0 -149
- package/src/src/platform/adapters/fs/veryfront/adapter-helpers.ts +0 -43
- package/src/src/platform/adapters/fs/veryfront/adapter.ts +0 -774
- package/src/src/platform/adapters/fs/veryfront/api-search-circuit-breaker.ts +0 -30
- package/src/src/platform/adapters/fs/veryfront/base-operations.ts +0 -13
- package/src/src/platform/adapters/fs/veryfront/cache-keys.ts +0 -41
- package/src/src/platform/adapters/fs/veryfront/content-metrics.ts +0 -208
- package/src/src/platform/adapters/fs/veryfront/default-invalidation-callbacks.ts +0 -66
- package/src/src/platform/adapters/fs/veryfront/directory-operations.ts +0 -162
- package/src/src/platform/adapters/fs/veryfront/extension-priority.ts +0 -18
- package/src/src/platform/adapters/fs/veryfront/file-list-access.ts +0 -109
- package/src/src/platform/adapters/fs/veryfront/file-list-index.ts +0 -259
- package/src/src/platform/adapters/fs/veryfront/in-flight-dedupe.ts +0 -63
- package/src/src/platform/adapters/fs/veryfront/index.ts +0 -8
- package/src/src/platform/adapters/fs/veryfront/invalidation-state.ts +0 -130
- package/src/src/platform/adapters/fs/veryfront/multi-project-adapter.ts +0 -266
- package/src/src/platform/adapters/fs/veryfront/path-normalizer.ts +0 -40
- package/src/src/platform/adapters/fs/veryfront/proxy-manager.ts +0 -621
- package/src/src/platform/adapters/fs/veryfront/read-operations-helpers.ts +0 -111
- package/src/src/platform/adapters/fs/veryfront/read-operations.ts +0 -778
- package/src/src/platform/adapters/fs/veryfront/request-context.ts +0 -100
- package/src/src/platform/adapters/fs/veryfront/retry.ts +0 -62
- package/src/src/platform/adapters/fs/veryfront/schemas/index.ts +0 -7
- package/src/src/platform/adapters/fs/veryfront/schemas/proxy-manager.schema.ts +0 -16
- package/src/src/platform/adapters/fs/veryfront/stat-operations-helpers.ts +0 -74
- package/src/src/platform/adapters/fs/veryfront/stat-operations.ts +0 -556
- package/src/src/platform/adapters/fs/veryfront/types.ts +0 -142
- package/src/src/platform/adapters/fs/veryfront/websocket-manager-helpers.ts +0 -94
- package/src/src/platform/adapters/fs/veryfront/websocket-manager.ts +0 -893
- package/src/src/platform/adapters/fs/wrapper.ts +0 -264
- package/src/src/platform/adapters/mock.ts +0 -185
- package/src/src/platform/adapters/node.ts +0 -18
- package/src/src/platform/adapters/redis/deno.ts +0 -120
- package/src/src/platform/adapters/redis/index.ts +0 -21
- package/src/src/platform/adapters/redis/interface.ts +0 -42
- package/src/src/platform/adapters/redis/modules.ts +0 -54
- package/src/src/platform/adapters/redis/node.ts +0 -127
- package/src/src/platform/adapters/redis/types.ts +0 -84
- package/src/src/platform/adapters/redis/utils.ts +0 -14
- package/src/src/platform/adapters/registry.ts +0 -202
- package/src/src/platform/adapters/runtime/bun/adapter.ts +0 -42
- package/src/src/platform/adapters/runtime/bun/environment-adapter.ts +0 -17
- package/src/src/platform/adapters/runtime/bun/filesystem-adapter.ts +0 -203
- package/src/src/platform/adapters/runtime/bun/http-server.ts +0 -45
- package/src/src/platform/adapters/runtime/bun/index.ts +0 -20
- package/src/src/platform/adapters/runtime/bun/types.ts +0 -45
- package/src/src/platform/adapters/runtime/bun/websocket-adapter.ts +0 -47
- package/src/src/platform/adapters/runtime/deno/adapter.ts +0 -458
- package/src/src/platform/adapters/runtime/deno/index.ts +0 -7
- package/src/src/platform/adapters/runtime/node/adapter.ts +0 -42
- package/src/src/platform/adapters/runtime/node/environment-adapter.ts +0 -17
- package/src/src/platform/adapters/runtime/node/filesystem-adapter.ts +0 -147
- package/src/src/platform/adapters/runtime/node/http-server.ts +0 -196
- package/src/src/platform/adapters/runtime/node/index.ts +0 -18
- package/src/src/platform/adapters/runtime/node/types.ts +0 -42
- package/src/src/platform/adapters/runtime/node/websocket-adapter.ts +0 -191
- package/src/src/platform/adapters/runtime/shared/env-to-object.ts +0 -10
- package/src/src/platform/adapters/runtime/shared/node-based-shell-adapter.ts +0 -32
- package/src/src/platform/adapters/runtime/shared/server-lifecycle.ts +0 -31
- package/src/src/platform/adapters/runtime/shared/shared-watcher.ts +0 -59
- package/src/src/platform/adapters/runtime/shared/temp-dir.ts +0 -6
- package/src/src/platform/adapters/runtime/shared/watcher-queue.ts +0 -63
- package/src/src/platform/adapters/runtime-detection.ts +0 -10
- package/src/src/platform/adapters/veryfront-api-client/client.ts +0 -512
- package/src/src/platform/adapters/veryfront-api-client/index.ts +0 -50
- package/src/src/platform/adapters/veryfront-api-client/operations.ts +0 -576
- package/src/src/platform/adapters/veryfront-api-client/retry-handler.ts +0 -158
- package/src/src/platform/adapters/veryfront-api-client/schemas/api.schema.ts +0 -320
- package/src/src/platform/adapters/veryfront-api-client/schemas/index.ts +0 -41
- package/src/src/platform/adapters/veryfront-api-client/types.ts +0 -33
- package/src/src/platform/cloud/resolver.ts +0 -149
- package/src/src/platform/compat/console/deno.ts +0 -51
- package/src/src/platform/compat/console/index.ts +0 -90
- package/src/src/platform/compat/console/node.ts +0 -61
- package/src/src/platform/compat/console/types.ts +0 -18
- package/src/src/platform/compat/constants.ts +0 -14
- package/src/src/platform/compat/dynamic-import.ts +0 -15
- package/src/src/platform/compat/esbuild-init.ts +0 -77
- package/src/src/platform/compat/esbuild-shared.ts +0 -22
- package/src/src/platform/compat/esbuild.ts +0 -53
- package/src/src/platform/compat/framework-source-resolver.ts +0 -221
- package/src/src/platform/compat/fs.ts +0 -354
- package/src/src/platform/compat/http/deno-server.ts +0 -55
- package/src/src/platform/compat/http/factory.ts +0 -12
- package/src/src/platform/compat/http/index.ts +0 -45
- package/src/src/platform/compat/http/node-server.ts +0 -103
- package/src/src/platform/compat/http/node-types.ts +0 -34
- package/src/src/platform/compat/http/request-adapter.ts +0 -16
- package/src/src/platform/compat/http/responses.ts +0 -227
- package/src/src/platform/compat/http/types.ts +0 -23
- package/src/src/platform/compat/http/websocket.ts +0 -34
- package/src/src/platform/compat/kv/factory.ts +0 -60
- package/src/src/platform/compat/kv/index.ts +0 -10
- package/src/src/platform/compat/kv/memory-adapter.ts +0 -68
- package/src/src/platform/compat/kv/sqlite-adapter.ts +0 -113
- package/src/src/platform/compat/kv/types.ts +0 -31
- package/src/src/platform/compat/media-types.ts +0 -27
- package/src/src/platform/compat/opaque-deps.ts +0 -70
- package/src/src/platform/compat/path/basic-operations.ts +0 -59
- package/src/src/platform/compat/path/index.ts +0 -15
- package/src/src/platform/compat/path/parse-format.ts +0 -33
- package/src/src/platform/compat/path/resolution.ts +0 -136
- package/src/src/platform/compat/path/runtime.ts +0 -26
- package/src/src/platform/compat/path/security.ts +0 -29
- package/src/src/platform/compat/path/types.ts +0 -22
- package/src/src/platform/compat/path/url-conversion.ts +0 -58
- package/src/src/platform/compat/process/command.ts +0 -297
- package/src/src/platform/compat/process/env.ts +0 -227
- package/src/src/platform/compat/process/lifecycle.ts +0 -330
- package/src/src/platform/compat/process/runtime-process.ts +0 -27
- package/src/src/platform/compat/process.ts +0 -37
- package/src/src/platform/compat/react-paths.ts +0 -84
- package/src/src/platform/compat/runtime.ts +0 -112
- package/src/src/platform/compat/std/dotenv.ts +0 -87
- package/src/src/platform/compat/std/front-matter-yaml.ts +0 -39
- package/src/src/platform/compat/stdin.ts +0 -271
- package/src/src/platform/compat/transform.ts +0 -44
- package/src/src/platform/compat/vfs-paths.ts +0 -59
- package/src/src/platform/core-platform.ts +0 -199
- package/src/src/platform/environment.ts +0 -35
- package/src/src/platform/index.ts +0 -73
- package/src/src/prompt/factory.ts +0 -59
- package/src/src/prompt/index.ts +0 -22
- package/src/src/prompt/registry.ts +0 -29
- package/src/src/prompt/schemas/prompt.schema.ts +0 -15
- package/src/src/prompt/types.ts +0 -9
- package/src/src/provider/index.ts +0 -55
- package/src/src/provider/local/embedding-runtime-adapter.ts +0 -37
- package/src/src/provider/local/env.ts +0 -37
- package/src/src/provider/local/local-embedding-engine.ts +0 -93
- package/src/src/provider/local/local-engine.ts +0 -317
- package/src/src/provider/local/model-catalog.ts +0 -139
- package/src/src/provider/local/model-runtime-adapter.ts +0 -202
- package/src/src/provider/model-registry.ts +0 -369
- package/src/src/provider/runtime-inspection.ts +0 -25
- package/src/src/provider/runtime-loader/provider-embedding-responses.ts +0 -61
- package/src/src/provider/runtime-loader/provider-endpoints.ts +0 -50
- package/src/src/provider/runtime-loader/provider-http.ts +0 -207
- package/src/src/provider/runtime-loader/provider-records.ts +0 -7
- package/src/src/provider/runtime-loader/provider-request-init.ts +0 -101
- package/src/src/provider/runtime-loader/provider-sse.ts +0 -29
- package/src/src/provider/runtime-loader/provider-usage.ts +0 -135
- package/src/src/provider/runtime-loader/tool-input-status.ts +0 -242
- package/src/src/provider/runtime-loader.ts +0 -517
- package/src/src/provider/shared/index.ts +0 -70
- package/src/src/provider/types.ts +0 -40
- package/src/src/provider/veryfront-cloud/context.ts +0 -28
- package/src/src/provider/veryfront-cloud/model-catalog.ts +0 -224
- package/src/src/provider/veryfront-cloud/openai.ts +0 -34
- package/src/src/provider/veryfront-cloud/provider.ts +0 -82
- package/src/src/provider/veryfront-cloud/shared.ts +0 -139
- package/src/src/proxy/cache/index.ts +0 -78
- package/src/src/proxy/cache/memory-cache.ts +0 -136
- package/src/src/proxy/cache/resilient-cache.ts +0 -204
- package/src/src/proxy/cache/tracing-cache.ts +0 -77
- package/src/src/proxy/cache/types.ts +0 -48
- package/src/src/proxy/env.ts +0 -11
- package/src/src/proxy/error-response.ts +0 -32
- package/src/src/proxy/handler.ts +0 -905
- package/src/src/proxy/log-noise.ts +0 -42
- package/src/src/proxy/logger.ts +0 -406
- package/src/src/proxy/main.ts +0 -624
- package/src/src/proxy/oauth-client.ts +0 -93
- package/src/src/proxy/renderer-router.ts +0 -126
- package/src/src/proxy/retry.ts +0 -19
- package/src/src/proxy/server-resolver.ts +0 -151
- package/src/src/proxy/token-manager.ts +0 -193
- package/src/src/proxy/tracing.ts +0 -203
- package/src/src/proxy/version.ts +0 -21
- package/src/src/react/compat/config-generator.ts +0 -161
- package/src/src/react/compat/hooks-adapter.ts +0 -241
- package/src/src/react/compat/index.ts +0 -50
- package/src/src/react/compat/ssr-adapter/html-wrapper.ts +0 -40
- package/src/src/react/compat/ssr-adapter/index.ts +0 -13
- package/src/src/react/compat/ssr-adapter/response-builder.ts +0 -44
- package/src/src/react/compat/ssr-adapter/server-loader.ts +0 -98
- package/src/src/react/compat/ssr-adapter/stream-renderer.ts +0 -248
- package/src/src/react/compat/ssr-adapter/string-renderer.ts +0 -75
- package/src/src/react/compat/ssr-adapter/types.ts +0 -36
- package/src/src/react/compat/version-detector/compatibility-checker.ts +0 -55
- package/src/src/react/compat/version-detector/feature-detector.ts +0 -92
- package/src/src/react/compat/version-detector/index.ts +0 -22
- package/src/src/react/compat/version-detector/types.ts +0 -42
- package/src/src/react/compat/version-detector/version-cache.ts +0 -36
- package/src/src/react/compat/version-detector/version-parser.ts +0 -33
- package/src/src/react/components/AppWrapper.tsx +0 -44
- package/src/src/react/components/Head.tsx +0 -12
- package/src/src/react/components/LayoutComponent.tsx +0 -62
- package/src/src/react/components/MDXProvider.tsx +0 -21
- package/src/src/react/components/ProviderComponent.tsx +0 -58
- package/src/src/react/components/chat/agent-card.tsx +0 -162
- package/src/src/react/components/chat/chat/components/animations.tsx +0 -62
- package/src/src/react/components/chat/chat/components/attachment-pill.tsx +0 -110
- package/src/src/react/components/chat/chat/components/branch-picker.tsx +0 -70
- package/src/src/react/components/chat/chat/components/code-block.tsx +0 -83
- package/src/src/react/components/chat/chat/components/drop-zone.tsx +0 -49
- package/src/src/react/components/chat/chat/components/empty-state.tsx +0 -121
- package/src/src/react/components/chat/chat/components/inference-badge.tsx +0 -48
- package/src/src/react/components/chat/chat/components/inline-citation.tsx +0 -150
- package/src/src/react/components/chat/chat/components/message-actions.tsx +0 -88
- package/src/src/react/components/chat/chat/components/message-edit-form.tsx +0 -95
- package/src/src/react/components/chat/chat/components/message-feedback.tsx +0 -72
- package/src/src/react/components/chat/chat/components/quick-actions.tsx +0 -38
- package/src/src/react/components/chat/chat/components/reasoning.tsx +0 -56
- package/src/src/react/components/chat/chat/components/sidebar.tsx +0 -204
- package/src/src/react/components/chat/chat/components/skill-badge.tsx +0 -51
- package/src/src/react/components/chat/chat/components/sources.tsx +0 -99
- package/src/src/react/components/chat/chat/components/step-indicator.tsx +0 -33
- package/src/src/react/components/chat/chat/components/tab-switcher.tsx +0 -69
- package/src/src/react/components/chat/chat/components/tool-ui.tsx +0 -204
- package/src/src/react/components/chat/chat/components/upgrade-cta.tsx +0 -56
- package/src/src/react/components/chat/chat/components/uploads-panel.tsx +0 -143
- package/src/src/react/components/chat/chat/composition/api.tsx +0 -17
- package/src/src/react/components/chat/chat/composition/chat-composer.tsx +0 -205
- package/src/src/react/components/chat/chat/composition/chat-empty.tsx +0 -77
- package/src/src/react/components/chat/chat/composition/chat-if.tsx +0 -25
- package/src/src/react/components/chat/chat/composition/chat-message-list.tsx +0 -356
- package/src/src/react/components/chat/chat/composition/chat-root.tsx +0 -183
- package/src/src/react/components/chat/chat/composition/error-banner.tsx +0 -38
- package/src/src/react/components/chat/chat/composition/message.tsx +0 -343
- package/src/src/react/components/chat/chat/composition/model-avatar.tsx +0 -103
- package/src/src/react/components/chat/chat/contexts/chat-context.tsx +0 -81
- package/src/src/react/components/chat/chat/contexts/composer-context.tsx +0 -62
- package/src/src/react/components/chat/chat/contexts/index.ts +0 -36
- package/src/src/react/components/chat/chat/contexts/message-context.tsx +0 -52
- package/src/src/react/components/chat/chat/contexts/thread-list-context.tsx +0 -43
- package/src/src/react/components/chat/chat/hooks/use-threads.ts +0 -208
- package/src/src/react/components/chat/chat/index.tsx +0 -529
- package/src/src/react/components/chat/chat/utils/export.ts +0 -75
- package/src/src/react/components/chat/chat/utils/message-parts.ts +0 -151
- package/src/src/react/components/chat/chat-with-sidebar.tsx +0 -373
- package/src/src/react/components/chat/chat.tsx +0 -146
- package/src/src/react/components/chat/csp-nonce.ts +0 -13
- package/src/src/react/components/chat/error-boundary.tsx +0 -108
- package/src/src/react/components/chat/icons/index.ts +0 -328
- package/src/src/react/components/chat/markdown.tsx +0 -284
- package/src/src/react/components/chat/message.tsx +0 -166
- package/src/src/react/components/chat/model-selector.tsx +0 -262
- package/src/src/react/components/chat/theme.ts +0 -325
- package/src/src/react/components/optimized-image/OptimizedBackgroundImage.tsx +0 -40
- package/src/src/react/components/optimized-image/OptimizedImage.tsx +0 -81
- package/src/src/react/components/optimized-image/SimpleOptimizedImage.tsx +0 -43
- package/src/src/react/components/optimized-image/helpers.ts +0 -29
- package/src/src/react/components/optimized-image/index.ts +0 -12
- package/src/src/react/components/optimized-image/useOptimizedImage.ts +0 -32
- package/src/src/react/components/optimized-image/utils.tsx +0 -53
- package/src/src/react/context/index.tsx +0 -13
- package/src/src/react/fonts/index.ts +0 -103
- package/src/src/react/head-collector.ts +0 -143
- package/src/src/react/index.ts +0 -99
- package/src/src/react/primitives/agent-primitives.tsx +0 -87
- package/src/src/react/primitives/chat-container.tsx +0 -17
- package/src/src/react/primitives/index.ts +0 -41
- package/src/src/react/primitives/input-box.tsx +0 -226
- package/src/src/react/primitives/message-list.tsx +0 -83
- package/src/src/react/primitives/tool-primitives.tsx +0 -120
- package/src/src/react/router/index.tsx +0 -13
- package/src/src/react/runtime/core.ts +0 -303
- package/src/src/registry/project-scoped-registry-manager.ts +0 -211
- package/src/src/registry/scoped-registry-facade.ts +0 -51
- package/src/src/rendering/app-reserved.ts +0 -109
- package/src/src/rendering/app-route-resolver.ts +0 -170
- package/src/src/rendering/cache/cache-coordinator.ts +0 -195
- package/src/src/rendering/cache/index.ts +0 -24
- package/src/src/rendering/cache/stores/api-store.ts +0 -222
- package/src/src/rendering/cache/stores/filesystem-store.ts +0 -114
- package/src/src/rendering/cache/stores/index.ts +0 -11
- package/src/src/rendering/cache/stores/kv-store.ts +0 -103
- package/src/src/rendering/cache/stores/memory-store.ts +0 -97
- package/src/src/rendering/cache/stores/redis-store.ts +0 -267
- package/src/src/rendering/cache/types.ts +0 -21
- package/src/src/rendering/chunk-optimizer.ts +0 -273
- package/src/src/rendering/cleanup.ts +0 -27
- package/src/src/rendering/component-handling.ts +0 -172
- package/src/src/rendering/context/render-context.ts +0 -143
- package/src/src/rendering/element-validator/element-inspector.ts +0 -190
- package/src/src/rendering/element-validator/element-normalizer.ts +0 -73
- package/src/src/rendering/element-validator/index.ts +0 -18
- package/src/src/rendering/element-validator/primitive-checks.ts +0 -124
- package/src/src/rendering/element-validator/types.ts +0 -15
- package/src/src/rendering/element-validator/validator-core.ts +0 -46
- package/src/src/rendering/factories/service-factories.ts +0 -112
- package/src/src/rendering/index.ts +0 -49
- package/src/src/rendering/layouts/index.ts +0 -26
- package/src/src/rendering/layouts/layout-applicator.ts +0 -468
- package/src/src/rendering/layouts/layout-collector.ts +0 -401
- package/src/src/rendering/layouts/layout-compiler.ts +0 -75
- package/src/src/rendering/layouts/types.ts +0 -16
- package/src/src/rendering/layouts/utils/app-resolver.ts +0 -71
- package/src/src/rendering/layouts/utils/applicator.ts +0 -228
- package/src/src/rendering/layouts/utils/compiler.ts +0 -30
- package/src/src/rendering/layouts/utils/component-loader.ts +0 -273
- package/src/src/rendering/layouts/utils/discovery.ts +0 -243
- package/src/src/rendering/layouts/utils/ensure-valid-child.ts +0 -51
- package/src/src/rendering/layouts/utils/hash-calculator.ts +0 -51
- package/src/src/rendering/orchestrator/compiler-service.ts +0 -37
- package/src/src/rendering/orchestrator/config.ts +0 -120
- package/src/src/rendering/orchestrator/css-cache.ts +0 -81
- package/src/src/rendering/orchestrator/css-candidate-manifest.ts +0 -224
- package/src/src/rendering/orchestrator/file-resolver/candidates.ts +0 -25
- package/src/src/rendering/orchestrator/file-resolver/index.ts +0 -84
- package/src/src/rendering/orchestrator/html-head.ts +0 -74
- package/src/src/rendering/orchestrator/html-imported-css.ts +0 -81
- package/src/src/rendering/orchestrator/html-project-css.ts +0 -203
- package/src/src/rendering/orchestrator/html-types.ts +0 -27
- package/src/src/rendering/orchestrator/html.ts +0 -487
- package/src/src/rendering/orchestrator/layout.ts +0 -321
- package/src/src/rendering/orchestrator/lifecycle.ts +0 -297
- package/src/src/rendering/orchestrator/mdx.ts +0 -78
- package/src/src/rendering/orchestrator/module-collection.ts +0 -73
- package/src/src/rendering/orchestrator/module-loader/cache.ts +0 -29
- package/src/src/rendering/orchestrator/module-loader/esm-rewriter.ts +0 -102
- package/src/src/rendering/orchestrator/module-loader/index.ts +0 -569
- package/src/src/rendering/orchestrator/path-helpers.ts +0 -26
- package/src/src/rendering/orchestrator/pipeline-helpers.ts +0 -35
- package/src/src/rendering/orchestrator/pipeline.ts +0 -889
- package/src/src/rendering/orchestrator/ssr-orchestrator.ts +0 -278
- package/src/src/rendering/orchestrator/ssr.ts +0 -228
- package/src/src/rendering/orchestrator/types.ts +0 -92
- package/src/src/rendering/page-renderer.ts +0 -252
- package/src/src/rendering/page-rendering.ts +0 -182
- package/src/src/rendering/page-resolution/index.ts +0 -21
- package/src/src/rendering/page-resolution/page-resolver.ts +0 -238
- package/src/src/rendering/renderer-concurrency.ts +0 -195
- package/src/src/rendering/renderer.ts +0 -593
- package/src/src/rendering/router-detection.ts +0 -247
- package/src/src/rendering/rsc/client-module-strategy.ts +0 -100
- package/src/src/rendering/rsc/component-analyzer.ts +0 -148
- package/src/src/rendering/rsc/constants.ts +0 -6
- package/src/src/rendering/rsc/export-extractor.ts +0 -44
- package/src/src/rendering/rsc/production-optimizer.ts +0 -128
- package/src/src/rendering/rsc/server-action-guard.ts +0 -5
- package/src/src/rendering/rsc/server-renderer/component-detector.ts +0 -47
- package/src/src/rendering/rsc/server-renderer/html-generator.ts +0 -50
- package/src/src/rendering/rsc/server-renderer/index.ts +0 -18
- package/src/src/rendering/rsc/server-renderer/prop-serializer.ts +0 -69
- package/src/src/rendering/rsc/server-renderer/rsc-renderer.ts +0 -47
- package/src/src/rendering/rsc/server-renderer/tree-processor.ts +0 -126
- package/src/src/rendering/rsc/types.ts +0 -8
- package/src/src/rendering/script-page-handling.ts +0 -392
- package/src/src/rendering/shared/context-aware-cache.ts +0 -281
- package/src/src/rendering/shared/shared-services.ts +0 -112
- package/src/src/rendering/snippet-renderer.ts +0 -386
- package/src/src/rendering/ssr/component-registry.ts +0 -326
- package/src/src/rendering/ssr/index.ts +0 -10
- package/src/src/rendering/ssr/mdx-module-loader.ts +0 -185
- package/src/src/rendering/ssr/mdx-renderer.ts +0 -66
- package/src/src/rendering/ssr/types.ts +0 -14
- package/src/src/rendering/ssr-globals/context.ts +0 -70
- package/src/src/rendering/ssr-globals/dom-stubs.ts +0 -435
- package/src/src/rendering/ssr-globals/fetch-interceptor.ts +0 -123
- package/src/src/rendering/ssr-globals/index.ts +0 -79
- package/src/src/rendering/ssr-globals.ts +0 -13
- package/src/src/rendering/ssr-renderer.ts +0 -258
- package/src/src/rendering/styles.ts +0 -15
- package/src/src/rendering/utils/index.ts +0 -22
- package/src/src/rendering/utils/react-helpers.ts +0 -31
- package/src/src/rendering/utils/stream-utils.ts +0 -129
- package/src/src/rendering/virtual-module-system.ts +0 -106
- package/src/src/repositories/schemas/index.ts +0 -14
- package/src/src/repositories/schemas/repository.schema.ts +0 -41
- package/src/src/repositories/types.ts +0 -26
- package/src/src/resource/factory.ts +0 -64
- package/src/src/resource/index.ts +0 -30
- package/src/src/resource/registry.ts +0 -41
- package/src/src/resource/schemas/index.ts +0 -14
- package/src/src/resource/schemas/resource.schema.ts +0 -21
- package/src/src/resource/types.ts +0 -36
- package/src/src/routing/api/api-route-matcher.ts +0 -196
- package/src/src/routing/api/context-builder.ts +0 -69
- package/src/src/routing/api/handler.ts +0 -341
- package/src/src/routing/api/index.ts +0 -26
- package/src/src/routing/api/method-validator.ts +0 -26
- package/src/src/routing/api/module-loader/esbuild-plugin.ts +0 -192
- package/src/src/routing/api/module-loader/external-import-rewriter.ts +0 -461
- package/src/src/routing/api/module-loader/http-validator.ts +0 -38
- package/src/src/routing/api/module-loader/loader-helpers.ts +0 -68
- package/src/src/routing/api/module-loader/loader.ts +0 -672
- package/src/src/routing/api/module-loader/security-config.ts +0 -28
- package/src/src/routing/api/module-loader/types.ts +0 -36
- package/src/src/routing/api/openapi/path-utils.ts +0 -132
- package/src/src/routing/api/openapi/spec-generator.ts +0 -265
- package/src/src/routing/api/openapi/types.ts +0 -135
- package/src/src/routing/api/route-discovery.ts +0 -70
- package/src/src/routing/api/route-executor.ts +0 -450
- package/src/src/routing/client/dom-utils.ts +0 -174
- package/src/src/routing/client/index.ts +0 -33
- package/src/src/routing/client/navigation-handlers.ts +0 -127
- package/src/src/routing/client/page-loader.ts +0 -215
- package/src/src/routing/client/page-transition.ts +0 -99
- package/src/src/routing/client/types.ts +0 -39
- package/src/src/routing/client/viewport-prefetch.ts +0 -81
- package/src/src/routing/index.ts +0 -54
- package/src/src/routing/matchers/index.ts +0 -11
- package/src/src/routing/matchers/pattern-route-matcher.ts +0 -46
- package/src/src/routing/matchers/route-matcher.ts +0 -41
- package/src/src/routing/matchers/route-parser.ts +0 -69
- package/src/src/routing/matchers/types.ts +0 -13
- package/src/src/routing/registry/index.ts +0 -7
- package/src/src/routing/registry/registry.ts +0 -161
- package/src/src/routing/registry/types.ts +0 -7
- package/src/src/routing/slug-mapper/dynamic-route-matcher.ts +0 -54
- package/src/src/routing/slug-mapper/index.ts +0 -11
- package/src/src/routing/slug-mapper/path-candidate-generator.ts +0 -58
- package/src/src/routing/slug-mapper/slug-normalizer.ts +0 -25
- package/src/src/routing/slug-mapper/types.ts +0 -8
- package/src/src/runtime/runtime-bridge.ts +0 -614
- package/src/src/sandbox/agent-service-tools.ts +0 -184
- package/src/src/sandbox/config.ts +0 -20
- package/src/src/sandbox/index.ts +0 -68
- package/src/src/sandbox/lazy-sandbox.ts +0 -736
- package/src/src/sandbox/sandbox.ts +0 -454
- package/src/src/sandbox/shell-tools.ts +0 -217
- package/src/src/sandbox/types.ts +0 -95
- package/src/src/schemas/common.ts +0 -99
- package/src/src/schemas/define.ts +0 -45
- package/src/src/schemas/index.ts +0 -65
- package/src/src/schemas/json-schema.ts +0 -33
- package/src/src/schemas/lazy.ts +0 -65
- package/src/src/schemas/primitives.ts +0 -84
- package/src/src/security/client/html-sanitizer.ts +0 -120
- package/src/src/security/csrf/helpers.ts +0 -144
- package/src/src/security/csrf/index.ts +0 -8
- package/src/src/security/deno-permissions.ts +0 -55
- package/src/src/security/http/auth.ts +0 -129
- package/src/src/security/http/base-handler.ts +0 -183
- package/src/src/security/http/client-hints.ts +0 -41
- package/src/src/security/http/config.ts +0 -96
- package/src/src/security/http/cors/constants.ts +0 -6
- package/src/src/security/http/cors/headers.ts +0 -80
- package/src/src/security/http/cors/index.ts +0 -36
- package/src/src/security/http/cors/middleware.ts +0 -42
- package/src/src/security/http/cors/preflight.ts +0 -86
- package/src/src/security/http/cors/types.ts +0 -30
- package/src/src/security/http/cors/validators.ts +0 -191
- package/src/src/security/http/csrf/csrf-handler.ts +0 -101
- package/src/src/security/http/csrf/index.ts +0 -7
- package/src/src/security/http/middleware/config-loader.ts +0 -61
- package/src/src/security/http/middleware/cors-handler.ts +0 -21
- package/src/src/security/http/middleware/index.ts +0 -16
- package/src/src/security/http/middleware/types.ts +0 -29
- package/src/src/security/http/response/builder.ts +0 -62
- package/src/src/security/http/response/cache-handler.ts +0 -34
- package/src/src/security/http/response/constants.ts +0 -15
- package/src/src/security/http/response/fluent-methods.ts +0 -137
- package/src/src/security/http/response/index.ts +0 -11
- package/src/src/security/http/response/response-methods.ts +0 -85
- package/src/src/security/http/response/security-handler.ts +0 -225
- package/src/src/security/http/response/static-helpers.ts +0 -150
- package/src/src/security/http/response/types.ts +0 -33
- package/src/src/security/index.ts +0 -100
- package/src/src/security/input-validation/errors.ts +0 -17
- package/src/src/security/input-validation/handler.ts +0 -47
- package/src/src/security/input-validation/index.ts +0 -18
- package/src/src/security/input-validation/limits.ts +0 -110
- package/src/src/security/input-validation/parsers.ts +0 -104
- package/src/src/security/input-validation/sanitizers.ts +0 -48
- package/src/src/security/input-validation/types.ts +0 -34
- package/src/src/security/path-validation/canonical.ts +0 -63
- package/src/src/security/path-validation/index.ts +0 -156
- package/src/src/security/path-validation/normalization.ts +0 -62
- package/src/src/security/path-validation/presets.ts +0 -73
- package/src/src/security/path-validation/rules.ts +0 -67
- package/src/src/security/path-validation/types.ts +0 -38
- package/src/src/security/path-validation.ts +0 -19
- package/src/src/security/sandbox/project-worker.ts +0 -421
- package/src/src/security/sandbox/worker-permissions.ts +0 -77
- package/src/src/security/sandbox/worker-pool.ts +0 -466
- package/src/src/security/sandbox/worker-types.ts +0 -212
- package/src/src/security/secure-fs.ts +0 -322
- package/src/src/security/utils/constant-time.ts +0 -15
- package/src/src/server/bootstrap.ts +0 -530
- package/src/src/server/build-app-route-renderer.ts +0 -148
- package/src/src/server/build-routes.ts +0 -137
- package/src/src/server/build-service-worker.ts +0 -170
- package/src/src/server/build-types.ts +0 -48
- package/src/src/server/context/cache-invalidation.ts +0 -153
- package/src/src/server/context/enriched-context-types.ts +0 -65
- package/src/src/server/context/enriched-context.ts +0 -58
- package/src/src/server/context/request-context.ts +0 -54
- package/src/src/server/dev-server/error-overlay/error-formatter.ts +0 -82
- package/src/src/server/dev-server/error-overlay/html-template.ts +0 -479
- package/src/src/server/dev-server/error-overlay/index.ts +0 -21
- package/src/src/server/dev-server/error-overlay/overlay-renderer.ts +0 -15
- package/src/src/server/dev-server/error-overlay/stack-parser.ts +0 -25
- package/src/src/server/dev-server/file-watch-setup.ts +0 -276
- package/src/src/server/dev-server/file-watcher.ts +0 -100
- package/src/src/server/dev-server/hmr-types.ts +0 -11
- package/src/src/server/dev-server/index.ts +0 -19
- package/src/src/server/dev-server/middleware.ts +0 -244
- package/src/src/server/dev-server/request-handler.ts +0 -186
- package/src/src/server/dev-server/route-discovery.ts +0 -241
- package/src/src/server/dev-server/server.ts +0 -487
- package/src/src/server/dev-server/types.ts +0 -36
- package/src/src/server/dev-server.ts +0 -8
- package/src/src/server/dev-ui/manifest.js +0 -31
- package/src/src/server/handlers/dev/dashboard/api.ts +0 -690
- package/src/src/server/handlers/dev/dashboard/html-shell.ts +0 -43
- package/src/src/server/handlers/dev/dashboard/index.ts +0 -59
- package/src/src/server/handlers/dev/dashboard/ui-handler.ts +0 -104
- package/src/src/server/handlers/dev/debug-context.handler.ts +0 -106
- package/src/src/server/handlers/dev/endpoints.handler.ts +0 -58
- package/src/src/server/handlers/dev/files/dev-file.handler.ts +0 -93
- package/src/src/server/handlers/dev/files/esbuild-bundler.ts +0 -11
- package/src/src/server/handlers/dev/files/esbuild-plugins.ts +0 -282
- package/src/src/server/handlers/dev/files/index.ts +0 -12
- package/src/src/server/handlers/dev/files/path-validator.ts +0 -60
- package/src/src/server/handlers/dev/framework-candidates.generated.ts +0 -5458
- package/src/src/server/handlers/dev/projects/api.ts +0 -34
- package/src/src/server/handlers/dev/projects/html-shell.ts +0 -58
- package/src/src/server/handlers/dev/projects/index.ts +0 -74
- package/src/src/server/handlers/dev/projects/ui-handler.ts +0 -129
- package/src/src/server/handlers/dev/scripts/error-overlay.ts +0 -100
- package/src/src/server/handlers/dev/scripts/hmr-scripts.ts +0 -435
- package/src/src/server/handlers/dev/shared/not-found-response.ts +0 -9
- package/src/src/server/handlers/dev/shared/ui-module-transform.ts +0 -56
- package/src/src/server/handlers/dev/styles-candidate-scanner.ts +0 -152
- package/src/src/server/handlers/dev/styles-css.handler.ts +0 -465
- package/src/src/server/handlers/monitoring/client-log.handler.ts +0 -127
- package/src/src/server/handlers/monitoring/health.handler.ts +0 -96
- package/src/src/server/handlers/monitoring/memory.handler.ts +0 -178
- package/src/src/server/handlers/monitoring/metrics.handler.ts +0 -67
- package/src/src/server/handlers/preview/hmr-client-manager.ts +0 -91
- package/src/src/server/handlers/preview/hmr-client-message.ts +0 -63
- package/src/src/server/handlers/preview/hmr-message-router.ts +0 -92
- package/src/src/server/handlers/preview/hmr-ping-keepalive.ts +0 -49
- package/src/src/server/handlers/preview/hmr.handler.ts +0 -306
- package/src/src/server/handlers/preview/markdown-html-generator.ts +0 -187
- package/src/src/server/handlers/preview/markdown-preview.handler.ts +0 -186
- package/src/src/server/handlers/request/agent-run-cancel.handler.ts +0 -88
- package/src/src/server/handlers/request/agent-run-resume.handler.ts +0 -110
- package/src/src/server/handlers/request/agent-stream.handler.ts +0 -284
- package/src/src/server/handlers/request/api/api-handler-wrapper.ts +0 -159
- package/src/src/server/handlers/request/api/app-router-handler.ts +0 -68
- package/src/src/server/handlers/request/api/app-router-resolver.ts +0 -166
- package/src/src/server/handlers/request/api/index.ts +0 -12
- package/src/src/server/handlers/request/api/pages-api-handler.ts +0 -111
- package/src/src/server/handlers/request/api/project-discovery.ts +0 -133
- package/src/src/server/handlers/request/api/security-headers.ts +0 -54
- package/src/src/server/handlers/request/api/types.ts +0 -12
- package/src/src/server/handlers/request/channel-dispatch-request.ts +0 -113
- package/src/src/server/handlers/request/channel-invoke.handler.ts +0 -49
- package/src/src/server/handlers/request/css.handler.ts +0 -106
- package/src/src/server/handlers/request/internal-agents-list.handler.ts +0 -101
- package/src/src/server/handlers/request/lib-modules.handler.ts +0 -124
- package/src/src/server/handlers/request/module/batch-module-handler.ts +0 -37
- package/src/src/server/handlers/request/module/data-endpoint-handler.ts +0 -79
- package/src/src/server/handlers/request/module/index.ts +0 -14
- package/src/src/server/handlers/request/module/module-server-handler.ts +0 -70
- package/src/src/server/handlers/request/module/module.handler.ts +0 -94
- package/src/src/server/handlers/request/module/page-data-endpoint-handler.ts +0 -108
- package/src/src/server/handlers/request/module/page-module-handler.ts +0 -83
- package/src/src/server/handlers/request/module/virtual-module-handler.ts +0 -58
- package/src/src/server/handlers/request/openapi-docs.handler.ts +0 -91
- package/src/src/server/handlers/request/openapi.handler.ts +0 -169
- package/src/src/server/handlers/request/prod-hydration-module.handler.ts +0 -60
- package/src/src/server/handlers/request/rsc/index.ts +0 -82
- package/src/src/server/handlers/request/snippet.handler.ts +0 -137
- package/src/src/server/handlers/request/ssr/error-page-fallback.ts +0 -285
- package/src/src/server/handlers/request/ssr/etag-handler.ts +0 -18
- package/src/src/server/handlers/request/ssr/index.ts +0 -10
- package/src/src/server/handlers/request/ssr/not-found-fallback.ts +0 -105
- package/src/src/server/handlers/request/ssr/ssr-response-builder.ts +0 -88
- package/src/src/server/handlers/request/ssr/ssr.handler.ts +0 -303
- package/src/src/server/handlers/request/static.handler.ts +0 -125
- package/src/src/server/handlers/response/base.ts +0 -1
- package/src/src/server/handlers/response/cors.ts +0 -67
- package/src/src/server/handlers/response/not-found.ts +0 -156
- package/src/src/server/handlers/studio/bridge-modules.handler.ts +0 -131
- package/src/src/server/handlers/types.ts +0 -22
- package/src/src/server/handlers/utils/content-types.ts +0 -107
- package/src/src/server/handlers/utils/etag.ts +0 -44
- package/src/src/server/index.ts +0 -386
- package/src/src/server/node-handler.ts +0 -42
- package/src/src/server/production-server.ts +0 -466
- package/src/src/server/project-env/cache.ts +0 -101
- package/src/src/server/project-env/fetcher.ts +0 -80
- package/src/src/server/project-env/index.ts +0 -9
- package/src/src/server/project-env/storage.ts +0 -54
- package/src/src/server/reload-notifier.ts +0 -165
- package/src/src/server/runtime-handler/adapter-factory.ts +0 -216
- package/src/src/server/runtime-handler/environment-resolution.ts +0 -139
- package/src/src/server/runtime-handler/handler-context-builder.ts +0 -138
- package/src/src/server/runtime-handler/index.ts +0 -716
- package/src/src/server/runtime-handler/isolation.ts +0 -119
- package/src/src/server/runtime-handler/local-project-discovery.ts +0 -168
- package/src/src/server/runtime-handler/project-isolation.ts +0 -256
- package/src/src/server/runtime-handler/project-resolution.ts +0 -278
- package/src/src/server/runtime-handler/projects-handler.ts +0 -133
- package/src/src/server/runtime-handler/proxy-environment.ts +0 -15
- package/src/src/server/runtime-handler/request-lifecycle.ts +0 -129
- package/src/src/server/runtime-handler/request-tracker.ts +0 -259
- package/src/src/server/runtime-handler/request-utils.ts +0 -88
- package/src/src/server/runtime-handler/timeout-manager.ts +0 -83
- package/src/src/server/runtime-handler/tracing.ts +0 -93
- package/src/src/server/schemas/action.schema.ts +0 -15
- package/src/src/server/schemas/index.ts +0 -7
- package/src/src/server/service-server.ts +0 -598
- package/src/src/server/services/rendering/ssr.service.ts +0 -375
- package/src/src/server/services/rsc/endpoints/action-handler.ts +0 -63
- package/src/src/server/services/rsc/endpoints/action-parser.ts +0 -44
- package/src/src/server/services/rsc/endpoints/endpoint-router.ts +0 -315
- package/src/src/server/services/rsc/endpoints/handler-registry.ts +0 -85
- package/src/src/server/services/rsc/endpoints/index.ts +0 -12
- package/src/src/server/services/rsc/endpoints/rsc-bundles.generated.ts +0 -13
- package/src/src/server/services/rsc/endpoints/script-handlers.ts +0 -122
- package/src/src/server/services/rsc/endpoints/types.ts +0 -23
- package/src/src/server/services/rsc/orchestrators/component-resolver.ts +0 -76
- package/src/src/server/services/rsc/orchestrators/handler.ts +0 -58
- package/src/src/server/services/rsc/orchestrators/index.ts +0 -16
- package/src/src/server/services/rsc/orchestrators/manifest-handler.ts +0 -96
- package/src/src/server/services/rsc/orchestrators/page-handler.ts +0 -103
- package/src/src/server/services/rsc/orchestrators/render-handler.ts +0 -161
- package/src/src/server/services/rsc/orchestrators/stream-handler.ts +0 -94
- package/src/src/server/services/rsc/orchestrators/types.ts +0 -36
- package/src/src/server/services/static/index.ts +0 -10
- package/src/src/server/services/static/static-file.service.ts +0 -333
- package/src/src/server/shared/browser-module-bundler.ts +0 -65
- package/src/src/server/shared/index.ts +0 -12
- package/src/src/server/shared/renderer/adapter.ts +0 -410
- package/src/src/server/shared/renderer/index.ts +0 -14
- package/src/src/server/shared/renderer/memory/pressure.ts +0 -69
- package/src/src/server/shared/renderer-factory.ts +0 -14
- package/src/src/server/utils/chunk-utils.ts +0 -17
- package/src/src/server/utils/domain-lookup.ts +0 -265
- package/src/src/server/utils/domain-parser.ts +0 -238
- package/src/src/server/utils/error-html.ts +0 -163
- package/src/src/server/utils/proxy-trust.ts +0 -56
- package/src/src/server/utils/request-host.ts +0 -12
- package/src/src/skill/allowed-tools.ts +0 -100
- package/src/src/skill/executor.ts +0 -213
- package/src/src/skill/index.ts +0 -60
- package/src/src/skill/parser.ts +0 -207
- package/src/src/skill/path-safety.ts +0 -212
- package/src/src/skill/prompt-augmentation.ts +0 -48
- package/src/src/skill/registry.ts +0 -51
- package/src/src/skill/tools.ts +0 -221
- package/src/src/skill/types.ts +0 -107
- package/src/src/studio/bridge/bridge-bundle.generated.ts +0 -9
- package/src/src/studio/element-selector-injector.ts +0 -96
- package/src/src/studio/hash-utils.ts +0 -1
- package/src/src/task/discovery.ts +0 -294
- package/src/src/task/runner.ts +0 -90
- package/src/src/task/types.ts +0 -46
- package/src/src/testing/assert.ts +0 -352
- package/src/src/testing/bdd.ts +0 -619
- package/src/src/testing/deno-compat.ts +0 -206
- package/src/src/testing/index.ts +0 -92
- package/src/src/testing/init.ts +0 -30
- package/src/src/testing/isolation.ts +0 -177
- package/src/src/testing/timing.ts +0 -27
- package/src/src/testing/utils.ts +0 -44
- package/src/src/tool/context7.ts +0 -34
- package/src/src/tool/executor.ts +0 -22
- package/src/src/tool/factory.ts +0 -234
- package/src/src/tool/host-tools.ts +0 -127
- package/src/src/tool/index.ts +0 -118
- package/src/src/tool/project-scoped-remote-tools.ts +0 -292
- package/src/src/tool/registry.ts +0 -32
- package/src/src/tool/remote-mcp.ts +0 -346
- package/src/src/tool/remote-source-tools.ts +0 -62
- package/src/src/tool/result.ts +0 -23
- package/src/src/tool/schema/index.ts +0 -8
- package/src/src/tool/schema/json-schema.ts +0 -9
- package/src/src/tool/schema/zod-json-schema.ts +0 -87
- package/src/src/tool/sleep.ts +0 -102
- package/src/src/tool/tracing.ts +0 -70
- package/src/src/tool/types.ts +0 -173
- package/src/src/transforms/css-modules/naming.ts +0 -152
- package/src/src/transforms/esm/bundle-deps-validator.ts +0 -187
- package/src/src/transforms/esm/bundle-manifest-ttl.ts +0 -58
- package/src/src/transforms/esm/bundle-manifest-types.ts +0 -14
- package/src/src/transforms/esm/bundle-manifest.ts +0 -182
- package/src/src/transforms/esm/bundle-recovery.ts +0 -356
- package/src/src/transforms/esm/cached-bundle-validation.ts +0 -44
- package/src/src/transforms/esm/html-content.ts +0 -13
- package/src/src/transforms/esm/http-bundler.ts +0 -300
- package/src/src/transforms/esm/http-cache-helpers.ts +0 -188
- package/src/src/transforms/esm/http-cache-invariants.ts +0 -128
- package/src/src/transforms/esm/http-cache-state.ts +0 -72
- package/src/src/transforms/esm/http-cache-types.ts +0 -66
- package/src/src/transforms/esm/http-cache-wrapper.ts +0 -442
- package/src/src/transforms/esm/http-cache.ts +0 -439
- package/src/src/transforms/esm/import-parser.ts +0 -254
- package/src/src/transforms/esm/import-rewriter.ts +0 -194
- package/src/src/transforms/esm/in-flight-manager.ts +0 -145
- package/src/src/transforms/esm/index.ts +0 -23
- package/src/src/transforms/esm/lexer.ts +0 -178
- package/src/src/transforms/esm/package-registry.ts +0 -176
- package/src/src/transforms/esm/path-resolver.ts +0 -114
- package/src/src/transforms/esm/react-imports.ts +0 -55
- package/src/src/transforms/esm/source-url-embed.ts +0 -31
- package/src/src/transforms/esm/specifier-resolver.ts +0 -118
- package/src/src/transforms/esm/transform-cache.ts +0 -317
- package/src/src/transforms/esm/transform-utils.ts +0 -30
- package/src/src/transforms/esm/types.ts +0 -22
- package/src/src/transforms/esm-transform.ts +0 -13
- package/src/src/transforms/import-rewriter/index.ts +0 -82
- package/src/src/transforms/import-rewriter/parse-cache.ts +0 -195
- package/src/src/transforms/import-rewriter/strategies/alias-strategy.ts +0 -79
- package/src/src/transforms/import-rewriter/strategies/bare-strategy.ts +0 -96
- package/src/src/transforms/import-rewriter/strategies/cross-project-strategy.ts +0 -54
- package/src/src/transforms/import-rewriter/strategies/import-map-strategy.ts +0 -137
- package/src/src/transforms/import-rewriter/strategies/index.ts +0 -25
- package/src/src/transforms/import-rewriter/strategies/node-builtin-strategy.ts +0 -56
- package/src/src/transforms/import-rewriter/strategies/react-strategy.ts +0 -48
- package/src/src/transforms/import-rewriter/strategies/relative-strategy.ts +0 -71
- package/src/src/transforms/import-rewriter/strategies/url-strategy.ts +0 -23
- package/src/src/transforms/import-rewriter/strategies/vendor-strategy.ts +0 -47
- package/src/src/transforms/import-rewriter/strategies/veryfront-strategy.ts +0 -97
- package/src/src/transforms/import-rewriter/types.ts +0 -182
- package/src/src/transforms/import-rewriter/unified-rewriter.ts +0 -136
- package/src/src/transforms/import-rewriter/url-builder.ts +0 -145
- package/src/src/transforms/md/compiler/index.ts +0 -7
- package/src/src/transforms/md/compiler/md-compiler.ts +0 -61
- package/src/src/transforms/md/utils.ts +0 -20
- package/src/src/transforms/mdx/compiler/frontmatter-extractor.ts +0 -66
- package/src/src/transforms/mdx/compiler/import-rewriter.ts +0 -125
- package/src/src/transforms/mdx/compiler/index.ts +0 -63
- package/src/src/transforms/mdx/compiler/mdx-compiler.ts +0 -62
- package/src/src/transforms/mdx/esm-module-loader/cache/index.ts +0 -466
- package/src/src/transforms/mdx/esm-module-loader/cache/local-fs.ts +0 -10
- package/src/src/transforms/mdx/esm-module-loader/cache-format.ts +0 -204
- package/src/src/transforms/mdx/esm-module-loader/components/resolver.ts +0 -75
- package/src/src/transforms/mdx/esm-module-loader/constants.ts +0 -28
- package/src/src/transforms/mdx/esm-module-loader/import-transformer.ts +0 -215
- package/src/src/transforms/mdx/esm-module-loader/index.ts +0 -31
- package/src/src/transforms/mdx/esm-module-loader/jsx/runtime-loader.ts +0 -17
- package/src/src/transforms/mdx/esm-module-loader/jsx-cache.ts +0 -45
- package/src/src/transforms/mdx/esm-module-loader/loader-helpers.ts +0 -224
- package/src/src/transforms/mdx/esm-module-loader/loader.ts +0 -30
- package/src/src/transforms/mdx/esm-module-loader/metadata/extractor.ts +0 -105
- package/src/src/transforms/mdx/esm-module-loader/metadata/index.ts +0 -8
- package/src/src/transforms/mdx/esm-module-loader/metadata/string-parser.ts +0 -69
- package/src/src/transforms/mdx/esm-module-loader/missing-module.ts +0 -57
- package/src/src/transforms/mdx/esm-module-loader/module-fetcher/cache-keys.ts +0 -34
- package/src/src/transforms/mdx/esm-module-loader/module-fetcher/dependency-recovery.ts +0 -173
- package/src/src/transforms/mdx/esm-module-loader/module-fetcher/distributed-cache.ts +0 -260
- package/src/src/transforms/mdx/esm-module-loader/module-fetcher/framework-validator.ts +0 -294
- package/src/src/transforms/mdx/esm-module-loader/module-fetcher/http-fetcher.ts +0 -85
- package/src/src/transforms/mdx/esm-module-loader/module-fetcher/import-rewriter.ts +0 -108
- package/src/src/transforms/mdx/esm-module-loader/module-fetcher/index.ts +0 -499
- package/src/src/transforms/mdx/esm-module-loader/module-fetcher/module-cache.ts +0 -88
- package/src/src/transforms/mdx/esm-module-loader/module-fetcher/nested-imports.ts +0 -95
- package/src/src/transforms/mdx/esm-module-loader/module-fetcher/render-sessions.ts +0 -100
- package/src/src/transforms/mdx/esm-module-loader/module-writer.ts +0 -425
- package/src/src/transforms/mdx/esm-module-loader/resolution/file-finder.ts +0 -186
- package/src/src/transforms/mdx/esm-module-loader/types.ts +0 -83
- package/src/src/transforms/mdx/esm-module-loader/utils/hash.ts +0 -1
- package/src/src/transforms/mdx/esm-module-loader/utils/stub-module.ts +0 -111
- package/src/src/transforms/mdx/index.ts +0 -116
- package/src/src/transforms/mdx/mdx-cache-adapter.ts +0 -213
- package/src/src/transforms/mdx/module-loader/types.ts +0 -55
- package/src/src/transforms/mdx/types.ts +0 -83
- package/src/src/transforms/npm-import-rewrites.ts +0 -105
- package/src/src/transforms/pipeline/context.ts +0 -124
- package/src/src/transforms/pipeline/index.ts +0 -372
- package/src/src/transforms/pipeline/stages/compile.ts +0 -70
- package/src/src/transforms/pipeline/stages/finalize.ts +0 -20
- package/src/src/transforms/pipeline/stages/index.ts +0 -14
- package/src/src/transforms/pipeline/stages/parse.ts +0 -29
- package/src/src/transforms/pipeline/stages/resolve-imports.ts +0 -48
- package/src/src/transforms/pipeline/stages/ssr-css-strip.ts +0 -201
- package/src/src/transforms/pipeline/stages/ssr-http-cache.ts +0 -49
- package/src/src/transforms/pipeline/stages/ssr-http-stub.ts +0 -96
- package/src/src/transforms/pipeline/stages/ssr-vf-modules/constants.ts +0 -62
- package/src/src/transforms/pipeline/stages/ssr-vf-modules/import-finder.ts +0 -46
- package/src/src/transforms/pipeline/stages/ssr-vf-modules/index.ts +0 -174
- package/src/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.ts +0 -219
- package/src/src/transforms/pipeline/stages/ssr-vf-modules/transform.ts +0 -434
- package/src/src/transforms/pipeline/stages/ssr-vf-modules.ts +0 -5
- package/src/src/transforms/pipeline/types.ts +0 -143
- package/src/src/transforms/plugins/babel-node-positions.ts +0 -22
- package/src/src/transforms/shared/cross-project-import.ts +0 -27
- package/src/src/transforms/shared/framework-bundle-paths.ts +0 -12
- package/src/src/transforms/shared/package-specifier.ts +0 -29
- package/src/src/transforms/shared/vendor-export-name.ts +0 -7
- package/src/src/transforms/veryfront-module-urls.ts +0 -131
- package/src/src/types/app.ts +0 -6
- package/src/src/types/branded.ts +0 -28
- package/src/src/types/bundler.ts +0 -51
- package/src/src/types/entities/getEntityInfo.ts +0 -448
- package/src/src/types/entities.ts +0 -99
- package/src/src/types/global-guards.ts +0 -21
- package/src/src/types/hmr.ts +0 -20
- package/src/src/types/index.ts +0 -207
- package/src/src/types/rsc.ts +0 -40
- package/src/src/types/server.ts +0 -153
- package/src/src/utils/base64url.ts +0 -15
- package/src/src/utils/box.ts +0 -300
- package/src/src/utils/bundle-manifest-init.ts +0 -119
- package/src/src/utils/bundle-manifest-kv.ts +0 -58
- package/src/src/utils/bundle-manifest-redis.ts +0 -55
- package/src/src/utils/bundle-manifest.ts +0 -161
- package/src/src/utils/cache/eviction/eviction-manager.ts +0 -168
- package/src/src/utils/cache/keys/namespace.ts +0 -25
- package/src/src/utils/cache/stores/memory/entry-manager.ts +0 -98
- package/src/src/utils/cache/stores/memory/lru-cache-adapter.ts +0 -220
- package/src/src/utils/cache/stores/memory/lru-list-manager.ts +0 -58
- package/src/src/utils/cache/stores/memory/lru-node.ts +0 -20
- package/src/src/utils/cache/stores/memory/types.ts +0 -32
- package/src/src/utils/cache-dir.ts +0 -80
- package/src/src/utils/cache-file-ops.ts +0 -110
- package/src/src/utils/cache-namespace.ts +0 -42
- package/src/src/utils/case-utils.ts +0 -10
- package/src/src/utils/circuit-breaker.ts +0 -216
- package/src/src/utils/clsx.ts +0 -38
- package/src/src/utils/constants/buffers.ts +0 -30
- package/src/src/utils/constants/build.ts +0 -6
- package/src/src/utils/constants/cache.ts +0 -208
- package/src/src/utils/constants/cdn.ts +0 -88
- package/src/src/utils/constants/crypto.ts +0 -12
- package/src/src/utils/constants/env.ts +0 -26
- package/src/src/utils/constants/hash.ts +0 -2
- package/src/src/utils/constants/hmr.ts +0 -24
- package/src/src/utils/constants/html.ts +0 -7
- package/src/src/utils/constants/http.ts +0 -66
- package/src/src/utils/constants/index.ts +0 -305
- package/src/src/utils/constants/limits.ts +0 -31
- package/src/src/utils/constants/metrics.ts +0 -39
- package/src/src/utils/constants/network.ts +0 -31
- package/src/src/utils/constants/priorities.ts +0 -31
- package/src/src/utils/constants/retry.ts +0 -16
- package/src/src/utils/constants/security.ts +0 -14
- package/src/src/utils/constants/server.ts +0 -137
- package/src/src/utils/cookie-utils.ts +0 -25
- package/src/src/utils/env-loader.ts +0 -170
- package/src/src/utils/feature-flags.ts +0 -10
- package/src/src/utils/file-discovery.ts +0 -250
- package/src/src/utils/hash-utils.ts +0 -56
- package/src/src/utils/html-escape.ts +0 -8
- package/src/src/utils/id.ts +0 -36
- package/src/src/utils/import-lockfile.ts +0 -274
- package/src/src/utils/import-map.ts +0 -53
- package/src/src/utils/index.ts +0 -129
- package/src/src/utils/logger/components.ts +0 -93
- package/src/src/utils/logger/core.ts +0 -165
- package/src/src/utils/logger/index.ts +0 -56
- package/src/src/utils/logger/logger.ts +0 -612
- package/src/src/utils/logger/request-context.ts +0 -47
- package/src/src/utils/lru-wrapper.ts +0 -114
- package/src/src/utils/memoize.ts +0 -100
- package/src/src/utils/memory/index.ts +0 -23
- package/src/src/utils/memory/profiler.ts +0 -250
- package/src/src/utils/mime-types.ts +0 -153
- package/src/src/utils/parallel.ts +0 -153
- package/src/src/utils/path-utils.ts +0 -151
- package/src/src/utils/perf-timer.ts +0 -140
- package/src/src/utils/platform.ts +0 -21
- package/src/src/utils/redis-client.ts +0 -133
- package/src/src/utils/request-id.ts +0 -18
- package/src/src/utils/route-path-utils.ts +0 -210
- package/src/src/utils/runtime-guards.ts +0 -42
- package/src/src/utils/semaphore.ts +0 -107
- package/src/src/utils/singleflight.ts +0 -25
- package/src/src/utils/version-constant.ts +0 -3
- package/src/src/utils/version.ts +0 -51
- package/src/src/workflow/api/index.ts +0 -8
- package/src/src/workflow/api/workflow-client.ts +0 -181
- package/src/src/workflow/api.ts +0 -195
- package/src/src/workflow/backends/memory.ts +0 -451
- package/src/src/workflow/backends/redis/index.ts +0 -657
- package/src/src/workflow/backends/redis/types.ts +0 -57
- package/src/src/workflow/backends/redis.ts +0 -10
- package/src/src/workflow/backends/shared/requeue-run.ts +0 -18
- package/src/src/workflow/backends/types.ts +0 -142
- package/src/src/workflow/blob/types.ts +0 -36
- package/src/src/workflow/blob/veryfront-cloud-storage.ts +0 -534
- package/src/src/workflow/claude-code/agent.ts +0 -264
- package/src/src/workflow/claude-code/event-publisher.ts +0 -340
- package/src/src/workflow/claude-code/index.ts +0 -113
- package/src/src/workflow/claude-code/react/index.ts +0 -23
- package/src/src/workflow/claude-code/react/use-claude-code-stream.ts +0 -377
- package/src/src/workflow/claude-code/react/use-claude-code-websocket.ts +0 -534
- package/src/src/workflow/claude-code/tool.ts +0 -209
- package/src/src/workflow/claude-code/types.ts +0 -425
- package/src/src/workflow/claude-code/websocket-publisher.ts +0 -501
- package/src/src/workflow/claude-code/workspace-sync.ts +0 -647
- package/src/src/workflow/discovery/index.ts +0 -16
- package/src/src/workflow/discovery/workflow-discovery.ts +0 -238
- package/src/src/workflow/dsl/branch.ts +0 -74
- package/src/src/workflow/dsl/index.ts +0 -29
- package/src/src/workflow/dsl/loop.ts +0 -129
- package/src/src/workflow/dsl/map.ts +0 -46
- package/src/src/workflow/dsl/parallel.ts +0 -56
- package/src/src/workflow/dsl/step.ts +0 -67
- package/src/src/workflow/dsl/sub-workflow.ts +0 -39
- package/src/src/workflow/dsl/validation.ts +0 -8
- package/src/src/workflow/dsl/wait.ts +0 -77
- package/src/src/workflow/dsl/workflow.ts +0 -122
- package/src/src/workflow/executor/checkpoint-manager.ts +0 -160
- package/src/src/workflow/executor/dag/graph.ts +0 -127
- package/src/src/workflow/executor/dag/index.ts +0 -684
- package/src/src/workflow/executor/dag/types.ts +0 -40
- package/src/src/workflow/executor/dag/utils.ts +0 -17
- package/src/src/workflow/executor/dag-executor.ts +0 -14
- package/src/src/workflow/executor/step-executor.ts +0 -339
- package/src/src/workflow/executor/workflow-executor.ts +0 -635
- package/src/src/workflow/index.ts +0 -148
- package/src/src/workflow/react/index.ts +0 -17
- package/src/src/workflow/react/use-approval.ts +0 -150
- package/src/src/workflow/react/use-workflow-list.ts +0 -181
- package/src/src/workflow/react/use-workflow-start.ts +0 -75
- package/src/src/workflow/react/use-workflow.ts +0 -205
- package/src/src/workflow/registry.ts +0 -311
- package/src/src/workflow/runtime/approval-manager.ts +0 -325
- package/src/src/workflow/schemas/index.ts +0 -57
- package/src/src/workflow/schemas/workflow.schema.ts +0 -262
- package/src/src/workflow/types.ts +0 -359
- package/src/src/workflow/worker/dynamic-job-entrypoint.ts +0 -254
- package/src/src/workflow/worker/executors/index.ts +0 -17
- package/src/src/workflow/worker/executors/k8s.ts +0 -337
- package/src/src/workflow/worker/executors/process.ts +0 -332
- package/src/src/workflow/worker/executors/types.ts +0 -149
- package/src/src/workflow/worker/index.ts +0 -78
- package/src/src/workflow/worker/job-entrypoint.ts +0 -215
- package/src/src/workflow/worker/job-manager.ts +0 -488
- package/src/src/workflow/worker/shared.ts +0 -127
- package/src/src/workflow/worker/workflow-worker.ts +0 -326
|
@@ -1,623 +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/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 </Head>\n {children}\n </>\n );\n}\n",
|
|
9
|
-
"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\nexport default function WorkflowDashboard(): JSX.Element {\n const [topic, setTopic] = useState('')\n const { start, isStarting } = useWorkflowStart({ workflowId: 'content-pipeline' })\n const { runs, isLoading } = 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 }\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 → Write → Review → 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\">{wf.input?.topic || '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",
|
|
10
|
-
"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 waiting_for_approval: '\\u23F8',\n failed: '\\u2717',\n}\n\nexport default function WorkflowDetail(): 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\">← Back</a>\n\n <h1 className=\"text-2xl font-bold text-neutral-900 dark:text-white mb-1\">{run.input?.topic || '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 {/* Steps */}\n <div className=\"space-y-4 mb-8\">\n {run.steps?.map((step: any) => (\n <div key={step.id} 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[step.status] || '\\u25CB'}</span>\n <div className=\"flex-1\">\n <p className=\"font-medium text-neutral-900 dark:text-white text-sm\">{step.name}</p>\n {step.output && (\n <p className=\"text-xs text-neutral-500 mt-1 line-clamp-2\">{typeof step.output === 'string' ? step.output : JSON.stringify(step.output)}</p>\n )}\n </div>\n <span className=\"text-xs text-neutral-400\">{step.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 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 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",
|
|
11
|
-
"globals.css": "@import \"tailwindcss\";\n",
|
|
12
|
-
"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 page.tsx Workflow dashboard\n workflows/[id]/page.tsx Run detail and approval UI\n```\n\nThis is a starter template to give you a good starting point — not a production-ready setup.\n",
|
|
13
|
-
"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 \"paths\": {\n \"@/*\": [\"./*\"]\n }\n },\n \"include\": [\"**/*.ts\", \"**/*.tsx\"],\n \"exclude\": [\"node_modules\"]\n}\n",
|
|
14
|
-
"workflows/content-pipeline.ts": "import { workflow, step, parallel, waitForApproval } from \"veryfront/workflow\";\n\nexport default workflow({\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 step(\"publish\", {\n execute: async ({ previous }) => {\n // Replace with your publishing logic\n return { published: true, url: `/articles/${Date.now()}` };\n },\n }),\n ],\n});\n"
|
|
15
|
-
}
|
|
16
|
-
},
|
|
17
|
-
"ai-agent": {
|
|
18
|
-
"files": {
|
|
19
|
-
"agents/assistant.ts": "import { agent } from \"veryfront/agent\";\n\nexport default agent({\n id: \"assistant\",\n system: \"You are a helpful assistant. Answer questions clearly and concisely.\",\n tools: true,\n maxSteps: 10,\n});\n",
|
|
20
|
-
"app/api/chat/route.ts": "import { createChatHandler } from \"veryfront/agent\";\n\nexport const POST = createChatHandler(\"assistant\");\n",
|
|
21
|
-
"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 Chat</title>\n </Head>\n <div className=\"flex flex-col h-screen bg-white dark:bg-neutral-900\">\n {children}\n </div>\n </>\n );\n}\n",
|
|
22
|
-
"app/page.tsx": "'use client'\n\nimport { Chat, useChat } from 'veryfront/chat'\n\nexport default function ChatPage(): JSX.Element {\n const chat = useChat({ api: '/api/chat' })\n\n return <Chat {...chat} className=\"flex-1 min-h-0\" placeholder=\"Message\" />\n}\n",
|
|
23
|
-
"globals.css": "@import \"tailwindcss\";\n",
|
|
24
|
-
"README.md": "# AI Agent\n\nA simple conversational AI with tool support.\n\n## What's included\n\n- Single assistant agent with streaming chat UI\n- Example calculator tool\n- `useChat` hook for real-time responses\n\n## Structure\n\n```\nagents/assistant.ts Agent definition\ntools/calculator.ts Example tool\napp/\n api/chat/route.ts Chat API endpoint\n page.tsx Chat interface\n```\n\nThis is a starter template to give you a good starting point — not a production-ready setup.\n",
|
|
25
|
-
"tools/calculator.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\n\nexport default tool({\n id: \"calculator\",\n description: \"Perform basic arithmetic operations\",\n inputSchema: defineSchema((v) => v.object({\n operation: v.enum([\"add\", \"subtract\", \"multiply\", \"divide\"]),\n a: v.number(),\n b: v.number(),\n }))(),\n execute: async ({ operation, a, b }) => {\n if (operation === \"divide\" && b === 0) {\n throw new Error(\"Cannot divide by zero\");\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",
|
|
26
|
-
"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 \"paths\": {\n \"@/*\": [\"./*\"]\n }\n },\n \"include\": [\"**/*.ts\", \"**/*.tsx\"],\n \"exclude\": [\"node_modules\"]\n}\n"
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
|
-
"coding-agent": {
|
|
30
|
-
"files": {
|
|
31
|
-
"agents/coder.ts": "import { agent } from \"veryfront/agent\";\n\nexport default agent({\n id: \"coder\",\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});\n",
|
|
32
|
-
"app/api/chat/route.ts": "import { createChatHandler } from \"veryfront/agent\";\n\nexport const POST = createChatHandler(\"coder\");\n",
|
|
33
|
-
"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 </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",
|
|
34
|
-
"app/page.tsx": "'use client'\n\nimport { Chat, useChat } from 'veryfront/chat'\n\nexport default function CodeAgent(): JSX.Element {\n const chat = useChat({ api: '/api/chat' })\n\n return (\n <Chat\n {...chat}\n className=\"flex-1 min-h-0\"\n placeholder=\"Describe what you want to build or fix...\"\n />\n )\n}\n",
|
|
35
|
-
"globals.css": "@import \"tailwindcss\";\n",
|
|
36
|
-
"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/chat/route.ts Chat API endpoint\n page.tsx Chat interface\n```\n\nThis is a starter template to give you a good starting point — not a production-ready setup.\n",
|
|
37
|
-
"tools/edit-file.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { readTextFile, writeTextFile, resolve, cwd } from \"veryfront/fs\";\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 const absolute = resolve(cwd(), path);\n\n let content: string;\n try {\n content = await readTextFile(absolute);\n } catch {\n return { error: `File not found: ${path}` };\n }\n\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",
|
|
38
|
-
"tools/list-files.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { readDir, resolve, cwd } from \"veryfront/fs\";\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 const absolute = resolve(cwd(), directory);\n const entries = await readDir(absolute);\n\n let files = entries\n .filter((e) => e.isFile)\n .map((e) => e.name);\n\n if (extensions?.length) {\n files = files.filter((f) =>\n extensions.some((ext) => f.endsWith(ext))\n );\n }\n\n return { directory, files, count: files.length };\n },\n});\n",
|
|
39
|
-
"tools/read-file.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { readTextFile, resolve, cwd } from \"veryfront/fs\";\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 try {\n const absolute = resolve(cwd(), path);\n const content = await readTextFile(absolute);\n return { path, content };\n } catch {\n return { error: `File not found: ${path}` };\n }\n },\n});\n",
|
|
40
|
-
"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 \"paths\": {\n \"@/*\": [\"./*\"]\n }\n },\n \"include\": [\"**/*.ts\", \"**/*.tsx\"],\n \"exclude\": [\"node_modules\"]\n}\n"
|
|
41
|
-
}
|
|
42
|
-
},
|
|
43
|
-
"docs-agent": {
|
|
44
|
-
"files": {
|
|
45
|
-
"agents/rag.ts": "import { agent } from \"veryfront/agent\";\n\nexport default agent({\n id: \"rag\",\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});\n",
|
|
46
|
-
"app/api/chat/route.ts": "import { createChatHandler } from \"veryfront/agent\";\nimport { store } from \"../../../store.ts\";\n\nexport const POST = createChatHandler(\"rag\", {\n beforeStream: async ({ lastUserText }) => {\n const query = lastUserText.trim();\n if (!query) return;\n\n try {\n await store.indexContentDir();\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: \"user\",\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",
|
|
47
|
-
"app/api/uploads/[id]/route.ts": "import { createUploadHandler } from \"veryfront/embedding\";\nimport { store } from \"../../../../store.ts\";\n\nexport const { DELETE } = createUploadHandler(store);\n",
|
|
48
|
-
"app/api/uploads/route.ts": "import { createUploadHandler } from \"veryfront/embedding\";\nimport { store } from \"../../../store.ts\";\n\nexport const { POST, GET } = createUploadHandler(store);\n",
|
|
49
|
-
"app/layout.tsx": "import \"../globals.css\";\nimport { Head } from \"veryfront/head\";\n\nexport default function RootLayout({ children }: { children: React.ReactNode }): React.ReactNode {\n return (\n <>\n <Head><title>Docs Agent</title></Head>\n <div className=\"flex flex-col h-screen\">\n {children}\n </div>\n </>\n );\n}\n",
|
|
50
|
-
"app/page.tsx": "'use client'\n\nimport { useState, useEffect, useCallback, useMemo } from 'react'\nimport { ChatWithSidebar, useChat, type QuickAction } 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\nconst QUICK_ACTIONS: QuickAction[] = [\n { id: 'ask-question', label: 'Ask Question', prompt: 'I have a question about this document: ' },\n { id: 'extract-insights', label: 'Extract Insights', prompt: 'Extract the key insights from the uploaded documents.' },\n { id: 'find-sources', label: 'Find Sources', prompt: 'Find relevant sources and references in the documents for: ' },\n]\n\ninterface Doc { id: string; title: string; source: string; url?: string }\n\nfunction useUploads(api: string) {\n const [docs, setDocs] = useState<Doc[]>([])\n const [uploading, setUploading] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n const refresh = useCallback(async () => {\n try {\n const res = await fetch(api)\n if (res.ok) {\n const data = await res.json()\n setDocs(Array.isArray(data) ? data : data.uploads ?? [])\n }\n } catch { /* ignore */ }\n }, [api])\n\n useEffect(() => { refresh() }, [refresh])\n\n const upload = useCallback(async (file: File) => {\n setUploading(true)\n setError(null)\n try {\n const form = new FormData()\n form.append('file', file)\n const res = await fetch(api, { method: 'POST', body: form })\n if (!res.ok) throw new Error(await res.text())\n await refresh()\n } catch (e) {\n setError(e instanceof Error ? e.message : 'Upload failed')\n } finally {\n setUploading(false)\n }\n }, [api, refresh])\n\n const remove = useCallback(async (id: string) => {\n await fetch(`${api}/${id}`, { method: 'DELETE' })\n await refresh()\n }, [api, refresh])\n\n const uploads = useMemo(\n () => docs.filter((d) => d.source.startsWith('upload:')),\n [docs],\n )\n\n return { uploads, uploading, error, upload, remove }\n}\n\nexport default function DocsChat() {\n const chat = useChat({ api: '/api/chat' })\n const docs = useUploads(UPLOAD_API)\n\n const attachmentItems = useMemo(() => {\n const items = docs.uploads.map((d) => ({\n id: d.id,\n name: d.title,\n status: 'ready' as const,\n }))\n if (docs.uploading) {\n items.push({ id: '__uploading', name: 'Uploading...', status: 'uploading' as const })\n }\n return items\n }, [docs.uploads, docs.uploading])\n\n const uploadFiles = useMemo(\n () => docs.uploads.map((d) => ({ id: d.id, name: d.title, url: d.url })),\n [docs.uploads],\n )\n\n const handleAttach = useCallback((files: FileList) => {\n for (const file of Array.from(files)) {\n docs.upload(file)\n }\n }, [docs.upload])\n\n const handleQuickAction = useCallback((action: QuickAction) => {\n if (action.prompt) chat.setInput(action.prompt)\n }, [chat.setInput])\n\n return (\n <ChatWithSidebar\n chat={chat}\n sidebar={{ storageKey: 'rag-threads' }}\n features={{ steps: true, tabs: true, sources: true, export: true }}\n models={{\n options: [\n {\n value: 'anthropic/claude-sonnet-4-6',\n label: 'Claude Sonnet',\n },\n {\n value: 'openai/gpt-4.1-mini',\n label: 'GPT-4.1 Mini',\n },\n {\n value: 'google/gemini-2.5-flash',\n label: 'Gemini 2.5 Flash',\n badge: 'Fast',\n },\n ],\n }}\n attachments={{\n uploads: uploadFiles,\n onRemoveUpload: docs.remove,\n onAttach: handleAttach,\n accept: ACCEPT,\n items: attachmentItems,\n onRemoveItem: docs.remove,\n }}\n quickActions={{\n actions: QUICK_ACTIONS,\n onAction: handleQuickAction,\n }}\n message={{\n renderTool: () => null,\n }}\n className=\"flex-1 min-h-0\"\n placeholder=\"Ask anything about your documents...\"\n emptyState={{ title: 'Docs Agent', description: 'Upload files and ask questions' }}\n />\n )\n}\n",
|
|
51
|
-
"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",
|
|
52
|
-
"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",
|
|
53
|
-
"globals.css": "@import \"tailwindcss\";\n",
|
|
54
|
-
"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- Original uploaded files stored in Veryfront Cloud project uploads when cloud bootstrap is present\n- Sample content in `/content` directory auto-indexed on first search\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 dev\n ```\n\n3. Open the app and upload a document or ask a question — the sample docs in `content/` are indexed automatically.\n\nIf you are using a self-hosted Veryfront API, also set `VERYFRONT_API_URL`.\n\n## Architecture\n\nRAG grounds LLM responses in your documents through three pipelines — **Ingestion**, **Query**, and **RAG** — 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. Embeddings are generated lazily on first search to keep uploads fast.\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 chat 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 a system message. 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/chat/route.ts Chat API endpoint\n api/uploads/route.ts Upload (POST) and list (GET) uploads\n api/uploads/[id]/route.ts Delete upload\n page.tsx Chat UI with document upload panel\n layout.tsx Root layout with header\n```\n\n## Framework usage\n\n| What | Framework | Template code |\n|------|-----------|---------------|\n| Chat UI + streaming | `Chat`, `useChat` | `page.tsx` |\n| Upload management | `useUploads` hook | `page.tsx` |\n| Source display | `showSources` prop on `Chat` | `page.tsx` |\n| Upload API routes | `createUploadHandler` | 1-line per route file |\n| Chat API route | `createChatHandler` | 1 line in `route.ts` |\n| Agent definition | `agent()` | Config object in `agents/rag.ts` |\n| RAG retrieval | `beforeStream` hook | Context injection in `api/chat/route.ts` |\n| Vector store | `ragStore()` | Config in `store.ts` |\n\n## Adding documents\n\n- Drop files into `content/` — they're indexed automatically on first search\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",
|
|
55
|
-
"store.ts": "import { ragStore } from \"veryfront/embedding\";\n\nexport const store = ragStore({\n storagePath: \"data/index.json\",\n contentDir: \"content\",\n});\n",
|
|
56
|
-
"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 \"paths\": {\n \"@/*\": [\"./*\"]\n }\n },\n \"include\": [\"**/*.ts\", \"**/*.tsx\"],\n \"exclude\": [\"node_modules\"]\n}\n"
|
|
57
|
-
}
|
|
58
|
-
},
|
|
59
|
-
"minimal": {
|
|
60
|
-
"files": {
|
|
61
|
-
"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",
|
|
62
|
-
"app/layout.tsx": "export default function RootLayout({\n children,\n}: {\n children: React.ReactNode;\n}): React.ReactNode {\n return (\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",
|
|
63
|
-
"app/page.tsx": "export default function HomePage(): 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",
|
|
64
|
-
"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 is a starter template to give you a good starting point — not a production-ready setup.\n"
|
|
65
|
-
}
|
|
66
|
-
},
|
|
67
|
-
"multi-agent-system": {
|
|
68
|
-
"files": {
|
|
69
|
-
"agents/orchestrator.ts": "import { agent, getAgentsAsTools } from \"veryfront/agent\";\n\nexport default agent({\n id: \"orchestrator\",\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 tools: getAgentsAsTools([\"researcher\", \"writer\"]),\n maxSteps: 10,\n});\n",
|
|
70
|
-
"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",
|
|
71
|
-
"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",
|
|
72
|
-
"app/api/chat/route.ts": "import { createChatHandler } from \"veryfront/agent\";\n\nexport const POST = createChatHandler(\"orchestrator\");\n",
|
|
73
|
-
"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 </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",
|
|
74
|
-
"app/page.tsx": "'use client'\n\nimport { Chat, useChat } from 'veryfront/chat'\n\nexport default function MultiAgentChat(): JSX.Element {\n const chat = useChat({ api: '/api/chat' })\n\n return <Chat {...chat} className=\"flex-1 min-h-0\" placeholder=\"Give the team a task...\" />\n}\n",
|
|
75
|
-
"globals.css": "@import \"tailwindcss\";\n",
|
|
76
|
-
"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- Agent-as-tool composition via `getAgentsAsTools()`\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/chat/route.ts Chat API endpoint\n page.tsx Chat interface\n```\n\nThis is a starter template to give you a good starting point — not a production-ready setup.\n",
|
|
77
|
-
"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: async ({ 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/code/guides/tools for setup instructions.\",\n );\n },\n});\n",
|
|
78
|
-
"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 \"paths\": {\n \"@/*\": [\"./*\"]\n }\n },\n \"include\": [\"**/*.ts\", \"**/*.tsx\"],\n \"exclude\": [\"node_modules\"]\n}\n"
|
|
79
|
-
}
|
|
80
|
-
},
|
|
81
|
-
"saas-starter": {
|
|
82
|
-
"files": {
|
|
83
|
-
"agents/assistant.ts": "import { agent } from \"veryfront/agent\";\n\nexport default agent({\n id: \"assistant\",\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});\n",
|
|
84
|
-
"app/api/chat/route.ts": "import { createChatHandler } from \"veryfront/agent\";\n\nexport const POST = createChatHandler(\"assistant\");\n",
|
|
85
|
-
"app/dashboard/page.tsx": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Chat, useChat } from \"veryfront/chat\";\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(): JSX.Element {\n const [conversations] = useState<Conversation[]>(INITIAL_CONVERSATIONS);\n const [activeId, setActiveId] = useState(\"1\");\n const chat = useChat({ api: \"/api/chat\" });\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 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 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 <Chat {...chat} className=\"flex-1 min-h-0\" placeholder=\"Message...\" />\n </main>\n </div>\n );\n}\n",
|
|
86
|
-
"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 </Head>\n <div className=\"antialiased\">\n {children}\n </div>\n </>\n );\n}\n",
|
|
87
|
-
"app/login/page.tsx": "\"use client\";\n\nexport default function LoginPage(): 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=\"/api/auth/google\"\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=\"/api/auth/github\"\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 ← Back to home\n </a>\n </p>\n </div>\n </div>\n );\n}\n",
|
|
88
|
-
"app/page.tsx": "export default function LandingPage(): 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 — ready for\n 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/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: \"Define agents with tools, memory, and streaming — auto-discovered 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: \"Auth, rate limiting, and deploy — 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",
|
|
89
|
-
"globals.css": "@import \"tailwindcss\";\n",
|
|
90
|
-
"README.md": "# SaaS Starter\n\nA production-ready app 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/chat/route.ts Chat API endpoint\n page.tsx Landing page\n login/page.tsx OAuth login\n dashboard/page.tsx Chat with sidebar\n```\n\nThis is a starter template to give you a good starting point — not a production-ready setup.\n",
|
|
91
|
-
"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) => v.object({\n query: v.string().describe(\"Search query\"),\n }))(),\n execute: async ({ 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",
|
|
92
|
-
"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 \"paths\": {\n \"@/*\": [\"./*\"]\n }\n },\n \"include\": [\"**/*.ts\", \"**/*.tsx\"],\n \"exclude\": [\"node_modules\"]\n}\n"
|
|
93
|
-
}
|
|
94
|
-
},
|
|
95
|
-
"integration:_base": {
|
|
96
|
-
"files": {
|
|
97
|
-
"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 = requireUserIdFromRequest(req);\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",
|
|
98
|
-
"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",
|
|
99
|
-
"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",
|
|
100
|
-
"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/chat' })\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} 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",
|
|
101
|
-
"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:read, chat:write, users:read, channels:history\",\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 discord: {\n title: \"Discord App Setup\",\n category: \"communication\",\n steps: [\n \"Go to Discord Developer Portal\",\n \"Click New Application\",\n \"Go to OAuth2 section\",\n \"Add redirect: http://localhost:3000/api/auth/discord/callback\",\n \"Copy Client ID and Secret to .env\",\n \"Add bot permissions as needed\",\n ],\n link: \"https://discord.com/developers/applications\",\n envVars: [\"DISCORD_CLIENT_ID\", \"DISCORD_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 dropbox: {\n title: \"Dropbox App Setup\",\n category: \"storage\",\n steps: [\n \"Go to Dropbox App Console\",\n \"Create new app\",\n \"Choose Scoped access and Full Dropbox\",\n \"Add redirect URI: http://localhost:3000/api/auth/dropbox/callback\",\n \"Copy App Key and Secret to .env\",\n ],\n link: \"https://www.dropbox.com/developers/apps\",\n envVars: [\"DROPBOX_CLIENT_ID\", \"DROPBOX_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 hubspot: {\n title: \"HubSpot App Setup\",\n category: \"sales\",\n steps: [\n \"Go to HubSpot Developer Portal\",\n \"Create new app\",\n \"Add redirect URI: http://localhost:3000/api/auth/hubspot/callback\",\n \"Select required scopes\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://developers.hubspot.com/\",\n envVars: [\"HUBSPOT_CLIENT_ID\", \"HUBSPOT_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 }): JSX.Element {\n const iconMap: Record<string, 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",
|
|
102
|
-
"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",
|
|
103
|
-
"lib/oauth-memory-store.ts": "import { MemoryTokenStore } from \"veryfront/oauth\";\n\nexport const oauthMemoryTokenStore = new MemoryTokenStore();\n",
|
|
104
|
-
"lib/oauth.ts": "import { type OAuthToken, tokenStore } 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 const token = await tokenStore.getToken(userId, service);\n if (!token) return null;\n\n const isExpired = token.expiresAt\n ? token.expiresAt < Date.now() + 5 * 60 * 1000\n : false;\n\n if (!isExpired || !token.refreshToken) return token.accessToken;\n\n try {\n const newToken = await refreshAccessToken(provider, token.refreshToken);\n await tokenStore.setToken(userId, service, newToken);\n return newToken.accessToken;\n } catch {\n await tokenStore.revokeToken(userId, service);\n return null;\n }\n}\n",
|
|
105
|
-
"lib/token-store-examples.ts": "/****\n * Production Token Store Examples\n *\n * Copy-paste implementations for different storage backends.\n * Each example includes encryption support via TOKEN_ENCRYPTION_KEY.\n *\n * @module\n */\n\nimport { createTokenStore, tokenStore, type TokenStore } from \"./token-store.ts\";\n\n// ============================================================================\n// Vercel KV Store\n// ============================================================================\n\n/**\n * Token store using Vercel KV (Redis-compatible)\n *\n * Required environment variables:\n * - KV_REST_API_URL\n * - KV_REST_API_TOKEN\n * - TOKEN_ENCRYPTION_KEY (recommended)\n *\n * @example\n * ```typescript\n * // lib/token-store.ts\n * import { createVercelKVStore } from './token-store-examples';\n * export const tokenStore = createVercelKVStore();\n * ```\n */\nexport function createVercelKVStore(): TokenStore {\n type VercelKV = typeof import(\"@vercel/kv\");\n let kvPromise: Promise<VercelKV> | null = null;\n\n async function getKV(): Promise<VercelKV[\"kv\"]> {\n kvPromise ??= import(\"@vercel/kv\");\n return (await kvPromise).kv;\n }\n\n return createTokenStore({\n async get(key: string): Promise<string | null> {\n const kv = await getKV();\n return kv.get<string>(key);\n },\n async set(key: string, value: string): Promise<void> {\n const kv = await getKV();\n await kv.set(key, value);\n },\n async delete(key: string): Promise<void> {\n const kv = await getKV();\n await kv.del(key);\n },\n });\n}\n\n// ============================================================================\n// Redis Store\n// ============================================================================\n\n/**\n * Token store using Redis\n *\n * Required environment variables:\n * - REDIS_URL (e.g., redis://localhost:6379)\n * - TOKEN_ENCRYPTION_KEY (recommended)\n *\n * @example\n * ```typescript\n * // lib/token-store.ts\n * import { createRedisStore } from './token-store-examples';\n * export const tokenStore = createRedisStore();\n * ```\n */\nexport function createRedisStore(): TokenStore {\n let clientPromise: Promise<ReturnType<(typeof import(\"redis\"))[\"createClient\"]>> | null = null;\n\n async function getClient(): Promise<ReturnType<(typeof import(\"redis\"))[\"createClient\"]>> {\n clientPromise ??= (async () => {\n const { createClient } = await import(\"redis\");\n const client = createClient({ url: process.env.REDIS_URL });\n await client.connect();\n return client;\n })();\n\n return clientPromise;\n }\n\n return createTokenStore({\n async get(key: string): Promise<string | null> {\n const client = await getClient();\n return client.get(key);\n },\n async set(key: string, value: string): Promise<void> {\n const client = await getClient();\n await client.set(key, value);\n },\n async delete(key: string): Promise<void> {\n const client = await getClient();\n await client.del(key);\n },\n });\n}\n\n// ============================================================================\n// PostgreSQL Store\n// ============================================================================\n\n/**\n * Token store using PostgreSQL\n *\n * Required environment variables:\n * - DATABASE_URL (e.g., postgres://user:pass@host:5432/db)\n * - TOKEN_ENCRYPTION_KEY (recommended)\n *\n * Required table (create with migration):\n * ```sql\n * CREATE TABLE oauth_tokens (\n * key VARCHAR(255) PRIMARY KEY,\n * value TEXT NOT NULL,\n * created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n * updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n * );\n * CREATE INDEX idx_oauth_tokens_key ON oauth_tokens(key);\n * ```\n *\n * @example\n * ```typescript\n * // lib/token-store.ts\n * import { createPostgresStore } from './token-store-examples';\n * export const tokenStore = createPostgresStore();\n * ```\n */\nexport function createPostgresStore(): TokenStore {\n let poolPromise: Promise<import(\"pg\").Pool> | null = null;\n\n async function getPool(): Promise<import(\"pg\").Pool> {\n poolPromise ??= (async () => {\n const { Pool } = await import(\"pg\");\n return new Pool({ connectionString: process.env.DATABASE_URL });\n })();\n\n return poolPromise;\n }\n\n return createTokenStore({\n async get(key: string): Promise<string | null> {\n const pool = await getPool();\n const result = await pool.query(\"SELECT value FROM oauth_tokens WHERE key = $1\", [key]);\n return result.rows[0]?.value ?? null;\n },\n async set(key: string, value: string): Promise<void> {\n const pool = await getPool();\n await pool.query(\n `INSERT INTO oauth_tokens (key, value, updated_at)\n VALUES ($1, $2, NOW())\n ON CONFLICT (key) DO UPDATE SET value = $2, updated_at = NOW()`,\n [key, value],\n );\n },\n async delete(key: string): Promise<void> {\n const pool = await getPool();\n await pool.query(\"DELETE FROM oauth_tokens WHERE key = $1\", [key]);\n },\n });\n}\n\n// ============================================================================\n// SQLite Store (for edge/serverless with D1, Turso, etc.)\n// ============================================================================\n\n/**\n * Token store using SQLite (Cloudflare D1, Turso, better-sqlite3)\n *\n * Required table:\n * ```sql\n * CREATE TABLE oauth_tokens (\n * key TEXT PRIMARY KEY,\n * value TEXT NOT NULL,\n * updated_at INTEGER DEFAULT (strftime('%s', 'now'))\n * );\n * ```\n *\n * @param db - SQLite database instance (D1Database, Connection, or Database)\n *\n * @example With Cloudflare D1\n * ```typescript\n * // In your API route\n * export async function GET(request: Request, { env }) {\n * const tokenStore = createSQLiteStore(env.DB);\n * // ...\n * }\n * ```\n *\n * @example With Turso\n * ```typescript\n * import { createClient } from '@libsql/client';\n * const db = createClient({ url: process.env.TURSO_URL, authToken: process.env.TURSO_AUTH_TOKEN });\n * export const tokenStore = createSQLiteStore(db);\n * ```\n */\nexport function createSQLiteStore(db: {\n prepare(sql: string): {\n bind(...args: unknown[]): { first(): Promise<{ value?: string } | null>; run(): Promise<void> };\n };\n}): TokenStore {\n return createTokenStore({\n async get(key: string): Promise<string | null> {\n const result = await db.prepare(\"SELECT value FROM oauth_tokens WHERE key = ?\").bind(key).first();\n return result?.value ?? null;\n },\n async set(key: string, value: string): Promise<void> {\n await db\n .prepare(\n `INSERT OR REPLACE INTO oauth_tokens (key, value, updated_at)\n VALUES (?, ?, strftime('%s', 'now'))`,\n )\n .bind(key, value)\n .run();\n },\n async delete(key: string): Promise<void> {\n await db.prepare(\"DELETE FROM oauth_tokens WHERE key = ?\").bind(key).run();\n },\n });\n}\n\n// ============================================================================\n// Cloudflare Workers KV Store\n// ============================================================================\n\n/**\n * Token store using Cloudflare Workers KV\n *\n * @param kv - KV namespace binding from worker environment\n *\n * @example\n * ```typescript\n * // In your worker\n * export default {\n * async fetch(request, env) {\n * const tokenStore = createWorkersKVStore(env.OAUTH_TOKENS);\n * // ...\n * }\n * };\n * ```\n */\nexport function createWorkersKVStore(kv: {\n get(key: string): Promise<string | null>;\n put(key: string, value: string): Promise<void>;\n delete(key: string): Promise<void>;\n}): TokenStore {\n return createTokenStore({\n get(key: string): Promise<string | null> {\n return kv.get(key);\n },\n set(key: string, value: string): Promise<void> {\n return kv.put(key, value);\n },\n delete(key: string): Promise<void> {\n return kv.delete(key);\n },\n });\n}\n\n// ============================================================================\n// Prisma Store\n// ============================================================================\n\n/**\n * Token store using Prisma ORM\n *\n * Required Prisma schema:\n * ```prisma\n * model OAuthToken {\n * key String @id\n * value String\n * updatedAt DateTime @updatedAt\n * }\n * ```\n *\n * @example\n * ```typescript\n * import { PrismaClient } from '@prisma/client';\n * const prisma = new PrismaClient();\n * export const tokenStore = createPrismaStore(prisma);\n * ```\n */\nexport function createPrismaStore(prisma: {\n oAuthToken: {\n findUnique(args: { where: { key: string } }): Promise<{ value: string } | null>;\n upsert(args: {\n where: { key: string };\n update: { value: string };\n create: { key: string; value: string };\n }): Promise<unknown>;\n delete(args: { where: { key: string } }): Promise<unknown>;\n };\n}): TokenStore {\n return createTokenStore({\n async get(key: string): Promise<string | null> {\n const record = await prisma.oAuthToken.findUnique({ where: { key } });\n return record?.value ?? null;\n },\n async set(key: string, value: string): Promise<void> {\n await prisma.oAuthToken.upsert({\n where: { key },\n update: { value },\n create: { key, value },\n });\n },\n async delete(key: string): Promise<void> {\n try {\n await prisma.oAuthToken.delete({ where: { key } });\n } catch {\n // Ignore if not found\n }\n },\n });\n}\n\n// ============================================================================\n// Drizzle ORM Store\n// ============================================================================\n\n/**\n * Token store using Drizzle ORM\n *\n * Required schema:\n * ```typescript\n * import { pgTable, text, timestamp } from 'drizzle-orm/pg-core';\n *\n * export const oauthTokens = pgTable('oauth_tokens', {\n * key: text('key').primaryKey(),\n * value: text('value').notNull(),\n * updatedAt: timestamp('updated_at').defaultNow(),\n * });\n * ```\n *\n * @example\n * ```typescript\n * import { drizzle } from 'drizzle-orm/postgres-js';\n * import postgres from 'postgres';\n * import { oauthTokens } from './schema';\n *\n * const client = postgres(process.env.DATABASE_URL!);\n * const db = drizzle(client);\n * export const tokenStore = createDrizzleStore(db, oauthTokens);\n * ```\n */\nexport function createDrizzleStore<T extends { key: unknown; value: unknown }>(\n db: {\n select(): {\n from(table: T): { where(condition: unknown): { get(): Promise<{ value: string } | undefined> } };\n };\n insert(table: T): {\n values(data: { key: string; value: string }): {\n onConflictDoUpdate(args: { target: unknown; set: { value: string } }): { execute(): Promise<void> };\n };\n };\n delete(table: T): { where(condition: unknown): { execute(): Promise<void> } };\n },\n table: T & { key: unknown; value: unknown },\n eq: (col: unknown, val: unknown) => unknown,\n): TokenStore {\n return createTokenStore({\n async get(key: string): Promise<string | null> {\n const result = await db.select().from(table).where(eq(table.key, key)).get();\n return result?.value ?? null;\n },\n async set(key: string, value: string): Promise<void> {\n await db\n .insert(table)\n .values({ key, value })\n .onConflictDoUpdate({ target: table.key, set: { value } })\n .execute();\n },\n async delete(key: string): Promise<void> {\n await db.delete(table).where(eq(table.key, key)).execute();\n },\n });\n}\n\n// ============================================================================\n// Auto-Select Store (Recommended)\n// ============================================================================\n\n/**\n * Automatically selects the appropriate token store based on environment\n *\n * Detection order:\n * 1. DATABASE_URL -> PostgreSQL\n * 2. KV_REST_API_URL -> Vercel KV\n * 3. REDIS_URL -> Redis\n * 4. Fallback -> In-memory (development only)\n *\n * @example\n * ```typescript\n * // lib/token-store.ts\n * import { createAutoStore } from './token-store-examples';\n * export const tokenStore = createAutoStore();\n * ```\n */\nexport function createAutoStore(): TokenStore {\n const env = process.env;\n\n if (env.DATABASE_URL) {\n console.log(\"[Token Store] Using PostgreSQL storage\");\n return createPostgresStore();\n }\n\n if (env.KV_REST_API_URL && env.KV_REST_API_TOKEN) {\n console.log(\"[Token Store] Using Vercel KV storage\");\n return createVercelKVStore();\n }\n\n if (env.REDIS_URL) {\n console.log(\"[Token Store] Using Redis storage\");\n return createRedisStore();\n }\n\n console.warn(\n \"[Token Store] No production storage configured. \" +\n \"Using in-memory storage (tokens will be lost on restart). \" +\n \"Set DATABASE_URL, KV_REST_API_URL, or REDIS_URL for production.\",\n );\n\n return tokenStore;\n}\n",
|
|
106
|
-
"lib/token-store.ts": "/********************************************************************************\n * OAuth Token Store\n *\n * Manages OAuth tokens for connected services.\n *\n * ## Storage Modes\n *\n * **Development (default)**: In-memory storage - tokens are lost on restart.\n * **Production**: Configure via environment variables:\n * - DATABASE_URL: Uses database storage (Postgres, SQLite, MySQL)\n * - KV_REST_API_URL + KV_REST_API_TOKEN: Uses Vercel KV\n * - REDIS_URL: Uses Redis\n * - TOKEN_ENCRYPTION_KEY: Enables AES-256-GCM encryption (recommended)\n *\n * ## Security\n *\n * Tokens contain sensitive OAuth credentials. In production:\n * 1. Always use encrypted storage (set TOKEN_ENCRYPTION_KEY)\n * 2. Use HTTPS for all connections\n * 3. Implement proper access control\n * 4. Rotate encryption keys periodically\n *\n * @see lib/token-store-examples.ts for complete production implementations\n ********************************************************************************/\n\nexport interface OAuthToken {\n accessToken: string;\n refreshToken?: string;\n expiresAt?: number;\n tokenType?: string;\n scope?: string;\n}\n\nexport interface TokenStore {\n getToken(userId: string, service: string): Promise<OAuthToken | null>;\n setToken(userId: string, service: string, token: OAuthToken): Promise<void>;\n revokeToken(userId: string, service: string): Promise<void>;\n isConnected(userId: string, service: string): Promise<boolean>;\n}\n\n/** Token store configuration for production backends */\nexport interface TokenStoreConfig {\n get: (key: string) => Promise<string | null>;\n set: (key: string, value: string) => Promise<void>;\n delete: (key: string) => Promise<void>;\n}\n\nconst AUTO_KEY_STORAGE = \"__veryfront_auto_encryption_key__\";\nconst TOKENS_KEY = \"__veryfront_oauth_tokens__\";\n\nconst globalStore = globalThis as Record<string, unknown>;\n\n// ============================================================================\n// Encryption Utilities\n// ============================================================================\n\nexport async function encryptToken(token: OAuthToken): Promise<string> {\n const key = getEncryptionKey();\n if (!key) return JSON.stringify(token);\n\n const data = new TextEncoder().encode(JSON.stringify(token));\n const iv = crypto.getRandomValues(new Uint8Array(12));\n const rawKey = new Uint8Array(key).buffer;\n\n const cryptoKey = await crypto.subtle.importKey(\"raw\", rawKey, \"AES-GCM\", false, [\"encrypt\"]);\n const encrypted = await crypto.subtle.encrypt({ name: \"AES-GCM\", iv }, cryptoKey, data);\n\n const combined = new Uint8Array(iv.length + encrypted.byteLength);\n combined.set(iv);\n combined.set(new Uint8Array(encrypted), iv.length);\n\n return `encrypted:${btoa(String.fromCharCode(...combined))}`;\n}\n\nexport async function decryptToken(encrypted: string): Promise<OAuthToken | null> {\n if (!encrypted.startsWith(\"encrypted:\")) {\n try {\n return JSON.parse(encrypted) as OAuthToken;\n } catch {\n return null;\n }\n }\n\n const key = getEncryptionKey();\n if (!key) {\n console.error(\"[Token Store] Cannot decrypt: TOKEN_ENCRYPTION_KEY not set\");\n return null;\n }\n\n try {\n const base64 = encrypted.slice(\"encrypted:\".length);\n const combined = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));\n\n const iv = combined.slice(0, 12);\n const ciphertext = combined.slice(12);\n const rawKey = new Uint8Array(key).buffer;\n\n const cryptoKey = await crypto.subtle.importKey(\"raw\", rawKey, \"AES-GCM\", false, [\"decrypt\"]);\n const decrypted = await crypto.subtle.decrypt({ name: \"AES-GCM\", iv }, cryptoKey, ciphertext);\n\n return JSON.parse(new TextDecoder().decode(decrypted)) as OAuthToken;\n } catch {\n console.error(\"[Token Store] Decryption failed\");\n return null;\n }\n}\n\nexport function generateEncryptionKey(): string {\n const bytes = crypto.getRandomValues(new Uint8Array(32));\n return Array.from(bytes)\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n}\n\nfunction getEnvVar(name: string): string | undefined {\n if (typeof process !== \"undefined\") return process.env?.[name];\n return (globalThis as { Deno?: { env?: { get?: (key: string) => string | undefined } } }).Deno\n ?.env?.get?.(name);\n}\n\nfunction hexToKeyBytes(keyHex: string): Uint8Array | null {\n if (keyHex.length !== 64) {\n console.error(\"[Token Store] TOKEN_ENCRYPTION_KEY must be 64 hex characters (32 bytes)\");\n return null;\n }\n\n const key = new Uint8Array(32);\n for (let i = 0; i < 32; i++) {\n key[i] = parseInt(keyHex.slice(i * 2, i * 2 + 2), 16);\n }\n return key;\n}\n\n/** Get encryption key from environment or auto-generate for development */\nfunction getEncryptionKey(): Uint8Array | null {\n const keyHex = getEnvVar(\"TOKEN_ENCRYPTION_KEY\");\n if (keyHex) return hexToKeyBytes(keyHex);\n\n if (!globalStore[AUTO_KEY_STORAGE]) {\n globalStore[AUTO_KEY_STORAGE] = generateEncryptionKey();\n }\n\n return hexToKeyBytes(globalStore[AUTO_KEY_STORAGE] as string);\n}\n\n// ============================================================================\n// Storage Mode Detection\n// ============================================================================\n\nexport type StorageMode = \"memory\" | \"database\" | \"kv\" | \"redis\" | \"custom\";\n\nexport function getStorageMode(): StorageMode {\n const env = typeof process !== \"undefined\"\n ? process.env\n : (globalThis as { Deno?: { env?: { toObject?: () => Record<string, string> } } }).Deno?.env\n ?.toObject?.() ?? {};\n\n if (env.DATABASE_URL) return \"database\";\n if (env.KV_REST_API_URL) return \"kv\";\n if (env.REDIS_URL) return \"redis\";\n return \"memory\";\n}\n\nexport function isEncryptionEnabled(): boolean {\n return getEncryptionKey() !== null;\n}\n\nfunction isProductionRuntime(): boolean {\n return getEnvVar(\"NODE_ENV\") === \"production\";\n}\n\n// ============================================================================\n// In-Memory Store (Development)\n// ============================================================================\n\nconst tokens = (globalStore[TOKENS_KEY] as Map<string, OAuthToken> | undefined) ??\n new Map<string, OAuthToken>();\nglobalStore[TOKENS_KEY] = tokens;\n\nfunction getKey(userId: string, service: string): string {\n return `${userId}:${service}`;\n}\n\nasync function isConnected(\n store: Pick<TokenStore, \"getToken\">,\n userId: string,\n service: string,\n): Promise<boolean> {\n const token = await store.getToken(userId, service);\n return !!token && (!token.expiresAt || token.expiresAt > Date.now());\n}\n\nconst inMemoryStore: TokenStore = {\n async getToken(userId: string, service: string): Promise<OAuthToken | null> {\n return tokens.get(getKey(userId, service)) ?? null;\n },\n\n async setToken(userId: string, service: string, token: OAuthToken): Promise<void> {\n tokens.set(getKey(userId, service), token);\n },\n\n async revokeToken(userId: string, service: string): Promise<void> {\n tokens.delete(getKey(userId, service));\n },\n\n async isConnected(userId: string, service: string): Promise<boolean> {\n return isConnected(this, userId, service);\n },\n};\n\n// ============================================================================\n// Token Store Factory\n// ============================================================================\n\nexport function createTokenStore(config: TokenStoreConfig): TokenStore {\n return {\n async getToken(userId: string, service: string): Promise<OAuthToken | null> {\n const data = await config.get(getKey(userId, service));\n if (!data) return null;\n return decryptToken(data);\n },\n\n async setToken(userId: string, service: string, token: OAuthToken): Promise<void> {\n await config.set(getKey(userId, service), await encryptToken(token));\n },\n\n async revokeToken(userId: string, service: string): Promise<void> {\n await config.delete(getKey(userId, service));\n },\n\n async isConnected(userId: string, service: string): Promise<boolean> {\n return isConnected(this, userId, service);\n },\n };\n}\n\n// ============================================================================\n// Default Export (Auto-detects environment)\n// ============================================================================\n\nexport function createDefaultTokenStore(): TokenStore {\n if (isProductionRuntime()) {\n throw new Error(\n \"In-memory token storage is not allowed in production. \" +\n \"Configure DATABASE_URL, KV_REST_API_URL, or REDIS_URL and wire a durable store from \" +\n \"lib/token-store-examples.ts.\",\n );\n }\n\n // The starter keeps the development store explicit. Production adapters in\n // token-store-examples.ts require provider-specific clients and credentials.\n return inMemoryStore;\n}\n\nlet defaultTokenStore: TokenStore | null = null;\n\nfunction getDefaultTokenStore(): TokenStore {\n defaultTokenStore ??= createDefaultTokenStore();\n return defaultTokenStore;\n}\n\nexport const tokenStore: TokenStore = {\n getToken(userId: string, service: string): Promise<OAuthToken | null> {\n return getDefaultTokenStore().getToken(userId, service);\n },\n\n setToken(userId: string, service: string, token: OAuthToken): Promise<void> {\n return getDefaultTokenStore().setToken(userId, service, token);\n },\n\n revokeToken(userId: string, service: string): Promise<void> {\n return getDefaultTokenStore().revokeToken(userId, service);\n },\n\n isConnected(userId: string, service: string): Promise<boolean> {\n return getDefaultTokenStore().isConnected(userId, service);\n },\n};\n\nif (\n !isProductionRuntime() &&\n getStorageMode() === \"memory\"\n) {\n console.warn(\n \"[Token Store] Using in-memory storage (development mode). \" +\n \"Tokens will be lost on restart. \" +\n \"Set DATABASE_URL, KV_REST_API_URL, or REDIS_URL for production.\",\n );\n}\n",
|
|
107
|
-
"lib/user-id.ts": "import type { ToolExecutionContext } from \"veryfront/tool\";\n\nfunction isProductionRuntime(): boolean {\n return Deno.env.get(\"NODE_ENV\") === \"production\";\n}\n\nfunction devUserId(): string {\n return Deno.env.get(\"VERYFRONT_DEV_USER_ID\") ?? \"dev-user\";\n}\n\nfunction requireUserId(value: string | null | undefined): string {\n if (typeof value === \"string\" && value.length > 0) {\n return value;\n }\n\n if (!isProductionRuntime()) {\n return devUserId();\n }\n\n throw new Error(\n \"Authenticated user id is required in production. \" +\n \"Pass the authenticated user's id from your session, JWT, or auth provider.\",\n );\n}\n\nexport function requireUserIdFromRequest(request: Request): string {\n return requireUserId(\n request.headers.get(\"x-veryfront-user-id\") ?? request.headers.get(\"x-user-id\"),\n );\n}\n\nexport function requireUserIdFromContext(context?: ToolExecutionContext): string {\n return requireUserId(context?.endUserId ?? context?.userId);\n}\n",
|
|
108
|
-
"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, Discord, 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, HubSpot, Pipedrive, Intercom, Zendesk, Freshdesk)\n- [Databases](#databases) (Supabase, Neon, Airtable, Snowflake)\n- [Cloud & Storage](#cloud--storage) (AWS, Dropbox, 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:read`, `chat:write`, `users:read`, `im:write`\n4. **Install to Workspace**\n\n```env\nSLACK_CLIENT_ID=your-client-id\nSLACK_CLIENT_SECRET=your-client-secret\n```\n\n### Discord\n\n1. Go to [Discord Developer Portal](https://discord.com/developers/applications)\n2. Create **New Application**\n3. Go to **OAuth2**:\n - Add redirect: `http://localhost:3000/api/auth/discord/callback`\n - Scopes: `identify`, `guilds`, `messages.read`\n\n```env\nDISCORD_CLIENT_ID=your-client-id\nDISCORD_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### HubSpot\n\n1. Go to [HubSpot Developers](https://developers.hubspot.com/)\n2. Create app in your developer account\n3. Configure OAuth redirect: `http://localhost:3000/api/auth/hubspot/callback`\n4. Select required scopes\n\n```env\nHUBSPOT_CLIENT_ID=your-client-id\nHUBSPOT_CLIENT_SECRET=your-client-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### Dropbox\n\n1. Go to [Dropbox App Console](https://www.dropbox.com/developers/apps)\n2. Create app with Full Dropbox or App folder access\n3. OAuth2 redirect: `http://localhost:3000/api/auth/dropbox/callback`\n\n```env\nDROPBOX_CLIENT_ID=your-app-key\nDROPBOX_CLIENT_SECRET=your-app-secret\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\nBy default, tokens are stored in memory. For production:\n\n1. Implement `TokenStore` interface in `lib/token-store.ts`\n2. Use Redis, database, or encrypted file storage\n3. Handle token refresh automatically\n\n## Production Checklist\n\n- [ ] Update all redirect URIs to production domain\n- [ ] Implement persistent token storage\n- [ ] Set up token encryption\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/veryfront)\n"
|
|
109
|
-
}
|
|
110
|
-
},
|
|
111
|
-
"integration:airtable": {
|
|
112
|
-
"files": {
|
|
113
|
-
"app/api/auth/airtable/callback/route.ts": "import { airtableConfig, createOAuthCallbackHandler } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string, userId: string) {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(\n serviceId: string,\n userId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string) {\n await tokenStore.revokeToken(userId, serviceId);\n },\n setState(\n state: string,\n meta: {\n userId: string;\n serviceId: string;\n codeVerifier?: string;\n redirectUri?: string;\n scopes?: string[];\n createdAt: number;\n },\n ) {\n return oauthMemoryTokenStore.setState(state, meta);\n },\n consumeState(state: string) {\n return oauthMemoryTokenStore.consumeState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(airtableConfig, { tokenStore: hybridTokenStore });\n",
|
|
114
|
-
"app/api/auth/airtable/route.ts": "import { airtableConfig, createOAuthInitHandler } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../../lib/user-id.ts\";\n\nfunction getUserId(request: Request): string {\n return requireUserIdFromRequest(request);\n}\n\nexport const GET = createOAuthInitHandler(airtableConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});",
|
|
115
|
-
"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\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): Promise<AirtableRecord> {\n return airtableFetch<AirtableRecord>(`/${baseId}/${encodeURIComponent(tableIdOrName)}`, {\n method: \"POST\",\n body: JSON.stringify({ fields }),\n });\n}\n\nexport async function createRecords(\n baseId: string,\n tableIdOrName: string,\n records: Array<{ fields: Record<string, unknown> }>,\n): Promise<AirtableRecord[]> {\n const response = await airtableFetch<{ records: AirtableRecord[] }>(\n `/${baseId}/${encodeURIComponent(tableIdOrName)}`,\n {\n method: \"POST\",\n body: JSON.stringify({ records }),\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 },\n): Promise<AirtableRecord> {\n return airtableFetch<AirtableRecord>(\n `/${baseId}/${encodeURIComponent(tableIdOrName)}/${recordId}`,\n {\n method: options?.destructive ? \"PUT\" : \"PATCH\",\n body: JSON.stringify({ fields }),\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 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",
|
|
116
|
-
"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: \"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 }))(),\n async execute({ baseId, tableIdOrName, fields }) {\n const record = await createRecord(baseId, tableIdOrName, fields);\n\n return { id: record.id, createdTime: record.createdTime, fields: record.fields };\n },\n});\n",
|
|
117
|
-
"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: \"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",
|
|
118
|
-
"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: \"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",
|
|
119
|
-
"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: \"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",
|
|
120
|
-
"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: \"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"
|
|
121
|
-
}
|
|
122
|
-
},
|
|
123
|
-
"integration:anthropic": {
|
|
124
|
-
"files": {
|
|
125
|
-
".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",
|
|
126
|
-
"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",
|
|
127
|
-
"tools/get-organization.ts": "import { tool } from 'veryfront/tool';\nimport { defineSchema } from 'veryfront/schemas';\nimport { getAnthropicAdminClient } from '../../lib/anthropic-admin-client';\n\nexport const getOrganization = tool({\n id: '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",
|
|
128
|
-
"tools/get-usage.ts": "import { tool } from 'veryfront/tool';\nimport { defineSchema } from 'veryfront/schemas';\nimport { getAnthropicAdminClient } from '../../lib/anthropic-admin-client';\n\nexport const getUsage = tool({\n id: '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",
|
|
129
|
-
"tools/list-api-keys.ts": "import { tool } from 'veryfront/tool';\nimport { defineSchema } from 'veryfront/schemas';\nimport { getAnthropicAdminClient } from '../../lib/anthropic-admin-client';\n\nexport const listAPIKeys = tool({\n id: '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",
|
|
130
|
-
"tools/list-members.ts": "import { tool } from 'veryfront/tool';\nimport { defineSchema } from 'veryfront/schemas';\nimport { getAnthropicAdminClient } from '../../lib/anthropic-admin-client';\n\nexport const listMembers = tool({\n id: '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",
|
|
131
|
-
"tools/list-workspaces.ts": "import { tool } from 'veryfront/tool';\nimport { defineSchema } from 'veryfront/schemas';\nimport { getAnthropicAdminClient } from '../../lib/anthropic-admin-client';\n\nexport const listWorkspaces = tool({\n id: '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"
|
|
132
|
-
}
|
|
133
|
-
},
|
|
134
|
-
"integration:asana": {
|
|
135
|
-
"files": {
|
|
136
|
-
".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",
|
|
137
|
-
"app/api/auth/asana/callback/route.ts": "import { asanaConfig, createOAuthCallbackHandler } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string, userId: string) {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(\n serviceId: string,\n userId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string) {\n await tokenStore.revokeToken(userId, serviceId);\n },\n setState(\n state: string,\n meta: {\n userId: string;\n serviceId: string;\n codeVerifier?: string;\n redirectUri?: string;\n scopes?: string[];\n createdAt: number;\n },\n ) {\n return oauthMemoryTokenStore.setState(state, meta);\n },\n consumeState(state: string) {\n return oauthMemoryTokenStore.consumeState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(asanaConfig, { tokenStore: hybridTokenStore });\n",
|
|
138
|
-
"app/api/auth/asana/route.ts": "import { asanaConfig, createOAuthInitHandler } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../../lib/user-id.ts\";\n\nfunction getUserId(request: Request): string {\n return requireUserIdFromRequest(request);\n}\n\nexport const GET = createOAuthInitHandler(asanaConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});",
|
|
139
|
-
"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",
|
|
140
|
-
"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: \"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",
|
|
141
|
-
"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: \"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",
|
|
142
|
-
"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: \"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",
|
|
143
|
-
"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: \"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",
|
|
144
|
-
"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: \"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"
|
|
145
|
-
}
|
|
146
|
-
},
|
|
147
|
-
"integration:aws": {
|
|
148
|
-
"files": {
|
|
149
|
-
".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",
|
|
150
|
-
"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",
|
|
151
|
-
"tools/get-s3-object.ts": "import { tool } from 'veryfront/tool';\nimport { defineSchema } from 'veryfront/schemas';\nimport { getAWSClient } from '../../lib/aws-client';\n\nexport const getS3ObjectTool = tool({\n id: '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",
|
|
152
|
-
"tools/list-ec2-instances.ts": "import { tool } from 'veryfront/tool';\nimport { defineSchema } from 'veryfront/schemas';\nimport { getAWSClient } from '../../lib/aws-client';\n\nexport const listEC2InstancesTool = tool({\n id: '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",
|
|
153
|
-
"tools/list-lambda-functions.ts": "import { tool } from 'veryfront/tool';\nimport { defineSchema } from 'veryfront/schemas';\nimport { getAWSClient } from '../../lib/aws-client';\n\nexport const listLambdaFunctionsTool = tool({\n id: '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",
|
|
154
|
-
"tools/list-s3-buckets.ts": "import { tool } from 'veryfront/tool';\nimport { defineSchema } from 'veryfront/schemas';\nimport { getAWSClient } from '../../lib/aws-client';\n\nexport const listS3BucketsTool = tool({\n id: '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",
|
|
155
|
-
"tools/list-s3-objects.ts": "import { tool } from 'veryfront/tool';\nimport { defineSchema } from 'veryfront/schemas';\nimport { getAWSClient } from '../../lib/aws-client';\n\nexport const listS3ObjectsTool = tool({\n id: '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"
|
|
156
|
-
}
|
|
157
|
-
},
|
|
158
|
-
"integration:bitbucket": {
|
|
159
|
-
"files": {
|
|
160
|
-
".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",
|
|
161
|
-
"app/api/auth/bitbucket/callback/route.ts": "/**\n * Bitbucket OAuth Callback\n *\n * Handles the OAuth callback from Atlassian and stores the tokens.\n */\n\nimport { bitbucketConfig, createOAuthCallbackHandler } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string, userId: string) {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(\n serviceId: string,\n userId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string) {\n await tokenStore.revokeToken(userId, serviceId);\n },\n setState(\n state: string,\n meta: {\n userId: string;\n serviceId: string;\n codeVerifier?: string;\n redirectUri?: string;\n scopes?: string[];\n createdAt: number;\n },\n ) {\n return oauthMemoryTokenStore.setState(state, meta);\n },\n consumeState(state: string) {\n return oauthMemoryTokenStore.consumeState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(bitbucketConfig, { tokenStore: hybridTokenStore });\n",
|
|
162
|
-
"app/api/auth/bitbucket/route.ts": "import { bitbucketConfig, createOAuthInitHandler } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../../lib/user-id.ts\";\n\nfunction getUserId(request: Request): string {\n return requireUserIdFromRequest(request);\n}\n\nexport const GET = createOAuthInitHandler(bitbucketConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});",
|
|
163
|
-
"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",
|
|
164
|
-
"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: \"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",
|
|
165
|
-
"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: \"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",
|
|
166
|
-
"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: \"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",
|
|
167
|
-
"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: \"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"
|
|
168
|
-
}
|
|
169
|
-
},
|
|
170
|
-
"integration:calendar": {
|
|
171
|
-
"files": {
|
|
172
|
-
".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",
|
|
173
|
-
"app/api/auth/calendar/callback/route.ts": "/**\n * Calendar OAuth Callback\n *\n * Handles the OAuth callback from Google and stores the tokens.\n */\n\nimport { calendarConfig, createOAuthCallbackHandler } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string, userId: string) {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(\n serviceId: string,\n userId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string) {\n await tokenStore.revokeToken(userId, serviceId);\n },\n setState(\n state: string,\n meta: {\n userId: string;\n serviceId: string;\n codeVerifier?: string;\n redirectUri?: string;\n scopes?: string[];\n createdAt: number;\n },\n ) {\n return oauthMemoryTokenStore.setState(state, meta);\n },\n consumeState(state: string) {\n return oauthMemoryTokenStore.consumeState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(calendarConfig, { tokenStore: hybridTokenStore });\n",
|
|
174
|
-
"app/api/auth/calendar/route.ts": "import { calendarConfig, createOAuthInitHandler } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../../lib/user-id.ts\";\n\nfunction getUserId(request: Request): string {\n return requireUserIdFromRequest(request);\n}\n\nexport const GET = createOAuthInitHandler(calendarConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});",
|
|
175
|
-
"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 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(options: CreateEventOptions, calendarId?: string): Promise<CalendarEvent>;\n getFreeBusy(options: FreeBusyOptions): Promise<FreeBusySlot[]>;\n findFreeSlots(options: FindFreeSlotsOptions): 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(calendarOAuthProvider, userId, \"calendar\");\n if (!token) {\n throw new Error(\"Calendar not connected. Please connect your Google Calendar first.\");\n }\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(`${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(options: ListEventsOptions = {}): 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(options: CreateEventOptions, calendarId = \"primary\"): Promise<CalendarEvent> {\n const startDate = typeof options.start === \"string\" ? options.start : options.start.toISOString();\n const endDate = typeof options.end === \"string\" ? options.end : 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>(`/calendars/${encodeURIComponent(calendarId)}/events`, {\n method: \"POST\",\n body: JSON.stringify(event),\n });\n }\n\n async function getFreeBusy(options: FreeBusyOptions): 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({ start: new Date(currentStart), end: new Date(busy.start) });\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(eventId: string, calendarId = \"primary\"): Promise<void> {\n const accessToken = await getAccessToken();\n\n const response = await fetch(\n `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/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 getFreeBusy,\n findFreeSlots,\n deleteEvent,\n };\n}\n\nexport type CalendarClient = ReturnType<typeof createCalendarClient>;\n",
|
|
176
|
-
"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: \"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",
|
|
177
|
-
"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: \"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",
|
|
178
|
-
"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: \"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"
|
|
179
|
-
}
|
|
180
|
-
},
|
|
181
|
-
"integration:confluence": {
|
|
182
|
-
"files": {
|
|
183
|
-
".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",
|
|
184
|
-
"app/api/auth/confluence/callback/route.ts": "import { confluenceConfig, createOAuthCallbackHandler } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string, userId: string) {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(\n serviceId: string,\n userId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string) {\n await tokenStore.revokeToken(userId, serviceId);\n },\n setState(\n state: string,\n meta: {\n userId: string;\n serviceId: string;\n codeVerifier?: string;\n redirectUri?: string;\n scopes?: string[];\n createdAt: number;\n },\n ) {\n return oauthMemoryTokenStore.setState(state, meta);\n },\n consumeState(state: string) {\n return oauthMemoryTokenStore.consumeState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(confluenceConfig, { tokenStore: hybridTokenStore });\n",
|
|
185
|
-
"app/api/auth/confluence/route.ts": "import { confluenceConfig, createOAuthInitHandler } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../../lib/user-id.ts\";\n\nfunction getUserId(request: Request): string {\n return requireUserIdFromRequest(request);\n}\n\nexport const GET = createOAuthInitHandler(confluenceConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});",
|
|
186
|
-
"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 interface ConfluencePage {\n id: string;\n type: \"page\" | \"blogpost\";\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\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 Error(`Confluence API error: ${response.status} ${error.message ?? response.statusText}`);\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\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/rest/api/space\", params),\n );\n\n return response.results ?? [];\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\nexport function getPage(pageId: string, expand?: string[]): Promise<ConfluencePage> {\n const params = new URLSearchParams();\n if (expand?.length) params.set(\"expand\", expand.join(\",\"));\n\n return confluenceFetch<ConfluencePage>(buildEndpoint(`/wiki/rest/api/content/${pageId}`, params));\n}\n\nexport function getPageContent(pageId: string): Promise<ConfluencePage> {\n return getPage(pageId, [\"body.storage\", \"body.view\", \"version\", \"space\"]);\n}\n\nexport function createPage(options: {\n spaceKey: string;\n title: string;\n content: string;\n parentId?: string;\n type?: \"page\" | \"blogpost\";\n}): Promise<ConfluencePage> {\n const body = {\n type: options.type ?? \"page\",\n title: options.title,\n space: { key: options.spaceKey },\n body: {\n storage: {\n value: options.content,\n representation: \"storage\" as const,\n },\n },\n ...(options.parentId ? { ancestors: [{ id: options.parentId }] } : {}),\n };\n\n return confluenceFetch<ConfluencePage>(\"/wiki/rest/api/content\", {\n method: \"POST\",\n body: JSON.stringify(body),\n });\n}\n\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 await getPage(pageId, [\"version\"]);\n\n const body: Record<string, unknown> = {\n version: {\n number: options.version,\n message: options.versionMessage,\n },\n type: \"page\",\n };\n\n if (options.title) body.title = options.title;\n\n if (options.content) {\n body.body = {\n storage: {\n value: options.content,\n representation: \"storage\",\n },\n };\n }\n\n return confluenceFetch<ConfluencePage>(`/wiki/rest/api/content/${pageId}`, {\n method: \"PUT\",\n body: JSON.stringify(body),\n });\n}\n\nexport function extractPlainText(storageHtml: string): string {\n return storageHtml\n .replace(/<[^>]*>/g, \" \")\n .replace(/ /g, \" \")\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/"/g, '\"')\n .replace(/'/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, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n",
|
|
187
|
-
"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: \"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,\n url: page._links.webui,\n version: page.version.number,\n spaceId: page.spaceId,\n };\n },\n});\n",
|
|
188
|
-
"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: \"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,\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",
|
|
189
|
-
"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: \"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",
|
|
190
|
-
"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: \"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",
|
|
191
|
-
"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) return undefined;\n\n const trimmed = content.trim();\n if (trimmed.startsWith(\"<\")) return content;\n\n return formatAsStorage(content);\n}\n\nexport default tool({\n id: \"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 const currentPage = await getPage(pageId, [\"version\"]);\n const storageContent = toStorageContent(content);\n\n const updatedPage = await updatePage(pageId, {\n title,\n content: storageContent,\n version: currentPage.version.number + 1,\n versionMessage,\n });\n\n return {\n id: updatedPage.id,\n title: updatedPage.title,\n type: updatedPage.type,\n url: updatedPage._links.webui,\n version: updatedPage.version.number,\n versionMessage: updatedPage.version.message,\n };\n },\n});\n"
|
|
192
|
-
}
|
|
193
|
-
},
|
|
194
|
-
"integration:discord": {
|
|
195
|
-
"files": {
|
|
196
|
-
".env.example": "# Discord OAuth Configuration\n# Get these from https://discord.com/developers/applications\n\n# Required: Your Discord application's Client ID\nDISCORD_CLIENT_ID=your_client_id_here\n\n# Required: Your Discord application's Client Secret\nDISCORD_CLIENT_SECRET=your_client_secret_here\n\n# Optional: Bot token for advanced bot features\n# Only needed if you want to use bot-specific functionality\nDISCORD_BOT_TOKEN=your_bot_token_here\n",
|
|
197
|
-
"app/api/auth/discord/callback/route.ts": "/**\n * Discord OAuth Callback\n *\n * Handles the OAuth callback from Discord and stores the tokens.\n */\n\nimport { createOAuthCallbackHandler, discordConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string, userId: string) {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(\n serviceId: string,\n userId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string) {\n await tokenStore.revokeToken(userId, serviceId);\n },\n setState(\n state: string,\n meta: {\n userId: string;\n serviceId: string;\n codeVerifier?: string;\n redirectUri?: string;\n scopes?: string[];\n createdAt: number;\n },\n ) {\n return oauthMemoryTokenStore.setState(state, meta);\n },\n consumeState(state: string) {\n return oauthMemoryTokenStore.consumeState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(discordConfig, { tokenStore: hybridTokenStore });\n",
|
|
198
|
-
"app/api/auth/discord/route.ts": "import { createOAuthInitHandler, discordConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../../lib/user-id.ts\";\n\nfunction getUserId(request: Request): string {\n return requireUserIdFromRequest(request);\n}\n\nexport const GET = createOAuthInitHandler(discordConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});",
|
|
199
|
-
"lib/discord-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst DISCORD_API_VERSION = \"v10\";\nconst DISCORD_BASE_URL = `https://discord.com/api/${DISCORD_API_VERSION}`;\n\ninterface DiscordUser {\n id: string;\n username: string;\n discriminator: string;\n global_name?: string | null;\n avatar?: string | null;\n bot?: boolean;\n system?: boolean;\n mfa_enabled?: boolean;\n banner?: string | null;\n accent_color?: number | null;\n locale?: string;\n verified?: boolean;\n email?: string | null;\n flags?: number;\n premium_type?: number;\n public_flags?: number;\n}\n\ninterface DiscordGuild {\n id: string;\n name: string;\n icon?: string | null;\n owner?: boolean;\n permissions?: string;\n features: string[];\n}\n\ninterface DiscordChannel {\n id: string;\n type: number;\n guild_id?: string;\n position?: number;\n name?: string;\n topic?: string | null;\n nsfw?: boolean;\n last_message_id?: string | null;\n bitrate?: number;\n user_limit?: number;\n rate_limit_per_user?: number;\n recipients?: DiscordUser[];\n icon?: string | null;\n owner_id?: string;\n application_id?: string;\n parent_id?: string | null;\n last_pin_timestamp?: string | null;\n rtc_region?: string | null;\n video_quality_mode?: number;\n message_count?: number;\n member_count?: number;\n flags?: number;\n}\n\ninterface DiscordMessage {\n id: string;\n channel_id: string;\n author: DiscordUser;\n content: string;\n timestamp: string;\n edited_timestamp?: string | null;\n tts: boolean;\n mention_everyone: boolean;\n mentions: DiscordUser[];\n mention_roles: string[];\n attachments: Array<{\n id: string;\n filename: string;\n size: number;\n url: string;\n proxy_url: string;\n height?: number | null;\n width?: number | null;\n content_type?: string;\n }>;\n embeds: unknown[];\n reactions?: Array<{\n count: number;\n me: boolean;\n emoji: {\n id: string | null;\n name: string | null;\n };\n }>;\n pinned: boolean;\n type: number;\n}\n\ninterface DiscordGuildMember {\n user?: DiscordUser;\n nick?: string | null;\n avatar?: string | null;\n roles: string[];\n joined_at: string;\n premium_since?: string | null;\n deaf: boolean;\n mute: boolean;\n flags: number;\n pending?: boolean;\n permissions?: string;\n communication_disabled_until?: string | null;\n}\n\nasync function discordFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with Discord. Please connect your account.\");\n }\n\n const response = await fetch(`${DISCORD_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 { message?: string };\n throw new Error(`Discord API error: ${response.status} ${error.message ?? response.statusText}`);\n }\n\n return response.json();\n}\n\nfunction buildQuery(\n options: Record<string, string | number | undefined>,\n limits?: Record<string, number>,\n): string {\n const params = new URLSearchParams();\n\n for (const [key, value] of Object.entries(options)) {\n if (value === undefined) continue;\n\n if (typeof value === \"number\") {\n const limit = limits?.[key];\n params.set(key, Math.min(value, limit ?? value).toString());\n continue;\n }\n\n params.set(key, value);\n }\n\n const query = params.toString();\n return query ? `?${query}` : \"\";\n}\n\nexport function getCurrentUser(): Promise<DiscordUser> {\n return discordFetch(\"/users/@me\");\n}\n\nexport function listGuilds(): Promise<DiscordGuild[]> {\n return discordFetch(\"/users/@me/guilds\");\n}\n\nexport function getGuild(guildId: string): Promise<DiscordGuild> {\n return discordFetch(`/guilds/${guildId}`);\n}\n\nexport function listChannels(guildId: string): Promise<DiscordChannel[]> {\n return discordFetch(`/guilds/${guildId}/channels`);\n}\n\nexport function getChannel(channelId: string): Promise<DiscordChannel> {\n return discordFetch(`/channels/${channelId}`);\n}\n\nexport function getMessages(\n channelId: string,\n options?: {\n limit?: number;\n before?: string;\n after?: string;\n around?: string;\n },\n): Promise<DiscordMessage[]> {\n const query = buildQuery(\n {\n limit: options?.limit,\n before: options?.before,\n after: options?.after,\n around: options?.around,\n },\n { limit: 100 },\n );\n\n return discordFetch(`/channels/${channelId}/messages${query}`);\n}\n\nexport function sendMessage(\n channelId: string,\n content: string,\n options?: {\n tts?: boolean;\n embeds?: unknown[];\n },\n): Promise<DiscordMessage> {\n const body: Record<string, unknown> = { content };\n\n if (options?.tts !== undefined) body.tts = options.tts;\n if (options?.embeds) body.embeds = options.embeds;\n\n return discordFetch(`/channels/${channelId}/messages`, {\n method: \"POST\",\n body: JSON.stringify(body),\n });\n}\n\nexport function getGuildMembers(\n guildId: string,\n options?: {\n limit?: number;\n after?: string;\n },\n): Promise<DiscordGuildMember[]> {\n const query = buildQuery({ limit: options?.limit, after: options?.after }, { limit: 1000 });\n return discordFetch(`/guilds/${guildId}/members${query}`);\n}\n\nexport function formatUsername(user: DiscordUser): string {\n if (user.discriminator === \"0\") return user.username;\n return `${user.username}#${user.discriminator}`;\n}\n\nfunction getCdnAssetUrl(\n basePath: string,\n id: string,\n hash: string | null | undefined,\n size: number,\n): string | null {\n if (!hash) return null;\n const extension = hash.startsWith(\"a_\") ? \"gif\" : \"png\";\n return `https://cdn.discordapp.com/${basePath}/${id}/${hash}.${extension}?size=${size}`;\n}\n\nexport function getAvatarUrl(user: DiscordUser, size: number = 128): string | null {\n return getCdnAssetUrl(\"avatars\", user.id, user.avatar, size);\n}\n\nexport function getGuildIconUrl(guild: DiscordGuild, size: number = 128): string | null {\n return getCdnAssetUrl(\"icons\", guild.id, guild.icon, size);\n}\n\nconst CHANNEL_TYPE_NAMES: Record<number, string> = {\n 0: \"Text\",\n 1: \"DM\",\n 2: \"Voice\",\n 3: \"Group DM\",\n 4: \"Category\",\n 5: \"Announcement\",\n 10: \"Announcement Thread\",\n 11: \"Public Thread\",\n 12: \"Private Thread\",\n 13: \"Stage Voice\",\n 14: \"Directory\",\n 15: \"Forum\",\n};\n\nexport function getChannelTypeName(type: number): string {\n return CHANNEL_TYPE_NAMES[type] ?? \"Unknown\";\n}\n",
|
|
200
|
-
"tools/get-messages.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { formatUsername, getMessages } from \"../../lib/discord-client.ts\";\n\nexport default tool({\n id: \"get-messages\",\n description:\n \"Get recent messages from a Discord channel. Returns message content, authors, timestamps, and attachments.\",\n inputSchema: defineSchema((v) => v.object({\n channelId: v.string().describe(\"The ID of the Discord channel to get messages from\"),\n limit: v\n .number()\n .min(1)\n .max(100)\n .default(50)\n .describe(\"Maximum number of messages to retrieve (1-100)\"),\n before: v.string().optional().describe(\"Get messages before this message ID\"),\n after: v.string().optional().describe(\"Get messages after this message ID\"),\n }))(),\n async execute({ channelId, limit, before, after }) {\n const messages = await getMessages(channelId, { limit, before, after });\n\n return messages.map((message) => ({\n id: message.id,\n content: message.content,\n author: {\n id: message.author.id,\n username: formatUsername(message.author),\n globalName: message.author.global_name,\n bot: message.author.bot,\n },\n timestamp: message.timestamp,\n editedTimestamp: message.edited_timestamp,\n pinned: message.pinned,\n mentions: message.mentions.map((user) => ({\n id: user.id,\n username: formatUsername(user),\n })),\n attachments: message.attachments.map((attachment) => ({\n id: attachment.id,\n filename: attachment.filename,\n url: attachment.url,\n size: attachment.size,\n contentType: attachment.content_type,\n })),\n hasEmbeds: message.embeds.length > 0,\n reactions: message.reactions?.map((reaction) => ({\n emoji: reaction.emoji.name,\n count: reaction.count,\n meReacted: reaction.me,\n })),\n }));\n },\n});\n",
|
|
201
|
-
"tools/get-user.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { formatUsername, getAvatarUrl, getCurrentUser } from \"../../lib/discord-client.ts\";\n\nexport default tool({\n id: \"get-user\",\n description:\n \"Get information about the authenticated Discord user. Returns username, ID, avatar, and account details.\",\n inputSchema: defineSchema((v) => v.object({\n includeAvatar: v.boolean().default(true).describe(\"Whether to include the avatar URL\"),\n }))(),\n async execute({ includeAvatar }) {\n const user = await getCurrentUser();\n\n return {\n id: user.id,\n username: formatUsername(user),\n globalName: user.global_name,\n avatar: includeAvatar ? getAvatarUrl(user) : undefined,\n bot: user.bot,\n system: user.system,\n mfaEnabled: user.mfa_enabled,\n banner: user.banner,\n accentColor: user.accent_color,\n locale: user.locale,\n verified: user.verified,\n email: user.email,\n premiumType: user.premium_type,\n publicFlags: user.public_flags,\n };\n },\n});\n",
|
|
202
|
-
"tools/list-channels.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { getChannelTypeName, listChannels } from \"../../lib/discord-client.ts\";\n\nexport default tool({\n id: \"list-channels\",\n description:\n \"List all channels in a Discord server (guild). Returns channel names, IDs, types, and basic information.\",\n inputSchema: defineSchema((v) => v.object({\n guildId: v.string().describe(\"The ID of the Discord server (guild) to list channels from\"),\n includeCategories: v.boolean().default(true).describe(\"Whether to include category channels\"),\n }))(),\n async execute({ guildId, includeCategories }) {\n const channels = await listChannels(guildId);\n\n const filteredChannels = includeCategories\n ? channels\n : channels.filter((channel) => channel.type !== 4);\n\n return filteredChannels.map((channel) => ({\n id: channel.id,\n name: channel.name,\n type: getChannelTypeName(channel.type),\n typeId: channel.type,\n topic: channel.topic,\n nsfw: channel.nsfw,\n position: channel.position,\n parentId: channel.parent_id,\n lastMessageId: channel.last_message_id,\n }));\n },\n});\n",
|
|
203
|
-
"tools/list-guilds.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { getGuildIconUrl, listGuilds } from \"../../lib/discord-client.ts\";\n\nexport default tool({\n id: \"list-guilds\",\n description:\n \"List all Discord servers (guilds) the authenticated user is a member of. Returns server names, IDs, and basic information.\",\n inputSchema: defineSchema((v) => v.object({\n includeIcons: v\n .boolean()\n .default(false)\n .describe(\"Whether to include icon URLs for servers\"),\n }))(),\n async execute({ includeIcons }) {\n const guilds = await listGuilds();\n\n return guilds.map((guild) => ({\n id: guild.id,\n name: guild.name,\n owner: guild.owner,\n icon: includeIcons ? getGuildIconUrl(guild) : undefined,\n features: guild.features,\n permissions: guild.permissions,\n }));\n },\n});\n",
|
|
204
|
-
"tools/send-message.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { formatUsername, sendMessage } from \"../../lib/discord-client.ts\";\n\nexport default tool({\n id: \"send-message\",\n description: \"Send a message to a Discord channel. Returns the sent message details.\",\n inputSchema: defineSchema((v) => v.object({\n channelId: v.string().describe(\"The ID of the Discord channel to send the message to\"),\n content: v\n .string()\n .min(1)\n .max(2000)\n .describe(\"The message content to send (1-2000 characters)\"),\n tts: v\n .boolean()\n .default(false)\n .describe(\"Whether this message should be sent as text-to-speech\"),\n }))(),\n async execute({ channelId, content, tts }) {\n const message = await sendMessage(channelId, content, { tts });\n\n return {\n id: message.id,\n content: message.content,\n channelId: message.channel_id,\n timestamp: message.timestamp,\n author: {\n id: message.author.id,\n username: formatUsername(message.author),\n globalName: message.author.global_name,\n },\n tts: message.tts,\n };\n },\n});\n"
|
|
205
|
-
}
|
|
206
|
-
},
|
|
207
|
-
"integration:docs-google": {
|
|
208
|
-
"files": {
|
|
209
|
-
".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",
|
|
210
|
-
"app/api/auth/docs-google/callback/route.ts": "import { createOAuthCallbackHandler, docsGoogleConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string, userId: string) {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(\n serviceId: string,\n userId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string) {\n await tokenStore.revokeToken(userId, serviceId);\n },\n setState(\n state: string,\n meta: {\n userId: string;\n serviceId: string;\n codeVerifier?: string;\n redirectUri?: string;\n scopes?: string[];\n createdAt: number;\n },\n ) {\n return oauthMemoryTokenStore.setState(state, meta);\n },\n consumeState(state: string) {\n return oauthMemoryTokenStore.consumeState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(docsGoogleConfig, { tokenStore: hybridTokenStore });\n",
|
|
211
|
-
"app/api/auth/docs-google/route.ts": "import { createOAuthInitHandler, docsGoogleConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../../lib/user-id.ts\";\n\nfunction getUserId(request: Request): string {\n return requireUserIdFromRequest(request);\n}\n\nexport const GET = createOAuthInitHandler(docsGoogleConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});",
|
|
212
|
-
"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 \"./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/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",
|
|
213
|
-
"lib/oauth.ts": "import { type OAuthToken, tokenStore } 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 const token = await tokenStore.getToken(userId, service);\n if (!token) return null;\n\n const isExpired = token.expiresAt\n ? token.expiresAt < Date.now() + 5 * 60 * 1000\n : false;\n\n if (!isExpired || !token.refreshToken) return token.accessToken;\n\n try {\n const newToken = await refreshAccessToken(provider, token.refreshToken);\n await tokenStore.setToken(userId, service, newToken);\n return newToken.accessToken;\n } catch {\n await tokenStore.revokeToken(userId, service);\n return null;\n }\n}\n",
|
|
214
|
-
"tools/create-document.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createDocsClient } from \"../../lib/docs-client.ts\";\n\nconst DEFAULT_USER_ID = \"demo-user\";\n\nexport default tool({\n id: \"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) => 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 async execute({ title, content }) {\n const client = createDocsClient(DEFAULT_USER_ID);\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",
|
|
215
|
-
"tools/get-document.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createDocsClient } from \"../../lib/docs-client.ts\";\n\nconst DEFAULT_USER_ID = \"demo-user\";\n\nexport default tool({\n id: \"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) => 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 async execute({ documentId, extractTextOnly }) {\n const client = createDocsClient(DEFAULT_USER_ID);\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",
|
|
216
|
-
"tools/list-documents.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createDocsClient } from \"../../lib/docs-client.ts\";\n\nconst DEFAULT_USER_ID = \"demo-user\";\n\nexport default tool({\n id: \"list-documents\",\n description:\n \"List recent Google Docs documents from Google Drive. Returns document names, IDs, and metadata.\",\n inputSchema: defineSchema((v) => 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\"])\n .default(\"modifiedTime\")\n .describe(\"Sort order for results\"),\n }))(),\n async execute({ maxResults, orderBy }) {\n const client = createDocsClient(DEFAULT_USER_ID);\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",
|
|
217
|
-
"tools/search-documents.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createDocsClient } from \"../../lib/docs-client.ts\";\n\nconst DEFAULT_USER_ID = \"demo-user\";\n\nexport default tool({\n id: \"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) => 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 async execute({ query, maxResults }) {\n const client = createDocsClient(DEFAULT_USER_ID);\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",
|
|
218
|
-
"tools/update-document.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createDocsClient, type Request } from \"../../lib/docs-client.ts\";\n\nconst DEFAULT_USER_ID = \"demo-user\";\n\nexport default tool({\n id: \"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) => v\n .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 .or(\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\"])\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 async execute(input): Promise<{\n documentId: string;\n success: true;\n replies: unknown;\n writeControl?: unknown;\n }> {\n const client = createDocsClient(DEFAULT_USER_ID);\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 default:\n throw new Error(`Unknown operation type: ${operation.type}`);\n }\n },\n});\n"
|
|
219
|
-
}
|
|
220
|
-
},
|
|
221
|
-
"integration:drive": {
|
|
222
|
-
"files": {
|
|
223
|
-
".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",
|
|
224
|
-
"app/api/auth/drive/callback/route.ts": "/**\n * Google Drive OAuth Callback\n *\n * Handles the OAuth callback from Google and stores the tokens.\n */\n\nimport { createOAuthCallbackHandler, driveConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string, userId: string) {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(\n serviceId: string,\n userId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string) {\n await tokenStore.revokeToken(userId, serviceId);\n },\n setState(\n state: string,\n meta: {\n userId: string;\n serviceId: string;\n codeVerifier?: string;\n redirectUri?: string;\n scopes?: string[];\n createdAt: number;\n },\n ) {\n return oauthMemoryTokenStore.setState(state, meta);\n },\n consumeState(state: string) {\n return oauthMemoryTokenStore.consumeState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(driveConfig, { tokenStore: hybridTokenStore });\n",
|
|
225
|
-
"app/api/auth/drive/route.ts": "import { createOAuthInitHandler, driveConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../../lib/user-id.ts\";\n\nfunction getUserId(request: Request): string {\n return requireUserIdFromRequest(request);\n}\n\nexport const GET = createOAuthInitHandler(driveConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});",
|
|
226
|
-
"lib/drive-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 // @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.readonly\",\n \"https://www.googleapis.com/auth/drive.file\",\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",
|
|
227
|
-
"lib/oauth.ts": "import { type OAuthToken, tokenStore } 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 const token = await tokenStore.getToken(userId, service);\n if (!token) return null;\n\n const isExpired = token.expiresAt\n ? token.expiresAt < Date.now() + 5 * 60 * 1000\n : false;\n\n if (!isExpired) return token.accessToken;\n if (!token.refreshToken) return token.accessToken;\n\n try {\n const newToken = await refreshAccessToken(provider, token.refreshToken);\n await tokenStore.setToken(userId, service, newToken);\n return newToken.accessToken;\n } catch {\n await tokenStore.revokeToken(userId, service);\n return null;\n }\n}\n",
|
|
228
|
-
"tools/create-folder.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createDriveClient } from \"../../lib/drive-client.ts\";\n\nconst DEFAULT_USER_ID = \"demo-user\";\n\nexport default tool({\n id: \"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) => 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 async execute({ name, parentId, description }) {\n const client = createDriveClient(DEFAULT_USER_ID);\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",
|
|
229
|
-
"tools/get-file.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createDriveClient } from \"../../lib/drive-client.ts\";\n\nconst DEFAULT_USER_ID = \"demo-user\";\nconst FOLDER_MIME_TYPE = \"application/vnd.google-apps.folder\";\n\nexport default tool({\n id: \"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) => v.object({\n fileId: v.string().describe(\"The ID of the file or folder to retrieve\"),\n }))(),\n async execute({ fileId }) {\n const client = createDriveClient(DEFAULT_USER_ID);\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",
|
|
230
|
-
"tools/list-files.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createDriveClient } from \"../../lib/drive-client.ts\";\n\nconst DEFAULT_USER_ID = \"demo-user\";\nconst FOLDER_MIME_TYPE = \"application/vnd.google-apps.folder\";\n\nexport default tool({\n id: \"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) => 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 async execute({ folderId, pageSize, pageToken, orderBy }) {\n const client = createDriveClient(DEFAULT_USER_ID);\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",
|
|
231
|
-
"tools/search-files.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createDriveClient } from \"../../lib/drive-client.ts\";\n\nconst DEFAULT_USER_ID = \"demo-user\";\nconst FOLDER_MIME_TYPE = \"application/vnd.google-apps.folder\";\n\nexport default tool({\n id: \"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) => 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 async execute({ query, pageSize, pageToken, orderBy }) {\n const client = createDriveClient(DEFAULT_USER_ID);\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",
|
|
232
|
-
"tools/upload-file.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createDriveClient } from \"../../lib/drive-client.ts\";\n\nconst DEFAULT_USER_ID = \"demo-user\";\n\nexport default tool({\n id: \"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) => 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 async execute({ name, content, mimeType, parentId, description }) {\n const client = createDriveClient(DEFAULT_USER_ID);\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"
|
|
233
|
-
}
|
|
234
|
-
},
|
|
235
|
-
"integration:dropbox": {
|
|
236
|
-
"files": {
|
|
237
|
-
".env.example": "# Dropbox Integration Environment Variables\n\n# Dropbox App Key (Client ID)\n# Get this from https://www.dropbox.com/developers/apps\nDROPBOX_APP_KEY=your_app_key_here\n\n# Dropbox App Secret\n# Get this from https://www.dropbox.com/developers/apps\nDROPBOX_APP_SECRET=your_app_secret_here\n\n# Setup Instructions:\n# 1. Go to https://www.dropbox.com/developers/apps\n# 2. Create a new app or select an existing one\n# 3. Choose \"Scoped access\" as the API type\n# 4. Select \"Full Dropbox\" or \"App folder\" access\n# 5. Copy the App Key and App Secret\n# 6. Add the OAuth2 redirect URI: http://localhost:3000/api/auth/dropbox/callback\n# 7. Enable the following permissions in the Permissions tab:\n# - files.content.read\n# - files.content.write\n# - files.metadata.read\n# - files.metadata.write\n# - account_info.read\n# 8. Submit the app for production use if needed\n",
|
|
238
|
-
"app/api/auth/dropbox/callback/route.ts": "import { createOAuthCallbackHandler, dropboxConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string, userId: string) {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(\n serviceId: string,\n userId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string) {\n await tokenStore.revokeToken(userId, serviceId);\n },\n setState(\n state: string,\n meta: {\n userId: string;\n serviceId: string;\n codeVerifier?: string;\n redirectUri?: string;\n scopes?: string[];\n createdAt: number;\n },\n ) {\n return oauthMemoryTokenStore.setState(state, meta);\n },\n consumeState(state: string) {\n return oauthMemoryTokenStore.consumeState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(dropboxConfig, { tokenStore: hybridTokenStore });\n",
|
|
239
|
-
"app/api/auth/dropbox/route.ts": "import { createOAuthInitHandler, dropboxConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../../lib/user-id.ts\";\n\nfunction getUserId(request: Request): string {\n return requireUserIdFromRequest(request);\n}\n\nexport const GET = createOAuthInitHandler(dropboxConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});",
|
|
240
|
-
"lib/dropbox-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst DROPBOX_API_URL = \"https://api.dropboxapi.com/2\";\nconst DROPBOX_CONTENT_URL = \"https://content.dropboxapi.com/2\";\n\nexport interface DropboxMetadata {\n \".tag\": \"file\" | \"folder\" | \"deleted\";\n name: string;\n path_lower?: string;\n path_display?: string;\n id: string;\n}\n\nexport interface DropboxFileMetadata extends DropboxMetadata {\n \".tag\": \"file\";\n client_modified: string;\n server_modified: string;\n rev: string;\n size: number;\n is_downloadable: boolean;\n content_hash?: string;\n}\n\nexport interface DropboxFolderMetadata extends DropboxMetadata {\n \".tag\": \"folder\";\n}\n\nexport interface ListFolderResult {\n entries: Array<DropboxFileMetadata | DropboxFolderMetadata>;\n cursor: string;\n has_more: boolean;\n}\n\nexport interface SearchResult {\n matches: Array<{\n match_type: {\n \".tag\": \"filename\" | \"content\" | \"both\";\n };\n metadata: {\n \".tag\": \"metadata\";\n metadata: DropboxFileMetadata | DropboxFolderMetadata;\n };\n }>;\n has_more: boolean;\n cursor?: string;\n}\n\nexport interface AccountInfo {\n account_id: string;\n name: {\n given_name: string;\n surname: string;\n familiar_name: string;\n display_name: string;\n };\n email: string;\n email_verified: boolean;\n disabled: boolean;\n country: string;\n locale: string;\n account_type: {\n \".tag\": \"basic\" | \"pro\" | \"business\";\n };\n}\n\nexport interface SpaceUsage {\n used: number;\n allocation: {\n \".tag\": \"individual\" | \"team\";\n allocated?: number;\n };\n}\n\nexport interface SharedLinkMetadata {\n url: string;\n id: string;\n name: string;\n path_lower?: string;\n link_permissions: {\n can_revoke: boolean;\n resolved_visibility?: {\n \".tag\": \"public\" | \"team_only\" | \"password\";\n };\n };\n}\n\nasync function requireAccessToken(): Promise<string> {\n const token = await getAccessToken();\n if (token) return token;\n throw new Error(\"Not authenticated with Dropbox. Please connect your account.\");\n}\n\nasync function parseDropboxError(response: Response): Promise<any> {\n return response.json().catch(() => ({}));\n}\n\nfunction throwDropboxError(response: Response, error: any): never {\n throw new Error(\n `Dropbox API error: ${response.status} ${error?.error_summary ?? response.statusText}`,\n );\n}\n\nasync function dropboxRPC<T>(\n endpoint: string,\n body: Record<string, unknown> = {},\n): Promise<T> {\n const token = await requireAccessToken();\n\n const response = await fetch(`${DROPBOX_API_URL}${endpoint}`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n throwDropboxError(response, await parseDropboxError(response));\n }\n\n return response.json();\n}\n\nasync function dropboxContent<T>(\n endpoint: string,\n args: Record<string, unknown>,\n content?: string | Uint8Array,\n): Promise<T> {\n const token = await requireAccessToken();\n\n const headers: Record<string, string> = {\n Authorization: `Bearer ${token}`,\n \"Dropbox-API-Arg\": JSON.stringify(args),\n };\n\n if (content != null) {\n headers[\"Content-Type\"] = \"application/octet-stream\";\n }\n\n const response = await fetch(`${DROPBOX_CONTENT_URL}${endpoint}`, {\n method: \"POST\",\n headers,\n body: content,\n });\n\n if (!response.ok) {\n throwDropboxError(response, await parseDropboxError(response));\n }\n\n return response.json();\n}\n\nexport function getCurrentAccount(): Promise<AccountInfo> {\n return dropboxRPC<AccountInfo>(\"/users/get_current_account\");\n}\n\nexport function getSpaceUsage(): Promise<SpaceUsage> {\n return dropboxRPC<SpaceUsage>(\"/users/get_space_usage\");\n}\n\nexport function listFolder(\n path: string = \"\",\n options?: {\n recursive?: boolean;\n includeDeleted?: boolean;\n includeHasExplicitSharedMembers?: boolean;\n limit?: number;\n },\n): Promise<ListFolderResult> {\n return dropboxRPC<ListFolderResult>(\"/files/list_folder\", {\n path: path || \"\",\n recursive: options?.recursive ?? false,\n include_deleted: options?.includeDeleted ?? false,\n include_has_explicit_shared_members: options?.includeHasExplicitSharedMembers ?? false,\n limit: options?.limit ?? 100,\n });\n}\n\nexport function listFolderContinue(cursor: string): Promise<ListFolderResult> {\n return dropboxRPC<ListFolderResult>(\"/files/list_folder/continue\", { cursor });\n}\n\nexport function getMetadata(\n path: string,\n options?: {\n includeMediaInfo?: boolean;\n includeDeleted?: boolean;\n },\n): Promise<DropboxFileMetadata | DropboxFolderMetadata> {\n return dropboxRPC<DropboxFileMetadata | DropboxFolderMetadata>(\"/files/get_metadata\", {\n path,\n include_media_info: options?.includeMediaInfo ?? false,\n include_deleted: options?.includeDeleted ?? false,\n });\n}\n\nexport async function downloadFile(path: string): Promise<{\n content: string;\n metadata: DropboxFileMetadata;\n}> {\n const token = await requireAccessToken();\n\n const response = await fetch(`${DROPBOX_CONTENT_URL}/files/download`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${token}`,\n \"Dropbox-API-Arg\": JSON.stringify({ path }),\n },\n });\n\n if (!response.ok) {\n throwDropboxError(response, await parseDropboxError(response));\n }\n\n const content = await response.text();\n const metadataHeader = response.headers.get(\"Dropbox-API-Result\");\n const metadata = metadataHeader ? JSON.parse(metadataHeader) : {};\n\n return { content, metadata };\n}\n\nexport function uploadFile(\n path: string,\n content: string | Uint8Array,\n options?: {\n mode?: \"add\" | \"overwrite\" | \"update\";\n autorename?: boolean;\n mute?: boolean;\n },\n): Promise<DropboxFileMetadata> {\n return dropboxContent<DropboxFileMetadata>(\n \"/files/upload\",\n {\n path,\n mode: options?.mode ?? \"add\",\n autorename: options?.autorename ?? false,\n mute: options?.mute ?? false,\n },\n content,\n );\n}\n\nexport function deleteFile(\n path: string,\n): Promise<DropboxFileMetadata | DropboxFolderMetadata> {\n return dropboxRPC<{ metadata: DropboxFileMetadata | DropboxFolderMetadata }>(\n \"/files/delete_v2\",\n { path },\n ).then((result) => result.metadata);\n}\n\nexport function moveFile(\n fromPath: string,\n toPath: string,\n options?: {\n allowSharedFolder?: boolean;\n autorename?: boolean;\n allowOwnershipTransfer?: boolean;\n },\n): Promise<DropboxFileMetadata | DropboxFolderMetadata> {\n return dropboxRPC<{ metadata: DropboxFileMetadata | DropboxFolderMetadata }>(\n \"/files/move_v2\",\n {\n from_path: fromPath,\n to_path: toPath,\n allow_shared_folder: options?.allowSharedFolder ?? false,\n autorename: options?.autorename ?? false,\n allow_ownership_transfer: options?.allowOwnershipTransfer ?? false,\n },\n ).then((result) => result.metadata);\n}\n\nexport function copyFile(\n fromPath: string,\n toPath: string,\n options?: {\n allowSharedFolder?: boolean;\n autorename?: boolean;\n allowOwnershipTransfer?: boolean;\n },\n): Promise<DropboxFileMetadata | DropboxFolderMetadata> {\n return dropboxRPC<{ metadata: DropboxFileMetadata | DropboxFolderMetadata }>(\n \"/files/copy_v2\",\n {\n from_path: fromPath,\n to_path: toPath,\n allow_shared_folder: options?.allowSharedFolder ?? false,\n autorename: options?.autorename ?? false,\n allow_ownership_transfer: options?.allowOwnershipTransfer ?? false,\n },\n ).then((result) => result.metadata);\n}\n\nexport function createFolder(\n path: string,\n autorename?: boolean,\n): Promise<DropboxFolderMetadata> {\n return dropboxRPC<{ metadata: DropboxFolderMetadata }>(\"/files/create_folder_v2\", {\n path,\n autorename: autorename ?? false,\n }).then((result) => result.metadata);\n}\n\nexport function searchFiles(\n query: string,\n options?: {\n path?: string;\n maxResults?: number;\n fileCategories?: Array<\n | \"image\"\n | \"document\"\n | \"pdf\"\n | \"spreadsheet\"\n | \"presentation\"\n | \"audio\"\n | \"video\"\n | \"folder\"\n | \"paper\"\n | \"others\"\n >;\n fileExtensions?: string[];\n },\n): Promise<SearchResult> {\n return dropboxRPC<SearchResult>(\"/files/search_v2\", {\n query,\n options: {\n path: options?.path ?? \"\",\n max_results: options?.maxResults ?? 20,\n file_categories: options?.fileCategories,\n filename_only: false,\n },\n });\n}\n\nexport async function createSharedLink(\n path: string,\n settings?: {\n requestedVisibility?: \"public\" | \"team_only\" | \"password\";\n linkPassword?: string;\n expires?: string;\n },\n): Promise<SharedLinkMetadata> {\n try {\n return await dropboxRPC<SharedLinkMetadata>(\"/sharing/create_shared_link_with_settings\", {\n path,\n settings: settings ?? {},\n });\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"shared_link_already_exists\")) {\n const links = await listSharedLinks(path);\n if (links.length > 0) return links[0];\n }\n throw error;\n }\n}\n\nexport async function listSharedLinks(path?: string): Promise<SharedLinkMetadata[]> {\n const result = await dropboxRPC<{ links: SharedLinkMetadata[] }>(\"/sharing/list_shared_links\", {\n path: path ?? \"\",\n });\n return result.links;\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(metadata: DropboxMetadata): metadata is DropboxFileMetadata {\n return metadata[\".tag\"] === \"file\";\n}\n\nexport function isFolder(metadata: DropboxMetadata): metadata is DropboxFolderMetadata {\n return metadata[\".tag\"] === \"folder\";\n}\n",
|
|
241
|
-
"tools/get-account.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport {\n formatFileSize,\n getCurrentAccount,\n getSpaceUsage,\n} from \"../../lib/dropbox-client.ts\";\n\nexport default tool({\n id: \"get-account\",\n description:\n \"Get current Dropbox account information including user details and storage usage.\",\n inputSchema: defineSchema((v) => v.object({\n includeSpaceUsage: v\n .boolean()\n .default(true)\n .describe(\"Whether to include storage usage information\"),\n }))(),\n async execute({ includeSpaceUsage }): Promise<Record<string, unknown>> {\n const account = await getCurrentAccount();\n\n const result: Record<string, unknown> = {\n accountId: account.account_id,\n name: {\n displayName: account.name.display_name,\n givenName: account.name.given_name,\n surname: account.name.surname,\n familiarName: account.name.familiar_name,\n },\n email: account.email,\n emailVerified: account.email_verified,\n accountType: account.account_type[\".tag\"],\n country: account.country,\n locale: account.locale,\n disabled: account.disabled,\n };\n\n if (!includeSpaceUsage) return result;\n\n try {\n const spaceUsage = await getSpaceUsage();\n const used = spaceUsage.used;\n const allocated = spaceUsage.allocation.allocated ?? 0;\n const hasAllocated = allocated > 0;\n\n result.storage = {\n used,\n usedFormatted: formatFileSize(used),\n allocated,\n allocatedFormatted: hasAllocated ? formatFileSize(allocated) : \"N/A\",\n allocationType: spaceUsage.allocation[\".tag\"],\n percentUsed: hasAllocated ? Math.round((used / allocated) * 100) : 0,\n available: hasAllocated ? allocated - used : 0,\n availableFormatted: hasAllocated\n ? formatFileSize(allocated - used)\n : \"N/A\",\n };\n } catch (error) {\n result.storageError =\n error instanceof Error ? error.message : \"Failed to get storage usage\";\n }\n\n return result;\n },\n});\n",
|
|
242
|
-
"tools/get-file.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { downloadFile, formatFileSize, getMetadata, isFile } from \"../../lib/dropbox-client.ts\";\n\nexport default tool({\n id: \"get-file\",\n description:\n \"Get file metadata and optionally download file content from Dropbox. Use this to read file information or retrieve file contents.\",\n inputSchema: defineSchema((v) => v.object({\n path: v.string().describe('Path to the file in Dropbox (e.g., \"/Documents/file.txt\")'),\n includeContent: v\n .boolean()\n .default(false)\n .describe(\"Whether to download and return the file content (only works for text files and small files)\"),\n }))(),\n async execute({ path, includeContent }): Promise<Record<string, unknown>> {\n const metadata = await getMetadata(path);\n\n if (!isFile(metadata)) {\n throw new Error(`Path \"${path}\" is not a file, it's a ${metadata[\".tag\"]}`);\n }\n\n const result: Record<string, unknown> = {\n name: metadata.name,\n path: metadata.path_display ?? metadata.path_lower ?? \"\",\n id: metadata.id,\n size: metadata.size,\n sizeFormatted: formatFileSize(metadata.size),\n modified: metadata.server_modified,\n clientModified: metadata.client_modified,\n isDownloadable: metadata.is_downloadable,\n rev: metadata.rev,\n };\n\n if (!includeContent) {\n return result;\n }\n\n if (!metadata.is_downloadable) {\n throw new Error(`File \"${path}\" is not downloadable`);\n }\n\n const maxSize = 1024 * 1024;\n if (metadata.size > maxSize) {\n throw new Error(\n `File is too large to download content (${formatFileSize(\n metadata.size,\n )}). Maximum size is 1MB. Use includeContent: false to get metadata only.`,\n );\n }\n\n try {\n const { content } = await downloadFile(path);\n result.content = content;\n result.contentLength = content.length;\n } catch (error) {\n result.contentError = error instanceof Error ? error.message : \"Failed to download content\";\n }\n\n return result;\n },\n});\n",
|
|
243
|
-
"tools/list-files.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { formatFileSize, isFile, listFolder } from \"../../lib/dropbox-client.ts\";\n\nexport default tool({\n id: \"list-files\",\n description:\n \"List files and folders in a Dropbox folder. Returns file/folder names, types, sizes, and modification dates.\",\n inputSchema: defineSchema((v) => v.object({\n path: v\n .string()\n .default(\"\")\n .describe(\n 'Path to the folder to list (empty string for root, or \"/FolderName\" for specific folder)',\n ),\n recursive: v.boolean().default(false).describe(\"Whether to list files recursively in subfolders\"),\n limit: v.number().min(1).max(500).default(100).describe(\"Maximum number of items to return\"),\n }))(),\n async execute({ path, recursive, limit }) {\n const result = await listFolder(path, { recursive, limit });\n\n const items = result.entries.map((entry) => {\n const baseInfo = {\n name: entry.name,\n path: entry.path_display ?? entry.path_lower ?? \"\",\n id: entry.id,\n type: entry[\".tag\"],\n };\n\n if (!isFile(entry)) {\n return baseInfo;\n }\n\n return {\n ...baseInfo,\n size: entry.size,\n sizeFormatted: formatFileSize(entry.size),\n modified: entry.server_modified,\n clientModified: entry.client_modified,\n isDownloadable: entry.is_downloadable,\n rev: entry.rev,\n };\n });\n\n return {\n items,\n count: items.length,\n hasMore: result.has_more,\n cursor: result.cursor,\n };\n },\n});\n",
|
|
244
|
-
"tools/search-files.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { formatFileSize, isFile, searchFiles } from \"../../lib/dropbox-client.ts\";\n\nexport default tool({\n id: \"search-files\",\n description:\n \"Search for files and folders in Dropbox 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 path: v.string().optional().describe(\"Optional path to limit search to a specific folder\"),\n maxResults: v.number().min(1).max(100).default(20).describe(\"Maximum number of results to return\"),\n fileCategories: v\n .array(\n v.enum([\n \"image\",\n \"document\",\n \"pdf\",\n \"spreadsheet\",\n \"presentation\",\n \"audio\",\n \"video\",\n \"folder\",\n \"paper\",\n \"others\",\n ]),\n )\n .optional()\n .describe(\"Filter by file categories\"),\n }))(),\n async execute({ query, path, maxResults, fileCategories }) {\n const result = await searchFiles(query, { path, maxResults, fileCategories });\n\n const matches = result.matches.map((match) => {\n const metadata = match.metadata.metadata;\n const baseInfo = {\n name: metadata.name,\n path: metadata.path_display ?? metadata.path_lower ?? \"\",\n id: metadata.id,\n type: metadata[\".tag\"],\n matchType: match.match_type[\".tag\"],\n };\n\n if (!isFile(metadata)) {\n return baseInfo;\n }\n\n return {\n ...baseInfo,\n size: metadata.size,\n sizeFormatted: formatFileSize(metadata.size),\n modified: metadata.server_modified,\n clientModified: metadata.client_modified,\n isDownloadable: metadata.is_downloadable,\n };\n });\n\n return {\n matches,\n count: matches.length,\n hasMore: result.has_more,\n query,\n };\n },\n});\n",
|
|
245
|
-
"tools/upload-file.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { formatFileSize, uploadFile } from \"../../lib/dropbox-client.ts\";\n\nexport default tool({\n id: \"upload-file\",\n description:\n \"Upload or update a file in Dropbox. Can create new files or overwrite existing ones.\",\n inputSchema: defineSchema((v) => v.object({\n path: v\n .string()\n .describe(\n 'Full path where the file should be saved in Dropbox (e.g., \"/Documents/notes.txt\")',\n ),\n content: v.string().describe(\"The content to write to the file\"),\n mode: v\n .enum([\"add\", \"overwrite\", \"update\"])\n .default(\"add\")\n .describe(\n 'Upload mode: \"add\" (fail if exists), \"overwrite\" (replace if exists), \"update\" (update specific revision)',\n ),\n autorename: v\n .boolean()\n .default(false)\n .describe(\"If true and file exists, automatically rename to avoid conflicts\"),\n }))(),\n async execute({ path, content, mode, autorename }) {\n if (!path.startsWith(\"/\")) {\n throw new Error('Path must start with \"/\" (e.g., \"/Documents/file.txt\")');\n }\n\n const result = await uploadFile(path, content, { mode, autorename, mute: false });\n const displayPath = result.path_display ?? result.path_lower ?? \"\";\n\n let message = `File updated successfully at ${result.path_display}`;\n if (mode === \"add\") {\n message = `File created successfully at ${result.path_display}`;\n }\n\n return {\n success: true,\n name: result.name,\n path: displayPath,\n id: result.id,\n size: result.size,\n sizeFormatted: formatFileSize(result.size),\n modified: result.server_modified,\n rev: result.rev,\n message,\n };\n },\n});\n"
|
|
246
|
-
}
|
|
247
|
-
},
|
|
248
|
-
"integration:figma": {
|
|
249
|
-
"files": {
|
|
250
|
-
".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",
|
|
251
|
-
"app/api/auth/figma/callback/route.ts": "import { createOAuthCallbackHandler, figmaConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string, userId: string) {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(\n serviceId: string,\n userId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string) {\n await tokenStore.revokeToken(userId, serviceId);\n },\n setState(\n state: string,\n meta: {\n userId: string;\n serviceId: string;\n codeVerifier?: string;\n redirectUri?: string;\n scopes?: string[];\n createdAt: number;\n },\n ) {\n return oauthMemoryTokenStore.setState(state, meta);\n },\n consumeState(state: string) {\n return oauthMemoryTokenStore.consumeState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(figmaConfig, { tokenStore: hybridTokenStore });\n",
|
|
252
|
-
"app/api/auth/figma/route.ts": "import { createOAuthInitHandler, figmaConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../../lib/user-id.ts\";\n\nfunction getUserId(request: Request): string {\n return requireUserIdFromRequest(request);\n}\n\nexport const GET = createOAuthInitHandler(figmaConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});",
|
|
253
|
-
"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 FigmaProject {\n id: string;\n name: string;\n}\n\nexport interface FigmaTeamProject {\n id: string;\n name: 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 getTeamProjects(teamId: string): Promise<{ projects: FigmaTeamProject[] }> {\n return figmaFetch<{ projects: FigmaTeamProject[] }>(`/teams/${teamId}/projects`);\n}\n\nexport function getProjectFiles(projectId: string): Promise<{\n files: Array<{\n key: string;\n name: string;\n thumbnail_url: string;\n last_modified: string;\n }>;\n}> {\n return figmaFetch(`/projects/${projectId}/files`);\n}\n\nexport function getUserFiles(): Promise<{\n files: Array<{\n key: string;\n name: string;\n thumbnail_url: string;\n last_modified: string;\n }>;\n}> {\n throw new Error(\n \"Getting user files requires team ID. Use getTeamProjects and getProjectFiles instead.\",\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",
|
|
254
|
-
"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",
|
|
255
|
-
"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: \"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",
|
|
256
|
-
"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: \"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",
|
|
257
|
-
"tools/list-files.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { getProjectFiles, getTeamProjects } from \"../../lib/figma-client.ts\";\n\nexport default tool({\n id: \"list-files\",\n description:\n \"List Figma files in a team project. Returns file names, keys, thumbnails, and last modified dates.\",\n inputSchema: defineSchema((v) => v.object({\n teamId: v.string().describe(\"The team ID to list projects from\"),\n projectId: v\n .string()\n .optional()\n .describe(\"Optional project ID to filter files. If not provided, lists all projects\"),\n limit: v.number().min(1).max(50).default(20).describe(\"Maximum number of files to return\"),\n }))(),\n async execute({ teamId, projectId, limit }) {\n if (projectId) {\n const { files } = await getProjectFiles(projectId);\n return files.slice(0, limit).map((file) => ({\n key: file.key,\n name: file.name,\n thumbnailUrl: file.thumbnail_url,\n lastModified: file.last_modified,\n url: `https://www.figma.com/file/${file.key}`,\n }));\n }\n\n const { projects } = await getTeamProjects(teamId);\n return {\n projects: projects.slice(0, limit).map((project) => ({\n id: project.id,\n name: project.name,\n })),\n message: \"Use project IDs to get files with the projectId parameter\",\n };\n },\n});\n",
|
|
258
|
-
"tools/list-projects.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { getProjectFiles, getTeamProjects } from \"../../lib/figma-client.ts\";\n\nexport default tool({\n id: \"list-projects\",\n description:\n \"List all projects in a Figma team. Optionally include file counts and recent files for each project.\",\n inputSchema: defineSchema((v) => v.object({\n teamId: v.string().describe(\"The team ID to list projects from\"),\n includeFiles: v.boolean().default(false).describe(\"Include recent files for each project\"),\n filesPerProject: v\n .number()\n .min(1)\n .max(10)\n .default(5)\n .describe(\"Number of recent files to include per project (if includeFiles is true)\"),\n limit: v.number().min(1).max(50).default(20).describe(\"Maximum number of projects to return\"),\n }))(),\n async execute({ teamId, includeFiles, filesPerProject, limit }) {\n const { projects: allProjects } = await getTeamProjects(teamId);\n const projects = allProjects.slice(0, limit);\n\n if (!includeFiles) {\n return { projects: projects.map(({ id, name }) => ({ id, name })) };\n }\n\n const projectsWithFiles = await Promise.all(\n projects.map(async ({ id, name }) => {\n try {\n const { files } = await getProjectFiles(id);\n const recentFiles = files.slice(0, filesPerProject).map((file) => ({\n key: file.key,\n name: file.name,\n thumbnailUrl: file.thumbnail_url,\n lastModified: file.last_modified,\n url: `https://www.figma.com/file/${file.key}`,\n }));\n\n return { id, name, fileCount: files.length, recentFiles };\n } catch (error) {\n return {\n id,\n name,\n fileCount: 0,\n recentFiles: [],\n error: error instanceof Error ? error.message : \"Unknown error\",\n };\n }\n }),\n );\n\n return { projects: projectsWithFiles, totalProjects: projects.length };\n },\n});\n",
|
|
259
|
-
"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: \"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"
|
|
260
|
-
}
|
|
261
|
-
},
|
|
262
|
-
"integration:github": {
|
|
263
|
-
"files": {
|
|
264
|
-
"app/api/auth/github/callback/route.ts": "import { createOAuthCallbackHandler, githubConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nconst hybridTokenStore = {\n async getTokens(serviceId: string, userId: string) {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(\n serviceId: string,\n userId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string) {\n await tokenStore.revokeToken(userId, serviceId);\n },\n setState(\n state: string,\n meta: {\n userId: string;\n serviceId: string;\n codeVerifier?: string;\n redirectUri?: string;\n scopes?: string[];\n createdAt: number;\n },\n ) {\n return oauthMemoryTokenStore.setState(state, meta);\n },\n consumeState(state: string) {\n return oauthMemoryTokenStore.consumeState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(githubConfig, { tokenStore: hybridTokenStore });\n",
|
|
265
|
-
"app/api/auth/github/route.ts": "import { createOAuthInitHandler, githubConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../../lib/user-id.ts\";\n\nfunction getUserId(request: Request): string {\n return requireUserIdFromRequest(request);\n}\n\nexport const GET = createOAuthInitHandler(githubConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});",
|
|
266
|
-
"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 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\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 listPullRequests(\n owner: string,\n repo: string,\n options?: { state?: \"open\" | \"closed\" | \"all\"; perPage?: number },\n ): Promise<GitHubPullRequest[]>;\n getPullRequest(owner: string, repo: string, pullNumber: number): Promise<GitHubPullRequest>;\n getPullRequestDiff(owner: string, repo: string, pullNumber: number): Promise<string>;\n createIssue(\n owner: string,\n repo: string,\n options: { title: string; body?: string; labels?: string[]; assignees?: string[] },\n ): Promise<GitHubIssue>;\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; perPage?: number },\n ): Promise<GitHubCommit[]>;\n getUser(): Promise<{ login: string; name: string; email: string }>;\n} {\n async function getAccessToken(): Promise<string> {\n const token = await getValidToken(githubOAuthProvider, userId, \"github\");\n if (!token) throw new Error(\"GitHub not connected. Please connect your GitHub 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(`${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(endpoint: string, accept: string): 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 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>(`/repos/${owner}/${repo}/pulls/${pullNumber}`);\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 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[]>(`/repos/${owner}/${repo}/issues${toQueryString(params)}`);\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.perPage) params.set(\"per_page\", String(options.perPage));\n\n return apiRequest<GitHubCommit[]>(`/repos/${owner}/${repo}/commits${toQueryString(params)}`);\n },\n\n getUser(): Promise<{ login: string; name: string; email: string }> {\n return apiRequest(\"/user\");\n },\n };\n}\n\nexport type GitHubClient = ReturnType<typeof createGitHubClient>;\n",
|
|
267
|
-
"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: \"create-issue\",\n description: \"Create a new issue in a GitHub repository\",\n inputSchema: defineSchema((v) => 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(\"Labels to add to the issue\"),\n assignees: v\n .array(v.string())\n .optional()\n .describe(\"GitHub usernames to assign to the issue\"),\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",
|
|
268
|
-
"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: \"get-pr-diff\",\n description: \"Get the diff for a pull request to review code changes\",\n inputSchema: defineSchema((v) => 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 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 const maxDiffLength = 50000;\n let truncatedDiff = diff;\n\n if (diff.length > maxDiffLength) {\n truncatedDiff = `${diff.substring(0, maxDiffLength)}\\n\\n... (diff truncated, ${\n diff.length - maxDiffLength\n } characters remaining)`;\n }\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: truncatedDiff,\n stats: {\n additions: pr.additions,\n deletions: pr.deletions,\n changedFiles: pr.changed_files,\n },\n message: `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",
|
|
269
|
-
"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: \"list-prs\",\n description: \"List pull requests for a GitHub repository\",\n inputSchema: defineSchema((v) => 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 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",
|
|
270
|
-
"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: \"list-repos\",\n description: \"List GitHub repositories for the authenticated user\",\n inputSchema: defineSchema((v) => 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 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"
|
|
271
|
-
}
|
|
272
|
-
},
|
|
273
|
-
"integration:gitlab": {
|
|
274
|
-
"files": {
|
|
275
|
-
".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",
|
|
276
|
-
"app/api/auth/gitlab/callback/route.ts": "import { createOAuthCallbackHandler, gitlabConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string, userId: string) {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(\n serviceId: string,\n userId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string) {\n await tokenStore.revokeToken(userId, serviceId);\n },\n setState(\n state: string,\n meta: {\n userId: string;\n serviceId: string;\n codeVerifier?: string;\n redirectUri?: string;\n scopes?: string[];\n createdAt: number;\n },\n ) {\n return oauthMemoryTokenStore.setState(state, meta);\n },\n consumeState(state: string) {\n return oauthMemoryTokenStore.consumeState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(gitlabConfig, { tokenStore: hybridTokenStore });\n",
|
|
277
|
-
"app/api/auth/gitlab/route.ts": "import { createOAuthInitHandler, gitlabConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../../lib/user-id.ts\";\n\nfunction getUserId(request: Request): string {\n return requireUserIdFromRequest(request);\n}\n\nexport const GET = createOAuthInitHandler(gitlabConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});",
|
|
278
|
-
"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\nfunction encodeProjectId(projectId: number | string): number | string {\n return typeof projectId === \"string\" ? encodeURIComponent(projectId) : projectId;\n}\n\nfunction buildQuery(params: URLSearchParams): string {\n const query = params.toString();\n return query ? `?${query}` : \"\";\n}\n\nasync function gitlabFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await getAccessToken();\n if (!token) throw new Error(\"Not authenticated with GitLab. Please connect your account.\");\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(projectId: number | string, issueIid: number): Promise<GitLabIssue> {\n return gitlabFetch<GitLabIssue>(`/projects/${encodeProjectId(projectId)}/issues/${issueIid}`);\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>(`/projects/${encodeProjectId(projectId)}/issues`, {\n method: \"POST\",\n body: JSON.stringify(body),\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) body.state_event = options.state === \"closed\" ? \"close\" : \"reopen\";\n if (options.labels) body.labels = options.labels.join(\",\");\n if (options.assigneeIds) body.assignee_ids = options.assigneeIds;\n\n return gitlabFetch<GitLabIssue>(`/projects/${encodeProjectId(projectId)}/issues/${issueIid}`, {\n method: \"PUT\",\n body: JSON.stringify(body),\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 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",
|
|
279
|
-
"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: \"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) => 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(\"Issue description in Markdown format\"),\n labels: v.array(v.string()).optional().describe('Labels to apply (e.g., [\"bug\", \"urgent\"])'),\n assigneeIds: v.array(v.number()).optional().describe(\"User IDs to assign the issue to\"),\n milestoneId: v.number().optional().describe(\"Milestone ID to associate with the issue\"),\n dueDate: v.string().optional().describe(\"Due date in YYYY-MM-DD format\"),\n }))(),\n async execute({ projectId, title, description, labels, assigneeIds, milestoneId, dueDate }) {\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 }) => ({ username, name })),\n webUrl: issue.web_url,\n createdAt: issue.created_at,\n },\n };\n },\n});\n",
|
|
280
|
-
"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: \"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) => 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 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",
|
|
281
|
-
"tools/list-merge-requests.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { formatMergeRequestForDisplay, listMergeRequests } from \"../../lib/gitlab-client.ts\";\n\nexport default tool({\n id: \"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) => 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(\"Maximum number of results to return\"),\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 =\n description.length > 200 ? `${description.substring(0, 200)}...` : 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",
|
|
282
|
-
"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: \"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) => v.object({\n search: v.string().optional().describe(\"Search query to filter projects by name or path\"),\n membership: v.boolean().default(true).describe(\"Only show projects where user is a member\"),\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(\"Maximum number of results to return\"),\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",
|
|
283
|
-
"tools/search-issues.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { formatIssueForDisplay, searchIssues } from \"../../lib/gitlab-client.ts\";\n\nexport default tool({\n id: \"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) => 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(\"Search query to filter issues by title or description\"),\n labels: v.array(v.string()).optional().describe('Filter by labels (e.g., [\"bug\", \"urgent\"])'),\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(\"Maximum number of results to return\"),\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 =\n description.length > 200 ? `${description.substring(0, 200)}...` : 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 }) => ({ username, name })),\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"
|
|
284
|
-
}
|
|
285
|
-
},
|
|
286
|
-
"integration:gmail": {
|
|
287
|
-
"files": {
|
|
288
|
-
".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",
|
|
289
|
-
"app/api/auth/gmail/callback/route.ts": "import { createOAuthCallbackHandler, gmailConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string, userId: string) {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(\n serviceId: string,\n userId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string) {\n await tokenStore.revokeToken(userId, serviceId);\n },\n setState(\n state: string,\n meta: {\n userId: string;\n serviceId: string;\n codeVerifier?: string;\n redirectUri?: string;\n scopes?: string[];\n createdAt: number;\n },\n ) {\n return oauthMemoryTokenStore.setState(state, meta);\n },\n consumeState(state: string) {\n return oauthMemoryTokenStore.consumeState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(gmailConfig, { tokenStore: hybridTokenStore });\n",
|
|
290
|
-
"app/api/auth/gmail/route.ts": "import { createOAuthInitHandler, gmailConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../../lib/user-id.ts\";\n\nfunction getUserId(request: Request): string {\n return requireUserIdFromRequest(request);\n}\n\nexport const GET = createOAuthInitHandler(gmailConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});",
|
|
291
|
-
"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",
|
|
292
|
-
"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\";\nimport type { OAuthToken } 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// TokenStore adapter keyed by (serviceId, userId). All API calls must pass\n// the authenticated user's id. Never use a shared development user id\n// in production; that re-introduces VULN-AUTH-2.\nconst tokenStoreAdapter = {\n async getTokens(serviceId: string, userId: string): Promise<OAuthToken | null> {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(\n serviceId: string,\n userId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ): Promise<void> {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string): Promise<void> {\n await tokenStore.revokeToken(userId, serviceId);\n },\n async setState(): Promise<void> {},\n async consumeState(): Promise<null> {\n return null;\n },\n};\n\nconst gmailService = new OAuthService(gmailConfig, tokenStoreAdapter);\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",
|
|
293
|
-
"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: \"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",
|
|
294
|
-
"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: \"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",
|
|
295
|
-
"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: \"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",
|
|
296
|
-
"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: \"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",
|
|
297
|
-
"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: \"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",
|
|
298
|
-
"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: \"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",
|
|
299
|
-
"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: \"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",
|
|
300
|
-
"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: \"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",
|
|
301
|
-
"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: \"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",
|
|
302
|
-
"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: \"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",
|
|
303
|
-
"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: \"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",
|
|
304
|
-
"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: \"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",
|
|
305
|
-
"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: \"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",
|
|
306
|
-
"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: \"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",
|
|
307
|
-
"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: \"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",
|
|
308
|
-
"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: \"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",
|
|
309
|
-
"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: \"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",
|
|
310
|
-
"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: \"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",
|
|
311
|
-
"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: \"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",
|
|
312
|
-
"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: \"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",
|
|
313
|
-
"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: \"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",
|
|
314
|
-
"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: \"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",
|
|
315
|
-
"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: \"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",
|
|
316
|
-
"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: \"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",
|
|
317
|
-
"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: \"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",
|
|
318
|
-
"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: \"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",
|
|
319
|
-
"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: \"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",
|
|
320
|
-
"tools/stop-mailbox-watch.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: \"stop-mailbox-watch\",\n description: \"Stop Gmail push notifications for the connected mailbox.\",\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 await gmail.stopMailboxWatch();\n\n return {\n success: true,\n message: \"Mailbox watch stopped.\",\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",
|
|
321
|
-
"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: \"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",
|
|
322
|
-
"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: \"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",
|
|
323
|
-
"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: \"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",
|
|
324
|
-
"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: \"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",
|
|
325
|
-
"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: \"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",
|
|
326
|
-
"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: \"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",
|
|
327
|
-
"tools/watch-mailbox.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: \"watch-mailbox\",\n description: \"Start Gmail push notifications for mailbox changes using a Cloud Pub/Sub topic.\",\n inputSchema: defineSchema((v) => v.object({\n topicName: v\n .string()\n .min(1)\n .describe(\"Cloud Pub/Sub topic name, for example projects/<PROJECT_ID>/topics/<TOPIC_ID>\"),\n labelIds: v.array(v.string().min(1)).optional().describe(\"Labels used to filter notifications\"),\n labelFilterBehavior: v\n .enum([\"include\", \"exclude\"])\n .optional()\n .describe(\"Whether labelIds are included or excluded\"),\n }))(),\n execute: async (input, context) => {\n const userId = resolveUserId(context);\n\n try {\n const gmail = createGmailClient(userId);\n return await gmail.watchMailbox(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"
|
|
328
|
-
}
|
|
329
|
-
},
|
|
330
|
-
"integration:hubspot": {
|
|
331
|
-
"files": {
|
|
332
|
-
".env.example": "# HubSpot OAuth Configuration\n# Get these from https://app.hubspot.com/developer\n\nHUBSPOT_CLIENT_ID=your_client_id_here\nHUBSPOT_CLIENT_SECRET=your_client_secret_here\n",
|
|
333
|
-
"app/api/auth/hubspot/callback/route.ts": "import { createOAuthCallbackHandler, hubspotConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string, userId: string) {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(\n serviceId: string,\n userId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string) {\n await tokenStore.revokeToken(userId, serviceId);\n },\n setState(\n state: string,\n meta: {\n userId: string;\n serviceId: string;\n codeVerifier?: string;\n redirectUri?: string;\n scopes?: string[];\n createdAt: number;\n },\n ) {\n return oauthMemoryTokenStore.setState(state, meta);\n },\n consumeState(state: string) {\n return oauthMemoryTokenStore.consumeState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(hubspotConfig, { tokenStore: hybridTokenStore });\n",
|
|
334
|
-
"app/api/auth/hubspot/route.ts": "import { createOAuthInitHandler, hubspotConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../../lib/user-id.ts\";\n\nfunction getUserId(request: Request): string {\n return requireUserIdFromRequest(request);\n}\n\nexport const GET = createOAuthInitHandler(hubspotConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});",
|
|
335
|
-
"lib/hubspot-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst HUBSPOT_BASE_URL = \"https://api.hubapi.com\";\n\ninterface HubSpotPagination {\n after?: string;\n next?: {\n after: string;\n link: string;\n };\n}\n\ninterface HubSpotResponse<T> {\n results: T[];\n paging?: HubSpotPagination;\n}\n\ninterface HubSpotContact {\n id: string;\n properties: {\n email?: string;\n firstname?: string;\n lastname?: string;\n phone?: string;\n company?: string;\n website?: string;\n jobtitle?: string;\n createdate?: string;\n lastmodifieddate?: string;\n [key: string]: string | undefined;\n };\n createdAt: string;\n updatedAt: string;\n archived: boolean;\n}\n\ninterface HubSpotCompany {\n id: string;\n properties: {\n name?: string;\n domain?: string;\n city?: string;\n state?: string;\n country?: string;\n industry?: string;\n phone?: string;\n createdate?: string;\n [key: string]: string | undefined;\n };\n createdAt: string;\n updatedAt: string;\n archived: boolean;\n}\n\ninterface HubSpotDeal {\n id: string;\n properties: {\n dealname?: string;\n amount?: string;\n dealstage?: string;\n pipeline?: string;\n closedate?: string;\n createdate?: string;\n [key: string]: string | undefined;\n };\n createdAt: string;\n updatedAt: string;\n archived: boolean;\n}\n\nfunction buildQueryString(options: {\n limit?: number;\n after?: string;\n properties?: string[];\n defaultProperties: string[];\n}): string {\n const params = new URLSearchParams();\n\n if (options.limit) params.set(\"limit\", options.limit.toString());\n if (options.after) params.set(\"after\", options.after);\n\n const properties =\n options.properties?.length ? options.properties : options.defaultProperties;\n\n for (const prop of properties) params.append(\"properties\", prop);\n\n const queryString = params.toString();\n return queryString ? `?${queryString}` : \"\";\n}\n\nasync function hubspotFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with HubSpot. Please connect your account.\");\n }\n\n const response = await fetch(`${HUBSPOT_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 { message?: string };\n throw new Error(\n `HubSpot API error: ${response.status} ${error.message ?? response.statusText}`,\n );\n }\n\n return response.json();\n}\n\n// ============================================================================\n// CONTACTS\n// ============================================================================\n\nexport function listContacts(options?: {\n limit?: number;\n after?: string;\n properties?: string[];\n}): Promise<HubSpotResponse<HubSpotContact>> {\n const query = buildQueryString({\n limit: options?.limit,\n after: options?.after,\n properties: options?.properties,\n defaultProperties: [\"email\", \"firstname\", \"lastname\", \"phone\", \"company\", \"jobtitle\"],\n });\n\n return hubspotFetch<HubSpotResponse<HubSpotContact>>(`/crm/v3/objects/contacts${query}`);\n}\n\nexport function getContact(contactId: string, properties?: string[]): Promise<HubSpotContact> {\n const query = buildQueryString({\n properties,\n defaultProperties: [\"email\", \"firstname\", \"lastname\", \"phone\", \"company\", \"jobtitle\", \"website\"],\n });\n\n return hubspotFetch<HubSpotContact>(`/crm/v3/objects/contacts/${contactId}${query}`);\n}\n\nexport function createContact(properties: {\n email: string;\n firstname?: string;\n lastname?: string;\n phone?: string;\n company?: string;\n website?: string;\n jobtitle?: string;\n [key: string]: string | undefined;\n}): Promise<HubSpotContact> {\n return hubspotFetch<HubSpotContact>(\"/crm/v3/objects/contacts\", {\n method: \"POST\",\n body: JSON.stringify({ properties }),\n });\n}\n\nexport function updateContact(\n contactId: string,\n properties: {\n email?: string;\n firstname?: string;\n lastname?: string;\n phone?: string;\n company?: string;\n website?: string;\n jobtitle?: string;\n [key: string]: string | undefined;\n },\n): Promise<HubSpotContact> {\n return hubspotFetch<HubSpotContact>(`/crm/v3/objects/contacts/${contactId}`, {\n method: \"PATCH\",\n body: JSON.stringify({ properties }),\n });\n}\n\nexport function searchContacts(options: {\n query?: string;\n filterGroups?: Array<{\n filters: Array<{\n propertyName: string;\n operator: string;\n value: string;\n }>;\n }>;\n properties?: string[];\n limit?: number;\n after?: string;\n}): Promise<HubSpotResponse<HubSpotContact>> {\n const body: Record<string, unknown> = {\n properties: options.properties?.length\n ? options.properties\n : [\"email\", \"firstname\", \"lastname\", \"phone\", \"company\", \"jobtitle\"],\n };\n\n if (options.filterGroups) body.filterGroups = options.filterGroups;\n if (options.limit) body.limit = options.limit;\n if (options.after) body.after = options.after;\n\n return hubspotFetch<HubSpotResponse<HubSpotContact>>(\"/crm/v3/objects/contacts/search\", {\n method: \"POST\",\n body: JSON.stringify(body),\n });\n}\n\n// ============================================================================\n// COMPANIES\n// ============================================================================\n\nexport function listCompanies(options?: {\n limit?: number;\n after?: string;\n properties?: string[];\n}): Promise<HubSpotResponse<HubSpotCompany>> {\n const query = buildQueryString({\n limit: options?.limit,\n after: options?.after,\n properties: options?.properties,\n defaultProperties: [\"name\", \"domain\", \"city\", \"state\", \"industry\", \"phone\"],\n });\n\n return hubspotFetch<HubSpotResponse<HubSpotCompany>>(`/crm/v3/objects/companies${query}`);\n}\n\nexport function getCompany(companyId: string, properties?: string[]): Promise<HubSpotCompany> {\n const query = buildQueryString({\n properties,\n defaultProperties: [\"name\", \"domain\", \"city\", \"state\", \"country\", \"industry\", \"phone\"],\n });\n\n return hubspotFetch<HubSpotCompany>(`/crm/v3/objects/companies/${companyId}${query}`);\n}\n\nexport function createCompany(properties: {\n name: string;\n domain?: string;\n city?: string;\n state?: string;\n country?: string;\n industry?: string;\n phone?: string;\n [key: string]: string | undefined;\n}): Promise<HubSpotCompany> {\n return hubspotFetch<HubSpotCompany>(\"/crm/v3/objects/companies\", {\n method: \"POST\",\n body: JSON.stringify({ properties }),\n });\n}\n\n// ============================================================================\n// DEALS\n// ============================================================================\n\nexport function listDeals(options?: {\n limit?: number;\n after?: string;\n properties?: string[];\n}): Promise<HubSpotResponse<HubSpotDeal>> {\n const query = buildQueryString({\n limit: options?.limit,\n after: options?.after,\n properties: options?.properties,\n defaultProperties: [\"dealname\", \"amount\", \"dealstage\", \"pipeline\", \"closedate\"],\n });\n\n return hubspotFetch<HubSpotResponse<HubSpotDeal>>(`/crm/v3/objects/deals${query}`);\n}\n\nexport function getDeal(dealId: string, properties?: string[]): Promise<HubSpotDeal> {\n const query = buildQueryString({\n properties,\n defaultProperties: [\"dealname\", \"amount\", \"dealstage\", \"pipeline\", \"closedate\"],\n });\n\n return hubspotFetch<HubSpotDeal>(`/crm/v3/objects/deals/${dealId}${query}`);\n}\n\nexport function createDeal(properties: {\n dealname: string;\n amount?: string;\n dealstage?: string;\n pipeline?: string;\n closedate?: string;\n [key: string]: string | undefined;\n}): Promise<HubSpotDeal> {\n return hubspotFetch<HubSpotDeal>(\"/crm/v3/objects/deals\", {\n method: \"POST\",\n body: JSON.stringify({ properties }),\n });\n}\n\nexport function updateDeal(\n dealId: string,\n properties: {\n dealname?: string;\n amount?: string;\n dealstage?: string;\n pipeline?: string;\n closedate?: string;\n [key: string]: string | undefined;\n },\n): Promise<HubSpotDeal> {\n return hubspotFetch<HubSpotDeal>(`/crm/v3/objects/deals/${dealId}`, {\n method: \"PATCH\",\n body: JSON.stringify({ properties }),\n });\n}\n\n// ============================================================================\n// HELPER FUNCTIONS\n// ============================================================================\n\nexport function formatContactName(contact: HubSpotContact): string {\n const parts = [contact.properties.firstname, contact.properties.lastname].filter(\n (p): p is string => Boolean(p),\n );\n\n if (parts.length) return parts.join(\" \");\n return contact.properties.email ?? \"Unnamed Contact\";\n}\n\nexport function formatCompanyName(company: HubSpotCompany): string {\n return company.properties.name ?? company.properties.domain ?? \"Unnamed Company\";\n}\n\nexport function formatDealName(deal: HubSpotDeal): string {\n return deal.properties.dealname ?? \"Unnamed Deal\";\n}\n\nexport type { HubSpotCompany, HubSpotContact, HubSpotDeal, HubSpotResponse };\n",
|
|
336
|
-
"tools/create-contact.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createContact, formatContactName } from \"../../lib/hubspot-client.ts\";\n\nexport default tool({\n id: \"create-contact\",\n description: \"Create a new contact in HubSpot CRM. Email is required, other fields are optional.\",\n inputSchema: defineSchema((v) => v.object({\n email: v.string().email().describe(\"Contact email address (required)\"),\n firstname: v.string().optional().describe(\"First name\"),\n lastname: v.string().optional().describe(\"Last name\"),\n phone: v.string().optional().describe(\"Phone number\"),\n company: v.string().optional().describe(\"Company name\"),\n jobtitle: v.string().optional().describe(\"Job title\"),\n website: v.string().optional().describe(\"Website URL\"),\n }))(),\n async execute({ email, firstname, lastname, phone, company, jobtitle, website }) {\n const properties: Record<string, string> = { email };\n\n if (firstname) properties.firstname = firstname;\n if (lastname) properties.lastname = lastname;\n if (phone) properties.phone = phone;\n if (company) properties.company = company;\n if (jobtitle) properties.jobtitle = jobtitle;\n if (website) properties.website = website;\n\n const contact = await createContact(properties);\n const name = formatContactName(contact);\n\n return {\n id: contact.id,\n name,\n email: contact.properties.email,\n phone: contact.properties.phone,\n company: contact.properties.company,\n jobTitle: contact.properties.jobtitle,\n website: contact.properties.website,\n createdAt: contact.createdAt,\n message: `Successfully created contact: ${name}`,\n };\n },\n});\n",
|
|
337
|
-
"tools/create-deal.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createDeal, formatDealName } from \"../../lib/hubspot-client.ts\";\n\nexport default tool({\n id: \"create-deal\",\n description:\n \"Create a new deal in HubSpot CRM. Deal name is required, other fields are optional.\",\n inputSchema: defineSchema((v) => v.object({\n dealname: v.string().describe(\"Deal name (required)\"),\n amount: v.string().optional().describe(\"Deal amount in the account currency\"),\n dealstage: v\n .string()\n .optional()\n .describe(\n 'Current stage of the deal (e.g., \"appointmentscheduled\", \"qualifiedtobuy\", \"closedwon\")',\n ),\n pipeline: v.string().optional().describe(\"Pipeline ID for the deal\"),\n closedate: v\n .string()\n .optional()\n .describe(\"Expected close date in format YYYY-MM-DD or timestamp\"),\n }))(),\n async execute({ dealname, amount, dealstage, pipeline, closedate }) {\n const properties: Record<string, string> = { dealname };\n\n if (amount) properties.amount = amount;\n if (dealstage) properties.dealstage = dealstage;\n if (pipeline) properties.pipeline = pipeline;\n if (closedate) properties.closedate = closedate;\n\n const deal = await createDeal(properties);\n const name = formatDealName(deal);\n\n return {\n id: deal.id,\n name,\n amount: deal.properties.amount,\n stage: deal.properties.dealstage,\n pipeline: deal.properties.pipeline,\n closeDate: deal.properties.closedate,\n createdAt: deal.createdAt,\n message: `Successfully created deal: ${name}`,\n };\n },\n});\n",
|
|
338
|
-
"tools/get-contact.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { formatContactName, getContact } from \"../../lib/hubspot-client.ts\";\n\nexport default tool({\n id: \"get-contact\",\n description:\n \"Get detailed information about a specific contact in HubSpot CRM by their contact ID.\",\n inputSchema: defineSchema((v) => v.object({\n contactId: v.string().describe(\"The HubSpot contact ID\"),\n properties: v\n .array(v.string())\n .optional()\n .describe(\n \"Additional properties to retrieve (e.g., website, city, state, notes)\",\n ),\n }))(),\n async execute({ contactId, properties }) {\n const contact = await getContact(contactId, properties);\n\n const additionalProperties = properties\n ? Object.fromEntries(\n properties\n .filter((prop) => contact.properties[prop] !== undefined)\n .map((prop) => [prop, contact.properties[prop]]),\n )\n : undefined;\n\n return {\n id: contact.id,\n name: formatContactName(contact),\n email: contact.properties.email,\n phone: contact.properties.phone,\n company: contact.properties.company,\n jobTitle: contact.properties.jobtitle,\n website: contact.properties.website,\n createdAt: contact.createdAt,\n updatedAt: contact.updatedAt,\n archived: contact.archived,\n additionalProperties,\n allProperties: contact.properties,\n };\n },\n});\n",
|
|
339
|
-
"tools/list-contacts.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { formatContactName, listContacts } from \"../../lib/hubspot-client.ts\";\n\nexport default tool({\n id: \"list-contacts\",\n description:\n \"List contacts from your HubSpot CRM. Returns contact information including name, email, phone, company, and job title.\",\n inputSchema: defineSchema((v) => v.object({\n limit: v.number().min(1).max(100).default(10).describe(\"Maximum number of contacts to return\"),\n properties: v\n .array(v.string())\n .optional()\n .describe(\"Additional properties to retrieve (e.g., website, city, state)\"),\n }))(),\n async execute({ limit, properties }) {\n const response = await listContacts({ limit, properties });\n\n return {\n contacts: response.results.map((contact) => {\n if (!properties) {\n return {\n id: contact.id,\n name: formatContactName(contact),\n email: contact.properties.email,\n phone: contact.properties.phone,\n company: contact.properties.company,\n jobTitle: contact.properties.jobtitle,\n createdAt: contact.createdAt,\n updatedAt: contact.updatedAt,\n additionalProperties: undefined,\n };\n }\n\n const additionalProperties = Object.fromEntries(\n properties\n .filter((prop) => contact.properties[prop] !== undefined)\n .map((prop) => [prop, contact.properties[prop]]),\n );\n\n return {\n id: contact.id,\n name: formatContactName(contact),\n email: contact.properties.email,\n phone: contact.properties.phone,\n company: contact.properties.company,\n jobTitle: contact.properties.jobtitle,\n createdAt: contact.createdAt,\n updatedAt: contact.updatedAt,\n additionalProperties,\n };\n }),\n hasMore: Boolean(response.paging?.next),\n nextAfter: response.paging?.next?.after,\n };\n },\n});\n",
|
|
340
|
-
"tools/list-deals.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { formatDealName, listDeals } from \"../../lib/hubspot-client.ts\";\n\nexport default tool({\n id: \"list-deals\",\n description:\n \"List sales deals from your HubSpot CRM. Returns deal information including name, amount, stage, and close date.\",\n inputSchema: defineSchema((v) => v.object({\n limit: v.number().min(1).max(100).default(10).describe(\"Maximum number of deals to return\"),\n properties: v.array(v.string()).optional().describe(\"Additional properties to retrieve\"),\n }))(),\n async execute({ limit, properties }) {\n const response = await listDeals({ limit, properties });\n\n return {\n deals: response.results.map((deal) => {\n let additionalProperties: Record<string, unknown> | undefined;\n\n if (properties) {\n additionalProperties = Object.fromEntries(\n properties\n .filter((prop) => deal.properties[prop] !== undefined)\n .map((prop) => [prop, deal.properties[prop]]),\n );\n }\n\n return {\n id: deal.id,\n name: formatDealName(deal),\n amount: deal.properties.amount,\n stage: deal.properties.dealstage,\n pipeline: deal.properties.pipeline,\n closeDate: deal.properties.closedate,\n createdAt: deal.createdAt,\n updatedAt: deal.updatedAt,\n additionalProperties,\n };\n }),\n hasMore: response.paging?.next != null,\n nextAfter: response.paging?.next?.after,\n };\n },\n});\n"
|
|
341
|
-
}
|
|
342
|
-
},
|
|
343
|
-
"integration:jira": {
|
|
344
|
-
"files": {
|
|
345
|
-
"app/api/auth/jira/callback/route.ts": "import { createOAuthCallbackHandler, jiraConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nconst hybridTokenStore = {\n async getTokens(serviceId: string, userId: string): Promise<unknown> {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(\n serviceId: string,\n userId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ): Promise<void> {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string): Promise<void> {\n await tokenStore.revokeToken(userId, serviceId);\n },\n setState(\n state: string,\n meta: {\n userId: string;\n serviceId: string;\n codeVerifier?: string;\n redirectUri?: string;\n scopes?: string[];\n createdAt: number;\n },\n ): Promise<void> {\n return oauthMemoryTokenStore.setState(state, meta);\n },\n consumeState(state: string): Promise<unknown> {\n return oauthMemoryTokenStore.consumeState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(jiraConfig, { tokenStore: hybridTokenStore });\n",
|
|
346
|
-
"app/api/auth/jira/route.ts": "import { createOAuthInitHandler, jiraConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../../lib/user-id.ts\";\n\nfunction getUserId(request: Request): string {\n return requireUserIdFromRequest(request);\n}\n\nexport const GET = createOAuthInitHandler(jiraConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});",
|
|
347
|
-
"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\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(\"Not authenticated with Jira. Please connect your account.\");\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 = `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 =\n (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 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",
|
|
348
|
-
"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: \"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) => 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('Type of issue: \"Task\", \"Bug\", \"Story\", \"Epic\", etc.'),\n description: v.string().optional().describe(\"Detailed description of the issue\"),\n priority: v\n .string()\n .optional()\n .describe('Priority: \"Highest\", \"High\", \"Medium\", \"Low\", \"Lowest\"'),\n assigneeId: v.string().optional().describe(\"Atlassian account ID of the assignee (optional)\"),\n labels: v.array(v.string()).optional().describe(\"Array of labels to add to the issue\"),\n }))(),\n async execute({ projectKey, summary, issueType, description, priority, assigneeId, labels }) {\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",
|
|
349
|
-
"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: \"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) => v.object({\n issueKey: v.string().describe('The issue key (e.g., \"PROJ-123\") or ID'),\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",
|
|
350
|
-
"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: \"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",
|
|
351
|
-
"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: \"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) => 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 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",
|
|
352
|
-
"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: \"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) => v.object({\n issueKey: v.string().describe('The issue key (e.g., \"PROJ-123\") to update'),\n summary: v.string().optional().describe(\"New summary/title for the issue\"),\n description: v.string().optional().describe(\"New description for the issue\"),\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 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 || 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"
|
|
353
|
-
}
|
|
354
|
-
},
|
|
355
|
-
"integration:linear": {
|
|
356
|
-
"files": {
|
|
357
|
-
".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",
|
|
358
|
-
"app/api/auth/linear/callback/route.ts": "/**\n * Linear OAuth Callback\n *\n * Handles the OAuth callback from Linear and stores the tokens.\n */\n\nimport { createOAuthCallbackHandler, linearConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string, userId: string) {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(\n serviceId: string,\n userId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string) {\n await tokenStore.revokeToken(userId, serviceId);\n },\n setState(\n state: string,\n meta: {\n userId: string;\n serviceId: string;\n codeVerifier?: string;\n redirectUri?: string;\n scopes?: string[];\n createdAt: number;\n },\n ) {\n return oauthMemoryTokenStore.setState(state, meta);\n },\n consumeState(state: string) {\n return oauthMemoryTokenStore.consumeState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(linearConfig, { tokenStore: hybridTokenStore });\n",
|
|
359
|
-
"app/api/auth/linear/route.ts": "import { createOAuthInitHandler, linearConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../../lib/user-id.ts\";\n\nfunction getUserId(request: Request): string {\n return requireUserIdFromRequest(request);\n}\n\nexport const GET = createOAuthInitHandler(linearConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});",
|
|
360
|
-
"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\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",
|
|
361
|
-
"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: \"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",
|
|
362
|
-
"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: \"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",
|
|
363
|
-
"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: \"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",
|
|
364
|
-
"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: \"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",
|
|
365
|
-
"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: \"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"
|
|
366
|
-
}
|
|
367
|
-
},
|
|
368
|
-
"integration:mixpanel": {
|
|
369
|
-
"files": {
|
|
370
|
-
".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",
|
|
371
|
-
"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",
|
|
372
|
-
"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: \"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",
|
|
373
|
-
"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: \"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",
|
|
374
|
-
"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: \"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",
|
|
375
|
-
"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: \"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",
|
|
376
|
-
"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: \"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"
|
|
377
|
-
}
|
|
378
|
-
},
|
|
379
|
-
"integration:neon": {
|
|
380
|
-
"files": {
|
|
381
|
-
".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",
|
|
382
|
-
"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",
|
|
383
|
-
"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",
|
|
384
|
-
"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: \"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",
|
|
385
|
-
"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: \"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",
|
|
386
|
-
"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: \"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",
|
|
387
|
-
"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: \"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",
|
|
388
|
-
"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: \"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"
|
|
389
|
-
}
|
|
390
|
-
},
|
|
391
|
-
"integration:notion": {
|
|
392
|
-
"files": {
|
|
393
|
-
".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",
|
|
394
|
-
"app/api/auth/notion/callback/route.ts": "import { createOAuthCallbackHandler, notionConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string, userId: string) {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(\n serviceId: string,\n userId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string) {\n await tokenStore.revokeToken(userId, serviceId);\n },\n setState(\n state: string,\n meta: {\n userId: string;\n serviceId: string;\n codeVerifier?: string;\n redirectUri?: string;\n scopes?: string[];\n createdAt: number;\n },\n ) {\n return oauthMemoryTokenStore.setState(state, meta);\n },\n consumeState(state: string) {\n return oauthMemoryTokenStore.consumeState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(notionConfig, { tokenStore: hybridTokenStore });\n",
|
|
395
|
-
"app/api/auth/notion/route.ts": "import { createOAuthInitHandler, notionConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../../lib/user-id.ts\";\n\nfunction getUserId(request: Request): string {\n return requireUserIdFromRequest(request);\n}\n\nexport const GET = createOAuthInitHandler(notionConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});",
|
|
396
|
-
"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",
|
|
397
|
-
"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: \"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",
|
|
398
|
-
"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: \"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",
|
|
399
|
-
"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: \"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",
|
|
400
|
-
"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: \"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"
|
|
401
|
-
}
|
|
402
|
-
},
|
|
403
|
-
"integration:onedrive": {
|
|
404
|
-
"files": {
|
|
405
|
-
".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",
|
|
406
|
-
"app/api/auth/onedrive/callback/route.ts": "/**\n * OneDrive OAuth Callback\n *\n * Handles the OAuth callback from Microsoft and stores the tokens.\n */\n\nimport { createOAuthCallbackHandler, oneDriveConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nconst hybridTokenStore = {\n async getTokens(serviceId: string, userId: string): Promise<unknown> {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(\n serviceId: string,\n userId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ): Promise<void> {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string): Promise<void> {\n await tokenStore.revokeToken(userId, serviceId);\n },\n setState(\n state: string,\n meta: {\n userId: string;\n serviceId: string;\n codeVerifier?: string;\n redirectUri?: string;\n scopes?: string[];\n createdAt: number;\n },\n ): Promise<void> {\n return oauthMemoryTokenStore.setState(state, meta);\n },\n consumeState(state: string): Promise<unknown> {\n return oauthMemoryTokenStore.consumeState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(oneDriveConfig, { tokenStore: hybridTokenStore });\n",
|
|
407
|
-
"app/api/auth/onedrive/route.ts": "import { createOAuthInitHandler, oneDriveConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../../lib/user-id.ts\";\n\nfunction getUserId(request: Request): string {\n return requireUserIdFromRequest(request);\n}\n\nexport const GET = createOAuthInitHandler(oneDriveConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});",
|
|
408
|
-
"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",
|
|
409
|
-
"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: \"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",
|
|
410
|
-
"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: \"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",
|
|
411
|
-
"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: \"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",
|
|
412
|
-
"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: \"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"
|
|
413
|
-
}
|
|
414
|
-
},
|
|
415
|
-
"integration:outlook": {
|
|
416
|
-
"files": {
|
|
417
|
-
".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",
|
|
418
|
-
"app/api/auth/outlook/callback/route.ts": "import { createOAuthCallbackHandler, outlookConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string, userId: string) {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(\n serviceId: string,\n userId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string) {\n await tokenStore.revokeToken(userId, serviceId);\n },\n setState(\n state: string,\n meta: {\n userId: string;\n serviceId: string;\n codeVerifier?: string;\n redirectUri?: string;\n scopes?: string[];\n createdAt: number;\n },\n ) {\n return oauthMemoryTokenStore.setState(state, meta);\n },\n consumeState(state: string) {\n return oauthMemoryTokenStore.consumeState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(outlookConfig, { tokenStore: hybridTokenStore });\n",
|
|
419
|
-
"app/api/auth/outlook/route.ts": "import { createOAuthInitHandler, outlookConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../../lib/user-id.ts\";\n\nfunction getUserId(request: Request): string {\n return requireUserIdFromRequest(request);\n}\n\nexport const GET = createOAuthInitHandler(outlookConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});",
|
|
420
|
-
"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\nexport interface OutlookMessage {\n id: string;\n subject: string;\n bodyPreview: string;\n body: {\n contentType: \"text\" | \"html\";\n content: string;\n };\n from: {\n emailAddress: {\n name: string;\n address: string;\n };\n };\n toRecipients: Array<{\n emailAddress: {\n name: string;\n address: string;\n };\n }>;\n ccRecipients?: Array<{\n emailAddress: {\n name: string;\n address: string;\n };\n }>;\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\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 = {\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 importance: options.importance ?? \"normal\",\n };\n\n await graphFetch(\"/sendMail\", {\n method: \"POST\",\n body: JSON.stringify({ message }),\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\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 from = message.from.emailAddress.name || message.from.emailAddress.address;\n const to = message.toRecipients.map((r) => r.emailAddress.address).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",
|
|
421
|
-
"tools/get-email.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { getEmail } from \"../../lib/outlook-client.ts\";\n\nexport default tool({\n id: \"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: {\n name: message.from.emailAddress.name,\n email: message.from.emailAddress.address,\n },\n to: message.toRecipients.map(({ emailAddress }) => ({\n name: emailAddress.name,\n email: emailAddress.address,\n })),\n cc: message.ccRecipients?.map(({ emailAddress }) => ({\n name: emailAddress.name,\n email: emailAddress.address,\n })),\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",
|
|
422
|
-
"tools/list-emails.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { listEmails } from \"../../lib/outlook-client.ts\";\n\nexport default tool({\n id: \"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: {\n name: msg.from.emailAddress.name,\n email: msg.from.emailAddress.address,\n },\n to: msg.toRecipients.map((r) => ({\n name: r.emailAddress.name,\n email: r.emailAddress.address,\n })),\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",
|
|
423
|
-
"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: \"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",
|
|
424
|
-
"tools/search-emails.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { searchEmails } from \"../../lib/outlook-client.ts\";\n\nexport default tool({\n id: \"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: {\n name: msg.from.emailAddress.name,\n email: msg.from.emailAddress.address,\n },\n to: msg.toRecipients.map((r) => ({\n name: r.emailAddress.name,\n email: r.emailAddress.address,\n })),\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",
|
|
425
|
-
"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: \"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"
|
|
426
|
-
}
|
|
427
|
-
},
|
|
428
|
-
"integration:posthog": {
|
|
429
|
-
"files": {
|
|
430
|
-
".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",
|
|
431
|
-
"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",
|
|
432
|
-
"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: \"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",
|
|
433
|
-
"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: \"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",
|
|
434
|
-
"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: \"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",
|
|
435
|
-
"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: \"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"
|
|
436
|
-
}
|
|
437
|
-
},
|
|
438
|
-
"integration:salesforce": {
|
|
439
|
-
"files": {
|
|
440
|
-
"app/api/auth/salesforce/callback/route.ts": "import { createOAuthCallbackHandler, salesforceConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string, userId: string) {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(\n serviceId: string,\n userId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string) {\n await tokenStore.revokeToken(userId, serviceId);\n },\n setState(\n state: string,\n meta: {\n userId: string;\n serviceId: string;\n codeVerifier?: string;\n redirectUri?: string;\n scopes?: string[];\n createdAt: number;\n },\n ) {\n return oauthMemoryTokenStore.setState(state, meta);\n },\n consumeState(state: string) {\n return oauthMemoryTokenStore.consumeState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(salesforceConfig, { tokenStore: hybridTokenStore });\n",
|
|
441
|
-
"app/api/auth/salesforce/route.ts": "import { createOAuthInitHandler, salesforceConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../../lib/user-id.ts\";\n\nfunction getUserId(request: Request): string {\n return requireUserIdFromRequest(request);\n}\n\nexport const GET = createOAuthInitHandler(salesforceConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});",
|
|
442
|
-
"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",
|
|
443
|
-
"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: \"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",
|
|
444
|
-
"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: \"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",
|
|
445
|
-
"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: \"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",
|
|
446
|
-
"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: \"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",
|
|
447
|
-
"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: \"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"
|
|
448
|
-
}
|
|
449
|
-
},
|
|
450
|
-
"integration:sentry": {
|
|
451
|
-
"files": {
|
|
452
|
-
".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",
|
|
453
|
-
"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",
|
|
454
|
-
"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: \"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",
|
|
455
|
-
"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: \"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",
|
|
456
|
-
"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: \"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",
|
|
457
|
-
"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: \"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"
|
|
458
|
-
}
|
|
459
|
-
},
|
|
460
|
-
"integration:servicenow": {
|
|
461
|
-
"files": {
|
|
462
|
-
".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",
|
|
463
|
-
"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",
|
|
464
|
-
"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",
|
|
465
|
-
"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",
|
|
466
|
-
"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",
|
|
467
|
-
"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",
|
|
468
|
-
"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",
|
|
469
|
-
"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",
|
|
470
|
-
"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"
|
|
471
|
-
}
|
|
472
|
-
},
|
|
473
|
-
"integration:sharepoint": {
|
|
474
|
-
"files": {
|
|
475
|
-
"app/api/auth/sharepoint/callback/route.ts": "import { createOAuthCallbackHandler, sharePointConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string, userId: string) {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(\n serviceId: string,\n userId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string) {\n await tokenStore.revokeToken(userId, serviceId);\n },\n setState(\n state: string,\n meta: {\n userId: string;\n serviceId: string;\n codeVerifier?: string;\n redirectUri?: string;\n scopes?: string[];\n createdAt: number;\n },\n ) {\n return oauthMemoryTokenStore.setState(state, meta);\n },\n consumeState(state: string) {\n return oauthMemoryTokenStore.consumeState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(sharePointConfig, { tokenStore: hybridTokenStore });\n",
|
|
476
|
-
"app/api/auth/sharepoint/route.ts": "import { createOAuthInitHandler, sharePointConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../../lib/user-id.ts\";\n\nfunction getUserId(request: Request): string {\n return requireUserIdFromRequest(request);\n}\n\nexport const GET = createOAuthInitHandler(sharePointConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});",
|
|
477
|
-
"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",
|
|
478
|
-
"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: \"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 contentMaxLength: v\n .number()\n .min(100)\n .max(100000)\n .default(50000)\n .describe(\"Maximum length of content to return if includeContent is true\"),\n }))(),\n async execute({ siteId, driveId, itemId, includeContent, contentMaxLength }) {\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 if (file.size >= contentMaxLength) {\n result.contentError = `File size (${formatBytes(file.size)}) exceeds maximum content length`;\n return result;\n }\n\n try {\n const content = await downloadFileAsText(siteId, driveId, itemId);\n const truncated = content.length > contentMaxLength;\n\n result.content = truncated\n ? `${content.substring(0, contentMaxLength)}\\n\\n[Content truncated...]`\n : content;\n result.contentTruncated = truncated;\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",
|
|
479
|
-
"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: \"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",
|
|
480
|
-
"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: \"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",
|
|
481
|
-
"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: \"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",
|
|
482
|
-
"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: \"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"
|
|
483
|
-
}
|
|
484
|
-
},
|
|
485
|
-
"integration:sheets": {
|
|
486
|
-
"files": {
|
|
487
|
-
".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",
|
|
488
|
-
"app/api/auth/sheets/callback/route.ts": "import { createOAuthCallbackHandler, sheetsConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string, userId: string) {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(\n serviceId: string,\n userId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string) {\n await tokenStore.revokeToken(userId, serviceId);\n },\n setState(\n state: string,\n meta: {\n userId: string;\n serviceId: string;\n codeVerifier?: string;\n redirectUri?: string;\n scopes?: string[];\n createdAt: number;\n },\n ) {\n return oauthMemoryTokenStore.setState(state, meta);\n },\n consumeState(state: string) {\n return oauthMemoryTokenStore.consumeState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(sheetsConfig, { tokenStore: hybridTokenStore });\n",
|
|
489
|
-
"app/api/auth/sheets/route.ts": "import { createOAuthInitHandler, sheetsConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../../lib/user-id.ts\";\n\nfunction getUserId(request: Request): string {\n return requireUserIdFromRequest(request);\n}\n\nexport const GET = createOAuthInitHandler(sheetsConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});",
|
|
490
|
-
"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 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 ],\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(\n spreadsheetId: string,\n range: string,\n values: unknown[][],\n valueInputOption?: \"RAW\" | \"USER_ENTERED\",\n ): Promise<{\n updates: {\n updatedRange: string;\n updatedRows: number;\n updatedColumns: number;\n updatedCells: number;\n };\n }>;\n clearRange(spreadsheetId: string, range: string): Promise<{ clearedRange: string }>;\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} {\n async function getAccessToken(): Promise<string> {\n const token = await getValidToken(sheetsOAuthProvider, userId, \"sheets\");\n if (token) return token;\n throw new Error(\"Google Sheets not connected. Please connect your Google account first.\");\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(`${serviceName} API error: ${response.status} - ${error}`);\n }\n\n return response.json();\n }\n\n function sheetsApiRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n return apiRequest<T>(SHEETS_API_BASE, \"Sheets\", 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 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[] }>(`/files?${params.toString()}`);\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<{ values?: unknown[][]; range: string }>(\n `/spreadsheets/${spreadsheetId}/values/${encodeURIComponent(range)}`,\n );\n\n return { values: result.values ?? [], range: result.range };\n },\n\n async readRanges(spreadsheetId: string, ranges: string[]): 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) => ({ values: vr.values ?? [], range: vr.range }));\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/${encodeURIComponent(options.range)}?valueInputOption=${valueInputOption}`,\n {\n method: \"PUT\",\n body: JSON.stringify({ values: options.values }),\n },\n );\n },\n\n appendRange(\n spreadsheetId: string,\n range: string,\n values: unknown[][],\n valueInputOption: \"RAW\" | \"USER_ENTERED\" = \"USER_ENTERED\",\n ): Promise<{\n updates: {\n updatedRange: string;\n updatedRows: number;\n updatedColumns: number;\n updatedCells: number;\n };\n }> {\n return sheetsApiRequest(\n `/spreadsheets/${spreadsheetId}/values/${encodeURIComponent(range)}:append?valueInputOption=${valueInputOption}`,\n {\n method: \"POST\",\n body: JSON.stringify({ values }),\n },\n );\n },\n\n clearRange(spreadsheetId: string, range: string): Promise<{ clearedRange: string }> {\n return sheetsApiRequest(`/spreadsheets/${spreadsheetId}/values/${encodeURIComponent(range)}:clear`, {\n method: \"POST\",\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}\n\nexport type SheetsClient = ReturnType<typeof createSheetsClient>;\n",
|
|
491
|
-
"tools/create-spreadsheet.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createSheetsClient } from \"../../lib/sheets-client.ts\";\n\nconst DEFAULT_USER_ID = \"demo-user\";\n\nexport default tool({\n id: \"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) => 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 async execute({ title, sheets, initialData }) {\n const client = createSheetsClient(DEFAULT_USER_ID);\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",
|
|
492
|
-
"tools/get-spreadsheet.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createSheetsClient } from \"../../lib/sheets-client.ts\";\n\nconst DEFAULT_USER_ID = \"demo-user\";\n\nexport default tool({\n id: \"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) => v.object({\n spreadsheetId: v\n .string()\n .describe(\"The ID of the spreadsheet (from URL or list-spreadsheets)\"),\n }))(),\n async execute({ spreadsheetId }) {\n const client = createSheetsClient(DEFAULT_USER_ID);\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",
|
|
493
|
-
"tools/list-spreadsheets.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createSheetsClient } from \"../../lib/sheets-client.ts\";\n\nconst DEFAULT_USER_ID = \"demo-user\";\n\nexport default tool({\n id: \"list-spreadsheets\",\n description:\n \"List recent Google Sheets spreadsheets from Google Drive. Returns spreadsheet names, IDs, and metadata.\",\n inputSchema: defineSchema((v) => 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\"])\n .default(\"modifiedTime\")\n .describe(\"Sort order for results\"),\n }))(),\n async execute({ maxResults, orderBy }) {\n const client = createSheetsClient(DEFAULT_USER_ID);\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",
|
|
494
|
-
"tools/read-range.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createSheetsClient } from \"../../lib/sheets-client.ts\";\n\nconst DEFAULT_USER_ID = \"demo-user\";\n\nexport default tool({\n id: \"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) => 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 async execute({ spreadsheetId, range }) {\n const client = createSheetsClient(DEFAULT_USER_ID);\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",
|
|
495
|
-
"tools/write-range.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createSheetsClient } from \"../../lib/sheets-client.ts\";\n\nconst DEFAULT_USER_ID = \"demo-user\";\n\nexport default tool({\n id: \"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) => 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\"])\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 async execute({ spreadsheetId, range, values, valueInputOption }) {\n const client = createSheetsClient(DEFAULT_USER_ID);\n return client.writeRange({ spreadsheetId, range, values, valueInputOption });\n },\n});\n"
|
|
496
|
-
}
|
|
497
|
-
},
|
|
498
|
-
"integration:shopify": {
|
|
499
|
-
"files": {
|
|
500
|
-
".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",
|
|
501
|
-
"app/api/auth/shopify/callback/route.ts": "import { createOAuthCallbackHandler, shopifyConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string, userId: string) {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(\n serviceId: string,\n userId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string) {\n await tokenStore.revokeToken(userId, serviceId);\n },\n setState(\n state: string,\n meta: {\n userId: string;\n serviceId: string;\n codeVerifier?: string;\n redirectUri?: string;\n scopes?: string[];\n createdAt: number;\n },\n ) {\n return oauthMemoryTokenStore.setState(state, meta);\n },\n consumeState(state: string) {\n return oauthMemoryTokenStore.consumeState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(shopifyConfig, { tokenStore: hybridTokenStore });\n",
|
|
502
|
-
"app/api/auth/shopify/route.ts": "import { createOAuthInitHandler, shopifyConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../../lib/user-id.ts\";\n\nfunction getUserId(request: Request): string {\n return requireUserIdFromRequest(request);\n}\n\nexport const GET = createOAuthInitHandler(shopifyConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});",
|
|
503
|
-
"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",
|
|
504
|
-
"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: \"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",
|
|
505
|
-
"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: \"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",
|
|
506
|
-
"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: \"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",
|
|
507
|
-
"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: \"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",
|
|
508
|
-
"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: \"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"
|
|
509
|
-
}
|
|
510
|
-
},
|
|
511
|
-
"integration:slack": {
|
|
512
|
-
"files": {
|
|
513
|
-
"app/api/auth/slack/callback/route.ts": "import { createOAuthCallbackHandler, slackConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string, userId: string) {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(\n serviceId: string,\n userId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string) {\n await tokenStore.revokeToken(userId, serviceId);\n },\n setState(\n state: string,\n meta: {\n userId: string;\n serviceId: string;\n codeVerifier?: string;\n redirectUri?: string;\n scopes?: string[];\n createdAt: number;\n },\n ) {\n return oauthMemoryTokenStore.setState(state, meta);\n },\n consumeState(state: string) {\n return oauthMemoryTokenStore.consumeState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(slackConfig, { tokenStore: hybridTokenStore });\n",
|
|
514
|
-
"app/api/auth/slack/route.ts": "import { createOAuthInitHandler, slackConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../../lib/user-id.ts\";\n\nfunction getUserId(request: Request): string {\n return requireUserIdFromRequest(request);\n}\n\nexport const GET = createOAuthInitHandler(slackConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});",
|
|
515
|
-
"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 \"users:read\",\n \"im:history\",\n \"im: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",
|
|
516
|
-
"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: \"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",
|
|
517
|
-
"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: \"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",
|
|
518
|
-
"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: \"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"
|
|
519
|
-
}
|
|
520
|
-
},
|
|
521
|
-
"integration:snowflake": {
|
|
522
|
-
"files": {
|
|
523
|
-
".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",
|
|
524
|
-
"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",
|
|
525
|
-
"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: \"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",
|
|
526
|
-
"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: \"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",
|
|
527
|
-
"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: \"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",
|
|
528
|
-
"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: \"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",
|
|
529
|
-
"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: \"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: \"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"
|
|
530
|
-
}
|
|
531
|
-
},
|
|
532
|
-
"integration:stripe": {
|
|
533
|
-
"files": {
|
|
534
|
-
".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",
|
|
535
|
-
"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",
|
|
536
|
-
"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",
|
|
537
|
-
"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: \"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",
|
|
538
|
-
"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: \"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",
|
|
539
|
-
"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: \"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",
|
|
540
|
-
"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: \"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",
|
|
541
|
-
"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: \"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"
|
|
542
|
-
}
|
|
543
|
-
},
|
|
544
|
-
"integration:supabase": {
|
|
545
|
-
"files": {
|
|
546
|
-
".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",
|
|
547
|
-
"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",
|
|
548
|
-
"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",
|
|
549
|
-
"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: \"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",
|
|
550
|
-
"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: \"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",
|
|
551
|
-
"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: \"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",
|
|
552
|
-
"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: \"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",
|
|
553
|
-
"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: \"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"
|
|
554
|
-
}
|
|
555
|
-
},
|
|
556
|
-
"integration:teams": {
|
|
557
|
-
"files": {
|
|
558
|
-
"app/api/auth/teams/callback/route.ts": "/**\n * Teams OAuth Callback\n *\n * Handles the OAuth callback from Microsoft and stores the tokens.\n */\n\nimport { createOAuthCallbackHandler, teamsConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string, userId: string) {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(\n serviceId: string,\n userId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string) {\n await tokenStore.revokeToken(userId, serviceId);\n },\n setState(\n state: string,\n meta: {\n userId: string;\n serviceId: string;\n codeVerifier?: string;\n redirectUri?: string;\n scopes?: string[];\n createdAt: number;\n },\n ) {\n return oauthMemoryTokenStore.setState(state, meta);\n },\n consumeState(state: string) {\n return oauthMemoryTokenStore.consumeState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(teamsConfig, { tokenStore: hybridTokenStore });\n",
|
|
559
|
-
"app/api/auth/teams/route.ts": "import { createOAuthInitHandler, teamsConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../../lib/user-id.ts\";\n\nfunction getUserId(request: Request): string {\n return requireUserIdFromRequest(request);\n}\n\nexport const GET = createOAuthInitHandler(teamsConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});",
|
|
560
|
-
"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(/ /g, \" \")\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/"/g, '\"')\n .trim();\n}\n",
|
|
561
|
-
"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: \"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",
|
|
562
|
-
"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: \"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",
|
|
563
|
-
"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: \"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",
|
|
564
|
-
"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: \"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",
|
|
565
|
-
"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: \"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"
|
|
566
|
-
}
|
|
567
|
-
},
|
|
568
|
-
"integration:trello": {
|
|
569
|
-
"files": {
|
|
570
|
-
".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",
|
|
571
|
-
"app/api/auth/trello/callback/route.ts": "import { createOAuthCallbackHandler, trelloConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string, userId: string) {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(\n serviceId: string,\n userId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string) {\n await tokenStore.revokeToken(userId, serviceId);\n },\n setState(\n state: string,\n meta: {\n userId: string;\n serviceId: string;\n codeVerifier?: string;\n redirectUri?: string;\n scopes?: string[];\n createdAt: number;\n },\n ) {\n return oauthMemoryTokenStore.setState(state, meta);\n },\n consumeState(state: string) {\n return oauthMemoryTokenStore.consumeState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(trelloConfig, { tokenStore: hybridTokenStore });\n",
|
|
572
|
-
"app/api/auth/trello/route.ts": "import { createOAuthInitHandler, trelloConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\nimport { requireUserIdFromRequest } from \"../../../../../lib/user-id.ts\";\n\nfunction getUserId(request: Request): string {\n return requireUserIdFromRequest(request);\n}\n\nexport const GET = createOAuthInitHandler(trelloConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});",
|
|
573
|
-
"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",
|
|
574
|
-
"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: \"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",
|
|
575
|
-
"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: \"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",
|
|
576
|
-
"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: \"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",
|
|
577
|
-
"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: \"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",
|
|
578
|
-
"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: \"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"
|
|
579
|
-
}
|
|
580
|
-
},
|
|
581
|
-
"integration:twilio": {
|
|
582
|
-
"files": {
|
|
583
|
-
".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",
|
|
584
|
-
"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",
|
|
585
|
-
"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: \"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",
|
|
586
|
-
"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: \"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",
|
|
587
|
-
"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: \"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",
|
|
588
|
-
"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: \"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",
|
|
589
|
-
"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: \"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"
|
|
590
|
-
}
|
|
591
|
-
},
|
|
592
|
-
"ai-rules:agents.md": {
|
|
593
|
-
"files": {
|
|
594
|
-
"agents.md": "# Veryfront Agent Instructions\n\nThis is a Veryfront project. Veryfront is a zero-config React meta-framework for AI-native applications.\n\n## Commands\n\n- `npx veryfront dev` - Start development server\n- `npx veryfront build` - Build for production\n- `npx veryfront deploy` - Deploy to cloud\n- `npx veryfront generate page <name>` - Generate a page\n- `npx veryfront generate api <name>` - Generate an API route\n\n## File Conventions\n\n- **Pages**: `src/pages/*.tsx` (file-based routing)\n- **APIs**: `src/api/*.ts` (serverless functions at `/api/*`)\n- **Components**: `src/components/*.tsx`\n- **AI Agents**: `agents/*.ts`\n- **MCP Tools**: `tools/*.ts`\n\n## Examples\n\n### Create a new page\n\n```tsx\n// src/pages/about.tsx\nexport default function About() {\n return <h1>About</h1>;\n}\n```\n\nThis page will be accessible at `/about`.\n\n### Create an API endpoint\n\n```ts\n// src/api/hello.ts\nexport function GET() {\n return Response.json({ message: \"Hello\" });\n}\n\nexport function POST(request: Request) {\n return Response.json({ success: true });\n}\n```\n\nThis API will be accessible at `/api/hello`.\n\n### Create a dynamic API route\n\n```ts\n// src/api/users/[id].ts\nexport function GET(_req: Request, { params }: { params: { id: string } }) {\n return Response.json({ userId: params.id });\n}\n```\n\nThis API will be accessible at `/api/users/:id`.\n\n## Technology Stack\n\n- **Runtime**: Deno\n- **UI Framework**: React 19\n- **Styling**: Tailwind CSS\n- **Language**: TypeScript\n\n## Best Practices\n\n- Co-locate tests with implementation files (`*.test.ts`)\n- Use `#veryfront/*` import aliases\n- Use React 19 features (use hook, Server Components)\n- Return `Response.json()` from API routes\n"
|
|
595
|
-
}
|
|
596
|
-
},
|
|
597
|
-
"ai-rules:claude-code.md": {
|
|
598
|
-
"files": {
|
|
599
|
-
"claude-code.md": "# Veryfront Project\n\nZero-config React meta-framework for AI-native applications.\n\n## Quick Reference\n\n| Command | Purpose |\n| -------------------------------- | ---------------------------------------- |\n| `veryfront dev` | Start dev server (http://localhost:3000) |\n| `veryfront build` | Production build |\n| `veryfront deploy` | Deploy to Veryfront cloud |\n| `veryfront generate page <name>` | Generate new page |\n| `veryfront generate api <name>` | Generate API route |\n\n## Project Structure\n\n- `src/pages/*.tsx` → Routes (file-based routing)\n- `src/api/*.ts` → API endpoints (`/api/*`)\n- `src/components/` → Shared components\n- `agents/` → AI agents\n- `tools/` → MCP tools\n\n## When Asked to Add Features\n\n1. **Pages**: Create in `src/pages/` (e.g., `about.tsx` → `/about`)\n2. **APIs**: Create in `src/api/` (e.g., `users.ts` → `/api/users`)\n3. **Components**: Create in `src/components/`\n4. **AI Agents**: Create in `agents/`\n\n## Code Patterns\n\n```tsx\n// Page (src/pages/about.tsx)\nexport default function About() {\n return <h1>About</h1>;\n}\n\n// API (src/api/hello.ts)\nexport function GET() {\n return Response.json({ message: \"Hello\" });\n}\n\n// Dynamic API (src/api/users/[id].ts)\nexport function GET(_req: Request, { params }: { params: { id: string } }) {\n return Response.json({ id: params.id });\n}\n```\n\n## Conventions\n\n- Use TypeScript\n- Use React 19 features\n- Use Tailwind for styling\n- Co-locate tests (`*.test.ts`)\n- Use `#veryfront/*` imports\n\n## Testing\n\n- Run `veryfront dev` and check browser\n- API endpoints at `/api/*`\n- Run `deno task test` for tests\n"
|
|
600
|
-
}
|
|
601
|
-
},
|
|
602
|
-
"ai-rules:copilot.md": {
|
|
603
|
-
"files": {
|
|
604
|
-
"copilot.md": "# Veryfront Project Instructions\n\nThis is a Veryfront project - a zero-config React meta-framework for AI-native applications.\n\n## CLI Commands\n\n- `veryfront dev` - Start development server with hot module replacement\n- `veryfront build` - Build for production deployment\n- `veryfront deploy` - Deploy to Veryfront cloud\n- `veryfront generate page <name>` - Scaffold a new page\n- `veryfront generate api <name>` - Scaffold an API route\n\n## File Structure\n\nThe project uses file-based routing:\n\n- `src/pages/*.tsx` - React pages (automatically become routes)\n- `src/api/*.ts` - API endpoints (accessible at `/api/*`)\n- `src/components/` - Shared React components\n- `agents/` - AI agent definitions\n- `tools/` - MCP tool implementations\n\n## Code Patterns\n\n### Creating a Page\n\n```tsx\n// src/pages/about.tsx → accessible at /about\nexport default function About() {\n return <h1>About</h1>;\n}\n```\n\n### Creating an API Endpoint\n\n```ts\n// src/api/hello.ts → accessible at /api/hello\nexport function GET() {\n return Response.json({ message: \"Hello\" });\n}\n\nexport function POST(request: Request) {\n // Handle POST request\n return Response.json({ success: true });\n}\n```\n\n### Dynamic Routes\n\n```ts\n// src/api/users/[id].ts → accessible at /api/users/:id\nexport function GET(_req: Request, { params }: { params: { id: string } }) {\n return Response.json({ userId: params.id });\n}\n```\n\n## Technology Stack\n\n- Runtime: Deno\n- UI: React 19 with Server Components\n- Styling: Tailwind CSS\n- Language: TypeScript (required)\n\n## Best Practices\n\n- Co-locate test files with implementation (`*.test.ts`)\n- Use `#veryfront/*` import aliases for framework modules\n- Follow React 19 patterns (use hook, Server Components)\n- Use Response.json() for API responses\n"
|
|
605
|
-
}
|
|
606
|
-
},
|
|
607
|
-
"ai-rules:cursor.md": {
|
|
608
|
-
"files": {
|
|
609
|
-
"cursor.md": "# Veryfront Project\n\nYou are in a Veryfront project - zero-config React meta-framework for AI-native applications.\n\n## Commands\n\n- `veryfront dev` - Start development server with HMR\n- `veryfront build` - Build for production\n- `veryfront deploy` - Deploy to Veryfront cloud\n- `veryfront generate page <name>` - Generate a new page\n- `veryfront generate api <name>` - Generate an API route\n\n## Project Structure\n\n```\nsrc/\n├── pages/ # File-based routing (pages/*.tsx → routes)\n├── api/ # API routes (api/*.ts → /api/*)\n├── components/ # Shared React components\n├── ai/\n│ ├── agents/ # AI agents\n│ └── tools/ # MCP tools\n└── styles/ # Global styles\n```\n\n## Adding Features\n\n- **New page**: Create `src/pages/about.tsx` → accessible at `/about`\n- **API endpoint**: Create `src/api/users.ts` → accessible at `/api/users`\n- **AI agent**: Create `agents/assistant.ts`\n- **MCP tool**: Create `tools/search.ts`\n\n## Conventions\n\n- TypeScript required\n- React 19 features (use, Server Components)\n- Tailwind CSS for styling\n- Co-locate tests with implementation (`*.test.ts`)\n- Use `#veryfront/*` imports for framework modules\n\n## File Patterns\n\n```tsx\n// Page component (src/pages/about.tsx)\nexport default function About() {\n return <h1>About</h1>;\n}\n\n// API route (src/api/hello.ts)\nexport function GET() {\n return Response.json({ message: \"Hello\" });\n}\n\n// API with params (src/api/users/[id].ts)\nexport function GET(_req: Request, { params }: { params: { id: string } }) {\n return Response.json({ id: params.id });\n}\n```\n\n## Testing\n\n- Run `veryfront dev` and check browser at http://localhost:3000\n- API endpoints available at `/api/*`\n- Use `deno task test` to run tests\n"
|
|
610
|
-
}
|
|
611
|
-
},
|
|
612
|
-
"ai-rules:skill.md": {
|
|
613
|
-
"files": {
|
|
614
|
-
"skill.md": "---\nname: veryfront\ndescription: Build and deploy fullstack AI-native React apps with Veryfront CLI\nlicense: MIT\ncompatibility: Claude Code, Cursor, VS Code, Codex, Gemini CLI\nmetadata:\n author: veryfront\n version: \"1.0\"\n---\n\n# Veryfront\n\nZero-config React meta-framework for AI-native applications.\n\n## Commands\n\n| Command | Purpose |\n| -------------------------------- | --------------------------------- |\n| `veryfront dev` | Start development server with HMR |\n| `veryfront build` | Build for production |\n| `veryfront deploy` | Deploy to Veryfront cloud |\n| `veryfront generate page <name>` | Generate a new page |\n| `veryfront generate api <name>` | Generate an API route |\n\n## Project Structure\n\n```\nsrc/\n├── pages/ # File-based routing (*.tsx → routes)\n├── api/ # API routes (*.ts → /api/*)\n├── components/ # Shared React components\n├── ai/\n│ ├── agents/ # AI agents\n│ └── tools/ # MCP tools\n└── styles/ # Global styles\n```\n\n## Adding Features\n\n- **New page**: Create `src/pages/about.tsx` → `/about`\n- **API endpoint**: Create `src/api/users.ts` → `/api/users`\n- **AI agent**: Create `agents/assistant.ts`\n- **MCP tool**: Create `tools/search.ts`\n\n## Code Examples\n\n### Page Component\n\n```tsx\n// src/pages/about.tsx\nexport default function About() {\n return <h1>About</h1>;\n}\n```\n\n### API Route\n\n```ts\n// src/api/hello.ts\nexport function GET() {\n return Response.json({ message: \"Hello\" });\n}\n```\n\n### Dynamic API Route\n\n```ts\n// src/api/users/[id].ts\nexport function GET(_req: Request, { params }: { params: { id: string } }) {\n return Response.json({ id: params.id });\n}\n```\n\n## Conventions\n\n- TypeScript required\n- React 19 features (use, Server Components)\n- Tailwind CSS for styling\n- Co-locate tests with implementation\n- Use `#veryfront/*` imports for framework modules\n\n## Testing\n\n- Development: `veryfront dev` (http://localhost:3000)\n- API endpoints: `/api/*`\n- Run tests: `deno task test`\n"
|
|
615
|
-
}
|
|
616
|
-
},
|
|
617
|
-
"ai-rules:windsurf.md": {
|
|
618
|
-
"files": {
|
|
619
|
-
"windsurf.md": "# Veryfront Project\n\nYou are in a Veryfront project - zero-config React meta-framework for AI-native applications.\n\n## Commands\n\n- `veryfront dev` - Start development server with HMR\n- `veryfront build` - Build for production\n- `veryfront deploy` - Deploy to Veryfront cloud\n- `veryfront generate page <name>` - Generate a new page\n- `veryfront generate api <name>` - Generate an API route\n\n## Project Structure\n\n```\nsrc/\n├── pages/ # File-based routing (pages/*.tsx → routes)\n├── api/ # API routes (api/*.ts → /api/*)\n├── components/ # Shared React components\n├── ai/\n│ ├── agents/ # AI agents\n│ └── tools/ # MCP tools\n└── styles/ # Global styles\n```\n\n## Adding Features\n\n- **New page**: Create `src/pages/about.tsx` → accessible at `/about`\n- **API endpoint**: Create `src/api/users.ts` → accessible at `/api/users`\n- **AI agent**: Create `agents/assistant.ts`\n- **MCP tool**: Create `tools/search.ts`\n\n## Conventions\n\n- TypeScript required\n- React 19 features (use, Server Components)\n- Tailwind CSS for styling\n- Co-locate tests with implementation (`*.test.ts`)\n- Use `#veryfront/*` imports for framework modules\n\n## File Patterns\n\n```tsx\n// Page component (src/pages/about.tsx)\nexport default function About() {\n return <h1>About</h1>;\n}\n\n// API route (src/api/hello.ts)\nexport function GET() {\n return Response.json({ message: \"Hello\" });\n}\n\n// API with params (src/api/users/[id].ts)\nexport function GET(_req: Request, { params }: { params: { id: string } }) {\n return Response.json({ id: params.id });\n}\n```\n\n## Testing\n\n- Run `veryfront dev` and check browser at http://localhost:3000\n- API endpoints available at `/api/*`\n- Use `deno task test` to run tests\n"
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
};
|