diff --git a/cmd/picoclaw/main.go b/cmd/picoclaw/main.go
index 2129662d7..b5f0cbdf6 100644
--- a/cmd/picoclaw/main.go
+++ b/cmd/picoclaw/main.go
@@ -14,6 +14,7 @@ import (
"io"
"io/fs"
"os"
+ "os/exec"
"os/signal"
"path/filepath"
"runtime"
@@ -138,6 +139,8 @@ func main() {
migrateCmd()
case "auth":
authCmd()
+ case "install":
+ installCmd()
case "cron":
cronCmd()
case "skills":
@@ -207,6 +210,7 @@ func printHelp() {
fmt.Println(" agent Interact with the agent directly")
fmt.Println(" auth Manage authentication (login, logout, status)")
fmt.Println(" gateway Start picoclaw gateway")
+ fmt.Println(" install Install/uninstall launchd service (macOS)")
fmt.Println(" status Show picoclaw status")
fmt.Println(" cron Manage scheduled tasks")
fmt.Println(" migrate Migrate from OpenClaw to PicoClaw")
@@ -1410,3 +1414,199 @@ func skillsShowCmd(loader *skills.SkillsLoader, skillName string) {
fmt.Println("----------------------")
fmt.Println(content)
}
+
+// installCmd handles the install subcommand
+func installCmd() {
+ if len(os.Args) < 3 {
+ doInstall()
+ return
+ }
+
+ switch os.Args[2] {
+ case "--help", "-h":
+ installHelp()
+ case "--uninstall":
+ uninstallInstall()
+ case "--status":
+ statusInstall()
+ default:
+ fmt.Printf("Unknown install option: %s\n", os.Args[2])
+ installHelp()
+ }
+}
+
+// installHelp displays help information for install command
+func installHelp() {
+ fmt.Println("\nInstall PicoClaw as a macOS service (launch agent)")
+ fmt.Println()
+ fmt.Println("Usage: picoclaw install [options]")
+ fmt.Println()
+ fmt.Println("Options:")
+ fmt.Println(" --uninstall Uninstall the launch agent")
+ fmt.Println(" --status Show installation status")
+ fmt.Println(" --help, -h Show this help message")
+ fmt.Println()
+ fmt.Println("Examples:")
+ fmt.Println(" picoclaw install Install as launch agent")
+ fmt.Println(" picoclaw install --uninstall Uninstall the launch agent")
+ fmt.Println(" picoclaw install --status Check installation status")
+}
+
+// doInstall executes the installation
+func doInstall() {
+ execPath, err := findExecutable()
+ if err != nil {
+ fmt.Printf("Error: %v\n", err)
+ os.Exit(1)
+ }
+
+ fmt.Printf("Found picoclaw at: %s\n", execPath)
+
+ // Generate plist content
+ plistContent := generatePlist(execPath)
+
+ // Get plist path
+ home, err := os.UserHomeDir()
+ if err != nil {
+ fmt.Printf("Error getting home directory: %v\n", err)
+ os.Exit(1)
+ }
+ plistDir := filepath.Join(home, "Library", "LaunchAgents")
+ plistPath := filepath.Join(plistDir, "io.picoclaw.gateway.plist")
+
+ // Create directory if not exists
+ if err := os.MkdirAll(plistDir, 0755); err != nil {
+ fmt.Printf("Error creating LaunchAgents directory: %v\n", err)
+ os.Exit(1)
+ }
+
+ // Write plist file
+ if err := os.WriteFile(plistPath, []byte(plistContent), 0644); err != nil {
+ fmt.Printf("Error writing plist file: %v\n", err)
+ os.Exit(1)
+ }
+
+ fmt.Printf("Created plist at: %s\n", plistPath)
+
+ // Load the launch agent
+ cmd := exec.Command("launchctl", "load", plistPath)
+ if output, err := cmd.CombinedOutput(); err != nil {
+ fmt.Printf("Error loading launch agent: %v\n", err)
+ fmt.Printf("Output: %s\n", string(output))
+ os.Exit(1)
+ }
+
+ fmt.Println("\n✓ PicoClaw gateway installed successfully!")
+ fmt.Println("The gateway will start automatically on login.")
+}
+
+// findExecutable finds the picoclaw executable path
+func findExecutable() (string, error) {
+ // 使用 exec.LookPath 查找
+ path, err := exec.LookPath("picoclaw")
+ if err == nil {
+ return path, nil
+ }
+
+ // 如果不是 "not found" 错误,返回原错误
+ if !strings.Contains(err.Error(), "not found") {
+ return "", err
+ }
+
+ // 尝试常见路径
+ home, err := os.UserHomeDir()
+ if err != nil {
+ return "", fmt.Errorf("cannot find home directory: %w", err)
+ }
+
+ commonPaths := []string{
+ "/usr/local/bin/picoclaw",
+ "/usr/bin/picoclaw",
+ filepath.Join(home, "go", "bin", "picoclaw"),
+ }
+
+ for _, p := range commonPaths {
+ if _, err := os.Stat(p); err == nil {
+ return p, nil
+ }
+ }
+
+ return "", fmt.Errorf("picoclaw not found. Please run 'go install' first or ensure picoclaw is in your PATH")
+}
+
+// generatePlist generates the plist content for launch agent
+func generatePlist(execPath string) string {
+ return fmt.Sprintf(`
+
+
+
+ Label
+ io.picoclaw.gateway
+ ProgramArguments
+
+ %s
+ gateway
+
+ RunAtLoad
+
+ KeepAlive
+
+
+`, execPath)
+}
+
+// uninstallInstall uninstalls the launch agent
+func uninstallInstall() {
+ home, _ := os.UserHomeDir()
+ plistPath := filepath.Join(home, "Library", "LaunchAgents", "io.picoclaw.gateway.plist")
+
+ if _, err := os.Stat(plistPath); os.IsNotExist(err) {
+ fmt.Println("PicoClaw is not installed.")
+ return
+ }
+
+ // Unload the launch agent
+ cmd := exec.Command("launchctl", "unload", plistPath)
+ if output, err := cmd.CombinedOutput(); err != nil {
+ fmt.Printf("Error unloading launch agent: %v\n", err)
+ fmt.Printf("Output: %s\n", string(output))
+ }
+
+ // Remove plist file
+ if err := os.Remove(plistPath); err != nil {
+ fmt.Printf("Error removing plist file: %v\n", err)
+ os.Exit(1)
+ }
+
+ fmt.Println("✓ PicoClaw gateway uninstalled successfully!")
+}
+
+// statusInstall shows the installation status
+func statusInstall() {
+ home, _ := os.UserHomeDir()
+ plistPath := filepath.Join(home, "Library", "LaunchAgents", "io.picoclaw.gateway.plist")
+
+ fmt.Println("\nPicoClaw Gateway Status:")
+ fmt.Println("------------------------")
+
+ if _, err := os.Stat(plistPath); os.IsNotExist(err) {
+ fmt.Println("Status: Not installed")
+ fmt.Println("Run 'picoclaw install' to install as launch agent")
+ return
+ }
+
+ fmt.Println("Status: Installed")
+ fmt.Printf("Plist: %s\n", plistPath)
+
+ // Check if loaded
+ cmd := exec.Command("launchctl", "list", "io.picoclaw.gateway")
+ output, err := cmd.CombinedOutput()
+ if err == nil {
+ fmt.Println("\nLaunch agent is loaded.")
+ if len(output) > 0 {
+ fmt.Println(string(output))
+ }
+ } else {
+ fmt.Println("\nLaunch agent is not running.")
+ }
+}
diff --git a/docs/plans/2026-02-15-macos-launchd-autostart-design.md b/docs/plans/2026-02-15-macos-launchd-autostart-design.md
new file mode 100644
index 000000000..8c9b6383b
--- /dev/null
+++ b/docs/plans/2026-02-15-macos-launchd-autostart-design.md
@@ -0,0 +1,52 @@
+# macOS 开机启动 (launchd) 设计方案
+
+## 概述
+
+添加 `picoclaw install` 命令,自动生成 macOS launchd 的 plist 配置文件并安装到 `~/Library/LaunchAgents/`,实现开机自动启动 `picoclaw gateway`。
+
+## 详细设计
+
+### 1. plist 文件内容
+
+```xml
+
+
+
+
+ Label
+ io.picoclaw.gateway
+ ProgramArguments
+
+ /usr/local/bin/picoclaw
+ gateway
+
+ RunAtLoad
+
+ KeepAlive
+
+
+
+```
+
+### 2. 命令设计
+
+| 命令 | 描述 |
+|------|------|
+| `picoclaw install` | 安装 launchd plist(当前用户) |
+| `picoclaw install --uninstall` | 卸载 launchd plist |
+| `picoclaw install --status` | 查看安装状态 |
+
+### 3. 实现位置
+
+在 `cmd/picoclaw/main.go` 中添加 `install` case。
+
+### 4. 可执行文件路径
+
+- 优先使用 `which picoclaw` 查找已安装路径
+- 如果找不到,提示用户需要先 `go install` 或手动复制
+
+## 实现步骤
+
+1. 在 main.go 添加 install case
+2. 实现 installCmd() 函数
+3. 添加 uninstall 和 status 子命令
diff --git a/docs/plans/2026-02-15-macos-launchd-autostart-implementation.md b/docs/plans/2026-02-15-macos-launchd-autostart-implementation.md
new file mode 100644
index 000000000..e9d329fce
--- /dev/null
+++ b/docs/plans/2026-02-15-macos-launchd-autostart-implementation.md
@@ -0,0 +1,252 @@
+# macOS 开机启动 (launchd) 实现计划
+
+> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
+
+**Goal:** 在 picoclaw 项目中添加 `picoclaw install` 命令,实现 macOS 开机自动启动 `picoclaw gateway`
+
+**Architecture:** 通过生成 launchd plist 配置文件到 `~/Library/LaunchAgents/` 目录,实现用户级开机自启动
+
+**Tech Stack:** Go, macOS launchd
+
+---
+
+## 实现步骤
+
+### Task 1: 添加 install case 到 main.go
+
+**Files:**
+- Modify: `cmd/picoclaw/main.go:126-145`
+
+**Step 1: 在 main.go 的 switch 语句中添加 install case**
+
+在 `case "cron":` 之前添加:
+
+```go
+case "install":
+ installCmd()
+```
+
+**Step 2: 运行测试验证**
+
+Run: `go build ./cmd/picoclaw/`
+Expected: 编译成功(因为 installCmd 尚未定义,会有警告)
+
+---
+
+### Task 2: 实现 installCmd 函数
+
+**Files:**
+- Modify: `cmd/picoclaw/main.go:199-216`
+
+**Step 1: 添加 installCmd 函数和帮助信息**
+
+在 `printHelp()` 函数后添加:
+
+```go
+func installCmd() {
+ args := os.Args[2:]
+ for _, arg := range args {
+ switch arg {
+ case "--uninstall":
+ uninstallInstall()
+ return
+ case "--status":
+ statusInstall()
+ return
+ case "--help", "-h":
+ installHelp()
+ return
+ }
+ }
+ doInstall()
+}
+
+func installHelp() {
+ fmt.Println("\nInstall commands:")
+ fmt.Println(" picoclaw install Install launchd plist (current user)")
+ fmt.Println(" picoclaw install --uninstall Uninstall launchd plist")
+ fmt.Println(" picoclaw install --status Show install status")
+}
+
+func doInstall() {
+ // 查找 picoclaw 可执行文件路径
+ execPath, err := findExecutable()
+ if err != nil {
+ fmt.Printf("Error: %v\n", err)
+ os.Exit(1)
+ }
+
+ // 生成 plist 内容
+ plistContent := generatePlist(execPath)
+
+ // 写入文件
+ plistPath := filepath.Join(os.Getenv("HOME"), "Library", "LaunchAgents", "io.picoclaw.gateway.plist")
+ if err := os.WriteFile(plistPath, []byte(plistContent), 0644); err != nil {
+ fmt.Printf("Error writing plist: %v\n", err)
+ os.Exit(1)
+ }
+
+ fmt.Printf("✓ Installed launchd plist to %s\n", plistPath)
+ fmt.Println("Run 'picoclaw install --status' to check status")
+}
+
+func findExecutable() (string, error) {
+ // 使用 which 查找
+ cmd := exec.Command("which", "picoclaw")
+ out, err := cmd.Output()
+ if err == nil {
+ return strings.TrimSpace(string(out)), nil
+ }
+
+ // 尝试常见路径
+ commonPaths := []string{
+ "/usr/local/bin/picoclaw",
+ "/usr/bin/picoclaw",
+ filepath.Join(os.Getenv("HOME"), "go", "bin", "picoclaw"),
+ }
+
+ for _, p := range commonPaths {
+ if _, err := os.Stat(p); err == nil {
+ return p, nil
+ }
+ }
+
+ return "", fmt.Errorf("picoclaw not found in PATH. Please run 'go install' first or ensure picoclaw is in your PATH")
+}
+
+func generatePlist(execPath string) string {
+ return `
+
+
+
+ Label
+ io.picoclaw.gateway
+ ProgramArguments
+
+ ` + execPath + `
+ gateway
+
+ RunAtLoad
+
+ KeepAlive
+
+
+
+`
+}
+
+func uninstallInstall() {
+ plistPath := filepath.Join(os.Getenv("HOME"), "Library", "LaunchAgents", "io.picoclaw.gateway.plist")
+ if _, err := os.Stat(plistPath); os.IsNotExist(err) {
+ fmt.Println("No launchd plist found.")
+ return
+ }
+
+ // 先 unload
+ exec.Command("launchctl", "unload", plistPath).Run()
+
+ if err := os.Remove(plistPath); err != nil {
+ fmt.Printf("Error removing plist: %v\n", err)
+ os.Exit(1)
+ }
+
+ fmt.Printf("✓ Removed %s\n", plistPath)
+}
+
+func statusInstall() {
+ plistPath := filepath.Join(os.Getenv("HOME"), "Library", "LaunchAgents", "io.picoclaw.gateway.plist")
+
+ if _, err := os.Stat(plistPath); os.IsNotExist(err) {
+ fmt.Println("Status: Not installed")
+ fmt.Println("Run 'picoclaw install' to install")
+ return
+ }
+
+ fmt.Println("Status: Installed")
+ fmt.Printf("Path: %s\n", plistPath)
+
+ // 检查是否正在运行
+ cmd := exec.Command("launchctl", "list", "io.picoclaw.gateway")
+ out, err := cmd.Output()
+ if err == nil {
+ fmt.Println("\nRunning: Yes")
+ fmt.Println(string(out))
+ } else {
+ fmt.Println("\nRunning: No")
+ }
+}
+```
+
+**Step 2: 添加 import**
+
+确保已导入 `os/exec`:
+
+```go
+import (
+ // ... 其他 import
+ "os/exec"
+)
+```
+
+**Step 3: 运行测试验证**
+
+Run: `go build ./cmd/picoclaw/`
+Expected: 编译成功
+
+**Step 4: 手动测试**
+
+```bash
+./picoclaw install --help
+./picoclaw install --status
+```
+
+---
+
+### Task 3: 更新帮助信息
+
+**Files:**
+- Modify: `cmd/picoclaw/main.go:201-215`
+
+**Step 1: 在 printHelp 中添加 install 命令**
+
+在 Commands 列表中添加:
+
+```go
+fmt.Println(" install Install/uninstall launchd service (macOS)")
+```
+
+---
+
+### Task 4: 测试完整流程
+
+**Step 1: 构建并测试安装**
+
+```bash
+go build -o picoclaw ./cmd/picoclaw/
+./picoclaw install
+./picoclaw install --status
+```
+
+Expected: 成功创建 plist 文件
+
+**Step 2: 测试卸载**
+
+```bash
+./picoclaw install --uninstall
+./picoclaw install --status
+```
+
+Expected: 成功删除 plist 文件
+
+---
+
+### Task 5: 提交代码
+
+```bash
+git add cmd/picoclaw/main.go
+git commit -m "feat: add macOS launchd autostart support
+
+- Add 'picoclaw install' command
+- Support install, uninstall, and status options
+- Auto-detect picoclaw executable path"
+```