@alumbwe/anvil 1.0.19 → 1.0.20

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 (494) hide show
  1. package/README.md +28 -30
  2. package/bin/anvil +2 -0
  3. package/package.json +71 -75
  4. package/src/agents/bundled-agents.generated.d.ts +14 -0
  5. package/src/agents/bundled-agents.generated.ts +3760 -0
  6. package/src/app.tsx +399 -0
  7. package/src/chat.tsx +1698 -0
  8. package/src/cli-args.ts +134 -0
  9. package/src/commands/__tests__/anvil-command-aliases.test.ts +56 -0
  10. package/src/commands/__tests__/bash-command.test.ts +445 -0
  11. package/src/commands/__tests__/command-args.test.ts +349 -0
  12. package/src/commands/__tests__/copy-conversation-clipboard.test.ts +108 -0
  13. package/src/commands/__tests__/copy-conversation.test.ts +226 -0
  14. package/src/commands/__tests__/diagnostics-command.test.ts +35 -0
  15. package/src/commands/__tests__/image.test.ts +99 -0
  16. package/src/commands/__tests__/init.test.ts +456 -0
  17. package/src/commands/__tests__/new-command-rotates-chat.test.ts +91 -0
  18. package/src/commands/__tests__/process-diagnostics.test.ts +54 -0
  19. package/src/commands/__tests__/prompt-builders.test.ts +28 -0
  20. package/src/commands/__tests__/router-input.test.ts +298 -0
  21. package/src/commands/ads.ts +44 -0
  22. package/src/commands/command-registry.ts +726 -0
  23. package/src/commands/copy-conversation.ts +392 -0
  24. package/src/commands/help.ts +13 -0
  25. package/src/commands/image.ts +20 -0
  26. package/src/commands/init.ts +121 -0
  27. package/src/commands/process-diagnostics.ts +120 -0
  28. package/src/commands/prompt-builders.ts +100 -0
  29. package/src/commands/publish.ts +203 -0
  30. package/src/commands/router-utils.ts +80 -0
  31. package/src/commands/router.ts +487 -0
  32. package/src/commands/usage.ts +26 -0
  33. package/src/components/__tests__/ad-banner.test.tsx +83 -0
  34. package/src/components/__tests__/anvil-landing-heading-row.test.tsx +91 -0
  35. package/src/components/__tests__/anvil-model-selector.test.tsx +519 -0
  36. package/src/components/__tests__/anvil-offer-invariants.test.ts +54 -0
  37. package/src/components/__tests__/anvil-takeover-prompt.test.tsx +212 -0
  38. package/src/components/__tests__/chat-input-bar.test.tsx +130 -0
  39. package/src/components/__tests__/grid-layout.integration.test.tsx +304 -0
  40. package/src/components/__tests__/grid-layout.test.tsx +1051 -0
  41. package/src/components/__tests__/message-block.completion.test.tsx +104 -0
  42. package/src/components/__tests__/message-block.streaming.test.tsx +78 -0
  43. package/src/components/__tests__/message-with-agents.test.tsx +579 -0
  44. package/src/components/__tests__/multiline-input.test.tsx +1132 -0
  45. package/src/components/__tests__/queue-panel.test.tsx +398 -0
  46. package/src/components/__tests__/selectable-list.test.ts +232 -0
  47. package/src/components/__tests__/status-bar.test.tsx +44 -0
  48. package/src/components/__tests__/status-indicator.test.tsx +204 -0
  49. package/src/components/__tests__/terminal-thinking-orb.test.tsx +111 -0
  50. package/src/components/__tests__/user-error-banner.test.tsx +102 -0
  51. package/src/components/ad-banner.tsx +323 -0
  52. package/src/components/agent-checklist.tsx +350 -0
  53. package/src/components/agent-mode-toggle.tsx +241 -0
  54. package/src/components/anvil-active-session-summary.tsx +63 -0
  55. package/src/components/anvil-landing-screen.tsx +865 -0
  56. package/src/components/anvil-mark.tsx +52 -0
  57. package/src/components/anvil-model-selector.tsx +1087 -0
  58. package/src/components/anvil-referral-banner.tsx +604 -0
  59. package/src/components/anvil-superseded-screen.tsx +62 -0
  60. package/src/components/ask-user/__tests__/multiple-choice-form.test.ts +432 -0
  61. package/src/components/ask-user/__tests__/validation.test.ts +213 -0
  62. package/src/components/ask-user/components/accordion-question.tsx +153 -0
  63. package/src/components/ask-user/components/custom-answer-input.tsx +66 -0
  64. package/src/components/ask-user/components/options-list.tsx +133 -0
  65. package/src/components/ask-user/components/question-header.tsx +80 -0
  66. package/src/components/ask-user/components/question-option.tsx +89 -0
  67. package/src/components/ask-user/constants.ts +38 -0
  68. package/src/components/ask-user/index.tsx +631 -0
  69. package/src/components/ask-user/utils/validation.ts +85 -0
  70. package/src/components/attachment-card.tsx +65 -0
  71. package/src/components/blocks/agent-block-grid.tsx +58 -0
  72. package/src/components/blocks/agent-branch-item.tsx +311 -0
  73. package/src/components/blocks/agent-branch-wrapper.tsx +493 -0
  74. package/src/components/blocks/agent-list-branch.tsx +77 -0
  75. package/src/components/blocks/ask-user-branch.tsx +81 -0
  76. package/src/components/blocks/block-helpers.ts +11 -0
  77. package/src/components/blocks/blocks-renderer.tsx +289 -0
  78. package/src/components/blocks/content-with-markdown.tsx +33 -0
  79. package/src/components/blocks/image-block.tsx +128 -0
  80. package/src/components/blocks/implementor-row.tsx +429 -0
  81. package/src/components/blocks/single-block.tsx +201 -0
  82. package/src/components/blocks/thinking-block.tsx +67 -0
  83. package/src/components/blocks/tool-block-group.tsx +57 -0
  84. package/src/components/blocks/tool-branch.tsx +206 -0
  85. package/src/components/blocks/user-content-copy.tsx +157 -0
  86. package/src/components/bottom-banner.tsx +136 -0
  87. package/src/components/build-mode-buttons.tsx +110 -0
  88. package/src/components/button.tsx +66 -0
  89. package/src/components/chat-header.tsx +77 -0
  90. package/src/components/chat-history-screen.tsx +413 -0
  91. package/src/components/chat-input-bar.tsx +523 -0
  92. package/src/components/clickable-title-box.tsx +66 -0
  93. package/src/components/clickable.tsx +119 -0
  94. package/src/components/collapse-button.tsx +34 -0
  95. package/src/components/copy-button.tsx +203 -0
  96. package/src/components/elapsed-timer.tsx +55 -0
  97. package/src/components/error-boundary.tsx +55 -0
  98. package/src/components/feedback-container.tsx +181 -0
  99. package/src/components/feedback-icon-button.tsx +89 -0
  100. package/src/components/feedback-input-mode.tsx +344 -0
  101. package/src/components/file-attachment-card.tsx +98 -0
  102. package/src/components/grid-layout.tsx +80 -0
  103. package/src/components/help-banner.tsx +127 -0
  104. package/src/components/highlighted-text.tsx +52 -0
  105. package/src/components/image-card.tsx +167 -0
  106. package/src/components/image-thumbnail.tsx +114 -0
  107. package/src/components/input-cursor.tsx +77 -0
  108. package/src/components/input-mode-banner.tsx +57 -0
  109. package/src/components/load-previous-button.tsx +48 -0
  110. package/src/components/login-modal.tsx +475 -0
  111. package/src/components/message-block.tsx +346 -0
  112. package/src/components/message-footer.tsx +258 -0
  113. package/src/components/message-with-agents.tsx +506 -0
  114. package/src/components/mode-divider.tsx +43 -0
  115. package/src/components/multiline-input.tsx +1241 -0
  116. package/src/components/out-of-credits-banner.tsx +160 -0
  117. package/src/components/pending-attachments-banner.tsx +116 -0
  118. package/src/components/pending-bash-message.tsx +59 -0
  119. package/src/components/progress-bar.tsx +81 -0
  120. package/src/components/project-picker-screen.tsx +485 -0
  121. package/src/components/publish-confirmation.tsx +493 -0
  122. package/src/components/publish-container.tsx +687 -0
  123. package/src/components/queue-panel.tsx +310 -0
  124. package/src/components/raised-pill.tsx +95 -0
  125. package/src/components/renderers/plan-box.tsx +59 -0
  126. package/src/components/review-screen.tsx +112 -0
  127. package/src/components/scroll-to-bottom-button.tsx +34 -0
  128. package/src/components/segmented-control.tsx +187 -0
  129. package/src/components/selectable-list.tsx +248 -0
  130. package/src/components/selected-chips.tsx +80 -0
  131. package/src/components/separator.tsx +40 -0
  132. package/src/components/session-ended-banner.tsx +226 -0
  133. package/src/components/shimmer-text.tsx +248 -0
  134. package/src/components/status-bar.tsx +313 -0
  135. package/src/components/subscription-limit-banner.tsx +206 -0
  136. package/src/components/suggested-prompts.tsx +184 -0
  137. package/src/components/suggestion-menu.tsx +215 -0
  138. package/src/components/terminal-command-display.tsx +151 -0
  139. package/src/components/terminal-link.tsx +97 -0
  140. package/src/components/terminal-thinking-orb.tsx +95 -0
  141. package/src/components/text-attachment-card.tsx +77 -0
  142. package/src/components/thinking.tsx +109 -0
  143. package/src/components/tools/__tests__/apply-patch.test.tsx +84 -0
  144. package/src/components/tools/__tests__/code-search.test.tsx +45 -0
  145. package/src/components/tools/__tests__/gravity-index.test.ts +76 -0
  146. package/src/components/tools/__tests__/render-ui.test.tsx +92 -0
  147. package/src/components/tools/__tests__/run-terminal-command.test.ts +233 -0
  148. package/src/components/tools/apply-patch.tsx +95 -0
  149. package/src/components/tools/code-search.tsx +45 -0
  150. package/src/components/tools/composio.tsx +152 -0
  151. package/src/components/tools/diff-viewer.tsx +76 -0
  152. package/src/components/tools/glob.tsx +61 -0
  153. package/src/components/tools/gravity-index.tsx +77 -0
  154. package/src/components/tools/list-directory.tsx +61 -0
  155. package/src/components/tools/read-docs.tsx +37 -0
  156. package/src/components/tools/read-files.tsx +91 -0
  157. package/src/components/tools/read-subtree.tsx +44 -0
  158. package/src/components/tools/read-url.tsx +33 -0
  159. package/src/components/tools/registry.ts +126 -0
  160. package/src/components/tools/render-ui.tsx +122 -0
  161. package/src/components/tools/run-terminal-command.tsx +79 -0
  162. package/src/components/tools/skill.tsx +29 -0
  163. package/src/components/tools/str-replace.tsx +77 -0
  164. package/src/components/tools/suggest-followups.tsx +369 -0
  165. package/src/components/tools/task-completed.tsx +16 -0
  166. package/src/components/tools/tool-call-item.tsx +273 -0
  167. package/src/components/tools/types.ts +55 -0
  168. package/src/components/tools/web-search.tsx +33 -0
  169. package/src/components/tools/write-file.tsx +16 -0
  170. package/src/components/tools/write-todos.tsx +95 -0
  171. package/src/components/top-banner.tsx +178 -0
  172. package/src/components/usage-banner.tsx +244 -0
  173. package/src/components/user-error-banner.tsx +56 -0
  174. package/src/components/validation-error-popover.tsx +192 -0
  175. package/src/contexts/__tests__/chat-runtime-context.test.tsx +149 -0
  176. package/src/contexts/chat-runtime-context.tsx +240 -0
  177. package/src/data/slash-commands.test.ts +24 -0
  178. package/src/data/slash-commands.ts +249 -0
  179. package/src/entry.ts +12 -0
  180. package/src/hooks/__tests__/holds-live-anvil-slot.test.ts +49 -0
  181. package/src/hooks/__tests__/session-fetch-signal.test.ts +63 -0
  182. package/src/hooks/__tests__/use-activity-query.test.ts +1000 -0
  183. package/src/hooks/__tests__/use-ask-user-bridge.test.ts +176 -0
  184. package/src/hooks/__tests__/use-auth-query.test.ts +21 -0
  185. package/src/hooks/__tests__/use-chat-input-remount.test.tsx +66 -0
  186. package/src/hooks/__tests__/use-clipboard.test.tsx +73 -0
  187. package/src/hooks/__tests__/use-connection-status.test.ts +112 -0
  188. package/src/hooks/__tests__/use-directory-browser.test.ts +203 -0
  189. package/src/hooks/__tests__/use-gravity-ad.test.ts +73 -0
  190. package/src/hooks/__tests__/use-grid-layout.test.ts +349 -0
  191. package/src/hooks/__tests__/use-input-history.test.ts +699 -0
  192. package/src/hooks/__tests__/use-message-queue-editing.test.tsx +209 -0
  193. package/src/hooks/__tests__/use-message-queue.test.tsx +71 -0
  194. package/src/hooks/__tests__/use-path-tab-completion.test.ts +315 -0
  195. package/src/hooks/__tests__/use-queue-controls.test.ts +68 -0
  196. package/src/hooks/__tests__/use-queue-ui.test.tsx +83 -0
  197. package/src/hooks/__tests__/use-searchable-list.test.ts +359 -0
  198. package/src/hooks/__tests__/use-send-message-timer.test.ts +146 -0
  199. package/src/hooks/__tests__/use-suggestion-engine-mention.test.ts +361 -0
  200. package/src/hooks/__tests__/use-suggestion-engine.test.ts +485 -0
  201. package/src/hooks/__tests__/use-terminal-focus.test.ts +66 -0
  202. package/src/hooks/__tests__/use-terminal-layout.test.ts +675 -0
  203. package/src/hooks/__tests__/use-timeout.test.ts +330 -0
  204. package/src/hooks/__tests__/use-usage-query.test.ts +502 -0
  205. package/src/hooks/__tests__/use-user-details-query.test.ts +195 -0
  206. package/src/hooks/helpers/__tests__/send-message.test.ts +1918 -0
  207. package/src/hooks/helpers/send-message.ts +654 -0
  208. package/src/hooks/stream-state.ts +83 -0
  209. package/src/hooks/use-activity-query.ts +668 -0
  210. package/src/hooks/use-agent-validation.ts +74 -0
  211. package/src/hooks/use-anvil-ctrl-c-exit.ts +23 -0
  212. package/src/hooks/use-anvil-session-progress.ts +34 -0
  213. package/src/hooks/use-anvil-session.ts +806 -0
  214. package/src/hooks/use-anvil-streak-query.ts +61 -0
  215. package/src/hooks/use-ask-user-bridge.ts +92 -0
  216. package/src/hooks/use-auth-query.ts +262 -0
  217. package/src/hooks/use-auth-state.ts +152 -0
  218. package/src/hooks/use-chat-input.ts +105 -0
  219. package/src/hooks/use-chat-keyboard.ts +321 -0
  220. package/src/hooks/use-chat-messages.ts +254 -0
  221. package/src/hooks/use-chat-state.ts +165 -0
  222. package/src/hooks/use-chat-streaming.ts +180 -0
  223. package/src/hooks/use-chat-ui.ts +131 -0
  224. package/src/hooks/use-clipboard.ts +151 -0
  225. package/src/hooks/use-connection-status.ts +142 -0
  226. package/src/hooks/use-directory-browser.ts +88 -0
  227. package/src/hooks/use-elapsed-time.ts +116 -0
  228. package/src/hooks/use-event.ts +34 -0
  229. package/src/hooks/use-exit-handler.ts +66 -0
  230. package/src/hooks/use-fetch-login-url.ts +65 -0
  231. package/src/hooks/use-fingerprint.ts +61 -0
  232. package/src/hooks/use-gravity-ad.ts +656 -0
  233. package/src/hooks/use-grid-layout.ts +73 -0
  234. package/src/hooks/use-input-history.ts +173 -0
  235. package/src/hooks/use-login-keyboard-handlers.ts +75 -0
  236. package/src/hooks/use-login-polling.ts +114 -0
  237. package/src/hooks/use-logo.tsx +161 -0
  238. package/src/hooks/use-message-queue.ts +387 -0
  239. package/src/hooks/use-now.ts +20 -0
  240. package/src/hooks/use-path-tab-completion.ts +88 -0
  241. package/src/hooks/use-publish-mutation.ts +60 -0
  242. package/src/hooks/use-queue-controls.ts +68 -0
  243. package/src/hooks/use-queue-ui.ts +74 -0
  244. package/src/hooks/use-scroll-management.ts +184 -0
  245. package/src/hooks/use-searchable-list.ts +98 -0
  246. package/src/hooks/use-send-message.ts +773 -0
  247. package/src/hooks/use-sheen-animation.tsx +88 -0
  248. package/src/hooks/use-subscription-query.ts +71 -0
  249. package/src/hooks/use-suggestion-engine.ts +783 -0
  250. package/src/hooks/use-terminal-breakpoints.ts +77 -0
  251. package/src/hooks/use-terminal-dimensions.ts +46 -0
  252. package/src/hooks/use-terminal-focus.ts +29 -0
  253. package/src/hooks/use-terminal-layout.ts +189 -0
  254. package/src/hooks/use-theme.tsx +140 -0
  255. package/src/hooks/use-timeout.ts +85 -0
  256. package/src/hooks/use-update-preference.ts +66 -0
  257. package/src/hooks/use-usage-monitor.ts +55 -0
  258. package/src/hooks/use-usage-query.ts +124 -0
  259. package/src/hooks/use-user-details-query.ts +93 -0
  260. package/src/hooks/use-why-did-you-update.ts +177 -0
  261. package/src/index.tsx +473 -0
  262. package/src/init/__tests__/init-direnv.test.ts +526 -0
  263. package/src/init/init-app.ts +34 -0
  264. package/src/init/init-direnv.ts +133 -0
  265. package/src/login/constants.ts +60 -0
  266. package/src/login/login-flow.ts +204 -0
  267. package/src/login/plain-login.ts +108 -0
  268. package/src/login/utils.ts +183 -0
  269. package/src/native/ripgrep.ts +80 -0
  270. package/src/polyfills/bun-strip-ansi.ts +12 -0
  271. package/src/pre-init/tree-sitter-wasm.ts +96 -0
  272. package/src/project-files.ts +102 -0
  273. package/src/setup/key-setup.ts +295 -0
  274. package/src/setup/load-env.ts +106 -0
  275. package/src/state/__tests__/chat-store-focus.test.ts +22 -0
  276. package/src/state/__tests__/feedback-store.test.ts +257 -0
  277. package/src/state/anvil-model-store.ts +42 -0
  278. package/src/state/anvil-session-store.ts +53 -0
  279. package/src/state/chat-history-store.ts +30 -0
  280. package/src/state/chat-store.ts +544 -0
  281. package/src/state/feedback-store.ts +160 -0
  282. package/src/state/login-store.ts +150 -0
  283. package/src/state/message-block-store.ts +149 -0
  284. package/src/state/publish-store.ts +146 -0
  285. package/src/state/queue-panel-store.ts +26 -0
  286. package/src/state/review-store.ts +24 -0
  287. package/src/types/anvil-session.ts +17 -0
  288. package/src/types/chat-state.ts +15 -0
  289. package/src/types/chat.ts +241 -0
  290. package/src/types/contracts/send-message.ts +12 -0
  291. package/src/types/env.ts +98 -0
  292. package/src/types/function-params.ts +28 -0
  293. package/src/types/react19-compat.d.ts +19 -0
  294. package/src/types/store.ts +111 -0
  295. package/src/types/theme-system.ts +144 -0
  296. package/src/types/utils.ts +3 -0
  297. package/src/utils/__tests__/active-run.test.ts +140 -0
  298. package/src/utils/__tests__/activity-tracker.test.ts +440 -0
  299. package/src/utils/__tests__/agent-display.test.ts +139 -0
  300. package/src/utils/__tests__/analytics-client.test.ts +262 -0
  301. package/src/utils/__tests__/anonymous-id.test.ts +74 -0
  302. package/src/utils/__tests__/anvil-api.test.ts +578 -0
  303. package/src/utils/__tests__/anvil-instance-owner.test.ts +69 -0
  304. package/src/utils/__tests__/anvil-model-navigation.test.ts +102 -0
  305. package/src/utils/__tests__/anvil-premium-reset.test.ts +79 -0
  306. package/src/utils/__tests__/anvil-referral-cache.test.ts +116 -0
  307. package/src/utils/__tests__/anvil-session-api.test.ts +215 -0
  308. package/src/utils/__tests__/anvil-session-display.test.ts +23 -0
  309. package/src/utils/__tests__/anvil-streak-line.test.ts +217 -0
  310. package/src/utils/__tests__/arrays.test.ts +264 -0
  311. package/src/utils/__tests__/bash-context-processor.test.ts +38 -0
  312. package/src/utils/__tests__/block-processor.test.ts +810 -0
  313. package/src/utils/__tests__/chat-history.test.ts +203 -0
  314. package/src/utils/__tests__/chat-input-key-intercept.test.ts +40 -0
  315. package/src/utils/__tests__/chat-meta.test.ts +130 -0
  316. package/src/utils/__tests__/clipboard-linux.test.ts +359 -0
  317. package/src/utils/__tests__/clipboard.test.ts +910 -0
  318. package/src/utils/__tests__/code-search-summary.test.ts +84 -0
  319. package/src/utils/__tests__/collapse-helpers.test.ts +1109 -0
  320. package/src/utils/__tests__/error-handling.test.ts +658 -0
  321. package/src/utils/__tests__/exit-cleanly.test.ts +110 -0
  322. package/src/utils/__tests__/feedback-helpers.test.ts +444 -0
  323. package/src/utils/__tests__/feedback-submission.test.ts +26 -0
  324. package/src/utils/__tests__/fetch-usage.test.ts +355 -0
  325. package/src/utils/__tests__/fingerprint.test.ts +144 -0
  326. package/src/utils/__tests__/fixtures/terminal-command-broker-child.ts +7 -0
  327. package/src/utils/__tests__/fixtures/terminal-command-broker-entry.ts +3 -0
  328. package/src/utils/__tests__/fixtures/terminal-command-broker-owner.ts +33 -0
  329. package/src/utils/__tests__/format-elapsed-time.test.ts +103 -0
  330. package/src/utils/__tests__/format-timeout.test.ts +87 -0
  331. package/src/utils/__tests__/image-dimensions.test.ts +249 -0
  332. package/src/utils/__tests__/image-processor.test.ts +209 -0
  333. package/src/utils/__tests__/implementor-helpers.test.ts +1458 -0
  334. package/src/utils/__tests__/keyboard-actions.test.ts +660 -0
  335. package/src/utils/__tests__/layout-helpers.test.ts +36 -0
  336. package/src/utils/__tests__/lazy-response-ads.test.ts +167 -0
  337. package/src/utils/__tests__/log-shipper.test.ts +53 -0
  338. package/src/utils/__tests__/markdown-renderer.test.tsx +421 -0
  339. package/src/utils/__tests__/message-block-helpers.test.ts +1263 -0
  340. package/src/utils/__tests__/message-updater.test.ts +645 -0
  341. package/src/utils/__tests__/osc-timeout-scenarios.test.ts +172 -0
  342. package/src/utils/__tests__/pending-attachments.test.ts +277 -0
  343. package/src/utils/__tests__/polling-backoff.test.ts +51 -0
  344. package/src/utils/__tests__/queue-panel-actions.test.ts +148 -0
  345. package/src/utils/__tests__/run-state-storage.test.ts +919 -0
  346. package/src/utils/__tests__/safe-json.test.ts +77 -0
  347. package/src/utils/__tests__/sdk-event-handlers.test.ts +685 -0
  348. package/src/utils/__tests__/send-message-helpers.test.ts +1898 -0
  349. package/src/utils/__tests__/send-message-timer.test.ts +64 -0
  350. package/src/utils/__tests__/settings.test.ts +99 -0
  351. package/src/utils/__tests__/strings.test.ts +192 -0
  352. package/src/utils/__tests__/terminal-color-detection.test.ts +425 -0
  353. package/src/utils/__tests__/terminal-command-broker.test.ts +528 -0
  354. package/src/utils/__tests__/terminal-enter-detection.test.ts +77 -0
  355. package/src/utils/__tests__/terminal-io.test.ts +137 -0
  356. package/src/utils/__tests__/terminal-protocol-controller.test.ts +119 -0
  357. package/src/utils/__tests__/text-layout.test.ts +82 -0
  358. package/src/utils/__tests__/theme-platform-detection.test.ts +125 -0
  359. package/src/utils/__tests__/think-tag-parser.test.ts +159 -0
  360. package/src/utils/__tests__/trace-writer.test.ts +223 -0
  361. package/src/utils/__tests__/trim-chat-logs.test.ts +78 -0
  362. package/src/utils/__tests__/usage-banner-state.test.ts +298 -0
  363. package/src/utils/__tests__/validation-error-formatting.test.ts +126 -0
  364. package/src/utils/__tests__/windows-terminal-health.test.ts +101 -0
  365. package/src/utils/__tests__/write-file-atomic.test.ts +127 -0
  366. package/src/utils/active-run.ts +131 -0
  367. package/src/utils/activity-tracker.ts +66 -0
  368. package/src/utils/agent-display.ts +87 -0
  369. package/src/utils/agent-helpers.ts +27 -0
  370. package/src/utils/agent-id-utils.ts +14 -0
  371. package/src/utils/ai-message-id.ts +12 -0
  372. package/src/utils/analytics.ts +361 -0
  373. package/src/utils/anonymous-id.ts +87 -0
  374. package/src/utils/anvil-agent-selection.ts +33 -0
  375. package/src/utils/anvil-api.ts +612 -0
  376. package/src/utils/anvil-client.ts +217 -0
  377. package/src/utils/anvil-instance-owner.ts +66 -0
  378. package/src/utils/anvil-model-navigation.ts +50 -0
  379. package/src/utils/anvil-premium-reset.ts +55 -0
  380. package/src/utils/anvil-referral-cache.ts +51 -0
  381. package/src/utils/anvil-session-api.ts +243 -0
  382. package/src/utils/anvil-session-display.ts +21 -0
  383. package/src/utils/anvil-streak-line.ts +93 -0
  384. package/src/utils/arrays.ts +117 -0
  385. package/src/utils/auth.ts +258 -0
  386. package/src/utils/bash-context-processor.ts +47 -0
  387. package/src/utils/bash-messages.ts +108 -0
  388. package/src/utils/block-margins.ts +35 -0
  389. package/src/utils/block-operations.ts +523 -0
  390. package/src/utils/block-processor.ts +213 -0
  391. package/src/utils/chat-history.ts +253 -0
  392. package/src/utils/chat-input-key-intercept.ts +52 -0
  393. package/src/utils/chat-meta.ts +90 -0
  394. package/src/utils/chat-scroll-accel.ts +151 -0
  395. package/src/utils/clipboard-image.ts +588 -0
  396. package/src/utils/clipboard.ts +397 -0
  397. package/src/utils/code-search-summary.ts +70 -0
  398. package/src/utils/collapse-helpers.ts +256 -0
  399. package/src/utils/config-dir.ts +24 -0
  400. package/src/utils/constants.ts +190 -0
  401. package/src/utils/create-event-handler-state.ts +73 -0
  402. package/src/utils/create-run-config.ts +121 -0
  403. package/src/utils/detect-shell.ts +116 -0
  404. package/src/utils/directory-browser.ts +72 -0
  405. package/src/utils/engagement.ts +55 -0
  406. package/src/utils/env.ts +92 -0
  407. package/src/utils/error-handling.ts +255 -0
  408. package/src/utils/error-messages.ts +62 -0
  409. package/src/utils/exit-cleanly.ts +86 -0
  410. package/src/utils/feedback-helpers.ts +103 -0
  411. package/src/utils/feedback-submission.ts +22 -0
  412. package/src/utils/fetch-usage.ts +76 -0
  413. package/src/utils/fingerprint.ts +245 -0
  414. package/src/utils/format-elapsed-time.ts +33 -0
  415. package/src/utils/format-session-units.ts +6 -0
  416. package/src/utils/format-timeout.ts +28 -0
  417. package/src/utils/format-validation-errors-for-message.ts +84 -0
  418. package/src/utils/git.ts +17 -0
  419. package/src/utils/helpers.ts +47 -0
  420. package/src/utils/image-display.ts +67 -0
  421. package/src/utils/image-handler.ts +325 -0
  422. package/src/utils/image-processor.ts +133 -0
  423. package/src/utils/image-thumbnail.ts +78 -0
  424. package/src/utils/implementor-helpers.ts +787 -0
  425. package/src/utils/input-modes.ts +174 -0
  426. package/src/utils/keyboard-actions.ts +392 -0
  427. package/src/utils/keypad-keys.ts +47 -0
  428. package/src/utils/layout-helpers.ts +33 -0
  429. package/src/utils/lazy-response-ads.ts +1 -0
  430. package/src/utils/local-agent-registry.ts +474 -0
  431. package/src/utils/log-shipper.ts +126 -0
  432. package/src/utils/logger.ts +327 -0
  433. package/src/utils/markdown-renderer.tsx +1090 -0
  434. package/src/utils/math.ts +3 -0
  435. package/src/utils/message-block-helpers.ts +684 -0
  436. package/src/utils/message-history.ts +143 -0
  437. package/src/utils/message-tree-utils.ts +67 -0
  438. package/src/utils/message-updater.ts +279 -0
  439. package/src/utils/open-file.ts +146 -0
  440. package/src/utils/open-url.ts +63 -0
  441. package/src/utils/path-completion.ts +111 -0
  442. package/src/utils/path-helpers.ts +38 -0
  443. package/src/utils/pending-attachments.ts +360 -0
  444. package/src/utils/polling-backoff.ts +59 -0
  445. package/src/utils/project-picker.ts +12 -0
  446. package/src/utils/queue-panel-actions.ts +70 -0
  447. package/src/utils/recent-projects.ts +156 -0
  448. package/src/utils/renderer-cleanup.ts +233 -0
  449. package/src/utils/response-ad-positions.ts +1 -0
  450. package/src/utils/run-state-storage.ts +603 -0
  451. package/src/utils/safe-json.ts +153 -0
  452. package/src/utils/sdk-event-handlers.ts +571 -0
  453. package/src/utils/send-message-helpers.ts +210 -0
  454. package/src/utils/send-message-timer.ts +110 -0
  455. package/src/utils/settings.ts +226 -0
  456. package/src/utils/skill-registry.ts +94 -0
  457. package/src/utils/spawn-agent-matcher.ts +55 -0
  458. package/src/utils/status-indicator-state.ts +107 -0
  459. package/src/utils/stream-chunk-processor.ts +57 -0
  460. package/src/utils/strings.ts +271 -0
  461. package/src/utils/subscription.ts +31 -0
  462. package/src/utils/syntax-highlighter.tsx +19 -0
  463. package/src/utils/terminal-color-detection.ts +474 -0
  464. package/src/utils/terminal-command-broker.ts +467 -0
  465. package/src/utils/terminal-enter-detection.ts +63 -0
  466. package/src/utils/terminal-images.ts +229 -0
  467. package/src/utils/terminal-io.ts +85 -0
  468. package/src/utils/terminal-protocol-controller.ts +173 -0
  469. package/src/utils/terminal-reset-sequences.ts +33 -0
  470. package/src/utils/terminal-title.ts +84 -0
  471. package/src/utils/terminal-watchdog.ts +366 -0
  472. package/src/utils/text-layout.ts +149 -0
  473. package/src/utils/theme-config.ts +141 -0
  474. package/src/utils/theme-system.ts +1206 -0
  475. package/src/utils/think-tag-parser.ts +95 -0
  476. package/src/utils/time-format.ts +21 -0
  477. package/src/utils/trace-writer.ts +162 -0
  478. package/src/utils/ui-constants.ts +69 -0
  479. package/src/utils/update-checker.ts +110 -0
  480. package/src/utils/usage-banner-state.ts +146 -0
  481. package/src/utils/validation-error-formatting.ts +86 -0
  482. package/src/utils/validation-error-helpers.ts +19 -0
  483. package/src/utils/windows-terminal-health.ts +124 -0
  484. package/src/utils/word-wrap-utils.ts +53 -0
  485. package/src/utils/write-file-atomic.ts +54 -0
  486. package/src/utils/yield-to-event-loop.ts +9 -0
  487. package/bin/anvil.cjs +0 -40
  488. package/dist/bundled.js +0 -123508
  489. package/dist/bundled.js.map +0 -828
  490. package/dist/vendor/ripgrep/arm64-darwin/rg +0 -0
  491. package/dist/vendor/ripgrep/arm64-linux/rg +0 -0
  492. package/dist/vendor/ripgrep/x64-darwin/rg +0 -0
  493. package/dist/vendor/ripgrep/x64-linux/rg +0 -0
  494. package/dist/vendor/ripgrep/x64-win32/rg.exe +0 -0
