Compare commits
13 commits
0f4a997b47
...
9b4df86182
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b4df86182 | ||
|
|
49183d7e8d | ||
|
|
e14f8daecb | ||
|
|
85dcfccad6 | ||
|
|
9a0efd7bab | ||
|
|
0132837d22 | ||
|
|
a680d8fcfa | ||
|
|
c87b154b61 | ||
|
|
994c0aea11 | ||
|
|
b6e6d25d75 | ||
|
|
8f891d5dd0 | ||
|
|
9068fde274 | ||
|
|
ce4a6c9bba |
30 changed files with 1144 additions and 163 deletions
17
README.md
17
README.md
|
|
@ -20,17 +20,6 @@
|
|||
|
||||
[中文](docs/project/README.zh.md) | [日本語](docs/project/README.ja.md) | [한국어](docs/project/README.ko.md) | [Português](docs/project/README.pt-br.md) | [Tiếng Việt](docs/project/README.vi.md) | [Français](docs/project/README.fr.md) | [Italiano](docs/project/README.it.md) | [Bahasa Indonesia](docs/project/README.id.md) | [Malay](docs/project/README.ms.md) | **English**
|
||||
|
||||
<p>
|
||||
<a href="https://picopaw.ai">
|
||||
<img src="assets/picopaw-banner-en.webp" alt="PicoPaw AI: Your AI Desktop Buddy" width="100%">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>PicoPaw AI is now live at <a href="https://picopaw.ai">picopaw.ai</a>.</strong><br>
|
||||
Create, preview, and share playful AI companions for the PicoClaw ecosystem.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
|
@ -60,15 +49,13 @@
|
|||
> **Security Notice**
|
||||
>
|
||||
> * **NO CRYPTO:** PicoClaw has **not** issued any official tokens or cryptocurrency. All claims on `pump.fun` or other trading platforms are **scams**.
|
||||
> * **OFFICIAL DOMAIN:** The **ONLY** official PicoClaw website is **[picoclaw.io](https://picoclaw.io)**, and company website is **[sipeed.com](https://sipeed.com)**
|
||||
> * **BEWARE:** Many lookalike `.ai/.org/.com/.net/...` domains have been registered by third parties. Only trust domains explicitly linked from this README.
|
||||
> * **OFFICIAL DOMAIN:** The **ONLY** official website is **[picoclaw.io](https://picoclaw.io)**, and company website is **[sipeed.com](https://sipeed.com)**
|
||||
> * **BEWARE:** Many `.ai/.org/.com/.net/...` domains have been registered by third parties. Do not trust them.
|
||||
> * **NOTE:** PicoClaw is in early rapid development. There may be unresolved security issues. Do not deploy to production before v1.0.
|
||||
> * **NOTE:** PicoClaw has recently merged many PRs. Recent builds may use 10-20MB RAM. Resource optimization is planned after feature stabilization.
|
||||
|
||||
## 📢 News
|
||||
|
||||
2026-06-11 🐾 **PicoPaw AI is live!** Explore the new PicoPaw companion experience at [picopaw.ai](https://picopaw.ai), with animated AI pet previews and ecosystem updates for PicoClaw users.
|
||||
|
||||
2026-05-11 🛒 **LicheeRV-Claw on AliExpress!** You can now purchase LicheeRV-Claw from [AliExpress](https://www.aliexpress.com/item/1005006519668532.html), making it easier to try PicoClaw on compact RISC-V hardware.
|
||||
|
||||
<p align="center">
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 188 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 188 KiB |
|
|
@ -20,17 +20,6 @@
|
|||
|
||||
[中文](README.zh.md) | [日本語](README.ja.md) | [한국어](README.ko.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) | [Malay](README.ms.md) | [English](../../README.md)
|
||||
|
||||
<p>
|
||||
<a href="https://picopaw.ai">
|
||||
<img src="../../assets/picopaw-banner-en.webp" alt="PicoPaw AI: Your AI Desktop Buddy" width="100%">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>PicoPaw AI is now live at <a href="https://picopaw.ai">picopaw.ai</a>.</strong><br>
|
||||
Create, preview, and share playful AI companions for the PicoClaw ecosystem.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
|
@ -68,8 +57,6 @@
|
|||
|
||||
## 📢 Actualités
|
||||
|
||||
2026-06-11 🐾 **PicoPaw AI is live!** Explore the new PicoPaw companion experience at [picopaw.ai](https://picopaw.ai), with animated AI pet previews and ecosystem updates for PicoClaw users.
|
||||
|
||||
2026-05-11 🛒 **LicheeRV-Claw disponible sur AliExpress !** Vous pouvez désormais acheter le LicheeRV-Claw sur [AliExpress](https://www.aliexpress.com/item/1005006519668532.html), ce qui facilite l'essai de PicoClaw sur du matériel RISC-V compact.
|
||||
|
||||
<p align="center">
|
||||
|
|
|
|||
|
|
@ -20,17 +20,6 @@
|
|||
|
||||
[中文](README.zh.md) | [日本語](README.ja.md) | [한국어](README.ko.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** | [Malay](README.ms.md) | [English](../../README.md)
|
||||
|
||||
<p>
|
||||
<a href="https://picopaw.ai">
|
||||
<img src="../../assets/picopaw-banner-en.webp" alt="PicoPaw AI: Your AI Desktop Buddy" width="100%">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>PicoPaw AI is now live at <a href="https://picopaw.ai">picopaw.ai</a>.</strong><br>
|
||||
Create, preview, and share playful AI companions for the PicoClaw ecosystem.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
|
@ -67,8 +56,6 @@
|
|||
|
||||
## 📢 Berita
|
||||
|
||||
2026-06-11 🐾 **PicoPaw AI is live!** Explore the new PicoPaw companion experience at [picopaw.ai](https://picopaw.ai), with animated AI pet previews and ecosystem updates for PicoClaw users.
|
||||
|
||||
2026-05-11 🛒 **LicheeRV-Claw tersedia di AliExpress!** Kini Anda dapat membeli LicheeRV-Claw di [AliExpress](https://www.aliexpress.com/item/1005006519668532.html), sehingga lebih mudah mencoba PicoClaw di hardware RISC-V ringkas.
|
||||
|
||||
<p align="center">
|
||||
|
|
|
|||
|
|
@ -20,17 +20,6 @@
|
|||
|
||||
[中文](README.zh.md) | [日本語](README.ja.md) | [한국어](README.ko.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) | [Malay](README.ms.md) | [English](../../README.md)
|
||||
|
||||
<p>
|
||||
<a href="https://picopaw.ai">
|
||||
<img src="../../assets/picopaw-banner-en.webp" alt="PicoPaw AI: Your AI Desktop Buddy" width="100%">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>PicoPaw AI is now live at <a href="https://picopaw.ai">picopaw.ai</a>.</strong><br>
|
||||
Create, preview, and share playful AI companions for the PicoClaw ecosystem.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
|
@ -67,8 +56,6 @@
|
|||
|
||||
## 📢 Novità
|
||||
|
||||
2026-06-11 🐾 **PicoPaw AI is live!** Explore the new PicoPaw companion experience at [picopaw.ai](https://picopaw.ai), with animated AI pet previews and ecosystem updates for PicoClaw users.
|
||||
|
||||
2026-05-11 🛒 **LicheeRV-Claw disponibile su AliExpress!** Ora puoi acquistare LicheeRV-Claw su [AliExpress](https://www.aliexpress.com/item/1005006519668532.html), rendendo più semplice provare PicoClaw su hardware RISC-V compatto.
|
||||
|
||||
<p align="center">
|
||||
|
|
|
|||
|
|
@ -20,17 +20,6 @@
|
|||
|
||||
[中文](README.zh.md) | **日本語** | [한국어](README.ko.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) | [Malay](README.ms.md) | [English](../../README.md)
|
||||
|
||||
<p>
|
||||
<a href="https://picopaw.ai">
|
||||
<img src="../../assets/picopaw-banner-en.webp" alt="PicoPaw AI: Your AI Desktop Buddy" width="100%">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>PicoPaw AI is now live at <a href="https://picopaw.ai">picopaw.ai</a>.</strong><br>
|
||||
Create, preview, and share playful AI companions for the PicoClaw ecosystem.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
|
@ -67,8 +56,6 @@
|
|||
|
||||
## 📢 ニュース
|
||||
|
||||
2026-06-11 🐾 **PicoPaw AI is live!** Explore the new PicoPaw companion experience at [picopaw.ai](https://picopaw.ai), with animated AI pet previews and ecosystem updates for PicoClaw users.
|
||||
|
||||
2026-05-11 🛒 **LicheeRV-Claw が AliExpress で購入可能に!** [AliExpress](https://www.aliexpress.com/item/1005006519668532.html) から LicheeRV-Claw を購入できるようになり、コンパクトな RISC-V ハードウェアで PicoClaw を試しやすくなりました。
|
||||
|
||||
<p align="center">
|
||||
|
|
|
|||
|
|
@ -20,17 +20,6 @@
|
|||
|
||||
[中文](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) | [Malay](README.ms.md) | [English](../../README.md)
|
||||
|
||||
<p>
|
||||
<a href="https://picopaw.ai">
|
||||
<img src="../../assets/picopaw-banner-en.webp" alt="PicoPaw AI: Your AI Desktop Buddy" width="100%">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>PicoPaw AI is now live at <a href="https://picopaw.ai">picopaw.ai</a>.</strong><br>
|
||||
Create, preview, and share playful AI companions for the PicoClaw ecosystem.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
|
@ -67,8 +56,6 @@
|
|||
|
||||
## 📢 뉴스
|
||||
|
||||
2026-06-11 🐾 **PicoPaw AI is live!** Explore the new PicoPaw companion experience at [picopaw.ai](https://picopaw.ai), with animated AI pet previews and ecosystem updates for PicoClaw users.
|
||||
|
||||
2026-05-11 🛒 **LicheeRV-Claw를 AliExpress에서 구매할 수 있습니다!** 이제 [AliExpress](https://www.aliexpress.com/item/1005006519668532.html)에서 LicheeRV-Claw를 구매해 소형 RISC-V 하드웨어에서 PicoClaw를 더 쉽게 사용해 볼 수 있습니다.
|
||||
|
||||
<p align="center">
|
||||
|
|
|
|||
|
|
@ -20,17 +20,6 @@
|
|||
|
||||
[中文](README.zh.md) | [日本語](README.ja.md) | [한국어](README.ko.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) | **Malay** | [English](../../README.md)
|
||||
|
||||
<p>
|
||||
<a href="https://picopaw.ai">
|
||||
<img src="../../assets/picopaw-banner-en.webp" alt="PicoPaw AI: Your AI Desktop Buddy" width="100%">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>PicoPaw AI is now live at <a href="https://picopaw.ai">picopaw.ai</a>.</strong><br>
|
||||
Create, preview, and share playful AI companions for the PicoClaw ecosystem.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
|
@ -67,8 +56,6 @@
|
|||
|
||||
## 📢 Berita
|
||||
|
||||
2026-06-11 🐾 **PicoPaw AI is live!** Explore the new PicoPaw companion experience at [picopaw.ai](https://picopaw.ai), with animated AI pet previews and ecosystem updates for PicoClaw users.
|
||||
|
||||
2026-05-11 🛒 **LicheeRV-Claw tersedia di AliExpress!** Anda kini boleh membeli LicheeRV-Claw di [AliExpress](https://www.aliexpress.com/item/1005006519668532.html), menjadikannya lebih mudah untuk mencuba PicoClaw pada perkakasan RISC-V yang kompak.
|
||||
|
||||
<p align="center">
|
||||
|
|
|
|||
|
|
@ -20,17 +20,6 @@
|
|||
|
||||
[中文](README.zh.md) | [日本語](README.ja.md) | [한국어](README.ko.md) | **Português** | [Tiếng Việt](README.vi.md) | [Français](README.fr.md) | [Italiano](README.it.md) | [Bahasa Indonesia](README.id.md) | [Malay](README.ms.md) | [English](../../README.md)
|
||||
|
||||
<p>
|
||||
<a href="https://picopaw.ai">
|
||||
<img src="../../assets/picopaw-banner-en.webp" alt="PicoPaw AI: Your AI Desktop Buddy" width="100%">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>PicoPaw AI is now live at <a href="https://picopaw.ai">picopaw.ai</a>.</strong><br>
|
||||
Create, preview, and share playful AI companions for the PicoClaw ecosystem.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
|
@ -67,8 +56,6 @@
|
|||
|
||||
## 📢 Novidades
|
||||
|
||||
2026-06-11 🐾 **PicoPaw AI is live!** Explore the new PicoPaw companion experience at [picopaw.ai](https://picopaw.ai), with animated AI pet previews and ecosystem updates for PicoClaw users.
|
||||
|
||||
2026-05-11 🛒 **LicheeRV-Claw no AliExpress!** Agora você pode comprar o LicheeRV-Claw no [AliExpress](https://www.aliexpress.com/item/1005006519668532.html), facilitando testar o PicoClaw em hardware RISC-V compacto.
|
||||
|
||||
<p align="center">
|
||||
|
|
|
|||
|
|
@ -20,17 +20,6 @@
|
|||
|
||||
[中文](README.zh.md) | [日本語](README.ja.md) | [한국어](README.ko.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) | [Malay](README.ms.md) | [English](../../README.md)
|
||||
|
||||
<p>
|
||||
<a href="https://picopaw.ai">
|
||||
<img src="../../assets/picopaw-banner-en.webp" alt="PicoPaw AI: Your AI Desktop Buddy" width="100%">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>PicoPaw AI is now live at <a href="https://picopaw.ai">picopaw.ai</a>.</strong><br>
|
||||
Create, preview, and share playful AI companions for the PicoClaw ecosystem.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
|
@ -67,8 +56,6 @@
|
|||
|
||||
## 📢 Tin tức
|
||||
|
||||
2026-06-11 🐾 **PicoPaw AI is live!** Explore the new PicoPaw companion experience at [picopaw.ai](https://picopaw.ai), with animated AI pet previews and ecosystem updates for PicoClaw users.
|
||||
|
||||
2026-05-11 🛒 **LicheeRV-Claw đã có trên AliExpress!** Bạn hiện có thể mua LicheeRV-Claw trên [AliExpress](https://www.aliexpress.com/item/1005006519668532.html), giúp việc thử PicoClaw trên phần cứng RISC-V nhỏ gọn dễ dàng hơn.
|
||||
|
||||
<p align="center">
|
||||
|
|
|
|||
|
|
@ -20,17 +20,6 @@
|
|||
|
||||
**中文** | [日本語](README.ja.md) | [한국어](README.ko.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) | [Malay](README.ms.md) | [English](../../README.md)
|
||||
|
||||
<p>
|
||||
<a href="https://picopaw.ai/zh">
|
||||
<img src="../../assets/picopaw-banner-zh.webp" alt="PicoPaw AI:你的 AI 桌面伙伴" width="100%">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>PicoPaw AI 已在 <a href="https://picopaw.ai/zh">picopaw.ai/zh</a> 上线。</strong><br>
|
||||
创建、预览并分享面向 PicoClaw 生态的 AI 桌面伙伴。
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
|
@ -60,15 +49,13 @@
|
|||
> **🚨 安全声明**
|
||||
>
|
||||
> - **无加密货币 (NO CRYPTO):** PicoClaw **没有** 发行任何官方代币、Token 或虚拟货币。所有在 `pump.fun` 或其他交易平台上的相关声称均为 **诈骗**。
|
||||
> - **官方域名:** 唯一的 PicoClaw 官方网站是 **[picoclaw.io](https://picoclaw.io)**,公司官网是 **[sipeed.com](https://sipeed.com)**。
|
||||
> - **警惕:** 许多相似的 `.ai/.org/.com/.net/...` 后缀域名被第三方抢注。请只信任本 README 明确链接的域名。
|
||||
> - **官方域名:** 唯一的官方网站是 **[picoclaw.io](https://picoclaw.io)**,公司官网是 **[sipeed.com](https://sipeed.com)**。
|
||||
> - **警惕:** 许多 `.ai/.org/.com/.net/...` 后缀的域名被第三方抢注,请勿轻信。
|
||||
> - **注意:** PicoClaw 正在初期的快速功能开发阶段,可能有尚未修复的网络安全问题,在 1.0 正式版发布前,请不要将其部署到生产环境中。
|
||||
> - **注意:** PicoClaw 最近合并了大量 PR,近期版本可能内存占用较大 (10~20MB),我们将在功能较为收敛后进行资源占用优化。
|
||||
|
||||
## 📢 新闻
|
||||
|
||||
2026-06-11 🐾 **PicoPaw AI 上线!** 访问 [picopaw.ai/zh](https://picopaw.ai/zh),体验全新的 PicoPaw 桌面伙伴,预览会动的 AI 宠物,并关注 PicoClaw 生态更新。
|
||||
|
||||
2026-05-11 🛒 **LicheeRV-Claw 已上架淘宝!** 现在可以在 [淘宝](https://item.taobao.com/item.htm?abbucket=20&id=764939520376) 购买 LicheeRV-Claw,更方便地在小型 RISC-V 硬件上体验 PicoClaw。
|
||||
|
||||
<p align="center">
|
||||
|
|
|
|||
17
go.mod
17
go.mod
|
|
@ -1,6 +1,6 @@
|
|||
module github.com/sipeed/picoclaw
|
||||
|
||||
go 1.25.11
|
||||
go 1.25.12
|
||||
|
||||
require (
|
||||
fyne.io/systray v1.12.2
|
||||
|
|
@ -32,6 +32,7 @@ require (
|
|||
github.com/modelcontextprotocol/go-sdk v1.6.1
|
||||
github.com/muesli/termenv v0.16.0
|
||||
github.com/mymmrac/telego v1.10.0
|
||||
github.com/nbd-wtf/go-nostr v0.52.3
|
||||
github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1
|
||||
github.com/openai/openai-go/v3 v3.22.0
|
||||
github.com/pion/rtp v1.10.2
|
||||
|
|
@ -60,6 +61,7 @@ require (
|
|||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 // indirect
|
||||
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.24 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 // indirect
|
||||
|
|
@ -76,6 +78,9 @@ require (
|
|||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/beeper/argo-go v1.1.2 // indirect
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect
|
||||
github.com/btcsuite/btcd/btcutil v1.1.5 // indirect
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
|
||||
github.com/buger/jsonparser v1.1.2 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||
github.com/charmbracelet/x/ansi v0.8.0 // indirect
|
||||
|
|
@ -84,6 +89,8 @@ require (
|
|||
github.com/cloudflare/circl v1.6.3 // indirect
|
||||
github.com/coder/websocket v1.8.14 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
|
|
@ -92,17 +99,23 @@ require (
|
|||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/invopop/jsonschema v0.14.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.42 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/pb33f/ordered-map/v2 v2.3.1 // indirect
|
||||
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/segmentio/asm v1.1.3 // indirect
|
||||
|
|
@ -119,7 +132,7 @@ require (
|
|||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.2 // indirect
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
|
||||
golang.org/x/text v0.38.0 // indirect
|
||||
golang.org/x/text v0.39.0 // indirect
|
||||
modernc.org/libc v1.73.4 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
|
|
|
|||
84
go.sum
84
go.sum
|
|
@ -19,10 +19,13 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 h1:RHK7bS+HQMs
|
|||
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 h1:ClzzXMDDuUbWfNNZqGeYq4PnYOlwlOVIvSyNaIy0ykg=
|
||||
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3/go.mod h1:we0YA5CsBbH5+/NUzC/AlMmxaDtWlXeNsqrwXjTzmzA=
|
||||
github.com/SevereCloud/vksdk/v3 v3.3.1 h1:O86zsp5LQnHE+O5acvuXM/s6S1LyxzVTkF6+Lup0Jyg=
|
||||
github.com/SevereCloud/vksdk/v3 v3.3.1/go.mod h1:c6WaA5aocUYsXfkcUbg2qy45V9M1VDcqHHmHIN14NAw=
|
||||
github.com/adhocore/gronx v1.20.0 h1:PD13Mo0wekkZ7ZZR9yb1TqeqTfybs7/K3ez9DmjQwEs=
|
||||
github.com/adhocore/gronx v1.20.0/go.mod h1:7oUY1WAU8rEJWmAxXR2DN0JaO4gi9khSgKjiRypqteg=
|
||||
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
||||
github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=
|
||||
github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
||||
|
|
@ -71,6 +74,30 @@ github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPn
|
|||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/beeper/argo-go v1.1.2 h1:UQI2G8F+NLfGTOmTUI0254pGKx/HUU/etbUGTJv91Fs=
|
||||
github.com/beeper/argo-go v1.1.2/go.mod h1:M+LJAnyowKVQ6Rdj6XYGEn+qcVFkb3R/MUpqkGR0hM4=
|
||||
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
|
||||
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
|
||||
github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
|
||||
github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.5 h1:+wER79R5670vs/ZusMTF1yTcRYE5GUsFbdjdisflzM8=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
|
||||
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
|
||||
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
|
||||
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
|
||||
github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
|
||||
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
||||
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
|
||||
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
|
||||
github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk=
|
||||
github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
|
|
@ -103,14 +130,23 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N
|
|||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
|
||||
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/dvyukov/go-fuzz v0.0.0-20200318091601-be3528f3a813/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
|
||||
github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
|
||||
github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
|
||||
github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg=
|
||||
|
|
@ -150,6 +186,7 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
|
|||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/gomarkdown/markdown v0.0.0-20260411013819-759bbc3e3207 h1:p7t34F7K4OCRQblcDhNJnP46Uaarz3z2cLcvOZYxWn8=
|
||||
github.com/gomarkdown/markdown v0.0.0-20260411013819-759bbc3e3207/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
|
|
@ -160,6 +197,7 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0=
|
||||
github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
|
|
@ -182,12 +220,20 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
|
|||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/invopop/jsonschema v0.14.0 h1:MHQqLhvpNUZfw+hM3AZDYK7jxO8FZoQeQM77g8iyZjg=
|
||||
github.com/invopop/jsonschema v0.14.0/go.mod h1:ygm6C2EaVNMBDPpaPlnOA2pFAxBnxGjFlMZABxm9n2I=
|
||||
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kagisearch/kagi-openapi-golang v0.0.0-20260526215348-96575e864d62 h1:nyUi7Wel3KlVSa5ArgX/snlizqfaxU48qtvXS/JK5GE=
|
||||
github.com/kagisearch/kagi-openapi-golang v0.0.0-20260526215348-96575e864d62/go.mod h1:vONkS+clG730HSKOw3nZVa22TjB21r6csKYzYt0a9zI=
|
||||
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
|
||||
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
|
||||
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
|
|
@ -209,6 +255,8 @@ github.com/line/line-bot-sdk-go/v8 v8.20.1 h1:OE7qwJbuZPz9MGOHHNI0E6rQNtIU7ENHyF
|
|||
github.com/line/line-bot-sdk-go/v8 v8.20.1/go.mod h1:QMXJwPka2ysSeVQKWXkBp8DzBFs+CFAXFNo75KJtWho=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
|
|
@ -223,17 +271,28 @@ github.com/minio/selfupdate v0.6.0 h1:i76PgT0K5xO9+hjzKcacQtO7+MjJ4JKA8Ak8XQ9DDw
|
|||
github.com/minio/selfupdate v0.6.0/go.mod h1:bO02GTIPCMQFTEvE5h4DjYB58bCoZ35XLeBf0buTDdM=
|
||||
github.com/modelcontextprotocol/go-sdk v1.6.1 h1:0zOSupjKUxPKSocPT1Wtago+mUHU2/uZ4xSOY0FGReU=
|
||||
github.com/modelcontextprotocol/go-sdk v1.6.1/go.mod h1:kzm3kzFL1/+AziGOE0nUs3gvPoNxMCvkxokMkuFapXQ=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/mymmrac/telego v1.10.0 h1:Upe0TqYyiK+yE5RFXXuQWVHGfLZnqvUfj4KZVjTcgWE=
|
||||
github.com/mymmrac/telego v1.10.0/go.mod h1:LsQKDA6EwssPP9XkORPXwwOFUGIRf/Wf2Wb8y3YyJdE=
|
||||
github.com/nbd-wtf/go-nostr v0.52.3 h1:Xd87pXfJEJRXHpM+fLjQQln8dBNNaoPA10V7BbyP4KI=
|
||||
github.com/nbd-wtf/go-nostr v0.52.3/go.mod h1:4avYoc9mDGZ9wHsvCOhHH9vPzKucCfuYBtJUSpHTfNk=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
|
|
@ -256,6 +315,8 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjL
|
|||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
|
|
@ -287,7 +348,10 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
|||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
|
|
@ -295,6 +359,7 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
|
|||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
|
||||
github.com/tencent-connect/botgo v0.2.1 h1:+BrTt9Zh+awL28GWC4g5Na3nQaGRWb0N5IctS8WqBCk=
|
||||
github.com/tencent-connect/botgo v0.2.1/go.mod h1:oO1sG9ybhXNickvt+CVym5khwQ+uKhTR+IhTqEfOVsI=
|
||||
github.com/tidwall/gjson v1.9.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
|
|
@ -355,6 +420,7 @@ go.yaml.in/yaml/v4 v4.0.0-rc.2 h1:/FrI8D64VSr4HtGIlUtlFMGsm7H7pWTbj6vOLVZcA6s=
|
|||
go.yaml.in/yaml/v4 v4.0.0-rc.2/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||
golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=
|
||||
golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
|
|
@ -370,13 +436,15 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
|
|
@ -407,6 +475,8 @@ golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
|
@ -432,14 +502,15 @@ golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
|||
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
|
||||
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
|
||||
golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
|
@ -449,8 +520,8 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f
|
|||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
||||
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
|
@ -472,6 +543,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
|
|||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
|
|
|||
|
|
@ -103,8 +103,25 @@ func NewAgentInstance(
|
|||
toolsRegistry.Register(tools.NewReadFileBytesTool(workspace, readRestrict, maxReadFileSize, allowReadPaths))
|
||||
}
|
||||
}
|
||||
if cfg.Tools.IsToolEnabled("edit_file") {
|
||||
toolsRegistry.Register(tools.NewEditFileTool(workspace, restrict, allowWritePaths))
|
||||
}
|
||||
if cfg.Tools.IsToolEnabled("append_file") {
|
||||
toolsRegistry.Register(tools.NewAppendFileTool(workspace, restrict, allowWritePaths))
|
||||
}
|
||||
// Build write_file's copy from the registered editors so it steers the agent
|
||||
// to edit_file/append_file only when those tools are actually available.
|
||||
if cfg.Tools.IsToolEnabled("write_file") {
|
||||
toolsRegistry.Register(tools.NewWriteFileTool(workspace, restrict, allowWritePaths))
|
||||
writeTool := tools.NewWriteFileTool(workspace, restrict, allowWritePaths)
|
||||
var altTools []string
|
||||
if toolsRegistry.HasRegistered("append_file") {
|
||||
altTools = append(altTools, "append_file")
|
||||
}
|
||||
if toolsRegistry.HasRegistered("edit_file") {
|
||||
altTools = append(altTools, "edit_file")
|
||||
}
|
||||
writeTool.SetAlternativeTools(altTools)
|
||||
toolsRegistry.Register(writeTool)
|
||||
}
|
||||
if cfg.Tools.IsToolEnabled("list_dir") {
|
||||
toolsRegistry.Register(tools.NewListDirTool(workspace, readRestrict, allowReadPaths))
|
||||
|
|
@ -119,13 +136,6 @@ func NewAgentInstance(
|
|||
}
|
||||
}
|
||||
|
||||
if cfg.Tools.IsToolEnabled("edit_file") {
|
||||
toolsRegistry.Register(tools.NewEditFileTool(workspace, restrict, allowWritePaths))
|
||||
}
|
||||
if cfg.Tools.IsToolEnabled("append_file") {
|
||||
toolsRegistry.Register(tools.NewAppendFileTool(workspace, restrict, allowWritePaths))
|
||||
}
|
||||
|
||||
sessionsDir := filepath.Join(workspace, "sessions")
|
||||
sessions := initSessionStore(sessionsDir)
|
||||
|
||||
|
|
|
|||
|
|
@ -584,6 +584,102 @@ func TestNewAgentInstance_ReadFileModeSelectsSchema(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// write_file copy names append_file/edit_file only when they are registered.
|
||||
func TestNewAgentInstance_WriteFileCopyReflectsAvailableAltTools(t *testing.T) {
|
||||
newCfg := func(editEnabled, appendEnabled bool) *config.Config {
|
||||
return &config.Config{
|
||||
Agents: config.AgentsConfig{
|
||||
Defaults: config.AgentDefaults{
|
||||
Workspace: t.TempDir(),
|
||||
ModelName: "test-model",
|
||||
},
|
||||
},
|
||||
Tools: config.ToolsConfig{
|
||||
WriteFile: config.ToolConfig{Enabled: true},
|
||||
EditFile: config.ToolConfig{Enabled: editEnabled},
|
||||
AppendFile: config.ToolConfig{Enabled: appendEnabled},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
writeToolDesc := func(cfg *config.Config) string {
|
||||
agent := NewAgentInstance(nil, &cfg.Agents.Defaults, cfg, &mockProvider{})
|
||||
writeTool, ok := agent.Tools.Get("write_file")
|
||||
if !ok {
|
||||
t.Fatal("write_file tool not registered")
|
||||
}
|
||||
return writeTool.Description()
|
||||
}
|
||||
|
||||
t.Run("only write_file exposed", func(t *testing.T) {
|
||||
desc := writeToolDesc(newCfg(false, false))
|
||||
if strings.Contains(desc, "append_file") || strings.Contains(desc, "edit_file") {
|
||||
t.Fatalf("write_file must not reference unavailable tools, got: %q", desc)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("only append_file exposed", func(t *testing.T) {
|
||||
desc := writeToolDesc(newCfg(false, true))
|
||||
if !strings.Contains(desc, "append_file") {
|
||||
t.Fatalf("expected write_file to reference append_file, got: %q", desc)
|
||||
}
|
||||
if strings.Contains(desc, "edit_file") {
|
||||
t.Fatalf("write_file must not reference disabled edit_file, got: %q", desc)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("both exposed", func(t *testing.T) {
|
||||
desc := writeToolDesc(newCfg(true, true))
|
||||
if !strings.Contains(desc, "append_file") || !strings.Contains(desc, "edit_file") {
|
||||
t.Fatalf("expected write_file to reference both alternatives, got: %q", desc)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Availability follows the per-agent allowlist, not just the enable flag:
|
||||
// editors enabled globally but hidden by frontmatter must not be named.
|
||||
func TestNewAgentInstance_WriteFileCopyExcludesAllowlistHiddenAltTools(t *testing.T) {
|
||||
workspace := setupWorkspace(t, map[string]string{
|
||||
"AGENT.md": "---\ntools: [write_file]\n---\n# Agent\n",
|
||||
})
|
||||
defer cleanupWorkspace(t, workspace)
|
||||
|
||||
cfg := &config.Config{
|
||||
Agents: config.AgentsConfig{
|
||||
Defaults: config.AgentDefaults{
|
||||
Workspace: workspace,
|
||||
ModelName: "test-model",
|
||||
},
|
||||
},
|
||||
Tools: config.ToolsConfig{
|
||||
WriteFile: config.ToolConfig{Enabled: true},
|
||||
EditFile: config.ToolConfig{Enabled: true},
|
||||
AppendFile: config.ToolConfig{Enabled: true},
|
||||
},
|
||||
}
|
||||
|
||||
agent := NewAgentInstance(&config.AgentConfig{
|
||||
ID: "restricted",
|
||||
Workspace: workspace,
|
||||
}, &cfg.Agents.Defaults, cfg, &mockProvider{})
|
||||
|
||||
if _, ok := agent.Tools.Get("edit_file"); ok {
|
||||
t.Fatal("edit_file should be blocked by the allowlist")
|
||||
}
|
||||
if _, ok := agent.Tools.Get("append_file"); ok {
|
||||
t.Fatal("append_file should be blocked by the allowlist")
|
||||
}
|
||||
|
||||
writeTool, ok := agent.Tools.Get("write_file")
|
||||
if !ok {
|
||||
t.Fatal("write_file tool not registered")
|
||||
}
|
||||
if desc := writeTool.Description(); strings.Contains(desc, "append_file") ||
|
||||
strings.Contains(desc, "edit_file") {
|
||||
t.Fatalf("write_file must not name allowlist-hidden tools, got: %q", desc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewAgentInstance_InvalidExecConfigDoesNotExit(t *testing.T) {
|
||||
workspace := t.TempDir()
|
||||
|
||||
|
|
|
|||
227
pkg/channels/buzz/buzz.go
Normal file
227
pkg/channels/buzz/buzz.go
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
// Package buzz implements a Channel for Buzz, a Nostr-based relay chat.
|
||||
//
|
||||
// Wire format: chat messages are kind:9 events scoped to a channel by an "h"
|
||||
// tag. Mentions are "p" tags carrying the mentioned pubkey. The relay requires
|
||||
// NIP-42 authentication (kind:22242) before it accepts a subscription.
|
||||
package buzz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip19"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/bus"
|
||||
"github.com/sipeed/picoclaw/pkg/channels"
|
||||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
"github.com/sipeed/picoclaw/pkg/logger"
|
||||
)
|
||||
|
||||
// kindStreamMessage is the Buzz chat message kind (NIP-29 style).
|
||||
const kindStreamMessage = 9
|
||||
|
||||
// BuzzChannel implements the Channel interface for a Buzz relay.
|
||||
type BuzzChannel struct {
|
||||
*channels.BaseChannel
|
||||
bc *config.Channel
|
||||
config *config.BuzzSettings
|
||||
|
||||
secretKey string
|
||||
publicKey string
|
||||
|
||||
relay *nostr.Relay
|
||||
sub *nostr.Subscription
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// NewBuzzChannel creates a new Buzz channel.
|
||||
func NewBuzzChannel(
|
||||
bc *config.Channel,
|
||||
cfg *config.BuzzSettings,
|
||||
messageBus *bus.MessageBus,
|
||||
) (*BuzzChannel, error) {
|
||||
if cfg.RelayURL == "" {
|
||||
return nil, fmt.Errorf("buzz relay_url is required")
|
||||
}
|
||||
if len(cfg.Channels) == 0 {
|
||||
return nil, fmt.Errorf("buzz channels is required: at least one channel ID to join")
|
||||
}
|
||||
|
||||
sk, err := normalizeSecretKey(cfg.PrivateKey.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pk, err := nostr.GetPublicKey(sk)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("buzz private_key is not a valid secret key: %w", err)
|
||||
}
|
||||
|
||||
base := channels.NewBaseChannel("buzz", cfg, messageBus, bc.AllowFrom,
|
||||
channels.WithGroupTrigger(bc.GroupTrigger),
|
||||
channels.WithReasoningChannelID(bc.ReasoningChannelID),
|
||||
)
|
||||
|
||||
return &BuzzChannel{
|
||||
BaseChannel: base,
|
||||
bc: bc,
|
||||
config: cfg,
|
||||
secretKey: sk,
|
||||
publicKey: pk,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// normalizeSecretKey accepts either a 64-char hex secret key or an nsec1
|
||||
// bech32 string and returns the hex form.
|
||||
func normalizeSecretKey(key string) (string, error) {
|
||||
key = strings.TrimSpace(key)
|
||||
if key == "" {
|
||||
return "", fmt.Errorf("buzz private_key is required")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(key, "nsec1") {
|
||||
prefix, value, err := nip19.Decode(key)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("buzz private_key: invalid nsec: %w", err)
|
||||
}
|
||||
if prefix != "nsec" {
|
||||
return "", fmt.Errorf("buzz private_key: expected nsec, got %s", prefix)
|
||||
}
|
||||
sk, ok := value.(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("buzz private_key: unexpected nsec payload")
|
||||
}
|
||||
return sk, nil
|
||||
}
|
||||
|
||||
if len(key) != 64 {
|
||||
return "", fmt.Errorf("buzz private_key must be 64-char hex or an nsec1 string")
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// Start connects to the relay, authenticates via NIP-42, and subscribes to the
|
||||
// configured channels.
|
||||
func (c *BuzzChannel) Start(ctx context.Context) error {
|
||||
logger.InfoC("buzz", "Starting Buzz channel")
|
||||
c.ctx, c.cancel = context.WithCancel(ctx)
|
||||
|
||||
relay, err := nostr.RelayConnect(c.ctx, c.config.RelayURL)
|
||||
if err != nil {
|
||||
c.cancel()
|
||||
return fmt.Errorf("buzz relay connect failed: %w", err)
|
||||
}
|
||||
c.relay = relay
|
||||
|
||||
// NIP-42: the relay challenges, we sign the auth event with the bot identity.
|
||||
// Buzz relays reject subscriptions from unauthenticated clients, so a failure
|
||||
// here is fatal rather than advisory.
|
||||
if err := relay.Auth(c.ctx, func(evt *nostr.Event) error {
|
||||
return evt.Sign(c.secretKey)
|
||||
}); err != nil {
|
||||
_ = relay.Close()
|
||||
c.cancel()
|
||||
return fmt.Errorf("buzz NIP-42 auth failed: %w", err)
|
||||
}
|
||||
|
||||
channelIDs := []string(c.config.Channels)
|
||||
filters := nostr.Filters{{
|
||||
Kinds: []int{kindStreamMessage},
|
||||
Tags: nostr.TagMap{"h": channelIDs},
|
||||
}}
|
||||
|
||||
sub, err := relay.Subscribe(c.ctx, filters)
|
||||
if err != nil {
|
||||
_ = relay.Close()
|
||||
c.cancel()
|
||||
return fmt.Errorf("buzz subscribe failed: %w", err)
|
||||
}
|
||||
c.sub = sub
|
||||
|
||||
c.wg.Add(1)
|
||||
go func() {
|
||||
defer c.wg.Done()
|
||||
c.consume(sub)
|
||||
}()
|
||||
|
||||
c.SetRunning(true)
|
||||
logger.InfoCF("buzz", "Buzz channel started", map[string]any{
|
||||
"relay": c.config.RelayURL,
|
||||
"pubkey": c.publicKey,
|
||||
"channels": len(channelIDs),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop closes the subscription and disconnects from the relay.
|
||||
func (c *BuzzChannel) Stop(ctx context.Context) error {
|
||||
logger.InfoC("buzz", "Stopping Buzz channel")
|
||||
c.SetRunning(false)
|
||||
|
||||
if c.cancel != nil {
|
||||
c.cancel()
|
||||
}
|
||||
if c.sub != nil {
|
||||
c.sub.Unsub()
|
||||
}
|
||||
if c.relay != nil {
|
||||
if err := c.relay.Close(); err != nil {
|
||||
logger.WarnCF("buzz", "Relay close failed", map[string]any{"error": err.Error()})
|
||||
}
|
||||
}
|
||||
c.wg.Wait()
|
||||
|
||||
logger.InfoC("buzz", "Buzz channel stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Send publishes a kind:9 message scoped to the target channel.
|
||||
func (c *BuzzChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
|
||||
if !c.IsRunning() {
|
||||
return nil, channels.ErrNotRunning
|
||||
}
|
||||
|
||||
target := msg.ChatID
|
||||
if target == "" {
|
||||
return nil, fmt.Errorf("chat ID is empty: %w", channels.ErrSendFailed)
|
||||
}
|
||||
if strings.TrimSpace(msg.Content) == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
tags := nostr.Tags{nostr.Tag{"h", target}}
|
||||
if c.config.ReplyInThread && msg.ReplyToMessageID != "" {
|
||||
tags = append(tags, nostr.Tag{"e", msg.ReplyToMessageID, "", "reply"})
|
||||
}
|
||||
|
||||
evt := nostr.Event{
|
||||
PubKey: c.publicKey,
|
||||
CreatedAt: nostr.Now(),
|
||||
Kind: kindStreamMessage,
|
||||
Tags: tags,
|
||||
Content: msg.Content,
|
||||
}
|
||||
if err := evt.Sign(c.secretKey); err != nil {
|
||||
return nil, fmt.Errorf("buzz sign failed: %w", errJoin(err, channels.ErrSendFailed))
|
||||
}
|
||||
|
||||
if err := c.relay.Publish(ctx, evt); err != nil {
|
||||
return nil, fmt.Errorf("buzz publish failed: %w", errJoin(err, channels.ErrSendFailed))
|
||||
}
|
||||
|
||||
logger.DebugCF("buzz", "Message sent", map[string]any{
|
||||
"channel": target,
|
||||
"event_id": evt.ID,
|
||||
})
|
||||
return []string{evt.ID}, nil
|
||||
}
|
||||
|
||||
// errJoin wraps err so that errors.Is(result, sentinel) holds for the sentinel
|
||||
// while preserving the underlying cause in the message.
|
||||
func errJoin(err, sentinel error) error {
|
||||
return fmt.Errorf("%v: %w", err, sentinel)
|
||||
}
|
||||
127
pkg/channels/buzz/buzz_test.go
Normal file
127
pkg/channels/buzz/buzz_test.go
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
package buzz
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip19"
|
||||
)
|
||||
|
||||
func TestNormalizeSecretKeyAcceptsHex(t *testing.T) {
|
||||
sk := nostr.GeneratePrivateKey()
|
||||
|
||||
got, err := normalizeSecretKey(sk)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got != sk {
|
||||
t.Fatalf("expected %s, got %s", sk, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeSecretKeyAcceptsNsec(t *testing.T) {
|
||||
sk := nostr.GeneratePrivateKey()
|
||||
nsec, err := nip19.EncodePrivateKey(sk)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to encode nsec: %v", err)
|
||||
}
|
||||
|
||||
got, err := normalizeSecretKey(nsec)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got != sk {
|
||||
t.Fatalf("expected nsec to decode to %s, got %s", sk, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeSecretKeyTrimsWhitespace(t *testing.T) {
|
||||
sk := nostr.GeneratePrivateKey()
|
||||
|
||||
got, err := normalizeSecretKey(" " + sk + "\n")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got != sk {
|
||||
t.Fatalf("expected %s, got %s", sk, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeSecretKeyRejectsBadInput(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"empty": "",
|
||||
"whitespace": " ",
|
||||
"short hex": "abcdef",
|
||||
"bad nsec": "nsec1notarealkey",
|
||||
"npub prefix": "npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
|
||||
}
|
||||
|
||||
for name, input := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if _, err := normalizeSecretKey(input); err == nil {
|
||||
t.Fatalf("expected an error for %q", input)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirstTagValue(t *testing.T) {
|
||||
tags := nostr.Tags{
|
||||
nostr.Tag{"p", "pubkey-one"},
|
||||
nostr.Tag{"h", "channel-abc"},
|
||||
nostr.Tag{"h", "channel-second"},
|
||||
nostr.Tag{"malformed"},
|
||||
}
|
||||
|
||||
if got := firstTagValue(tags, "h"); got != "channel-abc" {
|
||||
t.Fatalf("expected first h tag, got %q", got)
|
||||
}
|
||||
if got := firstTagValue(tags, "e"); got != "" {
|
||||
t.Fatalf("expected empty string for missing tag, got %q", got)
|
||||
}
|
||||
// A tag with no value must not panic or be treated as present.
|
||||
if got := firstTagValue(tags, "malformed"); got != "" {
|
||||
t.Fatalf("expected empty string for valueless tag, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasPTag(t *testing.T) {
|
||||
const self = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||
|
||||
tags := nostr.Tags{
|
||||
nostr.Tag{"h", "channel-abc"},
|
||||
nostr.Tag{"p", strings.ToUpper(self)},
|
||||
}
|
||||
if !hasPTag(tags, self) {
|
||||
t.Fatal("expected p tag match to be case-insensitive")
|
||||
}
|
||||
|
||||
other := nostr.Tags{
|
||||
nostr.Tag{"h", "channel-abc"},
|
||||
nostr.Tag{"p", "ffff"},
|
||||
}
|
||||
if hasPTag(other, self) {
|
||||
t.Fatal("expected no match for a different pubkey")
|
||||
}
|
||||
|
||||
// An "h" tag carrying our pubkey must not count as a mention.
|
||||
spoof := nostr.Tags{nostr.Tag{"h", self}}
|
||||
if hasPTag(spoof, self) {
|
||||
t.Fatal("expected only p tags to count as mentions")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShortPubkey(t *testing.T) {
|
||||
const full = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||
|
||||
got := shortPubkey(full)
|
||||
if !strings.HasPrefix(got, "01234567") || !strings.HasSuffix(got, "cdef") {
|
||||
t.Fatalf("unexpected short form: %s", got)
|
||||
}
|
||||
|
||||
// Short inputs are returned unchanged rather than sliced out of range.
|
||||
if got := shortPubkey("abc"); got != "abc" {
|
||||
t.Fatalf("expected short input unchanged, got %s", got)
|
||||
}
|
||||
}
|
||||
125
pkg/channels/buzz/handler.go
Normal file
125
pkg/channels/buzz/handler.go
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
package buzz
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/bus"
|
||||
"github.com/sipeed/picoclaw/pkg/identity"
|
||||
"github.com/sipeed/picoclaw/pkg/logger"
|
||||
)
|
||||
|
||||
// consume drains the subscription until the channel context is cancelled.
|
||||
func (c *BuzzChannel) consume(sub *nostr.Subscription) {
|
||||
for {
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
return
|
||||
case reason, ok := <-sub.ClosedReason:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
logger.WarnCF("buzz", "Relay closed subscription", map[string]any{"reason": reason})
|
||||
return
|
||||
case evt, ok := <-sub.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
c.onEvent(evt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// onEvent converts a kind:9 relay event into an inbound bus message.
|
||||
func (c *BuzzChannel) onEvent(evt *nostr.Event) {
|
||||
if evt == nil || evt.Kind != kindStreamMessage {
|
||||
return
|
||||
}
|
||||
|
||||
// Ignore our own messages, otherwise the agent replies to itself.
|
||||
if evt.PubKey == c.publicKey {
|
||||
return
|
||||
}
|
||||
|
||||
// The "h" tag scopes the event to a channel; without it there is nowhere to reply.
|
||||
chatID := firstTagValue(evt.Tags, "h")
|
||||
if chatID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
sender := bus.SenderInfo{
|
||||
Platform: "buzz",
|
||||
PlatformID: evt.PubKey,
|
||||
CanonicalID: identity.BuildCanonicalID("buzz", evt.PubKey),
|
||||
Username: shortPubkey(evt.PubKey),
|
||||
DisplayName: shortPubkey(evt.PubKey),
|
||||
}
|
||||
|
||||
if !c.IsAllowedSender(sender) {
|
||||
return
|
||||
}
|
||||
|
||||
// Mentions are "p" tags carrying the mentioned pubkey.
|
||||
isMentioned := hasPTag(evt.Tags, c.publicKey)
|
||||
|
||||
content := evt.Content
|
||||
respond, cleaned := c.ShouldRespondInGroup(isMentioned, content)
|
||||
if !respond {
|
||||
return
|
||||
}
|
||||
content = cleaned
|
||||
|
||||
if strings.TrimSpace(content) == "" {
|
||||
return
|
||||
}
|
||||
|
||||
inboundCtx := bus.InboundContext{
|
||||
Channel: "buzz",
|
||||
ChatID: chatID,
|
||||
ChatType: "channel",
|
||||
SenderID: evt.PubKey,
|
||||
MessageID: evt.ID,
|
||||
Mentioned: isMentioned,
|
||||
Raw: map[string]string{
|
||||
"platform": "buzz",
|
||||
"relay": c.config.RelayURL,
|
||||
"channel": chatID,
|
||||
},
|
||||
}
|
||||
|
||||
if err := c.HandleInboundContext(c.ctx, chatID, content, nil, inboundCtx, sender); err != nil {
|
||||
logger.WarnCF("buzz", "Inbound handling failed", map[string]any{
|
||||
"error": err.Error(),
|
||||
"event_id": evt.ID,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// firstTagValue returns the value of the first tag with the given key.
|
||||
func firstTagValue(tags nostr.Tags, key string) string {
|
||||
for _, t := range tags {
|
||||
if len(t) >= 2 && t[0] == key {
|
||||
return t[1]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// hasPTag reports whether any "p" tag references the given pubkey.
|
||||
func hasPTag(tags nostr.Tags, pubkey string) bool {
|
||||
for _, t := range tags {
|
||||
if len(t) >= 2 && t[0] == "p" && strings.EqualFold(t[1], pubkey) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// shortPubkey renders a pubkey as a readable handle for display purposes.
|
||||
func shortPubkey(pubkey string) string {
|
||||
if len(pubkey) <= 12 {
|
||||
return pubkey
|
||||
}
|
||||
return pubkey[:8] + "…" + pubkey[len(pubkey)-4:]
|
||||
}
|
||||
35
pkg/channels/buzz/init.go
Normal file
35
pkg/channels/buzz/init.go
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
package buzz
|
||||
|
||||
import (
|
||||
"github.com/sipeed/picoclaw/pkg/bus"
|
||||
"github.com/sipeed/picoclaw/pkg/channels"
|
||||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
)
|
||||
|
||||
func init() {
|
||||
channels.RegisterFactory(
|
||||
config.ChannelBuzz,
|
||||
func(channelName, channelType string, cfg *config.Config, b *bus.MessageBus) (channels.Channel, error) {
|
||||
bc := cfg.Channels[channelName]
|
||||
if bc == nil || !bc.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
decoded, err := bc.GetDecoded()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c, ok := decoded.(*config.BuzzSettings)
|
||||
if !ok {
|
||||
return nil, channels.ErrSendFailed
|
||||
}
|
||||
ch, err := NewBuzzChannel(bc, c, b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if channelName != config.ChannelBuzz {
|
||||
ch.SetName(channelName)
|
||||
}
|
||||
return ch, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -69,7 +69,8 @@ func NewLINEChannel(
|
|||
return nil, fmt.Errorf("failed to create LINE messaging client: %w", err)
|
||||
}
|
||||
|
||||
base := channels.NewBaseChannel("line", cfg, messageBus, bc.AllowFrom,
|
||||
base := channels.NewBaseChannel(
|
||||
"line", cfg, messageBus, bc.AllowFrom,
|
||||
channels.WithMaxMessageLength(5000),
|
||||
channels.WithGroupTrigger(bc.GroupTrigger),
|
||||
channels.WithReasoningChannelID(bc.ReasoningChannelID),
|
||||
|
|
@ -482,7 +483,7 @@ func (c *LINEChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]stri
|
|||
Messages: []messaging_api.MessageInterface{&textMsg},
|
||||
})
|
||||
if resp != nil && resp.Body != nil {
|
||||
resp.Body.Close()
|
||||
_ = resp.Body.Close()
|
||||
}
|
||||
if err == nil {
|
||||
logger.DebugCF("line", "Message sent via Reply API", map[string]any{
|
||||
|
|
@ -588,7 +589,7 @@ func (c *LINEChannel) StartTyping(ctx context.Context, chatID string) (func(), e
|
|||
// classifySDKError maps an SDK HTTP response to the project's sentinel errors.
|
||||
func classifySDKError(resp *http.Response, err error) error {
|
||||
if resp != nil && resp.Body != nil {
|
||||
resp.Body.Close()
|
||||
_ = resp.Body.Close()
|
||||
}
|
||||
if err == nil {
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -706,6 +706,23 @@ type IRCSettings struct {
|
|||
RequestCaps FlexibleStringSlice `json:"request_caps,omitempty" yaml:"-"`
|
||||
}
|
||||
|
||||
// BuzzSettings configures the Buzz channel, a Nostr-based relay chat.
|
||||
//
|
||||
// Messages are NIP-29 style: kind:9 events scoped to a channel by an "h" tag,
|
||||
// with mentions carried as "p" tags. The relay requires NIP-42 authentication
|
||||
// (kind:22242) before it will accept a subscription.
|
||||
type BuzzSettings struct {
|
||||
// RelayURL is the relay websocket endpoint, e.g. wss://relay.example.com.
|
||||
RelayURL string `json:"relay_url" yaml:"-" env:"PICOCLAW_CHANNELS_BUZZ_RELAY_URL"`
|
||||
// PrivateKey is the bot identity as a 64-char hex secret key or an nsec1 string.
|
||||
PrivateKey SecureString `json:"private_key,omitzero" yaml:"private_key,omitempty" env:"PICOCLAW_CHANNELS_BUZZ_PRIVATE_KEY"`
|
||||
// Channels lists the channel IDs ("h" tag values) to join.
|
||||
Channels FlexibleStringSlice `json:"channels" yaml:"-" env:"PICOCLAW_CHANNELS_BUZZ_CHANNELS"`
|
||||
// ReplyInThread emits an "e" tag referencing the triggering event so replies
|
||||
// thread under it rather than appearing as top-level channel messages.
|
||||
ReplyInThread bool `json:"reply_in_thread,omitempty" yaml:"-" env:"PICOCLAW_CHANNELS_BUZZ_REPLY_IN_THREAD"`
|
||||
}
|
||||
|
||||
type VKSettings struct {
|
||||
Token SecureString `json:"token,omitzero" yaml:"token,omitempty" env:"PICOCLAW_CHANNELS_VK_TOKEN"`
|
||||
GroupID int `json:"group_id" yaml:"-" env:"PICOCLAW_CHANNELS_VK_GROUP_ID"`
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ const (
|
|||
ChannelTeamsWebHook = "teams_webhook"
|
||||
ChannelMQTT = "mqtt"
|
||||
ChannelSlackWebHook = "slack_webhook"
|
||||
ChannelBuzz = "buzz"
|
||||
)
|
||||
|
||||
func initChannel() {
|
||||
|
|
@ -682,6 +683,7 @@ var channelSettingsFactory = map[string]any{
|
|||
ChannelTeamsWebHook: (TeamsWebhookSettings{}),
|
||||
ChannelMQTT: (MQTTSettings{}),
|
||||
ChannelSlackWebHook: (SlackWebhookSettings{}),
|
||||
ChannelBuzz: (BuzzSettings{}),
|
||||
}
|
||||
|
||||
// RegisterChannelSettings registers a settings struct prototype for a custom
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import (
|
|||
"github.com/sipeed/picoclaw/pkg/audio/tts"
|
||||
"github.com/sipeed/picoclaw/pkg/bus"
|
||||
"github.com/sipeed/picoclaw/pkg/channels"
|
||||
_ "github.com/sipeed/picoclaw/pkg/channels/buzz"
|
||||
_ "github.com/sipeed/picoclaw/pkg/channels/deltachat"
|
||||
_ "github.com/sipeed/picoclaw/pkg/channels/dingtalk"
|
||||
_ "github.com/sipeed/picoclaw/pkg/channels/discord"
|
||||
|
|
|
|||
|
|
@ -181,8 +181,15 @@ func buildParams(
|
|||
blocks = append(blocks, anthropic.NewTextBlock(msg.Content))
|
||||
}
|
||||
for _, tc := range msg.ToolCalls {
|
||||
// Resolve tool name: prefer tc.Name, fallback to tc.Function.Name
|
||||
// (tc.Name/tc.Arguments are json:"-" and may be empty when
|
||||
// history is reloaded from the session store)
|
||||
toolName := tc.Name
|
||||
if toolName == "" && tc.Function != nil {
|
||||
toolName = tc.Function.Name
|
||||
}
|
||||
// Skip tool calls with empty names to avoid API errors
|
||||
if tc.Name == "" {
|
||||
if toolName == "" {
|
||||
continue
|
||||
}
|
||||
args := tc.Arguments
|
||||
|
|
@ -194,7 +201,7 @@ func buildParams(
|
|||
if args == nil {
|
||||
args = map[string]any{}
|
||||
}
|
||||
blocks = append(blocks, anthropic.NewToolUseBlock(tc.ID, args, tc.Name))
|
||||
blocks = append(blocks, anthropic.NewToolUseBlock(tc.ID, args, toolName))
|
||||
}
|
||||
anthropicMessages = append(anthropicMessages, anthropic.NewAssistantMessage(blocks...))
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
|
|
@ -77,6 +78,124 @@ func TestBuildParams_ToolCallMessage(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestBuildParams_ToolCallFunctionFallback verifies that tool calls whose
|
||||
// runtime-only fields were lost in a JSON round-trip through the session store
|
||||
// (ToolCall.Name/Arguments are json:"-"; only ToolCall.Function survives) fall
|
||||
// back to Function.Name / Function.Arguments, so the tool_use block is still
|
||||
// emitted and its tool_result pair stays intact. Without the fallback the
|
||||
// tool_use is skipped and the orphaned tool_result 400s at the API
|
||||
// ("unexpected tool_use_id found in tool_result blocks").
|
||||
func TestBuildParams_ToolCallFunctionFallback(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
toolCall ToolCall
|
||||
wantSkipped bool
|
||||
wantToolName string
|
||||
wantInput map[string]any
|
||||
}{
|
||||
{
|
||||
name: "deserialized history shape falls back to Function fields",
|
||||
toolCall: ToolCall{
|
||||
ID: "toolu-fallback-1",
|
||||
Name: "",
|
||||
Arguments: nil,
|
||||
Function: &FunctionCall{Name: "x", Arguments: `{"a":1}`},
|
||||
},
|
||||
wantToolName: "x",
|
||||
wantInput: map[string]any{"a": float64(1)},
|
||||
},
|
||||
{
|
||||
name: "runtime shape with Name set and Function nil still works",
|
||||
toolCall: ToolCall{
|
||||
ID: "toolu-runtime-1",
|
||||
Name: "y",
|
||||
Arguments: map[string]any{"b": 2},
|
||||
},
|
||||
wantToolName: "y",
|
||||
wantInput: map[string]any{"b": 2},
|
||||
},
|
||||
{
|
||||
name: "both Name and Function.Name empty is skipped",
|
||||
toolCall: ToolCall{
|
||||
ID: "toolu-empty-1",
|
||||
Name: "",
|
||||
Function: &FunctionCall{Name: "", Arguments: `{"c":3}`},
|
||||
},
|
||||
wantSkipped: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
messages := []Message{
|
||||
{Role: "user", Content: "run the tool"},
|
||||
{Role: "assistant", Content: "", ToolCalls: []ToolCall{tt.toolCall}},
|
||||
{Role: "tool", ToolCallID: tt.toolCall.ID, Content: "result"},
|
||||
}
|
||||
|
||||
params, err := buildParams(messages, nil, "claude-sonnet-4.6", map[string]any{})
|
||||
if err != nil {
|
||||
t.Fatalf("buildParams() error: %v", err)
|
||||
}
|
||||
if len(params.Messages) != 3 {
|
||||
t.Fatalf("len(Messages) = %d, want 3", len(params.Messages))
|
||||
}
|
||||
|
||||
assistantMsg := params.Messages[1]
|
||||
var toolUses []*anthropic.ToolUseBlockParam
|
||||
for _, block := range assistantMsg.Content {
|
||||
if block.OfToolUse != nil {
|
||||
toolUses = append(toolUses, block.OfToolUse)
|
||||
}
|
||||
}
|
||||
|
||||
// The tool_result in the following user message always carries the
|
||||
// original ID; look it up once for both branches.
|
||||
toolResultMsg := params.Messages[2]
|
||||
if len(toolResultMsg.Content) != 1 || toolResultMsg.Content[0].OfToolResult == nil {
|
||||
t.Fatalf("message after assistant = %+v, want single tool_result block", toolResultMsg.Content)
|
||||
}
|
||||
toolResult := toolResultMsg.Content[0].OfToolResult
|
||||
|
||||
if tt.wantSkipped {
|
||||
if len(toolUses) != 0 {
|
||||
t.Fatalf("tool_use blocks = %d, want 0 (tool call skipped)", len(toolUses))
|
||||
}
|
||||
// Note: matching current behavior, the orphaned tool_result is
|
||||
// still emitted even though its tool_use block was skipped.
|
||||
if toolResult.ToolUseID != tt.toolCall.ID {
|
||||
t.Fatalf("orphaned tool_result ToolUseID = %q, want %q",
|
||||
toolResult.ToolUseID, tt.toolCall.ID)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// (a) tool_use block emitted with resolved name, id, and parsed input.
|
||||
if len(toolUses) != 1 {
|
||||
t.Fatalf("tool_use blocks = %d, want 1", len(toolUses))
|
||||
}
|
||||
toolUse := toolUses[0]
|
||||
if toolUse.Name != tt.wantToolName {
|
||||
t.Errorf("tool_use Name = %q, want %q", toolUse.Name, tt.wantToolName)
|
||||
}
|
||||
if toolUse.ID != tt.toolCall.ID {
|
||||
t.Errorf("tool_use ID = %q, want %q", toolUse.ID, tt.toolCall.ID)
|
||||
}
|
||||
gotInput, ok := toolUse.Input.(map[string]any)
|
||||
if !ok || !reflect.DeepEqual(gotInput, tt.wantInput) {
|
||||
t.Errorf("tool_use Input = %#v, want %#v", toolUse.Input, tt.wantInput)
|
||||
}
|
||||
|
||||
// (b) the following user message's tool_result references the same
|
||||
// id as the tool_use block — the pair is intact.
|
||||
if toolResult.ToolUseID != toolUse.ID {
|
||||
t.Errorf("tool_result ToolUseID = %q, want %q (paired with tool_use)",
|
||||
toolResult.ToolUseID, toolUse.ID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildParams_WithTools(t *testing.T) {
|
||||
tools := []ToolDefinition{
|
||||
{
|
||||
|
|
|
|||
|
|
@ -233,12 +233,26 @@ func buildRequestBody(
|
|||
|
||||
// Add tool_use blocks
|
||||
for _, tc := range msg.ToolCalls {
|
||||
if strings.TrimSpace(tc.Name) == "" {
|
||||
// Resolve tool name: prefer tc.Name, fallback to tc.Function.Name
|
||||
// (tc.Name/tc.Arguments are json:"-" and may be empty when
|
||||
// history is reloaded from the session store)
|
||||
toolName := tc.Name
|
||||
if toolName == "" && tc.Function != nil {
|
||||
toolName = tc.Function.Name
|
||||
}
|
||||
if strings.TrimSpace(toolName) == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle nil Arguments (GLM-4 may return null input)
|
||||
// Resolve arguments: prefer tc.Arguments, fallback to parsing
|
||||
// tc.Function.Arguments
|
||||
input := tc.Arguments
|
||||
if input == nil && tc.Function != nil && tc.Function.Arguments != "" {
|
||||
if err := json.Unmarshal([]byte(tc.Function.Arguments), &input); err != nil {
|
||||
input = map[string]any{}
|
||||
}
|
||||
}
|
||||
// Handle nil Arguments (GLM-4 may return null input)
|
||||
if input == nil {
|
||||
input = map[string]any{}
|
||||
}
|
||||
|
|
@ -246,7 +260,7 @@ func buildRequestBody(
|
|||
toolUse := map[string]any{
|
||||
"type": "tool_use",
|
||||
"id": tc.ID,
|
||||
"name": tc.Name,
|
||||
"name": toolName,
|
||||
"input": input,
|
||||
}
|
||||
content = append(content, toolUse)
|
||||
|
|
|
|||
|
|
@ -614,6 +614,126 @@ func TestBuildRequestBody_UserToolResultsMerged(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestBuildRequestBody_ToolCallFunctionFallback verifies that tool calls whose
|
||||
// runtime-only fields were lost in a JSON round-trip through the session store
|
||||
// (ToolCall.Name/Arguments are json:"-"; only ToolCall.Function survives) fall
|
||||
// back to Function.Name / Function.Arguments, so the tool_use block is still
|
||||
// emitted and its tool_result pair stays intact. Without the fallback the
|
||||
// tool_use is skipped and the orphaned tool_result 400s at the API
|
||||
// ("unexpected tool_use_id found in tool_result blocks").
|
||||
func TestBuildRequestBody_ToolCallFunctionFallback(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
toolCall ToolCall
|
||||
wantSkipped bool
|
||||
wantToolName string
|
||||
wantInput map[string]any
|
||||
}{
|
||||
{
|
||||
name: "deserialized history shape falls back to Function fields",
|
||||
toolCall: ToolCall{
|
||||
ID: "toolu-fallback-1",
|
||||
Name: "",
|
||||
Arguments: nil,
|
||||
Function: &FunctionCall{Name: "x", Arguments: `{"a":1}`},
|
||||
},
|
||||
wantToolName: "x",
|
||||
wantInput: map[string]any{"a": float64(1)},
|
||||
},
|
||||
{
|
||||
name: "runtime shape with Name set and Function nil still works",
|
||||
toolCall: ToolCall{
|
||||
ID: "toolu-runtime-1",
|
||||
Name: "y",
|
||||
Arguments: map[string]any{"b": 2},
|
||||
},
|
||||
wantToolName: "y",
|
||||
wantInput: map[string]any{"b": 2},
|
||||
},
|
||||
{
|
||||
name: "both Name and Function.Name empty is skipped",
|
||||
toolCall: ToolCall{
|
||||
ID: "toolu-empty-1",
|
||||
Name: "",
|
||||
Function: &FunctionCall{Name: "", Arguments: `{"c":3}`},
|
||||
},
|
||||
wantSkipped: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
messages := []Message{
|
||||
{Role: "user", Content: "run the tool"},
|
||||
{Role: "assistant", Content: "", ToolCalls: []ToolCall{tt.toolCall}},
|
||||
{Role: "tool", ToolCallID: tt.toolCall.ID, Content: "result"},
|
||||
}
|
||||
|
||||
got, err := buildRequestBody(messages, nil, "test-model", map[string]any{"max_tokens": 8192})
|
||||
if err != nil {
|
||||
t.Fatalf("buildRequestBody() error: %v", err)
|
||||
}
|
||||
|
||||
apiMessages := got["messages"].([]any)
|
||||
if len(apiMessages) != 3 {
|
||||
t.Fatalf("expected 3 API messages, got %d", len(apiMessages))
|
||||
}
|
||||
|
||||
assistantMsg := apiMessages[1].(map[string]any)
|
||||
content := assistantMsg["content"].([]any)
|
||||
|
||||
if tt.wantSkipped {
|
||||
if len(content) != 0 {
|
||||
t.Fatalf("assistant content = %#v, want empty (tool call skipped)", content)
|
||||
}
|
||||
// Note: matching current behavior, the orphaned tool_result is
|
||||
// still emitted in the following user message even though its
|
||||
// tool_use block was skipped.
|
||||
toolResultMsg := apiMessages[2].(map[string]any)
|
||||
blocks := toolResultMsg["content"].([]map[string]any)
|
||||
if len(blocks) != 1 || blocks[0]["tool_use_id"] != tt.toolCall.ID {
|
||||
t.Fatalf("orphaned tool_result = %#v, want single block with tool_use_id %q",
|
||||
blocks, tt.toolCall.ID)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// (a) tool_use block emitted with resolved name, id, and parsed input.
|
||||
if len(content) != 1 {
|
||||
t.Fatalf("assistant content length = %d, want 1 tool_use block", len(content))
|
||||
}
|
||||
toolUse := content[0].(map[string]any)
|
||||
if toolUse["type"] != "tool_use" {
|
||||
t.Fatalf("block type = %v, want tool_use", toolUse["type"])
|
||||
}
|
||||
if toolUse["name"] != tt.wantToolName {
|
||||
t.Errorf("tool_use name = %v, want %q", toolUse["name"], tt.wantToolName)
|
||||
}
|
||||
if toolUse["id"] != tt.toolCall.ID {
|
||||
t.Errorf("tool_use id = %v, want %q", toolUse["id"], tt.toolCall.ID)
|
||||
}
|
||||
if !reflect.DeepEqual(toolUse["input"], tt.wantInput) {
|
||||
t.Errorf("tool_use input = %#v, want %#v", toolUse["input"], tt.wantInput)
|
||||
}
|
||||
|
||||
// (b) the following user message's tool_result references the same
|
||||
// id as the tool_use block — the pair is intact.
|
||||
toolResultMsg := apiMessages[2].(map[string]any)
|
||||
if toolResultMsg["role"] != "user" {
|
||||
t.Fatalf("message after assistant role = %v, want user", toolResultMsg["role"])
|
||||
}
|
||||
blocks := toolResultMsg["content"].([]map[string]any)
|
||||
if len(blocks) != 1 {
|
||||
t.Fatalf("tool_result blocks = %d, want 1", len(blocks))
|
||||
}
|
||||
if blocks[0]["tool_use_id"] != toolUse["id"] {
|
||||
t.Errorf("tool_result tool_use_id = %v, want %v (paired with tool_use)",
|
||||
blocks[0]["tool_use_id"], toolUse["id"])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseResponseBodyEdgeCases tests edge cases for parseResponseBody.
|
||||
func TestParseResponseBodyEdgeCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
|
|
|
|||
|
|
@ -863,6 +863,7 @@ func getInt64Arg(args map[string]any, key string, defaultVal int64) (int64, erro
|
|||
|
||||
type WriteFileTool struct {
|
||||
fs fileSystem
|
||||
altTools []string
|
||||
}
|
||||
|
||||
func NewWriteFileTool(
|
||||
|
|
@ -874,7 +875,32 @@ func NewWriteFileTool(
|
|||
if len(allowPaths) > 0 {
|
||||
patterns = allowPaths[0]
|
||||
}
|
||||
return &WriteFileTool{fs: buildFs(workspace, restrict, patterns)}
|
||||
// Default to both alternatives so standalone callers keep the full guidance;
|
||||
// the agent wiring narrows this to the tools actually registered.
|
||||
return &WriteFileTool{
|
||||
fs: buildFs(workspace, restrict, patterns),
|
||||
altTools: []string{"append_file", "edit_file"},
|
||||
}
|
||||
}
|
||||
|
||||
// SetAlternativeTools limits which alternatives the copy names, so it never
|
||||
// directs the model to tools that are not available.
|
||||
func (t *WriteFileTool) SetAlternativeTools(names []string) {
|
||||
present := make(map[string]bool, len(names))
|
||||
for _, name := range names {
|
||||
present[name] = true
|
||||
}
|
||||
ordered := make([]string, 0, 2)
|
||||
for _, name := range []string{"append_file", "edit_file"} {
|
||||
if present[name] {
|
||||
ordered = append(ordered, name)
|
||||
}
|
||||
}
|
||||
t.altTools = ordered
|
||||
}
|
||||
|
||||
func (t *WriteFileTool) altToolsPhrase() string {
|
||||
return strings.Join(t.altTools, " or ")
|
||||
}
|
||||
|
||||
func (t *WriteFileTool) Name() string {
|
||||
|
|
@ -882,10 +908,24 @@ func (t *WriteFileTool) Name() string {
|
|||
}
|
||||
|
||||
func (t *WriteFileTool) Description() string {
|
||||
return "Write content to a file. Content is written byte-for-byte after argument decoding. Standard JSON escaping applies: \\n for newline and \\\\n for a literal backslash-n sequence. If the file already exists, you must set overwrite=true to replace it."
|
||||
desc := "Write content to a file, replacing any existing content. Content is written byte-for-byte after argument decoding. Standard JSON escaping applies: \\n for newline and \\\\n for a literal backslash-n sequence. If the file already exists you must set overwrite=true, which replaces the ENTIRE file."
|
||||
if phrase := t.altToolsPhrase(); phrase != "" {
|
||||
desc += fmt.Sprintf(
|
||||
" To add to or change part of an existing file without losing its current contents, use %s instead.",
|
||||
phrase,
|
||||
)
|
||||
}
|
||||
return desc
|
||||
}
|
||||
|
||||
func (t *WriteFileTool) Parameters() map[string]any {
|
||||
overwriteDesc := "Set to true to replace an existing file in full. This discards the file's current contents."
|
||||
if phrase := t.altToolsPhrase(); phrase != "" {
|
||||
overwriteDesc = fmt.Sprintf(
|
||||
"Set to true to replace an existing file in full. This discards the file's current contents — to preserve them, use %s instead of write_file.",
|
||||
phrase,
|
||||
)
|
||||
}
|
||||
return map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
|
|
@ -899,7 +939,7 @@ func (t *WriteFileTool) Parameters() map[string]any {
|
|||
},
|
||||
"overwrite": map[string]any{
|
||||
"type": "boolean",
|
||||
"description": "Must be set to true to overwrite an existing file.",
|
||||
"description": overwriteDesc,
|
||||
"default": false,
|
||||
},
|
||||
},
|
||||
|
|
@ -922,8 +962,20 @@ func (t *WriteFileTool) Execute(ctx context.Context, args map[string]any) *ToolR
|
|||
|
||||
if !overwrite {
|
||||
if _, err := t.fs.Open(path); err == nil {
|
||||
if phrase := t.altToolsPhrase(); phrase != "" {
|
||||
return ErrorResult(
|
||||
fmt.Sprintf("file: %s already exists. Set overwrite=true to replace.", path),
|
||||
fmt.Sprintf(
|
||||
"file: %s already exists. To add to it or change part of it without losing the current contents, use %s. Only set overwrite=true if you intend to replace the entire file.",
|
||||
path,
|
||||
phrase,
|
||||
),
|
||||
)
|
||||
}
|
||||
return ErrorResult(
|
||||
fmt.Sprintf(
|
||||
"file: %s already exists. Set overwrite=true only if you intend to replace the entire file, which discards its current contents.",
|
||||
path,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -250,6 +250,9 @@ func TestFilesystemTool_WriteFile_OverwriteDefaultBlocked(t *testing.T) {
|
|||
assert.True(t, result.IsError, "expected error when overwriting without overwrite=true")
|
||||
assert.Contains(t, result.ForLLM, "already exists")
|
||||
assert.Contains(t, result.ForLLM, "overwrite=true")
|
||||
// The guard must steer toward non-destructive tools rather than only coaching overwrite.
|
||||
assert.Contains(t, result.ForLLM, "append_file")
|
||||
assert.Contains(t, result.ForLLM, "edit_file")
|
||||
|
||||
// Original content must be untouched
|
||||
data, err := os.ReadFile(testFile)
|
||||
|
|
@ -257,6 +260,76 @@ func TestFilesystemTool_WriteFile_OverwriteDefaultBlocked(t *testing.T) {
|
|||
assert.Equal(t, "original", string(data))
|
||||
}
|
||||
|
||||
// Copy (description, overwrite param, guard) only names available alternatives.
|
||||
func TestFilesystemTool_WriteFile_AltToolsConditionalCopy(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
testFile := filepath.Join(tmpDir, "existing.txt")
|
||||
os.WriteFile(testFile, []byte("original"), 0o644)
|
||||
|
||||
overwriteParamDesc := func(tool *WriteFileTool) string {
|
||||
props := tool.Parameters()["properties"].(map[string]any)
|
||||
return props["overwrite"].(map[string]any)["description"].(string)
|
||||
}
|
||||
|
||||
t.Run("no alternatives available", func(t *testing.T) {
|
||||
tool := NewWriteFileTool("", false)
|
||||
tool.SetAlternativeTools(nil)
|
||||
|
||||
assert.NotContains(t, tool.Description(), "append_file")
|
||||
assert.NotContains(t, tool.Description(), "edit_file")
|
||||
assert.NotContains(t, overwriteParamDesc(tool), "append_file")
|
||||
assert.NotContains(t, overwriteParamDesc(tool), "edit_file")
|
||||
|
||||
result := tool.Execute(context.Background(), map[string]any{
|
||||
"path": testFile,
|
||||
"content": "new content",
|
||||
})
|
||||
assert.True(t, result.IsError, "expected overwrite guard to still block")
|
||||
assert.Contains(t, result.ForLLM, "already exists")
|
||||
assert.Contains(t, result.ForLLM, "overwrite=true")
|
||||
assert.NotContains(t, result.ForLLM, "append_file")
|
||||
assert.NotContains(t, result.ForLLM, "edit_file")
|
||||
})
|
||||
|
||||
t.Run("only append_file available", func(t *testing.T) {
|
||||
tool := NewWriteFileTool("", false)
|
||||
tool.SetAlternativeTools([]string{"append_file"})
|
||||
|
||||
assert.Contains(t, tool.Description(), "append_file")
|
||||
assert.NotContains(t, tool.Description(), "edit_file")
|
||||
assert.Contains(t, overwriteParamDesc(tool), "append_file")
|
||||
assert.NotContains(t, overwriteParamDesc(tool), "edit_file")
|
||||
|
||||
result := tool.Execute(context.Background(), map[string]any{
|
||||
"path": testFile,
|
||||
"content": "new content",
|
||||
})
|
||||
assert.True(t, result.IsError)
|
||||
assert.Contains(t, result.ForLLM, "append_file")
|
||||
assert.NotContains(t, result.ForLLM, "edit_file")
|
||||
})
|
||||
|
||||
t.Run("both available uses canonical order", func(t *testing.T) {
|
||||
tool := NewWriteFileTool("", false)
|
||||
// Reversed input to confirm the order is normalized.
|
||||
tool.SetAlternativeTools([]string{"edit_file", "append_file"})
|
||||
|
||||
assert.Contains(t, tool.Description(), "append_file or edit_file")
|
||||
|
||||
result := tool.Execute(context.Background(), map[string]any{
|
||||
"path": testFile,
|
||||
"content": "new content",
|
||||
})
|
||||
assert.True(t, result.IsError)
|
||||
assert.Contains(t, result.ForLLM, "append_file or edit_file")
|
||||
})
|
||||
|
||||
// Blocked writes must leave the original untouched.
|
||||
data, err := os.ReadFile(testFile)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "original", string(data))
|
||||
}
|
||||
|
||||
// TestFilesystemTool_WriteFile_OverwriteExplicitAllowed verifies that setting
|
||||
// overwrite=true replaces the existing file.
|
||||
func TestFilesystemTool_WriteFile_OverwriteExplicitAllowed(t *testing.T) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue