@alumbwe/anvil 1.0.20 → 1.0.21
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/package.json +6 -2
- package/src/agents/bundled-agents.generated.ts +4 -4
- package/src/chat.tsx +1703 -1698
- package/src/cli-args.ts +134 -134
- package/src/components/status-bar.tsx +331 -313
- package/src/hooks/helpers/send-message.ts +663 -654
- package/src/hooks/use-send-message.ts +806 -773
- package/src/index.tsx +473 -473
- package/src/state/chat-store.ts +553 -544
- package/src/utils/create-run-config.ts +121 -121
- package/src/utils/format-compact-number.ts +37 -0
- package/vendor/@anvil/common/package.json +27 -0
- package/vendor/@anvil/common/src/__tests__/agent-validation.test.ts +846 -0
- package/vendor/@anvil/common/src/__tests__/anvil-model-availability.test.ts +101 -0
- package/vendor/@anvil/common/src/__tests__/anvil-models.test.ts +1408 -0
- package/vendor/@anvil/common/src/__tests__/anvil-public-data-use-copy.test.ts +258 -0
- package/vendor/@anvil/common/src/__tests__/anvil-referral-tiers.test.ts +48 -0
- package/vendor/@anvil/common/src/__tests__/anvil-spend-ceilings.test.ts +282 -0
- package/vendor/@anvil/common/src/__tests__/anvil-trust.test.ts +526 -0
- package/vendor/@anvil/common/src/__tests__/deepseek-direct.test.ts +68 -0
- package/vendor/@anvil/common/src/__tests__/disposable-email.test.ts +132 -0
- package/vendor/@anvil/common/src/__tests__/dynamic-agent-template-schema.test.ts +422 -0
- package/vendor/@anvil/common/src/__tests__/env-ci.test.ts +167 -0
- package/vendor/@anvil/common/src/__tests__/env-process.test.ts +145 -0
- package/vendor/@anvil/common/src/__tests__/foreign-client-shipped-agents.test.ts +234 -0
- package/vendor/@anvil/common/src/__tests__/foreign-client-signals.test.ts +379 -0
- package/vendor/@anvil/common/src/__tests__/free-agents.test.ts +807 -0
- package/vendor/@anvil/common/src/__tests__/gravity-capi.test.ts +154 -0
- package/vendor/@anvil/common/src/__tests__/handlesteps-parsing.test.ts +246 -0
- package/vendor/@anvil/common/src/__tests__/kimi-k3-god-only.test.ts +77 -0
- package/vendor/@anvil/common/src/__tests__/model-config.test.ts +89 -0
- package/vendor/@anvil/common/src/__tests__/project-file-tree.test.ts +298 -0
- package/vendor/@anvil/common/src/__tests__/provisioned-model-tiers.test.ts +116 -0
- package/vendor/@anvil/common/src/__tests__/reasoning-effort.test.ts +167 -0
- package/vendor/@anvil/common/src/__tests__/reddit-capi.test.ts +132 -0
- package/vendor/@anvil/common/src/__tests__/response-ad-positions.test.ts +72 -0
- package/vendor/@anvil/common/src/__tests__/user-state.test.ts +30 -0
- package/vendor/@anvil/common/src/actions.ts +215 -0
- package/vendor/@anvil/common/src/analytics-core.ts +69 -0
- package/vendor/@anvil/common/src/analytics.ts +93 -0
- package/vendor/@anvil/common/src/api-keys/constants.ts +26 -0
- package/vendor/@anvil/common/src/browser-actions.ts +413 -0
- package/vendor/@anvil/common/src/constants/__tests__/anvil-onboarding-gate.test.ts +63 -0
- package/vendor/@anvil/common/src/constants/__tests__/anvil-onboarding.test.ts +246 -0
- package/vendor/@anvil/common/src/constants/__tests__/cf-worker-signals.test.ts +106 -0
- package/vendor/@anvil/common/src/constants/agents.ts +99 -0
- package/vendor/@anvil/common/src/constants/analytics-events.ts +435 -0
- package/vendor/@anvil/common/src/constants/anthropic.ts +73 -0
- package/vendor/@anvil/common/src/constants/anvil-data-use.ts +81 -0
- package/vendor/@anvil/common/src/constants/anvil-errors.ts +7 -0
- package/vendor/@anvil/common/src/constants/anvil-gemini-thinker.ts +21 -0
- package/vendor/@anvil/common/src/constants/anvil-model-ids.ts +11 -0
- package/vendor/@anvil/common/src/constants/anvil-models.ts +2130 -0
- package/vendor/@anvil/common/src/constants/anvil-onboarding-gate.ts +55 -0
- package/vendor/@anvil/common/src/constants/anvil-onboarding.ts +307 -0
- package/vendor/@anvil/common/src/constants/anvil-referral-tiers.ts +87 -0
- package/vendor/@anvil/common/src/constants/anvil-signup-block.ts +68 -0
- package/vendor/@anvil/common/src/constants/anvil-spend-ceilings.ts +450 -0
- package/vendor/@anvil/common/src/constants/anvil-trust.ts +976 -0
- package/vendor/@anvil/common/src/constants/auth.ts +18 -0
- package/vendor/@anvil/common/src/constants/byok.ts +2 -0
- package/vendor/@anvil/common/src/constants/cf-worker-signals.ts +132 -0
- package/vendor/@anvil/common/src/constants/composio.ts +34 -0
- package/vendor/@anvil/common/src/constants/deepseek-direct.ts +55 -0
- package/vendor/@anvil/common/src/constants/feedback.ts +13 -0
- package/vendor/@anvil/common/src/constants/foreign-client-signals.ts +272 -0
- package/vendor/@anvil/common/src/constants/free-agents.ts +777 -0
- package/vendor/@anvil/common/src/constants/gemini.ts +15 -0
- package/vendor/@anvil/common/src/constants/grant-priorities.ts +13 -0
- package/vendor/@anvil/common/src/constants/hosts.ts +6 -0
- package/vendor/@anvil/common/src/constants/images.ts +51 -0
- package/vendor/@anvil/common/src/constants/index.ts +7 -0
- package/vendor/@anvil/common/src/constants/knowledge.ts +35 -0
- package/vendor/@anvil/common/src/constants/limits.ts +23 -0
- package/vendor/@anvil/common/src/constants/model-config.ts +277 -0
- package/vendor/@anvil/common/src/constants/openrouter-attribution.ts +8 -0
- package/vendor/@anvil/common/src/constants/paths.ts +69 -0
- package/vendor/@anvil/common/src/constants/provider-routes.ts +317 -0
- package/vendor/@anvil/common/src/constants/reasoning-effort.ts +79 -0
- package/vendor/@anvil/common/src/constants/skills.ts +60 -0
- package/vendor/@anvil/common/src/constants/subscription-plans.ts +49 -0
- package/vendor/@anvil/common/src/constants/ui.ts +25 -0
- package/vendor/@anvil/common/src/env-ci.ts +36 -0
- package/vendor/@anvil/common/src/env-process.ts +95 -0
- package/vendor/@anvil/common/src/env-schema.ts +95 -0
- package/vendor/@anvil/common/src/env.ts +24 -0
- package/vendor/@anvil/common/src/gravity-capi.ts +207 -0
- package/vendor/@anvil/common/src/mcp/client.ts +233 -0
- package/vendor/@anvil/common/src/old-constants.ts +10 -0
- package/vendor/@anvil/common/src/project-file-tree.ts +355 -0
- package/vendor/@anvil/common/src/reddit-capi.ts +254 -0
- package/vendor/@anvil/common/src/schemas/feedback.ts +52 -0
- package/vendor/@anvil/common/src/schemas/logs.ts +66 -0
- package/vendor/@anvil/common/src/templates/agent-validation.ts +392 -0
- package/vendor/@anvil/common/src/templates/initial-agents-dir/LICENSE +202 -0
- package/vendor/@anvil/common/src/templates/initial-agents-dir/README.md +294 -0
- package/vendor/@anvil/common/src/templates/initial-agents-dir/examples/01-basic-diff-reviewer.ts +17 -0
- package/vendor/@anvil/common/src/templates/initial-agents-dir/examples/02-intermediate-git-committer.ts +78 -0
- package/vendor/@anvil/common/src/templates/initial-agents-dir/examples/03-advanced-file-explorer.ts +73 -0
- package/vendor/@anvil/common/src/templates/initial-agents-dir/my-custom-agent.ts +40 -0
- package/vendor/@anvil/common/src/templates/initial-agents-dir/package.json +6 -0
- package/vendor/@anvil/common/src/templates/initial-agents-dir/skills/README.md +65 -0
- package/vendor/@anvil/common/src/templates/initial-agents-dir/skills/example-skill/SKILL.md +29 -0
- package/vendor/@anvil/common/src/templates/initial-agents-dir/types/agent-definition.ts +497 -0
- package/vendor/@anvil/common/src/templates/initial-agents-dir/types/tools.ts +444 -0
- package/vendor/@anvil/common/src/templates/initial-agents-dir/types/util-types.ts +178 -0
- package/vendor/@anvil/common/src/testing/TESTING_PATTERNS.md +351 -0
- package/vendor/@anvil/common/src/testing/anvil-offer-invariants.ts +169 -0
- package/vendor/@anvil/common/src/testing/errors.ts +33 -0
- package/vendor/@anvil/common/src/testing/fixtures/agent-runtime.ts +334 -0
- package/vendor/@anvil/common/src/testing/impl/agent-runtime.ts +6 -0
- package/vendor/@anvil/common/src/testing/index.ts +84 -0
- package/vendor/@anvil/common/src/testing/mock-modules.ts +53 -0
- package/vendor/@anvil/common/src/testing/mock-types.ts +123 -0
- package/vendor/@anvil/common/src/testing/mocks/analytics.ts +261 -0
- package/vendor/@anvil/common/src/testing/mocks/child-process.ts +93 -0
- package/vendor/@anvil/common/src/testing/mocks/crypto.ts +218 -0
- package/vendor/@anvil/common/src/testing/mocks/database.ts +337 -0
- package/vendor/@anvil/common/src/testing/mocks/fetch.ts +219 -0
- package/vendor/@anvil/common/src/testing/mocks/filesystem.ts +166 -0
- package/vendor/@anvil/common/src/testing/mocks/index.ts +101 -0
- package/vendor/@anvil/common/src/testing/mocks/logger.ts +135 -0
- package/vendor/@anvil/common/src/testing/mocks/stream.ts +313 -0
- package/vendor/@anvil/common/src/testing/mocks/timers.ts +132 -0
- package/vendor/@anvil/common/src/testing/mocks/tree-sitter.ts +127 -0
- package/vendor/@anvil/common/src/testing/setup.ts +282 -0
- package/vendor/@anvil/common/src/testing-env-ci.ts +15 -0
- package/vendor/@anvil/common/src/testing-env-process.ts +78 -0
- package/vendor/@anvil/common/src/tools/__tests__/compile-tool-definitions.test.ts +34 -0
- package/vendor/@anvil/common/src/tools/compile-tool-definitions.ts +157 -0
- package/vendor/@anvil/common/src/tools/constants.ts +117 -0
- package/vendor/@anvil/common/src/tools/list.ts +190 -0
- package/vendor/@anvil/common/src/tools/params/__tests__/coerce-to-array.test.ts +213 -0
- package/vendor/@anvil/common/src/tools/params/__tests__/read-docs.test.ts +47 -0
- package/vendor/@anvil/common/src/tools/params/__tests__/render-ui.test.ts +65 -0
- package/vendor/@anvil/common/src/tools/params/tool/__tests__/run-terminal-command-timeout.test.ts +49 -0
- package/vendor/@anvil/common/src/tools/params/tool/add-message.ts +39 -0
- package/vendor/@anvil/common/src/tools/params/tool/add-subgoal.ts +57 -0
- package/vendor/@anvil/common/src/tools/params/tool/apply-patch.ts +110 -0
- package/vendor/@anvil/common/src/tools/params/tool/ask-user.ts +181 -0
- package/vendor/@anvil/common/src/tools/params/tool/browser-logs.ts +85 -0
- package/vendor/@anvil/common/src/tools/params/tool/cloud-plan-ready.ts +94 -0
- package/vendor/@anvil/common/src/tools/params/tool/code-search.ts +159 -0
- package/vendor/@anvil/common/src/tools/params/tool/composio.ts +131 -0
- package/vendor/@anvil/common/src/tools/params/tool/create-plan.ts +80 -0
- package/vendor/@anvil/common/src/tools/params/tool/end-turn.ts +57 -0
- package/vendor/@anvil/common/src/tools/params/tool/find-files.ts +60 -0
- package/vendor/@anvil/common/src/tools/params/tool/glob.ts +80 -0
- package/vendor/@anvil/common/src/tools/params/tool/gravity-index.ts +93 -0
- package/vendor/@anvil/common/src/tools/params/tool/list-directory.ts +58 -0
- package/vendor/@anvil/common/src/tools/params/tool/lookup-agent-info.ts +37 -0
- package/vendor/@anvil/common/src/tools/params/tool/propose-str-replace.ts +103 -0
- package/vendor/@anvil/common/src/tools/params/tool/propose-write-file.ts +71 -0
- package/vendor/@anvil/common/src/tools/params/tool/read-docs.ts +90 -0
- package/vendor/@anvil/common/src/tools/params/tool/read-files.ts +109 -0
- package/vendor/@anvil/common/src/tools/params/tool/read-subtree.ts +79 -0
- package/vendor/@anvil/common/src/tools/params/tool/read-url.ts +81 -0
- package/vendor/@anvil/common/src/tools/params/tool/render-ui.ts +168 -0
- package/vendor/@anvil/common/src/tools/params/tool/run-file-change-hooks.ts +57 -0
- package/vendor/@anvil/common/src/tools/params/tool/run-terminal-command.ts +211 -0
- package/vendor/@anvil/common/src/tools/params/tool/screenshot.ts +97 -0
- package/vendor/@anvil/common/src/tools/params/tool/set-messages.ts +44 -0
- package/vendor/@anvil/common/src/tools/params/tool/set-output.ts +61 -0
- package/vendor/@anvil/common/src/tools/params/tool/skill.ts +59 -0
- package/vendor/@anvil/common/src/tools/params/tool/spawn-agent-inline.ts +56 -0
- package/vendor/@anvil/common/src/tools/params/tool/spawn-agents.ts +154 -0
- package/vendor/@anvil/common/src/tools/params/tool/str-replace.ts +107 -0
- package/vendor/@anvil/common/src/tools/params/tool/suggest-followups.ts +97 -0
- package/vendor/@anvil/common/src/tools/params/tool/task-completed.ts +61 -0
- package/vendor/@anvil/common/src/tools/params/tool/think-deeply.ts +56 -0
- package/vendor/@anvil/common/src/tools/params/tool/update-subgoal.ts +89 -0
- package/vendor/@anvil/common/src/tools/params/tool/vision-analyze.ts +82 -0
- package/vendor/@anvil/common/src/tools/params/tool/web-search.ts +73 -0
- package/vendor/@anvil/common/src/tools/params/tool/write-file.ts +71 -0
- package/vendor/@anvil/common/src/tools/params/tool/write-todos.ts +67 -0
- package/vendor/@anvil/common/src/tools/params/utils.ts +150 -0
- package/vendor/@anvil/common/src/tools/utils.ts +24 -0
- package/vendor/@anvil/common/src/types/__tests__/dynamic-agent-template.test.ts +20 -0
- package/vendor/@anvil/common/src/types/agent-template.ts +215 -0
- package/vendor/@anvil/common/src/types/anvil-session.ts +535 -0
- package/vendor/@anvil/common/src/types/anvil-streak.ts +6 -0
- package/vendor/@anvil/common/src/types/anvil-usage.ts +64 -0
- package/vendor/@anvil/common/src/types/api/agents/publish.ts +61 -0
- package/vendor/@anvil/common/src/types/bun-test.d.ts +5 -0
- package/vendor/@anvil/common/src/types/contracts/agent-runtime.ts +75 -0
- package/vendor/@anvil/common/src/types/contracts/analytics.ts +9 -0
- package/vendor/@anvil/common/src/types/contracts/bigquery.ts +55 -0
- package/vendor/@anvil/common/src/types/contracts/billing.ts +46 -0
- package/vendor/@anvil/common/src/types/contracts/client.ts +53 -0
- package/vendor/@anvil/common/src/types/contracts/database.ts +112 -0
- package/vendor/@anvil/common/src/types/contracts/env.ts +203 -0
- package/vendor/@anvil/common/src/types/contracts/llm.ts +191 -0
- package/vendor/@anvil/common/src/types/contracts/logger.ts +14 -0
- package/vendor/@anvil/common/src/types/contracts/logs.ts +34 -0
- package/vendor/@anvil/common/src/types/contracts/trace.ts +21 -0
- package/vendor/@anvil/common/src/types/dynamic-agent-template.ts +328 -0
- package/vendor/@anvil/common/src/types/filesystem.ts +10 -0
- package/vendor/@anvil/common/src/types/function-params.ts +33 -0
- package/vendor/@anvil/common/src/types/grant.ts +20 -0
- package/vendor/@anvil/common/src/types/gravity-index.ts +170 -0
- package/vendor/@anvil/common/src/types/json.ts +29 -0
- package/vendor/@anvil/common/src/types/mcp.ts +24 -0
- package/vendor/@anvil/common/src/types/messages/anvil-message.ts +58 -0
- package/vendor/@anvil/common/src/types/messages/content-part.ts +59 -0
- package/vendor/@anvil/common/src/types/messages/data-content.ts +14 -0
- package/vendor/@anvil/common/src/types/messages/provider-metadata.ts +13 -0
- package/vendor/@anvil/common/src/types/organization.ts +118 -0
- package/vendor/@anvil/common/src/types/print-mode.ts +121 -0
- package/vendor/@anvil/common/src/types/publisher.ts +67 -0
- package/vendor/@anvil/common/src/types/session-state.ts +149 -0
- package/vendor/@anvil/common/src/types/skill.ts +86 -0
- package/vendor/@anvil/common/src/types/source.ts +11 -0
- package/vendor/@anvil/common/src/types/spawn.ts +13 -0
- package/vendor/@anvil/common/src/types/subscription.ts +67 -0
- package/vendor/@anvil/common/src/types/usage.ts +16 -0
- package/vendor/@anvil/common/src/types/util.ts +3 -0
- package/vendor/@anvil/common/src/util/__tests__/ad-user-agent.test.ts +26 -0
- package/vendor/@anvil/common/src/util/__tests__/analytics-dispatcher.test.ts +122 -0
- package/vendor/@anvil/common/src/util/__tests__/analytics-log.test.ts +102 -0
- package/vendor/@anvil/common/src/util/__tests__/analytics-sampling.test.ts +143 -0
- package/vendor/@anvil/common/src/util/__tests__/anvil-streak.test.ts +202 -0
- package/vendor/@anvil/common/src/util/__tests__/anvil-usage-summary.test.ts +193 -0
- package/vendor/@anvil/common/src/util/__tests__/axiom-only-log.test.ts +140 -0
- package/vendor/@anvil/common/src/util/__tests__/client-user-agent.test.ts +148 -0
- package/vendor/@anvil/common/src/util/__tests__/engagement-tracker.test.ts +115 -0
- package/vendor/@anvil/common/src/util/__tests__/env-file-path.test.ts +57 -0
- package/vendor/@anvil/common/src/util/__tests__/error-abort.test.ts +774 -0
- package/vendor/@anvil/common/src/util/__tests__/error-api-details.test.ts +206 -0
- package/vendor/@anvil/common/src/util/__tests__/file-read-limits.test.ts +173 -0
- package/vendor/@anvil/common/src/util/__tests__/format-code-search.test.ts +60 -0
- package/vendor/@anvil/common/src/util/__tests__/log-mirror.test.ts +31 -0
- package/vendor/@anvil/common/src/util/__tests__/messages.test.ts +1252 -0
- package/vendor/@anvil/common/src/util/__tests__/partial-json-delta.test.ts +505 -0
- package/vendor/@anvil/common/src/util/__tests__/path.test.ts +22 -0
- package/vendor/@anvil/common/src/util/__tests__/project-ignore.test.ts +90 -0
- package/vendor/@anvil/common/src/util/__tests__/promise.test.ts +325 -0
- package/vendor/@anvil/common/src/util/__tests__/rate-limit.test.ts +28 -0
- package/vendor/@anvil/common/src/util/__tests__/reddit-anvil-retention.test.ts +130 -0
- package/vendor/@anvil/common/src/util/__tests__/saxy.test.ts +1008 -0
- package/vendor/@anvil/common/src/util/__tests__/split-data.test.ts +289 -0
- package/vendor/@anvil/common/src/util/__tests__/string.test.ts +239 -0
- package/vendor/@anvil/common/src/util/__tests__/thread-title.test.ts +70 -0
- package/vendor/@anvil/common/src/util/__tests__/tool-result-media-order.test.ts +108 -0
- package/vendor/@anvil/common/src/util/__tests__/ttft-histogram.test.ts +180 -0
- package/vendor/@anvil/common/src/util/__tests__/with-timeout.test.ts +24 -0
- package/vendor/@anvil/common/src/util/__tests__/zoned-time.test.ts +88 -0
- package/vendor/@anvil/common/src/util/ad-user-agent.ts +25 -0
- package/vendor/@anvil/common/src/util/agent-file-utils.ts +110 -0
- package/vendor/@anvil/common/src/util/agent-id-parsing.ts +136 -0
- package/vendor/@anvil/common/src/util/agent-name-normalization.ts +38 -0
- package/vendor/@anvil/common/src/util/agent-name-resolver.ts +86 -0
- package/vendor/@anvil/common/src/util/analytics-dispatcher.ts +82 -0
- package/vendor/@anvil/common/src/util/analytics-log.ts +78 -0
- package/vendor/@anvil/common/src/util/analytics-sampling.ts +255 -0
- package/vendor/@anvil/common/src/util/anvil-model-availability.ts +102 -0
- package/vendor/@anvil/common/src/util/anvil-privacy.ts +82 -0
- package/vendor/@anvil/common/src/util/anvil-streak-line.ts +103 -0
- package/vendor/@anvil/common/src/util/anvil-streak.ts +156 -0
- package/vendor/@anvil/common/src/util/anvil-usage-summary.ts +125 -0
- package/vendor/@anvil/common/src/util/array.ts +31 -0
- package/vendor/@anvil/common/src/util/axiom-only-log.ts +124 -0
- package/vendor/@anvil/common/src/util/cache-debug.ts +171 -0
- package/vendor/@anvil/common/src/util/client-user-agent.ts +114 -0
- package/vendor/@anvil/common/src/util/credentials.ts +26 -0
- package/vendor/@anvil/common/src/util/currency.ts +25 -0
- package/vendor/@anvil/common/src/util/dates.ts +81 -0
- package/vendor/@anvil/common/src/util/disposable-email.ts +277 -0
- package/vendor/@anvil/common/src/util/engagement-tracker.ts +129 -0
- package/vendor/@anvil/common/src/util/env-file-path.ts +40 -0
- package/vendor/@anvil/common/src/util/error.ts +562 -0
- package/vendor/@anvil/common/src/util/file-read-limits.ts +236 -0
- package/vendor/@anvil/common/src/util/file.ts +339 -0
- package/vendor/@anvil/common/src/util/format-code-search.ts +115 -0
- package/vendor/@anvil/common/src/util/lazy-response-ads.ts +92 -0
- package/vendor/@anvil/common/src/util/log-data.ts +57 -0
- package/vendor/@anvil/common/src/util/log-ingest.ts +58 -0
- package/vendor/@anvil/common/src/util/log-mirror.ts +34 -0
- package/vendor/@anvil/common/src/util/lru-cache.ts +67 -0
- package/vendor/@anvil/common/src/util/messages.ts +685 -0
- package/vendor/@anvil/common/src/util/min-heap.ts +87 -0
- package/vendor/@anvil/common/src/util/model-utils.ts +4 -0
- package/vendor/@anvil/common/src/util/object.ts +128 -0
- package/vendor/@anvil/common/src/util/partial-json-delta.ts +89 -0
- package/vendor/@anvil/common/src/util/path.ts +15 -0
- package/vendor/@anvil/common/src/util/project-ignore.ts +119 -0
- package/vendor/@anvil/common/src/util/promise.ts +73 -0
- package/vendor/@anvil/common/src/util/random.ts +15 -0
- package/vendor/@anvil/common/src/util/rate-limit.ts +56 -0
- package/vendor/@anvil/common/src/util/reddit-anvil-retention.ts +60 -0
- package/vendor/@anvil/common/src/util/reddit-capi-events.ts +36 -0
- package/vendor/@anvil/common/src/util/response-ad-positions.ts +54 -0
- package/vendor/@anvil/common/src/util/saxy.ts +741 -0
- package/vendor/@anvil/common/src/util/skills.test.ts +44 -0
- package/vendor/@anvil/common/src/util/skills.ts +36 -0
- package/vendor/@anvil/common/src/util/split-data.ts +259 -0
- package/vendor/@anvil/common/src/util/stop-sequence.ts +61 -0
- package/vendor/@anvil/common/src/util/string.ts +426 -0
- package/vendor/@anvil/common/src/util/system-info.ts +53 -0
- package/vendor/@anvil/common/src/util/thread-title.ts +46 -0
- package/vendor/@anvil/common/src/util/ttft-histogram.ts +73 -0
- package/vendor/@anvil/common/src/util/xml-parser.ts +27 -0
- package/vendor/@anvil/common/src/util/xml.ts +17 -0
- package/vendor/@anvil/common/src/util/zod-schema.ts +25 -0
- package/vendor/@anvil/common/src/util/zoned-time.ts +136 -0
- package/vendor/@anvil/common/src/utils/ask-user-bridge.ts +44 -0
- package/vendor/@anvil/sdk/dist/index.cjs +52297 -0
- package/vendor/@anvil/sdk/dist/index.cjs.map +440 -0
- package/vendor/@anvil/sdk/dist/index.d.ts +4277 -0
- package/vendor/@anvil/sdk/dist/index.mjs +52290 -0
- package/vendor/@anvil/sdk/dist/index.mjs.map +440 -0
- package/vendor/@anvil/sdk/dist/vendor/ai.cjs +30886 -0
- package/vendor/@anvil/sdk/dist/vendor/ripgrep/arm64-darwin/rg +0 -0
- package/vendor/@anvil/sdk/dist/vendor/ripgrep/arm64-linux/rg +0 -0
- package/vendor/@anvil/sdk/dist/vendor/ripgrep/x64-darwin/rg +0 -0
- package/vendor/@anvil/sdk/dist/vendor/ripgrep/x64-linux/rg +0 -0
- package/vendor/@anvil/sdk/dist/vendor/ripgrep/x64-win32/rg.exe +0 -0
- package/vendor/@anvil/sdk/dist/wasm/tree-sitter-c-sharp.wasm +0 -0
- package/vendor/@anvil/sdk/dist/wasm/tree-sitter-cpp.wasm +0 -0
- package/vendor/@anvil/sdk/dist/wasm/tree-sitter-go.wasm +0 -0
- package/vendor/@anvil/sdk/dist/wasm/tree-sitter-java.wasm +0 -0
- package/vendor/@anvil/sdk/dist/wasm/tree-sitter-javascript.wasm +0 -0
- package/vendor/@anvil/sdk/dist/wasm/tree-sitter-python.wasm +0 -0
- package/vendor/@anvil/sdk/dist/wasm/tree-sitter-ruby.wasm +0 -0
- package/vendor/@anvil/sdk/dist/wasm/tree-sitter-rust.wasm +0 -0
- package/vendor/@anvil/sdk/dist/wasm/tree-sitter-tsx.wasm +0 -0
- package/vendor/@anvil/sdk/dist/wasm/tree-sitter-typescript.wasm +0 -0
- package/vendor/@anvil/sdk/dist/wasm/tree-sitter.wasm +0 -0
- package/vendor/@anvil/sdk/package.json +21 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
|
|
3
|
+
import * as ignore from 'ignore'
|
|
4
|
+
import { sortBy } from 'lodash'
|
|
5
|
+
|
|
6
|
+
import { DEFAULT_IGNORED_PATHS } from './constants/paths'
|
|
7
|
+
import { isEnvTemplateFilePath } from './util/env-file-path'
|
|
8
|
+
import { fileExists, isValidProjectRoot } from './util/file'
|
|
9
|
+
import { isPathInside } from './util/path'
|
|
10
|
+
import {
|
|
11
|
+
addIgnoreFileContents,
|
|
12
|
+
isIgnoredByIgnoreChain,
|
|
13
|
+
PROJECT_IGNORE_FILES,
|
|
14
|
+
} from './util/project-ignore'
|
|
15
|
+
|
|
16
|
+
import type { AnvilFileSystem } from './types/filesystem'
|
|
17
|
+
import type { DirectoryNode, FileTreeNode } from './util/file'
|
|
18
|
+
import type { DirIgnore } from './util/project-ignore'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Logs file tree errors in debug mode only.
|
|
22
|
+
* Errors are logged but not thrown to preserve tree-building behavior.
|
|
23
|
+
*
|
|
24
|
+
* File tree operations commonly encounter expected errors (permissions,
|
|
25
|
+
* deleted files) that are not fatal. We only log in debug mode to avoid
|
|
26
|
+
* noisy output during normal operation.
|
|
27
|
+
*/
|
|
28
|
+
function logFileTreeError(
|
|
29
|
+
operation: string,
|
|
30
|
+
filePath: string,
|
|
31
|
+
error: unknown,
|
|
32
|
+
): void {
|
|
33
|
+
// Only log in debug mode to avoid noisy output
|
|
34
|
+
if (!process.env.DEBUG && !process.env.ANVIL_DEBUG) {
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const err = error as { code?: string } | undefined
|
|
39
|
+
const code = err?.code
|
|
40
|
+
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
41
|
+
|
|
42
|
+
console.debug(
|
|
43
|
+
`[FileTree] ${operation} failed for "${filePath}"${
|
|
44
|
+
code ? ` (${code})` : ''
|
|
45
|
+
}: ${errorMessage}`,
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const DEFAULT_MAX_FILES = 10_000
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Everything downstream of the file tree is POSIX-only: `ignore` matches
|
|
53
|
+
* nothing but forward slashes, and glob patterns come from the model in that
|
|
54
|
+
* form too. `path.relative` returns backslashes on Windows, and `ignore`
|
|
55
|
+
* answers `false` for them instead of throwing — so nested rules like `build/`
|
|
56
|
+
* or `node_modules` silently stop pruning and the crawl burns its file budget
|
|
57
|
+
* on build output before it ever reaches the source tree.
|
|
58
|
+
*/
|
|
59
|
+
function toPosixPath(p: string): string {
|
|
60
|
+
return p.replace(/\\/g, '/')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function createDefaultIgnore(allowEnvTemplate = false): ignore.Ignore {
|
|
64
|
+
const defaultIgnore = ignore.default()
|
|
65
|
+
for (const pattern of DEFAULT_IGNORED_PATHS) {
|
|
66
|
+
if (allowEnvTemplate && (pattern === '.env' || pattern === '.env.*')) {
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
defaultIgnore.add(pattern)
|
|
70
|
+
}
|
|
71
|
+
return defaultIgnore
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// When the project root is the home directory (or an ancestor), a full scan
|
|
75
|
+
// could crawl the user's entire disk. Instead of disabling the file tree
|
|
76
|
+
// entirely, do a shallow capped scan so @ mentions still surface
|
|
77
|
+
// `project/docs/file.md`-style paths.
|
|
78
|
+
export const SHALLOW_SCAN_MAX_DEPTH = 3
|
|
79
|
+
const SHALLOW_SCAN_MAX_FILES = 2_000
|
|
80
|
+
const SHALLOW_SCAN_MAX_DIRS = 500
|
|
81
|
+
|
|
82
|
+
/** Whether `getProjectFileTree` will shallow-scan this root (see above). */
|
|
83
|
+
export function isShallowScanRoot(
|
|
84
|
+
projectRoot: string | undefined,
|
|
85
|
+
): projectRoot is string {
|
|
86
|
+
return !!projectRoot && !isValidProjectRoot(projectRoot)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function getProjectFileTree(params: {
|
|
90
|
+
projectRoot: string
|
|
91
|
+
maxFiles?: number
|
|
92
|
+
fs: AnvilFileSystem
|
|
93
|
+
}): Promise<FileTreeNode[]> {
|
|
94
|
+
const withDefaults = { maxFiles: DEFAULT_MAX_FILES, ...params }
|
|
95
|
+
const { projectRoot, fs } = withDefaults
|
|
96
|
+
let { maxFiles } = withDefaults
|
|
97
|
+
let maxDepth = Infinity
|
|
98
|
+
let maxDirs = Infinity
|
|
99
|
+
|
|
100
|
+
const _start = Date.now()
|
|
101
|
+
const defaultIgnore = createDefaultIgnore()
|
|
102
|
+
|
|
103
|
+
if (isShallowScanRoot(projectRoot)) {
|
|
104
|
+
defaultIgnore.add('.*')
|
|
105
|
+
maxDepth = SHALLOW_SCAN_MAX_DEPTH
|
|
106
|
+
maxFiles = Math.min(maxFiles, SHALLOW_SCAN_MAX_FILES)
|
|
107
|
+
maxDirs = SHALLOW_SCAN_MAX_DIRS
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const root: DirectoryNode = {
|
|
111
|
+
name: path.basename(projectRoot),
|
|
112
|
+
type: 'directory',
|
|
113
|
+
children: [],
|
|
114
|
+
filePath: '',
|
|
115
|
+
}
|
|
116
|
+
const queue: {
|
|
117
|
+
node: DirectoryNode
|
|
118
|
+
fullPath: string
|
|
119
|
+
ignores: DirIgnore[]
|
|
120
|
+
depth: number
|
|
121
|
+
}[] = [
|
|
122
|
+
{
|
|
123
|
+
node: root,
|
|
124
|
+
fullPath: projectRoot,
|
|
125
|
+
ignores: [{ base: '', ig: defaultIgnore }],
|
|
126
|
+
depth: 0,
|
|
127
|
+
},
|
|
128
|
+
]
|
|
129
|
+
let totalFiles = 0
|
|
130
|
+
let dirsScanned = 0
|
|
131
|
+
|
|
132
|
+
while (queue.length > 0 && totalFiles < maxFiles && dirsScanned < maxDirs) {
|
|
133
|
+
const { node, fullPath, ignores, depth } = queue.shift()!
|
|
134
|
+
dirsScanned++
|
|
135
|
+
const dirIgnores = [
|
|
136
|
+
...ignores,
|
|
137
|
+
{
|
|
138
|
+
base: toPosixPath(path.relative(projectRoot, fullPath)),
|
|
139
|
+
ig: await parseGitignore({ fullDirPath: fullPath, fs }),
|
|
140
|
+
},
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const files = await fs.readdir(fullPath)
|
|
145
|
+
for (const file of files) {
|
|
146
|
+
if (totalFiles >= maxFiles) break
|
|
147
|
+
|
|
148
|
+
const filePath = path.join(fullPath, file)
|
|
149
|
+
const relativeFilePath = toPosixPath(path.relative(projectRoot, filePath))
|
|
150
|
+
|
|
151
|
+
if (isIgnoredByIgnoreChain(dirIgnores, relativeFilePath)) continue
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const stats = await fs.stat(filePath)
|
|
155
|
+
if (stats.isDirectory()) {
|
|
156
|
+
const childNode: DirectoryNode = {
|
|
157
|
+
name: file,
|
|
158
|
+
type: 'directory',
|
|
159
|
+
children: [],
|
|
160
|
+
filePath: relativeFilePath,
|
|
161
|
+
}
|
|
162
|
+
node.children.push(childNode)
|
|
163
|
+
// Past maxDepth the directory still shows up as a node above, but
|
|
164
|
+
// its contents are not scanned.
|
|
165
|
+
if (depth + 1 < maxDepth) {
|
|
166
|
+
queue.push({
|
|
167
|
+
node: childNode,
|
|
168
|
+
fullPath: filePath,
|
|
169
|
+
ignores: dirIgnores,
|
|
170
|
+
depth: depth + 1,
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
const lastReadTime = stats.atimeMs
|
|
175
|
+
node.children.push({
|
|
176
|
+
name: file,
|
|
177
|
+
type: 'file',
|
|
178
|
+
lastReadTime,
|
|
179
|
+
filePath: relativeFilePath,
|
|
180
|
+
})
|
|
181
|
+
totalFiles++
|
|
182
|
+
}
|
|
183
|
+
} catch (error: unknown) {
|
|
184
|
+
// File may be inaccessible due to permissions or may have been deleted.
|
|
185
|
+
// Log with context for debugging, but continue building the tree.
|
|
186
|
+
logFileTreeError('fs.stat', filePath, error)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
} catch (error: unknown) {
|
|
190
|
+
// Directory may be inaccessible due to permissions.
|
|
191
|
+
// Log with context for debugging, but continue building the tree.
|
|
192
|
+
logFileTreeError('fs.readdir', fullPath, error)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return root.children
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function parseGitignoreWithMode(params: {
|
|
199
|
+
fullDirPath: string
|
|
200
|
+
fs: AnvilFileSystem
|
|
201
|
+
throwOnReadError: boolean
|
|
202
|
+
}): Promise<ignore.Ignore> {
|
|
203
|
+
const { fullDirPath, fs, throwOnReadError } = params
|
|
204
|
+
|
|
205
|
+
const ig = ignore.default()
|
|
206
|
+
const directoryEntries = throwOnReadError
|
|
207
|
+
? new Set(await fs.readdir(fullDirPath))
|
|
208
|
+
: undefined
|
|
209
|
+
|
|
210
|
+
for (const fileName of PROJECT_IGNORE_FILES) {
|
|
211
|
+
const ignoreFilePath = path.join(fullDirPath, fileName)
|
|
212
|
+
if (directoryEntries) {
|
|
213
|
+
if (!directoryEntries.has(fileName)) continue
|
|
214
|
+
} else {
|
|
215
|
+
const ignoreFileExists = await fileExists({ filePath: ignoreFilePath, fs })
|
|
216
|
+
if (!ignoreFileExists) continue
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
let ignoreContent: string
|
|
220
|
+
try {
|
|
221
|
+
ignoreContent = await fs.readFile(ignoreFilePath, 'utf8')
|
|
222
|
+
} catch (error: unknown) {
|
|
223
|
+
// Resilient file-tree callers continue without unreadable rules. Access
|
|
224
|
+
// checks can opt into throwing so their caller can fail closed.
|
|
225
|
+
logFileTreeError('fs.readFile (ignore file)', ignoreFilePath, error)
|
|
226
|
+
if (throwOnReadError) throw error
|
|
227
|
+
continue
|
|
228
|
+
}
|
|
229
|
+
addIgnoreFileContents(ig, ignoreContent, (pattern, error) =>
|
|
230
|
+
logFileTreeError('ignore.add (pattern)', pattern, error),
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return ig
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export async function parseGitignore(params: {
|
|
238
|
+
fullDirPath: string
|
|
239
|
+
fs: AnvilFileSystem
|
|
240
|
+
}): Promise<ignore.Ignore> {
|
|
241
|
+
return parseGitignoreWithMode({ ...params, throwOnReadError: false })
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function getAllFilePaths(
|
|
245
|
+
nodes: FileTreeNode[],
|
|
246
|
+
basePath: string = '',
|
|
247
|
+
): string[] {
|
|
248
|
+
return nodes.flatMap((node) => {
|
|
249
|
+
if (node.type === 'file') {
|
|
250
|
+
return [path.join(basePath, node.name)]
|
|
251
|
+
}
|
|
252
|
+
return getAllFilePaths(node.children || [], path.join(basePath, node.name))
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export interface PathInfo {
|
|
257
|
+
path: string
|
|
258
|
+
isDirectory: boolean
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function getAllPathsWithDirectories(
|
|
262
|
+
nodes: FileTreeNode[],
|
|
263
|
+
basePath: string = '',
|
|
264
|
+
): PathInfo[] {
|
|
265
|
+
return nodes.flatMap((node) => {
|
|
266
|
+
const nodePath = basePath ? path.join(basePath, node.name) : node.name
|
|
267
|
+
if (node.type === 'file') {
|
|
268
|
+
return [{ path: nodePath, isDirectory: false }]
|
|
269
|
+
}
|
|
270
|
+
// Include the directory itself, plus recurse into children
|
|
271
|
+
const dirEntry: PathInfo = { path: nodePath, isDirectory: true }
|
|
272
|
+
const children = getAllPathsWithDirectories(node.children || [], nodePath)
|
|
273
|
+
return [dirEntry, ...children]
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function flattenTree(nodes: FileTreeNode[]): FileTreeNode[] {
|
|
278
|
+
return nodes.flatMap((node) => {
|
|
279
|
+
if (node.type === 'file') {
|
|
280
|
+
return [node]
|
|
281
|
+
}
|
|
282
|
+
return flattenTree(node.children ?? [])
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function getLastReadFilePaths(
|
|
287
|
+
flattenedNodes: FileTreeNode[],
|
|
288
|
+
count: number,
|
|
289
|
+
) {
|
|
290
|
+
return sortBy(
|
|
291
|
+
flattenedNodes.filter((node) => node.lastReadTime),
|
|
292
|
+
'lastReadTime',
|
|
293
|
+
)
|
|
294
|
+
.reverse()
|
|
295
|
+
.slice(0, count)
|
|
296
|
+
.map((node) => node.filePath)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export async function isFileIgnored(params: {
|
|
300
|
+
filePath: string
|
|
301
|
+
projectRoot: string
|
|
302
|
+
fs: AnvilFileSystem
|
|
303
|
+
/** Allow the env template past built-in rules while strictly applying project rules. */
|
|
304
|
+
allowEnvTemplate?: boolean
|
|
305
|
+
}): Promise<boolean> {
|
|
306
|
+
const {
|
|
307
|
+
filePath,
|
|
308
|
+
projectRoot,
|
|
309
|
+
fs,
|
|
310
|
+
allowEnvTemplate: allowEnvTemplateRequested = false,
|
|
311
|
+
} = params
|
|
312
|
+
const allowEnvTemplate =
|
|
313
|
+
allowEnvTemplateRequested && isEnvTemplateFilePath(filePath)
|
|
314
|
+
|
|
315
|
+
const resolvedProjectRoot = path.resolve(projectRoot)
|
|
316
|
+
const fullFilePath = path.resolve(resolvedProjectRoot, filePath)
|
|
317
|
+
if (!isPathInside(resolvedProjectRoot, fullFilePath)) return false
|
|
318
|
+
|
|
319
|
+
const defaultIgnore = createDefaultIgnore(allowEnvTemplate)
|
|
320
|
+
|
|
321
|
+
const relativeFilePath = toPosixPath(
|
|
322
|
+
path.relative(resolvedProjectRoot, fullFilePath),
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
// Get ignore patterns from the directory containing the file and all parent directories
|
|
326
|
+
const dirIgnores: DirIgnore[] = []
|
|
327
|
+
let currentDir = path.dirname(fullFilePath)
|
|
328
|
+
while (true) {
|
|
329
|
+
let ig: ignore.Ignore
|
|
330
|
+
try {
|
|
331
|
+
ig = await parseGitignoreWithMode({
|
|
332
|
+
fullDirPath: currentDir,
|
|
333
|
+
fs,
|
|
334
|
+
throwOnReadError: allowEnvTemplate,
|
|
335
|
+
})
|
|
336
|
+
} catch (error) {
|
|
337
|
+
if (allowEnvTemplate) return true
|
|
338
|
+
throw error
|
|
339
|
+
}
|
|
340
|
+
dirIgnores.push({
|
|
341
|
+
base: toPosixPath(path.relative(resolvedProjectRoot, currentDir)),
|
|
342
|
+
ig,
|
|
343
|
+
})
|
|
344
|
+
if (path.relative(resolvedProjectRoot, currentDir) === '') break
|
|
345
|
+
|
|
346
|
+
const parentDir = path.dirname(currentDir)
|
|
347
|
+
if (parentDir === currentDir) break
|
|
348
|
+
currentDir = parentDir
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return isIgnoredByIgnoreChain(
|
|
352
|
+
[{ base: '', ig: defaultIgnore }, ...dirIgnores.reverse()],
|
|
353
|
+
relativeFilePath,
|
|
354
|
+
)
|
|
355
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
import { IS_PROD } from '@anvil/common/env'
|
|
4
|
+
import { extractClientIp } from '@anvil/common/util/rate-limit'
|
|
5
|
+
import type { Logger } from '@anvil/common/types/contracts/logger'
|
|
6
|
+
import type { RedditCapiEventName } from '@anvil/common/util/reddit-capi-events'
|
|
7
|
+
|
|
8
|
+
export type { RedditCapiEventName }
|
|
9
|
+
|
|
10
|
+
/** Reddit Ads pixel ID (public, also used for CAPI endpoint). */
|
|
11
|
+
export const REDDIT_PIXEL_ID = 'a2_j6o59svbxzzn'
|
|
12
|
+
|
|
13
|
+
const REDDIT_CAPI_ENDPOINT = `https://ads-api.reddit.com/api/v3/pixels/${REDDIT_PIXEL_ID}/conversion_events`
|
|
14
|
+
|
|
15
|
+
export type RedditActionSource = 'WEBSITE' | 'APP' | 'PHYSICAL_STORE' | 'OTHER'
|
|
16
|
+
|
|
17
|
+
export type RedditCapiUser = {
|
|
18
|
+
email?: string | null
|
|
19
|
+
externalId?: string | null
|
|
20
|
+
ipAddress?: string | null
|
|
21
|
+
userAgent?: string | null
|
|
22
|
+
clickId?: string | null
|
|
23
|
+
uuid?: string | null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function sha256(value: string): string {
|
|
27
|
+
return createHash('sha256').update(value).digest('hex')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeEmail(email: string): string {
|
|
31
|
+
const normalized = email.trim().toLowerCase()
|
|
32
|
+
const atIndex = normalized.lastIndexOf('@')
|
|
33
|
+
if (atIndex <= 0 || atIndex === normalized.length - 1) {
|
|
34
|
+
return normalized
|
|
35
|
+
}
|
|
36
|
+
const local = normalized.slice(0, atIndex)
|
|
37
|
+
const domain = normalized.slice(atIndex + 1)
|
|
38
|
+
return `${local.split('+')[0]?.replaceAll('.', '')}@${domain}`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function buildRedditCapiUserPayload(user: RedditCapiUser) {
|
|
42
|
+
const payload: Record<string, string> = {}
|
|
43
|
+
|
|
44
|
+
if (user.email) {
|
|
45
|
+
payload.email = sha256(normalizeEmail(user.email))
|
|
46
|
+
}
|
|
47
|
+
if (user.externalId) {
|
|
48
|
+
payload.external_id = sha256(user.externalId.trim())
|
|
49
|
+
}
|
|
50
|
+
if (user.ipAddress) {
|
|
51
|
+
payload.ip_address = user.ipAddress
|
|
52
|
+
}
|
|
53
|
+
if (user.userAgent) {
|
|
54
|
+
payload.user_agent = user.userAgent
|
|
55
|
+
}
|
|
56
|
+
if (user.uuid) {
|
|
57
|
+
payload.uuid = user.uuid
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return Object.keys(payload).length > 0 ? payload : undefined
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type SendRedditCustomConversionParams = {
|
|
64
|
+
/** Conversion access token from Reddit Events Manager (env-provided; no-op when unset). */
|
|
65
|
+
accessToken: string | undefined
|
|
66
|
+
customEventName: RedditCapiEventName
|
|
67
|
+
conversionId: string
|
|
68
|
+
actionSource: RedditActionSource
|
|
69
|
+
eventAt?: number
|
|
70
|
+
eventSourceUrl?: string
|
|
71
|
+
testId?: string
|
|
72
|
+
user: RedditCapiUser
|
|
73
|
+
fetchImpl?: typeof fetch
|
|
74
|
+
logger?: Logger
|
|
75
|
+
enabled?: boolean
|
|
76
|
+
sleepImpl?: (ms: number) => Promise<void>
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type RedditCapiDeliveryResult = 'sent' | 'disabled'
|
|
80
|
+
|
|
81
|
+
export class RedditCapiDeliveryError extends Error {
|
|
82
|
+
constructor(
|
|
83
|
+
message: string,
|
|
84
|
+
readonly status?: number,
|
|
85
|
+
) {
|
|
86
|
+
super(message)
|
|
87
|
+
this.name = 'RedditCapiDeliveryError'
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function eventSourceUrlWithClickId(
|
|
92
|
+
eventSourceUrl: string | undefined,
|
|
93
|
+
clickId: string | null | undefined,
|
|
94
|
+
): string | undefined {
|
|
95
|
+
if (!eventSourceUrl || !clickId) return eventSourceUrl
|
|
96
|
+
try {
|
|
97
|
+
const url = new URL(eventSourceUrl)
|
|
98
|
+
if (!url.searchParams.has('rdt_cid')) {
|
|
99
|
+
url.searchParams.set('rdt_cid', clickId)
|
|
100
|
+
}
|
|
101
|
+
return url.toString()
|
|
102
|
+
} catch {
|
|
103
|
+
return eventSourceUrl
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function buildRedditCustomConversionBody(
|
|
108
|
+
params: Pick<
|
|
109
|
+
SendRedditCustomConversionParams,
|
|
110
|
+
| 'customEventName'
|
|
111
|
+
| 'conversionId'
|
|
112
|
+
| 'actionSource'
|
|
113
|
+
| 'eventAt'
|
|
114
|
+
| 'eventSourceUrl'
|
|
115
|
+
| 'testId'
|
|
116
|
+
| 'user'
|
|
117
|
+
>,
|
|
118
|
+
) {
|
|
119
|
+
const user = buildRedditCapiUserPayload(params.user)
|
|
120
|
+
const eventSourceUrl = eventSourceUrlWithClickId(
|
|
121
|
+
params.eventSourceUrl,
|
|
122
|
+
params.user.clickId,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
data: {
|
|
127
|
+
...(params.testId ? { test_id: params.testId } : {}),
|
|
128
|
+
events: [
|
|
129
|
+
{
|
|
130
|
+
event_at: params.eventAt ?? Date.now(),
|
|
131
|
+
action_source: params.actionSource,
|
|
132
|
+
...(eventSourceUrl ? { event_source_url: eventSourceUrl } : {}),
|
|
133
|
+
type: {
|
|
134
|
+
tracking_type: 'CUSTOM',
|
|
135
|
+
custom_event_name: params.customEventName,
|
|
136
|
+
},
|
|
137
|
+
...(params.user.clickId ? { click_id: params.user.clickId } : {}),
|
|
138
|
+
metadata: {
|
|
139
|
+
conversion_id: params.conversionId,
|
|
140
|
+
},
|
|
141
|
+
...(user ? { user } : {}),
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function isRetryableStatus(status: number): boolean {
|
|
149
|
+
return status === 408 || status === 429 || status >= 500
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const sleep = (ms: number) =>
|
|
153
|
+
new Promise<void>((resolve) => setTimeout(resolve, ms))
|
|
154
|
+
|
|
155
|
+
/** Deliver one custom event, retrying one transient failure. */
|
|
156
|
+
export async function sendRedditCustomConversion(
|
|
157
|
+
params: SendRedditCustomConversionParams,
|
|
158
|
+
): Promise<RedditCapiDeliveryResult> {
|
|
159
|
+
if (!(params.enabled ?? IS_PROD) || !params.accessToken) {
|
|
160
|
+
return 'disabled'
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const fetchImpl = params.fetchImpl ?? fetch
|
|
164
|
+
const body = buildRedditCustomConversionBody(params)
|
|
165
|
+
const sleepImpl = params.sleepImpl ?? sleep
|
|
166
|
+
|
|
167
|
+
for (let attempt = 1; attempt <= 2; attempt += 1) {
|
|
168
|
+
try {
|
|
169
|
+
const response = await fetchImpl(REDDIT_CAPI_ENDPOINT, {
|
|
170
|
+
method: 'POST',
|
|
171
|
+
headers: {
|
|
172
|
+
Authorization: `Bearer ${params.accessToken}`,
|
|
173
|
+
'Content-Type': 'application/json',
|
|
174
|
+
'User-Agent': 'anvil/1.0 (reddit-capi)',
|
|
175
|
+
},
|
|
176
|
+
body: JSON.stringify(body),
|
|
177
|
+
signal: AbortSignal.timeout(3_000),
|
|
178
|
+
})
|
|
179
|
+
if (response.ok) {
|
|
180
|
+
params.logger?.info(
|
|
181
|
+
{
|
|
182
|
+
customEventName: params.customEventName,
|
|
183
|
+
conversionId: params.conversionId,
|
|
184
|
+
attempt,
|
|
185
|
+
},
|
|
186
|
+
'Reddit CAPI conversion delivered',
|
|
187
|
+
)
|
|
188
|
+
return 'sent'
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const responseBody = (await response.text().catch(() => '')).slice(0, 500)
|
|
192
|
+
if (attempt === 1 && isRetryableStatus(response.status)) {
|
|
193
|
+
await sleepImpl(100)
|
|
194
|
+
continue
|
|
195
|
+
}
|
|
196
|
+
throw new RedditCapiDeliveryError(
|
|
197
|
+
`Reddit CAPI rejected ${params.customEventName} (${response.status}): ${responseBody}`,
|
|
198
|
+
response.status,
|
|
199
|
+
)
|
|
200
|
+
} catch (error) {
|
|
201
|
+
if (
|
|
202
|
+
attempt === 1 &&
|
|
203
|
+
(!(error instanceof RedditCapiDeliveryError) ||
|
|
204
|
+
(error.status !== undefined && isRetryableStatus(error.status)))
|
|
205
|
+
) {
|
|
206
|
+
await sleepImpl(100)
|
|
207
|
+
continue
|
|
208
|
+
}
|
|
209
|
+
params.logger?.warn(
|
|
210
|
+
{
|
|
211
|
+
error,
|
|
212
|
+
customEventName: params.customEventName,
|
|
213
|
+
conversionId: params.conversionId,
|
|
214
|
+
attempt,
|
|
215
|
+
},
|
|
216
|
+
'Reddit CAPI conversion request failed',
|
|
217
|
+
)
|
|
218
|
+
if (error instanceof RedditCapiDeliveryError) throw error
|
|
219
|
+
throw new RedditCapiDeliveryError(
|
|
220
|
+
`Reddit CAPI request failed for ${params.customEventName}`,
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
throw new RedditCapiDeliveryError(
|
|
226
|
+
`Reddit CAPI request failed for ${params.customEventName}`,
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function redditConversionId(
|
|
231
|
+
event: RedditCapiEventName,
|
|
232
|
+
userId: string,
|
|
233
|
+
): string {
|
|
234
|
+
return sha256(`${event}:${userId}`)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Identity fields shared by every server-side Reddit conversion call, built
|
|
239
|
+
* from the incoming request's headers. Surface-specific attribution (click id,
|
|
240
|
+
* uuid) is layered on by callers that have it.
|
|
241
|
+
*/
|
|
242
|
+
export function redditUserFromRequestHeaders(params: {
|
|
243
|
+
userId: string
|
|
244
|
+
email?: string | null
|
|
245
|
+
headers: { get(name: string): string | null }
|
|
246
|
+
}): RedditCapiUser {
|
|
247
|
+
const ip = extractClientIp(params.headers)
|
|
248
|
+
return {
|
|
249
|
+
email: params.email,
|
|
250
|
+
externalId: params.userId,
|
|
251
|
+
ipAddress: ip === 'unknown' ? undefined : ip,
|
|
252
|
+
userAgent: params.headers.get('user-agent')?.trim() || undefined,
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { z } from 'zod/v4'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
FEEDBACK_CATEGORIES,
|
|
5
|
+
FEEDBACK_SOURCES,
|
|
6
|
+
MAX_ERRORS,
|
|
7
|
+
MAX_ERROR_ID_LENGTH,
|
|
8
|
+
MAX_ERROR_MESSAGE_LENGTH,
|
|
9
|
+
MAX_RECENT_MESSAGES,
|
|
10
|
+
MESSAGE_VARIANTS,
|
|
11
|
+
} from '../constants/feedback'
|
|
12
|
+
|
|
13
|
+
export const feedbackRequestSchema = z.object({
|
|
14
|
+
text: z.string().trim().min(1),
|
|
15
|
+
category: z.enum(FEEDBACK_CATEGORIES),
|
|
16
|
+
type: z.enum(['message', 'general']),
|
|
17
|
+
clientFeedbackId: z.string().uuid().optional(),
|
|
18
|
+
source: z.enum(FEEDBACK_SOURCES).optional(),
|
|
19
|
+
appVersion: z.string().max(100).optional(),
|
|
20
|
+
platform: z.string().max(100).optional(),
|
|
21
|
+
messageId: z.string().min(1).max(200).optional(),
|
|
22
|
+
messageVariant: z.enum(MESSAGE_VARIANTS).optional(),
|
|
23
|
+
completionTime: z.string().max(50).optional(),
|
|
24
|
+
credits: z.number().nonnegative().finite().optional(),
|
|
25
|
+
agentMode: z.string().max(100).optional(),
|
|
26
|
+
sessionCreditsUsed: z.number().nonnegative().finite().optional(),
|
|
27
|
+
recentMessages: z
|
|
28
|
+
.array(
|
|
29
|
+
z.object({
|
|
30
|
+
type: z.enum(MESSAGE_VARIANTS),
|
|
31
|
+
id: z.string().max(200),
|
|
32
|
+
completionTime: z.string().max(50).optional(),
|
|
33
|
+
credits: z.number().nonnegative().finite().optional(),
|
|
34
|
+
}),
|
|
35
|
+
)
|
|
36
|
+
.max(MAX_RECENT_MESSAGES)
|
|
37
|
+
.optional(),
|
|
38
|
+
errors: z
|
|
39
|
+
.array(
|
|
40
|
+
z.object({
|
|
41
|
+
id: z.string().max(MAX_ERROR_ID_LENGTH),
|
|
42
|
+
message: z.string().max(MAX_ERROR_MESSAGE_LENGTH),
|
|
43
|
+
}),
|
|
44
|
+
)
|
|
45
|
+
.max(MAX_ERRORS)
|
|
46
|
+
.optional(),
|
|
47
|
+
}).refine(
|
|
48
|
+
(data) => data.type !== 'message' || (data.messageId != null && data.messageId !== ''),
|
|
49
|
+
{ message: 'messageId is required when type is "message"', path: ['messageId'] },
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
export type FeedbackRequest = z.infer<typeof feedbackRequestSchema>
|