@alumbwe/anvil 1.0.42 → 1.0.49

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 (238) hide show
  1. package/bin/anvil.js +18 -0
  2. package/package.json +13 -6
  3. package/src/agents/bundled-agents.generated.ts +1 -1
  4. package/src/index.tsx +27 -4
  5. package/src/login/plain-login.ts +60 -0
  6. package/src/login/supabase-auth.ts +346 -0
  7. package/src/utils/anvil-client.ts +4 -0
  8. package/src/utils/auth.ts +16 -4
  9. package/src/utils/config-dir.ts +1 -1
  10. package/vendor/@anvil/common/src/constants/deepseek-direct.ts +30 -1
  11. package/vendor/@anvil/common/src/util/project-ignore.ts +4 -3
  12. package/vendor/@anvil/sdk/dist/index.cjs +46 -10
  13. package/vendor/@anvil/sdk/dist/index.mjs +46 -10
  14. package/src/commands/__tests__/anvil-command-aliases.test.ts +0 -56
  15. package/src/commands/__tests__/bash-command.test.ts +0 -445
  16. package/src/commands/__tests__/command-args.test.ts +0 -349
  17. package/src/commands/__tests__/copy-conversation-clipboard.test.ts +0 -108
  18. package/src/commands/__tests__/copy-conversation.test.ts +0 -226
  19. package/src/commands/__tests__/diagnostics-command.test.ts +0 -35
  20. package/src/commands/__tests__/image.test.ts +0 -99
  21. package/src/commands/__tests__/init.test.ts +0 -456
  22. package/src/commands/__tests__/new-command-rotates-chat.test.ts +0 -91
  23. package/src/commands/__tests__/process-diagnostics.test.ts +0 -54
  24. package/src/commands/__tests__/prompt-builders.test.ts +0 -28
  25. package/src/commands/__tests__/router-input.test.ts +0 -298
  26. package/src/components/__tests__/ad-banner.test.tsx +0 -83
  27. package/src/components/__tests__/anvil-landing-heading-row.test.tsx +0 -91
  28. package/src/components/__tests__/anvil-model-selector.test.tsx +0 -519
  29. package/src/components/__tests__/anvil-offer-invariants.test.ts +0 -54
  30. package/src/components/__tests__/anvil-takeover-prompt.test.tsx +0 -212
  31. package/src/components/__tests__/chat-input-bar.test.tsx +0 -130
  32. package/src/components/__tests__/grid-layout.integration.test.tsx +0 -304
  33. package/src/components/__tests__/grid-layout.test.tsx +0 -1051
  34. package/src/components/__tests__/message-block.completion.test.tsx +0 -104
  35. package/src/components/__tests__/message-block.streaming.test.tsx +0 -78
  36. package/src/components/__tests__/message-with-agents.test.tsx +0 -579
  37. package/src/components/__tests__/multiline-input.test.tsx +0 -1132
  38. package/src/components/__tests__/queue-panel.test.tsx +0 -398
  39. package/src/components/__tests__/selectable-list.test.ts +0 -232
  40. package/src/components/__tests__/status-bar.test.tsx +0 -44
  41. package/src/components/__tests__/status-indicator.test.tsx +0 -204
  42. package/src/components/__tests__/terminal-thinking-orb.test.tsx +0 -111
  43. package/src/components/__tests__/user-error-banner.test.tsx +0 -102
  44. package/src/components/ask-user/__tests__/multiple-choice-form.test.ts +0 -432
  45. package/src/components/ask-user/__tests__/validation.test.ts +0 -213
  46. package/src/components/tools/__tests__/apply-patch.test.tsx +0 -84
  47. package/src/components/tools/__tests__/code-search.test.tsx +0 -45
  48. package/src/components/tools/__tests__/gravity-index.test.ts +0 -76
  49. package/src/components/tools/__tests__/render-ui.test.tsx +0 -92
  50. package/src/components/tools/__tests__/run-terminal-command.test.ts +0 -233
  51. package/src/contexts/__tests__/chat-runtime-context.test.tsx +0 -149
  52. package/src/data/slash-commands.test.ts +0 -24
  53. package/src/hooks/__tests__/holds-live-anvil-slot.test.ts +0 -49
  54. package/src/hooks/__tests__/session-fetch-signal.test.ts +0 -63
  55. package/src/hooks/__tests__/use-activity-query.test.ts +0 -1000
  56. package/src/hooks/__tests__/use-ask-user-bridge.test.ts +0 -176
  57. package/src/hooks/__tests__/use-auth-query.test.ts +0 -21
  58. package/src/hooks/__tests__/use-chat-input-remount.test.tsx +0 -66
  59. package/src/hooks/__tests__/use-clipboard.test.tsx +0 -73
  60. package/src/hooks/__tests__/use-connection-status.test.ts +0 -112
  61. package/src/hooks/__tests__/use-directory-browser.test.ts +0 -203
  62. package/src/hooks/__tests__/use-gravity-ad.test.ts +0 -73
  63. package/src/hooks/__tests__/use-grid-layout.test.ts +0 -349
  64. package/src/hooks/__tests__/use-input-history.test.ts +0 -699
  65. package/src/hooks/__tests__/use-message-queue-editing.test.tsx +0 -209
  66. package/src/hooks/__tests__/use-message-queue.test.tsx +0 -71
  67. package/src/hooks/__tests__/use-path-tab-completion.test.ts +0 -315
  68. package/src/hooks/__tests__/use-queue-controls.test.ts +0 -68
  69. package/src/hooks/__tests__/use-queue-ui.test.tsx +0 -83
  70. package/src/hooks/__tests__/use-searchable-list.test.ts +0 -359
  71. package/src/hooks/__tests__/use-send-message-timer.test.ts +0 -146
  72. package/src/hooks/__tests__/use-suggestion-engine-mention.test.ts +0 -361
  73. package/src/hooks/__tests__/use-suggestion-engine.test.ts +0 -485
  74. package/src/hooks/__tests__/use-terminal-focus.test.ts +0 -66
  75. package/src/hooks/__tests__/use-terminal-layout.test.ts +0 -675
  76. package/src/hooks/__tests__/use-timeout.test.ts +0 -330
  77. package/src/hooks/__tests__/use-usage-query.test.ts +0 -502
  78. package/src/hooks/__tests__/use-user-details-query.test.ts +0 -195
  79. package/src/hooks/helpers/__tests__/send-message.test.ts +0 -1918
  80. package/src/init/__tests__/init-direnv.test.ts +0 -526
  81. package/src/state/__tests__/chat-store-focus.test.ts +0 -22
  82. package/src/state/__tests__/feedback-store.test.ts +0 -257
  83. package/src/utils/__tests__/active-run.test.ts +0 -140
  84. package/src/utils/__tests__/activity-tracker.test.ts +0 -440
  85. package/src/utils/__tests__/agent-display.test.ts +0 -139
  86. package/src/utils/__tests__/analytics-client.test.ts +0 -262
  87. package/src/utils/__tests__/anonymous-id.test.ts +0 -74
  88. package/src/utils/__tests__/anvil-api.test.ts +0 -578
  89. package/src/utils/__tests__/anvil-instance-owner.test.ts +0 -69
  90. package/src/utils/__tests__/anvil-model-navigation.test.ts +0 -102
  91. package/src/utils/__tests__/anvil-premium-reset.test.ts +0 -79
  92. package/src/utils/__tests__/anvil-referral-cache.test.ts +0 -116
  93. package/src/utils/__tests__/anvil-session-api.test.ts +0 -215
  94. package/src/utils/__tests__/anvil-session-display.test.ts +0 -23
  95. package/src/utils/__tests__/anvil-streak-line.test.ts +0 -217
  96. package/src/utils/__tests__/arrays.test.ts +0 -264
  97. package/src/utils/__tests__/bash-context-processor.test.ts +0 -38
  98. package/src/utils/__tests__/block-processor.test.ts +0 -810
  99. package/src/utils/__tests__/chat-history.test.ts +0 -203
  100. package/src/utils/__tests__/chat-input-key-intercept.test.ts +0 -40
  101. package/src/utils/__tests__/chat-meta.test.ts +0 -130
  102. package/src/utils/__tests__/clipboard-linux.test.ts +0 -359
  103. package/src/utils/__tests__/clipboard.test.ts +0 -910
  104. package/src/utils/__tests__/code-search-summary.test.ts +0 -84
  105. package/src/utils/__tests__/collapse-helpers.test.ts +0 -1109
  106. package/src/utils/__tests__/error-handling.test.ts +0 -658
  107. package/src/utils/__tests__/exit-cleanly.test.ts +0 -110
  108. package/src/utils/__tests__/feedback-helpers.test.ts +0 -444
  109. package/src/utils/__tests__/feedback-submission.test.ts +0 -26
  110. package/src/utils/__tests__/fetch-usage.test.ts +0 -355
  111. package/src/utils/__tests__/fingerprint.test.ts +0 -144
  112. package/src/utils/__tests__/fixtures/terminal-command-broker-child.ts +0 -7
  113. package/src/utils/__tests__/fixtures/terminal-command-broker-entry.ts +0 -3
  114. package/src/utils/__tests__/fixtures/terminal-command-broker-owner.ts +0 -33
  115. package/src/utils/__tests__/format-elapsed-time.test.ts +0 -103
  116. package/src/utils/__tests__/format-timeout.test.ts +0 -87
  117. package/src/utils/__tests__/image-dimensions.test.ts +0 -249
  118. package/src/utils/__tests__/image-processor.test.ts +0 -209
  119. package/src/utils/__tests__/implementor-helpers.test.ts +0 -1458
  120. package/src/utils/__tests__/keyboard-actions.test.ts +0 -660
  121. package/src/utils/__tests__/layout-helpers.test.ts +0 -36
  122. package/src/utils/__tests__/lazy-response-ads.test.ts +0 -167
  123. package/src/utils/__tests__/log-shipper.test.ts +0 -53
  124. package/src/utils/__tests__/markdown-renderer.test.tsx +0 -421
  125. package/src/utils/__tests__/message-block-helpers.test.ts +0 -1263
  126. package/src/utils/__tests__/message-updater.test.ts +0 -645
  127. package/src/utils/__tests__/osc-timeout-scenarios.test.ts +0 -172
  128. package/src/utils/__tests__/pending-attachments.test.ts +0 -277
  129. package/src/utils/__tests__/polling-backoff.test.ts +0 -51
  130. package/src/utils/__tests__/queue-panel-actions.test.ts +0 -148
  131. package/src/utils/__tests__/run-state-storage.test.ts +0 -919
  132. package/src/utils/__tests__/safe-json.test.ts +0 -77
  133. package/src/utils/__tests__/sdk-event-handlers.test.ts +0 -685
  134. package/src/utils/__tests__/send-message-helpers.test.ts +0 -1898
  135. package/src/utils/__tests__/send-message-timer.test.ts +0 -64
  136. package/src/utils/__tests__/settings.test.ts +0 -99
  137. package/src/utils/__tests__/strings.test.ts +0 -192
  138. package/src/utils/__tests__/terminal-color-detection.test.ts +0 -425
  139. package/src/utils/__tests__/terminal-command-broker.test.ts +0 -528
  140. package/src/utils/__tests__/terminal-enter-detection.test.ts +0 -77
  141. package/src/utils/__tests__/terminal-io.test.ts +0 -137
  142. package/src/utils/__tests__/terminal-protocol-controller.test.ts +0 -119
  143. package/src/utils/__tests__/text-layout.test.ts +0 -82
  144. package/src/utils/__tests__/theme-platform-detection.test.ts +0 -125
  145. package/src/utils/__tests__/think-tag-parser.test.ts +0 -159
  146. package/src/utils/__tests__/trace-writer.test.ts +0 -223
  147. package/src/utils/__tests__/trim-chat-logs.test.ts +0 -78
  148. package/src/utils/__tests__/usage-banner-state.test.ts +0 -298
  149. package/src/utils/__tests__/validation-error-formatting.test.ts +0 -126
  150. package/src/utils/__tests__/windows-terminal-health.test.ts +0 -101
  151. package/src/utils/__tests__/write-file-atomic.test.ts +0 -127
  152. package/vendor/@anvil/common/src/__tests__/agent-validation.test.ts +0 -846
  153. package/vendor/@anvil/common/src/__tests__/anvil-model-availability.test.ts +0 -101
  154. package/vendor/@anvil/common/src/__tests__/anvil-models.test.ts +0 -1408
  155. package/vendor/@anvil/common/src/__tests__/anvil-public-data-use-copy.test.ts +0 -258
  156. package/vendor/@anvil/common/src/__tests__/anvil-referral-tiers.test.ts +0 -48
  157. package/vendor/@anvil/common/src/__tests__/anvil-spend-ceilings.test.ts +0 -282
  158. package/vendor/@anvil/common/src/__tests__/anvil-trust.test.ts +0 -526
  159. package/vendor/@anvil/common/src/__tests__/deepseek-direct.test.ts +0 -68
  160. package/vendor/@anvil/common/src/__tests__/disposable-email.test.ts +0 -132
  161. package/vendor/@anvil/common/src/__tests__/dynamic-agent-template-schema.test.ts +0 -422
  162. package/vendor/@anvil/common/src/__tests__/env-ci.test.ts +0 -167
  163. package/vendor/@anvil/common/src/__tests__/env-process.test.ts +0 -145
  164. package/vendor/@anvil/common/src/__tests__/foreign-client-shipped-agents.test.ts +0 -234
  165. package/vendor/@anvil/common/src/__tests__/foreign-client-signals.test.ts +0 -379
  166. package/vendor/@anvil/common/src/__tests__/free-agents.test.ts +0 -807
  167. package/vendor/@anvil/common/src/__tests__/gravity-capi.test.ts +0 -154
  168. package/vendor/@anvil/common/src/__tests__/handlesteps-parsing.test.ts +0 -246
  169. package/vendor/@anvil/common/src/__tests__/kimi-k3-god-only.test.ts +0 -77
  170. package/vendor/@anvil/common/src/__tests__/model-config.test.ts +0 -89
  171. package/vendor/@anvil/common/src/__tests__/project-file-tree.test.ts +0 -298
  172. package/vendor/@anvil/common/src/__tests__/provisioned-model-tiers.test.ts +0 -116
  173. package/vendor/@anvil/common/src/__tests__/reasoning-effort.test.ts +0 -167
  174. package/vendor/@anvil/common/src/__tests__/reddit-capi.test.ts +0 -132
  175. package/vendor/@anvil/common/src/__tests__/response-ad-positions.test.ts +0 -72
  176. package/vendor/@anvil/common/src/__tests__/user-state.test.ts +0 -30
  177. package/vendor/@anvil/common/src/constants/__tests__/anvil-onboarding-gate.test.ts +0 -63
  178. package/vendor/@anvil/common/src/constants/__tests__/anvil-onboarding.test.ts +0 -246
  179. package/vendor/@anvil/common/src/constants/__tests__/cf-worker-signals.test.ts +0 -106
  180. package/vendor/@anvil/common/src/testing/TESTING_PATTERNS.md +0 -351
  181. package/vendor/@anvil/common/src/testing/anvil-offer-invariants.ts +0 -169
  182. package/vendor/@anvil/common/src/testing/errors.ts +0 -33
  183. package/vendor/@anvil/common/src/testing/fixtures/agent-runtime.ts +0 -334
  184. package/vendor/@anvil/common/src/testing/impl/agent-runtime.ts +0 -6
  185. package/vendor/@anvil/common/src/testing/index.ts +0 -84
  186. package/vendor/@anvil/common/src/testing/mock-modules.ts +0 -53
  187. package/vendor/@anvil/common/src/testing/mock-types.ts +0 -123
  188. package/vendor/@anvil/common/src/testing/mocks/analytics.ts +0 -261
  189. package/vendor/@anvil/common/src/testing/mocks/child-process.ts +0 -93
  190. package/vendor/@anvil/common/src/testing/mocks/crypto.ts +0 -218
  191. package/vendor/@anvil/common/src/testing/mocks/database.ts +0 -337
  192. package/vendor/@anvil/common/src/testing/mocks/fetch.ts +0 -219
  193. package/vendor/@anvil/common/src/testing/mocks/filesystem.ts +0 -166
  194. package/vendor/@anvil/common/src/testing/mocks/index.ts +0 -101
  195. package/vendor/@anvil/common/src/testing/mocks/logger.ts +0 -135
  196. package/vendor/@anvil/common/src/testing/mocks/stream.ts +0 -313
  197. package/vendor/@anvil/common/src/testing/mocks/timers.ts +0 -132
  198. package/vendor/@anvil/common/src/testing/mocks/tree-sitter.ts +0 -127
  199. package/vendor/@anvil/common/src/testing/setup.ts +0 -282
  200. package/vendor/@anvil/common/src/tools/__tests__/compile-tool-definitions.test.ts +0 -34
  201. package/vendor/@anvil/common/src/tools/params/__tests__/coerce-to-array.test.ts +0 -213
  202. package/vendor/@anvil/common/src/tools/params/__tests__/read-docs.test.ts +0 -47
  203. package/vendor/@anvil/common/src/tools/params/__tests__/render-ui.test.ts +0 -65
  204. package/vendor/@anvil/common/src/tools/params/tool/__tests__/run-terminal-command-timeout.test.ts +0 -49
  205. package/vendor/@anvil/common/src/types/__tests__/dynamic-agent-template.test.ts +0 -20
  206. package/vendor/@anvil/common/src/util/__tests__/ad-user-agent.test.ts +0 -26
  207. package/vendor/@anvil/common/src/util/__tests__/analytics-dispatcher.test.ts +0 -122
  208. package/vendor/@anvil/common/src/util/__tests__/analytics-log.test.ts +0 -102
  209. package/vendor/@anvil/common/src/util/__tests__/analytics-sampling.test.ts +0 -143
  210. package/vendor/@anvil/common/src/util/__tests__/anvil-streak.test.ts +0 -202
  211. package/vendor/@anvil/common/src/util/__tests__/anvil-usage-summary.test.ts +0 -193
  212. package/vendor/@anvil/common/src/util/__tests__/axiom-only-log.test.ts +0 -140
  213. package/vendor/@anvil/common/src/util/__tests__/client-user-agent.test.ts +0 -148
  214. package/vendor/@anvil/common/src/util/__tests__/engagement-tracker.test.ts +0 -115
  215. package/vendor/@anvil/common/src/util/__tests__/env-file-path.test.ts +0 -57
  216. package/vendor/@anvil/common/src/util/__tests__/error-abort.test.ts +0 -774
  217. package/vendor/@anvil/common/src/util/__tests__/error-api-details.test.ts +0 -206
  218. package/vendor/@anvil/common/src/util/__tests__/file-read-limits.test.ts +0 -173
  219. package/vendor/@anvil/common/src/util/__tests__/format-code-search.test.ts +0 -60
  220. package/vendor/@anvil/common/src/util/__tests__/log-mirror.test.ts +0 -31
  221. package/vendor/@anvil/common/src/util/__tests__/messages.test.ts +0 -1252
  222. package/vendor/@anvil/common/src/util/__tests__/partial-json-delta.test.ts +0 -505
  223. package/vendor/@anvil/common/src/util/__tests__/path.test.ts +0 -22
  224. package/vendor/@anvil/common/src/util/__tests__/project-ignore.test.ts +0 -90
  225. package/vendor/@anvil/common/src/util/__tests__/promise.test.ts +0 -325
  226. package/vendor/@anvil/common/src/util/__tests__/rate-limit.test.ts +0 -28
  227. package/vendor/@anvil/common/src/util/__tests__/reddit-anvil-retention.test.ts +0 -130
  228. package/vendor/@anvil/common/src/util/__tests__/saxy.test.ts +0 -1008
  229. package/vendor/@anvil/common/src/util/__tests__/split-data.test.ts +0 -289
  230. package/vendor/@anvil/common/src/util/__tests__/string.test.ts +0 -239
  231. package/vendor/@anvil/common/src/util/__tests__/thread-title.test.ts +0 -70
  232. package/vendor/@anvil/common/src/util/__tests__/tool-result-media-order.test.ts +0 -108
  233. package/vendor/@anvil/common/src/util/__tests__/ttft-histogram.test.ts +0 -180
  234. package/vendor/@anvil/common/src/util/__tests__/with-timeout.test.ts +0 -24
  235. package/vendor/@anvil/common/src/util/__tests__/zoned-time.test.ts +0 -88
  236. package/vendor/@anvil/common/src/util/skills.test.ts +0 -44
  237. package/vendor/@anvil/sdk/dist/index.cjs.map +0 -440
  238. package/vendor/@anvil/sdk/dist/index.mjs.map +0 -440
