Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
45c01f4
chore(deps): bump golang.org/x/oauth2 from 0.35.0 to 0.36.0 (#1596)
dependabot[bot] Mar 16, 2026
dd93630
chore(deps): bump github.com/mymmrac/telego from 1.6.0 to 1.7.0 (#1598)
dependabot[bot] Mar 16, 2026
e9d240d
chore(deps): bump github.com/caarlos0/env/v11 from 11.3.1 to 11.4.0 (…
dependabot[bot] Mar 16, 2026
2f40a8c
chore(deps): bump github.com/anthropics/anthropic-sdk-go (#1601)
dependabot[bot] Mar 16, 2026
43eb6fe
chore(deps): bump github.com/github/copilot-sdk/go from 0.1.23 to 0.1…
dependabot[bot] Mar 16, 2026
b8dfd0b
chore(deps): bump jotai from 2.18.0 to 2.18.1 in /web/frontend (#1605)
dependabot[bot] Mar 16, 2026
a93bd01
chore(deps-dev): bump @vitejs/plugin-react in /web/frontend (#1606)
dependabot[bot] Mar 16, 2026
3bf8a27
chore(deps): bump react-i18next from 16.5.4 to 16.5.8 in /web/fronten…
dependabot[bot] Mar 16, 2026
99304d1
chore(deps): bump dayjs from 1.11.19 to 1.11.20 in /web/frontend (#1608)
dependabot[bot] Mar 16, 2026
4178b2c
chore(deps): bump @tanstack/react-router in /web/frontend (#1609)
dependabot[bot] Mar 16, 2026
c806598
chore(web): upgrade eslint deps to resolve flatted vulnerability (#1629)
wj-xiao Mar 16, 2026
2f10b47
feat(credential): part1 add AES-GCM encryption, SecureStore, and onbo…
sky5454 Mar 16, 2026
4d4243b
chore(deps): bump docker/setup-buildx-action from 3 to 4 (#1595)
dependabot[bot] Mar 16, 2026
44ac304
chore(deps): bump actions/setup-node from 4 to 6 (#1597)
dependabot[bot] Mar 16, 2026
f247c3b
chore(deps): bump actions/setup-go from 5 to 6 (#1600)
dependabot[bot] Mar 16, 2026
b7b8d1e
chore(deps): bump docker/build-push-action from 6 to 7 (#1602)
dependabot[bot] Mar 16, 2026
0c94e6f
chore(deps): bump docker/login-action from 3 to 4 (#1604)
dependabot[bot] Mar 16, 2026
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
8 changes: 4 additions & 4 deletions .github/workflows/docker-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,19 @@ jobs:

# ── Docker Buildx ─────────────────────────
- name: πŸ”§ Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4

# ── Login to GHCR ─────────────────────────
- name: πŸ”‘ Login to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ${{ env.GHCR_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

# ── Login to Docker Hub ────────────────────
- name: πŸ”‘ Login to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ${{ env.DOCKERHUB_REGISTRY }}
username: ${{ secrets.DOCKERHUB_USERNAME }}
Expand All @@ -62,7 +62,7 @@ jobs:

# ── Build & Push ──────────────────────────
- name: πŸš€ Build and push Docker image
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
push: true
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ jobs:
go-version-file: go.mod

- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: 22

Expand All @@ -59,17 +59,17 @@ jobs:
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4

- name: Login to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Login to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: docker.io
username: ${{ secrets.DOCKERHUB_USERNAME }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
persist-credentials: false

- name: Setup Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version-file: go.mod

Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ jobs:
go-version-file: go.mod

- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: 22

Expand All @@ -77,17 +77,17 @@ jobs:
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4

- name: Login to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Login to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: docker.io
username: ${{ secrets.DOCKERHUB_USERNAME }}
Expand Down
7 changes: 6 additions & 1 deletion cmd/picoclaw/internal/onboard/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,19 @@ import (
var embeddedFiles embed.FS

func NewOnboardCommand() *cobra.Command {
var encrypt bool

cmd := &cobra.Command{
Use: "onboard",
Aliases: []string{"o"},
Short: "Initialize picoclaw configuration and workspace",
Run: func(cmd *cobra.Command, args []string) {
onboard()
onboard(encrypt)
},
}

cmd.Flags().BoolVar(&encrypt, "enc", false,
"Enable credential encryption (generates SSH key and prompts for passphrase)")

return cmd
}
5 changes: 4 additions & 1 deletion cmd/picoclaw/internal/onboard/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ func TestNewOnboardCommand(t *testing.T) {
assert.Nil(t, cmd.PersistentPreRun)
assert.Nil(t, cmd.PersistentPostRun)

assert.False(t, cmd.HasFlags())
assert.True(t, cmd.HasFlags())
encFlag := cmd.Flags().Lookup("enc")
require.NotNil(t, encFlag, "expected --enc flag to be registered")
assert.Equal(t, "false", encFlag.DefValue, "--enc should default to false")
assert.False(t, cmd.HasSubCommands())
}
133 changes: 121 additions & 12 deletions cmd/picoclaw/internal/onboard/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,71 @@ import (
"os"
"path/filepath"

"golang.org/x/term"

"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
"github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/credential"
)

func onboard() {
func onboard(encrypt bool) {
configPath := internal.GetConfigPath()

configExists := false
if _, err := os.Stat(configPath); err == nil {
fmt.Printf("Config already exists at %s\n", configPath)
fmt.Print("Overwrite? (y/n): ")
var response string
fmt.Scanln(&response)
if response != "y" {
fmt.Println("Aborted.")
return
configExists = true
if encrypt {
// Only ask for confirmation when *both* config and SSH key already exist,
// indicating a full re-onboard that would reset the config to defaults.
sshKeyPath, _ := credential.DefaultSSHKeyPath()
if _, err := os.Stat(sshKeyPath); err == nil {
// Both exist β€” confirm a full reset.
fmt.Printf("Config already exists at %s\n", configPath)
fmt.Print("Overwrite config with defaults? (y/n): ")
var response string
fmt.Scanln(&response)
if response != "y" {
fmt.Println("Aborted.")
return
}
configExists = false // user agreed to reset; treat as fresh
}
// Config exists but SSH key is missing β€” keep existing config, only add SSH key.
}
}

cfg := config.DefaultConfig()
var err error
if encrypt {
fmt.Println("\nSet up credential encryption")
fmt.Println("-----------------------------")
passphrase, pErr := promptPassphrase()
if pErr != nil {
fmt.Printf("Error: %v\n", pErr)
os.Exit(1)
}
// Expose the passphrase to credential.PassphraseProvider (which calls
// os.Getenv by default) so that SaveConfig can encrypt api_keys.
// This process is a one-shot CLI tool; the env var is never exposed outside
// the current process and disappears when it exits.
os.Setenv(credential.PassphraseEnvVar, passphrase)

if err = setupSSHKey(); err != nil {
fmt.Printf("Error generating SSH key: %v\n", err)
os.Exit(1)
}
}

var cfg *config.Config
if configExists {
// Preserve the existing config; SaveConfig will re-encrypt api_keys with the new passphrase.
cfg, err = config.LoadConfig(configPath)
if err != nil {
fmt.Printf("Error loading existing config: %v\n", err)
os.Exit(1)
}
} else {
cfg = config.DefaultConfig()
}
if err := config.SaveConfig(configPath, cfg); err != nil {
fmt.Printf("Error saving config: %v\n", err)
os.Exit(1)
Expand All @@ -33,17 +79,80 @@ func onboard() {
workspace := cfg.WorkspacePath()
createWorkspaceTemplates(workspace)

fmt.Printf("%s picoclaw is ready!\n", internal.Logo)
fmt.Printf("\n%s picoclaw is ready!\n", internal.Logo)
fmt.Println("\nNext steps:")
fmt.Println(" 1. Add your API key to", configPath)
if encrypt {
fmt.Println(" 1. Set your encryption passphrase before starting picoclaw:")
fmt.Println(" export PICOCLAW_KEY_PASSPHRASE=<your-passphrase> # Linux/macOS")
fmt.Println(" set PICOCLAW_KEY_PASSPHRASE=<your-passphrase> # Windows cmd")
fmt.Println("")
fmt.Println(" 2. Add your API key to", configPath)
} else {
fmt.Println(" 1. Add your API key to", configPath)
}
fmt.Println("")
fmt.Println(" Recommended:")
fmt.Println(" - OpenRouter: https://openrouter.ai/keys (access 100+ models)")
fmt.Println(" - Ollama: https://ollama.com (local, free)")
fmt.Println("")
fmt.Println(" See README.md for 17+ supported providers.")
fmt.Println("")
fmt.Println(" 2. Chat: picoclaw agent -m \"Hello!\"")
fmt.Println(" 3. Chat: picoclaw agent -m \"Hello!\"")
}

// promptPassphrase reads the encryption passphrase twice from the terminal
// (with echo disabled) and returns it. Returns an error if the passphrase is
// empty or if the two inputs do not match.
func promptPassphrase() (string, error) {
fmt.Print("Enter passphrase for credential encryption: ")
p1, err := term.ReadPassword(int(os.Stdin.Fd()))
fmt.Println()
if err != nil {
return "", fmt.Errorf("reading passphrase: %w", err)
}
if len(p1) == 0 {
return "", fmt.Errorf("passphrase must not be empty")
}

fmt.Print("Confirm passphrase: ")
p2, err := term.ReadPassword(int(os.Stdin.Fd()))
fmt.Println()
if err != nil {
return "", fmt.Errorf("reading passphrase confirmation: %w", err)
}

if string(p1) != string(p2) {
return "", fmt.Errorf("passphrases do not match")
}
return string(p1), nil
}

// setupSSHKey generates the picoclaw-specific SSH key at ~/.ssh/picoclaw_ed25519.key.
// If the key already exists the user is warned and asked to confirm overwrite.
// Answering anything other than "y" keeps the existing key (not an error).
func setupSSHKey() error {
keyPath, err := credential.DefaultSSHKeyPath()
if err != nil {
return fmt.Errorf("cannot determine SSH key path: %w", err)
}

if _, err := os.Stat(keyPath); err == nil {
fmt.Printf("\n⚠️ WARNING: %s already exists.\n", keyPath)
fmt.Println(" Overwriting will invalidate any credentials previously encrypted with this key.")
fmt.Print(" Overwrite? (y/n): ")
var response string
fmt.Scanln(&response)
if response != "y" {
fmt.Println("Keeping existing SSH key.")
return nil
}
}

if err := credential.GenerateSSHKey(keyPath); err != nil {
return err
}
fmt.Printf("SSH key generated: %s\n", keyPath)
return nil
}

func createWorkspaceTemplates(workspace string) {
Expand Down
Loading
Loading