Full Code of sipeed/picoclaw for AI

main fe87376d6a6f cached
695 files
3.9 MB
1.1M tokens
4859 symbols
1 requests
Download .txt
Showing preview only (4,222K chars total). Download the full file or copy to clipboard to get everything.
Repository: sipeed/picoclaw
Branch: main
Commit: fe87376d6a6f
Files: 695
Total size: 3.9 MB

Directory structure:
gitextract_tfefpv8c/

├── .dockerignore
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   ├── feature_request.md
│   │   └── general-task---todo.md
│   ├── dependabot.yml
│   ├── pull_request_template.md
│   └── workflows/
│       ├── build.yml
│       ├── docker-build.yml
│       ├── nightly.yml
│       ├── pr.yml
│       ├── release.yml
│       └── upload-tos.yml
├── .gitignore
├── .golangci.yaml
├── .goreleaser.yaml
├── CONTRIBUTING.md
├── CONTRIBUTING.zh.md
├── LICENSE
├── Makefile
├── README.fr.md
├── README.id.md
├── README.it.md
├── README.ja.md
├── README.md
├── README.pt-br.md
├── README.vi.md
├── README.zh.md
├── ROADMAP.md
├── cmd/
│   ├── picoclaw/
│   │   ├── internal/
│   │   │   ├── agent/
│   │   │   │   ├── command.go
│   │   │   │   ├── command_test.go
│   │   │   │   └── helpers.go
│   │   │   ├── auth/
│   │   │   │   ├── command.go
│   │   │   │   ├── command_test.go
│   │   │   │   ├── helpers.go
│   │   │   │   ├── login.go
│   │   │   │   ├── login_test.go
│   │   │   │   ├── logout.go
│   │   │   │   ├── logout_test.go
│   │   │   │   ├── models.go
│   │   │   │   ├── models_test.go
│   │   │   │   ├── status.go
│   │   │   │   └── status_test.go
│   │   │   ├── cron/
│   │   │   │   ├── add.go
│   │   │   │   ├── add_test.go
│   │   │   │   ├── command.go
│   │   │   │   ├── command_test.go
│   │   │   │   ├── disable.go
│   │   │   │   ├── disable_test.go
│   │   │   │   ├── enable.go
│   │   │   │   ├── enable_test.go
│   │   │   │   ├── helpers.go
│   │   │   │   ├── list.go
│   │   │   │   ├── list_test.go
│   │   │   │   ├── remove.go
│   │   │   │   └── remove_test.go
│   │   │   ├── gateway/
│   │   │   │   ├── command.go
│   │   │   │   └── command_test.go
│   │   │   ├── helpers.go
│   │   │   ├── helpers_test.go
│   │   │   ├── migrate/
│   │   │   │   ├── command.go
│   │   │   │   └── command_test.go
│   │   │   ├── model/
│   │   │   │   ├── command.go
│   │   │   │   └── command_test.go
│   │   │   ├── onboard/
│   │   │   │   ├── command.go
│   │   │   │   ├── command_test.go
│   │   │   │   ├── helpers.go
│   │   │   │   └── helpers_test.go
│   │   │   ├── skills/
│   │   │   │   ├── command.go
│   │   │   │   ├── command_test.go
│   │   │   │   ├── helpers.go
│   │   │   │   ├── install.go
│   │   │   │   ├── install_test.go
│   │   │   │   ├── installbuiltin.go
│   │   │   │   ├── installbuiltin_test.go
│   │   │   │   ├── list.go
│   │   │   │   ├── list_test.go
│   │   │   │   ├── listbuiltin.go
│   │   │   │   ├── listbuiltin_test.go
│   │   │   │   ├── remove.go
│   │   │   │   ├── remove_test.go
│   │   │   │   ├── search.go
│   │   │   │   ├── search_test.go
│   │   │   │   ├── show.go
│   │   │   │   └── show_test.go
│   │   │   ├── status/
│   │   │   │   ├── command.go
│   │   │   │   ├── command_test.go
│   │   │   │   └── helpers.go
│   │   │   └── version/
│   │   │       ├── command.go
│   │   │       └── command_test.go
│   │   ├── main.go
│   │   └── main_test.go
│   └── picoclaw-launcher-tui/
│       ├── internal/
│       │   ├── config/
│       │   │   └── store.go
│       │   └── ui/
│       │       ├── app.go
│       │       ├── channel.go
│       │       ├── gateway_posix.go
│       │       ├── gateway_windows.go
│       │       ├── menu.go
│       │       ├── model.go
│       │       └── style.go
│       └── main.go
├── config/
│   └── config.example.json
├── docker/
│   ├── Dockerfile
│   ├── Dockerfile.full
│   ├── Dockerfile.goreleaser
│   ├── Dockerfile.goreleaser.launcher
│   ├── docker-compose.full.yml
│   ├── docker-compose.yml
│   └── entrypoint.sh
├── docs/
│   ├── ANTIGRAVITY_AUTH.md
│   ├── ANTIGRAVITY_USAGE.md
│   ├── agent-refactor/
│   │   └── README.md
│   ├── channels/
│   │   ├── dingtalk/
│   │   │   └── README.zh.md
│   │   ├── discord/
│   │   │   └── README.zh.md
│   │   ├── feishu/
│   │   │   └── README.zh.md
│   │   ├── line/
│   │   │   └── README.zh.md
│   │   ├── maixcam/
│   │   │   └── README.zh.md
│   │   ├── matrix/
│   │   │   ├── README.md
│   │   │   └── README.zh.md
│   │   ├── onebot/
│   │   │   └── README.zh.md
│   │   ├── qq/
│   │   │   └── README.zh.md
│   │   ├── slack/
│   │   │   └── README.zh.md
│   │   ├── telegram/
│   │   │   └── README.zh.md
│   │   └── wecom/
│   │       ├── wecom_aibot/
│   │       │   └── README.zh.md
│   │       ├── wecom_app/
│   │       │   └── README.zh.md
│   │       └── wecom_bot/
│   │           └── README.zh.md
│   ├── chat-apps.md
│   ├── configuration.md
│   ├── credential_encryption.md
│   ├── debug.md
│   ├── design/
│   │   ├── issue-783-investigation-and-fix-plan.zh.md
│   │   ├── provider-refactoring-tests.md
│   │   └── provider-refactoring.md
│   ├── docker.md
│   ├── fr/
│   │   ├── chat-apps.md
│   │   ├── configuration.md
│   │   ├── docker.md
│   │   ├── providers.md
│   │   ├── spawn-tasks.md
│   │   ├── tools_configuration.md
│   │   └── troubleshooting.md
│   ├── it/
│   │   └── configuration.md
│   ├── ja/
│   │   ├── chat-apps.md
│   │   ├── configuration.md
│   │   ├── docker.md
│   │   ├── providers.md
│   │   ├── spawn-tasks.md
│   │   ├── tools_configuration.md
│   │   └── troubleshooting.md
│   ├── migration/
│   │   └── model-list-migration.md
│   ├── providers.md
│   ├── pt-br/
│   │   ├── chat-apps.md
│   │   ├── configuration.md
│   │   ├── docker.md
│   │   ├── providers.md
│   │   ├── spawn-tasks.md
│   │   ├── tools_configuration.md
│   │   └── troubleshooting.md
│   ├── spawn-tasks.md
│   ├── tools_configuration.md
│   ├── troubleshooting.md
│   ├── vi/
│   │   ├── chat-apps.md
│   │   ├── configuration.md
│   │   ├── docker.md
│   │   ├── providers.md
│   │   ├── spawn-tasks.md
│   │   ├── tools_configuration.md
│   │   └── troubleshooting.md
│   └── zh/
│       ├── chat-apps.md
│       ├── configuration.md
│       ├── docker.md
│       ├── providers.md
│       ├── spawn-tasks.md
│       ├── tools_configuration.md
│       └── troubleshooting.md
├── go.mod
├── go.sum
├── pkg/
│   ├── agent/
│   │   ├── context.go
│   │   ├── context_cache_test.go
│   │   ├── context_test.go
│   │   ├── instance.go
│   │   ├── instance_test.go
│   │   ├── loop.go
│   │   ├── loop_mcp.go
│   │   ├── loop_mcp_test.go
│   │   ├── loop_media.go
│   │   ├── loop_test.go
│   │   ├── memory.go
│   │   ├── mock_provider_test.go
│   │   ├── model_resolution.go
│   │   ├── registry.go
│   │   ├── registry_test.go
│   │   ├── thinking.go
│   │   └── thinking_test.go
│   ├── auth/
│   │   ├── anthropic_usage.go
│   │   ├── anthropic_usage_test.go
│   │   ├── oauth.go
│   │   ├── oauth_test.go
│   │   ├── pkce.go
│   │   ├── pkce_test.go
│   │   ├── store.go
│   │   ├── store_test.go
│   │   ├── token.go
│   │   └── token_test.go
│   ├── bus/
│   │   ├── bus.go
│   │   ├── bus_test.go
│   │   └── types.go
│   ├── channels/
│   │   ├── README.md
│   │   ├── README.zh.md
│   │   ├── base.go
│   │   ├── base_test.go
│   │   ├── dingtalk/
│   │   │   ├── dingtalk.go
│   │   │   └── init.go
│   │   ├── discord/
│   │   │   ├── discord.go
│   │   │   ├── discord_resolve_test.go
│   │   │   ├── discord_test.go
│   │   │   └── init.go
│   │   ├── errors.go
│   │   ├── errors_test.go
│   │   ├── errutil.go
│   │   ├── errutil_test.go
│   │   ├── feishu/
│   │   │   ├── common.go
│   │   │   ├── common_test.go
│   │   │   ├── feishu_32.go
│   │   │   ├── feishu_64.go
│   │   │   ├── feishu_64_test.go
│   │   │   ├── init.go
│   │   │   └── token_cache.go
│   │   ├── interfaces.go
│   │   ├── interfaces_command_test.go
│   │   ├── irc/
│   │   │   ├── handler.go
│   │   │   ├── init.go
│   │   │   ├── irc.go
│   │   │   └── irc_test.go
│   │   ├── line/
│   │   │   ├── init.go
│   │   │   ├── line.go
│   │   │   └── line_test.go
│   │   ├── maixcam/
│   │   │   ├── init.go
│   │   │   └── maixcam.go
│   │   ├── manager.go
│   │   ├── manager_channel.go
│   │   ├── manager_channel_test.go
│   │   ├── manager_test.go
│   │   ├── matrix/
│   │   │   ├── init.go
│   │   │   ├── matrix.go
│   │   │   └── matrix_test.go
│   │   ├── media.go
│   │   ├── onebot/
│   │   │   ├── init.go
│   │   │   └── onebot.go
│   │   ├── pico/
│   │   │   ├── init.go
│   │   │   ├── pico.go
│   │   │   └── protocol.go
│   │   ├── qq/
│   │   │   ├── botgo_logger.go
│   │   │   ├── init.go
│   │   │   ├── qq.go
│   │   │   └── qq_test.go
│   │   ├── registry.go
│   │   ├── slack/
│   │   │   ├── init.go
│   │   │   ├── slack.go
│   │   │   └── slack_test.go
│   │   ├── split.go
│   │   ├── split_test.go
│   │   ├── telegram/
│   │   │   ├── command_registration.go
│   │   │   ├── command_registration_test.go
│   │   │   ├── init.go
│   │   │   ├── parse_markdown_to_md_v2.go
│   │   │   ├── parse_markdown_to_md_v2_test.go
│   │   │   ├── parser_markdown_to_html.go
│   │   │   ├── telegram.go
│   │   │   ├── telegram_dispatch_test.go
│   │   │   ├── telegram_group_command_filter_test.go
│   │   │   ├── telegram_test.go
│   │   │   └── testdata/
│   │   │       └── md2_all_formats.txt
│   │   ├── webhook.go
│   │   ├── wecom/
│   │   │   ├── aibot.go
│   │   │   ├── aibot_test.go
│   │   │   ├── aibot_ws.go
│   │   │   ├── aibot_ws_test.go
│   │   │   ├── app.go
│   │   │   ├── app_test.go
│   │   │   ├── bot.go
│   │   │   ├── bot_test.go
│   │   │   ├── common.go
│   │   │   ├── dedupe.go
│   │   │   ├── dedupe_test.go
│   │   │   └── init.go
│   │   ├── whatsapp/
│   │   │   ├── init.go
│   │   │   ├── whatsapp.go
│   │   │   └── whatsapp_command_test.go
│   │   └── whatsapp_native/
│   │       ├── init.go
│   │       ├── whatsapp_command_test.go
│   │       ├── whatsapp_native.go
│   │       └── whatsapp_native_stub.go
│   ├── commands/
│   │   ├── builtin.go
│   │   ├── builtin_test.go
│   │   ├── cmd_check.go
│   │   ├── cmd_clear.go
│   │   ├── cmd_help.go
│   │   ├── cmd_list.go
│   │   ├── cmd_reload.go
│   │   ├── cmd_show.go
│   │   ├── cmd_start.go
│   │   ├── cmd_switch.go
│   │   ├── cmd_switch_test.go
│   │   ├── definition.go
│   │   ├── definition_test.go
│   │   ├── executor.go
│   │   ├── executor_test.go
│   │   ├── handler_agents.go
│   │   ├── registry.go
│   │   ├── registry_test.go
│   │   ├── request.go
│   │   ├── request_test.go
│   │   ├── runtime.go
│   │   └── show_list_handlers_test.go
│   ├── config/
│   │   ├── config.go
│   │   ├── config_test.go
│   │   ├── defaults.go
│   │   ├── envkeys.go
│   │   ├── migration.go
│   │   ├── migration_test.go
│   │   ├── model_config_test.go
│   │   ├── multikey_test.go
│   │   ├── version.go
│   │   └── version_test.go
│   ├── constants/
│   │   └── channels.go
│   ├── credential/
│   │   ├── credential.go
│   │   ├── credential_test.go
│   │   ├── keygen.go
│   │   ├── keygen_test.go
│   │   ├── store.go
│   │   └── store_test.go
│   ├── cron/
│   │   ├── service.go
│   │   └── service_test.go
│   ├── devices/
│   │   ├── events/
│   │   │   └── events.go
│   │   ├── service.go
│   │   ├── source.go
│   │   └── sources/
│   │       ├── usb_linux.go
│   │       └── usb_stub.go
│   ├── fileutil/
│   │   └── file.go
│   ├── gateway/
│   │   └── gateway.go
│   ├── health/
│   │   └── server.go
│   ├── heartbeat/
│   │   ├── service.go
│   │   └── service_test.go
│   ├── identity/
│   │   ├── identity.go
│   │   └── identity_test.go
│   ├── logger/
│   │   ├── logger.go
│   │   ├── logger_3rd_party.go
│   │   └── logger_test.go
│   ├── mcp/
│   │   ├── manager.go
│   │   └── manager_test.go
│   ├── media/
│   │   ├── store.go
│   │   ├── store_test.go
│   │   └── tempdir.go
│   ├── memory/
│   │   ├── jsonl.go
│   │   ├── jsonl_test.go
│   │   ├── migration.go
│   │   ├── migration_test.go
│   │   └── store.go
│   ├── migrate/
│   │   ├── internal/
│   │   │   ├── common.go
│   │   │   ├── common_test.go
│   │   │   └── types.go
│   │   ├── migrate.go
│   │   ├── migrate_test.go
│   │   └── sources/
│   │       └── openclaw/
│   │           ├── common.go
│   │           ├── openclaw_config.go
│   │           ├── openclaw_config_test.go
│   │           ├── openclaw_handler.go
│   │           └── openclaw_handler_test.go
│   ├── providers/
│   │   ├── anthropic/
│   │   │   ├── provider.go
│   │   │   ├── provider_test.go
│   │   │   └── thinking_test.go
│   │   ├── anthropic_messages/
│   │   │   ├── provider.go
│   │   │   └── provider_test.go
│   │   ├── antigravity_provider.go
│   │   ├── antigravity_provider_test.go
│   │   ├── azure/
│   │   │   ├── provider.go
│   │   │   └── provider_test.go
│   │   ├── claude_cli_provider.go
│   │   ├── claude_cli_provider_integration_test.go
│   │   ├── claude_cli_provider_test.go
│   │   ├── claude_provider.go
│   │   ├── claude_provider_test.go
│   │   ├── codex_cli_credentials.go
│   │   ├── codex_cli_credentials_test.go
│   │   ├── codex_cli_provider.go
│   │   ├── codex_cli_provider_integration_test.go
│   │   ├── codex_cli_provider_test.go
│   │   ├── codex_provider.go
│   │   ├── codex_provider_test.go
│   │   ├── common/
│   │   │   ├── common.go
│   │   │   └── common_test.go
│   │   ├── cooldown.go
│   │   ├── cooldown_test.go
│   │   ├── error_classifier.go
│   │   ├── error_classifier_test.go
│   │   ├── factory.go
│   │   ├── factory_provider.go
│   │   ├── factory_provider_test.go
│   │   ├── factory_test.go
│   │   ├── fallback.go
│   │   ├── fallback_multikey_test.go
│   │   ├── fallback_test.go
│   │   ├── github_copilot_provider.go
│   │   ├── http_provider.go
│   │   ├── legacy_provider.go
│   │   ├── model_ref.go
│   │   ├── model_ref_test.go
│   │   ├── openai_compat/
│   │   │   ├── provider.go
│   │   │   └── provider_test.go
│   │   ├── protocoltypes/
│   │   │   └── types.go
│   │   ├── tool_call_extract.go
│   │   ├── toolcall_utils.go
│   │   └── types.go
│   ├── routing/
│   │   ├── agent_id.go
│   │   ├── agent_id_test.go
│   │   ├── classifier.go
│   │   ├── features.go
│   │   ├── route.go
│   │   ├── route_test.go
│   │   ├── router.go
│   │   ├── router_test.go
│   │   ├── session_key.go
│   │   └── session_key_test.go
│   ├── session/
│   │   ├── jsonl_backend.go
│   │   ├── jsonl_backend_test.go
│   │   ├── manager.go
│   │   ├── manager_test.go
│   │   └── session_store.go
│   ├── skills/
│   │   ├── clawhub_registry.go
│   │   ├── clawhub_registry_test.go
│   │   ├── installer.go
│   │   ├── installer_test.go
│   │   ├── loader.go
│   │   ├── loader_test.go
│   │   ├── registry.go
│   │   ├── registry_test.go
│   │   ├── search_cache.go
│   │   └── search_cache_test.go
│   ├── state/
│   │   ├── state.go
│   │   └── state_test.go
│   ├── tools/
│   │   ├── base.go
│   │   ├── cron.go
│   │   ├── cron_test.go
│   │   ├── edit.go
│   │   ├── edit_test.go
│   │   ├── filesystem.go
│   │   ├── filesystem_test.go
│   │   ├── i2c.go
│   │   ├── i2c_linux.go
│   │   ├── i2c_other.go
│   │   ├── mcp_tool.go
│   │   ├── mcp_tool_test.go
│   │   ├── message.go
│   │   ├── message_test.go
│   │   ├── registry.go
│   │   ├── registry_test.go
│   │   ├── result.go
│   │   ├── result_test.go
│   │   ├── search_tool.go
│   │   ├── search_tools_test.go
│   │   ├── send_file.go
│   │   ├── send_file_test.go
│   │   ├── shell.go
│   │   ├── shell_process_unix.go
│   │   ├── shell_process_windows.go
│   │   ├── shell_test.go
│   │   ├── shell_timeout_unix_test.go
│   │   ├── skills_install.go
│   │   ├── skills_install_test.go
│   │   ├── skills_search.go
│   │   ├── skills_search_test.go
│   │   ├── spawn.go
│   │   ├── spawn_status.go
│   │   ├── spawn_status_test.go
│   │   ├── spawn_test.go
│   │   ├── spi.go
│   │   ├── spi_linux.go
│   │   ├── spi_other.go
│   │   ├── subagent.go
│   │   ├── subagent_tool_test.go
│   │   ├── toolloop.go
│   │   ├── types.go
│   │   ├── web.go
│   │   └── web_test.go
│   ├── utils/
│   │   ├── bm25.go
│   │   ├── bm25_test.go
│   │   ├── download.go
│   │   ├── http_client.go
│   │   ├── http_client_test.go
│   │   ├── http_retry.go
│   │   ├── http_retry_test.go
│   │   ├── markdown.go
│   │   ├── markdown_test.go
│   │   ├── media.go
│   │   ├── skills.go
│   │   ├── string.go
│   │   ├── string_test.go
│   │   └── zip.go
│   └── voice/
│       ├── transcriber.go
│       └── transcriber_test.go
├── scripts/
│   ├── build-macos-app.sh
│   ├── icon.icns
│   ├── setup.iss
│   ├── test-docker-mcp.sh
│   └── test-irc.sh
├── web/
│   ├── Makefile
│   ├── README.md
│   ├── backend/
│   │   ├── .gitignore
│   │   ├── api/
│   │   │   ├── channels.go
│   │   │   ├── config.go
│   │   │   ├── config_test.go
│   │   │   ├── gateway.go
│   │   │   ├── gateway_host.go
│   │   │   ├── gateway_host_test.go
│   │   │   ├── gateway_test.go
│   │   │   ├── launcher_config.go
│   │   │   ├── launcher_config_test.go
│   │   │   ├── log.go
│   │   │   ├── model_status.go
│   │   │   ├── models.go
│   │   │   ├── models_test.go
│   │   │   ├── oauth.go
│   │   │   ├── oauth_test.go
│   │   │   ├── pico.go
│   │   │   ├── pico_test.go
│   │   │   ├── router.go
│   │   │   ├── session.go
│   │   │   ├── session_test.go
│   │   │   ├── skills.go
│   │   │   ├── skills_test.go
│   │   │   ├── startup.go
│   │   │   ├── startup_test.go
│   │   │   ├── tools.go
│   │   │   └── tools_test.go
│   │   ├── app_runtime.go
│   │   ├── dist/
│   │   │   └── .gitkeep
│   │   ├── embed.go
│   │   ├── embed_test.go
│   │   ├── i18n.go
│   │   ├── launcherconfig/
│   │   │   ├── config.go
│   │   │   └── config_test.go
│   │   ├── main.go
│   │   ├── middleware/
│   │   │   ├── access_control.go
│   │   │   ├── access_control_test.go
│   │   │   └── middleware.go
│   │   ├── model/
│   │   │   └── status.go
│   │   ├── systray.go
│   │   ├── systray_unix.go
│   │   ├── systray_windows.go
│   │   ├── tray_stub_nocgo.go
│   │   ├── utils/
│   │   │   ├── banner.go
│   │   │   ├── onboard.go
│   │   │   ├── onboard_test.go
│   │   │   └── runtime.go
│   │   └── winres/
│   │       └── winres.json
│   ├── frontend/
│   │   ├── .editorconfig
│   │   ├── .gitignore
│   │   ├── .prettierignore
│   │   ├── components.json
│   │   ├── eslint.config.js
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── prettier.config.js
│   │   ├── public/
│   │   │   └── site.webmanifest
│   │   ├── scripts/
│   │   │   └── ensure-backend-gitkeep.cjs
│   │   ├── src/
│   │   │   ├── api/
│   │   │   │   ├── channels.ts
│   │   │   │   ├── gateway.ts
│   │   │   │   ├── models.ts
│   │   │   │   ├── oauth.ts
│   │   │   │   ├── pico.ts
│   │   │   │   ├── sessions.ts
│   │   │   │   ├── skills.ts
│   │   │   │   ├── system.ts
│   │   │   │   └── tools.ts
│   │   │   ├── components/
│   │   │   │   ├── app-header.tsx
│   │   │   │   ├── app-layout.tsx
│   │   │   │   ├── app-sidebar.tsx
│   │   │   │   ├── channels/
│   │   │   │   │   ├── channel-config-page.tsx
│   │   │   │   │   ├── channel-display-name.ts
│   │   │   │   │   └── channel-forms/
│   │   │   │   │       ├── discord-form.tsx
│   │   │   │   │       ├── feishu-form.tsx
│   │   │   │   │       ├── generic-form.tsx
│   │   │   │   │       ├── slack-form.tsx
│   │   │   │   │       └── telegram-form.tsx
│   │   │   │   ├── chat/
│   │   │   │   │   ├── assistant-message.tsx
│   │   │   │   │   ├── chat-composer.tsx
│   │   │   │   │   ├── chat-empty-state.tsx
│   │   │   │   │   ├── chat-page.tsx
│   │   │   │   │   ├── model-selector.tsx
│   │   │   │   │   ├── session-history-menu.tsx
│   │   │   │   │   ├── typing-indicator.tsx
│   │   │   │   │   └── user-message.tsx
│   │   │   │   ├── config/
│   │   │   │   │   ├── config-page.tsx
│   │   │   │   │   ├── config-sections.tsx
│   │   │   │   │   ├── form-model.ts
│   │   │   │   │   └── raw-config-page.tsx
│   │   │   │   ├── credentials/
│   │   │   │   │   ├── anthropic-credential-card.tsx
│   │   │   │   │   ├── antigravity-credential-card.tsx
│   │   │   │   │   ├── credential-card.tsx
│   │   │   │   │   ├── credentials-page.tsx
│   │   │   │   │   ├── device-code-sheet.tsx
│   │   │   │   │   ├── logout-confirm-dialog.tsx
│   │   │   │   │   ├── openai-credential-card.tsx
│   │   │   │   │   └── provider-status-line.tsx
│   │   │   │   ├── logs/
│   │   │   │   │   ├── ansi-log-line.tsx
│   │   │   │   │   ├── logs-page.tsx
│   │   │   │   │   └── logs-panel.tsx
│   │   │   │   ├── models/
│   │   │   │   │   ├── add-model-sheet.tsx
│   │   │   │   │   ├── delete-model-dialog.tsx
│   │   │   │   │   ├── edit-model-sheet.tsx
│   │   │   │   │   ├── model-card.tsx
│   │   │   │   │   ├── models-page.tsx
│   │   │   │   │   ├── provider-icon.tsx
│   │   │   │   │   ├── provider-label.ts
│   │   │   │   │   └── provider-section.tsx
│   │   │   │   ├── page-header.tsx
│   │   │   │   ├── secret-placeholder.ts
│   │   │   │   ├── shared-form.tsx
│   │   │   │   ├── skills/
│   │   │   │   │   └── skills-page.tsx
│   │   │   │   ├── tools/
│   │   │   │   │   └── tools-page.tsx
│   │   │   │   └── ui/
│   │   │   │       ├── alert-dialog.tsx
│   │   │   │       ├── button.tsx
│   │   │   │       ├── card.tsx
│   │   │   │       ├── collapsible.tsx
│   │   │   │       ├── dropdown-menu.tsx
│   │   │   │       ├── field.tsx
│   │   │   │       ├── input.tsx
│   │   │   │       ├── label.tsx
│   │   │   │       ├── scroll-area.tsx
│   │   │   │       ├── select.tsx
│   │   │   │       ├── separator.tsx
│   │   │   │       ├── sheet.tsx
│   │   │   │       ├── sidebar.tsx
│   │   │   │       ├── skeleton.tsx
│   │   │   │       ├── switch.tsx
│   │   │   │       ├── textarea.tsx
│   │   │   │       └── tooltip.tsx
│   │   │   ├── features/
│   │   │   │   └── chat/
│   │   │   │       ├── controller.ts
│   │   │   │       ├── history.ts
│   │   │   │       ├── protocol.ts
│   │   │   │       ├── state.ts
│   │   │   │       └── websocket.ts
│   │   │   ├── hooks/
│   │   │   │   ├── use-chat-models.ts
│   │   │   │   ├── use-credentials-page.ts
│   │   │   │   ├── use-gateway-logs.ts
│   │   │   │   ├── use-gateway.ts
│   │   │   │   ├── use-log-wrap-columns.ts
│   │   │   │   ├── use-mobile.ts
│   │   │   │   ├── use-pico-chat.ts
│   │   │   │   ├── use-session-history.ts
│   │   │   │   ├── use-sidebar-channels.ts
│   │   │   │   └── use-theme.ts
│   │   │   ├── i18n/
│   │   │   │   ├── index.ts
│   │   │   │   └── locales/
│   │   │   │       ├── en.json
│   │   │   │       └── zh.json
│   │   │   ├── index.css
│   │   │   ├── lib/
│   │   │   │   ├── ansi-log.ts
│   │   │   │   └── utils.ts
│   │   │   ├── main.tsx
│   │   │   ├── routeTree.gen.ts
│   │   │   ├── routes/
│   │   │   │   ├── __root.tsx
│   │   │   │   ├── agent/
│   │   │   │   │   ├── skills.tsx
│   │   │   │   │   └── tools.tsx
│   │   │   │   ├── agent.tsx
│   │   │   │   ├── channels/
│   │   │   │   │   ├── $name.tsx
│   │   │   │   │   └── route.tsx
│   │   │   │   ├── config.raw.tsx
│   │   │   │   ├── config.tsx
│   │   │   │   ├── credentials.tsx
│   │   │   │   ├── index.tsx
│   │   │   │   ├── logs.tsx
│   │   │   │   └── models.tsx
│   │   │   └── store/
│   │   │       ├── chat.ts
│   │   │       ├── gateway.ts
│   │   │       └── index.ts
│   │   ├── tsconfig.app.json
│   │   ├── tsconfig.json
│   │   ├── tsconfig.node.json
│   │   └── vite.config.ts
│   └── picoclaw-launcher.desktop
└── workspace/
    ├── AGENTS.md
    ├── IDENTITY.md
    ├── SOUL.md
    ├── USER.md
    ├── memory/
    │   └── MEMORY.md
    └── skills/
        ├── github/
        │   └── SKILL.md
        ├── hardware/
        │   ├── SKILL.md
        │   └── references/
        │       ├── board-pinout.md
        │       └── common-devices.md
        ├── skill-creator/
        │   └── SKILL.md
        ├── summarize/
        │   └── SKILL.md
        ├── tmux/
        │   ├── SKILL.md
        │   └── scripts/
        │       ├── find-sessions.sh
        │       └── wait-for-text.sh
        └── weather/
            └── SKILL.md

================================================
FILE CONTENTS
================================================

================================================
FILE: .dockerignore
================================================
.git
.gitignore
build/
.picoclaw/
config/
.env
.env.example
*.md
LICENSE
assets/


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Report a bug or unexpected behavior
title: "[BUG]"
labels: bug
assignees: ''

---

## Quick Summary

##  Environment & Tools
- **PicoClaw Version:** (e.g., v0.1.2 or commit hash)
- **Go Version:** (e.g., go 1.22)
- **AI Model & Provider:** (e.g., GPT-4o via OpenAI / DeepSeek via SiliconFlow)
- **Operating System:** (e.g., Ubuntu 22.04 / macOS / Android Termux)
- **Channels:** (e.g., Discord, Telegram, Feishu, ...)

## 📸 Steps to Reproduce
1. 
2. 
3. 

## ❌ Actual Behavior

## ✅ Expected Behavior

## 💬 Additional Context


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest a new idea or improvement
title: "[Feature]"
labels: enhancement
assignees: ''

---

## 🎯 The Goal / Use Case

## 💡 Proposed Solution

## 🛠 Potential Implementation (Optional)

## 🚦 Impact & Roadmap Alignment
- [ ] This is a Core Feature
- [ ] This is a Nice-to-Have / Enhancement
- [ ] This aligns with the current Roadmap

## 🔄 Alternatives Considered

## 💬 Additional Context


================================================
FILE: .github/ISSUE_TEMPLATE/general-task---todo.md
================================================
---
name: General Task / Todo
about: A specific piece of work like doc, refactoring, or maintenance.
title: "[Task]"
labels: ''
assignees: ''

---

## 📝 Objective

## 📋 To-Do List
- [ ] Step 1
- [ ] Step 2
- [ ] Step 3

## 🎯 Definition of Done (Acceptance Criteria)
- [ ] Documentation is updated in the README/docs folder.
- [ ] Code follows project linting standards.
- [ ] (If applicable) Basic tests pass.

## 💡 Context / Motivation

## 🔗 Related Issues / PRs
- Fixes #
- Relates to #


================================================
FILE: .github/dependabot.yml
================================================
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"

================================================
FILE: .github/pull_request_template.md
================================================
## 📝 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.

================================================
FILE: .github/workflows/build.yml
================================================
name: build

on:
  push:
    branches: [ "main" ]

jobs:
  build:
    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

      - name: Build
        run: make build-all


================================================
FILE: .github/workflows/docker-build.yml
================================================
name: 🐳 Build & Push Docker Image

on:
  workflow_call:
    inputs:
      tag:
        description: "Release tag"
        required: true
        type: string

env:
  GHCR_REGISTRY: ghcr.io
  GHCR_IMAGE_NAME: ${{ github.repository_owner }}/picoclaw
  DOCKERHUB_REGISTRY: docker.io
  DOCKERHUB_IMAGE_NAME: ${{ vars.DOCKERHUB_REPOSITORY }}

jobs:
  build:
    name: 🏗️ Build Docker Image
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      # ── Checkout ──────────────────────────────
      - name: 📥 Checkout repository
        uses: actions/checkout@v6
        with:
          ref: ${{ inputs.tag }}

      # ── Docker Buildx ─────────────────────────
      - name: 🔧 Set up Docker Buildx
        uses: docker/setup-buildx-action@v4

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

      # ── Login to Docker Hub ────────────────────
      - name: 🔑 Login to Docker Hub
        uses: docker/login-action@v4
        with:
          registry: ${{ env.DOCKERHUB_REGISTRY }}
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      # ── Metadata (tags & labels) ──────────────
      - name: 🏷️ Prepare image tags
        id: tags
        shell: bash
        run: |
          tag="${{ inputs.tag }}"
          echo "ghcr_tag=${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}:${tag}" >> "$GITHUB_OUTPUT"
          echo "ghcr_latest=${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}:latest" >> "$GITHUB_OUTPUT"
          echo "dockerhub_tag=${{ env.DOCKERHUB_REGISTRY }}/${{ env.DOCKERHUB_IMAGE_NAME }}:${tag}" >> "$GITHUB_OUTPUT"
          echo "dockerhub_latest=${{ env.DOCKERHUB_REGISTRY }}/${{ env.DOCKERHUB_IMAGE_NAME }}:latest" >> "$GITHUB_OUTPUT"

      # ── Build & Push ──────────────────────────
      - name: 🚀 Build and push Docker image
        uses: docker/build-push-action@v7
        with:
          context: .
          push: true
          tags: |
            ${{ steps.tags.outputs.ghcr_tag }}
            ${{ steps.tags.outputs.ghcr_latest }}
            ${{ steps.tags.outputs.dockerhub_tag }}
            ${{ steps.tags.outputs.dockerhub_latest }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          platforms: linux/amd64,linux/arm64,linux/riscv64


================================================
FILE: .github/workflows/nightly.yml
================================================
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 Node.js
        uses: actions/setup-node@v6
        with:
          node-version: 22

      - name: Setup pnpm
        run: corepack enable && corepack prepare pnpm@latest --activate

      - 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: 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 }}
          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; 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[@]}"



================================================
FILE: .github/workflows/pr.yml
================================================
name: PR

on:
  pull_request: { }

jobs:
  lint:
    name: Linter
    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

      - name: Run go generate
        run: go generate ./...

      - name: Golangci Lint
        uses: golangci/golangci-lint-action@v9
        with:
          version: v2.10.1

  vuln_check:
    name: Security Check
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v6
        with:
          persist-credentials: false

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

      - name: Run Govulncheck
        uses: golang/govulncheck-action@v1
        with:
          go-package: ./...

  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

      - name: Run go generate
        run: go generate ./...

      - name: Run go test
        run: go test ./...


================================================
FILE: .github/workflows/release.yml
================================================
name: Create Tag and Release

on:
  workflow_dispatch:
    inputs:
      tag:
        description: "Release tag (required, e.g. v0.2.0)"
        required: true
        type: string
      prerelease:
        description: "Mark as pre-release"
        required: false
        type: boolean
        default: false
      draft:
        description: "Create as draft"
        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@v6
        with:
          fetch-depth: 0

      - name: Create and push tag
        shell: bash
        env:
          RELEASE_TAG: ${{ inputs.tag }}
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git tag -a "$RELEASE_TAG" -m "Release $RELEASE_TAG"
          git push origin "$RELEASE_TAG"

  release:
    name: GoReleaser Release
    needs: create-tag
    runs-on: ubuntu-latest
    permissions:
      contents: write
      packages: write
    steps:
      - name: Checkout tag
        uses: actions/checkout@v6
        with:
          fetch-depth: 0
          ref: ${{ inputs.tag }}

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

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

      - name: Setup pnpm
        run: corepack enable && corepack prepare pnpm@latest --activate

      - 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: 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 }}
          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
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          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


================================================
FILE: .github/workflows/upload-tos.yml
================================================
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


================================================
FILE: .gitignore
================================================
# Binaries
# Go build artifacts
bin/
build/
*.exe
*.dll
*.so
*.dylib
*.test
*.out
/picoclaw
/picoclaw-test
cmd/**/workspace

# Picoclaw specific

# PicoClaw
.picoclaw/
config.json
sessions/
build/

# Coverage

# Secrets & Config (keep templates, ignore actual secrets)
.env
config/config.json

# Test
coverage.txt
coverage.html

# OS
.DS_Store

# Ralph workspace
ralph/
.ralph/
tasks/

# Plans
docs/plans/

# Editors
.vscode/
.idea/

# Added by goreleaser init:
dist/
*.vite/

# Windows Application Icon/Resource
*.syso

# Test telegram integration
cmd/telegram/

# Keep embedded backend dist directory placeholder in VCS
!web/backend/dist/
web/backend/dist/*
!web/backend/dist/.gitkeep


================================================
FILE: .golangci.yaml
================================================
version: "2"

linters:
  default: all
  disable:
    # TODO: Tweak for current project needs
    - containedctx
    - cyclop
    - depguard
    - dupword
    - err113
    - exhaustruct
    - funcorder
    - gochecknoglobals
    - 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:
    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


================================================
FILE: .goreleaser.yaml
================================================
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
version: 2

before:
  hooks:
    - go mod tidy
    - go generate ./...
    - sh -c 'cd web/frontend && pnpm install && pnpm build:backend'
    - go install github.com/tc-hib/go-winres@latest
    - go-winres make --in web/backend/winres/winres.json --out web/backend/rsrc --product-version={{ .Version }} --file-version={{ .Version }}

builds:
  - id: picoclaw
    env:
      - CGO_ENABLED=0
    tags:
      - 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={{ .Env.GOVERSION }}
    goos:
      - linux
      - windows
      - darwin
      - freebsd
      - netbsd
    goarch:
      - amd64
      - arm64
      - riscv64
      - 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:
      - stdjson
    ldflags:
      - -s -w
    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

  - id: picoclaw-launcher-tui
    binary: picoclaw-launcher-tui
    env:
      - CGO_ENABLED=0
    tags:
      - stdjson
    ldflags:
      - -s -w
    goos:
      - linux
      - windows
      - darwin
      - freebsd
      - netbsd
    goarch:
      - amd64
      - arm64
      - riscv64
      - loong64
      - arm
      - s390x
      - mipsle
    goarm:
      - "6"
      - "7"
    gomips:
      - softfloat
    main: ./cmd/picoclaw-launcher-tui
    ignore:
      - goos: windows
        goarch: arm
      - goos: netbsd
        goarch: s390x
      - goos: netbsd
        goarch: mips64
      - goos: netbsd
        goarch: arm

dockers_v2:
  - id: picoclaw
    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 }}'
    tags:
      - '{{ 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
      - picoclaw-launcher-tui
    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
        - picoclaw-launcher-tui
      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`.
    name_template: >-
      {{ .ProjectName }}_
      {{- title .Os }}_
      {{- if eq .Arch "amd64" }}x86_64
      {{- else if eq .Arch "386" }}i386
      {{- else }}{{ .Arch }}{{ end }}
      {{- if .Arm }}v{{ .Arm }}{{ end }}
    # use zip for windows archives
    format_overrides:
      - goos: windows
        formats: [zip]

nfpms:
  - id: picoclaw
    ids:
      - picoclaw
      - picoclaw-launcher
      - picoclaw-launcher-tui
    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:
    exclude:
      - "^docs:"
      - "^test:"

# upx:
#    - enabled: true
#      compress: best
#      lzma: true

release:
  disable: '{{ isEnvSet "NIGHTLY_BUILD" }}'
  footer: >-

    ---

    Released by [GoReleaser](https://github.com/goreleaser/goreleaser).


================================================
FILE: CONTRIBUTING.md
================================================
# 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.

---

## 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
```

### Running Tests

```bash
make test                                    # Run all tests
go test -run TestName -v ./pkg/session/      # Run a single test
go test -bench=. -benchmem -run='^$' ./...  # Run benchmarks
```

### Code Style

```bash
make fmt   # Format code
make vet   # Static analysis
make lint  # Full linter run
```

All CI checks must pass before a PR can be merged. Run `make check` locally before pushing to catch issues early.

---

## 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 https://www.conventionalcommits.org/zh-hans/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!


================================================
FILE: CONTRIBUTING.zh.md
================================================
# 参与贡献 PicoClaw

感谢你对 PicoClaw 的关注!本项目是一个社区驱动的开源项目,目标是构建 轻量灵活,人人可用 的个人AI助手。我们欢迎一切形式的贡献:Bug 修复、新功能、文档、翻译和测试。

PicoClaw 本身在很大程度上是借助 AI 辅助开发的——我们拥抱这种方式,并围绕它构建了贡献流程。

## 目录

- [行为准则](#行为准则)
- [贡献方式](#贡献方式)
- [快速开始](#快速开始)
- [开发环境配置](#开发环境配置)
- [提交修改](#提交修改)
- [AI 辅助贡献](#ai-辅助贡献)
- [Pull Request 流程](#pull-request-流程)
- [分支策略](#分支策略)
- [代码审查](#代码审查)
- [沟通渠道](#沟通渠道)

---

## 行为准则

我们致力于维护一个友好、互相尊重的社区环境。请保持善意、建设性的态度,并善意地理解他人。任何形式的骚扰或歧视均不被接受。

---

## 贡献方式

- **Bug 反馈** — 使用 Bug 报告模板提交 Issue。
- **功能建议** — 使用功能请求模板提交 Issue,建议在开始实现前先进行讨论。
- **代码贡献** — 修复 Bug 或实现新功能,参见下方工作流程。
- **文档改进** — 完善 README、文档、代码注释或翻译。
- **测试与验证** — 在新硬件、新渠道或新 LLM 提供商上运行 PicoClaw 并反馈结果。

对于较大的新功能,请先提交 Issue 讨论设计方案,再动手写代码。这能避免无效投入,也确保与项目方向保持一致。

---

## 快速开始

1. 在 GitHub 上 **Fork** 本仓库。
2. 将你的 Fork **克隆**到本地:
   ```bash
   git clone https://github.com/<你的用户名>/picoclaw.git
   cd picoclaw
   ```
3. 添加上游远程仓库:
   ```bash
   git remote add upstream https://github.com/sipeed/picoclaw.git
   ```

---

## 开发环境配置

### 前置依赖

- Go 1.25 或更高版本
- `make`

### 构建

```bash
make build       # 构建二进制文件(会先执行 go generate)
make generate    # 仅执行 go generate
make check       # 完整的提交前检查:deps + fmt + vet + test
```

### 运行测试

```bash
make test                                    # 运行所有测试
go test -run TestName -v ./pkg/session/      # 运行单个测试
go test -bench=. -benchmem -run='^$' ./...  # 运行基准测试
```

### 代码风格

```bash
make fmt   # 格式化代码
make vet   # 静态分析
make lint  # 完整的 lint 检查
```

所有 CI 检查通过后 PR 才能被合并。推送代码前请先在本地运行 `make check`,提前发现问题。

---

## 提交修改

### 分支管理

始终从 `main` 分支切出,并在 PR 中以 `main` 为目标分支。不要直接向 `main` 或任何 `release/*` 分支推送代码:

```bash
git checkout main
git pull upstream main
git checkout -b 你的功能分支名
```

请使用描述性的分支名,例如:`fix/telegram-timeout`、`feat/ollama-provider`、`docs/contributing-guide`。

### Commit 规范

- 使用英文撰写清晰、简洁的 commit 信息。
- 使用祈使句:写 "Add retry logic",而不是 "Added retry logic"。
- 有关联 Issue 时请引用:`Fix session leak (#123)`。
- 保持 commit 专注,每个 commit 只做一件事。
- 对于小的清理或拼写修正,提 PR 前请将其合并为一个 commit。
- 按照 https://www.conventionalcommits.org/zh-hans/v1.0.0/ 规范来撰写

### 保持与上游同步

提 PR 前,请将你的分支变基到上游 `main`:

```bash
git fetch upstream
git rebase upstream/main
```

---

## AI 辅助贡献

PicoClaw 在很大程度上借助 AI 辅助开发,我们完全拥抱这种开发方式。但贡献者必须清楚地了解自己在使用 AI 工具时所承担的责任。

### 必须披露 AI 使用情况

每个 PR 都必须通过 PR 模板中的 **🤖 AI 代码生成** 部分披露 AI 参与情况,共分三个级别:

| 级别 | 说明 |
|---|---|
| 🤖 完全由 AI 生成 | AI 编写代码,贡献者负责审查和验证 |
| 🛠️ 主要由 AI 生成 | AI 起草,贡献者做了较大修改 |
| 👨‍💻 主要由人工编写 | 贡献者主导,AI 仅提供辅助或未使用 AI |

我们期望你诚实填写。三种级别均可接受,没有任何歧视——重要的是贡献的质量。

### 你对提交的代码负全责

使用 AI 生成代码并不能减轻你作为贡献者的责任。在提交含有 AI 生成代码的 PR 之前,你必须:

- **逐行阅读并理解**生成的代码。
- **在真实环境中测试**(参见 PR 模板中的测试环境部分)。
- **检查安全问题** — AI 模型可能生成存在安全隐患的代码(如路径穿越、注入攻击、凭据泄露等),请仔细审查。
- **验证正确性** — AI 生成的逻辑可能听起来合理但实际上是错误的,请验证行为,而不仅仅是语法。

如果明显可以看出贡献者没有阅读或测试 AI 生成的代码,该 PR 将被直接关闭,不予审查。

### AI 生成代码的质量标准

AI 生成的代码与人工编写的代码遵循**相同的质量要求**:

- 必须通过所有 CI 检查(`make check`)。
- 必须符合 Go 惯用写法,并与现有代码库的风格保持一致。
- 不得引入不必要的抽象、死代码或过度设计。
- 须在适当的地方包含或更新测试。

### 安全审查

AI 生成的代码需要格外仔细的安全审查。请特别关注以下方面:

- 文件路径处理与沙箱逃逸(项目历史中的 commit `244eb0b` 就是真实案例)
- channel 处理器和 tool 实现中的外部输入校验
- 凭据或密钥的处理
- 命令执行(`exec.Command`、shell 调用等)

如果你不确定某段 AI 生成代码是否安全,请在 PR 中说明——审查者会帮助判断。

---

## Pull Request 流程

### 提 PR 前的检查

- [ ] 在本地运行 `make check` 并确认通过。
- [ ] 完整填写 PR 模板,包括 AI 披露部分。
- [ ] 在 PR 描述中关联相关 Issue。
- [ ] 保持 PR 专注,避免将不相关的修改混在一起。

### PR 模板各部分说明

PR 模板要求填写:

- **描述** — 这个改动做了什么,为什么要做?
- **变更类型** — Bug 修复、新功能、文档或重构。
- **AI 代码生成** — AI 参与情况披露(必填)。
- **关联 Issue** — 此 PR 解决的 Issue 链接。
- **技术背景** — 参考链接和设计理由(纯文档类 PR 可跳过)。
- **测试环境** — 用于测试的硬件、操作系统、模型/提供商和渠道。
- **验证证据** — 可选的日志或截图,用于证明改动有效。
- **检查清单** — 自我审查确认。

### PR 规模

请尽量提交小而易于审查的 PR。一个涉及 5 个文件共 200 行改动的 PR,远比涉及 30 个文件共 2000 行改动的 PR 容易审查。如果你的功能较大,可以考虑将其拆分为一系列逻辑完整的小 PR。

---

## 分支策略

### 长期分支

- **`main`** — 活跃开发分支。所有功能 PR 均以 `main` 为目标。该分支受保护:禁止直接推送,合并前必须获得至少一名维护者的批准。
- **`release/x.y`** — 稳定发布分支,在某个版本准备发布时从 `main` 切出。这些分支的保护级别高于 `main`。

### 合并到 `main` 的前提条件

PR 必须同时满足以下所有条件,才能被合并:

1. **CI 全部通过** — 所有 GitHub Actions 工作流(lint、test、build)均为绿色。
2. **获得审查者批准** — 至少一名维护者已批准该 PR。
3. **无未解决的审查意见** — 所有审查讨论线程均已关闭。
4. **PR 模板填写完整** — 包括 AI 披露和测试环境信息。

### 谁可以合并

只有维护者才能合并 PR。贡献者不能合并自己的 PR,即使拥有写权限也不行。

### 合并策略

为保持 `main` 历史清晰可读,我们对大多数 PR 使用 **Squash Merge**。每个合并的 PR 变为一个包含 PR 编号的单独 commit,例如:

```
feat: Add Ollama provider support (#491)
```

如果一个 PR 包含多个独立、结构清晰、能讲述完整故事的 commit,维护者可视情况使用普通 merge。

### Release 分支

当某个版本准备就绪时,维护者会从 `main` 切出 `release/x.y` 分支。此后:

- **新功能不会被回溯(backport)。** Release 分支切出后,不再接收任何新功能。
- **安全修复和关键 Bug 修复会被 cherry-pick 进来。** 若 `main` 上的某个修复属于安全漏洞、数据丢失或崩溃类问题,维护者会将相关 commit cherry-pick 到受影响的 `release/x.y` 分支,并发布补丁版本。

如果你认为 `main` 上的某个修复应该被回溯到某个 release 分支,请在 PR 描述中注明,或单独开一个 Issue 说明。最终决定由维护者做出。

Release 分支的保护级别高于 `main`,在任何情况下均不允许直接推送。

---

## 代码审查

### 对贡献者的建议

- 在合理时间内回复审查意见。如果需要更多时间,请告知。
- 更新 PR 以响应反馈时,简要说明改动内容(例如:"按建议改用了 `sync.RWMutex`")。
- 如果你不同意某条反馈,请礼貌地阐述你的理由——审查者也可能有判断失误的时候。
- 审查开始后请不要 force push——这会让审查者难以追踪变化。请使用额外的 commit,维护者在合并时会进行 squash。

### 对审查者的建议

审查重点:

1. **正确性** — 代码是否实现了其声称的功能?是否存在边界情况?
2. **安全性** — 对 AI 生成代码、tool 实现和 channel 处理器尤其需要关注。
3. **架构** — 实现方式是否与现有设计一致?
4. **简洁性** — 是否有更简单的方案?是否引入了不必要的复杂度?
5. **测试** — 改动是否有测试覆盖?现有测试是否仍然有意义?

请给出建设性且具体的反馈。"如果两个 goroutine 同时调用这个函数可能会有竞态条件,建议在这里加一个 mutex" 远比 "这里看起来有问题" 更有帮助。

### 审查者列表
提交对应PR后,可以参考下表联系对应的审查人员沟通

|Function| Reviewer|
|---     |---      |
|Provider|@yinwm   |
|Channel |@yinwm/@alexhoshina   |
|Agent   |@lxowalle/@Zhaoyikaiii|
|Tools   |@lxowalle|
|SKill   ||
|MCP     ||
|Optimization|@lxowalle|
|Security||
|AI CI   |@imguoguo|
|UX      ||
|Document||



---

## 沟通渠道

- **GitHub Issues** — Bug 报告、功能建议、设计讨论。
- **GitHub Discussions** — 一般性问题、想法交流、社区讨论。
- **Pull Request 评论** — 与具体代码相关的反馈。
- **Wechat&Discord** — 当你有至少一个已合并的PR后,我们会邀请你加入开发者交流群

有疑问时,请先开 Issue 讨论,再动手写代码。这几乎没有成本,却能避免大量无效投入。

---

## 关于本项目的 AI 驱动起源

PicoClaw 的架构在人工监督下,经由 AI 辅助完成了大量设计和实现工作。如果你发现某处看起来奇怪或过度设计,这可能是该过程留下的痕迹——欢迎提 Issue 讨论。

我们相信,负责任地使用 AI 辅助开发能产生优秀的成果。我们同样相信,人类必须对自己提交的内容负责。这两点并不矛盾。

感谢你的贡献!


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2026 PicoClaw contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
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.


================================================
FILE: Makefile
================================================
.PHONY: all build install uninstall clean help test

# Build variables
BINARY_NAME=picoclaw
BUILD_DIR=build
CMD_DIR=cmd/$(BINARY_NAME)
MAIN_GO=$(CMD_DIR)/main.go

# 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}')
CONFIG_PKG=github.com/sipeed/picoclaw/pkg/config
LDFLAGS=-X $(CONFIG_PKG).Version=$(VERSION) -X $(CONFIG_PKG).GitCommit=$(GIT_COMMIT) -X $(CONFIG_PKG).BuildTime=$(BUILD_TIME) -X $(CONFIG_PKG).GoVersion=$(GO_VERSION) -s -w

# Go variables
GO?=CGO_ENABLED=0 go
WEB_GO?=$(GO)
GOFLAGS?=-v -tags stdjson

# 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

# 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
WORKSPACE_DIR?=$(PICOCLAW_HOME)/workspace
WORKSPACE_SKILLS_DIR=$(WORKSPACE_DIR)/skills
BUILTIN_SKILLS_DIR=$(CURDIR)/skills

# OS detection
UNAME_S:=$(shell uname -s)
UNAME_M:=$(shell uname -m)

# Platform-specific settings
ifeq ($(UNAME_S),Linux)
	PLATFORM=linux
	ifeq ($(UNAME_M),x86_64)
		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_ENABLED=1 go
	ifeq ($(UNAME_M),x86_64)
		ARCH=amd64
	else ifeq ($(UNAME_M),arm64)
		ARCH=arm64
	else
		ARCH=$(UNAME_M)
	endif
else
	PLATFORM=$(UNAME_S)
	ARCH=$(UNAME_M)
endif

BINARY_PATH=$(BUILD_DIR)/$(BINARY_NAME)-$(PLATFORM)-$(ARCH)

# Default target
all: build

## generate: Run generate
generate:
	@echo "Run generate..."
	@rm -r ./$(CMD_DIR)/workspace 2>/dev/null || true
	@$(GO) generate ./...
	@echo "Run generate complete"

## build: Build the picoclaw binary for current platform
build: generate
	@echo "Building $(BINARY_NAME) for $(PLATFORM)/$(ARCH)..."
	@mkdir -p $(BUILD_DIR)
	@$(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BINARY_PATH) ./$(CMD_DIR)
	@echo "Build complete: $(BINARY_PATH)"
	@ln -sf $(BINARY_NAME)-$(PLATFORM)-$(ARCH) $(BUILD_DIR)/$(BINARY_NAME)

## build-launcher: Build the picoclaw-launcher (web console) binary
build-launcher:
	@echo "Building picoclaw-launcher for $(PLATFORM)/$(ARCH)..."
	@mkdir -p $(BUILD_DIR)
	@if [ ! -f web/backend/dist/index.html ]; then \
		echo "Building frontend..."; \
		cd web/frontend && pnpm install && pnpm build:backend; \
	fi
	@$(WEB_GO) build $(GOFLAGS) -o $(BUILD_DIR)/picoclaw-launcher-$(PLATFORM)-$(ARCH) ./web/backend
	@ln -sf picoclaw-launcher-$(PLATFORM)-$(ARCH) $(BUILD_DIR)/picoclaw-launcher
	@echo "Build complete: $(BUILD_DIR)/picoclaw-launcher"

## 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 whatsapp_native -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 ./$(CMD_DIR)
	GOOS=linux GOARCH=arm GOARM=7 $(GO) build -tags whatsapp_native -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm ./$(CMD_DIR)
	GOOS=linux GOARCH=arm64 $(GO) build -tags whatsapp_native -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 ./$(CMD_DIR)
	GOOS=linux GOARCH=loong64 $(GO) build -tags whatsapp_native -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-linux-loong64 ./$(CMD_DIR)
	GOOS=linux GOARCH=riscv64 $(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 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 whatsapp_native -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-arm64 ./$(CMD_DIR)
	GOOS=windows GOARCH=amd64 $(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 -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 -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 -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-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 picoclaw for all platforms
build-all: generate
	@echo "Building for multiple platforms..."
	@mkdir -p $(BUILD_DIR)
	GOOS=linux GOARCH=amd64 $(GO) build -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 ./$(CMD_DIR)
	GOOS=linux GOARCH=arm GOARM=7 $(GO) build -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm ./$(CMD_DIR)
	GOOS=linux GOARCH=arm64 $(GO) build -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 ./$(CMD_DIR)
	GOOS=linux GOARCH=loong64 $(GO) build -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-linux-loong64 ./$(CMD_DIR)
	GOOS=linux GOARCH=riscv64 $(GO) build -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-linux-riscv64 ./$(CMD_DIR)
	GOOS=linux GOARCH=mipsle GOMIPS=softfloat $(GO) build -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 -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-linux-armv7 ./$(CMD_DIR)
	GOOS=darwin GOARCH=arm64 $(GO) build -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-arm64 ./$(CMD_DIR)
	GOOS=windows GOARCH=amd64 $(GO) build -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe ./$(CMD_DIR)
	GOOS=netbsd GOARCH=amd64 $(GO) build -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-netbsd-amd64 ./$(CMD_DIR)
	GOOS=netbsd GOARCH=arm64 $(GO) build -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-netbsd-arm64 ./$(CMD_DIR)
	@echo "All builds complete"

## install: Install picoclaw to system and copy builtin skills
install: build
	@echo "Installing $(BINARY_NAME)..."
	@mkdir -p $(INSTALL_BIN_DIR)
	# 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!"

## uninstall: Remove picoclaw from system
uninstall:
	@echo "Uninstalling $(BINARY_NAME)..."
	@rm -f $(INSTALL_BIN_DIR)/$(BINARY_NAME)
	@echo "Removed binary from $(INSTALL_BIN_DIR)/$(BINARY_NAME)"
	@echo "Note: Only the executable file has been deleted."
	@echo "If you need to delete all configurations (config.json, workspace, etc.), run 'make uninstall-all'"

## uninstall-all: Remove picoclaw and all data
uninstall-all:
	@echo "Removing workspace and skills..."
	@rm -rf $(PICOCLAW_HOME)
	@echo "Removed workspace: $(PICOCLAW_HOME)"
	@echo "Complete uninstallation done!"

## clean: Remove build artifacts
clean:
	@echo "Cleaning build artifacts..."
	@rm -rf $(BUILD_DIR)
	@echo "Clean complete"

## vet: Run go vet for static analysis
vet: generate
	@packages="$$(go list ./...)" && \
		$(GO) vet $$(printf '%s\n' "$$packages" | grep -v '^github.com/sipeed/picoclaw/web/')
	@cd web/backend && $(WEB_GO) vet ./...

## test: Test Go code
test: generate
	@$(GO) test $$(go list ./... | grep -v github.com/sipeed/picoclaw/web/)
	@cd web && make test

## fmt: Format Go code
fmt:
	@$(GOLANGCI_LINT) fmt

## lint: Run linters
lint:
	@$(GOLANGCI_LINT) run

## fix: Fix linting issues
fix:
	@$(GOLANGCI_LINT) run --fix

## deps: Download dependencies
deps:
	@$(GO) mod download
	@$(GO) mod verify

## update-deps: Update dependencies
update-deps:
	@$(GO) get -u ./...
	@$(GO) mod tidy

## check: Run vet, fmt, and verify dependencies
check: deps fmt vet test

## 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:
	@echo "Building macOS .app bundle..."
	@if [ "$(UNAME_S)" != "Darwin" ]; then \
		echo "Error: This target is only available on macOS"; \
		exit 1; \
	fi
	@cd web && $(MAKE) build && cd ..
	@./scripts/build-macos-app.sh $(BINARY_NAME)-$(PLATFORM)-$(ARCH)
	@echo "macOS .app bundle created: $(BUILD_DIR)/PicoClaw.app"

## help: Show this help message
help:
	@echo "picoclaw Makefile"
	@echo ""
	@echo "Usage:"
	@echo "  make [target]"
	@echo ""
	@echo "Targets:"
	@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)"
	@echo "  WORKSPACE_DIR           # Workspace directory (default: ~/.picoclaw/workspace)"
	@echo "  VERSION                 # Version string (default: git describe)"
	@echo ""
	@echo "Current Configuration:"
	@echo "  Platform: $(PLATFORM)/$(ARCH)"
	@echo "  Binary: $(BINARY_PATH)"
	@echo "  Install Prefix: $(INSTALL_PREFIX)"
	@echo "  Workspace: $(WORKSPACE_DIR)"


================================================
FILE: README.fr.md
================================================
<div align="center">
  <img src="assets/logo.webp" alt="PicoClaw" width="512">

  <h1>PicoClaw : Assistant IA Ultra-Efficace en Go</h1>

  <h3>Matériel à $10 · <10 Mo de RAM · Démarrage en <1s · 皮皮虾,我们走!</h3>
  <p>
    <img src="https://img.shields.io/badge/Go-1.25+-00ADD8?style=flat&logo=go&logoColor=white" alt="Go">
    <img src="https://img.shields.io/badge/Arch-x86__64%2C%20ARM64%2C%20MIPS%2C%20RISC--V%2C%20LoongArch-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://docs.picoclaw.io/"><img src="https://img.shields.io/badge/Docs-Official-007acc?style=flat&logo=read-the-docs&logoColor=white" alt="Docs"></a>
    <a href="https://deepwiki.com/sipeed/picoclaw"><img src="https://img.shields.io/badge/Wiki-DeepWiki-FFA500?style=flat&logo=wikipedia&logoColor=white" alt="Wiki"></a>
    <br>
    <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>
    <a href="./assets/wechat.png"><img src="https://img.shields.io/badge/WeChat-Group-41d56b?style=flat&logo=wechat&logoColor=white"></a>
    <a href="https://discord.gg/V4sAZ9XWpN"><img src="https://img.shields.io/badge/Discord-Community-4c60eb?style=flat&logo=discord&logoColor=white" alt="Discord"></a>
  </p>

[中文](README.zh.md) | [日本語](README.ja.md) | [Português](README.pt-br.md) | [Tiếng Việt](README.vi.md) | **Français** | [Italiano](README.it.md) | [Bahasa Indonesia](README.id.md) | [English](README.md)

</div>

---

> **PicoClaw** est un projet open-source indépendant initié par [Sipeed](https://sipeed.com). Il est entièrement écrit en **Go** — ce n'est pas un fork d'OpenClaw, de NanoBot ou de tout autre projet.

🦐 **PicoClaw** est un assistant personnel IA ultra-léger inspiré de [NanoBot](https://github.com/HKUDS/nanobot), entièrement réécrit en **Go** via un processus d'auto-amorçage (self-bootstrapping) — où l'agent IA lui-même a piloté l'intégralité de la migration architecturale et de l'optimisation du code.

⚡️ **Extrêmement léger :** Fonctionne sur du matériel à seulement **$10** avec **<10 Mo** de RAM. C'est 99% de mémoire en moins qu'OpenClaw et 98% moins cher qu'un Mac mini !

<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]
> **🚨 SÉCURITÉ & CANAUX OFFICIELS**
>
> * **PAS DE CRYPTO :** PicoClaw n'a **AUCUN** token/jeton officiel. Toute annonce sur `pump.fun` ou d'autres plateformes de trading est une **ARNAQUE**.
>
> * **DOMAINE OFFICIEL :** Le **SEUL** site officiel est **[picoclaw.io](https://picoclaw.io)**, et le site de l'entreprise est **[sipeed.com](https://sipeed.com)**.
> * **Attention :** De nombreux domaines `.ai/.org/.com/.net/...` sont enregistrés par des tiers.
> * **Attention :** PicoClaw est en phase de développement précoce et peut présenter des problèmes de sécurité réseau non résolus. Ne déployez pas en environnement de production avant la version v1.0.
> * **Note :** PicoClaw a récemment fusionné de nombreuses PR, ce qui peut entraîner une empreinte mémoire plus importante (10–20 Mo) dans les dernières versions. Nous prévoyons de prioriser l'optimisation des ressources dès que l'ensemble des fonctionnalités sera stabilisé.

## 📢 Actualités

2026-03-17 🚀 **v0.2.3 publié !** Interface système tray (Windows & Linux), suivi de statut des sous-agents (`spawn_status`), rechargement à chaud expérimental du gateway, portes de sécurité cron, et 2 correctifs de sécurité. PicoClaw atteint **25K ⭐** !

2026-03-09 🎉 **v0.2.1 — Plus grande mise à jour !** Support du protocole MCP, 4 nouveaux canaux (Matrix/IRC/WeCom/Discord Proxy), 3 nouveaux fournisseurs (Kimi/Minimax/Avian), pipeline de vision, stockage mémoire JSONL, et routage de modèles.

2026-02-28 📦 **v0.2.0** publié avec support Docker Compose et lanceur Web UI.

2026-02-26 🎉 PicoClaw a atteint **20K étoiles** en seulement 17 jours ! L'orchestration automatique des canaux et les interfaces de capacités sont arrivées.

<details>
<summary>Actualités précédentes...</summary>

2026-02-16 🎉 PicoClaw a atteint 12K étoiles en une semaine ! Les rôles de mainteneurs communautaires et la [feuille de route](ROADMAP.md) sont officiellement publiés.

2026-02-13 🎉 PicoClaw a atteint 5000 étoiles en 4 jours ! La Feuille de Route du Projet et le Groupe de Développeurs sont en cours de mise en place.

2026-02-09 🎉 **PicoClaw est lancé !** Construit en 1 jour pour apporter les Agents IA au matériel à $10 avec <10 Mo de RAM. 🦐 PicoClaw, c'est parti !

</details>

## ✨ Fonctionnalités

🪶 **Ultra-Léger** : Empreinte mémoire <10 Mo — 99% plus petit que les fonctionnalités essentielles d'OpenClaw.*

💰 **Coût Minimal** : Suffisamment efficace pour fonctionner sur du matériel à $10 — 98% moins cher qu'un Mac mini.

⚡️ **Démarrage Éclair** : Temps de démarrage 400X plus rapide, boot en <1 seconde même sur un cœur unique à 0,6 GHz.

🌍 **Véritable Portabilité** : Un seul binaire autonome pour RISC-V, ARM, MIPS et x86. Un clic et c'est parti !

🤖 **Auto-Construit par l'IA** : Implémentation native en Go de manière autonome — 95% du cœur généré par l'Agent avec affinement humain dans la boucle.

🔌 **Support MCP** : Intégration native du [Model Context Protocol](https://modelcontextprotocol.io/) — connectez n'importe quel serveur MCP pour étendre les capacités de l'agent.

👁️ **Pipeline de Vision** : Envoyez des images et fichiers directement à l'agent — encodage base64 automatique pour les LLM multimodaux.

🧠 **Routage Intelligent** : Routage de modèles basé sur des règles — les requêtes simples vont vers des modèles légers, économisant les coûts API.

_*Les versions récentes peuvent utiliser 10–20 Mo en raison des fusions rapides de fonctionnalités. L'optimisation des ressources est prévue. La comparaison de démarrage est basée sur des benchmarks à cœur unique 0,8 GHz (voir tableau ci-dessous)._

|                               | OpenClaw      | NanoBot                  | **PicoClaw**                              |
| ----------------------------- | ------------- | ------------------------ | ----------------------------------------- |
| **Langage**                   | TypeScript    | Python                   | **Go**                                    |
| **RAM**                       | >1 Go         | >100 Mo                  | **< 10 Mo***                              |
| **Démarrage**</br>(cœur 0,8 GHz) | >500s     | >30s                     | **<1s**                                   |
| **Coût**                      | Mac Mini $599 | La plupart des SBC Linux </br>~$50 | **N'importe quelle carte Linux**</br>**À partir de $10** |

<img src="assets/compare.jpg" alt="PicoClaw" width="512">

## 🦾 Démonstration

### 🛠️ Flux de Travail Standard de l'Assistant

<table align="center">
  <tr align="center">
    <th><p align="center">🧩 Ingénieur Full-Stack</p></th>
    <th><p align="center">🗂️ Gestion des Logs & Planification</p></th>
    <th><p align="center">🔎 Recherche Web & Apprentissage</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">Développer • Déployer • Mettre à l'échelle</td>
    <td align="center">Planifier • Automatiser • Mémoriser</td>
    <td align="center">Découvrir • Analyser • Tendances</td>
  </tr>
</table>

### 📱 Utiliser sur d'anciens téléphones Android

Donnez une seconde vie à votre téléphone d'il y a dix ans ! Transformez-le en assistant IA intelligent avec PicoClaw. Démarrage rapide :

1. **Installez [Termux](https://github.com/termux/termux-app)** (Téléchargez depuis [GitHub Releases](https://github.com/termux/termux-app/releases), ou recherchez sur F-Droid / Google Play).
2. **Exécutez les commandes**

```bash
# Téléchargez la dernière version depuis https://github.com/sipeed/picoclaw/releases
wget https://github.com/sipeed/picoclaw/releases/latest/download/picoclaw_Linux_arm64.tar.gz
tar xzf picoclaw_Linux_arm64.tar.gz
pkg install proot
termux-chroot ./picoclaw onboard
```

Puis suivez les instructions de la section « Démarrage Rapide » pour terminer la configuration !

<img src="assets/termux.jpg" alt="PicoClaw" width="512">

### 🐜 Déploiement Innovant à Faible Empreinte

PicoClaw peut être déployé sur pratiquement n'importe quel appareil Linux !

- 9,9$ [LicheeRV-Nano](https://www.aliexpress.com/item/1005006519668532.html) version E (Ethernet) ou W (WiFi6), pour un Assistant Domotique Minimaliste
- 30~$50 [NanoKVM](https://www.aliexpress.com/item/1005007369816019.html), ou 100$ [NanoKVM-Pro](https://www.aliexpress.com/item/1005010048471263.html) pour la Maintenance Automatisée de Serveurs
- 50$ [MaixCAM](https://www.aliexpress.com/item/1005008053333693.html) ou 100$ [MaixCAM2](https://www.kickstarter.com/projects/zepan/maixcam2-build-your-next-gen-4k-ai-camera) pour la Surveillance Intelligente

<https://private-user-images.githubusercontent.com/83055338/547056448-e7b031ff-d6f5-4468-bcca-5726b6fecb5c.mp4>

🌟 Encore plus de scénarios de déploiement vous attendent !

## 📦 Installation

### Installer avec un binaire précompilé

Téléchargez le binaire pour votre plateforme depuis la page des [Releases](https://github.com/sipeed/picoclaw/releases).

### Installer depuis les sources (dernières fonctionnalités, recommandé pour le développement)

```bash
git clone https://github.com/sipeed/picoclaw.git

cd picoclaw
make deps

# Compiler, pas besoin d'installer
make build

# Compiler pour plusieurs plateformes
make build-all

# Compiler pour Raspberry Pi Zero 2 W (32-bit : make build-linux-arm ; 64-bit : make build-linux-arm64)
make build-pi-zero

# Compiler et Installer
make install
```

**Raspberry Pi Zero 2 W :** Utilisez le binaire correspondant à votre OS : Raspberry Pi OS 32-bit → `make build-linux-arm` ; 64-bit → `make build-linux-arm64`. Ou exécutez `make build-pi-zero` pour compiler les deux.

## 📚 Documentation

Pour des guides détaillés, consultez la documentation ci-dessous. Ce README ne couvre que le démarrage rapide.

| Sujet | Description |
|-------|-------------|
| 🐳 [Docker & Démarrage Rapide](docs/fr/docker.md) | Configuration Docker Compose, modes Launcher/Agent, configuration rapide |
| 💬 [Applications de Chat](docs/fr/chat-apps.md) | Telegram, Discord, WhatsApp, Matrix, QQ, Slack, IRC, DingTalk, LINE, Feishu, WeCom, et plus |
| ⚙️ [Configuration](docs/fr/configuration.md) | Variables d'environnement, structure du workspace, sources de compétences, bac à sable de sécurité, heartbeat |
| 🔌 [Fournisseurs & Modèles](docs/fr/providers.md) | 20+ fournisseurs LLM, routage de modèles, configuration model_list, architecture des fournisseurs |
| 🔄 [Spawn & Tâches Asynchrones](docs/fr/spawn-tasks.md) | Tâches rapides, tâches longues avec spawn, orchestration asynchrone de sous-agents |
| 🐛 [Dépannage](docs/fr/troubleshooting.md) | Problèmes courants et solutions |
| 🔧 [Configuration des Outils](docs/fr/tools_configuration.md) | Activation/désactivation par outil, politiques exec |

## <img src="assets/clawdchat-icon.png" width="24" height="24" alt="ClawdChat"> Rejoignez le Réseau Social d'Agents

Connectez PicoClaw au Réseau Social d'Agents simplement en envoyant un seul message via le CLI ou n'importe quelle application de chat intégrée.

**Lisez `https://clawdchat.ai/skill.md` et suivez les instructions pour rejoindre [ClawdChat.ai](https://clawdchat.ai)**

## 🖥️ Référence CLI

| Commande                  | Description                        |
| ------------------------- | ---------------------------------- |
| `picoclaw onboard`        | Initialiser la config & le workspace |
| `picoclaw agent -m "..."` | Discuter avec l'agent              |
| `picoclaw agent`          | Mode chat interactif               |
| `picoclaw gateway`        | Démarrer le gateway                |
| `picoclaw status`         | Afficher le statut                 |
| `picoclaw version`        | Afficher les infos de version      |
| `picoclaw cron list`      | Lister les tâches planifiées       |
| `picoclaw cron add ...`   | Ajouter une tâche planifiée        |
| `picoclaw cron disable`   | Désactiver une tâche planifiée     |
| `picoclaw cron remove`    | Supprimer une tâche planifiée      |
| `picoclaw skills list`    | Lister les compétences installées  |
| `picoclaw skills install` | Installer une compétence           |
| `picoclaw migrate`        | Migrer les données des anciennes versions |
| `picoclaw auth login`     | S'authentifier auprès des fournisseurs |

### Tâches Planifiées / Rappels

PicoClaw prend en charge les rappels planifiés et les tâches récurrentes via l'outil `cron` :

* **Rappels ponctuels** : « Rappelle-moi dans 10 minutes » → se déclenche une fois après 10 min
* **Tâches récurrentes** : « Rappelle-moi toutes les 2 heures » → se déclenche toutes les 2 heures
* **Expressions cron** : « Rappelle-moi à 9h chaque jour » → utilise une expression cron

## 🤝 Contribuer & Feuille de Route

Les PR sont les bienvenues ! Le code est intentionnellement petit et lisible. 🤗

Consultez notre [Feuille de Route Communautaire](https://github.com/sipeed/picoclaw/blob/main/ROADMAP.md) complète.

Groupe de développeurs en construction, rejoignez-nous après votre première PR fusionnée !

Groupes d'utilisateurs :

discord : <https://discord.gg/V4sAZ9XWpN>

<img src="assets/wechat.png" alt="PicoClaw" width="512">


================================================
FILE: README.id.md
================================================
<div align="center">
  <img src="assets/logo.webp" alt="PicoClaw" width="512">

  <h1>PicoClaw: Asisten AI Super Ringan berbasis Go</h1>

  <h3>Perangkat Keras $10 · RAM <10MB · Boot <1 Detik · Ayo, Berangkat!</h3>
  <p>
    <img src="https://img.shields.io/badge/Go-1.25+-00ADD8?style=flat&logo=go&logoColor=white" alt="Go">
    <img src="https://img.shields.io/badge/Arch-x86__64%2C%20ARM64%2C%20MIPS%2C%20RISC--V%2C%20LoongArch-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://docs.picoclaw.io/"><img src="https://img.shields.io/badge/Docs-Official-007acc?style=flat&logo=read-the-docs&logoColor=white" alt="Docs"></a>
    <a href="https://deepwiki.com/sipeed/picoclaw"><img src="https://img.shields.io/badge/Wiki-DeepWiki-FFA500?style=flat&logo=wikipedia&logoColor=white" alt="Wiki"></a>
    <br>
    <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>
    <a href="./assets/wechat.png"><img src="https://img.shields.io/badge/WeChat-Group-41d56b?style=flat&logo=wechat&logoColor=white"></a>
    <a href="https://discord.gg/V4sAZ9XWpN"><img src="https://img.shields.io/badge/Discord-Community-4c60eb?style=flat&logo=discord&logoColor=white" alt="Discord"></a>
  </p>

[中文](README.zh.md) | [日本語](README.ja.md) | [Português](README.pt-br.md) | [Tiếng Việt](README.vi.md) | [Français](README.fr.md) | [Italiano](README.it.md) | [English](README.md) | **Bahasa Indonesia**

</div>

---

> **PicoClaw** adalah proyek open-source independen yang diinisiasi oleh [Sipeed](https://sipeed.com). Ditulis sepenuhnya dalam **Go** — bukan fork dari OpenClaw, NanoBot, atau proyek lainnya.

🦐 PicoClaw adalah asisten AI pribadi yang super ringan, terinspirasi dari [NanoBot](https://github.com/HKUDS/nanobot), ditulis ulang sepenuhnya dalam Go melalui proses "self-bootstrapping" — di mana AI Agent itu sendiri yang memandu seluruh migrasi arsitektur dan optimasi kode.

⚡️ Berjalan di perangkat keras $10 dengan RAM <10MB: Hemat 99% memori dibanding OpenClaw dan 98% lebih murah dibanding Mac mini!

<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]
> **🚨 KEAMANAN & SALURAN RESMI**
>
> * **TANPA KRIPTO:** PicoClaw **TIDAK** memiliki token/koin resmi. Semua klaim di `pump.fun` atau platform trading lainnya adalah **PENIPUAN**.
>
> * **DOMAIN RESMI:** Satu-satunya website resmi adalah **[picoclaw.io](https://picoclaw.io)**, dan website perusahaan adalah **[sipeed.com](https://sipeed.com)**
> * **Peringatan:** Banyak domain `.ai/.org/.com/.net/...` yang didaftarkan oleh pihak ketiga.
> * **Peringatan:** PicoClaw masih dalam tahap pengembangan awal dan mungkin memiliki masalah keamanan jaringan yang belum teratasi. Jangan deploy ke lingkungan produksi sebelum rilis v1.0.
> * **Catatan:** PicoClaw baru-baru ini menggabungkan banyak PR, yang mungkin mengakibatkan penggunaan memori lebih besar (10–20MB) pada versi terbaru. Kami berencana untuk memprioritaskan optimasi sumber daya segera setelah fitur saat ini mencapai kondisi stabil.

## 📢 Berita

2026-03-17 🚀 **v0.2.3 Dirilis!** UI system tray (Windows & Linux), pelacakan status sub-agent (`spawn_status`), eksperimental gateway hot-reload, gerbang keamanan cron, dan 2 perbaikan keamanan. PicoClaw kini di **25K ⭐**!

2026-03-09 🎉 **v0.2.1 — Update terbesar!** Dukungan protokol MCP, 4 channel baru (Matrix/IRC/WeCom/Discord Proxy), 3 provider baru (Kimi/Minimax/Avian), pipeline vision, penyimpanan memori JSONL, dan routing model.

2026-02-28 📦 **v0.2.0** dirilis dengan dukungan Docker Compose dan launcher Web UI.

2026-02-26 🎉 PicoClaw mencapai **20K bintang** hanya dalam 17 hari! Orkestrasi channel otomatis dan antarmuka kapabilitas diluncurkan.

<details>
<summary>Berita lama...</summary>

2026-02-16 🎉 PicoClaw mencapai 12K bintang dalam satu minggu! Peran maintainer komunitas dan [roadmap](ROADMAP.md) resmi diposting.

2026-02-13 🎉 PicoClaw mencapai 5000 bintang dalam 4 hari! Roadmap Proyek dan pengaturan Grup Pengembang sedang berjalan.

2026-02-09 🎉 **PicoClaw Diluncurkan!** Dibangun dalam 1 hari untuk menghadirkan AI Agent ke perangkat keras $10 dengan RAM <10MB. 🦐 PicoClaw, Ayo Berangkat!

</details>

## ✨ Fitur

🪶 **Super Ringan**: Penggunaan memori <10MB — 99% lebih kecil dari fungsionalitas inti OpenClaw.*

💰 **Biaya Minimal**: Cukup efisien untuk berjalan di perangkat keras $10 — 98% lebih murah dari Mac mini.

⚡️ **Secepat Kilat**: Waktu startup 400X lebih cepat, boot dalam <1 detik bahkan di prosesor single core 0,6GHz.

🌍 **Portabilitas Sejati**: Satu binary mandiri untuk RISC-V, ARM, MIPS, dan x86, Satu Klik Langsung Jalan!

🤖 **AI-Bootstrapped**: Implementasi Go-native secara otonom — 95% kode inti dihasilkan oleh Agent dengan penyempurnaan human-in-the-loop.

🔌 **Dukungan MCP**: Integrasi [Model Context Protocol](https://modelcontextprotocol.io/) native — hubungkan server MCP mana pun untuk memperluas kapabilitas agent.

👁️ **Pipeline Vision**: Kirim gambar dan file langsung ke agent — encoding base64 otomatis untuk LLM multimodal.

🧠 **Routing Cerdas**: Routing model berbasis aturan — kueri sederhana diarahkan ke model ringan, menghemat biaya API.

_*Versi terbaru mungkin menggunakan 10–20MB karena penggabungan fitur yang cepat. Optimasi sumber daya direncanakan. Perbandingan startup berdasarkan benchmark prosesor single-core 0,8GHz (lihat tabel di bawah)._

|                               | OpenClaw      | NanoBot                  | **PicoClaw**                              |
| ----------------------------- | ------------- | ------------------------ | ----------------------------------------- |
| **Bahasa**                    | TypeScript    | Python                   | **Go**                                    |
| **RAM**                       | >1GB          | >100MB                   | **< 10MB***                               |
| **Startup**</br>(0,8GHz core) | >500d         | >30d                     | **<1d**                                   |
| **Biaya**                     | Mac Mini $599 | Kebanyakan Linux SBC </br>~$50 | **Semua Board Linux**</br>**Mulai dari $10** |

<img src="assets/compare.jpg" alt="PicoClaw" width="512">

## 🦾 Demonstrasi

### 🛠️ Alur Kerja Asisten Standar

<table align="center">
  <tr align="center">
    <th><p align="center">🧩 Full-Stack Engineer</p></th>
    <th><p align="center">🗂️ Pencatatan & Manajemen Perencanaan</p></th>
    <th><p align="center">🔎 Pencarian Web & Pembelajaran</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">Develop • Deploy • Scale</td>
    <td align="center">Jadwal • Otomasi • Memori</td>
    <td align="center">Penemuan • Wawasan • Tren</td>
  </tr>
</table>

### 📱 Jalankan di HP Android Lama

Berikan kehidupan kedua untuk HP lama Anda! Ubah menjadi Asisten AI pintar dengan PicoClaw. Panduan Cepat:

1. **Instal [Termux](https://github.com/termux/termux-app)** (Unduh dari [GitHub Releases](https://github.com/termux/termux-app/releases), atau cari di F-Droid / Google Play).
2. **Jalankan perintah**

```bash
# Unduh rilis terbaru dari https://github.com/sipeed/picoclaw/releases
wget https://github.com/sipeed/picoclaw/releases/latest/download/picoclaw_Linux_arm64.tar.gz
tar xzf picoclaw_Linux_arm64.tar.gz
pkg install proot
termux-chroot ./picoclaw onboard
```

Kemudian ikuti instruksi di bagian "Panduan Cepat" untuk menyelesaikan konfigurasi!

<img src="assets/termux.jpg" alt="PicoClaw" width="512">

### 🐜 Deploy Inovatif dengan Footprint Rendah

PicoClaw dapat di-deploy di hampir semua perangkat Linux!

- $9,9 [LicheeRV-Nano](https://www.aliexpress.com/item/1005006519668532.html) versi E(Ethernet) atau W(WiFi6), untuk Home Assistant Minimal
- $30~50 [NanoKVM](https://www.aliexpress.com/item/1005007369816019.html), atau $100 [NanoKVM-Pro](https://www.aliexpress.com/item/1005010048471263.html) untuk Pemeliharaan Server Otomatis
- $50 [MaixCAM](https://www.aliexpress.com/item/1005008053333693.html) atau $100 [MaixCAM2](https://www.kickstarter.com/projects/zepan/maixcam2-build-your-next-gen-4k-ai-camera) untuk Pemantauan Cerdas

<https://private-user-images.githubusercontent.com/83055338/547056448-e7b031ff-d6f5-4468-bcca-5726b6fecb5c.mp4>

🌟 Lebih Banyak Kasus Deploy Menanti!

## 📦 Instalasi

### Instal dengan binary yang sudah dikompilasi

Unduh binary untuk platform Anda dari halaman [Releases](https://github.com/sipeed/picoclaw/releases).

### Instal dari source (fitur terbaru, disarankan untuk pengembangan)

```bash
git clone https://github.com/sipeed/picoclaw.git

cd picoclaw
make deps

# Build, tidak perlu instal
make build

# Build untuk berbagai platform
make build-all

# Build untuk Raspberry Pi Zero 2 W (32-bit: make build-linux-arm; 64-bit: make build-linux-arm64)
make build-pi-zero

# Build dan Instal
make install
```

**Raspberry Pi Zero 2 W:** Gunakan binary yang sesuai dengan OS Anda: Raspberry Pi OS 32-bit → `make build-linux-arm`; 64-bit → `make build-linux-arm64`. Atau jalankan `make build-pi-zero` untuk build keduanya.

## 📚 Dokumentasi

Untuk panduan lengkap, lihat dokumen di bawah. README ini hanya berisi panduan cepat.

| Topik | Deskripsi |
|-------|-----------|
| 🐳 [Docker & Panduan Cepat](docs/docker.md) | Pengaturan Docker Compose, mode Launcher/Agent, konfigurasi Panduan Cepat |
| 💬 [Aplikasi Chat](docs/chat-apps.md) | Telegram, Discord, WhatsApp, Matrix, QQ, Slack, IRC, DingTalk, LINE, Feishu, WeCom, dan lainnya |
| ⚙️ [Konfigurasi](docs/configuration.md) | Variabel environment, tata letak workspace, sumber skill, sandbox keamanan, heartbeat |
| 🔌 [Provider & Model](docs/providers.md) | 20+ provider LLM, routing model, konfigurasi model_list, arsitektur provider |
| 🔄 [Spawn & Tugas Async](docs/spawn-tasks.md) | Tugas cepat, tugas panjang dengan spawn, orkestrasi sub-agent async |
| 🐛 [Pemecahan Masalah](docs/troubleshooting.md) | Masalah umum dan solusinya |
| 🔧 [Konfigurasi Tools](docs/tools_configuration.md) | Aktifkan/nonaktifkan tool, kebijakan exec |

## <img src="assets/clawdchat-icon.png" width="24" height="24" alt="ClawdChat"> Bergabung dengan Jaringan Sosial Agent

Hubungkan Picoclaw ke Jaringan Sosial Agent hanya dengan mengirim satu pesan melalui CLI atau Aplikasi Chat terintegrasi.

**Baca `https://clawdchat.ai/skill.md` dan ikuti instruksi untuk bergabung dengan [ClawdChat.ai](https://clawdchat.ai)**

## 🖥️ Referensi CLI

| Perintah                  | Deskripsi                        |
| ------------------------- | -------------------------------- |
| `picoclaw onboard`        | Inisialisasi konfigurasi & workspace |
| `picoclaw agent -m "..."` | Chat dengan agent                |
| `picoclaw agent`          | Mode chat interaktif             |
| `picoclaw gateway`        | Mulai gateway                    |
| `picoclaw status`         | Tampilkan status                 |
| `picoclaw version`        | Tampilkan info versi             |
| `picoclaw cron list`      | Daftar semua tugas terjadwal     |
| `picoclaw cron add ...`   | Tambah tugas terjadwal           |
| `picoclaw cron disable`   | Nonaktifkan tugas terjadwal      |
| `picoclaw cron remove`    | Hapus tugas terjadwal            |
| `picoclaw skills list`    | Daftar skill yang terinstal      |
| `picoclaw skills install` | Instal skill                     |
| `picoclaw migrate`        | Migrasi data dari versi lama     |
| `picoclaw auth login`     | Autentikasi dengan provider      |

### Tugas Terjadwal / Pengingat

PicoClaw mendukung pengingat terjadwal dan tugas berulang melalui tool `cron`:

* **Pengingat satu kali**: "Ingatkan saya dalam 10 menit" → terpicu sekali setelah 10 menit
* **Tugas berulang**: "Ingatkan saya setiap 2 jam" → terpicu setiap 2 jam
* **Ekspresi cron**: "Ingatkan saya jam 9 pagi setiap hari" → menggunakan ekspresi cron

## 🤝 Kontribusi & Roadmap

PR sangat diterima! Codebase sengaja dibuat kecil dan mudah dibaca. 🤗

Lihat [Roadmap Komunitas](https://github.com/sipeed/picoclaw/blob/main/ROADMAP.md) lengkap kami.

Grup pengembang sedang dibangun, bergabunglah setelah PR pertama Anda di-merge!

Grup Pengguna:

discord: <https://discord.gg/V4sAZ9XWpN>

<img src="assets/wechat.png" alt="PicoClaw" width="512">


================================================
FILE: README.it.md
================================================
<div align="center">
  <img src="assets/logo.webp" alt="PicoClaw" width="512">

  <h1>PicoClaw: Assistente IA Ultra-Efficiente in Go</h1>

  <h3>Hardware da $10 · <10MB RAM · Boot in <1s · 皮皮虾,我们走!</h3>
  <p>
    <img src="https://img.shields.io/badge/Go-1.25+-00ADD8?style=flat&logo=go&logoColor=white" alt="Go">
    <img src="https://img.shields.io/badge/Arch-x86__64%2C%20ARM64%2C%20MIPS%2C%20RISC--V%2C%20LoongArch-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://docs.picoclaw.io/"><img src="https://img.shields.io/badge/Docs-Official-007acc?style=flat&logo=read-the-docs&logoColor=white" alt="Docs"></a>
    <a href="https://deepwiki.com/sipeed/picoclaw"><img src="https://img.shields.io/badge/Wiki-DeepWiki-FFA500?style=flat&logo=wikipedia&logoColor=white" alt="Wiki"></a>
    <br>
    <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>
    <a href="./assets/wechat.png"><img src="https://img.shields.io/badge/WeChat-Group-41d56b?style=flat&logo=wechat&logoColor=white"></a>
    <a href="https://discord.gg/V4sAZ9XWpN"><img src="https://img.shields.io/badge/Discord-Community-4c60eb?style=flat&logo=discord&logoColor=white" alt="Discord"></a>
  </p>

[中文](README.zh.md) | [日本語](README.ja.md) | [Português](README.pt-br.md) | [Tiếng Việt](README.vi.md) | [Français](README.fr.md) | **Italiano** | [Bahasa Indonesia](README.id.md) | [English](README.md)

</div>

---

> **PicoClaw** è un progetto open-source indipendente avviato da [Sipeed](https://sipeed.com). È scritto interamente in **Go** — non è un fork di OpenClaw, NanoBot o di qualsiasi altro progetto.

🦐 PicoClaw è un assistente IA personale ultra-leggero ispirato a [NanoBot](https://github.com/HKUDS/nanobot), riscritto da zero in Go attraverso un processo di auto-bootstrapping, in cui l'agente IA stesso ha guidato l'intera migrazione architetturale e l'ottimizzazione del codice.

⚡️ Funziona su hardware da $10 con meno di 10MB di RAM: il 99% di memoria in meno rispetto a OpenClaw e il 98% più economico di un Mac mini!

<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]
> **🚨 SICUREZZA & CANALI UFFICIALI**
>
> * **NESSUNA CRYPTO:** PicoClaw non ha **NESSUN** token/coin ufficiale. Qualsiasi annuncio su `pump.fun` o altre piattaforme di trading è una **TRUFFA**.
>
> * **DOMINIO UFFICIALE:** L'**UNICO** sito ufficiale è **[picoclaw.io](https://picoclaw.io)**, e il sito aziendale è **[sipeed.com](https://sipeed.com)**.
> * **Attenzione:** Molti domini `.ai/.org/.com/.net/...` sono registrati da terze parti.
> * **Attenzione:** PicoClaw è in fase di sviluppo iniziale e potrebbe avere problemi di sicurezza di rete non risolti. Non distribuire in ambienti di produzione prima della release v1.0.
> * **Nota:** PicoClaw ha recentemente unito molte PR, il che potrebbe comportare un'impronta di memoria maggiore (10–20MB) nelle ultime versioni. Prevediamo di dare priorità all'ottimizzazione delle risorse non appena il set di funzionalità corrente raggiungerà uno stato stabile.

## 📢 Novità

2026-03-17 🚀 **v0.2.3 rilasciata!** Interfaccia system tray (Windows & Linux), tracciamento dello stato dei sub-agent (`spawn_status`), hot-reload sperimentale del gateway, gate di sicurezza per cron e 2 correzioni di sicurezza. PicoClaw raggiunge **25K ⭐**!

2026-03-09 🎉 **v0.2.1 — Il più grande aggiornamento di sempre!** Supporto al protocollo MCP, 4 nuovi canali (Matrix/IRC/WeCom/Discord Proxy), 3 nuovi provider (Kimi/Minimax/Avian), pipeline di visione, store di memoria JSONL e routing dei modelli.

2026-02-28 📦 **v0.2.0** rilasciata con supporto Docker Compose e launcher Web UI.

2026-02-26 🎉 PicoClaw ha raggiunto **20K stelle** in soli 17 giorni! Arrivate l'orchestrazione automatica dei canali e le interfacce di capacità.

<details>
<summary>Notizie precedenti...</summary>

2026-02-16 🎉 PicoClaw ha raggiunto 12K stelle in una settimana! Ruoli di maintainer della community e [roadmap](ROADMAP.md) pubblicati ufficialmente.

2026-02-13 🎉 PicoClaw ha raggiunto 5000 stelle in 4 giorni! Roadmap del progetto e gruppo sviluppatori in fase di avvio.

2026-02-09 🎉 **PicoClaw lanciato!** Costruito in 1 giorno per portare gli agenti IA su hardware da $10 con <10MB di RAM. 🦐 PicoClaw, andiamo!

</details>

## ✨ Caratteristiche

🪶 **Ultra-Leggero**: Impronta di memoria <10MB — il 99% più piccolo delle funzionalità principali di OpenClaw.*

💰 **Costo Minimo**: Abbastanza efficiente da girare su hardware da $10 — il 98% più economico di un Mac mini.

⚡️ **Avvio Fulmineo**: Tempo di avvio 400 volte più veloce, boot in meno di 1 secondo anche su un singolo core a 0,6 GHz.

🌍 **Vera Portabilità**: Singolo binario autonomo per RISC-V, ARM, MIPS e x86. Un click e si parte!

🤖 **Auto-Costruito dall'IA**: Implementazione nativa in Go in modo autonomo — 95% del core generato dall'Agent con perfezionamento umano nel ciclo.

🔌 **Supporto MCP**: Integrazione nativa del [Model Context Protocol](https://modelcontextprotocol.io/) — connetti qualsiasi server MCP per estendere le capacità dell'agent.

👁️ **Pipeline di Visione**: Invia immagini e file direttamente all'agent — codifica base64 automatica per LLM multimodali.

🧠 **Routing Intelligente**: Routing dei modelli basato su regole — le query semplici vanno verso modelli leggeri, risparmiando sui costi API.

_*Le versioni recenti potrebbero usare 10–20MB a causa delle fusioni rapide di funzionalità. L'ottimizzazione delle risorse è pianificata. Il confronto dell'avvio è basato su benchmark con singolo core a 0,8 GHz (vedi tabella sotto)._

|                               | OpenClaw      | NanoBot                  | **PicoClaw**                              |
| ----------------------------- | ------------- | ------------------------ | ----------------------------------------- |
| **Linguaggio**                | TypeScript    | Python                   | **Go**                                    |
| **RAM**                       | >1GB          | >100MB                   | **< 10MB***                               |
| **Avvio**</br>(core 0,8 GHz)  | >500s         | >30s                     | **<1s**                                   |
| **Costo**                     | Mac Mini $599 | La maggior parte degli SBC Linux </br>~$50 | **Qualsiasi scheda Linux**</br>**A partire da $10** |

<img src="assets/compare.jpg" alt="PicoClaw" width="512">

## 🦾 Dimostrazione

### 🛠️ Flussi di Lavoro Standard dell'Assistente

<table align="center">
  <tr align="center">
    <th><p align="center">🧩 Ingegnere Full-Stack</p></th>
    <th><p align="center">🗂️ Gestione Log & Pianificazione</p></th>
    <th><p align="center">🔎 Ricerca Web & Apprendimento</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">Sviluppa • Distribuisci • Scala</td>
    <td align="center">Pianifica • Automatizza • Memorizza</td>
    <td align="center">Scopri • Analizza • Tendenze</td>
  </tr>
</table>

### 📱 Usa su vecchi telefoni Android

Dai una seconda vita al tuo telefono di dieci anni fa! Trasformalo in un assistente IA intelligente con PicoClaw. Avvio rapido:

1. **Installa [Termux](https://github.com/termux/termux-app)** (Scarica da [GitHub Releases](https://github.com/termux/termux-app/releases), o cerca su F-Droid / Google Play).
2. **Esegui i comandi**

```bash
# Scarica l'ultima release da https://github.com/sipeed/picoclaw/releases
wget https://github.com/sipeed/picoclaw/releases/latest/download/picoclaw_Linux_arm64.tar.gz
tar xzf picoclaw_Linux_arm64.tar.gz
pkg install proot
termux-chroot ./picoclaw onboard
```

Poi segui le istruzioni nella sezione "Avvio Rapido" per completare la configurazione!

<img src="assets/termux.jpg" alt="PicoClaw" width="512">

### 🐜 Deploy Innovativo a Bassa Impronta

PicoClaw può essere distribuito su quasi qualsiasi dispositivo Linux!

- $9,9 [LicheeRV-Nano](https://www.aliexpress.com/item/1005006519668532.html) versione E (Ethernet) o W (WiFi6), per un Assistente Domotico Minimale
- $30~50 [NanoKVM](https://www.aliexpress.com/item/1005007369816019.html), o $100 [NanoKVM-Pro](https://www.aliexpress.com/item/1005010048471263.html) per la Manutenzione Automatizzata dei Server
- $50 [MaixCAM](https://www.aliexpress.com/item/1005008053333693.html) o $100 [MaixCAM2](https://www.kickstarter.com/projects/zepan/maixcam2-build-your-next-gen-4k-ai-camera) per il Monitoraggio Intelligente

<https://private-user-images.githubusercontent.com/83055338/547056448-e7b031ff-d6f5-4468-bcca-5726b6fecb5c.mp4>

🌟 Molti altri scenari di deploy ti aspettano!

## 📦 Installazione

### Installa con binario precompilato

Scarica il binario per la tua piattaforma dalla pagina delle [Releases](https://github.com/sipeed/picoclaw/releases).

### Installa dai sorgenti (ultime funzionalità, consigliato per lo sviluppo)

```bash
git clone https://github.com/sipeed/picoclaw.git

cd picoclaw
make deps

# Compila, senza installare
make build

# Compila per più piattaforme
make build-all

# Compila per Raspberry Pi Zero 2 W (32-bit: make build-linux-arm; 64-bit: make build-linux-arm64)
make build-pi-zero

# Compila e Installa
make install
```

**Raspberry Pi Zero 2 W:** Usa il binario che corrisponde al tuo OS: Raspberry Pi OS 32-bit → `make build-linux-arm`; 64-bit → `make build-linux-arm64`. Oppure esegui `make build-pi-zero` per compilare entrambi.

## 📚 Documentazione

Per guide dettagliate, consulta la documentazione qui sotto. Il README copre solo l'avvio rapido.

| Argomento | Descrizione |
|-----------|-------------|
| 🐳 [Docker & Avvio Rapido](docs/docker.md) | Configurazione Docker Compose, modalità Launcher/Agent, configurazione rapida |
| 💬 [App di Chat](docs/chat-apps.md) | Telegram, Discord, WhatsApp, Matrix, QQ, Slack, IRC, DingTalk, LINE, Feishu, WeCom e altro |
| ⚙️ [Configurazione](docs/it/configuration.md) | Variabili d'ambiente, struttura del workspace, sorgenti delle skill, sandbox di sicurezza, heartbeat |
| 🔌 [Provider & Modelli](docs/providers.md) | 20+ provider LLM, routing dei modelli, configurazione model_list, architettura dei provider |
| 🔄 [Spawn & Task Asincroni](docs/spawn-tasks.md) | Task veloci, task lunghi con spawn, orchestrazione asincrona di sub-agent |
| 🐛 [Risoluzione Problemi](docs/troubleshooting.md) | Problemi comuni e soluzioni |
| 🔧 [Configurazione degli Strumenti](docs/tools_configuration.md) | Abilitazione/disabilitazione per strumento, politiche exec |

## <img src="assets/clawdchat-icon.png" width="24" height="24" alt="ClawdChat"> Unisciti al Social Network degli Agent

Connetti PicoClaw al Social Network degli Agent semplicemente inviando un singolo messaggio tramite CLI o qualsiasi app di chat integrata.

**Leggi `https://clawdchat.ai/skill.md` e segui le istruzioni per unirti a [ClawdChat.ai](https://clawdchat.ai)**

## 🖥️ Riferimento CLI

| Comando                   | Descrizione                        |
| ------------------------- | ---------------------------------- |
| `picoclaw onboard`        | Inizializza config & workspace     |
| `picoclaw agent -m "..."` | Chatta con l'agent                 |
| `picoclaw agent`          | Modalità chat interattiva          |
| `picoclaw gateway`        | Avvia il gateway                   |
| `picoclaw status`         | Mostra lo stato                    |
| `picoclaw version`        | Mostra le info sulla versione      |
| `picoclaw cron list`      | Elenca tutti i job pianificati     |
| `picoclaw cron add ...`   | Aggiunge un job pianificato        |
| `picoclaw cron disable`   | Disabilita un job pianificato      |
| `picoclaw cron remove`    | Rimuove un job pianificato         |
| `picoclaw skills list`    | Elenca le skill installate         |
| `picoclaw skills install` | Installa una skill                 |
| `picoclaw migrate`        | Migra i dati dalle versioni precedenti |
| `picoclaw auth login`     | Autenticazione con i provider      |

### Task Pianificati / Promemoria

PicoClaw supporta promemoria pianificati e task ricorrenti tramite lo strumento `cron`:

* **Promemoria una tantum**: "Ricordami tra 10 minuti" → si attiva una volta dopo 10 min
* **Task ricorrenti**: "Ricordami ogni 2 ore" → si attiva ogni 2 ore
* **Espressioni cron**: "Ricordami alle 9 ogni giorno" → usa un'espressione cron

## 🤝 Contribuisci & Roadmap

Le PR sono benvenute! Il codice è volutamente piccolo e leggibile. 🤗

Consulta la nostra [Roadmap della Community](https://github.com/sipeed/picoclaw/blob/main/ROADMAP.md) completa.

Gruppo sviluppatori in costruzione, unisciti dopo la tua prima PR accettata!

Gruppi utenti:

discord: <https://discord.gg/V4sAZ9XWpN>

<img src="assets/wechat.png" alt="PicoClaw" width="512">


================================================
FILE: README.ja.md
================================================
<div align="center">
  <img src="assets/logo.webp" alt="PicoClaw" width="512">

  <h1>PicoClaw: Go で書かれた超効率 AI アシスタント</h1>

  <h3>$10 ハードウェア · <10MB RAM · <1秒起動 · 行くぜ、シャコ!</h3>
  <p>
    <img src="https://img.shields.io/badge/Go-1.25+-00ADD8?style=flat&logo=go&logoColor=white" alt="Go">
    <img src="https://img.shields.io/badge/Arch-x86__64%2C%20ARM64%2C%20MIPS%2C%20RISC--V%2C%20LoongArch-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://docs.picoclaw.io/"><img src="https://img.shields.io/badge/Docs-Official-007acc?style=flat&logo=read-the-docs&logoColor=white" alt="Docs"></a>
    <a href="https://deepwiki.com/sipeed/picoclaw"><img src="https://img.shields.io/badge/Wiki-DeepWiki-FFA500?style=flat&logo=wikipedia&logoColor=white" alt="Wiki"></a>
    <br>
    <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>
    <a href="./assets/wechat.png"><img src="https://img.shields.io/badge/WeChat-Group-41d56b?style=flat&logo=wechat&logoColor=white"></a>
    <a href="https://discord.gg/V4sAZ9XWpN"><img src="https://img.shields.io/badge/Discord-Community-4c60eb?style=flat&logo=discord&logoColor=white" alt="Discord"></a>
  </p>

[中文](README.zh.md) | **日本語** | [Português](README.pt-br.md) | [Tiếng Việt](README.vi.md) | [Français](README.fr.md) | [Italiano](README.it.md) | [Bahasa Indonesia](README.id.md) | [English](README.md)

</div>

---

> **PicoClaw** は [Sipeed](https://sipeed.com) が立ち上げた独立したオープンソースプロジェクトです。完全に **Go 言語**で一から書かれており、OpenClaw、NanoBot、その他のプロジェクトのフォークではありません。

🦐 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>

> [!CAUTION]
> **🚨 セキュリティ&公式チャンネル**
>
> * **暗号通貨なし:** PicoClaw には公式トークン/コインは**一切ありません**。`pump.fun` やその他の取引プラットフォームでの主張はすべて**詐欺**です。
>
> * **公式ドメイン:** **唯一**の公式サイトは **[picoclaw.io](https://picoclaw.io)**、企業サイトは **[sipeed.com](https://sipeed.com)** です。
> * **注意:** 多くの `.ai/.org/.com/.net/...` ドメインは第三者によって登録されています。
> * **注意:** PicoClaw は初期開発段階にあり、未解決のネットワークセキュリティ問題がある可能性があります。v1.0 リリース前に本番環境へのデプロイは避けてください。
> * **注記:** PicoClaw は最近多くの PR をマージしており、最新バージョンではメモリフットプリントが大きくなる場合があります(10〜20MB)。機能セットが安定次第、リソース最適化を優先する予定です。

## 📢 ニュース

2026-03-17 🚀 **v0.2.3 リリース!** システムトレイ UI(Windows & Linux)、サブエージェントステータス追跡(`spawn_status`)、実験的ゲートウェイホットリロード、cron セキュリティゲート、セキュリティ修正 2 件。PicoClaw **25K ⭐** 達成!

2026-03-09 🎉 **v0.2.1 — 史上最大のアップデート!** MCP プロトコル対応、4 つの新チャネル(Matrix/IRC/WeCom/Discord Proxy)、3 つの新プロバイダー(Kimi/Minimax/Avian)、ビジョンパイプライン、JSONL メモリストア、モデルルーティング。

2026-02-28 📦 **v0.2.0** リリース — Docker Compose 対応と Web UI ランチャー。

2026-02-26 🎉 PicoClaw がわずか 17 日で **20K スター** 達成!チャネル自動オーケストレーションとケイパビリティインターフェースが実装されました。

<details>
<summary>過去のニュース...</summary>

2026-02-16 🎉 PicoClaw が 1 週間で 12K スター達成!コミュニティメンテナーの役割と[ロードマップ](ROADMAP.md)が正式に公開されました。

2026-02-13 🎉 PicoClaw が 4 日間で 5000 スター達成!プロジェクトロードマップと開発者グループの準備が進行中。

2026-02-09 🎉 **PicoClaw リリース!** $10 ハードウェアで 10MB 未満の RAM で動く AI エージェントを 1 日で構築。🦐 行くぜ、シャコ!

</details>

## ✨ 特徴

🪶 **超軽量**: メモリフットプリント 10MB 未満 — OpenClaw のコア機能より 99% 小さい。*

💰 **最小コスト**: $10 ハードウェアで動作 — Mac mini より 98% 安い。

⚡️ **超高速**: 起動時間 400 倍高速、0.6GHz シングルコアでも 1 秒未満で起動。

🌍 **真のポータビリティ**: RISC-V、ARM、MIPS、x86 対応の単一バイナリ。ワンクリックで Go!

🤖 **AI ブートストラップ**: 自律的な Go ネイティブ実装 — コアの 95% が AI 生成、人間によるレビュー付き。

🔌 **MCP 対応**: ネイティブ [Model Context Protocol](https://modelcontextprotocol.io/) 統合 — 任意の MCP サーバーに接続してエージェント機能を拡張。

👁️ **ビジョンパイプライン**: 画像やファイルをエージェントに直接送信 — マルチモーダル LLM 向けの自動 base64 エンコーディング。

🧠 **スマートルーティング**: ルールベースのモデルルーティング — 簡単なクエリは軽量モデルへ、API コストを節約。

_*最近のバージョンでは急速な機能マージにより 10〜20MB になる場合があります。リソース最適化は計画中です。起動時間の比較は 0.8GHz シングルコアベンチマークに基づいています(下表参照)。_

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

### 📱 古い Android スマホで動かす

10 年前のスマホに第二の人生を!PicoClaw でスマート AI アシスタントに変身させましょう。クイックスタート:

1. **[Termux](https://github.com/termux/termux-app) をインストール**([GitHub Releases](https://github.com/termux/termux-app/releases) からダウンロード、または F-Droid / Google Play で検索)。
2. **コマンドを実行**

```bash
# https://github.com/sipeed/picoclaw/releases から最新リリースをダウンロード
wget https://github.com/sipeed/picoclaw/releases/latest/download/picoclaw_Linux_arm64.tar.gz
tar xzf picoclaw_Linux_arm64.tar.gz
pkg install proot
termux-chroot ./picoclaw onboard
```

その後「クイックスタート」セクションの手順に従って設定を完了してください!

<img src="assets/termux.jpg" alt="PicoClaw" width="512">

### 🐜 革新的な省フットプリントデプロイ

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

# Raspberry Pi Zero 2 W 向けビルド(32-bit: make build-linux-arm; 64-bit: make build-linux-arm64)
make build-pi-zero

# ビルドとインストール
make install
```

**Raspberry Pi Zero 2 W:** OS に合ったバイナリを使用してください:32-bit Raspberry Pi OS → `make build-linux-arm`、64-bit → `make build-linux-arm64`。または `make build-pi-zero` で両方をビルド。

## 📚 ドキュメント

詳細なガイドは以下のドキュメントを参照してください。この README はクイックスタートのみをカバーしています。

| トピック | 説明 |
|---------|------|
| 🐳 [Docker & クイックスタート](docs/ja/docker.md) | Docker Compose セットアップ、Launcher/Agent モード、クイックスタート設定 |
| 💬 [チャットアプリ](docs/ja/chat-apps.md) | Telegram、Discord、WhatsApp、Matrix、QQ、Slack、IRC、DingTalk、LINE、Feishu、WeCom など |
| ⚙️ [設定](docs/ja/configuration.md) | 環境変数、ワークスペース構成、スキルソース、セキュリティサンドボックス、ハートビート |
| 🔌 [プロバイダー&モデル](docs/ja/providers.md) | 20 以上の LLM プロバイダー、モデルルーティング、model_list 設定、プロバイダーアーキテクチャ |
| 🔄 [Spawn & 非同期タスク](docs/ja/spawn-tasks.md) | クイックタスク、spawn による長時間タスク、非同期サブエージェントオーケストレーション |
| 🐛 [トラブルシューティング](docs/ja/troubleshooting.md) | よくある問題と解決策 |
| 🔧 [ツール設定](docs/ja/tools_configuration.md) | ツールごとの有効/無効、exec ポリシー |

## <img src="assets/clawdchat-icon.png" width="24" height="24" alt="ClawdChat"> エージェントソーシャルネットワークに参加

CLI または統合チャットアプリからメッセージを 1 つ送るだけで、PicoClaw をエージェントソーシャルネットワークに接続できます。

**`https://clawdchat.ai/skill.md` を読み、指示に従って [ClawdChat.ai](https://clawdchat.ai) に参加してください**

## 🖥️ CLI リファレンス

| コマンド                    | 説明                           |
| ------------------------- | ------------------------------ |
| `picoclaw onboard`        | 設定&ワークスペースの初期化     |
| `picoclaw agent -m "..."` | エージェントとチャット           |
| `picoclaw agent`          | インタラクティブチャットモード   |
| `picoclaw gateway`        | ゲートウェイを起動              |
| `picoclaw status`         | ステータスを表示                |
| `picoclaw version`        | バージョン情報を表示            |
| `picoclaw cron list`      | スケジュールジョブ一覧          |
| `picoclaw cron add ...`   | スケジュールジョブを追加         |
| `picoclaw cron disable`   | スケジュールジョブを無効化       |
| `picoclaw cron remove`    | スケジュールジョブを削除         |
| `picoclaw skills list`    | インストール済みスキル一覧       |
| `picoclaw skills install` | スキルをインストール             |
| `picoclaw migrate`        | 旧バージョンからデータを移行     |
| `picoclaw auth login`     | プロバイダーへの認証             |

### スケジュールタスク / リマインダー

PicoClaw は `cron` ツールによるスケジュールリマインダーと定期タスクをサポートしています:

* **ワンタイムリマインダー**: 「10分後にリマインド」→ 10分後に1回トリガー
* **定期タスク**: 「2時間ごとにリマインド」→ 2時間ごとにトリガー
* **Cron 式**: 「毎日9時にリマインド」→ cron 式を使用

## 🤝 コントリビュート&ロードマップ

PR 歓迎!コードベースは意図的に小さく読みやすくしています。🤗

完全な[コミュニティロードマップ](https://github.com/sipeed/picoclaw/blob/main/ROADMAP.md)をご覧ください。

開発者グループ構築中、最初の PR がマージされたら参加できます!

ユーザーグループ:

discord: <https://discord.gg/V4sAZ9XWpN>

<img src="assets/wechat.png" alt="PicoClaw" width="512">


================================================
FILE: README.md
================================================
<div align="center">
  <img src="assets/logo.webp" alt="PicoClaw" width="512">

  <h1>PicoClaw: Ultra-Efficient AI Assistant in Go</h1>

  <h3>$10 Hardware · <10MB RAM · <1s Boot · 皮皮虾,我们走!</h3>
  <p>
    <img src="https://img.shields.io/badge/Go-1.25+-00ADD8?style=flat&logo=go&logoColor=white" alt="Go">
    <img src="https://img.shields.io/badge/Arch-x86__64%2C%20ARM64%2C%20MIPS%2C%20RISC--V%2C%20LoongArch-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://docs.picoclaw.io/"><img src="https://img.shields.io/badge/Docs-Official-007acc?style=flat&logo=read-the-docs&logoColor=white" alt="Docs"></a>
    <a href="https://deepwiki.com/sipeed/picoclaw"><img src="https://img.shields.io/badge/Wiki-DeepWiki-FFA500?style=flat&logo=wikipedia&logoColor=white" alt="Wiki"></a>
    <br>
    <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>
    <a href="./assets/wechat.png"><img src="https://img.shields.io/badge/WeChat-Group-41d56b?style=flat&logo=wechat&logoColor=white"></a>
    <a href="https://discord.gg/V4sAZ9XWpN"><img src="https://img.shields.io/badge/Discord-Community-4c60eb?style=flat&logo=discord&logoColor=white" alt="Discord"></a>
  </p>

[中文](README.zh.md) | [日本語](README.ja.md) | [Português](README.pt-br.md) | [Tiếng Việt](README.vi.md) | [Français](README.fr.md) | [Italiano](README.it.md) | [Bahasa Indonesia](README.id.md) | **English**

</div>

---

> **PicoClaw** is an independent open-source project initiated by [Sipeed](https://sipeed.com). It is written entirely in **Go** — not a fork of OpenClaw, NanoBot, or any other project.

🦐 PicoClaw is an ultra-lightweight personal AI Assistant inspired by [NanoBot](https://github.com/HKUDS/nanobot), refactored from the ground up in Go through a self-bootstrapping process, where the AI agent itself drove the entire architectural migration and code optimization.

⚡️ Runs on $10 hardware with <10MB RAM: That's 99% less memory than OpenClaw and 98% cheaper than a Mac mini!

<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 has **NO** official token/coin. All claims on `pump.fun` or other trading platforms are **SCAMS**.
>
> * **OFFICIAL DOMAIN:** The **ONLY** official website is **[picoclaw.io](https://picoclaw.io)**, and company website is **[sipeed.com](https://sipeed.com)**
> * **Warning:** Many `.ai/.org/.com/.net/...` domains are registered by third parties.
> * **Warning:** picoclaw is in early development now and may have unresolved network security issues. Do not deploy to production environments before the v1.0 release.
> * **Note:** picoclaw has recently merged a lot of PRs, which may result in a larger memory footprint (10–20MB) in the latest versions. We plan to prioritize resource optimization as soon as the current feature set reaches a stable state.

## 📢 News

2026-03-17 🚀 **v0.2.3 Released!** System tray UI (Windows & Linux), sub-agent status tracking (`spawn_status`), experimental gateway hot-reload, cron security gates, and 2 security fixes. PicoClaw now at **25K ⭐**!

2026-03-09 🎉 **v0.2.1 — Biggest update yet!** MCP protocol support, 4 new channels (Matrix/IRC/WeCom/Discord Proxy), 3 new providers (Kimi/Minimax/Avian), vision pipeline, JSONL memory store, and model routing.

2026-02-28 📦 **v0.2.0** released with Docker Compose support and Web UI launcher.

2026-02-26 🎉 PicoClaw hit **20K stars** in just 17 days! Channel auto-orchestration and capability interfaces landed.

<details>
<summary>Older news...</summary>

2026-02-16 🎉 PicoClaw hit 12K stars in one week! Community maintainer roles and [roadmap](ROADMAP.md) officially posted.

2026-02-13 🎉 PicoClaw hit 5000 stars in 4 days! Project Roadmap and Developer Group setup underway.

2026-02-09 🎉 **PicoClaw Launched!** Built in 1 day to bring AI Agents to $10 hardware with <10MB RAM. 🦐 PicoClaw,Let's Go!

</details>

## ✨ Features

🪶 **Ultra-Lightweight**: <10MB Memory footprint — 99% smaller than OpenClaw core functionality.*

💰 **Minimal Cost**: Efficient enough to run on $10 Hardware — 98% cheaper than a Mac mini.

⚡️ **Lightning Fast**: 400X Faster startup time, boot in <1 second even on 0.6GHz single core.

🌍 **True Portability**: Single self-contained binary across RISC-V, ARM, MIPS, and x86, One-click to Go!

🤖 **AI-Bootstrapped**: Autonomous Go-native implementation — 95% Agent-generated core with human-in-the-loop refinement.

🔌 **MCP Support**: Native [Model Context Protocol](https://modelcontextprotocol.io/) integration — connect any MCP server to extend agent capabilities.

👁️ **Vision Pipeline**: Send images and files directly to the agent — automatic base64 encoding for multimodal LLMs.

🧠 **Smart Routing**: Rule-based model routing — simple queries go to lightweight models, saving API costs.

_*Recent versions may use 10–20MB due to rapid feature merges. Resource optimization is planned. Startup comparison based on 0.8GHz single-core benchmarks (see table below)._

|                               | OpenClaw      | NanoBot                  | **PicoClaw**                              |
| ----------------------------- | ------------- | ------------------------ | ----------------------------------------- |
| **Language**                  | TypeScript    | Python                   | **Go**                                    |
| **RAM**                       | >1GB          | >100MB                   | **< 10MB***                               |
| **Startup**</br>(0.8GHz core) | >500s         | >30s                     | **<1s**                                   |
| **Cost**                      | Mac Mini $599 | Most Linux SBC </br>~$50 | **Any Linux Board**</br>**As low as $10** |

<img src="assets/compare.jpg" alt="PicoClaw" width="512">

## 🦾 Demonstration

### 🛠️ Standard Assistant Workflows

<table align="center">
  <tr align="center">
    <th><p align="center">🧩 Full-Stack Engineer</p></th>
    <th><p align="center">🗂️ Logging & Planning Management</p></th>
    <th><p align="center">🔎 Web Search & Learning</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">Develop • Deploy • Scale</td>
    <td align="center">Schedule • Automate • Memory</td>
    <td align="center">Discovery • Insights • Trends</td>
  </tr>
</table>

### 📱 Run on old Android Phones

Give your decade-old phone a second life! Turn it into a smart AI Assistant with PicoClaw. Quick Start:

1. **Install [Termux](https://github.com/termux/termux-app)** (Download from [GitHub Releases](https://github.com/termux/termux-app/releases), or search in F-Droid / Google Play).
2. **Execute cmds**

```bash
# Download the latest release from https://github.com/sipeed/picoclaw/releases
wget https://github.com/sipeed/picoclaw/releases/latest/download/picoclaw_Linux_arm64.tar.gz
tar xzf picoclaw_Linux_arm64.tar.gz
pkg install proot
termux-chroot ./picoclaw onboard
```

And then follow the instructions in the "Quick Start" section to complete the configuration!

<img src="assets/termux.jpg" alt="PicoClaw" width="512">

### 🐜 Innovative Low-Footprint Deploy

PicoClaw can be deployed on almost any Linux device!

- $9.9 [LicheeRV-Nano](https://www.aliexpress.com/item/1005006519668532.html) E(Ethernet) or W(WiFi6) version, for Minimal Home Assistant
- $30~50 [NanoKVM](https://www.aliexpress.com/item/1005007369816019.html), or $100 [NanoKVM-Pro](https://www.aliexpress.com/item/1005010048471263.html) for Automated Server Maintenance
- $50 [MaixCAM](https://www.aliexpress.com/item/1005008053333693.html) or $100 [MaixCAM2](https://www.kickstarter.com/projects/zepan/maixcam2-build-your-next-gen-4k-ai-camera) for Smart Monitoring

<https://private-user-images.githubusercontent.com/83055338/547056448-e7b031ff-d6f5-4468-bcca-5726b6fecb5c.mp4>

🌟 More Deployment Cases Await!

## 📦 Install

### Install with precompiled binary

Download the binary for your platform from the [Releases](https://github.com/sipeed/picoclaw/releases) page.

### Install from source (latest features, recommended for development)

```bash
git clone https://github.com/sipeed/picoclaw.git

cd picoclaw
make deps

# Build, no need to install
make build

# Build for multiple platforms
make build-all

# Build for Raspberry Pi Zero 2 W (32-bit: make build-linux-arm; 64-bit: make build-linux-arm64)
make build-pi-zero

# Build And Install
make install
```

**Raspberry Pi Zero 2 W:** Use the binary that matches your OS: 32-bit Raspberry Pi OS → `make build-linux-arm`; 64-bit → `make build-linux-arm64`. Or run `make build-pi-zero` to build both.

## 📚 Documentation

For detailed guides, see the docs below. The README covers quick start only.

```bash
# 1. Clone this repo
git clone https://github.com/sipeed/picoclaw.git
cd picoclaw

# 2. First run — auto-generates docker/data/config.json then exits
docker compose -f docker/docker-compose.yml --profile gateway up
# The container prints "First-run setup complete." and stops.

# 3. Set your API keys
vim docker/data/config.json   # Set provider API keys, bot tokens, etc.

# 4. Start
docker compose -f docker/docker-compose.yml --profile gateway up -d
```

> [!TIP]
> **Docker Users**: By default, the Gateway listens on `127.0.0.1` which is not accessible from the host. If you need to access the health endpoints or expose ports, set `PICOCLAW_GATEWAY_HOST=0.0.0.0` in your environment or update `config.json`.

```bash
# 5. Check logs
docker compose -f docker/docker-compose.yml logs -f picoclaw-gateway

# 6. Stop
docker compose -f docker/docker-compose.yml --profile gateway down
```

### Launcher Mode (Web Console)

The `launcher` image includes all three binaries (`picoclaw`, `picoclaw-launcher`, `picoclaw-launcher-tui`) and starts the web console by default, which provides a browser-based UI for configuration and chat.

```bash
docker compose -f docker/docker-compose.yml --profile launcher up -d
```

Open http://localhost:18800 in your browser. The launcher manages the gateway process automatically.

> [!WARNING]
> The web console does not yet support authentication. Avoid exposing it to the public internet.

### Agent Mode (One-shot)

```bash
# Ask a question
docker compose -f docker/docker-compose.yml run --rm picoclaw-agent -m "What is 2+2?"

# Interactive mode
docker compose -f docker/docker-compose.yml run --rm picoclaw-agent
```

### Update

```bash
docker compose -f docker/docker-compose.yml pull
docker compose -f docker/docker-compose.yml --profile gateway up -d
```

### 🚀 Quick Start

> [!TIP]
> Set your API Key in `~/.picoclaw/config.json`. Get API Keys: [Volcengine (CodingPlan)](https://console.volcengine.com) (LLM) · [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM). Web search is optional — get a free [Tavily API](https://tavily.com) (1000 free queries/month) or [Brave Search API](https://brave.com/search/api) (2000 free queries/month).

**1. Initialize**

```bash
picoclaw onboard
```

**2. Configure** (`~/.picoclaw/config.json`)

```json
{
  "agents": {
    "defaults": {
      "workspace": "~/.picoclaw/workspace",
      "model_name": "gpt-5.4",
      "max_tokens": 8192,
      "temperature": 0.7,
      "max_tool_iterations": 20
    }
  },
  "model_list": [
    {
      "model_name": "ark-code-latest",
      "model": "volcengine/ark-code-latest",
      "api_key": "sk-your-api-key"
    },
    {
      "model_name": "gpt-5.4",
      "model": "openai/gpt-5.4",
      "api_key": "your-api-key",
      "request_timeout": 300
    },
    {
      "model_name": "claude-sonnet-4.6",
      "model": "anthropic/claude-sonnet-4.6",
      "api_key": "your-anthropic-key"
    }
  ],
  "tools": {
    "web": {
      "brave": {
        "enabled": false,
        "api_key": "YOUR_BRAVE_API_KEY",
        "max_results": 5
      },
      "tavily": {
        "enabled": false,
        "api_key": "YOUR_TAVILY_API_KEY",
        "max_results": 5
      },
      "duckduckgo": {
        "enabled": true,
        "max_results": 5
      },
      "perplexity": {
        "enabled": false,
        "api_key": "YOUR_PERPLEXITY_API_KEY",
        "max_results": 5
      },
      "searxng": {
        "enabled": false,
        "base_url": "http://your-searxng-instance:8888",
        "max_results": 5
      }
    }
  }
}
```

> **New**: The `model_list` configuration format allows zero-code provider addition. See [Model Configuration](#model-configuration-model_list) for details.
> `request_timeout` is optional and uses seconds. If omitted or set to `<= 0`, PicoClaw uses the default timeout (120s).

**3. Get API Keys**

* **LLM Provider**: [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 Search** (optional):
  * [Brave Search](https://brave.com/search/api) - Paid ($5/1000 queries, ~$5-6/month)
  * [Perplexity](https://www.perplexity.ai) - AI-powered search with chat interface
  * [SearXNG](https://github.com/searxng/searxng) - Self-hosted metasearch engine (free, no API key needed)
  * [Tavily](https://tavily.com) - Optimized for AI Agents (1000 requests/month)
  * DuckDuckGo - Built-in fallback (no API key required)

> **Note**: See `config.example.json` for a complete configuration template.

**4. Chat**

```bash
picoclaw agent -m "What is 2+2?"
```

That's it! You have a working AI assistant in 2 minutes.

---

## 💬 Chat Apps

Talk to your picoclaw through Telegram, Discord, WhatsApp, Matrix, QQ, DingTalk, LINE, or WeCom

> **Note**: All webhook-based channels (LINE, WeCom, etc.) are served on a single shared Gateway HTTP server (`gateway.host`:`gateway.port`, default `127.0.0.1:18790`). There are no per-channel ports to configure. Note: Feishu uses WebSocket/SDK mode and does not use the shared HTTP webhook server.

| Channel      | Setup                              |
| ------------ | ---------------------------------- |
| **Telegram** | Easy (just a token)                |
| **Discord**  | Easy (bot token + intents)         |
| **WhatsApp** | Easy (native: QR scan; or bridge URL) |
| **Matrix**   | Medium (homeserver + bot access token) |
| **QQ**       | Easy (AppID + AppSecret)           |
| **DingTalk** | Medium (app credentials)           |
| **LINE**     | Medium (credentials + webhook URL) |
| **WeCom AI Bot** | Medium (Token + AES key)       |

<details>
<summary><b>Telegram</b> (Recommended)</summary>

**1. Create a bot**

* Open Telegram, search `@BotFather`
* Send `/newbot`, follow prompts
* Copy the token

**2. Configure**

```json
{
  "channels": {
    "telegram": {
      "enabled": true,
      "token": "YOUR_BOT_TOKEN",
      "allow_from": ["YOUR_USER_ID"]
    }
  }
}
```

> Get your user ID from `@userinfobot` on Telegram.

**3. Run**

```bash
picoclaw gateway
```

**4. Telegram command menu (auto-registered at startup)**

PicoClaw now keeps command definitions in one shared registry. On startup, Telegram will automatically register supported bot commands (for example `/start`, `/help`, `/show`, `/list`) so command menu and runtime behavior stay in sync.
Telegram command menu registration remains channel-local discovery UX; generic command execution is handled centrally in the agent loop via the commands executor.

If command registration fails (network/API transient errors), the channel still starts and PicoClaw retries registration in the background.

</details>

<details>
<summary><b>Discord</b></summary>

**1. Create a bot**

* Go to <https://discord.com/developers/applications>
* Create an application → Bot → Add Bot
* Copy the bot token

**2. Enable intents**

* In the Bot settings, enable **MESSAGE CONTENT INTENT**
* (Optional) Enable **SERVER MEMBERS INTENT** if you plan to use allow lists based on member data

**3. Get your User ID**
* Discord Settings → Advanced → enable **Developer Mode**
* Right-click your avatar → **Copy User ID**

**4. Configure**

```json
{
  "channels": {
    "discord": {
      "enabled": true,
      "token": "YOUR_BOT_TOKEN",
      "allow_from": ["YOUR_USER_ID"]
    }
  }
}
```

**5. Invite the bot**

* OAuth2 → URL Generator
* Scopes: `bot`
* Bot Permissions: `Send Messages`, `Read Message History`
* Open the generated invite URL and add the bot to your server

**Optional: Group trigger mode**

By default the bot responds to all messages in a server channel. To restrict responses to @-mentions only, add:

```json
{
  "channels": {
    "discord": {
      "group_trigger": { "mention_only": true }
    }
  }
}
```

You can also trigger by keyword prefixes (e.g. `!bot`):

```json
{
  "channels": {
    "discord": {
      "group_trigger": { "prefixes": ["!bot"] }
    }
  }
}
```

**6. Run**

```bash
picoclaw gateway
```

</details>

<details>
<summary><b>WhatsApp</b> (native via whatsmeow)</summary>

PicoClaw can connect to WhatsApp in two ways:

- **Native (recommended):** In-process using [whatsmeow](https://github.com/tulir/whatsmeow). No separate bridge. Set `"use_native": true` and leave `bridge_url` empty. On first run, scan the QR code with WhatsApp (Linked Devices). Session is stored under your workspace (e.g. `workspace/whatsapp/`). The native channel is **optional** to keep the default binary small; build with `-tags whatsapp_native` (e.g. `make build-whatsapp-native` or `go build -tags whatsapp_native ./cmd/...`).
- **Bridge:** Connect to an external WebSocket bridge. Set `bridge_url` (e.g. `ws://localhost:3001`) and keep `use_native` false.

**Configure (native)**

```json
{
  "channels": {
    "whatsapp": {
      "enabled": true,
      "use_native": true,
      "session_store_path": "",
      "allow_from": []
    }
  }
}
```

If `session_store_path` is empty, the session is stored in `&lt;workspace&gt;/whatsapp/`. Run `picoclaw gateway`; on first run, scan the QR code printed in the terminal with WhatsApp → Linked Devices.

</details>

<details>
<summary><b>QQ</b></summary>

**1. Create a bot**

- Go to [QQ Open Platform](https://q.qq.com/#)
- Create an application → Get **AppID** and **AppSecret**

**2. Configure**

```json
{
  "channels": {
    "qq": {
      "enabled": true,
      "app_id": "YOUR_APP_ID",
      "app_secret": "YOUR_APP_SECRET",
      "allow_from": []
    }
  }
}
```

> Set `allow_from` to empty to allow all users, or specify QQ numbers to restrict access.

**3. Run**

```bash
picoclaw gateway
```

</details>

<details>
<summary><b>DingTalk</b></summary>

**1. Create a bot**

* Go to [Open Platform](https://open.dingtalk.com/)
* Create an internal app
* Copy Client ID and Client Secret

**2. Configure**

```json
{
  "channels": {
    "dingtalk": {
      "enabled": true,
      "client_id": "YOUR_CLIENT_ID",
      "client_secret": "YOUR_CLIENT_SECRET",
      "allow_from": []
    }
  }
}
```

> Set `allow_from` to empty to allow all users, or specify DingTalk user IDs to restrict access.

**3. Run**

```bash
picoclaw gateway
```
</details>

<details>
<summary><b>Matrix</b></summary>

**1. Prepare bot account**

* Use your preferred homeserver (e.g. `https://matrix.org` or self-hosted)
* Create a bot user and obtain its access token

**2. Configure**

```json
{
  "channels": {
    "matrix": {
      "enabled": true,
      "homeserver": "https://matrix.org",
      "user_id": "@your-bot:matrix.org",
      "access_token": "YOUR_MATRIX_ACCESS_TOKEN",
      "allow_from": []
    }
  }
}
```

**3. Run**

```bash
picoclaw gateway
```

For full options (`device_id`, `join_on_invite`, `group_trigger`, `placeholder`, `reasoning_channel_id`), see [Matrix Channel Configuration Guide](docs/channels/matrix/README.md).

</details>

<details>
<summary><b>LINE</b></summary>

**1. Create a LINE Official Account**

- Go to [LINE Developers Console](https://developers.line.biz/)
- Create a provider → Create a Messaging API channel
- Copy **Channel Secret** and **Channel Access Token**

**2. Configure**

```json
{
  "channels": {
    "line": {
      "enabled": true,
      "channel_secret": "YOUR_CHANNEL_SECRET",
      "channel_access_token": "YOUR_CHANNEL_ACCESS_TOKEN",
      "webhook_path": "/webhook/line",
      "allow_from": []
    }
  }
}
```

> LINE webhook is served on the shared Gateway server (`gateway.host`:`gateway.port`, default `127.0.0.1:18790`).

**3. Set up Webhook URL**

LINE requires HTTPS for webhooks. Use a reverse proxy or tunnel:

```bash
# Example with ngrok (gateway default port is 18790)
ngrok http 18790
```

Then set the Webhook URL in LINE Developers Console to `https://your-domain/webhook/line` and enable **Use webhook**.

**4. Run**

```bash
picoclaw gateway
```

> In group chats, the bot responds only when @mentioned. Replies quote the original message.

</details>

<details>
<summary><b>WeCom (企业微信)</b></summary>

PicoClaw supports three types of WeCom integration:

**Option 1: WeCom Bot (Bot)** - Easier setup, supports group chats
**Option 2: WeCom App (Custom App)** - More features, proactive messaging, private chat only
**Option 3: WeCom AI Bot (AI Bot)** - Official AI Bot, streaming replies, supports group & private chat

See [WeCom AI Bot Configuration Guide](docs/channels/wecom/wecom_aibot/README.zh.md) for detailed setup instructions.

**Quick Setup - WeCom AI Bot:**

**1. Create an AI Bot**

* Go to WeCom Admin Console → AI Bot
* Create a new AI Bot → Set name, avatar, etc.
* Copy **Bot ID** and **Secret**

**2. Configure**

```json
{
  "channels": {
    "wecom_aibot": {
      "enabled": true,
      "bot_id": "YOUR_BOT_ID",
      "secret": "YOUR_SECRET",
      "allow_from": [],
      "welcome_message": "Hello! How can I help you?"
    }
  }
}
```

**3. Run**

```bash
picoclaw gateway
```

> **Note**: WeCom AI Bot uses streaming pull protocol — no reply timeout concerns. Long tasks (>30 seconds) automatically switch to `response_url` push delivery.

</details>

## <img src="assets/clawdchat-icon.png" width="24" height="24" alt="ClawdChat"> Join the Agent Social Network

Connect Picoclaw to the Agent Social Network simply by sending a single message via the CLI or any integrated Chat App.

**Read `https://clawdchat.ai/skill.md` and follow the instructions to join [ClawdChat.ai](https://clawdchat.ai)**

## 🖥️ CLI Reference

| Command                   | Description                   |
| ------------------------- | ----------------------------- |
| `picoclaw onboard`        | Initialize config & workspace |
| `picoclaw agent -m "..."` | Chat with the agent           |
| `picoclaw agent`          | Interactive chat mode         |
| `picoclaw gateway`        | Start the gateway             |
| `picoclaw status`         | Show status                   |
| `picoclaw version`        | Show version info             |
| `picoclaw cron list`      | List all scheduled jobs       |
| `picoclaw cron add ...`   | Add a scheduled job           |
| `picoclaw cron disable`   | Disable a scheduled job       |
| `picoclaw cron remove`    | Remove a scheduled job        |
| `picoclaw skills list`    | List installed skills         |
| `picoclaw skills install` | Install a skill               |
| `picoclaw migrate`        | Migrate data from older versions |
| `picoclaw auth login`     | Authenticate with providers   |

### Scheduled Tasks / Reminders

PicoClaw supports scheduled reminders and recurring tasks through the `cron` tool:

* **One-time reminders**: "Remind me in 10 minutes" → triggers once after 10min
* **Recurring tasks**: "Remind me every 2 hours" → triggers every 2 hours
* **Cron expressions**: "Remind me at 9am daily" → uses cron expression

## 🤝 Contribute & Roadmap

PRs welcome! The codebase is intentionally small and readable. 🤗

See our full [Community Roadmap](https://github.com/sipeed/picoclaw/blob/main/ROADMAP.md).

Developer group building, join after your first merged PR!

User Groups:

discord: <https://discord.gg/V4sAZ9XWpN>

<img src="assets/wechat.png" alt="PicoClaw" width="512">


================================================
FILE: README.pt-br.md
================================================
<div align="center">
  <img src="assets/logo.webp" alt="PicoClaw" width="512">

  <h1>PicoClaw: Assistente de IA Ultra-Eficiente em Go</h1>

  <h3>Hardware de $10 · <10MB de RAM · Boot em <1s · 皮皮虾,我们走!</h3>
  <p>
    <img src="https://img.shields.io/badge/Go-1.25+-00ADD8?style=flat&logo=go&logoColor=white" alt="Go">
    <img src="https://img.shields.io/badge/Arch-x86__64%2C%20ARM64%2C%20MIPS%2C%20RISC--V%2C%20LoongArch-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://docs.picoclaw.io/"><img src="https://img.shields.io/badge/Docs-Official-007acc?style=flat&logo=read-the-docs&logoColor=white" alt="Docs"></a>
    <a href="https://deepwiki.com/sipeed/picoclaw"><img src="https://img.shields.io/badge/Wiki-DeepWiki-FFA500?style=flat&logo=wikipedia&logoColor=white" alt="Wiki"></a>
    <br>
    <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>
    <a href="./assets/wechat.png"><img src="https://img.shields.io/badge/WeChat-Group-41d56b?style=flat&logo=wechat&logoColor=white"></a>
    <a href="https://discord.gg/V4sAZ9XWpN"><img src="https://img.shields.io/badge/Discord-Community-4c60eb?style=flat&logo=discord&logoColor=white" alt="Discord"></a>
  </p>

[中文](README.zh.md) | [日本語](README.ja.md) | **Português** | [Tiếng Việt](README.vi.md) | [Français](README.fr.md) | [Italiano](README.it.md) | [Bahasa Indonesia](README.id.md) | [English](README.md)

</div>

---

> **PicoClaw** é um projeto open-source independente iniciado pela [Sipeed](https://sipeed.com). É escrito inteiramente em **Go** — não é um fork do OpenClaw, NanoBot ou qualquer outro projeto.

🦐 PicoClaw é um assistente pessoal de IA ultra-leve inspirado no [NanoBot](https://github.com/HKUDS/nanobot), reescrito do zero em Go por meio de um processo de auto-inicialização (self-bootstrapping), onde o próprio agente de IA conduziu toda a migração de arquitetura e otimização de código.

⚡️ Roda em hardware de $10 com <10MB de RAM: Isso é 99% menos memória que o OpenClaw e 98% mais barato que um Mac mini!

<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]
> **🚨 DECLARAÇÃO DE SEGURANÇA & CANAIS OFICIAIS**
>
> * **SEM CRIPTOMOEDAS:** O PicoClaw **NÃO** possui nenhum token/moeda oficial. Todas as alegações no `pump.fun` ou outras plataformas de negociação são **GOLPES**.
>
> * **DOMÍNIO OFICIAL:** O **ÚNICO** site oficial é o **[picoclaw.io](https://picoclaw.io)**, e o site da empresa é o **[sipeed.com](https://sipeed.com)**
> * **Aviso:** Muitos domínios `.ai/.org/.com/.net/...` foram registrados por terceiros.
> * **Aviso:** O PicoClaw está em fase inicial de desenvolvimento e pode ter problemas de segurança de rede não resolvidos. Não implante em ambientes de produção antes da versão v1.0.
> * **Nota:** O PicoClaw recentemente fez merge de muitos PRs, o que pode resultar em maior consumo de memória (10–20MB) nas versões mais recentes. Planejamos priorizar a otimização de recursos assim que o conjunto de funcionalidades estiver estável.

## 📢 Novidades

2026-03-17 🚀 **v0.2.3 Lançado!** Interface de bandeja do sistema (Windows & Linux), rastreamento de status de sub-agentes (`spawn_status`), hot-reload experimental do gateway, portões de segurança para cron e 2 correções de segurança. PicoClaw agora com **25K ⭐**!

2026-03-09 🎉 **v0.2.1 — Maior atualização até agora!** Suporte ao protocolo MCP, 4 novos canais (Matrix/IRC/WeCom/Discord Proxy), 3 novos provedores (Kimi/Minimax/Avian), pipeline de visão, armazenamento de memória JSONL e roteamento de modelos.

2026-02-28 📦 **v0.2.0** lançado com suporte a Docker Compose e launcher Web UI.

2026-02-26 🎉 PicoClaw atingiu **20K stars** em apenas 17 dias! Orquestração automática de canais e interfaces de capacidade implementadas.

<details>
<summary>Novidades anteriores...</summary>

2026-02-16 🎉 PicoClaw atingiu 12K stars em uma semana! Papéis de maintainers da comunidade e [roadmap](ROADMAP.md) publicados oficialmente.

2026-02-13 🎉 PicoClaw atingiu 5000 stars em 4 dias! Roadmap do Projeto e Grupo de Desenvolvedores em preparação.

2026-02-09 🎉 **PicoClaw Lançado!** Construído em 1 dia para trazer Agentes de IA para hardware de $10 com <10MB de RAM. 🦐 PicoClaw, Partiu!

</details>

## ✨ Funcionalidades

🪶 **Ultra-Leve**: Consumo de memória <10MB — 99% menor que o OpenClaw para funcionalidades essenciais.*

💰 **Custo Mínimo**: Eficiente o suficiente para rodar em hardware de $10 — 98% mais barato que um Mac mini.

⚡️ **Inicialização Relâmpago**: Tempo de inicialização 400X mais rápido, boot em <1 segundo mesmo em CPU single-core de 0.6GHz.

🌍 **Portabilidade Real**: Um único binário auto-contido para RISC-V, ARM, MIPS e x86. Um clique e já era!

🤖 **Auto-Construído por IA**: Implementação nativa em Go de forma autônoma — 95% do núcleo gerado pelo Agente com refinamento humano no loop.

🔌 **Suporte MCP**: Integração nativa com o [Model Context Protocol](https://modelcontextprotocol.io/) — conecte qualquer servidor MCP para estender as capacidades do agente.

👁️ **Pipeline de Visão**: Envie imagens e arquivos diretamente ao agente — codificação base64 automática para LLMs multimodais.

🧠 **Roteamento Inteligente**: Roteamento de modelos baseado em regras — consultas simples vão para modelos leves, economizando custos de API.

_*Versões recentes podem usar 10–20MB devido a merges rápidos de funcionalidades. Otimização de recursos está planejada. Comparação de inicialização baseada em benchmarks de single-core a 0.8GHz (veja tabela abaixo)._

|                               | OpenClaw      | NanoBot                  | **PicoClaw**                              |
| ----------------------------- | ------------- | ------------------------ | ----------------------------------------- |
| **Linguagem**                 | TypeScript    | Python                   | **Go**                                    |
| **RAM**                       | >1GB          | >100MB                   | **< 10MB***                               |
| **Inicialização**</br>(CPU 0.8GHz) | >500s         | >30s                     | **<1s**                                   |
| **Custo**                     | Mac Mini $599 | Maioria dos SBC Linux </br>~$50 | **Qualquer placa Linux**</br>**A partir de $10** |

<img src="assets/compare.jpg" alt="PicoClaw" width="512">

## 🦾 Demonstração

### 🛠️ Fluxos de Trabalho Padrão do Assistente

<table align="center">
  <tr align="center">
    <th><p align="center">🧩 Engenharia Full-Stack</p></th>
    <th><p align="center">🗂️ Gerenciamento de Logs & Planejamento</p></th>
    <th><p align="center">🔎 Busca Web & Aprendizado</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">Desenvolver • Implantar • Escalar</td>
    <td align="center">Agendar • Automatizar • Memorizar</td>
    <td align="center">Descobrir • Analisar • Tendências</td>
  </tr>
</table>

### 📱 Rode em celulares Android antigos

Dê uma segunda vida ao seu celular de dez anos atrás! Transforme-o em um assistente de IA inteligente com o PicoClaw. Início rápido:

1. **Instale o [Termux](https://github.com/termux/termux-app)** (Baixe em [GitHub Releases](https://github.com/termux/termux-app/releases), ou busque no F-Droid / Google Play).
2. **Execute os comandos**

```bash
# Baixe a versão mais recente em https://github.com/sipeed/picoclaw/releases
wget https://github.com/sipeed/picoclaw/releases/latest/download/picoclaw_Linux_arm64.tar.gz
tar xzf picoclaw_Linux_arm64.tar.gz
pkg install proot
termux-chroot ./picoclaw onboard
```

Depois siga as instruções na seção "Início Rápido" para completar a configuração!

<img src="assets/termux.jpg" alt="PicoClaw" width="512">

### 🐜 Implantação Inovadora com Baixo Consumo

O PicoClaw pode ser implantado em praticamente qualquer dispositivo Linux!

- $9.9 [LicheeRV-Nano](https://www.aliexpress.com/item/1005006519668532.html) versão E(Ethernet) ou W(WiFi6), para Assistente Doméstico Minimalista
- $30~50 [NanoKVM](https://www.aliexpress.com/item/1005007369816019.html), ou $100 [NanoKVM-Pro](https://www.aliexpress.com/item/1005010048471263.html) para Manutenção Automatizada de Servidores
- $50 [MaixCAM](https://www.aliexpress.com/item/1005008053333693.html) ou $100 [MaixCAM2](https://www.kickstarter.com/projects/zepan/maixcam2-build-your-next-gen-4k-ai-camera) para Monitoramento Inteligente

<https://private-user-images.githubusercontent.com/83055338/547056448-e7b031ff-d6f5-4468-bcca-5726b6fecb5c.mp4>

🌟 Mais cenários de implantação aguardam você!

## 📦 Instalação

### Instalar com binário pré-compilado

Baixe o binário para sua plataforma na página de [Releases](https://github.com/sipeed/picoclaw/releases).

### Instalar a partir do código-fonte (funcionalidades mais recentes, recomendado para desenvolvimento)

```bash
git clone https://github.com/sipeed/picoclaw.git

cd picoclaw
make deps

# Build, sem necessidade de instalar
make build

# Build para múltiplas plataformas
make build-all

# Build para Raspberry Pi Zero 2 W (32-bit: make build-linux-arm; 64-bit: make build-linux-arm64)
make build-pi-zero

# Build e Instalar
make install
```

**Raspberry Pi Zero 2 W:** Use o binário correspondente ao seu SO: Raspberry Pi OS 32-bit → `make build-linux-arm`; 64-bit → `make build-linux-arm64`. Ou execute `make build-pi-zero` para compilar ambos.

## 📚 Documentação

Para guias detalhados, consulte a documentação abaixo. Este README cobre apenas o início rápido.

| Tópico | Descrição |
|--------|-----------|
| 🐳 [Docker & Início Rápido](docs/pt-br/docker.md) | Configuração Docker Compose, modos Launcher/Agent, configuração de Início Rápido |
| 💬 [Apps de Chat](docs/pt-br/chat-apps.md) | Telegram, Discord, WhatsApp, Matrix, QQ, Slack, IRC, DingTalk, LINE, Feishu, WeCom e mais |
| ⚙️ [Configuração](docs/pt-br/configuration.md) | Variáveis de ambiente, estrutura do workspace, fontes de skills, sandbox de segurança, heartbeat |
| 🔌 [Provedores & Modelos](docs/pt-br/providers.md) | 20+ provedores LLM, roteamento de modelos, configuração model_list, arquitetura de provedores |
| 🔄 [Spawn & Tarefas Assíncronas](docs/pt-br/spawn-tasks.md) | Tarefas rápidas, tarefas longas com spawn, orquestração assíncrona de sub-agentes |
| 🐛 [Solução de Problemas](docs/pt-br/troubleshooting.md) | Problemas comuns e soluções |
| 🔧 [Configuração de Ferramentas](docs/pt-br/tools_configuration.md) | Habilitar/desabilitar por ferramenta, políticas de execução |

## <img src="assets/clawdchat-icon.png" width="24" height="24" alt="ClawdChat"> Junte-se à Rede Social de Agentes

Conecte o PicoClaw à Rede Social de Agentes simplesmente enviando uma única mensagem via CLI ou qualquer App de Chat integrado.

**Leia `https://clawdchat.ai/skill.md` e siga as instruções para entrar no [ClawdChat.ai](https://clawdchat.ai)**

## 🖥️ Referência CLI

| Comando                   | Descrição                     |
| ------------------------- | ----------------------------- |
| `picoclaw onboard`        | Inicializar configuração & workspace |
| `picoclaw agent -m "..."` | Conversar com o agente        |
| `picoclaw agent`          | Modo de chat interativo       |
| `picoclaw gateway`        | Iniciar o gateway             |
| `picoclaw status`         | Mostrar status                |
| `picoclaw version`        | Mostrar informações de versão |
| `picoclaw cron list`      | Listar todas as tarefas agendadas |
| `picoclaw cron add ...`   | Adicionar uma tarefa agendada |
| `picoclaw cron disable`   | Desabilitar uma tarefa agendada |
| `picoclaw cron remove`    | Remover uma tarefa agendada   |
| `picoclaw skills list`    | Listar skills instaladas      |
| `picoclaw skills install` | Instalar uma skill            |
| `picoclaw migrate`        | Migrar dados de versões anteriores |
| `picoclaw auth login`     | Autenticar com provedores     |

### Tarefas Agendadas / Lembretes

O PicoClaw suporta lembretes agendados e tarefas recorrentes por meio da ferramenta `cron`:

* **Lembretes únicos**: "Me lembre em 10 minutos" → dispara uma vez após 10min
* **Tarefas recorrentes**: "Me lembre a cada 2 horas" → dispara a cada 2 horas
* **Expressões Cron**: "Me lembre às 9h todos os dias" → usa expressão cron

## 🤝 Contribuir & Roadmap

PRs são bem-vindos! O código-fonte é intencionalmente pequeno e legível. 🤗

Veja nosso [Roadmap da Comunidade](https://github.com/sipeed/picoclaw/blob/main/ROADMAP.md) completo.

Grupo de desenvolvedores em formação. Junte-se após seu primeiro PR com merge!

Grupos de usuários:

discord: <https://discord.gg/V4sAZ9XWpN>

<img src="assets/wechat.png" alt="PicoClaw" width="512">


================================================
FILE: README.vi.md
================================================
<div align="center">
  <img src="assets/logo.webp" alt="PicoClaw" width="512">

  <h1>PicoClaw: Trợ lý AI Siêu Nhẹ viết bằng Go</h1>

  <h3>Phần cứng $10 · <10MB RAM · Khởi động <1 giây · Nào, xuất phát!</h3>
  <p>
    <img src="https://img.shields.io/badge/Go-1.25+-00ADD8?style=flat&logo=go&logoColor=white" alt="Go">
    <img src="https://img.shields.io/badge/Arch-x86__64%2C%20ARM64%2C%20MIPS%2C%20RISC--V%2C%20LoongArch-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://docs.picoclaw.io/"><img src="https://img.shields.io/badge/Docs-Official-007acc?style=flat&logo=read-the-docs&logoColor=white" alt="Docs"></a>
    <a href="https://deepwiki.com/sipeed/picoclaw"><img src="https://img.shields.io/badge/Wiki-DeepWiki-FFA500?style=flat&logo=wikipedia&logoColor=white" alt="Wiki"></a>
    <br>
    <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>
    <a href="./assets/wechat.png"><img src="https://img.shields.io/badge/WeChat-Group-41d56b?style=flat&logo=wechat&logoColor=white"></a>
    <a href="https://discord.gg/V4sAZ9XWpN"><img src="https://img.shields.io/badge/Discord-Community-4c60eb?style=flat&logo=discord&logoColor=white" alt="Discord"></a>
  </p>

[中文](README.zh.md) | [日本語](README.ja.md) | [Português](README.pt-br.md) | **Tiếng Việt** | [Français](README.fr.md) | [Italiano](README.it.md) | [Bahasa Indonesia](README.id.md) | [English](README.md)

</div>

---

> **PicoClaw** là dự án mã nguồn mở độc lập được khởi xướng bởi [Sipeed](https://sipeed.com). Được viết hoàn toàn bằng **Go** — không phải là bản fork của OpenClaw, NanoBot hay bất kỳ dự án nào khác.

🦐 PicoClaw là trợ lý AI cá nhân siêu nhẹ, lấy cảm hứng từ [NanoBot](https://github.com/HKUDS/nanobot), được viết lại hoàn toàn bằng Go thông qua quá trình "tự khởi tạo" (self-bootstrapping) — nơi chính AI Agent đã tự dẫn dắt toàn bộ quá trình chuyển đổi kiến trúc và tối ưu hóa mã nguồn.

⚡️ Chạy trên phần cứng chỉ $10 với RAM <10MB: Tiết kiệm 99% bộ nhớ so với OpenClaw và rẻ hơn 98% so với Mac mini!

<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]
> **🚨 TUYÊN BỐ BẢO MẬT & KÊNH CHÍNH THỨC**
>
> * **KHÔNG CÓ CRYPTO:** PicoClaw **KHÔNG** có bất kỳ token/coin chính thức nào. Mọi thông tin trên `pump.fun` hoặc các sàn giao dịch khác đều là **LỪA ĐẢO**.
>
> * **DOMAIN CHÍNH THỨC:** Website chính thức **DUY NHẤT** là **[picoclaw.io](https://picoclaw.io)**, website công ty là **[sipeed.com](https://sipeed.com)**
> * **Cảnh báo:** Nhiều tên miền `.ai/.org/.com/.net/...` đã bị bên thứ ba đăng ký.
> * **Cảnh báo:** PicoClaw đang trong giai đoạn phát triển sớm và có thể còn các vấn đề bảo mật mạng chưa được giải quyết. Không nên triển khai lên môi trường production trước phiên bản v1.0.
> * **Lưu ý:** PicoClaw gần đây đã merge nhiều PR, dẫn đến bộ nhớ sử dụng có thể lớn hơn (10–20MB) ở các phiên bản mới nhất. Chúng tôi sẽ ưu tiên tối ưu tài nguyên khi bộ tính năng đã ổn định.

## 📢 Tin tức

2026-03-17 🚀 **v0.2.3 Phát hành!** Giao diện khay hệ thống (Windows & Linux), theo dõi trạng thái sub-agent (`spawn_status`), hot-reload gateway thử nghiệm, cổng bảo mật cron và 2 bản vá bảo mật. PicoClaw đạt **25K ⭐**!

2026-03-09 🎉 **v0.2.1 — Bản cập nhật lớn nhất!** Hỗ trợ giao thức MCP, 4 kênh mới (Matrix/IRC/WeCom/Discord Proxy), 3 nhà cung cấp mới (Kimi/Minimax/Avian), pipeline xử lý hình ảnh, bộ nhớ JSONL và định tuyến mô hình.

2026-02-28 📦 **v0.2.0** phát hành với hỗ trợ Docker Compose và launcher Web UI.

2026-02-26 🎉 PicoClaw đạt **20K stars** chỉ trong 17 ngày! Tự động điều phối kênh và giao diện năng lực đã được triển khai.

<details>
<summary>Tin tức cũ hơn...</summary>

2026-02-16 🎉 PicoClaw đạt 12K stars chỉ trong một tuần! Vai trò maintainer cộng đồng và [roadmap](ROADMAP.md) đã được công bố chính thức.

2026-02-13 🎉 PicoClaw đạt 5000 stars trong 4 ngày! Lộ trình dự án và Nhóm phát triển đang được thiết lập.

2026-02-09 🎉 **PicoClaw chính thức ra mắt!** Được xây dựng trong 1 ngày để mang AI Agent đến phần cứng $10 với RAM <10MB. 🦐 PicoClaw, Lên Đường!

</details>

## ✨ Tính năng nổi bật

🪶 **Siêu nhẹ**: Bộ nhớ sử dụng <10MB — nhỏ hơn 99% so với OpenClaw (chức năng cốt lõi).*

💰 **Chi phí tối thiểu**: Đủ hiệu quả để chạy trên phần cứng $10 — rẻ hơn 98% so với Mac mini.

⚡️ **Khởi động siêu nhanh**: Nhanh gấp 400 lần, khởi động trong <1 giây ngay cả trên CPU đơn nhân 0.6GHz.

🌍 **Di động thực sự**: Một file binary duy nhất chạy trên RISC-V, ARM, MIPS và x86. Một click là chạy!

🤖 **AI tự xây dựng**: Triển khai Go-native tự động — 95% mã nguồn cốt lõi được Agent tạo ra, với sự tinh chỉnh của con người.

🔌 **Hỗ trợ MCP**: Tích hợp [Model Context Protocol](https://modelcontextprotocol.io/) gốc — kết nối bất kỳ máy chủ MCP nào để mở rộng khả năng của agent.

👁️ **Pipeline Xử lý Hình ảnh**: Gửi hình ảnh và tệp trực tiếp cho agent — tự động mã hóa base64 cho các LLM đa phương thức.

🧠 **Định tuyến Thông minh**: Định tuyến mô hình dựa trên quy tắc — truy vấn đơn giản chuyển đến mô hình nhẹ, tiết kiệm chi phí API.

_*Các phiên bản gần đây có thể sử dụng 10–20MB do merge tính năng nhanh chóng. Tối ưu tài nguyên đang được lên kế hoạch. So sánh thời gian khởi động dựa trên benchmark đơn nhân 0.8GHz (xem bảng bên dưới)._

|                               | OpenClaw      | NanoBot                  | **PicoClaw**                              |
| ----------------------------- | ------------- | ------------------------ | ----------------------------------------- |
| **Ngôn ngữ**                  | TypeScript    | Python                   | **Go**                                    |
| **RAM**                       | >1GB          | >100MB                   | **< 10MB***                               |
| **Thời gian khởi động**</br>(CPU 0.8GHz) | >500s         | >30s                     | **<1s**                                   |
| **Chi phí**                   | Mac Mini $599 | Hầu hết SBC Linux ~$50  | **Mọi bo mạch Linux**</br>**Chỉ từ $10** |

<img src="assets/compare.jpg" alt="PicoClaw" width="512">

## 🦾 Demo

### 🛠️ Quy trình trợ lý tiêu chuẩn

<table align="center">
  <tr align="center">
    <th><p align="center">🧩 Lập trình Full-Stack</p></th>
    <th><p align="center">🗂️ Quản lý Nhật ký & Kế hoạch</p></th>
    <th><p align="center">🔎 Tìm kiếm Web & Học hỏi</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">Phát triển • Triển khai • Mở rộng</td>
    <td align="center">Lên lịch • Tự động hóa • Ghi nhớ</td>
    <td align="center">Khám phá • Phân tích • Xu hướng</td>
  </tr>
</table>

### 📱 Chạy trên điện thoại Android cũ

Hãy cho chiếc điện thoại cũ một cuộc sống mới! Biến nó thành trợ lý AI thông minh với PicoClaw. Bắt đầu nhanh:

1. **Cài đặt [Termux](https://github.com/termux/termux-app)** (Tải từ [GitHub Releases](https://github.com/termux/termux-app/releases), hoặc tìm trên F-Droid / Google Play).
2. **Chạy các lệnh**

```bash
# Tải phiên bản mới nhất từ https://github.com/sipeed/picoclaw/releases
wget https://github.com/sipeed/picoclaw/releases/latest/download/picoclaw_Linux_arm64.tar.gz
tar xzf picoclaw_Linux_arm64.tar.gz
pkg install proot
termux-chroot ./picoclaw onboard
```

Sau đó làm theo hướng dẫn trong phần "Bắt đầu nhanh" để hoàn tất cấu hình!

<img src="assets/termux.jpg" alt="PicoClaw" width="512">

### 🐜 Triển khai sáng tạo trên phần cứng tối thiểu

PicoClaw có thể triển khai trên hầu hết mọi thiết bị Linux!

- $9.9 [LicheeRV-Nano](https://www.aliexpress.com/item/1005006519668532.html) phiên bản E(Ethernet) hoặc W(WiFi6), dùng làm Trợ lý Gia đình tối giản
- $30~50 [NanoKVM](https://www.aliexpress.com/item/1005007369816019.html), hoặc $100 [NanoKVM-Pro](https://www.aliexpress.com/item/1005010048471263.html) dùng cho quản trị Server tự động
- $50 [MaixCAM](https://www.aliexpress.com/item/1005008053333693.html) hoặc $100 [MaixCAM2](https://www.kickstarter.com/projects/zepan/maixcam2-build-your-next-gen-4k-ai-camera) dùng cho Giám sát thông minh

<https://private-user-images.githubusercontent.com/83055338/547056448-e7b031ff-d6f5-4468-bcca-5726b6fecb5c.mp4>

🌟 Nhiều hình thức triển khai hơn đang chờ bạn khám phá!

## 📦 Cài đặt

### Cài đặt bằng binary biên dịch sẵn

Tải file binary cho nền tảng của bạn từ [trang Releases](https://github.com/sipeed/picoclaw/releases).

### Cài đặt từ mã nguồn (có tính năng mới nhất, khuyên dùng cho phát triển)

```bash
git clone https://github.com/sipeed/picoclaw.git

cd picoclaw
make deps

# Build (không cần cài đặt)
make build

# Build cho nhiều nền tảng
make build-all

# Build cho Raspberry Pi Zero 2 W (32-bit: make build-linux-arm; 64-bit: make build-linux-arm64)
make build-pi-zero

# Build và cài đặt
make install
```

**Raspberry Pi Zero 2 W:** Sử dụng binary phù hợp với hệ điều hành: Raspberry Pi OS 32-bit → `make build-linux-arm`; 64-bit → `make build-linux-arm64`. Hoặc chạy `make build-pi-zero` để build cả hai.

## 📚 Tài liệu

Để xem hướng dẫn chi tiết, tham khảo tài liệu bên dưới. README này chỉ bao gồm phần bắt đầu nhanh.

| Chủ đề | Mô tả |
|--------|-------|
| 🐳 [Docker & Bắt đầu nhanh](docs/vi/docker.md) | Thiết lập Docker Compose, chế độ Launcher/Agent, cấu hình Bắt đầu nhanh |
| 💬 [Ứng dụng Chat](docs/vi/chat-apps.md) | Telegram, Discord, WhatsApp, Matrix, QQ, Slack, IRC, DingTalk, LINE, Feishu, WeCom và nhiều hơn |
| ⚙️ [Cấu hình](docs/vi/configuration.md) | Biến môi trường, cấu trúc workspace, nguồn skill, sandbox bảo mật, heartbeat |
| 🔌 [Nhà cung cấp & Mô hình](docs/vi/providers.md) | 20+ nhà cung cấp LLM, định tuyến mô hình, cấu hình model_list, kiến trúc nhà cung cấp |
| 🔄 [Spawn & Tác vụ bất đồng bộ](docs/vi/spawn-tasks.md) | Tác vụ nhanh, tác vụ dài với spawn, điều phối sub-agent bất đồng bộ |
| 🐛 [Xử lý sự cố](docs/vi/troubleshooting.md) | Các vấn đề thường gặp và giải pháp |
| 🔧 [Cấu hình Công cụ](docs/vi/tools_configuration.md) | Bật/tắt từng công cụ, chính sách thực thi |

## <img src="assets/clawdchat-icon.png" width="24" height="24" alt="ClawdChat"> Tham gia Mạng xã hội Agent

Kết nối PicoClaw với Mạng xã hội Agent chỉ bằng cách gửi một tin nhắn qua CLI hoặc bất kỳ ứng dụng Chat nào đã tích hợp.

**Đọc `https://clawdchat.ai/skill.md` và làm theo hướng dẫn để tham gia [ClawdChat.ai](https://clawdchat.ai)**

## 🖥️ Tham chiếu CLI

| Lệnh                      | Mô tả                         |
| -------------------------- | ------------------------------ |
| `picoclaw onboard`         | Khởi tạo cấu hình & workspace |
| `picoclaw agent -m "..."`  | Trò chuyện với agent           |
| `picoclaw agent`           | Chế độ chat tương tác          |
| `picoclaw gateway`         | Khởi động gateway              |
| `picoclaw status`          | Hiển thị trạng thái            |
| `picoclaw version`         | Hiển thị thông tin phiên bản   |
| `picoclaw cron list`       | Liệt kê tất cả tác vụ định kỳ |
| `picoclaw cron add ...`    | Thêm tác vụ định kỳ           |
| `picoclaw cron disable`    | Tắt tác vụ định kỳ            |
| `picoclaw cron remove`     | Xóa tác vụ định kỳ            |
| `picoclaw skills list`     | Liệt kê các skill đã cài      |
| `picoclaw skills install`  | Cài đặt một skill              |
| `picoclaw migrate`         | Di chuyển dữ liệu từ phiên bản cũ |
| `picoclaw auth login`      | Xác thực với nhà cung cấp     |

### Tác vụ định kỳ / Nhắc nhở

PicoClaw hỗ trợ nhắc nhở theo lịch và tác vụ lặp lại thông qua công cụ `cron`:

* **Nhắc nhở một lần**: "Nhắc tôi sau 10 phút" → kích hoạt một lần sau 10 phút
* **Tác vụ lặp lại**: "Nhắc tôi mỗi 2 giờ" → kích hoạt mỗi 2 giờ
* **Biểu thức Cron**: "Nhắc tôi lúc 9 giờ sáng mỗi ngày" → sử dụng biểu thức cron

## 🤝 Đóng góp & Lộ trình

Chào đón mọi PR! Mã nguồn được thiết kế nhỏ gọn và dễ đọc. 🤗

Xem [Lộ trình Cộng đồng](https://github.com/sipeed/picoclaw/blob/main/ROADMAP.md) đầy đủ.

Nhóm phát triển đang được xây dựng. Tham gia sau khi có PR đầu tiên được merge!

Nhóm người dùng:

discord: <https://discord.gg/V4sAZ9XWpN>

<img src="assets/wechat.png" alt="PicoClaw" width="512">


================================================
FILE: README.zh.md
================================================
<div align="center">
<img src="assets/logo.webp" alt="PicoClaw" width="512">

<h1>PicoClaw: 基于Go语言的超高效 AI 助手</h1>

<h3>$10 硬件 · <10MB 内存 · <1s 启动 · 皮皮虾,我们走!</h3>
  <p>
    <img src="https://img.shields.io/badge/Go-1.25+-00ADD8?style=flat&logo=go&logoColor=white" alt="Go">
    <img src="https://img.shields.io/badge/Arch-x86__64%2C%20ARM64%2C%20MIPS%2C%20RISC--V%2C%20LoongArch-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://docs.picoclaw.io/"><img src="https://img.shields.io/badge/Docs-Official-007acc?style=flat&logo=read-the-docs&logoColor=white" alt="Docs"></a>
    <a href="https://deepwiki.com/sipeed/picoclaw"><img src="https://img.shields.io/badge/Wiki-DeepWiki-FFA500?style=flat&logo=wikipedia&logoColor=white" alt="Wiki"></a>
    <br>
    <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>
    <a href="./assets/wechat.png"><img src="https://img.shields.io/badge/WeChat-Group-41d56b?style=flat&logo=wechat&logoColor=white"></a>
    <a href="https://discord.gg/V4sAZ9XWpN"><img src="https://img.shields.io/badge/Discord-Community-4c60eb?style=flat&logo=discord&logoColor=white" alt="Discord"></a>
  </p>

**中文** | [日本語](README.ja.md) | [Português](README.pt-br.md) | [Tiếng Việt](README.vi.md) | [Français](README.fr.md) | [Italiano](README.it.md) | [Bahasa Indonesia](README.id.md) | [English](README.md)

</div>

---

> **PicoClaw** 是由 [矽速科技 (Sipeed)](https://sipeed.com) 发起的独立开源项目,完全使用 **Go 语言**从零编写——不是 OpenClaw、NanoBot 或其他项目的分支。

🦐 **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]
> **🚨 安全声明**
>
> - **无加密货币 (NO CRYPTO):** PicoClaw **没有** 发行任何官方代币、Token 或虚拟货币。所有在 `pump.fun` 或其他交易平台上的相关声称均为 **诈骗**。
> - **官方域名:** 唯一的官方网站是 **[picoclaw.io](https://picoclaw.io)**,公司官网是 **[sipeed.com](https://sipeed.com)**。
> - **警惕:** 许多 `.ai/.org/.com/.net/...` 后缀的域名被第三方抢注,请勿轻信。
> - **注意:** PicoClaw 正在初期的快速功能开发阶段,可能有尚未修复的网络安全问题,在 1.0 正式版发布前,请不要将其部署到生产环境中。
> - **注意:** PicoClaw 最近合并了大量 PR,近期版本可能内存占用较大 (10~20MB),我们将在功能较为收敛后进行资源占用优化。

## 📢 新闻

2026-03-17 🚀 **v0.2.3 发布!** 系统托盘 UI(Windows & Linux)、子 Agent 状态查询 (`spawn_status`)、实验性 Gateway 热重载、Cron 安全门控,以及 2 项安全修复。PicoClaw 已达 **25K ⭐**!

2026-03-09 🎉 **v0.2.1 — 史上最大更新!** MCP 协议支持、4 个新频道 (Matrix/IRC/WeCom/Discord Proxy)、3 个新 Provider (Kimi/Minimax/Avian)、视觉管线、JSONL 记忆存储、模型路由。

2026-02-28 📦 **v0.2.0** 发布,支持 Docker Compose 和 Web UI 启动器。

2026-02-26 🎉 PicoClaw 仅 17 天突破 **20K Stars**!频道自动编排和能力接口上线。

<details>
<summary>更早的新闻...</summary>

2026-02-16 🎉 PicoClaw 一周内突破 12K Stars!社区维护者角色和 [路线图](ROADMAP.md) 正式发布。

2026-02-13 🎉 PicoClaw 4 天内突破 5000 Stars!项目路线图和开发者群组筹建中。

2026-02-09 🎉 **PicoClaw 正式发布!** 仅用 1 天构建,将 AI Agent 带入 $10 硬件与 <10MB 内存的世界。🦐 皮皮虾,我们走!

</details>

## ✨ 特性

🪶 **超轻量级**: 核心功能内存占用 <10MB — 比 OpenClaw 小 99%。*

💰 **极低成本**: 高效到足以在 $10 的硬件上运行 — 比 Mac mini 便宜 98%。

⚡️ **闪电启动**: 启动速度快 400 倍,即使在 0.6GHz 单核处理器上也能在 1 秒内启动。

🌍 **真正可移植**: 跨 RISC-V、ARM、MIPS 和 x86 架构的单二进制文件,一键运行!

🤖 **AI 自举**: 纯 Go 语言原生实现 — 95% 的核心代码由 Agent 生成,并经由"人机回环"微调。

🔌 **MCP 支持**: 原生 [Model Context Protocol](https://modelcontextprotocol.io/) 集成 — 连接任意 MCP 服务器扩展 Agent 能力。

👁️ **视觉管线**: 直接向 Agent 发送图片和文件 — 自动 base64 编码对接多模态 LLM。

🧠 **智能路由**: 基于规则的模型路由 — 简单查询走轻量模型,节省 API 成本。

_*近期版本因快速合并 PR 可能占用 10–20MB,资源优化已列入计划。启动速度对比基于 0.8GHz 单核实测(见下方对比表)。_

|                                | 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 可以将你 10 年前的老旧手机废物利用,变身成为你的 AI 助理!快速指南:

1. 安装 [Termux](https://github.com/termux/termux-app)(可从 [GitHub Releases](https://github.com/termux/termux-app/releases) 下载,或在 F-Droid 等应用商店搜索)
2. 打开后执行指令

```bash
# 从 Release 页面下载最新版本
wget https://github.com/sipeed/picoclaw/releases/latest/download/picoclaw_Linux_arm64.tar.gz
tar xzf picoclaw_Linux_arm64.tar.gz
pkg install proot
termux-chroot ./picoclaw onboard
```

然后跟随下面的"快速开始"章节继续配置 PicoClaw 即可使用!

<img src="assets/termux.jpg" alt="PicoClaw" width="512">

### 🐜 创新的低占用部署

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>

🌟 更多部署案例敬请期待!

## 📦 安装

### 使用预编译二进制文件安装

从 [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

# 为 Raspberry Pi Zero 2 W 构建(32位: make build-linux-arm; 64位: make build-linux-arm64)
make build-pi-zero

# 构建并安装
make install
```

**Raspberry Pi Zero 2 W:** 请使用与系统匹配的二进制文件:32 位 Raspberry Pi OS → `make build-linux-arm`;64 位 → `make build-linux-arm64`。或运行 `make build-pi-zero` 同时构建两者。

## 📚 文档

详细指南请参阅以下文档,README 仅涵盖快速入门。

| 主题 | 说明 |
|------|------|
| 🐳 [Docker 与快速开始](docs/zh/docker.md) | Docker Compose 配置、Launcher/Agent 模式、快速开始 |
| 💬 [聊天应用配置](docs/zh/chat-apps.md) | Telegram、Discord、WhatsApp、Matrix、QQ、Slack、IRC、钉钉、LINE、飞书、企业微信等 |
| ⚙️ [配置指南](docs/zh/configuration.md) | 环境变量、工作区布局、技能来源、安全沙箱、心跳任务 |
| 🔌 [提供商与模型配置](docs/zh/providers.md) | 20+ LLM 提供商、模型路由、model_list 配置、Provider 架构 |
| 🔄 [异步任务与 Spawn](docs/zh/spawn-tasks.md) | 快速任务、长任务与 Spawn、异步子 Agent 编排 |
| 🐛 [疑难解答](docs/zh/troubleshooting.md) | 常见问题与解决方案 |
| 🔧 [工具配置](docs/zh/tools_configuration.md) | 工具启用/禁用、执行策略 |

## <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)**

## 🖥️ CLI 命令行参考

| 命令                       | 说明                   |
| ------------------------- | ---------------------- |
| `picoclaw onboard`        | 初始化配置与工作区       |
| `picoclaw agent -m "..."` | 与 Agent 对话           |
| `picoclaw agent`          | 交互式对话模式           |
| `picoclaw gateway`        | 启动网关                |
| `picoclaw status`         | 查看状态                |
| `picoclaw version`        | 查看版本信息             |
| `picoclaw cron list`      | 列出所有定时任务         |
| `picoclaw cron add ...`   | 添加定时任务             |
| `picoclaw cron disable`   | 禁用定时任务             |
| `picoclaw cron remove`    | 删除定时任务             |
| `picoclaw skills list`    | 列出已安装技能           |
| `picoclaw skills install` | 安装技能                |
| `picoclaw migrate`        | 从旧版本迁移数据         |
| `picoclaw auth login`     | 认证提供商               |

### 定时任务 / 提醒

PicoClaw 通过 `cron` 工具支持定时提醒和重复任务:

* **一次性提醒**: "10分钟后提醒我" → 10分钟后触发一次
* **重复任务**: "每2小时提醒我" → 每2小时触发
* **Cron 表达式**: "每天上午9点提醒我" → 使用 cron 表达式

## 🤝 贡献与路线图

欢迎提交 PR!代码库刻意保持小巧和可读。🤗

查看完整的 [社区路线图](https://github.com/sipeed/picoclaw/blob/main/ROADMAP.md)。

开发者群组正在组建中,入群门槛:至少合并过 1 个 PR。

用户群组:

Discord: <https://discord.gg/V4sAZ9XWpN>

<img src="assets/wechat.png" alt="PicoClaw" width="512">


================================================
FILE: ROADMAP.md
================================================

# 🦐 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!

================================================
FILE: cmd/picoclaw/internal/agent/command.go
================================================
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
}


================================================
FILE: cmd/picoclaw/internal/agent/command_test.go
================================================
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"))
}


================================================
FILE: cmd/picoclaw/internal/agent/helpers.go
================================================
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"
	}

	if debug {
		logger.SetLevel(logger.DEBUG)
		fmt.Println("🔍 Debug mode enabled")
	}

	cfg, err := internal.LoadConfig()
	if err != nil {
		return fmt.Errorf("error loading config: %w", err)
	}

	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()
	logger.InfoCF("agent", "Agent initialized",
		map[string]any{
			"tools_count":      startupInfo["tools"].(map[string]any)["count"],
			"skills_total":     startupInfo["skills"].(map[string]any)["total"],
			"skills_available": startupInfo["skills"].(map[string]any)["available"],
		})

	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)
	}
}


================================================
FILE: cmd/picoclaw/internal/auth/command.go
================================================
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(),
	)

	return cmd
}


================================================
FILE: cmd/picoclaw/internal/auth/command_test.go
================================================
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",
	}

	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)
	}
}


================================================
FILE: cmd/picoclaw/internal/auth/helpers.go
================================================
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"
	defaultAnthropicModel = "claude-sonnet-4.6"
)

func authLoginCmd(provider string, useDeviceCode bool, useOauth bool) error {
	switch provider {
	case "openai":
		return authLoginOpenAI(useDeviceCode)
	case "anthropic":
		return authLoginAnthropic(useOauth)
	case "google-antigravity", "antigravity":
		return authLoginGoogleAntigravity()
	default:
		return fmt.Errorf("unsupported provider: %s (%s)", provider, supportedProvidersMsg)
	}
}

func authLoginOpenAI(useDeviceCode bool) error {
	cfg := auth.OpenAIOAuthConfig()

	var cred *auth.AuthCredential
	var err error

	if useDeviceCode {
		cred, err = auth.LoginDeviceCode(cfg)
	} else {
		cred, err = auth.LoginBrowser(cfg)
	}

	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 Providers (legacy format)
		appCfg.Providers.OpenAI.AuthMethod = "oauth"

		// Update or add openai in ModelList
		foundOpenAI := false
		for i := range appCfg.ModelList {
			if isOpenAIModel(appCfg.ModelList[i].Model) {
				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() error {
	cfg := auth.GoogleAntigravityOAuthConfig()

	cred, err := auth.LoginBrowser(cfg)
	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 Providers (legacy format, for backward compatibility)
		appCfg.Providers.Antigravity.AuthMethod = "oauth"

		// Update or add antigravity in ModelList
		foundAntigravity := false
		for i := range appCfg.ModelList {
			if isAntigravityModel(appCfg.ModelList[i].Model) {
				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 {
		appCfg.Providers.Anthropic.AuthMethod = "oauth"

		found := false
		for i := range appCfg.ModelList {
			if isAnthropicModel(appCfg.ModelList[i].Model) {
				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":
			appCfg.Providers.Anthropic.AuthMethod = "token"
			// Update ModelList
			found := false
			for i := range appCfg.ModelList {
				if isAnthropicModel(appCfg.ModelList[i].Model) {
					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":
			appCfg.Providers.OpenAI.AuthMethod = "token"
			// Update ModelList
			found := false
			for i := range appCfg.ModelList {
				if isOpenAIModel(appCfg.ModelList[i].Model) {
					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].Model) {
						appCfg.ModelList[i].AuthMethod = ""
					}
				case "anthropic":
					if isAnthropicModel(appCfg.ModelList[i].Model) {
						appCfg.ModelList[i].AuthMethod = ""
					}
				case "google-antigravity", "antigravity":
					if isAntigravityModel(appCfg.ModelList[i].Model) {
						appCfg.ModelList[i].AuthMethod = ""
					}
				}
			}
			// Clear AuthMethod in Providers (legacy)
			switch provider {
			case "openai":
				appCfg.Providers.OpenAI.AuthMethod = ""
			case "anthropic":
				appCfg.Providers.Anthropic.AuthMethod = ""
			case "google-antigravity", "antigravity":
				appCfg.Providers.Antigravity.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 = ""
		}
		// Clear all AuthMethods in Providers (legacy)
		appCfg.Providers.OpenAI.AuthMethod = ""
		appCfg.Providers.Anthropic.AuthMethod = ""
		appCfg.Providers.Antigravity.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.P
Download .txt
gitextract_tfefpv8c/

├── .dockerignore
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   ├── feature_request.md
│   │   └── general-task---todo.md
│   ├── dependabot.yml
│   ├── pull_request_template.md
│   └── workflows/
│       ├── build.yml
│       ├── docker-build.yml
│       ├── nightly.yml
│       ├── pr.yml
│       ├── release.yml
│       └── upload-tos.yml
├── .gitignore
├── .golangci.yaml
├── .goreleaser.yaml
├── CONTRIBUTING.md
├── CONTRIBUTING.zh.md
├── LICENSE
├── Makefile
├── README.fr.md
├── README.id.md
├── README.it.md
├── README.ja.md
├── README.md
├── README.pt-br.md
├── README.vi.md
├── README.zh.md
├── ROADMAP.md
├── cmd/
│   ├── picoclaw/
│   │   ├── internal/
│   │   │   ├── agent/
│   │   │   │   ├── command.go
│   │   │   │   ├── command_test.go
│   │   │   │   └── helpers.go
│   │   │   ├── auth/
│   │   │   │   ├── command.go
│   │   │   │   ├── command_test.go
│   │   │   │   ├── helpers.go
│   │   │   │   ├── login.go
│   │   │   │   ├── login_test.go
│   │   │   │   ├── logout.go
│   │   │   │   ├── logout_test.go
│   │   │   │   ├── models.go
│   │   │   │   ├── models_test.go
│   │   │   │   ├── status.go
│   │   │   │   └── status_test.go
│   │   │   ├── cron/
│   │   │   │   ├── add.go
│   │   │   │   ├── add_test.go
│   │   │   │   ├── command.go
│   │   │   │   ├── command_test.go
│   │   │   │   ├── disable.go
│   │   │   │   ├── disable_test.go
│   │   │   │   ├── enable.go
│   │   │   │   ├── enable_test.go
│   │   │   │   ├── helpers.go
│   │   │   │   ├── list.go
│   │   │   │   ├── list_test.go
│   │   │   │   ├── remove.go
│   │   │   │   └── remove_test.go
│   │   │   ├── gateway/
│   │   │   │   ├── command.go
│   │   │   │   └── command_test.go
│   │   │   ├── helpers.go
│   │   │   ├── helpers_test.go
│   │   │   ├── migrate/
│   │   │   │   ├── command.go
│   │   │   │   └── command_test.go
│   │   │   ├── model/
│   │   │   │   ├── command.go
│   │   │   │   └── command_test.go
│   │   │   ├── onboard/
│   │   │   │   ├── command.go
│   │   │   │   ├── command_test.go
│   │   │   │   ├── helpers.go
│   │   │   │   └── helpers_test.go
│   │   │   ├── skills/
│   │   │   │   ├── command.go
│   │   │   │   ├── command_test.go
│   │   │   │   ├── helpers.go
│   │   │   │   ├── install.go
│   │   │   │   ├── install_test.go
│   │   │   │   ├── installbuiltin.go
│   │   │   │   ├── installbuiltin_test.go
│   │   │   │   ├── list.go
│   │   │   │   ├── list_test.go
│   │   │   │   ├── listbuiltin.go
│   │   │   │   ├── listbuiltin_test.go
│   │   │   │   ├── remove.go
│   │   │   │   ├── remove_test.go
│   │   │   │   ├── search.go
│   │   │   │   ├── search_test.go
│   │   │   │   ├── show.go
│   │   │   │   └── show_test.go
│   │   │   ├── status/
│   │   │   │   ├── command.go
│   │   │   │   ├── command_test.go
│   │   │   │   └── helpers.go
│   │   │   └── version/
│   │   │       ├── command.go
│   │   │       └── command_test.go
│   │   ├── main.go
│   │   └── main_test.go
│   └── picoclaw-launcher-tui/
│       ├── internal/
│       │   ├── config/
│       │   │   └── store.go
│       │   └── ui/
│       │       ├── app.go
│       │       ├── channel.go
│       │       ├── gateway_posix.go
│       │       ├── gateway_windows.go
│       │       ├── menu.go
│       │       ├── model.go
│       │       └── style.go
│       └── main.go
├── config/
│   └── config.example.json
├── docker/
│   ├── Dockerfile
│   ├── Dockerfile.full
│   ├── Dockerfile.goreleaser
│   ├── Dockerfile.goreleaser.launcher
│   ├── docker-compose.full.yml
│   ├── docker-compose.yml
│   └── entrypoint.sh
├── docs/
│   ├── ANTIGRAVITY_AUTH.md
│   ├── ANTIGRAVITY_USAGE.md
│   ├── agent-refactor/
│   │   └── README.md
│   ├── channels/
│   │   ├── dingtalk/
│   │   │   └── README.zh.md
│   │   ├── discord/
│   │   │   └── README.zh.md
│   │   ├── feishu/
│   │   │   └── README.zh.md
│   │   ├── line/
│   │   │   └── README.zh.md
│   │   ├── maixcam/
│   │   │   └── README.zh.md
│   │   ├── matrix/
│   │   │   ├── README.md
│   │   │   └── README.zh.md
│   │   ├── onebot/
│   │   │   └── README.zh.md
│   │   ├── qq/
│   │   │   └── README.zh.md
│   │   ├── slack/
│   │   │   └── README.zh.md
│   │   ├── telegram/
│   │   │   └── README.zh.md
│   │   └── wecom/
│   │       ├── wecom_aibot/
│   │       │   └── README.zh.md
│   │       ├── wecom_app/
│   │       │   └── README.zh.md
│   │       └── wecom_bot/
│   │           └── README.zh.md
│   ├── chat-apps.md
│   ├── configuration.md
│   ├── credential_encryption.md
│   ├── debug.md
│   ├── design/
│   │   ├── issue-783-investigation-and-fix-plan.zh.md
│   │   ├── provider-refactoring-tests.md
│   │   └── provider-refactoring.md
│   ├── docker.md
│   ├── fr/
│   │   ├── chat-apps.md
│   │   ├── configuration.md
│   │   ├── docker.md
│   │   ├── providers.md
│   │   ├── spawn-tasks.md
│   │   ├── tools_configuration.md
│   │   └── troubleshooting.md
│   ├── it/
│   │   └── configuration.md
│   ├── ja/
│   │   ├── chat-apps.md
│   │   ├── configuration.md
│   │   ├── docker.md
│   │   ├── providers.md
│   │   ├── spawn-tasks.md
│   │   ├── tools_configuration.md
│   │   └── troubleshooting.md
│   ├── migration/
│   │   └── model-list-migration.md
│   ├── providers.md
│   ├── pt-br/
│   │   ├── chat-apps.md
│   │   ├── configuration.md
│   │   ├── docker.md
│   │   ├── providers.md
│   │   ├── spawn-tasks.md
│   │   ├── tools_configuration.md
│   │   └── troubleshooting.md
│   ├── spawn-tasks.md
│   ├── tools_configuration.md
│   ├── troubleshooting.md
│   ├── vi/
│   │   ├── chat-apps.md
│   │   ├── configuration.md
│   │   ├── docker.md
│   │   ├── providers.md
│   │   ├── spawn-tasks.md
│   │   ├── tools_configuration.md
│   │   └── troubleshooting.md
│   └── zh/
│       ├── chat-apps.md
│       ├── configuration.md
│       ├── docker.md
│       ├── providers.md
│       ├── spawn-tasks.md
│       ├── tools_configuration.md
│       └── troubleshooting.md
├── go.mod
├── go.sum
├── pkg/
│   ├── agent/
│   │   ├── context.go
│   │   ├── context_cache_test.go
│   │   ├── context_test.go
│   │   ├── instance.go
│   │   ├── instance_test.go
│   │   ├── loop.go
│   │   ├── loop_mcp.go
│   │   ├── loop_mcp_test.go
│   │   ├── loop_media.go
│   │   ├── loop_test.go
│   │   ├── memory.go
│   │   ├── mock_provider_test.go
│   │   ├── model_resolution.go
│   │   ├── registry.go
│   │   ├── registry_test.go
│   │   ├── thinking.go
│   │   └── thinking_test.go
│   ├── auth/
│   │   ├── anthropic_usage.go
│   │   ├── anthropic_usage_test.go
│   │   ├── oauth.go
│   │   ├── oauth_test.go
│   │   ├── pkce.go
│   │   ├── pkce_test.go
│   │   ├── store.go
│   │   ├── store_test.go
│   │   ├── token.go
│   │   └── token_test.go
│   ├── bus/
│   │   ├── bus.go
│   │   ├── bus_test.go
│   │   └── types.go
│   ├── channels/
│   │   ├── README.md
│   │   ├── README.zh.md
│   │   ├── base.go
│   │   ├── base_test.go
│   │   ├── dingtalk/
│   │   │   ├── dingtalk.go
│   │   │   └── init.go
│   │   ├── discord/
│   │   │   ├── discord.go
│   │   │   ├── discord_resolve_test.go
│   │   │   ├── discord_test.go
│   │   │   └── init.go
│   │   ├── errors.go
│   │   ├── errors_test.go
│   │   ├── errutil.go
│   │   ├── errutil_test.go
│   │   ├── feishu/
│   │   │   ├── common.go
│   │   │   ├── common_test.go
│   │   │   ├── feishu_32.go
│   │   │   ├── feishu_64.go
│   │   │   ├── feishu_64_test.go
│   │   │   ├── init.go
│   │   │   └── token_cache.go
│   │   ├── interfaces.go
│   │   ├── interfaces_command_test.go
│   │   ├── irc/
│   │   │   ├── handler.go
│   │   │   ├── init.go
│   │   │   ├── irc.go
│   │   │   └── irc_test.go
│   │   ├── line/
│   │   │   ├── init.go
│   │   │   ├── line.go
│   │   │   └── line_test.go
│   │   ├── maixcam/
│   │   │   ├── init.go
│   │   │   └── maixcam.go
│   │   ├── manager.go
│   │   ├── manager_channel.go
│   │   ├── manager_channel_test.go
│   │   ├── manager_test.go
│   │   ├── matrix/
│   │   │   ├── init.go
│   │   │   ├── matrix.go
│   │   │   └── matrix_test.go
│   │   ├── media.go
│   │   ├── onebot/
│   │   │   ├── init.go
│   │   │   └── onebot.go
│   │   ├── pico/
│   │   │   ├── init.go
│   │   │   ├── pico.go
│   │   │   └── protocol.go
│   │   ├── qq/
│   │   │   ├── botgo_logger.go
│   │   │   ├── init.go
│   │   │   ├── qq.go
│   │   │   └── qq_test.go
│   │   ├── registry.go
│   │   ├── slack/
│   │   │   ├── init.go
│   │   │   ├── slack.go
│   │   │   └── slack_test.go
│   │   ├── split.go
│   │   ├── split_test.go
│   │   ├── telegram/
│   │   │   ├── command_registration.go
│   │   │   ├── command_registration_test.go
│   │   │   ├── init.go
│   │   │   ├── parse_markdown_to_md_v2.go
│   │   │   ├── parse_markdown_to_md_v2_test.go
│   │   │   ├── parser_markdown_to_html.go
│   │   │   ├── telegram.go
│   │   │   ├── telegram_dispatch_test.go
│   │   │   ├── telegram_group_command_filter_test.go
│   │   │   ├── telegram_test.go
│   │   │   └── testdata/
│   │   │       └── md2_all_formats.txt
│   │   ├── webhook.go
│   │   ├── wecom/
│   │   │   ├── aibot.go
│   │   │   ├── aibot_test.go
│   │   │   ├── aibot_ws.go
│   │   │   ├── aibot_ws_test.go
│   │   │   ├── app.go
│   │   │   ├── app_test.go
│   │   │   ├── bot.go
│   │   │   ├── bot_test.go
│   │   │   ├── common.go
│   │   │   ├── dedupe.go
│   │   │   ├── dedupe_test.go
│   │   │   └── init.go
│   │   ├── whatsapp/
│   │   │   ├── init.go
│   │   │   ├── whatsapp.go
│   │   │   └── whatsapp_command_test.go
│   │   └── whatsapp_native/
│   │       ├── init.go
│   │       ├── whatsapp_command_test.go
│   │       ├── whatsapp_native.go
│   │       └── whatsapp_native_stub.go
│   ├── commands/
│   │   ├── builtin.go
│   │   ├── builtin_test.go
│   │   ├── cmd_check.go
│   │   ├── cmd_clear.go
│   │   ├── cmd_help.go
│   │   ├── cmd_list.go
│   │   ├── cmd_reload.go
│   │   ├── cmd_show.go
│   │   ├── cmd_start.go
│   │   ├── cmd_switch.go
│   │   ├── cmd_switch_test.go
│   │   ├── definition.go
│   │   ├── definition_test.go
│   │   ├── executor.go
│   │   ├── executor_test.go
│   │   ├── handler_agents.go
│   │   ├── registry.go
│   │   ├── registry_test.go
│   │   ├── request.go
│   │   ├── request_test.go
│   │   ├── runtime.go
│   │   └── show_list_handlers_test.go
│   ├── config/
│   │   ├── config.go
│   │   ├── config_test.go
│   │   ├── defaults.go
│   │   ├── envkeys.go
│   │   ├── migration.go
│   │   ├── migration_test.go
│   │   ├── model_config_test.go
│   │   ├── multikey_test.go
│   │   ├── version.go
│   │   └── version_test.go
│   ├── constants/
│   │   └── channels.go
│   ├── credential/
│   │   ├── credential.go
│   │   ├── credential_test.go
│   │   ├── keygen.go
│   │   ├── keygen_test.go
│   │   ├── store.go
│   │   └── store_test.go
│   ├── cron/
│   │   ├── service.go
│   │   └── service_test.go
│   ├── devices/
│   │   ├── events/
│   │   │   └── events.go
│   │   ├── service.go
│   │   ├── source.go
│   │   └── sources/
│   │       ├── usb_linux.go
│   │       └── usb_stub.go
│   ├── fileutil/
│   │   └── file.go
│   ├── gateway/
│   │   └── gateway.go
│   ├── health/
│   │   └── server.go
│   ├── heartbeat/
│   │   ├── service.go
│   │   └── service_test.go
│   ├── identity/
│   │   ├── identity.go
│   │   └── identity_test.go
│   ├── logger/
│   │   ├── logger.go
│   │   ├── logger_3rd_party.go
│   │   └── logger_test.go
│   ├── mcp/
│   │   ├── manager.go
│   │   └── manager_test.go
│   ├── media/
│   │   ├── store.go
│   │   ├── store_test.go
│   │   └── tempdir.go
│   ├── memory/
│   │   ├── jsonl.go
│   │   ├── jsonl_test.go
│   │   ├── migration.go
│   │   ├── migration_test.go
│   │   └── store.go
│   ├── migrate/
│   │   ├── internal/
│   │   │   ├── common.go
│   │   │   ├── common_test.go
│   │   │   └── types.go
│   │   ├── migrate.go
│   │   ├── migrate_test.go
│   │   └── sources/
│   │       └── openclaw/
│   │           ├── common.go
│   │           ├── openclaw_config.go
│   │           ├── openclaw_config_test.go
│   │           ├── openclaw_handler.go
│   │           └── openclaw_handler_test.go
│   ├── providers/
│   │   ├── anthropic/
│   │   │   ├── provider.go
│   │   │   ├── provider_test.go
│   │   │   └── thinking_test.go
│   │   ├── anthropic_messages/
│   │   │   ├── provider.go
│   │   │   └── provider_test.go
│   │   ├── antigravity_provider.go
│   │   ├── antigravity_provider_test.go
│   │   ├── azure/
│   │   │   ├── provider.go
│   │   │   └── provider_test.go
│   │   ├── claude_cli_provider.go
│   │   ├── claude_cli_provider_integration_test.go
│   │   ├── claude_cli_provider_test.go
│   │   ├── claude_provider.go
│   │   ├── claude_provider_test.go
│   │   ├── codex_cli_credentials.go
│   │   ├── codex_cli_credentials_test.go
│   │   ├── codex_cli_provider.go
│   │   ├── codex_cli_provider_integration_test.go
│   │   ├── codex_cli_provider_test.go
│   │   ├── codex_provider.go
│   │   ├── codex_provider_test.go
│   │   ├── common/
│   │   │   ├── common.go
│   │   │   └── common_test.go
│   │   ├── cooldown.go
│   │   ├── cooldown_test.go
│   │   ├── error_classifier.go
│   │   ├── error_classifier_test.go
│   │   ├── factory.go
│   │   ├── factory_provider.go
│   │   ├── factory_provider_test.go
│   │   ├── factory_test.go
│   │   ├── fallback.go
│   │   ├── fallback_multikey_test.go
│   │   ├── fallback_test.go
│   │   ├── github_copilot_provider.go
│   │   ├── http_provider.go
│   │   ├── legacy_provider.go
│   │   ├── model_ref.go
│   │   ├── model_ref_test.go
│   │   ├── openai_compat/
│   │   │   ├── provider.go
│   │   │   └── provider_test.go
│   │   ├── protocoltypes/
│   │   │   └── types.go
│   │   ├── tool_call_extract.go
│   │   ├── toolcall_utils.go
│   │   └── types.go
│   ├── routing/
│   │   ├── agent_id.go
│   │   ├── agent_id_test.go
│   │   ├── classifier.go
│   │   ├── features.go
│   │   ├── route.go
│   │   ├── route_test.go
│   │   ├── router.go
│   │   ├── router_test.go
│   │   ├── session_key.go
│   │   └── session_key_test.go
│   ├── session/
│   │   ├── jsonl_backend.go
│   │   ├── jsonl_backend_test.go
│   │   ├── manager.go
│   │   ├── manager_test.go
│   │   └── session_store.go
│   ├── skills/
│   │   ├── clawhub_registry.go
│   │   ├── clawhub_registry_test.go
│   │   ├── installer.go
│   │   ├── installer_test.go
│   │   ├── loader.go
│   │   ├── loader_test.go
│   │   ├── registry.go
│   │   ├── registry_test.go
│   │   ├── search_cache.go
│   │   └── search_cache_test.go
│   ├── state/
│   │   ├── state.go
│   │   └── state_test.go
│   ├── tools/
│   │   ├── base.go
│   │   ├── cron.go
│   │   ├── cron_test.go
│   │   ├── edit.go
│   │   ├── edit_test.go
│   │   ├── filesystem.go
│   │   ├── filesystem_test.go
│   │   ├── i2c.go
│   │   ├── i2c_linux.go
│   │   ├── i2c_other.go
│   │   ├── mcp_tool.go
│   │   ├── mcp_tool_test.go
│   │   ├── message.go
│   │   ├── message_test.go
│   │   ├── registry.go
│   │   ├── registry_test.go
│   │   ├── result.go
│   │   ├── result_test.go
│   │   ├── search_tool.go
│   │   ├── search_tools_test.go
│   │   ├── send_file.go
│   │   ├── send_file_test.go
│   │   ├── shell.go
│   │   ├── shell_process_unix.go
│   │   ├── shell_process_windows.go
│   │   ├── shell_test.go
│   │   ├── shell_timeout_unix_test.go
│   │   ├── skills_install.go
│   │   ├── skills_install_test.go
│   │   ├── skills_search.go
│   │   ├── skills_search_test.go
│   │   ├── spawn.go
│   │   ├── spawn_status.go
│   │   ├── spawn_status_test.go
│   │   ├── spawn_test.go
│   │   ├── spi.go
│   │   ├── spi_linux.go
│   │   ├── spi_other.go
│   │   ├── subagent.go
│   │   ├── subagent_tool_test.go
│   │   ├── toolloop.go
│   │   ├── types.go
│   │   ├── web.go
│   │   └── web_test.go
│   ├── utils/
│   │   ├── bm25.go
│   │   ├── bm25_test.go
│   │   ├── download.go
│   │   ├── http_client.go
│   │   ├── http_client_test.go
│   │   ├── http_retry.go
│   │   ├── http_retry_test.go
│   │   ├── markdown.go
│   │   ├── markdown_test.go
│   │   ├── media.go
│   │   ├── skills.go
│   │   ├── string.go
│   │   ├── string_test.go
│   │   └── zip.go
│   └── voice/
│       ├── transcriber.go
│       └── transcriber_test.go
├── scripts/
│   ├── build-macos-app.sh
│   ├── icon.icns
│   ├── setup.iss
│   ├── test-docker-mcp.sh
│   └── test-irc.sh
├── web/
│   ├── Makefile
│   ├── README.md
│   ├── backend/
│   │   ├── .gitignore
│   │   ├── api/
│   │   │   ├── channels.go
│   │   │   ├── config.go
│   │   │   ├── config_test.go
│   │   │   ├── gateway.go
│   │   │   ├── gateway_host.go
│   │   │   ├── gateway_host_test.go
│   │   │   ├── gateway_test.go
│   │   │   ├── launcher_config.go
│   │   │   ├── launcher_config_test.go
│   │   │   ├── log.go
│   │   │   ├── model_status.go
│   │   │   ├── models.go
│   │   │   ├── models_test.go
│   │   │   ├── oauth.go
│   │   │   ├── oauth_test.go
│   │   │   ├── pico.go
│   │   │   ├── pico_test.go
│   │   │   ├── router.go
│   │   │   ├── session.go
│   │   │   ├── session_test.go
│   │   │   ├── skills.go
│   │   │   ├── skills_test.go
│   │   │   ├── startup.go
│   │   │   ├── startup_test.go
│   │   │   ├── tools.go
│   │   │   └── tools_test.go
│   │   ├── app_runtime.go
│   │   ├── dist/
│   │   │   └── .gitkeep
│   │   ├── embed.go
│   │   ├── embed_test.go
│   │   ├── i18n.go
│   │   ├── launcherconfig/
│   │   │   ├── config.go
│   │   │   └── config_test.go
│   │   ├── main.go
│   │   ├── middleware/
│   │   │   ├── access_control.go
│   │   │   ├── access_control_test.go
│   │   │   └── middleware.go
│   │   ├── model/
│   │   │   └── status.go
│   │   ├── systray.go
│   │   ├── systray_unix.go
│   │   ├── systray_windows.go
│   │   ├── tray_stub_nocgo.go
│   │   ├── utils/
│   │   │   ├── banner.go
│   │   │   ├── onboard.go
│   │   │   ├── onboard_test.go
│   │   │   └── runtime.go
│   │   └── winres/
│   │       └── winres.json
│   ├── frontend/
│   │   ├── .editorconfig
│   │   ├── .gitignore
│   │   ├── .prettierignore
│   │   ├── components.json
│   │   ├── eslint.config.js
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── prettier.config.js
│   │   ├── public/
│   │   │   └── site.webmanifest
│   │   ├── scripts/
│   │   │   └── ensure-backend-gitkeep.cjs
│   │   ├── src/
│   │   │   ├── api/
│   │   │   │   ├── channels.ts
│   │   │   │   ├── gateway.ts
│   │   │   │   ├── models.ts
│   │   │   │   ├── oauth.ts
│   │   │   │   ├── pico.ts
│   │   │   │   ├── sessions.ts
│   │   │   │   ├── skills.ts
│   │   │   │   ├── system.ts
│   │   │   │   └── tools.ts
│   │   │   ├── components/
│   │   │   │   ├── app-header.tsx
│   │   │   │   ├── app-layout.tsx
│   │   │   │   ├── app-sidebar.tsx
│   │   │   │   ├── channels/
│   │   │   │   │   ├── channel-config-page.tsx
│   │   │   │   │   ├── channel-display-name.ts
│   │   │   │   │   └── channel-forms/
│   │   │   │   │       ├── discord-form.tsx
│   │   │   │   │       ├── feishu-form.tsx
│   │   │   │   │       ├── generic-form.tsx
│   │   │   │   │       ├── slack-form.tsx
│   │   │   │   │       └── telegram-form.tsx
│   │   │   │   ├── chat/
│   │   │   │   │   ├── assistant-message.tsx
│   │   │   │   │   ├── chat-composer.tsx
│   │   │   │   │   ├── chat-empty-state.tsx
│   │   │   │   │   ├── chat-page.tsx
│   │   │   │   │   ├── model-selector.tsx
│   │   │   │   │   ├── session-history-menu.tsx
│   │   │   │   │   ├── typing-indicator.tsx
│   │   │   │   │   └── user-message.tsx
│   │   │   │   ├── config/
│   │   │   │   │   ├── config-page.tsx
│   │   │   │   │   ├── config-sections.tsx
│   │   │   │   │   ├── form-model.ts
│   │   │   │   │   └── raw-config-page.tsx
│   │   │   │   ├── credentials/
│   │   │   │   │   ├── anthropic-credential-card.tsx
│   │   │   │   │   ├── antigravity-credential-card.tsx
│   │   │   │   │   ├── credential-card.tsx
│   │   │   │   │   ├── credentials-page.tsx
│   │   │   │   │   ├── device-code-sheet.tsx
│   │   │   │   │   ├── logout-confirm-dialog.tsx
│   │   │   │   │   ├── openai-credential-card.tsx
│   │   │   │   │   └── provider-status-line.tsx
│   │   │   │   ├── logs/
│   │   │   │   │   ├── ansi-log-line.tsx
│   │   │   │   │   ├── logs-page.tsx
│   │   │   │   │   └── logs-panel.tsx
│   │   │   │   ├── models/
│   │   │   │   │   ├── add-model-sheet.tsx
│   │   │   │   │   ├── delete-model-dialog.tsx
│   │   │   │   │   ├── edit-model-sheet.tsx
│   │   │   │   │   ├── model-card.tsx
│   │   │   │   │   ├── models-page.tsx
│   │   │   │   │   ├── provider-icon.tsx
│   │   │   │   │   ├── provider-label.ts
│   │   │   │   │   └── provider-section.tsx
│   │   │   │   ├── page-header.tsx
│   │   │   │   ├── secret-placeholder.ts
│   │   │   │   ├── shared-form.tsx
│   │   │   │   ├── skills/
│   │   │   │   │   └── skills-page.tsx
│   │   │   │   ├── tools/
│   │   │   │   │   └── tools-page.tsx
│   │   │   │   └── ui/
│   │   │   │       ├── alert-dialog.tsx
│   │   │   │       ├── button.tsx
│   │   │   │       ├── card.tsx
│   │   │   │       ├── collapsible.tsx
│   │   │   │       ├── dropdown-menu.tsx
│   │   │   │       ├── field.tsx
│   │   │   │       ├── input.tsx
│   │   │   │       ├── label.tsx
│   │   │   │       ├── scroll-area.tsx
│   │   │   │       ├── select.tsx
│   │   │   │       ├── separator.tsx
│   │   │   │       ├── sheet.tsx
│   │   │   │       ├── sidebar.tsx
│   │   │   │       ├── skeleton.tsx
│   │   │   │       ├── switch.tsx
│   │   │   │       ├── textarea.tsx
│   │   │   │       └── tooltip.tsx
│   │   │   ├── features/
│   │   │   │   └── chat/
│   │   │   │       ├── controller.ts
│   │   │   │       ├── history.ts
│   │   │   │       ├── protocol.ts
│   │   │   │       ├── state.ts
│   │   │   │       └── websocket.ts
│   │   │   ├── hooks/
│   │   │   │   ├── use-chat-models.ts
│   │   │   │   ├── use-credentials-page.ts
│   │   │   │   ├── use-gateway-logs.ts
│   │   │   │   ├── use-gateway.ts
│   │   │   │   ├── use-log-wrap-columns.ts
│   │   │   │   ├── use-mobile.ts
│   │   │   │   ├── use-pico-chat.ts
│   │   │   │   ├── use-session-history.ts
│   │   │   │   ├── use-sidebar-channels.ts
│   │   │   │   └── use-theme.ts
│   │   │   ├── i18n/
│   │   │   │   ├── index.ts
│   │   │   │   └── locales/
│   │   │   │       ├── en.json
│   │   │   │       └── zh.json
│   │   │   ├── index.css
│   │   │   ├── lib/
│   │   │   │   ├── ansi-log.ts
│   │   │   │   └── utils.ts
│   │   │   ├── main.tsx
│   │   │   ├── routeTree.gen.ts
│   │   │   ├── routes/
│   │   │   │   ├── __root.tsx
│   │   │   │   ├── agent/
│   │   │   │   │   ├── skills.tsx
│   │   │   │   │   └── tools.tsx
│   │   │   │   ├── agent.tsx
│   │   │   │   ├── channels/
│   │   │   │   │   ├── $name.tsx
│   │   │   │   │   └── route.tsx
│   │   │   │   ├── config.raw.tsx
│   │   │   │   ├── config.tsx
│   │   │   │   ├── credentials.tsx
│   │   │   │   ├── index.tsx
│   │   │   │   ├── logs.tsx
│   │   │   │   └── models.tsx
│   │   │   └── store/
│   │   │       ├── chat.ts
│   │   │       ├── gateway.ts
│   │   │       └── index.ts
│   │   ├── tsconfig.app.json
│   │   ├── tsconfig.json
│   │   ├── tsconfig.node.json
│   │   └── vite.config.ts
│   └── picoclaw-launcher.desktop
└── workspace/
    ├── AGENTS.md
    ├── IDENTITY.md
    ├── SOUL.md
    ├── USER.md
    ├── memory/
    │   └── MEMORY.md
    └── skills/
        ├── github/
        │   └── SKILL.md
        ├── hardware/
        │   ├── SKILL.md
        │   └── references/
        │       ├── board-pinout.md
        │       └── common-devices.md
        ├── skill-creator/
        │   └── SKILL.md
        ├── summarize/
        │   └── SKILL.md
        ├── tmux/
        │   ├── SKILL.md
        │   └── scripts/
        │       ├── find-sessions.sh
        │       └── wait-for-text.sh
        └── weather/
            └── SKILL.md
Download .txt
Showing preview only (481K chars total). Download the full file or copy to clipboard to get everything.
SYMBOL INDEX (4859 symbols across 532 files)

FILE: cmd/picoclaw-launcher-tui/internal/config/store.go
  constant configDirName (line 12) | configDirName  = ".picoclaw"
  constant configFileName (line 13) | configFileName = "config.json"
  function ConfigPath (line 16) | func ConfigPath() (string, error) {
  function ConfigDir (line 24) | func ConfigDir() (string, error) {
  function Load (line 32) | func Load() (*picoclawconfig.Config, error) {
  function Save (line 40) | func Save(cfg *picoclawconfig.Config) error {

FILE: cmd/picoclaw-launcher-tui/internal/ui/app.go
  type appState (line 17) | type appState struct
    method push (line 79) | func (s *appState) push(name string, primitive tview.Primitive) {
    method pop (line 88) | func (s *appState) pop() {
    method mainMenu (line 106) | func (s *appState) mainMenu() tview.Primitive {
    method refreshMenu (line 122) | func (s *appState) refreshMenu(name string, menu *Menu) {
    method countChannels (line 133) | func (s *appState) countChannels() (enabled int, total int) {
    method applyChangesValidated (line 247) | func (s *appState) applyChangesValidated() bool {
    method requestExit (line 268) | func (s *appState) requestExit() {
    method requestStartTalk (line 281) | func (s *appState) requestStartTalk() {
    method requestStartGateway (line 293) | func (s *appState) requestStartGateway() {
    method viewGatewayLog (line 305) | func (s *appState) viewGatewayLog() {
    method selectedModelName (line 327) | func (s *appState) selectedModelName() string {
    method startTalk (line 356) | func (s *appState) startTalk() {
    method startGateway (line 373) | func (s *appState) startGateway() {
    method stopGateway (line 403) | func (s *appState) stopGateway() {
    method isGatewayRunning (line 411) | func (s *appState) isGatewayRunning() bool {
    method validateAgentModel (line 415) | func (s *appState) validateAgentModel() error {
    method isActiveModelValid (line 424) | func (s *appState) isActiveModelValid() bool {
    method hasEnabledChannel (line 438) | func (s *appState) hasEnabledChannel() bool {
    method confirmApplyOrDiscard (line 445) | func (s *appState) confirmApplyOrDiscard(onApply func(), onDiscard fun...
    method discardChanges (line 473) | func (s *appState) discardChanges() {
    method showMessage (line 487) | func (s *appState) showMessage(title, message string) {
  function Run (line 32) | func Run() error {
  function refreshMainMenuIfPresent (line 159) | func refreshMainMenuIfPresent(s *appState) {
  function refreshMainMenu (line 165) | func refreshMainMenu(menu *Menu, s *appState) {
  function rootModelLabel (line 338) | func rootModelLabel(selected string) string {
  function rootModelDescription (line 345) | func rootModelDescription() string {
  function rootChannelLabel (line 349) | func rootChannelLabel(valid bool) string {
  function loadOriginalConfig (line 505) | func loadOriginalConfig(path string) ([]byte, bool) {
  function writeOriginalConfig (line 516) | func writeOriginalConfig(path string, data []byte) error {
  function writeBackupConfig (line 520) | func writeBackupConfig(path string, data []byte) error {

FILE: cmd/picoclaw-launcher-tui/internal/ui/channel.go
  method buildChannelMenuItems (line 13) | func (s *appState) buildChannelMenuItems() []MenuItem {
  method channelMenu (line 96) | func (s *appState) channelMenu() tview.Primitive {
  function refreshChannelMenuFromState (line 108) | func refreshChannelMenuFromState(menu *Menu, s *appState) {
  method telegramForm (line 112) | func (s *appState) telegramForm() tview.Primitive {
  method discordForm (line 125) | func (s *appState) discordForm() tview.Primitive {
  method qqForm (line 138) | func (s *appState) qqForm() tview.Primitive {
  method maixcamForm (line 151) | func (s *appState) maixcamForm() tview.Primitive {
  method whatsappForm (line 162) | func (s *appState) whatsappForm() tview.Primitive {
  method feishuForm (line 172) | func (s *appState) feishuForm() tview.Primitive {
  method dingtalkForm (line 191) | func (s *appState) dingtalkForm() tview.Primitive {
  method slackForm (line 204) | func (s *appState) slackForm() tview.Primitive {
  method lineForm (line 217) | func (s *appState) lineForm() tview.Primitive {
  method matrixForm (line 237) | func (s *appState) matrixForm() tview.Primitive {
  method onebotForm (line 259) | func (s *appState) onebotForm() tview.Primitive {
  method wecomForm (line 287) | func (s *appState) wecomForm() tview.Primitive {
  method wecomAppForm (line 316) | func (s *appState) wecomAppForm() tview.Primitive {
  method makeChannelOnEnabled (line 349) | func (s *appState) makeChannelOnEnabled(enabledPtr *bool) func(bool) {
  function addAllowFromField (line 360) | func addAllowFromField(form *tview.Form, allowFrom *picoclawconfig.Flexi...
  function baseChannelForm (line 366) | func baseChannelForm(title string, enabled bool, onEnabled func(bool)) *...
  function wrapWithBack (line 377) | func wrapWithBack(form *tview.Form, s *appState) tview.Primitive {
  function splitCSV (line 391) | func splitCSV(input string) picoclawconfig.FlexibleStringSlice {
  function addIntField (line 404) | func addIntField(form *tview.Form, label string, value int, onChange fun...
  function addInt64Field (line 413) | func addInt64Field(form *tview.Form, label string, value int64, onChange...
  function channelItem (line 422) | func channelItem(label, description string, enabled bool, action MenuAct...

FILE: cmd/picoclaw-launcher-tui/internal/ui/gateway_posix.go
  function isGatewayProcessRunning (line 8) | func isGatewayProcessRunning() bool {
  function stopGatewayProcess (line 13) | func stopGatewayProcess() error {

FILE: cmd/picoclaw-launcher-tui/internal/ui/gateway_windows.go
  function isGatewayProcessRunning (line 8) | func isGatewayProcessRunning() bool {
  function stopGatewayProcess (line 13) | func stopGatewayProcess() error {

FILE: cmd/picoclaw-launcher-tui/internal/ui/menu.go
  type MenuAction (line 8) | type MenuAction
  type MenuItem (line 10) | type MenuItem struct
  type Menu (line 19) | type Menu struct
    method applyItems (line 47) | func (m *Menu) applyItems(items []MenuItem) {
  function NewMenu (line 24) | func NewMenu(title string, items []MenuItem) *Menu {

FILE: cmd/picoclaw-launcher-tui/internal/ui/model.go
  method modelMenu (line 16) | func (s *appState) modelMenu() tview.Primitive {
  method modelForm (line 92) | func (s *appState) modelForm(index int) tview.Primitive {
  function addInput (line 203) | func addInput(form *tview.Form, label, value string, onChange func(strin...
  function addIntInput (line 209) | func addIntInput(form *tview.Form, label string, value int, onChange fun...
  method addModel (line 218) | func (s *appState) addModel(model picoclawconfig.ModelConfig) {
  method deleteModel (line 222) | func (s *appState) deleteModel(index int) {
  function modelStatusColor (line 230) | func modelStatusColor(valid bool, selected bool) *tcell.Color {
  function refreshModelMenu (line 239) | func refreshModelMenu(menu *Menu, currentModel string, models []picoclaw...
  function refreshModelMenuFromState (line 259) | func refreshModelMenuFromState(menu *Menu, s *appState) {
  function isModelValid (line 303) | func isModelValid(model picoclawconfig.ModelConfig) bool {
  method modelNameExists (line 310) | func (s *appState) modelNameExists(name string, excludeIndex int) bool {
  method nextAvailableModelName (line 326) | func (s *appState) nextAvailableModelName(base string) string {
  method testModel (line 342) | func (s *appState) testModel(model *picoclawconfig.ModelConfig) {

FILE: cmd/picoclaw-launcher-tui/internal/ui/style.go
  constant colorBlue (line 9) | colorBlue = "[#3e5db9]"
  constant colorRed (line 10) | colorRed  = "[#d54646]"
  constant banner (line 11) | banner    = "\r\n[::b]" +
  function applyStyles (line 21) | func applyStyles() {
  function bannerView (line 35) | func bannerView() *tview.TextView {
  constant footerText (line 45) | footerText = "Esc: Back/Exit | Enter: Enter | ←↓↑→ : Move | Space: Selec...
  function footerView (line 47) | func footerView() *tview.TextView {

FILE: cmd/picoclaw-launcher-tui/main.go
  function main (line 10) | func main() {

FILE: cmd/picoclaw/internal/agent/command.go
  function NewAgentCommand (line 7) | func NewAgentCommand() *cobra.Command {

FILE: cmd/picoclaw/internal/agent/command_test.go
  function TestNewAgentCommand (line 10) | func TestNewAgentCommand(t *testing.T) {

FILE: cmd/picoclaw/internal/agent/helpers.go
  function agentCmd (line 21) | func agentCmd(message, sessionKey, model string, debug bool) error {
  function interactiveMode (line 80) | func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
  function simpleInteractiveMode (line 130) | func simpleInteractiveMode(agentLoop *agent.AgentLoop, sessionKey string) {

FILE: cmd/picoclaw/internal/auth/command.go
  function NewAuthCommand (line 5) | func NewAuthCommand() *cobra.Command {

FILE: cmd/picoclaw/internal/auth/command_test.go
  function TestNewAuthCommand (line 11) | func TestNewAuthCommand(t *testing.T) {

FILE: cmd/picoclaw/internal/auth/helpers.go
  constant supportedProvidersMsg (line 20) | supportedProvidersMsg = "supported providers: openai, anthropic, google-...
  constant defaultAnthropicModel (line 21) | defaultAnthropicModel = "claude-sonnet-4.6"
  function authLoginCmd (line 24) | func authLoginCmd(provider string, useDeviceCode bool, useOauth bool) er...
  function authLoginOpenAI (line 37) | func authLoginOpenAI(useDeviceCode bool) error {
  function authLoginGoogleAntigravity (line 98) | func authLoginGoogleAntigravity() error {
  function authLoginAnthropic (line 170) | func authLoginAnthropic(useOauth bool) error {
  function authLoginAnthropicSetupToken (line 201) | func authLoginAnthropicSetupToken() error {
  function fetchGoogleUserEmail (line 245) | func fetchGoogleUserEmail(accessToken string) (string, error) {
  function authLoginPasteToken (line 276) | func authLoginPasteToken(provider string) error {
  function authLogoutCmd (line 343) | func authLogoutCmd(provider string) error {
  function authStatusCmd (line 407) | func authStatusCmd() error {
  function authModelsCmd (line 459) | func authModelsCmd() error {
  function isAntigravityModel (line 511) | func isAntigravityModel(model string) bool {
  function isOpenAIModel (line 519) | func isOpenAIModel(model string) bool {
  function isAnthropicModel (line 525) | func isAnthropicModel(model string) bool {

FILE: cmd/picoclaw/internal/auth/login.go
  function newLoginCommand (line 5) | func newLoginCommand() *cobra.Command {

FILE: cmd/picoclaw/internal/auth/login_test.go
  function TestNewLoginSubCommand (line 11) | func TestNewLoginSubCommand(t *testing.T) {

FILE: cmd/picoclaw/internal/auth/logout.go
  function newLogoutCommand (line 5) | func newLogoutCommand() *cobra.Command {

FILE: cmd/picoclaw/internal/auth/logout_test.go
  function TestNewLogoutSubcommand (line 10) | func TestNewLogoutSubcommand(t *testing.T) {

FILE: cmd/picoclaw/internal/auth/models.go
  function newModelsCommand (line 5) | func newModelsCommand() *cobra.Command {

FILE: cmd/picoclaw/internal/auth/models_test.go
  function TestNewModelsCommand (line 10) | func TestNewModelsCommand(t *testing.T) {

FILE: cmd/picoclaw/internal/auth/status.go
  function newStatusCommand (line 5) | func newStatusCommand() *cobra.Command {

FILE: cmd/picoclaw/internal/auth/status_test.go
  function TestNewStatusSubcommand (line 10) | func TestNewStatusSubcommand(t *testing.T) {

FILE: cmd/picoclaw/internal/cron/add.go
  function newAddCommand (line 11) | func newAddCommand(storePath func() string) *cobra.Command {

FILE: cmd/picoclaw/internal/cron/add_test.go
  function TestNewAddSubcommand (line 11) | func TestNewAddSubcommand(t *testing.T) {
  function TestNewAddCommandEveryAndCronMutuallyExclusive (line 45) | func TestNewAddCommandEveryAndCronMutuallyExclusive(t *testing.T) {

FILE: cmd/picoclaw/internal/cron/command.go
  function NewCronCommand (line 12) | func NewCronCommand() *cobra.Command {

FILE: cmd/picoclaw/internal/cron/command_test.go
  function TestNewCronCommand (line 11) | func TestNewCronCommand(t *testing.T) {

FILE: cmd/picoclaw/internal/cron/disable.go
  function newDisableCommand (line 5) | func newDisableCommand(storePath func() string) *cobra.Command {

FILE: cmd/picoclaw/internal/cron/disable_test.go
  function TestDisableSubcommand (line 10) | func TestDisableSubcommand(t *testing.T) {

FILE: cmd/picoclaw/internal/cron/enable.go
  function newEnableCommand (line 5) | func newEnableCommand(storePath func() string) *cobra.Command {

FILE: cmd/picoclaw/internal/cron/enable_test.go
  function TestEnableSubcommand (line 10) | func TestEnableSubcommand(t *testing.T) {

FILE: cmd/picoclaw/internal/cron/helpers.go
  function cronListCmd (line 10) | func cronListCmd(storePath string) {
  function cronRemoveCmd (line 49) | func cronRemoveCmd(storePath, jobID string) {
  function cronSetJobEnabled (line 58) | func cronSetJobEnabled(storePath, jobID string, enabled bool) {

FILE: cmd/picoclaw/internal/cron/list.go
  function newListCommand (line 5) | func newListCommand(storePath func() string) *cobra.Command {

FILE: cmd/picoclaw/internal/cron/list_test.go
  function TestNewListSubcommand (line 10) | func TestNewListSubcommand(t *testing.T) {

FILE: cmd/picoclaw/internal/cron/remove.go
  function newRemoveCommand (line 5) | func newRemoveCommand(storePath func() string) *cobra.Command {

FILE: cmd/picoclaw/internal/cron/remove_test.go
  function TestNewRemoveSubcommand (line 10) | func TestNewRemoveSubcommand(t *testing.T) {

FILE: cmd/picoclaw/internal/gateway/command.go
  function NewGatewayCommand (line 14) | func NewGatewayCommand() *cobra.Command {

FILE: cmd/picoclaw/internal/gateway/command_test.go
  function TestNewGatewayCommand (line 10) | func TestNewGatewayCommand(t *testing.T) {

FILE: cmd/picoclaw/internal/helpers.go
  constant Logo (line 10) | Logo = "🦞"
  function GetPicoclawHome (line 14) | func GetPicoclawHome() string {
  function GetConfigPath (line 22) | func GetConfigPath() string {
  function LoadConfig (line 29) | func LoadConfig() (*config.Config, error) {
  function FormatVersion (line 35) | func FormatVersion() string {
  function FormatBuildInfo (line 41) | func FormatBuildInfo() (string, string) {
  function GetVersion (line 47) | func GetVersion() string {

FILE: cmd/picoclaw/internal/helpers_test.go
  function TestGetConfigPath (line 13) | func TestGetConfigPath(t *testing.T) {
  function TestGetConfigPath_WithPICOCLAW_HOME (line 22) | func TestGetConfigPath_WithPICOCLAW_HOME(t *testing.T) {
  function TestGetConfigPath_WithPICOCLAW_CONFIG (line 32) | func TestGetConfigPath_WithPICOCLAW_CONFIG(t *testing.T) {
  function TestGetConfigPath_Windows (line 43) | func TestGetConfigPath_Windows(t *testing.T) {

FILE: cmd/picoclaw/internal/migrate/command.go
  function NewMigrateCommand (line 9) | func NewMigrateCommand() *cobra.Command {

FILE: cmd/picoclaw/internal/migrate/command_test.go
  function TestNewMigrateCommand (line 10) | func TestNewMigrateCommand(t *testing.T) {

FILE: cmd/picoclaw/internal/model/command.go
  constant LocalModel (line 13) | LocalModel = "local-model"
  function NewModelCommand (line 15) | func NewModelCommand() *cobra.Command {
  function showCurrentModel (line 57) | func showCurrentModel(cfg *config.Config) {
  function listAvailableModels (line 74) | func listAvailableModels(cfg *config.Config) {
  function setDefaultModel (line 97) | func setDefaultModel(configPath string, cfg *config.Config, modelName st...
  function formatModelName (line 133) | func formatModelName(name string) string {

FILE: cmd/picoclaw/internal/model/command_test.go
  function initTest (line 18) | func initTest(t *testing.T) {
  function captureStdout (line 25) | func captureStdout(fn func()) string {
  function TestNewModelCommand (line 40) | func TestNewModelCommand(t *testing.T) {
  function TestShowCurrentModel_WithDefaultModel (line 60) | func TestShowCurrentModel_WithDefaultModel(t *testing.T) {
  function TestShowCurrentModel_NoDefaultModel (line 83) | func TestShowCurrentModel_NoDefaultModel(t *testing.T) {
  function TestShowCurrentModel_BackwardCompatibility (line 104) | func TestShowCurrentModel_BackwardCompatibility(t *testing.T) {
  function TestListAvailableModels_Empty (line 121) | func TestListAvailableModels_Empty(t *testing.T) {
  function TestListAvailableModels_WithModels (line 133) | func TestListAvailableModels_WithModels(t *testing.T) {
  function TestSetDefaultModel_ValidModel (line 157) | func TestSetDefaultModel_ValidModel(t *testing.T) {
  function TestSetDefaultModel_LegacyModelField (line 186) | func TestSetDefaultModel_LegacyModelField(t *testing.T) {
  function TestSetDefaultModel_InvalidModel (line 208) | func TestSetDefaultModel_InvalidModel(t *testing.T) {
  function TestSetDefaultModel_ModelWithoutAPIKey (line 225) | func TestSetDefaultModel_ModelWithoutAPIKey(t *testing.T) {
  function TestSetDefaultModel_SaveConfigError (line 243) | func TestSetDefaultModel_SaveConfigError(t *testing.T) {
  function TestFormatModelName (line 264) | func TestFormatModelName(t *testing.T) {
  function TestModelCommandExecution_Show (line 284) | func TestModelCommandExecution_Show(t *testing.T) {
  function TestModelCommandExecution_Set (line 312) | func TestModelCommandExecution_Set(t *testing.T) {
  function TestModelCommandExecution_TooManyArgs (line 340) | func TestModelCommandExecution_TooManyArgs(t *testing.T) {
  function TestListAvailableModels_MarkerLogic (line 348) | func TestListAvailableModels_MarkerLogic(t *testing.T) {

FILE: cmd/picoclaw/internal/onboard/command.go
  function NewOnboardCommand (line 13) | func NewOnboardCommand() *cobra.Command {

FILE: cmd/picoclaw/internal/onboard/command_test.go
  function TestNewOnboardCommand (line 10) | func TestNewOnboardCommand(t *testing.T) {

FILE: cmd/picoclaw/internal/onboard/helpers.go
  function onboard (line 16) | func onboard(encrypt bool) {
  function promptPassphrase (line 106) | func promptPassphrase() (string, error) {
  function setupSSHKey (line 133) | func setupSSHKey() error {
  function createWorkspaceTemplates (line 158) | func createWorkspaceTemplates(workspace string) {
  function copyEmbeddedToTarget (line 165) | func copyEmbeddedToTarget(targetDir string) error {

FILE: cmd/picoclaw/internal/onboard/helpers_test.go
  function TestCopyEmbeddedToTargetUsesAgentsMarkdown (line 9) | func TestCopyEmbeddedToTargetUsesAgentsMarkdown(t *testing.T) {

FILE: cmd/picoclaw/internal/skills/command.go
  type deps (line 13) | type deps struct
  function NewSkillsCommand (line 19) | func NewSkillsCommand() *cobra.Command {

FILE: cmd/picoclaw/internal/skills/command_test.go
  function TestNewSkillsCommand (line 10) | func TestNewSkillsCommand(t *testing.T) {

FILE: cmd/picoclaw/internal/skills/helpers.go
  constant skillsSearchMaxResults (line 18) | skillsSearchMaxResults = 20
  function skillsListCmd (line 20) | func skillsListCmd(loader *skills.SkillsLoader) {
  function skillsInstallCmd (line 38) | func skillsInstallCmd(installer *skills.SkillInstaller, repo string) err...
  function skillsInstallFromRegistry (line 54) | func skillsInstallFromRegistry(cfg *config.Config, registryName, slug st...
  function skillsRemoveCmd (line 121) | func skillsRemoveCmd(installer *skills.SkillInstaller, skillName string) {
  function skillsInstallBuiltinCmd (line 132) | func skillsInstallBuiltinCmd(workspace string) {
  function skillsListBuiltinCmd (line 168) | func skillsListBuiltinCmd() {
  function skillsSearchCmd (line 220) | func skillsSearchCmd(query string) {
  function skillsShowCmd (line 262) | func skillsShowCmd(loader *skills.SkillsLoader, skillName string) {
  function copyDirectory (line 274) | func copyDirectory(src, dst string) error {

FILE: cmd/picoclaw/internal/skills/install.go
  function newInstallCommand (line 12) | func newInstallCommand(installerFn func() (*skills.SkillInstaller, error...

FILE: cmd/picoclaw/internal/skills/install_test.go
  function TestNewInstallSubcommand (line 10) | func TestNewInstallSubcommand(t *testing.T) {
  function TestInstallCommandArgs (line 30) | func TestInstallCommandArgs(t *testing.T) {

FILE: cmd/picoclaw/internal/skills/installbuiltin.go
  function newInstallBuiltinCommand (line 5) | func newInstallBuiltinCommand(workspaceFn func() (string, error)) *cobra...

FILE: cmd/picoclaw/internal/skills/installbuiltin_test.go
  function TestNewInstallbuiltinSubcommand (line 10) | func TestNewInstallbuiltinSubcommand(t *testing.T) {

FILE: cmd/picoclaw/internal/skills/list.go
  function newListCommand (line 9) | func newListCommand(loaderFn func() (*skills.SkillsLoader, error)) *cobr...

FILE: cmd/picoclaw/internal/skills/list_test.go
  function TestNewListSubcommand (line 10) | func TestNewListSubcommand(t *testing.T) {

FILE: cmd/picoclaw/internal/skills/listbuiltin.go
  function newListBuiltinCommand (line 5) | func newListBuiltinCommand() *cobra.Command {

FILE: cmd/picoclaw/internal/skills/listbuiltin_test.go
  function TestNewListbuiltinSubcommand (line 10) | func TestNewListbuiltinSubcommand(t *testing.T) {

FILE: cmd/picoclaw/internal/skills/remove.go
  function newRemoveCommand (line 9) | func newRemoveCommand(installerFn func() (*skills.SkillInstaller, error)...

FILE: cmd/picoclaw/internal/skills/remove_test.go
  function TestNewRemoveSubcommand (line 10) | func TestNewRemoveSubcommand(t *testing.T) {

FILE: cmd/picoclaw/internal/skills/search.go
  function newSearchCommand (line 7) | func newSearchCommand() *cobra.Command {

FILE: cmd/picoclaw/internal/skills/search_test.go
  function TestNewSearchSubcommand (line 10) | func TestNewSearchSubcommand(t *testing.T) {

FILE: cmd/picoclaw/internal/skills/show.go
  function newShowCommand (line 9) | func newShowCommand(loaderFn func() (*skills.SkillsLoader, error)) *cobr...

FILE: cmd/picoclaw/internal/skills/show_test.go
  function TestNewShowSubcommand (line 10) | func TestNewShowSubcommand(t *testing.T) {

FILE: cmd/picoclaw/internal/status/command.go
  function NewStatusCommand (line 7) | func NewStatusCommand() *cobra.Command {

FILE: cmd/picoclaw/internal/status/command_test.go
  function TestNewStatusCommand (line 10) | func TestNewStatusCommand(t *testing.T) {

FILE: cmd/picoclaw/internal/status/helpers.go
  function statusCmd (line 12) | func statusCmd() {

FILE: cmd/picoclaw/internal/version/command.go
  function NewVersionCommand (line 12) | func NewVersionCommand() *cobra.Command {
  function printVersion (line 25) | func printVersion() {

FILE: cmd/picoclaw/internal/version/command_test.go
  function TestNewVersionCommand (line 10) | func TestNewVersionCommand(t *testing.T) {

FILE: cmd/picoclaw/main.go
  function NewPicoclawCommand (line 29) | func NewPicoclawCommand() *cobra.Command {
  constant colorBlue (line 55) | colorBlue = "\033[1;38;2;62;93;185m"
  constant colorRed (line 56) | colorRed  = "\033[1;38;2;213;70;70m"
  constant banner (line 57) | banner    = "\r\n" +
  function main (line 67) | func main() {

FILE: cmd/picoclaw/main_test.go
  function TestNewPicoclawCommand (line 15) | func TestNewPicoclawCommand(t *testing.T) {

FILE: pkg/agent/context.go
  type ContextBuilder (line 22) | type ContextBuilder struct
    method WithToolDiscovery (line 48) | func (cb *ContextBuilder) WithToolDiscovery(useBM25, useRegex bool) *C...
    method getIdentity (line 82) | func (cb *ContextBuilder) getIdentity() string {
    method getDiscoveryRule (line 112) | func (cb *ContextBuilder) getDiscoveryRule() string {
    method BuildSystemPrompt (line 131) | func (cb *ContextBuilder) BuildSystemPrompt() string {
    method BuildSystemPromptWithCache (line 166) | func (cb *ContextBuilder) BuildSystemPromptWithCache() string {
    method InvalidateCache (line 209) | func (cb *ContextBuilder) InvalidateCache() {
    method sourcePaths (line 224) | func (cb *ContextBuilder) sourcePaths() []string {
    method skillRoots (line 236) | func (cb *ContextBuilder) skillRoots() []string {
    method buildCacheBaseline (line 259) | func (cb *ContextBuilder) buildCacheBaseline() cacheBaseline {
    method sourceFilesChangedLocked (line 313) | func (cb *ContextBuilder) sourceFilesChangedLocked() bool {
    method fileChangedSince (line 348) | func (cb *ContextBuilder) fileChangedSince(path string) bool {
    method LoadBootstrapFiles (line 434) | func (cb *ContextBuilder) LoadBootstrapFiles() string {
    method buildDynamicContext (line 477) | func (cb *ContextBuilder) buildDynamicContext(channel, chatID, senderI...
    method BuildMessages (line 494) | func (cb *ContextBuilder) BuildMessages(
    method AddToolResult (line 714) | func (cb *ContextBuilder) AddToolResult(
    method AddAssistantMessage (line 726) | func (cb *ContextBuilder) AddAssistantMessage(
    method GetSkillsInfo (line 741) | func (cb *ContextBuilder) GetSkillsInfo() map[string]any {
  function getGlobalConfigDir (line 54) | func getGlobalConfigDir() string {
  function NewContextBuilder (line 65) | func NewContextBuilder(workspace string) *ContextBuilder {
  type cacheBaseline (line 250) | type cacheBaseline struct
  function skillFilesChangedSince (line 377) | func skillFilesChangedSince(skillRoots []string, filesAtCache map[string...
  function formatCurrentSenderLine (line 461) | func formatCurrentSenderLine(senderID, senderDisplayName string) string {
  function sanitizeHistoryForProvider (line 596) | func sanitizeHistoryForProvider(history []providers.Message) []providers...

FILE: pkg/agent/context_cache_test.go
  function setupWorkspace (line 16) | func setupWorkspace(t *testing.T, files map[string]string) string {
  function TestSingleSystemMessage (line 38) | func TestSingleSystemMessage(t *testing.T) {
  function TestBuildMessages_CurrentSenderDynamicContext (line 129) | func TestBuildMessages_CurrentSenderDynamicContext(t *testing.T) {
  function TestMtimeAutoInvalidation (line 195) | func TestMtimeAutoInvalidation(t *testing.T) {
  function TestExplicitInvalidateCache (line 281) | func TestExplicitInvalidateCache(t *testing.T) {
  function TestCacheStability (line 308) | func TestCacheStability(t *testing.T) {
  function TestNewFileCreationInvalidatesCache (line 337) | func TestNewFileCreationInvalidatesCache(t *testing.T) {
  function TestSkillFileContentChange (line 396) | func TestSkillFileContentChange(t *testing.T) {
  function TestGlobalSkillFileContentChange (line 450) | func TestGlobalSkillFileContentChange(t *testing.T) {
  function TestBuiltinSkillFileContentChange (line 507) | func TestBuiltinSkillFileContentChange(t *testing.T) {
  function TestSkillFileDeletionInvalidatesCache (line 567) | func TestSkillFileDeletionInvalidatesCache(t *testing.T) {
  function TestConcurrentBuildSystemPromptWithCache (line 608) | func TestConcurrentBuildSystemPromptWithCache(t *testing.T) {
  function TestEmptyWorkspaceBaselineDetectsNewFiles (line 675) | func TestEmptyWorkspaceBaselineDetectsNewFiles(t *testing.T) {
  function BenchmarkBuildMessagesWithCache (line 711) | func BenchmarkBuildMessagesWithCache(b *testing.B) {

FILE: pkg/agent/context_test.go
  function msg (line 9) | func msg(role, content string) providers.Message {
  function assistantWithTools (line 13) | func assistantWithTools(toolIDs ...string) providers.Message {
  function toolResult (line 21) | func toolResult(id string) providers.Message {
  function TestSanitizeHistoryForProvider_EmptyHistory (line 25) | func TestSanitizeHistoryForProvider_EmptyHistory(t *testing.T) {
  function TestSanitizeHistoryForProvider_SingleToolCall (line 37) | func TestSanitizeHistoryForProvider_SingleToolCall(t *testing.T) {
  function TestSanitizeHistoryForProvider_MultiToolCalls (line 52) | func TestSanitizeHistoryForProvider_MultiToolCalls(t *testing.T) {
  function TestSanitizeHistoryForProvider_AssistantToolCallAfterPlainAssistant (line 68) | func TestSanitizeHistoryForProvider_AssistantToolCallAfterPlainAssistant...
  function TestSanitizeHistoryForProvider_OrphanedLeadingTool (line 83) | func TestSanitizeHistoryForProvider_OrphanedLeadingTool(t *testing.T) {
  function TestSanitizeHistoryForProvider_ToolAfterUserDropped (line 96) | func TestSanitizeHistoryForProvider_ToolAfterUserDropped(t *testing.T) {
  function TestSanitizeHistoryForProvider_ToolAfterAssistantNoToolCalls (line 109) | func TestSanitizeHistoryForProvider_ToolAfterAssistantNoToolCalls(t *tes...
  function TestSanitizeHistoryForProvider_AssistantToolCallAtStart (line 123) | func TestSanitizeHistoryForProvider_AssistantToolCallAtStart(t *testing....
  function TestSanitizeHistoryForProvider_MultiToolCallsThenNewRound (line 137) | func TestSanitizeHistoryForProvider_MultiToolCallsThenNewRound(t *testin...
  function TestSanitizeHistoryForProvider_ConsecutiveMultiToolRounds (line 157) | func TestSanitizeHistoryForProvider_ConsecutiveMultiToolRounds(t *testin...
  function TestSanitizeHistoryForProvider_PlainConversation (line 176) | func TestSanitizeHistoryForProvider_PlainConversation(t *testing.T) {
  function roles (line 191) | func roles(msgs []providers.Message) []string {
  function assertRoles (line 199) | func assertRoles(t *testing.T, msgs []providers.Message, expected ...str...
  function TestSanitizeHistoryForProvider_IncompleteToolResults (line 215) | func TestSanitizeHistoryForProvider_IncompleteToolResults(t *testing.T) {
  function TestSanitizeHistoryForProvider_MissingAllToolResults (line 238) | func TestSanitizeHistoryForProvider_MissingAllToolResults(t *testing.T) {
  function TestSanitizeHistoryForProvider_PartialToolResultsInMiddle (line 258) | func TestSanitizeHistoryForProvider_PartialToolResultsInMiddle(t *testin...

FILE: pkg/agent/instance.go
  type AgentInstance (line 23) | type AgentInstance struct
    method Close (line 266) | func (a *AgentInstance) Close() error {
  function NewAgentInstance (line 54) | func NewAgentInstance(
  function resolveAgentWorkspace (line 201) | func resolveAgentWorkspace(agentCfg *config.AgentConfig, defaults *confi...
  function resolveAgentModel (line 215) | func resolveAgentModel(agentCfg *config.AgentConfig, defaults *config.Ag...
  function resolveAgentFallbacks (line 223) | func resolveAgentFallbacks(agentCfg *config.AgentConfig, defaults *confi...
  function compilePatterns (line 230) | func compilePatterns(patterns []string) []*regexp.Regexp {
  function buildAllowReadPatterns (line 243) | func buildAllowReadPatterns(cfg *config.Config) []*regexp.Regexp {
  function mediaTempDirPattern (line 260) | func mediaTempDirPattern() string {
  function initSessionStore (line 277) | func initSessionStore(dir string) session.SessionStore {
  function expandHome (line 300) | func expandHome(path string) string {

FILE: pkg/agent/instance_test.go
  function TestNewAgentInstance_UsesDefaultsTemperatureAndMaxTokens (line 14) | func TestNewAgentInstance_UsesDefaultsTemperatureAndMaxTokens(t *testing...
  function TestNewAgentInstance_DefaultsTemperatureWhenZero (line 46) | func TestNewAgentInstance_DefaultsTemperatureWhenZero(t *testing.T) {
  function TestNewAgentInstance_DefaultsTemperatureWhenUnset (line 75) | func TestNewAgentInstance_DefaultsTemperatureWhenUnset(t *testing.T) {
  function TestNewAgentInstance_ResolveCandidatesFromModelListAlias (line 101) | func TestNewAgentInstance_ResolveCandidatesFromModelListAlias(t *testing...
  function TestNewAgentInstance_AllowsMediaTempDirForReadListAndExec (line 168) | func TestNewAgentInstance_AllowsMediaTempDirForReadListAndExec(t *testin...
  function TestNewAgentInstance_InvalidExecConfigDoesNotExit (line 250) | func TestNewAgentInstance_InvalidExecConfigDoesNotExit(t *testing.T) {

FILE: pkg/agent/loop.go
  type AgentLoop (line 38) | type AgentLoop struct
    method Run (line 265) | func (al *AgentLoop) Run(ctx context.Context) error {
    method Stop (line 347) | func (al *AgentLoop) Stop() {
    method Close (line 352) | func (al *AgentLoop) Close() {
    method RegisterTool (line 367) | func (al *AgentLoop) RegisterTool(tool tools.Tool) {
    method SetChannelManager (line 376) | func (al *AgentLoop) SetChannelManager(cm *channels.Manager) {
    method ReloadProviderAndConfig (line 383) | func (al *AgentLoop) ReloadProviderAndConfig(
    method GetRegistry (line 477) | func (al *AgentLoop) GetRegistry() *AgentRegistry {
    method GetConfig (line 484) | func (al *AgentLoop) GetConfig() *config.Config {
    method SetMediaStore (line 491) | func (al *AgentLoop) SetMediaStore(s media.MediaStore) {
    method SetTranscriber (line 504) | func (al *AgentLoop) SetTranscriber(t voice.Transcriber) {
    method SetReloadFunc (line 509) | func (al *AgentLoop) SetReloadFunc(fn func() error) {
    method transcribeAudioInMessage (line 518) | func (al *AgentLoop) transcribeAudioInMessage(ctx context.Context, msg...
    method sendTranscriptionFeedback (line 573) | func (al *AgentLoop) sendTranscriptionFeedback(
    method RecordLastChannel (line 642) | func (al *AgentLoop) RecordLastChannel(channel string) error {
    method RecordLastChatID (line 651) | func (al *AgentLoop) RecordLastChatID(chatID string) error {
    method ProcessDirect (line 658) | func (al *AgentLoop) ProcessDirect(
    method ProcessDirectWithChannel (line 665) | func (al *AgentLoop) ProcessDirectWithChannel(
    method ProcessHeartbeat (line 686) | func (al *AgentLoop) ProcessHeartbeat(
    method processMessage (line 706) | func (al *AgentLoop) processMessage(ctx context.Context, msg bus.Inbou...
    method resolveMessageRoute (line 787) | func (al *AgentLoop) resolveMessageRoute(msg bus.InboundMessage) (rout...
    method processSystemMessage (line 816) | func (al *AgentLoop) processSystemMessage(
    method runAgentLoop (line 882) | func (al *AgentLoop) runAgentLoop(
    method targetReasoningChannelID (line 972) | func (al *AgentLoop) targetReasoningChannelID(channelName string) (cha...
    method handleReasoning (line 982) | func (al *AgentLoop) handleReasoning(
    method runLLMIteration (line 1029) | func (al *AgentLoop) runLLMIteration(
    method selectCandidates (line 1480) | func (al *AgentLoop) selectCandidates(
    method maybeSummarize (line 1511) | func (al *AgentLoop) maybeSummarize(agent *AgentInstance, sessionKey, ...
    method forceCompression (line 1530) | func (al *AgentLoop) forceCompression(agent *AgentInstance, sessionKey...
    method GetStartupInfo (line 1582) | func (al *AgentLoop) GetStartupInfo() map[string]any {
    method summarizeSession (line 1670) | func (al *AgentLoop) summarizeSession(agent *AgentInstance, sessionKey...
    method findNearestUserMessage (line 1754) | func (al *AgentLoop) findNearestUserMessage(messages []providers.Messa...
    method retryLLMCall (line 1778) | func (al *AgentLoop) retryLLMCall(
    method summarizeBatch (line 1820) | func (al *AgentLoop) summarizeBatch(
    method estimateTokens (line 1887) | func (al *AgentLoop) estimateTokens(messages []providers.Message) int {
    method handleCommand (line 1896) | func (al *AgentLoop) handleCommand(
    method buildCommandsRuntime (line 1939) | func (al *AgentLoop) buildCommandsRuntime(agent *AgentInstance, opts *...
  type processOptions (line 58) | type processOptions struct
  constant defaultResponse (line 73) | defaultResponse           = "I've completed processing but have no respo...
  constant sessionKeyAgentPrefix (line 74) | sessionKeyAgentPrefix     = "agent:"
  constant metadataKeyAccountID (line 75) | metadataKeyAccountID      = "account_id"
  constant metadataKeyGuildID (line 76) | metadataKeyGuildID        = "guild_id"
  constant metadataKeyTeamID (line 77) | metadataKeyTeamID         = "team_id"
  constant metadataKeyParentPeerKind (line 78) | metadataKeyParentPeerKind = "parent_peer_kind"
  constant metadataKeyParentPeerID (line 79) | metadataKeyParentPeerID   = "parent_peer_id"
  function NewAgentLoop (line 82) | func NewAgentLoop(
  function registerSharedTools (line 117) | func registerSharedTools(
  function inferMediaType (line 612) | func inferMediaType(filename, contentType string) string {
  function resolveScopeKey (line 809) | func resolveScopeKey(route routing.ResolvedRoute, msgSessionKey string) ...
  function formatMessagesForLog (line 1611) | func formatMessagesForLog(messages []providers.Message) string {
  function formatToolsForLog (line 1647) | func formatToolsForLog(toolDefs []providers.ToolDefinition) string {
  function mapCommandError (line 2021) | func mapCommandError(result commands.ExecuteResult) string {
  function extractPeer (line 2029) | func extractPeer(msg bus.InboundMessage) *routing.RoutePeer {
  function inboundMetadata (line 2044) | func inboundMetadata(msg bus.InboundMessage, key string) string {
  function extractParentPeer (line 2052) | func extractParentPeer(msg bus.InboundMessage) *routing.RoutePeer {
  function isNativeSearchProvider (line 2063) | func isNativeSearchProvider(p providers.LLMProvider) bool {
  function filterClientWebSearch (line 2072) | func filterClientWebSearch(tools []providers.ToolDefinition) []providers...
  function extractProvider (line 2084) | func extractProvider(registry *AgentRegistry) (providers.LLMProvider, bo...

FILE: pkg/agent/loop_mcp.go
  type mcpRuntime (line 20) | type mcpRuntime struct
    method setManager (line 27) | func (r *mcpRuntime) setManager(manager *mcp.Manager) {
    method setInitErr (line 34) | func (r *mcpRuntime) setInitErr(err error) {
    method getInitErr (line 40) | func (r *mcpRuntime) getInitErr() error {
    method takeManager (line 46) | func (r *mcpRuntime) takeManager() *mcp.Manager {
    method hasManager (line 54) | func (r *mcpRuntime) hasManager() bool {
  method ensureMCPInitialized (line 62) | func (al *AgentLoop) ensureMCPInitialized(ctx context.Context) error {
  function serverIsDeferred (line 215) | func serverIsDeferred(discoveryEnabled bool, serverCfg config.MCPServerC...

FILE: pkg/agent/loop_mcp_test.go
  function boolPtr (line 15) | func boolPtr(b bool) *bool { return &b }
  function TestServerIsDeferred (line 17) | func TestServerIsDeferred(t *testing.T) {

FILE: pkg/agent/loop_media.go
  function resolveMediaRefs (line 28) | func resolveMediaRefs(messages []providers.Message, store media.MediaSto...
  function detectMIME (line 92) | func detectMIME(localPath string, meta media.MediaMeta) string {
  function encodeImageToDataURL (line 105) | func encodeImageToDataURL(localPath, mime string, info os.FileInfo, maxS...
  function buildPathTag (line 146) | func buildPathTag(mime, localPath string) string {
  function injectPathTags (line 159) | func injectPathTags(content string, tags []string) string {

FILE: pkg/agent/loop_test.go
  type fakeChannel (line 25) | type fakeChannel struct
    method Name (line 27) | func (f *fakeChannel) Name() string                                   ...
    method Start (line 28) | func (f *fakeChannel) Start(ctx context.Context) error                ...
    method Stop (line 29) | func (f *fakeChannel) Stop(ctx context.Context) error                 ...
    method Send (line 30) | func (f *fakeChannel) Send(ctx context.Context, msg bus.OutboundMessag...
    method IsRunning (line 31) | func (f *fakeChannel) IsRunning() bool                                ...
    method IsAllowed (line 32) | func (f *fakeChannel) IsAllowed(string) bool                          ...
    method IsAllowedSender (line 33) | func (f *fakeChannel) IsAllowedSender(sender bus.SenderInfo) bool     ...
    method ReasoningChannelID (line 34) | func (f *fakeChannel) ReasoningChannelID() string                     ...
  type recordingProvider (line 36) | type recordingProvider struct
    method Chat (line 40) | func (r *recordingProvider) Chat(
    method GetDefaultModel (line 54) | func (r *recordingProvider) GetDefaultModel() string {
  function newTestAgentLoop (line 58) | func newTestAgentLoop(
  function TestProcessMessage_IncludesCurrentSenderInDynamicContext (line 82) | func TestProcessMessage_IncludesCurrentSenderInDynamicContext(t *testing...
  function TestRecordLastChannel (line 135) | func TestRecordLastChannel(t *testing.T) {
  function TestRecordLastChatID (line 152) | func TestRecordLastChatID(t *testing.T) {
  function TestNewAgentLoop_StateInitialized (line 169) | func TestNewAgentLoop_StateInitialized(t *testing.T) {
  function TestToolRegistry_ToolRegistration (line 207) | func TestToolRegistry_ToolRegistration(t *testing.T) {
  function TestToolContext_Updates (line 247) | func TestToolContext_Updates(t *testing.T) {
  function TestToolRegistry_GetDefinitions (line 264) | func TestToolRegistry_GetDefinitions(t *testing.T) {
  function TestAgentLoop_GetStartupInfo (line 302) | func TestAgentLoop_GetStartupInfo(t *testing.T) {
  function TestAgentLoop_Stop (line 344) | func TestAgentLoop_Stop(t *testing.T) {
  type simpleMockProvider (line 379) | type simpleMockProvider struct
    method Chat (line 383) | func (m *simpleMockProvider) Chat(
    method GetDefaultModel (line 396) | func (m *simpleMockProvider) GetDefaultModel() string {
  type countingMockProvider (line 400) | type countingMockProvider struct
    method Chat (line 405) | func (m *countingMockProvider) Chat(
    method GetDefaultModel (line 419) | func (m *countingMockProvider) GetDefaultModel() string {
  type mockCustomTool (line 424) | type mockCustomTool struct
    method Name (line 426) | func (m *mockCustomTool) Name() string {
    method Description (line 430) | func (m *mockCustomTool) Description() string {
    method Parameters (line 434) | func (m *mockCustomTool) Parameters() map[string]any {
    method Execute (line 441) | func (m *mockCustomTool) Execute(ctx context.Context, args map[string]...
  type testHelper (line 446) | type testHelper struct
    method executeAndGetResponse (line 490) | func (h testHelper) executeAndGetResponse(tb testing.TB, ctx context.C...
  function newChatCompletionTestServer (line 450) | func newChatCompletionTestServer(
  constant responseTimeout (line 502) | responseTimeout = 3 * time.Second
  function TestProcessMessage_UsesRouteSessionKey (line 504) | func TestProcessMessage_UsesRouteSessionKey(t *testing.T) {
  function TestProcessMessage_CommandOutcomes (line 560) | func TestProcessMessage_CommandOutcomes(t *testing.T) {
  function TestProcessMessage_SwitchModelShowModelConsistency (line 639) | func TestProcessMessage_SwitchModelShowModelConsistency(t *testing.T) {
  function TestProcessMessage_SwitchModelRejectsUnknownAlias (line 710) | func TestProcessMessage_SwitchModelRejectsUnknownAlias(t *testing.T) {
  function TestProcessMessage_SwitchModelRoutesSubsequentRequestsToSelectedProvider (line 775) | func TestProcessMessage_SwitchModelRoutesSubsequentRequestsToSelectedPro...
  function TestToolResult_SilentToolDoesNotSendUserMessage (line 892) | func TestToolResult_SilentToolDoesNotSendUserMessage(t *testing.T) {
  function TestToolResult_UserFacingToolDoesSendMessage (line 934) | func TestToolResult_UserFacingToolDoesSendMessage(t *testing.T) {
  type failFirstMockProvider (line 976) | type failFirstMockProvider struct
    method Chat (line 983) | func (m *failFirstMockProvider) Chat(
    method GetDefaultModel (line 1000) | func (m *failFirstMockProvider) GetDefaultModel() string {
  function TestAgentLoop_ContextExhaustionRetry (line 1005) | func TestAgentLoop_ContextExhaustionRetry(t *testing.T) {
  function TestProcessDirectWithChannel_TriggersMCPInitialization (line 1090) | func TestProcessDirectWithChannel_TriggersMCPInitialization(t *testing.T) {
  function TestTargetReasoningChannelID_AllChannels (line 1143) | func TestTargetReasoningChannelID_AllChannels(t *testing.T) {
  function TestHandleReasoning (line 1212) | func TestHandleReasoning(t *testing.T) {
  function TestResolveMediaRefs_ResolvesToBase64 (line 1384) | func TestResolveMediaRefs_ResolvesToBase64(t *testing.T) {
  function TestResolveMediaRefs_SkipsOversizedFile (line 1420) | func TestResolveMediaRefs_SkipsOversizedFile(t *testing.T) {
  function TestResolveMediaRefs_UnknownTypeInjectsPath (line 1444) | func TestResolveMediaRefs_UnknownTypeInjectsPath(t *testing.T) {
  function TestResolveMediaRefs_PassesThroughNonMediaRefs (line 1468) | func TestResolveMediaRefs_PassesThroughNonMediaRefs(t *testing.T) {
  function TestResolveMediaRefs_DoesNotMutateOriginal (line 1479) | func TestResolveMediaRefs_DoesNotMutateOriginal(t *testing.T) {
  function TestResolveMediaRefs_UsesMetaContentType (line 1504) | func TestResolveMediaRefs_UsesMetaContentType(t *testing.T) {
  function TestResolveMediaRefs_PDFInjectsFilePath (line 1527) | func TestResolveMediaRefs_PDFInjectsFilePath(t *testing.T) {
  function TestResolveMediaRefs_AudioInjectsAudioPath (line 1550) | func TestResolveMediaRefs_AudioInjectsAudioPath(t *testing.T) {
  function TestResolveMediaRefs_VideoInjectsVideoPath (line 1572) | func TestResolveMediaRefs_VideoInjectsVideoPath(t *testing.T) {
  function TestResolveMediaRefs_NoGenericTagAppendsPath (line 1594) | func TestResolveMediaRefs_NoGenericTagAppendsPath(t *testing.T) {
  function TestResolveMediaRefs_EmptyContentGetsPathTag (line 1613) | func TestResolveMediaRefs_EmptyContentGetsPathTag(t *testing.T) {
  function TestResolveMediaRefs_MixedImageAndFile (line 1633) | func TestResolveMediaRefs_MixedImageAndFile(t *testing.T) {
  type nativeSearchProvider (line 1670) | type nativeSearchProvider struct
    method Chat (line 1674) | func (p *nativeSearchProvider) Chat(
    method GetDefaultModel (line 1681) | func (p *nativeSearchProvider) GetDefaultModel() string { return "test...
    method SupportsNativeSearch (line 1683) | func (p *nativeSearchProvider) SupportsNativeSearch() bool { return p....
  type plainProvider (line 1685) | type plainProvider struct
    method Chat (line 1687) | func (p *plainProvider) Chat(
    method GetDefaultModel (line 1694) | func (p *plainProvider) GetDefaultModel() string { return "test-model" }
  function TestIsNativeSearchProvider_Supported (line 1696) | func TestIsNativeSearchProvider_Supported(t *testing.T) {
  function TestIsNativeSearchProvider_NotSupported (line 1702) | func TestIsNativeSearchProvider_NotSupported(t *testing.T) {
  function TestIsNativeSearchProvider_NoInterface (line 1708) | func TestIsNativeSearchProvider_NoInterface(t *testing.T) {
  function TestFilterClientWebSearch_RemovesWebSearch (line 1714) | func TestFilterClientWebSearch_RemovesWebSearch(t *testing.T) {
  function TestFilterClientWebSearch_NoWebSearch (line 1731) | func TestFilterClientWebSearch_NoWebSearch(t *testing.T) {
  function TestFilterClientWebSearch_EmptyInput (line 1742) | func TestFilterClientWebSearch_EmptyInput(t *testing.T) {

FILE: pkg/agent/memory.go
  type MemoryStore (line 22) | type MemoryStore struct
    method getTodayFile (line 45) | func (ms *MemoryStore) getTodayFile() string {
    method ReadLongTerm (line 54) | func (ms *MemoryStore) ReadLongTerm() string {
    method WriteLongTerm (line 62) | func (ms *MemoryStore) WriteLongTerm(content string) error {
    method ReadToday (line 70) | func (ms *MemoryStore) ReadToday() string {
    method AppendToday (line 80) | func (ms *MemoryStore) AppendToday(content string) error {
    method GetRecentDailyNotes (line 110) | func (ms *MemoryStore) GetRecentDailyNotes(days int) string {
    method GetMemoryContext (line 134) | func (ms *MemoryStore) GetMemoryContext() string {
  function NewMemoryStore (line 30) | func NewMemoryStore(workspace string) *MemoryStore {

FILE: pkg/agent/mock_provider_test.go
  type mockProvider (line 9) | type mockProvider struct
    method Chat (line 11) | func (m *mockProvider) Chat(
    method GetDefaultModel (line 24) | func (m *mockProvider) GetDefaultModel() string {

FILE: pkg/agent/model_resolution.go
  function buildModelListResolver (line 11) | func buildModelListResolver(cfg *config.Config) func(raw string) (string...
  function resolveModelCandidates (line 51) | func resolveModelCandidates(
  function resolvedCandidateModel (line 67) | func resolvedCandidateModel(candidates []providers.FallbackCandidate, fa...
  function resolvedCandidateProvider (line 74) | func resolvedCandidateProvider(candidates []providers.FallbackCandidate,...
  function resolvedModelConfig (line 81) | func resolvedModelConfig(cfg *config.Config, modelName, workspace string...

FILE: pkg/agent/registry.go
  type AgentRegistry (line 14) | type AgentRegistry struct
    method GetAgent (line 59) | func (r *AgentRegistry) GetAgent(agentID string) (*AgentInstance, bool) {
    method ResolveRoute (line 68) | func (r *AgentRegistry) ResolveRoute(input routing.RouteInput) routing...
    method ListAgentIDs (line 73) | func (r *AgentRegistry) ListAgentIDs() []string {
    method CanSpawnSubagent (line 84) | func (r *AgentRegistry) CanSpawnSubagent(parentAgentID, targetAgentID ...
    method ForEachTool (line 107) | func (r *AgentRegistry) ForEachTool(name string, fn func(tools.Tool)) {
    method Close (line 118) | func (r *AgentRegistry) Close() {
    method GetDefaultAgent (line 130) | func (r *AgentRegistry) GetDefaultAgent() *AgentInstance {
  function NewAgentRegistry (line 21) | func NewAgentRegistry(

FILE: pkg/agent/registry_test.go
  type mockRegistryProvider (line 11) | type mockRegistryProvider struct
    method Chat (line 13) | func (m *mockRegistryProvider) Chat(
    method GetDefaultModel (line 23) | func (m *mockRegistryProvider) GetDefaultModel() string {
  function testCfg (line 27) | func testCfg(agents []config.AgentConfig) *config.Config {
  function TestNewAgentRegistry_ImplicitMain (line 41) | func TestNewAgentRegistry_ImplicitMain(t *testing.T) {
  function TestNewAgentRegistry_ExplicitAgents (line 59) | func TestNewAgentRegistry_ExplicitAgents(t *testing.T) {
  function TestAgentRegistry_GetAgent_Normalize (line 85) | func TestAgentRegistry_GetAgent_Normalize(t *testing.T) {
  function TestAgentRegistry_GetDefaultAgent (line 100) | func TestAgentRegistry_GetDefaultAgent(t *testing.T) {
  function TestAgentRegistry_CanSpawnSubagent (line 114) | func TestAgentRegistry_CanSpawnSubagent(t *testing.T) {
  function TestAgentRegistry_CanSpawnSubagent_Wildcard (line 143) | func TestAgentRegistry_CanSpawnSubagent_Wildcard(t *testing.T) {
  function TestAgentInstance_Model (line 164) | func TestAgentInstance_Model(t *testing.T) {
  function TestAgentInstance_FallbackInheritance (line 177) | func TestAgentInstance_FallbackInheritance(t *testing.T) {
  function TestAgentInstance_FallbackExplicitEmpty (line 190) | func TestAgentInstance_FallbackExplicitEmpty(t *testing.T) {

FILE: pkg/agent/thinking.go
  type ThinkingLevel (line 10) | type ThinkingLevel
  constant ThinkingOff (line 13) | ThinkingOff      ThinkingLevel = "off"
  constant ThinkingLow (line 14) | ThinkingLow      ThinkingLevel = "low"
  constant ThinkingMedium (line 15) | ThinkingMedium   ThinkingLevel = "medium"
  constant ThinkingHigh (line 16) | ThinkingHigh     ThinkingLevel = "high"
  constant ThinkingXHigh (line 17) | ThinkingXHigh    ThinkingLevel = "xhigh"
  constant ThinkingAdaptive (line 18) | ThinkingAdaptive ThinkingLevel = "adaptive"
  function parseThinkingLevel (line 24) | func parseThinkingLevel(level string) ThinkingLevel {

FILE: pkg/agent/thinking_test.go
  function TestParseThinkingLevel (line 5) | func TestParseThinkingLevel(t *testing.T) {

FILE: pkg/auth/anthropic_usage.go
  constant anthropicBetaHeader (line 12) | anthropicBetaHeader = "oauth-2025-04-20"
  constant anthropicAPIVersion (line 13) | anthropicAPIVersion = "2023-06-01"
  function setAnthropicUsageURL (line 20) | func setAnthropicUsageURL(url string) { anthropicUsageURL = url }
  type AnthropicUsage (line 22) | type AnthropicUsage struct
  function FetchAnthropicUsage (line 27) | func FetchAnthropicUsage(token string) (*AnthropicUsage, error) {

FILE: pkg/auth/anthropic_usage_test.go
  function TestFetchAnthropicUsage_Success (line 10) | func TestFetchAnthropicUsage_Success(t *testing.T) {
  function TestFetchAnthropicUsage_Forbidden (line 40) | func TestFetchAnthropicUsage_Forbidden(t *testing.T) {
  function TestFetchAnthropicUsage_ServerError (line 60) | func TestFetchAnthropicUsage_ServerError(t *testing.T) {
  function TestFetchAnthropicUsage_MalformedJSON (line 80) | func TestFetchAnthropicUsage_MalformedJSON(t *testing.T) {

FILE: pkg/auth/oauth.go
  type OAuthProviderConfig (line 23) | type OAuthProviderConfig struct
  function OpenAIOAuthConfig (line 33) | func OpenAIOAuthConfig() OAuthProviderConfig {
  function GoogleAntigravityOAuthConfig (line 45) | func GoogleAntigravityOAuthConfig() OAuthProviderConfig {
  function decodeBase64 (line 61) | func decodeBase64(s string) string {
  function GenerateState (line 70) | func GenerateState() (string, error) {
  function LoginBrowser (line 78) | func LoginBrowser(cfg OAuthProviderConfig) (*AuthCredential, error) {
  type callbackResult (line 179) | type callbackResult struct
  type deviceCodeResponse (line 184) | type deviceCodeResponse struct
  type DeviceCodeInfo (line 191) | type DeviceCodeInfo struct
  function RequestDeviceCode (line 200) | func RequestDeviceCode(cfg OAuthProviderConfig) (*DeviceCodeInfo, error) {
  function PollDeviceCodeOnce (line 242) | func PollDeviceCodeOnce(cfg OAuthProviderConfig, deviceAuthID, userCode ...
  function parseDeviceCodeResponse (line 246) | func parseDeviceCodeResponse(body []byte) (deviceCodeResponse, error) {
  function parseFlexibleInt (line 269) | func parseFlexibleInt(raw json.RawMessage) (int, error) {
  function LoginDeviceCode (line 291) | func LoginDeviceCode(cfg OAuthProviderConfig) (*AuthCredential, error) {
  function pollDeviceCode (line 349) | func pollDeviceCode(cfg OAuthProviderConfig, deviceAuthID, userCode stri...
  function RefreshAccessToken (line 387) | func RefreshAccessToken(cred *AuthCredential, cfg OAuthProviderConfig) (...
  function BuildAuthorizeURL (line 440) | func BuildAuthorizeURL(cfg OAuthProviderConfig, pkce PKCECodes, state, r...
  function buildAuthorizeURL (line 444) | func buildAuthorizeURL(cfg OAuthProviderConfig, pkce PKCECodes, state, r...
  function ExchangeCodeForTokens (line 480) | func ExchangeCodeForTokens(cfg OAuthProviderConfig, code, codeVerifier, ...
  function parseTokenResponse (line 520) | func parseTokenResponse(body []byte, provider string) (*AuthCredential, ...
  function extractAccountID (line 560) | func extractAccountID(token string) string {
  function parseJWTClaims (line 593) | func parseJWTClaims(token string) (map[string]any, error) {
  function base64URLDecode (line 620) | func base64URLDecode(s string) ([]byte, error) {
  function OpenBrowser (line 626) | func OpenBrowser(url string) error {

FILE: pkg/auth/oauth_test.go
  function makeJWTForClaims (line 13) | func makeJWTForClaims(t *testing.T, claims map[string]any) string {
  function TestBuildAuthorizeURL (line 25) | func TestBuildAuthorizeURL(t *testing.T) {
  function TestBuildAuthorizeURLOpenAIExtras (line 69) | func TestBuildAuthorizeURLOpenAIExtras(t *testing.T) {
  function TestParseTokenResponse (line 91) | func TestParseTokenResponse(t *testing.T) {
  function TestParseTokenResponseExtractsAccountIDFromIDToken (line 122) | func TestParseTokenResponseExtractsAccountIDFromIDToken(t *testing.T) {
  function TestExtractAccountIDFromOrganizationsFallback (line 141) | func TestExtractAccountIDFromOrganizationsFallback(t *testing.T) {
  function TestParseTokenResponseNoAccessToken (line 153) | func TestParseTokenResponseNoAccessToken(t *testing.T) {
  function TestParseTokenResponseAccountIDFromIDToken (line 161) | func TestParseTokenResponseAccountIDFromIDToken(t *testing.T) {
  function makeJWTWithAccountID (line 181) | func makeJWTWithAccountID(accountID string) string {
  function TestExchangeCodeForTokens (line 189) | func TestExchangeCodeForTokens(t *testing.T) {
  function TestRefreshAccessToken (line 232) | func TestRefreshAccessToken(t *testing.T) {
  function TestRefreshAccessTokenNoRefreshToken (line 279) | func TestRefreshAccessTokenNoRefreshToken(t *testing.T) {
  function TestRefreshAccessTokenPreservesRefreshAndAccountID (line 293) | func TestRefreshAccessTokenPreservesRefreshAndAccountID(t *testing.T) {
  function TestOpenAIOAuthConfig (line 324) | func TestOpenAIOAuthConfig(t *testing.T) {
  function TestParseDeviceCodeResponseIntervalAsNumber (line 337) | func TestParseDeviceCodeResponseIntervalAsNumber(t *testing.T) {
  function TestParseDeviceCodeResponseIntervalAsString (line 356) | func TestParseDeviceCodeResponseIntervalAsString(t *testing.T) {
  function TestParseDeviceCodeResponseInvalidInterval (line 369) | func TestParseDeviceCodeResponseInvalidInterval(t *testing.T) {

FILE: pkg/auth/pkce.go
  type PKCECodes (line 9) | type PKCECodes struct
  function GeneratePKCE (line 14) | func GeneratePKCE() (PKCECodes, error) {

FILE: pkg/auth/pkce_test.go
  function TestGeneratePKCE (line 9) | func TestGeneratePKCE(t *testing.T) {
  function TestGeneratePKCEUniqueness (line 37) | func TestGeneratePKCEUniqueness(t *testing.T) {

FILE: pkg/auth/store.go
  type AuthCredential (line 13) | type AuthCredential struct
    method IsExpired (line 28) | func (c *AuthCredential) IsExpired() bool {
    method NeedsRefresh (line 35) | func (c *AuthCredential) NeedsRefresh() bool {
  type AuthStore (line 24) | type AuthStore struct
  function authFilePath (line 42) | func authFilePath() string {
  function LoadStore (line 50) | func LoadStore() (*AuthStore, error) {
  function SaveStore (line 70) | func SaveStore(store *AuthStore) error {
  function GetCredential (line 81) | func GetCredential(provider string) (*AuthCredential, error) {
  function SetCredential (line 93) | func SetCredential(provider string, cred *AuthCredential) error {
  function DeleteCredential (line 102) | func DeleteCredential(provider string) error {
  function DeleteAllCredentials (line 111) | func DeleteAllCredentials() error {

FILE: pkg/auth/store_test.go
  function TestAuthCredentialIsExpired (line 10) | func TestAuthCredentialIsExpired(t *testing.T) {
  function TestAuthCredentialNeedsRefresh (line 31) | func TestAuthCredentialNeedsRefresh(t *testing.T) {
  function TestStoreRoundtrip (line 53) | func TestStoreRoundtrip(t *testing.T) {
  function TestStoreFilePermissions (line 90) | func TestStoreFilePermissions(t *testing.T) {
  function TestStoreMultiProvider (line 116) | func TestStoreMultiProvider(t *testing.T) {
  function TestDeleteCredential (line 149) | func TestDeleteCredential(t *testing.T) {
  function TestLoadStoreEmpty (line 173) | func TestLoadStoreEmpty(t *testing.T) {

FILE: pkg/auth/token.go
  function LoginPasteToken (line 10) | func LoginPasteToken(provider string, r io.Reader) (*AuthCredential, err...
  function LoginSetupToken (line 34) | func LoginSetupToken(r io.Reader) (*AuthCredential, error) {
  function providerDisplayName (line 63) | func providerDisplayName(provider string) string {

FILE: pkg/auth/token_test.go
  function TestLoginSetupToken (line 8) | func TestLoginSetupToken(t *testing.T) {
  function TestLoginSetupToken_EmptyReader (line 55) | func TestLoginSetupToken_EmptyReader(t *testing.T) {

FILE: pkg/bus/bus.go
  constant defaultBusBufferSize (line 15) | defaultBusBufferSize = 64
  type MessageBus (line 17) | type MessageBus struct
    method PublishInbound (line 65) | func (mb *MessageBus) PublishInbound(ctx context.Context, msg InboundM...
    method InboundChan (line 69) | func (mb *MessageBus) InboundChan() <-chan InboundMessage {
    method PublishOutbound (line 73) | func (mb *MessageBus) PublishOutbound(ctx context.Context, msg Outboun...
    method OutboundChan (line 77) | func (mb *MessageBus) OutboundChan() <-chan OutboundMessage {
    method PublishOutboundMedia (line 81) | func (mb *MessageBus) PublishOutboundMedia(ctx context.Context, msg Ou...
    method OutboundMediaChan (line 85) | func (mb *MessageBus) OutboundMediaChan() <-chan OutboundMediaMessage {
    method Close (line 89) | func (mb *MessageBus) Close() {
  function NewMessageBus (line 28) | func NewMessageBus() *MessageBus {
  function publish (line 37) | func publish[T any](ctx context.Context, mb *MessageBus, ch chan T, msg ...

FILE: pkg/bus/bus_test.go
  function TestPublishConsume (line 10) | func TestPublishConsume(t *testing.T) {
  function TestPublishOutboundSubscribe (line 39) | func TestPublishOutboundSubscribe(t *testing.T) {
  function TestPublishInbound_ContextCancel (line 64) | func TestPublishInbound_ContextCancel(t *testing.T) {
  function TestPublishInbound_BusClosed (line 89) | func TestPublishInbound_BusClosed(t *testing.T) {
  function TestPublishOutbound_BusClosed (line 99) | func TestPublishOutbound_BusClosed(t *testing.T) {
  function TestConsumeInbound_ContextCancel (line 109) | func TestConsumeInbound_ContextCancel(t *testing.T) {
  function TestConsumeInbound_BusClosed (line 138) | func TestConsumeInbound_BusClosed(t *testing.T) {
  function TestSubscribeOutbound_BusClosed (line 156) | func TestSubscribeOutbound_BusClosed(t *testing.T) {
  function TestConcurrentPublishClose (line 166) | func TestConcurrentPublishClose(t *testing.T) {
  function TestPublishInbound_FullBuffer (line 208) | func TestPublishInbound_FullBuffer(t *testing.T) {
  function TestCloseIdempotent (line 234) | func TestCloseIdempotent(t *testing.T) {

FILE: pkg/bus/types.go
  type Peer (line 4) | type Peer struct
  type SenderInfo (line 10) | type SenderInfo struct
  type InboundMessage (line 18) | type InboundMessage struct
  type OutboundMessage (line 32) | type OutboundMessage struct
  type MediaPart (line 40) | type MediaPart struct
  type OutboundMediaMessage (line 49) | type OutboundMediaMessage struct

FILE: pkg/channels/base.go
  function init (line 26) | func init() {
  function uniqueID (line 42) | func uniqueID() string {
  type Channel (line 47) | type Channel interface
  type BaseChannelOption (line 59) | type BaseChannelOption
  function WithMaxMessageLength (line 64) | func WithMaxMessageLength(n int) BaseChannelOption {
  function WithGroupTrigger (line 69) | func WithGroupTrigger(gt config.GroupTriggerConfig) BaseChannelOption {
  function WithReasoningChannelID (line 74) | func WithReasoningChannelID(id string) BaseChannelOption {
  type MessageLengthProvider (line 81) | type MessageLengthProvider interface
  type BaseChannel (line 85) | type BaseChannel struct
    method MaxMessageLength (line 120) | func (c *BaseChannel) MaxMessageLength() int {
    method ShouldRespondInGroup (line 136) | func (c *BaseChannel) ShouldRespondInGroup(isMentioned bool, content s...
    method Name (line 164) | func (c *BaseChannel) Name() string {
    method ReasoningChannelID (line 168) | func (c *BaseChannel) ReasoningChannelID() string {
    method IsRunning (line 172) | func (c *BaseChannel) IsRunning() bool {
    method IsAllowed (line 176) | func (c *BaseChannel) IsAllowed(senderID string) bool {
    method IsAllowedSender (line 218) | func (c *BaseChannel) IsAllowedSender(sender bus.SenderInfo) bool {
    method HandleMessage (line 232) | func (c *BaseChannel) HandleMessage(
    method SetRunning (line 313) | func (c *BaseChannel) SetRunning(running bool) {
    method SetMediaStore (line 318) | func (c *BaseChannel) SetMediaStore(s media.MediaStore) { c.mediaStore...
    method GetMediaStore (line 321) | func (c *BaseChannel) GetMediaStore() media.MediaStore { return c.medi...
    method SetPlaceholderRecorder (line 324) | func (c *BaseChannel) SetPlaceholderRecorder(r PlaceholderRecorder) {
    method GetPlaceholderRecorder (line 329) | func (c *BaseChannel) GetPlaceholderRecorder() PlaceholderRecorder {
    method SetOwner (line 335) | func (c *BaseChannel) SetOwner(ch Channel) {
  function NewBaseChannel (line 99) | func NewBaseChannel(
  function BuildMediaScope (line 340) | func BuildMediaScope(channel, chatID, messageID string) string {

FILE: pkg/channels/base_test.go
  function TestBaseChannelIsAllowed (line 10) | func TestBaseChannelIsAllowed(t *testing.T) {
  function TestShouldRespondInGroup (line 59) | func TestShouldRespondInGroup(t *testing.T) {
  function TestIsAllowedSender (line 180) | func TestIsAllowedSender(t *testing.T) {

FILE: pkg/channels/dingtalk/dingtalk.go
  type DingTalkChannel (line 25) | type DingTalkChannel struct
    method Start (line 61) | func (c *DingTalkChannel) Start(ctx context.Context) error {
    method Stop (line 89) | func (c *DingTalkChannel) Stop(ctx context.Context) error {
    method Send (line 106) | func (c *DingTalkChannel) Send(ctx context.Context, msg bus.OutboundMe...
    method onChatBotMessageReceived (line 134) | func (c *DingTalkChannel) onChatBotMessageReceived(
    method SendDirectReply (line 212) | func (c *DingTalkChannel) SendDirectReply(ctx context.Context, session...
  function NewDingTalkChannel (line 38) | func NewDingTalkChannel(cfg config.DingTalkConfig, messageBus *bus.Messa...

FILE: pkg/channels/dingtalk/init.go
  function init (line 9) | func init() {

FILE: pkg/channels/discord/discord.go
  constant sendTimeout (line 27) | sendTimeout = 10 * time.Second
  type DiscordChannel (line 36) | type DiscordChannel struct
    method Start (line 79) | func (c *DiscordChannel) Start(ctx context.Context) error {
    method Stop (line 107) | func (c *DiscordChannel) Stop(ctx context.Context) error {
    method Send (line 131) | func (c *DiscordChannel) Send(ctx context.Context, msg bus.OutboundMes...
    method SendMedia (line 149) | func (c *DiscordChannel) SendMedia(ctx context.Context, msg bus.Outbou...
    method EditMessage (line 244) | func (c *DiscordChannel) EditMessage(ctx context.Context, chatID strin...
    method SendPlaceholder (line 252) | func (c *DiscordChannel) SendPlaceholder(ctx context.Context, chatID s...
    method sendChunk (line 270) | func (c *DiscordChannel) sendChunk(ctx context.Context, channelID, con...
    method handleMessage (line 315) | func (c *DiscordChannel) handleMessage(s *discordgo.Session, m *discor...
    method startTyping (line 468) | func (c *DiscordChannel) startTyping(chatID string) {
    method stopTyping (line 503) | func (c *DiscordChannel) stopTyping(chatID string) {
    method StartTyping (line 514) | func (c *DiscordChannel) StartTyping(ctx context.Context, chatID strin...
    method downloadAttachment (line 519) | func (c *DiscordChannel) downloadAttachment(url, filename string) stri...
    method resolveDiscordRefs (line 562) | func (c *DiscordChannel) resolveDiscordRefs(s *discordgo.Session, text...
    method stripBotMention (line 606) | func (c *DiscordChannel) stripBotMention(text string) string {
  function NewDiscordChannel (line 47) | func NewDiscordChannel(cfg config.DiscordConfig, bus *bus.MessageBus) (*...
  function appendContent (line 308) | func appendContent(content, suffix string) string {
  function applyDiscordProxy (line 526) | func applyDiscordProxy(session *discordgo.Session, proxyAddr string) err...

FILE: pkg/channels/discord/discord_resolve_test.go
  function TestChannelRefRegex (line 7) | func TestChannelRefRegex(t *testing.T) {
  function TestMsgLinkRegex (line 37) | func TestMsgLinkRegex(t *testing.T) {
  function TestMsgLinkRegex_MultipleMatches (line 88) | func TestMsgLinkRegex_MultipleMatches(t *testing.T) {

FILE: pkg/channels/discord/discord_test.go
  function TestApplyDiscordProxy_CustomProxy (line 11) | func TestApplyDiscordProxy_CustomProxy(t *testing.T) {
  function TestApplyDiscordProxy_FromEnvironment (line 44) | func TestApplyDiscordProxy_FromEnvironment(t *testing.T) {
  function TestApplyDiscordProxy_InvalidProxyURL (line 82) | func TestApplyDiscordProxy_InvalidProxyURL(t *testing.T) {

FILE: pkg/channels/discord/init.go
  function init (line 9) | func init() {

FILE: pkg/channels/errors_test.go
  function TestErrorsIs (line 9) | func TestErrorsIs(t *testing.T) {
  function TestErrorsIsAllTypes (line 19) | func TestErrorsIsAllTypes(t *testing.T) {
  function TestErrorMessages (line 40) | func TestErrorMessages(t *testing.T) {

FILE: pkg/channels/errutil.go
  function ClassifySendError (line 11) | func ClassifySendError(statusCode int, rawErr error) error {
  function ClassifyNetError (line 25) | func ClassifyNetError(err error) error {

FILE: pkg/channels/errutil_test.go
  function TestClassifySendError (line 9) | func TestClassifySendError(t *testing.T) {
  function TestClassifySendErrorNoFalsePositive (line 49) | func TestClassifySendErrorNoFalsePositive(t *testing.T) {
  function TestClassifyNetError (line 80) | func TestClassifyNetError(t *testing.T) {

FILE: pkg/channels/feishu/common.go
  function stringValue (line 15) | func stringValue(v *string) string {
  function buildMarkdownCard (line 24) | func buildMarkdownCard(content string) (string, error) {
  function extractJSONStringField (line 45) | func extractJSONStringField(content, field string) string {
  function extractImageKey (line 63) | func extractImageKey(content string) string { return extractJSONStringFi...
  function extractFileKey (line 67) | func extractFileKey(content string) string { return extractJSONStringFie...
  function extractFileName (line 70) | func extractFileName(content string) string { return extractJSONStringFi...
  function stripMentionPlaceholders (line 74) | func stripMentionPlaceholders(content string, mentions []*larkim.Mention...
  function extractCardImageKeys (line 91) | func extractCardImageKeys(rawContent string) (feishuKeys []string, exter...
  function isExternalURL (line 106) | func isExternalURL(s string) bool {
  function extractImageKeysRecursive (line 112) | func extractImageKeysRecursive(v any, feishuKeys, externalURLs *[]string) {

FILE: pkg/channels/feishu/common_test.go
  function TestExtractJSONStringField (line 10) | func TestExtractJSONStringField(t *testing.T) {
  function TestExtractImageKey (line 71) | func TestExtractImageKey(t *testing.T) {
  function TestExtractFileKey (line 104) | func TestExtractFileKey(t *testing.T) {
  function TestExtractFileName (line 137) | func TestExtractFileName(t *testing.T) {
  function TestBuildMarkdownCard (line 170) | func TestBuildMarkdownCard(t *testing.T) {
  function TestStripMentionPlaceholders (line 230) | func TestStripMentionPlaceholders(t *testing.T) {
  function TestExtractCardImageKeys (line 294) | func TestExtractCardImageKeys(t *testing.T) {

FILE: pkg/channels/feishu/feishu_32.go
  type FeishuChannel (line 15) | type FeishuChannel struct
    method Start (line 29) | func (c *FeishuChannel) Start(ctx context.Context) error {
    method Stop (line 34) | func (c *FeishuChannel) Stop(ctx context.Context) error {
    method Send (line 39) | func (c *FeishuChannel) Send(ctx context.Context, msg bus.OutboundMess...
    method EditMessage (line 44) | func (c *FeishuChannel) EditMessage(ctx context.Context, chatID, messa...
    method SendPlaceholder (line 49) | func (c *FeishuChannel) SendPlaceholder(ctx context.Context, chatID st...
    method ReactToMessage (line 54) | func (c *FeishuChannel) ReactToMessage(ctx context.Context, chatID, me...
    method SendMedia (line 59) | func (c *FeishuChannel) SendMedia(ctx context.Context, msg bus.Outboun...
  function NewFeishuChannel (line 22) | func NewFeishuChannel(cfg config.FeishuConfig, bus *bus.MessageBus) (*Fe...

FILE: pkg/channels/feishu/feishu_64.go
  constant errCodeTenantTokenInvalid (line 36) | errCodeTenantTokenInvalid = 99991663
  type FeishuChannel (line 38) | type FeishuChannel struct
    method Start (line 72) | func (c *FeishuChannel) Start(ctx context.Context) error {
    method Stop (line 118) | func (c *FeishuChannel) Stop(ctx context.Context) error {
    method Send (line 134) | func (c *FeishuChannel) Send(ctx context.Context, msg bus.OutboundMess...
    method EditMessage (line 182) | func (c *FeishuChannel) EditMessage(ctx context.Context, chatID, messa...
    method SendPlaceholder (line 206) | func (c *FeishuChannel) SendPlaceholder(ctx context.Context, chatID st...
    method ReactToMessage (line 250) | func (c *FeishuChannel) ReactToMessage(ctx context.Context, chatID, me...
    method SendMedia (line 313) | func (c *FeishuChannel) SendMedia(ctx context.Context, msg bus.Outboun...
    method sendMediaPart (line 337) | func (c *FeishuChannel) sendMediaPart(
    method handleMessageReceive (line 385) | func (c *FeishuChannel) handleMessageReceive(ctx context.Context, even...
    method fetchBotOpenID (line 494) | func (c *FeishuChannel) fetchBotOpenID(ctx context.Context) error {
    method isBotMentioned (line 529) | func (c *FeishuChannel) isBotMentioned(message *larkim.EventMessage) b...
    method downloadInboundMedia (line 593) | func (c *FeishuChannel) downloadInboundMedia(
    method downloadResource (line 651) | func (c *FeishuChannel) downloadResource(
    method sendCard (line 777) | func (c *FeishuChannel) sendCard(ctx context.Context, chatID, cardCont...
    method sendText (line 805) | func (c *FeishuChannel) sendText(ctx context.Context, chatID, text str...
    method sendImage (line 834) | func (c *FeishuChannel) sendImage(ctx context.Context, chatID string, ...
    method sendFile (line 880) | func (c *FeishuChannel) sendFile(ctx context.Context, chatID string, f...
    method invalidateTokenOnAuthError (line 957) | func (c *FeishuChannel) invalidateTokenOnAuthError(code int) {
  function NewFeishuChannel (line 51) | func NewFeishuChannel(cfg config.FeishuConfig, bus *bus.MessageBus) (*Fe...
  function extractContent (line 552) | func extractContent(messageType, rawContent string) string {
  function appendMediaTags (line 746) | func appendMediaTags(content, messageType string, mediaRefs []string) st...
  function extractFeishuSenderID (line 935) | func extractFeishuSenderID(sender *larkim.EventSender) string {

FILE: pkg/channels/feishu/feishu_64_test.go
  function TestExtractContent (line 11) | func TestExtractContent(t *testing.T) {
  function TestAppendMediaTags (line 108) | func TestAppendMediaTags(t *testing.T) {
  function TestExtractFeishuSenderID (line 198) | func TestExtractFeishuSenderID(t *testing.T) {

FILE: pkg/channels/feishu/init.go
  function init (line 9) | func init() {

FILE: pkg/channels/feishu/token_cache.go
  type tokenCache (line 12) | type tokenCache struct
    method Set (line 26) | func (c *tokenCache) Set(_ context.Context, key, value string, ttl tim...
    method Get (line 33) | func (c *tokenCache) Get(_ context.Context, key string) (string, error) {
    method InvalidateAll (line 48) | func (c *tokenCache) InvalidateAll() {
  type tokenEntry (line 17) | type tokenEntry struct
  function newTokenCache (line 22) | func newTokenCache() *tokenCache {

FILE: pkg/channels/interfaces.go
  type TypingCapable (line 12) | type TypingCapable interface
  type MessageEditor (line 18) | type MessageEditor interface
  type ReactionCapable (line 25) | type ReactionCapable interface
  type PlaceholderCapable (line 34) | type PlaceholderCapable interface
  type PlaceholderRecorder (line 41) | type PlaceholderRecorder interface
  type CommandRegistrarCapable (line 50) | type CommandRegistrarCapable interface

FILE: pkg/channels/interfaces_command_test.go
  type mockRegistrar (line 10) | type mockRegistrar struct
    method RegisterCommands (line 12) | func (mockRegistrar) RegisterCommands(context.Context, []commands.Defi...
  function TestCommandRegistrarCapable_Compiles (line 14) | func TestCommandRegistrarCapable_Compiles(t *testing.T) {

FILE: pkg/channels/irc/handler.go
  method onConnect (line 18) | func (c *IRCChannel) onConnect(conn *ircevent.Connection) {
  method onPrivmsg (line 34) | func (c *IRCChannel) onPrivmsg(conn *ircevent.Connection, e ircmsg.Messa...
  function nickMentionedAt (line 109) | func nickMentionedAt(content, botNick string) int {
  function isBotMentioned (line 135) | func isBotMentioned(content, botNick string) bool {
  function stripBotMention (line 140) | func stripBotMention(content, botNick string) string {

FILE: pkg/channels/irc/init.go
  function init (line 9) | func init() {

FILE: pkg/channels/irc/irc.go
  type IRCChannel (line 19) | type IRCChannel struct
    method Start (line 49) | func (c *IRCChannel) Start(ctx context.Context) error {
    method Stop (line 117) | func (c *IRCChannel) Stop(ctx context.Context) error {
    method Send (line 133) | func (c *IRCChannel) Send(ctx context.Context, msg bus.OutboundMessage...
    method StartTyping (line 166) | func (c *IRCChannel) StartTyping(ctx context.Context, chatID string) (...
  function NewIRCChannel (line 28) | func NewIRCChannel(cfg config.IRCConfig, messageBus *bus.MessageBus) (*I...
  function extractHost (line 188) | func extractHost(server string) string {

FILE: pkg/channels/irc/irc_test.go
  function TestNewIRCChannel (line 10) | func TestNewIRCChannel(t *testing.T) {
  function TestExtractHost (line 48) | func TestExtractHost(t *testing.T) {
  function TestNickMentionedAt (line 69) | func TestNickMentionedAt(t *testing.T) {
  function TestIsBotMentioned (line 96) | func TestIsBotMentioned(t *testing.T) {
  function TestStripBotMention (line 123) | func TestStripBotMention(t *testing.T) {

FILE: pkg/channels/line/init.go
  function init (line 9) | func init() {

FILE: pkg/channels/line/line.go
  constant lineAPIBase (line 27) | lineAPIBase          = "https://api.line.me/v2/bot"
  constant lineDataAPIBase (line 28) | lineDataAPIBase      = "https://api-data.line.me/v2/bot"
  constant lineReplyEndpoint (line 29) | lineReplyEndpoint    = lineAPIBase + "/message/reply"
  constant linePushEndpoint (line 30) | linePushEndpoint     = lineAPIBase + "/message/push"
  constant lineContentEndpoint (line 31) | lineContentEndpoint  = lineDataAPIBase + "/message/%s/content"
  constant lineBotInfoEndpoint (line 32) | lineBotInfoEndpoint  = lineAPIBase + "/info"
  constant lineLoadingEndpoint (line 33) | lineLoadingEndpoint  = lineAPIBase + "/chat/loading/start"
  constant lineReplyTokenMaxAge (line 34) | lineReplyTokenMaxAge = 25 * time.Second
  constant maxWebhookBodySize (line 38) | maxWebhookBodySize = 1 << 20
  type replyTokenEntry (line 41) | type replyTokenEntry struct
  type LINEChannel (line 49) | type LINEChannel struct
    method Start (line 84) | func (c *LINEChannel) Start(ctx context.Context) error {
    method fetchBotInfo (line 108) | func (c *LINEChannel) fetchBotInfo() error {
    method Stop (line 141) | func (c *LINEChannel) Stop(ctx context.Context) error {
    method WebhookPath (line 154) | func (c *LINEChannel) WebhookPath() string {
    method ServeHTTP (line 162) | func (c *LINEChannel) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    method webhookHandler (line 167) | func (c *LINEChannel) webhookHandler(w http.ResponseWriter, r *http.Re...
    method verifySignature (line 214) | func (c *LINEChannel) verifySignature(body []byte, signature string) b...
    method processEvent (line 262) | func (c *LINEChannel) processEvent(event lineEvent) {
    method isBotMentioned (line 401) | func (c *LINEChannel) isBotMentioned(msg lineMessage) bool {
    method stripBotMention (line 440) | func (c *LINEChannel) stripBotMention(text string, msg lineMessage) st...
    method resolveChatID (line 485) | func (c *LINEChannel) resolveChatID(source lineSource) string {
    method Send (line 498) | func (c *LINEChannel) Send(ctx context.Context, msg bus.OutboundMessag...
    method SendMedia (line 532) | func (c *LINEChannel) SendMedia(ctx context.Context, msg bus.OutboundM...
    method sendReply (line 571) | func (c *LINEChannel) sendReply(ctx context.Context, replyToken, conte...
    method sendPush (line 581) | func (c *LINEChannel) sendPush(ctx context.Context, to, content, quote...
    method StartTyping (line 595) | func (c *LINEChannel) StartTyping(ctx context.Context, chatID string) ...
    method sendLoading (line 636) | func (c *LINEChannel) sendLoading(ctx context.Context, chatID string) ...
    method callAPI (line 645) | func (c *LINEChannel) callAPI(ctx context.Context, endpoint string, pa...
    method downloadContent (line 677) | func (c *LINEChannel) downloadContent(messageID, filename string) stri...
  function NewLINEChannel (line 64) | func NewLINEChannel(cfg config.LINEConfig, messageBus *bus.MessageBus) (...
  type lineEvent (line 227) | type lineEvent struct
  type lineSource (line 235) | type lineSource struct
  type lineMessage (line 242) | type lineMessage struct
  type lineMentionee (line 255) | type lineMentionee struct
  function buildTextMessage (line 559) | func buildTextMessage(content, quoteToken string) map[string]string {

FILE: pkg/channels/line/line_test.go
  function TestWebhookRejectsOversizedBody (line 11) | func TestWebhookRejectsOversizedBody(t *testing.T) {
  function TestWebhookAcceptsMaxBodySize (line 25) | func TestWebhookAcceptsMaxBodySize(t *testing.T) {
  function TestWebhookRejectsOversizedBodyBeforeSignatureCheck (line 40) | func TestWebhookRejectsOversizedBodyBeforeSignatureCheck(t *testing.T) {
  function TestWebhookRejectsNonPostMethod (line 55) | func TestWebhookRejectsNonPostMethod(t *testing.T) {
  function TestWebhookRejectsInvalidSignature (line 68) | func TestWebhookRejectsInvalidSignature(t *testing.T) {

FILE: pkg/channels/maixcam/init.go
  function init (line 9) | func init() {

FILE: pkg/channels/maixcam/maixcam.go
  type MaixCamChannel (line 18) | type MaixCamChannel struct
    method Start (line 51) | func (c *MaixCamChannel) Start(ctx context.Context) error {
    method acceptConnections (line 76) | func (c *MaixCamChannel) acceptConnections() {
    method handleConnection (line 108) | func (c *MaixCamChannel) handleConnection(conn net.Conn) {
    method processMessage (line 141) | func (c *MaixCamChannel) processMessage(msg MaixCamMessage, conn net.C...
    method handlePersonDetection (line 156) | func (c *MaixCamChannel) handlePersonDetection(msg MaixCamMessage) {
    method handleStatusUpdate (line 212) | func (c *MaixCamChannel) handleStatusUpdate(msg MaixCamMessage) {
    method Stop (line 218) | func (c *MaixCamChannel) Stop(ctx context.Context) error {
    method Send (line 243) | func (c *MaixCamChannel) Send(ctx context.Context, msg bus.OutboundMes...
  type MaixCamMessage (line 28) | type MaixCamMessage struct
  function NewMaixCamChannel (line 35) | func NewMaixCamChannel(cfg config.MaixCamConfig, bus *bus.MessageBus) (*...

FILE: pkg/channels/manager.go
  constant defaultChannelQueueSize (line 29) | defaultChannelQueueSize = 16
  constant defaultRateLimit (line 30) | defaultRateLimit        = 10
  constant maxRetries (line 31) | maxRetries              = 3
  constant rateLimitDelay (line 32) | rateLimitDelay          = 1 * time.Second
  constant baseBackoff (line 33) | baseBackoff             = 500 * time.Millisecond
  constant maxBackoff (line 34) | maxBackoff              = 8 * time.Second
  constant janitorInterval (line 36) | janitorInterval = 10 * time.Second
  constant typingStopTTL (line 37) | typingStopTTL   = 5 * time.Minute
  constant placeholderTTL (line 38) | placeholderTTL  = 10 * time.Minute
  type typingEntry (line 42) | type typingEntry struct
  type reactionEntry (line 48) | type reactionEntry struct
  type placeholderEntry (line 54) | type placeholderEntry struct
  type channelWorker (line 70) | type channelWorker struct
  type Manager (line 79) | type Manager struct
    method RecordPlaceholder (line 101) | func (m *Manager) RecordPlaceholder(channel, chatID, placeholderID str...
    method SendPlaceholder (line 108) | func (m *Manager) SendPlaceholder(ctx context.Context, channel, chatID...
    method RecordTypingStop (line 129) | func (m *Manager) RecordTypingStop(channel, chatID string, stop func()) {
    method InvokeTypingStop (line 143) | func (m *Manager) InvokeTypingStop(channel, chatID string) {
    method RecordReactionUndo (line 154) | func (m *Manager) RecordReactionUndo(channel, chatID string, undo func...
    method preSend (line 161) | func (m *Manager) preSend(ctx context.Context, name string, msg bus.Ou...
    method initChannel (line 214) | func (m *Manager) initChannel(name, displayName string) {
    method initChannels (line 253) | func (m *Manager) initChannels(channels *config.ChannelsConfig) error {
    method SetupHTTPServer (line 340) | func (m *Manager) SetupHTTPServer(addr string, healthServer *health.Se...
    method StartAll (line 374) | func (m *Manager) StartAll(ctx context.Context) error {
    method StopAll (line 430) | func (m *Manager) StopAll(ctx context.Context) error {
    method runWorker (line 515) | func (m *Manager) runWorker(ctx context.Context, name string, w *chann...
    method sendWithRetry (line 548) | func (m *Manager) sendWithRetry(ctx context.Context, name string, w *c...
    method dispatchOutbound (line 655) | func (m *Manager) dispatchOutbound(ctx context.Context) {
    method dispatchOutboundMedia (line 675) | func (m *Manager) dispatchOutboundMedia(ctx context.Context) {
    method runMediaWorker (line 696) | func (m *Manager) runMediaWorker(ctx context.Context, name string, w *...
    method sendMediaWithRetry (line 713) | func (m *Manager) sendMediaWithRetry(ctx context.Context, name string,...
    method runTTLJanitor (line 775) | func (m *Manager) runTTLJanitor(ctx context.Context) {
    method GetChannel (line 816) | func (m *Manager) GetChannel(name string) (Channel, bool) {
    method GetStatus (line 823) | func (m *Manager) GetStatus() map[string]any {
    method GetEnabledChannels (line 837) | func (m *Manager) GetEnabledChannels() []string {
    method Reload (line 850) | func (m *Manager) Reload(ctx context.Context, cfg *config.Config) error {
    method RegisterChannel (line 910) | func (m *Manager) RegisterChannel(name string, channel Channel) {
    method UnregisterChannel (line 916) | func (m *Manager) UnregisterChannel(name string) {
    method SendMessage (line 933) | func (m *Manager) SendMessage(ctx context.Context, msg bus.OutboundMes...
    method SendToChannel (line 962) | func (m *Manager) SendToChannel(ctx context.Context, channelName, chat...
  type asyncTask (line 95) | type asyncTask struct
  function NewManager (line 193) | func NewManager(cfg *config.Config, messageBus *bus.MessageBus, store me...
  function newChannelWorker (line 496) | func newChannelWorker(name string, ch Channel) *channelWorker {
  function dispatchLoop (line 605) | func dispatchLoop[M any](

FILE: pkg/channels/manager_channel.go
  function toChannelHashes (line 12) | func toChannelHashes(cfg *config.Config) map[string]string {
  function compareChannels (line 32) | func compareChannels(old, news map[string]string) (added, removed []stri...
  function toChannelConfig (line 51) | func toChannelConfig(cfg *config.Config, list []string) (*config.Channel...

FILE: pkg/channels/manager_channel_test.go
  function TestToChannelHashes (line 12) | func TestToChannelHashes(t *testing.T) {

FILE: pkg/channels/manager_test.go
  type mockChannel (line 18) | type mockChannel struct
    method Send (line 27) | func (m *mockChannel) Send(ctx context.Context, msg bus.OutboundMessag...
    method Start (line 32) | func (m *mockChannel) Start(ctx context.Context) error { return nil }
    method Stop (line 33) | func (m *mockChannel) Stop(ctx context.Context) error  { return nil }
    method SendPlaceholder (line 35) | func (m *mockChannel) SendPlaceholder(ctx context.Context, chatID stri...
    method EditMessage (line 41) | func (m *mockChannel) EditMessage(ctx context.Context, chatID, message...
  function newTestManager (line 47) | func newTestManager() *Manager {
  function TestSendWithRetry_Success (line 54) | func TestSendWithRetry_Success(t *testing.T) {
  function TestSendWithRetry_TemporaryThenSuccess (line 78) | func TestSendWithRetry_TemporaryThenSuccess(t *testing.T) {
  function TestSendWithRetry_PermanentFailure (line 105) | func TestSendWithRetry_PermanentFailure(t *testing.T) {
  function TestSendWithRetry_NotRunning (line 129) | func TestSendWithRetry_NotRunning(t *testing.T) {
  function TestSendWithRetry_RateLimitRetry (line 153) | func TestSendWithRetry_RateLimitRetry(t *testing.T) {
  function TestSendWithRetry_MaxRetriesExhausted (line 186) | func TestSendWithRetry_MaxRetriesExhausted(t *testing.T) {
  function TestSendWithRetry_UnknownError (line 211) | func TestSendWithRetry_UnknownError(t *testing.T) {
  function TestSendWithRetry_ContextCancelled (line 238) | func TestSendWithRetry_ContextCancelled(t *testing.T) {
  function TestWorkerRateLimiter (line 270) | func TestWorkerRateLimiter(t *testing.T) {
  function TestNewChannelWorker_DefaultRate (line 322) | func TestNewChannelWorker_DefaultRate(t *testing.T) {
  function TestNewChannelWorker_ConfiguredRate (line 334) | func TestNewChannelWorker_ConfiguredRate(t *testing.T) {
  function TestRunWorker_MessageSplitting (line 345) | func TestRunWorker_MessageSplitting(t *testing.T) {
  type mockChannelWithLength (line 389) | type mockChannelWithLength struct
    method MaxMessageLength (line 394) | func (m *mockChannelWithLength) MaxMessageLength() int {
  function TestSendWithRetry_ExponentialBackoff (line 398) | func TestSendWithRetry_ExponentialBackoff(t *testing.T) {
  type mockMessageEditor (line 437) | type mockMessageEditor struct
    method EditMessage (line 442) | func (m *mockMessageEditor) EditMessage(ctx context.Context, chatID, m...
  function TestPreSend_PlaceholderEditSuccess (line 446) | func TestPreSend_PlaceholderEditSuccess(t *testing.T) {
  function TestPreSend_PlaceholderEditFails_FallsThrough (line 490) | func TestPreSend_PlaceholderEditFails_FallsThrough(t *testing.T) {
  function TestInvokeTypingStop_CallsRegisteredStop (line 514) | func TestInvokeTypingStop_CallsRegisteredStop(t *testing.T) {
  function TestInvokeTypingStop_NoOpWhenNoEntry (line 529) | func TestInvokeTypingStop_NoOpWhenNoEntry(t *testing.T) {
  function TestInvokeTypingStop_Idempotent (line 535) | func TestInvokeTypingStop_Idempotent(t *testing.T) {
  function TestPreSend_TypingStopCalled (line 551) | func TestPreSend_TypingStopCalled(t *testing.T) {
  function TestPreSend_NoRegisteredState (line 573) | func TestPreSend_NoRegisteredState(t *testing.T) {
  function TestPreSend_TypingAndPlaceholder (line 590) | func TestPreSend_TypingAndPlaceholder(t *testing.T) {
  function TestRecordPlaceholder_ConcurrentSafe (line 626) | func TestRecordPlaceholder_ConcurrentSafe(t *testing.T) {
  function TestRecordTypingStop_ConcurrentSafe (line 641) | func TestRecordTypingStop_ConcurrentSafe(t *testing.T) {
  function TestRecordTypingStop_ReplacesExistingStop (line 656) | func TestRecordTypingStop_ReplacesExistingStop(t *testing.T) {
  function TestSendWithRetry_PreSendEditsPlaceholder (line 687) | func TestSendWithRetry_PreSendEditsPlaceholder(t *testing.T) {
  function TestDispatcherExitsOnCancel (line 720) | func TestDispatcherExitsOnCancel(t *testing.T) {
  function TestDispatcherMediaExitsOnCancel (line 749) | func TestDispatcherMediaExitsOnCancel(t *testing.T) {
  function TestTypingStopJanitorEviction (line 779) | func TestTypingStopJanitorEviction(t *testing.T) {
  function TestPlaceholderJanitorEviction (line 821) | func TestPlaceholderJanitorEviction(t *testing.T) {
  function TestPreSendStillWorksWithWrappedTypes (line 847) | func TestPreSendStillWorksWithWrappedTypes(t *testing.T) {
  function TestLazyWorkerCreation (line 889) | func TestLazyWorkerCreation(t *testing.T) {
  function TestBuildMediaScope_FastIDUniqueness (line 916) | func TestBuildMediaScope_FastIDUniqueness(t *testing.T) {
  function TestBuildMediaScope_WithMessageID (line 940) | func TestBuildMediaScope_WithMessageID(t *testing.T) {
  function TestManager_PlaceholderConsumedByResponse (line 948) | func TestManager_PlaceholderConsumedByResponse(t *testing.T) {
  function TestSendMessage_Synchronous (line 1008) | func TestSendMessage_Synchronous(t *testing.T) {
  function TestSendMessage_UnknownChannel (line 1050) | func TestSendMessage_UnknownChannel(t *testing.T) {
  function TestSendMessage_NoWorker (line 1065) | func TestSendMessage_NoWorker(t *testing.T) {
  function TestSendMessage_WithRetry (line 1086) | func TestSendMessage_WithRetry(t *testing.T) {
  function TestSendMessage_WithSplitting (line 1123) | func TestSendMessage_WithSplitting(t *testing.T) {
  function TestSendMessage_PreservesOrdering (line 1160) | func TestSendMessage_PreservesOrdering(t *testing.T) {
  function TestManager_SendPlaceholder (line 1194) | func TestManager_SendPlaceholder(t *testing.T) {

FILE: pkg/channels/matrix/init.go
  function init (line 9) | func init() {

FILE: pkg/channels/matrix/matrix.go
  constant typingRefreshInterval (line 33) | typingRefreshInterval      = 20 * time.Second
  constant typingServerTTL (line 34) | typingServerTTL            = 30 * time.Second
  constant roomKindCacheTTL (line 35) | roomKindCacheTTL           = 5 * time.Minute
  constant roomKindCacheCleanupPeriod (line 36) | roomKindCacheCleanupPeriod = 1 * time.Minute
  constant roomKindCacheMaxEntries (line 37) | roomKindCacheMaxEntries    = 2048
  type roomKindCacheEntry (line 42) | type roomKindCacheEntry struct
  type roomKindCache (line 48) | type roomKindCache struct
    method get (line 70) | func (c *roomKindCache) get(roomID string, now time.Time) (bool, bool) {
    method set (line 86) | func (c *roomKindCache) set(roomID string, isGroup bool, now time.Time) {
    method cleanupExpired (line 112) | func (c *roomKindCache) cleanupExpired(now time.Time) int {
    method cleanupExpiredLocked (line 118) | func (c *roomKindCache) cleanupExpiredLocked(now time.Time) int {
    method evictOldestLocked (line 129) | func (c *roomKindCache) evictOldestLocked() bool {
  function newRoomKindCache (line 55) | func newRoomKindCache(maxEntries int, ttl time.Duration) *roomKindCache {
  type typingSession (line 150) | type typingSession struct
    method stop (line 161) | func (s *typingSession) stop() {
  function newTypingSession (line 155) | func newTypingSession() *typingSession {
  type MatrixChannel (line 168) | type MatrixChannel struct
    method Start (line 236) | func (c *MatrixChannel) Start(ctx context.Context) error {
    method Stop (line 260) | func (c *MatrixChannel) Stop(ctx context.Context) error {
    method Send (line 279) | func (c *MatrixChannel) Send(ctx context.Context, msg bus.OutboundMess...
    method messageContent (line 301) | func (c *MatrixChannel) messageContent(text string) *event.MessageEven...
    method SendMedia (line 311) | func (c *MatrixChannel) SendMedia(ctx context.Context, msg bus.Outboun...
    method StartTyping (line 424) | func (c *MatrixChannel) StartTyping(ctx context.Context, chatID string...
    method SendPlaceholder (line 463) | func (c *MatrixChannel) SendPlaceholder(ctx context.Context, chatID st...
    method EditMessage (line 490) | func (c *MatrixChannel) EditMessage(ctx context.Context, chatID string...
    method handleMemberEvent (line 506) | func (c *MatrixChannel) handleMemberEvent(ctx context.Context, evt *ev...
    method handleMessageEvent (line 536) | func (c *MatrixChannel) handleMessageEvent(ctx context.Context, evt *e...
    method extractInboundContent (line 645) | func (c *MatrixChannel) extractInboundContent(
    method extractInboundMedia (line 663) | func (c *MatrixChannel) extractInboundMedia(
    method storeMedia (line 693) | func (c *MatrixChannel) storeMedia(localPath string, meta media.MediaM...
    method downloadMedia (line 707) | func (c *MatrixChannel) downloadMedia(
    method isGroupRoom (line 923) | func (c *MatrixChannel) isGroupRoom(ctx context.Context, roomID id.Roo...
    method isBotMentioned (line 950) | func (c *MatrixChannel) isBotMentioned(msgEvt *event.MessageEventConte...
    method typingLoop (line 1037) | func (c *MatrixChannel) typingLoop(ctx context.Context, roomID id.Room...
    method stopTypingSessions (line 1064) | func (c *MatrixChannel) stopTypingSessions(ctx context.Context) {
    method baseContext (line 1080) | func (c *MatrixChannel) baseContext() context.Context {
    method runRoomKindCacheJanitor (line 1087) | func (c *MatrixChannel) runRoomKindCacheJanitor(ctx context.Context) {
    method stripSelfMention (line 1101) | func (c *MatrixChannel) stripSelfMention(text string) string {
  function NewMatrixChannel (line 186) | func NewMatrixChannel(cfg config.MatrixConfig, messageBus *bus.MessageBu...
  function markdownToHTML (line 273) | func markdownToHTML(md string) string {
  function matrixContentType (line 781) | func matrixContentType(msgEvt *event.MessageEventContent) string {
  function matrixMediaURI (line 788) | func matrixMediaURI(msgEvt *event.MessageEventContent) id.ContentURIStri...
  function matrixMediaKind (line 801) | func matrixMediaKind(msgType event.MessageType) string {
  function matrixOutboundMsgType (line 814) | func matrixOutboundMsgType(partType, filename, contentType string) event...
  function matrixOutboundContent (line 848) | func matrixOutboundContent(
  function matrixMediaLabel (line 878) | func matrixMediaLabel(msgEvt *event.MessageEventContent, fallback string...
  function matrixMediaFilename (line 891) | func matrixMediaFilename(label, mediaKind, contentType string) string {
  function matrixMediaExt (line 902) | func matrixMediaExt(filename, contentType, mediaKind string) string {
  function mentionsUserInFormattedBody (line 980) | func mentionsUserInFormattedBody(formattedBody string, userID id.UserID)...
  function decodeMatrixMentionHref (line 1021) | func decodeMatrixMentionHref(v string) string {
  function matrixMediaTempDir (line 1105) | func matrixMediaTempDir() (string, error) {
  function matrixLocalpart (line 1113) | func matrixLocalpart(userID id.UserID) string {
  function localpartMentionRegexp (line 1119) | func localpartMentionRegexp(localpart string) *regexp.Regexp {
  function stripUserMention (line 1132) | func stripUserMention(text string, userID id.UserID) string {
  function stripUserMentionWithRegexp (line 1136) | func stripUserMentionWithRegexp(text string, userID id.UserID, mentionR ...

FILE: pkg/channels/matrix/matrix_test.go
  function TestMatrixLocalpartMentionRegexp (line 21) | func TestMatrixLocalpartMentionRegexp(t *testing.T) {
  function TestStripUserMention (line 44) | func TestStripUserMention(t *testing.T) {
  function TestIsBotMentioned (line 63) | func TestIsBotMentioned(t *testing.T) {
  function TestRoomKindCache_ExpiresEntries (line 131) | func TestRoomKindCache_ExpiresEntries(t *testing.T) {
  function TestRoomKindCache_EvictsOldestWhenFull (line 145) | func TestRoomKindCache_EvictsOldestWhenFull(t *testing.T) {
  function TestMatrixMediaTempDir (line 164) | func TestMatrixMediaTempDir(t *testing.T) {
  function TestMatrixMediaExt (line 182) | func TestMatrixMediaExt(t *testing.T) {
  function TestDownloadMedia_WritesResponseToTempFile (line 203) | func TestDownloadMedia_WritesResponseToTempFile(t *testing.T) {
  function TestExtractInboundContent_ImageNoURLFallback (line 247) | func TestExtractInboundContent_ImageNoURLFallback(t *testing.T) {
  function TestExtractInboundContent_AudioNoURLFallback (line 266) | func TestExtractInboundContent_AudioNoURLFallback(t *testing.T) {
  function TestMatrixOutboundMsgType (line 286) | func TestMatrixOutboundMsgType(t *testing.T) {
  function TestMatrixOutboundContent (line 308) | func TestMatrixOutboundContent(t *testing.T) {
  function TestMarkdownToHTML (line 343) | func TestMarkdownToHTML(t *testing.T) {
  function TestMessageContent (line 366) | func TestMessageContent(t *testing.T) {

FILE: pkg/channels/media.go
  type MediaSender (line 13) | type MediaSender interface

FILE: pkg/channels/onebot/init.go
  function init (line 9) | func init() {

FILE: pkg/channels/onebot/onebot.go
  type OneBotChannel (line 24) | type OneBotChannel struct
    method setMsgEmojiLike (line 116) | func (c *OneBotChannel) setMsgEmojiLike(messageID string, emojiID int,...
    method ReactToMessage (line 135) | func (c *OneBotChannel) ReactToMessage(ctx context.Context, chatID, me...
    method Start (line 148) | func (c *OneBotChannel) Start(ctx context.Context) error {
    method connect (line 182) | func (c *OneBotChannel) connect() error {
    method pinger (line 215) | func (c *OneBotChannel) pinger(conn *websocket.Conn) {
    method fetchSelfID (line 237) | func (c *OneBotChannel) fetchSelfID() {
    method sendAPIRequest (line 283) | func (c *OneBotChannel) sendAPIRequest(action string, params any, time...
    method reconnectLoop (line 339) | func (c *OneBotChannel) reconnectLoop() {
    method Stop (line 366) | func (c *OneBotChannel) Stop(ctx context.Context) error {
    method Send (line 394) | func (c *OneBotChannel) Send(ctx context.Context, msg bus.OutboundMess...
    method SendMedia (line 449) | func (c *OneBotChannel) SendMedia(ctx context.Context, msg bus.Outboun...
    method buildMessageSegments (line 559) | func (c *OneBotChannel) buildMessageSegments(chatID, content string) [...
    method buildSendRequest (line 579) | func (c *OneBotChannel) buildSendRequest(msg bus.OutboundMessage) (str...
    method listen (line 600) | func (c *OneBotChannel) listen() {
    method parseMessageSegments (line 713) | func (c *OneBotChannel) parseMessageSegments(
    method handleRawEvent (line 845) | func (c *OneBotChannel) handleRawEvent(raw *oneBotRawEvent) {
    method handleMetaEvent (line 894) | func (c *OneBotChannel) handleMetaEvent(raw *oneBotRawEvent) {
    method handleNoticeEvent (line 902) | func (c *OneBotChannel) handleNoticeEvent(raw *oneBotRawEvent) {
    method handleMessage (line 919) | func (c *OneBotChannel) handleMessage(raw *oneBotRawEvent) {
    method isDuplicate (line 1077) | func (c *OneBotChannel) isDuplicate(messageID string) bool {
  type oneBotRawEvent (line 42) | type oneBotRawEvent struct
  type BotStatus (line 62) | type BotStatus struct
  function isAPIResponse (line 67) | func isAPIResponse(raw json.RawMessage) bool {
  type oneBotSender (line 82) | type oneBotSender struct
  type oneBotAPIRequest (line 88) | type oneBotAPIRequest struct
  type oneBotMessageSegment (line 94) | type oneBotMessageSegment struct
  function NewOneBotChannel (line 99) | func NewOneBotChannel(cfg config.OneBotConfig, messageBus *bus.MessageBu...
  function parseJSONInt64 (line 677) | func parseJSONInt64(raw json.RawMessage) (int64, error) {
  function parseJSONString (line 694) | func parseJSONString(raw json.RawMessage) string {
  type parseMessageResult (line 706) | type parseMessageResult struct
  function truncate (line 1099) | func truncate(s string, n int) string {

FILE: pkg/channels/pico/init.go
  function init (line 9) | func init() {

FILE: pkg/channels/pico/pico.go
  type picoConn (line 24) | type picoConn struct
    method writeJSON (line 33) | func (pc *picoConn) writeJSON(v any) error {
    method close (line 43) | func (pc *picoConn) close() {
  type PicoChannel (line 51) | type PicoChannel struct
    method Start (line 95) | func (c *PicoChannel) Start(ctx context.Context) error {
    method Stop (line 104) | func (c *PicoChannel) Stop(ctx context.Context) error {
    method WebhookPath (line 126) | func (c *PicoChannel) WebhookPath() string { return "/pico/" }
    method ServeHTTP (line 129) | func (c *PicoChannel) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    method Send (line 141) | func (c *PicoChannel) Send(ctx context.Context, msg bus.OutboundMessag...
    method EditMessage (line 154) | func (c *PicoChannel) EditMessage(ctx context.Context, chatID string, ...
    method StartTyping (line 163) | func (c *PicoChannel) StartTyping(ctx context.Context, chatID string) ...
    method SendPlaceholder (line 177) | func (c *PicoChannel) SendPlaceholder(ctx context.Context, chatID stri...
    method broadcastToSession (line 201) | func (c *PicoChannel) broadcastToSession(chatID string, msg PicoMessag...
    method handleWebSocket (line 232) | func (c *PicoChannel) handleWebSocket(w http.ResponseWriter, r *http.R...
    method authenticate (line 295) | func (c *PicoChannel) authenticate(r *http.Request) bool {
    method matchedSubprotocol (line 326) | func (c *PicoChannel) matchedSubprotocol(r *http.Request) string {
    method readLoop (line 337) | func (c *PicoChannel) readLoop(pc *picoConn) {
    method pingLoop (line 398) | func (c *PicoChannel) pingLoop(pc *picoConn, interval time.Duration) {
    method handleMessage (line 421) | func (c *PicoChannel) handleMessage(pc *picoConn, msg PicoMessage) {
    method handleMessageSend (line 438) | func (c *PicoChannel) handleMessageSend(pc *picoConn, msg PicoMessage) {
  function NewPicoChannel (line 62) | func NewPicoChannel(cfg config.PicoConfig, messageBus *bus.MessageBus) (...
  function truncate (line 481) | func truncate(s string, maxLen int) string {

FILE: pkg/channels/pico/protocol.go
  constant TypeMessageSend (line 8) | TypeMessageSend = "message.send"
  constant TypeMediaSend (line 9) | TypeMediaSend   = "media.send"
  constant TypePing (line 10) | TypePing        = "ping"
  constant TypeMessageCreate (line 13) | TypeMessageCreate = "message.create"
  constant TypeMessageUpdate (line 14) | TypeMessageUpdate = "message.update"
  constant TypeMediaCreate (line 15) | TypeMediaCreate   = "media.create"
  constant TypeTypingStart (line 16) | TypeTypingStart   = "typing.start"
  constant TypeTypingStop (line 17) | TypeTypingStop    = "typing.stop"
  constant TypeError (line 18) | TypeError         = "error"
  constant TypePong (line 19) | TypePong          = "pong"
  type PicoMessage (line 23) | type PicoMessage struct
  function newMessage (line 32) | func newMessage(msgType string, payload map[string]any) PicoMessage {
  function newError (line 41) | func newError(code, message string) PicoMessage {

FILE: pkg/channels/qq/botgo_logger.go
  type botGoLogger (line 12) | type botGoLogger struct
    method Info (line 20) | func (b *botGoLogger) Info(v ...any) {
    method Infof (line 29) | func (b *botGoLogger) Infof(format string, v ...any) {
  function newBotGoLogger (line 16) | func newBotGoLogger(component string) *botGoLogger {
  function shouldDemoteBotGoInfo (line 38) | func shouldDemoteBotGoInfo(message string) bool {

FILE: pkg/channels/qq/init.go
  function init (line 9) | func init() {

FILE: pkg/channels/qq/qq.go
  constant dedupTTL (line 38) | dedupTTL      = 5 * time.Minute
  constant dedupInterval (line 39) | dedupInterval = 60 * time.Second
  constant dedupMaxSize (line 40) | dedupMaxSize  = 10000
  constant typingResend (line 41) | typingResend  = 8 * time.Second
  constant typingSeconds (line 42) | typingSeconds = 10
  constant bytesPerMiB (line 43) | bytesPerMiB   = 1024 * 1024
  type qqAPI (line 46) | type qqAPI interface
  type QQChannel (line 57) | type QQChannel struct
    method Start (line 100) | func (c *QQChannel) Start(ctx context.Context) error {
    method Stop (line 174) | func (c *QQChannel) Stop(ctx context.Context) error {
    method getChatKind (line 191) | func (c *QQChannel) getChatKind(chatID string) string {
    method Send (line 203) | func (c *QQChannel) Send(ctx context.Context, msg bus.OutboundMessage)...
    method StartTyping (line 261) | func (c *QQChannel) StartTyping(ctx context.Context, chatID string) (f...
    method SendMedia (line 322) | func (c *QQChannel) SendMedia(ctx context.Context, msg bus.OutboundMed...
    method uploadMedia (line 363) | func (c *QQChannel) uploadMedia(
    method buildMediaUpload (line 389) | func (c *QQChannel) buildMediaUpload(part bus.MediaPart) (*qqMediaUplo...
    method sendUploadedMedia (line 440) | func (c *QQChannel) sendUploadedMedia(
    method applyPassiveReplyMetadata (line 467) | func (c *QQChannel) applyPassiveReplyMetadata(chatID string, msg *dto....
    method mediaUploadURL (line 483) | func (c *QQChannel) mediaUploadURL(chatKind, chatID string) string {
    method maxBase64FileSizeBytes (line 504) | func (c *QQChannel) maxBase64FileSizeBytes() int64 {
    method handleC2CMessage (line 512) | func (c *QQChannel) handleC2CMessage() event.C2CMessageEventHandler {
    method handleGroupATMessage (line 581) | func (c *QQChannel) handleGroupATMessage() event.GroupATMessageEventHa...
    method extractInboundAttachments (line 658) | func (c *QQChannel) extractInboundAttachments(
    method downloadAttachment (line 702) | func (c *QQChannel) downloadAttachment(urlStr, filename string) string {
    method downloadHeaders (line 716) | func (c *QQChannel) downloadHeaders() map[string]string {
    method isDuplicate (line 811) | func (c *QQChannel) isDuplicate(messageID string) bool {
    method dedupJanitor (line 839) | func (c *QQChannel) dedupJanitor() {
  function NewQQChannel (line 85) | func NewQQChannel(cfg config.QQConfig, messageBus *bus.MessageBus) (*QQC...
  type qqMediaUpload (line 356) | type qqMediaUpload struct
  function qqFileType (line 491) | func qqFileType(partType string) uint64 {
  function qqAttachmentFilename (line 738) | func qqAttachmentFilename(attachment *dto.MessageAttachment) string {
  function qqAttachmentKind (line 765) | func qqAttachmentKind(attachment *dto.MessageAttachment) string {
  function qqAttachmentNote (line 794) | func qqAttachmentNote(attachment *dto.MessageAttachment) string {
  function isHTTPURL (line 866) | func isHTTPURL(s string) bool {
  function appendContent (line 870) | func appendContent(content, suffix string) string {
  function sanitizeURLs (line 893) | func sanitizeURLs(text string) string {

FILE: pkg/channels/qq/qq_test.go
  function TestHandleC2CMessage_IncludesAccountIDMetadata (line 23) | func TestHandleC2CMessage_IncludesAccountIDMetadata(t *testing.T) {
  function TestHandleC2CMessage_AttachmentOnlyPublishesMedia (line 63) | func TestHandleC2CMessage_AttachmentOnlyPublishesMedia(t *testing.T) {
  function TestHandleGroupATMessage_AttachmentOnlyPublishesMedia (line 120) | func TestHandleGroupATMessage_AttachmentOnlyPublishesMedia(t *testing.T) {
  function TestSendMedia_UploadsLocalFileAsBase64 (line 171) | func TestSendMedia_UploadsLocalFileAsBase64(t *testing.T) {
  function TestSendMedia_UsesRemoteURLUploadForC2C (line 267) | func TestSendMedia_UsesRemoteURLUploadForC2C(t *testing.T) {
  function TestSendMedia_ReturnsSendFailedWithoutMediaStore (line 325) | func TestSendMedia_ReturnsSendFailedWithoutMediaStore(t *testing.T) {
  function TestSendMedia_ReturnsSendFailedWhenLocalFileExceedsBase64MiBLimit (line 349) | func TestSendMedia_ReturnsSendFailedWhenLocalFileExceedsBase64MiBLimit(t...
  type fakeQQAPI (line 402) | type fakeQQAPI struct
    method WS (line 418) | func (f *fakeQQAPI) WS(
    method PostGroupMessage (line 426) | func (f *fakeQQAPI) PostGroupMessage(
    method PostC2CMessage (line 436) | func (f *fakeQQAPI) PostC2CMessage(
    method Transport (line 446) | func (f *fakeQQAPI) Transport(_ context.Context, method, url string, b...
  type fakeTransportCall (line 412) | type fakeTransportCall struct
  function mustJSON (line 459) | func mustJSON(t *testing.T, v any) []byte {
  function waitInboundMessage (line 469) | func waitInboundMessage(t *testing.T, messageBus *bus.MessageBus) bus.In...
  function writeTempFile (line 488) | func writeTempFile(t *testing.T, dir, name string, content []byte) string {

FILE: pkg/channels/registry.go
  type ChannelFactory (line 12) | type ChannelFactory
  function RegisterFactory (line 20) | func RegisterFactory(name string, f ChannelFactory) {
  function getFactory (line 27) | func getFactory(name string) (ChannelFactory, bool) {

FILE: pkg/channels/slack/init.go
  function init (line 9) | func init() {

FILE: pkg/channels/slack/slack.go
  type SlackChannel (line 22) | type SlackChannel struct
    method Start (line 65) | func (c *SlackChannel) Start(ctx context.Context) error {
    method Stop (line 99) | func (c *SlackChannel) Stop(ctx context.Context) error {
    method Send (line 111) | func (c *SlackChannel) Send(ctx context.Context, msg bus.OutboundMessa...
    method SendMedia (line 155) | func (c *SlackChannel) SendMedia(ctx context.Context, msg bus.Outbound...
    method ReactToMessage (line 211) | func (c *SlackChannel) ReactToMessage(ctx context.Context, chatID, mes...
    method eventLoop (line 230) | func (c *SlackChannel) eventLoop() {
    method handleEventsAPI (line 253) | func (c *SlackChannel) handleEventsAPI(event socketmode.Event) {
    method handleMessageEvent (line 271) | func (c *SlackChannel) handleMessageEvent(ev *slackevents.MessageEvent) {
    method handleAppMention (line 382) | func (c *SlackChannel) handleAppMention(ev *slackevents.AppMentionEven...
    method handleSlashCommand (line 447) | func (c *SlackChannel) handleSlashCommand(event socketmode.Event) {
    method downloadSlackFile (line 505) | func (c *SlackChannel) downloadSlackFile(file slack.File) string {
    method stripBotMention (line 523) | func (c *SlackChannel) stripBotMention(text string) string {
  type slackMessageRef (line 34) | type slackMessageRef struct
  function NewSlackChannel (line 39) | func NewSlackChannel(cfg config.SlackConfig, messageBus *bus.MessageBus)...
  function parseSlackChatID (line 529) | func parseSlackChatID(chatID string) (channelID, threadTS string) {

FILE: pkg/channels/slack/slack_test.go
  function TestParseSlackChatID (line 10) | func TestParseSlackChatID(t *testing.T) {
  function TestStripBotMention (line 56) | func TestStripBotMention(t *testing.T) {
  function TestNewSlackChannel (line 101) | func TestNewSlackChannel(t *testing.T) {
  function TestSlackChannelIsAllowed (line 145) | func TestSlackChannelIsAllowed(t *testing.T) {

FILE: pkg/channels/split.go
  function SplitMessage (line 13) | func SplitMessage(content string, maxLen int) []string {
  function findLastUnclosedCodeBlockInRange (line 144) | func findLastUnclosedCodeBlockInRange(runes []rune, start, end int) int {
  function findNextClosingCodeBlockInRange (line 166) | func findNextClosingCodeBlockInRange(runes []rune, startIdx, end int) int {
  function findNewlineFrom (line 177) | func findNewlineFrom(runes []rune, from int) int {
  function findLastNewlineInRange (line 188) | func findLastNewlineInRange(runes []rune, start, end, searchWindow int) ...
  function findLastSpaceInRange (line 200) | func findLastSpaceInRange(runes []rune, start, end, searchWindow int) int {

FILE: pkg/channels/split_test.go
  function TestSplitMessage (line 8) | func TestSplitMessage(t *testing.T) {
  function TestFindLastNewlineInRange (line 152) | func TestFindLastNewlineInRange(t *testing.T) {
  function TestFindLastSpaceInRange (line 181) | func TestFindLastSpaceInRange(t *testing.T) {
  function TestFindNewlineFrom (line 208) | func TestFindNewlineFrom(t *testing.T) {
  function TestFindLastUnclosedCodeBlockInRange (line 232) | func TestFindLastUnclosedCodeBlockInRange(t *testing.T) {
  function TestFindNextClosingCodeBlockInRange (line 289) | func TestFindNextClosingCodeBlockInRange(t *testing.T) {
  function TestSplitMessage_CodeBlockIntegrity (line 335) | func TestSplitMessage_CodeBlockIntegrity(t *testing.T) {

FILE: pkg/channels/telegram/command_registration.go
  function commandRegistrationDelay (line 23) | func commandRegistrationDelay(attempt int) time.Duration {
  method RegisterCommands (line 33) | func (c *TelegramChannel) RegisterCommands(ctx context.Context, defs []c...
  method startCommandRegistration (line 60) | func (c *TelegramChannel) startCommandRegistration(ctx context.Context, ...

FILE: pkg/channels/telegram/command_registration_test.go
  function TestStartCommandRegistration_DoesNotBlock (line 13) | func TestStartCommandRegistration_DoesNotBlock(t *testing.T) {
  function TestStartCommandRegistration_RetriesUntilSuccessThenStops (line 33) | func TestStartCommandRegistration_RetriesUntilSuccessThenStops(t *testin...
  function TestStartCommandRegistration_StopsAfterCancel (line 71) | func TestStartCommandRegistration_StopsAfterCancel(t *testing.T) {

FILE: pkg/channels/telegram/init.go
  function init (line 9) | func init() {

FILE: pkg/channels/telegram/parse_markdown_to_md_v2.go
  type entityPattern (line 33) | type entityPattern struct
  function markdownToTelegramMarkdownV2 (line 91) | func markdownToTelegramMarkdownV2(text string) string {
  function processText (line 113) | func processText(text string) string {
  function escapeMarkdownV2 (line 178) | func escapeMarkdownV2(s string) string {

FILE: pkg/channels/telegram/parse_markdown_to_md_v2_test.go
  function Test_markdownToTelegramMarkdownV2 (line 13) | func Test_markdownToTelegramMarkdownV2(t *testing.T) {

FILE: pkg/channels/telegram/parser_markdown_to_html.go
  function markdownToTelegramHTML (line 8) | func markdownToTelegramHTML(text string) string {
  type codeBlockMatch (line 60) | type codeBlockMatch struct
  function extractCodeBlocks (line 65) | func extractCodeBlocks(text string) codeBlockMatch {
  type inlineCodeMatch (line 83) | type inlineCodeMatch struct
  function extractInlineCodes (line 88) | func extractInlineCodes(text string) inlineCodeMatch {
  function escapeHTML (line 106) | func escapeHTML(text string) string {

FILE: pkg/channels/telegram/telegram.go
  type TelegramChannel (line 42) | type TelegramChannel struct
    method Start (line 106) | func (c *TelegramChannel) Start(ctx context.Context) error {
    method Stop (line 148) | func (c *TelegramChannel) Stop(ctx context.Context) error {
    method Send (line 168) | func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMe...
    method sendChunk (line 272) | func (c *TelegramChannel) sendChunk(
    method StartTyping (line 316) | func (c *TelegramChannel) StartTyping(ctx context.Context, chatID stri...
    method EditMessage (line 351) | func (c *TelegramChannel) EditMessage(ctx context.Context, chatID stri...
    method SendPlaceholder (line 380) | func (c *TelegramChannel) SendPlaceholder(ctx context.Context, chatID ...
    method SendMedia (line 407) | func (c *TelegramChannel) SendMedia(ctx context.Context, msg bus.Outbo...
    method handleMessage (line 504) | func (c *TelegramChannel) handleMessage(ctx context.Context, message *...
    method downloadPhoto (line 682) | func (c *TelegramChannel) downloadPhoto(ctx context.Context, fileID st...
    method downloadFileWithInfo (line 694) | func (c *TelegramChannel) downloadFileWithInfo(file *telego.File, ext ...
    method downloadFile (line 709) | func (c *TelegramChannel) downloadFile(ctx context.Context, fileID, ex...
    method isBotMentioned (line 763) | func (c *TelegramChannel) isBotMentioned(message *telego.Message) bool {
    method stripBotMention (line 840) | func (c *TelegramChannel) stripBotMention(content string) string {
  function NewTelegramChannel (line 55) | func NewTelegramChannel(cfg *config.Config, bus *bus.MessageBus) (*Teleg...
  type sendChunkParams (line 261) | type sendChunkParams struct
  constant maxTypingDuration (line 308) | maxTypingDuration = 5 * time.Minute
  function parseContent (line 721) | func parseContent(text string, useMarkdownV2 bool) string {
  function parseTelegramChatID (line 731) | func parseTelegramChatID(chatID string) (int64, int, error) {
  function logParseFailed (line 748) | func logParseFailed(err error, useMarkdownV2 bool) {
  function telegramEntityTextAndList (line 799) | func telegramEntityTextAndList(message *telego.Message) (string, []teleg...
  function telegramEntityText (line 806) | func telegramEntityText(runes []rune, entity telego.MessageEntity) (stri...
  function isBotCommandEntityForThisBot (line 817) | func isBotCommandEntityForThisBot(entityText, botUsername string) bool {

FILE: pkg/channels/telegram/telegram_dispatch_test.go
  function TestHandleMessage_DoesNotConsumeGenericCommandsLocally (line 13) | func TestHandleMessage_DoesNotConsumeGenericCommandsLocally(t *testing.T) {

FILE: pkg/channels/telegram/telegram_group_command_filter_test.go
  type getMeCaller (line 18) | type getMeCaller struct
    method Call (line 22) | func (c getMeCaller) Call(_ context.Context, url string, _ *ta.Request...
  function newTestTelegramBot (line 30) | func newTestTelegramBot(t *testing.T, username string) *telego.Bot {
  function newGroupMentionOnlyChannel (line 44) | func newGroupMentionOnlyChannel(t *testing.T, botUsername string) (*Tele...
  function TestHandleMessage_GroupMentionOnly_BotCommandEntity (line 59) | func TestHandleMessage_GroupMentionOnly_BotCommandEntity(t *testing.T) {
  function TestIsBotMentioned_MentionEntityUnaffected (line 134) | func TestIsBotMentioned_MentionEntityUnaffected(t *testing.T) {

FILE: pkg/channels/telegram/telegram_test.go
  constant testToken (line 24) | testToken = "1234567890:aaaabbbbaaaabbbbaaaabbbbaaaabbbbccc"
  type stubCaller (line 27) | type stubCaller struct
    method Call (line 37) | func (s *stubCaller) Call(ctx context.Context, url string, data *ta.Re...
  type stubCall (line 32) | type stubCall struct
  type stubConstructor (line 43) | type stubConstructor struct
    method JSONRequest (line 50) | func (s *stubConstructor) JSONRequest(parameters any) (*ta.RequestData...
    method MultipartRequest (line 61) | func (s *stubConstructor) MultipartRequest(
  type multipartCall (line 45) | type multipartCall struct
  type multipartRecordingConstructor (line 68) | type multipartRecordingConstructor struct
    method MultipartRequest (line 73) | func (s *multipartRecordingConstructor) MultipartRequest(
  function successResponse (line 99) | func successResponse(t *testing.T) *ta.Response {
  function newTestChannel (line 108) | func newTestChannel(t *testing.T, caller *stubCaller) *TelegramChannel {
  function newTestChannelWithConstructor (line 112) | func newTestChannelWithConstructor(
  function TestSendMedia_ImageFallbacksToDocumentOnInvalidDimensions (line 139) | func TestSendMedia_ImageFallbacksToDocumentOnInvalidDimensions(t *testin...
  function TestSendMedia_ImageNonDimensionErrorDoesNotFallback (line 190) | func TestSendMedia_ImageNonDimensionErrorDoesNotFallback(t *testing.T) {
  function TestSend_EmptyContent (line 225) | func TestSend_EmptyContent(t *testing.T) {
  function TestSend_ShortMessage_SingleCall (line 243) | func TestSend_ShortMessage_SingleCall(t *testing.T) {
  function TestSend_LongMessage_SingleCall (line 260) | func TestSend_LongMessage_SingleCall(t *testing.T) {
  function TestSend_HTMLFallback_PerChunk (line 283) | func TestSend_HTMLFallback_PerChunk(t *testing.T) {
  function TestSend_HTMLFallback_BothFail (line 307) | func TestSend_HTMLFallback_BothFail(t *testing.T) {
  function TestSend_LongMessage_HTMLFallback_StopsOnError (line 325) | func TestSend_LongMessage_HTMLFallback_StopsOnError(t *testing.T) {
  function TestSend_MarkdownShortButHTMLLong_MultipleCalls (line 347) | func TestSend_MarkdownShortButHTMLLong_MultipleCalls(t *testing.T) {
  function TestSend_HTMLOverflow_WordBoundary (line 379) | func TestSend_HTMLOverflow_WordBoundary(t *testing.T) {
  function TestSend_NotRunning (line 428) | func TestSend_NotRunning(t *testing.T) {
  function TestSend_InvalidChatID (line 447) | func TestSend_InvalidChatID(t *testing.T) {
  function TestParseTelegramChatID_Plain (line 466) | func TestParseTelegramChatID_Plain(t *testing.T) {
  function TestParseTelegramChatID_NegativeGroup (line 473) | func TestParseTelegramChatID_NegativeGroup(t *testing.T) {
  function TestParseTelegramChatID_WithThreadID (line 480) | func TestParseTelegramChatID_WithThreadID(t *testing.T) {
  function TestParseTelegramChatID_GeneralTopic (line 487) | func TestParseTelegramChatID_GeneralTopic(t *testing.T) {
  function TestParseTelegramChatID_Invalid (line 494) | func TestParseTelegramChatID_Invalid(t *testing.T) {
  function TestParseTelegramChatID_InvalidThreadID (line 499) | func TestParseTelegramChatID_InvalidThreadID(t *testing.T) {
  function TestSend_WithForumThreadID (line 505) | func TestSend_WithForumThreadID(t *testing.T) {
  function TestHandleMessage_ForumTopic_SetsMetadata (line 522) | func TestHandleMessage_ForumTopic_SetsMetadata(t *testing.T) {
  function TestHandleMessage_NoForum_NoThreadMetadata (line 563) | func TestHandleMessage_NoForum_NoThreadMetadata(t *testing.T) {
  function TestHandleMessage_ReplyThread_NonForum_NoIsolation (line 602) | func TestHandleMessage_ReplyThread_NonForum_NoIsolation(t *testing.T) {

FILE: pkg/channels/webhook.go
  type WebhookHandler (line 8) | type WebhookHandler interface
  type HealthChecker (line 17) | type HealthChecker interface

FILE: pkg/channels/wecom/aibot.go
  type WeComAIBotChannel (line 30) | type WeComAIBotChannel struct
    method Name (line 179) | func (c *WeComAIBotChannel) Name() string {
    method Start (line 184) | func (c *WeComAIBotChannel) Start(ctx context.Context) error {
    method Stop (line 199) | func (c *WeComAIBotChannel) Stop(ctx context.Context) error {
    method Send (line 214) | func (c *WeComAIBotChannel) Send(ctx context.Context, msg bus.Outbound...
    method WebhookPath (line 290) | func (c *WeComAIBotChannel) WebhookPath() string {
    method ServeHTTP (line 298) | func (c *WeComAIBotChannel) ServeHTTP(w http.ResponseWriter, r *http.R...
    method HealthPath (line 303) | func (c *WeComAIBotChannel) HealthPath() string {
    method HealthHandler (line 308) | func (c *WeComAIBotChannel) HealthHandler(w http.ResponseWriter, r *ht...
    method handleWebhook (line 313) | func (c *WeComAIBotChannel) handleWebhook(w http.ResponseWriter, r *ht...
    method handleVerification (line 336) | func (c *WeComAIBotChannel) handleVerification(
    method handleMessageCallback (line 381) | func (c *WeComAIBotChannel) handleMessageCallback(
    method processMessage (line 467) | func (c *WeComAIBotChannel) processMessage(
    method handleTextMessage (line 503) | func (c *WeComAIBotChannel) handleTextMessage(
    method handleStreamMessage (line 585) | func (c *WeComAIBotChannel) handleStreamMessage(
    method handleImageMessage (line 624) | func (c *WeComAIBotChannel) handleImageMessage(
    method handleMixedMessage (line 652) | func (c *WeComAIBotChannel) handleMixedMessage(
    method handleEventMessage (line 669) | func (c *WeComAIBotChannel) handleEventMessage(
    method getStreamResponse (line 702) | func (c *WeComAIBotChannel) getStreamResponse(task *streamTask, timest...
    method removeTask (line 758) | func (c *WeComAIBotChannel) removeTask(task *streamTask) {
    method sendViaResponseURL (line 783) | func (c *WeComAIBotChannel) sendViaResponseURL(responseURL, content st...
    method encryptResponse (line 833) | func (c *WeComAIBotChannel) encryptResponse(
    method encryptEmptyResponse (line 888) | func (c *WeComAIBotChannel) encryptEmptyResponse(timestamp, nonce stri...
    method encryptMessage (line 896) | func (c *WeComAIBotChannel) encryptMessage(plaintext, receiveid string...
    method generateStreamID (line 987) | func (c *WeComAIBotChannel) generateStreamID() string {
    method cleanupLoop (line 992) | func (c *WeComAIBotChannel) cleanupLoop() {
    method cleanupOldTasks (line 1018) | func (c *WeComAIBotChannel) cleanupOldTasks() {
    method handleHealth (line 1088) | func (c *WeComAIBotChannel) handleHealth(w http.ResponseWriter, r *htt...
  type streamTask (line 46) | type streamTask struct
  type WeComAIBotMessage (line 66) | type WeComAIBotMessage struct
  type WeComAIBotMsgItemImage (line 107) | type WeComAIBotMsgItemImage struct
  type WeComAIBotMsgItem (line 113) | type WeComAIBotMsgItem struct
  type WeComAIBotStreamInfo (line 119) | type WeComAIBotStreamInfo struct
  type WeComAIBotStreamResponse (line 127) | type WeComAIBotStreamResponse struct
  type WeComAIBotEncryptedResponse (line 134) | type WeComAIBotEncryptedResponse struct
  function NewWeComAIBotChannel (line 146) | func NewWeComAIBotChannel(
  function generateRandomID (line 976) | func generateRandomID(n int) string {
  constant streamClosedGracePeriod (line 1014) | streamClosedGracePeriod = 10 * time.Minute
  constant taskMaxLifetime (line 1015) | taskMaxLifetime         = 1 * time.Hour

FILE: pkg/channels/wecom/aibot_test.go
  function TestNewWeComAIBotChannel_WebhookMode (line 16) | func TestNewWeComAIBotChannel_WebhookMode(t *testing.T) {
  function TestWeComAIBotWebhookChannelStartStop (line 67) | func TestWeComAIBotWebhookChannelStartStop(t *testing.T) {
  function TestWeComAIBotChannelWebhookPath (line 97) | func TestWeComAIBotChannelWebhookPath(t *testing.T) {
  function TestWeComAIBotChannelGetStreamResponseProcessingMessage (line 138) | func TestWeComAIBotChannelGetStreamResponseProcessingMessage(t *testing....
  function TestGenerateStreamID (line 219) | func TestGenerateStreamID(t *testing.T) {
  function TestEncryptDecrypt (line 245) | func TestEncryptDecrypt(t *testing.T) {
  function TestGenerateSignature (line 278) | func TestGenerateSignature(t *testing.T) {
  function decodeStreamResponse (line 293) | func decodeStreamResponse(t *testing.T, ch *WeComAIBotChannel, encrypted...
  function TestNewWeComAIBotChannel_WSMode (line 316) | func TestNewWeComAIBotChannel_WSMode(t *testing.T) {
  function TestWeComAIBotWSChannelStartStop (line 384) | func TestWeComAIBotWSChannelStartStop(t *testing.T) {
  function TestGenerateRandomID (line 415) | func TestGenerateRandomID(t *testing.T) {
  function TestWSGenerateID (line 429) | func TestWSGenerateID(t *testing.T) {
  function makeWebhookChannel (line 446) | func makeWebhookChannel(t *testing.T) *WeComAIBotChannel {
  function makeStreamTask (line 463) | func makeStreamTask(t *testing.T, ch *WeComAIBotChannel, streamID, chatI...
  function TestGetStreamResponse_ImmediateAnswer (line 482) | func TestGetStreamResponse_ImmediateAnswer(t *testing.T) {
  function TestGetStreamResponse_DeadlinePassed (line 508) | func TestGetStreamResponse_DeadlinePassed(t *testing.T) {
  function TestGetStreamResponse_StillPending (line 536) | func TestGetStreamResponse_StillPending(t *testing.T) {

FILE: pkg/channels/wecom/aibot_ws.go
  constant wsEndpoint (line 30) | wsEndpoint          = "wss://openws.work.weixin.qq.com"
  constant wsHeartbeatInterval (line 31) | wsHeartbeatInterval = 30 * time.Second
  constant wsConnectTimeout (line 32) | wsConnectTimeout    = 15 * time.Second
  constant wsSubscribeTimeout (line 33) | wsSubscribeTimeout  = 10 * time.Second
  constant wsSendMsgTimeout (line 34) | wsSendMsgTimeout    = 10 * time.Second
  constant wsRespondMsgTimeout (line 35) | wsRespondMsgTimeout = 10 * time.Second
  constant wsWelcomeMsgTimeout (line 36) | wsWelcomeMsgTimeout = 5 * time.Second
  constant wsMaxReconnectWait (line 37) | wsMaxReconnectWait  = 60 * time.Second
  constant wsInitialReconnect (line 38) | wsInitialReconnect  = time.Second
  constant wsStreamTickInterval (line 43) | wsStreamTickInterval = 30 * time.Second
  constant wsStreamMaxDuration (line 44) | wsStreamMaxDuration  = 5*time.Minute + 30*time.Second
  constant wsImageDownloadTimeout (line 47) | wsImageDownloadTimeout = 30 * time.Second
  constant wsLateReplyRouteTTL (line 50) | wsLateReplyRouteTTL = 30 * time.Minute
  constant wsStreamMaxContentBytes (line 55) | wsStreamMaxContentBytes = 20480
  type WeComAIBotWSChannel (line 66) | type WeComAIBotWSChannel struct
    method Name (line 248) | func (c *WeComAIBotWSChannel) Name() string { return "wecom_aibot" }
    method Start (line 251) | func (c *WeComAIBotWSChannel) Start(ctx context.Context) error {
    method Stop (line 261) | func (c *WeComAIBotWSChannel) Stop(_ context.Context) error {
    method Send (line 279) | func (c *WeComAIBotWSChannel) Send(ctx context.Context, msg bus.Outbou...
    method connectLoop (line 350) | func (c *WeComAIBotWSChannel) connectLoop() {
    method runConnection (line 395) | func (c *WeComAIBotWSChannel) runConnection() error {
    method sendAndWait (line 468) | func (c *WeComAIBotWSChannel) sendAndWait(
    method heartbeatLoop (line 515) | func (c *WeComAIBotWSChannel) heartbeatLoop(conn *websocket.Conn) {
    method readLoop (line 547) | func (c *WeComAIBotWSChannel) readLoop(conn *websocket.Conn) error {
    method handleEnvelope (line 591) | func (c *WeComAIBotWSChannel) handleEnvelope(env wsEnvelope) {
    method handleMsgCallback (line 604) | func (c *WeComAIBotWSChannel) handleMsgCallback(env wsEnvelope) {
    method handleEventCallback (line 642) | func (c *WeComAIBotWSChannel) handleEventCallback(env wsEnvelope) {
    method handleWSTextMessage (line 682) | func (c *WeComAIBotWSChannel) handleWSTextMessage(reqID string, msg We...
    method handleWSImageMessage (line 692) | func (c *WeComAIBotWSChannel) handleWSImageMessage(reqID string, msg W...
    method wsHandleMediaMessage (line 703) | func (c *WeComAIBotWSChannel) wsHandleMediaMessage(
    method handleWSMixedMessage (line 727) | func (c *WeComAIBotWSChannel) handleWSMixedMessage(reqID string, msg W...
    method dispatchWSAgentTask (line 781) | func (c *WeComAIBotWSChannel) dispatchWSAgentTask(
    method handleWSVoiceMessage (line 923) | func (c *WeComAIBotWSChannel) handleWSVoiceMessage(reqID string, msg W...
    method handleWSFileMessage (line 932) | func (c *WeComAIBotWSChannel) handleWSFileMessage(reqID string, msg We...
    method handleWSVideoMessage (line 942) | func (c *WeComAIBotWSChannel) handleWSVideoMessage(reqID string, msg W...
    method wsSendStreamChunk (line 954) | func (c *WeComAIBotWSChannel) wsSendStreamChunk(reqID, streamID string...
    method wsSendStreamFinish (line 983) | func (c *WeComAIBotWSChannel) wsSendStreamFinish(reqID, streamID, cont...
    method wsSendWelcomeMsg (line 988) | func (c *WeComAIBotWSChannel) wsSendWelcomeMsg(reqID, content string) {
    method wsSendActivePush (line 1008) | func (c *WeComAIBotWSChannel) wsSendActivePush(chatID string, chatType...
    method writeWSAndWait (line 1031) | func (c *WeComAIBotWSChannel) writeWSAndWait(cmd wsCommand, timeout ti...
    method cancelAllTasks (line 1057) | func (c *WeComAIBotWSChannel) cancelAllTasks() {
    method setReqState (line 1071) | func (c *WeComAIBotWSChannel) setReqState(reqID string, state *wsReqSt...
    method getReqState (line 1083) | func (c *WeComAIBotWSChannel) getReqState(reqID string) (*wsTask, wsLa...
    method deleteReqState (line 1097) | func (c *WeComAIBotWSChannel) deleteReqState(reqID string) {
    method clearReqTask (line 1103) | func (c *WeComAIBotWSChannel) clearReqTask(reqID string, task *wsTask) {
    method storeWSMedia (line 1143) | func (c *WeComAIBotWSChannel) storeWSMedia(
  type wsTask (line 92) | type wsTask struct
  type wsReqState (line 102) | type wsReqState struct
  type wsLateReplyRoute (line 107) | type wsLateReplyRoute struct
  type wsEnvelope (line 117) | type wsEnvelope struct
  type wsHeaders (line 125) | type wsHeaders struct
  type wsCommand (line 130) | type wsCommand struct
  type wsSendMsgBody (line 136) | type wsSendMsgBody struct
  type wsRespondMsgBody (line 144) | type wsRespondMsgBody struct
  type wsStreamContent (line 152) | type wsStreamContent struct
  type wsImageContent (line 159) | type wsImageContent struct
  type wsTextContent (line 164) | type wsTextContent struct
  type wsMarkdownContent (line 168) | type wsMarkdownContent struct
  type WeComAIBotWSMessage (line 176) | type WeComAIBotWSMessage struct
  function newWeComAIBotWSChannel (line 224) | func newWeComAIBotWSChannel(
  constant wsBackoffResetDuration (line 346) | wsBackoffResetDuration = time.Minute
  function wsChatTypeValue (line 1115) | func wsChatTypeValue(chatType string) uint32 {
  function wsChatID (line 1124) | func wsChatID(msg WeComAIBotWSMessage) string {
  function wsGenerateID (line 1133) | func wsGenerateID() string {
  function wsMediaExtFromContentType (line 1233) | func wsMediaExtFromContentType(contentType string) string {
  function wsLabelToDefaultExt (line 1288) | func wsLabelToDefaultExt(label string) string {
  function splitWSContent (line 1305) | func splitWSContent(content string, maxBytes int) []string {
  function splitAtByteBoundary (line 1328) | func splitAtByteBoundary(s string, maxBytes int) []string {

FILE: pkg/channels/wecom/aibot_ws_test.go
  function newTestWSChannel (line 19) | func newTestWSChannel(t *testing.T) *WeComAIBotWSChannel {
  function TestStoreWSMedia_NilStore (line 35) | func TestStoreWSMedia_NilStore(t *testing.T) {
  function TestStoreWSMedia_HTTPError (line 45) | func TestStoreWSMedia_HTTPError(t *testing.T) {
  function TestStoreWSMedia_ServerUnavailable (line 62) | func TestStoreWSMedia_ServerUnavailable(t *testing.T) {
  function TestStoreWSMedia_Success_NoAES (line 76) | func TestStoreWSMedia_Success_NoAES(t *testing.T) {
  function TestStoreWSMedia_MultipleMessages (line 121) | func TestStoreWSMedia_MultipleMessages(t *testing.T) {
  function TestStoreWSMedia_ContentTypeExt (line 171) | func TestStoreWSMedia_ContentTypeExt(t *testing.T) {
  function TestSplitWSContent (line 222) | func TestSplitWSContent(t *testing.T) {
  function TestSplitAtByteBoundary (line 271) | func TestSplitAtByteBoundary(t *testing.T) {

FILE: pkg/channels/wecom/app.go
  constant wecomAPIBase (line 28) | wecomAPIBase = "https://qyapi.weixin.qq.com"
  type WeComAppChannel (line 32) | type WeComAppChannel struct
    method Name (line 151) | func (c *WeComAppChannel) Name() string {
    method Start (line 156) | func (c *WeComAppChannel) Start(ctx context.Context) error {
    method Stop (line 182) | func (c *WeComAppChannel) Stop(ctx context.Context) error {
    method Send (line 195) | func (c *WeComAppChannel) Send(ctx context.Context, msg bus.OutboundMe...
    method SendMedia (line 214) | func (c *WeComAppChannel) SendMedia(ctx context.Context, msg bus.Outbo...
    method uploadMedia (line 287) | func (c *WeComAppChannel) uploadMedia(ctx context.Context, accessToken...
    method sendWeComMessage (line 354) | func (c *WeComAppChannel) sendWeComMessage(ctx context.Context, access...
    method sendImageMessage (line 414) | func (c *WeComAppChannel) sendImageMessage(ctx context.Context, access...
    method WebhookPath (line 425) | func (c *WeComAppChannel) WebhookPath() string {
    method ServeHTTP (line 433) | func (c *WeComAppChannel) ServeHTTP(w http.ResponseWriter, r *http.Req...
    method HealthPath (line 438) | func (c *WeComAppChannel) HealthPath() string {
    method HealthHandler (line 443) | func (c *WeComAppChannel) HealthHandler(w http.ResponseWriter, r *http...
    method handleWebhook (line 448) | func (c *WeComAppChannel) handleWebhook(w http.ResponseWriter, r *http...
    method handleVerification (line 478) | func (c *WeComAppChannel) handleVerification(ctx context.Context, w ht...
    method handleMessageCallback (line 542) | func (c *WeComAppChannel) handleMessageCallback(ctx context.Context, w...
    method processMessage (line 615) | func (c *WeComAppChannel) processMessage(ctx context.Context, msg WeCo...
    method tokenRefreshLoop (line 671) | func (c *WeComAppChannel) tokenRefreshLoop() {
    method refreshAccessToken (line 690) | func (c *WeComAppChannel) refreshAccessToken() error {
    method getAccessToken (line 724) | func (c *WeComAppChannel) getAccessToken() string {
    method sendTextMessage (line 736) | func (c *WeComAppChannel) sendTextMessage(ctx context.Context, accessT...
    method handleHealth (line 747) | func (c *WeComAppChannel) handleHealth(w http.ResponseWriter, r *http....
  type WeComXMLMessage (line 45) | type WeComXMLMessage struct
  type WeComTextMessage (line 70) | type WeComTextMessage struct
  type WeComMarkdownMessage (line 81) | type WeComMarkdownMessage struct
  type WeComImageMessage (line 91) | type WeComImageMessage struct
  type WeComAccessTokenResponse (line 101) | type WeComAccessTokenResponse struct
  type WeComSendMessageResponse (line 109) | type WeComSendMessageResponse struct
  type PKCS7Padding (line 118) | type PKCS7Padding struct
  function NewWeComAppChannel (line 121) | func NewWeComAppChannel(cfg config.WeComAppConfig, messageBus *bus.Messa...

FILE: pkg/channels/wecom/app_test.go
  function generateTestAESKeyApp (line 26) | func generateTestAESKeyApp() string {
  function encryptTestMessageApp (line 37) | func encryptTestMessageApp(message, aesKey string) (string, error) {
  function generateSignatureApp (line 81) | func generateSignatureApp(token, timestamp, nonce, msgEncrypt string) st...
  function TestNewWeComAppChannel (line 89) | func TestNewWeComAppChannel(t *testing.T) {
  function TestWeComAppChannelIsAllowed (line 148) | func TestWeComAppChannelIsAllowed(t *testing.T) {
  function TestWeComAppVerifySignature (line 181) | func TestWeComAppVerifySignature(t *testing.T) {
  function TestWeComAppDecryptMessage (line 227) | func TestWeComAppDecryptMessage(t *testing.T) {
  function TestWeComAppHandleVerification (line 326) | func TestWeComAppHandleVerification(t *testing.T) {
  function TestWeComAppHandleMessageCallback (line 394) | func TestWeComAppHandleMessageCallback(t *testing.T) {
  function TestWeComAppProcessMessage (line 509) | func TestWeComAppProcessMessage(t *testing.T) {
  function TestWeComAppHandleWebhook (line 595) | func TestWeComAppHandleWebhook(t *testing.T) {
  function TestWeComAppHandleHealth (line 666) | func TestWeComAppHandleHealth(t *testing.T) {
  function TestWeComAppAccessToken (line 695) | func TestWeComAppAccessToken(t *testing.T) {
  function TestWeComAppMessageStructures (line 736) | func TestWeComAppMessageStructures(t *testing.T) {
  function TestWeComAppXMLMessageStructure (line 872) | func TestWeComAppXMLMessageStructure(t *testing.T) {
  function TestWeComAppXMLMessageImage (line 913) | func TestWeComAppXMLMessageImage(t *testing.T) {
  function TestWeComAppXMLMessageVoice (line 943) | func TestWeComAppXMLMessageVoice(t *testing.T) {
  function TestWeComAppXMLMessageLocation (line 970) | func TestWeComAppXMLMessageLocation(t *testing.T) {
  function TestWeComAppXMLMessageLink (line 1008) | func TestWeComAppXMLMessageLink(t *testing.T) {
  function TestWeComAppXMLMessageEvent (line 1042) | func TestWeComAppXMLMessageEvent(t *testing.T) {

FILE: pkg/channels/wecom/bot.go
  type WeComBotChannel (line 24) | type WeComBotChannel struct
    method Name (line 114) | func (c *WeComBotChannel) Name() string {
    method Start (line 119) | func (c *WeComBotChannel) Start(ctx context.Context) error {
    method Stop (line 135) | func (c *WeComBotChannel) Stop(ctx context.Context) error {
    method Send (line 150) | func (c *WeComBotChannel) Send(ctx context.Context, msg bus.OutboundMe...
    method WebhookPath (line 164) | func (c *WeComBotChannel) WebhookPath() string {
    method ServeHTTP (line 172) | func (c *WeComBotChannel) ServeHTTP(w http.ResponseWriter, r *http.Req...
    method HealthPath (line 177) | func (c *WeComBotChannel) HealthPath() string {
    method HealthHandler (line 182) | func (c *WeComBotChannel) HealthHandler(w http.ResponseWriter, r *http...
    method handleWebhook (line 187) | func (c *WeComBotChannel) handleWebhook(w http.ResponseWriter, r *http...
    method handleVerification (line 206) | func (c *WeComBotChannel) handleVerification(ctx context.Context, w ht...
    method handleMessageCallback (line 245) | func (c *WeComBotChannel) handleMessageCallback(ctx context.Context, w...
    method processMessage (line 319) | func (c *WeComBotChannel) processMessage(ctx context.Context, msg WeCo...
    method sendWebhookReply (line 423) | func (c *WeComBotChannel) sendWebhookReply(ctx context.Context, userID...
    method handleHealth (line 491) | func (c *WeComBotChannel) handleHealth(w http.ResponseWriter, r *http....
  type WeComBotMessage (line 34) | type WeComBotMessage struct
  type WeComBotReplyMessage (line 76) | type WeComBotReplyMessage struct
  function NewWeComBotChannel (line 84) | func NewWeComBotChannel(cfg config.WeComConfig, messageBus *bus.MessageB...

FILE: pkg/channels/wecom/bot_test.go
  function generateTestAESKey (line 25) | func generateTestAESKey() string {
  function encryptTestMessage (line 36) | func encryptTestMessage(message, aesKey string) (string, error) {
  function generateSignature (line 80) | func generateSignature(token, timestamp, nonce, msgEncrypt string) string {
  function TestNewWeComBotChannel (line 88) | func TestNewWeComBotChannel(t *testing.T) {
  function TestWeComBotChannelIsAllowed (line 132) | func TestWeComBotChannelIsAllowed(t *testing.T) {
  function TestWeComBotVerifySignature (line 163) | func TestWeComBotVerifySignature(t *testing.T) {
  function TestWeComBotDecryptMessage (line 207) | func TestWeComBotDecryptMessage(t *testing.T) {
  function TestWeComBotPKCS7Unpad (line 284) | func TestWeComBotPKCS7Unpad(t *testing.T) {
  function TestWeComBotHandleVerification (line 338) | func TestWeComBotHandleVerification(t *testing.T) {
  function TestWeComBotHandleMessageCallback (line 404) | func TestWeComBotHandleMessageCallback(t *testing.T) {
  function TestWeComBotProcessMessage (line 531) | func TestWeComBotProcessMessage(t *testing.T) {
  function TestWeComBotHandleWebhook (line 600) | func TestWeComBotHandleWebhook(t *testing.T) {
  function TestWeComBotHandleHealth (line 669) | func TestWeComBotHandleHealth(t *testing.T) {
  function TestWeComBotReplyMessage (line 697) | func TestWeComBotReplyMessage(t *testing.T) {
  function TestWeComBotMessageStructure (line 711) | func TestWeComBotMessageStructure(t *testing.T) {

FILE: pkg/channels/wecom/common.go
  constant blockSize (line 18) | blockSize = 32
  function computeSignature (line 22) | func computeSignature(token, timestamp, nonce, encrypt string) string {
  function verifySignature (line 32) | func verifySignature(token, msgSignature, timestamp, nonce, msgEncrypt s...
  function decryptMessage (line 41) | func decryptMessage(encryptedMsg, encodingAESKey string) (string, error) {
  function decryptMessageWithVerify (line 47) | func decryptMessageWithVerify(encryptedMsg, encodingAESKey, receiveid st...
  function decodeWeComAESKey (line 78) | func decodeWeComAESKey(encodingAESKey string) ([]byte, error) {
  function encryptAESCBC (line 92) | func encryptAESCBC(aesKey, plaintext []byte) ([]byte, error) {
  function packWeComFrame (line 106) | func packWeComFrame(msg, receiveid string) ([]byte, error) {
  function unpackWeComFrame (line 128) | func unpackWeComFrame(data []byte, receiveid string) (string, error) {
  function decryptAESCBC (line 148) | func decryptAESCBC(aesKey, ciphertext []byte) ([]byte, error) {
  function pkcs7Pad (line 170) | func pkcs7Pad(data []byte, blockSize int) []byte {
  function pkcs7Unpad (line 180) | func pkcs7Unpad(data []byte) ([]byte, error) {

FILE: pkg/channels/wecom/dedupe.go
  constant wecomMaxProcessedMessages (line 5) | wecomMaxProcessedMessages = 1000
  type MessageDeduplicator (line 10) | type MessageDeduplicator struct
    method MarkMessageProcessed (line 31) | func (d *MessageDeduplicator) MarkMessageProcessed(msgID string) bool {
  function NewMessageDeduplicator (line 19) | func NewMessageDeduplicator(maxEntries int) *MessageDeduplicator {

FILE: pkg/channels/wecom/dedupe_test.go
  function TestMessageDeduplicator_DuplicateDetection (line 8) | func TestMessageDeduplicator_DuplicateDetection(t *testing.T) {
  function TestMessageDeduplicator_ConcurrentSameMessage (line 20) | func TestMessageDeduplicator_ConcurrentSameMessage(t *testing.T) {
  function TestMessageDeduplicator_CircularQueueEviction (line 50) | func TestMessageDeduplicator_CircularQueueEviction(t *testing.T) {

FILE: pkg/channels/wecom/init.go
  function init (line 9) | func init() {

FILE: pkg/channels/whatsapp/init.go
  function init (line 9) | func init() {

FILE: pkg/channels/whatsapp/whatsapp.go
  type WhatsAppChannel (line 20) | type WhatsAppChannel struct
    method Start (line 49) | func (c *WhatsAppChannel) Start(ctx context.Context) error {
    method Stop (line 81) | func (c *WhatsAppChannel) Stop(ctx context.Context) error {
    method Send (line 107) | func (c *WhatsAppChannel) Send(ctx context.Context, msg bus.OutboundMe...
    method listen (line 147) | func (c *WhatsAppChannel) listen() {
    method handleIncomingMessage (line 191) | func (c *WhatsAppChannel) handleIncomingMessage(msg map[string]any) {
  function NewWhatsAppChannel (line 31) | func NewWhatsAppChannel(cfg config.WhatsAppConfig, bus *bus.MessageBus) ...

FILE: pkg/channels/whatsapp/whatsapp_command_test.go
  function TestHandleIncomingMessage_DoesNotConsumeGenericCommandsLocally (line 12) | func TestHandleIncomingMessage_DoesNotConsumeGenericCommandsLocally(t *t...

FILE: pkg/channels/whatsapp_native/init.go
  function init (line 11) | func init() {

FILE: pkg/channels/whatsapp_native/whatsapp_command_test.go
  function TestHandleIncoming_DoesNotConsumeGenericCommandsLocally (line 20) | func TestHandleIncoming_DoesNotConsumeGenericCommandsLocally(t *testing....

FILE: pkg/channels/whatsapp_native/whatsapp_native.go
  constant sqliteDriver (line 40) | sqliteDriver   = "sqlite"
  constant whatsappDBName (line 41) | whatsappDBName = "store.db"
  constant reconnectInitial (line 43) | reconnectInitial    = 5 * time.Second
  constant reconnectMax (line 44) | reconnectMax        = 5 * time.Minute
  constant reconnectMultiplier (line 45) | reconnectMultiplier = 2.0
  type WhatsAppNativeChannel (line 49) | type WhatsAppNativeChannel struct
    method Start (line 83) | func (c *WhatsAppNativeChannel) Start(ctx context.Context) error {
    method Stop (line 213) | func (c *WhatsAppNativeChannel) Stop(ctx context.Context) error {
    method eventHandler (line 269) | func (c *WhatsAppNativeChannel) eventHandler(evt any) {
    method reconnectWithBackoff (line 297) | func (c *WhatsAppNativeChannel) reconnectWithBackoff() {
    method handleIncoming (line 343) | func (c *WhatsAppNativeChannel) handleIncoming(evt *events.Message) {
    method Send (line 399) | func (c *WhatsAppNativeChannel) Send(ctx context.Context, msg bus.Outb...
  function NewWhatsAppNativeChannel (line 66) | func NewWhatsAppNativeChannel(
  function parseJID (line 439) | func parseJID(s string) (types.JID, error) {

FILE: pkg/channels/whatsapp_native/whatsapp_native_stub.go
  function NewWhatsAppNativeChannel (line 15) | func NewWhatsAppNativeChannel(

FILE: pkg/commands/builtin.go
  function BuiltinDefinitions (line 7) | func BuiltinDefinitions() []Definition {

FILE: pkg/commands/builtin_test.go
  function findDefinitionByName (line 9) | func findDefinitionByName(t *testing.T, defs []Definition, name string) ...
  function TestBuiltinHelpHandler_ReturnsFormattedMessage (line 20) | func TestBuiltinHelpHandler_ReturnsFormattedMessage(t *testing.T) {
  function TestBuiltinShowChannel_PreservesUserVisibleBehavior (line 47) | func TestBuiltinShowChannel_PreservesUserVisibleBehavior(t *testing.T) {
  function TestBuiltinListChannels_UsesGetEnabledChannels (line 72) | func TestBuiltinListChannels_UsesGetEnabledChannels(t *testing.T) {
  function TestBuiltinShowAgents_RestoresOldBehavior (line 97) | func TestBuiltinShowAgents_RestoresOldBehavior(t *testing.T) {
  function TestBuiltinListAgents_RestoresOldBehavior (line 122) | func TestBuiltinListAgents_RestoresOldBehavior(t *testing.T) {

FILE: pkg/commands/cmd_check.go
  function checkCommand (line 8) | func checkCommand() Definition {

FILE: pkg/commands/cmd_clear.go
  function clearCommand (line 5) | func clearCommand() Definition {

FILE: pkg/commands/cmd_help.go
  function helpCommand (line 9) | func helpCommand() Definition {
  function formatHelpMessage (line 26) | func formatHelpMessage(defs []Definition) string {

FILE: pkg/commands/cmd_list.go
  function listCommand (line 9) | func listCommand() Definition {

FILE: pkg/commands/cmd_reload.go
  function reloadCommand (line 5) | func reloadCommand() Definition {

FILE: pkg/commands/cmd_show.go
  function showCommand (line 8) | func showCommand() Definition {

FILE: pkg/commands/cmd_start.go
  function startCommand (line 5) | func startCommand() Definition {

FILE: pkg/commands/cmd_switch.go
  function switchCommand (line 8) | func switchCommand() Definition {

FILE: pkg/commands/cmd_switch_test.go
  function TestSwitchModel_Success (line 9) | func TestSwitchModel_Success(t *testing.T) {
  function TestSwitchModel_MissingToKeyword (line 34) | func TestSwitchModel_MissingToKeyword(t *testing.T) {
  function TestSwitchModel_MissingValue (line 58) | func TestSwitchModel_MissingValue(t *testing.T) {
  function TestSwitchModel_Error (line 82) | func TestSwitchModel_Error(t *testing.T) {
  function TestSwitchModel_NilDep (line 106) | func TestSwitchModel_NilDep(t *testing.T) {
  function TestSwitchChannel_Redirect (line 125) | func TestSwitchChannel_Redirect(t *testing.T) {
  function TestCheckChannel_Success (line 145) | func TestCheckChannel_Success(t *testing.T) {
  function TestCheckChannel_Error (line 170) | func TestCheckChannel_Error(t *testing.T) {
  function TestCheckChannel_NilDep (line 194) | func TestCheckChannel_NilDep(t *testing.T) {
  function TestCheckChannel_MissingValue (line 213) | func TestCheckChannel_MissingValue(t *testing.T) {
  function TestSwitch_BangPrefix (line 237) | func TestSwitch_BangPrefix(t *testing.T) {
  function TestSwitch_NoSubCommand (line 261) | func TestSwitch_NoSubCommand(t *testing.T) {

FILE: pkg/commands/definition.go
  type SubCommand (line 9) | type SubCommand struct
  type Definition (line 23) | type Definition struct
    method EffectiveUsage (line 35) | func (d Definition) EffectiveUsage() string {

FILE: pkg/commands/definition_test.go
  function TestDefinition_EffectiveUsage_NoSubCommands (line 7) | func TestDefinition_EffectiveUsage_NoSubCommands(t *testing.T) {
  function TestDefinition_EffectiveUsage_WithSubCommands (line 14) | func TestDefinition_EffectiveUsage_WithSubCommands(t *testing.T) {
  function TestDefinition_EffectiveUsage_WithArgsUsage (line 29) | func TestDefinition_EffectiveUsage_WithArgsUsage(t *testing.T) {

FILE: pkg/commands/executor.go
  type Outcome (line 8) | type Outcome
  constant OutcomePassthrough (line 12) | OutcomePassthrough Outcome = iota
  constant OutcomeHandled (line 14) | OutcomeHandled
  type ExecuteResult (line 17) | type ExecuteResult struct
  type Executor (line 23) | type Executor struct
    method Execute (line 35) | func (e *Executor) Execute(ctx context.Context, req Request) ExecuteRe...
    method executeDefinition (line 53) | func (e *Executor) executeDefinition(ctx context.Context, req Request,...
  function NewExecutor (line 28) | func NewExecutor(reg *Registry, rt *Runtime) *Executor {

FILE: pkg/commands/executor_test.go
  function TestExecutor_RegisteredWithoutHandler_ReturnsPassthrough (line 10) | func TestExecutor_RegisteredWithoutHandler_ReturnsPassthrough(t *testing...
  function TestExecutor_UnknownSlashCommand_ReturnsPassthrough (line 20) | func TestExecutor_UnknownSlashCommand_ReturnsPassthrough(t *testing.T) {
  function TestExecutor_SupportedCommandWithHandler_ReturnsHandled (line 30) | func TestExecutor_SupportedCommandWithHandler_ReturnsHandled(t *testing....
  function TestExecutor_AliasWithoutHandler_ReturnsPassthrough (line 52) | func TestExecutor_AliasWithoutHandler_ReturnsPassthrough(t *testing.T) {
  function TestExecutor_AliasWithHandler_ReturnsHandled (line 70) | func TestExecutor_AliasWithHandler_ReturnsHandled(t *testing.T) {
  function TestExecutor_SupportedCommandWithNilHandler_ReturnsPassthrough (line 96) | func TestExecutor_SupportedCommandWithNilHandler_ReturnsPassthrough(t *t...
  function TestExecutor_NilHandlerDoesNotMaskLaterHandler (line 111) | func TestExecutor_NilHandlerDoesNotMaskLaterHandler(t *testing.T) {
  function TestExecutor_HandlerErrorIsPropagated (line 128) | func TestExecutor_HandlerErrorIsPropagated(t *testing.T) {
  function TestExecutor_SupportsBangPrefixAndCaseInsensitiveCommand (line 149) | func TestExecutor_SupportsBangPrefixAndCaseInsensitiveCommand(t *testing...
  function TestExecutor_SubCommand_RoutesToCorrectHandler (line 171) | func TestExecutor_SubCommand_RoutesToCorrectHandler(t *testing.T) {
  function TestExecutor_SubCommand_NoArg_RepliesUsage (line 196) | func TestExecutor_SubCommand_NoArg_RepliesUsage(t *testing.T) {
  function TestExecutor_SubCommand_UnknownArg_RepliesError (line 221) | func TestExecutor_SubCommand_UnknownArg_RepliesError(t *testing.T) {
  function TestExecutor_SubCommand_NilHandler_ReturnsPassthrough (line 245) | func TestExecutor_SubCommand_NilHandler_ReturnsPassthrough(t *testing.T) {

FILE: pkg/commands/handler_agents.go
  function agentsHandler (line 10) | func agentsHandler() Handler {

FILE: pkg/commands/registry.go
  type Registry (line 3) | type Registry struct
    method Definitions (line 27) | func (r *Registry) Definitions() []Definition {
    method Lookup (line 34) | func (r *Registry) Lookup(name string) (Definition, bool) {
  function NewRegistry (line 10) | func NewRegistry(defs []Definition) *Registry {
  function registerCommandName (line 46) | func registerCommandName(index map[string]int, name string, defIndex int) {

FILE: pkg/commands/registry_test.go
  function TestRegistry_Definitions_ReturnsCopy (line 5) | func TestRegistry_Definitions_ReturnsCopy(t *testing.T) {
  function TestRegistry_Lookup_MatchesByLowercaseNameAndAlias (line 24) | func TestRegistry_Lookup_MatchesByLowercaseNameAndAlias(t *testing.T) {

FILE: pkg/commands/request.go
  type Handler (line 8) | type Handler
  type Request (line 10) | type Request struct
  constant unavailableMsg (line 18) | unavailableMsg = "Command unavailable in current context."
  function parseCommandName (line 24) | func parseCommandName(input string) (string, bool) {
  function trimCommandPrefix (line 44) | func trimCommandPrefix(token string) (string, bool) {
  function HasCommandPrefix (line 55) | func HasCommandPrefix(input string) bool {
  function nthToken (line 65) | func nthToken(input string, n int) string {
  function normalizeCommandName (line 73) | func normalizeCommandName(name string) string {

FILE: pkg/commands/request_test.go
  function TestHasCommandPrefix (line 5) | func TestHasCommandPrefix(t *testing.T) {

FILE: pkg/commands/runtime.go
  type Runtime (line 8) | type Runtime struct

FILE: pkg/commands/show_list_handlers_test.go
  function TestShowListHandlers_ChannelPolicy (line 9) | func TestShowListHandlers_ChannelPolicy(t *testing.T) {
  function TestShowListHandlers_ListHandledOnAllChannels (line 59) | func TestShowListHandlers_ListHandledOnAllChannels(t *testing.T) {

FILE: pkg/config/config.go
  type FlexibleStringSlice (line 24) | type FlexibleStringSlice
    method UnmarshalJSON (line 26) | func (f *FlexibleStringSlice) UnmarshalJSON(data []byte) error {
    method UnmarshalText (line 57) | func (f *FlexibleStringSlice) UnmarshalText(text []byte) error {
  type Config (line 79) | type Config struct
    method MarshalJSON (line 105) | func (c Config) MarshalJSON() ([]byte, error) {
    method migrateChannelConfigs (line 993) | func (c *Config) migrateChannelConfigs() {
    method WorkspacePath (line 1026) | func (c *Config) WorkspacePath() string {
    method GetAPIKey (line 1030) | func (c *Config) GetAPIKey() string {
    method GetAPIBase (line 1061) | func (c *Config) GetAPIBase() string {
    method GetModelConfig (line 1094) | func (c *Config) GetModelConfig(modelName string) (*ModelConfig, error) {
    method findMatches (line 1109) | func (c *Config) findMatches(modelName string) []ModelConfig {
    method HasProvidersConfig (line 1120) | func (c *Config) HasProvidersConfig() bool {
    method ValidateModelList (line 1127) | func (c *Config) ValidateModelList() error {
  type BuildInfo (line 96) | type BuildInfo struct
  type AgentsConfig (line 128) | type AgentsConfig struct
  type AgentModelConfig (line 136) | type AgentModelConfig struct
    method UnmarshalJSON (line 141) | func (m *AgentModelConfig) UnmarshalJSON(data []byte) error {
    method MarshalJSON (line 161) | func (m AgentModelConfig) MarshalJSON() ([]byte, error) {
  type AgentConfig (line 172) | type AgentConfig struct
  type SubagentsConfig (line 182) | type SubagentsConfig struct
  type PeerMatch (line 187) | type PeerMatch struct
  type BindingMatch (line 192) | type BindingMatch struct
  type AgentBinding (line 200) | type AgentBinding struct
  type SessionConfig (line 205) | type SessionConfig struct
  type RoutingConfig (line 216) | type RoutingConfig struct
  type ToolFeedbackConfig (line 225) | type ToolFeedbackConfig struct
  type AgentDefaults (line 230) | type AgentDefaults struct
    method GetMaxMediaSize (line 255) | func (d *AgentDefaults) GetMaxMediaSize() int {
    method GetToolFeedbackMaxArgsLength (line 263) | func (d *AgentDefaults) GetToolFeedbackMaxArgsLength() int {
    method IsToolFeedbackEnabled (line 271) | func (d *AgentDefaults) IsToolFeedbackEnabled() bool {
    method GetModelName (line 277) | func (d *AgentDefaults) GetModelName() string {
  constant DefaultMaxMediaSize (line 251) | DefaultMaxMediaSize                = 20 * 1024 * 1024
  constant DefaultWeComAIBotProcessingMessage (line 252) | DefaultWeComAIBotProcessingMessage = "⏳ Processing, please wait. The res...
  type ChannelsConfig (line 284) | type ChannelsConfig struct
  type GroupTriggerConfig (line 304) | type GroupTriggerConfig struct
  type TypingConfig (line 310) | type TypingConfig struct
  type PlaceholderConfig (line 315) | type PlaceholderConfig struct
  type WhatsAppConfig (line 320) | type WhatsAppConfig struct
  type TelegramConfig (line 329) | type TelegramConfig struct
  type FeishuConfig (line 342) | type FeishuConfig struct
  type DiscordConfig (line 356) | type DiscordConfig struct
  type MaixCamConfig (line 368) | type MaixCamConfig struct
  type QQConfig (line 376) | type QQConfig struct
  type DingTalkConfig (line 388) | type DingTalkConfig struct
  type SlackConfig (line 397) | type SlackConfig struct
  type MatrixConfig (line 408) | type MatrixConfig struct
  type LINEConfig (line 422) | type LINEConfig struct
  type OneBotConfig (line 436) | type OneBotConfig struct
  type WeComConfig (line 449) | type WeComConfig struct
  type WeComAppConfig (line 463) | type WeComAppConfig struct
  type WeComAIBotConfig (line 479) | type WeComAIBotConfig struct
  type PicoConfig (line 494) | type PicoConfig struct
  type IRCConfig (line 507) | type IRCConfig struct
  type HeartbeatConfig (line 526) | type HeartbeatConfig struct
  type DevicesConfig (line 531) | type DevicesConfig struct
  type VoiceConfig (line 536) | type VoiceConfig struct
  type ProvidersConfig (line 540) | type ProvidersConfig struct
    method IsEmpty (line 570) | func (p ProvidersConfig) IsEmpty() bool {
    method MarshalJSON (line 600) | func (p ProvidersConfig) MarshalJSON() ([]byte, error) {
  type ProviderConfig (line 608) | type ProviderConfig struct
  type OpenAIProviderConfig (line 617) | type OpenAIProviderConfig struct
  type ModelConfig (line 629) | type ModelConfig struct
    method Validate (line 654) | func (c *ModelConfig) Validate() error {
  type GatewayConfig (line 664) | type GatewayConfig struct
  type ToolDiscoveryConfig (line 670) | type ToolDiscoveryConfig struct
  type ToolConfig (line 678) | type ToolConfig struct
  type BraveConfig (line 682) | type BraveConfig struct
  type TavilyConfig (line 689) | type TavilyConfig struct
  type DuckDuckGoConfig (line 697) | type DuckDuckGoConfig struct
  type PerplexityConfig (line 702) | type PerplexityConfig struct
  type SearXNGConfig (line 709) | type SearXNGConfig struct
  type GLMSearchConfig (line 715) | type GLMSearchConfig struct
  type WebToolsConfig (line 725) | type WebToolsConfig struct
  type CronToolsConfig (line 747) | type CronToolsConfig struct
  type ExecConfig (line 753) | type ExecConfig struct
  type SkillsToolsConfig (line 762) | type SkillsToolsConfig struct
  type MediaCleanupConfig (line 770) | type MediaCleanupConfig struct
  type ReadFileToolConfig (line 776) | type ReadFileToolConfig struct
  type ToolsConfig (line 781) | type ToolsConfig struct
    method IsToolEnabled (line 1242) | func (t *ToolsConfig) IsToolEnabled(name string) bool {
  type SearchCacheConfig (line 807) | type SearchCacheConfig struct
  type SkillsRegistriesConfig (line 812) | type SkillsRegistriesConfig struct
  type SkillsGithubConfig (line 816) | type SkillsGithubConfig struct
  type ClawHubRegistryConfig (line 821) | type ClawHubRegistryConfig struct
  type MCPServerConfig (line 834) | type MCPServerConfig struct
  type MCPConfig (line 858) | type MCPConfig struct
  function LoadConfig (line 865) | func LoadConfig(path string) (*Config, error) {
  function encryptPlaintextAPIKeys (line 946) | func encryptPlaintextAPIKeys(models []ModelConfig, passphrase string) ([...
  function resolveAPIKeys (line 971) | func resolveAPIKeys(models []ModelConfig, configDir string) error {
  function SaveConfig (line 1006) | func SaveConfig(path string, cfg *Config) error {
  function expandHome (line 1077) | func expandHome(path string) string {
  function MergeAPIKeys (line 1136) | func MergeAPIKeys(apiKey string, apiKeys []string) []string {
  function ExpandMultiKeyModels (line 1168) | func ExpandMultiKeyModels(models []ModelConfig) []ModelConfig {

FILE: pkg/config/config_test.go
  function mustSetupSSHKey (line 17) | func mustSetupSSHKey(t *testing.T) {
  function TestAgentModelConfig_UnmarshalString (line 26) | func TestAgentModelConfig_UnmarshalString(t *testing.T) {
  function TestAgentModelConfig_UnmarshalObject (line 39) | func TestAgentModelConfig_UnmarshalObject(t *testing.T) {
  function TestAgentModelConfig_MarshalString (line 56) | func TestAgentModelConfig_MarshalString(t *testing.T) {
  function TestAgentModelConfig_MarshalObject (line 67) | func TestAgentModelConfig_MarshalObject(t *testing.T) {
  function TestProvidersConfig_IsEmpty (line 80) | func TestProvidersConfig_IsEmpty(t *testing.T) {
  function TestAgentConfig_FullParse (line 96) | func TestAgentConfig_FullParse(t *testing.T) {
  function TestConfig_BackwardCompat_NoAgentsList (line 197) | func TestConfig_BackwardCompat_NoAgentsList(t *testing.T) {
  function TestDefaultConfig_HeartbeatEnabled (line 223) | func TestDefaultConfig_HeartbeatEnabled(t *testing.T) {
  function TestDefaultConfig_WorkspacePath (line 232) | func TestDefaultConfig_WorkspacePath(t *testing.T) {
  function TestDefaultConfig_Model (line 241) | func TestDefaultConfig_Model(t *testing.T) {
  function TestDefaultConfig_MaxTokens (line 250) | func TestDefaultConfig_MaxTokens(t *testing.T) {
  function TestDefaultConfig_MaxToolIterations (line 259) | func TestDefaultConfig_MaxToolIterations(t *testing.T) {
  function TestDefaultConfig_Temperature (line 268) | func TestDefaultConfig_Temperature(t *testing.T) {
  function TestDefaultConfig_Gateway (line 277) | func TestDefaultConfig_Gateway(t *testing.T) {
  function TestDefaultConfig_Providers (line 292) | func TestDefaultConfig_Providers(t *testing.T) {
  function TestDefaultConfig_Channels (line 307) | func TestDefaultConfig_Channels(t *testing.T) {
  function TestDefaultConfig_WebTools (line 325) | func TestDefaultConfig_WebTools(t *testing.T) {
  function TestSaveConfig_FilePermissions (line 340) | func TestSaveConfig_FilePermissions(t *testing.T) {
  function TestSaveConfig_IncludesEmptyLegacyModelField (line 364) | func TestSaveConfig_IncludesEmptyLegacyModelField(t *testing.T) {
  function TestConfig_Complete (line 384) | func TestConfig_Complete(t *testing.T) {
  function TestDefaultConfig_OpenAIWebSearchEnabled (line 413) | func TestDefaultConfig_OpenAIWebSearchEnabled(t *testing.T) {
  function TestDefaultConfig_WebPreferNativeEnabled (line 420) | func TestDefaultConfig_WebPreferNativeEnabled(t *testing.T) {
  function TestLoadConfig_WebPreferNativeDefaultsTrueWhenUnset (line 427) | func TestLoadConfig_WebPreferNativeDefaultsTrueWhenUnset(t *testing.T) {
  function TestLoadConfig_WebPreferNativeCanBeDisabled (line 443) | func TestLoadConfig_WebPreferNativeCanBeDisabled(t *testing.T) {
  function TestDefaultConfig_ExecAllowRemoteEnabled (line 459) | func TestDefaultConfig_ExecAllowRemoteEnabled(t *testing.T) {
  function TestDefaultConfig_CronAllowCommandEnabled (line 466) | func TestDefaultConfig_CronAllowCommandEnabled(t *testing.T) {
  function TestLoadConfig_OpenAIWebSearchDefaultsTrueWhenUnset (line 473) | func TestLoadConfig_OpenAIWebSearchDefaultsTrueWhenUnset(t *testing.T) {
  function TestLoadConfig_ExecAllowRemoteDefaultsTrueWhenUnset (line 489) | func TestLoadConfig_ExecAllowRemoteDefaultsTrueWhenUnset(t *testing.T) {
  function TestLoadConfig_CronAllowCommandDefaultsTrueWhenUnset (line 505) | func TestLoadConfig_CronAllowCommandDefaultsTrueWhenUnset(t *testing.T) {
  function TestLoadConfig_OpenAIWebSearchCanBeDisabled (line 521) | func TestLoadConfig_OpenAIWebSearchCanBeDisabled(t *testing.T) {
  function TestLoadConfig_WebToolsProxy (line 537) | func TestLoadConfig_WebToolsProxy(t *testing.T) {
  function TestDefaultConfig_SummarizationThresholds (line 560) | func TestDefaultConfig_SummarizationThresholds(t *testing.T) {
  function TestDefaultConfig_DMScope (line 571) | func TestDefaultConfig_DMScope(t *testing.T) {
  function TestDefaultConfig_WorkspacePath_Default (line 579) | func TestDefaultConfig_WorkspacePath_Default(t *testing.T) {
  function TestDefaultConfig_WorkspacePath_WithPicoclawHome (line 599) | func TestDefaultConfig_WorkspacePath_WithPicoclawHome(t *testing.T) {
  function TestFlexibleStringSlice_UnmarshalText (line 611) | func TestFlexibleStringSlice_UnmarshalText(t *testing.T) {
  function TestFlexibleStringSlice_UnmarshalText_EmptySliceConsistency (line 699) | func TestFlexibleStringSlice_UnmarshalText_EmptySliceConsistency(t *test...
  function TestLoadConfig_WarnsForPlaintextAPIKey (line 729) | func TestLoadConfig_WarnsForPlaintextAPIKey(t *testing.T) {
  function TestSaveConfig_EncryptsPlaintextAPIKey (line 757) | func TestSaveConfig_EncryptsPlaintextAPIKey(t *testing.T) {
  function TestLoadConfig_NoSealWithoutPassphrase (line 793) | func TestLoadConfig_NoSealWithoutPassphrase(t *testing.T) {
  function TestLoadConfig_FileRefNotSealed (line 816) | func TestLoadConfig_FileRefNotSealed(t *testing.T) {
  function TestSaveConfig_MixedKeys (line 846) | func TestSaveConfig_MixedKeys(t *testing.T) {
  function TestLoadConfig_MixedKeys_NoPassphrase (line 934) | func TestLoadConfig_MixedKeys_NoPassphrase(t *testing.T) {
  function TestSaveConfig_UsesPassphraseProvider (line 991) | func TestSaveConfig_UsesPassphraseProvider(t *testing.T) {
  function TestLoadConfig_UsesPassphraseProvider (line 1021) | func TestLoadConfig_UsesPassphraseProvider(t *testing.T) {

FILE: pkg/config/defaults.go
  function DefaultConfig (line 14) | func DefaultConfig() *Config {

FILE: pkg/config/envkeys.go
  constant EnvHome (line 18) | EnvHome = "PICOCLAW_HOME"
  constant EnvConfig (line 22) | EnvConfig = "PICOCLAW_CONFIG"
  constant EnvBuiltinSkills (line 27) | EnvBuiltinSkills = "PICOCLAW_BUILTIN_SKILLS"
  constant EnvBinary (line 32) | EnvBinary = "PICOCLAW_BINARY"
  constant EnvGatewayHost (line 36) | EnvGatewayHost = "PICOCLAW_GATEWAY_HOST"

FILE: pkg/config/migration.go
  function buildModelWithProtocol (line 16) | func buildModelWithProtocol(protocol, model string) string {
  type providerMigrationConfig (line 25) | type providerMigrationConfig struct
  function ConvertProvidersToModelList (line 37) | func ConvertProvidersToModelList(cfg *Config) []ModelConfig {
  function InheritProviderCredentials (line 514) | func InheritProviderCredentials(models []ModelConfig, providers Provider...

FILE: pkg/config/migration_test.go
  function TestConvertProvidersToModelList_OpenAI (line 13) | func TestConvertProvidersToModelList_OpenAI(t *testing.T) {
  function TestConvertProvidersToModelList_Anthropic (line 42) | func TestConvertProvidersToModelList_Anthropic(t *testing.T) {
  function TestConvertProvidersToModelList_LiteLLM (line 66) | func TestConvertProvidersToModelList_LiteLLM(t *testing.T) {
  function TestConvertProvidersToModelList_Multiple (line 93) | func TestConvertProvidersToModelList_Multiple(t *testing.T) {
  function TestConvertProvidersToModelList_Empty (line 121) | func TestConvertProvidersToModelList_Empty(t *testing.T) {
  function TestConvertProvidersToModelList_Nil (line 133) | func TestConvertProvidersToModelList_Nil(t *testing.T) {
  function TestConvertProvidersToModelList_AllProviders (line 141) | func TestConvertProvidersToModelList_AllProviders(t *testing.T) {
  function TestConvertProvidersToModelList_Proxy (line 178) | func TestConvertProvidersToModelList_Proxy(t *testing.T) {
  function TestConvertProvidersToModelList_RequestTimeout (line 201) | func TestConvertProvidersToModelList_RequestTimeout(t *testing.T) {
  function TestConvertProvidersToModelList_AuthMethod (line 222) | func TestConvertProvidersToModelList_AuthMethod(t *testing.T) {
  function TestConvertProvidersToModelList_PreservesUserModel_DeepSeek (line 242) | func TestConvertProvidersToModelList_PreservesUserModel_DeepSeek(t *test...
  function TestConvertProvidersToModelList_PreservesUserModel_OpenAI (line 267) | func TestConvertProvidersToModelList_PreservesUserModel_OpenAI(t *testin...
  function TestConvertProvidersToModelList_PreservesUserModel_Anthropic (line 291) | func TestConvertProvidersToModelList_PreservesUserModel_Anthropic(t *tes...
  function TestConvertProvidersToModelList_PreservesUserModel_Qwen (line 315) | func TestConvertProvidersToModelList_PreservesUserModel_Qwen(t *testing....
  function TestConvertProvidersToModelList_UsesDefaultWhenNoUserModel (line 339) | func TestConvertProvidersToModelList_UsesDefaultWhenNoUserModel(t *testi...
  function TestConvertProvidersToModelList_MultipleProviders_PreservesUserModel (line 364) | func TestConvertProvidersToModelList_MultipleProviders_PreservesUserMode...
  function TestConvertProvidersToModelList_ProviderNameAliases (line 399) | func TestConvertProvidersToModelList_ProviderNameAliases(t *testing.T) {
  function TestConvertProvidersToModelList_NoProviderField_SingleProvider (line 464) | func TestConvertProvidersToModelList_NoProviderField_SingleProvider(t *t...
  function TestConvertProvidersToModelList_NoProviderField_MultipleProviders (line 498) | func TestConvertProvidersToModelList_NoProviderField_MultipleProviders(t...
  function TestConvertProvidersToModelList_NoProviderField_NoModel (line 533) | func TestConvertProvidersToModelList_NoProviderField_NoModel(t *testing....
  function TestBuildModelWithProtocol_NoPrefix (line 561) | func TestBuildModelWithProtocol_NoPrefix(t *testing.T) {
  function TestBuildModelWithProtocol_AlreadyHasPrefix (line 568) | func TestBuildModelWithProtocol_AlreadyHasPrefix(t *testing.T) {
  function TestBuildModelWithProtocol_DifferentPrefix (line 575) | func TestBuildModelWithProtocol_DifferentPrefix(t *testing.T) {
  function TestConvertProvidersToModelList_LegacyModelWithProtocolPrefix (line 587) | func TestConvertProvidersToModelList_LegacyModelWithProtocolPrefix(t *te...
  function TestInheritProviderCredentials_FillsMissingAPIKey (line 619) | func TestInheritProviderCredentials_FillsMissingAPIKey(t *testing.T) {
  function TestInheritProviderCredentials_ExplicitValuesTakePrecedence (line 640) | func TestInheritProviderCredentials_ExplicitValuesTakePrecedence(t *test...
  function TestInheritProviderCredentials_MultipleModels (line 668) | func TestInheritProviderCredentials_MultipleModels(t *testing.T) {
  function TestInheritProviderCredentials_NoMatchingProvider (line 706) | func TestInheritProviderCredentials_NoMatchingProvider(t *testing.T) {
  function TestInheritProviderCredentials_EmptyProviders (line 722) | func TestInheritProviderCredentials_EmptyProviders(t *testing.T) {
  function TestInheritProviderCredentials_InheritsRequestTimeout (line 736) | func TestInheritProviderCredentials_InheritsRequestTimeout(t *testing.T) {

FILE: pkg/config/model_config_test.go
  function TestGetModelConfig_Found (line 15) | func TestGetModelConfig_Found(t *testing.T) {
  function TestGetModelConfig_NotFound (line 32) | func TestGetModelConfig_NotFound(t *testing.T) {
  function TestGetModelConfig_EmptyList (line 45) | func TestGetModelConfig_EmptyList(t *testing.T) {
  function TestGetModelConfig_RoundRobin (line 56) | func TestGetModelConfig_RoundRobin(t *testing.T) {
  function TestGetModelConfig_RoundRobinStartsFromFirstMatch (line 83) | func TestGetModelConfig_RoundRobinStartsFromFirstMatch(t *testing.T) {
  function TestGetModelConfig_Concurrent (line 113) | func TestGetModelConfig_Concurrent(t *testing.T) {
  function TestAgentDefaults_GetModelName_BackwardCompat (line 146) | func TestAgentDefaults_GetModelName_BackwardCompat(t *testing.T) {
  function TestAgentDefaults_JSON_BackwardCompat (line 178) | func TestAgentDefaults_JSON_BackwardCompat(t *testing.T) {
  function TestFullConfig_JSON_BackwardCompat (line 214) | func TestFullConfig_JSON_BackwardCompat(t *testing.T) {
  function TestModelConfig_Validate (line 277) | func TestModelConfig_Validate(t *testing.T) {
  function TestConfig_ValidateModelList (line 322) | func TestConfig_ValidateModelList(t *testing.T) {
  function TestModelConfig_RequestTimeoutParsing (line 397) | func TestModelConfig_RequestTimeoutParsing(t *testing.T) {
  function TestModelConfig_RequestTimeoutDefaultZeroValue (line 415) | func TestModelConfig_RequestTimeoutDefaultZeroValue(t *testing.T) {

FILE: pkg/config/multikey_test.go
  function TestExpandMultiKeyModels_SingleKey (line 7) | func TestExpandMultiKeyModels_SingleKey(t *testing.T) {
  function TestExpandMultiKeyModels_APIKeysOnly (line 35) | func TestExpandMultiKeyModels_APIKeysOnly(t *testing.T) {
  function TestExpandMultiKeyModels_APIKeyAndAPIKeys (line 89) | func TestExpandMultiKeyModels_APIKeyAndAPIKeys(t *testing.T) {
  function TestExpandMultiKeyModels_WithExistingFallbacks (line 116) | func TestExpandMultiKeyModels_WithExistingFallbacks(t *testing.T) {
  function TestExpandMultiKeyModels_EmptyAPIKeys (line 143) | func TestExpandMultiKeyModels_EmptyAPIKeys(t *testing.T) {
  function TestExpandMultiKeyModels_Deduplication (line 165) | func TestExpandMultiKeyModels_Deduplication(t *testing.T) {
  function TestExpandMultiKeyModels_PreservesOtherFields (line 191) | func TestExpandMultiKeyModels_PreservesOtherFields(t *testing.T) {
  function TestMergeAPIKeys (line 239) | func TestMergeAPIKeys(t *testing.T) {

FILE: pkg/config/version.go
  function FormatVersion (line 23) | func FormatVersion() string {
  function FormatBuildInfo (line 32) | func FormatBuildInfo() (string, string) {
  function GetVersion (line 42) | func GetVersion() string {

FILE: pkg/config/version_test.go
  function TestFormatVersion_NoGitCommit (line 10) | func TestFormatVersion_NoGitCommit(t *testing.T) {
  function TestFormatVersion_WithGitCommit (line 20) | func TestFormatVersion_WithGitCommit(t *testing.T) {
  function TestFormatBuildInfo_UsesBuildTimeAndGoVersion_WhenSet (line 30) | func TestFormatBuildInfo_UsesBuildTimeAndGoVersion_WhenSet(t *testing.T) {
  function TestFormatBuildInfo_EmptyBuildTime_ReturnsEmptyBuild (line 43) | func TestFormatBuildInfo_EmptyBuildTime_ReturnsEmptyBuild(t *testing.T) {
  function TestFormatBuildInfo_EmptyGoVersion_FallsBackToRuntimeVersion (line 56) | func TestFormatBuildInfo_EmptyGoVersion_FallsBackToRuntimeVersion(t *tes...
  function TestGetVersion (line 69) | func TestGetVersion(t *testing.T) {
  function TestGetVersion_Custom (line 77) | func TestGetVersion_Custom(t *testing.T) {
  function TestVersion_DefaultIsDev (line 85) | func TestVersion_DefaultIsDev(t *testing.T) {

FILE: pkg/constants/channels.go
  function IsInternalChannel (line 13) | func IsInternalChannel(channel string) bool {

FILE: pkg/credential/credential.go
  constant PassphraseEnvVar (line 45) | PassphraseEnvVar = "PICOCLAW_KEY_PASSPHRASE"
  constant SSHKeyPathEnvVar (line 71) | SSHKeyPathEnvVar = "PICOCLAW_SSH_KEY_PATH"
  constant picoclawHome (line 75) | picoclawHome = "PICOCLAW_HOME"
  constant fileScheme (line 78) | fileScheme = "file://"
  constant encScheme (line 79) | encScheme  = "enc://"
  constant hkdfInfo (line 80) | hkdfInfo   = "picoclaw-credential-v1"
  constant saltLen (line 81) | saltLen    = 16
  constant nonceLen (line 82) | nonceLen   = 12
  constant keyLen (line 83) | keyLen     = 32
  type Resolver (line 88) | type Resolver struct
    method Resolve (line 110) | func (r *Resolver) Resolve(raw string) (string, error) {
  function NewResolver (line 95) | func NewResolver(configDir string) *Resolver {
  function resolveEncrypted (line 156) | func resolveEncrypted(raw string) (string, error) {
  function Encrypt (line 203) | func Encrypt(passphrase, sshKeyPath, plaintext string) (string, error) {
  function isWithinDir (line 242) | func isWithinDir(path, dir string) bool {
  function allowedSSHKeyPath (line 251) | func allowedSSHKeyPath(path string) bool {
  function deriveKey (line 286) | func deriveKey(passphrase, sshKeyPath string, salt []byte) ([]byte, erro...
  function pickSSHKeyPath (line 322) | func pickSSHKeyPath(override string) string {
  function findDefaultSSHKey (line 333) | func findDefaultSSHKey() string {

FILE: pkg/credential/credential_test.go
  function TestResolve_PlainKey (line 11) | func TestResolve_PlainKey(t *testing.T) {
  function TestResolve_FileKey_Success (line 22) | func TestResolve_FileKey_Success(t *testing.T) {
  function TestResolve_FileKey_NotFound (line 39) | func TestResolve_FileKey_NotFound(t *testing.T) {
  function TestResolve_FileKey_Empty (line 47) | func TestResolve_FileKey_Empty(t *testing.T) {
  function TestResolve_EncKey_RoundTrip (line 62) | func TestResolve_EncKey_RoundTrip(t *testing.T) {
  function TestResolve_EncKey_WithSSHKey (line 92) | func TestResolve_EncKey_WithSSHKey(t *testing.T) {
  function TestResolve_EncKey_NoPassphrase (line 121) | func TestResolve_EncKey_NoPassphrase(t *testing.T) {
  function TestResolve_EncKey_BadCiphertext (line 143) | func TestResolve_EncKey_BadCiphertext(t *testing.T) {
  function TestResolve_EncKey_PayloadTooShort (line 154) | func TestResolve_EncKey_PayloadTooShort(t *testing.T) {
  function TestResolve_EncKey_WrongPassphrase (line 167) | func TestResolve_EncKey_WrongPassphrase(t *testing.T) {
  function TestEncrypt_EmptyPassphrase (line 189) | func TestEncrypt_EmptyPassphrase(t *testing.T) {
  function TestDeriveKey_SSHKeyNotFound (line 196) | func TestDeriveKey_SSHKeyNotFound(t *testing.T) {
  function TestResolve_FileRef_PathTraversal (line 226) | func TestResolve_FileRef_PathTraversal(t *testing.T) {
  function TestResolve_FileRef_withinConfigDir (line 251) | func TestResolve_FileRef_withinConfigDir(t *testing.T) {
  function TestEncrypt_SSHKeyOutsideAllowedDirs (line 268) | func TestEncrypt_SSHKeyOutsideAllowedDirs(t *testing.T) {

FILE: pkg/credential/keygen.go
  function DefaultSSHKeyPath (line 16) | func DefaultSSHKeyPath() (string, error) {
  function GenerateSSHKey (line 28) | func GenerateSSHKey(path string) error {

FILE: pkg/credential/keygen_test.go
  function TestGenerateSSHKey_CreatesFiles (line 13) | func TestGenerateSSHKey_CreatesFiles(t *testing.T) {
  function TestGenerateSSHKey_OverwritesExisting (line 76) | func TestGenerateSSHKey_OverwritesExisting(t *testing.T) {
  function TestGenerateSSHKey_CreatesDirectory (line 103) | func TestGenerateSSHKey_CreatesDirectory(t *testing.T) {

FILE: pkg/credential/store.go
  type SecureStore (line 10) | type SecureStore struct
    method SetString (line 20) | func (s *SecureStore) SetString(passphrase string) {
    method Get (line 29) | func (s *SecureStore) Get() string {
    method IsSet (line 37) | func (s *SecureStore) IsSet() bool {
    method Clear (line 42) | func (s *SecureStore) Clear() {
  function NewSecureStore (line 15) | func NewSecureStore() *SecureStore {

FILE: pkg/credential/store_test.go
  function TestSecureStore_SetGet (line 8) | func TestSecureStore_SetGet(t *testing.T) {
  function TestSecureStore_Clear (line 23) | func TestSecureStore_Clear(t *testing.T) {
  function TestSecureStore_SetOverwrites (line 36) | func TestSecureStore_SetOverwrites(t *testing.T) {
  function TestSecureStore_EmptyPassphrase (line 46) | func TestSecureStore_EmptyPassphrase(t *testing.T) {
  function TestSecureStore_ConcurrentSetGet (line 55) | func TestSecureStore_ConcurrentSetGet(t *testing.T) {

FILE: pkg/cron/service.go
  type CronSchedule (line 18) | type CronSchedule struct
  type CronPayload (line 26) | type CronPayload struct
  type CronJobState (line 35) | type CronJobState struct
  type CronJob (line 42) | type CronJob struct
  type CronStore (line 54) | type CronStore struct
  type JobHandler (line 59) | type JobHandler
  type CronService (line 61) | type CronService struct
    method Start (line 84) | func (cs *CronService) Start() error {
    method Stop (line 111) | func (cs *CronService) Stop() {
    method runLoop (line 126) | func (cs *CronService) runLoop(stopChan chan struct{}) {
    method checkJobs (line 173) | func (cs *CronService) checkJobs() {
    method executeJobByID (line 215) | func (cs *CronService) executeJobByID(jobID string) {
    method computeNextRun (line 304) | func (cs *CronService) computeNextRun(schedule *CronSchedule, nowMS in...
    method notify (line 339) | func (cs *CronService) notify() {
    method recomputeNextRuns (line 347) | func (cs *CronService) recomputeNextRuns() {
    method getNextWakeMS (line 357) | func (cs *CronService) getNextWakeMS() *int64 {
    method Load (line 369) | func (cs *CronService) Load() error {
    method SetOnJob (line 375) | func (cs *CronService) SetOnJob(handler JobHandler) {
    method loadStore (line 381) | func (cs *CronService) loadStore() error {
    method saveStoreUnsafe (line 398) | func (cs *CronService) saveStoreUnsafe() error {
    method AddJob (line 408) | func (cs *CronService) AddJob(
    method UpdateJob (line 453) | func (cs *CronService) UpdateJob(job *CronJob) error {
    method RemoveJob (line 470) | func (cs *CronService) RemoveJob(jobID string) bool {
    method removeJobUnsafe (line 477) | func (cs *CronService) removeJobUnsafe(jobID string) bool {
    method EnableJob (line 499) | func (cs *CronService) EnableJob(jobID string, enabled bool) *CronJob {
    method ListJobs (line 528) | func (cs *CronService) ListJobs(includeDisabled bool) []CronJob {
    method Status (line 546) | func (cs *CronService) Status() map[string]any {
  function NewCronService (line 72) | func NewCronService(storePath string, onJob JobHandler) *CronService {
  function generateID (line 564) | func generateID() string {

FILE: pkg/cron/service_test.go
  function TestSaveStore_FilePermissions (line 13) | func TestSaveStore_FilePermissions(t *testing.T) {
  function int64Ptr (line 39) | func int64Ptr(v int64) *int64 {
  function setupService (line 43) | func setupService(handler JobHandler) (*CronService, string) {
  function TestCronService_CRUD (line 49) | func TestCronService_CRUD(t *testing.T) {
  function TestCronService_ComputeNextRun (line 86) | func TestCronService_ComputeNextRun(t *testing.T) {
  function TestCronService_ExecutionFlow (line 115) | func TestCronService_ExecutionFlow(t *testing.T) {
  function TestCronService_PersistenceIntegrity (line 163) | func TestCronService_PersistenceIntegrity(t *testing.T) {
  function TestCronService_ConcurrentAccess (line 197) | func TestCronService_ConcurrentAccess(t *testing.T) {

FILE: pkg/devices/events/events.go
  type EventSource (line 5) | type EventSource interface
  type Action (line 11) | type Action
  constant ActionAdd (line 14) | ActionAdd    Action = "add"
  constant ActionRemove (line 15) | ActionRemove Action = "remove"
  constant ActionChange (line 16) | ActionChange Action = "change"
  type Kind (line 19) | type Kind
  constant KindUSB (line 22) | KindUSB       Kind = "usb"
  constant KindBluetooth (line 23) | KindBluetooth Kind = "bluetooth"
  constant KindPCI (line 24) | KindPCI       Kind = "pci"
  constant KindGeneric (line 25) | KindGeneric   Kind = "generic"
  type DeviceEvent (line 28) | type DeviceEvent struct
    method FormatMessage (line 39) | func (e *DeviceEvent) FormatMessage() string {

FILE: pkg/devices/service.go
  type Service (line 17) | type Service struct
    method SetBus (line 47) | func (s *Service) SetBus(msgBus *bus.MessageBus) {
    method Start (line 53) | func (s *Service) Start(ctx context.Context) error {
    method Stop (line 83) | func (s *Service) Stop() {
    method handleEvents (line 99) | func (s *Service) handleEvents(kind events.Kind, eventCh <-chan *event...
    method sendNotification (line 108) | func (s *Service) sendNotification(ev *events.DeviceEvent) {
  type Config (line 27) | type Config struct
  function NewService (line 33) | func NewService(cfg Config, stateMgr *state.Manager) *Service {
  function parseLastChannel (line 146) | func parseLastChannel(lastChannel string) (platform, userID string) {

FILE: pkg/devices/sources/usb_linux.go
  type USBMonitor (line 37) | type USBMonitor struct
    method Kind (line 46) | func (m *USBMonitor) Kind() events.Kind {
    method Start (line 50) | func (m *USBMonitor) Start(ctx context.Context) (<-chan *events.Device...
    method Stop (line 125) | func (m *USBMonitor) Stop() error {
  function NewUSBMonitor (line 42) | func NewUSBMonitor() *USBMonitor {
  function parseUSBEvent (line 135) | func parseUSBEvent(action string, props map[string]string) *events.Devic...

FILE: pkg/devices/sources/usb_stub.go
  type USBMonitor (line 11) | type USBMonitor struct
    method Kind (line 17) | func (m *USBMonitor) Kind() events.Kind {
    method Start (line 21) | func (m *USBMonitor) Start(ctx context.Context) (<-chan *events.Device...
    method Stop (line 27) | func (m *USBMonitor) Stop() error {
  function NewUSBMonitor (line 13) | func NewUSBMonitor() *USBMonitor {

FILE: pkg/fileutil/file.go
  function WriteFileAtomic (line 52) | func WriteFileAtomic(path string, data []byte, perm os.FileMode) error {

FILE: pkg/gateway/gateway.go
  constant serviceShutdownTimeout (line 46) | serviceShutdownTimeout  = 30 * time.Second
  constant providerReloadTimeout (line 47) | providerReloadTimeout   = 30 * time.Second
  constant gracefulShutdownTimeout (line 48) | gracefulShutdownTimeout = 15 * time.Second
  type services (line 51) | type services struct
  type startupBlockedProvider (line 62) | type startupBlockedProvider struct
    method Chat (line 66) | func (p *startupBlockedProvider) Chat(
    method GetDefaultModel (line 76) | func (p *startupBlockedProvider) GetDefaultModel() string {
  function Run (line 81) | func Run(debug bool, configPath string, allowEmptyStartup bool) error {
  function executeReload (line 199) | func executeReload(
  function createStartupProvider (line 212) | func createStartupProvider(
  function setupAndStartServices (line 229) | func setupAndStartServices(
  function stopAndCleanupServices (line 327) | func stopAndCleanupServices(runningServices *services, shutdownTimeout t...
  function shutdownGateway (line 351) | func shutdownGateway(
  function handleConfigReload (line 369) | func handleConfigReload(
  function restartServices (line 431) | func restartServices(
  function setupConfigWatcherPolling (line 526) | func setupConfigWatcherPolling(configPath string, debug bool) (chan *con...
  function getFileModTime (line 592) | func getFileModTime(path string) time.Time {
  function getFileSize (line 600) | func getFileSize(path string) int64 {
  function setupCronTool (line 608) | func setupCronTool(
  function createHeartbeatHandler (line 641) | func createHeartbeatHandler(agentLoop *agent.AgentLoop) func(prompt, cha...

FILE: pkg/health/server.go
  type Server (line 14) | type Server struct
    method Start (line 60) | func (s *Server) Start() error {
    method StartContext (line 67) | func (s *Server) StartContext(ctx context.Context) error {
    method Stop (line 85) | func (s *Server) Stop(ctx context.Context) error {
    method SetReady (line 92) | func (s *Server) SetReady(ready bool) {
    method RegisterCheck (line 98) | func (s *Server) RegisterCheck(name string, checkFn func() (bool, stri...
    method SetReloadFunc (line 112) | func (s *Server) SetReloadFunc(fn func() error) {
    method reloadHandler (line 118) | func (s *Server) reloadHandler(w http.ResponseWriter, r *http.Request) {
    method healthHandler (line 149) | func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) {
    method readyHandler (line 163) | func (s *Server) readyHandler(w http.ResponseWriter, r *http.Request) {
    method RegisterOnMux (line 203) | func (s *Server) RegisterOnMux(mux *http.ServeMux) {
  type Check (line 23) | type Check struct
  type StatusResponse (line 30) | type StatusResponse struct
  function NewServer (line 37) | func NewServer(host string, port int) *Server {
  function statusString (line 209) | func statusString(ok bool) string {

FILE: pkg/heartbeat/service.go
  constant minIntervalMinutes (line 27) | minIntervalMinutes     = 5
  constant defaultIntervalMinutes (line 28) | defaultIntervalMinutes = 30
  type HeartbeatHandler (line 34) | type HeartbeatHandler
  type HeartbeatService (line 37) | type HeartbeatService struct
    method SetBus (line 68) | func (hs *HeartbeatService) SetBus(msgBus *bus.MessageBus) {
    method SetHandler (line 75) | func (hs *HeartbeatService) SetHandler(handler HeartbeatHandler) {
    method Start (line 82) | func (hs *HeartbeatService) Start() error {
    method Stop (line 107) | func (hs *HeartbeatService) Stop() {
    method IsRunning (line 121) | func (hs *HeartbeatService) IsRunning() bool {
    method runLoop (line 128) | func (hs *HeartbeatService) runLoop(stopChan chan struct{}) {
    method executeHeartbeat (line 148) | func (hs *HeartbeatService) executeHeartbeat() {
    method buildPrompt (line 221) | func (hs *HeartbeatService) buildPrompt() string {
    method createDefaultHeartbeatTemplate (line 253) | func (hs *HeartbeatService) createDefaultHeartbeatTemplate() {
    method sendResponse (line 288) | func (hs *HeartbeatService) sendResponse(response string) {
    method parseLastChannel (line 325) | func (hs *HeartbeatService) parseLastChannel(lastChannel string) (plat...
    method logInfof (line 349) | func (hs *HeartbeatService) logInfof(format string, args ...any) {
    method logErrorf (line 354) | func (hs *HeartbeatService) logErrorf(format string, args ...any) {
    method logf (line 359) | func (hs *HeartbeatService) logf(level, format string, args ...any) {
  function NewHeartbeatService (line 49) | func NewHeartbeatService(workspace string, intervalMinutes int, enabled ...

FILE: pkg/heartbeat/service_test.go
  function TestExecuteHeartbeat_Async (line 12) | func TestExecuteHeartbeat_Async(t *testing.T) {
  function TestExecuteHeartbeat_ResultLogging (line 50) | func TestExecuteHeartbeat_ResultLogging(t *testing.T) {
  function TestHeartbeatService_StartStop (line 110) | func TestHeartbeatService_StartStop(t *testing.T) {
  function TestHeartbeatService_Disabled (line 129) | func TestHeartbeatService_Disabled(t *testing.T) {
  function TestExecuteHeartbeat_NilResult (line 146) | func TestExecuteHeartbeat_NilResult(t *testing.T) {
  function TestLogPath (line 168) | func TestLogPath(t *testing.T) {
  function TestHeartbeatFilePath (line 188) | func TestHeartbeatFilePath(t *testing.T) {

FILE: pkg/identity/identity.go
  function BuildCanonicalID (line 14) | func BuildCanonicalID(platform, platformID string) string {
  function ParseCanonicalID (line 25) | func ParseCanonicalID(canonical string) (platform, id string, ok bool) {
  function MatchAllowed (line 41) | func MatchAllowed(sender bus.SenderInfo, allowed string) bool {
  function isNumeric (line 98) | func isNumeric(s string) bool {

FILE: pkg/identity/identity_test.go
  function TestBuildCanonicalID (line 9) | func TestBuildCanonicalID(t *testing.T) {
  function TestParseCanonicalID (line 32) | func TestParseCanonicalID(t *testing.T) {
  function TestMatchAllowed (line 58) | func TestMatchAllowed(t *testing.T) {
  function TestIsNumeric (line 230) | func TestIsNumeric(t *testing.T) {

FILE: pkg/logger/logger.go
  constant DEBUG (line 18) | DEBUG = zerolog.DebugLevel
  constant INFO (line 19) | INFO  = zerolog.InfoLevel
  constant WARN (line 20) | WARN  = zerolog.WarnLevel
  constant ERROR (line 21) | ERROR = zerolog.ErrorLevel
  constant FATAL (line 22) | FATAL = zerolog.FatalLevel
  function init (line 42) | func init() {
  function formatFieldValue (line 59) | func formatFieldValue(i any) string {
  function SetLevel (line 90) | func SetLevel(level LogLevel) {
  function SetConsoleLevel (line 97) | func SetConsoleLevel(level LogLevel) {
  function GetLevel (line 103) | func GetLevel() LogLevel {
  function EnableFileLogging (line 109) | func EnableFileLogging(filePath string) error {
  function DisableFileLogging (line 132) | func DisableFileLogging() {
  function getCallerSkip (line 143) | func getCallerSkip() int {
  function getEvent (line 174) | func getEvent(logger zerolog.Logger, level LogLevel) *zerolog.Event {
  function logMessage (line 191) | func logMessage(level LogLevel, component string, message string, fields...
  function appendFields (line 225) | func appendFields(event *zerolog.Event, fields map[string]any) {
  function Debug (line 245) | func Debug(message string) {
  function DebugC (line 249) | func DebugC(component string, message string) {
  function Debugf (line 253) | func Debugf(message string, ss ...any) {
  function DebugF (line 257) | func DebugF(message string, fields map[string]any) {
  function DebugCF (line 261) | func DebugCF(component string, message string, fields map[string]any) {
  function Info (line 265) | func Info(message string) {
  function InfoC (line 269) | func InfoC(component string, message string) {
  function InfoF (line 273) | func InfoF(message string, fields map[string]any) {
  function Infof (line 277) | func Infof(message string, ss ...any) {
  function InfoCF (line 281) | func InfoCF(component string, message string, fields map[string]any) {
  function Warn (line 285) | func Warn(message string) {
  function WarnC (line 289) | func WarnC(component string, message string) {
  function WarnF (line 293) | func WarnF(message string, fields map[string]any) {
  function WarnCF (line 297) | func WarnCF(component string, message string, fields map[string]any) {
  function Error (line 301) | func Error(message string) {
  function ErrorC (line 305) | func ErrorC(component string, message string) {
  function Errorf (line 309) | func Errorf(message string, ss ...any) {
  function ErrorF (line 313) | func ErrorF(message string, fields map[string]any) {
  function ErrorCF (line 317) | func ErrorCF(component string, message string, fields map[string]any) {
  function Fatal (line 321) | func Fatal(message string) {
  function FatalC (line 325) | func FatalC(component string, message string) {
  function Fatalf (line 329) | func Fatalf(message string, ss ...any) {
  function FatalF (line 333) | func FatalF(message string, fields map[string]any) {
  function FatalCF (line 337) | func FatalCF(component string, message string, fields map[string]any) {

FILE: pkg/logger/logger_3rd_party.go
  function maskSecrets (line 16) | func maskSecrets(s string) string {
  type Logger (line 21) | type Logger struct
    method Debug (line 27) | func (b *Logger) Debug(v ...any) {
    method Info (line 32) | func (b *Logger) Info(v ...any) {
    method Warn (line 37) | func (b *Logger) Warn(v ...any) {
    method Error (line 42) | func (b *Logger) Error(v ...any) {
    method Debugf (line 47) | func (b *Logger) Debugf(format string, v ...any) {
    method Infof (line 52) | func (b *Logger) Infof(format string, v ...any) {
    method Warnf (line 57) | func (b *Logger) Warnf(format string, v ...any) {
    method Warningf (line 62) | func (b *Logger) Warningf(format string, v ...any) {
    method Errorf (line 67) | func (b *Logger) Errorf(format string, v ...any) {
    method Fatalf (line 72) | func (b *Logger) Fatalf(format string, v ...any) {
    method Log (line 84) | func
Condensed preview — 695 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (4,513K chars).
[
  {
    "path": ".dockerignore",
    "chars": 81,
    "preview": ".git\n.gitignore\nbuild/\n.picoclaw/\nconfig/\n.env\n.env.example\n*.md\nLICENSE\nassets/\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 554,
    "preview": "---\nname: Bug report\nabout: Report a bug or unexpected behavior\ntitle: \"[BUG]\"\nlabels: bug\nassignees: ''\n\n---\n\n## Quick "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 420,
    "preview": "---\nname: Feature request\nabout: Suggest a new idea or improvement\ntitle: \"[Feature]\"\nlabels: enhancement\nassignees: ''\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/general-task---todo.md",
    "chars": 489,
    "preview": "---\nname: General Task / Todo\nabout: A specific piece of work like doc, refactoring, or maintenance.\ntitle: \"[Task]\"\nlab"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 516,
    "preview": "version: 2\r\n\r\nupdates:\r\n\r\n  # Go dependencies (entire repo)\r\n  - package-ecosystem: \"gomod\"\r\n    directory: \"/\"\r\n    sch"
  },
  {
    "path": ".github/pull_request_template.md",
    "chars": 1300,
    "preview": "## 📝 Description\n\n<!-- Please briefly describe the changes and purpose of this PR -->\n\n## 🗣️ Type of Change\n- [ ] 🐞 Bug "
  },
  {
    "path": ".github/workflows/build.yml",
    "chars": 316,
    "preview": "name: build\n\non:\n  push:\n    branches: [ \"main\" ]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Ch"
  },
  {
    "path": ".github/workflows/docker-build.yml",
    "chars": 2541,
    "preview": "name: 🐳 Build & Push Docker Image\n\non:\n  workflow_call:\n    inputs:\n      tag:\n        description: \"Release tag\"\n      "
  },
  {
    "path": ".github/workflows/nightly.yml",
    "chars": 4492,
    "preview": "name: Nightly Build\n\non:\n  schedule:\n    - cron: '0 0 * * *'\n  workflow_dispatch:\n\npermissions:\n  contents: read\n\njobs:\n"
  },
  {
    "path": ".github/workflows/pr.yml",
    "chars": 1196,
    "preview": "name: PR\n\non:\n  pull_request: { }\n\njobs:\n  lint:\n    name: Linter\n    runs-on: ubuntu-latest\n    steps:\n      - name: Ch"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 3596,
    "preview": "name: Create Tag and Release\n\non:\n  workflow_dispatch:\n    inputs:\n      tag:\n        description: \"Release tag (require"
  },
  {
    "path": ".github/workflows/upload-tos.yml",
    "chars": 1542,
    "preview": "name: Upload to Volcengine TOS\n\non:\n  workflow_dispatch:\n    inputs:\n      tag:\n        description: \"Release tag to dow"
  },
  {
    "path": ".gitignore",
    "chars": 687,
    "preview": "# Binaries\n# Go build artifacts\nbin/\nbuild/\n*.exe\n*.dll\n*.so\n*.dylib\n*.test\n*.out\n/picoclaw\n/picoclaw-test\ncmd/**/worksp"
  },
  {
    "path": ".golangci.yaml",
    "chars": 3442,
    "preview": "version: \"2\"\n\nlinters:\n  default: all\n  disable:\n    # TODO: Tweak for current project needs\n    - containedctx\n    - cy"
  },
  {
    "path": ".goreleaser.yaml",
    "chars": 5730,
    "preview": "# yaml-language-server: $schema=https://goreleaser.com/static/schema.json\n# vim: set ts=2 sw=2 tw=0 fo=cnqoj\nversion: 2\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 11686,
    "preview": "# Contributing to PicoClaw\n\nThank you for your interest in contributing to PicoClaw! This project is a community-driven "
  },
  {
    "path": "CONTRIBUTING.zh.md",
    "chars": 5890,
    "preview": "# 参与贡献 PicoClaw\n\n感谢你对 PicoClaw 的关注!本项目是一个社区驱动的开源项目,目标是构建 轻量灵活,人人可用 的个人AI助手。我们欢迎一切形式的贡献:Bug 修复、新功能、文档、翻译和测试。\n\nPicoClaw 本身"
  },
  {
    "path": "LICENSE",
    "chars": 1078,
    "preview": "MIT License\n\nCopyright (c) 2026 PicoClaw contributors\n\nPermission is hereby granted, free of charge, to any person obtai"
  },
  {
    "path": "Makefile",
    "chars": 13681,
    "preview": ".PHONY: all build install uninstall clean help test\n\n# Build variables\nBINARY_NAME=picoclaw\nBUILD_DIR=build\nCMD_DIR=cmd/"
  },
  {
    "path": "README.fr.md",
    "chars": 14082,
    "preview": "<div align=\"center\">\n  <img src=\"assets/logo.webp\" alt=\"PicoClaw\" width=\"512\">\n\n  <h1>PicoClaw : Assistant IA Ultra-Effi"
  },
  {
    "path": "README.id.md",
    "chars": 13058,
    "preview": "<div align=\"center\">\n  <img src=\"assets/logo.webp\" alt=\"PicoClaw\" width=\"512\">\n\n  <h1>PicoClaw: Asisten AI Super Ringan "
  },
  {
    "path": "README.it.md",
    "chars": 13565,
    "preview": "<div align=\"center\">\n  <img src=\"assets/logo.webp\" alt=\"PicoClaw\" width=\"512\">\n\n  <h1>PicoClaw: Assistente IA Ultra-Effi"
  },
  {
    "path": "README.ja.md",
    "chars": 10208,
    "preview": "<div align=\"center\">\n  <img src=\"assets/logo.webp\" alt=\"PicoClaw\" width=\"512\">\n\n  <h1>PicoClaw: Go で書かれた超効率 AI アシスタント</h"
  },
  {
    "path": "README.md",
    "chars": 24921,
    "preview": "<div align=\"center\">\n  <img src=\"assets/logo.webp\" alt=\"PicoClaw\" width=\"512\">\n\n  <h1>PicoClaw: Ultra-Efficient AI Assis"
  },
  {
    "path": "README.pt-br.md",
    "chars": 13494,
    "preview": "<div align=\"center\">\n  <img src=\"assets/logo.webp\" alt=\"PicoClaw\" width=\"512\">\n\n  <h1>PicoClaw: Assistente de IA Ultra-E"
  },
  {
    "path": "README.vi.md",
    "chars": 12887,
    "preview": "<div align=\"center\">\n  <img src=\"assets/logo.webp\" alt=\"PicoClaw\" width=\"512\">\n\n  <h1>PicoClaw: Trợ lý AI Siêu Nhẹ viết "
  },
  {
    "path": "README.zh.md",
    "chars": 9218,
    "preview": "<div align=\"center\">\n<img src=\"assets/logo.webp\" alt=\"PicoClaw\" width=\"512\">\n\n<h1>PicoClaw: 基于Go语言的超高效 AI 助手</h1>\n\n<h3>$"
  },
  {
    "path": "ROADMAP.md",
    "chars": 5601,
    "preview": "\n# 🦐 PicoClaw Roadmap\n\n> **Vision**: To build the ultimate lightweight, secure, and fully autonomous AI Agent infrastruc"
  },
  {
    "path": "cmd/picoclaw/internal/agent/command.go",
    "chars": 748,
    "preview": "package agent\n\nimport (\n\t\"github.com/spf13/cobra\"\n)\n\nfunc NewAgentCommand() *cobra.Command {\n\tvar (\n\t\tmessage    string\n"
  },
  {
    "path": "cmd/picoclaw/internal/agent/command_test.go",
    "chars": 736,
    "preview": "package agent\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc"
  },
  {
    "path": "cmd/picoclaw/internal/agent/helpers.go",
    "chars": 3781,
    "preview": "package agent\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/ergochat/readli"
  },
  {
    "path": "cmd/picoclaw/internal/auth/command.go",
    "chars": 390,
    "preview": "package auth\n\nimport \"github.com/spf13/cobra\"\n\nfunc NewAuthCommand() *cobra.Command {\n\tcmd := &cobra.Command{\n\t\tUse:   \""
  },
  {
    "path": "cmd/picoclaw/internal/auth/command_test.go",
    "chars": 1151,
    "preview": "package auth\n\nimport (\n\t\"slices\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require"
  },
  {
    "path": "cmd/picoclaw/internal/auth/helpers.go",
    "chars": 13899,
    "preview": "package auth\n\nimport (\n\t\"bufio\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/sipeed/"
  },
  {
    "path": "cmd/picoclaw/internal/auth/login.go",
    "chars": 785,
    "preview": "package auth\n\nimport \"github.com/spf13/cobra\"\n\nfunc newLoginCommand() *cobra.Command {\n\tvar (\n\t\tprovider      string\n\t\tu"
  },
  {
    "path": "cmd/picoclaw/internal/auth/login_test.go",
    "chars": 625,
    "preview": "package auth\n\nimport (\n\t\"testing\"\n\n\t\"github.com/spf13/cobra\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr"
  },
  {
    "path": "cmd/picoclaw/internal/auth/logout.go",
    "chars": 441,
    "preview": "package auth\n\nimport \"github.com/spf13/cobra\"\n\nfunc newLogoutCommand() *cobra.Command {\n\tvar provider string\n\n\tcmd := &c"
  },
  {
    "path": "cmd/picoclaw/internal/auth/logout_test.go",
    "chars": 356,
    "preview": "package auth\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc "
  },
  {
    "path": "cmd/picoclaw/internal/auth/models.go",
    "chars": 265,
    "preview": "package auth\n\nimport \"github.com/spf13/cobra\"\n\nfunc newModelsCommand() *cobra.Command {\n\tcmd := &cobra.Command{\n\t\tUse:  "
  },
  {
    "path": "cmd/picoclaw/internal/auth/models_test.go",
    "chars": 335,
    "preview": "package auth\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc "
  },
  {
    "path": "cmd/picoclaw/internal/auth/status.go",
    "chars": 293,
    "preview": "package auth\n\nimport \"github.com/spf13/cobra\"\n\nfunc newStatusCommand() *cobra.Command {\n\tcmd := &cobra.Command{\n\t\tUse:  "
  },
  {
    "path": "cmd/picoclaw/internal/auth/status_test.go",
    "chars": 305,
    "preview": "package auth\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc "
  },
  {
    "path": "cmd/picoclaw/internal/cron/add.go",
    "chars": 1708,
    "preview": "package cron\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/sipeed/picoclaw/pkg/cron\"\n)\n\nfunc newAddCommand(s"
  },
  {
    "path": "cmd/picoclaw/internal/cron/add_test.go",
    "chars": 1372,
    "preview": "package cron\n\nimport (\n\t\"testing\"\n\n\t\"github.com/spf13/cobra\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr"
  },
  {
    "path": "cmd/picoclaw/internal/cron/command.go",
    "chars": 1105,
    "preview": "package cron\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/sipeed/picoclaw/cmd/picoclaw/int"
  },
  {
    "path": "cmd/picoclaw/internal/cron/command_test.go",
    "chars": 1180,
    "preview": "package cron\n\nimport (\n\t\"slices\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require"
  },
  {
    "path": "cmd/picoclaw/internal/cron/disable.go",
    "chars": 383,
    "preview": "package cron\n\nimport \"github.com/spf13/cobra\"\n\nfunc newDisableCommand(storePath func() string) *cobra.Command {\n\treturn "
  },
  {
    "path": "cmd/picoclaw/internal/cron/disable_test.go",
    "chars": 368,
    "preview": "package cron\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc "
  },
  {
    "path": "cmd/picoclaw/internal/cron/enable.go",
    "chars": 378,
    "preview": "package cron\n\nimport \"github.com/spf13/cobra\"\n\nfunc newEnableCommand(storePath func() string) *cobra.Command {\n\treturn &"
  },
  {
    "path": "cmd/picoclaw/internal/cron/enable_test.go",
    "chars": 364,
    "preview": "package cron\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc "
  },
  {
    "path": "cmd/picoclaw/internal/cron/helpers.go",
    "chars": 1567,
    "preview": "package cron\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/sipeed/picoclaw/pkg/cron\"\n)\n\nfunc cronListCmd(storePath string) {\n\tc"
  },
  {
    "path": "cmd/picoclaw/internal/cron/list.go",
    "chars": 325,
    "preview": "package cron\n\nimport \"github.com/spf13/cobra\"\n\nfunc newListCommand(storePath func() string) *cobra.Command {\n\tcmd := &co"
  },
  {
    "path": "cmd/picoclaw/internal/cron/list_test.go",
    "chars": 303,
    "preview": "package cron\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc "
  },
  {
    "path": "cmd/picoclaw/internal/cron/remove.go",
    "chars": 387,
    "preview": "package cron\n\nimport \"github.com/spf13/cobra\"\n\nfunc newRemoveCommand(storePath func() string) *cobra.Command {\n\tcmd := &"
  },
  {
    "path": "cmd/picoclaw/internal/cron/remove_test.go",
    "chars": 337,
    "preview": "package cron\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc "
  },
  {
    "path": "cmd/picoclaw/internal/gateway/command.go",
    "chars": 1285,
    "preview": "package gateway\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/sipeed/picoclaw/cmd/picoclaw/internal\"\n\t\"githu"
  },
  {
    "path": "cmd/picoclaw/internal/gateway/command_test.go",
    "chars": 677,
    "preview": "package gateway\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfu"
  },
  {
    "path": "cmd/picoclaw/internal/helpers.go",
    "chars": 1175,
    "preview": "package internal\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/sipeed/picoclaw/pkg/config\"\n)\n\nconst Logo = \"🦞\"\n\n// GetP"
  },
  {
    "path": "cmd/picoclaw/internal/helpers_test.go",
    "chars": 1302,
    "preview": "package internal\n\nimport (\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"gi"
  },
  {
    "path": "cmd/picoclaw/internal/migrate/command.go",
    "chars": 1564,
    "preview": "package migrate\n\nimport (\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/sipeed/picoclaw/pkg/migrate\"\n)\n\nfunc NewMigrateCommand"
  },
  {
    "path": "cmd/picoclaw/internal/migrate/command_test.go",
    "chars": 963,
    "preview": "package migrate\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfu"
  },
  {
    "path": "cmd/picoclaw/internal/model/command.go",
    "chars": 3652,
    "preview": "package model\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/sipeed/picoclaw/cmd/picoclaw/internal\"\n\t\"github."
  },
  {
    "path": "cmd/picoclaw/internal/model/command_test.go",
    "chars": 8876,
    "preview": "package model\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github"
  },
  {
    "path": "cmd/picoclaw/internal/onboard/command.go",
    "chars": 572,
    "preview": "package onboard\n\nimport (\n\t\"embed\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n//go:generate cp -r ../../../../workspace .\n//go:embed "
  },
  {
    "path": "cmd/picoclaw/internal/onboard/command_test.go",
    "chars": 784,
    "preview": "package onboard\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfu"
  },
  {
    "path": "cmd/picoclaw/internal/onboard/helpers.go",
    "chars": 6302,
    "preview": "package onboard\n\nimport (\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"golang.org/x/term\"\n\n\t\"github.com/sipeed/picoclaw/cmd"
  },
  {
    "path": "cmd/picoclaw/internal/onboard/helpers_test.go",
    "chars": 623,
    "preview": "package onboard\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n)\n\nfunc TestCopyEmbeddedToTargetUsesAgentsMarkdown(t *testin"
  },
  {
    "path": "cmd/picoclaw/internal/skills/command.go",
    "chars": 2094,
    "preview": "package skills\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/sipeed/picoclaw/cmd/picoclaw/i"
  },
  {
    "path": "cmd/picoclaw/internal/skills/command_test.go",
    "chars": 531,
    "preview": "package skills\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfun"
  },
  {
    "path": "cmd/picoclaw/internal/skills/helpers.go",
    "chars": 8134,
    "preview": "package skills\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/sipeed/picoclaw"
  },
  {
    "path": "cmd/picoclaw/internal/skills/install.go",
    "chars": 1278,
    "preview": "package skills\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/sipeed/picoclaw/cmd/picoclaw/internal\"\n\t\"github"
  },
  {
    "path": "cmd/picoclaw/internal/skills/install_test.go",
    "chars": 2231,
    "preview": "package skills\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfun"
  },
  {
    "path": "cmd/picoclaw/internal/skills/installbuiltin.go",
    "chars": 486,
    "preview": "package skills\n\nimport \"github.com/spf13/cobra\"\n\nfunc newInstallBuiltinCommand(workspaceFn func() (string, error)) *cobr"
  },
  {
    "path": "cmd/picoclaw/internal/skills/installbuiltin_test.go",
    "chars": 545,
    "preview": "package skills\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfun"
  },
  {
    "path": "cmd/picoclaw/internal/skills/list.go",
    "chars": 475,
    "preview": "package skills\n\nimport (\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/sipeed/picoclaw/pkg/skills\"\n)\n\nfunc newListCommand(load"
  },
  {
    "path": "cmd/picoclaw/internal/skills/list_test.go",
    "chars": 496,
    "preview": "package skills\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfun"
  },
  {
    "path": "cmd/picoclaw/internal/skills/listbuiltin.go",
    "chars": 326,
    "preview": "package skills\n\nimport \"github.com/spf13/cobra\"\n\nfunc newListBuiltinCommand() *cobra.Command {\n\tcmd := &cobra.Command{\n\t"
  },
  {
    "path": "cmd/picoclaw/internal/skills/listbuiltin_test.go",
    "chars": 498,
    "preview": "package skills\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfun"
  },
  {
    "path": "cmd/picoclaw/internal/skills/remove.go",
    "chars": 589,
    "preview": "package skills\n\nimport (\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/sipeed/picoclaw/pkg/skills\"\n)\n\nfunc newRemoveCommand(in"
  },
  {
    "path": "cmd/picoclaw/internal/skills/remove_test.go",
    "chars": 582,
    "preview": "package skills\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfun"
  },
  {
    "path": "cmd/picoclaw/internal/skills/search.go",
    "chars": 394,
    "preview": "package skills\n\nimport (\n\t\"github.com/spf13/cobra\"\n)\n\nfunc newSearchCommand() *cobra.Command {\n\tcmd := &cobra.Command{\n\t"
  },
  {
    "path": "cmd/picoclaw/internal/skills/search_test.go",
    "chars": 474,
    "preview": "package skills\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfun"
  },
  {
    "path": "cmd/picoclaw/internal/skills/show.go",
    "chars": 523,
    "preview": "package skills\n\nimport (\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/sipeed/picoclaw/pkg/skills\"\n)\n\nfunc newShowCommand(load"
  },
  {
    "path": "cmd/picoclaw/internal/skills/show_test.go",
    "chars": 493,
    "preview": "package skills\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfun"
  },
  {
    "path": "cmd/picoclaw/internal/status/command.go",
    "chars": 288,
    "preview": "package status\n\nimport (\n\t\"github.com/spf13/cobra\"\n)\n\nfunc NewStatusCommand() *cobra.Command {\n\tcmd := &cobra.Command{\n\t"
  },
  {
    "path": "cmd/picoclaw/internal/status/command_test.go",
    "chars": 539,
    "preview": "package status\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfun"
  },
  {
    "path": "cmd/picoclaw/internal/status/helpers.go",
    "chars": 2973,
    "preview": "package status\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/sipeed/picoclaw/cmd/picoclaw/internal\"\n\t\"github.com/sipeed/picoclaw/"
  },
  {
    "path": "cmd/picoclaw/internal/version/command.go",
    "chars": 646,
    "preview": "package version\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/sipeed/picoclaw/cmd/picoclaw/internal\"\n\t\"githu"
  },
  {
    "path": "cmd/picoclaw/internal/version/command_test.go",
    "chars": 581,
    "preview": "package version\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfu"
  },
  {
    "path": "cmd/picoclaw/main.go",
    "chars": 2348,
    "preview": "// PicoClaw - Ultra-lightweight personal AI agent\n// Inspired by and based on nanobot: https://github.com/HKUDS/nanobot\n"
  },
  {
    "path": "cmd/picoclaw/main_test.go",
    "chars": 1188,
    "preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"slices\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/"
  },
  {
    "path": "cmd/picoclaw-launcher-tui/internal/config/store.go",
    "chars": 863,
    "preview": "package configstore\n\nimport (\n\t\"errors\"\n\t\"os\"\n\t\"path/filepath\"\n\n\tpicoclawconfig \"github.com/sipeed/picoclaw/pkg/config\"\n"
  },
  {
    "path": "cmd/picoclaw-launcher-tui/internal/ui/app.go",
    "chars": 11726,
    "preview": "package ui\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/gdamore/tcell/v2\"\n\t\"github.com/ri"
  },
  {
    "path": "cmd/picoclaw-launcher-tui/internal/ui/channel.go",
    "chars": 13800,
    "preview": "package ui\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/gdamore/tcell/v2\"\n\t\"github.com/rivo/tview\"\n\n\tpicoclawconfig \"github"
  },
  {
    "path": "cmd/picoclaw-launcher-tui/internal/ui/gateway_posix.go",
    "chars": 356,
    "preview": "//go:build !windows\n// +build !windows\n\npackage ui\n\nimport \"os/exec\"\n\nfunc isGatewayProcessRunning() bool {\n\tcmd := exec"
  },
  {
    "path": "cmd/picoclaw-launcher-tui/internal/ui/gateway_windows.go",
    "chars": 319,
    "preview": "//go:build windows\n// +build windows\n\npackage ui\n\nimport \"os/exec\"\n\nfunc isGatewayProcessRunning() bool {\n\tcmd := exec.C"
  },
  {
    "path": "cmd/picoclaw-launcher-tui/internal/ui/menu.go",
    "chars": 1591,
    "preview": "package ui\n\nimport (\n\t\"github.com/gdamore/tcell/v2\"\n\t\"github.com/rivo/tview\"\n)\n\ntype MenuAction func()\n\ntype MenuItem st"
  },
  {
    "path": "cmd/picoclaw-launcher-tui/internal/ui/model.go",
    "chars": 10877,
    "preview": "package ui\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gdamore/tcell/v2\"\n\t\"github.com/rivo/tview"
  },
  {
    "path": "cmd/picoclaw-launcher-tui/internal/ui/style.go",
    "chars": 2179,
    "preview": "package ui\n\nimport (\n\t\"github.com/gdamore/tcell/v2\"\n\t\"github.com/rivo/tview\"\n)\n\nconst (\n\tcolorBlue = \"[#3e5db9]\"\n\tcolorR"
  },
  {
    "path": "cmd/picoclaw-launcher-tui/main.go",
    "chars": 205,
    "preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/sipeed/picoclaw/cmd/picoclaw-launcher-tui/internal/ui\"\n)\n\nfunc main() "
  },
  {
    "path": "config/config.example.json",
    "chars": 13054,
    "preview": "{\n  \"agents\": {\n    \"defaults\": {\n      \"workspace\": \"~/.picoclaw/workspace\",\n      \"restrict_to_workspace\": true,\n     "
  },
  {
    "path": "docker/Dockerfile",
    "chars": 1104,
    "preview": "# ============================================================\n# Stage 1: Build the picoclaw binary\n# =================="
  },
  {
    "path": "docker/Dockerfile.full",
    "chars": 1089,
    "preview": "# ============================================================\n# Stage 1: Build the picoclaw binary\n# =================="
  },
  {
    "path": "docker/Dockerfile.goreleaser",
    "chars": 240,
    "preview": "FROM alpine:3.21\n\nARG TARGETPLATFORM\n\nRUN apk add --no-cache ca-certificates tzdata\n\nCOPY $TARGETPLATFORM/picoclaw /usr/"
  },
  {
    "path": "docker/Dockerfile.goreleaser.launcher",
    "chars": 356,
    "preview": "FROM alpine:3.21\n\nARG TARGETPLATFORM\n\nRUN apk add --no-cache ca-certificates tzdata\n\nCOPY $TARGETPLATFORM/picoclaw /usr/"
  },
  {
    "path": "docker/docker-compose.full.yml",
    "chars": 1550,
    "preview": "services:\n  # ─────────────────────────────────────────────\n  # PicoClaw Agent (one-shot query) - Full MCP Support\n  #  "
  },
  {
    "path": "docker/docker-compose.yml",
    "chars": 1705,
    "preview": "services:\n  # ─────────────────────────────────────────────\n  # PicoClaw Agent (one-shot query)\n  #   docker compose -f "
  },
  {
    "path": "docker/entrypoint.sh",
    "chars": 533,
    "preview": "#!/bin/sh\nset -e\n\n# First-run: neither config nor workspace exists.\n# If config.json is already mounted but workspace is"
  },
  {
    "path": "docs/ANTIGRAVITY_AUTH.md",
    "chars": 20882,
    "preview": "# Antigravity Authentication & Integration Guide\n\n## Overview\n\n**Antigravity** (Google Cloud Code Assist) is a Google-ba"
  },
  {
    "path": "docs/ANTIGRAVITY_USAGE.md",
    "chars": 2554,
    "preview": "# Using Antigravity Provider in PicoClaw\n\nThis guide explains how to set up and use the **Antigravity** (Google Cloud Co"
  },
  {
    "path": "docs/agent-refactor/README.md",
    "chars": 3972,
    "preview": "# Agent Refactor\n\n## What this directory is for\n\nThis directory is the working area for the current Agent refactor.\n\nThe"
  },
  {
    "path": "docs/channels/dingtalk/README.zh.md",
    "chars": 782,
    "preview": "# 钉钉\n\n钉钉是阿里巴巴的企业通讯平台,在中国职场中广受欢迎。它采用流式 SDK 来维持持久连接。\n\n## 配置\n\n```json\n{\n  \"channels\": {\n    \"dingtalk\": {\n      \"enabled\": "
  },
  {
    "path": "docs/channels/discord/README.zh.md",
    "chars": 934,
    "preview": "# Discord\n\nDiscord 是一个专为社区设计的免费语音、视频和文本聊天应用。PicoClaw 通过 Discord Bot API 连接到 Discord 服务器,支持接收和发送消息。\n\n## 配置\n\n```json\n{\n  \""
  },
  {
    "path": "docs/channels/feishu/README.zh.md",
    "chars": 2031,
    "preview": "# 飞书\n\n飞书(国际版名称:Lark)是字节跳动旗下的企业协作平台。它通过事件驱动的 Webhook 同时支持中国和全球市场。\n\n## 配置\n\n```json\n{\n  \"channels\": {\n    \"feishu\": {\n     "
  },
  {
    "path": "docs/channels/line/README.zh.md",
    "chars": 1401,
    "preview": "# Line\n\nPicoClaw 通过 LINE Messaging API 配合 Webhook 回调功能实现对 LINE 的支持。\n\n## 配置\n\n```json\n{\n  \"channels\": {\n    \"line\": {\n    "
  },
  {
    "path": "docs/channels/maixcam/README.zh.md",
    "chars": 725,
    "preview": "# MaixCam\n\nMaixCam 是专用于连接矽速科技 MaixCAM 与 MaixCAM2 AI 摄像设备的通道。它采用 TCP 套接字实现双向通信,支持边缘 AI 部署场景。\n\n## 配置\n\n```json\n{\n  \"channel"
  },
  {
    "path": "docs/channels/matrix/README.md",
    "chars": 2403,
    "preview": "# Matrix Channel Configuration Guide\n\n## 1. Example Configuration\n\nAdd this to `config.json`:\n\n```json\n{\n  \"channels\": {"
  },
  {
    "path": "docs/channels/matrix/README.zh.md",
    "chars": 1569,
    "preview": "# Matrix 通道配置指南\n\n## 1. 配置示例\n\n在 `config.json` 中添加:\n\n```json\n{\n  \"channels\": {\n    \"matrix\": {\n      \"enabled\": true,\n    "
  },
  {
    "path": "docs/channels/onebot/README.zh.md",
    "chars": 755,
    "preview": "# OneBot\n\nOneBot 是一个面向 QQ 机器人的开放协议标准,为多种 QQ 机器人实现(例如 go-cqhttp、Mirai)提供了统一的接口。它使用 WebSocket 进行通信。\n\n## 配置\n\n```json\n{\n  \"c"
  },
  {
    "path": "docs/channels/qq/README.zh.md",
    "chars": 1088,
    "preview": "# QQ\n\nPicoClaw 通过 QQ 开放平台的官方机器人 API 提供对 QQ 的支持。\n\n## 配置\n\n```json\n{\n  \"channels\": {\n    \"qq\": {\n      \"enabled\": true,\n   "
  },
  {
    "path": "docs/channels/slack/README.zh.md",
    "chars": 990,
    "preview": "# Slack\n\nSlack 是全球领先的企业级即时通讯平台。PicoClaw 采用 Slack 的 Socket Mode 实现实时双向通信,无需配置公开的 Webhook 端点。\n\n## 配置\n\n```json\n{\n  \"channel"
  },
  {
    "path": "docs/channels/telegram/README.zh.md",
    "chars": 1003,
    "preview": "# Telegram\n\nTelegram Channel 通过 Telegram 机器人 API 使用长轮询实现基于机器人的通信。它支持文本消息、媒体附件(照片、语音、音频、文档)、通过 Groq Whisper 进行语音转录以及内置命令处"
  },
  {
    "path": "docs/channels/wecom/wecom_aibot/README.zh.md",
    "chars": 4697,
    "preview": "# 企业微信智能机器人 (AI Bot)\n\n企业微信智能机器人(AI Bot)是企业微信官方提供的 AI 对话接入方式,支持私聊与群聊,内置流式响应协议。PicoClaw 当前同时支持两种接入模式:\n\n- WebSocket 长连接模式:使"
  },
  {
    "path": "docs/channels/wecom/wecom_app/README.zh.md",
    "chars": 1581,
    "preview": "# 企业微信自建应用\n\n企业微信自建应用是指企业在企业微信中创建的应用,主要用于企业内部使用。通过企业微信自建应用,企业可以实现与员工的高效沟通和协作,提高工作效率。\n\n## 配置\n\n```json\n{\n  \"channels\": {\n  "
  },
  {
    "path": "docs/channels/wecom/wecom_bot/README.zh.md",
    "chars": 1241,
    "preview": "# 企业微信机器人\n\n企业微信机器人是企业微信提供的一种快速接入方式,可以通过 Webhook URL 接收消息。\n\n## 配置\n\n```json\n{\n  \"channels\": {\n    \"wecom\": {\n      \"enable"
  },
  {
    "path": "docs/chat-apps.md",
    "chars": 10918,
    "preview": "# 💬 Chat Apps Configuration\n\n> Back to [README](../README.md)\n\n## 💬 Chat Apps\n\nTalk to your picoclaw through Telegram, D"
  },
  {
    "path": "docs/configuration.md",
    "chars": 12827,
    "preview": "# ⚙️ Configuration Guide\n\n> Back to [README](../README.md)\n\n## ⚙️ Configuration\n\nConfig file: `~/.picoclaw/config.json`\n"
  },
  {
    "path": "docs/credential_encryption.md",
    "chars": 5090,
    "preview": "# Credential Encryption\n\nPicoClaw supports encrypting `api_key` values in `model_list` configuration entries.\nEncrypted "
  },
  {
    "path": "docs/debug.md",
    "chars": 4479,
    "preview": "# Debugging PicoClaw\n\nPicoClaw performs multiple complex interactions under the hood for every single request it receive"
  },
  {
    "path": "docs/design/issue-783-investigation-and-fix-plan.zh.md",
    "chars": 2304,
    "preview": "# Issue #783 调研与修复执行文档\n\n## 1. 问题澄清(已确认)\n\n- 现象:当 `agents.*.model.primary/fallbacks` 使用 `model_name` 别名(如 `step-3.5-flash`"
  },
  {
    "path": "docs/design/provider-refactoring-tests.md",
    "chars": 7855,
    "preview": "# Provider Architecture Refactoring - Test Suite Summary\n\nThis document summarizes the complete test suite designed for "
  },
  {
    "path": "docs/design/provider-refactoring.md",
    "chars": 8274,
    "preview": "# Provider Architecture Refactoring Design\n\n> Issue: #283\n> Discussion: #122\n> Branch: feat/refactor-provider-by-protoco"
  },
  {
    "path": "docs/docker.md",
    "chars": 5269,
    "preview": "# 🐳 Docker & Quick Start Guide\n\n> Back to [README](../README.md)\n\n## 🐳 Docker Compose\n\nYou can also run PicoClaw using D"
  },
  {
    "path": "docs/fr/chat-apps.md",
    "chars": 14229,
    "preview": "# 💬 Configuration des Applications de Chat\n\n> Retour au [README](../../README.fr.md)\n\n## 💬 Applications de Chat\n\nCommuni"
  },
  {
    "path": "docs/fr/configuration.md",
    "chars": 10177,
    "preview": "# ⚙️ Guide de Configuration\n\n> Retour au [README](../../README.fr.md)\n\n## ⚙️ Configuration\n\nFichier de configuration : `"
  },
  {
    "path": "docs/fr/docker.md",
    "chars": 5596,
    "preview": "# 🐳 Docker et Démarrage Rapide\n\n> Retour au [README](../../README.fr.md)\n\n## 🐳 Docker Compose\n\nVous pouvez également exé"
  },
  {
    "path": "docs/fr/providers.md",
    "chars": 16240,
    "preview": "# 🔌 Fournisseurs et Configuration des Modèles\n\n> Retour au [README](../../README.fr.md)\n\n### Fournisseurs\n\n> [!NOTE]\n> G"
  },
  {
    "path": "docs/fr/spawn-tasks.md",
    "chars": 2089,
    "preview": "# 🔄 Tâches Asynchrones et Spawn\n\n> Retour au [README](../../README.fr.md)\n\n## Tâches Rapides (réponse directe)\n\n- Rappor"
  },
  {
    "path": "docs/fr/tools_configuration.md",
    "chars": 14003,
    "preview": "# 🔧 Configuration des Outils\n\n> Retour au [README](../../README.fr.md)\n\nLa configuration des outils de PicoClaw se trouv"
  },
  {
    "path": "docs/fr/troubleshooting.md",
    "chars": 1547,
    "preview": "# 🐛 Dépannage\n\n> Retour au [README](../../README.fr.md)\n\n## \"model ... not found in model_list\" ou OpenRouter \"free is n"
  },
  {
    "path": "docs/it/configuration.md",
    "chars": 10209,
    "preview": "# ⚙️ Guida alla Configurazione\n\n> Torna al [README](../../README.md)\n\n## ⚙️ Configurazione\n\nFile di configurazione: `~/."
  },
  {
    "path": "docs/ja/chat-apps.md",
    "chars": 12728,
    "preview": "# 💬 チャットアプリ設定\n\n> [README](../../README.ja.md) に戻る\n\n## 💬 チャットアプリ連携\n\nPicoClaw は複数のチャットプラットフォームをサポートしており、Agent をどこにでも接続できます"
  },
  {
    "path": "docs/ja/configuration.md",
    "chars": 7478,
    "preview": "# ⚙️ 設定ガイド\n\n> [README](../../README.ja.md) に戻る\n\n## ⚙️ 設定詳細\n\n設定ファイルパス: `~/.picoclaw/config.json`\n\n### 環境変数\n\n環境変数を使用してデフォル"
  },
  {
    "path": "docs/ja/docker.md",
    "chars": 4690,
    "preview": "# 🐳 Docker とクイックスタート\n\n> [README](../../README.ja.md) に戻る\n\n## 🐳 Docker Compose\n\nDocker Compose を使用して PicoClaw を実行できます。ローカ"
  },
  {
    "path": "docs/ja/providers.md",
    "chars": 14602,
    "preview": "# 🔌 プロバイダーとモデル設定\n\n> [README](../../README.ja.md) に戻る\n\n### プロバイダー\n\n> [!NOTE]\n> Groq は Whisper による無料の音声文字起こしを提供しています。Groq "
  },
  {
    "path": "docs/ja/spawn-tasks.md",
    "chars": 1575,
    "preview": "# 🔄 非同期タスクと Spawn\n\n> [README](../../README.ja.md) に戻る\n\n### Spawn を使用した非同期タスク\n\n長時間実行タスク(Web 検索、API 呼び出し)には、`spawn` ツールを使用"
  },
  {
    "path": "docs/ja/tools_configuration.md",
    "chars": 10534,
    "preview": "# 🔧 ツール設定\n\n> [README](../../README.ja.md) に戻る\n\nPicoClaw のツール設定は `config.json` の `tools` フィールドにあります。\n\n## ディレクトリ構造\n\n```jso"
  },
  {
    "path": "docs/ja/troubleshooting.md",
    "chars": 1260,
    "preview": "# 🐛 トラブルシューティング\n\n> [README](../../README.ja.md) に戻る\n\n## \"model ... not found in model_list\" または OpenRouter \"free is not "
  },
  {
    "path": "docs/migration/model-list-migration.md",
    "chars": 6500,
    "preview": "# Migration Guide: From `providers` to `model_list`\n\nThis guide explains how to migrate from the legacy `providers` conf"
  },
  {
    "path": "docs/providers.md",
    "chars": 15955,
    "preview": "# 🔌 Providers & Model Configuration\n\n> Back to [README](../README.md)\n\n### Providers\n\n> [!NOTE]\n> Groq provides free voi"
  },
  {
    "path": "docs/pt-br/chat-apps.md",
    "chars": 11320,
    "preview": "# 💬 Configuração de Aplicativos de Chat\n\n> Voltar ao [README](../../README.pt-br.md)\n\n## 💬 Aplicativos de Chat\n\nConverse"
  },
  {
    "path": "docs/pt-br/configuration.md",
    "chars": 9723,
    "preview": "# ⚙️ Guia de Configuração\n\n> Voltar ao [README](../../README.pt-br.md)\n\n## ⚙️ Configuração\n\nArquivo de configuração: `~/"
  },
  {
    "path": "docs/pt-br/docker.md",
    "chars": 5457,
    "preview": "# 🐳 Docker e Início Rápido\n\n> Voltar ao [README](../../README.pt-br.md)\n\n## 🐳 Docker Compose\n\nVocê também pode executar "
  },
  {
    "path": "docs/pt-br/providers.md",
    "chars": 16023,
    "preview": "# 🔌 Provedores e Configuração de Modelos\n\n> Voltar ao [README](../../README.pt-br.md)\n\n### Provedores\n\n> [!NOTE]\n> O Gro"
  },
  {
    "path": "docs/pt-br/spawn-tasks.md",
    "chars": 1941,
    "preview": "# 🔄 Tarefas Assíncronas e Spawn\n\n> Voltar ao [README](../../README.pt-br.md)\n\n## Tarefas Rápidas (resposta direta)\n\n- In"
  },
  {
    "path": "docs/pt-br/tools_configuration.md",
    "chars": 13684,
    "preview": "# 🔧 Configuração de Ferramentas\n\n> Voltar ao [README](../../README.pt-br.md)\n\nA configuração de ferramentas do PicoClaw "
  },
  {
    "path": "docs/pt-br/troubleshooting.md",
    "chars": 1496,
    "preview": "# 🐛 Solução de Problemas\n\n> Voltar ao [README](../../README.pt-br.md)\n\n## \"model ... not found in model_list\" ou OpenRou"
  },
  {
    "path": "docs/spawn-tasks.md",
    "chars": 1823,
    "preview": "# 🔄 Spawn & Async Tasks\n\n> Back to [README](../README.md)\n\n## Quick Tasks (respond directly)\n\n- Report current time\n\n## "
  },
  {
    "path": "docs/tools_configuration.md",
    "chars": 15692,
    "preview": "# Tools Configuration\n\nPicoClaw's tools configuration is located in the `tools` field of `config.json`.\n\n## Directory St"
  },
  {
    "path": "docs/troubleshooting.md",
    "chars": 1359,
    "preview": "# Troubleshooting\n\n## \"model ... not found in model_list\" or OpenRouter \"free is not a valid model ID\"\n\n**Symptom:** You"
  },
  {
    "path": "docs/vi/chat-apps.md",
    "chars": 10541,
    "preview": "# 💬 Cấu Hình Ứng Dụng Chat\n\n> Quay lại [README](../../README.vi.md)\n\n## 💬 Ứng Dụng Chat\n\nTrò chuyện với picoclaw của bạn"
  },
  {
    "path": "docs/vi/configuration.md",
    "chars": 8815,
    "preview": "# ⚙️ Hướng Dẫn Cấu Hình\n\n> Quay lại [README](../../README.vi.md)\n\n## ⚙️ Cấu Hình\n\nFile cấu hình: `~/.picoclaw/config.jso"
  },
  {
    "path": "docs/vi/docker.md",
    "chars": 5246,
    "preview": "# 🐳 Docker và Bắt Đầu Nhanh\n\n> Quay lại [README](../../README.vi.md)\n\n## 🐳 Docker Compose\n\nBạn cũng có thể chạy PicoClaw"
  },
  {
    "path": "docs/vi/providers.md",
    "chars": 15631,
    "preview": "# 🔌 Nhà Cung Cấp và Cấu Hình Mô Hình\n\n> Quay lại [README](../../README.vi.md)\n\n### Nhà Cung Cấp\n\n> [!NOTE]\n> Groq cung c"
  },
  {
    "path": "docs/vi/spawn-tasks.md",
    "chars": 1861,
    "preview": "# 🔄 Tác Vụ Bất Đồng Bộ và Spawn\n\n> Quay lại [README](../../README.vi.md)\n\n## Tác Vụ Nhanh (phản hồi trực tiếp)\n\n- Báo cá"
  },
  {
    "path": "docs/vi/tools_configuration.md",
    "chars": 13026,
    "preview": "# 🔧 Cấu Hình Công Cụ\n\n> Quay lại [README](../../README.vi.md)\n\nCấu hình công cụ của PicoClaw nằm trong trường `tools` củ"
  },
  {
    "path": "docs/vi/troubleshooting.md",
    "chars": 1477,
    "preview": "# 🐛 Khắc Phục Sự Cố\n\n> Quay lại [README](../../README.vi.md)\n\n## \"model ... not found in model_list\" hoặc OpenRouter \"fr"
  },
  {
    "path": "docs/zh/chat-apps.md",
    "chars": 11885,
    "preview": "# 💬 聊天应用配置\n\n> 返回 [README](../../README.zh.md)\n\n## 💬 聊天应用集成 (Chat Apps)\n\nPicoClaw 支持多种聊天平台,使您的 Agent 能够连接到任何地方。\n\n> **注意**"
  },
  {
    "path": "docs/zh/configuration.md",
    "chars": 6683,
    "preview": "# ⚙️ 配置指南\n\n> 返回 [README](../../README.zh.md)\n\n## ⚙️ 配置详解\n\n配置文件路径: `~/.picoclaw/config.json`\n\n### 环境变量\n\n你可以使用环境变量覆盖默认路径。这"
  },
  {
    "path": "docs/zh/docker.md",
    "chars": 4443,
    "preview": "# 🐳 Docker 与快速开始\n\n> 返回 [README](../../README.zh.md)\n\n## 🐳 Docker Compose\n\n您也可以使用 Docker Compose 运行 PicoClaw,无需在本地安装任何环境。"
  },
  {
    "path": "docs/zh/providers.md",
    "chars": 13679,
    "preview": "# 🔌 提供商与模型配置\n\n> 返回 [README](../../README.zh.md)\n\n### 提供商 (Providers)\n\n> [!NOTE]\n> Groq 通过 Whisper 提供免费的语音转录。如果配置了 Groq,任"
  },
  {
    "path": "docs/zh/spawn-tasks.md",
    "chars": 1454,
    "preview": "# 🔄 异步任务与 Spawn\n\n> 返回 [README](../../README.zh.md)\n\n### 使用 Spawn 的异步任务\n\n对于耗时较长的任务(网络搜索、API 调用),使用 `spawn` 工具创建一个 **子 Age"
  },
  {
    "path": "docs/zh/tools_configuration.md",
    "chars": 9830,
    "preview": "# 🔧 工具配置\n\n> 返回 [README](../../README.zh.md)\n\nPicoClaw 的工具配置位于 `config.json` 的 `tools` 字段中。\n\n## 目录结构\n\n```json\n{\n  \"tools\""
  },
  {
    "path": "docs/zh/troubleshooting.md",
    "chars": 1179,
    "preview": "# 🐛 疑难解答\n\n> 返回 [README](../../README.zh.md)\n\n## \"model ... not found in model_list\" 或 OpenRouter \"free is not a valid mo"
  },
  {
    "path": "go.mod",
    "chars": 3893,
    "preview": "module github.com/sipeed/picoclaw\n\ngo 1.25.8\n\nrequire (\n\tfyne.io/systray v1.12.0\n\tgithub.com/adhocore/gronx v1.19.6\n\tgit"
  },
  {
    "path": "go.sum",
    "chars": 35849,
    "preview": "cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=\nfilippo.io/edwards255"
  },
  {
    "path": "pkg/agent/context.go",
    "chars": 24608,
    "preview": "package agent\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\""
  },
  {
    "path": "pkg/agent/context_cache_test.go",
    "chars": 22195,
    "preview": "package agent\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/sipeed/picoclaw/pkg/p"
  },
  {
    "path": "pkg/agent/context_test.go",
    "chars": 8724,
    "preview": "package agent\n\nimport (\n\t\"testing\"\n\n\t\"github.com/sipeed/picoclaw/pkg/providers\"\n)\n\nfunc msg(role, content string) provid"
  },
  {
    "path": "pkg/agent/instance.go",
    "chars": 10562,
    "preview": "package agent\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/sipeed/picoclaw/pkg/"
  },
  {
    "path": "pkg/agent/instance_test.go",
    "chars": 7740,
    "preview": "package agent\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/sipeed/picoclaw/pkg/config"
  },
  {
    "path": "pkg/agent/loop.go",
    "chars": 64120,
    "preview": "// PicoClaw - Ultra-lightweight personal AI agent\n// Inspired by and based on nanobot: https://github.com/HKUDS/nanobot\n"
  },
  {
    "path": "pkg/agent/loop_mcp.go",
    "chars": 5975,
    "preview": "// PicoClaw - Ultra-lightweight personal AI agent\n// Inspired by and based on nanobot: https://github.com/HKUDS/nanobot\n"
  },
  {
    "path": "pkg/agent/loop_mcp_test.go",
    "chars": 2047,
    "preview": "// PicoClaw - Ultra-lightweight personal AI agent\n// Inspired by and based on nanobot: https://github.com/HKUDS/nanobot\n"
  },
  {
    "path": "pkg/agent/loop_media.go",
    "chars": 4721,
    "preview": "// PicoClaw - Ultra-lightweight personal AI agent\n// Inspired by and based on nanobot: https://github.com/HKUDS/nanobot\n"
  },
  {
    "path": "pkg/agent/loop_test.go",
    "chars": 50742,
    "preview": "package agent\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sli"
  },
  {
    "path": "pkg/agent/memory.go",
    "chars": 4319,
    "preview": "// PicoClaw - Ultra-lightweight personal AI agent\n// Inspired by and based on nanobot: https://github.com/HKUDS/nanobot\n"
  },
  {
    "path": "pkg/agent/mock_provider_test.go",
    "chars": 484,
    "preview": "package agent\n\nimport (\n\t\"context\"\n\n\t\"github.com/sipeed/picoclaw/pkg/providers\"\n)\n\ntype mockProvider struct{}\n\nfunc (m *"
  },
  {
    "path": "pkg/agent/model_resolution.go",
    "chars": 2217,
    "preview": "package agent\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/sipeed/picoclaw/pkg/config\"\n\t\"github.com/sipeed/picoclaw/pkg/pro"
  },
  {
    "path": "pkg/agent/registry.go",
    "chars": 3789,
    "preview": "package agent\n\nimport (\n\t\"sync\"\n\n\t\"github.com/sipeed/picoclaw/pkg/config\"\n\t\"github.com/sipeed/picoclaw/pkg/logger\"\n\t\"git"
  },
  {
    "path": "pkg/agent/registry_test.go",
    "chars": 5660,
    "preview": "package agent\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/sipeed/picoclaw/pkg/config\"\n\t\"github.com/sipeed/picoclaw/pkg"
  },
  {
    "path": "pkg/agent/thinking.go",
    "chars": 1155,
    "preview": "package agent\n\nimport \"strings\"\n\n// ThinkingLevel controls how the provider sends thinking parameters.\n//\n//   - \"adapti"
  },
  {
    "path": "pkg/agent/thinking_test.go",
    "chars": 982,
    "preview": "package agent\n\nimport \"testing\"\n\nfunc TestParseThinkingLevel(t *testing.T) {\n\ttests := []struct {\n\t\tname  string\n\t\tinput"
  },
  {
    "path": "pkg/auth/anthropic_usage.go",
    "chars": 1830,
    "preview": "package auth\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"time\"\n)\n\nconst (\n\tanthropicBetaHeader = \"oauth-2025-0"
  },
  {
    "path": "pkg/auth/anthropic_usage_test.go",
    "chars": 3002,
    "preview": "package auth\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestFetchAnthropicUsage_Success(t "
  },
  {
    "path": "pkg/auth/oauth.go",
    "chars": 17878,
    "preview": "package auth\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"crypto/rand\"\n\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"i"
  },
  {
    "path": "pkg/auth/oauth_test.go",
    "chars": 10727,
    "preview": "package auth\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"strings\"\n\t\"test"
  },
  {
    "path": "pkg/auth/pkce.go",
    "chars": 529,
    "preview": "package auth\n\nimport (\n\t\"crypto/rand\"\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n)\n\ntype PKCECodes struct {\n\tCodeVerifier  stri"
  },
  {
    "path": "pkg/auth/pkce_test.go",
    "chars": 1251,
    "preview": "package auth\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"testing\"\n)\n\nfunc TestGeneratePKCE(t *testing.T) {\n\tcodes, e"
  },
  {
    "path": "pkg/auth/store.go",
    "chars": 2649,
    "preview": "package auth\n\nimport (\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/sipeed/picoclaw/pkg/config\"\n\t\"githu"
  }
]

// ... and 495 more files (download for full content)

About this extraction

This page contains the full source code of the sipeed/picoclaw GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 695 files (3.9 MB), approximately 1.1M tokens, and a symbol index with 4859 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!