synabun 2.1.0 → 2.2.1

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 (332) hide show
  1. package/CHANGELOG.md +139 -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 +504 -39
  71. package/neural-interface/i18n/pt-BR.json +504 -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-brains/codex.js +6 -1
  88. package/neural-interface/lib/assistant-budget-planner.js +7 -3
  89. package/neural-interface/lib/assistant-budget-split.js +42 -15
  90. package/neural-interface/lib/assistant-catalog.js +76 -9
  91. package/neural-interface/lib/assistant-config.js +6 -3
  92. package/neural-interface/lib/assistant-dispatch.js +211 -50
  93. package/neural-interface/lib/assistant-envelope.js +153 -1
  94. package/neural-interface/lib/assistant-evidence.js +80 -0
  95. package/neural-interface/lib/assistant-hidden-models.js +7 -5
  96. package/neural-interface/lib/assistant-persona.js +16 -11
  97. package/neural-interface/lib/assistant-plan-permissions.js +15 -0
  98. package/neural-interface/lib/assistant-router.js +237 -35
  99. package/neural-interface/lib/assistant-runtime.js +158 -12
  100. package/neural-interface/lib/assistant-task-prompt.js +10 -8
  101. package/neural-interface/lib/assistant-tools.js +2 -2
  102. package/neural-interface/lib/assistant-usage.js +29 -0
  103. package/neural-interface/lib/backup-service.js +6 -1
  104. package/neural-interface/lib/browser-guard-cdp.js +5 -4
  105. package/neural-interface/lib/browser-network-guard.js +274 -82
  106. package/neural-interface/lib/browser-tool-policy.js +12 -7
  107. package/neural-interface/lib/claude-agent-bridge.js +7 -0
  108. package/neural-interface/lib/claude-history-cache.js +210 -0
  109. package/neural-interface/lib/claude-model-catalog.js +5 -1
  110. package/neural-interface/lib/cli-install-api.js +1 -1
  111. package/neural-interface/lib/cli-install.js +160 -31
  112. package/neural-interface/lib/cli-resolver.js +9 -2
  113. package/neural-interface/lib/cli-update-planner.js +25 -15
  114. package/neural-interface/lib/codex-rollout-reader.js +52 -0
  115. package/neural-interface/lib/codex-worker-app-server.js +25 -6
  116. package/neural-interface/lib/editor-git-api.js +3 -20
  117. package/neural-interface/lib/exec-loop-command.js +9 -2
  118. package/neural-interface/lib/file-explorer-api.js +74 -40
  119. package/neural-interface/lib/file-explorer-watch.js +9 -3
  120. package/neural-interface/lib/git-bash/install.js +52 -6
  121. package/neural-interface/lib/git-bash/manager.js +59 -17
  122. package/neural-interface/lib/git-bash/pins.js +12 -0
  123. package/neural-interface/lib/git-read.js +2 -2
  124. package/neural-interface/lib/git-status-runner.js +390 -0
  125. package/neural-interface/lib/git-status-watchdog.js +40 -0
  126. package/neural-interface/lib/greeting/budgets.js +1 -0
  127. package/neural-interface/lib/greeting/engine.js +2 -2
  128. package/neural-interface/lib/greeting/sources.js +10 -12
  129. package/neural-interface/lib/greeting/writer.js +1 -1
  130. package/neural-interface/lib/mcp-client-config.js +37 -6
  131. package/neural-interface/lib/mcp-external-servers.js +40 -7
  132. package/neural-interface/lib/mcp-installer.js +2 -1
  133. package/neural-interface/lib/memory-session-capture.js +17 -3
  134. package/neural-interface/lib/morelogin.js +147 -25
  135. package/neural-interface/lib/native-loop-providers.js +423 -23
  136. package/neural-interface/lib/native-loop-runtime.js +23 -2
  137. package/neural-interface/lib/opencode-native-boundary.js +165 -22
  138. package/neural-interface/lib/owner-authority.js +1 -1
  139. package/neural-interface/lib/path-confine.js +12 -9
  140. package/neural-interface/lib/peer-identity.js +80 -6
  141. package/neural-interface/lib/pty-host/core.js +28 -1
  142. package/neural-interface/lib/pty-host/lifetime.js +12 -0
  143. package/neural-interface/lib/pty-host/manager.js +12 -1
  144. package/neural-interface/lib/remote-policy.js +1 -1
  145. package/neural-interface/lib/request-gate.js +3 -0
  146. package/neural-interface/lib/rulesets/api.js +6 -5
  147. package/neural-interface/lib/rulesets/fs-safe.js +1 -1
  148. package/neural-interface/lib/rulesets/installer.js +89 -10
  149. package/neural-interface/lib/rulesets/render.js +1 -1
  150. package/neural-interface/lib/session-cache-builder.js +64 -1
  151. package/neural-interface/lib/skills/catalog.js +5 -1
  152. package/neural-interface/lib/skills/creation-brief.js +1 -1
  153. package/neural-interface/lib/skills/creation-sources.js +19 -1
  154. package/neural-interface/lib/skills/deploy.js +9 -4
  155. package/neural-interface/lib/skills/draft-confinement.js +3 -1
  156. package/neural-interface/lib/skills/eval-confinement.js +3 -1
  157. package/neural-interface/lib/skills/eval-trace.js +16 -0
  158. package/neural-interface/lib/skills/export.js +5 -2
  159. package/neural-interface/lib/skills/known-bridge-digests.json +58 -4
  160. package/neural-interface/lib/skills/ownership.js +1 -1
  161. package/neural-interface/lib/skills/roots.js +10 -3
  162. package/neural-interface/lib/skills/runtime-checks.js +6 -3
  163. package/neural-interface/lib/skills/service.js +15 -7
  164. package/neural-interface/lib/skills/telemetry.js +6 -0
  165. package/neural-interface/lib/synabun-version.js +1 -1
  166. package/neural-interface/lib/system-activity.js +1 -1
  167. package/neural-interface/lib/whatsapp/brain.js +7 -3
  168. package/neural-interface/lib/whatsapp/cards.js +22 -9
  169. package/neural-interface/lib/whatsapp/config.js +1 -1
  170. package/neural-interface/lib/worker-confinement.js +77 -1
  171. package/neural-interface/lib/worker-question.js +74 -0
  172. package/neural-interface/lib/worker-resume.js +13 -4
  173. package/neural-interface/lib/worker-sandbox.js +46 -16
  174. package/neural-interface/package-lock.json +18 -2
  175. package/neural-interface/package.json +2 -1
  176. package/neural-interface/public/onboarding.html +1 -1
  177. package/neural-interface/public/shared/agent-runtime-options.js +89 -11
  178. package/neural-interface/public/shared/agy/agy-bypass.js +47 -0
  179. package/neural-interface/public/shared/agy/agy-client.js +266 -0
  180. package/neural-interface/public/shared/agy/agy-commands.js +120 -0
  181. package/neural-interface/public/shared/agy/agy-copy.js +326 -0
  182. package/neural-interface/public/shared/agy/agy-diff.js +161 -0
  183. package/neural-interface/public/shared/agy/agy-events.js +469 -0
  184. package/neural-interface/public/shared/agy/agy-greeting.js +26 -0
  185. package/neural-interface/public/shared/agy/agy-options.js +232 -0
  186. package/neural-interface/public/shared/agy/agy-panel.js +2543 -0
  187. package/neural-interface/public/shared/agy/agy-render.js +392 -0
  188. package/neural-interface/public/shared/agy/agy-setup.js +149 -0
  189. package/neural-interface/public/shared/agy/agy-styles.js +604 -0
  190. package/neural-interface/public/shared/agy/agy-tabs-logic.js +181 -0
  191. package/neural-interface/public/shared/api.js +4 -4
  192. package/neural-interface/public/shared/assistant/asst-agents-dock.js +3 -4
  193. package/neural-interface/public/shared/assistant/asst-api.js +15 -3
  194. package/neural-interface/public/shared/assistant/asst-brain-picker.js +56 -9
  195. package/neural-interface/public/shared/assistant/asst-budget.js +5 -4
  196. package/neural-interface/public/shared/assistant/asst-computer.js +26 -2
  197. package/neural-interface/public/shared/assistant/asst-control.js +48 -0
  198. package/neural-interface/public/shared/assistant/asst-hosts.js +16 -7
  199. package/neural-interface/public/shared/assistant/asst-limit-dock.js +6 -1
  200. package/neural-interface/public/shared/assistant/asst-limit-wait.js +38 -5
  201. package/neural-interface/public/shared/assistant/asst-motion-gate.js +80 -0
  202. package/neural-interface/public/shared/assistant/asst-panel.js +46 -9
  203. package/neural-interface/public/shared/assistant/asst-render.js +112 -18
  204. package/neural-interface/public/shared/assistant/asst-route.js +237 -57
  205. package/neural-interface/public/shared/assistant/asst-routes-editor.js +9 -8
  206. package/neural-interface/public/shared/assistant/asst-scene-styles.js +55 -23
  207. package/neural-interface/public/shared/assistant/asst-scenes.js +279 -80
  208. package/neural-interface/public/shared/assistant/asst-sidepanel-styles.js +4 -4
  209. package/neural-interface/public/shared/assistant/asst-sidepanel.js +51 -7
  210. package/neural-interface/public/shared/assistant/asst-slash.js +8 -2
  211. package/neural-interface/public/shared/assistant/asst-state.js +76 -9
  212. package/neural-interface/public/shared/assistant/asst-style-cache.js +44 -0
  213. package/neural-interface/public/shared/assistant/asst-styles.js +58 -4
  214. package/neural-interface/public/shared/assistant/asst-tool-kinds.js +34 -7
  215. package/neural-interface/public/shared/assistant/asst-usage.js +2 -1
  216. package/neural-interface/public/shared/cdx/cdx-output.js +26 -3
  217. package/neural-interface/public/shared/cdx/cdx-panel.js +14 -2
  218. package/neural-interface/public/shared/cdx/cdx-preview.js +12 -0
  219. package/neural-interface/public/shared/cdx/cdx-protocol.js +11 -10
  220. package/neural-interface/public/shared/cdx/cdx-render.js +10 -9
  221. package/neural-interface/public/shared/cdx/cdx-requests.js +11 -1
  222. package/neural-interface/public/shared/cdx/cdx-styles.js +4 -0
  223. package/neural-interface/public/shared/cdx/cdx-tabs.js +75 -5
  224. package/neural-interface/public/shared/cli-install-controller.js +218 -0
  225. package/neural-interface/public/shared/cli-install-view.js +7 -4
  226. package/neural-interface/public/shared/cli-status.js +9 -8
  227. package/neural-interface/public/shared/constants.js +3 -2
  228. package/neural-interface/public/shared/context-menu-model.js +1 -0
  229. package/neural-interface/public/shared/cp/cp-permissions.js +12 -0
  230. package/neural-interface/public/shared/cp/cp-restore.js +162 -0
  231. package/neural-interface/public/shared/cp/cp-tool-cards.js +10 -3
  232. package/neural-interface/public/shared/desktop-bridge.js +17 -0
  233. package/neural-interface/public/shared/desktop-titlebar-view.js +12 -0
  234. package/neural-interface/public/shared/desktop-titlebar.js +13 -4
  235. package/neural-interface/public/shared/desktop-update-view.js +1 -0
  236. package/neural-interface/public/shared/explorer/fx-api.js +4 -4
  237. package/neural-interface/public/shared/explorer/fx-links.js +3 -3
  238. package/neural-interface/public/shared/explorer/fx-logic.js +1 -1
  239. package/neural-interface/public/shared/explorer/fx-main.js +48 -16
  240. package/neural-interface/public/shared/explorer/fx-tree.js +1 -0
  241. package/neural-interface/public/shared/greeting/greeting-card.js +2 -2
  242. package/neural-interface/public/shared/greeting/greeting-view.js +1 -0
  243. package/neural-interface/public/shared/html-shell.js +21 -4
  244. package/neural-interface/public/shared/loading-teardown.js +34 -0
  245. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +6 -1
  246. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +28 -7
  247. package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +13 -4
  248. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +17 -1
  249. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +6 -0
  250. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +101 -68
  251. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +46 -3
  252. package/neural-interface/public/shared/provider-icons.js +4 -2
  253. package/neural-interface/public/shared/settings/settings-rules.js +2 -2
  254. package/neural-interface/public/shared/settings/settings-skill-folders-pane.js +1 -1
  255. package/neural-interface/public/shared/settings/settings-skill-folders.js +2 -2
  256. package/neural-interface/public/shared/showcase-privacy.js +6 -1
  257. package/neural-interface/public/shared/sidepanel-session-status.js +3 -3
  258. package/neural-interface/public/shared/skills/ss-creation.js +6 -3
  259. package/neural-interface/public/shared/skills/ss-dialog.js +5 -2
  260. package/neural-interface/public/shared/skills/ss-lint.js +9 -1
  261. package/neural-interface/public/shared/skills/ss-profiles.js +44 -6
  262. package/neural-interface/public/shared/skills/ss-review-view.js +2 -2
  263. package/neural-interface/public/shared/skills/studio/sx-actions.js +1 -1
  264. package/neural-interface/public/shared/skills/studio/sx-create-ai-view.js +1 -1
  265. package/neural-interface/public/shared/skills/studio/sx-create-ai.js +2 -1
  266. package/neural-interface/public/shared/skills/studio/sx-dom.js +2 -1
  267. package/neural-interface/public/shared/skills/studio/sx-fixes-view.js +359 -0
  268. package/neural-interface/public/shared/skills/studio/sx-fixes.js +327 -0
  269. package/neural-interface/public/shared/skills/studio/sx-inspector.js +1 -1
  270. package/neural-interface/public/shared/skills/studio/sx-install-flow.js +9 -6
  271. package/neural-interface/public/shared/skills/studio/sx-install.js +20 -3
  272. package/neural-interface/public/shared/skills/studio/sx-library-view.js +144 -35
  273. package/neural-interface/public/shared/skills/studio/sx-library.js +6 -2
  274. package/neural-interface/public/shared/skills/studio/sx-main.js +11 -2
  275. package/neural-interface/public/shared/skills/studio/sx-review-view.js +1 -1
  276. package/neural-interface/public/shared/skills/studio/sx-subject.js +7 -6
  277. package/neural-interface/public/shared/skills/studio/sx-workspace-view.js +6 -5
  278. package/neural-interface/public/shared/state.js +1 -1
  279. package/neural-interface/public/shared/storage-retry.js +47 -0
  280. package/neural-interface/public/shared/storage.js +25 -4
  281. package/neural-interface/public/shared/streaming-markdown.js +58 -0
  282. package/neural-interface/public/shared/styles.css +362 -35
  283. package/neural-interface/public/shared/synabun-mascot.js +27 -8
  284. package/neural-interface/public/shared/synabun-ticker.js +147 -12
  285. package/neural-interface/public/shared/text-cache.js +29 -0
  286. package/neural-interface/public/shared/tool-list-picker.js +2 -2
  287. package/neural-interface/public/shared/tool-list-view.js +2 -2
  288. package/neural-interface/public/shared/topright-overflow-logic.js +54 -0
  289. package/neural-interface/public/shared/ui-antigravity-panel.js +19 -0
  290. package/neural-interface/public/shared/ui-assistant.js +2 -0
  291. package/neural-interface/public/shared/ui-automation-studio.js +20 -12
  292. package/neural-interface/public/shared/ui-claude-panel.js +374 -134
  293. package/neural-interface/public/shared/ui-cli-banner.js +23 -167
  294. package/neural-interface/public/shared/ui-file-explorer.js +48 -6
  295. package/neural-interface/public/shared/ui-file-links.js +2 -2
  296. package/neural-interface/public/shared/ui-games.js +2 -2
  297. package/neural-interface/public/shared/ui-judgments.js +1 -1
  298. package/neural-interface/public/shared/ui-keybinds.js +8 -2
  299. package/neural-interface/public/shared/ui-link.js +3 -3
  300. package/neural-interface/public/shared/ui-loading.js +5 -4
  301. package/neural-interface/public/shared/ui-menubar.js +2 -2
  302. package/neural-interface/public/shared/ui-native-loop-router.js +4 -0
  303. package/neural-interface/public/shared/ui-navbar.js +45 -0
  304. package/neural-interface/public/shared/ui-notifications.js +1 -0
  305. package/neural-interface/public/shared/ui-resume.js +9 -7
  306. package/neural-interface/public/shared/ui-schedules-studio.js +41 -10
  307. package/neural-interface/public/shared/ui-scroll-follow.js +16 -5
  308. package/neural-interface/public/shared/ui-sessions.js +22 -12
  309. package/neural-interface/public/shared/ui-settings.js +90 -65
  310. package/neural-interface/public/shared/ui-sidepanel-session-entry.js +9 -2
  311. package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -3
  312. package/neural-interface/public/shared/ui-sidepanel-tray.js +48 -7
  313. package/neural-interface/public/shared/ui-sidepanel-windows.js +3 -3
  314. package/neural-interface/public/shared/ui-terminal.js +146 -10
  315. package/neural-interface/public/shared/ui-tool-updates.js +3 -3
  316. package/neural-interface/public/shared/ui-tooltip.js +71 -46
  317. package/neural-interface/public/shared/ui-topright-overflow.js +233 -0
  318. package/neural-interface/public/shared/ui-whiteboard.js +1 -1
  319. package/neural-interface/public/shared/ui-worker-end.js +1 -1
  320. package/neural-interface/public/shared/whatsapp/wa-view.js +2 -2
  321. package/neural-interface/public/shared/worker-questions.js +34 -0
  322. package/neural-interface/public/sidepanel-session.html +1 -1
  323. package/neural-interface/server.js +731 -466
  324. package/neural-interface/templates/rulesets/README.md +1 -1
  325. package/neural-interface/templates/rulesets/legacy-hashes.json +5 -5
  326. package/neural-interface/templates/rulesets/manifest.json +1 -1
  327. package/npm-shrinkwrap.json +18 -2
  328. package/package.json +2 -1
  329. package/skills/skill-forge/SKILL.md +4 -4
  330. package/skills/synabun/SKILL.md +1 -1
  331. package/skills/synabun/modules/schedule.md +3 -3
  332. /package/neural-interface/templates/rulesets/{gemini.md → antigravity.md} +0 -0
