synabun 2.1.0 → 2.2.0

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 (317) hide show
  1. package/CHANGELOG.md +121 -0
  2. package/README.md +1 -1
  3. package/docs/whatsapp.md +11 -11
  4. package/lib/client-config-repair.js +13 -9
  5. package/lib/external-tools.js +6 -5
  6. package/lib/update-snapshot.js +77 -3
  7. package/lib/virustotal.mjs +12 -1
  8. package/mcp-server/dist/index.js +1 -1
  9. package/mcp-server/dist/index.js.map +1 -1
  10. package/mcp-server/dist/services/budget-judgments.d.ts +1 -1
  11. package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -1
  12. package/mcp-server/dist/services/budget-judgments.js +5 -3
  13. package/mcp-server/dist/services/budget-judgments.js.map +1 -1
  14. package/mcp-server/dist/services/identity.d.ts +2 -2
  15. package/mcp-server/dist/services/identity.js +2 -2
  16. package/mcp-server/dist/services/memory-sessions.d.ts +1 -1
  17. package/mcp-server/dist/services/memory-sessions.d.ts.map +1 -1
  18. package/mcp-server/dist/services/memory-sessions.js +28 -2
  19. package/mcp-server/dist/services/memory-sessions.js.map +1 -1
  20. package/mcp-server/dist/services/neural-interface.d.ts +2 -1
  21. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
  22. package/mcp-server/dist/services/neural-interface.js.map +1 -1
  23. package/mcp-server/dist/services/server-instructions.d.ts.map +1 -1
  24. package/mcp-server/dist/services/server-instructions.js +2 -1
  25. package/mcp-server/dist/services/server-instructions.js.map +1 -1
  26. package/mcp-server/dist/services/tool-catalog.js +1 -1
  27. package/mcp-server/dist/services/tool-catalog.js.map +1 -1
  28. package/mcp-server/dist/services/tool-list.d.ts +1 -1
  29. package/mcp-server/dist/services/tool-list.d.ts.map +1 -1
  30. package/mcp-server/dist/services/tool-runtime.d.ts +2 -2
  31. package/mcp-server/dist/services/tool-runtime.d.ts.map +1 -1
  32. package/mcp-server/dist/services/tool-runtime.js +1 -1
  33. package/mcp-server/dist/services/tool-runtime.js.map +1 -1
  34. package/mcp-server/dist/services/typesafe-config.js +1 -1
  35. package/mcp-server/dist/services/typesafe-config.js.map +1 -1
  36. package/mcp-server/dist/tools/agents.d.ts +8 -8
  37. package/mcp-server/dist/tools/agents.d.ts.map +1 -1
  38. package/mcp-server/dist/tools/agents.js +10 -9
  39. package/mcp-server/dist/tools/agents.js.map +1 -1
  40. package/mcp-server/dist/tools/choice.d.ts +6 -0
  41. package/mcp-server/dist/tools/choice.d.ts.map +1 -1
  42. package/mcp-server/dist/tools/choice.js +18 -13
  43. package/mcp-server/dist/tools/choice.js.map +1 -1
  44. package/mcp-server/dist/tools/git-tools.js +2 -2
  45. package/mcp-server/dist/tools/git-tools.js.map +1 -1
  46. package/mcp-server/dist/tools/morelogin.d.ts.map +1 -1
  47. package/mcp-server/dist/tools/morelogin.js +9 -6
  48. package/mcp-server/dist/tools/morelogin.js.map +1 -1
  49. package/mcp-server/dist/tools/skill-tools.d.ts +8 -8
  50. package/mcp-server/dist/tools/skill-tools.d.ts.map +1 -1
  51. package/mcp-server/dist/tools/skill-tools.js +11 -8
  52. package/mcp-server/dist/tools/skill-tools.js.map +1 -1
  53. package/mcp-server/package-lock.json +2 -2
  54. package/mcp-server/package.json +1 -1
  55. package/mcp-server/src/index.ts +1 -1
  56. package/mcp-server/src/services/budget-judgments.ts +5 -3
  57. package/mcp-server/src/services/identity.ts +2 -2
  58. package/mcp-server/src/services/memory-sessions.ts +24 -3
  59. package/mcp-server/src/services/neural-interface.ts +2 -1
  60. package/mcp-server/src/services/server-instructions.ts +2 -1
  61. package/mcp-server/src/services/tool-catalog.ts +1 -1
  62. package/mcp-server/src/services/tool-list.ts +1 -1
  63. package/mcp-server/src/services/tool-runtime.ts +2 -2
  64. package/mcp-server/src/services/typesafe-config.ts +1 -1
  65. package/mcp-server/src/tools/agents.ts +10 -9
  66. package/mcp-server/src/tools/choice.ts +17 -12
  67. package/mcp-server/src/tools/git-tools.ts +2 -2
  68. package/mcp-server/src/tools/morelogin.ts +6 -4
  69. package/mcp-server/src/tools/skill-tools.ts +11 -8
  70. package/neural-interface/i18n/en.json +501 -39
  71. package/neural-interface/i18n/pt-BR.json +501 -39
  72. package/neural-interface/lib/agent-sandbox-elevation.js +6 -0
  73. package/neural-interface/lib/antigravity/acp-client-host.js +554 -0
  74. package/neural-interface/lib/antigravity/acp-connection.js +299 -0
  75. package/neural-interface/lib/antigravity/bypass.js +100 -0
  76. package/neural-interface/lib/antigravity/catalog.js +273 -0
  77. package/neural-interface/lib/antigravity/home.js +260 -0
  78. package/neural-interface/lib/antigravity/migrate-gemini.js +139 -0
  79. package/neural-interface/lib/antigravity/panel-bridge.js +1058 -0
  80. package/neural-interface/lib/antigravity/policy-hook.mjs +78 -0
  81. package/neural-interface/lib/antigravity/policy.js +74 -0
  82. package/neural-interface/lib/antigravity/tool-call.js +178 -0
  83. package/neural-interface/lib/antigravity/transcripts.js +252 -0
  84. package/neural-interface/lib/antigravity/usage.js +43 -0
  85. package/neural-interface/lib/assistant-api.js +1 -0
  86. package/neural-interface/lib/assistant-brains/antigravity.js +423 -0
  87. package/neural-interface/lib/assistant-budget-planner.js +7 -3
  88. package/neural-interface/lib/assistant-budget-split.js +42 -15
  89. package/neural-interface/lib/assistant-catalog.js +76 -9
  90. package/neural-interface/lib/assistant-config.js +6 -3
  91. package/neural-interface/lib/assistant-dispatch.js +207 -49
  92. package/neural-interface/lib/assistant-envelope.js +153 -1
  93. package/neural-interface/lib/assistant-evidence.js +80 -0
  94. package/neural-interface/lib/assistant-hidden-models.js +7 -5
  95. package/neural-interface/lib/assistant-persona.js +16 -11
  96. package/neural-interface/lib/assistant-plan-permissions.js +15 -0
  97. package/neural-interface/lib/assistant-router.js +237 -35
  98. package/neural-interface/lib/assistant-runtime.js +158 -12
  99. package/neural-interface/lib/assistant-task-prompt.js +10 -8
  100. package/neural-interface/lib/assistant-tools.js +2 -2
  101. package/neural-interface/lib/assistant-usage.js +29 -0
  102. package/neural-interface/lib/backup-service.js +6 -1
  103. package/neural-interface/lib/browser-guard-cdp.js +5 -4
  104. package/neural-interface/lib/browser-network-guard.js +274 -82
  105. package/neural-interface/lib/browser-tool-policy.js +12 -7
  106. package/neural-interface/lib/claude-agent-bridge.js +7 -0
  107. package/neural-interface/lib/claude-history-cache.js +210 -0
  108. package/neural-interface/lib/claude-model-catalog.js +5 -1
  109. package/neural-interface/lib/cli-install-api.js +1 -1
  110. package/neural-interface/lib/cli-install.js +160 -31
  111. package/neural-interface/lib/cli-resolver.js +9 -2
  112. package/neural-interface/lib/cli-update-planner.js +25 -15
  113. package/neural-interface/lib/codex-rollout-reader.js +52 -0
  114. package/neural-interface/lib/codex-worker-app-server.js +25 -6
  115. package/neural-interface/lib/editor-git-api.js +3 -20
  116. package/neural-interface/lib/exec-loop-command.js +9 -2
  117. package/neural-interface/lib/file-explorer-api.js +74 -40
  118. package/neural-interface/lib/file-explorer-watch.js +9 -3
  119. package/neural-interface/lib/git-bash/install.js +52 -6
  120. package/neural-interface/lib/git-bash/manager.js +59 -17
  121. package/neural-interface/lib/git-bash/pins.js +12 -0
  122. package/neural-interface/lib/git-read.js +2 -2
  123. package/neural-interface/lib/git-status-runner.js +390 -0
  124. package/neural-interface/lib/git-status-watchdog.js +40 -0
  125. package/neural-interface/lib/greeting/budgets.js +1 -0
  126. package/neural-interface/lib/greeting/engine.js +2 -2
  127. package/neural-interface/lib/greeting/sources.js +10 -12
  128. package/neural-interface/lib/greeting/writer.js +1 -1
  129. package/neural-interface/lib/mcp-client-config.js +37 -6
  130. package/neural-interface/lib/mcp-external-servers.js +13 -4
  131. package/neural-interface/lib/mcp-installer.js +2 -1
  132. package/neural-interface/lib/memory-session-capture.js +17 -3
  133. package/neural-interface/lib/morelogin.js +147 -25
  134. package/neural-interface/lib/native-loop-providers.js +420 -21
  135. package/neural-interface/lib/native-loop-runtime.js +23 -2
  136. package/neural-interface/lib/opencode-native-boundary.js +11 -2
  137. package/neural-interface/lib/owner-authority.js +1 -1
  138. package/neural-interface/lib/path-confine.js +12 -9
  139. package/neural-interface/lib/peer-identity.js +32 -2
  140. package/neural-interface/lib/pty-host/core.js +28 -1
  141. package/neural-interface/lib/pty-host/lifetime.js +12 -0
  142. package/neural-interface/lib/pty-host/manager.js +12 -1
  143. package/neural-interface/lib/remote-policy.js +1 -1
  144. package/neural-interface/lib/request-gate.js +3 -0
  145. package/neural-interface/lib/rulesets/api.js +6 -5
  146. package/neural-interface/lib/rulesets/fs-safe.js +1 -1
  147. package/neural-interface/lib/rulesets/installer.js +89 -10
  148. package/neural-interface/lib/rulesets/render.js +1 -1
  149. package/neural-interface/lib/session-cache-builder.js +64 -1
  150. package/neural-interface/lib/skills/catalog.js +5 -1
  151. package/neural-interface/lib/skills/creation-brief.js +1 -1
  152. package/neural-interface/lib/skills/creation-sources.js +19 -1
  153. package/neural-interface/lib/skills/deploy.js +9 -4
  154. package/neural-interface/lib/skills/draft-confinement.js +3 -1
  155. package/neural-interface/lib/skills/eval-confinement.js +3 -1
  156. package/neural-interface/lib/skills/eval-trace.js +16 -0
  157. package/neural-interface/lib/skills/export.js +5 -2
  158. package/neural-interface/lib/skills/known-bridge-digests.json +58 -4
  159. package/neural-interface/lib/skills/ownership.js +1 -1
  160. package/neural-interface/lib/skills/roots.js +10 -3
  161. package/neural-interface/lib/skills/runtime-checks.js +6 -3
  162. package/neural-interface/lib/skills/service.js +15 -7
  163. package/neural-interface/lib/skills/telemetry.js +6 -0
  164. package/neural-interface/lib/synabun-version.js +1 -1
  165. package/neural-interface/lib/system-activity.js +1 -1
  166. package/neural-interface/lib/whatsapp/brain.js +7 -3
  167. package/neural-interface/lib/whatsapp/cards.js +22 -9
  168. package/neural-interface/lib/whatsapp/config.js +1 -1
  169. package/neural-interface/lib/worker-confinement.js +77 -1
  170. package/neural-interface/lib/worker-question.js +74 -0
  171. package/neural-interface/lib/worker-resume.js +13 -4
  172. package/neural-interface/lib/worker-sandbox.js +46 -16
  173. package/neural-interface/package-lock.json +18 -2
  174. package/neural-interface/package.json +2 -1
  175. package/neural-interface/public/onboarding.html +1 -1
  176. package/neural-interface/public/shared/agent-runtime-options.js +89 -11
  177. package/neural-interface/public/shared/agy/agy-bypass.js +47 -0
  178. package/neural-interface/public/shared/agy/agy-client.js +266 -0
  179. package/neural-interface/public/shared/agy/agy-commands.js +120 -0
  180. package/neural-interface/public/shared/agy/agy-copy.js +326 -0
  181. package/neural-interface/public/shared/agy/agy-diff.js +161 -0
  182. package/neural-interface/public/shared/agy/agy-events.js +469 -0
  183. package/neural-interface/public/shared/agy/agy-greeting.js +26 -0
  184. package/neural-interface/public/shared/agy/agy-options.js +232 -0
  185. package/neural-interface/public/shared/agy/agy-panel.js +2543 -0
  186. package/neural-interface/public/shared/agy/agy-render.js +392 -0
  187. package/neural-interface/public/shared/agy/agy-setup.js +149 -0
  188. package/neural-interface/public/shared/agy/agy-styles.js +604 -0
  189. package/neural-interface/public/shared/agy/agy-tabs-logic.js +181 -0
  190. package/neural-interface/public/shared/api.js +4 -4
  191. package/neural-interface/public/shared/assistant/asst-agents-dock.js +3 -4
  192. package/neural-interface/public/shared/assistant/asst-api.js +1 -1
  193. package/neural-interface/public/shared/assistant/asst-brain-picker.js +56 -9
  194. package/neural-interface/public/shared/assistant/asst-budget.js +5 -4
  195. package/neural-interface/public/shared/assistant/asst-control.js +48 -0
  196. package/neural-interface/public/shared/assistant/asst-hosts.js +16 -7
  197. package/neural-interface/public/shared/assistant/asst-limit-dock.js +6 -1
  198. package/neural-interface/public/shared/assistant/asst-limit-wait.js +38 -5
  199. package/neural-interface/public/shared/assistant/asst-panel.js +45 -8
  200. package/neural-interface/public/shared/assistant/asst-render.js +51 -12
  201. package/neural-interface/public/shared/assistant/asst-route.js +237 -57
  202. package/neural-interface/public/shared/assistant/asst-routes-editor.js +9 -8
  203. package/neural-interface/public/shared/assistant/asst-scenes.js +54 -1
  204. package/neural-interface/public/shared/assistant/asst-sidepanel.js +51 -7
  205. package/neural-interface/public/shared/assistant/asst-slash.js +8 -2
  206. package/neural-interface/public/shared/assistant/asst-state.js +76 -9
  207. package/neural-interface/public/shared/assistant/asst-styles.js +34 -0
  208. package/neural-interface/public/shared/assistant/asst-tool-kinds.js +34 -7
  209. package/neural-interface/public/shared/assistant/asst-usage.js +2 -1
  210. package/neural-interface/public/shared/cdx/cdx-output.js +26 -3
  211. package/neural-interface/public/shared/cdx/cdx-panel.js +14 -2
  212. package/neural-interface/public/shared/cdx/cdx-preview.js +12 -0
  213. package/neural-interface/public/shared/cdx/cdx-protocol.js +11 -10
  214. package/neural-interface/public/shared/cdx/cdx-render.js +10 -9
  215. package/neural-interface/public/shared/cdx/cdx-requests.js +11 -1
  216. package/neural-interface/public/shared/cdx/cdx-styles.js +4 -0
  217. package/neural-interface/public/shared/cdx/cdx-tabs.js +75 -5
  218. package/neural-interface/public/shared/cli-install-controller.js +218 -0
  219. package/neural-interface/public/shared/cli-install-view.js +7 -4
  220. package/neural-interface/public/shared/cli-status.js +9 -8
  221. package/neural-interface/public/shared/constants.js +3 -2
  222. package/neural-interface/public/shared/context-menu-model.js +1 -0
  223. package/neural-interface/public/shared/cp/cp-restore.js +162 -0
  224. package/neural-interface/public/shared/cp/cp-tool-cards.js +10 -3
  225. package/neural-interface/public/shared/desktop-bridge.js +17 -0
  226. package/neural-interface/public/shared/desktop-titlebar-view.js +12 -0
  227. package/neural-interface/public/shared/desktop-titlebar.js +13 -4
  228. package/neural-interface/public/shared/desktop-update-view.js +1 -0
  229. package/neural-interface/public/shared/explorer/fx-api.js +4 -4
  230. package/neural-interface/public/shared/explorer/fx-links.js +3 -3
  231. package/neural-interface/public/shared/explorer/fx-logic.js +1 -1
  232. package/neural-interface/public/shared/explorer/fx-main.js +48 -16
  233. package/neural-interface/public/shared/explorer/fx-tree.js +1 -0
  234. package/neural-interface/public/shared/greeting/greeting-card.js +2 -2
  235. package/neural-interface/public/shared/greeting/greeting-view.js +1 -0
  236. package/neural-interface/public/shared/html-shell.js +21 -4
  237. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +27 -7
  238. package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +13 -4
  239. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +7 -1
  240. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +17 -0
  241. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +46 -3
  242. package/neural-interface/public/shared/provider-icons.js +4 -2
  243. package/neural-interface/public/shared/settings/settings-rules.js +2 -2
  244. package/neural-interface/public/shared/settings/settings-skill-folders-pane.js +1 -1
  245. package/neural-interface/public/shared/settings/settings-skill-folders.js +2 -2
  246. package/neural-interface/public/shared/showcase-privacy.js +6 -1
  247. package/neural-interface/public/shared/sidepanel-session-status.js +3 -3
  248. package/neural-interface/public/shared/skills/ss-creation.js +6 -3
  249. package/neural-interface/public/shared/skills/ss-dialog.js +5 -2
  250. package/neural-interface/public/shared/skills/ss-lint.js +9 -1
  251. package/neural-interface/public/shared/skills/ss-profiles.js +44 -6
  252. package/neural-interface/public/shared/skills/ss-review-view.js +2 -2
  253. package/neural-interface/public/shared/skills/studio/sx-actions.js +1 -1
  254. package/neural-interface/public/shared/skills/studio/sx-create-ai-view.js +1 -1
  255. package/neural-interface/public/shared/skills/studio/sx-create-ai.js +2 -1
  256. package/neural-interface/public/shared/skills/studio/sx-dom.js +2 -1
  257. package/neural-interface/public/shared/skills/studio/sx-fixes-view.js +359 -0
  258. package/neural-interface/public/shared/skills/studio/sx-fixes.js +327 -0
  259. package/neural-interface/public/shared/skills/studio/sx-inspector.js +1 -1
  260. package/neural-interface/public/shared/skills/studio/sx-install-flow.js +9 -6
  261. package/neural-interface/public/shared/skills/studio/sx-install.js +20 -3
  262. package/neural-interface/public/shared/skills/studio/sx-library-view.js +144 -35
  263. package/neural-interface/public/shared/skills/studio/sx-library.js +6 -2
  264. package/neural-interface/public/shared/skills/studio/sx-main.js +11 -2
  265. package/neural-interface/public/shared/skills/studio/sx-review-view.js +1 -1
  266. package/neural-interface/public/shared/skills/studio/sx-subject.js +7 -6
  267. package/neural-interface/public/shared/skills/studio/sx-workspace-view.js +6 -5
  268. package/neural-interface/public/shared/state.js +1 -1
  269. package/neural-interface/public/shared/streaming-markdown.js +58 -0
  270. package/neural-interface/public/shared/styles.css +354 -34
  271. package/neural-interface/public/shared/text-cache.js +29 -0
  272. package/neural-interface/public/shared/tool-list-picker.js +2 -2
  273. package/neural-interface/public/shared/tool-list-view.js +2 -2
  274. package/neural-interface/public/shared/topright-overflow-logic.js +54 -0
  275. package/neural-interface/public/shared/ui-antigravity-panel.js +19 -0
  276. package/neural-interface/public/shared/ui-assistant.js +2 -0
  277. package/neural-interface/public/shared/ui-automation-studio.js +20 -12
  278. package/neural-interface/public/shared/ui-claude-panel.js +299 -77
  279. package/neural-interface/public/shared/ui-cli-banner.js +23 -167
  280. package/neural-interface/public/shared/ui-file-explorer.js +48 -6
  281. package/neural-interface/public/shared/ui-file-links.js +2 -2
  282. package/neural-interface/public/shared/ui-games.js +2 -2
  283. package/neural-interface/public/shared/ui-judgments.js +1 -1
  284. package/neural-interface/public/shared/ui-keybinds.js +8 -2
  285. package/neural-interface/public/shared/ui-link.js +3 -3
  286. package/neural-interface/public/shared/ui-menubar.js +2 -2
  287. package/neural-interface/public/shared/ui-native-loop-router.js +4 -0
  288. package/neural-interface/public/shared/ui-navbar.js +45 -0
  289. package/neural-interface/public/shared/ui-notifications.js +1 -0
  290. package/neural-interface/public/shared/ui-resume.js +9 -7
  291. package/neural-interface/public/shared/ui-schedules-studio.js +41 -10
  292. package/neural-interface/public/shared/ui-scroll-follow.js +16 -5
  293. package/neural-interface/public/shared/ui-sessions.js +22 -12
  294. package/neural-interface/public/shared/ui-settings.js +90 -65
  295. package/neural-interface/public/shared/ui-sidepanel-session-entry.js +9 -2
  296. package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -3
  297. package/neural-interface/public/shared/ui-sidepanel-tray.js +48 -7
  298. package/neural-interface/public/shared/ui-sidepanel-windows.js +3 -3
  299. package/neural-interface/public/shared/ui-terminal.js +146 -10
  300. package/neural-interface/public/shared/ui-tool-updates.js +3 -3
  301. package/neural-interface/public/shared/ui-tooltip.js +71 -46
  302. package/neural-interface/public/shared/ui-topright-overflow.js +233 -0
  303. package/neural-interface/public/shared/ui-whiteboard.js +1 -1
  304. package/neural-interface/public/shared/ui-worker-end.js +1 -1
  305. package/neural-interface/public/shared/whatsapp/wa-view.js +2 -2
  306. package/neural-interface/public/shared/worker-questions.js +34 -0
  307. package/neural-interface/public/sidepanel-session.html +1 -1
  308. package/neural-interface/server.js +644 -426
  309. package/neural-interface/templates/rulesets/README.md +1 -1
  310. package/neural-interface/templates/rulesets/legacy-hashes.json +5 -5
  311. package/neural-interface/templates/rulesets/manifest.json +1 -1
  312. package/npm-shrinkwrap.json +18 -2
  313. package/package.json +2 -1
  314. package/skills/skill-forge/SKILL.md +4 -4
  315. package/skills/synabun/SKILL.md +1 -1
  316. package/skills/synabun/modules/schedule.md +3 -3
  317. /package/neural-interface/templates/rulesets/{gemini.md → antigravity.md} +0 -0
