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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readStoredToolListRecord, toolListRecordForUpdate } from './lib/tool-lists.js';
|
|
2
|
-
import { externalServerInventory, selectExternalServers, externalCatalog, EXTERNAL_SERVER_LIMITATIONS, scopeOpenCodeServers, retainExternalRefs, writePrivateMcpConfig, redactMcpValues,
|
|
2
|
+
import { externalServerInventory, selectExternalServers, externalCatalog, EXTERNAL_SERVER_LIMITATIONS, scopeOpenCodeServers, retainExternalRefs, writePrivateMcpConfig, redactMcpValues, mcpSecretValues } from './lib/mcp-external-servers.js';
|
|
3
3
|
import { openCodeWorkerData } from './lib/opencode-worker-data.js';
|
|
4
4
|
import { resumeSandboxPlan } from './lib/worker-resume.js';
|
|
5
5
|
import { waitForSynabunTools, synabunConfigured, invalidateSynabunTools, synabunToolsTimeoutText } from './lib/opencode-mcp-ready.js';
|
|
@@ -25,7 +25,7 @@ import { SANDBOX_EXEC, buildWorkerSandboxProfile, canonicalPath, sandboxExecArgv
|
|
|
25
25
|
import { createWorkerMcpChannel } from './lib/worker-mcp-channel.js';
|
|
26
26
|
import { createPageSession, PAGE_SECRET_PATHS, PAGE_SECRET_REFRESH_PATH, SERVICE_WORKER_PATH } from './lib/page-session.js';
|
|
27
27
|
import { normalizeViewUrl } from './public/shared/view-variant.js';
|
|
28
|
-
import { createPeerIdentity } from './lib/peer-identity.js';
|
|
28
|
+
import { createPeerIdentity, debugClientPolicy } from './lib/peer-identity.js';
|
|
29
29
|
import { createOwnerAuthority } from './lib/owner-authority.js';
|
|
30
30
|
import { collectRelayListenerPorts, localProxyPorts } from './lib/worker-relay-ports.js';
|
|
31
31
|
import { machineNames } from './lib/machine-names.js';
|
|
@@ -56,10 +56,11 @@ import { createEventLoopMonitor } from './lib/event-loop-monitor.js';
|
|
|
56
56
|
import { createRulesetInstaller } from './lib/rulesets/installer.js';
|
|
57
57
|
import { registerRulesetRoutes, registerLegacyRulesetRoutes } from './lib/rulesets/api.js';
|
|
58
58
|
import { createPtyHostManager } from './lib/pty-host/manager.js';
|
|
59
|
-
import { AGENT_HEADERS, hostNameOf, isAllowedHost, isAllowedWebSocketOrigin, parseCookies, refuseUpgrade } from './lib/http-guards.js';
|
|
59
|
+
import { AGENT_HEADERS, hostNameOf, isAllowedHost, isAllowedWebSocketOrigin, parseCookies, refuseUpgrade, requireLocal as requireLoopbackSocket } from './lib/http-guards.js';
|
|
60
60
|
import { createRequestGates, extraHostNames, hostsOf, savedValueOf, startedValueOf } from './lib/request-gate.js';
|
|
61
61
|
import { createGitDiffRouter } from './lib/git-diff-api.js';
|
|
62
|
-
import {
|
|
62
|
+
import { setGitFallbackFolder } from './lib/git-read.js';
|
|
63
|
+
import { gitStatusRunner, projectStatus } from './lib/git-status-runner.js';
|
|
63
64
|
import { createPhoneAuthority } from './lib/remote-policy.js';
|
|
64
65
|
import { brainCredentialOf } from './lib/virtual-socket.js';
|
|
65
66
|
import { buildTerminalEnv, interactiveShellArgs, cliWrapperArgs } from './lib/terminal-env.js';
|
|
@@ -104,6 +105,8 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, unlink
|
|
|
104
105
|
import * as fsp from 'fs/promises';
|
|
105
106
|
import { createInterface as createReadlineInterface } from 'readline';
|
|
106
107
|
import { jsonlLines } from './lib/jsonl-lines.js';
|
|
108
|
+
import { createHistoryCache } from './lib/claude-history-cache.js';
|
|
109
|
+
import { createRolloutReader } from './lib/codex-rollout-reader.js';
|
|
107
110
|
import { randomBytes, randomUUID, createHash, pbkdf2Sync, createDecipheriv } from 'crypto';
|
|
108
111
|
import { exec, execFile, execSync, spawn, spawnSync } from 'child_process';
|
|
109
112
|
import { promisify } from 'util';
|
|
@@ -192,6 +195,7 @@ import { codexReasoningEffort } from './lib/effort-levels.js';
|
|
|
192
195
|
import {
|
|
193
196
|
createClaudeNativeLoopAdapter,
|
|
194
197
|
createCodexNativeLoopAdapter,
|
|
198
|
+
createAntigravityNativeLoopAdapter,
|
|
195
199
|
createOpenCodeNativeLoopAdapter,
|
|
196
200
|
openCodeRunConfigFor,
|
|
197
201
|
openCodeRunEnvFor,
|
|
@@ -201,9 +205,9 @@ import {
|
|
|
201
205
|
} from './lib/native-loop-providers.js';
|
|
202
206
|
import { createOpenCodeActivityRelay } from './lib/opencode-worker-activity.js';
|
|
203
207
|
import { browserServePortDenial } from './lib/browser-tool-policy.js';
|
|
204
|
-
import { createNativeServeBoundary, nativeServeCapability } from './lib/opencode-native-boundary.js';
|
|
208
|
+
import { createNativeServeBoundary, nativeServeCapability, nativeServeReason, narrowOffenders } from './lib/opencode-native-boundary.js';
|
|
205
209
|
const nativeServeBoundary = createNativeServeBoundary();
|
|
206
|
-
import { setBrowserNetworkRule, usesNativeBrowserNetworkGuard, disposeBrowserNetworkGuard, MONEY_PATTERNS, SERVE_PORT_PATTERNS } from './lib/browser-network-guard.js';
|
|
210
|
+
import { setBrowserNetworkRule, usesNativeBrowserNetworkGuard, disposeBrowserNetworkGuard, recoverBrowserNetworkGuard, MONEY_PATTERNS, SERVE_PORT_PATTERNS } from './lib/browser-network-guard.js';
|
|
207
211
|
import { browserPolicyRoots } from './lib/browser-policy-roots.js';
|
|
208
212
|
import {
|
|
209
213
|
mergeCodexModelContextCapabilities,
|
|
@@ -234,12 +238,21 @@ import { initBlobStore, blobGetNamespace, blobGet, blobPut, blobDelete, blobClea
|
|
|
234
238
|
import { createUiBlobRouter } from './lib/ui-blob-api.js';
|
|
235
239
|
import { memoCache, asyncPool } from './lib/memo-cache.js';
|
|
236
240
|
import {
|
|
241
|
+
buildAntigravityMcpEntry,
|
|
237
242
|
buildClaudeMcpEntry,
|
|
238
243
|
buildCodexMcpKv,
|
|
239
|
-
|
|
244
|
+
describeAntigravityMcpEntry,
|
|
245
|
+
mergeAntigravityMcpConfig,
|
|
240
246
|
mergeClaudeMcpConfig,
|
|
241
|
-
|
|
247
|
+
parseAntigravityMcpConfig,
|
|
248
|
+
removeAntigravityMcpServer,
|
|
242
249
|
} from './lib/mcp-client-config.js';
|
|
250
|
+
import { ANTIGRAVITY_PANEL_CAPABILITIES, ANTIGRAVITY_WS_PATH, createAntigravityPanelBridge, validateRunHome } from './lib/antigravity/panel-bridge.js';
|
|
251
|
+
import { migrateGeminiFiles, migrateUiStateValues as migrateGeminiUiState } from './lib/antigravity/migrate-gemini.js';
|
|
252
|
+
import { groupSessionsByProject, listAntigravityModels, listAntigravitySessions } from './lib/antigravity/catalog.js';
|
|
253
|
+
import { defaultAntigravityPolicy } from './lib/antigravity/policy.js';
|
|
254
|
+
import { antigravityHomeDir, antigravityHomesRoot, pruneAntigravityHomes } from './lib/antigravity/home.js';
|
|
255
|
+
import { antigravityStores, findUserConversation, parseAntigravityTranscript, readTranscriptText } from './lib/antigravity/transcripts.js';
|
|
243
256
|
import {
|
|
244
257
|
buildAgentDisallowedTools,
|
|
245
258
|
agentAccessFrom,
|
|
@@ -304,7 +317,7 @@ import { registerSkillEvalRoutes } from './lib/skills/evals-api.js';
|
|
|
304
317
|
import { evalConfinementSupport } from './lib/skills/eval-confinement.js';
|
|
305
318
|
import { evalShellReadPaths, evalShellSupported, prepareEvalShell } from './lib/skills/eval-sandbox.js';
|
|
306
319
|
import { urlProblem as skillSourceUrlProblem } from './public/shared/skills/ss-creation.js';
|
|
307
|
-
import { claudeRowsToMessages, codexRolloutToMessages, opencodeRowsToMessages } from './lib/skills/creation-sources.js';
|
|
320
|
+
import { antigravityMessagesToMessages, claudeRowsToMessages, codexRolloutToMessages, opencodeRowsToMessages } from './lib/skills/creation-sources.js';
|
|
308
321
|
import { renderSkillBody } from './public/shared/skills/ss-args.js';
|
|
309
322
|
import { claudeBoolean } from './public/shared/skills/ss-profiles.js';
|
|
310
323
|
import { generateSessionTitle } from './lib/session-title-generator.js';
|
|
@@ -355,6 +368,7 @@ import {
|
|
|
355
368
|
import { ensureProjectCategories as ensureProjectCategoriesInFile, ensureCategories as ensureCategoriesInFile, FEATURE_CATEGORIES } from '../hooks/claude-code/shared.mjs';
|
|
356
369
|
import { getDataHome, getDataHomeDiagnostics, isGlobalInstall, pathIsInside } from '../lib/paths.js';
|
|
357
370
|
import { launcherLogPath } from '../lib/first-launch-protection.js';
|
|
371
|
+
import { prunePreUpdateSnapshots, resolvePreUpdateKeep } from '../lib/update-snapshot.js';
|
|
358
372
|
import { launcherRegistrationAllowed, registerStartLauncher, startLauncherState } from '../lib/start-launcher.js';
|
|
359
373
|
import { effectiveNpmPrefix } from '../lib/child-path.js';
|
|
360
374
|
import { checkRuntimeDependencies, fallbackInstallArgs, fallbackInstallEnv, findInstalled, mcpCompiler, npmCommand, npmInvocation } from '../lib/runtime-dependencies.js';
|
|
@@ -381,7 +395,7 @@ import {
|
|
|
381
395
|
getFbActionBudget, getFbEngagementTier, FB_ACTION_TYPES,
|
|
382
396
|
} from './lib/db.js';
|
|
383
397
|
import {
|
|
384
|
-
rebuildClaudeCache, rebuildCodexCache, rebuildOpencodeCache,
|
|
398
|
+
rebuildClaudeCache, rebuildCodexCache, rebuildOpencodeCache, rebuildAntigravityCache,
|
|
385
399
|
} from './lib/session-cache-builder.js';
|
|
386
400
|
import {
|
|
387
401
|
clearOpenCodeHistory,
|
|
@@ -433,6 +447,9 @@ try {
|
|
|
433
447
|
// allowed until a restart. What the environment itself gives is noted first and put back after the load; the
|
|
434
448
|
// file's own line is read when a request asks (publicWebOrigins).
|
|
435
449
|
const startedAllowedHosts = startedValueOf(process.env);
|
|
450
|
+
// The profiling exemption (lib/peer-identity.js debugClientPolicy) is read from the started environment only, before
|
|
451
|
+
// the data home's .env can add to it.
|
|
452
|
+
const startedDebugClient = debugClientPolicy(process.env);
|
|
436
453
|
dotenvConfig({ path: resolve(getDataHome(), '.env') });
|
|
437
454
|
startedAllowedHosts.restore();
|
|
438
455
|
|
|
@@ -542,7 +559,7 @@ app.use(requestGates.features);
|
|
|
542
559
|
// in front of every route (the page secret's pieces are made further down and read only when a request comes).
|
|
543
560
|
// This install's root (PACKAGE_ROOT, defined further down): SynaBun's own MCP server over HTTP is the one listening
|
|
544
561
|
// program the lookup knows, by its main script under it (mcp-server/dist/http.js, `npm run start:http`).
|
|
545
|
-
const _peerIdentity = createPeerIdentity({ log: (line) => console.warn(line), disabled: String(process.env.SYNABUN_PEER_IDENTITY || '').toLowerCase() === 'off', synabunRoots: [resolve(__dirname, '..')] });
|
|
562
|
+
const _peerIdentity = createPeerIdentity({ log: (line) => console.warn(line), disabled: String(process.env.SYNABUN_PEER_IDENTITY || '').toLowerCase() === 'off', synabunRoots: [resolve(__dirname, '..')], debugClient: startedDebugClient });
|
|
546
563
|
// The one decision for the owner's routes and sockets (lib/owner-authority.js): every write to /api but the hooks'
|
|
547
564
|
// and the agents' feature data, the two reads that hand out a secret, and the owner's sockets are refused to the
|
|
548
565
|
// automation browser (its marker or its process), relays, other SynaBun servers, remote logins and an agent's tree
|
|
@@ -2388,6 +2405,7 @@ function gracefulShutdown(reason = 'shutdown') {
|
|
|
2388
2405
|
}
|
|
2389
2406
|
|
|
2390
2407
|
async function runGracefulShutdown(reason) {
|
|
2408
|
+
gitStatusRunner.shutdown();
|
|
2391
2409
|
console.log(`[server] gracefulShutdown(${reason}) — releasing children...`);
|
|
2392
2410
|
|
|
2393
2411
|
// Native automation providers own SDK subprocesses/isolated serves that are
|
|
@@ -2399,6 +2417,8 @@ async function runGracefulShutdown(reason) {
|
|
|
2399
2417
|
try { keepRuns = _assistantDispatcher?.suspendForShutdown?.(reason) || []; } catch (err) { console.warn(' assistant suspend err:', err?.message); }
|
|
2400
2418
|
try { await _nativeLoopRuntime?.shutdown(reason, { keep: keepRuns }); } catch (err) { console.warn(' native loop stop err:', err?.message); }
|
|
2401
2419
|
try { stopAllIsolatedServes(); } catch (err) { console.warn(' isolated opencode stop err:', err?.message); }
|
|
2420
|
+
// The Antigravity panel's `agy acp` processes (one per open panel socket).
|
|
2421
|
+
try { await antigravityPanel.closeAll(); } catch (err) { console.warn(' antigravity panel stop err:', err?.message); }
|
|
2402
2422
|
// The code editor's completion serve, and today's autocomplete counters.
|
|
2403
2423
|
try { await editorCompletion.stop(); } catch (err) { console.warn(' editor completion stop err:', err?.message); }
|
|
2404
2424
|
|
|
@@ -2818,7 +2838,17 @@ app.post('/api/system/run-update', async (req, res) => {
|
|
|
2818
2838
|
appVersion: installPlan.current,
|
|
2819
2839
|
additionalEntries: getBackupAdditionalEntries(),
|
|
2820
2840
|
}));
|
|
2821
|
-
|
|
2841
|
+
// Older pre-update snapshots are pruned only now, after this one is written and verified: a
|
|
2842
|
+
// failed createVerifiedBackup above threw and never reached here.
|
|
2843
|
+
try {
|
|
2844
|
+
const configuredKeep = loadAutoBackupConfig()?.retention?.preUpdateKeep ?? null;
|
|
2845
|
+
prunePreUpdateSnapshots({
|
|
2846
|
+
folderPath: updateBackupDir,
|
|
2847
|
+
keep: resolvePreUpdateKeep(configuredKeep),
|
|
2848
|
+
keepPath: snapshot.path,
|
|
2849
|
+
log: (message) => console.log(`[update-backup] ${message}`),
|
|
2850
|
+
});
|
|
2851
|
+
} catch (error) { console.error(`[update-backup] retention cleanup failed: ${error?.message || error}`); }
|
|
2822
2852
|
const handoffPath = resolve(DATA_HOME, 'data', 'update-handoff.json');
|
|
2823
2853
|
writeFileSync(handoffPath, JSON.stringify({
|
|
2824
2854
|
version: 1,
|
|
@@ -3656,6 +3686,28 @@ function getClaudeSdkExecutable() {
|
|
|
3656
3686
|
});
|
|
3657
3687
|
}
|
|
3658
3688
|
|
|
3689
|
+
// Google Antigravity's CLI (`agy`): what the side panel (`agy acp`), the workers, the model list and the session list
|
|
3690
|
+
// start. The resolver's answer, as a program and the words before its own arguments: a binary as it is, a script
|
|
3691
|
+
// through node, a PowerShell script through PowerShell. A Windows command file (cmd.exe) cannot take words after the
|
|
3692
|
+
// fact, so it is reported as not runnable here (the terminal profile still runs it). null when none is installed.
|
|
3693
|
+
function antigravityLaunch() {
|
|
3694
|
+
const found = cliResolver.launcher('antigravity');
|
|
3695
|
+
_noteCliLookup('antigravity', found, 'Antigravity CLI');
|
|
3696
|
+
const spec = found.spec;
|
|
3697
|
+
if (!found.path || !spec || found.state === 'broken') return null;
|
|
3698
|
+
if (spec.kind === 'direct') return { command: spec.launchPath || found.path, args: [], version: found.version || null, path: found.path };
|
|
3699
|
+
if (spec.kind === 'node-script') return { command: process.execPath, args: [spec.launchPath], version: found.version || null, path: found.path };
|
|
3700
|
+
if (spec.kind === 'powershell') return { command: 'powershell.exe', args: ['-NoProfile', '-NonInteractive', '-File', spec.launchPath], version: found.version || null, path: found.path };
|
|
3701
|
+
return null;
|
|
3702
|
+
}
|
|
3703
|
+
// The environment an Antigravity process gets: the PATH a new terminal has (agy runs the user's tools), never the
|
|
3704
|
+
// owner's live data paths or a brain's credential.
|
|
3705
|
+
function antigravityChildEnv() {
|
|
3706
|
+
const env = withPathEnv({ ...process.env }, getAugmentedPath());
|
|
3707
|
+
for (const key of ['SQLITE_DB_PATH', 'MEMORY_DATA_DIR', 'SYNABUN_BRAIN_TOKEN', 'SYNABUN_ASSISTANT_SESSION', 'SYNABUN_ASSISTANT_THREADS']) delete env[key];
|
|
3708
|
+
return env;
|
|
3709
|
+
}
|
|
3710
|
+
|
|
3659
3711
|
// Codex: what the SDK and the app-server start. On Windows the native codex.exe
|
|
3660
3712
|
// an npm command file leads to (the SDK spawns without a shell); the file found
|
|
3661
3713
|
// otherwise. SynaBun carries no Codex (lib/external-tools.js): without one,
|
|
@@ -3706,22 +3758,25 @@ function opencodeServeLaunch(port, { env = process.env, password = null, passwor
|
|
|
3706
3758
|
_noteCliLookup('opencode', found, 'OpenCode');
|
|
3707
3759
|
const line = configuredCommandLine('opencode');
|
|
3708
3760
|
const nativeWebSocketSafe = nativeServeCapability({ version: found.version, commandLine: !!line, passwordFrom });
|
|
3709
|
-
|
|
3761
|
+
// What the browser boundary names when this serve is the one that blocks a native (MoreLogin) context. A command
|
|
3762
|
+
// line from Settings runs whatever it names: the resolver's version is of another binary, so none is reported.
|
|
3763
|
+
const nativeServe = { version: line ? null : (found.version || null), path: line || found.launchPath || found.path || null, reason: nativeServeReason({ version: found.version, commandLine: !!line, passwordFrom }) };
|
|
3764
|
+
nativeServeBoundary.assertLaunch(nativeWebSocketSafe, { port, ...nativeServe });
|
|
3710
3765
|
const spec = serveSpecFor(port, { env, password, passwordFrom });
|
|
3711
3766
|
const words = spec.args.join(' ');
|
|
3712
3767
|
if (IS_WIN) {
|
|
3713
3768
|
const spawnEnv = withPathEnv(spec.env, getAugmentedPath());
|
|
3714
3769
|
if (!line && found.spec) {
|
|
3715
3770
|
const launch = spawnArgsFor(found.spec, spec.args, { env: spawnEnv });
|
|
3716
|
-
return { command: launch.command, args: launch.args, options: { ...launch.options, env: spawnEnv, windowsHide: true }, label: `${found.launchPath} ${words}`, password: spec.password, passwordFrom: spec.passwordFrom, nativeWebSocketSafe };
|
|
3771
|
+
return { command: launch.command, args: launch.args, options: { ...launch.options, env: spawnEnv, windowsHide: true }, label: `${found.launchPath} ${words}`, password: spec.password, passwordFrom: spec.passwordFrom, nativeWebSocketSafe, nativeServe };
|
|
3717
3772
|
}
|
|
3718
3773
|
const text = `${line || 'opencode'} ${words}`;
|
|
3719
|
-
return { command: DEFAULT_SHELL, args: ['/d', '/s', '/c', text], options: { env: spawnEnv, windowsHide: true }, label: text, password: spec.password, passwordFrom: spec.passwordFrom, nativeWebSocketSafe };
|
|
3774
|
+
return { command: DEFAULT_SHELL, args: ['/d', '/s', '/c', text], options: { env: spawnEnv, windowsHide: true }, label: text, password: spec.password, passwordFrom: spec.passwordFrom, nativeWebSocketSafe, nativeServe };
|
|
3720
3775
|
}
|
|
3721
3776
|
const cmd = line || (found.path ? shellSingleQuote(found.path) : 'opencode');
|
|
3722
3777
|
const unset = spec.passwordFrom === 'stdout' ? `${serveUnsetCommand()}; ` : '';
|
|
3723
3778
|
// A packaged application's runtime and npm after the user's own (the login shell adds the user's profile on top).
|
|
3724
|
-
return { command: DEFAULT_SHELL, args: ['-lc', `${unset}exec ${cmd} ${words}`], options: { env: withPackagedPathLast(spec.env) }, label: `${cmd} ${words}`, password: spec.password, passwordFrom: spec.passwordFrom, nativeWebSocketSafe };
|
|
3779
|
+
return { command: DEFAULT_SHELL, args: ['-lc', `${unset}exec ${cmd} ${words}`], options: { env: withPackagedPathLast(spec.env) }, label: `${cmd} ${words}`, password: spec.password, passwordFrom: spec.passwordFrom, nativeWebSocketSafe, nativeServe };
|
|
3725
3780
|
}
|
|
3726
3781
|
|
|
3727
3782
|
// Which OpenCode the binary SynaBun starts is: the resolver's `--version` of it
|
|
@@ -5626,10 +5681,10 @@ CRITICAL — When you need clarification during planning:
|
|
|
5626
5681
|
return item;
|
|
5627
5682
|
}
|
|
5628
5683
|
|
|
5629
|
-
function
|
|
5630
|
-
if (!threadId) return
|
|
5684
|
+
function codexRolloutPathFor(threadId) {
|
|
5685
|
+
if (!threadId) return '';
|
|
5631
5686
|
const dbPath = resolve(activeCodexHome, 'state_5.sqlite');
|
|
5632
|
-
if (!existsSync(dbPath)) return
|
|
5687
|
+
if (!existsSync(dbPath)) return '';
|
|
5633
5688
|
|
|
5634
5689
|
let db = null;
|
|
5635
5690
|
let rolloutPath = '';
|
|
@@ -5642,20 +5697,27 @@ CRITICAL — When you need clarification during planning:
|
|
|
5642
5697
|
} finally {
|
|
5643
5698
|
try { db?.close(); } catch {}
|
|
5644
5699
|
}
|
|
5645
|
-
|
|
5700
|
+
return rolloutPath && existsSync(rolloutPath) ? rolloutPath : '';
|
|
5701
|
+
}
|
|
5646
5702
|
|
|
5647
|
-
|
|
5703
|
+
// The rollout as history items: read as a stream, parsed once while the file is unchanged
|
|
5704
|
+
// (lib/codex-rollout-reader.js), so the early preview and the fallback that follows share one read.
|
|
5705
|
+
async function loadCodexThreadHistoryFallback(threadId) {
|
|
5706
|
+
const rolloutPath = codexRolloutPathFor(threadId);
|
|
5707
|
+
if (!rolloutPath) return [];
|
|
5648
5708
|
try {
|
|
5649
|
-
|
|
5709
|
+
return await codexRolloutReader.items(rolloutPath, parseCodexRolloutLines);
|
|
5650
5710
|
} catch (err) {
|
|
5651
5711
|
console.warn('[codex-skin] Failed to read rollout history:', err.message);
|
|
5652
5712
|
return [];
|
|
5653
5713
|
}
|
|
5714
|
+
}
|
|
5654
5715
|
|
|
5716
|
+
async function parseCodexRolloutLines(lines) {
|
|
5655
5717
|
const items = [];
|
|
5656
5718
|
const calls = new Map();
|
|
5657
5719
|
let seq = 0;
|
|
5658
|
-
for (const line of
|
|
5720
|
+
for await (const line of lines) {
|
|
5659
5721
|
if (!line.trim()) continue;
|
|
5660
5722
|
let entry;
|
|
5661
5723
|
try {
|
|
@@ -6178,7 +6240,29 @@ CRITICAL — When you need clarification during planning:
|
|
|
6178
6240
|
}
|
|
6179
6241
|
|
|
6180
6242
|
|
|
6181
|
-
|
|
6243
|
+
// Only for the thread the client shows (it asks with `preview: true`), only before the app-server is up, read
|
|
6244
|
+
// without blocking its start; dropped once the real history went out. The read is reused by the fallback.
|
|
6245
|
+
function sendEarlyCodexRolloutPreview(threadId, state) {
|
|
6246
|
+
if (!threadId || initialized) return null;
|
|
6247
|
+
const job = loadCodexThreadHistoryFallback(threadId);
|
|
6248
|
+
job.then((previewItems) => {
|
|
6249
|
+
if (state.historySent || !Array.isArray(previewItems) || !previewItems.length) return;
|
|
6250
|
+
const previewThreadId = String(threadId);
|
|
6251
|
+
sendToClient({
|
|
6252
|
+
type: 'history',
|
|
6253
|
+
preview: true,
|
|
6254
|
+
threadId: previewThreadId,
|
|
6255
|
+
thread: { id: previewThreadId, turns: [] },
|
|
6256
|
+
fallbackItems: previewItems,
|
|
6257
|
+
historyMode: 'read',
|
|
6258
|
+
});
|
|
6259
|
+
}).catch(() => {});
|
|
6260
|
+
return job;
|
|
6261
|
+
}
|
|
6262
|
+
|
|
6263
|
+
async function bootstrapThread(threadId, cwd = PACKAGE_ROOT, { preview = false } = {}) {
|
|
6264
|
+
const previewState = { historySent: false };
|
|
6265
|
+
if (preview) sendEarlyCodexRolloutPreview(threadId, previewState);
|
|
6182
6266
|
await ensureInitialized();
|
|
6183
6267
|
if (!threadId) {
|
|
6184
6268
|
displayedThreadId = activeThreadId;
|
|
@@ -6193,11 +6277,13 @@ CRITICAL — When you need clarification during planning:
|
|
|
6193
6277
|
const historyMode = activeThreadId && effectiveThreadId && activeThreadId === effectiveThreadId
|
|
6194
6278
|
? 'resume'
|
|
6195
6279
|
: 'read';
|
|
6280
|
+
const fallbackItems = await loadCodexThreadHistoryFallback(effectiveThreadId);
|
|
6281
|
+
previewState.historySent = true;
|
|
6196
6282
|
sendToClient({
|
|
6197
6283
|
type: 'history',
|
|
6198
6284
|
threadId: effectiveThreadId,
|
|
6199
6285
|
thread: sanitizeCodexThreadForClient(thread),
|
|
6200
|
-
fallbackItems
|
|
6286
|
+
fallbackItems,
|
|
6201
6287
|
historyMode,
|
|
6202
6288
|
});
|
|
6203
6289
|
// `history` and `ready` must describe the same displayed thread. Returning
|
|
@@ -6206,6 +6292,7 @@ CRITICAL — When you need clarification during planning:
|
|
|
6206
6292
|
sendToClient({ type: 'ready', threadId: effectiveThreadId });
|
|
6207
6293
|
} catch (err) {
|
|
6208
6294
|
console.warn('[codex-skin] Failed to resume thread:', threadId, err.message);
|
|
6295
|
+
previewState.historySent = true;
|
|
6209
6296
|
sendToClient({ type: 'history', thread: null, resumeFailed: true, threadId });
|
|
6210
6297
|
sendToClient({ type: 'error', message: `Could not restore saved Codex thread. ${err.message}` });
|
|
6211
6298
|
sendToClient({ type: 'ready', threadId });
|
|
@@ -6318,7 +6405,7 @@ CRITICAL — When you need clarification during planning:
|
|
|
6318
6405
|
const runtime = opts.runtimeOptions || await resolveCodexThreadRuntimeOptions(opts);
|
|
6319
6406
|
displayedThreadId = String(threadId);
|
|
6320
6407
|
const readThread = await readThreadHistory(threadId).catch(() => null);
|
|
6321
|
-
const fallbackItems = loadCodexThreadHistoryFallback(threadId);
|
|
6408
|
+
const fallbackItems = await loadCodexThreadHistoryFallback(threadId);
|
|
6322
6409
|
const previewThread = readThread || (fallbackItems.length ? { id: threadId, turns: [] } : null);
|
|
6323
6410
|
if (previewThread) {
|
|
6324
6411
|
sendToClient({
|
|
@@ -6783,7 +6870,7 @@ CRITICAL — When you need clarification during planning:
|
|
|
6783
6870
|
}
|
|
6784
6871
|
// Resolve the tab's account BEFORE first spawn so CODEX_HOME is correct.
|
|
6785
6872
|
if (!child && msg.accountId) setActiveCodexAccount(msg.accountId);
|
|
6786
|
-
await bootstrapThread(msg.threadId || null, msg.cwd || PACKAGE_ROOT);
|
|
6873
|
+
await bootstrapThread(msg.threadId || null, msg.cwd || PACKAGE_ROOT, { preview: msg.preview === true });
|
|
6787
6874
|
return;
|
|
6788
6875
|
}
|
|
6789
6876
|
if (msg.type === 'permission_roots_list') {
|
|
@@ -8180,8 +8267,8 @@ function noteOpencodeServePort(port, delta) {
|
|
|
8180
8267
|
if (next > 0) _opencodeServePortCounts.set(p, next); else _opencodeServePortCounts.delete(p);
|
|
8181
8268
|
}
|
|
8182
8269
|
|
|
8183
|
-
function trackOpencodeServe(proc, { kind, id = null, port, token, nativeWebSocketSafe = false }) {
|
|
8184
|
-
nativeServeBoundary.track(proc, { port, safe: nativeWebSocketSafe });
|
|
8270
|
+
function trackOpencodeServe(proc, { kind, id = null, port, token, nativeWebSocketSafe = false, nativeServe = null }) {
|
|
8271
|
+
nativeServeBoundary.track(proc, { ...(nativeServe || {}), port, safe: nativeWebSocketSafe });
|
|
8185
8272
|
const pid = Number(proc?.pid) || null;
|
|
8186
8273
|
const exited = () => !proc || proc.exitCode != null || proc.signalCode != null;
|
|
8187
8274
|
if (!exited()) {
|
|
@@ -8707,7 +8794,7 @@ async function ensureIsolatedServe(termId, { xdgRoot: requestedXdgRoot = null, c
|
|
|
8707
8794
|
entry.proc = proc;
|
|
8708
8795
|
entry.password = launch.password || null;
|
|
8709
8796
|
// On record from its spawn: a crash of SynaBun from here on does not leave it running for good.
|
|
8710
|
-
const tracked = trackOpencodeServe(proc, { kind: 'isolated', id: termId, port, token, nativeWebSocketSafe: launch.nativeWebSocketSafe });
|
|
8797
|
+
const tracked = trackOpencodeServe(proc, { kind: 'isolated', id: termId, port, token, nativeWebSocketSafe: launch.nativeWebSocketSafe, nativeServe: launch.nativeServe });
|
|
8711
8798
|
// While it boots, its start sees an exit itself (another server on the port is a collision); afterwards
|
|
8712
8799
|
// its exit ends the serve.
|
|
8713
8800
|
let booting = true;
|
|
@@ -9023,6 +9110,30 @@ setInterval(() => {
|
|
|
9023
9110
|
|
|
9024
9111
|
process.once('exit', stopAllIsolatedServes);
|
|
9025
9112
|
|
|
9113
|
+
// ── Antigravity side panel (/ws/antigravity) ──
|
|
9114
|
+
// One `agy acp` (Google's Antigravity ACP server) per panel socket, one ACP session per tab
|
|
9115
|
+
// (lib/antigravity/panel-bridge.js; docs/antigravity-sidepanel.md). Pasted attachments are written under the data
|
|
9116
|
+
// home for the agent to read; nothing reaches Antigravity's own folders.
|
|
9117
|
+
const antigravityPanel = createAntigravityPanelBridge({
|
|
9118
|
+
resolveLaunch: async () => antigravityLaunch(),
|
|
9119
|
+
childEnv: () => antigravityChildEnv(),
|
|
9120
|
+
attachmentsRoot: resolve(DATA_HOME, 'data', 'antigravity', 'attachments'),
|
|
9121
|
+
homesRoot: () => antigravityHomesRoot(DATA_HOME),
|
|
9122
|
+
isRunHomeActive: (homeDir) => (_nativeLoopRuntime?.list({ activeOnly: true }) || []).some((run) => run.provider === 'antigravity' && run.antigravityHome === homeDir && (run.status === 'starting' || run.status === 'running')),
|
|
9123
|
+
// Bypass keeps the browser policy: a test runner passes from inside a registered code project (lib/antigravity/bypass.js).
|
|
9124
|
+
policyRoots: () => { try { return browserPolicyRoots(loadHookProjects()); } catch { return []; } },
|
|
9125
|
+
clientInfo: { name: 'synabun', title: 'SynaBun', version: String(process.env.npm_package_version || '0') },
|
|
9126
|
+
// Greeting (lib/greeting/): a prompt typed in the panel is noted, and the greeting a tab opened with is told to
|
|
9127
|
+
// the model once, before the typed words (public/shared/agy/agy-greeting.js markers).
|
|
9128
|
+
onHumanPrompt: ({ cwd, words }) => { try { _greeting?.noteActivity({ cwd: cwd || PACKAGE_ROOT, surface: 'antigravity-panel', words }); } catch {} },
|
|
9129
|
+
greetingNote: (id, opts) => _greeting?.consumeNote(id, opts) || '',
|
|
9130
|
+
// `/synabun …` reads the bundled skill, as the Codex panel's does (lib/skill-prompt-bridge.js).
|
|
9131
|
+
expandPrompt: (text) => expandSynabunCodexSlashPrompt(text, join(SKILLS_SOURCE_DIR, 'synabun', 'SKILL.md')),
|
|
9132
|
+
// A turn that ended may have added to the conversation's transcript: Resume search and memory sessions follow.
|
|
9133
|
+
onTurnEnd: ({ sessionId }) => { try { refreshAntigravitySession(sessionId); } catch {} },
|
|
9134
|
+
log: (line) => console.log(line),
|
|
9135
|
+
});
|
|
9136
|
+
|
|
9026
9137
|
function handleOpencodeV2Ws(ws) {
|
|
9027
9138
|
_ocpV2WsClients.add(ws);
|
|
9028
9139
|
ws._nativeLoopWindowId = null;
|
|
@@ -9553,7 +9664,7 @@ async function ensureOpencodeServer({ allowDuringHistoryClear = false } = {}) {
|
|
|
9553
9664
|
});
|
|
9554
9665
|
_ocpProc = proc;
|
|
9555
9666
|
// On record from its spawn: a crash of SynaBun from here on does not leave it running for good.
|
|
9556
|
-
tracked = trackOpencodeServe(proc, { kind: 'shared', id: 'shared', port: _ocpPort, token, nativeWebSocketSafe: launch.nativeWebSocketSafe });
|
|
9667
|
+
tracked = trackOpencodeServe(proc, { kind: 'shared', id: 'shared', port: _ocpPort, token, nativeWebSocketSafe: launch.nativeWebSocketSafe, nativeServe: launch.nativeServe });
|
|
9557
9668
|
const readPassword = servePasswordReader();
|
|
9558
9669
|
const outLog = createServeLogRedactor(line => console.log(`[opencode:stdout] ${line.trim()}`));
|
|
9559
9670
|
const errLog = createServeLogRedactor(line => console.log(`[opencode:stderr] ${line.trim()}`));
|
|
@@ -11001,7 +11112,7 @@ const _codexSettingsProbes = createLaunchCache({
|
|
|
11001
11112
|
});
|
|
11002
11113
|
setCodexSettingsStore({ get: (key) => _codexSettingsProbes.peek(key), set: (key, value) => _codexSettingsProbes.set(key, value) });
|
|
11003
11114
|
|
|
11004
|
-
async function getClaudeModelsForClient(force = false) {
|
|
11115
|
+
async function getClaudeModelsForClient(force = false, { nonBlocking = false } = {}) {
|
|
11005
11116
|
try {
|
|
11006
11117
|
const bin = getClaudeBin();
|
|
11007
11118
|
// Sidepanel sessions and native loops run this same installation. The Agent
|
|
@@ -11010,8 +11121,9 @@ async function getClaudeModelsForClient(force = false) {
|
|
|
11010
11121
|
// when the CLI changes).
|
|
11011
11122
|
checkClaudeCliSkew(bin).catch(() => {});
|
|
11012
11123
|
// Answered from the kept list at once (asked again in the background when it is old);
|
|
11013
|
-
// only a first read, another binary or account, or ?refresh=1 waits for the CLI
|
|
11014
|
-
|
|
11124
|
+
// only a first read, another binary or account, or ?refresh=1 waits for the CLI, unless
|
|
11125
|
+
// nonBlocking=true (e.g. cold /api/claude/config boot on Windows) warms it asynchronously.
|
|
11126
|
+
const result = await discoverClaudeModels(bin, { force, nonBlocking, cwd: PACKAGE_ROOT, account: claudeModelAccountKey() });
|
|
11015
11127
|
if (result?.models?.length) return result;
|
|
11016
11128
|
} catch (err) {
|
|
11017
11129
|
console.warn('[claude-models] discovery failed:', err.message);
|
|
@@ -11039,7 +11151,11 @@ app.get('/api/claude/config', async (req, res) => {
|
|
|
11039
11151
|
// Model ids come straight from the CLI and are already spawn-ready (they carry
|
|
11040
11152
|
// the `[1m]` suffix where applicable), so the client uses them verbatim — no
|
|
11041
11153
|
// "<id>:<contextWindow>" composite. See toCliModelName() for the legacy form.
|
|
11042
|
-
|
|
11154
|
+
// Default to non-blocking model discovery so cold panel opens on Windows never stall
|
|
11155
|
+
// behind a 3–7s `claude.exe` spawn; ui-claude-panel.js refreshes fallback models async.
|
|
11156
|
+
const { source, models } = await getClaudeModelsForClient(false, {
|
|
11157
|
+
nonBlocking: req.query?.waitModels !== '1',
|
|
11158
|
+
});
|
|
11043
11159
|
res.json({ ok: true, config: cliCfg, projects, models, modelsSource: source, bootId: SERVER_BOOT_ID });
|
|
11044
11160
|
} catch (err) {
|
|
11045
11161
|
res.status(500).json({ error: err.message });
|
|
@@ -14372,9 +14488,15 @@ function isSidepanelClaimTokenRevoked(windowId, claimToken) {
|
|
|
14372
14488
|
|
|
14373
14489
|
// Native provider run ledger. A sidepanel can attach after a page refresh or
|
|
14374
14490
|
// after a schedule started while no Neural Interface window was open.
|
|
14491
|
+
function withSidepanelRunHome(run) {
|
|
14492
|
+
if (!run || run.provider !== 'antigravity' || run.antigravityHome || !run.runId) return run;
|
|
14493
|
+
const home = _assistantDispatcher?.get?.(run.runId)?.antigravityHome || null;
|
|
14494
|
+
return home ? { ...run, antigravityHome: home } : run;
|
|
14495
|
+
}
|
|
14496
|
+
|
|
14375
14497
|
app.get('/api/sidepanel-runs', (req, res) => {
|
|
14376
14498
|
const activeOnly = req.query.active === '1' || req.query.active === 'true';
|
|
14377
|
-
res.json({ ok: true, runs: _nativeLoopRuntime?.list({ activeOnly }) || [] });
|
|
14499
|
+
res.json({ ok: true, runs: (_nativeLoopRuntime?.list({ activeOnly }) || []).map(withSidepanelRunHome) });
|
|
14378
14500
|
});
|
|
14379
14501
|
|
|
14380
14502
|
app.post('/api/sidepanel-runs/release-window', (req, res) => {
|
|
@@ -14386,7 +14508,7 @@ app.post('/api/sidepanel-runs/release-window', (req, res) => {
|
|
|
14386
14508
|
app.get('/api/sidepanel-runs/:runId', (req, res) => {
|
|
14387
14509
|
const run = _nativeLoopRuntime?.get(req.params.runId);
|
|
14388
14510
|
if (!run) return res.status(404).json({ error: 'Native sidepanel run not found' });
|
|
14389
|
-
res.json({ ok: true, run });
|
|
14511
|
+
res.json({ ok: true, run: withSidepanelRunHome(run) });
|
|
14390
14512
|
});
|
|
14391
14513
|
|
|
14392
14514
|
app.post('/api/sidepanel-runs/:runId/claim', (req, res) => {
|
|
@@ -14395,7 +14517,7 @@ app.post('/api/sidepanel-runs/:runId/claim', (req, res) => {
|
|
|
14395
14517
|
}
|
|
14396
14518
|
const result = _nativeLoopRuntime?.claim(req.params.runId, req.body?.windowId);
|
|
14397
14519
|
if (!result?.ok) return res.status(result?.reason === 'not_found' ? 404 : 409).json(result || { ok: false });
|
|
14398
|
-
res.json(result);
|
|
14520
|
+
res.json(result.run ? { ...result, run: withSidepanelRunHome(result.run) } : result);
|
|
14399
14521
|
});
|
|
14400
14522
|
|
|
14401
14523
|
app.post('/api/sidepanel-runs/:runId/release', (req, res) => {
|
|
@@ -14493,7 +14615,7 @@ app.post('/api/loop/launch', async (req, res) => {
|
|
|
14493
14615
|
const acq = await acquireLoopBrowserAndTab({ requestedBrowserSessionId, terminalSessionId });
|
|
14494
14616
|
if (acq.error) {
|
|
14495
14617
|
loopLog(terminalSessionId, 'launch:browser', 'ABORT: browser acquisition failed — returning 500', { error: acq.error });
|
|
14496
|
-
return res.status(500).json({ error: acq.error });
|
|
14618
|
+
return res.status(500).json({ error: acq.error, ...(acq.code ? { code: acq.code, offenders: acq.offenders } : {}) });
|
|
14497
14619
|
}
|
|
14498
14620
|
browserSessionId = acq.browserSessionId;
|
|
14499
14621
|
browserTabId = acq.browserTabId;
|
|
@@ -14533,7 +14655,7 @@ app.post('/api/loop/launch', async (req, res) => {
|
|
|
14533
14655
|
// .claude/ set awaitingNext and attachLoopDriver() drives /clear + re-prompt; if those
|
|
14534
14656
|
// hooks are absent (e.g. a claude-code task run under a hookless harness) the loop STALLS.
|
|
14535
14657
|
// 'exec': harness-agnostic — attachExecLoopDriver() owns iteration via sentinel echoes, so
|
|
14536
|
-
// OpenCode/Codex/
|
|
14658
|
+
// OpenCode/Codex/Antigravity self-advance. Facebook seeding must stay profile:opencode for this.
|
|
14537
14659
|
driverType: nativeRuntime ? 'native' : (cliProfile === 'claude-code' ? 'hook' : 'exec'),
|
|
14538
14660
|
runtimeType: nativeRuntime ? 'native' : 'pty',
|
|
14539
14661
|
surface: nativeRuntime ? 'sidepanel' : 'terminal',
|
|
@@ -14652,12 +14774,12 @@ app.post('/api/loop/launch', async (req, res) => {
|
|
|
14652
14774
|
loopLog(terminalSessionId, 'launch:driver', 'attaching hook-driven loop driver (claude-code)');
|
|
14653
14775
|
attachLoopDriver(terminalSessionId);
|
|
14654
14776
|
} else {
|
|
14655
|
-
// Codex/
|
|
14777
|
+
// Codex/Antigravity/other: spawn shell, server drives `<cli> exec` commands per iteration.
|
|
14656
14778
|
// The driver invokes a platform-native wrapper that captures the CLI
|
|
14657
14779
|
// status and emits a sentinel. Immune to PS1/PROMPT overrides.
|
|
14658
14780
|
loopLog(terminalSessionId, 'launch:spawn', 'creating shell PTY for exec-driven CLI', { profile: cliProfile, cwd: loopCwd });
|
|
14659
14781
|
createTerminalSession('shell', 120, 30, loopCwd, { extraEnv: loopExtraEnv, sessionId: terminalSessionId });
|
|
14660
|
-
loopLog(terminalSessionId, 'launch:driver', 'attaching exec loop driver (codex/
|
|
14782
|
+
loopLog(terminalSessionId, 'launch:driver', 'attaching exec loop driver (codex/antigravity/other)', { profile: cliProfile });
|
|
14661
14783
|
attachExecLoopDriver(terminalSessionId);
|
|
14662
14784
|
}
|
|
14663
14785
|
|
|
@@ -14774,7 +14896,7 @@ app.post('/api/loop/stop', async (req, res) => {
|
|
|
14774
14896
|
});
|
|
14775
14897
|
|
|
14776
14898
|
// POST /api/loop/resolve-from-ancestors — process-tree fallback for MCP env loss.
|
|
14777
|
-
// The MCP server walks its ancestor PID chain (codex/opencode/
|
|
14899
|
+
// The MCP server walks its ancestor PID chain (codex/opencode/antigravity → shell PTY)
|
|
14778
14900
|
// and POSTs the chain here. We match any PID against terminalSessions[*].pty.pid
|
|
14779
14901
|
// and return that loop's browserSessionId/browserTabId. Layer B of the
|
|
14780
14902
|
// defense-in-depth browser tab isolation fix.
|
|
@@ -14819,7 +14941,7 @@ app.post('/api/loop/recover-browser', async (req, res) => {
|
|
|
14819
14941
|
const terminalSessionId = req.body?.terminalSessionId || randomBytes(16).toString('hex');
|
|
14820
14942
|
const acq = await acquireLoopBrowserAndTab({ terminalSessionId, logTag: 'recover:browser' });
|
|
14821
14943
|
if (acq.error) {
|
|
14822
|
-
return res.status(500).json({ error: acq.error });
|
|
14944
|
+
return res.status(500).json({ error: acq.error, ...(acq.code ? { code: acq.code, offenders: acq.offenders } : {}) });
|
|
14823
14945
|
}
|
|
14824
14946
|
// Persist the new pins onto the loop's state file when we can identify it,
|
|
14825
14947
|
// so resolve-from-ancestors and loop/stop operate on the live session/tab.
|
|
@@ -15570,7 +15692,7 @@ async function launchScheduledLoop(schedule) {
|
|
|
15570
15692
|
}
|
|
15571
15693
|
|
|
15572
15694
|
// Native providers run headlessly in their durable provider conversation.
|
|
15573
|
-
//
|
|
15695
|
+
// Unsupported profiles retain the existing PTY drivers.
|
|
15574
15696
|
let runDescriptor = null;
|
|
15575
15697
|
if (nativeRuntime) {
|
|
15576
15698
|
const presentation = resolveScheduleRunPresentation(
|
|
@@ -16818,7 +16940,7 @@ function spawnAgentProcess(agent, prompt, extraEnv = {}) {
|
|
|
16818
16940
|
const sessionId = agent._activeSessionId || agent.sessionId;
|
|
16819
16941
|
|
|
16820
16942
|
const serverMap = JSON.parse(agent._mcpConfig).mcpServers;
|
|
16821
|
-
const mcpValues =
|
|
16943
|
+
const mcpValues = mcpSecretValues(serverMap, { env: process.env });
|
|
16822
16944
|
const mcpConfigPath = writePrivateMcpConfig(resolve(DATA_HOME, 'data', 'agents', agent.id, 'mcp.json'), serverMap);
|
|
16823
16945
|
const args = [
|
|
16824
16946
|
'--print',
|
|
@@ -17299,7 +17421,7 @@ app.post('/api/agents/launch', async (req, res) => {
|
|
|
17299
17421
|
const acq = await acquireSharedBrowserTab({ ownerKey: `agent:${agentId}`, ownerType: 'interactive', logTag: 'agent:browser' });
|
|
17300
17422
|
if (acq.error) {
|
|
17301
17423
|
console.warn(`[agent] Browser tab acquire failed: ${acq.error}`);
|
|
17302
|
-
return res.status(500).json({ error: `Could not start browser session for this automation: ${acq.error}
|
|
17424
|
+
return res.status(500).json({ error: `Could not start browser session for this automation: ${acq.error}`, ...(acq.code ? { code: acq.code, offenders: acq.offenders } : {}) });
|
|
17303
17425
|
}
|
|
17304
17426
|
browserSessionId = acq.browserSessionId;
|
|
17305
17427
|
browserTabId = acq.browserTabId;
|
|
@@ -17499,7 +17621,7 @@ function isSynabunGitHubRemote(remoteText) {
|
|
|
17499
17621
|
return /github\.com[:/]danilokhury\/Synabun(?:\.git)?(?:[\s)]|$)/i.test(String(remoteText || ''));
|
|
17500
17622
|
}
|
|
17501
17623
|
|
|
17502
|
-
function detectSynabunInstallSource() {
|
|
17624
|
+
async function detectSynabunInstallSource() {
|
|
17503
17625
|
if (PACKAGED_RUNTIME) return { kind: 'packaged-app' };
|
|
17504
17626
|
const normalizedRoot = PACKAGE_ROOT.replace(/\\/g, '/');
|
|
17505
17627
|
if (/\/node_modules\/synabun$/i.test(normalizedRoot)) {
|
|
@@ -17509,13 +17631,13 @@ function detectSynabunInstallSource() {
|
|
|
17509
17631
|
try {
|
|
17510
17632
|
// Run as reads (lib/git-read.js): the app's own checkout is a folder like any other, and can be a registered
|
|
17511
17633
|
// project. What git could not say is an empty answer.
|
|
17512
|
-
const git = (args) => {
|
|
17513
|
-
const result = spawnGitRead(args, { cwd: PACKAGE_ROOT, timeout: 2000 });
|
|
17634
|
+
const git = async (args) => {
|
|
17635
|
+
const result = await spawnGitRead(args, { cwd: PACKAGE_ROOT, timeout: 2000 });
|
|
17514
17636
|
return result.status === 0 ? result.stdout.toString().trim() : '';
|
|
17515
17637
|
};
|
|
17516
|
-
if (git(['rev-parse', '--is-inside-work-tree']) === 'true') {
|
|
17517
|
-
const remote = git(['remote', 'get-url', 'origin']);
|
|
17518
|
-
const remotes = git(['remote', '-v']);
|
|
17638
|
+
if (await git(['rev-parse', '--is-inside-work-tree']) === 'true') {
|
|
17639
|
+
const remote = await git(['remote', 'get-url', 'origin']);
|
|
17640
|
+
const remotes = await git(['remote', '-v']);
|
|
17519
17641
|
if (isSynabunGitHubRemote(`${remote}\n${remotes}`)) {
|
|
17520
17642
|
const firstRemote = remote || remotes.split(/\s+/).find(part => isSynabunGitHubRemote(part)) || null;
|
|
17521
17643
|
return { kind: 'github-clone', remote: firstRemote };
|
|
@@ -17600,7 +17722,7 @@ async function checkSynabunUpdate() {
|
|
|
17600
17722
|
const installPlan = buildSynabunInstallPlan({
|
|
17601
17723
|
...found,
|
|
17602
17724
|
current,
|
|
17603
|
-
installSource: updateAvailable ? detectSynabunInstallSource() : null,
|
|
17725
|
+
installSource: updateAvailable ? await detectSynabunInstallSource() : null,
|
|
17604
17726
|
repoUrl: SYNABUN_GITHUB_REPO_URL,
|
|
17605
17727
|
});
|
|
17606
17728
|
|
|
@@ -17742,6 +17864,8 @@ function systemActivitySources() {
|
|
|
17742
17864
|
const blockers = getOpenCodeHistoryBlockers();
|
|
17743
17865
|
return blockers.activeTurns + blockers.startingTurns;
|
|
17744
17866
|
},
|
|
17867
|
+
// A prompt running in an Antigravity panel tab (lib/antigravity/panel-bridge.js); dispatched runs are under assistant.
|
|
17868
|
+
'antigravity-panel': () => antigravityPanel.turnsRunning(),
|
|
17745
17869
|
'claude-cli': () => recentCliSessions(sessionRegistry),
|
|
17746
17870
|
loop: () => {
|
|
17747
17871
|
// A dispatched worker also runs in the native runtime: it is counted under assistant.
|
|
@@ -17770,7 +17894,9 @@ app.get('/api/system/activity', (req, res) => {
|
|
|
17770
17894
|
const TOOL_DEFS = [
|
|
17771
17895
|
{ key: 'claude-code', cmd: 'claude', versionArg: '--version', npm: '@anthropic-ai/claude-code' },
|
|
17772
17896
|
{ key: 'codex', cmd: 'codex', versionArg: '--version', npm: '@openai/codex' },
|
|
17773
|
-
|
|
17897
|
+
// Google's Antigravity CLI has no npm package: a Homebrew cask's newer version is what `brew outdated` says, and
|
|
17898
|
+
// the CLI updates itself (`agy update`).
|
|
17899
|
+
{ key: 'antigravity', cmd: 'agy', versionArg: '--version' },
|
|
17774
17900
|
// npm's opencode-ai is OpenCode 1: OpenCode 2's newer version is what Homebrew says (a brew install) or what OpenCode
|
|
17775
17901
|
// itself announces in its own interface.
|
|
17776
17902
|
{ key: 'opencode', cmd: 'opencode', versionArg: '--version' },
|
|
@@ -17970,7 +18096,11 @@ async function checkToolVersions({ force = false } = {}) {
|
|
|
17970
18096
|
|| 'unknown';
|
|
17971
18097
|
for (const [key, info] of Object.entries(results)) {
|
|
17972
18098
|
const source = info.installed ? info.installSource : dominantSource;
|
|
17973
|
-
|
|
18099
|
+
// The Antigravity CLI: the command of the install its button runs here (Homebrew's cask on a Mac that has
|
|
18100
|
+
// Homebrew, else Google's installer), so the banner never shows one command and runs another.
|
|
18101
|
+
info.installCommand = key === 'antigravity' && info.install?.available
|
|
18102
|
+
? cliInstaller.manualCommand(key)
|
|
18103
|
+
: buildInstallCommand(key, source, { platform: process.platform });
|
|
17974
18104
|
}
|
|
17975
18105
|
|
|
17976
18106
|
// A check started before a newer refresh must never publish its stale answer.
|
|
@@ -18039,7 +18169,7 @@ app.use('/api/cli/path', createCliPathApi({
|
|
|
18039
18169
|
pathManager: cliPathManager, resolver: cliResolver, isGuestRequest, port: () => PORT,
|
|
18040
18170
|
diagnostics: async () => {
|
|
18041
18171
|
const fresh = await freshTerminalPath();
|
|
18042
|
-
const statuses = await cliResolver.resolveAll(['claude-code', 'codex', 'opencode']);
|
|
18172
|
+
const statuses = await cliResolver.resolveAll(['claude-code', 'codex', 'opencode', 'antigravity']);
|
|
18043
18173
|
return { platform: process.platform, pathKey: Object.keys(fresh.env).find(key => key.toUpperCase() === 'PATH'),
|
|
18044
18174
|
entries: fresh.entries, os: fresh.os, pathext: fresh.env.PATHEXT || fresh.env.PathExt || null,
|
|
18045
18175
|
tools: Object.fromEntries(Object.entries(statuses).map(([tool, status]) => [tool, { path: status.path, launchPath: status.launchPath, ...cliPathManager.inspect(status) }])) };
|
|
@@ -18544,7 +18674,7 @@ const editorCompletionServe = createOpencodeCompletionEngine({
|
|
|
18544
18674
|
const token = opencodeServeRecords.newToken();
|
|
18545
18675
|
const launch = opencodeServeLaunch(port, { env: { ...env, [SERVE_TOKEN_ENV]: token }, passwordFrom: 'stdout' });
|
|
18546
18676
|
const proc = spawn(launch.command, launch.args, { stdio: ['ignore', 'pipe', 'pipe'], ...launch.options, cwd, detached: false, shell: false, windowsHide: true });
|
|
18547
|
-
editorCompletionServeTracks.set(proc, trackOpencodeServe(proc, { kind: 'completion', id: 'editor-complete', port, token, nativeWebSocketSafe: launch.nativeWebSocketSafe }));
|
|
18677
|
+
editorCompletionServeTracks.set(proc, trackOpencodeServe(proc, { kind: 'completion', id: 'editor-complete', port, token, nativeWebSocketSafe: launch.nativeWebSocketSafe, nativeServe: launch.nativeServe }));
|
|
18548
18678
|
const outLog = createServeLogRedactor(line => console.log(`[editor-complete:out] ${line.trim().slice(0, 500)}`));
|
|
18549
18679
|
const errLog = createServeLogRedactor(line => console.log(`[editor-complete:err] ${line.trim().slice(0, 500)}`));
|
|
18550
18680
|
proc.stdout?.on('data', d => outLog.push(d));
|
|
@@ -18753,7 +18883,7 @@ app.get('/api/project-files', editorOwnerGuard, async (req, res) => {
|
|
|
18753
18883
|
}
|
|
18754
18884
|
|
|
18755
18885
|
// Git info for status badges
|
|
18756
|
-
const git = getGitInfo(normalizedDir);
|
|
18886
|
+
const git = await getGitInfo(normalizedDir, { signal: gitRequestSignal(res) });
|
|
18757
18887
|
|
|
18758
18888
|
if (recursive || (search && search.length > 2)) {
|
|
18759
18889
|
// Recursive search mode
|
|
@@ -19171,8 +19301,9 @@ app.put('/api/morelogin/config', (req, res) => {
|
|
|
19171
19301
|
app.post('/api/morelogin/test', async (req, res) => {
|
|
19172
19302
|
try {
|
|
19173
19303
|
const status = await moreLogin.getMoreLoginStatus(_moreLoginOpts());
|
|
19174
|
-
if (!status.installed) return res.json({ ok: false, error:
|
|
19304
|
+
if (!status.installed) return res.json({ ok: false, error: `MoreLogin desktop app was not found and nothing answers on port ${status.port}.` });
|
|
19175
19305
|
if (!status.running) return res.json({ ok: false, error: `MoreLogin API not reachable on port ${status.port}. Open MoreLogin and enable its local API (Settings → API & MCP).` });
|
|
19306
|
+
if (status.apiError) return res.json({ ok: false, error: status.apiError });
|
|
19176
19307
|
res.json({ ok: true, port: status.port, profileCount: status.profileCount });
|
|
19177
19308
|
} catch (err) {
|
|
19178
19309
|
res.json({ ok: false, error: err.message });
|
|
@@ -19268,6 +19399,25 @@ function _normalizeMcpEntry(input) {
|
|
|
19268
19399
|
return { command, args, env, url, type };
|
|
19269
19400
|
}
|
|
19270
19401
|
|
|
19402
|
+
// ── Antigravity's MCP configuration (~/.gemini/config/mcp_config.json) ──
|
|
19403
|
+
// The one file `agy mcp add` writes and both the Antigravity CLI and app read: { mcpServers: { name: { command, args,
|
|
19404
|
+
// env, disabled } | { serverUrl, headers, disabled } } }. Antigravity creates it empty; an empty file reads as {}.
|
|
19405
|
+
function antigravityConfigDir() { return join(getHomePath(), '.gemini', 'config'); }
|
|
19406
|
+
function antigravityMcpConfigPath() { return join(antigravityConfigDir(), 'mcp_config.json'); }
|
|
19407
|
+
/** The parsed file ({} when missing or empty); throws on a file that is not a JSON object, so nothing overwrites it. */
|
|
19408
|
+
function readAntigravityMcpConfig() {
|
|
19409
|
+
const path = antigravityMcpConfigPath();
|
|
19410
|
+
if (!existsSync(path)) return {};
|
|
19411
|
+
return parseAntigravityMcpConfig(readFileSync(path, 'utf8'));
|
|
19412
|
+
}
|
|
19413
|
+
function writeAntigravityMcpConfig(data) {
|
|
19414
|
+
const path = antigravityMcpConfigPath();
|
|
19415
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
19416
|
+
const tmp = `${path}.synabun-${process.pid}.tmp`;
|
|
19417
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 });
|
|
19418
|
+
renameSync(tmp, path);
|
|
19419
|
+
}
|
|
19420
|
+
|
|
19271
19421
|
function _writeMoreLoginMcpToAgents(entry) {
|
|
19272
19422
|
const targets = [];
|
|
19273
19423
|
const home = getHomePath();
|
|
@@ -19283,18 +19433,16 @@ function _writeMoreLoginMcpToAgents(entry) {
|
|
|
19283
19433
|
targets.push('Claude Code');
|
|
19284
19434
|
}
|
|
19285
19435
|
} catch (e) { console.warn('[morelogin] Claude register failed:', e.message); }
|
|
19286
|
-
try { //
|
|
19287
|
-
|
|
19288
|
-
|
|
19289
|
-
|
|
19290
|
-
|
|
19291
|
-
|
|
19292
|
-
|
|
19293
|
-
|
|
19294
|
-
writeFileSync(p, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
19295
|
-
targets.push('Gemini');
|
|
19436
|
+
try { // Antigravity (~/.gemini/config/mcp_config.json, the CLI's and the app's): only where Antigravity has run
|
|
19437
|
+
if (existsSync(antigravityConfigDir())) {
|
|
19438
|
+
const data = readAntigravityMcpConfig();
|
|
19439
|
+
data.mcpServers = { ...(data.mcpServers || {}), [MORELOGIN_MCP_KEY]: buildAntigravityMcpEntry(entry.url
|
|
19440
|
+
? { type: entry.type || 'http', url: entry.url }
|
|
19441
|
+
: { command: entry.command, args: entry.args || [], env: entry.env }) };
|
|
19442
|
+
writeAntigravityMcpConfig(data);
|
|
19443
|
+
targets.push('Antigravity');
|
|
19296
19444
|
}
|
|
19297
|
-
} catch (e) { console.warn('[morelogin]
|
|
19445
|
+
} catch (e) { console.warn('[morelogin] Antigravity register failed:', e.message); }
|
|
19298
19446
|
try { // Codex (~/.codex/config.toml) — stdio only
|
|
19299
19447
|
const p = join(home, '.codex', 'config.toml');
|
|
19300
19448
|
if (existsSync(p) && entry.command) {
|
|
@@ -19332,13 +19480,11 @@ function _removeMoreLoginMcpFromAgents() {
|
|
|
19332
19480
|
}
|
|
19333
19481
|
} catch {}
|
|
19334
19482
|
try {
|
|
19335
|
-
|
|
19336
|
-
|
|
19337
|
-
const data = JSON.parse(readFileSync(p, 'utf8') || '{}');
|
|
19483
|
+
if (existsSync(antigravityMcpConfigPath())) {
|
|
19484
|
+
const data = readAntigravityMcpConfig();
|
|
19338
19485
|
if (data.mcpServers && data.mcpServers[MORELOGIN_MCP_KEY]) {
|
|
19339
|
-
|
|
19340
|
-
|
|
19341
|
-
targets.push('Gemini');
|
|
19486
|
+
writeAntigravityMcpConfig(removeAntigravityMcpServer(data, MORELOGIN_MCP_KEY));
|
|
19487
|
+
targets.push('Antigravity');
|
|
19342
19488
|
}
|
|
19343
19489
|
}
|
|
19344
19490
|
} catch {}
|
|
@@ -21164,10 +21310,106 @@ app.get('/api/opencode/sessions', (req, res) => {
|
|
|
21164
21310
|
}
|
|
21165
21311
|
});
|
|
21166
21312
|
|
|
21167
|
-
// GET /api/
|
|
21168
|
-
|
|
21169
|
-
|
|
21170
|
-
|
|
21313
|
+
// GET /api/antigravity/sessions — Antigravity's own conversations (ACP session/list on a short-lived `agy acp`),
|
|
21314
|
+
// grouped under the registered projects. A missing or signed-out CLI lists nothing and says why.
|
|
21315
|
+
let _antigravitySessionsCache = null; // { at, value }
|
|
21316
|
+
app.get('/api/antigravity/sessions', async (req, res) => {
|
|
21317
|
+
try {
|
|
21318
|
+
const projects = loadHookProjects();
|
|
21319
|
+
const limit = Math.min(parseInt(req.query.limit) || 30, 100);
|
|
21320
|
+
const offset = parseInt(req.query.offset) || 0;
|
|
21321
|
+
const search = (req.query.search || '').toString();
|
|
21322
|
+
const fresh = _antigravitySessionsCache && Date.now() - _antigravitySessionsCache.at < 15_000 && req.query.refresh !== '1';
|
|
21323
|
+
const listed = fresh ? _antigravitySessionsCache.value : await listAntigravitySessions({ launch: antigravityLaunch(), env: antigravityChildEnv() });
|
|
21324
|
+
if (!fresh && !listed.error) _antigravitySessionsCache = { at: Date.now(), value: listed };
|
|
21325
|
+
res.json({
|
|
21326
|
+
projects: groupSessionsByProject(listed.sessions, projects, { targetProject: req.query.project || null, search, offset, limit }),
|
|
21327
|
+
...(listed.error ? { error: listed.error } : {}),
|
|
21328
|
+
...(listed.authRequired ? { authRequired: true } : {}),
|
|
21329
|
+
...(listed.cliMissing ? { cliMissing: true } : {}),
|
|
21330
|
+
});
|
|
21331
|
+
} catch (err) {
|
|
21332
|
+
res.status(500).json({ error: err.message });
|
|
21333
|
+
}
|
|
21334
|
+
});
|
|
21335
|
+
|
|
21336
|
+
// GET /api/antigravity/models — what an ACP session offers SynaBun (lib/antigravity/catalog.js: a short-lived `agy acp`
|
|
21337
|
+
// in a throwaway home; `agy models` only when ACP fails), kept for a minute.
|
|
21338
|
+
// { models: [{ id, label, efforts?, default? }], source: 'live' | 'stale' | 'unavailable', via, signedIn?, tier?, error? }
|
|
21339
|
+
let _antigravityModelsCache = null; // { at, value }
|
|
21340
|
+
app.get('/api/antigravity/models', async (req, res) => {
|
|
21341
|
+
try {
|
|
21342
|
+
const age = _antigravityModelsCache ? Date.now() - _antigravityModelsCache.at : Infinity;
|
|
21343
|
+
if (age < 60_000 && req.query.refresh !== '1') return res.json(_antigravityModelsCache.value);
|
|
21344
|
+
const value = await listAntigravityModels({ launch: antigravityLaunch(), env: antigravityChildEnv() });
|
|
21345
|
+
if (value.source === 'live') {
|
|
21346
|
+
// A different list than the one the Assistant's catalog last read: its rows are rebuilt (as for Claude's).
|
|
21347
|
+
const ids = (rows) => (rows || []).map((row) => `${row.id}${row.default ? '*' : ''}`).join('\n');
|
|
21348
|
+
const changed = _antigravityModelsCache && ids(_antigravityModelsCache.value.models) !== ids(value.models);
|
|
21349
|
+
_antigravityModelsCache = { at: Date.now(), value };
|
|
21350
|
+
if (changed) _assistantCatalog?.refresh?.().catch(() => {});
|
|
21351
|
+
}
|
|
21352
|
+
else if (_antigravityModelsCache?.value?.models?.length) return res.json({ ..._antigravityModelsCache.value, source: 'stale', error: value.error });
|
|
21353
|
+
res.json(value);
|
|
21354
|
+
} catch (err) {
|
|
21355
|
+
res.status(500).json({ models: [], source: 'unavailable', error: err.message });
|
|
21356
|
+
}
|
|
21357
|
+
});
|
|
21358
|
+
|
|
21359
|
+
// POST /api/antigravity/policy — the question the PreToolUse hook of a SynaBun Antigravity home asks before every tool
|
|
21360
|
+
// call of a brain or run SynaBun started (lib/antigravity/policy-hook.mjs → lib/antigravity/policy.js). The home's
|
|
21361
|
+
// token names who registered the checker; an unknown token is refused. Loopback socket only (requireLocal), and only
|
|
21362
|
+
// { token, payload }: the answer is { decision: 'deny' | 'ask' | 'allow', reason? }.
|
|
21363
|
+
app.post('/api/antigravity/policy', requireLoopbackSocket, express.json({ limit: '4mb' }), async (req, res) => {
|
|
21364
|
+
const body = req.body && typeof req.body === 'object' && !Array.isArray(req.body) ? req.body : {};
|
|
21365
|
+
try {
|
|
21366
|
+
res.json(await defaultAntigravityPolicy.decide(body.token, body.payload && typeof body.payload === 'object' ? body.payload : {}));
|
|
21367
|
+
} catch (err) {
|
|
21368
|
+
res.json({ decision: 'deny', reason: `SynaBun could not check this call (${err?.message || err}).` });
|
|
21369
|
+
}
|
|
21370
|
+
});
|
|
21371
|
+
/** Where the hook of a SynaBun Antigravity home reaches this server (loopback, the port this server listens on). */
|
|
21372
|
+
function antigravityPolicyUrl() { return `http://127.0.0.1:${PORT}/api/antigravity/policy`; }
|
|
21373
|
+
/** The home of an Antigravity brain or run (lib/antigravity/home.js), under the data home's runtime folder. */
|
|
21374
|
+
function antigravityHomeFor(kind, id) { return antigravityHomeDir(antigravityHomesRoot(DATA_HOME), kind, id); }
|
|
21375
|
+
|
|
21376
|
+
// Antigravity's conversations as SynaBun keeps them (lib/antigravity/transcripts.js, read only): the user's own go
|
|
21377
|
+
// into Resume's index (session_cache + session_fts, provider antigravity), every one, SynaBun's brain and run homes
|
|
21378
|
+
// included, into memory capture (lib/memory-session-capture.js). A pass at start, one after a panel turn ends (that
|
|
21379
|
+
// conversation only, a moment later so agy has written its last step), one every five minutes, and a fresh one
|
|
21380
|
+
// before Resume or Skills Studio searches them (at most every 15 s).
|
|
21381
|
+
function antigravitySessionStores({ userOnly = false } = {}) {
|
|
21382
|
+
return antigravityStores({ homesRoot: userOnly ? null : antigravityHomesRoot(DATA_HOME) });
|
|
21383
|
+
}
|
|
21384
|
+
let _antigravityCacheAt = 0;
|
|
21385
|
+
function refreshAntigravityCatalog({ force = false } = {}) {
|
|
21386
|
+
if (!force && Date.now() - _antigravityCacheAt < 15_000) return null;
|
|
21387
|
+
_antigravityCacheAt = Date.now();
|
|
21388
|
+
try { return rebuildAntigravityCache({ stores: antigravitySessionStores() }); }
|
|
21389
|
+
catch (err) { console.warn(' Session cache (antigravity) error:', err.message); return null; }
|
|
21390
|
+
}
|
|
21391
|
+
const _antigravityRefreshTimers = new Map();
|
|
21392
|
+
function refreshAntigravitySession(sessionId) {
|
|
21393
|
+
if (typeof sessionId !== 'string' || !sessionId) return;
|
|
21394
|
+
clearTimeout(_antigravityRefreshTimers.get(sessionId));
|
|
21395
|
+
_antigravityRefreshTimers.set(sessionId, setTimeout(() => {
|
|
21396
|
+
_antigravityRefreshTimers.delete(sessionId);
|
|
21397
|
+
try { rebuildAntigravityCache({ stores: antigravitySessionStores({ userOnly: true }), only: sessionId }); }
|
|
21398
|
+
catch (err) { console.warn(' Session cache (antigravity) error:', err.message); }
|
|
21399
|
+
}, 1500));
|
|
21400
|
+
_antigravityRefreshTimers.get(sessionId).unref?.();
|
|
21401
|
+
}
|
|
21402
|
+
/** SynaBun's own MCP entry for an Antigravity home: the registration the Setup card writes (stdio, this install). */
|
|
21403
|
+
function antigravitySynabunMcpEntry() {
|
|
21404
|
+
const { envPath, mcpCommand, mcpArgs } = getMcpPaths();
|
|
21405
|
+
return { command: mcpCommand, args: mcpArgs, env: { DOTENV_PATH: envPath, SYNABUN_DATA_HOME: DATA_HOME, MEMORY_DATA_DIR: resolve(DATA_HOME, 'mcp-data') } };
|
|
21406
|
+
}
|
|
21407
|
+
|
|
21408
|
+
// GET /api/antigravity/panel — what the side panel's socket offers on this server (lib/antigravity/panel-bridge.js).
|
|
21409
|
+
// Admin-only like the socket itself, so the page tells a refused socket (403 here too: a guest, the automation
|
|
21410
|
+
// browser) from a server that predates it (404: it needs a restart) instead of reconnecting forever.
|
|
21411
|
+
app.get('/api/antigravity/panel', (req, res) => {
|
|
21412
|
+
res.json({ ok: true, socket: ANTIGRAVITY_WS_PATH, capabilities: [...ANTIGRAVITY_PANEL_CAPABILITIES], live: antigravityPanel.liveCount() });
|
|
21171
21413
|
});
|
|
21172
21414
|
|
|
21173
21415
|
// ── Hybrid session content search (FTS5 + semantic re-rank for Claude) ──
|
|
@@ -21204,6 +21446,9 @@ app.get('/api/session-search', async (req, res) => {
|
|
|
21204
21446
|
codexCatalogById = new Map(codexCatalog.map((session) => [session.sessionId, session]));
|
|
21205
21447
|
}
|
|
21206
21448
|
|
|
21449
|
+
// Antigravity's index follows its transcripts (refreshAntigravityCatalog: at most every 15 s).
|
|
21450
|
+
if (provider === 'antigravity') refreshAntigravityCatalog();
|
|
21451
|
+
|
|
21207
21452
|
// 1. FTS5 substring hits
|
|
21208
21453
|
const ftsRows = searchSessionFts(q, { provider, project: projectFilter, limit: limit * 2 });
|
|
21209
21454
|
const ftsMap = new Map();
|
|
@@ -21468,13 +21713,21 @@ app.post('/api/claude-code/sessions/:sessionId/fork', express.json(), claudeSess
|
|
|
21468
21713
|
app.delete('/api/claude-code/sessions/:sessionId', express.json(), claudeSessionRoute((req) => {
|
|
21469
21714
|
// Irreversible: the panel asks first, and says so here.
|
|
21470
21715
|
if (req.body?.confirm !== true) { const e = new Error('Deleting a session needs confirm: true'); e.status = 400; throw e; }
|
|
21471
|
-
|
|
21716
|
+
// The transcript cache keeps parsed rows by path: a deleted session's go with it (also when the delete fails half way).
|
|
21717
|
+
return Promise.resolve()
|
|
21718
|
+
.then(() => claudeSessionOps.remove(req.params.sessionId, req.body?.project))
|
|
21719
|
+
.finally(() => { try { claudeHistoryCache.forgetSession(req.params.sessionId); } catch {} });
|
|
21472
21720
|
}));
|
|
21473
21721
|
app.get('/api/claude-code/sessions/:sessionId/subagents', claudeSessionRoute(req => claudeSessionOps.subagents(req.params.sessionId, req.query.project)));
|
|
21474
21722
|
app.get('/api/claude-code/sessions/:sessionId/subagents/:agentId/messages', claudeSessionRoute(req => claudeSessionOps.subagentMessages(req.params.sessionId, req.params.agentId, req.query.project)));
|
|
21475
21723
|
|
|
21476
21724
|
// --- Session Messages (read JSONL for panel history) ---
|
|
21477
21725
|
|
|
21726
|
+
// The parsed transcripts of the last few sessions opened (lib/claude-history-cache.js).
|
|
21727
|
+
const claudeHistoryCache = createHistoryCache();
|
|
21728
|
+
// The Codex rollouts read last, parsed once for the early preview and the history fallback (lib/codex-rollout-reader.js).
|
|
21729
|
+
const codexRolloutReader = createRolloutReader();
|
|
21730
|
+
|
|
21478
21731
|
app.get('/api/claude-code/sessions/:sessionId/messages', async (req, res) => {
|
|
21479
21732
|
try {
|
|
21480
21733
|
const { sessionId } = req.params;
|
|
@@ -21514,22 +21767,17 @@ app.get('/api/claude-code/sessions/:sessionId/messages', async (req, res) => {
|
|
|
21514
21767
|
return res.status(missing.status).json(missing.body);
|
|
21515
21768
|
}
|
|
21516
21769
|
|
|
21517
|
-
//
|
|
21518
|
-
//
|
|
21519
|
-
//
|
|
21520
|
-
//
|
|
21521
|
-
//
|
|
21522
|
-
const rl = jsonlLines(openConfinedStream(found.realFolder, found.realPath, { encoding: 'utf-8' }));
|
|
21770
|
+
// Session JSONLs reach tens of MB and one session open asks this route several times (the panel's
|
|
21771
|
+
// snapshot probe, the rebuild, "load earlier", a clipped result): the collector of a transcript is
|
|
21772
|
+
// kept, and a file that grew is parsed only from where the last read stopped (lib/claude-history-cache.js:
|
|
21773
|
+
// whole lines split on "\n" only, read from the canonical path that was checked, opened without following
|
|
21774
|
+
// a link).
|
|
21523
21775
|
// Rows per transcript line: prompts (with their uuid), assistant text, thinking
|
|
21524
21776
|
// and tool calls, tool results with their typed output (they live inside user
|
|
21525
21777
|
// lines), compaction markers. The collector also follows the parentUuid chain,
|
|
21526
21778
|
// so the branches a conversation rewind abandoned are left out: what is shown
|
|
21527
21779
|
// is what the resumed model has. See lib/claude-history.js.
|
|
21528
|
-
const collector =
|
|
21529
|
-
for await (const line of rl) {
|
|
21530
|
-
if (!line.trim()) continue;
|
|
21531
|
-
try { collector.add(JSON.parse(line)); } catch {}
|
|
21532
|
-
}
|
|
21780
|
+
const collector = await claudeHistoryCache.collectorFor(found);
|
|
21533
21781
|
|
|
21534
21782
|
// `?tool=<tool_use_id>`: just that call's result (a restored snapshot holds
|
|
21535
21783
|
// only the clipped text of a long result and asks for the rest).
|
|
@@ -21747,7 +21995,7 @@ app.get('/api/claude-code/integrations', (req, res) => {
|
|
|
21747
21995
|
|
|
21748
21996
|
// POST /api/claude-code/integrations — enable hook for a target
|
|
21749
21997
|
// Optional `hook` param: 'SessionStart' | 'UserPromptSubmit' (omit for both)
|
|
21750
|
-
app.post('/api/claude-code/integrations', (req, res) => {
|
|
21998
|
+
app.post('/api/claude-code/integrations', async (req, res) => {
|
|
21751
21999
|
try {
|
|
21752
22000
|
const { target, projectPath, label, hook } = req.body;
|
|
21753
22001
|
// target: 'global' | 'project'
|
|
@@ -21817,7 +22065,7 @@ app.post('/api/claude-code/integrations', (req, res) => {
|
|
|
21817
22065
|
let dubiousOwnership = false;
|
|
21818
22066
|
let trustPath = null;
|
|
21819
22067
|
try {
|
|
21820
|
-
if (detectDubiousOwnership(normalized)) {
|
|
22068
|
+
if (await detectDubiousOwnership(normalized)) {
|
|
21821
22069
|
dubiousOwnership = true;
|
|
21822
22070
|
trustPath = normalizeGitPath(normalized);
|
|
21823
22071
|
}
|
|
@@ -22252,7 +22500,7 @@ app.delete('/api/claude-code/mcp', (req, res) => {
|
|
|
22252
22500
|
});
|
|
22253
22501
|
|
|
22254
22502
|
// ═══════════════════════════════════════════
|
|
22255
|
-
// Multi-Provider MCP Setup (
|
|
22503
|
+
// Multi-Provider MCP Setup (Antigravity, Codex)
|
|
22256
22504
|
// ═══════════════════════════════════════════
|
|
22257
22505
|
|
|
22258
22506
|
// ── Minimal TOML helpers (for Codex config.toml) ──
|
|
@@ -22411,51 +22659,43 @@ function buildCodexMcpCliCommand() {
|
|
|
22411
22659
|
return `codex mcp add SynaBun ${envArgs} -- node ${JSON.stringify(definition.args[0])}`;
|
|
22412
22660
|
}
|
|
22413
22661
|
|
|
22414
|
-
// ──
|
|
22662
|
+
// ── Antigravity MCP endpoints ──
|
|
22663
|
+
// ~/.gemini/config/mcp_config.json (lib/mcp-client-config.js): the file the Antigravity CLI and app share. A file that
|
|
22664
|
+
// is not a JSON object is never overwritten: the request fails and says so.
|
|
22415
22665
|
|
|
22416
|
-
app.get('/api/setup/
|
|
22666
|
+
app.get('/api/setup/antigravity/mcp', (req, res) => {
|
|
22417
22667
|
try {
|
|
22418
|
-
const
|
|
22419
|
-
if (!existsSync(
|
|
22420
|
-
const data =
|
|
22421
|
-
const
|
|
22422
|
-
res.json({ ok: true, connected });
|
|
22668
|
+
const path = antigravityMcpConfigPath();
|
|
22669
|
+
if (!existsSync(path)) return res.json({ ok: true, connected: false, path });
|
|
22670
|
+
const data = readAntigravityMcpConfig();
|
|
22671
|
+
const entry = data.mcpServers?.SynaBun;
|
|
22672
|
+
res.json({ ok: true, connected: !!entry && entry.disabled !== true, disabled: entry?.disabled === true, path });
|
|
22423
22673
|
} catch (err) {
|
|
22424
22674
|
res.status(500).json({ error: err.message });
|
|
22425
22675
|
}
|
|
22426
22676
|
});
|
|
22427
22677
|
|
|
22428
|
-
app.post('/api/setup/
|
|
22678
|
+
app.post('/api/setup/antigravity/mcp', (req, res) => {
|
|
22429
22679
|
try {
|
|
22430
|
-
const dir = join(getHomePath(), '.gemini');
|
|
22431
|
-
const settingsPath = join(dir, 'settings.json');
|
|
22432
22680
|
const { envPath, mcpCommand, mcpArgs } = getMcpPaths();
|
|
22433
|
-
|
|
22434
|
-
let data = {};
|
|
22435
|
-
try { data = JSON.parse(readFileSync(settingsPath, 'utf-8')); } catch {}
|
|
22436
|
-
data = mergeGeminiMcpConfig(data, {
|
|
22681
|
+
const data = mergeAntigravityMcpConfig(readAntigravityMcpConfig(), {
|
|
22437
22682
|
command: mcpCommand,
|
|
22438
22683
|
args: mcpArgs,
|
|
22439
22684
|
env: { DOTENV_PATH: envPath, SYNABUN_DATA_HOME: DATA_HOME, MEMORY_DATA_DIR: resolve(DATA_HOME, 'mcp-data') },
|
|
22440
22685
|
});
|
|
22441
|
-
|
|
22442
|
-
res.json({ ok: true, message: 'SynaBun MCP registered in
|
|
22686
|
+
writeAntigravityMcpConfig(data);
|
|
22687
|
+
res.json({ ok: true, message: 'SynaBun MCP registered in Antigravity. New Antigravity sessions connect to it.', rules: syncRulesWithMcp('antigravity', 'install', req.body) });
|
|
22443
22688
|
} catch (err) {
|
|
22444
22689
|
res.status(500).json({ error: err.message });
|
|
22445
22690
|
}
|
|
22446
22691
|
});
|
|
22447
22692
|
|
|
22448
|
-
app.delete('/api/setup/
|
|
22693
|
+
app.delete('/api/setup/antigravity/mcp', (req, res) => {
|
|
22449
22694
|
try {
|
|
22450
|
-
|
|
22451
|
-
|
|
22452
|
-
|
|
22453
|
-
|
|
22454
|
-
delete data.mcpServers.SynaBun;
|
|
22455
|
-
if (Object.keys(data.mcpServers).length === 0) delete data.mcpServers;
|
|
22456
|
-
writeFileSync(settingsPath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
22457
|
-
}
|
|
22458
|
-
res.json({ ok: true, message: 'SynaBun MCP removed from Gemini CLI.', rules: syncRulesWithMcp('gemini', 'remove') });
|
|
22695
|
+
if (!existsSync(antigravityMcpConfigPath())) return res.json({ ok: true, rules: syncRulesWithMcp('antigravity', 'remove') });
|
|
22696
|
+
const data = readAntigravityMcpConfig();
|
|
22697
|
+
if (data.mcpServers && data.mcpServers.SynaBun) writeAntigravityMcpConfig(removeAntigravityMcpServer(data));
|
|
22698
|
+
res.json({ ok: true, message: 'SynaBun MCP removed from Antigravity.', rules: syncRulesWithMcp('antigravity', 'remove') });
|
|
22459
22699
|
} catch (err) {
|
|
22460
22700
|
res.status(500).json({ error: err.message });
|
|
22461
22701
|
}
|
|
@@ -22516,11 +22756,11 @@ app.get('/api/setup/status', (req, res) => {
|
|
|
22516
22756
|
} catch {}
|
|
22517
22757
|
claude.cliCommand = claudeMcpAddCommand(mcpPaths);
|
|
22518
22758
|
|
|
22519
|
-
//
|
|
22520
|
-
let
|
|
22759
|
+
// Antigravity (~/.gemini/config/mcp_config.json)
|
|
22760
|
+
let antigravity = { connected: false };
|
|
22521
22761
|
try {
|
|
22522
|
-
const
|
|
22523
|
-
|
|
22762
|
+
const entry = readAntigravityMcpConfig().mcpServers?.SynaBun;
|
|
22763
|
+
antigravity.connected = !!entry && entry.disabled !== true;
|
|
22524
22764
|
} catch {}
|
|
22525
22765
|
|
|
22526
22766
|
// Codex
|
|
@@ -22540,7 +22780,7 @@ app.get('/api/setup/status', (req, res) => {
|
|
|
22540
22780
|
let rules = null;
|
|
22541
22781
|
try { rules = rulesetInstaller.status(); } catch (err) { rules = { ok: false, error: err.message }; }
|
|
22542
22782
|
|
|
22543
|
-
res.json({ ok: true, claude,
|
|
22783
|
+
res.json({ ok: true, claude, antigravity, codex, opencode, rules, paths: { mcpIndexPath, envPath, mcpCommand, mcpArgs } });
|
|
22544
22784
|
} catch (err) {
|
|
22545
22785
|
res.status(500).json({ error: err.message });
|
|
22546
22786
|
}
|
|
@@ -22640,7 +22880,7 @@ skillsService = createSkillsService({
|
|
|
22640
22880
|
// its skill hints); invite guests do not (broadcastSync keeps `skills:` to the owner).
|
|
22641
22881
|
broadcast: (event) => broadcastSync({ type: 'skills:changed', event }),
|
|
22642
22882
|
// Which assistants are installed here (the CLI resolver), for the default of "My assistants" (Settings → Skills).
|
|
22643
|
-
installedRuntimes: () => ({ claude: cliFoundForWarmup('claude-code'), codex: cliFoundForWarmup('codex'), opencode: cliFoundForWarmup('opencode') }),
|
|
22883
|
+
installedRuntimes: () => ({ claude: cliFoundForWarmup('claude-code'), codex: cliFoundForWarmup('codex'), opencode: cliFoundForWarmup('opencode'), antigravity: cliFoundForWarmup('antigravity') }),
|
|
22644
22884
|
runtimeChecks: skillsRuntimeChecks,
|
|
22645
22885
|
operationsOptions: { notify: (item) => {
|
|
22646
22886
|
const session = _assistantRuntime?.listSessions?.().find((s) => s.status !== 'ended');
|
|
@@ -22848,9 +23088,11 @@ function skillBrowserAvailability() {
|
|
|
22848
23088
|
const unreadable = browserConfigUnreadable(cfg);
|
|
22849
23089
|
if (unreadable) return { available: false, reason: `The browser settings cannot be read (${unreadable}): fix them in Settings → Browser.` };
|
|
22850
23090
|
if (cfg.connectMode === 'morelogin') {
|
|
22851
|
-
|
|
22852
|
-
|
|
22853
|
-
|
|
23091
|
+
// The exe in a known folder, or the local API answered lately (an install in a custom folder). This is read
|
|
23092
|
+
// synchronously, so a miss refreshes the answer in the background for the next call.
|
|
23093
|
+
const installed = moreLogin.isMoreLoginPresent();
|
|
23094
|
+
if (!installed) moreLogin.probeMoreLoginApi({ savedCfg: cfg, timeout: 1500 }).catch(() => {});
|
|
23095
|
+
return installed ? { available: true, reason: null } : { available: false, reason: 'MoreLogin is the default browser (Settings → Browser) and it was not found: its app is in no known folder and its local API does not answer.' };
|
|
22854
23096
|
}
|
|
22855
23097
|
if ([...browserSessions.values()].some((s) => s?.context)) return { available: true, reason: null };
|
|
22856
23098
|
if (findBrowserExecutable(cfg.channel)) return { available: true, reason: null };
|
|
@@ -22941,6 +23183,19 @@ async function skillSourceSessions({ provider, project = null, q = '', limit = 4
|
|
|
22941
23183
|
out.push({ provider, sessionId: row.id, title, project: proj.path, projectLabel: proj.label || basename(proj.path), modified: row.time_updated ? new Date(row.time_updated).toISOString() : null, messages: Number(row.message_count) || 0 });
|
|
22942
23184
|
}
|
|
22943
23185
|
}
|
|
23186
|
+
} else if (provider === 'antigravity') {
|
|
23187
|
+
// The user's own Antigravity conversations (the panel's and a terminal agy's), as Resume's index keeps them,
|
|
23188
|
+
// titled as Antigravity titles them (refreshAntigravityCatalog).
|
|
23189
|
+
refreshAntigravityCatalog();
|
|
23190
|
+
for (const entry of listSessionCache('antigravity', { limit: 100000 })) {
|
|
23191
|
+
if (entry.deleted || !entry.project_path) continue;
|
|
23192
|
+
const dir = resolve(entry.project_path);
|
|
23193
|
+
const proj = projects.find((p) => isInside(resolve(p.path), dir, { allowRoot: true }));
|
|
23194
|
+
if (!proj) continue;
|
|
23195
|
+
const title = String(entry.first_prompt || '');
|
|
23196
|
+
if (search && !title.toLowerCase().includes(search) && !String(entry.session_id).toLowerCase().includes(search)) continue;
|
|
23197
|
+
out.push({ provider, sessionId: entry.session_id, title, project: proj.path, projectLabel: proj.label || basename(proj.path), modified: entry.modified || null, messages: Number(entry.message_count) || 0 });
|
|
23198
|
+
}
|
|
22944
23199
|
}
|
|
22945
23200
|
return out.sort((a, b) => String(b.modified || '').localeCompare(String(a.modified || ''))).slice(0, Math.max(1, Math.min(100, Number(limit) || 40)));
|
|
22946
23201
|
}
|
|
@@ -22948,7 +23203,8 @@ async function skillSourceSessions({ provider, project = null, q = '', limit = 4
|
|
|
22948
23203
|
/**
|
|
22949
23204
|
* One session transcript as messages, read the way each runtime keeps it: Claude Code's transcript inside a
|
|
22950
23205
|
* registered project's folder (lib/path-confine.js findSessionTranscript), Codex's rollout inside its account's home,
|
|
22951
|
-
* OpenCode's rows of its database
|
|
23206
|
+
* OpenCode's rows of its database, Antigravity's transcript in the user's own ~/.gemini. Bounded: the first messages
|
|
23207
|
+
* and the last ones of a very long session.
|
|
22952
23208
|
*/
|
|
22953
23209
|
async function skillSourceConversation({ provider, sessionId, project = null }) {
|
|
22954
23210
|
const keep = (list, m) => { list.push(m); if (list.length > 2400) list.splice(300, list.length - 2100); };
|
|
@@ -22991,6 +23247,16 @@ async function skillSourceConversation({ provider, sessionId, project = null })
|
|
|
22991
23247
|
const rows = db.prepare(`SELECT type, data FROM session_message WHERE session_id = ? AND type IN ('user', 'assistant') ORDER BY seq ASC LIMIT 5000`).all(sessionId);
|
|
22992
23248
|
return { title: session.title || '', project, messages: opencodeRowsToMessages(rows) };
|
|
22993
23249
|
}
|
|
23250
|
+
if (provider === 'antigravity') {
|
|
23251
|
+
// The transcript in the user's own Antigravity folder (~/.gemini/antigravity-cli/brain/<id>/…), read as
|
|
23252
|
+
// lib/antigravity/transcripts.js reads it: no link followed, bounded; never the model's reasoning.
|
|
23253
|
+
const found = findUserConversation(sessionId);
|
|
23254
|
+
if (!found) throw Object.assign(new Error('That Antigravity conversation was not found.'), { status: 404, code: 'source_not_found' });
|
|
23255
|
+
const parsed = parseAntigravityTranscript(readTranscriptText(found.transcript));
|
|
23256
|
+
const messages = [];
|
|
23257
|
+
for (const m of antigravityMessagesToMessages(parsed.messages)) keep(messages, m);
|
|
23258
|
+
return { title: found.title || parsed.firstPrompt || '', project: found.cwd || project, messages };
|
|
23259
|
+
}
|
|
22994
23260
|
throw Object.assign(new Error('Unknown provider.'), { status: 400, code: 'source_failed' });
|
|
22995
23261
|
}
|
|
22996
23262
|
|
|
@@ -23078,7 +23344,8 @@ function getOpenCodeCommandDir() {
|
|
|
23078
23344
|
return xdgDefault;
|
|
23079
23345
|
}
|
|
23080
23346
|
|
|
23081
|
-
|
|
23347
|
+
// Antigravity reads its user skills from ~/.gemini/config/skills (lib/skills/roots.js), installed like the others.
|
|
23348
|
+
const SKILL_TARGETS = /** @type {const} */ (['claude', 'codex', 'opencode', 'antigravity']);
|
|
23082
23349
|
|
|
23083
23350
|
/** Where the old prompt / command bridge of a skill is (Codex, OpenCode), or null (Claude Code never had one). */
|
|
23084
23351
|
function legacyBridgePath(target, skillName) {
|
|
@@ -23340,6 +23607,7 @@ function listAvailableSkills() {
|
|
|
23340
23607
|
claude: isSkillInstalled('claude', dirName),
|
|
23341
23608
|
codex: isSkillInstalled('codex', dirName),
|
|
23342
23609
|
opencode: isSkillInstalled('opencode', dirName),
|
|
23610
|
+
antigravity: isSkillInstalled('antigravity', dirName),
|
|
23343
23611
|
});
|
|
23344
23612
|
// What each runtime has of it: a native skill folder ('native'; 'modified' when changed since SynaBun installed
|
|
23345
23613
|
// it; 'other' when it is not SynaBun's), and an old bridge not moved yet. `installedTargets` stays the one flag
|
|
@@ -23724,6 +23992,11 @@ app.post('/api/sidepanel/kill-session', async (req, res) => {
|
|
|
23724
23992
|
killed = true;
|
|
23725
23993
|
}
|
|
23726
23994
|
}
|
|
23995
|
+
} else if (provider === 'antigravity') {
|
|
23996
|
+
// An Antigravity session lives in the `agy acp` of the panel socket that opened it, which ends with that
|
|
23997
|
+
// socket: nothing outlives a closed panel, so there is nothing to kill here. Its conversation stays in
|
|
23998
|
+
// Antigravity's own history (Resume lists it).
|
|
23999
|
+
killed = false;
|
|
23727
24000
|
} else if (provider === 'opencode') {
|
|
23728
24001
|
// OpenCode sessions live on the OpenCode SDK server, not in an orphan
|
|
23729
24002
|
// map — DELETE them via the proxy.
|
|
@@ -24942,7 +25215,7 @@ function externalToolsCatalog(cwd = null) {
|
|
|
24942
25215
|
function applyOpenCodeExternalScope(xdgRoot, config, tools, cwd, { readProject = true } = {}) {
|
|
24943
25216
|
const scoped = scopeOpenCodeServers(config, tools, externalInventory('opencode', cwd || PACKAGE_ROOT, { readProject }));
|
|
24944
25217
|
Object.assign(config, scoped.config);
|
|
24945
|
-
_externalMcpLogValues.set(xdgRoot,
|
|
25218
|
+
_externalMcpLogValues.set(xdgRoot, mcpSecretValues(scoped.config.mcp.servers));
|
|
24946
25219
|
writeFileSync(resolve(xdgRoot, 'synabun-external-scope.json'), JSON.stringify(scoped.scope), { mode: 0o600 });
|
|
24947
25220
|
return scoped.scope;
|
|
24948
25221
|
}
|
|
@@ -25276,7 +25549,7 @@ app.get('/api/mcp/platforms', (req, res) => {
|
|
|
25276
25549
|
claudeCode: existsSync(join(home, '.claude.json')) || existsSync(join(home, '.claude')),
|
|
25277
25550
|
opencode: existsSync(getOpencodeConfigDir()),
|
|
25278
25551
|
codex: existsSync(join(home, '.codex')),
|
|
25279
|
-
|
|
25552
|
+
antigravity: existsSync(antigravityConfigDir()) || existsSync(join(home, '.gemini', 'antigravity-cli')),
|
|
25280
25553
|
});
|
|
25281
25554
|
});
|
|
25282
25555
|
|
|
@@ -25393,7 +25666,7 @@ app.post('/api/mcp/install/github', async (req, res) => {
|
|
|
25393
25666
|
});
|
|
25394
25667
|
|
|
25395
25668
|
// GET /api/mcp/installed-all — unified view of every externally-installed MCP
|
|
25396
|
-
// across Claude Code / OpenCode / Codex /
|
|
25669
|
+
// across Claude Code / OpenCode / Codex / Antigravity + SynaBun's registry + Claude Code plugins.
|
|
25397
25670
|
// Excludes SynaBun's built-in tool-group servers (they're baked in, not "installed").
|
|
25398
25671
|
app.get('/api/mcp/installed-all', (req, res) => {
|
|
25399
25672
|
try {
|
|
@@ -25440,17 +25713,17 @@ app.get('/api/mcp/installed-all', (req, res) => {
|
|
|
25440
25713
|
}
|
|
25441
25714
|
} catch {}
|
|
25442
25715
|
|
|
25443
|
-
//
|
|
25716
|
+
// Antigravity (~/.gemini/config/mcp_config.json: a remote server is `serverUrl`)
|
|
25444
25717
|
try {
|
|
25445
|
-
|
|
25446
|
-
|
|
25447
|
-
const data = JSON.parse(readFileSync(p, 'utf-8'));
|
|
25718
|
+
if (existsSync(antigravityMcpConfigPath())) {
|
|
25719
|
+
const data = readAntigravityMcpConfig();
|
|
25448
25720
|
for (const [name, cfg] of Object.entries(data.mcpServers || {})) {
|
|
25449
|
-
|
|
25450
|
-
|
|
25451
|
-
|
|
25452
|
-
|
|
25453
|
-
|
|
25721
|
+
const seen = describeAntigravityMcpEntry(cfg);
|
|
25722
|
+
touch(name, 'antigravity', {
|
|
25723
|
+
transport: seen.transport,
|
|
25724
|
+
command: seen.command || null,
|
|
25725
|
+
args: seen.args?.length ? seen.args : null,
|
|
25726
|
+
url: seen.url || null,
|
|
25454
25727
|
});
|
|
25455
25728
|
}
|
|
25456
25729
|
}
|
|
@@ -25666,16 +25939,11 @@ async function internalMcpSync({ name, config, platforms }) {
|
|
|
25666
25939
|
content = tomlUpsertSection(content, `mcp_servers.${name}`, kvPairs);
|
|
25667
25940
|
writeFileSync(configPath, content, 'utf-8');
|
|
25668
25941
|
results.codex = 'ok';
|
|
25669
|
-
} else if (p === '
|
|
25670
|
-
const
|
|
25671
|
-
|
|
25672
|
-
|
|
25673
|
-
|
|
25674
|
-
try { data = JSON.parse(readFileSync(settingsPath, 'utf-8')); } catch {}
|
|
25675
|
-
if (!data.mcpServers) data.mcpServers = {};
|
|
25676
|
-
data.mcpServers[name] = buildGeminiMcpEntry(config);
|
|
25677
|
-
writeFileSync(settingsPath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
25678
|
-
results.gemini = 'ok';
|
|
25942
|
+
} else if (p === 'antigravity') {
|
|
25943
|
+
const data = readAntigravityMcpConfig();
|
|
25944
|
+
data.mcpServers = { ...(data.mcpServers || {}), [name]: buildAntigravityMcpEntry(config) };
|
|
25945
|
+
writeAntigravityMcpConfig(data);
|
|
25946
|
+
results.antigravity = 'ok';
|
|
25679
25947
|
}
|
|
25680
25948
|
} catch (err) {
|
|
25681
25949
|
results[p] = `error: ${err.message}`;
|
|
@@ -25733,17 +26001,12 @@ app.delete('/api/mcp/sync', (req, res) => {
|
|
|
25733
26001
|
}
|
|
25734
26002
|
results.codex = 'ok';
|
|
25735
26003
|
|
|
25736
|
-
} else if (p === '
|
|
25737
|
-
|
|
25738
|
-
|
|
25739
|
-
|
|
25740
|
-
if (data.mcpServers?.[name]) {
|
|
25741
|
-
delete data.mcpServers[name];
|
|
25742
|
-
if (Object.keys(data.mcpServers).length === 0) delete data.mcpServers;
|
|
25743
|
-
writeFileSync(settingsPath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
25744
|
-
}
|
|
26004
|
+
} else if (p === 'antigravity') {
|
|
26005
|
+
if (existsSync(antigravityMcpConfigPath())) {
|
|
26006
|
+
const data = readAntigravityMcpConfig();
|
|
26007
|
+
if (data.mcpServers?.[name]) writeAntigravityMcpConfig(removeAntigravityMcpServer(data, name));
|
|
25745
26008
|
}
|
|
25746
|
-
results.
|
|
26009
|
+
results.antigravity = 'ok';
|
|
25747
26010
|
}
|
|
25748
26011
|
} catch (err) {
|
|
25749
26012
|
results[p] = `error: ${err.message}`;
|
|
@@ -26160,9 +26423,11 @@ const CLI_CONFIG_PATH = resolve(DATA_HOME, 'data', 'cli-config.json');
|
|
|
26160
26423
|
const CLI_DEFAULTS = {
|
|
26161
26424
|
'claude-code': { command: 'claude' },
|
|
26162
26425
|
'codex': { command: 'codex' },
|
|
26163
|
-
'
|
|
26426
|
+
'antigravity': { command: 'agy' },
|
|
26164
26427
|
'opencode': { command: 'opencode' },
|
|
26165
26428
|
};
|
|
26429
|
+
// Profiles SynaBun no longer drives: a path saved for one (Gemini CLI, replaced by Antigravity) is never a profile.
|
|
26430
|
+
const RETIRED_CLI_PROFILES = new Set(['gemini']);
|
|
26166
26431
|
|
|
26167
26432
|
// Parsed cli-config.json keyed by mtime — every terminal spawn resolves its
|
|
26168
26433
|
// profile through here, so skip the read + parse when the file is unchanged.
|
|
@@ -26175,7 +26440,9 @@ function loadCliConfig() {
|
|
|
26175
26440
|
if (!_cliConfigCache || _cliConfigCache.mtimeMs !== mtimeMs) {
|
|
26176
26441
|
_cliConfigCache = { mtimeMs, raw: JSON.parse(readFileSync(CLI_CONFIG_PATH, 'utf-8')) };
|
|
26177
26442
|
}
|
|
26178
|
-
|
|
26443
|
+
const saved = structuredClone(_cliConfigCache.raw);
|
|
26444
|
+
for (const retired of RETIRED_CLI_PROFILES) delete saved?.[retired];
|
|
26445
|
+
return { ...CLI_DEFAULTS, ...saved };
|
|
26179
26446
|
} catch { return { ...CLI_DEFAULTS }; }
|
|
26180
26447
|
}
|
|
26181
26448
|
|
|
@@ -26196,7 +26463,7 @@ const SHELL_PROFILE = { shell: DEFAULT_SHELL, args: interactiveShellArgs(), env:
|
|
|
26196
26463
|
/**
|
|
26197
26464
|
* Build terminal profile config dynamically from cli-config.json.
|
|
26198
26465
|
* Re-reads config each call so changes take effect without restart.
|
|
26199
|
-
* @param {string} profileId - CLI profile ID (e.g. 'claude-code', 'codex', '
|
|
26466
|
+
* @param {string} profileId - CLI profile ID (e.g. 'claude-code', 'codex', 'antigravity', 'shell')
|
|
26200
26467
|
* @param {Object} [opts] - Optional overrides
|
|
26201
26468
|
* @param {string} [opts.model] - Model flag for CLIs that support it (e.g. claude --model sonnet)
|
|
26202
26469
|
*/
|
|
@@ -26219,6 +26486,8 @@ function getTerminalProfile(profileId, opts = {}) {
|
|
|
26219
26486
|
cmd = `${cmd} resume ${terminalArgument(opts.resume, DEFAULT_SHELL)}`;
|
|
26220
26487
|
} else if (opts.resume && profileId === 'opencode') {
|
|
26221
26488
|
cmd = `${cmd} -s ${terminalArgument(opts.resume, DEFAULT_SHELL)}`;
|
|
26489
|
+
} else if (opts.resume && profileId === 'antigravity') {
|
|
26490
|
+
cmd = `${cmd} --conversation ${terminalArgument(opts.resume, DEFAULT_SHELL)}`;
|
|
26222
26491
|
}
|
|
26223
26492
|
// Append model flag for CLIs that support it
|
|
26224
26493
|
else if (opts.model) {
|
|
@@ -26236,7 +26505,7 @@ function getTerminalProfile(profileId, opts = {}) {
|
|
|
26236
26505
|
const modelMap = {
|
|
26237
26506
|
'claude-code': (m) => `${cmd} --model ${m}`,
|
|
26238
26507
|
'codex': (m) => `${cmd} --model ${m}`,
|
|
26239
|
-
'
|
|
26508
|
+
'antigravity': (m) => `${cmd} --model ${m}`,
|
|
26240
26509
|
// OpenCode's terminal app has no --model (it prints its help and exits): the model is picked in it.
|
|
26241
26510
|
'opencode': () => cmd,
|
|
26242
26511
|
};
|
|
@@ -26254,6 +26523,9 @@ function getTerminalProfile(profileId, opts = {}) {
|
|
|
26254
26523
|
if (codexEffort) {
|
|
26255
26524
|
cmd = `${cmd} -c model_reasoning_effort="${codexEffort}"`;
|
|
26256
26525
|
}
|
|
26526
|
+
} else if (opts.effort && profileId === 'antigravity') {
|
|
26527
|
+
// agy --effort: low | medium | high | xhigh | max (the model's own default when the flag is left out).
|
|
26528
|
+
if (['low', 'medium', 'high', 'xhigh', 'max'].includes(opts.effort)) cmd = `${cmd} --effort ${opts.effort}`;
|
|
26257
26529
|
}
|
|
26258
26530
|
// Every Codex terminal: the Windows sandbox the Admin sandbox setting allows. A bare value
|
|
26259
26531
|
// (Codex reads one that is not TOML as a string), so no shell can strip a quote from it.
|
|
@@ -26265,7 +26537,7 @@ function getTerminalProfile(profileId, opts = {}) {
|
|
|
26265
26537
|
// The loop runs in a PTY with no human to answer prompts, so accessing
|
|
26266
26538
|
// sibling projects or running Bash/Write would stall.
|
|
26267
26539
|
// Scoped to opts.loopBypass so interactive terminal sessions still prompt.
|
|
26268
|
-
if (opts.loopBypass && profileId === 'claude-code') {
|
|
26540
|
+
if (opts.loopBypass && (profileId === 'claude-code' || profileId === 'antigravity')) {
|
|
26269
26541
|
cmd = `${cmd} --dangerously-skip-permissions`;
|
|
26270
26542
|
}
|
|
26271
26543
|
|
|
@@ -26294,7 +26566,7 @@ function getTerminalProfile(profileId, opts = {}) {
|
|
|
26294
26566
|
// provider sign-in terminals. The desktop app uses this same server/PTY host.
|
|
26295
26567
|
async function freshTerminalPath(base = process.env) {
|
|
26296
26568
|
const os = await osPath.refresh({ force: true });
|
|
26297
|
-
const launchers = ['claude-code', 'codex', 'opencode', '
|
|
26569
|
+
const launchers = ['claude-code', 'codex', 'opencode', 'antigravity'].map(tool => cliResolver.launcher(tool));
|
|
26298
26570
|
// A packaged application's runtime and npm's global bin (rememberPackagedPath above), after the user's own.
|
|
26299
26571
|
const { last: synabunDirs, extra: extraDirs } = rememberedPackagedPath();
|
|
26300
26572
|
return { ...composeTerminalPath({ env: base, os, launchers, synabunDirs, extraDirs }), os, launchers };
|
|
@@ -26861,7 +27133,7 @@ async function driveNextIteration(session, loopState, filename) {
|
|
|
26861
27133
|
return true;
|
|
26862
27134
|
}
|
|
26863
27135
|
|
|
26864
|
-
// ── Exec-based Loop Driver (Codex,
|
|
27136
|
+
// ── Exec-based Loop Driver (Codex, Antigravity, etc.) ──
|
|
26865
27137
|
// Fully isolated from Claude's hook-based driver. Runs platform-native wrapper
|
|
26866
27138
|
// files inside a shell PTY and detects completion via sentinel echo markers.
|
|
26867
27139
|
// The wrapper captures the CLI status before printing SYNABUN_ITER_DONE_<N>, so
|
|
@@ -27191,6 +27463,31 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
27191
27463
|
policyRoots: taskBrowserPolicyRoots(state),
|
|
27192
27464
|
});
|
|
27193
27465
|
},
|
|
27466
|
+
// Google Antigravity: one `agy acp` per run (lib/native-loop-providers.js createAntigravityNativeLoopAdapter), in
|
|
27467
|
+
// the run's own Antigravity home (lib/antigravity/home.js: exactly the run's MCP servers, the policy hook that
|
|
27468
|
+
// asks POST /api/antigravity/policy before every tool call, the user's rules and skills). Approvals go to the
|
|
27469
|
+
// Assistant's broker under `ask`; without an installed CLI the run fails at once with the install hint.
|
|
27470
|
+
antigravity: (state) => {
|
|
27471
|
+
const launch = antigravityLaunch();
|
|
27472
|
+
if (!launch) throw new Error('Antigravity runs need the Antigravity CLI (agy). Install it from the Antigravity side panel (one click), or run `brew install --cask antigravity-cli` (macOS) or `curl -fsSL https://antigravity.google/cli/install.sh | bash` (Windows: `irm https://antigravity.google/cli/install.ps1 | iex`), sign in by running `agy` once, then try again.');
|
|
27473
|
+
// A resumed run uses the home its first start wrote (its conversation is there).
|
|
27474
|
+
const homeDir = state.resumingWorker && typeof state.antigravityHome === 'string' && state.antigravityHome
|
|
27475
|
+
? validateRunHome(state.antigravityHome, antigravityHomesRoot(DATA_HOME))
|
|
27476
|
+
: antigravityHomeFor('run', state.runId || state.terminalSessionId || `loop-${Date.now()}`);
|
|
27477
|
+
return createAntigravityNativeLoopAdapter({
|
|
27478
|
+
...state,
|
|
27479
|
+
launch,
|
|
27480
|
+
// The PATH a new terminal has: Antigravity runs the user's own tools.
|
|
27481
|
+
extraEnv: { PATH: antigravityChildEnv().PATH, ...(state.extraEnv || {}) },
|
|
27482
|
+
serverPort: PORT,
|
|
27483
|
+
homeDir,
|
|
27484
|
+
synabunMcpEntry: antigravitySynabunMcpEntry(),
|
|
27485
|
+
policy: defaultAntigravityPolicy,
|
|
27486
|
+
policyUrl: antigravityPolicyUrl(),
|
|
27487
|
+
// Assistant task runs: where the browser policy lets its test runners run besides its own project.
|
|
27488
|
+
policyRoots: taskBrowserPolicyRoots(state),
|
|
27489
|
+
});
|
|
27490
|
+
},
|
|
27194
27491
|
opencode: async (state) => {
|
|
27195
27492
|
// An OpenCode SynaBun does not drive: the run fails with the sentence before its configuration folder, its
|
|
27196
27493
|
// sandbox profile or its serve exist (a loop or a schedule was refused already; this is a worker's, a backstop).
|
|
@@ -27239,6 +27536,7 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
27239
27536
|
state.model,
|
|
27240
27537
|
{
|
|
27241
27538
|
SYNABUN_DESKTOP_GRANT: typeof state.desktopGrantProvider === 'function' ? state.desktopGrantProvider() : null,
|
|
27539
|
+
SYNABUN_RUN_MODE: state.runMode === 'task' ? 'task' : null,
|
|
27242
27540
|
},
|
|
27243
27541
|
// Assistant task runs: the browser policy plugin (loops and schedules unchanged) with the registered code
|
|
27244
27542
|
// projects its test runners may run in (the sandboxed serve cannot read the registry), and the run's confinement
|
|
@@ -27819,6 +28117,17 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
27819
28117
|
ClaudeSession: claudeBridgeModule.ClaudeSession,
|
|
27820
28118
|
sdkVersion: claudeBridgeModule.SDK_VERSION,
|
|
27821
28119
|
handleCodexSkinWebSocket, ensureIsolatedServe, stopIsolatedServe, setupOpencodeSidepanelConfig,
|
|
28120
|
+
// An Antigravity brain (lib/assistant-brains/antigravity.js): `agy acp` in the brain's own home, its hook asking
|
|
28121
|
+
// POST /api/antigravity/policy, SynaBun's MCP entry the Setup card writes.
|
|
28122
|
+
antigravity: {
|
|
28123
|
+
launch: antigravityLaunch,
|
|
28124
|
+
// The PATH a new terminal has (agy runs the user's tools); never the owner's live data paths or a credential.
|
|
28125
|
+
childEnv: antigravityChildEnv,
|
|
28126
|
+
home: (sessionId) => antigravityHomeFor('brain', sessionId),
|
|
28127
|
+
synabunMcpEntry: antigravitySynabunMcpEntry,
|
|
28128
|
+
policy: defaultAntigravityPolicy,
|
|
28129
|
+
policyUrl: antigravityPolicyUrl,
|
|
28130
|
+
},
|
|
27822
28131
|
broadcastSync,
|
|
27823
28132
|
dataDir: LOOP_DATA_DIR,
|
|
27824
28133
|
memory: assistantMemory,
|
|
@@ -28196,7 +28505,7 @@ function findLoopFileForTerminal(terminalSessionId) {
|
|
|
28196
28505
|
}
|
|
28197
28506
|
|
|
28198
28507
|
/**
|
|
28199
|
-
* Exec-based loop driver for non-Claude CLIs (Codex,
|
|
28508
|
+
* Exec-based loop driver for non-Claude CLIs (Codex, Antigravity, etc.).
|
|
28200
28509
|
* State machine: booting → ready → running → ready → ...
|
|
28201
28510
|
*
|
|
28202
28511
|
* Completion detection: sentinel echo approach.
|
|
@@ -28972,7 +29281,7 @@ app.delete('/api/terminal/sessions/:id', (req, res) => {
|
|
|
28972
29281
|
});
|
|
28973
29282
|
|
|
28974
29283
|
app.get('/api/terminal/profiles', (req, res) => {
|
|
28975
|
-
const profileIds = ['claude-code', 'codex', '
|
|
29284
|
+
const profileIds = ['claude-code', 'codex', 'antigravity', 'opencode', 'shell'];
|
|
28976
29285
|
const profiles = profileIds.map(id => {
|
|
28977
29286
|
const p = getTerminalProfile(id);
|
|
28978
29287
|
return { id, shell: p.shell, args: p.args };
|
|
@@ -29056,30 +29365,15 @@ function resolveGitPath() {
|
|
|
29056
29365
|
}
|
|
29057
29366
|
|
|
29058
29367
|
// Cross-platform git invocation. Uses resolved absolute path when possible (spawnable
|
|
29059
|
-
// directly
|
|
29060
|
-
|
|
29061
|
-
|
|
29062
|
-
|
|
29063
|
-
const gitPath = resolveGitPath();
|
|
29064
|
-
if (gitPath) {
|
|
29065
|
-
if (IS_WINDOWS && /\.(cmd|bat)$/i.test(gitPath)) {
|
|
29066
|
-
const quoted = args.map(a => /[\s()%&|<>^"']/.test(a) ? `"${String(a).replace(/"/g, '\\"')}"` : a);
|
|
29067
|
-
return spawnSync(gitPath, quoted, { ...baseOpts, shell: true });
|
|
29068
|
-
}
|
|
29069
|
-
return spawnSync(gitPath, args, baseOpts);
|
|
29070
|
-
}
|
|
29071
|
-
if (IS_WINDOWS) {
|
|
29072
|
-
const quoted = args.map(a => /[\s()%&|<>^"']/.test(a) ? `"${String(a).replace(/"/g, '\\"')}"` : a);
|
|
29073
|
-
return spawnSync('git', quoted, { ...baseOpts, shell: true });
|
|
29074
|
-
}
|
|
29075
|
-
return spawnSync('git', args, baseOpts);
|
|
29368
|
+
// directly without a shell). The runner handles a resolved Windows .cmd/.bat shim.
|
|
29369
|
+
async function spawnGit(args, opts = {}) {
|
|
29370
|
+
const binary = await resolveGitPathInBackground();
|
|
29371
|
+
return gitStatusRunner.runRaw(args, { ...opts, binary: binary || 'git', timeoutMs: opts.timeout || 5000 });
|
|
29076
29372
|
}
|
|
29077
29373
|
|
|
29078
|
-
|
|
29079
|
-
|
|
29080
|
-
|
|
29081
|
-
const read = gitReadSync(opts.cwd, args, { env: opts.env });
|
|
29082
|
-
return spawnGit(read.args, { ...opts, env: read.env });
|
|
29374
|
+
async function spawnGitRead(args, opts = {}) {
|
|
29375
|
+
const binary = await resolveGitPathInBackground();
|
|
29376
|
+
return gitStatusRunner.run(args, { ...opts, binary: binary || 'git', timeoutMs: opts.timeout || 5000 });
|
|
29083
29377
|
}
|
|
29084
29378
|
|
|
29085
29379
|
function logGitFailure(where, err) {
|
|
@@ -29118,22 +29412,20 @@ function resolveGitPathInBackground() {
|
|
|
29118
29412
|
|
|
29119
29413
|
// Startup probe — always logs git status so Windows users can see whether git works at all. Asynchronous:
|
|
29120
29414
|
// after the lookup above, one `git --version` (no folder, no repository's configuration) off the event loop.
|
|
29121
|
-
setImmediate(() => {
|
|
29122
|
-
|
|
29123
|
-
const
|
|
29124
|
-
|
|
29125
|
-
|
|
29126
|
-
|
|
29127
|
-
|
|
29128
|
-
});
|
|
29129
|
-
})).catch(() => {});
|
|
29415
|
+
setImmediate(async () => {
|
|
29416
|
+
try {
|
|
29417
|
+
const gitPath = await resolveGitPathInBackground();
|
|
29418
|
+
const result = await gitStatusRunner.run(['--version'], { binary: gitPath || 'git', timeoutMs: 3000 });
|
|
29419
|
+
if (result.ok) console.log(`[git] ${String(result.stdout).trim()} — OK`);
|
|
29420
|
+
else console.warn(`[git] startup probe FAILED: ${String(result.stderr).trim() || result.code}. File explorer git UI will be hidden.`);
|
|
29421
|
+
} catch { /* availability is reported on the first read */ }
|
|
29130
29422
|
});
|
|
29131
29423
|
|
|
29132
29424
|
// Git "dubious ownership" detection — CVE-2022-24765 safety check refuses to operate on
|
|
29133
29425
|
// repos owned by a different OS user/SID. Common on Windows external drives or repos
|
|
29134
29426
|
// copied from another machine. Returns true only when git emits the specific error.
|
|
29135
|
-
function detectDubiousOwnership(dir) {
|
|
29136
|
-
const r = spawnGitRead(['rev-parse', '--is-inside-work-tree'], { cwd: dir, timeout: 3000 });
|
|
29427
|
+
async function detectDubiousOwnership(dir) {
|
|
29428
|
+
const r = await spawnGitRead(['rev-parse', '--is-inside-work-tree'], { cwd: dir, timeout: 3000 });
|
|
29137
29429
|
if (r.status === 0) return false;
|
|
29138
29430
|
const stderr = r.stderr?.toString?.() || '';
|
|
29139
29431
|
return /dubious ownership|safe\.directory/i.test(stderr);
|
|
@@ -29146,91 +29438,27 @@ function normalizeGitPath(dir) {
|
|
|
29146
29438
|
}
|
|
29147
29439
|
|
|
29148
29440
|
/** Try to get git status for a directory. Returns { branch, statuses } or null. */
|
|
29149
|
-
function getGitInfo(dir) {
|
|
29150
|
-
|
|
29151
|
-
|
|
29152
|
-
|
|
29153
|
-
|
|
29154
|
-
|
|
29441
|
+
async function getGitInfo(dir, options = {}) {
|
|
29442
|
+
const result = projectStatus(await gitStatusRunner.read(dir, options), dir);
|
|
29443
|
+
if (!result.isGit) return null;
|
|
29444
|
+
const statuses = new Map();
|
|
29445
|
+
for (const change of result.changes) {
|
|
29446
|
+
const child = change.path.split('/')[0];
|
|
29447
|
+
if (gitStatusPriority(change.status) > gitStatusPriority(statuses.get(child))) statuses.set(child, change.status);
|
|
29155
29448
|
}
|
|
29449
|
+
return { branch: result.branch, statuses, scanState: result.timedOut ? 'timed-out' : result.ok ? 'ready' : 'failed' };
|
|
29450
|
+
}
|
|
29156
29451
|
|
|
29157
|
-
|
|
29158
|
-
|
|
29159
|
-
|
|
29160
|
-
|
|
29161
|
-
let branch = '';
|
|
29162
|
-
const showCurrent = spawnGitRead(['branch', '--show-current'], { cwd: dir, timeout: 3000 });
|
|
29163
|
-
if (showCurrent.status === 0) {
|
|
29164
|
-
branch = showCurrent.stdout.toString().trim();
|
|
29165
|
-
} else {
|
|
29166
|
-
logGitFailure('branch --show-current', showCurrent.error || { stderr: showCurrent.stderr });
|
|
29167
|
-
}
|
|
29168
|
-
if (!branch) {
|
|
29169
|
-
const abbrev = spawnGitRead(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: dir, timeout: 3000 });
|
|
29170
|
-
if (abbrev.status === 0) {
|
|
29171
|
-
const b = abbrev.stdout.toString().trim();
|
|
29172
|
-
if (b && b !== 'HEAD') branch = b;
|
|
29173
|
-
}
|
|
29174
|
-
}
|
|
29175
|
-
if (!branch) {
|
|
29176
|
-
const shortSha = spawnGitRead(['rev-parse', '--short', 'HEAD'], { cwd: dir, timeout: 3000 });
|
|
29177
|
-
if (shortSha.status === 0) branch = shortSha.stdout.toString().trim();
|
|
29178
|
-
}
|
|
29179
|
-
|
|
29180
|
-
try {
|
|
29181
|
-
// Get status for this directory (porcelain v1: XY <path>)
|
|
29182
|
-
const statusResult = spawnGitRead(['status', '--porcelain', '-u', '.'], { cwd: dir, maxBuffer: 256 * 1024 * 1024 });
|
|
29183
|
-
if (statusResult.status !== 0) {
|
|
29184
|
-
logGitFailure('status --porcelain', statusResult.error || { stderr: statusResult.stderr });
|
|
29185
|
-
return { branch, statuses: new Map() };
|
|
29186
|
-
}
|
|
29187
|
-
const raw = statusResult.stdout.toString();
|
|
29188
|
-
const statuses = new Map(); // name → status code
|
|
29189
|
-
|
|
29190
|
-
for (const line of raw.split('\n')) {
|
|
29191
|
-
if (!line || line.length < 4) continue;
|
|
29192
|
-
const xy = line.substring(0, 2); // index + worktree status
|
|
29193
|
-
let filePath = line.substring(3);
|
|
29194
|
-
// Handle renames: "R old -> new"
|
|
29195
|
-
const arrowIdx = filePath.indexOf(' -> ');
|
|
29196
|
-
if (arrowIdx !== -1) filePath = filePath.substring(arrowIdx + 4);
|
|
29197
|
-
|
|
29198
|
-
// Get the immediate child name relative to this directory
|
|
29199
|
-
const sep = filePath.indexOf('/');
|
|
29200
|
-
const childName = sep === -1 ? filePath : filePath.substring(0, sep);
|
|
29201
|
-
|
|
29202
|
-
// Map XY to a simplified status
|
|
29203
|
-
let status;
|
|
29204
|
-
const x = xy[0], y = xy[1];
|
|
29205
|
-
if (xy === '??') status = 'untracked';
|
|
29206
|
-
else if (xy === '!!') continue; // ignored
|
|
29207
|
-
else if (x === 'U' || y === 'U' || xy === 'DD' || xy === 'AA') status = 'conflict';
|
|
29208
|
-
else if (x === 'A') status = 'added';
|
|
29209
|
-
else if (x === 'D' || y === 'D') status = 'deleted';
|
|
29210
|
-
else if (x === 'R') status = 'renamed';
|
|
29211
|
-
else if (x !== ' ' && y !== ' ') status = 'mixed'; // staged + unstaged changes
|
|
29212
|
-
else if (x !== ' ') status = 'staged';
|
|
29213
|
-
else if (y !== ' ') status = 'modified';
|
|
29214
|
-
else continue;
|
|
29215
|
-
|
|
29216
|
-
// For directories: escalate priority (conflict > modified > staged > added > untracked)
|
|
29217
|
-
const existing = statuses.get(childName);
|
|
29218
|
-
if (!existing || gitStatusPriority(status) > gitStatusPriority(existing)) {
|
|
29219
|
-
statuses.set(childName, status);
|
|
29220
|
-
}
|
|
29221
|
-
}
|
|
29222
|
-
|
|
29223
|
-
return { branch, statuses };
|
|
29224
|
-
} catch (err) {
|
|
29225
|
-
logGitFailure('getGitInfo status-parse', err);
|
|
29226
|
-
return { branch, statuses: new Map() };
|
|
29227
|
-
}
|
|
29452
|
+
function gitRequestSignal(res) {
|
|
29453
|
+
const controller = new AbortController();
|
|
29454
|
+
res.once('close', () => controller.abort());
|
|
29455
|
+
return controller.signal;
|
|
29228
29456
|
}
|
|
29229
29457
|
|
|
29230
29458
|
const GIT_STATUS_PRIORITY = { untracked: 1, added: 2, renamed: 2, staged: 3, modified: 4, mixed: 5, deleted: 5, conflict: 6 };
|
|
29231
29459
|
function gitStatusPriority(s) { return GIT_STATUS_PRIORITY[s] || 0; }
|
|
29232
29460
|
|
|
29233
|
-
app.get('/api/terminal/files', (req, res) => {
|
|
29461
|
+
app.get('/api/terminal/files', async (req, res) => {
|
|
29234
29462
|
const dir = req.query.path;
|
|
29235
29463
|
if (!dir || typeof dir !== 'string') return res.status(400).json({ error: 'path required' });
|
|
29236
29464
|
const search = (req.query.search || '').trim().toLowerCase();
|
|
@@ -29287,7 +29515,7 @@ app.get('/api/terminal/files', (req, res) => {
|
|
|
29287
29515
|
});
|
|
29288
29516
|
|
|
29289
29517
|
// Try git — gracefully ignored if unavailable
|
|
29290
|
-
const git = getGitInfo(dir);
|
|
29518
|
+
const git = await getGitInfo(dir, { signal: gitRequestSignal(res) });
|
|
29291
29519
|
if (git) {
|
|
29292
29520
|
for (const item of items) {
|
|
29293
29521
|
const s = git.statuses.get(item.name);
|
|
@@ -29301,11 +29529,11 @@ app.get('/api/terminal/files', (req, res) => {
|
|
|
29301
29529
|
}
|
|
29302
29530
|
});
|
|
29303
29531
|
|
|
29304
|
-
app.get('/api/terminal/branches', (req, res) => {
|
|
29532
|
+
app.get('/api/terminal/branches', async (req, res) => {
|
|
29305
29533
|
const dir = req.query.path;
|
|
29306
29534
|
if (!dir || typeof dir !== 'string') return res.status(400).json({ error: 'path required' });
|
|
29307
29535
|
|
|
29308
|
-
const inside = spawnGitRead(['rev-parse', '--is-inside-work-tree'], { cwd: dir, timeout: 3000 });
|
|
29536
|
+
const inside = await spawnGitRead(['rev-parse', '--is-inside-work-tree'], { cwd: dir, timeout: 3000 });
|
|
29309
29537
|
if (inside.status !== 0) {
|
|
29310
29538
|
logGitFailure('branches: rev-parse', inside.error || { stderr: inside.stderr });
|
|
29311
29539
|
return res.json({ branches: [], current: null });
|
|
@@ -29314,11 +29542,11 @@ app.get('/api/terminal/branches', (req, res) => {
|
|
|
29314
29542
|
try {
|
|
29315
29543
|
// Current branch — chained fallback (older git may not support --show-current)
|
|
29316
29544
|
let current = '';
|
|
29317
|
-
const showCurrent = spawnGitRead(['branch', '--show-current'], { cwd: dir, timeout: 3000 });
|
|
29545
|
+
const showCurrent = await spawnGitRead(['branch', '--show-current'], { cwd: dir, timeout: 3000 });
|
|
29318
29546
|
if (showCurrent.status === 0) current = showCurrent.stdout.toString().trim();
|
|
29319
29547
|
else logGitFailure('branches: show-current', showCurrent.error || { stderr: showCurrent.stderr });
|
|
29320
29548
|
if (!current) {
|
|
29321
|
-
const abbrev = spawnGitRead(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: dir, timeout: 3000 });
|
|
29549
|
+
const abbrev = await spawnGitRead(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: dir, timeout: 3000 });
|
|
29322
29550
|
if (abbrev.status === 0) {
|
|
29323
29551
|
const b = abbrev.stdout.toString().trim();
|
|
29324
29552
|
if (b && b !== 'HEAD') current = b;
|
|
@@ -29327,7 +29555,7 @@ app.get('/api/terminal/branches', (req, res) => {
|
|
|
29327
29555
|
|
|
29328
29556
|
// All local branches — plain `git branch` parsed manually; avoids --format=%(refname:short)
|
|
29329
29557
|
// which can get mangled by cmd.exe variable expansion on Windows even with proper quoting.
|
|
29330
|
-
const branchResult = spawnGitRead(['branch'], { cwd: dir });
|
|
29558
|
+
const branchResult = await spawnGitRead(['branch'], { cwd: dir });
|
|
29331
29559
|
if (branchResult.status !== 0) logGitFailure('branch list', branchResult.error || { stderr: branchResult.stderr });
|
|
29332
29560
|
const raw = branchResult.status === 0 ? branchResult.stdout.toString() : '';
|
|
29333
29561
|
// Plain `git branch` output: each line is " branch" or "* branch" (current). Strip
|
|
@@ -29345,7 +29573,7 @@ app.get('/api/terminal/branches', (req, res) => {
|
|
|
29345
29573
|
// Trust a workspace path with git — runs `git config --global --add safe.directory <path>`
|
|
29346
29574
|
// so git stops refusing to operate on it (CVE-2022-24765 dubious-ownership protection).
|
|
29347
29575
|
// Triggered by the add-project UI flows when the server detects the dubious-ownership error.
|
|
29348
|
-
app.post('/api/terminal/trust-workspace', (req, res) => {
|
|
29576
|
+
app.post('/api/terminal/trust-workspace', async (req, res) => {
|
|
29349
29577
|
const { path: dir } = req.body || {};
|
|
29350
29578
|
if (!dir || typeof dir !== 'string') return res.status(400).json({ ok: false, error: 'path required' });
|
|
29351
29579
|
if (/[\x00-\x1f]/.test(dir) || dir === '*' || dir === '/' || dir === '\\') {
|
|
@@ -29361,7 +29589,7 @@ app.post('/api/terminal/trust-workspace', (req, res) => {
|
|
|
29361
29589
|
}
|
|
29362
29590
|
|
|
29363
29591
|
const gitPath = normalizeGitPath(abs);
|
|
29364
|
-
const r = spawnGit(['config', '--global', '--add', 'safe.directory', gitPath]);
|
|
29592
|
+
const r = await spawnGit(['config', '--global', '--add', 'safe.directory', gitPath]);
|
|
29365
29593
|
if (r.status !== 0) {
|
|
29366
29594
|
const stderr = r.stderr?.toString?.()?.trim() || r.error?.message || 'git config failed';
|
|
29367
29595
|
logGitFailure('config --add safe.directory', r.error || { stderr: r.stderr });
|
|
@@ -29369,12 +29597,12 @@ app.post('/api/terminal/trust-workspace', (req, res) => {
|
|
|
29369
29597
|
}
|
|
29370
29598
|
|
|
29371
29599
|
// Confirm by re-probing
|
|
29372
|
-
const stillDubious = detectDubiousOwnership(abs);
|
|
29600
|
+
const stillDubious = await detectDubiousOwnership(abs);
|
|
29373
29601
|
console.log(`[git] trusted workspace: ${gitPath}${stillDubious ? ' (but still flagged — check permissions)' : ''}`);
|
|
29374
29602
|
res.json({ ok: true, path: gitPath, stillDubious });
|
|
29375
29603
|
});
|
|
29376
29604
|
|
|
29377
|
-
app.post('/api/terminal/checkout', (req, res) => {
|
|
29605
|
+
app.post('/api/terminal/checkout', async (req, res) => {
|
|
29378
29606
|
const { path: dir, branch } = req.body;
|
|
29379
29607
|
if (!dir || !branch) return res.status(400).json({ error: 'path and branch required' });
|
|
29380
29608
|
|
|
@@ -29383,7 +29611,7 @@ app.post('/api/terminal/checkout', (req, res) => {
|
|
|
29383
29611
|
return res.status(400).json({ error: 'Invalid branch name' });
|
|
29384
29612
|
}
|
|
29385
29613
|
|
|
29386
|
-
const inside = spawnGitRead(['rev-parse', '--is-inside-work-tree'], { cwd: dir, timeout: 3000 });
|
|
29614
|
+
const inside = await spawnGitRead(['rev-parse', '--is-inside-work-tree'], { cwd: dir, timeout: 3000 });
|
|
29387
29615
|
if (inside.status !== 0) {
|
|
29388
29616
|
logGitFailure('checkout: rev-parse', inside.error || { stderr: inside.stderr });
|
|
29389
29617
|
return res.status(400).json({ error: 'Not a git repository' });
|
|
@@ -29393,7 +29621,8 @@ app.post('/api/terminal/checkout', (req, res) => {
|
|
|
29393
29621
|
// No '--' separator — that would make git treat the branch name as a file pathspec.
|
|
29394
29622
|
// (The switch itself is a write and runs plainly, with the repository's post-checkout hook; the two questions
|
|
29395
29623
|
// around it are reads.)
|
|
29396
|
-
const result = spawnGit(['checkout', branch], { cwd: dir, timeout: 10000 });
|
|
29624
|
+
const result = await spawnGit(['checkout', branch], { cwd: dir, timeout: 10000 });
|
|
29625
|
+
gitStatusRunner.invalidate(dir);
|
|
29397
29626
|
if (result.status !== 0) {
|
|
29398
29627
|
const stderr = result.stderr?.toString?.()?.trim() || result.error?.message || 'Checkout failed';
|
|
29399
29628
|
return res.status(500).json({ error: stderr });
|
|
@@ -29401,71 +29630,21 @@ app.post('/api/terminal/checkout', (req, res) => {
|
|
|
29401
29630
|
|
|
29402
29631
|
// Return confirmed branch name + git's own output
|
|
29403
29632
|
let current = '';
|
|
29404
|
-
const showCurrent = spawnGitRead(['branch', '--show-current'], { cwd: dir, timeout: 3000 });
|
|
29633
|
+
const showCurrent = await spawnGitRead(['branch', '--show-current'], { cwd: dir, timeout: 3000 });
|
|
29405
29634
|
if (showCurrent.status === 0) current = showCurrent.stdout.toString().trim();
|
|
29406
29635
|
else logGitFailure('checkout: show-current', showCurrent.error || { stderr: showCurrent.stderr });
|
|
29407
29636
|
const output = result.stderr?.toString?.()?.trim() || `Switched to branch '${current}'`;
|
|
29408
29637
|
res.json({ ok: true, branch: current, output });
|
|
29409
29638
|
});
|
|
29410
29639
|
|
|
29411
|
-
//
|
|
29412
|
-
|
|
29413
|
-
const
|
|
29414
|
-
|
|
29415
|
-
|
|
29416
|
-
|
|
29417
|
-
|
|
29418
|
-
|
|
29419
|
-
const read = async (args, options) => {
|
|
29420
|
-
const safe = await gitRead(dir, args);
|
|
29421
|
-
return execFileAsync('git', safe.args, { cwd: dir, env: safe.env, ...options });
|
|
29422
|
-
};
|
|
29423
|
-
try {
|
|
29424
|
-
await read(['rev-parse', '--is-inside-work-tree'], { timeout: 3000 });
|
|
29425
|
-
} catch {
|
|
29426
|
-
return { ok: true, isGit: false, branch: null, changes: [] };
|
|
29427
|
-
}
|
|
29428
|
-
|
|
29429
|
-
const resolveBranch = async () => {
|
|
29430
|
-
try {
|
|
29431
|
-
const { stdout } = await read(['branch', '--show-current'], { timeout: 3000 });
|
|
29432
|
-
let branch = stdout.trim();
|
|
29433
|
-
if (!branch) {
|
|
29434
|
-
const { stdout: head } = await read(['rev-parse', '--short', 'HEAD'], { timeout: 3000 });
|
|
29435
|
-
branch = head.trim();
|
|
29436
|
-
}
|
|
29437
|
-
return branch;
|
|
29438
|
-
} catch { return ''; }
|
|
29439
|
-
};
|
|
29440
|
-
|
|
29441
|
-
const [branch, statusOut] = await Promise.all([
|
|
29442
|
-
resolveBranch(),
|
|
29443
|
-
read(['status', '--porcelain'], { timeout: 5000, maxBuffer: 256 * 1024 * 1024 }),
|
|
29444
|
-
]);
|
|
29445
|
-
|
|
29446
|
-
const changes = [];
|
|
29447
|
-
for (const line of statusOut.stdout.split('\n')) {
|
|
29448
|
-
if (!line || line.length < 4) continue;
|
|
29449
|
-
const xy = line.substring(0, 2);
|
|
29450
|
-
let filePath = line.substring(3);
|
|
29451
|
-
const arrowIdx = filePath.indexOf(' -> ');
|
|
29452
|
-
if (arrowIdx !== -1) filePath = filePath.substring(arrowIdx + 4);
|
|
29453
|
-
const x = xy[0], y = xy[1];
|
|
29454
|
-
let staged = x !== ' ' && x !== '?';
|
|
29455
|
-
let status;
|
|
29456
|
-
if (xy === '??') status = 'untracked';
|
|
29457
|
-
else if (xy === '!!') continue;
|
|
29458
|
-
else if (x === 'U' || y === 'U' || xy === 'DD' || xy === 'AA') status = 'conflict';
|
|
29459
|
-
else if (x === 'A') status = 'added';
|
|
29460
|
-
else if (x === 'D' || y === 'D') status = 'deleted';
|
|
29461
|
-
else if (x === 'R') status = 'renamed';
|
|
29462
|
-
else if (x !== ' ' && y !== ' ') status = 'mixed';
|
|
29463
|
-
else if (x !== ' ') status = 'staged';
|
|
29464
|
-
else status = 'modified';
|
|
29465
|
-
changes.push({ path: filePath, status, staged });
|
|
29466
|
-
}
|
|
29467
|
-
|
|
29468
|
-
return { ok: true, isGit: true, branch, changes };
|
|
29640
|
+
// All callers share the canonical worktree's flight and last completed result.
|
|
29641
|
+
async function computeGitStatus(dir, options = {}) {
|
|
29642
|
+
const result = projectStatus(await gitStatusRunner.read(dir, options), dir);
|
|
29643
|
+
if (!result.ok || result.partial || result.stale) return { ok: false, isGit: !!result.isGit, branch: result.branch, changes: result.changes || [],
|
|
29644
|
+
partial: true, stale: !!result.stale, timedOut: !!result.timedOut, retryAt: result.retryAt,
|
|
29645
|
+
scanState: result.timedOut ? 'timed-out' : 'failed', error: 'Git status is incomplete. Refresh before switching branches or assuming the worktree is clean.' };
|
|
29646
|
+
if (!result.isGit) return { ok: true, isGit: false, branch: null, changes: [] };
|
|
29647
|
+
return { ok: true, isGit: true, branch: result.branch, changes: result.changes };
|
|
29469
29648
|
}
|
|
29470
29649
|
|
|
29471
29650
|
// The folder a git route is asked about: inside a registered folder, as the disk resolves it (the rule of
|
|
@@ -29479,11 +29658,9 @@ function registeredGitFolder(asked, res) {
|
|
|
29479
29658
|
// be started, failed or took too long, as the command lines the /api/git routes used to run did. For reads only
|
|
29480
29659
|
// (every caller is one): run as lib/git-read.js runs a read, so no program the repository's own configuration
|
|
29481
29660
|
// names is started. `git add` and `git commit` are not run through here: a commit keeps the repository's hooks.
|
|
29482
|
-
function gitOutput(dir, args, { timeout = 5000, maxBuffer } = {}) {
|
|
29483
|
-
const
|
|
29484
|
-
|
|
29485
|
-
if (result.error) throw result.error;
|
|
29486
|
-
if (result.status !== 0) throw new Error(result.stderr?.toString?.()?.trim?.() || `git ${args[0]} failed`);
|
|
29661
|
+
async function gitOutput(dir, args, { timeout = 5000, maxBuffer } = {}) {
|
|
29662
|
+
const result = await gitStatusRunner.run(args, { cwd: dir, timeoutMs: timeout, ...(maxBuffer ? { maxBuffer } : {}) });
|
|
29663
|
+
if (!result.ok) throw new Error(result.stderr?.toString?.()?.trim?.() || `git ${args[0]} failed`);
|
|
29487
29664
|
return result.stdout.toString();
|
|
29488
29665
|
}
|
|
29489
29666
|
|
|
@@ -29493,37 +29670,17 @@ function gitOutput(dir, args, { timeout = 5000, maxBuffer } = {}) {
|
|
|
29493
29670
|
app.get('/api/git/status', editorOwnerGuard, async (req, res) => {
|
|
29494
29671
|
const dir = req.query.path;
|
|
29495
29672
|
if (!dir || typeof dir !== 'string') return res.status(400).json({ error: 'path required' });
|
|
29496
|
-
|
|
29497
|
-
|
|
29498
|
-
if (!entry || Date.now() - entry.at >= GIT_STATUS_TTL_MS) {
|
|
29499
|
-
const promise = computeGitStatus(dir);
|
|
29500
|
-
entry = { at: Date.now(), promise };
|
|
29501
|
-
_gitStatusCache.set(dir, entry);
|
|
29502
|
-
// Failed computations shouldn't be served for the rest of the TTL.
|
|
29503
|
-
promise.catch(() => {
|
|
29504
|
-
if (_gitStatusCache.get(dir)?.promise === promise) _gitStatusCache.delete(dir);
|
|
29505
|
-
});
|
|
29506
|
-
// Keep the map bounded across many distinct paths.
|
|
29507
|
-
if (_gitStatusCache.size > 100) {
|
|
29508
|
-
const cutoff = Date.now() - 60_000;
|
|
29509
|
-
for (const [k, v] of _gitStatusCache) if (v.at < cutoff) _gitStatusCache.delete(k);
|
|
29510
|
-
}
|
|
29511
|
-
}
|
|
29512
|
-
|
|
29513
|
-
try {
|
|
29514
|
-
res.json(await entry.promise);
|
|
29515
|
-
} catch (err) {
|
|
29516
|
-
res.status(500).json({ error: err.stderr?.toString?.()?.trim?.() || err.message });
|
|
29517
|
-
}
|
|
29673
|
+
if (req.query.refresh === '1') gitStatusRunner.invalidate(dir, { refresh: true });
|
|
29674
|
+
res.json(await computeGitStatus(dir, { signal: gitRequestSignal(res) }));
|
|
29518
29675
|
});
|
|
29519
29676
|
|
|
29520
|
-
app.post('/api/git/commit', editorOwnerGuard, (req, res) => {
|
|
29677
|
+
app.post('/api/git/commit', editorOwnerGuard, async (req, res) => {
|
|
29521
29678
|
const { path: dir, message, files } = req.body;
|
|
29522
29679
|
if (!dir || typeof dir !== 'string' || !message) return res.status(400).json({ error: 'path and message required' });
|
|
29523
29680
|
if (typeof message !== 'string' || message.trim().length === 0) return res.status(400).json({ error: 'Commit message cannot be empty' });
|
|
29524
29681
|
|
|
29525
29682
|
try {
|
|
29526
|
-
gitOutput(dir, ['rev-parse', '--is-inside-work-tree'], { timeout: 3000 });
|
|
29683
|
+
await gitOutput(dir, ['rev-parse', '--is-inside-work-tree'], { timeout: 3000 });
|
|
29527
29684
|
} catch {
|
|
29528
29685
|
return res.status(400).json({ error: 'Not a git repository' });
|
|
29529
29686
|
}
|
|
@@ -29535,16 +29692,19 @@ app.post('/api/git/commit', editorOwnerGuard, (req, res) => {
|
|
|
29535
29692
|
for (const f of files) {
|
|
29536
29693
|
if (typeof f !== 'string' || f.includes('..')) return res.status(400).json({ error: 'Invalid file path' });
|
|
29537
29694
|
}
|
|
29538
|
-
const result =
|
|
29695
|
+
const result = await gitStatusRunner.runRaw(['add', '--', ...files], { cwd: dir, timeoutMs: 10000 });
|
|
29696
|
+
gitStatusRunner.invalidate(dir);
|
|
29539
29697
|
if (result.status !== 0) return res.status(500).json({ error: result.stderr?.toString?.()?.trim() || 'Stage failed' });
|
|
29540
29698
|
} else {
|
|
29541
29699
|
// Stage all changes
|
|
29542
|
-
const result =
|
|
29700
|
+
const result = await gitStatusRunner.runRaw(['add', '-A'], { cwd: dir, timeoutMs: 10000 });
|
|
29701
|
+
gitStatusRunner.invalidate(dir);
|
|
29543
29702
|
if (result.status !== 0) return res.status(500).json({ error: result.stderr?.toString?.()?.trim() || 'Stage failed' });
|
|
29544
29703
|
}
|
|
29545
29704
|
|
|
29546
29705
|
// Commit
|
|
29547
|
-
const result =
|
|
29706
|
+
const result = await gitStatusRunner.runRaw(['commit', '-m', message.trim()], { cwd: dir, timeoutMs: 15000 });
|
|
29707
|
+
gitStatusRunner.invalidate(dir);
|
|
29548
29708
|
if (result.status !== 0) {
|
|
29549
29709
|
const stderr = result.stderr?.toString?.()?.trim() || '';
|
|
29550
29710
|
const stdout = result.stdout?.toString?.()?.trim() || '';
|
|
@@ -29558,32 +29718,32 @@ app.post('/api/git/commit', editorOwnerGuard, (req, res) => {
|
|
|
29558
29718
|
}
|
|
29559
29719
|
});
|
|
29560
29720
|
|
|
29561
|
-
app.get('/api/git/diff-summary', editorOwnerGuard, (req, res) => {
|
|
29721
|
+
app.get('/api/git/diff-summary', editorOwnerGuard, async (req, res) => {
|
|
29562
29722
|
const asked = req.query.path;
|
|
29563
29723
|
if (!asked || typeof asked !== 'string') return res.status(400).json({ error: 'path required' });
|
|
29564
29724
|
const dir = registeredGitFolder(asked, res);
|
|
29565
29725
|
if (!dir) return;
|
|
29566
29726
|
|
|
29567
29727
|
try {
|
|
29568
|
-
gitOutput(dir, ['rev-parse', '--is-inside-work-tree'], { timeout: 3000 });
|
|
29728
|
+
await gitOutput(dir, ['rev-parse', '--is-inside-work-tree'], { timeout: 3000 });
|
|
29569
29729
|
} catch {
|
|
29570
29730
|
return res.status(400).json({ error: 'Not a git repository' });
|
|
29571
29731
|
}
|
|
29572
29732
|
|
|
29573
29733
|
try {
|
|
29574
29734
|
// Get diff stat for tracked files
|
|
29575
|
-
const stat = gitOutput(dir, ['diff', '--stat', '--stat-width=120']).trim();
|
|
29735
|
+
const stat = (await gitOutput(dir, ['diff', '--stat', '--stat-width=120'])).trim();
|
|
29576
29736
|
|
|
29577
29737
|
// Get diff stat for staged files
|
|
29578
|
-
const stagedStat = gitOutput(dir, ['diff', '--cached', '--stat', '--stat-width=120']).trim();
|
|
29738
|
+
const stagedStat = (await gitOutput(dir, ['diff', '--cached', '--stat', '--stat-width=120'])).trim();
|
|
29579
29739
|
|
|
29580
29740
|
// Get untracked files
|
|
29581
|
-
const untracked = gitOutput(dir, ['ls-files', '--others', '--exclude-standard']).trim();
|
|
29741
|
+
const untracked = (await gitOutput(dir, ['ls-files', '--others', '--exclude-standard'])).trim();
|
|
29582
29742
|
|
|
29583
29743
|
// Get short diff for context (limited to avoid huge payloads)
|
|
29584
|
-
const diff = gitOutput(dir, ['diff', '-U1', '--no-color'], { timeout: 8000, maxBuffer: 64 * 1024 }).substring(0, 4000);
|
|
29744
|
+
const diff = (await gitOutput(dir, ['diff', '-U1', '--no-color'], { timeout: 8000, maxBuffer: 64 * 1024 })).substring(0, 4000);
|
|
29585
29745
|
|
|
29586
|
-
const stagedDiff = gitOutput(dir, ['diff', '--cached', '-U1', '--no-color'], { timeout: 8000, maxBuffer: 64 * 1024 }).substring(0, 4000);
|
|
29746
|
+
const stagedDiff = (await gitOutput(dir, ['diff', '--cached', '-U1', '--no-color'], { timeout: 8000, maxBuffer: 64 * 1024 })).substring(0, 4000);
|
|
29587
29747
|
|
|
29588
29748
|
// Build a commit message from the changes
|
|
29589
29749
|
const changedFiles = [];
|
|
@@ -29651,7 +29811,7 @@ app.use(createGitDiffRouter({ fileRoots: allowedFileRoots, guard: editorOwnerGua
|
|
|
29651
29811
|
|
|
29652
29812
|
// ── Git Log ──
|
|
29653
29813
|
|
|
29654
|
-
app.get('/api/git/log', editorOwnerGuard, (req, res) => {
|
|
29814
|
+
app.get('/api/git/log', editorOwnerGuard, async (req, res) => {
|
|
29655
29815
|
const asked = req.query.path;
|
|
29656
29816
|
if (!asked || typeof asked !== 'string') return res.status(400).json({ error: 'path required' });
|
|
29657
29817
|
const dir = registeredGitFolder(asked, res);
|
|
@@ -29659,13 +29819,13 @@ app.get('/api/git/log', editorOwnerGuard, (req, res) => {
|
|
|
29659
29819
|
const count = Math.min(Math.max(parseInt(req.query.count) || 10, 1), 50);
|
|
29660
29820
|
|
|
29661
29821
|
try {
|
|
29662
|
-
gitOutput(dir, ['rev-parse', '--is-inside-work-tree'], { timeout: 3000 });
|
|
29822
|
+
await gitOutput(dir, ['rev-parse', '--is-inside-work-tree'], { timeout: 3000 });
|
|
29663
29823
|
} catch {
|
|
29664
29824
|
return res.status(400).json({ error: 'Not a git repository' });
|
|
29665
29825
|
}
|
|
29666
29826
|
|
|
29667
29827
|
try {
|
|
29668
|
-
const raw = gitOutput(dir, ['log', '--oneline', '--no-decorate', '-n', String(count)]).trim();
|
|
29828
|
+
const raw = (await gitOutput(dir, ['log', '--oneline', '--no-decorate', '-n', String(count)])).trim();
|
|
29669
29829
|
const commits = raw ? raw.split('\n').map(line => {
|
|
29670
29830
|
const spaceIdx = line.indexOf(' ');
|
|
29671
29831
|
return { hash: line.substring(0, spaceIdx), message: line.substring(spaceIdx + 1) };
|
|
@@ -29683,17 +29843,17 @@ app.post('/api/git/generate-message', editorOwnerGuard, async (req, res) => {
|
|
|
29683
29843
|
if (!dir || typeof dir !== 'string') return res.status(400).json({ error: 'path required' });
|
|
29684
29844
|
|
|
29685
29845
|
try {
|
|
29686
|
-
gitOutput(dir, ['rev-parse', '--is-inside-work-tree'], { timeout: 3000 });
|
|
29846
|
+
await gitOutput(dir, ['rev-parse', '--is-inside-work-tree'], { timeout: 3000 });
|
|
29687
29847
|
} catch {
|
|
29688
29848
|
return res.status(400).json({ error: 'Not a git repository' });
|
|
29689
29849
|
}
|
|
29690
29850
|
|
|
29691
29851
|
// Gather diff data — separate try/catch per command so one failure doesn't skip the rest
|
|
29692
29852
|
let diffText = '', stagedText = '', untrackedList = '', recentCommits = '';
|
|
29693
|
-
try { diffText = gitOutput(dir, ['diff', '--no-color'], { timeout: 8000, maxBuffer: 2 * 1024 * 1024 }).substring(0, 4000); } catch {}
|
|
29694
|
-
try { stagedText = gitOutput(dir, ['diff', '--cached', '--no-color'], { timeout: 8000, maxBuffer: 2 * 1024 * 1024 }).substring(0, 4000); } catch {}
|
|
29695
|
-
try { untrackedList = gitOutput(dir, ['ls-files', '--others', '--exclude-standard']).trim(); } catch {}
|
|
29696
|
-
try { recentCommits = gitOutput(dir, ['log', '--oneline', '--no-decorate', '-n', '5'], { timeout: 3000 }).trim(); } catch {}
|
|
29853
|
+
try { diffText = (await gitOutput(dir, ['diff', '--no-color'], { timeout: 8000, maxBuffer: 2 * 1024 * 1024 })).substring(0, 4000); } catch {}
|
|
29854
|
+
try { stagedText = (await gitOutput(dir, ['diff', '--cached', '--no-color'], { timeout: 8000, maxBuffer: 2 * 1024 * 1024 })).substring(0, 4000); } catch {}
|
|
29855
|
+
try { untrackedList = (await gitOutput(dir, ['ls-files', '--others', '--exclude-standard'])).trim(); } catch {}
|
|
29856
|
+
try { recentCommits = (await gitOutput(dir, ['log', '--oneline', '--no-decorate', '-n', '5'], { timeout: 3000 })).trim(); } catch {}
|
|
29697
29857
|
|
|
29698
29858
|
const allDiff = (diffText + '\n' + stagedText).trim();
|
|
29699
29859
|
if (!allDiff && !untrackedList) {
|
|
@@ -29744,8 +29904,8 @@ app.post('/api/git/generate-message', editorOwnerGuard, async (req, res) => {
|
|
|
29744
29904
|
// Fallback: heuristic (same logic as /api/git/diff-summary)
|
|
29745
29905
|
try {
|
|
29746
29906
|
let stat = '', stagedStat = '';
|
|
29747
|
-
try { stat = gitOutput(dir, ['diff', '--stat', '--stat-width=120'], { maxBuffer: 2 * 1024 * 1024 }).trim(); } catch {}
|
|
29748
|
-
try { stagedStat = gitOutput(dir, ['diff', '--cached', '--stat', '--stat-width=120'], { maxBuffer: 2 * 1024 * 1024 }).trim(); } catch {}
|
|
29907
|
+
try { stat = (await gitOutput(dir, ['diff', '--stat', '--stat-width=120'], { maxBuffer: 2 * 1024 * 1024 })).trim(); } catch {}
|
|
29908
|
+
try { stagedStat = (await gitOutput(dir, ['diff', '--cached', '--stat', '--stat-width=120'], { maxBuffer: 2 * 1024 * 1024 })).trim(); } catch {}
|
|
29749
29909
|
const changedFiles = [];
|
|
29750
29910
|
for (const line of (stat + '\n' + stagedStat).split('\n').filter(l => l.includes('|'))) {
|
|
29751
29911
|
const m = line.match(/^\s*(.+?)\s*\|/);
|
|
@@ -29823,7 +29983,7 @@ app.put('/api/cli/config', (req, res) => {
|
|
|
29823
29983
|
app.post('/api/cli/detect/:profileId', async (req, res) => {
|
|
29824
29984
|
try {
|
|
29825
29985
|
const { profileId } = req.params;
|
|
29826
|
-
if (!['claude-code', 'codex', '
|
|
29986
|
+
if (!['claude-code', 'codex', 'antigravity', 'opencode'].includes(profileId)) return res.status(400).json({ ok: false, error: 'Unknown profile' });
|
|
29827
29987
|
const status = await cliResolver.resolve(profileId, { force: true, configured: null });
|
|
29828
29988
|
res.json({
|
|
29829
29989
|
ok: true,
|
|
@@ -29873,7 +30033,7 @@ function stripAnsi(str) {
|
|
|
29873
30033
|
// stdin: false — message appended as the last CLI arg (flag takes a value)
|
|
29874
30034
|
const CLI_SIDECAR_CONFIG = {
|
|
29875
30035
|
'claude-code': { args: ['-p', '--verbose'], stdin: true }, // -p = non-interactive mode; reads prompt from stdin
|
|
29876
|
-
'
|
|
30036
|
+
'antigravity': { args: ['-p'], stdin: false }, // agy -p <prompt>: one print-mode turn
|
|
29877
30037
|
'codex': { args: ['-q'], stdin: false }, // -q <msg>; flag takes a value
|
|
29878
30038
|
'shell': null,
|
|
29879
30039
|
};
|
|
@@ -29888,7 +30048,7 @@ const CLI_LIVE_CONFIG = {
|
|
|
29888
30048
|
maxIdleMs: 120000, // 2 min absolute max wait
|
|
29889
30049
|
useBracketedPaste: true,
|
|
29890
30050
|
},
|
|
29891
|
-
'
|
|
30051
|
+
'antigravity': {
|
|
29892
30052
|
promptPatterns: [/>\s*$/m, /\u276F\s*$/m],
|
|
29893
30053
|
idleMs: 3000,
|
|
29894
30054
|
maxIdleMs: 120000,
|
|
@@ -30005,7 +30165,7 @@ function spawnSidecarProcess(linkId, sessionId, message) {
|
|
|
30005
30165
|
const sidecarConfig = CLI_SIDECAR_CONFIG[profile];
|
|
30006
30166
|
if (!sidecarConfig) throw new Error(`No sidecar config for profile: ${profile}`);
|
|
30007
30167
|
|
|
30008
|
-
// Get the CLI command from config (e.g. 'claude', '
|
|
30168
|
+
// Get the CLI command from config (e.g. 'claude', 'agy', 'codex')
|
|
30009
30169
|
const cliConfig = loadCliConfig();
|
|
30010
30170
|
const command = (cliConfig[profile]?.command || '').trim();
|
|
30011
30171
|
if (!command) throw new Error(`No CLI command configured for ${profile}`);
|
|
@@ -30025,7 +30185,7 @@ function spawnSidecarProcess(linkId, sessionId, message) {
|
|
|
30025
30185
|
// stdin-based: args are fixed, message piped after spawn (e.g. Claude Code)
|
|
30026
30186
|
} else if (IS_WIN) {
|
|
30027
30187
|
// Windows: cmd.exe shell quoting mangles multi-word args (splits by spaces),
|
|
30028
|
-
// causing CLIs
|
|
30188
|
+
// causing CLIs that take a value after -p (agy) to misparse -p "hello world" as -p + positional "hello" "world".
|
|
30029
30189
|
// Use PowerShell which correctly passes $msg as a single argument value.
|
|
30030
30190
|
tmpFile = join(os.tmpdir(), `synabun-relay-${randomBytes(4).toString('hex')}.txt`);
|
|
30031
30191
|
writeFileSync(tmpFile, message, 'utf-8');
|
|
@@ -30837,7 +30997,7 @@ async function acquireSharedBrowserTab({ requestedBrowserSessionId = null, owner
|
|
|
30837
30997
|
profileMode: result.profileMode, profileSource: result.profileSource,
|
|
30838
30998
|
});
|
|
30839
30999
|
} catch (err) {
|
|
30840
|
-
createError = err
|
|
31000
|
+
createError = browserStartFailure(err);
|
|
30841
31001
|
loopLog(ownerKey, logTag, 'strategy 2 FAILED: createBrowserSession threw', { err: err.message, stack: err.stack?.slice(0, 600) });
|
|
30842
31002
|
}
|
|
30843
31003
|
}
|
|
@@ -30847,8 +31007,9 @@ async function acquireSharedBrowserTab({ requestedBrowserSessionId = null, owner
|
|
|
30847
31007
|
// run's log, lastRunResult and the schedule:failed toast.
|
|
30848
31008
|
return {
|
|
30849
31009
|
error: createError
|
|
30850
|
-
? `Could not start the shared browser session: ${createError}`
|
|
31010
|
+
? `Could not start the shared browser session: ${createError.error}`
|
|
30851
31011
|
: 'Could not start the shared browser session. Open Browser Settings and verify the selected profile can launch.',
|
|
31012
|
+
...(createError?.code ? { code: createError.code, offenders: createError.offenders } : {}),
|
|
30852
31013
|
};
|
|
30853
31014
|
}
|
|
30854
31015
|
|
|
@@ -32013,34 +32174,72 @@ async function installMoneyGuardRoute(ctx) {
|
|
|
32013
32174
|
// still aborted. Managed contexts also block WS through Playwright. MoreLogin
|
|
32014
32175
|
// retains native WS only behind the verified server authentication boundary and
|
|
32015
32176
|
// credentials confined to this server, never an environment, page or log.
|
|
32016
|
-
|
|
32017
|
-
|
|
32018
|
-
|
|
32019
|
-
|
|
32020
|
-
|
|
32021
|
-
|
|
32177
|
+
// Records of serves this server did not start that may still run: each one could answer a page's WebSocket.
|
|
32178
|
+
function unverifiedServeRecords() {
|
|
32179
|
+
return opencodeServeRecords.list().filter(record =>
|
|
32180
|
+
!opencodeServeRecords.ownedHere(record) && opencodeServeRecords.verifySync(record) !== 'gone')
|
|
32181
|
+
.map(record => ({ ...record, claimable: (() => { try { return opencodeServeRecords.claimable(record); } catch { return false; } })() }));
|
|
32182
|
+
}
|
|
32183
|
+
const NATIVE_SERVE_WAIT_MS = Number(process.env.SYNABUN_NATIVE_SERVE_WAIT_MS) || 20_000;
|
|
32184
|
+
const NATIVE_SERVE_SWEEP_MS = Number(process.env.SYNABUN_NATIVE_SERVE_SWEEP_MS) || 10_000;
|
|
32185
|
+
// Left of a request's deadline for the answer itself (the MCP client gives a session create 70 s).
|
|
32186
|
+
const NATIVE_SERVE_REPLY_MARGIN_MS = 5_000;
|
|
32187
|
+
// Before refusing, repair what SynaBun can itself, bounded: stop the serves an ended SynaBun left running (the records'
|
|
32188
|
+
// own sweep, which signals only a provably-SynaBun process whose owner has ended; at most NATIVE_SERVE_SWEEP_MS, in the
|
|
32189
|
+
// background while the rest is read) and wait for SynaBun's own verified serves still starting (at most
|
|
32190
|
+
// NATIVE_SERVE_WAIT_MS), both inside the request's deadline. The context is held by the boundary from before the
|
|
32191
|
+
// first look (nativeServeBoundary.attach), so no unverified serve starts meanwhile. Nothing is cached: every attempt
|
|
32192
|
+
// reads the live state again. Authentication is never relaxed: what is left is named.
|
|
32193
|
+
async function protectNativeServeContext(ctx, { deadline = Infinity, signal = null } = {}) {
|
|
32194
|
+
try {
|
|
32195
|
+
return await nativeServeBoundary.attach(ctx, {
|
|
32196
|
+
state: () => ({ ports: [...liveOpencodeServePorts()], unverifiedRecords: unverifiedServeRecords() }),
|
|
32197
|
+
sweep: () => opencodeServeRecords.sweep(),
|
|
32198
|
+
waitMs: NATIVE_SERVE_WAIT_MS, sweepMs: NATIVE_SERVE_SWEEP_MS,
|
|
32199
|
+
deadline: Number.isFinite(deadline) ? deadline - NATIVE_SERVE_REPLY_MARGIN_MS : Infinity, signal,
|
|
32200
|
+
});
|
|
32201
|
+
} catch (error) {
|
|
32202
|
+
if (error?.offenders) console.warn(`[browser] native serve boundary refused: ${JSON.stringify(error.offenders)}`);
|
|
32203
|
+
throw error;
|
|
32204
|
+
}
|
|
32205
|
+
}
|
|
32206
|
+
|
|
32207
|
+
// The answer a failed browser start gives its caller: the sentence, and for the native serve boundary its code and
|
|
32208
|
+
// the serves it names (narrowed: never a token or a whole record).
|
|
32209
|
+
function browserStartFailure(err) {
|
|
32210
|
+
const out = { error: err?.message || String(err) };
|
|
32211
|
+
if (err?.code === 'NATIVE_SERVE_UNVERIFIED') { out.code = err.code; out.offenders = narrowOffenders(err.offenders); }
|
|
32212
|
+
return out;
|
|
32022
32213
|
}
|
|
32023
32214
|
|
|
32024
|
-
async function installServePortGuard(ctx) {
|
|
32215
|
+
async function installServePortGuard(ctx, { deadline = Infinity, signal = null } = {}) {
|
|
32025
32216
|
if (!ctx || ctx.__servePortGuardInstalled) return;
|
|
32026
32217
|
// routeWebSocket installs page bindings and replaces WebSocket on navigation,
|
|
32027
32218
|
// even with an empty matcher. Native contexts rely on the verified server's
|
|
32028
32219
|
// authentication; never silently route or drop protection for unknown serves.
|
|
32029
32220
|
const native = usesNativeBrowserNetworkGuard(ctx);
|
|
32030
|
-
const release = native ? protectNativeServeContext(ctx) : null;
|
|
32031
32221
|
const refused = (url) => { try { return !!browserServePortDenial(url?.href ?? url, liveOpencodeServePorts()); } catch { return false; } };
|
|
32222
|
+
const installRule = () => setBrowserNetworkRule(ctx, 'serve-port', {
|
|
32223
|
+
patterns: SERVE_PORT_PATTERNS,
|
|
32224
|
+
decide: request => refused(request.url) ? { abort: true } : null,
|
|
32225
|
+
});
|
|
32226
|
+
if (native) {
|
|
32227
|
+
// Held first (assertLaunch refuses an unverified start from here on), then the HTTP rule, then the bounded check.
|
|
32228
|
+
const release = nativeServeBoundary.reserve(ctx);
|
|
32229
|
+
try { await installRule(); } catch (error) {
|
|
32230
|
+
release();
|
|
32231
|
+
throw new Error(`Failed to install the OpenCode serve guard: ${error.message}`);
|
|
32232
|
+
}
|
|
32233
|
+
await protectNativeServeContext(ctx, { deadline, signal });
|
|
32234
|
+
ctx.__servePortGuardInstalled = true;
|
|
32235
|
+
return;
|
|
32236
|
+
}
|
|
32032
32237
|
try {
|
|
32033
|
-
await
|
|
32034
|
-
|
|
32035
|
-
|
|
32036
|
-
});
|
|
32037
|
-
if (!native) {
|
|
32038
|
-
if (typeof ctx.routeWebSocket !== 'function') throw new Error('WebSocket routing is unavailable');
|
|
32039
|
-
await ctx.routeWebSocket(refused, ws => Promise.resolve(ws.close({ code: 1008, reason: 'SynaBun: not an address the browser opens' })).catch(() => {}));
|
|
32040
|
-
}
|
|
32238
|
+
await installRule();
|
|
32239
|
+
if (typeof ctx.routeWebSocket !== 'function') throw new Error('WebSocket routing is unavailable');
|
|
32240
|
+
await ctx.routeWebSocket(refused, ws => Promise.resolve(ws.close({ code: 1008, reason: 'SynaBun: not an address the browser opens' })).catch(() => {}));
|
|
32041
32241
|
ctx.__servePortGuardInstalled = true;
|
|
32042
32242
|
} catch (error) {
|
|
32043
|
-
release?.();
|
|
32044
32243
|
throw new Error(`Failed to install the OpenCode serve guard: ${error.message}`);
|
|
32045
32244
|
}
|
|
32046
32245
|
}
|
|
@@ -32391,8 +32590,10 @@ async function createBrowserSession(options = {}) {
|
|
|
32391
32590
|
|| savedCfg.connectMode === 'morelogin';
|
|
32392
32591
|
if (_wantMoreLogin) {
|
|
32393
32592
|
try {
|
|
32394
|
-
|
|
32395
|
-
|
|
32593
|
+
// An install outside the known folders (a custom folder on Windows) still
|
|
32594
|
+
// counts when its local API answers.
|
|
32595
|
+
if (!moreLogin.isMoreLoginInstalled().installed && !(await moreLogin.probeMoreLoginApi({ savedCfg })).running) {
|
|
32596
|
+
throw new Error(`MoreLogin desktop app was not found and its local API does not answer on port ${moreLogin.getMoreLoginPort(savedCfg)}`);
|
|
32396
32597
|
}
|
|
32397
32598
|
// Installed but possibly not running (e.g. an autonomous schedule fires with
|
|
32398
32599
|
// nobody at the machine to open it) — auto-launch and wait for its local API
|
|
@@ -32718,7 +32919,7 @@ async function createBrowserSession(options = {}) {
|
|
|
32718
32919
|
const startServeDenial = browserServePortDenial(startUrl, liveOpencodeServePorts());
|
|
32719
32920
|
if (startServeDenial) throw new Error(startServeDenial);
|
|
32720
32921
|
await installMoneyGuardRoute(context);
|
|
32721
|
-
await installServePortGuard(context);
|
|
32922
|
+
await installServePortGuard(context, { deadline: options.browserRequest?.deadline, signal: options.browserRequest?.signal });
|
|
32722
32923
|
if (typeof _pageSession !== 'undefined') {
|
|
32723
32924
|
const marked = await _pageSession.markWorkerBrowserContext(context, { port: PORT, hostNames: machineNames.names() });
|
|
32724
32925
|
if (!marked) throw new Error('Cannot protect browser owner authority: local worker marker failed');
|
|
@@ -33730,30 +33931,41 @@ app.use('/api/browser', (req, res, next) => { res.setHeader('X-Synabun-Browser-D
|
|
|
33730
33931
|
// action, a new tab, CDP). Closing it, or listing its tabs, stays allowed.
|
|
33731
33932
|
app.use('/api/browser/sessions/:id', (req, res, next) => {
|
|
33732
33933
|
const protectedContext = browserSessions.get(req.params.id)?.context;
|
|
33733
|
-
const
|
|
33734
|
-
|
|
33735
|
-
|
|
33736
|
-
|
|
33737
|
-
|
|
33738
|
-
|
|
33739
|
-
|
|
33740
|
-
|
|
33741
|
-
|
|
33742
|
-
|
|
33743
|
-
|
|
33744
|
-
|
|
33745
|
-
|
|
33746
|
-
|
|
33747
|
-
|
|
33748
|
-
|
|
33749
|
-
|
|
33750
|
-
|
|
33751
|
-
|
|
33752
|
-
|
|
33753
|
-
|
|
33754
|
-
|
|
33755
|
-
|
|
33756
|
-
|
|
33934
|
+
const exempt = req.method === 'DELETE' || (req.method === 'GET' && req.path === '/tabs');
|
|
33935
|
+
const gate = () => {
|
|
33936
|
+
const unsafe = protectedContext?.__browserNetworkGuardError;
|
|
33937
|
+
if (unsafe && !exempt) {
|
|
33938
|
+
return res.status(503).json({ error: `Browser network protection unavailable: ${unsafe}`, networkGuard: true });
|
|
33939
|
+
}
|
|
33940
|
+
// Also cancel requests already waiting/probing when the protection disappears.
|
|
33941
|
+
// Cancellation prevents their next page action; an action that already began
|
|
33942
|
+
// retains the existing truthful "outcome uncertain" response semantics.
|
|
33943
|
+
if (req.method !== 'DELETE' && protectedContext?.on) {
|
|
33944
|
+
const cancel = reason => req.browserRequest?.cancelForProtection?.(reason);
|
|
33945
|
+
protectedContext.on('synabun-network-guard-fault', cancel);
|
|
33946
|
+
const release = () => protectedContext.removeListener('synabun-network-guard-fault', cancel);
|
|
33947
|
+
res.once('finish', release);
|
|
33948
|
+
res.once('close', release);
|
|
33949
|
+
}
|
|
33950
|
+
if (!req.get('X-Synabun-Terminal') || exempt) return next();
|
|
33951
|
+
const session = browserSessions.get(req.params.id);
|
|
33952
|
+
if (!session) return next();
|
|
33953
|
+
const cfg = loadBrowserConfig();
|
|
33954
|
+
if (cfg.connectMode !== 'morelogin' || sessionServesMoreLoginDefault(session, cfg)) return next();
|
|
33955
|
+
const envId = moreLoginDefaultEnvId(cfg);
|
|
33956
|
+
res.status(409).json({
|
|
33957
|
+
error: `Session ${req.params.id} is not the default browser (${session._profileMode || 'unknown'} mode${session._mlEnvId ? `, MoreLogin env ${session._mlEnvId}` : ''}; the default is MoreLogin${envId ? ` env ${envId}` : ''}), so SynaBun's browser tools do not act on it. Leave sessionId out: browser_navigate opens the page in the default browser.`,
|
|
33958
|
+
code: 'NOT_DEFAULT_BROWSER',
|
|
33959
|
+
});
|
|
33960
|
+
};
|
|
33961
|
+
// A faulted network guard re-arms on demand: a fresh protected attachment replaces
|
|
33962
|
+
// the broken one before this request runs, so a session never needs a server
|
|
33963
|
+
// restart to recover. Until a re-arm succeeds every action stays refused.
|
|
33964
|
+
if (protectedContext?.__browserNetworkGuardError && !exempt) {
|
|
33965
|
+
recoverBrowserNetworkGuard(protectedContext).then(gate).catch(next);
|
|
33966
|
+
return;
|
|
33967
|
+
}
|
|
33968
|
+
return gate();
|
|
33757
33969
|
});
|
|
33758
33970
|
// Read-only semantic context for the browser judgments (the judgments themselves run in the MCP layer).
|
|
33759
33971
|
app.use('/api/browser', createBrowserAssistRouter({ browserSessions, getTargetPage, normalizeSelector }));
|
|
@@ -33822,6 +34034,8 @@ app.post('/api/browser/sessions', async (req, res) => {
|
|
|
33822
34034
|
};
|
|
33823
34035
|
const result = await createBrowserSession({
|
|
33824
34036
|
url, width, height, screenWidth, screenHeight, deviceScaleFactor, timezone, _realHeaders,
|
|
34037
|
+
// Its deadline bounds the native serve check (installServePortGuard) inside the caller's timeout.
|
|
34038
|
+
browserRequest: req.browserRequest,
|
|
33825
34039
|
// Optional per-session override to target a specific MoreLogin env (multi-identity)
|
|
33826
34040
|
...(connectMode === 'morelogin' ? { connectMode } : {}),
|
|
33827
34041
|
...(moreloginEnvId ? { moreloginEnvId: String(moreloginEnvId) } : {}),
|
|
@@ -33848,7 +34062,7 @@ app.post('/api/browser/sessions', async (req, res) => {
|
|
|
33848
34062
|
});
|
|
33849
34063
|
} catch (err) {
|
|
33850
34064
|
console.error('Browser session create error:', err.message);
|
|
33851
|
-
res.status(500).json(
|
|
34065
|
+
res.status(500).json(browserStartFailure(err));
|
|
33852
34066
|
}
|
|
33853
34067
|
});
|
|
33854
34068
|
|
|
@@ -33873,7 +34087,7 @@ app.post('/api/browser/acquire', async (req, res) => {
|
|
|
33873
34087
|
const clientId = req.body?.clientId;
|
|
33874
34088
|
if (!clientId) return res.status(400).json({ error: 'clientId is required' });
|
|
33875
34089
|
const acq = await acquireSharedBrowserTab({ ownerKey: clientId, ownerType: 'interactive', logTag: 'acquire:interactive' });
|
|
33876
|
-
if (acq.error) return res.status(500).json({ error: acq.error });
|
|
34090
|
+
if (acq.error) return res.status(500).json({ error: acq.error, ...(acq.code ? { code: acq.code, offenders: acq.offenders } : {}) });
|
|
33877
34091
|
res.json({ ok: true, sessionId: acq.browserSessionId, tabId: acq.browserTabId || null });
|
|
33878
34092
|
} catch (err) {
|
|
33879
34093
|
res.status(500).json({ error: err.message });
|
|
@@ -35560,7 +35774,7 @@ app.post('/api/browser/sessions/:id/upload', async (req, res) => {
|
|
|
35560
35774
|
});
|
|
35561
35775
|
|
|
35562
35776
|
// Browser config endpoint
|
|
35563
|
-
app.get('/api/browser/config', (req, res) => {
|
|
35777
|
+
app.get('/api/browser/config', async (req, res) => {
|
|
35564
35778
|
const config = loadBrowserConfig();
|
|
35565
35779
|
const detected = findBrowserExecutable();
|
|
35566
35780
|
// Check if Chrome is currently running (informs profile mirroring warnings)
|
|
@@ -35581,7 +35795,11 @@ app.get('/api/browser/config', (req, res) => {
|
|
|
35581
35795
|
let attachSeeded = false;
|
|
35582
35796
|
try { attachSeeded = existsSync(getAttachProfileRoot()); } catch {}
|
|
35583
35797
|
let moreloginInstalled = false;
|
|
35584
|
-
|
|
35798
|
+
// The exe in a known folder, or a local API that answers (a custom install folder): a short probe only when needed.
|
|
35799
|
+
try {
|
|
35800
|
+
moreloginInstalled = moreLogin.isMoreLoginPresent()
|
|
35801
|
+
|| (await moreLogin.probeMoreLoginApi({ savedCfg: config, timeout: 1500 })).running;
|
|
35802
|
+
} catch {}
|
|
35585
35803
|
const unreadable = browserConfigUnreadable(config);
|
|
35586
35804
|
res.json({ config, detectedPath: detected, chromeRunning, attachSeeded, moreloginInstalled, ...(unreadable && { configError: `browser-config.json is unreadable (${unreadable}); saving these settings replaces it` }) });
|
|
35587
35805
|
});
|
|
@@ -35626,12 +35844,20 @@ app.get('/api/browser/detect-profiles', (req, res) => {
|
|
|
35626
35844
|
app.get('/api/browser/morelogin-envs', async (req, res) => {
|
|
35627
35845
|
try {
|
|
35628
35846
|
const cfg = loadBrowserConfig();
|
|
35629
|
-
const installed = moreLogin.isMoreLoginInstalled().installed;
|
|
35630
35847
|
const port = moreLogin.getMoreLoginPort(cfg);
|
|
35631
|
-
|
|
35848
|
+
// Probe first: the install-folder check knows only the default folders, and an
|
|
35849
|
+
// answering local API proves the app is there wherever it was installed.
|
|
35632
35850
|
const probe = await moreLogin.probeMoreLoginApi({ savedCfg: cfg, port });
|
|
35851
|
+
const installed = probe.running || moreLogin.isMoreLoginInstalled().installed;
|
|
35852
|
+
if (!installed) return res.json({ ok: true, installed: false, running: false, port, envs: [], error: probe.error || null });
|
|
35633
35853
|
if (!probe.running) return res.json({ ok: true, installed: true, running: false, port, envs: [], error: probe.error || null });
|
|
35854
|
+
// The API answered with an error (signature required, API off): say why, never an empty list.
|
|
35855
|
+
if (probe.apiError) return res.json({ ok: false, installed: true, running: true, port, envs: [], error: probe.apiError });
|
|
35634
35856
|
const envs = await moreLogin.listProfiles({ savedCfg: cfg, port });
|
|
35857
|
+
// The API counted profiles but none could be read: a shape problem, never "no profiles yet".
|
|
35858
|
+
if (probe.count > 0 && !envs.length) {
|
|
35859
|
+
return res.json({ ok: false, installed: true, running: true, port, envs: [], error: `MoreLogin reports ${probe.count} profile(s) but SynaBun could not read them from its answer; a newer MoreLogin may have changed its API.` });
|
|
35860
|
+
}
|
|
35635
35861
|
res.json({
|
|
35636
35862
|
ok: true, installed: true, running: true, port, envs,
|
|
35637
35863
|
// Observed website-policy blocks — MoreLogin's local API never exposes the
|
|
@@ -35882,10 +36108,11 @@ const httpServer = app.listen(PORT, async () => {
|
|
|
35882
36108
|
// Activation is an explicit user choice via Settings → Browser / the MoreLogin tab.
|
|
35883
36109
|
setTimeout(async () => {
|
|
35884
36110
|
try {
|
|
35885
|
-
|
|
35886
|
-
const cfg =
|
|
36111
|
+
const mlBootCfg = loadBrowserConfig();
|
|
36112
|
+
const cfg = mlBootCfg;
|
|
35887
36113
|
const port = moreLogin.getMoreLoginPort(cfg);
|
|
35888
36114
|
const probe = await moreLogin.probeMoreLoginApi({ savedCfg: cfg, port });
|
|
36115
|
+
if (!probe.running && !moreLogin.isMoreLoginInstalled().installed) return;
|
|
35889
36116
|
const isDefault = cfg.connectMode === 'morelogin';
|
|
35890
36117
|
console.log(` MoreLogin: detected${probe.running ? ` · API on :${port} (${probe.count} profile(s))` : ' · app not running'}${isDefault ? ' · default AI browser' : ''}`);
|
|
35891
36118
|
} catch { /* best-effort */ }
|
|
@@ -35949,6 +36176,12 @@ const httpServer = app.listen(PORT, async () => {
|
|
|
35949
36176
|
if (oc.updated > 0) console.log(` Session cache (opencode): indexed ${oc.updated}/${oc.sessions}`);
|
|
35950
36177
|
} catch (err) { console.warn(' Session cache (opencode) error:', err.message); }
|
|
35951
36178
|
},
|
|
36179
|
+
() => {
|
|
36180
|
+
const agy = refreshAntigravityCatalog({ force: true });
|
|
36181
|
+
if (agy?.updated > 0) console.log(` Session cache (antigravity): indexed ${agy.updated}/${agy.sessions}`);
|
|
36182
|
+
// Runs and brains end on their own: their conversations are picked up by this pass, every five minutes.
|
|
36183
|
+
setInterval(() => refreshAntigravityCatalog({ force: true }), 5 * 60 * 1000).unref?.();
|
|
36184
|
+
},
|
|
35952
36185
|
];
|
|
35953
36186
|
const next = () => {
|
|
35954
36187
|
const pass = passes.shift();
|
|
@@ -35988,6 +36221,18 @@ const httpServer = app.listen(PORT, async () => {
|
|
|
35988
36221
|
console.warn(' Hook dedup warning:', err.message);
|
|
35989
36222
|
}
|
|
35990
36223
|
|
|
36224
|
+
// Antigravity replaced Gemini CLI: what was saved under `gemini` (a CLI path, a keybind, loop templates and
|
|
36225
|
+
// schedules, the launch profile and resume tab in the UI state) is read as Antigravity's from this boot on
|
|
36226
|
+
// (lib/antigravity/migrate-gemini.js). Idempotent; a file it cannot read is left as it is. Never stops the boot.
|
|
36227
|
+
try {
|
|
36228
|
+
const migrated = migrateGeminiFiles({ dataHome: DATA_HOME, log: (line) => console.log(` ${line}`) });
|
|
36229
|
+
if (migrated.changed.includes('cli-config.json')) { _cliConfigCache = null; cliResolver.invalidate(); }
|
|
36230
|
+
const ui = loadUiState();
|
|
36231
|
+
if (migrateGeminiUiState(ui)) saveUiState(ui);
|
|
36232
|
+
} catch (err) {
|
|
36233
|
+
console.warn(' Antigravity migration warning:', err.message);
|
|
36234
|
+
}
|
|
36235
|
+
|
|
35991
36236
|
// Keep the installed SynaBun rules current: an untouched copy of an older
|
|
35992
36237
|
// ruleset is rewritten (unless the user switched that off), an edited copy is
|
|
35993
36238
|
// left alone, a deleted one counts as an opt-out. Never installs for a tool
|
|
@@ -36003,6 +36248,16 @@ const httpServer = app.listen(PORT, async () => {
|
|
|
36003
36248
|
console.warn(' Rules reconcile warning:', err.message);
|
|
36004
36249
|
}
|
|
36005
36250
|
|
|
36251
|
+
// SynaBun's Antigravity homes (lib/antigravity/home.js): a run's home lives a week past its last use (a run resumes
|
|
36252
|
+
// within minutes of a restart), an Assistant brain's two months (its conversation can be picked up later).
|
|
36253
|
+
try {
|
|
36254
|
+
const root = antigravityHomesRoot(DATA_HOME);
|
|
36255
|
+
const removed = [...pruneAntigravityHomes(root, { kinds: ['run', 'models'], keep: antigravityPanel.activeHomes() }), ...pruneAntigravityHomes(root, { kinds: ['brain'], maxAgeMs: 60 * 24 * 60 * 60_000 })];
|
|
36256
|
+
if (removed.length) console.log(` Antigravity: removed ${removed.length} unused SynaBun home${removed.length === 1 ? '' : 's'}`);
|
|
36257
|
+
} catch (err) {
|
|
36258
|
+
console.warn(' Antigravity homes warning:', err.message);
|
|
36259
|
+
}
|
|
36260
|
+
|
|
36006
36261
|
// Clean up orphaned loop state files from previous server runs.
|
|
36007
36262
|
// After a restart, PTY sessions are gone but loop files remain with awaitingNext: true.
|
|
36008
36263
|
// No terminal session exists to drive them, so delete them to prevent ghost loops.
|
|
@@ -36134,7 +36389,7 @@ httpServer.on('upgrade', function onUpgrade(req, socket, head) {
|
|
|
36134
36389
|
if (guest) {
|
|
36135
36390
|
if (url.pathname.startsWith('/ws/terminal/') && !invitePermissions.terminal) { socket.destroy(); return; }
|
|
36136
36391
|
if (url.pathname.startsWith('/ws/browser/') && !invitePermissions.browser) { socket.destroy(); return; }
|
|
36137
|
-
if (['/ws/claude-skin', '/ws/codex-skin', '/ws/opencode-v2'].includes(url.pathname) || url.pathname.startsWith('/ws/assistant/')) {
|
|
36392
|
+
if (['/ws/claude-skin', '/ws/codex-skin', '/ws/opencode-v2', '/ws/antigravity'].includes(url.pathname) || url.pathname.startsWith('/ws/assistant/')) {
|
|
36138
36393
|
socket.destroy();
|
|
36139
36394
|
return;
|
|
36140
36395
|
}
|
|
@@ -36165,7 +36420,7 @@ httpServer.on('upgrade', function onUpgrade(req, socket, head) {
|
|
|
36165
36420
|
return;
|
|
36166
36421
|
}
|
|
36167
36422
|
|
|
36168
|
-
if (url.pathname.startsWith('/ws/browser/') || url.pathname.startsWith('/ws/assistant/') || url.pathname === '/ws/whiteboard' || url.pathname === '/ws/cards' || url.pathname === '/ws/sync' || url.pathname === '/ws/claude-skin' || url.pathname === '/ws/codex-skin' || url.pathname === '/ws/opencode-v2' || url.pathname === '/ws/sessions') {
|
|
36423
|
+
if (url.pathname.startsWith('/ws/browser/') || url.pathname.startsWith('/ws/assistant/') || url.pathname === '/ws/whiteboard' || url.pathname === '/ws/cards' || url.pathname === '/ws/sync' || url.pathname === '/ws/claude-skin' || url.pathname === '/ws/codex-skin' || url.pathname === '/ws/opencode-v2' || url.pathname === '/ws/antigravity' || url.pathname === '/ws/sessions') {
|
|
36169
36424
|
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
36170
36425
|
wss.emit('connection', ws, req);
|
|
36171
36426
|
});
|
|
@@ -36232,6 +36487,12 @@ wss.on('connection', (ws, req) => {
|
|
|
36232
36487
|
return;
|
|
36233
36488
|
}
|
|
36234
36489
|
|
|
36490
|
+
// ── Route: Antigravity side panel (Google's agy over ACP) ──
|
|
36491
|
+
if (url.pathname === '/ws/antigravity') {
|
|
36492
|
+
antigravityPanel.handleSocket(ws);
|
|
36493
|
+
return;
|
|
36494
|
+
}
|
|
36495
|
+
|
|
36235
36496
|
// ── Terminal sessions ── are handed off to the pty-host in the upgrade
|
|
36236
36497
|
// handler (lib/pty-host); nothing else should reach this point.
|
|
36237
36498
|
try { ws.close(1008, 'Unsupported WebSocket path'); } catch {}
|
|
@@ -36816,11 +37077,15 @@ process.on('exit', () => {
|
|
|
36816
37077
|
try { stopOpencodeServer(); } catch {}
|
|
36817
37078
|
try { editorCompletionServe.stop('exit'); } catch {}
|
|
36818
37079
|
try { _claudeBridge?.shutdownAllBridges?.(); } catch {}
|
|
37080
|
+
try { antigravityPanel.closeAll(); } catch {}
|
|
36819
37081
|
try { saveSessionSnapshot(); } catch {}
|
|
36820
37082
|
try { flushUiStateSync(); } catch {}
|
|
36821
37083
|
try { flushWhiteboardSync(); } catch {}
|
|
36822
37084
|
try { flushCostDataSync(); } catch {}
|
|
36823
37085
|
try { memoryMap.flushSync(); } catch {}
|
|
37086
|
+
// The Assistant's session records (a rename among them): this handler runs before the Assistant's own
|
|
37087
|
+
// exit handler, which the SIGKILL below never lets run.
|
|
37088
|
+
try { _assistantRuntime?.flushIndex?.(); } catch {}
|
|
36824
37089
|
if (_preserveExitCode) return;
|
|
36825
37090
|
try { process.kill(process.pid, 'SIGKILL'); } catch {}
|
|
36826
37091
|
});
|