@@ -0,0 +1,423 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun — Assistant brain: Google Antigravity (`agy acp`, in the brain's own Antigravity home)
3
+ // ═══════════════════════════════════════════
4
+ //
5
+ // One `agy acp` process and one ACP session per Assistant session (lib/antigravity/acp-connection.js), started in
6
+ // the brain's own Antigravity home (lib/antigravity/home.js: `--gemini-dir`): SynaBun's MCP server (its child gets
7
+ // the assistant role, the session pin, the desktop grant and the brain's credential from the agent's environment)
8
+ // beside the user's own servers, the user's global rules and skills, and the policy hook that asks this process
9
+ // about every tool call before it runs (lib/antigravity/policy.js → deps.policyCheck: the browser policy, plan mode,
10
+ // a remote session's read-only rules and the route gate; a refusal reaches the model with its reason and the turn
11
+ // goes on). The persona rides as the session's appended system prompt (_meta.antigravity.systemPrompt.append, which
12
+ // agy advertises in initialize). ACP session/update notifications become the unified envelope
13
+ // (assistant-envelope.js createAcpEnvelopeTranslator); a permission request the approval mode does not answer
14
+ // becomes a card (control_request), answered once (never "always": Antigravity would save it for later sessions).
15
+ // The model and the thinking level are the session's config options, switched per turn; images ride as ACP image
16
+ // blocks; Stop is session/cancel; a resumed conversation is session/resume (no replay) in the same home.
17
+
18
+ import { randomUUID } from 'node:crypto';
19
+ import { fileURLToPath } from 'node:url';
20
+ import { acpErrorInfo, createAcpConnection, isAuthRequired } from '../antigravity/acp-connection.js';
21
+ import { assertSafeAntigravityProject, antigravityHomeReadPolicy, antigravityHomeSettings, homeMcpTools, policyHookCommand, prepareAntigravityHome, toAntigravityMcpEntry, userAntigravityMcpServers, userAntigravitySettings, userGeminiDir } from '../antigravity/home.js';
22
+ import { acpCall, acpReadOnlyKind, acpToolPaths, acpUiToolName } from '../antigravity/tool-call.js';
23
+ import { splitAntigravityModel } from '../antigravity/catalog.js';
24
+ import { acpTurnUsage } from '../antigravity/usage.js';
25
+ import { createAcpEnvelopeTranslator } from '../assistant-envelope.js';
26
+ import { planModeInstructions } from '../assistant-persona.js';
27
+
28
+ /** The hook script of every SynaBun Antigravity home. */
29
+ export const ANTIGRAVITY_BRAIN_POLICY_HOOK = fileURLToPath(new URL('../antigravity/policy-hook.mjs', import.meta.url));
30
+ const CANCEL_GRACE_MS = 8_000;
31
+ const MCP_WATCH_MS = 120_000;
32
+ const LEVELS = new Set(['minimal', 'low', 'medium', 'high', 'xhigh', 'max']);
33
+
34
+ /** The Assistant's Antigravity approval mode: 'auto' (every call the policy allows runs) or 'default' (cards). */
35
+ export function antigravityApprovalMode(mode) {
36
+ return mode === 'auto' || mode === 'acceptEdits' || mode === 'bypassPermissions' ? 'auto' : 'default';
37
+ }
38
+
39
+ /** Composer images ({ base64 | data, mediaType }) → ACP image blocks. */
40
+ export function acpImageBlocks(images = []) {
41
+ const blocks = [];
42
+ for (const image of Array.isArray(images) ? images : []) {
43
+ let data = image?.base64 || image?.data || null;
44
+ let mimeType = image?.mediaType || image?.media_type || image?.mime || image?.mimeType || 'image/png';
45
+ if (!data || typeof data !== 'string') continue;
46
+ const inline = /^data:([^;,]+);base64,(.*)$/s.exec(data);
47
+ if (inline) { mimeType = inline[1]; data = inline[2]; }
48
+ blocks.push({ type: 'image', mimeType, data });
49
+ }
50
+ return blocks;
51
+ }
52
+
53
+ /** The ACP permission option for an answer: explicit kinds only, never allow-always. */
54
+ function permissionOption(options, wanted) {
55
+ const list = Array.isArray(options) ? options.filter((option) => option && typeof option.optionId === 'string') : [];
56
+ for (const kind of wanted) {
57
+ if (kind === 'allow_always') continue;
58
+ const found = list.find((option) => option.kind === kind);
59
+ if (found) return found.optionId;
60
+ }
61
+ return null;
62
+ }
63
+
64
+ /**
65
+ * The MCP servers of a brain's home: SynaBun's own entry, then every enabled server of the user's own Antigravity
66
+ * configuration (a brain keeps the user's servers, as a Claude brain keeps the user's settings).
67
+ */
68
+ export function antigravityBrainServers({ synabunMcpEntry = null, userGemini = userGeminiDir() } = {}) {
69
+ const servers = {};
70
+ const user = userAntigravityMcpServers(userGemini);
71
+ const own = synabunMcpEntry || user.SynaBun || null;
72
+ if (own) servers.SynaBun = toAntigravityMcpEntry(own);
73
+ for (const [name, entry] of Object.entries(user)) {
74
+ if (name === 'SynaBun' || entry?.disabled === true) continue;
75
+ servers[name] = toAntigravityMcpEntry(entry);
76
+ }
77
+ return servers;
78
+ }
79
+
80
+ export function createAntigravityBrain({ session, sink, deps = {}, persona = '' } = {}) {
81
+ if (typeof deps.antigravityLaunch !== 'function') throw new Error('Antigravity brain requires deps.antigravityLaunch');
82
+ const brain = session.brain || {};
83
+ const cwd = brain.cwd || deps.PACKAGE_ROOT || process.cwd();
84
+ const connect = typeof deps.connect === 'function' ? deps.connect : createAcpConnection;
85
+ const homeDir = typeof deps.antigravityHome === 'function' ? deps.antigravityHome(session.id) : null;
86
+ const userGemini = deps.userGemini || userGeminiDir();
87
+ const remoteReadOnly = deps.remoteReadOnly === true;
88
+ let approvalMode = antigravityApprovalMode(brain.permissionMode);
89
+ let planMode = brain.planMode === true || brain.permissionMode === 'plan';
90
+ let conn = null;
91
+ let sessionId = session.providerSessionId || null;
92
+ let resumeWanted = !!sessionId;
93
+ let registration = null;
94
+ let configOptions = [];
95
+ let turn = null; // { planned, cancelled, cancelTimer, model }
96
+ let lastSessionUsage = deps.sessionUsageBase && typeof deps.sessionUsageBase === 'object' ? deps.sessionUsageBase : null;
97
+ let startPromise = null;
98
+ let disposed = false;
99
+ let mcpTimer = null;
100
+ let mcpReported = null;
101
+ let replaying = false;
102
+ const pendingControls = new Map(); // requestId → { resolve, options }
103
+
104
+ const emit = (packet) => { if (!disposed) sink.send(packet); };
105
+ const note = (text) => emit({ type: 'stderr', text });
106
+ const optionOf = (category) => configOptions.find((o) => o?.category === category || o?.id === category) || null;
107
+ const valuesOf = (option) => {
108
+ const out = [];
109
+ for (const entry of option?.options || []) for (const v of Array.isArray(entry?.options) ? entry.options : [entry]) if (v && v.value !== undefined) out.push(String(v.value));
110
+ return out;
111
+ };
112
+ const currentValue = (category) => { const option = optionOf(category); return option?.currentValue !== undefined && option?.currentValue !== null ? String(option.currentValue) : null; };
113
+
114
+ const translator = createAcpEnvelopeTranslator({
115
+ emit,
116
+ brain: { cwd, sessionId: () => sessionId, model: () => currentValue('model') || brain.model || null },
117
+ planTurn: () => turn?.planned === true,
118
+ });
119
+ emit({ type: 'engine', engine: 'antigravity', brain: 'antigravity' });
120
+
121
+ function applyModes(mode, plan) {
122
+ if (mode === 'plan') planMode = true;
123
+ else if (mode) approvalMode = antigravityApprovalMode(mode);
124
+ if (typeof plan === 'boolean') planMode = plan;
125
+ }
126
+
127
+ /**
128
+ * The policy hook's question for this brain: first the runtime's checks (deps.policyCheck: browser policy, plan mode, remote read-only, route gate); then the approval
129
+ * mode: auto lets every call run, default lets reads and SynaBun's own tools run and asks about the rest.
130
+ */
131
+ function policyCheck(call) {
132
+ let verdict = null;
133
+ try { verdict = typeof deps.policyCheck === 'function' ? deps.policyCheck(call) : null; } catch (error) { return { decision: 'deny', reason: `SynaBun could not check this call (${error?.message || error}).` }; }
134
+ if (verdict?.decision === 'deny') return verdict;
135
+ const homeRead = antigravityHomeReadPolicy(call, { homeDir, cwd });
136
+ if (homeRead) return homeRead;
137
+ if (remoteReadOnly || approvalMode === 'auto') return { decision: 'allow' };
138
+ if (acpReadOnlyKind(call.kind)) return { decision: 'allow' };
139
+ if (call.mcp?.server === 'SynaBun') return { decision: 'allow' };
140
+ return { decision: 'ask' };
141
+ }
142
+
143
+ function cancelControls(reason = 'turn_ended') {
144
+ for (const [requestId, pending] of pendingControls) {
145
+ pendingControls.delete(requestId);
146
+ emit({ type: 'control_cancelled', request_id: requestId, reason });
147
+ try { pending.resolve({ outcome: { outcome: 'cancelled' } }); } catch { /* answered */ }
148
+ }
149
+ }
150
+
151
+ async function onPermission(params, { signal } = {}) {
152
+ if (disposed) return { outcome: { outcome: 'cancelled' } };
153
+ const call = acpCall(params);
154
+ const options = params?.options || [];
155
+ const pick = (kinds) => {
156
+ const optionId = permissionOption(options, kinds);
157
+ return optionId ? { outcome: { outcome: 'selected', optionId } } : { outcome: { outcome: 'cancelled' } };
158
+ };
159
+ const verdict = policyCheck(call);
160
+ // The hook asked first; a refusal reaching here (a call the hook never saw) ends the turn, as a user's would.
161
+ if (verdict.decision === 'deny') return pick(['reject_once', 'reject_always']);
162
+ if (verdict.decision === 'allow') return pick(['allow_once']);
163
+ const requestId = `agy-${randomUUID()}`;
164
+ const packet = {
165
+ type: 'control_request',
166
+ request_id: requestId,
167
+ request: {
168
+ subtype: 'can_use_tool', provider: 'antigravity', kind: 'permission',
169
+ tool_name: acpUiToolName(call),
170
+ input: call.mcp ? { ...call.mcp.args } : { ...call.args, ...(call.command ? { command: call.command } : {}) },
171
+ title: call.title || null,
172
+ brain_native: { kind: 'permission', toolCallId: call.id, sessionId: params?.sessionId || sessionId, toolCall: params?.toolCall || null },
173
+ },
174
+ };
175
+ return new Promise((resolveAnswer) => {
176
+ pendingControls.set(requestId, { resolve: resolveAnswer, options });
177
+ signal?.addEventListener?.('abort', () => {
178
+ if (!pendingControls.delete(requestId)) return;
179
+ emit({ type: 'control_cancelled', request_id: requestId, reason: 'withdrawn' });
180
+ resolveAnswer({ outcome: { outcome: 'cancelled' } });
181
+ }, { once: true });
182
+ emit(packet);
183
+ });
184
+ }
185
+
186
+ function reportMcp() {
187
+ if (!homeDir) return null;
188
+ const tools = homeMcpTools(homeDir, 'SynaBun');
189
+ const status = tools.length ? 'connected' : null;
190
+ if (status && mcpReported !== status) {
191
+ mcpReported = status;
192
+ emit({ type: 'event', event: { type: 'system', subtype: 'mcp_status', servers: [{ name: 'SynaBun', status, tools: tools.length }] } });
193
+ }
194
+ return status;
195
+ }
196
+ function watchMcp() {
197
+ if (!homeDir || mcpTimer) return;
198
+ const until = Date.now() + MCP_WATCH_MS;
199
+ mcpTimer = setInterval(() => {
200
+ if (disposed || reportMcp() || Date.now() > until) { clearInterval(mcpTimer); mcpTimer = null; }
201
+ }, 1000);
202
+ mcpTimer.unref?.();
203
+ }
204
+
205
+ async function openSession() {
206
+ const meta = persona ? { antigravity: { systemPrompt: { append: persona } } } : undefined;
207
+ const init = await conn.initialize();
208
+ let answer = null;
209
+ if (sessionId && resumeWanted) {
210
+ const caps = init?.agentCapabilities?.sessionCapabilities || {};
211
+ // agy 1.4.12 sends the conversation's history again while it resumes (session/resume as session/load): the
212
+ // panel has it already, so nothing that arrives before the answer is shown.
213
+ replaying = true;
214
+ try {
215
+ answer = await conn.request(caps.resume ? 'session/resume' : 'session/load', { sessionId, cwd, mcpServers: [], ...(meta ? { _meta: meta } : {}) });
216
+ sessionId = String(answer?.sessionId || sessionId);
217
+ } catch (error) {
218
+ if (isAuthRequired(error)) throw error;
219
+ note(`Antigravity could not resume its conversation (${acpErrorInfo(error).message}); this brain starts a new one.`);
220
+ answer = null;
221
+ sessionId = null;
222
+ } finally {
223
+ replaying = false;
224
+ }
225
+ }
226
+ if (!answer) {
227
+ answer = await conn.request('session/new', { cwd, mcpServers: [], ...(meta ? { _meta: meta } : {}) });
228
+ sessionId = String(answer?.sessionId || '');
229
+ if (!sessionId) throw new Error('Antigravity did not return a session id');
230
+ lastSessionUsage = null;
231
+ }
232
+ resumeWanted = false;
233
+ configOptions = Array.isArray(answer?.configOptions) ? answer.configOptions : [];
234
+ try { deps.onAntigravitySession?.(sessionId); } catch { /* bookkeeping */ }
235
+ reportMcp();
236
+ watchMcp();
237
+ }
238
+
239
+ async function boot() {
240
+ if (disposed) throw new Error('Antigravity brain disposed');
241
+ const launch = deps.antigravityLaunch();
242
+ if (!launch?.command) throw new Error('The Antigravity CLI (agy) is not installed. Install it from the Antigravity side panel, sign in there, then try again — or switch the Assistant to another provider.');
243
+ assertSafeAntigravityProject(cwd);
244
+ let hookCommand = null;
245
+ if (homeDir) {
246
+ if (deps.policy?.register && deps.policyUrl) hookCommand = policyHookCommand({ script: deps.policyHookScript || ANTIGRAVITY_BRAIN_POLICY_HOOK });
247
+ prepareAntigravityHome({
248
+ dir: homeDir,
249
+ mcpServers: antigravityBrainServers({ synabunMcpEntry: typeof deps.synabunMcpEntry === 'function' ? deps.synabunMcpEntry() : deps.synabunMcpEntry, userGemini }),
250
+ hookCommand,
251
+ settings: antigravityHomeSettings({ userSettings: userAntigravitySettings(userGemini) }),
252
+ userGemini,
253
+ });
254
+ }
255
+ if (hookCommand) registration = deps.policy.register((call) => (disposed ? { decision: 'deny', reason: 'This Assistant session ended.' } : policyCheck(call)), { label: `brain ${session.id}` });
256
+ const base = typeof deps.childEnv === 'function' ? deps.childEnv() : { ...process.env };
257
+ for (const key of ['SYNABUN_TOOLS', 'SYNABUN_TOOLS_PATH', 'SYNABUN_PROFILE', 'SYNABUN_RUNTIME_PROFILE_PATH', 'SYNABUN_TOOL_CATALOG_MODE', 'SYNABUN_AGY_POLICY_URL', 'SYNABUN_AGY_POLICY_TOKEN', 'SYNABUN_BROWSER_SESSION', 'SYNABUN_BROWSER_TAB']) delete base[key];
258
+ const env = {
259
+ ...base,
260
+ // The brain's SynaBun MCP child (stdio) reads who it serves from here (mcp-server/src/services/identity.ts).
261
+ SYNABUN_ROLE: 'assistant',
262
+ SYNABUN_TERMINAL_SESSION: String(session.id),
263
+ ...(deps.desktopGrant ? { SYNABUN_DESKTOP_GRANT: String(deps.desktopGrant) } : {}),
264
+ ...(deps.brainCredential ? { SYNABUN_BRAIN_TOKEN: String(deps.brainCredential) } : {}),
265
+ ...(registration ? { SYNABUN_AGY_POLICY_URL: deps.policyUrl, SYNABUN_AGY_POLICY_TOKEN: registration.token } : {}),
266
+ };
267
+ conn = connect({
268
+ command: launch.command,
269
+ args: launch.args || [],
270
+ acpArgs: homeDir ? ['--gemini-dir', homeDir] : [],
271
+ cwd,
272
+ env,
273
+ clientInfo: { name: 'synabun', title: 'SynaBun Assistant', version: '0' },
274
+ onUpdate: (params) => {
275
+ if (disposed || replaying || !params?.update || !sessionId || params.sessionId !== sessionId) return;
276
+ // Between turns only titles and the like count: a stray message of a turn that already ended is not shown.
277
+ if (!turn && params.update.sessionUpdate !== 'session_info_update') return;
278
+ translator.handle(params.update);
279
+ },
280
+ onPermission,
281
+ onExit: (info) => {
282
+ if (disposed) return;
283
+ cancelControls('agent_exited');
284
+ const why = info?.error || (info?.signal ? `signal ${info.signal}` : `code ${info?.code}`);
285
+ if (turn) finishTurn(null, new Error(`Antigravity exited (${why})${info?.stderr ? `: ${String(info.stderr).trim().split('\n').slice(-2).join(' ')}` : ''}`));
286
+ else note(`Antigravity exited (${why}); the next message starts it again.`);
287
+ conn = null;
288
+ startPromise = null;
289
+ resumeWanted = !!sessionId;
290
+ },
291
+ });
292
+ try {
293
+ await openSession();
294
+ } catch (error) {
295
+ const auth = isAuthRequired(error);
296
+ const message = auth ? 'Antigravity is not signed in: open the Antigravity panel (or run `agy` in a terminal) and sign in, then try again.' : `Antigravity could not start: ${acpErrorInfo(error).message}`;
297
+ registration?.dispose();
298
+ registration = null;
299
+ try { await conn?.close(); } catch { /* gone */ }
300
+ conn = null;
301
+ startPromise = null;
302
+ throw Object.assign(new Error(message), { code: auth ? 'AUTH_REQUIRED' : 'START_FAILED' });
303
+ }
304
+ return true;
305
+ }
306
+ function start() {
307
+ if (!startPromise) startPromise = boot();
308
+ return startPromise;
309
+ }
310
+
311
+ async function setOption(category, value, label) {
312
+ const option = optionOf(category);
313
+ if (!value || !option) return;
314
+ if (String(option.currentValue) === String(value)) return;
315
+ const accepted = valuesOf(option);
316
+ if (accepted.length && !accepted.includes(String(value))) { note(`Antigravity offers no ${label} "${value}" here; it keeps ${option.currentValue}.`); return; }
317
+ try {
318
+ const answer = await conn.request('session/set_config_option', { sessionId, configId: option.id, value: String(value) });
319
+ if (Array.isArray(answer?.configOptions)) configOptions = answer.configOptions;
320
+ } catch (error) { note(`Antigravity kept its ${label} (${acpErrorInfo(error).message}).`); }
321
+ }
322
+ /** The turn's model and thinking level (the record may have changed since the session started; a routed turn names its own). */
323
+ async function applyModel(model, effort) {
324
+ const wanted = splitAntigravityModel(model, valuesOf(optionOf('model')));
325
+ if (wanted.model) await setOption('model', wanted.model, 'model');
326
+ const level = effort && LEVELS.has(String(effort).toLowerCase()) ? String(effort).toLowerCase() : wanted.effort;
327
+ if (level) await setOption('thought_level', level, 'thinking level');
328
+ }
329
+
330
+ function finishTurn(answer, error = null) {
331
+ const current = turn;
332
+ if (!current) return;
333
+ turn = null;
334
+ if (current.cancelTimer) clearTimeout(current.cancelTimer);
335
+ cancelControls('turn_ended');
336
+ const usage = answer?.usage ? acpTurnUsage(answer.usage, lastSessionUsage) : null;
337
+ if (answer?.usage) {
338
+ lastSessionUsage = answer.usage;
339
+ try { deps.onAntigravitySessionUsage?.(sessionId, answer.usage); } catch { /* bookkeeping */ }
340
+ }
341
+ if (usage) { try { deps.onAntigravityUsage?.({ usage, model: currentValue('model') || brain.model || null, sessionId }); } catch { /* metering never breaks a turn */ } }
342
+ const stopReason = current.cancelled ? 'cancelled' : (answer?.stopReason || 'end_turn');
343
+ translator.endTurn({ stopReason, usage, error: error && !current.cancelled ? (error.message || String(error)) : null, planned: current.planned === true });
344
+ }
345
+
346
+ async function runPrompt(blocks, { planned = false } = {}) {
347
+ translator.reset();
348
+ turn = { planned, cancelled: false, cancelTimer: null };
349
+ const mine = turn;
350
+ conn.request('session/prompt', { sessionId, prompt: blocks }, { timeoutMs: 0 })
351
+ .then((answer) => { if (turn === mine) finishTurn(answer); })
352
+ .catch((error) => { if (turn === mine) finishTurn(null, new Error(acpErrorInfo(error).message)); });
353
+ }
354
+
355
+ return {
356
+ kind: 'antigravity',
357
+ start,
358
+ async sendUserTurn({ text, images = [], model = null, effort = null, permissionMode: mode = null, planMode: plan = null } = {}) {
359
+ await start();
360
+ if (disposed || !conn) return false;
361
+ if (turn) throw new Error('Antigravity is still answering the last message');
362
+ applyModes(mode, plan);
363
+ if (resumeWanted && sessionId) await openSession();
364
+ await applyModel(model || brain.model || null, effort || brain.effort || null);
365
+ // A plan turn carries what plan mode means here (a remote session's read-only brain does not).
366
+ const planned = planMode && !remoteReadOnly;
367
+ const body = planned ? `<system-reminder>\n${planModeInstructions({ provider: 'antigravity' })}\n</system-reminder>\n\n${String(text ?? '')}` : String(text ?? '');
368
+ await runPrompt([{ type: 'text', text: body }, ...acpImageBlocks(images)], { planned });
369
+ return true;
370
+ },
371
+ async abort() {
372
+ if (!turn) { cancelControls('stopped'); return; }
373
+ const current = turn;
374
+ current.cancelled = true;
375
+ cancelControls('stopped');
376
+ try { if (conn && sessionId) await conn.notify('session/cancel', { sessionId }); } catch { /* the answer or the grace timer ends it */ }
377
+ // An agent that never answers the cancelled prompt: the turn ends here anyway.
378
+ current.cancelTimer = setTimeout(() => { if (turn === current) finishTurn(null); }, CANCEL_GRACE_MS);
379
+ current.cancelTimer.unref?.();
380
+ },
381
+ async setPermissionMode(mode, { planMode: plan = null } = {}) { applyModes(mode, plan); emit({ type: 'mode_changed', mode: approvalMode, planMode }); },
382
+ respondControl(requestId, response = {}) {
383
+ const pending = pendingControls.get(String(requestId));
384
+ if (!pending) { emit({ type: 'error', message: `No pending Antigravity request ${requestId}` }); return; }
385
+ pendingControls.delete(String(requestId));
386
+ // Once, whatever the answer says about "always": Antigravity would save it for every later session.
387
+ const allow = response?.behavior === 'allow';
388
+ const optionId = permissionOption(pending.options, allow ? ['allow_once'] : ['reject_once', 'reject_always']);
389
+ pending.resolve(optionId ? { outcome: { outcome: 'selected', optionId } } : { outcome: { outcome: 'cancelled' } });
390
+ },
391
+ // Antigravity's own /compact command, as a turn of its own (busy until it ends).
392
+ async compact() {
393
+ await start();
394
+ if (disposed || !conn || turn) return;
395
+ await runPrompt([{ type: 'text', text: '/compact' }]);
396
+ },
397
+ async mcpStatus() { if (!reportMcp()) emit({ type: 'event', event: { type: 'system', subtype: 'mcp_status', servers: [] } }); },
398
+ // The conversation to pick up: resumed before the next message (session/resume in the brain's home).
399
+ async resume(id) { if (!id) return; sessionId = String(id); resumeWanted = true; lastSessionUsage = null; },
400
+ async listSessions() { return []; },
401
+ isBusy() { return !!turn || pendingControls.size > 0; },
402
+ // The route gate: its hook checks every call before it runs; without one (no policy wired) the runtime checks
403
+ // the tool starts it sees (the reactive fallback).
404
+ get gateMode() { return registration ? 'hook' : 'reactive'; },
405
+ identity() {
406
+ return { providerSessionId: sessionId, providerThreadId: null, accountId: null, model: currentValue('model') || brain.model || null, effort: currentValue('thought_level') || brain.effort || null, cwd, busy: !!turn, home: homeDir };
407
+ },
408
+ async dispose() {
409
+ if (disposed) return;
410
+ const wasRunning = !!turn;
411
+ cancelControls('brain_disposed');
412
+ if (wasRunning && conn && sessionId) { try { await conn.notify('session/cancel', { sessionId }); } catch { /* closing anyway */ } }
413
+ disposed = true;
414
+ if (mcpTimer) { clearInterval(mcpTimer); mcpTimer = null; }
415
+ if (turn?.cancelTimer) clearTimeout(turn.cancelTimer);
416
+ turn = null;
417
+ registration?.dispose();
418
+ registration = null;
419
+ try { await conn?.close(); } catch { /* gone */ }
420
+ conn = null;
421
+ },
422
+ };
423
+ }
@@ -181,8 +181,13 @@ export function createCodexBrain({ session, sink, deps = {}, persona = '' } = {}
181
181
  queryPlanMode = planMode;
182
182
  // "<id>[extended]" = the model at its extended context window (the sidepanel's contextMode).
183
183
  const picked = model || brain.model || null;
184
+ // The Assistant composer uses base64 + mediaType; the Codex skin persists
185
+ // data URLs as native localImage inputs. Translate at this protocol boundary.
186
+ const pictures = (Array.isArray(images) ? images : [])
187
+ .filter((image) => image?.base64)
188
+ .map((image) => ({ dataUrl: `data:${image.mediaType || 'image/png'};base64,${image.base64}` }));
184
189
  sendQuery({
185
- prompt: text, images: Array.isArray(images) && images.length ? images : undefined,
190
+ prompt: text, images: pictures.length ? pictures : undefined,
186
191
  cwd: cwd || brain.cwd || deps.PACKAGE_ROOT || undefined, model: picked ? baseModelId(picked) : null, effort: effort || brain.effort || null,
187
192
  contextMode: contextVariantOf(picked) === 'extended' ? 'extended' : undefined,
188
193
  threadId: threadId || null, autoAccept: approvalMode === 'auto' && !planMode && !remoteReadOnly, planMode,
@@ -27,7 +27,7 @@ import { dirname } from 'node:path';
27
27
  import { BUDGET_DEFAULT_USD, BUDGET_WARN_RATIO, roundUsd } from './assistant-budget.js';
28
28
  import {
29
29
  BUDGET_PROVIDERS, MIN_SPEND_USD, billingOf, bucketEligibility, budgetView, coverageOf, coversWork, divideLeft,
30
- dryProviders, fallbackWeights, jevWeights, listPriceOf, priceLabel, providerSpendOf, rowOf, totalSpentOf,
30
+ dryProviders, fallbackWeights, isUnpricedProvider, jevWeights, listPriceOf, priceLabel, providerSpendOf, rowOf, totalSpentOf,
31
31
  } from './assistant-budget-split.js';
32
32
 
33
33
  export const SPLIT_REASONS = Object.freeze(['first_route', 'share_used_up', 'brain_turn', 'budget_changed', 'run_ended']);
@@ -156,13 +156,16 @@ export function createBudgetPlanner({
156
156
  function jevInput(id, { value, used, cov, eligible, brainInfo, cat, needs, task, reason, tasks }) {
157
157
  const rows = safe(() => accounts(), null);
158
158
  const prefs = safe(() => routing()?.preferences, {}) || {};
159
- const savedRoutes = Object.entries(prefs).filter(([, pref]) => pref?.provider).map(([taskClass, pref]) => {
159
+ const savedRoutes = Object.entries(prefs).filter(([, pref]) => pref?.provider && !isUnpricedProvider(pref.provider)).map(([taskClass, pref]) => {
160
160
  const info = modelInfo(cat, pref.provider, pref.model || null, accountFor(pref.provider, rows, brainInfo));
161
161
  return { taskClass, provider: pref.provider, model: pref.model || null, effort: pref.effort || null, price: info.price, billing: info.billing };
162
162
  });
163
163
  const recent = safe(() => recentSpend(id), null) || {};
164
164
  const providers = {};
165
165
  for (const provider of BUDGET_PROVIDERS) {
166
+ // A provider no budget can hold (it reports no prices) is never asked about and never described: Jev's state
167
+ // stays the one its questions were tuned on (budget-judgments.ts).
168
+ if (isUnpricedProvider(provider)) continue;
166
169
  const own = providerSpendOf(used, provider);
167
170
  const account = accountFor(provider, rows, brainInfo);
168
171
  const routed = savedRoutes.filter((r) => r.provider === provider);
@@ -308,7 +311,8 @@ export function createBudgetPlanner({
308
311
  if (!id || !limited()) return true;
309
312
  let cov = coverage(id);
310
313
  if (coversWork(cov, { provider, forBrain, free })) return true;
311
- if (cov.leftUsd < MIN_SPEND_USD || free) return false;
314
+ // No split gives an unpriced provider money: never a re-split (or a Jev request) for one.
315
+ if (cov.leftUsd < MIN_SPEND_USD || free || isUnpricedProvider(provider)) return false;
312
316
  const key = `${id}:${provider}:${forBrain ? 'brain' : 'runs'}`;
313
317
  const tried = needTries.get(key);
314
318
  const value = readSetting();
@@ -3,7 +3,10 @@
3
3
  // ═══════════════════════════════════════════
4
4
  //
5
5
  // A session with a budget (assistant-budget.js) divides what is left across
6
- // three buckets, one per provider: claude-code, codex, opencode. A provider's
6
+ // the buckets of the providers it can meter: claude-code, codex, opencode. A
7
+ // provider that reports no price (antigravity: tokens only) has a bucket that
8
+ // never takes a share: under a budget its runs are refused, under No limit
9
+ // they run and only their spend (none) is shown. A provider's
7
10
  // runs draw from its share; the Assistant's own turns (brain turns, direct and
8
11
  // continuation routes) draw from the share of the brain's provider, which
9
12
  // keeps a reserve for them that the workers on that provider cannot spend.
@@ -22,8 +25,16 @@
22
25
  import { listedCount, priceText, providerAllHidden, providerEmpty, rowIsHidden, findModel, defaultModelRow } from './assistant-catalog.js';
23
26
  import { BUDGET_WARN_RATIO, floorCents, roundUsd } from './assistant-budget.js';
24
27
 
25
- export const BUDGET_PROVIDERS = Object.freeze(['claude-code', 'codex', 'opencode']);
26
- export const PROVIDER_LABELS = Object.freeze({ 'claude-code': 'Claude Code', codex: 'Codex', opencode: 'OpenCode' });
28
+ export const BUDGET_PROVIDERS = Object.freeze(['claude-code', 'codex', 'opencode', 'antigravity']);
29
+ export const PROVIDER_LABELS = Object.freeze({ 'claude-code': 'Claude Code', codex: 'Codex', opencode: 'OpenCode', antigravity: 'Antigravity' });
30
+ /**
31
+ * Providers whose spend no budget can hold: they report tokens, never prices (Antigravity is billed by its Google
32
+ * sign-in). Under a budget they take no share, never cover work and their runs are refused (BUDGET_UNPRICED).
33
+ */
34
+ export const UNPRICED_PROVIDERS = Object.freeze(['antigravity']);
35
+ export const isUnpricedProvider = (provider) => UNPRICED_PROVIDERS.includes(provider);
36
+ /** Why a budget cannot hold an unpriced provider's runs, in one phrase. */
37
+ export const UNPRICED_PROVIDER_REASON = 'Antigravity reports tokens, not prices';
27
38
 
28
39
  // The brain's reserve inside its provider's share: 10 % of what is left, at least $0.50 (never more than is left).
29
40
  export const BRAIN_FLOOR_RATIO = 0.1;
@@ -48,6 +59,8 @@ const arr = (value) => (Array.isArray(value) ? value : []);
48
59
  const num = (value, fallback = 0) => (Number.isFinite(Number(value)) ? Number(value) : fallback);
49
60
  const r8 = (value) => Number((Number(value) || 0).toFixed(8));
50
61
  const isProvider = (value) => BUDGET_PROVIDERS.includes(value);
62
+ /** Can the provider take a share? Not when it is ineligible, nor ever when it reports no prices (whatever `eligible` says). */
63
+ const canTake = (eligible, provider) => !isUnpricedProvider(provider) && eligible?.[provider]?.eligible !== false;
51
64
 
52
65
  // ── buckets ───────────────────────────────────────────────────────────────
53
66
 
@@ -56,14 +69,15 @@ const isProvider = (value) => BUDGET_PROVIDERS.includes(value);
56
69
  * no_models the user disabled every model, or the provider answered with none connected
57
70
  * signed_out every account of the provider is signed out
58
71
  * limited every signed-in account is out of subscription usage
72
+ * unpriced the provider reports no prices (UNPRICED_PROVIDERS): a budget cannot hold its spend
59
73
  * A provider whose model list is merely unavailable stays eligible (routing is lenient there too). The
60
74
  * brain's provider is always eligible (its turns are paid from it); `reason` then says what else is wrong.
61
75
  */
62
76
  export function bucketEligibility({ catalog = null, accounts = null, brainProvider = null } = {}) {
63
77
  const out = {};
64
78
  for (const provider of BUDGET_PROVIDERS) {
65
- let reason = null;
66
- if (catalog) {
79
+ let reason = isUnpricedProvider(provider) ? 'unpriced' : null;
80
+ if (!reason && catalog) {
67
81
  if (providerEmpty(catalog, provider) || providerAllHidden(catalog, provider)) reason = 'no_models';
68
82
  else if (listedCount(catalog, provider) > 0 || arr(catalog.models?.[provider]).length) {
69
83
  const usable = arr(catalog.models?.[provider]).filter((row) => !rowIsHidden(catalog, { ...row, provider: row.provider || provider }) && row.status !== 'unavailable');
@@ -115,11 +129,13 @@ export function priceLabel(row, price) {
115
129
  }
116
130
  /**
117
131
  * How the work is paid for: 'free' (a free model), 'api' (an API key, or OpenCode's own providers), 'plan'
118
- * (a Claude / ChatGPT subscription). `account`: the account row the work would run under, when known.
132
+ * (a Claude / ChatGPT subscription, or Antigravity's Google sign-in, which reports no price at all).
133
+ * `account`: the account row the work would run under, when known.
119
134
  */
120
135
  export function billingOf(provider, price, account = null) {
121
136
  if (isFreePrice(price)) return 'free';
122
137
  if (provider === 'opencode') return 'api';
138
+ if (provider === 'antigravity') return 'plan';
123
139
  if (account?.mode === 'apiKey') return 'api';
124
140
  return 'plan';
125
141
  }
@@ -149,7 +165,7 @@ export function fallbackWeights({
149
165
  const weights = Object.fromEntries(BUDGET_PROVIDERS.map((p) => [p, 0]));
150
166
  const brainProvider = isProvider(brain?.provider) ? brain.provider : null;
151
167
  const price = (provider, model) => priceIndex(listPriceOf(rowOf(catalog, provider, model), { pricing, provider, model }));
152
- const add = (provider, model, weight) => { if (isProvider(provider) && eligible[provider]?.eligible !== false) weights[provider] += weight * price(provider, model); };
168
+ const add = (provider, model, weight) => { if (isProvider(provider) && canTake(eligible, provider)) weights[provider] += weight * price(provider, model); };
153
169
  const prefs = routing?.preferences && typeof routing.preferences === 'object' ? routing.preferences : {};
154
170
  for (const taskClass of classes) {
155
171
  // Chat always runs here, on the brain, whatever its saved route says (assistant-router.js evaluateRoute).
@@ -161,8 +177,8 @@ export function fallbackWeights({
161
177
  if (task?.provider) add(task.provider, task.model || null, 3 * classWeight(task.taskClass));
162
178
  if (brainProvider) add(brainProvider, brain?.model || null, 2);
163
179
  for (const run of arr(liveRuns)) add(run?.provider, run?.model || null, classWeight(run?.taskClass));
164
- for (const provider of arr(needs)) if (isProvider(provider) && eligible[provider]?.eligible !== false && weights[provider] <= 0) weights[provider] = Math.max(weights[provider], price(provider, null) > 0 ? 1 : 0);
165
- const pool = BUDGET_PROVIDERS.filter((p) => eligible[p]?.eligible !== false);
180
+ for (const provider of arr(needs)) if (isProvider(provider) && canTake(eligible, provider) && weights[provider] <= 0) weights[provider] = Math.max(weights[provider], price(provider, null) > 0 ? 1 : 0);
181
+ const pool = BUDGET_PROVIDERS.filter((p) => canTake(eligible, p));
166
182
  if (pool.length && pool.every((p) => !(weights[p] > 0))) for (const p of pool) weights[p] = 1;
167
183
  return weights;
168
184
  }
@@ -201,7 +217,7 @@ export function jevWeights(judgment, eligible = {}) {
201
217
  if (!scores || typeof scores !== 'object') return null;
202
218
  const out = Object.fromEntries(BUDGET_PROVIDERS.map((p) => [p, 0]));
203
219
  for (const provider of BUDGET_PROVIDERS) {
204
- if (eligible[provider]?.eligible === false) continue;
220
+ if (!canTake(eligible, provider)) continue;
205
221
  const share = shareFromScore(scores[provider]);
206
222
  if (share === null) return null;
207
223
  out[provider] = Math.max(0, share);
@@ -239,7 +255,7 @@ export function apportion(total, parts) {
239
255
  */
240
256
  export function divideLeft({ leftUsd = 0, weights = {}, eligible = {}, brainProvider = null } = {}) {
241
257
  const leftCents = floorCents(leftUsd);
242
- const pool = BUDGET_PROVIDERS.filter((p) => eligible[p]?.eligible !== false);
258
+ const pool = BUDGET_PROVIDERS.filter((p) => canTake(eligible, p));
243
259
  const zero = Object.fromEntries(BUDGET_PROVIDERS.map((p) => [p, 0]));
244
260
  if (!leftCents || !pool.length) return { allocCents: zero, leftCents, brainFloorCents: 0, floorsCents: { ...zero } };
245
261
  const parts = Object.fromEntries(pool.map((p) => [p, Math.max(0, num(weights[p]))]));
@@ -298,6 +314,11 @@ export function coverageOf({ amountUsd = 0, split = null, spend = null, brainPro
298
314
  const providers = {};
299
315
  for (const provider of BUDGET_PROVIDERS) {
300
316
  const used = providerSpendOf(spend, provider);
317
+ // An unpriced provider can spend nothing a budget could hold, split or not.
318
+ if (isUnpricedProvider(provider)) {
319
+ providers[provider] = { ...used, shareUsd: split ? 0 : null, leftUsd: 0, workerLeftUsd: 0, eligible: false };
320
+ continue;
321
+ }
301
322
  const eligible = split ? split.eligible?.[provider]?.eligible !== false : true;
302
323
  if (!split) {
303
324
  providers[provider] = { ...used, shareUsd: null, leftUsd, workerLeftUsd: leftUsd, eligible };
@@ -327,16 +348,22 @@ export function coverageOf({ amountUsd = 0, split = null, spend = null, brainPro
327
348
  export function coversWork(coverage, { provider, forBrain = false, free = false, minUsd = MIN_SPEND_USD } = {}) {
328
349
  if (!coverage) return true;
329
350
  if (coverage.leftUsd < minUsd) return false;
351
+ // A budget cannot hold what is never priced.
352
+ if (isUnpricedProvider(provider)) return false;
330
353
  if (free) return true;
331
354
  const row = coverage.providers?.[provider];
332
355
  if (!row) return false;
333
356
  return (forBrain ? row.leftUsd : row.workerLeftUsd) >= minUsd;
334
357
  }
335
358
 
336
- /** The providers whose share is used up for workers (or ineligible), in BUDGET_PROVIDERS order. */
359
+ /**
360
+ * The providers whose share is used up for workers (or ineligible), in BUDGET_PROVIDERS order. An unpriced
361
+ * provider is never among them: it has no share to run out of (its runs are refused under a budget for that).
362
+ */
337
363
  export function dryProviders(coverage, { minUsd = MIN_SPEND_USD } = {}) {
338
- if (!coverage?.hasSplit) return coverage && coverage.leftUsd < minUsd ? [...BUDGET_PROVIDERS] : [];
339
- return BUDGET_PROVIDERS.filter((p) => !coverage.providers[p] || coverage.providers[p].workerLeftUsd < minUsd);
364
+ const metered = BUDGET_PROVIDERS.filter((p) => !isUnpricedProvider(p));
365
+ if (!coverage?.hasSplit) return coverage && coverage.leftUsd < minUsd ? metered : [];
366
+ return metered.filter((p) => !coverage.providers[p] || coverage.providers[p].workerLeftUsd < minUsd);
340
367
  }
341
368
 
342
369
  // ── the view (API, live packet, agent_usage) ──────────────────────────────
@@ -380,7 +407,7 @@ export function budgetView({ sessionId = null, setting = null, coverage = null,
380
407
  const judged = split?.jev?.scores?.[provider] || null;
381
408
  return {
382
409
  provider, label: PROVIDER_LABELS[provider],
383
- eligible: row.eligible, reason: split?.eligible?.[provider]?.reason || null,
410
+ eligible: row.eligible, reason: split?.eligible?.[provider]?.reason || (isUnpricedProvider(provider) ? 'unpriced' : null),
384
411
  isBrain: provider === (cov.brainProvider || split?.brainProvider || null),
385
412
  shareUsd: row.shareUsd === null ? null : roundUsd(row.shareUsd),
386
413
  spentUsd: roundUsd(row.spentUsd), brainSpentUsd: roundUsd(row.brainUsd), runsSpentUsd: roundUsd(row.runsUsd),