Compare commits

..

2154 Commits

Author SHA1 Message Date
dependabot[bot] 168bcc6cb3 build(deps): bump github.com/github/copilot-sdk/go from 0.2.0 to 1.0.1
Bumps [github.com/github/copilot-sdk/go](https://github.com/github/copilot-sdk) from 0.2.0 to 1.0.1.
- [Release notes](https://github.com/github/copilot-sdk/releases)
- [Changelog](https://github.com/github/copilot-sdk/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/copilot-sdk/compare/v0.2.0...v1.0.1)

---
updated-dependencies:
- dependency-name: github.com/github/copilot-sdk/go
  dependency-version: 1.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-11 17:17:00 +00:00
Mauro fa6ed714c4 Merge pull request #3060 from chengzhichao-xydt/codex/error-wrap-and-marshal
fix: use %w for error wrapping and handle json.MarshalIndent error
2026-06-11 18:36:55 +02:00
Mauro f8472d6f27 Merge pull request #3067 from SiYue-ZO/fix/session-dm-scope-save
fix: add DmScope field to SessionConfig to persist dm_scope setting
2026-06-11 18:34:02 +02:00
Mauro 44fdf9a20b Merge pull request #3087 from jp39/fix/exec-relative-workspace-paths
fix(tools): allow workspace relative exec paths
2026-06-11 18:33:13 +02:00
jp39 17e4720203 fix(tools): allow workspace relative exec paths 2026-06-11 16:38:16 +02:00
SiYue-ZO ef002d9a5d fix: ensure dm_scope and dimensions stay in sync across all config paths
The reviewer identified two bugs in the original PR:

1. PATCH /api/config leaves session.dimensions stale: LoadConfig()
   derives dimensions from the old dm_scope, and the merge carries
   those stale dimensions forward. ApplyDmScope() then exits early
   because dimensions is already populated, causing a mismatch between
   dm_scope (new) and dimensions (old).

2. Legacy/default configs omit dm_scope in GET response: configs with
   explicit dimensions but no dm_scope (including DefaultConfig) return
   no dm_scope field, causing the frontend to fall back to its default
   ('per-channel-peer'), which may not match the actual dimensions.

Fix:
- Add DeriveDmScope() to reverse-map known dimensions arrays to
  dm_scope when dm_scope is empty.
- Call it in LoadConfig(), PUT handler, PATCH handler, and
  ResetToDefaults() for consistent normalization.
- In PATCH handler, clear stale dimensions from the merge result when
  the patch contains session.dm_scope but not session.dimensions,
  allowing ApplyDmScope() to re-derive from the new scope.
- Add comprehensive unit tests for DeriveDmScope() and scope
  transition scenarios.
2026-06-11 16:12:48 +08:00
lc6464 017601354b fix(web): harden trusted proxy client IP parsing 2026-06-11 15:16:05 +08:00
lc6464 52ab6c4694 feat(web): harden launcher access control 2026-06-11 15:16:05 +08:00
Mauro f8462855d8 Merge pull request #3095 from chengzhichao-xydt/codex/new-pr
fix(utils): add ok checks for http.Transport type assertions in CreateHTTPClient
2026-06-11 09:13:29 +02:00
肆月 2861fd90ab fix(launcher): hide console flashes in all Windows child processes (#3061)
* fix(launcher): hide console flashes in all Windows child processes

PR #2654 only applied HideWindow to child processes in gateway.go (powershell, tasklist, ps). Several other files still use exec.Command directly, causing visible console windows on Windows.

- startup.go: reg query/add/delete for autostart registry

- version.go: picoclaw version subcommand

- runtime.go: rundll32 for browser launch

- onboard.go: picoclaw onboard subcommand

Add launcherExecCommand to the utils package (matching the api package pattern) and replace all bare exec.Command calls on Windows paths.

* refactor: consolidate launcherExecCommand into utils package

Export LauncherExecCommand and ApplyLauncherProcAttrs from the utils
package as the single source of truth. The api package now imports
and delegates to these exported functions, eliminating code duplication.

Addresses review feedback from imguoguo on PR #3061.
2026-06-11 15:10:56 +08:00
LC 40fe1b0a2d fix(gitignore): normalize text encoding (#3084) 2026-06-11 15:06:12 +08:00
程智超0668000959 9955155389 fix(utils): add ok checks for http.Transport type assertions in CreateHTTPClient 2026-06-11 09:45:14 +08:00
Mauro d955d5bbf3 Merge pull request #3089 from cs8425/fix-win-os-root-api
fix os.Root api on windows issue
2026-06-11 00:42:08 +02:00
Mauro 2efbe5d560 Merge pull request #3085 from ACMYuechen/fix/tools-ssrf-198.18
fix(tools): block 198.18.0.0/15 in SSRF guard
2026-06-11 00:16:24 +02:00
Mauro bc6179917c Merge pull request #3043 from chengzhichao-xydt/codex/atoi-gateway-json
fix: check strconv.Atoi and json.Unmarshal errors
2026-06-10 23:58:23 +02:00
程智超0668000959 8a2c67fe70 fix: check strconv.Atoi and json.Unmarshal errors
short_retrieval.go: Check Atoi error even though regex ensures numeric input. gateway.go: Log warning when gateway config JSON is malformed instead of silently using defaults.
2026-06-10 14:03:53 +08:00
cs8425 355e83e07f fix os.Root api on windows issue 2026-06-10 12:31:35 +08:00
Mauro b9a8fad6fa Merge pull request #3064 from chengzhichao-xydt/codex/migration-model-name-type-assert
fix(config): add ok check for type assertion in migration model name indexing
2026-06-09 21:18:53 +02:00
Guoguo 0ab6924978 docs: update wechat qrcode (#3086)
Signed-off-by: Guoguo <i@qwq.trade>
2026-06-09 20:34:10 +08:00
Yue_chen 2ecdb893d5 fix(tools): block 198.18.0.0/15 in SSRF guard
RFC 2544 benchmark addresses (198.18.0.0/15) are not globally routable
but were missing from the isPrivateOrRestrictedIP blocklist, allowing
SSRF bypasses via literal IPv4.

Fixes #3077
2026-06-09 19:01:42 +08:00
SiYue-ZO 921d753cc0 fix: wire dm_scope into runtime session isolation dimensions
The dm_scope field was stored in config but never translated into the
dimensions array that the routing layer actually consumes. This meant
changing the session isolation scope in the UI had no effect at runtime.

Add ApplyDmScope() to SessionConfig which maps the user-facing dm_scope
values (per-channel-peer, per-channel, per-peer, global) to the
corresponding dimension arrays. Call it in LoadConfig post-processing
and in both the PATCH and PUT API handlers.

Includes table-driven tests covering all dm_scope values and the
precedence rule (explicit dimensions > derived from dm_scope).
2026-06-09 11:03:33 +08:00
SiYue-ZO 0bbd8f081e fix: add DmScope field to SessionConfig to persist dm_scope setting
The frontend sends dm_scope as part of the session config, but the
backend SessionConfig struct lacked the corresponding field. Go's
encoding/json silently discards unknown fields, so the value was lost
on every PATCH request. Additionally, MarshalJSON only emitted the
session block when Dimensions or IdentityLinks were set, so even a
stored dm_scope would not appear in GET responses.

- Add DmScope string field with json tag 'dm_scope' to SessionConfig
- Update MarshalJSON condition to include session when DmScope is set
2026-06-09 10:48:17 +08:00
程智超0668000959 fc90a5af23 fix(config): add ok check for type assertion in migration model name indexing 2026-06-09 09:41:14 +08:00
程智超0668000959 e2112e627c fix: use %w for error wrapping and handle json.MarshalIndent error 2026-06-09 09:04:56 +08:00
Mauro 46b29a0ae9 Merge pull request #3062 from trufae/health-ready
fix: health check always returning not ready
2026-06-08 19:04:09 +02:00
Mauro 13bf650807 Merge pull request #3058 from chengzhichao-xydt/codex/webfetch-allowed-host-type-assert
fix(webfetch): add ok check for type assertion in isAllowedFirstHopHost
2026-06-08 18:51:42 +02:00
Mauro 0f86d9aacb Merge pull request #3057 from chengzhichao-xydt/codex/subagent-spawn-type-assertions
fix(tools): add ok checks for type assertions in subagent and spawn tools
2026-06-08 18:51:14 +02:00
Mauro c215a4caaf Merge pull request #3056 from chengzhichao-xydt/codex/base-tool-type-assertions
fix(tools): add ok checks for context value type assertions in base.go
2026-06-08 18:50:27 +02:00
Mauro 5b9f9c85a9 Merge pull request #3055 from chengzhichao-xydt/codex/context-getwd-error
fix(agent): handle os.Getwd error in NewContextBuilder
2026-06-08 18:48:09 +02:00
Mauro b40a1d92cc Merge pull request #3052 from wzg-gie/fix/telegram-location-message
fix: handle Telegram location messages
2026-06-08 18:40:54 +02:00
pancake fac5603daf fix: health check always returning not ready 2026-06-08 12:14:06 +02:00
程智超0668000959 a4e8fe953e fix(webfetch): add ok check for type assertion in isAllowedFirstHopHost 2026-06-08 17:25:44 +08:00
程智超0668000959 77017eb57d fix(tools): add ok checks for type assertions in subagent and spawn tools 2026-06-08 17:25:19 +08:00
程智超0668000959 92a647bfcf fix(tools): add ok checks for context value type assertions in base.go 2026-06-08 17:24:50 +08:00
程智超0668000959 8a246c2282 fix(agent): handle os.Getwd error in NewContextBuilder without behavior regression 2026-06-08 16:52:00 +08:00
2023478 3bba6338ca fix: handle Telegram location messages 2026-06-08 15:53:42 +08:00
Mauro 12c36572a5 Merge pull request #3051 from chengzhichao-xydt/codex/error-wrap-percent-w
fix: use %w instead of %v for error wrapping in channels and mcp
2026-06-08 09:14:02 +02:00
Mauro 890780b924 Merge pull request #3050 from chengzhichao-xydt/codex/use-logger-for-warnings
refactor: replace log.Printf/fmt.Printf with structured logger
2026-06-08 09:12:35 +02:00
程智超0668000959 1ab442b12c refactor: replace log.Printf/fmt.Printf with structured logger
Replace raw log.Printf and fmt.Printf calls in pkg/state, pkg/agent, and pkg/tools with structured logger calls (WarnCF/InfoCF). This ensures warnings and info messages are routed through the configured logging infrastructure instead of raw stderr/stdout.
2026-06-08 09:18:02 +08:00
程智超0668000959 3f435c5e56 fix: use %w instead of %v for error wrapping
errutil.go: Change %v to %w in ClassifySendError and ClassifyNetError so callers can use errors.Is/errors.As on the underlying HTTP/network error.

isolated_command_transport.go: Change %v to %w in Close() and Write() error paths for the same reason.
2026-06-08 09:10:14 +08:00
Mauro 875cf4a2d4 Merge pull request #3042 from chengzhichao-xydt/codex/evolution-getwd-errors
fix: handle os.Getwd() error in evolution skills_recall and drafts
2026-06-08 00:13:32 +02:00
Mauro 5e7b84f429 Merge pull request #3046 from chengzhichao-xydt/codex/startup-info-type-assert
fix(agent): add ok checks for startup info type assertions
2026-06-08 00:11:03 +02:00
Mauro 1b3e887fc6 Merge pull request #3037 from jp39/kagi-native-web-search
Add native Kagi web search provider
2026-06-07 22:51:35 +02:00
Mauro d627dc8b57 Merge pull request #2902 from puneetdixit200/docs/termux-android-guide
docs: add Android Termux guide
2026-06-07 22:11:48 +02:00
jp39 0a3a7881c6 Add native Kagi web search provider 2026-06-07 16:27:50 +02:00
程智超0668000959 639f700c15 fix(agent): add ok checks for startup info type assertions
GetStartupInfo returns map[string]any, and type-asserting tools/skills entries without checking ok is fragile. While the current implementation always stores the correct types, a future refactor could cause silent nil dereference. Add ok checks with explicit nil fallback.
2026-06-07 21:28:45 +08:00
程智超0668000959 cbb684be01 fix: handle os.Getwd error in evolution skills_recall and drafts
When os.Getwd fails, wd is empty and builtinSkillsDir resolves to relative path, causing confusing downstream errors. Fall back to config.GetHome on error.
2026-06-07 21:05:16 +08:00
Mauro 67eaa984c7 Merge pull request #3040 from chengzhichao-xydt/codex/model-status-type-assert
fix: add ok check for singleflight type assertion in model probe
2026-06-07 15:03:45 +02:00
Mauro ebb04abb38 Merge pull request #3034 from chengzhichao-xydt/codex/feishu-resource-close-error
fix: check Close() error on feishu resource download
2026-06-07 14:58:21 +02:00
程智超0668000959 a011df1ddc fix: add ok check for singleflight type assertion in model probe
singleflight.Group.Do() returns any, which is type-asserted as bool
without an ok check at model_status.go:211. If a non-bool value is
returned (e.g. nil from shared/cache corruption), this panics.

Add ok check and return false (model probe failed) as a safe default.
2026-06-07 20:54:00 +08:00
程智超0668000959 f037a112b2 fix: avoid err shadow in feishu close check
Use distinct variable names (writeErr, closeErr) to avoid
shadowing the outer err, so a deferred close failure is
still captured.
2026-06-07 20:35:57 +08:00
Mauro 10115f941c Merge pull request #3035 from chengzhichao-xydt/codex/file-copy-close-errors
fix: check Close() error after io.Copy to writable files
2026-06-07 14:06:57 +02:00
Mauro db13367404 Merge pull request #3036 from SutraHsing/codex/2941-claude-sonnet-model-id
fix(config): use canonical Anthropic default model ID
2026-06-07 14:02:25 +02:00
Mauro 7c18fe8421 Merge pull request #3033 from chengzhichao-xydt/codex/media-close-errors
fix: check Close() error after downloading media file
2026-06-07 14:00:34 +02:00
Sutra Hsing 007b2ae8bd fix(config): use canonical Anthropic default model ID 2026-06-07 15:43:26 +08:00
程智超0668000959 2d1fb953fc fix: check Close() error after io.Copy to writable files 2026-06-07 12:09:01 +08:00
程智超0668000959 b1d727ebaf fix: check Close() error on feishu resource download 2026-06-07 11:57:10 +08:00
程智超0668000959 f7be21bb11 fix: check Close() error after downloading media file 2026-06-07 11:53:38 +08:00
Mauro 7d2b0c2a4d Merge pull request #3021 from chengzhichao-xydt/codex/safe-startup-info
fix: safe startup info map access to prevent panic on nil agent
2026-06-06 16:21:46 +02:00
Mauro c19e4e8db1 Merge pull request #3022 from chengzhichao-xydt/codex/sync-map-assertions
fix: add ok checks for sync.Map LoadAndDelete/Load type assertions
2026-06-06 16:20:28 +02:00
Mauro ebf17aa152 Merge pull request #3023 from chengzhichao-xydt/codex/updater-close-errors
fix: check Close() errors in updater extraction functions
2026-06-06 16:12:41 +02:00
程智超0668000959 4290aa8b5b fix: check Close() errors in updater extraction functions 2026-06-06 21:34:24 +08:00
程智超0668000959 5f0d368995 fix: add ok checks for sync.Map LoadAndDelete/Load type assertions 2026-06-06 21:17:07 +08:00
程智超0668000959 ddabaa69a4 fix: safe startup info map access to prevent panic on nil agent 2026-06-06 21:13:22 +08:00
Mauro ff7c92deee Merge pull request #3019 from chengzhichao-xydt/codex/lastinsertid-nilguard
fix: type-switch capture, nil guard, check LastInsertId errors
2026-06-06 12:55:38 +02:00
程智超0668000959 4752a67a7c fix: type-switch capture, nil guard, LastInsertId error check
Three defensive fixes: 1) whatsapp_native - use type-switch capture instead of redundant unchecked assertion 2) config - add nil receiver guard to FilterSensitiveData 3) seahorse/store - check LastInsertId error in 3 locations
2026-06-06 16:32:14 +08:00
Mauro 89ee8f1b39 Merge pull request #2915 from SiYue-ZO/feat/mimo-common-models
feat(providers): add CommonModels for MiMo provider
2026-06-05 20:52:29 +02:00
Mauro b10f9cdf18 Merge pull request #2985 from chengzhichao-xydt/codex/context-show-summarize-threshold
fix(context): show both summarize and compress thresholds in /context
2026-06-05 19:24:54 +02:00
Mauro 0b7aaac2b2 Merge pull request #3009 from chengzhichao-xydt/codex/onebot-group-reply-fix
fix(onebot): use prefixed chatID for group reply routing
2026-06-05 19:14:56 +02:00
Mauro 8e7e910f67 Merge pull request #3010 from chengzhichao-xydt/codex/channel-hash-type-assertions
fix(channels): add ok checks for type assertions in toChannelHashes
2026-06-05 19:12:45 +02:00
Mauro 71524183b6 Merge pull request #3011 from chengzhichao-xydt/codex/legacy-events-ok-assert
fix(agent): add ok check for LoadAndDelete type assertion
2026-06-05 19:12:22 +02:00
程智超0668000959 6c882ec5e7 fix(agent): log warning when LoadAndDelete type assertion fails
Add a warning log when the type assertion from sync.Map.LoadAndDelete fails in UnsubscribeEvents, per review suggestion. This makes a mismatched type observable for debugging.
2026-06-06 00:32:49 +08:00
程智超0668000959 9f246a6482 test(channels): add edge case tests for toChannelHashes type assertions
Add 3 tests covering scenarios that previously panicked: 1) missing enabled key in settings 2) enabled field with non-bool type 3) teams_webhook with webhooks using map[string]any from JSON unmarshal
2026-06-06 00:31:44 +08:00
程智超0668000959 7a7e205cc8 fix(context): expose history tokens and remove leaked state files
Address remaining review feedback: 1) Add HistoryTokens field to ContextUsage/ContextStats, showing history-only token count in /context and frontend UI alongside SummarizeAtTokens so users can see the actual summarization trigger comparison. 2) Remove .codebuddy/github-contribute/ state files accidentally included in the PR.
2026-06-06 00:28:32 +08:00
Mauro 1f2736915e Merge pull request #3013 from shenjiecode/docs/fix-skill-creator-scaffold
docs: remove missing skill-creator helper script references
2026-06-05 13:19:51 +02:00
Jay Shen 12ca46b1ab docs: remove missing skill-creator helper script references 2026-06-05 17:07:06 +08:00
Mauro cc712a1adb Merge pull request #2979 from afjcjsbx/fix/pr-2962
fix: support anthropic-sdk-go v1.46.0 in anthropic provider
2026-06-05 10:04:44 +02:00
Mauro 52e3ea72ba Merge pull request #3001 from chengzhichao-xydt/codex/workspace-guard-schemeless-url
fix(tools): allow scheme-less URLs in workspace guard
2026-06-05 08:54:59 +02:00
程智超0668000959 f0f809db35 fix(agent): add ok check for LoadAndDelete type assertion
sync.Map.LoadAndDelete returns any; unprotected type assertion could panic if an unexpected type were stored. Add ok check to safely handle mismatched types.
2026-06-05 10:12:14 +08:00
程智超0668000959 e5c7772d3c fix(channels): add ok checks for type assertions in toChannelHashes
Two type assertions in toChannelHashes could panic when channel config values had unexpected types from JSON unmarshal: 1) value[enabled].(bool) panics if the key is missing or not a bool 2) vv.(map[string]string) panics when JSON unmarshal produces map[string]any. Add ok checks to safely handle both cases.
2026-06-05 09:49:44 +08:00
程智超0668000959 32ea611f0c fix(onebot): use prefixed chatID for group reply routing
When an incoming group message is received, the inbound context ChatID was set to the raw group number without the group: prefix. This caused the outbound reply to use send_private_msg instead of send_group_msg. Fix by using the prefixed chatID as inbound context ChatID. Closes #3002
2026-06-05 09:37:00 +08:00
程智超0668000959 b6030f054d chore: update contribution state files 2026-06-05 09:28:44 +08:00
程智超0668000959 296a8ae287 fix(context): address review - clarify threshold alignment, i18n strings, add test coverage 2026-06-05 09:27:45 +08:00
程智超0668000959 a6735517d2 test(tools): add unit tests for scheme-less URL workspace guard detection 2026-06-05 09:17:40 +08:00
Meng Zhuo 5224b9a4bc Merge pull request #3008 from afjcjsbx/fix/larksuite-v3.9.4-compat
fix: adapt to larksuite oapi-sdk-go v3.9.4 breaking changes (follow-up to #3005)
2026-06-05 08:34:33 +08:00
Mauro 976ecc68b7 Merge pull request #3000 from chengzhichao-xydt/codex/pid-verify-process-identity
fix(pid): verify process identity in singleton PID check
2026-06-05 00:02:55 +02:00
Mauro dbd76fe541 Merge pull request #2999 from chengzhichao-xydt/codex/makefile-go-version-space
fix: handle space in go env GOVERSION with firstword
2026-06-04 23:39:43 +02:00
Mauro 49e3a03def fix: adapt to larksuite oapi-sdk-go v3.9.4 breaking changes
The SDK renamed ReceiveIdTypeChatId to CreateMessageV1ReceiveIDTypeChatId
in v3.9.4. Update all 5 usages in feishu_64.go and bump the dependency
version.

This fixes the build failure for Dependabot PR #3005.
2026-06-04 23:19:04 +02:00
Mauro d5bd06dc0d Merge pull request #3007 from SebastianBoehler/codex/fix-codex-oauth-stream-tools
fix: preserve streamed Codex tool calls
2026-06-04 21:24:54 +02:00
Mauro d009ba32b7 Merge pull request #3004 from sipeed/dependabot/go_modules/github.com/aws/aws-sdk-go-v2/service/bedrockruntime-1.53.3
build(deps): bump github.com/aws/aws-sdk-go-v2/service/bedrockruntime from 1.50.6 to 1.53.3
2026-06-04 20:12:50 +02:00
Mauro 9b0ab22b3d Merge pull request #3003 from sipeed/dependabot/go_modules/modernc.org/sqlite-1.51.0
build(deps): bump modernc.org/sqlite from 1.50.1 to 1.51.0
2026-06-04 20:12:10 +02:00
SebastianBoehler 3e6abba803 fix: preserve streamed Codex tool calls 2026-06-04 19:27:29 +02:00
dependabot[bot] 79aefc5062 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.6 to 1.53.3.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/ecr/v1.50.6...service/iot/v1.53.3)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2/service/bedrockruntime
  dependency-version: 1.53.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-04 17:15:07 +00:00
dependabot[bot] 9da23e7804 build(deps): bump modernc.org/sqlite from 1.50.1 to 1.51.0
Bumps [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) from 1.50.1 to 1.51.0.
- [Changelog](https://gitlab.com/cznic/sqlite/blob/master/CHANGELOG.md)
- [Commits](https://gitlab.com/cznic/sqlite/compare/v1.50.1...v1.51.0)

---
updated-dependencies:
- dependency-name: modernc.org/sqlite
  dependency-version: 1.51.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-04 17:13:45 +00:00
程智超0668000959 a90d8d35ee fix(pid): verify process identity in singleton PID check
isProcessRunning() previously only checked whether a PID existed via signal(0)/OpenProcess, without confirming the process was actually picoclaw. When the PID was reused by an unrelated process (e.g., systemd-resolved after a kill -9), the gateway would refuse to start with 'already running'.

Add isPicoclawProcess() that verifies the process name matches picoclaw:
- Unix: reads /proc/<pid>/comm
- Windows: calls QueryFullProcessImageNameW

If the running process is not picoclaw, treat the PID file as stale and proceed with normal startup. Falls back to trusting the liveness check when identity verification is unavailable (e.g., /proc unreadable, API call fails).

Fixes #2720.
2026-06-04 20:04:51 +08:00
程智超0668000959 b86ab71836 fix(tools): allow scheme-less URLs in workspace guard
The workspace guard's absolutePathPattern regex matches /Beijing?T in commands like 'curl wttr.in/Beijing'. Since 'wttr.in' is not a recognized web scheme, the path was routed through workspace sandbox validation, which could block legitimate scheme-less URL usage (curl allows bare domains without http://).

Add detection for domain-like tokens preceding /path matches:
- looksLikeDomain: checks for dot-separated tokens that don't end with common file extensions (.py, .go, .exe, etc.)
- localPathExists: verifies the token does not exist as a local filesystem entry

This dual guard prevents the symlink bypass identified in PR #2965 review: if 'foo.bar' exists as a local symlink or directory, the path still undergoes full workspace validation.

Fixes #1042.
2026-06-04 19:59:49 +08:00
Mauro 0ce6e20e08 Merge pull request #2996 from chengzhichao-xydt/codex/handle-json-marshal-errors
fix(tools): handle json.Marshal errors in exec tool responses
2026-06-04 11:50:42 +02:00
程智超0668000959 36ca85ad09 fix: handle space in go env GOVERSION with firstword
go env GOVERSION may return values like go1.25.10 X:nodwarf5 with an embedded space on some toolchain configurations, breaking -ldflags. Use firstword to extract only the first token. Fixes #2976.
2026-06-04 17:44:07 +08:00
程智超0668000959 734f53fb37 fix(tools): handle json.Marshal errors in exec tool responses
Replace 7 instances of ignored json.Marshal errors with proper error handling. Previously, if marshaling an ExecResponse failed, a nil byte slice would be silently converted to an empty string in the LLM response. Now each site returns ErrorResult with the marshal error message.
2026-06-04 17:31:26 +08:00
Mauro 6e9b5071b0 Merge pull request #2995 from chengzhichao-xydt/codex/update-docs-v0.2.9
docs: add v0.2.5~v0.2.9 release highlights to README News
2026-06-04 09:02:01 +02:00
Mauro aa49d066b0 Merge pull request #2992 from chengzhichao-xydt/codex/skip-main-session-alias-promotion
fix(session): skip main-session alias during history promotion
2026-06-04 09:01:26 +02:00
程智超0668000959 5f826f4448 fix(context): show both summarize and compress thresholds in /context
The /context command previously showed only the hard budget compression
threshold (contextWindow - maxTokens), which confused users who expected
to see the soft summarization trigger from summarize_token_percent.

This commit adds SummarizeAtTokens alongside the existing CompressAtTokens
so that both thresholds are visible:

- Compress at: contextWindow - maxTokens (hard budget, triggers proactive
  compression when exceeded)
- Summarize at: contextWindow * summarizeTokenPercent / 100 (soft trigger,
  matches maybeSummarize's threshold)

The fix updates the /context command output, the Web UI popover, and the
pico channel WebSocket payload.

Fixes #2968
2026-06-04 11:03:16 +08:00
程智超0668000959 04664ab514 fix(session): tighten main-session alias detection to exact 3-part format
Only match agent:X:main, not agent:X:direct:main or agent:X:slack:channel:main. Review feedback from afjcjsbx.
2026-06-04 11:01:10 +08:00
程智超0668000959 9c71a44421 fix(session): skip main-session alias during history promotion
The PromoteAliasHistory method previously promoted the first non-empty alias session into a new canonical session. When a user upgraded, the migrated main session contained old messages that were copied into every new Web UI session because agent:main:main is always the first alias.

Add isMainSessionAlias() to detect and skip the main session alias during promotion. Fixes #2972.
2026-06-04 11:01:10 +08:00
程智超0668000959 e1d9a62e0e docs: add v0.2.5~v0.2.9 release highlights to README News
The News section stopped at v0.2.4. Add release highlights for v0.2.5 through v0.2.9 based on GitHub release changelogs.
2026-06-04 10:59:17 +08:00
Meng Zhuo 709c8b2b52 Merge pull request #2997 from afjcjsbx/fix/update-go-1.25.11
fix(deps): bump go from 1.25.10 to 1.25.11 (GO-2026-5039)
2026-06-03 18:43:31 +08:00
Mauro 5d4840c979 fix(deps): bump go from 1.25.10 to 1.25.11 (GO-2026-5039)
net/textproto: header names not escaped in error messages

Affects go < 1.25.11. Fixed in go 1.25.11.
2026-06-03 12:37:54 +02:00
Meng Zhuo a502aa7f83 Merge pull request #2994 from afjcjsbx/feat/picoclaw-agent-skill-expansion
docs(skill): self describing Picoclaw agent skill
2026-06-03 08:59:33 +08:00
afjcjsbx e74ac70cf9 docs(skill): logs detection 2026-06-02 19:35:13 +02:00
afjcjsbx 8dffd6ff03 docs(skill): complete picoclaw-agent skill documentation 2026-06-02 19:29:27 +02:00
afjcjsbx 1903a18235 Merge remote-tracking branch 'upstream/main' 2026-06-02 18:59:12 +02:00
Mauro 004f9346c1 Merge pull request #2991 from chengzhichao-xydt/codex/retry-transient-llm-errors
fix(agent): retry transient LLM HTTP errors using provider error classifier
2026-06-02 18:45:35 +02:00
Mauro 827cd32ffc Merge pull request #2986 from chengzhichao-xydt/codex/session-manager-stop-cleanup
fix(tools): add Stop() to SessionManager to prevent goroutine leak
2026-06-02 18:37:55 +02:00
afjcjsbx 379ab9af2f Merge remote-tracking branch 'upstream/main' 2026-06-02 18:36:38 +02:00
程智超0668000959 e70a9fca7c fix(tools): use sync.Once for thread-safe Stop() in SessionManager
The Stop() method previously used a select/default pattern which was not
safe under concurrent calls — two goroutines could both pass the check
and attempt to close the same channel, causing a panic.

Replace with sync.Once to guarantee exactly-once close semantics,
matching the documented contract of being safe for concurrent use.

Review feedback: afjcjsbx
2026-06-02 20:20:30 +08:00
程智超0668000959 99a7179e76 fix(agent): retry transient LLM HTTP errors using provider error classifier
Previously, only timeout and network errors (matched via string
patterns) were retried. HTTP 500 server errors from
OpenRouter/OpenAI-compatible providers would fail the agent turn
immediately when no model fallback candidate was available.

This commit replaces the separate timeout/network retry branches
with a unified transientLLMRetryReason() helper that:
1. Uses providers.ClassifyError() to detect server_error (HTTP >=500),
   timeout, network, and rate_limit errors
2. Falls back to the existing string-based detection for errors
   not classified by the provider

A regression test (TestPipeline_CallLLM_HTTP5xxRetry) verifies that
HTTP 500 errors are retried and recover successfully.

This is a clean rebase of the approach originally proposed in #2768
by afjcjsbx.
2026-06-02 19:58:09 +08:00
Mauro 7b47872334 Merge pull request #2989 from yuxuan-7814/fix/2943-zhipu-error-1210
fix(providers): add Zhipu API error code 1210 to format error patterns
2026-06-02 12:20:16 +02:00
yuxuan-7814 5927ecc394 fix(providers): add Zhipu API error code 1210 to format error patterns
This fixes issue #2943 where WeChat channel image requests to Zhipu
GLM-5-Turbo vision API failed with error code 1210 (parameter error)
without triggering the fallback mechanism.

Changes:
- Added error code 1210 pattern matching to formatPatterns
- This allows the fallback mechanism to recognize Zhipu API parameter
  errors and fall back to alternative vision models

Closes #2943
2026-06-02 17:26:14 +08:00
程智超0668000959 bb57e0498c fix(tools): add Stop() to SessionManager to prevent goroutine leak
The SessionManager's background cleanup goroutine previously had no
shutdown mechanism. Each call to NewSessionManager() started a ticker
goroutine that ran indefinitely. In tests, where multiple
SessionManagers are created, this caused goroutine leaks.

This commit adds a Stop() method that cleanly shuts down the background
cleanup goroutine via a channel. Stop() is safe to call multiple times.
All existing tests now call t.Cleanup(sm.Stop) to ensure cleanup.
2026-06-02 17:13:31 +08:00
afjcjsbx e42006c10d Merge remote-tracking branch 'upstream/main' 2026-06-01 18:49:57 +02:00
Mauro 426046fca0 Merge pull request #2977 from SutraHsing/cron-get-update
feat(cron): add get and update actions to cron tool
2026-06-01 14:16:50 +02:00
sutra 28eafaeef2 refactor(cron): flatten if-else chains and suppress dupl lint 2026-06-01 20:08:40 +08:00
Mauro 1cfa781925 Merge pull request #2982 from loafoe/fix/bedrock-opus48-temperature
fix(bedrock): drop temperature for models that deprecate it (Opus 4.8)
2026-06-01 13:02:18 +02:00
Andy Lo-A-Foe 5a997a86f0 fix(bedrock): drop temperature for models that deprecate it
Claude Opus 4.8 on Bedrock rejects the temperature inference parameter
with a ValidationException ("temperature is deprecated for this model").
buildConverseParams now takes the model id and omits temperature for
claude-opus-4-8* (matching both bare model ids and region-prefixed
inference profiles), logging when it does so. max_tokens and all other
models are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 08:59:15 +02:00
afjcjsbx 672f86c670 Merge remote-tracking branch 'origin/main' 2026-05-31 23:11:43 +02:00
afjcjsbx 4e3e90df26 Merge remote-tracking branch 'upstream/main' 2026-05-31 23:11:18 +02:00
sutra be13201f02 feat(cron): restrict list/get/update to accessible jobs per channel 2026-05-31 19:20:41 +08:00
afjcjsbx 6e0e2906aa fix(anthropic): support anthropic-sdk-go v1.46.0 2026-05-31 11:54:35 +02:00
afjcjsbx c0f2714b66 Merge remote-tracking branch 'upstream/main' 2026-05-31 11:39:04 +02:00
Mauro ba8065923b Merge pull request #2856 from bogdanovich/feat/message-media-outbound
feat(message): support media attachments and Telegram rich delivery
2026-05-31 11:38:38 +02:00
Mauro 13e1833c81 Merge pull request #2967 from miruchigawa/main
fix(codex): preserve streamed output text deltas
2026-05-31 11:24:20 +02:00
sutra 1d8ef7dcfb feat(cron): add get and update actions to cron tool
Add GetJob and improved UpdateJob to CronService with proper cloning,
schedule diffing, and next-run recomputation. Expose get/update actions
in the cron tool so agents can inspect and partially update jobs without
losing payloads or needing remove+add cycles. Includes access control
for remote channels and command safety gates.
2026-05-31 10:55:54 +08:00
miruchigawa 93391223ea fix: format long line in codex_provider_test.go to satisfy golines 2026-05-31 05:00:22 +07:00
afjcjsbx 41a108c9af Merge remote-tracking branch 'upstream/main' 2026-05-30 20:29:53 +02:00
Mauro 1ce353ba28 Merge pull request #2969 from lc6464/feat/webchat-image-paste-dnd
feat(web): add chat image paste and drag-and-drop upload
2026-05-30 20:22:56 +02:00
dependabot[bot] 4b8761ce6d build(deps): bump github.com/anthropics/anthropic-sdk-go
Bumps [github.com/anthropics/anthropic-sdk-go](https://github.com/anthropics/anthropic-sdk-go) from 1.26.0 to 1.46.0.
- [Release notes](https://github.com/anthropics/anthropic-sdk-go/releases)
- [Changelog](https://github.com/anthropics/anthropic-sdk-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/anthropics/anthropic-sdk-go/compare/v1.26.0...v1.46.0)

---
updated-dependencies:
- dependency-name: github.com/anthropics/anthropic-sdk-go
  dependency-version: 1.46.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-30 17:51:35 +00:00
Mauro 63ba146015 Merge pull request #2974 from kunalk16/feat-i18n-bangla
feat(i18n): Add Bangla support bn-in
2026-05-30 19:47:17 +02:00
Mauro 16c26338b6 Merge pull request #2971 from kunalk16/feat-azureopenai-identity
feat(provider): Add optional Azure Identity support for Azure OpenAI provider
2026-05-30 19:46:32 +02:00
Kunal Karmakar 2391f32fc1 Add Bangla support bn-in 2026-05-30 14:52:54 +00:00
Kunal Karmakar 46e5b59d5f Fix linting 2026-05-30 14:11:39 +00:00
Kunal Karmakar 995005a0ba Add azure entra id support for azure openai provider 2026-05-30 13:40:29 +00:00
lc6464 1edb873ace feat(web): add chat image paste and drag-and-drop upload 2026-05-30 18:21:40 +08:00
miruchigawa 2ff8b01cc6 fix(codex): preserve streamed output text deltas
OpenAI/Codex OAuth streams can return text through response.output_text.delta while the final response.completed payload has response.output set to null. That made PicoClaw report an empty model response even though the backend returned valid content.

Accumulate streamed output_text delta events during the Codex response stream and use them as a fallback when the parsed final response has no content. Add a regression test covering the null final output case from issue #2953.
2026-05-30 10:12:29 +07:00
afjcjsbx e1bada5b94 Merge remote-tracking branch 'upstream/main' 2026-05-29 10:16:05 +02:00
Mauro e81d37108b Merge pull request #2932 from KrtCZ/feat/czech-i18n
feat(i18n): add Czech (cs) locale
2026-05-29 10:14:42 +02:00
Mauro 4e280c5f5e Merge pull request #2961 from sipeed/dependabot/go_modules/github.com/pion/rtp-1.10.2
build(deps): bump github.com/pion/rtp from 1.10.1 to 1.10.2
2026-05-29 10:11:30 +02:00
Mauro 6247f47628 Merge pull request #2960 from sipeed/dependabot/go_modules/github.com/caarlos0/env/v11-11.4.1
build(deps): bump github.com/caarlos0/env/v11 from 11.4.0 to 11.4.1
2026-05-29 10:11:08 +02:00
afjcjsbx 32282beef8 Merge remote-tracking branch 'upstream/main' 2026-05-29 10:03:53 +02:00
Guoguo f9f53e30ee docs: update wechat qrcode (#2966) 2026-05-29 10:33:02 +08:00
dependabot[bot] a34669a2d8 build(deps): bump github.com/pion/rtp from 1.10.1 to 1.10.2
Bumps [github.com/pion/rtp](https://github.com/pion/rtp) from 1.10.1 to 1.10.2.
- [Release notes](https://github.com/pion/rtp/releases)
- [Commits](https://github.com/pion/rtp/compare/v1.10.1...v1.10.2)

---
updated-dependencies:
- dependency-name: github.com/pion/rtp
  dependency-version: 1.10.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-28 19:27:12 +00:00
dependabot[bot] f797172a86 build(deps): bump github.com/caarlos0/env/v11 from 11.4.0 to 11.4.1
Bumps [github.com/caarlos0/env/v11](https://github.com/caarlos0/env) from 11.4.0 to 11.4.1.
- [Release notes](https://github.com/caarlos0/env/releases)
- [Commits](https://github.com/caarlos0/env/compare/v11.4.0...v11.4.1)

---
updated-dependencies:
- dependency-name: github.com/caarlos0/env/v11
  dependency-version: 11.4.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-28 19:27:05 +00:00
afjcjsbx 8e0964be24 Merge remote-tracking branch 'upstream/main' 2026-05-28 19:57:53 +02:00
Mauro 85751492c6 Merge pull request #2950 from yuxuan-7814/fix/2912-add-funding-yml
docs: add FUNDING.yml for GitHub Sponsors
2026-05-28 19:50:38 +02:00
Mauro 0b7e18cd9e Merge pull request #2949 from yuxuan-7814/fix/2944-termux-ssl-cert
fix: auto-detect Termux SSL certificate path
2026-05-28 19:43:18 +02:00
yuxuan-7814 e9e653fb13 docs: add FUNDING.yml for GitHub Sponsors
Add FUNDING.yml file to enable GitHub Sponsors button on the repo.
This makes it easy for users who benefit from PicoClaw to support
the project financially.

Closes #2912
2026-05-26 16:53:33 +08:00
yuxuan-7814 5755b5b323 fix: auto-detect Termux SSL certificate path
When running PicoClaw inside Termux or termux-chroot, HTTPS
requests fail with X509 certificate errors because the Go TLS
stack does not automatically detect the Termux CA bundle path.

This change adds automatic detection of Termux environments and
sets SSL_CERT_FILE to the correct CA bundle path before any
network operations. The detection checks:
- HOME or PATH contains 'com.termux'
- Common CA bundle locations in Termux prefix

Fixes #2944
2026-05-26 16:49:42 +08:00
afjcjsbx 65c09d4270 Merge remote-tracking branch 'upstream/main' 2026-05-26 09:22:23 +02:00
LC 28ec5793a8 feat(web): add line numbers and wrap toggle for code blocks (#2933)
* feat(web): add line numbers and wrap toggle for code blocks

* fix(web): preserve markdown code block copy semantics
2026-05-26 14:57:52 +08:00
Mauro c5a016ccc6 Merge pull request #2946 from lc6464/feat/seahorse-created-at-history
fix(seahorse,session): preserve created_at across history bootstrap
2026-05-26 08:40:52 +02:00
lc6464 9825b4782f fix(seahorse,session): preserve created_at across history bootstrap 2026-05-26 14:05:20 +08:00
afjcjsbx f5f6fdc1f9 Merge remote-tracking branch 'origin/main' 2026-05-25 15:33:21 +02:00
afjcjsbx cfbddcd117 Merge remote-tracking branch 'upstream/main' 2026-05-25 15:32:55 +02:00
afjcjsbx 7be20bf70a Merge remote-tracking branch 'upstream/main' 2026-05-25 13:52:26 +02:00
Mauro ab6d3946a5 Merge pull request #2938 from hschne/fix/cron-command-action
fix(cron): add missing action arg for command job execution
2026-05-24 22:07:18 +02:00
hschne 7af40d49eb fix(cron): add missing 'action' arg for command job execution
CronTool.ExecuteJob was calling ExecTool.Execute without setting
action='run' in the args map. ExecTool.Execute requires the action
field and returns ErrorResult('action is required') immediately when
it's missing. This caused all cron command jobs to silently fail.

Adds a test covering the command execution happy path.
2026-05-24 20:25:06 +02:00
afjcjsbx 239a98e18b Merge remote-tracking branch 'upstream/main' 2026-05-23 17:40:04 +02:00
Mauro d499cbece4 Merge pull request #2931 from hschne/fix/discord-image-download
fix(discord): download attachments for vision pipeline
2026-05-23 17:39:37 +02:00
afjcjsbx d48fa2e2fd Merge remote-tracking branch 'upstream/main' 2026-05-23 17:29:50 +02:00
Mauro e95bcaf3e3 Merge pull request #1 from afjcjsbx/codex/resolve-main-upstream-merge
Merge upstream/main into main
2026-05-23 17:24:20 +02:00
afjcjsbx fbea699936 chore: move resolved upstream merge off main 2026-05-23 17:15:00 +02:00
Martin Zapletal 23e1485a98 Add Čeština to language switcher 2026-05-23 13:42:03 +02:00
Martin Zapletal edcae17b41 Register Czech (cs) locale in i18n config 2026-05-23 13:35:44 +02:00
Martin Zapletal d609e83313 Add Czech (cs) locale (792 strings) 2026-05-23 13:16:25 +02:00
hschne 96b4c543f4 fix(discord): download attachments for vision pipeline
Discord only downloaded audio attachments before passing them to the agent. Non-audio attachments (images, videos, files) were passed as raw Discord CDN URLs, which do not flow through resolveMediaRefs and are not serialized as vision inputs.

Download every attachment, store it in the MediaStore with Discord's filename and content type metadata, and emit a media placeholder tag that matches the attachment kind. This lets resolveMediaRefs replace the placeholder with the local path-bearing tag and encode supported images for vision-capable providers. If a download fails, keep the previous raw URL fallback.
2026-05-23 10:01:33 +02:00
Mauro 477028f8f2 Merge pull request #2895 from afjcjsbx/fix/seahorse-fresh-tail-budget
fix(seahorse): enforce budget on fresh tail and rebuild paths
2026-05-23 09:47:26 +02:00
afjcjsbx 9bb44b0a80 fix lint 2026-05-23 09:42:56 +02:00
afjcjsbx 6a97b1b087 Merge remote-tracking branch 'upstream/main' into fix/seahorse-fresh-tail-budget
# Conflicts:
#	pkg/agent/pipeline_llm.go
#	pkg/agent/pipeline_setup.go
#	pkg/agent/turn_state.go
2026-05-23 09:33:33 +02:00
Mauro 020bef2759 Merge pull request #2928 from lc6464/feat/deepseek-thinking-fields
feat(openai_compat): map DeepSeek thinking fields
2026-05-23 09:24:50 +02:00
afjcjsbx 848bf77381 Merge branch 'fix/seahorse-fresh-tail-budget'
# Conflicts:
#	pkg/agent/pipeline_llm.go
#	pkg/agent/pipeline_setup.go
#	pkg/agent/turn_state.go
2026-05-23 09:23:30 +02:00
lc6464 3a454593ca feat(openai_compat): map DeepSeek thinking fields 2026-05-23 10:51:24 +08:00
Anton Bogdanovich ceebda35ee fix(message): gate local media attachments 2026-05-22 16:36:44 -07:00
Anton Bogdanovich 1bf0d898de test(message): cover slack and feishu media fallbacks 2026-05-22 16:28:28 -07:00
Anton Bogdanovich c05e5e29c6 test(message): cover pico and weixin media text semantics 2026-05-22 16:25:50 -07:00
Anton Bogdanovich 987f117f31 style(telegram): satisfy formatter rules 2026-05-22 16:25:50 -07:00
Anton Bogdanovich 5a4e42d1b6 feat(message): support media attachments in outbound tool 2026-05-22 16:25:50 -07:00
Mauro f09a7d67f7 Merge pull request #2930 from lc6464/fix/security-xnet-html-0.55.0
build(deps): bump golang.org/x/net to v0.55.0
2026-05-22 19:46:28 +02:00
Mauro 2cce7b8abe Merge pull request #2788 from LiusCraft/feat/session-message-timestamps
feat(session): add per-message created_at timestamps
2026-05-22 19:45:53 +02:00
lc6464 044a9d1df6 fix(deps): bump golang.org/x/net to v0.55.0 2026-05-23 00:33:03 +08:00
Mauro d3ac0a74c4 Merge pull request #2921 from sipeed/dependabot/go_modules/github.com/adhocore/gronx-1.20.0
build(deps): bump github.com/adhocore/gronx from 1.19.7 to 1.20.0
2026-05-22 08:49:50 +02:00
Mauro 24e8285e73 Merge pull request #2923 from sipeed/dependabot/go_modules/github.com/line/line-bot-sdk-go/v8-8.20.0
build(deps): bump github.com/line/line-bot-sdk-go/v8 from 8.19.0 to 8.20.0
2026-05-22 08:49:24 +02:00
LiusCraft 33e5503e26 fix(session): normalize CreatedAt in SessionManager AddFullMessage/SetHistory 2026-05-22 13:42:35 +08:00
LiusCraft fd08ebd3db fix(test): read back history after SetHistory in steering test for CreatedAt normalization 2026-05-22 13:15:56 +08:00
LiusCraft 34e73f6b1a fix(test): read back history after SetHistory to account for CreatedAt normalization 2026-05-22 13:15:56 +08:00
LiusCraft 3e30e8abc6 style: wrap long error messages to satisfy golines 2026-05-22 13:15:56 +08:00
LiusCraft 81bbef62b1 feat(session): add per-message created_at timestamps
- Persistence layer (jsonl.go addMsg/SetHistory) normalizes CreatedAt
  when missing so the invariant is guaranteed at the storage boundary
- API layer (session.go) exposes created_at on all transcript message
  types with session.updated fallback for legacy messages
- Frontend uses per-message timestamps when available
- messagesContentEqual ignores CreatedAt for tail-matching after
  JSONL roundtrip

Fixes #2787
2026-05-22 13:15:56 +08:00
lxowalle 2992eccbf0 feat: add request-scoped context policies (#2914)
* feat: add request-scoped context policies

Add named turn profiles under agents.defaults so callers can opt into
per-request context and tool policies without changing default chat behavior.

Profiles can disable history, system context, skill prompts, or tools, and can
limit skills/tools with allow lists. Wire profile selection through Pico message
payloads, agent turn execution, Web chat selection, and Web visual config.

Reject invalid turn profiles before saving config through Web APIs and document
the new request context policy behavior.

* fix: address turn profile review blockers

* feat: simplify request context policy config

* fix: suppress tool prompt when turn tools are disabled

* fix: enforce turn profile tool restrictions
2026-05-22 10:06:40 +08:00
dependabot[bot] 76175b4bcf build(deps): bump github.com/line/line-bot-sdk-go/v8
Bumps [github.com/line/line-bot-sdk-go/v8](https://github.com/line/line-bot-sdk-go) from 8.19.0 to 8.20.0.
- [Release notes](https://github.com/line/line-bot-sdk-go/releases)
- [Commits](https://github.com/line/line-bot-sdk-go/compare/v8.19.0...v8.20.0)

---
updated-dependencies:
- dependency-name: github.com/line/line-bot-sdk-go/v8
  dependency-version: 8.20.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-21 18:28:37 +00:00
dependabot[bot] 0dfdb54198 build(deps): bump github.com/adhocore/gronx from 1.19.7 to 1.20.0
Bumps [github.com/adhocore/gronx](https://github.com/adhocore/gronx) from 1.19.7 to 1.20.0.
- [Release notes](https://github.com/adhocore/gronx/releases)
- [Changelog](https://github.com/adhocore/gronx/blob/main/CHANGELOG.md)
- [Commits](https://github.com/adhocore/gronx/compare/v1.19.7...v1.20.0)

---
updated-dependencies:
- dependency-name: github.com/adhocore/gronx
  dependency-version: 1.20.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-21 18:28:28 +00:00
SiYue-ZO 1bc7abfb50 feat(providers): add CommonModels for MiMo provider
Add mimo-v2.5 (multimodal) and mimo-v2.5-pro to MiMo's CommonModels so
the WebUI recommends vision-capable models by default. mimo-v2.5 supports
image understanding while mimo-v2.5-pro is text-only.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:36:41 +08:00
LC 5bbebb5fc8 feat(provider): add gpt4free openai-compatible provider (#2909) 2026-05-21 16:08:46 +08:00
LC f55d7a0598 fix(i18n): sync locale strings for model provider UI (#2911) 2026-05-21 15:55:37 +08:00
Guoguo 30938df40b fix(web): use stored API key when fetching models for saved providers (#2910)
When editing an existing model, the edit form initializes apiKey as
empty for security. This caused "Fetch Available Models" to reject with
"please enter API Key first" even though the key is saved server-side.

Add model_index support: the frontend passes the model's index to the
backend, which looks up the stored key from config. The key never leaves
the backend. Provider and API base are validated to prevent a stored key
from being sent to an unrelated endpoint.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 15:51:45 +08:00
lxowalle e7e21df354 fix(agent): honor explicit thinking off (#2898)
* fix(agent): honor explicit thinking off

* fix(agent): address thinking off lint failures

* Clarify unset thinking level display

* fix ci
2026-05-21 11:07:39 +08:00
Mauro 33f9d63862 Merge pull request #2891 from SiYue-ZO/feat/factory-reset
feat: add reset to factory defaults
2026-05-20 21:59:38 +02:00
Puneet Dixit 17cf91771c docs: add Android Termux guide
Closes #286

Assisted-by: OpenAI Codex

Signed-off-by: Puneet Dixit <236133619+puneetdixit200@users.noreply.github.com>
2026-05-20 19:57:07 +05:30
afjcjsbx f0dcba8c5a fix(seahorse): preserve active tool-call turn when trimming fresh tail 2026-05-20 09:16:09 +02:00
LC b7db059544 feat(chat,seahorse): persist and display model_name across history (#2897)
* feat(chat,seahorse): persist and display model_name across history

* test(seahorse): fix lint regressions in repair coverage

* fix(pico): preserve model_name in live updates

* fix(pico): preserve model_name through live stream wrappers
2026-05-20 13:42:21 +08:00
LC 548dc15acd refactor(models): unify provider metadata around backend catalog (#2896)
* feat(models): unify provider metadata around backend catalog

- Move shared provider metadata and alias normalization into backend-owned provider catalog
- Expose display, fetch, auth, and default model metadata through /api/models provider_options
- Replace frontend static provider registry with catalog-driven selection, validation, grouping, and fallback rendering
- Treat provider default api_base as placeholder and effective fetch/test base while keep submitted api_base separate from derived defaults
- Add model page retry handling, touched locale updates, and provider metadata assertions in backend tests

* fix(models): canonicalize backend provider aliases and common models

* fix(models): restore deepseek common model recommendations
2026-05-20 11:50:34 +08:00
lxowalle 639b32703a feat: support streaming (#2892)
* Support streaming

* fix: stream pico reasoning updates

Route Pico reasoning through the active streamer and hide empty thought placeholders.

* fix: harden configured streaming delivery

* fix ci

* fix split issue
2026-05-19 16:38:47 +08:00
afjcjsbx fe7ded5c13 fix(agent): preserve active turn during context retry rebuild 2026-05-19 09:18:39 +02:00
afjcjsbx 1502636bf0 fix(seahorse): enforce budget on fresh tail and rebuild paths 2026-05-18 21:11:21 +02:00
LC 941bac2332 feat(web): add chat detail visibility selector (#2886) 2026-05-18 14:50:57 +08:00
SiYue-ZO 3f653161e3 feat(frontend): add factory reset button with confirmation dialog
Add resetAppConfig API function, AlertDialog-confirmed factory reset
button in config page, and i18n keys for en/zh/pt-br locales.
2026-05-18 13:53:34 +08:00
SiYue-ZO f53222f6a4 feat(api): add POST /api/config/reset endpoint
Add handleResetConfig handler that calls ResetToDefaults, applies
runtime log level, and restarts the gateway if running.
2026-05-18 13:02:45 +08:00
SiYue-ZO d61902d42a feat(config): add ResetToDefaults and CLI config reset command
Export MakeBackup for external use, add ResetToDefaults function that
backs up current config, creates defaults, and preserves security
credentials. Add `picoclaw config reset` CLI command with --force flag.
2026-05-18 13:01:39 +08:00
sky5454 cb5d33124c fix(powershell): windows security enhancement, sec deny powershell encoding bypass via iex inje… (#2836)
* fix(powershell):  sec deny  powershell encoding bypass   via iex injection.

* fix(exec): security guard bypass fixes for PowerShell/CMD encoding and path traversal

- Split deny patterns into defaultDenyPatterns (all platforms) and
  windowsDenyPatterns (Windows-only) to avoid false positives
- Add PowerShell encoding bypass detection:
  - [Text.Encoding] and [System.Text.Encoding] variants
  - -EncodedCommand short forms (-e, -ec, -enc)
  - .GetString([byte[]] with whitespace variations
  - FromBase64String decoding
  - PowerShell variable = [byte[](...) patterns
  - Literal \uXXXX Unicode escape sequences
- Expand PowerShell ($env:VAR) and CMD (%VAR%) environment variables
  before workspace path checking to prevent $env:USERPROFILE bypass
- Expand ~ to home directory on Windows
- Add .../.../ path traversal variant detection (blocks .../.../, ..../..../)
- Add symlink/junction resolution before workspace check
- Add Windows path normalization for ADS (file.txt:stream) and
  extended-length paths (\?\)
- Add comprehensive tests for all new patterns

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(exec): fix -EncodedCommand regex and rename Windows tests with expanded payloads

- Fix -EncodedCommand regex to match all short forms: -e, -ec, -enc, -en
- Rename Windows-specific tests with TestWindows_ prefix for clarity:
  - TestWindows_TildeBypassPrevented
  - TestWindows_SymlinkBypassPrevented
  - TestWindows_PowerShellEncodingBypass
- Expand test payloads:
  - [Text.Encoding]: add UTF8 and Unicode variants
  - -EncodedCommand: add -enc and -en forms
  - Unicode escape: add multiple \uXXXX forms

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ci: retest

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 10:28:03 +08:00
Mauro 68e572f969 fix(config): make load_image configurable (#2879) 2026-05-18 10:17:34 +08:00
LC 57876248e2 feat(provider): add SiliconFlow provider support (#2885) 2026-05-18 10:16:09 +08:00
LC 789f907f6d feat(chat): add independent code block copy and collapse controls (#2882)
* feat(chat): add independent copy and collapse controls for code blocks

* fix(chat): unify code block rendering styles

* fix(chat): refine code block labels

* feat(chat): highlight tool call code blocks as json
2026-05-18 10:01:39 +08:00
Guoguo feacd84b84 docs: update wechat qrcode (#2889) 2026-05-18 09:50:36 +08:00
肆月 604187e312 feat(web,api): test connection with real connectivity verification (#2833)
* feat(web,api): add fetch models and saved catalog support

Split from PR #2752 (part 2 of 3).

Backend:
- /api/models/catalog endpoint for browsing remote model catalogs
- /api/models/fetch endpoint for fetching available models from providers
- Credential reuse with provider/API base matching for security
- Default API base resolution for providers without explicit base

Frontend:
- FetchModelsDialog for importing models from remote providers
- CatalogDialog for browsing and importing from model catalogs
- Static import for FetchModelsDialog (replaces dynamic import from PR1)
- Dynamic import retained for TestModelDialog (PR3 territory)

* feat(web,api): add test connection with real connectivity verification

Split from PR #2752 (part 3 of 3).

Backend:
- /api/models/{index}/test endpoint for testing saved model configs
- /api/models/test-inline endpoint for testing unsaved form values
- Real network probe (GET /models) for connectivity verification
- Credential reuse with provider/API base matching for security
- Default API base resolution for providers without explicit base

Frontend:
- TestModelDialog for testing model connectivity
- Inline test support for add/edit model sheets
- Static import for TestModelDialog (replaces dynamic import from PR1)
2026-05-18 09:47:44 +08:00
美電球 0df050ff2e Merge pull request #2766 from SiYue-ZO/docs/v3-config-format-sync
docs: sync all documentation to V3 config format
2026-05-15 22:06:08 +08:00
Mauro 6817aa5311 Merge pull request #2811 from afjcjsbx/fix/mcp-streamable-http-support
fix(mcp): support streamable HTTP alias, request-response mode and integration tests
2026-05-15 12:55:58 +02:00
lxowalle 412705783d fix(pico): preserve image media across pico attachments and client (#2874)
* fix(pico): preserve image media across pico attachments and client

* * fix ci

* fix(pico): preserve text when client media parsing fails

- Skip non-inline Pico attachment URLs instead of treating them as invalid inline media
- Preserve pico_client text messages when malformed media payloads are received
- Add regression coverage for media.create, download attachments, and invalid media payloads

* fix lint
2026-05-15 15:49:07 +08:00
wenjie bfb2b35f74 chore: update slack-go to v0.23.1 (#2875)
Adapt Slack media uploads to the renamed UploadFile API.
2026-05-15 13:58:11 +08:00
wenjie b225629af8 build(deps): update @tailwindcss/vite to 4.3.0 (#2876) 2026-05-15 13:58:09 +08:00
美電球 c62a9bf55b Merge pull request #2862 from lc6464/fix/mimo-reasoning-history-replay
fix(openai_compat): align MiMo reasoning replay with DeepSeek
2026-05-15 12:09:52 +08:00
美電球 f7d25c6546 Merge pull request #2741 from lc6464/fix/deepseek-stream-reasoning-content
fix(openai_compat): parse reasoning_content in streaming responses
2026-05-15 12:09:26 +08:00
dependabot[bot] 215d98aa78 build(deps): bump tailwindcss from 4.2.4 to 4.3.0 in /web/frontend (#2872)
Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) from 4.2.4 to 4.3.0.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.0/packages/tailwindcss)

---
updated-dependencies:
- dependency-name: tailwindcss
  dependency-version: 4.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-15 11:23:13 +08:00
dependabot[bot] dab8391344 build(deps-dev): bump typescript-eslint in /web/frontend (#2871)
Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.59.1 to 8.59.3.
- [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.3/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: typescript-eslint
  dependency-version: 8.59.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-15 11:08:46 +08:00
dependabot[bot] a4abbf62e2 build(deps-dev): bump prettier-plugin-tailwindcss in /web/frontend (#2870)
Bumps [prettier-plugin-tailwindcss](https://github.com/tailwindlabs/prettier-plugin-tailwindcss) from 0.7.2 to 0.8.0.
- [Release notes](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.7.2...v0.8.0)

---
updated-dependencies:
- dependency-name: prettier-plugin-tailwindcss
  dependency-version: 0.8.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-15 10:58:34 +08:00
dependabot[bot] 8ab455171c build(deps): bump jotai from 2.19.1 to 2.20.0 in /web/frontend (#2869)
Bumps [jotai](https://github.com/pmndrs/jotai) from 2.19.1 to 2.20.0.
- [Release notes](https://github.com/pmndrs/jotai/releases)
- [Commits](https://github.com/pmndrs/jotai/compare/v2.19.1...v2.20.0)

---
updated-dependencies:
- dependency-name: jotai
  dependency-version: 2.20.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-15 10:46:39 +08:00
dependabot[bot] eec4436e64 build(deps): bump github.com/adhocore/gronx from 1.19.6 to 1.19.7 (#2868)
Bumps [github.com/adhocore/gronx](https://github.com/adhocore/gronx) from 1.19.6 to 1.19.7.
- [Release notes](https://github.com/adhocore/gronx/releases)
- [Changelog](https://github.com/adhocore/gronx/blob/main/CHANGELOG.md)
- [Commits](https://github.com/adhocore/gronx/compare/v1.19.6...v1.19.7)

---
updated-dependencies:
- dependency-name: github.com/adhocore/gronx
  dependency-version: 1.19.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-15 10:31:30 +08:00
dependabot[bot] dc41c9c566 build(deps): bump golang.org/x/net from 0.53.0 to 0.54.0 (#2867)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.53.0 to 0.54.0.
- [Commits](https://github.com/golang/net/compare/v0.53.0...v0.54.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.54.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-15 10:21:14 +08:00
dependabot[bot] 2f8429f57c build(deps): bump github.com/mymmrac/telego from 1.8.0 to 1.9.0 (#2866)
Bumps [github.com/mymmrac/telego](https://github.com/mymmrac/telego) from 1.8.0 to 1.9.0.
- [Release notes](https://github.com/mymmrac/telego/releases)
- [Commits](https://github.com/mymmrac/telego/compare/v1.8.0...v1.9.0)

---
updated-dependencies:
- dependency-name: github.com/mymmrac/telego
  dependency-version: 1.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-15 10:13:57 +08:00
dependabot[bot] d8385ce0a7 build(deps-dev): bump vite from 8.0.10 to 8.0.13 in /web/frontend (#2865)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.0.10 to 8.0.13.
- [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.13/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.13
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-15 10:08:02 +08:00
dependabot[bot] 89631b8671 build(deps): bump github.com/larksuite/oapi-sdk-go/v3 (#2864)
Bumps [github.com/larksuite/oapi-sdk-go/v3](https://github.com/larksuite/oapi-sdk-go) from 3.6.1 to 3.7.5.
- [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.6.1...v3.7.5)

---
updated-dependencies:
- dependency-name: github.com/larksuite/oapi-sdk-go/v3
  dependency-version: 3.7.5
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-15 10:06:33 +08:00
dependabot[bot] 4db1168962 build(deps): bump modernc.org/sqlite from 1.48.2 to 1.50.1 (#2863)
Bumps [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) from 1.48.2 to 1.50.1.
- [Changelog](https://gitlab.com/cznic/sqlite/blob/master/CHANGELOG.md)
- [Commits](https://gitlab.com/cznic/sqlite/compare/v1.48.2...v1.50.1)

---
updated-dependencies:
- dependency-name: modernc.org/sqlite
  dependency-version: 1.50.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-15 09:57:11 +08:00
肆月 f6190b54de feat(web,api): fetch models and saved catalog support (#2832)
* feat(web,api): add fetch models and saved catalog support

Split from PR #2752 (part 2 of 3).

Backend:
- /api/models/catalog endpoint for browsing remote model catalogs
- /api/models/fetch endpoint for fetching available models from providers
- Credential reuse with provider/API base matching for security
- Default API base resolution for providers without explicit base

Frontend:
- FetchModelsDialog for importing models from remote providers
- CatalogDialog for browsing and importing from model catalogs
- Static import for FetchModelsDialog (replaces dynamic import from PR1)
- Dynamic import retained for TestModelDialog (PR3 territory)

* fix(web,api): support bare-array responses in fetchOpenAICompatibleModels

* fix(web,api): tighten maskAPIKeyValue to match maskAPIKey policy

For 9-12 character keys, maskAPIKeyValue exposed first 4 + last 4
chars (only 1 char masked for a 9-char key). Now uses the same
policy as maskAPIKey: first 3 + last 2 for 9-12 chars, first 3 +
last 4 for longer keys. Adds tests covering all key length boundaries.
2026-05-15 09:49:03 +08:00
lc6464 6ae7dc38b9 fix(openai_compat): align MiMo reasoning replay with DeepSeek 2026-05-14 20:59:01 +08:00
lc6464 10f4466a7e fix(openai_compat): parse SSE events and reasoning variants in streams 2026-05-14 10:20:01 +08:00
Anton Bogdanovich 794eb04f32 feat(providers): add gemini web search provider (#2763)
* add gemini web search provider

* fix(web): prefer free providers before Gemini in auto mode

* fix(web): expose gemini api key and model settings

* fix(web): prefer configured providers before Gemini in auto mode

* fix(web): satisfy gemini lint checks

* fix(web): address gemini provider review feedback

* test(web): align auto-provider expectations

* fix(web): let gemini ignore search range
2026-05-14 09:50:47 +08:00
afjcjsbx ffb8243721 fix(integration): docker runner workspace mounts and go command path 2026-05-13 20:03:55 +02:00
afjcjsbx ec21ddc222 fix lint 2026-05-13 19:57:38 +02:00
afjcjsbx ffe091d8b2 docs(cli): mention streamable-http in mcp add transport help 2026-05-13 19:56:08 +02:00
afjcjsbx 4edbc73b64 fix(integration): execute suite commands directly in docker runner 2026-05-13 19:54:32 +02:00
afjcjsbx ffc8bdba36 fix(mcp): normalize streamable-http before config validation 2026-05-13 19:30:59 +02:00
Mauro eb0653074b Merge pull request #2857 from afjcjsbx/feat/edit-file-diff-preview
feat(tools): show unified diff for edit_file edits
2026-05-13 09:04:32 +02:00
Guoguo f62de5c0d4 docs: update wechat qrcode (#2860) 2026-05-13 11:24:11 +08:00
afjcjsbx e0370aafcc fix test 2026-05-12 23:23:26 +02:00
afjcjsbx 56cca3f12f fix(tools) limit edit diff preview size for user and model 2026-05-12 23:12:37 +02:00
afjcjsbx 87048499ff fix(tools) diff preview for files without trailing newline 2026-05-12 23:06:43 +02:00
afjcjsbx 4a81f0e740 feat(tools): show unified diff for edit_file edits 2026-05-12 18:06:47 +02:00
lxowalle 223ebdf0c7 docs: add evolution config controls (#2852)
* docs: add evolution config controls

* docs: address evolution config review
2026-05-12 11:23:06 +08:00
lxowalle 255a67e2da docs: add LicheeRV-Claw AliExpress news (#2854)
* docs: add LicheeRV-Claw AliExpress news

* * update picture

* * update multi-language docs

* * update taobao link for readme.zh.md
2026-05-12 10:18:59 +08:00
Mauro 777269b429 Merge pull request #2758 from bogdanovich/codex/telegram-media-groups
fix(telegram): media group album handling
2026-05-12 00:11:48 +02:00
肆月 d2c0b69243 feat(web,api): provider selection and model form foundation (#2831)
* feat: improve model configuration workflows

Add model catalog browsing, provider registry with form validation,
model fetch/test dialogs, and enhanced model management UI.

- Add model catalog API and catalog-dialog component for browsing saved models
- Add provider-registry with auto-populated form fields per provider
- Add provider-combobox, fetch-models-dialog, test-model-dialog components
- Add model-validation for provider-aware model ID validation
- Add command and popover UI components
- Enhance edit-model-sheet with tool schema transform support
- Add anthropic to protocolMetaByName for correct default API base
- Apply NormalizeBaseURL to anthropic provider for consistent URL handling
- Add i18n keys for new model management features (en/zh)

* fix(web): prevent auto-fetch when API key is missing in fetch models dialog

When a provider requires an API key but none is set, the dialog now shows
the warning without triggering a doomed fetch attempt. Fetch is deferred
until the user provides a key.

* fix(web): add credential warning for catalog imports from remote providers

When importing models from a catalog entry whose provider requires an API
key, a yellow warning banner now informs users that credentials will need
to be configured after import.

* feat(web,api): test connection with real connectivity verification and unsaved form values

Add POST /api/models/test-inline endpoint that performs actual network
probes (GET /models) instead of just checking config. Frontend Test
Connection now uses current form values (not saved state) and is
available in both Add and Edit model flows.

* style(web): apply linter formatting across model config components

Normalize quote style, import ordering, and class name ordering as
reported by the project linter.

* fix(web,api): fix edit test connection false negative and gate fetch for unsupported providers

- handleTestInlineModel now accepts optional model_index to fall back to stored credentials when api_key is empty, fixing false negatives when testing edited models
- Add supportsFetch to provider registry and FETCHABLE_PROVIDER_KEYS derived set
- Gate Fetch Models button to only show for OpenAI-compatible and Ollama providers
- Add backend guard in handleFetchModels to reject unsupported providers with clear error

* fix: address review feedback on model config workflow

- Send explicit {} for empty extra_body/custom_headers fields so the
  backend clears stored values instead of preserving them
- Merge backend provider_options with frontend PROVIDERS registry so
  the provider picker reflects backend-supported providers and policy
  fields (create_allowed, default_auth_method, auth_method_locked)
- Render provider combobox popover inside the sheet scroll container
  to fix wheel events scrolling the sheet instead of the provider list

* feat(web,api): add provider selection, model form foundation, and validation

Split from PR #2752 (part 1 of 3).

Backend:
- CRUD model endpoints (list/add/update/delete/set-default)
- Provider metadata with default API bases and model provider options
- Model ID validation and normalization
- Anthropic default API base normalization

Frontend:
- Provider registry with metadata, labels, icons, and aliases
- Provider combobox with backend option merging
- Model field validation with provider-aware checks
- Redesigned add/edit model sheets with provider selection
- Dynamic imports for fetch/catalog/test dialogs (coming in PR2/PR3)
- i18n support for model configuration UI
2026-05-11 16:57:37 +08:00
美電球 7dc78425d1 Merge pull request #2719 from loafoe/feat/slack-webhook-channel
feat(channels): add slack_webhook output-only channel
2026-05-11 16:22:27 +08:00
lxowalle b3a7b7ad64 feat: agent self evolution (#2847)
* feat: add agent self-evolution

* fix ci

* delete unused doc

* fix lint

* fix evolution review issues
2026-05-11 16:13:27 +08:00
Andy Lo-A-Foe b12f03be2e feat(channels): add slack_webhook channel
Add an output-only channel that sends messages to Slack via Incoming
Webhooks using Block Kit formatting.

Features:
- Multiple webhook targets with named routing (requires "default" target)
- Markdown to Slack mrkdwn conversion (bold, italic, strikethrough, links, lists)
- Code block handling with proper fence preservation across chunk splits
- Table rendering with aligned columns in code blocks
- Automatic text chunking at 3000 chars (Slack's text block limit)
- HTTPS-only webhook URL validation

Configuration example:
  channels:
    slack_webhook:
      webhooks:
        default:
          webhook_url: "https://hooks.slack.com/services/..."
          username: "PicoClaw"
          icon_emoji: ":robot_face:"

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-11 09:54:04 +02:00
美電球 894c6251c5 Merge pull request #2783 from zhangxinping666/codex/fix-reload-voice-media-store
fix(gateway): keep media store aligned after reload
2026-05-11 15:44:38 +08:00
美電球 306f96cfe3 Merge pull request #2645 from loafoe/feat/bedrock-streaming
feat(bedrock): implement StreamingProvider for real-time token streaming
2026-05-11 15:08:43 +08:00
Gabriel S. Vieira 1055e082a4 Add MCP section to config web UI (#2770)
* Add MCP section to config UI

* Handle MCP sse and URL-based server mapping

* Validate duplicate MCP server names before save

* Disable MCP discovery options based on mutual exclusivity in config section

Co-authored-by: Copilot <copilot@github.com>

* Clear stale MCP transport fields in patch payload

* Fix MCP config form state preservation and validation

* Avoid MCP form ID collisions for distinct server names

* Validate remote MCP URLs in config UI

* fix(config): correct MCP discovery merge patch behavior

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix(config): align MCP discovery semantics and MCP server editor behavior

* fix(config): validate MCP server fields only when active

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-11 11:09:27 +08:00
xp 91f024eb1d fix(gateway): keep media store aligned after reload 2026-05-10 10:26:57 +08:00
Anton Bogdanovich 6801cc7ab8 fix(telegram): wrap long voice media append 2026-05-09 12:59:41 -07:00
Anton Bogdanovich 09d3dff432 fix telegram media group album handling 2026-05-09 12:59:41 -07:00
Mauro 6e6293e596 Merge pull request #2158 from afjcjsbx/feat/agent-discovery-prompt
feat(agent): Multi-agent discovery prompt
2026-05-09 13:56:19 +02:00
Mauro f571a142bf Merge pull request #2823 from bogdanovich/fix/parent-session-tool-feedback-cleanup
fix(agent): dismiss tool feedback when outbound is skipped
2026-05-09 11:09:10 +02:00
Mauro af901617ac Merge pull request #2828 from bogdanovich/fix/queued-voice-followups
fix(agent): transcribe queued voice follow-ups
2026-05-09 10:51:45 +02:00
afjcjsbx 2ae25b1038 fix(agent): treat empty AGENT.md tools as allow none 2026-05-09 10:35:13 +02:00
Anton Bogdanovich e1ed47b0ff fix(agent): remove unused scheduled helper 2026-05-09 00:53:23 -07:00
Anton Bogdanovich 8362203631 fix(agent): transcribe queued voice follow-ups 2026-05-08 13:50:14 -07:00
afjcjsbx 148583e7bb fix(agent): hide discovery when spawn is unavailable 2026-05-08 22:23:50 +02:00
Anton Bogdanovich a3edbcd05e test(agent): satisfy lint for tool feedback cleanup 2026-05-08 10:33:16 -07:00
afjcjsbx c6a09a35e2 fix(agent): suppress MCP discovery when no servers are selectable 2026-05-08 13:48:47 +02:00
afjcjsbx ffa184d183 fix(agent): resolve primary provider from frontmatter model 2026-05-08 13:43:21 +02:00
美電球 8508f80608 Merge pull request #2705 from hehaijunandhenry/main
add MQTT channel support
2026-05-08 18:50:08 +08:00
hehaijunandhenry 569939a7b3 Fix stop_mqtt_channel 2026-05-08 17:21:25 +08:00
hehaijunandhenry 2287de521e Linter fixed 2026-05-08 15:49:28 +08:00
afjcjsbx 871892ff15 fix(tools): exempt MCP discovery tools from agent allowlists 2026-05-08 09:18:14 +02:00
李光春 d5c8bfffbc fix(docs): correct Baidu Search free tier from 1000/day to 1500/month (#2784) (#2825) 2026-05-08 15:14:33 +08:00
hehaijunandhenry f062cb41d7 1 2026-05-08 14:48:43 +08:00
Anton Bogdanovich 610e9e3fe8 fix(agent): dismiss session tool feedback on skipped outbound 2026-05-07 23:10:56 -07:00
美電球 01280eaa53 Merge pull request #2413 from ex-takashima/refactor/line-sdk
refactor(line): use official LINE Bot SDK v8
2026-05-08 14:00:00 +08:00
ex-takashima bacb9aba7c fix(line): close response body on successful SendMedia calls
Always route through classifySDKError to ensure resp.Body is
closed even when the API call succeeds.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-08 14:11:15 +09:00
ex-takashima 6d7d1b0909 fix(line): capture QuoteToken for all message types and handle location
- Store QuoteToken for image, video, and sticker messages (not just text)
- Add webhook.LocationMessageContent case to forward as [location] placeholder

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-08 14:05:58 +09:00
dependabot[bot] 3788e9edad build(deps): bump i18next from 26.0.8 to 26.0.10 in /web/frontend (#2809)
Bumps [i18next](https://github.com/i18next/i18next) from 26.0.8 to 26.0.10.
- [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.8...v26.0.10)

---
updated-dependencies:
- dependency-name: i18next
  dependency-version: 26.0.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-08 11:15:44 +08:00
dependabot[bot] c2044e5a2c build(deps): bump react-i18next from 17.0.4 to 17.0.6 in /web/frontend (#2808)
Bumps [react-i18next](https://github.com/i18next/react-i18next) from 17.0.4 to 17.0.6.
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/react-i18next/compare/v17.0.4...v17.0.6)

---
updated-dependencies:
- dependency-name: react-i18next
  dependency-version: 17.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-08 11:08:15 +08:00
dependabot[bot] 7c8cd7c66a build(deps-dev): bump globals from 17.5.0 to 17.6.0 in /web/frontend (#2807)
Bumps [globals](https://github.com/sindresorhus/globals) from 17.5.0 to 17.6.0.
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v17.5.0...v17.6.0)

---
updated-dependencies:
- dependency-name: globals
  dependency-version: 17.6.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-08 11:07:46 +08:00
dependabot[bot] f4338d3aab build(deps): bump @tabler/icons-react in /web/frontend (#2806)
Bumps [@tabler/icons-react](https://github.com/tabler/tabler-icons/tree/HEAD/packages/icons-react) from 3.41.1 to 3.43.0.
- [Release notes](https://github.com/tabler/tabler-icons/releases)
- [Commits](https://github.com/tabler/tabler-icons/commits/v3.43.0/packages/icons-react)

---
updated-dependencies:
- dependency-name: "@tabler/icons-react"
  dependency-version: 3.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-08 11:07:23 +08:00
dependabot[bot] b7edd35d13 build(deps): bump shadcn from 4.3.0 to 4.7.0 in /web/frontend (#2804)
Bumps [shadcn](https://github.com/shadcn-ui/ui/tree/HEAD/packages/shadcn) from 4.3.0 to 4.7.0.
- [Release notes](https://github.com/shadcn-ui/ui/releases)
- [Changelog](https://github.com/shadcn-ui/ui/blob/main/packages/shadcn/CHANGELOG.md)
- [Commits](https://github.com/shadcn-ui/ui/commits/shadcn@4.7.0/packages/shadcn)

---
updated-dependencies:
- dependency-name: shadcn
  dependency-version: 4.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-08 10:50:08 +08:00
dependabot[bot] d0ab5aed7a build(deps): bump fyne.io/systray from 1.12.0 to 1.12.1 (#2803)
Bumps [fyne.io/systray](https://github.com/fyne-io/systray) from 1.12.0 to 1.12.1.
- [Changelog](https://github.com/fyne-io/systray/blob/master/CHANGELOG.md)
- [Commits](https://github.com/fyne-io/systray/compare/v1.12.0...v1.12.1)

---
updated-dependencies:
- dependency-name: fyne.io/systray
  dependency-version: 1.12.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-08 10:47:29 +08:00
Mauro 1c25dcd239 build(go): bump Go to 1.25.10 to fix stdlib vulnerabilities (#2818) 2026-05-08 09:33:17 +08:00
afjcjsbx 0ac8703e0f docs(tests): document integration test strategy 2026-05-07 20:45:05 +02:00
afjcjsbx 131f33f084 fix(tests): add t.Parallel() to mcp streamable subtests 2026-05-07 20:39:53 +02:00
afjcjsbx 6dd30a0c77 fix(ci): ensure go binary resolution and correct command execution 2026-05-07 20:31:57 +02:00
afjcjsbx 7a8d7fb218 fix(scripts): allow command expansion in integration runner 2026-05-07 20:28:07 +02:00
afjcjsbx 703f630f33 fix(ci): define golang image and volumes for integration-runner 2026-05-07 19:44:06 +02:00
Mauro 2834db13de Merge pull request #2801 from sipeed/dependabot/go_modules/github.com/google/jsonschema-go-0.4.3
build(deps): bump github.com/google/jsonschema-go from 0.4.2 to 0.4.3
2026-05-07 19:28:26 +02:00
dependabot[bot] e948106d50 build(deps): bump github.com/google/jsonschema-go from 0.4.2 to 0.4.3
Bumps [github.com/google/jsonschema-go](https://github.com/google/jsonschema-go) from 0.4.2 to 0.4.3.
- [Release notes](https://github.com/google/jsonschema-go/releases)
- [Commits](https://github.com/google/jsonschema-go/compare/v0.4.2...0.4.3)

---
updated-dependencies:
- dependency-name: github.com/google/jsonschema-go
  dependency-version: 0.4.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-07 17:24:06 +00:00
afjcjsbx 6e8590900b fix(mcp): support streamable HTTP alias and request-response mode 2026-05-07 19:24:02 +02:00
afjcjsbx b8f4257cee fix(agent): filter discovery by spawn permissions 2026-05-07 18:26:09 +02:00
afjcjsbx 96fd887cad fix(agent): match MCP server allowlists case-insensitively 2026-05-07 18:17:37 +02:00
afjcjsbx dd8e247550 fix(agent): align MCP prompt registration with tool allowlist 2026-05-07 14:01:43 +02:00
afjcjsbx 27bd816b1c fix(agent): validate AGENT tool declarations from registry 2026-05-07 13:49:23 +02:00
afjcjsbx f1f6e1131b removed unused code 2026-05-07 13:20:39 +02:00
afjcjsbx 6f6270b39d Merge upstream/main into feat/agent-discovery-prompt 2026-05-07 13:16:30 +02:00
ex-takashima 41d6156dce style(line): shorten long line for golines linter
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-07 16:48:45 +09:00
ex-takashima ad78ba06ea fix(line): close HTTP response body from WithHttpInfo calls
Fix bodyclose linter errors by ensuring resp.Body is closed
after all *WithHttpInfo SDK calls.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-07 16:41:19 +09:00
ex-takashima 9b7fc7aa6c fix(line): classify SDK errors with HTTP status and add client timeout
Address review feedback:
- Use *WithHttpInfo SDK variants to get HTTP response status codes
- Map status codes via ClassifySendError (429→ErrRateLimit, 5xx→ErrTemporary, 4xx→ErrSendFailed)
- Fall back to ClassifyNetError for network-level failures
- Configure SDK with 30s timeout HTTP client

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-07 16:23:33 +09:00
hehaijunandhenry e7c0dc821a Merge remote-tracking branch 'remotes/upstream/main' 2026-05-07 14:39:29 +08:00
美電球 658961b728 Merge pull request #2531 from is-Xiaoen/feat/delegate-tool
feat(tools): add delegate tool for cross-agent task handoff
2026-05-07 11:25:41 +08:00
Mauro 788cda5c7a Merge pull request #2762 from afjcjsbx/feat/stop-command
feat(agent): stop command
2026-05-06 18:19:14 +02:00
SiYue e304dce40e docs: sync all documentation to V3 config format
- Replace config/config.example.json with V3 format (version: 3, api_keys array, channel_list)
- Update config-versioning.md: version 2→3, ConfigV2→ConfigV3, CurrentVersion=3
- Update 7 project READMEs: api_key→api_keys, add version: 3 to quick-start examples
- Update 12 security docs (ANTIGRAVITY_AUTH + credential_encryption): api_key→api_keys
- Update provider-refactoring.md: api_key→api_keys in all config examples
- Update security_configuration.md: api_key→api_keys in Before example
- Update 3 channel docs: channels→channel_list in JSON examples
2026-05-06 18:02:46 +08:00
LC 81a050555d feat(provider,web,asr): enhance model management with explicit provider metadata (#2701)
* feat(provider,web): enhance model management with provider options

* fix(asr): enhance compatibility for ElevenLabs transcription model

* fix(provider,web): align provider availability predicates and add flow gating

* fix(web,asr): preserve legacy elevenlabs transcription configs

* fix(provider,web,asr): normalize elevenlabs configs and gate default chat models

* fix: tighten provider catalog and elevenlabs compatibility
2026-05-06 16:06:49 +08:00
openapphub 4d3070e849 fix(web): 兼容 HTTP 环境复制按钮 (#2712)
Co-authored-by: openapphub <175949671+openapphub@users.noreply.github.com>
2026-05-06 14:44:36 +08:00
dependabot[bot] e3a05bd36d build(deps): bump @tailwindcss/vite from 4.2.2 to 4.2.4 in /web/frontend (#2734)
Bumps [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) from 4.2.2 to 4.2.4.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.2.4/packages/@tailwindcss-vite)

---
updated-dependencies:
- dependency-name: "@tailwindcss/vite"
  dependency-version: 4.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-06 14:32:00 +08:00
dependabot[bot] 0977f59fee build(deps): bump github.com/larksuite/oapi-sdk-go/v3 (#2736)
Bumps [github.com/larksuite/oapi-sdk-go/v3](https://github.com/larksuite/oapi-sdk-go) from 3.5.4 to 3.6.1.
- [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.4...v3.6.1)

---
updated-dependencies:
- dependency-name: github.com/larksuite/oapi-sdk-go/v3
  dependency-version: 3.6.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-06 14:27:14 +08:00
dependabot[bot] 00742b0196 build(deps): bump @tanstack/react-router in /web/frontend (#2733)
Bumps [@tanstack/react-router](https://github.com/TanStack/router/tree/HEAD/packages/react-router) from 1.168.23 to 1.169.2.
- [Release notes](https://github.com/TanStack/router/releases)
- [Changelog](https://github.com/TanStack/router/blob/main/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/TanStack/router/commits/@tanstack/react-router@1.169.2/packages/react-router)

---
updated-dependencies:
- dependency-name: "@tanstack/react-router"
  dependency-version: 1.168.26
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-06 14:17:21 +08:00
dependabot[bot] 0419497c72 build(deps): bump i18next from 26.0.7 to 26.0.8 in /web/frontend (#2732)
Bumps [i18next](https://github.com/i18next/i18next) from 26.0.7 to 26.0.8.
- [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.7...v26.0.8)

---
updated-dependencies:
- dependency-name: i18next
  dependency-version: 26.0.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-06 14:14:08 +08:00
dependabot[bot] 864bfa1cef build(deps-dev): bump typescript-eslint in /web/frontend (#2730)
Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.59.0 to 8.59.1.
- [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.1/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: typescript-eslint
  dependency-version: 8.59.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-06 14:12:19 +08:00
dependabot[bot] c0bc8a3f9d build(deps): bump tailwindcss from 4.2.2 to 4.2.4 in /web/frontend (#2729)
Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) from 4.2.2 to 4.2.4.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.2.4/packages/tailwindcss)

---
updated-dependencies:
- dependency-name: tailwindcss
  dependency-version: 4.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-06 14:10:28 +08:00
Diego Fornalha 96621eff21 feat(i18n): add Portuguese (Brazil) locale (#2037)
* feat(i18n): add Portuguese (Brazil) locale

Add pt-BR as the third supported language in the Web UI, alongside
English and Chinese. The browser language detector will auto-select
PT-BR for Portuguese-speaking users.

Changes:
- Add web/frontend/src/i18n/locales/pt-br.json with full translation
- Register pt-BR resource and dayjs locale in i18n/index.ts
- Add "Português (Brasil)" option to language selector dropdown

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore(i18n): refresh pt-br locale to match current en.json keys

Add 194 new keys (skills marketplace, tour, launcher login/setup, chat
disabled placeholders, web search tools, dashboard password, etc.) and
remove 15 outdated keys so pt-br.json now mirrors en.json (601/601 keys).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 11:33:39 +08:00
afjcjsbx a7e52e8a25 fix(agent): drain scoped follow-up queue when pending stop skips turn startup 2026-05-05 19:24:15 +02:00
Mauro eb4e187550 Merge pull request #2767 from afjcjsbx/fix/leaf-summary-target-validation
fix(seahorse): enforce target token thresholds for leaf summaries
2026-05-05 19:09:06 +02:00
Mauro 0129da1c8e Merge pull request #2773 from zhangxinping666/codex/fix-telegram-svg-media
fix(agent): send SVG attachments as files
2026-05-05 14:12:34 +02:00
xp d601b75268 fix(agent): send SVG attachments as files 2026-05-05 19:36:09 +08:00
Mauro 5745957429 Merge pull request #2731 from sipeed/dependabot/go_modules/github.com/aws/aws-sdk-go-v2/service/bedrockruntime-1.50.6
build(deps): bump github.com/aws/aws-sdk-go-v2/service/bedrockruntime from 1.50.5 to 1.50.6
2026-05-04 22:07:36 +02:00
Mauro ba4abff4a4 Merge pull request #2670 from david1gp/fix/tool-feedback-pretty-print
feat(agent): add pretty_print and disable_escape_html options for tool feedback
2026-05-04 21:47:32 +02:00
afjcjsbx a1b55fd4f9 fix(seahorse): enforce target token thresholds for leaf summaries 2026-05-04 14:10:42 +02:00
afjcjsbx d63430ab33 fix(agent): don't arm pending stop when /stop targets idle session 2026-05-04 13:10:02 +02:00
美電球 71c49812ae Merge pull request #2764 from alexhoshina/main
fix(agent): use runtime event kind for LLM retry
2026-05-04 15:48:11 +08:00
afjcjsbx 7a1f5fe8b9 fix test 2026-05-04 09:06:39 +02:00
Hoshina 057683d94c fix(agent): use runtime event kind for LLM retry 2026-05-04 15:06:34 +08:00
afjcjsbx a0245c7b02 feat(agent): stop command 2026-05-04 08:41:29 +02:00
afjcjsbx f3ef7090c5 feat(agent): stop command 2026-05-04 08:41:17 +02:00
Mauro be67aed4dc Merge pull request #2677 from alexhoshina/feat/runtime-events-plan
Feat/runtime events
2026-05-03 23:15:25 +02:00
Mauro f4a5d6e808 Merge pull request #2682 from dtapps/fix/docs-agent-defaults-model-format
docs: fix agents.defaults model configuration format
2026-05-03 23:14:26 +02:00
dependabot[bot] 330aa297e2 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.5 to 1.50.6.
- [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.5...service/ecr/v1.50.6)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2/service/bedrockruntime
  dependency-version: 1.50.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-03 18:30:21 +00:00
Mauro 4e8bd73a58 Merge pull request #2735 from sipeed/dependabot/go_modules/github.com/aws/aws-sdk-go-v2/config-1.32.17
build(deps): bump github.com/aws/aws-sdk-go-v2/config from 1.32.16 to 1.32.17
2026-05-03 20:27:49 +02:00
Mauro 828a7cba70 Merge pull request #2681 from afjcjsbx/fix/gemini-mcp-schema-sanitization
fix(mcp): sanitize MCP tool schemas for Gemini function calling
2026-05-03 20:25:35 +02:00
Mauro 490d90749c Merge pull request #2717 from LiusCraft/feat/deepseek-vision-unsupported-error
feat: add DeepSeek vision unsupported error detection
2026-05-03 20:24:56 +02:00
Mauro 272dee3fca Merge pull request #2669 from david1gp/fix/network-error-retry
feat(agent): add network error retry with configurable max retries and backoff
2026-05-03 20:18:18 +02:00
BeaconCat a94ba82181 chore: update WeChat group QR code (#2747)
Co-authored-by: BeaconCat <BeaconCat@users.noreply.github.com>
2026-05-02 16:55:53 +08:00
lc6464 b00ff5bc5d fix(openai_compat): parse reasoning_content in streaming responses 2026-05-01 15:20:35 +08:00
dependabot[bot] b792d8b77b build(deps): bump github.com/aws/aws-sdk-go-v2/config
Bumps [github.com/aws/aws-sdk-go-v2/config](https://github.com/aws/aws-sdk-go-v2) from 1.32.16 to 1.32.17.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/config/v1.32.16...config/v1.32.17)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2/config
  dependency-version: 1.32.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-30 17:19:52 +00:00
lxowalle 6e1fab80e2 Fix/build macos launcher failed (#2724)
* fix(release): drop stale launcher tui goreleaser target

* * delete unused file
2026-04-30 22:48:32 +08:00
lxowalle a7414608ed * fix build failed (#2723) 2026-04-30 18:04:48 +08:00
LC dbf5d9ce1f fix(seahorse): persist reasoning_content in sqlite history (#2707)
* fix(seahorse): persist reasoning_content in sqlite history

* fix(openai_compat): clarify DeepSeek reasoning replay rules

* style(seahorse): format files of seahorse

* fix(openai_compat): cover DeepSeek replay requirements

* fix(seahorse): repair missing reasoning_content during bootstrap

* fix(seahorse): repair reasoning_content on bootstrap prefix
2026-04-30 16:07:38 +08:00
Guoguo 5db008f384 fix(channels): dismiss tool feedback animation when turn ends via ResponseHandled (#2713)
* fix(channels): dismiss tool feedback animation when turn ends via ResponseHandled

When a tool sets ResponseHandled=true (e.g., send_file), the turn ends
without producing a final assistant response. This meant no outbound
message triggered FinalizeToolFeedbackMessage, leaving the animation
goroutine running indefinitely — editing the Feishu card every 3 seconds
with "." / ".." suffixes long after the tool had finished.

Fix: call DismissToolFeedback at "Tool output satisfied delivery" so the
tracker is cleared and the animation goroutine is stopped immediately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(adapters): add DismissToolFeedback to channelManagerAdapter

The adapter must implement the new interface method added in the
previous commit, otherwise the package fails to compile.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(channels): pass InboundContext to DismissToolFeedback for topic-aware keys

Telegram forum topics use scoped tracker keys like "chatID/topicID",
resolved via ToolFeedbackMessageChatID with the InboundContext. The
previous nil context caused the lookup to fall back to the raw chatID,
missing the topic-scoped entry and leaving the animation goroutine
orphaned in forum-topic conversations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* style: wrap long function signatures for golines

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 11:17:55 +08:00
Guoguo cb1e1a3595 fix(feishu): fix image download with API fallback and post image support (#2708)
* fix(feishu): fix image download with API fallback and post image support

- Add Image.Get API fallback when MessageResource.Get fails (different
  permission scope: im:resource vs im:message:readonly)
- Extract and download images from post (rich text) messages
- Extract images from interactive card messages
- Deduplicate post image keys across locales
- Add comprehensive tests for new helpers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(media): add image path tags alongside base64 for LLM file access

Images are still base64-encoded into msg.Media for multimodal LLMs,
but now also get [image:path] tags injected into message content so
the LLM knows the local file path for save/forward operations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(media): only auto-inject images for tool results, not user messages

Channel-received images (role=user) now get path tags only, letting
the LLM decide whether to view via load_image or just operate on
the file. Tool result images (role=tool, e.g. load_image) are
base64-encoded into a synthetic user message appended after the tool
message, since many LLM APIs don't support image_url in tool messages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(media): preserve tool-message ordering for multi-tool-call scenarios

Move synthetic user message (carrying base64 tool images) to after the
entire contiguous tool-message block instead of immediately after each
tool message. This preserves the assistant→tool→tool ordering required
by OpenAI-compatible APIs.

Also fix load_image to use generic [image: photo] placeholder so
injectPathTags can properly replace it with the actual path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(test): update load_image test for [image: photo] placeholder

The test was checking ForLLM for the media:// ref, but load_image now
emits the generic [image: photo] placeholder instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(media): match all channel image placeholders in injectPathTags

Different channels emit different placeholder formats — Telegram/Feishu
use [image: photo], WeCom/WeChat/Line use bare [image], QQ/Discord use
[image: <filename>]. The previous string-match code only handled
[image: photo], so for the other channels the path tag was appended as
a duplicate, producing content like "[image] [image:/path]".

Switch to per-type regex that matches all generic placeholder shapes
while leaving path tags ([image:/path]) untouched. Also fixes the same
issue for [audio], [video], [file] tags. Added test coverage for the
various placeholder shapes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(media): skip path tag append for JSON content (Feishu cards/posts)

When content is structured JSON (interactive cards, post messages),
injectPathTags now skips the fallback append — only placeholder
replacement is attempted. This prevents corrupting JSON payloads
like {"schema":"2.0",...} with appended [image:/path] tags.

Adds looksLikeJSON() helper and three test cases covering JSON
objects, arrays, and an end-to-end resolveMediaRefs scenario with
Feishu card content.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(media): prepend path tags for JSON content, narrow looksLikeJSON

Two fixes from code review:

1. looksLikeJSON now only checks for '{' prefix (not '['), avoiding
   false positives on regular text like "[update] see attached".

2. For JSON content (Feishu cards/posts), path tags are prepended
   before the JSON instead of being silently dropped. This ensures
   the LLM can discover attached images via the path tag while the
   JSON payload stays valid for downstream parsing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-30 11:08:00 +08:00
Andy Lo-A-Foe b03fa61764 test(bedrock): add unit tests for ChatStream/parseStreamResponse
Tests cover: text-only streaming with chunk accumulation, tool call
parsing with fragmented JSON, mixed text+tool responses, context
cancellation, invalid JSON fallback to raw payload, nil stream guard,
default finish reason, and all stop reason mappings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-29 21:17:16 +02:00
Andy Lo-A-Foe ad5232ade8 feat(bedrock): implement StreamingProvider for real-time token streaming
Adds ConverseStream API support to the Bedrock provider, implementing
the StreamingProvider interface. Tokens flow via onChunk callback for
real-time delivery to streaming-capable channels.

- Extract buildConverseParams to share request logic between Chat and ChatStream
- Add converseStreamReader interface for testability
- Preserve raw payload in Arguments on JSON parse failure
- Ensure Function.Arguments is always valid JSON
- Streaming timeout only applied when explicitly configured
- Capture stream Close() errors for diagnostics
- Consistent "bedrock conversestream" / "bedrock:" log prefixes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-29 21:17:16 +02:00
LiusCraft 1722cfc282 feat: add DeepSeek vision unsupported error detection
Add detection for 'unknown variant' + 'image_url' error pattern used by
DeepSeek and other strict providers when vision is not supported.
These providers reject the image_url field at the JSON schema level
rather than returning a semantic 'not supported' message.
2026-04-30 02:24:29 +08:00
hehaijunandhenry 5c0492900e add MQTT channel support 2026-04-29 18:27:33 +08:00
taonyx a36472b55f Add CLI support for custom OpenAI-compatible endpoints and remove TUI (#2710)
* feat(model): add `picoclaw model add` for custom OpenAI-compatible endpoints

Onboards a model from a user-supplied API base + key by hitting
GET <base>/models, prompting the user to pick one, and writing the entry
into model_list[] (with api_keys) plus setting it as the default model.
This was previously only available in the TUI launcher (issue #2208) and
is now accessible from the CLI:

    picoclaw model add -b URL -k KEY [-m MODEL] [-n ALIAS]

* chore: remove deprecated picoclaw-launcher-tui

Per RFC #2208, the TUI launcher is deprecated in favor of the CLI; its
"online model picker" feature has been ported to `picoclaw model add` in
the previous commit. This drops the binary and all build/release/docs
references:

- delete cmd/picoclaw-launcher-tui/ and assets/launcher-tui.jpg
- Makefile: remove the `build-launcher-tui` target
- .goreleaser.yaml: drop the build entry plus the `picoclaw-launcher-tui`
  ids from the launcher docker image, macOS notarize list, and nfpms
  contents
- docker/Dockerfile.goreleaser.launcher: drop the COPY for the TUI binary
- READMEs (root + 8 locales): remove the "TUI Launcher" section and
  screenshot link
- docs/guides/docker.*: update the "launcher image includes …" sentence
  to reflect the two remaining binaries

`make build` still succeeds; `go build ./web/backend` (the launcher
target) still succeeds. `picoclaw-launcher` (web console) is unaffected.
2026-04-29 17:52:47 +08:00
Guoguo 62d0e34ec9 fix(docker): restore make docker-build by adding build directives and fixing Go version (#2700)
* fix(docker): restore `make docker-build` by adding build directives and fixing Go version

docker-compose.yml only had `image:` references with no `build:` sections,
so `docker compose build` had nothing to build. Also fixed golang:1.26.0-alpine
(nonexistent) to golang:1.25-alpine in Dockerfile.full/heavy, and removed
LICENSE from .dockerignore since scripts/copydir.go needs it as a repo-root anchor
during `go generate`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(docker): inject version metadata ldflags in Dockerfile.launcher

Mirror the ldflags from web/Makefile (Version, GitCommit, BuildTime,
GoVersion) into the picoclaw-launcher go build command so Docker-built
launcher images include proper version/build metadata.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-29 16:19:15 +08:00
美電球 db1bc6a1f8 Merge pull request #2689 from afjcjsbx/fix/cron-session-key-propagation
fix(cron): propagate sessionKey to prevent duplicate tool responses
2026-04-28 23:03:09 +08:00
LC 9b109dc7a8 fix(serial_windows): remove unused import (#2697) 2026-04-28 16:19:59 +08:00
Guoguo fc24676924 Add cross-platform serial tool support (#2673)
* feat(tools): add cross-platform serial hardware tool

* feat(config): wire serial tool into runtime and dashboard

* hardware/serial: tighten validation and error handling

* hardware/serial: improve unix cancellation and timeout polling

* hardware/serial: improve windows I/O handling

* hardware/serial: fix darwin cross-compilation build

* docs(design): summarize hardware support and serial limits

* build: keep go generate on host during cross builds

* onboard: drop unrelated go generate change from serial work

* style(tools): wrap serial lines for golines
2026-04-28 13:10:32 +08:00
SiYue-ZO bd867a16cd style(tools): wrap serial lines for golines 2026-04-28 12:58:26 +08:00
SiYue-ZO 29e7461837 onboard: drop unrelated go generate change from serial work 2026-04-28 12:58:26 +08:00
SiYue-ZO 688d47d236 build: keep go generate on host during cross builds 2026-04-28 12:57:44 +08:00
SiYue-ZO 2baeee2834 docs(design): summarize hardware support and serial limits 2026-04-28 12:57:44 +08:00
SiYue-ZO 893e61dc51 hardware/serial: fix darwin cross-compilation build 2026-04-28 12:57:44 +08:00
SiYue-ZO 64e48163d0 hardware/serial: improve windows I/O handling 2026-04-28 12:57:25 +08:00
SiYue-ZO 1f0a5f4eda hardware/serial: improve unix cancellation and timeout polling 2026-04-28 12:57:09 +08:00
SiYue-ZO 338fa258b3 hardware/serial: tighten validation and error handling 2026-04-28 12:56:47 +08:00
SiYue-ZO 2114e1a53f feat(config): wire serial tool into runtime and dashboard 2026-04-28 12:56:27 +08:00
SiYue-ZO 0f52076762 feat(tools): add cross-platform serial hardware tool 2026-04-28 12:54:28 +08:00
LC c44bd6138c refactor(pico): unify message kind handling of tool_calls and thought (#2680)
* refactor(pico): unify message kind handling of tool_calls and thought

* fix(pico): add legacy compatibility for thought payload in Send method

Co-authored-by: Copilot <copilot@github.com>

---------

Co-authored-by: Copilot <copilot@github.com>
2026-04-28 10:17:12 +08:00
afjcjsbx 23df824c77 fix test 2026-04-27 21:27:02 +02:00
afjcjsbx 87ee76b117 Merge remote-tracking branch 'origin/fix/gemini-mcp-schema-sanitization' into fix/gemini-mcp-schema-sanitization 2026-04-27 21:18:29 +02:00
afjcjsbx 7b3e800407 fix test 2026-04-27 21:18:19 +02:00
Mauro c731ecdc74 Merge branch 'main' into fix/gemini-mcp-schema-sanitization 2026-04-27 21:14:25 +02:00
afjcjsbx cd7717bc15 feat(tool): tool schema semplification 2026-04-27 21:10:30 +02:00
afjcjsbx 0bb0fc429a fix(cron): propagate sessionKey to prevent duplicate tool responses 2026-04-27 13:17:25 +02:00
David Siewert e656ddf5bb fix: align struct tag spacing in AgentDefaults config 2026-04-27 16:47:28 +06:00
David Siewert 38baf1ccd0 fix(agent): normalize nil args and improve error handling in FormatArgsJSON
- Return fmt.Sprintf fallback instead of {} on encoding errors to preserve visibility
- Normalize nil to empty map in FormatArgsJSON for consistent output
- Remove redundant nil check in toolFeedbackArgsPreview wrapper
- Update test expectation: nil args now return {} not null
2026-04-27 16:31:20 +06:00
David Siewert 8dca2a1319 fix: improve error handling and nil consistency in FormatArgsJSON
- Use fmt.Sprintf fallback instead of {} on encoding errors
- Normalize nil args to {} in FormatArgsJSON for consistent output
- Update tests to expect {} instead of null for nil args

Based on PR #2670 review feedback from afjcjsbx
2026-04-27 16:05:10 +06:00
David Siewert 97b1c3efec fix duplicate toolFeedbackArgsPreview function declaration 2026-04-27 15:42:18 +06:00
Guoguo 0161298154 ci: add stale bot to auto-close inactive issues and PRs (#2685)
Two-phase strategy: 7 days inactive → stale warning, 7 more days → close.
Exempt labels: pinned, keep-open, wip, do-not-close, type: roadmap.
Draft PRs are also exempt. Runs daily at 03:00 JST.
Scan oldest items first (ascending: true) with 500 ops budget to avoid
backlog starvation.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-27 17:15:02 +08:00
ex-takashima 188ee24d2e Merge remote-tracking branch 'origin/main' into refactor/line-sdk
# Conflicts:
#	go.mod
#	go.sum
2026-04-27 17:47:41 +09:00
美電球 f90e756e21 Merge pull request #2663 from SiYue-ZO/feature/config-save-restart-prompts
feat: improve config save and restart feedback
2026-04-27 15:45:38 +08:00
Hoshina 78fd080189 fix(events): keep runtime observers non-blocking
Add a non-blocking runtime publish path and switch hot-path publishers to it.

Enforce subscription timeout boundaries, keep ordered subscriber snapshots up to date on subscribe changes, expose all runtime kinds to process hooks, add safe log attrs for non-agent events, and close the gateway message bus on full shutdown.
2026-04-27 13:09:03 +08:00
Mauro ed687d62ae fix(config): show precise malformed config diagnostics (#2415)
* fix(config): show precise malformed config diagnostics

* fix lint

* fix test
2026-04-27 09:45:52 +08:00
dtapps f62e8621fc docs: fix agents.defaults model configuration format
Change incorrect object format model.primary/fallbacks to correct
flat format model_name/model_fallbacks in Agent defaults example.

The AgentDefaults struct does not support the object format used
in AgentConfig, so the documentation example was misleading.
2026-04-27 09:30:11 +08:00
afjcjsbx 4eeb69688e fix lint 2026-04-26 22:33:35 +02:00
afjcjsbx 1ff8a418f6 fix(mcp): sanitize MCP tool schemas for Gemini function calling 2026-04-26 22:23:55 +02:00
SiYue-ZO ddf2d7c655 fix gateway boot signature after pico setup 2026-04-26 22:09:00 +08:00
SiYue-ZO cbe6a0907c feat: complete tool and model restart feedback 2026-04-26 22:09:00 +08:00
SiYue-ZO 02d9a0d190 feat: track channel and web search restart requirements 2026-04-26 22:09:00 +08:00
SiYue-ZO afc600baed feat: add config save and restart prompts 2026-04-26 22:09:00 +08:00
美電球 39dec35408 Merge pull request #2672 from lc6464/fix-tool-calls-thought-ui
feat(pico): add structured tool call support to web chat
2026-04-26 21:27:28 +08:00
lc6464 d6b38c4236 fix(chat): update tool_calls structure and ensure kind is always set 2026-04-26 20:13:13 +08:00
LC 1b9e7e32bd fix(chat): add \r? for regular expressions
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-26 19:59:55 +08:00
lc6464 1acab59fc7 fix(tests): format error message 2026-04-26 19:48:22 +08:00
lc6464 bfc37b784e fix(channels): bypass placeholder edits for thought and tool calls 2026-04-26 19:43:25 +08:00
Hoshina 4d6337fd26 fix runtime event logger reload and shutdown 2026-04-26 19:28:26 +08:00
Hoshina b3d9f86a01 feat(events): add configurable runtime event logging 2026-04-26 17:41:00 +08:00
Hoshina f4a24614b8 docs(events): remove stale subturn event names
Replace leftover SubTurnOrphanResultEvent and short subturn event references with runtime event kinds in comments, tests, and hook design notes.

Validation: GOCACHE=/tmp/picoclaw-go-cache go test ./pkg/agent -run TestSpawnSubTurn_OrphanResultRouting; make lint
2026-04-26 17:04:38 +08:00
Hoshina e613258fa5 feat(gateway): publish lifecycle runtime events
Emit gateway.start, gateway.ready, and gateway.shutdown on the shared runtime event bus, while keeping reload events on the same helper path.

Update subturn architecture docs to refer to runtime event kinds instead of the removed agent EventBus names.

Validation: GOCACHE=/tmp/picoclaw-go-cache go test ./pkg/gateway ./pkg/events; GOCACHE=/tmp/picoclaw-go-cache go test ./pkg/bus ./pkg/channels ./pkg/mcp ./pkg/tools/integration ./pkg/events ./pkg/gateway; make lint
2026-04-26 17:02:48 +08:00
Hoshina 795ee362ea refactor(events): emit agent runtime events directly
Remove the legacy EventKind/Event envelope mapping and let agent event emission build pkg/events.Event values directly.

Keep HookMeta as the shared hook metadata shape and preserve legacy observe string aliases by mapping them to runtime event kinds.

Validation: GOCACHE=/tmp/picoclaw-go-cache go test ./pkg/agent; make lint
2026-04-26 16:55:02 +08:00
Hoshina b954e6b8dc refactor(events): remove legacy agent event bus
Drop the old agent EventBus, SubscribeEvents/EventDrops public surface, legacy hook observer dispatch, and hook.event process notification path. Agent observations now flow through pkg/events runtime events.

Validation: go test ./pkg/agent; make lint
2026-04-26 16:39:35 +08:00
Hoshina fce800414d docs(events): align hook design with runtime observation
Mark the original hook design as an early record and update observer examples to pkg/events runtime events and hook.runtime_event.

Validation: make lint
2026-04-26 16:33:18 +08:00
Hoshina b2249df3ea refactor(events): split agent event payload types
Move agent domain event payload structs out of the legacy event envelope file so the remaining EventKind/Event/EventMeta compatibility layer can be removed independently later.

Validation: go test ./pkg/agent; make lint
2026-04-26 16:31:52 +08:00
Hoshina 6e8a81bfbf test(events): prefer runtime hook observation
Use RuntimeEventObserver for the normal in-process hook observer path and make the process-hook helper assert hook.runtime_event notifications.

Validation: go test ./pkg/agent; make lint
2026-04-26 16:28:41 +08:00
Hoshina dc80e8f5f2 test(events): migrate agent tests to runtime events
Move AgentLoop event assertions to the runtime event stream and keep the legacy SubscribeEvents test only for dual-publish compatibility.

Validation: go test ./pkg/agent; make lint
2026-04-26 16:23:58 +08:00
Hoshina d9717b5632 refactor(events): start runtime event consumer migration
Deprecate the legacy agent event APIs and add a runtime event test helper, then migrate the follow-up queued test to the runtime event stream.

Validation: go test ./pkg/agent; make lint
2026-04-26 16:11:09 +08:00
Hoshina 8caf9aeb2b feat(events): publish runtime service events
Migrate hook observation to runtime events and update the process hook notification protocol. Add runtime event publication for message bus failures, channel lifecycle/outbound flow, gateway reloads, MCP server state, and MCP tool calls.

Validation: go test ./pkg/events/... ./pkg/bus ./pkg/agent ./pkg/channels ./pkg/mcp ./pkg/tools/integration ./pkg/gateway; make lint
2026-04-26 16:05:10 +08:00
Hoshina eedebabbea feat(events): add runtime event bus
Introduce pkg/events with filtered channels, subscription policies, backpressure, and stats. Wire AgentLoop to dual-publish legacy agent events into runtime events while preserving old event APIs.

Validation: go test ./pkg/events/... ./pkg/agent; go test -race ./pkg/events/...; make lint
2026-04-26 15:36:03 +08:00
David Siewert f0dc709b17 fix(config): fix golines max-len for MaxLLMRetries field 2026-04-26 07:07:19 +06:00
David Siewert 4ddd650be4 align ToolFeedbackConfig field spacing 2026-04-26 07:06:36 +06:00
lc6464 9d42282672 fix(chat): tolerate animated legacy tool feedback parsing 2026-04-26 01:19:19 +08:00
David Siewert 9bc702ebaf fix test: enable pretty_print in tool feedback test 2026-04-25 23:09:40 +06:00
David Siewert 612097b411 fix(config): align gci formatting for LLM retry fields 2026-04-25 23:01:45 +06:00
lc6464 303ff8137d feat(chat): unify reasoning and tool call visibility 2026-04-26 00:50:18 +08:00
lc6464 6d04d15ce0 fix(tool-feedback): dedupe duplicate content and keep full explanations 2026-04-26 00:40:55 +08:00
David Siewert bdaff5cb69 Add pretty_print and disable_escape_html to tool_feedback defaults 2026-04-25 22:27:01 +06:00
David Siewert 1b2f8aac79 fix(config): align indentation for new LLM retry default fields 2026-04-25 22:12:41 +06:00
David Siewert 32c8b8ce6a chore(config): add default values for max_llm_retries and llm_retry_backoff_secs 2026-04-25 22:09:44 +06:00
David Siewert d2f6a08981 fix(config): align gci formatting for MaxLLMRetries field 2026-04-25 22:07:16 +06:00
lc6464 5cd10b594a feat(pico): add support for tool_calls in chat messages 2026-04-25 23:43:10 +08:00
David Siewert 3c4523e7aa test(agent): add unit tests for network error retry backoff strategy
- Test all network error types trigger retry (connection_reset, broken_pipe, read_tcp, eof, connection_refused)
- Test custom MaxLLMRetries and LLMRetryBackoffSecs config is respected
- Test retry count limit (1 initial + maxRetries retries)
- Add countingErrorProvider mock for deterministic call count verification
2026-04-25 21:19:13 +06:00
David Siewert fc89fea319 test(utils): add unit tests for FormatArgsJSON
Add tests for FormatArgsJSON covering:
- Default compact JSON output
- Pretty print formatting
- HTML escape disabling (preserves &&, <, >)
- Combined pretty print and escape disable
- Default HTML escaping behavior
- Nil args handling
2026-04-25 21:14:06 +06:00
David Siewert bcc3d447a1 feat(agent): add pretty_print and disable_escape_html options for tool feedback
- Add PrettyPrint and DisableEscapeHTML config options to ToolFeedbackConfig
- Add FormatArgsJSON helper function with configurable pretty printing and HTML escaping
- Add toolFeedbackArgsPreviewWithOptions to pass formatting options
- Update pipeline_execute.go to use new formatting options for tool feedback

This fixes the issue where '&&' would be displayed as '\u0026' in tool
feedback messages and provides optional pretty-printing for better
readability.
2026-04-25 20:46:16 +06:00
David Siewert 06fad95719 feat(agent): add network error retry with configurable max retries and backoff
- Add isNetworkError detection for connection reset, broken pipe, read/write tcp, EOF
- Add retry logic with configurable exponential backoff for network errors
- Add config options max_llm_retries and llm_retry_backoff_secs in agents.defaults
- Network errors now retry with backoff (was previously not retried)
- Timeout errors now use configurable backoff instead of hardcoded 5s
- Default: 2 retries with 2s backoff (3 total attempts)
2026-04-25 19:08:46 +06:00
美電球 77be169db4 Merge pull request #2654 from SiYue-ZO/fix/launcher-hide-windows-console-flash
fix(launcher): hide windows child-process console flashes
2026-04-25 18:00:50 +08:00
maxiaoyang 9f0f914ad7 Merge upstream/main into feat/delegate-tool
Resolves conflicts after the agent loop refactor on main:
- pkg/agent/loop.go was deleted upstream (logic split into agent.go,
  agent_init.go, pipeline.go, etc.); accepted the deletion.
- Moved the delegate tool registration block from the old loop.go
  into registerSharedTools in pkg/agent/agent_init.go, immediately
  after the spawn/spawn_status block. Logic and gating
  (len(registry.ListAgentIDs()) > 1) are unchanged.
- pkg/agent/subturn.go and pkg/agent/subturn_test.go merged cleanly
  on their own; TargetAgentID field, validation, registry lookup,
  and tests all preserved.

Verified locally:
- go build ./pkg/agent/... ./pkg/tools/...  clean
- go vet  clean
- TestDelegateTool* (17 cases) pass
- TestSpawnSubTurn_TargetAgentID_* (3 cases) pass
- TestDelegateToolRegistered_MultiAgent / _SingleAgent pass
- full pkg/agent + pkg/tools test suites green
2026-04-25 17:17:03 +08:00
美電球 726ef4fa99 Merge pull request #2661 from SiYue-ZO/feature/toggle-thought-visibility
feat: add thought visibility toggle
2026-04-25 17:15:49 +08:00
SiYue-ZO d784ec4611 feat: add thought visibility toggle 2026-04-25 17:08:37 +08:00
美電球 41f4d95597 Merge pull request #2657 from lc6464/fix-deepseek-v4-thinking-history
fix(reasoning): persist canonical history for DeepSeek and web chat
2026-04-25 15:08:48 +08:00
Mauro 04b62745e4 Merge pull request #2664 from afjcjsbx/fix/mcp-http-session-lifecycle
fix(mcp): retry tool calls on lost HTTP sessions and fix client lifec…
2026-04-25 08:51:28 +02:00
BeaconCat 78e4e59ac3 chore: update WeChat group QR code (#2667)
Co-authored-by: BeaconCat <BeaconCat@users.noreply.github.com>
2026-04-25 13:15:37 +08:00
lc6464 ae162a72b1 fix(message): ignore transient assistant thoughts in message count and history truncation 2026-04-25 12:26:28 +08:00
美電球 788f76f422 Merge pull request #2666 from afjcjsbx/fix/mcp-nil-arguments
fix(mcp): send empty object instead of null for tool
2026-04-25 11:38:48 +08:00
美電球 2f91cc0a80 Merge pull request #2660 from afjcjsbx/fix/tool-feedback-json-format
fix(tool-feedback): format tool args as JSON code blocks
2026-04-25 11:34:09 +08:00
美電球 93e9bddc6e Merge pull request #2659 from SiYue-ZO/fix/thought-bubble-collapse-state
fix: isolate thought bubble collapse state
2026-04-25 11:30:35 +08:00
美電球 caaad601af Merge pull request #2656 from alexhoshina/prompt-layering
Prompt layering
2026-04-25 11:22:21 +08:00
afjcjsbx 9d8f0dc877 fix(mcp): send empty object instead of null for tool 2026-04-24 21:24:29 +02:00
afjcjsbx 8f8af0874d fix(mcp): retry tool calls on lost HTTP sessions and fix client lifecycle 2026-04-24 20:20:57 +02:00
Hoshina 9ca73b944f fix(agent): preserve prompt hook and cache semantics 2026-04-25 01:25:17 +08:00
SiYue b4a5965602 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
2026-04-25 00:31:36 +08:00
afjcjsbx dce29c181f fix lint 2026-04-24 18:21:01 +02:00
afjcjsbx 94a6b0c0f5 fix(tool-feedback): format tool args as JSON code blocks 2026-04-24 18:07:48 +02:00
SiYue-ZO 683ce31f2b fix: isolate thought bubble collapse state 2026-04-24 23:58:42 +08:00
SiYue 494cc381b5 build(onboard): support codespace placeholder and path checks 2026-04-24 23:31:04 +08:00
SiYue e1863234f0 fix(launcher): hide windows child-process console flashes
- hide windows when launching gateway process from launcher

- hide windows for powershell/tasklist process inspection commands
2026-04-24 23:31:04 +08:00
SiYue a977a92729 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
2026-04-24 23:31:04 +08:00
SiYue 193e1a3cd0 Fix Windows build flow 2026-04-24 23:28:35 +08:00
SiYue f6bceb29a3 Fix Windows build flow 2026-04-24 23:27:59 +08:00
LC 979ff00cc3 fix(messageutil): remove dead code
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-04-24 22:16:18 +08:00
lc6464 bb0f983708 fix(reasoning): persist canonical history for DeepSeek and web chat 2026-04-24 21:45:41 +08:00
Hoshina 48d8952591 feat(agent): migrate tool prompts to capability slots 2026-04-24 19:36:46 +08:00
Mauro 8d51d306b3 Merge pull request #2641 from afjcjsbx/feat/mcp-cli
feat(mcp): add show, add, list, remove, test, edit cli commands
2026-04-24 13:06:34 +02:00
Hoshina 2e65b1be83 feat(agent): add structured prompt layering 2026-04-24 18:14:28 +08:00
肆月 ccd19a48ce 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
2026-04-24 17:50:59 +08:00
afjcjsbx 07032df037 fix(mcp): normalize local command paths and document env-file usage 2026-04-24 10:32:55 +02:00
BeaconCat f334ac6d01 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 <BeaconCat@users.noreply.github.com>
2026-04-24 15:26:34 +08:00
afjcjsbx f4dbac0dcf fix(mcp): expand home paths for local stdio server commands 2026-04-24 07:47:26 +02:00
Junghwan 293477b02a 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
2026-04-24 13:45:25 +08:00
dependabot[bot] 47a881b11f 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] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-24 12:05:08 +08:00
dependabot[bot] 743d7e69f2 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] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-24 12:04:47 +08:00
dependabot[bot] 047a904b4f 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] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-24 12:02:04 +08:00
dependabot[bot] 1dba8e9e91 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] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-24 11:59:54 +08:00
dependabot[bot] 73594a07ca 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] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-24 11:59:30 +08:00
dependabot[bot] ffd22c7fb6 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] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-24 11:55:19 +08:00
dependabot[bot] 39d7b3a63e 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] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-24 11:50:45 +08:00
Mauro 9fc72c1fb3 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
2026-04-24 11:49:41 +08:00
美電球 0d1b041d74 Merge pull request #2485 from afjcjsbx/fix/telegram-oauth-links
fix(telegram): preserve raw OAuth links in HTML rendering
2026-04-24 11:35:16 +08:00
BeaconCat 9fba52d0fa 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 <BeaconCat@users.noreply.github.com>
2026-04-24 09:53:07 +08:00
Mauro f440047263 Merge pull request #2640 from sipeed/dependabot/go_modules/github.com/aws/aws-sdk-go-v2/service/bedrockruntime-1.50.5
build(deps): bump github.com/aws/aws-sdk-go-v2/service/bedrockruntime from 1.50.4 to 1.50.5
2026-04-23 19:32:21 +02:00
afjcjsbx 2da05c2ad3 feat(mcp): add show, add, list, remove, test, edit cli commands 2026-04-23 19:20:15 +02:00
dependabot[bot] ac4db35c0b 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] <support@github.com>
2026-04-23 17:19:51 +00:00
美電球 0c0a582559 Merge pull request #2586 from kunalk16/fix-functions-deduplication
refactor(deduplication): functions deduplication in pkg/providers
2026-04-23 20:55:11 +08:00
wenjie cac4f21746 fix(tools): improve web search provider fallback (#2629)
- centralize web search provider readiness and resolution logic
- fall back when the configured provider is unavailable or invalid
- allow native-search-capable models to use built-in search without the client tool
- simplify the tools page and add direct access to web search settings
- add backend, agent, and integration tests for the new selection behavior
2026-04-23 15:39:16 +08:00
Kunal Karmakar 7616470137 Revert deduplication 2026-04-23 05:50:38 +00:00
Kunal Karmakar 4ae11406d2 Deduplicate further functions 2026-04-23 05:50:37 +00:00
Kunal Karmakar bc077db0ee Deduplicate ParseDataAudioURL function 2026-04-23 05:50:37 +00:00
Kunal Karmakar e901e70c14 Fix linting 2026-04-23 05:47:58 +00:00
Kunal Karmakar c71146b1d5 Functions deduplication 2026-04-23 05:47:58 +00:00
lxowalle 451db2f5d8 Feat(channels): unify animated tool feedback across chat channels and Pico (#2622)
* feat(channels): unify tool feedback animation across discord telegram and feishu

* fix(tool-feedback): unify fallback and single-message delivery

* fix(channels): finalize tool feedback in place

* fix ci

* feat: improve tool feedback

* fix review blockers in pico token cache and tool feedback

fix(provider): preserve function thought signatures

fix(feishu): recover tool feedback after edit fallback

* * delete dead code

* fix(pico): clean up tool feedback progress state

* fix ci

* fix(web): preserve tool feedback line breaks in chat

* fix(channels): preserve tool feedback progress state

fix(pico): preserve context usage when finalizing tool feedback

chore: record branch review pass

fix: preserve tool feedback finalization state

fix(web): handle pico history update fallback

* fix ci
2026-04-23 10:35:50 +08:00
Mauro 68ceb54b36 Merge pull request #2535 from afjcjsbx/feat/mcp-channel-commands
feat(commands): add MCP slash commands and tool details
2026-04-22 14:54:28 +02:00
wenjie f367a9c010 fix(web): use raw token for Pico media proxy and refresh chat attachment UI (#2618) 2026-04-22 15:14:20 +08:00
lxowalle 77b0c43392 refactor: support explicit provider field in model list entries (#2609)
* refactor: support explicit model list providers

* fix(web): preserve explicit model providers

* fix(web): preserve legacy provider prefixes on model updates

fix(models): normalize explicit provider-prefixed ids

fix(api): preserve legacy model updates across providers

fix(agent): preserve config identity for explicit provider refs

* fix ci
2026-04-22 11:28:47 +08:00
Mauro 3316ee6923 feat(web): download files on frontend (#2563)
* feat(web): download attachments in frontend

* fix: proxy pico media and force svg downloads

* feat(web): hide ephemeral media refs from persisted session history
2026-04-22 11:28:04 +08:00
Guoguo 023ca2e4c1 ci(release): split tag creation and release into separate workflows (#2614)
- Add `create-tag.yml`: creates annotated tag at a specified commit or
  latest main HEAD, with duplicate tag and commit validation
- Simplify `release.yml`: only accepts existing tags, removes create_tag
  toggle, validates tag via GitHub API before checkout
- Always checkout main branch (fetch-depth: 0 fetches full history),
  then create tag at the specified commit

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-22 10:07:14 +08:00
Mauro 279c496bb2 Merge pull request #2613 from sky5454/tmp_govet_fix
chore(isolation): fix govet shadow declaration of "err" shadows
2026-04-21 18:30:48 +02:00
sky5454 d0507df894 chore(isolation): fix govet shadow declaration of "err" shadows 2026-04-21 23:23:50 +08:00
wenjie 71c877a67f refactor(web): switch dashboard auth from tokens to passwords (#2608)
- replace token-based launcher auth with password-based login and sessions
- migrate legacy launcher_token values into bcrypt-backed password storage
- add one-shot local auto-login bootstrap
- update config UI, i18n strings, docs, and auth-related tests
2026-04-21 18:04:15 +08:00
肆月 a5379d5fff feat(feishu): Add group chat trigger and random emoji response frontend configuration (#2607)
- 添加 group_trigger.mention_only 开关配置(群聊仅提及时响应)
- 添加 random_reaction_emoji 数组配置(自定义表情回应列表)
- 更新中英文国际化翻译
2026-04-21 18:01:16 +08:00
afjcjsbx 175682f152 chore: refresh PR mergeability 2026-04-21 11:01:04 +02:00
afjcjsbx 5a13616b64 fix(mcp): surface MCP init failures to command handlers 2026-04-21 11:01:04 +02:00
afjcjsbx e5a6960078 fix lint 2026-04-21 11:01:04 +02:00
afjcjsbx 276f5425f0 feat(commands): add MCP slash commands and tool details 2026-04-21 11:01:04 +02:00
Guoguo 6ca7311273 feat(agent): add context usage ring indicator and /context command (#2537)
Add a context window usage indicator to the web chat UI and a /context
slash command that works across all channels.

Backend:
- Add computeContextUsage() estimating history + system + tool tokens
- Attach ContextUsage to outbound messages via the pico WebSocket protocol
- Add /context command showing context stats as formatted text
- Add EstimateSystemTokens() on ContextBuilder for system prompt estimation

Frontend:
- Add ContextUsageRing component (SVG ring + hover/tap popover)
- Show usage percentage, token counts, and compression threshold
- Hover on desktop (150ms leave delay), tap on mobile
- "View Details" sends /context with 1s cooldown
- i18n support (en/zh) for popover labels

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-21 16:30:02 +08:00
LC 9c3dc0ee3a fix(auth): canonicalize Google Antigravity provider and enhance credential management (#2599)
* fix(auth): canonicalize Google Antigravity provider and enhance credential management

* fix(auth): improve error handling in credential storage tests

* fix(auth): stabilize canonical provider merge precedence
2026-04-21 16:28:29 +08:00
LC b798fa4b7b docs: update documentation for Gemini native protocol (#2601)
* docs: update documentation for Gemini native protocol

* docs: fix capitalization and grammar of Gemini
2026-04-21 16:15:09 +08:00
wenjie ba6992234f feat(web): support list editing for channel array fields (#2595)
Add reusable channel array list controls and parsing utilities for channel forms.
Normalize channel string-array payloads in the backend, including pasted values,
numeric IDs, hidden characters, duplicates, and empty clears.
Also allow FlexibleStringSlice to unmarshal null values and cover the new behavior
with backend and config tests.
2026-04-21 16:04:28 +08:00
wenjie dcb4b67e00 fix(web): clean up restored chat transcripts and optimize chat UI (#2605)
Filter raw tool messages from session history and avoid duplicate summaries for visible message-tool output. Preserve final assistant replies after tool delivery and add coverage for visible transcript counts.

Also refine the chat UI with collapsible reasoning blocks, send shortcut hints, command-style user messages, stable scroll gutters, and updated i18n strings.
2026-04-21 11:52:58 +08:00
sky5454 329e68e017 refactor(agent): Agent Looper refactor phase2, restructure pipeline and rename loop files to agent (#2585)
* refactor(agent): introduce interfaces for MessageBus and ChannelManager

Phase 2 of loop.go refactor — dependency inversion using adapter pattern.

- Add interfaces.MessageBus and interfaces.ChannelManager interfaces
- Create adapters/messagebus.go wrapping *bus.MessageBus
- Create adapters/channelmanager.go wrapping *channels.Manager
- Update AgentLoop to use interfaces instead of concrete types
- Update registerSharedTools to accept interfaces.MessageBus

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(agent): restructure pipeline and rename loop files

Pipeline refactoring:
- Split pipeline.go (1400 lines) into focused files:
  - pipeline_setup.go (~115 lines): SetupTurn method
  - pipeline_llm.go (~519 lines): CallLLM method
  - pipeline_execute.go (~693 lines): ExecuteTools method
  - pipeline_finalize.go (~78 lines): Finalize method
- Pipeline struct and NewPipeline remain in pipeline.go (~39 lines)

Agent file renaming:
- Rename loop_*.go to agent_*.go for consistent naming:
  - loop.go -> agent.go, loop_message.go -> agent_message.go, etc.
- Merge turn.go + turn_exec.go into turn_state.go
- Rename loop_turn.go -> turn_coord.go

Documentation:
- Update docs/pipeline-restructuring-plan.md
- Add docs/agent-rename-plan.md

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(agent): code format  fixed

* refactor(agent): code test file added/renamed

* docs(agent): update agent refactor docs

* fix(agent): fix agent hardAbortX

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-21 10:55:50 +08:00
Guoguo 4e2f80b79a docs: update wechat qrcode (#2604) 2026-04-21 09:37:31 +08:00
lxowalle 6421f146a9 Revert "Feat/channel tool feedback animation (#2569)" (#2596)
This reverts commit e556a816e4.
2026-04-20 18:30:29 +08:00
lxowalle e556a816e4 Feat/channel tool feedback animation (#2569)
* feat(channels): unify tool feedback animation across discord telegram and feishu

* fix(tool-feedback): unify fallback and single-message delivery

* fix(channels): finalize tool feedback in place

* fix ci

* feat: improve tool feedback
2026-04-20 15:20:26 +08:00
wenjie 8461c996e5 chore(web): update linting and router dependencies (#2592)
Bump TanStack Router, ESLint, React Hooks plugin, TypeScript ESLint, and Prettier packages. Disable the react-hooks/set-state-in-effect rule in the frontend ESLint config.
2026-04-20 11:18:42 +08:00
Guoguo 74c98a5acf refactor(web): secure Pico websocket access behind launcher auth (#2545)
* refactor(web): secure Pico websocket access behind launcher auth

- stop exposing the raw Pico token to the frontend
- add /api/pico/info for non-secret Pico connection metadata
- proxy /pico/ws through the launcher with same-origin and dashboard auth checks
- inject the upstream Pico websocket protocol server-side
- update frontend chat connection flow and Vite websocket proxy path
- refresh related docs and tests

* fix(web): improve Pico URL and origin handling behind proxies

- read client scheme from X-Forwarded-Proto and RFC 7239 Forwarded
- derive client-visible ports from forwarded host information
- add coverage for HTTPS origins without explicit ports
- verify behavior when proxies omit forwarded protocol headers

* fix(web): stop pinning Pico WebSocket origins during setup

- remove request-origin seeding from `EnsurePicoChannel`
- keep `allow_origins` empty by default for auto-configured Pico channels
- relax launcher Pico WebSocket proxy origin validation
- update Pico backend tests for the new setup and proxy behavior
2026-04-20 11:17:42 +08:00
wenjie f8190f04b7 fix(web): stop pinning Pico WebSocket origins during setup
- remove request-origin seeding from `EnsurePicoChannel`
- keep `allow_origins` empty by default for auto-configured Pico channels
- relax launcher Pico WebSocket proxy origin validation
- update Pico backend tests for the new setup and proxy behavior
2026-04-20 10:11:03 +08:00
wenjie d002e1517b fix(web): improve Pico URL and origin handling behind proxies
- read client scheme from X-Forwarded-Proto and RFC 7239 Forwarded
- derive client-visible ports from forwarded host information
- add coverage for HTTPS origins without explicit ports
- verify behavior when proxies omit forwarded protocol headers
2026-04-20 10:11:03 +08:00
wenjie 4b76196e2c refactor(web): secure Pico websocket access behind launcher auth
- stop exposing the raw Pico token to the frontend
- add /api/pico/info for non-secret Pico connection metadata
- proxy /pico/ws through the launcher with same-origin and dashboard auth checks
- inject the upstream Pico websocket protocol server-side
- update frontend chat connection flow and Vite websocket proxy path
- refresh related docs and tests
2026-04-20 10:11:03 +08:00
美電球 6126ede963 Merge pull request #2566 from lc6464/refactor/providers-tools-layout
refactor(providers,tools): reorganize packages and facades
2026-04-17 22:51:49 +08:00
美電球 9fe678247f docs: add session and routing documentation (#2571) 2026-04-17 21:25:18 +08:00
Guoguo 15a3560533 docs: reorganize docs by type and add layout guidance (#2567)
* refactor(docs): reorganize docs by type and locale

* chore(docs): add docs layout lint target and contributor guidance

Introduce a lint-docs script and Makefile target for common
documentation naming and placement checks. Expand docs/README.md
with layout and translation conventions, and update CONTRIBUTING.md
to point contributors to the new docs guidance and validation step.

* docs: add section index pages and fix localized doc links

- add reader navigation to docs/README.md
- add index pages for guides, reference, operations, security, architecture, and migration
- update localized project README links to prefer existing translated docs

* docs: fix broken wecom link in Malay README
2026-04-17 18:51:07 +08:00
wenjie 2708c834d0 build(deps): patch gomarkdown and upgrade shadcn (#2568) 2026-04-17 15:40:23 +08:00
lc6464 743cd3602b fix(tools): centralize shared LLM note constants 2026-04-17 14:31:43 +08:00
lc6464 9b4efddd9b fix(providers,tools): address linter issues after reorg 2026-04-17 14:16:18 +08:00
wenjie 16d174e124 docs: fix broken wecom link in Malay README 2026-04-17 14:05:57 +08:00
wenjie 610f68adcf docs: add section index pages and fix localized doc links
- add reader navigation to docs/README.md
- add index pages for guides, reference, operations, security, architecture, and migration
- update localized project README links to prefer existing translated docs
2026-04-17 14:00:45 +08:00
wenjie de3d042d1b chore(docs): add docs layout lint target and contributor guidance
Introduce a lint-docs script and Makefile target for common
documentation naming and placement checks. Expand docs/README.md
with layout and translation conventions, and update CONTRIBUTING.md
to point contributors to the new docs guidance and validation step.
2026-04-17 14:00:45 +08:00
wenjie 4e1ceee62e refactor(docs): reorganize docs by type and locale 2026-04-17 14:00:45 +08:00
lc6464 4c133dc2d9 refactor(tools): reorganize tool packages and facades 2026-04-17 13:44:31 +08:00
daming大铭 0da962c4b4 Merge pull request #2564 from sky5454/looper_refactor
Looper refactor: Split the monolithic 4384-line `loop.go` into 12 focused source files as Phase 1 of the agent refactor.
2026-04-17 13:18:11 +08:00
lc6464 ee634dc8db refactor(providers): reorganize provider packages and facades 2026-04-17 12:42:03 +08:00
sky5454 b0d3f19a6a docs(agent-refactor): document loop.go file split
Add loop-split.md explaining the 12-file split of the original
4384-line loop.go, covering the file map, extraction method,
and future phase 2 plans.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-17 11:22:37 +08:00
sky5454 12d5421c26 refactor(agent): split loop.go into focused sub-packages
Break up the monolithic 4384-line loop.go into 12 focused files:
- loop.go: core AgentLoop struct and main Run loop
- loop_turn.go: turn execution logic (runTurn, askSideQuestion, etc.)
- loop_utils.go: pure utility functions (formatters, helpers)
- loop_init.go: constructor and tool registration
- loop_message.go: message handling (processMessage, routing)
- loop_command.go: command processing (/use, /btw, etc.)
- loop_mcp.go: MCP runtime management
- loop_event.go: event/hook system helpers
- loop_media.go: media resolution and artifact handling
- loop_outbound.go: response publishing
- loop_transcribe.go: audio transcription
- loop_steering.go: steering queue and continuation
- loop_inject.go: setter injection methods

No functional changes - pure code movement with updated imports.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-17 11:17:12 +08:00
dependabot[bot] 72f30c58e9 build(deps-dev): bump @types/node from 25.5.0 to 25.6.0 in /web/frontend (#2562)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.5.0 to 25.6.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.6.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-17 10:30:11 +08:00
dependabot[bot] 235cb11beb build(deps-dev): bump globals from 17.4.0 to 17.5.0 in /web/frontend (#2561)
Bumps [globals](https://github.com/sindresorhus/globals) from 17.4.0 to 17.5.0.
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v17.4.0...v17.5.0)

---
updated-dependencies:
- dependency-name: globals
  dependency-version: 17.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-17 10:24:18 +08:00
dependabot[bot] 74856d3747 build(deps): bump @tanstack/react-query in /web/frontend (#2560)
Bumps [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) from 5.97.0 to 5.99.0.
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query@5.99.0/packages/react-query)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.99.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-17 10:23:07 +08:00
dependabot[bot] c36a48cf4b build(deps): bump react-i18next from 17.0.2 to 17.0.3 in /web/frontend (#2559)
Bumps [react-i18next](https://github.com/i18next/react-i18next) from 17.0.2 to 17.0.3.
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/react-i18next/compare/v17.0.2...v17.0.3)

---
updated-dependencies:
- dependency-name: react-i18next
  dependency-version: 17.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-17 10:20:32 +08:00
dependabot[bot] e77c4eba3e build(deps): bump maunium.net/go/mautrix from 0.26.4 to 0.27.0 (#2557)
Bumps [maunium.net/go/mautrix](https://github.com/mautrix/go) from 0.26.4 to 0.27.0.
- [Release notes](https://github.com/mautrix/go/releases)
- [Changelog](https://github.com/mautrix/go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mautrix/go/compare/v0.26.4...v0.27.0)

---
updated-dependencies:
- dependency-name: maunium.net/go/mautrix
  dependency-version: 0.27.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-17 10:14:08 +08:00
dependabot[bot] d73897da8e build(deps): bump @tanstack/react-router in /web/frontend (#2555)
Bumps [@tanstack/react-router](https://github.com/TanStack/router/tree/HEAD/packages/react-router) from 1.168.8 to 1.168.22.
- [Release notes](https://github.com/TanStack/router/releases)
- [Changelog](https://github.com/TanStack/router/blob/main/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/TanStack/router/commits/@tanstack/react-router@1.168.22/packages/react-router)

---
updated-dependencies:
- dependency-name: "@tanstack/react-router"
  dependency-version: 1.168.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-17 10:11:47 +08:00
dependabot[bot] 9c97442f7c build(deps): bump go.mau.fi/util from 0.9.7 to 0.9.8 (#2553)
Bumps [go.mau.fi/util](https://github.com/mautrix/go-util) from 0.9.7 to 0.9.8.
- [Release notes](https://github.com/mautrix/go-util/releases)
- [Changelog](https://github.com/mautrix/go-util/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mautrix/go-util/compare/v0.9.7...v0.9.8)

---
updated-dependencies:
- dependency-name: go.mau.fi/util
  dependency-version: 0.9.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-17 10:09:16 +08:00
dependabot[bot] 6375440152 build(deps): bump pnpm/action-setup from 4 to 6 (#2552)
Bumps [pnpm/action-setup](https://github.com/pnpm/action-setup) from 4 to 6.
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/v4...v6)

---
updated-dependencies:
- dependency-name: pnpm/action-setup
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-17 10:07:10 +08:00
daming大铭 928a27359f Merge pull request #2549 from lc6464/gateway-auth-no-browser
feat(auth): add no-browser option for OAuth login
2026-04-17 09:54:03 +08:00
daming大铭 ba08d52351 Merge pull request #2474 from srcrs/fix-cron-independent-sessions
fix(cron): make each job execution use an independent session
2026-04-16 23:52:29 +08:00
daming大铭 b1475122da Merge pull request #2547 from lc6464/chore/issue-2538-network-fallback
feat(network): improve network error classification and fallback handling
2026-04-16 23:17:20 +08:00
lc6464 ffd30d7db7 fix(auth): improve no-browser OAuth login 2026-04-16 23:01:28 +08:00
daming大铭 eb24269651 Merge pull request #2503 from cytown/loop
refactor: make agent loop support parallel and update docs
2026-04-16 22:47:34 +08:00
lc6464 2b844778ff refactor(tests): extract common logic for fallback error handling into a helper function 2026-04-16 22:45:31 +08:00
lc6464 ab019d3f18 feat(auth): add no-browser option for OAuth login 2026-04-16 22:19:34 +08:00
LC 7aa2d672ce fix(network): classify timeout errors as FailoverTimeout
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-04-16 22:00:13 +08:00
lc6464 c3f4000817 feat(network): implement network error classification and fallback handling 2026-04-16 19:59:37 +08:00
wenjie 7fdc9c7b64 fix(web): support proxies in SearXNG and web fetch (#2542)
Propagate the configured HTTP client and proxy settings to the
SearXNG search provider.

Allow web_fetch to connect to the configured proxy as the first hop
without bypassing the existing private-host checks for redirect
targets and fetched URLs.

Add tests for loopback proxy fetches and SearXNG proxy propagation.
2026-04-16 17:15:47 +08:00
wenjie 7f56ca8cc6 feat(web): refactor tools page into tabbed library and web search settings (#2539)
- split the tools page into focused components and a shared hook
- add separate Tool Library and Web Search tabs
- refresh web search settings layout and localized copy
- make provider expansion keyboard accessible
- restore wrapping for long tool names in library cards
- allow custom styling for KeyInput
2026-04-16 17:14:35 +08:00
Cytown f5e779e22e refactor: make agent loop support parallel and update docs 2026-04-16 14:43:15 +08:00
lxowalle e22b4e1eee feat(agent): support btw side questions (#2532) 2026-04-16 10:53:09 +08:00
wenjie a8d0b03515 fix(web): save channel configs with nested channel_list patches (#2530)
Persist channel settings through the current channel_list schema, keeping common
channel fields at the top level and channel-specific fields under settings.
Return common fields and default config shapes from channel config endpoints, and
add coverage for nested patches, missing channel defaults, and secret handling.
2026-04-16 10:30:16 +08:00
wenjie f32b303d2a fix(web): avoid resetting web search draft on config refetch (#2536) 2026-04-16 10:26:18 +08:00
xiaoen a34120b821 test(agent): assert child turn uses target agent model
Replace generic mockProvider with modelRecordingProvider that captures
the model parameter passed to Chat(). After delegation from alpha to
beta, assert the recorded model is "model-beta" — proving the child
turn actually ran with the target agent's configuration, not the
caller's.

Also add wiring tests:
- TestDelegateToolNotRegistered_SingleAgent: single-agent has no
  delegate in its tool registry
- TestDelegateToolRegistered_MultiAgent: both agents in a two-agent
  setup have the delegate tool

Ref: #2148
2026-04-15 22:27:05 +08:00
xiaoen 6ee66123f2 refactor(agent): simplify delegate registration gate
Remove the IsToolEnabled("delegate") check — there is no "delegate"
entry in ToolsConfig, so the check was always true. The only real
gate is len(agents) > 1, which is the intended behavior: delegate
is auto-registered in multi-agent setups.

Ref: #2148
2026-04-15 22:24:47 +08:00
xiaoen 6db17b8211 test(tools): verify normalization prevents self-delegation bypass
Add table-driven test with case and whitespace variants (ALPHA,
" Alpha ", "  alpha  ") that should all be caught by the self-check
after normalization.

Ref: #2148
2026-04-15 22:23:47 +08:00
xiaoen df486b9939 fix(tools): normalize agent_id before self-check and delegation
Apply routing.NormalizeAgentID to the raw agent_id input before any
logic runs. This prevents case/whitespace variants like "ALPHA" or
" alpha " from bypassing the self-delegation guard while still
resolving to the same agent in the registry.

The normalized value is used consistently for self-check, allowlist,
SpawnSubTurn, and result attribution.

Ref: #2148
2026-04-15 22:23:17 +08:00
ex-takashima 5b0c9e2708 Merge remote-tracking branch 'origin/main' into refactor/line-sdk
# Conflicts:
#	pkg/channels/line/line.go
2026-04-15 23:07:04 +09:00
xiaoen 039f35563e feat(agent): wire delegate tool registration for multi-agent setups
Register the delegate tool in registerSharedTools when multiple agents
are configured. Gated independently from the subagent tool — delegate
uses SubTurn directly and does not depend on SubagentManager.

Self-delegation is prevented by injecting the current agent ID.
Permission is enforced via CanSpawnSubagent (reuses allow_agents config).

Single-agent setups are unaffected: the tool is not registered when
only one agent exists in the registry.

Ref: #2148
2026-04-15 21:29:29 +08:00
xiaoen 0ff78fa53f test(tools): add delegate tool unit tests
12 test cases covering:
- success path with result attribution
- agent_id validation (missing, empty, whitespace, wrong type)
- task validation (missing, empty, whitespace)
- permission denied / allowed via allowlist checker
- self-delegation blocked
- nil spawner, spawner error, nil result from spawner
- open access when no allowlist checker is set

Ref: #2148
2026-04-15 21:28:54 +08:00
xiaoen 484ef399f1 feat(tools): add delegate tool for synchronous cross-agent task handoff
delegate(agent_id, task) hands off a task to a named agent and blocks
until the result is ready. The target agent runs with its own config
via the TargetAgentID mechanism in SubTurnConfig.

Key behaviors:
- Self-delegation explicitly rejected
- Permission gated by subagents.allow_agents (D4)
- Spawner errors preserve the underlying error via WithError
- Nil result from spawner handled gracefully
- Response attributed with target agent ID

Ref: #2148
2026-04-15 21:28:31 +08:00
xiaoen c8335bfd47 test(agent): verify TargetAgentID resolves to correct agent instance
Add multi-agent test setup (newMultiAgentLoop) with two agents using
distinct models (model-alpha, model-beta).

Three new tests:
- UsesTargetAgent: parent=alpha delegates to beta, event log confirms
  child runs as agent_id=beta with model=model-beta
- NotFound: TargetAgentID pointing to nonexistent agent returns error
- EmptyModelAccepted: empty Model field accepted when TargetAgentID
  provides the model implicitly

Ref: #2148
2026-04-15 21:27:39 +08:00
xiaoen c47f5fd2c4 feat(agent): add TargetAgentID to SubTurnConfig for cross-agent delegation
When TargetAgentID is set, spawnSubTurn resolves the target AgentInstance
from the registry and uses it as the base for the child turn. This gives
the child turn the target's workspace, model, tools, and system prompt
instead of inheriting from the caller.

Model validation is relaxed: empty Model is accepted when TargetAgentID
provides the model implicitly via the resolved agent instance.

Ref: #2148
2026-04-15 21:27:13 +08:00
BeaconCat f1b659e5ef membench: add LLM-as-Judge evaluation mode (#2484)
* membench: add LLM-as-Judge evaluation mode

Add --eval-mode=llm to membench for LLM-based answer generation and
semantic scoring via an OpenAI-compatible API endpoint.

New files:
- llm_client.go: generic OpenAI-compatible chat completion client
  with support for API key, configurable timeout, and optional
  chat_template_kwargs (for llama.cpp thinking models)
- eval_llm.go: LLM answer generation + LLM-as-Judge scoring for
  both legacy and seahorse retrieval modes

Changes to main.go:
- --eval-mode flag (token|llm) to select evaluation strategy
- --api-base, --api-key, --model flags with env var fallback
  (MEMBENCH_API_BASE, MEMBENCH_API_KEY, MEMBENCH_MODEL)
- --no-thinking flag for llama.cpp + Qwen thinking models
- --limit flag to cap QA questions per sample for quick testing

* style: fix golangci-lint formatting (gofmt + golines)

* fix: address Copilot review feedback

- Validate --model is required for LLM eval mode
- Use rune-based truncation to preserve valid UTF-8
- Precompute totalQA count outside inner loop
- Log SearchMessages errors instead of silently skipping

* fix: address Copilot review round 2

- Validate --eval-mode accepts only 'token' or 'llm'
- Normalize base URL to avoid /v1/v1 duplication
- Separate token/LLM results for correct PrintComparison labeling
- Log ExpandMessages errors instead of silently ignoring
- Short-circuit with 0 scores when no context retrieved (match token eval)
- Add --timeout flag wired to LLMClientOptions.Timeout

* fix: address review P1+P2 — sort alignment, failure sentinel, score parser

- P1: Replace hand-rolled sortByRank with sort.Slice (ascending, best
  first) matching eval.go's EvalSeahorse — ensures BudgetTruncate keeps
  best-ranked messages when truncation occurs
- P2: Use -1.0 sentinel for LLM API failures and parse errors, distinct
  from genuine 0.0 score; aggregateMetrics skips -1.0 entries for F1
  averaging while still counting HitRate
- P2: Use regexp \b([1-5])\b for judge score extraction instead of
  first-digit scan — avoids misparses on '5/5', 'Score: 3' etc.

* fix: address Copilot review round 2

- Fix F1/HitRate weighted aggregation: track ValidF1Count separately so
  computeModeAgg weights F1 by valid scores only, not TotalQuestions
- No-context retrieval failure uses 0.0 (genuine bad score) instead of
  -1.0 sentinel (reserved for API/parse failures)
- Validate --timeout > 0 to prevent disabling HTTP timeouts

* fix: remove hardcoded /v1 from API base URL

Users now provide the full versioned path in --api-base (e.g. /v1, /v4).
Code only appends /chat/completions. Default changed to
http://127.0.0.1:8080/v1 for backward compatibility.

* fix: address Copilot review round 3

- ValidF1Count=0 when all scores are sentinel (no forced =1)
- Backward compat: old eval JSON without ValidF1Count falls back to
  TotalQuestions in computeModeAgg
- Skip empty section in PrintComparison when tokenResults is empty
- Update --api-base flag help to document /v1 default and version path
- Add sentinel aggregation unit tests (partial, all, weighted)

* feat: add --retries flag with exponential backoff for transient LLM errors

Retry on timeout, 5xx, and 429 (rate limit) with 1s/2s/4s backoff.
Default 3 retries, configurable via --retries. Context cancellation
is respected between retries.

* fix: address Copilot review round 4

- runReport splits results by mode suffix into token/llm for PrintComparison
- backward compat fallback (ValidF1Count=0 -> TotalQuestions) only for
  non-LLM modes; LLM modes keep ValidF1Count=0 when all scores sentinel
- MaxRetries==0 means no retry; only negative falls back to default 3
- truncateStr uses []rune to avoid cutting multi-byte UTF-8 characters
- Complete() returns error on empty LLM response (vs silent empty string)

* feat: --no-thinking adapts to llama.cpp, Ollama, and GLM backends

Send all three disable-thinking fields simultaneously:
- chat_template_kwargs.enable_thinking=false (llama.cpp, GLM)
- think=false (Ollama 0.9+)
- thinking.type=disabled (GLM/Zhipu)
Each backend picks the field it recognizes and ignores the rest.
Also bumps max_tokens from 512 to 2048 for thinking models.

* feat: mixed model eval + concurrent QA workers

- Add --judge-model, --judge-api-base, --judge-api-key flags for separate judge model
- Add --concurrency flag (default 1) with semaphore-based goroutine pool
- Add reasoning_content fallback for GLM/DeepSeek style responses
- Prepend /no_think to system prompt for Ollama /v1 compatibility
- Reduce default MaxTokens from 2048 to 512 (answers are 1-3 sentences)
- Extract evalQAWorker and buildSeahorseContext for shared concurrent logic

---------

Co-authored-by: BeaconCat <BeaconCat@users.noreply.github.com>
2026-04-15 21:15:17 +08:00
美電球 ead2dc9699 Merge pull request #2524 from SiYue-ZO/feature/sogou-web-search-default
Add configurable Sogou-backed web search
2026-04-15 20:50:53 +08:00
wenjie 7bd11181a6 fix(agent): preserve reused tool call IDs across turns (#2528)
Scope tool result deduplication to each assistant tool-call block so providers
that reuse call IDs across separate turns do not lose valid tool results. Also
drop invalid empty tool call IDs and orphaned tool messages after validation.
2026-04-15 20:18:09 +08:00
daming大铭 100e576609 Merge pull request #2529 from lc6464/feat/web-code-highlight
feat(web): add markdown syntax highlighting for chat and skills
2026-04-15 18:50:40 +08:00
SiYue-ZO 2784223ad5 Make web search auto-switch with UI language
Default the sample web search provider to auto, route Sogou vs DuckDuckGo dynamically based on query/UI language, and sync frontend language changes back to the backend so Current Service and runtime selection stay aligned.
2026-04-15 18:45:28 +08:00
lc6464 5a2e7795cd refactor(web): improve theme style element management in useHighlightTheme hook 2026-04-15 18:30:43 +08:00
lc6464 acbe654674 chore(web): move app providers out of main entry 2026-04-15 17:36:22 +08:00
lc6464 389f492d8c refactor(web): use official highlight themes for markdown 2026-04-15 17:19:48 +08:00
lc6464 25ac563406 feat(web): add syntax highlighting for markdown code blocks 2026-04-15 14:54:13 +08:00
Mauro bb14a5c7cc Merge pull request #2525 from afjcjsbx/fix/vision-unsupported-media-stuck
fix(agent): recover after image-input-unsupported failures
2026-04-15 07:54:33 +02:00
SiYue-ZO bb953b788b test(api): fix web tools lint issues 2026-04-15 13:35:39 +08:00
SiYue-ZO 75e93b5189 Merge remote-tracking branch 'upstream/main' into feature/sogou-web-search-default
# Conflicts:
#	pkg/tools/web.go
#	pkg/tools/web_test.go
2026-04-15 13:28:05 +08:00
SiYue-ZO 0b84f0ae0a fix(web): address sogou search review feedback 2026-04-15 13:03:06 +08:00
Cytown d0ff24aa87 remove useless backend output for platform-token (#2500) 2026-04-15 11:38:47 +08:00
wenjie 51ab3b1385 fix(web): restore chat composer disabled-state messaging and clean up code (#2526) 2026-04-15 11:24:27 +08:00
lxowalle 773a94c414 fix(web_search): validate missing API key/URL directly in Search methods (#2517) 2026-04-15 09:55:05 +08:00
肆月 bf6d4fd997 feat(web): show disabled reasons in tooltips when buttons are disabled (#2430)
* feat(web): show disabled reasons in tooltips when buttons are disabled

- Add disabled reason tooltips for model card actions (set default, delete)
- Add disabled reason tooltips for marketplace skill card install button
- Add disabled reason display for chat input when disabled
- Add internationalization support for all disabled reasons (en/zh)
- Model card: Show specific reasons when set-default or delete buttons are disabled
- Marketplace skill card: Show specific reasons when install button is disabled
- Chat composer: Show reason text below input when input is disabled

* fix: show disabled action reasons via tooltips

* fix(web): restore accessible labels for model action tooltips
2026-04-15 09:49:45 +08:00
afjcjsbx e60a687387 fix lint 2026-04-14 22:35:02 +02:00
afjcjsbx 7824bc715f add test 2026-04-14 22:31:30 +02:00
afjcjsbx d3d639cb7d fix lint 2026-04-14 22:21:33 +02:00
afjcjsbx 1245f2ddf6 fix(agent): recover after image-input-unsupported failures 2026-04-14 22:15:28 +02:00
srcrs d8e7a6129f fix(cron): add blank line between default and localmodule imports for gci
gci linter requires a blank line separating import sections (default vs
localmodule). Missing separator caused CI failure.
2026-04-15 02:07:35 +08:00
美電球 c0fadc5918 Merge pull request #2523 from lc6464/feat/web-chat-disabled-reasons-hint
feat(web): show disabled chat reasons in composer
2026-04-15 00:42:55 +08:00
美電球 b52eb58f03 Merge pull request #2514 from lc6464/fix/issue-2488-host-binding
feat(launcher): add host overrides for launcher and gateway
2026-04-14 23:48:24 +08:00
lc6464 0bb9bedc44 fix(web): address latest Copilot review points 2026-04-14 23:39:59 +08:00
SiYue-ZO dcf21ef11c Fix provider return formatting for golines 2026-04-14 23:26:40 +08:00
lc6464 79f87d151e fix(web): show localhost entry only for local binds 2026-04-14 23:24:14 +08:00
SiYue-ZO 824e800d70 Fix Sogou user agent formatting for linter 2026-04-14 23:22:37 +08:00
SiYue-ZO 9ded7933f0 Fix golines formatting for web search changes 2026-04-14 23:16:23 +08:00
SiYue-ZO 93977bf348 Add configurable Sogou-backed web search 2026-04-14 22:58:07 +08:00
lc6464 d4313b5e5f feat(web): show disabled chat reasons in composer 2026-04-14 22:22:30 +08:00
Caize Wu 08fc305d5e Merge pull request #2518 from imguoguo/update-wechat-qr
docs: update wechat qrcode
2026-04-14 17:34:06 +08:00
Guoguo 8ca89c49ab docs: update wechat qrcode 2026-04-14 02:30:26 -07:00
lc6464 24382271d6 fix(web): align wildcard advertise IP preference 2026-04-14 15:17:27 +08:00
lxowalle 0425cd4d77 refactor skills registries and add GitHub-backed skill discovery (#2442)
* refactor skills registries and add GitHub-backed skill discovery

* fix ci

* fix command error

* fix default skills install registry behavior

* fix github registry URL parsing and versioned skill links

* fix skills registry config compatibility and URL installs

* * fix lint

* fix deprecated github base url compatibility

* fix skills registry yaml and github default branch handling

* fix github skills registry fallback and install metadata

* fix cli skills install origin metadata

* fix clawhub registry env compatibility

* fix skills registry config merge compatibility

* fix skill install metadata consistency and onboard template copy

* fix yaml overrides for default skills registries

* fix install_skill registry metadata normalization

* fix github skill URL parsing for slash branch names

* fix skills registry install/search validation and github URLs

* fix github skill URL host validation

* fix install_skill validation for invalid registry archives

* fix redundant skills registry names in saved config

* fix github blob skill URL installs and metadata links

* fix github registry URL scheme validation

* fix v0 skills migration preserving github registry defaults

* fix github blob skill install directory resolution

* fix install_skill rollback on origin metadata write failure

* fix github skill URL validation and registry JSON merging

* fix github registry target resolution and metadata links

* fix install_skill force reinstall rollback

* fix skills config compatibility and legacy security overlays

* fix ci
2026-04-14 15:14:16 +08:00
lc6464 ae195831bb fix: resolve PR2514 lint regressions 2026-04-14 14:49:23 +08:00
lc6464 93bf871bd2 fix(launcher): refine console host display 2026-04-14 14:04:37 +08:00
lc6464 d4d652b455 feat(host): complete launcher and gateway multi-host binding support
- add shared netbind planning for strict tcp4/tcp6 bind semantics
- support launcher/gateway host env overrides and launcher-to-gateway forwarding
- cover host binding and forwarding with network and subprocess env tests
2026-04-14 14:04:36 +08:00
lc6464 7b38d437ba feat(launcher): support multi-host bind and strict host semantics 2026-04-14 14:03:24 +08:00
lc6464 e7b3654313 fix(host): modernize default host selection order 2026-04-14 14:03:23 +08:00
lc6464 448027c02a fix(host): align launcher and gateway host normalization semantics 2026-04-14 14:03:22 +08:00
lc6464 4e977367c2 feat(launcher): add host overrides for launcher and gateway 2026-04-14 14:00:54 +08:00
daming大铭 df9124b824 Merge pull request #2249 from alexhoshina/refactor-inbound-context-routing-session
Refactor inbound context routing session
2026-04-14 12:45:34 +08:00
美電球 08283dde61 Merge pull request #2489 from afjcjsbx/fix/mcp-reload-discovery-tools
fix(agent): reinitialize MCP and discovery tools after reload
2026-04-14 11:54:47 +08:00
wenjie f82fe5a2ec ci: use pnpm/action-setup and sync README install steps (#2512)
* ci(workflows): use pnpm/action-setup in build and release pipelines

Replace the corepack-based pnpm setup with pnpm/action-setup
and pin pnpm to v10.33.0 in the create_dmg, nightly, and
release GitHub Actions workflows.

* docs(readme): update pnpm setup instructions across translated READMEs
2026-04-14 10:44:47 +08:00
wenjie 64c3542b91 fix(updater): retry release fetches (#2511) 2026-04-14 10:44:21 +08:00
Hoshina 93f69a98ba merge: integrate main channel config changes 2026-04-14 00:34:17 +08:00
美電球 04e99a1264 Merge pull request #2508 from cytown/channel2
fix some bugs:
2026-04-14 00:19:15 +08:00
Cytown f16bade919 fix some bugs:
Fix hiddenValues in manager_channel.go — use comma-ok type assertions to avoid panics                               │
  Add GetDecoded() error handling in weixin.go saveWeixinConfig for consistency with wecom.go                         │
  Fix stray quotes in docs/configuration.md JSON examples                                                             │
  Add V2→V3 migration section to docs/config-versioning.md
  Fix feishu init with 32bit wrong signature cause build fail
2026-04-14 00:15:35 +08:00
daming大铭 cbd38dfd28 Merge pull request #2481 from cytown/channel
refactor(config):  make config.Channel to multiple instance support
2026-04-13 23:41:32 +08:00
美電球 aa1d7c55be Merge pull request #2507 from cytown/allow
bug fix for allowFrom contains empty string
2026-04-13 23:39:54 +08:00
Cytown 036f65b179 bug fix for allowFrom contains empty string 2026-04-13 23:34:44 +08:00
Hoshina 69ff6909e1 merge: integrate main seahorse context changes 2026-04-13 23:02:38 +08:00
Hoshina c5c5ea22d6 fix(session): address review regressions 2026-04-13 22:51:44 +08:00
daming大铭 7db2e7d579 Merge pull request #2495 from liuy/feat/seahorse-clear
feat(agent): /clear clears seahorse DB
2026-04-13 22:28:33 +08:00
Cytown 667fc85d54 refactor(config): make config.Channel to multiple instance support
add new field type to Channel struct
config.channels refactor to channel_list
update config version to 3
update the docs
2026-04-13 22:21:21 +08:00
taonyx 2e149f44dd Merge pull request #2497 from wj-xiao/build/split-core-builds
build(release): move Android bundle publishing into GoReleaser
2026-04-13 14:25:17 +08:00
Hoshina 0c6ad33a9c merge: integrate main into refactor-inbound-context-routing-session 2026-04-13 13:25:07 +08:00
Hoshina 0f23535165 fix(runtime): address session promotion and steering regressions 2026-04-13 12:35:27 +08:00
wenjie 6a870cb260 ci(build): remove unused Node.js and pnpm setup from core build workflow 2026-04-13 11:56:43 +08:00
wenjie d73a0e89b4 build(release): move Android bundle publishing into GoReleaser
- build the Android universal bundle from GoReleaser hooks
- attach the bundle as a release asset
- remove the separate post-release upload step
- simplify Make targets around cross-platform builds
2026-04-13 11:52:35 +08:00
Liu Yuan 4532627f71 test(seahorse): add TestTriggerMigration for old-DB trigger upgrade path
Verifies that databases created with the old buggy FTS5 DELETE trigger
body are correctly migrated by runSchema: the old trigger causes DELETE
to fail, and after re-running runSchema (which drops and recreates the
triggers with the corrected body) DELETE works normally.
2026-04-13 11:37:50 +08:00
Liu Yuan b8819bdbff fix(seahorse): drop/recreate FTS5 triggers so existing DBs get corrected bodies
`CREATE TRIGGER IF NOT EXISTS` does not replace an existing trigger body.
On databases created with the old (buggy) DELETE-FROM-FTS syntax, the
bad trigger body persisted after code updates. Now we explicitly DROP
each trigger before CREATE, so any existing DB gets the corrected body
on next startup — no manual DB deletion required.
2026-04-13 11:29:02 +08:00
wenjie ea2107e8a9 build(release): split core builds from release-only artifacts
- add a dedicated build-release-artifacts target for Android bundle packaging
- switch CI and release workflows to Corepack-managed pnpm with cache support
- pin the frontend pnpm version and make dependency installs deterministic
- inject version metadata into launcher binaries in GoReleaser
- update build documentation to reflect the new workflow
2026-04-13 11:23:55 +08:00
Liu Yuan f7e768152e feat(agent): /clear now clears seahorse DB in addition to JSONL
- Add Clear(ctx, sessionKey) to ContextManager interface
- Implement Clear for legacy (JSONL) and seahorse (DB + JSONL)
- Add Engine.ClearSession + Store.ClearConversation
- Fix FTS5 DELETE trigger syntax in schema (was using wrong
  external-content FTS5 syntax; now uses standard DELETE FROM)
- Fix ClearSession to skip sessions never ingested (was creating
  blank conversations record via GetOrCreateConversation)
- Simplify summary_parents DELETE into single OR statement
- Add TestStoreClearConversation unit test
2026-04-13 11:04:45 +08:00
Guoguo 2b2bc26f8e docs: fix Conventional Commits links in CONTRIBUTING files (#2494)
- CONTRIBUTING.md: change link from zh-hans to en locale
- CONTRIBUTING.zh.md: fix NBSP causing surrounding text to be absorbed into the link
- Both files now use proper markdown link syntax
2026-04-13 10:46:17 +08:00
afjcjsbx 815e43e3ef fix(agent): reinitialize MCP and discovery tools after reload 2026-04-12 21:37:19 +02:00
daming大铭 6d03791929 Merge pull request #2475 from lc6464/fix/issue-2448-separate-thought-message
feat(gemini,pico): separate thought messages and add native Gemini provider
2026-04-12 19:20:19 +08:00
daming大铭 18d35c7d5d Merge pull request #2486 from sky5454/main
build: add Android arm64 cross-compile support
2026-04-12 18:58:37 +08:00
sky5454 681b2a258b build: address PR review — fix Android launcher flags, systray tag, rename target 2026-04-12 18:50:52 +08:00
dataCenter430 b6617a4b17 feat(cli): structured terminal UI for PicoClaw CLI like modern CLIs (#2229)
* feat(cli): add boxed help/error UI with no-color support

* fix: CI testing error

* fix: lint errors

* fix linter error

* fix: address review
2026-04-12 18:44:24 +08:00
sky5454 168b6bec58 build(android): ci build added 2026-04-12 18:35:05 +08:00
sky5454 080f532d82 build: add Android arm64 cross-compile support
- Add build-android-arm64, build-launcher-android-arm64, build-all-android
  targets to Makefile and web/Makefile
- Use -tags stdjson (no goolm) for Android; CGO_ENABLED=0 throughout
- Output staged as build/android-staging/arm64-v8a/libpicoclaw{,-web}.so
  for JNI consumption; zip packaging handled by CI
- Exclude Matrix channel from android builds (channel_matrix.go) to avoid
  modernc.org/sqlite CGO dependency
- Exclude systray from android builds; use headless stub instead
  (systray.go / systray_stub_nocgo.go)
2026-04-12 17:41:10 +08:00
afjcjsbx 34b9d5d6fa fix(telegram): preserve raw OAuth links in HTML rendering 2026-04-12 10:44:09 +02:00
srcrs 2b73978c5f fix(cron): add agent: prefix to session key so resolveScopeKey preserves it
Cron session keys "agent:cron-{id}-{uuid}" were being silently ignored by
resolveScopeKey, which only recognizes keys prefixed with "agent:". This
caused multiple executions of the same job to share a session. Also
switch from timestamp to UUID to avoid collisions in concurrent scenarios.
2026-04-11 23:27:07 +08:00
lc6464 6fbd7e0a3f fix(gemini): align thoughtSignature and stream tool IDs 2026-04-11 12:02:58 +08:00
lc6464 e9f55d776d fix(review): address copilot backpressure and SSE parse feedback 2026-04-11 11:18:41 +08:00
lc6464 86917faa9b fix(ci): resolve lint header casing and fallback test routing 2026-04-11 02:23:35 +08:00
lc6464 b73caebe6f fix(chat): improve thought readability in dark mode 2026-04-11 01:44:39 +08:00
lc6464 cbae69ad64 fix(gemini): honor pro-model thinking constraints 2026-04-11 01:38:13 +08:00
lc6464 83e93ca572 fix(gemini): align thinking-off and system prompt semantics 2026-04-11 01:15:38 +08:00
lc6464 459e78c076 fix(gemini): harden dedicated provider compatibility 2026-04-11 00:50:24 +08:00
srcrs 36b9693d31 fix(cron): make each job execution use an independent session
Previously all executions of the same cron job reused the session key
"cron-{jobID}", causing conversation history to accumulate across runs.
Now each run gets a unique key "cron-{jobID}-{timestamp}", preventing
cross-execution interference.
2026-04-10 23:49:31 +08:00
lc6464 c8bac699fe fix(pico): separate thought and normal messages 2026-04-10 20:23:12 +08:00
Guoguo 748ac58dd1 fix(chat): keep tool-call summary and assistant output in sync (#2449)
* fix(chat): keep tool summaries and assistant output together

* fix(pico): stream assistant text between tool calls

* fix(pico): avoid duplicate final websocket message

* fix(review): align tool feedback reconstruction with runtime behavior

* style(lint): satisfy gci and golines for review fixes

* fix(agent): gate pico interim publish for internal turns
2026-04-10 15:08:30 +08:00
winterfx 187189ad4a fix(seahorse): sanitize user input for FTS5 MATCH queries (#2436)
User input containing FTS5 operators (-, +, *, OR, NOT, :, quotes,
parentheses) could cause query errors or unexpected search results.
Wrap each token in double quotes to force literal matching while
preserving user-quoted phrases.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 11:59:50 +08:00
wenjie d9977715a3 fix(launcher): align react and react-dom versions (#2467)
Pin react and react-dom to 19.2.5 to avoid runtime crashes caused by a version mismatch.
Refresh the pnpm lockfile to keep frontend dependencies in sync.
2026-04-10 11:13:05 +08:00
wenjie 795ec9af05 fix(launcher): fall back to token auth on unsupported platforms (#2466)
Handle platforms where the dashboard password store is unavailable
by treating legacy token auth as initialized, rejecting password
setup, and adding platform-specific store stubs and tests.
2026-04-10 11:12:54 +08:00
dependabot[bot] 7788ed4677 build(deps): bump github.com/modelcontextprotocol/go-sdk (#2455)
Bumps [github.com/modelcontextprotocol/go-sdk](https://github.com/modelcontextprotocol/go-sdk) from 1.4.1 to 1.5.0.
- [Release notes](https://github.com/modelcontextprotocol/go-sdk/releases)
- [Commits](https://github.com/modelcontextprotocol/go-sdk/compare/v1.4.1...v1.5.0)

---
updated-dependencies:
- dependency-name: github.com/modelcontextprotocol/go-sdk
  dependency-version: 1.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-10 10:46:45 +08:00
dependabot[bot] e58f00b0c1 build(deps): bump shadcn from 4.1.2 to 4.2.0 in /web/frontend (#2459)
Bumps [shadcn](https://github.com/shadcn-ui/ui/tree/HEAD/packages/shadcn) from 4.1.2 to 4.2.0.
- [Release notes](https://github.com/shadcn-ui/ui/releases)
- [Changelog](https://github.com/shadcn-ui/ui/blob/main/packages/shadcn/CHANGELOG.md)
- [Commits](https://github.com/shadcn-ui/ui/commits/shadcn@4.2.0/packages/shadcn)

---
updated-dependencies:
- dependency-name: shadcn
  dependency-version: 4.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-10 10:28:03 +08:00
dependabot[bot] f1fe2db7ac build(deps): bump @tanstack/react-query in /web/frontend (#2458)
Bumps [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) from 5.96.1 to 5.97.0.
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query@5.97.0/packages/react-query)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.97.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-10 10:27:18 +08:00
dependabot[bot] 19493140eb build(deps): bump react from 19.2.4 to 19.2.5 in /web/frontend (#2456)
Bumps [react](https://github.com/facebook/react/tree/HEAD/packages/react) from 19.2.4 to 19.2.5.
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.5/packages/react)

---
updated-dependencies:
- dependency-name: react
  dependency-version: 19.2.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-10 10:19:39 +08:00
dependabot[bot] c6d15da1ea build(deps): bump golang.org/x/sys from 0.42.0 to 0.43.0 (#2450)
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.42.0 to 0.43.0.
- [Commits](https://github.com/golang/sys/compare/v0.42.0...v0.43.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sys
  dependency-version: 0.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-10 10:18:25 +08:00
dependabot[bot] 484070736d build(deps): bump jotai from 2.19.0 to 2.19.1 in /web/frontend (#2452)
Bumps [jotai](https://github.com/pmndrs/jotai) from 2.19.0 to 2.19.1.
- [Release notes](https://github.com/pmndrs/jotai/releases)
- [Commits](https://github.com/pmndrs/jotai/compare/v2.19.0...v2.19.1)

---
updated-dependencies:
- dependency-name: jotai
  dependency-version: 2.19.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-10 10:13:42 +08:00
dependabot[bot] 0e57a446dc build(deps-dev): bump vite from 8.0.3 to 8.0.8 in /web/frontend (#2451)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.0.3 to 8.0.8.
- [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.8/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-10 10:13:13 +08:00
Mauro 491418775b fix(gateway): log startup errors before exit (#2414)
* fix(gateway): log startup errors before exit

* preserve deferred startup failure logging
2026-04-10 10:10:45 +08:00
Mauro 282ebcd956 Merge pull request #2457 from sipeed/dependabot/go_modules/modernc.org/sqlite-1.48.2
build(deps): bump modernc.org/sqlite from 1.48.0 to 1.48.2
2026-04-09 22:14:55 +02:00
Mauro dde61365d4 Merge pull request #2420 from lahuman/docs/tool-escape-semantics
docs(tool): use provider-agnostic JSON escaping guidance
2026-04-09 20:50:06 +02:00
Mauro d7d4374617 Merge pull request #2453 from sipeed/dependabot/go_modules/github.com/aws/aws-sdk-go-v2/config-1.32.14
build(deps): bump github.com/aws/aws-sdk-go-v2/config from 1.32.12 to 1.32.14
2026-04-09 20:42:54 +02:00
Mauro d03d519c6d Merge pull request #2454 from sipeed/dependabot/go_modules/github.com/mymmrac/telego-1.8.0
build(deps): bump github.com/mymmrac/telego from 1.7.0 to 1.8.0
2026-04-09 20:42:07 +02:00
dependabot[bot] 919e9eb645 build(deps): bump modernc.org/sqlite from 1.48.0 to 1.48.2
Bumps [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) from 1.48.0 to 1.48.2.
- [Changelog](https://gitlab.com/cznic/sqlite/blob/master/CHANGELOG.md)
- [Commits](https://gitlab.com/cznic/sqlite/compare/v1.48.0...v1.48.2)

---
updated-dependencies:
- dependency-name: modernc.org/sqlite
  dependency-version: 1.48.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-09 17:18:28 +00:00
dependabot[bot] 01a33bbb61 build(deps): bump github.com/mymmrac/telego from 1.7.0 to 1.8.0
Bumps [github.com/mymmrac/telego](https://github.com/mymmrac/telego) from 1.7.0 to 1.8.0.
- [Release notes](https://github.com/mymmrac/telego/releases)
- [Commits](https://github.com/mymmrac/telego/compare/v1.7.0...v1.8.0)

---
updated-dependencies:
- dependency-name: github.com/mymmrac/telego
  dependency-version: 1.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-09 17:18:19 +00:00
dependabot[bot] c71cd1eede build(deps): bump github.com/aws/aws-sdk-go-v2/config
Bumps [github.com/aws/aws-sdk-go-v2/config](https://github.com/aws/aws-sdk-go-v2) from 1.32.12 to 1.32.14.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/config/v1.32.12...config/v1.32.14)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2/config
  dependency-version: 1.32.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-09 17:18:14 +00:00
lc6464 bd88385923 fix(agent): gate pico interim publish for internal turns 2026-04-10 00:19:45 +08:00
lc6464 58f634b582 style(lint): satisfy gci and golines for review fixes 2026-04-10 00:02:20 +08:00
lc6464 bd13092831 fix(review): align tool feedback reconstruction with runtime behavior 2026-04-09 23:52:02 +08:00
lc6464 9982ee29a8 fix(pico): avoid duplicate final websocket message 2026-04-09 22:59:36 +08:00
lc6464 2aeed8fb3a fix(pico): stream assistant text between tool calls 2026-04-09 22:32:35 +08:00
lc6464 5b596ed2f0 fix(chat): keep tool summaries and assistant output together 2026-04-09 22:16:36 +08:00
Mauro 20d3522069 Merge pull request #2418 from lahuman/docs/korean-readme
docs: add Korean README translation
2026-04-09 11:25:15 +02:00
Guoguo 5e44a99410 fix(docker): run self-built images as root for parity with release (#2435)
The self-built docker/Dockerfile and docker/Dockerfile.heavy created a
dedicated picoclaw user (uid 1000) and stored config at
/home/picoclaw/.picoclaw, while the released images from
Dockerfile.goreleaser (and Dockerfile.full) run as root at
/root/.picoclaw. Both docker-compose files mount ./data:/root/.picoclaw,
so self-built images silently broke when used with the shared compose.

Drop the picoclaw user switch and align both Dockerfiles on root +
/root/.picoclaw. Dockerfile also adopts the release entrypoint.sh so
first-run behavior matches between self-built and release tags.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 10:53:52 +08:00
wenjie a9720daa45 fix(test): skip TestPrepareCommand_AppliesUserEnv on unsupported operating systems (#2434) 2026-04-09 10:14:08 +08:00
k a2f02e4b18 Revert "test(agent): remove unused respondWithMediaHook field"
This reverts commit 087e355885.
2026-04-09 07:47:42 +09:00
sky5454 06023c79fa feat(launcher): standard HTTP login/setup/logout flow for dashboard, frontend and backend impl. and fix windows pid lock for ws (#2339)
* feat(launcher): replace token-in-logs auth with standard HTTP login flow

## Problem

Previously users had to find the one-time token from console logs or
log files to access the dashboard - a non-standard, error-prone workflow
with no clear path for changing credentials.

## Solution: standard HTTP API login with bcrypt-backed password store

### Auth flow (new)
1. First run: browser opens, session guard detects uninitialized state,
   redirects to /launcher-setup
2. User sets a password (min 8 chars) via POST /api/auth/setup {password, confirm},
   bcrypt(cost=12) hash stored in ~/.picoclaw/launcher-auth.db (SQLite)
3. Subsequent logins: POST /api/auth/login {password}, HttpOnly cookie
   picoclaw_launcher_auth (HMAC-SHA256 signed, 7-day expiry)
4. 401 on any API call, frontend redirects to /launcher-login
5. Logout: POST /api/auth/logout, cookie cleared, redirect to login

### Backend changes
- web/backend/api/auth.go: renamed Token to Password; added handleSetup;
  launcherAuthStatusResponse now includes Initialized bool; PasswordStore
  interface wires bcrypt store into handlers
- web/backend/dashboardauth/: new package - Store with New(dir) / Open(path);
  SetPassword (bcrypt cost=12), VerifyPassword, IsInitialized
  - sql.go: all DB-layer constants (DBFilename, sqliteDriver, bcryptCost,
    four SQL query strings) - compile-time constants, zero runtime overhead
- web/backend/middleware/launcher_dashboard_auth.go: /launcher-setup and
  /api/auth/setup added to public paths
- web/backend/main.go:
  - dashboardauth.New(picoHome) replaces manual path construction
  - maskSecret(): suffix only revealed when >=5 chars hidden (length >= 12),
    preventing 8-char minimum passwords from leaking their tail
- web/backend/main_test.go: TestMaskSecret updated with boundary cases

### Forward-compatibility: pkg/credential integration

If the dashboard password is later reused as the enc:// passphrase,
the bcrypt hash in launcher-auth.db becomes an offline oracle.
Recommended mitigation (not yet implemented): derive two independent
subkeys via HKDF before use:

  bcrypt(HKDF(password, info="picoclaw-dashboard-login-v1"))  stored in DB
  HKDF(password, info="picoclaw-credential-enc-v1")           passed to PassphraseProvider

This isolates the two domains: cracking the bcrypt hash yields only the
login subkey, which is computationally independent of the enc:// subkey.

* fix(auth): replace wastedassign ok := false with var ok bool

* refactor(tray): remove copy-token clipboard feature

Dashboard login now uses standard web auth (bcrypt + session cookie).
The system tray 'Copy dashboard token' menu item is no longer needed.

- Delete tray_offers_copy.go and tray_offers_copy_stub.go
- Remove mCopyTok menu item and clipboard handler from systray.go
- Remove launcherDashboardTokenForClipboard var from main.go
- Remove MenuCopyToken/MenuCopyTokenHint keys from i18n.go

* feat(launcher-ui): standard HTTP login/setup/logout flow for dashboard

Replaces the previous "find token in logs" workflow with a proper
browser-based authentication UI backed by the new /api/auth/* endpoints.

### New pages
- /launcher-setup: first-run password initialization form (password +
  confirm, min 8 chars); calls POST /api/auth/setup; redirects to login
  on success
- /launcher-login: standard password login form; calls POST /api/auth/login;
  sets HttpOnly session cookie on success

### Session guard (src/routes/__root.tsx)
A useEffect on every non-auth page load calls GET /api/auth/status:
- initialized=false  -> redirect to /launcher-setup
- authenticated=false -> redirect to /launcher-login
This ensures the setup/login UI is shown even when the ?token= URL
mechanism auto-logs in (first-run case).

### Logout button (src/components/app-header.tsx)
IconLogout button added to the header with a confirm AlertDialog;
calls POST /api/auth/logout then redirects to /launcher-login.

### API layer
- src/api/launcher-auth.ts: LauncherAuthStatus gains initialized bool;
  postLauncherDashboardSetup() added; LauncherAuthTokenHelp removed
- src/api/http.ts: 401 guard uses isLauncherAuthPathname() (covers both
  /launcher-login and /launcher-setup) to prevent redirect loops
- src/lib/launcher-login-path.ts: isLauncherSetupPathname() and
  isLauncherAuthPathname() added

### Routing
- src/routeTree.gen.ts: /launcher-setup route registered throughout
- src/routes/launcher-login.tsx: tokenHelp UI removed; useEffect added
  to redirect to setup when initialized=false

### i18n
- en.json / zh.json: launcherSetup block added; launcherLogin keys
  updated to use passwordLabel/passwordPlaceholder

* fix(lint): ts lint fixed 1

* fix(auth): detail auth error handle

* fix(login):  frontend web auth error handle

* fix(frontend): auth error handler 5xx
2026-04-08 21:43:51 +08:00
美電球 3e3b6aed90 fix(tools): message tool no longer suppresses reply to originating chat (#2180)
When the message tool sent to a different chat (e.g., a group), the
agent's final response to the originating chat was incorrectly skipped
because HasSentInRound() was a simple bool that didn't distinguish
targets. Replace with HasSentTo(channel, chatID) that tracks all
send targets per round and only suppresses when the target matches.

Fixes cross-conversation message causing "Processing..." to hang.
2026-04-08 21:40:12 +08:00
k 087e355885 test(agent): remove unused respondWithMediaHook field 2026-04-08 19:53:11 +09:00
k 1dc25e7cf5 test(agent): remove unused respondWithMediaHook field 2026-04-08 19:44:07 +09:00
lxowalle 51eecde01e Feat/support isolation (#2423)
* * completed

* * optimzie

* * fix format

* * fix pr check

* try to fix ci

* * Indicates that Windows does not support expos_paths, adding more mount paths for the Linux platform.

* fix isolation startup lifecycle and MCP transport wrapping

* fix isolation startup cleanup and optional Linux mounts

* fix isolation path handling for relative hooks

Preserve relative command and working-directory semantics when Linux isolation wraps subprocesses, and restore absolute argv path exposure to avoid startup regressions. Add hook coverage and docs updates so isolation-enabled process hooks keep working as configured.

* * fix ci
2026-04-08 18:15:42 +08:00
ywj 8b3e502690 fix(feishu): enrich reply context for card and file replies (#2144)
* fix(feishu): enrich reply context for card and file replies

* refactor(feishu): extract reply functions to feishu_reply.go

- Move reply-related functions to new feishu_reply.go
- Move corresponding tests to feishu_reply_test.go
- Extract magic number 600 to maxReplyContextLen constant
- Unify replyTargetID/replyTargetFromMessage (prefer parent_id, fallback root_id)
- Add source comment for containsFeishuUpgradePlaceholder

* fix(feishu): skip API fallback for non-thread messages, prepend replied media refs

- resolveReplyTargetMessageID: only call fetchMessageByID fallback when
  ThreadId is set, avoiding unnecessary API calls for non-reply messages
- prependReplyContext: prepend replied media refs before current media refs
  to maintain correct ordering

* fix(feishu): add message cache for fetchMessageByID to avoid repeated downloads

- Add messageCache (sync.Map) to FeishuChannel struct
- Cache fetched messages with 30s TTL to avoid re-downloading attachments
  when multiple users reply to the same parent message in a thread
- Cleanup expired entries on read access (no background goroutine needed)

* fix(feishu): early-return for non-reply messages, add cache and fetchMessageByID comment

* fix: remove duplicate test and fix gci import order

* fix(feishu): remove duplicate prependReplyContext call
2026-04-08 14:26:17 +08:00
wenjie 7d16764674 fix(gateway): validate PID ownership and clean stale pid files (#2422)
* fix(gateway): validate PID ownership and clean stale pid files

- include `pid` in health responses for runtime PID verification
- add `RemovePidFileIfPID` to safely delete PID files only on PID match
- sanitize gateway PID data via process-command checks with health fallback
- ignore and remove stale/non-gateway PID files before gateway operations
- refuse stop/restart actions when the attached process is not a gateway
- update gateway and websocket tests to cover PID validation and safety paths

* test(seahorse): use shared in-memory SQLite DB in tests to fix async compaction failures

* test: remove unused sendMediaErr field from hook test mock
2026-04-08 14:23:21 +08:00
k 8f7eae8b37 docs(tool): use provider-agnostic JSON escaping guidance 2026-04-08 14:19:11 +09:00
k 862421b146 docs: add Korean README translation 2026-04-08 13:42:57 +09:00
Harmoon ee29aaa871 Enhance hooks with respond action and comprehensive documentation (#2215)
* feat(hooks): add respond action for tool execution bypass

Add a new HookActionRespond that allows hooks to return tool results directly, skipping actual tool execution. This enables plugin tool injection, caching, and mocking capabilities.

- Add HookActionRespond constant and support in HookManager
- Extend ToolCallHookRequest with HookResult field
- Implement respond action handling in process hooks and agent loop
- Add comprehensive tests for respond and deny_tool actions
- Update documentation with hook actions table and examples

* docs(hooks): add JSON-RPC protocol and plugin tool injection documentation

Add comprehensive documentation for hook JSON-RPC protocol and plugin tool injection capabilities:

- Add "Hook Actions" section to README.zh.md explaining respond action for tool execution bypass
- Create hook-json-protocol.md/.zh.md detailing JSON-RPC 2.0 protocol for all hook methods
- Create plugin-tool-injection.md/.zh.md with complete examples for external tool implementation
- Document how hooks can inject tool definitions and return results via respond action
- Include Python and Go examples for weather query plugin implementation

* feat(agent): emit tool events and feedback for hook results

Add ToolExecStart event emission and tool feedback for hook results to ensure consistent behavior between normal tool execution and hook bypass scenarios. This maintains parity in event tracking and user feedback when tools are executed via hooks.

* style(agent): format whitespace in hook structs and constants

Remove trailing whitespace and standardize spacing in JSON struct tags, constants, and test data for improved code consistency.

* feat(hooks): add media support for plugin tool injection

Extend the hook respond action to support media file handling:
- Add `media` field for returning images and files from hooks
- Add `response_handled` field to control turn completion behavior
- When response_handled=true, media is automatically delivered to user
- When response_handled=false, media is passed to LLM for vision requests

This enables plugins to directly return generated images, downloaded
files, and other media content either to users or for LLM analysis.

* docs(hooks): document security implications of respond action

Add security boundary documentation explaining that the respond action
bypasses ApproveTool checks, allowing hooks to return results for any
tool without approval. Include recommendations for secure hook
implementation and code comments marking the security considerations.

Changes:
- Add "Security Boundaries" section to plugin-tool-injection docs
- Document bypass of approval checks and associated risks
- Provide security recommendations and example code
- Add inline security comments in hooks.go and loop.go

* refactor(agent): improve completeness of tool result cloning and hook processing

Extend cloneToolResult to properly copy ArtifactTags and Messages fields,
ensuring deep copies of all ToolResult data. Consolidate event emission
and user message handling to match the normal tool execution flow.

* fix(agent): align hook respond path with normal tool execution flow

The hook respond code path was missing several critical behaviors that
existed in normal tool execution:

- Add logging for tool calls with arguments preview
- Add is_tool_call metadata to user-facing messages
- Handle attachment delivery failures by setting error state and
  notifying LLM
- Set ResponseHandled=false when using bus for media delivery
- Check for steering messages and graceful interrupts after tool
  execution, skipping remaining tools when appropriate
- Poll for SubTurn results that arrived during tool execution

This ensures consistent behavior between hook-responded tool calls and
normally executed tool calls.

* test(agent): add tests for hook respond media error handling

Add comprehensive tests for the hook respond code path when media
delivery fails. Tests cover error media channel scenarios and verify
proper error state handling.

Also document that AfterTool is not called when using respond action,
as it provides the final answer directly (design decision).
2026-04-08 11:47:02 +08:00
wenjie 330de0c382 fix(agent): disable seahorse context manager on freebsd/arm (#2417)
* fix(agent): disable seahorse context manager on freebsd/arm

Exclude freebsd/arm from the seahorse-enabled build and route it to the
unsupported stub implementation.

This avoids freebsd/arm build failures caused by modernc sqlite/libc while
keeping picoclaw buildable on that target.

* build: bump Go version from 1.25.8 to 1.25.9

* ci: install and run govulncheck directly in PR workflow
2026-04-08 10:57:22 +08:00
Hoshina 296077eabf fix(session): restore thread and legacy compatibility 2026-04-08 00:32:53 +08:00
ex-takashima fe51cd504f refactor(line): use official LINE Bot SDK v8
Replace hand-rolled HTTP/HMAC/JSON code (~270 lines) with the official
line-bot-sdk-go v8, reducing maintenance burden and eliminating potential
bugs in signature verification, request construction, and response parsing.

This continues the work started in #500 by @xiaket, addressing all review
feedback and rebasing onto current main.

Changes:
- Replace bytes/crypto/json/io imports with line-bot-sdk-go/v8
- Use webhook.ParseRequest for body reading + signature verification
- Use messaging_api.MessagingApiAPI for ReplyMessage/PushMessage/ShowLoadingAnimation/GetBotInfo
- Type-switch on webhook.MessageEvent message types (TextMessageContent,
  ImageMessageContent, etc.) instead of JSON unmarshalling
- Type-switch on webhook.SourceInterface (UserSource/GroupSource/RoomSource)
- Type-switch on webhook.Mentionee (UserMentionee/AllMentionee)

Review feedback addressed (from #500):
- Use WithContext(ctx) on all SDK calls to preserve cancellation/timeout
- Fix variable shadowing of isMentioned (declared at function scope)
- Remove reflect-based message ID extraction (use type switch + msg.Id)
- Use mentionee.IsSelf for cleaner bot mention detection
- Preserve body size security check via http.MaxBytesReader before
  webhook.ParseRequest (compatible with #1413)

All existing tests pass without modification.
2026-04-08 00:38:55 +09:00
Hoshina a827d01d7c test(channels): normalize manager outbound test message 2026-04-07 23:09:26 +08:00
Hoshina 27db03e5ca fix(config): migrate legacy bindings and optimize session resolve 2026-04-07 22:57:10 +08:00
Hoshina 3d60385958 refactor(session): tighten legacy boundary and tool context 2026-04-07 22:39:46 +08:00
Hoshina 9f23ec22d6 refactor(agent): normalize dispatch and outbound turn metadata 2026-04-07 22:12:23 +08:00
Hoshina e32a209683 Merge branch 'main' into refactor-inbound-context-routing-session
# Conflicts:
#	pkg/agent/eventbus_test.go
#	pkg/agent/loop.go
#	pkg/bus/bus.go
#	pkg/bus/types.go
#	pkg/channels/pico/pico.go
#	pkg/channels/telegram/telegram.go
#	pkg/config/config.go
#	web/backend/api/session.go
#	web/backend/api/session_test.go
2026-04-07 21:41:02 +08:00
Hoshina 528c57dda0 refactor(channels): merge non-web fixes from main 2026-04-07 21:19:11 +08:00
Hoshina e6e724a827 refactor(config): reconcile defaults with main 2026-04-07 21:19:06 +08:00
Hoshina 718a5e7c75 refactor(runtime): merge bus context and handled tool delivery 2026-04-07 21:05:53 +08:00
corevibe555 6ce0306c66 fix: use per candidate provider for model_fallbacks (#2143)
* fix: use per-candidate provider for model_fallbacks

Each fallback model now uses its own api_base and api_key from
model_list instead of inheriting the primary model's provider config.

Previously, a single LLMProvider was created from the primary model's
ModelConfig and reused for all fallback candidates — only the model ID
string was swapped. This caused all fallback requests to be routed to
the primary provider's endpoint, making cross-provider fallback chains
non-functional (e.g., OpenRouter primary with Gemini fallback would
send the Gemini request to OpenRouter's API).

Fix: pre-create a per-candidate LLMProvider at agent initialization
time by looking up each candidate's ModelConfig from model_list. The
fallback run closure now selects the correct provider per candidate
via CandidateProviders map, falling back to agent.Provider when no
override is found.

Fixes #2140

Made-with: Cursor

test: add test for instance.go

fix: fix test

refactor: optimize

fix: fix Golang lint issues

chore: comment cleanup

* refactor: use resolvedModelConfig() instead of buildModelIndex()

* fix
2026-04-07 20:07:56 +08:00
Andy Lo-A-Foe 1fc2710999 feat(channels): add teams_webhook output-only channel (#2244)
Add Microsoft Teams webhook integration via Power Automate workflows.

Features:
- Output-only channel for sending notifications to Teams
- Multiple webhook targets with named configuration
- Required "default" target with automatic fallback
- Rich Adaptive Card formatting with full-width rendering
- Markdown table conversion to native Adaptive Card Tables
- Column widths based on header content length
- HTTPS-only webhook URL validation
- Proper error classification for retry behavior

Configuration:
- channels.teams_webhook.enabled: bool
- channels.teams_webhook.webhooks: map of named targets
  - Each target has webhook_url (SecureString) and optional title

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-07 19:24:27 +08:00
Guoguo 6a8552a664 fix(web): derive WebSocket URL from browser location instead of backend (#2405)
The frontend previously used ws_url returned by /api/pico/token, which
is built from the launcher's own port. Behind a reverse proxy this can
produce incorrect URLs (e.g. ws://localhost:18800 instead of the
proxy's public address).

Since the launcher already proxies /pico/ws on the same port, the
frontend can simply use window.location.host to construct the
WebSocket URL, which is always correct regardless of proxy layers.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 17:37:01 +08:00
wenjie 7bf6cbe1fa fix(gateway): harden PID liveness handling and websocket proxy state (#2403)
- treat `EPERM` from `signal(0)` as “process exists” on Unix
- classify malformed PID files as invalid and auto-remove them during read
- keep cached `pidData` only for transient races and downgrade `running` to `stopped` when the tracked process is gone
- refresh PID data on WebSocket proxy requests and reject stale cached gateway state
- add regression tests for invalid PID files, status downgrade, on-demand PID loading, and stale proxy rejection
2026-04-07 16:34:42 +08:00
LC 38a498e202 feat(provider): support custom headers injection for HTTP providers (#2402)
* feat(provider): support custom headers injection for HTTP providers

* fix(provider): resolve lint problem

* fix(provider): align stream user-agent and header precedence docs
2026-04-07 16:05:21 +08:00
eturn 778f939302 fix [BUG] WebUI cannot connect to the gateway started by WebUI (#2267)
#2213
2026-04-07 15:46:45 +08:00
BeaconCat 84edc462d6 assets: update WeChat QR code image (#2385)
Co-authored-by: BeaconCat <BeaconCat@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 14:09:11 +08:00
Liu Yuan f0e6b7aa37 fix(seahorse): correct bm25 rank semantics in comments (#2360)
SQLite FTS5 bm25() returns negative values where numerically smaller
(more negative) indicates a better match. The official docs state:

  "The better the match, the numerically smaller the value returned."

Two comments incorrectly stated "closer to 0 = better match" and
"lower = better match". Updated all rank descriptions to use the
unambiguous "more negative = higher relevance" phrasing.

This matters because these comments are used as tool prompt hints
for LLM agents, and incorrect semantics could lead to wrong ranking
decisions.
2026-04-07 12:32:28 +08:00
wenjie 661ce5e311 fix(build): gate seahorse context manager on unsupported platforms (#2384)
- add build tags to exclude context_seahorse.go on mipsle and netbsd
- add context_seahorse_unsupported.go to keep registration and return a clear runtime error
- remove unused indirect dependency github.com/reiver/go-porterstemmer from go.mod and go.sum
2026-04-07 11:49:35 +08:00
dependabot[bot] c3e7396a3d build(deps): bump github.com/pion/rtp from 1.8.7 to 1.10.1 (#2290)
Bumps [github.com/pion/rtp](https://github.com/pion/rtp) from 1.8.7 to 1.10.1.
- [Release notes](https://github.com/pion/rtp/releases)
- [Commits](https://github.com/pion/rtp/compare/v1.8.7...v1.10.1)

---
updated-dependencies:
- dependency-name: github.com/pion/rtp
  dependency-version: 1.10.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-07 09:58:20 +08:00
dependabot[bot] 29277d4b3b build(deps): bump modernc.org/sqlite from 1.47.0 to 1.48.0 (#2289)
Bumps [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) from 1.47.0 to 1.48.0.
- [Changelog](https://gitlab.com/cznic/sqlite/blob/master/CHANGELOG.md)
- [Commits](https://gitlab.com/cznic/sqlite/compare/v1.47.0...v1.48.0)

---
updated-dependencies:
- dependency-name: modernc.org/sqlite
  dependency-version: 1.48.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-07 09:56:46 +08:00
Guoguo 9ec27835cf fix(docker): add -console flag and open network for launcher (#2314)
- Add -console to Dockerfile CMD so launcher outputs logs to stdout,
  making docker logs work as expected
- Remove 127.0.0.1 bind from ports to allow public network access
- Add commented PICOCLAW_LAUNCHER_TOKEN env var example

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:34:54 +08:00
Liu Yuan 1175f4a62b feat(membench): add LOCOMO memory benchmark tool (#2353)
Benchmark tool comparing legacy session manager vs seahorse short memory
retrieval on the LOCOMO long-term conversational memory dataset.

- cmd/membench/: CLI with ingest/eval/report/run subcommands
- Mode A (legacy): recency-biased budget truncation baseline
- Mode B (seahorse): per-keyword trigram FTS5 search + expand
- Metrics: Token-Overlap F1 and Recall Hit Rate
- `make mem` builds, downloads data, runs benchmark end-to-end
2026-04-06 17:26:43 +08:00
Liu Yuan 15a70ac45c feat(seahorse): implement short-term memory engine (LCM) (#2285)
* feat(seahorse): implement short-term memory engine of seahorse

Add pkg/seahorse/ module implementing a SQLite-backed DAG-based summary
hierarchy for context management, ported from lossless-claw's LCM design:

- types.go + short_constants.go: core types (Message, Summary, Conversation,
  ContextItem) and configuration constants (fanout, token targets, thresholds)
- migration.go: idempotent DB schema with FTS5 trigram tokenizer for CJK
- store.go: full SQLite CRUD (conversations, messages, summaries DAG,
  context_items with ordinal gap numbering, FTS5 search)
- short_engine.go: Engine lifecycle (NewEngine, Ingest, Assemble, Compact),
  session pattern filtering (ignore/stateless glob→regex compilation),
  per-session mutex via sync.Map
- short_assembler.go: budget-aware context assembly with fresh tail protection
  (32 messages), oldest-first eviction, summary XML formatting, RebuildContextItems
- short_compaction.go: leaf compaction (messages→summary) and condensed
  compaction (summaries→higher-level summary), 3-level LLM escalation,
  CompactUntilUnder for emergency overflow
- short_retrieval.go: lookupByID, FTS5/LIKE search, recursive expand with
  token cap
- context_seahorse.go: agent.ContextManager adapter, registered as "seahorse",
  provider↔seahorse message type conversion (ToolCalls, tool_result)

* fix(seahorse): correct 3 adapter bugs in context management

- TokenCount: use full message (Content+ToolCalls+Media) instead of Content-only
- Empty Content: rebuild Content from tool_result Parts when stored empty
- Duplicate summaries: summaries only in Summary field, not in History messages
- Grep: fix SearchResult.Snippet→Content for summaries
- Schema: fix FTS5 SQL uses VIRTUAL TABLE not TEMP TABLE
- TestFTS5SQLConstants: verify FTS5 SQL syntax correctness
- Test: fix flaky TestCompactLeaf

* fix(agent): ingest steering messages into seahorse SQLite

Steering messages were only persisted to session JSONL but not ingested
into seahorse SQLite, causing them to be missing from context assembly.

Added `ts.ingestMessage(turnCtx, al, pm)` call in the steering message
injection block alongside the existing JSONL persistence.

Test: TestSeahorseSteeringMessageIngested verifies steering messages
appear in seahorse SQLite DB after being processed.

* fix(seahorse): address 3 blocking bugs from code review

- Fix resequenceContextItemsTx scan error handling (store.go:850)
  Changed `return err` to `return scanErr` to properly propagate scan errors
  instead of returning nil (which silently corrupts data)

- Fix sql.NullString for INTEGER column (store.go:847)
  Changed `mid` from sql.NullString to sql.NullInt64 since message_id
  is INTEGER in schema. Removed unnecessary strconv.ParseInt call.

- Fix compactCondensed fallback deleting non-candidate items
  Added ReplaceContextItemsWithSummary method for per-item deletion
  when candidates are not contiguous in ordinal space.
  Optimized to use range deletion when candidates are consecutive.

* fix(seahorse): pass Budget to Compact for correct condensed threshold

Issue #4 from PR review: When Budget was not passed to seahorse.Compact,
it defaulted to `tokensBefore * 0.75`, making `tokensBefore > budget`
always true and causing condensed compaction to trigger unnecessarily.

Changes:
- context_seahorse.go: Forward Budget from CompactRequest to CompactInput
- loop.go: Pass Budget (ContextWindow) in all 3 Compact calls
- Add test verifying condensed is skipped when tokens < threshold
- Fix lint issues in store.go and store_test.go

* fix(seahorse): add mutex for assembler lazy initialization

Issue #5 from PR review: The check-then-create pattern for e.assembler
was a data race when multiple goroutines called Assemble() concurrently:
    if e.assembler == nil {
        e.assembler = &Assembler{...}
    }

Changes:
- Add assemblerMu sync.Mutex to Engine struct
- Add initAssemblerOnce() using double-checked locking (same pattern as initCompactionOnce)
- Add TestAssemblerLazyInitRace to verify thread-safety

* fix(seahorse): handle non-consecutive depths in selectShallowestCondensationCandidate

Issue #8 from PR review: the loop iterated depth 0, 1, 2... assuming
consecutive keys, but break when key was missing caused deeper depths
to never be checked.

Fix: collect all existing depth keys, sort, then iterate in order.

* fix(seahorse): wrap DeleteMessagesAfterID and appendContextItems in transactions

- DeleteMessagesAfterID: wrap all DELETE operations in a transaction for
  atomicity, remove redundant manual FTS delete (handled by trigger)
- appendContextItems: use transaction to fix read-then-write race condition
- Add GetMaxOrdinalTx and resolveItemTokenCountTx for transaction-scoped queries
- Remove unused resolveItemTokenCount function

Fixes PR review issues 6 and 7.

* fix(seahorse): derive readable content from Parts and cap CompactUntilUnder iterations

- Derive readable content from MessageParts in AddMessageWithParts so
  FTS5 indexing and summary formatting can access tool call information
- formatMessagesForSummary and truncateSummary now fall back to Parts
  when Content is empty, fixing blank summaries for Part-based messages
- Add MaxCompactIterations (20) to prevent CompactUntilUnder infinite
  loops; exceeded iterations are logged as warnings
2026-04-05 09:05:16 +08:00
LC 71337b6f52 fix(tool): clarify write_file nested-JSON escape semantics and add tests (#2320)
* fix(tool): clarify write_file nested-JSON escape semantics and add tests

* fix(tool): improve formatting of escaping rules in CLI tool prompt

* fix(tool): align escape notation with function.arguments layer
2026-04-04 17:56:49 +08:00
Mauro 84e42d6904 Merge pull request #2316 from zeroznet/fix/help-banner-double-v
fix: avoid duplicate v in CLI help banner
2026-04-03 23:14:07 +02:00
Robert Bopko e8d92e4a36 test: update root help banner expectation 2026-04-03 21:59:57 +02:00
Robert Bopko cbd0798a56 fix: avoid duplicate v in CLI help banner 2026-04-03 19:58:52 +02:00
Mauro d8c5183d9a feat(mcp): store oversized text results as artifacts (#2308)
* feat(mcp): store oversized text results as artifacts

* feat(mcp): fix doc

* fix(mcp): preserve raw MCP payload in text artifacts

* fix(mcp): avoid leaking large text when artifact persistence fails

* chore(mcp): clarify inline text limit and cover artifact edge cases
2026-04-04 01:30:36 +08:00
wenjie bd56e10bb8 fix(web): improve logs panel scroll handling (#2305)
- forward refs through ScrollArea so logs can access the viewport
- keep logs pinned to the bottom only when the user is already near it
- apply import and className ordering cleanup across frontend components
2026-04-03 15:37:23 +08:00
wenjie 7f7b4c430b feat(web): persist dashboard token in launcher config (#2304)
- add `launcher_token` to launcher config API/schema and save/load flow
- update dashboard token resolution order: env var -> launcher config -> random
- expose token source in startup logs and auth help metadata (including config path)
- add launcher token input to the config page and wire frontend form/API updates
- update login help/i18n copy and extend backend tests for new token-source behavior
2026-04-03 14:54:27 +08:00
wenjie f2a19ab947 feat(web): support image messages in pico chat (#2299) 2026-04-03 14:15:20 +08:00
Cytown f3ad5d9305 bug: fix typo in Makefile cause ln not work (#2301) 2026-04-03 12:43:39 +08:00
Cytown 5b116b093f fix comment in Makefile (#2300) 2026-04-03 11:49:24 +08:00
Cytown 170ae09606 fix windows make build error and support custom build env (#2281) 2026-04-03 11:39:37 +08:00
linhaolin1 b5ce6209fd feat: add VK channel support (#2276)
* feat: add VK channel support

- Add VK channel implementation using vksdk
- Support text messages and media attachments
- Implement Long Poll API for real-time messaging
- Add group chat support with trigger prefixes
- Add user whitelist (allow_from) configuration
- Add VK channel documentation

Files:
- pkg/channels/vk/: VK channel implementation
- pkg/config/config.go: Add VKConfig structure
- pkg/channels/manager.go: Register VK channel
- pkg/gateway/gateway.go: Import VK channel package
- docs/channels/vk/: Usage documentation

* test: add unit tests for VK channel

- Test channel initialization with various configurations
- Test allow_from whitelist functionality
- Test group trigger configuration
- Test max message length (4000 chars)
- Test message splitting logic
- Test attachment processing

All tests passing ✓

* fix: resolve linting issues in VK channel

- Format VKConfig struct tags to comply with golines
- Remove unused mu sync.Mutex field
- Remove unused stripPrefix method

All tests passing ✓

* style: format VKConfig with golines

- Align struct tags to match project style
- Match formatting with other channel configs (Telegram, etc.)
- Fix golines linting error

* style: fix struct tag formatting in config.go

* docs: update VK channel docs to use secure token storage

* feat(vk): add voice capabilities support

- Implement VoiceCapabilities() method for VK channel
- Add audio_message attachment handling in processAttachments
- Add comprehensive tests for voice capabilities
- Support both ASR (speech-to-text) and TTS (text-to-speech)

* docs: add VK channel to documentation and update voice support

- Add VK channel to README.md and README.zh.md channel lists
- Update VK channel documentation with voice message support
- Document ASR and TTS capabilities for VK channel
- Add voice transcription configuration reference
2026-04-03 10:56:26 +08:00
lxowalle 849e37cf79 * Load zoneinfo from TZ and ZONEINFO env (#2279) 2026-04-03 10:12:15 +08:00
dependabot[bot] 8aa110c02a build(deps): bump shadcn from 4.1.1 to 4.1.2 in /web/frontend (#2297)
Bumps [shadcn](https://github.com/shadcn-ui/ui/tree/HEAD/packages/shadcn) from 4.1.1 to 4.1.2.
- [Release notes](https://github.com/shadcn-ui/ui/releases)
- [Changelog](https://github.com/shadcn-ui/ui/blob/main/packages/shadcn/CHANGELOG.md)
- [Commits](https://github.com/shadcn-ui/ui/commits/shadcn@4.1.2/packages/shadcn)

---
updated-dependencies:
- dependency-name: shadcn
  dependency-version: 4.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-03 09:21:52 +08:00
dependabot[bot] 7fd6772196 build(deps): bump @tanstack/react-query in /web/frontend (#2296)
Bumps [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) from 5.95.2 to 5.96.1.
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query@5.96.1/packages/react-query)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.96.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-03 09:21:27 +08:00
dependabot[bot] 4169eb3b72 build(deps): bump react-i18next from 16.6.6 to 17.0.2 in /web/frontend (#2295)
Bumps [react-i18next](https://github.com/i18next/react-i18next) from 16.6.6 to 17.0.2.
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/react-i18next/compare/v16.6.6...v17.0.2)

---
updated-dependencies:
- dependency-name: react-i18next
  dependency-version: 17.0.2
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-03 09:20:58 +08:00
dependabot[bot] 8dfea249da build(deps-dev): bump eslint-plugin-react-refresh in /web/frontend (#2294)
Bumps [eslint-plugin-react-refresh](https://github.com/ArnaudBarre/eslint-plugin-react-refresh) from 0.4.26 to 0.5.2.
- [Release notes](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/releases)
- [Changelog](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/compare/v0.4.26...v0.5.2)

---
updated-dependencies:
- dependency-name: eslint-plugin-react-refresh
  dependency-version: 0.5.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-03 09:11:09 +08:00
dependabot[bot] 465baba994 build(deps): bump i18next from 26.0.1 to 26.0.3 in /web/frontend (#2292)
Bumps [i18next](https://github.com/i18next/i18next) from 26.0.1 to 26.0.3.
- [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.1...v26.0.3)

---
updated-dependencies:
- dependency-name: i18next
  dependency-version: 26.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-03 09:10:38 +08:00
Mauro f542c92970 Merge pull request #2288 from sipeed/dependabot/go_modules/github.com/rs/zerolog-1.35.0
build(deps): bump github.com/rs/zerolog from 1.34.0 to 1.35.0
2026-04-02 22:35:21 +02:00
Mauro 6842a41b06 Merge pull request #2287 from sipeed/dependabot/github_actions/actions/upload-artifact-7
build(deps): bump actions/upload-artifact from 4 to 7
2026-04-02 22:34:40 +02:00
dependabot[bot] b732abf758 build(deps): bump github.com/rs/zerolog from 1.34.0 to 1.35.0
Bumps [github.com/rs/zerolog](https://github.com/rs/zerolog) from 1.34.0 to 1.35.0.
- [Commits](https://github.com/rs/zerolog/compare/v1.34.0...v1.35.0)

---
updated-dependencies:
- dependency-name: github.com/rs/zerolog
  dependency-version: 1.35.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-02 17:17:07 +00:00
dependabot[bot] de2f2eb71b build(deps): bump actions/upload-artifact from 4 to 7
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-02 17:16:53 +00:00
Mauro b114dcaeb1 feat(model): llm rate limiting (#2198)
* feat(model): rate limiting

* fix(agent): preserve per-model identity in rate limiting and fallback

* fix test
2026-04-02 19:26:26 +08:00
wenjie dad5dcc30f refactor(web): load channel configs without exposing secret values (#2277)
* refactor(web): load channel configs without exposing secret values

- add a dedicated channel config API that returns sanitized config plus
  configured secret metadata
- update channel config pages and forms to use secret presence for
  placeholders, validation, reset, and save behavior
- refresh the channel settings layout and clean up related i18n copy
- add backend tests for the new channel config endpoint

* fix(config): restore missing strings import
2026-04-02 19:09:33 +08:00
wenjie e075be6b10 feat(web): move version display to the config page header (#2273)
- remove version details from the sidebar footer
- show the current app version as a badge in the config page header
- add a reusable Badge UI component for the new version label
2026-04-02 19:09:27 +08:00
Mauro bae4342af1 Feat/tool read_file by lines (#1981)
* feat(tool): read_file tool by lines

* fix test

* restore old bytes read_file tool

* unified read_file tool

* revert

* fix doc

* fix test

* fix doc

* fix offset

* fix default start_line

* fix line format

* fix bug

* removed legacy test

* enhanced infos

* improvements

* feat(tool): read_file tool by lines
2026-04-02 18:49:08 +08:00
lxowalle 03b97e412e docs: optimize readme for android (#2272)
* * update readme.md

* update

* * update
2026-04-02 14:23:01 +08:00
SakoroYou 257aa0ff57 fix(channels): fail fast when all channel startups fail (#2262)
* fix(channels): fail fast when all channel startups fail

* fix(channels): preserve startup errors and cover fail-fast semantics
2026-04-02 14:14:47 +08:00
Cytown adf78092da fix typo in create_dmg.yml (#2255) 2026-04-02 12:02:24 +08:00
Cytown 2c446e1e07 feat: add userAgent config for ModelConfig (#2242)
* feat: add userAgent config for ModelConfig

* update docs for ModelConfig.userAgent

* make defaut userAgent to PicoClaw and add test case
2026-04-02 11:44:13 +08:00
Mauro 415abc8cd4 Merge pull request #2092 from badgerbees/fix/telegram-edit-timeout
fix(telegram): avoid duplicate messages on streaming edit timeouts
2026-04-01 23:47:08 +02:00
Badgerbees 33ce6ed482 resolve conflicts 2026-04-02 04:33:08 +07:00
Liu Yuan 7eba27c3c4 feat: add ContextManager abstraction for pluggable context management (#2203)
- Define ContextManager interface with Assemble/Compact/Ingest methods
- Implement legacyContextManager wrapping existing summarization logic
- Wire Assemble (before BuildMessages), Compact (post-turn + overflow),
  and Ingest (after message persistence) into agent loop
- Add ContextManager config field and factory registry with config passthrough
- Remove old maybeSummarize/summarizeSession/summarizeBatch/etc from loop.go
- All existing tests pass with default (legacy) config

Co-authored-by: Liu Yuan <namei.unix@gmail.com>
2026-04-02 00:08:15 +08:00
Cytown 2973b30ad7 implement create dmg for macOS 10.11 & above (#2252) 2026-04-01 23:56:46 +08:00
LC bbcfeaa361 feat(provider): add Venice AI support and update related documentation (#2238)
* feat(provider): add Venice AI support and update related documentation

* revert(asr): restore asr files to previous commit

* feat(config): add Venice API base URL and local LM Studio configuration

* fix(config): update Venice API base URL to correct endpoint
2026-04-01 23:50:29 +08:00
Cytown 9ac21c5908 add missing recover panic in subturn.go (#2253) 2026-04-01 23:44:41 +08:00
sky5454 49e61fa07f feat(updater): robust self-update selection & extraction (nightly default) (#2201)
* feat(updater): add web self-update endpoint and updater package

* feat(selfupgrade): when url empty, using GetTestReleaseAPIURL for test .

* feat(selfupgrade):  only GetTestReleaseAPIURL  .

* feat(upgrade): cli  $0 update work well!

* fix(ci): fix ci err

* fix(test): fix ci test

* fix(ci): fix ci  lint fmt err

* test(updater): add test for updater

* fix(ci): fix ci  lint var copy err

* fix(ci): retry ci

* updater: require checksum verification, prefer API digest, verify SHA256, fix zip extraction, update tests

* fix(lint): lint fixed

* fix(lint): lint fixed2

* updater: stream download and verify sha256; add http client timeout and progress

Avoid double-download by streaming asset into temp file while computing SHA256 and verifying against checksum; replace http.Get with shared httpClient (2m timeout) to prevent hangs; add simple stderr progress display; remove unused helpers.
2026-04-01 23:41:32 +08:00
Cytown e2a9bb97c7 unify all panic event to panic log file (#2250) 2026-04-01 23:26:49 +08:00
Hoshina 168b75ae21 style(lint): fix config and qq formatting 2026-04-01 22:51:28 +08:00
Hoshina bef17d6453 feat(routing): add ordered dispatch rules 2026-04-01 22:13:04 +08:00
Hoshina 82bfe0d9a0 docs(config): remove legacy bindings guide 2026-04-01 21:34:49 +08:00
Hoshina 19a01d4264 refactor(routing): remove legacy bindings config 2026-04-01 21:34:39 +08:00
Hoshina 3a9d1fc6fd test(channels): update inbound context assertions 2026-04-01 21:34:24 +08:00
reusu 31afad6e87 feat: add load_image tool for local file vision (#2116)
* feat: add load_image tool for local file vision

* fix: address load_image PR review feedback

- Exclude load_image from sub-agent tools via Unregister after Clone,
  since RunToolLoop does not call resolveMediaRefs
- Add ToolRegistry.Unregister() method
- Fix scope collision: use channel:chatID instead of filename
- Add channel/chatID context resolution matching send_file pattern
- Add comment explaining iteration > 1 guard on resolveMediaRefs
- Remove emoji from ForUser for consistency with send_file
- Add load_image_test.go

* feat: enable load_image for subagents via MediaResolver in RunToolLoop

Instead of removing load_image from sub-agent tools (28f69e71), inject a
MediaResolver into the legacy RunToolLoop fallback path so media:// refs
are resolved to base64 before each LLM call — matching the main agent
loop behavior.

- Add MediaResolver field to ToolLoopConfig and call it on iteration > 1
- Add SubagentManager.SetMediaResolver() and wire it through runTask
- Remove ToolRegistry.Unregister() (no longer needed)
- Restore load_image in sub-agent tool set (revert Clone+Unregister)
- Add TestSubagentManager_SetMediaResolver_StoresResolver

* refactor(load_image): remove prompt parameter from tool schema

* test(tools): add success-path test for LoadImageTool

Add TestLoadImage_SuccessPath that creates a real PNG file with valid
magic bytes, calls Execute with WithToolContext, and verifies:
- result.IsError == false
- ToolResult.Media contains a media:// ref
- ToolResult.ForLLM contains the [image: marker
- media ref is resolvable in the store

Add explanatory comment in loop.go for why Media and ArtifactTags
coexist on non-ResponseHandled tool results (e.g. load_image).

* fix: preallocate slice in tests and add ResponseHandled guard in toolloop

Fix prealloc linter failure in load_image_test.go.

Prevent double-resolving media by checking ResponseHandled in toolloop.go.

* Register TTS tool if provider is available

---------

Co-authored-by: Reusu <admin@yumao.name>
Co-authored-by: 美電球 <hoshina@evaz.org>
2026-04-01 21:32:10 +08:00
Hoshina 53482a17bc refactor(web): resolve pico sessions from scope metadata 2026-04-01 20:57:15 +08:00
Hoshina 59dee895fc refactor(runtime): drop non-session legacy context compatibility 2026-04-01 20:56:48 +08:00
wenjie c0464bdd5d feat(web): add skill marketplace hub and registry install flow (#2246)
- add backend APIs for searching and installing registry skills, including origin metadata and concurrency-safe workspace writes
- introduce /agent/hub as the default agent entry with marketplace search and install UI
- refactor the skills and tools pages with filtering, dialogs, detail views, import validation, and updated i18n
- expand backend tests for search, install, import, rollback, and concurrent requests
2026-04-01 19:25:31 +08:00
Hoshina ca9652e120 refactor(session): replace dm scope with dimensions policy 2026-04-01 17:19:50 +08:00
Hoshina 3957e2cc72 feat(session): persist scope metadata and aliases 2026-04-01 16:25:05 +08:00
Hoshina bb2167e3f3 feat(event): log turn context fields 2026-04-01 15:46:35 +08:00
Hoshina e0ceea91f6 refactor(context): carry route and scope through runtime 2026-04-01 15:23:36 +08:00
Cytown a9c76eca21 bug: fix picoToken is empty when gateway started by launcher (#2241) 2026-04-01 14:59:18 +08:00
Hoshina 79de00f7f3 refactor(agent): carry inbound context through events and hooks 2026-04-01 14:37:43 +08:00
Hoshina fcab3a1b7c refactor(routing): move session allocation out of router 2026-04-01 14:26:12 +08:00
LC f327859cce fix(api): enhance model availability probing with backoff and caching mechanisms (#2231)
* fix(api): enhance model availability probing with backoff and caching mechanisms

* fix(lint): resolve gci and predeclared issues in model probe

* fix(api): address copilot review feedback on probe cache key and test stability

* fix(api): reduce probe cache key fragmentation
2026-04-01 14:15:28 +08:00
Hoshina 2095ec8700 refactor(agent): route using inbound context 2026-04-01 14:08:44 +08:00
Hoshina 963ed07d69 refactor(channels): emit inbound context in secondary adapters 2026-04-01 13:58:31 +08:00
Hoshina cf11ff70c3 refactor(channels): emit inbound context in primary adapters 2026-04-01 13:50:24 +08:00
Hoshina 9cfa3c3ba6 refactor(inbound): add inbound context compatibility bridge 2026-04-01 13:35:18 +08:00
Hua Audio 0f395ce110 Refactor/asr tts (#1939)
* refactor: update ASR and TTS implementations

* fix lint

* Integrating asr/tts models w/ new security config

* update documents

* add arbitrary whisper transcriptor support

* update documents

* fix lint

* add mimo tts
2026-04-01 12:21:21 +08:00
lxowalle ff90a65814 docs: update support android news (#2228) 2026-04-01 10:50:15 +08:00
Badgerbees b90a6d12ea fix(telegram): refine duplicate-message protection with narrow error classification
Addresses reviewer concerns regarding silent message loss by narrowing the
error swallowing logic in EditMessage:
- Excludes context.DeadlineExceeded and context.Canceled from being swallowed,
  ensuring local timeouts before transmission still trigger a fallback send.
- Adds an explicit check for the 'message is not modified' error to safely
  identify edits that have already landed on Telegram's servers.
- Narrowly targets confirmed post-connect dropouts (e.g., connection reset)
  instead of broad network-ish string matching.
- Fixes the missing isPostConnectError definition and required errors import.
2026-04-01 03:13:34 +07:00
Mauro c7461f9e96 Merge pull request #2221 from Alexandersfg4/doc/option-use-markdown-v2
doc: added documentaion for use_markdown_v2
2026-03-31 20:35:05 +02:00
LC 3b3f95c44c feat(web): refine model availability states and preserve API key preview placeholder (#2226)
* feat(web): clarify model availability and status display

- Rename model availability field from configured to available across backend API and frontend usage

- Keep status as reason classification (configured/unconfigured/unreachable) and show unreachable in UI

- Preserve API key preview even when local service is unreachable

- Update backend tests to assert both availability and status semantics

* fix(web): clarify unreachable model status and wording

- Show unreachable status in model cards instead of API key preview when service is down

- Keep API key placeholder preview in model settings whenever an API key is already saved

- Rename model status wording from configured to available across backend, frontend, and i18n

- Update backend model status tests to match renamed status semantics

* style(web): standardize formatting in handleListModels function

* refactor(web): enforce status field as required to follow backend behavior
2026-03-31 22:52:04 +08:00
wenjie 2bf842e460 feat(web): add service log level controls (#2227)
- centralize gateway log level resolution and normalization
- propagate debug flags to spawned launcher and gateway processes
- add a log level selector to the logs page
- cover the new behavior with backend and config tests
2026-03-31 20:32:42 +08:00
Mauro 848f9dd2e9 Merge pull request #2014 from badgerbees/fix/context-pruning-guards
fix(agent): include SystemParts in token estimation and add reasoning guards
2026-03-31 13:30:00 +02:00
Badgerbees 1a44752dc5 fix(agent): prevent double-counting system message tokens in estimator
Treat SystemParts as an alternative representation of message Content
rather than an additive one. This prevents systematic overestimation
of system message tokens which could trigger premature context
pruning or summarization.
- Picks the maximum of Content vs. SystemParts to stay conservative.
- Adds a per-part overhead (20 chars) to account for JSON metadata.
- Streamlines the ReasoningContent counting logic.
Fixes a deficiency where structured blocks for cache-aware adapters
caused overestimated budgets or hidden overflows.
2026-03-31 17:09:01 +07:00
Badgerbees 93f391a6bf fix(agent): include SystemParts in token estimation and add reasoning guards 2026-03-31 16:33:24 +07:00
wenjie dd54601f2d fix(web): hydrate cached Pico token for websocket proxy (#2222)
Load the Pico token from config before validating websocket proxy requests
when the launcher attaches to an existing gateway and the in-memory cache
is still empty
2026-03-31 17:01:44 +08:00
Meng Zhuo a098dfba84 Merge pull request #1957 from lepotatoguy/web-ui-input-fix
fix: detecting the external port that is being used, returning correct websocket
2026-03-31 16:57:40 +08:00
Aleksandr Bortnikov c783bab2d7 doc: added documentaion for use_markdown_v2 2026-03-31 10:53:24 +03:00
Cytown e4893d27d7 fix test and lint in web (#2219) 2026-03-31 15:50:55 +08:00
LePotatoGuy 61a31df168 use explicit port headers before falling back to wsPort in picoWebUIAddr 2026-03-31 02:11:17 -05:00
Cytown 31fcf55297 fix linter (#2206) 2026-03-31 15:06:47 +08:00
LC ee02e30992 feat(provider): add lmstudio and align local provider default auth/base handling (#2193)
* feat(provider): add lmstudio vendor and local no-key behavior

* refactor(provider): consolidate protocol metadata and local tests

* fix(provider): sync lmstudio probing and model normalization

* test(web): format lmstudio model status cases for golines
2026-03-31 14:48:18 +08:00
BeaconCat d11f1bc064 assets: update WeChat QR code image (#2207)
Co-authored-by: BeaconCat <BeaconCat@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 14:21:42 +08:00
DimonB c36b06a901 Fix Telegram HTML links broken by italic regex matching inside href URLs (#2164)
reItalic (_text_) ran after reLink converted [text](url) to <a href>,
injecting <i> tags into URLs containing underscores (e.g. Google Flights
URL-safe base64 in the tfs param). Telegram silently dropped such malformed
<a> tags, causing only 1 of 3 links to appear in messages.

Fix: extract markdown links into placeholders before any formatting runs,
restore them as <a href> last — same pattern used for code blocks.
2026-03-31 11:46:06 +08:00
DimonB 6c0798ca3f feat(channels): make Channel.Send return delivered message IDs (#2190)
* feat(channels): Channel.Send and MediaSender.SendMedia return delivered message IDs

Change Channel.Send signature from (ctx, msg) error to (ctx, msg) ([]string, error)
and MediaSender.SendMedia similarly, so callers can capture platform message IDs
for threading, reactions, and history annotation.

Adapters that return real IDs: Telegram (per-chunk MessageID), Discord (Message.ID),
Slack Send (ts), QQ (sentMsg.ID), Matrix (EventID). Slack SendMedia returns nil
because UploadFileV2 does not expose the posted message timestamp in its response.
All other adapters return nil IDs.

preSend and sendWithRetry in manager.go updated to propagate ([]string, bool).
README examples updated for both English and Chinese docs.

* style: apply golangci-lint fixes (golines)

* docs: fix Send migration guide — restore old error-only signature in before/after example
2026-03-31 11:07:32 +08:00
Mauro 2d8556205f feat(telegram): include quoted reply context and media in inbound turns (#2200) 2026-03-31 10:46:41 +08:00
daming大铭 073cc3f65e Merge pull request #2196 from SiYue-ZO/feature/tour-guide
feat: add first-time tour guide for new users
2026-03-31 10:28:32 +08:00
Mauro 4d34824737 Merge pull request #2088 from badgerbees/fix/telegram-dm-policy-security
fix(channels): add security audit for open-by-default bots
2026-03-30 23:58:25 +02:00
Mauro a995a94990 Merge pull request #1826 from 3mp3ri0r/fix/container-run-app-stopped-on-sigint-sigterm
fix: container run but app stopped on SIGINT or SIGTERM
2026-03-30 23:47:05 +02:00
Mauro 4125f8ac14 Merge pull request #1849 from gaaralbakuu/main
Fix: Provider github copilot cannot create session
2026-03-30 23:37:26 +02:00
SiYue-ZO b8327462f9 feat: add first-time tour guide for new users
- Add tour guide component with floating bubbles
- Guide users through: Welcome -> Configure Models -> Start Gateway -> View Docs
- Use localStorage to persist tour state
- Support i18n (Chinese and English)
- Highlight target elements with spotlight mask
- Allow skipping tour at any time
2026-03-31 00:43:35 +08:00
Mauro 7b3f47128f Merge pull request #2176 from Alix-007/fix/issue-2135-retry-after
fix(utils): honor Retry-After for 429 retries
2026-03-30 16:27:46 +02:00
Alix-007 711685192c utils: gofumpt http retry formatting 2026-03-30 22:08:21 +08:00
Alix-007 345d4fddc9 utils: make retry-after numeric clamp overflow-safe 2026-03-30 22:02:16 +08:00
Alix-007 9440bebca6 utils: anchor date retry-after to response date and cap delay 2026-03-30 21:58:12 +08:00
Mauro 187b2c2185 Merge pull request #2004 from Huangting-xy/docs-add-security-config-ref
docs(configuration): add security config reference at document start
2026-03-30 14:24:34 +02:00
Mauro ffa65b53ed Merge pull request #1982 from Kathent/fix-deny-pattern
fix: more accurate deny pattern for disk wiping
2026-03-30 14:24:17 +02:00
Mauro 34b4848214 Merge pull request #1838 from jonahzheng/patch-1
Update helpers.go
2026-03-30 14:23:38 +02:00
Mauro 174c4e5d3b Merge pull request #2000 from Alix-007/docs/issue-1868-cron-docs
docs: add cron job behavior guide
2026-03-30 14:18:43 +02:00
Mauro 0fb45505bf Merge pull request #1988 from loafoe/main
feat(bedrock): detect SSO token expiration and provide actionable error
2026-03-30 14:17:19 +02:00
Mauro 45582b0b52 Merge pull request #1510 from dim/matrix/improved-formatting
Improve white-space and general rendering of CommonMark in Matrix
2026-03-30 14:02:19 +02:00
daming大铭 a5f8b0f98d Merge pull request #2134 from cytown/t3
add pid file for gateway running and auth token for /reload and pico channel
2026-03-30 18:55:01 +08:00
Mauro 1154017563 Merge pull request #2129 from kunalk16/chore-azure-openai-responses-tests
chore(tests): update tests and error cases handling for azure openai provider
2026-03-30 12:29:41 +02:00
Cytown f9bfa6b9a8 Merge branch 'main' into t3 2026-03-30 18:07:20 +08:00
Cytown 50b8d9bf83 Merge branch 'main' into t3 2026-03-30 18:01:07 +08:00
Cytown 275c1012f1 make gateway reload use new loglevel (#2155) 2026-03-30 18:00:18 +08:00
Cytown 010d807e61 update docs according to newest config version 2 (#2186) 2026-03-30 17:59:56 +08:00
daming大铭 803b8bc02f Merge pull request #2131 from imalasong/pr/3
fix(feishu): skip empty random_reaction_emoji entries
2026-03-30 17:51:26 +08:00
Cytown 7a1f2aba03 add check for gateway port and fix logger.Fatal not record issue (#2185) 2026-03-30 17:43:10 +08:00
daming大铭 cbe92286e9 Merge pull request #2184 from cytown/config
refactor config and add ModelConfig.Enabled
2026-03-30 17:23:07 +08:00
LC ff0266a40e feat(web): display backend version info in sidebar (#2087)
* feat(web): display backend version info in sidebar

* fix(web): improve version parsing and timeout behavior

* refactor(web): remove useless --version fallback

* feat(web): implement version info caching and improve retrieval logic

* fix(web): clarify version timeout rationale

* fix(web): harden gateway version probing and tests

* style(web): split regexp to two lines for lint
2026-03-30 16:44:50 +08:00
Alix-007 e88df4ff9c feat(tools): add reaction tool and reply-aware message sends (#2156)
- Add `reaction` tool that reacts to a message (defaults to current inbound message via context)
- Extend `message` tool with optional `reply_to_message_id` parameter
- Introduce `WithToolInboundContext` to inject inbound message IDs into tool execution context
- Surface `MessageID` and `ReplyToMessageID` in `processOptions` for tool-surface consumption

Refs #2137
2026-03-30 16:31:34 +08:00
mattn 5e7545a22a perf: precompute BM25 index for repeated searches (#2177) 2026-03-30 16:30:25 +08:00
dependabot[bot] 5e1b6a3971 build(deps-dev): bump globals from 16.5.0 to 17.4.0 in /web/frontend (#2067)
Bumps [globals](https://github.com/sindresorhus/globals) from 16.5.0 to 17.4.0.
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v16.5.0...v17.4.0)

---
updated-dependencies:
- dependency-name: globals
  dependency-version: 17.4.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-30 16:18:08 +08:00
dependabot[bot] 7dc0d02a5e build(deps): bump i18next from 25.8.20 to 25.10.10 in /web/frontend (#2065)
Bumps [i18next](https://github.com/i18next/i18next) from 25.8.20 to 25.10.10.
- [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/v25.8.20...v25.10.10)

---
updated-dependencies:
- dependency-name: i18next
  dependency-version: 25.10.10
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-30 16:14:20 +08:00
dependabot[bot] 5c6e13e188 build(deps): bump modernc.org/sqlite from 1.46.1 to 1.47.0 (#2063)
Bumps [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) from 1.46.1 to 1.47.0.
- [Changelog](https://gitlab.com/cznic/sqlite/blob/master/CHANGELOG.md)
- [Commits](https://gitlab.com/cznic/sqlite/compare/v1.46.1...v1.47.0)

---
updated-dependencies:
- dependency-name: modernc.org/sqlite
  dependency-version: 1.47.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-30 16:05:11 +08:00
dependabot[bot] fd9914dd92 build(deps): bump github.com/aws/aws-sdk-go-v2/service/bedrockruntime (#2061)
Bumps [github.com/aws/aws-sdk-go-v2/service/bedrockruntime](https://github.com/aws/aws-sdk-go-v2) from 1.50.2 to 1.50.3.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/s3/v1.50.2...service/s3/v1.50.3)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2/service/bedrockruntime
  dependency-version: 1.50.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-30 15:58:04 +08:00
dependabot[bot] 74dfd9364c build(deps): bump golang.org/x/time from 0.14.0 to 0.15.0 (#2059)
Bumps [golang.org/x/time](https://github.com/golang/time) from 0.14.0 to 0.15.0.
- [Commits](https://github.com/golang/time/compare/v0.14.0...v0.15.0)

---
updated-dependencies:
- dependency-name: golang.org/x/time
  dependency-version: 0.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-30 15:51:19 +08:00
dependabot[bot] d844bf3683 build(deps): bump github.com/github/copilot-sdk/go from 0.1.32 to 0.2.0 (#2058)
Bumps [github.com/github/copilot-sdk/go](https://github.com/github/copilot-sdk) from 0.1.32 to 0.2.0.
- [Release notes](https://github.com/github/copilot-sdk/releases)
- [Changelog](https://github.com/github/copilot-sdk/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/copilot-sdk/compare/v0.1.32...v0.2.0)

---
updated-dependencies:
- dependency-name: github.com/github/copilot-sdk/go
  dependency-version: 0.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-30 15:47:17 +08:00
wenjie f07a8a89d5 chore(web): patch vulnerable frontend tooling dependencies (#2182)
- upgrade Vite, ESLint, React plugin, and related frontend packages to secure versions
- refresh the pnpm lockfile to pull in patched transitive dependencies
- raise the required Node.js version to match the patched toolchain
- update the web README with the new frontend runtime requirement
2026-03-30 15:29:43 +08:00
smallwhite 89af3b2511 fix(tools): message tool no longer suppresses reply to originating chat
When the message tool sent to a different chat (e.g., a group), the
agent's final response to the originating chat was incorrectly skipped
because HasSentInRound() was a simple bool that didn't distinguish
targets. Replace with HasSentTo(channel, chatID) that tracks all
send targets per round and only suppresses when the target matches.

Fixes cross-conversation message causing "Processing..." to hang.
2026-03-30 15:06:22 +08:00
wenjie edda02ce67 build(web): refactor launcher build flow and expand WebUI documentation (#2174)
- delegate root launcher builds to the web Makefile
- add dedicated frontend and dev picoclaw build targets
- document the WebUI architecture, runtime behavior, and build workflow
2026-03-30 14:45:52 +08:00
BeaconCat b67d3cfbd8 docs: document gateway.log_level in all READMEs and i18n configuration docs (#2178)
* docs: document gateway.log_level in all READMEs and i18n configuration docs

Add gateway log level note to Channels section in all 9 READMEs and
add Gateway Log Level section to zh/fr/ja/pt-br/vi configuration docs.

- gateway.log_level (default: fatal) controls log verbosity
- Supported values: debug, info, warn, error, fatal
- Can also be set via PICOCLAW_LOG_LEVEL env var
- English docs/configuration.md already had this section

* fix(docs): correct gateway.log_level default from fatal to warn

DefaultConfig() sets Gateway.LogLevel to "warn", not "fatal".
Update all READMEs and i18n configuration docs to reflect the
actual default value.

---------

Co-authored-by: BeaconCat <BeaconCat@users.noreply.github.com>
2026-03-30 14:44:32 +08:00
Cytown 93757812fc refactor config and add ModelConfig.Enabled 2026-03-30 14:01:20 +08:00
Alix-007 cd3f6600ca fix(utils): honor Retry-After for 429 retries 2026-03-30 13:04:51 +08:00
afjcjsbx 765a165475 fix(agent): warn on unknown frontmatter capabilities 2026-03-29 23:48:06 +02:00
afjcjsbx abeb2d8e0a fix(agent): fall back to first AGENT line for discovery 2026-03-29 23:44:41 +02:00
afjcjsbx f5f1dc9808 fix(agent): load only allowed MCP servers 2026-03-29 23:43:35 +02:00
afjcjsbx 409251e69d fix(agent): fail closed on invalid AGENT frontmatter 2026-03-29 23:41:32 +02:00
afjcjsbx 847218ef29 refactor(agent): added mcp allowlist 2026-03-29 23:22:47 +02:00
afjcjsbx 0ef25f779e refactor(agent): move delegation details out of discovery prompt 2026-03-29 22:57:57 +02:00
afjcjsbx 6429f6af9a refactor(agent): source discovery identity from AGENT.md frontmatter 2026-03-29 22:43:20 +02:00
沈青川 93f4c4a843 fix(web): skills page uses theme colors for dark mode (#2166)
- Remove bg-white/80 override on skill cards so bg-card/text-card-foreground apply
- Use bg-muted + text-foreground for skill path block readability

Made-with: Cursor
2026-03-30 01:33:08 +08:00
daming大铭 1fc5345857 refactor(cron): remove deliver and type params, unify agent execution path (#2147)
The agent path now publishes to outbound bus directly (since #2100),
making the deliver=true direct-to-bus shortcut and the directive type
prompt wrapping redundant. All cron jobs now uniformly route through
the agent. This is an intentional behavior change: old jobs with
deliver=true will execute through the agent instead of bypassing it.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 22:52:34 +08:00
Cytown 1ef0553929 add logger test case for console log format for component (#2162) 2026-03-29 22:32:39 +08:00
Alix-007 a4574f72a3 fix(web/config): persist Discord token updates from channel settings (#2024)
* fix: save Discord token updates from channel settings

- preserve secret fields from PUT/PATCH /api/config payloads via setters

- include _token edit fields in channel save payload construction

- add regression test for Discord token patch flow (issue #2005)

* fix: resolve shadow lint warnings in config secret mapping

* fix(web/api): adapt config secret patch path after #2068

---------

Co-authored-by: Alix-007 <267018309+Alix-007@users.noreply.github.com>
2026-03-29 22:19:13 +08:00
afjcjsbx bca131909d fix lint 2026-03-29 14:27:22 +02:00
afjcjsbx 07748bf076 chore: revert unrelated golines formatting 2026-03-29 14:06:19 +02:00
afjcjsbx 3b173c0bee feat(agent): add multi-agent discovery prompt and per-agent 2026-03-29 13:58:19 +02:00
daming大铭 e34c4f82e0 Merge pull request #2154 from cytown/logger
make logger more clear with highlight component and use package name for default component
2026-03-29 19:47:51 +08:00
Cytown 42e3aaff35 make logger more clear with highlight component and use package name for default component 2026-03-29 18:35:24 +08:00
Cytown f0c0219c4c fix for review 2026-03-29 16:58:48 +08:00
Cytown 9c28870e80 Merge branch 'main' into t3 2026-03-29 16:48:56 +08:00
李光春 e70928cc6f feat(mcp): support DisableStandaloneSSE for HTTP transport (#2108) 2026-03-29 14:37:22 +08:00
沈青川 e414b82ac3 fix(cron): publish agent response to outbound bus for cron-triggered jobs (#2100)
* fix(cron): publish agent response to outbound bus for cron-triggered jobs

When a cron job triggers agent execution via ProcessDirectWithChannel,
the agent response was silently discarded — the code assumed AgentLoop
would auto-publish it, but SendResponse is false on this path.

Delegate to PublishResponseIfNeeded (exported from AgentLoop) so the
response reaches the originating channel (e.g. Telegram) only when the
message tool did not already deliver content in the same round.

Also adds a "directive" message type to CronPayload, allowing cron jobs
to instruct the agent to execute a task rather than echo static text.

* fix(cron): add type validation and directive test coverage

Address reviewer blocking feedback:

1. Server-side whitelist for `type` parameter — the `enum` in
   Parameters() is only an LLM schema hint; any string was persisted.
   Now `addJob` rejects values other than "message" and "directive".

2. Comprehensive test coverage for the directive code path:
   - directive adds prompt prefix to ProcessDirectWithChannel
   - deliver=true + directive routes through agent (not direct publish)
   - directive prompt content, sessionKey, channel, chatID are correct
   - invalid type is rejected; valid types ("", "message", "directive") pass
   - deliver=true message type goes directly to bus (regression)
   - agent error path does not trigger publish (regression)

Also merge the two UpdateJob calls in addJob into one to avoid
redundant disk I/O (non-blocking suggestion from review).

* fix(cron): remove omitempty from CronPayload.Type for consistent JSON

Empty string and "message" are semantically equivalent defaults;
always serializing the field avoids asymmetric JSON output.

* test(cron): remove redundant test, strengthen error path coverage

- Remove ExecuteJobDirectivePassesCorrectContent: its assertions on
  sessionKey/channel/chatID duplicate ExecuteJobPublishesAgentResponse;
  its prompt check duplicates DirectiveAddsPromptPrefix.
- Strengthen DirectiveAddsPromptPrefix with exact prompt match and
  publish response assertion.
- Fix ReturnsErrorWithoutPublish: set non-empty stub response so the
  test verifies the error branch early-return, not the response==""
  guard.

* fix(ci): satisfy golines and gosmopolitan in cron code
2026-03-29 13:47:28 +08:00
zeed zhao 6ea364e67d feat(web): protect launcher dashboard with token and SPA login (#1953)
Add token-based authentication for the Launcher's embedded Web Dashboard.

- Ephemeral token generated in-memory each run (or via PICOCLAW_LAUNCHER_TOKEN env var)
- HMAC-SHA256 session cookie (HttpOnly, SameSite=Lax, Secure when HTTPS)
- Bearer token support for API/script access
- Rate limiting on login (10 attempts/IP/min)
- Referrer-Policy: no-referrer on all responses
- POST-only logout with JSON content-type (CSRF-safe)
- System tray "Copy dashboard token" action
- Login page shows contextual help (console/tray/log file path)
- Path traversal protection via path.Clean
- X-Forwarded-Host/Port/Proto support for reverse proxy deployments
- Full i18n support (English, Chinese)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 13:11:43 +08:00
Andy Lo-A-Foe 4f90909af3 feat(bedrock): detect SSO token expiration and provide actionable error
When AWS SSO credentials expire, provide a clear error message instructing
the user to run 'aws sso login' to refresh their session.
2026-03-28 22:18:45 +01:00
Cytown 475d377af1 Merge branch 'main' into t3 2026-03-29 01:25:20 +08:00
Cytown 0bb561548f add pid file for gateway running and auth token for /reload and pico channel 2026-03-29 01:14:39 +08:00
imalasong 43095543ab fix(feishu): skip empty random_reaction_emoji entries
Feishu returns 231001 when emoji_type is empty. Config slices like
["", "Pin"] could randomly select an empty string; filter and
trim entries and fall back to Pin when none remain.

Made-with: Cursor
2026-03-28 23:36:49 +08:00
肆月 27f638e909 fix: unified restart required (#1978)
Unified restart-required detection and notification mechanism so that model, tool, and configuration changes all follow the same signature-based comparison logic.
2026-03-28 22:13:50 +08:00
Kunal Karmakar e23eda5365 Update tests and error cases handling 2026-03-28 13:33:48 +00:00
Kunal Karmakar 1809d04905 chore(provider): use openai responses api for azure openai endpoints (#2110)
Migrate Azure OpenAI provider from legacy Chat Completions API to the OpenAI Responses API.

- Switch API endpoint from `/openai/deployments/{deployment}/chat/completions` to `/openai/v1/responses`
- Change auth header from `Api-Key` to `Authorization: Bearer`
- Use `responses.ResponseNewParams` SDK types for request construction
- Extract shared Responses API utilities into `openai_responses_common` package
- Deduplicate 178 lines from codex_provider.go by reusing shared package
- Add 593 lines of comprehensive test coverage for the shared package

Closes #2111

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 20:50:24 +08:00
Mauro 026a1339c7 simplified duplicated code (#1935) 2026-03-28 20:28:24 +08:00
champly 11dec0c80a fix(weixin): persist context tokens to disk to survive restarts (#2124) 2026-03-28 20:23:14 +08:00
Guoguo 30155c1c59 Merge pull request #2119 from BeaconCat/fix/update-assets
docs: add macOS Gatekeeper bypass guide to WebUI Launcher section
docs: update WeCom channel docs and README provider/channel tables
docs: add Malay README and docs, add v0.2.4 news to all languages
docs(wecom): add fr/ja/pt-br/vi translations for unified WeCom channel
2026-03-28 19:02:43 +08:00
Guoguo 465ca0361c docs(wecom): add fr/ja/pt-br/vi translations for unified WeCom channel docs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 03:58:52 -07:00
Guoguo 62d40a02d4 fix: resolve typecheck errors in loop_test.go and dingtalk_test.go (#2122)
- loop_test.go: replace undefined WithSecurity/SecurityConfig/ModelSecurityEntry
  with direct APIKeys field using SimpleSecureStrings()
- dingtalk_test.go: use ClientSecret.String() and ClientSecret.Set()
  instead of non-existent ClientSecret() and SetClientSecret() methods

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 18:58:09 +08:00
BeaconCat 4d6292ca20 Merge branch 'main' into fix/update-assets 2026-03-28 18:47:49 +08:00
BeaconCat 836cbc3066 docs: add Malay README and docs, add v0.2.4 news to all languages
- Add README.my.md (full Malay translation from English, including
  macOS guide, MiMo provider, unified WeCom row, all sections)
- Add docs/my/ (chat-apps, configuration, debug, docker, spawn-tasks,
  troubleshooting) from upstream PR #1770
- Add [Malay](README.my.md) language link to all 8 existing READMEs
- Add v0.2.4 news entry to all 9 READMEs (en/zh/fr/ja/pt-br/vi/id/it/my)
- Move 2026-02-26 20K Stars entry into Earlier news in all READMEs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 18:46:21 +08:00
Alix-007 b6951b6925 fix(dingtalk): honor mention-only groups and strip leading mentions (#2054)
* fix(dingtalk): honor @mention flag in mention-only groups

* fix(dingtalk): strip leading mentions in group payloads

---------

Co-authored-by: Alix-007 <267018309+Alix-007@users.noreply.github.com>
2026-03-28 18:26:10 +08:00
Hua Audio 0e13f6bdec fix/wechat-new-protocol (#2106)
* fix/wechat-new-protocol

* fix cdn download logic
2026-03-28 18:18:01 +08:00
BeaconCat ba96f11f90 docs: update WeCom channel docs and README provider/channel tables
- Rewrite docs/channels/wecom/README.md and README.zh.md with unified
  3-option setup guide (Web UI QR / CLI QR / manual config), full config
  table with defaults and env vars, runtime behavior details, and
  migration notes from legacy wecom_bot/wecom_app/wecom_aibot
- Add assets/wecom-qr-binding.jpg screenshot for Web UI QR binding flow
- Remove obsolete docs/channels/wecom/wecom_bot/, wecom_app/, wecom_aibot/
  subdirectories (18 files, all language variants)
- Update Channels table in all 8 READMEs: replace 3 legacy WeCom rows
  with single unified WeCom row; zh README links to README.zh.md,
  others link to README.md
- Add Xiaomi MiMo (mimo/) to Providers table in all 8 READMEs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 18:02:13 +08:00
Muhammad Asyraf d7c0205052 docs: add Malay language (#1770)
* Add comprehensive documentation for PicoClaw configuration, chat applications, debugging, Docker setup, async tasks, and troubleshooting on MY language:

- Introduced a new document on MY language for chat applications configuration detailing setup for Telegram, Discord, WhatsApp, and others.
- Created a configuration guide on MY language outlining environment variables, workspace structure, and security settings.
- Added a debugging section to assist users in troubleshooting and understanding agent interactions on MY language.
- Provided a Docker guide on MY language for easy deployment using Docker Compose.
- Documented the use of spawn on MY language for asynchronous tasks and how to configure heartbeat settings.
- Included a troubleshooting section on MY language for common model-related errors.

* docs: add Malay language support to documentation

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-28 17:34:58 +08:00
BeaconCat 5d0cf36a18 docs: add macOS Gatekeeper bypass guide to WebUI Launcher section
Add a collapsible macOS security warning guide under the WebUI Launcher
section in all 8 README languages (en/zh/fr/ja/pt-br/vi/id/it).

- New assets: macos-gatekeeper-warning.jpg, macos-gatekeeper-allow.jpg
- Updated asset: launcher-tui.jpg
- Two-step guide: shows Gatekeeper warning screenshot, then
  Privacy & Security → Open Anyway flow

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 17:29:16 +08:00
Mauro 230942d234 fix(loop): polling (#2103) 2026-03-28 16:36:06 +08:00
xiwuqi e011284d8f fix(agent): use light provider for routed model calls (#2038) 2026-03-28 15:25:23 +08:00
BeaconCat c6061dd0d7 docs: update WeChat QR code and TUI launcher screenshot (#2109)
* docs: update WeChat QR code and TUI launcher screenshot

* docs: convert launcher-tui.jpg from PNG to proper JPEG format

---------

Co-authored-by: BeaconCat <BeaconCat@users.noreply.github.com>
2026-03-28 14:22:46 +08:00
Cytown f1cb7cc8f5 fix gateway reload will cause pico stop working issue (#2082)
* fix gateway reload will cause pico stop working issue

* fix for review
2026-03-28 11:30:31 +08:00
Mauro 60d7ec20a5 feat(log): prompt tokens (#2047) 2026-03-28 02:00:12 +08:00
Cytown b646d3b8fe refactor config and security to simplified the structure (#2068) 2026-03-28 00:03:34 +08:00
Cytown 98c78363b3 change default debug level to warn (#2084) 2026-03-27 21:04:28 +08:00
Badgerbees 8d5fc736d6 security: add open-by-default warning and '*' allow_from support 2026-03-27 20:04:21 +07:00
Cytown 0c9e4f0658 fix for FlexibleStringSlice cause picoclaw start crash issue (#2078) 2026-03-27 20:49:51 +08:00
daming大铭 25ce52715d Merge pull request #2070 from afjcjsbx/feat/improve-web-tools
feat(tools) time range in web_search
2026-03-27 19:36:24 +08:00
Mauro 76cd7f8ad5 Merge pull request #2085 from lc6464/fix/chat/break-word
fix(chat): add break-words class to user message for better text wrapping
2026-03-27 10:56:47 +01:00
lc6464 b5e29ae501 fix(chat): add break-words class to user message for better text wrapping 2026-03-27 17:11:19 +08:00
Meng Zhuo 9cbb4ab7ad Merge pull request #2071 from afjcjsbx/fix/array-placeholder
fix(config): array placeholder
2026-03-27 09:24:23 +08:00
afjcjsbx d385491592 fix(config): array placeholder 2026-03-26 21:40:38 +01:00
afjcjsbx e2018c4aa7 fix lint 2026-03-26 21:33:43 +01:00
afjcjsbx b7f6ab7176 fix conf 2026-03-26 21:27:35 +01:00
afjcjsbx 48c04e050d feat(tools) range in web_search 2026-03-26 21:02:46 +01:00
Mauro e6c05cb4ec Merge pull request #2069 from Alix-007/docs/issue-1908-model-cascade
docs(providers): clarify automatic model failover cascade configuration
2026-03-26 20:51:09 +01:00
Alix-007 9f02a5f33c docs(providers): clarify automatic model failover cascade 2026-03-27 02:29:48 +08:00
Mauro 463a647a33 Merge pull request #2043 from apnea/main
docs(providers): add Z.AI Coding Plan to providers info
2026-03-26 13:33:53 +01:00
apnea 06be65e2e2 Fix API key links for Z.AI API key and add Z.AI example
Updated API key links for Z.AI Coding Plan and explicit Z.AI config example
2026-03-26 11:09:09 +01:00
Mauro 4bdf8f0e1d Merge pull request #1829 from perhapzz/test/add-fileutil-health-tests
test: add unit tests for pkg/fileutil and pkg/health
2026-03-26 10:42:13 +01:00
pete 5db5717fdb docs: Add Z.AI Coding Plan provider example
## Summary
- Add zai-coding to Providers table
- Add Z.AI Coding Plan to All Supported Vendors table
- Add Z.AI Coding Plan configuration example with troubleshooting note
2026-03-26 09:19:15 +01:00
Alix-007 9d6a445bb1 docs: clarify gateway.log_level default and options (#2013) (#2015)
Co-authored-by: Alix-007 <267018309+Alix-007@users.noreply.github.com>
2026-03-26 09:32:56 +08:00
Alix-007 5c210e6f15 fix(config): disable tool feedback by default (#2026)
Co-authored-by: Alix-007 <267018309+Alix-007@users.noreply.github.com>
2026-03-26 09:31:42 +08:00
Alix-007 9503f38ace docs: clarify gateway vs launcher chat endpoints (#2025)
Co-authored-by: Alix-007 <267018309+Alix-007@users.noreply.github.com>
2026-03-26 09:27:34 +08:00
Mauro 5db1e94693 Merge pull request #1998 from abnerhexu/main
feat(config): allow placeholder text to be string or list
2026-03-25 22:47:39 +01:00
Mauro 1dff5e6903 Merge pull request #2016 from badgerbees/fix/context-overflow-errors
fix(providers): improve context overflow detection and classification
2026-03-25 21:58:53 +01:00
Badgerbees ae94893605 adding test units 2026-03-26 03:03:19 +07:00
Badgerbees 97dec16769 fix(providers): improve context overflow detection and classification 2026-03-26 01:07:56 +07:00
柚子 ed618e14aa feat(channels): support multi-message sending via split marker (#2008)
* Add multi-message sending via split marker

* Add marker and length split integration tests

Tests that SplitByMarker and SplitMessage work together correctly, and
that code block boundaries are preserved during marker splitting.

* Simplify message chunking logic in channel worker

Extract splitByLength helper function and remove goto-based control
flow.
The logic now flows more naturally - try marker splitting first, then
fall
back to length-based splitting.

* Update multi-message output instructions in agent context

* Add split_on_marker to config defaults

* Add split_on_marker config option

* Rename 'Multi-Message Sending' setting to 'Chatty Mode'

* Add SplitOnMarker config option
2026-03-26 01:33:49 +08:00
daming大铭 82c78e853b build(deps): upgrade pty and reorganize sqlite dependencies (#2012)
- Upgrade github.com/creack/pty from v1.1.9 to v1.1.24
- Move github.com/mattn/go-sqlite3 to indirect dependency
- Move rsc.io/qr from indirect to direct dependency
2026-03-25 17:35:56 +01:00
daming大铭 664e23e4fb Merge pull request #1828 from liuy/feat/logging-config
feat(logger): add PICOCLAW_LOG_FILE env var for file-only logging
2026-03-25 23:54:29 +08:00
daming大铭 70c4714988 feat(tools): add exec tool enhancement with background execution and PTY support
Merge #1869: Unified exec tool with actions (run/list/poll/read/write/send-keys/kill), PTY support, background execution, process session management.
2026-03-25 23:50:42 +08:00
肆月 bb2eddc79d Feature/add mimo provider (#1987)
* feat: add Xiaomi MiMo provider support

- Add 'mimo' protocol prefix support in factory_provider.go
- Add default API base URL for MiMo: https://api.xiaomimimo.com/v1
- Update provider-label.ts to include Xiaomi MiMo label
- Add MiMo to provider tables in both English and Chinese documentation
- Add comprehensive unit tests for MiMo provider

MiMo API is compatible with OpenAI API format, making it easy to integrate
with the existing HTTPProvider infrastructure.

Users can now use MiMo by configuring:
{
  "model_name": "mimo",
  "model": "mimo/mimo-v2-pro",
  "api_key": "your-mimo-api-key"
}

* hassas dosyaları kaldırma

* Add .security.yml and onboard to .gitignore
2026-03-25 23:29:44 +08:00
Liu Yuan 155af28841 feat(logger): add PICOCLAW_LOG_FILE env var for file-only logging 2026-03-25 21:34:27 +08:00
BeaconCat a97d433902 docs: update WeChat community QR code (#2003)
Co-authored-by: BeaconCat <BeaconCat@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 21:27:54 +08:00
Huangting-xy 6b1d08f454 docs(configuration): add security config reference at document start
Add a prominent reference to security_configuration.md at the beginning
of the configuration guide. This helps new users quickly find
information about storing API keys in .security.yml.

Fixes #1986

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 21:22:42 +08:00
Liu Yuan 3f1ac297d4 feat(tools): add exec tool enhancement with background execution and PTY support (#1752)
- Unified exec tool with actions: run/list/poll/read/write/send-keys/kill
- PTY support using creack/pty library
- Process session management with background execution
- Process group kill for cleaning up child processes
- Session cleanup: 30-minute TTL for old sessions
- Output buffer: 100MB limit with truncation

Actions:
- run: execute command (sync or background)
- list: list all sessions
- poll: check session status
- read: read session output
- write: send input to session stdin
- send-keys: send special keys (up, down, ctrl-c, enter, etc.)
- kill: terminate session

Tests:
- PTY: allowed commands, write/read, poll, kill, process group kill
- Non-PTY: background execution, list, read, write, poll, kill, process group kill
- Session management: add/get/remove/list/cleanup
2026-03-25 21:02:49 +08:00
Guoguo e4f4afcd4d fix(release): ignore nightly tags in goreleaser changelog (#1999)
GoReleaser was picking nightly tags as the "previous tag" when
generating changelogs, causing release changelogs to be incomplete.
Add git.ignore_tags to skip nightly tags.
2026-03-25 18:49:44 +08:00
Alix-007 59babde0cf docs: clarify cron disabled execution behavior 2026-03-25 18:34:20 +08:00
Alix-007 f30f57bfab docs: add cron job behavior guide 2026-03-25 18:28:04 +08:00
hezixu 123b9db6a9 fix: correct struct field alignment for gci 2026-03-25 18:03:27 +08:00
hezixu dc956f2feb feat(config): allow placeholder text to be string or list
Allow PlaceholderConfig.Text to accept either a single string or an
array of strings, from which one is randomly selected at runtime.
This maintains backward compatibility with existing single-string configs
while enabling random placeholder selection.

Changes:
- Modify PlaceholderConfig.Text type from string to FlexibleStringSlice
- Add GetRandomText() helper method for random selection
- Update SendPlaceholder in all channels to use GetRandomText()
- Update config.example.json with array placeholder examples
- Update Matrix channel documentation
2026-03-25 17:57:11 +08:00
taorye 5f50ae5e76 Merge pull request #1997 from wj-xiao/fix/freebsd-build
fix(build): disable Matrix gateway import on freebsd/arm
2026-03-25 17:06:36 +08:00
wenjie 51f8285f93 fix(build): disable Matrix gateway import on freebsd/arm
Exclude the Matrix gateway shim from freebsd/arm builds because
modernc.org/libc currently fails to compile on that target.
Document the upstream 32-bit FreeBSD codegen mismatch as well.
2026-03-25 16:59:11 +08:00
taorye ee03d1247d chore(tui): add build target for picoclaw-launcher TUI and create README for TUI launcher (#1995) 2026-03-25 16:17:26 +08:00
wenjie eb307e942b feat(web): add WeCom QR binding flow to channel settings (#1994)
- add backend WeCom QR flow endpoints and in-memory flow state management
- add frontend WeCom binding UI with QR polling and channel enable toggle
- update channel config behavior and i18n strings for WeCom and WeChat
- apply minor formatting cleanup in model-related components
2026-03-25 16:15:04 +08:00
lxowalle 6bd8fec87a Fix security config precedence during migration (#1984)
* Fix security config precedence during migration

* add doc

* fix ci

* add baidu search
2026-03-25 15:29:43 +08:00
BeaconCat 77d4716a82 config: add baidu_search example to config.example.json (#1990)
Add Baidu Qianfan AI Search configuration block after glm_search,
matching the BaiduSearchConfig struct defaults (enabled: false,
max_results: 10).

Co-authored-by: BeaconCat <BeaconCat@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 15:11:02 +08:00
Luo Peng 95204dbf17 fix(lint): remove CGO_ENABLED=0 for lint and fix (#1989)
* fix(lint): remove CGO_ENABLED=0 for lint and fix

* fix makefile
2026-03-25 14:42:29 +08:00
taorye aa9bd69f6e Merge pull request #1985 from wj-xiao/refactor/split-systray
refactor(web): split systray platform build files
2026-03-25 14:25:21 +08:00
xiwuqi 85dfb341a8 fix(agent): suppress heartbeat tool feedback (#1937) 2026-03-25 14:22:41 +08:00
daming大铭 3d20976803 Merge pull request #1948 from cytown/fix-doc
update security migration documents
2026-03-25 14:14:26 +08:00
wenjie a10036a7f1 refactor(web): clean up systray platform build files
Separate embedded tray icons into platform-specific files, rename the
no-cgo systray stub for consistency, and add the app version to the
launcher startup log.
2026-03-25 11:44:57 +08:00
Cytown 4398e3e070 Merge branch 'main' into fix-doc 2026-03-25 11:13:17 +08:00
柚子 3b3062abe8 feat(models): add extra_body config field in model add/edit UI (#1969)
* Add extraBody field to model configuration forms

This adds a new field allowing users to specify additional JSON fields
to inject into the request body when configuring models.

* Handle ExtraBody clearing when frontend sends empty object

The backend now interprets an empty object sent from the frontend as a
signal to clear the ExtraBody field, while nil/undefined preserves the
existing value. Frontend changed to send {} instead of undefined when
the field is empty.
2026-03-25 11:11:02 +08:00
柚子 adf1a5749d feat(config): add command pattern detection tool in exec settings (#1971)
* Add command pattern testing endpoint and UI tool

Adds a new API endpoint `/api/config/test-command-patterns` that tests a
command against configured whitelist and blacklist patterns, along with
a frontend UI component to interactively test patterns.

* Only process deny patterns when enableDenyPatterns is true
2026-03-25 10:19:20 +08:00
kathent ae021ef843 fix: more accurate deny pattern for disk wiping 2026-03-25 10:14:16 +08:00
daming大铭 8da0638ee3 Merge pull request #1976 from alexhoshina/refactor/wecom
docs(wecom): align docs with unified channel
2026-03-25 00:19:10 +08:00
daming大铭 eee74f3d97 Merge pull request #1977 from uiYzzi/fix/virtual-models-bug
fix: prevent virtual models from being persisted when save
2026-03-25 00:14:39 +08:00
uiyzzi be6bf9f6c6 Add virtual model support for multi-key expansion
Virtual models generated from multi-key expansion are now marked and
filtered during config persistence. Virtual models display with a badge
in the UI and cannot be set as default.
2026-03-25 00:00:36 +08:00
xiwuqi 9fb01bc7f8 fix(config): persist disabled placeholder settings (#1902) 2026-03-24 23:49:01 +08:00
Luo Peng 2ccac1819c fix(build): exclude matrix on unsupported mipsle and netbsd targets (#1975) 2026-03-24 23:40:51 +08:00
daming大铭 1b9445b806 Merge pull request #1955 from alexhoshina/refactor/wecom
Refactor/wecom
2026-03-24 23:37:35 +08:00
Christoforus Surjoputro 08fa9bb64b fix: agent triggered on empty message in telegram (#1927)
* add handler for empty message

* fix undefined: time

* fix linter

* update test to remove 100ms wait time since the handleMessage publishes synchronously
2026-03-24 23:31:03 +08:00
LC 6aff5b7ccd fix(pico): use O(1) session indexing and harden websocket concurrency handling (#1970)
* perf(pico): implement O(1) session lookup for pico connections

- Replace `sync.Map` with `connections` and `sessionConnections`.
- Add `addConnection`, `removeConnection`, `sessionConnectionsSnapshot`, and `takeAllConnections` with `connsMu` for concurrency.
- `broadcastToSession` now dispatches directly to `sessionConnections`.
- Add `newUniqueConnID` to avoid UUID collision/overwrites.
- Ensure `Stop` and `readLoop` use the new helpers for safe cleanup and correct `connCount` updates.

* refactor(pico): replace addConnection with createAndAddConnection for atomic connID generation

* refactor(pico): clear connections in one time to improve perf

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix(pico): keep connCount consistent with connection indexes

* refactor(pico): make connCount a regular int guarded by connsMu

* fix(pico): enforce MaxConnections atomically on registration

* fix(pico): use temporary over-limit error and remove conn counter

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-24 23:25:27 +08:00
daming大铭 9381da29bf feat(matrix): support encrypted messages with E2EE
Merging after review. E2EE support for Matrix channel.
2026-03-24 22:52:34 +08:00
Mauro 2a0efb6e52 Merge pull request #1889 from afjcjsbx/fix/binary-tool-output-handling
fix(tool): route binary outputs through the media pipeline
2026-03-24 15:37:06 +01:00
Hoshina 94fe54b9f6 docs(wecom): align docs with unified channel 2026-03-24 21:53:35 +08:00
RussellLuo 74a9dcaa5c fix(ci): Make CI happy 2026-03-24 21:51:58 +08:00
RussellLuo 7f163658c9 docs(matrix): Update docs 2026-03-24 21:46:12 +08:00
RussellLuo fab9603547 feat(matrix): support encrypted messages with E2EE
- Add `crypto_database_path` and `crypto_passphrase` configuration
- Integrate cryptohelper for decrypting `m.room.encrypted` events
- Handle both plaintext and encrypted messages in `handleMessageEvent`
- Enable `goolm` build tag for libsignal crypto support

Fixes #1840.
2026-03-24 21:46:10 +08:00
wenjie 4d7a629b79 feat(web): improve Weixin channel binding flow (#1968)
- persist Weixin bindings, enable the channel automatically, and try to restart the gateway
- refresh frontend channel and gateway state after successful binding
- harden QR polling state handling and update related channel UI behavior
- localize sidebar channel priority, add Weixin icon support, and add backend test coverage
2026-03-24 20:33:32 +08:00
Hoshina cd48c3bde8 fix(config): remove stale wecom security merge fields 2026-03-24 20:27:31 +08:00
Hoshina 3b498d2e4b feat(wecom): add channel-side streaming support 2026-03-24 20:23:29 +08:00
Hoshina 11b6b10d59 fix(linter): fix ci lint err 2026-03-24 20:23:29 +08:00
Hoshina c3631d84ba feat(wecom): send media via temp uploads 2026-03-24 20:23:29 +08:00
Hoshina e760cb737c feat(auth): add wecom cli qr login 2026-03-24 20:23:29 +08:00
Hoshina b0bcf1d3c9 docs(wecom): update examples and docs 2026-03-24 20:23:29 +08:00
Hoshina a1f95f02bc refactor(wecom): rebuild ai bot channel 2026-03-24 20:23:29 +08:00
lxowalle 8b6cbd9909 Fix: Prevent security.yml from being overwritten during config migration (#1966) 2026-03-24 20:02:58 +08:00
美電球 f2f6987f00 test(agent): allow mock custom tool args (#1965) 2026-03-24 19:27:29 +08:00
hsguo fa5ab72022 WeChat Web QR Code Integration (#1961) 2026-03-24 18:37:41 +08:00
Sabyasachi Patra fcc20ec72c feat(tools): add tool argument schema validation before execution (#1877)
Validate tool call arguments against each tool's Parameters() JSON Schema
in ExecuteWithContext() before calling Execute(). This prevents type
confusion, argument injection, and missing-field errors from reaching tools.

Validates: required fields, type matching (string/integer/number/boolean/
array/object), enum membership, nested objects (recursive), array element
types. Rejects unexpected extra properties unless additionalProperties is
set to true (for MCP tool compatibility).

Returns ToolResult{IsError: true} on failure so the LLM can self-correct.

Ref: Security Hardening > Tool abuse prevention via strict parameter validation
2026-03-24 18:35:56 +08:00
taorye ff50ffa123 Merge pull request #1962 from wj-xiao/fix/configure-pico-channel
fix(web): auto-configure Pico channel on launcher startup
2026-03-24 18:26:25 +08:00
wenjie dea99da7d9 fix(web): auto-configure Pico channel on launcher startup
Export EnsurePicoChannel and reuse it during launcher and gateway startup
so the Pico channel is initialized earlier with a generated token when
needed.

Also extend backend tests to cover startup-time Pico setup behavior and
keep the setup path idempotent.
2026-03-24 18:06:29 +08:00
wenjie ffbcbea4dc fix(web): persist api_key when adding models (#1958)
Make POST /api/models capture the request's api_key and store it via
ModelConfig.SetAPIKey before saving config, so newly added models keep
their credentials in the security config.

Add a backend API test covering model creation with api_key persistence.
2026-03-24 17:31:28 +08:00
wenjie d23c24ce72 fix(config): normalize empty security config before save/load (#1956)
Normalize missing security sections when attaching, loading, and saving
security config so existing config files without `.security.yml` can still
be updated safely. This fixes Pico channel setup for legacy/existing configs
and adds coverage for the missing security file path and unexported JSON
field behavior.
2026-03-24 17:03:28 +08:00
daming大铭 b17cbe5234 fix: apply security credentials before config validation in web handlers
Merge PR #1929
2026-03-24 16:27:57 +08:00
Cytown d921bbb667 bug fix for security initial cause can't save model in launcher (#1952) 2026-03-24 16:24:12 +08:00
daming大铭 6e31f15467 fix(web): ensure at least 40% of the characters are hidden for api key
Merge pull request #1944 from lc6464/fix/web/mask-api-key
2026-03-24 15:54:51 +08:00
lc6464 1ef2b6903d test(web): add percentage checking of characters displaying in APIKey 2026-03-24 13:54:04 +08:00
Cytown de11f95b65 update security migration documents 2026-03-24 13:38:13 +08:00
Hua Audio b23a6b3f54 Feat/move weixin login to auth and update docs (#1945)
* move weixin to auth and update docs

* fix ci test
2026-03-24 06:33:35 +01:00
lc6464 66d2efc9d1 test(web): add test for maskAPIKey 2026-03-24 12:36:31 +08:00
lc6464 f1ac1a1072 fix(web): ensure at least 40% of the characters are masked for api key
- keys longer than 12 chars show prefix + last 4 chars
- keys 9-12 chars show prefix + last 2 chars
- shorter keys are fully masked
2026-03-24 12:20:57 +08:00
LC ce1619051d fix(chat): avoid full secret exposure for 7-char secrets (#1942)
- ensure at least 40% of the characters are masked for secrets of length 4 or more
- secrets with length <= 6 now show first and last char with mask
- secrets with length <= 12 now show first two and last two chars
- longer secrets show 3 prefix and 4 suffix
2026-03-24 11:26:20 +08:00
Phạm Minh Đạt d805e12a60 Merge branch 'sipeed:main' into main 2026-03-24 09:58:46 +07:00
Cytown cf9e0496f7 fix launcher can't save model api_key issue (#1928)
* fix launcher can't save model api_key issue

* add backup for old data before migrate config and fix migrate to empty
security issue
2026-03-24 03:26:11 +01:00
Mauro aa3300c1bd feat(web): Tool feedback on UI (#1933)
* feat(web): tool feedback

* feat(web): tool feedback

* fix test
2026-03-24 09:19:51 +08:00
美電球 69cf9342e1 Merge pull request #1938 from huaaudio/fix/weixin-load
fix weixin config loading incorrectly
2026-03-24 08:37:34 +08:00
Huaaudio 6ea9636861 fix weixin config 2026-03-24 01:33:05 +01:00
Orkun Manap dd9adf8a04 feat: add ElevenLabs Scribe STT transcriber and Telegram SendVoice support (#1905)
* feat: add ElevenLabs Scribe STT transcriber and Telegram SendVoice support

Add ElevenLabsTranscriber as an alternative speech-to-text provider using
the ElevenLabs Scribe API (scribe_v1). This enables voice message
transcription for users who already have an ElevenLabs API key, without
requiring a separate Groq account.

Changes:
- Add ElevenLabsTranscriber implementing the Transcriber interface
- Update DetectTranscriber to check providers.elevenlabs.api_key first,
  falling back to Groq for backward compatibility
- Add ElevenLabs to ProvidersConfig
- Add "voice" media type for OGG files with "voice" in filename
- Add SendVoice support in Telegram channel for voice bubble messages
- Add comprehensive tests for ElevenLabs transcriber

Configuration:
  "providers": {
    "elevenlabs": {
      "api_key": "sk_your_key_here"
    }
  }

Closes #1503 (partial)

* fix: move voice-bubble detection into Telegram channel to avoid regression in other channels

Address review feedback: keep inferMediaType returning "audio" for all
OGG files. Voice-bubble detection (SendVoice vs SendAudio) is now done
inside the Telegram channel based on filename, so other channels that
map "audio" explicitly are unaffected.

* fix: align VoiceConfig struct tags to pass golines formatter

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(agent): use ModelName in loop test added by upstream

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 22:11:10 +01:00
美電球 f06173a5e0 fix(qq): preserve filenames in file uploads (#1913) 2026-03-23 22:00:15 +01:00
daming大铭 2c48cd3461 Merge pull request #1907 from xiwuqi/wuxi/fix-reasoning-channel-content
fix(agent): route reasoning_content to reasoning channel
2026-03-24 01:24:14 +08:00
Andy Lo-A-Foe b787131c82 feat(providers): add AWS Bedrock provider (#1903)
Add support for AWS Bedrock as an LLM provider using the Converse API.
The implementation is behind a build tag (-tags bedrock) to keep the
default binary size small.

Features:
- AWS SDK v2 with automatic credential chain (env vars, profiles, IAM roles)
- Converse API for unified access to Claude, Llama, Mistral models
- Tool/function calling support with proper document handling
- Image support with base64 decoding and size limits
- Request timeout configuration
- Region validation and endpoint resolution for all AWS partitions

Usage:
  go build -tags bedrock
  model: bedrock/us.anthropic.claude-sonnet-4-20250514-v1:0
  api_base: us-east-1  (or full endpoint URL)
2026-03-24 01:10:56 +08:00
daming大铭 40571996b1 Merge pull request #1930 from uiYzzi/feat/filter-sensitive-data-from-tool-results
feat(security): filter sensitive data from tool results before sending to LLM
2026-03-24 00:07:17 +08:00
afjcjsbx 5d5536a1a6 fix delivery and steering 2026-03-23 14:09:52 +01:00
uiyzzi cf80ec8382 Update config_test.go 2026-03-23 20:58:14 +08:00
uiyzzi 16d23d8cdc feat(security): add sensitive data filtering for tool results sent to
LLM

Prevent LLM from seeing its own credentials (API keys, tokens, secrets)
by filtering sensitive values from tool call results before sending to
the
model. Values are collected from .security.yml and replaced with
[FILTERED] using an efficient strings.Replacer (O(n+m)).

- Add FilterSensitiveData and FilterMinLength to ToolsConfig
- Implement SensitiveDataReplacer() with sync.Once caching in
  SecurityConfig
- Use reflection to collect all sensitive values (Model API keys,
  channel
  tokens, web tool API keys, skills tokens)
- Apply filtering in agent loop at 4 tool result locations
- Add comprehensive tests covering all token types
2026-03-23 20:55:41 +08:00
afjcjsbx 8ed171dbe6 resolved conflicts 2026-03-23 13:43:02 +01:00
Kristjan Kruus 1f9d390a64 fix: apply security credentials before config validation in web handlers
- Move SecurityCopyFrom() before validateConfig() in PUT and PATCH handlers
- Make SecurityCopyFrom() call applySecurityConfig() to populate private fields
- Add tests for config save with security-only channel tokens

Without this fix, saving config via the web UI fails with 'channels.pico.token
is required' (and similar for Telegram/Discord) when tokens are stored in
.security.yml, because the validation ran before security credentials were
copied to the config struct.
2026-03-23 14:26:51 +02:00
afjcjsbx fddfd56b50 Merge branch 'main' into fix/binary-tool-output-handling
# Conflicts:
#	pkg/agent/loop.go
#	pkg/agent/loop_test.go
#	pkg/commands/builtin_test.go
#	pkg/tools/send_file_test.go
2026-03-23 13:16:23 +01:00
美電球 96e312680d Merge pull request #1926 from cytown/fix
fix for ci/cd
2026-03-23 18:25:34 +08:00
Cytown d77375721a fix for ci/cd 2026-03-23 18:15:16 +08:00
美電球 4e3769e989 fix(agent): use ModelName in loop tests (#1923) 2026-03-23 17:28:15 +08:00
LC 8e3e517135 feat: render mixed Markdown+HTML in assistant messages and skills (#1900)
* feat(chat): render mixed Markdown+HTML in assistant messages using rehype-raw + rehype-sanitize (safe default)

* build: remove irrelevant changes of pnpm-lock.yaml

* feat(skills): enable rendering of Markdown with HTML in skill details using rehype-raw and rehype-sanitize

* fix(agent): use ModelName in loop tests
2026-03-23 17:25:27 +08:00
Liqiang Liu f81b44bf19 fix(provider): deduplicate tool results and merge consecutive tool_result blocks for Anthropic API (#1793)
Anthropic API returns 400 when multiple tool_result blocks share the same
tool_use_id, or when consecutive tool results are sent as separate user
messages. This fix:

1. Adds ToolCallID deduplication in sanitizeHistoryForProvider (context.go)
   to drop duplicate tool results before sending to any provider.
2. Merges consecutive tool result messages into a single user message with
   multiple tool_result content blocks in Anthropic's buildRequestBody,
   for both "user" (with ToolCallID) and "tool" role messages.
3. Adds tests for both behaviors.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 17:24:46 +08:00
lc6464 1961aab850 fix(agent): use ModelName in loop tests 2026-03-23 17:23:33 +08:00
daming大铭 e7ee80ff32 Merge pull request #1878 from uiYzzi/feat/provider-extra-body-config
feat(providers): add extra_body config to inject custom fields into request body
2026-03-23 17:23:21 +08:00
daming大铭 c3285625b0 Merge pull request #1918 from cytown/panic
Merging - approved after code review with Codex adversarial check. All CI checks passed.
2026-03-23 17:20:11 +08:00
uiyzzi 02393b3087 Merge branch 'feat/provider-extra-body-config' of github.com:uiYzzi/picoclaw into feat/provider-extra-body-config
# Conflicts:
#	pkg/config/config_test.go
2026-03-23 16:49:43 +08:00
uiyzzi d1d2155edb Use ModelName instead of Model in test config structs 2026-03-23 16:47:13 +08:00
uiyzzi c7544f7cb9 feat(providers): add extra_body config to inject custom fields into request body
Allow configuring provider-specific fields like reasoning_split for minimax via
the model config's extra_body map. These fields are merged into the request
body last, giving them precedence over default values.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 16:44:12 +08:00
uiyzzi 79df938696 Use getter/setter methods for API key access in ModelConfig 2026-03-23 16:39:43 +08:00
uiyzzi 608ec6d329 Move minimax reasoning_split injection to provider factory
Inject reasoning_split at provider creation time to allow user ExtraBody
settings to be preserved
2026-03-23 16:39:43 +08:00
uiyzzi f2985b8bee feat(providers): add extra_body config to inject custom fields into request body
Allow configuring provider-specific fields like reasoning_split for minimax via
the model config's extra_body map. These fields are merged into the request
body last, giving them precedence over default values.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 16:39:42 +08:00
uiyzzi b24c577e38 Add security config to ExtraBody round-trip test 2026-03-23 16:29:25 +08:00
Mauro 054b55fdfc Merge pull request #1893 from afjcjsbx/feat/skill-channel-commands
feat(skills): add channel commands to list and force installed skills
2026-03-23 09:04:06 +01:00
uiyzzi 7767feb724 Merge branch 'feat/provider-extra-body-config' of github.com:uiYzzi/picoclaw into feat/provider-extra-body-config
# Conflicts:
#	pkg/config/config_test.go
#	pkg/providers/factory_provider.go
#	pkg/providers/factory_provider_test.go
2026-03-23 15:54:02 +08:00
uiyzzi 2d9517c655 Use getter/setter methods for API key access in ModelConfig 2026-03-23 15:51:13 +08:00
uiyzzi 53c6dd3812 Move minimax reasoning_split injection to provider factory
Inject reasoning_split at provider creation time to allow user ExtraBody
settings to be preserved
2026-03-23 15:46:04 +08:00
uiyzzi 8a046e951a feat(providers): add extra_body config to inject custom fields into request body
Allow configuring provider-specific fields like reasoning_split for minimax via
the model config's extra_body map. These fields are merged into the request
body last, giving them precedence over default values.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 15:45:45 +08:00
Cytown df17684dd4 implement panic log for gateway and launcher
add file logger to gateway

ref issue: #1734

Signed-off-by: Cytown <cytown@gmail.com>
2026-03-23 15:40:30 +08:00
柚子 6a5a4a5617 Merge branch 'main' into feat/provider-extra-body-config 2026-03-23 15:33:04 +08:00
daming大铭 cff9065084 Merge pull request #1352 from cytown/version
refactor Config to add Version and migratable
2026-03-23 15:06:44 +08:00
Cytown 36f9d20de1 Merge branch 'main' into version 2026-03-23 15:00:18 +08:00
xiwuqi d014f3e989 fix(api): include auth header in local model probe (#1896) 2026-03-23 13:41:40 +08:00
Kunal Karmakar 40279c8dde chore(config): move loglevel settings under gateway (#1912)
* Move log level config to gateway property

* Fix unit test

* Fix linting

* Fix linting

* Add comment for log level
2026-03-23 06:14:53 +01:00
美電球 75270c4777 Fix 1886 media cleanup policy (#1887)
* fix(media): track cleanup ownership per path

Add explicit cleanup policy handling to MediaStore and count refs by path before deleting the underlying file. This prevents cleanup from removing shared files until the final ref is gone.

Refs #1886

* fix(tools): keep send_file refs forget-only

Mark send_file media registrations as forget-only so cleanup drops the ref without deleting the original workspace file.

Refs #1886

* fix(channels): declare managed media cleanup policy

Explicitly mark downloaded and managed channel media as delete-on-cleanup so media ownership is visible at each registration site.

Refs #1886
2026-03-23 12:13:59 +08:00
Cytown 5a8aab8143 Merge branch 'main' into version 2026-03-23 11:41:36 +08:00
Cytown 310f788f5f rename security.yml to .security.yml 2026-03-23 11:20:42 +08:00
Cytown 7bf4831059 Merge branch 'main' into version 2026-03-23 10:54:08 +08:00
Caize Wu 3a61892313 Merge pull request #1875 from BeaconCat/docs/readme-restructure-v2
docs: restructure README with Quick Start Guide, i18n, and Weixin channel
2026-03-23 10:30:05 +08:00
BeaconCat 48cba906cd fix: restore missing assets and address Copilot review comments
- Add hardware-banner.jpg, launcher-webui.jpg, launcher-tui.jpg (lost in
  previous force push)
- Add io.LimitReader (1MB) to BaiduSearchProvider response body read
- Add no-results fallback and "Results for: ... (via Baidu Search)" header
- Add api_keys field to Brave and Perplexity tables in fr/ja/pt-br/vi
  tools_configuration.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 10:21:06 +08:00
xiwuqi 336d5d4c07 fix(agent): route reasoning_content to reasoning channel 2026-03-22 19:57:47 -05:00
Mauro 3500080abb Merge pull request #1891 from RussellLuo/audio-transcription
feat(voice): add audio-model transcription support
2026-03-23 00:23:30 +01:00
RussellLuo d4e56bc3d5 Fix lint 2026-03-23 07:13:43 +08:00
afjcjsbx 1e98f86fa9 fix Ooutboundmedia 2026-03-23 00:08:43 +01:00
afjcjsbx f735b0551c fix 2026-03-22 23:46:10 +01:00
afjcjsbx 388505d7e0 fix lint 2026-03-22 23:39:33 +01:00
afjcjsbx b90c5007f6 resolve conflicts 2026-03-22 23:36:25 +01:00
afjcjsbx 14a4983af3 Merge branch 'main' into fix/binary-tool-output-handling
# Conflicts:
#	pkg/agent/loop.go
#	pkg/tools/result.go
2026-03-22 23:08:27 +01:00
afjcjsbx be59133ce9 resolve conflicts 2026-03-22 20:58:46 +01:00
afjcjsbx d3ba40090b Merge branch 'main' into feat/skill-channel-commands
# Conflicts:
#	pkg/agent/loop.go
2026-03-22 20:51:16 +01:00
BeaconCat 4bc64497e3 fix(lint): run golangci-lint fmt to fix golines/gci struct tag formatting
golangci-lint v2.10.1 treats golines as a formatter. Running
`golangci-lint fmt` normalizes struct tag alignment in GLMSearchConfig,
WebToolsConfig, and MCPConfig — removing manual padding that golines
flagged as improperly formatted.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 01:10:09 +08:00
BeaconCat c786f35b32 fix(lint): fix golines/gci formatting in WebToolsConfig
Run golines then gci to reach a stable state that satisfies both linters.
BaiduSearchConfig field caused gofumpt to re-align the struct, shifting
ToolConfig tag spacing and triggering golines on each subsequent fix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 01:03:49 +08:00
BeaconCat b150d7d523 fix(lint): fix gci import formatting in config.go
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 00:57:14 +08:00
BeaconCat 30db993993 fix(lint): fix golines line length in WebToolsConfig struct
Remove extra alignment space on ToolConfig field introduced by gofumpt
when BaiduSearchConfig was added, keeping all lines under 120 chars.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 00:54:50 +08:00
BeaconCat 60a7098fd3 feat(search): add Baidu Qianfan AI Search provider with i18n docs
- Add BaiduSearchConfig struct and register in WebToolsConfig/defaults
- Insert Baidu Search in priority chain: DuckDuckGo > Baidu > GLM Search
- Use perplexityTimeout (30s) — Qianfan is LLM-based
- Fix response parsing: use references[] field per API spec
- Add baidu_search block to config.example.json

docs: sync configuration.md and README Documentation table across all languages

- Complete truncated configuration.md for fr/ja/pt-br/vi/zh: add Spawn
  async flow diagram, Providers table, Model Configuration (all vendors,
  examples, load balancing, migration), Provider Architecture, Scheduled
  Tasks, and Advanced Topics links
- Add Hooks/Steering/SubTurn entries to Documentation table in all 8
  READMEs (en/zh/fr/id/it/ja/pt-br/vi), ordered before Troubleshooting
- Add Baidu Search row to web search table in all 8 READMEs and
  tools_configuration.md (en + 5 i18n); zh README reorders search
  engines with China-friendly options first
- Add Matrix channel docs translations (fr/ja/pt-br/vi)
- Add Weixin channel to chat-apps.md and all README Channels tables

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 00:51:27 +08:00
RussellLuo fca01583bf fix(lint): align VoiceConfig env tags 2026-03-23 00:03:44 +08:00
RussellLuo 4d2b244522 refactor(voice): share audio format support and restrict transcriber selection 2026-03-22 23:40:13 +08:00
daming大铭 4d84bd90cd Merge pull request #1894 from sipeed/refactor/agent
refactor(agent): consolidate Agent model - Phase 1 complete
2026-03-22 23:26:33 +08:00
yinwm 5790d3e9dd docs(it): add model command to CLI Reference 2026-03-22 22:56:51 +08:00
yinwm 6f1737eb73 docs: sync CLI Reference across all README translations
- Add `picoclaw model` command to English README
- Add `picoclaw model` command to Indonesian README

All other translations already had the command.
2026-03-22 22:55:08 +08:00
yinwm 6df5ea170e docs: add picoclaw model command to CLI Reference
The model command was missing from the README CLI Reference table.
2026-03-22 22:48:50 +08:00
yinwm 724cc1bd33 fix: resolve merge conflict markers in README files
Use main branch versions which have complete content.
2026-03-22 22:41:39 +08:00
afjcjsbx d7d2bf69bf feat(skills): add channel commands to list and force installed skills 2026-03-22 15:33:25 +01:00
yinwm 1984bb5bbd fix(test): mock gateway health check in status tests
Two gateway tests were flaky due to race conditions:
- TestGatewayStatusReturnsRestartingDuringRestartGap
- TestGatewayRestartReturnsErrorStatusWhenReplacementFailsToStart

The handleGatewayStatus function calls getGatewayHealth which can
override the test's expected status. By mocking gatewayHealthGet
to return an error, the tests now reliably verify the expected
status values.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 22:21:27 +08:00
yinwm c48954d32d merge: sync main into refactor/agent 2026-03-22 21:44:17 +08:00
daming大铭 729a878f73 Merge pull request #1636 from lppp04808/feat/subturn-poc
feat(agent): subturn
2026-03-22 21:17:33 +08:00
RussellLuo 92678d1700 docs(voice): Update docs for audio-transcription 2026-03-22 21:04:10 +08:00
afjcjsbx 930dd028f1 fix err and placeholder 2026-03-22 13:47:23 +01:00
uiyzzi de0364c8ec Move minimax reasoning_split injection to provider factory
Inject reasoning_split at provider creation time to allow user ExtraBody
settings to be preserved
2026-03-22 20:37:06 +08:00
Administrator 7868c5811a fix(agent): fix subturn panic result, hard abort rollback, and drain bus exit
- spawnSubTurn: set result=nil on panic instead of constructing a non-nil ToolResult
- HardAbort: roll back session history to initialHistoryLength after Finish()
- drainBusToSteering: switch to non-blocking reads after first message so function
  returns promptly when the inbound channel is empty
- remove obsolete documentation files
2026-03-22 20:35:14 +08:00
RussellLuo 8ad4b9b497 feat(voice): add audio-model transcription support
- Add `AudioModelTranscriber` for model-based audio transcription via LLM providers
- Support selecting a transcription model with `voice.model_name` in config
- Keep Groq transcription as a fallback and move it into dedicated files with focused tests
- Serialize `data:audio/...` media as input_audio for OpenAI-compatible providers
- Improve transcription logging by rendering error fields as strings
- Add coverage for transcriber detection, audio-model behavior, provider audio serialization, and Groq transcription

Fixes #1890.
2026-03-22 20:07:22 +08:00
Cytown 284ced1f5c Merge branch 'main' into version 2026-03-22 19:58:33 +08:00
Administrator 7ba8682ac5 Merge branch 'refactor/agent' into feat/subturn-poc 2026-03-22 19:51:43 +08:00
Administrator f7f27e237a merge: resolve conflicts between refactor/agent and main 2026-03-22 19:21:58 +08:00
afjcjsbx df4f322f09 fix(tool): route binary outputs through the media pipeline. 2026-03-22 12:05:28 +01:00
Mauro 2f6f25dc58 Merge pull request #1882 from lc6464/frontend-fix
fix(chat): preserve blank lines and add input hint
2026-03-22 10:48:59 +01:00
daming大铭 809aef87d7 Merge pull request #1885 from alexhoshina/fix-1884-qq-long-audio-file-fallback
fix(qq): send long audio as file
2026-03-22 17:37:29 +08:00
Hoshina 2c317444c5 fix(qq): send long audio as file
Downgrade outbound QQ audio to file upload when it exceeds the 60 second voice limit or its duration cannot be detected.

Refs #1884
2026-03-22 17:19:11 +08:00
lc6464 7eaadfd273 fix(chat): preserve blank lines and add input hint
- Add Tailwind `whitespace-pre-wrap` to the user message bubble of web chat so spaces and blank lines can be rendered correctly.
- Update chat input placeholders in en.json and zh.json to show Enter vs Shift+Enter.
2026-03-22 15:59:19 +08:00
uiyzzi a005e5bb70 feat(providers): add extra_body config to inject custom fields into request body
Allow configuring provider-specific fields like reasoning_split for minimax via
the model config's extra_body map. These fields are merged into the request
body last, giving them precedence over default values.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 15:49:25 +08:00
daming大铭 0432facffc Merge pull request #1863 from alexhoshina/feat/hook-manager
Feat/hook manager
2026-03-22 14:36:07 +08:00
Hua Audio dd82794255 Feat/weixin openclaw port (#1873)
* init

* fix lint

* fix go test

* update docs

* incorporate pr review

* Update pkg/channels/weixin/weixin.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* feat(weixin): add media sync and typing support

* test(weixin): cover media and sync helpers

---------

Co-authored-by: zhangmikoto <i@electromaster.me>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Hoshina <hoshina@evaz.org>
2026-03-22 14:23:39 +08:00
daming大铭 04def0f10b Merge pull request #1844 from afjcjsbx/fix/scope-steering
fix(agent) scope steering
2026-03-22 14:22:43 +08:00
Administrator 482c88cd15 remove merge conflict markers from .gitignore 2026-03-22 13:48:03 +08:00
Administrator 88d754b172 merge main 2026-03-22 13:47:14 +08:00
daming大铭 931eee92a0 Merge pull request #1853 from kunalk16/feat-configurable-logger
feat(logging): add configurability for log levels preference
2026-03-22 13:27:06 +08:00
Caize Wu 9107740781 Merge pull request #1857 from lc6464/main
docs: clean up README and update QRCode
2026-03-22 11:12:30 +08:00
Cytown 3dfe484f66 make yaml indent with 2 2026-03-22 11:07:22 +08:00
Cytown 4e876ebeee remove useless logs output 2026-03-22 09:52:25 +08:00
Mauro c0bb8d6df9 Merge pull request #1617 from yzxlr/codex/fix-1561-heartbeat-template-idle
fix(heartbeat): ignore untouched default template
2026-03-21 20:18:39 +01:00
Mauro e6ea9c4ff3 Merge pull request #1855 from badgerbees/fix/telegram-group-id-validation
fix(identity): support negative integers in isNumeric for Telegram group IDs
2026-03-21 20:16:49 +01:00
Cytown 7c854fe6d7 Merge branch 'main' into version 2026-03-22 02:53:55 +08:00
Mauro 7a47d7a55c Merge pull request #1782 from biisal/chore/docker-data-in-gitignore
chore: Ignore the `docker/data` directory.
2026-03-21 19:52:25 +01:00
Mauro 5286464bfc Merge pull request #1861 from amirmamaghani/feat/agent-browser-skill-heavy-dockerfile
feat: add agent-browser skill and Dockerfile.heavy
2026-03-21 19:47:07 +01:00
Cytown e455eb5e67 refactor: seperate security.yml for store keys 2026-03-22 01:55:00 +08:00
daming大铭 3cd674e3b8 Merge pull request #1865 from sipeed/revert-1752-feat/exec-tool-enhancement
Revert "feat(tools): add exec tool enhancement with background execution and PTY support"
2026-03-22 00:46:56 +08:00
daming大铭 ebcd5645f1 Revert "feat(tools): add exec tool enhancement with background execution and …"
This reverts commit f901af8cbc.
2026-03-22 00:39:47 +08:00
Administrator 24d6cb5272 Merge branch 'upstream-main' into feat/subturn-poc 2026-03-21 23:42:25 +08:00
Hoshina 9978c9550b docs(hooks): inline and translate hook examples 2026-03-21 23:18:29 +08:00
Liu Yuan f901af8cbc feat(tools): add exec tool enhancement with background execution and PTY support (#1752)
- Unified exec tool with actions: run/list/poll/read/write/send-keys/kill
- PTY support using creack/pty library
- Process session management with background execution
- Process group kill for cleaning up child processes
- Session cleanup: 30-minute TTL for old sessions
- Output buffer: 100MB limit with truncation

Actions:
- run: execute command (sync or background)
- list: list all sessions
- poll: check session status
- read: read session output
- write: send input to session stdin
- send-keys: send special keys (up, down, ctrl-c, enter, etc.)
- kill: terminate session

Tests:
- PTY: allowed commands, write/read, poll, kill, process group kill
- Non-PTY: background execution, list, read, write, poll, kill, process group kill
- Session management: add/get/remove/list/cleanup
2026-03-21 22:38:03 +08:00
Amir Mamaghani 520391643b feat: add agent-browser skill and Dockerfile.heavy with full runtime
Add agent-browser skill to the default workspace with complete CLI
reference for browser automation via Chrome/Chromium CDP. The skill
includes a runtime guard that checks for the binary before use.

Add Dockerfile.heavy — a batteries-included container image with:
- Node.js 24 + npm
- Python 3 + pip + uv
- Chromium + Playwright (for agent-browser)
- agent-browser CLI pre-installed
- Non-root picoclaw user (UID/GID 1000)
- Default workspace with all skills
- Persistent workspace volume

This complements the existing minimal Dockerfile and Dockerfile.full
for deployments that need browser automation and rich tool support.
2026-03-21 15:14:32 +01:00
Hoshina 337e43e5a5 feat(agent): add configurable hook mounting 2026-03-21 19:46:16 +08:00
Hoshina cf68c91eca feat(agent): add hook manager foundation 2026-03-21 19:15:10 +08:00
LC f71a6ff76c docs: update alt text of wechat.png with a more meaningful description
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-21 18:52:39 +08:00
lc6464 e2e3e6d5b0 docs: update WeChat QRCode for README 2026-03-21 18:40:10 +08:00
lc6464 ab93c235ae docs: clean up README by removing duplicate sections 2026-03-21 18:36:29 +08:00
Administrator 670b433f1a refactor: replace interface{} with any for improved type clarity 2026-03-21 18:24:56 +08:00
Badgerbees bc0be17e88 fix(identity): support negative integers in isNumeric for Telegram group IDs 2026-03-21 17:09:02 +07:00
Phạm Minh Đạt 6b9ceaa08f Merge branch 'sipeed:main' into main 2026-03-21 16:49:28 +07:00
Administrator 1bd144ac13 Merge branch 'upstream-main' into feat/subturn-poc 2026-03-21 17:13:26 +08:00
Administrator 087e8519c5 refactor: improve code readability and consistency across multiple files 2026-03-21 17:12:45 +08:00
Kunal Karmakar 073ae4864f Fix spelling 2026-03-21 07:20:59 +00:00
Kunal Karmakar 4c8526d917 Merge branch 'feat-configurable-logger' of https://github.com/kunalk16/picoclaw into feat-configurable-logger 2026-03-21 06:54:55 +00:00
Kunal Karmakar 647071d342 Add default value for config 2026-03-21 06:54:49 +00:00
Kunal Karmakar 92b7687068 Add configurable logger 2026-03-21 06:54:49 +00:00
Kunal Karmakar 650827103d Merge branch 'main' of https://github.com/sipeed/picoclaw into feat-configurable-logger 2026-03-21 06:53:17 +00:00
Kunal Karmakar f35516c5c9 Add default value for config 2026-03-21 06:53:05 +00:00
BeaconCat 6148ccc529 docs(feishu): note that Feishu channel does not support 32-bit devices (#1851)
Co-authored-by: BeaconCat <BeaconCat@users.noreply.github.com>
2026-03-21 14:36:51 +08:00
Phạm Minh Đạt f81269e77f Merge branch 'sipeed:main' into main 2026-03-21 12:40:26 +07:00
Kunal Karmakar 8490084640 Merge branch 'main' of https://github.com/sipeed/picoclaw into feat-configurable-logger 2026-03-21 05:18:55 +00:00
Kunal Karmakar 329322075d Add configurable logger 2026-03-21 05:18:25 +00:00
Mauro 100720bb74 Merge pull request #1818 from Alix-007/fix/issue-1815-empty-response-message
fix(agent): separate empty-response and tool-limit fallbacks
2026-03-20 23:23:48 +01:00
afjcjsbx 9e344594a2 fix logic 2026-03-20 21:07:07 +01:00
afjcjsbx 827449aff3 fix lint 2026-03-20 20:12:55 +01:00
afjcjsbx 1c6586681d fix(agent) scope steering 2026-03-20 19:44:00 +01:00
Phạm Minh Đạt 8f56cceb07 missing , 2026-03-20 22:17:17 +07:00
enoch.z e4b104c0de Update helpers.go
correction of the promt for the "picoclaw onboard" command.
2026-03-20 23:15:18 +08:00
Phạm Minh Đạt efbe806913 Fix bug double , 2026-03-20 22:15:13 +07:00
Phạm Minh Đạt 7970e2da15 Merge branch 'sipeed:main' into main 2026-03-20 21:49:29 +07:00
Phạm Minh Đạt ba1538f31d Fix: cannot create session github copilot 2026-03-20 21:48:29 +07:00
BeaconCat 403ceb39be docs: fix inaccuracies, add translations, and expand channel docs (#1837)
## Config field fixes (cross-verified against Go source)
- MaixCam: server_address → host + port
- IRC: use_tls → tls, channels_to_join → channels (all 6 languages)
- WeCom AI Bot: callback port 18791 → 18790
- credential_encryption: base_url → api_base, add required model field,
  remove incorrect passphrase-only mode docs
- providers.md: agents.defaults.model → model_name (×4), remove
  non-existent session.backlog_limit
- migration guide, troubleshooting: agents.defaults.model → model_name
- ANTIGRAVITY_AUTH: fix file path, Go 1.21 → 1.25, model → model_name
- spawn-tasks: fix truncated file, add Heartbeat introduction
- tools_configuration: add Tavily/SearXNG/GLMSearch, exec allow_remote/
  timeout_seconds/custom_allow_patterns, cron allow_command, skills
  github/search_cache, clawhub timeout/max_zip_size/max_response_size
- configuration: fix builtin skills path (build-time embedded, not cwd),
  HEARTBEAT.md marked auto-generated

## Broken link fixes (15 total)
- chat-apps.md: WeCom/Matrix links with wrong relative paths
- providers.md: migration link with extra docs/ prefix
- hardware-compatibility.md: README links with wrong depth (all 5 langs)
- chat-apps.md: WhatsApp dead links → anchor links (zh/ja)

## Getting-started accuracy
- README (all 6 langs): add picoclaw.io as recommended download,
  add missing picoclaw model CLI command
- docker.md: clarify first-run trigger condition (all 6 langs)
- configuration.md: fix builtin skills path description (all 6 langs)

## QQ channel
- Add quick setup via q.qq.com/qqbot/openclaw (one-click bot creation)
- Add manual setup as fallback (all 6 languages)

## Feishu channel
- Update setup flow: WebSocket/SDK mode, no webhook URL needed
- Preserve Lark international domain note (all 6 languages)

## chat-apps.md
- Add Feishu, Slack, IRC, OneBot detail sections (all 6 languages)
- Add MaixCam section to ja/fr/pt-br/vi
- Fix all channel doc links to point to correct language version

## New translations (25 files, 5 docs × 5 languages)
debug.md, credential_encryption.md, hardware-compatibility.md,
ANTIGRAVITY_AUTH.md, ANTIGRAVITY_USAGE.md → zh/ja/fr/pt-br/vi

## Channel docs (6 languages each, 60 new files)
telegram, discord, qq, feishu, maixcam, dingtalk, line, slack, onebot,
wecom/wecom_aibot, wecom/wecom_app, wecom/wecom_bot

Co-authored-by: BeaconCat <BeaconCat@users.noreply.github.com>
2026-03-20 22:37:05 +08:00
liqianjie 0fe058254c fix: add fallback DNS resolver for Android with multi-DNS support (#1835)
On Android, /etc/resolv.conf does not exist, causing Go's default DNS
resolution to fail. This adds an init() hook that:

1. Detects missing /etc/resolv.conf (Android environment)
2. Configures a custom resolver with PreferGo: true
3. Supports multiple DNS servers via PICOCLAW_DNS_SERVER env var
   - Semicolon-separated: "8.8.8.8:53;1.1.1.1:53"
   - Single server also works: "8.8.8.8"
   - Auto-appends :53 if port omitted
4. Round-robin rotation across configured servers
5. Defaults to Google DNS + Cloudflare DNS

Also patches http.DefaultTransport to use the custom resolver.
2026-03-20 22:32:21 +08:00
Amir Mamaghani 71134babb9 feat(telegram): stream LLM responses via sendMessageDraft (#1101)
* feat(telegram): stream LLM responses in real-time via sendMessageDraft

Implements real-time token streaming to Telegram using the sendMessageDraft
API (telego v1.6.0). Instead of showing only a "Thinking..." placeholder
until the full response arrives, users now see partial LLM output appear
in the chat as it's generated.

The streaming pipeline threads through all layers:

- StreamingProvider interface (providers/types.go): opt-in ChatStream()
  method that receives an onChunk callback with accumulated text
- OpenAI-compatible SSE streaming (openai_compat/provider.go): parses
  SSE events with stream:true, handles text deltas and tool call assembly
- Anthropic native streaming (anthropic/provider.go): uses SDK's
  NewStreaming() for direct Anthropic API connections
- HTTPProvider delegation (http_provider.go): delegates ChatStream to
  the underlying openai_compat provider
- StreamingCapable + Streamer interfaces (channels/interfaces.go):
  opt-in channel capability like TypingCapable/PlaceholderCapable
- Telegram streamer (telegram/telegram.go): BeginStream returns a
  telegramStreamer that throttles sendMessageDraft calls (3s/200 chars)
  with graceful degradation on API errors
- StreamDelegate bridge (bus/bus.go): decouples agent loop from channel
  manager without tight imports
- Manager integration (manager.go): implements StreamDelegate, tracks
  streamActive state, coordinates with placeholder editing
- Agent loop (loop.go): uses ChatStream when both provider and channel
  support streaming, cancels stream on tool calls, skips PublishOutbound
  when Finalize already delivered the message

Graceful degradation:
- Bots without forum/topics mode: first sendMessageDraft error sets
  failed=true, subsequent Updates become no-ops, Finalize still delivers
  via SendMessage. User sees normal non-streaming behavior.
- Non-streaming providers: type assertion fails, falls back to Chat()
- Config opt-out: streaming.enabled (default true) in telegram config

Closes #1098

* fix(telegram): delete placeholder message when streaming delivers response

When streaming was active, the "Thinking..." placeholder message stayed
in the chat because preSend only deleted the tracking entry without
removing the actual Telegram message. Now preSend deletes the placeholder
via the new MessageDeleter interface when streamActive is set.

* refactor(streaming): remove dead code and simplify streaming wiring

- Delete unused Anthropic ChatStream/parseStream (-131 lines) — factory
  creates HTTPProvider for all OpenAI-compat providers including OpenRouter
- Simplify runLLMIteration from 4 to 3 return values (remove unused
  streamed bool)
- Replace managerStreamer struct with finalizeHookStreamer using embedding
  (Update/Cancel promoted, only Finalize overridden)

* fix(streaming): skip streamer acquisition when SendResponse is false

Heartbeat messages set SendResponse=false but the streaming path
was unconditionally acquiring a streamer, causing HEARTBEAT_OK to
leak to Telegram via streamer.Finalize().

* fix(streaming): guard streamer for non-sendable messages, add streaming config

Skip streamer acquisition for heartbeat (NoHistory=true), preventing
HEARTBEAT_OK from leaking to Telegram via streamer.Finalize().

Add streaming.enabled to Telegram defaults and example config.

* feat(telegram): stream LLM responses in real-time via sendMessageDraft

Implements real-time token streaming to Telegram using the sendMessageDraft
API (telego v1.6.0). Instead of showing only a "Thinking..." placeholder
until the full response arrives, users now see partial LLM output appear
in the chat as it's generated.

The streaming pipeline threads through all layers:

- StreamingProvider interface (providers/types.go): opt-in ChatStream()
  method that receives an onChunk callback with accumulated text
- OpenAI-compatible SSE streaming (openai_compat/provider.go): parses
  SSE events with stream:true, handles text deltas and tool call assembly
- Anthropic native streaming (anthropic/provider.go): uses SDK's
  NewStreaming() for direct Anthropic API connections
- HTTPProvider delegation (http_provider.go): delegates ChatStream to
  the underlying openai_compat provider
- StreamingCapable + Streamer interfaces (channels/interfaces.go):
  opt-in channel capability like TypingCapable/PlaceholderCapable
- Telegram streamer (telegram/telegram.go): BeginStream returns a
  telegramStreamer that throttles sendMessageDraft calls (3s/200 chars)
  with graceful degradation on API errors
- StreamDelegate bridge (bus/bus.go): decouples agent loop from channel
  manager without tight imports
- Manager integration (manager.go): implements StreamDelegate, tracks
  streamActive state, coordinates with placeholder editing
- Agent loop (loop.go): uses ChatStream when both provider and channel
  support streaming, cancels stream on tool calls, skips PublishOutbound
  when Finalize already delivered the message

Graceful degradation:
- Bots without forum/topics mode: first sendMessageDraft error sets
  failed=true, subsequent Updates become no-ops, Finalize still delivers
  via SendMessage. User sees normal non-streaming behavior.
- Non-streaming providers: type assertion fails, falls back to Chat()
- Config opt-out: streaming.enabled (default true) in telegram config

Closes #1098

* fix(telegram): delete placeholder message when streaming delivers response

When streaming was active, the "Thinking..." placeholder message stayed
in the chat because preSend only deleted the tracking entry without
removing the actual Telegram message. Now preSend deletes the placeholder
via the new MessageDeleter interface when streamActive is set.

* refactor(streaming): remove dead code and simplify streaming wiring

- Delete unused Anthropic ChatStream/parseStream (-131 lines) — factory
  creates HTTPProvider for all OpenAI-compat providers including OpenRouter
- Simplify runLLMIteration from 4 to 3 return values (remove unused
  streamed bool)
- Replace managerStreamer struct with finalizeHookStreamer using embedding
  (Update/Cancel promoted, only Finalize overridden)

* fix(streaming): skip streamer acquisition when SendResponse is false

Heartbeat messages set SendResponse=false but the streaming path
was unconditionally acquiring a streamer, causing HEARTBEAT_OK to
leak to Telegram via streamer.Finalize().

* fix(streaming): guard streamer for non-sendable messages, add streaming config

Skip streamer acquisition for heartbeat (NoHistory=true), preventing
HEARTBEAT_OK from leaking to Telegram via streamer.Finalize().

Add streaming.enabled to Telegram defaults and example config.

* fix(picoclaw): add missing closing brace for StreamingProvider interface

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: resolve golangci-lint formatting issues

Fix gci import ordering in telegram and anthropic provider, and break
long function signature in openai_compat provider to satisfy golines.

* fix: address code review feedback on streaming PR

- Deduplicate Streamer interface: alias channels.Streamer to bus.Streamer
  to prevent type drift across packages
- Increase SSE scanner buffer to 10MB max to handle large single-line
  responses that exceed bufio.Scanner's 64KB default
- Switch draftID generation from math/rand to crypto/rand for
  collision-resistant random IDs
- Add context cancellation check in SSE parsing loop so cancelled
  streams stop processing immediately
- Log Finalize failures with chat_id and content length for debugging
  silent message delivery failures

* feat: make streaming throttle interval and min growth configurable

Move hardcoded streamThrottleInterval (3s) and streamMinGrowth (200)
into StreamingConfig so they can be tuned per deployment via config
or environment variables.

* fix(telegram): use parseTelegramChatID in DeleteMessage and BeginStream

These two functions called undefined parseChatID. Use
parseTelegramChatID with _ for the unused threadID instead of adding
a wrapper function. Fixes all three CI checks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(streaming): set streamActive only after successful Finalize

Move onFinalize hook to run after Streamer.Finalize succeeds, so that
if Finalize fails the streamActive flag stays false and the regular
placeholder fallback path remains available.

Addresses review feedback from @alexhoshina.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 21:04:14 +08:00
daming大铭 73a683fd16 Merge pull request #1827 from alexhoshina/refactor/agent-loop
Refactor/agent loop
2026-03-20 20:56:53 +08:00
Amir Mamaghani 544940807f feat(pico): add pico_client outbound WebSocket channel (#1198)
* feat(pico): add pico_client outbound WebSocket channel

Add a client-mode counterpart to the existing pico server channel.
pico_client connects to a remote Pico Protocol WebSocket server,
enabling picoclaw to bridge messages with external Pico-compatible
services.

Includes config, factory registration, manager wiring, 8 unit tests,
and a minimal echo-server example for interactive testing.

* fix(pico): address PR #1198 review — goroutine leak, race, auth

- Add per-connection context cancel to picoConn to prevent pingLoop
  goroutine leak on disconnect
- Re-acquire mutex in StartTyping stop closure to avoid stale conn race
- Remove query-param token auth from echo server (header-only)
- Move ListenAndServe to main goroutine where log.Fatal is safe

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: replace ConsumeInbound with InboundChan select in client test

MessageBus does not expose a ConsumeInbound method. Use a select on
InboundChan() with context cancellation, matching the pattern used in
the bus package tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 20:43:40 +08:00
daming大铭 54de9ade7d Merge pull request #1822 from alexhoshina/feat/agent-eventbus
Feat/agent eventbus
2026-03-20 20:14:06 +08:00
taorye 75cfee46de Merge pull request #1832 from taorye/main
refactor(tui): enhance TUI configuration for picoclaw-launcher-tui
2026-03-20 19:46:46 +08:00
taorye 955d6e70f1 refactor: update interface types to use 'any' and improve code formatting 2026-03-20 19:41:59 +08:00
taorye ed47d5f7c3 feat: add onboarding command execution for non-existent config directory 2026-03-20 19:28:58 +08:00
taorye 8c44597c3d feat: add chat functionality to home page for interactive AI sessions 2026-03-20 19:28:58 +08:00
taorye 02da117199 feat: add gateway management page to TUI and integrate into home menu 2026-03-20 19:28:57 +08:00
taorye 7b4d5d4513 feat: add channels management page and integrate into home menu 2026-03-20 19:28:57 +08:00
taorye 545b7afe41 feat: add model selection synchronization to main config in TUI 2026-03-20 19:28:57 +08:00
taorye 74a145c291 style: apply cyberpunk theme to TUI components for enhanced visual appeal 2026-03-20 19:28:57 +08:00
taorye 119cc2e8e1 refactor: enhance TUI configuration and user management with improved UI elements and concurrency 2026-03-20 19:28:57 +08:00
taorye 5a199ec993 feat: implement TUI configuration and user management for picoclaw-launcher-tui 2026-03-20 19:28:54 +08:00
taorye 998b456b65 Remove UI components and gateway management for picoclaw-launcher-tui
- Deleted channel management UI from channel.go, including all associated forms and menu items.
- Removed platform-specific gateway process management from gateway_posix.go and gateway_windows.go.
- Eliminated menu structure and item management from menu.go.
- Removed model management and configuration handling from model.go.
- Deleted style definitions and application logic from style.go.
- Cleared main entry point in main.go.
2026-03-20 19:24:10 +08:00
perhapzz 2a28198d0f fix: check json.Decode errors and use errors.New instead of fmt.Errorf 2026-03-20 10:48:04 +00:00
perhapzz 0276554d9c test(fileutil,health): add unit tests for WriteFileAtomic and health server
Add comprehensive test coverage for two previously untested packages:

pkg/fileutil (9 tests):
- Basic write and read-back
- File permissions (0600)
- Overwrite existing files
- Empty data handling
- Nested directory auto-creation
- No temp files left after success
- Large file (1MB) handling
- Concurrent write safety
- Invalid path error handling

pkg/health (15 tests):
- Health endpoint returns 200 with status, uptime, pid
- Ready endpoint returns 503 when not ready
- Ready endpoint returns 200 when ready
- Ready fails when any registered check fails
- Ready passes with all checks passing
- Reload rejects non-POST methods
- Reload returns 503 when no reload func set
- Reload calls registered function on success
- Reload returns 500 on function error
- SetReady toggle behavior
- Multiple health checks interaction
- RegisterOnMux works with custom ServeMux
- NewServer defaults
- StartContext graceful shutdown on cancel
- statusString helper
2026-03-20 10:41:08 +00:00
Hoshina 2b3c95b1f1 fix: lint err 2026-03-20 17:46:31 +08:00
Hoshina 0e075f7300 feat(agent): centralize turn lifecycle and continue queued steering
Refactor agent loop execution around runTurn, add explicit turn state and interrupt semantics, and automatically continue queued steering that misses the current turn boundary.
2026-03-20 17:28:12 +08:00
Christoforus Surjoputro d08bb02f8f update restart policy to unless-stopped 2026-03-20 15:35:18 +07:00
wenjie fe87376d6a chore(deps): upgrade modelcontextprotocol go-sdk to v1.4.1 for security fixes (#1823) 2026-03-20 16:13:10 +08:00
Hoshina a65e0e95d6 fix: lint err 2026-03-20 15:45:27 +08:00
Hoshina 57cde73b36 feat(agent): expand event bus coverage 2026-03-20 15:29:52 +08:00
wenjie 68d182a26e chore(deps): bump Go toolchain to 1.25.8 for stdlib security fixes (#1821) 2026-03-20 15:19:33 +08:00
wenjie bda18f5ee4 chore(deps): upgrade eslint dependency chain to resolve flatted vulnerability (#1820) 2026-03-20 15:18:15 +08:00
Hoshina 50cc7100ce feat(agent): make event logs show event kind clearly 2026-03-20 15:06:43 +08:00
Hoshina af61d0bca7 feat(agent): add event bus foundation 2026-03-20 14:53:22 +08:00
Alix-007 82d574eb7b fix(agent): separate empty-response and tool-limit fallbacks 2026-03-20 14:37:47 +08:00
dependabot[bot] cff85cfe5c chore(deps): bump tailwindcss from 4.2.1 to 4.2.2 in /web/frontend (#1809)
* chore(deps): bump tailwindcss from 4.2.1 to 4.2.2 in /web/frontend

Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) from 4.2.1 to 4.2.2.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.2.2/packages/tailwindcss)

---
updated-dependencies:
- dependency-name: tailwindcss
  dependency-version: 4.2.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix(frontend): align tailwind vite deps

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: wenjie <meetwenjie@gmail.com>
2026-03-20 13:53:31 +08:00
dependabot[bot] 1fd6dd1ffb chore(deps): bump shadcn from 4.0.5 to 4.0.8 in /web/frontend (#1808)
Bumps [shadcn](https://github.com/shadcn-ui/ui/tree/HEAD/packages/shadcn) from 4.0.5 to 4.0.8.
- [Release notes](https://github.com/shadcn-ui/ui/releases)
- [Changelog](https://github.com/shadcn-ui/ui/blob/main/packages/shadcn/CHANGELOG.md)
- [Commits](https://github.com/shadcn-ui/ui/commits/shadcn@4.0.8/packages/shadcn)

---
updated-dependencies:
- dependency-name: shadcn
  dependency-version: 4.0.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-20 13:35:48 +08:00
ywj 009a8d702b Feat/feishu card parsing (#1534)
* feat(feishu): add interactive card message parsing

Add support for parsing inbound Feishu interactive card messages.
When a user sends a card message, the text content is now extracted
and passed to the LLM for processing.

- Add extractCardText() to recursively extract text from card JSON
- Support both JSON 1.0 (legacy) and JSON 2.0 schema formats
- Handle nested elements: header, body, actions, columns
- Extract text from markdown, lark_md, and plain_text elements
- Add comprehensive unit tests for card parsing

Fixes #<issue_number>

💘 Generated with Crush

Assisted-by: GLM-5 via Crush <crush@charm.land>

* feat(feishu): extract and download images from interactive cards

When receiving interactive card messages, extract embedded images
(img_key, src, icon_key) and download them for LLM processing.

- Add extractCardImageKeys() to recursively extract image keys from card JSON
- Support img elements (img_key, src) and icon elements (icon_key)
- Update downloadInboundMedia() to handle MsgTypeInteractive
- Add comprehensive unit tests for image extraction

Images are downloaded and stored via MediaStore, then appended to
the message content as [image: photo] tags for LLM visibility.

💘 Generated with Crush

Assisted-by: GLM-5 via Crush <crush@charm.land>

* fix(feishu): simplify card parsing - pass raw JSON, only extract images

Address review feedback: text extraction cannot exhaustively handle all
card formats (i18n_elements, div.fields, etc.). Pass raw JSON to LLM
instead - same approach as MsgTypePost. Only image extraction remains
as images must be downloaded for LLM to process.

- Remove extractCardText() and helper functions
- extractContent() now returns raw JSON for MsgTypeInteractive
- Keep extractCardImageKeys() for downloading embedded images
- Update tests to expect raw JSON for interactive cards

* fix(feishu): don't append media tags to interactive card JSON

Appending media tags like "[attachment]" to raw JSON content produces
invalid JSON format. For interactive cards, the JSON already contains
image information and media refs are downloaded separately.

- Skip appendMediaTags for MsgTypeInteractive to preserve valid JSON
- Add test case for interactive card with images

* fix(feishu): filter out external URLs from card image extraction

Only Feishu-hosted image keys (img_xxx, icon_xxx) can be downloaded via
the Feishu API. External URLs in src field (https://...) should be
filtered out to avoid download failures.

- Add isFeishuImageKey() to detect Feishu-hosted keys vs external URLs
- Update extractImageKeysRecursive to skip external URLs in src field
- Add tests for external URL filtering and mixed scenarios

* feat(feishu): support downloading external images from interactive cards

Previously only Feishu-hosted images (img_key, icon_key) could be
downloaded. Now external URLs in src field are also downloaded via
HTTP and made available to the LLM.

- extractCardImageKeys now returns two slices: Feishu keys and external URLs
- Add downloadExternalImage to download images from HTTP URLs
- Update downloadInboundMedia to handle both Feishu API and HTTP downloads
- Update tests for new function signature

* fix(feishu): use HTTP client with timeout for external image downloads

Replaced http.DefaultClient with a client that has a 30-second timeout
to prevent hanging on unresponsive external URLs.

Generated with Crush

Assisted-by: GLM-5 via Crush <crush@charm.land>

* fix(feishu): resolve lint errors for shadow and formatting

- Rename err variables to avoid shadowing in downloadExternalImage
- Fix struct field alignment in TestExtractCardImageKeys

Generated with Crush

Assisted-by: GLM-5 via Crush <crush@charm.land>

* refactor(feishu): pass external image URLs to LLM instead of downloading

Instead of downloading external images from interactive cards, pass
the URLs directly to LLM. This reduces network overhead and lets
vision-capable models fetch images as needed.

- Remove downloadExternalImage function
- Append external URLs to card content for LLM processing
- Only download Feishu-hosted images via API

💘 Generated with Crush

Assisted-by: GLM-5 via Crush <crush@charm.land>

* fix(feishu): add blank line between functions for gci formatting

* fix(feishu): keep interactive card content as valid JSON
2026-03-20 12:59:43 +08:00
dependabot[bot] 8a488eeeed chore(deps-dev): bump typescript-eslint in /web/frontend (#1807)
Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.56.1 to 8.57.1.
- [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.57.1/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: typescript-eslint
  dependency-version: 8.57.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-20 12:11:25 +08:00
dependabot[bot] 736baf2217 chore(deps-dev): bump @types/node in /web/frontend (#1806)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 24.11.0 to 25.5.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.5.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-20 12:02:56 +08:00
dependabot[bot] c9ac19c0cc chore(deps): bump maunium.net/go/mautrix from 0.26.3 to 0.26.4 (#1805)
Bumps [maunium.net/go/mautrix](https://github.com/mautrix/go) from 0.26.3 to 0.26.4.
- [Release notes](https://github.com/mautrix/go/releases)
- [Changelog](https://github.com/mautrix/go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mautrix/go/compare/v0.26.3...v0.26.4)

---
updated-dependencies:
- dependency-name: maunium.net/go/mautrix
  dependency-version: 0.26.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-20 11:58:24 +08:00
Administrator 4f646ef2b8 Merge branch 'main' into feat/subturn-poc 2026-03-20 11:51:25 +08:00
dependabot[bot] 77d0c67e58 chore(deps): bump @tabler/icons-react in /web/frontend (#1803)
Bumps [@tabler/icons-react](https://github.com/tabler/tabler-icons/tree/HEAD/packages/icons-react) from 3.38.0 to 3.40.0.
- [Release notes](https://github.com/tabler/tabler-icons/releases)
- [Commits](https://github.com/tabler/tabler-icons/commits/v3.40.0/packages/icons-react)

---
updated-dependencies:
- dependency-name: "@tabler/icons-react"
  dependency-version: 3.40.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-20 11:50:56 +08:00
dependabot[bot] 80d9a90c52 chore(deps): bump github.com/ergochat/irc-go from 0.5.0 to 0.6.0 (#1800)
Bumps [github.com/ergochat/irc-go](https://github.com/ergochat/irc-go) from 0.5.0 to 0.6.0.
- [Release notes](https://github.com/ergochat/irc-go/releases)
- [Changelog](https://github.com/ergochat/irc-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ergochat/irc-go/compare/v0.5.0...v0.6.0)

---
updated-dependencies:
- dependency-name: github.com/ergochat/irc-go
  dependency-version: 0.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-20 11:45:37 +08:00
Administrator e71ef3764d fix(test): reduce blank identifiers to comply with dogsled linter
Changed newTestAgentLoop calls from using 3 blank identifiers to 2 by
assigning the unused provider parameter and explicitly marking it as
unused with `_ = provider`. This fixes the dogsled linter violations
that were causing CI failures.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 11:12:47 +08:00
Mauro 71ce21963b Merge pull request #1798 from sipeed/dependabot/github_actions/goreleaser/goreleaser-action-7
chore(deps): bump goreleaser/goreleaser-action from 6 to 7
2026-03-19 22:20:38 +01:00
Mauro ffe0289f77 Merge pull request #1799 from sipeed/dependabot/github_actions/docker/setup-qemu-action-4
chore(deps): bump docker/setup-qemu-action from 3 to 4
2026-03-19 22:17:56 +01:00
Mauro bd4317f1f4 Merge pull request #1390 from kiannidev/fix/1323-telegram-endless-typing
fix(telegram): stop typing indicator when LLM fails or hangs
2026-03-19 21:52:10 +01:00
dependabot[bot] 876898fec6 chore(deps): bump docker/setup-qemu-action from 3 to 4
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-19 17:17:54 +00:00
dependabot[bot] 5ada0dfed3 chore(deps): bump goreleaser/goreleaser-action from 6 to 7
Bumps [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) from 6 to 7.
- [Release notes](https://github.com/goreleaser/goreleaser-action/releases)
- [Commits](https://github.com/goreleaser/goreleaser-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: goreleaser/goreleaser-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-19 17:17:48 +00:00
Maksim 16a7da7517 docs: describe how to disable "exec" tool (#1703) 2026-03-20 00:25:00 +08:00
美電球 75d86721a3 Feat/wecom aibot processing message config (#1785)
* feat(wecom_aibot): make processing message configurable

* docs(wecom): document ai bot processing message

* test(wecom_aibot): adapt webhook tests to channel interface

* fix: lint err
2026-03-20 00:23:40 +08:00
opcache e3cc5b1000 Fix the limitation on the number of tables in cards caused by Feishu (#1736)
* Fix the limitation on the number of tables in cards caused by Feishu

* Only match the error code 11310
2026-03-19 23:46:17 +08:00
美電球 d715ff5031 docs: expand bindings guide with recipes and troubleshooting (#1788) 2026-03-19 23:30:25 +08:00
Administrator c18d8a2ecc Merge branch 'upstream-main' into feat/subturn-poc 2026-03-19 22:12:51 +08:00
Adi Susilayasa 9a3ca8e54d feat(provider): add Alibaba Coding Plan and regional Qwen endpoints (#1748)
* feat(provider): add Alibaba Coding Plan and regional Qwen endpoints

- Add Alibaba Coding Plan provider with OpenAI-compatible endpoint
  (https://coding-intl.dashscope.aliyuncs.com/v1)
- Add Coding Plan Anthropic-compatible endpoint
  (https://coding-intl.dashscope.aliyuncs.com/apps/anthropic)
- Add regional Qwen endpoints (qwen-intl, qwen-us)
- Add provider aliases: coding-plan, alibaba-coding, qwen-coding
- Normalize provider names for coding-plan variants

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(provider): add reviewer-requested fixes for Alibaba Coding Plan

- Add qwen-international, dashscope-intl, dashscope-us aliases to switch case
- Add coding-plan-anthropic case with anthropicmessages.NewProviderWithTimeout
- Add alibaba-coding-anthropic -> coding-plan-anthropic normalization
- Add qwen-international -> qwen-intl and dashscope-us -> qwen-us normalization

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* test(provider): add tests for Alibaba Coding Plan protocol aliases

- Add tests for qwen-international, dashscope-intl, dashscope-us aliases
- Add tests for coding-plan-anthropic and alibaba-coding-anthropic
- Add getDefaultAPIBase tests for all new aliases
- Add normalization tests for new provider aliases

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-19 22:07:30 +08:00
Alix-007 276a0cb92c fix(agent): rebind provider after /switch model to (#1769)
* fix(agent): rebind provider after model switch

* test(agent): deduplicate switch model mock servers

---------

Co-authored-by: Alix-007 <267018309+Alix-007@users.noreply.github.com>
2026-03-19 21:44:01 +08:00
Alix-007 05c65d2fe7 fix(provider): skip empty anthropic tool names (#1772)
Co-authored-by: Alix-007 <267018309+Alix-007@users.noreply.github.com>
2026-03-19 21:35:17 +08:00
I Putu Eddy Irawan bb59518958 docs: add Indonesian (Bahasa Indonesia) README translation (#1777)
- Rewrite README.id.md to match current upstream structure (~250 lines)
- Detailed docs moved to docs/*.md, README is quick-start only
- Sync badges (Go 1.25+, LoongArch), news (v0.2.3), Termux instructions
- Add Bahasa Indonesia + Italiano to language selectors in all 8 READMEs
2026-03-19 21:28:35 +08:00
Bijin 38e1fe435a fix(config): model_list inherits api_key/api_base from providers (#1786)
When both providers and model_list are configured, model_list entries
with empty api_key or api_base now automatically inherit from the
matching provider (matched by protocol prefix in the Model field).

Example: a model_list entry with model='deepseek/deepseek-chat' and
no api_key will inherit from providers.deepseek.api_key.

Explicit model_list values always take precedence.

Changes:
- Add InheritProviderCredentials() in migration.go
- Call it in LoadConfig() after provider-to-model-list conversion
- Add protocolProviderMapping for all 25 supported protocols
- 6 new tests covering inheritance, precedence, and edge cases

Closes #1635
2026-03-19 21:24:46 +08:00
SakoroYou 844a4eefc7 fix(agent): avoid process exit on exec init failure and add regression test (#1784)
* fix(agent): make exec tool init failure non-fatal

* test(agent): add regression test for invalid exec config fallback
2026-03-19 21:11:36 +08:00
Administrator 583c586db6 Merge branch 'main' into feat/subturn-poc 2026-03-19 20:20:31 +08:00
ZHANG RUI 9a25fad20a Implement the latest long-connection mode for the WeCom AI Bot. (#1295)
* feat(wecom): add WebSocket long-connection support for WeCom AI Bot

- Introduced WeComAIBotWSChannel to handle WebSocket connections.
- Updated NewWeComAIBotChannel to prioritize WebSocket mode when BotID and Secret are provided.
- Enhanced WeComAIBotConfig to include BotID and Secret for WebSocket mode.
- Implemented message handling for text, image, voice, and mixed messages in WebSocket mode.
- Added tests for WebSocket mode functionality and ensured backward compatibility with webhook mode.
- Refactored existing code to improve clarity and maintainability.

* feat(wecom): implement periodic processing hints and enforce WeCom stream deadline

* feat(wecom): update WeCom AI Bot setup instructions and configuration parameters

* feat(wecom): enhance WeCom AI Bot with image handling and media support

* feat(wecom): refactor WeCom AI Bot task management to use req_id for concurrent message handling

* feat(wecom): refactor WeCom AI Bot to manage request states and late replies

* feat(wecom): add response timeout handling and improve WebSocket command acknowledgment

* fix(wecom): improve error handling for late reply proactive push delivery

* refactor(wecom): reorganize WeCom AI Bot configuration fields for improved readability

* fix(wecom): update error message for websocket delivery failure in late reply proactive push

* feat(wecom): implement shared HTTP clients for WeCom image handling and response URL posting

* refactor(wecom): simplify image download and storage process in storeWSImage

* fix(wecom): improve error logging for WebSocket message handling and proactive push delivery

* fix(wecom): enhance WebSocket connection stability and task cancellation handling

* fix(wecom): improve WS image message handling by ensuring proper error response and initializing mediaRefs

* feat(wecom): enhance WeCom AIBot WebSocket handling with message deduplication and support for file and video messages

* refactor(wecom): rename image handling functions to media handling and enhance media type support

* feat(wecom): implement byte-aware content splitting for WeCom AI Bot stream messages

* refactor(wecom): remove max message length constraint from WeCom AIBot WS channel
2026-03-19 20:06:51 +08:00
Avisek 41ebe1e1c7 chore: Ignore the docker/data directory. 2026-03-19 16:44:07 +05:30
Cytown 94fcb25039 Merge branch 'main' into version 2026-03-19 18:16:15 +08:00
Mauro 7673b626b3 feat(tool): debug tool usage via channels (#1332)
* feat(tool): debug usage via channel

* set defaults

* fix conflicts
2026-03-19 18:08:50 +08:00
Cytown cfd3a1b441 Merge branch 'main' into version 2026-03-19 18:04:58 +08:00
Administrator 54889f21a7 Merge branch 'upstream-main' into feat/subturn-poc 2026-03-19 17:05:09 +08:00
Mauro a4b5a9eec1 feat(mcp): per server deferred mode (#1654)
* feat(mcp): per server deferred mode

* fix deferred behavior
2026-03-19 17:03:17 +08:00
Mauro ff975abec2 feat(tool): anti cloudflare challenge in web_fetch (#1762)
* feat(tool): anti-cloudflare-challenge

* fix lint
2026-03-19 17:01:45 +08:00
kiannidev 440bc2687e Merge remote branch fix/1323-telegram-endless-typing
Made-with: Cursor
2026-03-19 10:56:11 +02:00
kiannidev 310358d3da Merge upstream/main into fix/1323-telegram-endless-typing
Made-with: Cursor
2026-03-19 10:53:50 +02:00
Administrator 532ea4b56c Merge branch 'upstream-main' into feat/subturn-poc 2026-03-19 16:47:35 +08:00
美電球 828971d549 Feat/qq local file upload (#1722)
* feat(qq): support media uploads and inbound attachments

* docs(qq): document media size limit settings

* chore(web): add QQ media size limit hints

* fix(qq): demote botgo heartbeat logs

* style(qq): fix lint issues
2026-03-19 16:27:34 +08:00
Cytown a8ce992429 refactor[gateway]: just reload the changed channels on reload occurred (#1773) 2026-03-19 15:28:52 +08:00
Administrator 24a382bc0a merge main 2026-03-19 13:53:31 +08:00
Administrator 29a161e757 fix(tools): prevent nil pointer dereference in spawn tools
Add nil checks in NewSpawnTool and NewSubagentTool constructors to
handle nil manager gracefully. Fix spelling errors (cancelled->canceled)
and remove unused test code. Update tests to use mock spawner.
2026-03-19 13:51:11 +08:00
Cytown 2a6ade0fe4 feat: add /reload to gateway api and command (#1725)
* feat: add /reload to gateway api and command

* prevent duplicate reload request in same time
2026-03-19 13:42:36 +08:00
Administrator e801ccb674 Merge branch 'upstream-main' into feat/subturn-poc 2026-03-19 13:09:05 +08:00
Administrator ce311be70b feat(subturn): add configurable runtime parameters under agents.defaults
Replace hardcoded constants with config-driven parameters in agents.defaults:
- MaxDepth, MaxConcurrent, DefaultTimeout, DefaultTokenBudget, ConcurrencyTimeout
- Support JSON config and env vars (PICOCLAW_AGENTS_DEFAULTS_SUBTURN_*)
- Add getSubTurnConfig() for runtime config resolution with defaults
- Apply defaultTokenBudget when no explicit budget is provided

Rationale: SubTurn is agent execution infrastructure, not a tool, so it belongs
in agents.defaults rather than tools config.

Example:
{
  "agents": {
    "defaults": {
      "subturn": {
        "max_depth": 5,
        "max_concurrent": 10,
        "default_timeout_minutes": 10
      }
    }
  }
}
2026-03-19 13:08:46 +08:00
Administrator 99b189d3fb feat(subturn): implement token budget tracking for SubTurns 2026-03-19 12:38:18 +08:00
Mauro 14a28ae93e docs: note that workspace config files are hot-reloaded (#1747)
* docs: note that workspace config files are hot-reloaded via mtime tracking

* refactor files

* refactor files
2026-03-19 11:30:08 +08:00
Mauro e931756fee feat(tool): overwrite flag in write_file (#1761)
* feat: overwrite flag in write file tool

* fix error message
2026-03-19 11:22:52 +08:00
Administrator 01c2f8d608 refactor(subturn): remove redundant system prompt handling in runTurn function 2026-03-19 11:10:44 +08:00
Meng Zhuo 8a188cf7fc Merge pull request #1759 from afjcjsbx/docs/add-italian-language
docs: add Italian language
2026-03-19 10:36:45 +08:00
Administrator 53404f18ca feat(subturn): support stateful iteration for evaluator-optimizer pattern
Add ActualSystemPrompt and InitialMessages fields to SubTurnConfig to enable
stateful worker context passing across multiple evaluation iterations.

Changes:
- Add ActualSystemPrompt field to separate system role from user task description
- Add InitialMessages field to preload ephemeral session history before agent loop starts
- Add Messages field to ToolResult for carrying session history (internal use, not serialized)
- Update runTurn to inject system prompt and preload history from InitialMessages
- Update AgentLoopSpawner to map new fields from tools.SubTurnConfig to agent.SubTurnConfig

This enables the evaluator-optimizer execution strategy in team tool to maintain
worker context across iterations while keeping SubTurn isolation intact.
2026-03-19 10:15:00 +08:00
Administrator c732e63650 Merge branch 'upstream-main' into feat/subturn-poc 2026-03-19 09:16:38 +08:00
afjcjsbx 3e2ce06155 docs: add Italian language 2026-03-18 19:41:57 +01:00
Liu Yuan e73d9d959e feat(config): support multiple API keys for failover (#1707)
* feat(config): support multiple API keys for failover

Add api_keys field to ModelConfig to support multiple API keys with
automatic failover. When multiple keys are configured, they are expanded
into separate model entries with fallbacks set up for key-level failover.

Example config:
  {
    "model_name": "glm-4.7",
    "model": "zhipu/glm-4.7",
    "api_keys": ["key1", "key2", "key3"]
  }

Expands internally to:
  - glm-4.7 (key1) -> fallbacks: [glm-4.7__key_1, glm-4.7__key_2]
  - glm-4.7__key_1 (key2)
  - glm-4.7__key_2 (key3)

Backward compatible: single api_key still works as before.

* fix(providers): change cooldown tracking from provider to ModelKey

This enables proper key-switching when multiple API keys share the same
provider. Previously, when one key failed, all keys were blocked because
cooldown was tracked per-provider.

Now each (provider, model) combination has independent cooldown, allowing
fallback to alternate keys when one is rate limited.

Includes TestMultiKeyWithModelFallback and related failover tests.
2026-03-19 00:57:20 +08:00
Liqiang Lau 08f305d712 feat: add IsLark field to FeishuConfig to switch between Feishu and Lark domains (#1753)
* feat(feishu): add Lark (international) support via IsLark config field

Add IsLark field to FeishuConfig to switch between Feishu and Lark
domains. Also fix domain inconsistency where WS client defaulted to
LarkBaseUrl while HTTP client used FeishuBaseUrl.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: update documentation and web UI for Lark support

Add is_lark field to config example, feishu docs, i18n translations,
and web frontend form.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 00:29:55 +08:00
Paolo Anzani eb86e10e5c fix(tools): propagate tool registry to subagents (#1711)
* fix(tools): propagate tool registry to subagents via Clone

SubagentManager was created with an empty ToolRegistry and SetTools()
was never called, causing all subagent tool invocations to fail with
"tool not found". This was a regression from the multi-agent refactor.

Fix: clone the parent agent's tool registry into the subagent manager
after creation but before spawn/spawn_status registration — giving
subagents access to file, exec, web, and other tools while preventing
recursive subagent spawning.

- Add ToolRegistry.Clone() for independent shallow copies
- Call subagentManager.SetTools(agent.Tools.Clone()) in registerSharedTools
- Add tests for Clone isolation, empty clone, and hidden tool state

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(tools): fix cron_test build error and add TTL clone test

- Fix cron_test.go:229 — replace non-existent SubscribeOutbound(ctx)
  with select on OutboundChan(), matching the MessageBus channel API
- Add TestToolRegistry_Clone_PreservesTTLValue per reviewer feedback
- Add version reset note to Clone() doc comment

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 00:17:16 +08:00
linhaolin1 f93d2b4533 fix: Avoid failure of the main agent process due to tool call failures (#1023)
* Avoid failure of the main agent process due to tool call failures or abnormal returns

* rename recover
2026-03-19 00:10:26 +08:00
Administrator 431a53cbb1 Merge branch 'upstream-main' into feat/subturn-poc 2026-03-18 22:57:01 +08:00
美電球 899558bbfa Feat/issue 1218 agent md context structure (#1705)
* feat(agent): add structured agent definition loader

Parse AGENT.md frontmatter into a runtime definition and pair it with SOUL.md while keeping a legacy AGENTS.md fallback for transition.

Refs #1218

* refactor(agent): build context from structured agent files

Use AGENT.md and SOUL.md as the structured bootstrap source, ignore IDENTITY.md for structured agents, remove USER.md from the new context flow, and update pkg/agent tests accordingly.

Refs #1218

* refactor(onboard): switch workspace templates to AGENT.md

Replace the legacy AGENTS.md, IDENTITY.md, and USER.md templates with a structured AGENT.md plus SOUL.md, and update the onboard template test to assert the new generated files.

Refs #1218

* docs(readme): update workspace layout for AGENT.md

Refresh the documented workspace tree across the README translations so onboarding now points to AGENT.md and SOUL.md instead of the retired AGENTS.md, IDENTITY.md, and USER.md files.

Refs #1218

* feat(agent): restore workspace USER.md context

* docs(readme): document workspace USER.md layout

* fix: sort agent definition imports for gci
2026-03-18 22:42:57 +08:00
Darren.Zeng 54654d2794 fix(anthropic): skip tool calls with empty names to prevent API errors (#1739)
When building parameters for Anthropic API calls, tool calls with empty
names would cause 400 Bad Request errors with the message:
'tool_use.name: String should have at least 1 character'

This fix adds a check to skip tool calls that have empty names, preventing
the API error and allowing the conversation to continue normally.

Fixes #1658
2026-03-18 21:55:01 +08:00
Alexander 12f4029610 feat: telegram use parse mode ModeMarkdownV2 instead of ModeHTML (#1018)
* feat: telegram use parse mode ModeMarkdownV2 instead of ModeHTML

* handle expandable block quotation starts, add test for all md2 formats

* fix: linter issue

* feat: added flag use_markdown_v2, corrected config, updated
documentation

* move parseChatID to parser_markdown_to_html

* fix: tests and linter issues

* fix: case with ~

* test: fixed Test_markdownToTelegramMarkdownV2

* fix: regex block-quote line  >

* fix: linter issues

* fix: send chunk param mismatched, in edit msg use HTML parse mode too

* fix: remove from .gitignore redundant comment
2026-03-18 21:29:21 +08:00
Vast-stars 3e9b7ce9c1 fix(feishu): invalidate cached token on auth error to enable retry recovery (#1318)
The Lark SDK v3's built-in token retry loop does not clear stale tokens
from cache when the server returns error 99991663 (tenant_access_token
invalid), causing all API calls to fail until the token naturally
expires (~2 hours).

- Add tokenCache struct (implementing larkcore.Cache) with
  Get/Set/InvalidateAll methods and proper expired-entry cleanup
- Wire custom cache into lark.NewClient via WithTokenCache()
- Add invalidateTokenOnAuthError helper called in all API methods
2026-03-18 19:07:49 +08:00
Alex 578f90855e feat: Add Novita provider support (#1677)
* Add Novita provider support

- Add 'novita' prefix to normalizeModel switch in openai_compat provider
- Add Novita provider to all_supported_vendors table in README.md
- Add test cases for Novita model prefix stripping

Novita endpoint: https://api.novita.ai/openai
Default models: deepseek/deepseek-v3.2, zai-org/glm-5, minimax/minimax-m2.5

* feat: complete Novita provider integration

* chore: drop README changes from Novita PR

* fix: remove duplicate function declarations in openai_compat provider

The functions buildToolsList, SupportsNativeSearch, and isNativeSearchHost
were declared twice, causing compilation failures in all CI checks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: break long line in novita test to satisfy golines linter

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 18:29:27 +08:00
Administrator 3611034795 fix(agent): implement Critical flag, complete tools.SubTurnConfig, remove redundant subTurnResults
- Critical flag was declared but never acted on; non-critical SubTurns
  now break out of the iteration loop when IsParentEnded() returns true
- tools.SubTurnConfig was missing Critical/Timeout/MaxContextRunes,
  making those fields unreachable from the tools layer; added fields and
  wired them through AgentLoopSpawner.SpawnSubTurn
- Removed subTurnResults sync.Map from AgentLoop — it was a redundant
  alias for the same channel already stored in turnState.pendingResults;
  dequeuePendingSubTurnResults now reads directly via activeTurnStates
- Replace hardcoded concurrencySem size 5 with maxConcurrentSubTurns constant
- Update affected tests to match new dequeuePendingSubTurnResults API
2026-03-18 18:22:06 +08:00
dev-miro26 c07f5c948f refactor: centralize environment variable key constants (#1730)
* refactor: centralize environment variable key constants

* refactor: update environment variable constants and usage in gateway
2026-03-18 18:03:24 +08:00
Cytown affd77f989 fix for feat(web): implement macOS app feature and file logger (#1735) 2026-03-18 18:00:14 +08:00
lxowalle b6c5f587c9 Update qrcode of wechat group (#1744) 2026-03-18 17:58:55 +08:00
badgerbees a1e8ee56f0 fix(telegram): improve HTML chunking and preserve word boundaries (#1651)
* fix(telegram): improve HTML chunking and preserve word boundaries

* fix(telegram): address copilot feedback, filter empty chunks and add word-boundary regression test

* style(telegram): fix gofmt and gci lint errors in tests

* fix to feedback
2026-03-18 16:44:30 +08:00
BeaconCat 363861c917 docs: restructure READMEs and add i18n documentation (#1729)
Restructure all 6 README files (en, zh, ja, fr, pt-br, vi) from
~1200-1580 lines down to ~250 lines each. Long sections (Chat Apps,
Providers, Configuration, Docker, Spawn Tasks, Troubleshooting, Tools)
are extracted into dedicated docs under docs/{lang}/ subdirectories.

Changes:
- Split README content into 7 sub-documents per language (42 new files)
- Update News section with v0.2.3/v0.2.1/v0.2.0/20K milestones
- Add 3 new Features (MCP Support, Vision Pipeline, Smart Routing)
- Complete CLI reference (14 commands, was 7)
- Fix Go badge 1.21+ -> 1.25+ (matches go.mod)
- Add LoongArch to architecture badge
- Fix Install section: hardcoded v0.1.1 -> latest/download URL
- Add Termux GitHub links
- Fix currency symbol placement ($599 not 599$)
- Add missing channels (Feishu, Slack, IRC, OneBot, MaixCam, Pico)
- Add missing providers (Kimi, Minimax, Avian, Mistral, Longcat, ModelScope)
- Add missing security docs (allow_read/write_paths, allow_remote, symlink)
- Remove incorrect azure from Providers table (azure uses model_list only)
- Cross-verified all claims against source code

Co-authored-by: BeaconCat <BeaconCat@users.noreply.github.com>
2026-03-18 15:26:39 +08:00
Administrator 777230dcd1 feat(agent): implement /subagents command and fix sub-turn observability
- Added `/subagents` platform command to visualize the active task tree.
- Implemented GetAllActiveTurns and FormatTree in AgentLoop to support cross-session observability.
- Fixed a bug where sub-turns spawned via tools were not registered in the global `activeTurnStates` map, making them invisible to system queries.
- Enhanced tree rendering logic to identify and display "orphaned" subagents (children that outlive their parent turns).
- Registered the new command in `builtin.go` and injected the turn state provider into the commands runtime.

Modified Files:
- pkg/agent/turn_state.go: Added TurnInfo snapshotting and recursive tree formatting.
- pkg/agent/loop.go: Injected GetActiveTurn hook and implemented multi-root forest rendering.
- pkg/agent/subturn.go: Added child turn registration into activeTurnStates.
- pkg/commands/cmd_subagents.go: New command implementation.
- pkg/commands/builtin.go: Command registration.
2026-03-18 14:46:20 +08:00
Cytown e6ebeaed13 feat(web): implement macOS app feature and file logger (#1723) 2026-03-18 14:43:58 +08:00
kpdev 317c70a7b2 Merge branch 'main' into fix/1323-telegram-endless-typing 2026-03-17 23:25:15 -07:00
Administrator e20ff43f8b fix(agent): resolve subturn deadlocks, panics and context retry state
This commit addresses several critical concurrency and state management bugs within the SubTurn execution and delivery logic.

1. Fix Goroutine Leak & Deadlock in deliverSubTurnResult:
   - Replaced non-blocking select with a safe blocking select that listens to `resultChan` and a new `<-parentTS.Finished()` channel.
   - This ensures results are not arbitrarily dropped when the channel is full (preventing orphaned valid results), while also guaranteeing the child goroutine safely unblocks and exits if the parent finishes execution early.

2. Prevent "Send on Closed Channel" Fatal Panics:
   - Removed `close(pendingResults)` and `drainPendingResults` from `turnState.Finish()`.
   - The pendingResults channel is now naturally garbage collected, completely eliminating the race condition panic when a child attempts delivery at the exact moment the parent finishes.
   - Added a `defer recover()` failsafe inside deliverSubTurnResult to gracefully emit Orphan events in extreme edge cases.

3. Fix Truncation Recovery Prompt Drop:
   - Fixed the runTurn truncation retry logic by introducing an explicit `promptAlreadyAdded` boolean.
   - Ensures that the dynamically generated `recoveryPrompt` is correctly injected into the LLM history sequence on subsequent iterations, adhering to API roles without duplicating arrays.

4. Test Suite Stabilization:
   - Fixed TestDeliverSubTurnResultNoDeadlock to accurately wait for deterministic deliveries instead of racing timeouts.
   - Replaced defunct closed-channel tests with TestFinishedChannelClosedState matching the new Finished() mechanism.
   - Fixed the Finish(true) parameter in TestGrandchildAbort_CascadingCancellation to correctly validate Context cascade behavior.
   - All tests now pass cleanly without hanging or emitting false positives.
2026-03-18 13:10:36 +08:00
Administrator c7ea018a73 fix(agent): prevent duplicate history during subturn context recoveries
Problem:
During subturn context limit or truncation recoveries, the recovery loops repeatedly
called `runAgentLoop` with the same or modified `UserMessage`. Because `runAgentLoop`
unconditionally adds the `UserMessage` to the session history, this resulted in:
1. Duplicate User Messages polluting the history upon `context_length_exceeded` retries.
2. The possibility of injecting empty User Messages if `opts.UserMessage` was artificially blanked out to work around the duplication.
3. Messy or duplicate entries during `finish_reason="truncated"` recovery injections.

Solution:
- Introduce `SkipAddUserMessage` boolean to `processOptions` to explicitly control whether the agent loop should write the user prompt to history.
- Add an explicit `opts.UserMessage != ""` check in `runAgentLoop` to prevent polluting history with empty message content.
- In `subturn.go`'s recovery loop, set `SkipAddUserMessage: contextRetryCount > 0` to skip writing the user message on context
2026-03-18 12:18:32 +08:00
dataCenter430 f79469c19d Add model-native search (prefer_native) for OpenAI/Codex (#1618)
* config: add prefer_native and NativeSearchCapable for model-native search

* providers: implement native web search for OpenAI and Codex

* agent: use provider-native search when prefer_native and supported

* tests: add coverage for model-native search

* fix: Golang lint errors

* fix: update the code based on the review

* fix: update codex_provider_test
2026-03-18 11:55:30 +08:00
Zenix f12c09b767 fix: retry on dimension failure for tg media upload (#1409) 2026-03-18 10:46:35 +08:00
Meng Zhuo cefa140bd2 Merge pull request #1622 from afjcjsbx/feat/markdown-output-format-web-fetch
feat(tool): markdown format in web_fetch tool output
2026-03-18 09:15:13 +08:00
Mauro 513537d230 Merge pull request #1702 from Alix-007/fix/issue-1153-model-round-robin-cleanbase
fix(config): start model round robin from the first match
2026-03-17 22:28:48 +01:00
Mauro 74f2a1513e Merge pull request #1479 from securityguy/fix/claude-cli-error-surfacing
fix(claude_cli): surface stdout in error when CLI exits non-zero
2026-03-17 22:16:42 +01:00
Mauro f901218b43 Merge pull request #1640 from argobell/main
fix(logger): append fields to fileEvent instead of event in file logger block
2026-03-17 22:12:00 +01:00
afjcjsbx 9835e821d7 Merge branch 'main' into feat/markdown-output-format-web-fetch 2026-03-17 21:45:18 +01:00
Mauro 7bf12c3d5f Merge pull request #1710 from liuy/fix/cron-test-subscribe-outbound
fix(cron): update test to use OutboundChan instead of removed SubscribeOutbound
2026-03-17 20:40:49 +01:00
Mauro 5e92a38236 Merge pull request #1490 from is-Xiaoen/refactor/context-boundary
refactor(agent): context boundary detection, proactive budget check, and safe compression
2026-03-17 19:41:15 +01:00
Liu Yuan 61a899cfbc fix(cron): update test to use OutboundChan instead of removed SubscribeOutbound
The SubscribeOutbound method was removed in commit 9c31b0c but cron_test.go
was not updated to use the new OutboundChan() API.
2026-03-18 01:37:07 +08:00
afjcjsbx 13d4801601 Merge branch 'main' into feat/markdown-output-format-web-fetch 2026-03-17 17:21:14 +01:00
afjcjsbx 8f460726cc fix lint + error check 2026-03-17 17:14:23 +01:00
juju 9c31b0ca95 fix: Fixed the bug where the bus was closed and consumers had unfinished messages. (#1179)
* fix: Fixed the bug where the bus was closed and consumers had unfinished messages.

* fix: remove unnecessary blank line in Close method

* fix: refactor message bus and channel handling for improved performance and reliability

* fix: improve message handling and bus closure logic for better reliability

* fix: reduce sleep duration in agent loop for improved responsiveness

* fix the test case
2026-03-18 00:12:12 +08:00
juju f776611e29 feat(cron): refactor scheduler to event-driven model and add unit tests (#1313)
* feat(cron): enhance CronService with wake channel and improve job scheduling logic

* fix(cron): update file permission mode to use octal notation in test and fix some lint errors

* fix(cron): improve wake channel handling and enhance concurrency in tests
2026-03-18 00:02:51 +08:00
Mauro 3791f06faf Merge branch 'main' into feat/markdown-output-format-web-fetch 2026-03-17 16:37:22 +01:00
Alix-007 c639e2c216 feat(agent): include current sender in dynamic context (#1696)
* feat(agent): include current sender in dynamic context

* test(agent): keep current-sender regression ASCII-only

---------

Co-authored-by: Alix-007 <267018309+Alix-007@users.noreply.github.com>
2026-03-17 23:31:56 +08:00
Alix-007 b4468313e4 feat(web): whitelist private fetch targets (#1688)
* feat(web): whitelist private fetch targets

* test(web): avoid accept error shadowing

---------

Co-authored-by: Alix-007 <267018309+Alix-007@users.noreply.github.com>
2026-03-17 23:22:05 +08:00
Administrator f8defe3ae1 feat(agent): implement graceful finish vs hard abort for SubTurn lifecycle
Problem:
When parent turn finishes early, all child SubTurns receive "context canceled"
error,because child context was derived from parent context.

Solution:
Implement a lifecycle management system that distinguishes between:
- Graceful finish (Finish(false)): signals parentEnded, children continue
- Hard abort (Finish(true)): immediately cancels all children

Changes:
- turn_state.go:
  - Add parentEnded atomic.Bool to signal parent completion
  - Add parentTurnState reference for IsParentEnded() checks
  - Modify Finish(isHardAbort bool) to distinguish abort types

- subturn.go:
  - Add Critical bool to SubTurnConfig (Critical SubTurns continue after parent ends)
  - Add Timeout time.Duration for SubTurn self-protection
  - Use independent context (context.Background()) instead of derived context
  - SubTurns check IsParentEnded() to decide whether to continue or exit

- loop.go:
  - Call Finish(false) for normal completion (graceful)
  - Add IsParentEnded() check in LLM iteration loop

- steering.go:
  - HardAbort calls Finish(true) to immediately cancel children

Behavior:
- Normal finish: parentEnded=true, children continue, orphan results delivered
- Hard abort: all children cancelled immediately via context
- Critical SubTurns: continue running after parent finishes gracefully
- Non-Critical SubTurns: can exit gracefully when IsParentEnded() returns true
2026-03-17 23:06:16 +08:00
Administrator e05d2620e1 Added tests to verify SubTurn context cancellation behavior when parent
finishes early - identified need for Critical+heartbeat+timeout mechanism.
2026-03-17 22:31:56 +08:00
Alix-007 fcf406bf2e fix(config): start model round robin from the first match 2026-03-17 21:59:04 +08:00
Administrator e00a3d9017 Merge upstream/main into feat/subturn-poc
Includes JSONL session persistence (#1170), spawn_status tool, Azure provider,
credential encryption, and various fixes. SubTurn features preserved and
integrated with new spawn_status functionality.
2026-03-17 21:55:20 +08:00
BeaconCat 5bc4fe4dea docs: add project identity statement and normalize NanoBot capitalization across all READMEs (#1695)
Add a clear identity statement to all 6 README files clarifying that
PicoClaw is an independent open-source project by Sipeed, written
entirely in Go, and not a fork of OpenClaw, NanoBot, or any other
project. This addresses common AI hallucinations found during testing
of 11 AI tools. Also normalizes [nanobot] to [NanoBot] for consistent
capitalization.

Co-authored-by: BeaconCat <BeaconCat@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 20:51:07 +08:00
Mauro 11a7ee5080 Merge pull request #1690 from Alix-007/docs/issue-529-exec-guard-limitations
docs(exec): document build tool guard limitation
2026-03-17 13:50:37 +01:00
wenjie 12c01327dd Remove redundant Darwin tray stub (#1694) 2026-03-17 20:34:11 +08:00
wenjie 3e33d1053c fix(backend): add no-cgo tray fallback for darwin and freebsd (#1691)
* refactor(backend): add darwin no-cgo tray fallback

* fix(release): stub tray for freebsd builds without cgo
2026-03-17 20:13:11 +08:00
Administrator 2fec249be1 refactor(agent): improve SubTurn error handling and logging
- Fix context cancellation check order in concurrency timeout
  - Add structured logging for panic recovery
  - Replace println with proper logger for channel full warning
  - Simplify tool registry initialization logic
  - Remove unused ErrConcurrencyLimitExceeded error
2026-03-17 20:02:56 +08:00
wenjie 174fbba14c refactor(backend): add darwin no-cgo tray fallback (#1689) 2026-03-17 19:43:44 +08:00
Alix-007 da1fddc4f0 docs(exec): document build tool guard limitation 2026-03-17 19:43:02 +08:00
Cytown afe22c5adf bug fix: gateway should not start when gateway server is not running (#1562) 2026-03-17 19:07:36 +08:00
wenjie 7b9fdaec32 feat(config): add exec controls and gate cron commands on exec settings (#1685)
- add a dedicated exec settings section in the config page
- support timeout and custom allow/deny regex patterns for exec
- validate custom exec regex patterns in the config API
- block cron command scheduling and execution when exec is disabled
- update tests and i18n strings for the new command settings
2026-03-17 18:56:52 +08:00
wenjie 8a44410e37 feat: add web gateway hot reload and polling state sync (#1684)
* feat(gateway): support hot reload and empty startup

- extract gateway runtime into pkg/gateway
- add gateway.hot_reload config with default and example values
- allow starting the gateway without a default model via --allow-empty
- stop treating missing enabled channels as a startup error
- update related tests

* feat: replace gateway SSE updates with polling-based state sync

- remove gateway SSE broadcasting and event endpoint
- add polling-based gateway status refresh with stopping state handling
- detect when gateway restart is required after default model changes
- resolve gateway health and websocket proxy targets from configured host
- update gateway UI labels and add backend/frontend test coverage
2026-03-17 18:46:00 +08:00
Liu Yuan 11207186c8 fix: proxy WebSocket through web server port (#1665)
- Modify buildWsURL to use web server port (18800) instead of gateway port (18790)
- Add WebSocket proxy handler to forward /pico/ws to gateway
- Gateway port is read from config (cfg.Gateway.Port), defaults to 18790
- This allows WebSocket connections through the same port as the web UI,
  avoiding the need to expose extra ports for Tailscale/Docker
2026-03-17 17:36:06 +08:00
Mauro 8a8cc35645 Merge pull request #1663 from hyperwd/fix/glm-nil-input
fix(providers): handle nil input in GLM series tool_use blocks
2026-03-17 08:42:17 +01:00
wenjie 0499cdab72 build: use WEB_GO for web targets and preserve backend dist directory (#1671)
Separate web Go commands from the default Go toolchain so web builds,
tests, and vet can enable CGO on Darwin without affecting the rest of
the project. Also ensure frontend backend builds recreate backend/dist
with a .gitkeep file so the embedded output directory remains tracked.
2026-03-17 15:23:49 +08:00
Desmond Foo b402888bfa feat(tools): add SpawnStatusTool for reporting subagent statuses (#1540)
* feat(tools): add SpawnStatusTool for reporting subagent statuses

* feat(tools): enhance SpawnStatusTool to restrict task visibility by conversation context

* feat(tests): add Unicode result truncation and channel filtering tests for SpawnStatusTool

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* feat(tools): enhance SpawnStatusTool with task ID validation and sorting by creation timestamp

* feat(tools): update SpawnStatusTool description and parameter documentation for clarity

* refactor(tests): improve comments for clarity in ChannelFiltering test case

* fix(tools): update no subagents message for clarity and remove unnecessary locking in runTask

* fix(tools): improve description clarity for SpawnStatusTool regarding task context

* feat(tools): add spawn_status tool configuration and registration

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix(agent): improve subagent management for spawn and spawn_status tools

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix(tests): update ResultTruncation_Unicode test to use valid CJK character

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: lxowalle <83055338+lxowalle@users.noreply.github.com>
2026-03-17 14:41:43 +08:00
Cytown e41423483e add systray ui for all platform (#1649)
* add systray ui for all platform

* update from getlantern/systray to fyne.io/systray for fix test
2026-03-17 14:12:32 +08:00
Administrator a26a7db7d2 moved turnState and related code from subturn.go to a new turn_state.go file
Created /pkg/agent/turn_state.go (246 lines) containing:
  - turnStateKeyType and context key management
  - turnState struct definition
  - TurnInfo struct and GetActiveTurn() method
  - newTurnState(), Finish(), and drainPendingResults() methods
  - ephemeralSessionStore implementation
  - All context helper functions (withTurnState, TurnStateFromContext, etc.)

  Updated /pkg/agent/subturn.go (428 lines) by:
  - Removing the moved turnState struct and methods
  - Removing unused imports (sync, session)
  - Keeping SubTurn spawning logic, config, events, and result delivery

  All tests pass and the code compiles successfully.
2026-03-17 14:11:38 +08:00
wenjie cef0f28881 fix(tools): normalize whitelist path checks for symlinked allowed roots (#1660)
- keep regex whitelist matching for existing configs
- add normalized directory-prefix checks for literal allow-path patterns
- support allowed roots that resolve through symlinks
- add regression coverage for symlink-backed whitelist paths
2026-03-17 14:10:11 +08:00
Administrator 12a8590ada fix(agent): enhance SubTurn robustness and fix race conditions
Major improvements to SubTurn implementation:

**Fixes:**
- Channel close race condition (sync.Once)
- Semaphore blocking timeout (30s)
- Redundant context wrapping
- Memory accumulation (auto-truncate at 50 msgs)
- Channel draining on Finish()
- Missing depth limit logging
- Model validation

**Enhancements:**
- Comprehensive documentation (150+ lines)
- 11 new tests covering edge cases
- Improved error messages

All tests pass. Production-ready.

Related: #1316
2026-03-17 12:50:32 +08:00
Zane Tung 8d97896a0d fix(providers): handle nil input in GLM series tool_use blocks
- add defensive nil check for tool call Arguments field
- replace nil input with empty object to comply with Anthropic spec
- prevent API errors when GLM models return null input in tool_use blocks

Zhipu AI's GLM series models may return tool_use blocks with null input field,
which causes their API to reject subsequent requests with error:
"ClaudeContentBlockToolResult object has no attribute id"

This fix ensures compatibility by converting nil inputs to empty objects {},
matching the Anthropic Messages API specification while maintaining backward
compatibility with other providers.
2026-03-17 12:16:24 +08:00
xiaoen c63c6449b4 fix(agent): forceCompression recovers from single oversized Turn
When the entire session history is a single Turn (e.g. one user message
followed by a massive tool response), findSafeBoundary returns 0 and
forceCompression previously did nothing — leaving the agent stuck in
a context-exceeded retry loop.

Now falls back to keeping only the most recent user message when no
safe Turn boundary exists. This breaks Turn atomicity as a last resort
but guarantees the agent can recover.

Also updates docs/agent-refactor/context.md to document this behavior.

Ref #1490
2026-03-17 10:23:16 +08:00
wenjie fcb69860c4 feat(web): add configurable cron command execution settings (#1647)
- add tools.cron.allow_command config with a default value of true
- require command_confirm only when cron command execution is disabled
- expose cron command permission and timeout settings in the config UI
- add backend tests and update i18n strings
2026-03-17 09:44:32 +08:00
Cytown be4a33cc15 refactor gateway/helpers and add server.pid to health (#1646) 2026-03-17 09:35:52 +08:00
Mauro 79b0568d75 Merge pull request #1536 from alexhoshina/fix/allow-picoclaw-media-tempdir
Fix: allow picoclaw media tempdir
2026-03-16 21:30:42 +01:00
Mauro dfafdf7c53 Merge pull request #1570 from alexhoshina/fix/cron-deliver-default-false
fix(cron): default scheduled jobs to agent execution
2026-03-16 21:05:06 +01:00
Mauro 2f61440269 Merge pull request #1645 from dimonb/fix/mask-bot-tokens-in-logger
Fix/mask bot tokens in logger
2026-03-16 20:55:24 +01:00
Administrator 672d11c7d4 fix(agent): prevent double result delivery and panic bypass in SubTurn
- Fix synchronous SubTurn calls placing results in pendingResults channel,
  causing double delivery. Now only async calls (Async=true) use the channel.
- Move deliverSubTurnResult into defer to ensure result delivery even when
  runTurn panics. Add TestSpawnSubTurn_PanicRecovery to verify.
- Fix ContextWindow incorrectly set to MaxTokens; now inherits from
  parentAgent.ContextWindow.
- Add TestSpawnSubTurn_ResultDeliverySync to verify sync behavior.
2026-03-16 23:48:51 +08:00
Administrator 3c2d373a5c fix(agent): resolve race conditions and resource leaks in SubTurn
Critical fixes (5):
- Fix turnState hierarchy corruption in nested SubTurns by checking context
  before creating new root turnState in runAgentLoop
- Fix deadlock risk in deliverSubTurnResult by separating lock and channel ops
- Fix session rollback race in HardAbort by calling Finish() before rollback
- Fix resource leak by closing pendingResults channel in Finish() with recovery
- Add thread-safety docs for childTurnIDs and isFinished fields

Medium priority fixes (5):
- Move globalTurnCounter to AgentLoop.subTurnCounter to prevent ID conflicts
- Improve semaphore acquisition to ensure release even on early validation failures
- Document design choice: ephemeral sessions start empty for complete isolation
- Add final poll before Finish() to capture late-arriving SubTurn results
- Remove duplicate channel registration in spawnSubTurn to fix timing issues

Testing:
- Add 6 new tests covering hierarchy, deadlock, ordering, channel lifecycle,
  final poll, and semaphore behavior
- All 12 SubTurn tests passing with race detector

This resolves 10 critical and medium issues (5 race conditions, 2 resource leaks,
3 timing issues) identified in code review, bringing SubTurn to production-ready state.
2026-03-16 22:54:01 +08:00
Administrator 6b5d7e3fd7 fix(agent): resolve critical race conditions and resource leaks in SubTurn
- Fix turnState hierarchy corruption when SubTurns recursively call runAgentLoop
  by checking context for existing turnState before creating new root
- Fix deadlock risk in deliverSubTurnResult by separating lock and channel operations
- Fix session rollback race in HardAbort by calling Finish() before rollback
- Fix resource leak by closing pendingResults channel in Finish() with panic recovery
- Add thread-safety documentation for childTurnIDs and isFinished fields
- Move globalTurnCounter to AgentLoop.subTurnCounter to prevent ID conflicts
- Improve semaphore acquisition to ensure release even on early validation failures
- Document design choice: ephemeral sessions start empty for complete isolation
- Add 5 new tests: hierarchy, deadlock, order, channel close, and semaphore
2026-03-16 22:37:21 +08:00
pixiaoka 9d761b7f5b Delete .claude/settings.json 2026-03-16 22:00:37 +08:00
Administrator acd436acfe feat(agent): add session state rollback on hard abort
- Add initialHistoryLength field to turnState to snapshot session state at turn start
- Save initial history length in runAgentLoop when creating root turnState
- Implement session rollback in HardAbort via SetHistory, truncating to initial length
- Add TestHardAbortSessionRollback to verify history rollback after abort
- Import providers package in subturn_test.go for Message type

This ensures that when a user triggers hard abort, all messages added during
the aborted turn are discarded, restoring the session to its pre-turn state.
2026-03-16 21:49:58 +08:00
Administrator 1236dd9e6d feat(agent): add concurrency semaphore and hard abort for SubTurn
- Add maxConcurrentSubTurns constant (5) and concurrencySem channel to turnState
- Acquire/release semaphore in spawnSubTurn to limit concurrent child turns per parent
- Add activeTurnStates sync.Map to AgentLoop for tracking root turn states by session
- Implement HardAbort(sessionKey) method to trigger cascading cancellation via turnState.Finish()
- Register/unregister root turnState in runAgentLoop for hard abort lookup
- Add TestSubTurnConcurrencySemaphore to verify semaphore capacity enforcement
- Add TestHardAbortCascading to verify context cancellation propagates to child turns
2026-03-16 21:03:58 +08:00
Administrator ceeae15d8a feat(agent): wire SubTurn into AgentLoop and Spawn Tool
- Add subTurnResults sync.Map to AgentLoop for per-session channel tracking
- Add register/unregister/dequeue methods in steering.go
- Poll SubTurn results in runLLMIteration at loop start and after each tool,
  injecting results as [SubTurn Result] messages into parent conversation
- Initialize root turnState in runAgentLoop, propagate via context
  (withTurnState/turnStateFromContext), call rootTS.Finish() on completion
- Wire Spawn Tool to spawnSubTurn via SetSpawner in registerSharedTools,
  recovering parentTS from context for proper turn hierarchy
- Refactor subagent.go to use SetSpawner pattern
- Add TestSubTurnResultChannelRegistration and TestDequeuePendingSubTurnResults
2026-03-16 20:44:04 +08:00
Dmitrii Balabanov 64ceb5ab76 fix(logger): show first/last 4 chars of bot token for identification 2026-03-16 12:48:28 +02:00
Dmitrii Balabanov 8fc36a4f9b fix(logger): mask bot tokens in 3rd-party logger output 2026-03-16 12:48:28 +02:00
Dimitrij Denissenko 26fa98c359 Align rendering with Matrix' CommonMark guidelines 2026-03-16 09:07:04 +00:00
Argobell 1ace296b91 fix: use fileEvent instead of event when appending fields for file logger
Co-authored-by: argobell <183611258+argobell@users.noreply.github.com>
2026-03-16 16:46:13 +08:00
Argobell 0459deca03 Initial plan 2026-03-16 16:45:39 +08:00
wenjie c513ad22d7 fix(web): refactor pico chat flow and fix proxied websocket URLs (#1639)
- move chat controller, state, protocol, history, and websocket logic into a dedicated chat feature module
- improve chat reconnection, session hydration, and send gating based on actual websocket state
- preserve gateway status during transient SSE disconnects and update stop state immediately
- generate wss websocket URLs behind HTTPS proxies and add backend tests for forwarded proto handling
2026-03-16 16:25:16 +08:00
xiaoen 08259d7e9a docs(agent-refactor): add context.md for Track 6 boundary clarification
Document the semantic boundaries of context management as called for
in the agent-refactor README (suggested document split, item 5):

- context window region definitions and history budget formula
- ContextWindow vs MaxTokens distinction
- session history contents (no system prompt stored)
- Turn as the atomic compression unit (#1316)
- three compression paths and their ordering
- token estimation approach and its limitations
- interface boundaries between budget functions and BuildMessages

Also documents known gaps: summarization trigger not using the full
budget formula, heuristic-only token estimation, and reactive retry
not preserving media references.

Ref #1439
2026-03-16 14:48:35 +08:00
xiaoen b768dab822 test(agent): use realistic session data in context retry test
Session history only stores user/assistant/tool messages — the system
prompt is built dynamically by BuildMessages. Remove the incorrect
system message from TestAgentLoop_ContextExhaustionRetry test data
to match the real data model that forceCompression operates on.
2026-03-16 14:48:35 +08:00
xiaoen 7c1a1c2c1a style(agent): fix gci comment alignment in test 2026-03-16 14:48:35 +08:00
xiaoen edbdc3bcf1 fix(agent): findSafeBoundary returns 0 for single-Turn history
When the entire history is a single Turn (one user message followed by
tool calls and responses, no subsequent user message), the only Turn
boundary is at index 0. Previously the fallback returned targetIndex,
which could land on a tool or assistant message — splitting the Turn.

Return 0 instead, so callers (forceCompression, summarizeSession) see
mid <= 0 and skip compression rather than cutting inside the Turn.
2026-03-16 14:48:35 +08:00
xiaoen 8034ee7be1 fix(agent): correct media token arithmetic and tool call double-counting
Two estimation bugs fixed:

1. Media tokens were added to the chars accumulator before the chars*2/5
   conversion, resulting in 256*2/5=102 tokens per item instead of 256.
   Fix: add media tokens directly to the final token count, bypassing
   the character-based heuristic.

2. estimateMessageTokens counted both tc.Name and tc.Function.Name for
   tool calls, but providers only send one (OpenAI-compat uses
   function.name, Anthropic uses tc.Name). Fix: count tc.Function.Name
   when Function is present, fall back to tc.Name only otherwise.

Also fix i18n hint text: "auto-detect" was misleading — the backend
uses a 4x max_tokens heuristic, not actual model detection.
2026-03-16 14:48:34 +08:00
xiaoen 639739cb85 refactor(agent): use Turn as the atomic unit for compression cut-off
Introduce parseTurnBoundaries() which identifies each Turn start index
in the session history. A Turn is a complete "user input → LLM iterations
→ final response" cycle (as defined in the agent refactor design #1316).

findSafeBoundary now uses Turn boundaries instead of raw role-scanning,
making the intent explicit: "find the nearest Turn boundary."

forceCompression drops the oldest half of Turns (not arbitrary messages),
which is simpler and more intuitive. The Turn-based approach naturally
prevents splitting tool-call sequences since each Turn is atomic.
2026-03-16 14:48:34 +08:00
xiaoen efd403242e fix(agent): preallocate messages slice in budget test
Fixes prealloc lint warning by using make() with capacity hint.
2026-03-16 14:48:34 +08:00
xiaoen b7f1c2b5fc test(agent): add realistic session-shaped tests for context budget
Add tests that reflect actual session data shape: history starts with
user messages (no system prompt), includes chained tool-call sequences,
reasoning content, and media items. Exercises the proactive budget check
path with BuildMessages-style assembled messages.
2026-03-16 14:48:34 +08:00
xiaoen e35906bb14 feat(config): expose context_window in example config and web UI
Add context_window to config.example.json, the web configuration page
(form model, input field, save handler), and i18n strings (en/zh).
The field is optional — leaving it empty falls back to the 4x max_tokens
heuristic.
2026-03-16 14:48:33 +08:00
xiaoen d5fdd5ebd2 fix(agent): include ReasoningContent and Media in token estimation
estimateMessageTokens now counts ReasoningContent (extended thinking /
chain-of-thought) which can be substantial and is persisted in session
history. Media items get a fixed per-item overhead (256 tokens) since
actual cost depends on provider-specific image tokenization.
2026-03-16 14:48:33 +08:00
xiaoen 9c65d78b07 fix(agent): forceCompression must not assume history[0] is system prompt
Session history (GetHistory) contains only user/assistant/tool messages.
The system prompt is built dynamically by BuildMessages and is never
stored in session. The previous code incorrectly treated history[0] as
a system prompt, skipping the first user message and appending a
compression note to it.

Fix: operate on the full history slice, and record the compression
note in the session summary (which BuildMessages already injects into
the system prompt) rather than modifying any history message.
2026-03-16 14:48:33 +08:00
xiaoen 9c82b0baa2 refactor(agent): context boundary detection, proactive budget check, and safe compression
Separate context_window from max_tokens — they serve different purposes
(input capacity vs output generation limit). The previous conflation caused
premature summarization or missed compression triggers.

Changes:
- Add context_window field to AgentDefaults config (default: 4x max_tokens)
- Extract boundary-safe truncation helpers (isSafeBoundary, findSafeBoundary)
  into context_budget.go — pure functions with no AgentLoop dependency
- forceCompression: align split to safe boundary so tool-call sequences
  (assistant+ToolCalls → tool results) are never torn apart
- summarizeSession: use findSafeBoundary instead of hardcoded keep-last-4
- estimateTokens: count ToolCalls arguments and ToolCallID metadata,
  not just Content — fixes systematic undercounting in tool-heavy sessions
- Add proactive context budget check before LLM call in runAgentLoop,
  preventing 400 context-length errors instead of reacting to them
- Add estimateToolDefsTokens for tool definition token cost

Closes #556, closes #665
Ref #1439
2026-03-16 14:48:32 +08:00
Administrator ae23193295 feat(agent): port subturn PoC to refactor/agent branch
- Replace duplicate types (ToolResult/Session/Message) with real project types
- Implement ephemeralSessionStore satisfying session.SessionStore interface
- Connect runTurn to real AgentLoop via runAgentLoop + AgentInstance
- Fix subturn_test.go to match updated signatures and types

Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
2026-03-16 14:31:32 +08:00
dependabot[bot] 0c94e6f7b3 chore(deps): bump docker/login-action from 3 to 4 (#1604)
Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 14:11:22 +08:00
dependabot[bot] b7b8d1eeca chore(deps): bump docker/build-push-action from 6 to 7 (#1602)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6 to 7.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 14:10:19 +08:00
dependabot[bot] f247c3bc00 chore(deps): bump actions/setup-go from 5 to 6 (#1600)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5 to 6.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 14:09:36 +08:00
dependabot[bot] 44ac304e5b chore(deps): bump actions/setup-node from 4 to 6 (#1597)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 14:09:01 +08:00
dependabot[bot] 4d4243b919 chore(deps): bump docker/setup-buildx-action from 3 to 4 (#1595)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 14:08:29 +08:00
sky5454 2f10b47f59 feat(credential): part1 add AES-GCM encryption, SecureStore, and onboard ke… (#1521)
* feat(credential): add AES-GCM encryption, SecureStore, and onboard keygen

- pkg/credential: new package with AES-256-GCM enc:// credential format,
  HKDF-SHA256 key derivation (passphrase + optional SSH key binding),
  ErrPassphraseRequired / ErrDecryptionFailed sentinel errors,
  and PassphraseProvider hook for runtime passphrase injection

- pkg/credential/store: lock-free SecureStore via atomic.Pointer[string];
  passphrase never written to disk or os.Environ

- pkg/credential/keygen: ed25519 SSH key generation helper used by onboard

- pkg/config: replace os.Getenv(PassphraseEnvVar) with
  credential.PassphraseProvider() at all three call sites so that
  LoadConfig and SaveConfig use whatever passphrase source is active

- cmd/picoclaw/onboard: prompt for passphrase with echo-off, generate
  picoclaw-specific SSH key, re-encrypt existing config on re-onboard

- docs/credential_encryption.md: design doc for the enc:// format

* fix(credential): address Copilot review comments on PR #1521

- credential.go: decouple ErrPassphraseRequired from env var name;
  message is now 'enc:// passphrase required' since PassphraseProvider
  may come from any source, not just os.Environ

- credential.go: Resolver resolves symlinks via EvalSymlinks before the
  isWithinDir containment check, preventing symlink-based path traversal
  for file:// credential references

- store.go: tighten comment to describe only what SecureStore guarantees
  (in-memory only); remove claims about how callers transport the value

- store_test.go: replace the meaningless GetReturnsCopy test (Go strings
  are immutable, equality across two calls proves nothing) with
  TestSecureStore_ConcurrentSetGet that exercises atomic.Pointer under
  10-goroutine concurrent Set/Get load

- config_test.go: update error-message assertion to match new sentinel text

- docs/credential_encryption.md: remove reference to non-existent
  'picoclaw encrypt' subcommand; describe the onboard flow instead

* fix(config): encryptPlaintextAPIKeys: struct-based encryption, fail-fast, remove raw []byte

* fix(credential): require SSH private key for encryption/decryption, remove passphrase-only mode

* lint: fix credential keygen lint, fix test keygen

* onboard: make encryption opt-in via --enc flag

Encryption (passphrase prompt + SSH key generation) is now only
triggered when the user passes --enc to 'picoclaw onboard'.
Without the flag, onboard skips the credential-encryption setup and
writes a plain config + workspace templates directly.

- Add --enc BoolFlag in NewOnboardCommand()
- Pass encrypt bool into onboard()
- Guard passphrase prompt, SSH key generation, and related env-var
  setup behind the encrypt branch
- Adjust 'Next steps' output so the passphrase reminder only appears
  when --enc was used
2026-03-16 14:06:32 +08:00
wenjie c8065989b0 chore(web): upgrade eslint deps to resolve flatted vulnerability (#1629) 2026-03-16 11:58:06 +08:00
dependabot[bot] 4178b2cec5 chore(deps): bump @tanstack/react-router in /web/frontend (#1609)
Bumps [@tanstack/react-router](https://github.com/TanStack/router/tree/HEAD/packages/react-router) from 1.163.3 to 1.167.0.
- [Release notes](https://github.com/TanStack/router/releases)
- [Changelog](https://github.com/TanStack/router/blob/main/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/TanStack/router/commits/@tanstack/react-router@1.167.0/packages/react-router)

---
updated-dependencies:
- dependency-name: "@tanstack/react-router"
  dependency-version: 1.167.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 11:05:31 +08:00
dependabot[bot] 99304d1f8e chore(deps): bump dayjs from 1.11.19 to 1.11.20 in /web/frontend (#1608)
Bumps [dayjs](https://github.com/iamkun/dayjs) from 1.11.19 to 1.11.20.
- [Release notes](https://github.com/iamkun/dayjs/releases)
- [Changelog](https://github.com/iamkun/dayjs/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/iamkun/dayjs/compare/v1.11.19...v1.11.20)

---
updated-dependencies:
- dependency-name: dayjs
  dependency-version: 1.11.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 11:05:17 +08:00
dependabot[bot] 3bf8a27570 chore(deps): bump react-i18next from 16.5.4 to 16.5.8 in /web/frontend (#1607)
Bumps [react-i18next](https://github.com/i18next/react-i18next) from 16.5.4 to 16.5.8.
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/react-i18next/compare/v16.5.4...v16.5.8)

---
updated-dependencies:
- dependency-name: react-i18next
  dependency-version: 16.5.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 11:05:03 +08:00
dependabot[bot] a93bd01329 chore(deps-dev): bump @vitejs/plugin-react in /web/frontend (#1606)
Bumps [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) from 5.1.4 to 5.2.0.
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/plugin-react@5.2.0/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@5.2.0/packages/plugin-react)

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 5.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 11:04:50 +08:00
dependabot[bot] b8dfd0befc chore(deps): bump jotai from 2.18.0 to 2.18.1 in /web/frontend (#1605)
Bumps [jotai](https://github.com/pmndrs/jotai) from 2.18.0 to 2.18.1.
- [Release notes](https://github.com/pmndrs/jotai/releases)
- [Commits](https://github.com/pmndrs/jotai/compare/v2.18.0...v2.18.1)

---
updated-dependencies:
- dependency-name: jotai
  dependency-version: 2.18.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 10:58:48 +08:00
dependabot[bot] 43eb6fe20c chore(deps): bump github.com/github/copilot-sdk/go from 0.1.23 to 0.1.32 (#1603)
Bumps [github.com/github/copilot-sdk/go](https://github.com/github/copilot-sdk) from 0.1.23 to 0.1.32.
- [Release notes](https://github.com/github/copilot-sdk/releases)
- [Changelog](https://github.com/github/copilot-sdk/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/copilot-sdk/compare/v0.1.23...v0.1.32)

---
updated-dependencies:
- dependency-name: github.com/github/copilot-sdk/go
  dependency-version: 0.1.32
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 10:58:18 +08:00
dependabot[bot] 2f40a8c165 chore(deps): bump github.com/anthropics/anthropic-sdk-go (#1601)
Bumps [github.com/anthropics/anthropic-sdk-go](https://github.com/anthropics/anthropic-sdk-go) from 1.22.1 to 1.26.0.
- [Release notes](https://github.com/anthropics/anthropic-sdk-go/releases)
- [Changelog](https://github.com/anthropics/anthropic-sdk-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/anthropics/anthropic-sdk-go/compare/v1.22.1...v1.26.0)

---
updated-dependencies:
- dependency-name: github.com/anthropics/anthropic-sdk-go
  dependency-version: 1.26.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 10:51:55 +08:00
dependabot[bot] e9d240d760 chore(deps): bump github.com/caarlos0/env/v11 from 11.3.1 to 11.4.0 (#1599)
Bumps [github.com/caarlos0/env/v11](https://github.com/caarlos0/env) from 11.3.1 to 11.4.0.
- [Release notes](https://github.com/caarlos0/env/releases)
- [Commits](https://github.com/caarlos0/env/compare/v11.3.1...v11.4.0)

---
updated-dependencies:
- dependency-name: github.com/caarlos0/env/v11
  dependency-version: 11.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 10:47:46 +08:00
dependabot[bot] dd936302d1 chore(deps): bump github.com/mymmrac/telego from 1.6.0 to 1.7.0 (#1598)
Bumps [github.com/mymmrac/telego](https://github.com/mymmrac/telego) from 1.6.0 to 1.7.0.
- [Release notes](https://github.com/mymmrac/telego/releases)
- [Commits](https://github.com/mymmrac/telego/compare/v1.6.0...v1.7.0)

---
updated-dependencies:
- dependency-name: github.com/mymmrac/telego
  dependency-version: 1.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 10:46:54 +08:00
dependabot[bot] 45c01f4d91 chore(deps): bump golang.org/x/oauth2 from 0.35.0 to 0.36.0 (#1596)
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.35.0 to 0.36.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.35.0...v0.36.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-version: 0.36.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 10:42:04 +08:00
BitToby 71e2b636d6 fix: Use secure defaults for Pico channel setup and stop leaking the token in the URL (#1563)
* fix: Use secure defaults for Pico channel setup and stop leaking the token in the URL

* fix: Derive default allow_origins from the setup request's Origin header instead of hardcoding localhost ports
2026-03-16 09:58:37 +08:00
afjcjsbx de68688c75 fix lint 2026-03-15 22:30:02 +01:00
afjcjsbx d5c2bc538a feat(tool): markdown format in output web_fetch tool 2026-03-15 22:12:03 +01:00
Mauro 021aa7d6d5 feat(agent): steering (#1517)
* feat(agent): steering

* fix loop

* fix lint

* fix lint
2026-03-16 00:08:16 +08:00
duomi 5660b8f24b fix(heartbeat): ignore untouched default template 2026-03-15 21:58:21 +08:00
Caize Wu f2addff099 Merge pull request #1590 from sky5454/main
feat/sec add github's dependabot to scan the lib sec.
2026-03-15 18:56:54 +08:00
sky5454 54f870c255 feat/sec add github's dependabot to scan the lib sec. 2026-03-15 18:02:26 +08:00
Caize Wu 96fd4e0519 Merge pull request #1583 from alexhoshina/fix/#1422-lint-err
fix(provider/azure): lint err
2026-03-15 13:13:43 +08:00
Hoshina f7dd040ae4 fix(provider/azure): lint err 2026-03-15 12:45:11 +08:00
Mauro 5a251b46af Merge pull request #1442 from afjcjsbx/feat/logger-stdout-formatting
feat(logger): Custom console formatter for JSON and multiline strings
2026-03-14 22:04:51 +01:00
Kunal Karmakar 5fb4b3bedf feat(provider): add support for azure openai provider (#1422)
* Add support for azure openai provider

* Add checks for deployment model name

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Addressing @Copilot suggestion to remove the init() function which seemed redundant

* Fix readme

* Fix linting checks

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-14 22:52:34 +08:00
Hoshina f71eaaf7f8 fix(cron): default scheduled jobs to agent execution 2026-03-14 21:03:23 +08:00
Hoshina bb1a414527 fix(tools): harden whitelist path resolution 2026-03-14 19:58:23 +08:00
Guoguo 0f700a6bf0 docs: update wechat qrcode (#1564) 2026-03-14 18:36:19 +08:00
Caize Wu 9ab1450ab5 Merge pull request #1531 from horsley/chore/add-deepwiki-badge-readmes
docs: add Ask DeepWiki badge to all README variants
2026-03-14 17:28:18 +08:00
horsley 93369c0011 docs: reorganize README badge layout with docs and wiki links 2026-03-14 09:20:49 +00:00
dataCenter430 0c5d7500e8 feat: expose local file paths for non-image media to enable agent file tools (#1516)
* feat: expose local file paths for non-image media to enable agent file tools

* fix: Golang Lint error
2026-03-14 12:09:11 +08:00
Hoshina 345452fba8 refactor(tools): remove unused validatePath wrapper 2026-03-14 12:08:11 +08:00
Hoshina 1bc05e8392 fix(tools): allow sandbox access to temp media files 2026-03-14 12:02:06 +08:00
Hoshina b9aaad95cd refactor(media): centralize temp media dir path 2026-03-14 12:01:47 +08:00
horsley de0dd241b9 docs: add Ask DeepWiki badge to all README variants 2026-03-14 02:52:06 +00:00
GPER 555af137b4 添加使用火山coding plain配置参数 (#1511) 2026-03-14 09:30:02 +08:00
Alix-007 c68b4f3903 fix(qq): populate account bindings metadata (#1456)
Co-authored-by: XYSK-lilong007 <267018309+XYSK-lilong007@users.noreply.github.com>
2026-03-13 23:08:55 +08:00
afjcjsbx 78c9b86d7e added tests 2026-03-13 14:02:28 +01:00
iMil 86da6a7d56 #434 added NetBSD support for picoclaw, but since then, picoclaw-launcher{-tui} appeared (#1508) 2026-03-13 19:52:32 +08:00
wenjie 4d8fdb0b3d feat(web): use a global WebSocket for Pico chat sessions (#1507)
- centralize Pico chat connection and session state in a shared store
- move chat lifecycle control out of usePicoChat
- hydrate and restore the active session across the app
2026-03-13 19:04:18 +08:00
Meng Zhuo 27fef9eab8 Merge pull request #1441 from Alix-007/fix/issue-1269-weather-skill-reliability
fix(skill): tighten weather location matching guidance
2026-03-13 18:10:56 +08:00
lxowalle 2f83c185ae Fix the issue where the cursor moves inaccurately left and right after entering Chinese when running the picoclaw agent. (#1505) 2026-03-13 17:58:34 +08:00
wenjie c69c48ad46 refactor(web): split gateway logs out of the status endpoint (#1504)
- add a dedicated /api/gateway/logs endpoint for incremental log polling
- keep /api/gateway/status focused on runtime and health data only
- update frontend log fetching to use the new API and add backend tests covering the status/logs separation and cleared-log behavior
2026-03-13 17:58:20 +08:00
Hakancan 6b72326be1 fix: safety guard incorrectly blocks commands with URLs (#1254)
* fix: safety guard incorrectly blocks commands with URLs

The absolutePathPattern regex was matching URL path components like
//github.com as file system paths, causing commands containing URLs
to be incorrectly blocked by the workspace restriction safety guard.

For example, 'agent-browser open https://github.com' would be blocked
because //github.com was treated as an absolute file path outside
the working directory.

The fix adds a check to skip any path match that starts with '//',
as these are URL path components, not file system paths.

Fixes #1203

* fix: handle file:// URIs correctly in safety guard

The previous fix skipped all paths starting with '//', which incorrectly
also skipped file:// URIs that could escape the workspace sandbox.

Changes:
- Only skip '//' paths when preceded by web URL schemes (http:, https:, ftp:, etc.)
- file:// URIs are now properly checked against workspace boundaries
- Added TestShellTool_FileURISandboxing to verify the fix

Fixes security issue raised by @alexhoshina in PR #1254

* style: fix gofumpt formatting

* fix(safety-guard): use exact match position to prevent URL exemption bypass

Using strings.Index(cmd, raw) always returned the first occurrence of the
matched substring, allowing a bypass where the same //path appeared both
inside a URL and as a standalone shell path (e.g. echo https://etc/passwd
&& cat //etc/passwd would skip the second match).

Switch to FindAllStringIndex so each match is evaluated at its actual
position in the command string.

Adds TestShellTool_URLBypassPrevented to cover the exploit scenario.
2026-03-13 17:16:05 +08:00
lxowalle 9530883d2c Fix/Add warning tips for MCP initialization when no valid servers configured (#1497)
* add tips for mcp

* fix test issue
2026-03-13 16:43:00 +08:00
wenjie 87257819f6 feat(web): add restart-required state for default model changes (#1499)
- track boot and config default models in gateway status/events
- preserve running, starting, and restarting states during health checks
- add safer gateway restart handling with stronger backend test coverage
- expose restart-required UI and refresh model state after default model update
2026-03-13 16:30:59 +08:00
美電球 4ccea5eb93 fix(identity): prevent allowlist ID entries from matching usernames (#1406) 2026-03-13 15:41:18 +08:00
iMil 516f7103b0 add NetBSD to the list of released platforms (#434)
* add NetBSD to the list of released platforms

* ignore platforms s390x mips64 and arm for NetBSD

* add NetBSD to the build-all target
2026-03-13 15:19:37 +08:00
Cytown 9676e51e89 make gateway aware of config.json change (#1187)
* make gateway aware of config.json change

* fix according to code review

* fix lint

* fix review comment

* fix for review

* refactor to fix review

* fix for review

* fix for review
2026-03-13 14:27:46 +08:00
Cytown dfa36f39cb add model command to set default model (#1250)
* add model command to set default model

* fix for ci

* fix test for model

* fix active agent not recognized

* implement test for model command

* fix local-model can not set as default issue

* fix review comment

* fix for comment
2026-03-13 14:10:11 +08:00
Zane Tung 9fed4ec136 feat: add anthropic-messages protocol for native Anthropic Messages API support Fixes #269 (#1284)
* feat: add anthropic-messages protocol support

Add native Anthropic Messages API format support to enable
compatibility with custom endpoints that only support Anthropic's
native message format (not OpenAI-compatible format).

Changes:
- Add new pkg/providers/anthropic_messages package with HTTP-based provider
- Implement Anthropic Messages API request/response format conversion
- Add anthropic-messages protocol support in factory_provider.go
- Include comprehensive unit tests (64.2% coverage)

Features:
- Support for system, user, assistant, and tool messages
- Support for tool calls (tool_use blocks)
- Proper header handling (x-api-key, anthropic-version)
- Configurable max_tokens and temperature
- Automatic base URL normalization

Configuration example:
  model: "anthropic-messages/claude-opus-4-6"
  api_base: "https://api.anthropic.com"
  api_key: "sk-..."

Tested with actual API endpoint, verified compatibility
with Anthropic Messages API specification.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* docs: add anthropic-messages protocol examples to README and config

Add configuration examples and documentation for the new
anthropic-messages protocol:

- config.example.json: Add claude-opus-4.6 example with anthropic-messages
- README.md: Add "Anthropic Messages API (native format)" section
- README.zh.md: Add Chinese version of the documentation

This helps users understand when to use anthropic-messages vs
anthropic protocol and fixes issue #269.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: format code with gofmt -s

- Align constant definitions in provider.go
- Align struct fields in test cases
- Fix gofmt formatting issues reported in review

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: address linter errors

- Fix HTTP header canonical form: "x-api-key" → "X-API-Key"
- Fix HTTP header canonical form: "anthropic-version" → "Anthropic-Version"
- Format imports with gci (standard, default, localmodule order)
- Format code with golines (max line length 120)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: resolve golangci-lint errors in anthropic-messages provider

- add nolint comment for canonicalheader rule on X-API-Key header (Anthropic API requires exact casing)
- fix golines formatting issues in provider_test.go (split long lines under 120 chars)
- fix long comment line in factory_provider.go (split into two lines)

Resolves CI linter failures for the anthropic-messages protocol implementation.

* fix(providers): address review comments in anthropic-messages provider

- fix normalizeBaseURL edge case that incorrectly appends /v1 to URLs already containing /v1 path (e.g., https://api.example.com/v1/proxy)
- remove dead code for apiBase empty check as normalizeBaseURL() always provides a default value
- update test to use proper constructor instead of direct struct initialization
- add detailed comments explaining the URL normalization logic

Resolves review comments on PR #1284

* fix(providers): remove hardcoded max_tokens in anthropic-messages provider

- remove hardcoded max_tokens value (4096) from buildRequestBody
- read max_tokens directly from options parameter
- add error handling when max_tokens is missing from options
- update test cases to include max_tokens in options

This fix ensures the provider respects the config default value (32768)
or system fallback (8192) instead of always using the hardcoded 4096.

* fix(providers): improve error handling and add edge case tests

- fix ToolCalls nil vs empty slice issue to ensure consistent JSON serialization
- add detailed HTTP error handling for common status codes (401, 429, 400, 404, 500, 503)
- add edge case tests for buildRequestBody and parseResponseBody
- clarify anthropic vs anthropic-messages protocol differences in docs

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-13 14:09:40 +08:00
leamon 0fb92b21b6 enhance skill installer (#1252)
* enhance skill installer

* enhance install skills v2

* go file formate

* fix:use proxy download skills;many chunck download;simple code

* add default config to config.example.json, download skill from github use proxy and token

---------

Co-authored-by: FantasticCode2019 <1443996278@qq.com>
2026-03-13 14:04:02 +08:00
dataCenter430 b811e9186c feat(provider): add ModelScope as OpenAI-compatible provider (#1486)
* feat(provider): add ModelScope as OpenAI-compatible provider

* test(provider): add ModelScope provider and migration tests

* docs: add ModelScope to README provider tables and free tier sections

* chore: add ModelScope to example config and env template
2026-03-13 14:02:23 +08:00
Cytown 83e24e8ceb fix 3rd party logger not correct output (#1482) 2026-03-13 11:20:17 +08:00
lxowalle f01eeac300 update volcengine doc (#1481) 2026-03-13 11:15:39 +08:00
Alix-007 d24fccd34f Merge pull request #1385 from Alix-007/fix/issue-1373-restore-last-session
fix(web): restore the last active chat session
2026-03-13 10:34:47 +08:00
Eric Jacksch 56fb0dc4e3 fix(claude_cli): surface stdout in error when CLI exits non-zero
When the claude CLI exits with a non-zero status, the previous error
handler only checked stderr. However, the CLI writes its output
(including error details) to stdout, especially when invoked with
--output-format json. This left the caller with only "exit status 1"
and no actionable information.

Now includes both stderr and stdout in the error message so the actual
failure reason is visible in logs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 22:01:56 -04:00
wenjie c508d6ffce Merge pull request #1429 from darrenzeng2025/fix/svg-mime-type
fix(web): correct SVG MIME type to image/svg+xml
2026-03-13 09:46:14 +08:00
afjcjsbx a01af36af4 feat(logger): add custom console formatter for JSON and multiline strings 2026-03-12 18:58:24 +01:00
李龙 0668001470 047a9bb835 fix(skill): tighten weather location matching guidance 2026-03-13 01:43:19 +08:00
don 19835b2f60 fix(line): limit webhook request body size to prevent DoS (#1413)
* fix(line): limit webhook request body size to prevent DoS

Add io.LimitReader with 1 MB cap on the LINE webhook handler to prevent
unauthenticated memory exhaustion via oversized POST requests.

Follows the same pattern used in the WeCom channel (io.LimitReader).
Requests exceeding the limit are rejected with 413 Request Entity Too Large.

Fixes #1407

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(line): hoist body size const, add boundary tests

- Move maxWebhookBodySize to package-level const
- Add TestWebhookAcceptsMaxBodySize (exact limit → 403, not 413)
- Add TestWebhookRejectsOversizedBodyBeforeSignatureCheck
- Use const in test instead of magic number

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 23:55:40 +08:00
Horsley Lee 8f49af99f9 fix(matrix): stream inbound media downloads to disk (#1436) 2026-03-12 23:48:26 +08:00
曾文锋0668000834 e4460d3815 fix(web): correct SVG MIME type to image/svg+xml
Go's built-in mime.TypeByExtension returns 'image/svg' for .svg files,
but the correct MIME type per RFC 6838 is 'image/svg+xml'. This fix
registers the correct MIME type when setting up the static file server.

Fixes #1410
2026-03-12 20:39:26 +08:00
wenjie d18a319b0c fix(web): render ansi logs with wrapped lines (#1425) 2026-03-12 19:12:19 +08:00
wenjie 7872bb3f0a Merge pull request #1421 from sipeed/refactor/config-ui
refactor(web): redesign config pages and extract raw JSON editor
2026-03-12 18:15:16 +08:00
Guoguo 6460a0a7c7 fix(nightly): reuse single nightly tag, no per-day tags (#1415)
- Disable goreleaser GitHub release for nightly (Docker still pushed)
- Use GORELEASER_CURRENT_TAG with local-only tag for version/validation
- Force-update single `nightly` git tag instead of creating per-day tags
- Docker tags use only `nightly`/`nightly-launcher`, no per-day versions
- Set --latest=false on nightly release to avoid occupying latest
- Simplify workflow from 3 jobs to 1 job, remove all cleanup steps

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 16:16:42 +08:00
Guoguo 1e024321c0 refactor: update model name and add VolcEngine coding plan (#1412)
* docs: swap header logo to webp, move meme logo to bottom

Replace header logo with assets/logo.webp across all 6 README
language variants and move the original meme logo (logo.jpg)
to the bottom of each file.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: update GPT model names to gpt-5.4 and refine provider descriptions

Update all 6 language README variants:
- Correct GPT model references from gpt-5.2/gpt4 to gpt-5.4
- Refine provider descriptions in API Key comparison tables

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: update default model to gpt-5.4, codex to gpt-5.3-codex

Update OpenAI default model references from gpt-5.2 to gpt-5.4
across source code, config examples, tests, and docs. Set Codex
default model to gpt-5.3-codex.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 16:10:29 +08:00
Alix-007 b5bd434ddb fix(web): allow horizontal scroll in raw json editor (#1386)
Co-authored-by: XYSK-lilong007 <267018309+XYSK-lilong007@users.noreply.github.com>
Co-authored-by: wenjie <meetwenjie@gmail.com>
2026-03-12 14:51:52 +08:00
wenjie 7273c7fe35 Merge pull request #1377 from Alix-007/fix/issue-1364-firefox-raw-json
fix: keep raw JSON editor full-height in Firefox
2026-03-12 14:43:08 +08:00
Alix-007 3bcbfd99b9 fix(channels): stop stale typing loops on overwrite (#1392)
Co-authored-by: XYSK-lilong007 <267018309+XYSK-lilong007@users.noreply.github.com>
2026-03-12 14:31:00 +08:00
Cytown 7359b2c86c add testcase for migrate from v0 to v1 2026-03-12 14:09:31 +08:00
Cytown 927958e6b3 Merge branch 'main' into version 2026-03-12 13:57:21 +08:00
Cytown 1c123e0162 refactor Config to add Version and migratable 2026-03-12 13:52:55 +08:00
Guoguo b4d00c631d docs: update wechat qrcode (#1394) 2026-03-12 11:01:11 +08:00
Mahendra Teja 8cac29d9bb docs: remove stale TOOLS.md references (#1388)
TOOLS.md was intentionally removed in 21d60f6 and #771, as tools are
now provided to the LLM via JSON schema through ToProviderDefs().
These references were missed during that cleanup.

Suggested by @yinwm in #1355.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 09:47:01 +08:00
kiannidev dc037f0d79 fix(telegram): stop typing indicator when LLM fails or hangs 2026-03-12 02:22:04 +02:00
Mahendra Teja 6612ca099a fix(openai_compat): improve prompt_cache_key host matching (#1387)
LGTM! The changes improve the robustness of prompt_cache_key host matching and add Azure OpenAI support. Thanks for the contribution!
2026-03-12 03:24:31 +08:00
amagi 49204df678 fix(openai_compat): accept object tool call arguments (#1292) 2026-03-12 02:47:22 +08:00
Cytown d920b78b41 refactor logger to zerolog (#1239)
* refactor logger to zerolog

* modify dingtalk and discord logger

* fix for lint

* fix for review

* fix for file leak

* fix for review
2026-03-12 02:35:37 +08:00
LeaderOnePro 9222351871 feat(providers): add LongCat model provider support (#1317)
* feat(providers): add LongCat model provider support

Add LongCat as an OpenAI-compatible provider with base URL
https://api.longcat.chat/openai and default model LongCat-Flash-Thinking.
Includes provider config, migration, factory routing, example config,
tests, and README entries for all 6 locales.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(providers): address LongCat review feedback

- Add dedicated factory routing test for LongCat provider
- Add longcat to DefaultAPIBase test coverage
- Set default api_base in example config providers section

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test(providers): add ResolveProviderSelection tests for LongCat

Add two test cases to TestResolveProviderSelection:
- Explicit provider selection with api_base default and proxy wiring
- Fallback inference from model name with api_base default

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-12 02:34:42 +08:00
Darren.Zeng 8431fa3e04 fix(config): support Chinese comma separator in allow_from environment variables (#1301)
Add UnmarshalText method to FlexibleStringSlice to support both English
(,) and Chinese (,) comma separators in environment variables.

Includes comprehensive unit tests covering:
- English commas, Chinese commas, mixed commas
- Single values, whitespace trimming
- Empty strings, edge cases

Fixes #1280
2026-03-12 02:33:33 +08:00
Dimitrij Denissenko 39a451d312 Enable rich-text messages in matrix channel (#1370)
* Enable rich-text messages in matrix channel

* Fix lint
2026-03-12 01:25:28 +08:00
Mahendra Teja 4a80c6f58c fix(openai_compat): only send prompt_cache_key to OpenAI endpoints (#1353)
Non-OpenAI providers (Mistral, DeepSeek, Groq, etc.) reject unknown
request fields with 422 errors. The previous blocklist only excluded
Google/Gemini, but the comment already noted this feature is
OpenAI-only. Flip to an allowlist so only api.openai.com receives
the field.

Fixes #1333
2026-03-12 01:21:54 +08:00
XYSK-lilong007 fcf9545736 fix: use native textarea scrolling for raw config editor 2026-03-12 01:21:44 +08:00
Congregalis 9b0a48ac6d fix(agent): initialize MCP in direct agent mode (#1361) 2026-03-12 01:06:48 +08:00
wenjie 4a8a2e9c23 chore(frontend): update pnpm-lock.yaml (#1368) 2026-03-11 20:23:37 +08:00
wenjie 8949a2575b Add exec allow_remote config support in web settings (#1363)
- default tools.exec.allow_remote to true when omitted in config loading
- preserve allow_remote in OpenClaw config migration and API updates
- expose allow_remote in the web config form with i18n strings
- add backend and config tests covering the new default behavior
2026-03-11 19:57:59 +08:00
wenjie 8c2a9332c6 fix(security): harden unauthenticated tool-exec paths (#1360)
* fix(security): harden unauthenticated tool-exec paths (GHSA-pv8c-p6jf-3fpp)

- Exec tool: channel-based access control (default deny remote)
- Cron tool: command scheduling restricted to internal channels
- Web fetch: SSRF defense-in-depth (pre-flight + dial-time + redirect checks)
- File permissions: session/state dirs 0700, files 0600
- Registry: inject __channel/__chat_id into tool args (replaces racy SetContext)

28 new security regression tests.

(cherry picked from commit 191446ae19021604d3d5b0d9376b9655ab749105)

* fix(exec): revalidate working_dir before command start

* test(web): allow local oversized payload fixture

---------

Co-authored-by: xj <gh-xj@users.noreply.github.com>
2026-03-11 19:22:20 +08:00
wenjie dea06c391c feat(web): add agent management UI and improve launcher integration (#1358)
* Improve the web launcher and gateway integration across backend and frontend.

- add runtime model availability checks for local and OAuth-backed models
- support launcher-driven gateway host overrides and websocket URL resolution
- add gateway log clearing and keep incremental log sync consistent after resets
- migrate session history APIs to JSONL metadata-backed storage with legacy fallback
- expose session titles and improve chat history loading and error handling
- move shared backend runtime helpers into the web utils package
- avoid blocking web startup when automatic onboard initialization fails
- add backend tests covering gateway readiness, host resolution, models, logs, and sessions

* feat(agent): add skills and tools management APIs and UI

- add backend APIs to list, view, import, and delete skills
- add tool status and toggle endpoints with dependency-aware config updates
- add agent skills/tools pages, routes, sidebar entries, and i18n strings
- add backend tests for the new skills and tools flows

* chore(frontend): upgrade shadcn to 4.0.5 and refresh lockfile

* chore(web): keep backend dist placeholder tracked
2026-03-11 18:37:00 +08:00
nayihz 8a398988d7 refactor skills loader markdown metadata parsing (#1354) 2026-03-11 18:08:00 +08:00
Mauro 30584f04cb Merge pull request #1214 from afjcjsbx/feat/echo-voice-audio-transcription
feat(channel): echo voice audio transcription feedback
2026-03-11 08:45:25 +01:00
wenjie e74820cf69 fix: skip meta json files during session migration (#1340) 2026-03-11 14:29:42 +08:00
Cage d5cbf198b2 fix: resolve gateway binary path, pass --config flag, and clarify empty model error (#1337) 2026-03-11 12:54:08 +08:00
美電球 755fa32336 Merge pull request #1330 from statxc/fix/session-key-sanitize-slash
fix(session): sanitize '/' and '\' in session keys so forum topic key…
2026-03-11 12:18:54 +08:00
afjcjsbx 08cc09e091 resolve conflicts 2026-03-11 00:17:10 +01:00
afjcjsbx 87d458f519 Merge remote-tracking branch 'origin/main' into feat/echo-voice-audio-transcription
# Conflicts:
#	pkg/channels/telegram/telegram.go
#	pkg/config/config.go
#	pkg/config/defaults.go
2026-03-11 00:06:37 +01:00
Mauro 9cd2d21800 Merge pull request #1207 from afjcjsbx/feat/debug-mode-no-truncate
feat: no-truncate param for debug
2026-03-10 17:13:44 +01:00
statxc 2e3e6788ab fix(session): sanitize '/' and '\' in session keys so forum topic keys don't create invalid paths 2026-03-10 16:11:34 +00:00
美電球 54f0680add Merge pull request #1291 from statxc/feat/telegram-forum-topics
feat(telegram): support forum topics with per-topic session isolation
2026-03-10 21:38:40 +08:00
statxc 320fcd1f02 fix: Add IsForum check so only forum topic threads get session isolation, not regular group reply threads 2026-03-10 13:25:14 +00:00
Guoguo 8654ec90d9 feat(docker): add launcher bundle image with all three binaries (#1309)
Add a new Docker image variant tagged as `launcher` that includes
picoclaw, picoclaw-launcher, and picoclaw-launcher-tui. The image
defaults to running picoclaw-launcher (web console) instead of gateway.

Original minimal single-binary image remains unchanged.

New files:
- docker/Dockerfile.goreleaser.launcher: goreleaser Docker with 3 binaries

Updated:
- .goreleaser.yaml: new dockers_v2 entry for launcher tag
2026-03-10 17:42:21 +08:00
lxowalle 680e845d61 feat:Modify the location where version is obtained, and insert version information into the context (#1300)
* feat:migrate version info from internal package to pkg/config

* * fix lint issue
2026-03-10 17:42:05 +08:00
yanhool 95716b106b feat(web_search): add load balance and failover for api keys (#982)
* feat(web_search): add load balance and failover for api keys

* feat(web_search): add load balance and failover for api keys

* lint

* new iter to get api key

* deleted conflicts
2026-03-10 19:34:11 +11:00
is-Xiaoen 26f623ed32 feat(session): integrate JSONL persistence into agent loop (#1170)
* feat(session): add SessionStore interface and JSONL backend adapter

Extract a SessionStore interface from the methods the agent loop uses
(AddMessage, GetHistory, SetSummary, TruncateHistory, Save, etc.).
Both SessionManager and the new JSONLBackend satisfy this interface,
allowing the persistence layer to be swapped transparently.

JSONLBackend wraps memory.Store and maps its error-returning API to
the fire-and-forget contract that the agent loop expects — write
errors are logged, reads return empty defaults on failure. Save()
triggers compaction to reclaim space after logical truncation.

Part of #1169

* test(session): add JSONLBackend integration tests

8 tests covering the full SessionStore contract through the JSONL
backend: message roundtrip, tool calls, summary, truncation with
compaction, history replacement, empty sessions, session isolation,
and the complete summarization flow (SetSummary → TruncateHistory →
Save).

Includes compile-time interface satisfaction checks for both
SessionManager and JSONLBackend.

Part of #1169

* feat(agent): wire JSONL session store into agent loop

Replace the concrete *SessionManager field with the SessionStore
interface and initialize the JSONL backend by default. Legacy .json
session files are auto-migrated on first startup. Falls back to
SessionManager if the JSONL store cannot be initialized.

The agent loop code (loop.go) requires zero changes — all method
calls work identically through the interface.

Closes #1169

* fix(session): propagate compact error from Save

Save() was swallowing the error returned by Compact and always
returning nil. Callers checking Save's return value would never
see a compaction failure. Return the error directly so the agent
loop can log or handle it as needed.

* feat(session): add Close to SessionStore interface

Add Close() error to SessionStore so callers can release resources
through the interface. JSONLBackend already had Close; this adds
a no-op implementation to SessionManager for compatibility.

* fix(session): close session stores on shutdown and harden migration

- Add Close() to AgentInstance, AgentRegistry, and AgentLoop so JSONL
  file handles are released during gateway shutdown and CLI exit.
- Fall back to SessionManager when migration fails, preventing a split
  state where some sessions live in JSONL and others remain in JSON.
- Add defer agentLoop.Close() in the CLI agent command path.
- Document SessionStore interface methods (fire-and-forget contract).
2026-03-10 15:14:09 +08:00
statxc 3f1e89da7f fix: solve Lint errors 2026-03-10 04:22:00 +00:00
美電球 2312553286 feat(channels): enhance QQ channel with group/typing/media support and URL sanitization (#1208)
* feat(channels): enhance QQ channel with group support, typing, media, and URL sanitization

Add group message routing alongside existing C2C (direct) support using
chatType sync.Map to track whether a chatID is group or direct. Implement
passive reply with msg_id/msg_seq tracking for multi-part responses.

Add StartTyping (InputNotify msg_type=6 with periodic resend), SendMedia
(RichMediaMessage for HTTP/HTTPS URLs), and configurable Markdown message
support. Replace unbounded dedup map with TTL-based expiry and janitor
goroutine.

Sanitize URLs in group messages by replacing dots in domains with fullwidth
period to avoid QQ's URL blacklist rejection (error 40054010). Add rate
limit config (5 msg/s) and MaxMessageLength/SendMarkdown config fields.

* fix(channels): address review feedback on QQ channel implementation

- Fix goroutine leak: reinitialize done channel and sync.Once in Start()
  to prevent multiple janitor goroutines on restart
- Fix double-close panic: guard close(done) with sync.Once in Stop()
- Fix StartTyping context: use c.ctx (channel lifecycle) instead of
  caller's ctx (request lifecycle) for typing goroutine
- Refactor: extract getChatKind() helper to deduplicate chatType lookup
  across Send(), StartTyping(), and SendMedia()
- Fix: use new(atomic.Uint64) instead of taking address of local var
- Fix: require explicit http(s):// scheme in URL regex to avoid false
  positives on version strings like "1.2.3"
- Optimize: collect expired keys before deleting in dedupJanitor to
  reduce lock hold time
- Fix: remove MaxMessageLength zero-value override in NewQQChannel
  since defaults.go already sets 2000

* fix(channels): address second round of review feedback on QQ channel

- Fix SendMedia: bypass media store for direct http(s) URLs in part.Ref;
  only fall back to store.Resolve for media:// refs; log clear warning
  for local-only paths instead of silently skipping
- Fix chatType routing: default unknown chatIDs to "group" (safer for QQ
  since outbound-only destinations like reasoning_channel_id are groups);
  pre-register reasoning_channel_id as group at Start() time; add debug
  log for untracked chatIDs
- Add dedup hard cap (10000 entries): evict oldest entry when map
  exceeds capacity to prevent unbounded memory growth under high traffic
2026-03-10 12:07:02 +08:00
statxc 123275fcbe feat(telegram): support forum topics with per-topic session isolation 2026-03-10 02:54:10 +00:00
afjcjsbx 86ce76219f no-truncate shorthand flag 2026-03-10 00:15:47 +01:00
afjcjsbx cc955627b7 debug doc 2026-03-10 00:10:07 +01:00
afjcjsbx 68e40aeb47 fix typo 2026-03-09 23:56:59 +01:00
Mauro b89f6445d1 feat(mcp): tool search tools (#1243)
* feat(mcp): tool search tools

* removed unused call_discovered_tool

* improvements and optimizations

* fix gate mcp enabled

* fix TOCTOU race BM25 cache version check

* fix encapsulation bypass on registry internals

* safety comment on TickTTL

* added more unit tests

* enhanced logs
2026-03-09 18:21:49 +01:00
Hua Audio c45c5073c0 Feat/nightly align with gorelease and release note (#1285)
* feat/nightly gorelease and changelog

* update to skip docker hub when nightly

* update goreleaser check

* Update with correct lower case for ghcr

* Update with correct syntax

* remove lower case

* Update to prevent gorelease overwrite nightly changelog

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update according to review suggestions

* Update .github/workflows/nightly.yml

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Remove redundant gh download

* Update to delete nightly tag before creating

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-09 17:53:25 +01:00
Meng Zhuo 110fc71349 chore: drop unnessary crypto/rand (#1267) 2026-03-09 22:45:01 +08:00
Meng Zhuo 9a13ed50d0 Merge pull request #1107 from afjcjsbx/fix/deny-reading-binary-files
fix(tool) prevent read huge files in tool
2026-03-09 22:11:27 +08:00
Guoguo 457533b960 docs: update wechat qrcode (#1272) 2026-03-09 21:34:27 +08:00
wenjie e55b3b7a8d feat(web): migrate launcher to modular web frontend/backend and improve management UX (#1275)
* refactor: remove the legacy picoclaw-launcher

* feat: create initial web frontend and backend structure

* feat(packaging): add desktop entry for PicoClaw Launcher (#1062)

- Add .desktop file with Terminal=true, named "PicoClaw Launcher"
- Install to /usr/share/applications/ for app menu visibility
- Add 512x512 PNG icon to /usr/share/icons/hicolor/

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* `make dev`: If you haven't built it before, you need to run `build` first.

* feat(web): comprehensive web UI and backend refactoring
This commit introduces a major overhaul of both the frontend web UI and the Go backend API, transitioning to a highly modular architecture and integrating new core features.
Backend:
- Refactored monolithic API endpoints into domain-specific modules (config, gateway, log, models, pico, session).
- Cleaned up obsolete files (`server.go`, `status.go`, WebSocket handlers) and outdated tests.
- Implemented Gateway process lifecycle management (start/stop/restart) and real-time log streaming.
Frontend:
- Integrated Shadcn UI components to establish a modern, consistent design system.
- Introduced a new application layout featuring a responsive sidebar (`app-sidebar`) and header.
- Implemented internationalization (i18n) with initial support for English and Chinese.
- Restructured API clients, hooks, and Zustand stores into logical domains.
- Added new management pages for Settings, Logs, Models, Providers, and Credentials.
- Upgraded the Pico chat interface with session history management and dynamic model selection.
Build & Config:
- Updated frontend dependencies, Vite configuration, and lockfiles.
- Refined routing setup and overarching application stylesheets.

* feat(web): enhance model management, sorting, and deletion logic
- Implement model sorting in UI (default > configured > unconfigured)
- Prevent deletion of default models in the frontend
- Update backend to clear default settings when a model is deleted
- Add existence validation when setting a default model via API
- Group models in chat UI by type (API Key, OAuth, Local)
- Conditionally display model selector in chat based on configuration status

* refactor(web): refactor chat page into modular components/hooks and update i18n

- split chat route into dedicated chat components (page, composer, empty state, messages, history, model selector)
- extract model/session logic into use-chat-models and use-session-history hooks
- update chat locale keys in en/zh and add empty-state/history-related translations

* refactor(models): refactor models page into modular components and improve UX

- split /models route into dedicated components (page, provider section, card, add/edit sheets, delete dialog)
- add provider grouping/sorting, provider labels/icons, and a no-default hint in the models page
- add "Set as default model" toggle to add/edit flows with safer defaults
- introduce shared form helpers and new UI primitives (field, label, switch)
- update i18n strings (en/zh) for models and gateway header text usage
- apply minor UI polish (models nav icon, separator client directive)

* fix(web): add SPA index fallback for embedded frontend routes

Serve existing static assets as-is, keep /api/* and missing asset paths returning 404, and add tests for SPA fallback behavior on refresh.

* fix(frontend/chat): normalize message timestamp units to prevent invalid far-future dates

* chore: delete TestSPARouteFallsBackToIndex

* feat: update build for web-based launcher (#1186)

- Makefile: add build-launcher target (builds frontend + Go backend)
- GoReleaser: point picoclaw-launcher build to web/backend, add frontend
  build hook, restore winres hook with updated paths
- Restore icon.ico and winres config from main for Windows builds

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* feat(credentials): add multi-provider OAuth credential management

- add backend `/api/oauth/*` endpoints for provider status, browser/device-code/token login, flow query/polling, and logout
- extend API handler with OAuth flow/state tracking and route registration, plus OAuth unit tests
- implement frontend credentials page/components for OpenAI, Anthropic, and Google Antigravity login/logout
- add OAuth API client and `useCredentialsPage` hook, with new EN/ZH i18n strings

* chore: remove placeholder index.html from dist (#1188)

The .gitkeep is sufficient for go:embed to find the dist directory.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix(frontend): polish model and credential UX; remove Providers nav

- remove the Providers item from sidebar navigation and locale keys
- simplify chat composer by dropping attach/voice action buttons
- support ReactNode titles in credential cards and add provider brand icons
- refine sheet header/footer styling and device-code footer button hierarchy
- disable “Set default” when a model is unconfigured or already default

* feat(web): Update  config page (#1173)

* feat(web): Update  config page

* fix(web): useEffect resets editorValue whenever config changes

* fix(web): react-hooks/set-state-in-effect error & pnpm lint #1173

* feat(web): add channel management page for web console (#1190)

* feat(web): add channel management page for web console

Add a complete channel management UI that allows users to configure
messaging channels (Telegram, Discord, Slack, Feishu, etc.) directly
from the web console instead of manually editing config.json.

Backend: GET/PUT/PATCH API endpoints for listing, updating, and
toggling channels with secret field masking.

Frontend: Channel cards grid with enable/disable toggles, per-channel
configuration sheets with dedicated forms for major platforms and a
generic fallback for others.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(web/channels): move channels to own sidebar group and fix sheet padding

- Channels now has its own navigation group instead of being under Services
- Fix edit sheet form content padding (px-1 -> px-4) to match header/footer
- Fix naked return lint error in extractChannelInfo

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix(web): harden channel config updates and resolve frontend lint issues

- validate channel PUT/PATCH updates before saving and return structured validation errors
- require `enabled` in toggle requests to avoid silent false defaults
- support editing `allow_origins` in the generic channel form and parse string/array inputs on backend
- replace channel form `any` usage with `ChannelConfig` (`Record<string, unknown>`) and add safe value helpers
- add i18n strings for allow-origins fields and apply related frontend formatting cleanups

* fix(frontend): prevent false "Invalid JSON" errors in config editor

* feat: add startup readiness checks and propagate start availability to UI

- add gateway precondition validation for default model and credentials
- auto-start gateway on backend boot when conditions are met
- include gateway_start_allowed and gateway_start_reason in status updates
- prevent frontend start actions when gateway cannot be started

* feat(web): revamp channel config UX with catalog-based routing

- replace legacy channel management endpoints with a backend channel catalog API
- switch frontend channel updates to PATCH /api/config and per-channel config pages
- add dynamic channel items in the sidebar with support for expand/collapse
- migrate /channels to nested routes (/channels/$name) and remove old card/sheet flow
- improve channel forms with clearer hints, required/error states, and reusable switch cards
- fix Discord mention-only toggle to read/write group_trigger.mention_only

* refactor(frontend): move shared-form to components and unify default-model switch with SwitchCardField

* fix(frontend): improve model form validation and unify secret placeholder handling

- block duplicate model aliases when adding a model (with localized error messages)
- share masked secret placeholder logic across model and channel forms
- refresh gateway state after setting the default model
- apply minor UI cleanup to provider icon rendering

* feat(web): add visual system config and launcher/autostart controls

- add launcher config model and persistence (`launcher-config.json`) for port/public/CIDR settings
- add system APIs for launch-at-login and launcher parameters
- apply CIDR-based access-control middleware to backend HTTP routes
- split config routing into visual config and raw JSON config pages
- add frontend system API client and visual config sections for runtime/devices/launcher
- expand i18n strings (en/zh) for new config UI
- improve sidebar active matching and session ID generation fallback

* refactor(frontend): remove i18n fallback strings and drop providers route

- Replace `t(key, defaultValue)` calls with key-only translations across UI pages
- Clean up locale files by pruning unused keys and adding missing shared keys
- Remove the obsolete `/providers` page and update generated route tree

* fix(backend): correct gateway status detection on Windows

* fix(repo): keep web backend dist placeholder tracked

---------

Co-authored-by: Guoguo <16666742+imguoguo@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Dihubopen <dihubcn@gmail.com>
Co-authored-by: Dihubopen <130813726+Dihubopen@users.noreply.github.com>
2026-03-09 19:42:03 +08:00
taorye ead22368bd Enhance model selection and add footer navigation instructions (#1271)
* fix(tui): fix model selection and enforce unique model_name, also fix model form button highlight

* feat(tui): add footer view with navigation instructions and update menu structure

* fix(tui): update model selection labels for clarity and consistency

* refactor(tui): remove unused rootChannelDescription function

* refactor(tui): simplify rootModelDescription and remove unused 'q' event handling in channel menu

* fix(tui): keep selected model name updated

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-09 19:22:16 +08:00
Guoguo f505f009df feat(release): add macOS binary notarization via goreleaser (#1274)
Add notarize.macos section to .goreleaser.yaml using anchore/quill
for cross-platform code signing and Apple notarization of darwin
binaries. Covers all three build targets (picoclaw, picoclaw-launcher,
picoclaw-launcher-tui).

Notarization is gated on MACOS_SIGN_P12 being set, so releases
without the secrets configured will skip this step gracefully.

Required GitHub secrets:
- MACOS_SIGN_P12: base64-encoded .p12 certificate
- MACOS_SIGN_PASSWORD: certificate password
- MACOS_NOTARY_ISSUER_ID: App Store Connect issuer UUID
- MACOS_NOTARY_KEY_ID: App Store Connect API key ID
- MACOS_NOTARY_KEY: base64-encoded .p8 API key

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:50:11 +08:00
lxowalle abafa3c2aa * add minimax provider (#1273) 2026-03-09 18:43:58 +08:00
afjcjsbx f89c9673cb sync sendmessage function 2026-03-09 11:38:23 +01:00
afjcjsbx 584564af63 fix lint 2026-03-09 11:02:31 +01:00
lxowalle aaf99d7a30 feat: add /clear command to clear chat history (#1266)
* * add clear command to clear chat history

* check nil

* * update comment
2026-03-09 16:39:33 +08:00
afjcjsbx ff54128ab4 refined code 2026-03-09 09:32:21 +01:00
Guoguo 82773fcd42 feat(release): add linux/s390x and linux/mipsle to goreleaser builds (#1265)
Add s390x and mipsle (softfloat) architecture targets to all three
goreleaser build entries (picoclaw, picoclaw-launcher, picoclaw-launcher-tui).

Go only supports these architectures on Linux, so no additional
ignore entries are needed — goreleaser skips unsupported OS/arch
combinations automatically.

mipsle uses GOMIPS=softfloat for maximum compatibility with
FPU-less MIPS cores (e.g. MT7620 with MIPS 24KEc).

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 15:41:13 +08:00
lxowalle ba4b702675 fix: retryLLM return empty (#1264) 2026-03-09 14:39:26 +08:00
lxowalle 2c3952b8c0 Fix: improve history compression with retry logic and multi-byte character support (#1167)
* first commit

* Reduce retry wait time to 100ms

* * Add incremental delay and modify the context truncation logic
2026-03-09 13:41:41 +08:00
zhangxiaoyu.york a521a49162 fix:fix cmd example (#1166) 2026-03-09 05:49:20 +01:00
Hua Audio ad9d5a3d19 feat(ci/cd) Add nightly build workflow (#1226)
* Feat/nightly build (#2)

* feat: add nightly build workflow

- Add nightly.yml GitHub Actions workflow for daily builds
- Schedule: daily at 2 AM UTC
- Build using GoReleaser snapshot mode
- Upload artifacts to GitHub Releases as pre-release
- Create nightly Docker tags
- Clean old nightly releases (keep last 30)
- Add research documentation for nightly build setup

* fix: correct yaml syntax error in nightly workflow

* feat: restore nightly build workflow

---------

Co-authored-by: Hua <zhangmikoto@gmail.com>

* fix: use explicit tags instead of metadata-action

* Refactor nightly build workflow for clarity and efficiency

Refactor nightly build workflow to improve clarity and efficiency. Update job names, streamline version generation, and enhance Docker build process.

* remove unused research docs

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* incorporate review suggestions

* Update .github/workflows/nightly.yml

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update .github/workflows/nightly.yml

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* correct release naming

* update base version regular

* Update .github/workflows/nightly.yml

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update .github/workflows/nightly.yml

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update docker metadata and pass version as env

* Update release note

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update .github/workflows/nightly.yml

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update prerelease flag and rolling release

* Update to set provenance to false

---------

Co-authored-by: Hua <zhangmikoto@gmail.com>
Co-authored-by: zhangmikoto <i@electromaster.me>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-09 05:37:31 +01:00
afjcjsbx c69095457f Merge remote-tracking branch 'refs/remotes/origin/main' into fix/deny-reading-binary-files 2026-03-09 00:30:37 +01:00
afjcjsbx 536e26aff1 Removed the old heavy logic 2026-03-08 18:22:15 +01:00
afjcjsbx f87ab99833 fix empty strings on failed transcription 2026-03-08 18:00:02 +01:00
afjcjsbx f219ca1263 telegram reply only on first message 2026-03-08 17:57:43 +01:00
afjcjsbx 3b5d04956e fixed double message on slack thread 2026-03-08 17:41:53 +01:00
美電球 7ea7bb0717 Merge pull request #1171 from mutezebra/feat/feishu-random-emoji-v2
feat(feishu): add random reaction emoji config
2026-03-08 23:47:45 +08:00
美電球 b767ca9c3c Merge pull request #1220 from horsley/feat/matrix-channel-support
feat: add Matrix channel support
2026-03-08 22:58:16 +08:00
horsley fb2bfe4b3c fix(matrix): satisfy golines in mention regex test 2026-03-08 10:53:45 +00:00
mutezebra 08d668c165 chore(config): gofmt 格式化 FeishuConfig 字段对齐 2026-03-08 17:32:24 +08:00
mutezebra 6aa1d02fff fix(feishu): 用 crypto/rand 选择随机表情并修正示例配置 2026-03-08 17:30:50 +08:00
horsley 6e16ac7f68 fix(matrix): bound room cache and align temp media dir 2026-03-08 09:23:02 +00:00
horsley cd955d730b fix(ci): resolve linter and security check failures 2026-03-08 08:06:28 +00:00
mutezebra b15cff1266 Merge upstream/main and resolve conflicts in .env.example 2026-03-08 15:32:11 +08:00
Meng Zhuo 81dfdf5f45 Merge pull request #1100 from zihan987/main
feat: add Vivgrid provider support
2026-03-08 11:03:13 +08:00
horsley 64b99b34bb fix(matrix): improve group mention detection 2026-03-07 18:05:09 +00:00
afjcjsbx 5b1f11aaf6 resolve conflicts 2026-03-07 18:56:38 +01:00
afjcjsbx 424c40e98b Merge remote-tracking branch 'origin/main' into feat/echo-voice-audio-transcription
# Conflicts:
#	pkg/channels/telegram/telegram.go
2026-03-07 18:55:32 +01:00
afjcjsbx 2effc2b4bd slack reply message with audio transcription 2026-03-07 18:47:22 +01:00
horsley a66eac42c4 feat: add Matrix channel support 2026-03-07 17:44:24 +00:00
zihan987 4df4138663 Fix Vivgrid docs and inference logic 2026-03-07 09:20:56 -08:00
美電球 4768edc67b Merge pull request #1215 from yinwm/refactor/agent
docs: add agent refactor working notes
2026-03-08 00:25:47 +08:00
yinwm 726a87b70f docs: add agent refactor working notes 2026-03-08 00:22:31 +08:00
美電球 826f92cf53 Merge pull request #935 from putueddy/feat/telegram-chunking
feat: add message chunking in Telegram Send method
2026-03-07 23:47:06 +08:00
afjcjsbx 73243c9014 fix lint 2026-03-07 16:45:21 +01:00
afjcjsbx a0591f0c08 unit test placeholder logic 2026-03-07 16:40:26 +01:00
afjcjsbx 68bdf66168 fix lint 2026-03-07 16:24:49 +01:00
afjcjsbx 48d8c8738d discord reply message on transcript echo 2026-03-07 16:18:53 +01:00
I Putu Eddy Irawan f07dbd1db2 fix: remove redundant SplitMessage in Send() per review feedback
WithMaxMessageLength(4000) already ensures msg.Content ≤ 4000 chars
before reaching Send(), making the SplitMessage call redundant.
The HTML expansion safety net (re-split when >4096 after conversion)
is still preserved.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 22:01:04 +07:00
afjcjsbx 0c117a073f feat(channel): echo voice audio transcription 2026-03-07 15:49:33 +01:00
Hua Audio 66e6fb6c79 feat(agent) fallback to reasoning content (#992) 2026-03-07 14:17:33 +01:00
Meng Zhuo aeabbcf2e8 Merge pull request #1138 from amirmamaghani/feat/irc-channel
feat(channels): add IRC channel integration
2026-03-07 20:25:26 +08:00
Mauro a32a4e007d Merge pull request #1143 from blib/bug/parallel-execution
fix: background task results silently dropped
2026-03-07 11:09:19 +01:00
Mauro 440d665baa Merge pull request #1075 from qs3c/fix/1068-html-response-error
fix(openai_compat): clarify HTML response parse errors
2026-03-07 09:56:50 +01:00
afjcjsbx 569d509de5 no-truncate param 2026-03-07 09:48:44 +01:00
amagi 53cba73283 fix: resolve openai compat lint issues 2026-03-07 16:12:23 +08:00
amagi 6eaa49f7ab fix: improve openai compat HTML response handling 2026-03-07 15:50:08 +08:00
afjcjsbx 674f00ec63 set offset and length in read_file tool 2026-03-07 00:33:27 +01:00
Guoguo b8f8e3f25f docs: update wechat qrcode (#1192) 2026-03-07 07:31:23 +08:00
dependabot[bot] 91a633c009 chore(deps): bump filippo.io/edwards25519 from 1.1.0 to 1.1.1 (#1200)
Bumps [filippo.io/edwards25519](https://github.com/FiloSottile/edwards25519) from 1.1.0 to 1.1.1.
- [Commits](https://github.com/FiloSottile/edwards25519/compare/v1.1.0...v1.1.1)

---
updated-dependencies:
- dependency-name: filippo.io/edwards25519
  dependency-version: 1.1.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-07 09:26:52 +11:00
dependabot[bot] 78aa45f107 chore(deps): bump github.com/modelcontextprotocol/go-sdk (#1199)
Bumps [github.com/modelcontextprotocol/go-sdk](https://github.com/modelcontextprotocol/go-sdk) from 1.3.0 to 1.3.1.
- [Release notes](https://github.com/modelcontextprotocol/go-sdk/releases)
- [Commits](https://github.com/modelcontextprotocol/go-sdk/compare/v1.3.0...v1.3.1)

---
updated-dependencies:
- dependency-name: github.com/modelcontextprotocol/go-sdk
  dependency-version: 1.3.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-07 09:26:30 +11:00
Amir Mamaghani 94b6b656c2 docs: add user, real_name, and request_caps to IRC example config 2026-03-06 20:21:53 +01:00
Amir Mamaghani a89ba06cb8 fix: address review feedback from @mengzhuo
- Add separate User and RealName config fields (fall back to Nick)
- Make RequestCaps configurable (defaults to server-time, message-tags)
- Refactor isBotMentioned into nickMentionedAt returning position;
  stripBotMention now uses nickMentionedAt internally
- Replace custom isAlphanumeric with unicode.IsLetter/unicode.IsDigit
- Update tests for new nickMentionedAt function
2026-03-06 20:09:37 +01:00
fishtrees 1945436dd4 feat(cron): add execution lifecycle logging (#1185)
- Log job start with name, id, schedule kind, and channel
- Log job completion with duration and next run time
- Log job errors with duration and error message
- Helps diagnose scheduler stalls and connection issues
2026-03-06 20:46:52 +08:00
zihan987 e6f5467711 Fix golines for vivgrid case 2026-03-06 04:20:22 -08:00
Qiaochu Hu 7f6d95c026 fix: handle zero values in cron schedule type assertions (#1147)
Fixes #1126

Go type assertions return true for zero values, which caused recurring
cron jobs (every_seconds/cron_expr) to silently become one-time 'at' tasks
when LLMs filled unused optional parameters with default values (0).

Changes:
- Add validity checks after type assertions: atSeconds > 0, everySeconds > 0, cronExpr != ""
- This ensures zero values are treated as 'not set' rather than valid schedule values
- Recurring tasks like "remind me every 2 hours" now correctly create recurring jobs
2026-03-06 20:11:08 +08:00
BallerIsLeet 23abbb67ea feat(auth): add Anthropic OAuth setup-token login (#926)
* feat(auth): add Anthropic OAuth setup-token login flow

Add support for Anthropic's OAuth-based setup tokens (sk-ant-oat01-*)
as an alternative to API keys. This includes:

- New `--setup-token` flag on `auth login` command
- Interactive login menu for Anthropic (setup token vs API key)
- Setup token validation and credential storage with oauth auth method
- Usage endpoint integration to show 5h/7d utilization in `auth status`
- Streaming support for OAuth tokens (required by Anthropic API)
- Model ID normalization (dots to hyphens) for API compatibility
- Remove .env.example (secrets should not be templated)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(auth):  update related functionality

* refactor(auth): organize constants and improve header casing in requests fo CI

* fix(auth): fix golint again

* fix(auth): handle nil arguments in tool calls for buildParams function

---------

Co-authored-by: Baller <sharonms3377@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 19:58:23 +08:00
甜航 c3af1543db feat(build): add MIPS32 LE (mipsle) cross-compilation support (#1051) 2026-03-06 19:54:56 +08:00
shikihane c368b5b359 feat(feishu,tools): add outbound media delivery via send_file tool (#1156)
* feat(feishu): implement SendMedia and add send_file tool

Add outbound media support for the Feishu channel so the agent can send
images and files to users via the MediaStore pipeline.

Feishu channel:
- SendMedia dispatches media parts as image or file uploads
- sendImage uploads via Image.Create then sends image message
- sendFile uploads via File.Create then sends file message
- feishuFileType maps extensions to Feishu file_type values

send_file tool:
- New tool lets the LLM send a local file to the current chat
- Validates path, registers file in MediaStore, returns media ref
- Agent loop wires tool registration, MediaStore propagation, and
  context updates

Tested on Radxa Cubie A7A (arm64) with Feishu websocket channel.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(agent): publish outbound media regardless of SendResponse flag

The SendResponse flag controls whether the agent loop publishes the
final text response (callers that publish it themselves set this to
false). However, the media publish path was also gated behind this
flag, which meant tool-produced media was silently dropped for normal
channel messages.

Media should be published immediately when a tool returns media refs,
independent of how the text response is delivered.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(tools): use magic-bytes MIME detection and add file size limit to send_file

- Replace hardcoded extension-to-MIME map with h2non/filetype (magic
  bytes) + mime.TypeByExtension fallback, consistent with the vision
  pipeline in resolveMediaRefs
- Add configurable max file size check (defaults to config.DefaultMaxMediaSize,
  20 MB) to prevent oversized uploads
- Add tests for magic-bytes detection, extension fallback, size limit,
  and default max size

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(agent): add ForEachTool to AgentRegistry for cross-agent tool lookup

Extract the pattern of iterating agents to find a named tool into
AgentRegistry.ForEachTool, simplifying SetMediaStore propagation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(agent,tools): adapt send_file to ctx-based channel injection after upstream refactor

Replace ContextualTool interface (removed upstream) with direct ctx
reading in SendFileTool.Execute, using ToolChannel/ToolChatID helpers.
Remove updateToolContexts which is no longer needed since ExecuteWithContext
already injects channel/chatID into ctx for all tools.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(tools): support toggling send_file tool via config

Add SendFileConfig with Enabled field to ToolsConfig, defaulting to
true. Wrap send_file tool registration in loop.go with the config
check, consistent with the pattern used by other tools.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 19:42:52 +08:00
Meng Zhuo 3738040987 Merge pull request #1127 from mosir/fix/reload-config-selfkill-guard
fix(exec): block kill command pattern in safety guard
2026-03-06 19:06:03 +08:00
wangyanfu2 ac37d6b626 fix: disable closing custom model dialog by clicking backdrop (#1180) 2026-03-06 11:41:22 +01:00
mosir aa2d6b39f5 refactor(tools): remove redundant kill -9 pattern 2026-03-06 18:34:46 +08:00
mosir d0f627697e Merge branch 'sipeed:main' into fix/reload-config-selfkill-guard 2026-03-06 18:27:46 +08:00
Ming b716b8a053 feat(commands): centralized command registry with sub-command routing (#959)
* feat(commands): Session management [Phase 1/2] command centralization and registration

* docs: add design for command registry post-review fixes

Documents the architecture decisions for fixing 5 Important issues
from code review: SubCommand pattern, Deps struct, command-group files,
Executor caching, and Telegram registration dedup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(commands): add SubCommand type and EffectiveUsage method

Introduce SubCommand struct for declaring sub-commands structurally
within a parent command Definition. The EffectiveUsage() method
auto-generates usage strings from sub-command names and args,
preventing drift between help text and actual handler behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(commands): add Deps struct and secondToken helper, remove dead contains()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(commands): add sub-command routing to Executor

Uses Registry.Lookup for O(1) command dispatch instead of iterating
all definitions. Definitions with SubCommands are routed to matching
sub-command handlers. Missing or unknown sub-commands reply with
auto-generated usage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(commands): split into command-group files with Deps injection

Extract show/list/start/help into individual cmd_*.go files.
Replace config.Config parameter with Deps struct for runtime data.
Restore /show agents and /list agents sub-commands.
Use EffectiveUsage for auto-generated help text.
Bridge external callers (agent/loop.go, telegram.go) with Deps wrapper
until Task 5 fully wires the Deps fields.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* perf(commands): cache Executor in AgentLoop, wire Deps with runtime callbacks

Create Executor once in NewAgentLoop instead of per-message. Deps
closures capture AgentLoop pointer for late-bound access to
channelManager and runtime agent model.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(telegram): remove duplicate initBotCommands, keep async startCommandRegistration only

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore(commands): restore Outcome comments and annotate Deps.Config

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(commands): consolidate /switch into commands package, fix ! prefix

Move /switch model and /switch channel handling from inline loop.go
logic into cmd_switch.go using the SubCommand + Deps pattern. This
removes the OutcomePassthrough branch in handleCommand entirely.

Also replace the hardcoded "/" prefix check with commands.HasCommandPrefix
so that "!" prefixed commands are correctly routed to the Executor.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: add docs/plans to .gitignore and untrack existing files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(commands): address code review findings

- Remove dead ExecuteResult.Reply field and unused branch in loop.go
- Extract shared agentsHandler for /show agents and /list agents
- Remove redundant firstToken/secondToken (use nthToken instead)
- Simplify Telegram startup: pass BuiltinDefinitions directly
- Centralize req.Reply nil guard in executeDefinition
- Extract unavailableMsg constant (was duplicated 5 times)
- Remove unused MessageID from Request
- Remove stale "reserved for Phase 2" comment on Deps.Config

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(commands): replace Deps with per-request Runtime

Separate stateless Registry (cached on AgentLoop) from per-request
Runtime (passed to handlers at execution time). This enables future
session management features to inject per-request context without
modifying the command registry.

- Rename Deps → Runtime, move to runtime.go
- Change Handler signature: func(ctx, req) error → func(ctx, req, rt *Runtime) error
- NewExecutor now takes (registry, runtime) — executor is created per-request
- BuiltinDefinitions() no longer takes parameters (stateless)
- AgentLoop caches cmdRegistry, builds Runtime via buildRuntime()
- Update all cmd_*.go handlers and tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style: fix gci import grouping and godoc formatting

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(onboard): skip legacy AGENT.md when copying embedded workspace templates

The workspace/ directory contains both AGENT.md (legacy) and AGENTS.md
(current). copyEmbeddedToTarget was copying both, causing the test
TestCopyEmbeddedToTargetUsesAgentsMarkdown to fail. Skip AGENT.md
during the walk to match the expected behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(agent): address self-review comments on loop.go

- Move cmdRegistry init into struct literal (review comment #11)
- Rename buildRuntime → buildCommandsRuntime for clarity (review comment #12)
- Add comment to default switch case explaining passthrough (review comment #13)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(commands): address code review findings on naming and correctness

- Rename dispatcher.go → request.go (no Dispatcher type remains)
- Rename cmd_agents.go → handler_agents.go (shared handler, not a top-level command)
- Add modelMu to protect AgentInstance.Model writes in SwitchModel
- Add ListDefinitions to Runtime so /help uses registry instead of BuiltinDefinitions()
- Fix SwitchChannel message: validation-only callback should not say "Switched"
- Propagate Reply errors in executor instead of discarding with _ =
- Add HasCommandPrefix unit test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(onboard): extract legacy filename to constant

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(agent): handle commands before route error check

Move handleCommand() before the routeErr gate so global commands
(/help, /show, /switch) remain available even when routing fails.
Context-dependent commands that need a routed agent will report
"unavailable" through their nil-Runtime guards.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* revert: remove unnecessary AGENT.md skip in onboard

Reverts 02d0c04 and 74deae1. The test failure was caused by a local
leftover workspace/AGENT.md file (gitignored but embedded by go:embed).
Deleting the local file fixes the root cause; the code-level skip was
never needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: executeDefinition Unknown option

* fix(agent): use routed agent for model commands, restore Telegram command diff

- Remove modelMu: message processing is serial, no concurrent writes
- Pass routed agent to handleCommand/buildCommandsRuntime instead of
  always using default agent
- GetModelInfo/SwitchModel are nil when agent is nil (route failed),
  handlers reply "unavailable"
- Restore GetMyCommands + slices.Equal check before SetMyCommands to
  avoid unnecessary Telegram API calls on restart

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(commands): remove unintended config mutation in SwitchModel

SwitchModel should only update the routed agent's runtime Model field.
Writing to cfg.Agents.Defaults.ModelName was a behavioral change that
corrupts the default agent config when switching a non-default agent.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(commands): move /switch channel to /check channel

/switch channel only validates availability, not actually switching.
Rename to /check channel to match actual behavior. /switch channel
now shows a redirect message pointing users to the new command.

Addresses review feedback from yinwm on PR #959.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 17:31:40 +08:00
Guoguo c3c293297d feat: add upload_tos toggle to release workflow (#1183)
Add a boolean input (default: true) to control whether release artifacts
are uploaded to Volcengine TOS.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 16:27:43 +08:00
Yajun Yao 7df7e0495c fix deepseek-chat bug (#1066)
Co-authored-by: FantasticCode2019 <1443996278@qq.com>
2026-03-06 16:04:31 +08:00
zihan987 a2f63e4207 Fix HasProvidersConfig 2026-03-06 00:03:10 -08:00
zihan987 7308f0621b Merge upstream main 2026-03-05 23:58:59 -08:00
Meng Zhuo 9b1e73de9c Merge pull request #994 from is-Xiaoen/feat/model-routing
feat(routing): intelligent model routing based on structural complexity scoring
2026-03-06 15:47:53 +08:00
美電球 4d965f2c81 Merge pull request #1047 from AaronJny/feat/discord-reply-context
feat(discord): support referenced messages and resolve channel/link references
2026-03-06 15:03:29 +08:00
xiaoen b84adacc2f fix(routing): address review feedback on CJK estimation and observability
1. CJK token estimation: replace flat rune_count/3 with script-aware
   counting — CJK runes (U+2E80–U+9FFF, U+F900–U+FAFF, U+AC00–U+D7AF)
   count as 1 token each, non-CJK runes at /4. This fixes a 3x
   underestimate for Chinese/Japanese/Korean text that could incorrectly
   route complex CJK messages to the light model.

2. Routing observability: SelectModel now returns the computed score as
   a third value. selectCandidates logs the score on both paths — Info
   level for light model selection, Debug level for primary model
   selection.

3. Added tests: TestExtractFeatures_TokenEstimate_Mixed (CJK+ASCII mix),
   TestRouter_SelectModel_ReturnsScore.

Addresses review feedback from @mingmxren.
2026-03-06 13:10:20 +08:00
mutezebra 109a382507 chore(feishu): add random_reaction_emoji to example config 2026-03-06 12:53:48 +08:00
mutezebra 92a0db4993 chore(feishu): document reaction emoji option and promote filetype dep
Add an info log for `RandomReactionEmoji` configuration.
2026-03-06 12:53:48 +08:00
mutezebra 9f017d077e feat(feishu): add random reaction emoji config
- Add random_reaction_emoji config for Feishu channel
- Update .env.example with new env vars
- Update documentation (README.zh.md)
2026-03-06 12:53:47 +08:00
xiaoen 04ddb6b472 chore: remove accidentally committed local files 2026-03-06 12:20:21 +08:00
Guoguo 46201fb679 feat: upload release artifacts to Volcengine TOS (#1164)
Add reusable workflow (upload-tos.yml) to upload release archives to
Volcengine TOS bucket. Supports both workflow_call from release pipeline
and manual workflow_dispatch trigger. Uploads to versioned ({tag}/) and
latest/ directories.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:47:29 +08:00
Meng Zhuo f600829158 Merge pull request #1157 from wangyanfu2/fix-config-shell-command-exec-timeout
fix(tools): make exec tool timeout configurable via config
2026-03-06 11:42:26 +08:00
xiaoen e433bb8b7f merge: resolve conflicts with upstream/main
Upstream added ThinkingLevel, SummarizeMessageThreshold,
SummarizeTokenPercent, MaxMediaSize, and maybeSummarize.
Our branch added Router, LightCandidates, and selectCandidates.
Both sets of changes are kept. Dead updateToolContexts removed
(upstream deleted it; no callers exist).
2026-03-06 11:27:48 +08:00
wangyanfu2 65e1434e1b style: fix gofmt formatting in ExecConfig struct
Remove extra trailing whitespace between struct tag and inline comment
on TimeoutSeconds field to comply with gofmt formatting rules.
2026-03-06 11:19:25 +08:00
Meng Zhuo 8581d46eaa Merge pull request #1142 from mattn/fix/handle-io-readall-errors
fix: handle ignored io.ReadAll errors across codebase
2026-03-06 10:25:19 +08:00
Meng Zhuo 651cb2ebda Merge pull request #1155 from keithy/feature/picoclaw-home-env
feat: honor PICOCLAW_HOME env var for config, auth, and workspace paths
2026-03-06 10:13:14 +08:00
wangyanfu2 e0d2be35c2 fix(tools): make exec tool timeout configurable via config
Add TimeoutSeconds field to ExecConfig so the shell command execution
timeout can be configured instead of being hardcoded to 60s.

- Add TimeoutSeconds int field to ExecConfig in pkg/config/config.go
  with json/env tags (PICOCLAW_TOOLS_EXEC_TIMEOUT_SECONDS)
- Set default value of 60s in DefaultConfig() in pkg/config/defaults.go
- Read TimeoutSeconds from config in NewExecToolWithConfig() in
  pkg/tools/shell.go; falls back to 60s when value is 0 or unset
2026-03-06 09:51:58 +08:00
Keith Patrick 51e8479f99 feat: honor PICOCLAW_HOME env var for config, auth, and workspace paths 2026-03-05 22:08:37 +00:00
Mauro 23da4503c1 Merge pull request #1145 from Esubaalew/fix/upstream-skills-global-toggle
fix(agent): respect global skills toggle for skill tools
2026-03-05 22:17:32 +01:00
Mauro a3648aee19 Merge pull request #534 from truongvinht/feat/searxng
feat: Add SearXNG as web search provider
2026-03-05 21:59:26 +01:00
Truong Vinh Tran 4599064f2a Merge upstream/main into feat/searxng
Resolved conflicts in 3 files:
- config/config.example.json: keep both searxng and glm_search configs
- pkg/agent/loop.go: adopt upstream's IsToolEnabled guard + keep searxng fields
- pkg/config/config.go: adopt upstream's ToolConfig embed + keep SearXNG field

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 21:36:05 +01:00
Amir Mamaghani c10959b645 test(irc): add unit tests for IRC channel
Test NewIRCChannel validation, extractHost, isBotMentioned,
stripBotMention, and isAlphanumeric helper functions.
2026-03-05 19:23:40 +01:00
amagi c1a3876f7d fix: improve error handling for non-JSON responses by checking content type and using a streaming JSON parser. 2026-03-06 01:51:24 +08:00
Amir Mamaghani 1604582a41 fix: resolve gci lint errors in IRC channel files
Sort irc import alphabetically in helpers.go and fix struct field
alignment in irc.go to satisfy golangci-lint gci formatter.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 16:03:10 +01:00
Mauro 445c32af4f Merge pull request #1135 from qs3c/fix/1134-clawhub-429-retry
fix(skills): retry ClawHub requests on HTTP 429
2026-03-05 14:25:25 +01:00
Yasuhiro Matsumoto 03d6ad420f fix: resolve merge conflicts in wecom error handling
Combine both shadow variable fix (readErr) and proper error
classification (ClassifySendError) in wecom app and bot channels.
2026-03-05 22:01:32 +09:00
Yasuhiro Matsumoto b878272962 fix: resolve govet shadow and golines lint errors in wecom channels 2026-03-05 21:54:13 +09:00
esubaalew f046ba59e8 fix(agent): respect global skills toggle for skill tools 2026-03-05 15:40:06 +03:00
Boris Bliznioukov 00ad6be7ea Update pkg/agent/loop.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-05 13:30:24 +01:00
mattn 42a32fbf3b Update pkg/channels/wecom/app.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-05 21:20:51 +09:00
mattn ee2ebc8bf3 Update pkg/channels/wecom/app.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-05 21:20:40 +09:00
mattn ca4e44bd0f Update pkg/channels/wecom/bot.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-05 21:20:31 +09:00
mattn 8d2f2d67b2 Update pkg/channels/line/line.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-05 21:20:20 +09:00
Boris Bliznioukov 968fff07b9 fix: background task results silently dropped
Signed-off-by: Boris Bliznioukov <blib@mail.com>
2026-03-05 13:07:17 +01:00
Yasuhiro Matsumoto 943385105f fix: handle ignored io.ReadAll errors across codebase
io.ReadAll errors were silently discarded with `body, _ := io.ReadAll(...)`,
which could cause empty or partial data to be used for JSON unmarshaling
or error messages. This adds proper error checks for all instances.
2026-03-05 20:56:38 +09:00
qs3c 9216cd14b5 fix(openai_compat): handle html error bodies and reduce allocations 2026-03-05 19:42:58 +08:00
qs3c 536e9ac9de refactor(skills): reuse shared HTTP retry helper 2026-03-05 19:10:36 +08:00
Amir Mamaghani 40b7b6ee4b feat(channels): add IRC channel integration
Add IRC as a new channel for picoclaw, supporting server connections,
channel joins, DMs, mention-based group triggers, and IRCv3 typing
indicators. Uses ergochat/irc-go for connection management with SASL,
NickServ, and automatic reconnection support.

Closes #1137
2026-03-05 11:48:17 +01:00
Mauro 74b5af9e53 Merge pull request #1105 from cornjosh/fix/registry-flag-usage
fix(skills): use --registry flag value as registry name
2026-03-05 11:42:13 +01:00
cornjosh ab120af649 fix(skills): use --registry flag value as registry name
The --registry flag value was previously ignored and only used as a
switch. Now the flag value is properly used as the registry name.

Fixes #1104

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-03-05 17:10:04 +08:00
zihan987 91f52c4586 Resolve merge conflicts 2026-03-04 23:50:58 -08:00
qs3c 7a2fdc24dc fix(skills): retry ClawHub requests on 429 2026-03-05 15:00:06 +08:00
lxowalle 6f5930624b Feat/add tool enable or disable configuration (#1071)
* Add tools enable or diable config
2026-03-05 14:53:26 +08:00
zihan987 d1cf680657 Resolve merge conflicts 2026-03-04 22:53:17 -08:00
lxowalle 10ad9e83f9 docs: update license (#1131) 2026-03-05 14:15:16 +08:00
mosir 0f568aceb7 Merge branch 'sipeed:main' into fix/reload-config-selfkill-guard 2026-03-05 13:58:44 +08:00
美電球 464ae1846e Merge pull request #1106 from afjcjsbx/fix/prevent-audio-as-image-url
fix(provider) prevent audio as image url
2026-03-05 12:55:46 +08:00
mosir 5c599d2dac fix(exec): block kill command pattern in safety guard 2026-03-05 12:45:53 +08:00
Mauro 41bb78f593 feat(ci) govulncheck (#1086)
* feat(ci) govulncheck

* feat(ci) disable persist-credentials
2026-03-05 11:13:11 +08:00
Boris Bliznioukov aef1e8e8c4 fix: eliminate data races on shared tool instances (#1080)
* fix: eliminate data races on shared tool instances

Signed-off-by: Boris Bliznioukov <blib@mail.com>

* fix: remove unused indirect dependency on github.com/gdamore/tcell/v2

Signed-off-by: Boris Bliznioukov <blib@mail.com>

* fix: reviewer comments improve context handling for tool execution and ensure defaults for non-conversation callers

Signed-off-by: Boris Bliznioukov <blib@mail.com>

---------

Signed-off-by: Boris Bliznioukov <blib@mail.com>
2026-03-05 09:57:33 +08:00
Larry Koo 204038ec60 feat: add extended thinking support for Anthropic models (#1076)
* feat: add extended thinking support for Anthropic models

Support configurable thinking levels (off/low/medium/high/xhigh/adaptive)
via `agents.defaults.thinking_level` config field.

- "adaptive": uses Anthropic's adaptive thinking API (Claude 4.6+)
- "low/medium/high/xhigh": uses budget_tokens (all thinking-capable models)
- "off": disables thinking (default)

API constraints handled:
- Temperature cleared when thinking is enabled
- budget_tokens clamped to max_tokens-1
- Thinking response blocks parsed into Reasoning field

Relates to #645, #966

* fix: address PR review feedback for thinking support

- Add ThinkingCapable interface for provider capability detection
- Warn when thinking_level is set but provider doesn't support it
- Warn when temperature is cleared due to thinking enabled
- Adjust budget values per Anthropic best practices (medium=16K, xhigh=64K)
- Add budget clamp warning and 80% threshold warning
- Add parseResponse thinking block tests
- Add thinking_level field to config.example.json

* refactor: move ThinkingLevel from AgentDefaults to ModelConfig

Thinking is a model-level capability, not a global agent property.
Per-model config avoids silent ignoring on non-Anthropic providers
and eliminates spurious warning logs in multi-provider setups.

Addresses PR #1076 review feedback from @yinwm.
2026-03-05 09:51:18 +08:00
Meng Zhuo 325af2163b Merge pull request #844 from avianion/feat/add-avian-provider
feat: add Avian as a named LLM provider
2026-03-05 09:45:06 +08:00
afjcjsbx 47d7b9b04c resolve makezero linter error 2026-03-04 23:05:52 +01:00
afjcjsbx 1b990d9acd fix lint 2026-03-04 22:59:58 +01:00
afjcjsbx c87375588e prevent read binary file in tool 2026-03-04 22:39:08 +01:00
Truong Vinh Tran de0f15d548 style: fix golines struct tag alignment in SearXNGConfig
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:48:36 +01:00
Truong Vinh Tran e4daab8b09 Merge upstream/main into feat/searxng
Resolve merge conflicts to keep both SearXNG and GLM Search
providers. Updated search priority order to:
Perplexity > Brave > SearXNG > Tavily > DuckDuckGo > GLM Search

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:42:03 +01:00
Kyle D 0c97cb30d8 fix: update provider count in migration test to include Avian
The TestConvertProvidersToModelList_AllProviders test expected 19
providers but adding Avian brings the total to 20.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 20:14:58 +00:00
afjcjsbx b9ee9b33f5 prevent audio as image url 2026-03-04 19:34:08 +01:00
zihan987 0c17c075da Merge remote-tracking branch 'origin_picoclaw/main' 2026-03-04 09:58:20 -08:00
Mauro 3e5b849984 Merge pull request #947 from dim/fix/transcription
Fix voice transcription
2026-03-04 18:37:24 +01:00
zihan987 ea0b634b3b add Vivgrid config 2026-03-04 09:19:03 -08:00
amagi a305c0a479 fix(openai_compat): avoid predeclared identifier in preview 2026-03-04 23:57:26 +08:00
I Putu Eddy Irawan 11017ac7ba merge: resolve conflicts with upstream/main
Accept upstream versions for all non-Telegram files to keep PR
scope focused on Telegram message chunking only.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:27:55 +07:00
I Putu Eddy Irawan bd0018a5d7 fix: use queue-based re-splitting for HTML expansion validation
After re-splitting an oversized chunk, sub-chunks were sent without
verifying their HTML also fits under 4096 chars. Non-uniform HTML
expansion (e.g. a sub-chunk dense with bold/links) could still exceed
the limit. Use a queue that pushes sub-chunks back for re-validation
instead of sending them blindly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:19:04 +07:00
I Putu Eddy Irawan 3de4cb863b fix: pass original markdown to sendHTMLChunk for plain-text fallback
When HTML parsing fails, the fallback was re-sending the same HTML
string with ParseMode cleared, showing raw HTML tags to users.
Now pass the original markdown chunk so the fallback displays
readable plain text instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:15:17 +07:00
Mauro 858e51da62 Merge pull request #1096 from Oceanpie/docs/summarize-config-example
docs(config): expose summarization thresholds in config example
2026-03-04 16:09:43 +01:00
Oceanpie b3946984ad docs(config): expose summarization thresholds in config example 2026-03-04 21:55:02 +08:00
I Putu Eddy Irawan 8bd1935efb telegram: lower MaxMessageLength to 4000 for HTML expansion margin
The Manager splits at MaxMessageLength before calling Send(), and
Telegram's Send() was re-splitting at 4000 internally. Aligning the
channel-level limit to 4000 avoids that redundant second split while
preserving the safety margin for markdown-to-HTML expansion.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 20:46:43 +07:00
Mauro a00ecedeb6 Merge pull request #1081 from rankaiyx/rankaiyx-patch-1
Update README.md to fix ROADMAP.md path
2026-03-04 13:35:06 +01:00
rankaiyx 93689b8231 Update README.md 2026-03-04 20:16:16 +08:00
daming大铭 c8178f4ad4 Merge pull request #732 from is-Xiaoen/feat/jsonl-memory-store
feat(memory): JSONL-backed session persistence
2026-03-04 19:34:00 +08:00
xiaoen f9f726c0c1 fix(memory): fsync appended message for consistent durability
addMsg now calls f.Sync() before f.Close(), matching the durability
guarantee of writeMeta and rewriteJSONL (both use WriteFileAtomic
with fsync). Without this, a power loss could leave the appended
line in the kernel page cache only — lost on reboot.
2026-03-04 19:21:34 +08:00
Dimitrij Denissenko 494953fb78 Fix lint 2026-03-04 10:21:59 +00:00
qs3c 4946a8b449 fix(openai_compat): clarify HTML response errors 2026-03-04 17:54:04 +08:00
Guoguo 028605cfd0 feat: execute LLM tool calls in parallel for faster response (#1070)
When the LLM returns multiple tool calls, they are now executed
concurrently using goroutines + sync.WaitGroup instead of sequentially.
Results are collected in an indexed slice and processed in original order
to preserve message ordering. MessageTool.sentInRound is changed to
atomic.Bool for thread safety.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 17:17:28 +08:00
Meng Zhuo 2a577f7a1d chore: alter env timezone from Asia/Tokyo to Asia/Shanghai (#1054) 2026-03-04 17:05:57 +08:00
zihan987 42fc589a75 add Vivgrid config example to README 2026-03-03 23:49:49 -08:00
王路路 e0616362fe fix(discord): prevent duplicate link expansion and add regex tests
Address Copilot review feedback:
- Move resolveDiscordRefs(content) before the referenced message
  concatenation to prevent message links in quoted replies from being
  expanded twice.
- Add unit tests for channelRefRe and msgLinkRe regex patterns,
  covering valid/invalid inputs and the 3-link cap.
2026-03-04 15:11:30 +08:00
shikihane b82bb9acc0 feat(tools): add GLM Search (智谱) web search provider (#1057)
* feat(config): add GLMSearchConfig for GLM Search provider

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test(tools): add failing tests for GLM Search provider

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(tools): add GLMSearchProvider for web search

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(agent): wire GLM Search config into web search tool registration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 14:58:12 +08:00
Rahul Bansal df1b53fdf9 feat: make summarization message threshold and token percent configurable (#854) (#1029)
Co-authored-by: Rahul Bansal <rahul@hudle.in>
2026-03-04 11:23:01 +08:00
Meng Zhuo b075ee43d8 Merge pull request #1052 from imguoguo/update-wechat-qr
docs: update wechat qrcode
2026-03-04 10:53:36 +08:00
Guoguo 16209d1da9 docs: update wechat qrcode 2026-03-03 18:48:08 -08:00
王路路 38263333ed fix(discord): prevent cross-guild message leakage in link expansion
Security fix: resolveDiscordRefs now takes a guildID parameter and
skips message links pointing to a different guild, preventing the bot
from leaking content across guilds.

Also uses s.State.Channel() cache before falling back to API calls
to reduce Discord API usage and rate limit risk.
2026-03-04 10:12:37 +08:00
王路路 c3e029061b refactor(discord): self-review fixes for resolveDiscordRefs
- Guard against nil ReferencedMessage.Author to prevent panic
- Hoist regexp.MustCompile to package-level vars to avoid
  re-compilation on every handleMessage call
- Both are defensive programming improvements
2026-03-04 10:12:37 +08:00
王路路 922604fc7e feat(discord): resolve channel references and expand message links
Add resolveDiscordRefs method that:
1. Resolves <#id> channel mentions to #channel-name by calling
   the Discord API to fetch channel info
2. Expands Discord message links (up to 3) by fetching the linked
   message content and appending it as '[linked message from User]: content'

Applied to both quoted/referenced messages and the main message
content for full context resolution.
2026-03-04 10:12:37 +08:00
王路路 465819e1c6 feat(discord): support referenced/quoted messages in replies
When a user replies to a message in Discord, the bot now reads
m.ReferencedMessage and prepends its content to the incoming
message as '[quoted message from Username]: content'.

This gives the LLM full context of what message the user is
replying to, enabling meaningful follow-up conversations.
2026-03-04 10:11:39 +08:00
Kyle D a4546ffb8f feat: add Avian as a named LLM provider
Add Avian (https://avian.io) as an OpenAI-compatible provider with
API base https://api.avian.io/v1 and AVIAN_API_KEY env var support.

Models: deepseek/deepseek-v3.2, moonshotai/kimi-k2.5, z-ai/glm-5,
minimax/minimax-m2.5. Supports chat completions, streaming, and
function calling.

Changes:
- Add Avian to ProvidersConfig struct, IsEmpty(), HasProvidersConfig()
- Add avian protocol to factory provider and default API base
- Add avian case to legacy provider selection (factory.go)
- Add avian migration rule for old config format
- Add default model entries to ModelList (deepseek-v3.2, kimi-k2.5)
- Add avian to example config
- Update AllProviders test count from 18 to 19
2026-03-03 19:36:46 +00:00
美電球 bea238c337 Merge pull request #853 from nayihz/feat_discord_proxy
feat(discord): add proxy support and tests
2026-03-04 01:03:57 +08:00
I Putu Eddy Irawan fe97387f0f Merge remote-tracking branch 'origin/feat/exa-search' into deploy/pi-integration 2026-03-03 22:29:10 +07:00
I Putu Eddy Irawan b7aaa5b887 Merge remote-tracking branch 'origin/feat/dotenv-loading' into deploy/pi-integration 2026-03-03 22:29:05 +07:00
I Putu Eddy Irawan 8ed351cf28 Merge remote-tracking branch 'origin/feat/telegram-chunking' into deploy/pi-integration 2026-03-03 22:28:59 +07:00
I Putu Eddy Irawan c5d2298490 Merge remote-tracking branch 'origin/feat/kimi-opencode-providers' into deploy/pi-integration 2026-03-03 22:28:54 +07:00
I Putu Eddy Irawan d257f1aaef merge: resolve conflict with upstream/main in provider_test.go
Keep both KimiCodeUserAgent test (from this branch) and
serializeMessages tests (from upstream) — no overlap.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 22:18:20 +07:00
I Putu Eddy Irawan e503c87c18 fix: add LiteLLM to env overrides and fix malformed .env test
- Add missing LITELLM entry to loadProviderEnvOverrides so
  PICOCLAW_PROVIDERS_LITELLM_API_KEY/API_BASE env vars work
- Replace valid .env content in TestLoadConfig_MalformedDotenv_NonFatal
  with genuinely malformed content (bare key without '=') to actually
  exercise the non-fatal error path

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 22:14:25 +07:00
I Putu Eddy Irawan 5b608ae678 test: use guardCommand directly and improve assertions in DiskWiping test
Address Copilot review feedback:
- Use guardCommand() instead of Execute() to avoid running dangerous
  commands (format, mkfs, diskpart) on the host if regex regresses
- Assert error message contains "blocked" for blocked commands
- Replace "go fmt ./..." with "echo go fmt ./..." to avoid accidental
  file modification

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 22:05:32 +07:00
I Putu Eddy Irawan 56ad77b735 Merge upstream/main into feat/dotenv-loading
Resolve go.mod conflict: keep h2non/filetype from upstream,
tcell/v2 stays in direct deps only (not duplicated in indirect).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 22:01:04 +07:00
I Putu Eddy Irawan 5dcd42e5d3 Merge upstream/main into fix/bugfixes
Resolve conflicts:
- provider.go: keep upstream's serializeMessages (supersedes stripSystemParts)
- provider_test.go: keep upstream's serializeMessages tests
- loop_test.go: add slices import needed by upstream tests
- shell.go: merge PR's --format deny fix with upstream's block device
  pattern, safePaths, and absolutePathPattern
- shell_test.go: include tests from both branches

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 21:55:26 +07:00
I Putu Eddy Irawan e54b1d39a5 refactor: parse Kimi API hostname once in constructor instead of per-call
Avoid re-parsing apiBase URL on every Chat() invocation by computing
isKimiAPI once in NewProvider(). Also document why the KimiCLI/0.77
User-Agent string is required.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 21:34:31 +07:00
Mauro 4a7605ee14 Merge pull request #1024 from wangyanfu2/fix-TavilySearch-response
fix: add HTTP status code check in BraveSearchProvider
2026-03-03 11:42:05 +01:00
nayihz 69b1ae48d5 Merge remote-tracking branch 'origin/main' into feat_discord_proxy 2026-03-03 18:38:57 +08:00
daming大铭 a65ccc0d1d Merge pull request #1020 from shikihane/feat/agent-vision-pipeline-v2
feat(agent): add vision/image support with streaming base64 and filetype detection
2026-03-03 18:35:23 +08:00
daming大铭 cf68166cf2 Merge pull request #1000 from alexhoshina/main
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
2026-03-03 18:30:14 +08:00
pikaxinge 3902061db1 fix(agent): invalidate system prompt cache for global/builtin skills (#845)
* fix(agent): invalidate system prompt cache for global/builtin skills

* test(agent): avoid os.Chdir in builtin skill cache test

* fix(agent): harden skill cache invalidation checks
2026-03-03 18:25:00 +08:00
wangyanfu2 7de4cc5ebd fix: add HTTP status code check in BraveSearchProvider
- Add status code validation after reading response body, consistent
  with TavilySearchProvider and PerplexitySearchProvider
2026-03-03 17:54:43 +08:00
Guoguo 1265655ef0 feat(telegram): add base_url support for custom Telegram Bot API server (#1021)
* feat(telegram): add base_url support for custom Telegram Bot API server

Allow users to specify a custom Telegram Bot API server URL via
config field `base_url` or env var `PICOCLAW_CHANNELS_TELEGRAM_BASE_URL`.
Defaults to the official https://api.telegram.org when left empty.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(telegram): trim whitespace and trailing slash from base_url

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 17:27:57 +08:00
shikihane 6ccb68c63e fix: resolve linter issues (gci import grouping, gofumpt, govet shadow)
- Separate third-party imports from local module imports (gci)
- Fix byte slice literal formatting (gofumpt)
- Rename shadowed err variable to ftErr (govet)
- Remove trailing blank lines in test files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 17:04:54 +08:00
Hoshina fa1cb9cc74 fix(feishu): address PR #1000 review comments from @xiaket
- Consolidate extractImageKey/extractFileKey/extractFileName into shared
  extractJSONStringField helper to reduce code duplication
- Move mentionPlaceholderRegex to package-level position after imports
- Rename feishuCfg field to config for clarity within FeishuChannel
- Replace @_user_1 heuristic with GET /open-apis/bot/v3/info API call
  at startup for reliable bot @mention detection
- Fix double close on file handle in downloadResource by removing defer
  and using explicit close in both success and error paths
- Add unit tests for common.go and feishu_64.go helpers (53 test cases)
2026-03-03 16:44:24 +08:00
shikihane 43227411ee feat(agent): wire media refs through agent pipeline to LLM provider
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 16:28:20 +08:00
shikihane 03f7ae494f feat(openai_compat): implement serializeMessages with multipart media support
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 16:28:20 +08:00
shikihane 6fd65825e7 feat(agent): implement resolveMediaRefs with streaming base64 and filetype detection
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 16:28:20 +08:00
shikihane 559cef3d5b chore: add h2non/filetype dependency for magic-bytes MIME detection
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 16:28:20 +08:00
shikihane 4c6c05a251 feat(config): add configurable max_media_size with 20MB default
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 16:27:29 +08:00
shikihane 6689c0b1c0 feat(providers): add Media field to Message struct for vision support
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 16:27:29 +08:00
daming大铭 de2ccb5da4 Merge pull request #999 from yinwm/fix/mcp-race-condition-and-resource-leak
fix(mcp): resolve TOCTOU race condition and resource leak
2026-03-03 15:06:57 +08:00
daming大铭 227f22d28a Merge pull request #1002 from afjcjsbx/docs/mcp-http-server-example
docs(mcp): http server example
2026-03-03 14:44:07 +08:00
xiaoen 1a922c96a8 merge: resolve conflict with main in loop.go
Main reformatted the fallback.Execute call to multi-line (golines);
our branch renamed agent.Candidates → activeCandidates for routing.
Kept both: multi-line formatting + routing variable.
2026-03-03 12:22:45 +08:00
lxowalle 435223f500 * Add new style banner for picoclaw and picoclaw-launcher-tui (#1008)
* Add new style banner for picoclaw and picoclaw-launcher-tui
2026-03-03 12:04:28 +08:00
Caize Wu 3bb4f4ecc6 Merge pull request #1010 from sipeed/revert-990-feat/agent-vision-pipeline
revert: "feat(agent): add vision/image support to agent pipeline"
2026-03-03 11:41:58 +08:00
Guoguo 407707a7cc Revert "feat(agent): add vision/image support to agent pipeline" 2026-03-03 11:38:32 +08:00
Orgmar 12d4570a36 Merge pull request #990 from shikihane/feat/agent-vision-pipeline
feat(agent): add vision/image support to agent pipeline
2026-03-03 11:32:07 +08:00
shikihane 8ebeefc59f fix(agent,openai_compat): address review feedback on vision pipeline
- serializeMessages: preserve ToolCallID/ToolCalls when Media is present
- resolveMediaRefs: add 20MB file size limit to prevent OOM
- mimeFromExtension: return empty string for unknown extensions
- Add 11 unit tests for serializeMessages, resolveMediaRefs, mimeFromExtension

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 11:13:22 +08:00
I Putu Eddy Irawan 0e810a2ec4 fix: tighten HTML-expansion test to stay under chunk size
Reduce markdown input from 700 to 600 repeats (3600 runes) so it stays
under the 4000-rune chunk threshold. This ensures the test actually
exercises the HTML-expansion re-splitting logic rather than being split
at the markdown level first.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 09:20:16 +07:00
I Putu Eddy Irawan 2fc87985d2 fix: add kimi-code migration alias and User-Agent test
- Add "kimi-code" to the moonshot provider's providerNames in
  ConvertProvidersToModelList so configs using
  agents.defaults.provider: "kimi-code" migrate correctly.
- Add TestProviderChat_KimiCodeUserAgent verifying that
  User-Agent: KimiCLI/0.77 is set when apiBase hostname is
  api.kimi.com and not set for other hosts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 09:18:26 +07:00
I Putu Eddy Irawan df53f4411a fix: format long lines in telegram_test.go to satisfy golines linter
Break function signatures and assert calls that exceed the 120-char
golines limit onto multiple lines.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 09:11:24 +07:00
Alfonso 946af6b53d feat: add LiteLLM provider alias support (#930) 2026-03-03 08:23:55 +11:00
afjcjsbx 23bb0828b1 mcp http server example 2026-03-02 19:50:34 +01:00
Hoshina 595de7814d fix(feishu): remove dead fetchBotOpenID stub and fix misleading comment 2026-03-03 01:39:33 +08:00
Hoshina 42eb6ea410 fix(feishu): address review findings
- Remove stale "falls back to plain text" comment on Send
- Add empty ChatID validation in SendMedia to match Send
- Use messageID+fileKey as local filename to avoid write collisions
- Check allowlist before downloading inbound media to avoid wasted I/O
- Return errUnsupported consistently from all 32-bit stub methods
2026-03-03 01:27:46 +08:00
Hoshina 0bee9d7bcf fix(feishu): resolve lint issues 2026-03-03 01:04:11 +08:00
Hoshina c9fb681f3b feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).

Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-03 00:49:32 +08:00
yinwm 78aba700d5 fix(mcp): resolve TOCTOU race condition and resource leak
- Use atomic.Bool for closed flag to prevent TOCTOU race between
  CallTool and Close operations
- Add double-check pattern in CallTool for thread-safe closed state
- Use atomic Swap in Close to ensure no new calls can start after
  closed flag is set
- Move MCP manager cleanup defer before initialization to handle
  partial initialization failures
- Update tests to use atomic.Bool operations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 00:47:25 +08:00
daming大铭 0150947e61 Merge pull request #282 from yuchou87/mcp-tools-support
feat: Add mcp tools support
2026-03-03 00:18:14 +08:00
daming大铭 4e348e39ac Merge branch 'main' into mcp-tools-support 2026-03-03 00:17:39 +08:00
daming大铭 475d8f948b Merge pull request #727 from Esubaalew/fix/wecom-dedupe-race
fix(wecom): remove message-dedupe data races and fix amnesia cliff
2026-03-02 23:57:42 +08:00
esubaalew 2e0be92776 fix(wecom): resolve upstream rebase conflicts after channel refactor
Rebase onto latest upstream/main, keep ring-buffer dedupe behavior, move dedupe tests to pkg/channels/wecom, and ensure wecom/channels race tests pass.
2026-03-02 18:54:11 +03:00
I Putu Eddy Irawan 84ded81a8c Address Copilot review feedback for .env loading
- Add migrateChannelConfigs() and ValidateModelList() to the fresh-
  install path (no config.json) so legacy env vars are migrated and
  model list is validated consistently with the normal loading path
- Use os.LookupEnv instead of os.Getenv in loadProviderEnvOverrides
  so explicitly empty env vars (e.g. PICOCLAW_PROVIDERS_X_API_BASE=)
  can clear values from config.json
- Guard .env loading with sync.Once to avoid repeated disk I/O and
  noisy log messages when LoadConfig is called from polling handlers
- Add tests: .env file loading, missing config.json with env vars,
  malformed .env non-fatal behavior, and LookupEnv empty-override

Note: go.mod tcell/v2 and tview are correctly listed as direct deps
(they are imported by the launcher TUI); upstream go.mod was stale.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 22:50:59 +07:00
esubaalew 29e9b6b4b5 fix(wecom): replace dedupe map rotation with circular queue
The previous dedupe map rotation logic completely cleared the map when it reached max size, causing an 'amnesia cliff' where immediately arriving duplicates of just-forgotten messages would be processed.

This change replaces that with a MessageDeduplicator struct that uses a circular queue (ring buffer) to track insertions. When the limit is reached, it only evicts the absolute oldest message from the map, completely resolving the cliff issue.

This also cleans up the WeCom Bot and App webhook handlers by encapsulating the mutex and map state.
2026-03-02 18:50:51 +03:00
esubaalew 8640c8177c fix(wecom): correctly retain boundary message during dedupe map rotation
When the dedupe map rotates, the previous logic entirely cleared the map, meaning the message that triggered the rotation was immediately forgotten and could be duplicated immediately.

This change seeds the new map with the current message to prevent that. Also adds a defensive nil check.
2026-03-02 18:50:29 +03:00
esubaalew 1e2ab4a5e5 test(wecom): add dedupe helper coverage and align constant usage
Use wecomMaxProcessedMessages in tests and add a concurrent same-message test
to lock in race-safety behavior for markMessageProcessed.
2026-03-02 18:50:29 +03:00
esubaalew db17cdc86d test(wecom): align dedupe rotation behavior and add helper tests
Match rotation semantics to prior behavior by fully resetting the dedupe map
once the size limit is exceeded, and add focused tests for duplicate detection
and boundary rotation behavior.
2026-03-02 18:50:29 +03:00
esubaalew 18d89937ad fix(wecom): remove message-dedupe data races in bot/app channels
Centralize dedupe map access behind a mutex-safe helper and use it in both
WeCom bot and WeCom app channels to eliminate concurrent map access races while
preserving current dedupe behavior.
2026-03-02 18:50:29 +03:00
I Putu Eddy Irawan 8219b5a26f Address Copilot review feedback for Exa search provider
- Add explicit empty-results handling ("No results for: <query>")
- Add "Results for: <query> (via Exa)" header and align per-result
  format with Brave/Tavily/DuckDuckGo/Perplexity
- Add tests: provider priority (Perplexity > Exa > Brave), proxy
  propagation, successful search with header/attribution, empty
  results, and max-results capping

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 22:43:43 +07:00
daming大铭 25639168ea Merge pull request #300 from mymmrac/telegram-bot-commands
feat(telegram): Init bot commands on start
2026-03-02 23:43:10 +08:00
I Putu Eddy Irawan 33109a1676 Address Copilot review: handle HTML expansion exceeding Telegram limit
When markdownToTelegramHTML expands a chunk beyond 4096 chars (e.g.
**a** → <b>a</b>), re-split the markdown with a proportionally smaller
maxLen so each resulting HTML chunk fits within Telegram's limit.

Extract sendHTMLChunk helper to avoid duplicating the HTML-send +
plain-text-fallback logic.

Add test case for markdown-short-but-HTML-long scenario to verify
the re-splitting behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 22:35:41 +07:00
daming大铭 8fddbaed4f Merge pull request #682 from Esubaalew/fix/makefile-test-vet-generate
fix: add generate dependency to test and vet Makefile targets
2026-03-02 23:31:03 +08:00
I Putu Eddy Irawan 4b7e8d9cb9 feat: add Exa AI search provider
Add Exa (https://exa.ai) as a new web search provider option, slotting
into the priority chain between Perplexity and Brave. Configurable via
config.json or PICOCLAW_TOOLS_WEB_EXA_* environment variables.

Results are capped to the requested count for consistency with other
search providers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 22:29:26 +07:00
I Putu Eddy Irawan d9b4af797d feat: add .env file loading and provider env overrides
Load .env files from the config directory before reading config.json,
enabling secrets and API keys to be stored outside version control.
Supports fresh installs (no config.json) by applying env vars and
provider overrides to the default config.

Adds loadProviderEnvOverrides() for PICOCLAW_PROVIDERS_<NAME>_API_KEY
and _API_BASE environment variables across all standard providers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 22:26:36 +07:00
xiaoen 09e68cb63b fix(routing): resolve golines, gosmopolitan and misspell lint failures
- classifier.go: s/honour/honor/ (American English per misspell)
- router.go: break SelectModel signature across lines (golines)
- router_test.go: break long Message literal (golines)
- router_test.go: replace CJK string literal with rune slice so
  gosmopolitan does not flag the source file; behaviour is identical
2026-03-02 23:11:45 +08:00
I Putu Eddy Irawan 4a067cd9ed Merge branch 'main' into feat/kimi-opencode-providers 2026-03-02 22:08:50 +07:00
I Putu Eddy Irawan 3501962977 test: add unit tests for Telegram Send() method
Cover empty content early return, single-message send,
multi-chunk splitting for long messages, HTML-to-plain-text
fallback per chunk, and error propagation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 21:54:35 +07:00
xiaoen 02e8192349 feat(agent): wire model routing into the agent loop
instance.go:
  - Add Router *routing.Router and LightCandidates []FallbackCandidate
    to AgentInstance.
  - At agent creation, when routing.enabled and light_model resolves
    successfully in model_list, pre-build the Router and resolve the
    light model candidates once. If the light model isn't in model_list,
    log a warning and disable routing for that agent gracefully.

loop.go:
  - Add selectCandidates(agent, userMsg, history) helper.
    It calls Router.SelectModel and returns either agent.Candidates /
    agent.Model (primary tier) or agent.LightCandidates / light_model
    (light tier). Returns primary unchanged when routing is disabled.
  - In runLLMIteration, resolve (activeCandidates, activeModel) once
    before entering the tool-iteration loop. The model tier is sticky
    for the entire turn so a multi-step tool chain doesn't switch
    models mid-way.
  - Replace hard-coded agent.Candidates / agent.Model references in
    callLLM and the debug log with the resolved active values.

The fallback chain and retry logic are untouched. When light_model
returns an error the fallback chain handles escalation normally.
2026-03-02 22:42:52 +08:00
xiaoen 1943c3e660 feat(routing): add language-agnostic model complexity scorer
Add three new files to pkg/routing/:

features.go — ExtractFeatures(msg, history) → Features
  Computes five structural dimensions with zero keyword matching:
  - TokenEstimate: rune_count/3 (CJK-safe token proxy)
  - CodeBlockCount: ``` pairs in the message
  - RecentToolCalls: tool call count in the last 6 history entries
  - ConversationDepth: total messages in session
  - HasAttachments: data URIs or media file extensions

classifier.go — Classifier interface + RuleClassifier
  RuleClassifier uses a weighted sum that is capped at 1.0:
    code block      → +0.40  (triggers heavy model alone at 0.35 threshold)
    token > 200     → +0.35  (triggers heavy model alone)
    tool calls > 3  → +0.25
    token 50-200    → +0.15
    conversation depth > 10 → +0.10
    attachment      → 1.00 (hard gate, always heavy)

router.go — Router wraps config + Classifier
  Router.SelectModel(msg, history, primaryModel) returns either the
  configured light_model or the primary model depending on whether
  the complexity score clears the threshold. Threshold defaults to
  0.35 when zero/negative to prevent misconfiguration.

router_test.go — 34 tests covering all branches and edge cases
2026-03-02 22:42:20 +08:00
xiaoen c5a21b269f feat(config): add RoutingConfig to AgentDefaults
Introduce RoutingConfig with three fields:
  - enabled: activates per-turn model routing
  - light_model: references a model_name in model_list
  - threshold: complexity score cutoff in [0,1]

When routing.enabled is true and the incoming message scores below
threshold, the agent switches to light_model for that turn. Absent or
disabled config leaves existing behaviour completely unchanged.

Example:
  "agents": {
    "defaults": {
      "model": "claude-sonnet-4-6",
      "routing": {
        "enabled": true,
        "light_model": "gemini-flash",
        "threshold": 0.35
      }
    }
  }
2026-03-02 22:40:52 +08:00
美電球 f2ab1a74da Merge pull request #893 from reevoid/rui-dev
Add WeCom AIBot channel implementation and tests
2026-03-02 21:50:21 +08:00
Caize Wu 929589a025 Merge pull request #987 from lxowalle/doc/update_contribute
* update contributing.md
2026-03-02 21:24:31 +08:00
lxowalle 4402fcf63c * update contributing.md 2026-03-02 21:20:23 +08:00
lxowalle 5fa2e1d1e4 * update contributing.md 2026-03-02 21:17:59 +08:00
daming大铭 faec0261d0 Merge pull request #535 from xiaket/ci-enable-dupl-linter
ci: enable duplication linter in CI
2026-03-02 18:55:35 +08:00
Zhang Rui 23f48d7c4e refactor(aibot): remove downloadAndDecryptImage function to streamline image handling 2026-03-02 18:21:53 +08:00
nayihz 9be6fb1a7d Merge branch 'main' into feat_discord_proxy 2026-03-02 18:15:20 +08:00
shikihane 18b36af934 feat(agent): add resolveMediaRefs to convert media:// refs to base64 data URLs
Without this function, media:// refs stored by MediaStore are passed
directly to the LLM API, which rejects them as invalid URLs.

resolveMediaRefs() runs after BuildMessages() and before the LLM call,
converting each media:// ref to a data:image/...;base64,... URL that
vision-capable models can process.

Also adds mimeFromExtension() helper for MIME type inference from
file extensions when ContentType metadata is not available.
2026-03-02 18:08:32 +08:00
Zhang Rui edd339e056 fix(wecom): handle empty response by encrypting and returning a default response 2026-03-02 17:42:54 +08:00
Zhang Rui 619948f8ff fix(wecom): improve error message for response_url delivery failure 2026-03-02 17:42:54 +08:00
Zhang Rui bf4445f1f3 refactor(docs): remove webhook_host and webhook_port from configuration examples 2026-03-02 17:42:54 +08:00
Zhang Rui d4824a00b6 refactor(config): remove WebhookHost and WebhookPort from WeComAIBotConfig 2026-03-02 17:42:54 +08:00
Zhang Rui 55c556a4c5 fix(wecom): update CanonicalID generation to use identity.BuildCanonicalID for consistency 2026-03-02 17:42:54 +08:00
Zhang Rui 79b7fb7792 fix(wecom): improve error handling in sendViaResponseURL and remove task on failure 2026-03-02 17:42:54 +08:00
Zhang Rui 79bc06c0ba refactor(wecom): simplify stream message structure by introducing WeComAIBotMsgItem and WeComAIBotMsgItemImage types 2026-03-02 17:42:54 +08:00
Zhang Rui 880c402ab7 refactor(wecom): streamline AES encryption/decryption and improve task management logic 2026-03-02 17:42:54 +08:00
Zhang Rui 8f3d611a4c refactor(wecom): replace generateSignature with computeSignature and update related tests 2026-03-02 17:42:54 +08:00
Zhang Rui 81f6787dd5 fix(docs): update WeCom AI Bot timeout duration in README and improve streamTask comments 2026-03-02 17:42:54 +08:00
ZHANG RUI e88b39f21e Update pkg/channels/wecom/aibot.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-02 17:42:54 +08:00
Zhang Rui 4a87090fd9 fix(docs): update WeCom AI Bot task timeout duration in README 2026-03-02 17:42:54 +08:00
Zhang Rui a87e6b0551 feat(wecom-aibot): enhance stream task management with StreamClosedAt and improved cleanup logic 2026-03-02 17:42:54 +08:00
Zhang Rui 4e09c91dda feat(wecom-aibot): add context management for stream tasks to improve agent cancellation 2026-03-02 17:42:54 +08:00
ZHANG RUI 0b6d913dfc Update pkg/channels/wecom/aibot.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-02 17:42:54 +08:00
ZHANG RUI aa9ce6955b Update pkg/channels/wecom/aibot.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-02 17:42:54 +08:00
ZHANG RUI e33712deff Update pkg/channels/wecom/aibot.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-02 17:42:54 +08:00
Zhang Rui e894f8d39a feat(wecom-aibot): add reasoning_channel_id to configuration and enhance message handling limits 2026-03-02 17:42:54 +08:00
Zhang Rui a25726e798 feat(wecom): add WeCom AI Bot integration and update documentation 2026-03-02 17:42:50 +08:00
Zhang Rui c7d4012fc9 fix(wecom-aibot): correct variable name in JSON parsing in message callback handler 2026-03-02 17:42:42 +08:00
Zhang Rui 6caee427bb Add WeCom AIBot channel implementation and tests
- Introduced WeCom AIBot channel configuration in config.go with relevant fields.
- Implemented WeCom AIBot channel factory registration in init.go.
- Created unit tests for WeCom AIBot channel functionalities including initialization, start/stop behavior, webhook path handling, message encryption/decryption, and signature generation.
- Set default values for WeCom AIBot configuration in defaults.go.
2026-03-02 17:42:42 +08:00
shikihane a4e5c391bd fix(openai_compat): preserve reasoning_content in serializeMessages
The serializeMessages() function was not preserving the reasoning_content
field when serializing messages for vision API calls. This caused the
TestProviderChat_PreservesReasoningContentInHistory test to fail.

This fix ensures reasoning_content is included in both text-only messages
and vision messages with media attachments.

Co-authored-by: Zachary Guerrero <zack.grrr@gmail.com>
2026-03-02 17:38:08 +08:00
shikihane 6997edc82e feat(agent): wire Media through agent pipeline (cherry-pick PR #555)
Add Media field to processOptions, pass msg.Media from inbound
messages through to BuildMessages and serializeMessages so
vision-capable LLMs receive image_url content parts.

Based on work by @as3k in sipeed/picoclaw#555.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 17:18:04 +08:00
Zachary Guerrero 3d54a77c40 feat: add Media field to Message struct and implement serializeMessages for vision API support
- Add Media []string field to Message struct for image/media URLs
- Implement serializeMessages() to format messages with image_url content parts
- Enables OpenAI-compatible vision APIs to receive image attachments
2026-03-02 17:18:04 +08:00
daming大铭 26d1b8e374 Merge pull request #946 from winterfx/fix/preserve-reasoning-content-in-history
fix: preserve reasoning_content in multi-turn conversation history
2026-03-02 16:31:03 +08:00
Huang Rui d5370c9605 fix(tools): allow /dev/null redirection and add read/write sandbox split (#967)
* fix(tools): allow /dev/null redirection and add read/write sandbox split

- Remove deny pattern that incorrectly blocked redirects to /dev/null
- Expand block device write pattern to cover nvme, mmcblk, vd, xvd,
  hd, loop, dm-, md, sr and nbd in addition to sd
- Add safe path whitelist for kernel pseudo-devices so workspace path
  check does not reject /dev/null, /dev/zero, /dev/random, /dev/urandom,
  /dev/stdin, /dev/stdout and /dev/stderr
- Add allow_read_outside_workspace config option (default true) so file
  read and list tools are unrestricted while write tools stay sandboxed

Closes https://github.com/sipeed/picoclaw/issues/964
Closes https://github.com/sipeed/picoclaw/issues/965

Signed-off-by: Huang Rui <vowstar@gmail.com>

* feat(tools): add configurable allow patterns and path whitelists

- Add custom_allow_patterns to exec config so users can exempt specific
  commands from deny pattern checks
- Add allow_read_paths and allow_write_paths regex lists to tools config
  for whitelisting specific paths outside the workspace
- Introduce whitelistFs that wraps sandboxFs and falls through to hostFs
  for paths matching whitelist patterns
- Use variadic constructor signatures to keep backward compatibility

Suggested-by: lxowalle
Signed-off-by: Huang Rui <vowstar@gmail.com>

---------

Signed-off-by: Huang Rui <vowstar@gmail.com>
2026-03-02 12:22:02 +08:00
Mauro b26337501c fix: error check on state (#864) 2026-03-02 11:59:26 +11:00
Meng Zhuo 83dbff7785 Merge pull request #883 from afjcjsbx/fix/max-payload-size-in-web-fetch
fix: max payload size in web fetch
2026-03-02 08:40:11 +08:00
afjcjsbx e0667304d1 fixed conflicts 2026-03-01 23:44:21 +01:00
Mauro b86bf5b7ea Merge branch 'main' into fix/max-payload-size-in-web-fetch 2026-03-01 22:38:16 +01:00
Dimitrij Denissenko b74f92ed28 A more neutral and elegant voice.Transcriber interface 2026-03-01 21:02:16 +00:00
Luca Martinetti fc9f1ec921 fix: return fetched content to LLM in web_fetch tool (#833)
* fix: return fetched content to LLM in web_fetch tool

WebFetchTool.Execute was setting ForLLM to a summary string
("Fetched N bytes from URL ...") instead of the actual extracted
text. This meant the LLM never saw the page content and could not
answer questions based on fetched web pages.

Return the extracted text in ForLLM so the model can use it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: put full JSON result in ForLLM, summary in ForUser

Accept suggestion from afjcjsbx: the LLM should receive the full JSON
result (including extracted text) while the user sees a short summary.
Update tests to match the new field assignment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 07:48:11 +11:00
Keith d4bc28c113 feat(config): Add support for env var configuration (#896)
* feat(config): Add support for env var configuration

This commit introduces support for two environment variables,
allowing users to override the default paths for picoclaw's home
directory and configuration file.

- `PICOCLAW_CONFIG`: Directly specifies the path to the `config.json` file.
This is initialised first, takes precedence over the hardcoded path, and is ideal
for containerized deployments or custom config management.

- `PICOCLAW_HOME`: Overrides the root directory for all picoclaw data, (except the config)
(e.g., `~/.picoclaw`). This is useful for portable installations or placing
data in non-standard locations.

This change provides greater flexibility for running picoclaw in various environments without
being tied to the default home directory structure.

* `README.md` updated explain PICOCLAW_CONFIG and PICOCLAW_HOME

* docs: translate environment variables section to multiple languages

---------

Co-authored-by: picoclaw <picoclaw@sipeed.com>
2026-03-02 07:41:12 +11:00
美電球 3926585786 Merge pull request #916 from alexhoshina/fix/channel-config-cleanup
docs: sync READMEs, examples, and channel docs to match current config
2026-03-01 22:28:55 +08:00
Hoshina cd3a4e1d1e docs: fix review feedback from PR #916
- Remove Feishu from webhook channel list in README.md and README.zh.md;
  add clarifying note that Feishu uses WebSocket/SDK mode instead
- Replace Chinese text in README.vi.md header with Vietnamese equivalent
- Translate mixed-language WeCom note in README.vi.md to full Vietnamese
- Mark webhook_path as optional (否) in docs/channels/line/README.zh.md
- Remove incorrect yaml struct tags from new-channel example in
  pkg/channels/README.md and README.zh.md (config uses json tags only)
- Fix multi-mode initChannel example to use whatsapp/whatsapp_native
  (matching the "WhatsApp Bridge vs Native" comment) instead of matrix
- Correct ReasoningChannelID description: list the 12 channels that
  have the field and note that PicoConfig does not expose it
2026-03-01 22:21:49 +08:00
Tong Niu d6e88da8ba fix(pkg):do regex precompile insteadd on the fly (#911)
* fix(pkg/providers):do regex precompile insteadd on the fly

* fix(providers): replace HTTP-specific regex with standalone status code matcher

The precompiled HTTP regex used uppercase "HTTP" which never matched
because ClassifyError lowercases the input. Replace it with a
case-insensitive word-boundary pattern that matches any standalone
3-digit status code (300-599), which also subsumes the HTTP/x.x case.

Add test case for standalone status code extraction.

* fix(providers): restore http regex and add standalone status code matcher

Restore the http-prefixed regex (without unnecessary (?i) flag since
input is already lowercased by ClassifyError) as a mid-priority pattern
to reduce false positives. Add a standalone word-boundary matcher as a
fallback for bare status codes like "429". Fix test to use lowercased
input matching the actual calling convention.

* perf(tools): move path regex compilation from per-call to package init

The path regex in guardCommand was compiled on every call. Hoist it
to a package-level var (absolutePathPattern) alongside defaultDenyPatterns
in a single var block, so it is compiled once at init time.

* style(tools): move inline comment to fix golines formatting error
2026-03-01 23:06:31 +11:00
GhostC 71bdeb41c9 fix: improve error handling in GitHub Copilot provider (#919)
- Fix ignored error from SendAndWait call
- Improve error message for unimplemented stdio mode with helpful guidance
- Add TODO comment with reference link for future stdio implementation
2026-03-01 22:58:41 +11:00
美電球 1ebfbc1c6b Update docs/channels/line/README.zh.md
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-01 18:59:23 +08:00
Guoguo 25f26f305b docs: update wechat qrcode (#955)
Signed-off-by: Guoguo <i@qwq.trade>
2026-03-01 18:27:37 +08:00
Dimitrij Denissenko b1386ad71f Fix voice transcription 2026-03-01 08:39:05 +00:00
winterfx 9efdde25ad fix: preserve reasoning_content in multi-turn conversation history
The openaiMessage struct and stripSystemParts() were not carrying over
the ReasoningContent field when serializing conversation history for
API requests. This caused thinking models (e.g. kimi-k2.5) to receive
incomplete assistant messages on subsequent turns, resulting in 400
errors from the Moonshot API.

Add the ReasoningContent field to openaiMessage and copy it in
stripSystemParts(). Also add a test to verify reasoning_content is
preserved when sending conversation history.

Fixes #588
Related: #876

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 16:23:05 +08:00
Meng Zhuo f7136b6a5d Merge pull request #861 from p3ddd/refactor/modernize
refactor(modernize): apply safe modernize fixes
2026-03-01 15:38:59 +08:00
Kai Xia 434b03ed67 remove wrapper methods
Signed-off-by: Kai Xia <kaix+github@fastmail.com>
2026-03-01 18:24:11 +11:00
Kai Xia 32c864c309 enable dupl check
Signed-off-by: Kai Xia <kaix+github@fastmail.com>
2026-03-01 18:17:32 +11:00
xiaoen 6d894d6138 refactor(memory): use fileutil.WriteFileAtomic and log corrupt lines
- Replace manual temp+rename in writeMeta and rewriteJSONL with the
  project's standard fileutil.WriteFileAtomic. This adds fsync before
  rename, which is important for flash storage on embedded devices
  where power loss can leave zero-length files after an unsynced rename.
- Log a warning when readMessages skips a corrupt line, so operators
  can see that data was lost after a crash instead of silently dropping it.
- Document the lossy sanitizeKey mapping (telegram:123 → telegram_123)
  as an intentional tradeoff.
2026-03-01 14:46:54 +08:00
xiaoen cd500d2046 Merge branch 'main' into feat/jsonl-memory-store 2026-03-01 14:35:14 +08:00
Tong Niu 44a52c0cf6 fix(tools): close resp.Body on retry cancel and cache http.Client instances (#940)
* fix(tools): close resp.Body on retry cancel and cache http.Client instances

Fix resp.Body leak in DoRequestWithRetry where req.Body (request) was
incorrectly closed instead of resp.Body (response) on context cancel.
Cache http.Client on web search/fetch provider structs and channel
adapters (WeCom, LINE) to avoid per-call allocation overhead.

* fix(channels): preserve original http client timeouts for LINE and WeCom

Split LINE single 60s client into infoClient (10s) for bot info lookups
and apiClient (30s) for messaging API calls. Lower WeCom cached client
base timeout from 60s to 30s (matching uploadMedia), and ensure it is
always >= the configured ReplyTimeout so the per-request context
deadline remains the effective limit.

* refactor(tools): extract timeout consts and deduplicate WebFetchTool constructors

Address PR review feedback from xiaket:
- Define searchTimeout, perplexityTimeout, fetchTimeout, defaultMaxChars,
  and maxRedirects as package-level consts instead of magic numbers.
- Remove misleading "No proxy" comment in NewWebFetchTool.
- Deduplicate NewWebFetchTool by delegating to NewWebFetchToolWithProxy.

* test(utils): add context cancellation test for DoRequestWithRetry

Verify that resp.Body is properly closed when the context is canceled
during retry sleep, covering the C8 resp.Body leak fix.

* fix(utils): close resp in test to satisfy bodyclose linter

* fix(utils): eliminate flakiness in context cancellation retry test

Synchronize cancellation using an onRoundTrip callback from the
transport wrapper instead of a timing-based context timeout. This
ensures the first client.Do completes before cancel fires, so
cancellation always hits during sleepWithCtx.
2026-03-01 16:55:46 +11:00
Owen Wu b3c3b02666 fix(onboard): use AGENTS.md template instead of AGENT.md (#931)
* fix(onboard): use AGENTS.md workspace template

* test(onboard): move AGENTS template regression test to new package
2026-03-01 16:25:31 +11:00
DM cadcdc0b41 fix(skills): use registry-backed search for skills discovery (#929)
* fix(skills): use registry-backed search for skills discovery

Signed-off-by: dwizzle204 <25712917+dwizzle204@users.noreply.github.com>

* fix(skills): address review comments for registry search

Signed-off-by: dwizzle204 <25712917+dwizzle204@users.noreply.github.com>

---------

Signed-off-by: dwizzle204 <25712917+dwizzle204@users.noreply.github.com>
Co-authored-by: dwizzle204 <25712917+dwizzle204@users.noreply.github.com>
2026-03-01 15:20:20 +11:00
yuchou87 a2591e03a9 fix: improve MCP tool name collision safety and registry overwrite warning
- MCPTool.Name(): append FNV-32a hash of original (unsanitized) server+tool
  names whenever sanitization is lossy or total length exceeds 64 chars,
  ensuring names that differ only in disallowed characters remain distinct
- ToolRegistry.Register(): emit warn log when a tool registration overwrites
  an existing tool with the same name, making collisions observable
- scripts/test-docker-mcp.sh: switch shebang from #/bin/bash /Users/yuchou/Work/klook-calendar/klook-google-cal-sync/src/googlecalconversrv/bin/start.sh to #  for portability on minimal distros and Nix environments
2026-03-01 12:00:26 +08:00
yuchou87 0eec640c37 fix: correct MCP server install test in test-docker-mcp.sh
server-filesystem does not support --help; use timeout + /dev/null stdin
to verify installation and startup without hanging the test script
2026-03-01 11:24:12 +08:00
daming大铭 33f67e8275 Merge pull request #918 from alexhoshina/fix/wecom-resource-leaks
fix(wecom): fix context leak in Start() and data race in processedMsgs
2026-03-01 10:57:07 +08:00
yuchou87 ef738f4787 fix: address PR review feedback for MCP tools support
- Avoid logging sensitive cfg.Args in ConnectServer; log args_count instead
- Sanitize server/tool name components in MCPTool.Name() to ensure valid
  identifiers for downstream providers (lowercase, [a-z0-9_-] only)
- Add slack as 5th MCP server example in config.example.json
- Move Dockerfile.full and docker-compose.full.yml into docker/ directory
  for consistency with existing docker/Dockerfile and docker/docker-compose.yml
- Fix all Makefile docker-* targets to reference correct compose file paths
- Fix docker/docker-compose.full.yml build context (.. ) and volume paths
- Fix scripts/test-docker-mcp.sh compose file path and replace cowsay test
  with actual @modelcontextprotocol/server-filesystem MCP server test
2026-03-01 10:56:02 +08:00
I Putu Eddy Irawan 2dccee5044 Address Copilot review feedback for Telegram message chunking
- Add early return for empty content to avoid silent no-op
- Split raw markdown before HTML conversion so SplitMessage's
  code-fence-aware logic works correctly and HTML tags/entities
  are never broken by mid-tag splitting

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 09:40:09 +07:00
I Putu Eddy Irawan 9c91d66427 Address Copilot review feedback for Kimi/Opencode providers
- Allow APIBase-only config for opencode provider selection (like VLLM)
- Keep moonshot provider on moonshot.cn/v1 default, only use kimi.com/coding/v1 for kimi/kimi-code
- Use url.Parse hostname match for Kimi User-Agent check instead of strings.Contains
- Add opencode to DefaultAPIBase test cases in factory_provider_test.go
- Add opencode migration tests (full config + APIBase-only) in migration_test.go
- Update AllProviders test count to include opencode (18 -> 19)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 09:22:49 +07:00
I Putu Eddy Irawan 81aeaf1ca0 fix: address Copilot review feedback on PR #932
- Deny regex: expand left boundary to match shell separators (;, &&, ||)
  to prevent bypass via chained commands like ";format c:"
- Path regex: add "." to initial char class to catch hidden dirs (/.ssh),
  add "=" to left boundary to catch flag-attached paths (--file=/etc/passwd)
- Add test: ModelName must match user model for GetModelConfig lookup
- Add test: stripSystemParts preserves reasoning_content in wire format
- Add test: forceCompression avoids orphaning tool result messages
- Add test: deny pattern blocks disk-wiping commands with shell separators
  while allowing legitimate --format flags

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 09:08:11 +07:00
I Putu Eddy Irawan a6f4274870 feat: add message chunking in Telegram Send method
Split HTML content into 4000-char chunks before sending to handle cases
where markdown-to-HTML conversion causes messages to exceed Telegram's
4096-character limit. Uses the existing SplitMessage utility which
preserves code block integrity across chunk boundaries.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 08:56:12 +07:00
I Putu Eddy Irawan ec540312da feat: add Kimi/Moonshot and Opencode provider support
- Add "kimi", "kimi-code", "moonshot" provider cases in factory.go
  with default API base https://api.kimi.com/coding/v1
- Add Kimi Code API User-Agent header (KimiCLI/0.77) for api.kimi.com
- Add "opencode" provider with default API base https://opencode.ai/zen/v1
- Add "opencode" to recognized HTTP-compatible protocols in factory_provider
- Add Opencode field to ProvidersConfig, IsEmpty, HasProvidersConfig
- Add opencode migration entry in ConvertProvidersToModelList
- Update moonshot fallback API base from api.moonshot.cn to api.kimi.com
2026-03-01 08:48:04 +07:00
I Putu Eddy Irawan ee5b61884a fix: migration ModelName, reasoning_content, shell regex, loop boundary
1. migration.go: Set ModelName to userModel when provider matches so
   GetModelConfig(userModel) can find the entry. Previously the migration
   created entries with the provider name as ModelName (e.g. "moonshot")
   but lookup used the model name (e.g. "k2p5"), causing "model not found".

2. openai_compat/provider.go: Preserve reasoning_content in conversation
   history. Thinking models (e.g. Kimi K2, DeepSeek-R1) return
   reasoning_content which must be echoed back. Without it, APIs return
   400: "thinking is enabled but reasoning_content is missing".

3. shell.go: Fix deny pattern regex for format/mkfs/diskpart to use
   (?:^|\s) instead of \b to avoid matching --format flags.
   Fix path extraction regex to use submatch to avoid matching flags
   like -rf as paths.

4. loop.go: Adjust forceCompression mid-point to avoid splitting
   tool-call/result message pairs, which causes API errors.
2026-03-01 08:44:15 +07:00
yuchou87 077d7c8d9b chore: fix lint issues in mcp and agent packages
- Fix gci import ordering in manager.go, manager_test.go
- Fix gofmt formatting in loop.go, manager.go, mcp_tool.go, mcp_tool_test.go
- Fix gofumpt formatting in manager_test.go
- Fix golines line length issues in manager.go, mcp_tool_test.go
- Fix wastedassign: replace redundant zero-value init with var declaration in loop.go
2026-03-01 08:53:13 +08:00
Artem Yadelskyi b0c8fc4a7e feat(telegram): Fix conflicts 2026-02-28 23:32:15 +02:00
Artem Yadelskyi aeed392c3f Merge branch 'main' into telegram-bot-commands
# Conflicts:
#	pkg/channels/telegram.go
2026-02-28 23:27:26 +02:00
Hoshina c57a9c14e7 docs: sync READMEs, examples, and channel docs to match current config
- Update config.example.json to remove dead webhook_host/webhook_port
  and unused typing/placeholder fields
- Sync all READMEs (en/zh/ja/pt-br/fr/vi) with current channel config
- Update Discord docs: mention_only → group_trigger
- Update LINE, WeCom, WeComApp channel docs
2026-02-28 22:25:37 +08:00
Hoshina e9b4886573 fix(wecom): fix context leak in Start() and data race in processedMsgs
Cancel the constructor-created context before overwriting in Start()
to prevent the original cancel function from becoming unreachable.

Move len(processedMsgs) check inside the write lock to eliminate a
data race, and re-insert the current msgID after map reset to prevent
duplicate processing of the in-flight message.

Applies to both WeComBotChannel and WeComAppChannel.
2026-02-28 22:08:14 +08:00
daming大铭 9c9524f934 Merge pull request #914 from alexhoshina/fix/wecom-context-canceled
fix(wecom): use channel context instead of HTTP request context for async message processing
2026-02-28 21:56:17 +08:00
Hoshina 8e06e2adbd fix(wecom): initialize context in constructors to prevent nil panic in tests
The ctx field was only set in Start(), so tests calling handleMessageCallback
without Start() caused a nil pointer dereference in MessageBus.PublishInbound.
2026-02-28 21:45:08 +08:00
Hoshina 62f59f76e3 fix(wecom): use channel context instead of HTTP request context for async message processing
The HTTP request context is canceled as soon as the handler returns the
response, causing PublishInbound to fail with "context canceled" when
processMessage runs asynchronously in a goroutine. Use the channel's
long-lived context (c.ctx) instead.
2026-02-28 21:31:08 +08:00
afjcjsbx b88e590c6c moved fetch limit bytes in config file 2026-02-28 13:34:33 +01:00
lxowalle 8207c1c7e6 Feat/update migrate (#910)
* * update migrate

* * rename handlers to sources

* * delete dead code

* * fix go test error
2026-02-28 19:59:17 +08:00
taorye 27e988c484 feat(tui): Add configurable Launcher and Gateway process management (#909)
- Implement POSIX-specific gateway process management in gateway_posix.go
- Implement Windows-specific gateway process management in gateway_windows.go
- Create a menu system in menu.go for user interaction
- Develop model management functionality in model.go, including adding, deleting, and testing models
- Introduce a style configuration in style.go for consistent UI appearance
- Set up the main application entry point in main.go
- Update go.mod and go.sum to include necessary dependencies for tcell and tview
2026-02-28 19:37:18 +08:00
Guoguo 08599f8736 build: add armv6 support to goreleaser (#905)
Add GOARM=6 targets for both picoclaw and picoclaw-launcher builds
to support older ARM devices like Raspberry Pi Zero/1.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 18:54:23 +08:00
Guoguo 5e028a847c feat: add picoclaw-launcher with web UI for configuration and gateway management (#904)
A standalone web-based tool for managing picoclaw configuration, OAuth
authentication providers, and gateway process lifecycle. Features include
a sidebar layout with i18n (en/zh) and theme support, real-time gateway
log streaming, startup prerequisites checks, and Windows icon embedding.

Co-authored-by: wj-xiao <meetwenjie@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 18:38:38 +08:00
Mauro 172e6ebe5f fix(exec) fail close on invalid deny pattern (#781)
* fix(exec) fail close on invalid deny pattern

* fix: error check

* resolve conflicts
2026-02-28 16:24:26 +08:00
wenjie 6c8866de6f fix: propagate error when no channels are enabled during startup (#897) 2026-02-28 16:04:44 +08:00
yuchou87 257b0d82b5 Merge branch 'main' into mcp-tools-support
# Conflicts:
#	go.mod
#	go.sum
#	pkg/agent/loop.go
#	pkg/config/config.go
2026-02-28 15:55:25 +08:00
daming大铭 feee0da945 Merge pull request #884 from alexhoshina/fix/memory-leak-whatsapp-reasoning
Fix/memory leak whatsapp reasoning
2026-02-28 14:28:44 +08:00
Hoshina 871b2d7342 fix(whatsapp_native,agent): fixes for resource leak and log noise
- WhatsApp Start(): use deferred cleanup to nil out c.client/c.container
  and disconnect/close resources on any error after struct fields are
  assigned, preventing stale references and double-close in Stop()
- handleReasoning: treat bus.ErrBusClosed as an expected condition
  (DEBUG level) alongside context timeout/cancel, avoiding WARN noise
  during normal shutdown
2026-02-28 14:13:27 +08:00
daming大铭 8529abbc91 Merge pull request #681 from dimensi/bugfix/falsy-context-deadline
fix: distinguish network timeouts from context window errors
2026-02-28 14:12:09 +08:00
Hoshina 7f425f1d11 fix(agent): correct misspelling of 'canceled' 2026-02-28 13:00:40 +08:00
Hoshina d1b10a0004 fix(whatsapp_native,agent): address second round of review feedback
- WhatsApp Send(): detect unpaired state (Store.ID == nil) and return
  ErrTemporary instead of attempting to send while QR login is pending
- handleReasoning: check the returned error type (DeadlineExceeded /
  Canceled) instead of ctx.Err() to decide log level, so pubCtx
  timeouts on a full bus are correctly classified as expected
- Test: fill bus with a short-timeout loop instead of hardcoding the
  buffer size (64), making the test resilient to buffer size changes
2026-02-28 12:54:09 +08:00
Hoshina fc28c2660a fix(whatsapp_native): close TOCTOU race between eventHandler and Stop
Move the stopping check and wg.Add(1) inside reconnectMu in
eventHandler, and set the stopping flag under the same lock in Stop().
This makes the two operations atomic with respect to each other,
preventing the race where:
1. eventHandler checks stopping (false)
2. Stop() sets stopping=true and enters wg.Wait() (wg is 0)
3. eventHandler calls wg.Add(1) → panic or goroutine leak
2026-02-28 12:38:07 +08:00
Petrichor 62bdece7f5 chore: resolve conflicts with upstream/main 2026-02-28 12:21:54 +08:00
Hoshina 9b80fdf885 fix(whatsapp_native,agent): address PR #884 review feedback
- Use c.runCtx for GetQRChannel so the QR producer is canceled on Stop()
- Add atomic stopping guard to prevent wg.Add/wg.Wait race in eventHandler
- Make Stop() context-aware: disconnect client before waiting, respect ctx deadline
- Reduce reasoning publish log noise: use debug level for expected ctx errors
- Add test for handleReasoning when outbound bus is full (timeout path)
2026-02-28 03:05:23 +08:00
afjcjsbx a9a307584b fix: max payload size in web fetch 2026-02-27 18:56:02 +01:00
Hoshina 1d0220f9fd fix(agent): prevent reasoning goroutine accumulation on full bus
Add a 5-second timeout to handleReasoning's PublishOutbound call so
fire-and-forget goroutines do not block indefinitely when the outbound
bus channel is full. Reasoning output is best-effort; on timeout the
publish is abandoned with a warning log instead of holding the
goroutine alive.

Fixes goroutine leak introduced in #802.
2026-02-28 01:39:17 +08:00
Hoshina c7d75a18f8 fix(whatsapp_native): fix goroutine and resource leak in Start/Stop lifecycle
- Move runCtx/runCancel creation before event handler registration and
  QR loop so Stop() can cancel at any point during startup
- Replace blocking QR event loop in Start() with a background goroutine
  that selects on runCtx.Done(), preventing Start() from hanging
  indefinitely when waiting for QR scan
- Track all background goroutines (QR handler, reconnect) with
  sync.WaitGroup; Stop() waits for them to finish before releasing
  client/container resources
- Cancel runCtx on error paths in Start() to avoid leaked contexts

Fixes resource leak introduced in #655.
2026-02-28 01:39:06 +08:00
美電球 cdbc9c4bd6 Merge branch 'sipeed:main' into main 2026-02-28 01:28:39 +08:00
daming大铭 2f4f45080b Merge pull request #882 from sipeed/fix/issue#565
Update config file reference from config.yaml to config.json
2026-02-28 00:41:23 +08:00
美電球 ebfa72a286 Update config file reference from config.yaml to config.json
Closes: #565
2026-02-28 00:36:39 +08:00
daming大铭 1211218b60 Merge pull request #881 from mosir/fix/onboard-include-empty-model
fix(config): keep empty agents.defaults.model in saved config
2026-02-28 00:28:02 +08:00
daming大铭 70fcbc5700 Merge pull request #824 from 0xYiliu/fix/issue-783-fallback-alias-resolution
fix: resolve fallback model alias parsing for issue #783
2026-02-28 00:25:18 +08:00
mosir 1161aee872 fix(config): keep empty agents.defaults.model in saved config 2026-02-28 00:18:10 +08:00
daming大铭 5b96923d66 Merge pull request #877 from sipeed/refactor/channel-system
Refactor/channel system
2026-02-28 00:08:36 +08:00
Petrichor f2a71ca824 fix(lint): format imports in agent_id_test 2026-02-27 21:20:56 +08:00
美電球 c119e0d5e8 Merge pull request #655 from adityakalro/main
Added a native WhatsApp channel implementation.
2026-02-27 20:25:59 +08:00
美電球 f6c275f70c Fix formatting of WhatsAppConfig struct fields 2026-02-27 20:25:02 +08:00
美電球 75a86ebe03 Resolve merge conflict in config.example.json
Removed conflicting lines and kept 'allow_from' as an empty array.
2026-02-27 20:23:20 +08:00
美電球 6b427afa44 Remove ignored files from .gitignore 2026-02-27 20:22:40 +08:00
美電球 67e1dab408 Update test case for unicode letters preservation 2026-02-27 20:20:58 +08:00
美電球 3ad937f05b Update function comment for SanitizeMessageContent 2026-02-27 20:20:25 +08:00
美電球 6fcc80bf44 Reformat WhatsAppConfig struct fields alignment 2026-02-27 20:20:00 +08:00
Hoshina 7276a2d651 Fix lint errors 2026-02-27 20:15:21 +08:00
美電球 fa68023ac2 Merge branch 'refactor/channel-system' into main 2026-02-27 20:04:08 +08:00
daming大铭 90e49bc671 Merge pull request #802 from biisal/reasoning-chnl
Fix Reasoning Content Being Silently Dropped by Adding Channel-Aware Reasoning Routing #645
2026-02-27 18:51:48 +08:00
Hoshina d429dcdd76 chore: fix go fmt formatting issues after rebase
Apply go fmt to files that had formatting inconsistencies
(alignment, indentation) after rebasing onto refactor/channel-system.
2026-02-27 17:35:50 +08:00
Avisek Ray b1a6b3898d Merge branch 'sipeed:main' into reasoning-chnl 2026-02-27 14:50:14 +05:30
lxowalle 29d4019e62 fix: set max tokens to 32k, default model to null (#858)
* * Set default value of max tokens to 32768

* * set max tokens to 32k, default model to null

* * Fix format error
2026-02-27 17:02:25 +08:00
Avisek f96cf3f8cc feat: Add reasoning_channel_id to communication platform configurations and improve message bus context cancellation handling. 2026-02-27 16:58:42 +08:00
Avisek 9f95aad5f3 feat: Introduce LLM reasoning fields to LLM responses and enable routing reasoning output to dedicated channels. 2026-02-27 16:58:42 +08:00
Petrichor 222d1a3086 refactor(modernize): apply safe modernize fixes 2026-02-27 16:35:07 +08:00
lxowalle b6927c9a7a Prompt to modify the max_tool_iterations parameter. (#855)
* Prompt to modify the max_tool_iterations parameter.

* fix typo and set max iterations to 50
2026-02-27 15:42:47 +08:00
nayihz b5a4bb28b6 feat(discord): add proxy support and tests 2026-02-27 14:42:28 +08:00
lxowalle a91a4e5978 * update wechat qrcode & delete unused mp4 file (#852) 2026-02-27 14:36:26 +08:00
Aditya Kalro 42ee9ab1e3 Complete the whatsapp native channel implementation based on the new channel interface 2026-02-26 22:35:52 -08:00
Aditya Kalro a8644ca1c5 Refactor whatsapp native channel based on the new channel interface 2026-02-26 22:35:20 -08:00
daming大铭 2c8416e658 Merge pull request #842 from sipeed/revert-767-update-wechat-group
docs: update wechat qrcode
2026-02-27 10:59:05 +08:00
Guoguo 7592ccdab2 Revert "docs: update wechat qrcode (#767)"
This reverts commit d1d19b12ce.
2026-02-27 10:52:45 +08:00
Meng Zhuo 69e5b619cb Merge pull request #706 from mosir/fix/atomic-file-writes
refactor(pkg/utils): add unified atomic file write utility
2026-02-27 10:42:30 +08:00
Guoguo a5c8179fa8 chore(docker): reorganize docker files and add first-run entrypoint (#812)
* chore(docker): move Dockerfile into docker/ directory

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(docker): add entrypoint script to goreleaser Dockerfile

- entrypoint.sh: on first run (config and workspace both absent) runs
  picoclaw onboard then exits for the user to configure; subsequent
  starts exec picoclaw gateway directly
- Dockerfile.goreleaser: copy and use entrypoint.sh, run as root
- .goreleaser.yaml: update dockerfile path, add entrypoint.sh to
  extra_files so it is included in the docker build context

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(docker): update docker-compose to use pre-built image and bind mount

- Use docker.io/sipeed/picoclaw:latest instead of building locally
- Replace named volume with bind mount ./data:/root/.picoclaw
- Move docker-compose.yml into docker/ directory

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs: update Docker Compose section to reflect new docker/ layout

- Use docker compose -f docker/docker-compose.yml for all commands
- Update setup flow: first run generates docker/data/config.json,
  container exits, user edits config, then restarts
- Replace "Rebuild" section with "Update" (docker pull) since the
  compose file now uses the pre-built sipeed/picoclaw image
- Apply same changes to README.zh.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(docker): use restart: on-failure to prevent restart after first-run setup

unless-stopped restarts the container regardless of exit code, causing
an infinite loop when entrypoint exits 0 after the initial onboard.
on-failure only restarts on non-zero exit (i.e. crashes), so the
container stays stopped after setup until the user restarts it manually.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs: sync Docker Compose section across all language READMEs

Apply the same updates as the English/Chinese READMEs:
- Use docker compose -f docker/docker-compose.yml for all commands
- Update setup flow to first-run auto-config pattern
- Replace build/rebuild section with update via docker pull
- Affected: README.fr.md, README.ja.md, README.pt-br.md, README.vi.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 10:10:17 +08:00
Hoshina 779e4dfc38 docs(channels): add English README for channel system architecture 2026-02-27 03:33:21 +08:00
Hoshina e5788e7f95 docs(channels): add Chinese README for channel system architecture 2026-02-27 03:33:21 +08:00
Hoshina 29ed650107 feat(channels): auto-orchestrate Placeholder/Typing/Reaction via capability interfaces
Define PlaceholderCapable, TypingCapable, and ReactionCapable interfaces
and have BaseChannel.HandleMessage auto-detect and trigger all three as
independent pipelines on inbound messages. This replaces the scattered
manual orchestration code in each channel's handleMessage with a single
unified dispatch in the framework layer.

Changes:
- Add PlaceholderCapable interface to interfaces.go
- Add ReactionCapable + RecordReactionUndo to interfaces.go
- BaseChannel.HandleMessage auto-triggers Typing → Reaction → Placeholder
- Manager gains reactionUndos sync.Map with TTL janitor cleanup
- Telegram: extract SendPlaceholder from manual code, add StartTyping
- Discord: add SendPlaceholder + StartTyping
- Pico: add SendPlaceholder (uses Pico Protocol message.create)
- Slack: extract ReactToMessage from manual code
- OneBot: extract ReactToMessage, remove leaked pendingEmojiMsg sync.Map
- LINE: move group-chat guard into StartTyping, remove manual orchestration
- Config: add Placeholder to PicoConfig; remove from Slack/LINE/OneBot
  (no MessageEditor, so placeholder config was dead code)
2026-02-27 03:33:21 +08:00
mosir b8c0d136c8 Merge branch 'sipeed:main' into fix/atomic-file-writes 2026-02-27 00:21:27 +08:00
Yiliu 99582bbd91 docs: add issue 783 investigation and execution plan 2026-02-26 23:50:48 +08:00
Yiliu 3a3862340a fix(agent): resolve fallback model aliases from model_list 2026-02-26 23:50:40 +08:00
Yiliu fb96645ea9 fix(providers): support lookup-based fallback candidate resolution 2026-02-26 23:50:33 +08:00
Hoshina ba98069a00 fix: resolve wastedassign lint warnings in channel subpackages
Remove wasted initial assignments before switch statements in
onebot (segType), telegram (filename), and wecom (mediaType).
2026-02-26 23:36:06 +08:00
Hoshina 35a035bdda fix: port main branch changes to channel subpackages after rebase
Port changes that were applied to the old pkg/channels/*.go files on main
to their new locations in channel subpackages:
- telegram: precompile regex, var transcribedText, GetModelName()
- discord: var transcribedText declaration
- onebot: resp.Body.Close(), "canceled" spelling, remove empty line
- slack: named return values in parseSlackChatID
- wecom: remove sendMarkdownMessage dead code
- whatsapp: resp.Body.Close() after Dial
- gateway/helpers: remove unused errors import
2026-02-26 23:24:35 +08:00
Hoshina 1d4fe4652a fix(bus): increase message bus buffer size from 16 to 64
Prevents potential backpressure under load when multiple channels
publish concurrently in gateway mode, where SDK callbacks blocking
on a full buffer can cause message loss or timeouts.
2026-02-26 22:46:57 +08:00
daming大铭 3584c0c7be Merge pull request #766 from penzhan8451/main
fix: openrouter in providers and modellist length is greater than 0
2026-02-26 22:25:14 +08:00
ex-takashima 0a7c929905 fix(media): separate import groups for gci linter
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 22:45:59 +09:00
ex-takashima 94aa2b1788 fix(media): use project logger and harden map cleanup
- Replace stdlib log.Printf with logger.InfoCF/WarnCF for consistency
  with the rest of the codebase (addresses @nikolasdehor review point #3)
- ReleaseAll: clean refToScope/refs mappings even if refs entry is missing
- CleanExpired: guard refToScope lookup before scope cleanup
- Add TestReleaseAllCleansMappingsIfRefsMissing for robustness

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 22:39:58 +09:00
mosir 433af435a9 style: fix gci import grouping in config, cron, and skills installer 2026-02-26 20:38:11 +08:00
mosir d88700971f merge: resolve conflicts with main 2026-02-26 20:29:24 +08:00
penglp a161bf9e24 Merge branch 'main' of https://github.com/sipeed/picoclaw 2026-02-26 19:29:32 +08:00
ex-takashima 61eae92b38 fix(line): log loading refresh errors, skip typing without recorder
Address review feedback from @alexhoshina and Codex:
- Log sendLoading errors in ticker goroutine instead of discarding
- Only start typing indicator when PlaceholderRecorder is available
  to avoid wasted API calls and unnecessary goroutine creation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 20:29:09 +09:00
ex-takashima e268ea82b9 Revert "feat(line): add StartTyping and PlaceholderRecorder integration"
This reverts commit ad736d71cb.
2026-02-26 20:17:45 +09:00
mattn 8a1fb03974 Perf/precompile regex (#687)
* perf: pre-compile regexes at package level

Move regexp.MustCompile calls from inside methods to package-level
variables in web.go (7 regexes) and loader.go (2 regexes).
This avoids repeated compilation on every invocation.

Amp-Thread-ID: https://ampcode.com/threads/T-019c79c3-ea1c-7471-b09d-be90ba0e1ca0
Co-authored-by: Amp <amp@ampcode.com>

* perf: pre-compile regexes at package level

* retain the helpful comment

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-02-26 20:44:03 +11:00
ztechenbo cb3191c8c1 build: support armv81 arch in Makefile (#776)
Co-authored-by: 陈波0668000637 <chen.bo222@xydigit.com>
2026-02-26 17:41:01 +08:00
daming大铭 b1c61cd8df Merge pull request #808 from alexhoshina/config/change-default-dm-scope-to-per-channel-peer
config: change default dm_scope to per-channel-peer
2026-02-26 17:39:16 +08:00
ian f3c1162001 feat(skills): add retry for HTTP requests in skill installer (#261)
* feat(skills): add retry mechanism for HTTP requests

Implement a retry mechanism with exponential backoff for HTTP requests in the skill installer. This improves reliability when fetching skills from GitHub by automatically retrying failed requests up to 3 times.

Add comprehensive tests to verify retry behavior under different scenarios including success on different attempts and proper delay between retries.

* fix: improve http request retry logic with status code checks

Add shouldRetry helper function to determine retryable status codes.
Close response body between retry attempts and break early for non-retryable status codes.

* refactor: remove unused BuiltinSkill struct

The struct was not being used anywhere in the codebase, so it's safe to remove it to reduce clutter and improve maintainability.

* refactor(http): move retry logic to utils package

Extract HTTP retry functionality from skills package to utils for better reusability
Add context-aware sleep function and comprehensive tests

* refactor(http): extract retry delay unit to variable

Extract hardcoded retry delay unit to a variable for better testability and flexibility. Update tests to use milliseconds for faster execution while maintaining the same behavior.

* test(http_retry): remove t.Parallel from test cases

* test(http_retry): remove redundant test cases for retry success

The removed test cases for success on second and third attempts were redundant since the retry logic is already covered by other tests. This simplifies the test suite while maintaining coverage.
2026-02-26 20:35:26 +11:00
Hoshina 21654f1335 config: change default dm_scope to per-channel-peer
Change the default value of session.dm_scope from "main" to
"per-channel-peer" to provide better conversation isolation by
default. This prevents context leakage between different users
and channels.
2026-02-26 16:51:18 +08:00
xiaoen e810331dd8 fix(memory): use SetHistory in migration for crash idempotency
MigrateFromJSON previously called AddFullMessage in a loop, then
renamed the .json file to .json.migrated. If the process crashed
after appending some messages but before the rename, a retry would
re-read the same .json and append all messages again — duplicating
whatever was written before the crash.

Switch to SetHistory which atomically replaces the session contents.
A retry after crash overwrites the partial data instead of appending.
2026-02-26 16:15:11 +08:00
xiaoen 9c72317b9b fix(memory): write meta before JSONL rewrite for crash safety
In SetHistory and Compact, the JSONL file was rewritten before updating
the meta file. If the process crashed between the two writes, the meta
still had a large Skip value pointing past the now-shorter JSONL file,
causing GetHistory to return empty — effectively data loss.

Reverse the order: write meta (with Skip=0) first, then rewrite JSONL.
On crash between the two writes, the old uncompacted file is still
intact and GetHistory reads from line 1, returning stale-but-complete
data. The next operation self-corrects.
2026-02-26 16:13:57 +08:00
xiaoen 1f0b85280a fix(memory): always reconcile line count in TruncateHistory
A crash between the JSONL append and the meta update in addMsg can
leave meta.Count stale (e.g. file has 101 lines but meta says 100).
The previous code only reconciled when Count==0, so a nonzero stale
count was silently trusted, causing keepLast/skip to be calculated
against the wrong total.

Now TruncateHistory always counts the actual lines on disk. This is
cheap (scan without unmarshal) and TruncateHistory is not a hot path.
2026-02-26 16:12:34 +08:00
Yiliu 438f764c7a fix(providers): support per-model request_timeout in model_list (#733)
* fix(providers): support per-model request_timeout in model_list

* fix(lint): format provider constructors for golines

* refactor(providers): adopt functional options and preserve timeout migration

* docs(readme): sync request_timeout guidance across translated docs

---------

Co-authored-by: Yiliu <yiliu@affiliate-guide.com>
2026-02-26 19:08:19 +11:00
Guoguo 6a4116b8a0 ci: fix go generate not running in subdirectories (#807)
Changed `go generate ./cmd/picoclaw` to `go generate ./cmd/picoclaw/...`
so that the workspace embed in cmd/picoclaw/internal/onboard is correctly
generated before building.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 19:03:58 +11:00
ex-takashima e450e9e053 feat(line): add StartTyping and PlaceholderRecorder integration
Implement TypingCapable interface for LINE channel using the
loading animation API (1:1 chats only, no group support).

- Add StartTyping() with 50s periodic refresh and context-based stop
- Integrate PlaceholderRecorder.RecordTypingStop in processEvent
- Skip RecordPlaceholder (LINE has no message edit API)
- Change sendLoading to accept context and return error
- Relax callAPI status check from 200 to 2xx range

Design consulted with Codex (GPT-5.2).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 16:43:41 +09:00
xiaoen d55e5540af fix(memory): bound lock memory and increase scanner buffer
Address feedback from @yinwm for long-running daemon use:

- Replace sync.Map with a fixed-size sharded lock array (64 mutexes).
  Keys are mapped via FNV hash, so memory is O(1) regardless of how
  many sessions are created over the process lifetime.

- Increase scanner buffer cap from 1 MB to 10 MB. Tool results
  (read_file on large files, web search responses) can easily exceed
  1 MB. The scanner still starts at 64 KB and only grows as needed.
2026-02-26 15:35:04 +08:00
ex-takashima d804f9cb3f fix(media): guard Interval<=0 panic, two-phase ReleaseAll
Address Codex (GPT-5.2) review feedback:
- Start: guard against Interval<=0 or MaxAge<=0 to prevent
  time.NewTicker panic on misconfiguration
- ReleaseAll: split into two phases (collect under lock, delete
  after unlock) matching CleanExpired pattern
- ReleaseAll: log file removal errors
- Add TestStartZeroIntervalNoPanic and TestStartZeroMaxAgeNoPanic

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 16:33:32 +09:00
ex-takashima b705e58528 fix(media): address review comments on TTL cleanup
- CleanExpired: split into two phases — collect expired entries under
  lock, then delete files after releasing the lock to minimize contention
- CleanExpired: guard against zero MaxAge (no-op if unconfigured)
- CleanExpired: log file removal errors instead of silently ignoring
- Start: protect with startOnce to prevent multiple goroutines
- Stop: rename once -> stopOnce for clarity
- cmd_gateway: call mediaStore.Stop() on error path after Start()
- Add TestCleanExpiredZeroMaxAge and double-Start test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 16:22:49 +09:00
Vinh Tran 2580ef31ca Merge remote-tracking branch 'origin/main' into feat/searxng 2026-02-26 08:21:09 +01:00
Guoguo a5cc4db514 ci: remove version from rpm and deb file name (#804)
Signed-off-by: Guoguo <i@qwq.trade>
2026-02-26 14:53:10 +08:00
xiaoen 5d73ee2d9a refactor(memory): use sync.Map for session locks and skip-scan in readMessages
Address review feedback from @Zhaoyikaiii:

- Replace map[string]*sync.Mutex + separate mu with sync.Map.LoadOrStore
  for simpler, lock-free session lock management.

- Add skip parameter to readMessages so callers (GetHistory, Compact)
  can skip truncated lines without paying the json.Unmarshal cost.

- Add countLines helper for TruncateHistory's count reconciliation,
  avoiding full deserialization when only the line count is needed.
2026-02-26 14:31:02 +08:00
mosir 16a1c96e40 Merge branch 'sipeed:main' into fix/atomic-file-writes 2026-02-26 13:50:55 +08:00
美電球 95b246f5a0 Merge pull request #790 from rordd/fix/gemini-prompt-cache-key
fix: exclude prompt_cache_key for Gemini API requests
2026-02-26 13:17:28 +08:00
Aditya Kalro 49612adf8a Rebuilt after the refactoring of the base channel implementation. 2026-02-25 20:50:40 -08:00
lxowalle 8f606733a2 fix: hide compressed historical messages notification (#799) 2026-02-26 12:36:19 +08:00
mosir be4b8fa684 Merge branch 'sipeed:main' into fix/atomic-file-writes 2026-02-26 12:14:44 +08:00
lxowalle 851920d4b0 docs: fix readme typo (#798) 2026-02-26 11:54:05 +08:00
daming大铭 f24407672b Merge pull request #768 from avaksru/main
add support for 32-bit ARM (ARMv7) builds
2026-02-26 11:49:08 +08:00
penglp 78ba0575b7 Merge branch 'main' of https://github.com/sipeed/picoclaw 2026-02-26 10:47:35 +08:00
xiaoen b464687e2f feat(memory): add Compact method for physical JSONL compaction
Address file growth concern from #711 review: logical truncation via
skip offset is fast but leaves dead lines on disk indefinitely.

Compact() rewrites the JSONL file keeping only active messages, using
the same temp+rename pattern for crash safety. No-op when skip == 0.
The caller (lifecycle manager or agent loop) decides when to trigger
compaction — e.g. when skipped lines exceed active lines.
2026-02-26 08:42:35 +08:00
xiaoen 903681207b feat(memory): support migration from legacy JSON sessions
Read existing sessions/*.json files, convert to JSONL format, and
rename originals to .json.migrated as backup. The migration is
idempotent — second runs skip already-migrated files.

Session keys are read from JSON content (not filenames) so that
sanitized names like telegram_123 correctly map back to telegram:123.
2026-02-26 08:35:05 +08:00
xiaoen 529622b7d3 test(memory): add unit, concurrency, and benchmark tests
Cover all Store interface methods plus edge cases:
- Basic roundtrip, ordering, empty session, tool calls
- Logical truncation (keep last N, keep zero, keep more than exist)
- SetHistory replacing all + resetting skip offset
- Crash recovery with partial JSON lines
- Persistence across store instances
- Concurrent add+read (10 goroutines x 20 msgs)
- Simulated #704 race (summarizer vs main loop)
- Benchmarks for AddMessage and GetHistory (100/1000 msgs)
2026-02-26 08:35:04 +08:00
xiaoen 9f36e50807 feat(memory): implement append-only JSONL session store
Add JSONLStore that persists sessions as .jsonl files (one message per
line) plus .meta.json for summary and truncation offset.

Key design decisions:
- Append-only writes — no full-file rewrites on AddMessage
- Logical truncation via skip offset instead of physical deletion
- Per-session mutex for safe concurrent access
- Crash recovery: malformed trailing lines are silently skipped
- Atomic metadata writes using temp+rename

Zero new dependencies — pure stdlib.

Refs #711
2026-02-26 08:35:04 +08:00
xiaoen 32ec8cadeb feat(memory): define Store interface for session persistence
Introduce a backend-agnostic Store interface in pkg/memory/ that maps
one-to-one with the current SessionManager API. Each method is atomic
— no separate Save() call needed.

Refs #711
2026-02-26 08:35:04 +08:00
임창욱 ea902429f2 fix: exclude prompt_cache_key for Gemini API requests
Gemini's OpenAI-compat endpoint rejects unknown fields.
Only send prompt_cache_key to OpenAI-native endpoints.
2026-02-25 14:48:51 -08:00
mosir 87e674ba15 Merge branch 'sipeed:main' into fix/atomic-file-writes 2026-02-25 23:44:27 +08:00
daming大铭 094d65916d Merge pull request #779 from wgjtyu/main
add proxy support for TavilySearchProvider
2026-02-25 23:41:16 +08:00
George Wang ef1989f12e Update pkg/tools/web.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-25 23:04:41 +08:00
George Wang c8a553f109 add proxy support for TavilySearchProvider 2026-02-25 22:37:05 +08:00
mosir 6e754a86f3 merge: resolve conflicts with main 2026-02-25 21:53:04 +08:00
avaksru 162f38cd4f fix Code Review: PR #768 2026-02-25 16:29:04 +03:00
Zhaoyikaiii 740cdcaeaf fix: remove redundant tools definitions from system prompt (#771)
* fix: remove redundant tools definitions from system prompt

Tools are already provided to the LLM via JSON schema through
ToProviderDefs(), so the text-based tools section in the system
prompt is redundant.

This removes the buildToolsSection() logic and the tools field
from ContextBuilder, reducing system prompt length while maintaining
the "ALWAYS use tools" rule reminder.

Fixes #731

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: correct spelling 'initialized' (was 'initialised')

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-25 20:44:07 +08:00
daming大铭 53578da51b Merge pull request #617 from Zhaoyikaiii/fix/repeated-context-reprocessing
fix: implement caching for system prompt to avoid repeated context reprocessing (fixes #607)
2026-02-25 19:51:23 +08:00
Guoguo d1d19b12ce docs: update wechat qrcode (#767)
Signed-off-by: Guoguo <i@qwq.trade>
2026-02-25 22:19:55 +11:00
daming大铭 f7fc8bb6e9 Merge pull request #770 from xiaket/ci-golangci-cleanup
(ci) golangci cleanup
2026-02-25 19:18:11 +08:00
daming大铭 9c7933dd00 Merge pull request #730 from winterfx/main
fix: multi-tool-call results incorrectly dropped causing LLM API 400
2026-02-25 19:12:03 +08:00
Kai Xia 9be1cd6277 a moved case of nakedret
Signed-off-by: Kai Xia <kaix+github@fastmail.com>
2026-02-25 21:32:57 +11:00
Kai Xia b190e6e910 enable whitespace
Whitespace is a linter that checks for unnecessary newlines at the start and end of functions, if, for, etc.

Signed-off-by: Kai Xia <kaix+github@fastmail.com>
2026-02-25 21:31:07 +11:00
Kai Xia d8b164b3d4 enable wastedassign
Finds wasted assignment statements.

Signed-off-by: Kai Xia <kaix+github@fastmail.com>
2026-02-25 21:31:07 +11:00
Kai Xia 6830790692 enable predeclared
Find code that shadows one of Go's predeclared identifiers.

Signed-off-by: Kai Xia <kaix+github@fastmail.com>
2026-02-25 21:31:07 +11:00
Kai Xia 4e6589d51f enable prealloc
Find slice declarations that could potentially be pre-allocated.

Signed-off-by: Kai Xia <kaix+github@fastmail.com>
2026-02-25 21:31:07 +11:00
Kai Xia 09cf8efde6 enable nakedret
Checks that functions with naked returns are not longer than a maximum size (can be zero).

Signed-off-by: Kai Xia <kaix+github@fastmail.com>
2026-02-25 21:31:07 +11:00
Kai Xia c5e8e19f54 enable misspell
Finds commonly misspelled English words.

Signed-off-by: Kai Xia <kaix+github@fastmail.com>
2026-02-25 21:14:19 +11:00
Kai Xia 1fab1967d2 enable goprintffuncname
Checks that printf-like functions are named with `f` at the end.

Signed-off-by: Kai Xia <kaix+github@fastmail.com>
2026-02-25 21:14:19 +11:00
Kai Xia 06daa30e75 enable dogsled
Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()).

Signed-off-by: Kai Xia <kaix+github@fastmail.com>
2026-02-25 21:14:19 +11:00
Kai Xia 95f22bc07b enable bodyclose
Checks whether HTTP response body is closed successfully.

Signed-off-by: Kai Xia <kaix+github@fastmail.com>
2026-02-25 21:14:19 +11:00
Guoguo 974337f4ab ci: add rpm and deb support in goreleaser
Signed-off-by: Guoguo <i@qwq.trade>
2026-02-25 16:32:40 +08:00
Guoguo 43611e2c4e ci: add loongarch64, remove s390x and mips64 support in goreleaser
Signed-off-by: Guoguo <i@qwq.trade>
2026-02-25 16:32:40 +08:00
avaksru f7d487ea30 Enable Docker Hub login in release workflow 2026-02-25 11:02:09 +03:00
avaksru a527976e68 Restore dockers_v2 configuration for picoclaw
Re-enable dockers_v2 configuration for picoclaw with specified details.
2026-02-25 11:01:33 +03:00
Ruslan Semagin 73f27803d4 refactor(cli): migrate to Cobra-based command structure (#429)
* refactor(cli): migrate to Cobra-based command structure

Refactor CLI to use Cobra instead of manual os.Args parsing.

- Introduce root command and structured subcommands under cmd/picoclaw/internal
- Convert agent, auth, cron, gateway, migrate, onboard, skills, status and version to Cobra commands
- Replace manual flag parsing with Cobra flags
- Remove direct os.Args usage from command handlers
- Keep existing command behavior and output semantics

This change focuses on CLI structure and maintainability.
No business logic changes intended.

* chore(cli): remove version2 alias and make cobra a direct dependency

* test(cli): add basic command tests

- Add tests for CLI command tree and flag parsing
- Align LDFLAGS injection path for version info
- Remove unused manual help function

* test: migrate command tests to testify assertions

Replace standard library testing error checks (t.Error*, t.Fatalf)
with assert/require from stretchr/testify across all cobra command tests
for improved readability and consistency.

* fix(cli): make linter happy

* test: avoid duplication in windows config path test

* test: simplify allowed command checks using slices.Contains

* fix(skills): register subcommands during command construction

- Move subcommand registration out of PersistentPreRunE
- Ensure `picoclaw skills <subcommand>` resolves correctly
- Minor install command and test cleanups

* refactor(cli): address review feedback and improve command clarity

* fix(authLogoutCmd): rm os.Exit
2026-02-25 18:47:45 +11:00
Zhaoyikaiii edc78191c9 style: fix gci formatting in protocoltypes/types.go 2026-02-25 15:36:54 +08:00
avaksru 85276057a0 Disable dockers_v2 section in goreleaser config
Comment out dockers_v2 configuration in .goreleaser.yaml
2026-02-25 10:32:50 +03:00
Zhaoyikaiii 1f7cbd9164 fix: cache system prompt with mtime-based auto-invalidation (#607)
Avoid rebuilding the entire system prompt on every BuildMessages() call
by caching the static portion (identity, bootstrap, skills summary,
memory) and only recomputing it when workspace source files change.

Key changes:

- ContextBuilder caches the static prompt behind an RWMutex with
  double-checked locking. Source file changes are detected via cheap
  os.Stat mtime checks so no explicit invalidation is needed.

- Track file existence at cache time (existedAtCache map) so that
  newly created or deleted bootstrap/memory files also trigger a
  rebuild — the old modifiedSince() silently returned false on
  os.IsNotExist.

- Walk the skills directory recursively with filepath.WalkDir to
  catch content-only edits at any nesting depth; directory mtime
  alone misses in-place file modifications on most filesystems.

- ToolRegistry.sortedToolNames() sorts tool names before iteration,
  ensuring deterministic tool definition order across calls — a
  prerequisite for LLM-side prefix/KV cache reuse.

- Merge all context (static + dynamic + summary) into a single
  system message for provider compatibility: the Anthropic adapter
  extracts messages[0] as the top-level system parameter, and Codex
  reads only the first system message as instructions.

- Fix a data race in BuildMessages() where cachedSystemPrompt was
  read without holding the lock in a debug log statement.

- Add tests: single system message invariant, mtime auto-invalidation,
  new-file creation detection, skill file content change, explicit
  InvalidateCache, cache stability, concurrent access (20 goroutines
  x 50 iterations, passes go test -race), and a benchmark.
2026-02-25 15:27:45 +08:00
avaksru 7de75192b8 Disable Docker Hub login in release.yml
Comment out Docker Hub login steps in release workflow.
2026-02-25 10:10:44 +03:00
avaksru 14cb16f113 Add goarm versions for ARM architecture in config 2026-02-25 10:02:15 +03:00
avaksru 19c6890807 Add ARMv7 build target to Makefile 2026-02-25 09:46:42 +03:00
penglp 81c8c07b79 fix:Openrouter in providers and modellist 2026-02-25 14:44:49 +08:00
Hoshina c241c55a0d fix(channels): address PR #734 review comments
Rename fastID() to uniqueID() with a security caveat comment clarifying
the ID is not cryptographically secure, and add unit tests for the
refactored index-based split helper functions.
2026-02-25 11:51:21 +08:00
Achton Smidt Winther ec6da7a530 fix: reject empty task in spawn tool (#740)
The spawn tool accepts empty strings as valid task arguments, which
causes a subagent to run with no meaningful work. The subagent's
completion message is then routed back to the originating channel
(e.g. Signal, Discord), where the main agent processes it and may
hallucinate an unrelated response that gets sent to users.

Validate that the task parameter is non-empty after trimming whitespace.

Related: #545

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 07:39:49 +11:00
Nikita Nafranets a4b6cea103 fix: distinguish network timeouts from context window errors
HTTP timeouts (context deadline exceeded, Client.Timeout) were
incorrectly classified as context window errors, triggering useless
history compression. Replace broad substring checks ("context",
"token", "length") with specific patterns for real context limit
errors and explicitly exclude timeout errors from that path.

Additionally, timeout errors were not retried at all — the retry
loop only handled context window errors. Now timeouts are retried
up to 2 times with exponential backoff (5s, 10s).
2026-02-24 21:54:44 +03:00
mosir f86de3cbb9 Merge remote-tracking branch 'sipeed/main' into fix/atomic-file-writes 2026-02-25 00:03:55 +08:00
mosir 7a2d353d0f Merge branch 'main' of github.com:mosir/picoclaw into fix/atomic-file-writes 2026-02-24 23:59:05 +08:00
mosir 11996f1a0b refactor(pkg): move atomic file write to dedicated fileutil package 2026-02-24 23:57:13 +08:00
daming大铭 fd26fa7459 Merge pull request #587 from nayihz/feat_webtool_proxy
feat: add HTTP proxy support for web tools
2026-02-24 23:51:01 +08:00
mosir 4aed3591e7 refactor(pkg/utils): improve WriteFileAtomic with stronger durability guarantees 2026-02-24 23:49:40 +08:00
daming大铭 eb138a3f13 Merge pull request #642 from Lixeer/main
fix: better session management for `github_copilot_provider`
2026-02-24 23:18:14 +08:00
Lixeer d09c64fcee fix: implement code review suggestions
Address all feedback from PR review:
- Lock granularity
- Empty response handling
- Shutdown race condition
- Interface naming
2026-02-24 22:33:04 +08:00
Hoshina 72e897f95a fix(channels): fix memory hazards in channel abstraction layer
Address 7 memory/architecture issues affecting long-running gateway
processes on embedded devices (<10MB RAM):

- Fix dispatcher busy-wait: remove select+default pattern that caused
  CPU spin after context cancellation; SubscribeOutbound handles ctx
  internally
- Add TTL janitor for typingStops/placeholders sync.Map entries to
  prevent unbounded accumulation when outbound paths fail
- Reduce queue buffers from 100 to 16 slots (~84% memory reduction)
- Optimize SplitMessage with index-based rune operations to reduce
  intermediate string/rune allocations
- Replace uuid.New() with atomic counter + random prefix for media
  scope IDs (eliminates per-call crypto/rand syscall)
- Lazy channel worker creation: defer goroutine+buffer allocation
  until channel.Start() succeeds
2026-02-24 22:30:22 +08:00
Kai Xia(夏恺) 100356e8ec refactor: cleanup dead code and turn on dead code detection in CI (#515)
* cleanup dead code.

Signed-off-by: Kai Xia <kaix+github@fastmail.com>

* add these two back with flag.

Signed-off-by: Kai Xia <kaix+github@fastmail.com>

* fix ci

Signed-off-by: Kai Xia <kaix+github@fastmail.com>

* remove this confusing line

Signed-off-by: Kai Xia <kaix+github@fastmail.com>

* make fmt

Signed-off-by: Kai Xia <kaix+github@fastmail.com>

* remove unused method.

picked up by golangci-lint run

Signed-off-by: Kai Xia <kaix+github@fastmail.com>

---------

Signed-off-by: Kai Xia <kaix+github@fastmail.com>
2026-02-24 21:52:25 +08:00
winterfx b47a39af9c fix: handle multi-tool-call orphan detection in sanitizeHistoryForProvider
Walk backwards over preceding tool messages to find the nearest assistant
with ToolCalls, instead of only checking the immediate predecessor. Add
unit tests for sanitizeHistoryForProvider covering key edge cases.
2026-02-24 21:35:15 +08:00
ex-takashima 4ada4063d7 feat(media): integrate TTL cleanup into FileMediaStore
Add background TTL-based cleanup (L2 safety net) directly into
FileMediaStore so file deletion and in-memory ref removal happen
atomically under the same mutex, preventing dangling references.

- Add storedAt timestamp and refToScope reverse map to mediaEntry
- Add CleanExpired() for atomic TTL-based expiration
- Add Start()/Stop() for background goroutine lifecycle
- Add MediaCleanupConfig (enabled, max_age, interval) to config
- Wire up in cmd_gateway.go with config-driven defaults
- Add 8 new tests including concurrent cleanup safety

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 21:24:32 +09:00
daming大铭 18ba88869a Merge pull request #722 from ihao/main
fix: add CGO_ENABLED=0 for static build to fix cross-platform GLIBC e…
2026-02-24 19:53:38 +08:00
daming大铭 b10555cad2 Merge pull request #726 from xiaket/devx-make-improvements
DevX: minor improvements in Makefile
2026-02-24 19:46:36 +08:00
daming大铭 9cc0f8e685 Merge pull request #724 from mqyang56/fix/model-list-default-value-leak
fix: prevent DefaultConfig template values from leaking into user model_list entries
2026-02-24 19:43:23 +08:00
daming大铭 d20cb364a2 Merge pull request #677 from yinwm/refactor/model-to-model-name
refactor(config): rename model field to model_name
2026-02-24 19:05:40 +08:00
yinwm 01e2354b97 fix: align map values for proper formatting 2026-02-24 19:04:38 +08:00
Kai Xia 78e5bdad29 minor improvements in Makefile
Signed-off-by: Kai Xia <kaix+github@fastmail.com>
2026-02-24 22:04:23 +11:00
esubaalew 89bc7aaea5 fix: add generate dependency to test and vet Makefile targets
make test and make vet fail on a fresh clone because the go:embed
workspace directory does not exist until go generate runs. The build
target already depends on generate, but test and vet did not.

Also fixes the test target comment which incorrectly read '## fmt: Format Go code'.
2026-02-24 13:42:04 +03:00
yangmanqing 0d761ca608 fix: prevent DefaultConfig template values from leaking into user model_list entries 2026-02-24 17:57:28 +08:00
root 8405d390df fix: add CGO_ENABLED=0 for static build to fix cross-platform GLIBC errors 2026-02-24 04:19:18 -05:00
nayihz 76f2b42d5b feat: improve web proxy handling and coverage 2026-02-24 17:17:14 +08:00
Guoguo 8774526616 docs: add wechat and discord badge (#707)
Signed-off-by: Guoguo <i@qwq.trade>
2026-02-24 14:47:40 +08:00
daming大铭 b6e965e549 Merge pull request #604 from winterfx/fix/reasoning-content-missing
fix: preserve reasoning_content for OpenAI-compatible reasoning models
2026-02-24 14:20:36 +08:00
Guoguo 0434b49e8d docs: update wechat qrcode (#705) 2026-02-24 13:58:54 +08:00
mosir c56fcedcb1 refactor(pkg/utils): add unified atomic file write utility 2026-02-24 13:22:52 +08:00
Hoshina 0ede643e78 chore: apply PR #697 comment translations to refactored channel subpackages
Translate Chinese comments to English in qq, slack, and telegram channel
implementations, following the translation work done in PR #697. The
original PR modified the old parent package files, but these have been
moved to subpackages during the refactor, so translations are applied
to the new locations.
2026-02-24 12:17:11 +08:00
Meng Zhuo 7cbfa89a96 Merge pull request #697 from xiaket/doc-remove-chinese-comments
[doc]translate Chinese comments
2026-02-24 08:36:18 +08:00
Aditya Kalro 04806bffe3 Moving logging from INFO to DEBUG for messages
Removing extrnaeous comments about mutex in loop.go

Made-with: Cursor
2026-02-23 15:50:55 -08:00
Kai Xia 6fb61539d7 translate Chinese comments
Signed-off-by: Kai Xia <kaix+github@fastmail.com>
2026-02-24 10:27:49 +11:00
mattn 6fe3920a4d perf: refactoring collecting skills (#688)
* perf: refactoring collecting skills

* Fix order to store dir.Name()

* Add tests
2026-02-24 10:07:09 +11:00
Goksu Ceylan 09b1992dd7 fix(security): ensure custom deny patterns extend defaults instead of replacing them (#479)
* fix (security): custom deny patterns denying default patterns

* fix formatting whitespace
2026-02-24 09:02:44 +11:00
0x5487 2fa51d7b86 fix(security): change gateway default bind to 127.0.0.1 (#393)
* chore: Update default host bindings from 0.0.0.0 to 127.0.0.1 for various services and examples.

* config: Update default host bindings to 0.0.0.0 for improved Docker accessibility and add related documentation.

* chore: resolve conflict

* chore: remove link

* docs: Add a tip for Docker users regarding gateway host configuration to the French and Vietnamese READMEs.

* fix: typo issue

* docs: Update Chinese README.zh.md.
2026-02-24 08:54:10 +11:00
Chujiang 8a53cb9665 fix: align Docker Go version with go.mod and optimize logger (#596)
- Update Dockerfile to use golang:1.25-alpine to match go.mod (go 1.25.7)
- Optimize logger by avoiding string concatenation in file writes
- Add explicit empty string assignment for fieldStr when no fields

These changes improve build consistency and reduce memory allocations
in the hot logging path, which is important for the project's goal
of running on resource-constrained devices (<10MB RAM).

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 08:52:16 +11:00
Hoshina 6852f24025 fix: address PR #662 review comments (bus drain, context timeouts, onebot leak)
- Drain buffered messages in MessageBus.Close() so they aren't silently lost
- Replace all context.TODO() with context.WithTimeout(5s) across 7 call sites
- Fix OneBot pending channel leak: send nil sentinel in Stop() and handle
  nil response in sendAPIRequest() to unblock waiting goroutines
2026-02-23 21:34:37 +08:00
美電球 ae74fa3812 Merge pull request #541 from edouard-claude/feat/mistral-provider
feat: add native Mistral AI provider support
2026-02-23 21:06:26 +08:00
Zenix 6d487a12b2 fix: make install should be aware of the textfile busy since it tries to overwrite the file with non-atomic operation (#558) 2026-02-23 21:29:43 +11:00
0x5487 19c698356c fix(security): workspace sandbox avoid time-of-check/time-of-use (TOCTOU) races (#464)
* chore: Update default host bindings from 0.0.0.0 to 127.0.0.1 for various services and examples.

* config: Update default host bindings to 0.0.0.0 for improved Docker accessibility and add related documentation.

* refactor: reimplement filesystem tools with `os.OpenRoot` for enhanced security and simplified path validation.

* chore: revert other PR content from this branch

* docs: Update Chinese README.

* docs: Update Chinese README.

* docs: Update Chinese README.

* refactor: Reorder filesystem helper functions, extract directory entry formatting logic, and enhance `WriteFileTool`'s result message.

* feat: Enhance `mkdirAllInRoot` to prevent creating directories over existing files and add tests for directory creation functionality.

* Refactor filesystem tools to use a `fileReadWriter` interface for both host and sandboxed I/O, improving atomic writes and error handling.

* refactor: unify filesystem read/write operations with atomic write guarantees and clearer naming.

* refactor: rename `appendFileWithRW` function to `appendFile`

* refactor: unify filesystem access by introducing a `fileSystem` interface and updating tools to use it directly, removing `os.Root` dependency from `sandboxFs`.

* chore: run make fmt

* fix: `validatePath` now returns an error when the workspace is empty.
2026-02-23 20:09:53 +11:00
yinwm e76e45f30f Merge remote-tracking branch 'origin/main' into refactor/model-to-model-name 2026-02-23 16:56:20 +08:00
yinwm 712f5a8300 refactor(config): rename model field to model_name
The configuration field for specifying the model has been renamed from
"model" to "model_name" for better clarity and consistency with the
model_list configuration.

A GetModelName() accessor method has been added to maintain backward
compatibility. Existing configurations using the old "model" field will
continue to work correctly.

This change affects:
- Configuration structure (AgentDefaults struct)
- All references across the codebase
- Documentation in all language variants
- Example configuration files
2026-02-23 16:55:06 +08:00
Aditya Kalro 071505e797 Removing the agentMu mutex from the AgentLoop 2026-02-22 21:03:42 -08:00
Aditya Kalro 16a36ea416 Adding a new target to the Makefile to build for multiple platforms with WhatsApp native support. 2026-02-22 20:58:59 -08:00
Vidish 4cc8b90da9 Fix: missing Tavily config in loop.go, and the invalid config param in web_search (#660) 2026-02-23 12:12:34 +08:00
Aditya Kalro 25362ec763 Add new build tag for WhatsApp native support to keep the binary smaller. 2026-02-22 19:22:32 -08:00
Aditya Kalro 76f8ab827f Handle dis 2026-02-22 19:18:02 -08:00
Aditya Kalro 91eff9b34c Changing the logging to use the logger package to be consistent. 2026-02-22 19:10:25 -08:00
Aditya Kalro 81234f7e54 Sanitize WhatsApp messages and remove extra log messages. 2026-02-22 18:20:24 -08:00
Hoshina 26bee0b791 refactor(loop): disable media cleanup to prevent premature file deletion 2026-02-23 08:20:15 +08:00
Hoshina 56d80373eb feat(identity): add unified user identity with canonical platform:id format
Introduce SenderInfo struct and pkg/identity package to standardize user
identification across all channels. Each channel now constructs structured
sender info (platform, platformID, canonicalID, username, displayName)
instead of ad-hoc string IDs. Allow-list matching supports all legacy
formats (numeric ID, @username, id|username) plus the new canonical
"platform:id" format. Session key resolution also handles canonical
peerIDs for backward-compatible identity link matching.
2026-02-23 06:56:48 +08:00
Kai Xia(夏恺) 8928f83c7f remove old roadmap (#632) 2026-02-23 09:45:17 +11:00
美電球 6b429de927 golangci-lint run --fix on master (#656)
Signed-off-by: Kai Xia <kaix+github@fastmail.com>
2026-02-23 06:22:47 +08:00
Hoshina f645e9a377 fix: address PR review feedback across channel system
- MediaStore: use full UUID to prevent ref collisions, preserve and
  expose metadata via ResolveWithMeta, include underlying OS errors
- Agent loop: populate MediaPart Type/Filename/ContentType from
  MediaStore metadata so channels can dispatch media correctly
- SplitMessage: fix byte-vs-rune index mixup in code block header
  parsing, remove dead candidateStr variable
- Pico auth: restrict query-param token behind AllowTokenQuery config
  flag (default false) to prevent token leakage via logs/referer
- HandleMessage: replace context.TODO with caller-propagated ctx,
  log PublishInbound failures instead of silently discarding
- Gateway shutdown: use fresh 15s timeout context for StopAll so
  graceful shutdown is not short-circuited by the cancelled parent ctx
2026-02-23 06:03:23 +08:00
Hoshina a7276e2632 refactor(channels): move SplitMessage from pkg/utils to pkg/channels
Message splitting is exclusively a Manager responsibility. Moving it
into the channels package eliminates the cross-package dependency and
aligns with the refactoring plan.
2026-02-23 05:46:34 +08:00
Hoshina 5d304a9aeb fix: resolve golangci-lint issues in channel system 2026-02-23 05:22:18 +08:00
Kai Xia 4a73415e05 golangci-lint run --fix on master
Signed-off-by: Kai Xia <kaix+github@fastmail.com>
2026-02-23 08:09:26 +11:00
Hoshina 60b68b305a feat(channels): add typing/placeholder automation and Pico Protocol channel (Phase 10 + 7)
Phase 10: Define TypingCapable, MessageEditor, PlaceholderRecorder interfaces.
Manager orchestrates outbound typing stop and placeholder editing via preSend.
Migrate Telegram, Discord, Slack, OneBot to register state with Manager instead
of handling locally in Send. Phase 7: Add native WebSocket Pico Protocol channel
as reference implementation of all optional capability interfaces.
2026-02-23 04:55:15 +08:00
Aditya Kalro c1ed163e77 Added a native WhatsApp channel implementation. 2026-02-22 12:29:27 -08:00
Hoshina f8b656ec37 refactor(channels): standardize group chat trigger filtering (Phase 8)
Add unified ShouldRespondInGroup to BaseChannel, replacing scattered
per-channel group filtering logic. Introduce GroupTriggerConfig (with
mention_only + prefixes), TypingConfig, and PlaceholderConfig types.
Migrate Discord MentionOnly, OneBot checkGroupTrigger, and LINE
hardcoded mention-only to the shared mechanism. Add group trigger
entry points for Slack, Telegram, QQ, Feishu, DingTalk, and WeCom.
Legacy config fields are preserved with automatic migration.
2026-02-23 04:11:11 +08:00
Hoshina e00745489d refactor(channels): remove channel-side voice transcription (Phase 12)
Remove SetTranscriber and inline transcription logic from 4 channels
(Telegram, Discord, Slack, OneBot) and the gateway wiring. Voice/audio
files are still downloaded and stored in MediaStore with simple text
annotations ([voice], [audio: filename], [file: name]). The pkg/voice
package is preserved for future Agent-level transcription middleware.
2026-02-23 03:47:12 +08:00
Hoshina e10b1e1fd4 feat(channels): add MediaSender optional interface for outbound media
Add outbound media sending capability so the agent can publish media
attachments (images, files, audio, video) through channels via the bus.

- Add MediaPart and OutboundMediaMessage types to bus
- Add PublishOutboundMedia/SubscribeOutboundMedia bus methods
- Add MediaSender interface discovered via type assertion by Manager
- Add media dispatch/worker in Manager with shared retry logic
- Extend ToolResult with Media field and MediaResult constructor
- Publish outbound media from agent loop on tool results
- Implement SendMedia for Telegram, Discord, Slack, LINE, OneBot, WeCom
2026-02-23 03:10:57 +08:00
Hoshina 65a09208c4 refactor(channels): consolidate HTTP servers into shared server managed by Manager
Merge 3 independent channel HTTP servers (LINE :18791, WeCom Bot :18793,
WeCom App :18792) and the health server (:18790) into a single shared
HTTP server on the Gateway address. Channels implement WebhookHandler
and/or HealthChecker interfaces to register their handlers on the shared
mux. Also change Gateway default host from 0.0.0.0 to 127.0.0.1 for
security.
2026-02-23 02:39:09 +08:00
Hoshina d72c9c1ee6 refactor(channels): standardize Send error classification with sentinel types
All 12 channel Send methods now return proper sentinel errors (ErrNotRunning,
ErrTemporary, ErrRateLimit, ErrSendFailed) instead of plain fmt.Errorf strings,
enabling Manager's sendWithRetry classification logic to actually work.

- Add ClassifySendError/ClassifyNetError helpers in errutil.go for HTTP-based channels
- LINE/WeCom Bot/WeCom App: use ClassifySendError for HTTP status-based classification
- SDK channels (Telegram/Discord/Slack/QQ/DingTalk/Feishu): wrap errors as ErrTemporary
- WebSocket channels (OneBot/WhatsApp/MaixCam): wrap write errors as ErrTemporary
- WhatsApp: add missing IsRunning() check in Send
- WhatsApp/OneBot/MaixCam: add ctx.Done() check before entering write path
- Telegram Stop: clean up placeholders sync.Map to prevent state leaks
2026-02-23 01:45:48 +08:00
Hoshina afc7a1988f refactor(bus): fix deadlock and concurrency issues in MessageBus
PublishInbound/PublishOutbound held RLock during blocking channel sends,
deadlocking against Close() which needs a write lock when the buffer is
full. ConsumeInbound/SubscribeOutbound used bare receives instead of
comma-ok, causing zero-value processing or busy loops after close.

Replace sync.RWMutex+bool with atomic.Bool+done channel so Publish
methods use a lock-free 3-way select (send / done / ctx.Done). Add
context.Context parameter to both Publish methods so callers can cancel
or timeout blocked sends. Close() now only sets the atomic flag and
closes the done channel—never closes the data channels—eliminating
send-on-closed-channel panics.

- Remove dead code: RegisterHandler, GetHandler, handlers map,
  MessageHandler type (zero callers across the whole repo)
- Add ErrBusClosed sentinel error
- Update all 10 caller sites to pass context
- Add msgBus.Close() to gateway and agent shutdown flows
- Add pkg/bus/bus_test.go with 11 test cases covering basic round-trip,
  context cancellation, closed-bus behavior, concurrent publish+close,
  full-buffer timeout, and idempotent Close
2026-02-23 00:44:45 +08:00
Vidish c6865fe852 feat: integrate Tavily search (#340)
* feat: integrate Tavily search

* fix: set include_raw_content to false in Tavily search as wealready get relevant data inside content

* refactor: update Go type declarations to `any`, apply formatting fixes.
2026-02-23 00:30:14 +08:00
Hoshina 38a26d702c refactor(channels): add per-channel rate limiting and send retry with error classification
Define sentinel error types (ErrNotRunning, ErrRateLimit, ErrTemporary,
ErrSendFailed) so the Manager can classify Send failures and choose the
right retry strategy: permanent errors bail immediately, rate-limit
errors use a fixed 1s delay, and temporary/unknown errors use exponential
backoff (500ms→1s→2s, capped at 8s, up to 3 retries). A per-channel
token-bucket rate limiter (golang.org/x/time/rate) throttles outbound
sends before they hit the platform API.
2026-02-22 23:51:55 +08:00
Hoshina 038fdf5000 refactor(media): add MediaStore for unified media file lifecycle management
Channels previously deleted downloaded media files via defer os.Remove,
racing with the async Agent consumer. Introduce MediaStore to decouple
file ownership: channels register files on download, Agent releases them
after processing via ReleaseAll(scope).

- New pkg/media with MediaStore interface + FileMediaStore implementation
- InboundMessage gains MediaScope field for lifecycle tracking
- BaseChannel gains SetMediaStore/GetMediaStore + BuildMediaScope helper
- Manager injects MediaStore into channels; AgentLoop releases on completion
- Telegram, Discord, Slack, OneBot, LINE channels migrated from defer
  os.Remove to store.Store() with media:// refs
2026-02-22 23:27:55 +08:00
Lixeer 3d605a4f53 fix: run fmt and lint 2026-02-22 23:02:29 +08:00
Hoshina a91de8546c refactor(channels): unify message splitting and add per-channel worker queues
Move message splitting from individual channels (Discord) to the Manager
layer via per-channel worker goroutines. Each channel now declares its
max message length through BaseChannelOption/MessageLengthProvider, and
the Manager automatically splits oversized outbound messages before
dispatch. This prevents one slow channel from blocking all others.

- Add WithMaxMessageLength option and MessageLengthProvider interface
- Set platform-specific limits (Discord 2000, Telegram 4096, Slack 40000, etc.)
- Convert SplitMessage to rune-aware counting for correct Unicode handling
- Replace single dispatcher goroutine with per-channel buffered worker queues
- Remove Discord's internal SplitMessage call (now handled centrally)
2026-02-22 22:46:29 +08:00
Lixeer a849e02917 fix: better session management for github_copilot_provider 2026-02-22 22:30:53 +08:00
Hoshina c669784216 refactor(channels): unify Start/Stop lifecycle and fix goroutine/context leaks
- OneBot: remove close(ch) race in Stop() pending cleanup; add WriteDeadline to Send/sendAPIRequest
- Telegram: add cancelCtx; Stop() now calls bh.Stop(), cancel(), and cleans up thinking CancelFuncs
- Discord: add cancelCtx via WithCancel; Stop() calls cancel(); remove unused getContext()
- WhatsApp: add cancelCtx; Send() adds WriteDeadline; replace stdlib log with project logger
- MaixCam: add cancelCtx; Send() adds WriteDeadline; Stop() calls cancel() before closing
2026-02-22 22:25:07 +08:00
Hoshina 931093c19d refactor(bus,channels): promote peer and messageID from metadata to structured fields
Add bus.Peer struct and explicit Peer/MessageID fields to InboundMessage,
replacing the implicit peer_kind/peer_id/message_id metadata convention.

- Add Peer{Kind, ID} type to pkg/bus/types.go
- Extend InboundMessage with Peer and MessageID fields
- Change BaseChannel.HandleMessage signature to accept peer and messageID
- Adapt all 12 channel implementations to pass structured peer/messageID
- Simplify agent extractPeer() to read msg.Peer directly
- extractParentPeer unchanged (parent_peer still via metadata)
2026-02-22 21:57:12 +08:00
King Tai cb0c8703fb test(tools,utils): add ToolRegistry unit tests and fix Truncate panic on negative maxLen (#517)
Add comprehensive unit tests for the ToolRegistry covering registration,
lookup, execution, context injection, async callbacks, schema generation,
provider definition conversion, and concurrent access.

Fix a defensive edge case in Truncate where a negative maxLen would cause
a slice bounds panic, and add table-driven tests covering boundary
conditions, zero/negative lengths, and Unicode handling.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-22 21:40:59 +11:00
Ali Zulfiqar 6b55fb5f1d docs: fix typos, broken links and inconsistencies in README (#608)
* docs: fix typos, broken links and inconsistencies in README

* docs: revert unintentional bullet style changes

* docs: fix changes

* docs: fixing issues

* docs: updating roadmap link

* docs: removing *
2026-02-22 21:00:15 +11:00
Edouard CLAUDE 34a8ce5af0 fix: remove extra fields from ToolCall JSON serialization
Mistral's API strictly validates tool_calls in assistant messages and
rejects non-standard fields. The ToolCall struct had Name and Arguments
as top-level JSON fields, duplicating data already in Function.Name
and Function.Arguments. OpenAI silently ignored these extras but
Mistral returns 422.

Change json tags to "-" so these internal fields are no longer
serialized to API payloads while remaining available in Go code.
2026-02-22 11:40:21 +04:00
Edouard CLAUDE 65422a16a4 feat: add native Mistral AI provider support
Add Mistral as a first-class provider alongside the 17 existing ones.
Mistral uses the OpenAI-compatible API at https://api.mistral.ai/v1
with provider-specific model prefix stripping (mistral/model → model).

Changes:
- Add Mistral to ProvidersConfig, IsEmpty(), HasProvidersConfig()
- Add mistral entry in default model_list (defaults.go)
- Add mistral protocol in factory_provider.go and getDefaultAPIBase()
- Add mistral prefix stripping in openai_compat normalizeModel()
- Add mistral case in legacy factory.go resolveProviderSelection()
- Add mistral migration entry in ConvertProvidersToModelList()
- Add mistral to supported providers in migrate/config.go
- Add mistral section in config.example.json
- Update AllProviders test (17 → 18 providers)

Tested end-to-end with mistral-small-latest model.
2026-02-22 11:40:21 +04:00
Yoftahe Abraham cec6fd4cd4 fix: should use fmt.Printf instead of fmt.Print(fmt.Sprintf(...)) (#623) 2026-02-22 18:27:38 +11:00
yuchou87 4e330b297c test(mcp): add manager behavior and lifecycle unit tests 2026-02-22 15:13:29 +08:00
yuchou87 16a3b96dde fix(mcp): validate workspace before resolving relative env_file 2026-02-22 15:06:57 +08:00
yuchou87 6aade43236 docs: add MCP tool configuration documentation 2026-02-22 15:03:20 +08:00
yuchou87 672da984e5 Merge branch 'main' into mcp-tools-support 2026-02-22 14:48:07 +08:00
kernoeb b9a66248d8 fix: resolve Groq STT key from model_list when providers.groq is absent (#602)
When users migrate from the legacy `providers` config to the new
`model_list` format, voice transcription silently breaks on Telegram,
Discord and Slack channels.

The gateway was reading the Groq API key exclusively from
`cfg.Providers.Groq.APIKey`, which is empty once the key is defined
only inside a `model_list` entry. The transcriber was never initialized,
so voice messages fell back to a plain `[voice]` placeholder.

This fix also scans `model_list` for any entry whose `model` field
starts with `groq/` and uses its `api_key` as a fallback, preserving
full backward compatibility with the legacy `providers.groq` field.
2026-02-22 11:32:44 +11:00
Albert Simon c51ceac70b fix: updated model configuration links at readme (#544)
Signed-off-by: Albert Simon <simon.albert75@gmail.com>
2026-02-22 09:53:53 +11:00
winterfx d224397f40 fix: preserve reasoning_content for OpenAI-compatible reasoning models
Models like Moonshot kimi-k2.5 and DeepSeek-R1 return a
reasoning_content field in assistant messages. When thinking is enabled,
the API requires this field to be echoed back in subsequent requests.
PicoClaw was silently dropping it, causing 400 errors on tool-call
round-trips.

- Add ReasoningContent to Message and LLMResponse types
- Parse reasoning_content in openai_compat parseResponse()
- Carry reasoning_content through assistant tool-call messages
- Add unit test for reasoning_content parsing

Fixes #588
2026-02-21 23:29:40 +08:00
daming大铭 40f9630eea Merge pull request #590 from alexhoshina/docs
docs: add Chinese channel documentation
2026-02-21 22:49:05 +08:00
zepan aea4f25c83 1. update wechat qrcode. 2. add CONTRIBUTING.md 2026-02-21 22:45:47 +08:00
Hoshina 023b245a28 docs: add Chinese channel documentation 2026-02-21 18:00:19 +08:00
Hoshina b25b3c1324 fix: golangci-lint run --fix 2026-02-21 16:35:56 +08:00
美電球 bb8b9243b7 Merge pull request #592 from alexhoshina/main
fix: golangci-lint run --fix
2026-02-21 16:21:34 +08:00
Hoshina 0066602294 fix: golangci-lint run --fix 2026-02-21 16:20:15 +08:00
Hoshina 3df7f70540 fix: golangci-lint fmt 2026-02-21 16:05:39 +08:00
yuchou87 cfc29a1383 fix(mcp): prevent use-after-close race between CallTool and Close
A race could occur when Close() called conn.Session.Close() concurrently
with an in-flight conn.Session.CallTool(), leading to undefined behavior.

Fix by adding a sync.WaitGroup to Manager:
- CallTool increments the WaitGroup while holding the read lock (after
  checking m.closed), ensuring no new calls are counted after Close sets
  the flag
- Close sets m.closed=true, releases the write lock, then waits for all
  in-flight calls to finish via wg.Wait() before closing sessions
2026-02-21 14:10:48 +08:00
yuchou87 11dbc301f9 perf(agent): cache ListAgentIDs() result before MCP tool registration loop
ListAgentIDs() was called on every iteration of the inner tool loop,
causing repeated allocations. Capture the slice once and reuse it for
both agentCount and the registration loop.
2026-02-21 13:48:41 +08:00
yuchou87 d2b3fc1dd0 fix(mcp): include server name and cause in Close() errors
Previously Close() discarded all underlying errors and returned only
'failed to close N server(s)', making debugging impossible.

Now each error wraps the server name and original cause, and all errors
are joined so callers can inspect the full failure list.
2026-02-21 13:46:06 +08:00
yuchou87 33058b534e fix(mcp): reject empty keys in loadEnvFile
A line like '=value' would result in envVars[""] = "value", producing
an invalid environment entry for the child process. Return an error
instead when the key is empty.
2026-02-21 13:45:00 +08:00
yuchou87 59e9c55454 docs(config): restore MCP server examples in config.example.json
Add back filesystem, github, brave-search, and postgres as example MCP
server configurations. These were removed from DefaultConfig() to reduce
memory footprint, but should remain in the example config as documentation
for users setting up MCP servers.
2026-02-21 13:42:26 +08:00
yuchou87 246fdf3f33 fix(mcp): guard against nil result from CallTool
CallTool can return (nil, nil) if the underlying MCP library misbehaves.
Without a nil check, result.IsError would panic. Return an explicit error
ToolResult instead.
2026-02-21 13:40:55 +08:00
yuchou87 fb2b594060 fix(scripts): specify service name in docker compose build
Avoid building zero services when all services are gated behind profiles.
Without an explicit service target, 'docker compose build' silently skips
all profile-gated services, causing subsequent 'docker compose run' to
use stale or missing images.
2026-02-21 13:39:42 +08:00
yuchou87 d867e86dbe Merge branch 'main' into mcp-tools-support
# Conflicts:
#	config/config.example.json
#	pkg/config/config.go
2026-02-21 13:28:15 +08:00
Luke Milby 80c8b57533 Fix Memory Write (#557)
* fix issue where memory will only trigger when asked to remember something

* updated prompt for memory usage
2026-02-21 08:21:38 +08:00
Meng Zhuo 273a8a2318 Merge pull request #550 from mymmrac/govet-linter
feat(linter): Fix govet linter
2026-02-21 08:20:35 +08:00
Meng Zhuo b3e20c7c71 Merge pull request #491 from PixelTux/ollama
fix(docker): enable container-to-host connectivity for localhost services (e.g., Ollama)
2026-02-21 08:18:56 +08:00
Goksu Ceylan 244eb0b47d fix (security): ExecTool working_dir sandbox escape (#478)
* fix (security) Shell working_dir bypass

* Feedback from @mengzhuo & Discord
- reuse internal security package to validate path
- add tests for workspace escape
2026-02-21 08:15:46 +08:00
Artem Yadelskyi 02b4d9fbe2 feat(linter): Fix govet linter 2026-02-20 22:35:16 +02:00
Artem Yadelskyi 2bf467fbbe feat(telegram): Fix conflicts 2026-02-20 22:14:02 +02:00
Artem Yadelskyi 50d2616172 Merge branch 'main' into telegram-bot-commands
# Conflicts:
#	pkg/channels/telegram.go
2026-02-20 22:13:26 +02:00
danieldd e883e14b81 Merge pull request #548 from mymmrac/build-no-fmt
feat(ci): Remove fmt from build step
2026-02-21 03:13:22 +07:00
Artem Yadelskyi c2ace2561c feat(ci): Remove fmt from build step 2026-02-20 22:09:36 +02:00
danieldd df2c42466c Merge pull request #435 from mymmrac/fix-formatting
feat(fmt): Run formatters
2026-02-21 03:04:42 +07:00
danieldd 1e3a9eb3c4 Merge pull request #546 from harshbansal7/readme_fix
[Fix] Fix Readme Reference for Model Configuration.
2026-02-21 02:38:08 +07:00
harshbansal7 123cffa85a fix 2 2026-02-21 01:04:48 +05:30
harshbansal7 5ca239b5c5 fix 2026-02-21 01:02:35 +05:30
Artem Yadelskyi e1ba69293e feat(telegram): Updated log message 2026-02-20 20:34:09 +02:00
Artem Yadelskyi c319db431e Merge branch 'main' into telegram-bot-commands 2026-02-20 20:32:00 +02:00
Artem Yadelskyi 26bca10b81 feat(telegram): Do not fail on commands init 2026-02-20 20:31:56 +02:00
Artem Yadelskyi 0675ce7c38 feat(fmt): Fix formatting 2026-02-20 20:03:11 +02:00
Artem Yadelskyi ad8c2d48c8 Merge branch 'main' into fix-formatting
# Conflicts:
#	cmd/picoclaw/main.go
#	pkg/agent/context.go
#	pkg/agent/loop.go
#	pkg/channels/dingtalk.go
#	pkg/channels/feishu_64.go
#	pkg/channels/line.go
#	pkg/channels/manager.go
#	pkg/config/config.go
#	pkg/migrate/migrate_test.go
#	pkg/providers/anthropic/provider_test.go
#	pkg/providers/claude_provider_test.go
#	pkg/providers/http_provider.go
#	pkg/providers/openai_compat/provider.go
#	pkg/providers/protocoltypes/types.go
#	pkg/providers/types.go
2026-02-20 20:02:53 +02:00
daming大铭 e23795e51b Merge pull request #537 from Esubaalew/main
fix: correct documentation misalignment across translations and guides
2026-02-21 00:01:36 +08:00
Hoshina d97848389b refactor(channels): replace bool with atomic.Bool for running state in BaseChannel 2026-02-21 00:00:29 +08:00
Hoshina cd22272354 refactor(channels): remove redundant setRunning method from BaseChannel 2026-02-20 23:52:41 +08:00
Meng Zhuo 5b525f6139 Merge pull request #378 from lunareed720/fix/exec-timeout-process-tree
fix(exec): kill child process tree on timeout to prevent orphaned tasks
2026-02-20 23:32:05 +08:00
Hoshina 59a889b608 refactor(channels): remove old channel files from parent package 2026-02-20 23:26:33 +08:00
Hoshina 6122ab664b refactor(channels): add channel subpackages and update gateway imports 2026-02-20 23:25:44 +08:00
Meng Zhuo 55227762e4 Merge pull request #524 from mattn/perf/strings-builder
Use strings.Builder instead of += concatenation in loops
2026-02-20 23:24:47 +08:00
esubaalew 838a69085b fix: correct docs misalignment across translations and guides
- Fix DingTalk section referencing "QQ numbers" instead of DingTalk user IDs
- Fix Anthropic example showing OAuth when code uses paste-token auth
- Replace OpenClaw references in ANTIGRAVITY_AUTH.md with actual PicoClaw paths and Go patterns
- Fix auth file path from auth-profiles.json to auth.json in ANTIGRAVITY_USAGE.md
- Remove non-existent approval tool from tools_configuration.md, add skills tool docs
- Update Quick Start configs in fr/pt-br/vi/ja translations to use model_list format
- Fix allowFrom camelCase to allow_from in fr/pt-br translations
- Fix camelCase config keys in ja translation
- Update zh/ja web search config from old flat format to brave/duckduckgo
- Fix broken ClawdChat link and trailing commas in zh translation
- Add missing qwen/cerebras providers to fr/pt-br/vi translation tables
- Add missing protocol prefixes to migration guide
- Fix typos in community roadmap
2026-02-20 18:23:22 +03:00
Hoshina 083e29ebd9 refactor(channels): replace direct constructors with factory registry in manager 2026-02-20 23:19:40 +08:00
Hoshina dfcf15bfff refactor(channels): add factory registry and export SetRunning on BaseChannel 2026-02-20 23:18:46 +08:00
daming大铭 1ef33c90ed Merge pull request #474 from swordkee/main
add wecom and wecomApp
2026-02-20 21:17:59 +08:00
Truong Vinh Tran 5d2674b336 docs: Update Brave Search pricing - now $5/1000 queries (no free tier)
Brave Search discontinued free tier on Feb 12, 2026.
  Updated all README references to reflect paid pricing.
  Emphasized SearXNG as free alternative.

  Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-20 14:02:46 +01:00
swordkee 0f70f783bd feat: add wecom and wecomApp test 2026-02-20 20:01:22 +08:00
Yasuhiro Matsumoto df49f6698a Fix 2026-02-20 20:48:43 +09:00
Truong Vinh Tran a5043854c3 docs: Add SearXNG to example configuration file
Update config.example.json to include SearXNG web search provider
configuration alongside existing Brave, DuckDuckGo, and Perplexity options.

This ensures users have a complete reference for all available search
providers when setting up their PicoClaw instance.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-20 12:39:25 +01:00
swordkee ca481035a4 feat: add wecom and wecomApp test 2026-02-20 19:39:12 +08:00
Truong Vinh Tran 25d8f0e1ca docs: Add SearXNG web search provider documentation
Update README to document the new SearXNG search provider option alongside
existing Brave, DuckDuckGo, and Perplexity providers.

Changes:
- Document provider priority order: Perplexity > Brave > SearXNG > DuckDuckGo
- Add SearXNG configuration examples in Quick Start and Full Config sections
- Expand "Get API Keys" section with all 4 search provider options
- Enhance troubleshooting section with detailed setup instructions for each provider
- Add SearXNG to API Key Comparison table (unlimited/self-hosted)

SearXNG benefits documented:
- Zero cost with no API fees or rate limits
- Privacy-focused self-hosted solution
- Aggregates 70+ search engines for comprehensive results
- Solves datacenter IP blocking issues on Oracle Cloud, GCP, AWS, Azure
- No API key required, just deploy and configure base URL

This documentation complements the code implementation in commit e7d8975.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-20 12:37:58 +01:00
Vernon Stinebaker 2fb2a733d4 feat(discord): add mention_only option for @-mention responses (#518)
* feat(discord): add mention_only option for @-mention responses

Add MentionOnly config option to Discord channel. When enabled, the bot
only responds when explicitly @-mentioned, useful for shared servers.

- Add MentionOnly bool field to DiscordConfig
- Store botUserID on startup for mention checking
- Check m.Mentions before processing messages when MentionOnly is true
- Update config example and README documentation

* fix(discord): resolve race condition and strip mention from content

- Get botUserID before opening session to avoid race condition
- Add stripBotMention to remove @mention from message content
- Handles both <@USER_ID> and <@!USER_ID> mention formats

* fix(discord): skip mention_only check for DMs

DMs should always be responded to regardless of mention_only setting.
Added check to skip the mention_only logic when GuildID is empty.

* Update README.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Hua Audio <161028864+Huaaudio@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-20 12:18:37 +01:00
Yasuhiro Matsumoto bca92433ba Use strings.Builder instead of += concatenation in loops 2026-02-20 20:09:13 +09:00
Truong Vinh Tran e7d8975f1c feat: Add SearXNG search provider support
Implements SearXNG as a third web search provider to address Oracle Cloud
datacenter IP blocking issues and provide a cost-free, self-hosted alternative
to commercial search APIs.

Changes:
- Add SearXNGConfig struct with Enabled, BaseURL, and MaxResults fields
- Implement SearXNGSearchProvider with JSON API integration
- Update provider priority: Perplexity > Brave > SearXNG > DuckDuckGo
- Wire SearXNG configuration through agent tool registration
- Add default config values (disabled by default, empty BaseURL)

Benefits:
- Solves DuckDuckGo datacenter IP blocking (138 bytes redirect responses)
- Zero-cost alternative to Brave Search API ($5/1000 queries)
- Self-hosted solution with 70+ aggregated search engines
- Privacy-focused with no rate limits or API keys required
- Ideal for Oracle Cloud, GCP, AWS, and Azure VM deployments

The implementation follows the existing provider interface pattern and
maintains backward compatibility with all existing search providers.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-20 12:02:00 +01:00
Harsh Bansal d692cc0cc6 Feature: Implement Skill Discovery - With Clawhub Integration and Caching (#332)
* Add Find Skills and Install Skills

* Improvements

* fix file name

* Update pkg/skills/clawhub_registry.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix

* Comments addressed

* Resolve comments

* fix tests

* fixes

* Comments resolved

* Update pkg/skills/search_cache_repro_test.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* minor fix

* fix test

* fixes

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-20 18:55:04 +08:00
swordkee 14ccfb39d9 feat: add wecom and wecomApp test 2026-02-20 18:28:10 +08:00
swordkee 59772cdbf2 feat: add wecom and wecomApp channel support 2026-02-20 17:40:59 +08:00
lxowalle f1223eec42 fix: revert enable endy patterns (#519) 2026-02-20 17:16:42 +08:00
daming大铭 36a8a038ee Merge pull request #514 from CrisisAlpha/docs/config-example-add-missing-sections
docs(config): add missing duckduckgo, exec, and qq sections to example config
2026-02-20 16:46:12 +08:00
CrisisAlpha be55204696 docs(config): add missing duckduckgo, exec, and qq sections to example config
config.example.json was missing three sections that exist in the Go
config structs and defaults:

- tools.web.duckduckgo: DuckDuckGo is enabled by default in
  defaults.go and requires no API key (free search provider), but
  users who copy the example config silently lose it since the
  section was omitted.

- tools.exec: The ExecConfig struct supports enable_deny_patterns
  and custom_deny_patterns for security hardening, but users had
  no way to discover these options from the example.

- channels.qq: The QQ channel was the only channel in ChannelsConfig
  missing from the example while all others were present.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-20 15:11:40 +08:00
daming大铭 e599573ed4 Merge pull request #492 from yinwm/feat/refactor-provider-by-protocol
feat(config): refactor provider architecture to protocol-based model_list
2026-02-20 13:31:30 +08:00
yinwm 723f4e84ef Merge upstream main into feat/refactor-provider-by-protocol
Resolved conflicts:
- pkg/config/config.go: Removed duplicate DefaultConfig() (already in defaults.go)

Upstream changes:
- Added Session.DMScope default value ("main")
- Various channel improvements

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 13:29:30 +08:00
hsohinna 4adafa8890 fix(channels): channels session key routing (#489)
* fix(onebot): add metadata for direct and group message handling
* fix(qq): add metadata for direct and group message handling
* fix(dingtalk): add metadata for direct and group message handling
* fix(feishu): add metadata for direct and group message handling
* fix(whatsapp): add metadata for direct and group message handlinga
* fix(line): add metadata for direct and group message handling
* fix(maixcam): add metadata for person detection handling
* fix(config): add default session configuration with DMScope
2026-02-20 13:27:08 +08:00
yinwm 23c39f41df Merge upstream main into feat/refactor-provider-by-protocol
Resolved conflicts:
- pkg/config/config.go: Removed duplicate DefaultConfig() (already in defaults.go)
- pkg/config/defaults.go: Updated Temperature to *float64 (nil default)

Upstream changes included:
- Temperature changed from float64 to *float64 (nil means use provider default)
- New HeartbeatConfig and DevicesConfig
- Various agent and tool improvements

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 13:25:26 +08:00
yinwm ea447c6b68 refactor(auth): extract supported providers message as constant
Address review comment from @xiaket - the "Supported providers" message
was printed in multiple places. Now extracted as a constant.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 13:20:59 +08:00
yinwm dc9fb327c2 chore: update Claude model references to claude-sonnet-4.6
Replace all claude-sonnet-4 references with claude-sonnet-4.6 across
codebase including documentation, tests, and configuration examples.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:15:04 +08:00
yinwm 7572e3b95d fix(config): allow duplicate model_name for load balancing
Remove duplicate model_name check in ValidateModelList to support
load balancing feature where multiple configs can share the same
model_name for round-robin selection.

Update tests to reflect the new behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 11:46:28 +08:00
yinwm a1d694b8f1 fix(migrate): add github_copilot to supportedProviders
Add github_copilot to the supportedProviders map to match
the providers handled in MergeConfig.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 11:43:45 +08:00
yinwm 5cd1597674 fix: remove unnecessary lock mechanism and upgrade Claude 3 to Claude 4
- Remove sync.RWMutex and rrCounters from Config struct
- Simplify GetModelConfig to use global atomic counter for load balancing
- Remove unnecessary locks from HasProvidersConfig, SaveConfig, etc.
- Add buildModelWithProtocol helper to handle models with existing prefix
- Fix TestCreateProviderReturnsHTTPProviderForOpenRouter to use model_list
- Upgrade all Claude 3 references to Claude 4 across documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 11:34:52 +08:00
yinwm b7c906fe18 docs: update providers deprecation comment
Change "removed in v2.0" to "removed in a future version"
for the deprecated providers section.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 10:52:03 +08:00
yinwm 6ad85d225b fix(auth): preserve model_list and use gpt-5.2 for Codex API
Auth fixes:
- Fix OpenAI/Anthropic OAuth and token login to update ModelList
- Fix logout to clear AuthMethod in ModelList
- Add helper functions: isOpenAIModel, isAnthropicModel, isAntigravityModel
- Fix slice bounds panic in isAntigravityModel using strings.HasPrefix
- All auth operations now preserve existing model_list configuration

Factory provider fixes:
- Add OAuth support for openai protocol in CreateProviderFromConfig
- CodexAuthProvider is now used when auth_method is oauth/token

Default model updates:
- OpenAI login: set default model to gpt-5.2
- Anthropic login: set default model to claude-sonnet-4
- Antigravity login: set default model to gemini-flash (remove provider field)

Model changes:
- Change default OpenAI model from gpt-4o to gpt-5.2
- gpt-5.2 is compatible with Codex API (chatgpt.com backend)
- Update all README files, config examples, and migration code

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 10:48:27 +08:00
yinwm df6958f312 feat(config): add complete model_list template with all 17 providers
- Include all 17 supported providers in default config as templates
- Each entry has model_name, model, api_base, and empty api_key
- Add comments with API key links for each provider
- Keep onboard message simple (only OpenRouter and Ollama)
- Fix duplicate model_name (cerebras-llama-3.3-70b)

Providers included:
Zhipu, OpenAI, Anthropic, DeepSeek, Gemini, Qwen, Moonshot,
Groq, OpenRouter, NVIDIA, Cerebras, Volcengine, ShengsuanYun,
Antigravity, GitHub Copilot, Ollama, VLLM

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 09:30:09 +08:00
Artem Yadelskyi a896831903 feat(fmt): Fix formatting 2026-02-19 22:05:15 +02:00
Artem Yadelskyi 2038f04d0d Merge branch 'main' into fix-formatting
# Conflicts:
#	pkg/agent/loop.go
#	pkg/agent/loop_test.go
#	pkg/channels/discord.go
#	pkg/channels/onebot.go
#	pkg/config/config.go
#	pkg/tools/subagent_tool_test.go
2026-02-19 22:04:48 +02:00
cointem 394d1d1197 fix: Templates update (#485)
* fix: add MaxTokens and Temperature fields to AgentInstance and update related logic

* feat: add MaxTokens and Temperature options to SubagentManager and update tool loop logic

* feat: add default temperature handling and update related tests

* feat: allow temperature 0 and distinguish unset

* fix: format MockLLMProvider struct in subagent_tool_test.go
2026-02-19 19:16:37 +01:00
yinwm e2d37f09bf style: run gofmt to fix code formatting
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 01:27:00 +08:00
yinwm 9f5ff95cc2 docs: add model_list configuration to all language READMEs
Add comprehensive Model Configuration (model_list) section to all 6 language versions:
- English, Chinese (zh), French (fr), Japanese (ja), Portuguese (pt-br), Vietnamese (vi)

Key additions:
- Complete vendor list (17 providers) with protocol prefixes and API base URLs
- Basic and vendor-specific configuration examples
- Load balancing documentation
- Migration guide from legacy providers config
- Multi-agent support design rationale

Replace Chinese vendor names with English/Pinyin in non-Chinese versions for better readability.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 01:22:06 +08:00
yinwm c08deb93d1 refactor(config): use provider-specific protocol instead of generic openai protocol
Update model configurations to use provider-specific protocols (zhipu, vllm,
gemini, shengsuanyun, deepseek, volcengine) instead of using the generic
"openai" protocol for all providers. This change ensures each provider
uses its correct protocol identifier and model naming convention.
2026-02-20 01:07:36 +08:00
yinwm 7f241647be feat(providers): add thought_signature support for gemini
Add support for persisting thought_signature metadata from Google/Gemini 3
models. This introduces ExtraContent and GoogleExtra types to handle
provider-specific metadata, and ensures thought signatures are properly
preserved through the tool call lifecycle.
2026-02-20 00:36:31 +08:00
yinwm 68cdafc5f2 refactor(providers): restructure provider creation with protocol-based configuration
- Move provider creation logic to factory_provider.go with protocol-based approach
- Add OpenAIProviderConfig with WebSearch support and embedded ProviderConfig
- Add maxTokensField to OpenAI-compatible provider for configurable token field
- Introduce new providers: Ollama, DeepSeek, GitHubCopilot, Antigravity, Qwen
- Remove redundant CreateProvider function from factory.go
- Add ThoughtSignature field to FunctionCall for tool response handling
- Remove duplicate Name field assignment in tool loop
- Update tests to reflect new provider configuration structure
2026-02-20 00:12:01 +08:00
yinwm f8f1d539d4 Merge remote-tracking branch 'origin/main' into feat/refactor-provider-by-protocol 2026-02-20 00:11:46 +08:00
PixelTux 676bd6d222 extra_hosts mapping to have enables container-to-host connectivity 2026-02-19 15:52:46 +01:00
yinwm 1e96733435 fix(agent): avoid consecutive system messages in compression
Append emergency compression note to the original system prompt
instead of creating a separate system message. Some APIs like
Zhipu reject two consecutive system messages.
2026-02-19 22:47:03 +08:00
Edouard CLAUDE 521359ed4f docs: add French README (README.fr.md) (#408) 2026-02-19 14:23:06 +01:00
Jex 213274002a fix: keep Discord typing indicator alive during agent processing (#391)
* fix: keep Discord typing indicator alive during agent processing

Discord's ChannelTyping() expires after ~10s, but agent processing
(LLM + tool execution) typically takes 30-60s+. Replace single-fire
ChannelTyping() with a self-managed typing loop inside DiscordChannel.

- startTyping(chatID): goroutine refreshes ChannelTyping every 8s
- stopTyping(chatID): called in Send() when response is dispatched
- Stop() cleans up all typing goroutines on shutdown
- startTyping placed after all early returns to prevent goroutine leaks

Typing lifecycle fully contained in channel layer, no interface changes.

Fixes #390

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add goroutine safety to Discord typing indicator

- Add 5-minute timeout as safety net to prevent indefinite goroutine leaks
  when agent produces no outbound message (empty response, panic, etc.)
- Listen on c.ctx.Done() so goroutine exits when channel context is cancelled
- Log ChannelTyping() errors at debug level for diagnostics (rate limits, session closed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 13:28:58 +01:00
yuchou87 a7a4e88fff fix(agent): use fallback workspace path for MCP initialization
Use cfg.WorkspacePath() as a fallback when defaultAgent is nil or
its Workspace is empty. This ensures MCP servers with relative
envFile paths can always resolve them correctly, even when agents
haven't been fully initialized yet.

Previously, workspacePath would be an empty string in these cases,
causing relative envFile paths to fail to resolve. Now the fallback
guarantees a valid workspace path is always provided to
LoadFromMCPConfig.

Addresses Copilot code review feedback.
2026-02-19 20:03:00 +08:00
yuchou87 f1b798434d fix(mcp): prevent race condition between CallTool and Close
Add a closed flag to the Manager struct to prevent CallTool from
accessing server connections after Close has been called. The flag
is checked within the RLock in CallTool to ensure thread-safety.

Previously, CallTool could obtain a server reference using RLock,
then that reference could be closed by Close() running concurrently,
leading to use-after-close errors. Now:

1. CallTool checks the closed flag before accessing servers
2. Close sets the closed flag before closing connections
3. CallTool directly accesses m.servers within the lock instead
   of using GetServer() to avoid releasing the lock prematurely

This ensures CallTool will not use a server connection that is
being closed or has been closed.

Addresses Copilot code review feedback.
2026-02-19 19:47:05 +08:00
yuchou87 7577414761 fix(mcp): ensure proper environment variable override semantics
Use a map to merge environment variables with guaranteed override
behavior. Config variables (cfg.Env) now properly override file
variables (envFile), which in turn override parent process environment.

Previously, simply appending to a slice could result in duplicate
variables, and while most systems use the last occurrence, this
behavior is not guaranteed and could lead to unexpected results.

Addresses Copilot code review feedback.
2026-02-19 19:45:15 +08:00
yuchou87 f0ce26ff2b style(config): use snake_case for EnvFile JSON field name
Change 'envFile' to 'env_file' to maintain consistency with the rest
of the codebase which uses snake_case for JSON field names (e.g.,
'api_key', 'api_base', 'max_results', 'exec_timeout_minutes').

Addresses Copilot code review feedback.
2026-02-19 19:43:48 +08:00
yuchou87 dea381c385 improve(agent): clarify MCP tool registration logging
Separate tool counting metrics for better clarity:
- unique_tools: number of distinct MCP tools
- total_registrations: total tool registrations across all agents
- agent_count: number of agents receiving the tools

Previously, tool_count was misleading as it showed total registrations,
making it appear that more unique tools were registered than actually exist.

Addresses Copilot code review feedback.
2026-02-19 19:31:13 +08:00
yuchou87 ffa01986ce fix(agent): scope MCP manager cleanup to successful initialization
Move defer cleanup inside else block to only clean up when MCP servers
are successfully initialized. This prevents unnecessary cleanup attempts
when LoadFromMCPConfig fails.

Addresses Copilot code review feedback.
2026-02-19 19:26:02 +08:00
yuchou87 a5d2e109bf chore: merge main branch into mcp-tools-support
Resolved conflicts in:
- config/config.example.json: Added empty MCP config block
- pkg/config/config.go: Added MCP config structures to new ToolsConfig
- pkg/agent/loop.go: Integrated MCP tools with new AgentRegistry architecture

MCP tools now register to all agents in the registry during startup.
2026-02-19 19:06:37 +08:00
tpkeeper 12f0c4a6cf fix: ensure tool name is correctly assigned in LLM iteration(missing tool call name in debug mode logs) (#454)
* fix: ensure tool name is correctly assigned in LLM iteration

* fix: ensure tool name is correctly included in assistant message
2026-02-19 12:06:09 +01:00
yuchou87 47533a00cd style: format code with gofmt 2026-02-19 18:53:24 +08:00
Ruslan Semagin 32c5c4b3a4 refactor: replace bool map with set-style map for internal channels (#472)
* refactor: replace bool map with set-style map for internal channels

Use map[string]struct{} and comma-ok idiom for clearer and more idiomatic membership checks.

* Update pkg/constants/channels.go

Co-authored-by: Harsh Bansal <122075346+harshbansal7@users.noreply.github.com>

---------

Co-authored-by: Harsh Bansal <122075346+harshbansal7@users.noreply.github.com>
2026-02-19 11:48:17 +01:00
hsohinna 56a060ff61 feat(onebot): enhance OneBot channel (#192)
* fix: change BotStatus type to json.RawMessage and add isAPIResponse function

* feat(onebot): add rich media, API callback, keepalive and voice transcription

   Comprehensive improvements to the OneBot channel for better NapCatQQ
   compatibility:

   - Add echo-based API callback mechanism (sendAPIRequest) for
     request/response correlation via pending map
   - Add WebSocket ping/pong keepalive (30s ping, 60s read deadline)
   - Fetch bot self ID via get_login_info on connect/reconnect
   - Refactor parseMessageContentEx into parseMessageSegments supporting
     image, record, video, file, reply, face, forward segments
   - Add voice transcription via Groq transcriber (SetTranscriber)
   - Switch to message segment array format for sending with auto reply
     quote via lastMessageID tracking
   - Add message_sent event handling and detailed notice event processing
     (recall, poke, group increase/decrease, friend add, etc.)
   - Use sync/atomic for echoCounter, optimize listen() lock pattern
   - Clean up pending callbacks on Stop(), defer temp file cleanup
   - Mount Groq transcriber on OneBot channel in main.go gateway

* feat(onebot): add user ID allowlist check for incoming messages

- Currently, the agent does not respond to messages sent by users outside the allowlist.

* refactor(onebot): simplify channel implementation and add emoji reaction

- onebot.go from 1179 to 980 lines (~17%)
2026-02-19 14:39:35 +08:00
yinwm 58b5e21d90 fix(config): support legacy config without provider field
When no provider field is set but model is specified, use the user's model
as ModelName for the first provider. This maintains backward compatibility
with old configs that relied on implicit provider selection and ensures
GetModelConfig(model) can find the model by its configured name.
2026-02-19 13:05:21 +08:00
yinwm 1e26312cb3 feat(config): validate duplicate model names
Add validation to ensure model_name is unique across all entries in
model_list. This prevents potential conflicts when multiple model
configs share the same model_name identifier.
2026-02-19 12:45:12 +08:00
yinwm ec86b21d3f fix: improve migration logic and reduce code duplication
- Preserve user's configured model during config migration (issue #5)
- Simplify ExtractProtocol using strings.Cut
- Extract NormalizeToolCall to shared utility, removing ~70 lines of duplicate code
- Clean up unused fields in providerMigrationConfig struct

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:22:39 +08:00
mattn e8afd31b28 Replace \s+ with [^\S\n]+ to preserve newlines (#299) 2026-02-19 02:02:28 +01:00
Kai Xia(夏恺) d167b47431 dead code cleanup (#210) 2026-02-19 01:54:13 +01:00
fipso bb0424e1e2 fix: also use max_completion_tokens for gpt5 era models (#445) 2026-02-19 01:29:34 +01:00
Hua Audio 59fd391248 Merge pull request #436 from Huaaudio/feat/base-layer-message-split
Refactor/base layer message split from #143
2026-02-18 23:29:29 +01:00
Hua Audio 0d6b22fb3a Update pkg/utils/message.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-18 23:26:39 +01:00
Huaaudio 98afd39913 remove unicode 2026-02-18 23:18:17 +01:00
Huaaudio a46fe140a3 update dynamic buffer 2026-02-18 23:14:44 +01:00
Huaaudio 7d8894d842 update message test, change dynamic buffer 2026-02-18 23:14:24 +01:00
Hua Audio dfc3dffd06 Update pkg/utils/message.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-18 22:43:49 +01:00
Huaaudio 82a2faed9d Privated function 2026-02-18 22:37:45 +01:00
Huaaudio f38ce0d4ac Update to support extra long code blocks 2026-02-18 22:31:18 +01:00
Hua Audio 4ccee85561 Update pkg/utils/message.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-18 22:16:19 +01:00
Hua Audio 0a9d24e2a2 Merge branch 'sipeed:main' into feat/base-layer-message-split 2026-02-18 22:03:11 +01:00
Hua Audio 048cd08e41 Merge pull request #437 from harshbansal7/fix_build
[Hot Fix] Fix Failing Linter
2026-02-18 22:02:33 +01:00
harshbansal7 b122abd30f fix 2026-02-19 02:28:44 +05:30
Huaaudio e35a827624 update documents 2026-02-18 21:44:25 +01:00
Hua e03124dc8a refactor: improve SplitMessage API clarity
- Accept hard upper limit (maxLen) instead of pre-subtracted value
- Caller now passes actual platform limit (e.g., 2000 for Discord)
- Internal buffer of 500 chars is handled within message.go
- Preferred split at maxLen - 500, may extend to maxLen for code blocks
- Never exceeds maxLen, no more mental math for callers
2026-02-18 20:21:51 +00:00
Hua 94a1b8664b refactor: extract message splitting logic to shared utils
- Move FindLast, findLast, and SplitMessage from discord.go to pkg/utils/message.go
- Update discord.go to use utils.SplitMessage()
- Makes splitting logic reusable across other channels
2026-02-18 20:01:53 +00:00
Artem Yadelskyi d07ac54eef feat(fmt): Fix fmt 2026-02-18 21:55:55 +02:00
Artem Yadelskyi 5ff4a0f0ef Merge branch 'main' into fix-formatting 2026-02-18 21:55:29 +02:00
Leandro Barbosa f7ec89d82d Merge pull request #411 from harshbansal7/frontmatter_fix
Bug Fix: Fix parsing of SKILL.md file frontmatter - regex
2026-02-18 16:48:47 -03:00
Artem Yadelskyi 9e120f90ea feat(fmt): Run formatters 2026-02-18 21:48:23 +02:00
harshbansal7 287100f303 Comments resolved 2026-02-18 23:13:47 +05:30
yinwm 09a0d19119 fix: add VLLM default API base and implement MaxTokensField support
1. Add VLLM default API base (http://localhost:8000/v1)
   - Previously returned empty string, causing provider creation to fail

2. Implement MaxTokensField configuration
   - Add maxTokensField field to HTTPProvider
   - Add NewHTTPProviderWithMaxTokensField constructor
   - Use configured field name for max_tokens parameter
   - Fallback to model-based detection for backward compatibility

3. Add tests for VLLM, deepseek, ollama default API bases

Example config usage:
{
  "model_name": "glm-4",
  "model": "openai/glm-4",
  "max_tokens_field": "max_completion_tokens"
}

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 01:43:24 +08:00
yinwm e1583f3b13 refactor: simplify legacy_provider.go from 349 to 49 lines
- Move OAuth helper functions to factory_provider.go
- Add auto-migration in LoadConfig: old providers -> model_list
- Add Workspace field to ModelConfig for CLI-based providers
- Fix OAuth handling to use auth store instead of raw APIKey
- Update tests to use new model_list configuration format

This eliminates the giant switch-case in legacy_provider.go,
achieving the goal of "zero-code provider addition" from the
design document (issue #283).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 01:30:19 +08:00
yinwm ef7078a356 refactor: reorganize commands and provider architecture
Refactor command handlers into separate files to improve code organization
and maintainability. Each command (agent, auth, cron, gateway, migrate,
onboard, skills, status) now has its own dedicated file.

Restructure provider creation to support new model_list configuration
system that enables zero-code addition of OpenAI-compatible providers.
Move legacy provider logic to separate file for backward compatibility.

Move configuration functions from config.go to separate files
(defaults.go, migration.go) for better organization.
2026-02-19 01:03:34 +08:00
yinwm a73d8e1a16 feat: add model_list configuration for zero-code provider addition
- Add ModelConfig struct with protocol prefix support (openai/, anthropic/, etc.)
- Implement GetModelConfig with round-robin load balancing
- Add CreateProviderFromConfig factory for protocol-based routing
- Add ModelRegistry for thread-safe endpoint selection
- Maintain full backward compatibility with legacy providers config
- Update README.md and README.zh.md with model_list documentation
- Add migration guide at docs/migration/model-list-migration.md

Supported protocols: openai, anthropic, antigravity, claude-cli, codex-cli,
github-copilot, openrouter, groq, deepseek, cerebras, qwen, zhipu, gemini

Closes #283

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 23:26:00 +08:00
Leandro Barbosa b1e3b11a5d Merge pull request #304 from mymmrac/golangci-lint
feat(linters): Added golangci-lint config & CI job
2026-02-18 12:16:58 -03:00
zepan 1b3da2ca29 1. update wechat group qrcode 2026-02-18 23:03:24 +08:00
Leandro Barbosa 1eb6b83b8f Merge pull request #418 from DanielVenturini/fix/add-missing-accentuation-in-ptbr-readme
docs(readme): add brazilian accentuation on pt-br README
2026-02-18 11:52:48 -03:00
Leandro Barbosa ab805fd5e3 Merge pull request #131 from Leeaandrob/feat/multi-agent-routing
feat: model fallback chain + multi-agent routing
2026-02-18 11:48:37 -03:00
Leandro Barbosa 447c17aeb1 merge: sync upstream/main (PR #213) into feat/multi-agent-routing
Resolve conflicts in pkg/providers/types.go and pkg/agent/loop.go:
- types.go: use protocoltypes aliases from PR #213, keep fallback types
- loop.go: drop old single-agent createToolRegistry (replaced by multi-agent pattern)

Refactor to align with PR #213 patterns:
- instance.go: use NewExecToolWithConfig (accept full config for deny patterns)
- registry.go: pass full config to NewAgentInstance
- loop.go: add Perplexity web search options to registerSharedTools
2026-02-18 11:39:14 -03:00
Artem Yadelskyi df52d4ad01 feat(linters): Fix linter 2026-02-18 16:26:35 +02:00
Artem Yadelskyi ef8965048a Merge branch 'main' into golangci-lint 2026-02-18 16:26:18 +02:00
Leandro Barbosa e61786cc6b Merge pull request #213 from jmahotiedu/refactor/provider-protocol-122
Refactor providers by protocol family (discussion #122)
2026-02-18 11:25:01 -03:00
Artem Yadelskyi b88f4c9ab5 feat(linters): Fix linter 2026-02-18 16:24:55 +02:00
Artem Yadelskyi 272cabc627 feat(linters): Fix version 2026-02-18 16:24:30 +02:00
Artem Yadelskyi d6f052f6b1 feat(linters): Fixed golangci-lint version 2026-02-18 16:23:31 +02:00
Artem Yadelskyi 24e35a199b Merge branch 'main' into golangci-lint
# Conflicts:
#	.github/workflows/release.yml
2026-02-18 16:22:43 +02:00
Artem Yadelskyi bebf4b0c17 Merge branch 'main' into telegram-bot-commands 2026-02-18 16:21:37 +02:00
Leandro Barbosa 8a3be993cd Merge remote-tracking branch 'upstream/main' into refactor/provider-protocol-122 2026-02-18 11:19:09 -03:00
Daniel Venturini f8bd883387 docs(readme): add brazilian accentuation on pt-br README 2026-02-18 11:11:41 -03:00
Leandro Barbosa 87aee78900 Merge pull request #337 from quybquang/docs/add-vietnamese-readme
docs: add Vietnamese README (README.vi.md)
2026-02-18 11:11:31 -03:00
Leandro Barbosa 2276bd149e merge: sync upstream/main, wire WebSearch through factory
Merge upstream/main into refactor/provider-protocol-122.
Resolve http_provider.go conflict (keep thin delegate).
Wire OpenAIProviderConfig.WebSearch through providerSelection
and into CodexProvider for codex-auth and codex-cli-token paths.
2026-02-18 11:09:18 -03:00
AlbertBui010 1e88df3ea8 Merge branch 'upstream/main' into docs/add-vietnamese-readme
Resolved conflicts in README.md, README.ja.md, and README.zh.md by keeping both Portuguese (upstream) and Vietnamese (local) language links.
2026-02-18 20:01:42 +07:00
lxowalle eda6e37332 feat: Support modifying the command filtering list of the exec tool (#410) 2026-02-18 19:31:15 +08:00
harshbansal7 02b5811b95 add support for \r as well 2026-02-18 16:58:27 +05:30
harshbansal7 994ec72d91 Fix parsing of SKILL.md file frontmatter - regex 2026-02-18 16:55:20 +05:30
Leandro Barbosa b77a40315e Merge pull request #218 from mattn/fix-readme-ja
Fix Japanese translation
2026-02-18 07:58:06 -03:00
Zenix 3390576eea Feature/websearch OpenAI (#118)
* feature: add web search for codex models

* fix: use more elegant way to solve the issue.
2026-02-18 16:30:30 +08:00
lxowalle 193fbcab11 docs: update PR template 2026-02-18 16:01:41 +08:00
lxowalle 01d694b998 fix: Add comprehensive command injection and system abuse prevention patterns (#401)
* Add comprehensive command injection and system abuse prevention patterns

* fix: Container running as root
2026-02-18 15:33:34 +08:00
Leandro Barbosa 8807d8254f Merge pull request #362 from blib/feat-bin-size
chore(build): reduce binary size by ~8 MB
2026-02-17 18:38:18 -03:00
Leandro Barbosa eeac7c7a67 Merge pull request #385 from Leeaandrob/docs/add-portuguese-br-readme
docs: add Brazilian Portuguese README (README.pt-br.md)
2026-02-17 17:56:36 -03:00
Leandro Barbosa f820da42d7 docs: add Brazilian Portuguese README (README.pt-br.md)
Add complete pt-BR translation of the README and update language
navigation links across all existing READMEs (English, Chinese,
Japanese) to include the Portuguese option.
2026-02-17 17:52:28 -03:00
Artem Yadelskyi d49ce32010 Merge branch 'main' into telegram-bot-commands 2026-02-17 20:21:46 +02:00
Artem Yadelskyi 0785a05a48 Merge branch 'main' into golangci-lint 2026-02-17 20:20:22 +02:00
Luna Reed acac1972e6 fix(exec): terminate process tree on timeout 2026-02-18 02:01:29 +08:00
AlbertBui010 8428446d69 docs: fix allow_from typo in config examples 2026-02-18 00:17:56 +07:00
QUY BUI QUANG 2ee2858912 Merge branch 'main' into docs/add-vietnamese-readme 2026-02-17 23:46:19 +07:00
AlbertBui010 b83304845e docs: resolve conflict in README.ja.md 2026-02-17 23:39:17 +07:00
yinwm 5d1669ecc4 Merge PR #343: Add Google Antigravity provider and harden tool-call compatibility 2026-02-18 00:13:24 +08:00
Jared Mahotiere c4cbb5fb35 providers: finalize PR213 review fixes
Phase 1: centralize protocol message/tool/response types in protocoltypes and keep compatibility aliases in providers and protocol packages.

Phase 1: preserve HTTPProvider constructor compatibility and route Anthropic api_base through factory auth/provider constructors with base URL normalization.

Phase 2: expand provider routing/auth tests (deepseek/nvidia/shengsuanyun, codex/claude oauth/codex-cli) and add openai_compat + anthropic coverage for proxy transport, model normalization, numeric option coercion, token-source refresh, and base URL behavior.

Phase 3: apply gofmt and validate with Dockerized tests (go test ./pkg/providers/... ./pkg/migrate and go test ./...).
2026-02-17 11:13:10 -05:00
Leandro Barbosa ba47892bcf Merge pull request #327 from humaid0x/fix-japanese-readme-link
docs: add missing Chinese language link to Japanese README
2026-02-17 13:12:47 -03:00
Leandro Barbosa 4fde0175cf Merge pull request #227 from mattn/fix-shadowing-running
Fix shadowing field runnnig
2026-02-17 12:58:48 -03:00
yinwm 6913edbb5b Merge PR #368: Add Volcengine (doubao) provider 2026-02-17 23:51:40 +08:00
yinwm 6992012737 Merge PR #333: Add Cerebras provider 2026-02-17 23:41:19 +08:00
yinwm de4ef9a8be Merge PR #365: Add Qwen provider 2026-02-17 23:37:55 +08:00
likeaturtle bb0eadded0 Optimize ./picoclaw status output to support all config file configurations. 2026-02-17 23:29:27 +08:00
zenix.huang 0d16525fab fix: codex tool call 2026-02-17 22:56:31 +08:00
zenix.huang 4cd3f99dd6 fix: remove max_tokens 2026-02-17 22:56:31 +08:00
likeaturtle 6cd419b6e2 Fix the case sensitivity issue when automatically recognizing VolcEngine LLM model names. 2026-02-17 22:49:43 +08:00
mrbeandev 84110aa408 fix(antigravity): preserve thought signature on tool call parts 2026-02-17 20:05:47 +05:30
mrbeandev 99c32714f1 fix(antigravity): sanitize invalid tool-call history ordering 2026-02-17 20:05:41 +05:30
mrbeandev caf3913347 fix(antigravity): normalize tool calls to avoid empty function names 2026-02-17 20:05:35 +05:30
mrbeandev d1655d5996 fix(antigravity): update default model from gemini-3-flash-preview to gemini-3-flash 2026-02-17 20:05:29 +05:30
mrbeandev 1765f6d0e7 fix: strip antigravity prefix and improve model list for flash-preview 2026-02-17 20:05:25 +05:30
mrbeandev d3fe8c5e17 feat: use gemini-3-flash-preview as default model name 2026-02-17 20:05:20 +05:30
mrbeandev d28fc0d48d docs: update manual auth instructions 2026-02-17 20:05:15 +05:30
mrbeandev 29e07ec7b4 feat: add manual callback URL entry for headless OAuth flow 2026-02-17 20:05:10 +05:30
mrbeandev 848aaedc24 feat: complete Antigravity provider integration with robust error handling and docs 2026-02-17 20:05:06 +05:30
mrbeandev 33915fb712 fix(gemini): preserve thought_signature in tool calls to prevent 400 errors 2026-02-17 20:04:45 +05:30
likeaturtle 2f24be6c59 add Volcengine LLM (doubao) support 2026-02-17 22:31:19 +08:00
Jared Mahotiere e3c246a36f Merge origin/main into refactor/provider-protocol-122 2026-02-17 09:28:56 -05:00
HansonJames f0e90e6379 feat: Add the Qwen provider 2026-02-17 22:07:58 +08:00
Boris Bliznioukov 2d876eaa98 feat(goreleaser): enhance build flags with versioning and commit info
Signed-off-by: Boris Bliznioukov <blib@mail.com>
2026-02-17 15:00:06 +01:00
Boris Bliznioukov 2d758d714f feat(goreleaser): add 'stdjson' tag to picoclaw build configuration
Signed-off-by: Boris Bliznioukov <blib@mail.com>
2026-02-17 14:55:37 +01:00
lxowalle 920e30a241 fix:pr-272 reverted the changes from pr-227 (#361) 2026-02-17 21:31:54 +08:00
Boris Bliznioukov ad747e8e89 fix(Makefile): update LDFLAGS and GOFLAGS for optimized build size
Signed-off-by: Boris Bliznioukov <blib@mail.com>
2026-02-17 14:27:03 +01:00
daming大铭 7b9b8104c8 Merge pull request #225 from yinwm/feat/cron-exec-timeout-config
feat(cron): add configurable execution timeout for cron jobs
2026-02-17 21:12:59 +08:00
yinwm 881999aceb refactor(shell): interpret zero timeout as unlimited execution
Replace unconditional WithTimeout usage with conditional context creation
based on timeout configuration. Zero values now bypass timeout enforcement,
using WithCancel for graceful cancellation while preserving existing timeout
behavior for positive values. Simplifies CronTool initialization by removing
unnecessary conditional timeout assignment.
2026-02-17 21:10:20 +08:00
Hua Audio f929268ab2 feat: Add Perplexity search provider integration (#138)
* feat: Add Perplexity search provider integration

- Add PerplexityConfig struct to config package
- Add PerplexitySearchProvider implementing SearchProvider interface
- Update WebSearchTool to support Perplexity with priority system (Perplexity > Brave > DuckDuckGo)
- Update agent loop to pass Perplexity config options
- Update config.example.json with Perplexity configuration template
- Uses Perplexity's 'sonar' model for web search capabilities

* Edit config example

* make fmt

---------

Co-authored-by: Hua <zhangmikoto@gmail.com>
2026-02-17 21:02:56 +08:00
yinwm 684e7413e1 Merge remote-tracking branch 'origin/main' into feat/cron-exec-timeout-config 2026-02-17 20:53:31 +08:00
zepan da79c201c7 1. fix typo 2026-02-17 18:03:02 +08:00
zepan 5fb2721d22 1. add android phone termux quick guide 2026-02-17 18:01:39 +08:00
zepan 951b05d255 1. add AI Code Generation selection in pr template 2026-02-17 17:15:40 +08:00
zepan ac4b16dfb4 1. rename doc to docs 2026-02-17 16:51:38 +08:00
zepan 0fadbcd340 1. add roadmap.md 2026-02-17 16:03:07 +08:00
Guoguo a961a2df87 fix(ci): use env var for release tag (#342)
Signed-off-by: Guoguo <i@qwq.trade>
2026-02-17 14:32:51 +08:00
yuchou87 e38364b08a build(docker): migrate full image from Debian to Alpine base
Replace node:24-bookworm-slim with node:24-alpine3.23 to reduce
image size and improve build efficiency.

Changes:
- Base image: node:24-bookworm-slim → node:24-alpine3.23
- Package manager: apt-get → apk
- Package names: python3-pip → py3-pip
- Remove python3-venv (included in Alpine Python3)
- Use apk --no-cache for cleaner image layers

Expected benefits:
- Reduce base image size by ~100-200MB (30-40% reduction)
- Faster image pulls and container startup
- Full MCP support maintained (Node.js, Python, uv)

Estimated final image size: ~600-700MB (vs ~800MB before)
2026-02-17 10:57:38 +08:00
yuchou87 4113190c2a chore(config): remove example MCP servers from default config
Remove pre-populated example servers (filesystem, github, brave-search,
postgres) from DefaultConfig() to reduce memory footprint per instance.

Changes:
- Set MCP.Servers to empty map instead of 4 example servers
- Reduces default config size by ~500 bytes per instance
- Users should add MCP servers via config.json or documentation

Example configurations are still available in:
- README.md MCP section
- config.example.json
- Official MCP documentation

This optimization benefits deployments with many agent instances.
2026-02-17 10:41:29 +08:00
yuchou87 6892d006d6 perf(agent): reduce memory footprint by storing minimal MCP dependencies
Replace full *config.Config reference with config.MCPConfig value type
in AgentLoop to allow garbage collection of unused configuration data.

Changes:
- AgentLoop now stores only MCPConfig and workspacePath (minimal deps)
- Add mcp.Manager.LoadFromMCPConfig() for minimal dependency version
- Keep LoadFromConfig() for backward compatibility
- Full Config object can be GC'd after NewAgentLoop() returns

This optimization reduces memory usage by not holding references to
unused channel, provider, gateway, and device configurations.
2026-02-17 10:39:39 +08:00
AlbertBui010 75fb728a11 docs: add Vietnamese README (README.vi.md)
- Add full Vietnamese translation of README.md
- Update language selector links in README.md, README.zh.md, README.ja.md
2026-02-17 09:17:03 +07:00
zepan 57dac394c5 update pr template 2026-02-17 09:30:30 +08:00
Yasuhiro Matsumoto 5772b9241b Better nuance 2026-02-17 08:25:21 +09:00
Yasuhiro Matsumoto 852d361eb0 Add new provider cerebras 2026-02-17 08:23:44 +09:00
Humaid Koreshi ff3c875b3f docs: add missing Chinese language link to Japanese README 2026-02-17 02:15:59 +06:00
Artem Yadelskyi 67d07109a9 feat(linters): Removed fmt check (present in linters) 2026-02-16 17:15:02 +02:00
Artem Yadelskyi 552d6f10ea Merge branch 'main' into golangci-lint 2026-02-16 17:14:19 +02:00
Artem Yadelskyi d9b5f64777 feat(linters): Temporarily disable most linters 2026-02-16 17:13:35 +02:00
Leandro Barbosa 12007b5670 merge: sync upstream/main into feat/multi-agent-routing
Resolve conflicts:
- pkg/agent/loop.go: integrate context compression, command handling,
  utf8 token estimation, and summarization notification into
  multi-agent routing architecture
- pkg/config/config_test.go: merge imports from both branches
- pkg/agent/loop_test.go: update test to use registry-based sessions
2026-02-16 10:34:55 -03:00
yuchou87 0f6fadb445 fix(agent): register MCP tools after server initialization
Critical bug fix:
- MCP tools were never registered because servers loaded in Run()
  but tool registration happened in NewAgentLoop() with empty manager
- Move MCP tool registration from createToolRegistry to Run()
- Register MCP tools for both main agent and subag after successful server loading
- Add subagentManager field to AgentLoop for dynamic registration
- Add tool_count logging for better observability

This ensures MCP tools are properly available to both agent and subagents.
2026-02-16 20:00:37 +08:00
yuchou87 2318232b71 fix(agent): ensure MCP cleanup on all Run() exit paths
- Add defer in Run() to guarantee MCP connection cleanup
- Handles both normal termination and context cancellation
- Prevents resource leaks when Run() exits via ctx.Done()
- MCP Manager.Close() is idempotent, safe to call from both defer and Stop()

This fixes GitHub Copilot feedback that MCP cleanup only happened in
Stop() but Run() could return on ctx.Done() without cleanup, causing
subprocess/session leaks on normal cancellation shutdown.
2026-02-16 19:56:00 +08:00
yuchou87 aed7296c0d fix(agent): tie MCP connections to agent lifecycle context
- Defer MCP server initialization to Run() using agent's context
- Add mcpConfig and mcpInitOnce fields to AgentLoop
- Use sync.Once to ensure MCP loads exactly once with proper context
- Prevents orphaned subprocesses and resource leaks on cancellation

This fixes GitHub Copilot feedback that MCP connections with
context.Background() won't terminate when the agent stops, causing
potential resource leaks and orphaned stdio/SSE connections.
2026-02-16 19:53:15 +08:00
Artem Yadelskyi d69ef653df feat(linters): Added job names 2026-02-16 13:51:10 +02:00
yuchou87 02c1792015 fix(tools): preserve MCP tool InputSchema via JSON marshal/unmarshal
- Handle json.RawMessage and []byte types by direct unmarshal
- Use JSON marshal/unmarshal for struct types to preserve schema
- Add test case for json.RawMessage schema
- Fixes issue where non-map schemas returned empty object

This fixes GitHub Copilot feedback that Parameters() was dropping
tool schema when InputSchema wasn't already map[string]interface{}
2026-02-16 19:50:00 +08:00
Artem Yadelskyi 35670d5a58 feat(linters): Added golangci-lint config & CI job 2026-02-16 13:45:36 +02:00
yuchou87 20f8bb200b refactor(tools): use MCPManager interface in NewMCPTool constructor
- Change NewMCPTool to accept MCPManager interface instead of concrete *mcp.Manager
- Remove unused mcpPkg import from mcp_tool.go
- Remove newMCPToolForTest helper function as NewMCPTool now accepts interface
- Update all tests to use NewMCPTool directly with MockMCPManager
- Improves testability and follows dependency inversion principle
2026-02-16 19:43:05 +08:00
yuchou87 a026d56c0f chore(deps): consolidate indirect require for uritemplate
- Move standalone indirect require line into existing require block
- Maintain alphabetical ordering of dependencies
- Keep module file stable and avoid churn
2026-02-16 19:40:14 +08:00
yuchou87 a4265b3f16 fix(mcp): resolve relative envFile paths against workspace directory
- Resolve relative envFile paths relative to workspace instead of CWD
- Add filepath import for path operations
- Pass workspace path to goroutines for path resolution
- Improves portability in Docker environments where CWD may vary
- Absolute envFile paths continue to work as before
2026-02-16 19:38:27 +08:00
yuchou87 77d26e5ce3 fix(mcp): return aggregated error when all servers fail to connect
- Add errors.Join to return aggregated error when all enabled MCP servers fail
- Track enabled server count separately from total configured servers
- Return error only when all servers fail, not for partial failures
- Improve logging with accurate server counts (enabled vs connected)
- Maintains fault tolerance: partial failures don't stop initialization
2026-02-16 19:33:31 +08:00
Artem Yadelskyi 1b1e472df2 feat(telegram): Changed command scope 2026-02-16 13:25:24 +02:00
Artem Yadelskyi d1a66cbf50 feat(telegram): Fix text 2026-02-16 13:24:21 +02:00
Artem Yadelskyi bfb9d8f644 feat(telegram): Init bot commands on start 2026-02-16 13:20:36 +02:00
yuchou87 24610693e4 chore(docker): add execute permission to test script
Make scripts/test-docker-mcp.sh executable
2026-02-16 16:40:35 +08:00
yuchou87 87e0336d62 chore(deps): format go.mod
Add blank line for better formatting consistency
2026-02-16 16:38:21 +08:00
yuchou87 acb974fcf1 Merge branch 'main' into mcp-tools-support 2026-02-16 16:37:24 +08:00
yuchou87 e91e716958 fix(docker): use service names instead of --profile flag for build
Replace --profile flags with explicit service names in build commands.
The 'docker compose build' command does not support --profile flag;
profiles are only used for runtime operations like 'up' and 'run'.

Changes:
- docker-build: specify picoclaw-agent picoclaw-gateway
- docker-build-full: specify picoclaw-agent picoclaw-gateway

Fixes: unknown flag: --profile error
2026-02-16 16:02:10 +08:00
yuchou87 fcedba1c9d fix(docker): add profiles to build commands
Add --profile gateway --profile agent flags to docker build commands
to ensure services are built even when using profiles in compose files.

Without profiles specified, docker compose build skips all services
that have a profile defined, resulting in 'No services to build' warning.

Changes:
- docker-build: add --profile flags
- docker-build-full: add --profile flags

Fixes: WARN[0000] No services to build
2026-02-16 15:59:34 +08:00
yuchou87 1764181e6f fix(docker): correct uv installation path
Fix uv symlink path from /root/.cargo/bin to /root/.local/bin.
The uv installer puts binaries in ~/.local/bin, not ~/.cargo/bin.

Changes:
- Update uv symlink source: /root/.local/bin/uv
- Add uvx symlink as well (installed alongside uv)

Fixes: /bin/sh: 1: uv: not found error during build
2026-02-16 15:57:30 +08:00
yuchou87 1c9c32022e fix(docker): ensure uv is accessible in system PATH
Symlink uv from /root/.cargo/bin to /usr/local/bin to make it
accessible without relying on ENV PATH setting. Add version check
to verify successful installation during build.

Changes:
- Symlink uv to /usr/local/bin/uv
- Add 'uv --version' validation step
- Remove ENV PATH setting (no longer needed)

Fixes: uv: not found error in test script
2026-02-16 15:52:59 +08:00
yuchou87 c05742330d refactor(docker): migrate to docker compose v2 syntax
Replace docker-compose (v1) with docker compose (v2) command syntax
across all files. Docker Compose v2 is now the default in modern
Docker installations and uses 'docker compose' instead of 'docker-compose'.

Changes:
- scripts/test-docker-mcp.sh: update all 8 docker-compose commands
- Makefile: update all 8 docker-compose commands in docker-* targets
- No changes to file names (docker-compose.full.yml remains as-is)

Compatibility: Requires Docker with Compose v2 plugin (Docker Desktop
or docker-compose-plugin package)
2026-02-16 15:50:46 +08:00
yuchou87 b9c2b3555a fix(docker): override entrypoint in test script to avoid interactive mode
Add --entrypoint sh flag to docker-compose run commands in test script
to bypass picoclaw agent's interactive mode. This allows direct command
execution for testing MCP tools.

Changes:
- Add --entrypoint sh to all docker-compose run commands
- Use SERVICE variable for better maintainability
- Simplify command syntax: sh -c 'cmd' → -c 'cmd'
2026-02-16 15:49:14 +08:00
yuchou87 51ed54a414 refactor(docker): switch to node:24-bookworm-slim base image
Replace debian:bookworm-slim with node:24-bookworm-slim to:
- Use latest Node.js 24 LTS and npm
- Fix npm version compatibility issues (npm@11 requires node >=20.17)
- Simplify Dockerfile by removing nodejs/npm installation
- Better optimization from official Node.js image

Changes:
- Dockerfile.full: use node:24-bookworm-slim base
- Remove nodejs, npm installation steps
- Remove npm upgrade step (included in base image)
- Update Makefile descriptions to reflect Node.js 24
2026-02-16 15:40:07 +08:00
yuchou87 ce3fc4bc67 feat(docker): add full-featured Docker image with MCP tools support
Add Dockerfile.full with Debian-based runtime including git, nodejs, npm, python3, and uv for MCP servers. Add docker-compose.full.yml with npm cache optimization. Add Makefile targets for docker-build-full, docker-run-full, and docker-test. Add test script for MCP tools validation.
2026-02-16 14:48:40 +08:00
Yasuhiro Matsumoto 7ce5b75178 Fix shadowing field runnnig 2026-02-16 00:47:17 +09:00
Yasuhiro Matsumoto 97bf4ff3fd Fix Japanese translation 2026-02-15 23:56:13 +09:00
Jared Mahotiere 362c49a69d docs(test): document protocol architecture and migration compatibility 2026-02-15 08:04:16 -05:00
Jared Mahotiere 762565b0d4 refactor(providers): move anthropic logic to protocol package 2026-02-15 08:04:12 -05:00
Jared Mahotiere a6e885bb47 refactor(providers): extract protocol factory and openai-compat transport 2026-02-15 08:04:07 -05:00
yinwm 40f90281e5 Merge remote-tracking branch 'upstream/main' into feat/cron-exec-timeout-config 2026-02-15 18:41:54 +08:00
yinwm 82856bc57a feat(cron): add configurable execution timeout for cron jobs
Add a new configuration option `exec_timeout_minutes` under the `tools.cron`
section to control the maximum execution time for cron jobs. The default
timeout is set to 5 minutes, which is appropriate for LLM operations.

The configuration can be set in the config file or via the
`PICOCLAW_TOOLS_CRON_EXEC_TIMEOUT_MINUTES` environment variable. A value of
0 disables the timeout entirely.

This change improves system reliability by preventing cron jobs from running
indefinitely in case of unexpected failures or hanging processes.
2026-02-15 18:41:39 +08:00
yuchou87 91c168db20 feat(mcp): add Model Context Protocol integration
Implement comprehensive MCP support with stdio/HTTP/SSE transports, environment variable configuration (env and envFile), custom headers, tool registration, and automatic resource cleanup. Includes full test coverage and VSCode-compatible configuration.

- Added pkg/mcp/manager.go for server lifecycle management
- Added pkg/tools/mcp_tool.go for tool wrapping
- Integrated into agent loop with cleanup
- Support for envFile loading (.env format)
- Headers injection for HTTP/SSE authentication
- Example configs for filesystem, github, brave-search, postgres
2026-02-15 17:26:36 +08:00
Leandro Barbosa 5e89264536 merge: sync upstream/main into feat/multi-agent-routing
Update registerSharedTools to use new WebSearchToolOptions API and
add hardware tools (I2C, SPI) from upstream. Accept upstream's
new web tools config test.
2026-02-14 10:38:04 -03:00
Leandro Barbosa 0f5b2f67bb style: fix gofmt formatting in cooldown files
Remove extra spaces in comment alignment to pass fmt-check CI.
2026-02-13 12:26:44 -03:00
Leandro Barbosa 8a6fb7d9e3 merge: sync upstream/main into feat/multi-agent-routing
Resolve conflicts in loop.go, config.go, config_test.go,
spawn.go, and subagent.go. Integrate upstream ToolResult/AsyncTool
pattern with multi-agent routing features. Rename mockProvider
to mockRegistryProvider in registry_test.go to avoid redeclaration
with upstream's loop_test.go.
2026-02-13 12:24:26 -03:00
Leandro Barbosa 272536a11a feat: add multi-agent routing with declarative bindings
Implement per-agent workspace/model/session isolation with 7-level
priority routing cascade (peer > parent_peer > guild > team > account >
channel > default). Backward compatible - empty agents.list creates
implicit "main" agent from defaults.

Core components:
- routing/agent_id.go: ID normalization with pre-compiled regex
- routing/session_key.go: 4 DM scope modes with identity links
- routing/route.go: RouteResolver with priority-based binding matcher
- agent/instance.go: Per-agent state (workspace, sessions, tools, model)
- agent/registry.go: Agent lifecycle, route resolution, subagent ACL

Integration:
- config.go: AgentModelConfig (flexible JSON), bindings, session config
- loop.go: Complete rewrite for multi-agent dispatch
- Channel adapters: peer_kind/peer_id metadata (telegram, discord, slack)
- spawn.go: Subagent allowlist enforcement per agent

Validated end-to-end with Discord channel-based bindings, default
fallback routing, and per-agent session persistence.
2026-02-13 12:12:33 -03:00
Leandro Barbosa 6e7149509a feat: add model fallback chain with error classification
Add 2-layer fallback system (text + image) with automatic candidate
resolution. Includes error classifier (~40 patterns), per-provider
cooldown (exponential backoff), and model reference parsing.

- FailoverError/FailoverReason types for structured error handling
- ErrorClassifier with rate_limit, billing, auth, timeout patterns
- FallbackChain with cooldown management and candidate rotation
- ModelRef parser for provider/model string format
- 128 tests, 95%+ coverage
2026-02-13 12:12:12 -03:00
1382 changed files with 346255 additions and 16870 deletions
+2 -1
View File
@@ -1,3 +1,5 @@
# Do NOT exclude LICENSE or .github — scripts/copydir.go uses them as repo-root anchors
# during `go generate`, which runs inside `make build` in the Dockerfile.
.git
.gitignore
build/
@@ -6,5 +8,4 @@ config/
.env
.env.example
*.md
LICENSE
assets/
+7 -4
View File
@@ -5,15 +5,18 @@
# ANTHROPIC_API_KEY=sk-ant-xxx
# OPENAI_API_KEY=sk-xxx
# GEMINI_API_KEY=xxx
# MODELSCOPE_API_KEY=xxx
# CLAUDE_CODE_OAUTH=xxx
# ── Chat Channel ──────────────────────────
# TELEGRAM_BOT_TOKEN=123456:ABC...
# DISCORD_BOT_TOKEN=xxx
# LINE_CHANNEL_SECRET=xxx
# LINE_CHANNEL_ACCESS_TOKEN=xxx
# Feishu (飞书)
# PICOCLAW_CHANNELS_FEISHU_APP_ID=cli_xxx
# PICOCLAW_CHANNELS_FEISHU_APP_SECRET=xxx
# PICOCLAW_CHANNELS_FEISHU_RANDOM_REACTION_EMOJI=Typing,OneSecond
# ── Web Search (optional) ────────────────
# BRAVE_SEARCH_API_KEY=BSA...
# ── Timezone ──────────────────────────────
TZ=Asia/Tokyo
TZ=Asia/Shanghai
+4
View File
@@ -0,0 +1,4 @@
# Ensure shell scripts always use LF line endings regardless of OS.
*.sh text eol=lf
docker/entrypoint.sh text eol=lf
.gitignore text eol=lf
+3
View File
@@ -0,0 +1,3 @@
# These are supported funding model platforms
github: [sipeed]
+27
View File
@@ -0,0 +1,27 @@
version: 2
updates:
# Go dependencies (entire repo)
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
labels:
- "dependencies"
- "go"
# Frontend dependencies
- package-ecosystem: "npm"
directory: "/web/frontend"
schedule:
interval: "weekly"
labels:
- "dependencies"
- "frontend"
# GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
+43
View File
@@ -0,0 +1,43 @@
## 📝 Description
<!-- Please briefly describe the changes and purpose of this PR -->
## 🗣️ Type of Change
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (non-breaking change which adds functionality)
- [ ] 📖 Documentation update
- [ ] ⚡ Code refactoring (no functional changes, no api changes)
## 🤖 AI Code Generation
- [ ] 🤖 Fully AI-generated (100% AI, 0% Human)
- [ ] 🛠️ Mostly AI-generated (AI draft, Human verified/modified)
- [ ] 👨‍💻 Mostly Human-written (Human lead, AI assisted or none)
## 🔗 Related Issue
<!-- Please link the related issue(s) (e.g., Fixes #123, Closes #456) -->
## 📚 Technical Context (Skip for Docs)
- **Reference URL:**
- **Reasoning:**
## 🧪 Test Environment
- **Hardware:** <!-- e.g. Raspberry Pi 5, Orange Pi, PC-->
- **OS:** <!-- e.g. Debian 12, Ubuntu 22.04 -->
- **Model/Provider:** <!-- e.g. OpenAI GPT-4o, Kimi k2, DeepSeek-V3 -->
- **Channels:** <!-- e.g. Discord, Telegram, Feishu, ... -->
## 📸 Evidence (Optional)
<details>
<summary>Click to view Logs/Screenshots</summary>
<!-- Please paste relevant screenshots or logs here -->
</details>
## ☑️ Checklist
- [ ] My code/docs follow the style of this project.
- [ ] I have performed a self-review of my own changes.
- [ ] I have updated the documentation accordingly.
+16 -10
View File
@@ -2,24 +2,30 @@ name: build
on:
push:
branches: ["main"]
branches: [ "main" ]
jobs:
build:
integration:
name: Integration Tests
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Run Docker-backed integration suites
run: bash ./scripts/run-integration-tests.sh
build:
needs: integration
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version-file: go.mod
- name: fmt
run: |
make fmt
git diff --exit-code || (echo "::error::Code is not formatted. Run 'make fmt' and commit the changes." && exit 1)
- name: Build
- name: Build core binaries
run: make build-all
+60
View File
@@ -0,0 +1,60 @@
name: Create Tag
on:
workflow_dispatch:
inputs:
tag:
description: "Tag name (required, e.g. v0.2.0)"
required: true
type: string
commit:
description: "Target commit SHA (leave empty for latest main)"
required: false
type: string
default: ""
jobs:
create-tag:
name: Create Git Tag
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: main
- name: Validate commit exists
if: ${{ inputs.commit != '' }}
shell: bash
run: |
if ! git cat-file -t "${{ inputs.commit }}" &>/dev/null; then
echo "::error::Commit '${{ inputs.commit }}' does not exist."
exit 1
fi
- name: Check tag does not already exist
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if gh api "repos/${{ github.repository }}/git/ref/tags/${{ inputs.tag }}" --silent 2>/dev/null; then
echo "::error::Tag '${{ inputs.tag }}' already exists."
exit 1
fi
- name: Create and push tag
shell: bash
run: |
TARGET="${{ inputs.commit || 'HEAD' }}"
COMMIT_SHA=$(git rev-parse "$TARGET")
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -a "${{ inputs.tag }}" "$COMMIT_SHA" -m "Release ${{ inputs.tag }}"
git push origin "${{ inputs.tag }}"
echo "### Tag Created" >> "$GITHUB_STEP_SUMMARY"
echo "- **Tag:** \`${{ inputs.tag }}\`" >> "$GITHUB_STEP_SUMMARY"
echo "- **Commit:** \`${COMMIT_SHA}\`" >> "$GITHUB_STEP_SUMMARY"
echo "- **Branch:** \`$(git branch -r --contains "$COMMIT_SHA" | head -1 | xargs)\`" >> "$GITHUB_STEP_SUMMARY"
+71
View File
@@ -0,0 +1,71 @@
name: Create macOS DMG
on:
workflow_dispatch:
jobs:
build:
name: Build ${{ matrix.arch }}
runs-on: macos-latest
strategy:
matrix:
# This creates two parallel jobs
arch: [arm64, amd64]
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: main
# 1. Install Go from go.mod
- name: Setup Go
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
# 3. Build the application bundle
- name: Build with Make
run: make build ARCH=${{ matrix.arch }} && make build-macos-app ARCH=${{ matrix.arch }}
# 4. Apply ad-hoc signing
- name: Ad-hoc Sign
run: codesign --force --deep --sign - "build/PicoClaw Launcher.app"
# 5. Install the DMG packaging tool
- name: Install create-dmg
run: brew install create-dmg
# 6. Create the DMG
- name: Create DMG
run: |
mkdir -p dist
create-dmg \
--volname "PicoClaw Installer" \
--window-pos 200 120 \
--window-size 800 400 \
--icon-size 100 \
--icon "PicoClaw Launcher.app" 200 190 \
--hide-extension "PicoClaw Launcher.app" \
--app-drop-link 600 185 \
"dist/picoclaw-${{ matrix.arch }}.dmg" \
"build/PicoClaw Launcher.app"
# 7. Upload the DMG as a GitHub artifact
- name: Upload DMG
uses: actions/upload-artifact@v7
with:
name: macos-dmg-${{ matrix.arch }}
path: dist/*.dmg
+5 -5
View File
@@ -25,17 +25,17 @@ jobs:
steps:
# ── Checkout ──────────────────────────────
- name: 📥 Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
ref: ${{ inputs.tag }}
# ── Docker Buildx ─────────────────────────
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
# ── Login to GHCR ─────────────────────────
- name: 🔑 Login to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ${{ env.GHCR_REGISTRY }}
username: ${{ github.actor }}
@@ -43,7 +43,7 @@ jobs:
# ── Login to Docker Hub ────────────────────
- name: 🔑 Login to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ${{ env.DOCKERHUB_REGISTRY }}
username: ${{ secrets.DOCKERHUB_USERNAME }}
@@ -62,7 +62,7 @@ jobs:
# ── Build & Push ──────────────────────────
- name: 🚀 Build and push Docker image
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
push: true
+146
View File
@@ -0,0 +1,146 @@
name: Nightly Build
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
permissions:
contents: read
jobs:
nightly:
name: Nightly Build
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- 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
COMPARE_URL="https://github.com/${{ github.repository }}/commits/main"
if [ -n "$BASE_VERSION" ] && [ "$BASE_VERSION" != "v0.0.0" ]; then
COMPARE_URL="https://github.com/${{ github.repository }}/compare/${BASE_VERSION}...main"
fi
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "changelog=**Full Changelog**: $COMPARE_URL" >> "$GITHUB_OUTPUT"
- name: Setup Go from go.mod
id: setup-go
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: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
registry: docker.io
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Install zip
run: sudo apt-get install -y zip
- name: Create local tag for GoReleaser
run: git tag "${{ steps.version.outputs.version }}"
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v7
with:
distribution: goreleaser
version: ~> v2
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }}
DOCKERHUB_IMAGE_NAME: ${{ vars.DOCKERHUB_REPOSITORY }}
GOVERSION: ${{ steps.setup-go.outputs.go-version }}
GORELEASER_CURRENT_TAG: ${{ steps.version.outputs.version }}
INCLUDE_ANDROID_BUNDLE: "true"
NIGHTLY_BUILD: "true"
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 }}
- name: Update nightly release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ steps.version.outputs.version }}
run: |
CHANGELOG='${{ steps.version.outputs.changelog }}'
NOTES=$(cat <<EOF
Nightly build for **${VERSION}**
This is an automated build and may be unstable. Use with caution.
${CHANGELOG}
EOF
)
# Delete existing nightly release and tag
gh release delete nightly --cleanup-tag -y 2>/dev/null || true
# Force-update nightly tag to current HEAD
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -fa nightly -m "Nightly build ${VERSION}"
git push origin nightly
# Collect release artifacts from goreleaser dist/
ASSETS=()
for f in dist/*.tar.gz dist/*.zip dist/*.deb dist/*.rpm dist/checksums.txt build/picoclaw-android-universal.zip; do
[ -f "$f" ] && ASSETS+=("$f")
done
# Create nightly release (prerelease, NOT latest)
gh release create nightly \
--title "Nightly Build" \
--notes "$NOTES" \
--target "${{ github.sha }}" \
--prerelease \
--latest=false \
"${ASSETS[@]}"
+47 -29
View File
@@ -1,52 +1,61 @@
name: pr-check
name: PR
on:
pull_request:
pull_request: { }
jobs:
fmt-check:
lint:
name: Linter
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Check formatting
run: |
make fmt
git diff --exit-code || (echo "::error::Code is not formatted. Run 'make fmt' and commit the changes." && exit 1)
vet:
runs-on: ubuntu-latest
needs: fmt-check
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version-file: go.mod
- name: Run go generate
run: go generate ./...
- name: Run go vet
run: go vet ./...
- name: Golangci Lint
uses: golangci/golangci-lint-action@v9
with:
version: v2.10.1
args: --build-tags=goolm,stdjson
test:
vuln_check:
name: Security Check
runs-on: ubuntu-latest
needs: fmt-check
env:
GOFLAGS: -tags=goolm,stdjson
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Setup Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version-file: go.mod
- name: Install govulncheck
run: go install golang.org/x/vuln/cmd/govulncheck@v1.1.4
- name: Run Govulncheck
run: govulncheck -C . -format text ./...
test:
name: Tests
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
@@ -54,5 +63,14 @@ jobs:
run: go generate ./...
- name: Run go test
run: go test ./...
run: go test -tags goolm,stdjson ./...
integration:
name: Integration Tests
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Run Docker-backed integration suites
run: bash ./scripts/run-integration-tests.sh
+57 -29
View File
@@ -1,10 +1,10 @@
name: Create Tag and Release
name: Release
on:
workflow_dispatch:
inputs:
tag:
description: "Release tag (required, e.g. v0.2.0)"
description: "Existing tag to release (e.g. v0.2.0)"
required: true
type: string
prerelease:
@@ -17,68 +17,80 @@ on:
required: false
type: boolean
default: false
upload_tos:
description: "Upload to Volcengine TOS"
required: false
type: boolean
default: true
jobs:
create-tag:
name: Create Git Tag
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Create and push tag
shell: bash
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -a "${{ inputs.tag }}" -m "Release ${{ inputs.tag }}"
git push origin "${{ inputs.tag }}"
release:
name: GoReleaser Release
needs: create-tag
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
steps:
- name: Verify tag exists
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if ! gh api "repos/${{ github.repository }}/git/ref/tags/${{ inputs.tag }}" --silent 2>/dev/null; then
echo "::error::Tag '${{ inputs.tag }}' does not exist. Create it first using the 'Create Tag' workflow."
exit 1
fi
- name: Checkout tag
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ inputs.tag }}
- name: Setup Go from go.mod
uses: actions/setup-go@v5
id: setup-go
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: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: docker.io
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Install zip
run: sudo apt-get install -y zip
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
uses: goreleaser/goreleaser-action@v7
with:
distribution: goreleaser
version: ~> v2
@@ -87,6 +99,13 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }}
DOCKERHUB_IMAGE_NAME: ${{ vars.DOCKERHUB_REPOSITORY }}
GOVERSION: ${{ steps.setup-go.outputs.go-version }}
INCLUDE_ANDROID_BUNDLE: "true"
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 }}
- name: Apply release flags
shell: bash
@@ -96,3 +115,12 @@ jobs:
gh release edit "${{ inputs.tag }}" \
--draft=${{ inputs.draft }} \
--prerelease=${{ inputs.prerelease }}
upload-tos:
name: Upload to TOS
needs: release
if: ${{ inputs.upload_tos }}
uses: ./.github/workflows/upload-tos.yml
with:
tag: ${{ inputs.tag }}
secrets: inherit
+64
View File
@@ -0,0 +1,64 @@
name: Close stale issues and PRs
on:
schedule:
# Run daily at 03:00 JST (18:00 UTC)
- cron: "0 18 * * *"
workflow_dispatch:
permissions:
issues: write
pull-requests: write
jobs:
stale:
runs-on: ubuntu-latest
steps:
- name: Mark and close stale issues and PRs
uses: actions/stale@v10
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
# ── Issue: 7 days inactive → stale; 7 more days → close ──
days-before-issue-stale: 7
days-before-issue-close: 7
stale-issue-label: "stale"
stale-issue-message: >
This issue has had no activity for 7 days and has been marked as stale.
If it is still relevant, please reply or update; otherwise it will be
closed automatically in 7 days.
close-issue-message: >
This issue has been closed after 14 days of inactivity.
If it is still needed, feel free to reopen it anytime.
close-issue-reason: "not_planned"
# ── PR: 7 days inactive → stale; 7 more days → close ──
days-before-pr-stale: 7
days-before-pr-close: 7
stale-pr-label: "stale"
stale-pr-message: >
This PR has had no activity for 7 days and has been marked as stale.
If you are still working on it, please push an update or leave a comment;
otherwise it will be closed automatically in 7 days.
close-pr-message: >
This PR has been closed after 14 days of inactivity.
If you would like to continue, feel free to reopen it or submit a new PR.
# ── Protected labels (exempt from stale processing) ──
exempt-issue-labels: "pinned,keep-open,wip,do-not-close,type: roadmap"
exempt-pr-labels: "pinned,keep-open,wip,do-not-close,type: roadmap"
# ── Exempt draft PRs ──
exempt-draft-pr: true
# ── Remove stale label when activity resumes ──
remove-stale-when-updated: true
remove-issue-stale-when-updated: true
remove-pr-stale-when-updated: true
# ── Scan oldest items first so old stale items are not starved ──
ascending: true
# ── Throttle: max operations per run ──
operations-per-run: 500
+49
View File
@@ -0,0 +1,49 @@
name: Upload to Volcengine TOS
on:
workflow_dispatch:
inputs:
tag:
description: "Release tag to download and upload (e.g. v0.2.0)"
required: true
type: string
workflow_call:
inputs:
tag:
description: "Release tag to download and upload"
required: true
type: string
jobs:
upload-tos:
name: Upload to Volcengine TOS
runs-on: ubuntu-latest
steps:
- name: Download release assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mkdir -p artifacts
gh release download "${{ inputs.tag }}" \
--repo "${{ github.repository }}" \
--dir artifacts \
--pattern "*.tar.gz" \
--pattern "*.zip" \
--pattern "*.rpm" \
--pattern "*.deb"
- name: Upload to Volcengine TOS
env:
AWS_ACCESS_KEY_ID: ${{ secrets.VOLC_TOS_ACCESS_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.VOLC_TOS_SECRET_KEY }}
AWS_DEFAULT_REGION: cn-beijing
run: |
aws configure set default.s3.addressing_style virtual
TOS_ENDPOINT="https://tos-s3-cn-beijing.volces.com"
# Upload to versioned directory
aws s3 sync artifacts/ "s3://picoclaw-downloads/${{ inputs.tag }}/" \
--endpoint-url "$TOS_ENDPOINT"
# Upload to latest (overwrite)
aws s3 sync artifacts/ "s3://picoclaw-downloads/latest/" \
--endpoint-url "$TOS_ENDPOINT" \
--delete
+31 -1
View File
@@ -10,7 +10,7 @@ build/
*.out
/picoclaw
/picoclaw-test
cmd/picoclaw/workspace
cmd/**/workspace
# Picoclaw specific
@@ -25,6 +25,9 @@ build/
# Secrets & Config (keep templates, ignore actual secrets)
.env
config/config.json
.security.yml
onboard
# Test
coverage.txt
@@ -38,9 +41,36 @@ ralph/
.ralph/
tasks/
# Plans
docs/plans/
docs/superpowers/
# Editors
.vscode/
.idea/
# Added by goreleaser init:
dist/
*.vite/
# Windows Application Icon/Resource
*.syso
.cache/
web/frontend/.pnpm-store/
_tmp_*
web/frontend/_tmp_*
# Test telegram integration
cmd/telegram/
# Keep embedded backend dist directory placeholder in VCS
!web/backend/dist/
web/backend/dist/*
!web/backend/dist/.gitkeep
.claude/
docker/data
.omc/
.worktrees/
+178
View File
@@ -0,0 +1,178 @@
version: "2"
linters:
default: all
disable:
# TODO: Tweak for current project needs
- containedctx
- cyclop
- depguard
- dupword
- err113
- exhaustruct
- funcorder
- gochecknoglobals
- gosmopolitan # Project legitimately uses CJK text in tests (FTS5, token counting)
- godot
- intrange
- ireturn
- nlreturn
- noctx
- noinlineerr
- nonamedreturns
- tagliatelle
- testpackage
- varnamelen
- wrapcheck
- wsl
- wsl_v5
# TODO: Disabled, because they are failing at the moment, we should fix them and enable (step by step)
- contextcheck
- embeddedstructfieldcheck
- errcheck
- errchkjson
- errorlint
- exhaustive
- forbidigo
- forcetypeassert
- funlen
- gochecknoinits
- gocognit
- goconst
- gocritic
- gocyclo
- godox
- gosec
- ineffassign
- lll
- maintidx
- mnd
- modernize
- nestif
- nilnil
- paralleltest
- perfsprint
- revive
- staticcheck
- tagalign
- testifylint
- thelper
- unparam
- usestdlibvars
- usetesting
settings:
gomoddirectives:
replace-allow-list:
- github.com/bwmarrin/discordgo
errcheck:
check-type-assertions: true
check-blank: true
exhaustive:
default-signifies-exhaustive: true
funlen:
lines: 120
statements: 40
gocognit:
min-complexity: 25
gocyclo:
min-complexity: 20
govet:
enable-all: true
disable:
- fieldalignment
lll:
line-length: 120
tab-width: 4
misspell:
locale: US
mnd:
checks:
- argument
- assign
- case
- condition
- operation
- return
nakedret:
max-func-lines: 3
revive:
enable-all-rules: true
rules:
- name: add-constant
disabled: true
- name: argument-limit
arguments:
- 7
severity: warning
- name: banned-characters
disabled: true
- name: cognitive-complexity
disabled: true
- name: comment-spacings
arguments:
- nolint
severity: warning
- name: cyclomatic
disabled: true
- name: file-header
disabled: true
- name: function-result-limit
arguments:
- 3
severity: warning
- name: function-length
disabled: true
- name: line-length-limit
disabled: true
- name: max-public-structs
disabled: true
- name: modifies-value-receiver
disabled: true
- name: package-comments
disabled: true
- name: unused-receiver
disabled: true
exclusions:
generated: lax
rules:
- linters:
- lll
source: '^//go:generate '
- linters:
- funlen
- maintidx
- gocognit
- gocyclo
path: _test\.go$
- linters:
- nolintlint
path: 'pkg/tools/(i2c\.go|spi\.go)$'
issues:
max-issues-per-linter: 0
max-same-issues: 0
formatters:
enable:
- gci
- gofmt
- gofumpt
- goimports
- golines
settings:
gci:
sections:
- standard
- default
- localmodule
custom-order: true
gofmt:
simplify: true
rewrite-rules:
- pattern: "interface{}"
replacement: "any"
- pattern: "a[b:len(a)]"
replacement: "a[b:]"
golines:
max-len: 120
+144 -8
View File
@@ -2,48 +2,154 @@
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
version: 2
git:
ignore_tags:
- nightly
- ".*-nightly.*"
before:
hooks:
- go mod tidy
- go generate ./cmd/picoclaw
- go generate ./...
- sh -c 'cd web/frontend && CI=true pnpm install --frozen-lockfile && pnpm build:backend'
- sh -c 'GOBIN="$(go env GOPATH)/bin"; mkdir -p "$GOBIN"; go install github.com/tc-hib/go-winres@v0.3.3 && "$GOBIN/go-winres" make --in web/backend/winres/winres.json --out web/backend/rsrc --product-version={{ .Version }} --file-version={{ .Version }}'
- sh -c 'if [ "${INCLUDE_ANDROID_BUNDLE:-}" = "true" ]; then make build-android-bundle; fi'
builds:
- id: picoclaw
env:
- CGO_ENABLED=0
tags:
- goolm
- stdjson
ldflags:
- -s -w
- -X github.com/sipeed/picoclaw/pkg/config.Version={{ .Version }}
- -X github.com/sipeed/picoclaw/pkg/config.GitCommit={{ .ShortCommit }}
- -X github.com/sipeed/picoclaw/pkg/config.BuildTime={{ .Date }}
- -X github.com/sipeed/picoclaw/pkg/config.GoVersion={{ with index .Env "GOVERSION" }}{{ . }}{{ else }}unknown{{ end }}
goos:
- linux
- windows
- darwin
- freebsd
- netbsd
goarch:
- amd64
- arm64
- riscv64
- s390x
- mips64
- loong64
- arm
- s390x
- mipsle
goarm:
- "6"
- "7"
gomips:
- softfloat
main: ./cmd/picoclaw
ignore:
- goos: windows
goarch: arm
- goos: netbsd
goarch: s390x
- goos: netbsd
goarch: mips64
- goos: netbsd
goarch: arm
- id: picoclaw-launcher
binary: picoclaw-launcher
env:
- CGO_ENABLED=0
tags:
- goolm
- stdjson
ldflags:
- -s -w
- -X github.com/sipeed/picoclaw/pkg/config.Version={{ .Version }}
- -X github.com/sipeed/picoclaw/pkg/config.GitCommit={{ .ShortCommit }}
- -X github.com/sipeed/picoclaw/pkg/config.BuildTime={{ .Date }}
- -X github.com/sipeed/picoclaw/pkg/config.GoVersion={{ with index .Env "GOVERSION" }}{{ . }}{{ else }}unknown{{ end }}
goos:
- linux
- windows
- darwin
- freebsd
- netbsd
goarch:
- amd64
- arm64
- riscv64
- loong64
- arm
- s390x
- mipsle
goarm:
- "6"
- "7"
gomips:
- softfloat
main: ./web/backend
ignore:
- goos: windows
goarch: arm
- goos: netbsd
goarch: s390x
- goos: netbsd
goarch: mips64
- goos: netbsd
goarch: arm
dockers_v2:
- id: picoclaw
dockerfile: Dockerfile.goreleaser
dockerfile: docker/Dockerfile.goreleaser
extra_files:
- docker/entrypoint.sh
ids:
- picoclaw
images:
- "ghcr.io/{{ .Env.GITHUB_REPOSITORY_OWNER }}/picoclaw"
- "docker.io/{{ .Env.DOCKERHUB_IMAGE_NAME }}"
- 'docker.io/{{ .Env.DOCKERHUB_IMAGE_NAME }}'
tags:
- "{{ .Tag }}"
- "latest"
- '{{ if isEnvSet "NIGHTLY_BUILD" }}nightly{{ else }}{{ .Tag }}{{ end }}'
- '{{ if isEnvSet "NIGHTLY_BUILD" }}nightly{{ else }}latest{{ end }}'
platforms:
- linux/amd64
- linux/arm64
- linux/riscv64
- id: picoclaw-launcher
dockerfile: docker/Dockerfile.goreleaser.launcher
ids:
- picoclaw
- picoclaw-launcher
images:
- "ghcr.io/{{ .Env.GITHUB_REPOSITORY_OWNER }}/picoclaw"
- 'docker.io/{{ .Env.DOCKERHUB_IMAGE_NAME }}'
tags:
- '{{ if isEnvSet "NIGHTLY_BUILD" }}nightly-launcher{{ else }}{{ .Tag }}-launcher{{ end }}'
- '{{ if isEnvSet "NIGHTLY_BUILD" }}nightly-launcher{{ else }}launcher{{ end }}'
platforms:
- linux/amd64
- linux/arm64
- linux/riscv64
notarize:
macos:
- enabled: '{{ isEnvSet "MACOS_SIGN_P12" }}'
ids:
- picoclaw
- picoclaw-launcher
sign:
certificate: "{{.Env.MACOS_SIGN_P12}}"
password: "{{.Env.MACOS_SIGN_PASSWORD}}"
notarize:
issuer_id: "{{.Env.MACOS_NOTARY_ISSUER_ID}}"
key_id: "{{.Env.MACOS_NOTARY_KEY_ID}}"
key: "{{.Env.MACOS_NOTARY_KEY}}"
wait: true
timeout: 20m
archives:
- formats: [tar.gz]
# this name template makes the OS and Arch compatible with the results of `uname`.
@@ -59,6 +165,33 @@ archives:
- goos: windows
formats: [zip]
nfpms:
- id: picoclaw
ids:
- picoclaw
- picoclaw-launcher
package_name: picoclaw
file_name_template: >-
{{ .PackageName }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "arm64" }}aarch64
{{- else if eq .Arch "arm" }}armv{{ .Arm }}
{{- else }}{{ .Arch }}{{ end }}
vendor: picoclaw
homepage: https://github.com/{{ .Env.GITHUB_REPOSITORY_OWNER }}/picoclaw
maintainer: picoclaw contributors
description: picoclaw - a tool for managing and running tasks
license: MIT
formats:
- rpm
- deb
bindir: /usr/bin
contents:
- src: web/picoclaw-launcher.desktop
dst: /usr/share/applications/picoclaw-launcher.desktop
- src: web/picoclaw-launcher.png
dst: /usr/share/icons/hicolor/512x512/apps/picoclaw-launcher.png
changelog:
sort: asc
filters:
@@ -72,6 +205,9 @@ changelog:
# lzma: true
release:
disable: '{{ isEnvSet "NIGHTLY_BUILD" }}'
extra_files:
- glob: ./build/picoclaw-android-universal.zip
footer: >-
---
+308
View File
@@ -0,0 +1,308 @@
# Contributing to PicoClaw
Thank you for your interest in contributing to PicoClaw! This project is a community-driven effort to build the lightweight and versatile personal AI assistant. We welcome contributions of all kinds: bug fixes, features, documentation, translations, and testing.
PicoClaw itself was substantially developed with AI assistance — we embrace this approach and have built our contribution process around it.
## Table of Contents
- [Code of Conduct](#code-of-conduct)
- [Ways to Contribute](#ways-to-contribute)
- [Getting Started](#getting-started)
- [Development Setup](#development-setup)
- [Making Changes](#making-changes)
- [AI-Assisted Contributions](#ai-assisted-contributions)
- [Pull Request Process](#pull-request-process)
- [Branch Strategy](#branch-strategy)
- [Code Review](#code-review)
- [Communication](#communication)
---
## Code of Conduct
We are committed to maintaining a welcoming and respectful community. Be kind, constructive, and assume good faith. Harassment or discrimination of any kind will not be tolerated.
---
## Ways to Contribute
- **Bug reports** — Open an issue using the bug report template.
- **Feature requests** — Open an issue using the feature request template; discuss before implementing.
- **Code** — Fix bugs or implement features. See the workflow below.
- **Documentation** — Improve READMEs, docs, inline comments, or translations.
- **Testing** — Run PicoClaw on new hardware, channels, or LLM providers and report your results.
For substantial new features, please open an issue first to discuss the design before writing code. This prevents wasted effort and ensures alignment with the project's direction.
For documentation contributions, prefer the layout and naming conventions in [`docs/README.md`](docs/README.md). Run `make lint-docs` after adding or moving Markdown files to catch common consistency issues early.
---
## Getting Started
1. **Fork** the repository on GitHub.
2. **Clone** your fork locally:
```bash
git clone https://github.com/<your-username>/picoclaw.git
cd picoclaw
```
3. Add the upstream remote:
```bash
git remote add upstream https://github.com/sipeed/picoclaw.git
```
---
## Development Setup
### Prerequisites
- Go 1.25 or later
- `make`
### Build
```bash
make build # Build binary (runs go generate first)
make generate # Run go generate only
make check # Full pre-commit check: deps + fmt + vet + test + docs consistency checks
```
### Running Tests
```bash
make test # Run all tests
make integration-test # Run Docker-backed integration suites
go test -run TestName -v ./pkg/session/ # Run a single test
go test -bench=. -benchmem -run='^$' ./... # Run benchmarks
```
Docker-backed integration suites are auto-discovered from [`integration/suites/`](integration/suites/). See [`integration/README.md`](integration/README.md) for the suite layout and the conventions used by CI.
### Code Style
```bash
make fmt # Format code
make vet # Static analysis
make lint # Full linter run
make lint-docs # Check common documentation layout and naming conventions
```
All CI checks must pass before a PR can be merged. Run `make check` locally before pushing to catch issues early, including the common docs consistency checks from `make lint-docs`.
---
## Making Changes
### Branching
Always branch off `main` and target `main` in your PR. Never push directly to `main` or any `release/*` branch:
```bash
git checkout main
git pull upstream main
git checkout -b your-feature-branch
```
Use descriptive branch names, e.g. `fix/telegram-timeout`, `feat/ollama-provider`, `docs/contributing-guide`.
### Commits
- Write clear, concise commit messages in English.
- Use the imperative mood: "Add retry logic" not "Added retry logic".
- Reference the related issue when relevant: `Fix session leak (#123)`.
- Keep commits focused. One logical change per commit is preferred.
- For minor cleanups or typo fixes, squash them into a single commit before opening a PR.
- Refer to [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)
### Keeping Up to Date
Rebase your branch onto upstream `main` before opening a PR:
```bash
git fetch upstream
git rebase upstream/main
```
---
## AI-Assisted Contributions
PicoClaw was built with substantial AI assistance, and we fully embrace AI-assisted development. However, contributors must understand their responsibilities when using AI tools.
### Disclosure Is Required
Every PR must disclose AI involvement using the PR template's **🤖 AI Code Generation** section. There are three levels:
| Level | Description |
|---|---|
| 🤖 Fully AI-generated | AI wrote the code; contributor reviewed and validated it |
| 🛠️ Mostly AI-generated | AI produced the draft; contributor made significant modifications |
| 👨‍💻 Mostly Human-written | Contributor led; AI provided suggestions or none at all |
Honest disclosure is expected. There is no stigma attached to any level — what matters is the quality of the contribution.
### You Are Responsible for What You Submit
Using AI to generate code does not reduce your responsibility as the contributor. Before opening a PR with AI-generated code, you must:
- **Read and understand** every line of the generated code.
- **Test it** in a real environment (see the Test Environment section of the PR template).
- **Check for security issues** — AI models can generate subtly insecure code (e.g., path traversal, injection, credential exposure). Review carefully.
- **Verify correctness** — AI-generated logic can be plausible-sounding but wrong. Validate the behavior, not just the syntax.
PRs where it is clear the contributor has not read or tested the AI-generated code will be closed without review.
### AI-Generated Code Quality Standards
AI-generated contributions are held to the **same quality bar** as human-written code:
- It must pass all CI checks (`make check`).
- It must be idiomatic Go and consistent with the existing codebase style.
- It must not introduce unnecessary abstractions, dead code, or over-engineering.
- It must include or update tests where appropriate.
### Security Review
AI-generated code requires extra security scrutiny. Pay special attention to:
- File path handling and sandbox escapes (see commit `244eb0b` for a real example)
- External input validation in channel handlers and tool implementations
- Credential or secret handling
- Command execution (`exec.Command`, shell invocations)
If you are unsure whether a piece of AI-generated code is safe, say so in the PR — reviewers will help.
---
## Pull Request Process
### Before Opening a PR
- [ ] Run `make check` and ensure it passes locally.
- [ ] Fill in the PR template completely, including the AI disclosure section.
- [ ] Link any related issue(s) in the PR description.
- [ ] Keep the PR focused. Avoid bundling unrelated changes together.
### PR Template Sections
The PR template asks for:
- **Description** — What does this change do and why?
- **Type of Change** — Bug fix, feature, docs, or refactor.
- **AI Code Generation** — Disclosure of AI involvement (required).
- **Related Issue** — Link to the issue this addresses.
- **Technical Context** — Reference URLs and reasoning (skip for pure docs PRs).
- **Test Environment** — Hardware, OS, model/provider, and channels used for testing.
- **Evidence** — Optional logs or screenshots demonstrating the change works.
- **Checklist** — Self-review confirmation.
### PR Size
Prefer small, reviewable PRs. A PR that changes 200 lines across 5 files is much easier to review than one that changes 2000 lines across 30 files. If your feature is large, consider splitting it into a series of smaller, logically complete PRs.
---
## Branch Strategy
### Long-Lived Branches
- **`main`** — the active development branch. All feature PRs target `main`. The branch is protected: direct pushes are not permitted, and at least one maintainer approval is required before merging.
- **`release/x.y`** — stable release branches, cut from `main` when a version is ready to ship. These branches are more strictly protected than `main`.
### Requirements to Merge into `main`
A PR can only be merged when all of the following are satisfied:
1. **CI passes** — All GitHub Actions workflows (lint, test, build) must be green.
2. **Reviewer approval** — At least one maintainer has approved the PR.
3. **No unresolved review comments** — All review threads must be resolved.
4. **PR template is complete** — Including AI disclosure and test environment.
### Who Can Merge
Only maintainers can merge PRs. Contributors cannot merge their own PRs, even if they have write access.
### Merge Strategy
We use **squash merge** for most PRs to keep the `main` history clean and readable. Each merged PR becomes a single commit referencing the PR number, e.g.:
```
feat: Add Ollama provider support (#491)
```
If a PR consists of multiple independent, well-separated commits that tell a clear story, a regular merge may be used at the maintainer's discretion.
### Release Branches
When a version is ready, maintainers cut a `release/x.y` branch from `main`. After that point:
- **New features are not backported.** The release branch receives no new functionality after it is cut.
- **Security fixes and critical bug fixes are cherry-picked.** If a fix in `main` qualifies (security vulnerability, data loss, crash), maintainers will cherry-pick the relevant commit(s) onto the affected `release/x.y` branch and issue a patch release.
If you believe a fix in `main` should be backported to a release branch, note it in the PR description or open a separate issue. The decision rests with the maintainers.
Release branches have stricter protections than `main` and are never directly pushed to under any circumstances.
---
## Code Review
### For Contributors
- Respond to review comments within a reasonable time. If you need more time, say so.
- When you update a PR in response to feedback, briefly note what changed (e.g., "Updated to use `sync.RWMutex` as suggested").
- If you disagree with feedback, engage respectfully. Explain your reasoning; reviewers can be wrong too.
- Do not force-push after a review has started — it makes it harder for reviewers to see what changed. Use additional commits instead; the maintainer will squash on merge.
### For Reviewers
Review for:
1. **Correctness** — Does the code do what it claims? Are there edge cases?
2. **Security** — Especially for AI-generated code, tool implementations, and channel handlers.
3. **Architecture** — Is the approach consistent with the existing design?
4. **Simplicity** — Is there a simpler solution? Does this add unnecessary complexity?
5. **Tests** — Are the changes covered by tests? Are existing tests still meaningful?
Be constructive and specific. "This could have a race condition if two goroutines call this concurrently — consider using a mutex here" is better than "this looks wrong".
### Reviewer List
Once your PR is submitted, you can reach out to the assigned reviewers listed in the following table.
|Function| Reviewer|
|--- |--- |
|Provider|@yinwm |
|Channel |@yinwm/@alexhoshina |
|Agent |@lxowalle/@Zhaoyikaiii|
|Tools |@lxowalle|
|SKill ||
|MCP ||
|Optimization|@lxowalle|
|Security||
|AI CI |@imguoguo|
|UX ||
|Document||
---
## Communication
- **GitHub Issues** — Bug reports, feature requests, design discussions.
- **GitHub Discussions** — General questions, ideas, community conversation.
- **Pull Request comments** — Code-specific feedback.
- **Wechat&Discord** — We will invite you when you have at least one merged PR
When in doubt, open an issue before writing code. It costs little and prevents wasted effort.
---
## A Note on the Project's AI-Driven Origin
PicoClaw's architecture was substantially designed and implemented with AI assistance, guided by human oversight. If you find something that looks odd or over-engineered, it may be an artifact of that process — opening an issue to discuss it is always welcome.
We believe AI-assisted development done responsibly produces great results. We also believe humans must remain accountable for what they ship. These two beliefs are not in conflict.
Thank you for contributing!
-4
View File
@@ -19,7 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
PicoClaw is heavily inspired by and based on [nanobot](https://github.com/HKUDS/nanobot) by HKUDS.
+375 -36
View File
@@ -1,26 +1,92 @@
.PHONY: all build install uninstall clean help test
.PHONY: all build install uninstall clean help test integration-test build-all lint-docs
# Build variables
BINARY_NAME=picoclaw
BUILD_DIR=build
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}')
LDFLAGS=-ldflags "-X main.version=$(VERSION) -X main.gitCommit=$(GIT_COMMIT) -X main.buildTime=$(BUILD_TIME) -X main.goVersion=$(GO_VERSION)"
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),$(firstword $(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?=go
GOFLAGS?=-v
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)
GO_BUILD_TAGS_NO_GOOLM:=$(subst $(space),$(comma),$(strip $(filter-out goolm,$(subst $(comma),$(space),$(GO_BUILD_TAGS)))))
GOFLAGS_NO_GOOLM?=-v -tags $(GO_BUILD_TAGS_NO_GOOLM)
# Patch MIPS LE ELF e_flags (offset 36) for NaN2008-only kernels (e.g. Ingenic X2600).
#
# Bytes (octal): \004 \024 \000 \160 → little-endian 0x70001404
# 0x70000000 EF_MIPS_ARCH_32R2 MIPS32 Release 2
# 0x00001000 EF_MIPS_ABI_O32 O32 ABI
# 0x00000400 EF_MIPS_NAN2008 IEEE 754-2008 NaN encoding
# 0x00000004 EF_MIPS_CPIC PIC calling sequence
#
# Go's GOMIPS=softfloat emits no FP instructions, so the NaN mode is irrelevant
# at runtime — this is purely an ELF metadata fix to satisfy the kernel's check.
# patchelf cannot modify e_flags; dd at a fixed offset is the most portable way.
#
# Ref: https://codebrowser.dev/linux/linux/arch/mips/include/asm/elf.h.html
define PATCH_MIPS_FLAGS
@if [ -f "$(1)" ]; then \
printf '\004\024\000\160' | dd of=$(1) bs=1 seek=36 count=4 conv=notrunc 2>/dev/null || \
{ echo "Error: failed to patch MIPS e_flags for $(1)"; exit 1; }; \
else \
echo "Error: $(1) not found, cannot patch MIPS e_flags"; exit 1; \
fi
endef
# Patch creack/pty for loong64 support (upstream doesn't have ztypes_loong64.go)
PTY_PATCH_LOONG64=pty_dir=$$(go env GOMODCACHE)/github.com/creack/pty@v1.1.9; \
if [ -d "$$pty_dir" ] && [ ! -f "$$pty_dir/ztypes_loong64.go" ]; then \
chmod +w "$$pty_dir" 2>/dev/null || true; \
printf '//go:build linux && loong64\npackage pty\ntype (_C_int int32; _C_uint uint32)\n' > "$$pty_dir/ztypes_loong64.go"; \
fi
# Golangci-lint
GOLANGCI_LINT?=golangci-lint
# Installation
INSTALL_PREFIX?=$(HOME)/.local
INSTALL_BIN_DIR=$(INSTALL_PREFIX)/bin
INSTALL_MAN_DIR=$(INSTALL_PREFIX)/share/man/man1
INSTALL_TMP_SUFFIX=.new
# Workspace and Skills
PICOCLAW_HOME?=$(HOME)/.picoclaw
@@ -28,9 +94,24 @@ WORKSPACE_DIR?=$(PICOCLAW_HOME)/workspace
WORKSPACE_SKILLS_DIR=$(WORKSPACE_DIR)/skills
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)
@@ -39,23 +120,67 @@ ifeq ($(UNAME_S),Linux)
ARCH=amd64
else ifeq ($(UNAME_M),aarch64)
ARCH=arm64
else ifeq ($(UNAME_M),armv81)
ARCH=arm64
else ifeq ($(UNAME_M),loongarch64)
ARCH=loong64
else ifeq ($(UNAME_M),riscv64)
ARCH=riscv64
else ifeq ($(UNAME_M),mipsel)
ARCH=mipsle
else
ARCH=$(UNAME_M)
endif
else ifeq ($(UNAME_S),Darwin)
PLATFORM=darwin
WEB_GO=CGO_LDFLAGS="-mmacosx-version-min=10.11" CGO_CFLAGS="-mmacosx-version-min=10.11" CGO_ENABLED=1 go
ifeq ($(UNAME_M),x86_64)
ARCH=amd64
ARCH?=amd64
else ifeq ($(UNAME_M),arm64)
ARCH=arm64
ARCH?=arm64
else
ARCH=$(UNAME_M)
ARCH?=$(UNAME_M)
endif
else
PLATFORM=$(UNAME_S)
ARCH=$(UNAME_M)
ifeq ($(UNAME_M),x86_64)
ARCH?=amd64
else
ARCH?=$(UNAME_M)
endif
# Detect Windows (Git Bash / MSYS2)
IS_WINDOWS:=$(if $(findstring MINGW,$(UNAME_S)),yes,$(if $(findstring MSYS,$(UNAME_S)),yes,$(if $(findstring CYGWIN,$(UNAME_S)),yes,no)))
ifeq ($(IS_WINDOWS),yes)
EXT=.exe
LNCMD=cp
else ifeq ($(UNAME_S),windows) # failsafe for force windows build in other OS using UNAME_S=windows
EXT=.exe
endif
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
ifneq ($(strip $(GOOS)),)
PLATFORM:=$(GOOS)
endif
ifneq ($(strip $(GOARCH)),)
ARCH:=$(GOARCH)
endif
ifeq ($(PLATFORM),windows)
EXT=.exe
endif
BINARY_PATH=$(BUILD_DIR)/$(BINARY_NAME)-$(PLATFORM)-$(ARCH)
@@ -66,35 +191,155 @@ 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 }"
@$(POWERSHELL) "$$env:GOOS=''; $$env:GOARCH=''; $(GO) generate ./..."
else
@rm -r ./$(CMD_DIR)/workspace 2>/dev/null || true
@$(GO) generate ./...
@GOOS=$$($(GO) env GOHOSTOS) GOARCH=$$($(GO) env GOHOSTARCH) $(GO) generate ./...
endif
@echo "Run generate complete"
## build: Build the picoclaw binary for current platform
build: generate
@echo "Building $(BINARY_NAME) for $(PLATFORM)/$(ARCH)..."
@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)
@$(GO) build $(GOFLAGS) $(LDFLAGS) -o $(BINARY_PATH) ./$(CMD_DIR)
@echo "Build complete: $(BINARY_PATH)"
@ln -sf $(BINARY_NAME)-$(PLATFORM)-$(ARCH) $(BUILD_DIR)/$(BINARY_NAME)
@GOOS=$(PLATFORM) 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-all: Build picoclaw for all platforms
## 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)
@GOOS=$(PLATFORM) GOARCH=$(ARCH) $(MAKE) -C web build \
OUTPUT="$(CURDIR)/$(BUILD_DIR)/picoclaw-launcher-$(PLATFORM)-$(ARCH)$(EXT)" \
WEB_GO='$(WEB_GO)' \
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:
@$(MAKE) -C web build-frontend
## build-whatsapp-native: Build with WhatsApp native (whatsmeow) support; larger binary
build-whatsapp-native: generate
## @echo "Building $(BINARY_NAME) with WhatsApp native for $(PLATFORM)/$(ARCH)..."
@echo "Building for multiple platforms..."
@mkdir -p $(BUILD_DIR)
GOOS=linux GOARCH=amd64 $(GO) build -tags $(GO_BUILD_TAGS),whatsapp_native -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 ./$(CMD_DIR)
GOOS=linux GOARCH=arm GOARM=7 $(GO) build -tags $(GO_BUILD_TAGS),whatsapp_native -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm ./$(CMD_DIR)
GOOS=linux GOARCH=arm64 $(GO) build -tags $(GO_BUILD_TAGS),whatsapp_native -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 ./$(CMD_DIR)
GOOS=linux GOARCH=loong64 $(GO) build -tags $(GO_BUILD_TAGS),whatsapp_native -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-linux-loong64 ./$(CMD_DIR)
GOOS=linux GOARCH=riscv64 $(GO) build -tags $(GO_BUILD_TAGS),whatsapp_native -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-linux-riscv64 ./$(CMD_DIR)
GOOS=linux GOARCH=mipsle GOMIPS=softfloat $(GO) build -tags $(GO_BUILD_TAGS_NO_GOOLM),whatsapp_native -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-linux-mipsle ./$(CMD_DIR)
$(call PATCH_MIPS_FLAGS,$(BUILD_DIR)/$(BINARY_NAME)-linux-mipsle)
GOOS=darwin GOARCH=arm64 $(GO) build -tags $(GO_BUILD_TAGS),whatsapp_native -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-arm64 ./$(CMD_DIR)
GOOS=windows GOARCH=amd64 $(GO) build -tags $(GO_BUILD_TAGS),whatsapp_native -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe ./$(CMD_DIR)
## @$(GO) build $(GOFLAGS) -tags whatsapp_native -ldflags "$(LDFLAGS)" -o $(BINARY_PATH) ./$(CMD_DIR)
@echo "Build complete"
## @ln -sf $(BINARY_NAME)-$(PLATFORM)-$(ARCH) $(BUILD_DIR)/$(BINARY_NAME)
## build-linux-arm: Build for Linux ARMv7 (e.g. Raspberry Pi Zero 2 W 32-bit)
build-linux-arm: generate
@echo "Building for linux/arm (GOARM=7)..."
@mkdir -p $(BUILD_DIR)
GOOS=linux GOARCH=arm GOARM=7 $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm ./$(CMD_DIR)
@echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME)-linux-arm"
## build-linux-arm64: Build for Linux ARM64 (e.g. Raspberry Pi Zero 2 W 64-bit)
build-linux-arm64: generate
@echo "Building for linux/arm64..."
@mkdir -p $(BUILD_DIR)
GOOS=linux GOARCH=arm64 $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 ./$(CMD_DIR)
@echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64"
## build-linux-mipsle: Build for Linux MIPS32 LE
build-linux-mipsle: generate
@echo "Building for linux/mipsle (softfloat)..."
@mkdir -p $(BUILD_DIR)
GOOS=linux GOARCH=mipsle GOMIPS=softfloat $(GO) build $(GOFLAGS_NO_GOOLM) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-linux-mipsle ./$(CMD_DIR)
$(call PATCH_MIPS_FLAGS,$(BUILD_DIR)/$(BINARY_NAME)-linux-mipsle)
@echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME)-linux-mipsle"
## build-android-arm64: Build core for Android ARM64
build-android-arm64: generate
@echo "Building for android/arm64..."
@mkdir -p $(BUILD_DIR)
GOOS=android GOARCH=arm64 $(GO) build -tags stdjson -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-android-arm64 ./$(CMD_DIR)
@echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME)-android-arm64"
## build-launcher-android-arm64: Build launcher for Android ARM64
build-launcher-android-arm64:
@echo "Building picoclaw-launcher for android/arm64..."
@mkdir -p $(BUILD_DIR)
@$(MAKE) -C web build-android-arm64 \
OUTPUT_ANDROID_ARM64="$(CURDIR)/$(BUILD_DIR)/picoclaw-launcher-android-arm64" \
GO='$(GO)' \
LDFLAGS='$(LDFLAGS)'
@echo "Build complete: $(BUILD_DIR)/picoclaw-launcher-android-arm64"
## build-android-bundle: Build core and launcher for all Android architectures and package as universal zip
build-android-bundle: generate
@echo "Building core for all Android architectures..."
@mkdir -p $(BUILD_DIR)
GOOS=android GOARCH=arm64 $(GO) build -tags stdjson -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-android-arm64 ./$(CMD_DIR)
@echo "Building launcher for Android arm64..."
@$(MAKE) build-launcher-android-arm64
@echo "Staging JNI libs..."
@rm -rf $(BUILD_DIR)/android-staging
@mkdir -p $(BUILD_DIR)/android-staging/arm64-v8a
@cp $(BUILD_DIR)/$(BINARY_NAME)-android-arm64 $(BUILD_DIR)/android-staging/arm64-v8a/libpicoclaw.so
@cp $(BUILD_DIR)/picoclaw-launcher-android-arm64 $(BUILD_DIR)/android-staging/arm64-v8a/libpicoclaw-web.so
@cd $(BUILD_DIR)/android-staging && zip -r ../picoclaw-android-universal.zip .
@rm -rf $(BUILD_DIR)/android-staging
@echo "All Android builds complete: $(BUILD_DIR)/picoclaw-android-universal.zip"
## build-pi-zero: Build for Raspberry Pi Zero 2 W (32-bit and 64-bit)
build-pi-zero: build-linux-arm build-linux-arm64
@echo "Pi Zero 2 W builds: $(BUILD_DIR)/$(BINARY_NAME)-linux-arm (32-bit), $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 (64-bit)"
## build-all: Build the picoclaw core binary for all Makefile-managed platforms
build-all: generate
@echo "Building for multiple platforms..."
@mkdir -p $(BUILD_DIR)
GOOS=linux GOARCH=amd64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 ./$(CMD_DIR)
GOOS=linux GOARCH=arm64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 ./$(CMD_DIR)
GOOS=linux GOARCH=riscv64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-riscv64 ./$(CMD_DIR)
GOOS=darwin GOARCH=arm64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-arm64 ./$(CMD_DIR)
GOOS=windows GOARCH=amd64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe ./$(CMD_DIR)
@echo "All builds complete"
GOOS=linux GOARCH=amd64 $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 ./$(CMD_DIR)
GOOS=linux GOARCH=arm GOARM=7 $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm ./$(CMD_DIR)
GOOS=linux GOARCH=arm64 $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 ./$(CMD_DIR)
@$(PTY_PATCH_LOONG64)
GOOS=linux GOARCH=loong64 $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-linux-loong64 ./$(CMD_DIR)
GOOS=linux GOARCH=riscv64 $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-linux-riscv64 ./$(CMD_DIR)
GOOS=linux GOARCH=mipsle GOMIPS=softfloat $(GO) build $(GOFLAGS_NO_GOOLM) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-linux-mipsle ./$(CMD_DIR)
$(call PATCH_MIPS_FLAGS,$(BUILD_DIR)/$(BINARY_NAME)-linux-mipsle)
GOOS=linux GOARCH=arm GOARM=7 $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-linux-armv7 ./$(CMD_DIR)
GOOS=darwin GOARCH=arm64 $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-arm64 ./$(CMD_DIR)
GOOS=windows GOARCH=amd64 $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe ./$(CMD_DIR)
GOOS=netbsd GOARCH=amd64 $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-netbsd-amd64 ./$(CMD_DIR)
GOOS=netbsd GOARCH=arm64 $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-netbsd-arm64 ./$(CMD_DIR)
@echo "Core builds complete"
## install: Install picoclaw to system and copy builtin skills
install: build
@echo "Installing $(BINARY_NAME)..."
@mkdir -p $(INSTALL_BIN_DIR)
@cp $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_BIN_DIR)/$(BINARY_NAME)
@chmod +x $(INSTALL_BIN_DIR)/$(BINARY_NAME)
# Copy binary with temporary suffix to ensure atomic update
@cp $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_BIN_DIR)/$(BINARY_NAME)$(INSTALL_TMP_SUFFIX)
@chmod +x $(INSTALL_BIN_DIR)/$(BINARY_NAME)$(INSTALL_TMP_SUFFIX)
@mv -f $(INSTALL_BIN_DIR)/$(BINARY_NAME)$(INSTALL_TMP_SUFFIX) $(INSTALL_BIN_DIR)/$(BINARY_NAME)
@echo "Installed binary to $(INSTALL_BIN_DIR)/$(BINARY_NAME)"
@echo "Installation complete!"
@@ -116,20 +361,44 @@ 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
vet:
@$(GO) vet ./...
vet: generate
@packages="$$($(GO) list $(GOFLAGS) ./...)" && \
$(GO) vet $(GOFLAGS) $$(printf '%s\n' "$$packages" | grep -v '^github.com/sipeed/picoclaw/web/')
@cd web/backend && $(WEB_GO) vet ./...
## fmt: Format Go code
test:
@$(GO) test ./...
## test: Test Go code
test: generate
@$(GO) test $(GOFLAGS) $$($(GO) list $(GOFLAGS) ./... | grep -v github.com/sipeed/picoclaw/web/)
@cd web && make test
## integration-test: Run Docker-backed integration test suites
integration-test:
@bash ./scripts/run-integration-tests.sh
## fmt: Format Go code
fmt:
@$(GO) fmt ./...
@$(GOLANGCI_LINT) fmt
## lint-docs: Check common documentation layout and naming conventions
lint-docs:
@./scripts/lint-docs.sh
## lint: Run linters
lint:
@$(GOLANGCI_LINT) run --build-tags $(GO_BUILD_TAGS)
@./scripts/lint-docs.sh
## fix: Fix linting issues
fix:
@$(GOLANGCI_LINT) run --fix --build-tags $(GO_BUILD_TAGS)
## deps: Download dependencies
deps:
@@ -141,13 +410,81 @@ update-deps:
@$(GO) get -u ./...
@$(GO) mod tidy
## check: Run vet, fmt, and verify dependencies
check: deps fmt vet test
## check: Run deps, fmt, vet, tests, and docs consistency checks
check: deps fmt vet test lint-docs
## run: Build and run picoclaw
run: build
@$(BUILD_DIR)/$(BINARY_NAME) $(ARGS)
## docker-build: Build Docker image (minimal Alpine-based)
docker-build:
@echo "Building minimal Docker image (Alpine-based)..."
docker compose -f docker/docker-compose.yml build picoclaw-agent picoclaw-gateway
## docker-build-full: Build Docker image with full MCP support (Node.js 24)
docker-build-full:
@echo "Building full-featured Docker image (Node.js 24)..."
docker compose -f docker/docker-compose.full.yml build picoclaw-agent picoclaw-gateway
## docker-test: Test MCP tools in Docker container
docker-test:
@echo "Testing MCP tools in Docker..."
@chmod +x scripts/test-docker-mcp.sh
@./scripts/test-docker-mcp.sh
## docker-run: Run picoclaw gateway in Docker (Alpine-based)
docker-run:
docker compose -f docker/docker-compose.yml --profile gateway up
## docker-run-full: Run picoclaw gateway in Docker (full-featured)
docker-run-full:
docker compose -f docker/docker-compose.full.yml --profile gateway up
## docker-run-agent: Run picoclaw agent in Docker (interactive, Alpine-based)
docker-run-agent:
docker compose -f docker/docker-compose.yml run --rm picoclaw-agent
## docker-run-agent-full: Run picoclaw agent in Docker (interactive, full-featured)
docker-run-agent-full:
docker compose -f docker/docker-compose.full.yml run --rm picoclaw-agent
## docker-clean: Clean Docker images and volumes
docker-clean:
docker compose -f docker/docker-compose.yml down -v
docker compose -f docker/docker-compose.full.yml down -v
docker rmi picoclaw:latest picoclaw:full 2>/dev/null || true
## build-macos-app: Build PicoClaw macOS .app bundle (no terminal window)
build-macos-app:build-launcher
@echo "Building macOS .app bundle..."
@if [ "$(UNAME_S)" != "Darwin" ]; then \
echo "Error: This target is only available on macOS"; \
exit 1; \
fi
@./scripts/build-macos-app.sh $(PLATFORM)-$(ARCH)
@echo "macOS .app bundle created: $(BUILD_DIR)/PicoClaw.app"
## mem: Build membench, download LOCOMO data (if needed), run benchmark, and show results
mem:
@echo "Building membench..."
@mkdir -p $(BUILD_DIR)
@$(GO) build -o $(BUILD_DIR)/membench ./cmd/membench
@echo "Build complete: $(BUILD_DIR)/membench"
@if [ ! -f $(BUILD_DIR)/memdata/locomo10.json ]; then \
echo "Downloading LOCOMO dataset..."; \
mkdir -p $(BUILD_DIR)/memdata; \
curl -sfL "https://raw.githubusercontent.com/snap-research/locomo/main/data/locomo10.json" \
-o $(BUILD_DIR)/memdata/locomo10.json && [ -s $(BUILD_DIR)/memdata/locomo10.json ] || { echo "Error: LOCOMO download failed"; exit 1; }; \
echo "Download complete"; \
else \
echo "LOCOMO dataset already exists, skipping download"; \
fi
@echo "Running benchmark..."
@rm -rf $(BUILD_DIR)/memout
@$(BUILD_DIR)/membench run --data $(BUILD_DIR)/memdata --out $(BUILD_DIR)/memout --budget 4000
## help: Show this help message
help:
@echo "picoclaw Makefile"
@@ -156,13 +493,15 @@ help:
@echo " make [target]"
@echo ""
@echo "Targets:"
@grep -E '^## ' $(MAKEFILE_LIST) | sed 's/## / /'
@grep -E '^## ' $(MAKEFILE_LIST) | sort | awk -F': ' '{printf " %-16s %s\n", substr($$1, 4), $$2}'
@echo ""
@echo "Examples:"
@echo " make build # Build for current platform"
@echo " make install # Install to ~/.local/bin"
@echo " make uninstall # Remove from /usr/local/bin"
@echo " make install-skills # Install skills to workspace"
@echo " make docker-build # Build minimal Docker image"
@echo " make docker-test # Test MCP tools in Docker"
@echo ""
@echo "Environment Variables:"
@echo " INSTALL_PREFIX # Installation prefix (default: ~/.local)"
-769
View File
@@ -1,769 +0,0 @@
<div align="center">
<img src="assets/logo.jpg" alt="PicoClaw" width="512">
<h1>PicoClaw: Go で書かれた超効率 AI アシスタント</h1>
<h3>$10 ハードウェア · 10MB RAM · 1秒起動 · 皮皮虾,我们走!</h3>
<h3></h3>
<p>
<img src="https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go&logoColor=white" alt="Go">
<img src="https://img.shields.io/badge/Arch-x86__64%2C%20ARM64%2C%20RISC--V-blue" alt="Hardware">
<img src="https://img.shields.io/badge/license-MIT-green" alt="License">
</p>
**日本語** | [English](README.md)
</div>
---
🦐 PicoClaw は [nanobot](https://github.com/HKUDS/nanobot) にインスパイアされた超軽量パーソナル AI アシスタントです。Go でゼロからリファクタリングされ、AI エージェント自身がアーキテクチャの移行とコード最適化を推進するセルフブートストラッピングプロセスで構築されました。
⚡️ $10 のハードウェアで 10MB 未満の RAM で動作:OpenClaw より 99% 少ないメモリ、Mac mini より 98% 安い!
<table align="center">
<tr align="center">
<td align="center" valign="top">
<p align="center">
<img src="assets/picoclaw_mem.gif" width="360" height="240">
</p>
</td>
<td align="center" valign="top">
<p align="center">
<img src="assets/licheervnano.png" width="400" height="240">
</p>
</td>
</tr>
</table>
## 📢 ニュース
2026-02-09 🎉 PicoClaw リリース!$10 ハードウェアで 10MB 未満の RAM で動く AI エージェントを 1 日で構築。🦐 皮皮虾,我们走!
## ✨ 特徴
🪶 **超軽量**: メモリフットプリント 10MB 未満 — Clawdbot のコア機能より 99% 小さい。
💰 **最小コスト**: $10 ハードウェアで動作 — Mac mini より 98% 安い。
⚡️ **超高速**: 起動時間 400 倍高速、0.6GHz シングルコアでも 1 秒で起動。
🌍 **真のポータビリティ**: RISC-V、ARM、x86 対応の単一バイナリ。ワンクリックで Go!
🤖 **AI ブートストラップ**: 自律的な Go ネイティブ実装 — コアの 95% が AI 生成、人間によるレビュー付き。
| | OpenClaw | NanoBot | **PicoClaw** |
| --- | --- | --- |--- |
| **言語** | TypeScript | Python | **Go** |
| **RAM** | >1GB |>100MB| **< 10MB** |
| **起動時間**</br>(0.8GHz コア) | >500秒 | >30秒 | **<1秒** |
| **コスト** | Mac Mini 599$ | 大半の Linux SBC </br>~50$ |**あらゆる Linux ボード**</br>**最安 10$** |
<img src="assets/compare.jpg" alt="PicoClaw" width="512">
## 🦾 デモンストレーション
### 🛠️ スタンダードアシスタントワークフロー
<table align="center">
<tr align="center">
<th><p align="center">🧩 フルスタックエンジニア</p></th>
<th><p align="center">🗂️ ログ&計画管理</p></th>
<th><p align="center">🔎 Web 検索&学習</p></th>
</tr>
<tr>
<td align="center"><p align="center"><img src="assets/picoclaw_code.gif" width="240" height="180"></p></td>
<td align="center"><p align="center"><img src="assets/picoclaw_memory.gif" width="240" height="180"></p></td>
<td align="center"><p align="center"><img src="assets/picoclaw_search.gif" width="240" height="180"></p></td>
</tr>
<tr>
<td align="center">開発 · デプロイ · スケール</td>
<td align="center">スケジュール · 自動化 · メモリ</td>
<td align="center">発見 · インサイト · トレンド</td>
</tr>
</table>
### 🐜 革新的な省フットプリントデプロイ
PicoClaw はほぼすべての Linux デバイスにデプロイできます!
- $9.9 [LicheeRV-Nano](https://www.aliexpress.com/item/1005006519668532.html) E(Ethernet) または W(WiFi6) バージョン、最小ホームアシスタントに
- $30~50 [NanoKVM](https://www.aliexpress.com/item/1005007369816019.html) または $100 [NanoKVM-Pro](https://www.aliexpress.com/item/1005010048471263.html) サーバー自動メンテナンスに
- $50 [MaixCAM](https://www.aliexpress.com/item/1005008053333693.html) または $100 [MaixCAM2](https://www.kickstarter.com/projects/zepan/maixcam2-build-your-next-gen-4k-ai-camera) スマート監視に
https://private-user-images.githubusercontent.com/83055338/547056448-e7b031ff-d6f5-4468-bcca-5726b6fecb5c.mp4
🌟 もっと多くのデプロイ事例が待っています!
## 📦 インストール
### コンパイル済みバイナリでインストール
[リリースページ](https://github.com/sipeed/picoclaw/releases) からお使いのプラットフォーム用のファームウェアをダウンロードしてください。
### ソースからインストール(最新機能、開発向け推奨)
```bash
git clone https://github.com/sipeed/picoclaw.git
cd picoclaw
make deps
# ビルド(インストール不要)
make build
# 複数プラットフォーム向けビルド
make build-all
# ビルドとインストール
make install
```
## 🐳 Docker Compose
Docker Compose を使えば、ローカルにインストールせずに PicoClaw を実行できます。
```bash
# 1. リポジトリをクローン
git clone https://github.com/sipeed/picoclaw.git
cd picoclaw
# 2. API キーを設定
cp config/config.example.json config/config.json
vim config/config.json # DISCORD_BOT_TOKEN, プロバイダーの API キーを設定
# 3. ビルドと起動
docker compose --profile gateway up -d
# 4. ログ確認
docker compose logs -f picoclaw-gateway
# 5. 停止
docker compose --profile gateway down
```
### Agent モード(ワンショット)
```bash
# 質問を投げる
docker compose run --rm picoclaw-agent -m "What is 2+2?"
# インタラクティブモード
docker compose run --rm picoclaw-agent
```
### リビルド
```bash
docker compose --profile gateway build --no-cache
docker compose --profile gateway up -d
```
### 🚀 クイックスタート(ネイティブ)
> [!TIP]
> `~/.picoclaw/config.json` に API キーを設定してください。
> API キーの取得先: [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM)
> Web 検索は **任意** です - 無料の [Brave Search API](https://brave.com/search/api) (月 2000 クエリ無料)
**1. 初期化**
```bash
picoclaw onboard
```
**2. 設定** (`~/.picoclaw/config.json`)
```json
{
"agents": {
"defaults": {
"workspace": "~/.picoclaw/workspace",
"model": "glm-4.7",
"max_tokens": 8192,
"temperature": 0.7,
"max_tool_iterations": 20
}
},
"providers": {
"openrouter": {
"api_key": "xxx",
"api_base": "https://openrouter.ai/api/v1"
}
},
"tools": {
"web": {
"search": {
"api_key": "YOUR_BRAVE_API_KEY",
"max_results": 5
}
}
},
"heartbeat": {
"enabled": true,
"interval": 30
}
}
```
**3. API キーの取得**
- **LLM プロバイダー**: [OpenRouter](https://openrouter.ai/keys) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) · [Anthropic](https://console.anthropic.com) · [OpenAI](https://platform.openai.com) · [Gemini](https://aistudio.google.com/api-keys)
- **Web 検索**(任意): [Brave Search](https://brave.com/search/api) - 無料枠あり(月 2000 リクエスト)
> **注意**: 完全な設定テンプレートは `config.example.json` を参照してください。
**3. チャット**
```bash
picoclaw agent -m "What is 2+2?"
```
これだけです!2 分で AI アシスタントが動きます。
---
## 💬 チャットアプリ
Telegram、Discord、QQ、DingTalk、LINE で PicoClaw と会話できます
| チャネル | セットアップ |
|---------|------------|
| **Telegram** | 簡単(トークンのみ) |
| **Discord** | 簡単(Bot トークン + Intents |
| **QQ** | 簡単(AppID + AppSecret |
| **DingTalk** | 普通(アプリ認証情報) |
| **LINE** | 普通(認証情報 + Webhook URL |
<details>
<summary><b>Telegram</b>(推奨)</summary>
**1. Bot を作成**
- Telegram を開き、`@BotFather` を検索
- `/newbot` を送信、プロンプトに従う
- トークンをコピー
**2. 設定**
```json
{
"channels": {
"telegram": {
"enabled": true,
"token": "YOUR_BOT_TOKEN",
"allowFrom": ["YOUR_USER_ID"]
}
}
}
```
> ユーザー ID は Telegram の `@userinfobot` から取得できます。
**3. 起動**
```bash
picoclaw gateway
```
</details>
<details>
<summary><b>Discord</b></summary>
**1. Bot を作成**
- https://discord.com/developers/applications にアクセス
- アプリケーションを作成 → Bot → Add Bot
- Bot トークンをコピー
**2. Intents を有効化**
- Bot の設定画面で **MESSAGE CONTENT INTENT** を有効化
- (任意)**SERVER MEMBERS INTENT** も有効化
**3. ユーザー ID を取得**
- Discord 設定 → 詳細設定 → **開発者モード** を有効化
- 自分のアバターを右クリック → **ユーザーIDをコピー**
**4. 設定**
```json
{
"channels": {
"discord": {
"enabled": true,
"token": "YOUR_BOT_TOKEN",
"allowFrom": ["YOUR_USER_ID"]
}
}
}
```
**5. Bot を招待**
- OAuth2 → URL Generator
- Scopes: `bot`
- Bot Permissions: `Send Messages`, `Read Message History`
- 生成された招待 URL を開き、サーバーに Bot を追加
**6. 起動**
```bash
picoclaw gateway
```
</details>
<details>
<summary><b>QQ</b></summary>
**1. Bot を作成**
- [QQ オープンプラットフォーム](https://q.qq.com/#) にアクセス
- アプリケーションを作成 → **AppID****AppSecret** を取得
**2. 設定**
```json
{
"channels": {
"qq": {
"enabled": true,
"app_id": "YOUR_APP_ID",
"app_secret": "YOUR_APP_SECRET",
"allow_from": []
}
}
}
```
> `allow_from` を空にすると全ユーザーを許可、QQ番号を指定してアクセス制限可能。
**3. 起動**
```bash
picoclaw gateway
```
</details>
<details>
<summary><b>DingTalk</b></summary>
**1. Bot を作成**
- [オープンプラットフォーム](https://open.dingtalk.com/) にアクセス
- 内部アプリを作成
- Client ID と Client Secret をコピー
**2. 設定**
```json
{
"channels": {
"dingtalk": {
"enabled": true,
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"allow_from": []
}
}
}
```
> `allow_from` を空にすると全ユーザーを許可、ユーザーIDを指定してアクセス制限可能。
**3. 起動**
```bash
picoclaw gateway
```
</details>
<details>
<summary><b>LINE</b></summary>
**1. LINE 公式アカウントを作成**
- [LINE Developers Console](https://developers.line.biz/) にアクセス
- プロバイダーを作成 → Messaging API チャネルを作成
- **チャネルシークレット** と **チャネルアクセストークン** をコピー
**2. 設定**
```json
{
"channels": {
"line": {
"enabled": true,
"channel_secret": "YOUR_CHANNEL_SECRET",
"channel_access_token": "YOUR_CHANNEL_ACCESS_TOKEN",
"webhook_host": "0.0.0.0",
"webhook_port": 18791,
"webhook_path": "/webhook/line",
"allow_from": []
}
}
}
```
**3. Webhook URL を設定**
LINE の Webhook には HTTPS が必要です。リバースプロキシまたはトンネルを使用してください:
```bash
# ngrok の例
ngrok http 18791
```
LINE Developers Console で Webhook URL を `https://あなたのドメイン/webhook/line` に設定し、**Webhook の利用** を有効にしてください。
**4. 起動**
```bash
picoclaw gateway
```
> グループチャットでは @メンション時のみ応答します。返信は元メッセージを引用する形式です。
> **Docker Compose**: `picoclaw-gateway` サービスに `ports: ["18791:18791"]` を追加して Webhook ポートを公開してください。
</details>
## ⚙️ 設定
設定ファイル: `~/.picoclaw/config.json`
### ワークスペース構成
PicoClaw は設定されたワークスペース(デフォルト: `~/.picoclaw/workspace`)にデータを保存します:
```
~/.picoclaw/workspace/
├── sessions/ # 会話セッションと履歴
├── memory/ # 長期メモリ(MEMORY.md
├── state/ # 永続状態(最後のチャネルなど)
├── cron/ # スケジュールジョブデータベース
├── skills/ # カスタムスキル
├── AGENTS.md # エージェントの行動ガイド
├── HEARTBEAT.md # 定期タスクプロンプト(30分ごとに確認)
├── IDENTITY.md # エージェントのアイデンティティ
├── SOUL.md # エージェントのソウル
├── TOOLS.md # ツールの説明
└── USER.md # ユーザー設定
```
### 🔒 セキュリティサンドボックス
PicoClaw はデフォルトでサンドボックス環境で実行されます。エージェントは設定されたワークスペース内のファイルにのみアクセスし、コマンドを実行できます。
#### デフォルト設定
```json
{
"agents": {
"defaults": {
"workspace": "~/.picoclaw/workspace",
"restrict_to_workspace": true
}
}
}
```
| オプション | デフォルト | 説明 |
|-----------|-----------|------|
| `workspace` | `~/.picoclaw/workspace` | エージェントの作業ディレクトリ |
| `restrict_to_workspace` | `true` | ファイル/コマンドアクセスをワークスペースに制限 |
#### 保護対象ツール
`restrict_to_workspace: true` の場合、以下のツールがサンドボックス化されます:
| ツール | 機能 | 制限 |
|-------|------|------|
| `read_file` | ファイル読み込み | ワークスペース内のファイルのみ |
| `write_file` | ファイル書き込み | ワークスペース内のファイルのみ |
| `list_dir` | ディレクトリ一覧 | ワークスペース内のディレクトリのみ |
| `edit_file` | ファイル編集 | ワークスペース内のファイルのみ |
| `append_file` | ファイル追記 | ワークスペース内のファイルのみ |
| `exec` | コマンド実行 | コマンドパスはワークスペース内である必要あり |
#### exec ツールの追加保護
`restrict_to_workspace: false` でも、`exec` ツールは以下の危険なコマンドをブロックします:
- `rm -rf`, `del /f`, `rmdir /s` — 一括削除
- `format`, `mkfs`, `diskpart` — ディスクフォーマット
- `dd if=` — ディスクイメージング
- `/dev/sd[a-z]` への書き込み — 直接ディスク書き込み
- `shutdown`, `reboot`, `poweroff` — システムシャットダウン
- フォークボム `:(){ :|:& };:`
#### エラー例
```
[ERROR] tool: Tool execution failed
{tool=exec, error=Command blocked by safety guard (path outside working dir)}
```
```
[ERROR] tool: Tool execution failed
{tool=exec, error=Command blocked by safety guard (dangerous pattern detected)}
```
#### 制限の無効化(セキュリティリスク)
エージェントにワークスペース外のパスへのアクセスが必要な場合:
**方法1: 設定ファイル**
```json
{
"agents": {
"defaults": {
"restrict_to_workspace": false
}
}
}
```
**方法2: 環境変数**
```bash
export PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE=false
```
> ⚠️ **警告**: この制限を無効にすると、エージェントはシステム上の任意のパスにアクセスできるようになります。制御された環境でのみ慎重に使用してください。
#### セキュリティ境界の一貫性
`restrict_to_workspace` 設定は、すべての実行パスで一貫して適用されます:
| 実行パス | セキュリティ境界 |
|---------|-----------------|
| メインエージェント | `restrict_to_workspace` ✅ |
| サブエージェント / Spawn | 同じ制限を継承 ✅ |
| ハートビートタスク | 同じ制限を継承 ✅ |
すべてのパスで同じワークスペース制限が適用されます — サブエージェントやスケジュールタスクを通じてセキュリティ境界をバイパスする方法はありません。
### ハートビート(定期タスク)
PicoClaw は自動的に定期タスクを実行できます。ワークスペースに `HEARTBEAT.md` ファイルを作成します:
```markdown
# 定期タスク
- 重要なメールをチェック
- 今後の予定を確認
- 天気予報をチェック
```
エージェントは30分ごと(設定可能)にこのファイルを読み込み、利用可能なツールを使ってタスクを実行します。
#### spawn で非同期タスク実行
時間のかかるタスク(Web検索、API呼び出し)には `spawn` ツールを使って**サブエージェント**を作成します:
```markdown
# 定期タスク
## クイックタスク(直接応答)
- 現在時刻を報告
## 長時間タスク(spawn で非同期)
- AIニュースを検索して要約
- メールをチェックして重要なメッセージを報告
```
**主な特徴:**
| 機能 | 説明 |
|------|------|
| **spawn** | 非同期サブエージェントを作成、ハートビートをブロックしない |
| **独立コンテキスト** | サブエージェントは独自のコンテキストを持ち、セッション履歴なし |
| **message ツール** | サブエージェントは message ツールで直接ユーザーと通信 |
| **非ブロッキング** | spawn 後、ハートビートは次のタスクへ継続 |
#### サブエージェントの通信方法
```
ハートビート発動
エージェントが HEARTBEAT.md を読む
長いタスク: spawn サブエージェント
↓ ↓
次のタスクへ継続 サブエージェントが独立して動作
↓ ↓
全タスク完了 message ツールを使用
↓ ↓
HEARTBEAT_OK 応答 ユーザーが直接結果を受け取る
```
サブエージェントはツール(message、web_search など)にアクセスでき、メインエージェントを経由せずにユーザーと通信できます。
**設定:**
```json
{
"heartbeat": {
"enabled": true,
"interval": 30
}
}
```
| オプション | デフォルト | 説明 |
|-----------|-----------|------|
| `enabled` | `true` | ハートビートの有効/無効 |
| `interval` | `30` | チェック間隔(分)、最小5分 |
**環境変数:**
- `PICOCLAW_HEARTBEAT_ENABLED=false` で無効化
- `PICOCLAW_HEARTBEAT_INTERVAL=60` で間隔変更
### 基本設定
1. **設定ファイルの作成:**
```bash
cp config.example.json config/config.json
```
2. **設定の編集:**
```json
{
"providers": {
"openrouter": {
"api_key": "sk-or-v1-..."
}
},
"channels": {
"discord": {
"enabled": true,
"token": "YOUR_DISCORD_BOT_TOKEN"
}
}
}
```
3. **実行**
```bash
picoclaw agent -m "Hello"
```
</details>
<details>
<summary><b>完全な設定例</b></summary>
```json
{
"agents": {
"defaults": {
"model": "anthropic/claude-opus-4-5"
}
},
"providers": {
"openrouter": {
"apiKey": "sk-or-v1-xxx"
},
"groq": {
"apiKey": "gsk_xxx"
}
},
"channels": {
"telegram": {
"enabled": true,
"token": "123456:ABC...",
"allowFrom": ["123456789"]
},
"discord": {
"enabled": true,
"token": "",
"allow_from": [""]
},
"whatsapp": {
"enabled": false
},
"feishu": {
"enabled": false,
"appId": "cli_xxx",
"appSecret": "xxx",
"encryptKey": "",
"verificationToken": "",
"allowFrom": []
}
},
"tools": {
"web": {
"search": {
"apiKey": "BSA..."
}
}
},
"heartbeat": {
"enabled": true,
"interval": 30
}
}
```
</details>
## CLI リファレンス
| コマンド | 説明 |
|---------|------|
| `picoclaw onboard` | 設定&ワークスペースの初期化 |
| `picoclaw agent -m "..."` | エージェントとチャット |
| `picoclaw agent` | インタラクティブチャットモード |
| `picoclaw gateway` | ゲートウェイを起動 |
| `picoclaw status` | ステータスを表示 |
## 🤝 コントリビュート&ロードマップ
PR 歓迎!コードベースは意図的に小さく読みやすくしています。🤗
Discord: https://discord.gg/V4sAZ9XWpN
<img src="assets/wechat.png" alt="PicoClaw" width="512">
## 🐛 トラブルシューティング
### Web 検索で「API 配置问题」と表示される
検索 API キーをまだ設定していない場合、これは正常です。PicoClaw は手動検索用の便利なリンクを提供します。
Web 検索を有効にするには:
1. [https://brave.com/search/api](https://brave.com/search/api) で無料の API キーを取得(月 2000 クエリ無料)
2. `~/.picoclaw/config.json` に追加:
```json
{
"tools": {
"web": {
"search": {
"api_key": "YOUR_BRAVE_API_KEY",
"max_results": 5
}
}
}
}
```
### コンテンツフィルタリングエラーが出る
一部のプロバイダー(Zhipu など)にはコンテンツフィルタリングがあります。クエリを言い換えるか、別のモデルを使用してください。
### Telegram Bot で「Conflict: terminated by other getUpdates」と表示される
別のインスタンスが実行中の場合に発生します。`picoclaw gateway` が 1 つだけ実行されていることを確認してください。
---
## 📝 API キー比較
| サービス | 無料枠 | ユースケース |
|---------|--------|------------|
| **OpenRouter** | 月 200K トークン | 複数モデル(Claude, GPT-4 など) |
| **Zhipu** | 月 200K トークン | 中国ユーザー向け最適 |
| **Brave Search** | 月 2000 クエリ | Web 検索機能 |
| **Groq** | 無料枠あり | 高速推論(Llama, Mixtral |
+522 -709
View File
File diff suppressed because it is too large Load Diff
-721
View File
@@ -1,721 +0,0 @@
<div align="center">
<img src="assets/logo.jpg" alt="PicoClaw" width="512">
<h1>PicoClaw: 基于Go语言的超高效 AI 助手</h1>
<h3>10$硬件 · 10MB内存 · 1秒启动 · 皮皮虾,我们走!</h3>
<p>
<img src="https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go&logoColor=white" alt="Go">
<img src="https://img.shields.io/badge/Arch-x86__64%2C%20ARM64%2C%20RISC--V-blue" alt="Hardware">
<img src="https://img.shields.io/badge/license-MIT-green" alt="License">
<br>
<a href="https://picoclaw.io"><img src="https://img.shields.io/badge/Website-picoclaw.io-blue?style=flat&logo=google-chrome&logoColor=white" alt="Website"></a>
<a href="https://x.com/SipeedIO"><img src="https://img.shields.io/badge/X_(Twitter)-SipeedIO-black?style=flat&logo=x&logoColor=white" alt="Twitter"></a>
</p>
**中文** | [日本語](README.ja.md) | [English](README.md)
</div>
---
🦐 **PicoClaw** 是一个受 [nanobot](https://github.com/HKUDS/nanobot) 启发的超轻量级个人 AI 助手。它采用 **Go 语言** 从零重构,经历了一个“自举”过程——即由 AI Agent 自身驱动了整个架构迁移和代码优化。
⚡️ **极致轻量**:可在 **10 美元** 的硬件上运行,内存占用 **<10MB**。这意味着比 OpenClaw 节省 99% 的内存,比 Mac mini 便宜 98%
<table align="center">
<tr align="center">
<td align="center" valign="top">
<p align="center">
<img src="assets/picoclaw_mem.gif" width="360" height="240">
</p>
</td>
<td align="center" valign="top">
<p align="center">
<img src="assets/licheervnano.png" width="400" height="240">
</p>
</td>
</tr>
</table>
注意:人手有限,中文文档可能略有滞后,请优先查看英文文档。
> [!CAUTION]
> **🚨 SECURITY & OFFICIAL CHANNELS / 安全声明**
> * **无加密货币 (NO CRYPTO):** PicoClaw **没有** 发行任何官方代币、Token 或虚拟货币。所有在 `pump.fun` 或其他交易平台上的相关声称均为 **诈骗**。
> * **官方域名:** 唯一的官方网站是 **[picoclaw.io](https://picoclaw.io)**,公司官网是 **[sipeed.com](https://sipeed.com)**。
> * **警惕:** 许多 `.ai/.org/.com/.net/...` 后缀的域名被第三方抢注,请勿轻信。
> * **注意:** picoclaw正在初期的快速功能开发阶段,可能有尚未修复的网络安全问题,在1.0正式版发布前,请不要将其部署到生产环境中
> * **注意:** picoclaw最近合并了大量PRs,近期版本可能内存占用较大(10~20MB),我们将在功能较为收敛后进行资源占用优化.
## 📢 新闻 (News)
2026-02-16 🎉 PicoClaw 在一周内突破了12K star! 感谢大家的关注!PicoClaw 的成长速度超乎我们预期. 由于PR数量的快速膨胀,我们亟需社区开发者参与维护. 我们需要的志愿者角色和roadmap已经发布到了[这里](doc/picoclaw_community_roadmap_260216.md), 期待你的参与!
2026-02-13 🎉 **PicoClaw 在 4 天内突破 5000 Stars** 感谢社区的支持!由于正值中国春节假期,PR 和 Issue 涌入较多,我们正在利用这段时间敲定 **项目路线图 (Roadmap)** 并组建 **开发者群组**,以便加速 PicoClaw 的开发。
🚀 **行动号召:** 请在 GitHub Discussions 中提交您的功能请求 (Feature Requests)。我们将在接下来的周会上进行审查和优先级排序。
2026-02-09 🎉 **PicoClaw 正式发布!** 仅用 1 天构建,旨在将 AI Agent 带入 10 美元硬件与 <10MB 内存的世界。🦐 PicoClaw(皮皮虾),我们走!
## ✨ 特性
🪶 **超轻量级**: 核心功能内存占用 <10MB — 比 Clawdbot 小 99%。
💰 **极低成本**: 高效到足以在 10 美元的硬件上运行 — 比 Mac mini 便宜 98%。
⚡️ **闪电启动**: 启动速度快 400 倍,即使在 0.6GHz 单核处理器上也能在 1 秒内启动。
🌍 **真正可移植**: 跨 RISC-V、ARM 和 x86 架构的单二进制文件,一键运行!
🤖 **AI 自举**: 纯 Go 语言原生实现 — 95% 的核心代码由 Agent 生成,并经由“人机回环 (Human-in-the-loop)”微调。
| | OpenClaw | NanoBot | **PicoClaw** |
| --- | --- | --- | --- |
| **语言** | TypeScript | Python | **Go** |
| **RAM** | >1GB | >100MB | **< 10MB** |
| **启动时间**</br>(0.8GHz core) | >500s | >30s | **<1s** |
| **成本** | Mac Mini $599 | 大多数 Linux 开发板 ~$50 | **任意 Linux 开发板**</br>**低至 $10** |
<img src="assets/compare.jpg" alt="PicoClaw" width="512">
## 🦾 演示
### 🛠️ 标准助手工作流
<table align="center">
<tr align="center">
<th><p align="center">🧩 全栈工程师模式</p></th>
<th><p align="center">🗂️ 日志与规划管理</p></th>
<th><p align="center">🔎 网络搜索与学习</p></th>
</tr>
<tr>
<td align="center"><p align="center"><img src="assets/picoclaw_code.gif" width="240" height="180"></p></td>
<td align="center"><p align="center"><img src="assets/picoclaw_memory.gif" width="240" height="180"></p></td>
<td align="center"><p align="center"><img src="assets/picoclaw_search.gif" width="240" height="180"></p></td>
</tr>
<tr>
<td align="center">开发 • 部署 • 扩展</td>
<td align="center">日程 • 自动化 • 记忆</td>
<td align="center">发现 • 洞察 • 趋势</td>
</tr>
</table>
### 🐜 创新的低占用部署
PicoClaw 几乎可以部署在任何 Linux 设备上!
* $9.9 [LicheeRV-Nano](https://www.aliexpress.com/item/1005006519668532.html) E(网口) 或 W(WiFi6) 版本,用于极简家庭助手。
* $30~50 [NanoKVM](https://www.aliexpress.com/item/1005007369816019.html),或 $100 [NanoKVM-Pro](https://www.aliexpress.com/item/1005010048471263.html),用于自动化服务器运维。
* $50 [MaixCAM](https://www.aliexpress.com/item/1005008053333693.html) 或 $100 [MaixCAM2](https://www.kickstarter.com/projects/zepan/maixcam2-build-your-next-gen-4k-ai-camera),用于智能监控。
[https://private-user-images.githubusercontent.com/83055338/547056448-e7b031ff-d6f5-4468-bcca-5726b6fecb5c.mp4](https://private-user-images.githubusercontent.com/83055338/547056448-e7b031ff-d6f5-4468-bcca-5726b6fecb5c.mp4)
🌟 更多部署案例敬请期待!
## 📦 安装
### 使用预编译二进制文件安装
从 [Release 页面](https://github.com/sipeed/picoclaw/releases) 下载适用于您平台的固件。
### 从源码安装(获取最新特性,开发推荐)
```bash
git clone https://github.com/sipeed/picoclaw.git
cd picoclaw
make deps
# 构建(无需安装)
make build
# 为多平台构建
make build-all
# 构建并安装
make install
```
## 🐳 Docker Compose
您也可以使用 Docker Compose 运行 PicoClaw,无需在本地安装任何环境。
```bash
# 1. 克隆仓库
git clone https://github.com/sipeed/picoclaw.git
cd picoclaw
# 2. 设置 API Key
cp config/config.example.json config/config.json
vim config/config.json # 设置 DISCORD_BOT_TOKEN, API keys 等
# 3. 构建并启动
docker compose --profile gateway up -d
# 4. 查看日志
docker compose logs -f picoclaw-gateway
# 5. 停止
docker compose --profile gateway down
```
### Agent 模式 (一次性运行)
```bash
# 提问
docker compose run --rm picoclaw-agent -m "2+2 等于几?"
# 交互模式
docker compose run --rm picoclaw-agent
```
### 重新构建
```bash
docker compose --profile gateway build --no-cache
docker compose --profile gateway up -d
```
### 🚀 快速开始
> [!TIP]
> 在 `~/.picoclaw/config.json` 中设置您的 API Key。
> 获取 API Key: [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu (智谱)](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM)
> 网络搜索是 **可选的** - 获取免费的 [Brave Search API](https://brave.com/search/api) (每月 2000 次免费查询)
**1. 初始化 (Initialize)**
```bash
picoclaw onboard
```
**2. 配置 (Configure)** (`~/.picoclaw/config.json`)
```json
{
"agents": {
"defaults": {
"workspace": "~/.picoclaw/workspace",
"model": "glm-4.7",
"max_tokens": 8192,
"temperature": 0.7,
"max_tool_iterations": 20
}
},
"providers": {
"openrouter": {
"api_key": "xxx",
"api_base": "https://openrouter.ai/api/v1"
}
},
"tools": {
"web": {
"search": {
"api_key": "YOUR_BRAVE_API_KEY",
"max_results": 5
}
}
}
}
```
**3. 获取 API Key**
* **LLM 提供商**: [OpenRouter](https://openrouter.ai/keys) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) · [Anthropic](https://console.anthropic.com) · [OpenAI](https://platform.openai.com) · [Gemini](https://aistudio.google.com/api-keys)
* **网络搜索** (可选): [Brave Search](https://brave.com/search/api) - 提供免费层级 (2000 请求/月)
> **注意**: 完整的配置模板请参考 `config.example.json`。
**4. 对话 (Chat)**
```bash
picoclaw agent -m "2+2 等于几?"
```
就是这样!您在 2 分钟内就拥有了一个可工作的 AI 助手。
---
## 💬 聊天应用集成 (Chat Apps)
通过 Telegram, Discord 或钉钉与您的 PicoClaw 对话。
| 渠道 | 设置难度 |
| --- | --- |
| **Telegram** | 简单 (仅需 token) |
| **Discord** | 简单 (bot token + intents) |
| **QQ** | 简单 (AppID + AppSecret) |
| **钉钉 (DingTalk)** | 中等 (app credentials) |
<details>
<summary><b>Telegram</b> (推荐)</summary>
**1. 创建机器人**
* 打开 Telegram,搜索 `@BotFather`
* 发送 `/newbot`,按照提示操作
* 复制 token
**2. 配置**
```json
{
"channels": {
"telegram": {
"enabled": true,
"token": "YOUR_BOT_TOKEN",
"allowFrom": ["YOUR_USER_ID"]
}
}
}
```
> 从 Telegram 上的 `@userinfobot` 获取您的用户 ID。
**3. 运行**
```bash
picoclaw gateway
```
</details>
<details>
<summary><b>Discord</b></summary>
**1. 创建机器人**
* 前往 [https://discord.com/developers/applications](https://discord.com/developers/applications)
* Create an application → Bot → Add Bot
* 复制 bot token
**2. 开启 Intents**
* 在 Bot 设置中,开启 **MESSAGE CONTENT INTENT**
* (可选) 如果计划基于成员数据使用白名单,开启 **SERVER MEMBERS INTENT**
**3. 获取您的 User ID**
* Discord 设置 → Advanced → 开启 **Developer Mode**
* 右键点击您的头像 → **Copy User ID**
**4. 配置**
```json
{
"channels": {
"discord": {
"enabled": true,
"token": "YOUR_BOT_TOKEN",
"allowFrom": ["YOUR_USER_ID"]
}
}
}
```
**5. 邀请机器人**
* OAuth2 → URL Generator
* Scopes: `bot`
* Bot Permissions: `Send Messages`, `Read Message History`
* 打开生成的邀请 URL,将机器人添加到您的服务器
**6. 运行**
```bash
picoclaw gateway
```
</details>
<details>
<summary><b>QQ</b></summary>
**1. 创建机器人**
* 前往 [QQ 开放平台](https://q.qq.com/#)
* 创建应用 → 获取 **AppID****AppSecret**
**2. 配置**
```json
{
"channels": {
"qq": {
"enabled": true,
"app_id": "YOUR_APP_ID",
"app_secret": "YOUR_APP_SECRET",
"allow_from": []
}
}
}
```
> 将 `allow_from` 设为空以允许所有用户,或指定 QQ 号以限制访问。
**3. 运行**
```bash
picoclaw gateway
```
</details>
<details>
<summary><b>钉钉 (DingTalk)</b></summary>
**1. 创建机器人**
* 前往 [开放平台](https://open.dingtalk.com/)
* 创建内部应用
* 复制 Client ID 和 Client Secret
**2. 配置**
```json
{
"channels": {
"dingtalk": {
"enabled": true,
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"allow_from": []
}
}
}
```
> 将 `allow_from` 设为空以允许所有用户,或指定 ID 以限制访问。
**3. 运行**
```bash
picoclaw gateway
```
</details>
## <img src="assets/clawdchat-icon.png" width="24" height="24" alt="ClawdChat"> 加入 Agent 社交网络
只需通过 CLI 或任何集成的聊天应用发送一条消息,即可将 PicoClaw 连接到 Agent 社交网络。
**阅读 `https://clawdchat.ai/skill.md` 并按照说明加入 [ClawdChat.ai**](https://clawdchat.ai)
## ⚙️ 配置详解
配置文件路径: `~/.picoclaw/config.json`
### 工作区布局 (Workspace Layout)
PicoClaw 将数据存储在您配置的工作区中(默认:`~/.picoclaw/workspace`):
```
~/.picoclaw/workspace/
├── sessions/ # 对话会话和历史
├── memory/ # 长期记忆 (MEMORY.md)
├── state/ # 持久化状态 (最后一次频道等)
├── cron/ # 定时任务数据库
├── skills/ # 自定义技能
├── AGENTS.md # Agent 行为指南
├── HEARTBEAT.md # 周期性任务提示词 (每 30 分钟检查一次)
├── IDENTITY.md # Agent 身份设定
├── SOUL.md # Agent 灵魂/性格
├── TOOLS.md # 工具描述
└── USER.md # 用户偏好
```
### 心跳 / 周期性任务 (Heartbeat)
PicoClaw 可以自动执行周期性任务。在工作区创建 `HEARTBEAT.md` 文件:
```markdown
# Periodic Tasks
- Check my email for important messages
- Review my calendar for upcoming events
- Check the weather forecast
```
Agent 将每隔 30 分钟(可配置)读取此文件,并使用可用工具执行任务。
#### 使用 Spawn 的异步任务
对于耗时较长的任务(网络搜索、API 调用),使用 `spawn` 工具创建一个 **子 Agent (subagent)**
```markdown
# Periodic Tasks
## Quick Tasks (respond directly)
- Report current time
## Long Tasks (use spawn for async)
- Search the web for AI news and summarize
- Check email and report important messages
```
**关键行为:**
| 特性 | 描述 |
| --- | --- |
| **spawn** | 创建异步子 Agent,不阻塞主心跳进程 |
| **独立上下文** | 子 Agent 拥有独立上下文,无会话历史 |
| **message tool** | 子 Agent 通过 message 工具直接与用户通信 |
| **非阻塞** | spawn 后,心跳继续处理下一个任务 |
#### 子 Agent 通信原理
```
心跳触发 (Heartbeat triggers)
Agent 读取 HEARTBEAT.md
对于长任务: spawn 子 Agent
↓ ↓
继续下一个任务 子 Agent 独立工作
↓ ↓
所有任务完成 子 Agent 使用 "message" 工具
↓ ↓
响应 HEARTBEAT_OK 用户直接收到结果
```
子 Agent 可以访问工具(message, web_search 等),并且无需通过主 Agent 即可独立与用户通信。
**配置:**
```json
{
"heartbeat": {
"enabled": true,
"interval": 30
}
}
```
| 选项 | 默认值 | 描述 |
| --- | --- | --- |
| `enabled` | `true` | 启用/禁用心跳 |
| `interval` | `30` | 检查间隔,单位分钟 (最小: 5) |
**环境变量:**
* `PICOCLAW_HEARTBEAT_ENABLED=false` 禁用
* `PICOCLAW_HEARTBEAT_INTERVAL=60` 更改间隔
### 提供商 (Providers)
> [!NOTE]
> Groq 通过 Whisper 提供免费的语音转录。如果配置了 Groq,Telegram 语音消息将被自动转录为文字。
| 提供商 | 用途 | 获取 API Key |
| --- | --- | --- |
| `gemini` | LLM (Gemini 直连) | [aistudio.google.com](https://aistudio.google.com) |
| `zhipu` | LLM (智谱直连) | [bigmodel.cn](bigmodel.cn) |
| `openrouter(待测试)` | LLM (推荐,可访问所有模型) | [openrouter.ai](https://openrouter.ai) |
| `anthropic(待测试)` | LLM (Claude 直连) | [console.anthropic.com](https://console.anthropic.com) |
| `openai(待测试)` | LLM (GPT 直连) | [platform.openai.com](https://platform.openai.com) |
| `deepseek(待测试)` | LLM (DeepSeek 直连) | [platform.deepseek.com](https://platform.deepseek.com) |
| `groq` | LLM + **语音转录** (Whisper) | [console.groq.com](https://console.groq.com) |
<details>
<summary><b>智谱 (Zhipu) 配置示例</b></summary>
**1. 获取 API key 和 base URL**
* 获取 [API key](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)
**2. 配置**
```json
{
"agents": {
"defaults": {
"workspace": "~/.picoclaw/workspace",
"model": "glm-4.7",
"max_tokens": 8192,
"temperature": 0.7,
"max_tool_iterations": 20
}
},
"providers": {
"zhipu": {
"api_key": "Your API Key",
"api_base": "https://open.bigmodel.cn/api/paas/v4"
},
},
}
```
**3. 运行**
```bash
picoclaw agent -m "你好"
```
</details>
<details>
<summary><b>完整配置示例</b></summary>
```json
{
"agents": {
"defaults": {
"model": "anthropic/claude-opus-4-5"
}
},
"providers": {
"openrouter": {
"api_key": "sk-or-v1-xxx"
},
"groq": {
"api_key": "gsk_xxx"
}
},
"channels": {
"telegram": {
"enabled": true,
"token": "123456:ABC...",
"allow_from": ["123456789"]
},
"discord": {
"enabled": true,
"token": "",
"allow_from": [""]
},
"whatsapp": {
"enabled": false
},
"feishu": {
"enabled": false,
"app_id": "cli_xxx",
"app_secret": "xxx",
"encrypt_key": "",
"verification_token": "",
"allow_from": []
},
"qq": {
"enabled": false,
"app_id": "",
"app_secret": "",
"allow_from": []
}
},
"tools": {
"web": {
"search": {
"api_key": "BSA..."
}
}
},
"heartbeat": {
"enabled": true,
"interval": 30
}
}
```
</details>
## CLI 命令行参考
| 命令 | 描述 |
| --- | --- |
| `picoclaw onboard` | 初始化配置和工作区 |
| `picoclaw agent -m "..."` | 与 Agent 对话 |
| `picoclaw agent` | 交互式聊天模式 |
| `picoclaw gateway` | 启动网关 (Gateway) |
| `picoclaw status` | 显示状态 |
| `picoclaw cron list` | 列出所有定时任务 |
| `picoclaw cron add ...` | 添加定时任务 |
### 定时任务 / 提醒 (Scheduled Tasks)
PicoClaw 通过 `cron` 工具支持定时提醒和重复任务:
* **一次性提醒**: "Remind me in 10 minutes" (10分钟后提醒我) → 10分钟后触发一次
* **重复任务**: "Remind me every 2 hours" (每2小时提醒我) → 每2小时触发
* **Cron 表达式**: "Remind me at 9am daily" (每天上午9点提醒我) → 使用 cron 表达式
任务存储在 `~/.picoclaw/workspace/cron/` 中并自动处理。
## 🤝 贡献与路线图 (Roadmap)
欢迎提交 PR!代码库刻意保持小巧和可读。🤗
路线图即将发布...
开发者群组正在组建中,入群门槛:至少合并过 1 个 PR。
用户群组:
Discord: [https://discord.gg/V4sAZ9XWpN](https://discord.gg/V4sAZ9XWpN)
<img src="assets/wechat.png" alt="PicoClaw" width="512">
## 🐛 疑难解答 (Troubleshooting)
### 网络搜索提示 "API 配置问题"
如果您尚未配置搜索 API Key,这是正常的。PicoClaw 会提供手动搜索的帮助链接。
启用网络搜索:
1. 在 [https://brave.com/search/api](https://brave.com/search/api) 获取免费 API Key (每月 2000 次免费查询)
2. 添加到 `~/.picoclaw/config.json`:
```json
{
"tools": {
"web": {
"search": {
"api_key": "YOUR_BRAVE_API_KEY",
"max_results": 5
}
}
}
}
```
### 遇到内容过滤错误 (Content Filtering Errors)
某些提供商(如智谱)有严格的内容过滤。尝试改写您的问题或使用其他模型。
### Telegram bot 提示 "Conflict: terminated by other getUpdates"
这表示有另一个机器人实例正在运行。请确保同一时间只有一个 `picoclaw gateway` 进程在运行。
---
## 📝 API Key 对比
| 服务 | 免费层级 | 适用场景 |
| --- | --- | --- |
| **OpenRouter** | 200K tokens/月 | 多模型聚合 (Claude, GPT-4 等) |
| **智谱 (Zhipu)** | 200K tokens/月 | 最适合中国用户 |
| **Brave Search** | 2000 次查询/月 | 网络搜索功能 |
| **Groq** | 提供免费层级 | 极速推理 (Llama, Mixtral) |
+116
View File
@@ -0,0 +1,116 @@
# 🦐 PicoClaw Roadmap
> **Vision**: To build the ultimate lightweight, secure, and fully autonomous AI Agent infrastructure.automate the mundane, unleash your creativity
---
## 🚀 1. Core Optimization: Extreme Lightweight
*Our defining characteristic. We fight software bloat to ensure PicoClaw runs smoothly on the smallest embedded devices.*
* [**Memory Footprint Reduction**](https://github.com/sipeed/picoclaw/issues/346)
* **Goal**: Run smoothly on 64MB RAM embedded boards (e.g., low-end RISC-V SBCs) with the core process consuming < 20MB.
* **Context**: RAM is expensive and scarce on edge devices. Memory optimization takes precedence over storage size.
* **Action**: Analyze memory growth between releases, remove redundant dependencies, and optimize data structures.
## 🛡️ 2. Security Hardening: Defense in Depth
*Paying off early technical debt. We invite security experts to help build a "Secure-by-Default" agent.*
* **Input Defense & Permission Control**
* **Prompt Injection Defense**: Harden JSON extraction logic to prevent LLM manipulation.
* **Tool Abuse Prevention**: Strict parameter validation to ensure generated commands stay within safe boundaries.
* **SSRF Protection**: Built-in blocklists for network tools to prevent accessing internal IPs (LAN/Metadata services).
* **Sandboxing & Isolation**
* **Filesystem Sandbox**: Restrict file R/W operations to specific directories only.
* **Context Isolation**: Prevent data leakage between different user sessions or channels.
* **Privacy Redaction**: Auto-redact sensitive info (API Keys, PII) from logs and standard outputs.
* **Authentication & Secrets**
* **Crypto Upgrade**: Adopt modern algorithms like `ChaCha20-Poly1305` for secret storage.
* **OAuth 2.0 Flow**: Deprecate hardcoded API keys in the CLI; move to secure OAuth flows.
## 🔌 3. Connectivity: Protocol-First Architecture
*Connect every model, reach every platform.*
* **Provider**
* [**Architecture Upgrade**](https://github.com/sipeed/picoclaw/issues/283): Refactor from "Vendor-based" to "Protocol-based" classification (e.g., OpenAI-compatible, Ollama-compatible). *(Status: In progress by @Daming, ETA 5 days)*
* **Local Models**: Deep integration with **Ollama**, **vLLM**, **LM Studio**, and **Mistral** (local inference).
* **Online Models**: Continued support for frontier closed-source models.
* **Channel**
* **IM Matrix**: QQ, WeChat (Work), DingTalk, Feishu (Lark), Telegram, Discord, WhatsApp, LINE, Slack, Email, KOOK, Signal, ...
* **Standards**: Support for the **OneBot** protocol.
* [**attachment**](https://github.com/sipeed/picoclaw/issues/348): Native handling of images, audio, and video attachments.
* **Skill Marketplace**
* [**Discovery skills**](https://github.com/sipeed/picoclaw/issues/287): Implement `find_skill` to automatically discover and install skills from the [GitHub Skills Repo] or other registries.
## 🧠 4. Advanced Capabilities: From Chatbot to Agentic AI
*Beyond conversation—focusing on action and collaboration.*
* **Operations**
* [**MCP Support**](https://github.com/sipeed/picoclaw/issues/290): Native support for the **Model Context Protocol (MCP)**.
* [**Browser Automation**](https://github.com/sipeed/picoclaw/issues/293): Headless browser control via CDP (Chrome DevTools Protocol) or ActionBook.
* [**Mobile Operation**](https://github.com/sipeed/picoclaw/issues/292): Android device control (similar to BotDrop).
* **Multi-Agent Collaboration**
* [**Basic Multi-Agent**](https://github.com/sipeed/picoclaw/issues/294) implement
* [**Model Routing**](https://github.com/sipeed/picoclaw/issues/295): "Smart Routing" — dispatch simple tasks to small/local models (fast/cheap) and complex tasks to SOTA models (smart).
* [**Swarm Mode**](https://github.com/sipeed/picoclaw/issues/284): Collaboration between multiple PicoClaw instances on the same network.
* [**AIEOS**](https://github.com/sipeed/picoclaw/issues/296): Exploring AI-Native Operating System interaction paradigms.
## 📚 5. Developer Experience (DevEx) & Documentation
*Lowering the barrier to entry so anyone can deploy in minutes.*
* [**QuickGuide (Zero-Config Start)**](https://github.com/sipeed/picoclaw/issues/350)
* Interactive CLI Wizard: If launched without config, automatically detect the environment and guide the user through Token/Network setup step-by-step.
* **Comprehensive Documentation**
* **Platform Guides**: Dedicated guides for Windows, macOS, Linux, and Android.
* **Step-by-Step Tutorials**: "Babysitter-level" guides for configuring Providers and Channels.
* **AI-Assisted Docs**: Using AI to auto-generate API references and code comments (with human verification to prevent hallucinations).
## 🤖 6. Engineering: AI-Powered Open Source
*Born from Vibe Coding, we continue to use AI to accelerate development.*
* **AI-Enhanced CI/CD**
* Integrate AI for automated Code Review, Linting, and PR Labeling.
* **Bot Noise Reduction**: Optimize bot interactions to keep PR timelines clean.
* **Issue Triage**: AI agents to analyze incoming issues and suggest preliminary fixes.
## 🎨 7. Brand & Community
* [**Logo Design**](https://github.com/sipeed/picoclaw/issues/297): We are looking for a **Mantis Shrimp (Stomatopoda)** logo design!
* *Concept*: Needs to reflect "Small but Mighty" and "Lightning Fast Strikes."
---
### 🤝 Call for Contributions
We welcome community contributions to any item on this roadmap! Please comment on the relevant Issue or submit a PR. Let's build the best Edge AI Agent together!
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

+412
View File
@@ -0,0 +1,412 @@
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"sort"
"strings"
"github.com/sipeed/picoclaw/pkg/seahorse"
)
// EvalResult holds per-sample evaluation results for one mode.
type EvalResult struct {
Mode string `json:"mode"`
SampleID string `json:"sampleId"`
QAResults []QAResult `json:"qaResults"`
Agg AggMetrics `json:"aggregated"`
}
// QAResult holds metrics for a single QA pair.
type QAResult struct {
Question string `json:"question"`
Category int `json:"category"`
GoldAnswer string `json:"goldAnswer"`
TokenF1 float64 `json:"tokenF1"`
HitRate float64 `json:"hitRate"`
}
// AggMetrics holds aggregated evaluation metrics.
type AggMetrics struct {
OverallF1 float64 `json:"overallF1"`
OverallHitRate float64 `json:"overallHitRate"`
ByCategory map[int]*CatMetrics `json:"byCategory"`
TotalQuestions int `json:"totalQuestions"`
ValidF1Count int `json:"validF1Count"`
}
// CatMetrics holds metrics for a single category.
type CatMetrics struct {
F1 float64 `json:"f1"`
HitRate float64 `json:"hitRate"`
QuestionCount int `json:"questionCount"`
ValidF1Count int `json:"validF1Count"`
}
// EvalLegacy evaluates using legacy session store (raw history + budget truncation).
func EvalLegacy(
ctx context.Context,
samples []LocomoSample,
legacy *LegacyStore,
budgetTokens int,
) []EvalResult {
results := make([]EvalResult, 0, len(samples))
for si := range samples {
sample := &samples[si]
history := legacy.GetHistory(sample.SampleID)
// Convert messages to content strings
allContent := make([]string, 0, len(history))
for _, msg := range history {
allContent = append(allContent, msg.Content)
}
qaResults := make([]QAResult, 0, len(sample.QA))
for qi := range sample.QA {
qa := &sample.QA[qi]
// Budget truncate the full history
truncated, _ := BudgetTruncate(allContent, budgetTokens)
context := StringListToContent(truncated)
f1 := TokenOverlapF1(context, qa.AnswerString())
hitRate := RecallHitRate(qa.Evidence, sample, context)
qaResults = append(qaResults, QAResult{
Question: qa.Question,
Category: qa.Category,
GoldAnswer: qa.AnswerString(),
TokenF1: f1,
HitRate: hitRate,
})
}
results = append(results, EvalResult{
Mode: "legacy",
SampleID: sample.SampleID,
QAResults: qaResults,
Agg: aggregateMetrics(qaResults),
})
}
return results
}
// EvalSeahorse evaluates using seahorse short memory (per-keyword search + expand).
func EvalSeahorse(
ctx context.Context,
samples []LocomoSample,
ir *SeahorseIngestResult,
budgetTokens int,
) []EvalResult {
store := ir.Engine.GetRetrieval().Store()
retrieval := ir.Engine.GetRetrieval()
results := make([]EvalResult, 0, len(samples))
for si := range samples {
sample := &samples[si]
convID, ok := ir.ConvMap[sample.SampleID]
if !ok {
log.Printf("WARN: no conversation ID for sample %s", sample.SampleID)
continue
}
qaResults := make([]QAResult, 0, len(sample.QA))
for qi := range sample.QA {
qa := &sample.QA[qi]
keywords := ExtractKeywords(qa.Question)
// Search each keyword individually and union results,
// tracking best BM25 rank per message for relevance sorting.
bestRank := map[int64]float64{}
for _, kw := range keywords {
searchResults, err := store.SearchMessages(ctx, seahorse.SearchInput{
Pattern: kw,
ConversationID: convID,
Limit: 20,
})
if err != nil {
log.Printf("WARN: search failed for keyword %q: %v", kw, err)
continue
}
for _, sr := range searchResults {
if sr.MessageID > 0 {
if prev, ok := bestRank[sr.MessageID]; !ok || sr.Rank < prev {
bestRank[sr.MessageID] = sr.Rank
}
}
}
}
// Sort messageIDs by rank ascending (best/most-negative first).
// BudgetTruncate walks from the front, keeping best-ranked messages.
// Note: SQLite FTS5 bm25() returns negative values where more
// negative = better match.
messageIDs := make([]int64, 0, len(bestRank))
for id := range bestRank {
messageIDs = append(messageIDs, id)
}
sort.Slice(messageIDs, func(i, j int) bool {
return bestRank[messageIDs[i]] < bestRank[messageIDs[j]]
})
// Expand messages to get full content
var contentParts []string
if len(messageIDs) > 0 {
expandResult, err := retrieval.ExpandMessages(ctx, messageIDs)
if err != nil {
log.Printf("WARN: expand failed for sample %s: %v", sample.SampleID, err)
} else {
for _, msg := range expandResult.Messages {
contentParts = append(contentParts, msg.Content)
}
}
}
if len(contentParts) == 0 {
qaResults = append(qaResults, QAResult{
Question: qa.Question,
Category: qa.Category,
GoldAnswer: qa.AnswerString(),
TokenF1: 0.0,
HitRate: 0.0,
})
continue
}
// Budget truncate (drop worst-ranked)
truncated, _ := BudgetTruncate(contentParts, budgetTokens)
context := StringListToContent(truncated)
f1 := TokenOverlapF1(context, qa.AnswerString())
hitRate := RecallHitRate(qa.Evidence, sample, context)
qaResults = append(qaResults, QAResult{
Question: qa.Question,
Category: qa.Category,
GoldAnswer: qa.AnswerString(),
TokenF1: f1,
HitRate: hitRate,
})
}
results = append(results, EvalResult{
Mode: "seahorse",
SampleID: sample.SampleID,
QAResults: qaResults,
Agg: aggregateMetrics(qaResults),
})
}
return results
}
// aggregateMetrics computes overall and per-category metrics.
func aggregateMetrics(qaResults []QAResult) AggMetrics {
type catAccum struct {
f1Sum float64
f1Count int
hitRateSum float64
hitRateCount int
}
byCatAcc := map[int]*catAccum{}
totalF1 := 0.0
totalHitRate := 0.0
validF1Count := 0
for _, qr := range qaResults {
// Skip sentinel -1.0 scores (LLM API/parse failures) from F1 averaging.
if qr.TokenF1 >= 0 {
totalF1 += qr.TokenF1
validF1Count++
}
totalHitRate += qr.HitRate
acc, ok := byCatAcc[qr.Category]
if !ok {
acc = &catAccum{}
byCatAcc[qr.Category] = acc
}
if qr.TokenF1 >= 0 {
acc.f1Sum += qr.TokenF1
acc.f1Count++
}
acc.hitRateSum += qr.HitRate
acc.hitRateCount++
}
nHit := len(qaResults)
if nHit == 0 {
nHit = 1
}
byCat := map[int]*CatMetrics{}
for cat, acc := range byCatAcc {
cm := &CatMetrics{
QuestionCount: acc.hitRateCount,
ValidF1Count: acc.f1Count,
}
if acc.f1Count > 0 {
cm.F1 = acc.f1Sum / float64(acc.f1Count)
}
if acc.hitRateCount > 0 {
cm.HitRate = acc.hitRateSum / float64(acc.hitRateCount)
}
byCat[cat] = cm
}
var overallF1 float64
if validF1Count > 0 {
overallF1 = totalF1 / float64(validF1Count)
}
return AggMetrics{
OverallF1: overallF1,
OverallHitRate: totalHitRate / float64(nHit),
ByCategory: byCat,
TotalQuestions: len(qaResults),
ValidF1Count: validF1Count,
}
}
// SaveResults writes per-sample eval results to JSON files.
func SaveResults(results []EvalResult, outDir string) error {
if err := os.MkdirAll(outDir, 0o755); err != nil {
return fmt.Errorf("create output dir: %w", err)
}
for _, r := range results {
path := filepath.Join(outDir, fmt.Sprintf("eval_%s_%s.json", r.Mode, r.SampleID))
data, err := json.MarshalIndent(r, "", " ")
if err != nil {
return fmt.Errorf("marshal result: %w", err)
}
if err := os.WriteFile(path, data, 0o644); err != nil {
return fmt.Errorf("write result: %w", err)
}
}
return nil
}
// SaveAggregated writes a combined results.json with all modes.
func SaveAggregated(results []EvalResult, outDir string) error {
byMode := map[string][]EvalResult{}
for _, r := range results {
byMode[r.Mode] = append(byMode[r.Mode], r)
}
aggMap := map[string]AggMetrics{}
for mode, modeResults := range byMode {
aggMap[mode] = computeModeAgg(modeResults)
}
data, err := json.MarshalIndent(aggMap, "", " ")
if err != nil {
return err
}
return os.WriteFile(filepath.Join(outDir, "results.json"), data, 0o644)
}
// computeModeAgg aggregates results for a single mode using weighted averaging
// (weighted by question count per sample). All modes must have the same Mode field.
func computeModeAgg(results []EvalResult) AggMetrics {
agg := AggMetrics{ByCategory: map[int]*CatMetrics{}}
for _, r := range results {
// Backward compat: old eval JSON (token mode) without ValidF1Count → use TotalQuestions.
// LLM modes may legitimately have ValidF1Count==0 (all failures).
vf1 := r.Agg.ValidF1Count
if vf1 == 0 && r.Agg.TotalQuestions > 0 && !strings.HasSuffix(r.Mode, "-llm") {
vf1 = r.Agg.TotalQuestions
}
agg.OverallF1 += r.Agg.OverallF1 * float64(vf1)
agg.OverallHitRate += r.Agg.OverallHitRate * float64(r.Agg.TotalQuestions)
agg.TotalQuestions += r.Agg.TotalQuestions
agg.ValidF1Count += vf1
for cat, cm := range r.Agg.ByCategory {
existing, ok := agg.ByCategory[cat]
if !ok {
existing = &CatMetrics{}
agg.ByCategory[cat] = existing
}
cvf1 := cm.ValidF1Count
if cvf1 == 0 && cm.QuestionCount > 0 && !strings.HasSuffix(r.Mode, "-llm") {
cvf1 = cm.QuestionCount
}
existing.F1 += cm.F1 * float64(cvf1)
existing.HitRate += cm.HitRate * float64(cm.QuestionCount)
existing.QuestionCount += cm.QuestionCount
existing.ValidF1Count += cvf1
}
}
if agg.ValidF1Count > 0 {
agg.OverallF1 /= float64(agg.ValidF1Count)
}
if agg.TotalQuestions > 0 {
agg.OverallHitRate /= float64(agg.TotalQuestions)
}
for _, cat := range agg.ByCategory {
if cat.ValidF1Count > 0 {
cat.F1 /= float64(cat.ValidF1Count)
}
if cat.QuestionCount > 0 {
cat.HitRate /= float64(cat.QuestionCount)
}
}
return agg
}
// printSection prints a single comparison table section.
func printSection(title string, results []EvalResult) {
fmt.Printf("\n--- %s ---\n", title)
byMode := map[string][]EvalResult{}
for _, r := range results {
byMode[r.Mode] = append(byMode[r.Mode], r)
}
modes := map[string]AggMetrics{}
for mode, modeResults := range byMode {
modes[mode] = computeModeAgg(modeResults)
}
modeKeys := make([]string, 0, len(modes))
for k := range modes {
modeKeys = append(modeKeys, k)
}
sort.Strings(modeKeys)
// Collect all category keys across modes
catSet := map[int]bool{}
for _, agg := range modes {
for cat := range agg.ByCategory {
catSet[cat] = true
}
}
cats := make([]int, 0, len(catSet))
for cat := range catSet {
cats = append(cats, cat)
}
sort.Ints(cats)
fmt.Printf("%-10s %-8s %-8s", "Mode", "HitRate", "F1")
for _, cat := range cats {
fmt.Printf(" %-7s", fmt.Sprintf("C%d", cat))
}
fmt.Println()
fmt.Println(strings.Repeat("-", 10+8+8+7*len(cats)+8))
for _, mode := range modeKeys {
agg := modes[mode]
fmt.Printf("%-10s %-8.4f %-8.4f", mode, agg.OverallHitRate, agg.OverallF1)
for _, cat := range cats {
if cm, ok := agg.ByCategory[cat]; ok {
fmt.Printf(" %-7.4f", cm.HitRate)
} else {
fmt.Printf(" %-7s", "N/A")
}
}
fmt.Println()
}
}
// PrintComparison outputs a human-readable comparison table to stdout.
func PrintComparison(results []EvalResult, llmResults []EvalResult) {
if len(results) > 0 {
printSection("No LLM generation", results)
}
if len(llmResults) > 0 {
printSection("With LLM", llmResults)
}
}
+346
View File
@@ -0,0 +1,346 @@
package main
import (
"context"
"fmt"
"log"
"regexp"
"sort"
"strconv"
"strings"
"sync"
"github.com/sipeed/picoclaw/pkg/seahorse"
)
const answerSystemPrompt = `You are a helpful assistant. Given conversation context, answer the question concisely and accurately. If the answer is not in the context, say "I don't know". Answer in 1-3 sentences maximum.`
const judgeSystemPrompt = `You are an impartial judge evaluating answer quality.
Compare the candidate answer against the reference answer.
Consider semantic equivalence — different wording expressing the same meaning should score high.
Output ONLY a single integer score from 1 to 5:
1 = completely wrong or irrelevant
2 = partially related but mostly incorrect
3 = partially correct, missing key details
4 = mostly correct with minor omissions
5 = fully correct, semantically equivalent
Output ONLY the number, nothing else.`
// generateAnswer asks the LLM to answer a question given retrieved context.
func generateAnswer(ctx context.Context, client *LLMClient, contextText, question string) (string, error) {
// Truncate context to avoid exceeding model limits while preserving valid UTF-8.
contextRunes := []rune(contextText)
if len(contextRunes) > 6000 {
contextText = string(contextRunes[:6000]) + "\n... [truncated]"
}
userPrompt := fmt.Sprintf("## Conversation Context\n\n%s\n\n## Question\n\n%s", contextText, question)
return client.Complete(ctx, answerSystemPrompt, userPrompt)
}
// scoreRe matches the first standalone integer 1-5 in the judge response.
var scoreRe = regexp.MustCompile(`\b([1-5])\b`)
// judgeAnswer asks the LLM to score the candidate answer vs the gold answer.
// Returns a score from 0.0 to 1.0, or -1.0 on parse failure.
func judgeAnswer(
ctx context.Context,
judgeClient *LLMClient,
question, goldAnswer, candidateAnswer string,
) (float64, error) {
userPrompt := fmt.Sprintf(
"Question: %s\n\nReference Answer: %s\n\nCandidate Answer: %s\n\nScore:",
question, goldAnswer, candidateAnswer,
)
response, err := judgeClient.Complete(ctx, judgeSystemPrompt, userPrompt)
if err != nil {
return -1.0, err
}
response = strings.TrimSpace(response)
if m := scoreRe.FindStringSubmatch(response); len(m) == 2 {
score, _ := strconv.Atoi(m[1])
return float64(score-1) / 4.0, nil // Normalize 1-5 to 0.0-1.0
}
log.Printf("WARNING: could not parse judge score from: %q, returning -1", response)
return -1.0, nil
}
// qaWork describes one QA evaluation unit.
type qaWork struct {
sampleID string
qaIndex int
globalIndex int
totalQA int
qa *LocomoQA
contextText string
sample *LocomoSample
}
// qaResult collects one QA evaluation output.
type qaResultOut struct {
index int // position in the flat QA list for ordering
result QAResult
answer string
score float64
}
// evalQAWorker processes a single QA item: generate answer + judge score.
func evalQAWorker(
ctx context.Context,
w qaWork,
answerClient, judgeClient *LLMClient,
logPrefix string,
) qaResultOut {
llmAnswer, err := generateAnswer(ctx, answerClient, w.contextText, w.qa.Question)
if err != nil {
log.Printf("WARN: LLM generation failed for sample %s Q%d: %v", w.sampleID, w.qaIndex, err)
llmAnswer = ""
}
score := -1.0
if llmAnswer != "" {
score, err = judgeAnswer(ctx, judgeClient, w.qa.Question, w.qa.AnswerString(), llmAnswer)
if err != nil {
log.Printf("WARN: LLM judge failed for sample %s Q%d: %v", w.sampleID, w.qaIndex, err)
}
}
hitRate := RecallHitRate(w.qa.Evidence, w.sample, w.contextText)
log.Printf("[%s] sample=%s q=%d/%d score=%.2f answer=%q",
logPrefix, w.sampleID, w.globalIndex, w.totalQA, score, truncateStr(llmAnswer, 80))
return qaResultOut{
index: w.globalIndex,
result: QAResult{
Question: w.qa.Question,
Category: w.qa.Category,
GoldAnswer: w.qa.AnswerString(),
TokenF1: score,
HitRate: hitRate,
},
answer: llmAnswer,
score: score,
}
}
// EvalLegacyLLM evaluates legacy store using LLM generation + LLM-as-Judge.
func EvalLegacyLLM(
ctx context.Context,
samples []LocomoSample,
legacy *LegacyStore,
budgetTokens int,
answerClient, judgeClient *LLMClient,
concurrency int,
) []EvalResult {
if concurrency < 1 {
concurrency = 1
}
totalQA := countTotalQA(samples)
results := make([]EvalResult, 0, len(samples))
for si := range samples {
sample := &samples[si]
history := legacy.GetHistory(sample.SampleID)
allContent := make([]string, 0, len(history))
for _, msg := range history {
allContent = append(allContent, msg.Content)
}
truncated, _ := BudgetTruncate(allContent, budgetTokens)
contextText := StringListToContent(truncated)
qaResults := make([]QAResult, len(sample.QA))
if concurrency <= 1 {
for qi := range sample.QA {
out := evalQAWorker(ctx, qaWork{
sampleID: sample.SampleID, qaIndex: qi,
globalIndex: si*len(sample.QA) + qi + 1, totalQA: totalQA,
qa: &sample.QA[qi], contextText: contextText, sample: sample,
}, answerClient, judgeClient, "legacy-llm")
qaResults[qi] = out.result
}
} else {
sem := make(chan struct{}, concurrency)
var wg sync.WaitGroup
for qi := range sample.QA {
wg.Add(1)
go func() {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
out := evalQAWorker(ctx, qaWork{
sampleID: sample.SampleID, qaIndex: qi,
globalIndex: si*len(sample.QA) + qi + 1, totalQA: totalQA,
qa: &sample.QA[qi], contextText: contextText, sample: sample,
}, answerClient, judgeClient, "legacy-llm")
qaResults[qi] = out.result // safe: each goroutine writes distinct index
}()
}
wg.Wait()
}
results = append(results, EvalResult{
Mode: "legacy-llm",
SampleID: sample.SampleID,
QAResults: qaResults,
Agg: aggregateMetrics(qaResults),
})
}
return results
}
// buildSeahorseContext retrieves context for a seahorse QA item.
func buildSeahorseContext(
ctx context.Context,
ir *SeahorseIngestResult,
sample *LocomoSample,
qa *LocomoQA,
budgetTokens int,
) string {
store := ir.Engine.GetRetrieval().Store()
retrieval := ir.Engine.GetRetrieval()
convID := ir.ConvMap[sample.SampleID]
keywords := ExtractKeywords(qa.Question)
bestRank := map[int64]float64{}
for _, kw := range keywords {
searchResults, err := store.SearchMessages(ctx, seahorse.SearchInput{
Pattern: kw,
ConversationID: convID,
Limit: 20,
})
if err != nil {
continue
}
for _, sr := range searchResults {
if sr.MessageID > 0 {
if prev, ok := bestRank[sr.MessageID]; !ok || sr.Rank < prev {
bestRank[sr.MessageID] = sr.Rank
}
}
}
}
messageIDs := make([]int64, 0, len(bestRank))
for id := range bestRank {
messageIDs = append(messageIDs, id)
}
sort.Slice(messageIDs, func(i, j int) bool {
return bestRank[messageIDs[i]] < bestRank[messageIDs[j]]
})
var contentParts []string
if len(messageIDs) > 0 {
expandResult, err := retrieval.ExpandMessages(ctx, messageIDs)
if err == nil {
for _, msg := range expandResult.Messages {
contentParts = append(contentParts, msg.Content)
}
}
}
if len(contentParts) == 0 {
return ""
}
truncated, _ := BudgetTruncate(contentParts, budgetTokens)
return StringListToContent(truncated)
}
// EvalSeahorseLLM evaluates seahorse retrieval using LLM generation + LLM-as-Judge.
func EvalSeahorseLLM(
ctx context.Context,
samples []LocomoSample,
ir *SeahorseIngestResult,
budgetTokens int,
answerClient, judgeClient *LLMClient,
concurrency int,
) []EvalResult {
if concurrency < 1 {
concurrency = 1
}
totalQA := countTotalQA(samples)
results := make([]EvalResult, 0, len(samples))
for si := range samples {
sample := &samples[si]
if _, ok := ir.ConvMap[sample.SampleID]; !ok {
log.Printf("WARN: no conversation ID for sample %s", sample.SampleID)
continue
}
qaResults := make([]QAResult, len(sample.QA))
evalOne := func(qi int) {
qa := &sample.QA[qi]
contextText := buildSeahorseContext(ctx, ir, sample, qa, budgetTokens)
if contextText == "" {
qaResults[qi] = QAResult{
Question: qa.Question,
Category: qa.Category,
GoldAnswer: qa.AnswerString(),
TokenF1: 0.0,
HitRate: 0.0,
}
log.Printf("[seahorse-llm] sample=%s q=%d/%d score=0.00 answer=(no context)",
sample.SampleID, si*len(sample.QA)+qi+1, totalQA)
return
}
out := evalQAWorker(ctx, qaWork{
sampleID: sample.SampleID, qaIndex: qi,
globalIndex: si*len(sample.QA) + qi + 1, totalQA: totalQA,
qa: qa, contextText: contextText, sample: sample,
}, answerClient, judgeClient, "seahorse-llm")
qaResults[qi] = out.result
}
if concurrency <= 1 {
for qi := range sample.QA {
evalOne(qi)
}
} else {
sem := make(chan struct{}, concurrency)
var wg sync.WaitGroup
for qi := range sample.QA {
wg.Add(1)
go func() {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
evalOne(qi)
}()
}
wg.Wait()
}
results = append(results, EvalResult{
Mode: "seahorse-llm",
SampleID: sample.SampleID,
QAResults: qaResults,
Agg: aggregateMetrics(qaResults),
})
}
return results
}
func countTotalQA(samples []LocomoSample) int {
n := 0
for i := range samples {
n += len(samples[i].QA)
}
return n
}
func truncateStr(s string, maxLen int) string {
s = strings.ReplaceAll(s, "\n", " ")
runes := []rune(s)
if len(runes) > maxLen {
return string(runes[:maxLen]) + "..."
}
return s
}
+182
View File
@@ -0,0 +1,182 @@
package main
import (
"math"
"testing"
)
func TestComputeModeAggAllCategories(t *testing.T) {
results := []EvalResult{
{
Mode: "test",
SampleID: "s1",
QAResults: []QAResult{
{Category: 1, TokenF1: 0.5, HitRate: 0.8},
{Category: 2, TokenF1: 0.3, HitRate: 0.6},
{Category: 3, TokenF1: 0.1, HitRate: 0.4},
{Category: 4, TokenF1: 0.7, HitRate: 0.9},
{Category: 5, TokenF1: 0.2, HitRate: 0.1},
},
},
}
for i := range results {
results[i].Agg = aggregateMetrics(results[i].QAResults)
}
got := computeModeAgg(results)
// Should have all 5 categories
for cat := 1; cat <= 5; cat++ {
cm, ok := got.ByCategory[cat]
if !ok {
t.Errorf("ByCategory missing category %d", cat)
continue
}
if cm.QuestionCount != 1 {
t.Errorf("ByCategory[%d].QuestionCount = %d, want 1", cat, cm.QuestionCount)
}
}
// Verify specific F1 values per category
wantF1 := map[int]float64{1: 0.5, 2: 0.3, 3: 0.1, 4: 0.7, 5: 0.2}
for cat, want := range wantF1 {
if cm, ok := got.ByCategory[cat]; ok {
if math.Abs(cm.F1-want) > 1e-9 {
t.Errorf("ByCategory[%d].F1 = %.4f, want %.4f", cat, cm.F1, want)
}
}
}
}
func TestComputeModeAgg(t *testing.T) {
// Two samples with different question counts:
// sample-a: 2 questions, F1 = [0.4, 0.6] → avg 0.5
// sample-b: 8 questions, F1 = [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1] → avg 0.1
//
// Unweighted (PrintComparison bug): (0.5 + 0.1) / 2 = 0.3
// Weighted (correct): (0.4+0.6 + 0.1*8) / 10 = 1.8 / 10 = 0.18
results := []EvalResult{
{
Mode: "test",
SampleID: "sample-a",
QAResults: []QAResult{
{TokenF1: 0.4, HitRate: 0.5},
{TokenF1: 0.6, HitRate: 0.7},
},
},
{
Mode: "test",
SampleID: "sample-b",
QAResults: []QAResult{
{TokenF1: 0.1, HitRate: 0.2},
{TokenF1: 0.1, HitRate: 0.2},
{TokenF1: 0.1, HitRate: 0.2},
{TokenF1: 0.1, HitRate: 0.2},
{TokenF1: 0.1, HitRate: 0.2},
{TokenF1: 0.1, HitRate: 0.2},
{TokenF1: 0.1, HitRate: 0.2},
{TokenF1: 0.1, HitRate: 0.2},
},
},
}
// Compute per-sample aggregates
for i := range results {
results[i].Agg = aggregateMetrics(results[i].QAResults)
}
got := computeModeAgg(results)
// Weighted: (0.4+0.6+0.1*8) / 10 = 1.8/10 = 0.18
wantF1 := 0.18
if math.Abs(got.OverallF1-wantF1) > 1e-9 {
t.Errorf("OverallF1 = %.6f, want %.6f (weighted average)", got.OverallF1, wantF1)
}
// Weighted: (0.5+0.7+0.2*8) / 10 = 2.8/10 = 0.28
wantRecall := 0.28
if math.Abs(got.OverallHitRate-wantRecall) > 1e-9 {
t.Errorf("OverallHitRate = %.6f, want %.6f (weighted average)", got.OverallHitRate, wantRecall)
}
if got.TotalQuestions != 10 {
t.Errorf("TotalQuestions = %d, want 10", got.TotalQuestions)
}
}
func TestAggregateMetricsSentinel(t *testing.T) {
qa := []QAResult{
{Category: 1, TokenF1: 0.8, HitRate: 0.5},
{Category: 1, TokenF1: -1.0, HitRate: 0.3},
{Category: 1, TokenF1: 0.4, HitRate: 0.7},
}
agg := aggregateMetrics(qa)
if agg.ValidF1Count != 2 {
t.Errorf("ValidF1Count = %d, want 2", agg.ValidF1Count)
}
if agg.TotalQuestions != 3 {
t.Errorf("TotalQuestions = %d, want 3", agg.TotalQuestions)
}
wantF1 := (0.8 + 0.4) / 2.0
if math.Abs(agg.OverallF1-wantF1) > 1e-9 {
t.Errorf("OverallF1 = %.6f, want %.6f", agg.OverallF1, wantF1)
}
wantHR := (0.5 + 0.3 + 0.7) / 3.0
if math.Abs(agg.OverallHitRate-wantHR) > 1e-9 {
t.Errorf("OverallHitRate = %.6f, want %.6f", agg.OverallHitRate, wantHR)
}
}
func TestAggregateMetricsAllSentinel(t *testing.T) {
qa := []QAResult{
{Category: 1, TokenF1: -1.0, HitRate: 0.5},
{Category: 1, TokenF1: -1.0, HitRate: 0.3},
}
agg := aggregateMetrics(qa)
if agg.ValidF1Count != 0 {
t.Errorf("ValidF1Count = %d, want 0", agg.ValidF1Count)
}
if agg.OverallF1 != 0 {
t.Errorf("OverallF1 = %.6f, want 0", agg.OverallF1)
}
}
func TestComputeModeAggSentinelWeighting(t *testing.T) {
results := []EvalResult{
{
Mode: "test",
SampleID: "s1",
QAResults: []QAResult{
{Category: 1, TokenF1: 0.8, HitRate: 0.5},
{Category: 1, TokenF1: -1.0, HitRate: 0.3},
},
},
{
Mode: "test",
SampleID: "s2",
QAResults: []QAResult{
{Category: 1, TokenF1: 0.4, HitRate: 0.6},
{Category: 1, TokenF1: 0.6, HitRate: 0.8},
},
},
}
for i := range results {
results[i].Agg = aggregateMetrics(results[i].QAResults)
}
got := computeModeAgg(results)
// s1: ValidF1Count=1, F1=0.8; s2: ValidF1Count=2, F1=0.5
// Weighted: (0.8*1 + 0.5*2) / 3 = 1.8/3 = 0.6
wantF1 := 0.6
if math.Abs(got.OverallF1-wantF1) > 1e-9 {
t.Errorf("OverallF1 = %.6f, want %.6f", got.OverallF1, wantF1)
}
if got.ValidF1Count != 3 {
t.Errorf("ValidF1Count = %d, want 3", got.ValidF1Count)
}
if got.TotalQuestions != 4 {
t.Errorf("TotalQuestions = %d, want 4", got.TotalQuestions)
}
}
+85
View File
@@ -0,0 +1,85 @@
package main
import (
"context"
"fmt"
"log"
"github.com/sipeed/picoclaw/pkg/seahorse"
)
// ConvMap stores the mapping from sampleID to seahorse ConversationID.
type ConvMap map[string]int64
// SeahorseIngestResult holds the results of ingesting into seahorse.
type SeahorseIngestResult struct {
Engine *seahorse.Engine
ConvMap ConvMap // sampleID → conversationID
}
// IngestSeahorse loads all LOCOMO samples into a seahorse Engine.
// Returns the engine and a mapping from sampleID to conversationID for scoped retrieval.
func IngestSeahorse(ctx context.Context, samples []LocomoSample, dbPath string) (*SeahorseIngestResult, error) {
noopFn := func(ctx context.Context, prompt string, opts seahorse.CompleteOptions) (string, error) {
return "", nil
}
engine, err := seahorse.NewEngine(seahorse.Config{
DBPath: dbPath,
}, noopFn)
if err != nil {
return nil, fmt.Errorf("create seahorse engine: %w", err)
}
store := engine.GetRetrieval().Store()
convMap := make(ConvMap)
for si := range samples {
sample := &samples[si]
sessionKey := "locomo-" + sample.SampleID
// Check if conversation already exists (idempotent)
existing, _ := store.GetConversationBySessionKey(ctx, sessionKey)
if existing != nil {
convMap[sample.SampleID] = existing.ConversationID
log.Printf("Skipping existing sample %s: convID=%d", sample.SampleID, existing.ConversationID)
continue
}
turns := GetTurns(sample)
// Convert turns to seahorse messages
msgs := make([]seahorse.Message, 0, len(turns))
for _, turn := range turns {
content := turn.Speaker + ": " + turn.Text
msgs = append(msgs, seahorse.Message{
Role: "user",
Content: content,
TokenCount: len(turn.Text) / 4,
})
}
// Ingest all turns for this sample
_, err := engine.Ingest(ctx, sessionKey, msgs)
if err != nil {
return nil, fmt.Errorf("ingest sample %s: %w", sample.SampleID, err)
}
// Get the conversation ID for scoped retrieval
conv, err := store.GetConversationBySessionKey(ctx, sessionKey)
if err != nil {
return nil, fmt.Errorf("get conversation for %s: %w", sample.SampleID, err)
}
if conv == nil {
return nil, fmt.Errorf("conversation not found for %s after ingest", sample.SampleID)
}
convMap[sample.SampleID] = conv.ConversationID
log.Printf("Ingested sample %s: %d turns, convID=%d", sample.SampleID, len(turns), conv.ConversationID)
}
log.Printf("Seahorse ingestion complete: %d samples, %d conversations", len(samples), len(convMap))
return &SeahorseIngestResult{
Engine: engine,
ConvMap: convMap,
}, nil
}
+79
View File
@@ -0,0 +1,79 @@
package main
import (
"context"
"encoding/json"
"path/filepath"
"testing"
"github.com/sipeed/picoclaw/pkg/seahorse"
)
func TestIngestSeahorseIdempotent(t *testing.T) {
ctx := context.Background()
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "test.db")
// Minimal test data
samples := []LocomoSample{
{
SampleID: "test-1",
Conversation: map[string]json.RawMessage{
"session_1": json.RawMessage(`[
{"speaker":"A","dia_id":"D1:1","text":"hello world this is a test message"},
{"speaker":"B","dia_id":"D1:2","text":"another message for testing purposes"}
]`),
},
},
}
// First ingestion
result1, err := IngestSeahorse(ctx, samples, dbPath)
if err != nil {
t.Fatalf("first ingest failed: %v", err)
}
convCount1 := len(result1.ConvMap)
result1.Engine.Close()
// Second ingestion on same DB — should reuse existing data
result2, err := IngestSeahorse(ctx, samples, dbPath)
if err != nil {
t.Fatalf("second ingest failed: %v", err)
}
defer result2.Engine.Close()
// ConvMap should have same number of entries (no duplicates)
if len(result2.ConvMap) != convCount1 {
t.Errorf("second ingest convMap has %d entries, want %d (same as first)",
len(result2.ConvMap), convCount1)
}
// Verify conversation IDs are the same (reused, not new ones)
for id, cid1 := range result1.ConvMap {
cid2, ok := result2.ConvMap[id]
if !ok {
t.Errorf("sample %s missing from second ConvMap", id)
continue
}
if cid2 != cid1 {
t.Errorf("sample %s: second ingest got convID %d, want %d (reused)", id, cid2, cid1)
}
}
// Verify no duplicate messages by counting
store := result2.Engine.GetRetrieval().Store()
for _, convID := range result2.ConvMap {
msgs, err := store.SearchMessages(ctx, seahorse.SearchInput{
Pattern: "test",
ConversationID: convID,
Limit: 100,
})
if err != nil {
t.Fatalf("search failed: %v", err)
}
// Should find exactly 1 message containing "test" (the first turn)
if len(msgs) > 2 {
t.Errorf("found %d messages for 'test' in conv %d, expected ≤2 (no duplicates)", len(msgs), convID)
}
}
}
+34
View File
@@ -0,0 +1,34 @@
package main
import (
"github.com/sipeed/picoclaw/pkg/providers"
"github.com/sipeed/picoclaw/pkg/session"
)
// LegacyStore wraps session.SessionManager for legacy baseline.
type LegacyStore struct {
sm *session.SessionManager
}
// NewLegacyStore creates a new in-memory session manager.
func NewLegacyStore() *LegacyStore {
return &LegacyStore{
sm: session.NewSessionManager(""),
}
}
// IngestSample loads all turns from a LOCOMO sample into the legacy session store.
func (ls *LegacyStore) IngestSample(sample *LocomoSample) {
sessionKey := "locomo-" + sample.SampleID
turns := GetTurns(sample)
for _, turn := range turns {
content := turn.Speaker + ": " + turn.Text
ls.sm.AddMessage(sessionKey, "user", content)
}
}
// GetHistory returns all messages for a sample's session.
func (ls *LegacyStore) GetHistory(sampleID string) []providers.Message {
sessionKey := "locomo-" + sampleID
return ls.sm.GetHistory(sessionKey)
}
+198
View File
@@ -0,0 +1,198 @@
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strings"
"time"
)
// LLMClient wraps an OpenAI-compatible chat completion endpoint.
type LLMClient struct {
BaseURL string
Model string
APIKey string
NoThinking bool // send chat_template_kwargs to disable thinking (llama.cpp specific)
MaxRetries int // max retry attempts for transient errors (0 = no retry)
Client *http.Client
}
// LLMClientOptions configures the LLM client.
type LLMClientOptions struct {
BaseURL string
Model string
APIKey string
Timeout time.Duration
NoThinking bool
MaxRetries int // max retry attempts (default 3)
}
// NewLLMClient creates a client for an OpenAI-compatible chat completion API.
func NewLLMClient(opts LLMClientOptions) *LLMClient {
if opts.Timeout == 0 {
opts.Timeout = 120 * time.Second
}
maxRetries := opts.MaxRetries
if maxRetries < 0 {
maxRetries = 3
}
return &LLMClient{
BaseURL: strings.TrimRight(opts.BaseURL, "/"),
Model: opts.Model,
APIKey: opts.APIKey,
NoThinking: opts.NoThinking,
MaxRetries: maxRetries,
Client: &http.Client{
Timeout: opts.Timeout,
},
}
}
type chatRequest struct {
Model string `json:"model"`
Messages []chatMessage `json:"messages"`
Temperature float64 `json:"temperature"`
MaxTokens int `json:"max_tokens"`
ChatTemplateKwargs map[string]any `json:"chat_template_kwargs,omitempty"` // llama.cpp
Think *bool `json:"think,omitempty"` // Ollama
Thinking map[string]any `json:"thinking,omitempty"` // GLM (智谱)
}
type chatMessage struct {
Role string `json:"role"`
Content string `json:"content"`
}
type chatResponse struct {
Choices []struct {
Message struct {
Content string `json:"content"`
ReasoningContent string `json:"reasoning_content,omitempty"`
} `json:"message"`
} `json:"choices"`
}
// Complete sends a chat completion request and returns the assistant's reply.
func (c *LLMClient) Complete(ctx context.Context, systemPrompt, userPrompt string) (string, error) {
sysContent := systemPrompt
if c.NoThinking && sysContent != "" {
// Prepend /no_think tag — works with Ollama /v1 endpoint and
// Qwen chat templates where the JSON think field is ignored.
sysContent = "/no_think\n" + sysContent
}
messages := []chatMessage{}
if sysContent != "" {
messages = append(messages, chatMessage{Role: "system", Content: sysContent})
}
messages = append(messages, chatMessage{Role: "user", Content: userPrompt})
body := chatRequest{
Model: c.Model,
Messages: messages,
Temperature: 0.1,
MaxTokens: 512,
}
if c.NoThinking {
// llama.cpp: chat_template_kwargs
body.ChatTemplateKwargs = map[string]any{
"enable_thinking": false,
}
// Ollama (0.9+): think field
thinkFalse := false
body.Think = &thinkFalse
// GLM (智谱): thinking field
body.Thinking = map[string]any{
"type": "disabled",
}
}
jsonBody, err := json.Marshal(body)
if err != nil {
return "", fmt.Errorf("marshal request: %w", err)
}
endpoint := strings.TrimRight(c.BaseURL, "/") + "/chat/completions"
req, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewReader(jsonBody))
if err != nil {
return "", fmt.Errorf("create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
if c.APIKey != "" {
req.Header.Set("Authorization", "Bearer "+c.APIKey)
}
var respBody []byte
var lastErr error
for attempt := 0; attempt <= c.MaxRetries; attempt++ {
if attempt > 0 {
backoff := time.Duration(1<<(attempt-1)) * time.Second // 1s, 2s, 4s, ...
log.Printf("LLM retry %d/%d after %v: %v", attempt, c.MaxRetries, backoff, lastErr)
select {
case <-ctx.Done():
return "", ctx.Err()
case <-time.After(backoff):
}
// Rebuild request (body reader is consumed)
req, err = http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewReader(jsonBody))
if err != nil {
return "", fmt.Errorf("create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
if c.APIKey != "" {
req.Header.Set("Authorization", "Bearer "+c.APIKey)
}
}
var resp *http.Response
resp, lastErr = c.Client.Do(req)
if lastErr != nil {
continue // network/timeout error → retry
}
respBody, lastErr = io.ReadAll(resp.Body)
resp.Body.Close()
if lastErr != nil {
continue
}
if resp.StatusCode == 429 || resp.StatusCode >= 500 {
lastErr = fmt.Errorf("API error %d: %s", resp.StatusCode, string(respBody))
continue // rate limit or server error → retry
}
if resp.StatusCode != 200 {
return "", fmt.Errorf("API error %d: %s", resp.StatusCode, string(respBody))
}
lastErr = nil
break
}
if lastErr != nil {
return "", fmt.Errorf("after %d retries: %w", c.MaxRetries, lastErr)
}
var chatResp chatResponse
if err := json.Unmarshal(respBody, &chatResp); err != nil {
return "", fmt.Errorf("parse response: %w", err)
}
if len(chatResp.Choices) == 0 {
return "", fmt.Errorf("no choices in response")
}
content := strings.TrimSpace(chatResp.Choices[0].Message.Content)
// Strip any residual <think>...</think> blocks
if idx := strings.Index(content, "</think>"); idx >= 0 {
content = strings.TrimSpace(content[idx+len("</think>"):])
}
// Fallback: GLM/DeepSeek put thinking output in reasoning_content when thinking is enabled
if content == "" && chatResp.Choices[0].Message.ReasoningContent != "" {
content = strings.TrimSpace(chatResp.Choices[0].Message.ReasoningContent)
}
if content == "" {
return "", fmt.Errorf("empty LLM response")
}
return content, nil
}
+142
View File
@@ -0,0 +1,142 @@
package main
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
)
// LocomoSample represents one conversation sample from the LOCOMO dataset.
type LocomoSample struct {
SampleID string `json:"sample_id"`
Conversation map[string]json.RawMessage `json:"conversation"`
QA []LocomoQA `json:"qa"`
}
// LocomoTurn represents a single turn in a conversation.
type LocomoTurn struct {
Speaker string `json:"speaker"`
DiaID string `json:"dia_id"`
Text string `json:"text"`
}
// LocomoQA represents a question-answer pair with evidence.
type LocomoQA struct {
Question string `json:"question"`
Answer json.RawMessage `json:"answer"` // can be string or int (category 1-4)
AdversarialAnswer string `json:"adversarial_answer"` // category 5 only
Evidence []string `json:"evidence"`
Category int `json:"category"` // 1=single-hop, 2=multi-hop, 3=open-ended, 5=adversarial
}
// AnswerString returns the answer as a string, handling both string and int types.
func (qa *LocomoQA) AnswerString() string {
// Prefer answer field (category 1-4)
if len(qa.Answer) > 0 {
var s string
if err := json.Unmarshal(qa.Answer, &s); err == nil {
return s
}
var n json.Number
if err := json.Unmarshal(qa.Answer, &n); err == nil {
return n.String()
}
return strings.Trim(string(qa.Answer), `"`)
}
// Fallback to adversarial_answer (category 5)
return qa.AdversarialAnswer
}
// LoadDataset reads all JSON files from dataDir and returns parsed samples.
func LoadDataset(dataDir string) ([]LocomoSample, error) {
entries, err := os.ReadDir(dataDir)
if err != nil {
return nil, fmt.Errorf("read data dir %s: %w", dataDir, err)
}
var samples []LocomoSample
for _, entry := range entries {
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".json") {
path := filepath.Join(dataDir, entry.Name())
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read file %s: %w", path, err)
}
var batch []LocomoSample
if err := json.Unmarshal(data, &batch); err != nil {
return nil, fmt.Errorf("parse file %s: %w", path, err)
}
samples = append(samples, batch...)
}
}
return samples, nil
}
// GetSessionNames returns sorted session keys (session_1, session_2, ...) from conversation.
func GetSessionNames(conv map[string]json.RawMessage) []string {
var names []string
for k := range conv {
if strings.HasPrefix(k, "session_") && !strings.Contains(k, "_date_time") {
names = append(names, k)
}
}
sort.Slice(names, func(i, j int) bool {
ni := sessionNum(names[i])
nj := sessionNum(names[j])
return ni < nj
})
return names
}
func sessionNum(key string) int {
// "session_1" → 1, "session_10" → 10
parts := strings.SplitN(key, "_", 2)
if len(parts) < 2 {
return 0
}
n, _ := strconv.Atoi(parts[1])
return n
}
// GetTurns flattens all sessions' turns in chronological order.
func GetTurns(sample *LocomoSample) []LocomoTurn {
names := GetSessionNames(sample.Conversation)
var all []LocomoTurn
for _, name := range names {
raw, ok := sample.Conversation[name]
if !ok {
continue
}
var turns []LocomoTurn
if err := json.Unmarshal(raw, &turns); err != nil {
log.Printf("WARNING: unmarshal failed for session %q in sample %s: %v", name, sample.SampleID, err)
continue
}
all = append(all, turns...)
}
return all
}
// GetTurnByDiaID finds a specific turn by dia_id (e.g. "D1:3").
func GetTurnByDiaID(sample *LocomoSample, diaID string) *LocomoTurn {
turns := GetTurns(sample)
for i := range turns {
if turns[i].DiaID == diaID {
return &turns[i]
}
}
return nil
}
// GetSpeakers returns the two speaker names from conversation metadata.
func GetSpeakers(conv map[string]json.RawMessage) (string, string) {
var a, b string
json.Unmarshal(conv["speaker_a"], &a)
json.Unmarshal(conv["speaker_b"], &b)
return a, b
}
+67
View File
@@ -0,0 +1,67 @@
package main
import (
"encoding/json"
"testing"
)
func TestAnswerString(t *testing.T) {
tests := []struct {
name string
json string
want string
}{
{
"string answer",
`{"question":"Q","answer":"Paris","evidence":[],"category":1}`,
"Paris",
},
{
"int answer",
`{"question":"Q","answer":42,"evidence":[],"category":1}`,
"42",
},
{
"adversarial answer (category 5)",
`{"question":"Q","evidence":[],"category":5,"adversarial_answer":"self-care is important"}`,
"self-care is important",
},
{
"both answer and adversarial_answer present",
`{"question":"Q","answer":"normal","evidence":[],"category":5,"adversarial_answer":"adversarial"}`,
"normal",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var qa LocomoQA
if err := json.Unmarshal([]byte(tt.json), &qa); err != nil {
t.Fatalf("unmarshal: %v", err)
}
got := qa.AnswerString()
if got != tt.want {
t.Errorf("AnswerString() = %q, want %q", got, tt.want)
}
})
}
}
func TestGetSessionNames(t *testing.T) {
conv := map[string]json.RawMessage{
"session_2": {},
"session_1": {},
"session_10": {},
"session_1_date_time": {},
"speaker_a": {},
}
names := GetSessionNames(conv)
want := []string{"session_1", "session_2", "session_10"}
if len(names) != len(want) {
t.Fatalf("got %v, want %v", names, want)
}
for i, n := range names {
if n != want[i] {
t.Errorf("names[%d] = %q, want %q", i, n, want[i])
}
}
}
+361
View File
@@ -0,0 +1,361 @@
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"time"
"github.com/spf13/cobra"
"github.com/sipeed/picoclaw/pkg/logger"
)
var (
flagData string
flagOut string
flagMode string
flagBudget int
flagEvalMode string
flagAPIBase string
flagAPIKey string
flagModel string
flagNoThinking bool
flagLimit int
flagTimeout int
flagRetries int
flagJudgeModel string
flagJudgeAPIBase string
flagJudgeAPIKey string
flagConcurrency int
)
func main() {
// Suppress seahorse INFO logs during benchmark
logger.SetLevel(logger.WARN)
rootCmd := &cobra.Command{
Use: "membench",
Short: "Memory benchmark tool for picoclaw",
}
ingestCmd := &cobra.Command{
Use: "ingest",
Short: "Load LOCOMO data into storage backends",
RunE: runIngest,
}
ingestCmd.Flags().StringVar(&flagData, "data", "", "LOCOMO dataset directory (required)")
ingestCmd.Flags().StringVar(&flagOut, "out", "./bench-out", "output working directory")
ingestCmd.Flags().StringVar(&flagMode, "mode", "all", "modes to ingest: legacy, seahorse, or all")
evalCmd := &cobra.Command{
Use: "eval",
Short: "Run QA evaluation against ingested data",
RunE: runEval,
}
evalCmd.Flags().StringVar(&flagData, "data", "", "LOCOMO dataset directory (required)")
evalCmd.Flags().StringVar(&flagOut, "out", "./bench-out", "output working directory")
evalCmd.Flags().StringVar(&flagMode, "mode", "all", "modes to evaluate: legacy, seahorse, or all")
evalCmd.Flags().IntVar(&flagBudget, "budget", 4000, "token budget for retrieval")
evalCmd.Flags().
StringVar(&flagEvalMode, "eval-mode", "token", "evaluation mode: token (direct match) or llm (LLM-as-Judge)")
evalCmd.Flags().
StringVar(&flagAPIBase, "api-base", "", "API base URL with version path, e.g. http://host/v1 (default: http://127.0.0.1:8080/v1, env: MEMBENCH_API_BASE)")
evalCmd.Flags().StringVar(&flagAPIKey, "api-key", "", "API key for the LLM endpoint (env: MEMBENCH_API_KEY)")
evalCmd.Flags().StringVar(&flagModel, "model", "", "model name for LLM eval (env: MEMBENCH_MODEL)")
evalCmd.Flags().
BoolVar(&flagNoThinking, "no-thinking", false, "disable thinking mode via chat_template_kwargs (llama.cpp + Qwen)")
evalCmd.Flags().IntVar(&flagLimit, "limit", 0, "max QA questions per sample (0 = all)")
evalCmd.Flags().IntVar(&flagTimeout, "timeout", 120, "HTTP timeout in seconds for LLM requests")
evalCmd.Flags().IntVar(&flagRetries, "retries", 3, "max retry attempts for transient LLM errors (timeout/5xx/429)")
evalCmd.Flags().StringVar(&flagJudgeModel, "judge-model", "", "model for judge scoring (defaults to --model)")
evalCmd.Flags().
StringVar(&flagJudgeAPIBase, "judge-api-base", "", "API base URL for judge model (defaults to --api-base)")
evalCmd.Flags().StringVar(&flagJudgeAPIKey, "judge-api-key", "", "API key for judge model (defaults to --api-key)")
evalCmd.Flags().IntVar(&flagConcurrency, "concurrency", 1, "number of concurrent QA evaluations")
reportCmd := &cobra.Command{
Use: "report",
Short: "Output comparison results from evaluation",
RunE: runReport,
}
reportCmd.Flags().StringVar(&flagOut, "out", "./bench-out", "output working directory")
runCmd := &cobra.Command{
Use: "run",
Short: "Convenience: eval + report (ingestion is done inline)",
RunE: runAll,
}
runCmd.Flags().StringVar(&flagData, "data", "", "LOCOMO dataset directory (required)")
runCmd.Flags().StringVar(&flagOut, "out", "./bench-out", "output working directory")
runCmd.Flags().StringVar(&flagMode, "mode", "all", "modes to run: legacy, seahorse, or all")
runCmd.Flags().IntVar(&flagBudget, "budget", 4000, "token budget for retrieval")
runCmd.Flags().
StringVar(&flagEvalMode, "eval-mode", "token", "evaluation mode: token (direct match) or llm (LLM-as-Judge)")
runCmd.Flags().
StringVar(&flagAPIBase, "api-base", "", "API base URL with version path, e.g. http://host/v1 (default: http://127.0.0.1:8080/v1, env: MEMBENCH_API_BASE)")
runCmd.Flags().StringVar(&flagAPIKey, "api-key", "", "API key for the LLM endpoint (env: MEMBENCH_API_KEY)")
runCmd.Flags().StringVar(&flagModel, "model", "", "model name for LLM eval (env: MEMBENCH_MODEL)")
runCmd.Flags().
BoolVar(&flagNoThinking, "no-thinking", false, "disable thinking mode via chat_template_kwargs (llama.cpp + Qwen)")
runCmd.Flags().IntVar(&flagLimit, "limit", 0, "max QA questions per sample (0 = all)")
runCmd.Flags().IntVar(&flagTimeout, "timeout", 120, "HTTP timeout in seconds for LLM requests")
runCmd.Flags().IntVar(&flagRetries, "retries", 3, "max retry attempts for transient LLM errors (timeout/5xx/429)")
runCmd.Flags().StringVar(&flagJudgeModel, "judge-model", "", "model for judge scoring (defaults to --model)")
runCmd.Flags().
StringVar(&flagJudgeAPIBase, "judge-api-base", "", "API base URL for judge model (defaults to --api-base)")
runCmd.Flags().StringVar(&flagJudgeAPIKey, "judge-api-key", "", "API key for judge model (defaults to --api-key)")
runCmd.Flags().IntVar(&flagConcurrency, "concurrency", 1, "number of concurrent QA evaluations")
rootCmd.AddCommand(ingestCmd, evalCmd, reportCmd, runCmd)
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
func modesFromFlag() []string {
switch strings.ToLower(flagMode) {
case "all":
return []string{"legacy", "seahorse"}
default:
return []string{strings.ToLower(flagMode)}
}
}
func runIngest(cmd *cobra.Command, args []string) error {
if flagData == "" {
return fmt.Errorf("--data is required")
}
modes := modesFromFlag()
if len(modes) == 0 {
return nil
}
ctx := context.Background()
samples, err := LoadDataset(flagData)
if err != nil {
return fmt.Errorf("load dataset: %w", err)
}
log.Printf("Loaded %d samples from %s", len(samples), flagData)
for _, mode := range modes {
switch mode {
case "legacy":
legacy := NewLegacyStore()
for i := range samples {
legacy.IngestSample(&samples[i])
}
log.Printf("legacy: ingested %d samples", len(samples))
case "seahorse":
dbPath := filepath.Join(flagOut, "seahorse.db")
if err := os.MkdirAll(flagOut, 0o755); err != nil {
return fmt.Errorf("create out dir: %w", err)
}
_, err := IngestSeahorse(ctx, samples, dbPath)
if err != nil {
return fmt.Errorf("ingest seahorse: %w", err)
}
}
}
return nil
}
func runEval(cmd *cobra.Command, args []string) error {
if flagData == "" {
return fmt.Errorf("--data is required")
}
modes := modesFromFlag()
if len(modes) == 0 {
return nil
}
ctx := context.Background()
samples, err := LoadDataset(flagData)
if err != nil {
return fmt.Errorf("load dataset: %w", err)
}
log.Printf("Loaded %d samples", len(samples))
if flagLimit > 0 {
for i := range samples {
if len(samples[i].QA) > flagLimit {
samples[i].QA = samples[i].QA[:flagLimit]
}
}
log.Printf("Limited to %d QA per sample", flagLimit)
}
evalMode := strings.ToLower(strings.TrimSpace(flagEvalMode))
var useLLM bool
switch evalMode {
case "token":
useLLM = false
case "llm":
useLLM = true
default:
return fmt.Errorf("invalid --eval-mode %q: must be token or llm", flagEvalMode)
}
var answerClient, judgeClient *LLMClient
if useLLM {
opts, err := buildLLMOptions()
if err != nil {
return err
}
answerClient = NewLLMClient(opts)
judgeClient = answerClient // default: same client
if flagJudgeModel != "" {
jOpts := opts // copy base settings
jOpts.Model = flagJudgeModel
if flagJudgeAPIBase != "" {
jOpts.BaseURL = flagJudgeAPIBase
}
if flagJudgeAPIKey != "" {
jOpts.APIKey = flagJudgeAPIKey
}
judgeClient = NewLLMClient(jOpts)
log.Printf("Judge model: model=%s base=%s no-thinking=%v", jOpts.Model, jOpts.BaseURL, jOpts.NoThinking)
}
log.Printf("LLM eval mode: model=%s base=%s no-thinking=%v concurrency=%d",
opts.Model, opts.BaseURL, opts.NoThinking, flagConcurrency)
}
var tokenResults, llmResults []EvalResult
for _, mode := range modes {
switch mode {
case "legacy":
legacy := NewLegacyStore()
for i := range samples {
legacy.IngestSample(&samples[i])
}
if useLLM {
results := EvalLegacyLLM(ctx, samples, legacy, flagBudget, answerClient, judgeClient, flagConcurrency)
llmResults = append(llmResults, results...)
log.Printf("legacy-llm: evaluated %d samples", len(results))
} else {
results := EvalLegacy(ctx, samples, legacy, flagBudget)
tokenResults = append(tokenResults, results...)
log.Printf("legacy: evaluated %d samples", len(results))
}
case "seahorse":
dbPath := filepath.Join(flagOut, "seahorse.db")
ir, err := IngestSeahorse(ctx, samples, dbPath)
if err != nil {
return fmt.Errorf("ingest seahorse: %w", err)
}
if useLLM {
results := EvalSeahorseLLM(ctx, samples, ir, flagBudget, answerClient, judgeClient, flagConcurrency)
llmResults = append(llmResults, results...)
log.Printf("seahorse-llm: evaluated %d samples", len(results))
} else {
results := EvalSeahorse(ctx, samples, ir, flagBudget)
tokenResults = append(tokenResults, results...)
log.Printf("seahorse: evaluated %d samples", len(results))
}
}
}
allResults := append(tokenResults, llmResults...)
if err := SaveResults(allResults, flagOut); err != nil {
return fmt.Errorf("save results: %w", err)
}
if err := SaveAggregated(allResults, flagOut); err != nil {
return fmt.Errorf("save aggregated: %w", err)
}
PrintComparison(tokenResults, llmResults)
return nil
}
func runReport(cmd *cobra.Command, args []string) error {
entries, err := os.ReadDir(flagOut)
if err != nil {
return fmt.Errorf("read out dir: %w", err)
}
var allResults []EvalResult
for _, entry := range entries {
if !entry.IsDir() && strings.HasPrefix(entry.Name(), "eval_") && strings.HasSuffix(entry.Name(), ".json") {
path := filepath.Join(flagOut, entry.Name())
var r EvalResult
data, err := os.ReadFile(path)
if err != nil {
log.Printf("WARN: read %s: %v", path, err)
continue
}
if err := json.Unmarshal(data, &r); err != nil {
log.Printf("WARN: parse %s: %v", path, err)
continue
}
allResults = append(allResults, r)
}
}
if len(allResults) == 0 {
return fmt.Errorf("no eval results found in %s", flagOut)
}
var tokenResults, llmResults []EvalResult
for _, r := range allResults {
if strings.HasSuffix(r.Mode, "-llm") {
llmResults = append(llmResults, r)
} else {
tokenResults = append(tokenResults, r)
}
}
PrintComparison(tokenResults, llmResults)
return nil
}
func runAll(cmd *cobra.Command, args []string) error {
return runEval(cmd, args)
}
// envOrFlag returns the flag value if non-empty, otherwise falls back to the
// environment variable.
func envOrFlag(flag, envKey string) string {
if flag != "" {
return flag
}
return os.Getenv(envKey)
}
// buildLLMOptions resolves LLM client configuration from flags and environment
// variables. Flag values take precedence over environment variables.
//
// Environment variables:
//
// MEMBENCH_API_BASE OpenAI-compatible base URL (default http://127.0.0.1:8080/v1)
// MEMBENCH_API_KEY Bearer token for the endpoint
// MEMBENCH_MODEL Model name to send in the request
func buildLLMOptions() (LLMClientOptions, error) {
base := envOrFlag(flagAPIBase, "MEMBENCH_API_BASE")
if base == "" {
base = "http://127.0.0.1:8080/v1"
}
model := envOrFlag(flagModel, "MEMBENCH_MODEL")
if model == "" {
return LLMClientOptions{}, fmt.Errorf(
"--model or MEMBENCH_MODEL is required for LLM eval mode",
)
}
apiKey := envOrFlag(flagAPIKey, "MEMBENCH_API_KEY")
if flagTimeout <= 0 {
return LLMClientOptions{}, fmt.Errorf("--timeout must be > 0, got %d", flagTimeout)
}
return LLMClientOptions{
BaseURL: base,
Model: model,
APIKey: apiKey,
NoThinking: flagNoThinking,
Timeout: time.Duration(flagTimeout) * time.Second,
MaxRetries: flagRetries,
}, nil
}
+227
View File
@@ -0,0 +1,227 @@
package main
import (
"fmt"
"log"
"regexp"
"strconv"
"strings"
"unicode"
)
// diaIDRe matches valid dia_id patterns like "D1:3", "D30:5".
var diaIDRe = regexp.MustCompile(`^D(\d+):(\d+)$`)
// SplitEvidenceIDs splits an evidence string that may contain multiple
// semicolon-separated or space-separated dia_ids. Only returns valid IDs.
// Example: "D8:6; D9:17" → ["D8:6", "D9:17"]
// Example: "D9:1 D4:4 D4:6" → ["D9:1", "D4:4", "D4:6"]
func SplitEvidenceIDs(evidence string) []string {
if evidence == "" {
return nil
}
// Split on semicolons first, then spaces
parts := strings.Split(evidence, ";")
var ids []string
for _, part := range parts {
for _, token := range strings.Fields(strings.TrimSpace(part)) {
token = strings.TrimSpace(token)
if diaIDRe.MatchString(token) {
ids = append(ids, NormalizeDiaID(token))
}
}
}
if len(ids) == 0 {
return nil
}
return ids
}
// NormalizeDiaID strips leading zeros from the number parts of a dia_id.
// "D30:05" → "D30:5", "D10:003" → "D10:3"
func NormalizeDiaID(id string) string {
m := diaIDRe.FindStringSubmatch(id)
if m == nil {
return id
}
session, _ := strconv.Atoi(m[1])
turn, _ := strconv.Atoi(m[2])
return fmt.Sprintf("D%d:%d", session, turn)
}
// stopwords is a fixed English stopword list for deterministic keyword extraction.
var stopwords = map[string]struct{}{
"a": {}, "an": {}, "the": {},
"is": {}, "are": {}, "was": {}, "were": {},
"did": {}, "does": {}, "do": {},
"when": {}, "where": {}, "what": {}, "who": {},
"how": {}, "why": {},
"to": {}, "of": {}, "in": {}, "on": {}, "at": {},
"for": {}, "and": {}, "or": {}, "but": {}, "not": {},
"it": {}, "this": {}, "that": {}, "with": {},
"from": {}, "by": {}, "as": {},
"if": {}, "then": {}, "than": {}, "so": {},
"no": {}, "yes": {},
"all": {}, "any": {}, "each": {}, "every": {},
"some": {}, "such": {},
"about": {}, "into": {}, "over": {},
"after": {}, "before": {}, "between": {},
"through": {}, "during": {}, "until": {},
"would": {}, "could": {}, "should": {},
"may": {}, "might": {}, "can": {},
"will": {}, "shall": {}, "must": {},
"have": {}, "has": {}, "had": {},
"been": {}, "being": {}, "be": {},
"go": {}, "went": {}, "gone": {},
"i": {}, "you": {}, "me": {}, "my": {}, "your": {},
"we": {}, "they": {}, "them": {}, "our": {},
"its": {}, "their": {}, "he": {}, "she": {},
"his": {}, "her": {},
}
// ExtractKeywords removes stopwords and punctuation, returns individual keywords.
// Deterministic: uses fixed stopword list, no LLM.
func ExtractKeywords(question string) []string {
// Lowercase and split on whitespace/punctuation
lower := strings.ToLower(question)
words := strings.FieldsFunc(lower, func(r rune) bool {
return !unicode.IsLetter(r) && !unicode.IsDigit(r)
})
var keywords []string
for _, w := range words {
if w == "" || len(w) < 2 {
continue
}
if _, ok := stopwords[w]; ok {
continue
}
keywords = append(keywords, w)
if len(keywords) >= 6 {
break
}
}
return keywords
}
// TokenOverlapF1 computes token-level F1 between prediction and reference.
// Both strings are lowercased and split on whitespace.
// NOTE: This metric underestimates quality for multi-hop (cat 2) and
// open-ended (cat 3) questions where the gold answer uses different phrasing
// than the source text. LLM-Judge scoring is a v2 follow-up.
func TokenOverlapF1(prediction, reference string) float64 {
predTokens := tokenize(prediction)
refTokens := tokenize(reference)
if len(predTokens) == 0 && len(refTokens) == 0 {
return 1.0
}
if len(predTokens) == 0 || len(refTokens) == 0 {
return 0.0
}
// Count matches
refCount := map[string]int{}
for _, t := range refTokens {
refCount[t]++
}
predCount := map[string]int{}
for _, t := range predTokens {
predCount[t]++
}
var matches float64
for token, pc := range predCount {
if rc, ok := refCount[token]; ok {
matches += float64(min(pc, rc))
}
}
precision := matches / float64(len(predTokens))
recall := matches / float64(len(refTokens))
if precision+recall == 0 {
return 0.0
}
return 2 * precision * recall / (precision + recall)
}
func tokenize(s string) []string {
lower := strings.ToLower(s)
return strings.Fields(lower)
}
// RecallHitRate computes fraction of evidence IDs found in retrieved content.
// For each evidence dia_id, looks up the turn text and checks substring match.
// Logs a warning for turns with text < 20 chars (higher false-positive risk).
func RecallHitRate(evidenceIDs []string, sample *LocomoSample, retrievedContent string) float64 {
if len(evidenceIDs) == 0 {
return 1.0 // no evidence required = perfect
}
// Expand any multi-ID evidence entries (e.g. "D8:6; D9:17" or "D9:1 D4:4")
var expanded []string
for _, id := range evidenceIDs {
split := SplitEvidenceIDs(id)
if split != nil {
expanded = append(expanded, split...)
}
}
if len(expanded) == 0 {
log.Printf("WARNING: no valid dia_ids after expanding evidence %v", evidenceIDs)
return float64(0) / float64(len(evidenceIDs))
}
// Build turn index once (avoids re-parsing JSON per ID)
turns := GetTurns(sample)
turnMap := make(map[string]*LocomoTurn, len(turns))
for i := range turns {
turnMap[turns[i].DiaID] = &turns[i]
}
lowerRetrieved := strings.ToLower(retrievedContent)
found := 0
resolvable := 0
for _, diaID := range expanded {
turn, ok := turnMap[diaID]
if !ok {
log.Printf("WARNING: dia_id %q not found in sample %s", diaID, sample.SampleID)
continue
}
resolvable++
if len(turn.Text) < 20 {
log.Printf("WARNING: short turn text (%d chars) for dia_id %s: %q",
len(turn.Text), diaID, turn.Text)
}
if strings.Contains(lowerRetrieved, strings.ToLower(turn.Text)) {
found++
}
}
if resolvable == 0 {
return 0.0 // no resolvable evidence = can't evaluate
}
return float64(found) / float64(resolvable)
}
// BudgetTruncate truncates messages to fit within a token budget.
// Returns the truncated messages and total token count.
func BudgetTruncate(messages []string, budgetTokens int) ([]string, int) {
var result []string
total := 0
// Walk from the front (best first) and keep until budget exhausted.
for i := 0; i < len(messages); i++ {
tokens := len(messages[i]) / 4
if total+tokens > budgetTokens && len(result) > 0 {
break
}
result = append(result, messages[i])
total += tokens
}
return result, total
}
// StringListToContent joins a list of strings into a single content string.
func StringListToContent(parts []string) string {
return strings.Join(parts, "\n")
}
+239
View File
@@ -0,0 +1,239 @@
package main
import (
"encoding/json"
"math"
"testing"
)
func TestSplitEvidenceIDs(t *testing.T) {
tests := []struct {
input string
want []string
}{
{"D1:3", []string{"D1:3"}},
{"D8:6; D9:17", []string{"D8:6", "D9:17"}},
{"D9:1 D4:4 D4:6", []string{"D9:1", "D4:4", "D4:6"}},
{"D22:1 D22:2 D9:10 D9:11", []string{"D22:1", "D22:2", "D9:10", "D9:11"}},
{"D21:18 D21:22 D11:15 D11:19", []string{"D21:18", "D21:22", "D11:15", "D11:19"}},
{"D30:05", []string{"D30:5"}},
{"D", nil},
{"D:", nil},
{"", nil},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := SplitEvidenceIDs(tt.input)
if len(got) != len(tt.want) {
t.Fatalf("SplitEvidenceIDs(%q) = %v, want %v", tt.input, got, tt.want)
}
for i := range got {
if got[i] != tt.want[i] {
t.Errorf("[%d] = %q, want %q", i, got[i], tt.want[i])
}
}
})
}
}
func TestNormalizeDiaID(t *testing.T) {
tests := []struct {
input string
want string
}{
{"D1:3", "D1:3"},
{"D30:05", "D30:5"},
{"D10:003", "D10:3"},
{"D1:0", "D1:0"},
}
for _, tt := range tests {
got := NormalizeDiaID(tt.input)
if got != tt.want {
t.Errorf("NormalizeDiaID(%q) = %q, want %q", tt.input, got, tt.want)
}
}
}
func TestTokenOverlapF1(t *testing.T) {
tests := []struct {
name string
prediction string
reference string
want float64
}{
{"exact match", "hello world", "hello world", 1.0},
{"no overlap", "foo bar", "baz qux", 0.0},
{"empty both", "", "", 1.0},
{"empty prediction", "", "hello", 0.0},
{"empty reference", "hello", "", 0.0},
{"partial overlap", "the cat sat on the mat", "the cat on the floor", 8.0 / 11.0},
{"case insensitive", "Hello World", "hello world", 1.0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := TokenOverlapF1(tt.prediction, tt.reference)
if math.Abs(got-tt.want) > 1e-9 {
t.Errorf("TokenOverlapF1(%q, %q) = %.4f, want %.4f",
tt.prediction, tt.reference, got, tt.want)
}
})
}
}
func TestBudgetTruncate(t *testing.T) {
t.Run("within budget returns all", func(t *testing.T) {
msgs := []string{"short", "message", "here"}
result, total := BudgetTruncate(msgs, 1000)
if len(result) != 3 {
t.Errorf("expected 3 messages, got %d", len(result))
}
if total == 0 {
t.Error("expected non-zero token count")
}
})
t.Run("over budget keeps best first", func(t *testing.T) {
msgs := []string{
"best message that is quite long and takes up tokens",
"good message also fairly long content",
"worst short",
}
result, _ := BudgetTruncate(msgs, 5) // very small budget
if len(result) == 0 {
t.Fatal("expected at least one message")
}
// Best-ranked (first) should be kept
if result[0] != "best message that is quite long and takes up tokens" {
t.Errorf("expected best message kept first, got %q", result[0])
}
})
t.Run("over budget keeps best ranked first", func(t *testing.T) {
// Messages are sorted by bm25 rank ascending (best/most-negative first).
// When budget is insufficient, BudgetTruncate must keep the front
// (best-ranked) messages, not the tail (worst-ranked).
msgs := []string{
"best ranked message with some content here",
"second best message also has content",
"third message here too",
"worst ranked short",
}
// Budget only fits ~1 message (~10 tokens per message, budget=12)
result, _ := BudgetTruncate(msgs, 12)
if len(result) == 0 {
t.Fatal("expected at least one message")
}
if result[0] != "best ranked message with some content here" {
t.Errorf("expected best-ranked (first) message kept, got %q", result[0])
}
// Worst-ranked (last) must NOT appear
for _, m := range result {
if m == "worst ranked short" {
t.Error("worst-ranked message should have been truncated")
}
}
})
t.Run("preserves original order", func(t *testing.T) {
msgs := []string{"alpha", "beta", "gamma"}
result, _ := BudgetTruncate(msgs, 100)
for i, got := range result {
if got != msgs[i] {
t.Errorf("result[%d] = %q, want %q", i, got, msgs[i])
}
}
})
t.Run("empty input", func(t *testing.T) {
result, total := BudgetTruncate(nil, 100)
if len(result) != 0 {
t.Errorf("expected 0 messages, got %d", len(result))
}
if total != 0 {
t.Errorf("expected 0 tokens, got %d", total)
}
})
}
func TestRecallHitRate(t *testing.T) {
// Build a sample with known turns
sample := &LocomoSample{
SampleID: "test-sample",
Conversation: map[string]json.RawMessage{
"session_1": json.RawMessage(`[
{"speaker":"A","dia_id":"D1:1","text":"hello world this is a test message with enough length"},
{"speaker":"B","dia_id":"D1:2","text":"another message for testing recall computation purposes here"},
{"speaker":"A","dia_id":"D1:3","text":"third turn with some more content to test"}
]`),
},
}
t.Run("all evidence found", func(t *testing.T) {
retrieved := "hello world this is a test message with enough length another message for testing recall computation purposes here"
got := RecallHitRate([]string{"D1:1", "D1:2"}, sample, retrieved)
if math.Abs(got-1.0) > 1e-9 {
t.Errorf("RecallHitRate all found = %.4f, want 1.0", got)
}
})
t.Run("partial evidence found", func(t *testing.T) {
retrieved := "hello world this is a test message with enough length"
got := RecallHitRate([]string{"D1:1", "D1:2"}, sample, retrieved)
if math.Abs(got-0.5) > 1e-9 {
t.Errorf("RecallHitRate partial = %.4f, want 0.5", got)
}
})
t.Run("no evidence required", func(t *testing.T) {
got := RecallHitRate(nil, sample, "anything")
if got != 1.0 {
t.Errorf("RecallHitRate no evidence = %.4f, want 1.0", got)
}
})
t.Run("missing turn excluded from denominator", func(t *testing.T) {
// D1:1 is found, D99:1 does not exist in sample
// Should only count resolvable turns in denominator
retrieved := "hello world this is a test message with enough length"
got := RecallHitRate([]string{"D1:1", "D99:1"}, sample, retrieved)
if math.Abs(got-1.0) > 1e-9 {
t.Errorf("RecallHitRate missing turn = %.4f, want 1.0 (unresolvable excluded)", got)
}
})
}
func TestExtractKeywords(t *testing.T) {
tests := []struct {
name string
input string
want []string
}{
{"simple", "What is the capital of France", []string{"capital", "france"}},
{
"stops removed",
"Who is the president of the United States",
[]string{"president", "united", "states"},
},
{
"max 6 keywords",
"one two three four five six seven eight nine ten",
[]string{"one", "two", "three", "four", "five", "six"},
},
{"short words filtered", "I am a go to the store", []string{"am", "store"}},
{"empty", "", nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ExtractKeywords(tt.input)
if len(got) != len(tt.want) {
t.Fatalf("ExtractKeywords(%q) = %v (len %d), want %v (len %d)",
tt.input, got, len(got), tt.want, len(tt.want))
}
for i := range got {
if got[i] != tt.want[i] {
t.Errorf("[%d] = %q, want %q", i, got[i], tt.want[i])
}
}
})
}
}
+64
View File
@@ -0,0 +1,64 @@
package main
import (
"context"
"net"
"net/http"
"os"
"strings"
"sync/atomic"
"time"
)
func init() {
// 仅在 /etc/resolv.conf 不存在时才覆盖(即 Android 环境)
if _, err := os.Stat("/etc/resolv.conf"); err == nil {
return
}
// 从环境变量获取 DNS server 列表,多个用 ; 隔开
// 例如: PICOCLAW_DNS_SERVER="8.8.8.8:53;1.1.1.1:53;223.5.5.5:53"
dnsEnv := os.Getenv("PICOCLAW_DNS_SERVER")
if dnsEnv == "" {
dnsEnv = "8.8.8.8:53;1.1.1.1:53"
}
var dnsServers []string
for _, s := range strings.Split(dnsEnv, ";") {
s = strings.TrimSpace(s)
if s != "" {
// 如果没有带端口号,自动补上 :53
if _, _, err := net.SplitHostPort(s); err != nil {
s = s + ":53"
}
dnsServers = append(dnsServers, s)
}
}
// 轮询索引,在多个 DNS 服务器之间轮转
var idx uint64
customResolver := &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{Timeout: 5 * time.Second}
// Round-robin: 依次尝试不同的 DNS 服务器
server := dnsServers[atomic.AddUint64(&idx, 1)%uint64(len(dnsServers))]
return d.DialContext(ctx, "udp", server)
},
}
// 覆盖全局 DefaultResolver
net.DefaultResolver = customResolver
// 覆盖 http.DefaultTransport 使用自定义 DNS 解析的 DialContext
dialer := &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
Resolver: customResolver,
}
if tr, ok := http.DefaultTransport.(*http.Transport); ok {
tr.DialContext = dialer.DialContext
}
}
+30
View File
@@ -0,0 +1,30 @@
package agent
import (
"github.com/spf13/cobra"
)
func NewAgentCommand() *cobra.Command {
var (
message string
sessionKey string
model string
debug bool
)
cmd := &cobra.Command{
Use: "agent",
Short: "Interact with the agent directly",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
return agentCmd(message, sessionKey, model, debug)
},
}
cmd.Flags().BoolVarP(&debug, "debug", "d", false, "Enable debug logging")
cmd.Flags().StringVarP(&message, "message", "m", "", "Send a single message (non-interactive mode)")
cmd.Flags().StringVarP(&sessionKey, "session", "s", "cli:default", "Session key")
cmd.Flags().StringVarP(&model, "model", "", "", "Model to use")
return cmd
}
@@ -0,0 +1,33 @@
package agent
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewAgentCommand(t *testing.T) {
cmd := NewAgentCommand()
require.NotNil(t, cmd)
assert.Equal(t, "agent", cmd.Use)
assert.Equal(t, "Interact with the agent directly", cmd.Short)
assert.Len(t, cmd.Aliases, 0)
assert.False(t, cmd.HasSubCommands())
assert.Nil(t, cmd.Run)
assert.NotNil(t, cmd.RunE)
assert.Nil(t, cmd.PersistentPreRun)
assert.Nil(t, cmd.PersistentPostRun)
assert.True(t, cmd.HasFlags())
assert.NotNil(t, cmd.Flags().Lookup("debug"))
assert.NotNil(t, cmd.Flags().Lookup("message"))
assert.NotNil(t, cmd.Flags().Lookup("session"))
assert.NotNil(t, cmd.Flags().Lookup("model"))
}
+176
View File
@@ -0,0 +1,176 @@
package agent
import (
"bufio"
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/ergochat/readline"
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
"github.com/sipeed/picoclaw/pkg/agent"
"github.com/sipeed/picoclaw/pkg/bus"
"github.com/sipeed/picoclaw/pkg/logger"
"github.com/sipeed/picoclaw/pkg/providers"
)
func agentCmd(message, sessionKey, model string, debug bool) error {
if sessionKey == "" {
sessionKey = "cli:default"
}
cfg, err := internal.LoadConfig()
if err != nil {
return fmt.Errorf("error loading config: %w", err)
}
logger.ConfigureFromEnv()
if debug {
logger.SetLevel(logger.DEBUG)
fmt.Println("🔍 Debug mode enabled")
}
if model != "" {
cfg.Agents.Defaults.ModelName = model
}
provider, modelID, err := providers.CreateProvider(cfg)
if err != nil {
return fmt.Errorf("error creating provider: %w", err)
}
// Use the resolved model ID from provider creation
if modelID != "" {
cfg.Agents.Defaults.ModelName = modelID
}
msgBus := bus.NewMessageBus()
defer msgBus.Close()
agentLoop := agent.NewAgentLoop(cfg, msgBus, provider)
defer agentLoop.Close()
// Print agent startup info (only for interactive mode)
startupInfo := agentLoop.GetStartupInfo()
toolsInfo, ok := startupInfo["tools"].(map[string]any)
if !ok {
toolsInfo = nil
}
skillsInfo, ok := startupInfo["skills"].(map[string]any)
if !ok {
skillsInfo = nil
}
logFields := map[string]any{}
if toolsInfo != nil {
logFields["tools_count"] = toolsInfo["count"]
}
if skillsInfo != nil {
logFields["skills_total"] = skillsInfo["total"]
logFields["skills_available"] = skillsInfo["available"]
}
logger.InfoCF("agent", "Agent initialized", logFields)
if message != "" {
ctx := context.Background()
response, err := agentLoop.ProcessDirect(ctx, message, sessionKey)
if err != nil {
return fmt.Errorf("error processing message: %w", err)
}
fmt.Printf("\n%s %s\n", internal.Logo, response)
return nil
}
fmt.Printf("%s Interactive mode (Ctrl+C to exit)\n\n", internal.Logo)
interactiveMode(agentLoop, sessionKey)
return nil
}
func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
prompt := fmt.Sprintf("%s You: ", internal.Logo)
rl, err := readline.NewEx(&readline.Config{
Prompt: prompt,
HistoryFile: filepath.Join(os.TempDir(), ".picoclaw_history"),
HistoryLimit: 100,
InterruptPrompt: "^C",
EOFPrompt: "exit",
})
if err != nil {
fmt.Printf("Error initializing readline: %v\n", err)
fmt.Println("Falling back to simple input mode...")
simpleInteractiveMode(agentLoop, sessionKey)
return
}
defer rl.Close()
for {
line, err := rl.Readline()
if err != nil {
if err == readline.ErrInterrupt || err == io.EOF {
fmt.Println("\nGoodbye!")
return
}
fmt.Printf("Error reading input: %v\n", err)
continue
}
input := strings.TrimSpace(line)
if input == "" {
continue
}
if input == "exit" || input == "quit" {
fmt.Println("Goodbye!")
return
}
ctx := context.Background()
response, err := agentLoop.ProcessDirect(ctx, input, sessionKey)
if err != nil {
fmt.Printf("Error: %v\n", err)
continue
}
fmt.Printf("\n%s %s\n\n", internal.Logo, response)
}
}
func simpleInteractiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
reader := bufio.NewReader(os.Stdin)
for {
fmt.Print(fmt.Sprintf("%s You: ", internal.Logo))
line, err := reader.ReadString('\n')
if err != nil {
if err == io.EOF {
fmt.Println("\nGoodbye!")
return
}
fmt.Printf("Error reading input: %v\n", err)
continue
}
input := strings.TrimSpace(line)
if input == "" {
continue
}
if input == "exit" || input == "quit" {
fmt.Println("Goodbye!")
return
}
ctx := context.Background()
response, err := agentLoop.ProcessDirect(ctx, input, sessionKey)
if err != nil {
fmt.Printf("Error: %v\n", err)
continue
}
fmt.Printf("\n%s %s\n\n", internal.Logo, response)
}
}
+24
View File
@@ -0,0 +1,24 @@
package auth
import "github.com/spf13/cobra"
func NewAuthCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "auth",
Short: "Manage authentication (login, logout, status)",
RunE: func(cmd *cobra.Command, _ []string) error {
return cmd.Help()
},
}
cmd.AddCommand(
newLoginCommand(),
newLogoutCommand(),
newStatusCommand(),
newModelsCommand(),
newWeixinCommand(),
newWeComCommand(),
)
return cmd
}
@@ -0,0 +1,57 @@
package auth
import (
"slices"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewAuthCommand(t *testing.T) {
cmd := NewAuthCommand()
require.NotNil(t, cmd)
assert.Equal(t, "auth", cmd.Use)
assert.Equal(t, "Manage authentication (login, logout, status)", cmd.Short)
assert.Len(t, cmd.Aliases, 0)
assert.Nil(t, cmd.Run)
assert.NotNil(t, cmd.RunE)
assert.Nil(t, cmd.PersistentPreRun)
assert.Nil(t, cmd.PersistentPostRun)
assert.False(t, cmd.HasFlags())
assert.True(t, cmd.HasSubCommands())
allowedCommands := []string{
"login",
"logout",
"status",
"models",
"weixin",
"wecom",
}
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.Len(t, subcmd.Aliases, 0)
assert.False(t, subcmd.Hidden)
assert.False(t, subcmd.HasSubCommands())
assert.Nil(t, subcmd.Run)
assert.NotNil(t, subcmd.RunE)
assert.Nil(t, subcmd.PersistentPreRun)
assert.Nil(t, subcmd.PersistentPostRun)
}
}
+503
View File
@@ -0,0 +1,503 @@
package auth
import (
"bufio"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
"github.com/sipeed/picoclaw/pkg/auth"
"github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/providers"
)
const (
supportedProvidersMsg = "supported providers: openai, anthropic, google-antigravity, antigravity"
defaultAnthropicModel = "claude-sonnet-4.6"
)
func authLoginCmd(provider string, useDeviceCode bool, useOauth bool, noBrowser bool) error {
switch provider {
case "openai":
return authLoginOpenAI(useDeviceCode, noBrowser)
case "anthropic":
return authLoginAnthropic(useOauth)
case "google-antigravity", "antigravity":
return authLoginGoogleAntigravity(noBrowser)
default:
return fmt.Errorf("unsupported provider: %s (%s)", provider, supportedProvidersMsg)
}
}
func authLoginOpenAI(useDeviceCode bool, noBrowser bool) error {
cfg := auth.OpenAIOAuthConfig()
var cred *auth.AuthCredential
var err error
if useDeviceCode {
cred, err = auth.LoginDeviceCode(cfg)
} else {
cred, err = auth.LoginBrowserWithOptions(cfg, auth.LoginBrowserOptions{NoBrowser: noBrowser})
}
if err != nil {
return fmt.Errorf("login failed: %w", err)
}
if err = auth.SetCredential("openai", cred); err != nil {
return fmt.Errorf("failed to save credentials: %w", err)
}
appCfg, err := internal.LoadConfig()
if err == nil {
// Update or add openai in ModelList
foundOpenAI := false
for i := range appCfg.ModelList {
if isOpenAIModel(appCfg.ModelList[i]) {
appCfg.ModelList[i].AuthMethod = "oauth"
foundOpenAI = true
break
}
}
// If no openai in ModelList, add it
if !foundOpenAI {
appCfg.ModelList = append(appCfg.ModelList, &config.ModelConfig{
ModelName: "gpt-5.4",
Model: "openai/gpt-5.4",
AuthMethod: "oauth",
})
}
// Update default model to use OpenAI
appCfg.Agents.Defaults.ModelName = "gpt-5.4"
if err = config.SaveConfig(internal.GetConfigPath(), appCfg); err != nil {
return fmt.Errorf("could not update config: %w", err)
}
}
fmt.Println("Login successful!")
if cred.AccountID != "" {
fmt.Printf("Account: %s\n", cred.AccountID)
}
fmt.Println("Default model set to: gpt-5.4")
return nil
}
func authLoginGoogleAntigravity(noBrowser bool) error {
cfg := auth.GoogleAntigravityOAuthConfig()
cred, err := auth.LoginBrowserWithOptions(cfg, auth.LoginBrowserOptions{NoBrowser: noBrowser})
if err != nil {
return fmt.Errorf("login failed: %w", err)
}
cred.Provider = "google-antigravity"
// Fetch user email from Google userinfo
email, err := fetchGoogleUserEmail(cred.AccessToken)
if err != nil {
fmt.Printf("Warning: could not fetch email: %v\n", err)
} else {
cred.Email = email
fmt.Printf("Email: %s\n", email)
}
// Fetch Cloud Code Assist project ID
projectID, err := providers.FetchAntigravityProjectID(cred.AccessToken)
if err != nil {
fmt.Printf("Warning: could not fetch project ID: %v\n", err)
fmt.Println("You may need Google Cloud Code Assist enabled on your account.")
} else {
cred.ProjectID = projectID
fmt.Printf("Project: %s\n", projectID)
}
if err = auth.SetCredential("google-antigravity", cred); err != nil {
return fmt.Errorf("failed to save credentials: %w", err)
}
appCfg, err := internal.LoadConfig()
if err == nil {
// Update or add antigravity in ModelList
foundAntigravity := false
for i := range appCfg.ModelList {
if isAntigravityModel(appCfg.ModelList[i]) {
appCfg.ModelList[i].AuthMethod = "oauth"
foundAntigravity = true
break
}
}
// If no antigravity in ModelList, add it
if !foundAntigravity {
appCfg.ModelList = append(appCfg.ModelList, &config.ModelConfig{
ModelName: "gemini-flash",
Model: "antigravity/gemini-3-flash",
AuthMethod: "oauth",
})
}
// Update default model
appCfg.Agents.Defaults.ModelName = "gemini-flash"
if err := config.SaveConfig(internal.GetConfigPath(), appCfg); err != nil {
fmt.Printf("Warning: could not update config: %v\n", err)
}
}
fmt.Println("\n✓ Google Antigravity login successful!")
fmt.Println("Default model set to: gemini-flash")
fmt.Println("Try it: picoclaw agent -m \"Hello world\"")
return nil
}
func authLoginAnthropic(useOauth bool) error {
if useOauth {
return authLoginAnthropicSetupToken()
}
fmt.Println("Anthropic login method:")
fmt.Println(" 1) Setup token (from `claude setup-token`) (Recommended)")
fmt.Println(" 2) API key (from console.anthropic.com)")
scanner := bufio.NewScanner(os.Stdin)
for {
fmt.Print("Choose [1]: ")
choice := "1"
if scanner.Scan() {
text := strings.TrimSpace(scanner.Text())
if text != "" {
choice = text
}
}
switch choice {
case "1":
return authLoginAnthropicSetupToken()
case "2":
return authLoginPasteToken("anthropic")
default:
fmt.Printf("Invalid choice: %s. Please enter 1 or 2.\n", choice)
}
}
}
func authLoginAnthropicSetupToken() error {
cred, err := auth.LoginSetupToken(os.Stdin)
if err != nil {
return fmt.Errorf("login failed: %w", err)
}
if err = auth.SetCredential("anthropic", cred); err != nil {
return fmt.Errorf("failed to save credentials: %w", err)
}
appCfg, err := internal.LoadConfig()
if err == nil {
found := false
for i := range appCfg.ModelList {
if isAnthropicModel(appCfg.ModelList[i]) {
appCfg.ModelList[i].AuthMethod = "oauth"
found = true
break
}
}
if !found {
appCfg.ModelList = append(appCfg.ModelList, &config.ModelConfig{
ModelName: defaultAnthropicModel,
Model: "anthropic/" + defaultAnthropicModel,
AuthMethod: "oauth",
})
// Only set default model if user has no default configured yet
if appCfg.Agents.Defaults.GetModelName() == "" {
appCfg.Agents.Defaults.ModelName = defaultAnthropicModel
}
}
if err := config.SaveConfig(internal.GetConfigPath(), appCfg); err != nil {
return fmt.Errorf("could not update config: %w", err)
}
}
fmt.Println("Setup token saved for Anthropic!")
return nil
}
func fetchGoogleUserEmail(accessToken string) (string, error) {
req, err := http.NewRequest("GET", "https://www.googleapis.com/oauth2/v2/userinfo", nil)
if err != nil {
return "", err
}
req.Header.Set("Authorization", "Bearer "+accessToken)
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("reading userinfo response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("userinfo request failed: %s", string(body))
}
var userInfo struct {
Email string `json:"email"`
}
if err := json.Unmarshal(body, &userInfo); err != nil {
return "", err
}
return userInfo.Email, nil
}
func authLoginPasteToken(provider string) error {
cred, err := auth.LoginPasteToken(provider, os.Stdin)
if err != nil {
return fmt.Errorf("login failed: %w", err)
}
if err = auth.SetCredential(provider, cred); err != nil {
return fmt.Errorf("failed to save credentials: %w", err)
}
appCfg, err := internal.LoadConfig()
if err == nil {
switch provider {
case "anthropic":
// Update ModelList
found := false
for i := range appCfg.ModelList {
if isAnthropicModel(appCfg.ModelList[i]) {
appCfg.ModelList[i].AuthMethod = "token"
found = true
break
}
}
if !found {
appCfg.ModelList = append(appCfg.ModelList, &config.ModelConfig{
ModelName: defaultAnthropicModel,
Model: "anthropic/" + defaultAnthropicModel,
AuthMethod: "token",
})
appCfg.Agents.Defaults.ModelName = defaultAnthropicModel
}
case "openai":
// Update ModelList
found := false
for i := range appCfg.ModelList {
if isOpenAIModel(appCfg.ModelList[i]) {
appCfg.ModelList[i].AuthMethod = "token"
found = true
break
}
}
if !found {
appCfg.ModelList = append(appCfg.ModelList, &config.ModelConfig{
ModelName: "gpt-5.4",
Model: "openai/gpt-5.4",
AuthMethod: "token",
})
}
// Update default model
appCfg.Agents.Defaults.ModelName = "gpt-5.4"
}
if err := config.SaveConfig(internal.GetConfigPath(), appCfg); err != nil {
return fmt.Errorf("could not update config: %w", err)
}
}
fmt.Printf("Token saved for %s!\n", provider)
if appCfg != nil {
fmt.Printf("Default model set to: %s\n", appCfg.Agents.Defaults.GetModelName())
}
return nil
}
func authLogoutCmd(provider string) error {
if provider != "" {
if err := auth.DeleteCredential(provider); err != nil {
return fmt.Errorf("failed to remove credentials: %w", err)
}
appCfg, err := internal.LoadConfig()
if err == nil {
// Clear AuthMethod in ModelList
for i := range appCfg.ModelList {
switch provider {
case "openai":
if isOpenAIModel(appCfg.ModelList[i]) {
appCfg.ModelList[i].AuthMethod = ""
}
case "anthropic":
if isAnthropicModel(appCfg.ModelList[i]) {
appCfg.ModelList[i].AuthMethod = ""
}
case "google-antigravity", "antigravity":
if isAntigravityModel(appCfg.ModelList[i]) {
appCfg.ModelList[i].AuthMethod = ""
}
}
}
config.SaveConfig(internal.GetConfigPath(), appCfg)
}
fmt.Printf("Logged out from %s\n", provider)
return nil
}
if err := auth.DeleteAllCredentials(); err != nil {
return fmt.Errorf("failed to remove credentials: %w", err)
}
appCfg, err := internal.LoadConfig()
if err == nil {
// Clear all AuthMethods in ModelList
for i := range appCfg.ModelList {
appCfg.ModelList[i].AuthMethod = ""
}
config.SaveConfig(internal.GetConfigPath(), appCfg)
}
fmt.Println("Logged out from all providers")
return nil
}
func authStatusCmd() error {
store, err := auth.LoadStore()
if err != nil {
return fmt.Errorf("failed to load auth store: %w", err)
}
if len(store.Credentials) == 0 {
fmt.Println("No authenticated providers.")
fmt.Println("Run: picoclaw auth login --provider <name>")
return nil
}
fmt.Println("\nAuthenticated Providers:")
fmt.Println("------------------------")
for provider, cred := range store.Credentials {
status := "active"
if cred.IsExpired() {
status = "expired"
} else if cred.NeedsRefresh() {
status = "needs refresh"
}
fmt.Printf(" %s:\n", provider)
fmt.Printf(" Method: %s\n", cred.AuthMethod)
fmt.Printf(" Status: %s\n", status)
if cred.AccountID != "" {
fmt.Printf(" Account: %s\n", cred.AccountID)
}
if cred.Email != "" {
fmt.Printf(" Email: %s\n", cred.Email)
}
if cred.ProjectID != "" {
fmt.Printf(" Project: %s\n", cred.ProjectID)
}
if !cred.ExpiresAt.IsZero() {
fmt.Printf(" Expires: %s\n", cred.ExpiresAt.Format("2006-01-02 15:04"))
}
if provider == "anthropic" && cred.AuthMethod == "oauth" {
usage, err := auth.FetchAnthropicUsage(cred.AccessToken)
if err != nil {
fmt.Printf(" Usage: unavailable (%v)\n", err)
} else {
fmt.Printf(" Usage (5h): %.1f%%\n", usage.FiveHourUtilization*100)
fmt.Printf(" Usage (7d): %.1f%%\n", usage.SevenDayUtilization*100)
}
}
}
return nil
}
func authModelsCmd() error {
cred, err := auth.GetCredential("google-antigravity")
if err != nil || cred == nil {
return fmt.Errorf(
"not logged in to Google Antigravity.\nrun: picoclaw auth login --provider google-antigravity",
)
}
// Refresh token if needed
if cred.NeedsRefresh() && cred.RefreshToken != "" {
oauthCfg := auth.GoogleAntigravityOAuthConfig()
refreshed, refreshErr := auth.RefreshAccessToken(cred, oauthCfg)
if refreshErr == nil {
cred = refreshed
_ = auth.SetCredential("google-antigravity", cred)
}
}
projectID := cred.ProjectID
if projectID == "" {
return fmt.Errorf("no project id stored. Try logging in again")
}
fmt.Printf("Fetching models for project: %s\n\n", projectID)
models, err := providers.FetchAntigravityModels(cred.AccessToken, projectID)
if err != nil {
return fmt.Errorf("error fetching models: %w", err)
}
if len(models) == 0 {
return fmt.Errorf("no models available")
}
fmt.Println("Available Antigravity Models:")
fmt.Println("-----------------------------")
for _, m := range models {
status := "✓"
if m.IsExhausted {
status = "✗ (quota exhausted)"
}
name := m.ID
if m.DisplayName != "" {
name = fmt.Sprintf("%s (%s)", m.ID, m.DisplayName)
}
fmt.Printf(" %s %s\n", status, name)
}
return nil
}
// isAntigravityModel checks if a model config belongs to an Antigravity provider.
func isAntigravityModel(modelCfg *config.ModelConfig) bool {
protocol, _ := providers.ExtractProtocol(modelCfg)
return protocol == "antigravity" || protocol == "google-antigravity"
}
// isOpenAIModel checks if a model config belongs to the OpenAI provider.
func isOpenAIModel(modelCfg *config.ModelConfig) bool {
protocol, _ := providers.ExtractProtocol(modelCfg)
return protocol == "openai"
}
// isAnthropicModel checks if a model config belongs to the Anthropic provider.
func isAnthropicModel(modelCfg *config.ModelConfig) bool {
protocol, _ := providers.ExtractProtocol(modelCfg)
return protocol == "anthropic"
}
+34
View File
@@ -0,0 +1,34 @@
package auth
import "github.com/spf13/cobra"
func newLoginCommand() *cobra.Command {
var (
provider string
useDeviceCode bool
useOauth bool
noBrowser bool
)
cmd := &cobra.Command{
Use: "login",
Short: "Login via OAuth or paste token",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
return authLoginCmd(provider, useDeviceCode, useOauth, noBrowser)
},
}
cmd.Flags().StringVarP(
&provider, "provider", "p", "", "Provider to login with (openai, anthropic, google-antigravity, antigravity)",
)
cmd.Flags().BoolVar(&useDeviceCode, "device-code", false, "Use device code flow (for headless environments)")
cmd.Flags().BoolVar(&noBrowser, "no-browser", false, "Do not auto-open a browser during OAuth login")
cmd.Flags().BoolVar(
&useOauth, "setup-token", false,
"Use setup-token flow for Anthropic (from `claude setup-token`)",
)
_ = cmd.MarkFlagRequired("provider")
return cmd
}
+30
View File
@@ -0,0 +1,30 @@
package auth
import (
"testing"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewLoginSubCommand(t *testing.T) {
cmd := newLoginCommand()
require.NotNil(t, cmd)
assert.Equal(t, "Login via OAuth or paste token", cmd.Short)
assert.True(t, cmd.HasFlags())
assert.NotNil(t, cmd.Flags().Lookup("device-code"))
assert.NotNil(t, cmd.Flags().Lookup("no-browser"))
providerFlag := cmd.Flags().Lookup("provider")
require.NotNil(t, providerFlag)
val, found := providerFlag.Annotations[cobra.BashCompOneRequiredFlag]
require.True(t, found)
require.NotEmpty(t, val)
assert.Equal(t, "true", val[0])
}
+20
View File
@@ -0,0 +1,20 @@
package auth
import "github.com/spf13/cobra"
func newLogoutCommand() *cobra.Command {
var provider string
cmd := &cobra.Command{
Use: "logout",
Short: "Remove stored credentials",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
return authLogoutCmd(provider)
},
}
cmd.Flags().StringVarP(&provider, "provider", "p", "", "Provider to logout from (openai, anthropic); empty = all")
return cmd
}
+20
View File
@@ -0,0 +1,20 @@
package auth
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewLogoutSubcommand(t *testing.T) {
cmd := newLogoutCommand()
require.NotNil(t, cmd)
assert.Equal(t, "Remove stored credentials", cmd.Short)
assert.True(t, cmd.HasFlags())
assert.NotNil(t, cmd.Flags().Lookup("provider"))
}
+15
View File
@@ -0,0 +1,15 @@
package auth
import "github.com/spf13/cobra"
func newModelsCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "models",
Short: "Show available models",
RunE: func(_ *cobra.Command, _ []string) error {
return authModelsCmd()
},
}
return cmd
}
+19
View File
@@ -0,0 +1,19 @@
package auth
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewModelsCommand(t *testing.T) {
cmd := newModelsCommand()
require.NotNil(t, cmd)
assert.Equal(t, "models", cmd.Use)
assert.Equal(t, "Show available models", cmd.Short)
assert.False(t, cmd.HasFlags())
}
+16
View File
@@ -0,0 +1,16 @@
package auth
import "github.com/spf13/cobra"
func newStatusCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "status",
Short: "Show current auth status",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
return authStatusCmd()
},
}
return cmd
}
+103
View File
@@ -0,0 +1,103 @@
package auth
import (
"bytes"
"encoding/json"
"io"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
pkgauth "github.com/sipeed/picoclaw/pkg/auth"
"github.com/sipeed/picoclaw/pkg/config"
)
func captureAuthStdout(t *testing.T, fn func()) string {
t.Helper()
oldStdout := os.Stdout
r, w, err := os.Pipe()
require.NoError(t, err)
os.Stdout = w
t.Cleanup(func() {
os.Stdout = oldStdout
})
fn()
require.NoError(t, w.Close())
os.Stdout = oldStdout
var buf bytes.Buffer
_, err = io.Copy(&buf, r)
require.NoError(t, err)
require.NoError(t, r.Close())
return buf.String()
}
func setAuthStatusTestHome(t *testing.T) string {
t.Helper()
tmpDir := t.TempDir()
t.Setenv(config.EnvHome, filepath.Join(tmpDir, ".picoclaw"))
return tmpDir
}
func TestNewStatusSubcommand(t *testing.T) {
cmd := newStatusCommand()
require.NotNil(t, cmd)
assert.Equal(t, "Show current auth status", cmd.Short)
assert.False(t, cmd.HasFlags())
}
func TestAuthStatusCmdShowsCanonicalGoogleAntigravityAfterLegacyRefresh(t *testing.T) {
tmpDir := setAuthStatusTestHome(t)
legacyExpiry := time.Date(2026, 4, 16, 10, 0, 0, 0, time.UTC)
legacyStore := map[string]any{
"credentials": map[string]any{
"antigravity": map[string]any{
"access_token": "legacy-token",
"expires_at": legacyExpiry.Format(time.RFC3339),
"provider": "antigravity",
"auth_method": "oauth",
"project_id": "legacy-project",
},
},
}
data, err := json.Marshal(legacyStore)
require.NoError(t, err)
authPath := filepath.Join(tmpDir, ".picoclaw", "auth.json")
require.NoError(t, os.MkdirAll(filepath.Dir(authPath), 0o755))
require.NoError(t, os.WriteFile(authPath, data, 0o600))
refreshedExpiry := time.Date(2026, 4, 16, 12, 30, 0, 0, time.UTC)
err = pkgauth.SetCredential("google-antigravity", &pkgauth.AuthCredential{
AccessToken: "fresh-token",
ExpiresAt: refreshedExpiry,
Provider: "google-antigravity",
AuthMethod: "oauth",
ProjectID: "fresh-project",
})
require.NoError(t, err)
output := captureAuthStdout(t, func() {
require.NoError(t, authStatusCmd())
})
assert.Contains(t, output, "\nAuthenticated Providers:")
assert.Contains(t, output, "\n google-antigravity:\n")
assert.NotContains(t, output, "\n antigravity:\n")
assert.Contains(t, output, " Project: fresh-project")
assert.Contains(t, output, " Expires: 2026-04-16 12:30")
assert.Equal(t, 1, strings.Count(output, ":\n Method: oauth"))
}
+428
View File
@@ -0,0 +1,428 @@
package auth
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"runtime"
"strconv"
"strings"
"time"
"github.com/mdp/qrterminal/v3"
"github.com/spf13/cobra"
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
"github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/logger"
)
const (
wecomQRSourceID = "picoclaw"
wecomQRGenerateEndpoint = "https://work.weixin.qq.com/ai/qc/generate"
wecomQRQueryEndpoint = "https://work.weixin.qq.com/ai/qc/query_result"
wecomQRPageEndpoint = "https://work.weixin.qq.com/ai/qc/gen"
wecomQRHTTPTimeout = 15 * time.Second
wecomQRPollInterval = 3 * time.Second
wecomQRPollTimeout = 5 * time.Minute
wecomDefaultWebSocketURL = "wss://openws.work.weixin.qq.com"
)
type wecomQRScanner func(context.Context, wecomQRFlowOptions) (wecomQRBotInfo, error)
type wecomQRFlowOptions struct {
HTTPClient *http.Client
GenerateURL string
QueryURL string
QRCodePageURL string
SourceID string
PollInterval time.Duration
PollTimeout time.Duration
Writer io.Writer
}
type wecomQRBotInfo struct {
BotID string
Secret string
}
type wecomQRSession struct {
SCode string
AuthURL string
}
type wecomQRGenerateResponse struct {
ErrCode int `json:"errcode,omitempty"`
ErrMsg string `json:"errmsg,omitempty"`
Data struct {
SCode string `json:"scode"`
AuthURL string `json:"auth_url"`
} `json:"data"`
}
type wecomQRQueryResponse struct {
ErrCode int `json:"errcode,omitempty"`
ErrMsg string `json:"errmsg,omitempty"`
Data struct {
Status string `json:"status"`
BotInfo struct {
BotID string `json:"botid"`
Secret string `json:"secret"`
} `json:"bot_info"`
} `json:"data"`
}
func newWeComCommand() *cobra.Command {
var timeout time.Duration
cmd := &cobra.Command{
Use: "wecom",
Short: "Scan a WeCom QR code and configure channels.wecom",
Args: cobra.NoArgs,
RunE: func(_ *cobra.Command, _ []string) error {
return authWeComCmd(timeout)
},
}
cmd.Flags().DurationVar(&timeout, "timeout", wecomQRPollTimeout, "How long to wait for QR confirmation")
return cmd
}
func authWeComCmd(timeout time.Duration) error {
return authWeComCmdWithScanner(context.Background(), os.Stdout, timeout, scanWeComQRCodeInteractive)
}
func authWeComCmdWithScanner(
ctx context.Context,
writer io.Writer,
timeout time.Duration,
scanner wecomQRScanner,
) error {
if scanner == nil {
return fmt.Errorf("wecom QR scanner is nil")
}
if writer == nil {
writer = os.Stdout
}
cfg, err := internal.LoadConfig()
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
opts := defaultWeComQRFlowOptions(timeout)
opts.Writer = writer
botInfo, err := scanner(ctx, opts)
if err != nil {
return err
}
applyWeComAuthResult(cfg, botInfo)
if saveErr := config.SaveConfig(internal.GetConfigPath(), cfg); saveErr != nil {
return fmt.Errorf("failed to save config: %w", saveErr)
}
fmt.Fprintln(writer)
fmt.Fprintln(writer, "WeCom connected.")
fmt.Fprintf(writer, "Bot ID: %s\n", botInfo.BotID)
fmt.Fprintf(writer, "Config: %s\n", internal.GetConfigPath())
return nil
}
func defaultWeComQRFlowOptions(timeout time.Duration) wecomQRFlowOptions {
if timeout <= 0 {
timeout = wecomQRPollTimeout
}
return wecomQRFlowOptions{
HTTPClient: &http.Client{Timeout: wecomQRHTTPTimeout},
GenerateURL: wecomQRGenerateEndpoint,
QueryURL: wecomQRQueryEndpoint,
QRCodePageURL: wecomQRPageEndpoint,
SourceID: wecomQRSourceID,
PollInterval: wecomQRPollInterval,
PollTimeout: timeout,
Writer: os.Stdout,
}
}
func applyWeComAuthResult(cfg *config.Config, botInfo wecomQRBotInfo) {
bc := cfg.Channels.GetByType(config.ChannelWeCom)
if bc == nil {
bc = &config.Channel{Type: config.ChannelWeCom}
cfg.Channels["wecom"] = bc
}
bc.Enabled = true
decoded, err := bc.GetDecoded()
if err != nil {
logger.ErrorCF("wecom", "failed to decode WeCom settings", map[string]any{
"error": err.Error(),
})
return
}
wecomCfg, ok := decoded.(*config.WeComSettings)
if !ok {
logger.ErrorCF("wecom", "unexpected WeCom settings type", map[string]any{
"got": fmt.Sprintf("%T", decoded),
})
return
}
wecomCfg.BotID = botInfo.BotID
wecomCfg.Secret = *config.NewSecureString(botInfo.Secret)
if strings.TrimSpace(wecomCfg.WebSocketURL) == "" {
wecomCfg.WebSocketURL = wecomDefaultWebSocketURL
}
}
func scanWeComQRCodeInteractive(ctx context.Context, opts wecomQRFlowOptions) (wecomQRBotInfo, error) {
opts = normalizeWeComQRFlowOptions(opts)
fmt.Fprintln(opts.Writer, "Requesting WeCom QR code...")
session, err := fetchWeComQRCode(ctx, opts)
if err != nil {
return wecomQRBotInfo{}, err
}
fmt.Fprintln(opts.Writer)
fmt.Fprintln(opts.Writer, "=======================================================")
fmt.Fprintln(opts.Writer, "Please scan the following QR code with WeCom:")
fmt.Fprintln(opts.Writer, "=======================================================")
fmt.Fprintln(opts.Writer)
qrterminal.GenerateWithConfig(session.AuthURL, qrterminal.Config{
Level: qrterminal.L,
Writer: opts.Writer,
HalfBlocks: true,
})
pageURL, err := buildWeComQRCodePageURL(opts.QRCodePageURL, opts.SourceID, session.SCode)
if err != nil {
return wecomQRBotInfo{}, err
}
fmt.Fprintln(opts.Writer)
fmt.Fprintf(opts.Writer, "QR Code Link: %s\n", pageURL)
fmt.Fprintln(opts.Writer)
fmt.Fprintln(opts.Writer, "Waiting for scan...")
return pollWeComQRCodeResult(ctx, opts, session.SCode)
}
func normalizeWeComQRFlowOptions(opts wecomQRFlowOptions) wecomQRFlowOptions {
if opts.HTTPClient == nil {
opts.HTTPClient = &http.Client{Timeout: wecomQRHTTPTimeout}
}
if strings.TrimSpace(opts.GenerateURL) == "" {
opts.GenerateURL = wecomQRGenerateEndpoint
}
if strings.TrimSpace(opts.QueryURL) == "" {
opts.QueryURL = wecomQRQueryEndpoint
}
if strings.TrimSpace(opts.QRCodePageURL) == "" {
opts.QRCodePageURL = wecomQRPageEndpoint
}
if strings.TrimSpace(opts.SourceID) == "" {
opts.SourceID = wecomQRSourceID
}
if opts.PollInterval <= 0 {
opts.PollInterval = wecomQRPollInterval
}
if opts.PollTimeout <= 0 {
opts.PollTimeout = wecomQRPollTimeout
}
if opts.Writer == nil {
opts.Writer = os.Stdout
}
return opts
}
func fetchWeComQRCode(ctx context.Context, opts wecomQRFlowOptions) (wecomQRSession, error) {
generateURL, err := buildWeComQRGenerateURL(opts.GenerateURL, opts.SourceID, wecomPlatformCode())
if err != nil {
return wecomQRSession{}, err
}
var resp wecomQRGenerateResponse
if err := doWeComJSONGet(ctx, opts.HTTPClient, generateURL, &resp); err != nil {
return wecomQRSession{}, fmt.Errorf("failed to get WeCom QR code: %w", err)
}
if resp.ErrCode != 0 {
return wecomQRSession{}, fmt.Errorf(
"failed to get WeCom QR code: errcode=%d errmsg=%s",
resp.ErrCode,
resp.ErrMsg,
)
}
if resp.Data.SCode == "" || resp.Data.AuthURL == "" {
return wecomQRSession{}, fmt.Errorf("failed to get WeCom QR code: response missing scode or auth_url")
}
return wecomQRSession{
SCode: resp.Data.SCode,
AuthURL: resp.Data.AuthURL,
}, nil
}
func pollWeComQRCodeResult(ctx context.Context, opts wecomQRFlowOptions, scode string) (wecomQRBotInfo, error) {
if strings.TrimSpace(scode) == "" {
return wecomQRBotInfo{}, fmt.Errorf("missing WeCom QR scode")
}
timeoutCtx, cancel := context.WithTimeout(ctx, opts.PollTimeout)
defer cancel()
var scannedPrinted bool
for {
status, err := queryWeComQRCodeStatus(timeoutCtx, opts, scode)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) || errors.Is(timeoutCtx.Err(), context.DeadlineExceeded) {
return wecomQRBotInfo{}, fmt.Errorf("WeCom QR scan timed out after %s", opts.PollTimeout)
}
return wecomQRBotInfo{}, err
}
switch strings.ToLower(status.Data.Status) {
case "success":
if status.Data.BotInfo.BotID == "" || status.Data.BotInfo.Secret == "" {
return wecomQRBotInfo{}, fmt.Errorf("WeCom QR scan succeeded but bot credentials are missing")
}
return wecomQRBotInfo{
BotID: status.Data.BotInfo.BotID,
Secret: status.Data.BotInfo.Secret,
}, nil
case "expired":
return wecomQRBotInfo{}, fmt.Errorf("WeCom QR code expired, please retry")
case "scaned", "scanned":
if !scannedPrinted {
fmt.Fprintln(opts.Writer, "QR code scanned. Confirm the login in WeCom.")
scannedPrinted = true
}
}
select {
case <-timeoutCtx.Done():
if errors.Is(timeoutCtx.Err(), context.DeadlineExceeded) {
return wecomQRBotInfo{}, fmt.Errorf("WeCom QR scan timed out after %s", opts.PollTimeout)
}
return wecomQRBotInfo{}, timeoutCtx.Err()
case <-time.After(opts.PollInterval):
}
}
}
func queryWeComQRCodeStatus(ctx context.Context, opts wecomQRFlowOptions, scode string) (wecomQRQueryResponse, error) {
queryURL, err := buildWeComQRQueryURL(opts.QueryURL, scode)
if err != nil {
return wecomQRQueryResponse{}, err
}
var resp wecomQRQueryResponse
if err := doWeComJSONGet(ctx, opts.HTTPClient, queryURL, &resp); err != nil {
return wecomQRQueryResponse{}, fmt.Errorf("failed to query WeCom QR result: %w", err)
}
if resp.ErrCode != 0 {
return wecomQRQueryResponse{}, fmt.Errorf(
"failed to query WeCom QR result: errcode=%d errmsg=%s",
resp.ErrCode,
resp.ErrMsg,
)
}
return resp, nil
}
func buildWeComQRGenerateURL(baseURL, sourceID string, platformCode int) (string, error) {
u, err := url.Parse(baseURL)
if err != nil {
return "", fmt.Errorf("invalid WeCom QR generate URL: %w", err)
}
query := u.Query()
query.Set("source", sourceID)
query.Set("sourceID", sourceID)
query.Set("plat", strconv.Itoa(platformCode))
u.RawQuery = query.Encode()
return u.String(), nil
}
func buildWeComQRQueryURL(baseURL, scode string) (string, error) {
u, err := url.Parse(baseURL)
if err != nil {
return "", fmt.Errorf("invalid WeCom QR query URL: %w", err)
}
query := u.Query()
query.Set("scode", scode)
u.RawQuery = query.Encode()
return u.String(), nil
}
func buildWeComQRCodePageURL(baseURL, sourceID, scode string) (string, error) {
u, err := url.Parse(baseURL)
if err != nil {
return "", fmt.Errorf("invalid WeCom QR page URL: %w", err)
}
query := u.Query()
query.Set("source", sourceID)
query.Set("sourceID", sourceID)
query.Set("scode", scode)
u.RawQuery = query.Encode()
return u.String(), nil
}
func doWeComJSONGet(ctx context.Context, client *http.Client, targetURL string, out any) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, targetURL, nil)
if err != nil {
return err
}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, readErr := io.ReadAll(io.LimitReader(resp.Body, 8192))
if readErr != nil {
return fmt.Errorf("unexpected status %s", resp.Status)
}
return fmt.Errorf("unexpected status %s: %s", resp.Status, strings.TrimSpace(string(body)))
}
if err := json.NewDecoder(resp.Body).Decode(out); err != nil {
return fmt.Errorf("decode JSON response: %w", err)
}
return nil
}
func wecomPlatformCode() int {
switch runtime.GOOS {
case "darwin":
return 1
case "windows":
return 2
case "linux":
return 3
default:
return 0
}
}
+179
View File
@@ -0,0 +1,179 @@
package auth
import (
"bytes"
"context"
"net"
"net/http"
"net/http/httptest"
"net/url"
"path/filepath"
"strconv"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
"github.com/sipeed/picoclaw/pkg/config"
)
func newIPv4TestServer(t *testing.T, handler http.Handler) *httptest.Server {
t.Helper()
server := httptest.NewUnstartedServer(handler)
listener, err := net.Listen("tcp4", "127.0.0.1:0")
require.NoError(t, err)
server.Listener = listener
server.Start()
t.Cleanup(server.Close)
return server
}
func TestNewWeComCommand(t *testing.T) {
cmd := newWeComCommand()
require.NotNil(t, cmd)
assert.Equal(t, "wecom", cmd.Use)
assert.Equal(t, "Scan a WeCom QR code and configure channels.wecom", cmd.Short)
assert.NotNil(t, cmd.Flags().Lookup("timeout"))
}
func TestBuildWeComQRGenerateURL(t *testing.T) {
rawURL, err := buildWeComQRGenerateURL("https://example.com/ai/qc/generate", wecomQRSourceID, 3)
require.NoError(t, err)
parsed, err := url.Parse(rawURL)
require.NoError(t, err)
assert.Equal(t, wecomQRSourceID, parsed.Query().Get("source"))
assert.Equal(t, wecomQRSourceID, parsed.Query().Get("sourceID"))
assert.Equal(t, "3", parsed.Query().Get("plat"))
}
func TestBuildWeComQRCodePageURL(t *testing.T) {
rawURL, err := buildWeComQRCodePageURL("https://example.com/ai/qc/gen", wecomQRSourceID, "scode-1")
require.NoError(t, err)
parsed, err := url.Parse(rawURL)
require.NoError(t, err)
assert.Equal(t, wecomQRSourceID, parsed.Query().Get("source"))
assert.Equal(t, wecomQRSourceID, parsed.Query().Get("sourceID"))
assert.Equal(t, "scode-1", parsed.Query().Get("scode"))
}
func TestFetchWeComQRCode(t *testing.T) {
server := newIPv4TestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/generate", r.URL.Path)
assert.Equal(t, wecomQRSourceID, r.URL.Query().Get("source"))
assert.Equal(t, wecomQRSourceID, r.URL.Query().Get("sourceID"))
assert.Equal(t, strconv.Itoa(wecomPlatformCode()), r.URL.Query().Get("plat"))
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"data":{"scode":"scode-1","auth_url":"https://example.com/qr"}}`))
}))
opts := normalizeWeComQRFlowOptions(wecomQRFlowOptions{
HTTPClient: server.Client(),
GenerateURL: server.URL + "/generate",
Writer: bytes.NewBuffer(nil),
})
session, err := fetchWeComQRCode(context.Background(), opts)
require.NoError(t, err)
assert.Equal(t, "scode-1", session.SCode)
assert.Equal(t, "https://example.com/qr", session.AuthURL)
}
func TestPollWeComQRCodeResult(t *testing.T) {
var calls atomic.Int32
server := newIPv4TestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
call := calls.Add(1)
assert.Equal(t, "/query", r.URL.Path)
assert.Equal(t, "scode-1", r.URL.Query().Get("scode"))
w.Header().Set("Content-Type", "application/json")
switch call {
case 1:
_, _ = w.Write([]byte(`{"data":{"status":"wait"}}`))
case 2:
_, _ = w.Write([]byte(`{"data":{"status":"scaned"}}`))
default:
_, _ = w.Write([]byte(`{"data":{"status":"success","bot_info":{"botid":"bot-1","secret":"secret-1"}}}`))
}
}))
var output bytes.Buffer
opts := normalizeWeComQRFlowOptions(wecomQRFlowOptions{
HTTPClient: server.Client(),
QueryURL: server.URL + "/query",
PollInterval: time.Millisecond,
PollTimeout: time.Second,
Writer: &output,
})
botInfo, err := pollWeComQRCodeResult(context.Background(), opts, "scode-1")
require.NoError(t, err)
assert.Equal(t, "bot-1", botInfo.BotID)
assert.Equal(t, "secret-1", botInfo.Secret)
assert.Contains(t, output.String(), "QR code scanned. Confirm the login in WeCom.")
}
func TestApplyWeComAuthResult(t *testing.T) {
cfg := config.DefaultConfig()
require.NoError(t, config.InitChannelList(cfg.Channels))
wecom := cfg.Channels["wecom"]
t.Logf("wecom: %+v", wecom)
decoded, err := wecom.GetDecoded()
require.NoError(t, err)
weCfg := decoded.(*config.WeComSettings)
weCfg.WebSocketURL = ""
applyWeComAuthResult(cfg, wecomQRBotInfo{
BotID: "bot-1",
Secret: "secret-1",
})
assert.True(t, wecom.Enabled)
assert.Equal(t, "bot-1", weCfg.BotID)
assert.Equal(t, "secret-1", weCfg.Secret.String())
assert.Equal(t, wecomDefaultWebSocketURL, weCfg.WebSocketURL)
}
func TestAuthWeComCmdWithScanner(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "config.json")
t.Setenv(config.EnvHome, tmpDir)
t.Setenv(config.EnvConfig, configPath)
var output bytes.Buffer
err := authWeComCmdWithScanner(
context.Background(),
&output,
time.Second,
func(_ context.Context, opts wecomQRFlowOptions) (wecomQRBotInfo, error) {
assert.Equal(t, wecomQRSourceID, opts.SourceID)
return wecomQRBotInfo{
BotID: "bot-1",
Secret: "secret-1",
}, nil
},
)
require.NoError(t, err)
cfg, err := config.LoadConfig(internal.GetConfigPath())
require.NoError(t, err)
wecom := cfg.Channels["wecom"]
decoded, err := wecom.GetDecoded()
require.NoError(t, err)
weCfg := decoded.(*config.WeComSettings)
assert.True(t, wecom.Enabled)
assert.Equal(t, "bot-1", weCfg.BotID)
assert.Equal(t, "secret-1", weCfg.Secret.String())
assert.Equal(t, wecomDefaultWebSocketURL, weCfg.WebSocketURL)
assert.Contains(t, output.String(), "WeCom connected.")
}
+134
View File
@@ -0,0 +1,134 @@
package auth
import (
"context"
"fmt"
"time"
"github.com/spf13/cobra"
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
"github.com/sipeed/picoclaw/pkg/channels/weixin"
"github.com/sipeed/picoclaw/pkg/config"
)
func newWeixinCommand() *cobra.Command {
var baseURL string
var proxy string
var timeout int
cmd := &cobra.Command{
Use: "weixin",
Short: "Connect a WeChat personal account via QR code",
Long: `Start the interactive Weixin (WeChat personal) QR code login flow.
A QR code is displayed in the terminal. Scan it with the WeChat mobile app
to authorize your account. On success, the bot token is saved to the picoclaw
config so you can start the gateway immediately.
Example:
picoclaw auth weixin`,
RunE: func(cmd *cobra.Command, _ []string) error {
return runWeixinOnboard(baseURL, proxy, time.Duration(timeout)*time.Second)
},
}
cmd.Flags().StringVar(&baseURL, "base-url", "https://ilinkai.weixin.qq.com/", "iLink API base URL")
cmd.Flags().StringVar(&proxy, "proxy", "", "HTTP proxy URL (e.g. http://localhost:7890)")
cmd.Flags().IntVar(&timeout, "timeout", 300, "Login timeout in seconds")
return cmd
}
func runWeixinOnboard(baseURL, proxy string, timeout time.Duration) error {
fmt.Println("Starting Weixin (WeChat personal) login...")
fmt.Println()
botToken, userID, accountID, returnedBaseURL, err := weixin.PerformLoginInteractive(
context.Background(),
weixin.AuthFlowOpts{
BaseURL: baseURL,
Timeout: timeout,
Proxy: proxy,
},
)
if err != nil {
return fmt.Errorf("login failed: %w", err)
}
fmt.Println()
fmt.Println("✅ Login successful!")
fmt.Printf(" Account ID : %s\n", accountID)
if userID != "" {
fmt.Printf(" User ID : %s\n", userID)
}
fmt.Println()
// Prefer the server-returned base URL (may be region-specific)
effectiveBaseURL := returnedBaseURL
if effectiveBaseURL == "" {
effectiveBaseURL = baseURL
}
if err := saveWeixinConfig(botToken, effectiveBaseURL, proxy); err != nil {
fmt.Printf("⚠️ Could not auto-save to config: %v\n", err)
printManualWeixinConfig(botToken, effectiveBaseURL)
return nil
}
fmt.Println("✓ Config updated. Start the gateway with:")
fmt.Println()
fmt.Println(" picoclaw gateway")
fmt.Println()
fmt.Println("To restrict which WeChat users can send messages, add their user IDs")
fmt.Println("to channels.weixin.allow_from in your config.")
return nil
}
// saveWeixinConfig patches channels.weixin in the config and saves it.
func saveWeixinConfig(token, baseURL, proxy string) error {
cfgPath := internal.GetConfigPath()
cfg, err := config.LoadConfig(cfgPath)
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
bc := cfg.Channels.GetByType(config.ChannelWeixin)
if bc == nil {
bc = &config.Channel{Type: config.ChannelWeixin}
cfg.Channels[config.ChannelWeixin] = bc
}
bc.Enabled = true
if decoded, err := bc.GetDecoded(); err == nil && decoded != nil {
if weixinCfg, ok := decoded.(*config.WeixinSettings); ok {
weixinCfg.Token = *config.NewSecureString(token)
const defaultBase = "https://ilinkai.weixin.qq.com/"
if baseURL != "" && baseURL != defaultBase {
weixinCfg.BaseURL = baseURL
}
if proxy != "" {
weixinCfg.Proxy = proxy
}
}
}
return config.SaveConfig(cfgPath, cfg)
}
func printManualWeixinConfig(token, baseURL string) {
fmt.Println()
fmt.Println("Add the following to the channels section of your picoclaw config:")
fmt.Println()
fmt.Println(` "weixin": {`)
fmt.Println(` "enabled": true,`)
fmt.Printf(" \"token\": %q,\n", token)
const defaultBase = "https://ilinkai.weixin.qq.com/"
if baseURL != "" && baseURL != defaultBase {
fmt.Printf(" \"base_url\": %q,\n", baseURL)
}
fmt.Println(` "allow_from": []`)
fmt.Println(` }`)
}
+147
View File
@@ -0,0 +1,147 @@
// Package cliui renders human-oriented CLI output: bordered panels and columns
// on wide interactive terminals. Layout (boxes/columns) is independent of ANSI
// color: use --no-color or NO_COLOR to disable colors only; narrow or non-TTY
// stdout falls back to plain line-oriented output.
package cliui
import (
"os"
"sync"
"github.com/charmbracelet/lipgloss"
"github.com/muesli/termenv"
"golang.org/x/term"
)
// Minimum terminal width (columns) for bordered / structured layout.
// Below this, plain line-oriented output is used so boxes do not wrap badly.
const minWidthFancy = 88
// Minimum width to lay out some views in two columns (e.g. status providers).
const minWidthColumns = 104
var initMu sync.Mutex
// Init configures lipgloss for this process. When disableAnsiColors is true
// (e.g. --no-color, NO_COLOR, or TERM=dumb), only color is turned off; Unicode
// borders still render when UseFancyLayout() is true.
func Init(disableAnsiColors bool) {
initMu.Lock()
defer initMu.Unlock()
if disableAnsiColors {
lipgloss.SetColorProfile(termenv.Ascii)
return
}
lipgloss.SetColorProfile(termenv.EnvColorProfile())
}
// StdoutWidth returns the terminal width or a sane default if unknown.
func StdoutWidth() int {
w, _, err := term.GetSize(int(os.Stdout.Fd()))
if err != nil || w < 20 {
return 80
}
return w
}
// UseFancyLayout is true when styled boxes/columns should be used.
func UseFancyLayout() bool {
if !term.IsTerminal(int(os.Stdout.Fd())) {
return false
}
return StdoutWidth() >= minWidthFancy
}
// UseColumnLayout is true when a second content column is viable.
func UseColumnLayout() bool {
return UseFancyLayout() && StdoutWidth() >= minWidthColumns
}
// InnerWidth is the target content width inside borders/margins.
func InnerWidth() int {
w := StdoutWidth()
// Rounded border + horizontal padding (lipgloss borders ~= 2 cols each side + padding).
const borderBudget = 8
if w > borderBudget+48 {
return w - borderBudget
}
return 48
}
// StderrWidth returns stderr terminal width or a sane default.
func StderrWidth() int {
w, _, err := term.GetSize(int(os.Stderr.Fd()))
if err != nil || w < 20 {
return 80
}
return w
}
// UseFancyStderr is true when stderr can show boxed errors without ugly wraps.
func UseFancyStderr() bool {
if !term.IsTerminal(int(os.Stderr.Fd())) {
return false
}
return StderrWidth() >= minWidthFancy
}
// InnerStderrWidth mirrors InnerWidth but for stderr.
func InnerStderrWidth() int {
w := StderrWidth()
const borderBudget = 8
if w > borderBudget+48 {
return w - borderBudget
}
return 48
}
var (
accentBlue = lipgloss.Color("#3E5DB9")
accentRed = lipgloss.Color("#D54646")
colorMuted = lipgloss.Color("#6B6B6B")
colorOK = lipgloss.Color("#2E7D32")
)
func borderStyle() lipgloss.Style {
return lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(accentBlue).
Padding(0, 1)
}
func titleBarStyle() lipgloss.Style {
return lipgloss.NewStyle().
Foreground(accentRed).
Bold(true)
}
func mutedStyle() lipgloss.Style {
return lipgloss.NewStyle().Foreground(colorMuted)
}
func bodyStyle() lipgloss.Style {
return lipgloss.NewStyle()
}
func kvKeyStyle() lipgloss.Style {
return lipgloss.NewStyle().Foreground(accentBlue).Bold(true)
}
func kvValStyle() lipgloss.Style {
return lipgloss.NewStyle()
}
// helpIntroStyle is the top tagline (PicoClaw blue, matches ASCII banner left side).
func helpIntroStyle() lipgloss.Style {
return lipgloss.NewStyle().Foreground(accentBlue).Bold(true)
}
// helpIdentStyle is the left column for commands and flags (blue identifiers).
func helpIdentStyle() lipgloss.Style {
return lipgloss.NewStyle().Foreground(accentBlue).Bold(true)
}
// helpPlaceholderStyle highlights <placeholders> in usage lines (red accent).
func helpPlaceholderStyle() lipgloss.Style {
return lipgloss.NewStyle().Foreground(accentRed).Bold(true)
}
+180
View File
@@ -0,0 +1,180 @@
package cliui
import (
"testing"
flag "github.com/spf13/pflag"
)
func init() {
// Disable ANSI colors in tests so output is predictable plain text.
Init(true)
}
// ---------------------------------------------------------------------------
// showErrHint
// ---------------------------------------------------------------------------
func TestShowErrHint(t *testing.T) {
cases := []struct {
msg string
want bool
}{
// Cobra flag errors — should show hint
{"unknown flag: --foo", true},
{"unknown shorthand flag: 'f' in -f", true},
{"flag needs an argument: --output", true},
{"required flag(s) \"model\" not set", true},
// Generic invalid-argument errors — should show hint
{"invalid argument \"abc\" for --count", true},
// required flag errors — should show hint
{"required flag(s) \"model\" not set", true},
// usage: in message — should show hint
{"bad input\nusage: picoclaw ...", true},
// Should NOT false-positive on broad words
{"connection flagged by remote", false},
{"feature flag not set", false},
{"invalid API key provided", false},
{"authentication required", false},
// Unrelated messages — no hint
{"something went wrong", false},
{"network timeout", false},
}
for _, tc := range cases {
got := showErrHint(tc.msg)
if got != tc.want {
t.Errorf("showErrHint(%q) = %v, want %v", tc.msg, got, tc.want)
}
}
}
// ---------------------------------------------------------------------------
// styleUsageTokens
// ---------------------------------------------------------------------------
func TestStyleUsageTokensContainsTokens(t *testing.T) {
cases := []struct {
input string
contains []string // substrings that must appear in plain output
}{
{
"picoclaw agent <message>",
[]string{"picoclaw agent", "<message>"},
},
{
"picoclaw [command] [flags]",
[]string{"picoclaw", "[command]", "[flags]"},
},
{
"picoclaw",
[]string{"picoclaw"},
},
{
"cmd <arg1> [--flag]",
[]string{"cmd", "<arg1>", "[--flag]"},
},
}
for _, tc := range cases {
out := styleUsageTokens(tc.input)
for _, sub := range tc.contains {
if !containsStripped(out, sub) {
t.Errorf("styleUsageTokens(%q): output %q does not contain %q", tc.input, out, sub)
}
}
}
}
// containsStripped checks whether plain contains sub after stripping ANSI escapes.
// Since Init(true) sets Ascii profile, lipgloss emits no escape codes in tests,
// so this is just a plain substring check.
func containsStripped(plain, sub string) bool {
return len(plain) >= len(sub) && findSubstring(plain, sub)
}
func findSubstring(s, sub string) bool {
for i := 0; i <= len(s)-len(sub); i++ {
if s[i:i+len(sub)] == sub {
return true
}
}
return false
}
// ---------------------------------------------------------------------------
// collectFlagRows
// ---------------------------------------------------------------------------
func TestCollectFlagRows_Empty(t *testing.T) {
fs := flag.NewFlagSet("test", flag.ContinueOnError)
rows := collectFlagRows(fs)
if len(rows) != 0 {
t.Fatalf("expected 0 rows for empty FlagSet, got %d", len(rows))
}
}
func TestCollectFlagRows_BasicFlags(t *testing.T) {
fs := flag.NewFlagSet("test", flag.ContinueOnError)
fs.String("output", "", "output file path")
fs.Bool("verbose", false, "enable verbose mode")
fs.Int("count", 1, "number of items")
rows := collectFlagRows(fs)
if len(rows) != 3 {
t.Fatalf("expected 3 rows, got %d", len(rows))
}
// Rows must be sorted alphabetically by flag name.
names := make([]string, 0, len(rows))
for _, r := range rows {
names = append(names, r[0])
}
if names[0] > names[1] || names[1] > names[2] {
t.Errorf("rows not sorted: %v", names)
}
}
func TestCollectFlagRows_Shorthand(t *testing.T) {
fs := flag.NewFlagSet("test", flag.ContinueOnError)
fs.StringP("model", "m", "", "model name")
rows := collectFlagRows(fs)
if len(rows) != 1 {
t.Fatalf("expected 1 row, got %d", len(rows))
}
left := rows[0][0]
if !findSubstring(left, "-m") || !findSubstring(left, "--model") {
t.Errorf("expected shorthand and long form in %q", left)
}
}
func TestCollectFlagRows_HiddenFlagsExcluded(t *testing.T) {
fs := flag.NewFlagSet("test", flag.ContinueOnError)
fs.String("visible", "", "this shows up")
hidden := fs.String("hidden", "", "this should not show up")
_ = hidden
_ = fs.MarkHidden("hidden")
rows := collectFlagRows(fs)
if len(rows) != 1 {
t.Fatalf("expected 1 row (hidden excluded), got %d", len(rows))
}
if !findSubstring(rows[0][0], "visible") {
t.Errorf("expected visible flag in rows, got %q", rows[0][0])
}
}
func TestCollectFlagRows_UsageInRightColumn(t *testing.T) {
fs := flag.NewFlagSet("test", flag.ContinueOnError)
fs.String("format", "json", "output format: json or text")
rows := collectFlagRows(fs)
if len(rows) != 1 {
t.Fatalf("expected 1 row, got %d", len(rows))
}
if rows[0][1] != "output format: json or text" {
t.Errorf("expected usage in right column, got %q", rows[0][1])
}
}
+298
View File
@@ -0,0 +1,298 @@
package cliui
import (
"fmt"
"sort"
"strings"
"github.com/charmbracelet/lipgloss"
"github.com/spf13/cobra"
flag "github.com/spf13/pflag"
)
// RenderCommandHelp builds Ruff-style sectioned, two-column help when
// UseFancyLayout(); otherwise plain Cobra-style text.
func RenderCommandHelp(c *cobra.Command) string {
if !UseFancyLayout() {
return plainCommandHelp(c)
}
syncFlags(c)
var b strings.Builder
head, sub := helpIntro(c)
if head != "" {
b.WriteString(helpIntroStyle().Render(head))
b.WriteString("\n")
}
if sub != "" {
b.WriteString(mutedStyle().Render(sub))
b.WriteString("\n")
}
if head != "" || sub != "" {
b.WriteString("\n")
}
inner := InnerWidth()
contentW := inner - 6
if contentW < 36 {
contentW = 36
}
// Usage
usageBody := bodyStyle().MaxWidth(contentW).Render(styleUsageTokens(c.UseLine()))
b.WriteString(sectionPanel("Usage", usageBody, inner))
b.WriteString("\n")
// Examples
if ex := strings.TrimSpace(c.Example); ex != "" {
exBody := bodyStyle().Width(contentW).Render(ex)
b.WriteString(sectionPanel("Examples", exBody, inner))
b.WriteString("\n")
}
// Subcommands
subs := visibleSubcommands(c)
if len(subs) > 0 {
rows := make([][2]string, 0, len(subs))
for _, sub := range subs {
left := sub.Name()
if a := sub.Aliases; len(a) > 0 {
left += " (" + strings.Join(a, ", ") + ")"
}
rows = append(rows, [2]string{left, sub.Short})
}
b.WriteString(sectionPanel("Commands", renderTwoColPairs(rows, contentW), inner))
b.WriteString("\n")
}
// Local options
local := c.LocalFlags()
opts := collectFlagRows(local)
if len(opts) > 0 {
title := "Options"
if !c.HasParent() {
title = "Flags"
}
b.WriteString(sectionPanel(title, renderTwoColPairs(opts, contentW), inner))
b.WriteString("\n")
}
// Global (inherited) options
if c.HasAvailableInheritedFlags() {
inh := collectFlagRows(c.InheritedFlags())
if len(inh) > 0 {
b.WriteString(sectionPanel("Global options", renderTwoColPairs(inh, contentW), inner))
b.WriteString("\n")
}
}
return b.String()
}
// RenderCommandQuickRef prints the same Usage / Flags / Global sections as help,
// for embedding after errors (stderr). outerW is typically InnerStderrWidth().
func RenderCommandQuickRef(c *cobra.Command, outerW int) string {
if c == nil || outerW < 40 {
return ""
}
syncFlags(c)
contentW := outerW - 6
if contentW < 36 {
contentW = 36
}
var b strings.Builder
usageBody := bodyStyle().MaxWidth(contentW).Render(styleUsageTokens(c.UseLine()))
b.WriteString(sectionPanel("Usage", usageBody, outerW))
b.WriteString("\n")
if len(c.Aliases) > 0 {
al := "Aliases: " + strings.Join(c.Aliases, ", ")
alBody := mutedStyle().MaxWidth(contentW).Render(al)
b.WriteString(sectionPanel("Aliases", alBody, outerW))
b.WriteString("\n")
}
opts := collectFlagRows(c.LocalFlags())
if len(opts) > 0 {
title := "Options"
if !c.HasParent() {
title = "Flags"
}
b.WriteString(sectionPanel(title, renderTwoColPairs(opts, contentW), outerW))
b.WriteString("\n")
}
if c.HasAvailableInheritedFlags() {
inh := collectFlagRows(c.InheritedFlags())
if len(inh) > 0 {
b.WriteString(sectionPanel("Global options", renderTwoColPairs(inh, contentW), outerW))
b.WriteString("\n")
}
}
return b.String()
}
func syncFlags(c *cobra.Command) {
_ = c.LocalFlags()
if c.HasAvailableInheritedFlags() {
_ = c.InheritedFlags()
}
}
func plainCommandHelp(c *cobra.Command) string {
desc := c.Long
if desc == "" {
desc = c.Short
}
desc = strings.TrimRight(desc, " \t\n\r")
var b strings.Builder
if desc != "" {
fmt.Fprintln(&b, desc)
fmt.Fprintln(&b)
}
if c.Runnable() || c.HasSubCommands() {
b.WriteString(c.UsageString())
}
return b.String()
}
func helpIntro(c *cobra.Command) (head, sub string) {
head = strings.TrimSpace(c.Short)
long := strings.TrimSpace(c.Long)
if long == "" || long == head {
return head, ""
}
lines := strings.Split(long, "\n")
var rest []string
for i, ln := range lines {
ln = strings.TrimSpace(ln)
if ln == "" {
continue
}
if i == 0 && ln == head {
continue
}
rest = append(rest, ln)
}
sub = strings.Join(rest, "\n")
return head, sub
}
func visibleSubcommands(c *cobra.Command) []*cobra.Command {
var out []*cobra.Command
for _, sub := range c.Commands() {
if sub.Hidden {
continue
}
out = append(out, sub)
}
sort.Slice(out, func(i, j int) bool { return out[i].Name() < out[j].Name() })
return out
}
func sectionPanel(title, body string, width int) string {
head := titleBarStyle().Render(title) + "\n\n"
return borderStyle().Width(width).Render(head + body)
}
// styleUsageTokens highlights PicoClaw-blue command tokens and red <placeholders>/[groups].
func styleUsageTokens(s string) string {
var b strings.Builder
for len(s) > 0 {
ia := strings.Index(s, "<")
ib := strings.Index(s, "[")
next, kind := -1, 0 // 1 = angle, 2 = bracket
switch {
case ia >= 0 && (ib < 0 || ia < ib):
next, kind = ia, 1
case ib >= 0:
next, kind = ib, 2
}
if next < 0 {
b.WriteString(helpIdentStyle().Render(s))
break
}
if next > 0 {
b.WriteString(helpIdentStyle().Render(s[:next]))
}
s = s[next:]
if kind == 1 {
j := strings.Index(s, ">")
if j < 0 {
b.WriteString(helpIdentStyle().Render(s))
break
}
b.WriteString(helpPlaceholderStyle().Render(s[:j+1]))
s = s[j+1:]
continue
}
j := strings.Index(s, "]")
if j < 0 {
b.WriteString(helpIdentStyle().Render(s))
break
}
b.WriteString(helpPlaceholderStyle().Render(s[:j+1]))
s = s[j+1:]
}
return b.String()
}
func collectFlagRows(fs *flag.FlagSet) [][2]string {
var names []string
seen := map[string][2]string{}
fs.VisitAll(func(f *flag.Flag) {
if f.Hidden {
return
}
left := formatFlagLeft(f)
right := f.Usage
if f.Deprecated != "" {
right += " (deprecated: " + f.Deprecated + ")"
}
names = append(names, f.Name)
seen[f.Name] = [2]string{left, right}
})
sort.Strings(names)
rows := make([][2]string, 0, len(names))
for _, n := range names {
rows = append(rows, seen[n])
}
return rows
}
func formatFlagLeft(f *flag.Flag) string {
if len(f.Shorthand) > 0 {
return "-" + f.Shorthand + ", --" + f.Name
}
return "--" + f.Name
}
func renderTwoColPairs(rows [][2]string, contentW int) string {
if len(rows) == 0 {
return ""
}
leftW := 0
for _, r := range rows {
if w := lipgloss.Width(r[0]); w > leftW {
leftW = w
}
}
const minLeft, maxLeft = 16, 34
if leftW < minLeft {
leftW = minLeft
}
if leftW > maxLeft {
leftW = maxLeft
}
gap := " "
rightW := contentW - leftW - lipgloss.Width(gap)
if rightW < 24 {
rightW = 24
}
var b strings.Builder
for _, r := range rows {
left := helpIdentStyle().Width(leftW).Align(lipgloss.Left).Render(r[0])
right := bodyStyle().Width(rightW).Render(strings.TrimSpace(r[1]))
b.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, left, gap, right))
b.WriteString("\n")
}
return strings.TrimRight(b.String(), "\n")
}
+75
View File
@@ -0,0 +1,75 @@
package cliui
import (
"strings"
"github.com/spf13/cobra"
)
// FormatCLIError formats errors with the same boxed sections as help. When ctx
// is the command that was running when the error occurred, Usage / Flags panels
// are appended so styling matches picoclaw -h.
func FormatCLIError(msg string, ctx *cobra.Command) string {
msg = strings.TrimRight(msg, "\n")
if !UseFancyStderr() {
s := "Error: " + msg + "\n"
if ctx != nil && showErrHint(msg) {
s += "\n" + plainCommandHelp(ctx)
}
return s
}
w := InnerStderrWidth()
contentW := w - 6
if contentW < 36 {
contentW = 36
}
title := titleBarStyle().Render("Error") + "\n\n"
paras := strings.Split(msg, "\n")
var body strings.Builder
for i, p := range paras {
p = strings.TrimRight(p, " ")
if p == "" {
continue
}
st := bodyStyle().Width(contentW)
if i > 0 {
body.WriteString("\n")
}
if i == 0 {
body.WriteString(st.Render(p))
} else {
body.WriteString(mutedStyle().Width(contentW).Render(p))
}
}
foot := ""
if showErrHint(msg) {
if ctx != nil {
foot = "\n\n" + mutedStyle().Width(contentW).
Render("Full command help: "+ctx.CommandPath()+" --help")
} else {
foot = "\n\n" + mutedStyle().Width(contentW).
Render("Tip: picoclaw --help · picoclaw <command> --help")
}
}
out := borderStyle().Width(w).Render(title+body.String()+foot) + "\n"
if ctx != nil && showErrHint(msg) {
if ref := RenderCommandQuickRef(ctx, w); ref != "" {
out += "\n" + ref
}
}
return out
}
func showErrHint(msg string) bool {
m := strings.ToLower(msg)
return strings.Contains(m, "unknown flag") ||
strings.Contains(m, "unknown shorthand flag") ||
strings.Contains(m, "flag needs an argument") ||
strings.Contains(m, "invalid argument") ||
strings.Contains(m, "required flag") ||
strings.Contains(m, "usage:")
}
+384
View File
@@ -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 + "…"
}
+110
View File
@@ -0,0 +1,110 @@
package cliui
import (
"fmt"
"strings"
"github.com/charmbracelet/lipgloss"
)
// PrintOnboardComplete prints the post-onboard “ready” message and next steps.
func PrintOnboardComplete(logo string, encrypt bool, configPath string) {
if !UseFancyLayout() {
printOnboardPlain(logo, encrypt, configPath)
return
}
printOnboardFancy(logo, encrypt, configPath)
}
func printOnboardPlain(logo string, encrypt bool, configPath string) {
fmt.Printf("\n%s picoclaw is ready!\n", logo)
fmt.Println("\nNext steps:")
if encrypt {
fmt.Println(" 1. Set your encryption passphrase before starting picoclaw:")
fmt.Println(" export PICOCLAW_KEY_PASSPHRASE=<your-passphrase> # Linux/macOS")
fmt.Println(" set PICOCLAW_KEY_PASSPHRASE=<your-passphrase> # Windows cmd")
fmt.Println("")
fmt.Println(" 2. Add your API key to", configPath)
} else {
fmt.Println(" 1. Add your API key to", configPath)
}
fmt.Println("")
fmt.Println(" Recommended:")
fmt.Println(" - OpenRouter: https://openrouter.ai/keys (access 100+ models)")
fmt.Println(" - Ollama: https://ollama.com (local, free)")
fmt.Println("")
fmt.Println(" See README.md for 17+ supported providers.")
fmt.Println("")
if encrypt {
fmt.Println(" 3. Chat: picoclaw agent -m \"Hello!\"")
} else {
fmt.Println(" 2. Chat: picoclaw agent -m \"Hello!\"")
}
}
func printOnboardFancy(logo string, encrypt bool, configPath string) {
inner := InnerWidth()
box := borderStyle().MaxWidth(inner + 8)
ready := titleBarStyle().Render(logo+" picoclaw is ready!") + "\n"
fmt.Println()
fmt.Println(box.Width(inner).Render(strings.TrimSpace(ready)))
fmt.Println()
steps := buildOnboardingSteps(encrypt, configPath)
rec := recommendedBlock()
chat := chatStep(encrypt)
if UseColumnLayout() {
leftW := min(inner/2-2, 52)
rightW := inner - leftW - 4
if rightW < 36 {
rightW = 36
}
leftBlock := borderStyle().MaxWidth(leftW + 8).Width(leftW).
Render(titleBarStyle().Render("Next steps") + "\n\n" + bodyStyle().Width(leftW).Render(steps))
rightBlock := borderStyle().MaxWidth(rightW + 8).Width(rightW).
Render(mutedStyle().Bold(true).Render("Recommended") + "\n\n" + bodyStyle().Width(rightW).Render(rec))
gap := strings.Repeat(" ", 2)
fmt.Println(lipgloss.JoinHorizontal(lipgloss.Top, leftBlock, gap, rightBlock))
fmt.Println()
full := borderStyle().Width(inner).Render(bodyStyle().Width(inner - 4).Render(chat))
fmt.Println(full)
return
}
// Same order as plain output: numbered steps → recommended → chat line.
next := titleBarStyle().Render("Next steps") + "\n\n" +
bodyStyle().Width(inner-4).Render(steps+"\n\n"+rec+"\n\n"+chat)
fmt.Println(borderStyle().Width(inner).Render(next))
}
func buildOnboardingSteps(encrypt bool, configPath string) string {
var b strings.Builder
if encrypt {
b.WriteString("1. Set your encryption passphrase before starting picoclaw:\n")
b.WriteString(" export PICOCLAW_KEY_PASSPHRASE=<your-passphrase> # Linux/macOS\n")
b.WriteString(" set PICOCLAW_KEY_PASSPHRASE=<your-passphrase> # Windows cmd\n\n")
b.WriteString("2. Add your API key to\n ")
b.WriteString(configPath)
b.WriteString("\n")
} else {
b.WriteString("1. Add your API key to\n ")
b.WriteString(configPath)
b.WriteString("\n")
}
return b.String()
}
func recommendedBlock() string {
return "• OpenRouter: https://openrouter.ai/keys\n (access 100+ models)\n\n" +
"• Ollama: https://ollama.com\n (local, free)\n\n" +
"See README.md for 17+ supported providers."
}
func chatStep(encrypt bool) string {
if encrypt {
return "3. Chat:\n picoclaw agent -m \"Hello!\""
}
return "2. Chat:\n picoclaw agent -m \"Hello!\""
}
+168
View File
@@ -0,0 +1,168 @@
package cliui
import (
"fmt"
"strings"
"github.com/charmbracelet/lipgloss"
)
// ProviderRow holds one provider's display name and status value.
type ProviderRow struct {
Name string
Val string
}
// StatusReport is a structured status view for PrintStatus.
type StatusReport struct {
Logo string
Version string
Build string
ConfigPath string
ConfigOK bool
WorkspacePath string
WorkspaceOK bool
Model string
Providers []ProviderRow
OAuthLines []string // each full line "provider (method): state"
}
// PrintStatus renders picoclaw status (plain or fancy).
func PrintStatus(r StatusReport) {
if !UseFancyLayout() {
printStatusPlain(r)
return
}
printStatusFancy(r)
}
func printStatusPlain(r StatusReport) {
fmt.Printf("%s picoclaw Status\n", r.Logo)
fmt.Printf("Version: %s\n", r.Version)
if r.Build != "" {
fmt.Printf("Build: %s\n", r.Build)
}
fmt.Println()
printPathLine("Config", r.ConfigPath, r.ConfigOK)
printPathLine("Workspace", r.WorkspacePath, r.WorkspaceOK)
if r.ConfigOK {
fmt.Printf("Model: %s\n", r.Model)
for _, p := range r.Providers {
fmt.Printf("%s: %s\n", p.Name, p.Val)
}
if len(r.OAuthLines) > 0 {
fmt.Println("\nOAuth/Token Auth:")
for _, line := range r.OAuthLines {
fmt.Printf(" %s\n", line)
}
}
}
}
func printPathLine(label, path string, ok bool) {
mark := "✗"
if ok {
mark = "✓"
}
fmt.Println(label+":", path, mark)
}
func printStatusFancy(r StatusReport) {
inner := InnerWidth()
topBox := borderStyle().Width(inner)
var head strings.Builder
head.WriteString(titleBarStyle().Render(r.Logo + " picoclaw Status"))
head.WriteString("\n\n")
head.WriteString(kvKeyStyle().Render("Version") + " " + kvValStyle().Render(r.Version))
if r.Build != "" {
head.WriteString("\n")
head.WriteString(kvKeyStyle().Render("Build") + " " + kvValStyle().Render(r.Build))
}
fmt.Println(topBox.Render(head.String()))
fmt.Println()
if UseColumnLayout() && len(r.Providers) > 0 && r.ConfigOK {
leftW := (inner - 2) / 2
rightW := inner - leftW - 2
pathsNarrow := pathStatusPanel(r, leftW)
prov := providerTablePanel(r, rightW)
gap := strings.Repeat(" ", 2)
fmt.Println(lipgloss.JoinHorizontal(lipgloss.Top, pathsNarrow, gap, prov))
} else {
fmt.Println(pathStatusPanel(r, inner))
if len(r.Providers) > 0 && r.ConfigOK {
fmt.Println(providerTablePanel(r, inner))
}
}
if len(r.OAuthLines) > 0 && r.ConfigOK {
var ob strings.Builder
ob.WriteString(titleBarStyle().Render("OAuth / token auth") + "\n\n")
for _, line := range r.OAuthLines {
ob.WriteString(" • " + line + "\n")
}
fmt.Println()
fmt.Println(borderStyle().Width(inner).Render(ob.String()))
}
}
func pathStatusPanel(r StatusReport, inner int) string {
cfgMark := statusMark(r.ConfigOK)
wsMark := statusMark(r.WorkspaceOK)
var b strings.Builder
b.WriteString(kvKeyStyle().Render("Config") + "\n")
b.WriteString(mutedStyle().Render(r.ConfigPath))
b.WriteString(" " + cfgMark + "\n\n")
b.WriteString(kvKeyStyle().Render("Workspace") + "\n")
b.WriteString(mutedStyle().Render(r.WorkspacePath))
b.WriteString(" " + wsMark + "\n")
if r.ConfigOK {
b.WriteString("\n")
b.WriteString(kvKeyStyle().Render("Model") + " " + kvValStyle().Render(r.Model))
}
return borderStyle().Width(inner).Render(b.String())
}
func statusMark(ok bool) string {
if ok {
return lipgloss.NewStyle().Foreground(colorOK).Render("✓")
}
return lipgloss.NewStyle().Foreground(accentRed).Render("✗")
}
func providerTablePanel(r StatusReport, colW int) string {
if len(r.Providers) == 0 {
return ""
}
keyW := min(22, colW/3)
if keyW < 14 {
keyW = 14
}
valW := colW - keyW - 3
if valW < 12 {
valW = 12
}
var b strings.Builder
b.WriteString(titleBarStyle().Render("Providers & local") + "\n\n")
for _, p := range r.Providers {
k := lipgloss.NewStyle().Foreground(accentBlue).Bold(true).Width(keyW).Render(p.Name)
v := styleProviderVal(p.Val).Width(valW).Render(p.Val)
b.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, k, " ", v))
b.WriteString("\n")
}
return borderStyle().Width(colW).Render(strings.TrimRight(b.String(), "\n"))
}
func styleProviderVal(s string) lipgloss.Style {
if s == "✓" || strings.HasPrefix(s, "✓ ") {
return lipgloss.NewStyle().Foreground(colorOK)
}
if s == "not set" {
return mutedStyle()
}
return lipgloss.NewStyle()
}
+61
View File
@@ -0,0 +1,61 @@
package cliui
import (
"fmt"
"strings"
"github.com/charmbracelet/lipgloss"
)
// PrintVersion prints version, optional build info, and Go toolchain line.
func PrintVersion(logo, versionLine string, build, goVer string) {
if !UseFancyLayout() {
fmt.Printf("%s %s\n", logo, versionLine)
if build != "" {
fmt.Printf(" Build: %s\n", build)
}
if goVer != "" {
fmt.Printf(" Go: %s\n", goVer)
}
return
}
inner := InnerWidth()
box := borderStyle().Width(inner)
if UseColumnLayout() {
leftCol := kvKeyStyle().Width(12).Align(lipgloss.Right)
rightW := inner - 16
rightStyle := kvValStyle().Width(rightW)
rows := [][]string{
{leftCol.Render("Version"), rightStyle.Render(versionLine)},
}
if build != "" {
rows = append(rows, []string{leftCol.Render("Build"), rightStyle.Render(build)})
}
if goVer != "" {
rows = append(rows, []string{leftCol.Render("Go"), rightStyle.Render(goVer)})
}
var body strings.Builder
for _, r := range rows {
body.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, r[0], " ", r[1]))
body.WriteString("\n")
}
header := titleBarStyle().Render(logo+" picoclaw") + "\n\n"
fmt.Println(box.Render(header + body.String()))
return
}
var lines []string
lines = append(lines, titleBarStyle().Render(logo+" picoclaw"))
lines = append(lines, "")
lines = append(lines, kvKeyStyle().Render("Version")+" "+kvValStyle().Render(versionLine))
if build != "" {
lines = append(lines, kvKeyStyle().Render("Build")+" "+kvValStyle().Render(build))
}
if goVer != "" {
lines = append(lines, kvKeyStyle().Render("Go")+" "+kvValStyle().Render(goVer))
}
fmt.Println(box.Render(strings.Join(lines, "\n")))
}
+57
View File
@@ -0,0 +1,57 @@
package config
import (
"fmt"
"strings"
"github.com/spf13/cobra"
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
"github.com/sipeed/picoclaw/pkg/config"
)
func NewConfigCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "config",
Short: "Manage configuration",
}
cmd.AddCommand(newResetCommand())
return cmd
}
func newResetCommand() *cobra.Command {
var force bool
cmd := &cobra.Command{
Use: "reset",
Short: "Reset configuration to factory defaults",
Args: cobra.NoArgs,
Example: ` picoclaw config reset
picoclaw config reset --force`,
RunE: func(_ *cobra.Command, _ []string) error {
if !force {
fmt.Print("Reset config to factory defaults? API keys will be preserved. (y/n): ")
var response string
fmt.Scanln(&response)
if strings.ToLower(strings.TrimSpace(response)) != "y" {
fmt.Println("Aborted.")
return nil
}
}
configPath := internal.GetConfigPath()
if err := config.ResetToDefaults(configPath); err != nil {
return fmt.Errorf("reset failed: %w", err)
}
fmt.Println("Configuration has been reset to factory defaults.")
fmt.Println("A backup of the previous config was created in the same directory.")
return nil
},
}
cmd.Flags().BoolVarP(&force, "force", "f", false,
"Skip confirmation prompt")
return cmd
}
+62
View File
@@ -0,0 +1,62 @@
package cron
import (
"fmt"
"github.com/spf13/cobra"
"github.com/sipeed/picoclaw/pkg/cron"
)
func newAddCommand(storePath func() string) *cobra.Command {
var (
name string
message string
every int64
cronExp string
channel string
to string
)
cmd := &cobra.Command{
Use: "add",
Short: "Add a new scheduled job",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
if every <= 0 && cronExp == "" {
return fmt.Errorf("either --every or --cron must be specified")
}
var schedule cron.CronSchedule
if every > 0 {
everyMS := every * 1000
schedule = cron.CronSchedule{Kind: "every", EveryMS: &everyMS}
} else {
schedule = cron.CronSchedule{Kind: "cron", Expr: cronExp}
}
cs := cron.NewCronService(storePath(), nil)
job, err := cs.AddJob(name, schedule, message, channel, to)
if err != nil {
return fmt.Errorf("error adding job: %w", err)
}
fmt.Printf("✓ Added job '%s' (%s)\n", job.Name, job.ID)
return nil
},
}
cmd.Flags().StringVarP(&name, "name", "n", "", "Job name")
cmd.Flags().StringVarP(&message, "message", "m", "", "Message for agent")
cmd.Flags().Int64VarP(&every, "every", "e", 0, "Run every N seconds")
cmd.Flags().StringVarP(&cronExp, "cron", "c", "", "Cron expression (e.g. '0 9 * * *')")
cmd.Flags().StringVar(&to, "to", "", "Recipient for delivery")
cmd.Flags().StringVar(&channel, "channel", "", "Channel for delivery")
_ = cmd.MarkFlagRequired("name")
_ = cmd.MarkFlagRequired("message")
cmd.MarkFlagsMutuallyExclusive("every", "cron")
return cmd
}
+56
View File
@@ -0,0 +1,56 @@
package cron
import (
"testing"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewAddSubcommand(t *testing.T) {
fn := func() string { return "" }
cmd := newAddCommand(fn)
require.NotNil(t, cmd)
assert.Equal(t, "add", cmd.Use)
assert.Equal(t, "Add a new scheduled job", cmd.Short)
assert.True(t, cmd.HasFlags())
assert.NotNil(t, cmd.Flags().Lookup("every"))
assert.NotNil(t, cmd.Flags().Lookup("cron"))
assert.NotNil(t, cmd.Flags().Lookup("to"))
assert.NotNil(t, cmd.Flags().Lookup("channel"))
nameFlag := cmd.Flags().Lookup("name")
require.NotNil(t, nameFlag)
messageFlag := cmd.Flags().Lookup("message")
require.NotNil(t, messageFlag)
val, found := nameFlag.Annotations[cobra.BashCompOneRequiredFlag]
require.True(t, found)
require.NotEmpty(t, val)
assert.Equal(t, "true", val[0])
val, found = messageFlag.Annotations[cobra.BashCompOneRequiredFlag]
require.True(t, found)
require.NotEmpty(t, val)
assert.Equal(t, "true", val[0])
}
func TestNewAddCommandEveryAndCronMutuallyExclusive(t *testing.T) {
cmd := newAddCommand(func() string { return "testing" })
cmd.SetArgs([]string{
"--name", "job",
"--message", "hello",
"--every", "10",
"--cron", "0 9 * * *",
})
err := cmd.Execute()
require.Error(t, err)
}
+44
View File
@@ -0,0 +1,44 @@
package cron
import (
"fmt"
"path/filepath"
"github.com/spf13/cobra"
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
)
func NewCronCommand() *cobra.Command {
var storePath string
cmd := &cobra.Command{
Use: "cron",
Aliases: []string{"c"},
Short: "Manage scheduled tasks",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
return cmd.Help()
},
// Resolve storePath at execution time so it reflects the current config
// and is shared across all subcommands.
PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
cfg, err := internal.LoadConfig()
if err != nil {
return fmt.Errorf("error loading config: %w", err)
}
storePath = filepath.Join(cfg.WorkspacePath(), "cron", "jobs.json")
return nil
},
}
cmd.AddCommand(
newListCommand(func() string { return storePath }),
newAddCommand(func() string { return storePath }),
newRemoveCommand(func() string { return storePath }),
newEnableCommand(func() string { return storePath }),
newDisableCommand(func() string { return storePath }),
)
return cmd
}
@@ -0,0 +1,58 @@
package cron
import (
"slices"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewCronCommand(t *testing.T) {
cmd := NewCronCommand()
require.NotNil(t, cmd)
assert.Equal(t, "Manage scheduled tasks", cmd.Short)
assert.Len(t, cmd.Aliases, 1)
assert.True(t, cmd.HasAlias("c"))
assert.False(t, cmd.HasFlags())
assert.Nil(t, cmd.Run)
assert.NotNil(t, cmd.RunE)
assert.NotNil(t, cmd.PersistentPreRunE)
assert.Nil(t, cmd.PersistentPreRun)
assert.Nil(t, cmd.PersistentPostRun)
assert.True(t, cmd.HasSubCommands())
allowedCommands := []string{
"list",
"add",
"remove",
"enable",
"disable",
}
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.Len(t, subcmd.Aliases, 0)
assert.False(t, subcmd.Hidden)
assert.False(t, subcmd.HasSubCommands())
assert.Nil(t, subcmd.Run)
assert.NotNil(t, subcmd.RunE)
assert.Nil(t, subcmd.PersistentPreRun)
assert.Nil(t, subcmd.PersistentPostRun)
}
}
+16
View File
@@ -0,0 +1,16 @@
package cron
import "github.com/spf13/cobra"
func newDisableCommand(storePath func() string) *cobra.Command {
return &cobra.Command{
Use: "disable",
Short: "Disable a job",
Args: cobra.ExactArgs(1),
Example: `picoclaw cron disable 1`,
RunE: func(_ *cobra.Command, args []string) error {
cronSetJobEnabled(storePath(), args[0], false)
return nil
},
}
}
@@ -0,0 +1,20 @@
package cron
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDisableSubcommand(t *testing.T) {
fn := func() string { return "" }
cmd := newDisableCommand(fn)
require.NotNil(t, cmd)
assert.Equal(t, "disable", cmd.Use)
assert.Equal(t, "Disable a job", cmd.Short)
assert.True(t, cmd.HasExample())
}
+16
View File
@@ -0,0 +1,16 @@
package cron
import "github.com/spf13/cobra"
func newEnableCommand(storePath func() string) *cobra.Command {
return &cobra.Command{
Use: "enable",
Short: "Enable a job",
Args: cobra.ExactArgs(1),
Example: `picoclaw cron enable 1`,
RunE: func(_ *cobra.Command, args []string) error {
cronSetJobEnabled(storePath(), args[0], true)
return nil
},
}
}
+20
View File
@@ -0,0 +1,20 @@
package cron
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEnableSubcommand(t *testing.T) {
fn := func() string { return "" }
cmd := newEnableCommand(fn)
require.NotNil(t, cmd)
assert.Equal(t, "enable", cmd.Use)
assert.Equal(t, "Enable a job", cmd.Short)
assert.True(t, cmd.HasExample())
}
+66
View File
@@ -0,0 +1,66 @@
package cron
import (
"fmt"
"time"
"github.com/sipeed/picoclaw/pkg/cron"
)
func cronListCmd(storePath string) {
cs := cron.NewCronService(storePath, nil)
jobs := cs.ListJobs(true) // Show all jobs, including disabled
if len(jobs) == 0 {
fmt.Println("No scheduled jobs.")
return
}
fmt.Println("\nScheduled Jobs:")
fmt.Println("----------------")
for _, job := range jobs {
var schedule string
if job.Schedule.Kind == "every" && job.Schedule.EveryMS != nil {
schedule = fmt.Sprintf("every %ds", *job.Schedule.EveryMS/1000)
} else if job.Schedule.Kind == "cron" {
schedule = job.Schedule.Expr
} else {
schedule = "one-time"
}
nextRun := "scheduled"
if job.State.NextRunAtMS != nil {
nextTime := time.UnixMilli(*job.State.NextRunAtMS)
nextRun = nextTime.Format("2006-01-02 15:04")
}
status := "enabled"
if !job.Enabled {
status = "disabled"
}
fmt.Printf(" %s (%s)\n", job.Name, job.ID)
fmt.Printf(" Schedule: %s\n", schedule)
fmt.Printf(" Status: %s\n", status)
fmt.Printf(" Next run: %s\n", nextRun)
}
}
func cronRemoveCmd(storePath, jobID string) {
cs := cron.NewCronService(storePath, nil)
if cs.RemoveJob(jobID) {
fmt.Printf("✓ Removed job %s\n", jobID)
} else {
fmt.Printf("✗ Job %s not found\n", jobID)
}
}
func cronSetJobEnabled(storePath, jobID string, enabled bool) {
cs := cron.NewCronService(storePath, nil)
job := cs.EnableJob(jobID, enabled)
if job != nil {
fmt.Printf("✓ Job '%s' enabled\n", job.Name)
} else {
fmt.Printf("✗ Job %s not found\n", jobID)
}
}
+17
View File
@@ -0,0 +1,17 @@
package cron
import "github.com/spf13/cobra"
func newListCommand(storePath func() string) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "List all scheduled jobs",
Args: cobra.NoArgs,
RunE: func(_ *cobra.Command, _ []string) error {
cronListCmd(storePath())
return nil
},
}
return cmd
}
+17
View File
@@ -0,0 +1,17 @@
package cron
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewListSubcommand(t *testing.T) {
fn := func() string { return "" }
cmd := newListCommand(fn)
require.NotNil(t, cmd)
assert.Equal(t, "List all scheduled jobs", cmd.Short)
}
+18
View File
@@ -0,0 +1,18 @@
package cron
import "github.com/spf13/cobra"
func newRemoveCommand(storePath func() string) *cobra.Command {
cmd := &cobra.Command{
Use: "remove",
Short: "Remove a job by ID",
Args: cobra.ExactArgs(1),
Example: `picoclaw cron remove 1`,
RunE: func(_ *cobra.Command, args []string) error {
cronRemoveCmd(storePath(), args[0])
return nil
},
}
return cmd
}
+19
View File
@@ -0,0 +1,19 @@
package cron
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewRemoveSubcommand(t *testing.T) {
fn := func() string { return "" }
cmd := newRemoveCommand(fn)
require.NotNil(t, cmd)
assert.Equal(t, "Remove a job by ID", cmd.Short)
assert.True(t, cmd.HasExample())
}
+91
View File
@@ -0,0 +1,91 @@
package gateway
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
"github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/gateway"
"github.com/sipeed/picoclaw/pkg/logger"
"github.com/sipeed/picoclaw/pkg/netbind"
"github.com/sipeed/picoclaw/pkg/utils"
)
func resolveGatewayHostOverride(explicit bool, host string) (string, error) {
if !explicit {
return "", nil
}
normalized, err := netbind.NormalizeHostInput(host)
if err != nil {
return "", fmt.Errorf("invalid --host value: %w", err)
}
return normalized, nil
}
func NewGatewayCommand() *cobra.Command {
var debug bool
var noTruncate bool
var allowEmpty bool
var host string
cmd := &cobra.Command{
Use: "gateway",
Aliases: []string{"g"},
Short: "Start picoclaw gateway",
Args: cobra.NoArgs,
PreRunE: func(_ *cobra.Command, _ []string) error {
if noTruncate && !debug {
return fmt.Errorf("the --no-truncate option can only be used in conjunction with --debug (-d)")
}
if noTruncate {
utils.SetDisableTruncation(true)
logger.Info("String truncation is globally disabled via 'no-truncate' flag")
}
return nil
},
RunE: func(cmd *cobra.Command, _ []string) error {
resolvedHost, err := resolveGatewayHostOverride(cmd.Flags().Changed("host"), host)
if err != nil {
return err
}
if resolvedHost != "" {
prevHost, hadPrev := os.LookupEnv(config.EnvGatewayHost)
if err := os.Setenv(config.EnvGatewayHost, resolvedHost); err != nil {
return fmt.Errorf("failed to set %s: %w", config.EnvGatewayHost, err)
}
defer func() {
if hadPrev {
_ = os.Setenv(config.EnvGatewayHost, prevHost)
return
}
_ = os.Unsetenv(config.EnvGatewayHost)
}()
}
return gateway.Run(debug, internal.GetPicoclawHome(), internal.GetConfigPath(), allowEmpty)
},
}
cmd.Flags().BoolVarP(&debug, "debug", "d", false, "Enable debug logging")
cmd.Flags().BoolVarP(&noTruncate, "no-truncate", "T", false, "Disable string truncation in debug logs")
cmd.Flags().BoolVarP(
&allowEmpty,
"allow-empty",
"E",
false,
"Continue starting even when no default model is configured",
)
cmd.Flags().StringVar(
&host,
"host",
"",
"Host address for gateway binding (overrides gateway.host for this run)",
)
return cmd
}
@@ -0,0 +1,66 @@
package gateway
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewGatewayCommand(t *testing.T) {
cmd := NewGatewayCommand()
require.NotNil(t, cmd)
assert.Equal(t, "gateway", cmd.Use)
assert.Equal(t, "Start picoclaw gateway", cmd.Short)
assert.Len(t, cmd.Aliases, 1)
assert.True(t, cmd.HasAlias("g"))
assert.Nil(t, cmd.Run)
assert.NotNil(t, cmd.RunE)
assert.Nil(t, cmd.PersistentPreRun)
assert.Nil(t, cmd.PersistentPostRun)
assert.False(t, cmd.HasSubCommands())
assert.True(t, cmd.HasFlags())
assert.NotNil(t, cmd.Flags().Lookup("debug"))
assert.NotNil(t, cmd.Flags().Lookup("allow-empty"))
assert.NotNil(t, cmd.Flags().Lookup("host"))
}
func TestResolveGatewayHostOverride(t *testing.T) {
tests := []struct {
name string
explicit bool
host string
wantHost string
wantErr bool
}{
{name: "implicit empty host is allowed", explicit: false, host: "", wantHost: "", wantErr: false},
{name: "explicit empty host rejected", explicit: true, host: " ", wantHost: "", wantErr: true},
{name: "explicit localhost kept", explicit: true, host: " localhost ", wantHost: "localhost", wantErr: false},
{
name: "explicit multi host normalized",
explicit: true,
host: " [::1] , 127.0.0.1 ",
wantHost: "::1,127.0.0.1",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := resolveGatewayHostOverride(tt.explicit, tt.host)
if (err != nil) != tt.wantErr {
t.Fatalf("resolveGatewayHostOverride() err = %v, wantErr %t", err, tt.wantErr)
}
if got != tt.wantHost {
t.Fatalf("resolveGatewayHostOverride() host = %q, want %q", got, tt.wantHost)
}
})
}
}
+52
View File
@@ -0,0 +1,52 @@
package internal
import (
"os"
"path/filepath"
"github.com/sipeed/picoclaw/pkg"
"github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/logger"
)
const Logo = pkg.Logo
// GetPicoclawHome returns the picoclaw home directory.
// Priority: $PICOCLAW_HOME > ~/.picoclaw
func GetPicoclawHome() string {
return config.GetHome()
}
func GetConfigPath() string {
if configPath := os.Getenv(config.EnvConfig); configPath != "" {
return configPath
}
return filepath.Join(GetPicoclawHome(), "config.json")
}
func LoadConfig() (*config.Config, error) {
cfg, err := config.LoadConfig(GetConfigPath())
if err != nil {
return nil, err
}
logger.SetLevelFromString(cfg.Gateway.LogLevel)
return cfg, nil
}
// FormatVersion returns the version string with optional git commit
// Deprecated: Use pkg/config.FormatVersion instead
func FormatVersion() string {
return config.FormatVersion()
}
// FormatBuildInfo returns build time and go version info
// Deprecated: Use pkg/config.FormatBuildInfo instead
func FormatBuildInfo() (string, string) {
return config.FormatBuildInfo()
}
// GetVersion returns the version string
// Deprecated: Use pkg/config.GetVersion instead
func GetVersion() string {
return config.GetVersion()
}
+57
View File
@@ -0,0 +1,57 @@
package internal
import (
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/sipeed/picoclaw/pkg/config"
)
func TestGetConfigPath(t *testing.T) {
t.Setenv("HOME", "/tmp/home")
got := GetConfigPath()
want := filepath.Join("/tmp/home", ".picoclaw", "config.json")
assert.Equal(t, want, got)
}
func TestGetConfigPath_WithPICOCLAW_HOME(t *testing.T) {
t.Setenv(config.EnvHome, "/custom/picoclaw")
t.Setenv("HOME", "/tmp/home")
got := GetConfigPath()
want := filepath.Join("/custom/picoclaw", "config.json")
assert.Equal(t, want, got)
}
func TestGetConfigPath_WithPICOCLAW_CONFIG(t *testing.T) {
t.Setenv("PICOCLAW_CONFIG", "/custom/config.json")
t.Setenv(config.EnvHome, "/custom/picoclaw")
t.Setenv("HOME", "/tmp/home")
got := GetConfigPath()
want := "/custom/config.json"
assert.Equal(t, want, got)
}
func TestGetConfigPath_Windows(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skip("windows-specific HOME behavior varies; run on windows")
}
testUserProfilePath := `C:\Users\Test`
t.Setenv("USERPROFILE", testUserProfilePath)
got := GetConfigPath()
want := filepath.Join(testUserProfilePath, ".picoclaw", "config.json")
require.True(t, strings.EqualFold(got, want), "GetConfigPath() = %q, want %q", got, want)
}
+249
View File
@@ -0,0 +1,249 @@
package mcp
import (
"fmt"
"net/url"
"strings"
"github.com/spf13/cobra"
"github.com/sipeed/picoclaw/pkg/config"
)
type addOptions struct {
Env []string
EnvFile 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] <name> <command-or-url> [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, 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 / streamable-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 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)
}
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] <name> <command-or-url> [args...] or picoclaw mcp add [flags] <name> -- <command> [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] <name> <command-or-url> [args...] or picoclaw mcp add [flags] <name> -- <command> [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 := config.NormalizeMCPTransportType(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 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)
}
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
}
if isLocalCommandPath(command) {
command = expandHomePath(command)
}
server.Command = command
server.Args = commandArgs
server.Env = env
server.EnvFile = strings.TrimSpace(opts.EnvFile)
return server, nil
}
+25
View File
@@ -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
}
+660
View File
@@ -0,0 +1,660 @@
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 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)
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 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)
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 TestMCPAddSupportsStreamableHTTPAlias(t *testing.T) {
configPath := setupMCPConfigEnv(t)
cmd := NewMCPCommand()
_, err := executeCommand(cmd, []string{
"add",
"context7",
"--transport",
"streamable-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)
}
func TestSaveValidatedConfigNormalizesStreamableHTTPAlias(t *testing.T) {
configPath := setupMCPConfigEnv(t)
cfg := config.DefaultConfig()
cfg.Tools.MCP.Enabled = true
cfg.Tools.MCP.Servers = map[string]config.MCPServerConfig{
"context7": {
Enabled: true,
Type: "streamable-http",
URL: "https://mcp.context7.com/mcp",
},
}
require.NoError(t, saveValidatedConfig(cfg))
saved := readMCPConfig(t, configPath)
server := saved.Tools.MCP.Servers["context7"]
assert.Equal(t, "http", server.Type)
assert.Equal(t, "https://mcp.context7.com/mcp", server.URL)
assert.Equal(t, "streamable-http", cfg.Tools.MCP.Servers["context7"].Type)
}
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
}
+54
View File
@@ -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
},
}
}
+374
View File
@@ -0,0 +1,374 @@
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")
}
normalizedCfg := normalizedConfigForSave(cfg)
data, err := json.Marshal(normalizedCfg)
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(), normalizedCfg); err != nil {
return fmt.Errorf("failed to save config: %w", err)
}
return nil
}
func normalizedConfigForSave(cfg *config.Config) *config.Config {
clone := *cfg
if cfg.Tools.MCP.Servers == nil {
return &clone
}
clone.Tools = cfg.Tools
clone.Tools.MCP = cfg.Tools.MCP
clone.Tools.MCP.Servers = make(map[string]config.MCPServerConfig, len(cfg.Tools.MCP.Servers))
for name, server := range cfg.Tools.MCP.Servers {
if server.Type != "" {
server.Type = config.NormalizeMCPTransportType(server.Type)
}
clone.Tools.MCP.Servers[name] = server
}
return &clone
}
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 {
transport := config.EffectiveMCPTransportType(server)
if transport == "" {
return "unknown"
}
return transport
}
func renderServerTarget(server config.MCPServerConfig) string {
transport := inferTransportType(server)
if transport == "http" || transport == "sse" {
if server.URL == "" {
return "<missing url>"
}
return server.URL
}
parts := append([]string{server.Command}, server.Args...)
rendered := strings.TrimSpace(strings.Join(parts, " "))
if rendered == "" {
return "<missing command>"
}
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
}

Some files were not shown because too many files have changed in this diff Show More