diff --git a/AGENTS.md b/AGENTS.md
index 595b63e..d89097f 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -35,7 +35,8 @@ and you **must** pause there — do not bypass prompts or run with
it themselves** — you should not pipe input into it.
Up front the user picks which MCP hosts to wire: Claude Code,
- Gemini CLI, Codex CLI, GitHub Copilot CLI, Cursor, or none.
+ Gemini CLI, Codex CLI, GitHub Copilot CLI, Cursor, VS Code,
+ Antigravity, or none.
OpenClaw is a separate yes/no step later in setup. If Claude Code is
selected, there are two scope decisions that only the user can make:
diff --git a/README.md b/README.md
index b5a3bfd..afcd114 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,8 @@
+
+
diff --git a/cmd/clawdchan/agents.go b/cmd/clawdchan/agents.go
index a0728dd..5d316b9 100644
--- a/cmd/clawdchan/agents.go
+++ b/cmd/clawdchan/agents.go
@@ -58,6 +58,8 @@ func allAgents() []*agentWiring {
codexAgent(),
copilotAgent(),
cursorAgent(),
+ vscodeAgent(),
+ antigravityAgent(),
}
}
@@ -99,15 +101,18 @@ func describeScopeFlag(a *agentWiring, scope string) string {
}
// mcpScopeChoices returns the MCP-registration scope names the agent
-// supports. Gemini and CC support user+project; Codex and Copilot are
-// user-only (per their upstream docs).
+// supports. Gemini and CC support user+project; the rest are user-only
+// (per their upstream docs).
func mcpScopeChoices(a *agentWiring) []string {
switch a.key {
case "cc", "gemini":
return []string{"user", "project"}
default:
- // Codex, Copilot, and Cursor are user-scope only. Cursor specifically
- // ignores project-level .cursor/mcp.json — only ~/.cursor/mcp.json loads.
+ // Codex, Copilot, Cursor, VS Code, and Antigravity are user-scope only.
+ // Cursor specifically ignores project-level .cursor/mcp.json — only
+ // ~/.cursor/mcp.json loads. VS Code and Antigravity also support a
+ // workspace file (.vscode/mcp.json, etc.) but registering at user
+ // scope means a single setup run covers every project the user opens.
return []string{"user"}
}
}
diff --git a/cmd/clawdchan/colors.go b/cmd/clawdchan/colors.go
index 0591768..d1a4691 100644
--- a/cmd/clawdchan/colors.go
+++ b/cmd/clawdchan/colors.go
@@ -4,6 +4,7 @@ import (
"fmt"
"os"
"strconv"
+ "strings"
"github.com/mattn/go-isatty"
)
@@ -106,10 +107,27 @@ const (
// documented for the in-tree OpenClaw host. Change when we adopt
// a real brand color.
openclawRed = "#E34234"
+ // vscodeBlue is the published "VS Code Blue" used in the product
+ // icon. Source: https://code.visualstudio.com/brand
+ vscodeBlue = "#007ACC"
)
-// agentColor returns the brand hex for an agent key ("cc", "gemini",
-// "codex", "copilot", "openclaw"). Returns "" for unknown keys.
+// antigravityGradient is the per-character color sweep used for
+// Google Antigravity. Antigravity's brand identity is a gradient
+// (pink → purple → blue → cyan → mint), not a single hex; rendering
+// it as an interpolated sweep across the display name is the
+// closest the terminal can get to the actual product mark.
+var antigravityGradient = []string{
+ "#FF5A8A", // rose
+ "#A155F0", // purple
+ "#5B7FFF", // blue
+ "#00C8FF", // cyan
+ "#34F5C5", // mint
+}
+
+// agentColor returns the brand hex for an agent key. Returns "" for
+// unknown keys, or for keys whose brand identity is a gradient rather
+// than a single hex (Antigravity — see antigravityGradient).
func agentColor(key string) string {
switch key {
case "cc":
@@ -122,17 +140,23 @@ func agentColor(key string) string {
return copilotPurple
case "openclaw":
return openclawRed
+ case "vscode":
+ return vscodeBlue
}
return ""
}
// agentStyle wraps s in bold + the agent's brand color, using 24-bit
// ANSI truecolor. Falls back to plain bold when color is disabled or
-// the key is unknown.
+// the key is unknown. Antigravity is a gradient, not a single color,
+// so it routes through agentGradient.
func agentStyle(key, s string) string {
if !colorEnabled {
return s
}
+ if key == "antigravity" {
+ return agentGradient(s, antigravityGradient)
+ }
h := agentColor(key)
if h == "" {
return bold(s)
@@ -144,6 +168,51 @@ func agentStyle(key, s string) string {
return fmt.Sprintf("\x1b[1;38;2;%d;%d;%dm%s\x1b[0m", r, g, b, s)
}
+// agentGradient renders s with a per-character color sweep across the
+// supplied hex stops, linearly interpolated in RGB. Bold attribute is
+// applied across the whole run so the gradient still reads as a label.
+// Returns plain s when color is disabled, falls back to plain bold on
+// malformed stops or empty input.
+func agentGradient(s string, stops []string) string {
+ if !colorEnabled || len(stops) < 2 {
+ return bold(s)
+ }
+ type rgb struct{ r, g, b int }
+ pal := make([]rgb, 0, len(stops))
+ for _, h := range stops {
+ r, g, b, ok := parseHex(h)
+ if !ok {
+ return bold(s)
+ }
+ pal = append(pal, rgb{r, g, b})
+ }
+ runes := []rune(s)
+ if len(runes) == 0 {
+ return s
+ }
+ var out strings.Builder
+ out.WriteString("\x1b[1m")
+ for i, ch := range runes {
+ pos := 0.0
+ if len(runes) > 1 {
+ pos = float64(i) / float64(len(runes)-1)
+ }
+ seg := pos * float64(len(pal)-1)
+ a := int(seg)
+ b := a + 1
+ if b >= len(pal) {
+ b = len(pal) - 1
+ }
+ t := seg - float64(a)
+ r := pal[a].r + int(float64(pal[b].r-pal[a].r)*t)
+ g := pal[a].g + int(float64(pal[b].g-pal[a].g)*t)
+ bl := pal[a].b + int(float64(pal[b].b-pal[a].b)*t)
+ fmt.Fprintf(&out, "\x1b[38;2;%d;%d;%dm%c", r, g, bl, ch)
+ }
+ out.WriteString("\x1b[0m")
+ return out.String()
+}
+
// parseHex parses a "#RRGGBB" string into 0..255 components.
func parseHex(h string) (r, g, b int, ok bool) {
if len(h) != 7 || h[0] != '#' {
diff --git a/cmd/clawdchan/setup_agent_antigravity.go b/cmd/clawdchan/setup_agent_antigravity.go
new file mode 100644
index 0000000..f25bed5
--- /dev/null
+++ b/cmd/clawdchan/setup_agent_antigravity.go
@@ -0,0 +1,106 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+// antigravityAgent wires the Google Antigravity host. Antigravity is a
+// VS Code-derived editor in Google's Gemini ecosystem; its MCP config
+// lives under the parent ~/.gemini directory, not its own home, which
+// is why the path is ~/.gemini/antigravity/mcp_config.json on every
+// platform. The schema reuses the "mcpServers" convention shared with
+// Cursor, Gemini CLI, and GitHub Copilot CLI.
+//
+// User-scope only — Antigravity's documented entry point ("Manage MCP
+// Servers" → "View raw config") opens a single file, with no project
+// scope mentioned in the upstream docs.
+//
+// Reference: https://antigravity.google (Editor → MCP Integration).
+func antigravityAgent() *agentWiring {
+ return &agentWiring{
+ key: "antigravity",
+ flagName: "antigravity",
+ displayName: "Antigravity",
+ defaultOn: false,
+ scopeFlags: []string{"mcp"},
+ setup: setupAntigravity,
+ doctorReport: doctorAntigravity,
+ uninstallHints: uninstallHintsAntigravity,
+ }
+}
+
+// antigravityUserMCPPath returns the user-scope mcp_config.json path
+// for Antigravity. Layout matches the docs verbatim on every platform:
+// ~/.gemini/antigravity/mcp_config.json.
+func antigravityUserMCPPath() (string, error) {
+ home, err := os.UserHomeDir()
+ if err != nil {
+ return "", err
+ }
+ return filepath.Join(home, ".gemini", "antigravity", "mcp_config.json"), nil
+}
+
+func setupAntigravity(yes bool, scopes map[string]string) error {
+ scope := strings.ToLower(strings.TrimSpace(scopes["mcp"]))
+ if scope == "" && (yes || !stdinIsTTY()) {
+ scope = "user"
+ }
+ if scope == "" {
+ fmt.Println(" MCP scope " + dim("(~/.gemini/antigravity/mcp_config.json — user only)") + ":")
+ fmt.Printf(" %s user %s\n", cyan("[1]"), green("(recommended)"))
+ fmt.Printf(" %s skip\n", cyan("[2]"))
+ switch promptChoice(" Choice [1]: ", 1, 2) {
+ case 2:
+ scope = "skip"
+ default:
+ scope = "user"
+ }
+ }
+
+ switch scope {
+ case "user":
+ mcpBin, err := requireMCPBinary()
+ if err != nil {
+ return err
+ }
+ path, err := antigravityUserMCPPath()
+ if err != nil {
+ return err
+ }
+ _, err = mergeJSONMCPServer(path, map[string]any{
+ "command": mcpBin,
+ }, "(user)")
+ return err
+ case "skip":
+ fmt.Printf(" %s MCP %s %s\n", okTag(), dim("→"), dim("skipped"))
+ return nil
+ default:
+ return fmt.Errorf("unknown -antigravity-mcp-scope %q (use user|skip)", scope)
+ }
+}
+
+func doctorAntigravity() []string {
+ path, err := antigravityUserMCPPath()
+ if err != nil {
+ return nil
+ }
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return nil
+ }
+ if strings.Contains(string(data), `"clawdchan"`) {
+ return []string{"Antigravity: MCP registered (user scope, " + path + ")"}
+ }
+ return nil
+}
+
+func uninstallHintsAntigravity() []string {
+ return []string{
+ "If you wired Antigravity, remove the clawdchan entry from:",
+ " ~/.gemini/antigravity/mcp_config.json",
+ "Delete the \"clawdchan\" key under the \"mcpServers\" object.",
+ }
+}
diff --git a/cmd/clawdchan/setup_agent_vscode.go b/cmd/clawdchan/setup_agent_vscode.go
new file mode 100644
index 0000000..3d295ef
--- /dev/null
+++ b/cmd/clawdchan/setup_agent_vscode.go
@@ -0,0 +1,112 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+// vscodeAgent is the Visual Studio Code wiring. VS Code 1.102+ supports
+// MCP servers natively. User-scope config lives at
+// `/Code/User/mcp.json` — the same User profile dir that
+// holds settings.json/keybindings.json — and uses a top-level "servers"
+// key rather than the "mcpServers" convention shared by the other hosts.
+//
+// We register at user scope only. Workspace `.vscode/mcp.json` is also
+// supported by VS Code, but matching the Cursor/Codex/Copilot pattern
+// keeps the setup flow consistent and means a single registration works
+// across every project the user opens.
+//
+// Reference: https://code.visualstudio.com/docs/copilot/customization/mcp-servers
+func vscodeAgent() *agentWiring {
+ return &agentWiring{
+ key: "vscode",
+ flagName: "vscode",
+ displayName: "VS Code",
+ defaultOn: false,
+ scopeFlags: []string{"mcp"},
+ setup: setupVSCode,
+ doctorReport: doctorVSCode,
+ uninstallHints: uninstallHintsVSCode,
+ }
+}
+
+// vscodeUserMCPPath returns the user-scope mcp.json path for VS Code
+// Stable. The User folder under the OS-specific config dir is where VS
+// Code stores per-user settings.json and keybindings.json; mcp.json
+// lives alongside them.
+func vscodeUserMCPPath() (string, error) {
+ cfg, err := os.UserConfigDir()
+ if err != nil {
+ return "", err
+ }
+ return filepath.Join(cfg, "Code", "User", "mcp.json"), nil
+}
+
+func setupVSCode(yes bool, scopes map[string]string) error {
+ scope := strings.ToLower(strings.TrimSpace(scopes["mcp"]))
+ if scope == "" && (yes || !stdinIsTTY()) {
+ scope = "user"
+ }
+ if scope == "" {
+ fmt.Println(" MCP scope " + dim("(VS Code user profile — Code/User/mcp.json)") + ":")
+ fmt.Printf(" %s user %s\n", cyan("[1]"), green("(recommended)"))
+ fmt.Printf(" %s skip\n", cyan("[2]"))
+ switch promptChoice(" Choice [1]: ", 1, 2) {
+ case 2:
+ scope = "skip"
+ default:
+ scope = "user"
+ }
+ }
+
+ switch scope {
+ case "user":
+ mcpBin, err := requireMCPBinary()
+ if err != nil {
+ return err
+ }
+ path, err := vscodeUserMCPPath()
+ if err != nil {
+ return err
+ }
+ if _, err := mergeJSONMCPServerAt(path, "servers", map[string]any{
+ "command": mcpBin,
+ }, "(user)"); err != nil {
+ return err
+ }
+ fmt.Printf(" %s Reload VS Code %s to pick up the new server.\n",
+ warnTag(), dim("(Command Palette → \"Developer: Reload Window\")"))
+ return nil
+ case "skip":
+ fmt.Printf(" %s MCP %s %s\n", okTag(), dim("→"), dim("skipped"))
+ return nil
+ default:
+ return fmt.Errorf("unknown -vscode-mcp-scope %q (use user|skip)", scope)
+ }
+}
+
+func doctorVSCode() []string {
+ path, err := vscodeUserMCPPath()
+ if err != nil {
+ return nil
+ }
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return nil
+ }
+ if strings.Contains(string(data), `"clawdchan"`) {
+ return []string{"VS Code: MCP registered (user scope, " + path + ")"}
+ }
+ return nil
+}
+
+func uninstallHintsVSCode() []string {
+ return []string{
+ "If you wired VS Code, remove the clawdchan entry from the user mcp.json:",
+ " Command Palette → \"MCP: Open User Configuration\"",
+ " (or edit the file at /Code/User/mcp.json)",
+ "Delete the \"clawdchan\" key under the \"servers\" object.",
+ }
+}
diff --git a/cmd/clawdchan/setup_agents_test.go b/cmd/clawdchan/setup_agents_test.go
index e323f77..6dee994 100644
--- a/cmd/clawdchan/setup_agents_test.go
+++ b/cmd/clawdchan/setup_agents_test.go
@@ -201,6 +201,98 @@ key = "y"
}
}
+// TestMergeJSONMCPServerAt_EmptyFile guards against a real install
+// case: Antigravity (and likely others) create a zero-byte stub
+// mcp_config.json on first run. json.Unmarshal of empty bytes errors
+// with "unexpected end of JSON input"; the helper has to treat
+// zero-length files as if they were "{}".
+func TestMergeJSONMCPServerAt_EmptyFile(t *testing.T) {
+ dir := t.TempDir()
+ path := filepath.Join(dir, "mcp_config.json")
+ if err := os.WriteFile(path, []byte{}, 0o644); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := mergeJSONMCPServer(path, map[string]any{
+ "command": "/bin/clawdchan-mcp",
+ }, "(user)"); err != nil {
+ t.Fatalf("mergeJSONMCPServer on empty file: %v", err)
+ }
+ data, _ := os.ReadFile(path)
+ var obj map[string]any
+ if err := json.Unmarshal(data, &obj); err != nil {
+ t.Fatalf("result not valid JSON: %v\n%s", err, string(data))
+ }
+ servers, _ := obj["mcpServers"].(map[string]any)
+ if _, ok := servers["clawdchan"]; !ok {
+ t.Errorf("clawdchan entry missing: %s", string(data))
+ }
+}
+
+// TestMergeJSONMCPServerAt_VSCodeKey verifies that VS Code's "servers"
+// top-level key is honored end-to-end: the entry lands under "servers",
+// the wrong key "mcpServers" is NOT created as a sibling, and any
+// pre-existing "servers" siblings survive.
+func TestMergeJSONMCPServerAt_VSCodeKey(t *testing.T) {
+ dir := t.TempDir()
+ path := filepath.Join(dir, "mcp.json")
+ existing := `{"servers":{"playwright":{"command":"npx","args":["-y","@microsoft/mcp-server-playwright"]}}}`
+ if err := os.WriteFile(path, []byte(existing), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := mergeJSONMCPServerAt(path, "servers", map[string]any{
+ "command": "/bin/clawdchan-mcp",
+ }, "(user)"); err != nil {
+ t.Fatalf("mergeJSONMCPServerAt: %v", err)
+ }
+ data, _ := os.ReadFile(path)
+ var obj map[string]any
+ if err := json.Unmarshal(data, &obj); err != nil {
+ t.Fatalf("unmarshal: %v", err)
+ }
+ if _, wrong := obj["mcpServers"]; wrong {
+ t.Errorf("must not create mcpServers when topKey is servers: %s", string(data))
+ }
+ servers, ok := obj["servers"].(map[string]any)
+ if !ok {
+ t.Fatalf("servers missing: %s", string(data))
+ }
+ if _, ok := servers["playwright"]; !ok {
+ t.Errorf("sibling 'playwright' dropped")
+ }
+ entry, ok := servers["clawdchan"].(map[string]any)
+ if !ok {
+ t.Fatalf("no clawdchan entry: %s", string(data))
+ }
+ if entry["command"] != "/bin/clawdchan-mcp" {
+ t.Errorf("command=%v", entry["command"])
+ }
+}
+
+// TestVSCodeUserMCPPath_Layout pins the mcp.json path under the OS
+// config dir's Code/User folder — the location VS Code uses for
+// user-profile settings.
+func TestVSCodeUserMCPPath_Layout(t *testing.T) {
+ got, err := vscodeUserMCPPath()
+ if err != nil {
+ t.Fatalf("vscodeUserMCPPath: %v", err)
+ }
+ if !strings.HasSuffix(filepath.ToSlash(got), "Code/User/mcp.json") {
+ t.Errorf("path = %q, want suffix Code/User/mcp.json", got)
+ }
+}
+
+// TestAntigravityUserMCPPath_Layout pins the gemini-rooted layout
+// documented by Antigravity (.gemini/antigravity/mcp_config.json).
+func TestAntigravityUserMCPPath_Layout(t *testing.T) {
+ got, err := antigravityUserMCPPath()
+ if err != nil {
+ t.Fatalf("antigravityUserMCPPath: %v", err)
+ }
+ if !strings.HasSuffix(filepath.ToSlash(got), ".gemini/antigravity/mcp_config.json") {
+ t.Errorf("path = %q, want suffix .gemini/antigravity/mcp_config.json", got)
+ }
+}
+
// TestAgentRegistryUnique guards against copy-paste mistakes where two
// agents share a flag name — which would silently break CLI parsing.
func TestAgentRegistryUnique(t *testing.T) {
diff --git a/cmd/clawdchan/setup_helpers.go b/cmd/clawdchan/setup_helpers.go
index 7395674..e4e2715 100644
--- a/cmd/clawdchan/setup_helpers.go
+++ b/cmd/clawdchan/setup_helpers.go
@@ -28,17 +28,26 @@ func requireMCPBinary() (string, error) {
// sibling field. Creates the file (and its parent dir) if missing.
//
// Used by hosts whose config format is a plain JSON object keyed by
-// mcpServers (Gemini CLI, GitHub Copilot CLI, Cursor). Returns whether
-// the clawdchan entry was newly added (true) or updated in place
-// (false). The logSuffix is the parenthetical shown at the end of the
-// success line, e.g. "(trust=true)" for Gemini or `(tools=["*"])` for
-// Copilot.
+// mcpServers (Gemini CLI, GitHub Copilot CLI, Cursor, Antigravity).
+// Returns whether the clawdchan entry was newly added (true) or
+// updated in place (false). The logSuffix is the parenthetical shown
+// at the end of the success line, e.g. "(trust=true)" for Gemini or
+// `(tools=["*"])` for Copilot.
func mergeJSONMCPServer(path string, entry map[string]any, logSuffix string) (bool, error) {
+ return mergeJSONMCPServerAt(path, "mcpServers", entry, logSuffix)
+}
+
+// mergeJSONMCPServerAt is the general form. VS Code is the one host
+// that nests its servers under the top-level "servers" key (matching
+// the MCP spec's native naming) instead of "mcpServers".
+func mergeJSONMCPServerAt(path, topKey string, entry map[string]any, logSuffix string) (bool, error) {
data, err := os.ReadFile(path)
- if err != nil {
- if !os.IsNotExist(err) {
- return false, err
- }
+ if err != nil && !os.IsNotExist(err) {
+ return false, err
+ }
+ // Missing file and empty-stub file (some hosts — e.g. Antigravity —
+ // create a zero-byte placeholder on first run) both decode as {}.
+ if len(data) == 0 {
data = []byte("{}")
}
var obj map[string]any
@@ -48,13 +57,13 @@ func mergeJSONMCPServer(path string, entry map[string]any, logSuffix string) (bo
if obj == nil {
obj = map[string]any{}
}
- servers, _ := obj["mcpServers"].(map[string]any)
+ servers, _ := obj[topKey].(map[string]any)
if servers == nil {
servers = map[string]any{}
}
_, existed := servers["clawdchan"]
servers["clawdchan"] = entry
- obj["mcpServers"] = servers
+ obj[topKey] = servers
out, err := json.MarshalIndent(obj, "", " ")
if err != nil {
diff --git a/npm/README.md b/npm/README.md
index ef8af7a..cb4f3fc 100644
--- a/npm/README.md
+++ b/npm/README.md
@@ -2,7 +2,8 @@
Let your Claude talk to mine — private channel between two (human, agent)
pairs. Today the setup flow wires Claude Code, Gemini CLI, Codex CLI,
-GitHub Copilot CLI, Cursor, and OpenClaw.
+GitHub Copilot CLI, Cursor, VS Code, and Antigravity, with an optional
+OpenClaw gateway step.
```sh
npm i -g clawdchan
diff --git a/site/antigravity-mark.png b/site/antigravity-mark.png
new file mode 100644
index 0000000..c677e4f
Binary files /dev/null and b/site/antigravity-mark.png differ
diff --git a/site/index.html b/site/index.html
index a0e3bb7..c31f114 100644
--- a/site/index.html
+++ b/site/index.html
@@ -53,6 +53,8 @@ Let your Claude talk to mine.
+
+