package/bin/anvil.js CHANGED
@@ -26,6 +26,24 @@ const PKG_ROOT = path.resolve(HERE, '..')
26
26
  const ENTRY = path.join(PKG_ROOT, 'src', 'entry.ts')
27
27
  const IS_WIN = process.platform === 'win32'
28
28
  const BUN_EXE = IS_WIN ? 'bun.exe' : 'bun'
29
+ const COMPILED_BINARY = IS_WIN
30
+ ? path.join(HERE, 'anvil.exe')
31
+ : path.join(HERE, 'anvil')
32
+
33
+ // Release installs include a compiled binary. Prefer it so the global npm
34
+ // shim runs the same build as `cli/bin/anvil.exe` and does not require Bun.
35
+ if (!process.versions.bun && fs.existsSync(COMPILED_BINARY)) {
36
+ const result = spawnSync(COMPILED_BINARY, process.argv.slice(2), {
37
+ stdio: 'inherit',
38
+ env: process.env,
39
+ windowsHide: true,
40
+ })
41
+ if (result.error) {
42
+ console.error(`Failed to start compiled Anvil (${COMPILED_BINARY}):`, result.error.message)
43
+ process.exit(1)
44
+ }
45
+ process.exit(result.status ?? (result.signal ? 1 : 0))
46
+ }
29
47
 