@@ -0,0 +1,210 @@
1
+ // ── Claude Code transcripts, parsed once ──
2
+ // GET /api/claude-code/sessions/:id/messages used to read and JSON.parse a
3
+ // session's whole JSONL on every request, and one session open makes several:
4
+ // the panel's snapshot probe (limit=1), the rebuild (limit=500), "load earlier",
5
+ // "show all" of a clipped result (?tool=). A transcript runs to tens of MB, and
6
+ // on Windows (slower file reads, every read scanned by the antivirus) each pass
7
+ // costs seconds. This keeps the collector of the last few transcripts read and,
8
+ // when the file grew, parses only what was appended.
9
+ //
10
+ // A kept collector is used only for the same file (device and inode) when
11
+ // everything it read is still there unchanged. Same size and time: reused as it
12
+ // is. Grown: the first HEAD_BYTES of the file and the last TAIL_BYTES before
13
+ // where the last read stopped must still be what was read (a rewrite followed by
14
+ // growth changes one of them). Any other change (shorter, same size with a new
15
+ // time, an edit in place) reads the file again from the start. Only whole lines are taken: a last line
16
+ // still being written (no newline yet, and not JSON yet) is read next time.
17
+ // The file is always opened through openConfinedStream, from the canonical
18
+ // paths findSessionTranscript checked.
19
+
20
+ import { statSync } from 'node:fs';
21
+ import { openConfinedStream } from './confined-fs.js';
22
+ import { createHistoryCollector } from './claude-history.js';
23
+
24
+ const NEWLINE = 0x0a;
25
+ const TAIL_BYTES = 4096;
26
+ const HEAD_BYTES = 4096;
27
+ export const HISTORY_CACHE_LIMITS = Object.freeze({
28
+ maxEntries: 4,
29
+ maxIdleMs: 10 * 60_000,
30
+ // A bigger transcript is still answered, from a fresh read each time: it is not kept in memory.
31
+ maxCachedBytes: 256 * 1024 * 1024,
32
+ // All kept transcripts together (bytes parsed, a stand-in for the rows they hold): the least recently used go first.
33
+ maxTotalBytes: 384 * 1024 * 1024,
34
+ });
35
+
36
+ function parseLine(buf, collector) {
37
+ let text = buf.toString('utf-8');
38
+ if (text.endsWith('\r')) text = text.slice(0, -1);
39
+ if (!text.trim()) return true;
40
+ try { collector.add(JSON.parse(text)); return true; } catch { return false; }
41
+ }
42
+
43
+ /**
44
+ * Read `found.realPath` from `from` (an offset at a line start), feeding whole
45
+ * lines to `collector`. With `expectTail`, the first bytes read must equal it
46
+ * (the bytes before the old end); otherwise nothing is added and `null` is
47
+ * returned. Returns `{ offset, tail }`: where the next read starts, and the
48
+ * bytes before it.
49
+ */
50
+ async function readFrom(found, collector, { from = 0, expectTail = null, open }) {
51
+ const skip = expectTail ? expectTail.length : 0;
52
+ const stream = open(found.realFolder, found.realPath, { start: Math.max(0, from - skip) });
53
+ let head = skip ? Buffer.alloc(0) : null; // collects the bytes to compare
54
+ let carry = Buffer.alloc(0);
55
+ let offset = from;
56
+ let tail = expectTail || Buffer.alloc(0);
57
+ let fileHead = from === 0 ? Buffer.alloc(0) : null; // the first bytes of the file, taken on a read from the start
58
+ const consumed = (bytes) => {
59
+ if (fileHead && fileHead.length < HEAD_BYTES) fileHead = Buffer.concat([fileHead, bytes.subarray(0, HEAD_BYTES - fileHead.length)]);
60
+ offset += bytes.length;
61
+ tail = Buffer.concat([tail, bytes.subarray(Math.max(0, bytes.length - TAIL_BYTES))]);
62
+ if (tail.length > TAIL_BYTES) tail = tail.subarray(tail.length - TAIL_BYTES);
63
+ };
64
+ try {
65
+ for await (const raw of stream) {
66
+ let chunk = Buffer.isBuffer(raw) ? raw : Buffer.from(String(raw), 'utf-8');
67
+ if (head) {
68
+ const need = skip - head.length;
69
+ head = Buffer.concat([head, chunk.subarray(0, need)]);
70
+ chunk = chunk.subarray(need);
71
+ if (head.length < skip) continue;
72
+ if (!head.equals(expectTail)) return null; // the file changed under what was read
73
+ head = null;
74
+ }
75
+ let data = carry.length ? Buffer.concat([carry, chunk]) : chunk;
76
+ let start = 0;
77
+ let index;
78
+ while ((index = data.indexOf(NEWLINE, start)) !== -1) {
79
+ parseLine(data.subarray(start, index), collector);
80
+ consumed(data.subarray(start, index + 1));
81
+ start = index + 1;
82
+ }
83
+ carry = Buffer.from(data.subarray(start));
84
+ }
85
+ } finally {
86
+ stream.destroy?.();
87
+ }
88
+ if (head) return null; // the file ends before the bytes that were read last time
89
+ // A last line without its newline: taken when it is whole JSON already, else read next time.
90
+ if (carry.length && parseLine(carry, collector)) consumed(carry);
91
+ return { offset, tail, head: fileHead };
92
+ }
93
+
94
+ /** The first `length` bytes of the file, or less when it is shorter. */
95
+ async function readHead(found, length, open) {
96
+ if (!length) return Buffer.alloc(0);
97
+ const stream = open(found.realFolder, found.realPath, { start: 0, end: length - 1 });
98
+ const parts = [];
99
+ let got = 0;
100
+ try {
101
+ for await (const raw of stream) {
102
+ const chunk = Buffer.isBuffer(raw) ? raw : Buffer.from(String(raw), 'utf-8');
103
+ parts.push(chunk); got += chunk.length;
104
+ if (got >= length) break;
105
+ }
106
+ } finally { stream.destroy?.(); }
107
+ return Buffer.concat(parts).subarray(0, length);
108
+ }
109
+
110
+ /**
111
+ * @param o.open (folder, file, options) => readable stream (openConfinedStream)
112
+ * @param o.stat (path) => fs.Stats
113
+ * @param o.newCollector () => a history collector (lib/claude-history.js)
114
+ */
115
+ export function createHistoryCache({
116
+ open = openConfinedStream,
117
+ stat = statSync,
118
+ newCollector = createHistoryCollector,
119
+ now = Date.now,
120
+ limits = HISTORY_CACHE_LIMITS,
121
+ } = {}) {
122
+ const entries = new Map(); // realPath → { dev, ino, size, mtimeMs, offset, tail, collector, usedAt }
123
+ const running = new Map(); // realPath → promise of the collector being brought up to date
124
+ const stats = { fresh: 0, appended: 0, reused: 0, reread: 0 };
125
+
126
+ const evict = (keep = null) => {
127
+ const at = now();
128
+ for (const [key, entry] of entries) if (key !== keep && at - entry.usedAt > limits.maxIdleMs) entries.delete(key);
129
+ while (entries.size > limits.maxEntries) entries.delete(entries.keys().next().value);
130
+ const maxTotal = limits.maxTotalBytes ?? Infinity;
131
+ let total = 0;
132
+ for (const entry of entries.values()) total += entry.offset;
133
+ for (const [key, entry] of entries) { // oldest first
134
+ if (total <= maxTotal) break;
135
+ entries.delete(key); total -= entry.offset;
136
+ }
137
+ };
138
+ const touch = (key, entry) => {
139
+ entry.usedAt = now();
140
+ entries.delete(key); entries.set(key, entry); // most recently used last
141
+ evict(key);
142
+ };
143
+ // Kept only while it fits: a transcript that grew past the limit is answered this once, then dropped.
144
+ const fits = (size) => size <= limits.maxCachedBytes;
145
+
146
+ async function update(found) {
147
+ const key = found.realPath;
148
+ evict(); // an idle entry goes before it could be reused
149
+ const st = stat(key);
150
+ let entry = entries.get(key) || null;
151
+ if (entry && (entry.dev !== st.dev || entry.ino !== st.ino || st.size < entry.offset)) entry = null;
152
+ if (entry && st.size === entry.size && st.mtimeMs === entry.mtimeMs) {
153
+ stats.reused++;
154
+ touch(key, entry);
155
+ return entry.collector;
156
+ }
157
+ // Changed without growing (same size with a new time, an edit in place): nothing tells what changed.
158
+ if (entry && st.size <= entry.size) { stats.reread++; entry = null; }
159
+ if (entry) {
160
+ let read = null;
161
+ const head = entry.head.length ? await readHead(found, entry.head.length, open) : entry.head;
162
+ if (head.equals(entry.head)) {
163
+ read = await readFrom(found, entry.collector, { from: entry.offset, expectTail: entry.offset ? entry.tail : null, open });
164
+ }
165
+ if (read) {
166
+ stats.appended++;
167
+ Object.assign(entry, { size: st.size, mtimeMs: st.mtimeMs, offset: read.offset, tail: read.tail });
168
+ if (fits(st.size)) touch(key, entry); else entries.delete(key);
169
+ return entry.collector;
170
+ }
171
+ stats.reread++;
172
+ }
173
+ entries.delete(key);
174
+ const collector = newCollector();
175
+ const read = await readFrom(found, collector, { from: 0, open });
176
+ stats.fresh++;
177
+ if (read && fits(st.size)) {
178
+ entries.set(key, { dev: st.dev, ino: st.ino, size: st.size, mtimeMs: st.mtimeMs, offset: read.offset, tail: read.tail, head: read.head, collector, usedAt: now() });
179
+ evict(key);
180
+ }
181
+ return collector;
182
+ }
183
+
184
+ return {
185
+ /** The collector of the transcript `found` names, with every whole line it holds now. */
186
+ async collectorFor(found) {
187
+ const key = found?.realPath;
188
+ if (!key || !found.realFolder) throw new Error('A checked transcript path is required.');
189
+ // One read per file at a time: a second request waits for it and then checks again.
190
+ while (running.has(key)) { try { await running.get(key); } catch {} }
191
+ const job = update(found);
192
+ running.set(key, job);
193
+ try { return await job; } catch (err) { entries.delete(key); throw err; } finally { if (running.get(key) === job) running.delete(key); }
194
+ },
195
+ forget(realPath) { entries.delete(realPath); },
196
+ /** Drop every kept transcript of a session (its file is `<sessionId>.jsonl`, in any project folder). */
197
+ forgetSession(sessionId) {
198
+ if (typeof sessionId !== 'string' || !sessionId) return 0;
199
+ let dropped = 0;
200
+ for (const key of [...entries.keys()]) {
201
+ const name = key.split(/[\\/]/).pop();
202
+ if (name === `${sessionId}.jsonl`) { entries.delete(key); dropped++; }
203
+ }
204
+ return dropped;
205
+ },
206
+ clear() { entries.clear(); },
207
+ get size() { return entries.size; },
208
+ get stats() { return { ...stats }; },
209
+ };
210
+ }
@@ -324,7 +324,7 @@ export function queryClaudeCliModels(claudeBin, { cwd, timeoutMs = DISCOVERY_TIM
324
324
  // re-discovers. A list that is merely old is still given at once, and the CLI is
325
325
  // asked again in the background (one discovery at a time): only a first read, a
326
326
  // changed key or `force` waits for the CLI.
327
- export async function discoverClaudeModels(claudeBin, { force = false, cwd, query = queryClaudeCliModels, account = '' } = {}) {
327
+ export async function discoverClaudeModels(claudeBin, { force = false, nonBlocking = false, cwd, query = queryClaudeCliModels, account = '' } = {}) {
328
328
  const bin = String(claudeBin || '');
329
329
  const key = cacheKeyOf(bin, account);
330
330
  adoptStoredModels(key);
@@ -333,6 +333,10 @@ export async function discoverClaudeModels(claudeBin, { force = false, cwd, quer
333
333
  if (Date.now() - _cache.at >= CACHE_TTL_MS) discover(bin, key, cwd, query).catch(() => {});
334
334
  return { source: 'cache', models: _cache.models };
335
335
  }
336
+ if (!force && nonBlocking) {
337
+ discover(bin, key, cwd, query).catch(() => {});
338
+ return { source: 'fallback', models: [...CLAUDE_FALLBACK_MODELS] };
339
+ }
336
340
  return discover(bin, key, cwd, query);
337
341
  }
338
342
 
@@ -24,7 +24,7 @@ import { Router } from 'express';
24
24
  import { guestForbidden, noStore, requireLocal, requireLocalHost, requireSameOriginJson, sendGuardError, uiOnly } from './http-guards.js';
25
25
  import { openNdjsonStream } from './whatsapp/api.js';
26
26
 
27
- export const INSTALLABLE_TOOLS = Object.freeze(['claude-code', 'codex', 'opencode']);
27
+ export const INSTALLABLE_TOOLS = Object.freeze(['claude-code', 'codex', 'opencode', 'antigravity']);
28
28
 
29
29
  /**
30
30
  * @param {object} deps
@@ -3,10 +3,12 @@
3
3
  // ═══════════════════════════════════════════
4
4
  //
5
5
  // What the side panels' Install button runs (POST /api/cli/install/:tool,
6
- // lib/cli-install-api.js) when Claude Code, Codex or OpenCode is not installed.
7
- // Each tool is installed the way its vendor documents for the system, from the
8
- // vendor's own source, with nothing else needed (no Node, npm, Homebrew or
9
- // Scoop). The sources are fixed here; a request only names the tool.
6
+ // lib/cli-install-api.js) when Claude Code, Codex, OpenCode or the Antigravity
7
+ // CLI is not installed. Each tool is installed the way its vendor documents for
8
+ // the system, from the vendor's own source, with nothing else needed (no Node,
9
+ // npm or Scoop; Homebrew only where it is installed already, for the
10
+ // Antigravity CLI's cask on macOS). The sources are fixed here; a request only
11
+ // names the tool.
10
12
  //
11
13
  // Claude Code Anthropic's native installer: https://claude.ai/install.sh
12
14
  // (bash) on macOS and Linux, https://claude.ai/install.ps1
@@ -24,21 +26,32 @@
24
26
  // runs (it puts opencode in ~/.opencode/bin). Windows has no
25
27
  // one-click install: OpenCode 2 comes from WinGet there
26
28
  // (Anomaly.OpenCode), and the banner shows that command.
29
+ // Antigravity Google's Antigravity CLI (agy). On a Mac with Homebrew, the
30
+ // cask antigravity-cli (Google's signed binary, linked as agy):
31
+ // `brew install --cask antigravity-cli`. Otherwise Google's
32
+ // installer https://antigravity.google/cli/install.sh (bash) on
33
+ // macOS and Linux, https://antigravity.google/cli/install.ps1
34
+ // (powershell.exe -NoProfile -ExecutionPolicy Bypass -File) on
35
+ // Windows, from antigravity.google only. The script checks the
36
+ // binary against the SHA-512 of Google's release manifest and
37
+ // puts agy in ~/.local/bin (%LOCALAPPDATA%\agy\bin on Windows).
27
38
  //
28
39
  // Every download is HTTPS, every redirect too, and only to the vendor's own
29
- // hosts. Nothing runs as administrator; everything lands in the user's own
40
+ // hosts (Homebrew downloads a cask itself and checks it against the cask's
41
+ // SHA-256). Nothing runs as administrator; everything lands in the user's own
30
42
  // folders. Install consents to adding the resolved CLI's folder to persistent
31
43
  // PATH when the vendor did not do so. That is a visible final step, using the
32
44
  // same writer as Add to PATH; a failed PATH write leaves the CLI installed and
33
45
  // reports a warning. Installers run without a terminal and without questions
34
- // (CODEX_NON_INTERACTIVE=1, PowerShell -NonInteractive).
46
+ // (CODEX_NON_INTERACTIVE=1, PowerShell -NonInteractive; NONINTERACTIVE=1 and
47
+ // no auto-update for Homebrew).
35
48
  //
36
49
  // One install per tool at a time. Every failure is a CliInstallError with a
37
50
  // `code` the page has words for. Nothing here throws to a caller of start().
38
51
 
39
52
  import { spawn } from 'node:child_process';
40
53
  import { createHash } from 'node:crypto';
41
- import { existsSync, rmSync } from 'node:fs';
54
+ import { accessSync, constants as fsConstants, existsSync, rmSync, statSync } from 'node:fs';
42
55
  import { mkdtemp, open } from 'node:fs/promises';
43
56
  import os from 'node:os';
44
57
  import path from 'node:path';
@@ -74,7 +87,7 @@ export const CLI_INSTALL_GUIDES = Object.freeze({
74
87
  'claude-code': 'https://code.claude.com/docs/en/setup',
75
88
  codex: 'https://learn.chatgpt.com/docs/codex/cli',
76
89
  opencode: 'https://opencode.ai/v2/docs',
77
- gemini: 'https://github.com/google-gemini/gemini-cli',
90
+ antigravity: 'https://antigravity.google/docs/getting-started',
78
91
  });
79
92
 
80
93
  const GITHUB_DOWNLOAD_HOSTS = ['github.com', 'objects.githubusercontent.com', 'release-assets.githubusercontent.com', 'github-releases.githubusercontent.com'];
@@ -88,10 +101,13 @@ const hostIs = (...hosts) => (url) => hosts.includes(url.hostname);
88
101
 
89
102
  /**
90
103
  * The one-click method for a tool on this system, or null when there is none
91
- * (Gemini CLI, OpenCode on Windows, a 32-bit or other system). Pure: tests ask
92
- * it for every OS.
104
+ * (OpenCode on Windows, a 32-bit or other system). `brew` is the path of a
105
+ * Homebrew this process can run (findBrew), or null: on macOS the Antigravity
106
+ * CLI is installed through it (its cask), else with Google's installer.
107
+ * `manualCommand`, when a plan has one, is what to type for the same install.
108
+ * Pure: tests ask it for every OS.
93
109
  */
94
- export function installPlanFor(toolKey, { platform = process.platform, arch = process.arch, env = process.env, home } = {}) {
110
+ export function installPlanFor(toolKey, { platform = process.platform, arch = process.arch, env = process.env, home, brew = null } = {}) {
95
111
  if (!(SUPPORTED[platform] || []).includes(arch)) return null;
96
112
  const windows = platform === 'win32';
97
113
  switch (toolKey) {
@@ -131,19 +147,77 @@ export function installPlanFor(toolKey, { platform = process.platform, arch = pr
131
147
  label: 'OpenCode 2 install script',
132
148
  env: {},
133
149
  };
150
+ case 'antigravity':
151
+ // Homebrew's cask on a Mac that has Homebrew: the CLI then updates with the user's other casks.
152
+ if (platform === 'darwin' && brew) {
153
+ return {
154
+ tool: toolKey,
155
+ kind: 'brew',
156
+ brew,
157
+ args: ['install', '--cask', 'antigravity-cli'],
158
+ source: 'https://formulae.brew.sh/cask/antigravity-cli',
159
+ label: 'Homebrew cask antigravity-cli',
160
+ manualCommand: 'brew install --cask antigravity-cli',
161
+ env: { HOMEBREW_NO_AUTO_UPDATE: '1', HOMEBREW_NO_ENV_HINTS: '1', NONINTERACTIVE: '1' },
162
+ };
163
+ }
164
+ return {
165
+ tool: toolKey,
166
+ kind: 'script',
167
+ interpreter: windows ? 'powershell' : 'bash',
168
+ url: windows ? 'https://antigravity.google/cli/install.ps1' : 'https://antigravity.google/cli/install.sh',
169
+ allow: hostIs('antigravity.google'),
170
+ source: windows ? 'https://antigravity.google/cli/install.ps1' : 'https://antigravity.google/cli/install.sh',
171
+ label: 'Google Antigravity CLI installer',
172
+ manualCommand: windows ? 'irm https://antigravity.google/cli/install.ps1 | iex' : 'curl -fsSL https://antigravity.google/cli/install.sh | bash',
173
+ env: {},
174
+ };
134
175
  default:
135
176
  return null;
136
177
  }
137
178
  }
138
179
 
139
- /** What the status of a tool says about its one-click install. */
140
- export function describeInstall(toolKey, options) {
141
- const plan = installPlanFor(toolKey, options);
180
+ /** A file this process may run (a link is followed). */
181
+ function isExecutableFile(file) {
182
+ try {
183
+ if (!statSync(file).isFile()) return false;
184
+ accessSync(file, fsConstants.X_OK);
185
+ return true;
186
+ } catch {
187
+ return false;
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Homebrew's brew on macOS: /opt/homebrew/bin (Apple silicon), /usr/local/bin
193
+ * (Intel), then each folder of PATH; the first executable file. A relative
194
+ * PATH folder is never looked in. Null on any other system, and on a Mac
195
+ * without Homebrew.
196
+ */
197
+ export function findBrew({ platform = process.platform, env = process.env, isExecutable = isExecutableFile } = {}) {
198
+ if (platform !== 'darwin') return null;
199
+ const asked = new Set();
200
+ for (const folder of ['/opt/homebrew/bin', '/usr/local/bin', ...String(env.PATH || '').split(':')]) {
201
+ if (!folder || !path.posix.isAbsolute(folder)) continue;
202
+ const file = path.posix.join(folder, 'brew');
203
+ if (asked.has(file)) continue;
204
+ asked.add(file);
205
+ if (isExecutable(file)) return file;
206
+ }
207
+ return null;
208
+ }
209
+
210
+ function describePlan(plan) {
142
211
  return plan
143
- ? { available: true, method: plan.kind, source: plan.source, interpreter: plan.interpreter, label: plan.label }
212
+ ? { available: true, method: plan.kind, source: plan.source, interpreter: plan.interpreter || null, label: plan.label }
144
213
  : { available: false, method: null, source: null, interpreter: null, label: null };
145
214
  }
146
215
 
216
+ /** What the status of a tool says about its one-click install. */
217
+ export function describeInstall(toolKey, options) {
218
+ return describePlan(installPlanFor(toolKey, options));
219
+ }
220
+
147
221
  // ── Transfers ───────────────────────────────────────────────────────────────
148
222
 
149
223
  function errorCode(error) {
@@ -353,6 +427,19 @@ export function createLineSplitter(onLine) {
353
427
  };
354
428
  }
355
429
 
430
+ /**
431
+ * The step a line of Homebrew's output begins, or null: its download (and its
432
+ * download queue's "Fetching"), the check of what it downloaded against the
433
+ * cask's checksum, and the install itself (moving and linking the binary).
434
+ */
435
+ export function brewStepOf(line) {
436
+ const text = String(line ?? '').trim();
437
+ if (/^==> (?:Downloading|Fetching)\b/.test(text)) return 'download';
438
+ if (/^==> Verifying\b/.test(text) || /checksum|sha-?256/i.test(text)) return 'verify';
439
+ if (/^==> (?:Installing|Moving|Linking)\b/.test(text)) return 'run';
440
+ return null;
441
+ }
442
+
356
443
  function sameDir(a, b, platform) {
357
444
  const clean = (value) => {
358
445
  const text = String(value || '').replace(/[\\/]+$/, '');
@@ -372,6 +459,7 @@ export function onPathList(dir, entries, platform = process.platform) {
372
459
  * @param {object} deps
373
460
  * @param {{ resolve: Function }} deps.resolver cli-resolver.js: the final check (force)
374
461
  * @param {() => Promise<string[]>} [deps.readOsPath] the OS PATH a new terminal gets, read afresh
462
+ * @param {(o: { platform: string, env: object }) => string|null} [deps.findBrew] Homebrew's brew, or null (findBrew)
375
463
  * @param {(tool: string, ok: boolean, status: object|null) => void} [deps.onFinished]
376
464
  */
377
465
  export function createCliInstaller({
@@ -383,6 +471,7 @@ export function createCliInstaller({
383
471
  tmpRoot = os.tmpdir(),
384
472
  fetchImpl = fetch,
385
473
  spawnImpl = spawn,
474
+ findBrew: lookForBrew = findBrew,
386
475
  readOsPath = async () => [],
387
476
  addToPath = null,
388
477
  installerPath = null,
@@ -394,9 +483,12 @@ export function createCliInstaller({
394
483
  if (!resolver?.resolve) throw new Error('createCliInstaller needs the CLI resolver');
395
484
  const jobs = new Map();
396
485
  const windows = platform === 'win32';
397
- const planFor = (toolKey) => installPlanFor(toolKey, { platform, arch, env, home });
486
+ // Homebrew is looked for at every ask: one installed meanwhile counts from the next check on.
487
+ const brewOf = () => { try { return lookForBrew({ platform, env }) || null; } catch { return null; } };
488
+ const planFor = (toolKey) => installPlanFor(toolKey, { platform, arch, env, home, brew: brewOf() });
398
489
  const guideOf = (toolKey) => CLI_INSTALL_GUIDES[toolKey] || null;
399
- const manualOf = (toolKey) => buildInstallCommand(toolKey, 'unknown', { platform });
490
+ // The command for the very install the button runs here, when its plan names one; else the vendor's for this system.
491
+ const manualOf = (toolKey) => planFor(toolKey)?.manualCommand || buildInstallCommand(toolKey, 'unknown', { platform });
400
492
 
401
493
  function publicJob(job) {
402
494
  return {
@@ -510,15 +602,19 @@ export function createCliInstaller({
510
602
  return file;
511
603
  }
512
604
 
513
- function runScript(job, plan, scriptFile, workDir, osEntries, { line }) {
514
- const interpreter = interpreterFor(plan);
605
+ /**
606
+ * Run an installer to its end: no stdin, every line of its output to `onLine`, stopped with everything it
607
+ * started when it runs longer than timeoutMs or the job is stopped. Resolves on exit code 0. `name` is what
608
+ * the sentence of a failure calls it (`slowName`, the one of a timeout).
609
+ */
610
+ function runChild(job, plan, { file, args, cwd, osEntries, onLine, name, slowName = name }) {
515
611
  const childEnv = { ...withPathEnv({ ...env, ...plan.env }, installerPathValue(osEntries), platform), TERM: 'dumb', NO_COLOR: '1' };
516
612
  delete childEnv.FORCE_COLOR;
517
613
  return new Promise((resolve, reject) => {
518
614
  let child;
519
615
  try {
520
- child = spawnImpl(interpreter.file, interpreter.args(scriptFile), {
521
- cwd: workDir,
616
+ child = spawnImpl(file, args, {
617
+ cwd,
522
618
  env: childEnv,
523
619
  stdio: ['ignore', 'pipe', 'pipe'],
524
620
  windowsHide: true,
@@ -527,18 +623,18 @@ export function createCliInstaller({
527
623
  detached: !windows,
528
624
  });
529
625
  } catch (error) {
530
- reject(asInstallError(error, 'installer-failed', `${interpreter.file} could not be started.`));
626
+ reject(asInstallError(error, 'installer-failed', `${file} could not be started.`));
531
627
  return;
532
628
  }
533
629
  job.child = child;
534
- const out = createLineSplitter(line);
535
- const err = createLineSplitter(line);
630
+ const out = createLineSplitter(onLine);
631
+ const err = createLineSplitter(onLine);
536
632
  child.stdout?.on('data', chunk => out.push(chunk.toString('utf8')));
537
633
  child.stderr?.on('data', chunk => err.push(chunk.toString('utf8')));
538
634
  let settled = false;
539
635
  const timer = setTimeout(() => {
540
636
  stopChild(child, windows, spawnImpl);
541
- finish(new CliInstallError('timeout', `The installer was still running after ${Math.round(timeoutMs / 60_000)} minutes and was stopped.`));
637
+ finish(new CliInstallError('timeout', `${slowName} was still running after ${Math.round(timeoutMs / 60_000)} minutes and was stopped.`));
542
638
  }, timeoutMs);
543
639
  timer.unref?.();
544
640
  const onAbort = () => { stopChild(child, windows, spawnImpl); finish(new CliInstallError('cancelled', 'The install was stopped.')); };
@@ -552,14 +648,43 @@ export function createCliInstaller({
552
648
  err.end();
553
649
  job.child = null;
554
650
  if (error) reject(error);
555
- else if (code !== 0) reject(new CliInstallError('installer-failed', `The ${plan.label} stopped with exit code ${code}.`, { detail: code }));
651
+ else if (code !== 0) reject(new CliInstallError('installer-failed', `${name} stopped with exit code ${code}.`, { detail: code }));
556
652
  else resolve();
557
653
  }
558
- child.on('error', error => finish(asInstallError(error, 'installer-failed', `${interpreter.file} could not be started (${errorCode(error) || error?.message}).`)));
654
+ child.on('error', error => finish(asInstallError(error, 'installer-failed', `${file} could not be started (${errorCode(error) || error?.message}).`)));
559
655
  child.on('close', (code, signal) => finish(null, code ?? (signal ? 128 : 1)));
560
656
  });
561
657
  }
562
658
 
659
+ function runScript(job, plan, scriptFile, workDir, osEntries, { line }) {
660
+ const interpreter = interpreterFor(plan);
661
+ return runChild(job, plan, {
662
+ file: interpreter.file, args: interpreter.args(scriptFile), cwd: workDir, osEntries, onLine: line,
663
+ name: `The ${plan.label}`, slowName: 'The installer',
664
+ });
665
+ }
666
+
667
+ /**
668
+ * Homebrew installs the cask (`brew install --cask …`, never auto-updating first). It downloads and checks the
669
+ * cask itself: its own lines move the steps (brewStepOf), from 'run' on.
670
+ */
671
+ function runBrew(job, plan, workDir, osEntries, { step, line }) {
672
+ let at = 'run';
673
+ step(at);
674
+ return runChild(job, plan, {
675
+ file: plan.brew, args: plan.args, cwd: workDir, osEntries,
676
+ onLine: (text) => {
677
+ const next = brewStepOf(text);
678
+ if (next && next !== at) {
679
+ at = next;
680
+ step(next);
681
+ }
682
+ line(text);
683
+ },
684
+ name: `brew ${plan.args.join(' ')}`,
685
+ });
686
+ }
687
+
563
688
  async function run(job) {
564
689
  const { plan, tool } = job;
565
690
  const io = stepper(job);
@@ -569,9 +694,13 @@ export function createCliInstaller({
569
694
  emit(job, { type: 'start', tool, method: plan.kind, source: plan.source, label: plan.label, platform, arch });
570
695
  try { before = await readOsPath(); } catch { before = []; }
571
696
  workDir = await mkdtemp(path.join(tmpRoot, `synabun-cli-install-${tool}-`));
572
- const script = await fetchScript(job, plan, workDir, io);
573
- io.step('run');
574
- await runScript(job, plan, script, workDir, before, io);
697
+ if (plan.kind === 'brew') {
698
+ await runBrew(job, plan, workDir, before, io);
699
+ } else {
700
+ const script = await fetchScript(job, plan, workDir, io);
701
+ io.step('run');
702
+ await runScript(job, plan, script, workDir, before, io);
703
+ }
575
704
  io.step('check');
576
705
  const status = await resolver.resolve(tool, { force: true });
577
706
  if (status?.state !== 'installed') {
@@ -618,7 +747,7 @@ export function createCliInstaller({
618
747
  }
619
748
 
620
749
  return {
621
- describe: (toolKey) => describeInstall(toolKey, { platform, arch, env, home }),
750
+ describe: (toolKey) => describePlan(planFor(toolKey)),
622
751
  guideUrl: guideOf,
623
752
  manualCommand: manualOf,
624
753
  jobs() {
@@ -1,5 +1,5 @@
1
1
  // ═══════════════════════════════════════════
2
- // SynaBun — where the user's Claude Code, Codex and OpenCode are
2
+ // SynaBun — where the user's Claude Code, Codex, OpenCode and Antigravity (agy) are
3
3
  // ═══════════════════════════════════════════
4
4
  //
5
5
  // One answer for every place that looks for one of these CLIs: the version
@@ -55,7 +55,7 @@ export const CLI_TOOLS = Object.freeze({
55
55
  'claude-code': Object.freeze({ key: 'claude-code', command: 'claude', label: 'Claude Code' }),
56
56
  codex: Object.freeze({ key: 'codex', command: 'codex', label: 'Codex' }),
57
57
  opencode: Object.freeze({ key: 'opencode', command: 'opencode', label: 'OpenCode' }),
58
- gemini: Object.freeze({ key: 'gemini', command: 'gemini', label: 'Gemini CLI' }),
58
+ antigravity: Object.freeze({ key: 'antigravity', command: 'agy', label: 'Antigravity CLI' }),
59
59
  });
60
60
 
61
61
  const PROBE_TIMEOUT_MS = 15_000;
@@ -134,6 +134,10 @@ function wingetPackageFiles({ localAppData, prefixes, exe, readdir, pathImpl })
134
134
  * OpenCode install script: ~/.opencode/bin/opencode (older scripts:
135
135
  * $OPENCODE_INSTALL_DIR, $XDG_BIN_DIR); on Windows the folder
136
136
  * SynaBun's own install uses, %USERPROFILE%\.opencode\bin
137
+ * Antigravity Google's installer (antigravity.google/cli/install.sh, checked
138
+ * 2026-10-30): ~/.local/bin/agy; install.ps1 on Windows:
139
+ * %LOCALAPPDATA%\agy\bin\agy.exe. Homebrew's cask links agy
140
+ * into Homebrew's bin, which the package-manager folders cover.
137
141
  */
138
142
  export function toolSpecificFiles(toolKey, {
139
143
  platform = process.platform,
@@ -168,6 +172,9 @@ export function toolSpecificFiles(toolKey, {
168
172
  if (env.XDG_BIN_DIR) add(join(env.XDG_BIN_DIR, exe('opencode')), 'standalone');
169
173
  if (windows) for (const file of wingetPackageFiles({ localAppData, prefixes: ['sst.opencode', 'anomalyco.opencode', 'opencode.'], exe: 'opencode.exe', readdir, pathImpl })) add(file, 'winget');
170
174
  break;
175
+ case 'antigravity':
176
+ add(windows ? join(localAppData, 'agy', 'bin', 'agy.exe') : join(home, '.local', 'bin', 'agy'), 'standalone');
177
+ break;
171
178
  default:
172
179
  break;
173
180
  }