@alumbwe/anvil 1.0.20 → 1.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (328) hide show
  1. package/package.json +74 -70
  2. package/src/agents/bundled-agents.generated.ts +4 -4
  3. package/src/chat.tsx +1703 -1698
  4. package/src/cli-args.ts +134 -134
  5. package/src/components/status-bar.tsx +331 -313
  6. package/src/hooks/helpers/send-message.ts +663 -654
  7. package/src/hooks/use-send-message.ts +806 -773
  8. package/src/index.tsx +473 -473
  9. package/src/state/chat-store.ts +553 -544
  10. package/src/utils/create-run-config.ts +121 -121
  11. package/src/utils/format-compact-number.ts +37 -0
  12. package/vendor/@anvil/common/package.json +27 -0
  13. package/vendor/@anvil/common/src/__tests__/agent-validation.test.ts +846 -0
  14. package/vendor/@anvil/common/src/__tests__/anvil-model-availability.test.ts +101 -0
  15. package/vendor/@anvil/common/src/__tests__/anvil-models.test.ts +1408 -0
  16. package/vendor/@anvil/common/src/__tests__/anvil-public-data-use-copy.test.ts +258 -0
  17. package/vendor/@anvil/common/src/__tests__/anvil-referral-tiers.test.ts +48 -0
  18. package/vendor/@anvil/common/src/__tests__/anvil-spend-ceilings.test.ts +282 -0
  19. package/vendor/@anvil/common/src/__tests__/anvil-trust.test.ts +526 -0
  20. package/vendor/@anvil/common/src/__tests__/deepseek-direct.test.ts +68 -0
  21. package/vendor/@anvil/common/src/__tests__/disposable-email.test.ts +132 -0
  22. package/vendor/@anvil/common/src/__tests__/dynamic-agent-template-schema.test.ts +422 -0
  23. package/vendor/@anvil/common/src/__tests__/env-ci.test.ts +167 -0
  24. package/vendor/@anvil/common/src/__tests__/env-process.test.ts +145 -0
  25. package/vendor/@anvil/common/src/__tests__/foreign-client-shipped-agents.test.ts +234 -0
  26. package/vendor/@anvil/common/src/__tests__/foreign-client-signals.test.ts +379 -0
  27. package/vendor/@anvil/common/src/__tests__/free-agents.test.ts +807 -0
  28. package/vendor/@anvil/common/src/__tests__/gravity-capi.test.ts +154 -0
  29. package/vendor/@anvil/common/src/__tests__/handlesteps-parsing.test.ts +246 -0
  30. package/vendor/@anvil/common/src/__tests__/kimi-k3-god-only.test.ts +77 -0
  31. package/vendor/@anvil/common/src/__tests__/model-config.test.ts +89 -0
  32. package/vendor/@anvil/common/src/__tests__/project-file-tree.test.ts +298 -0
  33. package/vendor/@anvil/common/src/__tests__/provisioned-model-tiers.test.ts +116 -0
  34. package/vendor/@anvil/common/src/__tests__/reasoning-effort.test.ts +167 -0
  35. package/vendor/@anvil/common/src/__tests__/reddit-capi.test.ts +132 -0
  36. package/vendor/@anvil/common/src/__tests__/response-ad-positions.test.ts +72 -0
  37. package/vendor/@anvil/common/src/__tests__/user-state.test.ts +30 -0
  38. package/vendor/@anvil/common/src/actions.ts +215 -0
  39. package/vendor/@anvil/common/src/analytics-core.ts +69 -0
  40. package/vendor/@anvil/common/src/analytics.ts +93 -0
  41. package/vendor/@anvil/common/src/api-keys/constants.ts +26 -0
  42. package/vendor/@anvil/common/src/browser-actions.ts +413 -0
  43. package/vendor/@anvil/common/src/constants/__tests__/anvil-onboarding-gate.test.ts +63 -0
  44. package/vendor/@anvil/common/src/constants/__tests__/anvil-onboarding.test.ts +246 -0
  45. package/vendor/@anvil/common/src/constants/__tests__/cf-worker-signals.test.ts +106 -0
  46. package/vendor/@anvil/common/src/constants/agents.ts +99 -0
  47. package/vendor/@anvil/common/src/constants/analytics-events.ts +435 -0
  48. package/vendor/@anvil/common/src/constants/anthropic.ts +73 -0
  49. package/vendor/@anvil/common/src/constants/anvil-data-use.ts +81 -0
  50. package/vendor/@anvil/common/src/constants/anvil-errors.ts +7 -0
  51. package/vendor/@anvil/common/src/constants/anvil-gemini-thinker.ts +21 -0
  52. package/vendor/@anvil/common/src/constants/anvil-model-ids.ts +11 -0
  53. package/vendor/@anvil/common/src/constants/anvil-models.ts +2130 -0
  54. package/vendor/@anvil/common/src/constants/anvil-onboarding-gate.ts +55 -0
  55. package/vendor/@anvil/common/src/constants/anvil-onboarding.ts +307 -0
  56. package/vendor/@anvil/common/src/constants/anvil-referral-tiers.ts +87 -0
  57. package/vendor/@anvil/common/src/constants/anvil-signup-block.ts +68 -0
  58. package/vendor/@anvil/common/src/constants/anvil-spend-ceilings.ts +450 -0
  59. package/vendor/@anvil/common/src/constants/anvil-trust.ts +976 -0
  60. package/vendor/@anvil/common/src/constants/auth.ts +18 -0
  61. package/vendor/@anvil/common/src/constants/byok.ts +2 -0
  62. package/vendor/@anvil/common/src/constants/cf-worker-signals.ts +132 -0
  63. package/vendor/@anvil/common/src/constants/composio.ts +34 -0
  64. package/vendor/@anvil/common/src/constants/deepseek-direct.ts +55 -0
  65. package/vendor/@anvil/common/src/constants/feedback.ts +13 -0
  66. package/vendor/@anvil/common/src/constants/foreign-client-signals.ts +272 -0
  67. package/vendor/@anvil/common/src/constants/free-agents.ts +777 -0
  68. package/vendor/@anvil/common/src/constants/gemini.ts +15 -0
  69. package/vendor/@anvil/common/src/constants/grant-priorities.ts +13 -0
  70. package/vendor/@anvil/common/src/constants/hosts.ts +6 -0
  71. package/vendor/@anvil/common/src/constants/images.ts +51 -0
  72. package/vendor/@anvil/common/src/constants/index.ts +7 -0
  73. package/vendor/@anvil/common/src/constants/knowledge.ts +35 -0
  74. package/vendor/@anvil/common/src/constants/limits.ts +23 -0
  75. package/vendor/@anvil/common/src/constants/model-config.ts +277 -0
  76. package/vendor/@anvil/common/src/constants/openrouter-attribution.ts +8 -0
  77. package/vendor/@anvil/common/src/constants/paths.ts +69 -0
  78. package/vendor/@anvil/common/src/constants/provider-routes.ts +317 -0
  79. package/vendor/@anvil/common/src/constants/reasoning-effort.ts +79 -0
  80. package/vendor/@anvil/common/src/constants/skills.ts +60 -0
  81. package/vendor/@anvil/common/src/constants/subscription-plans.ts +49 -0
  82. package/vendor/@anvil/common/src/constants/ui.ts +25 -0
  83. package/vendor/@anvil/common/src/env-ci.ts +36 -0
  84. package/vendor/@anvil/common/src/env-process.ts +95 -0
  85. package/vendor/@anvil/common/src/env-schema.ts +95 -0
  86. package/vendor/@anvil/common/src/env.ts +24 -0
  87. package/vendor/@anvil/common/src/gravity-capi.ts +207 -0
  88. package/vendor/@anvil/common/src/mcp/client.ts +233 -0
  89. package/vendor/@anvil/common/src/old-constants.ts +10 -0
  90. package/vendor/@anvil/common/src/project-file-tree.ts +355 -0
  91. package/vendor/@anvil/common/src/reddit-capi.ts +254 -0
  92. package/vendor/@anvil/common/src/schemas/feedback.ts +52 -0
  93. package/vendor/@anvil/common/src/schemas/logs.ts +66 -0
  94. package/vendor/@anvil/common/src/templates/agent-validation.ts +392 -0
  95. package/vendor/@anvil/common/src/templates/initial-agents-dir/LICENSE +202 -0
  96. package/vendor/@anvil/common/src/templates/initial-agents-dir/README.md +294 -0
  97. package/vendor/@anvil/common/src/templates/initial-agents-dir/examples/01-basic-diff-reviewer.ts +17 -0
  98. package/vendor/@anvil/common/src/templates/initial-agents-dir/examples/02-intermediate-git-committer.ts +78 -0
  99. package/vendor/@anvil/common/src/templates/initial-agents-dir/examples/03-advanced-file-explorer.ts +73 -0
  100. package/vendor/@anvil/common/src/templates/initial-agents-dir/my-custom-agent.ts +40 -0
  101. package/vendor/@anvil/common/src/templates/initial-agents-dir/package.json +6 -0
  102. package/vendor/@anvil/common/src/templates/initial-agents-dir/skills/README.md +65 -0
  103. package/vendor/@anvil/common/src/templates/initial-agents-dir/skills/example-skill/SKILL.md +29 -0
  104. package/vendor/@anvil/common/src/templates/initial-agents-dir/types/agent-definition.ts +497 -0
  105. package/vendor/@anvil/common/src/templates/initial-agents-dir/types/tools.ts +444 -0
  106. package/vendor/@anvil/common/src/templates/initial-agents-dir/types/util-types.ts +178 -0
  107. package/vendor/@anvil/common/src/testing/TESTING_PATTERNS.md +351 -0
  108. package/vendor/@anvil/common/src/testing/anvil-offer-invariants.ts +169 -0
  109. package/vendor/@anvil/common/src/testing/errors.ts +33 -0
  110. package/vendor/@anvil/common/src/testing/fixtures/agent-runtime.ts +334 -0
  111. package/vendor/@anvil/common/src/testing/impl/agent-runtime.ts +6 -0
  112. package/vendor/@anvil/common/src/testing/index.ts +84 -0
  113. package/vendor/@anvil/common/src/testing/mock-modules.ts +53 -0
  114. package/vendor/@anvil/common/src/testing/mock-types.ts +123 -0
  115. package/vendor/@anvil/common/src/testing/mocks/analytics.ts +261 -0
  116. package/vendor/@anvil/common/src/testing/mocks/child-process.ts +93 -0
  117. package/vendor/@anvil/common/src/testing/mocks/crypto.ts +218 -0
  118. package/vendor/@anvil/common/src/testing/mocks/database.ts +337 -0
  119. package/vendor/@anvil/common/src/testing/mocks/fetch.ts +219 -0
  120. package/vendor/@anvil/common/src/testing/mocks/filesystem.ts +166 -0
  121. package/vendor/@anvil/common/src/testing/mocks/index.ts +101 -0
  122. package/vendor/@anvil/common/src/testing/mocks/logger.ts +135 -0
  123. package/vendor/@anvil/common/src/testing/mocks/stream.ts +313 -0
  124. package/vendor/@anvil/common/src/testing/mocks/timers.ts +132 -0
  125. package/vendor/@anvil/common/src/testing/mocks/tree-sitter.ts +127 -0
  126. package/vendor/@anvil/common/src/testing/setup.ts +282 -0
  127. package/vendor/@anvil/common/src/testing-env-ci.ts +15 -0
  128. package/vendor/@anvil/common/src/testing-env-process.ts +78 -0
  129. package/vendor/@anvil/common/src/tools/__tests__/compile-tool-definitions.test.ts +34 -0
  130. package/vendor/@anvil/common/src/tools/compile-tool-definitions.ts +157 -0
  131. package/vendor/@anvil/common/src/tools/constants.ts +117 -0
  132. package/vendor/@anvil/common/src/tools/list.ts +190 -0
  133. package/vendor/@anvil/common/src/tools/params/__tests__/coerce-to-array.test.ts +213 -0
  134. package/vendor/@anvil/common/src/tools/params/__tests__/read-docs.test.ts +47 -0
  135. package/vendor/@anvil/common/src/tools/params/__tests__/render-ui.test.ts +65 -0
  136. package/vendor/@anvil/common/src/tools/params/tool/__tests__/run-terminal-command-timeout.test.ts +49 -0
  137. package/vendor/@anvil/common/src/tools/params/tool/add-message.ts +39 -0
  138. package/vendor/@anvil/common/src/tools/params/tool/add-subgoal.ts +57 -0
  139. package/vendor/@anvil/common/src/tools/params/tool/apply-patch.ts +110 -0
  140. package/vendor/@anvil/common/src/tools/params/tool/ask-user.ts +181 -0
  141. package/vendor/@anvil/common/src/tools/params/tool/browser-logs.ts +85 -0
  142. package/vendor/@anvil/common/src/tools/params/tool/cloud-plan-ready.ts +94 -0
  143. package/vendor/@anvil/common/src/tools/params/tool/code-search.ts +159 -0
  144. package/vendor/@anvil/common/src/tools/params/tool/composio.ts +131 -0
  145. package/vendor/@anvil/common/src/tools/params/tool/create-plan.ts +80 -0
  146. package/vendor/@anvil/common/src/tools/params/tool/end-turn.ts +57 -0
  147. package/vendor/@anvil/common/src/tools/params/tool/find-files.ts +60 -0
  148. package/vendor/@anvil/common/src/tools/params/tool/glob.ts +80 -0
  149. package/vendor/@anvil/common/src/tools/params/tool/gravity-index.ts +93 -0
  150. package/vendor/@anvil/common/src/tools/params/tool/list-directory.ts +58 -0
  151. package/vendor/@anvil/common/src/tools/params/tool/lookup-agent-info.ts +37 -0
  152. package/vendor/@anvil/common/src/tools/params/tool/propose-str-replace.ts +103 -0
  153. package/vendor/@anvil/common/src/tools/params/tool/propose-write-file.ts +71 -0
  154. package/vendor/@anvil/common/src/tools/params/tool/read-docs.ts +90 -0
  155. package/vendor/@anvil/common/src/tools/params/tool/read-files.ts +109 -0
  156. package/vendor/@anvil/common/src/tools/params/tool/read-subtree.ts +79 -0
  157. package/vendor/@anvil/common/src/tools/params/tool/read-url.ts +81 -0
  158. package/vendor/@anvil/common/src/tools/params/tool/render-ui.ts +168 -0
  159. package/vendor/@anvil/common/src/tools/params/tool/run-file-change-hooks.ts +57 -0
  160. package/vendor/@anvil/common/src/tools/params/tool/run-terminal-command.ts +211 -0
  161. package/vendor/@anvil/common/src/tools/params/tool/screenshot.ts +97 -0
  162. package/vendor/@anvil/common/src/tools/params/tool/set-messages.ts +44 -0
  163. package/vendor/@anvil/common/src/tools/params/tool/set-output.ts +61 -0
  164. package/vendor/@anvil/common/src/tools/params/tool/skill.ts +59 -0
  165. package/vendor/@anvil/common/src/tools/params/tool/spawn-agent-inline.ts +56 -0
  166. package/vendor/@anvil/common/src/tools/params/tool/spawn-agents.ts +154 -0
  167. package/vendor/@anvil/common/src/tools/params/tool/str-replace.ts +107 -0
  168. package/vendor/@anvil/common/src/tools/params/tool/suggest-followups.ts +97 -0
  169. package/vendor/@anvil/common/src/tools/params/tool/task-completed.ts +61 -0
  170. package/vendor/@anvil/common/src/tools/params/tool/think-deeply.ts +56 -0
  171. package/vendor/@anvil/common/src/tools/params/tool/update-subgoal.ts +89 -0
  172. package/vendor/@anvil/common/src/tools/params/tool/vision-analyze.ts +82 -0
  173. package/vendor/@anvil/common/src/tools/params/tool/web-search.ts +73 -0
  174. package/vendor/@anvil/common/src/tools/params/tool/write-file.ts +71 -0
  175. package/vendor/@anvil/common/src/tools/params/tool/write-todos.ts +67 -0
  176. package/vendor/@anvil/common/src/tools/params/utils.ts +150 -0
  177. package/vendor/@anvil/common/src/tools/utils.ts +24 -0
  178. package/vendor/@anvil/common/src/types/__tests__/dynamic-agent-template.test.ts +20 -0
  179. package/vendor/@anvil/common/src/types/agent-template.ts +215 -0
  180. package/vendor/@anvil/common/src/types/anvil-session.ts +535 -0
  181. package/vendor/@anvil/common/src/types/anvil-streak.ts +6 -0
  182. package/vendor/@anvil/common/src/types/anvil-usage.ts +64 -0
  183. package/vendor/@anvil/common/src/types/api/agents/publish.ts +61 -0
  184. package/vendor/@anvil/common/src/types/bun-test.d.ts +5 -0
  185. package/vendor/@anvil/common/src/types/contracts/agent-runtime.ts +75 -0
  186. package/vendor/@anvil/common/src/types/contracts/analytics.ts +9 -0
  187. package/vendor/@anvil/common/src/types/contracts/bigquery.ts +55 -0
  188. package/vendor/@anvil/common/src/types/contracts/billing.ts +46 -0
  189. package/vendor/@anvil/common/src/types/contracts/client.ts +53 -0
  190. package/vendor/@anvil/common/src/types/contracts/database.ts +112 -0
  191. package/vendor/@anvil/common/src/types/contracts/env.ts +203 -0
  192. package/vendor/@anvil/common/src/types/contracts/llm.ts +191 -0
  193. package/vendor/@anvil/common/src/types/contracts/logger.ts +14 -0
  194. package/vendor/@anvil/common/src/types/contracts/logs.ts +34 -0
  195. package/vendor/@anvil/common/src/types/contracts/trace.ts +21 -0
  196. package/vendor/@anvil/common/src/types/dynamic-agent-template.ts +328 -0
  197. package/vendor/@anvil/common/src/types/filesystem.ts +10 -0
  198. package/vendor/@anvil/common/src/types/function-params.ts +33 -0
  199. package/vendor/@anvil/common/src/types/grant.ts +20 -0
  200. package/vendor/@anvil/common/src/types/gravity-index.ts +170 -0
  201. package/vendor/@anvil/common/src/types/json.ts +29 -0
  202. package/vendor/@anvil/common/src/types/mcp.ts +24 -0
  203. package/vendor/@anvil/common/src/types/messages/anvil-message.ts +58 -0
  204. package/vendor/@anvil/common/src/types/messages/content-part.ts +59 -0
  205. package/vendor/@anvil/common/src/types/messages/data-content.ts +14 -0
  206. package/vendor/@anvil/common/src/types/messages/provider-metadata.ts +13 -0
  207. package/vendor/@anvil/common/src/types/organization.ts +118 -0
  208. package/vendor/@anvil/common/src/types/print-mode.ts +121 -0
  209. package/vendor/@anvil/common/src/types/publisher.ts +67 -0
  210. package/vendor/@anvil/common/src/types/session-state.ts +149 -0
  211. package/vendor/@anvil/common/src/types/skill.ts +86 -0
  212. package/vendor/@anvil/common/src/types/source.ts +11 -0
  213. package/vendor/@anvil/common/src/types/spawn.ts +13 -0
  214. package/vendor/@anvil/common/src/types/subscription.ts +67 -0
  215. package/vendor/@anvil/common/src/types/usage.ts +16 -0
  216. package/vendor/@anvil/common/src/types/util.ts +3 -0
  217. package/vendor/@anvil/common/src/util/__tests__/ad-user-agent.test.ts +26 -0
  218. package/vendor/@anvil/common/src/util/__tests__/analytics-dispatcher.test.ts +122 -0
  219. package/vendor/@anvil/common/src/util/__tests__/analytics-log.test.ts +102 -0
  220. package/vendor/@anvil/common/src/util/__tests__/analytics-sampling.test.ts +143 -0
  221. package/vendor/@anvil/common/src/util/__tests__/anvil-streak.test.ts +202 -0
  222. package/vendor/@anvil/common/src/util/__tests__/anvil-usage-summary.test.ts +193 -0
  223. package/vendor/@anvil/common/src/util/__tests__/axiom-only-log.test.ts +140 -0
  224. package/vendor/@anvil/common/src/util/__tests__/client-user-agent.test.ts +148 -0
  225. package/vendor/@anvil/common/src/util/__tests__/engagement-tracker.test.ts +115 -0
  226. package/vendor/@anvil/common/src/util/__tests__/env-file-path.test.ts +57 -0
  227. package/vendor/@anvil/common/src/util/__tests__/error-abort.test.ts +774 -0
  228. package/vendor/@anvil/common/src/util/__tests__/error-api-details.test.ts +206 -0
  229. package/vendor/@anvil/common/src/util/__tests__/file-read-limits.test.ts +173 -0
  230. package/vendor/@anvil/common/src/util/__tests__/format-code-search.test.ts +60 -0
  231. package/vendor/@anvil/common/src/util/__tests__/log-mirror.test.ts +31 -0
  232. package/vendor/@anvil/common/src/util/__tests__/messages.test.ts +1252 -0
  233. package/vendor/@anvil/common/src/util/__tests__/partial-json-delta.test.ts +505 -0
  234. package/vendor/@anvil/common/src/util/__tests__/path.test.ts +22 -0
  235. package/vendor/@anvil/common/src/util/__tests__/project-ignore.test.ts +90 -0
  236. package/vendor/@anvil/common/src/util/__tests__/promise.test.ts +325 -0
  237. package/vendor/@anvil/common/src/util/__tests__/rate-limit.test.ts +28 -0
  238. package/vendor/@anvil/common/src/util/__tests__/reddit-anvil-retention.test.ts +130 -0
  239. package/vendor/@anvil/common/src/util/__tests__/saxy.test.ts +1008 -0
  240. package/vendor/@anvil/common/src/util/__tests__/split-data.test.ts +289 -0
  241. package/vendor/@anvil/common/src/util/__tests__/string.test.ts +239 -0
  242. package/vendor/@anvil/common/src/util/__tests__/thread-title.test.ts +70 -0
  243. package/vendor/@anvil/common/src/util/__tests__/tool-result-media-order.test.ts +108 -0
  244. package/vendor/@anvil/common/src/util/__tests__/ttft-histogram.test.ts +180 -0
  245. package/vendor/@anvil/common/src/util/__tests__/with-timeout.test.ts +24 -0
  246. package/vendor/@anvil/common/src/util/__tests__/zoned-time.test.ts +88 -0
  247. package/vendor/@anvil/common/src/util/ad-user-agent.ts +25 -0
  248. package/vendor/@anvil/common/src/util/agent-file-utils.ts +110 -0
  249. package/vendor/@anvil/common/src/util/agent-id-parsing.ts +136 -0
  250. package/vendor/@anvil/common/src/util/agent-name-normalization.ts +38 -0
  251. package/vendor/@anvil/common/src/util/agent-name-resolver.ts +86 -0
  252. package/vendor/@anvil/common/src/util/analytics-dispatcher.ts +82 -0
  253. package/vendor/@anvil/common/src/util/analytics-log.ts +78 -0
  254. package/vendor/@anvil/common/src/util/analytics-sampling.ts +255 -0
  255. package/vendor/@anvil/common/src/util/anvil-model-availability.ts +102 -0
  256. package/vendor/@anvil/common/src/util/anvil-privacy.ts +82 -0
  257. package/vendor/@anvil/common/src/util/anvil-streak-line.ts +103 -0
  258. package/vendor/@anvil/common/src/util/anvil-streak.ts +156 -0
  259. package/vendor/@anvil/common/src/util/anvil-usage-summary.ts +125 -0
  260. package/vendor/@anvil/common/src/util/array.ts +31 -0
  261. package/vendor/@anvil/common/src/util/axiom-only-log.ts +124 -0
  262. package/vendor/@anvil/common/src/util/cache-debug.ts +171 -0
  263. package/vendor/@anvil/common/src/util/client-user-agent.ts +114 -0
  264. package/vendor/@anvil/common/src/util/credentials.ts +26 -0
  265. package/vendor/@anvil/common/src/util/currency.ts +25 -0
  266. package/vendor/@anvil/common/src/util/dates.ts +81 -0
  267. package/vendor/@anvil/common/src/util/disposable-email.ts +277 -0
  268. package/vendor/@anvil/common/src/util/engagement-tracker.ts +129 -0
  269. package/vendor/@anvil/common/src/util/env-file-path.ts +40 -0
  270. package/vendor/@anvil/common/src/util/error.ts +562 -0
  271. package/vendor/@anvil/common/src/util/file-read-limits.ts +236 -0
  272. package/vendor/@anvil/common/src/util/file.ts +339 -0
  273. package/vendor/@anvil/common/src/util/format-code-search.ts +115 -0
  274. package/vendor/@anvil/common/src/util/lazy-response-ads.ts +92 -0
  275. package/vendor/@anvil/common/src/util/log-data.ts +57 -0
  276. package/vendor/@anvil/common/src/util/log-ingest.ts +58 -0
  277. package/vendor/@anvil/common/src/util/log-mirror.ts +34 -0
  278. package/vendor/@anvil/common/src/util/lru-cache.ts +67 -0
  279. package/vendor/@anvil/common/src/util/messages.ts +685 -0
  280. package/vendor/@anvil/common/src/util/min-heap.ts +87 -0
  281. package/vendor/@anvil/common/src/util/model-utils.ts +4 -0
  282. package/vendor/@anvil/common/src/util/object.ts +128 -0
  283. package/vendor/@anvil/common/src/util/partial-json-delta.ts +89 -0
  284. package/vendor/@anvil/common/src/util/path.ts +15 -0
  285. package/vendor/@anvil/common/src/util/project-ignore.ts +119 -0
  286. package/vendor/@anvil/common/src/util/promise.ts +73 -0
  287. package/vendor/@anvil/common/src/util/random.ts +15 -0
  288. package/vendor/@anvil/common/src/util/rate-limit.ts +56 -0
  289. package/vendor/@anvil/common/src/util/reddit-anvil-retention.ts +60 -0
  290. package/vendor/@anvil/common/src/util/reddit-capi-events.ts +36 -0
  291. package/vendor/@anvil/common/src/util/response-ad-positions.ts +54 -0
  292. package/vendor/@anvil/common/src/util/saxy.ts +741 -0
  293. package/vendor/@anvil/common/src/util/skills.test.ts +44 -0
  294. package/vendor/@anvil/common/src/util/skills.ts +36 -0
  295. package/vendor/@anvil/common/src/util/split-data.ts +259 -0
  296. package/vendor/@anvil/common/src/util/stop-sequence.ts +61 -0
  297. package/vendor/@anvil/common/src/util/string.ts +426 -0
  298. package/vendor/@anvil/common/src/util/system-info.ts +53 -0
  299. package/vendor/@anvil/common/src/util/thread-title.ts +46 -0
  300. package/vendor/@anvil/common/src/util/ttft-histogram.ts +73 -0
  301. package/vendor/@anvil/common/src/util/xml-parser.ts +27 -0
  302. package/vendor/@anvil/common/src/util/xml.ts +17 -0
  303. package/vendor/@anvil/common/src/util/zod-schema.ts +25 -0
  304. package/vendor/@anvil/common/src/util/zoned-time.ts +136 -0
  305. package/vendor/@anvil/common/src/utils/ask-user-bridge.ts +44 -0
  306. package/vendor/@anvil/sdk/dist/index.cjs +52297 -0
  307. package/vendor/@anvil/sdk/dist/index.cjs.map +440 -0
  308. package/vendor/@anvil/sdk/dist/index.d.ts +4277 -0
  309. package/vendor/@anvil/sdk/dist/index.mjs +52290 -0
  310. package/vendor/@anvil/sdk/dist/index.mjs.map +440 -0
  311. package/vendor/@anvil/sdk/dist/vendor/ai.cjs +30886 -0
  312. package/vendor/@anvil/sdk/dist/vendor/ripgrep/arm64-darwin/rg +0 -0
  313. package/vendor/@anvil/sdk/dist/vendor/ripgrep/arm64-linux/rg +0 -0
  314. package/vendor/@anvil/sdk/dist/vendor/ripgrep/x64-darwin/rg +0 -0
  315. package/vendor/@anvil/sdk/dist/vendor/ripgrep/x64-linux/rg +0 -0
  316. package/vendor/@anvil/sdk/dist/vendor/ripgrep/x64-win32/rg.exe +0 -0
  317. package/vendor/@anvil/sdk/dist/wasm/tree-sitter-c-sharp.wasm +0 -0
  318. package/vendor/@anvil/sdk/dist/wasm/tree-sitter-cpp.wasm +0 -0
  319. package/vendor/@anvil/sdk/dist/wasm/tree-sitter-go.wasm +0 -0
  320. package/vendor/@anvil/sdk/dist/wasm/tree-sitter-java.wasm +0 -0
  321. package/vendor/@anvil/sdk/dist/wasm/tree-sitter-javascript.wasm +0 -0
  322. package/vendor/@anvil/sdk/dist/wasm/tree-sitter-python.wasm +0 -0
  323. package/vendor/@anvil/sdk/dist/wasm/tree-sitter-ruby.wasm +0 -0
  324. package/vendor/@anvil/sdk/dist/wasm/tree-sitter-rust.wasm +0 -0
  325. package/vendor/@anvil/sdk/dist/wasm/tree-sitter-tsx.wasm +0 -0
  326. package/vendor/@anvil/sdk/dist/wasm/tree-sitter-typescript.wasm +0 -0
  327. package/vendor/@anvil/sdk/dist/wasm/tree-sitter.wasm +0 -0
  328. 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>