@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,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who gets shown the onboarding form.
|
|
3
|
+
*
|
|
4
|
+
* Nothing here is a permission check. Onboarding never refuses a request on any
|
|
5
|
+
* surface — the entire mechanism is one redirect, to a form with a Skip button,
|
|
6
|
+
* fired at most once per browser. What this decides is who is interrupted.
|
|
7
|
+
*
|
|
8
|
+
* The answer is: **anyone who has not answered, once.** New signups get it on
|
|
9
|
+
* their way in; a long-standing account gets it the next time they sign in
|
|
10
|
+
* through the web portal. There is deliberately no cutover date any more. One
|
|
11
|
+
* existed when the form was a blocking gate, to keep existing users from being
|
|
12
|
+
* locked out of a product they already used — with nothing to lock them out of,
|
|
13
|
+
* a cutover only means never learning anything about the 200,000 accounts we
|
|
14
|
+
* already have. The Skip button and the seen-cookie are what protect them now,
|
|
15
|
+
* and they cost a single dismissible screen instead of an exemption.
|
|
16
|
+
*
|
|
17
|
+
* The polarity is deliberate: **an unset or `off` switch shows it to NOBODY.**
|
|
18
|
+
* Absence of config disables the feature rather than enabling it universally.
|
|
19
|
+
* This repo has already paid for the opposite convention once — a deleted env
|
|
20
|
+
* var silently activating a default is documented in
|
|
21
|
+
* db-capacity-and-scaling.md §10.6 — and here that mistake would present as
|
|
22
|
+
* every user meeting a form nobody decided to launch.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export type OnboardingRequirementInput = {
|
|
26
|
+
/** Parsed `ANVIL_ONBOARDING_ENABLED`. */
|
|
27
|
+
enabled: boolean
|
|
28
|
+
/** Whether the user has answered every question. */
|
|
29
|
+
complete: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type OnboardingRequirement =
|
|
33
|
+
| { required: false; reason: 'gate_disabled' | 'already_complete' }
|
|
34
|
+
| { required: true }
|
|
35
|
+
|
|
36
|
+
export function evaluateOnboardingRequirement(
|
|
37
|
+
input: OnboardingRequirementInput,
|
|
38
|
+
): OnboardingRequirement {
|
|
39
|
+
// Not switched on — nobody is asked. See the polarity note above.
|
|
40
|
+
if (!input.enabled) return { required: false, reason: 'gate_disabled' }
|
|
41
|
+
if (input.complete) return { required: false, reason: 'already_complete' }
|
|
42
|
+
return { required: true }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Read the on/off switch.
|
|
47
|
+
*
|
|
48
|
+
* Anything other than an explicit affirmative is off, so a typo, an empty
|
|
49
|
+
* string or a deleted variable all fail the same safe way.
|
|
50
|
+
*/
|
|
51
|
+
export function parseOnboardingEnabled(raw: string | null | undefined): boolean {
|
|
52
|
+
const trimmed = raw?.trim().toLowerCase()
|
|
53
|
+
return trimmed === 'on' || trimmed === 'true' || trimmed === '1'
|
|
54
|
+
}
|
|
55
|
+
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The post-signup onboarding questionnaire.
|
|
3
|
+
*
|
|
4
|
+
* Five questions, each a fixed option list plus a free-text "Other". Shared
|
|
5
|
+
* between the form, the persistence layer and the admin view so a renamed
|
|
6
|
+
* option cannot silently split one answer into two in the analytics.
|
|
7
|
+
*
|
|
8
|
+
* Why the option ids are opaque and permanent
|
|
9
|
+
* -------------------------------------------
|
|
10
|
+
* The stored value is the id, never the label. Labels are copy and will be
|
|
11
|
+
* reworded; ids are data. Changing a label is free, changing an id
|
|
12
|
+
* retroactively rewrites history — so ids here are append-only. Retire an
|
|
13
|
+
* option by removing it from the list, never by renaming it.
|
|
14
|
+
*
|
|
15
|
+
* When an option is retired or broadened, its old answers do not disappear:
|
|
16
|
+
* `ONBOARDING_LEGACY_OPTION_IDS` below maps the retired id onto whichever
|
|
17
|
+
* current option now covers it, and the admin tally applies that at read time.
|
|
18
|
+
* Nothing rewrites what is stored — the stored answer stays exactly what the
|
|
19
|
+
* person picked on the day they picked it.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export type OnboardingQuestionId =
|
|
23
|
+
| 'referral_source'
|
|
24
|
+
| 'role'
|
|
25
|
+
| 'proficiency'
|
|
26
|
+
| 'intended_use'
|
|
27
|
+
| 'subscriptions'
|
|
28
|
+
|
|
29
|
+
export type OnboardingOption = {
|
|
30
|
+
/** Stored verbatim. Append-only — see the note above. */
|
|
31
|
+
id: string
|
|
32
|
+
label: string
|
|
33
|
+
/** Choosing this clears every other option, and vice versa. Only meaningful
|
|
34
|
+
* on a multi-select — "none of them" and "these ones" cannot both be true. */
|
|
35
|
+
exclusive?: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type OnboardingQuestion = {
|
|
39
|
+
id: OnboardingQuestionId
|
|
40
|
+
prompt: string
|
|
41
|
+
options: OnboardingOption[]
|
|
42
|
+
/** Whether more than one option may be chosen. */
|
|
43
|
+
multi: boolean
|
|
44
|
+
/** `scale` renders as a slider: the options are an ordered ramp, so the
|
|
45
|
+
* distance between two answers means something and the control should say
|
|
46
|
+
* so. Everything else is a chip list. Storage is identical either way. */
|
|
47
|
+
kind?: 'choice' | 'scale'
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Every question offers this. The accompanying free text is stored separately
|
|
51
|
+
* so "other" stays countable while the text stays readable. */
|
|
52
|
+
export const OTHER_OPTION_ID = 'other'
|
|
53
|
+
|
|
54
|
+
export const ANVIL_ONBOARDING_QUESTIONS: readonly OnboardingQuestion[] = [
|
|
55
|
+
{
|
|
56
|
+
id: 'referral_source',
|
|
57
|
+
prompt: 'Where did you hear about Anvil?',
|
|
58
|
+
// Attribution is the one answer we cannot reconstruct later: referrer
|
|
59
|
+
// headers are stripped, and self-report is the only signal for word of
|
|
60
|
+
// mouth, which is where most of it actually comes from.
|
|
61
|
+
//
|
|
62
|
+
// Instagram leads because it was the single largest write-in by a distance,
|
|
63
|
+
// and it shares an audience with TikTok closely enough that splitting them
|
|
64
|
+
// would buy two thin numbers instead of one usable one.
|
|
65
|
+
options: [
|
|
66
|
+
// Keeps the `tiktok` id: those answers are the same audience, and a new
|
|
67
|
+
// id would restart the count from zero for the sake of a tidier string.
|
|
68
|
+
{ id: 'tiktok', label: 'Instagram / TikTok' },
|
|
69
|
+
{ id: 'youtube', label: 'YouTube' },
|
|
70
|
+
{ id: 'x_twitter', label: 'X / Twitter' },
|
|
71
|
+
{ id: 'search', label: 'Google / AI search' },
|
|
72
|
+
{ id: 'friend', label: 'A friend' },
|
|
73
|
+
{ id: 'reddit', label: 'Reddit' },
|
|
74
|
+
{ id: 'github', label: 'GitHub' },
|
|
75
|
+
{ id: OTHER_OPTION_ID, label: 'Somewhere else' },
|
|
76
|
+
],
|
|
77
|
+
multi: false,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: 'role',
|
|
81
|
+
prompt: 'What best describes you?',
|
|
82
|
+
options: [
|
|
83
|
+
{ id: 'professional_dev', label: 'Developer' },
|
|
84
|
+
{ id: 'founder', label: 'Founder' },
|
|
85
|
+
{ id: 'student', label: 'Student' },
|
|
86
|
+
{ id: 'hobbyist', label: 'Hobbyist' },
|
|
87
|
+
{ id: 'pm', label: 'Designer or PM' },
|
|
88
|
+
{ id: 'non_technical', label: 'Non-technical' },
|
|
89
|
+
{ id: OTHER_OPTION_ID, label: 'Something else' },
|
|
90
|
+
],
|
|
91
|
+
multi: false,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: 'proficiency',
|
|
95
|
+
prompt: 'How much do you code?',
|
|
96
|
+
options: [
|
|
97
|
+
{ id: 'none', label: 'Not at all' },
|
|
98
|
+
{ id: 'beginner', label: 'Beginner' },
|
|
99
|
+
{ id: 'intermediate', label: 'Intermediate' },
|
|
100
|
+
{ id: 'advanced', label: 'Advanced' },
|
|
101
|
+
{ id: 'expert', label: 'Expert' },
|
|
102
|
+
],
|
|
103
|
+
// No Other: this is an ordered scale, and a free-text answer on an ordinal
|
|
104
|
+
// question is unusable for the segmentation it exists to support.
|
|
105
|
+
multi: false,
|
|
106
|
+
kind: 'scale',
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: 'intended_use',
|
|
110
|
+
prompt: 'What will you build with Anvil?',
|
|
111
|
+
options: [
|
|
112
|
+
{ id: 'website', label: 'Websites and apps' },
|
|
113
|
+
{ id: 'work', label: 'Work projects' },
|
|
114
|
+
{ id: 'side_projects', label: 'Side projects' },
|
|
115
|
+
{ id: 'learning', label: 'Learning to code' },
|
|
116
|
+
{ id: 'automation', label: 'Scripts and automation' },
|
|
117
|
+
{ id: OTHER_OPTION_ID, label: 'Something else' },
|
|
118
|
+
],
|
|
119
|
+
multi: true,
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: 'subscriptions',
|
|
123
|
+
prompt: 'Current subscriptions that you’ve had, cancelled or will cancel',
|
|
124
|
+
// The one question here that prices the product rather than describing the
|
|
125
|
+
// user: what someone already pays for — and is willing to stop paying for —
|
|
126
|
+
// says who we actually compete with. Deliberately loose about tense, since
|
|
127
|
+
// "about to cancel" is the same competitive signal as "already did" and
|
|
128
|
+
// splitting the two would buy two thin numbers instead of one usable one.
|
|
129
|
+
options: [
|
|
130
|
+
{ id: 'none', label: 'None', exclusive: true },
|
|
131
|
+
{ id: 'claude_code', label: 'Claude Code' },
|
|
132
|
+
{ id: 'cursor', label: 'Cursor' },
|
|
133
|
+
{ id: 'opencode', label: 'opencode' },
|
|
134
|
+
{ id: 'copilot', label: 'GitHub Copilot' },
|
|
135
|
+
{ id: 'codex', label: 'Codex' },
|
|
136
|
+
{ id: 'gemini', label: 'Gemini' },
|
|
137
|
+
{ id: 'app_builder', label: 'Lovable / Replit' },
|
|
138
|
+
{ id: OTHER_OPTION_ID, label: 'Something else' },
|
|
139
|
+
],
|
|
140
|
+
multi: true,
|
|
141
|
+
},
|
|
142
|
+
] as const
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Retired option id → the current option that now covers it.
|
|
146
|
+
*
|
|
147
|
+
* Read-time only, applied by the admin tally. A retired option's answers are
|
|
148
|
+
* still real answers; folding them into their successor keeps a broadened
|
|
149
|
+
* option's count honest instead of resetting it to zero on the day of the edit.
|
|
150
|
+
* An id with no successor here is simply not counted.
|
|
151
|
+
*/
|
|
152
|
+
export const ONBOARDING_LEGACY_OPTION_IDS: Partial<
|
|
153
|
+
Record<OnboardingQuestionId, Record<string, string>>
|
|
154
|
+
> = {
|
|
155
|
+
// Discord and blog/newsletter were dropped as options; both are honestly
|
|
156
|
+
// "somewhere else" rather than any of the channels that remain.
|
|
157
|
+
referral_source: { discord: OTHER_OPTION_ID, blog_news: OTHER_OPTION_ID },
|
|
158
|
+
// Data/ML folded into the broader developer bucket, designer into the
|
|
159
|
+
// designer-or-PM one.
|
|
160
|
+
role: { data_ml: 'professional_dev', designer: 'pm' },
|
|
161
|
+
// Prototypes and demos are websites and apps; code review has no successor
|
|
162
|
+
// narrow enough to claim it, so it goes to "something else".
|
|
163
|
+
intended_use: { prototyping: 'website', debugging: OTHER_OPTION_ID },
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Write-in text → the option it should have been.
|
|
168
|
+
*
|
|
169
|
+
* People type an answer that already exists on the list, at volume: Instagram
|
|
170
|
+
* was the top write-in on a question that did not offer it, and every variant
|
|
171
|
+
* of "ChatGPT" is the AI-search answer. Matching is read-time and deliberately
|
|
172
|
+
* narrow — a pattern that fires on an ambiguous word buys a wrong count, which
|
|
173
|
+
* is worse than the "other" bucket it came from.
|
|
174
|
+
*/
|
|
175
|
+
export const ONBOARDING_OTHER_TEXT_RULES: Partial<
|
|
176
|
+
Record<OnboardingQuestionId, { optionId: string; pattern: RegExp }[]>
|
|
177
|
+
> = {
|
|
178
|
+
referral_source: [
|
|
179
|
+
{ optionId: 'tiktok', pattern: /insta|\btiktok\b|\btik tok\b|\big\b/i },
|
|
180
|
+
{
|
|
181
|
+
optionId: 'search',
|
|
182
|
+
pattern:
|
|
183
|
+
/\bai\b|chat\s?gpt|\bgpt\b|claude|gemini|perplexity|copilot|grok|deepseek/i,
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Which option a write-in should be counted as, if any.
|
|
190
|
+
*
|
|
191
|
+
* Rules are tried in order and the first match wins, so the more specific
|
|
192
|
+
* pattern must come first — "insta" before the AI catch-all, since "instagram
|
|
193
|
+
* AI" is an Instagram answer.
|
|
194
|
+
*/
|
|
195
|
+
export function classifyOnboardingOtherText(
|
|
196
|
+
questionId: OnboardingQuestionId,
|
|
197
|
+
text: string,
|
|
198
|
+
): string | null {
|
|
199
|
+
const rules = ONBOARDING_OTHER_TEXT_RULES[questionId]
|
|
200
|
+
if (!rules) return null
|
|
201
|
+
for (const rule of rules) {
|
|
202
|
+
if (rule.pattern.test(text)) return rule.optionId
|
|
203
|
+
}
|
|
204
|
+
return null
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export type OnboardingAnswer = {
|
|
208
|
+
questionId: OnboardingQuestionId
|
|
209
|
+
/** Chosen option ids. Single-choice questions carry exactly one. */
|
|
210
|
+
optionIds: string[]
|
|
211
|
+
/** Free text, only when `other` is among the chosen ids. */
|
|
212
|
+
otherText?: string
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export type OnboardingSubmission = {
|
|
216
|
+
answers: OnboardingAnswer[]
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export const ONBOARDING_OTHER_TEXT_MAX = 200
|
|
220
|
+
|
|
221
|
+
export type OnboardingValidationError = {
|
|
222
|
+
/** Null when the complaint is about the submission as a whole rather than
|
|
223
|
+
* about one question — the only such case is an entirely empty one. */
|
|
224
|
+
questionId: OnboardingQuestionId | null
|
|
225
|
+
message: string
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Validate a submission against the question set.
|
|
230
|
+
*
|
|
231
|
+
* Deliberately strict about ids and lenient about text: an unknown option id
|
|
232
|
+
* means the client is out of date or forged, and accepting it would put a value
|
|
233
|
+
* in the analytics that no question can explain. Free text is merely trimmed
|
|
234
|
+
* and truncated — it is never interpreted, only displayed.
|
|
235
|
+
*/
|
|
236
|
+
export function validateOnboardingSubmission(
|
|
237
|
+
submission: OnboardingSubmission,
|
|
238
|
+
questions: readonly OnboardingQuestion[] = ANVIL_ONBOARDING_QUESTIONS,
|
|
239
|
+
): { ok: true; answers: OnboardingAnswer[] } | { ok: false; errors: OnboardingValidationError[] } {
|
|
240
|
+
const errors: OnboardingValidationError[] = []
|
|
241
|
+
const cleaned: OnboardingAnswer[] = []
|
|
242
|
+
|
|
243
|
+
for (const question of questions) {
|
|
244
|
+
const answer = submission.answers.find((a) => a.questionId === question.id)
|
|
245
|
+
const optionIds = answer?.optionIds ?? []
|
|
246
|
+
|
|
247
|
+
if (optionIds.length === 0) {
|
|
248
|
+
errors.push({ questionId: question.id, message: 'Please choose an option.' })
|
|
249
|
+
continue
|
|
250
|
+
}
|
|
251
|
+
if (!question.multi && optionIds.length > 1) {
|
|
252
|
+
errors.push({ questionId: question.id, message: 'Choose one option.' })
|
|
253
|
+
continue
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const valid = new Set(question.options.map((o) => o.id))
|
|
257
|
+
const unknown = optionIds.filter((id) => !valid.has(id))
|
|
258
|
+
if (unknown.length > 0) {
|
|
259
|
+
errors.push({ questionId: question.id, message: 'Unrecognised option.' })
|
|
260
|
+
continue
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const exclusive = question.options.filter((o) => o.exclusive).map((o) => o.id)
|
|
264
|
+
if (optionIds.length > 1 && optionIds.some((id) => exclusive.includes(id))) {
|
|
265
|
+
errors.push({
|
|
266
|
+
questionId: question.id,
|
|
267
|
+
message: 'That answer cannot be combined with the others.',
|
|
268
|
+
})
|
|
269
|
+
continue
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const wantsOther = optionIds.includes(OTHER_OPTION_ID)
|
|
273
|
+
const otherText = answer?.otherText?.trim()
|
|
274
|
+
if (wantsOther && !otherText) {
|
|
275
|
+
errors.push({
|
|
276
|
+
questionId: question.id,
|
|
277
|
+
message: 'Tell us a little more.',
|
|
278
|
+
})
|
|
279
|
+
continue
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
cleaned.push({
|
|
283
|
+
questionId: question.id,
|
|
284
|
+
optionIds,
|
|
285
|
+
// Dropped unless `other` was chosen, so stray text cannot ride along on
|
|
286
|
+
// an answer that has nowhere to show it.
|
|
287
|
+
...(wantsOther && otherText
|
|
288
|
+
? { otherText: otherText.slice(0, ONBOARDING_OTHER_TEXT_MAX) }
|
|
289
|
+
: {}),
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return errors.length > 0 ? { ok: false, errors } : { ok: true, answers: cleaned }
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Every question answered. Decides only whether someone who returns to the
|
|
297
|
+
* form still sees it — nothing is withheld either way. */
|
|
298
|
+
export function isOnboardingComplete(
|
|
299
|
+
answers: readonly OnboardingAnswer[] | null | undefined,
|
|
300
|
+
questions: readonly OnboardingQuestion[] = ANVIL_ONBOARDING_QUESTIONS,
|
|
301
|
+
): boolean {
|
|
302
|
+
if (!answers || answers.length === 0) return false
|
|
303
|
+
const answered = new Set(
|
|
304
|
+
answers.filter((a) => a.optionIds.length > 0).map((a) => a.questionId),
|
|
305
|
+
)
|
|
306
|
+
return questions.every((q) => answered.has(q.id))
|
|
307
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anvil referral program constants.
|
|
3
|
+
*
|
|
4
|
+
* The referral REWARD is consolidated (2026-07-30) to exactly two things,
|
|
5
|
+
* both session-shaped and both defined next to the pools they feed:
|
|
6
|
+
*
|
|
7
|
+
* - FULL access tier: +1 daily GLM 5.2 session per qualified referral,
|
|
8
|
+
* uncapped (glmWeeklySessionsFromStats in packages/billing).
|
|
9
|
+
* - LIMITED access tier: +1 daily free session per qualified referral,
|
|
10
|
+
* capped at REFERRAL_CLI_DAILY_SESSION_BONUS_CAP (GLM is geo-gated).
|
|
11
|
+
*
|
|
12
|
+
* The old web tier ladder (tiered daily message limits + deploy-watermark
|
|
13
|
+
* removal) is GONE: the limits were never enforced anywhere — real limits are
|
|
14
|
+
* the session pools in anvil-models.ts — and the "Powered by Anvil"
|
|
15
|
+
* deploy watermark is globally disabled (prod_branding_injection_enabled =
|
|
16
|
+
* false), so both perks were marketing for things users didn't get. What
|
|
17
|
+
* remains in this file is the qualification machinery: GitHub account-age
|
|
18
|
+
* bars, attribution windows, and anti-farming ceilings.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Referred users must have a GitHub account at least this old for the
|
|
22
|
+
* referral to count. Younger accounts can still sign up normally — the
|
|
23
|
+
* referrer just gets no credit (anti-farming). */
|
|
24
|
+
export const MIN_GITHUB_ACCOUNT_AGE_MONTHS = 4
|
|
25
|
+
|
|
26
|
+
/** GLM 5.2 referral program uses a stricter account-age bar than the web
|
|
27
|
+
* program (no public-repo requirement, but the account must be a full year
|
|
28
|
+
* old) since the reward — paid GLM serverless time — costs more to abuse. */
|
|
29
|
+
export const MIN_GITHUB_ACCOUNT_AGE_MONTHS_GLM = 12
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Unified referral system (docs/referrals.md): a single 4-month GitHub
|
|
33
|
+
* account-age bar for ALL products (no public-repo requirement), consolidating
|
|
34
|
+
* the old per-program bars (cli/glm = 12mo, web = 4mo).
|
|
35
|
+
*/
|
|
36
|
+
export const MIN_GITHUB_ACCOUNT_AGE_MONTHS_REFERRAL = 4
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Anvil CLI limited-tier perk: each qualified referral whose referred user
|
|
40
|
+
* activated at the *limited* access tier grants +1 daily free-mode session,
|
|
41
|
+
* capped here (e.g. 6 base + 3 = 9/day). Full-access referrals instead grant
|
|
42
|
+
* GLM sessions, which are uncapped.
|
|
43
|
+
*/
|
|
44
|
+
export const REFERRAL_CLI_DAILY_SESSION_BONUS_CAP = 3
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Max attributed web signups (pending + completed) per referrer. The shared
|
|
48
|
+
* `user.referral_limit` column (default 5) governs the CLI program; the web
|
|
49
|
+
* program needs its own headroom — generous enough for unqualified signups,
|
|
50
|
+
* small enough to bound farming.
|
|
51
|
+
*/
|
|
52
|
+
export const ANVIL_WEB_REFERRAL_LIMIT = 20
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A referral can only be attributed within this many days of the referred
|
|
56
|
+
* user's signup — stops a referrer from claiming long-pre-existing accounts.
|
|
57
|
+
* Shared by the unified referral_v2 redemption path and its clients.
|
|
58
|
+
*/
|
|
59
|
+
export const REFERRAL_SIGNUP_WINDOW_DAYS = 30
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Max attributed referrals (referral_v2 rows) a single referrer may accumulate.
|
|
63
|
+
* Since the GLM reward was uncapped (2026-07-30) this is no longer a pure
|
|
64
|
+
* anti-spam ceiling: GLM entitlement now scales 1:1 with qualified referrals,
|
|
65
|
+
* so this limit IS the effective maximum (100 GLM sessions/day). The other
|
|
66
|
+
* rewards are still capped at read time (the web tier ladder, the CLI bonus at
|
|
67
|
+
* REFERRAL_CLI_DAILY_SESSION_BONUS_CAP), so for those it stays a formality.
|
|
68
|
+
*/
|
|
69
|
+
export const ANVIL_REFERRAL_SIGNUP_LIMIT = 100
|
|
70
|
+
|
|
71
|
+
/** Whether a GitHub account created at `githubCreatedAtMs` satisfies the
|
|
72
|
+
* referral age requirement at time `nowMs`. Months are computed on the
|
|
73
|
+
* calendar (e.g. created Jan 15 qualifies on/after May 15). `minMonths`
|
|
74
|
+
* defaults to the web bar; the GLM program passes
|
|
75
|
+
* MIN_GITHUB_ACCOUNT_AGE_MONTHS_GLM. */
|
|
76
|
+
export function isGithubAccountOldEnoughForReferral(
|
|
77
|
+
githubCreatedAtMs: number | null | undefined,
|
|
78
|
+
nowMs: number = Date.now(),
|
|
79
|
+
minMonths: number = MIN_GITHUB_ACCOUNT_AGE_MONTHS,
|
|
80
|
+
): boolean {
|
|
81
|
+
if (githubCreatedAtMs == null || !Number.isFinite(githubCreatedAtMs)) {
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
const threshold = new Date(githubCreatedAtMs)
|
|
85
|
+
threshold.setUTCMonth(threshold.getUTCMonth() + minMonths)
|
|
86
|
+
return nowMs >= threshold.getTime()
|
|
87
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signup-refusal reasons and their user-facing copy.
|
|
3
|
+
*
|
|
4
|
+
* Lives in `common`, not in `packages/auth`, because the login pages are client
|
|
5
|
+
* components: importing this from the auth barrel would drag `db`, `stripe` and
|
|
6
|
+
* the server env schema into the browser bundle. The decision logic stays in
|
|
7
|
+
* `@anvil/auth/signup-gate`; only the vocabulary is shared.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Why a signup was refused. Also the `?error=` code on the login redirect. */
|
|
11
|
+
export type SignupBlockReason =
|
|
12
|
+
| 'captcha_missing'
|
|
13
|
+
| 'captcha_invalid'
|
|
14
|
+
// Deliberately distinct from the two above rather than folded into them.
|
|
15
|
+
// The providers fail for different populations — a filter that blocks
|
|
16
|
+
// challenges.cloudflare.com usually does not block www.google.com, and vice
|
|
17
|
+
// versa — so a shared reason code would make the one question worth asking
|
|
18
|
+
// after a refusal spike ("which provider is failing?") unanswerable from the
|
|
19
|
+
// logs. That is exactly the ambiguity that cost four attempts on 2026-08-12.
|
|
20
|
+
| 'recaptcha_missing'
|
|
21
|
+
| 'recaptcha_invalid'
|
|
22
|
+
| 'mailbox_already_registered'
|
|
23
|
+
| 'privacy_egress'
|
|
24
|
+
| 'untrusted_client_ip'
|
|
25
|
+
| 'ip_signup_velocity'
|
|
26
|
+
| 'prefix_signup_velocity'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Deliberately actionable and non-accusatory.
|
|
30
|
+
*
|
|
31
|
+
* Someone hitting `privacy_egress` is far more likely to be a privacy-conscious
|
|
32
|
+
* developer on a VPN than a reseller, and the message they get should name the
|
|
33
|
+
* one thing that fixes it rather than implying they did something wrong. A
|
|
34
|
+
* refused signup with no explanation is indistinguishable from the product
|
|
35
|
+
* being broken, and generates a support ticket instead of a retry.
|
|
36
|
+
*/
|
|
37
|
+
export const SIGNUP_BLOCK_MESSAGES: Record<SignupBlockReason, string> = {
|
|
38
|
+
captcha_missing:
|
|
39
|
+
'Please complete the verification check on the sign-in page and try again.',
|
|
40
|
+
captcha_invalid:
|
|
41
|
+
'That verification check could not be confirmed. Please try again.',
|
|
42
|
+
// Names the host, because the overwhelmingly likely cause is an extension or
|
|
43
|
+
// DNS filter blocking it — a cause the user can fix in ten seconds if they
|
|
44
|
+
// know it and cannot fix at all if they do not. Retrying does not help here,
|
|
45
|
+
// so "please try again" alone would be actively misleading.
|
|
46
|
+
recaptcha_missing:
|
|
47
|
+
'Please complete the verification check on the sign-in page and try again. If it never appeared, an ad blocker or network filter may be blocking www.google.com/recaptcha.',
|
|
48
|
+
recaptcha_invalid:
|
|
49
|
+
'That verification check could not be confirmed. Please try again.',
|
|
50
|
+
mailbox_already_registered:
|
|
51
|
+
'An account already exists for this email address. Try signing in instead.',
|
|
52
|
+
privacy_egress:
|
|
53
|
+
'Accounts cannot be created over a VPN, proxy, or hosting provider. Please turn it off and try again — you can turn it back on afterwards.',
|
|
54
|
+
untrusted_client_ip:
|
|
55
|
+
'We could not verify where this request came from. Please try again, or contact support if it keeps happening.',
|
|
56
|
+
ip_signup_velocity:
|
|
57
|
+
'Too many accounts have been created from this network today. Please try again tomorrow, or contact support if you are on a shared connection.',
|
|
58
|
+
prefix_signup_velocity:
|
|
59
|
+
'Too many accounts have been created from this network today. Please try again tomorrow, or contact support if you are on a shared connection.',
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** True when `code` is a reason this module has copy for. Narrows an untrusted
|
|
63
|
+
* query-string value without a cast at every call site. */
|
|
64
|
+
export function isSignupBlockReason(
|
|
65
|
+
code: string | null | undefined,
|
|
66
|
+
): code is SignupBlockReason {
|
|
67
|
+
return Boolean(code) && code! in SIGNUP_BLOCK_MESSAGES
|
|
68
|
+
}
|