Skip to content
Closed

[...] #343

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
167 changes: 164 additions & 3 deletions cmd/picoclaw/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"bufio"
"context"
"embed"
"encoding/json"
"fmt"
"io"
"io/fs"
Expand Down Expand Up @@ -373,6 +374,7 @@ func migrateHelp() {
func agentCmd() {
message := ""
sessionKey := "cli:default"
modelOverride := ""

args := os.Args[2:]
for i := 0; i < len(args); i++ {
Expand All @@ -390,6 +392,11 @@ func agentCmd() {
sessionKey = args[i+1]
i++
}
case "--model", "-model":
if i+1 < len(args) {
modelOverride = args[i+1]
i++
}
}
}

Expand All @@ -399,6 +406,10 @@ func agentCmd() {
os.Exit(1)
}

if modelOverride != "" {
cfg.Agents.Defaults.Model = modelOverride
}

provider, err := providers.CreateProvider(cfg)
if err != nil {
fmt.Printf("Error creating provider: %v\n", err)
Expand Down Expand Up @@ -777,6 +788,8 @@ func authCmd() {
authLogoutCmd()
case "status":
authStatusCmd()
case "models":
authModelsCmd()
default:
fmt.Printf("Unknown auth command: %s\n", os.Args[2])
authHelp()
Expand All @@ -788,15 +801,18 @@ func authHelp() {
fmt.Println(" login Login via OAuth or paste token")
fmt.Println(" logout Remove stored credentials")
fmt.Println(" status Show current auth status")
fmt.Println(" models List available Antigravity models")
fmt.Println()
fmt.Println("Login options:")
fmt.Println(" --provider <name> Provider to login with (openai, anthropic)")
fmt.Println(" --provider <name> Provider to login with (openai, anthropic, google-antigravity)")
fmt.Println(" --device-code Use device code flow (for headless environments)")
fmt.Println()
fmt.Println("Examples:")
fmt.Println(" picoclaw auth login --provider openai")
fmt.Println(" picoclaw auth login --provider openai --device-code")
fmt.Println(" picoclaw auth login --provider anthropic")
fmt.Println(" picoclaw auth login --provider google-antigravity")
fmt.Println(" picoclaw auth models")
fmt.Println(" picoclaw auth logout --provider openai")
fmt.Println(" picoclaw auth status")
}
Expand All @@ -820,7 +836,7 @@ func authLoginCmd() {

if provider == "" {
fmt.Println("Error: --provider is required")
fmt.Println("Supported providers: openai, anthropic")
fmt.Println("Supported providers: openai, anthropic, google-antigravity")
return
}

Expand All @@ -829,9 +845,11 @@ func authLoginCmd() {
authLoginOpenAI(useDeviceCode)
case "anthropic":
authLoginPasteToken(provider)
case "google-antigravity", "antigravity":
authLoginGoogleAntigravity()
default:
fmt.Printf("Unsupported provider: %s\n", provider)
fmt.Println("Supported providers: openai, anthropic")
fmt.Println("Supported providers: openai, anthropic, google-antigravity")
}
}

Expand Down Expand Up @@ -871,6 +889,88 @@ func authLoginOpenAI(useDeviceCode bool) {
}
}

func authLoginGoogleAntigravity() {
cfg := auth.GoogleAntigravityOAuthConfig()

cred, err := auth.LoginBrowser(cfg)
if err != nil {
fmt.Printf("Login failed: %v\n", err)
os.Exit(1)
}

cred.Provider = "google-antigravity"

// Fetch user email from Google userinfo
email, err := fetchGoogleUserEmail(cred.AccessToken)
if err != nil {
fmt.Printf("Warning: could not fetch email: %v\n", err)
} else {
cred.Email = email
fmt.Printf("Email: %s\n", email)
}

// Fetch Cloud Code Assist project ID
projectID, err := providers.FetchAntigravityProjectID(cred.AccessToken)
if err != nil {
fmt.Printf("Warning: could not fetch project ID: %v\n", err)
fmt.Println("You may need Google Cloud Code Assist enabled on your account.")
} else {
cred.ProjectID = projectID
fmt.Printf("Project: %s\n", projectID)
}

if err := auth.SetCredential("google-antigravity", cred); err != nil {
fmt.Printf("Failed to save credentials: %v\n", err)
os.Exit(1)
}

appCfg, err := loadConfig()
if err == nil {
appCfg.Providers.Antigravity.AuthMethod = "oauth"
if appCfg.Agents.Defaults.Provider == "" {
appCfg.Agents.Defaults.Provider = "antigravity"
}
if appCfg.Agents.Defaults.Provider == "antigravity" || appCfg.Agents.Defaults.Provider == "google-antigravity" {
appCfg.Agents.Defaults.Model = "gemini-3-flash"
}
if err := config.SaveConfig(getConfigPath(), appCfg); err != nil {
fmt.Printf("Warning: could not update config: %v\n", err)
}
}

fmt.Println("\n✓ Google Antigravity login successful!")
fmt.Println("Config updated: provider=antigravity, model=gemini-3-flash")
fmt.Println("Try it: picoclaw agent -m \"Hello world\"")
}

func fetchGoogleUserEmail(accessToken string) (string, error) {
req, err := http.NewRequest("GET", "https://www.googleapis.com/oauth2/v2/userinfo", nil)
if err != nil {
return "", err
}
req.Header.Set("Authorization", "Bearer "+accessToken)

client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()

body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("userinfo request failed: %s", string(body))
}

var userInfo struct {
Email string `json:"email"`
}
if err := json.Unmarshal(body, &userInfo); err != nil {
return "", err
}
return userInfo.Email, nil
}

func authLoginPasteToken(provider string) {
cred, err := auth.LoginPasteToken(provider, os.Stdin)
if err != nil {
Expand Down Expand Up @@ -926,6 +1026,8 @@ func authLogoutCmd() {
appCfg.Providers.OpenAI.AuthMethod = ""
case "anthropic":
appCfg.Providers.Anthropic.AuthMethod = ""
case "google-antigravity", "antigravity":
appCfg.Providers.Antigravity.AuthMethod = ""
}
config.SaveConfig(getConfigPath(), appCfg)
}
Expand All @@ -941,6 +1043,7 @@ func authLogoutCmd() {
if err == nil {
appCfg.Providers.OpenAI.AuthMethod = ""
appCfg.Providers.Anthropic.AuthMethod = ""
appCfg.Providers.Antigravity.AuthMethod = ""
config.SaveConfig(getConfigPath(), appCfg)
}

Expand Down Expand Up @@ -977,12 +1080,70 @@ func authStatusCmd() {
if cred.AccountID != "" {
fmt.Printf(" Account: %s\n", cred.AccountID)
}
if cred.Email != "" {
fmt.Printf(" Email: %s\n", cred.Email)
}
if cred.ProjectID != "" {
fmt.Printf(" Project: %s\n", cred.ProjectID)
}
if !cred.ExpiresAt.IsZero() {
fmt.Printf(" Expires: %s\n", cred.ExpiresAt.Format("2006-01-02 15:04"))
}
}
}

func authModelsCmd() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generic Name. This looks to only handle Antigravity auth only. Move auth to provider.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right — authModelsCmd is misleading since it only handles the Antigravity OAuth flow. I will rename it to something specific like antigravityAuthCmd and move the auth logic into the Antigravity provider package so it follows the same pattern as the other providers. The CLI entry point will just call the provider method.

cred, err := auth.GetCredential("google-antigravity")
if err != nil || cred == nil {
fmt.Println("Not logged in to Google Antigravity.")
fmt.Println("Run: picoclaw auth login --provider google-antigravity")
return
}

// Refresh token if needed
if cred.NeedsRefresh() && cred.RefreshToken != "" {
oauthCfg := auth.GoogleAntigravityOAuthConfig()
refreshed, refreshErr := auth.RefreshAccessToken(cred, oauthCfg)
if refreshErr == nil {
cred = refreshed
_ = auth.SetCredential("google-antigravity", cred)
}
}

projectID := cred.ProjectID
if projectID == "" {
fmt.Println("No project ID stored. Try logging in again.")
return
}

fmt.Printf("Fetching models for project: %s\n\n", projectID)

models, err := providers.FetchAntigravityModels(cred.AccessToken, projectID)
if err != nil {
fmt.Printf("Error fetching models: %v\n", err)
return
}

if len(models) == 0 {
fmt.Println("No models available.")
return
}

fmt.Println("Available Antigravity Models:")
fmt.Println("-----------------------------")
for _, m := range models {
status := "✓"
if m.IsExhausted {
status = "✗ (quota exhausted)"
}
name := m.ID
if m.DisplayName != "" {
name = fmt.Sprintf("%s (%s)", m.ID, m.DisplayName)
}
fmt.Printf(" %s %s\n", status, name)
}
}

func getConfigPath() string {
home, _ := os.UserHomeDir()
return filepath.Join(home, ".picoclaw", "config.json")
Expand Down
Loading