@@ -0,0 +1,96 @@
1
+ // Find tree-sitter.wasm so the SDK's tree-sitter parser singleton can load
2
+ // it at runtime. Must be the very first import in `index.tsx`: subsequent
3
+ // imports (the SDK / code-map) eagerly construct the parser, and its init
4
+ // reads what we publish here on `globalThis` and via the env var.
5
+ //
6
+ // Final approach after several attempts to embed the wasm into the bun
7
+ // --compile binary all failed on Windows (the bytes ended up in the
8
+ // binary, but every JS-level retrieval mechanism — `with { type: 'file' }`
9
+ // import binding, base64 string literals, chunked base64 in a generated
10
+ // module, function-export wrappers — was either tree-shaken, transformed
11
+ // by the minifier, or otherwise stripped):
12
+ //
13
+ // ship tree-sitter.wasm as a sibling file next to the binary.
14
+ //
15
+ // It's 200KB, the npm tarball already contains the binary; adding one
16
+ // more file is trivial. The build script copies the wasm into `cli/bin/`
17
+ // after compile, the release workflow tarballs both, and the anvil /
18
+ // anvil downloader extracts both into the same directory. At runtime,
19
+ // `process.execPath` plus a relative file lookup gets us the wasm with
20
+ // zero bundler involvement.
21
+
22
+ import { existsSync, readFileSync } from 'fs'
23
+ import { dirname, isAbsolute, join, resolve } from 'path'
24
+
25
+ // Where to look for the sibling tree-sitter.wasm. We can't just use
26
+ // `dirname(process.execPath)`: at pre-init time inside a bun --compile
27
+ // binary on Windows, `process.execPath` returns the *bunfs* internal
28
+ // path (`B:\~BUN\root\<binary>.exe`) rather than the on-disk path of
29
+ // the .exe the user invoked. By the time main() runs it switches to
30
+ // the disk path, but pre-init has long since bailed out.
31
+ //
32
+ // Try several sources in order; the first whose sibling .wasm exists
33
+ // wins. argv[0] is normally the path the binary was invoked with —
34
+ // always a real disk path, never bunfs. execPath is kept as a fallback
35
+ // for environments where argv[0] is something exotic.
36
+ const candidates = (
37
+ [process.argv[0], process.execPath] as Array<string | undefined>
38
+ )
39
+ .filter((p): p is string => typeof p === 'string' && p.length > 0)
40
+ .map((p) => (isAbsolute(p) ? p : resolve(p)))
41
+ .map((p) => join(dirname(p), 'tree-sitter.wasm'))
42
+
43
+ const siblingPath = candidates.find((p) => existsSync(p))
44
+
45
+ // Pre-init diagnostic — only fires when --smoke-tree-sitter is set so we
46
+ // don't spam every run. We need to see what argv[0] / execPath looked
47
+ // like at this exact phase on Windows: the round-7 main() diag showed
48
+ // disk paths, but pre-init silently bailed, meaning module-init time
49
+ // gives different values. argv[0] alone wasn't enough to fix it.
50
+ if (process.argv.includes('--smoke-tree-sitter')) {
51
+ console.error(
52
+ `[pre-init diag] argv[0]=${process.argv[0]}\n` +
53
+ `[pre-init diag] execPath=${process.execPath}\n` +
54
+ `[pre-init diag] candidates=${JSON.stringify(candidates)}\n` +
55
+ `[pre-init diag] resolved siblingPath=${siblingPath ?? '<none>'}\n`,
56
+ )
57
+ }
58
+
59
+ if (siblingPath) {
60
+ // Tell init-node.ts (in code-map / the SDK bundle) where the wasm
61
+ // is. The locateFile callback there will hand this path to
62
+ // emscripten, which fs.readFile's it.
63
+ process.env.ANVIL_TREE_SITTER_WASM_PATH = siblingPath
64
+
65
+ // Also publish on globalThis so the smoke handler in index.tsx can
66
+ // read it without touching process.env (which is gated by the env
67
+ // architecture check outside the allowlisted pre-init files).
68
+ ;(
69
+ globalThis as { __ANVIL_TREE_SITTER_WASM_PATH__?: string }
70
+ ).__ANVIL_TREE_SITTER_WASM_PATH__ = siblingPath
71
+
72
+ // Also try the synchronous-bytes path: hand the bytes straight to
73
+ // Parser.init({ wasmBinary }) so the SDK doesn't need to round-trip
74
+ // through emscripten's path resolution. Both channels feed the same
75
+ // tree-sitter init; whichever one trips first wins.
76
+ try {
77
+ const buf = readFileSync(siblingPath)
78
+ ;(
79
+ globalThis as { __ANVIL_TREE_SITTER_WASM_BINARY__?: Uint8Array }
80
+ ).__ANVIL_TREE_SITTER_WASM_BINARY__ = new Uint8Array(
81
+ buf.buffer,
82
+ buf.byteOffset,
83
+ buf.byteLength,
84
+ )
85
+ } catch (err) {
86
+ console.error(
87
+ '[tree-sitter pre-init] readFileSync failed for sibling wasm at',
88
+ siblingPath,
89
+ '—',
90
+ err instanceof Error ? err.message : String(err),
91
+ )
92
+ }
93
+ }
94
+
95
+ // `--smoke-tree-sitter` is the deterministic CI gate. The handler lives at
96
+ // the top of main() in cli/src/index.tsx (before parseArgs).
@@ -0,0 +1,102 @@
1
+ import { mkdirSync, readdirSync, statSync } from 'fs'
2
+ import path from 'path'
3
+
4
+ import { getConfigDir } from './utils/auth'
5
+
6
+ let projectRoot: string | undefined
7
+ let currentChatId: string | undefined
8
+
9
+ function ensureChatDirectory(dir: string) {
10
+ mkdirSync(dir, { recursive: true })
11
+ }
12
+
13
+ export function setProjectRoot(dir: string) {
14
+ projectRoot = dir
15
+ return projectRoot
16
+ }
17
+
18
+ export function getProjectRoot() {
19
+ if (!projectRoot) {
20
+ throw new Error('Project root not set')
21
+ }
22
+ return projectRoot
23
+ }
24
+
25
+ export function tryGetProjectRoot() {
26
+ return projectRoot
27
+ }
28
+
29
+ export function getCurrentChatId() {
30
+ if (!currentChatId) {
31
+ currentChatId = new Date().toISOString().replace(/:/g, '-')
32
+ }
33
+ return currentChatId
34
+ }
35
+
36
+ export function setCurrentChatId(chatId: string) {
37
+ currentChatId = chatId
38
+ return currentChatId
39
+ }
40
+
41
+ export function startNewChat() {
42
+ currentChatId = new Date().toISOString().replace(/:/g, '-')
43
+ return currentChatId
44
+ }
45
+
46
+ // Get the project-specific data directory
47
+ export function getProjectDataDir(): string {
48
+ const root = getProjectRoot()
49
+ if (!root) {
50
+ throw new Error('Project root not set')
51
+ }
52
+
53
+ const baseName = path.basename(root)
54
+ const baseDir = path.join(getConfigDir(), 'projects', baseName)
55
+
56
+ return baseDir
57
+ }
58
+
59
+ /**
60
+ * Find the most recent chat directory based on modification time
61
+ * Returns null if no chat directories exist
62
+ */
63
+ export function getMostRecentChatDir(): string | null {
64
+ try {
65
+ const chatsDir = path.join(getProjectDataDir(), 'chats')
66
+ if (!statSync(chatsDir, { throwIfNoEntry: false })) {
67
+ return null
68
+ }
69
+
70
+ const chatDirs = readdirSync(chatsDir)
71
+ .map((name) => {
72
+ const fullPath = path.join(chatsDir, name)
73
+ try {
74
+ const stat = statSync(fullPath)
75
+ return { name, fullPath, mtime: stat.mtime }
76
+ } catch {
77
+ return null
78
+ }
79
+ })
80
+ .filter(
81
+ (item): item is { name: string; fullPath: string; mtime: Date } =>
82
+ item !== null && statSync(item.fullPath).isDirectory(),
83
+ )
84
+
85
+ if (chatDirs.length === 0) {
86
+ return null
87
+ }
88
+
89
+ // Sort by modification time, most recent first
90
+ chatDirs.sort((a, b) => b.mtime.getTime() - a.mtime.getTime())
91
+ return chatDirs[0].fullPath
92
+ } catch {
93
+ return null
94
+ }
95
+ }
96
+
97
+ export function getCurrentChatDir(): string {
98
+ const chatId = getCurrentChatId()
99
+ const dir = path.join(getProjectDataDir(), 'chats', chatId)
100
+ ensureChatDirectory(dir)
101
+ return dir
102
+ }
@@ -0,0 +1,295 @@
1
+ /**
2
+ * Interactive key-setup wizard for first-time CLI boot.
3
+ *
4
+ * Prompts the user to paste API keys for DeepSeek (required), Gemini
5
+ * (required), and Serper (optional). Saves them to .env.local so they
6
+ * persist across sessions.
7
+ *
8
+ * Runs BEFORE the TUI renderer starts, using plain console I/O.
9
+ */
10
+
11
+ import fs from 'fs'
12
+ import os from 'os'
13
+ import path from 'path'
14
+ import { createInterface } from 'readline'
15
+
16
+ import { cyan, green, yellow, bold, dim } from 'picocolors'
17
+
18
+ // ─── constants ───────────────────────────────────────────────────────────────
19
+
20
+ const REQUIRED_KEYS = [
21
+ {
22
+ envVar: 'DEEPSEEK_API_KEY',
23
+ label: 'DeepSeek API Key',
24
+ helpUrl: 'https://platform.deepseek.com/api_keys',
25
+ description: 'Powers the main coding agent (DeepSeek V4).',
26
+ },
27
+ {
28
+ envVar: 'GEMINI_API_KEY',
29
+ label: 'Gemini API Key',
30
+ helpUrl: 'https://aistudio.google.com/apikey',
31
+ description: 'Powers vision analysis, file search, and helper sub-agents.',
32
+ },
33
+ ] as const
34
+
35
+ const OPTIONAL_KEYS = [
36
+ {
37
+ envVar: 'SERPER_API_KEY',
38
+ label: 'Serper API Key',
39
+ helpUrl: 'https://serper.dev',
40
+ description: 'Enables Google Search (web_search tool). Without it, web search is disabled.',
41
+ },
42
+ ] as const
43
+
44
+ // ─── helpers ─────────────────────────────────────────────────────────────────
45
+
46
+ function askQuestion(
47
+ rl: ReturnType<typeof createInterface>,
48
+ question: string,
49
+ ): Promise<string> {
50
+ return new Promise((resolve) => {
51
+ rl.question(question, (answer) => {
52
+ resolve(answer.trim())
53
+ })
54
+ })
55
+ }
56
+
57
+ function isValidApiKey(key: string): boolean {
58
+ // Loose check: non-empty, no whitespace, reasonable length
59
+ return key.length > 5 && !/\s/.test(key)
60
+ }
61
+
62
+ function maskKey(key: string): string {
63
+ if (key.length <= 8) return '••••••••'
64
+ return key.slice(0, 4) + '•'.repeat(Math.min(key.length - 8, 20)) + key.slice(-4)
65
+ }
66
+
67
+ // ─── .env.local writer ───────────────────────────────────────────────────────
68
+
69
+ function getGlobalEnvPath(): string {
70
+ return path.join(os.homedir(), '.env.local')
71
+ }
72
+
73
+ function readExistingEnv(filePath: string): Record<string, string> {
74
+ const result: Record<string, string> = {}
75
+ if (!fs.existsSync(filePath)) return result
76
+
77
+ const content = fs.readFileSync(filePath, 'utf-8')
78
+ for (const rawLine of content.split('\n')) {
79
+ const line = rawLine.trim()
80
+ if (!line || line.startsWith('#') || line.startsWith('//')) continue
81
+ const eqIndex = line.indexOf('=')
82
+ if (eqIndex === -1) continue
83
+ const key = line.slice(0, eqIndex).trim()
84
+ let value = line.slice(eqIndex + 1).trim()
85
+ if (
86
+ (value.startsWith('"') && value.endsWith('"')) ||
87
+ (value.startsWith("'") && value.endsWith("'"))
88
+ ) {
89
+ value = value.slice(1, -1)
90
+ }
91
+ if (key) result[key] = value
92
+ }
93
+ return result
94
+ }
95
+
96
+ function writeEnvFile(
97
+ filePath: string,
98
+ vars: Record<string, string>,
99
+ ): void {
100
+ const lines: string[] = [
101
+ '# API keys for Anvil / Freebuff CLI',
102
+ '# Generated by the setup wizard — do not commit this file.',
103
+ '',
104
+ ]
105
+
106
+ // Group: required keys first, then optional, then anything else
107
+ const allKeyOrder = [
108
+ ...REQUIRED_KEYS.map((k) => k.envVar),
109
+ ...OPTIONAL_KEYS.map((k) => k.envVar),
110
+ ]
111
+
112
+ const written = new Set<string>()
113
+
114
+ for (const key of allKeyOrder) {
115
+ if (key in vars) {
116
+ lines.push(`${key}=${vars[key]}`)
117
+ written.add(key)
118
+ }
119
+ }
120
+
121
+ // Append any extra keys not in our schema
122
+ for (const [key, value] of Object.entries(vars)) {
123
+ if (!written.has(key)) {
124
+ lines.push(`${key}=${value}`)
125
+ }
126
+ }
127
+
128
+ lines.push('')
129
+ fs.writeFileSync(filePath, lines.join('\n'), 'utf-8')
130
+ }
131
+
132
+ // ─── main wizard ─────────────────────────────────────────────────────────────
133
+
134
+ export interface KeySetupResult {
135
+ /** true if the wizard ran and saved keys */
136
+ completed: boolean
137
+ /** true if all required keys are present (either pre-existing or entered) */
138
+ allRequired: boolean
139
+ }
140
+
141
+ /**
142
+ * Run the interactive key-setup wizard.
143
+ *
144
+ * @param cwd Project root (where .env.local is written)
145
+ * @param force If true, re-prompt even if keys already exist (for `setup` command)
146
+ */
147
+ export async function runKeySetup(
148
+ _cwd: string,
149
+ force = false,
150
+ ): Promise<KeySetupResult> {
151
+ const envPath = getGlobalEnvPath()
152
+ const existing = readExistingEnv(envPath)
153
+
154
+ // Check which keys are already set (in existing .env.local OR in process.env)
155
+ const hasKey = (envVar: string): boolean =>
156
+ Boolean(existing[envVar] || process.env[envVar])
157
+
158
+ const missingRequired = REQUIRED_KEYS.filter((k) => !hasKey(k.envVar))
159
+ const missingOptional = OPTIONAL_KEYS.filter((k) => !hasKey(k.envVar))
160
+
161
+ // If nothing is missing and not forced, skip the wizard
162
+ if (missingRequired.length === 0 && missingOptional.length === 0 && !force) {
163
+ return { completed: false, allRequired: true }
164
+ }
165
+
166
+ console.log()
167
+ console.log(bold(' ╔══════════════════════════════════════════════╗'))
168
+ console.log(bold(' ║ 🔑 API Key Setup Wizard ║'))
169
+ console.log(bold(' ╚══════════════════════════════════════════════╝'))
170
+ console.log()
171
+
172
+ if (missingRequired.length > 0) {
173
+ console.log(cyan(' Required keys to get started:'))
174
+ console.log()
175
+ for (const k of missingRequired) {
176
+ console.log(` ${bold(k.label)}`)
177
+ console.log(` ${dim(k.description)}`)
178
+ console.log(` Get one at: ${cyan(k.helpUrl)}`)
179
+ console.log()
180
+ }
181
+ }
182
+
183
+ if (missingOptional.length > 0 && !force) {
184
+ console.log(yellow(' Optional keys (can be added later):'))
185
+ console.log()
186
+ for (const k of missingOptional) {
187
+ console.log(` ${bold(k.label)}`)
188
+ console.log(` ${dim(k.description)}`)
189
+ console.log(` Get one at: ${cyan(k.helpUrl)}`)
190
+ console.log()
191
+ }
192
+ }
193
+
194
+ const rl = createInterface({
195
+ input: process.stdin,
196
+ output: process.stdout,
197
+ })
198
+
199
+ const newVars: Record<string, string> = { ...existing }
200
+
201
+ try {
202
+ // ── Required keys ──────────────────────────────────────────────────
203
+ for (const k of REQUIRED_KEYS) {
204
+ if (hasKey(k.envVar) && !force) continue
205
+
206
+ const existingVal = existing[k.envVar] || process.env[k.envVar]
207
+ const suffix = existingVal ? ` ${dim(`(currently: ${maskKey(existingVal)})`)}` : ''
208
+
209
+ let attempts = 0
210
+ while (attempts < 3) {
211
+ const answer = await askQuestion(
212
+ rl,
213
+ ` Paste your ${k.label}${suffix}\n → `,
214
+ )
215
+
216
+ // Allow skipping if there's already a value (force mode)
217
+ if (!answer && existingVal) {
218
+ newVars[k.envVar] = existingVal
219
+ break
220
+ }
221
+
222
+ if (!answer) {
223
+ console.log(yellow(` ⚠ ${k.label} is required. Press Ctrl+C to abort.`))
224
+ attempts++
225
+ continue
226
+ }
227
+
228
+ if (!isValidApiKey(answer)) {
229
+ console.log(yellow(' ⚠ That doesn\'t look like a valid API key. Please try again.'))
230
+ attempts++
231
+ continue
232
+ }
233
+
234
+ newVars[k.envVar] = answer
235
+ console.log(green(` ✓ ${k.label} saved`))
236
+ console.log()
237
+ break
238
+ }
239
+
240
+ if (attempts >= 3) {
241
+ console.log()
242
+ console.log(yellow(' Too many attempts. Please set the key manually in .env.local'))
243
+ console.log(yellow(` and re-run the CLI.`))
244
+ console.log()
245
+ rl.close()
246
+ return { completed: false, allRequired: false }
247
+ }
248
+ }
249
+
250
+ // ── Optional keys ──────────────────────────────────────────────────
251
+ for (const k of OPTIONAL_KEYS) {
252
+ if (hasKey(k.envVar) && !force) continue
253
+
254
+ const existingVal = existing[k.envVar] || process.env[k.envVar]
255
+ const suffix = existingVal ? ` ${dim(`(currently: ${maskKey(existingVal)})`)}` : ''
256
+
257
+ const answer = await askQuestion(
258
+ rl,
259
+ ` Paste your ${k.label} ${dim('(optional, press Enter to skip)')}${suffix}\n → `,
260
+ )
261
+
262
+ if (answer && isValidApiKey(answer)) {
263
+ newVars[k.envVar] = answer
264
+ console.log(green(` ✓ ${k.label} saved`))
265
+ console.log()
266
+ } else if (answer && !isValidApiKey(answer)) {
267
+ console.log(yellow(' ⚠ Invalid format, skipping.'))
268
+ console.log()
269
+ } else {
270
+ console.log(dim(' Skipped.'))
271
+ console.log()
272
+ }
273
+ }
274
+ } finally {
275
+ rl.close()
276
+ }
277
+
278
+ // ── Save to .env.local ────────────────────────────────────────────────
279
+ writeEnvFile(envPath, newVars)
280
+
281
+ // Also load into process.env so the current session works immediately
282
+ for (const [key, value] of Object.entries(newVars)) {
283
+ if (!process.env[key]) {
284
+ process.env[key] = value
285
+ }
286
+ }
287
+
288
+ console.log(green(` ✓ Keys saved to ${envPath}`))
289
+ console.log()
290
+ console.log(dim(' Tip: Add .env.local to your .gitignore to keep keys out of git.'))
291
+ console.log()
292
+
293
+ const allRequired = REQUIRED_KEYS.every((k) => Boolean(newVars[k.envVar]))
294
+ return { completed: true, allRequired }
295
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Load environment variables from .env.local at runtime.
3
+ *
4
+ * Keys live in the user's HOME directory (~/.env.local) so they persist
5
+ * across every project and working directory. Bun does NOT auto-load
6
+ * .env.local for compiled binaries, so this module parses the simple
7
+ * KEY=VALUE format and applies values to process.env.
8
+ */
9
+
10
+ import fs from 'fs'
11
+ import os from 'os'
12
+ import path from 'path'
13
+
14
+ /**
15
+ * Resolve the global .env.local path in the user's home directory.
16
+ */
17
+ function getGlobalEnvPath(): string {
18
+ return path.join(os.homedir(), '.env.local')
19
+ }
20
+
21
+ /**
22
+ * Parse a .env.local file into a record of key → value.
23
+ * Handles:
24
+ * - # and // comments
25
+ * - KEY=value, KEY="value", KEY='value'
26
+ * - Trims whitespace around keys and values
27
+ */
28
+ function parseEnvFile(content: string): Record<string, string> {
29
+ const result: Record<string, string> = {}
30
+
31
+ for (const rawLine of content.split('\n')) {
32
+ const line = rawLine.trim()
33
+
34
+ // Skip empty lines and comments
35
+ if (!line || line.startsWith('#') || line.startsWith('//')) continue
36
+
37
+ const eqIndex = line.indexOf('=')
38
+ if (eqIndex === -1) continue
39
+
40
+ const key = line.slice(0, eqIndex).trim()
41
+ let value = line.slice(eqIndex + 1).trim()
42
+
43
+ // Strip surrounding quotes (single or double)
44
+ if (
45
+ (value.startsWith('"') && value.endsWith('"')) ||
46
+ (value.startsWith("'") && value.endsWith("'"))
47
+ ) {
48
+ value = value.slice(1, -1)
49
+ }
50
+
51
+ if (key) {
52
+ result[key] = value
53
+ }
54
+ }
55
+
56
+ return result
57
+ }
58
+
59
+ /**
60
+ * Load .env.local into process.env. Existing env vars are NOT overwritten.
61
+ * Looks in ~/ .env.local first, then walks up from cwd.
62
+ *
63
+ * @returns The path to the loaded .env.local, or null if none found.
64
+ */
65
+ export function loadEnvLocal(cwd?: string): string | null {
66
+ // Always check the global home-dir location first.
67
+ const globalPath = getGlobalEnvPath()
68
+ let envPath: string | null = null
69
+
70
+ if (fs.existsSync(globalPath)) {
71
+ envPath = globalPath
72
+ } else if (cwd) {
73
+ // Fallback: walk up from cwd (legacy behavior).
74
+ let dir = path.resolve(cwd)
75
+ const root = path.parse(dir).root
76
+ while (dir !== root) {
77
+ const candidate = path.join(dir, '.env.local')
78
+ if (fs.existsSync(candidate)) {
79
+ envPath = candidate
80
+ break
81
+ }
82
+ const parent = path.dirname(dir)
83
+ if (parent === dir) break
84
+ dir = parent
85
+ }
86
+ }
87
+
88
+ if (!envPath) return null
89
+
90
+ const content = fs.readFileSync(envPath, 'utf-8')
91
+ const vars = parseEnvFile(content)
92
+
93
+ let loaded = 0
94
+ for (const [key, value] of Object.entries(vars)) {
95
+ if (!(key in process.env)) {
96
+ process.env[key] = value
97
+ loaded++
98
+ }
99
+ }
100
+
101
+ if (loaded > 0) {
102
+ console.log(` Loaded ${loaded} env var(s) from ${envPath}`)
103
+ }
104
+
105
+ return envPath
106
+ }
@@ -0,0 +1,22 @@
1
+ import { afterEach, describe, expect, test } from 'bun:test'
2
+
3
+ import { useChatStore } from '../chat-store'
4
+
5
+ afterEach(() => {
6
+ const store = useChatStore.getState()
7
+ store.setInputFocused(true)
8
+ store.setIsFocusSupported(false)
9
+ })
10
+
11
+ describe('chat store focus state', () => {
12
+ test('chat reset preserves terminal focus and detected capabilities', () => {
13
+ const store = useChatStore.getState()
14
+ store.setInputFocused(false)
15
+ store.setIsFocusSupported(true)
16
+
17
+ store.reset()
18
+
19
+ expect(useChatStore.getState().inputFocused).toBe(false)
20
+ expect(useChatStore.getState().isFocusSupported).toBe(true)
21
+ })
22
+ })