30
48
  /** Check that a candidate command is a working bun executable. */
31
49
  function isBun(cmd) {
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@alumbwe/anvil",
3
- "version": "1.0.42",
3
+ "version": "1.0.49",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
7
- "anvil": "./bin/anvil.js"
7
+ "anvil": "bin/anvil.js"
8
8
  },
9
9
  "files": [
10
10
  "bin/anvil.js",
@@ -13,10 +13,16 @@
13
13
  "vendor/",
14
14
  "tsconfig.json",
15
15
  "bunfig.toml",
16
- "!src/__tests__/",
17
- "!src/testing/",
18
- "!src/smoke/",
19
- "!src/dist-private/"
16
+ "!**/__tests__/**",
17
+ "!**/__mocks__/**",
18
+ "!**/__fixtures__/**",
19
+ "!**/*.test.*",
20
+ "!**/*.spec.*",
21
+ "!**/testing/**",
22
+ "!**/smoke/**",
23
+ "!src/dist-private/**",
24
+ "!**/*.map",
25
+ "!**/*.tsbuildinfo"
20
26
  ],
21
27
  "repository": {
22
28
  "type": "git",
@@ -63,6 +69,7 @@
63
69
  "node-machine-id": "^1.1.12",
64
70
  "open": "^10.1.0",
65
71
  "partial-json": "^0.1.7",
72
+ "picocolors": "^1.1.1",
66
73
  "pino": "9.4.0",
67
74
  "posthog-node": "^5.8.0",
68
75
  "react": "^19.0.0",
@@ -4,7 +4,7 @@
4
4
  * This file is generated by scripts/prebuild-agents.ts
5
5
  * It contains all bundled agent definitions from the agents/ directory.
6
6
  *
7
- * Generated at: 2026-09-11T11:23:27.005Z
7
+ * Generated at: 2026-11-01T15:44:55.857Z
8
8
  * Agent count: 80
9
9
  */
10
10
 
package/src/index.tsx CHANGED
@@ -27,6 +27,8 @@ import { App } from './app'
27
27
  import { loadPackageVersion, parseArgs } from './cli-args'
28
28
  import { handlePublish } from './commands/publish'
29
29
  import { runPlainLogin } from './login/plain-login'
30
+ import { ensureSignedIn } from './login/supabase-auth'
31
+ import { isManagedGatewayMode } from '@anvil/common/constants/deepseek-direct'
30
32
  import { initializeApp } from './init/init-app'
31
33
  import { loadEnvLocal } from './setup/load-env'
32
34
  import { runKeySetup } from './setup/key-setup'
@@ -248,19 +250,40 @@ async function main(): Promise<void> {
248
250
  process.exit(0)
249
251
  }
250
252
 
251
- // First-run: if required API keys are missing, launch the setup wizard.
252
- // This runs BEFORE the TUI renderer so it can use plain console I/O.
253
- if (!process.env.DEEPSEEK_API_KEY || !process.env.GEMINI_API_KEY) {
253
+ // First-run: required local API keys. In managed gateway mode the DeepSeek
254
+ // (model) key lives on the SERVER — the gateway injects it — so the CLI must
255
+ // Managed mode does not require local provider keys. In direct development
256
+ // mode both provider keys are required locally.
257
+ const gatewayMode = isManagedGatewayMode()
258
+ // Managed mode keeps both model keys in Supabase; an installed CLI only
259
+ // needs its stored Supabase access token. Local provider keys are required
260
+ // only for direct development mode.
261
+ if (!gatewayMode && (!process.env.DEEPSEEK_API_KEY || !process.env.GEMINI_API_KEY)) {
254
262
  const result = await runKeySetup(process.cwd())
255
263
  if (!result.allRequired) {
256
264
  console.error(
257
- '\n DeepSeek and Gemini API keys are required.\n' +
265
+ '\n Required API keys are missing.\n' +
258
266
  ' Run with `setup` to reconfigure, or set them in .env.local manually.\n',
259
267
  )
260
268
  process.exit(1)
261
269
  }
262
270
  }
263
271
 
272
+ // Managed gateway mode: `anvil` recognizes whether you're signed in and, if
273
+ // not, launches the browser sign-in before starting. (Direct mode and the
274
+ // login/setup commands are handled above and need no gate here.)
275
+ if (gatewayMode && !isPublishCommand) {
276
+ try {
277
+ await ensureSignedIn()
278
+ setApiClientAuthToken(getAuthToken())
279
+ } catch {
280
+ console.error(
281
+ '\n Sign-in required to use Anvil. Run `anvil login` and try again.\n',
282
+ )
283
+ process.exit(1)
284
+ }
285
+ }
286
+
264
287
  // Show project picker only when user starts at the home directory or an ancestor
265
288
  const projectRoot = getProjectRoot()
266
289
  const homeDir = os.homedir()
@@ -1,8 +1,14 @@
1
1
  import { AnalyticsEvent } from '@anvil/common/constants/analytics-events'
2
+ import { isManagedGatewayMode } from '@anvil/common/constants/deepseek-direct'
2
3
  import { cyan, green, red, yellow, bold } from 'picocolors'
3
4
 
4
5
  import { LOGIN_WEBSITE_URL } from './constants'
5
6
  import { generateLoginUrl, pollLoginStatus } from './login-flow'
7
+ import {
8
+ isSupabaseAuthConfigured,
9
+ loginViaBrowser,
10
+ signInWithPassword,
11
+ } from './supabase-auth'
6
12
  import {
7
13
  flushAnalytics,
8
14
  identifyUser,
@@ -23,7 +29,61 @@ import type { User } from '../utils/auth'
23
29
  * This is the escape hatch for remote/SSH environments where the TUI's
24
30
  * clipboard and browser integration don't work.
25
31
  */
32
+ /**
33
+ * Managed gateway mode: authenticate with Supabase (the cli-agent gateway
34
+ * verifies a Supabase JWT). Opens the browser to the hosted sign-in page and
35
+ * captures the session via a local loopback callback. Non-interactive when
36
+ * ANVIL_LOGIN_EMAIL + ANVIL_LOGIN_PASSWORD are set.
37
+ */
38
+ export async function runSupabaseLogin(): Promise<void> {
39
+ console.log()
40
+ console.log(bold('Anvil Login'))
41
+ console.log()
42
+
43
+ const envEmail = process.env.ANVIL_LOGIN_EMAIL
44
+ const envPassword = process.env.ANVIL_LOGIN_PASSWORD
45
+
46
+ try {
47
+ let user
48
+ if (envEmail && envPassword) {
49
+ console.log(`Signing in as ${envEmail}...`)
50
+ user = await signInWithPassword(envEmail, envPassword)
51
+ } else {
52
+ console.log('Waiting for you to finish signing in in your browser…')
53
+ user = await loginViaBrowser()
54
+ }
55
+
56
+ if (user.id) {
57
+ identifyUser(user.id, { email: user.email, anvil: IS_ANVIL })
58
+ trackEvent(AnalyticsEvent.LOGIN, {
59
+ userId: user.id,
60
+ via: 'plain_command',
61
+ hasEmail: Boolean(user.email),
62
+ hasName: Boolean(user.name),
63
+ })
64
+ await flushAnalytics()
65
+ }
66
+
67
+ console.log()
68
+ console.log(green(`✓ Logged in as ${user.name} (${user.email})`))
69
+ console.log()
70
+ console.log('You can now run ' + cyan('anvil') + ' to start.')
71
+ process.exit(0)
72
+ } catch (error) {
73
+ console.error(
74
+ red(`Login failed: ${error instanceof Error ? error.message : String(error)}`),
75
+ )
76
+ process.exit(1)
77
+ }
78
+ }
79
+
26
80
  export async function runPlainLogin(): Promise<void> {
81
+ // Managed gateway mode uses Supabase auth (the gateway verifies a Supabase
82
+ // JWT), not the Anvil device-code flow.
83
+ if (isManagedGatewayMode() && isSupabaseAuthConfigured()) {
84
+ return runSupabaseLogin()
85
+ }
86
+
27
87
  const fingerprintId = await getFingerprintId()
28
88
 
29
89
  console.log()
@@ -0,0 +1,346 @@
1
+ /**
2
+ * supabase-auth.ts — CLI authentication for managed gateway mode.
3
+ *
4
+ * The cli-agent gateway authenticates each request with a Supabase JWT
5
+ * (`sb.auth.getUser(token)`). So in managed mode the CLI must hold a Supabase
6
+ * session, not an Anvil API key. This module talks to Supabase's GoTrue REST
7
+ * API directly (no extra dependency), stores the session in the normal
8
+ * credentials file as `authToken` (= access token) plus the refresh token and
9
+ * expiry, and refreshes the access token before it expires.
10
+ *
11
+ * Supported sign-in methods:
12
+ * - email OTP: requestEmailOtp() → verifyEmailOtp()
13
+ * - email + password: signInWithPassword()
14
+ */
15
+
16
+ import http from 'node:http'
17
+
18
+ import open from 'open'
19
+
20
+ import { logger } from '../utils/logger'
21
+ import { getUserCredentials, saveUserCredentials, type User } from '../utils/auth'
22
+
23
+ /** GoTrue session as returned by /token and /verify. */
24
+ interface SupabaseSession {
25
+ access_token: string
26
+ refresh_token: string
27
+ expires_at?: number
28
+ expires_in?: number
29
+ user?: {
30
+ id?: string
31
+ email?: string
32
+ user_metadata?: { name?: string; full_name?: string }
33
+ }
34
+ }
35
+
36
+ function supabaseUrl(): string | undefined {
37
+ const url = process.env.SUPABASE_URL ?? process.env.NEXT_PUBLIC_SUPABASE_URL
38
+ return url ? url.replace(/\/+$/, '') : undefined
39
+ }
40
+
41
+ /** The anon/publishable key — GoTrue requires it as the `apikey` header. */
42
+ function supabaseAnonKey(): string | undefined {
43
+ return (
44
+ process.env.SUPABASE_PUBLISHABLE_KEY ??
45
+ process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY ??
46
+ process.env.SUPABASE_ANON_KEY ??
47
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
48
+ )
49
+ }
50
+
51
+ /** True when Supabase auth can run (URL + anon key configured). */
52
+ export function isSupabaseAuthConfigured(): boolean {
53
+ return Boolean(supabaseUrl() && supabaseAnonKey())
54
+ }
55
+
56
+ function authConfig(): { url: string; anonKey: string } {
57
+ const url = supabaseUrl()
58
+ const anonKey = supabaseAnonKey()
59
+ if (!url || !anonKey) {
60
+ throw new Error(
61
+ 'Supabase is not configured. Set SUPABASE_URL and SUPABASE_PUBLISHABLE_KEY.',
62
+ )
63
+ }
64
+ return { url, anonKey }
65
+ }
66
+
67
+ async function gotrue(
68
+ path: string,
69
+ body: Record<string, unknown>,
70
+ ): Promise<SupabaseSession> {
71
+ const { url, anonKey } = authConfig()
72
+ const res = await fetch(`${url}/auth/v1${path}`, {
73
+ method: 'POST',
74
+ headers: {
75
+ 'Content-Type': 'application/json',
76
+ apikey: anonKey,
77
+ Authorization: `Bearer ${anonKey}`,
78
+ },
79
+ body: JSON.stringify(body),
80
+ })
81
+ const text = await res.text()
82
+ let json: unknown
83
+ try {
84
+ json = text ? JSON.parse(text) : {}
85
+ } catch {
86
+ json = {}
87
+ }
88
+ if (!res.ok) {
89
+ const message =
90
+ (json as { error_description?: string; msg?: string; error?: string })
91
+ ?.error_description ??
92
+ (json as { msg?: string }).msg ??
93
+ (json as { error?: string }).error ??
94
+ `Supabase auth failed (${res.status})`
95
+ throw new Error(message)
96
+ }
97
+ return json as SupabaseSession
98
+ }
99
+
100
+ /** Normalize a session's expiry to epoch seconds. */
101
+ function expiresAtOf(session: SupabaseSession): number {
102
+ if (typeof session.expires_at === 'number') return session.expires_at
103
+ const ttl = typeof session.expires_in === 'number' ? session.expires_in : 3600
104
+ return Math.floor(Date.now() / 1000) + ttl
105
+ }
106
+
107
+ /** Map a Supabase session to the credentials `User` shape and persist it. */
108
+ export function saveSupabaseSession(session: SupabaseSession): User {
109
+ const meta = session.user?.user_metadata
110
+ const user: User = {
111
+ id: session.user?.id,
112
+ email: session.user?.email ?? '',
113
+ name: meta?.name ?? meta?.full_name ?? session.user?.email ?? 'Anvil user',
114
+ authToken: session.access_token,
115
+ refreshToken: session.refresh_token,
116
+ expiresAt: expiresAtOf(session),
117
+ provider: 'supabase',
118
+ }
119
+ saveUserCredentials(user)
120
+ return user
121
+ }
122
+
123
+ /** Send an email OTP (6-digit code / magic link) for sign-in or signup. */
124
+ export async function requestEmailOtp(email: string): Promise<void> {
125
+ await gotrue('/otp', { email, create_user: true })
126
+ }
127
+
128
+ /** Verify an email OTP code and persist the resulting session. */
129
+ export async function verifyEmailOtp(email: string, token: string): Promise<User> {
130
+ const session = await gotrue('/verify', { type: 'email', email, token })
131
+ return saveSupabaseSession(session)
132
+ }
133
+
134
+ /** Sign in with email + password and persist the resulting session. */
135
+ export async function signInWithPassword(
136
+ email: string,
137
+ password: string,
138
+ ): Promise<User> {
139
+ const session = await gotrue('/token?grant_type=password', { email, password })
140
+ return saveSupabaseSession(session)
141
+ }
142
+
143
+ /** Exchange a refresh token for a new session and persist it. */
144
+ async function refreshSupabaseSession(refreshToken: string): Promise<User> {
145
+ const session = await gotrue('/token?grant_type=refresh_token', {
146
+ refresh_token: refreshToken,
147
+ })
148
+ return saveSupabaseSession(session)
149
+ }
150
+
151
+ // ── Browser sign-in (loopback) ─────────────────────────────────────────────
152
+
153
+ /** Hosted sign-in page that completes auth in the browser, then redirects the
154
+ * session back to the CLI's loopback callback. Override with ANVIL_LOGIN_URL. */
155
+ export function getLoginPageUrl(): string {
156
+ return (
157
+ process.env.ANVIL_LOGIN_URL ?? 'https://cli.tryanvil.dev/login'
158
+ ).replace(/\/+$/, '')
159
+ }
160
+
161
+ /** Decode a JWT payload (no verification — only for display id/email). */
162
+ function decodeJwt(token: string): { sub?: string; email?: string } {
163
+ try {
164
+ const part = token.split('.')[1] ?? ''
165
+ const json = Buffer.from(
166
+ part.replace(/-/g, '+').replace(/_/g, '/'),
167
+ 'base64',
168
+ ).toString('utf8')
169
+ return JSON.parse(json)
170
+ } catch {
171
+ return {}
172
+ }
173
+ }
174
+
175
+ /** Served on the loopback /callback: reads the URL fragment and posts the
176
+ * tokens back to this same server (same-origin, so no mixed-content issues). */
177
+ function callbackHtml(): string {
178
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" />
179
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
180
+ <title>Anvil — signing in</title>
181
+ <style>body{margin:0;height:100vh;display:grid;place-items:center;background:#0a0a0a;color:#f5f5f5;font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}div{text-align:center}p{color:#a0a0a0}</style>
182
+ </head><body><div><h2>Signing you in…</h2><p id="m">One moment.</p></div>
183
+ <script>
184
+ (function(){
185
+ var p = new URLSearchParams(location.hash.replace(/^#/,''));
186
+ var body = {access_token:p.get('access_token'),refresh_token:p.get('refresh_token'),expires_at:p.get('expires_at')};
187
+ if(!body.access_token){document.getElementById('m').textContent='No token found. Return to the terminal and run anvil login again.';return;}
188
+ fetch('/token',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(body)})
189
+ .then(function(){document.getElementById('m').textContent='You are signed in. You can close this tab and return to your terminal.';})
190
+ .catch(function(){document.getElementById('m').textContent='Could not reach the CLI. Is "anvil login" still running?';});
191
+ })();
192
+ </script></body></html>`
193
+ }
194
+
195
+ /**
196
+ * Open the browser to the hosted sign-in page and capture the session via a
197
+ * local loopback callback (the gh/vercel-style flow). Resolves once the
198
+ * browser posts back the Supabase session, which is then persisted.
199
+ */
200
+ export async function loginViaBrowser(timeoutMs = 300_000): Promise<User> {
201
+ if (!isSupabaseAuthConfigured()) {
202
+ throw new Error(
203
+ 'Supabase is not configured. Set SUPABASE_URL and SUPABASE_PUBLISHABLE_KEY.',
204
+ )
205
+ }
206
+
207
+ return new Promise<User>((resolve, reject) => {
208
+ let settled = false
209
+ const server = http.createServer((req, res) => {
210
+ const url = new URL(req.url ?? '/', 'http://127.0.0.1')
211
+ if (req.method === 'GET' && url.pathname === '/callback') {
212
+ res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' })
213
+ res.end(callbackHtml())
214
+ return
215
+ }
216
+ if (req.method === 'POST' && url.pathname === '/token') {
217
+ let data = ''
218
+ req.on('data', (c) => {
219
+ data += c
220
+ if (data.length > 1_000_000) req.destroy()
221
+ })
222
+ req.on('end', () => {
223
+ try {
224
+ const parsed = JSON.parse(data || '{}') as {
225
+ access_token?: string
226
+ refresh_token?: string
227
+ expires_at?: string
228
+ }
229
+ if (!parsed.access_token) {
230
+ res.writeHead(400)
231
+ res.end('{"error":"no token"}')
232
+ return
233
+ }
234
+ const claims = decodeJwt(parsed.access_token)
235
+ const user = saveSupabaseSession({
236
+ access_token: parsed.access_token,
237
+ refresh_token: parsed.refresh_token ?? '',
238
+ expires_at: parsed.expires_at ? Number(parsed.expires_at) : undefined,
239
+ user: {
240
+ id: claims.sub,
241
+ email: claims.email,
242
+ },
243
+ })
244
+ res.writeHead(200, { 'content-type': 'application/json' })
245
+ res.end('{"ok":true}')
246
+ finish(() => resolve(user))
247
+ } catch (error) {
248
+ res.writeHead(500)
249
+ res.end('{"error":"bad request"}')
250
+ finish(() =>
251
+ reject(error instanceof Error ? error : new Error(String(error))),
252
+ )
253
+ }
254
+ })
255
+ return
256
+ }
257
+ res.writeHead(404)
258
+ res.end('not found')
259
+ })
260
+
261
+ const timer = setTimeout(
262
+ () => finish(() => reject(new Error('Login timed out. Please run anvil login again.'))),
263
+ timeoutMs,
264
+ )
265
+
266
+ function finish(then: () => void) {
267
+ if (settled) return
268
+ settled = true
269
+ clearTimeout(timer)
270
+ // Give the browser a beat to receive the 200 before the socket closes.
271
+ setTimeout(() => {
272
+ try {
273
+ server.close()
274
+ } catch {
275
+ /* already closed */
276
+ }
277
+ then()
278
+ }, 150)
279
+ }
280
+
281
+ server.on('error', (error) => finish(() => reject(error)))
282
+ server.listen(0, '127.0.0.1', () => {
283
+ const address = server.address()
284
+ const port = typeof address === 'object' && address ? address.port : 0
285
+ const redirectUri = `http://127.0.0.1:${port}/callback`
286
+ const loginUrl = `${getLoginPageUrl()}?redirect_uri=${encodeURIComponent(redirectUri)}`
287
+ console.log('\nOpening your browser to sign in…')
288
+ console.log(loginUrl, '\n')
289
+ void open(loginUrl).catch(() => {
290
+ console.log('Could not open a browser automatically — open the URL above.')
291
+ })
292
+ })
293
+ })
294
+ }
295
+
296
+ /** True when a Supabase session is already stored. */
297
+ export function hasSupabaseSession(): boolean {
298
+ const user = getUserCredentials()
299
+ return Boolean(user?.provider === 'supabase' && user.authToken)
300
+ }
301
+
302
+ /**
303
+ * Ensure the user is signed in for managed gateway mode. No-op when a Supabase
304
+ * session already exists. Non-interactive with ANVIL_LOGIN_EMAIL/PASSWORD;
305
+ * otherwise opens the browser sign-in. Throws if sign-in doesn't complete.
306
+ */
307
+ export async function ensureSignedIn(): Promise<void> {
308
+ if (hasSupabaseSession()) return
309
+ const email = process.env.ANVIL_LOGIN_EMAIL
310
+ const password = process.env.ANVIL_LOGIN_PASSWORD
311
+ if (email && password) {
312
+ await signInWithPassword(email, password)
313
+ return
314
+ }
315
+ await loginViaBrowser()
316
+ }
317
+
318
+ /** Seconds of headroom before expiry at which we proactively refresh. */
319
+ const REFRESH_SKEW_SECONDS = 60
320
+
321
+ /**
322
+ * Ensure the stored Supabase access token is still valid, refreshing it when
323
+ * it is within {@link REFRESH_SKEW_SECONDS} of expiry. No-op for non-Supabase
324
+ * credentials (direct/anvil). Returns the current valid access token, or
325
+ * undefined when there is nothing to refresh / refresh failed.
326
+ */
327
+ export async function ensureFreshAuthToken(): Promise<string | undefined> {
328
+ const user = getUserCredentials()
329
+ if (!user || user.provider !== 'supabase') return user?.authToken
330
+ const now = Math.floor(Date.now() / 1000)
331
+ const notExpiring =
332
+ typeof user.expiresAt === 'number' &&
333
+ user.expiresAt - REFRESH_SKEW_SECONDS > now
334
+ if (notExpiring) return user.authToken
335
+ if (!user.refreshToken) return user.authToken
336
+ try {
337
+ const refreshed = await refreshSupabaseSession(user.refreshToken)
338
+ return refreshed.authToken
339
+ } catch (error) {
340
+ logger.warn(
341
+ { error: error instanceof Error ? error.message : String(error) },
342
+ 'Supabase token refresh failed; using the existing token',
343
+ )
344
+ return user.authToken
345
+ }
346
+ }
@@ -3,6 +3,7 @@ import { AskUserBridge } from '@anvil/common/utils/ask-user-bridge'
3
3
  import { AnvilClient } from '@anvil/sdk'
4
4
 
5
5
  import { getAuthTokenDetails } from './auth'
6
+ import { ensureFreshAuthToken } from '../login/supabase-auth'
6
7
  import { getCliEnv, getSystemProcessEnv } from './env'
7
8
  import { loadAgentDefinitions } from './local-agent-registry'
8
9
  import { logger } from './logger'
@@ -48,6 +49,9 @@ export function resetAnvilClient(): void {
48
49
 
49
50
  export async function getAnvilClient(): Promise<AnvilClient | null> {
50
51
  if (!clientInstance) {
52
+ // Managed gateway mode: refresh the Supabase access token if it is near
53
+ // expiry before reading it (no-op for direct/anvil credentials).
54
+ await ensureFreshAuthToken()
51
55
  const { token: apiKey } = getAuthTokenDetails()
52
56
 
53
57
  if (!apiKey) {
package/src/utils/auth.ts CHANGED
@@ -2,7 +2,10 @@ import fs from 'fs'
2
2
  import path from 'path'
3
3
 
4
4
  import { getCiEnv } from '@anvil/common/env-ci'
5
- import { getDirectDeepSeekApiKey } from '@anvil/common/constants/deepseek-direct'
5
+ import {
6
+ getDirectDeepSeekApiKey,
7
+ isManagedGatewayMode,
8
+ } from '@anvil/common/constants/deepseek-direct'
6
9
  import { z } from 'zod'
7
10
 
8
11
 
@@ -21,6 +24,13 @@ const userSchema = z.object({
21
24
  fingerprintId: z.string().optional(),
22
25
  fingerprintHash: z.string().optional(),
23
26
  credits: z.number().optional(),
27
+ // Managed gateway mode (Supabase): `authToken` holds the Supabase access
28
+ // token (a JWT the cli-agent gateway verifies). These let the CLI refresh it
29
+ // without a re-login. Optional + additive, so existing credentials still parse.
30
+ provider: z.string().optional(),
31
+ refreshToken: z.string().optional(),
32
+ /** Access-token expiry, epoch seconds. */
33
+ expiresAt: z.number().optional(),
24
34
  })
25
35
 
26
36
  export type User = z.infer<typeof userSchema>
@@ -117,10 +127,12 @@ export const getAuthTokenDetails = (
117
127
  return { token: envToken, source: 'environment' }
118
128
  }
119
129
 
120
- // Direct-to-DeepSeek mode: the DeepSeek key doubles as the auth token so
121
- // the CLI treats the user as authenticated without a Anvil backend.
130
+ // Direct-to-DeepSeek mode: the DeepSeek key doubles as the auth token so the
131
+ // CLI treats the user as authenticated without an Anvil backend. In managed
132
+ // gateway mode this does NOT apply — the gateway needs a real Supabase JWT,
133
+ // so a local DeepSeek key must never count as being signed in.
122
134
  const deepseekToken = getDirectDeepSeekApiKey()
123
- if (deepseekToken) {
135
+ if (deepseekToken && !isManagedGatewayMode()) {
124
136
  return { token: deepseekToken, source: 'environment' }
125
137
  }
126
138
 
@@ -15,7 +15,7 @@ export const getConfigDir = (): string => {
15
15
  return path.join(
16
16
  os.homedir(),
17
17
  '.config',
18
- 'manicode' +
18
+ 'anvil' +
19
19
  // on a development stack?
20
20
  (env.NEXT_PUBLIC_CB_ENVIRONMENT !== 'prod'
21
21
  ? `-${env.NEXT_PUBLIC_CB_ENVIRONMENT}`
@@ -18,7 +18,13 @@
18
18
  * served by DeepSeek at all and falls back to the default DeepSeek model.
19
19
  */
20
20
 
21
- export const DEEPSEEK_BASE_URL = 'https://api.deepseek.com'
21
+ export const DEEPSEEK_BASE_URL = 'https://api.deepseek.com'
22
+
23
+ // The managed gateway is deployed with the CLI's Supabase project. Keep this
24
+ // public endpoint in the client so an installed CLI can run without a local
25
+ // .env file; authentication still comes from the user's stored Supabase JWT.
26
+ export const MANAGED_GATEWAY_URL =
27
+ 'https://tqolntrdmbbaptmxihtq.supabase.co/functions/v1/cli-agent'
22
28
 
23
29
  /** Model name sent to DeepSeek for ids that are not a DeepSeek lane model. */
24
30
  export const DEFAULT_DIRECT_DEEPSEEK_MODEL = 'deepseek-v4-pro'
@@ -31,6 +37,29 @@ export const getDirectDeepSeekApiKey = (): string | undefined =>
31
37
  export const isDirectDeepSeekMode = (): boolean =>
32
38
  Boolean(getDirectDeepSeekApiKey())
33
39
 
40
+ /**
41
+ * Managed gateway mode ("no direct mode"): the CLI routes every model call
42
+ * through the Anvil managed gateway (the Supabase `cli-agent` Edge Function)
43
+ * instead of calling a provider directly. The gateway holds the provider key,
44
+ * injects the private system prompt server-side, and meters credits — so the
45
+ * brain and the key never ship in the client.
46
+ *
47
+ * Enabled whenever a gateway URL is configured: `ANVIL_GATEWAY_URL` wins,
48
+ * otherwise it is derived from `SUPABASE_URL` / `NEXT_PUBLIC_SUPABASE_URL`.
49
+ * When on, it takes precedence over direct mode, and the CLI uses a local run
50
+ * identity (no external account backend is required).
51
+ */
52
+ export const getManagedGatewayUrl = (): string | undefined => {
53
+ // ANVIL_GATEWAY_URL remains an escape hatch for development and tests, but
54
+ // production installs must not depend on a locally supplied environment.
55
+ const explicit = process.env.ANVIL_GATEWAY_URL?.trim()
56
+ return (explicit || MANAGED_GATEWAY_URL).replace(/\/+$/, '')
57
+ }
58
+
59
+ /** True when the managed gateway is configured (see {@link getManagedGatewayUrl}). */
60
+ export const isManagedGatewayMode = (): boolean =>
61
+ Boolean(getManagedGatewayUrl())
62
+
34
63
  /** Map a Codebuff model ID to the model name DeepSeek's API accepts. */
35
64
  export function remapModelForDirectDeepSeek(model: string): string {
36
65
  const override = process.env.DEEPSEEK_MODEL
@@ -1,9 +1,10 @@
1
1
  import * as ignore from 'ignore'
2
2
 
3
3
  export const PROJECT_IGNORE_FILES = [
4
- '.gitignore',
5
- '.anvilignore',
6
- '.manicodeignore',
4
+ '.gitignore',
5
+ '.anvilignore',
6
+ // Legacy name retained so existing projects keep their ignore rules.
7
+ '.manicodeignore',
7
8
  // Legacy: respect ignore files created by pre-Anvil installs.
8
9
  '.codebuffignore',
9
10
  ] as const