diff --git a/cmd/picoclaw/main.go b/cmd/picoclaw/main.go index 10b53948b..3987e06c0 100644 --- a/cmd/picoclaw/main.go +++ b/cmd/picoclaw/main.go @@ -405,6 +405,9 @@ func agentCmd() { os.Exit(1) } + // Apply workspace upgrades before starting the agent + migrate.UpgradeWorkspace(cfg.WorkspacePath()) + msgBus := bus.NewMessageBus() agentLoop := agent.NewAgentLoop(cfg, msgBus, provider) @@ -540,6 +543,9 @@ func gatewayCmd() { os.Exit(1) } + // Apply workspace upgrades before starting the agent + migrate.UpgradeWorkspace(cfg.WorkspacePath()) + msgBus := bus.NewMessageBus() agentLoop := agent.NewAgentLoop(cfg, msgBus, provider) diff --git a/pkg/migrate/upgrade.go b/pkg/migrate/upgrade.go new file mode 100644 index 000000000..1dc9e5392 --- /dev/null +++ b/pkg/migrate/upgrade.go @@ -0,0 +1,51 @@ +package migrate + +import ( + "os" + "path/filepath" + "strings" + + "github.com/sipeed/picoclaw/pkg/logger" +) + +// UpgradeWorkspace applies incremental upgrades to an existing workspace. +// Each upgrade is idempotent — it checks whether it has already been applied +// before making changes. +func UpgradeWorkspace(workspace string) { + upgradeAgentMediaSection(workspace) +} + +// upgradeAgentMediaSection ensures AGENT.md contains the media sending instructions. +// Added in v0.x to teach the LLM it can send files via the message tool. +func upgradeAgentMediaSection(workspace string) { + agentPath := filepath.Join(workspace, "AGENT.md") + + data, err := os.ReadFile(agentPath) + if err != nil { + return // File doesn't exist or unreadable — skip + } + + content := string(data) + + // Already applied + if strings.Contains(content, "## Media & File Sending") { + return + } + + section := ` + +## Media & File Sending + +You CAN send files directly to users. When you need to share a file (image, document, audio, video), use the ` + "`message`" + ` tool with the ` + "`media`" + ` parameter containing the local file path(s). The file will be delivered natively through the user's channel (Telegram, Discord, Slack, etc.). Do NOT tell users you cannot send files — just send them.` + + content += section + + if err := os.WriteFile(agentPath, []byte(content), 0644); err != nil { + logger.ErrorCF("migrate", "Failed to upgrade AGENT.md", map[string]interface{}{ + "error": err.Error(), + }) + return + } + + logger.InfoC("migrate", "Upgraded AGENT.md with media sending instructions") +} diff --git a/pkg/migrate/upgrade_test.go b/pkg/migrate/upgrade_test.go new file mode 100644 index 000000000..358e6588c --- /dev/null +++ b/pkg/migrate/upgrade_test.go @@ -0,0 +1,70 @@ +package migrate + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestUpgradeAgentMediaSection(t *testing.T) { + t.Run("appends media section to existing AGENT.md", func(t *testing.T) { + workspace := t.TempDir() + agentPath := filepath.Join(workspace, "AGENT.md") + os.WriteFile(agentPath, []byte("# Agent Instructions\n\nBe helpful."), 0644) + + UpgradeWorkspace(workspace) + + data, err := os.ReadFile(agentPath) + if err != nil { + t.Fatalf("reading AGENT.md: %v", err) + } + content := string(data) + + if !strings.Contains(content, "## Media & File Sending") { + t.Error("expected media section to be appended") + } + if !strings.Contains(content, "You CAN send files directly") { + t.Error("expected media instructions in content") + } + // Original content preserved + if !strings.Contains(content, "# Agent Instructions") { + t.Error("original content should be preserved") + } + }) + + t.Run("idempotent — does not duplicate section", func(t *testing.T) { + workspace := t.TempDir() + agentPath := filepath.Join(workspace, "AGENT.md") + os.WriteFile(agentPath, []byte("# Agent Instructions\n\nBe helpful."), 0644) + + UpgradeWorkspace(workspace) + UpgradeWorkspace(workspace) + + data, _ := os.ReadFile(agentPath) + count := strings.Count(string(data), "## Media & File Sending") + if count != 1 { + t.Errorf("expected exactly 1 media section, got %d", count) + } + }) + + t.Run("skips when AGENT.md does not exist", func(t *testing.T) { + workspace := t.TempDir() + // No AGENT.md created — should not panic or error + UpgradeWorkspace(workspace) + }) + + t.Run("skips when section already present", func(t *testing.T) { + workspace := t.TempDir() + agentPath := filepath.Join(workspace, "AGENT.md") + original := "# Agent\n\n## Media & File Sending\n\nAlready here." + os.WriteFile(agentPath, []byte(original), 0644) + + UpgradeWorkspace(workspace) + + data, _ := os.ReadFile(agentPath) + if string(data) != original { + t.Error("file should not be modified when section already exists") + } + }) +}