mirror of
https://github.com/ZSeven-W/openpencil.git
synced 2026-06-01 03:14:29 +07:00
V0.4.3 (#50)
* chore(ai): update dependencies and enhance logging functionality - Bump version of `@github/copilot-sdk` and related packages to `0.1.32` and `1.0.7` for improved features and bug fixes. - Update Discord invite links across multiple README files to the new server. - Introduce a new logging utility in `server/utils/server-logger.ts` for better server process logging, including automatic log cleanup and directory management. - Enhance the `connect-agent.ts` and `install-agent.ts` files to improve OpenCode binary resolution and installation commands. - Refactor `resolve-claude-cli.ts` and `resolve-copilot-cli.ts` to include detailed logging for CLI binary resolution processes. This update improves dependency management, enhances user experience with updated links, and provides better insights into server operations through logging. * chore: bump version from 0.4.0 to 0.4.3 in package.json
This commit is contained in:
parent
18be43416a
commit
6a1891fc6e
42 changed files with 854 additions and 271 deletions
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
|
|
@ -1,5 +1,5 @@
|
|||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Discord
|
||||
url: https://discord.gg/KwXp6BJD
|
||||
url: https://discord.gg/h9Fmyy6pVh
|
||||
about: Ask questions and chat with the community
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
<a href="https://github.com/ZSeven-W/openpencil/stargazers"><img src="https://img.shields.io/github/stars/ZSeven-W/openpencil?style=flat&color=cfb537" alt="Stars" /></a>
|
||||
<a href="https://github.com/ZSeven-W/openpencil/blob/main/LICENSE"><img src="https://img.shields.io/github/license/ZSeven-W/openpencil?color=64748b" alt="License" /></a>
|
||||
<a href="https://github.com/ZSeven-W/openpencil/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/ZSeven-W/openpencil/ci.yml?branch=main&label=CI" alt="CI" /></a>
|
||||
<a href="https://discord.gg/KwXp6BJD"><img src="https://img.shields.io/discord/1476517942949580952?label=Discord&logo=discord&logoColor=white&color=5865F2" alt="Discord" /></a>
|
||||
<a href="https://discord.gg/h9Fmyy6pVh"><img src="https://img.shields.io/discord/1476517942949580952?label=Discord&logo=discord&logoColor=white&color=5865F2" alt="Discord" /></a>
|
||||
</p>
|
||||
|
||||
<br />
|
||||
|
|
@ -301,7 +301,7 @@ Beiträge sind willkommen! Siehe [CLAUDE.md](./CLAUDE.md) für Architekturdetail
|
|||
|
||||
## Community
|
||||
|
||||
<a href="https://discord.gg/KwXp6BJD">
|
||||
<a href="https://discord.gg/h9Fmyy6pVh">
|
||||
<img src="./public/logo-discord.svg" alt="Discord" width="16" />
|
||||
<strong> Unserem Discord beitreten</strong>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
<a href="https://github.com/ZSeven-W/openpencil/stargazers"><img src="https://img.shields.io/github/stars/ZSeven-W/openpencil?style=flat&color=cfb537" alt="Stars" /></a>
|
||||
<a href="https://github.com/ZSeven-W/openpencil/blob/main/LICENSE"><img src="https://img.shields.io/github/license/ZSeven-W/openpencil?color=64748b" alt="License" /></a>
|
||||
<a href="https://github.com/ZSeven-W/openpencil/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/ZSeven-W/openpencil/ci.yml?branch=main&label=CI" alt="CI" /></a>
|
||||
<a href="https://discord.gg/KwXp6BJD"><img src="https://img.shields.io/discord/1476517942949580952?label=Discord&logo=discord&logoColor=white&color=5865F2" alt="Discord" /></a>
|
||||
<a href="https://discord.gg/h9Fmyy6pVh"><img src="https://img.shields.io/discord/1476517942949580952?label=Discord&logo=discord&logoColor=white&color=5865F2" alt="Discord" /></a>
|
||||
</p>
|
||||
|
||||
<br />
|
||||
|
|
@ -312,7 +312,7 @@ bun run electron:build # Empaquetado de Electron
|
|||
|
||||
## Comunidad
|
||||
|
||||
<a href="https://discord.gg/KwXp6BJD">
|
||||
<a href="https://discord.gg/h9Fmyy6pVh">
|
||||
<img src="./public/logo-discord.svg" alt="Discord" width="16" />
|
||||
<strong> Únete a nuestro Discord</strong>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
<a href="https://github.com/ZSeven-W/openpencil/stargazers"><img src="https://img.shields.io/github/stars/ZSeven-W/openpencil?style=flat&color=cfb537" alt="Stars" /></a>
|
||||
<a href="https://github.com/ZSeven-W/openpencil/blob/main/LICENSE"><img src="https://img.shields.io/github/license/ZSeven-W/openpencil?color=64748b" alt="License" /></a>
|
||||
<a href="https://github.com/ZSeven-W/openpencil/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/ZSeven-W/openpencil/ci.yml?branch=main&label=CI" alt="CI" /></a>
|
||||
<a href="https://discord.gg/KwXp6BJD"><img src="https://img.shields.io/discord/1476517942949580952?label=Discord&logo=discord&logoColor=white&color=5865F2" alt="Discord" /></a>
|
||||
<a href="https://discord.gg/h9Fmyy6pVh"><img src="https://img.shields.io/discord/1476517942949580952?label=Discord&logo=discord&logoColor=white&color=5865F2" alt="Discord" /></a>
|
||||
</p>
|
||||
|
||||
<br />
|
||||
|
|
@ -312,7 +312,7 @@ Les contributions sont les bienvenues ! Consultez [CLAUDE.md](./CLAUDE.md) pour
|
|||
|
||||
## Communauté
|
||||
|
||||
<a href="https://discord.gg/KwXp6BJD">
|
||||
<a href="https://discord.gg/h9Fmyy6pVh">
|
||||
<img src="./public/logo-discord.svg" alt="Discord" width="16" />
|
||||
<strong> Rejoindre notre Discord</strong>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
<a href="https://github.com/ZSeven-W/openpencil/stargazers"><img src="https://img.shields.io/github/stars/ZSeven-W/openpencil?style=flat&color=cfb537" alt="Stars" /></a>
|
||||
<a href="https://github.com/ZSeven-W/openpencil/blob/main/LICENSE"><img src="https://img.shields.io/github/license/ZSeven-W/openpencil?color=64748b" alt="License" /></a>
|
||||
<a href="https://github.com/ZSeven-W/openpencil/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/ZSeven-W/openpencil/ci.yml?branch=main&label=CI" alt="CI" /></a>
|
||||
<a href="https://discord.gg/KwXp6BJD"><img src="https://img.shields.io/discord/1476517942949580952?label=Discord&logo=discord&logoColor=white&color=5865F2" alt="Discord" /></a>
|
||||
<a href="https://discord.gg/h9Fmyy6pVh"><img src="https://img.shields.io/discord/1476517942949580952?label=Discord&logo=discord&logoColor=white&color=5865F2" alt="Discord" /></a>
|
||||
</p>
|
||||
|
||||
<br />
|
||||
|
|
@ -301,7 +301,7 @@ bun run electron:build # Electron पैकेज
|
|||
|
||||
## समुदाय
|
||||
|
||||
<a href="https://discord.gg/KwXp6BJD">
|
||||
<a href="https://discord.gg/h9Fmyy6pVh">
|
||||
<img src="./public/logo-discord.svg" alt="Discord" width="16" />
|
||||
<strong> हमारे Discord में शामिल हों</strong>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
<a href="https://github.com/ZSeven-W/openpencil/stargazers"><img src="https://img.shields.io/github/stars/ZSeven-W/openpencil?style=flat&color=cfb537" alt="Stars" /></a>
|
||||
<a href="https://github.com/ZSeven-W/openpencil/blob/main/LICENSE"><img src="https://img.shields.io/github/license/ZSeven-W/openpencil?color=64748b" alt="License" /></a>
|
||||
<a href="https://github.com/ZSeven-W/openpencil/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/ZSeven-W/openpencil/ci.yml?branch=main&label=CI" alt="CI" /></a>
|
||||
<a href="https://discord.gg/KwXp6BJD"><img src="https://img.shields.io/discord/1476517942949580952?label=Discord&logo=discord&logoColor=white&color=5865F2" alt="Discord" /></a>
|
||||
<a href="https://discord.gg/h9Fmyy6pVh"><img src="https://img.shields.io/discord/1476517942949580952?label=Discord&logo=discord&logoColor=white&color=5865F2" alt="Discord" /></a>
|
||||
</p>
|
||||
|
||||
<br />
|
||||
|
|
@ -301,7 +301,7 @@ Kontribusi sangat disambut! Lihat [CLAUDE.md](./CLAUDE.md) untuk detail arsitekt
|
|||
|
||||
## Komunitas
|
||||
|
||||
<a href="https://discord.gg/KwXp6BJD">
|
||||
<a href="https://discord.gg/h9Fmyy6pVh">
|
||||
<img src="./public/logo-discord.svg" alt="Discord" width="16" />
|
||||
<strong> Bergabung dengan Discord kami</strong>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
<a href="https://github.com/ZSeven-W/openpencil/stargazers"><img src="https://img.shields.io/github/stars/ZSeven-W/openpencil?style=flat&color=cfb537" alt="Stars" /></a>
|
||||
<a href="https://github.com/ZSeven-W/openpencil/blob/main/LICENSE"><img src="https://img.shields.io/github/license/ZSeven-W/openpencil?color=64748b" alt="License" /></a>
|
||||
<a href="https://github.com/ZSeven-W/openpencil/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/ZSeven-W/openpencil/ci.yml?branch=main&label=CI" alt="CI" /></a>
|
||||
<a href="https://discord.gg/KwXp6BJD"><img src="https://img.shields.io/discord/1476517942949580952?label=Discord&logo=discord&logoColor=white&color=5865F2" alt="Discord" /></a>
|
||||
<a href="https://discord.gg/h9Fmyy6pVh"><img src="https://img.shields.io/discord/1476517942949580952?label=Discord&logo=discord&logoColor=white&color=5865F2" alt="Discord" /></a>
|
||||
</p>
|
||||
|
||||
<br />
|
||||
|
|
@ -301,7 +301,7 @@ bun run electron:build # Electron パッケージング
|
|||
|
||||
## コミュニティ
|
||||
|
||||
<a href="https://discord.gg/KwXp6BJD">
|
||||
<a href="https://discord.gg/h9Fmyy6pVh">
|
||||
<img src="./public/logo-discord.svg" alt="Discord" width="16" />
|
||||
<strong> Discord に参加する</strong>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
<a href="https://github.com/ZSeven-W/openpencil/stargazers"><img src="https://img.shields.io/github/stars/ZSeven-W/openpencil?style=flat&color=cfb537" alt="Stars" /></a>
|
||||
<a href="https://github.com/ZSeven-W/openpencil/blob/main/LICENSE"><img src="https://img.shields.io/github/license/ZSeven-W/openpencil?color=64748b" alt="License" /></a>
|
||||
<a href="https://github.com/ZSeven-W/openpencil/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/ZSeven-W/openpencil/ci.yml?branch=main&label=CI" alt="CI" /></a>
|
||||
<a href="https://discord.gg/KwXp6BJD"><img src="https://img.shields.io/discord/1476517942949580952?label=Discord&logo=discord&logoColor=white&color=5865F2" alt="Discord" /></a>
|
||||
<a href="https://discord.gg/h9Fmyy6pVh"><img src="https://img.shields.io/discord/1476517942949580952?label=Discord&logo=discord&logoColor=white&color=5865F2" alt="Discord" /></a>
|
||||
</p>
|
||||
|
||||
<br />
|
||||
|
|
@ -301,7 +301,7 @@ bun run electron:build # Electron 패키징
|
|||
|
||||
## 커뮤니티
|
||||
|
||||
<a href="https://discord.gg/KwXp6BJD">
|
||||
<a href="https://discord.gg/h9Fmyy6pVh">
|
||||
<img src="./public/logo-discord.svg" alt="Discord" width="16" />
|
||||
<strong> Discord에 참여하기</strong>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
<a href="https://github.com/ZSeven-W/openpencil/stargazers"><img src="https://img.shields.io/github/stars/ZSeven-W/openpencil?style=flat&color=cfb537" alt="Stars" /></a>
|
||||
<a href="https://github.com/ZSeven-W/openpencil/blob/main/LICENSE"><img src="https://img.shields.io/github/license/ZSeven-W/openpencil?color=64748b" alt="License" /></a>
|
||||
<a href="https://github.com/ZSeven-W/openpencil/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/ZSeven-W/openpencil/ci.yml?branch=main&label=CI" alt="CI" /></a>
|
||||
<a href="https://discord.gg/KwXp6BJD"><img src="https://img.shields.io/discord/1476517942949580952?label=Discord&logo=discord&logoColor=white&color=5865F2" alt="Discord" /></a>
|
||||
<a href="https://discord.gg/h9Fmyy6pVh"><img src="https://img.shields.io/discord/1476517942949580952?label=Discord&logo=discord&logoColor=white&color=5865F2" alt="Discord" /></a>
|
||||
</p>
|
||||
|
||||
<br />
|
||||
|
|
@ -301,7 +301,7 @@ Contributions are welcome! See [CLAUDE.md](./CLAUDE.md) for architecture details
|
|||
|
||||
## Community
|
||||
|
||||
<a href="https://discord.gg/KwXp6BJD">
|
||||
<a href="https://discord.gg/h9Fmyy6pVh">
|
||||
<img src="./public/logo-discord.svg" alt="Discord" width="16" />
|
||||
<strong> Join our Discord</strong>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
<a href="https://github.com/ZSeven-W/openpencil/stargazers"><img src="https://img.shields.io/github/stars/ZSeven-W/openpencil?style=flat&color=cfb537" alt="Stars" /></a>
|
||||
<a href="https://github.com/ZSeven-W/openpencil/blob/main/LICENSE"><img src="https://img.shields.io/github/license/ZSeven-W/openpencil?color=64748b" alt="License" /></a>
|
||||
<a href="https://github.com/ZSeven-W/openpencil/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/ZSeven-W/openpencil/ci.yml?branch=main&label=CI" alt="CI" /></a>
|
||||
<a href="https://discord.gg/KwXp6BJD"><img src="https://img.shields.io/discord/1476517942949580952?label=Discord&logo=discord&logoColor=white&color=5865F2" alt="Discord" /></a>
|
||||
<a href="https://discord.gg/h9Fmyy6pVh"><img src="https://img.shields.io/discord/1476517942949580952?label=Discord&logo=discord&logoColor=white&color=5865F2" alt="Discord" /></a>
|
||||
</p>
|
||||
|
||||
<br />
|
||||
|
|
@ -301,7 +301,7 @@ Contribuições são bem-vindas! Consulte o [CLAUDE.md](./CLAUDE.md) para detalh
|
|||
|
||||
## Comunidade
|
||||
|
||||
<a href="https://discord.gg/KwXp6BJD">
|
||||
<a href="https://discord.gg/h9Fmyy6pVh">
|
||||
<img src="./public/logo-discord.svg" alt="Discord" width="16" />
|
||||
<strong> Entre no nosso Discord</strong>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
<a href="https://github.com/ZSeven-W/openpencil/stargazers"><img src="https://img.shields.io/github/stars/ZSeven-W/openpencil?style=flat&color=cfb537" alt="Stars" /></a>
|
||||
<a href="https://github.com/ZSeven-W/openpencil/blob/main/LICENSE"><img src="https://img.shields.io/github/license/ZSeven-W/openpencil?color=64748b" alt="License" /></a>
|
||||
<a href="https://github.com/ZSeven-W/openpencil/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/ZSeven-W/openpencil/ci.yml?branch=main&label=CI" alt="CI" /></a>
|
||||
<a href="https://discord.gg/KwXp6BJD"><img src="https://img.shields.io/discord/1476517942949580952?label=Discord&logo=discord&logoColor=white&color=5865F2" alt="Discord" /></a>
|
||||
<a href="https://discord.gg/h9Fmyy6pVh"><img src="https://img.shields.io/discord/1476517942949580952?label=Discord&logo=discord&logoColor=white&color=5865F2" alt="Discord" /></a>
|
||||
</p>
|
||||
|
||||
<br />
|
||||
|
|
@ -301,7 +301,7 @@ bun run electron:build # Упаковка Electron
|
|||
|
||||
## Сообщество
|
||||
|
||||
<a href="https://discord.gg/KwXp6BJD">
|
||||
<a href="https://discord.gg/h9Fmyy6pVh">
|
||||
<img src="./public/logo-discord.svg" alt="Discord" width="16" />
|
||||
<strong> Присоединяйтесь к нашему Discord</strong>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
<a href="https://github.com/ZSeven-W/openpencil/stargazers"><img src="https://img.shields.io/github/stars/ZSeven-W/openpencil?style=flat&color=cfb537" alt="Stars" /></a>
|
||||
<a href="https://github.com/ZSeven-W/openpencil/blob/main/LICENSE"><img src="https://img.shields.io/github/license/ZSeven-W/openpencil?color=64748b" alt="License" /></a>
|
||||
<a href="https://github.com/ZSeven-W/openpencil/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/ZSeven-W/openpencil/ci.yml?branch=main&label=CI" alt="CI" /></a>
|
||||
<a href="https://discord.gg/KwXp6BJD"><img src="https://img.shields.io/discord/1476517942949580952?label=Discord&logo=discord&logoColor=white&color=5865F2" alt="Discord" /></a>
|
||||
<a href="https://discord.gg/h9Fmyy6pVh"><img src="https://img.shields.io/discord/1476517942949580952?label=Discord&logo=discord&logoColor=white&color=5865F2" alt="Discord" /></a>
|
||||
</p>
|
||||
|
||||
<br />
|
||||
|
|
@ -301,7 +301,7 @@ bun run electron:build # Electron package
|
|||
|
||||
## ชุมชน
|
||||
|
||||
<a href="https://discord.gg/KwXp6BJD">
|
||||
<a href="https://discord.gg/h9Fmyy6pVh">
|
||||
<img src="./public/logo-discord.svg" alt="Discord" width="16" />
|
||||
<strong> เข้าร่วม Discord ของเรา</strong>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
<a href="https://github.com/ZSeven-W/openpencil/stargazers"><img src="https://img.shields.io/github/stars/ZSeven-W/openpencil?style=flat&color=cfb537" alt="Stars" /></a>
|
||||
<a href="https://github.com/ZSeven-W/openpencil/blob/main/LICENSE"><img src="https://img.shields.io/github/license/ZSeven-W/openpencil?color=64748b" alt="License" /></a>
|
||||
<a href="https://github.com/ZSeven-W/openpencil/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/ZSeven-W/openpencil/ci.yml?branch=main&label=CI" alt="CI" /></a>
|
||||
<a href="https://discord.gg/KwXp6BJD"><img src="https://img.shields.io/discord/1476517942949580952?label=Discord&logo=discord&logoColor=white&color=5865F2" alt="Discord" /></a>
|
||||
<a href="https://discord.gg/h9Fmyy6pVh"><img src="https://img.shields.io/discord/1476517942949580952?label=Discord&logo=discord&logoColor=white&color=5865F2" alt="Discord" /></a>
|
||||
</p>
|
||||
|
||||
<br />
|
||||
|
|
@ -301,7 +301,7 @@ Katkılarınızı bekliyoruz! Mimari ayrıntılar ve kod stili için [CLAUDE.md]
|
|||
|
||||
## Topluluk
|
||||
|
||||
<a href="https://discord.gg/KwXp6BJD">
|
||||
<a href="https://discord.gg/h9Fmyy6pVh">
|
||||
<img src="./public/logo-discord.svg" alt="Discord" width="16" />
|
||||
<strong> Discord'umuza katılın</strong>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
<a href="https://github.com/ZSeven-W/openpencil/stargazers"><img src="https://img.shields.io/github/stars/ZSeven-W/openpencil?style=flat&color=cfb537" alt="Stars" /></a>
|
||||
<a href="https://github.com/ZSeven-W/openpencil/blob/main/LICENSE"><img src="https://img.shields.io/github/license/ZSeven-W/openpencil?color=64748b" alt="License" /></a>
|
||||
<a href="https://github.com/ZSeven-W/openpencil/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/ZSeven-W/openpencil/ci.yml?branch=main&label=CI" alt="CI" /></a>
|
||||
<a href="https://discord.gg/KwXp6BJD"><img src="https://img.shields.io/discord/1476517942949580952?label=Discord&logo=discord&logoColor=white&color=5865F2" alt="Discord" /></a>
|
||||
<a href="https://discord.gg/h9Fmyy6pVh"><img src="https://img.shields.io/discord/1476517942949580952?label=Discord&logo=discord&logoColor=white&color=5865F2" alt="Discord" /></a>
|
||||
</p>
|
||||
|
||||
<br />
|
||||
|
|
@ -301,7 +301,7 @@ Chào mừng đóng góp! Xem [CLAUDE.md](./CLAUDE.md) để biết chi tiết v
|
|||
|
||||
## Cộng đồng
|
||||
|
||||
<a href="https://discord.gg/KwXp6BJD">
|
||||
<a href="https://discord.gg/h9Fmyy6pVh">
|
||||
<img src="./public/logo-discord.svg" alt="Discord" width="16" />
|
||||
<strong> Tham gia Discord của chúng tôi</strong>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
<a href="https://github.com/ZSeven-W/openpencil/stargazers"><img src="https://img.shields.io/github/stars/ZSeven-W/openpencil?style=flat&color=cfb537" alt="Stars" /></a>
|
||||
<a href="https://github.com/ZSeven-W/openpencil/blob/main/LICENSE"><img src="https://img.shields.io/github/license/ZSeven-W/openpencil?color=64748b" alt="License" /></a>
|
||||
<a href="https://github.com/ZSeven-W/openpencil/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/ZSeven-W/openpencil/ci.yml?branch=main&label=CI" alt="CI" /></a>
|
||||
<a href="https://discord.gg/KwXp6BJD"><img src="https://img.shields.io/discord/1476517942949580952?label=Discord&logo=discord&logoColor=white&color=5865F2" alt="Discord" /></a>
|
||||
<a href="https://discord.gg/h9Fmyy6pVh"><img src="https://img.shields.io/discord/1476517942949580952?label=Discord&logo=discord&logoColor=white&color=5865F2" alt="Discord" /></a>
|
||||
</p>
|
||||
|
||||
<br />
|
||||
|
|
@ -301,7 +301,7 @@ bun run electron:build # Electron 封裝
|
|||
|
||||
## 社群
|
||||
|
||||
<a href="https://discord.gg/KwXp6BJD">
|
||||
<a href="https://discord.gg/h9Fmyy6pVh">
|
||||
<img src="./public/logo-discord.svg" alt="Discord" width="16" />
|
||||
<strong> 加入我們的 Discord</strong>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
<a href="https://github.com/ZSeven-W/openpencil/stargazers"><img src="https://img.shields.io/github/stars/ZSeven-W/openpencil?style=flat&color=cfb537" alt="Stars" /></a>
|
||||
<a href="https://github.com/ZSeven-W/openpencil/blob/main/LICENSE"><img src="https://img.shields.io/github/license/ZSeven-W/openpencil?color=64748b" alt="License" /></a>
|
||||
<a href="https://github.com/ZSeven-W/openpencil/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/ZSeven-W/openpencil/ci.yml?branch=main&label=CI" alt="CI" /></a>
|
||||
<a href="https://discord.gg/KwXp6BJD"><img src="https://img.shields.io/discord/1476517942949580952?label=Discord&logo=discord&logoColor=white&color=5865F2" alt="Discord" /></a>
|
||||
<a href="https://discord.gg/h9Fmyy6pVh"><img src="https://img.shields.io/discord/1476517942949580952?label=Discord&logo=discord&logoColor=white&color=5865F2" alt="Discord" /></a>
|
||||
</p>
|
||||
|
||||
<br />
|
||||
|
|
@ -302,7 +302,7 @@ bun run electron:build # Electron 打包
|
|||
|
||||
## 社区
|
||||
|
||||
<a href="https://discord.gg/KwXp6BJD">
|
||||
<a href="https://discord.gg/h9Fmyy6pVh">
|
||||
<img src="./public/logo-discord.svg" alt="Discord" width="16" />
|
||||
<strong> 加入我们的 Discord</strong>
|
||||
</a>
|
||||
|
|
|
|||
18
bun.lock
18
bun.lock
|
|
@ -19,7 +19,7 @@
|
|||
"@fontsource/raleway": "^5.2.8",
|
||||
"@fontsource/roboto": "^5.2.10",
|
||||
"@fontsource/source-sans-3": "^5.2.9",
|
||||
"@github/copilot-sdk": "^0.1.30",
|
||||
"@github/copilot-sdk": "^0.1.32",
|
||||
"@iconify-json/feather": "^1.2.1",
|
||||
"@iconify-json/lucide": "^1.2.93",
|
||||
"@iconify-json/simple-icons": "^1.2.71",
|
||||
|
|
@ -263,21 +263,21 @@
|
|||
|
||||
"@fontsource/source-sans-3": ["@fontsource/source-sans-3@5.2.9", "https://registry.npmmirror.com/@fontsource/source-sans-3/-/source-sans-3-5.2.9.tgz", {}, "sha512-u3ymIq4rfmCCyB9MEw/sFR5lPVJ1yTNXmIMbUz+9kVCFIHvNtfzXOEBuvkg3Tk0zhmioPeJ28ZK5smZ7TurezQ=="],
|
||||
|
||||
"@github/copilot": ["@github/copilot@0.0.420", "https://registry.npmmirror.com/@github/copilot/-/copilot-0.0.420.tgz", { "optionalDependencies": { "@github/copilot-darwin-arm64": "0.0.420", "@github/copilot-darwin-x64": "0.0.420", "@github/copilot-linux-arm64": "0.0.420", "@github/copilot-linux-x64": "0.0.420", "@github/copilot-win32-arm64": "0.0.420", "@github/copilot-win32-x64": "0.0.420" }, "bin": { "copilot": "npm-loader.js" } }, "sha512-UpPuSjxUxQ+j02WjZEFffWf0scLb23LvuGHzMFtaSsweR+P/BdbtDUI5ZDIA6T0tVyyt6+X1/vgfsJiRqd6jig=="],
|
||||
"@github/copilot": ["@github/copilot@1.0.7", "https://registry.npmmirror.com/@github/copilot/-/copilot-1.0.7.tgz", { "optionalDependencies": { "@github/copilot-darwin-arm64": "1.0.7", "@github/copilot-darwin-x64": "1.0.7", "@github/copilot-linux-arm64": "1.0.7", "@github/copilot-linux-x64": "1.0.7", "@github/copilot-win32-arm64": "1.0.7", "@github/copilot-win32-x64": "1.0.7" }, "bin": { "copilot": "npm-loader.js" } }, "sha512-KHBaJ1kbc19pqUMnB9LubPtwWVOaDCzWbzwsJss+DvHyCpr8wP8jR3GEZUnhq3rsuXI96ZKEeEozXM0NqxCAiw=="],
|
||||
|
||||
"@github/copilot-darwin-arm64": ["@github/copilot-darwin-arm64@0.0.420", "https://registry.npmmirror.com/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-0.0.420.tgz", { "os": "darwin", "cpu": "arm64", "bin": { "copilot-darwin-arm64": "copilot" } }, "sha512-sj8Oxcf3oKDbeUotm2gtq5YU1lwCt3QIzbMZioFD/PMLOeqSX/wrecI+c0DDYXKofFhALb0+DxxnWgbEs0mnkQ=="],
|
||||
"@github/copilot-darwin-arm64": ["@github/copilot-darwin-arm64@1.0.7", "https://registry.npmmirror.com/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.7.tgz", { "os": "darwin", "cpu": "arm64", "bin": { "copilot-darwin-arm64": "copilot" } }, "sha512-yQITowpkQYamww59CwcG5JTWV9ahj7nMH6oqObMJaeqXnG7j7dqE/YhLkujQZ3XR8VXAoIa1rZ3TahdMu94gOA=="],
|
||||
|
||||
"@github/copilot-darwin-x64": ["@github/copilot-darwin-x64@0.0.420", "https://registry.npmmirror.com/@github/copilot-darwin-x64/-/copilot-darwin-x64-0.0.420.tgz", { "os": "darwin", "cpu": "x64", "bin": { "copilot-darwin-x64": "copilot" } }, "sha512-2acA93IqXz1uuz3TVUm0Y7BVrBr0MySh1kQa8LqMILhTsG0YHRMm8ybzTp2HA7Mi1tl5CjqMSk163kkS7OzfUA=="],
|
||||
"@github/copilot-darwin-x64": ["@github/copilot-darwin-x64@1.0.7", "https://registry.npmmirror.com/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.7.tgz", { "os": "darwin", "cpu": "x64", "bin": { "copilot-darwin-x64": "copilot" } }, "sha512-23vP5bHaFA030nB3tr+dUUdRm2SqmQbs2fZUQ4F7JeYy59jp9hi8lBdaZp/TeQnjEirAUU9H2HZxsGRIIUWp7g=="],
|
||||
|
||||
"@github/copilot-linux-arm64": ["@github/copilot-linux-arm64@0.0.420", "https://registry.npmmirror.com/@github/copilot-linux-arm64/-/copilot-linux-arm64-0.0.420.tgz", { "os": "linux", "cpu": "arm64", "bin": { "copilot-linux-arm64": "copilot" } }, "sha512-h/IvEryTOYm1HzR2GNq8s2aDtN4lvT4MxldfZuS42CtWJDOfVG2jLLsoHWU1T3QV8j1++PmDgE//HX0JLpLMww=="],
|
||||
"@github/copilot-linux-arm64": ["@github/copilot-linux-arm64@1.0.7", "https://registry.npmmirror.com/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.7.tgz", { "os": "linux", "cpu": "arm64", "bin": { "copilot-linux-arm64": "copilot" } }, "sha512-g0mB98oyXKcpd4sMNBc5n1h3UhLy9AGRlT//VL8BXPSzvlTH/dJP3fdx74pbLSgvz105to/YUMmEAFfv25VNaw=="],
|
||||
|
||||
"@github/copilot-linux-x64": ["@github/copilot-linux-x64@0.0.420", "https://registry.npmmirror.com/@github/copilot-linux-x64/-/copilot-linux-x64-0.0.420.tgz", { "os": "linux", "cpu": "x64", "bin": { "copilot-linux-x64": "copilot" } }, "sha512-iL2NpZvXIDZ+3lw7sO2fo5T0nKmP5dZbU2gdYcv+SFBm/ONhCxIY5VRX4yN/9VkFaa9ePv5JzCnsl3vZINiDxg=="],
|
||||
"@github/copilot-linux-x64": ["@github/copilot-linux-x64@1.0.7", "https://registry.npmmirror.com/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.7.tgz", { "os": "linux", "cpu": "x64", "bin": { "copilot-linux-x64": "copilot" } }, "sha512-TRxzvTo9I4ehYJLFHTCJSJYQ4QnO/V9zebqwszxHpJRxuBd7FV4cxLmfOBqZcUpEpZgBH+VJ4OG98BPW7YEtJQ=="],
|
||||
|
||||
"@github/copilot-sdk": ["@github/copilot-sdk@0.1.30", "https://registry.npmmirror.com/@github/copilot-sdk/-/copilot-sdk-0.1.30.tgz", { "dependencies": { "@github/copilot": "^0.0.420", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" } }, "sha512-Stg+h8xsPRR0TNGBQfd9laxhJfWZ6DsdpbowcKIZoyKxZvMAbjnY0zyDeOpewJbxWBTJVhBZb5okOq6iaPNMZw=="],
|
||||
"@github/copilot-sdk": ["@github/copilot-sdk@0.1.32", "https://registry.npmmirror.com/@github/copilot-sdk/-/copilot-sdk-0.1.32.tgz", { "dependencies": { "@github/copilot": "^1.0.2", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" } }, "sha512-mPWM0fw1Gqc/SW8nl45K8abrFH+92fO7y6tRtRl5imjS5hGapLf/dkX5WDrgPtlsflD0c41lFXVUri5NVJwtoA=="],
|
||||
|
||||
"@github/copilot-win32-arm64": ["@github/copilot-win32-arm64@0.0.420", "https://registry.npmmirror.com/@github/copilot-win32-arm64/-/copilot-win32-arm64-0.0.420.tgz", { "os": "win32", "cpu": "arm64", "bin": { "copilot-win32-arm64": "copilot.exe" } }, "sha512-Njlc2j9vYSBAL+lC6FIEhQ3C+VxO3xavwKnw0ecVRiNLcGLyPrTdzPfPQOmEjC63gpVCqLabikoDGv8fuLPA2w=="],
|
||||
"@github/copilot-win32-arm64": ["@github/copilot-win32-arm64@1.0.7", "https://registry.npmmirror.com/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.7.tgz", { "os": "win32", "cpu": "arm64", "bin": { "copilot-win32-arm64": "copilot.exe" } }, "sha512-4yFgW1K0MlKBrK5BwMIj4nMu5KSFfytNXrs8iOpVgp7erEvKVyN7VXb6SWkoU3M9TfeNlqP6Uje2rxDvgR1u5w=="],
|
||||
|
||||
"@github/copilot-win32-x64": ["@github/copilot-win32-x64@0.0.420", "https://registry.npmmirror.com/@github/copilot-win32-x64/-/copilot-win32-x64-0.0.420.tgz", { "os": "win32", "cpu": "x64", "bin": { "copilot-win32-x64": "copilot.exe" } }, "sha512-rZlH35oNehAP2DvQbu4vQFVNeCh/1p3rUjafBYaEY0Nkhx7RmdrYBileL5U3PtRPPRsBPaq3Qp+pVIrGoCDLzQ=="],
|
||||
"@github/copilot-win32-x64": ["@github/copilot-win32-x64@1.0.7", "https://registry.npmmirror.com/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.7.tgz", { "os": "win32", "cpu": "x64", "bin": { "copilot-win32-x64": "copilot.exe" } }, "sha512-RDZlvPf/q6B54wLXJRmI39fc9+pwfcAjSwUqw0FeQruCTQgoUl8eo9NqeVWDFlr3RdzgVSMUiJHc3aiifVG6lA=="],
|
||||
|
||||
"@hono/node-server": ["@hono/node-server@1.19.9", "https://registry.npmmirror.com/@hono/node-server/-/node-server-1.19.9.tgz", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="],
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "openpencil",
|
||||
"version": "0.4.0",
|
||||
"version": "0.4.3",
|
||||
"description": "The world's first open-source AI-native vector design tool and the first to feature concurrent Agent Teams. Design-as-Code. Turn prompts into UI directly on the live canvas. A modern alternative to Pencil.",
|
||||
"author": {
|
||||
"name": "ZSeven-W",
|
||||
|
|
@ -42,7 +42,7 @@
|
|||
"@fontsource/raleway": "^5.2.8",
|
||||
"@fontsource/roboto": "^5.2.10",
|
||||
"@fontsource/source-sans-3": "^5.2.9",
|
||||
"@github/copilot-sdk": "^0.1.30",
|
||||
"@github/copilot-sdk": "^0.1.32",
|
||||
"@iconify-json/feather": "^1.2.1",
|
||||
"@iconify-json/lucide": "^1.2.93",
|
||||
"@iconify-json/simple-icons": "^1.2.71",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { defineEventHandler, readBody, setResponseHeaders } from 'h3'
|
||||
import { existsSync } from 'node:fs'
|
||||
import type { GroupedModel } from '../../../src/types/agent-settings'
|
||||
import { resolveClaudeCli } from '../../utils/resolve-claude-cli'
|
||||
import { serverLog } from '../../utils/server-logger'
|
||||
import {
|
||||
buildClaudeAgentEnv,
|
||||
getClaudeAgentDebugFilePath,
|
||||
|
|
@ -15,6 +17,10 @@ interface ConnectResult {
|
|||
models: GroupedModel[]
|
||||
error?: string
|
||||
notInstalled?: boolean
|
||||
/** Human-readable connection status, e.g. "Connected via API key" */
|
||||
connectionInfo?: string
|
||||
/** Config file path for the hint (client renders localized text) */
|
||||
hintPath?: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -65,7 +71,9 @@ const FALLBACK_CLAUDE_MODELS: GroupedModel[] = [
|
|||
|
||||
/** Connect to Claude Code via Agent SDK and fetch real supported models */
|
||||
async function connectClaudeCode(): Promise<ConnectResult> {
|
||||
serverLog.info('[connect-agent] connecting to Claude Code...')
|
||||
const claudePath = resolveClaudeCli()
|
||||
serverLog.info(`[connect-agent] resolved claude path: ${claudePath ?? 'NOT FOUND'}`)
|
||||
if (!claudePath) {
|
||||
return { connected: false, models: [], notInstalled: true, error: 'Claude Code CLI not found' }
|
||||
}
|
||||
|
|
@ -75,6 +83,8 @@ async function connectClaudeCode(): Promise<ConnectResult> {
|
|||
|
||||
const env = buildClaudeAgentEnv()
|
||||
const debugFile = getClaudeAgentDebugFilePath()
|
||||
serverLog.info(`[connect-agent] claude env keys: ${Object.keys(env).join(', ')}`)
|
||||
serverLog.info(`[connect-agent] claude debugFile: ${debugFile ?? 'none'}`)
|
||||
|
||||
const q = query({
|
||||
prompt: '',
|
||||
|
|
@ -89,7 +99,17 @@ async function connectClaudeCode(): Promise<ConnectResult> {
|
|||
},
|
||||
})
|
||||
|
||||
serverLog.info('[connect-agent] querying supportedModels...')
|
||||
const raw = await q.supportedModels()
|
||||
|
||||
// Fetch account info (email, org, subscription type)
|
||||
let account: { email?: string; organization?: string; subscriptionType?: string; apiKeySource?: string } | null = null
|
||||
try {
|
||||
account = await q.accountInfo()
|
||||
serverLog.info(`[connect-agent] claude account: email=${account?.email ?? 'n/a'}, type=${account?.subscriptionType ?? 'n/a'}, source=${account?.apiKeySource ?? 'n/a'}`)
|
||||
} catch {
|
||||
serverLog.info('[connect-agent] accountInfo() not available')
|
||||
}
|
||||
q.close()
|
||||
|
||||
const models: GroupedModel[] = raw.map((m) => ({
|
||||
|
|
@ -99,19 +119,107 @@ async function connectClaudeCode(): Promise<ConnectResult> {
|
|||
provider: 'anthropic' as const,
|
||||
}))
|
||||
|
||||
return { connected: true, models }
|
||||
serverLog.info(`[connect-agent] claude connected, ${models.length} models found`)
|
||||
const claudeInfo = buildClaudeConnectionInfo(env, account)
|
||||
return { connected: true, models, ...claudeInfo }
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? error.message : 'Failed to connect'
|
||||
serverLog.error(`[connect-agent] claude connection error: ${msg}`)
|
||||
// Third-party API proxies often don't support the supportedModels() call,
|
||||
// causing "query closed before response". Fall back to a default model list
|
||||
// so users can still connect and choose a model.
|
||||
if (/closed before|closed early|query closed/i.test(msg)) {
|
||||
return { connected: true, models: FALLBACK_CLAUDE_MODELS }
|
||||
serverLog.info('[connect-agent] using fallback model list (proxy detected)')
|
||||
const fallbackEnv = buildClaudeAgentEnv()
|
||||
const claudeInfo = buildClaudeConnectionInfo(fallbackEnv, null)
|
||||
return { connected: true, models: FALLBACK_CLAUDE_MODELS, ...claudeInfo }
|
||||
}
|
||||
return { connected: false, models: [], error: friendlyClaudeError(msg) }
|
||||
}
|
||||
}
|
||||
|
||||
/** Resolve config file path (cross-platform) */
|
||||
function configPath(unixPath: string, winPath: string): string {
|
||||
return process.platform === 'win32' ? winPath : unixPath
|
||||
}
|
||||
|
||||
/** Build Claude connection info from env + SDK account info */
|
||||
function buildClaudeConnectionInfo(
|
||||
env: Record<string, string | undefined>,
|
||||
account: { email?: string; organization?: string; subscriptionType?: string; apiKeySource?: string } | null,
|
||||
): { connectionInfo: string; hintPath?: string } {
|
||||
const hp = configPath('~/.claude/settings.json', '%USERPROFILE%\\.claude\\settings.json')
|
||||
const apiKey = env.ANTHROPIC_API_KEY
|
||||
const baseUrl = env.ANTHROPIC_BASE_URL
|
||||
|
||||
if (account?.email) {
|
||||
const sub = account.subscriptionType ?? 'subscription'
|
||||
return { connectionInfo: `Connected via ${sub} (${account.email})`, hintPath: hp }
|
||||
}
|
||||
if (apiKey && baseUrl) {
|
||||
return { connectionInfo: 'Connected via API key (custom endpoint)', hintPath: hp }
|
||||
}
|
||||
if (apiKey) {
|
||||
const masked = apiKey.length > 12 ? `${apiKey.slice(0, 8)}...` : '***'
|
||||
return { connectionInfo: `Connected via API key (${masked})`, hintPath: hp }
|
||||
}
|
||||
return { connectionInfo: 'Connected via subscription', hintPath: hp }
|
||||
}
|
||||
|
||||
/** Decode a JWT payload (no verification — just base64url decode the middle part) */
|
||||
function decodeJwtPayload(token: string): Record<string, unknown> | null {
|
||||
try {
|
||||
const parts = token.split('.')
|
||||
if (parts.length !== 3) return null
|
||||
// base64url → base64 → Buffer → JSON
|
||||
const b64 = parts[1].replace(/-/g, '+').replace(/_/g, '/')
|
||||
const padded = b64 + '='.repeat((4 - (b64.length % 4)) % 4)
|
||||
return JSON.parse(Buffer.from(padded, 'base64').toString('utf-8'))
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/** Build Codex CLI connection info by reading ~/.codex/auth.json + JWT tokens */
|
||||
async function buildCodexConnectionInfo(): Promise<{ connectionInfo: string; hintPath?: string }> {
|
||||
const { readFile } = await import('node:fs/promises')
|
||||
const { homedir } = await import('node:os')
|
||||
const { join } = await import('node:path')
|
||||
const hp = configPath('~/.codex/config.json', '%USERPROFILE%\\.codex\\config.json')
|
||||
|
||||
if (process.env.OPENAI_API_KEY) {
|
||||
const key = process.env.OPENAI_API_KEY
|
||||
const masked = key.length > 12 ? `${key.slice(0, 8)}...` : '***'
|
||||
return { connectionInfo: `Connected via API key (${masked})`, hintPath: hp }
|
||||
}
|
||||
|
||||
try {
|
||||
const authPath = join(homedir(), '.codex', 'auth.json')
|
||||
const raw = await readFile(authPath, 'utf-8')
|
||||
const auth = JSON.parse(raw) as { auth_mode?: string; tokens?: { id_token?: string } }
|
||||
|
||||
const idToken = auth.tokens?.id_token
|
||||
if (idToken) {
|
||||
const payload = decodeJwtPayload(idToken)
|
||||
if (payload) {
|
||||
const email = payload.email as string | undefined
|
||||
const authClaims = payload['https://api.openai.com/auth'] as Record<string, unknown> | undefined
|
||||
const plan = authClaims?.chatgpt_plan_type as string | undefined
|
||||
serverLog.info(`[connect-agent] codex JWT: email=${email ?? 'n/a'}, plan=${plan ?? 'n/a'}`)
|
||||
if (email) {
|
||||
const label = plan ?? auth.auth_mode ?? 'subscription'
|
||||
return { connectionInfo: `Connected via ${label} (${email})`, hintPath: hp }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (auth.auth_mode) {
|
||||
return { connectionInfo: `Connected via ${auth.auth_mode}`, hintPath: hp }
|
||||
}
|
||||
} catch { /* auth.json not found */ }
|
||||
|
||||
return { connectionInfo: 'Connected via Codex CLI', hintPath: hp }
|
||||
}
|
||||
|
||||
/** Map raw Agent SDK errors to user-friendly messages */
|
||||
function friendlyClaudeError(raw: string): string {
|
||||
if (/process exited with code 1|invalid model|unknown model|model.*not/i.test(raw)) {
|
||||
|
|
@ -131,33 +239,85 @@ function friendlyClaudeError(raw: string): string {
|
|||
|
||||
/** Connect to Codex CLI and fetch its supported models from the local cache */
|
||||
async function connectCodexCli(): Promise<ConnectResult> {
|
||||
serverLog.info('[connect-agent] connecting to Codex CLI...')
|
||||
try {
|
||||
const { execSync } = await import('node:child_process')
|
||||
const { readFile } = await import('node:fs/promises')
|
||||
const { homedir } = await import('node:os')
|
||||
const { join } = await import('node:path')
|
||||
const isWin = process.platform === 'win32'
|
||||
|
||||
// Check if codex binary exists
|
||||
const whichCmd = process.platform === 'win32' ? 'where codex 2>nul' : 'which codex 2>/dev/null || echo ""'
|
||||
const which = execSync(whichCmd, {
|
||||
encoding: 'utf-8',
|
||||
timeout: 5000,
|
||||
}).trim().split(/\r?\n/)[0]?.trim() ?? ''
|
||||
// Check if codex binary exists — PATH, npm prefix, then common locations
|
||||
let which = ''
|
||||
|
||||
if (!which) {
|
||||
return { connected: false, models: [], notInstalled: true, error: 'Codex CLI not found' }
|
||||
// 1. PATH lookup
|
||||
try {
|
||||
const whichCmd = isWin ? 'where codex 2>nul' : 'which codex 2>/dev/null || echo ""'
|
||||
serverLog.info(`[connect-agent] codex PATH lookup: ${whichCmd}`)
|
||||
const result = execSync(whichCmd, {
|
||||
encoding: 'utf-8',
|
||||
timeout: 5000,
|
||||
}).trim().split(/\r?\n/)[0]?.trim() ?? ''
|
||||
if (result && existsSync(result)) which = result
|
||||
serverLog.info(`[connect-agent] codex PATH result: "${result}" (exists=${result ? existsSync(result) : false})`)
|
||||
} catch (err) {
|
||||
serverLog.info(`[connect-agent] codex PATH lookup failed: ${err instanceof Error ? err.message : err}`)
|
||||
}
|
||||
|
||||
// Verify codex is responsive
|
||||
// 2. npm prefix -g (Windows: npm global creates .cmd wrappers)
|
||||
if (!which && isWin) {
|
||||
try {
|
||||
serverLog.info('[connect-agent] codex: trying npm.cmd prefix -g')
|
||||
const prefix = execSync('npm.cmd prefix -g', {
|
||||
encoding: 'utf-8',
|
||||
timeout: 5000,
|
||||
}).trim()
|
||||
serverLog.info(`[connect-agent] codex npm global prefix: "${prefix}"`)
|
||||
if (prefix) {
|
||||
const bin = join(prefix, 'codex.cmd')
|
||||
serverLog.info(`[connect-agent] codex npm global bin: "${bin}" (exists=${existsSync(bin)})`)
|
||||
if (existsSync(bin)) which = bin
|
||||
}
|
||||
} catch (err) {
|
||||
serverLog.info(`[connect-agent] codex npm prefix -g failed: ${err instanceof Error ? err.message : err}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Common install locations
|
||||
if (!which && isWin) {
|
||||
const candidates = [
|
||||
join(process.env.APPDATA || '', 'npm', 'codex.cmd'),
|
||||
join(process.env.NVM_SYMLINK || '', 'codex.cmd'),
|
||||
join(process.env.FNM_MULTISHELL_PATH || '', 'codex.cmd'),
|
||||
]
|
||||
for (const c of candidates) {
|
||||
const exists = c ? existsSync(c) : false
|
||||
serverLog.info(`[connect-agent] codex candidate: "${c}" (exists=${exists})`)
|
||||
if (c && exists) { which = c; break }
|
||||
}
|
||||
}
|
||||
|
||||
if (!which) {
|
||||
serverLog.warn('[connect-agent] codex not found')
|
||||
return { connected: false, models: [], notInstalled: true, error: 'Codex CLI not found' }
|
||||
}
|
||||
serverLog.info(`[connect-agent] codex resolved: "${which}"`)
|
||||
|
||||
|
||||
// Verify codex is responsive — on Windows, use the resolved path or .cmd wrapper
|
||||
const versionCmd = isWin ? `"${which}" --version 2>&1` : 'codex --version 2>&1'
|
||||
try {
|
||||
execSync('codex --version 2>&1', { encoding: 'utf-8', timeout: 5000 })
|
||||
} catch {
|
||||
const ver = execSync(versionCmd, { encoding: 'utf-8', timeout: 5000 }).trim()
|
||||
serverLog.info(`[connect-agent] codex version: ${ver}`)
|
||||
} catch (err) {
|
||||
serverLog.error(`[connect-agent] codex --version failed: ${err instanceof Error ? err.message : err}`)
|
||||
return { connected: false, models: [], error: 'Codex CLI not responding' }
|
||||
}
|
||||
|
||||
// Read models from Codex CLI's local models cache
|
||||
let models: GroupedModel[] = []
|
||||
const cachePath = join(homedir(), '.codex', 'models_cache.json')
|
||||
serverLog.info(`[connect-agent] codex models cache: "${cachePath}" (exists=${existsSync(cachePath)})`)
|
||||
|
||||
try {
|
||||
const raw = await readFile(cachePath, 'utf-8')
|
||||
|
|
@ -182,17 +342,21 @@ async function connectCodexCli(): Promise<ConnectResult> {
|
|||
provider: 'openai' as const,
|
||||
}))
|
||||
}
|
||||
} catch {
|
||||
// Cache file not found or unreadable
|
||||
} catch (cacheErr) {
|
||||
serverLog.info(`[connect-agent] codex cache read failed: ${cacheErr instanceof Error ? cacheErr.message : cacheErr}`)
|
||||
}
|
||||
|
||||
if (models.length === 0) {
|
||||
serverLog.info('[connect-agent] codex: no models found')
|
||||
return { connected: false, models: [], error: 'No models found. Try running codex once to populate the model cache.' }
|
||||
}
|
||||
|
||||
return { connected: true, models }
|
||||
serverLog.info(`[connect-agent] codex connected, ${models.length} models found`)
|
||||
const codexInfo = await buildCodexConnectionInfo()
|
||||
return { connected: true, models, ...codexInfo }
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? error.message : 'Failed to connect'
|
||||
serverLog.error(`[connect-agent] codex connection error: ${msg}`)
|
||||
return { connected: false, models: [], error: msg }
|
||||
}
|
||||
}
|
||||
|
|
@ -205,23 +369,34 @@ async function resolveOpencodeBinary(): Promise<string | undefined> {
|
|||
const { join } = await import('node:path')
|
||||
const isWin = process.platform === 'win32'
|
||||
|
||||
serverLog.info(`[resolve-opencode] platform=${process.platform}, isWindows=${isWin}`)
|
||||
|
||||
// 1. Try PATH lookup
|
||||
try {
|
||||
const cmd = isWin ? 'where opencode' : 'which opencode 2>/dev/null'
|
||||
const cmd = isWin ? 'where opencode 2>nul' : 'which opencode 2>/dev/null'
|
||||
serverLog.info(`[resolve-opencode] PATH lookup: ${cmd}`)
|
||||
const result = execSync(cmd, { encoding: 'utf-8', timeout: 5000 }).trim().split(/\r?\n/)[0]?.trim()
|
||||
serverLog.info(`[resolve-opencode] PATH result: "${result}" (exists=${result ? existsSync(result) : false})`)
|
||||
if (result && existsSync(result)) return result
|
||||
} catch { /* not in PATH */ }
|
||||
} catch (err) {
|
||||
serverLog.info(`[resolve-opencode] PATH lookup failed: ${err instanceof Error ? err.message : err}`)
|
||||
}
|
||||
|
||||
// 2. Try `npm prefix -g` to find actual npm global bin directory
|
||||
// On Windows, must use `npm.cmd` since Electron spawns cmd.exe
|
||||
try {
|
||||
const npmCmd = isWin ? 'npm.cmd prefix -g' : 'npm prefix -g'
|
||||
serverLog.info(`[resolve-opencode] npm prefix lookup: ${npmCmd}`)
|
||||
const prefix = execSync(npmCmd, { encoding: 'utf-8', timeout: 5000 }).trim()
|
||||
serverLog.info(`[resolve-opencode] npm global prefix: "${prefix}"`)
|
||||
if (prefix) {
|
||||
const bin = isWin ? join(prefix, 'opencode.cmd') : join(prefix, 'bin', 'opencode')
|
||||
serverLog.info(`[resolve-opencode] npm global bin: "${bin}" (exists=${existsSync(bin)})`)
|
||||
if (existsSync(bin)) return bin
|
||||
}
|
||||
} catch { /* npm not available */ }
|
||||
} catch (err) {
|
||||
serverLog.info(`[resolve-opencode] npm prefix -g failed: ${err instanceof Error ? err.message : err}`)
|
||||
}
|
||||
|
||||
// 3. Common install locations
|
||||
// npm -g → %APPDATA%\npm (Windows), /usr/local (macOS/Linux)
|
||||
|
|
@ -251,27 +426,35 @@ async function resolveOpencodeBinary(): Promise<string | undefined> {
|
|||
join(home, '.local', 'bin', 'opencode'),
|
||||
]
|
||||
for (const c of candidates) {
|
||||
if (c && existsSync(c)) return c
|
||||
const exists = c ? existsSync(c) : false
|
||||
serverLog.info(`[resolve-opencode] candidate: "${c}" (exists=${exists})`)
|
||||
if (c && exists) return c
|
||||
}
|
||||
|
||||
serverLog.info('[resolve-opencode] no opencode binary found')
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** Connect to OpenCode and fetch its configured providers/models. */
|
||||
async function connectOpenCode(): Promise<ConnectResult> {
|
||||
serverLog.info('[connect-agent] connecting to OpenCode...')
|
||||
try {
|
||||
const binaryPath = await resolveOpencodeBinary()
|
||||
serverLog.info(`[connect-agent] resolved opencode path: ${binaryPath ?? 'NOT FOUND'}`)
|
||||
if (!binaryPath) {
|
||||
return { connected: false, models: [], notInstalled: true, error: 'OpenCode CLI not found' }
|
||||
}
|
||||
|
||||
const { getOpencodeClient, releaseOpencodeServer } = await import('../../utils/opencode-client')
|
||||
serverLog.info('[connect-agent] creating opencode client...')
|
||||
const { client, server } = await getOpencodeClient()
|
||||
|
||||
serverLog.info('[connect-agent] fetching opencode providers...')
|
||||
const { data, error } = await client.config.providers()
|
||||
releaseOpencodeServer(server)
|
||||
|
||||
if (error) {
|
||||
serverLog.error(`[connect-agent] opencode providers error: ${JSON.stringify(error)}`)
|
||||
return { connected: false, models: [], error: 'Failed to fetch providers from OpenCode server.' }
|
||||
}
|
||||
|
||||
|
|
@ -289,21 +472,34 @@ async function connectOpenCode(): Promise<ConnectResult> {
|
|||
}
|
||||
|
||||
if (models.length === 0) {
|
||||
serverLog.info('[connect-agent] opencode: no models found')
|
||||
return { connected: false, models: [], error: 'No models configured in OpenCode. Run "opencode" to set up providers.' }
|
||||
}
|
||||
|
||||
return { connected: true, models }
|
||||
const providerNames = (data?.providers ?? []).map((p) => p.name || p.id).filter(Boolean)
|
||||
const providerSummary = providerNames.length > 0
|
||||
? `Connected (${providerNames.slice(0, 3).join(', ')}${providerNames.length > 3 ? ` +${providerNames.length - 3}` : ''})`
|
||||
: 'Connected via OpenCode server'
|
||||
serverLog.info(`[connect-agent] opencode connected, ${models.length} models found`)
|
||||
return {
|
||||
connected: true, models,
|
||||
connectionInfo: providerSummary,
|
||||
hintPath: configPath('~/.opencode/config.json', '%USERPROFILE%\\.opencode\\config.json'),
|
||||
}
|
||||
} catch (error) {
|
||||
const raw = error instanceof Error ? error.message : 'Failed to connect'
|
||||
serverLog.error(`[connect-agent] opencode connection error: ${raw}`)
|
||||
return { connected: false, models: [], error: friendlyOpenCodeError(raw) }
|
||||
}
|
||||
}
|
||||
|
||||
/** Connect to GitHub Copilot CLI via @github/copilot-sdk and fetch available models. */
|
||||
async function connectCopilot(): Promise<ConnectResult> {
|
||||
serverLog.info('[connect-agent] connecting to Copilot...')
|
||||
// Use standalone copilot binary to avoid Bun's node:sqlite issue
|
||||
const { resolveCopilotCli } = await import('../../utils/copilot-client')
|
||||
const cliPath = resolveCopilotCli()
|
||||
serverLog.info(`[connect-agent] resolved copilot path: ${cliPath ?? 'NOT FOUND'}`)
|
||||
if (!cliPath) {
|
||||
return { connected: false, models: [], notInstalled: true, error: 'GitHub Copilot CLI not found' }
|
||||
}
|
||||
|
|
@ -312,10 +508,12 @@ async function connectCopilot(): Promise<ConnectResult> {
|
|||
const { CopilotClient } = await import('@github/copilot-sdk')
|
||||
const client = new CopilotClient({ autoStart: true, cliPath })
|
||||
|
||||
serverLog.info('[connect-agent] starting copilot client...')
|
||||
await client.start()
|
||||
|
||||
let models: GroupedModel[] = []
|
||||
try {
|
||||
serverLog.info('[connect-agent] listing copilot models...')
|
||||
const modelList = await client.listModels()
|
||||
models = modelList
|
||||
.filter((m) => !m.policy || m.policy.state === 'enabled')
|
||||
|
|
@ -327,19 +525,39 @@ async function connectCopilot(): Promise<ConnectResult> {
|
|||
}))
|
||||
} catch (listErr) {
|
||||
const msg = listErr instanceof Error ? listErr.message : 'Failed to list models'
|
||||
serverLog.error(`[connect-agent] copilot listModels error: ${msg}`)
|
||||
await client.stop().catch(() => {})
|
||||
return { connected: false, models: [], error: friendlyCopilotError(msg) }
|
||||
}
|
||||
|
||||
// Try to get auth status for user info
|
||||
const copilotHintPath = configPath('~/.config/github-copilot/config.json', '%USERPROFILE%\\.config\\github-copilot\\config.json')
|
||||
let copilotInfo: { connectionInfo: string; hintPath?: string } = { connectionInfo: 'Connected via GitHub', hintPath: copilotHintPath }
|
||||
try {
|
||||
const authStatus = await client.getAuthStatus()
|
||||
serverLog.info(`[connect-agent] copilot auth: ${JSON.stringify(authStatus)}`)
|
||||
if (authStatus?.login) {
|
||||
const method = authStatus.authType ? ` (${authStatus.authType})` : ''
|
||||
copilotInfo = { connectionInfo: `Connected as @${authStatus.login}${method}`, hintPath: copilotHintPath }
|
||||
} else if (authStatus?.statusMessage) {
|
||||
copilotInfo = { connectionInfo: authStatus.statusMessage, hintPath: copilotHintPath }
|
||||
}
|
||||
} catch (authErr) {
|
||||
serverLog.warn(`[connect-agent] copilot getAuthStatus failed: ${authErr instanceof Error ? authErr.message : authErr}`)
|
||||
}
|
||||
|
||||
await client.stop()
|
||||
|
||||
if (models.length === 0) {
|
||||
serverLog.info('[connect-agent] copilot: no models found')
|
||||
return { connected: false, models: [], error: 'No models found. Run "copilot login" to authenticate first.' }
|
||||
}
|
||||
|
||||
return { connected: true, models }
|
||||
serverLog.info(`[connect-agent] copilot connected, ${models.length} models found`)
|
||||
return { connected: true, models, ...copilotInfo }
|
||||
} catch (error) {
|
||||
const raw = error instanceof Error ? error.message : 'Failed to connect'
|
||||
serverLog.error(`[connect-agent] copilot connection error: ${raw}`)
|
||||
return { connected: false, models: [], error: friendlyCopilotError(raw) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,7 +128,8 @@ async function tryNpmInstall(pkg: string, binary: string): Promise<InstallResult
|
|||
}
|
||||
|
||||
try {
|
||||
execSync(`npm install -g ${pkg}`, {
|
||||
const npmBin = process.platform === 'win32' ? 'npm.cmd' : 'npm'
|
||||
execSync(`${npmBin} install -g ${pkg}`, {
|
||||
encoding: 'utf-8',
|
||||
timeout: 180_000,
|
||||
stdio: 'pipe',
|
||||
|
|
|
|||
|
|
@ -1,16 +1,64 @@
|
|||
import { execSync } from 'node:child_process'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { serverLog } from './server-logger'
|
||||
|
||||
const isWindows = process.platform === 'win32'
|
||||
|
||||
/** Resolve the standalone copilot CLI binary path to avoid Bun's node:sqlite issue */
|
||||
export function resolveCopilotCli(): string | undefined {
|
||||
serverLog.info(`[resolve-copilot] platform=${process.platform}, isWindows=${isWindows}`)
|
||||
|
||||
// 1. Try PATH lookup
|
||||
try {
|
||||
const cmd = isWindows ? 'where copilot 2>nul' : 'which copilot 2>/dev/null'
|
||||
serverLog.info(`[resolve-copilot] PATH lookup: ${cmd}`)
|
||||
const result = execSync(cmd, { encoding: 'utf-8', timeout: 5000 }).trim()
|
||||
// `where` on Windows may return multiple lines
|
||||
const path = result.split(/\r?\n/)[0]?.trim()
|
||||
return path || undefined
|
||||
} catch {
|
||||
return undefined
|
||||
serverLog.info(`[resolve-copilot] PATH result: "${path}" (exists=${path ? existsSync(path) : false})`)
|
||||
if (path && existsSync(path)) return path
|
||||
} catch (err) {
|
||||
serverLog.info(`[resolve-copilot] PATH lookup failed: ${err instanceof Error ? err.message : err}`)
|
||||
}
|
||||
|
||||
// 2. Try `npm prefix -g` on Windows (npm install -g creates .cmd wrappers)
|
||||
if (isWindows) {
|
||||
try {
|
||||
serverLog.info('[resolve-copilot] trying npm.cmd prefix -g')
|
||||
const prefix = execSync('npm.cmd prefix -g', {
|
||||
encoding: 'utf-8',
|
||||
timeout: 5000,
|
||||
}).trim()
|
||||
serverLog.info(`[resolve-copilot] npm global prefix: "${prefix}"`)
|
||||
if (prefix) {
|
||||
const bin = join(prefix, 'copilot.cmd')
|
||||
serverLog.info(`[resolve-copilot] npm global bin: "${bin}" (exists=${existsSync(bin)})`)
|
||||
if (existsSync(bin)) return bin
|
||||
}
|
||||
} catch (err) {
|
||||
serverLog.info(`[resolve-copilot] npm prefix -g failed: ${err instanceof Error ? err.message : err}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Common install locations
|
||||
if (isWindows) {
|
||||
const candidates = [
|
||||
// npm global
|
||||
join(process.env.APPDATA || '', 'npm', 'copilot.cmd'),
|
||||
// nvm-windows / fnm
|
||||
join(process.env.NVM_SYMLINK || '', 'copilot.cmd'),
|
||||
join(process.env.FNM_MULTISHELL_PATH || '', 'copilot.cmd'),
|
||||
// winget / native
|
||||
join(process.env.LOCALAPPDATA || '', 'Microsoft', 'WinGet', 'Links', 'copilot.exe'),
|
||||
]
|
||||
for (const c of candidates) {
|
||||
const exists = c ? existsSync(c) : false
|
||||
serverLog.info(`[resolve-copilot] candidate: "${c}" (exists=${exists})`)
|
||||
if (c && exists) return c
|
||||
}
|
||||
}
|
||||
|
||||
serverLog.warn('[resolve-copilot] no copilot binary found')
|
||||
return undefined
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { execSync } from 'node:child_process'
|
|||
import { existsSync } from 'node:fs'
|
||||
import { homedir, platform } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { serverLog } from './server-logger'
|
||||
|
||||
const isWindows = platform() === 'win32'
|
||||
|
||||
|
|
@ -15,19 +16,52 @@ const isWindows = platform() === 'win32'
|
|||
* binaries and spawns them directly (no `node` wrapper needed).
|
||||
*/
|
||||
export function resolveClaudeCli(): string | undefined {
|
||||
serverLog.info(`[resolve-claude-cli] platform=${platform()}, isWindows=${isWindows}`)
|
||||
|
||||
// 1. Try PATH lookup
|
||||
try {
|
||||
const cmd = isWindows ? 'where claude' : 'which claude 2>/dev/null'
|
||||
const p = execSync(cmd, {
|
||||
const cmd = isWindows ? 'where claude 2>nul' : 'which claude 2>/dev/null'
|
||||
serverLog.info(`[resolve-claude-cli] PATH lookup: ${cmd}`)
|
||||
const raw = execSync(cmd, {
|
||||
encoding: 'utf-8',
|
||||
timeout: 3000,
|
||||
}).trim().split(/\r?\n/)[0] // `where` on Windows may return multiple lines
|
||||
}).trim()
|
||||
const p = raw.split(/\r?\n/)[0] // `where` on Windows may return multiple lines
|
||||
serverLog.info(`[resolve-claude-cli] PATH lookup result: "${p}" (exists=${p ? existsSync(p) : false})`)
|
||||
if (p && existsSync(p)) return p
|
||||
} catch { /* not in PATH */ }
|
||||
} catch (err) {
|
||||
serverLog.info(`[resolve-claude-cli] PATH lookup failed: ${err instanceof Error ? err.message : err}`)
|
||||
}
|
||||
|
||||
// 2. Common install locations
|
||||
// 2. Try `npm prefix -g` to find actual npm global bin directory
|
||||
// On Windows, must use `npm.cmd` since Electron spawns cmd.exe
|
||||
if (isWindows) {
|
||||
try {
|
||||
serverLog.info('[resolve-claude-cli] trying npm.cmd prefix -g')
|
||||
const prefix = execSync('npm.cmd prefix -g', {
|
||||
encoding: 'utf-8',
|
||||
timeout: 5000,
|
||||
}).trim()
|
||||
serverLog.info(`[resolve-claude-cli] npm global prefix: "${prefix}"`)
|
||||
if (prefix) {
|
||||
const bin = join(prefix, 'claude.cmd')
|
||||
serverLog.info(`[resolve-claude-cli] checking npm global bin: "${bin}" (exists=${existsSync(bin)})`)
|
||||
if (existsSync(bin)) return bin
|
||||
}
|
||||
} catch (err) {
|
||||
serverLog.info(`[resolve-claude-cli] npm prefix -g failed: ${err instanceof Error ? err.message : err}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Common install locations
|
||||
const candidates = isWindows
|
||||
? [
|
||||
// npm global (npm install -g creates .cmd wrappers here)
|
||||
join(process.env.APPDATA || '', 'npm', 'claude.cmd'),
|
||||
// nvm-windows / fnm
|
||||
join(process.env.NVM_SYMLINK || '', 'claude.cmd'),
|
||||
join(process.env.FNM_MULTISHELL_PATH || '', 'claude.cmd'),
|
||||
// Native .exe install locations
|
||||
join(process.env.LOCALAPPDATA || '', 'Programs', 'claude-code', 'claude.exe'),
|
||||
join(process.env.LOCALAPPDATA || '', 'Microsoft', 'WinGet', 'Links', 'claude.exe'),
|
||||
join(homedir(), '.claude', 'local', 'claude.exe'),
|
||||
|
|
@ -39,8 +73,11 @@ export function resolveClaudeCli(): string | undefined {
|
|||
'/opt/homebrew/bin/claude',
|
||||
]
|
||||
for (const c of candidates) {
|
||||
if (c && existsSync(c)) return c
|
||||
const exists = c ? existsSync(c) : false
|
||||
serverLog.info(`[resolve-claude-cli] candidate: "${c}" (exists=${exists})`)
|
||||
if (c && exists) return c
|
||||
}
|
||||
|
||||
serverLog.warn('[resolve-claude-cli] no claude binary found')
|
||||
return undefined
|
||||
}
|
||||
|
|
|
|||
88
server/utils/server-logger.ts
Normal file
88
server/utils/server-logger.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
/**
|
||||
* Simple file logger for the Nitro server process.
|
||||
*
|
||||
* Writes to `~/.openpencil/logs/server-{YYYY-MM-DD}.log`.
|
||||
* Also forwards to console for dev mode visibility.
|
||||
* Keeps the last 7 days of logs, auto-cleans on first write.
|
||||
*/
|
||||
|
||||
import { appendFileSync, existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from 'node:fs'
|
||||
import { homedir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
|
||||
const MAX_LOG_DAYS = 7
|
||||
|
||||
const logDir = join(homedir(), '.openpencil', 'logs')
|
||||
let dirEnsured = false
|
||||
let cleanedUp = false
|
||||
|
||||
function ensureDir(): void {
|
||||
if (dirEnsured) return
|
||||
try {
|
||||
if (!existsSync(logDir)) {
|
||||
mkdirSync(logDir, { recursive: true })
|
||||
}
|
||||
dirEnsured = true
|
||||
} catch {
|
||||
// Silently fail — logging is best-effort
|
||||
}
|
||||
}
|
||||
|
||||
function todayStamp(): string {
|
||||
return new Date().toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
function timestamp(): string {
|
||||
return new Date().toISOString()
|
||||
}
|
||||
|
||||
function getLogFilePath(): string {
|
||||
return join(logDir, `server-${todayStamp()}.log`)
|
||||
}
|
||||
|
||||
function cleanOldLogs(): void {
|
||||
if (cleanedUp) return
|
||||
cleanedUp = true
|
||||
try {
|
||||
const files = readdirSync(logDir)
|
||||
const cutoff = Date.now() - MAX_LOG_DAYS * 24 * 60 * 60 * 1000
|
||||
for (const file of files) {
|
||||
if (!file.endsWith('.log')) continue
|
||||
const filePath = join(logDir, file)
|
||||
try {
|
||||
const s = statSync(filePath)
|
||||
if (s.mtimeMs < cutoff) {
|
||||
unlinkSync(filePath)
|
||||
}
|
||||
} catch {
|
||||
// ignore individual file errors
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
function writeLine(level: string, msg: string): void {
|
||||
const line = `${timestamp()} [${level}] ${msg}\n`
|
||||
// Forward to console
|
||||
if (level === 'ERROR') {
|
||||
process.stderr.write(line)
|
||||
} else {
|
||||
process.stdout.write(line)
|
||||
}
|
||||
// Write to file
|
||||
try {
|
||||
ensureDir()
|
||||
cleanOldLogs()
|
||||
appendFileSync(getLogFilePath(), line, 'utf-8')
|
||||
} catch {
|
||||
// Disk full or permission error — silently drop
|
||||
}
|
||||
}
|
||||
|
||||
export const serverLog = {
|
||||
info: (msg: string) => writeLine('INFO', msg),
|
||||
warn: (msg: string) => writeLine('WARN', msg),
|
||||
error: (msg: string) => writeLine('ERROR', msg),
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ import type { SVGProps } from 'react'
|
|||
export default function CopilotLogo(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 98 96"
|
||||
viewBox="-14 -14 126 124"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import type { ComponentType, SVGProps } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { X, Check, Loader2, Unplug, AlertCircle, Zap, Terminal, Play, Square, Globe, Copy, RefreshCw, Download, ExternalLink } from 'lucide-react'
|
||||
import {
|
||||
X, Check, Loader2, Unplug, AlertCircle, Terminal, Play, Square,
|
||||
Copy, Download, ExternalLink, Pen, Settings,
|
||||
} from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
|
|
@ -43,9 +46,11 @@ const PROVIDER_META: Record<
|
|||
},
|
||||
}
|
||||
|
||||
type SettingsTab = 'agents' | 'mcp' | 'system'
|
||||
|
||||
async function connectAgent(
|
||||
agent: 'claude-code' | 'codex-cli' | 'opencode' | 'copilot',
|
||||
): Promise<{ connected: boolean; models: GroupedModel[]; error?: string; notInstalled?: boolean }> {
|
||||
): Promise<{ connected: boolean; models: GroupedModel[]; error?: string; notInstalled?: boolean; connectionInfo?: string; hintPath?: string }> {
|
||||
try {
|
||||
const res = await fetch('/api/ai/connect-agent', {
|
||||
method: 'POST',
|
||||
|
|
@ -89,7 +94,8 @@ async function callMcpInstall(
|
|||
return res.json()
|
||||
}
|
||||
|
||||
function ProviderRow({ type }: { type: AIProviderType }) {
|
||||
/* ---------- ProviderCard ---------- */
|
||||
function ProviderCard({ type }: { type: AIProviderType }) {
|
||||
const { t } = useTranslation()
|
||||
const provider = useAgentSettingsStore((s) => s.providers[type])
|
||||
const connect = useAgentSettingsStore((s) => s.connectProvider)
|
||||
|
|
@ -111,7 +117,7 @@ function ProviderRow({ type }: { type: AIProviderType }) {
|
|||
setInstallInfo(null)
|
||||
const result = await connectAgent(meta.agent)
|
||||
if (result.connected) {
|
||||
connect(type, meta.agent, result.models)
|
||||
connect(type, meta.agent, result.models, result.connectionInfo, result.hintPath)
|
||||
persist()
|
||||
} else if (result.notInstalled) {
|
||||
setNotInstalled(true)
|
||||
|
|
@ -134,7 +140,6 @@ function ProviderRow({ type }: { type: AIProviderType }) {
|
|||
setInstallInfo(null)
|
||||
const result = await installAgent(meta.agent)
|
||||
if (result.success) {
|
||||
// Auto-connect after successful install
|
||||
setIsInstalling(false)
|
||||
setNotInstalled(false)
|
||||
handleConnect()
|
||||
|
|
@ -160,7 +165,6 @@ function ProviderRow({ type }: { type: AIProviderType }) {
|
|||
|
||||
const { Icon } = meta
|
||||
|
||||
// Button logic: connected → Disconnect, installing → spinner, notInstalled (no instructions yet) → Install, else → Connect
|
||||
const renderAction = () => {
|
||||
if (provider.isConnected) {
|
||||
return (
|
||||
|
|
@ -211,41 +215,47 @@ function ProviderRow({ type }: { type: AIProviderType }) {
|
|||
<div className="group">
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-2.5 px-3 py-1.5 rounded-lg transition-colors',
|
||||
'flex items-center gap-3 px-3.5 py-2.5 rounded-lg border transition-colors',
|
||||
provider.isConnected
|
||||
? 'bg-secondary/40'
|
||||
: 'hover:bg-secondary/30',
|
||||
? 'bg-secondary/30 border-border'
|
||||
: 'border-transparent hover:bg-secondary/20',
|
||||
)}
|
||||
>
|
||||
{/* Icon */}
|
||||
<div
|
||||
className={cn(
|
||||
'w-6 h-6 rounded-md flex items-center justify-center shrink-0 transition-colors',
|
||||
provider.isConnected ? 'bg-foreground/10 text-foreground' : 'bg-secondary text-muted-foreground',
|
||||
'w-9 h-9 rounded-lg flex items-center justify-center shrink-0 transition-colors',
|
||||
provider.isConnected ? 'bg-foreground/8 text-foreground' : 'bg-secondary text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
<Icon className="w-3.5 h-3.5" />
|
||||
<Icon className="w-5 h-5" />
|
||||
</div>
|
||||
|
||||
{/* Name + description */}
|
||||
{/* Name + status */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[13px] font-medium text-foreground leading-tight">{t(meta.labelKey)}</span>
|
||||
<span className="text-[10px] text-muted-foreground leading-tight hidden sm:inline">{t(meta.descriptionKey)}</span>
|
||||
</div>
|
||||
{provider.isConnected && (
|
||||
<span className="text-[13px] font-medium text-foreground leading-tight block">{t(meta.labelKey)}</span>
|
||||
{provider.isConnected && provider.connectionInfo && (
|
||||
<span className="text-[11px] text-green-500 leading-tight flex items-center gap-1 mt-0.5">
|
||||
<Check size={10} strokeWidth={2.5} />
|
||||
{provider.connectionInfo}
|
||||
</span>
|
||||
)}
|
||||
{provider.isConnected && !provider.connectionInfo && (
|
||||
<span className="text-[11px] text-green-500 leading-tight flex items-center gap-1 mt-0.5">
|
||||
<Check size={10} strokeWidth={2.5} />
|
||||
{t('agents.modelCount', { count: provider.models.length })}
|
||||
</span>
|
||||
)}
|
||||
{!provider.isConnected && !notInstalled && !error && (
|
||||
<span className="text-[11px] text-muted-foreground leading-tight mt-0.5 block">{t(meta.descriptionKey)}</span>
|
||||
)}
|
||||
{notInstalled && !isInstalling && !error && (
|
||||
<span className="text-[10px] text-amber-500 leading-tight mt-0.5 block">
|
||||
<span className="text-[11px] text-amber-500 leading-tight mt-0.5 block">
|
||||
{t('agents.notInstalled')}
|
||||
</span>
|
||||
)}
|
||||
{error && (
|
||||
<span className="text-[10px] text-destructive leading-tight mt-0.5 block">{error}</span>
|
||||
<span className="text-[11px] text-destructive leading-tight mt-0.5 block">{error}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
@ -274,10 +284,179 @@ function ProviderRow({ type }: { type: AIProviderType }) {
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Provider-specific hint */}
|
||||
{provider.isConnected && provider.hintPath && (
|
||||
<p className="text-[10px] text-muted-foreground/60 px-3.5 mt-1">
|
||||
{t('settings.envHint', { path: provider.hintPath })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/* ---------- Sidebar nav item ---------- */
|
||||
function NavItem({ icon: IconComp, label, active, onClick }: {
|
||||
icon: ComponentType<{ size?: number; className?: string }>; label: string; active?: boolean; onClick: () => void
|
||||
}) {
|
||||
return (
|
||||
<button onClick={onClick} className={cn(
|
||||
'flex items-center gap-2.5 w-full px-3 py-1.5 rounded-lg text-[13px] transition-colors text-left',
|
||||
active ? 'bg-secondary text-foreground font-medium' : 'text-muted-foreground hover:text-foreground hover:bg-secondary/40',
|
||||
)}>
|
||||
<IconComp size={14} className="shrink-0" />
|
||||
{label}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
/* ---------- Agents page ---------- */
|
||||
function AgentsPage() {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div>
|
||||
<h3 className="text-[15px] font-semibold text-foreground mb-4">{t('settings.agents')}</h3>
|
||||
<div className="space-y-1">
|
||||
<ProviderCard type="anthropic" />
|
||||
<ProviderCard type="openai" />
|
||||
<ProviderCard type="opencode" />
|
||||
<ProviderCard type="copilot" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/* ---------- MCP page ---------- */
|
||||
interface McpPageProps {
|
||||
mcpServerRunning: boolean; mcpServerLocalIp: string | null; mcpServerLoading: boolean
|
||||
mcpServerError: string | null; mcpHttpPort: number; configCopied: boolean
|
||||
mcpIntegrations: { tool: string; displayName: string; enabled: boolean }[]
|
||||
mcpInstalling: string | null; mcpError: string | null; isBusy: boolean
|
||||
onServerToggle: () => void; onCopyConfig: () => void
|
||||
onPortBlur: (value: string) => void; onToggleMCP: (tool: string) => void
|
||||
}
|
||||
function McpPage(props: McpPageProps) {
|
||||
const {
|
||||
mcpServerRunning, mcpServerLocalIp, mcpServerLoading, mcpServerError,
|
||||
mcpHttpPort, mcpIntegrations, mcpInstalling, mcpError, isBusy, configCopied,
|
||||
onServerToggle, onCopyConfig, onPortBlur, onToggleMCP,
|
||||
} = props
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div>
|
||||
{/* MCP Server */}
|
||||
<div className="mb-6">
|
||||
<h3 className="text-[15px] font-semibold text-foreground mb-3">{t('agents.mcpServer')}</h3>
|
||||
<div className="flex items-center gap-2.5 px-3.5 py-2.5 rounded-lg border border-border bg-secondary/20">
|
||||
<div
|
||||
className={cn(
|
||||
'w-2 h-2 rounded-full shrink-0',
|
||||
mcpServerRunning ? 'bg-green-500' : 'bg-muted-foreground/30',
|
||||
)}
|
||||
/>
|
||||
<span className="text-[13px] text-foreground flex-1">
|
||||
{mcpServerRunning ? t('agents.mcpServerRunning') : t('agents.mcpServerStopped')}
|
||||
</span>
|
||||
<span className="text-[11px] text-muted-foreground shrink-0">{t('agents.port')}</span>
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
defaultValue={mcpHttpPort}
|
||||
key={mcpHttpPort}
|
||||
onBlur={(e) => onPortBlur(e.target.value)}
|
||||
disabled={mcpServerRunning || mcpServerLoading}
|
||||
className="h-6 w-[52px] text-[11px] text-center tabular-nums bg-secondary text-foreground rounded border border-input focus:border-ring outline-none transition-colors disabled:opacity-50"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={mcpServerRunning ? 'outline' : 'default'}
|
||||
onClick={onServerToggle}
|
||||
disabled={mcpServerLoading}
|
||||
className="h-7 px-3 text-[11px] shrink-0"
|
||||
>
|
||||
{mcpServerLoading ? (
|
||||
<Loader2 size={11} className="animate-spin" />
|
||||
) : mcpServerRunning ? (
|
||||
<>
|
||||
<Square size={10} className="mr-1" />
|
||||
{t('agents.mcpServerStop')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play size={10} className="mr-1" />
|
||||
{t('agents.mcpServerStart')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
{mcpServerRunning && mcpServerLocalIp && (
|
||||
<div className="mt-2 px-3.5 py-2 rounded-lg bg-secondary/15 border border-border/50">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-[11px] text-muted-foreground">{t('agents.mcpClientConfig')}</span>
|
||||
<Button variant="ghost" size="icon-sm" onClick={onCopyConfig} className="shrink-0 h-5 w-5">
|
||||
{configCopied ? <Check size={9} className="text-green-500" /> : <Copy size={9} />}
|
||||
</Button>
|
||||
</div>
|
||||
<code className="text-[10px] text-muted-foreground font-mono select-all leading-none">
|
||||
{`{ "type": "http", "url": "http://${mcpServerLocalIp}:${mcpHttpPort}/mcp" }`}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
{mcpServerError && (
|
||||
<div className="flex items-center gap-1.5 mt-2 px-1">
|
||||
<AlertCircle size={11} className="text-destructive shrink-0" />
|
||||
<p className="text-[11px] text-destructive">{mcpServerError}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* MCP Integrations */}
|
||||
<div>
|
||||
<h3 className="text-[15px] font-semibold text-foreground mb-1">{t('agents.mcpIntegrations')}</h3>
|
||||
<p className="text-[11px] text-muted-foreground mb-3">{t('agents.mcpRestart')}</p>
|
||||
<div className="grid grid-cols-2 gap-1.5">
|
||||
{mcpIntegrations.map((m) => (
|
||||
<div
|
||||
key={m.tool}
|
||||
className={cn(
|
||||
'flex items-center justify-between py-2 px-3.5 rounded-lg border transition-colors',
|
||||
m.enabled ? 'bg-secondary/30 border-border' : 'border-transparent hover:bg-secondary/20',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 min-w-0">
|
||||
<span
|
||||
className={cn(
|
||||
'text-[12px] truncate',
|
||||
m.enabled ? 'text-foreground' : 'text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
{m.displayName}
|
||||
</span>
|
||||
{mcpInstalling === m.tool && (
|
||||
<Loader2 size={10} className="animate-spin text-muted-foreground shrink-0" />
|
||||
)}
|
||||
</div>
|
||||
<Switch
|
||||
checked={m.enabled}
|
||||
disabled={isBusy}
|
||||
onCheckedChange={() => onToggleMCP(m.tool)}
|
||||
className="shrink-0 ml-2"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{mcpError && (
|
||||
<div className="flex items-center gap-1.5 mt-2 px-1">
|
||||
<AlertCircle size={11} className="text-destructive shrink-0" />
|
||||
<p className="text-[11px] text-destructive">{mcpError}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/* ---------- Main Dialog ---------- */
|
||||
export default function AgentSettingsDialog() {
|
||||
const { t } = useTranslation()
|
||||
const open = useAgentSettingsStore((s) => s.dialogOpen)
|
||||
|
|
@ -292,6 +471,8 @@ export default function AgentSettingsDialog() {
|
|||
const setMcpServerStatus = useAgentSettingsStore((s) => s.setMcpServerStatus)
|
||||
const dialogRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const [activeTab, setActiveTab] = useState<SettingsTab>('agents')
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
|
|
@ -313,7 +494,6 @@ export default function AgentSettingsDialog() {
|
|||
setIsElectron(!!window.electronAPI)
|
||||
}, [])
|
||||
|
||||
// Fetch MCP server status on dialog open
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
fetch('/api/mcp/server')
|
||||
|
|
@ -324,7 +504,6 @@ export default function AgentSettingsDialog() {
|
|||
.catch(() => {})
|
||||
}, [open, setMcpServerStatus])
|
||||
|
||||
// Fetch auto-update setting on dialog open (Electron only)
|
||||
useEffect(() => {
|
||||
if (!open || !window.electronAPI?.updater?.getAutoCheck) return
|
||||
window.electronAPI.updater.getAutoCheck()
|
||||
|
|
@ -389,11 +568,8 @@ export default function AgentSettingsDialog() {
|
|||
if (result.success) {
|
||||
toggleMCP(tool)
|
||||
persist()
|
||||
// When the server fell back to HTTP mode (no node installed),
|
||||
// the MCP HTTP server was auto-started — sync UI status
|
||||
if (result.fallbackHttp) {
|
||||
setMcpServerStatus(true, null)
|
||||
// Refresh actual status (localIp) after server finishes starting
|
||||
setTimeout(() => {
|
||||
fetch('/api/mcp/server')
|
||||
.then((r) => r.json())
|
||||
|
|
@ -437,188 +613,90 @@ export default function AgentSettingsDialog() {
|
|||
/>
|
||||
<div
|
||||
ref={dialogRef}
|
||||
className="relative bg-card rounded-xl border border-border w-[480px] max-h-[80vh] overflow-hidden shadow-xl flex flex-col"
|
||||
className="relative bg-card rounded-xl border border-border w-[720px] h-[520px] overflow-hidden shadow-xl flex"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-5 pt-3 pb-2">
|
||||
<h3 className="text-sm font-semibold text-foreground">
|
||||
{t('agents.title')}
|
||||
</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => setDialogOpen(false)}
|
||||
>
|
||||
<X size={14} />
|
||||
</Button>
|
||||
{/* Sidebar */}
|
||||
<div className="w-[200px] shrink-0 border-r border-border flex flex-col bg-card">
|
||||
<div className="px-4 pt-4 pb-3">
|
||||
<h2 className="text-[15px] font-semibold text-foreground">{t('settings.title')}</h2>
|
||||
</div>
|
||||
<nav className="flex-1 px-2 space-y-0.5">
|
||||
<NavItem
|
||||
icon={Pen}
|
||||
label={t('settings.agents')}
|
||||
active={activeTab === 'agents'}
|
||||
onClick={() => setActiveTab('agents')}
|
||||
/>
|
||||
<NavItem
|
||||
icon={Terminal}
|
||||
label={t('settings.mcp')}
|
||||
active={activeTab === 'mcp'}
|
||||
onClick={() => setActiveTab('mcp')}
|
||||
/>
|
||||
<NavItem
|
||||
icon={Settings}
|
||||
label={t('settings.system')}
|
||||
active={activeTab === 'system'}
|
||||
onClick={() => setActiveTab('system')}
|
||||
/>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Scrollable content */}
|
||||
<div className="flex-1 overflow-y-auto px-5 pb-4">
|
||||
{/* Agents section */}
|
||||
<div className="mb-3">
|
||||
<div className="flex items-center gap-2 mb-1 px-1">
|
||||
<Zap size={12} className="text-muted-foreground" />
|
||||
<h4 className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider">
|
||||
{t('agents.agentsOnCanvas')}
|
||||
</h4>
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
<ProviderRow type="anthropic" />
|
||||
<ProviderRow type="openai" />
|
||||
<ProviderRow type="opencode" />
|
||||
<ProviderRow type="copilot" />
|
||||
</div>
|
||||
{/* Content */}
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
{/* Close button */}
|
||||
<div className="flex justify-end px-4 pt-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => setDialogOpen(false)}
|
||||
>
|
||||
<X size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="h-px bg-border mb-3" />
|
||||
{/* Page content */}
|
||||
<div className="flex-1 overflow-y-auto px-5 pb-5">
|
||||
{activeTab === 'agents' && <AgentsPage />}
|
||||
|
||||
{/* MCP Server section */}
|
||||
<div className="mb-3">
|
||||
<div className="flex items-center gap-2 mb-1.5 px-1">
|
||||
<Globe size={12} className="text-muted-foreground" />
|
||||
<h4 className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider">
|
||||
{t('agents.mcpServer')}
|
||||
</h4>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-secondary/30">
|
||||
{/* Status indicator */}
|
||||
<div
|
||||
className={cn(
|
||||
'w-2 h-2 rounded-full shrink-0',
|
||||
mcpServerRunning ? 'bg-green-500' : 'bg-muted-foreground/30',
|
||||
)}
|
||||
{activeTab === 'mcp' && (
|
||||
<McpPage
|
||||
mcpServerRunning={mcpServerRunning}
|
||||
mcpServerLocalIp={mcpServerLocalIp}
|
||||
mcpServerLoading={mcpServerLoading}
|
||||
mcpServerError={mcpServerError}
|
||||
mcpHttpPort={mcpHttpPort}
|
||||
mcpIntegrations={mcpIntegrations}
|
||||
mcpInstalling={mcpInstalling}
|
||||
mcpError={mcpError}
|
||||
isBusy={isBusy}
|
||||
configCopied={configCopied}
|
||||
onServerToggle={handleMcpServerToggle}
|
||||
onCopyConfig={handleCopyConfig}
|
||||
onPortBlur={handlePortBlur}
|
||||
onToggleMCP={handleToggleMCP}
|
||||
/>
|
||||
<span className="text-[12px] text-foreground flex-1">
|
||||
{mcpServerRunning ? t('agents.mcpServerRunning') : t('agents.mcpServerStopped')}
|
||||
</span>
|
||||
<span className="text-[11px] text-muted-foreground shrink-0">{t('agents.port')}</span>
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
defaultValue={mcpHttpPort}
|
||||
key={mcpHttpPort}
|
||||
onBlur={(e) => handlePortBlur(e.target.value)}
|
||||
disabled={mcpServerRunning || mcpServerLoading}
|
||||
className="h-6 w-[52px] text-[11px] text-center tabular-nums bg-secondary text-foreground rounded border border-input focus:border-ring outline-none transition-colors disabled:opacity-50"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={mcpServerRunning ? 'outline' : 'default'}
|
||||
onClick={handleMcpServerToggle}
|
||||
disabled={mcpServerLoading}
|
||||
className="h-7 px-3 text-[11px] shrink-0"
|
||||
>
|
||||
{mcpServerLoading ? (
|
||||
<Loader2 size={11} className="animate-spin" />
|
||||
) : mcpServerRunning ? (
|
||||
<>
|
||||
<Square size={10} className="mr-1" />
|
||||
{t('agents.mcpServerStop')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play size={10} className="mr-1" />
|
||||
{t('agents.mcpServerStart')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
{mcpServerRunning && mcpServerLocalIp && (
|
||||
<div className="mt-1.5 px-3 py-1.5 rounded-lg bg-secondary/20">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] text-muted-foreground">{t('agents.mcpClientConfig')}</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={handleCopyConfig}
|
||||
className="shrink-0 h-5 w-5"
|
||||
>
|
||||
{configCopied ? <Check size={9} className="text-green-500" /> : <Copy size={9} />}
|
||||
</Button>
|
||||
</div>
|
||||
<code className="text-[10px] text-muted-foreground font-mono select-all leading-none">{`{ "type": "http", "url": "http://${mcpServerLocalIp}:${mcpHttpPort}/mcp" }`}</code>
|
||||
</div>
|
||||
)}
|
||||
{mcpServerError && (
|
||||
<div className="flex items-center gap-1.5 mt-2 px-1">
|
||||
<AlertCircle size={11} className="text-destructive shrink-0" />
|
||||
<p className="text-[10px] text-destructive">{mcpServerError}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="h-px bg-border mb-3" />
|
||||
|
||||
{/* MCP integrations section */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1.5 px-1">
|
||||
<Terminal size={12} className="text-muted-foreground" />
|
||||
<h4 className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider">
|
||||
{t('agents.mcpIntegrations')}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-x-2 gap-y-0">
|
||||
{mcpIntegrations.map((m) => (
|
||||
<div
|
||||
key={m.tool}
|
||||
className={cn(
|
||||
'flex items-center justify-between py-1.5 px-3 rounded-lg transition-colors',
|
||||
m.enabled ? 'bg-secondary/40' : 'hover:bg-secondary/20',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 min-w-0">
|
||||
<span
|
||||
className={cn(
|
||||
'text-[12px] truncate',
|
||||
m.enabled ? 'text-foreground' : 'text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
{m.displayName}
|
||||
</span>
|
||||
{mcpInstalling === m.tool && (
|
||||
<Loader2 size={10} className="animate-spin text-muted-foreground shrink-0" />
|
||||
)}
|
||||
{activeTab === 'system' && (
|
||||
<div>
|
||||
<h3 className="text-[15px] font-semibold text-foreground mb-4">{t('settings.system')}</h3>
|
||||
{isElectron && (
|
||||
<div className="flex items-center justify-between px-3.5 py-2.5 rounded-lg border border-border bg-secondary/20">
|
||||
<div>
|
||||
<span className="text-[13px] text-foreground block leading-tight">{t('agents.autoUpdate')}</span>
|
||||
<span className="text-[11px] text-muted-foreground mt-0.5 block">{t('settings.autoUpdateDesc')}</span>
|
||||
</div>
|
||||
<Switch checked={autoUpdateEnabled} onCheckedChange={handleAutoUpdateToggle} />
|
||||
</div>
|
||||
<Switch
|
||||
checked={m.enabled}
|
||||
disabled={isBusy}
|
||||
onCheckedChange={() => handleToggleMCP(m.tool)}
|
||||
className="shrink-0 ml-2"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{mcpError && (
|
||||
<div className="flex items-center gap-1.5 mt-2 px-1">
|
||||
<AlertCircle size={11} className="text-destructive shrink-0" />
|
||||
<p className="text-[10px] text-destructive">{mcpError}</p>
|
||||
)}
|
||||
{!isElectron && (
|
||||
<div className="rounded-lg border border-border bg-secondary/20 px-4 py-6 text-center">
|
||||
<p className="text-[13px] text-muted-foreground">{t('settings.systemDesktopOnly')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<p className="text-[10px] text-muted-foreground/60 mt-2 px-1">
|
||||
{t('agents.mcpRestart')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Auto-update toggle (Electron only) */}
|
||||
{isElectron && (
|
||||
<>
|
||||
<div className="h-px bg-border my-3" />
|
||||
<div className="flex items-center justify-between px-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<RefreshCw size={12} className="text-muted-foreground" />
|
||||
<span className="text-[12px] text-foreground">{t('agents.autoUpdate')}</span>
|
||||
</div>
|
||||
<Switch
|
||||
checked={autoUpdateEnabled}
|
||||
onCheckedChange={handleAutoUpdateToggle}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -365,6 +365,13 @@ const de: TranslationKeys = {
|
|||
'agents.installing': 'Installiere...',
|
||||
'agents.installFailed': 'Installation fehlgeschlagen',
|
||||
'agents.viewDocs': 'Doku',
|
||||
'settings.title': 'Einstellungen',
|
||||
'settings.agents': 'Agents',
|
||||
'settings.mcp': 'MCP',
|
||||
'settings.system': 'System',
|
||||
'settings.autoUpdateDesc': 'Beim Start automatisch nach neuen Versionen suchen',
|
||||
'settings.systemDesktopOnly': 'Systemeinstellungen sind in der Desktop-App verfügbar.',
|
||||
'settings.envHint': 'Sie können zusätzliche Umgebungsvariablen in {{path}} festlegen.',
|
||||
|
||||
// ── Figma Import ──
|
||||
'figma.title': 'Aus Figma importieren',
|
||||
|
|
|
|||
|
|
@ -361,6 +361,13 @@ const en = {
|
|||
'agents.installing': 'Installing...',
|
||||
'agents.installFailed': 'Installation failed',
|
||||
'agents.viewDocs': 'Docs',
|
||||
'settings.title': 'Settings',
|
||||
'settings.agents': 'Agents',
|
||||
'settings.mcp': 'MCP',
|
||||
'settings.system': 'System',
|
||||
'settings.autoUpdateDesc': 'Automatically check for new versions on startup',
|
||||
'settings.systemDesktopOnly': 'System settings are available in the desktop app.',
|
||||
'settings.envHint': 'You can set additional environment variables in {{path}}.',
|
||||
|
||||
// ── Figma Import ──
|
||||
'figma.title': 'Import from Figma',
|
||||
|
|
|
|||
|
|
@ -370,6 +370,13 @@ const es: TranslationKeys = {
|
|||
'agents.installing': 'Instalando...',
|
||||
'agents.installFailed': 'Instalación fallida',
|
||||
'agents.viewDocs': 'Docs',
|
||||
'settings.title': 'Configuración',
|
||||
'settings.agents': 'Agents',
|
||||
'settings.mcp': 'MCP',
|
||||
'settings.system': 'Sistema',
|
||||
'settings.autoUpdateDesc': 'Buscar automáticamente nuevas versiones al iniciar',
|
||||
'settings.systemDesktopOnly': 'La configuración del sistema está disponible en la aplicación de escritorio.',
|
||||
'settings.envHint': 'Puedes establecer variables de entorno adicionales en {{path}}.',
|
||||
|
||||
// ── Figma Import ──
|
||||
'figma.title': 'Importar desde Figma',
|
||||
|
|
|
|||
|
|
@ -368,6 +368,13 @@ const fr: TranslationKeys = {
|
|||
'agents.installing': 'Installation...',
|
||||
'agents.installFailed': "Échec de l'installation",
|
||||
'agents.viewDocs': 'Docs',
|
||||
'settings.title': 'Paramètres',
|
||||
'settings.agents': 'Agents',
|
||||
'settings.mcp': 'MCP',
|
||||
'settings.system': 'Système',
|
||||
'settings.autoUpdateDesc': 'Vérifier automatiquement les nouvelles versions au démarrage',
|
||||
'settings.systemDesktopOnly': 'Les paramètres système sont disponibles dans l\'application de bureau.',
|
||||
'settings.envHint': 'Vous pouvez définir des variables d\'environnement supplémentaires dans {{path}}.',
|
||||
|
||||
// ── Figma Import ──
|
||||
'figma.title': 'Importer depuis Figma',
|
||||
|
|
|
|||
|
|
@ -363,6 +363,13 @@ const hi: TranslationKeys = {
|
|||
'agents.installing': 'स्थापित हो रहा है...',
|
||||
'agents.installFailed': 'स्थापना विफल',
|
||||
'agents.viewDocs': 'दस्तावेज़',
|
||||
'settings.title': 'सेटिंग्स',
|
||||
'settings.agents': 'Agents',
|
||||
'settings.mcp': 'MCP',
|
||||
'settings.system': 'सिस्टम',
|
||||
'settings.autoUpdateDesc': 'स्टार्टअप पर स्वचालित रूप से नए संस्करणों की जाँच करें',
|
||||
'settings.systemDesktopOnly': 'सिस्टम सेटिंग्स डेस्कटॉप ऐप में उपलब्ध हैं।',
|
||||
'settings.envHint': 'आप {{path}} में अतिरिक्त पर्यावरण चर सेट कर सकते हैं।',
|
||||
|
||||
// ── Figma Import ──
|
||||
'figma.title': 'Figma से आयात करें',
|
||||
|
|
|
|||
|
|
@ -363,6 +363,13 @@ const id: TranslationKeys = {
|
|||
'agents.installing': 'Memasang...',
|
||||
'agents.installFailed': 'Pemasangan gagal',
|
||||
'agents.viewDocs': 'Dokumen',
|
||||
'settings.title': 'Pengaturan',
|
||||
'settings.agents': 'Agents',
|
||||
'settings.mcp': 'MCP',
|
||||
'settings.system': 'Sistem',
|
||||
'settings.autoUpdateDesc': 'Periksa versi baru secara otomatis saat memulai',
|
||||
'settings.systemDesktopOnly': 'Pengaturan sistem tersedia di aplikasi desktop.',
|
||||
'settings.envHint': 'Anda dapat mengatur variabel lingkungan tambahan di {{path}}.',
|
||||
|
||||
// ── Figma Import ──
|
||||
'figma.title': 'Impor dari Figma',
|
||||
|
|
|
|||
|
|
@ -366,6 +366,13 @@ const ja: TranslationKeys = {
|
|||
'agents.installing': 'インストール中...',
|
||||
'agents.installFailed': 'インストール失敗',
|
||||
'agents.viewDocs': 'ドキュメント',
|
||||
'settings.title': '設定',
|
||||
'settings.agents': 'Agents',
|
||||
'settings.mcp': 'MCP',
|
||||
'settings.system': 'システム',
|
||||
'settings.autoUpdateDesc': '起動時に新しいバージョンを自動的に確認する',
|
||||
'settings.systemDesktopOnly': 'システム設定はデスクトップアプリで利用できます。',
|
||||
'settings.envHint': '{{path}} で追加の環境変数を設定できます。',
|
||||
|
||||
// ── Figma Import ──
|
||||
'figma.title': 'Figma からインポート',
|
||||
|
|
|
|||
|
|
@ -363,6 +363,13 @@ const ko: TranslationKeys = {
|
|||
'agents.installing': '설치 중...',
|
||||
'agents.installFailed': '설치 실패',
|
||||
'agents.viewDocs': '문서',
|
||||
'settings.title': '설정',
|
||||
'settings.agents': 'Agents',
|
||||
'settings.mcp': 'MCP',
|
||||
'settings.system': '시스템',
|
||||
'settings.autoUpdateDesc': '시작 시 자동으로 새 버전 확인',
|
||||
'settings.systemDesktopOnly': '시스템 설정은 데스크톱 앱에서 사용할 수 있습니다.',
|
||||
'settings.envHint': '{{path}}에서 추가 환경 변수를 설정할 수 있습니다.',
|
||||
|
||||
// ── Figma Import ──
|
||||
'figma.title': 'Figma에서 가져오기',
|
||||
|
|
|
|||
|
|
@ -365,6 +365,13 @@ const pt: TranslationKeys = {
|
|||
'agents.installing': 'Instalando...',
|
||||
'agents.installFailed': 'Falha na instalação',
|
||||
'agents.viewDocs': 'Docs',
|
||||
'settings.title': 'Configurações',
|
||||
'settings.agents': 'Agents',
|
||||
'settings.mcp': 'MCP',
|
||||
'settings.system': 'Sistema',
|
||||
'settings.autoUpdateDesc': 'Verificar automaticamente novas versões ao iniciar',
|
||||
'settings.systemDesktopOnly': 'As configurações do sistema estão disponíveis no aplicativo de desktop.',
|
||||
'settings.envHint': 'Você pode definir variáveis de ambiente adicionais em {{path}}.',
|
||||
|
||||
// ── Figma Import ──
|
||||
'figma.title': 'Importar do Figma',
|
||||
|
|
|
|||
|
|
@ -365,6 +365,13 @@ const ru: TranslationKeys = {
|
|||
'agents.installing': 'Установка...',
|
||||
'agents.installFailed': 'Ошибка установки',
|
||||
'agents.viewDocs': 'Документация',
|
||||
'settings.title': 'Настройки',
|
||||
'settings.agents': 'Agents',
|
||||
'settings.mcp': 'MCP',
|
||||
'settings.system': 'Система',
|
||||
'settings.autoUpdateDesc': 'Автоматически проверять наличие новых версий при запуске',
|
||||
'settings.systemDesktopOnly': 'Системные настройки доступны в настольном приложении.',
|
||||
'settings.envHint': 'Вы можете задать дополнительные переменные окружения в {{path}}.',
|
||||
|
||||
// ── Figma Import ──
|
||||
'figma.title': 'Импорт из Figma',
|
||||
|
|
|
|||
|
|
@ -363,6 +363,13 @@ const th: TranslationKeys = {
|
|||
'agents.installing': 'กำลังติดตั้ง...',
|
||||
'agents.installFailed': 'การติดตั้งล้มเหลว',
|
||||
'agents.viewDocs': 'เอกสาร',
|
||||
'settings.title': 'การตั้งค่า',
|
||||
'settings.agents': 'Agents',
|
||||
'settings.mcp': 'MCP',
|
||||
'settings.system': 'ระบบ',
|
||||
'settings.autoUpdateDesc': 'ตรวจสอบเวอร์ชันใหม่โดยอัตโนมัติเมื่อเริ่มต้น',
|
||||
'settings.systemDesktopOnly': 'การตั้งค่าระบบใช้ได้ในแอปเดสก์ท็อป',
|
||||
'settings.envHint': 'คุณสามารถตั้งค่าตัวแปรสภาพแวดล้อมเพิ่มเติมได้ใน {{path}}',
|
||||
|
||||
// ── Figma Import ──
|
||||
'figma.title': 'นำเข้าจาก Figma',
|
||||
|
|
|
|||
|
|
@ -363,6 +363,13 @@ const tr: TranslationKeys = {
|
|||
'agents.installing': 'Yükleniyor...',
|
||||
'agents.installFailed': 'Yükleme başarısız',
|
||||
'agents.viewDocs': 'Belgeler',
|
||||
'settings.title': 'Ayarlar',
|
||||
'settings.agents': 'Agents',
|
||||
'settings.mcp': 'MCP',
|
||||
'settings.system': 'Sistem',
|
||||
'settings.autoUpdateDesc': 'Başlangıçta yeni sürümleri otomatik olarak kontrol et',
|
||||
'settings.systemDesktopOnly': 'Sistem ayarları masaüstü uygulamasında kullanılabilir.',
|
||||
'settings.envHint': '{{path}} dosyasında ek ortam değişkenleri ayarlayabilirsiniz.',
|
||||
|
||||
// ── Figma Import ──
|
||||
'figma.title': 'Figma\'dan İçe Aktar',
|
||||
|
|
|
|||
|
|
@ -363,6 +363,13 @@ const vi: TranslationKeys = {
|
|||
'agents.installing': 'Đang cài đặt...',
|
||||
'agents.installFailed': 'Cài đặt thất bại',
|
||||
'agents.viewDocs': 'Tài liệu',
|
||||
'settings.title': 'Cài đặt',
|
||||
'settings.agents': 'Agents',
|
||||
'settings.mcp': 'MCP',
|
||||
'settings.system': 'Hệ thống',
|
||||
'settings.autoUpdateDesc': 'Tự động kiểm tra phiên bản mới khi khởi động',
|
||||
'settings.systemDesktopOnly': 'Cài đặt hệ thống khả dụng trong ứng dụng máy tính.',
|
||||
'settings.envHint': 'Bạn có thể đặt thêm biến môi trường trong {{path}}.',
|
||||
|
||||
// ── Figma Import ──
|
||||
'figma.title': 'Nhập từ Figma',
|
||||
|
|
|
|||
|
|
@ -355,6 +355,13 @@ const zhTW: TranslationKeys = {
|
|||
'agents.installing': '安裝中...',
|
||||
'agents.installFailed': '安裝失敗',
|
||||
'agents.viewDocs': '文件',
|
||||
'settings.title': '設定',
|
||||
'settings.agents': 'Agents',
|
||||
'settings.mcp': 'MCP',
|
||||
'settings.system': '系統',
|
||||
'settings.autoUpdateDesc': '啟動時自動檢查新版本',
|
||||
'settings.systemDesktopOnly': '系統設定僅在桌面應用程式中可用。',
|
||||
'settings.envHint': '你可以在 {{path}} 中設定額外的環境變數。',
|
||||
|
||||
// ── Figma Import ──
|
||||
'figma.title': '從 Figma 匯入',
|
||||
|
|
|
|||
|
|
@ -355,6 +355,13 @@ const zh: TranslationKeys = {
|
|||
'agents.installing': '安装中...',
|
||||
'agents.installFailed': '安装失败',
|
||||
'agents.viewDocs': '文档',
|
||||
'settings.title': '设置',
|
||||
'settings.agents': 'Agents',
|
||||
'settings.mcp': 'MCP',
|
||||
'settings.system': '系统',
|
||||
'settings.autoUpdateDesc': '启动时自动检查新版本',
|
||||
'settings.systemDesktopOnly': '系统设置仅在桌面应用中可用。',
|
||||
'settings.envHint': '你可以在 {{path}} 中设置额外的环境变量。',
|
||||
|
||||
// ── Figma Import ──
|
||||
'figma.title': '从 Figma 导入',
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ interface AgentSettingsState extends PersistedState {
|
|||
provider: AIProviderType,
|
||||
method: AIProviderConfig['connectionMethod'],
|
||||
models: GroupedModel[],
|
||||
connectionInfo?: string,
|
||||
hintPath?: string,
|
||||
) => void
|
||||
disconnectProvider: (provider: AIProviderType) => void
|
||||
toggleMCPIntegration: (tool: string) => void
|
||||
|
|
@ -88,7 +90,7 @@ export const useAgentSettingsStore = create<AgentSettingsState>((set, get) => ({
|
|||
mcpServerRunning: false,
|
||||
mcpServerLocalIp: null,
|
||||
|
||||
connectProvider: (provider, method, models) =>
|
||||
connectProvider: (provider, method, models, connectionInfo, hintPath) =>
|
||||
set((s) => ({
|
||||
providers: {
|
||||
...s.providers,
|
||||
|
|
@ -97,6 +99,8 @@ export const useAgentSettingsStore = create<AgentSettingsState>((set, get) => ({
|
|||
isConnected: true,
|
||||
connectionMethod: method,
|
||||
models,
|
||||
connectionInfo,
|
||||
hintPath,
|
||||
},
|
||||
},
|
||||
})),
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@ export interface AIProviderConfig {
|
|||
connectionMethod: 'claude-code' | 'codex-cli' | 'opencode' | 'copilot' | null
|
||||
/** Models fetched when the user connects this provider */
|
||||
models: GroupedModel[]
|
||||
/** Human-readable connection status, e.g. "Connected via API key" */
|
||||
connectionInfo?: string
|
||||
/** Config file path for the hint (client renders localized text) */
|
||||
hintPath?: string
|
||||
}
|
||||
|
||||
export type MCPCliTool =
|
||||
|
|
|
|||
Loading…
Reference in a new issue