From 34b9d5d6fa2cbeb805cc9be506c5e4300f88d161 Mon Sep 17 00:00:00 2001 From: afjcjsbx Date: Sun, 12 Apr 2026 10:44:09 +0200 Subject: [PATCH 01/36] fix(telegram): preserve raw OAuth links in HTML rendering --- .../telegram/parser_markdown_to_html.go | 45 ++++++++++++++++++- .../telegram/parser_markdown_to_html_test.go | 10 +++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/pkg/channels/telegram/parser_markdown_to_html.go b/pkg/channels/telegram/parser_markdown_to_html.go index 95dc3e9d6..0614b6e32 100644 --- a/pkg/channels/telegram/parser_markdown_to_html.go +++ b/pkg/channels/telegram/parser_markdown_to_html.go @@ -2,9 +2,13 @@ package telegram import ( "fmt" + "html" + "regexp" "strings" ) +var reRawURL = regexp.MustCompile(`https?://[^\s<]+`) + func markdownToTelegramHTML(text string) string { if text == "" { return "" @@ -19,6 +23,9 @@ func markdownToTelegramHTML(text string) string { links := extractLinks(text) text = links.text + rawURLs := extractRawURLs(text) + text = rawURLs.text + text = reHeading.ReplaceAllString(text, "$1") text = reBlockquote.ReplaceAllString(text, "$1") @@ -43,10 +50,19 @@ func markdownToTelegramHTML(text string) string { for i, lnk := range links.links { label := escapeHTML(lnk[0]) - url := lnk[1] + url := escapeHTMLAttr(lnk[1]) text = strings.ReplaceAll(text, fmt.Sprintf("\x00LK%d\x00", i), fmt.Sprintf(`%s`, url, label)) } + for i, rawURL := range rawURLs.urls { + escaped := escapeHTML(rawURL) + text = strings.ReplaceAll( + text, + fmt.Sprintf("\x00RU%d\x00", i), + fmt.Sprintf(`%s`, escapeHTMLAttr(rawURL), escaped), + ) + } + for i, code := range inlineCodes.codes { escaped := escapeHTML(code) text = strings.ReplaceAll(text, fmt.Sprintf("\x00IC%d\x00", i), fmt.Sprintf("%s", escaped)) @@ -92,6 +108,11 @@ type codeBlockMatch struct { codes []string } +type rawURLMatch struct { + text string + urls []string +} + func extractCodeBlocks(text string) codeBlockMatch { matches := reCodeBlock.FindAllStringSubmatch(text, -1) @@ -110,6 +131,24 @@ func extractCodeBlocks(text string) codeBlockMatch { return codeBlockMatch{text: text, codes: codes} } +func extractRawURLs(text string) rawURLMatch { + matches := reRawURL.FindAllString(text, -1) + + urls := make([]string, 0, len(matches)) + for _, match := range matches { + urls = append(urls, match) + } + + i := 0 + text = reRawURL.ReplaceAllStringFunc(text, func(string) string { + placeholder := fmt.Sprintf("\x00RU%d\x00", i) + i++ + return placeholder + }) + + return rawURLMatch{text: text, urls: urls} +} + type inlineCodeMatch struct { text string codes []string @@ -139,3 +178,7 @@ func escapeHTML(text string) string { text = strings.ReplaceAll(text, ">", ">") return text } + +func escapeHTMLAttr(text string) string { + return html.EscapeString(text) +} diff --git a/pkg/channels/telegram/parser_markdown_to_html_test.go b/pkg/channels/telegram/parser_markdown_to_html_test.go index 7754ee076..a05b39877 100644 --- a/pkg/channels/telegram/parser_markdown_to_html_test.go +++ b/pkg/channels/telegram/parser_markdown_to_html_test.go @@ -32,6 +32,11 @@ func Test_markdownToTelegramHTML(t *testing.T) { input: "[click here](https://example.com/path)", expected: `click here`, }, + { + name: "raw oauth url with underscores survives", + input: "Apri https://accounts.google.com/o/oauth2/auth?response_type=code&client_id=test-client&redirect_uri=http%3A%2F%2Flocalhost%3A8001%2Foauth2callback&code_challenge=abc_def&code_challenge_method=S256", + expected: `Apri https://accounts.google.com/o/oauth2/auth?response_type=code&client_id=test-client&redirect_uri=http%3A%2F%2Flocalhost%3A8001%2Foauth2callback&code_challenge=abc_def&code_challenge_method=S256`, + }, { name: "link with underscores in URL is not corrupted by italic regex", // Google Flights URLs use URL-safe base64 with underscores in the tfs param. @@ -45,6 +50,11 @@ func Test_markdownToTelegramHTML(t *testing.T) { input: "[first](https://a.com/path_one) and [second](https://b.com/path_two_x)", expected: `first and second`, }, + { + name: "markdown link query params are escaped in href", + input: "[oauth](https://example.com/cb?response_type=code&client_id=test-client)", + expected: `oauth`, + }, { name: "link label with HTML special chars is escaped", input: "[a & b](https://example.com)", From ac4db35c0b6304c3f690a3a0d2ad0a0bb9ac8abd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 17:19:51 +0000 Subject: [PATCH 02/36] build(deps): bump github.com/aws/aws-sdk-go-v2/service/bedrockruntime Bumps [github.com/aws/aws-sdk-go-v2/service/bedrockruntime](https://github.com/aws/aws-sdk-go-v2) from 1.50.4 to 1.50.5. - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/ssm/v1.50.4...service/ssm/v1.50.5) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go-v2/service/bedrockruntime dependency-version: 1.50.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 12 ++++++------ go.sum | 26 ++++++++++++-------------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/go.mod b/go.mod index a8b540662..391f5a2fd 100644 --- a/go.mod +++ b/go.mod @@ -9,9 +9,9 @@ require ( github.com/adhocore/gronx v1.19.6 github.com/anthropics/anthropic-sdk-go v1.26.0 github.com/atc0005/go-teams-notify/v2 v2.14.0 - github.com/aws/aws-sdk-go-v2 v1.41.5 + github.com/aws/aws-sdk-go-v2 v1.41.6 github.com/aws/aws-sdk-go-v2/config v1.32.14 - github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.50.4 + github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.50.5 github.com/bwmarrin/discordgo v0.29.0 github.com/caarlos0/env/v11 v11.4.0 github.com/charmbracelet/lipgloss v1.1.0 @@ -55,11 +55,11 @@ require ( require ( aead.dev/minisign v0.2.0 // indirect filippo.io/edwards25519 v1.2.0 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.9 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.19.14 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.22 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.22 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect @@ -67,7 +67,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/sso v1.30.15 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 // indirect - github.com/aws/smithy-go v1.24.2 // indirect + github.com/aws/smithy-go v1.25.0 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beeper/argo-go v1.1.2 // indirect github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect diff --git a/go.sum b/go.sum index f63c7b44e..854fdaed1 100644 --- a/go.sum +++ b/go.sum @@ -23,24 +23,24 @@ github.com/anthropics/anthropic-sdk-go v1.26.0 h1:oUTzFaUpAevfuELAP1sjL6CQJ9HHAf github.com/anthropics/anthropic-sdk-go v1.26.0/go.mod h1:qUKmaW+uuPB64iy1l+4kOSvaLqPXnHTTBKH6RVZ7q5Q= github.com/atc0005/go-teams-notify/v2 v2.14.0 h1:7N+xw+COnYANLREaAveQ65rsNQ12nIZJED9nMLyscCo= github.com/atc0005/go-teams-notify/v2 v2.14.0/go.mod h1:EECsWM2b0Hvoz7O+QdlsvyN2KCUOFQCGj8bUBXv3A3Q= -github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY= -github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI= +github.com/aws/aws-sdk-go-v2 v1.41.6 h1:1AX0AthnBQzMx1vbmir3Y4WsnJgiydmnJjiLu+LvXOg= +github.com/aws/aws-sdk-go-v2 v1.41.6/go.mod h1:dy0UzBIfwSeot4grGvY1AqFWN5zgziMmWGzysDnHFcQ= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.9 h1:adBsCIIpLbLmYnkQU+nAChU5yhVTvu5PerROm+/Kq2A= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.9/go.mod h1:uOYhgfgThm/ZyAuJGNQ5YgNyOlYfqnGpTHXvk3cpykg= github.com/aws/aws-sdk-go-v2/config v1.32.14 h1:opVIRo/ZbbI8OIqSOKmpFaY7IwfFUOCCXBsUpJOwDdI= github.com/aws/aws-sdk-go-v2/config v1.32.14/go.mod h1:U4/V0uKxh0Tl5sxmCBZ3AecYny4UNlVmObYjKuuaiOo= github.com/aws/aws-sdk-go-v2/credentials v1.19.14 h1:n+UcGWAIZHkXzYt87uMFBv/l8THYELoX6gVcUvgl6fI= github.com/aws/aws-sdk-go-v2/credentials v1.19.14/go.mod h1:cJKuyWB59Mqi0jM3nFYQRmnHVQIcgoxjEMAbLkpr62w= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 h1:NUS3K4BTDArQqNu2ih7yeDLaS3bmHD0YndtA6UP884g= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21/go.mod h1:YWNWJQNjKigKY1RHVJCuupeWDrrHjRqHm0N9rdrWzYI= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.22 h1:GmLa5Kw1ESqtFpXsx5MmC84QWa/ZrLZvlJGa2y+4kcQ= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.22/go.mod h1:6sW9iWm9DK9YRpRGga/qzrzNLgKpT2cIxb7Vo2eNOp0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.22 h1:dY4kWZiSaXIzxnKlj17nHnBcXXBfac6UlsAx2qL6XrU= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.22/go.mod h1:KIpEUx0JuRZLO7U6cbV204cWAEco2iC3l061IxlwLtI= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= -github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.50.4 h1:W6tKfa/s37faUnwJ71pGqsBO7/wfUX1L7tVprupQGo4= -github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.50.4/go.mod h1:BZ+9thH0QOTDUwE8KAv/ZwUzsNC7CSMJXj/wtnZMs5k= +github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.50.5 h1:ZGTl4Rxft1uyENAlGESY04hMzE4cLLNUPI7dGw08haw= +github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.50.5/go.mod h1:jnugA+VgESQGgXuEKK6zVToET/DtODq7LQYpe+BkKT4= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto= @@ -53,8 +53,8 @@ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 h1:dzztQ1YmfPrxdrOiuZRMF6f github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19/go.mod h1:YO8TrYtFdl5w/4vmjL8zaBSsiNp3w0L1FfKVKenZT7w= github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 h1:p8ogvvLugcR/zLBXTXrTkj0RYBUdErbMnAFFp12Lm/U= github.com/aws/aws-sdk-go-v2/service/sts v1.41.10/go.mod h1:60dv0eZJfeVXfbT1tFJinbHrDfSJ2GZl4Q//OSSNAVw= -github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= -github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aws/smithy-go v1.25.0 h1:Sz/XJ64rwuiKtB6j98nDIPyYrV1nVNJ4YU74gttcl5U= +github.com/aws/smithy-go v1.25.0/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/beeper/argo-go v1.1.2 h1:UQI2G8F+NLfGTOmTUI0254pGKx/HUU/etbUGTJv91Fs= @@ -138,8 +138,6 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab h1:VYNivV7P8IRHUam2swVUNkhIdp0LRRFKe4hXNnoZKTc= -github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/gomarkdown/markdown v0.0.0-20260411013819-759bbc3e3207 h1:p7t34F7K4OCRQblcDhNJnP46Uaarz3z2cLcvOZYxWn8= github.com/gomarkdown/markdown v0.0.0-20260411013819-759bbc3e3207/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= From 2da05c2ad3b831e705f6be1ac8d5bd578b6ffc72 Mon Sep 17 00:00:00 2001 From: afjcjsbx Date: Thu, 23 Apr 2026 19:20:15 +0200 Subject: [PATCH 03/36] feat(mcp): add show, add, list, remove, test, edit cli commands --- README.md | 20 +- cmd/picoclaw/internal/cliui/mcp_show.go | 384 +++++++++++++++ cmd/picoclaw/internal/mcp/add.go | 232 +++++++++ cmd/picoclaw/internal/mcp/command.go | 25 + cmd/picoclaw/internal/mcp/command_test.go | 557 ++++++++++++++++++++++ cmd/picoclaw/internal/mcp/edit.go | 54 +++ cmd/picoclaw/internal/mcp/helpers.go | 359 ++++++++++++++ cmd/picoclaw/internal/mcp/list.go | 78 +++ cmd/picoclaw/internal/mcp/remove.go | 39 ++ cmd/picoclaw/internal/mcp/show.go | 237 +++++++++ cmd/picoclaw/internal/mcp/test.go | 46 ++ cmd/picoclaw/main.go | 2 + cmd/picoclaw/main_test.go | 1 + docs/project/README.it.md | 20 +- docs/reference/README.md | 1 + docs/reference/mcp-cli.md | 349 ++++++++++++++ docs/reference/tools_configuration.md | 11 + 17 files changed, 2413 insertions(+), 2 deletions(-) create mode 100644 cmd/picoclaw/internal/cliui/mcp_show.go create mode 100644 cmd/picoclaw/internal/mcp/add.go create mode 100644 cmd/picoclaw/internal/mcp/command.go create mode 100644 cmd/picoclaw/internal/mcp/command_test.go create mode 100644 cmd/picoclaw/internal/mcp/edit.go create mode 100644 cmd/picoclaw/internal/mcp/helpers.go create mode 100644 cmd/picoclaw/internal/mcp/list.go create mode 100644 cmd/picoclaw/internal/mcp/remove.go create mode 100644 cmd/picoclaw/internal/mcp/show.go create mode 100644 cmd/picoclaw/internal/mcp/test.go create mode 100644 docs/reference/mcp-cli.md diff --git a/README.md b/README.md index 5aac4bbc9..9668fcf34 100644 --- a/README.md +++ b/README.md @@ -571,7 +571,19 @@ PicoClaw natively supports [MCP](https://modelcontextprotocol.io/) — connect a } ``` -For full MCP configuration (stdio, SSE, HTTP transports, Tool Discovery), see [Tools Configuration - MCP](docs/reference/tools_configuration.md#mcp-tool). +You can manage common MCP setups directly from the CLI instead of editing JSON by hand: + +```bash +picoclaw mcp add filesystem -- npx -y @modelcontextprotocol/server-filesystem /tmp +picoclaw mcp list +picoclaw mcp test filesystem +``` + +`picoclaw mcp` is a configuration manager: it updates `config.json` under `tools.mcp.servers`, but it does not keep the server process running itself. + +Use `picoclaw mcp edit` when you need advanced fields such as `headers`, `env_file`, or `deferred`. + +For full MCP configuration (stdio, SSE, HTTP transports, Tool Discovery), see [Tools Configuration - MCP](docs/reference/tools_configuration.md#mcp-tool). For CLI usage and examples, see [MCP Server CLI](docs/reference/mcp-cli.md). ## ClawdChat Join the Agent Social Network @@ -591,6 +603,11 @@ Connect PicoClaw to the Agent Social Network simply by sending a single message | `picoclaw status` | Show status | | `picoclaw version` | Show version info | | `picoclaw model` | View or switch the default model | +| `picoclaw mcp list` | List configured MCP servers | +| `picoclaw mcp add ...` | Add or update an MCP server entry | +| `picoclaw mcp test` | Probe a configured MCP server | +| `picoclaw mcp edit` | Open config for advanced MCP editing | +| `picoclaw mcp remove` | Remove an MCP server entry | | `picoclaw cron list` | List all scheduled jobs | | `picoclaw cron add ...` | Add a scheduled job | | `picoclaw cron disable` | Disable a scheduled job | @@ -619,6 +636,7 @@ For detailed guides beyond this README: | [Docker & Quick Start](docs/guides/docker.md) | Docker Compose setup, Launcher/Agent modes | | [Chat Apps](docs/guides/chat-apps.md) | All 17+ channel setup guides | | [Configuration](docs/guides/configuration.md) | Environment variables, workspace layout, security sandbox | +| [MCP Server CLI](docs/reference/mcp-cli.md) | Add, list, test, edit, and remove MCP server entries from the CLI | | [Scheduled Tasks and Cron Jobs](docs/reference/cron.md) | Cron schedule types, deliver modes, command gates, job storage | | [Providers & Models](docs/guides/providers.md) | 30+ LLM providers, model routing, model_list configuration | | [Spawn & Async Tasks](docs/guides/spawn-tasks.md) | Quick tasks, long tasks with spawn, async sub-agent orchestration | diff --git a/cmd/picoclaw/internal/cliui/mcp_show.go b/cmd/picoclaw/internal/cliui/mcp_show.go new file mode 100644 index 000000000..5d5af1e75 --- /dev/null +++ b/cmd/picoclaw/internal/cliui/mcp_show.go @@ -0,0 +1,384 @@ +package cliui + +import ( + "fmt" + "io" + "strings" + + "github.com/charmbracelet/lipgloss" +) + +// MCPShowServer holds the server metadata for PrintMCPShow. +type MCPShowServer struct { + Name string + Type string + Target string + Enabled bool + EffectiveDeferred bool // resolved value (per-server override or global default) + DeferredExplicit bool // true = per-server override set, false = inherited from global + EnvKeys []string // sorted env var names (values intentionally omitted) + EnvFile string + Headers []string // sorted header names +} + +// MCPShowTool holds one tool's info for PrintMCPShow. +type MCPShowTool struct { + Name string + Description string + Parameters []MCPShowParam +} + +// MCPShowParam is one parameter entry. +type MCPShowParam struct { + Name string + Type string + Description string + Required bool +} + +// PrintMCPShow renders the mcp show output (plain or fancy). +// w is where the output is written; pass cmd.OutOrStdout() from cobra commands. +func PrintMCPShow(w io.Writer, server MCPShowServer, tools []MCPShowTool, disabled bool) { + if !UseFancyLayout() { + printMCPShowPlain(w, server, tools, disabled) + return + } + printMCPShowFancy(w, server, tools, disabled) +} + +// ── plain (narrow / non-TTY) ──────────────────────────────────────────────── + +func printMCPShowPlain(w io.Writer, server MCPShowServer, tools []MCPShowTool, disabled bool) { + fmt.Fprintf(w, "Server: %s\n", server.Name) + fmt.Fprintf(w, "Type: %s\n", server.Type) + fmt.Fprintf(w, "Target: %s\n", server.Target) + fmt.Fprintf(w, "Enabled: %s\n", boolWord(server.Enabled)) + deferredLabel := boolWord(server.EffectiveDeferred) + if !server.DeferredExplicit { + deferredLabel += " (default)" + } + fmt.Fprintf(w, "Deferred: %s\n", deferredLabel) + if len(server.EnvKeys) > 0 { + fmt.Fprintf(w, "Env vars: %s\n", strings.Join(server.EnvKeys, ", ")) + } + if server.EnvFile != "" { + fmt.Fprintf(w, "Env file: %s\n", server.EnvFile) + } + if len(server.Headers) > 0 { + fmt.Fprintf(w, "Headers: %s\n", strings.Join(server.Headers, ", ")) + } + fmt.Fprintln(w) + + if disabled { + fmt.Fprintln(w, "Server is disabled; skipping tool discovery.") + return + } + if len(tools) == 0 { + fmt.Fprintln(w, "No tools exposed by this server.") + return + } + + fmt.Fprintf(w, "Tools (%d):\n", len(tools)) + for _, tool := range tools { + fmt.Fprintf(w, " %s\n", tool.Name) + if tool.Description != "" { + fmt.Fprintf(w, " %s\n", truncateDescription(tool.Description, 120)) + } + if len(tool.Parameters) == 0 { + fmt.Fprintln(w, " Parameters: none") + continue + } + for _, p := range tool.Parameters { + line := fmt.Sprintf(" - %s", p.Name) + if p.Type != "" { + line += fmt.Sprintf(" (%s", p.Type) + if p.Required { + line += ", required" + } + line += ")" + } else if p.Required { + line += " (required)" + } + if p.Description != "" { + line += ": " + truncateDescription(p.Description, 80) + } + fmt.Fprintln(w, line) + } + } +} + +// ── fancy (wide TTY) ──────────────────────────────────────────────────────── + +var ( + mcpToolNameStyle = func() lipgloss.Style { + return lipgloss.NewStyle().Foreground(accentBlue).Bold(true) + } + mcpParamNameStyle = func() lipgloss.Style { + return lipgloss.NewStyle().Foreground(accentRed).Bold(true) + } + mcpTagStyle = func() lipgloss.Style { + return lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")) + } + mcpRequiredStyle = func() lipgloss.Style { + return lipgloss.NewStyle().Foreground(lipgloss.Color("#D54646")).Bold(true) + } + mcpOptionalStyle = func() lipgloss.Style { + return lipgloss.NewStyle().Foreground(lipgloss.Color("#6B6B6B")) + } + mcpDescStyle = func() lipgloss.Style { + return lipgloss.NewStyle().Foreground(lipgloss.Color("#CCCCCC")) + } +) + +func printMCPShowFancy(w io.Writer, server MCPShowServer, tools []MCPShowTool, disabled bool) { + inner := InnerWidth() + box := borderStyle().Width(inner) + + var b strings.Builder + + // ── server header ── + b.WriteString(titleBarStyle().Render("⬡ " + server.Name)) + b.WriteString("\n\n") + + keyW := 10 + writeKV := func(key, val string) { + k := kvKeyStyle().Width(keyW).Render(key) + b.WriteString(k + " " + val + "\n") + } + + writeKV("Type", server.Type) + writeKV("Target", server.Target) + writeKV("Enabled", coloredBool(server.Enabled)) + deferredVal := coloredBool(server.EffectiveDeferred) + if !server.DeferredExplicit { + deferredVal += " " + mcpTagStyle().Render("(default)") + } + writeKV("Deferred", deferredVal) + if len(server.EnvKeys) > 0 { + writeKV("Env vars", mutedStyle().Render(strings.Join(server.EnvKeys, ", "))) + } + if server.EnvFile != "" { + writeKV("Env file", mutedStyle().Render(server.EnvFile)) + } + if len(server.Headers) > 0 { + writeKV("Headers", mutedStyle().Render(strings.Join(server.Headers, ", "))) + } + + if disabled { + b.WriteString("\n") + b.WriteString(mutedStyle().Render("Server is disabled; skipping tool discovery.")) + fmt.Fprintln(w, box.Render(b.String())) + return + } + + if len(tools) == 0 { + b.WriteString("\n") + b.WriteString(mutedStyle().Render("No tools exposed by this server.")) + fmt.Fprintln(w, box.Render(b.String())) + return + } + + // ── tools section ── + b.WriteString("\n") + b.WriteString(kvKeyStyle().Render(fmt.Sprintf("Tools (%d)", len(tools)))) + b.WriteString("\n") + + contentW := inner - 4 // account for box padding + for i, tool := range tools { + if i > 0 { + b.WriteString(strings.Repeat("─", contentW) + "\n") + } + b.WriteString("\n") + + // Tool name + index badge + badge := mcpTagStyle().Render(fmt.Sprintf("[%d/%d]", i+1, len(tools))) + b.WriteString(" " + mcpToolNameStyle().Render(tool.Name) + " " + badge + "\n") + + // Description (wrapped to content width) + if tool.Description != "" { + desc := truncateDescription(tool.Description, 160) + b.WriteString(" " + mcpDescStyle().Render(desc) + "\n") + } + + // Parameters + if len(tool.Parameters) == 0 { + b.WriteString(" " + mcpTagStyle().Render("no parameters") + "\n") + continue + } + + b.WriteString("\n") + for _, p := range tool.Parameters { + // name + pName := mcpParamNameStyle().Render(p.Name) + + // type tag + typeTag := "" + if p.Type != "" { + typeTag = " " + mcpTagStyle().Render("<"+p.Type+">") + } + + // required / optional badge + var reqBadge string + if p.Required { + reqBadge = " " + mcpRequiredStyle().Render("required") + } else { + reqBadge = " " + mcpOptionalStyle().Render("optional") + } + + b.WriteString(" " + pName + typeTag + reqBadge + "\n") + + if p.Description != "" { + desc := truncateDescription(p.Description, 120) + b.WriteString(" " + mutedStyle().Render(desc) + "\n") + } + } + } + + fmt.Fprintln(w, box.Render(b.String())) +} + +// ── mcp list ──────────────────────────────────────────────────────────────── + +// MCPListRow is one row in the mcp list output. +type MCPListRow struct { + Name string + Type string + Target string + Status string // "enabled", "disabled", "ok (N tools)", "error" + EffectiveDeferred bool // resolved value (per-server override or global default) + DeferredExplicit bool // true = per-server override set, false = inherited from global +} + +// PrintMCPList renders the mcp list output (plain or fancy). +func PrintMCPList(w io.Writer, rows []MCPListRow) { + if !UseFancyLayout() { + printMCPListPlain(w, rows) + return + } + printMCPListFancy(w, rows) +} + +func printMCPListPlain(w io.Writer, rows []MCPListRow) { + headers := []string{"Name", "Type", "Command", "Status", "Deferred"} + tableRows := make([][]string, len(rows)) + for i, r := range rows { + deferred := boolWord(r.EffectiveDeferred) + if !r.DeferredExplicit { + deferred += " (default)" + } + tableRows[i] = []string{r.Name, r.Type, r.Target, r.Status, deferred} + } + // reuse the ASCII table renderer already in helpers.go via the caller + // (list.go still uses renderTable for the plain path) + widths := make([]int, len(headers)) + for i, h := range headers { + widths[i] = len(h) + } + for _, row := range tableRows { + for i, cell := range row { + if len(cell) > widths[i] { + widths[i] = len(cell) + } + } + } + border := func() { + fmt.Fprint(w, "+") + for _, width := range widths { + fmt.Fprint(w, strings.Repeat("-", width+2)+"+") + } + fmt.Fprintln(w) + } + writeRow := func(row []string) { + fmt.Fprint(w, "|") + for i, cell := range row { + fmt.Fprintf(w, " %s%s |", cell, strings.Repeat(" ", widths[i]-len(cell))) + } + fmt.Fprintln(w) + } + border() + writeRow(headers) + border() + for _, row := range tableRows { + writeRow(row) + } + border() +} + +func printMCPListFancy(w io.Writer, rows []MCPListRow) { + inner := InnerWidth() + box := borderStyle().Width(inner) + + var b strings.Builder + + title := fmt.Sprintf("MCP Servers (%d)", len(rows)) + b.WriteString(titleBarStyle().Render(title)) + b.WriteString("\n") + + contentW := inner - 4 + for i, row := range rows { + if i > 0 { + b.WriteString(strings.Repeat("─", contentW) + "\n") + } + b.WriteString("\n") + + statusBadge := mcpListStatusStyle(row.Status).Render(row.Status) + var deferredBadge string + if row.EffectiveDeferred { + if row.DeferredExplicit { + deferredBadge = " " + mcpTagStyle().Render("deferred") + } else { + deferredBadge = " " + mcpOptionalStyle().Render("deferred (default)") + } + } + b.WriteString(" " + mcpToolNameStyle().Render(row.Name) + " " + statusBadge + deferredBadge + "\n") + b.WriteString(" " + mcpTagStyle().Render(row.Type+" "+row.Target) + "\n") + } + + fmt.Fprintln(w, box.Render(b.String())) +} + +func mcpListStatusStyle(status string) lipgloss.Style { + switch { + case status == "enabled": + return lipgloss.NewStyle().Foreground(lipgloss.Color("#2E7D32")).Bold(true) + case status == "disabled": + return lipgloss.NewStyle().Foreground(lipgloss.Color("#6B6B6B")) + case strings.HasPrefix(status, "ok"): + return lipgloss.NewStyle().Foreground(lipgloss.Color("#2E7D32")).Bold(true) + case status == "error": + return lipgloss.NewStyle().Foreground(lipgloss.Color("#D54646")).Bold(true) + default: + return lipgloss.NewStyle() + } +} + +// ── helpers ───────────────────────────────────────────────────────────────── + +func boolWord(v bool) string { + if v { + return "yes" + } + return "no" +} + +func coloredBool(v bool) string { + if v { + return lipgloss.NewStyle().Foreground(lipgloss.Color("#2E7D32")).Bold(true).Render("yes") + } + return lipgloss.NewStyle().Foreground(lipgloss.Color("#D54646")).Render("no") +} + +// truncateDescription strips newlines, collapses whitespace, and caps length. +func truncateDescription(s string, maxLen int) string { + // collapse newlines and repeated spaces into a single space + s = strings.Join(strings.Fields(s), " ") + if len(s) <= maxLen { + return s + } + // cut at last space before maxLen + cut := s[:maxLen] + if idx := strings.LastIndex(cut, " "); idx > maxLen/2 { + cut = cut[:idx] + } + return cut + "…" +} diff --git a/cmd/picoclaw/internal/mcp/add.go b/cmd/picoclaw/internal/mcp/add.go new file mode 100644 index 000000000..57e8f5368 --- /dev/null +++ b/cmd/picoclaw/internal/mcp/add.go @@ -0,0 +1,232 @@ +package mcp + +import ( + "fmt" + "net/url" + "strings" + + "github.com/spf13/cobra" + + "github.com/sipeed/picoclaw/pkg/config" +) + +type addOptions struct { + Env []string + Headers []string + Transport string + Force bool + Deferred *bool // nil = not set, true = deferred, false = not deferred +} + +func newAddCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "add [flags] [args...]", + Short: "Add or update an MCP server", + DisableFlagParsing: true, + RunE: func(cmd *cobra.Command, args []string) error { + opts, name, target, targetArgs, showHelp, err := parseAddArgs(args) + if showHelp { + return cmd.Help() + } + if err != nil { + return err + } + + cfg, err := loadConfig() + if err != nil { + return err + } + if cfg.Tools.MCP.Servers == nil { + cfg.Tools.MCP.Servers = make(map[string]config.MCPServerConfig) + } + + if _, exists := cfg.Tools.MCP.Servers[name]; exists && !opts.Force { + var overwrite bool + + overwrite, err = confirmOverwrite(cmd.InOrStdin(), cmd.OutOrStdout(), name) + if err != nil { + return fmt.Errorf("failed to confirm overwrite: %w", err) + } + if !overwrite { + return fmt.Errorf("aborted: MCP server %q already exists", name) + } + } + + server, err := buildServerConfig(target, targetArgs, opts) + if err != nil { + return err + } + + cfg.Tools.MCP.Enabled = true + cfg.Tools.MCP.Servers[name] = server + + if err := saveValidatedConfig(cfg); err != nil { + return err + } + + fmt.Fprintf(cmd.OutOrStdout(), "✓ MCP server %q saved.\n", name) + return nil + }, + } + + flags := cmd.Flags() + flags.StringArrayP("env", "e", nil, "Environment variable in KEY=value format (repeatable)") + flags.StringArrayP("header", "H", nil, "HTTP header in 'Name: Value' or 'Name=Value' format (repeatable)") + flags.StringP("transport", "t", "stdio", "Transport type: stdio, http, or sse") + flags.BoolP("force", "f", false, "Overwrite an existing server without prompting") + flags.Bool("deferred", false, "Mark server as deferred (tools hidden until explicitly activated)") + flags.Bool("no-deferred", false, "Mark server as non-deferred (tools always active)") + + return cmd +} + +func parseAddArgs(args []string) (addOptions, string, string, []string, bool, error) { + opts := addOptions{Transport: "stdio"} + var positional []string + serverArgs := make([]string, 0) + explicitCommand := make([]string, 0) + + for i := 0; i < len(args); i++ { + arg := args[i] + + switch { + case arg == "--help" || arg == "-h": + return addOptions{}, "", "", nil, true, nil + case arg == "--": + if i+1 < len(args) { + explicitCommand = append(explicitCommand, args[i+1:]...) + } + i = len(args) + case arg == "--force" || arg == "-f": + opts.Force = true + case arg == "--deferred": + t := true + opts.Deferred = &t + case arg == "--no-deferred": + f := false + opts.Deferred = &f + case arg == "--transport" || arg == "-t": + if i+1 >= len(args) { + return addOptions{}, "", "", nil, false, fmt.Errorf("missing value for %s", arg) + } + i++ + opts.Transport = args[i] + case strings.HasPrefix(arg, "--transport="): + opts.Transport = strings.TrimPrefix(arg, "--transport=") + case arg == "--env" || arg == "-e": + if i+1 >= len(args) { + return addOptions{}, "", "", nil, false, fmt.Errorf("missing value for %s", arg) + } + i++ + opts.Env = append(opts.Env, args[i]) + case strings.HasPrefix(arg, "--env="): + opts.Env = append(opts.Env, strings.TrimPrefix(arg, "--env=")) + case arg == "--header" || arg == "-H": + if i+1 >= len(args) { + return addOptions{}, "", "", nil, false, fmt.Errorf("missing value for %s", arg) + } + i++ + opts.Headers = append(opts.Headers, args[i]) + case strings.HasPrefix(arg, "--header="): + opts.Headers = append(opts.Headers, strings.TrimPrefix(arg, "--header=")) + case strings.HasPrefix(arg, "-") && len(positional) >= 2: + serverArgs = append(serverArgs, args[i:]...) + i = len(args) + default: + positional = append(positional, arg) + } + } + + if len(explicitCommand) > 0 { + if len(positional) != 1 { + return addOptions{}, "", "", nil, false, fmt.Errorf( + "usage: picoclaw mcp add [flags] [args...] or picoclaw mcp add [flags] -- [args...]", + ) + } + if len(explicitCommand) == 0 { + return addOptions{}, "", "", nil, false, fmt.Errorf("missing stdio command after --") + } + return opts, positional[0], explicitCommand[0], explicitCommand[1:], false, nil + } + + if len(positional) < 2 { + return addOptions{}, "", "", nil, false, fmt.Errorf( + "usage: picoclaw mcp add [flags] [args...] or picoclaw mcp add [flags] -- [args...]", + ) + } + + targetArgs := make([]string, 0, len(positional)-2+len(serverArgs)) + targetArgs = append(targetArgs, positional[2:]...) + targetArgs = append(targetArgs, serverArgs...) + + return opts, positional[0], positional[1], targetArgs, false, nil +} + +func buildServerConfig(target string, args []string, opts addOptions) (config.MCPServerConfig, error) { + transport := strings.ToLower(strings.TrimSpace(opts.Transport)) + if transport == "" { + transport = "stdio" + } + switch transport { + case "stdio", "http", "sse": + default: + return config.MCPServerConfig{}, fmt.Errorf("unsupported transport %q", opts.Transport) + } + + env, err := parseEnvAssignments(opts.Env) + if err != nil { + return config.MCPServerConfig{}, err + } + headers, err := parseHeaderAssignments(opts.Headers) + if err != nil { + return config.MCPServerConfig{}, err + } + + server := config.MCPServerConfig{ + Enabled: true, + Type: transport, + Deferred: opts.Deferred, + } + + switch transport { + case "http", "sse": + if len(env) > 0 { + return config.MCPServerConfig{}, fmt.Errorf("--env can only be used with stdio transport") + } + if len(args) > 0 { + return config.MCPServerConfig{}, fmt.Errorf("%s transport does not accept command arguments", transport) + } + parsedURL, err := url.ParseRequestURI(target) + if err != nil || parsedURL.Scheme == "" || parsedURL.Host == "" { + return config.MCPServerConfig{}, fmt.Errorf("invalid MCP URL %q", target) + } + server.URL = target + server.Headers = headers + return server, nil + } + + if len(headers) > 0 { + return config.MCPServerConfig{}, fmt.Errorf("--header can only be used with http or sse transport") + } + + if looksLikeRemoteURL(target) { + return config.MCPServerConfig{}, fmt.Errorf( + "target %q looks like a remote MCP URL, but transport is %q. Use --transport http or --transport sse", + target, + transport, + ) + } + + command := target + commandArgs := append([]string(nil), args...) + + if err := validateLocalCommandPath(target); err != nil { + return config.MCPServerConfig{}, err + } + + server.Command = command + server.Args = commandArgs + server.Env = env + + return server, nil +} diff --git a/cmd/picoclaw/internal/mcp/command.go b/cmd/picoclaw/internal/mcp/command.go new file mode 100644 index 000000000..d6e21181a --- /dev/null +++ b/cmd/picoclaw/internal/mcp/command.go @@ -0,0 +1,25 @@ +package mcp + +import "github.com/spf13/cobra" + +func NewMCPCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "mcp", + Short: "Manage MCP server configuration", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + return cmd.Help() + }, + } + + cmd.AddCommand( + newAddCommand(), + newRemoveCommand(), + newListCommand(), + newEditCommand(), + newTestCommand(), + newShowCommand(), + ) + + return cmd +} diff --git a/cmd/picoclaw/internal/mcp/command_test.go b/cmd/picoclaw/internal/mcp/command_test.go new file mode 100644 index 000000000..6fd8a6f58 --- /dev/null +++ b/cmd/picoclaw/internal/mcp/command_test.go @@ -0,0 +1,557 @@ +package mcp + +import ( + "bytes" + "context" + "os" + "os/exec" + "path/filepath" + "slices" + "strings" + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/sipeed/picoclaw/pkg/config" +) + +func TestNewMCPCommand(t *testing.T) { + cmd := NewMCPCommand() + + require.NotNil(t, cmd) + + assert.Equal(t, "mcp", cmd.Use) + assert.Equal(t, "Manage MCP server configuration", cmd.Short) + assert.True(t, cmd.HasSubCommands()) + + allowedCommands := []string{ + "add", + "remove", + "list", + "edit", + "test", + "show", + } + + subcommands := cmd.Commands() + assert.Len(t, subcommands, len(allowedCommands)) + + for _, subcmd := range subcommands { + found := slices.Contains(allowedCommands, subcmd.Name()) + assert.True(t, found, "unexpected subcommand %q", subcmd.Name()) + assert.False(t, subcmd.Hidden) + } +} + +func TestMCPAddAddsGenericStdioServer(t *testing.T) { + configPath := setupMCPConfigEnv(t) + + cmd := NewMCPCommand() + output, err := executeCommand(cmd, []string{ + "add", + "sqlite", + "npx", + "-y", + "@modelcontextprotocol/server-sqlite", + "--db", + "./mydb.db", + }, "") + require.NoError(t, err) + assert.Contains(t, output, `MCP server "sqlite" saved`) + + cfg := readMCPConfig(t, configPath) + require.True(t, cfg.Tools.MCP.Enabled) + + server, ok := cfg.Tools.MCP.Servers["sqlite"] + require.True(t, ok) + assert.True(t, server.Enabled) + assert.Equal(t, "stdio", server.Type) + assert.Equal(t, "npx", server.Command) + assert.Equal(t, []string{"-y", "@modelcontextprotocol/server-sqlite", "--db", "./mydb.db"}, server.Args) +} + +func TestMCPAddSupportsHeadersAfterURL(t *testing.T) { + configPath := setupMCPConfigEnv(t) + + cmd := NewMCPCommand() + _, err := executeCommand(cmd, []string{ + "add", + "apify", + "https://mcp.apify.com/", + "-t", + "http", + "--header", + "Authorization: Bearer OMITTED", + }, "") + require.NoError(t, err) + + cfg := readMCPConfig(t, configPath) + server := cfg.Tools.MCP.Servers["apify"] + assert.Equal(t, "http", server.Type) + assert.Equal(t, "https://mcp.apify.com/", server.URL) + assert.Equal(t, map[string]string{"Authorization": "Bearer OMITTED"}, server.Headers) +} + +func TestMCPAddSupportsTransportBeforeName(t *testing.T) { + configPath := setupMCPConfigEnv(t) + + cmd := NewMCPCommand() + _, err := executeCommand(cmd, []string{ + "add", + "--transport", + "sse", + "fiscal-ai", + "https://api.fiscal.ai/mcp/sse", + }, "") + require.NoError(t, err) + + cfg := readMCPConfig(t, configPath) + server := cfg.Tools.MCP.Servers["fiscal-ai"] + assert.Equal(t, "sse", server.Type) + assert.Equal(t, "https://api.fiscal.ai/mcp/sse", server.URL) +} + +func TestMCPAddSupportsExplicitStdioCommandAfterSeparator(t *testing.T) { + configPath := setupMCPConfigEnv(t) + + cmd := NewMCPCommand() + _, err := executeCommand(cmd, []string{ + "add", + "--transport", + "stdio", + "--env", + "AIRTABLE_API_KEY=YOUR_KEY", + "airtable", + "--", + "npx", + "-y", + "airtable-mcp-server", + }, "") + require.NoError(t, err) + + cfg := readMCPConfig(t, configPath) + server := cfg.Tools.MCP.Servers["airtable"] + assert.Equal(t, "stdio", server.Type) + assert.Equal(t, "npx", server.Command) + assert.Equal(t, []string{"-y", "airtable-mcp-server"}, server.Args) + assert.Equal(t, map[string]string{"AIRTABLE_API_KEY": "YOUR_KEY"}, server.Env) +} + +func TestMCPAddRejectsNonExecutableLocalCommand(t *testing.T) { + setupMCPConfigEnv(t) + + tmpDir := t.TempDir() + localCmd := filepath.Join(tmpDir, "server.sh") + require.NoError(t, os.WriteFile(localCmd, []byte("#!/bin/sh\nexit 0\n"), 0o644)) + + cmd := NewMCPCommand() + _, err := executeCommand(cmd, []string{"add", "local", localCmd}, "") + require.Error(t, err) + assert.Contains(t, err.Error(), "not executable") +} + +func TestMCPAddShowsClearErrorForRemoteURLWithoutTransport(t *testing.T) { + setupMCPConfigEnv(t) + + cmd := NewMCPCommand() + _, err := executeCommand(cmd, []string{"add", "apify", "https://mcp.apify.com/"}, "") + require.Error(t, err) + assert.Contains(t, err.Error(), `looks like a remote MCP URL`) + assert.Contains(t, err.Error(), `Use --transport http or --transport sse`) +} + +func TestMCPAddOverwritePromptDecline(t *testing.T) { + configPath := setupMCPConfigEnv(t) + writeMCPConfig(t, configPath, &config.Config{ + Tools: config.ToolsConfig{ + MCP: config.MCPConfig{ + ToolConfig: config.ToolConfig{Enabled: true}, + Servers: map[string]config.MCPServerConfig{ + "filesystem": { + Enabled: true, + Type: "stdio", + Command: "old", + }, + }, + }, + }, + }) + + cmd := NewMCPCommand() + output, err := executeCommand(cmd, []string{"add", "filesystem", "new-command"}, "n\n") + require.Error(t, err) + assert.Contains(t, output, `Overwrite? [y/N]:`) + assert.Contains(t, err.Error(), "aborted") + + cfg := readMCPConfig(t, configPath) + assert.Equal(t, "old", cfg.Tools.MCP.Servers["filesystem"].Command) +} + +func TestMCPAddOverwriteWithConfirmation(t *testing.T) { + configPath := setupMCPConfigEnv(t) + writeMCPConfig(t, configPath, &config.Config{ + Tools: config.ToolsConfig{ + MCP: config.MCPConfig{ + ToolConfig: config.ToolConfig{Enabled: true}, + Servers: map[string]config.MCPServerConfig{ + "filesystem": { + Enabled: true, + Type: "stdio", + Command: "old", + }, + }, + }, + }, + }) + + cmd := NewMCPCommand() + _, err := executeCommand(cmd, []string{"add", "filesystem", "new-command"}, "y\n") + require.NoError(t, err) + + cfg := readMCPConfig(t, configPath) + assert.Equal(t, "new-command", cfg.Tools.MCP.Servers["filesystem"].Command) +} + +func TestMCPAddHTTPServer(t *testing.T) { + configPath := setupMCPConfigEnv(t) + + cmd := NewMCPCommand() + _, err := executeCommand(cmd, []string{ + "add", + "context7", + "--transport", + "http", + "https://mcp.context7.com/mcp", + }, "") + require.NoError(t, err) + + cfg := readMCPConfig(t, configPath) + server := cfg.Tools.MCP.Servers["context7"] + assert.Equal(t, "http", server.Type) + assert.Equal(t, "https://mcp.context7.com/mcp", server.URL) + assert.Empty(t, server.Command) +} + +func TestMCPRemoveRemovesLastServerAndDisablesMCP(t *testing.T) { + configPath := setupMCPConfigEnv(t) + writeMCPConfig(t, configPath, &config.Config{ + Tools: config.ToolsConfig{ + MCP: config.MCPConfig{ + ToolConfig: config.ToolConfig{Enabled: true}, + Servers: map[string]config.MCPServerConfig{ + "filesystem": { + Enabled: true, + Type: "stdio", + Command: "npx", + }, + }, + }, + }, + }) + + cmd := NewMCPCommand() + output, err := executeCommand(cmd, []string{"remove", "filesystem"}, "") + require.NoError(t, err) + assert.Contains(t, output, `MCP server "filesystem" removed`) + + cfg := readMCPConfig(t, configPath) + assert.False(t, cfg.Tools.MCP.Enabled) + assert.Empty(t, cfg.Tools.MCP.Servers) +} + +func TestMCPListPrintsTable(t *testing.T) { + configPath := setupMCPConfigEnv(t) + writeMCPConfig(t, configPath, &config.Config{ + Tools: config.ToolsConfig{ + MCP: config.MCPConfig{ + ToolConfig: config.ToolConfig{Enabled: true}, + Servers: map[string]config.MCPServerConfig{ + "context7": { + Enabled: true, + Type: "http", + URL: "https://mcp.context7.com/mcp", + }, + "filesystem": { + Enabled: false, + Type: "stdio", + Command: "npx", + Args: []string{"-y", "@modelcontextprotocol/server-filesystem", "/tmp"}, + }, + }, + }, + }, + }) + + cmd := NewMCPCommand() + output, err := executeCommand(cmd, []string{"list"}, "") + require.NoError(t, err) + assert.Contains(t, output, "| Name") + assert.Contains(t, output, "context7") + assert.Contains(t, output, "filesystem") + assert.Contains(t, output, "https://mcp.context7.com/mcp") + assert.Contains(t, output, "disabled") +} + +func TestMCPListWithStatusUsesProbe(t *testing.T) { + configPath := setupMCPConfigEnv(t) + writeMCPConfig(t, configPath, &config.Config{ + Tools: config.ToolsConfig{ + MCP: config.MCPConfig{ + ToolConfig: config.ToolConfig{Enabled: true}, + Servers: map[string]config.MCPServerConfig{ + "filesystem": { + Enabled: true, + Type: "stdio", + Command: "npx", + }, + }, + }, + }, + }) + + originalProbe := serverProbe + defer func() { serverProbe = originalProbe }() + serverProbe = func(_ context.Context, name string, server config.MCPServerConfig, workspacePath string) (probeResult, error) { + assert.Equal(t, "filesystem", name) + assert.Equal(t, readMCPConfig(t, configPath).WorkspacePath(), workspacePath) + assert.Equal(t, "npx", server.Command) + return probeResult{ToolCount: 3}, nil + } + + cmd := NewMCPCommand() + output, err := executeCommand(cmd, []string{"list", "--status"}, "") + require.NoError(t, err) + assert.Contains(t, output, "ok (3 tools)") +} + +func TestMCPEditUsesEditor(t *testing.T) { + configPath := setupMCPConfigEnv(t) + + originalEditor := editorCommand + defer func() { editorCommand = originalEditor }() + + var gotName string + var gotArgs []string + editorCommand = func(name string, args ...string) *exec.Cmd { + gotName = name + gotArgs = append([]string(nil), args...) + return exec.Command("sh", "-c", "exit 0") + } + + t.Setenv("EDITOR", `dummy-editor --wait`) + + cmd := NewMCPCommand() + _, err := executeCommand(cmd, []string{"edit"}, "") + require.NoError(t, err) + + assert.Equal(t, "dummy-editor", gotName) + assert.Equal(t, []string{"--wait", configPath}, gotArgs) + _, statErr := os.Stat(configPath) + assert.NoError(t, statErr) +} + +func TestMCPEditRequiresEditor(t *testing.T) { + setupMCPConfigEnv(t) + t.Setenv("EDITOR", "") + + cmd := NewMCPCommand() + _, err := executeCommand(cmd, []string{"edit"}, "") + require.Error(t, err) + assert.Contains(t, err.Error(), "$EDITOR is not set") +} + +func TestMCPTestUsesProbe(t *testing.T) { + configPath := setupMCPConfigEnv(t) + writeMCPConfig(t, configPath, &config.Config{ + Tools: config.ToolsConfig{ + MCP: config.MCPConfig{ + ToolConfig: config.ToolConfig{Enabled: true}, + Servers: map[string]config.MCPServerConfig{ + "filesystem": { + Enabled: false, + Type: "stdio", + Command: "npx", + }, + }, + }, + }, + }) + + originalProbe := serverProbe + defer func() { serverProbe = originalProbe }() + serverProbe = func(_ context.Context, name string, _ config.MCPServerConfig, workspacePath string) (probeResult, error) { + assert.Equal(t, "filesystem", name) + assert.Equal(t, readMCPConfig(t, configPath).WorkspacePath(), workspacePath) + return probeResult{ToolCount: 2}, nil + } + + cmd := NewMCPCommand() + output, err := executeCommand(cmd, []string{"test", "filesystem"}, "") + require.NoError(t, err) + assert.Contains(t, output, `MCP server "filesystem" reachable (2 tools)`) +} + +func TestMCPAddDeferredFlag(t *testing.T) { + configPath := setupMCPConfigEnv(t) + + cmd := NewMCPCommand() + _, err := executeCommand(cmd, []string{"add", "--deferred", "myserver", "npx", "my-mcp"}, "") + require.NoError(t, err) + + cfg := readMCPConfig(t, configPath) + server := cfg.Tools.MCP.Servers["myserver"] + require.NotNil(t, server.Deferred) + assert.True(t, *server.Deferred) +} + +func TestMCPAddNoDeferredFlag(t *testing.T) { + configPath := setupMCPConfigEnv(t) + + cmd := NewMCPCommand() + _, err := executeCommand(cmd, []string{"add", "--no-deferred", "myserver", "npx", "my-mcp"}, "") + require.NoError(t, err) + + cfg := readMCPConfig(t, configPath) + server := cfg.Tools.MCP.Servers["myserver"] + require.NotNil(t, server.Deferred) + assert.False(t, *server.Deferred) +} + +func TestMCPAddNoDeferredByDefault(t *testing.T) { + configPath := setupMCPConfigEnv(t) + + cmd := NewMCPCommand() + _, err := executeCommand(cmd, []string{"add", "myserver", "npx", "my-mcp"}, "") + require.NoError(t, err) + + cfg := readMCPConfig(t, configPath) + server := cfg.Tools.MCP.Servers["myserver"] + assert.Nil(t, server.Deferred) +} + +func TestMCPShowNotFound(t *testing.T) { + configPath := setupMCPConfigEnv(t) + writeMCPConfig(t, configPath, nil) + + cmd := NewMCPCommand() + _, err := executeCommand(cmd, []string{"show", "missing"}, "") + require.Error(t, err) + assert.Contains(t, err.Error(), `"missing" not found`) +} + +func TestMCPShowDisabledServer(t *testing.T) { + configPath := setupMCPConfigEnv(t) + writeMCPConfig(t, configPath, &config.Config{ + Tools: config.ToolsConfig{ + MCP: config.MCPConfig{ + ToolConfig: config.ToolConfig{Enabled: true}, + Servers: map[string]config.MCPServerConfig{ + "myserver": { + Enabled: false, + Type: "stdio", + Command: "npx", + }, + }, + }, + }, + }) + + cmd := NewMCPCommand() + output, err := executeCommand(cmd, []string{"show", "myserver"}, "") + require.NoError(t, err) + assert.Contains(t, output, "myserver") + assert.Contains(t, output, "disabled") +} + +func TestMCPShowUsesProbe(t *testing.T) { + configPath := setupMCPConfigEnv(t) + writeMCPConfig(t, configPath, &config.Config{ + Tools: config.ToolsConfig{ + MCP: config.MCPConfig{ + ToolConfig: config.ToolConfig{Enabled: true}, + Servers: map[string]config.MCPServerConfig{ + "myserver": { + Enabled: true, + Type: "stdio", + Command: "npx", + }, + }, + }, + }, + }) + + original := serverShowProbe + defer func() { serverShowProbe = original }() + serverShowProbe = func(_ context.Context, name string, _ config.MCPServerConfig, _ string) ([]toolDetail, error) { + assert.Equal(t, "myserver", name) + return []toolDetail{ + { + Name: "read_file", + Description: "Read a file from the filesystem", + Parameters: []paramDetail{ + {Name: "path", Type: "string", Description: "File path", Required: true}, + {Name: "encoding", Type: "string", Description: "Character encoding", Required: false}, + }, + }, + { + Name: "list_dir", + Description: "List directory contents", + Parameters: nil, + }, + }, nil + } + + cmd := NewMCPCommand() + output, err := executeCommand(cmd, []string{"show", "myserver"}, "") + require.NoError(t, err) + assert.Contains(t, output, "myserver") + assert.Contains(t, output, "read_file") + assert.Contains(t, output, "Read a file from the filesystem") + assert.Contains(t, output, "path") + assert.Contains(t, output, "string") + assert.Contains(t, output, "required") + assert.Contains(t, output, "list_dir") + assert.Contains(t, output, "none") +} + +func setupMCPConfigEnv(t *testing.T) string { + t.Helper() + + configPath := filepath.Join(t.TempDir(), "config.json") + t.Setenv(config.EnvConfig, configPath) + t.Setenv(config.EnvHome, filepath.Dir(configPath)) + return configPath +} + +func writeMCPConfig(t *testing.T, path string, cfg *config.Config) { + t.Helper() + + if cfg == nil { + cfg = config.DefaultConfig() + } + + require.NoError(t, config.SaveConfig(path, cfg)) +} + +func readMCPConfig(t *testing.T, path string) *config.Config { + t.Helper() + + cfg, err := config.LoadConfig(path) + require.NoError(t, err) + return cfg +} + +func executeCommand(cmd *cobra.Command, args []string, stdin string) (string, error) { + var stdout bytes.Buffer + var stderr bytes.Buffer + + cmd.SetArgs(args) + cmd.SetOut(&stdout) + cmd.SetErr(&stderr) + cmd.SetIn(strings.NewReader(stdin)) + + err := cmd.Execute() + return stdout.String() + stderr.String(), err +} diff --git a/cmd/picoclaw/internal/mcp/edit.go b/cmd/picoclaw/internal/mcp/edit.go new file mode 100644 index 000000000..06dcb6aef --- /dev/null +++ b/cmd/picoclaw/internal/mcp/edit.go @@ -0,0 +1,54 @@ +package mcp + +import ( + "fmt" + "os" + "strings" + + "github.com/spf13/cobra" + "go.mau.fi/util/shlex" + + "github.com/sipeed/picoclaw/cmd/picoclaw/internal" +) + +func newEditCommand() *cobra.Command { + return &cobra.Command{ + Use: "edit", + Short: "Open the PicoClaw config in $EDITOR", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + editor := strings.TrimSpace(os.Getenv("EDITOR")) + if editor == "" { + return fmt.Errorf("$EDITOR is not set") + } + + cfg, err := loadConfig() + if err != nil { + return err + } + if err = saveValidatedConfig(cfg); err != nil { + return err + } + + editorArgs, err := shlex.Split(editor) + if err != nil { + return fmt.Errorf("failed to parse $EDITOR: %w", err) + } + if len(editorArgs) == 0 { + return fmt.Errorf("$EDITOR is empty") + } + + editorArgs = append(editorArgs, internal.GetConfigPath()) + process := editorCommand(editorArgs[0], editorArgs[1:]...) + process.Stdin = cmd.InOrStdin() + process.Stdout = cmd.OutOrStdout() + process.Stderr = cmd.ErrOrStderr() + + if err := process.Run(); err != nil { + return fmt.Errorf("failed to start editor: %w", err) + } + + return nil + }, + } +} diff --git a/cmd/picoclaw/internal/mcp/helpers.go b/cmd/picoclaw/internal/mcp/helpers.go new file mode 100644 index 000000000..0fb0b245c --- /dev/null +++ b/cmd/picoclaw/internal/mcp/helpers.go @@ -0,0 +1,359 @@ +package mcp + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/url" + "os" + "os/exec" + "path/filepath" + "runtime" + "sort" + "strings" + "sync" + + "github.com/google/jsonschema-go/jsonschema" + + "github.com/sipeed/picoclaw/cmd/picoclaw/internal" + "github.com/sipeed/picoclaw/pkg/config" + picomcp "github.com/sipeed/picoclaw/pkg/mcp" +) + +type probeResult struct { + ToolCount int +} + +var ( + editorCommand = exec.Command + serverProbe = defaultServerProbe + + mcpConfigSchemaOnce sync.Once + mcpConfigSchema *jsonschema.Resolved + errMcpConfigSchema error +) + +const mcpConfigSchemaJSON = `{ + "type": "object", + "properties": { + "tools": { + "type": "object", + "properties": { + "mcp": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "discovery": { "type": "object", "additionalProperties": true }, + "max_inline_text_chars": { "type": "integer" }, + "servers": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "deferred": { "type": "boolean" }, + "command": { "type": "string" }, + "args": { + "type": "array", + "items": { "type": "string" } + }, + "env": { + "type": "object", + "additionalProperties": { "type": "string" } + }, + "env_file": { "type": "string" }, + "type": { + "type": "string", + "enum": ["stdio", "http", "sse"] + }, + "url": { "type": "string" }, + "headers": { + "type": "object", + "additionalProperties": { "type": "string" } + } + }, + "required": ["enabled"], + "anyOf": [ + { "required": ["command"] }, + { "required": ["url"] } + ], + "additionalProperties": false + } + } + }, + "required": ["enabled"], + "additionalProperties": true + } + }, + "required": ["mcp"], + "additionalProperties": true + } + }, + "required": ["tools"], + "additionalProperties": true +}` + +func loadConfig() (*config.Config, error) { + cfg, err := config.LoadConfig(internal.GetConfigPath()) + if err != nil { + return nil, fmt.Errorf("failed to load config: %w", err) + } + return cfg, nil +} + +func saveValidatedConfig(cfg *config.Config) error { + if cfg == nil { + return fmt.Errorf("config is nil") + } + + data, err := json.Marshal(cfg) + if err != nil { + return fmt.Errorf("failed to serialize config: %w", err) + } + + if err := validateConfigDocument(data); err != nil { + return err + } + + if err := config.SaveConfig(internal.GetConfigPath(), cfg); err != nil { + return fmt.Errorf("failed to save config: %w", err) + } + + return nil +} + +func validateConfigDocument(data []byte) error { + var instance map[string]any + if err := json.Unmarshal(data, &instance); err != nil { + return fmt.Errorf("failed to decode serialized config: %w", err) + } + + schema, err := loadMCPConfigSchema() + if err != nil { + return fmt.Errorf("failed to load MCP config schema: %w", err) + } + + if err := schema.Validate(instance); err != nil { + return fmt.Errorf("config validation failed: %w", err) + } + + return nil +} + +func loadMCPConfigSchema() (*jsonschema.Resolved, error) { + mcpConfigSchemaOnce.Do(func() { + var schema jsonschema.Schema + if err := json.Unmarshal([]byte(mcpConfigSchemaJSON), &schema); err != nil { + errMcpConfigSchema = err + return + } + mcpConfigSchema, errMcpConfigSchema = schema.Resolve(nil) + }) + + return mcpConfigSchema, errMcpConfigSchema +} + +func inferTransportType(server config.MCPServerConfig) string { + switch server.Type { + case "stdio", "http", "sse": + return server.Type + } + if server.URL != "" { + return "sse" + } + if server.Command != "" { + return "stdio" + } + return "unknown" +} + +func renderServerTarget(server config.MCPServerConfig) string { + transport := inferTransportType(server) + if transport == "http" || transport == "sse" { + if server.URL == "" { + return "" + } + return server.URL + } + + parts := append([]string{server.Command}, server.Args...) + rendered := strings.TrimSpace(strings.Join(parts, " ")) + if rendered == "" { + return "" + } + return rendered +} + +func sortedServerNames(servers map[string]config.MCPServerConfig) []string { + names := make([]string, 0, len(servers)) + for name := range servers { + names = append(names, name) + } + sort.Strings(names) + return names +} + +func parseEnvAssignments(values []string) (map[string]string, error) { + if len(values) == 0 { + return nil, nil + } + + env := make(map[string]string, len(values)) + for _, entry := range values { + key, value, found := strings.Cut(entry, "=") + if !found { + return nil, fmt.Errorf("invalid env assignment %q: expected KEY=value", entry) + } + key = strings.TrimSpace(key) + if key == "" { + return nil, fmt.Errorf("invalid env assignment %q: key cannot be empty", entry) + } + env[key] = value + } + + return env, nil +} + +func parseHeaderAssignments(values []string) (map[string]string, error) { + if len(values) == 0 { + return nil, nil + } + + headers := make(map[string]string, len(values)) + for _, entry := range values { + key, value, found := strings.Cut(entry, ":") + if !found { + key, value, found = strings.Cut(entry, "=") + } + if !found { + return nil, fmt.Errorf("invalid header %q: expected 'Name: Value' or 'Name=Value'", entry) + } + key = strings.TrimSpace(key) + value = strings.TrimSpace(value) + if key == "" { + return nil, fmt.Errorf("invalid header %q: name cannot be empty", entry) + } + headers[key] = value + } + + return headers, nil +} + +func looksLikeRemoteURL(target string) bool { + parsedURL, err := url.ParseRequestURI(target) + if err != nil { + return false + } + if parsedURL.Host == "" { + return false + } + switch strings.ToLower(parsedURL.Scheme) { + case "http", "https": + return true + default: + return false + } +} + +func isLocalCommandPath(command string) bool { + if command == "" { + return false + } + if looksLikeRemoteURL(command) { + return false + } + return filepath.IsAbs(command) || + filepath.VolumeName(command) != "" || + strings.HasPrefix(command, "."+string(os.PathSeparator)) || + strings.HasPrefix(command, ".."+string(os.PathSeparator)) || + command == "." || + command == ".." || + strings.ContainsRune(command, os.PathSeparator) +} + +func expandHomePath(path string) string { + if path == "" || path[0] != '~' { + return path + } + home, err := os.UserHomeDir() + if err != nil { + return path + } + if path == "~" { + return home + } + if strings.HasPrefix(path, "~/") || strings.HasPrefix(path, "~\\") { + return filepath.Join(home, path[2:]) + } + return path +} + +func validateLocalCommandPath(command string) error { + if !isLocalCommandPath(command) { + return nil + } + + path := expandHomePath(command) + info, err := os.Stat(path) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("local command %q does not exist", command) + } + return fmt.Errorf("failed to stat local command %q: %w", command, err) + } + if info.IsDir() { + return fmt.Errorf("local command %q is a directory", command) + } + if runtime.GOOS != "windows" && info.Mode()&0o111 == 0 { + return fmt.Errorf("local command %q is not executable", command) + } + return nil +} + +func defaultServerProbe( + ctx context.Context, + name string, + server config.MCPServerConfig, + workspacePath string, +) (probeResult, error) { + mgr := picomcp.NewManager() + defer func() { _ = mgr.Close() }() + + server.Enabled = true + mcpCfg := config.MCPConfig{ + ToolConfig: config.ToolConfig{Enabled: true}, + Servers: map[string]config.MCPServerConfig{ + name: server, + }, + } + + if err := mgr.LoadFromMCPConfig(ctx, mcpCfg, workspacePath); err != nil { + return probeResult{}, err + } + + conn, ok := mgr.GetServer(name) + if !ok { + return probeResult{}, fmt.Errorf("server %q did not register a connection", name) + } + + return probeResult{ToolCount: len(conn.Tools)}, nil +} + +func confirmOverwrite(r io.Reader, w io.Writer, name string) (bool, error) { + if _, err := fmt.Fprintf(w, "MCP server %q already exists. Overwrite? [y/N]: ", name); err != nil { + return false, err + } + + var answer string + if _, err := fmt.Fscanln(r, &answer); err != nil { + if errors.Is(err, io.EOF) { + return false, nil + } + return false, err + } + + answer = strings.TrimSpace(strings.ToLower(answer)) + return answer == "y" || answer == "yes", nil +} diff --git a/cmd/picoclaw/internal/mcp/list.go b/cmd/picoclaw/internal/mcp/list.go new file mode 100644 index 000000000..f95fcf65d --- /dev/null +++ b/cmd/picoclaw/internal/mcp/list.go @@ -0,0 +1,78 @@ +package mcp + +import ( + "context" + "fmt" + "time" + + "github.com/spf13/cobra" + + "github.com/sipeed/picoclaw/cmd/picoclaw/internal/cliui" +) + +func newListCommand() *cobra.Command { + var ( + includeStatus bool + timeout time.Duration + ) + + cmd := &cobra.Command{ + Use: "list", + Short: "List configured MCP servers", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + cfg, err := loadConfig() + if err != nil { + return err + } + + if len(cfg.Tools.MCP.Servers) == 0 { + fmt.Fprintln(cmd.OutOrStdout(), "No MCP servers configured.") + return nil + } + + rows := make([]cliui.MCPListRow, 0, len(cfg.Tools.MCP.Servers)) + for _, name := range sortedServerNames(cfg.Tools.MCP.Servers) { + server := cfg.Tools.MCP.Servers[name] + status := "disabled" + if server.Enabled { + status = "enabled" + } + + if includeStatus && server.Enabled { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + result, probeErr := serverProbe(ctx, name, server, cfg.WorkspacePath()) + cancel() + if probeErr != nil { + status = "error" + } else { + status = fmt.Sprintf("ok (%d tools)", result.ToolCount) + } + } + + effectiveDeferred := cfg.Tools.MCP.Discovery.Enabled + deferredExplicit := server.Deferred != nil + if deferredExplicit { + effectiveDeferred = *server.Deferred + } + + rows = append(rows, cliui.MCPListRow{ + Name: name, + Type: inferTransportType(server), + Target: renderServerTarget(server), + Status: status, + EffectiveDeferred: effectiveDeferred, + DeferredExplicit: deferredExplicit, + }) + } + + cliui.PrintMCPList(cmd.OutOrStdout(), rows) + return nil + }, + } + + cmd.Flags().BoolVar(&includeStatus, "status", false, "Ping enabled servers and show live status") + cmd.Flags().DurationVar(&timeout, "timeout", 5*time.Second, "Timeout for each live status check") + + return cmd +} diff --git a/cmd/picoclaw/internal/mcp/remove.go b/cmd/picoclaw/internal/mcp/remove.go new file mode 100644 index 000000000..d82af941d --- /dev/null +++ b/cmd/picoclaw/internal/mcp/remove.go @@ -0,0 +1,39 @@ +package mcp + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +func newRemoveCommand() *cobra.Command { + return &cobra.Command{ + Use: "remove ", + Short: "Remove an MCP server from config", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := loadConfig() + if err != nil { + return err + } + + name := args[0] + if _, exists := cfg.Tools.MCP.Servers[name]; !exists { + return fmt.Errorf("MCP server %q not found", name) + } + + delete(cfg.Tools.MCP.Servers, name) + if len(cfg.Tools.MCP.Servers) == 0 { + cfg.Tools.MCP.Servers = nil + cfg.Tools.MCP.Enabled = false + } + + if err := saveValidatedConfig(cfg); err != nil { + return err + } + + fmt.Fprintf(cmd.OutOrStdout(), "✓ MCP server %q removed.\n", name) + return nil + }, + } +} diff --git a/cmd/picoclaw/internal/mcp/show.go b/cmd/picoclaw/internal/mcp/show.go new file mode 100644 index 000000000..65953c2da --- /dev/null +++ b/cmd/picoclaw/internal/mcp/show.go @@ -0,0 +1,237 @@ +package mcp + +import ( + "context" + "encoding/json" + "fmt" + "sort" + "strings" + "time" + + "github.com/spf13/cobra" + + "github.com/sipeed/picoclaw/cmd/picoclaw/internal/cliui" + "github.com/sipeed/picoclaw/pkg/config" + picomcp "github.com/sipeed/picoclaw/pkg/mcp" +) + +type toolDetail struct { + Name string + Description string + Parameters []paramDetail +} + +type paramDetail struct { + Name string + Type string + Description string + Required bool +} + +var serverShowProbe = defaultServerShowProbe + +func defaultServerShowProbe( + ctx context.Context, + name string, + server config.MCPServerConfig, + workspacePath string, +) ([]toolDetail, error) { + mgr := picomcp.NewManager() + defer func() { _ = mgr.Close() }() + + server.Enabled = true + mcpCfg := config.MCPConfig{ + ToolConfig: config.ToolConfig{Enabled: true}, + Servers: map[string]config.MCPServerConfig{ + name: server, + }, + } + + if err := mgr.LoadFromMCPConfig(ctx, mcpCfg, workspacePath); err != nil { + return nil, err + } + + conn, ok := mgr.GetServer(name) + if !ok { + return nil, fmt.Errorf("server %q did not register a connection", name) + } + + details := make([]toolDetail, 0, len(conn.Tools)) + for _, tool := range conn.Tools { + details = append(details, toolDetail{ + Name: tool.Name, + Description: tool.Description, + Parameters: extractParameters(tool.InputSchema), + }) + } + return details, nil +} + +func extractParameters(schema any) []paramDetail { + schemaMap := normalizeSchema(schema) + properties, ok := schemaMap["properties"].(map[string]any) + if !ok || len(properties) == 0 { + return nil + } + + required := make(map[string]struct{}) + switch raw := schemaMap["required"].(type) { + case []string: + for _, name := range raw { + required[name] = struct{}{} + } + case []any: + for _, value := range raw { + if name, ok := value.(string); ok { + required[name] = struct{}{} + } + } + } + + names := make([]string, 0, len(properties)) + for name := range properties { + names = append(names, name) + } + sort.Strings(names) + + params := make([]paramDetail, 0, len(names)) + for _, name := range names { + param := paramDetail{Name: name} + if propMap, ok := properties[name].(map[string]any); ok { + if typeName, ok := propMap["type"].(string); ok { + param.Type = strings.TrimSpace(typeName) + } + if desc, ok := propMap["description"].(string); ok { + param.Description = strings.TrimSpace(desc) + } + } + _, param.Required = required[name] + params = append(params, param) + } + return params +} + +func normalizeSchema(schema any) map[string]any { + if schema == nil { + return map[string]any{} + } + if schemaMap, ok := schema.(map[string]any); ok { + return schemaMap + } + + var jsonData []byte + switch raw := schema.(type) { + case json.RawMessage: + jsonData = raw + case []byte: + jsonData = raw + default: + var err error + jsonData, err = json.Marshal(schema) + if err != nil { + return map[string]any{} + } + } + + var result map[string]any + if err := json.Unmarshal(jsonData, &result); err != nil { + return map[string]any{} + } + return result +} + +func newShowCommand() *cobra.Command { + var timeout time.Duration + + cmd := &cobra.Command{ + Use: "show ", + Short: "Show details and tools for a configured MCP server", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := loadConfig() + if err != nil { + return err + } + + name := args[0] + server, exists := cfg.Tools.MCP.Servers[name] + if !exists { + return fmt.Errorf("MCP server %q not found", name) + } + + serverInfo := buildServerInfo(name, server, cfg.Tools.MCP.Discovery.Enabled) + + if !server.Enabled { + cliui.PrintMCPShow(cmd.OutOrStdout(), serverInfo, nil, true) + return nil + } + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + details, err := serverShowProbe(ctx, name, server, cfg.WorkspacePath()) + if err != nil { + return fmt.Errorf("failed to connect to MCP server %q: %w", name, err) + } + + tools := make([]cliui.MCPShowTool, 0, len(details)) + for _, d := range details { + params := make([]cliui.MCPShowParam, 0, len(d.Parameters)) + for _, p := range d.Parameters { + params = append(params, cliui.MCPShowParam{ + Name: p.Name, + Type: p.Type, + Description: p.Description, + Required: p.Required, + }) + } + tools = append(tools, cliui.MCPShowTool{ + Name: d.Name, + Description: d.Description, + Parameters: params, + }) + } + + cliui.PrintMCPShow(cmd.OutOrStdout(), serverInfo, tools, false) + return nil + }, + } + + cmd.Flags().DurationVar(&timeout, "timeout", 10*time.Second, "Connection timeout") + + return cmd +} + +func buildServerInfo(name string, server config.MCPServerConfig, discoveryEnabled bool) cliui.MCPShowServer { + effectiveDeferred := discoveryEnabled + deferredExplicit := server.Deferred != nil + if deferredExplicit { + effectiveDeferred = *server.Deferred + } + info := cliui.MCPShowServer{ + Name: name, + Type: inferTransportType(server), + Target: renderServerTarget(server), + Enabled: server.Enabled, + EffectiveDeferred: effectiveDeferred, + DeferredExplicit: deferredExplicit, + EnvFile: server.EnvFile, + } + if len(server.Env) > 0 { + keys := make([]string, 0, len(server.Env)) + for k := range server.Env { + keys = append(keys, k) + } + sort.Strings(keys) + info.EnvKeys = keys + } + if len(server.Headers) > 0 { + keys := make([]string, 0, len(server.Headers)) + for k := range server.Headers { + keys = append(keys, k) + } + sort.Strings(keys) + info.Headers = keys + } + return info +} diff --git a/cmd/picoclaw/internal/mcp/test.go b/cmd/picoclaw/internal/mcp/test.go new file mode 100644 index 000000000..101cfee65 --- /dev/null +++ b/cmd/picoclaw/internal/mcp/test.go @@ -0,0 +1,46 @@ +package mcp + +import ( + "context" + "fmt" + "time" + + "github.com/spf13/cobra" +) + +func newTestCommand() *cobra.Command { + var timeout time.Duration + + cmd := &cobra.Command{ + Use: "test ", + Short: "Test connectivity for a configured MCP server", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := loadConfig() + if err != nil { + return err + } + + name := args[0] + server, exists := cfg.Tools.MCP.Servers[name] + if !exists { + return fmt.Errorf("MCP server %q not found", name) + } + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + result, err := serverProbe(ctx, name, server, cfg.WorkspacePath()) + if err != nil { + return fmt.Errorf("failed to reach MCP server %q: %w", name, err) + } + + fmt.Fprintf(cmd.OutOrStdout(), "✓ MCP server %q reachable (%d tools).\n", name, result.ToolCount) + return nil + }, + } + + cmd.Flags().DurationVar(&timeout, "timeout", 5*time.Second, "Connection timeout") + + return cmd +} diff --git a/cmd/picoclaw/main.go b/cmd/picoclaw/main.go index 0867203a6..abcf03a34 100644 --- a/cmd/picoclaw/main.go +++ b/cmd/picoclaw/main.go @@ -19,6 +19,7 @@ import ( "github.com/sipeed/picoclaw/cmd/picoclaw/internal/cliui" "github.com/sipeed/picoclaw/cmd/picoclaw/internal/cron" "github.com/sipeed/picoclaw/cmd/picoclaw/internal/gateway" + "github.com/sipeed/picoclaw/cmd/picoclaw/internal/mcp" "github.com/sipeed/picoclaw/cmd/picoclaw/internal/migrate" "github.com/sipeed/picoclaw/cmd/picoclaw/internal/model" "github.com/sipeed/picoclaw/cmd/picoclaw/internal/onboard" @@ -87,6 +88,7 @@ picoclaw --no-color status`, gateway.NewGatewayCommand(), status.NewStatusCommand(), cron.NewCronCommand(), + mcp.NewMCPCommand(), migrate.NewMigrateCommand(), skills.NewSkillsCommand(), model.NewModelCommand(), diff --git a/cmd/picoclaw/main_test.go b/cmd/picoclaw/main_test.go index 309e60ba9..037c7c2e6 100644 --- a/cmd/picoclaw/main_test.go +++ b/cmd/picoclaw/main_test.go @@ -41,6 +41,7 @@ func TestNewPicoclawCommand(t *testing.T) { "auth", "cron", "gateway", + "mcp", "migrate", "model", "onboard", diff --git a/docs/project/README.it.md b/docs/project/README.it.md index eb2f7c95b..cd7ecac7d 100644 --- a/docs/project/README.it.md +++ b/docs/project/README.it.md @@ -554,7 +554,19 @@ PicoClaw supporta nativamente [MCP](https://modelcontextprotocol.io/) — connet } ``` -Per la configurazione MCP completa (trasporti stdio, SSE, HTTP, Tool Discovery), vedi [Configurazione degli Strumenti - MCP](../reference/tools_configuration.md#mcp-tool). +Puoi gestire i casi MCP più comuni direttamente dalla CLI senza modificare a mano il JSON: + +```bash +picoclaw mcp add filesystem -- npx -y @modelcontextprotocol/server-filesystem /tmp +picoclaw mcp list +picoclaw mcp test filesystem +``` + +`picoclaw mcp` agisce come configuration manager: aggiorna `config.json` sotto `tools.mcp.servers`, ma non mantiene in esecuzione il processo del server. + +Usa `picoclaw mcp edit` quando ti servono campi avanzati come `headers`, `env_file` o `deferred`. + +Per la configurazione MCP completa (trasporti stdio, SSE, HTTP, Tool Discovery), vedi [Configurazione degli Strumenti - MCP](../reference/tools_configuration.md#mcp-tool). Per la reference della CLI, vedi [MCP Server CLI](../reference/mcp-cli.md). ## ClawdChat Unisciti al Social Network degli Agent @@ -574,6 +586,11 @@ Connetti PicoClaw al Social Network degli Agent semplicemente inviando un singol | `picoclaw status` | Mostra lo stato | | `picoclaw version` | Mostra le info sulla versione | | `picoclaw model` | Visualizza o cambia il modello predefinito | +| `picoclaw mcp list` | Elenca i server MCP configurati | +| `picoclaw mcp add ...` | Aggiunge o aggiorna un server MCP | +| `picoclaw mcp test` | Verifica la raggiungibilità di un server MCP | +| `picoclaw mcp edit` | Apre la config per modifiche MCP avanzate | +| `picoclaw mcp remove` | Rimuove un server MCP dalla config | | `picoclaw cron list` | Elenca tutti i job pianificati | | `picoclaw cron add ...` | Aggiunge un job pianificato | | `picoclaw cron disable` | Disabilita un job pianificato | @@ -600,6 +617,7 @@ Per guide dettagliate oltre questo README: | [Docker & Avvio Rapido](../guides/docker.md) | Configurazione Docker Compose, modalità Launcher/Agent | | [App di Chat](../guides/chat-apps.md) | Tutte le guide di configurazione per 17+ channel | | [Configurazione](../guides/configuration.md) | Variabili d'ambiente, struttura del workspace, sandbox di sicurezza | +| [MCP Server CLI](../reference/mcp-cli.md) | Aggiunta, elenco, test, modifica e rimozione dei server MCP da CLI | | [Provider & Modelli](../guides/providers.md) | 30+ provider LLM, routing dei modelli, configurazione model_list | | [Spawn & Task Asincroni](../guides/spawn-tasks.md) | Task veloci, task lunghi con spawn, orchestrazione asincrona di sub-agent | | [Hooks](../architecture/hooks/README.md) | Sistema di hook event-driven: observer, interceptor, approval hook | diff --git a/docs/reference/README.md b/docs/reference/README.md index eec5c09b4..2e0f53cf7 100644 --- a/docs/reference/README.md +++ b/docs/reference/README.md @@ -3,6 +3,7 @@ Reference docs for precise configuration, runtime behavior, and tool semantics. - [Tools Configuration](tools_configuration.md): per-tool configuration, execution policies, MCP, and Skills. +- [MCP Server CLI](mcp-cli.md): add, list, test, edit, and remove MCP server entries from the command line. - [Scheduled Tasks and Cron Jobs](cron.md): schedule types, delivery modes, command gates, and storage. - [Config Schema Versioning Guide](config-versioning.md): config schema migration and compatibility notes. - [Dynamic Rate Limiting](rate-limiting.md): request throttling behavior for LLM providers. diff --git a/docs/reference/mcp-cli.md b/docs/reference/mcp-cli.md new file mode 100644 index 000000000..77291b140 --- /dev/null +++ b/docs/reference/mcp-cli.md @@ -0,0 +1,349 @@ +# MCP Server CLI + +> Back to [README](../README.md) + +PicoClaw includes an `mcp` CLI command group for managing MCP server entries in `config.json`. + +This CLI acts as a **configuration manager**: + +- it adds, updates, removes, and validates entries under `tools.mcp.servers` +- it does **not** keep MCP servers running itself +- the gateway / host still starts the configured servers when MCP is enabled + +## Where It Writes + +The CLI updates the same config file used by the rest of PicoClaw: + +- `PICOCLAW_CONFIG` if set +- otherwise `~/.picoclaw/config.json` + +When the CLI writes the file, it: + +- saves atomically +- preserves the standard 2-space JSON formatting used by PicoClaw +- validates the generated JSON before writing + +Behavior notes: + +- `picoclaw mcp add ...` enables `tools.mcp.enabled` +- removing the last server with `picoclaw mcp remove ...` disables `tools.mcp.enabled` + +## Quick Start + +Add a stdio server via `npx`: + +```bash +picoclaw mcp add filesystem -- npx -y @modelcontextprotocol/server-filesystem /tmp +``` + +Add a stdio server with environment variables: + +```bash +picoclaw mcp add github --env GITHUB_PERSONAL_ACCESS_TOKEN=ghp_xxx -- npx -y @modelcontextprotocol/server-github +``` + +Add a remote HTTP server: + +```bash +picoclaw mcp add context7 --transport http https://mcp.context7.com/mcp +``` + +Add a remote HTTP server with auth header, even with flags after the URL: + +```bash +picoclaw mcp add apify "https://mcp.apify.com/" -t http --header "Authorization: Bearer OMITTED" +``` + +Add a stdio server using an explicit command separator: + +```bash +picoclaw mcp add --transport stdio --env AIRTABLE_API_KEY=YOUR_KEY airtable -- npx -y airtable-mcp-server +``` + +Inspect the configured entries: + +```bash +picoclaw mcp list +picoclaw mcp list --status +``` + +Inspect one server's full details and its exposed tools: + +```bash +picoclaw mcp show filesystem +``` + +Probe a single server entry: + +```bash +picoclaw mcp test filesystem +``` + +Open the raw config for advanced editing: + +```bash +picoclaw mcp edit +``` + +## Command Summary + +| Command | Purpose | +|---------|---------| +| `picoclaw mcp add [flags] [args...]` | Add or update an MCP server entry | +| `picoclaw mcp remove ` | Remove a server entry from config | +| `picoclaw mcp list` | List configured MCP servers | +| `picoclaw mcp show ` | Show full details and tools for one server | +| `picoclaw mcp test ` | Try connecting to one configured server | +| `picoclaw mcp edit` | Open `config.json` in `$EDITOR` | + +## `picoclaw mcp add` + +Syntax: + +```bash +picoclaw mcp add [flags] [args...] +``` + +Supported flags: + +| Flag | Meaning | +|------|---------| +| `--env`, `-e` | Add a stdio environment variable in `KEY=value` format. Repeatable. | +| `--header`, `-H` | Add an HTTP header in `Name: Value` or `Name=Value` format. Repeatable. | +| `--transport`, `-t` | Transport type: `stdio` (default), `http`, or `sse`. | +| `--force`, `-f` | Overwrite an existing server entry without confirmation. | +| `--deferred` | Mark the server as deferred: tools are hidden and discoverable on demand. | +| `--no-deferred` | Mark the server as non-deferred: tools are always loaded into context. | + +When neither `--deferred` nor `--no-deferred` is passed, the `deferred` field is omitted from the stored config and the global `discovery.enabled` value applies at runtime. + +Supported forms: + +```bash +picoclaw mcp add [flags] [args...] +picoclaw mcp add [flags] -- [args...] +``` + +Parsing behavior: + +- CLI flags can appear before the name, between the name and target, or after the URL for remote transports +- for `stdio`, the most robust form is `-- [args...]` +- use the `--` separator when the stdio command itself has arguments that may look like PicoClaw CLI flags +- without `--`, PicoClaw treats the first two non-flag tokens as `` and `` + +Example: + +```bash +picoclaw mcp add sqlite npx -y @modelcontextprotocol/server-sqlite --db ./mydb.db +``` + +This stores: + +```json +{ + "tools": { + "mcp": { + "enabled": true, + "servers": { + "sqlite": { + "enabled": true, + "type": "stdio", + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-sqlite", "--db", "./mydb.db"] + } + } + } + } +} +``` + +Adding the same server with `--deferred` stores the extra field: + +```bash +picoclaw mcp add --deferred sqlite npx -y @modelcontextprotocol/server-sqlite --db ./mydb.db +``` + +```json +{ + "sqlite": { + "enabled": true, + "type": "stdio", + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-sqlite", "--db", "./mydb.db"], + "deferred": true + } +} +``` + +### Add Command Rules + +For `stdio`: + +- `` is treated as the command +- `[args...]` are stored in `args` +- `--env` is supported +- `--header` is rejected +- `-- [args...]` is supported and recommended for unambiguous parsing + +For `http` / `sse`: + +- `` must be a valid URL +- extra command args are rejected +- `--env` is rejected +- `--header` is supported and stored in `headers` + +Overwrite behavior: + +- if `` already exists, PicoClaw asks for confirmation +- use `--force` to skip the prompt + +Local path validation: + +- if the command looks like a local path such as `./server.py` or `/opt/mcp/server` +- PicoClaw checks that the file exists +- on non-Windows platforms, it also checks that the file is executable + +Clear URL/transport error: + +- if the target looks like `https://...` but transport is still `stdio`, PicoClaw returns an explicit error telling you to use `--transport http` or `--transport sse` + +## `picoclaw mcp remove` + +Syntax: + +```bash +picoclaw mcp remove +``` + +This removes the named entry from `tools.mcp.servers`. + +If the removed server was the last configured MCP server, PicoClaw also disables `tools.mcp.enabled`. + +## `picoclaw mcp list` + +Syntax: + +```bash +picoclaw mcp list +picoclaw mcp list --status +``` + +On wide terminals the output is a styled box (same look as `mcp show`). On narrow terminals or when stdout is not a TTY, a plain ASCII table is printed instead. + +Output fields: + +| Field | Meaning | +|-------|---------| +| `Name` | Server key inside `tools.mcp.servers` | +| `Type` | Effective transport: `stdio`, `http`, or `sse` | +| `Command` / `Target` | Stored command line for stdio servers, or URL for remote servers | +| `Status` | `enabled` / `disabled` by default; with `--status`: `ok (N tools)` or `error` | +| `Deferred` | `deferred` if the per-server override is `true`; `eager` if `false`; omitted if not set | + +Notes: + +- without `--status`, PicoClaw prints configuration state only +- with `--status`, PicoClaw tries to connect to each enabled server and reports `ok (N tools)` or `error` +- to see the full list of tools a server exposes, use `picoclaw mcp show ` + +## `picoclaw mcp show` + +Syntax: + +```bash +picoclaw mcp show +picoclaw mcp show --timeout 15s +``` + +This connects to the named server and prints: + +- server metadata: name, transport type, target, enabled state, deferred override, env var names, env file, header names +- every tool the server exposes, with its name, description, and parameters (name, type, required/optional, description) + +On wide terminals the output is a styled box matching the `mcp list` look. On narrow terminals or non-TTY stdout, plain text is printed instead. + +Example output (wide terminal): + +``` +╭──────────────────────────────────────────────────────────╮ +│ ⬡ filesystem │ +│ │ +│ Type stdio │ +│ Target npx -y @modelcontextprotocol/server-fs /tmp │ +│ Enabled yes │ +│ Deferred no │ +│ │ +│ Tools (3) │ +│ │ +│ read_file [1/3] │ +│ Read the complete contents of a file from the disk │ +│ │ +│ path required │ +│ Path to the file to read │ +│ ──────────────────────────────────────────────────────── │ +│ ... │ +╰──────────────────────────────────────────────────────────╯ +``` + +Flags: + +| Flag | Default | Meaning | +|------|---------|---------| +| `--timeout` | `10s` | Connection timeout | + +Notes: + +- if the server is disabled in config, `mcp show` prints the metadata only and skips tool discovery +- `mcp show` always connects live to fetch the tool list; use `mcp test` if you only need a reachability check + +## `picoclaw mcp test` + +Syntax: + +```bash +picoclaw mcp test +``` + +This performs a direct connection test for one configured entry and prints the number of discovered tools when successful. + +It is useful when: + +- you want to verify a newly added server before starting the gateway +- you want to debug one server without probing the whole list +- the entry is currently disabled in config but you still want to validate its definition + +## `picoclaw mcp edit` + +Syntax: + +```bash +picoclaw mcp edit +``` + +This opens the config file in the editor pointed to by `$EDITOR`. + +Use it when you need to configure MCP fields that are not exposed directly by `picoclaw mcp add`, such as: + +- `env_file` + +If `$EDITOR` is not set, the command fails with an explicit error. + +## Recommended Workflow + +For common cases: + +1. Add the server with `picoclaw mcp add` (include `--deferred` if you want tools hidden by default). +2. Verify connectivity and inspect the exposed tools with `picoclaw mcp show `. +3. Check all servers at a glance with `picoclaw mcp list --status`. +4. Start PicoClaw normally so the configured MCP server is loaded by the host. + +For advanced cases (e.g. `env_file`): + +1. Add the base entry with `picoclaw mcp add`. +2. Run `picoclaw mcp edit` to fill in `env_file` or other fields not exposed as CLI flags. +3. Run `picoclaw mcp show ` to confirm the final configuration and tool list. + +## Related Docs + +- [Tools Configuration](tools_configuration.md#mcp-tool): MCP config structure, transports, discovery, and examples +- [README](../README.md): high-level overview diff --git a/docs/reference/tools_configuration.md b/docs/reference/tools_configuration.md index fa33f0bb4..810d91ef2 100644 --- a/docs/reference/tools_configuration.md +++ b/docs/reference/tools_configuration.md @@ -258,6 +258,17 @@ For schedule types, execution modes (`deliver`, agent turn, and command jobs), p The MCP tool enables integration with external Model Context Protocol servers. +If you prefer not to edit JSON manually, PicoClaw also provides an MCP configuration manager CLI: + +- `picoclaw mcp add` — add or update a server (supports `--deferred` / `--no-deferred`) +- `picoclaw mcp list` — list all configured servers with status and deferred state +- `picoclaw mcp show ` — show full details and the tool list for one server +- `picoclaw mcp test ` — connectivity check for one server +- `picoclaw mcp remove ` — remove a server entry +- `picoclaw mcp edit` — open `config.json` in `$EDITOR` for advanced edits + +These commands manage the same `tools.mcp.servers` section documented below. See [MCP Server CLI](mcp-cli.md) for command syntax, examples, and behavior details. + ### Tool Discovery (Lazy Loading) When connecting to multiple MCP servers, exposing hundreds of tools simultaneously can exhaust the LLM's context window From 9fba52d0fa8f6510187085be0654286dd25ab95c Mon Sep 17 00:00:00 2001 From: BeaconCat <111232138+BeaconCat@users.noreply.github.com> Date: Fri, 24 Apr 2026 09:53:07 +0800 Subject: [PATCH 04/36] ci: parallel macOS CGO launcher build, lowercase Docker tags, conditional Docker Hub login (#2643) - Add build-macos-launcher job (runs on macOS, parallel with GoReleaser) that builds the CGO-enabled launcher with systray support, signs and notarizes it via rcodesign, then uploads as artifact. - Add patch-macos-archives job (runs on cheap Linux runner, needs both GoReleaser and build-macos-launcher) that downloads the launcher artifact and darwin release archives, replaces the launcher binary, and re-uploads the patched archives. - Fix Docker image tag errors: GITHUB_REPOSITORY_OWNER is immutable in GitHub Actions. Introduce REPO_OWNER (lowercase) in workflows and reference it in .goreleaser.yaml for GHCR image names and nfpms homepage. - Make Docker Hub login conditional on DOCKERHUB_USERNAME secret being set, so forks without Docker Hub credentials don't fail. - Make Docker Hub image in goreleaser conditional on DOCKERHUB_IMAGE_NAME being non-empty (empty image names are ignored by GoReleaser). Verified on fork: both nightly and release workflows pass all jobs. Nightly: https://github.com/BeaconCat/picoclaw/actions/runs/24848808843 Release: https://github.com/BeaconCat/picoclaw/actions/runs/24849753787 Co-authored-by: BeaconCat --- .github/workflows/nightly.yml | 160 +++++++++++++++++++++++++++++++++- .github/workflows/release.yml | 151 +++++++++++++++++++++++++++++++- .goreleaser.yaml | 10 +-- 3 files changed, 313 insertions(+), 8 deletions(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 39ad8810e..d507234dc 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -74,7 +74,10 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Login to Docker Hub + if: env.DOCKERHUB_USERNAME != '' uses: docker/login-action@v4 + env: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} with: registry: docker.io username: ${{ secrets.DOCKERHUB_USERNAME }} @@ -86,6 +89,10 @@ jobs: - name: Create local tag for GoReleaser run: git tag "${{ steps.version.outputs.version }}" + - name: Lowercase owner for Docker tags + id: repo + run: echo "owner=$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT" + - name: Run GoReleaser uses: goreleaser/goreleaser-action@v7 with: @@ -94,7 +101,7 @@ jobs: args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }} + REPO_OWNER: ${{ steps.repo.outputs.owner }} DOCKERHUB_IMAGE_NAME: ${{ vars.DOCKERHUB_REPOSITORY }} GOVERSION: ${{ steps.setup-go.outputs.go-version }} GORELEASER_CURRENT_TAG: ${{ steps.version.outputs.version }} @@ -144,3 +151,154 @@ jobs: --prerelease \ --latest=false \ "${ASSETS[@]}" + + build-macos-launcher: + name: Build macOS Launcher (${{ matrix.arch_name }}) + runs-on: macos-latest + permissions: + contents: read + strategy: + matrix: + include: + - goarch: arm64 + arch_name: arm64 + - goarch: amd64 + arch_name: x86_64 + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup Go from go.mod + uses: actions/setup-go@v6 + with: + go-version-file: go.mod + + - name: Setup pnpm + uses: pnpm/action-setup@v6 + with: + version: 10.33.0 + run_install: false + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 22 + cache: pnpm + cache-dependency-path: web/frontend/pnpm-lock.yaml + + - name: Build frontend + run: | + cd web/frontend + CI=true pnpm install --frozen-lockfile + pnpm build:backend + + - name: Compute version + id: version + run: | + DATE=$(date -u +%Y%m%d) + SHA=$(git rev-parse --short=8 HEAD) + BASE_VERSION=$(git describe --tags --match "v*" --exclude "*nightly*" --abbrev=0 2>/dev/null || true) + if [ -z "$BASE_VERSION" ] || [ "$BASE_VERSION" = "v0.0.0" ]; then + VERSION="v0.0.0-nightly.${DATE}.${SHA}" + else + VERSION="${BASE_VERSION}-nightly.${DATE}.${SHA}" + fi + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + + - name: Build picoclaw-launcher with CGO + env: + CGO_ENABLED: "1" + GOOS: darwin + GOARCH: ${{ matrix.goarch }} + run: | + SDK_PATH=$(xcrun --show-sdk-path) + export CGO_CFLAGS="-isysroot ${SDK_PATH} -mmacosx-version-min=11.0" + export CGO_LDFLAGS="-isysroot ${SDK_PATH}" + + go generate ./... + go build -tags "goolm,stdjson" \ + -ldflags "-s -w \ + -X github.com/sipeed/picoclaw/pkg/config.Version=${{ steps.version.outputs.version }} \ + -X github.com/sipeed/picoclaw/pkg/config.GitCommit=$(git rev-parse --short HEAD) \ + -X github.com/sipeed/picoclaw/pkg/config.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + -o picoclaw-launcher-cgo \ + ./web/backend + + - name: Sign and notarize launcher binary + if: env.MACOS_SIGN_P12 != '' + env: + MACOS_SIGN_P12: ${{ secrets.MACOS_SIGN_P12 }} + MACOS_SIGN_PASSWORD: ${{ secrets.MACOS_SIGN_PASSWORD }} + MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }} + MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }} + MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }} + run: | + pip3 install rcodesign + + echo "$MACOS_SIGN_P12" | base64 -d > cert.p12 + + rcodesign sign \ + --p12-file cert.p12 \ + --p12-password "$MACOS_SIGN_PASSWORD" \ + picoclaw-launcher-cgo + + echo "$MACOS_NOTARY_KEY" > notary-key.p8 + + rcodesign notary-submit \ + --api-key-path notary-key.p8 \ + --api-issuer "$MACOS_NOTARY_ISSUER_ID" \ + --wait \ + picoclaw-launcher-cgo + + rm -f cert.p12 notary-key.p8 + + - name: Upload launcher artifact + uses: actions/upload-artifact@v4 + with: + name: macos-launcher-${{ matrix.arch_name }} + path: picoclaw-launcher-cgo + retention-days: 1 + + patch-macos-archives: + name: Patch macOS Archives + needs: [nightly, build-macos-launcher] + runs-on: ubuntu-latest + permissions: + contents: write + strategy: + matrix: + include: + - arch_name: arm64 + - arch_name: x86_64 + steps: + - name: Download launcher artifact + uses: actions/download-artifact@v4 + with: + name: macos-launcher-${{ matrix.arch_name }} + + - name: Patch darwin release archive + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + ARCHIVE_NAME="picoclaw_Darwin_${{ matrix.arch_name }}.tar.gz" + + gh release download nightly \ + --repo "${{ github.repository }}" \ + --pattern "${ARCHIVE_NAME}" \ + --dir ./patch-tmp + + mkdir -p ./patch-extracted + tar xzf "./patch-tmp/${ARCHIVE_NAME}" -C ./patch-extracted + + cp picoclaw-launcher-cgo ./patch-extracted/picoclaw-launcher + chmod +x ./patch-extracted/picoclaw-launcher + + tar czf "${ARCHIVE_NAME}" -C ./patch-extracted . + + gh release upload nightly \ + --repo "${{ github.repository }}" \ + "${ARCHIVE_NAME}" --clobber + + echo "✅ Patched ${ARCHIVE_NAME} with CGO launcher (systray enabled)" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a52b6df8f..9aa054943 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -80,7 +80,10 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Login to Docker Hub + if: env.DOCKERHUB_USERNAME != '' uses: docker/login-action@v4 + env: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} with: registry: docker.io username: ${{ secrets.DOCKERHUB_USERNAME }} @@ -89,6 +92,10 @@ jobs: - name: Install zip run: sudo apt-get install -y zip + - name: Lowercase owner for Docker tags + id: repo + run: echo "owner=$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT" + - name: Run GoReleaser uses: goreleaser/goreleaser-action@v7 with: @@ -97,7 +104,7 @@ jobs: args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }} + REPO_OWNER: ${{ steps.repo.outputs.owner }} DOCKERHUB_IMAGE_NAME: ${{ vars.DOCKERHUB_REPOSITORY }} GOVERSION: ${{ steps.setup-go.outputs.go-version }} INCLUDE_ANDROID_BUNDLE: "true" @@ -116,9 +123,149 @@ jobs: --draft=${{ inputs.draft }} \ --prerelease=${{ inputs.prerelease }} + build-macos-launcher: + name: Build macOS Launcher (${{ matrix.arch_name }}) + runs-on: macos-latest + permissions: + contents: read + strategy: + matrix: + include: + - goarch: arm64 + arch_name: arm64 + - goarch: amd64 + arch_name: x86_64 + steps: + - name: Checkout tag + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ inputs.tag }} + + - name: Setup Go from go.mod + uses: actions/setup-go@v6 + with: + go-version-file: go.mod + + - name: Setup pnpm + uses: pnpm/action-setup@v6 + with: + version: 10.33.0 + run_install: false + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 22 + cache: pnpm + cache-dependency-path: web/frontend/pnpm-lock.yaml + + - name: Build frontend + run: | + cd web/frontend + CI=true pnpm install --frozen-lockfile + pnpm build:backend + + - name: Build picoclaw-launcher with CGO + env: + CGO_ENABLED: "1" + GOOS: darwin + GOARCH: ${{ matrix.goarch }} + run: | + SDK_PATH=$(xcrun --show-sdk-path) + export CGO_CFLAGS="-isysroot ${SDK_PATH} -mmacosx-version-min=11.0" + export CGO_LDFLAGS="-isysroot ${SDK_PATH}" + + go generate ./... + go build -tags "goolm,stdjson" \ + -ldflags "-s -w \ + -X github.com/sipeed/picoclaw/pkg/config.Version=${{ inputs.tag }} \ + -X github.com/sipeed/picoclaw/pkg/config.GitCommit=$(git rev-parse --short HEAD) \ + -X github.com/sipeed/picoclaw/pkg/config.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + -o picoclaw-launcher-cgo \ + ./web/backend + + - name: Sign and notarize launcher binary + if: env.MACOS_SIGN_P12 != '' + env: + MACOS_SIGN_P12: ${{ secrets.MACOS_SIGN_P12 }} + MACOS_SIGN_PASSWORD: ${{ secrets.MACOS_SIGN_PASSWORD }} + MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }} + MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }} + MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }} + run: | + pip3 install rcodesign + + echo "$MACOS_SIGN_P12" | base64 -d > cert.p12 + + rcodesign sign \ + --p12-file cert.p12 \ + --p12-password "$MACOS_SIGN_PASSWORD" \ + picoclaw-launcher-cgo + + echo "$MACOS_NOTARY_KEY" > notary-key.p8 + + rcodesign notary-submit \ + --api-key-path notary-key.p8 \ + --api-issuer "$MACOS_NOTARY_ISSUER_ID" \ + --wait \ + picoclaw-launcher-cgo + + rm -f cert.p12 notary-key.p8 + + - name: Upload launcher artifact + uses: actions/upload-artifact@v4 + with: + name: macos-launcher-${{ matrix.arch_name }} + path: picoclaw-launcher-cgo + retention-days: 1 + + patch-macos-archives: + name: Patch macOS Archives + needs: [release, build-macos-launcher] + runs-on: ubuntu-latest + permissions: + contents: write + strategy: + matrix: + include: + - arch_name: arm64 + - arch_name: x86_64 + steps: + - name: Download launcher artifact + uses: actions/download-artifact@v4 + with: + name: macos-launcher-${{ matrix.arch_name }} + + - name: Patch darwin release archive + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ inputs.tag }} + run: | + ARCHIVE_NAME="picoclaw_Darwin_${{ matrix.arch_name }}.tar.gz" + + gh release download "${TAG}" \ + --repo "${{ github.repository }}" \ + --pattern "${ARCHIVE_NAME}" \ + --dir ./patch-tmp + + mkdir -p ./patch-extracted + tar xzf "./patch-tmp/${ARCHIVE_NAME}" -C ./patch-extracted + + cp picoclaw-launcher-cgo ./patch-extracted/picoclaw-launcher + chmod +x ./patch-extracted/picoclaw-launcher + + tar czf "${ARCHIVE_NAME}" -C ./patch-extracted . + + gh release upload "${TAG}" \ + --repo "${{ github.repository }}" \ + "${ARCHIVE_NAME}" --clobber + + echo "Patched ${ARCHIVE_NAME} with CGO launcher (systray enabled)" + upload-tos: name: Upload to TOS - needs: release + needs: [release, patch-macos-archives] if: ${{ inputs.upload_tos }} uses: ./.github/workflows/upload-tos.yml with: diff --git a/.goreleaser.yaml b/.goreleaser.yaml index d8c51b069..b330c60f5 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -151,8 +151,8 @@ dockers_v2: ids: - picoclaw images: - - "ghcr.io/{{ .Env.GITHUB_REPOSITORY_OWNER }}/picoclaw" - - 'docker.io/{{ .Env.DOCKERHUB_IMAGE_NAME }}' + - "ghcr.io/{{ .Env.REPO_OWNER }}/picoclaw" + - '{{ with .Env.DOCKERHUB_IMAGE_NAME }}docker.io/{{ . }}{{ end }}' tags: - '{{ if isEnvSet "NIGHTLY_BUILD" }}nightly{{ else }}{{ .Tag }}{{ end }}' - '{{ if isEnvSet "NIGHTLY_BUILD" }}nightly{{ else }}latest{{ end }}' @@ -168,8 +168,8 @@ dockers_v2: - picoclaw-launcher - picoclaw-launcher-tui images: - - "ghcr.io/{{ .Env.GITHUB_REPOSITORY_OWNER }}/picoclaw" - - 'docker.io/{{ .Env.DOCKERHUB_IMAGE_NAME }}' + - "ghcr.io/{{ .Env.REPO_OWNER }}/picoclaw" + - '{{ with .Env.DOCKERHUB_IMAGE_NAME }}docker.io/{{ . }}{{ end }}' tags: - '{{ if isEnvSet "NIGHTLY_BUILD" }}nightly-launcher{{ else }}{{ .Tag }}-launcher{{ end }}' - '{{ if isEnvSet "NIGHTLY_BUILD" }}nightly-launcher{{ else }}launcher{{ end }}' @@ -224,7 +224,7 @@ nfpms: {{- else if eq .Arch "arm" }}armv{{ .Arm }} {{- else }}{{ .Arch }}{{ end }} vendor: picoclaw - homepage: https://github.com/{{ .Env.GITHUB_REPOSITORY_OWNER }}/picoclaw + homepage: https://github.com/{{ .Env.REPO_OWNER }}/picoclaw maintainer: picoclaw contributors description: picoclaw - a tool for managing and running tasks license: MIT From 9fc72c1fb328cf791da5ff239b45c087ea276349 Mon Sep 17 00:00:00 2001 From: Mauro Date: Fri, 24 Apr 2026 05:49:41 +0200 Subject: [PATCH 05/36] feat(tool-feedback): add separate message mode for chat feedback (#2644) * feat(tool-feedback): add separate message mode for chat feedback * add parameter in conf --- config/config.example.json | 3 +- docs/operations/debug.md | 4 +- pkg/channels/manager.go | 71 ++++++- pkg/channels/manager_test.go | 184 ++++++++++++++++++ pkg/config/config.go | 12 +- pkg/config/config_test.go | 6 + pkg/config/defaults.go | 5 +- .../src/components/config/config-page.tsx | 1 + .../src/components/config/config-sections.tsx | 12 ++ .../src/components/config/form-model.ts | 6 + web/frontend/src/i18n/locales/en.json | 2 + web/frontend/src/i18n/locales/zh.json | 2 + 12 files changed, 292 insertions(+), 16 deletions(-) diff --git a/config/config.example.json b/config/config.example.json index 858472488..30460c231 100644 --- a/config/config.example.json +++ b/config/config.example.json @@ -13,7 +13,8 @@ "split_on_marker": false, "tool_feedback": { "enabled": false, - "max_args_length": 300 + "max_args_length": 300, + "separate_messages": false } } }, diff --git a/docs/operations/debug.md b/docs/operations/debug.md index b9e776f0f..eacd72380 100644 --- a/docs/operations/debug.md +++ b/docs/operations/debug.md @@ -65,7 +65,8 @@ Debug logs are server-side only. If you want the agent to send a visible notific "defaults": { "tool_feedback": { "enabled": true, - "max_args_length": 300 + "max_args_length": 300, + "separate_messages": true } } } @@ -85,6 +86,7 @@ When `enabled` is `true`, every tool call sends a short message to the chat befo | Field | Type | Default | Description | |---|---|---|---| | `enabled` | bool | `false` | Send a chat notification for each tool call | +| `separate_messages` | bool | `false` | Keep every tool feedback update as a separate chat message instead of reusing a single placeholder/progress message | | `max_args_length` | int | `300` | Maximum characters of the serialised arguments included in the notification | ### Environment variables diff --git a/pkg/channels/manager.go b/pkg/channels/manager.go index 2ffb1bb10..7974a39e4 100644 --- a/pkg/channels/manager.go +++ b/pkg/channels/manager.go @@ -170,6 +170,20 @@ func dismissTrackedToolFeedbackMessage( } } +func clearTrackedToolFeedbackMessage( + ch Channel, + chatID string, + outboundCtx *bus.InboundContext, +) { + trackedChatID := trackedToolFeedbackMessageChatID(ch, chatID, outboundCtx) + if trackedChatID == "" { + return + } + if tracker, ok := ch.(toolFeedbackMessageTracker); ok { + tracker.ClearToolFeedbackMessage(trackedChatID) + } +} + func prepareToolFeedbackMessageContent(ch Channel, content string) string { prepared := strings.TrimSpace(content) if prepared == "" { @@ -183,6 +197,13 @@ func prepareToolFeedbackMessageContent(ch Channel, content string) string { return prepared } +func (m *Manager) toolFeedbackSeparateMessagesEnabled() bool { + if m == nil || m.config == nil { + return false + } + return m.config.Agents.Defaults.IsToolFeedbackSeparateMessagesEnabled() +} + // RecordPlaceholder registers a placeholder message for later editing. // Implements PlaceholderRecorder. func (m *Manager) RecordPlaceholder(channel, chatID, placeholderID string) { @@ -264,6 +285,7 @@ func (m *Manager) preSend(ctx context.Context, name string, msg bus.OutboundMess } isToolFeedback := outboundMessageIsToolFeedback(msg) + separateToolFeedbackMessages := m.toolFeedbackSeparateMessagesEnabled() // 3. If a stream already finalized this chat, stale tool feedback must be // dropped without consuming the final-response marker. Streaming finalization @@ -288,14 +310,28 @@ func (m *Manager) preSend(ctx context.Context, name string, msg bus.OutboundMess } } if !isToolFeedback { - dismissTrackedToolFeedbackMessage(ctx, ch, chatID, &msg.Context) + if separateToolFeedbackMessages { + clearTrackedToolFeedbackMessage(ch, chatID, &msg.Context) + } else { + dismissTrackedToolFeedbackMessage(ctx, ch, chatID, &msg.Context) + } } return nil, true } + if separateToolFeedbackMessages { + clearTrackedToolFeedbackMessage(ch, chatID, &msg.Context) + } + // 5. Try editing placeholder if v, loaded := m.placeholders.LoadAndDelete(key); loaded { if entry, ok := v.(placeholderEntry); ok && entry.id != "" { + if isToolFeedback && separateToolFeedbackMessages { + if deleter, ok := ch.(MessageDeleter); ok { + deleter.DeleteMessage(ctx, chatID, entry.id) // best effort + } + return nil, false + } if editor, ok := ch.(MessageEditor); ok { content := msg.Content trackedContent := msg.Content @@ -345,6 +381,10 @@ func (m *Manager) preSendMedia(ctx context.Context, name string, msg bus.Outboun // 3. Clear any finalized stream marker for this chat before media delivery. m.streamActive.LoadAndDelete(key) + if m.toolFeedbackSeparateMessagesEnabled() { + clearTrackedToolFeedbackMessage(ch, chatID, &msg.Context) + } + // 4. Delete placeholder if present. if v, loaded := m.placeholders.LoadAndDelete(key); loaded { if entry, ok := v.(placeholderEntry); ok && entry.id != "" { @@ -408,15 +448,26 @@ func (m *Manager) GetStreamer(ctx context.Context, channelName, chatID string) ( return &finalizeHookStreamer{ Streamer: streamer, onFinalize: func(finalizeCtx context.Context) { - dismissTrackedToolFeedbackMessage( - finalizeCtx, - ch, - chatID, - &bus.InboundContext{ - Channel: channelName, - ChatID: chatID, - }, - ) + if m.toolFeedbackSeparateMessagesEnabled() { + clearTrackedToolFeedbackMessage( + ch, + chatID, + &bus.InboundContext{ + Channel: channelName, + ChatID: chatID, + }, + ) + } else { + dismissTrackedToolFeedbackMessage( + finalizeCtx, + ch, + chatID, + &bus.InboundContext{ + Channel: channelName, + ChatID: chatID, + }, + ) + } m.streamActive.Store(key, true) }, }, true diff --git a/pkg/channels/manager_test.go b/pkg/channels/manager_test.go index 273c90468..a5d7c2838 100644 --- a/pkg/channels/manager_test.go +++ b/pkg/channels/manager_test.go @@ -804,6 +804,20 @@ type mockResolvedToolFeedbackEditor struct { resolveChatIDFn func(chatID string, outboundCtx *bus.InboundContext) string } +type mockDeletingMessageEditor struct { + mockMessageEditor + deleteCalls int + deletedChatID string + deletedMessageID string +} + +func (m *mockDeletingMessageEditor) DeleteMessage(_ context.Context, chatID, messageID string) error { + m.deleteCalls++ + m.deletedChatID = chatID + m.deletedMessageID = messageID + return nil +} + func (m *mockResolvedToolFeedbackEditor) ToolFeedbackMessageChatID( chatID string, outboundCtx *bus.InboundContext, @@ -1062,6 +1076,101 @@ func TestPreSend_NonToolFeedbackDefersTrackedMessageFinalizationToChannelSend(t } } +func TestPreSend_ToolFeedbackSeparateMessagesDeletesPlaceholderAndSkipsEdit(t *testing.T) { + m := newTestManager() + m.config = &config.Config{ + Agents: config.AgentsConfig{ + Defaults: config.AgentDefaults{ + ToolFeedback: config.ToolFeedbackConfig{ + Enabled: true, + SeparateMessages: true, + }, + }, + }, + } + + ch := &mockDeletingMessageEditor{ + mockMessageEditor: mockMessageEditor{ + editFn: func(_ context.Context, _, _, _ string) error { + t.Fatal("expected placeholder edit to be skipped in separate message mode") + return nil + }, + }, + } + + m.RecordPlaceholder("test", "123", "456") + + msg := testOutboundMessage(bus.OutboundMessage{ + Channel: "test", + ChatID: "123", + Content: "hello", + Context: bus.InboundContext{ + Channel: "test", + ChatID: "123", + Raw: map[string]string{ + "message_kind": "tool_feedback", + }, + }, + }) + + msgIDs, handled := m.preSend(context.Background(), "test", msg, ch) + if handled { + t.Fatalf("expected preSend to fall through so the channel can send a new message, got %v", msgIDs) + } + if ch.deleteCalls != 1 { + t.Fatalf("expected placeholder deletion, got %d delete calls", ch.deleteCalls) + } + if ch.deletedChatID != "123" || ch.deletedMessageID != "456" { + t.Fatalf("unexpected placeholder deletion target: %s/%s", ch.deletedChatID, ch.deletedMessageID) + } + if ch.recordedMessageID != "" { + t.Fatalf("expected no tracked placeholder record, got %q", ch.recordedMessageID) + } + if ch.clearedChatID != "123" { + t.Fatalf("expected tracked tool feedback state to be cleared before sending, got %q", ch.clearedChatID) + } +} + +func TestPreSend_NonToolFeedbackSeparateMessagesClearsTrackedMessageWithoutDismiss(t *testing.T) { + m := newTestManager() + m.config = &config.Config{ + Agents: config.AgentsConfig{ + Defaults: config.AgentDefaults{ + ToolFeedback: config.ToolFeedbackConfig{ + Enabled: true, + SeparateMessages: true, + }, + }, + }, + } + + ch := &mockMessageEditor{} + + msg := testOutboundMessage(bus.OutboundMessage{ + Channel: "test", + ChatID: "123", + Content: "final reply", + Context: bus.InboundContext{ + Channel: "test", + ChatID: "123", + }, + }) + + _, handled := m.preSend(context.Background(), "test", msg, ch) + if handled { + t.Fatal("expected preSend to leave final delivery to the channel") + } + if ch.clearedChatID != "123" { + t.Fatalf("expected tracked tool feedback state to be cleared, got %q", ch.clearedChatID) + } + if ch.dismissedChatID != "" { + t.Fatalf("expected tracked tool feedback message to be preserved, got dismissal for %q", ch.dismissedChatID) + } + if ch.finalizeCalled { + t.Fatal("expected separate message mode to skip in-place finalization") + } +} + func TestPreSend_StaleToolFeedbackDoesNotConsumeStreamActiveMarker(t *testing.T) { m := newTestManager() m.streamActive.Store("test:123", true) @@ -1153,6 +1262,38 @@ func TestPreSendMedia_LeavesTrackedMessageForChannelSend(t *testing.T) { } } +func TestPreSendMedia_SeparateMessagesClearsTrackedMessageWithoutDismiss(t *testing.T) { + m := newTestManager() + m.config = &config.Config{ + Agents: config.AgentsConfig{ + Defaults: config.AgentDefaults{ + ToolFeedback: config.ToolFeedbackConfig{ + Enabled: true, + SeparateMessages: true, + }, + }, + }, + } + + ch := &mockMessageEditor{} + + m.preSendMedia(context.Background(), "test", bus.OutboundMediaMessage{ + ChatID: "123", + Context: bus.InboundContext{ + Channel: "test", + ChatID: "123", + }, + }, ch) + + if ch.clearedChatID != "123" { + t.Fatalf("expected tracked tool feedback state to be cleared before media delivery, got %q", ch.clearedChatID) + } + if ch.dismissedChatID != "" { + t.Fatalf("expected tracked tool feedback message to be preserved"+ + " for media delivery, got %q", ch.dismissedChatID) + } +} + func TestSplitOutboundMessageContent_ToolFeedbackTruncatesInsteadOfSplitting(t *testing.T) { msg := testOutboundMessage(bus.OutboundMessage{ Channel: "test", @@ -1232,6 +1373,49 @@ func TestGetStreamer_FinalizeDismissesTrackedToolFeedback(t *testing.T) { } } +func TestGetStreamer_FinalizeSeparateMessagesClearsTrackedToolFeedback(t *testing.T) { + m := newTestManager() + m.config = &config.Config{ + Agents: config.AgentsConfig{ + Defaults: config.AgentDefaults{ + ToolFeedback: config.ToolFeedbackConfig{ + Enabled: true, + SeparateMessages: true, + }, + }, + }, + } + ch := &mockStreamingChannel{ + mockMessageEditor: mockMessageEditor{}, + streamer: &mockStreamer{ + finalizeFn: func(_ context.Context, content string) error { + if content != "final reply" { + t.Fatalf("unexpected finalize content: %q", content) + } + return nil + }, + }, + } + m.channels["test"] = ch + + streamer, ok := m.GetStreamer(context.Background(), "test", "123") + if !ok { + t.Fatal("expected streamer to be available") + } + if err := streamer.Finalize(context.Background(), "final reply"); err != nil { + t.Fatalf("Finalize() error = %v", err) + } + if ch.clearedChatID != "123" { + t.Fatalf("expected tracked tool feedback to be cleared for chat 123, got %q", ch.clearedChatID) + } + if ch.dismissedChatID != "" { + t.Fatalf("expected tracked tool feedback message to be preserved, got dismissal for %q", ch.dismissedChatID) + } + if _, ok := m.streamActive.Load("test:123"); !ok { + t.Fatal("expected streamActive marker to be recorded after finalize") + } +} + func TestGetStreamer_FinalizeDismissesResolvedTrackedToolFeedback(t *testing.T) { m := newTestManager() ch := &mockStreamingChannel{ diff --git a/pkg/config/config.go b/pkg/config/config.go index 161108638..6bb8d3ce6 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -247,8 +247,9 @@ type SubTurnConfig struct { } type ToolFeedbackConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_AGENTS_DEFAULTS_TOOL_FEEDBACK_ENABLED"` - MaxArgsLength int `json:"max_args_length" env:"PICOCLAW_AGENTS_DEFAULTS_TOOL_FEEDBACK_MAX_ARGS_LENGTH"` + Enabled bool `json:"enabled" env:"PICOCLAW_AGENTS_DEFAULTS_TOOL_FEEDBACK_ENABLED"` + MaxArgsLength int `json:"max_args_length" env:"PICOCLAW_AGENTS_DEFAULTS_TOOL_FEEDBACK_MAX_ARGS_LENGTH"` + SeparateMessages bool `json:"separate_messages" env:"PICOCLAW_AGENTS_DEFAULTS_TOOL_FEEDBACK_SEPARATE_MESSAGES"` } type AgentDefaults struct { @@ -299,6 +300,13 @@ func (d *AgentDefaults) IsToolFeedbackEnabled() bool { return d.ToolFeedback.Enabled } +// IsToolFeedbackSeparateMessagesEnabled returns true when each tool feedback +// update should be sent as its own chat message instead of editing a single +// in-place progress message. +func (d *AgentDefaults) IsToolFeedbackSeparateMessagesEnabled() bool { + return d.ToolFeedback.SeparateMessages +} + // GetModelName returns the effective model name for the agent defaults. // It prefers the new "model_name" field but falls back to "model" for backward compatibility. func (d *AgentDefaults) GetModelName() string { diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 624cc7305..2be1bcc67 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -787,6 +787,9 @@ func TestDefaultConfig_ToolFeedbackDisabled(t *testing.T) { if cfg.Agents.Defaults.ToolFeedback.Enabled { t.Fatal("DefaultConfig().Agents.Defaults.ToolFeedback.Enabled should be false") } + if cfg.Agents.Defaults.ToolFeedback.SeparateMessages { + t.Fatal("DefaultConfig().Agents.Defaults.ToolFeedback.SeparateMessages should be false") + } } func TestLoadConfig_ToolFeedbackDefaultsFalseWhenUnset(t *testing.T) { @@ -807,6 +810,9 @@ func TestLoadConfig_ToolFeedbackDefaultsFalseWhenUnset(t *testing.T) { if cfg.Agents.Defaults.ToolFeedback.Enabled { t.Fatal("agents.defaults.tool_feedback.enabled should remain false when unset in config file") } + if cfg.Agents.Defaults.ToolFeedback.SeparateMessages { + t.Fatal("agents.defaults.tool_feedback.separate_messages should remain false when unset in config file") + } } func TestLoadConfig_WebPreferNativeDefaultsTrueWhenUnset(t *testing.T) { diff --git a/pkg/config/defaults.go b/pkg/config/defaults.go index 35ef7cdd8..f3aaca7ab 100644 --- a/pkg/config/defaults.go +++ b/pkg/config/defaults.go @@ -35,8 +35,9 @@ func DefaultConfig() *Config { SummarizeTokenPercent: 75, SteeringMode: "one-at-a-time", ToolFeedback: ToolFeedbackConfig{ - Enabled: false, - MaxArgsLength: 300, + Enabled: false, + MaxArgsLength: 300, + SeparateMessages: false, }, SplitOnMarker: false, }, diff --git a/web/frontend/src/components/config/config-page.tsx b/web/frontend/src/components/config/config-page.tsx index f50503dec..cc1a4624e 100644 --- a/web/frontend/src/components/config/config-page.tsx +++ b/web/frontend/src/components/config/config-page.tsx @@ -244,6 +244,7 @@ export function ConfigPage() { tool_feedback: { enabled: form.toolFeedbackEnabled, max_args_length: toolFeedbackMaxArgsLength, + separate_messages: form.toolFeedbackSeparateMessages, }, max_tokens: maxTokens, context_window: contextWindow, diff --git a/web/frontend/src/components/config/config-sections.tsx b/web/frontend/src/components/config/config-sections.tsx index 25c335ab1..fa6b3a079 100644 --- a/web/frontend/src/components/config/config-sections.tsx +++ b/web/frontend/src/components/config/config-sections.tsx @@ -113,6 +113,18 @@ export function AgentDefaultsSection({ } /> + {form.toolFeedbackEnabled && ( + + onFieldChange("toolFeedbackSeparateMessages", checked) + } + /> + )} + {form.toolFeedbackEnabled && ( Date: Fri, 24 Apr 2026 11:50:45 +0800 Subject: [PATCH 06/36] build(deps): bump react-i18next from 17.0.3 to 17.0.4 in /web/frontend (#2631) Bumps [react-i18next](https://github.com/i18next/react-i18next) from 17.0.3 to 17.0.4. - [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md) - [Commits](https://github.com/i18next/react-i18next/compare/v17.0.3...v17.0.4) --- updated-dependencies: - dependency-name: react-i18next dependency-version: 17.0.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- web/frontend/package.json | 2 +- web/frontend/pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/web/frontend/package.json b/web/frontend/package.json index 835682617..2e5914dd5 100644 --- a/web/frontend/package.json +++ b/web/frontend/package.json @@ -33,7 +33,7 @@ "radix-ui": "^1.4.3", "react": "19.2.5", "react-dom": "19.2.5", - "react-i18next": "^17.0.3", + "react-i18next": "^17.0.4", "react-markdown": "^10.1.0", "react-textarea-autosize": "^8.5.9", "rehype-highlight": "^7.0.2", diff --git a/web/frontend/pnpm-lock.yaml b/web/frontend/pnpm-lock.yaml index 210c111c5..24458b580 100644 --- a/web/frontend/pnpm-lock.yaml +++ b/web/frontend/pnpm-lock.yaml @@ -57,8 +57,8 @@ importers: specifier: 19.2.5 version: 19.2.5(react@19.2.5) react-i18next: - specifier: ^17.0.3 - version: 17.0.3(i18next@26.0.3(typescript@5.9.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3) + specifier: ^17.0.4 + version: 17.0.4(i18next@26.0.3(typescript@5.9.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3) react-markdown: specifier: ^10.1.0 version: 10.1.0(@types/react@19.2.14)(react@19.2.5) @@ -3357,8 +3357,8 @@ packages: peerDependencies: react: ^19.2.5 - react-i18next@17.0.3: - resolution: {integrity: sha512-x4xjvUNZ56T+zfXWNedNnCET9Xq1IBYWX7IsWo5cCQ/RT+Rm7GWqt0h9PShFi4IhyMnsdiu1C6Jc4DE+/S3PFQ==} + react-i18next@17.0.4: + resolution: {integrity: sha512-hQipmK4EF0y6RO6tt6WuqnmWpWYEXmQUUzecmMBuNsIgYd3smXcG4GtYPWhvgxn0pqMOItKlEO8H24HCs5hc3g==} peerDependencies: i18next: '>= 26.0.1' react: '>= 16.8.0' @@ -7386,7 +7386,7 @@ snapshots: react: 19.2.5 scheduler: 0.27.0 - react-i18next@17.0.3(i18next@26.0.3(typescript@5.9.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3): + react-i18next@17.0.4(i18next@26.0.3(typescript@5.9.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3): dependencies: '@babel/runtime': 7.29.2 html-parse-stringify: 3.0.1 From ffd22c7fb6e03b72b8529357bc0d8faa4fe1987e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Apr 2026 11:55:19 +0800 Subject: [PATCH 07/36] build(deps): bump i18next from 26.0.3 to 26.0.7 in /web/frontend (#2632) Bumps [i18next](https://github.com/i18next/i18next) from 26.0.3 to 26.0.7. - [Release notes](https://github.com/i18next/i18next/releases) - [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md) - [Commits](https://github.com/i18next/i18next/compare/v26.0.3...v26.0.7) --- updated-dependencies: - dependency-name: i18next dependency-version: 26.0.7 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- web/frontend/package.json | 2 +- web/frontend/pnpm-lock.yaml | 18 ++++++++---------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/web/frontend/package.json b/web/frontend/package.json index 2e5914dd5..74183ca0a 100644 --- a/web/frontend/package.json +++ b/web/frontend/package.json @@ -27,7 +27,7 @@ "clsx": "^2.1.1", "dayjs": "^1.11.20", "highlight.js": "^11.11.1", - "i18next": "^26.0.3", + "i18next": "^26.0.7", "i18next-browser-languagedetector": "^8.2.1", "jotai": "^2.19.1", "radix-ui": "^1.4.3", diff --git a/web/frontend/pnpm-lock.yaml b/web/frontend/pnpm-lock.yaml index 24458b580..ab8f02b9f 100644 --- a/web/frontend/pnpm-lock.yaml +++ b/web/frontend/pnpm-lock.yaml @@ -39,8 +39,8 @@ importers: specifier: ^11.11.1 version: 11.11.1 i18next: - specifier: ^26.0.3 - version: 26.0.3(typescript@5.9.3) + specifier: ^26.0.7 + version: 26.0.7(typescript@5.9.3) i18next-browser-languagedetector: specifier: ^8.2.1 version: 8.2.1 @@ -58,7 +58,7 @@ importers: version: 19.2.5(react@19.2.5) react-i18next: specifier: ^17.0.4 - version: 17.0.4(i18next@26.0.3(typescript@5.9.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3) + version: 17.0.4(i18next@26.0.7(typescript@5.9.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3) react-markdown: specifier: ^10.1.0 version: 10.1.0(@types/react@19.2.14)(react@19.2.5) @@ -2544,8 +2544,8 @@ packages: i18next-browser-languagedetector@8.2.1: resolution: {integrity: sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==} - i18next@26.0.3: - resolution: {integrity: sha512-1571kXINxHKY7LksWp8wP+zP0YqHSSpl/OW0Y0owFEf2H3s8gCAffWaZivcz14rMkOvn3R/psiQxVsR9t2Nafg==} + i18next@26.0.7: + resolution: {integrity: sha512-f7tL/iw0VQsx4nC5oNxBM2RjM8alNys5KzyiQTU6A9TI5TI89py4/Ez1cKFvHiLWsvzOXvuGUES+Kk/A2WiANQ==} peerDependencies: typescript: ^5 || ^6 peerDependenciesMeta: @@ -6469,9 +6469,7 @@ snapshots: dependencies: '@babel/runtime': 7.29.2 - i18next@26.0.3(typescript@5.9.3): - dependencies: - '@babel/runtime': 7.29.2 + i18next@26.0.7(typescript@5.9.3): optionalDependencies: typescript: 5.9.3 @@ -7386,11 +7384,11 @@ snapshots: react: 19.2.5 scheduler: 0.27.0 - react-i18next@17.0.4(i18next@26.0.3(typescript@5.9.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3): + react-i18next@17.0.4(i18next@26.0.7(typescript@5.9.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3): dependencies: '@babel/runtime': 7.29.2 html-parse-stringify: 3.0.1 - i18next: 26.0.3(typescript@5.9.3) + i18next: 26.0.7(typescript@5.9.3) react: 19.2.5 use-sync-external-store: 1.6.0(react@19.2.5) optionalDependencies: From 73594a07ca394f052517d58121892449a3d216c0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Apr 2026 11:59:30 +0800 Subject: [PATCH 08/36] build(deps): bump github.com/aws/aws-sdk-go-v2/config (#2633) Bumps [github.com/aws/aws-sdk-go-v2/config](https://github.com/aws/aws-sdk-go-v2) from 1.32.14 to 1.32.16. - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Commits](https://github.com/aws/aws-sdk-go-v2/compare/config/v1.32.14...config/v1.32.16) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go-v2/config dependency-version: 1.32.16 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 20 ++++++++++---------- go.sum | 40 ++++++++++++++++++++-------------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/go.mod b/go.mod index 391f5a2fd..08e03a895 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/anthropics/anthropic-sdk-go v1.26.0 github.com/atc0005/go-teams-notify/v2 v2.14.0 github.com/aws/aws-sdk-go-v2 v1.41.6 - github.com/aws/aws-sdk-go-v2/config v1.32.14 + github.com/aws/aws-sdk-go-v2/config v1.32.16 github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.50.5 github.com/bwmarrin/discordgo v0.29.0 github.com/caarlos0/env/v11 v11.4.0 @@ -56,17 +56,17 @@ require ( aead.dev/minisign v0.2.0 // indirect filippo.io/edwards25519 v1.2.0 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.9 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.19.14 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.15 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.22 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.22 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.22 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.30.15 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.23 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.8 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.22 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.10 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.16 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.20 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.42.0 // indirect github.com/aws/smithy-go v1.25.0 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beeper/argo-go v1.1.2 // indirect diff --git a/go.sum b/go.sum index 854fdaed1..5eae9399d 100644 --- a/go.sum +++ b/go.sum @@ -27,32 +27,32 @@ github.com/aws/aws-sdk-go-v2 v1.41.6 h1:1AX0AthnBQzMx1vbmir3Y4WsnJgiydmnJjiLu+Lv github.com/aws/aws-sdk-go-v2 v1.41.6/go.mod h1:dy0UzBIfwSeot4grGvY1AqFWN5zgziMmWGzysDnHFcQ= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.9 h1:adBsCIIpLbLmYnkQU+nAChU5yhVTvu5PerROm+/Kq2A= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.9/go.mod h1:uOYhgfgThm/ZyAuJGNQ5YgNyOlYfqnGpTHXvk3cpykg= -github.com/aws/aws-sdk-go-v2/config v1.32.14 h1:opVIRo/ZbbI8OIqSOKmpFaY7IwfFUOCCXBsUpJOwDdI= -github.com/aws/aws-sdk-go-v2/config v1.32.14/go.mod h1:U4/V0uKxh0Tl5sxmCBZ3AecYny4UNlVmObYjKuuaiOo= -github.com/aws/aws-sdk-go-v2/credentials v1.19.14 h1:n+UcGWAIZHkXzYt87uMFBv/l8THYELoX6gVcUvgl6fI= -github.com/aws/aws-sdk-go-v2/credentials v1.19.14/go.mod h1:cJKuyWB59Mqi0jM3nFYQRmnHVQIcgoxjEMAbLkpr62w= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 h1:NUS3K4BTDArQqNu2ih7yeDLaS3bmHD0YndtA6UP884g= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21/go.mod h1:YWNWJQNjKigKY1RHVJCuupeWDrrHjRqHm0N9rdrWzYI= +github.com/aws/aws-sdk-go-v2/config v1.32.16 h1:Q0iQ7quUgJP0F/SCRTieScnaMdXr9h/2+wze1u3cNeM= +github.com/aws/aws-sdk-go-v2/config v1.32.16/go.mod h1:duCCnJEFqpt2RC6no1iK6q+8HpwOAkiUua0pY507dQc= +github.com/aws/aws-sdk-go-v2/credentials v1.19.15 h1:fyvgWTszojq8hEnMi8PPBTvZdTtEVmAVyo+NFLHBhH4= +github.com/aws/aws-sdk-go-v2/credentials v1.19.15/go.mod h1:gJiYyMOjNg8OEdRWOf3CrFQxM2a98qmrtjx1zuiQfB8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.22 h1:IOGsJ1xVWhsi+ZO7/NW8OuZZBtMJLZbk4P5HDjJO0jQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.22/go.mod h1:b+hYdbU+jGKfXE8kKM6g1+h+L/Go3vMvzlxBsiuGsxg= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.22 h1:GmLa5Kw1ESqtFpXsx5MmC84QWa/ZrLZvlJGa2y+4kcQ= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.22/go.mod h1:6sW9iWm9DK9YRpRGga/qzrzNLgKpT2cIxb7Vo2eNOp0= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.22 h1:dY4kWZiSaXIzxnKlj17nHnBcXXBfac6UlsAx2qL6XrU= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.22/go.mod h1:KIpEUx0JuRZLO7U6cbV204cWAEco2iC3l061IxlwLtI= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.23 h1:FPXsW9+gMuIeKmz7j6ENWcWtBGTe1kH8r9thNt5Uxx4= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.23/go.mod h1:7J8iGMdRKk6lw2C+cMIphgAnT8uTwBwNOsGkyOCm80U= github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.50.5 h1:ZGTl4Rxft1uyENAlGESY04hMzE4cLLNUPI7dGw08haw= github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.50.5/go.mod h1:jnugA+VgESQGgXuEKK6zVToET/DtODq7LQYpe+BkKT4= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 h1:QKZH0S178gCmFEgst8hN0mCX1KxLgHBKKY/CLqwP8lg= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.9/go.mod h1:7yuQJoT+OoH8aqIxw9vwF+8KpvLZ8AWmvmUWHsGQZvI= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.15 h1:lFd1+ZSEYJZYvv9d6kXzhkZu07si3f+GQ1AaYwa2LUM= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.15/go.mod h1:WSvS1NLr7JaPunCXqpJnWk1Bjo7IxzZXrZi1QQCkuqM= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 h1:dzztQ1YmfPrxdrOiuZRMF6fuOwWlWpD2StNLTceKpys= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19/go.mod h1:YO8TrYtFdl5w/4vmjL8zaBSsiNp3w0L1FfKVKenZT7w= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 h1:p8ogvvLugcR/zLBXTXrTkj0RYBUdErbMnAFFp12Lm/U= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.10/go.mod h1:60dv0eZJfeVXfbT1tFJinbHrDfSJ2GZl4Q//OSSNAVw= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.8 h1:HtOTYcbVcGABLOVuPYaIihj6IlkqubBwFj10K5fxRek= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.8/go.mod h1:VsK9abqQeGlzPgUr+isNWzPlK2vKe9INMLWnY65f5Xs= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.22 h1:PUmZeJU6Y1Lbvt9WFuJ0ugUK2xn6hIWUBBbKuOWF30s= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.22/go.mod h1:nO6egFBoAaoXze24a2C0NjQCvdpk8OueRoYimvEB9jo= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.10 h1:a1Fq/KXn75wSzoJaPQTgZO0wHGqE9mjFnylnqEPTchA= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.10/go.mod h1:p6+MXNxW7IA6dMgHfTAzljuwSKD0NCm/4lbS4t6+7vI= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.16 h1:x6bKbmDhsgSZwv6q19wY/u3rLk/3FGjJWyqKcIRufpE= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.16/go.mod h1:CudnEVKRtLn0+3uMV0yEXZ+YZOKnAtUJ5DmDhilVnIw= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.20 h1:oK/njaL8GtyEihkWMD4k3VgHCT64RQKkZwh0DG5j8ak= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.20/go.mod h1:JHs8/y1f3zY7U5WcuzoJ/yAYGYtNIVPKLIbp61euvmg= +github.com/aws/aws-sdk-go-v2/service/sts v1.42.0 h1:ks8KBcZPh3PYISr5dAiXCM5/Thcuxk8l+PG4+A0exds= +github.com/aws/aws-sdk-go-v2/service/sts v1.42.0/go.mod h1:pFw33T0WLvXU3rw1WBkpMlkgIn54eCB5FYLhjDc9Foo= github.com/aws/smithy-go v1.25.0 h1:Sz/XJ64rwuiKtB6j98nDIPyYrV1nVNJ4YU74gttcl5U= github.com/aws/smithy-go v1.25.0/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= From 1dba8e9e917cd89ae30d503d44aed65dfb1391b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Apr 2026 11:59:54 +0800 Subject: [PATCH 09/36] build(deps-dev): bump vite from 8.0.8 to 8.0.10 in /web/frontend (#2634) Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.0.8 to 8.0.10. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v8.0.10/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 8.0.10 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- web/frontend/package.json | 2 +- web/frontend/pnpm-lock.yaml | 214 +++++++++++++++++------------------- 2 files changed, 103 insertions(+), 113 deletions(-) diff --git a/web/frontend/package.json b/web/frontend/package.json index 74183ca0a..a3bcd0b5f 100644 --- a/web/frontend/package.json +++ b/web/frontend/package.json @@ -66,6 +66,6 @@ "prettier-plugin-tailwindcss": "^0.7.2", "typescript": "~5.9.3", "typescript-eslint": "^8.58.2", - "vite": "^8.0.8" + "vite": "^8.0.10" } } diff --git a/web/frontend/pnpm-lock.yaml b/web/frontend/pnpm-lock.yaml index ab8f02b9f..046ae610c 100644 --- a/web/frontend/pnpm-lock.yaml +++ b/web/frontend/pnpm-lock.yaml @@ -16,7 +16,7 @@ importers: version: 3.41.1(react@19.2.5) '@tailwindcss/vite': specifier: ^4.2.2 - version: 4.2.2(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.2.2(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) '@tanstack/react-query': specifier: ^5.99.0 version: 5.99.0(react@19.2.5) @@ -104,7 +104,7 @@ importers: version: 0.5.19(tailwindcss@4.2.2) '@tanstack/router-plugin': specifier: ^1.164.0 - version: 1.167.9(@tanstack/react-router@1.168.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 1.167.9(@tanstack/react-router@1.168.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) '@trivago/prettier-plugin-sort-imports': specifier: ^6.0.2 version: 6.0.2(prettier@3.8.3) @@ -122,7 +122,7 @@ importers: version: 8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3) '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 6.0.1(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) eslint: specifier: ^10.2.1 version: 10.2.1(jiti@2.6.1) @@ -151,8 +151,8 @@ importers: specifier: ^8.58.2 version: 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3) vite: - specifier: ^8.0.8 - version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) + specifier: ^8.0.10 + version: 8.0.10(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) packages: @@ -299,11 +299,11 @@ packages: peerDependencies: '@noble/ciphers': ^1.0.0 - '@emnapi/core@1.9.2': - resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} - '@emnapi/runtime@1.9.2': - resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} @@ -608,8 +608,8 @@ packages: resolution: {integrity: sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA==} engines: {node: '>=18'} - '@napi-rs/wasm-runtime@1.1.3': - resolution: {integrity: sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==} + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} peerDependencies: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 @@ -650,8 +650,8 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} - '@oxc-project/types@0.124.0': - resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==} + '@oxc-project/types@0.127.0': + resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} @@ -1343,103 +1343,103 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} - '@rolldown/binding-android-arm64@1.0.0-rc.15': - resolution: {integrity: sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==} + '@rolldown/binding-android-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.0-rc.15': - resolution: {integrity: sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==} + '@rolldown/binding-darwin-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-rc.15': - resolution: {integrity: sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==} + '@rolldown/binding-darwin-x64@1.0.0-rc.17': + resolution: {integrity: sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.0-rc.15': - resolution: {integrity: sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==} + '@rolldown/binding-freebsd-x64@1.0.0-rc.17': + resolution: {integrity: sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': - resolution: {integrity: sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': + resolution: {integrity: sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': - resolution: {integrity: sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==} + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': - resolution: {integrity: sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==} + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': + resolution: {integrity: sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': - resolution: {integrity: sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==} + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': - resolution: {integrity: sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==} + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': - resolution: {integrity: sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==} + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': - resolution: {integrity: sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==} + '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': + resolution: {integrity: sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': - resolution: {integrity: sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==} + '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': - resolution: {integrity: sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==} - engines: {node: '>=14.0.0'} + '@rolldown/binding-wasm32-wasi@1.0.0-rc.17': + resolution: {integrity: sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': - resolution: {integrity: sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==} + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': + resolution: {integrity: sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': - resolution: {integrity: sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==} + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': + resolution: {integrity: sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rolldown/pluginutils@1.0.0-rc.15': - resolution: {integrity: sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==} + '@rolldown/pluginutils@1.0.0-rc.17': + resolution: {integrity: sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==} '@rolldown/pluginutils@1.0.0-rc.7': resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} @@ -3233,10 +3233,6 @@ packages: resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==} engines: {node: ^10 || ^12 || >=14} - postcss@8.5.9: - resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==} - engines: {node: ^10 || ^12 || >=14} - powershell-utils@0.1.0: resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} engines: {node: '>=20'} @@ -3474,8 +3470,8 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rolldown@1.0.0-rc.15: - resolution: {integrity: sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==} + rolldown@1.0.0-rc.17: + resolution: {integrity: sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -3872,8 +3868,8 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - vite@8.0.8: - resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==} + vite@8.0.10: + resolution: {integrity: sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -4212,13 +4208,13 @@ snapshots: dependencies: '@noble/ciphers': 1.3.0 - '@emnapi/core@1.9.2': + '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.9.2': + '@emnapi/runtime@1.10.0': dependencies: tslib: 2.8.1 optional: true @@ -4451,10 +4447,10 @@ snapshots: outvariant: 1.4.3 strict-event-emitter: 0.5.1 - '@napi-rs/wasm-runtime@1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: - '@emnapi/core': 1.9.2 - '@emnapi/runtime': 1.9.2 + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 '@tybys/wasm-util': 0.10.1 optional: true @@ -4489,7 +4485,7 @@ snapshots: '@open-draft/until@2.1.0': {} - '@oxc-project/types@0.124.0': {} + '@oxc-project/types@0.127.0': {} '@radix-ui/number@1.1.1': {} @@ -5238,56 +5234,56 @@ snapshots: '@radix-ui/rect@1.1.1': {} - '@rolldown/binding-android-arm64@1.0.0-rc.15': + '@rolldown/binding-android-arm64@1.0.0-rc.17': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-rc.15': + '@rolldown/binding-darwin-arm64@1.0.0-rc.17': optional: true - '@rolldown/binding-darwin-x64@1.0.0-rc.15': + '@rolldown/binding-darwin-x64@1.0.0-rc.17': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-rc.15': + '@rolldown/binding-freebsd-x64@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': + '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': + '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': + '@rolldown/binding-wasm32-wasi@1.0.0-rc.17': dependencies: - '@emnapi/core': 1.9.2 - '@emnapi/runtime': 1.9.2 - '@napi-rs/wasm-runtime': 1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': optional: true - '@rolldown/pluginutils@1.0.0-rc.15': {} + '@rolldown/pluginutils@1.0.0-rc.17': {} '@rolldown/pluginutils@1.0.0-rc.7': {} @@ -5368,12 +5364,12 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 4.2.2 - '@tailwindcss/vite@4.2.2(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))': + '@tailwindcss/vite@4.2.2(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))': dependencies: '@tailwindcss/node': 4.2.2 '@tailwindcss/oxide': 4.2.2 tailwindcss: 4.2.2 - vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) + vite: 8.0.10(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) '@tanstack/history@1.161.6': {} @@ -5446,7 +5442,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.167.9(@tanstack/react-router@1.168.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))': + '@tanstack/router-plugin@1.167.9(@tanstack/react-router@1.168.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) @@ -5463,7 +5459,7 @@ snapshots: zod: 3.25.76 optionalDependencies: '@tanstack/react-router': 1.168.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) + vite: 8.0.10(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) transitivePeerDependencies: - supports-color @@ -5651,10 +5647,10 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react@6.0.1(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))': + '@vitejs/plugin-react@6.0.1(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 - vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) + vite: 8.0.10(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) accepts@2.0.0: dependencies: @@ -7265,12 +7261,6 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - postcss@8.5.9: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - powershell-utils@0.1.0: {} prelude-ls@1.2.1: {} @@ -7533,26 +7523,26 @@ snapshots: reusify@1.1.0: {} - rolldown@1.0.0-rc.15: + rolldown@1.0.0-rc.17: dependencies: - '@oxc-project/types': 0.124.0 - '@rolldown/pluginutils': 1.0.0-rc.15 + '@oxc-project/types': 0.127.0 + '@rolldown/pluginutils': 1.0.0-rc.17 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-rc.15 - '@rolldown/binding-darwin-arm64': 1.0.0-rc.15 - '@rolldown/binding-darwin-x64': 1.0.0-rc.15 - '@rolldown/binding-freebsd-x64': 1.0.0-rc.15 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.15 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.15 - '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.15 - '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.15 - '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.15 - '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.15 - '@rolldown/binding-linux-x64-musl': 1.0.0-rc.15 - '@rolldown/binding-openharmony-arm64': 1.0.0-rc.15 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.15 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.15 - '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.15 + '@rolldown/binding-android-arm64': 1.0.0-rc.17 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.17 + '@rolldown/binding-darwin-x64': 1.0.0-rc.17 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.17 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.17 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.17 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.17 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.17 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.17 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.17 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.17 router@2.2.0: dependencies: @@ -7983,12 +7973,12 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0): + vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 - postcss: 8.5.9 - rolldown: 1.0.0-rc.15 + postcss: 8.5.10 + rolldown: 1.0.0-rc.17 tinyglobby: 0.2.16 optionalDependencies: '@types/node': 25.6.0 From 047a904b4f0b4fe248416bc3e76ac8257b6eeadb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Apr 2026 12:02:04 +0800 Subject: [PATCH 10/36] build(deps): bump github.com/rs/zerolog from 1.35.0 to 1.35.1 (#2635) Bumps [github.com/rs/zerolog](https://github.com/rs/zerolog) from 1.35.0 to 1.35.1. - [Commits](https://github.com/rs/zerolog/compare/v1.35.0...v1.35.1) --- updated-dependencies: - dependency-name: github.com/rs/zerolog dependency-version: 1.35.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 08e03a895..1cfcf2f78 100644 --- a/go.mod +++ b/go.mod @@ -34,7 +34,7 @@ require ( github.com/pion/rtp v1.10.1 github.com/pion/webrtc/v3 v3.3.6 github.com/rivo/tview v0.42.0 - github.com/rs/zerolog v1.35.0 + github.com/rs/zerolog v1.35.1 github.com/slack-go/slack v0.17.3 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 diff --git a/go.sum b/go.sum index 5eae9399d..f26a0fe2a 100644 --- a/go.sum +++ b/go.sum @@ -241,8 +241,8 @@ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/rs/zerolog v1.35.0 h1:VD0ykx7HMiMJytqINBsKcbLS+BJ4WYjz+05us+LRTdI= -github.com/rs/zerolog v1.35.0/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= +github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI= +github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc= github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= From 743d7e69f2f81ca18d737a21014705531b450c71 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Apr 2026 12:04:47 +0800 Subject: [PATCH 11/36] build(deps): bump github.com/larksuite/oapi-sdk-go/v3 (#2637) Bumps [github.com/larksuite/oapi-sdk-go/v3](https://github.com/larksuite/oapi-sdk-go) from 3.5.3 to 3.5.4. - [Release notes](https://github.com/larksuite/oapi-sdk-go/releases) - [Changelog](https://github.com/larksuite/oapi-sdk-go/blob/v3_main/changelog.md) - [Commits](https://github.com/larksuite/oapi-sdk-go/compare/v3.5.3...v3.5.4) --- updated-dependencies: - dependency-name: github.com/larksuite/oapi-sdk-go/v3 dependency-version: 3.5.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1cfcf2f78..4afbe9d85 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 github.com/h2non/filetype v1.1.3 - github.com/larksuite/oapi-sdk-go/v3 v3.5.3 + github.com/larksuite/oapi-sdk-go/v3 v3.5.4 github.com/mdp/qrterminal/v3 v3.2.1 github.com/minio/selfupdate v0.6.0 github.com/modelcontextprotocol/go-sdk v1.5.0 diff --git a/go.sum b/go.sum index f26a0fe2a..19547816d 100644 --- a/go.sum +++ b/go.sum @@ -183,8 +183,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/larksuite/oapi-sdk-go/v3 v3.5.3 h1:xvf8Dv29kBXC5/DNDCLhHkAFW8l/0LlQJimO5Zn+JUk= -github.com/larksuite/oapi-sdk-go/v3 v3.5.3/go.mod h1:ZEplY+kwuIrj/nqw5uSCINNATcH3KdxSN7y+UxYY5fI= +github.com/larksuite/oapi-sdk-go/v3 v3.5.4 h1:U2S9x9LrfH++ZqJ+YAiUlqzCWJmVXhFdS8Z7rIBH8H0= +github.com/larksuite/oapi-sdk-go/v3 v3.5.4/go.mod h1:ZEplY+kwuIrj/nqw5uSCINNATcH3KdxSN7y+UxYY5fI= github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= From 47a881b11fc2da7f62af201f0e9e37ab14d9d090 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Apr 2026 12:05:08 +0800 Subject: [PATCH 12/36] build(deps-dev): bump typescript-eslint in /web/frontend (#2638) Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.58.2 to 8.59.0. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.59.0/packages/typescript-eslint) --- updated-dependencies: - dependency-name: typescript-eslint dependency-version: 8.59.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- web/frontend/package.json | 2 +- web/frontend/pnpm-lock.yaml | 173 +++++++++++++++++++++++++++++++----- 2 files changed, 153 insertions(+), 22 deletions(-) diff --git a/web/frontend/package.json b/web/frontend/package.json index a3bcd0b5f..ab07b40a2 100644 --- a/web/frontend/package.json +++ b/web/frontend/package.json @@ -65,7 +65,7 @@ "prettier": "^3.8.3", "prettier-plugin-tailwindcss": "^0.7.2", "typescript": "~5.9.3", - "typescript-eslint": "^8.58.2", + "typescript-eslint": "^8.59.0", "vite": "^8.0.10" } } diff --git a/web/frontend/pnpm-lock.yaml b/web/frontend/pnpm-lock.yaml index 046ae610c..cb5ca18de 100644 --- a/web/frontend/pnpm-lock.yaml +++ b/web/frontend/pnpm-lock.yaml @@ -119,7 +119,7 @@ importers: version: 19.2.3(@types/react@19.2.14) '@typescript-eslint/eslint-plugin': specifier: ^8.58.2 - version: 8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3) + version: 8.58.2(@typescript-eslint/parser@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3) '@vitejs/plugin-react': specifier: ^6.0.1 version: 6.0.1(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) @@ -148,8 +148,8 @@ importers: specifier: ~5.9.3 version: 5.9.3 typescript-eslint: - specifier: ^8.58.2 - version: 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3) + specifier: ^8.59.0 + version: 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3) vite: specifier: ^8.0.10 version: 8.0.10(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) @@ -1736,8 +1736,16 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.58.2': - resolution: {integrity: sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==} + '@typescript-eslint/eslint-plugin@8.59.0': + resolution: {integrity: sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.59.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.59.0': + resolution: {integrity: sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -1749,16 +1757,32 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/project-service@8.59.0': + resolution: {integrity: sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/scope-manager@8.58.2': resolution: {integrity: sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/scope-manager@8.59.0': + resolution: {integrity: sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/tsconfig-utils@8.58.2': resolution: {integrity: sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/tsconfig-utils@8.59.0': + resolution: {integrity: sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/type-utils@8.58.2': resolution: {integrity: sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1766,16 +1790,33 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/type-utils@8.59.0': + resolution: {integrity: sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/types@8.58.2': resolution: {integrity: sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/types@8.59.0': + resolution: {integrity: sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@8.58.2': resolution: {integrity: sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/typescript-estree@8.59.0': + resolution: {integrity: sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/utils@8.58.2': resolution: {integrity: sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1783,10 +1824,21 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/utils@8.59.0': + resolution: {integrity: sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/visitor-keys@8.58.2': resolution: {integrity: sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/visitor-keys@8.59.0': + resolution: {integrity: sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} @@ -3732,8 +3784,8 @@ packages: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} - typescript-eslint@8.58.2: - resolution: {integrity: sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ==} + typescript-eslint@8.59.0: + resolution: {integrity: sha512-BU3ONW9X+v90EcCH9ZS6LMackcVtxRLlI3XrYyqZIwVSHIk7Qf7bFw1z0M9Q0IUxhTMZCf8piY9hTYaNEIASrw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -5554,10 +5606,10 @@ snapshots: '@types/validate-npm-package-name@4.0.2': {} - '@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.58.2 '@typescript-eslint/type-utils': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/utils': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3) @@ -5570,12 +5622,28 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.59.0(@typescript-eslint/parser@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.58.2 - '@typescript-eslint/types': 8.58.2 - '@typescript-eslint/typescript-estree': 8.58.2(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.58.2 + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.59.0 + '@typescript-eslint/type-utils': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.59.0 + eslint: 10.2.1(jiti@2.6.1) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.59.0 + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/typescript-estree': 8.59.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.59.0 debug: 4.4.3 eslint: 10.2.1(jiti@2.6.1) typescript: 5.9.3 @@ -5584,8 +5652,17 @@ snapshots: '@typescript-eslint/project-service@8.58.2(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.58.2(typescript@5.9.3) - '@typescript-eslint/types': 8.58.2 + '@typescript-eslint/tsconfig-utils': 8.59.0(typescript@5.9.3) + '@typescript-eslint/types': 8.59.0 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.59.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.59.0(typescript@5.9.3) + '@typescript-eslint/types': 8.59.0 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: @@ -5596,10 +5673,19 @@ snapshots: '@typescript-eslint/types': 8.58.2 '@typescript-eslint/visitor-keys': 8.58.2 + '@typescript-eslint/scope-manager@8.59.0': + dependencies: + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/visitor-keys': 8.59.0 + '@typescript-eslint/tsconfig-utils@8.58.2(typescript@5.9.3)': dependencies: typescript: 5.9.3 + '@typescript-eslint/tsconfig-utils@8.59.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + '@typescript-eslint/type-utils@8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.58.2 @@ -5612,8 +5698,22 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/type-utils@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/typescript-estree': 8.59.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3) + debug: 4.4.3 + eslint: 10.2.1(jiti@2.6.1) + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/types@8.58.2': {} + '@typescript-eslint/types@8.59.0': {} + '@typescript-eslint/typescript-estree@8.58.2(typescript@5.9.3)': dependencies: '@typescript-eslint/project-service': 8.58.2(typescript@5.9.3) @@ -5629,6 +5729,21 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/typescript-estree@8.59.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.59.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.59.0(typescript@5.9.3) + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/visitor-keys': 8.59.0 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.7.4 + tinyglobby: 0.2.16 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/utils@8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1)) @@ -5640,11 +5755,27 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/utils@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.59.0 + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/typescript-estree': 8.59.0(typescript@5.9.3) + eslint: 10.2.1(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/visitor-keys@8.58.2': dependencies: '@typescript-eslint/types': 8.58.2 eslint-visitor-keys: 5.0.1 + '@typescript-eslint/visitor-keys@8.59.0': + dependencies: + '@typescript-eslint/types': 8.59.0 + eslint-visitor-keys: 5.0.1 + '@ungap/structured-clone@1.3.0': {} '@vitejs/plugin-react@6.0.1(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))': @@ -7836,12 +7967,12 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.2 - typescript-eslint@8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3): + typescript-eslint@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.58.2(typescript@5.9.3) - '@typescript-eslint/utils': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.59.0(@typescript-eslint/parser@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.59.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3) eslint: 10.2.1(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: From 293477b02a13bb47515c8ec1dca23150844e6b5b Mon Sep 17 00:00:00 2001 From: Junghwan <70629228+shaun0927@users.noreply.github.com> Date: Fri, 24 Apr 2026 14:45:25 +0900 Subject: [PATCH 13/36] Keep launcher locale changes from mutating shared web-search routing (#2573) The launcher wired UI language changes into a process-global backend switch that changed auto web-search provider selection and the reported current service for every handler in the same process. This narrows the fix to the validated leak: remove backend sync from frontend locale changes, drop the now-unused UI endpoint, and make auto selection fall back to a stable default when the query itself does not contain a script hint. Constraint: Keep the patch small and mergeable without redesigning per-user preference storage Rejected: Add per-user backend language state | larger scope than the validated bug and unclear maintainer preference Rejected: Persist preferred language in config | still shares mutable state across clients of the same instance Confidence: high Scope-risk: narrow Reversibility: clean Directive: If locale-aware provider routing is reintroduced later, scope it to explicit config or request context instead of package-global state Tested: go test ./web/backend/api ./pkg/tools -count=1; pnpm lint; pnpm build Not-tested: Full make check; live multi-browser manual launcher run after the backend endpoint removal --- pkg/tools/integration/web.go | 27 ++--------------- pkg/tools/integration/web_test.go | 19 ++---------- pkg/tools/integration_facade.go | 8 ------ web/backend/api/router.go | 1 - web/backend/api/tools_test.go | 13 +-------- web/backend/api/ui.go | 27 ----------------- web/backend/api/ui_test.go | 48 ------------------------------- web/backend/main.go | 2 -- web/frontend/src/i18n/index.ts | 10 ------- 9 files changed, 5 insertions(+), 150 deletions(-) delete mode 100644 web/backend/api/ui.go delete mode 100644 web/backend/api/ui_test.go diff --git a/pkg/tools/integration/web.go b/pkg/tools/integration/web.go index 56663ecda..75821e40d 100644 --- a/pkg/tools/integration/web.go +++ b/pkg/tools/integration/web.go @@ -58,8 +58,6 @@ var ( reSogouRealURL = regexp.MustCompile(`url=([^&]+)`) ) -var preferredWebSearchLanguage atomic.Value - type APIKeyPool struct { keys []string current uint32 @@ -250,27 +248,6 @@ func mapBaiduRecencyFilter(rangeCode string) string { } } -func normalizePreferredWebSearchLanguage(lang string) string { - lang = strings.ToLower(strings.TrimSpace(lang)) - switch { - case strings.HasPrefix(lang, "zh"), lang == "chinese": - return "zh" - case strings.HasPrefix(lang, "en"), lang == "english": - return "en" - default: - return "" - } -} - -func SetPreferredWebSearchLanguage(lang string) { - preferredWebSearchLanguage.Store(normalizePreferredWebSearchLanguage(lang)) -} - -func GetPreferredWebSearchLanguage() string { - lang, _ := preferredWebSearchLanguage.Load().(string) - return lang -} - type BraveSearchProvider struct { keyPool *APIKeyPool proxy string @@ -1420,7 +1397,7 @@ func containsLatinLetter(text string) bool { func prefersDuckDuckGoQuery(text string) bool { trimmed := strings.TrimSpace(text) if trimmed == "" { - return GetPreferredWebSearchLanguage() == "en" + return false } if containsHan(trimmed) { return false @@ -1428,7 +1405,7 @@ func prefersDuckDuckGoQuery(text string) bool { if containsLatinLetter(trimmed) { return true } - return GetPreferredWebSearchLanguage() == "en" + return false } func (opts WebSearchToolOptions) buildProviderResolver() (func(query string) (SearchProvider, int), error) { diff --git a/pkg/tools/integration/web_test.go b/pkg/tools/integration/web_test.go index d47d8e7c9..ba6b3da45 100644 --- a/pkg/tools/integration/web_test.go +++ b/pkg/tools/integration/web_test.go @@ -1778,11 +1778,6 @@ func TestApplySogouRangeHint(t *testing.T) { } func TestPrefersDuckDuckGoQuery(t *testing.T) { - SetPreferredWebSearchLanguage("") - t.Cleanup(func() { - SetPreferredWebSearchLanguage("") - }) - tests := []struct { name string query string @@ -1805,19 +1800,9 @@ func TestPrefersDuckDuckGoQuery(t *testing.T) { } } -func TestPrefersDuckDuckGoQuery_FallsBackToPreferredLanguage(t *testing.T) { - SetPreferredWebSearchLanguage("en") - t.Cleanup(func() { - SetPreferredWebSearchLanguage("") - }) - - if !prefersDuckDuckGoQuery("2026 04 15") { - t.Fatal("numeric query should prefer DuckDuckGo when preferred language is English") - } - - SetPreferredWebSearchLanguage("zh") +func TestPrefersDuckDuckGoQuery_DoesNotUseGlobalLanguageFallback(t *testing.T) { if prefersDuckDuckGoQuery("2026 04 15") { - t.Fatal("numeric query should prefer Sogou when preferred language is Chinese") + t.Fatal("numeric query should default to Sogou when no script-specific hint is present") } } diff --git a/pkg/tools/integration_facade.go b/pkg/tools/integration_facade.go index b05a22fe2..193ecd6f5 100644 --- a/pkg/tools/integration_facade.go +++ b/pkg/tools/integration_facade.go @@ -65,14 +65,6 @@ func NewAPIKeyPool(keys []string) *APIKeyPool { return integrationtools.NewAPIKeyPool(keys) } -func SetPreferredWebSearchLanguage(lang string) { - integrationtools.SetPreferredWebSearchLanguage(lang) -} - -func GetPreferredWebSearchLanguage() string { - return integrationtools.GetPreferredWebSearchLanguage() -} - func WebSearchToolOptionsFromConfig(cfg *config.Config) WebSearchToolOptions { return integrationtools.WebSearchToolOptionsFromConfig(cfg) } diff --git a/web/backend/api/router.go b/web/backend/api/router.go index f4ac78ab4..76f63607e 100644 --- a/web/backend/api/router.go +++ b/web/backend/api/router.go @@ -89,7 +89,6 @@ func (h *Handler) RegisterRoutes(mux *http.ServeMux) { // Skills and tools support/actions h.registerSkillRoutes(mux) h.registerToolRoutes(mux) - h.registerUIRoutes(mux) // OS startup / launch-at-login h.registerStartupRoutes(mux) diff --git a/web/backend/api/tools_test.go b/web/backend/api/tools_test.go index ffeae9b64..c98067e41 100644 --- a/web/backend/api/tools_test.go +++ b/web/backend/api/tools_test.go @@ -9,7 +9,6 @@ import ( "testing" "github.com/sipeed/picoclaw/pkg/config" - picotools "github.com/sipeed/picoclaw/pkg/tools" ) func TestHandleListTools(t *testing.T) { @@ -517,22 +516,12 @@ func TestResolveCurrentWebSearchProvider_FallsBackWhenProviderIsUnknown(t *testi } } -func TestResolveCurrentWebSearchProvider_UsesPreferredLanguageForSogouAndDuckDuckGo(t *testing.T) { +func TestResolveCurrentWebSearchProvider_PrefersStableDefaultForSogouAndDuckDuckGo(t *testing.T) { cfg := config.DefaultConfig() cfg.Tools.Web.Provider = "auto" cfg.Tools.Web.Sogou.Enabled = true cfg.Tools.Web.DuckDuckGo.Enabled = true - picotools.SetPreferredWebSearchLanguage("en") - t.Cleanup(func() { - picotools.SetPreferredWebSearchLanguage("") - }) - - if got := resolveCurrentWebSearchProvider(cfg); got != "duckduckgo" { - t.Fatalf("resolveCurrentWebSearchProvider() = %q, want duckduckgo", got) - } - - picotools.SetPreferredWebSearchLanguage("zh") if got := resolveCurrentWebSearchProvider(cfg); got != "sogou" { t.Fatalf("resolveCurrentWebSearchProvider() = %q, want sogou", got) } diff --git a/web/backend/api/ui.go b/web/backend/api/ui.go deleted file mode 100644 index 90d96403e..000000000 --- a/web/backend/api/ui.go +++ /dev/null @@ -1,27 +0,0 @@ -package api - -import ( - "encoding/json" - "net/http" - - "github.com/sipeed/picoclaw/pkg/tools" -) - -type uiLanguageRequest struct { - Language string `json:"language"` -} - -func (h *Handler) registerUIRoutes(mux *http.ServeMux) { - mux.HandleFunc("POST /api/ui/language", h.handleSetUILanguage) -} - -func (h *Handler) handleSetUILanguage(w http.ResponseWriter, r *http.Request) { - var req uiLanguageRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "invalid request body", http.StatusBadRequest) - return - } - - tools.SetPreferredWebSearchLanguage(req.Language) - w.WriteHeader(http.StatusNoContent) -} diff --git a/web/backend/api/ui_test.go b/web/backend/api/ui_test.go deleted file mode 100644 index 3de35b7cb..000000000 --- a/web/backend/api/ui_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package api - -import ( - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/sipeed/picoclaw/pkg/tools" -) - -func TestHandleSetUILanguage(t *testing.T) { - tools.SetPreferredWebSearchLanguage("") - t.Cleanup(func() { - tools.SetPreferredWebSearchLanguage("") - }) - - h := NewHandler("") - mux := http.NewServeMux() - h.RegisterRoutes(mux) - - rec := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodPost, "/api/ui/language", strings.NewReader(`{"language":"zh"}`)) - req.Header.Set("Content-Type", "application/json") - mux.ServeHTTP(rec, req) - - if rec.Code != http.StatusNoContent { - t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusNoContent, rec.Body.String()) - } - if got := tools.GetPreferredWebSearchLanguage(); got != "zh" { - t.Fatalf("preferred web search language = %q, want zh", got) - } -} - -func TestHandleSetUILanguage_RejectsInvalidJSON(t *testing.T) { - h := NewHandler("") - mux := http.NewServeMux() - h.RegisterRoutes(mux) - - rec := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodPost, "/api/ui/language", strings.NewReader(`{`)) - req.Header.Set("Content-Type", "application/json") - mux.ServeHTTP(rec, req) - - if rec.Code != http.StatusBadRequest { - t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest) - } -} diff --git a/web/backend/main.go b/web/backend/main.go index f5362174b..fa2448d5c 100644 --- a/web/backend/main.go +++ b/web/backend/main.go @@ -29,7 +29,6 @@ import ( "github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/logger" "github.com/sipeed/picoclaw/pkg/netbind" - "github.com/sipeed/picoclaw/pkg/tools" "github.com/sipeed/picoclaw/web/backend/api" "github.com/sipeed/picoclaw/web/backend/dashboardauth" "github.com/sipeed/picoclaw/web/backend/launcherconfig" @@ -409,7 +408,6 @@ func main() { if *lang != "" { SetLanguage(*lang) } - tools.SetPreferredWebSearchLanguage(string(GetLanguage())) // Resolve config path configPath := utils.GetDefaultConfigPath() diff --git a/web/frontend/src/i18n/index.ts b/web/frontend/src/i18n/index.ts index 5c3a26d48..bdc1fe917 100644 --- a/web/frontend/src/i18n/index.ts +++ b/web/frontend/src/i18n/index.ts @@ -7,8 +7,6 @@ import i18n from "i18next" import LanguageDetector from "i18next-browser-languagedetector" import { initReactI18next } from "react-i18next" -import { launcherFetch } from "@/api/http" - import en from "./locales/en.json" import zh from "./locales/zh.json" @@ -46,14 +44,6 @@ i18n.on("languageChanged", (lng) => { } else { dayjs.locale("en") } - - void launcherFetch("/api/ui/language", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ language: lng }), - }).catch(() => { - // Keep UI language changes responsive even if backend sync fails. - }) }) export default i18n From f4dbac0dcf000c3eeb37d67db7211ba59749a1a6 Mon Sep 17 00:00:00 2001 From: afjcjsbx Date: Fri, 24 Apr 2026 07:47:26 +0200 Subject: [PATCH 14/36] fix(mcp): expand home paths for local stdio server commands --- cmd/picoclaw/internal/mcp/add.go | 3 +++ cmd/picoclaw/internal/mcp/command_test.go | 22 ++++++++++++++++++++++ pkg/mcp/manager.go | 20 +++++++++++++++++++- pkg/mcp/manager_test.go | 16 ++++++++++++++++ 4 files changed, 60 insertions(+), 1 deletion(-) diff --git a/cmd/picoclaw/internal/mcp/add.go b/cmd/picoclaw/internal/mcp/add.go index 57e8f5368..87917095e 100644 --- a/cmd/picoclaw/internal/mcp/add.go +++ b/cmd/picoclaw/internal/mcp/add.go @@ -223,6 +223,9 @@ func buildServerConfig(target string, args []string, opts addOptions) (config.MC if err := validateLocalCommandPath(target); err != nil { return config.MCPServerConfig{}, err } + if isLocalCommandPath(command) { + command = expandHomePath(command) + } server.Command = command server.Args = commandArgs diff --git a/cmd/picoclaw/internal/mcp/command_test.go b/cmd/picoclaw/internal/mcp/command_test.go index 6fd8a6f58..9f8d822e4 100644 --- a/cmd/picoclaw/internal/mcp/command_test.go +++ b/cmd/picoclaw/internal/mcp/command_test.go @@ -152,6 +152,28 @@ func TestMCPAddRejectsNonExecutableLocalCommand(t *testing.T) { assert.Contains(t, err.Error(), "not executable") } +func TestMCPAddExpandsHomeInSavedLocalCommand(t *testing.T) { + configPath := setupMCPConfigEnv(t) + + homeDir := t.TempDir() + t.Setenv("HOME", homeDir) + t.Setenv("USERPROFILE", homeDir) + + localCmd := filepath.Join(homeDir, "bin", "my-mcp") + require.NoError(t, os.MkdirAll(filepath.Dir(localCmd), 0o755)) + require.NoError(t, os.WriteFile(localCmd, []byte("#!/bin/sh\nexit 0\n"), 0o755)) + + tildeCmd := "~" + string(os.PathSeparator) + filepath.Join("bin", "my-mcp") + + cmd := NewMCPCommand() + _, err := executeCommand(cmd, []string{"add", "local-home", tildeCmd}, "") + require.NoError(t, err) + + cfg := readMCPConfig(t, configPath) + server := cfg.Tools.MCP.Servers["local-home"] + assert.Equal(t, localCmd, server.Command) +} + func TestMCPAddShowsClearErrorForRemoteURLWithoutTransport(t *testing.T) { setupMCPConfigEnv(t) diff --git a/pkg/mcp/manager.go b/pkg/mcp/manager.go index f589f82a9..e28388827 100644 --- a/pkg/mcp/manager.go +++ b/pkg/mcp/manager.go @@ -25,6 +25,24 @@ type headerTransport struct { headers map[string]string } +func expandHomeCommandPath(command string) string { + if command == "" || command[0] != '~' { + return command + } + + home, err := os.UserHomeDir() + if err != nil { + return command + } + if command == "~" { + return home + } + if strings.HasPrefix(command, "~/") || strings.HasPrefix(command, "~\\") { + return filepath.Join(home, command[2:]) + } + return command +} + func (t *headerTransport) RoundTrip(req *http.Request) (*http.Response, error) { // Clone the request to avoid modifying the original req = req.Clone(req.Context()) @@ -324,7 +342,7 @@ func (m *Manager) ConnectServer( "command": cfg.Command, }) // Create command with context - cmd := exec.CommandContext(ctx, cfg.Command, cfg.Args...) + cmd := exec.CommandContext(ctx, expandHomeCommandPath(cfg.Command), cfg.Args...) // Build environment variables with proper override semantics // Use a map to ensure config variables override file variables diff --git a/pkg/mcp/manager_test.go b/pkg/mcp/manager_test.go index f353942ab..fff315655 100644 --- a/pkg/mcp/manager_test.go +++ b/pkg/mcp/manager_test.go @@ -136,6 +136,22 @@ func TestLoadEnvFileNotFound(t *testing.T) { } } +func TestExpandHomeCommandPath(t *testing.T) { + homeDir := t.TempDir() + t.Setenv("HOME", homeDir) + t.Setenv("USERPROFILE", homeDir) + + want := filepath.Join(homeDir, "bin", "my-mcp") + got := expandHomeCommandPath("~" + string(os.PathSeparator) + filepath.Join("bin", "my-mcp")) + if got != want { + t.Fatalf("expandHomeCommandPath() = %q, want %q", got, want) + } + + if got := expandHomeCommandPath("npx"); got != "npx" { + t.Fatalf("expandHomeCommandPath() should leave bare commands unchanged, got %q", got) + } +} + func TestEnvFilePriority(t *testing.T) { // Create a temporary .env file tmpDir := t.TempDir() From f334ac6d0113a79b4003b6b8f5965f9248e76216 Mon Sep 17 00:00:00 2001 From: BeaconCat <111232138+BeaconCat@users.noreply.github.com> Date: Fri, 24 Apr 2026 15:26:34 +0800 Subject: [PATCH 15/36] fix: treat PID=1 as stale in PID file singleton check, fix govet shadow, add .gitattributes (#2642) - pid: When a container stops and leaves behind a PID file with PID 1 on a shared volume, the host's init process (PID 1) passes the isProcessRunning check, blocking new gateway starts. Treat recorded PID 1 as always stale in both WritePidFile and ReadPidFileWithCheck. Added unit tests covering the PID=1 container leftover scenario. - isolation: Fix govet shadow warning on platform_windows.go line 105 where := shadows the outer err variable. Changed to = assignment. - gitattributes: Enforce LF line endings for shell scripts to prevent CRLF issues when checking out on Windows (breaks Docker entrypoint). Co-authored-by: BeaconCat --- .gitattributes | 3 +++ docker/entrypoint.sh | 6 +++++ pkg/isolation/platform_windows.go | 2 +- pkg/pid/pidfile.go | 15 +++++++++++- pkg/pid/pidfile_test.go | 40 +++++++++++++++++++++++++++++++ 5 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..3d72ace94 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# Ensure shell scripts always use LF line endings regardless of OS. +*.sh text eol=lf +docker/entrypoint.sh text eol=lf diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index b6fc724b5..6fafb5150 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -12,4 +12,10 @@ if [ ! -d "${HOME}/.picoclaw/workspace" ] && [ ! -f "${HOME}/.picoclaw/config.js exit 0 fi +# Remove stale PID file from a previous container run. +# After docker kill / OOM / crash the PID file may linger on the bind-mounted +# volume and block the next gateway start (the recorded PID could collide with +# an unrelated process inside the new container). +rm -f "${HOME}/.picoclaw/.picoclaw.pid" + exec picoclaw gateway "$@" diff --git a/pkg/isolation/platform_windows.go b/pkg/isolation/platform_windows.go index 9b39c85cf..1b3be8bd3 100644 --- a/pkg/isolation/platform_windows.go +++ b/pkg/isolation/platform_windows.go @@ -102,7 +102,7 @@ func postStartPlatformIsolation(cmd *exec.Cmd, isolation config.IsolationConfig, return fmt.Errorf("open process for job assignment: %w", err) } - if err := windows.AssignProcessToJobObject(job, proc); err != nil { + if err = windows.AssignProcessToJobObject(job, proc); err != nil { _ = windows.CloseHandle(proc) _ = windows.CloseHandle(job) if resources.token != 0 { diff --git a/pkg/pid/pidfile.go b/pkg/pid/pidfile.go index f7c1f42b2..00601195f 100644 --- a/pkg/pid/pidfile.go +++ b/pkg/pid/pidfile.go @@ -58,7 +58,12 @@ func WritePidFile(homePath, host string, port int) (*PidFileData, error) { if data, err := readPidFileUnlocked(pidPath); err == nil { if os.Getpid() != data.PID { logger.Infof("found pid file (PID: %d, version: %s)", data.PID, data.Version) - if isProcessRunning(data.PID) { + // PID 1 is typically init/systemd on the host or the entrypoint + // inside a container. When a container stops and leaves behind a + // PID file on a shared volume, the host's PID 1 (init) would + // pass the isProcessRunning check, blocking new gateway starts. + // Treat recorded PID 1 as always stale. + if data.PID != 1 && isProcessRunning(data.PID) { return nil, fmt.Errorf("gateway is already running (PID: %d, version: %s)", data.PID, data.Version) } logger.Warnf("not running (PID: %d) so will remove the pid file: %s", data.PID, pidPath) @@ -124,6 +129,14 @@ func ReadPidFileWithCheck(homePath string) *PidFileData { return nil } + // Treat PID 1 as stale when we are not PID 1 ourselves (container + // leftover on a shared volume — host PID 1 is init, not gateway). + if data.PID == 1 && os.Getpid() != 1 { + logger.Debugf("stale container PID 1, remove pid file: %s", pidPath) + os.Remove(pidPath) + return nil + } + if !isProcessRunning(data.PID) { logger.Debugf("process not running, remove pid file: %s", pidPath) os.Remove(pidPath) diff --git a/pkg/pid/pidfile_test.go b/pkg/pid/pidfile_test.go index 2da44bbbc..2d3c11f63 100644 --- a/pkg/pid/pidfile_test.go +++ b/pkg/pid/pidfile_test.go @@ -278,6 +278,46 @@ func TestRemovePidFileIfPIDMismatch(t *testing.T) { } } +// TestWritePidFileContainerPID1 verifies that a leftover PID file with PID 1 +// (typical container entrypoint) is treated as stale and overwritten. +func TestWritePidFileContainerPID1(t *testing.T) { + dir := tmpDir(t) + + stale := PidFileData{PID: 1, Token: "deadbeef12345678deadbeef12345678"} + raw, _ := json.MarshalIndent(stale, "", " ") + os.WriteFile(filepath.Join(dir, pidFileName), raw, 0o600) + + data, err := WritePidFile(dir, "127.0.0.1", 18790) + if err != nil { + t.Fatalf("WritePidFile should treat PID 1 as stale, got error: %v", err) + } + if data.PID != os.Getpid() { + t.Errorf("PID = %d, want %d", data.PID, os.Getpid()) + } +} + +// TestReadPidFileWithCheckContainerPID1 verifies that a leftover PID file +// with PID 1 is treated as stale and cleaned up. +func TestReadPidFileWithCheckContainerPID1(t *testing.T) { + if os.Getpid() == 1 { + t.Skip("test not meaningful when running as PID 1") + } + dir := tmpDir(t) + + stale := PidFileData{PID: 1, Token: "deadbeef12345678deadbeef12345678"} + raw, _ := json.MarshalIndent(stale, "", " ") + os.WriteFile(filepath.Join(dir, pidFileName), raw, 0o600) + + data := ReadPidFileWithCheck(dir) + if data != nil { + t.Error("expected nil for PID 1 leftover") + } + + if _, err := os.Stat(filepath.Join(dir, pidFileName)); !os.IsNotExist(err) { + t.Error("PID 1 leftover file should be removed") + } +} + // TestReadPidFileUnlockedInvalidJSON returns error for malformed content. func TestReadPidFileUnlockedInvalidJSON(t *testing.T) { dir := tmpDir(t) From 07032df0378fef239cfd038aede4b955234c9d40 Mon Sep 17 00:00:00 2001 From: afjcjsbx Date: Fri, 24 Apr 2026 10:32:55 +0200 Subject: [PATCH 16/36] fix(mcp): normalize local command paths and document env-file usage --- README.md | 3 +- cmd/picoclaw/internal/mcp/add.go | 16 ++++++++- cmd/picoclaw/internal/mcp/command_test.go | 40 +++++++++++++++++++++++ docs/project/README.it.md | 3 +- docs/reference/mcp-cli.md | 26 +++++++++++---- 5 files changed, 78 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 9668fcf34..73cc877fa 100644 --- a/README.md +++ b/README.md @@ -581,7 +581,8 @@ picoclaw mcp test filesystem `picoclaw mcp` is a configuration manager: it updates `config.json` under `tools.mcp.servers`, but it does not keep the server process running itself. -Use `picoclaw mcp edit` when you need advanced fields such as `headers`, `env_file`, or `deferred`. +Use `picoclaw mcp edit` when you need advanced fields that are not covered by `picoclaw mcp add`. +For example, `picoclaw mcp add` supports `--deferred` and `--env-file`, while `picoclaw mcp edit` is still useful for direct JSON editing and uncommon MCP settings. For full MCP configuration (stdio, SSE, HTTP transports, Tool Discovery), see [Tools Configuration - MCP](docs/reference/tools_configuration.md#mcp-tool). For CLI usage and examples, see [MCP Server CLI](docs/reference/mcp-cli.md). diff --git a/cmd/picoclaw/internal/mcp/add.go b/cmd/picoclaw/internal/mcp/add.go index 87917095e..8ad68571f 100644 --- a/cmd/picoclaw/internal/mcp/add.go +++ b/cmd/picoclaw/internal/mcp/add.go @@ -12,6 +12,7 @@ import ( type addOptions struct { Env []string + EnvFile string Headers []string Transport string Force bool @@ -70,7 +71,8 @@ func newAddCommand() *cobra.Command { } flags := cmd.Flags() - flags.StringArrayP("env", "e", nil, "Environment variable in KEY=value format (repeatable)") + flags.StringArrayP("env", "e", nil, "Environment variable in KEY=value format (repeatable, saved to config)") + flags.String("env-file", "", "Path to an env file for stdio servers (recommended for secrets)") flags.StringArrayP("header", "H", nil, "HTTP header in 'Name: Value' or 'Name=Value' format (repeatable)") flags.StringP("transport", "t", "stdio", "Transport type: stdio, http, or sse") flags.BoolP("force", "f", false, "Overwrite an existing server without prompting") @@ -119,8 +121,16 @@ func parseAddArgs(args []string) (addOptions, string, string, []string, bool, er } i++ opts.Env = append(opts.Env, args[i]) + case arg == "--env-file": + if i+1 >= len(args) { + return addOptions{}, "", "", nil, false, fmt.Errorf("missing value for %s", arg) + } + i++ + opts.EnvFile = args[i] case strings.HasPrefix(arg, "--env="): opts.Env = append(opts.Env, strings.TrimPrefix(arg, "--env=")) + case strings.HasPrefix(arg, "--env-file="): + opts.EnvFile = strings.TrimPrefix(arg, "--env-file=") case arg == "--header" || arg == "-H": if i+1 >= len(args) { return addOptions{}, "", "", nil, false, fmt.Errorf("missing value for %s", arg) @@ -193,6 +203,9 @@ func buildServerConfig(target string, args []string, opts addOptions) (config.MC if len(env) > 0 { return config.MCPServerConfig{}, fmt.Errorf("--env can only be used with stdio transport") } + if strings.TrimSpace(opts.EnvFile) != "" { + return config.MCPServerConfig{}, fmt.Errorf("--env-file can only be used with stdio transport") + } if len(args) > 0 { return config.MCPServerConfig{}, fmt.Errorf("%s transport does not accept command arguments", transport) } @@ -230,6 +243,7 @@ func buildServerConfig(target string, args []string, opts addOptions) (config.MC server.Command = command server.Args = commandArgs server.Env = env + server.EnvFile = strings.TrimSpace(opts.EnvFile) return server, nil } diff --git a/cmd/picoclaw/internal/mcp/command_test.go b/cmd/picoclaw/internal/mcp/command_test.go index 9f8d822e4..be1c9763e 100644 --- a/cmd/picoclaw/internal/mcp/command_test.go +++ b/cmd/picoclaw/internal/mcp/command_test.go @@ -139,6 +139,46 @@ func TestMCPAddSupportsExplicitStdioCommandAfterSeparator(t *testing.T) { assert.Equal(t, map[string]string{"AIRTABLE_API_KEY": "YOUR_KEY"}, server.Env) } +func TestMCPAddSupportsEnvFileForStdio(t *testing.T) { + configPath := setupMCPConfigEnv(t) + + cmd := NewMCPCommand() + _, err := executeCommand(cmd, []string{ + "add", + "--env-file", + ".env.mcp", + "filesystem", + "npx", + "-y", + "@modelcontextprotocol/server-filesystem", + }, "") + require.NoError(t, err) + + cfg := readMCPConfig(t, configPath) + server := cfg.Tools.MCP.Servers["filesystem"] + assert.Equal(t, "stdio", server.Type) + assert.Equal(t, "npx", server.Command) + assert.Equal(t, []string{"-y", "@modelcontextprotocol/server-filesystem"}, server.Args) + assert.Equal(t, ".env.mcp", server.EnvFile) +} + +func TestMCPAddRejectsEnvFileForHTTP(t *testing.T) { + setupMCPConfigEnv(t) + + cmd := NewMCPCommand() + _, err := executeCommand(cmd, []string{ + "add", + "--transport", + "http", + "--env-file", + ".env.mcp", + "context7", + "https://mcp.context7.com/mcp", + }, "") + require.Error(t, err) + assert.Contains(t, err.Error(), "--env-file can only be used with stdio transport") +} + func TestMCPAddRejectsNonExecutableLocalCommand(t *testing.T) { setupMCPConfigEnv(t) diff --git a/docs/project/README.it.md b/docs/project/README.it.md index cd7ecac7d..b3db6fece 100644 --- a/docs/project/README.it.md +++ b/docs/project/README.it.md @@ -564,7 +564,8 @@ picoclaw mcp test filesystem `picoclaw mcp` agisce come configuration manager: aggiorna `config.json` sotto `tools.mcp.servers`, ma non mantiene in esecuzione il processo del server. -Usa `picoclaw mcp edit` quando ti servono campi avanzati come `headers`, `env_file` o `deferred`. +Usa `picoclaw mcp edit` quando ti servono campi avanzati che non sono coperti da `picoclaw mcp add`. +Per esempio, `picoclaw mcp add` supporta `--deferred` e `--env-file`, mentre `picoclaw mcp edit` resta utile per modifiche JSON dirette e opzioni MCP meno comuni. Per la configurazione MCP completa (trasporti stdio, SSE, HTTP, Tool Discovery), vedi [Configurazione degli Strumenti - MCP](../reference/tools_configuration.md#mcp-tool). Per la reference della CLI, vedi [MCP Server CLI](../reference/mcp-cli.md). diff --git a/docs/reference/mcp-cli.md b/docs/reference/mcp-cli.md index 77291b140..18b2b4c1c 100644 --- a/docs/reference/mcp-cli.md +++ b/docs/reference/mcp-cli.md @@ -36,12 +36,18 @@ Add a stdio server via `npx`: picoclaw mcp add filesystem -- npx -y @modelcontextprotocol/server-filesystem /tmp ``` -Add a stdio server with environment variables: +Add a stdio server with environment variables saved in config: ```bash picoclaw mcp add github --env GITHUB_PERSONAL_ACCESS_TOKEN=ghp_xxx -- npx -y @modelcontextprotocol/server-github ``` +Add a stdio server using an env file for secrets: + +```bash +picoclaw mcp add github --env-file .env.github -- npx -y @modelcontextprotocol/server-github +``` + Add a remote HTTP server: ```bash @@ -108,7 +114,8 @@ Supported flags: | Flag | Meaning | |------|---------| -| `--env`, `-e` | Add a stdio environment variable in `KEY=value` format. Repeatable. | +| `--env`, `-e` | Add a stdio environment variable in `KEY=value` format. Repeatable. Values are saved to config. | +| `--env-file` | Attach an env file path to a stdio server. Recommended for secrets you do not want stored inline in `config.json`. | | `--header`, `-H` | Add an HTTP header in `Name: Value` or `Name=Value` format. Repeatable. | | `--transport`, `-t` | Transport type: `stdio` (default), `http`, or `sse`. | | `--force`, `-f` | Overwrite an existing server entry without confirmation. | @@ -131,6 +138,11 @@ Parsing behavior: - use the `--` separator when the stdio command itself has arguments that may look like PicoClaw CLI flags - without `--`, PicoClaw treats the first two non-flag tokens as `` and `` +Secret handling: + +- `--env KEY=value` stores the resolved value directly in `config.json` +- use `--env-file` instead when the value is sensitive and should stay outside the main config file + Example: ```bash @@ -182,6 +194,7 @@ For `stdio`: - `` is treated as the command - `[args...]` are stored in `args` - `--env` is supported +- `--env-file` is supported and stored in `env_file` - `--header` is rejected - `-- [args...]` is supported and recommended for unambiguous parsing @@ -190,6 +203,7 @@ For `http` / `sse`: - `` must be a valid URL - extra command args are rejected - `--env` is rejected +- `--env-file` is rejected - `--header` is supported and stored in `headers` Overwrite behavior: @@ -322,9 +336,7 @@ picoclaw mcp edit This opens the config file in the editor pointed to by `$EDITOR`. -Use it when you need to configure MCP fields that are not exposed directly by `picoclaw mcp add`, such as: - -- `env_file` +Use it when you need to configure MCP fields that are not exposed directly by `picoclaw mcp add`. If `$EDITOR` is not set, the command fails with an explicit error. @@ -337,10 +349,10 @@ For common cases: 3. Check all servers at a glance with `picoclaw mcp list --status`. 4. Start PicoClaw normally so the configured MCP server is loaded by the host. -For advanced cases (e.g. `env_file`): +For advanced cases: 1. Add the base entry with `picoclaw mcp add`. -2. Run `picoclaw mcp edit` to fill in `env_file` or other fields not exposed as CLI flags. +2. Run `picoclaw mcp edit` to fill in fields that are not exposed as CLI flags. 3. Run `picoclaw mcp show ` to confirm the final configuration and tool list. ## Related Docs From ccd19a48ce5cc22c578239a73d3eea61bd6d5d4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=82=86=E6=9C=88?= <2835601846@qq.com> Date: Fri, 24 Apr 2026 17:50:59 +0800 Subject: [PATCH 17/36] Fix Windows build flow (#2487) * Fix Windows build flow * build(makefile): make windows recipes shell-safe - avoid backslash line-continuation in Windows build-launcher recipe - replace cmd-specific if-not-exist with PowerShell check in web build-frontend * Fix Windows build flow * build(makefile): make windows recipes shell-safe - avoid backslash line-continuation in Windows build-launcher recipe - replace cmd-specific if-not-exist with PowerShell check in web build-frontend * build(web): avoid shell-expanding powershell vars in windows recipe - rewrite build-frontend Windows command without PowerShell local vars - keep install-stamp hash check logic --- .gitignore | 4 ++ Makefile | 92 +++++++++++++++++++++--- cmd/picoclaw/internal/onboard/command.go | 2 +- scripts/copydir.go | 84 ++++++++++++++++++++++ web/Makefile | 90 ++++++++++++++++++++--- 5 files changed, 253 insertions(+), 19 deletions(-) create mode 100644 scripts/copydir.go diff --git a/.gitignore b/.gitignore index 135867842..e1736f56b 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,10 @@ dist/ # Windows Application Icon/Resource *.syso +.cache/ +web/frontend/.pnpm-store/ +_tmp_* +web/frontend/_tmp_* # Test telegram integration cmd/telegram/ diff --git a/Makefile b/Makefile index c5d691c29..acb258370 100644 --- a/Makefile +++ b/Makefile @@ -7,19 +7,43 @@ CMD_DIR=cmd/$(BINARY_NAME) MAIN_GO=$(CMD_DIR)/main.go EXT= +ifeq ($(OS),Windows_NT) + POWERSHELL=powershell -NoProfile -Command + WINDOWS_GOARCH_RAW:=$(strip $(shell go env GOARCH 2>NUL)) +endif + # Version -VERSION?=$(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") -GIT_COMMIT=$(shell git rev-parse --short=8 HEAD 2>/dev/null || echo "dev") -BUILD_TIME=$(shell date +%FT%T%z) -GO_VERSION=$(shell $(GO) version | awk '{print $$3}') +ifeq ($(OS),Windows_NT) + VERSION_RAW:=$(strip $(shell git describe --tags --always --dirty 2>NUL)) + GIT_COMMIT_RAW:=$(strip $(shell git rev-parse --short=8 HEAD 2>NUL)) + BUILD_TIME_RAW:=$(strip $(shell powershell -NoProfile -Command "Get-Date -Format 'yyyy-MM-ddTHH:mm:ssK'")) + GO_VERSION_RAW:=$(strip $(shell go env GOVERSION 2>NUL)) +else + VERSION_RAW:=$(strip $(shell git describe --tags --always --dirty 2>/dev/null)) + GIT_COMMIT_RAW:=$(strip $(shell git rev-parse --short=8 HEAD 2>/dev/null)) + BUILD_TIME_RAW:=$(strip $(shell date +%FT%T%z)) + GO_VERSION_RAW:=$(strip $(shell go env GOVERSION 2>/dev/null)) +endif +VERSION?=$(if $(VERSION_RAW),$(VERSION_RAW),dev) +GIT_COMMIT=$(if $(GIT_COMMIT_RAW),$(GIT_COMMIT_RAW),dev) +BUILD_TIME=$(if $(BUILD_TIME_RAW),$(BUILD_TIME_RAW),dev) +GO_VERSION=$(if $(GO_VERSION_RAW),$(GO_VERSION_RAW),unknown) CONFIG_PKG=github.com/sipeed/picoclaw/pkg/config LDFLAGS=-X $(CONFIG_PKG).Version=$(VERSION) -X $(CONFIG_PKG).GitCommit=$(GIT_COMMIT) -X $(CONFIG_PKG).BuildTime=$(BUILD_TIME) -X $(CONFIG_PKG).GoVersion=$(GO_VERSION) -s -w # Go variables -GO?=CGO_ENABLED=0 go +GO?=go WEB_GO?=$(GO) +CGO_ENABLED?=0 GO_BUILD_TAGS?=goolm,stdjson GOFLAGS?=-v -tags $(GO_BUILD_TAGS) +GOCACHE?=$(CURDIR)/.cache/go-build +GOMODCACHE?=$(CURDIR)/.cache/go-mod +GOTOOLCHAIN?=local +export CGO_ENABLED +export GOCACHE +export GOMODCACHE +export GOTOOLCHAIN comma:=, empty:= space:=$(empty) $(empty) @@ -73,8 +97,21 @@ BUILTIN_SKILLS_DIR=$(CURDIR)/skills LNCMD=ln -sf # OS detection -UNAME_S?=$(shell uname -s) -UNAME_M?=$(shell uname -m) +ifeq ($(OS),Windows_NT) + UNAME_S=Windows + ifeq ($(WINDOWS_GOARCH_RAW),amd64) + UNAME_M=x86_64 + else ifeq ($(WINDOWS_GOARCH_RAW),arm64) + UNAME_M=arm64 + else ifeq ($(WINDOWS_GOARCH_RAW),386) + UNAME_M=x86 + else + UNAME_M=$(if $(WINDOWS_GOARCH_RAW),$(WINDOWS_GOARCH_RAW),x86_64) + endif +else + UNAME_S?=$(shell uname -s) + UNAME_M?=$(shell uname -m) +endif # Platform-specific settings ifeq ($(UNAME_S),Linux) @@ -122,6 +159,18 @@ else endif +ifeq ($(OS),Windows_NT) + PLATFORM=windows + ifeq ($(UNAME_M),x86_64) + ARCH?=amd64 + else ifeq ($(UNAME_M),arm64) + ARCH?=arm64 + else + ARCH?=$(UNAME_M) + endif + EXT=.exe +endif + BINARY_PATH=$(BUILD_DIR)/$(BINARY_NAME)-$(PLATFORM)-$(ARCH) # Default target @@ -130,21 +179,37 @@ all: build ## generate: Run generate generate: @echo "Run generate..." +ifeq ($(OS),Windows_NT) + @$(POWERSHELL) "if (Test-Path -LiteralPath './$(CMD_DIR)/workspace') { Remove-Item -LiteralPath './$(CMD_DIR)/workspace' -Recurse -Force }" +else @rm -r ./$(CMD_DIR)/workspace 2>/dev/null || true +endif @$(GO) generate ./... @echo "Run generate complete" ## build: Build the picoclaw binary for current platform build: generate @echo "Building $(BINARY_NAME)$(EXT) for $(PLATFORM)/$(ARCH)..." +ifeq ($(OS),Windows_NT) + @$(POWERSHELL) "New-Item -ItemType Directory -Force -Path '$(BUILD_DIR)' | Out-Null" + @$(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BINARY_PATH)$(EXT) ./$(CMD_DIR) + @$(POWERSHELL) "Copy-Item -LiteralPath '$(BINARY_PATH)$(EXT)' -Destination '$(BUILD_DIR)/$(BINARY_NAME)$(EXT)' -Force" +else @mkdir -p $(BUILD_DIR) @GOARCH=${ARCH} $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BINARY_PATH)$(EXT) ./$(CMD_DIR) @echo "Build complete: $(BINARY_PATH)$(EXT)" @$(LNCMD) $(BINARY_NAME)-$(PLATFORM)-$(ARCH)$(EXT) $(BUILD_DIR)/$(BINARY_NAME)$(EXT) +endif + @echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME)$(EXT)" ## build-launcher: Build the picoclaw-launcher (web console) binary build-launcher: @echo "Building picoclaw-launcher for $(PLATFORM)/$(ARCH)..." +ifeq ($(OS),Windows_NT) + @$(POWERSHELL) "New-Item -ItemType Directory -Force -Path '$(BUILD_DIR)' | Out-Null" + @$(MAKE) -C web build PLATFORM="$(PLATFORM)" ARCH="$(ARCH)" EXT="$(EXT)" OUTPUT="$(CURDIR)/$(BUILD_DIR)/picoclaw-launcher-$(PLATFORM)-$(ARCH)$(EXT)" GO_BUILD_TAGS="$(GO_BUILD_TAGS)" + @$(POWERSHELL) "Copy-Item -LiteralPath '$(BUILD_DIR)/picoclaw-launcher-$(PLATFORM)-$(ARCH)$(EXT)' -Destination '$(BUILD_DIR)/picoclaw-launcher$(EXT)' -Force" +else @mkdir -p $(BUILD_DIR) @GOARCH=${ARCH} $(MAKE) -C web build \ OUTPUT="$(CURDIR)/$(BUILD_DIR)/picoclaw-launcher-$(PLATFORM)-$(ARCH)$(EXT)" \ @@ -152,6 +217,7 @@ build-launcher: GO_BUILD_TAGS='$(GO_BUILD_TAGS)' \ LDFLAGS='$(LDFLAGS)' @$(LNCMD) picoclaw-launcher-$(PLATFORM)-$(ARCH)$(EXT) $(BUILD_DIR)/picoclaw-launcher$(EXT) +endif @echo "Build complete: $(BUILD_DIR)/picoclaw-launcher$(EXT)" build-launcher-frontend: @@ -160,10 +226,16 @@ build-launcher-frontend: ## build-launcher-tui: Build the picoclaw-launcher TUI binary build-launcher-tui: @echo "Building picoclaw-launcher-tui for $(PLATFORM)/$(ARCH)..." +ifeq ($(OS),Windows_NT) + @$(POWERSHELL) "New-Item -ItemType Directory -Force -Path '$(BUILD_DIR)' | Out-Null" + @$(GO) build $(GOFLAGS) -o $(BUILD_DIR)/picoclaw-launcher-tui-$(PLATFORM)-$(ARCH)$(EXT) ./cmd/picoclaw-launcher-tui + @$(POWERSHELL) "Copy-Item -LiteralPath '$(BUILD_DIR)/picoclaw-launcher-tui-$(PLATFORM)-$(ARCH)$(EXT)' -Destination '$(BUILD_DIR)/picoclaw-launcher-tui$(EXT)' -Force" +else @mkdir -p $(BUILD_DIR) @$(GO) build $(GOFLAGS) -o $(BUILD_DIR)/picoclaw-launcher-tui-$(PLATFORM)-$(ARCH) ./cmd/picoclaw-launcher-tui @ln -sf picoclaw-launcher-tui-$(PLATFORM)-$(ARCH) $(BUILD_DIR)/picoclaw-launcher-tui - @echo "Build complete: $(BUILD_DIR)/picoclaw-launcher-tui" +endif + @echo "Build complete: $(BUILD_DIR)/picoclaw-launcher-tui$(EXT)" ## build-whatsapp-native: Build with WhatsApp native (whatsmeow) support; larger binary build-whatsapp-native: generate @@ -290,7 +362,11 @@ uninstall-all: ## clean: Remove build artifacts clean: @echo "Cleaning build artifacts..." +ifeq ($(OS),Windows_NT) + @$(POWERSHELL) "if (Test-Path -LiteralPath '$(BUILD_DIR)') { Remove-Item -LiteralPath '$(BUILD_DIR)' -Recurse -Force }" +else @rm -rf $(BUILD_DIR) +endif @echo "Clean complete" ## vet: Run go vet for static analysis diff --git a/cmd/picoclaw/internal/onboard/command.go b/cmd/picoclaw/internal/onboard/command.go index 4be19b2a5..bf8f4104f 100644 --- a/cmd/picoclaw/internal/onboard/command.go +++ b/cmd/picoclaw/internal/onboard/command.go @@ -6,7 +6,7 @@ import ( "github.com/spf13/cobra" ) -//go:generate cp -r ../../../../workspace . +//go:generate go run ../../../../scripts/copydir.go ../../../../workspace ./workspace //go:embed workspace var embeddedFiles embed.FS diff --git a/scripts/copydir.go b/scripts/copydir.go new file mode 100644 index 000000000..74eff6c72 --- /dev/null +++ b/scripts/copydir.go @@ -0,0 +1,84 @@ +package main + +import ( + "fmt" + "io" + "os" + "path/filepath" +) + +func main() { + if len(os.Args) != 3 { + fmt.Fprintf(os.Stderr, "usage: go run scripts/copydir.go \n") + os.Exit(2) + } + + src := os.Args[1] + dst := os.Args[2] + + if err := os.RemoveAll(dst); err != nil { + fmt.Fprintf(os.Stderr, "remove %s: %v\n", dst, err) + os.Exit(1) + } + + if err := copyTree(src, dst); err != nil { + fmt.Fprintf(os.Stderr, "copy %s -> %s: %v\n", src, dst, err) + os.Exit(1) + } +} + +func copyTree(src, dst string) error { + info, err := os.Stat(src) + if err != nil { + return err + } + if !info.IsDir() { + return fmt.Errorf("source is not a directory: %s", src) + } + + return filepath.Walk(src, func(path string, entry os.FileInfo, walkErr error) error { + if walkErr != nil { + return walkErr + } + + rel, err := filepath.Rel(src, path) + if err != nil { + return err + } + + target := dst + if rel != "." { + target = filepath.Join(dst, rel) + } + + if entry.IsDir() { + return os.MkdirAll(target, entry.Mode()) + } + + return copyFile(path, target, entry.Mode()) + }) +} + +func copyFile(src, dst string, mode os.FileMode) error { + if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil { + return err + } + + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + + out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode) + if err != nil { + return err + } + defer out.Close() + + if _, err := io.Copy(out, in); err != nil { + return err + } + + return out.Close() +} diff --git a/web/Makefile b/web/Makefile index 4dca810e7..254c439e9 100644 --- a/web/Makefile +++ b/web/Makefile @@ -2,15 +2,24 @@ build-android-arm64 build-android-bundle # Go variables -GO?=CGO_ENABLED=0 go +GO?=go WEB_GO?=$(GO) +CGO_ENABLED?=0 GO_BUILD_TAGS?=goolm,stdjson GOFLAGS?=-v -tags $(GO_BUILD_TAGS) +GOCACHE?=$(abspath ../.cache/go-build) +GOMODCACHE?=$(abspath ../.cache/go-mod) +GOTOOLCHAIN?=local +export CGO_ENABLED +export GOCACHE +export GOMODCACHE +export GOTOOLCHAIN # Build variables BUILD_DIR=build -OUTPUT?=$(BUILD_DIR)/picoclaw-launcher -OUTPUT_ANDROID_ARM64?=$(BUILD_DIR)/picoclaw-launcher-android-arm64 +EXT= +OUTPUT?=$(BUILD_DIR)/picoclaw-launcher$(EXT) +OUTPUT_ANDROID_ARM64?=$(BUILD_DIR)/picoclaw-launcher-android-arm64$(EXT) FRONTEND_DIR=frontend FRONTEND_INSTALL_STAMP=$(FRONTEND_DIR)/node_modules/.picoclaw-install-stamp BACKEND_DIR=backend @@ -19,18 +28,47 @@ PICOCLAW_BINARY_NAME=picoclaw PICOCLAW_BINARY?=$(abspath ../build/$(PICOCLAW_BINARY_NAME)) LAUNCHER_GUI_LDFLAG= +ifeq ($(OS),Windows_NT) + POWERSHELL=powershell -NoProfile -Command + WINDOWS_GOARCH_RAW:=$(strip $(shell go env GOARCH 2>NUL)) +endif + # Version -VERSION?=$(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") -GIT_COMMIT=$(shell git rev-parse --short=8 HEAD 2>/dev/null || echo "dev") -BUILD_TIME=$(shell date +%FT%T%z) -GO_VERSION=$(shell $(WEB_GO) version | awk '{print $$3}') +ifeq ($(OS),Windows_NT) + VERSION_RAW:=$(strip $(shell git describe --tags --always --dirty 2>NUL)) + GIT_COMMIT_RAW:=$(strip $(shell git rev-parse --short=8 HEAD 2>NUL)) + BUILD_TIME_RAW:=$(strip $(shell powershell -NoProfile -Command "Get-Date -Format 'yyyy-MM-ddTHH:mm:ssK'")) + GO_VERSION_RAW:=$(strip $(shell go env GOVERSION 2>NUL)) +else + VERSION_RAW:=$(strip $(shell git describe --tags --always --dirty 2>/dev/null)) + GIT_COMMIT_RAW:=$(strip $(shell git rev-parse --short=8 HEAD 2>/dev/null)) + BUILD_TIME_RAW:=$(strip $(shell date +%FT%T%z)) + GO_VERSION_RAW:=$(strip $(shell go env GOVERSION 2>/dev/null)) +endif +VERSION?=$(if $(VERSION_RAW),$(VERSION_RAW),dev) +GIT_COMMIT=$(if $(GIT_COMMIT_RAW),$(GIT_COMMIT_RAW),dev) +BUILD_TIME=$(if $(BUILD_TIME_RAW),$(BUILD_TIME_RAW),dev) +GO_VERSION=$(if $(GO_VERSION_RAW),$(GO_VERSION_RAW),unknown) CONFIG_PKG=github.com/sipeed/picoclaw/pkg/config LDFLAGS=-X $(CONFIG_PKG).Version=$(VERSION) -X $(CONFIG_PKG).GitCommit=$(GIT_COMMIT) -X $(CONFIG_PKG).BuildTime=$(BUILD_TIME) -X $(CONFIG_PKG).GoVersion=$(GO_VERSION) -s -w # OS detection -UNAME_S:=$(shell uname -s) -UNAME_M:=$(shell uname -m) +ifeq ($(OS),Windows_NT) + UNAME_S=Windows + ifeq ($(WINDOWS_GOARCH_RAW),amd64) + UNAME_M=x86_64 + else ifeq ($(WINDOWS_GOARCH_RAW),arm64) + UNAME_M=arm64 + else ifeq ($(WINDOWS_GOARCH_RAW),386) + UNAME_M=x86 + else + UNAME_M=$(if $(WINDOWS_GOARCH_RAW),$(WINDOWS_GOARCH_RAW),x86_64) + endif +else + UNAME_S:=$(shell uname -s) + UNAME_M:=$(shell uname -m) +endif # Platform-specific settings ifeq ($(UNAME_S),Linux) @@ -62,7 +100,14 @@ else ifeq ($(UNAME_S),Darwin) endif else ifeq ($(UNAME_S),Windows) PLATFORM=windows - ARCH=$(UNAME_M) + ifeq ($(UNAME_M),x86_64) + ARCH=amd64 + else ifeq ($(UNAME_M),arm64) + ARCH=arm64 + else + ARCH=$(UNAME_M) + endif + EXT=.exe PICOCLAW_BINARY_NAME=picoclaw.exe LAUNCHER_GUI_LDFLAG=-H=windowsgui else @@ -91,21 +136,36 @@ dev-backend: # Build frontend and embed into Go binary build: build-frontend +ifeq ($(OS),Windows_NT) + @$(POWERSHELL) "New-Item -ItemType Directory -Force -Path (Split-Path -Parent '$(OUTPUT)') | Out-Null" +else @mkdir -p "$$(dirname "$(OUTPUT)")" +endif ${WEB_GO} build $(GOFLAGS) -ldflags "$(LAUNCHER_LDFLAGS)" -o "$(OUTPUT)" ./$(BACKEND_DIR)/ # Build launcher for Android ARM64 (frontend must already be built) build-android-arm64: build-frontend +ifeq ($(OS),Windows_NT) + @$(POWERSHELL) "New-Item -ItemType Directory -Force -Path '$(BUILD_DIR)' | Out-Null" +else @mkdir -p $(BUILD_DIR) +endif GOOS=android GOARCH=arm64 $(GO) build -tags stdjson -ldflags "$(LDFLAGS)" -o "$(OUTPUT_ANDROID_ARM64)" ./$(BACKEND_DIR)/ # Build launcher for all Android architectures build-android-bundle: build-frontend +ifeq ($(OS),Windows_NT) + @$(POWERSHELL) "New-Item -ItemType Directory -Force -Path '$(BUILD_DIR)' | Out-Null" +else @mkdir -p $(BUILD_DIR) +endif GOOS=android GOARCH=arm64 $(GO) build -tags stdjson -ldflags "$(LDFLAGS)" -o "$(BUILD_DIR)/picoclaw-launcher-android-arm64" ./$(BACKEND_DIR)/ @echo "All Android launcher builds complete" build-frontend: +ifeq ($(OS),Windows_NT) + @$(POWERSHELL) "if ((-not (Test-Path -LiteralPath '$(FRONTEND_DIR)/node_modules')) -or (-not (Test-Path -LiteralPath '$(FRONTEND_DIR)/node_modules/.bin/tsc')) -or (-not (Test-Path -LiteralPath '$(FRONTEND_INSTALL_STAMP)')) -or ((Get-Content -LiteralPath '$(FRONTEND_INSTALL_STAMP)' -Raw).Trim() -ne (((Get-FileHash -LiteralPath '$(FRONTEND_DIR)/package.json' -Algorithm SHA256).Hash + ':' + (Get-FileHash -LiteralPath '$(FRONTEND_DIR)/pnpm-lock.yaml' -Algorithm SHA256).Hash)))) { Write-Host 'Installing frontend dependencies...'; Push-Location '$(FRONTEND_DIR)'; try { pnpm install --frozen-lockfile } finally { Pop-Location }; Set-Content -LiteralPath '$(FRONTEND_INSTALL_STAMP)' -Value (((Get-FileHash -LiteralPath '$(FRONTEND_DIR)/package.json' -Algorithm SHA256).Hash + ':' + (Get-FileHash -LiteralPath '$(FRONTEND_DIR)/pnpm-lock.yaml' -Algorithm SHA256).Hash)) -NoNewline }" +else @expected_stamp="$$(cat $(FRONTEND_DIR)/package.json $(FRONTEND_DIR)/pnpm-lock.yaml | cksum | awk '{print $$1 ":" $$2}')"; \ if [ ! -d $(FRONTEND_DIR)/node_modules ] || \ [ ! -x $(FRONTEND_DIR)/node_modules/.bin/tsc ] || \ @@ -115,12 +175,17 @@ build-frontend: (cd $(FRONTEND_DIR) && CI=true pnpm install --frozen-lockfile) && \ printf '%s\n' "$$expected_stamp" > $(FRONTEND_INSTALL_STAMP); \ fi +endif @echo "Building frontend..." @cd $(FRONTEND_DIR) && pnpm build:backend build-dev-picoclaw: @echo "Building picoclaw for launcher development..." +ifeq ($(OS),Windows_NT) + @$(POWERSHELL) "New-Item -ItemType Directory -Force -Path (Split-Path -Parent '$(PICOCLAW_BINARY)') | Out-Null" +else @mkdir -p "$$(dirname "$(PICOCLAW_BINARY)")" +endif @$(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o "$(PICOCLAW_BINARY)" ../cmd/picoclaw # Run all tests @@ -135,5 +200,10 @@ lint: # Clean build artifacts clean: +ifeq ($(OS),Windows_NT) + @$(POWERSHELL) "$$paths=@('$(FRONTEND_DIR)/dist','$(BACKEND_DIST)','$(BUILD_DIR)'); foreach($$p in $$paths){ if (Test-Path -LiteralPath $$p) { Remove-Item -LiteralPath $$p -Recurse -Force } }" + @node $(FRONTEND_DIR)/scripts/ensure-backend-gitkeep.cjs +else rm -rf $(FRONTEND_DIR)/dist $(BACKEND_DIST) $(BUILD_DIR) node $(FRONTEND_DIR)/scripts/ensure-backend-gitkeep.cjs +endif From 2e65b1be833ab9d9bc5e707e622c74b47c2d48ba Mon Sep 17 00:00:00 2001 From: Hoshina Date: Fri, 24 Apr 2026 18:14:28 +0800 Subject: [PATCH 18/36] feat(agent): add structured prompt layering --- pkg/agent/context.go | 254 +++++++++++--- pkg/agent/hooks.go | 46 +++ pkg/agent/hooks_test.go | 95 ++++++ pkg/agent/pipeline_execute.go | 4 +- pkg/agent/pipeline_llm.go | 6 +- pkg/agent/pipeline_setup.go | 25 +- pkg/agent/prompt.go | 483 +++++++++++++++++++++++++++ pkg/agent/prompt_test.go | 207 ++++++++++++ pkg/agent/prompt_turn.go | 117 +++++++ pkg/agent/steering.go | 1 + pkg/agent/turn_coord.go | 2 +- pkg/agent/turn_state.go | 5 +- pkg/providers/protocoltypes/types.go | 13 + 13 files changed, 1184 insertions(+), 74 deletions(-) create mode 100644 pkg/agent/prompt.go create mode 100644 pkg/agent/prompt_test.go create mode 100644 pkg/agent/prompt_turn.go diff --git a/pkg/agent/context.go b/pkg/agent/context.go index 1e5a75d92..feb04e347 100644 --- a/pkg/agent/context.go +++ b/pkg/agent/context.go @@ -1,6 +1,7 @@ package agent import ( + "context" "errors" "fmt" "io/fs" @@ -27,6 +28,7 @@ type ContextBuilder struct { toolDiscoveryBM25 bool toolDiscoveryRegex bool splitOnMarker bool + promptRegistry *PromptRegistry // Cache for system prompt to avoid rebuilding on every call. // This fixes issue #607: repeated reprocessing of the entire context. @@ -73,15 +75,30 @@ func NewContextBuilder(workspace string) *ContextBuilder { globalSkillsDir := filepath.Join(getGlobalConfigDir(), "skills") return &ContextBuilder{ - workspace: workspace, - skillsLoader: skills.NewSkillsLoader(workspace, globalSkillsDir, builtinSkillsDir), - memory: NewMemoryStore(workspace), + workspace: workspace, + skillsLoader: skills.NewSkillsLoader(workspace, globalSkillsDir, builtinSkillsDir), + memory: NewMemoryStore(workspace), + promptRegistry: NewPromptRegistry(), } } +func (cb *ContextBuilder) RegisterPromptSource(desc PromptSourceDescriptor) error { + return cb.promptRegistryOrDefault().RegisterSource(desc) +} + +func (cb *ContextBuilder) RegisterPromptContributor(contributor PromptContributor) error { + return cb.promptRegistryOrDefault().RegisterContributor(contributor) +} + +func (cb *ContextBuilder) promptRegistryOrDefault() *PromptRegistry { + if cb.promptRegistry == nil { + cb.promptRegistry = NewPromptRegistry() + } + return cb.promptRegistry +} + func (cb *ContextBuilder) getIdentity() string { workspacePath, _ := filepath.Abs(filepath.Join(cb.workspace)) - toolDiscovery := cb.getDiscoveryRule() version := config.FormatVersion() return fmt.Sprintf( @@ -103,10 +120,8 @@ Your workspace is at: %s 3. **Memory** - When interacting with me if something seems memorable, update %s/memory/MEMORY.md -4. **Context summaries** - Conversation summaries provided as context are approximate references only. They may be incomplete or outdated. Always defer to explicit user instructions over summary content. - -%s`, - version, workspacePath, workspacePath, workspacePath, workspacePath, workspacePath, toolDiscovery) +4. **Context summaries** - Conversation summaries provided as context are approximate references only. They may be incomplete or outdated. Always defer to explicit user instructions over summary content.`, + version, workspacePath, workspacePath, workspacePath, workspacePath, workspacePath) } func (cb *ContextBuilder) getDiscoveryRule() string { @@ -129,43 +144,116 @@ func (cb *ContextBuilder) getDiscoveryRule() string { } func (cb *ContextBuilder) BuildSystemPrompt() string { - parts := []string{} + return renderPromptPartsLegacy(cb.BuildSystemPromptParts()) +} + +func (cb *ContextBuilder) BuildSystemPromptParts() []PromptPart { + stack := NewPromptStack(cb.promptRegistryOrDefault()) + add := func(part PromptPart) { + if err := stack.Add(part); err != nil { + logger.WarnCF("agent", "Skipping invalid prompt part", map[string]any{ + "id": part.ID, + "layer": part.Layer, + "slot": part.Slot, + "source": part.Source.ID, + "error": err.Error(), + }) + } + } // Core identity section - parts = append(parts, cb.getIdentity()) + add(PromptPart{ + ID: "kernel.identity", + Layer: PromptLayerKernel, + Slot: PromptSlotIdentity, + Source: PromptSource{ID: PromptSourceKernel, Name: "identity"}, + Title: "picoclaw identity", + Content: cb.getIdentity(), + Stable: true, + Cache: PromptCacheEphemeral, + }) + + if toolDiscovery := cb.getDiscoveryRule(); toolDiscovery != "" { + add(PromptPart{ + ID: "capability.tool_discovery", + Layer: PromptLayerCapability, + Slot: PromptSlotTooling, + Source: PromptSource{ID: PromptSourceToolDiscovery, Name: "tool_registry:discovery"}, + Title: "tool discovery", + Content: toolDiscovery, + Stable: true, + Cache: PromptCacheEphemeral, + }) + } // Bootstrap files bootstrapContent := cb.LoadBootstrapFiles() if bootstrapContent != "" { - parts = append(parts, bootstrapContent) + add(PromptPart{ + ID: "instruction.workspace", + Layer: PromptLayerInstruction, + Slot: PromptSlotWorkspace, + Source: PromptSource{ID: PromptSourceWorkspace, Name: "workspace"}, + Title: "workspace instructions", + Content: bootstrapContent, + Stable: true, + Cache: PromptCacheEphemeral, + }) } // Skills - show summary, AI can read full content with read_file tool skillsSummary := cb.skillsLoader.BuildSkillsSummary() if skillsSummary != "" { - parts = append(parts, fmt.Sprintf(`# Skills + add(PromptPart{ + ID: "capability.skill_catalog", + Layer: PromptLayerCapability, + Slot: PromptSlotSkillCatalog, + Source: PromptSource{ID: PromptSourceSkillCatalog, Name: "skill:index"}, + Title: "skill catalog", + Content: fmt.Sprintf(`# Skills The following skills extend your capabilities. To use a skill, read its SKILL.md file using the read_file tool. -%s`, skillsSummary)) +%s`, skillsSummary), + Stable: true, + Cache: PromptCacheEphemeral, + }) } // Memory context memoryContext := cb.memory.GetMemoryContext() if memoryContext != "" { - parts = append(parts, "# Memory\n\n"+memoryContext) + add(PromptPart{ + ID: "context.memory", + Layer: PromptLayerContext, + Slot: PromptSlotMemory, + Source: PromptSource{ID: PromptSourceMemory, Name: "memory:workspace"}, + Title: "memory", + Content: "# Memory\n\n" + memoryContext, + Stable: true, + Cache: PromptCacheEphemeral, + }) } // Multi-Message Sending (if enabled) if cb.splitOnMarker { - parts = append(parts, `# MULTI-MESSAGE OUTPUT + add(PromptPart{ + ID: "context.output_policy.split_on_marker", + Layer: PromptLayerContext, + Slot: PromptSlotOutput, + Source: PromptSource{ID: PromptSourceOutputPolicy, Name: "split_on_marker"}, + Title: "multi-message output policy", + Content: `# MULTI-MESSAGE OUTPUT You MUST frequently use <|[SPLIT]|> to break your responses into multiple short messages. NEVER output a single long wall of text. Actively split distinct concepts or parts. Example: Message part 1<|[SPLIT]|>Message part 2<|[SPLIT]|>Message part 3 -Each part separated by the marker will be sent as an independent message.`) +Each part separated by the marker will be sent as an independent message.`, + Stable: true, + Cache: PromptCacheEphemeral, + }) } - // Join with "---" separator - return strings.Join(parts, "\n\n---\n\n") + stack.Seal() + return stack.Parts() } // BuildSystemPromptWithCache returns the cached system prompt if available @@ -548,6 +636,20 @@ func (cb *ContextBuilder) BuildMessages( channel, chatID, senderID, senderDisplayName string, activeSkills ...string, ) []providers.Message { + return cb.BuildMessagesFromPrompt(PromptBuildRequest{ + History: history, + Summary: summary, + CurrentMessage: currentMessage, + Media: media, + Channel: channel, + ChatID: chatID, + SenderID: senderID, + SenderDisplayName: senderDisplayName, + ActiveSkills: append([]string(nil), activeSkills...), + }) +} + +func (cb *ContextBuilder) BuildMessagesFromPrompt(req PromptBuildRequest) []providers.Message { messages := []providers.Message{} // The static part (identity, bootstrap, skills, memory) is cached locally to @@ -562,7 +664,7 @@ func (cb *ContextBuilder) BuildMessages( staticPrompt := cb.BuildSystemPromptWithCache() // Build short dynamic context (time, runtime, session) — changes per request - dynamicCtx := cb.buildDynamicContext(channel, chatID, senderID, senderDisplayName) + dynamicCtx := cb.buildDynamicContext(req.Channel, req.ChatID, req.SenderID, req.SenderDisplayName) // Compose a single system message: static (cached) + dynamic + optional summary. // Keeping all system content in one message ensures every provider adapter can @@ -573,25 +675,77 @@ func (cb *ContextBuilder) BuildMessages( // cache-aware adapters (Anthropic) can set per-block cache_control. // The static block is marked "ephemeral" — its prefix hash is stable // across requests, enabling LLM-side KV cache reuse. - stringParts := []string{staticPrompt, dynamicCtx} + stringParts := []string{staticPrompt} contentBlocks := []providers.ContentBlock{ - {Type: "text", Text: staticPrompt, CacheControl: &providers.CacheControl{Type: "ephemeral"}}, - {Type: "text", Text: dynamicCtx}, + promptContentBlock(PromptPart{ + ID: "kernel.static", + Layer: PromptLayerKernel, + Slot: PromptSlotIdentity, + Source: PromptSource{ID: PromptSourceKernel, Name: "static"}, + Content: staticPrompt, + }, &providers.CacheControl{Type: "ephemeral"}), } - if skillsText := cb.buildActiveSkillsContext(activeSkills); skillsText != "" { - stringParts = append(stringParts, skillsText) - contentBlocks = append(contentBlocks, providers.ContentBlock{Type: "text", Text: skillsText}) + promptParts := append([]PromptPart(nil), req.Overlays...) + promptParts = append(promptParts, cb.buildActiveSkillsPromptParts(req.ActiveSkills)...) + if contributedParts, err := cb.promptRegistryOrDefault().Collect(context.Background(), req); err != nil { + logger.WarnCF("agent", "Prompt contributor collection failed", map[string]any{ + "error": err.Error(), + }) + } else { + promptParts = append(promptParts, contributedParts...) } - if summary != "" { - summaryText := fmt.Sprintf( - "CONTEXT_SUMMARY: The following is an approximate summary of prior conversation "+ - "for reference only. It may be incomplete or outdated — always defer to explicit instructions.\n\n%s", - summary) - stringParts = append(stringParts, summaryText) - contentBlocks = append(contentBlocks, providers.ContentBlock{Type: "text", Text: summaryText}) + if len(promptParts) > 0 { + for _, overlay := range sortPromptParts(promptParts) { + if strings.TrimSpace(overlay.Content) == "" { + continue + } + if err := cb.promptRegistryOrDefault().ValidatePart(overlay); err != nil { + logger.WarnCF("agent", "Skipping invalid prompt overlay", map[string]any{ + "id": overlay.ID, + "layer": overlay.Layer, + "slot": overlay.Slot, + "source": overlay.Source.ID, + "error": err.Error(), + }) + continue + } + stringParts = append(stringParts, overlay.Content) + contentBlocks = append(contentBlocks, promptContentBlock(overlay, nil)) + } + } + + runtimePart := PromptPart{ + ID: "context.runtime", + Layer: PromptLayerContext, + Slot: PromptSlotRuntime, + Source: PromptSource{ID: PromptSourceRuntime, Name: "runtime"}, + Title: "runtime context", + Content: dynamicCtx, + Stable: false, + Cache: PromptCacheNone, + } + stringParts = append(stringParts, dynamicCtx) + contentBlocks = append(contentBlocks, promptContentBlock(runtimePart, nil)) + + if req.Summary != "" { + summaryPart := PromptPart{ + ID: "context.summary", + Layer: PromptLayerContext, + Slot: PromptSlotSummary, + Source: PromptSource{ID: PromptSourceSummary, Name: "context.summary"}, + Title: "context summary", + Content: fmt.Sprintf( + "CONTEXT_SUMMARY: The following is an approximate summary of prior conversation "+ + "for reference only. It may be incomplete or outdated — always defer to explicit instructions.\n\n%s", + req.Summary), + Stable: false, + Cache: PromptCacheNone, + } + stringParts = append(stringParts, summaryPart.Content) + contentBlocks = append(contentBlocks, promptContentBlock(summaryPart, nil)) } fullSystemPrompt := strings.Join(stringParts, "\n\n---\n\n") @@ -608,7 +762,8 @@ func (cb *ContextBuilder) BuildMessages( "static_chars": len(staticPrompt), "dynamic_chars": len(dynamicCtx), "total_chars": len(fullSystemPrompt), - "has_summary": summary != "", + "has_summary": req.Summary != "", + "overlays": len(req.Overlays), "cached": isCached, }) @@ -619,7 +774,7 @@ func (cb *ContextBuilder) BuildMessages( "preview": preview, }) - history = sanitizeHistoryForProvider(history) + history := sanitizeHistoryForProvider(req.History) // Single system message containing all context — compatible with all providers. // SystemParts enables cache-aware adapters to set per-block cache_control; @@ -636,15 +791,8 @@ func (cb *ContextBuilder) BuildMessages( // Add current user message. Media-only turns must still be preserved so // multimodal providers receive the uploaded image even when the user sends // no accompanying text. - if strings.TrimSpace(currentMessage) != "" || len(media) > 0 { - msg := providers.Message{ - Role: "user", - Content: currentMessage, - } - if len(media) > 0 { - msg.Media = append([]string(nil), media...) - } - messages = append(messages, msg) + if strings.TrimSpace(req.CurrentMessage) != "" || len(req.Media) > 0 { + messages = append(messages, userPromptMessage(req.CurrentMessage, req.Media)) } return messages @@ -870,6 +1018,26 @@ The following skills are active for this request. Follow them when relevant. %s`, content) } +func (cb *ContextBuilder) buildActiveSkillsPromptParts(skillNames []string) []PromptPart { + skillsText := cb.buildActiveSkillsContext(skillNames) + if strings.TrimSpace(skillsText) == "" { + return nil + } + + return []PromptPart{ + { + ID: "capability.active_skills", + Layer: PromptLayerCapability, + Slot: PromptSlotActiveSkill, + Source: PromptSource{ID: PromptSourceActiveSkills, Name: "skill:active"}, + Title: "active skills", + Content: skillsText, + Stable: false, + Cache: PromptCacheNone, + }, + } +} + func (cb *ContextBuilder) ListSkillNames() []string { if cb.skillsLoader == nil { return nil diff --git a/pkg/agent/hooks.go b/pkg/agent/hooks.go index 687e54532..e9863c4f6 100644 --- a/pkg/agent/hooks.go +++ b/pkg/agent/hooks.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "io" + "reflect" "sort" "sync" "time" @@ -325,6 +326,7 @@ func (hm *HookManager) BeforeLLM(ctx context.Context, req *LLMHookRequest) (*LLM switch decision.normalizedAction() { case HookActionContinue, HookActionModify: if next != nil { + next = hm.applyBeforeLLMControls(reg.Name, current, next) current = next } case HookActionAbortTurn, HookActionHardAbort: @@ -367,6 +369,50 @@ func (hm *HookManager) AfterLLM(ctx context.Context, resp *LLMHookResponse) (*LL return current, HookDecision{Action: HookActionContinue} } +func (hm *HookManager) applyBeforeLLMControls( + hookName string, + current *LLMHookRequest, + next *LLMHookRequest, +) *LLMHookRequest { + if next == nil || current == nil { + return next + } + if llmHookSystemMessagesUnchanged(current.Messages, next.Messages) { + return next + } + + logger.WarnCF("hooks", "Hook attempted to modify system prompt; preserving original messages", map[string]any{ + "hook": hookName, + }) + next.Messages = cloneProviderMessages(current.Messages) + return next +} + +func llmHookSystemMessagesUnchanged(before, after []providers.Message) bool { + beforeSystem := systemMessageFingerprints(before) + afterSystem := systemMessageFingerprints(after) + return reflect.DeepEqual(beforeSystem, afterSystem) +} + +type systemMessageFingerprint struct { + Index int + Message providers.Message +} + +func systemMessageFingerprints(messages []providers.Message) []systemMessageFingerprint { + var fingerprints []systemMessageFingerprint + for i, msg := range messages { + if msg.Role != "system" { + continue + } + fingerprints = append(fingerprints, systemMessageFingerprint{ + Index: i, + Message: cloneProviderMessages([]providers.Message{msg})[0], + }) + } + return fingerprints +} + func (hm *HookManager) BeforeTool( ctx context.Context, call *ToolCallHookRequest, diff --git a/pkg/agent/hooks_test.go b/pkg/agent/hooks_test.go index 1cfa341a7..419752e5b 100644 --- a/pkg/agent/hooks_test.go +++ b/pkg/agent/hooks_test.go @@ -149,6 +149,101 @@ func (h *llmObserverHook) AfterLLM( return next, HookDecision{Action: HookActionModify}, nil } +type llmSystemRewriteHook struct{} + +func (h *llmSystemRewriteHook) BeforeLLM( + ctx context.Context, + req *LLMHookRequest, +) (*LLMHookRequest, HookDecision, error) { + next := req.Clone() + next.Model = "changed-model" + next.Messages[0].Content = "rewritten system" + return next, HookDecision{Action: HookActionModify}, nil +} + +func (h *llmSystemRewriteHook) AfterLLM( + ctx context.Context, + resp *LLMHookResponse, +) (*LLMHookResponse, HookDecision, error) { + return resp.Clone(), HookDecision{Action: HookActionContinue}, nil +} + +type llmUserAppendHook struct{} + +func (h *llmUserAppendHook) BeforeLLM( + ctx context.Context, + req *LLMHookRequest, +) (*LLMHookRequest, HookDecision, error) { + next := req.Clone() + next.Messages = append(next.Messages, providers.Message{Role: "user", Content: "extra user context"}) + return next, HookDecision{Action: HookActionModify}, nil +} + +func (h *llmUserAppendHook) AfterLLM( + ctx context.Context, + resp *LLMHookResponse, +) (*LLMHookResponse, HookDecision, error) { + return resp.Clone(), HookDecision{Action: HookActionContinue}, nil +} + +func TestHookManager_BeforeLLMControlsSystemPromptMutation(t *testing.T) { + hm := NewHookManager(nil) + if err := hm.Mount(NamedHook("rewrite-system", &llmSystemRewriteHook{})); err != nil { + t.Fatalf("Mount() error = %v", err) + } + + req := &LLMHookRequest{ + Model: "original-model", + Messages: []providers.Message{ + { + Role: "system", + Content: "original system", + SystemParts: []providers.ContentBlock{ + {Type: "text", Text: "original system"}, + }, + }, + {Role: "user", Content: "hello"}, + }, + } + + got, decision := hm.BeforeLLM(context.Background(), req) + if decision.normalizedAction() != HookActionContinue { + t.Fatalf("decision = %v, want continue", decision) + } + if got.Model != "changed-model" { + t.Fatalf("model = %q, want changed-model", got.Model) + } + if got.Messages[0].Content != "original system" { + t.Fatalf("system content = %q, want original system", got.Messages[0].Content) + } + if got.Messages[1].Content != "hello" { + t.Fatalf("user content = %q, want hello", got.Messages[1].Content) + } +} + +func TestHookManager_BeforeLLMAllowsNonSystemMessageMutation(t *testing.T) { + hm := NewHookManager(nil) + if err := hm.Mount(NamedHook("append-user", &llmUserAppendHook{})); err != nil { + t.Fatalf("Mount() error = %v", err) + } + + req := &LLMHookRequest{ + Model: "model", + Messages: []providers.Message{ + {Role: "system", Content: "system"}, + {Role: "user", Content: "hello"}, + }, + } + + got, _ := hm.BeforeLLM(context.Background(), req) + if len(got.Messages) != 3 { + t.Fatalf("messages len = %d, want 3", len(got.Messages)) + } + if got.Messages[2].Role != "user" || got.Messages[2].Content != "extra user context" { + t.Fatalf("appended message = %#v, want extra user context", got.Messages[2]) + } +} + func TestAgentLoop_Hooks_ObserverAndLLMInterceptor(t *testing.T) { provider := &llmHookTestProvider{} al, agent, cleanup := newHookTestLoop(t, provider) diff --git a/pkg/agent/pipeline_execute.go b/pkg/agent/pipeline_execute.go index 48e72e096..0e67e4faf 100644 --- a/pkg/agent/pipeline_execute.go +++ b/pkg/agent/pipeline_execute.go @@ -260,7 +260,7 @@ toolLoop: case result, ok := <-ts.pendingResults: if ok && result != nil && result.ForLLM != "" { content := al.cfg.FilterSensitiveData(result.ForLLM) - msg := providers.Message{Role: "user", Content: fmt.Sprintf("[SubTurn Result] %s", content)} + msg := subTurnResultPromptMessage(content) messages = append(messages, msg) ts.agent.Sessions.AddFullMessage(ts.sessionKey, msg) } @@ -631,7 +631,7 @@ toolLoop: case result, ok := <-ts.pendingResults: if ok && result != nil && result.ForLLM != "" { content := al.cfg.FilterSensitiveData(result.ForLLM) - msg := providers.Message{Role: "user", Content: fmt.Sprintf("[SubTurn Result] %s", content)} + msg := subTurnResultPromptMessage(content) messages = append(messages, msg) ts.agent.Sessions.AddFullMessage(ts.sessionKey, msg) } diff --git a/pkg/agent/pipeline_llm.go b/pkg/agent/pipeline_llm.go index 7b3fee208..04cb98df4 100644 --- a/pkg/agent/pipeline_llm.go +++ b/pkg/agent/pipeline_llm.go @@ -319,10 +319,8 @@ func (p *Pipeline) CallLLM( exec.history = asmResp.History exec.summary = asmResp.Summary } - exec.messages = ts.agent.ContextBuilder.BuildMessages( - exec.history, exec.summary, "", - nil, ts.channel, ts.chatID, ts.opts.Dispatch.SenderID(), ts.opts.SenderDisplayName, - activeSkillNames(ts.agent, ts.opts)..., + exec.messages = ts.agent.ContextBuilder.BuildMessagesFromPrompt( + promptBuildRequestForTurn(ts, exec.history, exec.summary, "", nil), ) exec.callMessages = exec.messages if exec.gracefulTerminal { diff --git a/pkg/agent/pipeline_setup.go b/pkg/agent/pipeline_setup.go index e6ead1012..219e4e5de 100644 --- a/pkg/agent/pipeline_setup.go +++ b/pkg/agent/pipeline_setup.go @@ -31,16 +31,8 @@ func (p *Pipeline) SetupTurn(ctx context.Context, ts *turnState) (*turnExecution } ts.captureRestorePoint(history, summary) - messages := ts.agent.ContextBuilder.BuildMessages( - history, - summary, - ts.userMessage, - ts.media, - ts.channel, - ts.chatID, - ts.opts.Dispatch.SenderID(), - ts.opts.SenderDisplayName, - activeSkillNames(ts.agent, ts.opts)..., + messages := ts.agent.ContextBuilder.BuildMessagesFromPrompt( + promptBuildRequestForTurn(ts, history, summary, ts.userMessage, ts.media), ) messages = resolveMediaRefs(messages, p.MediaStore, maxMediaSize) @@ -69,22 +61,15 @@ func (p *Pipeline) SetupTurn(ctx context.Context, ts *turnState) (*turnExecution history = resp.History summary = resp.Summary } - messages = ts.agent.ContextBuilder.BuildMessages( - history, summary, ts.userMessage, - ts.media, ts.channel, ts.chatID, - ts.opts.Dispatch.SenderID(), ts.opts.SenderDisplayName, - activeSkillNames(ts.agent, ts.opts)..., + messages = ts.agent.ContextBuilder.BuildMessagesFromPrompt( + promptBuildRequestForTurn(ts, history, summary, ts.userMessage, ts.media), ) messages = resolveMediaRefs(messages, p.MediaStore, maxMediaSize) } } if !ts.opts.NoHistory && (strings.TrimSpace(ts.userMessage) != "" || len(ts.media) > 0) { - rootMsg := providers.Message{ - Role: "user", - Content: ts.userMessage, - Media: append([]string(nil), ts.media...), - } + rootMsg := userPromptMessage(ts.userMessage, ts.media) if len(rootMsg.Media) > 0 { ts.agent.Sessions.AddFullMessage(ts.sessionKey, rootMsg) } else { diff --git a/pkg/agent/prompt.go b/pkg/agent/prompt.go new file mode 100644 index 000000000..aa06bba46 --- /dev/null +++ b/pkg/agent/prompt.go @@ -0,0 +1,483 @@ +package agent + +import ( + "context" + "fmt" + "slices" + "strings" + "sync" + + "github.com/sipeed/picoclaw/pkg/logger" + "github.com/sipeed/picoclaw/pkg/providers" +) + +type PromptLayer string + +const ( + PromptLayerKernel PromptLayer = "kernel" + PromptLayerInstruction PromptLayer = "instruction" + PromptLayerCapability PromptLayer = "capability" + PromptLayerContext PromptLayer = "context" + PromptLayerTurn PromptLayer = "turn" +) + +type PromptSlot string + +const ( + PromptSlotIdentity PromptSlot = "identity" + PromptSlotHierarchy PromptSlot = "hierarchy" + PromptSlotWorkspace PromptSlot = "workspace" + PromptSlotTooling PromptSlot = "tooling" + PromptSlotMCP PromptSlot = "mcp" + PromptSlotSkillCatalog PromptSlot = "skill_catalog" + PromptSlotActiveSkill PromptSlot = "active_skill" + PromptSlotMemory PromptSlot = "memory" + PromptSlotRuntime PromptSlot = "runtime" + PromptSlotSummary PromptSlot = "summary" + PromptSlotMessage PromptSlot = "message" + PromptSlotSteering PromptSlot = "steering" + PromptSlotSubTurn PromptSlot = "subturn" + PromptSlotInterrupt PromptSlot = "interrupt" + PromptSlotOutput PromptSlot = "output" +) + +type PromptSourceID string + +const ( + PromptSourceKernel PromptSourceID = "runtime.kernel" + PromptSourceHierarchy PromptSourceID = "runtime.hierarchy" + PromptSourceWorkspace PromptSourceID = "workspace.definition" + PromptSourceRuntime PromptSourceID = "runtime.context" + PromptSourceSummary PromptSourceID = "context.summary" + PromptSourceMemory PromptSourceID = "memory:workspace" + PromptSourceSkillCatalog PromptSourceID = "skill:index" + PromptSourceActiveSkills PromptSourceID = "skill:active" + PromptSourceToolDiscovery PromptSourceID = "tool_registry:discovery" + PromptSourceOutputPolicy PromptSourceID = "runtime.output" + PromptSourceSubTurnProfile PromptSourceID = "subturn.profile" + PromptSourceUserMessage PromptSourceID = "turn:user_message" + PromptSourceSteering PromptSourceID = "turn:steering" + PromptSourceSubTurnResult PromptSourceID = "turn:subturn_result" + PromptSourceInterrupt PromptSourceID = "turn:interrupt" +) + +type PromptCachePolicy string + +const ( + PromptCacheDefault PromptCachePolicy = "" + PromptCacheEphemeral PromptCachePolicy = "ephemeral" + PromptCacheNone PromptCachePolicy = "none" +) + +type PromptPlacement struct { + Layer PromptLayer + Slot PromptSlot +} + +type PromptSourceDescriptor struct { + ID PromptSourceID + Owner string + Description string + Allowed []PromptPlacement + StableByDefault bool +} + +type PromptSource struct { + ID PromptSourceID + Name string + Path string +} + +type PromptPart struct { + ID string + Layer PromptLayer + Slot PromptSlot + Source PromptSource + Title string + Content string + Stable bool + Cache PromptCachePolicy +} + +type PromptBuildRequest struct { + History []providers.Message + Summary string + + CurrentMessage string + Media []string + + Channel string + ChatID string + SenderID string + SenderDisplayName string + + ActiveSkills []string + Overlays []PromptPart +} + +type PromptContributor interface { + PromptSource() PromptSourceDescriptor + ContributePrompt(ctx context.Context, req PromptBuildRequest) ([]PromptPart, error) +} + +type PromptRegistry struct { + mu sync.RWMutex + sources map[PromptSourceID]PromptSourceDescriptor + contributors []PromptContributor + warned map[PromptSourceID]struct{} +} + +func NewPromptRegistry() *PromptRegistry { + r := &PromptRegistry{ + sources: make(map[PromptSourceID]PromptSourceDescriptor), + warned: make(map[PromptSourceID]struct{}), + } + for _, desc := range builtinPromptSources() { + if err := r.RegisterSource(desc); err != nil { + logger.WarnCF("agent", "Failed to register builtin prompt source", map[string]any{ + "source": desc.ID, + "error": err.Error(), + }) + } + } + return r +} + +func builtinPromptSources() []PromptSourceDescriptor { + return []PromptSourceDescriptor{ + { + ID: PromptSourceKernel, + Owner: "agent", + Description: "Core picoclaw identity and hard rules", + Allowed: []PromptPlacement{{Layer: PromptLayerKernel, Slot: PromptSlotIdentity}}, + StableByDefault: true, + }, + { + ID: PromptSourceHierarchy, + Owner: "agent", + Description: "Prompt hierarchy rules", + Allowed: []PromptPlacement{{Layer: PromptLayerKernel, Slot: PromptSlotHierarchy}}, + StableByDefault: true, + }, + { + ID: PromptSourceWorkspace, + Owner: "workspace", + Description: "Workspace and agent definition files", + Allowed: []PromptPlacement{{Layer: PromptLayerInstruction, Slot: PromptSlotWorkspace}}, + StableByDefault: true, + }, + { + ID: PromptSourceToolDiscovery, + Owner: "tools", + Description: "Tool discovery instructions", + Allowed: []PromptPlacement{{Layer: PromptLayerCapability, Slot: PromptSlotTooling}}, + StableByDefault: true, + }, + { + ID: PromptSourceSkillCatalog, + Owner: "skills", + Description: "Installed skill catalog", + Allowed: []PromptPlacement{{Layer: PromptLayerCapability, Slot: PromptSlotSkillCatalog}}, + StableByDefault: true, + }, + { + ID: PromptSourceActiveSkills, + Owner: "skills", + Description: "Active skill instructions for the current request", + Allowed: []PromptPlacement{{Layer: PromptLayerCapability, Slot: PromptSlotActiveSkill}}, + StableByDefault: false, + }, + { + ID: PromptSourceMemory, + Owner: "memory", + Description: "Workspace memory context", + Allowed: []PromptPlacement{{Layer: PromptLayerContext, Slot: PromptSlotMemory}}, + StableByDefault: true, + }, + { + ID: PromptSourceRuntime, + Owner: "agent", + Description: "Per-request runtime context", + Allowed: []PromptPlacement{{Layer: PromptLayerContext, Slot: PromptSlotRuntime}}, + StableByDefault: false, + }, + { + ID: PromptSourceSummary, + Owner: "context_manager", + Description: "Conversation summary context", + Allowed: []PromptPlacement{{Layer: PromptLayerContext, Slot: PromptSlotSummary}}, + StableByDefault: false, + }, + { + ID: PromptSourceOutputPolicy, + Owner: "agent", + Description: "Output formatting policy", + Allowed: []PromptPlacement{{Layer: PromptLayerContext, Slot: PromptSlotOutput}}, + StableByDefault: true, + }, + { + ID: PromptSourceSubTurnProfile, + Owner: "subturn", + Description: "Child agent profile instructions", + Allowed: []PromptPlacement{{Layer: PromptLayerInstruction, Slot: PromptSlotWorkspace}}, + StableByDefault: false, + }, + { + ID: PromptSourceUserMessage, + Owner: "turn", + Description: "Current user message for this turn", + Allowed: []PromptPlacement{{Layer: PromptLayerTurn, Slot: PromptSlotMessage}}, + StableByDefault: false, + }, + { + ID: PromptSourceSteering, + Owner: "turn", + Description: "Steering message injected into a running turn", + Allowed: []PromptPlacement{{Layer: PromptLayerTurn, Slot: PromptSlotSteering}}, + StableByDefault: false, + }, + { + ID: PromptSourceSubTurnResult, + Owner: "turn", + Description: "SubTurn result injected into a parent turn", + Allowed: []PromptPlacement{{Layer: PromptLayerTurn, Slot: PromptSlotSubTurn}}, + StableByDefault: false, + }, + { + ID: PromptSourceInterrupt, + Owner: "turn", + Description: "Graceful interrupt hint injected into the terminal LLM call", + Allowed: []PromptPlacement{{Layer: PromptLayerTurn, Slot: PromptSlotInterrupt}}, + StableByDefault: false, + }, + } +} + +func (r *PromptRegistry) RegisterSource(desc PromptSourceDescriptor) error { + if r == nil { + return fmt.Errorf("prompt registry is nil") + } + desc.ID = PromptSourceID(strings.TrimSpace(string(desc.ID))) + if desc.ID == "" { + return fmt.Errorf("prompt source id is required") + } + if len(desc.Allowed) == 0 { + return fmt.Errorf("prompt source %q must declare at least one placement", desc.ID) + } + + r.mu.Lock() + defer r.mu.Unlock() + r.sources[desc.ID] = clonePromptSourceDescriptor(desc) + return nil +} + +func (r *PromptRegistry) RegisterContributor(contributor PromptContributor) error { + if r == nil { + return fmt.Errorf("prompt registry is nil") + } + if contributor == nil { + return fmt.Errorf("prompt contributor is nil") + } + if err := r.RegisterSource(contributor.PromptSource()); err != nil { + return err + } + + r.mu.Lock() + defer r.mu.Unlock() + r.contributors = append(r.contributors, contributor) + return nil +} + +func (r *PromptRegistry) Collect(ctx context.Context, req PromptBuildRequest) ([]PromptPart, error) { + if r == nil { + return nil, nil + } + + r.mu.RLock() + contributors := append([]PromptContributor(nil), r.contributors...) + r.mu.RUnlock() + + var parts []PromptPart + for _, contributor := range contributors { + contributed, err := contributor.ContributePrompt(ctx, req) + if err != nil { + return nil, err + } + for _, part := range contributed { + if err := r.ValidatePart(part); err != nil { + return nil, err + } + parts = append(parts, part) + } + } + return parts, nil +} + +func (r *PromptRegistry) ValidatePart(part PromptPart) error { + if r == nil { + return nil + } + sourceID := PromptSourceID(strings.TrimSpace(string(part.Source.ID))) + if sourceID == "" { + return fmt.Errorf("prompt part %q has empty source id", part.ID) + } + + r.mu.Lock() + defer r.mu.Unlock() + + desc, ok := r.sources[sourceID] + if !ok { + if _, warned := r.warned[sourceID]; !warned { + r.warned[sourceID] = struct{}{} + logger.WarnCF("agent", "Unregistered prompt source allowed in compatibility mode", map[string]any{ + "source": sourceID, + "layer": part.Layer, + "slot": part.Slot, + "part": part.ID, + }) + } + return nil + } + if promptPlacementAllowed(desc.Allowed, PromptPlacement{Layer: part.Layer, Slot: part.Slot}) { + return nil + } + return fmt.Errorf("prompt source %q cannot write to %s/%s", sourceID, part.Layer, part.Slot) +} + +func promptPlacementAllowed(allowed []PromptPlacement, placement PromptPlacement) bool { + return slices.ContainsFunc(allowed, func(candidate PromptPlacement) bool { + return candidate.Layer == placement.Layer && candidate.Slot == placement.Slot + }) +} + +func clonePromptSourceDescriptor(desc PromptSourceDescriptor) PromptSourceDescriptor { + desc.Allowed = append([]PromptPlacement(nil), desc.Allowed...) + return desc +} + +type PromptStack struct { + registry *PromptRegistry + parts []PromptPart + sealed bool +} + +func NewPromptStack(registry *PromptRegistry) *PromptStack { + return &PromptStack{registry: registry} +} + +func (s *PromptStack) Add(part PromptPart) error { + if s == nil { + return fmt.Errorf("prompt stack is nil") + } + if s.sealed { + return fmt.Errorf("prompt stack is sealed") + } + if strings.TrimSpace(part.Content) == "" { + return nil + } + if strings.TrimSpace(part.ID) == "" { + return fmt.Errorf("prompt part id is required") + } + if s.registry != nil { + if err := s.registry.ValidatePart(part); err != nil { + return err + } + } + s.parts = append(s.parts, part) + return nil +} + +func (s *PromptStack) Seal() { + if s != nil { + s.sealed = true + } +} + +func (s *PromptStack) Parts() []PromptPart { + if s == nil || len(s.parts) == 0 { + return nil + } + return append([]PromptPart(nil), s.parts...) +} + +func renderPromptPartsLegacy(parts []PromptPart) string { + textParts := make([]string, 0, len(parts)) + for _, part := range sortPromptParts(parts) { + if strings.TrimSpace(part.Content) == "" { + continue + } + textParts = append(textParts, part.Content) + } + return strings.Join(textParts, "\n\n---\n\n") +} + +func sortPromptParts(parts []PromptPart) []PromptPart { + sorted := append([]PromptPart(nil), parts...) + slices.SortStableFunc(sorted, func(a, b PromptPart) int { + if d := layerPriority(b.Layer) - layerPriority(a.Layer); d != 0 { + return d + } + if d := slotPriority(b.Slot) - slotPriority(a.Slot); d != 0 { + return d + } + if a.Source.ID != b.Source.ID { + return strings.Compare(string(a.Source.ID), string(b.Source.ID)) + } + return strings.Compare(a.ID, b.ID) + }) + return sorted +} + +func layerPriority(layer PromptLayer) int { + switch layer { + case PromptLayerKernel: + return 100 + case PromptLayerInstruction: + return 80 + case PromptLayerCapability: + return 60 + case PromptLayerContext: + return 40 + case PromptLayerTurn: + return 20 + default: + return 0 + } +} + +func slotPriority(slot PromptSlot) int { + switch slot { + case PromptSlotIdentity: + return 1000 + case PromptSlotHierarchy: + return 990 + case PromptSlotWorkspace: + return 900 + case PromptSlotTooling: + return 800 + case PromptSlotMCP: + return 790 + case PromptSlotSkillCatalog: + return 780 + case PromptSlotActiveSkill: + return 770 + case PromptSlotMemory: + return 700 + case PromptSlotOutput: + return 695 + case PromptSlotRuntime: + return 690 + case PromptSlotSummary: + return 680 + case PromptSlotMessage: + return 600 + case PromptSlotSteering: + return 590 + case PromptSlotSubTurn: + return 580 + case PromptSlotInterrupt: + return 570 + default: + return 0 + } +} diff --git a/pkg/agent/prompt_test.go b/pkg/agent/prompt_test.go new file mode 100644 index 000000000..af46eae7a --- /dev/null +++ b/pkg/agent/prompt_test.go @@ -0,0 +1,207 @@ +package agent + +import ( + "context" + "encoding/json" + "strings" + "testing" +) + +func TestPromptRegistry_RejectsRegisteredSourceWrongPlacement(t *testing.T) { + registry := NewPromptRegistry() + if err := registry.RegisterSource(PromptSourceDescriptor{ + ID: "test:source", + Owner: "test", + Allowed: []PromptPlacement{{Layer: PromptLayerCapability, Slot: PromptSlotTooling}}, + }); err != nil { + t.Fatalf("RegisterSource() error = %v", err) + } + + err := registry.ValidatePart(PromptPart{ + ID: "wrong.placement", + Layer: PromptLayerContext, + Slot: PromptSlotRuntime, + Source: PromptSource{ID: "test:source"}, + Content: "runtime text", + }) + if err == nil { + t.Fatal("ValidatePart() error = nil, want placement error") + } +} + +func TestPromptRegistry_AllowsUnregisteredSourceInCompatibilityMode(t *testing.T) { + registry := NewPromptRegistry() + + err := registry.ValidatePart(PromptPart{ + ID: "unregistered.part", + Layer: PromptLayerCapability, + Slot: PromptSlotMCP, + Source: PromptSource{ID: "mcp:dynamic-server"}, + Content: "dynamic MCP prompt", + }) + if err != nil { + t.Fatalf("ValidatePart() error = %v, want nil for unregistered source", err) + } +} + +func TestRenderPromptPartsLegacy_UsesLayerAndSlotOrder(t *testing.T) { + parts := []PromptPart{ + { + ID: "context.runtime", + Layer: PromptLayerContext, + Slot: PromptSlotRuntime, + Source: PromptSource{ID: PromptSourceRuntime}, + Content: "runtime", + }, + { + ID: "kernel.identity", + Layer: PromptLayerKernel, + Slot: PromptSlotIdentity, + Source: PromptSource{ID: PromptSourceKernel}, + Content: "kernel", + }, + { + ID: "capability.skill", + Layer: PromptLayerCapability, + Slot: PromptSlotActiveSkill, + Source: PromptSource{ID: "skill:test"}, + Content: "skill", + }, + { + ID: "instruction.workspace", + Layer: PromptLayerInstruction, + Slot: PromptSlotWorkspace, + Source: PromptSource{ID: PromptSourceWorkspace}, + Content: "workspace", + }, + } + + got := renderPromptPartsLegacy(parts) + want := strings.Join([]string{"kernel", "workspace", "skill", "runtime"}, "\n\n---\n\n") + if got != want { + t.Fatalf("renderPromptPartsLegacy() = %q, want %q", got, want) + } +} + +func TestBuildMessagesFromPrompt_IncludesSystemPromptOverlay(t *testing.T) { + t.Setenv("PICOCLAW_BUILTIN_SKILLS", t.TempDir()) + cb := NewContextBuilder(t.TempDir()) + + messages := cb.BuildMessagesFromPrompt(PromptBuildRequest{ + CurrentMessage: "do child task", + Overlays: promptOverlaysForOptions(processOptions{ + SystemPromptOverride: "Use child-only system instructions.", + }), + }) + + if len(messages) < 2 { + t.Fatalf("messages len = %d, want at least 2", len(messages)) + } + if messages[0].Role != "system" { + t.Fatalf("messages[0].Role = %q, want system", messages[0].Role) + } + if !strings.Contains(messages[0].Content, "Use child-only system instructions.") { + t.Fatalf("system prompt missing overlay: %q", messages[0].Content) + } + if messages[1].Role != "user" || messages[1].Content != "do child task" { + t.Fatalf("messages[1] = %#v, want user task", messages[1]) + } +} + +func TestBuildMessagesFromPrompt_AttachesInternalPromptMetadata(t *testing.T) { + t.Setenv("PICOCLAW_BUILTIN_SKILLS", t.TempDir()) + cb := NewContextBuilder(t.TempDir()) + + messages := cb.BuildMessagesFromPrompt(PromptBuildRequest{ + CurrentMessage: "hello", + Summary: "prior context", + }) + if len(messages) != 2 { + t.Fatalf("messages len = %d, want 2", len(messages)) + } + + system := messages[0] + if len(system.SystemParts) < 3 { + t.Fatalf("system parts len = %d, want at least 3", len(system.SystemParts)) + } + if system.SystemParts[0].PromptLayer != string(PromptLayerKernel) || + system.SystemParts[0].PromptSlot != string(PromptSlotIdentity) || + system.SystemParts[0].PromptSource != string(PromptSourceKernel) { + t.Fatalf("static system metadata = %#v, want kernel identity", system.SystemParts[0]) + } + + var hasRuntime, hasSummary bool + for _, part := range system.SystemParts { + switch part.PromptSource { + case string(PromptSourceRuntime): + hasRuntime = true + case string(PromptSourceSummary): + hasSummary = true + } + } + if !hasRuntime { + t.Fatal("system parts missing runtime prompt metadata") + } + if !hasSummary { + t.Fatal("system parts missing summary prompt metadata") + } + + user := messages[1] + if user.PromptLayer != string(PromptLayerTurn) || + user.PromptSlot != string(PromptSlotMessage) || + user.PromptSource != string(PromptSourceUserMessage) { + t.Fatalf("user message metadata = %#v, want turn message", user) + } + + data, err := json.Marshal(messages) + if err != nil { + t.Fatalf("json.Marshal() error = %v", err) + } + if strings.Contains(string(data), "PromptSource") || + strings.Contains(string(data), "PromptLayer") || + strings.Contains(string(data), "PromptSlot") { + t.Fatalf("internal prompt metadata leaked into JSON: %s", data) + } +} + +type testPromptContributor struct { + desc PromptSourceDescriptor + part PromptPart +} + +func (c testPromptContributor) PromptSource() PromptSourceDescriptor { + return c.desc +} + +func (c testPromptContributor) ContributePrompt(_ context.Context, _ PromptBuildRequest) ([]PromptPart, error) { + return []PromptPart{c.part}, nil +} + +func TestContextBuilder_CollectsRegisteredPromptContributors(t *testing.T) { + t.Setenv("PICOCLAW_BUILTIN_SKILLS", t.TempDir()) + cb := NewContextBuilder(t.TempDir()) + + sourceID := PromptSourceID("test:contributor") + err := cb.RegisterPromptContributor(testPromptContributor{ + desc: PromptSourceDescriptor{ + ID: sourceID, + Owner: "test", + Allowed: []PromptPlacement{{Layer: PromptLayerCapability, Slot: PromptSlotMCP}}, + }, + part: PromptPart{ + ID: "capability.mcp.test", + Layer: PromptLayerCapability, + Slot: PromptSlotMCP, + Source: PromptSource{ID: sourceID, Name: "test"}, + Content: "registered contributor prompt", + }, + }) + if err != nil { + t.Fatalf("RegisterPromptContributor() error = %v", err) + } + + messages := cb.BuildMessagesFromPrompt(PromptBuildRequest{CurrentMessage: "hello"}) + if !strings.Contains(messages[0].Content, "registered contributor prompt") { + t.Fatalf("system prompt missing contributor content: %q", messages[0].Content) + } +} diff --git a/pkg/agent/prompt_turn.go b/pkg/agent/prompt_turn.go new file mode 100644 index 000000000..7b7d295c1 --- /dev/null +++ b/pkg/agent/prompt_turn.go @@ -0,0 +1,117 @@ +package agent + +import ( + "fmt" + "strings" + + "github.com/sipeed/picoclaw/pkg/providers" +) + +func promptBuildRequestForTurn( + ts *turnState, + history []providers.Message, + summary string, + currentMessage string, + media []string, +) PromptBuildRequest { + return PromptBuildRequest{ + History: history, + Summary: summary, + CurrentMessage: currentMessage, + Media: append([]string(nil), media...), + Channel: ts.channel, + ChatID: ts.chatID, + SenderID: ts.opts.Dispatch.SenderID(), + SenderDisplayName: ts.opts.SenderDisplayName, + ActiveSkills: activeSkillNames(ts.agent, ts.opts), + Overlays: promptOverlaysForOptions(ts.opts), + } +} + +func promptOverlaysForOptions(opts processOptions) []PromptPart { + systemPrompt := strings.TrimSpace(opts.SystemPromptOverride) + if systemPrompt == "" { + return nil + } + + return []PromptPart{ + { + ID: "instruction.subturn_profile", + Layer: PromptLayerInstruction, + Slot: PromptSlotWorkspace, + Source: PromptSource{ID: PromptSourceSubTurnProfile, Name: "subturn.profile"}, + Title: "SubTurn System Instructions", + Content: systemPrompt, + Stable: false, + Cache: PromptCacheNone, + }, + } +} + +func promptContentBlock(part PromptPart, cache *providers.CacheControl) providers.ContentBlock { + return providers.ContentBlock{ + Type: "text", + Text: part.Content, + CacheControl: cache, + PromptLayer: string(part.Layer), + PromptSlot: string(part.Slot), + PromptSource: string(part.Source.ID), + } +} + +func promptMessageWithMetadata( + msg providers.Message, + layer PromptLayer, + slot PromptSlot, + source PromptSourceID, +) providers.Message { + msg.PromptLayer = string(layer) + msg.PromptSlot = string(slot) + msg.PromptSource = string(source) + return msg +} + +func promptMessageWithDefaultMetadata( + msg providers.Message, + layer PromptLayer, + slot PromptSlot, + source PromptSourceID, +) providers.Message { + if strings.TrimSpace(msg.PromptSource) != "" { + return msg + } + return promptMessageWithMetadata(msg, layer, slot, source) +} + +func userPromptMessage(content string, media []string) providers.Message { + msg := providers.Message{ + Role: "user", + Content: content, + } + if len(media) > 0 { + msg.Media = append([]string(nil), media...) + } + return promptMessageWithMetadata(msg, PromptLayerTurn, PromptSlotMessage, PromptSourceUserMessage) +} + +func steeringPromptMessage(msg providers.Message) providers.Message { + return promptMessageWithDefaultMetadata(msg, PromptLayerTurn, PromptSlotSteering, PromptSourceSteering) +} + +func subTurnResultPromptMessage(content string) providers.Message { + return promptMessageWithMetadata( + providers.Message{Role: "user", Content: fmt.Sprintf("[SubTurn Result] %s", content)}, + PromptLayerTurn, + PromptSlotSubTurn, + PromptSourceSubTurnResult, + ) +} + +func interruptPromptMessage(content string) providers.Message { + return promptMessageWithMetadata( + providers.Message{Role: "user", Content: content}, + PromptLayerTurn, + PromptSlotInterrupt, + PromptSourceInterrupt, + ) +} diff --git a/pkg/agent/steering.go b/pkg/agent/steering.go index bff01fbf8..2efa7bbf4 100644 --- a/pkg/agent/steering.go +++ b/pkg/agent/steering.go @@ -187,6 +187,7 @@ func (al *AgentLoop) enqueueSteeringMessage(scope, agentID string, msg providers return fmt.Errorf("steering queue is not initialized") } + msg = steeringPromptMessage(msg) if err := al.steering.pushScope(scope, msg); err != nil { logger.WarnCF("agent", "Failed to enqueue steering message", map[string]any{ "error": err.Error(), diff --git a/pkg/agent/turn_coord.go b/pkg/agent/turn_coord.go index 4c8335933..ade2b7c21 100644 --- a/pkg/agent/turn_coord.go +++ b/pkg/agent/turn_coord.go @@ -111,7 +111,7 @@ func (al *AgentLoop) runTurn(ctx context.Context, ts *turnState, pipeline *Pipel case result, ok := <-ts.pendingResults: if ok && result != nil && result.ForLLM != "" { content := al.cfg.FilterSensitiveData(result.ForLLM) - msg := providers.Message{Role: "user", Content: fmt.Sprintf("[SubTurn Result] %s", content)} + msg := subTurnResultPromptMessage(content) pendingMessages = append(pendingMessages, msg) } default: diff --git a/pkg/agent/turn_state.go b/pkg/agent/turn_state.go index 8b5fd4e2c..360c3b7d5 100644 --- a/pkg/agent/turn_state.go +++ b/pkg/agent/turn_state.go @@ -527,10 +527,7 @@ func (ts *turnState) interruptHintMessage() providers.Message { if hint != "" { content += "\n\nInterrupt hint: " + hint } - return providers.Message{ - Role: "user", - Content: content, - } + return interruptPromptMessage(content) } // ============================================================================= diff --git a/pkg/providers/protocoltypes/types.go b/pkg/providers/protocoltypes/types.go index f3553f8b0..ca123a2b2 100644 --- a/pkg/providers/protocoltypes/types.go +++ b/pkg/providers/protocoltypes/types.go @@ -61,6 +61,13 @@ type ContentBlock struct { Type string `json:"type"` // "text" Text string `json:"text"` CacheControl *CacheControl `json:"cache_control,omitempty"` + + // Prompt metadata is internal to the agent runtime. It records which + // structured prompt segment produced this block without changing provider + // JSON. + PromptLayer string `json:"-"` + PromptSlot string `json:"-"` + PromptSource string `json:"-"` } type Attachment struct { @@ -80,6 +87,12 @@ type Message struct { SystemParts []ContentBlock `json:"system_parts,omitempty"` // structured system blocks for cache-aware adapters ToolCalls []ToolCall `json:"tool_calls,omitempty"` ToolCallID string `json:"tool_call_id,omitempty"` + + // Prompt metadata is internal to the agent runtime. It records where a + // message or system part came from without changing provider/session JSON. + PromptLayer string `json:"-"` + PromptSlot string `json:"-"` + PromptSource string `json:"-"` } type ToolDefinition struct { From 48d89525918b7c45e6e9090d8bba4555d68e7a23 Mon Sep 17 00:00:00 2001 From: Hoshina Date: Fri, 24 Apr 2026 19:36:46 +0800 Subject: [PATCH 19/36] feat(agent): migrate tool prompts to capability slots --- pkg/agent/agent_mcp.go | 19 ++++ pkg/agent/context.go | 70 ++++++++----- pkg/agent/hooks.go | 22 ++-- pkg/agent/hooks_test.go | 75 +++++++++++++ pkg/agent/prompt.go | 15 ++- pkg/agent/prompt_contributors.go | 139 +++++++++++++++++++++++++ pkg/agent/prompt_test.go | 56 ++++++++++ pkg/providers/protocoltypes/types.go | 7 ++ pkg/tools/integration/mcp_tool.go | 9 ++ pkg/tools/integration/mcp_tool_test.go | 17 +++ pkg/tools/registry.go | 25 +++++ pkg/tools/registry_test.go | 50 +++++++++ pkg/tools/search_tool.go | 16 +++ pkg/tools/shared/base.go | 18 ++++ pkg/tools/shared_facade.go | 8 ++ 15 files changed, 510 insertions(+), 36 deletions(-) create mode 100644 pkg/agent/prompt_contributors.go diff --git a/pkg/agent/agent_mcp.go b/pkg/agent/agent_mcp.go index 251d32b58..fcb57a5d4 100644 --- a/pkg/agent/agent_mcp.go +++ b/pkg/agent/agent_mcp.go @@ -135,6 +135,25 @@ func (al *AgentLoop) ensureMCPInitialized(ctx context.Context) error { serverCfg := al.cfg.Tools.MCP.Servers[serverName] registerAsHidden := serverIsDeferred(al.cfg.Tools.MCP.Discovery.Enabled, serverCfg) + for _, agentID := range agentIDs { + agent, ok := al.registry.GetAgent(agentID) + if !ok || agent.ContextBuilder == nil { + continue + } + if err := agent.ContextBuilder.RegisterPromptContributor(mcpServerPromptContributor{ + serverName: serverName, + toolCount: len(conn.Tools), + deferred: registerAsHidden, + }); err != nil { + logger.WarnCF("agent", "Failed to register MCP prompt contributor", + map[string]any{ + "agent_id": agentID, + "server": serverName, + "error": err.Error(), + }) + } + } + for _, tool := range conn.Tools { for _, agentID := range agentIDs { agent, ok := al.registry.GetAgent(agentID) diff --git a/pkg/agent/context.go b/pkg/agent/context.go index feb04e347..ecde7c33e 100644 --- a/pkg/agent/context.go +++ b/pkg/agent/context.go @@ -22,13 +22,11 @@ import ( ) type ContextBuilder struct { - workspace string - skillsLoader *skills.SkillsLoader - memory *MemoryStore - toolDiscoveryBM25 bool - toolDiscoveryRegex bool - splitOnMarker bool - promptRegistry *PromptRegistry + workspace string + skillsLoader *skills.SkillsLoader + memory *MemoryStore + splitOnMarker bool + promptRegistry *PromptRegistry // Cache for system prompt to avoid rebuilding on every call. // This fixes issue #607: repeated reprocessing of the entire context. @@ -50,8 +48,16 @@ type ContextBuilder struct { } func (cb *ContextBuilder) WithToolDiscovery(useBM25, useRegex bool) *ContextBuilder { - cb.toolDiscoveryBM25 = useBM25 - cb.toolDiscoveryRegex = useRegex + if useBM25 || useRegex { + if err := cb.RegisterPromptContributor(toolDiscoveryPromptContributor{ + useBM25: useBM25, + useRegex: useRegex, + }); err != nil { + logger.WarnCF("agent", "Failed to register tool discovery prompt contributor", map[string]any{ + "error": err.Error(), + }) + } + } return cb } @@ -83,11 +89,19 @@ func NewContextBuilder(workspace string) *ContextBuilder { } func (cb *ContextBuilder) RegisterPromptSource(desc PromptSourceDescriptor) error { - return cb.promptRegistryOrDefault().RegisterSource(desc) + err := cb.promptRegistryOrDefault().RegisterSource(desc) + if err == nil { + cb.InvalidateCache() + } + return err } func (cb *ContextBuilder) RegisterPromptContributor(contributor PromptContributor) error { - return cb.promptRegistryOrDefault().RegisterContributor(contributor) + err := cb.promptRegistryOrDefault().RegisterContributor(contributor) + if err == nil { + cb.InvalidateCache() + } + return err } func (cb *ContextBuilder) promptRegistryOrDefault() *PromptRegistry { @@ -124,16 +138,16 @@ Your workspace is at: %s version, workspacePath, workspacePath, workspacePath, workspacePath, workspacePath) } -func (cb *ContextBuilder) getDiscoveryRule() string { - if !cb.toolDiscoveryBM25 && !cb.toolDiscoveryRegex { +func formatToolDiscoveryRule(useBM25, useRegex bool) string { + if !useBM25 && !useRegex { return "" } var toolNames []string - if cb.toolDiscoveryBM25 { + if useBM25 { toolNames = append(toolNames, `"tool_search_tool_bm25"`) } - if cb.toolDiscoveryRegex { + if useRegex { toolNames = append(toolNames, `"tool_search_tool_regex"`) } @@ -173,19 +187,6 @@ func (cb *ContextBuilder) BuildSystemPromptParts() []PromptPart { Cache: PromptCacheEphemeral, }) - if toolDiscovery := cb.getDiscoveryRule(); toolDiscovery != "" { - add(PromptPart{ - ID: "capability.tool_discovery", - Layer: PromptLayerCapability, - Slot: PromptSlotTooling, - Source: PromptSource{ID: PromptSourceToolDiscovery, Name: "tool_registry:discovery"}, - Title: "tool discovery", - Content: toolDiscovery, - Stable: true, - Cache: PromptCacheEphemeral, - }) - } - // Bootstrap files bootstrapContent := cb.LoadBootstrapFiles() if bootstrapContent != "" { @@ -318,6 +319,19 @@ func (cb *ContextBuilder) EstimateSystemTokens(summary string, activeSkills []st totalChars += 7 // separator \n\n---\n\n } + if contributedParts, err := cb.promptRegistryOrDefault().Collect(context.Background(), PromptBuildRequest{ + Summary: summary, + ActiveSkills: append([]string(nil), activeSkills...), + }); err == nil { + for _, part := range contributedParts { + if strings.TrimSpace(part.Content) == "" { + continue + } + totalChars += utf8.RuneCountInString(part.Content) + totalChars += 7 // separator + } + } + if summary != "" { // Matches the CONTEXT_SUMMARY: prefix added in BuildMessages const summaryPrefix = "CONTEXT_SUMMARY: The following is an approximate summary of prior conversation " + diff --git a/pkg/agent/hooks.go b/pkg/agent/hooks.go index e9863c4f6..69336b89f 100644 --- a/pkg/agent/hooks.go +++ b/pkg/agent/hooks.go @@ -377,14 +377,18 @@ func (hm *HookManager) applyBeforeLLMControls( if next == nil || current == nil { return next } - if llmHookSystemMessagesUnchanged(current.Messages, next.Messages) { - return next + if !llmHookSystemMessagesUnchanged(current.Messages, next.Messages) { + logger.WarnCF("hooks", "Hook attempted to modify system prompt; preserving original messages", map[string]any{ + "hook": hookName, + }) + next.Messages = cloneProviderMessages(current.Messages) + } + if !llmHookToolDefinitionsUnchanged(current.Tools, next.Tools) { + logger.WarnCF("hooks", "Hook attempted to modify tool definitions; preserving original tools", map[string]any{ + "hook": hookName, + }) + next.Tools = cloneToolDefinitions(current.Tools) } - - logger.WarnCF("hooks", "Hook attempted to modify system prompt; preserving original messages", map[string]any{ - "hook": hookName, - }) - next.Messages = cloneProviderMessages(current.Messages) return next } @@ -413,6 +417,10 @@ func systemMessageFingerprints(messages []providers.Message) []systemMessageFing return fingerprints } +func llmHookToolDefinitionsUnchanged(before, after []providers.ToolDefinition) bool { + return reflect.DeepEqual(cloneToolDefinitions(before), cloneToolDefinitions(after)) +} + func (hm *HookManager) BeforeTool( ctx context.Context, call *ToolCallHookRequest, diff --git a/pkg/agent/hooks_test.go b/pkg/agent/hooks_test.go index 419752e5b..a936296dd 100644 --- a/pkg/agent/hooks_test.go +++ b/pkg/agent/hooks_test.go @@ -186,6 +186,36 @@ func (h *llmUserAppendHook) AfterLLM( return resp.Clone(), HookDecision{Action: HookActionContinue}, nil } +type llmToolRewriteHook struct{} + +func (h *llmToolRewriteHook) BeforeLLM( + ctx context.Context, + req *LLMHookRequest, +) (*LLMHookRequest, HookDecision, error) { + next := req.Clone() + next.Model = "changed-model" + next.Tools[0].Function.Description = "rewritten tool" + next.Tools = append(next.Tools, providers.ToolDefinition{ + Type: "function", + Function: providers.ToolFunctionDefinition{ + Name: "hook_tool", + Description: "hook tool", + Parameters: map[string]any{"type": "object"}, + }, + PromptLayer: string(PromptLayerCapability), + PromptSlot: string(PromptSlotTooling), + PromptSource: "hook:test", + }) + return next, HookDecision{Action: HookActionModify}, nil +} + +func (h *llmToolRewriteHook) AfterLLM( + ctx context.Context, + resp *LLMHookResponse, +) (*LLMHookResponse, HookDecision, error) { + return resp.Clone(), HookDecision{Action: HookActionContinue}, nil +} + func TestHookManager_BeforeLLMControlsSystemPromptMutation(t *testing.T) { hm := NewHookManager(nil) if err := hm.Mount(NamedHook("rewrite-system", &llmSystemRewriteHook{})); err != nil { @@ -244,6 +274,51 @@ func TestHookManager_BeforeLLMAllowsNonSystemMessageMutation(t *testing.T) { } } +func TestHookManager_BeforeLLMControlsToolDefinitionMutation(t *testing.T) { + hm := NewHookManager(nil) + if err := hm.Mount(NamedHook("rewrite-tool", &llmToolRewriteHook{})); err != nil { + t.Fatalf("Mount() error = %v", err) + } + + req := &LLMHookRequest{ + Model: "original-model", + Messages: []providers.Message{ + {Role: "system", Content: "system"}, + {Role: "user", Content: "hello"}, + }, + Tools: []providers.ToolDefinition{ + { + Type: "function", + Function: providers.ToolFunctionDefinition{ + Name: "mcp_github_create_issue", + Description: "create issue", + Parameters: map[string]any{"type": "object"}, + }, + PromptLayer: string(PromptLayerCapability), + PromptSlot: string(PromptSlotMCP), + PromptSource: "mcp:github", + }, + }, + } + + got, decision := hm.BeforeLLM(context.Background(), req) + if decision.normalizedAction() != HookActionContinue { + t.Fatalf("decision = %v, want continue", decision) + } + if got.Model != "changed-model" { + t.Fatalf("model = %q, want changed-model", got.Model) + } + if len(got.Tools) != 1 { + t.Fatalf("tools len = %d, want original 1", len(got.Tools)) + } + if got.Tools[0].Function.Description != "create issue" { + t.Fatalf("tool description = %q, want original", got.Tools[0].Function.Description) + } + if got.Tools[0].PromptSource != "mcp:github" || got.Tools[0].PromptSlot != string(PromptSlotMCP) { + t.Fatalf("tool prompt metadata = %#v, want original mcp metadata", got.Tools[0]) + } +} + func TestAgentLoop_Hooks_ObserverAndLLMInterceptor(t *testing.T) { provider := &llmHookTestProvider{} al, agent, cleanup := newHookTestLoop(t, provider) diff --git a/pkg/agent/prompt.go b/pkg/agent/prompt.go index aa06bba46..be5ccddf2 100644 --- a/pkg/agent/prompt.go +++ b/pkg/agent/prompt.go @@ -52,6 +52,7 @@ const ( PromptSourceMemory PromptSourceID = "memory:workspace" PromptSourceSkillCatalog PromptSourceID = "skill:index" PromptSourceActiveSkills PromptSourceID = "skill:active" + PromptSourceToolRegistry PromptSourceID = "tool_registry:native" PromptSourceToolDiscovery PromptSourceID = "tool_registry:discovery" PromptSourceOutputPolicy PromptSourceID = "runtime.output" PromptSourceSubTurnProfile PromptSourceID = "subturn.profile" @@ -173,6 +174,13 @@ func builtinPromptSources() []PromptSourceDescriptor { Allowed: []PromptPlacement{{Layer: PromptLayerCapability, Slot: PromptSlotTooling}}, StableByDefault: true, }, + { + ID: PromptSourceToolRegistry, + Owner: "tools", + Description: "Native provider tool definitions", + Allowed: []PromptPlacement{{Layer: PromptLayerCapability, Slot: PromptSlotTooling}}, + StableByDefault: true, + }, { ID: PromptSourceSkillCatalog, Owner: "skills", @@ -278,12 +286,17 @@ func (r *PromptRegistry) RegisterContributor(contributor PromptContributor) erro if contributor == nil { return fmt.Errorf("prompt contributor is nil") } - if err := r.RegisterSource(contributor.PromptSource()); err != nil { + desc := contributor.PromptSource() + desc.ID = PromptSourceID(strings.TrimSpace(string(desc.ID))) + if err := r.RegisterSource(desc); err != nil { return err } r.mu.Lock() defer r.mu.Unlock() + r.contributors = slices.DeleteFunc(r.contributors, func(existing PromptContributor) bool { + return PromptSourceID(strings.TrimSpace(string(existing.PromptSource().ID))) == desc.ID + }) r.contributors = append(r.contributors, contributor) return nil } diff --git a/pkg/agent/prompt_contributors.go b/pkg/agent/prompt_contributors.go new file mode 100644 index 000000000..960572e03 --- /dev/null +++ b/pkg/agent/prompt_contributors.go @@ -0,0 +1,139 @@ +package agent + +import ( + "context" + "fmt" + "strings" +) + +type toolDiscoveryPromptContributor struct { + useBM25 bool + useRegex bool +} + +func (c toolDiscoveryPromptContributor) PromptSource() PromptSourceDescriptor { + return PromptSourceDescriptor{ + ID: PromptSourceToolDiscovery, + Owner: "tools", + Description: "Tool discovery instructions", + Allowed: []PromptPlacement{{Layer: PromptLayerCapability, Slot: PromptSlotTooling}}, + StableByDefault: true, + } +} + +func (c toolDiscoveryPromptContributor) ContributePrompt( + _ context.Context, + _ PromptBuildRequest, +) ([]PromptPart, error) { + content := formatToolDiscoveryRule(c.useBM25, c.useRegex) + if strings.TrimSpace(content) == "" { + return nil, nil + } + + return []PromptPart{ + { + ID: "capability.tool_discovery", + Layer: PromptLayerCapability, + Slot: PromptSlotTooling, + Source: PromptSource{ID: PromptSourceToolDiscovery, Name: "tool_registry:discovery"}, + Title: "tool discovery", + Content: content, + Stable: true, + Cache: PromptCacheEphemeral, + }, + }, nil +} + +type mcpServerPromptContributor struct { + serverName string + toolCount int + deferred bool +} + +func (c mcpServerPromptContributor) PromptSource() PromptSourceDescriptor { + return PromptSourceDescriptor{ + ID: mcpPromptSourceID(c.serverName), + Owner: "mcp", + Description: fmt.Sprintf("MCP server %q capability prompt", c.serverName), + Allowed: []PromptPlacement{{Layer: PromptLayerCapability, Slot: PromptSlotMCP}}, + StableByDefault: true, + } +} + +func (c mcpServerPromptContributor) ContributePrompt( + _ context.Context, + _ PromptBuildRequest, +) ([]PromptPart, error) { + serverName := strings.TrimSpace(c.serverName) + if serverName == "" || c.toolCount <= 0 { + return nil, nil + } + + availability := "available as native tools" + if c.deferred { + availability = "hidden behind tool discovery until unlocked" + } + + return []PromptPart{ + { + ID: "capability.mcp." + promptSourceComponent(serverName), + Layer: PromptLayerCapability, + Slot: PromptSlotMCP, + Source: PromptSource{ID: mcpPromptSourceID(serverName), Name: "mcp:" + serverName}, + Title: "MCP server capability", + Content: fmt.Sprintf( + "MCP server `%s` is connected. It contributes %d tool(s), currently %s.", + serverName, + c.toolCount, + availability, + ), + Stable: true, + Cache: PromptCacheEphemeral, + }, + }, nil +} + +func mcpPromptSourceID(serverName string) PromptSourceID { + return PromptSourceID("mcp:" + promptSourceComponent(serverName)) +} + +func promptSourceComponent(value string) string { + const maxLen = 64 + + value = strings.ToLower(strings.TrimSpace(value)) + if value == "" { + return "unnamed" + } + + var b strings.Builder + lastWasSep := false + for _, r := range value { + switch { + case r >= 'a' && r <= 'z': + b.WriteRune(r) + lastWasSep = false + case r >= '0' && r <= '9': + b.WriteRune(r) + lastWasSep = false + case r == '-' || r == '_': + if !lastWasSep && b.Len() > 0 { + b.WriteRune(r) + lastWasSep = true + } + default: + if !lastWasSep && b.Len() > 0 { + b.WriteRune('_') + lastWasSep = true + } + } + } + + result := strings.Trim(b.String(), "_") + if result == "" { + return "unnamed" + } + if len(result) > maxLen { + return result[:maxLen] + } + return result +} diff --git a/pkg/agent/prompt_test.go b/pkg/agent/prompt_test.go index af46eae7a..b3f609610 100644 --- a/pkg/agent/prompt_test.go +++ b/pkg/agent/prompt_test.go @@ -164,6 +164,62 @@ func TestBuildMessagesFromPrompt_AttachesInternalPromptMetadata(t *testing.T) { } } +func TestContextBuilder_CollectsToolDiscoveryContributor(t *testing.T) { + t.Setenv("PICOCLAW_BUILTIN_SKILLS", t.TempDir()) + cb := NewContextBuilder(t.TempDir()).WithToolDiscovery(true, false) + + messages := cb.BuildMessagesFromPrompt(PromptBuildRequest{CurrentMessage: "hello"}) + system := messages[0] + if !strings.Contains(system.Content, "tool_search_tool_bm25") { + t.Fatalf("system prompt missing tool discovery rule: %q", system.Content) + } + + var found bool + for _, part := range system.SystemParts { + if part.PromptSource == string(PromptSourceToolDiscovery) { + found = true + if part.PromptLayer != string(PromptLayerCapability) || part.PromptSlot != string(PromptSlotTooling) { + t.Fatalf("tool discovery metadata = %#v, want capability/tooling", part) + } + } + } + if !found { + t.Fatal("system parts missing tool discovery prompt metadata") + } +} + +func TestContextBuilder_CollectsMCPServerContributor(t *testing.T) { + t.Setenv("PICOCLAW_BUILTIN_SKILLS", t.TempDir()) + cb := NewContextBuilder(t.TempDir()) + err := cb.RegisterPromptContributor(mcpServerPromptContributor{ + serverName: "GitHub Server", + toolCount: 3, + deferred: true, + }) + if err != nil { + t.Fatalf("RegisterPromptContributor() error = %v", err) + } + + messages := cb.BuildMessagesFromPrompt(PromptBuildRequest{CurrentMessage: "hello"}) + system := messages[0] + if !strings.Contains(system.Content, "MCP server `GitHub Server` is connected") { + t.Fatalf("system prompt missing MCP contributor content: %q", system.Content) + } + + var found bool + for _, part := range system.SystemParts { + if part.PromptSource == "mcp:github_server" { + found = true + if part.PromptLayer != string(PromptLayerCapability) || part.PromptSlot != string(PromptSlotMCP) { + t.Fatalf("mcp metadata = %#v, want capability/mcp", part) + } + } + } + if !found { + t.Fatal("system parts missing MCP prompt metadata") + } +} + type testPromptContributor struct { desc PromptSourceDescriptor part PromptPart diff --git a/pkg/providers/protocoltypes/types.go b/pkg/providers/protocoltypes/types.go index ca123a2b2..bab4433e7 100644 --- a/pkg/providers/protocoltypes/types.go +++ b/pkg/providers/protocoltypes/types.go @@ -98,6 +98,13 @@ type Message struct { type ToolDefinition struct { Type string `json:"type"` Function ToolFunctionDefinition `json:"function"` + + // Prompt metadata is internal to the agent runtime. Tool definitions are + // model-visible capability prompts even though providers send them outside + // the system message. + PromptLayer string `json:"-"` + PromptSlot string `json:"-"` + PromptSource string `json:"-"` } type ToolFunctionDefinition struct { diff --git a/pkg/tools/integration/mcp_tool.go b/pkg/tools/integration/mcp_tool.go index 340bb9e8e..78c348316 100644 --- a/pkg/tools/integration/mcp_tool.go +++ b/pkg/tools/integration/mcp_tool.go @@ -15,6 +15,7 @@ import ( "github.com/sipeed/picoclaw/pkg/logger" "github.com/sipeed/picoclaw/pkg/media" + toolshared "github.com/sipeed/picoclaw/pkg/tools/shared" ) // MCPManager defines the interface for MCP manager operations @@ -161,6 +162,14 @@ func (t *MCPTool) Description() string { return fmt.Sprintf("[MCP:%s] %s", t.serverName, desc) } +func (t *MCPTool) PromptMetadata() toolshared.PromptMetadata { + return toolshared.PromptMetadata{ + Layer: toolshared.ToolPromptLayerCapability, + Slot: toolshared.ToolPromptSlotMCP, + Source: "mcp:" + sanitizeIdentifierComponent(t.serverName), + } +} + // Parameters returns the tool parameters schema func (t *MCPTool) Parameters() map[string]any { // The InputSchema is already a JSON Schema object diff --git a/pkg/tools/integration/mcp_tool_test.go b/pkg/tools/integration/mcp_tool_test.go index e5c54abb6..7b0b2cd5a 100644 --- a/pkg/tools/integration/mcp_tool_test.go +++ b/pkg/tools/integration/mcp_tool_test.go @@ -11,6 +11,7 @@ import ( "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/sipeed/picoclaw/pkg/media" + toolshared "github.com/sipeed/picoclaw/pkg/tools/shared" ) // MockMCPManager is a mock implementation of MCPManager interface for testing @@ -104,6 +105,22 @@ func TestMCPTool_Name(t *testing.T) { } } +func TestMCPTool_PromptMetadata(t *testing.T) { + manager := &MockMCPManager{} + tool := NewMCPTool(manager, "GitHub Server", &mcp.Tool{Name: "create_issue"}) + + metadata := tool.PromptMetadata() + if metadata.Layer != toolshared.ToolPromptLayerCapability { + t.Fatalf("metadata.Layer = %q, want %q", metadata.Layer, toolshared.ToolPromptLayerCapability) + } + if metadata.Slot != toolshared.ToolPromptSlotMCP { + t.Fatalf("metadata.Slot = %q, want %q", metadata.Slot, toolshared.ToolPromptSlotMCP) + } + if metadata.Source != "mcp:github_server" { + t.Fatalf("metadata.Source = %q, want mcp:github_server", metadata.Source) + } +} + // TestMCPTool_Description verifies tool description generation func TestMCPTool_Description(t *testing.T) { tests := []struct { diff --git a/pkg/tools/registry.go b/pkg/tools/registry.go index e51dff71a..0ff9293a3 100644 --- a/pkg/tools/registry.go +++ b/pkg/tools/registry.go @@ -352,6 +352,7 @@ func (r *ToolRegistry) ToProviderDefs() []providers.ToolDefinition { name, _ := fn["name"].(string) desc, _ := fn["description"].(string) params, _ := fn["parameters"].(map[string]any) + metadata := promptMetadataForTool(entry.Tool) definitions = append(definitions, providers.ToolDefinition{ Type: "function", @@ -360,11 +361,35 @@ func (r *ToolRegistry) ToProviderDefs() []providers.ToolDefinition { Description: desc, Parameters: params, }, + PromptLayer: metadata.Layer, + PromptSlot: metadata.Slot, + PromptSource: metadata.Source, }) } return definitions } +func promptMetadataForTool(tool Tool) PromptMetadata { + metadata := PromptMetadata{ + Layer: ToolPromptLayerCapability, + Slot: ToolPromptSlotTooling, + Source: ToolPromptSourceRegistry, + } + if provider, ok := tool.(PromptMetadataProvider); ok { + provided := provider.PromptMetadata() + if provided.Layer != "" { + metadata.Layer = provided.Layer + } + if provided.Slot != "" { + metadata.Slot = provided.Slot + } + if provided.Source != "" { + metadata.Source = provided.Source + } + } + return metadata +} + // List returns a list of all registered tool names. func (r *ToolRegistry) List() []string { r.mu.RLock() diff --git a/pkg/tools/registry_test.go b/pkg/tools/registry_test.go index 16bd30928..eac96382f 100644 --- a/pkg/tools/registry_test.go +++ b/pkg/tools/registry_test.go @@ -39,6 +39,15 @@ func (m *mockContextAwareTool) Execute(ctx context.Context, _ map[string]any) *T return m.result } +type mockPromptMetadataTool struct { + mockRegistryTool + metadata PromptMetadata +} + +func (m *mockPromptMetadataTool) PromptMetadata() PromptMetadata { + return m.metadata +} + type mockAsyncRegistryTool struct { mockRegistryTool lastCB AsyncCallback @@ -375,6 +384,47 @@ func TestToolToSchema(t *testing.T) { } } +func TestToolRegistry_ToProviderDefsAttachesPromptMetadata(t *testing.T) { + r := NewToolRegistry() + r.Register(newMockTool("native", "native tool")) + r.Register(&mockPromptMetadataTool{ + mockRegistryTool: mockRegistryTool{ + name: "mcp_demo", + desc: "mcp tool", + params: map[string]any{"type": "object"}, + }, + metadata: PromptMetadata{ + Layer: ToolPromptLayerCapability, + Slot: ToolPromptSlotMCP, + Source: "mcp:demo", + }, + }) + + defs := r.ToProviderDefs() + if len(defs) != 2 { + t.Fatalf("ToProviderDefs() len = %d, want 2", len(defs)) + } + + byName := make(map[string]providers.ToolDefinition, len(defs)) + for _, def := range defs { + byName[def.Function.Name] = def + } + + native := byName["native"] + if native.PromptLayer != ToolPromptLayerCapability || + native.PromptSlot != ToolPromptSlotTooling || + native.PromptSource != ToolPromptSourceRegistry { + t.Fatalf("native prompt metadata = %#v, want default tooling source", native) + } + + mcp := byName["mcp_demo"] + if mcp.PromptLayer != ToolPromptLayerCapability || + mcp.PromptSlot != ToolPromptSlotMCP || + mcp.PromptSource != "mcp:demo" { + t.Fatalf("mcp prompt metadata = %#v, want mcp source", mcp) + } +} + func TestToolRegistry_Clone(t *testing.T) { r := NewToolRegistry() r.Register(newMockTool("read_file", "reads files")) diff --git a/pkg/tools/search_tool.go b/pkg/tools/search_tool.go index f41c80d90..c5884c9de 100644 --- a/pkg/tools/search_tool.go +++ b/pkg/tools/search_tool.go @@ -34,6 +34,14 @@ func (t *RegexSearchTool) Description() string { return "Search available hidden tools on-demand using a regex pattern. Returns JSON schemas of discovered tools." } +func (t *RegexSearchTool) PromptMetadata() PromptMetadata { + return PromptMetadata{ + Layer: ToolPromptLayerCapability, + Slot: ToolPromptSlotTooling, + Source: ToolPromptSourceDiscovery, + } +} + func (t *RegexSearchTool) Parameters() map[string]any { return map[string]any{ "type": "object", @@ -95,6 +103,14 @@ func (t *BM25SearchTool) Description() string { return "Search available hidden tools on-demand using natural language query describing the action you need to perform. Returns JSON schemas of discovered tools." } +func (t *BM25SearchTool) PromptMetadata() PromptMetadata { + return PromptMetadata{ + Layer: ToolPromptLayerCapability, + Slot: ToolPromptSlotTooling, + Source: ToolPromptSourceDiscovery, + } +} + func (t *BM25SearchTool) Parameters() map[string]any { return map[string]any{ "type": "object", diff --git a/pkg/tools/shared/base.go b/pkg/tools/shared/base.go index 5498d24ab..298e1b478 100644 --- a/pkg/tools/shared/base.go +++ b/pkg/tools/shared/base.go @@ -14,6 +14,24 @@ type Tool interface { Execute(ctx context.Context, args map[string]any) *ToolResult } +const ( + ToolPromptLayerCapability = "capability" + ToolPromptSlotTooling = "tooling" + ToolPromptSlotMCP = "mcp" + ToolPromptSourceRegistry = "tool_registry:native" + ToolPromptSourceDiscovery = "tool_registry:discovery" +) + +type PromptMetadata struct { + Layer string + Slot string + Source string +} + +type PromptMetadataProvider interface { + PromptMetadata() PromptMetadata +} + // --- Request-scoped tool context (channel / chatID) --- // // Carried via context.Value so that concurrent tool calls each receive diff --git a/pkg/tools/shared_facade.go b/pkg/tools/shared_facade.go index 6e40e4e3a..8409ea060 100644 --- a/pkg/tools/shared_facade.go +++ b/pkg/tools/shared_facade.go @@ -22,12 +22,20 @@ type ( Tool = toolshared.Tool AsyncCallback = toolshared.AsyncCallback AsyncExecutor = toolshared.AsyncExecutor + PromptMetadata = toolshared.PromptMetadata + PromptMetadataProvider = toolshared.PromptMetadataProvider ToolResult = toolshared.ToolResult ) const ( handledToolLLMNote = toolshared.HandledToolLLMNote artifactPathsLLMNote = toolshared.ArtifactPathsLLMNote + + ToolPromptLayerCapability = toolshared.ToolPromptLayerCapability + ToolPromptSlotTooling = toolshared.ToolPromptSlotTooling + ToolPromptSlotMCP = toolshared.ToolPromptSlotMCP + ToolPromptSourceRegistry = toolshared.ToolPromptSourceRegistry + ToolPromptSourceDiscovery = toolshared.ToolPromptSourceDiscovery ) func WithToolContext(ctx context.Context, channel, chatID string) context.Context { From bb0f983708839d94a87cd8a0b1cc4222b2bc3044 Mon Sep 17 00:00:00 2001 From: lc6464 <64722907+lc6464@users.noreply.github.com> Date: Fri, 24 Apr 2026 21:45:41 +0800 Subject: [PATCH 20/36] fix(reasoning): persist canonical history for DeepSeek and web chat --- pkg/agent/agent_test.go | 230 +++++++++++++++ pkg/agent/agent_utils.go | 22 +- pkg/agent/pipeline_finalize.go | 8 +- pkg/agent/pipeline_llm.go | 7 +- pkg/agent/subturn.go | 6 + pkg/memory/jsonl.go | 12 + pkg/memory/jsonl_test.go | 62 ++++ pkg/providers/factory_provider.go | 24 +- pkg/providers/httpapi/http_provider.go | 7 + pkg/providers/messageutil/messageutil.go | 41 +++ pkg/providers/openai_compat/provider.go | 115 +++++++- pkg/providers/openai_compat/provider_test.go | 286 ++++++++++++++++++- pkg/session/manager.go | 10 +- web/backend/api/session.go | 48 +++- web/backend/api/session_test.go | 177 +++++++++++- web/frontend/src/api/sessions.ts | 1 + web/frontend/src/features/chat/history.ts | 3 +- 17 files changed, 1016 insertions(+), 43 deletions(-) create mode 100644 pkg/providers/messageutil/messageutil.go diff --git a/pkg/agent/agent_test.go b/pkg/agent/agent_test.go index b0aa3b468..1cc143847 100644 --- a/pkg/agent/agent_test.go +++ b/pkg/agent/agent_test.go @@ -1720,6 +1720,38 @@ func (m *messageToolProvider) GetDefaultModel() string { return "message-tool-model" } +type reasoningVisibleToolProvider struct { + filePath string + calls int +} + +func (m *reasoningVisibleToolProvider) Chat( + ctx context.Context, + messages []providers.Message, + tools []providers.ToolDefinition, + model string, + opts map[string]any, +) (*providers.LLMResponse, error) { + m.calls++ + if m.calls == 1 { + return &providers.LLMResponse{ + Content: "I'll inspect that file now.", + ReasoningContent: "Read the file before answering.", + ToolCalls: []providers.ToolCall{{ + ID: "call_read_file", + Type: "function", + Name: "read_file", + Arguments: map[string]any{"path": m.filePath}, + }}, + }, nil + } + return &providers.LLMResponse{Content: "DONE"}, nil +} + +func (m *reasoningVisibleToolProvider) GetDefaultModel() string { + return "reasoning-visible-tool-model" +} + type artifactThenSendProvider struct { calls int } @@ -1866,6 +1898,28 @@ func TestToolFeedbackExplanationFromResponse_UsesCurrentContentFirst(t *testing. } } +func TestSideQuestionResponseContent_FallsBackWhenContentIsWhitespace(t *testing.T) { + response := &providers.LLMResponse{ + Content: " \n\t ", + ReasoningContent: "reasoning fallback", + } + + if got := sideQuestionResponseContent(response); got != "reasoning fallback" { + t.Fatalf("sideQuestionResponseContent() = %q, want %q", got, "reasoning fallback") + } +} + +func TestResponseReasoningContent_FallsBackWhenReasoningIsWhitespace(t *testing.T) { + response := &providers.LLMResponse{ + Reasoning: " \n\t ", + ReasoningContent: "structured reasoning fallback", + } + + if got := responseReasoningContent(response); got != "structured reasoning fallback" { + t.Fatalf("responseReasoningContent() = %q, want %q", got, "structured reasoning fallback") + } +} + func TestToolFeedbackExplanationFromResponse_UsesExplicitToolCallExtraContent(t *testing.T) { response := &providers.LLMResponse{ ToolCalls: []providers.ToolCall{{ @@ -3957,6 +4011,182 @@ func TestProcessMessage_PublishesToolFeedbackWhenEnabled(t *testing.T) { } } +func TestProcessMessage_PersistsReasoningContentInSessionHistory(t *testing.T) { + tmpDir := t.TempDir() + cfg := &config.Config{ + Agents: config.AgentsConfig{ + Defaults: config.AgentDefaults{ + Workspace: tmpDir, + ModelName: "test-model", + MaxTokens: 4096, + MaxToolIterations: 10, + }, + }, + } + + msgBus := bus.NewMessageBus() + provider := &reasoningContentProvider{ + response: "final answer", + reasoningContent: "thinking trace", + } + al := NewAgentLoop(cfg, msgBus, provider) + + response, err := al.processMessage(context.Background(), bus.InboundMessage{ + Channel: "pico", + SenderID: "user1", + ChatID: "pico:test-session", + Content: "hello", + }) + if err != nil { + t.Fatalf("processMessage() error = %v", err) + } + if response != "final answer" { + t.Fatalf("processMessage() response = %q, want %q", response, "final answer") + } + + store := al.GetRegistry().GetDefaultAgent().Sessions + sessionKeys := store.ListSessions() + if len(sessionKeys) != 1 { + t.Fatalf("session keys = %v, want exactly 1 active session", sessionKeys) + } + history := store.GetHistory(sessionKeys[0]) + if len(history) < 2 { + t.Fatalf("session history len = %d, want at least 2", len(history)) + } + + last := history[len(history)-1] + if last.Role != "assistant" { + t.Fatalf("last message role = %q, want assistant", last.Role) + } + if last.Content != "final answer" { + t.Fatalf("last message content = %q, want %q", last.Content, "final answer") + } + if last.ReasoningContent != "thinking trace" { + t.Fatalf("last message reasoning_content = %q, want %q", last.ReasoningContent, "thinking trace") + } +} + +func TestProcessMessage_PersistsReasoningToolResponseAsSingleAssistantRecord(t *testing.T) { + tmpDir := t.TempDir() + inspectPath := filepath.Join(tmpDir, "inspect.txt") + if err := os.WriteFile(inspectPath, []byte("inspect me"), 0o644); err != nil { + t.Fatalf("WriteFile(inspectPath) error = %v", err) + } + + cfg := config.DefaultConfig() + cfg.Agents.Defaults.Workspace = tmpDir + cfg.Agents.Defaults.ModelName = "test-model" + cfg.Agents.Defaults.MaxTokens = 4096 + cfg.Agents.Defaults.MaxToolIterations = 10 + + msgBus := bus.NewMessageBus() + provider := &reasoningVisibleToolProvider{filePath: inspectPath} + al := NewAgentLoop(cfg, msgBus, provider) + + response, err := al.processMessage(context.Background(), bus.InboundMessage{ + Channel: "telegram", + SenderID: "user1", + ChatID: "chat1", + Content: "hello", + }) + if err != nil { + t.Fatalf("processMessage() error = %v", err) + } + if response != "DONE" { + t.Fatalf("processMessage() response = %q, want %q", response, "DONE") + } + + store := al.GetRegistry().GetDefaultAgent().Sessions + sessionKeys := store.ListSessions() + if len(sessionKeys) != 1 { + t.Fatalf("session keys = %v, want exactly 1 active session", sessionKeys) + } + + history := store.GetHistory(sessionKeys[0]) + if len(history) < 3 { + t.Fatalf("session history len = %d, want at least 3", len(history)) + } + + var assistantWithToolCall *providers.Message + for i := range history { + msg := history[i] + if msg.Role == "assistant" && len(msg.ToolCalls) > 0 { + assistantWithToolCall = &msg + break + } + } + if assistantWithToolCall == nil { + t.Fatal("expected assistant history record with tool_calls") + } + if assistantWithToolCall.Content != "I'll inspect that file now." { + t.Fatalf("assistant content = %q, want %q", assistantWithToolCall.Content, "I'll inspect that file now.") + } + if assistantWithToolCall.ReasoningContent != "Read the file before answering." { + t.Fatalf("assistant reasoning_content = %q, want preserved", assistantWithToolCall.ReasoningContent) + } + if len(assistantWithToolCall.ToolCalls) != 1 { + t.Fatalf("assistant tool calls = %+v, want single read_file tool", assistantWithToolCall.ToolCalls) + } + if got := providers.NormalizeToolCall(assistantWithToolCall.ToolCalls[0]).Name; got != "read_file" { + t.Fatalf("assistant tool calls = %+v, want single read_file tool", assistantWithToolCall.ToolCalls) + } + + sessionDir := filepath.Join(tmpDir, "sessions") + entries, err := os.ReadDir(sessionDir) + if err != nil { + t.Fatalf("ReadDir(%q) error = %v", sessionDir, err) + } + + var jsonlPath string + for _, entry := range entries { + if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".jsonl") { + continue + } + jsonlPath = filepath.Join(sessionDir, entry.Name()) + break + } + if jsonlPath == "" { + t.Fatal("expected session jsonl file to be created") + } + + data, err := os.ReadFile(jsonlPath) + if err != nil { + t.Fatalf("ReadFile(%q) error = %v", jsonlPath, err) + } + + lines := strings.Split(strings.TrimSpace(string(data)), "\n") + if len(lines) < 3 { + t.Fatalf("jsonl lines = %d, want at least 3", len(lines)) + } + + matchingRecords := 0 + for _, line := range lines { + var msg providers.Message + if err := json.Unmarshal([]byte(line), &msg); err != nil { + t.Fatalf("Unmarshal(jsonl line) error = %v", err) + } + if msg.Role != "assistant" { + continue + } + if msg.Content == "I'll inspect that file now." || msg.ReasoningContent == "Read the file before answering." { + matchingRecords++ + toolName := "" + if len(msg.ToolCalls) == 1 { + toolName = providers.NormalizeToolCall(msg.ToolCalls[0]).Name + } + if msg.Content != "I'll inspect that file now." || + msg.ReasoningContent != "Read the file before answering." || + len(msg.ToolCalls) != 1 || + toolName != "read_file" { + t.Fatalf("assistant jsonl record = %+v, want content+reasoning+tool_calls in one line", msg) + } + } + } + if matchingRecords != 1 { + t.Fatalf("matching assistant jsonl records = %d, want exactly 1 canonical assistant record", matchingRecords) + } +} + func TestProcessMessage_DoesNotLeakReasoningContentInToolFeedback(t *testing.T) { tmpDir := t.TempDir() heartbeatFile := filepath.Join(tmpDir, "tool-feedback-reasoning.txt") diff --git a/pkg/agent/agent_utils.go b/pkg/agent/agent_utils.go index ff98dad68..f142a5531 100644 --- a/pkg/agent/agent_utils.go +++ b/pkg/agent/agent_utils.go @@ -5,6 +5,7 @@ package agent import ( "context" "fmt" + "maps" "path/filepath" "strings" "time" @@ -465,17 +466,28 @@ func sideQuestionResponseContent(response *providers.LLMResponse) string { if response == nil { return "" } - if response.Content != "" { + if strings.TrimSpace(response.Content) != "" { return response.Content } - return response.ReasoningContent + return responseReasoningContent(response) +} + +func responseReasoningContent(response *providers.LLMResponse) string { + if response == nil { + return "" + } + if strings.TrimSpace(response.Reasoning) != "" { + return response.Reasoning + } + if strings.TrimSpace(response.ReasoningContent) != "" { + return response.ReasoningContent + } + return "" } func shallowCloneLLMOptions(opts map[string]any) map[string]any { clone := make(map[string]any, len(opts)) - for k, v := range opts { - clone[k] = v - } + maps.Copy(clone, opts) return clone } diff --git a/pkg/agent/pipeline_finalize.go b/pkg/agent/pipeline_finalize.go index 43d44099a..a2be6f65b 100644 --- a/pkg/agent/pipeline_finalize.go +++ b/pkg/agent/pipeline_finalize.go @@ -40,8 +40,12 @@ func (p *Pipeline) Finalize( ts.setPhase(TurnPhaseFinalizing) ts.setFinalContent(finalContent) if !ts.opts.NoHistory { - finalMsg := providers.Message{Role: "assistant", Content: finalContent} - ts.agent.Sessions.AddMessage(ts.sessionKey, finalMsg.Role, finalMsg.Content) + finalMsg := providers.Message{ + Role: "assistant", + Content: finalContent, + ReasoningContent: responseReasoningContent(exec.response), + } + ts.agent.Sessions.AddFullMessage(ts.sessionKey, finalMsg) ts.recordPersistedMessage(finalMsg) ts.ingestMessage(turnCtx, al, finalMsg) if err := ts.agent.Sessions.Save(ts.sessionKey); err != nil { diff --git a/pkg/agent/pipeline_llm.go b/pkg/agent/pipeline_llm.go index 7b3fee208..ec268942c 100644 --- a/pkg/agent/pipeline_llm.go +++ b/pkg/agent/pipeline_llm.go @@ -384,10 +384,7 @@ func (p *Pipeline) CallLLM( } } - reasoningContent := exec.response.Reasoning - if reasoningContent == "" { - reasoningContent = exec.response.ReasoningContent - } + reasoningContent := responseReasoningContent(exec.response) if ts.channel == "pico" { go al.publishPicoReasoning(turnCtx, reasoningContent, ts.chatID) } else { @@ -496,7 +493,7 @@ func (p *Pipeline) CallLLM( assistantMsg := providers.Message{ Role: "assistant", Content: exec.response.Content, - ReasoningContent: exec.response.ReasoningContent, + ReasoningContent: reasoningContent, } for _, tc := range exec.normalizedToolCalls { argumentsJSON, _ := json.Marshal(tc.Arguments) diff --git a/pkg/agent/subturn.go b/pkg/agent/subturn.go index a65467dbb..4d824bd3a 100644 --- a/pkg/agent/subturn.go +++ b/pkg/agent/subturn.go @@ -10,6 +10,7 @@ import ( "github.com/sipeed/picoclaw/pkg/logger" "github.com/sipeed/picoclaw/pkg/providers" + "github.com/sipeed/picoclaw/pkg/providers/messageutil" "github.com/sipeed/picoclaw/pkg/tools" ) @@ -623,6 +624,10 @@ func (e *ephemeralSessionStore) AddMessage(_, role, content string) { } func (e *ephemeralSessionStore) AddFullMessage(_ string, msg providers.Message) { + if messageutil.IsTransientAssistantThoughtMessage(msg) { + return + } + e.mu.Lock() defer e.mu.Unlock() e.history = append(e.history, msg) @@ -652,6 +657,7 @@ func (e *ephemeralSessionStore) SetSummary(_, summary string) { func (e *ephemeralSessionStore) SetHistory(_ string, history []providers.Message) { e.mu.Lock() defer e.mu.Unlock() + history = messageutil.FilterInvalidHistoryMessages(history) e.history = make([]providers.Message, len(history)) copy(e.history, history) e.truncateLocked() diff --git a/pkg/memory/jsonl.go b/pkg/memory/jsonl.go index 8d3320f3f..072795bc9 100644 --- a/pkg/memory/jsonl.go +++ b/pkg/memory/jsonl.go @@ -16,6 +16,7 @@ import ( "github.com/sipeed/picoclaw/pkg/fileutil" "github.com/sipeed/picoclaw/pkg/providers" + "github.com/sipeed/picoclaw/pkg/providers/messageutil" ) const ( @@ -482,6 +483,9 @@ func readMessages(path string, skip int) ([]providers.Message, error) { lineNum, filepath.Base(path), err) continue } + if messageutil.IsTransientAssistantThoughtMessage(msg) { + continue + } msgs = append(msgs, msg) } if scanner.Err() != nil { @@ -535,6 +539,10 @@ func (s *JSONLStore) AddFullMessage( // addMsg is the shared implementation for AddMessage and AddFullMessage. func (s *JSONLStore) addMsg(sessionKey string, msg providers.Message) error { + if messageutil.IsTransientAssistantThoughtMessage(msg) { + return nil + } + l := s.sessionLock(sessionKey) l.Lock() defer l.Unlock() @@ -684,6 +692,8 @@ func (s *JSONLStore) SetHistory( sessionKey string, history []providers.Message, ) error { + history = messageutil.FilterInvalidHistoryMessages(history) + l := s.sessionLock(sessionKey) l.Lock() defer l.Unlock() @@ -762,6 +772,8 @@ func (s *JSONLStore) Compact( func (s *JSONLStore) rewriteJSONL( sessionKey string, msgs []providers.Message, ) error { + msgs = messageutil.FilterInvalidHistoryMessages(msgs) + var buf bytes.Buffer for i, msg := range msgs { line, err := json.Marshal(msg) diff --git a/pkg/memory/jsonl_test.go b/pkg/memory/jsonl_test.go index b64c1b25f..f72b84804 100644 --- a/pkg/memory/jsonl_test.go +++ b/pkg/memory/jsonl_test.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "reflect" + "strings" "sync" "testing" @@ -155,6 +156,27 @@ func TestAddFullMessage_ToolCallID(t *testing.T) { } } +func TestAddFullMessage_DropsTransientAssistantThought(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + err := store.AddFullMessage(ctx, "transient-thought", providers.Message{ + Role: "assistant", + ReasoningContent: "internal chain of thought", + }) + if err != nil { + t.Fatalf("AddFullMessage: %v", err) + } + + history, err := store.GetHistory(ctx, "transient-thought") + if err != nil { + t.Fatalf("GetHistory: %v", err) + } + if len(history) != 0 { + t.Fatalf("expected transient thought to be discarded, got %d messages", len(history)) + } +} + func TestGetHistory_EmptySession(t *testing.T) { store := newTestStore(t) ctx := context.Background() @@ -243,6 +265,46 @@ func TestSetSummary_GetSummary(t *testing.T) { } } +func TestSetHistory_DropsTransientAssistantThought(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + newHistory := []providers.Message{ + {Role: "user", Content: "hello"}, + {Role: "assistant", ReasoningContent: "internal chain of thought"}, + {Role: "assistant", Content: "visible answer", ReasoningContent: "visible thought"}, + } + + err := store.SetHistory(ctx, "replace", newHistory) + if err != nil { + t.Fatalf("SetHistory: %v", err) + } + + history, err := store.GetHistory(ctx, "replace") + if err != nil { + t.Fatalf("GetHistory: %v", err) + } + if len(history) != 2 { + t.Fatalf("expected transient thought to be removed, got %d messages", len(history)) + } + if history[0].Role != "user" || history[0].Content != "hello" { + t.Fatalf("history[0] = %+v, want user/hello", history[0]) + } + if history[1].Role != "assistant" || history[1].Content != "visible answer" || + history[1].ReasoningContent != "visible thought" { + t.Fatalf("history[1] = %+v, want assistant visible answer with reasoning", history[1]) + } + + data, err := os.ReadFile(store.jsonlPath("replace")) + if err != nil { + t.Fatalf("ReadFile(jsonl): %v", err) + } + lines := strings.Split(strings.TrimSpace(string(data)), "\n") + if len(lines) != 2 { + t.Fatalf("jsonl line count = %d, want 2", len(lines)) + } +} + func TestSessionMetaScopeAndAliasesPersist(t *testing.T) { store := newTestStore(t) ctx := context.Background() diff --git a/pkg/providers/factory_provider.go b/pkg/providers/factory_provider.go index 86d009811..ce83c6c54 100644 --- a/pkg/providers/factory_provider.go +++ b/pkg/providers/factory_provider.go @@ -178,7 +178,7 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err if apiBase == "" { apiBase = getDefaultAPIBase(protocol) } - return NewHTTPProviderWithMaxTokensFieldAndRequestTimeout( + provider := NewHTTPProviderWithMaxTokensFieldAndRequestTimeout( cfg.APIKey(), apiBase, cfg.Proxy, @@ -187,7 +187,9 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err cfg.RequestTimeout, cfg.ExtraBody, cfg.CustomHeaders, - ), modelID, nil + ) + provider.SetProviderName(protocol) + return provider, modelID, nil case "azure", "azure-openai": // Azure OpenAI uses deployment-based URLs, api-key header auth, @@ -257,7 +259,7 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err if apiBase == "" { apiBase = getDefaultAPIBase(protocol) } - return NewHTTPProviderWithMaxTokensFieldAndRequestTimeout( + provider := NewHTTPProviderWithMaxTokensFieldAndRequestTimeout( cfg.APIKey(), apiBase, cfg.Proxy, @@ -266,7 +268,9 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err cfg.RequestTimeout, cfg.ExtraBody, cfg.CustomHeaders, - ), modelID, nil + ) + provider.SetProviderName(protocol) + return provider, modelID, nil case "gemini": if cfg.APIKey() == "" && cfg.APIBase == "" { @@ -302,7 +306,7 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err if _, ok := extraBody["reasoning_split"]; !ok { extraBody["reasoning_split"] = true } - return NewHTTPProviderWithMaxTokensFieldAndRequestTimeout( + provider := NewHTTPProviderWithMaxTokensFieldAndRequestTimeout( cfg.APIKey(), apiBase, cfg.Proxy, @@ -311,7 +315,9 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err cfg.RequestTimeout, extraBody, cfg.CustomHeaders, - ), modelID, nil + ) + provider.SetProviderName(protocol) + return provider, modelID, nil case "anthropic": if cfg.AuthMethod == "oauth" || cfg.AuthMethod == "token" { @@ -330,7 +336,7 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err if cfg.APIKey() == "" { return nil, "", fmt.Errorf("api_key is required for anthropic protocol (model: %s)", cfg.Model) } - return NewHTTPProviderWithMaxTokensFieldAndRequestTimeout( + provider := NewHTTPProviderWithMaxTokensFieldAndRequestTimeout( cfg.APIKey(), apiBase, cfg.Proxy, @@ -339,7 +345,9 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err cfg.RequestTimeout, cfg.ExtraBody, cfg.CustomHeaders, - ), modelID, nil + ) + provider.SetProviderName(protocol) + return provider, modelID, nil case "anthropic-messages": // Anthropic Messages API with native format (HTTP-based, no SDK) diff --git a/pkg/providers/httpapi/http_provider.go b/pkg/providers/httpapi/http_provider.go index a84962622..90f389cc8 100644 --- a/pkg/providers/httpapi/http_provider.go +++ b/pkg/providers/httpapi/http_provider.go @@ -77,3 +77,10 @@ func (p *HTTPProvider) GetDefaultModel() string { func (p *HTTPProvider) SupportsNativeSearch() bool { return p.delegate.SupportsNativeSearch() } + +func (p *HTTPProvider) SetProviderName(providerName string) { + if p == nil || p.delegate == nil { + return + } + p.delegate.SetProviderName(providerName) +} diff --git a/pkg/providers/messageutil/messageutil.go b/pkg/providers/messageutil/messageutil.go new file mode 100644 index 000000000..6f6d08f46 --- /dev/null +++ b/pkg/providers/messageutil/messageutil.go @@ -0,0 +1,41 @@ +package messageutil + +import ( + "strings" + + "github.com/sipeed/picoclaw/pkg/providers/protocoltypes" +) + +// IsTransientAssistantThoughtMessage reports whether msg is an invalid +// reasoning-only assistant history record. These "hanging" thought messages +// are not a canonical persisted format and should be discarded instead of +// replayed or reconstructed. +func IsTransientAssistantThoughtMessage(msg protocoltypes.Message) bool { + return msg.Role == "assistant" && + strings.TrimSpace(msg.Content) == "" && + strings.TrimSpace(msg.ReasoningContent) != "" && + len(msg.ToolCalls) == 0 && + len(msg.Media) == 0 && + len(msg.Attachments) == 0 && + strings.TrimSpace(msg.ToolCallID) == "" +} + +// FilterInvalidHistoryMessages removes invalid persisted history records such +// as transient assistant thought-only messages. +func FilterInvalidHistoryMessages(history []protocoltypes.Message) []protocoltypes.Message { + if len(history) == 0 { + return []protocoltypes.Message{} + } + + filtered := make([]protocoltypes.Message, 0, len(history)) + for _, msg := range history { + if IsTransientAssistantThoughtMessage(msg) { + continue + } + filtered = append(filtered, msg) + } + if filtered == nil { + return []protocoltypes.Message{} + } + return filtered +} diff --git a/pkg/providers/openai_compat/provider.go b/pkg/providers/openai_compat/provider.go index 29667cd31..c3733ce3a 100644 --- a/pkg/providers/openai_compat/provider.go +++ b/pkg/providers/openai_compat/provider.go @@ -15,6 +15,7 @@ import ( "time" "github.com/sipeed/picoclaw/pkg/providers/common" + "github.com/sipeed/picoclaw/pkg/providers/messageutil" "github.com/sipeed/picoclaw/pkg/providers/protocoltypes" ) @@ -34,6 +35,7 @@ type ( type Provider struct { apiKey string apiBase string + providerName string maxTokensField string // Field name for max tokens (e.g., "max_completion_tokens" for o1/glm models) httpClient *http.Client extraBody map[string]any // Additional fields to inject into request body @@ -95,6 +97,12 @@ func WithCustomHeaders(customHeaders map[string]string) Option { } } +func WithProviderName(providerName string) Option { + return func(p *Provider) { + p.providerName = strings.ToLower(strings.TrimSpace(providerName)) + } +} + func NewProvider(apiKey, apiBase, proxy string, opts ...Option) *Provider { p := &Provider{ apiKey: apiKey, @@ -136,7 +144,7 @@ func (p *Provider) buildRequestBody( requestBody := map[string]any{ "model": model, - "messages": common.SerializeMessages(messages), + "messages": common.SerializeMessages(p.prepareMessagesForRequest(messages)), } // When fallback uses a different provider (e.g. DeepSeek), that provider must not inject web_search_preview. @@ -196,6 +204,111 @@ func (p *Provider) applyCustomHeaders(req *http.Request) { } } +func (p *Provider) SetProviderName(providerName string) { + p.providerName = strings.ToLower(strings.TrimSpace(providerName)) +} + +func (p *Provider) prepareMessagesForRequest(messages []Message) []Message { + if len(messages) == 0 { + return nil + } + + if p.isDeepSeekReasoningProvider() { + return filterDeepSeekReasoningMessages(messages) + } + return stripReasoningMessages(messages) +} + +func (p *Provider) isDeepSeekReasoningProvider() bool { + return p.providerName == "deepseek" || isDeepSeekHost(p.apiBase) +} + +func isDeepSeekHost(apiBase string) bool { + parsed, err := url.Parse(strings.TrimSpace(apiBase)) + if err != nil { + return false + } + host := strings.ToLower(strings.TrimSpace(parsed.Hostname())) + return host == "deepseek.com" || strings.HasSuffix(host, ".deepseek.com") +} + +func filterDeepSeekReasoningMessages(messages []Message) []Message { + out := make([]Message, 0, len(messages)) + start := 0 + + flush := func(end int) { + if end <= start { + return + } + out = append(out, filterDeepSeekReasoningTurn(messages[start:end])...) + start = end + } + + for i := 1; i < len(messages); i++ { + if messages[i].Role == "user" { + flush(i) + } + } + flush(len(messages)) + + return out +} + +func filterDeepSeekReasoningTurn(messages []Message) []Message { + hasToolInteraction := false + for _, msg := range messages { + if msg.Role == "tool" || (msg.Role == "assistant" && len(msg.ToolCalls) > 0) { + hasToolInteraction = true + break + } + } + + out := make([]Message, 0, len(messages)) + for _, msg := range messages { + if messageutil.IsTransientAssistantThoughtMessage(msg) { + continue + } + + cloned := msg + if cloned.Role == "assistant" && strings.TrimSpace(cloned.ReasoningContent) != "" && !hasToolInteraction { + cloned.ReasoningContent = "" + } + if assistantMessageEmpty(cloned) { + continue + } + out = append(out, cloned) + } + + return out +} + +func stripReasoningMessages(messages []Message) []Message { + out := make([]Message, 0, len(messages)) + for _, msg := range messages { + if messageutil.IsTransientAssistantThoughtMessage(msg) { + continue + } + + cloned := msg + cloned.ReasoningContent = "" + if assistantMessageEmpty(cloned) { + continue + } + out = append(out, cloned) + } + return out +} + +func assistantMessageEmpty(msg Message) bool { + return msg.Role == "assistant" && + strings.TrimSpace(msg.Content) == "" && + strings.TrimSpace(msg.ReasoningContent) == "" && + len(msg.ToolCalls) == 0 && + len(msg.Media) == 0 && + len(msg.Attachments) == 0 && + strings.TrimSpace(msg.ToolCallID) == "" +} + func (p *Provider) Chat( ctx context.Context, messages []Message, diff --git a/pkg/providers/openai_compat/provider_test.go b/pkg/providers/openai_compat/provider_test.go index d140d63d6..594048ea5 100644 --- a/pkg/providers/openai_compat/provider_test.go +++ b/pkg/providers/openai_compat/provider_test.go @@ -202,7 +202,7 @@ func TestProviderChat_ParsesReasoningContent(t *testing.T) { } } -func TestProviderChat_PreservesReasoningContentInHistory(t *testing.T) { +func TestProviderChat_StripsReasoningContentForNonDeepSeekHistory(t *testing.T) { var requestBody map[string]any server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -225,8 +225,6 @@ func TestProviderChat_PreservesReasoningContentInHistory(t *testing.T) { p := NewProvider("key", server.URL, "") - // Simulate a multi-turn conversation where the assistant's previous - // reply included reasoning_content (e.g. from kimi-k2.5). messages := []Message{ {Role: "user", Content: "What is 1+1?"}, {Role: "assistant", Content: "2", ReasoningContent: "Let me think... 1+1=2"}, @@ -238,7 +236,6 @@ func TestProviderChat_PreservesReasoningContentInHistory(t *testing.T) { t.Fatalf("Chat() error = %v", err) } - // Verify reasoning_content is preserved in the serialized request. reqMessages, ok := requestBody["messages"].([]any) if !ok { t.Fatalf("messages is not []any: %T", requestBody["messages"]) @@ -247,11 +244,288 @@ func TestProviderChat_PreservesReasoningContentInHistory(t *testing.T) { if !ok { t.Fatalf("assistant message is not map[string]any: %T", reqMessages[1]) } - if assistantMsg["reasoning_content"] != "Let me think... 1+1=2" { - t.Errorf("reasoning_content not preserved in request, got %v", assistantMsg["reasoning_content"]) + if _, exists := assistantMsg["reasoning_content"]; exists { + t.Fatalf( + "reasoning_content should be stripped for non-DeepSeek providers, got %v", + assistantMsg["reasoning_content"], + ) } } +func TestProviderChat_DeepSeekOmitsReasoningContentForNonToolTurnHistory(t *testing.T) { + var requestBody map[string]any + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + resp := map[string]any{ + "choices": []map[string]any{ + { + "message": map[string]any{"content": "ok"}, + "finish_reason": "stop", + }, + }, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) + })) + defer server.Close() + + p := NewProvider("key", server.URL, "") + p.apiBase = "https://api.deepseek.com/v1" + p.httpClient = &http.Client{ + Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) { + r.URL, _ = url.Parse(server.URL + r.URL.Path) + return http.DefaultTransport.RoundTrip(r) + }), + } + + messages := []Message{ + {Role: "user", Content: "What is 1+1?"}, + {Role: "assistant", Content: "2", ReasoningContent: "Let me think... 1+1=2"}, + {Role: "user", Content: "What about 2+2?"}, + } + + _, err := p.Chat(t.Context(), messages, nil, "deepseek-v4-flash", nil) + if err != nil { + t.Fatalf("Chat() error = %v", err) + } + + reqMessages, ok := requestBody["messages"].([]any) + if !ok { + t.Fatalf("messages is not []any: %T", requestBody["messages"]) + } + assistantMsg, ok := reqMessages[1].(map[string]any) + if !ok { + t.Fatalf("assistant message is not map[string]any: %T", reqMessages[1]) + } + if _, exists := assistantMsg["reasoning_content"]; exists { + t.Fatalf( + "reasoning_content should be omitted for DeepSeek non-tool turns, got %v", + assistantMsg["reasoning_content"], + ) + } +} + +func TestProviderChat_DeepSeekPreservesReasoningContentForToolTurnHistory(t *testing.T) { + var requestBody map[string]any + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + resp := map[string]any{ + "choices": []map[string]any{ + { + "message": map[string]any{"content": "ok"}, + "finish_reason": "stop", + }, + }, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) + })) + defer server.Close() + + p := NewProvider("key", server.URL, "") + p.SetProviderName("deepseek") + + messages := []Message{ + {Role: "user", Content: "How's the weather tomorrow?"}, + { + Role: "assistant", + Content: "Let me check the date first.", + ReasoningContent: "I need tomorrow's date before checking the weather.", + ToolCalls: []ToolCall{{ + ID: "call_1", + Type: "function", + Function: &FunctionCall{ + Name: "get_date", + Arguments: "{}", + }, + }}, + }, + {Role: "tool", ToolCallID: "call_1", Content: "2026-04-24"}, + { + Role: "assistant", + Content: "Tomorrow is 2026-04-25.", + ReasoningContent: "Now I can share the final answer.", + }, + {Role: "user", Content: "What about Guangzhou?"}, + } + + _, err := p.Chat(t.Context(), messages, nil, "deepseek-v4-flash", nil) + if err != nil { + t.Fatalf("Chat() error = %v", err) + } + + reqMessages, ok := requestBody["messages"].([]any) + if !ok { + t.Fatalf("messages is not []any: %T", requestBody["messages"]) + } + if len(reqMessages) != len(messages) { + t.Fatalf("len(messages) = %d, want %d", len(reqMessages), len(messages)) + } + + firstAssistant, ok := reqMessages[1].(map[string]any) + if !ok { + t.Fatalf("first assistant message is not map[string]any: %T", reqMessages[1]) + } + if firstAssistant["reasoning_content"] != "I need tomorrow's date before checking the weather." { + t.Fatalf("first assistant reasoning_content = %v, want preserved", firstAssistant["reasoning_content"]) + } + + finalAssistant, ok := reqMessages[3].(map[string]any) + if !ok { + t.Fatalf("final assistant message is not map[string]any: %T", reqMessages[3]) + } + if finalAssistant["reasoning_content"] != "Now I can share the final answer." { + t.Fatalf("final assistant reasoning_content = %v, want preserved", finalAssistant["reasoning_content"]) + } +} + +func TestProviderChat_HistoryCanonicalizationMatrix(t *testing.T) { + baseMessages := []Message{ + {Role: "user", Content: "turn1"}, + {Role: "assistant", Content: "plain visible", ReasoningContent: "plain thought"}, + {Role: "user", Content: "turn2"}, + { + Role: "assistant", + Content: "", + ReasoningContent: "tool thought", + ToolCalls: []ToolCall{{ + ID: "call_read_file", + Type: "function", + Function: &FunctionCall{ + Name: "read_file", + Arguments: `{"path":"README.md"}`, + }, + }}, + }, + {Role: "tool", ToolCallID: "call_read_file", Content: "file content"}, + {Role: "user", Content: "turn3"}, + { + Role: "assistant", + Content: "tool visible only", + ToolCalls: []ToolCall{{ + ID: "call_list_dir", + Type: "function", + Function: &FunctionCall{ + Name: "list_dir", + Arguments: `{"path":"."}`, + }, + }}, + }, + {Role: "tool", ToolCallID: "call_list_dir", Content: "dir listing"}, + {Role: "user", Content: "turn4"}, + { + Role: "assistant", + Content: "tool visible and thought", + ReasoningContent: "tool mixed thought", + ToolCalls: []ToolCall{{ + ID: "call_exec", + Type: "function", + Function: &FunctionCall{ + Name: "exec", + Arguments: `{"command":"pwd"}`, + }, + }}, + }, + {Role: "tool", ToolCallID: "call_exec", Content: "pwd output"}, + {Role: "user", Content: "current turn"}, + } + + captureRequestMessages := func(t *testing.T, providerName string) []map[string]any { + t.Helper() + + var requestBody map[string]any + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + resp := map[string]any{ + "choices": []map[string]any{ + { + "message": map[string]any{"content": "ok"}, + "finish_reason": "stop", + }, + }, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) + })) + defer server.Close() + + p := NewProvider("key", server.URL, "") + if providerName != "" { + p.SetProviderName(providerName) + } + + _, err := p.Chat(t.Context(), baseMessages, nil, "test-model", nil) + if err != nil { + t.Fatalf("Chat() error = %v", err) + } + + rawMessages, ok := requestBody["messages"].([]any) + if !ok { + t.Fatalf("messages is not []any: %T", requestBody["messages"]) + } + + out := make([]map[string]any, 0, len(rawMessages)) + for i, raw := range rawMessages { + msg, ok := raw.(map[string]any) + if !ok { + t.Fatalf("messages[%d] is %T, want map[string]any", i, raw) + } + out = append(out, msg) + } + return out + } + + t.Run("deepseek", func(t *testing.T) { + msgs := captureRequestMessages(t, "deepseek") + if len(msgs) != len(baseMessages) { + t.Fatalf("len(messages) = %d, want %d", len(msgs), len(baseMessages)) + } + + if _, ok := msgs[1]["reasoning_content"]; ok { + t.Fatalf( + "turn1 reasoning_content should be stripped for DeepSeek non-tool turn, got %v", + msgs[1]["reasoning_content"], + ) + } + if msgs[3]["reasoning_content"] != "tool thought" { + t.Fatalf("turn2 reasoning_content = %v, want preserved", msgs[3]["reasoning_content"]) + } + if _, ok := msgs[6]["reasoning_content"]; ok { + t.Fatalf("turn3 reasoning_content should be absent, got %v", msgs[6]["reasoning_content"]) + } + if msgs[9]["reasoning_content"] != "tool mixed thought" { + t.Fatalf("turn4 reasoning_content = %v, want preserved", msgs[9]["reasoning_content"]) + } + if msgs[9]["content"] != "tool visible and thought" { + t.Fatalf("turn4 content = %v, want preserved", msgs[9]["content"]) + } + }) + + t.Run("non-deepseek", func(t *testing.T) { + msgs := captureRequestMessages(t, "") + for i, msg := range msgs { + if _, ok := msg["reasoning_content"]; ok { + t.Fatalf( + "messages[%d] reasoning_content should be stripped for non-DeepSeek providers, got %v", + i, + msg["reasoning_content"], + ) + } + } + }) +} + func TestProviderChat_HTTPError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "bad request", http.StatusBadRequest) diff --git a/pkg/session/manager.go b/pkg/session/manager.go index 7f87d460a..1d6fa3106 100644 --- a/pkg/session/manager.go +++ b/pkg/session/manager.go @@ -9,6 +9,7 @@ import ( "time" "github.com/sipeed/picoclaw/pkg/providers" + "github.com/sipeed/picoclaw/pkg/providers/messageutil" ) type Session struct { @@ -69,6 +70,10 @@ func (sm *SessionManager) AddMessage(sessionKey, role, content string) { // AddFullMessage adds a complete message with tool calls and tool call ID to the session. // This is used to save the full conversation flow including tool calls and tool results. func (sm *SessionManager) AddFullMessage(sessionKey string, msg providers.Message) { + if messageutil.IsTransientAssistantThoughtMessage(msg) { + return + } + sm.mu.Lock() defer sm.mu.Unlock() @@ -196,8 +201,7 @@ func (sm *SessionManager) Save(key string) error { Updated: stored.Updated, } if len(stored.Messages) > 0 { - snapshot.Messages = make([]providers.Message, len(stored.Messages)) - copy(snapshot.Messages, stored.Messages) + snapshot.Messages = messageutil.FilterInvalidHistoryMessages(stored.Messages) } else { snapshot.Messages = []providers.Message{} } @@ -270,6 +274,7 @@ func (sm *SessionManager) loadSessions() error { if err := json.Unmarshal(data, &session); err != nil { continue } + session.Messages = messageutil.FilterInvalidHistoryMessages(session.Messages) sm.sessions[session.Key] = &session } @@ -290,6 +295,7 @@ func (sm *SessionManager) SetHistory(key string, history []providers.Message) { session, ok := sm.sessions[key] if ok { + history = messageutil.FilterInvalidHistoryMessages(history) // Create a deep copy to strictly isolate internal state // from the caller's slice. msgs := make([]providers.Message, len(history)) diff --git a/web/backend/api/session.go b/web/backend/api/session.go index 6ac1eb988..7bfe43026 100644 --- a/web/backend/api/session.go +++ b/web/backend/api/session.go @@ -15,6 +15,7 @@ import ( "github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/memory" "github.com/sipeed/picoclaw/pkg/providers" + "github.com/sipeed/picoclaw/pkg/providers/messageutil" "github.com/sipeed/picoclaw/pkg/session" "github.com/sipeed/picoclaw/pkg/utils" ) @@ -48,6 +49,7 @@ type sessionListItem struct { type sessionChatMessage struct { Role string `json:"role"` Content string `json:"content"` + Kind string `json:"kind,omitempty"` Media []string `json:"media,omitempty"` Attachments []sessionChatAttachment `json:"attachments,omitempty"` } @@ -473,6 +475,18 @@ func sessionChatMessagePreview(msg sessionChatMessage) string { } func visibleSessionMessages(messages []providers.Message, toolFeedbackMaxArgsLength int) []sessionChatMessage { + return sessionTranscriptMessages(messages, toolFeedbackMaxArgsLength, false) +} + +func detailSessionMessages(messages []providers.Message, toolFeedbackMaxArgsLength int) []sessionChatMessage { + return sessionTranscriptMessages(messages, toolFeedbackMaxArgsLength, true) +} + +func sessionTranscriptMessages( + messages []providers.Message, + toolFeedbackMaxArgsLength int, + includeThoughts bool, +) []sessionChatMessage { transcript := make([]sessionChatMessage, 0, len(messages)) for _, msg := range messages { @@ -494,11 +508,14 @@ func visibleSessionMessages(messages []providers.Message, toolFeedbackMaxArgsLen } case "assistant": - // Reasoning-only assistant messages are transient display artifacts and - // should not be restored from session history. - if assistantMessageTransientThought(msg) { + if messageutil.IsTransientAssistantThoughtMessage(msg) { continue } + if includeThoughts { + if thoughtMsg, ok := assistantThoughtMessage(msg); ok { + transcript = append(transcript, thoughtMsg) + } + } toolSummaryMessages := visibleAssistantToolSummaryMessages(msg.ToolCalls, toolFeedbackMaxArgsLength) if len(toolSummaryMessages) > 0 { @@ -672,18 +689,25 @@ func sessionAttachmentType(attachment providers.Attachment) string { } } -func assistantMessageTransientThought(msg providers.Message) bool { - return strings.TrimSpace(msg.Content) == "" && - strings.TrimSpace(msg.ReasoningContent) != "" && - len(msg.ToolCalls) == 0 && - len(msg.Media) == 0 && - len(msg.Attachments) == 0 -} - func assistantMessageInternalOnly(msg providers.Message) bool { return strings.TrimSpace(msg.Content) == handledToolResponseSummaryText } +func assistantThoughtMessage(msg providers.Message) (sessionChatMessage, bool) { + reasoning := strings.TrimSpace(msg.ReasoningContent) + if reasoning == "" { + return sessionChatMessage{}, false + } + if reasoning == strings.TrimSpace(msg.Content) { + return sessionChatMessage{}, false + } + return sessionChatMessage{ + Role: "assistant", + Content: reasoning, + Kind: "thought", + }, true +} + func visibleAssistantToolSummaryMessages( toolCalls []providers.ToolCall, toolFeedbackMaxArgsLength int, @@ -962,7 +986,7 @@ func (h *Handler) handleGetSession(w http.ResponseWriter, r *http.Request) { } } - messages := visibleSessionMessages(sess.Messages, toolFeedbackMaxArgsLength) + messages := detailSessionMessages(sess.Messages, toolFeedbackMaxArgsLength) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{ diff --git a/web/backend/api/session_test.go b/web/backend/api/session_test.go index 6afb8a94f..f8fb2483a 100644 --- a/web/backend/api/session_test.go +++ b/web/backend/api/session_test.go @@ -423,7 +423,7 @@ func TestHandleSessions_JSONLScopeDiscovery(t *testing.T) { } } -func TestHandleGetSession_OmitsTransientThoughtMessages(t *testing.T) { +func TestHandleGetSession_SkipsTransientThoughtMessages(t *testing.T) { configPath, cleanup := setupOAuthTestEnv(t) defer cleanup() @@ -460,6 +460,7 @@ func TestHandleGetSession_OmitsTransientThoughtMessages(t *testing.T) { Messages []struct { Role string `json:"role"` Content string `json:"content"` + Kind string `json:"kind"` } `json:"messages"` } if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { @@ -476,6 +477,180 @@ func TestHandleGetSession_OmitsTransientThoughtMessages(t *testing.T) { } } +func TestHandleGetSession_ReconstructsThoughtFromAssistantReasoningContent(t *testing.T) { + configPath, cleanup := setupOAuthTestEnv(t) + defer cleanup() + + dir := sessionsTestDir(t, configPath) + store, err := memory.NewJSONLStore(dir) + if err != nil { + t.Fatalf("NewJSONLStore() error = %v", err) + } + + sessionKey := picoSessionPrefix + "detail-reasoning-content" + for _, msg := range []providers.Message{ + {Role: "user", Content: "hello"}, + {Role: "assistant", Content: "final visible answer", ReasoningContent: "internal chain of thought"}, + } { + if err := store.AddFullMessage(nil, sessionKey, msg); err != nil { + t.Fatalf("AddFullMessage() error = %v", err) + } + } + + h := NewHandler(configPath) + mux := http.NewServeMux() + h.RegisterRoutes(mux) + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/sessions/detail-reasoning-content", nil) + mux.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String()) + } + + var resp struct { + Messages []struct { + Role string `json:"role"` + Content string `json:"content"` + Kind string `json:"kind"` + } `json:"messages"` + } + if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { + t.Fatalf("Unmarshal() error = %v", err) + } + if len(resp.Messages) != 3 { + t.Fatalf("len(resp.Messages) = %d, want 3", len(resp.Messages)) + } + if resp.Messages[1].Role != "assistant" || + resp.Messages[1].Content != "internal chain of thought" || + resp.Messages[1].Kind != "thought" { + t.Fatalf("thought message = %#v, want assistant thought/internal chain of thought", resp.Messages[1]) + } + if resp.Messages[2].Role != "assistant" || resp.Messages[2].Content != "final visible answer" { + t.Fatalf("final message = %#v, want assistant/final visible answer", resp.Messages[2]) + } +} + +func TestHandleGetSession_ReconstructsRefreshMatrixForThoughtAndToolSummary(t *testing.T) { + configPath, cleanup := setupOAuthTestEnv(t) + defer cleanup() + + dir := sessionsTestDir(t, configPath) + store, err := memory.NewJSONLStore(dir) + if err != nil { + t.Fatalf("NewJSONLStore() error = %v", err) + } + + sessionKey := picoSessionPrefix + "detail-refresh-matrix" + for _, msg := range []providers.Message{ + {Role: "user", Content: "turn1"}, + {Role: "assistant", Content: "plain visible", ReasoningContent: "plain thought"}, + {Role: "user", Content: "turn2"}, + { + Role: "assistant", + ReasoningContent: "tool thought", + ToolCalls: []providers.ToolCall{{ + ID: "call_read_file", + Type: "function", + Function: &providers.FunctionCall{ + Name: "read_file", + Arguments: `{"path":"README.md"}`, + }, + }}, + }, + {Role: "tool", ToolCallID: "call_read_file", Content: "file result"}, + {Role: "user", Content: "turn3"}, + { + Role: "assistant", + Content: "tool visible only", + ToolCalls: []providers.ToolCall{{ + ID: "call_list_dir", + Type: "function", + Function: &providers.FunctionCall{ + Name: "list_dir", + Arguments: `{"path":"."}`, + }, + }}, + }, + {Role: "tool", ToolCallID: "call_list_dir", Content: "dir result"}, + {Role: "user", Content: "turn4"}, + { + Role: "assistant", + Content: "tool visible and thought", + ReasoningContent: "tool mixed thought", + ToolCalls: []providers.ToolCall{{ + ID: "call_exec", + Type: "function", + Function: &providers.FunctionCall{ + Name: "exec", + Arguments: `{"command":"pwd"}`, + }, + }}, + }, + {Role: "tool", ToolCallID: "call_exec", Content: "pwd result"}, + } { + if err := store.AddFullMessage(nil, sessionKey, msg); err != nil { + t.Fatalf("AddFullMessage() error = %v", err) + } + } + + h := NewHandler(configPath) + mux := http.NewServeMux() + h.RegisterRoutes(mux) + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/sessions/detail-refresh-matrix", nil) + mux.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String()) + } + + var resp struct { + Messages []struct { + Role string `json:"role"` + Content string `json:"content"` + Kind string `json:"kind"` + } `json:"messages"` + } + if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { + t.Fatalf("Unmarshal() error = %v", err) + } + + if len(resp.Messages) != 13 { + t.Fatalf("len(resp.Messages) = %d, want 13", len(resp.Messages)) + } + + assertMessage := func(index int, role, kind, content string) { + t.Helper() + msg := resp.Messages[index] + if msg.Role != role || msg.Kind != kind || msg.Content != content { + t.Fatalf("messages[%d] = %#v, want role=%q kind=%q content=%q", index, msg, role, kind, content) + } + } + + assertMessage(0, "user", "", "turn1") + assertMessage(1, "assistant", "thought", "plain thought") + assertMessage(2, "assistant", "", "plain visible") + assertMessage(3, "user", "", "turn2") + assertMessage(4, "assistant", "thought", "tool thought") + if !strings.Contains(resp.Messages[5].Content, "`read_file`") { + t.Fatalf("messages[5] = %#v, want read_file tool summary", resp.Messages[5]) + } + assertMessage(6, "user", "", "turn3") + if !strings.Contains(resp.Messages[7].Content, "`list_dir`") { + t.Fatalf("messages[7] = %#v, want list_dir tool summary", resp.Messages[7]) + } + assertMessage(8, "assistant", "", "tool visible only") + assertMessage(9, "user", "", "turn4") + assertMessage(10, "assistant", "thought", "tool mixed thought") + if !strings.Contains(resp.Messages[11].Content, "`exec`") { + t.Fatalf("messages[11] = %#v, want exec tool summary", resp.Messages[11]) + } + assertMessage(12, "assistant", "", "tool visible and thought") +} + func TestHandleGetSession_ReconstructsVisibleMessageToolOutputWithoutDuplicateSummary(t *testing.T) { configPath, cleanup := setupOAuthTestEnv(t) defer cleanup() diff --git a/web/frontend/src/api/sessions.ts b/web/frontend/src/api/sessions.ts index 912fbecd8..d98914a59 100644 --- a/web/frontend/src/api/sessions.ts +++ b/web/frontend/src/api/sessions.ts @@ -14,6 +14,7 @@ export interface SessionDetail { messages: { role: "user" | "assistant" content: string + kind?: "normal" | "thought" media?: string[] attachments?: { type?: "image" | "audio" | "video" | "file" diff --git a/web/frontend/src/features/chat/history.ts b/web/frontend/src/features/chat/history.ts index a3e6ce14d..6c8691fba 100644 --- a/web/frontend/src/features/chat/history.ts +++ b/web/frontend/src/features/chat/history.ts @@ -45,7 +45,8 @@ export async function loadSessionMessages( id: `hist-${index}-${Date.now()}`, role: message.role, content: message.content, - kind: message.role === "assistant" ? "normal" : undefined, + kind: + message.role === "assistant" ? (message.kind ?? "normal") : undefined, attachments: toChatAttachments({ media: message.media, attachments: message.attachments, From 979ff00cc398bb1455696fe039b15c8db7d547d0 Mon Sep 17 00:00:00 2001 From: LC <64722907+lc6464@users.noreply.github.com> Date: Fri, 24 Apr 2026 22:16:18 +0800 Subject: [PATCH 21/36] fix(messageutil): remove dead code Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- pkg/providers/messageutil/messageutil.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/pkg/providers/messageutil/messageutil.go b/pkg/providers/messageutil/messageutil.go index 6f6d08f46..c4382d894 100644 --- a/pkg/providers/messageutil/messageutil.go +++ b/pkg/providers/messageutil/messageutil.go @@ -34,8 +34,5 @@ func FilterInvalidHistoryMessages(history []protocoltypes.Message) []protocoltyp } filtered = append(filtered, msg) } - if filtered == nil { - return []protocoltypes.Message{} - } return filtered } From f6bceb29a324461074136d25d40f2bf10e1c17c3 Mon Sep 17 00:00:00 2001 From: SiYue <2835601846@qq.com> Date: Sun, 12 Apr 2026 21:27:40 +0800 Subject: [PATCH 22/36] Fix Windows build flow --- web/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/Makefile b/web/Makefile index 254c439e9..0c9344a89 100644 --- a/web/Makefile +++ b/web/Makefile @@ -164,7 +164,7 @@ endif build-frontend: ifeq ($(OS),Windows_NT) - @$(POWERSHELL) "if ((-not (Test-Path -LiteralPath '$(FRONTEND_DIR)/node_modules')) -or (-not (Test-Path -LiteralPath '$(FRONTEND_DIR)/node_modules/.bin/tsc')) -or (-not (Test-Path -LiteralPath '$(FRONTEND_INSTALL_STAMP)')) -or ((Get-Content -LiteralPath '$(FRONTEND_INSTALL_STAMP)' -Raw).Trim() -ne (((Get-FileHash -LiteralPath '$(FRONTEND_DIR)/package.json' -Algorithm SHA256).Hash + ':' + (Get-FileHash -LiteralPath '$(FRONTEND_DIR)/pnpm-lock.yaml' -Algorithm SHA256).Hash)))) { Write-Host 'Installing frontend dependencies...'; Push-Location '$(FRONTEND_DIR)'; try { pnpm install --frozen-lockfile } finally { Pop-Location }; Set-Content -LiteralPath '$(FRONTEND_INSTALL_STAMP)' -Value (((Get-FileHash -LiteralPath '$(FRONTEND_DIR)/package.json' -Algorithm SHA256).Hash + ':' + (Get-FileHash -LiteralPath '$(FRONTEND_DIR)/pnpm-lock.yaml' -Algorithm SHA256).Hash)) -NoNewline }" + @$(POWERSHELL) "$$frontend='$(FRONTEND_DIR)'; $$stamp='$(FRONTEND_INSTALL_STAMP)'; $$pkg='$(FRONTEND_DIR)/package.json'; $$lock='$(FRONTEND_DIR)/pnpm-lock.yaml'; $$expected=((Get-FileHash -LiteralPath $$pkg -Algorithm SHA256).Hash + ':' + (Get-FileHash -LiteralPath $$lock -Algorithm SHA256).Hash); $$nodeModules=$$frontend + '/node_modules'; $$tsc=$$nodeModules + '/.bin/tsc'; $$current=''; if (Test-Path -LiteralPath $$stamp) { $$current=(Get-Content -LiteralPath $$stamp -Raw).Trim() }; if ((-not (Test-Path -LiteralPath $$nodeModules)) -or (-not (Test-Path -LiteralPath $$tsc)) -or ($$current -ne $$expected)) { Write-Host 'Installing frontend dependencies...'; Push-Location $$frontend; try { $$env:CI='true'; pnpm install --frozen-lockfile } finally { Pop-Location; Remove-Item Env:CI -ErrorAction SilentlyContinue }; Set-Content -LiteralPath $$stamp -Value $$expected -NoNewline }" else @expected_stamp="$$(cat $(FRONTEND_DIR)/package.json $(FRONTEND_DIR)/pnpm-lock.yaml | cksum | awk '{print $$1 ":" $$2}')"; \ if [ ! -d $(FRONTEND_DIR)/node_modules ] || \ From 193e1a3cd0444a3da1fa9ee222e6c0e113dc5f5d Mon Sep 17 00:00:00 2001 From: SiYue <2835601846@qq.com> Date: Sun, 12 Apr 2026 21:27:40 +0800 Subject: [PATCH 23/36] Fix Windows build flow --- Makefile | 17 +++++++++++++++++ web/Makefile | 16 ++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/Makefile b/Makefile index acb258370..11adcef06 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,11 @@ EXT= ifeq ($(OS),Windows_NT) POWERSHELL=powershell -NoProfile -Command +<<<<<<< HEAD +======= + GO=go + WEB_GO=go +>>>>>>> 83c79087 (Fix Windows build flow) WINDOWS_GOARCH_RAW:=$(strip $(shell go env GOARCH 2>NUL)) endif @@ -40,7 +45,10 @@ GOFLAGS?=-v -tags $(GO_BUILD_TAGS) GOCACHE?=$(CURDIR)/.cache/go-build GOMODCACHE?=$(CURDIR)/.cache/go-mod GOTOOLCHAIN?=local +<<<<<<< HEAD export CGO_ENABLED +======= +>>>>>>> 83c79087 (Fix Windows build flow) export GOCACHE export GOMODCACHE export GOTOOLCHAIN @@ -207,7 +215,16 @@ build-launcher: @echo "Building picoclaw-launcher for $(PLATFORM)/$(ARCH)..." ifeq ($(OS),Windows_NT) @$(POWERSHELL) "New-Item -ItemType Directory -Force -Path '$(BUILD_DIR)' | Out-Null" +<<<<<<< HEAD @$(MAKE) -C web build PLATFORM="$(PLATFORM)" ARCH="$(ARCH)" EXT="$(EXT)" OUTPUT="$(CURDIR)/$(BUILD_DIR)/picoclaw-launcher-$(PLATFORM)-$(ARCH)$(EXT)" GO_BUILD_TAGS="$(GO_BUILD_TAGS)" +======= + @$(MAKE) -C web build \ + PLATFORM="$(PLATFORM)" \ + ARCH="$(ARCH)" \ + EXT="$(EXT)" \ + OUTPUT="$(CURDIR)/$(BUILD_DIR)/picoclaw-launcher-$(PLATFORM)-$(ARCH)$(EXT)" \ + GO_BUILD_TAGS="$(GO_BUILD_TAGS)" +>>>>>>> 83c79087 (Fix Windows build flow) @$(POWERSHELL) "Copy-Item -LiteralPath '$(BUILD_DIR)/picoclaw-launcher-$(PLATFORM)-$(ARCH)$(EXT)' -Destination '$(BUILD_DIR)/picoclaw-launcher$(EXT)' -Force" else @mkdir -p $(BUILD_DIR) diff --git a/web/Makefile b/web/Makefile index 0c9344a89..5580827bb 100644 --- a/web/Makefile +++ b/web/Makefile @@ -10,7 +10,10 @@ GOFLAGS?=-v -tags $(GO_BUILD_TAGS) GOCACHE?=$(abspath ../.cache/go-build) GOMODCACHE?=$(abspath ../.cache/go-mod) GOTOOLCHAIN?=local +<<<<<<< HEAD export CGO_ENABLED +======= +>>>>>>> 83c79087 (Fix Windows build flow) export GOCACHE export GOMODCACHE export GOTOOLCHAIN @@ -30,6 +33,11 @@ LAUNCHER_GUI_LDFLAG= ifeq ($(OS),Windows_NT) POWERSHELL=powershell -NoProfile -Command +<<<<<<< HEAD +======= + GO=go + WEB_GO=go +>>>>>>> 83c79087 (Fix Windows build flow) WINDOWS_GOARCH_RAW:=$(strip $(shell go env GOARCH 2>NUL)) endif @@ -164,6 +172,7 @@ endif build-frontend: ifeq ($(OS),Windows_NT) +<<<<<<< HEAD @$(POWERSHELL) "$$frontend='$(FRONTEND_DIR)'; $$stamp='$(FRONTEND_INSTALL_STAMP)'; $$pkg='$(FRONTEND_DIR)/package.json'; $$lock='$(FRONTEND_DIR)/pnpm-lock.yaml'; $$expected=((Get-FileHash -LiteralPath $$pkg -Algorithm SHA256).Hash + ':' + (Get-FileHash -LiteralPath $$lock -Algorithm SHA256).Hash); $$nodeModules=$$frontend + '/node_modules'; $$tsc=$$nodeModules + '/.bin/tsc'; $$current=''; if (Test-Path -LiteralPath $$stamp) { $$current=(Get-Content -LiteralPath $$stamp -Raw).Trim() }; if ((-not (Test-Path -LiteralPath $$nodeModules)) -or (-not (Test-Path -LiteralPath $$tsc)) -or ($$current -ne $$expected)) { Write-Host 'Installing frontend dependencies...'; Push-Location $$frontend; try { $$env:CI='true'; pnpm install --frozen-lockfile } finally { Pop-Location; Remove-Item Env:CI -ErrorAction SilentlyContinue }; Set-Content -LiteralPath $$stamp -Value $$expected -NoNewline }" else @expected_stamp="$$(cat $(FRONTEND_DIR)/package.json $(FRONTEND_DIR)/pnpm-lock.yaml | cksum | awk '{print $$1 ":" $$2}')"; \ @@ -171,6 +180,13 @@ else [ ! -x $(FRONTEND_DIR)/node_modules/.bin/tsc ] || \ [ ! -f $(FRONTEND_INSTALL_STAMP) ] || \ [ "$$(cat $(FRONTEND_INSTALL_STAMP) 2>/dev/null)" != "$$expected_stamp" ]; then \ +======= + @if not exist "$(FRONTEND_DIR)\node_modules" (echo Installing frontend dependencies... && cd $(FRONTEND_DIR) && pnpm install --frozen-lockfile --store-dir .pnpm-store) else echo Frontend dependencies already installed. +else + @if [ ! -d $(FRONTEND_DIR)/node_modules ] || \ + [ $(FRONTEND_DIR)/package.json -nt $(FRONTEND_DIR)/node_modules ] || \ + [ $(FRONTEND_DIR)/pnpm-lock.yaml -nt $(FRONTEND_DIR)/node_modules ]; then \ +>>>>>>> 83c79087 (Fix Windows build flow) echo "Installing frontend dependencies..."; \ (cd $(FRONTEND_DIR) && CI=true pnpm install --frozen-lockfile) && \ printf '%s\n' "$$expected_stamp" > $(FRONTEND_INSTALL_STAMP); \ From a977a92729052214ca6a8428dc6c656eba9a50df Mon Sep 17 00:00:00 2001 From: SiYue <2835601846@qq.com> Date: Fri, 24 Apr 2026 16:40:06 +0800 Subject: [PATCH 24/36] build(web): avoid shell-expanding powershell vars in windows recipe - rewrite build-frontend Windows command without PowerShell local vars - keep install-stamp hash check logic --- Makefile | 17 ----------------- web/Makefile | 18 +----------------- 2 files changed, 1 insertion(+), 34 deletions(-) diff --git a/Makefile b/Makefile index 11adcef06..acb258370 100644 --- a/Makefile +++ b/Makefile @@ -9,11 +9,6 @@ EXT= ifeq ($(OS),Windows_NT) POWERSHELL=powershell -NoProfile -Command -<<<<<<< HEAD -======= - GO=go - WEB_GO=go ->>>>>>> 83c79087 (Fix Windows build flow) WINDOWS_GOARCH_RAW:=$(strip $(shell go env GOARCH 2>NUL)) endif @@ -45,10 +40,7 @@ GOFLAGS?=-v -tags $(GO_BUILD_TAGS) GOCACHE?=$(CURDIR)/.cache/go-build GOMODCACHE?=$(CURDIR)/.cache/go-mod GOTOOLCHAIN?=local -<<<<<<< HEAD export CGO_ENABLED -======= ->>>>>>> 83c79087 (Fix Windows build flow) export GOCACHE export GOMODCACHE export GOTOOLCHAIN @@ -215,16 +207,7 @@ build-launcher: @echo "Building picoclaw-launcher for $(PLATFORM)/$(ARCH)..." ifeq ($(OS),Windows_NT) @$(POWERSHELL) "New-Item -ItemType Directory -Force -Path '$(BUILD_DIR)' | Out-Null" -<<<<<<< HEAD @$(MAKE) -C web build PLATFORM="$(PLATFORM)" ARCH="$(ARCH)" EXT="$(EXT)" OUTPUT="$(CURDIR)/$(BUILD_DIR)/picoclaw-launcher-$(PLATFORM)-$(ARCH)$(EXT)" GO_BUILD_TAGS="$(GO_BUILD_TAGS)" -======= - @$(MAKE) -C web build \ - PLATFORM="$(PLATFORM)" \ - ARCH="$(ARCH)" \ - EXT="$(EXT)" \ - OUTPUT="$(CURDIR)/$(BUILD_DIR)/picoclaw-launcher-$(PLATFORM)-$(ARCH)$(EXT)" \ - GO_BUILD_TAGS="$(GO_BUILD_TAGS)" ->>>>>>> 83c79087 (Fix Windows build flow) @$(POWERSHELL) "Copy-Item -LiteralPath '$(BUILD_DIR)/picoclaw-launcher-$(PLATFORM)-$(ARCH)$(EXT)' -Destination '$(BUILD_DIR)/picoclaw-launcher$(EXT)' -Force" else @mkdir -p $(BUILD_DIR) diff --git a/web/Makefile b/web/Makefile index 5580827bb..254c439e9 100644 --- a/web/Makefile +++ b/web/Makefile @@ -10,10 +10,7 @@ GOFLAGS?=-v -tags $(GO_BUILD_TAGS) GOCACHE?=$(abspath ../.cache/go-build) GOMODCACHE?=$(abspath ../.cache/go-mod) GOTOOLCHAIN?=local -<<<<<<< HEAD export CGO_ENABLED -======= ->>>>>>> 83c79087 (Fix Windows build flow) export GOCACHE export GOMODCACHE export GOTOOLCHAIN @@ -33,11 +30,6 @@ LAUNCHER_GUI_LDFLAG= ifeq ($(OS),Windows_NT) POWERSHELL=powershell -NoProfile -Command -<<<<<<< HEAD -======= - GO=go - WEB_GO=go ->>>>>>> 83c79087 (Fix Windows build flow) WINDOWS_GOARCH_RAW:=$(strip $(shell go env GOARCH 2>NUL)) endif @@ -172,21 +164,13 @@ endif build-frontend: ifeq ($(OS),Windows_NT) -<<<<<<< HEAD - @$(POWERSHELL) "$$frontend='$(FRONTEND_DIR)'; $$stamp='$(FRONTEND_INSTALL_STAMP)'; $$pkg='$(FRONTEND_DIR)/package.json'; $$lock='$(FRONTEND_DIR)/pnpm-lock.yaml'; $$expected=((Get-FileHash -LiteralPath $$pkg -Algorithm SHA256).Hash + ':' + (Get-FileHash -LiteralPath $$lock -Algorithm SHA256).Hash); $$nodeModules=$$frontend + '/node_modules'; $$tsc=$$nodeModules + '/.bin/tsc'; $$current=''; if (Test-Path -LiteralPath $$stamp) { $$current=(Get-Content -LiteralPath $$stamp -Raw).Trim() }; if ((-not (Test-Path -LiteralPath $$nodeModules)) -or (-not (Test-Path -LiteralPath $$tsc)) -or ($$current -ne $$expected)) { Write-Host 'Installing frontend dependencies...'; Push-Location $$frontend; try { $$env:CI='true'; pnpm install --frozen-lockfile } finally { Pop-Location; Remove-Item Env:CI -ErrorAction SilentlyContinue }; Set-Content -LiteralPath $$stamp -Value $$expected -NoNewline }" + @$(POWERSHELL) "if ((-not (Test-Path -LiteralPath '$(FRONTEND_DIR)/node_modules')) -or (-not (Test-Path -LiteralPath '$(FRONTEND_DIR)/node_modules/.bin/tsc')) -or (-not (Test-Path -LiteralPath '$(FRONTEND_INSTALL_STAMP)')) -or ((Get-Content -LiteralPath '$(FRONTEND_INSTALL_STAMP)' -Raw).Trim() -ne (((Get-FileHash -LiteralPath '$(FRONTEND_DIR)/package.json' -Algorithm SHA256).Hash + ':' + (Get-FileHash -LiteralPath '$(FRONTEND_DIR)/pnpm-lock.yaml' -Algorithm SHA256).Hash)))) { Write-Host 'Installing frontend dependencies...'; Push-Location '$(FRONTEND_DIR)'; try { pnpm install --frozen-lockfile } finally { Pop-Location }; Set-Content -LiteralPath '$(FRONTEND_INSTALL_STAMP)' -Value (((Get-FileHash -LiteralPath '$(FRONTEND_DIR)/package.json' -Algorithm SHA256).Hash + ':' + (Get-FileHash -LiteralPath '$(FRONTEND_DIR)/pnpm-lock.yaml' -Algorithm SHA256).Hash)) -NoNewline }" else @expected_stamp="$$(cat $(FRONTEND_DIR)/package.json $(FRONTEND_DIR)/pnpm-lock.yaml | cksum | awk '{print $$1 ":" $$2}')"; \ if [ ! -d $(FRONTEND_DIR)/node_modules ] || \ [ ! -x $(FRONTEND_DIR)/node_modules/.bin/tsc ] || \ [ ! -f $(FRONTEND_INSTALL_STAMP) ] || \ [ "$$(cat $(FRONTEND_INSTALL_STAMP) 2>/dev/null)" != "$$expected_stamp" ]; then \ -======= - @if not exist "$(FRONTEND_DIR)\node_modules" (echo Installing frontend dependencies... && cd $(FRONTEND_DIR) && pnpm install --frozen-lockfile --store-dir .pnpm-store) else echo Frontend dependencies already installed. -else - @if [ ! -d $(FRONTEND_DIR)/node_modules ] || \ - [ $(FRONTEND_DIR)/package.json -nt $(FRONTEND_DIR)/node_modules ] || \ - [ $(FRONTEND_DIR)/pnpm-lock.yaml -nt $(FRONTEND_DIR)/node_modules ]; then \ ->>>>>>> 83c79087 (Fix Windows build flow) echo "Installing frontend dependencies..."; \ (cd $(FRONTEND_DIR) && CI=true pnpm install --frozen-lockfile) && \ printf '%s\n' "$$expected_stamp" > $(FRONTEND_INSTALL_STAMP); \ From e1863234f0370a2b6325e203ed83910dbb754112 Mon Sep 17 00:00:00 2001 From: SiYue <2835601846@qq.com> Date: Fri, 24 Apr 2026 16:50:19 +0800 Subject: [PATCH 25/36] fix(launcher): hide windows child-process console flashes - hide windows when launching gateway process from launcher - hide windows for powershell/tasklist process inspection commands --- web/backend/api/exec_nonwindows.go | 11 +++++++++++ web/backend/api/exec_windows.go | 24 ++++++++++++++++++++++++ web/backend/api/gateway.go | 9 +++++---- 3 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 web/backend/api/exec_nonwindows.go create mode 100644 web/backend/api/exec_windows.go diff --git a/web/backend/api/exec_nonwindows.go b/web/backend/api/exec_nonwindows.go new file mode 100644 index 000000000..a68a3bfd7 --- /dev/null +++ b/web/backend/api/exec_nonwindows.go @@ -0,0 +1,11 @@ +//go:build !windows + +package api + +import "os/exec" + +func launcherExecCommand(name string, args ...string) *exec.Cmd { + return exec.Command(name, args...) +} + +func applyLauncherWindowsProcAttrs(_ *exec.Cmd) {} diff --git a/web/backend/api/exec_windows.go b/web/backend/api/exec_windows.go new file mode 100644 index 000000000..1e76f8c73 --- /dev/null +++ b/web/backend/api/exec_windows.go @@ -0,0 +1,24 @@ +//go:build windows + +package api + +import ( + "os/exec" + "syscall" +) + +func launcherExecCommand(name string, args ...string) *exec.Cmd { + cmd := exec.Command(name, args...) + applyLauncherWindowsProcAttrs(cmd) + return cmd +} + +func applyLauncherWindowsProcAttrs(cmd *exec.Cmd) { + if cmd == nil { + return + } + if cmd.SysProcAttr == nil { + cmd.SysProcAttr = &syscall.SysProcAttr{} + } + cmd.SysProcAttr.HideWindow = true +} diff --git a/web/backend/api/gateway.go b/web/backend/api/gateway.go index 201000ff3..d3e5ae1d5 100644 --- a/web/backend/api/gateway.go +++ b/web/backend/api/gateway.go @@ -164,7 +164,7 @@ func isLikelyGatewayProcess(pid int) (bool, bool) { `$p=Get-CimInstance Win32_Process -Filter "ProcessId = %d"; if ($null -eq $p) { "" } else { $p.CommandLine }`, pid, ) - out, err := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", psCmd).Output() + out, err := launcherExecCommand("powershell", "-NoProfile", "-NonInteractive", "-Command", psCmd).Output() if err == nil { cmdline := strings.TrimSpace(string(out)) if cmdline != "" { @@ -173,7 +173,7 @@ func isLikelyGatewayProcess(pid int) (bool, bool) { } // Fallback: determine only whether the process still exists. - out, err = exec.Command("tasklist", "/FI", "PID eq "+strconv.Itoa(pid), "/FO", "CSV", "/NH").Output() + out, err = launcherExecCommand("tasklist", "/FI", "PID eq "+strconv.Itoa(pid), "/FO", "CSV", "/NH").Output() if err != nil { return false, false } @@ -187,7 +187,7 @@ func isLikelyGatewayProcess(pid int) (bool, bool) { if strings.Contains(line, "\"picoclaw.exe\"") { return true, true } - return false, false + return false, true } if strings.Contains(line, "no tasks are running") { return false, true @@ -195,7 +195,7 @@ func isLikelyGatewayProcess(pid int) (bool, bool) { return false, true } - out, err := exec.Command("ps", "-o", "command=", "-p", strconv.Itoa(pid)).Output() + out, err := launcherExecCommand("ps", "-o", "command=", "-p", strconv.Itoa(pid)).Output() if err != nil { return false, false } @@ -706,6 +706,7 @@ func (h *Handler) startGatewayLocked(initialStatus string, existingPid int) (int logger.InfoC("gateway", fmt.Sprintf("Starting gateway process (%s)", execPath)) cmd = gatewayExecCommand(execPath, h.gatewayCommandArgs()...) + applyLauncherWindowsProcAttrs(cmd) cmd.Env = os.Environ() // Forward the launcher's config path via the environment variable that // GetConfigPath() already reads, so the gateway sub-process uses the same From 494cc381b5bf390bcd4243627ef12dd78dd50ec6 Mon Sep 17 00:00:00 2001 From: SiYue <2835601846@qq.com> Date: Fri, 24 Apr 2026 23:13:09 +0800 Subject: [PATCH 26/36] build(onboard): support codespace placeholder and path checks --- cmd/picoclaw/internal/onboard/command.go | 2 +- scripts/copydir.go | 80 +++++++++++++++++++++++- 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/cmd/picoclaw/internal/onboard/command.go b/cmd/picoclaw/internal/onboard/command.go index bf8f4104f..3f0ff0d8d 100644 --- a/cmd/picoclaw/internal/onboard/command.go +++ b/cmd/picoclaw/internal/onboard/command.go @@ -6,7 +6,7 @@ import ( "github.com/spf13/cobra" ) -//go:generate go run ../../../../scripts/copydir.go ../../../../workspace ./workspace +//go:generate go run ../../../../scripts/copydir.go "${DOLLAR}{codespace}/workspace" ./workspace //go:embed workspace var embeddedFiles embed.FS diff --git a/scripts/copydir.go b/scripts/copydir.go index 74eff6c72..35622ab17 100644 --- a/scripts/copydir.go +++ b/scripts/copydir.go @@ -5,6 +5,7 @@ import ( "io" "os" "path/filepath" + "strings" ) func main() { @@ -13,8 +14,36 @@ func main() { os.Exit(2) } - src := os.Args[1] - dst := os.Args[2] + repoRoot, err := findRepoRoot() + if err != nil { + fmt.Fprintf(os.Stderr, "locate repo root: %v\n", err) + os.Exit(1) + } + + src, err := normalizePathArg(os.Args[1], repoRoot) + if err != nil { + fmt.Fprintf(os.Stderr, "resolve src path: %v\n", err) + os.Exit(1) + } + + dst, err := normalizePathArg(os.Args[2], repoRoot) + if err != nil { + fmt.Fprintf(os.Stderr, "resolve dst path: %v\n", err) + os.Exit(1) + } + + if err := ensurePathWithinRepo(repoRoot, src); err != nil { + fmt.Fprintf(os.Stderr, "invalid src path: %v\n", err) + os.Exit(1) + } + if err := ensurePathWithinRepo(repoRoot, dst); err != nil { + fmt.Fprintf(os.Stderr, "invalid dst path: %v\n", err) + os.Exit(1) + } + if samePath(repoRoot, dst) { + fmt.Fprintln(os.Stderr, "invalid dst path: destination cannot be repo root") + os.Exit(1) + } if err := os.RemoveAll(dst); err != nil { fmt.Fprintf(os.Stderr, "remove %s: %v\n", dst, err) @@ -27,6 +56,53 @@ func main() { } } +func findRepoRoot() (string, error) { + wd, err := os.Getwd() + if err != nil { + return "", err + } + + cur, err := filepath.Abs(wd) + if err != nil { + return "", err + } + + for { + if _, err := os.Stat(filepath.Join(cur, ".git")); err == nil { + return filepath.Clean(cur), nil + } + parent := filepath.Dir(cur) + if parent == cur { + return "", fmt.Errorf("could not find .git from %s", wd) + } + cur = parent + } +} + +func normalizePathArg(arg, repoRoot string) (string, error) { + resolved := strings.ReplaceAll(arg, "${codespace}", repoRoot) + abs, err := filepath.Abs(resolved) + if err != nil { + return "", err + } + return filepath.Clean(abs), nil +} + +func ensurePathWithinRepo(repoRoot, path string) error { + rel, err := filepath.Rel(repoRoot, path) + if err != nil { + return err + } + if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) { + return fmt.Errorf("path %s is outside repository root %s", path, repoRoot) + } + return nil +} + +func samePath(a, b string) bool { + return filepath.Clean(a) == filepath.Clean(b) +} + func copyTree(src, dst string) error { info, err := os.Stat(src) if err != nil { From 683ce31f2b1ced90f24d90a5138274165c730531 Mon Sep 17 00:00:00 2001 From: SiYue-ZO <2835601846@qq.com> Date: Fri, 24 Apr 2026 23:58:42 +0800 Subject: [PATCH 27/36] fix: isolate thought bubble collapse state --- web/frontend/src/components/chat/assistant-message.tsx | 5 ++--- web/frontend/src/store/chat.ts | 2 -- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/web/frontend/src/components/chat/assistant-message.tsx b/web/frontend/src/components/chat/assistant-message.tsx index c09f5a06d..814ddc2f9 100644 --- a/web/frontend/src/components/chat/assistant-message.tsx +++ b/web/frontend/src/components/chat/assistant-message.tsx @@ -6,7 +6,6 @@ import { IconDownload, IconFileText, } from "@tabler/icons-react" -import { useAtom } from "jotai" import { useState } from "react" import { useTranslation } from "react-i18next" import ReactMarkdown from "react-markdown" @@ -18,7 +17,7 @@ import remarkGfm from "remark-gfm" import { Button } from "@/components/ui/button" import { formatMessageTime } from "@/hooks/use-pico-chat" import { cn } from "@/lib/utils" -import { type ChatAttachment, showThoughtsAtom } from "@/store/chat" +import { type ChatAttachment } from "@/store/chat" interface AssistantMessageProps { content: string @@ -42,7 +41,7 @@ export function AssistantMessage({ const fileAttachments = attachments.filter( (attachment) => attachment.type !== "image", ) - const [isExpanded, setIsExpanded] = useAtom(showThoughtsAtom) + const [isExpanded, setIsExpanded] = useState(true) const formattedTimestamp = timestamp !== "" ? formatMessageTime(timestamp) : "" diff --git a/web/frontend/src/store/chat.ts b/web/frontend/src/store/chat.ts index 393254416..21d6ad709 100644 --- a/web/frontend/src/store/chat.ts +++ b/web/frontend/src/store/chat.ts @@ -57,8 +57,6 @@ const DEFAULT_CHAT_STATE: ChatStoreState = { export const chatAtom = atom(DEFAULT_CHAT_STATE) -export const showThoughtsAtom = atom(true) - const store = getDefaultStore() export function getChatState() { From 94a6b0c0f5bda043c39ca2318c4f99fec9eb1a3b Mon Sep 17 00:00:00 2001 From: afjcjsbx Date: Fri, 24 Apr 2026 18:07:48 +0200 Subject: [PATCH 28/36] fix(tool-feedback): format tool args as JSON code blocks --- pkg/agent/agent_test.go | 25 ++++++++++++++++++- pkg/agent/agent_utils.go | 13 ++++++++++ pkg/agent/pipeline_execute.go | 18 ++++++++++--- .../telegram/parser_markdown_to_html_test.go | 5 ++++ pkg/utils/tool_feedback.go | 24 ++++++++++++------ pkg/utils/tool_feedback_test.go | 19 ++++++++++---- 6 files changed, 87 insertions(+), 17 deletions(-) diff --git a/pkg/agent/agent_test.go b/pkg/agent/agent_test.go index b0aa3b468..030b65a6d 100644 --- a/pkg/agent/agent_test.go +++ b/pkg/agent/agent_test.go @@ -1965,6 +1965,17 @@ func TestToolFeedbackExplanationFromResponse_DoesNotUseReasoningContent(t *testi } } +func TestToolFeedbackArgsPreview_UsesJSONAndTruncates(t *testing.T) { + got := toolFeedbackArgsPreview(map[string]any{ + "path": "README.md", + "limit": 42, + }, 128) + want := "{\n \"limit\": 42,\n \"path\": \"README.md\"\n}" + if got != want { + t.Fatalf("toolFeedbackArgsPreview() = %q, want %q", got, want) + } +} + type picoInterleavedContentProvider struct { calls int } @@ -3940,6 +3951,12 @@ func TestProcessMessage_PublishesToolFeedbackWhenEnabled(t *testing.T) { if !strings.Contains(outbound.Content, "check tool feedback") { t.Fatalf("tool feedback content = %q, want current user intent fallback", outbound.Content) } + if !strings.Contains(outbound.Content, "\"path\":") { + t.Fatalf("tool feedback content = %q, want serialized tool arguments", outbound.Content) + } + if !strings.Contains(outbound.Content, heartbeatFile) { + t.Fatalf("tool feedback content = %q, want tool argument value", outbound.Content) + } if strings.Contains(outbound.Content, "Previous turn explanation") { t.Fatalf("tool feedback content = %q, want no previous assistant fallback", outbound.Content) } @@ -4012,6 +4029,12 @@ func TestProcessMessage_DoesNotLeakReasoningContentInToolFeedback(t *testing.T) if !strings.Contains(outbound.Content, "check reasoning fallback") { t.Fatalf("tool feedback content = %q, want current user intent fallback", outbound.Content) } + if !strings.Contains(outbound.Content, "\"path\":") { + t.Fatalf("tool feedback content = %q, want serialized tool arguments", outbound.Content) + } + if !strings.Contains(outbound.Content, heartbeatFile) { + t.Fatalf("tool feedback content = %q, want tool argument value", outbound.Content) + } if strings.Contains(outbound.Content, "Read README.md first") { t.Fatalf("tool feedback content = %q, should not leak hidden reasoning", outbound.Content) } @@ -4310,7 +4333,7 @@ func TestRun_PicoToolFeedbackSuppressesDuplicateInterimAssistantContent(t *testi } } - if outputs[0] != "🔧 `tool_limit_test_tool`\nintermediate model text" { + if outputs[0] != "🔧 `tool_limit_test_tool`\nintermediate model text\n```json\n{\n \"value\": \"x\"\n}\n```" { t.Fatalf("first outbound content = %q, want tool feedback summary", outputs[0]) } if outputs[1] != "final model text" { diff --git a/pkg/agent/agent_utils.go b/pkg/agent/agent_utils.go index ff98dad68..17fb9dd1f 100644 --- a/pkg/agent/agent_utils.go +++ b/pkg/agent/agent_utils.go @@ -4,6 +4,7 @@ package agent import ( "context" + "encoding/json" "fmt" "path/filepath" "strings" @@ -170,6 +171,18 @@ func toolFeedbackExplanationFromMessages(messages []providers.Message) string { return "" } +func toolFeedbackArgsPreview(args map[string]any, maxLen int) string { + if args == nil { + args = map[string]any{} + } + + argsJSON, err := json.MarshalIndent(args, "", " ") + if err != nil { + return utils.Truncate(fmt.Sprintf("%v", args), maxLen) + } + return utils.Truncate(string(argsJSON), maxLen) +} + func shouldPublishToolFeedback(cfg *config.Config, ts *turnState) bool { if ts == nil || ts.channel == "" || ts.opts.SuppressToolFeedback { return false diff --git a/pkg/agent/pipeline_execute.go b/pkg/agent/pipeline_execute.go index 48e72e096..727e3428f 100644 --- a/pkg/agent/pipeline_execute.go +++ b/pkg/agent/pipeline_execute.go @@ -81,13 +81,18 @@ toolLoop: ) if shouldPublishToolFeedback(al.cfg, ts) { + toolFeedbackMaxLen := al.cfg.Agents.Defaults.GetToolFeedbackMaxArgsLength() toolFeedbackExplanation := toolFeedbackExplanationForToolCall( exec.response, tc, messages, - al.cfg.Agents.Defaults.GetToolFeedbackMaxArgsLength(), + toolFeedbackMaxLen, + ) + feedbackMsg := utils.FormatToolFeedbackMessage( + toolName, + toolFeedbackExplanation, + toolFeedbackArgsPreview(toolArgs, toolFeedbackMaxLen), ) - feedbackMsg := utils.FormatToolFeedbackMessage(toolName, toolFeedbackExplanation) fbCtx, fbCancel := context.WithTimeout(turnCtx, 3*time.Second) _ = al.bus.PublishOutbound(fbCtx, outboundMessageForTurnWithKind(ts, feedbackMsg, messageKindToolFeedback)) fbCancel() @@ -358,13 +363,18 @@ toolLoop: ) if shouldPublishToolFeedback(al.cfg, ts) { + toolFeedbackMaxLen := al.cfg.Agents.Defaults.GetToolFeedbackMaxArgsLength() toolFeedbackExplanation := toolFeedbackExplanationForToolCall( exec.response, tc, messages, - al.cfg.Agents.Defaults.GetToolFeedbackMaxArgsLength(), + toolFeedbackMaxLen, + ) + feedbackMsg := utils.FormatToolFeedbackMessage( + toolName, + toolFeedbackExplanation, + toolFeedbackArgsPreview(toolArgs, toolFeedbackMaxLen), ) - feedbackMsg := utils.FormatToolFeedbackMessage(toolName, toolFeedbackExplanation) fbCtx, fbCancel := context.WithTimeout(turnCtx, 3*time.Second) _ = al.bus.PublishOutbound(fbCtx, outboundMessageForTurnWithKind(ts, feedbackMsg, messageKindToolFeedback)) fbCancel() diff --git a/pkg/channels/telegram/parser_markdown_to_html_test.go b/pkg/channels/telegram/parser_markdown_to_html_test.go index a05b39877..a54a1c2c7 100644 --- a/pkg/channels/telegram/parser_markdown_to_html_test.go +++ b/pkg/channels/telegram/parser_markdown_to_html_test.go @@ -65,6 +65,11 @@ func Test_markdownToTelegramHTML(t *testing.T) { input: "a & b < c > d", expected: "a & b < c > d", }, + { + name: "code block with language", + input: "```json\n{\n \"path\": \"README.md\"\n}\n```", + expected: "
{\n  \"path\": \"README.md\"\n}\n
", + }, } for _, tc := range cases { diff --git a/pkg/utils/tool_feedback.go b/pkg/utils/tool_feedback.go index 1a8b6c747..de7cb467e 100644 --- a/pkg/utils/tool_feedback.go +++ b/pkg/utils/tool_feedback.go @@ -7,21 +7,31 @@ import ( const ToolFeedbackContinuationHint = "Continuing the current task." -// FormatToolFeedbackMessage renders the model-provided explanation for why a -// tool is being executed. When the model does not provide one, it keeps only -// the tool line and does not expose raw arguments or fallback text. -func FormatToolFeedbackMessage(toolName, explanation string) string { +// FormatToolFeedbackMessage renders a tool feedback message for chat channels. +// It keeps the tool name on the first line for animation and can include both +// a human explanation and the serialized tool arguments in the body. +func FormatToolFeedbackMessage(toolName, explanation, argsPreview string) string { toolName = strings.TrimSpace(toolName) explanation = strings.TrimSpace(explanation) + argsPreview = strings.TrimSpace(argsPreview) + + bodyLines := make([]string, 0, 2) + if explanation != "" { + bodyLines = append(bodyLines, explanation) + } + if argsPreview != "" { + bodyLines = append(bodyLines, "```json\n"+argsPreview+"\n```") + } + body := strings.Join(bodyLines, "\n") if toolName == "" { - return explanation + return body } - if explanation == "" { + if body == "" { return fmt.Sprintf("\U0001f527 `%s`", toolName) } - return fmt.Sprintf("\U0001f527 `%s`\n%s", toolName, explanation) + return fmt.Sprintf("\U0001f527 `%s`\n%s", toolName, body) } // FitToolFeedbackMessage keeps tool feedback within a single outbound message. diff --git a/pkg/utils/tool_feedback_test.go b/pkg/utils/tool_feedback_test.go index 316ce2408..c30f53827 100644 --- a/pkg/utils/tool_feedback_test.go +++ b/pkg/utils/tool_feedback_test.go @@ -6,29 +6,38 @@ func TestFormatToolFeedbackMessage(t *testing.T) { got := FormatToolFeedbackMessage( "read_file", "I will read README.md first to confirm the current project structure.", + "{\n \"path\": \"README.md\"\n}", ) - want := "\U0001f527 `read_file`\nI will read README.md first to confirm the current project structure." + want := "\U0001f527 `read_file`\nI will read README.md first to confirm the current project structure.\n```json\n{\n \"path\": \"README.md\"\n}\n```" if got != want { t.Fatalf("FormatToolFeedbackMessage() = %q, want %q", got, want) } } -func TestFormatToolFeedbackMessage_EmptyExplanationKeepsOnlyToolLine(t *testing.T) { - got := FormatToolFeedbackMessage("read_file", "") - want := "\U0001f527 `read_file`" +func TestFormatToolFeedbackMessage_EmptyExplanationShowsArgs(t *testing.T) { + got := FormatToolFeedbackMessage("read_file", "", "{\n \"path\": \"README.md\"\n}") + want := "\U0001f527 `read_file`\n```json\n{\n \"path\": \"README.md\"\n}\n```" if got != want { t.Fatalf("FormatToolFeedbackMessage() = %q, want %q", got, want) } } func TestFormatToolFeedbackMessage_EmptyToolNameOmitsToolLine(t *testing.T) { - got := FormatToolFeedbackMessage("", "Continue drafting the final response.") + got := FormatToolFeedbackMessage("", "Continue drafting the final response.", "") want := "Continue drafting the final response." if got != want { t.Fatalf("FormatToolFeedbackMessage() = %q, want %q", got, want) } } +func TestFormatToolFeedbackMessage_EmptyExplanationAndArgsKeepsOnlyToolLine(t *testing.T) { + got := FormatToolFeedbackMessage("read_file", "", "") + want := "\U0001f527 `read_file`" + if got != want { + t.Fatalf("FormatToolFeedbackMessage() = %q, want %q", got, want) + } +} + func TestFitToolFeedbackMessage_TruncatesBodyWithinSingleMessage(t *testing.T) { got := FitToolFeedbackMessage( "\U0001f527 `read_file`\nRead README.md first to confirm the current project structure.", From dce29c181f183e6f3d6021b320fc0e21f244faed Mon Sep 17 00:00:00 2001 From: afjcjsbx Date: Fri, 24 Apr 2026 18:21:01 +0200 Subject: [PATCH 29/36] fix lint --- web/backend/api/session.go | 34 ++++++++++++++++++++++++++++----- web/backend/api/session_test.go | 11 ++++++++--- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/web/backend/api/session.go b/web/backend/api/session.go index 6ac1eb988..85f928ec6 100644 --- a/web/backend/api/session.go +++ b/web/backend/api/session.go @@ -2,6 +2,7 @@ package api import ( "bufio" + "bytes" "encoding/json" "errors" "net/http" @@ -593,7 +594,15 @@ func toolSummaryContainsContent(summary, content string) bool { } _, body, hasBody := strings.Cut(summary, "\n") - return hasBody && strings.TrimSpace(body) == content + if !hasBody { + return false + } + body = strings.TrimSpace(body) + if body == content { + return true + } + firstSection, _, _ := strings.Cut(body, "\n```") + return strings.TrimSpace(firstSection) == content } func sessionAttachments(msg providers.Message) []sessionChatAttachment { @@ -714,7 +723,8 @@ func visibleAssistantToolSummaryMessages( Role: "assistant", Content: utils.FormatToolFeedbackMessage( name, - visibleAssistantToolSummaryText(tc, toolFeedbackMaxArgsLength), + visibleAssistantToolFeedbackExplanation(tc, toolFeedbackMaxArgsLength), + visibleAssistantToolArgsPreview(tc, toolFeedbackMaxArgsLength), ), }) } @@ -722,7 +732,7 @@ func visibleAssistantToolSummaryMessages( return messages } -func visibleAssistantToolSummaryText( +func visibleAssistantToolFeedbackExplanation( tc providers.ToolCall, toolFeedbackMaxArgsLength int, ) string { @@ -731,18 +741,32 @@ func visibleAssistantToolSummaryText( return utils.Truncate(explanation, toolFeedbackMaxArgsLength) } } + return "" +} +func visibleAssistantToolArgsPreview( + tc providers.ToolCall, + toolFeedbackMaxArgsLength int, +) string { argsJSON := "" if tc.Function != nil { argsJSON = tc.Function.Arguments } if strings.TrimSpace(argsJSON) == "" && len(tc.Arguments) > 0 { - if encodedArgs, err := json.Marshal(tc.Arguments); err == nil { + if encodedArgs, err := json.MarshalIndent(tc.Arguments, "", " "); err == nil { argsJSON = string(encodedArgs) } } + argsJSON = strings.TrimSpace(argsJSON) + if argsJSON == "" { + return "" + } + var pretty bytes.Buffer + if err := json.Indent(&pretty, []byte(argsJSON), "", " "); err == nil { + argsJSON = pretty.String() + } - return utils.Truncate(strings.TrimSpace(argsJSON), toolFeedbackMaxArgsLength) + return utils.Truncate(argsJSON, toolFeedbackMaxArgsLength) } func visibleAssistantToolMessages(toolCalls []providers.ToolCall) []sessionChatMessage { diff --git a/web/backend/api/session_test.go b/web/backend/api/session_test.go index 6afb8a94f..428c565a8 100644 --- a/web/backend/api/session_test.go +++ b/web/backend/api/session_test.go @@ -1056,8 +1056,11 @@ func TestHandleGetSession_UsesConfiguredToolFeedbackMaxArgsLength(t *testing.T) if !strings.Contains(resp.Messages[1].Content, wantPreview) { t.Fatalf("tool summary = %q, want preview %q", resp.Messages[1].Content, wantPreview) } - if strings.Contains(resp.Messages[1].Content, argsJSON) { - t.Fatalf("tool summary = %q, expected configured truncation", resp.Messages[1].Content) + wantArgsPreview := visibleAssistantToolArgsPreview(providers.ToolCall{ + Function: &providers.FunctionCall{Arguments: argsJSON}, + }, 20) + if !strings.Contains(resp.Messages[1].Content, wantArgsPreview) { + t.Fatalf("tool summary = %q, want args preview %q", resp.Messages[1].Content, wantArgsPreview) } if !strings.Contains(resp.Messages[1].Content, "`read_file`") { t.Fatalf("tool summary = %q, want read_file summary", resp.Messages[1].Content) @@ -1132,7 +1135,9 @@ func TestHandleGetSession_FallsBackToLegacyToolArgumentsWhenExplanationMissing(t t.Fatalf("len(resp.Messages) = %d, want at least 2", len(resp.Messages)) } - wantPreview := utils.Truncate(argsJSON, 20) + wantPreview := visibleAssistantToolArgsPreview(providers.ToolCall{ + Function: &providers.FunctionCall{Arguments: argsJSON}, + }, 20) if !strings.Contains(resp.Messages[1].Content, "`read_file`") { t.Fatalf("tool summary = %q, want read_file summary", resp.Messages[1].Content) } From b4a59656025c6eb863ec4755b67a58820aab6d69 Mon Sep 17 00:00:00 2001 From: SiYue <2835601846@qq.com> Date: Sat, 25 Apr 2026 00:31:36 +0800 Subject: [PATCH 30/36] refactor(onboard,api): harden copydir repo-root detection and use platform-neutral proc attrs naming Address latest review comments from sky5454 in PR #2654. scripts/copydir.go: - Improve repository root detection in a safer, more deterministic way. - Prefer locating repo root from the script source path via runtime.Caller(), then fallback to upward search from current working directory. - Replace .git-only root detection with repository anchor validation: go.sum, LICENSE, and .github must exist. - Keep \ placeholder expansion and existing in-repo path guards. - Preserve destination safety check to prevent deleting/copying to repo root. web/backend/api: - Rename applyLauncherWindowsProcAttrs() to applyLauncherProcAttrs() to expose a platform-independent interface name. - Keep platform-specific behavior split by build tags: windows keeps HideWindow SysProcAttr setup, non-windows remains no-op. - Update gateway startup path to call the renamed helper. Why: - Follow reviewer feedback to avoid relying on .git detection alone and prefer runtime/file-anchor based repository location. - Improve naming clarity by making cross-platform interfaces generic while preserving OS-specific implementation details internally. Validation: - go test ./cmd/picoclaw/internal/onboard - go test ./web/backend/api --- scripts/copydir.go | 30 ++++++++++++++++++++++++++++-- web/backend/api/exec_nonwindows.go | 2 +- web/backend/api/exec_windows.go | 4 ++-- web/backend/api/gateway.go | 2 +- 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/scripts/copydir.go b/scripts/copydir.go index 35622ab17..6e2777612 100644 --- a/scripts/copydir.go +++ b/scripts/copydir.go @@ -5,6 +5,7 @@ import ( "io" "os" "path/filepath" + "runtime" "strings" ) @@ -57,6 +58,17 @@ func main() { } func findRepoRoot() (string, error) { + _, file, _, ok := runtime.Caller(0) + if !ok { + return "", fmt.Errorf("unable to locate copydir.go source path") + } + + scriptDir := filepath.Dir(file) + candidate := filepath.Clean(filepath.Join(scriptDir, "..")) + if err := validateRepoRoot(candidate); err == nil { + return candidate, nil + } + wd, err := os.Getwd() if err != nil { return "", err @@ -68,17 +80,31 @@ func findRepoRoot() (string, error) { } for { - if _, err := os.Stat(filepath.Join(cur, ".git")); err == nil { + if err := validateRepoRoot(cur); err == nil { return filepath.Clean(cur), nil } parent := filepath.Dir(cur) if parent == cur { - return "", fmt.Errorf("could not find .git from %s", wd) + return "", fmt.Errorf("could not find repository root from %s", wd) } cur = parent } } +func validateRepoRoot(root string) error { + anchors := []string{ + filepath.Join(root, "go.sum"), + filepath.Join(root, "LICENSE"), + filepath.Join(root, ".github"), + } + for _, anchor := range anchors { + if _, err := os.Stat(anchor); err != nil { + return fmt.Errorf("missing repo anchor %s: %w", anchor, err) + } + } + return nil +} + func normalizePathArg(arg, repoRoot string) (string, error) { resolved := strings.ReplaceAll(arg, "${codespace}", repoRoot) abs, err := filepath.Abs(resolved) diff --git a/web/backend/api/exec_nonwindows.go b/web/backend/api/exec_nonwindows.go index a68a3bfd7..0dc3c0e94 100644 --- a/web/backend/api/exec_nonwindows.go +++ b/web/backend/api/exec_nonwindows.go @@ -8,4 +8,4 @@ func launcherExecCommand(name string, args ...string) *exec.Cmd { return exec.Command(name, args...) } -func applyLauncherWindowsProcAttrs(_ *exec.Cmd) {} +func applyLauncherProcAttrs(_ *exec.Cmd) {} diff --git a/web/backend/api/exec_windows.go b/web/backend/api/exec_windows.go index 1e76f8c73..86d3193a0 100644 --- a/web/backend/api/exec_windows.go +++ b/web/backend/api/exec_windows.go @@ -9,11 +9,11 @@ import ( func launcherExecCommand(name string, args ...string) *exec.Cmd { cmd := exec.Command(name, args...) - applyLauncherWindowsProcAttrs(cmd) + applyLauncherProcAttrs(cmd) return cmd } -func applyLauncherWindowsProcAttrs(cmd *exec.Cmd) { +func applyLauncherProcAttrs(cmd *exec.Cmd) { if cmd == nil { return } diff --git a/web/backend/api/gateway.go b/web/backend/api/gateway.go index d3e5ae1d5..606c8351d 100644 --- a/web/backend/api/gateway.go +++ b/web/backend/api/gateway.go @@ -706,7 +706,7 @@ func (h *Handler) startGatewayLocked(initialStatus string, existingPid int) (int logger.InfoC("gateway", fmt.Sprintf("Starting gateway process (%s)", execPath)) cmd = gatewayExecCommand(execPath, h.gatewayCommandArgs()...) - applyLauncherWindowsProcAttrs(cmd) + applyLauncherProcAttrs(cmd) cmd.Env = os.Environ() // Forward the launcher's config path via the environment variable that // GetConfigPath() already reads, so the gateway sub-process uses the same From 9ca73b944f0620a91e0dbb71cd11183efb32af87 Mon Sep 17 00:00:00 2001 From: Hoshina Date: Sat, 25 Apr 2026 01:25:17 +0800 Subject: [PATCH 31/36] fix(agent): preserve prompt hook and cache semantics --- pkg/agent/hooks.go | 28 +++++++++++- pkg/agent/hooks_test.go | 93 ++++++++++++++++++++++++++++++++++++++++ pkg/agent/prompt_test.go | 12 ++++++ pkg/agent/prompt_turn.go | 12 ++++++ 4 files changed, 144 insertions(+), 1 deletion(-) diff --git a/pkg/agent/hooks.go b/pkg/agent/hooks.go index 69336b89f..2d60e8120 100644 --- a/pkg/agent/hooks.go +++ b/pkg/agent/hooks.go @@ -409,6 +409,7 @@ func systemMessageFingerprints(messages []providers.Message) []systemMessageFing if msg.Role != "system" { continue } + msg = providerVisibleMessage(msg) fingerprints = append(fingerprints, systemMessageFingerprint{ Index: i, Message: cloneProviderMessages([]providers.Message{msg})[0], @@ -418,7 +419,32 @@ func systemMessageFingerprints(messages []providers.Message) []systemMessageFing } func llmHookToolDefinitionsUnchanged(before, after []providers.ToolDefinition) bool { - return reflect.DeepEqual(cloneToolDefinitions(before), cloneToolDefinitions(after)) + return reflect.DeepEqual(providerVisibleToolDefinitions(before), providerVisibleToolDefinitions(after)) +} + +func providerVisibleMessage(msg providers.Message) providers.Message { + msg.PromptLayer = "" + msg.PromptSlot = "" + msg.PromptSource = "" + if len(msg.SystemParts) > 0 { + msg.SystemParts = append([]providers.ContentBlock(nil), msg.SystemParts...) + for i := range msg.SystemParts { + msg.SystemParts[i].PromptLayer = "" + msg.SystemParts[i].PromptSlot = "" + msg.SystemParts[i].PromptSource = "" + } + } + return msg +} + +func providerVisibleToolDefinitions(defs []providers.ToolDefinition) []providers.ToolDefinition { + cloned := cloneToolDefinitions(defs) + for i := range cloned { + cloned[i].PromptLayer = "" + cloned[i].PromptSlot = "" + cloned[i].PromptSource = "" + } + return cloned } func (hm *HookManager) BeforeTool( diff --git a/pkg/agent/hooks_test.go b/pkg/agent/hooks_test.go index a936296dd..65400e4b8 100644 --- a/pkg/agent/hooks_test.go +++ b/pkg/agent/hooks_test.go @@ -2,6 +2,7 @@ package agent import ( "context" + "encoding/json" "errors" "os" "strings" @@ -186,6 +187,46 @@ func (h *llmUserAppendHook) AfterLLM( return resp.Clone(), HookDecision{Action: HookActionContinue}, nil } +type llmJSONRoundTripUserAppendHook struct{} + +type jsonRoundTripLLMHookRequest struct { + Model string `json:"model"` + Messages []providers.Message `json:"messages,omitempty"` + Tools []providers.ToolDefinition `json:"tools,omitempty"` +} + +func (h *llmJSONRoundTripUserAppendHook) BeforeLLM( + ctx context.Context, + req *LLMHookRequest, +) (*LLMHookRequest, HookDecision, error) { + payload := jsonRoundTripLLMHookRequest{ + Model: req.Model, + Messages: req.Messages, + Tools: req.Tools, + } + data, err := json.Marshal(payload) + if err != nil { + return nil, HookDecision{}, err + } + var decoded jsonRoundTripLLMHookRequest + if err := json.Unmarshal(data, &decoded); err != nil { + return nil, HookDecision{}, err + } + next := req.Clone() + next.Model = decoded.Model + next.Messages = decoded.Messages + next.Tools = decoded.Tools + next.Messages = append(next.Messages, providers.Message{Role: "user", Content: "json extra user context"}) + return next, HookDecision{Action: HookActionModify}, nil +} + +func (h *llmJSONRoundTripUserAppendHook) AfterLLM( + ctx context.Context, + resp *LLMHookResponse, +) (*LLMHookResponse, HookDecision, error) { + return resp.Clone(), HookDecision{Action: HookActionContinue}, nil +} + type llmToolRewriteHook struct{} func (h *llmToolRewriteHook) BeforeLLM( @@ -274,6 +315,58 @@ func TestHookManager_BeforeLLMAllowsNonSystemMessageMutation(t *testing.T) { } } +func TestHookManager_BeforeLLMAllowsJSONRoundTripNonSystemMessageMutation(t *testing.T) { + hm := NewHookManager(nil) + if err := hm.Mount(NamedHook("json-append-user", &llmJSONRoundTripUserAppendHook{})); err != nil { + t.Fatalf("Mount() error = %v", err) + } + + req := &LLMHookRequest{ + Model: "model", + Messages: []providers.Message{ + { + Role: "system", + Content: "system", + PromptLayer: string(PromptLayerKernel), + PromptSlot: string(PromptSlotIdentity), + PromptSource: string(PromptSourceKernel), + SystemParts: []providers.ContentBlock{ + { + Type: "text", + Text: "system", + CacheControl: &providers.CacheControl{Type: "ephemeral"}, + PromptLayer: string(PromptLayerKernel), + PromptSlot: string(PromptSlotIdentity), + PromptSource: string(PromptSourceKernel), + }, + }, + }, + {Role: "user", Content: "hello"}, + }, + Tools: []providers.ToolDefinition{ + { + Type: "function", + Function: providers.ToolFunctionDefinition{ + Name: "mcp_github_create_issue", + Description: "create issue", + Parameters: map[string]any{"type": "object"}, + }, + PromptLayer: string(PromptLayerCapability), + PromptSlot: string(PromptSlotMCP), + PromptSource: "mcp:github", + }, + }, + } + + got, _ := hm.BeforeLLM(context.Background(), req) + if len(got.Messages) != 3 { + t.Fatalf("messages len = %d, want 3", len(got.Messages)) + } + if got.Messages[2].Role != "user" || got.Messages[2].Content != "json extra user context" { + t.Fatalf("appended message = %#v, want json extra user context", got.Messages[2]) + } +} + func TestHookManager_BeforeLLMControlsToolDefinitionMutation(t *testing.T) { hm := NewHookManager(nil) if err := hm.Mount(NamedHook("rewrite-tool", &llmToolRewriteHook{})); err != nil { diff --git a/pkg/agent/prompt_test.go b/pkg/agent/prompt_test.go index b3f609610..b76b0040d 100644 --- a/pkg/agent/prompt_test.go +++ b/pkg/agent/prompt_test.go @@ -135,8 +135,14 @@ func TestBuildMessagesFromPrompt_AttachesInternalPromptMetadata(t *testing.T) { switch part.PromptSource { case string(PromptSourceRuntime): hasRuntime = true + if part.CacheControl != nil { + t.Fatalf("runtime cache control = %#v, want nil", part.CacheControl) + } case string(PromptSourceSummary): hasSummary = true + if part.CacheControl != nil { + t.Fatalf("summary cache control = %#v, want nil", part.CacheControl) + } } } if !hasRuntime { @@ -181,6 +187,9 @@ func TestContextBuilder_CollectsToolDiscoveryContributor(t *testing.T) { if part.PromptLayer != string(PromptLayerCapability) || part.PromptSlot != string(PromptSlotTooling) { t.Fatalf("tool discovery metadata = %#v, want capability/tooling", part) } + if part.CacheControl == nil || part.CacheControl.Type != "ephemeral" { + t.Fatalf("tool discovery cache control = %#v, want ephemeral", part.CacheControl) + } } } if !found { @@ -213,6 +222,9 @@ func TestContextBuilder_CollectsMCPServerContributor(t *testing.T) { if part.PromptLayer != string(PromptLayerCapability) || part.PromptSlot != string(PromptSlotMCP) { t.Fatalf("mcp metadata = %#v, want capability/mcp", part) } + if part.CacheControl == nil || part.CacheControl.Type != "ephemeral" { + t.Fatalf("mcp cache control = %#v, want ephemeral", part.CacheControl) + } } } if !found { diff --git a/pkg/agent/prompt_turn.go b/pkg/agent/prompt_turn.go index 7b7d295c1..588a8f00f 100644 --- a/pkg/agent/prompt_turn.go +++ b/pkg/agent/prompt_turn.go @@ -49,6 +49,9 @@ func promptOverlaysForOptions(opts processOptions) []PromptPart { } func promptContentBlock(part PromptPart, cache *providers.CacheControl) providers.ContentBlock { + if cache == nil { + cache = cacheControlForPromptPart(part) + } return providers.ContentBlock{ Type: "text", Text: part.Content, @@ -59,6 +62,15 @@ func promptContentBlock(part PromptPart, cache *providers.CacheControl) provider } } +func cacheControlForPromptPart(part PromptPart) *providers.CacheControl { + switch part.Cache { + case PromptCacheEphemeral: + return &providers.CacheControl{Type: "ephemeral"} + default: + return nil + } +} + func promptMessageWithMetadata( msg providers.Message, layer PromptLayer, From 8f8af0874deb1306b21856c24a5809e8398df680 Mon Sep 17 00:00:00 2001 From: afjcjsbx Date: Fri, 24 Apr 2026 20:20:57 +0200 Subject: [PATCH 32/36] fix(mcp): retry tool calls on lost HTTP sessions and fix client lifecycle --- pkg/mcp/manager.go | 198 +++++++++++++++++++++++++++++------- pkg/mcp/manager_test.go | 215 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 377 insertions(+), 36 deletions(-) diff --git a/pkg/mcp/manager.go b/pkg/mcp/manager.go index e28388827..92ea426a6 100644 --- a/pkg/mcp/manager.go +++ b/pkg/mcp/manager.go @@ -117,10 +117,12 @@ func loadEnvFile(path string) (map[string]string, error) { // ServerConnection represents a connection to an MCP server type ServerConnection struct { - Name string - Client *mcp.Client - Session *mcp.ClientSession - Tools []*mcp.Tool + Name string + Config config.MCPServerConfig + Client *mcp.Client + Session *mcp.ClientSession + Tools []*mcp.Tool + reconnectMu sync.Mutex } // Manager manages multiple MCP server connections @@ -131,6 +133,8 @@ type Manager struct { wg sync.WaitGroup // tracks in-flight CallTool calls } +var connectServerFunc = connectServer + // NewManager creates a new MCP manager func NewManager() *Manager { return &Manager{ @@ -260,6 +264,28 @@ func (m *Manager) ConnectServer( name string, cfg config.MCPServerConfig, ) error { + conn, err := connectServerFunc(ctx, name, cfg) + if err != nil { + return err + } + + m.mu.Lock() + defer m.mu.Unlock() + + if m.closed.Load() { + _ = conn.Session.Close() + return fmt.Errorf("manager is closed") + } + + m.servers[name] = conn + return nil +} + +func connectServer( + ctx context.Context, + name string, + cfg config.MCPServerConfig, +) (*ServerConnection, error) { logger.InfoCF("mcp", "Connecting to MCP server", map[string]any{ "server": name, @@ -285,14 +311,14 @@ func (m *Manager) ConnectServer( } else if cfg.Command != "" { transportType = "stdio" } else { - return fmt.Errorf("either URL or command must be provided") + return nil, fmt.Errorf("either URL or command must be provided") } } switch transportType { case "sse", "http": if cfg.URL == "" { - return fmt.Errorf("URL is required for SSE/HTTP transport") + return nil, fmt.Errorf("URL is required for SSE/HTTP transport") } // Configure DisableStandaloneSSE based on transport type. @@ -334,7 +360,7 @@ func (m *Manager) ConnectServer( transport = sseTransport case "stdio": if cfg.Command == "" { - return fmt.Errorf("command is required for stdio transport") + return nil, fmt.Errorf("command is required for stdio transport") } logger.DebugCF("mcp", "Using stdio transport", map[string]any{ @@ -359,7 +385,7 @@ func (m *Manager) ConnectServer( if cfg.EnvFile != "" { envVars, err := loadEnvFile(cfg.EnvFile) if err != nil { - return fmt.Errorf("failed to load env file %s: %w", cfg.EnvFile, err) + return nil, fmt.Errorf("failed to load env file %s: %w", cfg.EnvFile, err) } for k, v := range envVars { envMap[k] = v @@ -385,7 +411,7 @@ func (m *Manager) ConnectServer( cmd.Env = env transport = &isolatedCommandTransport{Command: cmd} default: - return fmt.Errorf( + return nil, fmt.Errorf( "unsupported transport type: %s (supported: stdio, sse, http)", transportType, ) @@ -394,7 +420,7 @@ func (m *Manager) ConnectServer( // Connect to server session, err := client.Connect(ctx, transport, nil) if err != nil { - return fmt.Errorf("failed to connect: %w", err) + return nil, fmt.Errorf("failed to connect: %w", err) } // Get server info @@ -408,38 +434,19 @@ func (m *Manager) ConnectServer( }) // List available tools if supported - var tools []*mcp.Tool - if initResult.Capabilities.Tools != nil { - for tool, err := range session.Tools(ctx, nil) { - if err != nil { - logger.WarnCF("mcp", "Error listing tool", - map[string]any{ - "server": name, - "error": err.Error(), - }) - continue - } - tools = append(tools, tool) - } - - logger.InfoCF("mcp", "Listed tools from MCP server", - map[string]any{ - "server": name, - "toolCount": len(tools), - }) + tools, err := listServerTools(ctx, name, session, initResult) + if err != nil { + _ = session.Close() + return nil, err } - // Store connection - m.mu.Lock() - m.servers[name] = &ServerConnection{ + return &ServerConnection{ Name: name, + Config: cfg, Client: client, Session: session, Tools: tools, - } - m.mu.Unlock() - - return nil + }, nil } // GetServers returns all connected servers @@ -498,12 +505,131 @@ func (m *Manager) CallTool( result, err := conn.Session.CallTool(ctx, params) if err != nil { + if shouldReconnectCallError(err) { + logger.WarnCF("mcp", "MCP server session was lost during tool call, reconnecting", + map[string]any{ + "server": serverName, + "tool": toolName, + "error": err.Error(), + }) + + reconnectedConn, reconnectErr := m.reconnectServer(ctx, serverName, conn) + if reconnectErr != nil { + return nil, fmt.Errorf("failed to recover lost MCP session: %w", reconnectErr) + } + + result, err = reconnectedConn.Session.CallTool(ctx, params) + if err == nil { + return result, nil + } + } + return nil, fmt.Errorf("failed to call tool: %w", err) } return result, nil } +func listServerTools( + ctx context.Context, + name string, + session *mcp.ClientSession, + initResult *mcp.InitializeResult, +) ([]*mcp.Tool, error) { + var tools []*mcp.Tool + if initResult.Capabilities.Tools == nil { + return tools, nil + } + + for tool, err := range session.Tools(ctx, nil) { + if err != nil { + logger.WarnCF("mcp", "Error listing tool", + map[string]any{ + "server": name, + "error": err.Error(), + }) + continue + } + tools = append(tools, tool) + } + + logger.InfoCF("mcp", "Listed tools from MCP server", + map[string]any{ + "server": name, + "toolCount": len(tools), + }) + + return tools, nil +} + +func shouldReconnectCallError(err error) bool { + if err == nil { + return false + } + if errors.Is(err, mcp.ErrSessionMissing) { + return true + } + return strings.Contains(strings.ToLower(err.Error()), mcp.ErrSessionMissing.Error()) +} + +func (m *Manager) reconnectServer( + ctx context.Context, + serverName string, + staleConn *ServerConnection, +) (*ServerConnection, error) { + if staleConn == nil { + return nil, fmt.Errorf("server %s not found", serverName) + } + + staleConn.reconnectMu.Lock() + defer staleConn.reconnectMu.Unlock() + + if m.closed.Load() { + return nil, fmt.Errorf("manager is closed") + } + + m.mu.RLock() + currentConn, ok := m.servers[serverName] + m.mu.RUnlock() + if !ok { + return nil, fmt.Errorf("server %s not found", serverName) + } + if currentConn != staleConn { + return currentConn, nil + } + + freshConn, err := connectServerFunc(ctx, serverName, staleConn.Config) + if err != nil { + return nil, err + } + + m.mu.Lock() + if m.closed.Load() { + m.mu.Unlock() + _ = freshConn.Session.Close() + return nil, fmt.Errorf("manager is closed") + } + + currentConn, ok = m.servers[serverName] + if !ok { + m.mu.Unlock() + _ = freshConn.Session.Close() + return nil, fmt.Errorf("server %s not found", serverName) + } + + if currentConn == staleConn { + m.servers[serverName] = freshConn + staleToClose := staleConn + m.mu.Unlock() + _ = staleToClose.Session.Close() + return freshConn, nil + } + + m.mu.Unlock() + _ = freshConn.Session.Close() + return currentConn, nil +} + // Close closes all server connections func (m *Manager) Close() error { // Use Swap to atomically set closed=true and get the previous value diff --git a/pkg/mcp/manager_test.go b/pkg/mcp/manager_test.go index fff315655..682d4c346 100644 --- a/pkg/mcp/manager_test.go +++ b/pkg/mcp/manager_test.go @@ -2,11 +2,16 @@ package mcp import ( "context" + "encoding/json" + "fmt" + "io" "os" "path/filepath" "strings" + "sync" "testing" + "github.com/modelcontextprotocol/go-sdk/jsonrpc" sdkmcp "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/sipeed/picoclaw/pkg/config" @@ -312,6 +317,81 @@ func TestCallTool_ErrorsForClosedOrMissingServer(t *testing.T) { }) } +func TestCallTool_ReconnectsWhenHTTPServerLosesSession(t *testing.T) { + originalConnectServerFunc := connectServerFunc + t.Cleanup(func() { + connectServerFunc = originalConnectServerFunc + }) + + staleConn, staleTransport, err := newScriptedServerConnection( + "session-1", + nil, + fmt.Errorf(`sending "tools/call": failed to connect (session ID: session-1): %w`, sdkmcp.ErrSessionMissing), + ) + if err != nil { + t.Fatalf("newScriptedServerConnection(stale) error = %v", err) + } + freshConn, freshTransport, err := newScriptedServerConnection( + "session-2", + &sdkmcp.CallToolResult{ + Content: []sdkmcp.Content{ + &sdkmcp.TextContent{Text: "reconnected"}, + }, + }, + nil, + ) + if err != nil { + t.Fatalf("newScriptedServerConnection(fresh) error = %v", err) + } + + connectCalls := 0 + connectServerFunc = func(ctx context.Context, name string, cfg config.MCPServerConfig) (*ServerConnection, error) { + connectCalls++ + if connectCalls == 1 { + return freshConn, nil + } + return nil, fmt.Errorf("unexpected reconnect attempt %d", connectCalls) + } + + mgr := NewManager() + mgr.servers["flaky"] = staleConn + + result, err := mgr.CallTool(context.Background(), "flaky", "echo", map[string]any{ + "query": "hello", + }) + if err != nil { + t.Fatalf("CallTool() error = %v", err) + } + if result == nil || len(result.Content) != 1 { + t.Fatalf("CallTool() returned unexpected content: %#v", result) + } + + text, ok := result.Content[0].(*sdkmcp.TextContent) + if !ok { + t.Fatalf("CallTool() content type = %T, want *sdkmcp.TextContent", result.Content[0]) + } + if text.Text != "reconnected" { + t.Fatalf("CallTool() text = %q, want %q", text.Text, "reconnected") + } + + conn, ok := mgr.GetServer("flaky") + if !ok { + t.Fatal("expected flaky server to remain connected after reconnect") + } + if conn.Session.ID() != "session-2" { + t.Fatalf("Session.ID() = %q, want %q", conn.Session.ID(), "session-2") + } + if connectCalls != 1 { + t.Fatalf("connectCalls = %d, want 1", connectCalls) + } + if staleTransport.toolCallCalls != 1 { + t.Fatalf("stale toolCallCalls = %d, want 1", staleTransport.toolCallCalls) + } + if freshTransport.toolCallCalls != 1 { + t.Fatalf("fresh toolCallCalls = %d, want 1", freshTransport.toolCallCalls) + } +} + func TestClose_IdempotentOnEmptyManager(t *testing.T) { mgr := NewManager() @@ -322,3 +402,138 @@ func TestClose_IdempotentOnEmptyManager(t *testing.T) { t.Fatalf("second close should be idempotent, got: %v", err) } } + +func newScriptedServerConnection( + sessionID string, + toolCallResult *sdkmcp.CallToolResult, + toolCallErr error, +) (*ServerConnection, *scriptedTransport, error) { + transport := &scriptedTransport{ + sessionID: sessionID, + toolCallResult: toolCallResult, + toolCallErr: toolCallErr, + } + + client := sdkmcp.NewClient(&sdkmcp.Implementation{ + Name: "picoclaw-test", + Version: "1.0.0", + }, nil) + session, err := client.Connect(context.Background(), transport, nil) + if err != nil { + return nil, nil, err + } + + return &ServerConnection{ + Name: "flaky", + Config: config.MCPServerConfig{Enabled: true, Type: "http", URL: "https://example.invalid/mcp"}, + Client: client, + Session: session, + Tools: []*sdkmcp.Tool{ + { + Name: "echo", + Description: "Echo test tool", + InputSchema: map[string]any{"type": "object"}, + }, + }, + }, transport, nil +} + +type scriptedTransport struct { + sessionID string + toolCallResult *sdkmcp.CallToolResult + toolCallErr error + + mu sync.Mutex + toolCallCalls int + closed bool + incoming chan jsonrpc.Message +} + +func (t *scriptedTransport) Connect(context.Context) (sdkmcp.Connection, error) { + if t.incoming == nil { + t.incoming = make(chan jsonrpc.Message, 4) + } + return t, nil +} + +func (t *scriptedTransport) Read(ctx context.Context) (jsonrpc.Message, error) { + select { + case <-ctx.Done(): + return nil, ctx.Err() + case msg, ok := <-t.incoming: + if !ok { + return nil, io.EOF + } + return msg, nil + } +} + +func (t *scriptedTransport) Write(ctx context.Context, msg jsonrpc.Message) error { + req, ok := msg.(*jsonrpc.Request) + if !ok { + return nil + } + + switch req.Method { + case "initialize": + payload, err := json.Marshal(&sdkmcp.InitializeResult{ + ProtocolVersion: "2025-11-25", + ServerInfo: &sdkmcp.Implementation{ + Name: "scripted-test-server", + Version: "1.0.0", + }, + Capabilities: &sdkmcp.ServerCapabilities{ + Tools: &sdkmcp.ToolCapabilities{}, + }, + }) + if err != nil { + return err + } + select { + case <-ctx.Done(): + return ctx.Err() + case t.incoming <- &jsonrpc.Response{ID: req.ID, Result: payload}: + return nil + } + + case "notifications/initialized": + return nil + + case "tools/call": + t.mu.Lock() + t.toolCallCalls++ + t.mu.Unlock() + + if t.toolCallErr != nil { + return t.toolCallErr + } + + payload, err := json.Marshal(t.toolCallResult) + if err != nil { + return err + } + select { + case <-ctx.Done(): + return ctx.Err() + case t.incoming <- &jsonrpc.Response{ID: req.ID, Result: payload}: + return nil + } + } + + return fmt.Errorf("unexpected method %q", req.Method) +} + +func (t *scriptedTransport) Close() error { + t.mu.Lock() + defer t.mu.Unlock() + if t.closed { + return nil + } + t.closed = true + close(t.incoming) + return nil +} + +func (t *scriptedTransport) SessionID() string { + return t.sessionID +} From 9d8f0dc877024a56646a0e9f5b98ddaa80dbe948 Mon Sep 17 00:00:00 2001 From: afjcjsbx Date: Fri, 24 Apr 2026 21:24:29 +0200 Subject: [PATCH 33/36] fix(mcp): send empty object instead of null for tool --- pkg/agent/hooks.go | 2 +- pkg/agent/hooks_test.go | 50 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/pkg/agent/hooks.go b/pkg/agent/hooks.go index 687e54532..6ef796543 100644 --- a/pkg/agent/hooks.go +++ b/pkg/agent/hooks.go @@ -788,7 +788,7 @@ func cloneLLMResponse(resp *providers.LLMResponse) *providers.LLMResponse { func cloneStringAnyMap(src map[string]any) map[string]any { if len(src) == 0 { - return nil + return map[string]any{} } cloned := make(map[string]any, len(src)) diff --git a/pkg/agent/hooks_test.go b/pkg/agent/hooks_test.go index 1cfa341a7..e479fbff9 100644 --- a/pkg/agent/hooks_test.go +++ b/pkg/agent/hooks_test.go @@ -1168,6 +1168,56 @@ func TestAgentLoop_HookRespond_SteeringSkipsRemaining(t *testing.T) { } } +func TestCloneStringAnyMap_EmptyMapReturnsNonNil(t *testing.T) { + tests := []struct { + name string + input map[string]any + wantNil bool + wantLen int + }{ + { + name: "nil input returns empty map", + input: nil, + wantNil: false, + wantLen: 0, + }, + { + name: "empty map returns empty map", + input: map[string]any{}, + wantNil: false, + wantLen: 0, + }, + { + name: "populated map is cloned", + input: map[string]any{"key": "value"}, + wantNil: false, + wantLen: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := cloneStringAnyMap(tt.input) + if result == nil { + t.Fatal("cloneStringAnyMap returned nil — MCP tool calls " + + "with no arguments would send null instead of {}") + } + if len(result) != tt.wantLen { + t.Fatalf("expected len %d, got %d", tt.wantLen, len(result)) + } + }) + } + + t.Run("clone does not share underlying map", func(t *testing.T) { + src := map[string]any{"a": 1} + cloned := cloneStringAnyMap(src) + cloned["b"] = 2 + if _, ok := src["b"]; ok { + t.Fatal("modifying clone should not affect source") + } + }) +} + func filterEvents(events []Event, kind EventKind) []Event { var result []Event for _, evt := range events { From ae162a72b1dab1f2990d9c87927acbb9578c7970 Mon Sep 17 00:00:00 2001 From: lc6464 <64722907+lc6464@users.noreply.github.com> Date: Sat, 25 Apr 2026 12:25:09 +0800 Subject: [PATCH 34/36] fix(message): ignore transient assistant thoughts in message count and history truncation --- pkg/memory/jsonl.go | 76 ++++++++++++++++++++------------- pkg/memory/jsonl_test.go | 51 ++++++++++++++++++++++ web/backend/api/session.go | 3 ++ web/backend/api/session_test.go | 59 +++++++++++++++++++++++++ 4 files changed, 159 insertions(+), 30 deletions(-) diff --git a/pkg/memory/jsonl.go b/pkg/memory/jsonl.go index 072795bc9..492205114 100644 --- a/pkg/memory/jsonl.go +++ b/pkg/memory/jsonl.go @@ -10,6 +10,7 @@ import ( "log" "os" "path/filepath" + "sort" "strings" "sync" "time" @@ -406,12 +407,9 @@ func (s *JSONLStore) promoteAliasHistoryLocked( } func (s *JSONLStore) sessionHasVisibleContentLocked(sessionKey string, meta SessionMeta) (bool, error) { - if meta.Count-meta.Skip > 0 || strings.TrimSpace(meta.Summary) != "" { + if strings.TrimSpace(meta.Summary) != "" { return true, nil } - if meta.Count != 0 || meta.Skip != 0 { - return false, nil - } history, err := readMessages(s.jsonlPath(sessionKey), meta.Skip) if err != nil { return false, err @@ -498,28 +496,44 @@ func readMessages(path string, skip int) ([]providers.Message, error) { return msgs, nil } -// countLines counts the total number of non-empty lines in a .jsonl file. -// Used by TruncateHistory to reconcile a stale meta.Count without -// the overhead of unmarshaling every message. -func countLines(path string) (int, error) { +// scanRetainedMessageLines returns the total number of non-empty raw JSONL +// lines plus the raw line numbers that survive readMessages filtering. +// TruncateHistory uses this to compute keepLast against retained messages +// while preserving the raw-line skip offset stored in metadata. +func scanRetainedMessageLines(path string) (int, []int, error) { f, err := os.Open(path) if os.IsNotExist(err) { - return 0, nil + return 0, []int{}, nil } if err != nil { - return 0, fmt.Errorf("memory: open jsonl: %w", err) + return 0, nil, fmt.Errorf("memory: open jsonl: %w", err) } defer f.Close() - n := 0 + rawCount := 0 + retained := make([]int, 0) scanner := bufio.NewScanner(f) scanner.Buffer(make([]byte, 0, 64*1024), maxLineSize) for scanner.Scan() { - if len(scanner.Bytes()) > 0 { - n++ + line := scanner.Bytes() + if len(line) == 0 { + continue } + rawCount++ + + var msg providers.Message + if err := json.Unmarshal(line, &msg); err != nil { + continue + } + if messageutil.IsTransientAssistantThoughtMessage(msg) { + continue + } + retained = append(retained, rawCount) } - return n, scanner.Err() + if err := scanner.Err(); err != nil { + return 0, nil, err + } + return rawCount, retained, nil } func (s *JSONLStore) AddMessage( @@ -663,24 +677,26 @@ func (s *JSONLStore) TruncateHistory( return err } - // Always reconcile meta.Count with the actual line count on disk. - // A crash between the JSONL append and the meta update in addMsg - // leaves meta.Count stale (e.g. file has 101 lines but meta says - // 100). Counting lines is cheap — no unmarshal, just a scan — and - // TruncateHistory is not a hot path, so always re-count. - n, countErr := countLines(s.jsonlPath(sessionKey)) - if countErr != nil { - return countErr + rawCount, retainedRawLines, scanErr := scanRetainedMessageLines(s.jsonlPath(sessionKey)) + if scanErr != nil { + return scanErr } - meta.Count = n - - if keepLast <= 0 { + meta.Count = rawCount + if meta.Skip > meta.Count { meta.Skip = meta.Count - } else { - effective := meta.Count - meta.Skip - if keepLast < effective { - meta.Skip = meta.Count - keepLast - } + } + + activeStart := sort.Search(len(retainedRawLines), func(i int) bool { + return retainedRawLines[i] > meta.Skip + }) + activeRetainedCount := len(retainedRawLines) - activeStart + + switch { + case keepLast <= 0 || activeRetainedCount == 0: + meta.Skip = meta.Count + case keepLast < activeRetainedCount: + activeRawLines := retainedRawLines[activeStart:] + meta.Skip = activeRawLines[activeRetainedCount-keepLast-1] } meta.UpdatedAt = time.Now() diff --git a/pkg/memory/jsonl_test.go b/pkg/memory/jsonl_test.go index f72b84804..3a7b98130 100644 --- a/pkg/memory/jsonl_test.go +++ b/pkg/memory/jsonl_test.go @@ -9,6 +9,7 @@ import ( "strings" "sync" "testing" + "time" "github.com/sipeed/picoclaw/pkg/providers" ) @@ -795,6 +796,56 @@ func TestTruncateHistory_StaleMetaCount(t *testing.T) { } } +func TestTruncateHistory_IgnoresTransientThoughtForKeepLast(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + sessionKey := "transient-keep-last" + now := time.Now() + + rawJSONL := strings.Join([]string{ + `{"role":"user","content":"a"}`, + `{"role":"assistant","content":"b"}`, + `{"role":"assistant","content":"","reasoning_content":"dangling thought"}`, + `{"role":"user","content":"c"}`, + `{"role":"assistant","content":"d"}`, + }, "\n") + "\n" + if err := os.WriteFile(store.jsonlPath(sessionKey), []byte(rawJSONL), 0o644); err != nil { + t.Fatalf("WriteFile(jsonl): %v", err) + } + if err := store.writeMeta(sessionKey, SessionMeta{ + Key: sessionKey, + Count: 5, + Skip: 0, + CreatedAt: now, + UpdatedAt: now, + }); err != nil { + t.Fatalf("writeMeta: %v", err) + } + + if err := store.TruncateHistory(ctx, sessionKey, 2); err != nil { + t.Fatalf("TruncateHistory: %v", err) + } + + history, err := store.GetHistory(ctx, sessionKey) + if err != nil { + t.Fatalf("GetHistory: %v", err) + } + if len(history) != 2 { + t.Fatalf("expected 2 retained messages, got %d", len(history)) + } + if history[0].Content != "c" || history[1].Content != "d" { + t.Fatalf("kept history = %+v, want c,d", history) + } + + meta, err := store.readMeta(sessionKey) + if err != nil { + t.Fatalf("readMeta: %v", err) + } + if meta.Skip != 2 { + t.Fatalf("meta.Skip = %d, want 2 raw lines skipped", meta.Skip) + } +} + func TestCrashRecovery_PartialLine(t *testing.T) { store := newTestStore(t) ctx := context.Background() diff --git a/web/backend/api/session.go b/web/backend/api/session.go index 7bfe43026..287892cd2 100644 --- a/web/backend/api/session.go +++ b/web/backend/api/session.go @@ -155,6 +155,9 @@ func (h *Handler) readSessionMessages(path string, skip int) ([]providers.Messag if err := json.Unmarshal(line, &msg); err != nil { continue } + if messageutil.IsTransientAssistantThoughtMessage(msg) { + continue + } msgs = append(msgs, msg) } if err := scanner.Err(); err != nil { diff --git a/web/backend/api/session_test.go b/web/backend/api/session_test.go index f8fb2483a..8d92760d2 100644 --- a/web/backend/api/session_test.go +++ b/web/backend/api/session_test.go @@ -8,6 +8,7 @@ import ( "path/filepath" "strings" "testing" + "time" "github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/memory" @@ -101,6 +102,64 @@ func TestHandleListSessions_JSONLStorage(t *testing.T) { } } +func TestHandleListSessions_TransientThoughtDoesNotInflateMessageCount(t *testing.T) { + configPath, cleanup := setupOAuthTestEnv(t) + defer cleanup() + + dir := sessionsTestDir(t, configPath) + sessionKey := legacyPicoSessionPrefix + "history-jsonl-transient" + base := filepath.Join(dir, sanitizeSessionKey(sessionKey)) + now := time.Now().UTC() + + rawJSONL := strings.Join([]string{ + `{"role":"user","content":"keep me"}`, + `{"role":"assistant","content":"","reasoning_content":"dangling thought"}`, + `{"role":"assistant","content":"and me"}`, + }, "\n") + "\n" + if err := os.WriteFile(base+".jsonl", []byte(rawJSONL), 0o644); err != nil { + t.Fatalf("WriteFile(jsonl) error = %v", err) + } + metaData, err := json.Marshal(memory.SessionMeta{ + Key: sessionKey, + Count: 3, + Skip: 0, + CreatedAt: now, + UpdatedAt: now, + }) + if err != nil { + t.Fatalf("Marshal(meta) error = %v", err) + } + if err := os.WriteFile(base+".meta.json", metaData, 0o644); err != nil { + t.Fatalf("WriteFile(meta) error = %v", err) + } + + h := NewHandler(configPath) + mux := http.NewServeMux() + h.RegisterRoutes(mux) + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/sessions", nil) + mux.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String()) + } + + var items []sessionListItem + if err := json.Unmarshal(rec.Body.Bytes(), &items); err != nil { + t.Fatalf("Unmarshal() error = %v", err) + } + if len(items) != 1 { + t.Fatalf("len(items) = %d, want 1", len(items)) + } + if items[0].ID != "history-jsonl-transient" { + t.Fatalf("items[0].ID = %q, want %q", items[0].ID, "history-jsonl-transient") + } + if items[0].MessageCount != 2 { + t.Fatalf("items[0].MessageCount = %d, want 2 after dropping transient thought", items[0].MessageCount) + } +} + func TestHandleListSessions_TitleUsesFirstUserMessage(t *testing.T) { configPath, cleanup := setupOAuthTestEnv(t) defer cleanup() From 78e4e59ac30df05747e1a8095837f2dde2819a05 Mon Sep 17 00:00:00 2001 From: BeaconCat <111232138+BeaconCat@users.noreply.github.com> Date: Sat, 25 Apr 2026 13:15:37 +0800 Subject: [PATCH 35/36] chore: update WeChat group QR code (#2667) Co-authored-by: BeaconCat --- assets/wechat.png | Bin 365055 -> 369090 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/assets/wechat.png b/assets/wechat.png index c4128854704b5ae92a296cf2c0cb61b5323a57e4..b368f75d3a837d2fee35d01eac6384f2797723df 100644 GIT binary patch literal 369090 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

8kF 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

~3&>~>-7gE4svWECD4%v%AUk+hx)uFvV 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

l5u~HeZ$E(uPoaFJvIeSGAf|a9 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