veryfront 0.1.501 → 0.1.503
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/cli/commands/analyze-chunks/handler.d.ts +4 -4
- package/esm/cli/commands/analyze-chunks/handler.d.ts.map +1 -1
- package/esm/cli/commands/analyze-chunks/handler.js +6 -5
- package/esm/cli/commands/build/handler.d.ts +39 -27
- package/esm/cli/commands/build/handler.d.ts.map +1 -1
- package/esm/cli/commands/build/handler.js +15 -14
- package/esm/cli/commands/clean/command.js +1 -1
- package/esm/cli/commands/clean/handler.d.ts.map +1 -1
- package/esm/cli/commands/clean/handler.js +9 -8
- package/esm/cli/commands/demo/handler.d.ts +5 -5
- package/esm/cli/commands/demo/handler.d.ts.map +1 -1
- package/esm/cli/commands/demo/handler.js +7 -6
- package/esm/cli/commands/deploy/command.d.ts +30 -19
- package/esm/cli/commands/deploy/command.d.ts.map +1 -1
- package/esm/cli/commands/deploy/command.js +11 -10
- package/esm/cli/commands/dev/handler.d.ts +6 -6
- package/esm/cli/commands/dev/handler.d.ts.map +1 -1
- package/esm/cli/commands/dev/handler.js +8 -7
- package/esm/cli/commands/doctor/handler.d.ts +3 -3
- package/esm/cli/commands/doctor/handler.d.ts.map +1 -1
- package/esm/cli/commands/doctor/handler.js +5 -4
- package/esm/cli/commands/files/command.d.ts +38 -36
- package/esm/cli/commands/files/command.d.ts.map +1 -1
- package/esm/cli/commands/files/command.js +35 -31
- package/esm/cli/commands/generate/handler.d.ts +4 -4
- package/esm/cli/commands/generate/handler.d.ts.map +1 -1
- package/esm/cli/commands/generate/handler.js +6 -5
- package/esm/cli/commands/generate/integration-generator-helpers.js +4 -4
- package/esm/cli/commands/generate/integration-generator.js +8 -4
- package/esm/cli/commands/install/handler.d.ts +5 -5
- package/esm/cli/commands/install/handler.d.ts.map +1 -1
- package/esm/cli/commands/install/handler.js +7 -6
- package/esm/cli/commands/install/install.d.ts.map +1 -1
- package/esm/cli/commands/install/install.js +7 -4
- package/esm/cli/commands/install/types.d.ts +52 -43
- package/esm/cli/commands/install/types.d.ts.map +1 -1
- package/esm/cli/commands/install/types.js +26 -21
- package/esm/cli/commands/install/uninstall.d.ts.map +1 -1
- package/esm/cli/commands/install/uninstall.js +4 -2
- package/esm/cli/commands/knowledge/command.d.ts +19 -17
- package/esm/cli/commands/knowledge/command.d.ts.map +1 -1
- package/esm/cli/commands/knowledge/command.js +21 -20
- package/esm/cli/commands/lock/handler.d.ts.map +1 -1
- package/esm/cli/commands/lock/handler.js +10 -9
- package/esm/cli/commands/mcp/handler.d.ts +3 -3
- package/esm/cli/commands/mcp/handler.d.ts.map +1 -1
- package/esm/cli/commands/mcp/handler.js +5 -4
- package/esm/cli/commands/merge/command.d.ts +21 -15
- package/esm/cli/commands/merge/command.d.ts.map +1 -1
- package/esm/cli/commands/merge/command.js +9 -8
- package/esm/cli/commands/open/command.d.ts +17 -12
- package/esm/cli/commands/open/command.d.ts.map +1 -1
- package/esm/cli/commands/open/command.js +7 -6
- package/esm/cli/commands/pull/command.d.ts +36 -25
- package/esm/cli/commands/pull/command.d.ts.map +1 -1
- package/esm/cli/commands/pull/command.js +14 -13
- package/esm/cli/commands/push/command.d.ts +27 -19
- package/esm/cli/commands/push/command.d.ts.map +1 -1
- package/esm/cli/commands/push/command.js +11 -10
- package/esm/cli/commands/routes/handler.d.ts +4 -4
- package/esm/cli/commands/routes/handler.d.ts.map +1 -1
- package/esm/cli/commands/routes/handler.js +6 -5
- package/esm/cli/commands/schema/handler.d.ts.map +1 -1
- package/esm/cli/commands/schema/handler.js +5 -4
- package/esm/cli/commands/serve/handler.d.ts +9 -9
- package/esm/cli/commands/serve/handler.d.ts.map +1 -1
- package/esm/cli/commands/serve/handler.js +10 -9
- package/esm/cli/commands/start/handler.d.ts +6 -6
- package/esm/cli/commands/start/handler.d.ts.map +1 -1
- package/esm/cli/commands/start/handler.js +8 -7
- package/esm/cli/commands/studio/handler.d.ts +5 -5
- package/esm/cli/commands/studio/handler.d.ts.map +1 -1
- package/esm/cli/commands/studio/handler.js +7 -6
- package/esm/cli/commands/styles/command.d.ts.map +1 -1
- package/esm/cli/commands/styles/command.js +9 -8
- package/esm/cli/commands/styles/handler.d.ts +12 -12
- package/esm/cli/commands/styles/handler.d.ts.map +1 -1
- package/esm/cli/commands/styles/handler.js +7 -6
- package/esm/cli/commands/task/handler.d.ts +12 -12
- package/esm/cli/commands/task/handler.d.ts.map +1 -1
- package/esm/cli/commands/task/handler.js +7 -6
- package/esm/cli/commands/test/handler.d.ts.map +1 -1
- package/esm/cli/commands/test/handler.js +6 -5
- package/esm/cli/commands/up/command.d.ts +14 -10
- package/esm/cli/commands/up/command.d.ts.map +1 -1
- package/esm/cli/commands/up/command.js +6 -5
- package/esm/cli/commands/uploads/command.d.ts +43 -41
- package/esm/cli/commands/uploads/command.d.ts.map +1 -1
- package/esm/cli/commands/uploads/command.js +40 -36
- package/esm/cli/commands/worker/handler.d.ts +20 -23
- package/esm/cli/commands/worker/handler.d.ts.map +1 -1
- package/esm/cli/commands/worker/handler.js +11 -10
- package/esm/cli/commands/workflow/handler.d.ts +14 -16
- package/esm/cli/commands/workflow/handler.d.ts.map +1 -1
- package/esm/cli/commands/workflow/handler.js +8 -7
- package/esm/cli/mcp/jsonrpc.d.ts +34 -18
- package/esm/cli/mcp/jsonrpc.d.ts.map +1 -1
- package/esm/cli/mcp/jsonrpc.js +21 -17
- package/esm/cli/mcp/remote-file-tools.d.ts +79 -83
- package/esm/cli/mcp/remote-file-tools.d.ts.map +1 -1
- package/esm/cli/mcp/remote-file-tools.js +79 -67
- package/esm/cli/mcp/server.d.ts +0 -1
- package/esm/cli/mcp/server.d.ts.map +1 -1
- package/esm/cli/mcp/server.js +2 -60
- package/esm/cli/mcp/tools/bootstrap-tool.d.ts +5 -5
- package/esm/cli/mcp/tools/bootstrap-tool.d.ts.map +1 -1
- package/esm/cli/mcp/tools/bootstrap-tool.js +5 -4
- package/esm/cli/mcp/tools/build-tool.d.ts +9 -9
- package/esm/cli/mcp/tools/build-tool.d.ts.map +1 -1
- package/esm/cli/mcp/tools/build-tool.js +9 -8
- package/esm/cli/mcp/tools/catalog-tools.d.ts +18 -33
- package/esm/cli/mcp/tools/catalog-tools.d.ts.map +1 -1
- package/esm/cli/mcp/tools/catalog-tools.js +19 -14
- package/esm/cli/mcp/tools/context7-tools.d.ts.map +1 -1
- package/esm/cli/mcp/tools/context7-tools.js +9 -9
- package/esm/cli/mcp/tools/deploy-tool.d.ts +7 -7
- package/esm/cli/mcp/tools/deploy-tool.d.ts.map +1 -1
- package/esm/cli/mcp/tools/deploy-tool.js +7 -6
- package/esm/cli/mcp/tools/dev-tools.d.ts +31 -35
- package/esm/cli/mcp/tools/dev-tools.d.ts.map +1 -1
- package/esm/cli/mcp/tools/dev-tools.js +31 -25
- package/esm/cli/mcp/tools/introspection-tools.d.ts.map +1 -1
- package/esm/cli/mcp/tools/introspection-tools.js +6 -6
- package/esm/cli/mcp/tools/project-tools.d.ts +20 -25
- package/esm/cli/mcp/tools/project-tools.d.ts.map +1 -1
- package/esm/cli/mcp/tools/project-tools.js +20 -16
- package/esm/cli/mcp/tools/run-lint-tool.d.ts +5 -5
- package/esm/cli/mcp/tools/run-lint-tool.d.ts.map +1 -1
- package/esm/cli/mcp/tools/run-lint-tool.js +5 -4
- package/esm/cli/mcp/tools/run-tests-tool.d.ts +7 -7
- package/esm/cli/mcp/tools/run-tests-tool.d.ts.map +1 -1
- package/esm/cli/mcp/tools/run-tests-tool.js +7 -6
- package/esm/cli/mcp/tools/scaffold-tools.d.ts +12 -33
- package/esm/cli/mcp/tools/scaffold-tools.d.ts.map +1 -1
- package/esm/cli/mcp/tools/scaffold-tools.js +23 -21
- package/esm/cli/mcp/tools/skill-tools.d.ts +10 -10
- package/esm/cli/mcp/tools/skill-tools.d.ts.map +1 -1
- package/esm/cli/mcp/tools/skill-tools.js +8 -8
- package/esm/cli/mcp/tools.d.ts +27 -48
- package/esm/cli/mcp/tools.d.ts.map +1 -1
- package/esm/cli/mcp/tools.js +26 -21
- package/esm/cli/shared/args.d.ts +11 -8
- package/esm/cli/shared/args.d.ts.map +1 -1
- package/esm/cli/shared/args.js +9 -6
- package/esm/cli/shared/config.d.ts +38 -20
- package/esm/cli/shared/config.d.ts.map +1 -1
- package/esm/cli/shared/config.js +20 -17
- package/esm/cli/shared/types.d.ts +4 -7
- package/esm/cli/shared/types.d.ts.map +1 -1
- package/esm/cli/shared/types.js +3 -2
- package/esm/cli/skills/types.d.ts +30 -16
- package/esm/cli/skills/types.d.ts.map +1 -1
- package/esm/cli/skills/types.js +17 -15
- package/esm/cli/templates/manifest.js +211 -211
- package/esm/deno.d.ts +5 -0
- package/esm/deno.js +10 -4
- package/esm/extensions/ext-zod/src/adapter.d.ts +22 -0
- package/esm/extensions/ext-zod/src/adapter.d.ts.map +1 -0
- package/esm/extensions/ext-zod/src/adapter.js +159 -0
- package/esm/extensions/ext-zod/src/index.d.ts +19 -0
- package/esm/extensions/ext-zod/src/index.d.ts.map +1 -0
- package/esm/extensions/ext-zod/src/index.js +32 -0
- package/esm/extensions/ext-zod/src/json-schema.d.ts +15 -0
- package/esm/extensions/ext-zod/src/json-schema.d.ts.map +1 -0
- package/esm/extensions/ext-zod/src/json-schema.js +247 -0
- package/esm/src/agent/ag-ui-detached-start.d.ts +97 -50
- package/esm/src/agent/ag-ui-detached-start.d.ts.map +1 -1
- package/esm/src/agent/ag-ui-detached-start.js +26 -17
- package/esm/src/agent/ag-ui-forwarded-context.d.ts +3 -3
- package/esm/src/agent/ag-ui-forwarded-context.d.ts.map +1 -1
- package/esm/src/agent/ag-ui-forwarded-context.js +3 -2
- package/esm/src/agent/ag-ui-handler.d.ts.map +1 -1
- package/esm/src/agent/ag-ui-handler.js +5 -3
- package/esm/src/agent/ag-ui-host-support.d.ts +119 -65
- package/esm/src/agent/ag-ui-host-support.d.ts.map +1 -1
- package/esm/src/agent/ag-ui-host-support.js +48 -42
- package/esm/src/agent/ag-ui-request-shared.d.ts.map +1 -1
- package/esm/src/agent/ag-ui-request-shared.js +11 -2
- package/esm/src/agent/ag-ui-run-control.d.ts +15 -8
- package/esm/src/agent/ag-ui-run-control.d.ts.map +1 -1
- package/esm/src/agent/ag-ui-run-control.js +18 -13
- package/esm/src/agent/ag-ui-runtime-handler.d.ts.map +1 -1
- package/esm/src/agent/ag-ui-runtime-handler.js +5 -3
- package/esm/src/agent/ag-ui-sse-parser.d.ts +57 -0
- package/esm/src/agent/ag-ui-sse-parser.d.ts.map +1 -0
- package/esm/src/agent/ag-ui-sse-parser.js +227 -0
- package/esm/src/agent/ag-ui-tool-shared.d.ts.map +1 -1
- package/esm/src/agent/ag-ui-tool-shared.js +3 -2
- package/esm/src/agent/chat-handler.d.ts.map +1 -1
- package/esm/src/agent/chat-handler.js +58 -57
- package/esm/src/agent/composition/composition.js +2 -2
- package/esm/src/agent/conversation-bootstrap.d.ts +13 -16
- package/esm/src/agent/conversation-bootstrap.d.ts.map +1 -1
- package/esm/src/agent/conversation-bootstrap.js +19 -12
- package/esm/src/agent/conversation-hosted-lifecycle.d.ts.map +1 -1
- package/esm/src/agent/conversation-run-context.d.ts +1 -10
- package/esm/src/agent/conversation-run-context.d.ts.map +1 -1
- package/esm/src/agent/conversation-run-events.d.ts +9 -5
- package/esm/src/agent/conversation-run-events.d.ts.map +1 -1
- package/esm/src/agent/conversation-run-events.js +6 -4
- package/esm/src/agent/default-hosted-invoke-agent-tool.d.ts +37 -16
- package/esm/src/agent/default-hosted-invoke-agent-tool.d.ts.map +1 -1
- package/esm/src/agent/default-hosted-invoke-agent-tool.js +10 -5
- package/esm/src/agent/durable.d.ts +70 -129
- package/esm/src/agent/durable.d.ts.map +1 -1
- package/esm/src/agent/durable.js +107 -109
- package/esm/src/agent/hosted-ag-ui-chat-request.d.ts +29 -11
- package/esm/src/agent/hosted-ag-ui-chat-request.d.ts.map +1 -1
- package/esm/src/agent/hosted-ag-ui-chat-request.js +16 -15
- package/esm/src/agent/hosted-agent-service-config.d.ts +6 -6
- package/esm/src/agent/hosted-agent-service-routes.d.ts.map +1 -1
- package/esm/src/agent/hosted-agent-service-routes.js +4 -3
- package/esm/src/agent/hosted-chat-request-parser.d.ts.map +1 -1
- package/esm/src/agent/hosted-chat-request-parser.js +7 -4
- package/esm/src/agent/hosted-chat-request.d.ts +291 -20
- package/esm/src/agent/hosted-chat-request.d.ts.map +1 -1
- package/esm/src/agent/hosted-chat-request.js +33 -31
- package/esm/src/agent/hosted-child-fork-step-message-preparation.d.ts.map +1 -1
- package/esm/src/agent/hosted-child-status.d.ts.map +1 -1
- package/esm/src/agent/hosted-child-status.js +2 -0
- package/esm/src/agent/hosted-child-tool-input.d.ts +21 -11
- package/esm/src/agent/hosted-child-tool-input.d.ts.map +1 -1
- package/esm/src/agent/hosted-child-tool-input.js +12 -10
- package/esm/src/agent/hosted-form-input-tool.d.ts +78 -86
- package/esm/src/agent/hosted-form-input-tool.d.ts.map +1 -1
- package/esm/src/agent/hosted-form-input-tool.js +10 -3
- package/esm/src/agent/human-input.d.ts +337 -277
- package/esm/src/agent/human-input.d.ts.map +1 -1
- package/esm/src/agent/human-input.js +88 -72
- package/esm/src/agent/index.d.ts +8 -7
- package/esm/src/agent/index.d.ts.map +1 -1
- package/esm/src/agent/index.js +8 -7
- package/esm/src/agent/input-request-protocol.d.ts +461 -1611
- package/esm/src/agent/input-request-protocol.d.ts.map +1 -1
- package/esm/src/agent/input-request-protocol.js +120 -107
- package/esm/src/agent/invoke-agent-child-runs.d.ts +109 -107
- package/esm/src/agent/invoke-agent-child-runs.d.ts.map +1 -1
- package/esm/src/agent/invoke-agent-child-runs.js +36 -29
- package/esm/src/agent/runtime-ag-ui-contract.d.ts +206 -206
- package/esm/src/agent/runtime-ag-ui-contract.d.ts.map +1 -1
- package/esm/src/agent/runtime-ag-ui-contract.js +96 -91
- package/esm/src/agent/runtime-agent-definition.d.ts +47 -24
- package/esm/src/agent/runtime-agent-definition.d.ts.map +1 -1
- package/esm/src/agent/runtime-agent-definition.js +26 -20
- package/esm/src/agent/runtime-agent-invocation-contract.d.ts +306 -168
- package/esm/src/agent/runtime-agent-invocation-contract.d.ts.map +1 -1
- package/esm/src/agent/runtime-agent-invocation-contract.js +116 -101
- package/esm/src/agent/runtime-builtin-skill-files.d.ts.map +1 -1
- package/esm/src/agent/runtime-builtin-skill-files.js +3 -2
- package/esm/src/agent/runtime-client-profile.d.ts +23 -32
- package/esm/src/agent/runtime-client-profile.d.ts.map +1 -1
- package/esm/src/agent/runtime-client-profile.js +29 -27
- package/esm/src/agent/runtime-load-skill-tool.d.ts +6 -7
- package/esm/src/agent/runtime-load-skill-tool.d.ts.map +1 -1
- package/esm/src/agent/runtime-load-skill-tool.js +7 -6
- package/esm/src/agent/runtime-project-files-client.d.ts +19 -10
- package/esm/src/agent/runtime-project-files-client.d.ts.map +1 -1
- package/esm/src/agent/runtime-project-files-client.js +33 -35
- package/esm/src/agent/runtime-skill-metadata.d.ts +6 -20
- package/esm/src/agent/runtime-skill-metadata.d.ts.map +1 -1
- package/esm/src/agent/runtime-skill-metadata.js +24 -18
- package/esm/src/agent/runtime-upload-url-client.d.ts.map +1 -1
- package/esm/src/agent/runtime-upload-url-client.js +19 -25
- package/esm/src/agent/schemas/agent.schema.d.ts +258 -307
- package/esm/src/agent/schemas/agent.schema.d.ts.map +1 -1
- package/esm/src/agent/schemas/agent.schema.js +105 -101
- package/esm/src/agent/schemas/index.d.ts +2 -2
- package/esm/src/agent/schemas/index.d.ts.map +1 -1
- package/esm/src/agent/schemas/index.js +2 -2
- package/esm/src/agent/schemas/tool.schema.d.ts +5 -5
- package/esm/src/agent/schemas/tool.schema.d.ts.map +1 -1
- package/esm/src/agent/schemas/tool.schema.js +4 -4
- package/esm/src/cache/schemas/cache-backend.schema.d.ts +15 -14
- package/esm/src/cache/schemas/cache-backend.schema.d.ts.map +1 -1
- package/esm/src/cache/schemas/cache-backend.schema.js +10 -7
- package/esm/src/cache/schemas/cache-key.schema.d.ts +12 -10
- package/esm/src/cache/schemas/cache-key.schema.d.ts.map +1 -1
- package/esm/src/cache/schemas/cache-key.schema.js +8 -6
- package/esm/src/channels/control-plane.d.ts +246 -156
- package/esm/src/channels/control-plane.d.ts.map +1 -1
- package/esm/src/channels/control-plane.js +80 -73
- package/esm/src/channels/invoke.d.ts +220 -134
- package/esm/src/channels/invoke.d.ts.map +1 -1
- package/esm/src/channels/invoke.js +105 -93
- package/esm/src/chat/ag-ui.d.ts +494 -273
- package/esm/src/chat/ag-ui.d.ts.map +1 -1
- package/esm/src/chat/ag-ui.js +164 -180
- package/esm/src/chat/conversation.d.ts +273 -183
- package/esm/src/chat/conversation.d.ts.map +1 -1
- package/esm/src/chat/conversation.js +97 -111
- package/esm/src/chat/types.d.ts +635 -14
- package/esm/src/chat/types.d.ts.map +1 -1
- package/esm/src/chat/types.js +146 -174
- package/esm/src/config/schemas/config.schema.d.ts +799 -395
- package/esm/src/config/schemas/config.schema.d.ts.map +1 -1
- package/esm/src/config/schemas/config.schema.js +300 -268
- package/esm/src/data/schemas/data.schema.d.ts +89 -48
- package/esm/src/data/schemas/data.schema.d.ts.map +1 -1
- package/esm/src/data/schemas/data.schema.js +36 -29
- package/esm/src/extensions/builtin-extensions.d.ts.map +1 -1
- package/esm/src/extensions/builtin-extensions.js +9 -0
- package/esm/src/extensions/schema/index.d.ts +1 -1
- package/esm/src/extensions/schema/index.d.ts.map +1 -1
- package/esm/src/extensions/schema/json-schema.d.ts +23 -0
- package/esm/src/extensions/schema/json-schema.d.ts.map +1 -0
- package/esm/src/extensions/schema/json-schema.js +8 -0
- package/esm/src/extensions/schema/schema-validator.d.ts +97 -4
- package/esm/src/extensions/schema/schema-validator.d.ts.map +1 -1
- package/esm/src/html/hydration-script-builder/hydration-data-generator.js +1 -1
- package/esm/src/html/schemas/html.schema.d.ts +107 -90
- package/esm/src/html/schemas/html.schema.d.ts.map +1 -1
- package/esm/src/html/schemas/html.schema.js +60 -52
- package/esm/src/html/utils.d.ts.map +1 -1
- package/esm/src/html/utils.js +1 -1
- package/esm/src/integrations/schema.d.ts +455 -433
- package/esm/src/integrations/schema.d.ts.map +1 -1
- package/esm/src/integrations/schema.js +105 -95
- package/esm/src/internal-agents/ag-ui-sse.d.ts.map +1 -1
- package/esm/src/internal-agents/ag-ui-sse.js +60 -82
- package/esm/src/internal-agents/control-plane-auth.d.ts +10 -10
- package/esm/src/internal-agents/control-plane-auth.d.ts.map +1 -1
- package/esm/src/internal-agents/run-stream.d.ts.map +1 -1
- package/esm/src/internal-agents/run-stream.js +3 -2
- package/esm/src/internal-agents/schema.d.ts +272 -281
- package/esm/src/internal-agents/schema.d.ts.map +1 -1
- package/esm/src/internal-agents/schema.js +55 -47
- package/esm/src/issues/core.d.ts.map +1 -1
- package/esm/src/issues/mcp.d.ts.map +1 -1
- package/esm/src/issues/mcp.js +47 -41
- package/esm/src/issues/schemas/index.d.ts +1 -1
- package/esm/src/issues/schemas/index.d.ts.map +1 -1
- package/esm/src/issues/schemas/index.js +1 -1
- package/esm/src/issues/schemas/issue.schema.d.ts +151 -116
- package/esm/src/issues/schemas/issue.schema.d.ts.map +1 -1
- package/esm/src/issues/schemas/issue.schema.js +64 -55
- package/esm/src/jobs/schemas.d.ts +1087 -658
- package/esm/src/jobs/schemas.d.ts.map +1 -1
- package/esm/src/jobs/schemas.js +244 -214
- package/esm/src/mcp/index.d.ts +6 -2
- package/esm/src/mcp/index.d.ts.map +1 -1
- package/esm/src/mcp/index.js +6 -2
- package/esm/src/mcp/schemas/index.d.ts +1 -1
- package/esm/src/mcp/schemas/index.d.ts.map +1 -1
- package/esm/src/mcp/schemas/index.js +1 -1
- package/esm/src/mcp/schemas/mcp.schema.d.ts +59 -31
- package/esm/src/mcp/schemas/mcp.schema.d.ts.map +1 -1
- package/esm/src/mcp/schemas/mcp.schema.js +28 -24
- package/esm/src/mcp/types.d.ts +2 -2
- package/esm/src/mcp/types.d.ts.map +1 -1
- package/esm/src/oauth/providers/atlassian.d.ts +36 -30
- package/esm/src/oauth/providers/atlassian.d.ts.map +1 -1
- package/esm/src/oauth/providers/common.d.ts +312 -260
- package/esm/src/oauth/providers/common.d.ts.map +1 -1
- package/esm/src/oauth/providers/google.d.ts +48 -40
- package/esm/src/oauth/providers/google.d.ts.map +1 -1
- package/esm/src/oauth/providers/microsoft.d.ts +48 -40
- package/esm/src/oauth/providers/microsoft.d.ts.map +1 -1
- package/esm/src/oauth/schemas/oauth.schema.d.ts +180 -92
- package/esm/src/oauth/schemas/oauth.schema.d.ts.map +1 -1
- package/esm/src/oauth/schemas/oauth.schema.js +67 -59
- package/esm/src/platform/adapters/fs/github/github-api-client.d.ts.map +1 -1
- package/esm/src/platform/adapters/fs/github/github-api-client.js +4 -4
- package/esm/src/platform/adapters/fs/github/index.d.ts +1 -1
- package/esm/src/platform/adapters/fs/github/index.d.ts.map +1 -1
- package/esm/src/platform/adapters/fs/github/index.js +1 -1
- package/esm/src/platform/adapters/fs/github/schemas/github-api.schema.d.ts +97 -140
- package/esm/src/platform/adapters/fs/github/schemas/github-api.schema.d.ts.map +1 -1
- package/esm/src/platform/adapters/fs/github/schemas/github-api.schema.js +40 -37
- package/esm/src/platform/adapters/fs/github/schemas/index.d.ts +1 -1
- package/esm/src/platform/adapters/fs/github/schemas/index.d.ts.map +1 -1
- package/esm/src/platform/adapters/fs/github/schemas/index.js +1 -1
- package/esm/src/platform/adapters/fs/veryfront/proxy-manager.d.ts.map +1 -1
- package/esm/src/platform/adapters/fs/veryfront/proxy-manager.js +7 -4
- package/esm/src/platform/adapters/fs/veryfront/schemas/index.d.ts +1 -1
- package/esm/src/platform/adapters/fs/veryfront/schemas/index.d.ts.map +1 -1
- package/esm/src/platform/adapters/fs/veryfront/schemas/index.js +1 -1
- package/esm/src/platform/adapters/fs/veryfront/schemas/proxy-manager.schema.d.ts +11 -11
- package/esm/src/platform/adapters/fs/veryfront/schemas/proxy-manager.schema.d.ts.map +1 -1
- package/esm/src/platform/adapters/fs/veryfront/schemas/proxy-manager.schema.js +10 -10
- package/esm/src/platform/adapters/fs/veryfront/stat-operations.d.ts.map +1 -1
- package/esm/src/platform/adapters/fs/veryfront/stat-operations.js +1 -0
- package/esm/src/platform/adapters/veryfront-api-client/client.d.ts +63 -63
- package/esm/src/platform/adapters/veryfront-api-client/index.d.ts +1 -1
- package/esm/src/platform/adapters/veryfront-api-client/index.d.ts.map +1 -1
- package/esm/src/platform/adapters/veryfront-api-client/index.js +1 -1
- package/esm/src/platform/adapters/veryfront-api-client/operations.d.ts.map +1 -1
- package/esm/src/platform/adapters/veryfront-api-client/operations.js +11 -21
- package/esm/src/platform/adapters/veryfront-api-client/schemas/api.schema.d.ts +444 -471
- package/esm/src/platform/adapters/veryfront-api-client/schemas/api.schema.d.ts.map +1 -1
- package/esm/src/platform/adapters/veryfront-api-client/schemas/api.schema.js +150 -122
- package/esm/src/platform/adapters/veryfront-api-client/schemas/index.d.ts +1 -1
- package/esm/src/platform/adapters/veryfront-api-client/schemas/index.d.ts.map +1 -1
- package/esm/src/platform/adapters/veryfront-api-client/schemas/index.js +1 -1
- package/esm/src/prompt/schemas/prompt.schema.d.ts +10 -9
- package/esm/src/prompt/schemas/prompt.schema.d.ts.map +1 -1
- package/esm/src/prompt/schemas/prompt.schema.js +8 -8
- package/esm/src/proxy/handler.d.ts.map +1 -1
- package/esm/src/proxy/handler.js +0 -1
- package/esm/src/repositories/schemas/repository.schema.d.ts +40 -25
- package/esm/src/repositories/schemas/repository.schema.d.ts.map +1 -1
- package/esm/src/repositories/schemas/repository.schema.js +23 -19
- package/esm/src/resource/index.d.ts +6 -2
- package/esm/src/resource/index.d.ts.map +1 -1
- package/esm/src/resource/index.js +6 -2
- package/esm/src/resource/schemas/index.d.ts +1 -1
- package/esm/src/resource/schemas/index.d.ts.map +1 -1
- package/esm/src/resource/schemas/index.js +1 -1
- package/esm/src/resource/schemas/resource.schema.d.ts +13 -16
- package/esm/src/resource/schemas/resource.schema.d.ts.map +1 -1
- package/esm/src/resource/schemas/resource.schema.js +9 -6
- package/esm/src/resource/types.d.ts +3 -3
- package/esm/src/resource/types.d.ts.map +1 -1
- package/esm/src/routing/api/openapi/types.d.ts +4 -4
- package/esm/src/routing/api/openapi/types.d.ts.map +1 -1
- package/esm/src/sandbox/hosted-tools.d.ts.map +1 -1
- package/esm/src/sandbox/hosted-tools.js +11 -13
- package/esm/src/schemas/common.d.ts +48 -28
- package/esm/src/schemas/common.d.ts.map +1 -1
- package/esm/src/schemas/common.js +67 -28
- package/esm/src/schemas/define.d.ts +3 -2
- package/esm/src/schemas/define.d.ts.map +1 -1
- package/esm/src/schemas/define.js +12 -6
- package/esm/src/schemas/index.d.ts +8 -4
- package/esm/src/schemas/index.d.ts.map +1 -1
- package/esm/src/schemas/index.js +8 -7
- package/esm/src/schemas/json-schema.d.ts +25 -0
- package/esm/src/schemas/json-schema.d.ts.map +1 -0
- package/esm/src/schemas/json-schema.js +27 -0
- package/esm/src/schemas/primitives.d.ts +34 -23
- package/esm/src/schemas/primitives.d.ts.map +1 -1
- package/esm/src/schemas/primitives.js +25 -18
- package/esm/src/security/input-validation/handler.d.ts +3 -3
- package/esm/src/security/input-validation/handler.d.ts.map +1 -1
- package/esm/src/security/input-validation/parsers.d.ts +4 -4
- package/esm/src/security/input-validation/parsers.d.ts.map +1 -1
- package/esm/src/security/input-validation/parsers.js +35 -45
- package/esm/src/server/bootstrap.js +2 -2
- package/esm/src/server/dev-ui/manifest.js +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 +1 -0
- package/esm/src/server/handlers/request/agent-run-cancel.handler.d.ts.map +1 -1
- package/esm/src/server/handlers/request/agent-run-cancel.handler.js +2 -3
- package/esm/src/server/handlers/request/agent-run-resume.handler.d.ts.map +1 -1
- package/esm/src/server/handlers/request/agent-run-resume.handler.js +4 -5
- package/esm/src/server/handlers/request/agent-stream.handler.d.ts.map +1 -1
- package/esm/src/server/handlers/request/agent-stream.handler.js +3 -4
- package/esm/src/server/handlers/request/internal-agents-list.handler.d.ts.map +1 -1
- package/esm/src/server/handlers/request/internal-agents-list.handler.js +2 -4
- package/esm/src/server/runtime-handler/environment-resolution.d.ts.map +1 -1
- package/esm/src/server/runtime-handler/environment-resolution.js +4 -5
- package/esm/src/server/runtime-handler/request-utils.d.ts.map +1 -1
- package/esm/src/server/runtime-handler/request-utils.js +1 -2
- package/esm/src/server/schemas/action.schema.d.ts +10 -6
- package/esm/src/server/schemas/action.schema.d.ts.map +1 -1
- package/esm/src/server/schemas/action.schema.js +7 -5
- package/esm/src/skill/tools.d.ts.map +1 -1
- package/esm/src/skill/tools.js +24 -21
- package/esm/src/tool/factory.d.ts +1 -1
- package/esm/src/tool/factory.d.ts.map +1 -1
- package/esm/src/tool/factory.js +12 -1
- package/esm/src/tool/host-tools.d.ts +1 -1
- package/esm/src/tool/host-tools.d.ts.map +1 -1
- package/esm/src/tool/host-tools.js +17 -3
- package/esm/src/tool/index.d.ts +9 -5
- package/esm/src/tool/index.d.ts.map +1 -1
- package/esm/src/tool/index.js +9 -5
- package/esm/src/tool/remote-source-tools.d.ts.map +1 -1
- package/esm/src/tool/remote-source-tools.js +9 -2
- package/esm/src/tool/schema/json-schema.d.ts +8 -15
- package/esm/src/tool/schema/json-schema.d.ts.map +1 -1
- package/esm/src/tool/schema/json-schema.js +7 -0
- package/esm/src/tool/schema/zod-json-schema.d.ts +33 -4
- package/esm/src/tool/schema/zod-json-schema.d.ts.map +1 -1
- package/esm/src/tool/schema/zod-json-schema.js +70 -159
- package/esm/src/tool/sleep.d.ts +21 -11
- package/esm/src/tool/sleep.d.ts.map +1 -1
- package/esm/src/tool/sleep.js +42 -4
- package/esm/src/tool/types.d.ts +12 -8
- package/esm/src/tool/types.d.ts.map +1 -1
- package/esm/src/transforms/npm-import-rewrites.d.ts +1 -1
- package/esm/src/transforms/npm-import-rewrites.d.ts.map +1 -1
- package/esm/src/transforms/npm-import-rewrites.js +1 -3
- package/esm/src/utils/version-constant.d.ts +1 -1
- package/esm/src/utils/version-constant.js +1 -1
- package/esm/src/workflow/api.d.ts +22 -22
- package/esm/src/workflow/blob/veryfront-cloud-storage.d.ts.map +1 -1
- package/esm/src/workflow/blob/veryfront-cloud-storage.js +37 -33
- package/esm/src/workflow/claude-code/tool.d.ts +37 -44
- package/esm/src/workflow/claude-code/tool.d.ts.map +1 -1
- package/esm/src/workflow/claude-code/tool.js +11 -19
- package/esm/src/workflow/dsl/workflow.d.ts +3 -3
- package/esm/src/workflow/dsl/workflow.d.ts.map +1 -1
- package/esm/src/workflow/schemas/index.d.ts +1 -1
- package/esm/src/workflow/schemas/index.d.ts.map +1 -1
- package/esm/src/workflow/schemas/index.js +1 -1
- package/esm/src/workflow/schemas/workflow.schema.d.ts +194 -177
- package/esm/src/workflow/schemas/workflow.schema.d.ts.map +1 -1
- package/esm/src/workflow/schemas/workflow.schema.js +101 -83
- package/esm/src/workflow/types.d.ts +3 -3
- package/esm/src/workflow/types.d.ts.map +1 -1
- package/package.json +4 -1
- package/src/cli/commands/analyze-chunks/handler.ts +9 -5
- package/src/cli/commands/build/handler.ts +20 -15
- package/src/cli/commands/clean/command.ts +1 -1
- package/src/cli/commands/clean/handler.ts +12 -8
- package/src/cli/commands/demo/handler.ts +12 -7
- package/src/cli/commands/deploy/command.ts +17 -12
- package/src/cli/commands/dev/handler.ts +11 -7
- package/src/cli/commands/doctor/handler.ts +8 -4
- package/src/cli/commands/files/command.ts +64 -44
- package/src/cli/commands/generate/handler.ts +9 -5
- package/src/cli/commands/generate/integration-generator-helpers.ts +4 -4
- package/src/cli/commands/generate/integration-generator.ts +8 -4
- package/src/cli/commands/install/detect.ts +1 -1
- package/src/cli/commands/install/handler.ts +10 -6
- package/src/cli/commands/install/install.ts +20 -12
- package/src/cli/commands/install/types.ts +58 -41
- package/src/cli/commands/install/uninstall.ts +6 -2
- package/src/cli/commands/knowledge/command.ts +60 -52
- package/src/cli/commands/lock/handler.ts +13 -9
- package/src/cli/commands/mcp/handler.ts +8 -4
- package/src/cli/commands/merge/command.ts +14 -9
- package/src/cli/commands/open/command.ts +12 -7
- package/src/cli/commands/pull/command.ts +20 -15
- package/src/cli/commands/push/command.ts +17 -12
- package/src/cli/commands/routes/handler.ts +9 -5
- package/src/cli/commands/schema/handler.ts +8 -4
- package/src/cli/commands/serve/handler.ts +15 -11
- package/src/cli/commands/start/handler.ts +11 -7
- package/src/cli/commands/studio/handler.ts +10 -6
- package/src/cli/commands/styles/command.ts +22 -17
- package/src/cli/commands/styles/handler.ts +12 -7
- package/src/cli/commands/task/handler.ts +12 -7
- package/src/cli/commands/test/handler.ts +9 -5
- package/src/cli/commands/up/command.ts +11 -6
- package/src/cli/commands/uploads/command.ts +72 -52
- package/src/cli/commands/worker/handler.ts +16 -11
- package/src/cli/commands/workflow/handler.ts +13 -8
- package/src/cli/mcp/jsonrpc.ts +32 -19
- package/src/cli/mcp/remote-file-tools.ts +134 -97
- package/src/cli/mcp/server.ts +2 -80
- package/src/cli/mcp/tools/bootstrap-tool.ts +11 -7
- package/src/cli/mcp/tools/build-tool.ts +32 -28
- package/src/cli/mcp/tools/catalog-tools.ts +53 -42
- package/src/cli/mcp/tools/context7-tools.ts +29 -25
- package/src/cli/mcp/tools/deploy-tool.ts +17 -13
- package/src/cli/mcp/tools/dev-tools.ts +90 -66
- package/src/cli/mcp/tools/introspection-tools.ts +8 -6
- package/src/cli/mcp/tools/project-tools.ts +45 -32
- package/src/cli/mcp/tools/run-lint-tool.ts +11 -7
- package/src/cli/mcp/tools/run-tests-tool.ts +17 -13
- package/src/cli/mcp/tools/scaffold-tools.ts +43 -36
- package/src/cli/mcp/tools/skill-tools.ts +17 -12
- package/src/cli/mcp/tools.ts +65 -51
- package/src/cli/shared/args.ts +12 -9
- package/src/cli/shared/config.ts +32 -22
- package/src/cli/shared/project-source-context.ts +1 -1
- package/src/cli/shared/types.ts +5 -3
- package/src/cli/skills/types.ts +29 -24
- package/src/cli/templates/manifest.js +211 -211
- package/src/deno.js +10 -4
- package/src/extensions/ext-zod/src/adapter.ts +238 -0
- package/src/extensions/ext-zod/src/index.ts +36 -0
- package/src/extensions/ext-zod/src/json-schema.ts +311 -0
- package/src/src/agent/ag-ui-detached-start.ts +44 -21
- package/src/src/agent/ag-ui-forwarded-context.ts +6 -4
- package/src/src/agent/ag-ui-handler.ts +7 -3
- package/src/src/agent/ag-ui-host-support.ts +84 -66
- package/src/src/agent/ag-ui-request-shared.ts +16 -2
- package/src/src/agent/ag-ui-run-control.ts +26 -15
- package/src/src/agent/ag-ui-runtime-handler.ts +7 -3
- package/src/src/agent/ag-ui-sse-parser.ts +306 -0
- package/src/src/agent/ag-ui-tool-shared.ts +5 -2
- package/src/src/agent/chat-handler.ts +89 -67
- package/src/src/agent/composition/composition.ts +2 -2
- package/src/src/agent/conversation-bootstrap.ts +38 -17
- package/src/src/agent/conversation-hosted-lifecycle.ts +12 -10
- package/src/src/agent/conversation-run-events.ts +13 -5
- package/src/src/agent/default-hosted-invoke-agent-tool.ts +19 -8
- package/src/src/agent/durable.ts +186 -144
- package/src/src/agent/hosted-ag-ui-chat-request.ts +26 -16
- package/src/src/agent/hosted-agent-service-routes.ts +4 -3
- package/src/src/agent/hosted-chat-request-parser.ts +8 -4
- package/src/src/agent/hosted-chat-request.ts +46 -32
- package/src/src/agent/hosted-child-fork-step-message-preparation.ts +3 -1
- package/src/src/agent/hosted-child-status.ts +8 -1
- package/src/src/agent/hosted-child-tool-input.ts +27 -19
- package/src/src/agent/hosted-durable-child-fork-execution.ts +3 -3
- package/src/src/agent/hosted-form-input-tool.ts +15 -5
- package/src/src/agent/human-input.ts +129 -92
- package/src/src/agent/index.ts +38 -20
- package/src/src/agent/input-request-protocol.ts +193 -124
- package/src/src/agent/invoke-agent-child-runs.ts +64 -44
- package/src/src/agent/runtime-ag-ui-contract.ts +159 -123
- package/src/src/agent/runtime-agent-definition.ts +45 -24
- package/src/src/agent/runtime-agent-invocation-contract.ts +241 -174
- package/src/src/agent/runtime-builtin-skill-files.ts +3 -2
- package/src/src/agent/runtime-client-profile.ts +51 -34
- package/src/src/agent/runtime-load-skill-tool.ts +19 -12
- package/src/src/agent/runtime-project-files-client.ts +51 -39
- package/src/src/agent/runtime-skill-metadata.ts +41 -24
- package/src/src/agent/runtime-upload-url-client.ts +24 -24
- package/src/src/agent/schemas/agent.schema.ts +179 -142
- package/src/src/agent/schemas/index.ts +15 -15
- package/src/src/agent/schemas/tool.schema.ts +8 -5
- package/src/src/cache/schemas/cache-backend.schema.ts +18 -9
- package/src/src/cache/schemas/cache-key.schema.ts +13 -7
- package/src/src/channels/control-plane.ts +132 -96
- package/src/src/channels/invoke.ts +165 -120
- package/src/src/chat/ag-ui.ts +229 -225
- package/src/src/chat/conversation.ts +121 -124
- package/src/src/chat/types.ts +230 -205
- package/src/src/config/schemas/config.schema.ts +653 -610
- package/src/src/data/schemas/data.schema.ts +62 -39
- package/src/src/extensions/builtin-extensions.ts +9 -0
- package/src/src/extensions/schema/index.ts +3 -0
- package/src/src/extensions/schema/json-schema.ts +23 -0
- package/src/src/extensions/schema/schema-validator.ts +105 -6
- package/src/src/html/html-shell-generator.ts +1 -1
- package/src/src/html/hydration-script-builder/hydration-data-generator.ts +2 -2
- package/src/src/html/schemas/html.schema.ts +81 -65
- package/src/src/html/utils.ts +4 -1
- package/src/src/integrations/schema.ts +140 -111
- package/src/src/internal-agents/ag-ui-sse.ts +85 -83
- package/src/src/internal-agents/run-stream.ts +4 -2
- package/src/src/internal-agents/schema.ts +120 -89
- package/src/src/issues/core.ts +3 -1
- package/src/src/issues/mcp.ts +81 -58
- package/src/src/issues/schemas/index.ts +11 -0
- package/src/src/issues/schemas/issue.schema.ts +99 -73
- package/src/src/jobs/jobs-client.ts +4 -4
- package/src/src/jobs/schemas.ts +414 -297
- package/src/src/mcp/index.ts +6 -2
- package/src/src/mcp/schemas/index.ts +3 -0
- package/src/src/mcp/schemas/mcp.schema.ts +45 -29
- package/src/src/mcp/types.ts +2 -3
- package/src/src/oauth/schemas/oauth.schema.ts +96 -70
- package/src/src/platform/adapters/fs/github/github-api-client.ts +6 -6
- package/src/src/platform/adapters/fs/github/index.ts +5 -5
- package/src/src/platform/adapters/fs/github/schemas/github-api.schema.ts +58 -42
- package/src/src/platform/adapters/fs/github/schemas/index.ts +5 -5
- package/src/src/platform/adapters/fs/veryfront/proxy-manager.ts +9 -5
- package/src/src/platform/adapters/fs/veryfront/schemas/index.ts +1 -1
- package/src/src/platform/adapters/fs/veryfront/schemas/proxy-manager.schema.ts +14 -11
- package/src/src/platform/adapters/fs/veryfront/stat-operations.ts +1 -0
- package/src/src/platform/adapters/veryfront-api-client/index.ts +17 -16
- package/src/src/platform/adapters/veryfront-api-client/operations.ts +20 -31
- package/src/src/platform/adapters/veryfront-api-client/schemas/api.schema.ts +231 -150
- package/src/src/platform/adapters/veryfront-api-client/schemas/index.ts +17 -16
- package/src/src/prompt/schemas/prompt.schema.ts +13 -10
- package/src/src/proxy/handler.ts +0 -1
- package/src/src/rendering/orchestrator/html-project-css.ts +1 -1
- package/src/src/rendering/renderer.ts +2 -2
- package/src/src/repositories/schemas/repository.schema.ts +36 -22
- package/src/src/resource/index.ts +6 -2
- package/src/src/resource/schemas/index.ts +2 -0
- package/src/src/resource/schemas/resource.schema.ts +17 -8
- package/src/src/resource/types.ts +3 -3
- package/src/src/routing/api/openapi/types.ts +5 -5
- package/src/src/sandbox/hosted-tools.ts +17 -13
- package/src/src/schemas/common.ts +83 -31
- package/src/src/schemas/define.ts +13 -6
- package/src/src/schemas/index.ts +29 -17
- package/src/src/schemas/json-schema.ts +33 -0
- package/src/src/schemas/primitives.ts +71 -35
- package/src/src/security/http/config.ts +1 -1
- package/src/src/security/input-validation/handler.ts +3 -3
- package/src/src/security/input-validation/parsers.ts +40 -45
- package/src/src/server/bootstrap.ts +3 -3
- package/src/src/server/dev-ui/manifest.js +1 -1
- package/src/src/server/handlers/dev/framework-candidates.generated.ts +1 -0
- package/src/src/server/handlers/request/agent-run-cancel.handler.ts +2 -6
- package/src/src/server/handlers/request/agent-run-resume.handler.ts +4 -9
- package/src/src/server/handlers/request/agent-stream.handler.ts +2 -4
- package/src/src/server/handlers/request/internal-agents-list.handler.ts +2 -5
- package/src/src/server/runtime-handler/environment-resolution.ts +4 -5
- package/src/src/server/runtime-handler/request-utils.ts +1 -2
- package/src/src/server/schemas/action.schema.ts +12 -6
- package/src/src/skill/tools.ts +37 -25
- package/src/src/tool/factory.ts +23 -5
- package/src/src/tool/host-tools.ts +16 -6
- package/src/src/tool/index.ts +9 -5
- package/src/src/tool/remote-source-tools.ts +10 -2
- package/src/src/tool/schema/json-schema.ts +9 -15
- package/src/src/tool/schema/zod-json-schema.ts +75 -209
- package/src/src/tool/sleep.ts +55 -7
- package/src/src/tool/types.ts +12 -8
- package/src/src/transforms/npm-import-rewrites.ts +1 -3
- package/src/src/utils/version-constant.ts +1 -1
- package/src/src/workflow/blob/veryfront-cloud-storage.ts +53 -39
- package/src/src/workflow/claude-code/tool.ts +29 -38
- package/src/src/workflow/dsl/workflow.ts +3 -3
- package/src/src/workflow/schemas/index.ts +17 -0
- package/src/src/workflow/schemas/workflow.schema.ts +183 -123
- package/src/src/workflow/types.ts +3 -3
- package/esm/src/schemas/zod-adapter.d.ts +0 -25
- package/esm/src/schemas/zod-adapter.d.ts.map +0 -1
- package/esm/src/schemas/zod-adapter.js +0 -120
- package/src/src/schemas/zod-adapter.ts +0 -180
|
@@ -4,7 +4,7 @@ export default {
|
|
|
4
4
|
"multi-agent-system": {
|
|
5
5
|
"files": {
|
|
6
6
|
"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",
|
|
7
|
-
"tools/web-search.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
7
|
+
"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",
|
|
8
8
|
"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",
|
|
9
9
|
"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",
|
|
10
10
|
"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",
|
|
@@ -18,7 +18,7 @@ export default {
|
|
|
18
18
|
"ai-agent": {
|
|
19
19
|
"files": {
|
|
20
20
|
"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",
|
|
21
|
-
"tools/calculator.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
21
|
+
"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",
|
|
22
22
|
"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",
|
|
23
23
|
"globals.css": "@import \"tailwindcss\";\n",
|
|
24
24
|
"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",
|
|
@@ -30,9 +30,9 @@ export default {
|
|
|
30
30
|
"coding-agent": {
|
|
31
31
|
"files": {
|
|
32
32
|
"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",
|
|
33
|
-
"tools/edit-file.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
34
|
-
"tools/read-file.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
35
|
-
"tools/list-files.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
33
|
+
"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",
|
|
34
|
+
"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",
|
|
35
|
+
"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",
|
|
36
36
|
"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",
|
|
37
37
|
"globals.css": "@import \"tailwindcss\";\n",
|
|
38
38
|
"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",
|
|
@@ -65,7 +65,7 @@ export default {
|
|
|
65
65
|
"saas-starter": {
|
|
66
66
|
"files": {
|
|
67
67
|
"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",
|
|
68
|
-
"tools/search.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
68
|
+
"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",
|
|
69
69
|
"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",
|
|
70
70
|
"globals.css": "@import \"tailwindcss\";\n",
|
|
71
71
|
"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",
|
|
@@ -94,11 +94,11 @@ export default {
|
|
|
94
94
|
},
|
|
95
95
|
"integration:neon": {
|
|
96
96
|
"files": {
|
|
97
|
-
"tools/list-tables.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
98
|
-
"tools/describe-table.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
99
|
-
"tools/query-database.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
100
|
-
"tools/list-projects.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
101
|
-
"tools/list-branches.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
97
|
+
"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",
|
|
98
|
+
"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",
|
|
99
|
+
"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",
|
|
100
|
+
"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",
|
|
101
|
+
"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",
|
|
102
102
|
".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",
|
|
103
103
|
"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",
|
|
104
104
|
"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"
|
|
@@ -106,11 +106,11 @@ export default {
|
|
|
106
106
|
},
|
|
107
107
|
"integration:gitlab": {
|
|
108
108
|
"files": {
|
|
109
|
-
"tools/list-merge-requests.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
110
|
-
"tools/create-issue.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
111
|
-
"tools/list-projects.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
112
|
-
"tools/get-issue.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
113
|
-
"tools/search-issues.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
109
|
+
"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",
|
|
110
|
+
"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",
|
|
111
|
+
"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",
|
|
112
|
+
"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",
|
|
113
|
+
"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",
|
|
114
114
|
".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",
|
|
115
115
|
"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",
|
|
116
116
|
"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",
|
|
@@ -119,22 +119,22 @@ export default {
|
|
|
119
119
|
},
|
|
120
120
|
"integration:anthropic": {
|
|
121
121
|
"files": {
|
|
122
|
-
"tools/list-workspaces.ts": "import { tool } from 'veryfront/tool';\nimport {
|
|
123
|
-
"tools/list-api-keys.ts": "import { tool } from 'veryfront/tool';\nimport {
|
|
124
|
-
"tools/get-usage.ts": "import { tool } from 'veryfront/tool';\nimport {
|
|
125
|
-
"tools/list-members.ts": "import { tool } from 'veryfront/tool';\nimport {
|
|
126
|
-
"tools/get-organization.ts": "import { tool } from 'veryfront/tool';\nimport {
|
|
122
|
+
"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",
|
|
123
|
+
"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",
|
|
124
|
+
"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",
|
|
125
|
+
"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",
|
|
126
|
+
"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",
|
|
127
127
|
".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",
|
|
128
128
|
"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"
|
|
129
129
|
}
|
|
130
130
|
},
|
|
131
131
|
"integration:discord": {
|
|
132
132
|
"files": {
|
|
133
|
-
"tools/get-messages.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
134
|
-
"tools/list-guilds.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
135
|
-
"tools/get-user.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
136
|
-
"tools/list-channels.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
137
|
-
"tools/send-message.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
133
|
+
"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",
|
|
134
|
+
"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",
|
|
135
|
+
"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",
|
|
136
|
+
"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",
|
|
137
|
+
"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",
|
|
138
138
|
".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",
|
|
139
139
|
"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",
|
|
140
140
|
"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",
|
|
@@ -143,11 +143,11 @@ export default {
|
|
|
143
143
|
},
|
|
144
144
|
"integration:snowflake": {
|
|
145
145
|
"files": {
|
|
146
|
-
"tools/list-databases.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
147
|
-
"tools/list-schemas.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
148
|
-
"tools/list-tables.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
149
|
-
"tools/describe-table.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
150
|
-
"tools/run-query.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
146
|
+
"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",
|
|
147
|
+
"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",
|
|
148
|
+
"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",
|
|
149
|
+
"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",
|
|
150
|
+
"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",
|
|
151
151
|
".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",
|
|
152
152
|
"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"
|
|
153
153
|
}
|
|
@@ -169,9 +169,9 @@ export default {
|
|
|
169
169
|
},
|
|
170
170
|
"integration:calendar": {
|
|
171
171
|
"files": {
|
|
172
|
-
"tools/find-free-time.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
173
|
-
"tools/create-event.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
174
|
-
"tools/list-events.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
172
|
+
"tools/find-free-time.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createCalendarClient } from \"../../lib/calendar-client.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 // Default to \"current-user\" for development; in production, always pass userId from session\n const userId = context?.userId ?? \"current-user\";\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",
|
|
173
|
+
"tools/create-event.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createCalendarClient } from \"../../lib/calendar-client.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 = context?.userId ?? \"current-user\";\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",
|
|
174
|
+
"tools/list-events.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createCalendarClient } from \"../../lib/calendar-client.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 = context?.userId ?? \"current-user\";\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",
|
|
175
175
|
".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",
|
|
176
176
|
"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",
|
|
177
177
|
"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",
|
|
@@ -180,11 +180,11 @@ export default {
|
|
|
180
180
|
},
|
|
181
181
|
"integration:hubspot": {
|
|
182
182
|
"files": {
|
|
183
|
-
"tools/create-contact.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
184
|
-
"tools/list-contacts.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
185
|
-
"tools/list-deals.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
186
|
-
"tools/create-deal.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
187
|
-
"tools/get-contact.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
183
|
+
"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",
|
|
184
|
+
"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",
|
|
185
|
+
"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",
|
|
186
|
+
"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",
|
|
187
|
+
"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",
|
|
188
188
|
".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",
|
|
189
189
|
"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",
|
|
190
190
|
"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",
|
|
@@ -193,11 +193,11 @@ export default {
|
|
|
193
193
|
},
|
|
194
194
|
"integration:shopify": {
|
|
195
195
|
"files": {
|
|
196
|
-
"tools/list-products.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
197
|
-
"tools/list-orders.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
198
|
-
"tools/get-order.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
199
|
-
"tools/list-customers.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
200
|
-
"tools/get-product.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
196
|
+
"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",
|
|
197
|
+
"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",
|
|
198
|
+
"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",
|
|
199
|
+
"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",
|
|
200
|
+
"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",
|
|
201
201
|
".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",
|
|
202
202
|
"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",
|
|
203
203
|
"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",
|
|
@@ -206,21 +206,21 @@ export default {
|
|
|
206
206
|
},
|
|
207
207
|
"integration:aws": {
|
|
208
208
|
"files": {
|
|
209
|
-
"tools/list-ec2-instances.ts": "import { tool } from 'veryfront/tool';\nimport {
|
|
210
|
-
"tools/list-s3-objects.ts": "import { tool } from 'veryfront/tool';\nimport {
|
|
211
|
-
"tools/list-lambda-functions.ts": "import { tool } from 'veryfront/tool';\nimport {
|
|
212
|
-
"tools/list-s3-buckets.ts": "import { tool } from 'veryfront/tool';\nimport {
|
|
213
|
-
"tools/get-s3-object.ts": "import { tool } from 'veryfront/tool';\nimport {
|
|
209
|
+
"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",
|
|
210
|
+
"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",
|
|
211
|
+
"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",
|
|
212
|
+
"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",
|
|
213
|
+
"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",
|
|
214
214
|
".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",
|
|
215
215
|
"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"
|
|
216
216
|
}
|
|
217
217
|
},
|
|
218
218
|
"integration:notion": {
|
|
219
219
|
"files": {
|
|
220
|
-
"tools/query-database.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
221
|
-
"tools/read-page.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
222
|
-
"tools/create-page.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
223
|
-
"tools/search-notion.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
220
|
+
"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",
|
|
221
|
+
"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",
|
|
222
|
+
"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",
|
|
223
|
+
"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",
|
|
224
224
|
".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",
|
|
225
225
|
"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",
|
|
226
226
|
"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",
|
|
@@ -229,10 +229,10 @@ export default {
|
|
|
229
229
|
},
|
|
230
230
|
"integration:github": {
|
|
231
231
|
"files": {
|
|
232
|
-
"tools/create-issue.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
233
|
-
"tools/get-pr-diff.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
234
|
-
"tools/list-repos.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
235
|
-
"tools/list-prs.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
232
|
+
"tools/create-issue.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGitHubClient } from \"../../lib/github-client.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 = context?.userId ?? \"current-user\";\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",
|
|
233
|
+
"tools/get-pr-diff.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGitHubClient } from \"../../lib/github-client.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 // Default to \"current-user\" for development; in production, always pass userId from session\n const userId = context?.userId ?? \"current-user\";\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",
|
|
234
|
+
"tools/list-repos.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGitHubClient } from \"../../lib/github-client.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 // Default to \"current-user\" for development; in production, always pass userId from session\n const userId = context?.userId ?? \"current-user\";\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",
|
|
235
|
+
"tools/list-prs.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createGitHubClient } from \"../../lib/github-client.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 = context?.userId ?? \"current-user\";\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",
|
|
236
236
|
"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",
|
|
237
237
|
"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",
|
|
238
238
|
"app/api/auth/github/route.ts": "import { createOAuthInitHandler, githubConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT).\n// NEVER return a shared constant in production - it breaks per-user token isolation (VULN-AUTH-2).\nfunction getUserId(_request: Request): string {\n return \"current-user\";\n}\n\nexport const GET = createOAuthInitHandler(githubConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});"
|
|
@@ -240,11 +240,11 @@ export default {
|
|
|
240
240
|
},
|
|
241
241
|
"integration:stripe": {
|
|
242
242
|
"files": {
|
|
243
|
-
"tools/list-subscriptions.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
244
|
-
"tools/list-payments.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
245
|
-
"tools/get-balance.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
246
|
-
"tools/list-customers.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
247
|
-
"tools/get-customer.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
243
|
+
"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",
|
|
244
|
+
"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",
|
|
245
|
+
"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",
|
|
246
|
+
"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",
|
|
247
|
+
"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",
|
|
248
248
|
".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",
|
|
249
249
|
"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",
|
|
250
250
|
"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"
|
|
@@ -252,11 +252,11 @@ export default {
|
|
|
252
252
|
},
|
|
253
253
|
"integration:figma": {
|
|
254
254
|
"files": {
|
|
255
|
-
"tools/get-file.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
256
|
-
"tools/get-comments.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
257
|
-
"tools/list-projects.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
258
|
-
"tools/post-comment.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
259
|
-
"tools/list-files.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
255
|
+
"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",
|
|
256
|
+
"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",
|
|
257
|
+
"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",
|
|
258
|
+
"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",
|
|
259
|
+
"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",
|
|
260
260
|
".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",
|
|
261
261
|
"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",
|
|
262
262
|
"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",
|
|
@@ -266,11 +266,11 @@ export default {
|
|
|
266
266
|
},
|
|
267
267
|
"integration:linear": {
|
|
268
268
|
"files": {
|
|
269
|
-
"tools/create-issue.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
270
|
-
"tools/update-issue.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
271
|
-
"tools/list-projects.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
272
|
-
"tools/get-issue.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
273
|
-
"tools/search-issues.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
269
|
+
"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",
|
|
270
|
+
"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",
|
|
271
|
+
"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",
|
|
272
|
+
"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",
|
|
273
|
+
"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",
|
|
274
274
|
".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",
|
|
275
275
|
"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",
|
|
276
276
|
"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",
|
|
@@ -279,10 +279,10 @@ export default {
|
|
|
279
279
|
},
|
|
280
280
|
"integration:onedrive": {
|
|
281
281
|
"files": {
|
|
282
|
-
"tools/upload-file.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
283
|
-
"tools/search-files.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
284
|
-
"tools/download-file.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
285
|
-
"tools/list-files.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
282
|
+
"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",
|
|
283
|
+
"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",
|
|
284
|
+
"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",
|
|
285
|
+
"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",
|
|
286
286
|
".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",
|
|
287
287
|
"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",
|
|
288
288
|
"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",
|
|
@@ -291,11 +291,11 @@ export default {
|
|
|
291
291
|
},
|
|
292
292
|
"integration:asana": {
|
|
293
293
|
"files": {
|
|
294
|
-
"tools/list-tasks.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
295
|
-
"tools/get-task.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
296
|
-
"tools/list-projects.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
297
|
-
"tools/update-task.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
298
|
-
"tools/create-task.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
294
|
+
"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",
|
|
295
|
+
"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",
|
|
296
|
+
"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",
|
|
297
|
+
"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",
|
|
298
|
+
"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",
|
|
299
299
|
".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",
|
|
300
300
|
"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",
|
|
301
301
|
"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",
|
|
@@ -304,31 +304,31 @@ export default {
|
|
|
304
304
|
},
|
|
305
305
|
"integration:posthog": {
|
|
306
306
|
"files": {
|
|
307
|
-
"tools/get-trends.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
308
|
-
"tools/capture-event.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
309
|
-
"tools/list-feature-flags.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
310
|
-
"tools/list-persons.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
307
|
+
"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",
|
|
308
|
+
"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",
|
|
309
|
+
"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",
|
|
310
|
+
"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",
|
|
311
311
|
".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",
|
|
312
312
|
"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"
|
|
313
313
|
}
|
|
314
314
|
},
|
|
315
315
|
"integration:sentry": {
|
|
316
316
|
"files": {
|
|
317
|
-
"tools/resolve-issue.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
318
|
-
"tools/list-projects.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
319
|
-
"tools/list-issues.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
320
|
-
"tools/get-issue.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
317
|
+
"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",
|
|
318
|
+
"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",
|
|
319
|
+
"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",
|
|
320
|
+
"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",
|
|
321
321
|
".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",
|
|
322
322
|
"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"
|
|
323
323
|
}
|
|
324
324
|
},
|
|
325
325
|
"integration:drive": {
|
|
326
326
|
"files": {
|
|
327
|
-
"tools/create-folder.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
328
|
-
"tools/get-file.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
329
|
-
"tools/upload-file.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
330
|
-
"tools/search-files.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
331
|
-
"tools/list-files.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
327
|
+
"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",
|
|
328
|
+
"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",
|
|
329
|
+
"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",
|
|
330
|
+
"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",
|
|
331
|
+
"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",
|
|
332
332
|
".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",
|
|
333
333
|
"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",
|
|
334
334
|
"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",
|
|
@@ -338,10 +338,10 @@ export default {
|
|
|
338
338
|
},
|
|
339
339
|
"integration:bitbucket": {
|
|
340
340
|
"files": {
|
|
341
|
-
"tools/list-pull-requests.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
342
|
-
"tools/list-issues.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
343
|
-
"tools/list-repositories.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
344
|
-
"tools/create-pull-request.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
341
|
+
"tools/list-pull-requests.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createBitbucketClient } from \"../../lib/bitbucket-client.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 = context?.userId ?? \"current-user\";\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",
|
|
342
|
+
"tools/list-issues.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createBitbucketClient } from \"../../lib/bitbucket-client.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 = context?.userId ?? \"current-user\";\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",
|
|
343
|
+
"tools/list-repositories.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createBitbucketClient } from \"../../lib/bitbucket-client.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 = context?.userId ?? \"current-user\";\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",
|
|
344
|
+
"tools/create-pull-request.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createBitbucketClient } from \"../../lib/bitbucket-client.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 = context?.userId ?? \"current-user\";\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",
|
|
345
345
|
".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",
|
|
346
346
|
"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",
|
|
347
347
|
"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",
|
|
@@ -350,11 +350,11 @@ export default {
|
|
|
350
350
|
},
|
|
351
351
|
"integration:servicenow": {
|
|
352
352
|
"files": {
|
|
353
|
-
"tools/create-incident.ts": "import {
|
|
354
|
-
"tools/get-incident.ts": "/**\n * Get ServiceNow Incident Tool\n */\n\nimport {
|
|
355
|
-
"tools/list-incidents.ts": "import {
|
|
356
|
-
"tools/search-knowledge.ts": "import {
|
|
357
|
-
"tools/update-incident.ts": "import {
|
|
353
|
+
"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",
|
|
354
|
+
"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",
|
|
355
|
+
"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",
|
|
356
|
+
"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",
|
|
357
|
+
"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",
|
|
358
358
|
".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",
|
|
359
359
|
"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",
|
|
360
360
|
"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",
|
|
@@ -363,11 +363,11 @@ export default {
|
|
|
363
363
|
},
|
|
364
364
|
"integration:sharepoint": {
|
|
365
365
|
"files": {
|
|
366
|
-
"tools/get-site.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
367
|
-
"tools/get-file.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
368
|
-
"tools/list-sites.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
369
|
-
"tools/upload-file.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
370
|
-
"tools/list-files.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
366
|
+
"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",
|
|
367
|
+
"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",
|
|
368
|
+
"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",
|
|
369
|
+
"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",
|
|
370
|
+
"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",
|
|
371
371
|
"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",
|
|
372
372
|
"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",
|
|
373
373
|
"app/api/auth/sharepoint/route.ts": "import { createOAuthInitHandler, sharePointConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT).\n// NEVER return a shared constant in production - it breaks per-user token isolation (VULN-AUTH-2).\nfunction getUserId(_request: Request): string {\n return \"current-user\";\n}\n\nexport const GET = createOAuthInitHandler(sharePointConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});"
|
|
@@ -375,41 +375,41 @@ export default {
|
|
|
375
375
|
},
|
|
376
376
|
"integration:gmail": {
|
|
377
377
|
"files": {
|
|
378
|
-
"tools/create-draft.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
379
|
-
"tools/get-email.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
380
|
-
"tools/trash-thread.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
381
|
-
"tools/delete-label.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
382
|
-
"tools/get-profile.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
383
|
-
"tools/archive-email.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
384
|
-
"tools/apply-labels.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
385
|
-
"tools/send-draft.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
386
|
-
"tools/create-label.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
387
|
-
"tools/list-emails.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
388
|
-
"tools/delete-draft.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
389
|
-
"tools/list-history.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
390
|
-
"tools/stop-mailbox-watch.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
391
|
-
"tools/search-emails.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
392
|
-
"tools/list-drafts.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
393
|
-
"tools/batch-modify-emails.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
394
|
-
"tools/modify-email-labels.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
395
|
-
"tools/trash-email.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
396
|
-
"tools/get-thread.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
397
|
-
"tools/update-label.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
398
|
-
"tools/watch-mailbox.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
399
|
-
"tools/batch-delete-emails.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
400
|
-
"tools/get-label.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
401
|
-
"tools/modify-thread-labels.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
402
|
-
"tools/send-email.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
403
|
-
"tools/update-draft.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
404
|
-
"tools/untrash-email.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
405
|
-
"tools/get-draft.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
406
|
-
"tools/get-attachment.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
407
|
-
"tools/delete-email.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
408
|
-
"tools/untrash-thread.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
409
|
-
"tools/list-labels.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
410
|
-
"tools/delete-thread.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
411
|
-
"tools/mark-email-read.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
412
|
-
"tools/list-threads.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
378
|
+
"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",
|
|
379
|
+
"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",
|
|
380
|
+
"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",
|
|
381
|
+
"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",
|
|
382
|
+
"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",
|
|
383
|
+
"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",
|
|
384
|
+
"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",
|
|
385
|
+
"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",
|
|
386
|
+
"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",
|
|
387
|
+
"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",
|
|
388
|
+
"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",
|
|
389
|
+
"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",
|
|
390
|
+
"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",
|
|
391
|
+
"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",
|
|
392
|
+
"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",
|
|
393
|
+
"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",
|
|
394
|
+
"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",
|
|
395
|
+
"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",
|
|
396
|
+
"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",
|
|
397
|
+
"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",
|
|
398
|
+
"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",
|
|
399
|
+
"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",
|
|
400
|
+
"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",
|
|
401
|
+
"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",
|
|
402
|
+
"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",
|
|
403
|
+
"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",
|
|
404
|
+
"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",
|
|
405
|
+
"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",
|
|
406
|
+
"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",
|
|
407
|
+
"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",
|
|
408
|
+
"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",
|
|
409
|
+
"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",
|
|
410
|
+
"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",
|
|
411
|
+
"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",
|
|
412
|
+
"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",
|
|
413
413
|
".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",
|
|
414
414
|
"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 \"current-user\" constant\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",
|
|
415
415
|
"lib/context.ts": "import type { ToolExecutionContext } from \"veryfront/tool\";\n\nexport function resolveUserId(context?: ToolExecutionContext): string {\n if (typeof context?.endUserId === \"string\" && context.endUserId.length > 0) {\n return context.endUserId;\n }\n\n if (typeof context?.userId === \"string\" && context.userId.length > 0) {\n return context.userId;\n }\n\n return \"current-user\";\n}\n",
|
|
@@ -419,11 +419,11 @@ export default {
|
|
|
419
419
|
},
|
|
420
420
|
"integration:sheets": {
|
|
421
421
|
"files": {
|
|
422
|
-
"tools/list-spreadsheets.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
423
|
-
"tools/get-spreadsheet.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
424
|
-
"tools/create-spreadsheet.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
425
|
-
"tools/read-range.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
426
|
-
"tools/write-range.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
422
|
+
"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",
|
|
423
|
+
"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",
|
|
424
|
+
"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",
|
|
425
|
+
"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",
|
|
426
|
+
"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",
|
|
427
427
|
".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",
|
|
428
428
|
"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",
|
|
429
429
|
"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",
|
|
@@ -432,11 +432,11 @@ export default {
|
|
|
432
432
|
},
|
|
433
433
|
"integration:salesforce": {
|
|
434
434
|
"files": {
|
|
435
|
-
"tools/create-lead.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
436
|
-
"tools/list-accounts.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
437
|
-
"tools/list-opportunities.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
438
|
-
"tools/get-account.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
439
|
-
"tools/list-contacts.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
435
|
+
"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",
|
|
436
|
+
"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",
|
|
437
|
+
"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",
|
|
438
|
+
"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",
|
|
439
|
+
"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",
|
|
440
440
|
"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",
|
|
441
441
|
"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",
|
|
442
442
|
"app/api/auth/salesforce/route.ts": "import { createOAuthInitHandler, salesforceConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT).\n// NEVER return a shared constant in production - it breaks per-user token isolation (VULN-AUTH-2).\nfunction getUserId(_request: Request): string {\n return \"current-user\";\n}\n\nexport const GET = createOAuthInitHandler(salesforceConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});"
|
|
@@ -444,11 +444,11 @@ export default {
|
|
|
444
444
|
},
|
|
445
445
|
"integration:confluence": {
|
|
446
446
|
"files": {
|
|
447
|
-
"tools/search-content.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
448
|
-
"tools/create-page.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
449
|
-
"tools/list-spaces.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
450
|
-
"tools/update-page.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
451
|
-
"tools/get-page.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
447
|
+
"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",
|
|
448
|
+
"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",
|
|
449
|
+
"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",
|
|
450
|
+
"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",
|
|
451
|
+
"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",
|
|
452
452
|
".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",
|
|
453
453
|
"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",
|
|
454
454
|
"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",
|
|
@@ -457,11 +457,11 @@ export default {
|
|
|
457
457
|
},
|
|
458
458
|
"integration:jira": {
|
|
459
459
|
"files": {
|
|
460
|
-
"tools/create-issue.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
461
|
-
"tools/update-issue.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
462
|
-
"tools/list-projects.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
463
|
-
"tools/get-issue.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
464
|
-
"tools/search-issues.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
460
|
+
"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",
|
|
461
|
+
"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",
|
|
462
|
+
"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",
|
|
463
|
+
"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",
|
|
464
|
+
"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",
|
|
465
465
|
"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",
|
|
466
466
|
"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",
|
|
467
467
|
"app/api/auth/jira/route.ts": "import { createOAuthInitHandler, jiraConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT).\n// NEVER return a shared constant in production - it breaks per-user token isolation (VULN-AUTH-2).\nfunction getUserId(_request: Request): string {\n return \"current-user\";\n}\n\nexport const GET = createOAuthInitHandler(jiraConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});"
|
|
@@ -469,11 +469,11 @@ export default {
|
|
|
469
469
|
},
|
|
470
470
|
"integration:trello": {
|
|
471
471
|
"files": {
|
|
472
|
-
"tools/update-card.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
473
|
-
"tools/list-cards.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
474
|
-
"tools/get-card.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
475
|
-
"tools/list-boards.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
476
|
-
"tools/create-card.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
472
|
+
"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",
|
|
473
|
+
"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",
|
|
474
|
+
"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",
|
|
475
|
+
"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",
|
|
476
|
+
"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",
|
|
477
477
|
".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",
|
|
478
478
|
"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",
|
|
479
479
|
"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",
|
|
@@ -482,22 +482,22 @@ export default {
|
|
|
482
482
|
},
|
|
483
483
|
"integration:mixpanel": {
|
|
484
484
|
"files": {
|
|
485
|
-
"tools/get-funnel.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
486
|
-
"tools/get-retention.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
487
|
-
"tools/list-cohorts.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
488
|
-
"tools/track-event.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
489
|
-
"tools/query-events.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
485
|
+
"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",
|
|
486
|
+
"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",
|
|
487
|
+
"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",
|
|
488
|
+
"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",
|
|
489
|
+
"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",
|
|
490
490
|
".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",
|
|
491
491
|
"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"
|
|
492
492
|
}
|
|
493
493
|
},
|
|
494
494
|
"integration:airtable": {
|
|
495
495
|
"files": {
|
|
496
|
-
"tools/get-record.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
497
|
-
"tools/list-bases.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
498
|
-
"tools/create-record.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
499
|
-
"tools/get-base.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
500
|
-
"tools/list-records.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
496
|
+
"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",
|
|
497
|
+
"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",
|
|
498
|
+
"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",
|
|
499
|
+
"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",
|
|
500
|
+
"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",
|
|
501
501
|
"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",
|
|
502
502
|
"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",
|
|
503
503
|
"app/api/auth/airtable/route.ts": "import { airtableConfig, createOAuthInitHandler } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT).\n// NEVER return a shared constant in production - it breaks per-user token isolation (VULN-AUTH-2).\nfunction getUserId(_request: Request): string {\n return \"current-user\";\n}\n\nexport const GET = createOAuthInitHandler(airtableConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});"
|
|
@@ -505,11 +505,11 @@ export default {
|
|
|
505
505
|
},
|
|
506
506
|
"integration:outlook": {
|
|
507
507
|
"files": {
|
|
508
|
-
"tools/get-email.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
509
|
-
"tools/list-emails.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
510
|
-
"tools/list-folders.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
511
|
-
"tools/search-emails.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
512
|
-
"tools/send-email.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
508
|
+
"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",
|
|
509
|
+
"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",
|
|
510
|
+
"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",
|
|
511
|
+
"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",
|
|
512
|
+
"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",
|
|
513
513
|
".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",
|
|
514
514
|
"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",
|
|
515
515
|
"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",
|
|
@@ -518,11 +518,11 @@ export default {
|
|
|
518
518
|
},
|
|
519
519
|
"integration:dropbox": {
|
|
520
520
|
"files": {
|
|
521
|
-
"tools/get-file.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
522
|
-
"tools/upload-file.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
523
|
-
"tools/get-account.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
524
|
-
"tools/search-files.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
525
|
-
"tools/list-files.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
521
|
+
"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",
|
|
522
|
+
"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",
|
|
523
|
+
"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",
|
|
524
|
+
"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",
|
|
525
|
+
"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",
|
|
526
526
|
".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",
|
|
527
527
|
"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",
|
|
528
528
|
"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",
|
|
@@ -531,9 +531,9 @@ export default {
|
|
|
531
531
|
},
|
|
532
532
|
"integration:slack": {
|
|
533
533
|
"files": {
|
|
534
|
-
"tools/get-messages.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
535
|
-
"tools/list-channels.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
536
|
-
"tools/send-message.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
534
|
+
"tools/get-messages.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createSlackClient } from \"../../lib/slack-client.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 = context?.userId ?? \"current-user\";\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",
|
|
535
|
+
"tools/list-channels.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createSlackClient } from \"../../lib/slack-client.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 = context?.userId ?? \"current-user\";\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",
|
|
536
|
+
"tools/send-message.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { createSlackClient } from \"../../lib/slack-client.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 = context?.userId ?? \"current-user\";\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",
|
|
537
537
|
"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",
|
|
538
538
|
"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",
|
|
539
539
|
"app/api/auth/slack/route.ts": "import { createOAuthInitHandler, slackConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT).\n// NEVER return a shared constant in production - it breaks per-user token isolation (VULN-AUTH-2).\nfunction getUserId(_request: Request): string {\n return \"current-user\";\n}\n\nexport const GET = createOAuthInitHandler(slackConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});"
|
|
@@ -541,22 +541,22 @@ export default {
|
|
|
541
541
|
},
|
|
542
542
|
"integration:twilio": {
|
|
543
543
|
"files": {
|
|
544
|
-
"tools/send-sms.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
545
|
-
"tools/get-message.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
546
|
-
"tools/list-calls.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
547
|
-
"tools/send-whatsapp.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
548
|
-
"tools/list-messages.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
544
|
+
"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",
|
|
545
|
+
"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",
|
|
546
|
+
"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",
|
|
547
|
+
"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",
|
|
548
|
+
"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",
|
|
549
549
|
".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",
|
|
550
550
|
"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"
|
|
551
551
|
}
|
|
552
552
|
},
|
|
553
553
|
"integration:teams": {
|
|
554
554
|
"files": {
|
|
555
|
-
"tools/get-messages.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
556
|
-
"tools/list-chats.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
557
|
-
"tools/list-channels.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
558
|
-
"tools/send-message.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
559
|
-
"tools/list-teams.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
555
|
+
"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",
|
|
556
|
+
"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",
|
|
557
|
+
"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",
|
|
558
|
+
"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",
|
|
559
|
+
"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",
|
|
560
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
561
|
"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",
|
|
562
562
|
"app/api/auth/teams/route.ts": "import { createOAuthInitHandler, teamsConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT).\n// NEVER return a shared constant in production - it breaks per-user token isolation (VULN-AUTH-2).\nfunction getUserId(_request: Request): string {\n return \"current-user\";\n}\n\nexport const GET = createOAuthInitHandler(teamsConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});"
|
|
@@ -564,11 +564,11 @@ export default {
|
|
|
564
564
|
},
|
|
565
565
|
"integration:supabase": {
|
|
566
566
|
"files": {
|
|
567
|
-
"tools/list-tables.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
568
|
-
"tools/delete-row.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
569
|
-
"tools/insert-row.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
570
|
-
"tools/query-table.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
571
|
-
"tools/update-row.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
567
|
+
"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",
|
|
568
|
+
"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",
|
|
569
|
+
"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",
|
|
570
|
+
"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",
|
|
571
|
+
"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",
|
|
572
572
|
".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",
|
|
573
573
|
"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",
|
|
574
574
|
"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"
|
|
@@ -576,11 +576,11 @@ export default {
|
|
|
576
576
|
},
|
|
577
577
|
"integration:docs-google": {
|
|
578
578
|
"files": {
|
|
579
|
-
"tools/search-documents.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
580
|
-
"tools/update-document.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
581
|
-
"tools/list-documents.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
582
|
-
"tools/create-document.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
583
|
-
"tools/get-document.ts": "import { tool } from \"veryfront/tool\";\nimport {
|
|
579
|
+
"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",
|
|
580
|
+
"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",
|
|
581
|
+
"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",
|
|
582
|
+
"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",
|
|
583
|
+
"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",
|
|
584
584
|
".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",
|
|
585
585
|
"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",
|
|
586
586
|
"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",
|