Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
<img src="https://img.shields.io/badge/Codex%20CLI-000000?logo=openai&logoColor=white" alt="Codex CLI">
<img src="https://img.shields.io/badge/GitHub%20Copilot%20CLI-000000?logo=githubcopilot&logoColor=white" alt="GitHub Copilot CLI">
<img src="https://img.shields.io/badge/Cursor-000000?logo=cursor&logoColor=white" alt="Cursor">
<img src="https://img.shields.io/badge/VS%20Code-007ACC?logo=visualstudiocode&logoColor=white" alt="VS Code">
<img src="site/antigravity-mark.png" alt="Antigravity" height="20">
<img src="https://img.shields.io/badge/OpenClaw-2d2b55?logo=data%3Aimage%2Fpng%3Bbase64%2CiVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAHiUlEQVR42s1XXWhcxxX%2Bzsy9u6v17sqW%2FCe5iezUhVZyY4IKaYMdKFaILYc4rb2KUxKcNKTkwUahxBhDYbWNSTH0xSUpaZtfSiFoTaEiTh5SMCUUUyPhRrFlEqU4dtvY2PIq1v7de2fmnD5oV5JlyVb71AvDHWaG833nO2fOPZfwXzy5XE4NjI0Rrl4lrF4thUIBAJDNZtFYG%2BjslHw%2Bz0u1SQstCkAEiBw92g6Ru6H1FE6f%2FpwKhWgpRiWbjWHLlo0mDJv9ePyf1N%2F%2Fr4bN%2BWe9RVhNH1y%2B%2FNqVzz67sfaee1qwc%2Bc3ZfPma%2BGnn94Xlsvfg7Ubas6tFBFoogmn9QXd1HRqVXf333HXXSsxOTl5%2FdKli2vvv9%2FcZHMpCtyixOHDq6ILFwZsubxXjGnxRWCY4UTgeFbtQATi%2B5MqmXwXbW0Dba%2B9dnUxz%2B9IQHI5Rfk815544hmUSr%2F0rG2ZiiJEIiwAO2YSETgRsAhERJyIIhGV1Bqh1kU0Nx%2F8%2BtDQmwIoAhbMC7VIDDXl81zZvfvlRKn0ZhgELUVjbCAiTqBExBNAM6BldngMqBCQSWNsUKu16GLxjfM9PS8TwJLN6iURkGxWU6HgKo8%2FPpAMw8PXazUbigiJeM0xn5p8DcsMmfZ6ZlgRJBQhE%2FMJRF4ISDEIbLxSOfzJQw8NUKHgFiJxE4HBOniwb9%2FOeKWSmwwCy4DWImQAHB%2F%2FAiOXvkRCa9i69DwDrnBmsoSh8%2F%2BAjQw0gRygr4ehVaVSbvTRR3dSoeAG55GguQkHADh6NFX66KNRFYYdVWZxIqrJ0zgw%2FAl%2B%2F%2FAWNK1dhT%2Bc%2BAu%2Bu241SpEBi6BJK5y6NokfV6qo%2FfQZ7H7rj%2Fi5VihrBRFhn5lcInFxw2OP3bvy0KHy3FuhAEBEaHz79hgAlM%2BceTbNvL7K7GQ6qVB1jJPFr4A1raityOD0jSnEiGDrSegTYbhUQQ0A1q%2FDX4MQVWtRt61qIi5uzPqLH374LACMb98eExECAGpk%2B7QehBu9vWd1FHVWnWMnoq0IkkrhrSvX8IuxcXTEYvj1fZ1Y7XsInAOLwCPgcmTxwvhFXJoq4kDHeuxZ3YJSnQSLsC9CJhY7v3V4uAsiDdUVAUDl4MF2NzHxDRuGd2Ni4h3nHLk5MXY8nWCXjcEypeCLoOpcwzisCHwAATOmRNBKhEqdnIjAMsMyw%2Fc8uaujY18qk7lUy2TGO1599UsKn3rqR7ZY%2FFUURa1lY1AOQzgRZOJxaKKZe26ZoYlgmGFFgDkEG%2FuNpAodA5hdj2mNdek0FBE8pWCZUWO%2BblpbX6Dijh0lrtVSRWNcYK2KnKNyFGFFIoGVySQi5yB1T%2BcUnZvAZ5Saty8AQmvRnk6jLZXCjTBEzRgJrGVxTle1LntBEKTKxkjknI6cQ1gfigjTfmAGQG4Dvti%2BAChHEUrT4Aido5BZV40RG0UpL1Dq32zt2pq1iJipEkVKEyEdj8PU4zjf61tIYLrOLkSOAHwVBEhoDQFQM4ZD54SthfX9K4qbmo75RNoao4MoUpFz%2BFomA6p73vDKzZvPEAEg9croGorNGQLAieBKpYKKMbDOqcgYrQGtk8lj%2BtiRI3%2F7fGQkzcxtTuvJDZnMioTvwzDfZKwxd3VPZwCZp880SMwLh6tfOcOMyDmw718gpW7UEom3d5w%2BnZ%2BthO%2B%2FH48%2B%2BKDz%2Brlzw9UwJCEiZp6VeP587nuxvKjvu%2Bm5aK1l%2BcaN33ng%2BefHqLc3nKmEg4Cm3t7wT1u3jkZEowmlIMzuFsD%2FAbyuhvMBsNajDzz55Cj19oaDgJ4h0Ae44e5uv6%2Bvz3mZzOu%2BUjQTgnomzze6VHCuNy2%2BUhTPZN6gvj73m%2B5uv286mrNfw%2B6RESsA%2Bdu2vTNFdCFOpBwzS73zkZs9WrQeyBxy9X32AFXzvC829%2FS8LQD9ZGTE3vI5JkAK2axas39%2F2Vu%2B%2FMWY5xGLsGsUoTlG3UL3fYHbUleDfc%2BjRGvri2vy%2BXIhm1VzWzRasCE5ftyd7%2Bn5bbJSeW4iiowA%2FmKV73aFyTGbtFJ%2BkE6%2F%2FsjIyHODe%2FbovkLB3bYnFIAKgMqePavPHzhwIlar9UwYYyDisQjxbQAbyekAYedsSinfJJN%2F3vH0048U%2BvttFuD5DapaqCU%2FBwht2hS17t79gyCReG%2BF5%2FnMTCziZNZ7YRFmEXYiMueqOnGO0p7nu1TqxIa9e39I%2Ff3hOUAW6o4XbErzAOdyObVm%2F%2F5y18mTu6Lm5pdivh8kiLStgykR8gDlAUrVlXEiEgd0zPcDNDe%2F9PDw8K5vHTpUyuVyKr9IV7yk%2FwIAOJvNdlavXPlZODW11zgHxGKXAUwys7IiK2wQrCEieOn0u01tbUceHBoam29jyW35vD8kGuzsjG0qFMZMe%2FsrpJSLibj4smUnvj8ycu%2B2M2e%2B7SeT7%2FkiDkTOa29%2F5cGhobHBzs4Y7gB%2BRwIzQnR1OcnllK5WV6W09tKe51lrVxIRE5FzzrWmtfZSWnscBKskl1Po6mp8m277eEv52cwWCkyAnNq16%2BPJKDoI54iYzzZCqIh%2BV7T2FDOLD3xM%2BTzLHcL7f%2FP8BxADSQ8npiw1AAAAAElFTkSuQmCC" alt="OpenClaw">
</p>

Expand Down
13 changes: 9 additions & 4 deletions cmd/clawdchan/agents.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ func allAgents() []*agentWiring {
codexAgent(),
copilotAgent(),
cursorAgent(),
vscodeAgent(),
antigravityAgent(),
}
}

Expand Down Expand Up @@ -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.
Comment on lines +113 to +115
return []string{"user"}
}
}
Expand Down
75 changes: 72 additions & 3 deletions cmd/clawdchan/colors.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"strconv"
"strings"

"github.com/mattn/go-isatty"
)
Expand Down Expand Up @@ -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":
Expand All @@ -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)
Expand All @@ -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] != '#' {
Expand Down
106 changes: 106 additions & 0 deletions cmd/clawdchan/setup_agent_antigravity.go
Original file line number Diff line number Diff line change
@@ -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.",
}
}
112 changes: 112 additions & 0 deletions cmd/clawdchan/setup_agent_vscode.go
Original file line number Diff line number Diff line change
@@ -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
// `<UserConfigDir>/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 <UserConfigDir>/Code/User/mcp.json)",
"Delete the \"clawdchan\" key under the \"servers\" object.",
}
}
Loading
Loading