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