synabun 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +121 -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 +501 -39
- package/neural-interface/i18n/pt-BR.json +501 -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-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 +207 -49
- 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 +13 -4
- 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 +420 -21
- package/neural-interface/lib/native-loop-runtime.js +23 -2
- package/neural-interface/lib/opencode-native-boundary.js +11 -2
- 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 +32 -2
- 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 +1 -1
- 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-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-panel.js +45 -8
- package/neural-interface/public/shared/assistant/asst-render.js +51 -12
- 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-scenes.js +54 -1
- 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-styles.js +34 -0
- 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-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/ocp-v2/ocp-v2-panel.js +27 -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 +7 -1
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +17 -0
- 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/streaming-markdown.js +58 -0
- package/neural-interface/public/shared/styles.css +354 -34
- 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 +299 -77
- 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-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 +644 -426
- 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
|
@@ -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,
|
|
@@ -203,7 +207,7 @@ import { createOpenCodeActivityRelay } from './lib/opencode-worker-activity.js';
|
|
|
203
207
|
import { browserServePortDenial } from './lib/browser-tool-policy.js';
|
|
204
208
|
import { createNativeServeBoundary, nativeServeCapability } 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,
|
|
@@ -2388,6 +2402,7 @@ function gracefulShutdown(reason = 'shutdown') {
|
|
|
2388
2402
|
}
|
|
2389
2403
|
|
|
2390
2404
|
async function runGracefulShutdown(reason) {
|
|
2405
|
+
gitStatusRunner.shutdown();
|
|
2391
2406
|
console.log(`[server] gracefulShutdown(${reason}) — releasing children...`);
|
|
2392
2407
|
|
|
2393
2408
|
// Native automation providers own SDK subprocesses/isolated serves that are
|
|
@@ -2399,6 +2414,8 @@ async function runGracefulShutdown(reason) {
|
|
|
2399
2414
|
try { keepRuns = _assistantDispatcher?.suspendForShutdown?.(reason) || []; } catch (err) { console.warn(' assistant suspend err:', err?.message); }
|
|
2400
2415
|
try { await _nativeLoopRuntime?.shutdown(reason, { keep: keepRuns }); } catch (err) { console.warn(' native loop stop err:', err?.message); }
|
|
2401
2416
|
try { stopAllIsolatedServes(); } catch (err) { console.warn(' isolated opencode stop err:', err?.message); }
|
|
2417
|
+
// The Antigravity panel's `agy acp` processes (one per open panel socket).
|
|
2418
|
+
try { await antigravityPanel.closeAll(); } catch (err) { console.warn(' antigravity panel stop err:', err?.message); }
|
|
2402
2419
|
// The code editor's completion serve, and today's autocomplete counters.
|
|
2403
2420
|
try { await editorCompletion.stop(); } catch (err) { console.warn(' editor completion stop err:', err?.message); }
|
|
2404
2421
|
|
|
@@ -2818,7 +2835,17 @@ app.post('/api/system/run-update', async (req, res) => {
|
|
|
2818
2835
|
appVersion: installPlan.current,
|
|
2819
2836
|
additionalEntries: getBackupAdditionalEntries(),
|
|
2820
2837
|
}));
|
|
2821
|
-
|
|
2838
|
+
// Older pre-update snapshots are pruned only now, after this one is written and verified: a
|
|
2839
|
+
// failed createVerifiedBackup above threw and never reached here.
|
|
2840
|
+
try {
|
|
2841
|
+
const configuredKeep = loadAutoBackupConfig()?.retention?.preUpdateKeep ?? null;
|
|
2842
|
+
prunePreUpdateSnapshots({
|
|
2843
|
+
folderPath: updateBackupDir,
|
|
2844
|
+
keep: resolvePreUpdateKeep(configuredKeep),
|
|
2845
|
+
keepPath: snapshot.path,
|
|
2846
|
+
log: (message) => console.log(`[update-backup] ${message}`),
|
|
2847
|
+
});
|
|
2848
|
+
} catch (error) { console.error(`[update-backup] retention cleanup failed: ${error?.message || error}`); }
|
|
2822
2849
|
const handoffPath = resolve(DATA_HOME, 'data', 'update-handoff.json');
|
|
2823
2850
|
writeFileSync(handoffPath, JSON.stringify({
|
|
2824
2851
|
version: 1,
|
|
@@ -3656,6 +3683,28 @@ function getClaudeSdkExecutable() {
|
|
|
3656
3683
|
});
|
|
3657
3684
|
}
|
|
3658
3685
|
|
|
3686
|
+
// Google Antigravity's CLI (`agy`): what the side panel (`agy acp`), the workers, the model list and the session list
|
|
3687
|
+
// start. The resolver's answer, as a program and the words before its own arguments: a binary as it is, a script
|
|
3688
|
+
// through node, a PowerShell script through PowerShell. A Windows command file (cmd.exe) cannot take words after the
|
|
3689
|
+
// fact, so it is reported as not runnable here (the terminal profile still runs it). null when none is installed.
|
|
3690
|
+
function antigravityLaunch() {
|
|
3691
|
+
const found = cliResolver.launcher('antigravity');
|
|
3692
|
+
_noteCliLookup('antigravity', found, 'Antigravity CLI');
|
|
3693
|
+
const spec = found.spec;
|
|
3694
|
+
if (!found.path || !spec || found.state === 'broken') return null;
|
|
3695
|
+
if (spec.kind === 'direct') return { command: spec.launchPath || found.path, args: [], version: found.version || null, path: found.path };
|
|
3696
|
+
if (spec.kind === 'node-script') return { command: process.execPath, args: [spec.launchPath], version: found.version || null, path: found.path };
|
|
3697
|
+
if (spec.kind === 'powershell') return { command: 'powershell.exe', args: ['-NoProfile', '-NonInteractive', '-File', spec.launchPath], version: found.version || null, path: found.path };
|
|
3698
|
+
return null;
|
|
3699
|
+
}
|
|
3700
|
+
// The environment an Antigravity process gets: the PATH a new terminal has (agy runs the user's tools), never the
|
|
3701
|
+
// owner's live data paths or a brain's credential.
|
|
3702
|
+
function antigravityChildEnv() {
|
|
3703
|
+
const env = withPathEnv({ ...process.env }, getAugmentedPath());
|
|
3704
|
+
for (const key of ['SQLITE_DB_PATH', 'MEMORY_DATA_DIR', 'SYNABUN_BRAIN_TOKEN', 'SYNABUN_ASSISTANT_SESSION', 'SYNABUN_ASSISTANT_THREADS']) delete env[key];
|
|
3705
|
+
return env;
|
|
3706
|
+
}
|
|
3707
|
+
|
|
3659
3708
|
// Codex: what the SDK and the app-server start. On Windows the native codex.exe
|
|
3660
3709
|
// an npm command file leads to (the SDK spawns without a shell); the file found
|
|
3661
3710
|
// otherwise. SynaBun carries no Codex (lib/external-tools.js): without one,
|
|
@@ -5626,10 +5675,10 @@ CRITICAL — When you need clarification during planning:
|
|
|
5626
5675
|
return item;
|
|
5627
5676
|
}
|
|
5628
5677
|
|
|
5629
|
-
function
|
|
5630
|
-
if (!threadId) return
|
|
5678
|
+
function codexRolloutPathFor(threadId) {
|
|
5679
|
+
if (!threadId) return '';
|
|
5631
5680
|
const dbPath = resolve(activeCodexHome, 'state_5.sqlite');
|
|
5632
|
-
if (!existsSync(dbPath)) return
|
|
5681
|
+
if (!existsSync(dbPath)) return '';
|
|
5633
5682
|
|
|
5634
5683
|
let db = null;
|
|
5635
5684
|
let rolloutPath = '';
|
|
@@ -5642,20 +5691,27 @@ CRITICAL — When you need clarification during planning:
|
|
|
5642
5691
|
} finally {
|
|
5643
5692
|
try { db?.close(); } catch {}
|
|
5644
5693
|
}
|
|
5645
|
-
|
|
5694
|
+
return rolloutPath && existsSync(rolloutPath) ? rolloutPath : '';
|
|
5695
|
+
}
|
|
5646
5696
|
|
|
5647
|
-
|
|
5697
|
+
// The rollout as history items: read as a stream, parsed once while the file is unchanged
|
|
5698
|
+
// (lib/codex-rollout-reader.js), so the early preview and the fallback that follows share one read.
|
|
5699
|
+
async function loadCodexThreadHistoryFallback(threadId) {
|
|
5700
|
+
const rolloutPath = codexRolloutPathFor(threadId);
|
|
5701
|
+
if (!rolloutPath) return [];
|
|
5648
5702
|
try {
|
|
5649
|
-
|
|
5703
|
+
return await codexRolloutReader.items(rolloutPath, parseCodexRolloutLines);
|
|
5650
5704
|
} catch (err) {
|
|
5651
5705
|
console.warn('[codex-skin] Failed to read rollout history:', err.message);
|
|
5652
5706
|
return [];
|
|
5653
5707
|
}
|
|
5708
|
+
}
|
|
5654
5709
|
|
|
5710
|
+
async function parseCodexRolloutLines(lines) {
|
|
5655
5711
|
const items = [];
|
|
5656
5712
|
const calls = new Map();
|
|
5657
5713
|
let seq = 0;
|
|
5658
|
-
for (const line of
|
|
5714
|
+
for await (const line of lines) {
|
|
5659
5715
|
if (!line.trim()) continue;
|
|
5660
5716
|
let entry;
|
|
5661
5717
|
try {
|
|
@@ -6178,7 +6234,29 @@ CRITICAL — When you need clarification during planning:
|
|
|
6178
6234
|
}
|
|
6179
6235
|
|
|
6180
6236
|
|
|
6181
|
-
|
|
6237
|
+
// Only for the thread the client shows (it asks with `preview: true`), only before the app-server is up, read
|
|
6238
|
+
// without blocking its start; dropped once the real history went out. The read is reused by the fallback.
|
|
6239
|
+
function sendEarlyCodexRolloutPreview(threadId, state) {
|
|
6240
|
+
if (!threadId || initialized) return null;
|
|
6241
|
+
const job = loadCodexThreadHistoryFallback(threadId);
|
|
6242
|
+
job.then((previewItems) => {
|
|
6243
|
+
if (state.historySent || !Array.isArray(previewItems) || !previewItems.length) return;
|
|
6244
|
+
const previewThreadId = String(threadId);
|
|
6245
|
+
sendToClient({
|
|
6246
|
+
type: 'history',
|
|
6247
|
+
preview: true,
|
|
6248
|
+
threadId: previewThreadId,
|
|
6249
|
+
thread: { id: previewThreadId, turns: [] },
|
|
6250
|
+
fallbackItems: previewItems,
|
|
6251
|
+
historyMode: 'read',
|
|
6252
|
+
});
|
|
6253
|
+
}).catch(() => {});
|
|
6254
|
+
return job;
|
|
6255
|
+
}
|
|
6256
|
+
|
|
6257
|
+
async function bootstrapThread(threadId, cwd = PACKAGE_ROOT, { preview = false } = {}) {
|
|
6258
|
+
const previewState = { historySent: false };
|
|
6259
|
+
if (preview) sendEarlyCodexRolloutPreview(threadId, previewState);
|
|
6182
6260
|
await ensureInitialized();
|
|
6183
6261
|
if (!threadId) {
|
|
6184
6262
|
displayedThreadId = activeThreadId;
|
|
@@ -6193,11 +6271,13 @@ CRITICAL — When you need clarification during planning:
|
|
|
6193
6271
|
const historyMode = activeThreadId && effectiveThreadId && activeThreadId === effectiveThreadId
|
|
6194
6272
|
? 'resume'
|
|
6195
6273
|
: 'read';
|
|
6274
|
+
const fallbackItems = await loadCodexThreadHistoryFallback(effectiveThreadId);
|
|
6275
|
+
previewState.historySent = true;
|
|
6196
6276
|
sendToClient({
|
|
6197
6277
|
type: 'history',
|
|
6198
6278
|
threadId: effectiveThreadId,
|
|
6199
6279
|
thread: sanitizeCodexThreadForClient(thread),
|
|
6200
|
-
fallbackItems
|
|
6280
|
+
fallbackItems,
|
|
6201
6281
|
historyMode,
|
|
6202
6282
|
});
|
|
6203
6283
|
// `history` and `ready` must describe the same displayed thread. Returning
|
|
@@ -6206,6 +6286,7 @@ CRITICAL — When you need clarification during planning:
|
|
|
6206
6286
|
sendToClient({ type: 'ready', threadId: effectiveThreadId });
|
|
6207
6287
|
} catch (err) {
|
|
6208
6288
|
console.warn('[codex-skin] Failed to resume thread:', threadId, err.message);
|
|
6289
|
+
previewState.historySent = true;
|
|
6209
6290
|
sendToClient({ type: 'history', thread: null, resumeFailed: true, threadId });
|
|
6210
6291
|
sendToClient({ type: 'error', message: `Could not restore saved Codex thread. ${err.message}` });
|
|
6211
6292
|
sendToClient({ type: 'ready', threadId });
|
|
@@ -6318,7 +6399,7 @@ CRITICAL — When you need clarification during planning:
|
|
|
6318
6399
|
const runtime = opts.runtimeOptions || await resolveCodexThreadRuntimeOptions(opts);
|
|
6319
6400
|
displayedThreadId = String(threadId);
|
|
6320
6401
|
const readThread = await readThreadHistory(threadId).catch(() => null);
|
|
6321
|
-
const fallbackItems = loadCodexThreadHistoryFallback(threadId);
|
|
6402
|
+
const fallbackItems = await loadCodexThreadHistoryFallback(threadId);
|
|
6322
6403
|
const previewThread = readThread || (fallbackItems.length ? { id: threadId, turns: [] } : null);
|
|
6323
6404
|
if (previewThread) {
|
|
6324
6405
|
sendToClient({
|
|
@@ -6783,7 +6864,7 @@ CRITICAL — When you need clarification during planning:
|
|
|
6783
6864
|
}
|
|
6784
6865
|
// Resolve the tab's account BEFORE first spawn so CODEX_HOME is correct.
|
|
6785
6866
|
if (!child && msg.accountId) setActiveCodexAccount(msg.accountId);
|
|
6786
|
-
await bootstrapThread(msg.threadId || null, msg.cwd || PACKAGE_ROOT);
|
|
6867
|
+
await bootstrapThread(msg.threadId || null, msg.cwd || PACKAGE_ROOT, { preview: msg.preview === true });
|
|
6787
6868
|
return;
|
|
6788
6869
|
}
|
|
6789
6870
|
if (msg.type === 'permission_roots_list') {
|
|
@@ -9023,6 +9104,30 @@ setInterval(() => {
|
|
|
9023
9104
|
|
|
9024
9105
|
process.once('exit', stopAllIsolatedServes);
|
|
9025
9106
|
|
|
9107
|
+
// ── Antigravity side panel (/ws/antigravity) ──
|
|
9108
|
+
// One `agy acp` (Google's Antigravity ACP server) per panel socket, one ACP session per tab
|
|
9109
|
+
// (lib/antigravity/panel-bridge.js; docs/antigravity-sidepanel.md). Pasted attachments are written under the data
|
|
9110
|
+
// home for the agent to read; nothing reaches Antigravity's own folders.
|
|
9111
|
+
const antigravityPanel = createAntigravityPanelBridge({
|
|
9112
|
+
resolveLaunch: async () => antigravityLaunch(),
|
|
9113
|
+
childEnv: () => antigravityChildEnv(),
|
|
9114
|
+
attachmentsRoot: resolve(DATA_HOME, 'data', 'antigravity', 'attachments'),
|
|
9115
|
+
homesRoot: () => antigravityHomesRoot(DATA_HOME),
|
|
9116
|
+
isRunHomeActive: (homeDir) => (_nativeLoopRuntime?.list({ activeOnly: true }) || []).some((run) => run.provider === 'antigravity' && run.antigravityHome === homeDir && (run.status === 'starting' || run.status === 'running')),
|
|
9117
|
+
// Bypass keeps the browser policy: a test runner passes from inside a registered code project (lib/antigravity/bypass.js).
|
|
9118
|
+
policyRoots: () => { try { return browserPolicyRoots(loadHookProjects()); } catch { return []; } },
|
|
9119
|
+
clientInfo: { name: 'synabun', title: 'SynaBun', version: String(process.env.npm_package_version || '0') },
|
|
9120
|
+
// Greeting (lib/greeting/): a prompt typed in the panel is noted, and the greeting a tab opened with is told to
|
|
9121
|
+
// the model once, before the typed words (public/shared/agy/agy-greeting.js markers).
|
|
9122
|
+
onHumanPrompt: ({ cwd, words }) => { try { _greeting?.noteActivity({ cwd: cwd || PACKAGE_ROOT, surface: 'antigravity-panel', words }); } catch {} },
|
|
9123
|
+
greetingNote: (id, opts) => _greeting?.consumeNote(id, opts) || '',
|
|
9124
|
+
// `/synabun …` reads the bundled skill, as the Codex panel's does (lib/skill-prompt-bridge.js).
|
|
9125
|
+
expandPrompt: (text) => expandSynabunCodexSlashPrompt(text, join(SKILLS_SOURCE_DIR, 'synabun', 'SKILL.md')),
|
|
9126
|
+
// A turn that ended may have added to the conversation's transcript: Resume search and memory sessions follow.
|
|
9127
|
+
onTurnEnd: ({ sessionId }) => { try { refreshAntigravitySession(sessionId); } catch {} },
|
|
9128
|
+
log: (line) => console.log(line),
|
|
9129
|
+
});
|
|
9130
|
+
|
|
9026
9131
|
function handleOpencodeV2Ws(ws) {
|
|
9027
9132
|
_ocpV2WsClients.add(ws);
|
|
9028
9133
|
ws._nativeLoopWindowId = null;
|
|
@@ -11001,7 +11106,7 @@ const _codexSettingsProbes = createLaunchCache({
|
|
|
11001
11106
|
});
|
|
11002
11107
|
setCodexSettingsStore({ get: (key) => _codexSettingsProbes.peek(key), set: (key, value) => _codexSettingsProbes.set(key, value) });
|
|
11003
11108
|
|
|
11004
|
-
async function getClaudeModelsForClient(force = false) {
|
|
11109
|
+
async function getClaudeModelsForClient(force = false, { nonBlocking = false } = {}) {
|
|
11005
11110
|
try {
|
|
11006
11111
|
const bin = getClaudeBin();
|
|
11007
11112
|
// Sidepanel sessions and native loops run this same installation. The Agent
|
|
@@ -11010,8 +11115,9 @@ async function getClaudeModelsForClient(force = false) {
|
|
|
11010
11115
|
// when the CLI changes).
|
|
11011
11116
|
checkClaudeCliSkew(bin).catch(() => {});
|
|
11012
11117
|
// 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
|
-
|
|
11118
|
+
// only a first read, another binary or account, or ?refresh=1 waits for the CLI, unless
|
|
11119
|
+
// nonBlocking=true (e.g. cold /api/claude/config boot on Windows) warms it asynchronously.
|
|
11120
|
+
const result = await discoverClaudeModels(bin, { force, nonBlocking, cwd: PACKAGE_ROOT, account: claudeModelAccountKey() });
|
|
11015
11121
|
if (result?.models?.length) return result;
|
|
11016
11122
|
} catch (err) {
|
|
11017
11123
|
console.warn('[claude-models] discovery failed:', err.message);
|
|
@@ -11039,7 +11145,11 @@ app.get('/api/claude/config', async (req, res) => {
|
|
|
11039
11145
|
// Model ids come straight from the CLI and are already spawn-ready (they carry
|
|
11040
11146
|
// the `[1m]` suffix where applicable), so the client uses them verbatim — no
|
|
11041
11147
|
// "<id>:<contextWindow>" composite. See toCliModelName() for the legacy form.
|
|
11042
|
-
|
|
11148
|
+
// Default to non-blocking model discovery so cold panel opens on Windows never stall
|
|
11149
|
+
// behind a 3–7s `claude.exe` spawn; ui-claude-panel.js refreshes fallback models async.
|
|
11150
|
+
const { source, models } = await getClaudeModelsForClient(false, {
|
|
11151
|
+
nonBlocking: req.query?.waitModels !== '1',
|
|
11152
|
+
});
|
|
11043
11153
|
res.json({ ok: true, config: cliCfg, projects, models, modelsSource: source, bootId: SERVER_BOOT_ID });
|
|
11044
11154
|
} catch (err) {
|
|
11045
11155
|
res.status(500).json({ error: err.message });
|
|
@@ -14372,9 +14482,15 @@ function isSidepanelClaimTokenRevoked(windowId, claimToken) {
|
|
|
14372
14482
|
|
|
14373
14483
|
// Native provider run ledger. A sidepanel can attach after a page refresh or
|
|
14374
14484
|
// after a schedule started while no Neural Interface window was open.
|
|
14485
|
+
function withSidepanelRunHome(run) {
|
|
14486
|
+
if (!run || run.provider !== 'antigravity' || run.antigravityHome || !run.runId) return run;
|
|
14487
|
+
const home = _assistantDispatcher?.get?.(run.runId)?.antigravityHome || null;
|
|
14488
|
+
return home ? { ...run, antigravityHome: home } : run;
|
|
14489
|
+
}
|
|
14490
|
+
|
|
14375
14491
|
app.get('/api/sidepanel-runs', (req, res) => {
|
|
14376
14492
|
const activeOnly = req.query.active === '1' || req.query.active === 'true';
|
|
14377
|
-
res.json({ ok: true, runs: _nativeLoopRuntime?.list({ activeOnly }) || [] });
|
|
14493
|
+
res.json({ ok: true, runs: (_nativeLoopRuntime?.list({ activeOnly }) || []).map(withSidepanelRunHome) });
|
|
14378
14494
|
});
|
|
14379
14495
|
|
|
14380
14496
|
app.post('/api/sidepanel-runs/release-window', (req, res) => {
|
|
@@ -14386,7 +14502,7 @@ app.post('/api/sidepanel-runs/release-window', (req, res) => {
|
|
|
14386
14502
|
app.get('/api/sidepanel-runs/:runId', (req, res) => {
|
|
14387
14503
|
const run = _nativeLoopRuntime?.get(req.params.runId);
|
|
14388
14504
|
if (!run) return res.status(404).json({ error: 'Native sidepanel run not found' });
|
|
14389
|
-
res.json({ ok: true, run });
|
|
14505
|
+
res.json({ ok: true, run: withSidepanelRunHome(run) });
|
|
14390
14506
|
});
|
|
14391
14507
|
|
|
14392
14508
|
app.post('/api/sidepanel-runs/:runId/claim', (req, res) => {
|
|
@@ -14395,7 +14511,7 @@ app.post('/api/sidepanel-runs/:runId/claim', (req, res) => {
|
|
|
14395
14511
|
}
|
|
14396
14512
|
const result = _nativeLoopRuntime?.claim(req.params.runId, req.body?.windowId);
|
|
14397
14513
|
if (!result?.ok) return res.status(result?.reason === 'not_found' ? 404 : 409).json(result || { ok: false });
|
|
14398
|
-
res.json(result);
|
|
14514
|
+
res.json(result.run ? { ...result, run: withSidepanelRunHome(result.run) } : result);
|
|
14399
14515
|
});
|
|
14400
14516
|
|
|
14401
14517
|
app.post('/api/sidepanel-runs/:runId/release', (req, res) => {
|
|
@@ -14533,7 +14649,7 @@ app.post('/api/loop/launch', async (req, res) => {
|
|
|
14533
14649
|
// .claude/ set awaitingNext and attachLoopDriver() drives /clear + re-prompt; if those
|
|
14534
14650
|
// hooks are absent (e.g. a claude-code task run under a hookless harness) the loop STALLS.
|
|
14535
14651
|
// 'exec': harness-agnostic — attachExecLoopDriver() owns iteration via sentinel echoes, so
|
|
14536
|
-
// OpenCode/Codex/
|
|
14652
|
+
// OpenCode/Codex/Antigravity self-advance. Facebook seeding must stay profile:opencode for this.
|
|
14537
14653
|
driverType: nativeRuntime ? 'native' : (cliProfile === 'claude-code' ? 'hook' : 'exec'),
|
|
14538
14654
|
runtimeType: nativeRuntime ? 'native' : 'pty',
|
|
14539
14655
|
surface: nativeRuntime ? 'sidepanel' : 'terminal',
|
|
@@ -14652,12 +14768,12 @@ app.post('/api/loop/launch', async (req, res) => {
|
|
|
14652
14768
|
loopLog(terminalSessionId, 'launch:driver', 'attaching hook-driven loop driver (claude-code)');
|
|
14653
14769
|
attachLoopDriver(terminalSessionId);
|
|
14654
14770
|
} else {
|
|
14655
|
-
// Codex/
|
|
14771
|
+
// Codex/Antigravity/other: spawn shell, server drives `<cli> exec` commands per iteration.
|
|
14656
14772
|
// The driver invokes a platform-native wrapper that captures the CLI
|
|
14657
14773
|
// status and emits a sentinel. Immune to PS1/PROMPT overrides.
|
|
14658
14774
|
loopLog(terminalSessionId, 'launch:spawn', 'creating shell PTY for exec-driven CLI', { profile: cliProfile, cwd: loopCwd });
|
|
14659
14775
|
createTerminalSession('shell', 120, 30, loopCwd, { extraEnv: loopExtraEnv, sessionId: terminalSessionId });
|
|
14660
|
-
loopLog(terminalSessionId, 'launch:driver', 'attaching exec loop driver (codex/
|
|
14776
|
+
loopLog(terminalSessionId, 'launch:driver', 'attaching exec loop driver (codex/antigravity/other)', { profile: cliProfile });
|
|
14661
14777
|
attachExecLoopDriver(terminalSessionId);
|
|
14662
14778
|
}
|
|
14663
14779
|
|
|
@@ -14774,7 +14890,7 @@ app.post('/api/loop/stop', async (req, res) => {
|
|
|
14774
14890
|
});
|
|
14775
14891
|
|
|
14776
14892
|
// POST /api/loop/resolve-from-ancestors — process-tree fallback for MCP env loss.
|
|
14777
|
-
// The MCP server walks its ancestor PID chain (codex/opencode/
|
|
14893
|
+
// The MCP server walks its ancestor PID chain (codex/opencode/antigravity → shell PTY)
|
|
14778
14894
|
// and POSTs the chain here. We match any PID against terminalSessions[*].pty.pid
|
|
14779
14895
|
// and return that loop's browserSessionId/browserTabId. Layer B of the
|
|
14780
14896
|
// defense-in-depth browser tab isolation fix.
|
|
@@ -15570,7 +15686,7 @@ async function launchScheduledLoop(schedule) {
|
|
|
15570
15686
|
}
|
|
15571
15687
|
|
|
15572
15688
|
// Native providers run headlessly in their durable provider conversation.
|
|
15573
|
-
//
|
|
15689
|
+
// Unsupported profiles retain the existing PTY drivers.
|
|
15574
15690
|
let runDescriptor = null;
|
|
15575
15691
|
if (nativeRuntime) {
|
|
15576
15692
|
const presentation = resolveScheduleRunPresentation(
|
|
@@ -17499,7 +17615,7 @@ function isSynabunGitHubRemote(remoteText) {
|
|
|
17499
17615
|
return /github\.com[:/]danilokhury\/Synabun(?:\.git)?(?:[\s)]|$)/i.test(String(remoteText || ''));
|
|
17500
17616
|
}
|
|
17501
17617
|
|
|
17502
|
-
function detectSynabunInstallSource() {
|
|
17618
|
+
async function detectSynabunInstallSource() {
|
|
17503
17619
|
if (PACKAGED_RUNTIME) return { kind: 'packaged-app' };
|
|
17504
17620
|
const normalizedRoot = PACKAGE_ROOT.replace(/\\/g, '/');
|
|
17505
17621
|
if (/\/node_modules\/synabun$/i.test(normalizedRoot)) {
|
|
@@ -17509,13 +17625,13 @@ function detectSynabunInstallSource() {
|
|
|
17509
17625
|
try {
|
|
17510
17626
|
// Run as reads (lib/git-read.js): the app's own checkout is a folder like any other, and can be a registered
|
|
17511
17627
|
// project. What git could not say is an empty answer.
|
|
17512
|
-
const git = (args) => {
|
|
17513
|
-
const result = spawnGitRead(args, { cwd: PACKAGE_ROOT, timeout: 2000 });
|
|
17628
|
+
const git = async (args) => {
|
|
17629
|
+
const result = await spawnGitRead(args, { cwd: PACKAGE_ROOT, timeout: 2000 });
|
|
17514
17630
|
return result.status === 0 ? result.stdout.toString().trim() : '';
|
|
17515
17631
|
};
|
|
17516
|
-
if (git(['rev-parse', '--is-inside-work-tree']) === 'true') {
|
|
17517
|
-
const remote = git(['remote', 'get-url', 'origin']);
|
|
17518
|
-
const remotes = git(['remote', '-v']);
|
|
17632
|
+
if (await git(['rev-parse', '--is-inside-work-tree']) === 'true') {
|
|
17633
|
+
const remote = await git(['remote', 'get-url', 'origin']);
|
|
17634
|
+
const remotes = await git(['remote', '-v']);
|
|
17519
17635
|
if (isSynabunGitHubRemote(`${remote}\n${remotes}`)) {
|
|
17520
17636
|
const firstRemote = remote || remotes.split(/\s+/).find(part => isSynabunGitHubRemote(part)) || null;
|
|
17521
17637
|
return { kind: 'github-clone', remote: firstRemote };
|
|
@@ -17600,7 +17716,7 @@ async function checkSynabunUpdate() {
|
|
|
17600
17716
|
const installPlan = buildSynabunInstallPlan({
|
|
17601
17717
|
...found,
|
|
17602
17718
|
current,
|
|
17603
|
-
installSource: updateAvailable ? detectSynabunInstallSource() : null,
|
|
17719
|
+
installSource: updateAvailable ? await detectSynabunInstallSource() : null,
|
|
17604
17720
|
repoUrl: SYNABUN_GITHUB_REPO_URL,
|
|
17605
17721
|
});
|
|
17606
17722
|
|
|
@@ -17742,6 +17858,8 @@ function systemActivitySources() {
|
|
|
17742
17858
|
const blockers = getOpenCodeHistoryBlockers();
|
|
17743
17859
|
return blockers.activeTurns + blockers.startingTurns;
|
|
17744
17860
|
},
|
|
17861
|
+
// A prompt running in an Antigravity panel tab (lib/antigravity/panel-bridge.js); dispatched runs are under assistant.
|
|
17862
|
+
'antigravity-panel': () => antigravityPanel.turnsRunning(),
|
|
17745
17863
|
'claude-cli': () => recentCliSessions(sessionRegistry),
|
|
17746
17864
|
loop: () => {
|
|
17747
17865
|
// A dispatched worker also runs in the native runtime: it is counted under assistant.
|
|
@@ -17770,7 +17888,9 @@ app.get('/api/system/activity', (req, res) => {
|
|
|
17770
17888
|
const TOOL_DEFS = [
|
|
17771
17889
|
{ key: 'claude-code', cmd: 'claude', versionArg: '--version', npm: '@anthropic-ai/claude-code' },
|
|
17772
17890
|
{ key: 'codex', cmd: 'codex', versionArg: '--version', npm: '@openai/codex' },
|
|
17773
|
-
|
|
17891
|
+
// Google's Antigravity CLI has no npm package: a Homebrew cask's newer version is what `brew outdated` says, and
|
|
17892
|
+
// the CLI updates itself (`agy update`).
|
|
17893
|
+
{ key: 'antigravity', cmd: 'agy', versionArg: '--version' },
|
|
17774
17894
|
// npm's opencode-ai is OpenCode 1: OpenCode 2's newer version is what Homebrew says (a brew install) or what OpenCode
|
|
17775
17895
|
// itself announces in its own interface.
|
|
17776
17896
|
{ key: 'opencode', cmd: 'opencode', versionArg: '--version' },
|
|
@@ -17970,7 +18090,11 @@ async function checkToolVersions({ force = false } = {}) {
|
|
|
17970
18090
|
|| 'unknown';
|
|
17971
18091
|
for (const [key, info] of Object.entries(results)) {
|
|
17972
18092
|
const source = info.installed ? info.installSource : dominantSource;
|
|
17973
|
-
|
|
18093
|
+
// The Antigravity CLI: the command of the install its button runs here (Homebrew's cask on a Mac that has
|
|
18094
|
+
// Homebrew, else Google's installer), so the banner never shows one command and runs another.
|
|
18095
|
+
info.installCommand = key === 'antigravity' && info.install?.available
|
|
18096
|
+
? cliInstaller.manualCommand(key)
|
|
18097
|
+
: buildInstallCommand(key, source, { platform: process.platform });
|
|
17974
18098
|
}
|
|
17975
18099
|
|
|
17976
18100
|
// A check started before a newer refresh must never publish its stale answer.
|
|
@@ -18039,7 +18163,7 @@ app.use('/api/cli/path', createCliPathApi({
|
|
|
18039
18163
|
pathManager: cliPathManager, resolver: cliResolver, isGuestRequest, port: () => PORT,
|
|
18040
18164
|
diagnostics: async () => {
|
|
18041
18165
|
const fresh = await freshTerminalPath();
|
|
18042
|
-
const statuses = await cliResolver.resolveAll(['claude-code', 'codex', 'opencode']);
|
|
18166
|
+
const statuses = await cliResolver.resolveAll(['claude-code', 'codex', 'opencode', 'antigravity']);
|
|
18043
18167
|
return { platform: process.platform, pathKey: Object.keys(fresh.env).find(key => key.toUpperCase() === 'PATH'),
|
|
18044
18168
|
entries: fresh.entries, os: fresh.os, pathext: fresh.env.PATHEXT || fresh.env.PathExt || null,
|
|
18045
18169
|
tools: Object.fromEntries(Object.entries(statuses).map(([tool, status]) => [tool, { path: status.path, launchPath: status.launchPath, ...cliPathManager.inspect(status) }])) };
|
|
@@ -18753,7 +18877,7 @@ app.get('/api/project-files', editorOwnerGuard, async (req, res) => {
|
|
|
18753
18877
|
}
|
|
18754
18878
|
|
|
18755
18879
|
// Git info for status badges
|
|
18756
|
-
const git = getGitInfo(normalizedDir);
|
|
18880
|
+
const git = await getGitInfo(normalizedDir, { signal: gitRequestSignal(res) });
|
|
18757
18881
|
|
|
18758
18882
|
if (recursive || (search && search.length > 2)) {
|
|
18759
18883
|
// Recursive search mode
|
|
@@ -19171,8 +19295,9 @@ app.put('/api/morelogin/config', (req, res) => {
|
|
|
19171
19295
|
app.post('/api/morelogin/test', async (req, res) => {
|
|
19172
19296
|
try {
|
|
19173
19297
|
const status = await moreLogin.getMoreLoginStatus(_moreLoginOpts());
|
|
19174
|
-
if (!status.installed) return res.json({ ok: false, error:
|
|
19298
|
+
if (!status.installed) return res.json({ ok: false, error: `MoreLogin desktop app was not found and nothing answers on port ${status.port}.` });
|
|
19175
19299
|
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).` });
|
|
19300
|
+
if (status.apiError) return res.json({ ok: false, error: status.apiError });
|
|
19176
19301
|
res.json({ ok: true, port: status.port, profileCount: status.profileCount });
|
|
19177
19302
|
} catch (err) {
|
|
19178
19303
|
res.json({ ok: false, error: err.message });
|
|
@@ -19268,6 +19393,25 @@ function _normalizeMcpEntry(input) {
|
|
|
19268
19393
|
return { command, args, env, url, type };
|
|
19269
19394
|
}
|
|
19270
19395
|
|
|
19396
|
+
// ── Antigravity's MCP configuration (~/.gemini/config/mcp_config.json) ──
|
|
19397
|
+
// The one file `agy mcp add` writes and both the Antigravity CLI and app read: { mcpServers: { name: { command, args,
|
|
19398
|
+
// env, disabled } | { serverUrl, headers, disabled } } }. Antigravity creates it empty; an empty file reads as {}.
|
|
19399
|
+
function antigravityConfigDir() { return join(getHomePath(), '.gemini', 'config'); }
|
|
19400
|
+
function antigravityMcpConfigPath() { return join(antigravityConfigDir(), 'mcp_config.json'); }
|
|
19401
|
+
/** The parsed file ({} when missing or empty); throws on a file that is not a JSON object, so nothing overwrites it. */
|
|
19402
|
+
function readAntigravityMcpConfig() {
|
|
19403
|
+
const path = antigravityMcpConfigPath();
|
|
19404
|
+
if (!existsSync(path)) return {};
|
|
19405
|
+
return parseAntigravityMcpConfig(readFileSync(path, 'utf8'));
|
|
19406
|
+
}
|
|
19407
|
+
function writeAntigravityMcpConfig(data) {
|
|
19408
|
+
const path = antigravityMcpConfigPath();
|
|
19409
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
19410
|
+
const tmp = `${path}.synabun-${process.pid}.tmp`;
|
|
19411
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 });
|
|
19412
|
+
renameSync(tmp, path);
|
|
19413
|
+
}
|
|
19414
|
+
|
|
19271
19415
|
function _writeMoreLoginMcpToAgents(entry) {
|
|
19272
19416
|
const targets = [];
|
|
19273
19417
|
const home = getHomePath();
|
|
@@ -19283,18 +19427,16 @@ function _writeMoreLoginMcpToAgents(entry) {
|
|
|
19283
19427
|
targets.push('Claude Code');
|
|
19284
19428
|
}
|
|
19285
19429
|
} 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');
|
|
19430
|
+
try { // Antigravity (~/.gemini/config/mcp_config.json, the CLI's and the app's): only where Antigravity has run
|
|
19431
|
+
if (existsSync(antigravityConfigDir())) {
|
|
19432
|
+
const data = readAntigravityMcpConfig();
|
|
19433
|
+
data.mcpServers = { ...(data.mcpServers || {}), [MORELOGIN_MCP_KEY]: buildAntigravityMcpEntry(entry.url
|
|
19434
|
+
? { type: entry.type || 'http', url: entry.url }
|
|
19435
|
+
: { command: entry.command, args: entry.args || [], env: entry.env }) };
|
|
19436
|
+
writeAntigravityMcpConfig(data);
|
|
19437
|
+
targets.push('Antigravity');
|
|
19296
19438
|
}
|
|
19297
|
-
} catch (e) { console.warn('[morelogin]
|
|
19439
|
+
} catch (e) { console.warn('[morelogin] Antigravity register failed:', e.message); }
|
|
19298
19440
|
try { // Codex (~/.codex/config.toml) — stdio only
|
|
19299
19441
|
const p = join(home, '.codex', 'config.toml');
|
|
19300
19442
|
if (existsSync(p) && entry.command) {
|
|
@@ -19332,13 +19474,11 @@ function _removeMoreLoginMcpFromAgents() {
|
|
|
19332
19474
|
}
|
|
19333
19475
|
} catch {}
|
|
19334
19476
|
try {
|
|
19335
|
-
|
|
19336
|
-
|
|
19337
|
-
const data = JSON.parse(readFileSync(p, 'utf8') || '{}');
|
|
19477
|
+
if (existsSync(antigravityMcpConfigPath())) {
|
|
19478
|
+
const data = readAntigravityMcpConfig();
|
|
19338
19479
|
if (data.mcpServers && data.mcpServers[MORELOGIN_MCP_KEY]) {
|
|
19339
|
-
|
|
19340
|
-
|
|
19341
|
-
targets.push('Gemini');
|
|
19480
|
+
writeAntigravityMcpConfig(removeAntigravityMcpServer(data, MORELOGIN_MCP_KEY));
|
|
19481
|
+
targets.push('Antigravity');
|
|
19342
19482
|
}
|
|
19343
19483
|
}
|
|
19344
19484
|
} catch {}
|
|
@@ -21164,10 +21304,106 @@ app.get('/api/opencode/sessions', (req, res) => {
|
|
|
21164
21304
|
}
|
|
21165
21305
|
});
|
|
21166
21306
|
|
|
21167
|
-
// GET /api/
|
|
21168
|
-
|
|
21169
|
-
|
|
21170
|
-
|
|
21307
|
+
// GET /api/antigravity/sessions — Antigravity's own conversations (ACP session/list on a short-lived `agy acp`),
|
|
21308
|
+
// grouped under the registered projects. A missing or signed-out CLI lists nothing and says why.
|
|
21309
|
+
let _antigravitySessionsCache = null; // { at, value }
|
|
21310
|
+
app.get('/api/antigravity/sessions', async (req, res) => {
|
|
21311
|
+
try {
|
|
21312
|
+
const projects = loadHookProjects();
|
|
21313
|
+
const limit = Math.min(parseInt(req.query.limit) || 30, 100);
|
|
21314
|
+
const offset = parseInt(req.query.offset) || 0;
|
|
21315
|
+
const search = (req.query.search || '').toString();
|
|
21316
|
+
const fresh = _antigravitySessionsCache && Date.now() - _antigravitySessionsCache.at < 15_000 && req.query.refresh !== '1';
|
|
21317
|
+
const listed = fresh ? _antigravitySessionsCache.value : await listAntigravitySessions({ launch: antigravityLaunch(), env: antigravityChildEnv() });
|
|
21318
|
+
if (!fresh && !listed.error) _antigravitySessionsCache = { at: Date.now(), value: listed };
|
|
21319
|
+
res.json({
|
|
21320
|
+
projects: groupSessionsByProject(listed.sessions, projects, { targetProject: req.query.project || null, search, offset, limit }),
|
|
21321
|
+
...(listed.error ? { error: listed.error } : {}),
|
|
21322
|
+
...(listed.authRequired ? { authRequired: true } : {}),
|
|
21323
|
+
...(listed.cliMissing ? { cliMissing: true } : {}),
|
|
21324
|
+
});
|
|
21325
|
+
} catch (err) {
|
|
21326
|
+
res.status(500).json({ error: err.message });
|
|
21327
|
+
}
|
|
21328
|
+
});
|
|
21329
|
+
|
|
21330
|
+
// GET /api/antigravity/models — what an ACP session offers SynaBun (lib/antigravity/catalog.js: a short-lived `agy acp`
|
|
21331
|
+
// in a throwaway home; `agy models` only when ACP fails), kept for a minute.
|
|
21332
|
+
// { models: [{ id, label, efforts?, default? }], source: 'live' | 'stale' | 'unavailable', via, signedIn?, tier?, error? }
|
|
21333
|
+
let _antigravityModelsCache = null; // { at, value }
|
|
21334
|
+
app.get('/api/antigravity/models', async (req, res) => {
|
|
21335
|
+
try {
|
|
21336
|
+
const age = _antigravityModelsCache ? Date.now() - _antigravityModelsCache.at : Infinity;
|
|
21337
|
+
if (age < 60_000 && req.query.refresh !== '1') return res.json(_antigravityModelsCache.value);
|
|
21338
|
+
const value = await listAntigravityModels({ launch: antigravityLaunch(), env: antigravityChildEnv() });
|
|
21339
|
+
if (value.source === 'live') {
|
|
21340
|
+
// A different list than the one the Assistant's catalog last read: its rows are rebuilt (as for Claude's).
|
|
21341
|
+
const ids = (rows) => (rows || []).map((row) => `${row.id}${row.default ? '*' : ''}`).join('\n');
|
|
21342
|
+
const changed = _antigravityModelsCache && ids(_antigravityModelsCache.value.models) !== ids(value.models);
|
|
21343
|
+
_antigravityModelsCache = { at: Date.now(), value };
|
|
21344
|
+
if (changed) _assistantCatalog?.refresh?.().catch(() => {});
|
|
21345
|
+
}
|
|
21346
|
+
else if (_antigravityModelsCache?.value?.models?.length) return res.json({ ..._antigravityModelsCache.value, source: 'stale', error: value.error });
|
|
21347
|
+
res.json(value);
|
|
21348
|
+
} catch (err) {
|
|
21349
|
+
res.status(500).json({ models: [], source: 'unavailable', error: err.message });
|
|
21350
|
+
}
|
|
21351
|
+
});
|
|
21352
|
+
|
|
21353
|
+
// POST /api/antigravity/policy — the question the PreToolUse hook of a SynaBun Antigravity home asks before every tool
|
|
21354
|
+
// call of a brain or run SynaBun started (lib/antigravity/policy-hook.mjs → lib/antigravity/policy.js). The home's
|
|
21355
|
+
// token names who registered the checker; an unknown token is refused. Loopback socket only (requireLocal), and only
|
|
21356
|
+
// { token, payload }: the answer is { decision: 'deny' | 'ask' | 'allow', reason? }.
|
|
21357
|
+
app.post('/api/antigravity/policy', requireLoopbackSocket, express.json({ limit: '4mb' }), async (req, res) => {
|
|
21358
|
+
const body = req.body && typeof req.body === 'object' && !Array.isArray(req.body) ? req.body : {};
|
|
21359
|
+
try {
|
|
21360
|
+
res.json(await defaultAntigravityPolicy.decide(body.token, body.payload && typeof body.payload === 'object' ? body.payload : {}));
|
|
21361
|
+
} catch (err) {
|
|
21362
|
+
res.json({ decision: 'deny', reason: `SynaBun could not check this call (${err?.message || err}).` });
|
|
21363
|
+
}
|
|
21364
|
+
});
|
|
21365
|
+
/** Where the hook of a SynaBun Antigravity home reaches this server (loopback, the port this server listens on). */
|
|
21366
|
+
function antigravityPolicyUrl() { return `http://127.0.0.1:${PORT}/api/antigravity/policy`; }
|
|
21367
|
+
/** The home of an Antigravity brain or run (lib/antigravity/home.js), under the data home's runtime folder. */
|
|
21368
|
+
function antigravityHomeFor(kind, id) { return antigravityHomeDir(antigravityHomesRoot(DATA_HOME), kind, id); }
|
|
21369
|
+
|
|
21370
|
+
// Antigravity's conversations as SynaBun keeps them (lib/antigravity/transcripts.js, read only): the user's own go
|
|
21371
|
+
// into Resume's index (session_cache + session_fts, provider antigravity), every one, SynaBun's brain and run homes
|
|
21372
|
+
// included, into memory capture (lib/memory-session-capture.js). A pass at start, one after a panel turn ends (that
|
|
21373
|
+
// conversation only, a moment later so agy has written its last step), one every five minutes, and a fresh one
|
|
21374
|
+
// before Resume or Skills Studio searches them (at most every 15 s).
|
|
21375
|
+
function antigravitySessionStores({ userOnly = false } = {}) {
|
|
21376
|
+
return antigravityStores({ homesRoot: userOnly ? null : antigravityHomesRoot(DATA_HOME) });
|
|
21377
|
+
}
|
|
21378
|
+
let _antigravityCacheAt = 0;
|
|
21379
|
+
function refreshAntigravityCatalog({ force = false } = {}) {
|
|
21380
|
+
if (!force && Date.now() - _antigravityCacheAt < 15_000) return null;
|
|
21381
|
+
_antigravityCacheAt = Date.now();
|
|
21382
|
+
try { return rebuildAntigravityCache({ stores: antigravitySessionStores() }); }
|
|
21383
|
+
catch (err) { console.warn(' Session cache (antigravity) error:', err.message); return null; }
|
|
21384
|
+
}
|
|
21385
|
+
const _antigravityRefreshTimers = new Map();
|
|
21386
|
+
function refreshAntigravitySession(sessionId) {
|
|
21387
|
+
if (typeof sessionId !== 'string' || !sessionId) return;
|
|
21388
|
+
clearTimeout(_antigravityRefreshTimers.get(sessionId));
|
|
21389
|
+
_antigravityRefreshTimers.set(sessionId, setTimeout(() => {
|
|
21390
|
+
_antigravityRefreshTimers.delete(sessionId);
|
|
21391
|
+
try { rebuildAntigravityCache({ stores: antigravitySessionStores({ userOnly: true }), only: sessionId }); }
|
|
21392
|
+
catch (err) { console.warn(' Session cache (antigravity) error:', err.message); }
|
|
21393
|
+
}, 1500));
|
|
21394
|
+
_antigravityRefreshTimers.get(sessionId).unref?.();
|
|
21395
|
+
}
|
|
21396
|
+
/** SynaBun's own MCP entry for an Antigravity home: the registration the Setup card writes (stdio, this install). */
|
|
21397
|
+
function antigravitySynabunMcpEntry() {
|
|
21398
|
+
const { envPath, mcpCommand, mcpArgs } = getMcpPaths();
|
|
21399
|
+
return { command: mcpCommand, args: mcpArgs, env: { DOTENV_PATH: envPath, SYNABUN_DATA_HOME: DATA_HOME, MEMORY_DATA_DIR: resolve(DATA_HOME, 'mcp-data') } };
|
|
21400
|
+
}
|
|
21401
|
+
|
|
21402
|
+
// GET /api/antigravity/panel — what the side panel's socket offers on this server (lib/antigravity/panel-bridge.js).
|
|
21403
|
+
// Admin-only like the socket itself, so the page tells a refused socket (403 here too: a guest, the automation
|
|
21404
|
+
// browser) from a server that predates it (404: it needs a restart) instead of reconnecting forever.
|
|
21405
|
+
app.get('/api/antigravity/panel', (req, res) => {
|
|
21406
|
+
res.json({ ok: true, socket: ANTIGRAVITY_WS_PATH, capabilities: [...ANTIGRAVITY_PANEL_CAPABILITIES], live: antigravityPanel.liveCount() });
|
|
21171
21407
|
});
|
|
21172
21408
|
|
|
21173
21409
|
// ── Hybrid session content search (FTS5 + semantic re-rank for Claude) ──
|
|
@@ -21204,6 +21440,9 @@ app.get('/api/session-search', async (req, res) => {
|
|
|
21204
21440
|
codexCatalogById = new Map(codexCatalog.map((session) => [session.sessionId, session]));
|
|
21205
21441
|
}
|
|
21206
21442
|
|
|
21443
|
+
// Antigravity's index follows its transcripts (refreshAntigravityCatalog: at most every 15 s).
|
|
21444
|
+
if (provider === 'antigravity') refreshAntigravityCatalog();
|
|
21445
|
+
|
|
21207
21446
|
// 1. FTS5 substring hits
|
|
21208
21447
|
const ftsRows = searchSessionFts(q, { provider, project: projectFilter, limit: limit * 2 });
|
|
21209
21448
|
const ftsMap = new Map();
|
|
@@ -21468,13 +21707,21 @@ app.post('/api/claude-code/sessions/:sessionId/fork', express.json(), claudeSess
|
|
|
21468
21707
|
app.delete('/api/claude-code/sessions/:sessionId', express.json(), claudeSessionRoute((req) => {
|
|
21469
21708
|
// Irreversible: the panel asks first, and says so here.
|
|
21470
21709
|
if (req.body?.confirm !== true) { const e = new Error('Deleting a session needs confirm: true'); e.status = 400; throw e; }
|
|
21471
|
-
|
|
21710
|
+
// The transcript cache keeps parsed rows by path: a deleted session's go with it (also when the delete fails half way).
|
|
21711
|
+
return Promise.resolve()
|
|
21712
|
+
.then(() => claudeSessionOps.remove(req.params.sessionId, req.body?.project))
|
|
21713
|
+
.finally(() => { try { claudeHistoryCache.forgetSession(req.params.sessionId); } catch {} });
|
|
21472
21714
|
}));
|
|
21473
21715
|
app.get('/api/claude-code/sessions/:sessionId/subagents', claudeSessionRoute(req => claudeSessionOps.subagents(req.params.sessionId, req.query.project)));
|
|
21474
21716
|
app.get('/api/claude-code/sessions/:sessionId/subagents/:agentId/messages', claudeSessionRoute(req => claudeSessionOps.subagentMessages(req.params.sessionId, req.params.agentId, req.query.project)));
|
|
21475
21717
|
|
|
21476
21718
|
// --- Session Messages (read JSONL for panel history) ---
|
|
21477
21719
|
|
|
21720
|
+
// The parsed transcripts of the last few sessions opened (lib/claude-history-cache.js).
|
|
21721
|
+
const claudeHistoryCache = createHistoryCache();
|
|
21722
|
+
// The Codex rollouts read last, parsed once for the early preview and the history fallback (lib/codex-rollout-reader.js).
|
|
21723
|
+
const codexRolloutReader = createRolloutReader();
|
|
21724
|
+
|
|
21478
21725
|
app.get('/api/claude-code/sessions/:sessionId/messages', async (req, res) => {
|
|
21479
21726
|
try {
|
|
21480
21727
|
const { sessionId } = req.params;
|
|
@@ -21514,22 +21761,17 @@ app.get('/api/claude-code/sessions/:sessionId/messages', async (req, res) => {
|
|
|
21514
21761
|
return res.status(missing.status).json(missing.body);
|
|
21515
21762
|
}
|
|
21516
21763
|
|
|
21517
|
-
//
|
|
21518
|
-
//
|
|
21519
|
-
//
|
|
21520
|
-
//
|
|
21521
|
-
//
|
|
21522
|
-
const rl = jsonlLines(openConfinedStream(found.realFolder, found.realPath, { encoding: 'utf-8' }));
|
|
21764
|
+
// Session JSONLs reach tens of MB and one session open asks this route several times (the panel's
|
|
21765
|
+
// snapshot probe, the rebuild, "load earlier", a clipped result): the collector of a transcript is
|
|
21766
|
+
// kept, and a file that grew is parsed only from where the last read stopped (lib/claude-history-cache.js:
|
|
21767
|
+
// whole lines split on "\n" only, read from the canonical path that was checked, opened without following
|
|
21768
|
+
// a link).
|
|
21523
21769
|
// Rows per transcript line: prompts (with their uuid), assistant text, thinking
|
|
21524
21770
|
// and tool calls, tool results with their typed output (they live inside user
|
|
21525
21771
|
// lines), compaction markers. The collector also follows the parentUuid chain,
|
|
21526
21772
|
// so the branches a conversation rewind abandoned are left out: what is shown
|
|
21527
21773
|
// 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
|
-
}
|
|
21774
|
+
const collector = await claudeHistoryCache.collectorFor(found);
|
|
21533
21775
|
|
|
21534
21776
|
// `?tool=<tool_use_id>`: just that call's result (a restored snapshot holds
|
|
21535
21777
|
// only the clipped text of a long result and asks for the rest).
|
|
@@ -21747,7 +21989,7 @@ app.get('/api/claude-code/integrations', (req, res) => {
|
|
|
21747
21989
|
|
|
21748
21990
|
// POST /api/claude-code/integrations — enable hook for a target
|
|
21749
21991
|
// Optional `hook` param: 'SessionStart' | 'UserPromptSubmit' (omit for both)
|
|
21750
|
-
app.post('/api/claude-code/integrations', (req, res) => {
|
|
21992
|
+
app.post('/api/claude-code/integrations', async (req, res) => {
|
|
21751
21993
|
try {
|
|
21752
21994
|
const { target, projectPath, label, hook } = req.body;
|
|
21753
21995
|
// target: 'global' | 'project'
|
|
@@ -21817,7 +22059,7 @@ app.post('/api/claude-code/integrations', (req, res) => {
|
|
|
21817
22059
|
let dubiousOwnership = false;
|
|
21818
22060
|
let trustPath = null;
|
|
21819
22061
|
try {
|
|
21820
|
-
if (detectDubiousOwnership(normalized)) {
|
|
22062
|
+
if (await detectDubiousOwnership(normalized)) {
|
|
21821
22063
|
dubiousOwnership = true;
|
|
21822
22064
|
trustPath = normalizeGitPath(normalized);
|
|
21823
22065
|
}
|
|
@@ -22252,7 +22494,7 @@ app.delete('/api/claude-code/mcp', (req, res) => {
|
|
|
22252
22494
|
});
|
|
22253
22495
|
|
|
22254
22496
|
// ═══════════════════════════════════════════
|
|
22255
|
-
// Multi-Provider MCP Setup (
|
|
22497
|
+
// Multi-Provider MCP Setup (Antigravity, Codex)
|
|
22256
22498
|
// ═══════════════════════════════════════════
|
|
22257
22499
|
|
|
22258
22500
|
// ── Minimal TOML helpers (for Codex config.toml) ──
|
|
@@ -22411,51 +22653,43 @@ function buildCodexMcpCliCommand() {
|
|
|
22411
22653
|
return `codex mcp add SynaBun ${envArgs} -- node ${JSON.stringify(definition.args[0])}`;
|
|
22412
22654
|
}
|
|
22413
22655
|
|
|
22414
|
-
// ──
|
|
22656
|
+
// ── Antigravity MCP endpoints ──
|
|
22657
|
+
// ~/.gemini/config/mcp_config.json (lib/mcp-client-config.js): the file the Antigravity CLI and app share. A file that
|
|
22658
|
+
// is not a JSON object is never overwritten: the request fails and says so.
|
|
22415
22659
|
|
|
22416
|
-
app.get('/api/setup/
|
|
22660
|
+
app.get('/api/setup/antigravity/mcp', (req, res) => {
|
|
22417
22661
|
try {
|
|
22418
|
-
const
|
|
22419
|
-
if (!existsSync(
|
|
22420
|
-
const data =
|
|
22421
|
-
const
|
|
22422
|
-
res.json({ ok: true, connected });
|
|
22662
|
+
const path = antigravityMcpConfigPath();
|
|
22663
|
+
if (!existsSync(path)) return res.json({ ok: true, connected: false, path });
|
|
22664
|
+
const data = readAntigravityMcpConfig();
|
|
22665
|
+
const entry = data.mcpServers?.SynaBun;
|
|
22666
|
+
res.json({ ok: true, connected: !!entry && entry.disabled !== true, disabled: entry?.disabled === true, path });
|
|
22423
22667
|
} catch (err) {
|
|
22424
22668
|
res.status(500).json({ error: err.message });
|
|
22425
22669
|
}
|
|
22426
22670
|
});
|
|
22427
22671
|
|
|
22428
|
-
app.post('/api/setup/
|
|
22672
|
+
app.post('/api/setup/antigravity/mcp', (req, res) => {
|
|
22429
22673
|
try {
|
|
22430
|
-
const dir = join(getHomePath(), '.gemini');
|
|
22431
|
-
const settingsPath = join(dir, 'settings.json');
|
|
22432
22674
|
const { envPath, mcpCommand, mcpArgs } = getMcpPaths();
|
|
22433
|
-
|
|
22434
|
-
let data = {};
|
|
22435
|
-
try { data = JSON.parse(readFileSync(settingsPath, 'utf-8')); } catch {}
|
|
22436
|
-
data = mergeGeminiMcpConfig(data, {
|
|
22675
|
+
const data = mergeAntigravityMcpConfig(readAntigravityMcpConfig(), {
|
|
22437
22676
|
command: mcpCommand,
|
|
22438
22677
|
args: mcpArgs,
|
|
22439
22678
|
env: { DOTENV_PATH: envPath, SYNABUN_DATA_HOME: DATA_HOME, MEMORY_DATA_DIR: resolve(DATA_HOME, 'mcp-data') },
|
|
22440
22679
|
});
|
|
22441
|
-
|
|
22442
|
-
res.json({ ok: true, message: 'SynaBun MCP registered in
|
|
22680
|
+
writeAntigravityMcpConfig(data);
|
|
22681
|
+
res.json({ ok: true, message: 'SynaBun MCP registered in Antigravity. New Antigravity sessions connect to it.', rules: syncRulesWithMcp('antigravity', 'install', req.body) });
|
|
22443
22682
|
} catch (err) {
|
|
22444
22683
|
res.status(500).json({ error: err.message });
|
|
22445
22684
|
}
|
|
22446
22685
|
});
|
|
22447
22686
|
|
|
22448
|
-
app.delete('/api/setup/
|
|
22687
|
+
app.delete('/api/setup/antigravity/mcp', (req, res) => {
|
|
22449
22688
|
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') });
|
|
22689
|
+
if (!existsSync(antigravityMcpConfigPath())) return res.json({ ok: true, rules: syncRulesWithMcp('antigravity', 'remove') });
|
|
22690
|
+
const data = readAntigravityMcpConfig();
|
|
22691
|
+
if (data.mcpServers && data.mcpServers.SynaBun) writeAntigravityMcpConfig(removeAntigravityMcpServer(data));
|
|
22692
|
+
res.json({ ok: true, message: 'SynaBun MCP removed from Antigravity.', rules: syncRulesWithMcp('antigravity', 'remove') });
|
|
22459
22693
|
} catch (err) {
|
|
22460
22694
|
res.status(500).json({ error: err.message });
|
|
22461
22695
|
}
|
|
@@ -22516,11 +22750,11 @@ app.get('/api/setup/status', (req, res) => {
|
|
|
22516
22750
|
} catch {}
|
|
22517
22751
|
claude.cliCommand = claudeMcpAddCommand(mcpPaths);
|
|
22518
22752
|
|
|
22519
|
-
//
|
|
22520
|
-
let
|
|
22753
|
+
// Antigravity (~/.gemini/config/mcp_config.json)
|
|
22754
|
+
let antigravity = { connected: false };
|
|
22521
22755
|
try {
|
|
22522
|
-
const
|
|
22523
|
-
|
|
22756
|
+
const entry = readAntigravityMcpConfig().mcpServers?.SynaBun;
|
|
22757
|
+
antigravity.connected = !!entry && entry.disabled !== true;
|
|
22524
22758
|
} catch {}
|
|
22525
22759
|
|
|
22526
22760
|
// Codex
|
|
@@ -22540,7 +22774,7 @@ app.get('/api/setup/status', (req, res) => {
|
|
|
22540
22774
|
let rules = null;
|
|
22541
22775
|
try { rules = rulesetInstaller.status(); } catch (err) { rules = { ok: false, error: err.message }; }
|
|
22542
22776
|
|
|
22543
|
-
res.json({ ok: true, claude,
|
|
22777
|
+
res.json({ ok: true, claude, antigravity, codex, opencode, rules, paths: { mcpIndexPath, envPath, mcpCommand, mcpArgs } });
|
|
22544
22778
|
} catch (err) {
|
|
22545
22779
|
res.status(500).json({ error: err.message });
|
|
22546
22780
|
}
|
|
@@ -22640,7 +22874,7 @@ skillsService = createSkillsService({
|
|
|
22640
22874
|
// its skill hints); invite guests do not (broadcastSync keeps `skills:` to the owner).
|
|
22641
22875
|
broadcast: (event) => broadcastSync({ type: 'skills:changed', event }),
|
|
22642
22876
|
// 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') }),
|
|
22877
|
+
installedRuntimes: () => ({ claude: cliFoundForWarmup('claude-code'), codex: cliFoundForWarmup('codex'), opencode: cliFoundForWarmup('opencode'), antigravity: cliFoundForWarmup('antigravity') }),
|
|
22644
22878
|
runtimeChecks: skillsRuntimeChecks,
|
|
22645
22879
|
operationsOptions: { notify: (item) => {
|
|
22646
22880
|
const session = _assistantRuntime?.listSessions?.().find((s) => s.status !== 'ended');
|
|
@@ -22848,9 +23082,11 @@ function skillBrowserAvailability() {
|
|
|
22848
23082
|
const unreadable = browserConfigUnreadable(cfg);
|
|
22849
23083
|
if (unreadable) return { available: false, reason: `The browser settings cannot be read (${unreadable}): fix them in Settings → Browser.` };
|
|
22850
23084
|
if (cfg.connectMode === 'morelogin') {
|
|
22851
|
-
|
|
22852
|
-
|
|
22853
|
-
|
|
23085
|
+
// The exe in a known folder, or the local API answered lately (an install in a custom folder). This is read
|
|
23086
|
+
// synchronously, so a miss refreshes the answer in the background for the next call.
|
|
23087
|
+
const installed = moreLogin.isMoreLoginPresent();
|
|
23088
|
+
if (!installed) moreLogin.probeMoreLoginApi({ savedCfg: cfg, timeout: 1500 }).catch(() => {});
|
|
23089
|
+
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
23090
|
}
|
|
22855
23091
|
if ([...browserSessions.values()].some((s) => s?.context)) return { available: true, reason: null };
|
|
22856
23092
|
if (findBrowserExecutable(cfg.channel)) return { available: true, reason: null };
|
|
@@ -22941,6 +23177,19 @@ async function skillSourceSessions({ provider, project = null, q = '', limit = 4
|
|
|
22941
23177
|
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
23178
|
}
|
|
22943
23179
|
}
|
|
23180
|
+
} else if (provider === 'antigravity') {
|
|
23181
|
+
// The user's own Antigravity conversations (the panel's and a terminal agy's), as Resume's index keeps them,
|
|
23182
|
+
// titled as Antigravity titles them (refreshAntigravityCatalog).
|
|
23183
|
+
refreshAntigravityCatalog();
|
|
23184
|
+
for (const entry of listSessionCache('antigravity', { limit: 100000 })) {
|
|
23185
|
+
if (entry.deleted || !entry.project_path) continue;
|
|
23186
|
+
const dir = resolve(entry.project_path);
|
|
23187
|
+
const proj = projects.find((p) => isInside(resolve(p.path), dir, { allowRoot: true }));
|
|
23188
|
+
if (!proj) continue;
|
|
23189
|
+
const title = String(entry.first_prompt || '');
|
|
23190
|
+
if (search && !title.toLowerCase().includes(search) && !String(entry.session_id).toLowerCase().includes(search)) continue;
|
|
23191
|
+
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 });
|
|
23192
|
+
}
|
|
22944
23193
|
}
|
|
22945
23194
|
return out.sort((a, b) => String(b.modified || '').localeCompare(String(a.modified || ''))).slice(0, Math.max(1, Math.min(100, Number(limit) || 40)));
|
|
22946
23195
|
}
|
|
@@ -22948,7 +23197,8 @@ async function skillSourceSessions({ provider, project = null, q = '', limit = 4
|
|
|
22948
23197
|
/**
|
|
22949
23198
|
* One session transcript as messages, read the way each runtime keeps it: Claude Code's transcript inside a
|
|
22950
23199
|
* registered project's folder (lib/path-confine.js findSessionTranscript), Codex's rollout inside its account's home,
|
|
22951
|
-
* OpenCode's rows of its database
|
|
23200
|
+
* OpenCode's rows of its database, Antigravity's transcript in the user's own ~/.gemini. Bounded: the first messages
|
|
23201
|
+
* and the last ones of a very long session.
|
|
22952
23202
|
*/
|
|
22953
23203
|
async function skillSourceConversation({ provider, sessionId, project = null }) {
|
|
22954
23204
|
const keep = (list, m) => { list.push(m); if (list.length > 2400) list.splice(300, list.length - 2100); };
|
|
@@ -22991,6 +23241,16 @@ async function skillSourceConversation({ provider, sessionId, project = null })
|
|
|
22991
23241
|
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
23242
|
return { title: session.title || '', project, messages: opencodeRowsToMessages(rows) };
|
|
22993
23243
|
}
|
|
23244
|
+
if (provider === 'antigravity') {
|
|
23245
|
+
// The transcript in the user's own Antigravity folder (~/.gemini/antigravity-cli/brain/<id>/…), read as
|
|
23246
|
+
// lib/antigravity/transcripts.js reads it: no link followed, bounded; never the model's reasoning.
|
|
23247
|
+
const found = findUserConversation(sessionId);
|
|
23248
|
+
if (!found) throw Object.assign(new Error('That Antigravity conversation was not found.'), { status: 404, code: 'source_not_found' });
|
|
23249
|
+
const parsed = parseAntigravityTranscript(readTranscriptText(found.transcript));
|
|
23250
|
+
const messages = [];
|
|
23251
|
+
for (const m of antigravityMessagesToMessages(parsed.messages)) keep(messages, m);
|
|
23252
|
+
return { title: found.title || parsed.firstPrompt || '', project: found.cwd || project, messages };
|
|
23253
|
+
}
|
|
22994
23254
|
throw Object.assign(new Error('Unknown provider.'), { status: 400, code: 'source_failed' });
|
|
22995
23255
|
}
|
|
22996
23256
|
|
|
@@ -23078,7 +23338,8 @@ function getOpenCodeCommandDir() {
|
|
|
23078
23338
|
return xdgDefault;
|
|
23079
23339
|
}
|
|
23080
23340
|
|
|
23081
|
-
|
|
23341
|
+
// Antigravity reads its user skills from ~/.gemini/config/skills (lib/skills/roots.js), installed like the others.
|
|
23342
|
+
const SKILL_TARGETS = /** @type {const} */ (['claude', 'codex', 'opencode', 'antigravity']);
|
|
23082
23343
|
|
|
23083
23344
|
/** Where the old prompt / command bridge of a skill is (Codex, OpenCode), or null (Claude Code never had one). */
|
|
23084
23345
|
function legacyBridgePath(target, skillName) {
|
|
@@ -23340,6 +23601,7 @@ function listAvailableSkills() {
|
|
|
23340
23601
|
claude: isSkillInstalled('claude', dirName),
|
|
23341
23602
|
codex: isSkillInstalled('codex', dirName),
|
|
23342
23603
|
opencode: isSkillInstalled('opencode', dirName),
|
|
23604
|
+
antigravity: isSkillInstalled('antigravity', dirName),
|
|
23343
23605
|
});
|
|
23344
23606
|
// What each runtime has of it: a native skill folder ('native'; 'modified' when changed since SynaBun installed
|
|
23345
23607
|
// it; 'other' when it is not SynaBun's), and an old bridge not moved yet. `installedTargets` stays the one flag
|
|
@@ -23724,6 +23986,11 @@ app.post('/api/sidepanel/kill-session', async (req, res) => {
|
|
|
23724
23986
|
killed = true;
|
|
23725
23987
|
}
|
|
23726
23988
|
}
|
|
23989
|
+
} else if (provider === 'antigravity') {
|
|
23990
|
+
// An Antigravity session lives in the `agy acp` of the panel socket that opened it, which ends with that
|
|
23991
|
+
// socket: nothing outlives a closed panel, so there is nothing to kill here. Its conversation stays in
|
|
23992
|
+
// Antigravity's own history (Resume lists it).
|
|
23993
|
+
killed = false;
|
|
23727
23994
|
} else if (provider === 'opencode') {
|
|
23728
23995
|
// OpenCode sessions live on the OpenCode SDK server, not in an orphan
|
|
23729
23996
|
// map — DELETE them via the proxy.
|
|
@@ -25276,7 +25543,7 @@ app.get('/api/mcp/platforms', (req, res) => {
|
|
|
25276
25543
|
claudeCode: existsSync(join(home, '.claude.json')) || existsSync(join(home, '.claude')),
|
|
25277
25544
|
opencode: existsSync(getOpencodeConfigDir()),
|
|
25278
25545
|
codex: existsSync(join(home, '.codex')),
|
|
25279
|
-
|
|
25546
|
+
antigravity: existsSync(antigravityConfigDir()) || existsSync(join(home, '.gemini', 'antigravity-cli')),
|
|
25280
25547
|
});
|
|
25281
25548
|
});
|
|
25282
25549
|
|
|
@@ -25393,7 +25660,7 @@ app.post('/api/mcp/install/github', async (req, res) => {
|
|
|
25393
25660
|
});
|
|
25394
25661
|
|
|
25395
25662
|
// GET /api/mcp/installed-all — unified view of every externally-installed MCP
|
|
25396
|
-
// across Claude Code / OpenCode / Codex /
|
|
25663
|
+
// across Claude Code / OpenCode / Codex / Antigravity + SynaBun's registry + Claude Code plugins.
|
|
25397
25664
|
// Excludes SynaBun's built-in tool-group servers (they're baked in, not "installed").
|
|
25398
25665
|
app.get('/api/mcp/installed-all', (req, res) => {
|
|
25399
25666
|
try {
|
|
@@ -25440,17 +25707,17 @@ app.get('/api/mcp/installed-all', (req, res) => {
|
|
|
25440
25707
|
}
|
|
25441
25708
|
} catch {}
|
|
25442
25709
|
|
|
25443
|
-
//
|
|
25710
|
+
// Antigravity (~/.gemini/config/mcp_config.json: a remote server is `serverUrl`)
|
|
25444
25711
|
try {
|
|
25445
|
-
|
|
25446
|
-
|
|
25447
|
-
const data = JSON.parse(readFileSync(p, 'utf-8'));
|
|
25712
|
+
if (existsSync(antigravityMcpConfigPath())) {
|
|
25713
|
+
const data = readAntigravityMcpConfig();
|
|
25448
25714
|
for (const [name, cfg] of Object.entries(data.mcpServers || {})) {
|
|
25449
|
-
|
|
25450
|
-
|
|
25451
|
-
|
|
25452
|
-
|
|
25453
|
-
|
|
25715
|
+
const seen = describeAntigravityMcpEntry(cfg);
|
|
25716
|
+
touch(name, 'antigravity', {
|
|
25717
|
+
transport: seen.transport,
|
|
25718
|
+
command: seen.command || null,
|
|
25719
|
+
args: seen.args?.length ? seen.args : null,
|
|
25720
|
+
url: seen.url || null,
|
|
25454
25721
|
});
|
|
25455
25722
|
}
|
|
25456
25723
|
}
|
|
@@ -25666,16 +25933,11 @@ async function internalMcpSync({ name, config, platforms }) {
|
|
|
25666
25933
|
content = tomlUpsertSection(content, `mcp_servers.${name}`, kvPairs);
|
|
25667
25934
|
writeFileSync(configPath, content, 'utf-8');
|
|
25668
25935
|
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';
|
|
25936
|
+
} else if (p === 'antigravity') {
|
|
25937
|
+
const data = readAntigravityMcpConfig();
|
|
25938
|
+
data.mcpServers = { ...(data.mcpServers || {}), [name]: buildAntigravityMcpEntry(config) };
|
|
25939
|
+
writeAntigravityMcpConfig(data);
|
|
25940
|
+
results.antigravity = 'ok';
|
|
25679
25941
|
}
|
|
25680
25942
|
} catch (err) {
|
|
25681
25943
|
results[p] = `error: ${err.message}`;
|
|
@@ -25733,17 +25995,12 @@ app.delete('/api/mcp/sync', (req, res) => {
|
|
|
25733
25995
|
}
|
|
25734
25996
|
results.codex = 'ok';
|
|
25735
25997
|
|
|
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
|
-
}
|
|
25998
|
+
} else if (p === 'antigravity') {
|
|
25999
|
+
if (existsSync(antigravityMcpConfigPath())) {
|
|
26000
|
+
const data = readAntigravityMcpConfig();
|
|
26001
|
+
if (data.mcpServers?.[name]) writeAntigravityMcpConfig(removeAntigravityMcpServer(data, name));
|
|
25745
26002
|
}
|
|
25746
|
-
results.
|
|
26003
|
+
results.antigravity = 'ok';
|
|
25747
26004
|
}
|
|
25748
26005
|
} catch (err) {
|
|
25749
26006
|
results[p] = `error: ${err.message}`;
|
|
@@ -26160,9 +26417,11 @@ const CLI_CONFIG_PATH = resolve(DATA_HOME, 'data', 'cli-config.json');
|
|
|
26160
26417
|
const CLI_DEFAULTS = {
|
|
26161
26418
|
'claude-code': { command: 'claude' },
|
|
26162
26419
|
'codex': { command: 'codex' },
|
|
26163
|
-
'
|
|
26420
|
+
'antigravity': { command: 'agy' },
|
|
26164
26421
|
'opencode': { command: 'opencode' },
|
|
26165
26422
|
};
|
|
26423
|
+
// Profiles SynaBun no longer drives: a path saved for one (Gemini CLI, replaced by Antigravity) is never a profile.
|
|
26424
|
+
const RETIRED_CLI_PROFILES = new Set(['gemini']);
|
|
26166
26425
|
|
|
26167
26426
|
// Parsed cli-config.json keyed by mtime — every terminal spawn resolves its
|
|
26168
26427
|
// profile through here, so skip the read + parse when the file is unchanged.
|
|
@@ -26175,7 +26434,9 @@ function loadCliConfig() {
|
|
|
26175
26434
|
if (!_cliConfigCache || _cliConfigCache.mtimeMs !== mtimeMs) {
|
|
26176
26435
|
_cliConfigCache = { mtimeMs, raw: JSON.parse(readFileSync(CLI_CONFIG_PATH, 'utf-8')) };
|
|
26177
26436
|
}
|
|
26178
|
-
|
|
26437
|
+
const saved = structuredClone(_cliConfigCache.raw);
|
|
26438
|
+
for (const retired of RETIRED_CLI_PROFILES) delete saved?.[retired];
|
|
26439
|
+
return { ...CLI_DEFAULTS, ...saved };
|
|
26179
26440
|
} catch { return { ...CLI_DEFAULTS }; }
|
|
26180
26441
|
}
|
|
26181
26442
|
|
|
@@ -26196,7 +26457,7 @@ const SHELL_PROFILE = { shell: DEFAULT_SHELL, args: interactiveShellArgs(), env:
|
|
|
26196
26457
|
/**
|
|
26197
26458
|
* Build terminal profile config dynamically from cli-config.json.
|
|
26198
26459
|
* Re-reads config each call so changes take effect without restart.
|
|
26199
|
-
* @param {string} profileId - CLI profile ID (e.g. 'claude-code', 'codex', '
|
|
26460
|
+
* @param {string} profileId - CLI profile ID (e.g. 'claude-code', 'codex', 'antigravity', 'shell')
|
|
26200
26461
|
* @param {Object} [opts] - Optional overrides
|
|
26201
26462
|
* @param {string} [opts.model] - Model flag for CLIs that support it (e.g. claude --model sonnet)
|
|
26202
26463
|
*/
|
|
@@ -26219,6 +26480,8 @@ function getTerminalProfile(profileId, opts = {}) {
|
|
|
26219
26480
|
cmd = `${cmd} resume ${terminalArgument(opts.resume, DEFAULT_SHELL)}`;
|
|
26220
26481
|
} else if (opts.resume && profileId === 'opencode') {
|
|
26221
26482
|
cmd = `${cmd} -s ${terminalArgument(opts.resume, DEFAULT_SHELL)}`;
|
|
26483
|
+
} else if (opts.resume && profileId === 'antigravity') {
|
|
26484
|
+
cmd = `${cmd} --conversation ${terminalArgument(opts.resume, DEFAULT_SHELL)}`;
|
|
26222
26485
|
}
|
|
26223
26486
|
// Append model flag for CLIs that support it
|
|
26224
26487
|
else if (opts.model) {
|
|
@@ -26236,7 +26499,7 @@ function getTerminalProfile(profileId, opts = {}) {
|
|
|
26236
26499
|
const modelMap = {
|
|
26237
26500
|
'claude-code': (m) => `${cmd} --model ${m}`,
|
|
26238
26501
|
'codex': (m) => `${cmd} --model ${m}`,
|
|
26239
|
-
'
|
|
26502
|
+
'antigravity': (m) => `${cmd} --model ${m}`,
|
|
26240
26503
|
// OpenCode's terminal app has no --model (it prints its help and exits): the model is picked in it.
|
|
26241
26504
|
'opencode': () => cmd,
|
|
26242
26505
|
};
|
|
@@ -26254,6 +26517,9 @@ function getTerminalProfile(profileId, opts = {}) {
|
|
|
26254
26517
|
if (codexEffort) {
|
|
26255
26518
|
cmd = `${cmd} -c model_reasoning_effort="${codexEffort}"`;
|
|
26256
26519
|
}
|
|
26520
|
+
} else if (opts.effort && profileId === 'antigravity') {
|
|
26521
|
+
// agy --effort: low | medium | high | xhigh | max (the model's own default when the flag is left out).
|
|
26522
|
+
if (['low', 'medium', 'high', 'xhigh', 'max'].includes(opts.effort)) cmd = `${cmd} --effort ${opts.effort}`;
|
|
26257
26523
|
}
|
|
26258
26524
|
// Every Codex terminal: the Windows sandbox the Admin sandbox setting allows. A bare value
|
|
26259
26525
|
// (Codex reads one that is not TOML as a string), so no shell can strip a quote from it.
|
|
@@ -26265,7 +26531,7 @@ function getTerminalProfile(profileId, opts = {}) {
|
|
|
26265
26531
|
// The loop runs in a PTY with no human to answer prompts, so accessing
|
|
26266
26532
|
// sibling projects or running Bash/Write would stall.
|
|
26267
26533
|
// Scoped to opts.loopBypass so interactive terminal sessions still prompt.
|
|
26268
|
-
if (opts.loopBypass && profileId === 'claude-code') {
|
|
26534
|
+
if (opts.loopBypass && (profileId === 'claude-code' || profileId === 'antigravity')) {
|
|
26269
26535
|
cmd = `${cmd} --dangerously-skip-permissions`;
|
|
26270
26536
|
}
|
|
26271
26537
|
|
|
@@ -26294,7 +26560,7 @@ function getTerminalProfile(profileId, opts = {}) {
|
|
|
26294
26560
|
// provider sign-in terminals. The desktop app uses this same server/PTY host.
|
|
26295
26561
|
async function freshTerminalPath(base = process.env) {
|
|
26296
26562
|
const os = await osPath.refresh({ force: true });
|
|
26297
|
-
const launchers = ['claude-code', 'codex', 'opencode', '
|
|
26563
|
+
const launchers = ['claude-code', 'codex', 'opencode', 'antigravity'].map(tool => cliResolver.launcher(tool));
|
|
26298
26564
|
// A packaged application's runtime and npm's global bin (rememberPackagedPath above), after the user's own.
|
|
26299
26565
|
const { last: synabunDirs, extra: extraDirs } = rememberedPackagedPath();
|
|
26300
26566
|
return { ...composeTerminalPath({ env: base, os, launchers, synabunDirs, extraDirs }), os, launchers };
|
|
@@ -26861,7 +27127,7 @@ async function driveNextIteration(session, loopState, filename) {
|
|
|
26861
27127
|
return true;
|
|
26862
27128
|
}
|
|
26863
27129
|
|
|
26864
|
-
// ── Exec-based Loop Driver (Codex,
|
|
27130
|
+
// ── Exec-based Loop Driver (Codex, Antigravity, etc.) ──
|
|
26865
27131
|
// Fully isolated from Claude's hook-based driver. Runs platform-native wrapper
|
|
26866
27132
|
// files inside a shell PTY and detects completion via sentinel echo markers.
|
|
26867
27133
|
// The wrapper captures the CLI status before printing SYNABUN_ITER_DONE_<N>, so
|
|
@@ -27191,6 +27457,31 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
27191
27457
|
policyRoots: taskBrowserPolicyRoots(state),
|
|
27192
27458
|
});
|
|
27193
27459
|
},
|
|
27460
|
+
// Google Antigravity: one `agy acp` per run (lib/native-loop-providers.js createAntigravityNativeLoopAdapter), in
|
|
27461
|
+
// the run's own Antigravity home (lib/antigravity/home.js: exactly the run's MCP servers, the policy hook that
|
|
27462
|
+
// asks POST /api/antigravity/policy before every tool call, the user's rules and skills). Approvals go to the
|
|
27463
|
+
// Assistant's broker under `ask`; without an installed CLI the run fails at once with the install hint.
|
|
27464
|
+
antigravity: (state) => {
|
|
27465
|
+
const launch = antigravityLaunch();
|
|
27466
|
+
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.');
|
|
27467
|
+
// A resumed run uses the home its first start wrote (its conversation is there).
|
|
27468
|
+
const homeDir = state.resumingWorker && typeof state.antigravityHome === 'string' && state.antigravityHome
|
|
27469
|
+
? validateRunHome(state.antigravityHome, antigravityHomesRoot(DATA_HOME))
|
|
27470
|
+
: antigravityHomeFor('run', state.runId || state.terminalSessionId || `loop-${Date.now()}`);
|
|
27471
|
+
return createAntigravityNativeLoopAdapter({
|
|
27472
|
+
...state,
|
|
27473
|
+
launch,
|
|
27474
|
+
// The PATH a new terminal has: Antigravity runs the user's own tools.
|
|
27475
|
+
extraEnv: { PATH: antigravityChildEnv().PATH, ...(state.extraEnv || {}) },
|
|
27476
|
+
serverPort: PORT,
|
|
27477
|
+
homeDir,
|
|
27478
|
+
synabunMcpEntry: antigravitySynabunMcpEntry(),
|
|
27479
|
+
policy: defaultAntigravityPolicy,
|
|
27480
|
+
policyUrl: antigravityPolicyUrl(),
|
|
27481
|
+
// Assistant task runs: where the browser policy lets its test runners run besides its own project.
|
|
27482
|
+
policyRoots: taskBrowserPolicyRoots(state),
|
|
27483
|
+
});
|
|
27484
|
+
},
|
|
27194
27485
|
opencode: async (state) => {
|
|
27195
27486
|
// An OpenCode SynaBun does not drive: the run fails with the sentence before its configuration folder, its
|
|
27196
27487
|
// sandbox profile or its serve exist (a loop or a schedule was refused already; this is a worker's, a backstop).
|
|
@@ -27239,6 +27530,7 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
27239
27530
|
state.model,
|
|
27240
27531
|
{
|
|
27241
27532
|
SYNABUN_DESKTOP_GRANT: typeof state.desktopGrantProvider === 'function' ? state.desktopGrantProvider() : null,
|
|
27533
|
+
SYNABUN_RUN_MODE: state.runMode === 'task' ? 'task' : null,
|
|
27242
27534
|
},
|
|
27243
27535
|
// Assistant task runs: the browser policy plugin (loops and schedules unchanged) with the registered code
|
|
27244
27536
|
// projects its test runners may run in (the sandboxed serve cannot read the registry), and the run's confinement
|
|
@@ -27819,6 +28111,17 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
27819
28111
|
ClaudeSession: claudeBridgeModule.ClaudeSession,
|
|
27820
28112
|
sdkVersion: claudeBridgeModule.SDK_VERSION,
|
|
27821
28113
|
handleCodexSkinWebSocket, ensureIsolatedServe, stopIsolatedServe, setupOpencodeSidepanelConfig,
|
|
28114
|
+
// An Antigravity brain (lib/assistant-brains/antigravity.js): `agy acp` in the brain's own home, its hook asking
|
|
28115
|
+
// POST /api/antigravity/policy, SynaBun's MCP entry the Setup card writes.
|
|
28116
|
+
antigravity: {
|
|
28117
|
+
launch: antigravityLaunch,
|
|
28118
|
+
// The PATH a new terminal has (agy runs the user's tools); never the owner's live data paths or a credential.
|
|
28119
|
+
childEnv: antigravityChildEnv,
|
|
28120
|
+
home: (sessionId) => antigravityHomeFor('brain', sessionId),
|
|
28121
|
+
synabunMcpEntry: antigravitySynabunMcpEntry,
|
|
28122
|
+
policy: defaultAntigravityPolicy,
|
|
28123
|
+
policyUrl: antigravityPolicyUrl,
|
|
28124
|
+
},
|
|
27822
28125
|
broadcastSync,
|
|
27823
28126
|
dataDir: LOOP_DATA_DIR,
|
|
27824
28127
|
memory: assistantMemory,
|
|
@@ -28196,7 +28499,7 @@ function findLoopFileForTerminal(terminalSessionId) {
|
|
|
28196
28499
|
}
|
|
28197
28500
|
|
|
28198
28501
|
/**
|
|
28199
|
-
* Exec-based loop driver for non-Claude CLIs (Codex,
|
|
28502
|
+
* Exec-based loop driver for non-Claude CLIs (Codex, Antigravity, etc.).
|
|
28200
28503
|
* State machine: booting → ready → running → ready → ...
|
|
28201
28504
|
*
|
|
28202
28505
|
* Completion detection: sentinel echo approach.
|
|
@@ -28972,7 +29275,7 @@ app.delete('/api/terminal/sessions/:id', (req, res) => {
|
|
|
28972
29275
|
});
|
|
28973
29276
|
|
|
28974
29277
|
app.get('/api/terminal/profiles', (req, res) => {
|
|
28975
|
-
const profileIds = ['claude-code', 'codex', '
|
|
29278
|
+
const profileIds = ['claude-code', 'codex', 'antigravity', 'opencode', 'shell'];
|
|
28976
29279
|
const profiles = profileIds.map(id => {
|
|
28977
29280
|
const p = getTerminalProfile(id);
|
|
28978
29281
|
return { id, shell: p.shell, args: p.args };
|
|
@@ -29056,30 +29359,15 @@ function resolveGitPath() {
|
|
|
29056
29359
|
}
|
|
29057
29360
|
|
|
29058
29361
|
// 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);
|
|
29362
|
+
// directly without a shell). The runner handles a resolved Windows .cmd/.bat shim.
|
|
29363
|
+
async function spawnGit(args, opts = {}) {
|
|
29364
|
+
const binary = await resolveGitPathInBackground();
|
|
29365
|
+
return gitStatusRunner.runRaw(args, { ...opts, binary: binary || 'git', timeoutMs: opts.timeout || 5000 });
|
|
29076
29366
|
}
|
|
29077
29367
|
|
|
29078
|
-
|
|
29079
|
-
|
|
29080
|
-
|
|
29081
|
-
const read = gitReadSync(opts.cwd, args, { env: opts.env });
|
|
29082
|
-
return spawnGit(read.args, { ...opts, env: read.env });
|
|
29368
|
+
async function spawnGitRead(args, opts = {}) {
|
|
29369
|
+
const binary = await resolveGitPathInBackground();
|
|
29370
|
+
return gitStatusRunner.run(args, { ...opts, binary: binary || 'git', timeoutMs: opts.timeout || 5000 });
|
|
29083
29371
|
}
|
|
29084
29372
|
|
|
29085
29373
|
function logGitFailure(where, err) {
|
|
@@ -29118,22 +29406,20 @@ function resolveGitPathInBackground() {
|
|
|
29118
29406
|
|
|
29119
29407
|
// Startup probe — always logs git status so Windows users can see whether git works at all. Asynchronous:
|
|
29120
29408
|
// 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(() => {});
|
|
29409
|
+
setImmediate(async () => {
|
|
29410
|
+
try {
|
|
29411
|
+
const gitPath = await resolveGitPathInBackground();
|
|
29412
|
+
const result = await gitStatusRunner.run(['--version'], { binary: gitPath || 'git', timeoutMs: 3000 });
|
|
29413
|
+
if (result.ok) console.log(`[git] ${String(result.stdout).trim()} — OK`);
|
|
29414
|
+
else console.warn(`[git] startup probe FAILED: ${String(result.stderr).trim() || result.code}. File explorer git UI will be hidden.`);
|
|
29415
|
+
} catch { /* availability is reported on the first read */ }
|
|
29130
29416
|
});
|
|
29131
29417
|
|
|
29132
29418
|
// Git "dubious ownership" detection — CVE-2022-24765 safety check refuses to operate on
|
|
29133
29419
|
// repos owned by a different OS user/SID. Common on Windows external drives or repos
|
|
29134
29420
|
// 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 });
|
|
29421
|
+
async function detectDubiousOwnership(dir) {
|
|
29422
|
+
const r = await spawnGitRead(['rev-parse', '--is-inside-work-tree'], { cwd: dir, timeout: 3000 });
|
|
29137
29423
|
if (r.status === 0) return false;
|
|
29138
29424
|
const stderr = r.stderr?.toString?.() || '';
|
|
29139
29425
|
return /dubious ownership|safe\.directory/i.test(stderr);
|
|
@@ -29146,91 +29432,27 @@ function normalizeGitPath(dir) {
|
|
|
29146
29432
|
}
|
|
29147
29433
|
|
|
29148
29434
|
/** Try to get git status for a directory. Returns { branch, statuses } or null. */
|
|
29149
|
-
function getGitInfo(dir) {
|
|
29150
|
-
|
|
29151
|
-
|
|
29152
|
-
|
|
29153
|
-
|
|
29154
|
-
|
|
29155
|
-
|
|
29156
|
-
|
|
29157
|
-
// Get branch name — try --show-current first (git 2.22+), then fall back to
|
|
29158
|
-
// --abbrev-ref HEAD (works on older git; detached HEAD returns the literal "HEAD"),
|
|
29159
|
-
// then short SHA for detached HEAD. The chained fallback is essential for Windows users
|
|
29160
|
-
// with older Git for Windows installs where --show-current exits non-zero.
|
|
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();
|
|
29435
|
+
async function getGitInfo(dir, options = {}) {
|
|
29436
|
+
const result = projectStatus(await gitStatusRunner.read(dir, options), dir);
|
|
29437
|
+
if (!result.isGit) return null;
|
|
29438
|
+
const statuses = new Map();
|
|
29439
|
+
for (const change of result.changes) {
|
|
29440
|
+
const child = change.path.split('/')[0];
|
|
29441
|
+
if (gitStatusPriority(change.status) > gitStatusPriority(statuses.get(child))) statuses.set(child, change.status);
|
|
29178
29442
|
}
|
|
29443
|
+
return { branch: result.branch, statuses, scanState: result.timedOut ? 'timed-out' : result.ok ? 'ready' : 'failed' };
|
|
29444
|
+
}
|
|
29179
29445
|
|
|
29180
|
-
|
|
29181
|
-
|
|
29182
|
-
|
|
29183
|
-
|
|
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
|
-
}
|
|
29446
|
+
function gitRequestSignal(res) {
|
|
29447
|
+
const controller = new AbortController();
|
|
29448
|
+
res.once('close', () => controller.abort());
|
|
29449
|
+
return controller.signal;
|
|
29228
29450
|
}
|
|
29229
29451
|
|
|
29230
29452
|
const GIT_STATUS_PRIORITY = { untracked: 1, added: 2, renamed: 2, staged: 3, modified: 4, mixed: 5, deleted: 5, conflict: 6 };
|
|
29231
29453
|
function gitStatusPriority(s) { return GIT_STATUS_PRIORITY[s] || 0; }
|
|
29232
29454
|
|
|
29233
|
-
app.get('/api/terminal/files', (req, res) => {
|
|
29455
|
+
app.get('/api/terminal/files', async (req, res) => {
|
|
29234
29456
|
const dir = req.query.path;
|
|
29235
29457
|
if (!dir || typeof dir !== 'string') return res.status(400).json({ error: 'path required' });
|
|
29236
29458
|
const search = (req.query.search || '').trim().toLowerCase();
|
|
@@ -29287,7 +29509,7 @@ app.get('/api/terminal/files', (req, res) => {
|
|
|
29287
29509
|
});
|
|
29288
29510
|
|
|
29289
29511
|
// Try git — gracefully ignored if unavailable
|
|
29290
|
-
const git = getGitInfo(dir);
|
|
29512
|
+
const git = await getGitInfo(dir, { signal: gitRequestSignal(res) });
|
|
29291
29513
|
if (git) {
|
|
29292
29514
|
for (const item of items) {
|
|
29293
29515
|
const s = git.statuses.get(item.name);
|
|
@@ -29301,11 +29523,11 @@ app.get('/api/terminal/files', (req, res) => {
|
|
|
29301
29523
|
}
|
|
29302
29524
|
});
|
|
29303
29525
|
|
|
29304
|
-
app.get('/api/terminal/branches', (req, res) => {
|
|
29526
|
+
app.get('/api/terminal/branches', async (req, res) => {
|
|
29305
29527
|
const dir = req.query.path;
|
|
29306
29528
|
if (!dir || typeof dir !== 'string') return res.status(400).json({ error: 'path required' });
|
|
29307
29529
|
|
|
29308
|
-
const inside = spawnGitRead(['rev-parse', '--is-inside-work-tree'], { cwd: dir, timeout: 3000 });
|
|
29530
|
+
const inside = await spawnGitRead(['rev-parse', '--is-inside-work-tree'], { cwd: dir, timeout: 3000 });
|
|
29309
29531
|
if (inside.status !== 0) {
|
|
29310
29532
|
logGitFailure('branches: rev-parse', inside.error || { stderr: inside.stderr });
|
|
29311
29533
|
return res.json({ branches: [], current: null });
|
|
@@ -29314,11 +29536,11 @@ app.get('/api/terminal/branches', (req, res) => {
|
|
|
29314
29536
|
try {
|
|
29315
29537
|
// Current branch — chained fallback (older git may not support --show-current)
|
|
29316
29538
|
let current = '';
|
|
29317
|
-
const showCurrent = spawnGitRead(['branch', '--show-current'], { cwd: dir, timeout: 3000 });
|
|
29539
|
+
const showCurrent = await spawnGitRead(['branch', '--show-current'], { cwd: dir, timeout: 3000 });
|
|
29318
29540
|
if (showCurrent.status === 0) current = showCurrent.stdout.toString().trim();
|
|
29319
29541
|
else logGitFailure('branches: show-current', showCurrent.error || { stderr: showCurrent.stderr });
|
|
29320
29542
|
if (!current) {
|
|
29321
|
-
const abbrev = spawnGitRead(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: dir, timeout: 3000 });
|
|
29543
|
+
const abbrev = await spawnGitRead(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: dir, timeout: 3000 });
|
|
29322
29544
|
if (abbrev.status === 0) {
|
|
29323
29545
|
const b = abbrev.stdout.toString().trim();
|
|
29324
29546
|
if (b && b !== 'HEAD') current = b;
|
|
@@ -29327,7 +29549,7 @@ app.get('/api/terminal/branches', (req, res) => {
|
|
|
29327
29549
|
|
|
29328
29550
|
// All local branches — plain `git branch` parsed manually; avoids --format=%(refname:short)
|
|
29329
29551
|
// which can get mangled by cmd.exe variable expansion on Windows even with proper quoting.
|
|
29330
|
-
const branchResult = spawnGitRead(['branch'], { cwd: dir });
|
|
29552
|
+
const branchResult = await spawnGitRead(['branch'], { cwd: dir });
|
|
29331
29553
|
if (branchResult.status !== 0) logGitFailure('branch list', branchResult.error || { stderr: branchResult.stderr });
|
|
29332
29554
|
const raw = branchResult.status === 0 ? branchResult.stdout.toString() : '';
|
|
29333
29555
|
// Plain `git branch` output: each line is " branch" or "* branch" (current). Strip
|
|
@@ -29345,7 +29567,7 @@ app.get('/api/terminal/branches', (req, res) => {
|
|
|
29345
29567
|
// Trust a workspace path with git — runs `git config --global --add safe.directory <path>`
|
|
29346
29568
|
// so git stops refusing to operate on it (CVE-2022-24765 dubious-ownership protection).
|
|
29347
29569
|
// Triggered by the add-project UI flows when the server detects the dubious-ownership error.
|
|
29348
|
-
app.post('/api/terminal/trust-workspace', (req, res) => {
|
|
29570
|
+
app.post('/api/terminal/trust-workspace', async (req, res) => {
|
|
29349
29571
|
const { path: dir } = req.body || {};
|
|
29350
29572
|
if (!dir || typeof dir !== 'string') return res.status(400).json({ ok: false, error: 'path required' });
|
|
29351
29573
|
if (/[\x00-\x1f]/.test(dir) || dir === '*' || dir === '/' || dir === '\\') {
|
|
@@ -29361,7 +29583,7 @@ app.post('/api/terminal/trust-workspace', (req, res) => {
|
|
|
29361
29583
|
}
|
|
29362
29584
|
|
|
29363
29585
|
const gitPath = normalizeGitPath(abs);
|
|
29364
|
-
const r = spawnGit(['config', '--global', '--add', 'safe.directory', gitPath]);
|
|
29586
|
+
const r = await spawnGit(['config', '--global', '--add', 'safe.directory', gitPath]);
|
|
29365
29587
|
if (r.status !== 0) {
|
|
29366
29588
|
const stderr = r.stderr?.toString?.()?.trim() || r.error?.message || 'git config failed';
|
|
29367
29589
|
logGitFailure('config --add safe.directory', r.error || { stderr: r.stderr });
|
|
@@ -29369,12 +29591,12 @@ app.post('/api/terminal/trust-workspace', (req, res) => {
|
|
|
29369
29591
|
}
|
|
29370
29592
|
|
|
29371
29593
|
// Confirm by re-probing
|
|
29372
|
-
const stillDubious = detectDubiousOwnership(abs);
|
|
29594
|
+
const stillDubious = await detectDubiousOwnership(abs);
|
|
29373
29595
|
console.log(`[git] trusted workspace: ${gitPath}${stillDubious ? ' (but still flagged — check permissions)' : ''}`);
|
|
29374
29596
|
res.json({ ok: true, path: gitPath, stillDubious });
|
|
29375
29597
|
});
|
|
29376
29598
|
|
|
29377
|
-
app.post('/api/terminal/checkout', (req, res) => {
|
|
29599
|
+
app.post('/api/terminal/checkout', async (req, res) => {
|
|
29378
29600
|
const { path: dir, branch } = req.body;
|
|
29379
29601
|
if (!dir || !branch) return res.status(400).json({ error: 'path and branch required' });
|
|
29380
29602
|
|
|
@@ -29383,7 +29605,7 @@ app.post('/api/terminal/checkout', (req, res) => {
|
|
|
29383
29605
|
return res.status(400).json({ error: 'Invalid branch name' });
|
|
29384
29606
|
}
|
|
29385
29607
|
|
|
29386
|
-
const inside = spawnGitRead(['rev-parse', '--is-inside-work-tree'], { cwd: dir, timeout: 3000 });
|
|
29608
|
+
const inside = await spawnGitRead(['rev-parse', '--is-inside-work-tree'], { cwd: dir, timeout: 3000 });
|
|
29387
29609
|
if (inside.status !== 0) {
|
|
29388
29610
|
logGitFailure('checkout: rev-parse', inside.error || { stderr: inside.stderr });
|
|
29389
29611
|
return res.status(400).json({ error: 'Not a git repository' });
|
|
@@ -29393,7 +29615,8 @@ app.post('/api/terminal/checkout', (req, res) => {
|
|
|
29393
29615
|
// No '--' separator — that would make git treat the branch name as a file pathspec.
|
|
29394
29616
|
// (The switch itself is a write and runs plainly, with the repository's post-checkout hook; the two questions
|
|
29395
29617
|
// around it are reads.)
|
|
29396
|
-
const result = spawnGit(['checkout', branch], { cwd: dir, timeout: 10000 });
|
|
29618
|
+
const result = await spawnGit(['checkout', branch], { cwd: dir, timeout: 10000 });
|
|
29619
|
+
gitStatusRunner.invalidate(dir);
|
|
29397
29620
|
if (result.status !== 0) {
|
|
29398
29621
|
const stderr = result.stderr?.toString?.()?.trim() || result.error?.message || 'Checkout failed';
|
|
29399
29622
|
return res.status(500).json({ error: stderr });
|
|
@@ -29401,71 +29624,21 @@ app.post('/api/terminal/checkout', (req, res) => {
|
|
|
29401
29624
|
|
|
29402
29625
|
// Return confirmed branch name + git's own output
|
|
29403
29626
|
let current = '';
|
|
29404
|
-
const showCurrent = spawnGitRead(['branch', '--show-current'], { cwd: dir, timeout: 3000 });
|
|
29627
|
+
const showCurrent = await spawnGitRead(['branch', '--show-current'], { cwd: dir, timeout: 3000 });
|
|
29405
29628
|
if (showCurrent.status === 0) current = showCurrent.stdout.toString().trim();
|
|
29406
29629
|
else logGitFailure('checkout: show-current', showCurrent.error || { stderr: showCurrent.stderr });
|
|
29407
29630
|
const output = result.stderr?.toString?.()?.trim() || `Switched to branch '${current}'`;
|
|
29408
29631
|
res.json({ ok: true, branch: current, output });
|
|
29409
29632
|
});
|
|
29410
29633
|
|
|
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 };
|
|
29634
|
+
// All callers share the canonical worktree's flight and last completed result.
|
|
29635
|
+
async function computeGitStatus(dir, options = {}) {
|
|
29636
|
+
const result = projectStatus(await gitStatusRunner.read(dir, options), dir);
|
|
29637
|
+
if (!result.ok || result.partial || result.stale) return { ok: false, isGit: !!result.isGit, branch: result.branch, changes: result.changes || [],
|
|
29638
|
+
partial: true, stale: !!result.stale, timedOut: !!result.timedOut, retryAt: result.retryAt,
|
|
29639
|
+
scanState: result.timedOut ? 'timed-out' : 'failed', error: 'Git status is incomplete. Refresh before switching branches or assuming the worktree is clean.' };
|
|
29640
|
+
if (!result.isGit) return { ok: true, isGit: false, branch: null, changes: [] };
|
|
29641
|
+
return { ok: true, isGit: true, branch: result.branch, changes: result.changes };
|
|
29469
29642
|
}
|
|
29470
29643
|
|
|
29471
29644
|
// The folder a git route is asked about: inside a registered folder, as the disk resolves it (the rule of
|
|
@@ -29479,11 +29652,9 @@ function registeredGitFolder(asked, res) {
|
|
|
29479
29652
|
// be started, failed or took too long, as the command lines the /api/git routes used to run did. For reads only
|
|
29480
29653
|
// (every caller is one): run as lib/git-read.js runs a read, so no program the repository's own configuration
|
|
29481
29654
|
// 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`);
|
|
29655
|
+
async function gitOutput(dir, args, { timeout = 5000, maxBuffer } = {}) {
|
|
29656
|
+
const result = await gitStatusRunner.run(args, { cwd: dir, timeoutMs: timeout, ...(maxBuffer ? { maxBuffer } : {}) });
|
|
29657
|
+
if (!result.ok) throw new Error(result.stderr?.toString?.()?.trim?.() || `git ${args[0]} failed`);
|
|
29487
29658
|
return result.stdout.toString();
|
|
29488
29659
|
}
|
|
29489
29660
|
|
|
@@ -29493,37 +29664,17 @@ function gitOutput(dir, args, { timeout = 5000, maxBuffer } = {}) {
|
|
|
29493
29664
|
app.get('/api/git/status', editorOwnerGuard, async (req, res) => {
|
|
29494
29665
|
const dir = req.query.path;
|
|
29495
29666
|
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
|
-
}
|
|
29667
|
+
if (req.query.refresh === '1') gitStatusRunner.invalidate(dir, { refresh: true });
|
|
29668
|
+
res.json(await computeGitStatus(dir, { signal: gitRequestSignal(res) }));
|
|
29518
29669
|
});
|
|
29519
29670
|
|
|
29520
|
-
app.post('/api/git/commit', editorOwnerGuard, (req, res) => {
|
|
29671
|
+
app.post('/api/git/commit', editorOwnerGuard, async (req, res) => {
|
|
29521
29672
|
const { path: dir, message, files } = req.body;
|
|
29522
29673
|
if (!dir || typeof dir !== 'string' || !message) return res.status(400).json({ error: 'path and message required' });
|
|
29523
29674
|
if (typeof message !== 'string' || message.trim().length === 0) return res.status(400).json({ error: 'Commit message cannot be empty' });
|
|
29524
29675
|
|
|
29525
29676
|
try {
|
|
29526
|
-
gitOutput(dir, ['rev-parse', '--is-inside-work-tree'], { timeout: 3000 });
|
|
29677
|
+
await gitOutput(dir, ['rev-parse', '--is-inside-work-tree'], { timeout: 3000 });
|
|
29527
29678
|
} catch {
|
|
29528
29679
|
return res.status(400).json({ error: 'Not a git repository' });
|
|
29529
29680
|
}
|
|
@@ -29535,16 +29686,19 @@ app.post('/api/git/commit', editorOwnerGuard, (req, res) => {
|
|
|
29535
29686
|
for (const f of files) {
|
|
29536
29687
|
if (typeof f !== 'string' || f.includes('..')) return res.status(400).json({ error: 'Invalid file path' });
|
|
29537
29688
|
}
|
|
29538
|
-
const result =
|
|
29689
|
+
const result = await gitStatusRunner.runRaw(['add', '--', ...files], { cwd: dir, timeoutMs: 10000 });
|
|
29690
|
+
gitStatusRunner.invalidate(dir);
|
|
29539
29691
|
if (result.status !== 0) return res.status(500).json({ error: result.stderr?.toString?.()?.trim() || 'Stage failed' });
|
|
29540
29692
|
} else {
|
|
29541
29693
|
// Stage all changes
|
|
29542
|
-
const result =
|
|
29694
|
+
const result = await gitStatusRunner.runRaw(['add', '-A'], { cwd: dir, timeoutMs: 10000 });
|
|
29695
|
+
gitStatusRunner.invalidate(dir);
|
|
29543
29696
|
if (result.status !== 0) return res.status(500).json({ error: result.stderr?.toString?.()?.trim() || 'Stage failed' });
|
|
29544
29697
|
}
|
|
29545
29698
|
|
|
29546
29699
|
// Commit
|
|
29547
|
-
const result =
|
|
29700
|
+
const result = await gitStatusRunner.runRaw(['commit', '-m', message.trim()], { cwd: dir, timeoutMs: 15000 });
|
|
29701
|
+
gitStatusRunner.invalidate(dir);
|
|
29548
29702
|
if (result.status !== 0) {
|
|
29549
29703
|
const stderr = result.stderr?.toString?.()?.trim() || '';
|
|
29550
29704
|
const stdout = result.stdout?.toString?.()?.trim() || '';
|
|
@@ -29558,32 +29712,32 @@ app.post('/api/git/commit', editorOwnerGuard, (req, res) => {
|
|
|
29558
29712
|
}
|
|
29559
29713
|
});
|
|
29560
29714
|
|
|
29561
|
-
app.get('/api/git/diff-summary', editorOwnerGuard, (req, res) => {
|
|
29715
|
+
app.get('/api/git/diff-summary', editorOwnerGuard, async (req, res) => {
|
|
29562
29716
|
const asked = req.query.path;
|
|
29563
29717
|
if (!asked || typeof asked !== 'string') return res.status(400).json({ error: 'path required' });
|
|
29564
29718
|
const dir = registeredGitFolder(asked, res);
|
|
29565
29719
|
if (!dir) return;
|
|
29566
29720
|
|
|
29567
29721
|
try {
|
|
29568
|
-
gitOutput(dir, ['rev-parse', '--is-inside-work-tree'], { timeout: 3000 });
|
|
29722
|
+
await gitOutput(dir, ['rev-parse', '--is-inside-work-tree'], { timeout: 3000 });
|
|
29569
29723
|
} catch {
|
|
29570
29724
|
return res.status(400).json({ error: 'Not a git repository' });
|
|
29571
29725
|
}
|
|
29572
29726
|
|
|
29573
29727
|
try {
|
|
29574
29728
|
// Get diff stat for tracked files
|
|
29575
|
-
const stat = gitOutput(dir, ['diff', '--stat', '--stat-width=120']).trim();
|
|
29729
|
+
const stat = (await gitOutput(dir, ['diff', '--stat', '--stat-width=120'])).trim();
|
|
29576
29730
|
|
|
29577
29731
|
// Get diff stat for staged files
|
|
29578
|
-
const stagedStat = gitOutput(dir, ['diff', '--cached', '--stat', '--stat-width=120']).trim();
|
|
29732
|
+
const stagedStat = (await gitOutput(dir, ['diff', '--cached', '--stat', '--stat-width=120'])).trim();
|
|
29579
29733
|
|
|
29580
29734
|
// Get untracked files
|
|
29581
|
-
const untracked = gitOutput(dir, ['ls-files', '--others', '--exclude-standard']).trim();
|
|
29735
|
+
const untracked = (await gitOutput(dir, ['ls-files', '--others', '--exclude-standard'])).trim();
|
|
29582
29736
|
|
|
29583
29737
|
// 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);
|
|
29738
|
+
const diff = (await gitOutput(dir, ['diff', '-U1', '--no-color'], { timeout: 8000, maxBuffer: 64 * 1024 })).substring(0, 4000);
|
|
29585
29739
|
|
|
29586
|
-
const stagedDiff = gitOutput(dir, ['diff', '--cached', '-U1', '--no-color'], { timeout: 8000, maxBuffer: 64 * 1024 }).substring(0, 4000);
|
|
29740
|
+
const stagedDiff = (await gitOutput(dir, ['diff', '--cached', '-U1', '--no-color'], { timeout: 8000, maxBuffer: 64 * 1024 })).substring(0, 4000);
|
|
29587
29741
|
|
|
29588
29742
|
// Build a commit message from the changes
|
|
29589
29743
|
const changedFiles = [];
|
|
@@ -29651,7 +29805,7 @@ app.use(createGitDiffRouter({ fileRoots: allowedFileRoots, guard: editorOwnerGua
|
|
|
29651
29805
|
|
|
29652
29806
|
// ── Git Log ──
|
|
29653
29807
|
|
|
29654
|
-
app.get('/api/git/log', editorOwnerGuard, (req, res) => {
|
|
29808
|
+
app.get('/api/git/log', editorOwnerGuard, async (req, res) => {
|
|
29655
29809
|
const asked = req.query.path;
|
|
29656
29810
|
if (!asked || typeof asked !== 'string') return res.status(400).json({ error: 'path required' });
|
|
29657
29811
|
const dir = registeredGitFolder(asked, res);
|
|
@@ -29659,13 +29813,13 @@ app.get('/api/git/log', editorOwnerGuard, (req, res) => {
|
|
|
29659
29813
|
const count = Math.min(Math.max(parseInt(req.query.count) || 10, 1), 50);
|
|
29660
29814
|
|
|
29661
29815
|
try {
|
|
29662
|
-
gitOutput(dir, ['rev-parse', '--is-inside-work-tree'], { timeout: 3000 });
|
|
29816
|
+
await gitOutput(dir, ['rev-parse', '--is-inside-work-tree'], { timeout: 3000 });
|
|
29663
29817
|
} catch {
|
|
29664
29818
|
return res.status(400).json({ error: 'Not a git repository' });
|
|
29665
29819
|
}
|
|
29666
29820
|
|
|
29667
29821
|
try {
|
|
29668
|
-
const raw = gitOutput(dir, ['log', '--oneline', '--no-decorate', '-n', String(count)]).trim();
|
|
29822
|
+
const raw = (await gitOutput(dir, ['log', '--oneline', '--no-decorate', '-n', String(count)])).trim();
|
|
29669
29823
|
const commits = raw ? raw.split('\n').map(line => {
|
|
29670
29824
|
const spaceIdx = line.indexOf(' ');
|
|
29671
29825
|
return { hash: line.substring(0, spaceIdx), message: line.substring(spaceIdx + 1) };
|
|
@@ -29683,17 +29837,17 @@ app.post('/api/git/generate-message', editorOwnerGuard, async (req, res) => {
|
|
|
29683
29837
|
if (!dir || typeof dir !== 'string') return res.status(400).json({ error: 'path required' });
|
|
29684
29838
|
|
|
29685
29839
|
try {
|
|
29686
|
-
gitOutput(dir, ['rev-parse', '--is-inside-work-tree'], { timeout: 3000 });
|
|
29840
|
+
await gitOutput(dir, ['rev-parse', '--is-inside-work-tree'], { timeout: 3000 });
|
|
29687
29841
|
} catch {
|
|
29688
29842
|
return res.status(400).json({ error: 'Not a git repository' });
|
|
29689
29843
|
}
|
|
29690
29844
|
|
|
29691
29845
|
// Gather diff data — separate try/catch per command so one failure doesn't skip the rest
|
|
29692
29846
|
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 {}
|
|
29847
|
+
try { diffText = (await gitOutput(dir, ['diff', '--no-color'], { timeout: 8000, maxBuffer: 2 * 1024 * 1024 })).substring(0, 4000); } catch {}
|
|
29848
|
+
try { stagedText = (await gitOutput(dir, ['diff', '--cached', '--no-color'], { timeout: 8000, maxBuffer: 2 * 1024 * 1024 })).substring(0, 4000); } catch {}
|
|
29849
|
+
try { untrackedList = (await gitOutput(dir, ['ls-files', '--others', '--exclude-standard'])).trim(); } catch {}
|
|
29850
|
+
try { recentCommits = (await gitOutput(dir, ['log', '--oneline', '--no-decorate', '-n', '5'], { timeout: 3000 })).trim(); } catch {}
|
|
29697
29851
|
|
|
29698
29852
|
const allDiff = (diffText + '\n' + stagedText).trim();
|
|
29699
29853
|
if (!allDiff && !untrackedList) {
|
|
@@ -29744,8 +29898,8 @@ app.post('/api/git/generate-message', editorOwnerGuard, async (req, res) => {
|
|
|
29744
29898
|
// Fallback: heuristic (same logic as /api/git/diff-summary)
|
|
29745
29899
|
try {
|
|
29746
29900
|
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 {}
|
|
29901
|
+
try { stat = (await gitOutput(dir, ['diff', '--stat', '--stat-width=120'], { maxBuffer: 2 * 1024 * 1024 })).trim(); } catch {}
|
|
29902
|
+
try { stagedStat = (await gitOutput(dir, ['diff', '--cached', '--stat', '--stat-width=120'], { maxBuffer: 2 * 1024 * 1024 })).trim(); } catch {}
|
|
29749
29903
|
const changedFiles = [];
|
|
29750
29904
|
for (const line of (stat + '\n' + stagedStat).split('\n').filter(l => l.includes('|'))) {
|
|
29751
29905
|
const m = line.match(/^\s*(.+?)\s*\|/);
|
|
@@ -29823,7 +29977,7 @@ app.put('/api/cli/config', (req, res) => {
|
|
|
29823
29977
|
app.post('/api/cli/detect/:profileId', async (req, res) => {
|
|
29824
29978
|
try {
|
|
29825
29979
|
const { profileId } = req.params;
|
|
29826
|
-
if (!['claude-code', 'codex', '
|
|
29980
|
+
if (!['claude-code', 'codex', 'antigravity', 'opencode'].includes(profileId)) return res.status(400).json({ ok: false, error: 'Unknown profile' });
|
|
29827
29981
|
const status = await cliResolver.resolve(profileId, { force: true, configured: null });
|
|
29828
29982
|
res.json({
|
|
29829
29983
|
ok: true,
|
|
@@ -29873,7 +30027,7 @@ function stripAnsi(str) {
|
|
|
29873
30027
|
// stdin: false — message appended as the last CLI arg (flag takes a value)
|
|
29874
30028
|
const CLI_SIDECAR_CONFIG = {
|
|
29875
30029
|
'claude-code': { args: ['-p', '--verbose'], stdin: true }, // -p = non-interactive mode; reads prompt from stdin
|
|
29876
|
-
'
|
|
30030
|
+
'antigravity': { args: ['-p'], stdin: false }, // agy -p <prompt>: one print-mode turn
|
|
29877
30031
|
'codex': { args: ['-q'], stdin: false }, // -q <msg>; flag takes a value
|
|
29878
30032
|
'shell': null,
|
|
29879
30033
|
};
|
|
@@ -29888,7 +30042,7 @@ const CLI_LIVE_CONFIG = {
|
|
|
29888
30042
|
maxIdleMs: 120000, // 2 min absolute max wait
|
|
29889
30043
|
useBracketedPaste: true,
|
|
29890
30044
|
},
|
|
29891
|
-
'
|
|
30045
|
+
'antigravity': {
|
|
29892
30046
|
promptPatterns: [/>\s*$/m, /\u276F\s*$/m],
|
|
29893
30047
|
idleMs: 3000,
|
|
29894
30048
|
maxIdleMs: 120000,
|
|
@@ -30005,7 +30159,7 @@ function spawnSidecarProcess(linkId, sessionId, message) {
|
|
|
30005
30159
|
const sidecarConfig = CLI_SIDECAR_CONFIG[profile];
|
|
30006
30160
|
if (!sidecarConfig) throw new Error(`No sidecar config for profile: ${profile}`);
|
|
30007
30161
|
|
|
30008
|
-
// Get the CLI command from config (e.g. 'claude', '
|
|
30162
|
+
// Get the CLI command from config (e.g. 'claude', 'agy', 'codex')
|
|
30009
30163
|
const cliConfig = loadCliConfig();
|
|
30010
30164
|
const command = (cliConfig[profile]?.command || '').trim();
|
|
30011
30165
|
if (!command) throw new Error(`No CLI command configured for ${profile}`);
|
|
@@ -30025,7 +30179,7 @@ function spawnSidecarProcess(linkId, sessionId, message) {
|
|
|
30025
30179
|
// stdin-based: args are fixed, message piped after spawn (e.g. Claude Code)
|
|
30026
30180
|
} else if (IS_WIN) {
|
|
30027
30181
|
// Windows: cmd.exe shell quoting mangles multi-word args (splits by spaces),
|
|
30028
|
-
// causing CLIs
|
|
30182
|
+
// causing CLIs that take a value after -p (agy) to misparse -p "hello world" as -p + positional "hello" "world".
|
|
30029
30183
|
// Use PowerShell which correctly passes $msg as a single argument value.
|
|
30030
30184
|
tmpFile = join(os.tmpdir(), `synabun-relay-${randomBytes(4).toString('hex')}.txt`);
|
|
30031
30185
|
writeFileSync(tmpFile, message, 'utf-8');
|
|
@@ -32391,8 +32545,10 @@ async function createBrowserSession(options = {}) {
|
|
|
32391
32545
|
|| savedCfg.connectMode === 'morelogin';
|
|
32392
32546
|
if (_wantMoreLogin) {
|
|
32393
32547
|
try {
|
|
32394
|
-
|
|
32395
|
-
|
|
32548
|
+
// An install outside the known folders (a custom folder on Windows) still
|
|
32549
|
+
// counts when its local API answers.
|
|
32550
|
+
if (!moreLogin.isMoreLoginInstalled().installed && !(await moreLogin.probeMoreLoginApi({ savedCfg })).running) {
|
|
32551
|
+
throw new Error(`MoreLogin desktop app was not found and its local API does not answer on port ${moreLogin.getMoreLoginPort(savedCfg)}`);
|
|
32396
32552
|
}
|
|
32397
32553
|
// Installed but possibly not running (e.g. an autonomous schedule fires with
|
|
32398
32554
|
// nobody at the machine to open it) — auto-launch and wait for its local API
|
|
@@ -33730,30 +33886,41 @@ app.use('/api/browser', (req, res, next) => { res.setHeader('X-Synabun-Browser-D
|
|
|
33730
33886
|
// action, a new tab, CDP). Closing it, or listing its tabs, stays allowed.
|
|
33731
33887
|
app.use('/api/browser/sessions/:id', (req, res, next) => {
|
|
33732
33888
|
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
|
-
|
|
33889
|
+
const exempt = req.method === 'DELETE' || (req.method === 'GET' && req.path === '/tabs');
|
|
33890
|
+
const gate = () => {
|
|
33891
|
+
const unsafe = protectedContext?.__browserNetworkGuardError;
|
|
33892
|
+
if (unsafe && !exempt) {
|
|
33893
|
+
return res.status(503).json({ error: `Browser network protection unavailable: ${unsafe}`, networkGuard: true });
|
|
33894
|
+
}
|
|
33895
|
+
// Also cancel requests already waiting/probing when the protection disappears.
|
|
33896
|
+
// Cancellation prevents their next page action; an action that already began
|
|
33897
|
+
// retains the existing truthful "outcome uncertain" response semantics.
|
|
33898
|
+
if (req.method !== 'DELETE' && protectedContext?.on) {
|
|
33899
|
+
const cancel = reason => req.browserRequest?.cancelForProtection?.(reason);
|
|
33900
|
+
protectedContext.on('synabun-network-guard-fault', cancel);
|
|
33901
|
+
const release = () => protectedContext.removeListener('synabun-network-guard-fault', cancel);
|
|
33902
|
+
res.once('finish', release);
|
|
33903
|
+
res.once('close', release);
|
|
33904
|
+
}
|
|
33905
|
+
if (!req.get('X-Synabun-Terminal') || exempt) return next();
|
|
33906
|
+
const session = browserSessions.get(req.params.id);
|
|
33907
|
+
if (!session) return next();
|
|
33908
|
+
const cfg = loadBrowserConfig();
|
|
33909
|
+
if (cfg.connectMode !== 'morelogin' || sessionServesMoreLoginDefault(session, cfg)) return next();
|
|
33910
|
+
const envId = moreLoginDefaultEnvId(cfg);
|
|
33911
|
+
res.status(409).json({
|
|
33912
|
+
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.`,
|
|
33913
|
+
code: 'NOT_DEFAULT_BROWSER',
|
|
33914
|
+
});
|
|
33915
|
+
};
|
|
33916
|
+
// A faulted network guard re-arms on demand: a fresh protected attachment replaces
|
|
33917
|
+
// the broken one before this request runs, so a session never needs a server
|
|
33918
|
+
// restart to recover. Until a re-arm succeeds every action stays refused.
|
|
33919
|
+
if (protectedContext?.__browserNetworkGuardError && !exempt) {
|
|
33920
|
+
recoverBrowserNetworkGuard(protectedContext).then(gate).catch(next);
|
|
33921
|
+
return;
|
|
33922
|
+
}
|
|
33923
|
+
return gate();
|
|
33757
33924
|
});
|
|
33758
33925
|
// Read-only semantic context for the browser judgments (the judgments themselves run in the MCP layer).
|
|
33759
33926
|
app.use('/api/browser', createBrowserAssistRouter({ browserSessions, getTargetPage, normalizeSelector }));
|
|
@@ -35560,7 +35727,7 @@ app.post('/api/browser/sessions/:id/upload', async (req, res) => {
|
|
|
35560
35727
|
});
|
|
35561
35728
|
|
|
35562
35729
|
// Browser config endpoint
|
|
35563
|
-
app.get('/api/browser/config', (req, res) => {
|
|
35730
|
+
app.get('/api/browser/config', async (req, res) => {
|
|
35564
35731
|
const config = loadBrowserConfig();
|
|
35565
35732
|
const detected = findBrowserExecutable();
|
|
35566
35733
|
// Check if Chrome is currently running (informs profile mirroring warnings)
|
|
@@ -35581,7 +35748,11 @@ app.get('/api/browser/config', (req, res) => {
|
|
|
35581
35748
|
let attachSeeded = false;
|
|
35582
35749
|
try { attachSeeded = existsSync(getAttachProfileRoot()); } catch {}
|
|
35583
35750
|
let moreloginInstalled = false;
|
|
35584
|
-
|
|
35751
|
+
// The exe in a known folder, or a local API that answers (a custom install folder): a short probe only when needed.
|
|
35752
|
+
try {
|
|
35753
|
+
moreloginInstalled = moreLogin.isMoreLoginPresent()
|
|
35754
|
+
|| (await moreLogin.probeMoreLoginApi({ savedCfg: config, timeout: 1500 })).running;
|
|
35755
|
+
} catch {}
|
|
35585
35756
|
const unreadable = browserConfigUnreadable(config);
|
|
35586
35757
|
res.json({ config, detectedPath: detected, chromeRunning, attachSeeded, moreloginInstalled, ...(unreadable && { configError: `browser-config.json is unreadable (${unreadable}); saving these settings replaces it` }) });
|
|
35587
35758
|
});
|
|
@@ -35626,12 +35797,20 @@ app.get('/api/browser/detect-profiles', (req, res) => {
|
|
|
35626
35797
|
app.get('/api/browser/morelogin-envs', async (req, res) => {
|
|
35627
35798
|
try {
|
|
35628
35799
|
const cfg = loadBrowserConfig();
|
|
35629
|
-
const installed = moreLogin.isMoreLoginInstalled().installed;
|
|
35630
35800
|
const port = moreLogin.getMoreLoginPort(cfg);
|
|
35631
|
-
|
|
35801
|
+
// Probe first: the install-folder check knows only the default folders, and an
|
|
35802
|
+
// answering local API proves the app is there wherever it was installed.
|
|
35632
35803
|
const probe = await moreLogin.probeMoreLoginApi({ savedCfg: cfg, port });
|
|
35804
|
+
const installed = probe.running || moreLogin.isMoreLoginInstalled().installed;
|
|
35805
|
+
if (!installed) return res.json({ ok: true, installed: false, running: false, port, envs: [], error: probe.error || null });
|
|
35633
35806
|
if (!probe.running) return res.json({ ok: true, installed: true, running: false, port, envs: [], error: probe.error || null });
|
|
35807
|
+
// The API answered with an error (signature required, API off): say why, never an empty list.
|
|
35808
|
+
if (probe.apiError) return res.json({ ok: false, installed: true, running: true, port, envs: [], error: probe.apiError });
|
|
35634
35809
|
const envs = await moreLogin.listProfiles({ savedCfg: cfg, port });
|
|
35810
|
+
// The API counted profiles but none could be read: a shape problem, never "no profiles yet".
|
|
35811
|
+
if (probe.count > 0 && !envs.length) {
|
|
35812
|
+
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.` });
|
|
35813
|
+
}
|
|
35635
35814
|
res.json({
|
|
35636
35815
|
ok: true, installed: true, running: true, port, envs,
|
|
35637
35816
|
// Observed website-policy blocks — MoreLogin's local API never exposes the
|
|
@@ -35882,10 +36061,11 @@ const httpServer = app.listen(PORT, async () => {
|
|
|
35882
36061
|
// Activation is an explicit user choice via Settings → Browser / the MoreLogin tab.
|
|
35883
36062
|
setTimeout(async () => {
|
|
35884
36063
|
try {
|
|
35885
|
-
|
|
35886
|
-
const cfg =
|
|
36064
|
+
const mlBootCfg = loadBrowserConfig();
|
|
36065
|
+
const cfg = mlBootCfg;
|
|
35887
36066
|
const port = moreLogin.getMoreLoginPort(cfg);
|
|
35888
36067
|
const probe = await moreLogin.probeMoreLoginApi({ savedCfg: cfg, port });
|
|
36068
|
+
if (!probe.running && !moreLogin.isMoreLoginInstalled().installed) return;
|
|
35889
36069
|
const isDefault = cfg.connectMode === 'morelogin';
|
|
35890
36070
|
console.log(` MoreLogin: detected${probe.running ? ` · API on :${port} (${probe.count} profile(s))` : ' · app not running'}${isDefault ? ' · default AI browser' : ''}`);
|
|
35891
36071
|
} catch { /* best-effort */ }
|
|
@@ -35949,6 +36129,12 @@ const httpServer = app.listen(PORT, async () => {
|
|
|
35949
36129
|
if (oc.updated > 0) console.log(` Session cache (opencode): indexed ${oc.updated}/${oc.sessions}`);
|
|
35950
36130
|
} catch (err) { console.warn(' Session cache (opencode) error:', err.message); }
|
|
35951
36131
|
},
|
|
36132
|
+
() => {
|
|
36133
|
+
const agy = refreshAntigravityCatalog({ force: true });
|
|
36134
|
+
if (agy?.updated > 0) console.log(` Session cache (antigravity): indexed ${agy.updated}/${agy.sessions}`);
|
|
36135
|
+
// Runs and brains end on their own: their conversations are picked up by this pass, every five minutes.
|
|
36136
|
+
setInterval(() => refreshAntigravityCatalog({ force: true }), 5 * 60 * 1000).unref?.();
|
|
36137
|
+
},
|
|
35952
36138
|
];
|
|
35953
36139
|
const next = () => {
|
|
35954
36140
|
const pass = passes.shift();
|
|
@@ -35988,6 +36174,18 @@ const httpServer = app.listen(PORT, async () => {
|
|
|
35988
36174
|
console.warn(' Hook dedup warning:', err.message);
|
|
35989
36175
|
}
|
|
35990
36176
|
|
|
36177
|
+
// Antigravity replaced Gemini CLI: what was saved under `gemini` (a CLI path, a keybind, loop templates and
|
|
36178
|
+
// schedules, the launch profile and resume tab in the UI state) is read as Antigravity's from this boot on
|
|
36179
|
+
// (lib/antigravity/migrate-gemini.js). Idempotent; a file it cannot read is left as it is. Never stops the boot.
|
|
36180
|
+
try {
|
|
36181
|
+
const migrated = migrateGeminiFiles({ dataHome: DATA_HOME, log: (line) => console.log(` ${line}`) });
|
|
36182
|
+
if (migrated.changed.includes('cli-config.json')) { _cliConfigCache = null; cliResolver.invalidate(); }
|
|
36183
|
+
const ui = loadUiState();
|
|
36184
|
+
if (migrateGeminiUiState(ui)) saveUiState(ui);
|
|
36185
|
+
} catch (err) {
|
|
36186
|
+
console.warn(' Antigravity migration warning:', err.message);
|
|
36187
|
+
}
|
|
36188
|
+
|
|
35991
36189
|
// Keep the installed SynaBun rules current: an untouched copy of an older
|
|
35992
36190
|
// ruleset is rewritten (unless the user switched that off), an edited copy is
|
|
35993
36191
|
// left alone, a deleted one counts as an opt-out. Never installs for a tool
|
|
@@ -36003,6 +36201,16 @@ const httpServer = app.listen(PORT, async () => {
|
|
|
36003
36201
|
console.warn(' Rules reconcile warning:', err.message);
|
|
36004
36202
|
}
|
|
36005
36203
|
|
|
36204
|
+
// SynaBun's Antigravity homes (lib/antigravity/home.js): a run's home lives a week past its last use (a run resumes
|
|
36205
|
+
// within minutes of a restart), an Assistant brain's two months (its conversation can be picked up later).
|
|
36206
|
+
try {
|
|
36207
|
+
const root = antigravityHomesRoot(DATA_HOME);
|
|
36208
|
+
const removed = [...pruneAntigravityHomes(root, { kinds: ['run', 'models'], keep: antigravityPanel.activeHomes() }), ...pruneAntigravityHomes(root, { kinds: ['brain'], maxAgeMs: 60 * 24 * 60 * 60_000 })];
|
|
36209
|
+
if (removed.length) console.log(` Antigravity: removed ${removed.length} unused SynaBun home${removed.length === 1 ? '' : 's'}`);
|
|
36210
|
+
} catch (err) {
|
|
36211
|
+
console.warn(' Antigravity homes warning:', err.message);
|
|
36212
|
+
}
|
|
36213
|
+
|
|
36006
36214
|
// Clean up orphaned loop state files from previous server runs.
|
|
36007
36215
|
// After a restart, PTY sessions are gone but loop files remain with awaitingNext: true.
|
|
36008
36216
|
// No terminal session exists to drive them, so delete them to prevent ghost loops.
|
|
@@ -36134,7 +36342,7 @@ httpServer.on('upgrade', function onUpgrade(req, socket, head) {
|
|
|
36134
36342
|
if (guest) {
|
|
36135
36343
|
if (url.pathname.startsWith('/ws/terminal/') && !invitePermissions.terminal) { socket.destroy(); return; }
|
|
36136
36344
|
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/')) {
|
|
36345
|
+
if (['/ws/claude-skin', '/ws/codex-skin', '/ws/opencode-v2', '/ws/antigravity'].includes(url.pathname) || url.pathname.startsWith('/ws/assistant/')) {
|
|
36138
36346
|
socket.destroy();
|
|
36139
36347
|
return;
|
|
36140
36348
|
}
|
|
@@ -36165,7 +36373,7 @@ httpServer.on('upgrade', function onUpgrade(req, socket, head) {
|
|
|
36165
36373
|
return;
|
|
36166
36374
|
}
|
|
36167
36375
|
|
|
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') {
|
|
36376
|
+
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
36377
|
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
36170
36378
|
wss.emit('connection', ws, req);
|
|
36171
36379
|
});
|
|
@@ -36232,6 +36440,12 @@ wss.on('connection', (ws, req) => {
|
|
|
36232
36440
|
return;
|
|
36233
36441
|
}
|
|
36234
36442
|
|
|
36443
|
+
// ── Route: Antigravity side panel (Google's agy over ACP) ──
|
|
36444
|
+
if (url.pathname === '/ws/antigravity') {
|
|
36445
|
+
antigravityPanel.handleSocket(ws);
|
|
36446
|
+
return;
|
|
36447
|
+
}
|
|
36448
|
+
|
|
36235
36449
|
// ── Terminal sessions ── are handed off to the pty-host in the upgrade
|
|
36236
36450
|
// handler (lib/pty-host); nothing else should reach this point.
|
|
36237
36451
|
try { ws.close(1008, 'Unsupported WebSocket path'); } catch {}
|
|
@@ -36816,11 +37030,15 @@ process.on('exit', () => {
|
|
|
36816
37030
|
try { stopOpencodeServer(); } catch {}
|
|
36817
37031
|
try { editorCompletionServe.stop('exit'); } catch {}
|
|
36818
37032
|
try { _claudeBridge?.shutdownAllBridges?.(); } catch {}
|
|
37033
|
+
try { antigravityPanel.closeAll(); } catch {}
|
|
36819
37034
|
try { saveSessionSnapshot(); } catch {}
|
|
36820
37035
|
try { flushUiStateSync(); } catch {}
|
|
36821
37036
|
try { flushWhiteboardSync(); } catch {}
|
|
36822
37037
|
try { flushCostDataSync(); } catch {}
|
|
36823
37038
|
try { memoryMap.flushSync(); } catch {}
|
|
37039
|
+
// The Assistant's session records (a rename among them): this handler runs before the Assistant's own
|
|
37040
|
+
// exit handler, which the SIGKILL below never lets run.
|
|
37041
|
+
try { _assistantRuntime?.flushIndex?.(); } catch {}
|
|
36824
37042
|
if (_preserveExitCode) return;
|
|
36825
37043
|
try { process.kill(process.pid, 'SIGKILL'); } catch {}
|
|
36826
37044
|
});
|