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.
- package/CHANGELOG.md +139 -0
- package/README.md +1 -1
- package/docs/whatsapp.md +11 -11
- package/lib/client-config-repair.js +13 -9
- package/lib/external-tools.js +6 -5
- package/lib/update-snapshot.js +77 -3
- package/lib/virustotal.mjs +12 -1
- package/mcp-server/dist/index.js +1 -1
- package/mcp-server/dist/index.js.map +1 -1
- package/mcp-server/dist/services/budget-judgments.d.ts +1 -1
- package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -1
- package/mcp-server/dist/services/budget-judgments.js +5 -3
- package/mcp-server/dist/services/budget-judgments.js.map +1 -1
- package/mcp-server/dist/services/identity.d.ts +2 -2
- package/mcp-server/dist/services/identity.js +2 -2
- package/mcp-server/dist/services/memory-sessions.d.ts +1 -1
- package/mcp-server/dist/services/memory-sessions.d.ts.map +1 -1
- package/mcp-server/dist/services/memory-sessions.js +28 -2
- package/mcp-server/dist/services/memory-sessions.js.map +1 -1
- package/mcp-server/dist/services/neural-interface.d.ts +2 -1
- package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
- package/mcp-server/dist/services/neural-interface.js.map +1 -1
- package/mcp-server/dist/services/server-instructions.d.ts.map +1 -1
- package/mcp-server/dist/services/server-instructions.js +2 -1
- package/mcp-server/dist/services/server-instructions.js.map +1 -1
- package/mcp-server/dist/services/tool-catalog.js +1 -1
- package/mcp-server/dist/services/tool-catalog.js.map +1 -1
- package/mcp-server/dist/services/tool-list.d.ts +1 -1
- package/mcp-server/dist/services/tool-list.d.ts.map +1 -1
- package/mcp-server/dist/services/tool-runtime.d.ts +2 -2
- package/mcp-server/dist/services/tool-runtime.d.ts.map +1 -1
- package/mcp-server/dist/services/tool-runtime.js +1 -1
- package/mcp-server/dist/services/tool-runtime.js.map +1 -1
- package/mcp-server/dist/services/typesafe-config.js +1 -1
- package/mcp-server/dist/services/typesafe-config.js.map +1 -1
- package/mcp-server/dist/tools/agents.d.ts +8 -8
- package/mcp-server/dist/tools/agents.d.ts.map +1 -1
- package/mcp-server/dist/tools/agents.js +10 -9
- package/mcp-server/dist/tools/agents.js.map +1 -1
- package/mcp-server/dist/tools/choice.d.ts +6 -0
- package/mcp-server/dist/tools/choice.d.ts.map +1 -1
- package/mcp-server/dist/tools/choice.js +18 -13
- package/mcp-server/dist/tools/choice.js.map +1 -1
- package/mcp-server/dist/tools/git-tools.js +2 -2
- package/mcp-server/dist/tools/git-tools.js.map +1 -1
- package/mcp-server/dist/tools/morelogin.d.ts.map +1 -1
- package/mcp-server/dist/tools/morelogin.js +9 -6
- package/mcp-server/dist/tools/morelogin.js.map +1 -1
- package/mcp-server/dist/tools/skill-tools.d.ts +8 -8
- package/mcp-server/dist/tools/skill-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/skill-tools.js +11 -8
- package/mcp-server/dist/tools/skill-tools.js.map +1 -1
- package/mcp-server/package-lock.json +2 -2
- package/mcp-server/package.json +1 -1
- package/mcp-server/src/index.ts +1 -1
- package/mcp-server/src/services/budget-judgments.ts +5 -3
- package/mcp-server/src/services/identity.ts +2 -2
- package/mcp-server/src/services/memory-sessions.ts +24 -3
- package/mcp-server/src/services/neural-interface.ts +2 -1
- package/mcp-server/src/services/server-instructions.ts +2 -1
- package/mcp-server/src/services/tool-catalog.ts +1 -1
- package/mcp-server/src/services/tool-list.ts +1 -1
- package/mcp-server/src/services/tool-runtime.ts +2 -2
- package/mcp-server/src/services/typesafe-config.ts +1 -1
- package/mcp-server/src/tools/agents.ts +10 -9
- package/mcp-server/src/tools/choice.ts +17 -12
- package/mcp-server/src/tools/git-tools.ts +2 -2
- package/mcp-server/src/tools/morelogin.ts +6 -4
- package/mcp-server/src/tools/skill-tools.ts +11 -8
- package/neural-interface/i18n/en.json +504 -39
- package/neural-interface/i18n/pt-BR.json +504 -39
- package/neural-interface/lib/agent-sandbox-elevation.js +6 -0
- package/neural-interface/lib/antigravity/acp-client-host.js +554 -0
- package/neural-interface/lib/antigravity/acp-connection.js +299 -0
- package/neural-interface/lib/antigravity/bypass.js +100 -0
- package/neural-interface/lib/antigravity/catalog.js +273 -0
- package/neural-interface/lib/antigravity/home.js +260 -0
- package/neural-interface/lib/antigravity/migrate-gemini.js +139 -0
- package/neural-interface/lib/antigravity/panel-bridge.js +1058 -0
- package/neural-interface/lib/antigravity/policy-hook.mjs +78 -0
- package/neural-interface/lib/antigravity/policy.js +74 -0
- package/neural-interface/lib/antigravity/tool-call.js +178 -0
- package/neural-interface/lib/antigravity/transcripts.js +252 -0
- package/neural-interface/lib/antigravity/usage.js +43 -0
- package/neural-interface/lib/assistant-api.js +1 -0
- package/neural-interface/lib/assistant-brains/antigravity.js +423 -0
- package/neural-interface/lib/assistant-brains/codex.js +6 -1
- package/neural-interface/lib/assistant-budget-planner.js +7 -3
- package/neural-interface/lib/assistant-budget-split.js +42 -15
- package/neural-interface/lib/assistant-catalog.js +76 -9
- package/neural-interface/lib/assistant-config.js +6 -3
- package/neural-interface/lib/assistant-dispatch.js +211 -50
- package/neural-interface/lib/assistant-envelope.js +153 -1
- package/neural-interface/lib/assistant-evidence.js +80 -0
- package/neural-interface/lib/assistant-hidden-models.js +7 -5
- package/neural-interface/lib/assistant-persona.js +16 -11
- package/neural-interface/lib/assistant-plan-permissions.js +15 -0
- package/neural-interface/lib/assistant-router.js +237 -35
- package/neural-interface/lib/assistant-runtime.js +158 -12
- package/neural-interface/lib/assistant-task-prompt.js +10 -8
- package/neural-interface/lib/assistant-tools.js +2 -2
- package/neural-interface/lib/assistant-usage.js +29 -0
- package/neural-interface/lib/backup-service.js +6 -1
- package/neural-interface/lib/browser-guard-cdp.js +5 -4
- package/neural-interface/lib/browser-network-guard.js +274 -82
- package/neural-interface/lib/browser-tool-policy.js +12 -7
- package/neural-interface/lib/claude-agent-bridge.js +7 -0
- package/neural-interface/lib/claude-history-cache.js +210 -0
- package/neural-interface/lib/claude-model-catalog.js +5 -1
- package/neural-interface/lib/cli-install-api.js +1 -1
- package/neural-interface/lib/cli-install.js +160 -31
- package/neural-interface/lib/cli-resolver.js +9 -2
- package/neural-interface/lib/cli-update-planner.js +25 -15
- package/neural-interface/lib/codex-rollout-reader.js +52 -0
- package/neural-interface/lib/codex-worker-app-server.js +25 -6
- package/neural-interface/lib/editor-git-api.js +3 -20
- package/neural-interface/lib/exec-loop-command.js +9 -2
- package/neural-interface/lib/file-explorer-api.js +74 -40
- package/neural-interface/lib/file-explorer-watch.js +9 -3
- package/neural-interface/lib/git-bash/install.js +52 -6
- package/neural-interface/lib/git-bash/manager.js +59 -17
- package/neural-interface/lib/git-bash/pins.js +12 -0
- package/neural-interface/lib/git-read.js +2 -2
- package/neural-interface/lib/git-status-runner.js +390 -0
- package/neural-interface/lib/git-status-watchdog.js +40 -0
- package/neural-interface/lib/greeting/budgets.js +1 -0
- package/neural-interface/lib/greeting/engine.js +2 -2
- package/neural-interface/lib/greeting/sources.js +10 -12
- package/neural-interface/lib/greeting/writer.js +1 -1
- package/neural-interface/lib/mcp-client-config.js +37 -6
- package/neural-interface/lib/mcp-external-servers.js +40 -7
- package/neural-interface/lib/mcp-installer.js +2 -1
- package/neural-interface/lib/memory-session-capture.js +17 -3
- package/neural-interface/lib/morelogin.js +147 -25
- package/neural-interface/lib/native-loop-providers.js +423 -23
- package/neural-interface/lib/native-loop-runtime.js +23 -2
- package/neural-interface/lib/opencode-native-boundary.js +165 -22
- package/neural-interface/lib/owner-authority.js +1 -1
- package/neural-interface/lib/path-confine.js +12 -9
- package/neural-interface/lib/peer-identity.js +80 -6
- package/neural-interface/lib/pty-host/core.js +28 -1
- package/neural-interface/lib/pty-host/lifetime.js +12 -0
- package/neural-interface/lib/pty-host/manager.js +12 -1
- package/neural-interface/lib/remote-policy.js +1 -1
- package/neural-interface/lib/request-gate.js +3 -0
- package/neural-interface/lib/rulesets/api.js +6 -5
- package/neural-interface/lib/rulesets/fs-safe.js +1 -1
- package/neural-interface/lib/rulesets/installer.js +89 -10
- package/neural-interface/lib/rulesets/render.js +1 -1
- package/neural-interface/lib/session-cache-builder.js +64 -1
- package/neural-interface/lib/skills/catalog.js +5 -1
- package/neural-interface/lib/skills/creation-brief.js +1 -1
- package/neural-interface/lib/skills/creation-sources.js +19 -1
- package/neural-interface/lib/skills/deploy.js +9 -4
- package/neural-interface/lib/skills/draft-confinement.js +3 -1
- package/neural-interface/lib/skills/eval-confinement.js +3 -1
- package/neural-interface/lib/skills/eval-trace.js +16 -0
- package/neural-interface/lib/skills/export.js +5 -2
- package/neural-interface/lib/skills/known-bridge-digests.json +58 -4
- package/neural-interface/lib/skills/ownership.js +1 -1
- package/neural-interface/lib/skills/roots.js +10 -3
- package/neural-interface/lib/skills/runtime-checks.js +6 -3
- package/neural-interface/lib/skills/service.js +15 -7
- package/neural-interface/lib/skills/telemetry.js +6 -0
- package/neural-interface/lib/synabun-version.js +1 -1
- package/neural-interface/lib/system-activity.js +1 -1
- package/neural-interface/lib/whatsapp/brain.js +7 -3
- package/neural-interface/lib/whatsapp/cards.js +22 -9
- package/neural-interface/lib/whatsapp/config.js +1 -1
- package/neural-interface/lib/worker-confinement.js +77 -1
- package/neural-interface/lib/worker-question.js +74 -0
- package/neural-interface/lib/worker-resume.js +13 -4
- package/neural-interface/lib/worker-sandbox.js +46 -16
- package/neural-interface/package-lock.json +18 -2
- package/neural-interface/package.json +2 -1
- package/neural-interface/public/onboarding.html +1 -1
- package/neural-interface/public/shared/agent-runtime-options.js +89 -11
- package/neural-interface/public/shared/agy/agy-bypass.js +47 -0
- package/neural-interface/public/shared/agy/agy-client.js +266 -0
- package/neural-interface/public/shared/agy/agy-commands.js +120 -0
- package/neural-interface/public/shared/agy/agy-copy.js +326 -0
- package/neural-interface/public/shared/agy/agy-diff.js +161 -0
- package/neural-interface/public/shared/agy/agy-events.js +469 -0
- package/neural-interface/public/shared/agy/agy-greeting.js +26 -0
- package/neural-interface/public/shared/agy/agy-options.js +232 -0
- package/neural-interface/public/shared/agy/agy-panel.js +2543 -0
- package/neural-interface/public/shared/agy/agy-render.js +392 -0
- package/neural-interface/public/shared/agy/agy-setup.js +149 -0
- package/neural-interface/public/shared/agy/agy-styles.js +604 -0
- package/neural-interface/public/shared/agy/agy-tabs-logic.js +181 -0
- package/neural-interface/public/shared/api.js +4 -4
- package/neural-interface/public/shared/assistant/asst-agents-dock.js +3 -4
- package/neural-interface/public/shared/assistant/asst-api.js +15 -3
- package/neural-interface/public/shared/assistant/asst-brain-picker.js +56 -9
- package/neural-interface/public/shared/assistant/asst-budget.js +5 -4
- package/neural-interface/public/shared/assistant/asst-computer.js +26 -2
- package/neural-interface/public/shared/assistant/asst-control.js +48 -0
- package/neural-interface/public/shared/assistant/asst-hosts.js +16 -7
- package/neural-interface/public/shared/assistant/asst-limit-dock.js +6 -1
- package/neural-interface/public/shared/assistant/asst-limit-wait.js +38 -5
- package/neural-interface/public/shared/assistant/asst-motion-gate.js +80 -0
- package/neural-interface/public/shared/assistant/asst-panel.js +46 -9
- package/neural-interface/public/shared/assistant/asst-render.js +112 -18
- package/neural-interface/public/shared/assistant/asst-route.js +237 -57
- package/neural-interface/public/shared/assistant/asst-routes-editor.js +9 -8
- package/neural-interface/public/shared/assistant/asst-scene-styles.js +55 -23
- package/neural-interface/public/shared/assistant/asst-scenes.js +279 -80
- package/neural-interface/public/shared/assistant/asst-sidepanel-styles.js +4 -4
- package/neural-interface/public/shared/assistant/asst-sidepanel.js +51 -7
- package/neural-interface/public/shared/assistant/asst-slash.js +8 -2
- package/neural-interface/public/shared/assistant/asst-state.js +76 -9
- package/neural-interface/public/shared/assistant/asst-style-cache.js +44 -0
- package/neural-interface/public/shared/assistant/asst-styles.js +58 -4
- package/neural-interface/public/shared/assistant/asst-tool-kinds.js +34 -7
- package/neural-interface/public/shared/assistant/asst-usage.js +2 -1
- package/neural-interface/public/shared/cdx/cdx-output.js +26 -3
- package/neural-interface/public/shared/cdx/cdx-panel.js +14 -2
- package/neural-interface/public/shared/cdx/cdx-preview.js +12 -0
- package/neural-interface/public/shared/cdx/cdx-protocol.js +11 -10
- package/neural-interface/public/shared/cdx/cdx-render.js +10 -9
- package/neural-interface/public/shared/cdx/cdx-requests.js +11 -1
- package/neural-interface/public/shared/cdx/cdx-styles.js +4 -0
- package/neural-interface/public/shared/cdx/cdx-tabs.js +75 -5
- package/neural-interface/public/shared/cli-install-controller.js +218 -0
- package/neural-interface/public/shared/cli-install-view.js +7 -4
- package/neural-interface/public/shared/cli-status.js +9 -8
- package/neural-interface/public/shared/constants.js +3 -2
- package/neural-interface/public/shared/context-menu-model.js +1 -0
- package/neural-interface/public/shared/cp/cp-permissions.js +12 -0
- package/neural-interface/public/shared/cp/cp-restore.js +162 -0
- package/neural-interface/public/shared/cp/cp-tool-cards.js +10 -3
- package/neural-interface/public/shared/desktop-bridge.js +17 -0
- package/neural-interface/public/shared/desktop-titlebar-view.js +12 -0
- package/neural-interface/public/shared/desktop-titlebar.js +13 -4
- package/neural-interface/public/shared/desktop-update-view.js +1 -0
- package/neural-interface/public/shared/explorer/fx-api.js +4 -4
- package/neural-interface/public/shared/explorer/fx-links.js +3 -3
- package/neural-interface/public/shared/explorer/fx-logic.js +1 -1
- package/neural-interface/public/shared/explorer/fx-main.js +48 -16
- package/neural-interface/public/shared/explorer/fx-tree.js +1 -0
- package/neural-interface/public/shared/greeting/greeting-card.js +2 -2
- package/neural-interface/public/shared/greeting/greeting-view.js +1 -0
- package/neural-interface/public/shared/html-shell.js +21 -4
- package/neural-interface/public/shared/loading-teardown.js +34 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +6 -1
- package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +28 -7
- package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +13 -4
- package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +17 -1
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +6 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +101 -68
- package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +46 -3
- package/neural-interface/public/shared/provider-icons.js +4 -2
- package/neural-interface/public/shared/settings/settings-rules.js +2 -2
- package/neural-interface/public/shared/settings/settings-skill-folders-pane.js +1 -1
- package/neural-interface/public/shared/settings/settings-skill-folders.js +2 -2
- package/neural-interface/public/shared/showcase-privacy.js +6 -1
- package/neural-interface/public/shared/sidepanel-session-status.js +3 -3
- package/neural-interface/public/shared/skills/ss-creation.js +6 -3
- package/neural-interface/public/shared/skills/ss-dialog.js +5 -2
- package/neural-interface/public/shared/skills/ss-lint.js +9 -1
- package/neural-interface/public/shared/skills/ss-profiles.js +44 -6
- package/neural-interface/public/shared/skills/ss-review-view.js +2 -2
- package/neural-interface/public/shared/skills/studio/sx-actions.js +1 -1
- package/neural-interface/public/shared/skills/studio/sx-create-ai-view.js +1 -1
- package/neural-interface/public/shared/skills/studio/sx-create-ai.js +2 -1
- package/neural-interface/public/shared/skills/studio/sx-dom.js +2 -1
- package/neural-interface/public/shared/skills/studio/sx-fixes-view.js +359 -0
- package/neural-interface/public/shared/skills/studio/sx-fixes.js +327 -0
- package/neural-interface/public/shared/skills/studio/sx-inspector.js +1 -1
- package/neural-interface/public/shared/skills/studio/sx-install-flow.js +9 -6
- package/neural-interface/public/shared/skills/studio/sx-install.js +20 -3
- package/neural-interface/public/shared/skills/studio/sx-library-view.js +144 -35
- package/neural-interface/public/shared/skills/studio/sx-library.js +6 -2
- package/neural-interface/public/shared/skills/studio/sx-main.js +11 -2
- package/neural-interface/public/shared/skills/studio/sx-review-view.js +1 -1
- package/neural-interface/public/shared/skills/studio/sx-subject.js +7 -6
- package/neural-interface/public/shared/skills/studio/sx-workspace-view.js +6 -5
- package/neural-interface/public/shared/state.js +1 -1
- package/neural-interface/public/shared/storage-retry.js +47 -0
- package/neural-interface/public/shared/storage.js +25 -4
- package/neural-interface/public/shared/streaming-markdown.js +58 -0
- package/neural-interface/public/shared/styles.css +362 -35
- package/neural-interface/public/shared/synabun-mascot.js +27 -8
- package/neural-interface/public/shared/synabun-ticker.js +147 -12
- package/neural-interface/public/shared/text-cache.js +29 -0
- package/neural-interface/public/shared/tool-list-picker.js +2 -2
- package/neural-interface/public/shared/tool-list-view.js +2 -2
- package/neural-interface/public/shared/topright-overflow-logic.js +54 -0
- package/neural-interface/public/shared/ui-antigravity-panel.js +19 -0
- package/neural-interface/public/shared/ui-assistant.js +2 -0
- package/neural-interface/public/shared/ui-automation-studio.js +20 -12
- package/neural-interface/public/shared/ui-claude-panel.js +374 -134
- package/neural-interface/public/shared/ui-cli-banner.js +23 -167
- package/neural-interface/public/shared/ui-file-explorer.js +48 -6
- package/neural-interface/public/shared/ui-file-links.js +2 -2
- package/neural-interface/public/shared/ui-games.js +2 -2
- package/neural-interface/public/shared/ui-judgments.js +1 -1
- package/neural-interface/public/shared/ui-keybinds.js +8 -2
- package/neural-interface/public/shared/ui-link.js +3 -3
- package/neural-interface/public/shared/ui-loading.js +5 -4
- package/neural-interface/public/shared/ui-menubar.js +2 -2
- package/neural-interface/public/shared/ui-native-loop-router.js +4 -0
- package/neural-interface/public/shared/ui-navbar.js +45 -0
- package/neural-interface/public/shared/ui-notifications.js +1 -0
- package/neural-interface/public/shared/ui-resume.js +9 -7
- package/neural-interface/public/shared/ui-schedules-studio.js +41 -10
- package/neural-interface/public/shared/ui-scroll-follow.js +16 -5
- package/neural-interface/public/shared/ui-sessions.js +22 -12
- package/neural-interface/public/shared/ui-settings.js +90 -65
- package/neural-interface/public/shared/ui-sidepanel-session-entry.js +9 -2
- package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -3
- package/neural-interface/public/shared/ui-sidepanel-tray.js +48 -7
- package/neural-interface/public/shared/ui-sidepanel-windows.js +3 -3
- package/neural-interface/public/shared/ui-terminal.js +146 -10
- package/neural-interface/public/shared/ui-tool-updates.js +3 -3
- package/neural-interface/public/shared/ui-tooltip.js +71 -46
- package/neural-interface/public/shared/ui-topright-overflow.js +233 -0
- package/neural-interface/public/shared/ui-whiteboard.js +1 -1
- package/neural-interface/public/shared/ui-worker-end.js +1 -1
- package/neural-interface/public/shared/whatsapp/wa-view.js +2 -2
- package/neural-interface/public/shared/worker-questions.js +34 -0
- package/neural-interface/public/sidepanel-session.html +1 -1
- package/neural-interface/server.js +731 -466
- package/neural-interface/templates/rulesets/README.md +1 -1
- package/neural-interface/templates/rulesets/legacy-hashes.json +5 -5
- package/neural-interface/templates/rulesets/manifest.json +1 -1
- package/npm-shrinkwrap.json +18 -2
- package/package.json +2 -1
- package/skills/skill-forge/SKILL.md +4 -4
- package/skills/synabun/SKILL.md +1 -1
- package/skills/synabun/modules/schedule.md +3 -3
- /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:
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
|
165
|
-
const pool = BUDGET_PROVIDERS.filter((p) => eligible
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
339
|
-
|
|
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),
|