diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 069611e3..17358552 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -199,6 +199,10 @@ jobs:
echo "$LOG" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
+ - name: Lowercase image path for the install snippet
+ id: meta
+ run: echo "repo=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT"
+
- name: Create GitHub Release
if: steps.check.outputs.exists == 'false'
uses: softprops/action-gh-release@v2
@@ -214,7 +218,7 @@ jobs:
**Docker:**
```bash
- docker run -it -v ~/.teleton:/data ghcr.io/${{ github.repository }}:latest setup
+ docker run -it -v ~/.teleton:/data ghcr.io/${{ steps.meta.outputs.repo }}:latest setup
```
## Changes
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 58be54bb..e9eaa06d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [0.9.0] - 2026-06-19
+
+### Added
+
+- **Native `gocoon` provider**: an OpenAI-compatible, pure-Go COCOON client ([gocoon](https://github.com/TONresistor/gocoon)). Turnkey lifecycle — auto-install and integrity-verify the runner binary, supervise it, and set up / top-up / withdraw the TON payment channel from the CLI (`teleton gocoon`) or the WebUI Gocoon page. Runner pinned to v0.2.0, default model `Qwen/Qwen3-32B`.
+- **WebUI Gocoon page**: channel status (including the fund address), top-up, withdraw, unstake, and wallet reset.
+- **Codex** in the WebUI provider selector.
+
+### Changed
+
+- **Replaced the `cocoon` provider with `gocoon`** (**Breaking**). Tool-calling now uses the model's native `tools`/`tool_calls` instead of the old XML-injection shim, which has been removed (`src/cocoon/` deleted). Set `agent.provider: gocoon` and rename the `cocoon:` config block to `gocoon:` (`gocoon.port`, default `10000`); legacy `cocoon` configs are auto-migrated on load.
+- **ToolSearch mode enabled by default.**
+- **Local SSE proxy** fronts the gocoon runner — fixes the pi-ai "empty response / zero tokens" issue and surfaces runner errors to the client.
+- **Auto-start is non-fatal**: an unfunded channel keeps the agent and WebUI running so it can be funded from the Gocoon page (requires ≥2 TON free in the fund wallet).
+- Re-verify the runner binary integrity on every launch, not only on install.
+
+### Fixed
+
+- **Unstake / withdraw**: `withdrawAll` scanned the owner wallet instead of the fund wallet, so the channel close was skipped and the staked TON stayed locked while only the liquid balance was swept. Now scans the fund address and reads channel state.
+- Harden withdraw against transient errors and re-runs (idempotent, re-broadcast-safe).
+- Hold the wallet tx-lock during the agent-wallet sweep to prevent a concurrent send from stealing the seqno.
+- Gate top-up on a running runner; show the fund address in status.
+- CI: lowercase the ghcr.io path in the release-notes Docker step.
+
+### Removed
+
+- The `cocoon` provider and its XML tool-adapter (`src/cocoon/`).
+- Dead code: `events/bus`, `memory/agent/sessions`, `telegram/callbacks/*`, web `SearchInput`, `lib/a11y`, and assorted unused helpers.
+
## [0.8.1] - 2026-03-05
### Added
diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md
index de017ddb..f99105b0 100644
--- a/GETTING_STARTED.md
+++ b/GETTING_STARTED.md
@@ -113,7 +113,7 @@ Configuration is in `~/.teleton/config.yaml`. The setup wizard generates everyth
```yaml
agent:
- provider: "anthropic" # anthropic | openai | google | xai | groq | openrouter | moonshot | mistral | cerebras | zai | minimax | huggingface | cocoon | local
+ provider: "anthropic" # anthropic | openai | google | xai | groq | openrouter | moonshot | mistral | cerebras | zai | minimax | huggingface | gocoon | local
model: "claude-opus-4-5-20251101"
max_tokens: 4096
temperature: 0.7
@@ -142,7 +142,7 @@ agent:
model: "gpt-4o"
```
-Supported: `anthropic`, `openai`, `google`, `xai`, `groq`, `openrouter`, `moonshot`, `mistral`, `cerebras`, `zai`, `minimax`, `huggingface`, `cocoon`, `local`
+Supported: `anthropic`, `openai`, `google`, `xai`, `groq`, `openrouter`, `moonshot`, `mistral`, `cerebras`, `zai`, `minimax`, `huggingface`, `gocoon`, `local`
---
diff --git a/README.md b/README.md
index 63b25bd7..51a853ed 100644
--- a/README.md
+++ b/README.md
@@ -85,7 +85,7 @@
## Prerequisites
- **Node.js 20.0.0+** - [Download](https://nodejs.org/)
-- **LLM API Key** - One of: [Anthropic](https://console.anthropic.com/) (recommended), [OpenAI](https://platform.openai.com/), [Google](https://aistudio.google.com/), [xAI](https://console.x.ai/), [Groq](https://console.groq.com/), [OpenRouter](https://openrouter.ai/), [Moonshot](https://platform.moonshot.ai/), [Mistral](https://console.mistral.ai/), [Cerebras](https://cloud.cerebras.ai/), [ZAI](https://open.bigmodel.cn/), [MiniMax](https://platform.minimaxi.com/), [Hugging Face](https://huggingface.co/settings/tokens) — or keyless: Codex (auto-detect), Cocoon (TON), Local (Ollama/vLLM)
+- **LLM API Key** - One of: [Anthropic](https://console.anthropic.com/) (recommended), [OpenAI](https://platform.openai.com/), [Google](https://aistudio.google.com/), [xAI](https://console.x.ai/), [Groq](https://console.groq.com/), [OpenRouter](https://openrouter.ai/), [Moonshot](https://platform.moonshot.ai/), [Mistral](https://console.mistral.ai/), [Cerebras](https://cloud.cerebras.ai/), [ZAI](https://open.bigmodel.cn/), [MiniMax](https://platform.minimaxi.com/), [Hugging Face](https://huggingface.co/settings/tokens) — or keyless: Codex (auto-detect), Gocoon (TON), Local (Ollama/vLLM)
- **Telegram Account** - Dedicated account recommended for security
- **Telegram API Credentials** - From [my.telegram.org/apps](https://my.telegram.org/apps)
- **Your Telegram User ID** - Message [@userinfobot](https://t.me/userinfobot)
@@ -172,7 +172,7 @@ The `teleton setup` wizard generates a fully configured `~/.teleton/config.yaml`
```yaml
agent:
- provider: "anthropic" # anthropic | openai | google | xai | groq | openrouter | moonshot | mistral | cerebras | zai | minimax | huggingface | cocoon | local
+ provider: "anthropic" # anthropic | openai | google | xai | groq | openrouter | moonshot | mistral | cerebras | zai | minimax | huggingface | gocoon | local
api_key: "sk-ant-api03-..."
model: "claude-haiku-4-5-20251001"
utility_model: "claude-haiku-4-5-20251001" # for summarization, compaction, vision
@@ -235,7 +235,7 @@ ton_proxy: # Optional: .ton domain proxy
ZAI GLM-5
|
MiniMax M2.5
|
Hugging Face DeepSeek V3.2
|
- Cocoon Decentralized (TON)
|
+ Gocoon Decentralized (TON)
|
Local Ollama, vLLM, LM Studio
|
@@ -475,7 +475,7 @@ The SDK provides namespaced access to core services:
| Layer | Technology |
|-------|------------|
-| LLM | Multi-provider via [pi-ai](https://github.com/mariozechner/pi-ai) (15 providers: Anthropic, Codex, OpenAI, Google, xAI, Groq, OpenRouter, Moonshot, Mistral, Cerebras, ZAI, MiniMax, Hugging Face, Cocoon, Local) |
+| LLM | Multi-provider via [pi-ai](https://github.com/mariozechner/pi-ai) (15 providers: Anthropic, Codex, OpenAI, Google, xAI, Groq, OpenRouter, Moonshot, Mistral, Cerebras, ZAI, MiniMax, Hugging Face, Gocoon, Local) |
| Telegram Userbot | [GramJS](https://gram.js.org/) Layer 223 fork (MTProto) |
| Inline Bot | [Grammy](https://grammy.dev/) (Bot API, for deals) |
| Blockchain | [TON SDK](https://github.com/ton-org/ton) (W5R1 wallet) |
@@ -593,7 +593,7 @@ packages/sdk/ # Published @teleton-agent/sdk
| **Payment security** | `INSERT OR IGNORE` on tx hashes prevents double-spend, atomic status transitions prevent race conditions |
| **Exec audit** | All YOLO mode commands logged to `exec_audit` table with user, command, output, and timestamps |
| **Pino redaction** | Structured logging with automatic redaction of apiKey, password, secret, token, mnemonic fields |
-| **Tool scoping** | Financial tools DM-only, moderation group-only, admin-only policies, per-chat permissions configurable at runtime |
+| **Tool access control** | Per-tool access level (all, allow-list, admin, off), DM vs group gated by global policies, per-group module permissions, all runtime-configurable |
### Reporting Vulnerabilities
diff --git a/docs/configuration.md b/docs/configuration.md
index 60e3364f..b59220a2 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -23,7 +23,7 @@ Run `teleton setup` to generate a config file interactively, or copy `config.exa
- [plugins](#plugins)
- [ton_proxy](#ton_proxy)
- [api](#api)
-- [cocoon](#cocoon)
+- [gocoon](#gocoon)
- [tonapi_key](#tonapi_key)
- [toncenter_api_key](#toncenter_api_key)
- [tavily_api_key](#tavily_api_key)
@@ -38,7 +38,7 @@ LLM provider and agentic loop configuration.
| Key | Type | Default | Description |
|-----|------|---------|-------------|
-| `agent.provider` | `enum` | `"anthropic"` | LLM provider. One of: `anthropic`, `codex`, `openai`, `google`, `xai`, `groq`, `openrouter`, `moonshot`, `mistral`, `cerebras`, `zai`, `minimax`, `huggingface`, `cocoon`, `local`. |
+| `agent.provider` | `enum` | `"anthropic"` | LLM provider. One of: `anthropic`, `codex`, `openai`, `google`, `xai`, `groq`, `openrouter`, `moonshot`, `mistral`, `cerebras`, `zai`, `minimax`, `huggingface`, `gocoon`, `local`. |
| `agent.api_key` | `string` | `""` | API key for the chosen provider. Can be overridden with `TELETON_API_KEY` env var. |
| `agent.model` | `string` | `"claude-haiku-4-5-20251001"` | Primary model ID. Auto-detected from provider if not set (only for non-Anthropic providers). |
| `agent.utility_model` | `string` | *auto-detected* | Cheap/fast model used for summarization and compaction. If omitted, the platform selects one based on the provider (e.g., `claude-haiku-4-5-20251001` for Anthropic, `gpt-4o-mini` for OpenAI). |
@@ -96,7 +96,7 @@ When you change the `provider` and omit `model`, the platform auto-selects:
| `zai` | `glm-5.1` | `glm-4.5-air` |
| `minimax` | `MiniMax-M2.7` | `MiniMax-M2.7` |
| `huggingface` | `deepseek-ai/DeepSeek-V3.2` | `Qwen/Qwen3-Next-80B-A3B-Instruct` |
-| `cocoon` | `Qwen/Qwen3-32B` | `Qwen/Qwen3-32B` |
+| `gocoon` | `Qwen/Qwen3-32B` | `Qwen/Qwen3-32B` |
| `local` | `auto` | `auto` |
---
@@ -485,20 +485,20 @@ api:
---
-## cocoon
+## gocoon
-Cocoon Network configuration. The Cocoon provider is a decentralized LLM proxy that pays in TON. It requires an external `cocoon-cli` process running on the specified port.
+Gocoon configuration. Gocoon is a pure-Go COCOON client: a decentralized LLM that pays in TON. It exposes a native OpenAI-compatible API (with function calling) and requires the `gocoon-runner` process running on the specified port.
| Key | Type | Default | Description |
|-----|------|---------|-------------|
-| `cocoon.port` | `number` | `10000` | HTTP port of the `cocoon-cli` proxy (1-65535). |
+| `gocoon.port` | `number` | `10000` | HTTP port of the `gocoon-runner` OpenAI-compatible API (1-65535). |
-The `cocoon` section is optional. Only needed when `agent.provider` is set to `"cocoon"`.
+The `gocoon` section is optional. Only needed when `agent.provider` is set to `"gocoon"`.
### Example
```yaml
-cocoon:
+gocoon:
port: 10000
```
diff --git a/docs/deployment.md b/docs/deployment.md
index 9b7341a4..4c157d68 100644
--- a/docs/deployment.md
+++ b/docs/deployment.md
@@ -27,7 +27,7 @@ Before deploying, make sure you have:
1. **Node.js 20+** (required by the `engines` field in `package.json`)
2. **npm** (ships with Node.js)
3. **Telegram API credentials** -- obtain `api_id` and `api_hash` from [my.telegram.org/apps](https://my.telegram.org/apps)
-4. **LLM API key** -- from your chosen provider (Anthropic, OpenAI, Google, xAI, Groq, OpenRouter, Moonshot, Mistral, Cerebras, ZAI, MiniMax, Hugging Face, or Cocoon)
+4. **LLM API key** -- from your chosen provider (Anthropic, OpenAI, Google, xAI, Groq, OpenRouter, Moonshot, Mistral, Cerebras, ZAI, MiniMax, Hugging Face, or Gocoon)
5. **Build tools** (only for source/Docker builds) -- `python3`, `make`, `g++` for native modules (`better-sqlite3`)
---
diff --git a/knip.json b/knip.json
index cc4af4a3..567b7121 100644
--- a/knip.json
+++ b/knip.json
@@ -7,7 +7,6 @@
"src/webui/**/*.ts",
"src/agent/index.ts",
"src/telegram/index.ts",
- "src/telegram/callbacks/index.ts",
"src/sdk/hooks/index.ts",
"src/constants/index.ts",
"src/api/**/*.ts"
diff --git a/package-lock.json b/package-lock.json
index 21165f17..ab8546dc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "teleton",
- "version": "0.8.6",
+ "version": "0.9.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "teleton",
- "version": "0.8.6",
+ "version": "0.9.0",
"license": "MIT",
"workspaces": [
"packages/*"
@@ -3206,9 +3206,9 @@
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/eventemitter": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
- "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz",
+ "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/fetch": {
@@ -3226,12 +3226,6 @@
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
"license": "BSD-3-Clause"
},
- "node_modules/@protobufjs/inquire": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz",
- "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==",
- "license": "BSD-3-Clause"
- },
"node_modules/@protobufjs/path": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
@@ -3251,9 +3245,9 @@
"license": "BSD-3-Clause"
},
"node_modules/@rolldown/binding-android-arm64": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz",
- "integrity": "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
+ "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==",
"cpu": [
"arm64"
],
@@ -3268,9 +3262,9 @@
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz",
- "integrity": "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz",
+ "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==",
"cpu": [
"arm64"
],
@@ -3285,9 +3279,9 @@
}
},
"node_modules/@rolldown/binding-darwin-x64": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz",
- "integrity": "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz",
+ "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==",
"cpu": [
"x64"
],
@@ -3302,9 +3296,9 @@
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz",
- "integrity": "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz",
+ "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==",
"cpu": [
"x64"
],
@@ -3319,9 +3313,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz",
- "integrity": "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz",
+ "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==",
"cpu": [
"arm"
],
@@ -3336,9 +3330,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz",
- "integrity": "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz",
+ "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==",
"cpu": [
"arm64"
],
@@ -3353,9 +3347,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz",
- "integrity": "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz",
+ "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==",
"cpu": [
"arm64"
],
@@ -3370,9 +3364,9 @@
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz",
- "integrity": "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz",
+ "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==",
"cpu": [
"ppc64"
],
@@ -3387,9 +3381,9 @@
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz",
- "integrity": "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz",
+ "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==",
"cpu": [
"s390x"
],
@@ -3404,9 +3398,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz",
- "integrity": "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz",
+ "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==",
"cpu": [
"x64"
],
@@ -3421,9 +3415,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz",
- "integrity": "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz",
+ "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==",
"cpu": [
"x64"
],
@@ -3438,9 +3432,9 @@
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz",
- "integrity": "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz",
+ "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==",
"cpu": [
"arm64"
],
@@ -3455,9 +3449,9 @@
}
},
"node_modules/@rolldown/binding-wasm32-wasi": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz",
- "integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz",
+ "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==",
"cpu": [
"wasm32"
],
@@ -3474,9 +3468,9 @@
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz",
- "integrity": "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
+ "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==",
"cpu": [
"arm64"
],
@@ -3491,9 +3485,9 @@
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz",
- "integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz",
+ "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==",
"cpu": [
"x64"
],
@@ -7397,16 +7391,16 @@
}
},
"node_modules/form-data": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
- "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
+ "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
- "hasown": "^2.0.2",
- "mime-types": "^2.1.12"
+ "hasown": "^2.0.4",
+ "mime-types": "^2.1.35"
},
"engines": {
"node": ">= 6"
@@ -7892,9 +7886,9 @@
}
},
"node_modules/hasown": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
- "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@@ -7910,9 +7904,9 @@
"license": "MIT"
},
"node_modules/hono": {
- "version": "4.12.23",
- "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.23.tgz",
- "integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==",
+ "version": "4.12.26",
+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.26.tgz",
+ "integrity": "sha512-uyZtpnYxM9CmQ7QsQknM4zN8EftNqhON1qYeIKM0Se67CCEe2c44xyGURwB0axX2fBDu1dqHrHAc1hmNT8ITkw==",
"license": "MIT",
"engines": {
"node": ">=16.9.0"
@@ -10471,9 +10465,9 @@
}
},
"node_modules/postcss": {
- "version": "8.5.14",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
- "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
+ "version": "8.5.15",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
+ "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"dev": true,
"funding": [
{
@@ -10491,7 +10485,7 @@
],
"license": "MIT",
"dependencies": {
- "nanoid": "^3.3.11",
+ "nanoid": "^3.3.12",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -10696,24 +10690,23 @@
}
},
"node_modules/protobufjs": {
- "version": "7.5.9",
- "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.9.tgz",
- "integrity": "sha512-Od4muIm3HW1AouyHF5lONOf1FWo3hY1NbFDoy191X9GzhpgW1clCoaFjfVs2rKJNFYpTNJbje4cbAIDBZJ63ZA==",
+ "version": "7.6.4",
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz",
+ "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==",
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
"@protobufjs/codegen": "^2.0.5",
- "@protobufjs/eventemitter": "^1.1.0",
+ "@protobufjs/eventemitter": "^1.1.1",
"@protobufjs/fetch": "^1.1.1",
"@protobufjs/float": "^1.0.2",
- "@protobufjs/inquire": "^1.1.2",
"@protobufjs/path": "^1.1.2",
"@protobufjs/pool": "^1.1.0",
"@protobufjs/utf8": "^1.1.1",
"@types/node": ">=13.7.0",
- "long": "^5.0.0"
+ "long": "^5.3.2"
},
"engines": {
"node": ">=12.0.0"
@@ -11293,13 +11286,13 @@
}
},
"node_modules/rolldown": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz",
- "integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
+ "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@oxc-project/types": "=0.130.0",
+ "@oxc-project/types": "=0.133.0",
"@rolldown/pluginutils": "^1.0.0"
},
"bin": {
@@ -11309,21 +11302,31 @@
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
- "@rolldown/binding-android-arm64": "1.0.1",
- "@rolldown/binding-darwin-arm64": "1.0.1",
- "@rolldown/binding-darwin-x64": "1.0.1",
- "@rolldown/binding-freebsd-x64": "1.0.1",
- "@rolldown/binding-linux-arm-gnueabihf": "1.0.1",
- "@rolldown/binding-linux-arm64-gnu": "1.0.1",
- "@rolldown/binding-linux-arm64-musl": "1.0.1",
- "@rolldown/binding-linux-ppc64-gnu": "1.0.1",
- "@rolldown/binding-linux-s390x-gnu": "1.0.1",
- "@rolldown/binding-linux-x64-gnu": "1.0.1",
- "@rolldown/binding-linux-x64-musl": "1.0.1",
- "@rolldown/binding-openharmony-arm64": "1.0.1",
- "@rolldown/binding-wasm32-wasi": "1.0.1",
- "@rolldown/binding-win32-arm64-msvc": "1.0.1",
- "@rolldown/binding-win32-x64-msvc": "1.0.1"
+ "@rolldown/binding-android-arm64": "1.0.3",
+ "@rolldown/binding-darwin-arm64": "1.0.3",
+ "@rolldown/binding-darwin-x64": "1.0.3",
+ "@rolldown/binding-freebsd-x64": "1.0.3",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.3",
+ "@rolldown/binding-linux-arm64-gnu": "1.0.3",
+ "@rolldown/binding-linux-arm64-musl": "1.0.3",
+ "@rolldown/binding-linux-ppc64-gnu": "1.0.3",
+ "@rolldown/binding-linux-s390x-gnu": "1.0.3",
+ "@rolldown/binding-linux-x64-gnu": "1.0.3",
+ "@rolldown/binding-linux-x64-musl": "1.0.3",
+ "@rolldown/binding-openharmony-arm64": "1.0.3",
+ "@rolldown/binding-wasm32-wasi": "1.0.3",
+ "@rolldown/binding-win32-arm64-msvc": "1.0.3",
+ "@rolldown/binding-win32-x64-msvc": "1.0.3"
+ }
+ },
+ "node_modules/rolldown/node_modules/@oxc-project/types": {
+ "version": "0.133.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
+ "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
}
},
"node_modules/rollup": {
@@ -12334,9 +12337,9 @@
}
},
"node_modules/tar": {
- "version": "7.5.15",
- "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz",
- "integrity": "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==",
+ "version": "7.5.16",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz",
+ "integrity": "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==",
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/fs-minipass": "^4.0.0",
@@ -12508,9 +12511,9 @@
}
},
"node_modules/tinyglobby": {
- "version": "0.2.16",
- "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
- "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -12781,9 +12784,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
- "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
+ "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
"cpu": [
"ppc64"
],
@@ -12798,9 +12801,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/android-arm": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
- "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
+ "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
"cpu": [
"arm"
],
@@ -12815,9 +12818,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/android-arm64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
- "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
+ "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
"cpu": [
"arm64"
],
@@ -12832,9 +12835,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/android-x64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
- "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
+ "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
"cpu": [
"x64"
],
@@ -12849,9 +12852,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
- "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
+ "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
"cpu": [
"arm64"
],
@@ -12866,9 +12869,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/darwin-x64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
- "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
+ "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
"cpu": [
"x64"
],
@@ -12883,9 +12886,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
- "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
"cpu": [
"arm64"
],
@@ -12900,9 +12903,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
- "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
+ "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
"cpu": [
"x64"
],
@@ -12917,9 +12920,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/linux-arm": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
- "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
+ "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
"cpu": [
"arm"
],
@@ -12934,9 +12937,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/linux-arm64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
- "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
+ "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
"cpu": [
"arm64"
],
@@ -12951,9 +12954,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/linux-ia32": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
- "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
+ "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
"cpu": [
"ia32"
],
@@ -12968,9 +12971,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/linux-loong64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
- "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
+ "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
"cpu": [
"loong64"
],
@@ -12985,9 +12988,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
- "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
+ "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
"cpu": [
"mips64el"
],
@@ -13002,9 +13005,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
- "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
+ "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
"cpu": [
"ppc64"
],
@@ -13019,9 +13022,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
- "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
+ "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
"cpu": [
"riscv64"
],
@@ -13036,9 +13039,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/linux-s390x": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
- "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
+ "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
"cpu": [
"s390x"
],
@@ -13053,9 +13056,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/linux-x64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
- "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
+ "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
"cpu": [
"x64"
],
@@ -13070,9 +13073,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/netbsd-arm64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
- "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
"cpu": [
"arm64"
],
@@ -13087,9 +13090,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
- "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
+ "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
"cpu": [
"x64"
],
@@ -13104,9 +13107,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/openbsd-arm64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
- "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
"cpu": [
"arm64"
],
@@ -13121,9 +13124,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
- "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
+ "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
"cpu": [
"x64"
],
@@ -13138,9 +13141,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/openharmony-arm64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
- "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
+ "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
"cpu": [
"arm64"
],
@@ -13155,9 +13158,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/sunos-x64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
- "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
+ "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
"cpu": [
"x64"
],
@@ -13172,9 +13175,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/win32-arm64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
- "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
+ "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
"cpu": [
"arm64"
],
@@ -13189,9 +13192,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/win32-ia32": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
- "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
+ "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
"cpu": [
"ia32"
],
@@ -13206,9 +13209,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/win32-x64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
- "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
+ "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
"cpu": [
"x64"
],
@@ -13223,9 +13226,9 @@
}
},
"node_modules/tsx/node_modules/esbuild": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
- "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
+ "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -13236,32 +13239,32 @@
"node": ">=18"
},
"optionalDependencies": {
- "@esbuild/aix-ppc64": "0.28.0",
- "@esbuild/android-arm": "0.28.0",
- "@esbuild/android-arm64": "0.28.0",
- "@esbuild/android-x64": "0.28.0",
- "@esbuild/darwin-arm64": "0.28.0",
- "@esbuild/darwin-x64": "0.28.0",
- "@esbuild/freebsd-arm64": "0.28.0",
- "@esbuild/freebsd-x64": "0.28.0",
- "@esbuild/linux-arm": "0.28.0",
- "@esbuild/linux-arm64": "0.28.0",
- "@esbuild/linux-ia32": "0.28.0",
- "@esbuild/linux-loong64": "0.28.0",
- "@esbuild/linux-mips64el": "0.28.0",
- "@esbuild/linux-ppc64": "0.28.0",
- "@esbuild/linux-riscv64": "0.28.0",
- "@esbuild/linux-s390x": "0.28.0",
- "@esbuild/linux-x64": "0.28.0",
- "@esbuild/netbsd-arm64": "0.28.0",
- "@esbuild/netbsd-x64": "0.28.0",
- "@esbuild/openbsd-arm64": "0.28.0",
- "@esbuild/openbsd-x64": "0.28.0",
- "@esbuild/openharmony-arm64": "0.28.0",
- "@esbuild/sunos-x64": "0.28.0",
- "@esbuild/win32-arm64": "0.28.0",
- "@esbuild/win32-ia32": "0.28.0",
- "@esbuild/win32-x64": "0.28.0"
+ "@esbuild/aix-ppc64": "0.28.1",
+ "@esbuild/android-arm": "0.28.1",
+ "@esbuild/android-arm64": "0.28.1",
+ "@esbuild/android-x64": "0.28.1",
+ "@esbuild/darwin-arm64": "0.28.1",
+ "@esbuild/darwin-x64": "0.28.1",
+ "@esbuild/freebsd-arm64": "0.28.1",
+ "@esbuild/freebsd-x64": "0.28.1",
+ "@esbuild/linux-arm": "0.28.1",
+ "@esbuild/linux-arm64": "0.28.1",
+ "@esbuild/linux-ia32": "0.28.1",
+ "@esbuild/linux-loong64": "0.28.1",
+ "@esbuild/linux-mips64el": "0.28.1",
+ "@esbuild/linux-ppc64": "0.28.1",
+ "@esbuild/linux-riscv64": "0.28.1",
+ "@esbuild/linux-s390x": "0.28.1",
+ "@esbuild/linux-x64": "0.28.1",
+ "@esbuild/netbsd-arm64": "0.28.1",
+ "@esbuild/netbsd-x64": "0.28.1",
+ "@esbuild/openbsd-arm64": "0.28.1",
+ "@esbuild/openbsd-x64": "0.28.1",
+ "@esbuild/openharmony-arm64": "0.28.1",
+ "@esbuild/sunos-x64": "0.28.1",
+ "@esbuild/win32-arm64": "0.28.1",
+ "@esbuild/win32-ia32": "0.28.1",
+ "@esbuild/win32-x64": "0.28.1"
}
},
"node_modules/tsyringe": {
@@ -13417,9 +13420,9 @@
}
},
"node_modules/undici": {
- "version": "7.25.0",
- "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz",
- "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==",
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
+ "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
"license": "MIT",
"engines": {
"node": ">=20.18.1"
@@ -13489,17 +13492,17 @@
}
},
"node_modules/vite": {
- "version": "8.0.13",
- "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz",
- "integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==",
+ "version": "8.0.16",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
+ "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
"dev": true,
"license": "MIT",
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
- "postcss": "^8.5.14",
- "rolldown": "1.0.1",
- "tinyglobby": "^0.2.16"
+ "postcss": "^8.5.15",
+ "rolldown": "1.0.3",
+ "tinyglobby": "^0.2.17"
},
"bin": {
"vite": "bin/vite.js"
@@ -13872,9 +13875,9 @@
}
},
"node_modules/ws": {
- "version": "8.20.1",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
- "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
+ "version": "8.21.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
+ "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
diff --git a/package.json b/package.json
index 4f3d3f42..763c40dd 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "teleton",
- "version": "0.8.6",
+ "version": "0.9.0",
"workspaces": [
"packages/*"
],
@@ -32,7 +32,6 @@
"files": [
"dist/",
"bin/",
- "scripts/",
"src/templates/"
],
"scripts": {
diff --git a/src/agent/__tests__/runtime-hooks.test.ts b/src/agent/__tests__/runtime-hooks.test.ts
index b970e779..bdd78a09 100644
--- a/src/agent/__tests__/runtime-hooks.test.ts
+++ b/src/agent/__tests__/runtime-hooks.test.ts
@@ -286,8 +286,8 @@ describe("Runtime Hook Integration", () => {
expect(mockLogger.error).not.toHaveBeenCalled();
});
- it("5.9 Cocoon provider path: hooks fire same as standard path", async () => {
- // Hooks fire before the Cocoon/standard fork — same event for both paths
+ it("5.9 gocoon provider path: hooks fire same as standard path", async () => {
+ // Hooks fire before the gocoon/standard fork — same event for both paths
const beforeCalls: string[] = [];
const afterCalls: string[] = [];
@@ -310,7 +310,7 @@ describe("Runtime Hook Integration", () => {
const runner = createHookRunner(registry, { logger: mockLogger });
- // Simulate tool call (same event regardless of Cocoon or standard provider)
+ // Simulate tool call (same event regardless of gocoon or standard provider)
const beforeEvent: BeforeToolCallEvent = {
toolName: "ton_get_balance",
params: {},
diff --git a/src/agent/client.ts b/src/agent/client.ts
index 89ceafe3..449c9727 100644
--- a/src/agent/client.ts
+++ b/src/agent/client.ts
@@ -19,7 +19,7 @@ import { getProviderModel } from "../providers/model-resolver.js";
// layer so non-agent consumers (e.g. memory) can resolve models without importing
// from agent/. Re-exported here for backward compatibility with existing importers.
export {
- registerCocoonModels,
+ registerGocoonModels,
registerLocalModels,
getProviderModel,
getUtilityModel,
@@ -38,15 +38,10 @@ const RETRY_401_PROVIDERS: { provider: string; refresh: () => Promise[\s\S]*?<\/think>/g;
/**
* Shared post-processing for both complete() and stream() responses: strip
- * blocks (Cocoon, Mistral, etc.), persist the transcript, extract the
+ * blocks (Mistral, local models, etc.), persist the transcript, extract the
* text content, and append the response to the context.
*/
function finalizeResponse(
@@ -106,23 +101,10 @@ export async function chatWithContext(
): Promise {
const provider = (config.provider || "anthropic") as SupportedProvider;
const model = getProviderModel(provider, config.model);
- const isCocoon = provider === "cocoon";
-
- let tools =
+ const tools =
provider === "google" && options.tools ? sanitizeToolsForGemini(options.tools) : options.tools;
- // Cocoon: disable thinking mode + inject tools into system prompt
- let systemPrompt = options.systemPrompt || options.context.systemPrompt || "";
- let cocoonAllowedTools: Set | undefined;
- if (isCocoon) {
- systemPrompt = "/no_think\n" + systemPrompt;
- if (tools && tools.length > 0) {
- cocoonAllowedTools = new Set(tools.map((t) => t.name));
- const { injectToolsIntoSystemPrompt } = await import("../cocoon/tool-adapter.js");
- systemPrompt = injectToolsIntoSystemPrompt(systemPrompt, tools);
- tools = undefined; // Don't send via API
- }
- }
+ const systemPrompt = options.systemPrompt || options.context.systemPrompt || "";
const context: Context = {
...options.context,
@@ -139,10 +121,6 @@ export async function chatWithContext(
sessionId: options.sessionId,
cacheRetention: "long",
};
- if (isCocoon) {
- const { stripCocoonPayload } = await import("../cocoon/tool-adapter.js");
- completeOptions.onPayload = stripCocoonPayload;
- }
let response = await complete(model, context, completeOptions as ProviderStreamOptions);
@@ -157,24 +135,6 @@ export async function chatWithContext(
}
}
- // Cocoon: parse from text response
- if (isCocoon) {
- const textBlock = response.content.find((b) => b.type === "text");
- if (textBlock?.type === "text" && textBlock.text.includes("")) {
- const { parseToolCallsFromText, extractPlainText } =
- await import("../cocoon/tool-adapter.js");
- const syntheticCalls = parseToolCallsFromText(textBlock.text, cocoonAllowedTools);
- if (syntheticCalls.length > 0) {
- const plainText = extractPlainText(textBlock.text);
- response.content = [
- ...(plainText ? [{ type: "text" as const, text: plainText }] : []),
- ...syntheticCalls,
- ];
- (response as { stopReason: AssistantMessage["stopReason"] }).stopReason = "toolUse";
- }
- }
- }
-
return finalizeResponse(response, context, options);
}
diff --git a/src/agent/runtime.ts b/src/agent/runtime.ts
index 40e92f54..a7ee66c4 100644
--- a/src/agent/runtime.ts
+++ b/src/agent/runtime.ts
@@ -44,6 +44,7 @@ import type {
ToolCall,
AssistantMessage,
Message,
+ ToolResultMessage,
} from "@mariozechner/pi-ai";
import { CompactionManager, DEFAULT_COMPACTION_CONFIG } from "../memory/compaction.js";
import { maskOldToolResults } from "../memory/observation-masking.js";
@@ -1029,13 +1030,14 @@ export class AgentRuntime {
log.warn(`Tool result too large, truncated to ${resultText.length} chars`);
}
- const { buildToolResultMessage } = await import("../cocoon/tool-adapter.js");
- const resultMsg = buildToolResultMessage(
- sink.provider,
- block,
- resultText,
- !exec.result.success
- );
+ const resultMsg: ToolResultMessage = {
+ role: "toolResult",
+ toolCallId: block.id,
+ toolName: block.name,
+ content: [{ type: "text", text: resultText }],
+ isError: !exec.result.success,
+ timestamp: Date.now(),
+ };
resultMessages.push(resultMsg);
appendToTranscript(sink.sessionId, resultMsg);
}
@@ -1411,23 +1413,6 @@ export class AgentRuntime {
return rows.map((r) => r.chat_id);
}
- setSoul(soul: string): void {
- this.soul = soul;
- }
-
- configureCompaction(config: {
- enabled?: boolean;
- maxMessages?: number;
- maxTokens?: number;
- }): void {
- this.compactionManager.updateConfig(config);
- log.info({ config: this.compactionManager.getConfig() }, `Compaction config updated`);
- }
-
- getCompactionConfig() {
- return this.compactionManager.getConfig();
- }
-
private _memoryStatsCache: {
data: { totalMessages: number; totalChats: number; knowledgeChunks: number };
expiry: number;
diff --git a/src/agent/token-usage.ts b/src/agent/token-usage.ts
index a7fec503..d7ee7067 100644
--- a/src/agent/token-usage.ts
+++ b/src/agent/token-usage.ts
@@ -16,8 +16,3 @@ export function accumulateTokenUsage(usage: {
globalTokenUsage.totalTokens += usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
globalTokenUsage.totalCost += usage.totalCost;
}
-
-export function resetTokenUsage() {
- globalTokenUsage.totalTokens = 0;
- globalTokenUsage.totalCost = 0;
-}
diff --git a/src/agent/tools/exec/index.ts b/src/agent/tools/exec/index.ts
index e27970e1..7dfce89d 100644
--- a/src/agent/tools/exec/index.ts
+++ b/src/agent/tools/exec/index.ts
@@ -1,6 +1 @@
export { default as execModule } from "./module.js";
-export { execRunTool, createExecRunExecutor } from "./run.js";
-export { execInstallTool, createExecInstallExecutor } from "./install.js";
-export { execServiceTool, createExecServiceExecutor } from "./service.js";
-export { execStatusTool, createExecStatusExecutor } from "./status.js";
-export type { ExecResult, ExecAuditEntry, RunOptions } from "./types.js";
diff --git a/src/agent/tools/registry.ts b/src/agent/tools/registry.ts
index a1aaa29d..b3e37089 100644
--- a/src/agent/tools/registry.ts
+++ b/src/agent/tools/registry.ts
@@ -6,7 +6,6 @@ import type {
RuntimeMode,
Tool,
ToolContext,
- ToolEntry,
ToolExecutor,
ToolMode,
ToolResult,
@@ -340,7 +339,7 @@ export class ToolRegistry {
/**
* Legacy single-value scope view (derived from the level). Kept for backward-
- * compatible callers (getEntry, getModuleTools).
+ * compatible callers (getModuleTools).
*/
private getEffectiveScope(toolName: string): ToolScope {
return levelToScope(this.getEffectiveLevel(toolName));
@@ -525,22 +524,6 @@ export class ToolRegistry {
return registered?.tool.parameters ?? null;
}
- /**
- * Return a ToolEntry snapshot for a named tool (scope, mode, tags, executor).
- * Returns null if the tool is not registered.
- */
- getEntry(name: string): ToolEntry | null {
- const registered = this.tools.get(name);
- if (!registered) return null;
- return {
- tool: registered.tool,
- executor: registered.executor,
- scope: this.getEffectiveScope(name),
- mode: this.tools.get(name)?.mode ?? "both",
- tags: this.tools.get(name)?.tags,
- };
- }
-
/**
* Returns true if a tool passes all scope/mode/enabled filters for the given context.
* Extracted from getForContext() for reuse by tool_search and getCoreTools().
diff --git a/src/agent/tools/scope.ts b/src/agent/tools/scope.ts
index 6d80ca65..3545e400 100644
--- a/src/agent/tools/scope.ts
+++ b/src/agent/tools/scope.ts
@@ -12,8 +12,6 @@ import type { ToolScope } from "./types.js";
*/
export type ToolAccessLevel = "all" | "allowlist" | "admin" | "off";
-export const TOOL_ACCESS_LEVELS = ["all", "allowlist", "admin", "off"] as const;
-
export function isToolAccessLevel(v: unknown): v is ToolAccessLevel {
return v === "all" || v === "allowlist" || v === "admin" || v === "off";
}
diff --git a/src/agent/tools/telegram/media/vision-analyze.ts b/src/agent/tools/telegram/media/vision-analyze.ts
index b97aa959..9e61d326 100644
--- a/src/agent/tools/telegram/media/vision-analyze.ts
+++ b/src/agent/tools/telegram/media/vision-analyze.ts
@@ -103,7 +103,7 @@ export const visionAnalyzeExecutor: ToolExecutor = async (
// Get API key from context
const currentProvider = context.config?.agent?.provider;
const apiKey = context.config?.agent?.api_key;
- if (!apiKey && currentProvider !== "local" && currentProvider !== "cocoon") {
+ if (!apiKey && currentProvider !== "local" && currentProvider !== "gocoon") {
return {
success: false,
error: "No API key configured for vision analysis",
diff --git a/src/agent/tools/ton/jetton-holders.ts b/src/agent/tools/ton/jetton-holders.ts
index c917a558..e3940e05 100644
--- a/src/agent/tools/ton/jetton-holders.ts
+++ b/src/agent/tools/ton/jetton-holders.ts
@@ -98,12 +98,6 @@ export const jettonHoldersExecutor: ToolExecutor = async (
};
});
- // Calculate concentration (top holder %)
- const _totalTop = holders.reduce(
- (sum: number, h: FormattedHolder) => sum + parseFloat(h.balance.replace(/,/g, "")),
- 0
- );
-
let message = `Top ${holders.length} holders of ${symbol}:\n\n`;
holders.forEach((h) => {
const nameTag = h.name ? ` (${h.name})` : "";
diff --git a/src/api/schemas/common.ts b/src/api/schemas/common.ts
index 0bd5fd1a..b49202f9 100644
--- a/src/api/schemas/common.ts
+++ b/src/api/schemas/common.ts
@@ -12,14 +12,6 @@ export const ProblemDetailSchema = z.object({
export type ProblemDetail = z.infer;
-/** Standard API response envelope */
-export const ApiResponseSchema = (dataSchema: T) =>
- z.object({
- success: z.boolean(),
- data: dataSchema.optional(),
- error: ProblemDetailSchema.optional(),
- });
-
/** Create a Problem Detail JSON object */
export function createProblem(
status: number,
diff --git a/src/bot/index.ts b/src/bot/index.ts
index e23bcbe6..6a09a815 100644
--- a/src/bot/index.ts
+++ b/src/bot/index.ts
@@ -507,15 +507,4 @@ export class DealBot {
}
}
-export {
- getDeal,
- getDealsAwaitingVerification,
- getDealsAwaitingExecution,
-} from "./services/deal-service.js";
-export {
- buildCompletedMessage,
- buildMessageForState,
- buildSendingMessage,
- buildFailedMessage,
-} from "./services/message-builder.js";
export { VerificationPoller } from "./services/verification-poller.js";
diff --git a/src/bot/services/deal-service.ts b/src/bot/services/deal-service.ts
index b77d0c28..382be3e4 100644
--- a/src/bot/services/deal-service.ts
+++ b/src/bot/services/deal-service.ts
@@ -78,13 +78,6 @@ export function getDeal(db: Database.Database, dealId: string): DealContext | nu
return rowToDeal(row);
}
-/**
- * Update deal status
- */
-export function updateDealStatus(db: Database.Database, dealId: string, status: DealStatus): void {
- db.prepare(`UPDATE deals SET status = ? WHERE id = ?`).run(status, dealId);
-}
-
/**
* Mark deal as accepted and extend expiry to 5 minutes from now
*/
@@ -169,24 +162,6 @@ export function getDealsAwaitingVerification(db: Database.Database): DealContext
return rows.map(rowToDeal);
}
-/**
- * Get verified deals awaiting execution
- */
-export function getDealsAwaitingExecution(db: Database.Database): DealContext[] {
- const rows = db
- .prepare(
- `SELECT
- ${DEAL_COLUMNS}
- FROM deals
- WHERE status = 'verified' AND agent_sent_at IS NULL
- ORDER BY user_payment_verified_at ASC
- LIMIT 10`
- )
- .all() as DealRow[];
-
- return rows.map(rowToDeal);
-}
-
/**
* Update user trade stats
*/
diff --git a/src/bot/services/message-builder.ts b/src/bot/services/message-builder.ts
index 6f38f30f..946adc7f 100644
--- a/src/bot/services/message-builder.ts
+++ b/src/bot/services/message-builder.ts
@@ -210,13 +210,6 @@ ${esc(error || "Unknown error")}`;
return { text, buttons: [] };
}
-/**
- * Wrong user
- */
-export function buildWrongUserMessage(deal: DealContext): string {
- return `🚫 This deal is for @${esc(String(deal.username || deal.userId))} only.`;
-}
-
/**
* Not found
*/
diff --git a/src/bot/types.ts b/src/bot/types.ts
index 26347b96..152a90e0 100644
--- a/src/bot/types.ts
+++ b/src/bot/types.ts
@@ -45,16 +45,6 @@ export type DealStatus =
| "cancelled"
| "failed";
-export type MessageState =
- | "proposal" // Accept/Decline buttons
- | "accepted" // Payment/gift instructions + "I've sent"
- | "payment_claimed" // Verifying...
- | "verified" // Sending agent's part...
- | "completed" // Final recap
- | "declined" // Declined message
- | "expired" // Expired message
- | "failed"; // Error message
-
export interface CallbackData {
action: "accept" | "decline" | "sent" | "copy_addr" | "copy_memo" | "refresh";
dealId: string;
@@ -74,10 +64,6 @@ export function splitPrefix(raw: string): { prefix: string; rest: string } | nul
return { prefix: raw.slice(0, colonIdx), rest: raw.slice(colonIdx + 1) };
}
-export function encodeCallback(data: CallbackData): string {
- return `${data.action}:${data.dealId}`;
-}
-
/**
* Callback-data routing contract (Grammy `callback_query:data` middleware chain).
*
diff --git a/src/cli/commands/doctor.ts b/src/cli/commands/doctor.ts
index 76bf39ad..baebb666 100644
--- a/src/cli/commands/doctor.ts
+++ b/src/cli/commands/doctor.ts
@@ -140,7 +140,7 @@ async function checkApiKey(workspaceDir: string): Promise {
};
}
- if (provider === "cocoon" || provider === "local") {
+ if (provider === "gocoon" || provider === "local") {
return {
name: `${meta.displayName}`,
status: "ok",
diff --git a/src/cli/commands/gocoon.ts b/src/cli/commands/gocoon.ts
new file mode 100644
index 00000000..0eb18826
--- /dev/null
+++ b/src/cli/commands/gocoon.ts
@@ -0,0 +1,185 @@
+import type { Command } from "commander";
+import { confirm } from "@inquirer/prompts";
+import {
+ ensureGocoonBinaries,
+ init,
+ isInstalled,
+ runnerBaseUrl,
+ topup,
+ waitFunded,
+ walletInfo,
+ withdrawAll,
+ resetWallet,
+ GOCOON_DEFAULT_PORT,
+ GOCOON_VERSION,
+ type GocoonProgress,
+} from "../../gocoon/index.js";
+import { BOLD, DIM, GREEN, RED, TON, YELLOW, noteBox } from "../prompts.js";
+import { getErrorMessage } from "../../utils/errors.js";
+
+function renderProgress(e: GocoonProgress): void {
+ const tag = DIM(`[${e.stage}]`);
+ if (e.status === "ok") console.log(` ${GREEN("ok")} ${tag} ${e.message}`);
+ else if (e.status === "error") console.log(` ${RED("x")} ${tag} ${e.message}`);
+ else if (e.status === "warn" || e.status === "skipped")
+ console.log(` ${YELLOW("-")} ${tag} ${e.message}`);
+ else console.log(` ${TON(">")} ${tag} ${e.message}`);
+}
+
+const indent = (line: string): void => console.log(DIM(" " + line));
+
+async function runInit(): Promise {
+ console.log(BOLD(TON("\n Gocoon setup\n")));
+
+ console.log(` ${TON(">")} Installing gocoon ${GOCOON_VERSION}`);
+ await ensureGocoonBinaries();
+ console.log(` ${GREEN("ok")} Binaries ready`);
+
+ console.log(` ${TON(">")} Preparing COCOON wallet`);
+ const summary = await init();
+ console.log(` ${GREEN("ok")} Wallet ready`);
+
+ noteBox(
+ `Send ${BOLD(summary.recommendedFundingTon + " TON")} (mainnet) to:\n\n` +
+ ` ${summary.fundAddress}\n\n` +
+ `This stakes your COCOON payment channel. Recoverable later with\n` +
+ DIM("teleton gocoon withdraw "),
+ "Fund COCOON wallet",
+ YELLOW
+ );
+
+ const sent = await confirm({ message: "Have you sent the TON?", default: false });
+ if (!sent) {
+ console.log(YELLOW("\n Setup paused. Run `teleton gocoon init` again once funded.\n"));
+ return;
+ }
+
+ console.log(` ${TON(">")} Waiting for funding to confirm on-chain`);
+ await waitFunded(indent);
+ console.log(` ${GREEN("ok")} Wallet funded`);
+
+ noteBox(
+ `Set ${BOLD("agent.provider: gocoon")} in your config.\n` +
+ `On ${BOLD("teleton start")} the runner launches automatically.`,
+ "Gocoon ready",
+ GREEN
+ );
+}
+
+async function runTopup(amountTon: string): Promise {
+ await topup(amountTon, GOCOON_DEFAULT_PORT, indent);
+ console.log(` ${GREEN("ok")} Topped up channel with ${amountTon} TON`);
+}
+
+async function runWithdraw(destination: string, skipConfirm: boolean): Promise {
+ if (!skipConfirm) {
+ const ok = await confirm({
+ message: `Withdraw everything to ${destination}? Closes the channel and drains the COCOON + agent wallets. Irreversible.`,
+ default: false,
+ });
+ if (!ok) {
+ console.log(YELLOW("\n Withdraw cancelled.\n"));
+ return;
+ }
+ }
+ console.log();
+ await withdrawAll(destination, renderProgress);
+}
+
+async function runReset(skipConfirm: boolean, force: boolean): Promise {
+ if (!skipConfirm) {
+ const ok = await confirm({
+ message:
+ "Reset the COCOON wallet? Deletes the local wallet + config so a fresh wallet is created next setup. Withdraw everything first. Irreversible.",
+ default: false,
+ });
+ if (!ok) {
+ console.log(YELLOW("\n Reset cancelled.\n"));
+ return;
+ }
+ }
+ await resetWallet({ force });
+ console.log(
+ ` ${GREEN("ok")} COCOON wallet reset. Run ${DIM("teleton gocoon init")} to create a fresh wallet.`
+ );
+}
+
+async function runStatus(): Promise {
+ console.log(BOLD("\n Gocoon status\n"));
+ const installed = isInstalled();
+ console.log(` Installed: ${installed ? GREEN(GOCOON_VERSION) : YELLOW("not installed")}`);
+
+ if (installed) {
+ try {
+ const info = await walletInfo();
+ console.log(` Fund address: ${info.fundAddress}`);
+ console.log(` Balance: ${info.balanceTon} TON`);
+ } catch {
+ console.log(` Fund address: ${DIM("not set up, run `teleton gocoon init`")}`);
+ }
+ }
+
+ const up = await fetch(`${runnerBaseUrl()}/jsonstats`, { signal: AbortSignal.timeout(800) })
+ .then(() => true)
+ .catch(() => false);
+ console.log(` Runner: ${up ? GREEN("running") : DIM("not running")}\n`);
+}
+
+async function guard(fn: () => Promise): Promise {
+ try {
+ await fn();
+ } catch (err) {
+ console.error(RED(`\n ${getErrorMessage(err)}\n`));
+ process.exit(1);
+ }
+}
+
+export function registerGocoonCommand(program: Command): void {
+ const gocoon = program
+ .command("gocoon")
+ .description("Manage gocoon (decentralized LLM on TON): install, setup, top-up, withdraw")
+ .option("-i, --init", "Install and set up gocoon (wallet + funding)")
+ .action(async (opts: { init?: boolean }) => {
+ if (opts.init) {
+ await guard(runInit);
+ return;
+ }
+ gocoon.help();
+ });
+
+ gocoon
+ .command("init")
+ .alias("i")
+ .description("Install gocoon and set up the COCOON wallet + funding")
+ .action(() => guard(runInit));
+
+ gocoon
+ .command("topup")
+ .description("Add TON to the payment channel (the runner must be active)")
+ .requiredOption("--amount ", "Amount in TON to add (e.g. 5)")
+ .action((opts: { amount: string }) => guard(() => runTopup(opts.amount)));
+
+ gocoon
+ .command("withdraw ")
+ .description("Close the channel and withdraw COCOON + agent wallets to ")
+ .option("--yes", "Skip the confirmation prompt")
+ .action((destination: string, opts: { yes?: boolean }) =>
+ guard(() => runWithdraw(destination, Boolean(opts.yes)))
+ );
+
+ gocoon
+ .command("reset")
+ .description(
+ "Delete the local COCOON wallet + config so a fresh wallet is created next setup (withdraw first)"
+ )
+ .option("--yes", "Skip the confirmation prompt")
+ .option("--force", "Reset even if the wallet still has funds or an active channel")
+ .action((opts: { yes?: boolean; force?: boolean }) =>
+ guard(() => runReset(Boolean(opts.yes), Boolean(opts.force)))
+ );
+
+ gocoon
+ .command("status")
+ .description("Show gocoon install + wallet + runner status")
+ .action(() => guard(runStatus));
+}
diff --git a/src/cli/commands/onboard.ts b/src/cli/commands/onboard.ts
index a3f8cf74..01b1c48a 100644
--- a/src/cli/commands/onboard.ts
+++ b/src/cli/commands/onboard.ts
@@ -262,7 +262,7 @@ interface BuildConfigInput {
tonapiKey?: string;
toncenterApiKey?: string;
tavilyApiKey?: string;
- cocoonPort?: number;
+ gocoonPort?: number;
sessionPath: string;
workspaceRoot: string;
}
@@ -348,6 +348,7 @@ function buildConfig(input: BuildConfigInput): Config {
],
skip_unlimited_providers: false,
},
+ tool_search: { enabled: true },
logging: { level: "info", pretty: true },
mcp: { servers: {} },
capabilities: {
@@ -367,8 +368,8 @@ function buildConfig(input: BuildConfigInput): Config {
self_configurable: false,
},
plugins: {},
- ...(input.provider === "cocoon" && input.cocoonPort
- ? { cocoon: { port: input.cocoonPort } }
+ ...(input.provider === "gocoon" && input.gocoonPort
+ ? { gocoon: { port: input.gocoonPort } }
: {}),
tonapi_key: input.tonapiKey,
toncenter_api_key: input.toncenterApiKey,
@@ -466,7 +467,7 @@ interface OnboardState {
selectedModel: string;
apiKey: string;
localBaseUrl: string;
- cocoonInstance: number;
+ gocoonInstance: number;
apiId: number;
apiHash: string;
phone: string;
@@ -592,7 +593,7 @@ async function stepProvider(
selectedProvider: SupportedProvider;
apiKey: string;
localBaseUrl: string;
- cocoonInstance: number;
+ gocoonInstance: number;
selectedModel: string;
}> {
redraw(1);
@@ -622,16 +623,16 @@ async function stepProvider(
);
}
- // API key (or Cocoon / Local setup)
+ // API key (or Gocoon / Local setup)
let apiKey = "";
let localBaseUrl = "";
- let cocoonInstance = 10000;
- if (selectedProvider === "cocoon") {
- // Cocoon Network — no API key, managed externally via cocoon-cli
+ let gocoonInstance = 10000;
+ if (selectedProvider === "gocoon") {
+ // Gocoon — no API key, decentralized LLM on TON via the gocoon-runner
apiKey = "";
- const cocoonPort = await input({
- message: "Cocoon proxy HTTP port",
+ const gocoonPort = await input({
+ message: "gocoon-runner HTTP port",
default: "10000",
theme,
validate: (value = "") => {
@@ -639,17 +640,17 @@ async function stepProvider(
return n >= 1 && n <= 65535 ? true : "Must be a port number (1-65535)";
},
});
- cocoonInstance = parseInt(cocoonPort.trim(), 10);
+ gocoonInstance = parseInt(gocoonPort.trim(), 10);
noteBox(
- "Cocoon Network — Decentralized LLM on TON\n" +
- "No API key needed. Requires cocoon-cli running externally.\n" +
- `Teleton will connect to http://localhost:${cocoonInstance}/v1/`,
- "Cocoon Network",
+ "Gocoon — Decentralized LLM on TON\n" +
+ "No API key needed. Requires the gocoon runner running externally.\n" +
+ `Teleton will connect to http://localhost:${gocoonInstance}/v1/`,
+ "Gocoon",
TON
);
- STEPS[1].value = `${providerMeta.displayName} ${DIM(`port ${cocoonInstance}`)}`;
+ STEPS[1].value = `${providerMeta.displayName} ${DIM(`port ${gocoonInstance}`)}`;
} else if (selectedProvider === "local") {
// Local LLM — no API key, needs base URL
apiKey = "";
@@ -733,7 +734,7 @@ async function stepProvider(
// Model selection (advanced mode only, after provider + API key)
let selectedModel = providerMeta.defaultModel;
- if (selectedProvider !== "cocoon" && selectedProvider !== "local") {
+ if (selectedProvider !== "gocoon" && selectedProvider !== "local") {
const providerModels = getModelsForProvider(selectedProvider);
const modelChoices = [
...providerModels,
@@ -762,7 +763,7 @@ async function stepProvider(
STEPS[1].value = `${STEPS[1].value ?? providerMeta.displayName}, ${modelLabel}`;
}
- return { selectedProvider, apiKey, localBaseUrl, cocoonInstance, selectedModel };
+ return { selectedProvider, apiKey, localBaseUrl, gocoonInstance, selectedModel };
}
/** Step 2: Config — admin user ID, DM/group policies, iterations, exec mode. */
@@ -1255,7 +1256,7 @@ async function stepConnect(
tonapiKey: state.tonapiKey,
toncenterApiKey: state.toncenterApiKey,
tavilyApiKey: state.tavilyApiKey,
- cocoonPort: state.selectedProvider === "cocoon" ? state.cocoonInstance : undefined,
+ gocoonPort: state.selectedProvider === "gocoon" ? state.gocoonInstance : undefined,
sessionPath: workspace.sessionPath,
workspaceRoot: workspace.root,
});
@@ -1324,7 +1325,7 @@ async function runInteractiveOnboarding(
selectedModel: "",
apiKey: "",
localBaseUrl: "",
- cocoonInstance: 10000,
+ gocoonInstance: 10000,
apiId: 0,
apiHash: "",
phone: "",
@@ -1360,7 +1361,7 @@ async function runInteractiveOnboarding(
state.selectedProvider = provider.selectedProvider;
state.apiKey = provider.apiKey;
state.localBaseUrl = provider.localBaseUrl;
- state.cocoonInstance = provider.cocoonInstance;
+ state.gocoonInstance = provider.gocoonInstance;
state.selectedModel = provider.selectedModel;
// Step 2: Config
@@ -1418,7 +1419,7 @@ async function runNonInteractiveOnboarding(
): Promise {
const selectedProvider = options.provider || "anthropic";
const nonInteractiveMode = options.mode || "user";
- const needsApiKey = selectedProvider !== "cocoon" && selectedProvider !== "local";
+ const needsApiKey = selectedProvider !== "gocoon" && selectedProvider !== "local";
if (nonInteractiveMode === "bot") {
if (!options.botToken) {
prompter.error("Non-interactive bot mode requires: --bot-token");
diff --git a/src/cli/index.ts b/src/cli/index.ts
index bc8fdfab..7b3b19b4 100755
--- a/src/cli/index.ts
+++ b/src/cli/index.ts
@@ -4,6 +4,7 @@ import { doctorCommand } from "./commands/doctor.js";
import { mcpAddCommand, mcpRemoveCommand, mcpListCommand } from "./commands/mcp.js";
import { configCommand } from "./commands/config.js";
import { apiRotateKeyCommand, apiFingerprintCommand } from "./commands/api.js";
+import { registerGocoonCommand } from "./commands/gocoon.js";
import { main as startApp } from "../index.js";
import { configExists, getDefaultConfigPath } from "../config/loader.js";
import { readFileSync, existsSync } from "fs";
@@ -239,6 +240,9 @@ program
}
});
+// gocoon — decentralized LLM on TON (install, setup, top-up, withdraw)
+registerGocoonCommand(program);
+
program.action(() => {
program.help();
});
diff --git a/src/cocoon/__tests__/cocoon-mock.test.ts b/src/cocoon/__tests__/cocoon-mock.test.ts
deleted file mode 100644
index ab7cfe2d..00000000
--- a/src/cocoon/__tests__/cocoon-mock.test.ts
+++ /dev/null
@@ -1,208 +0,0 @@
-import { describe, it, expect, beforeAll, afterAll } from "vitest";
-import { createServer, type Server, type IncomingMessage, type ServerResponse } from "http";
-import { registerCocoonModels } from "../../agent/client.js";
-
-// ── Mock Cocoon Proxy ────────────────────────────────────────────────
-
-const MOCK_MODELS = [
- { id: "Qwen/Qwen3-32B", object: "model" },
- { id: "Qwen/Qwen3-8B", object: "model" },
-];
-
-let server: Server;
-let port: number;
-let socketAvailable = true;
-
-function createMockCocoonProxy(): Promise<{ server: Server; port: number }> {
- return new Promise((resolve, reject) => {
- const srv = createServer((req: IncomingMessage, res: ServerResponse) => {
- let body = "";
- req.on("data", (chunk: Buffer) => (body += chunk.toString()));
- req.on("end", () => {
- res.setHeader("Content-Type", "application/json");
-
- if (req.url === "/v1/models" && req.method === "GET") {
- res.end(JSON.stringify({ object: "list", data: MOCK_MODELS }));
- return;
- }
-
- if (req.url === "/v1/chat/completions" && req.method === "POST") {
- const payload = JSON.parse(body);
-
- const violations: string[] = [];
- if (payload.tools) violations.push("tools should be stripped");
- if (payload.tool_choice) violations.push("tool_choice should be stripped");
- if (payload.store) violations.push("store should be stripped");
-
- if (violations.length > 0) {
- res.statusCode = 400;
- res.end(JSON.stringify({ error: { message: violations.join(", ") } }));
- return;
- }
-
- const systemMsg = payload.messages?.find((m: { role: string }) => m.role === "system");
- const hasToolsInPrompt = systemMsg?.content?.includes("");
-
- if (hasToolsInPrompt) {
- res.end(
- JSON.stringify({
- id: "mock-1",
- object: "chat.completion",
- choices: [
- {
- index: 0,
- message: {
- role: "assistant",
- content: `I'll search for that information.\n\n{"name": "web_search", "arguments": {"query": "test query"}}\n`,
- },
- finish_reason: "stop",
- },
- ],
- usage: { prompt_tokens: 100, completion_tokens: 50, total_tokens: 150 },
- })
- );
- } else {
- res.end(
- JSON.stringify({
- id: "mock-2",
- object: "chat.completion",
- choices: [
- {
- index: 0,
- message: { role: "assistant", content: "Hello! How can I help you?" },
- finish_reason: "stop",
- },
- ],
- usage: { prompt_tokens: 50, completion_tokens: 20, total_tokens: 70 },
- })
- );
- }
- return;
- }
-
- res.statusCode = 404;
- res.end(JSON.stringify({ error: { message: "Not found" } }));
- });
- });
-
- srv.listen(0, "127.0.0.1", () => {
- const addr = srv.address();
- const p = typeof addr === "object" && addr ? addr.port : 0;
- resolve({ server: srv, port: p });
- });
- srv.once("error", reject);
- });
-}
-
-// ── Tests ────────────────────────────────────────────────────────────
-
-describe("Cocoon Mock Server", () => {
- beforeAll(async () => {
- try {
- const mock = await createMockCocoonProxy();
- server = mock.server;
- port = mock.port;
- } catch {
- socketAvailable = false;
- }
- });
-
- afterAll(
- () =>
- new Promise((resolve) => {
- if (!server) {
- resolve();
- return;
- }
- server.close(() => resolve());
- })
- );
-
- // ── registerCocoonModels ─────────────────────────────────────────
-
- it("should discover models from /v1/models", async () => {
- if (!socketAvailable) return;
- const ids = await registerCocoonModels(port);
- expect(ids).toHaveLength(2);
- expect(ids).toContain("Qwen/Qwen3-32B");
- expect(ids).toContain("Qwen/Qwen3-8B");
- });
-
- it("should return empty array if proxy is down", async () => {
- const ids = await registerCocoonModels(59999);
- expect(ids).toEqual([]);
- });
-
- it("should return empty array if response has no models", async () => {
- if (!socketAvailable) return;
- const emptySrv = createServer((_req, res) => {
- res.setHeader("Content-Type", "application/json");
- res.end(JSON.stringify({ object: "list", data: [] }));
- });
- await new Promise((resolve, reject) => {
- emptySrv.once("error", reject);
- emptySrv.listen(0, "127.0.0.1", () => resolve());
- });
- const addr = emptySrv.address();
- const emptyPort = typeof addr === "object" && addr ? addr.port : 0;
-
- const ids = await registerCocoonModels(emptyPort);
- expect(ids).toEqual([]);
-
- await new Promise((resolve) => emptySrv.close(() => resolve()));
- });
-
- // ── Payload validation ───────────────────────────────────────────
-
- it("should reject requests with unsupported fields", async () => {
- if (!socketAvailable) return;
- const res = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- model: "Qwen/Qwen3-32B",
- messages: [{ role: "user", content: "hi" }],
- tools: [{ name: "bad" }],
- }),
- });
- expect(res.status).toBe(400);
- const body = (await res.json()) as { error: { message: string } };
- expect(body.error.message).toContain("tools should be stripped");
- });
-
- it("should accept clean cocoon payload", async () => {
- if (!socketAvailable) return;
- const res = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- model: "Qwen/Qwen3-32B",
- messages: [{ role: "user", content: "hi" }],
- presence_penalty: 1.5,
- }),
- });
- expect(res.status).toBe(200);
- const body = (await res.json()) as { choices: { message: { content: string } }[] };
- expect(body.choices[0].message.content).toContain("Hello");
- });
-
- it("should return tool_call in response when tools in system prompt", async () => {
- if (!socketAvailable) return;
- const res = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- model: "Qwen/Qwen3-32B",
- messages: [
- { role: "system", content: "You are helpful.\n\n[...]" },
- { role: "user", content: "search for cats" },
- ],
- presence_penalty: 1.5,
- }),
- });
- expect(res.status).toBe(200);
- const body = (await res.json()) as { choices: { message: { content: string } }[] };
- expect(body.choices[0].message.content).toContain("");
- expect(body.choices[0].message.content).toContain("web_search");
- });
-});
diff --git a/src/cocoon/__tests__/tool-adapter.test.ts b/src/cocoon/__tests__/tool-adapter.test.ts
deleted file mode 100644
index 72ba1adc..00000000
--- a/src/cocoon/__tests__/tool-adapter.test.ts
+++ /dev/null
@@ -1,328 +0,0 @@
-import { describe, it, expect } from "vitest";
-import {
- injectToolsIntoSystemPrompt,
- stripCocoonPayload,
- parseToolCallsFromText,
- extractPlainText,
- wrapToolResult,
-} from "../tool-adapter.js";
-import type { Tool } from "@mariozechner/pi-ai";
-
-// ── Helpers ──────────────────────────────────────────────────────────
-
-const makeTool = (name: string, desc = "A test tool"): Tool => ({
- name,
- description: desc,
- parameters: {
- type: "object",
- properties: { query: { type: "string", description: "search query" } },
- required: ["query"],
- },
-});
-
-// ── injectToolsIntoSystemPrompt ──────────────────────────────────────
-
-describe("injectToolsIntoSystemPrompt", () => {
- it("should append tool definitions in XML block", () => {
- const result = injectToolsIntoSystemPrompt("You are helpful.", [makeTool("web_search")]);
- expect(result).toContain("You are helpful.");
- expect(result).toContain("");
- expect(result).toContain("");
- expect(result).toContain('"name":"web_search"');
- expect(result).toContain("");
- });
-
- it("should include all tools", () => {
- const tools = [makeTool("tool_a"), makeTool("tool_b"), makeTool("tool_c")];
- const result = injectToolsIntoSystemPrompt("sys", tools);
- expect(result).toContain('"name":"tool_a"');
- expect(result).toContain('"name":"tool_b"');
- expect(result).toContain('"name":"tool_c"');
- });
-
- it("should return system prompt unchanged if no tools", () => {
- expect(injectToolsIntoSystemPrompt("sys", [])).toBe("sys");
- });
-
- it("should include parameter schemas", () => {
- const result = injectToolsIntoSystemPrompt("sys", [makeTool("t")]);
- expect(result).toContain('"parameters"');
- expect(result).toContain('"query"');
- });
-
- it("should handle tools with complex parameter schemas", () => {
- const tool: Tool = {
- name: "complex_tool",
- description: "Tool with nested params",
- parameters: {
- type: "object",
- properties: {
- config: {
- type: "object",
- properties: {
- nested: { type: "array", items: { type: "string" } },
- },
- },
- },
- required: [],
- },
- };
- const result = injectToolsIntoSystemPrompt("sys", [tool]);
- expect(result).toContain('"complex_tool"');
- expect(result).toContain('"nested"');
- });
-});
-
-// ── stripCocoonPayload ───────────────────────────────────────────────
-
-describe("stripCocoonPayload", () => {
- it("should remove unsupported fields", () => {
- const payload: Record = {
- model: "Qwen/Qwen3-32B",
- messages: [],
- tools: [{ name: "a" }],
- tool_choice: "auto",
- store: true,
- reasoning_effort: "high",
- stream_options: { include_usage: true },
- };
- stripCocoonPayload(payload);
- expect(payload.model).toBe("Qwen/Qwen3-32B");
- expect(payload.messages).toEqual([]);
- expect(payload).not.toHaveProperty("tools");
- expect(payload).not.toHaveProperty("tool_choice");
- expect(payload).not.toHaveProperty("store");
- expect(payload).not.toHaveProperty("reasoning_effort");
- expect(payload).not.toHaveProperty("stream_options");
- });
-
- it("should set presence_penalty to 1.5 if not provided", () => {
- const payload: Record = {};
- stripCocoonPayload(payload);
- expect(payload.presence_penalty).toBe(1.5);
- });
-
- it("should preserve existing presence_penalty", () => {
- const payload: Record = { presence_penalty: 2.0 };
- stripCocoonPayload(payload);
- expect(payload.presence_penalty).toBe(2.0);
- });
-
- it("should preserve presence_penalty = 0", () => {
- // 0 is falsy but should NOT be overridden by ?? 1.5
- const payload: Record = { presence_penalty: 0 };
- stripCocoonPayload(payload);
- expect(payload.presence_penalty).toBe(0);
- });
-
- it("should handle null payload gracefully", () => {
- expect(() => stripCocoonPayload(null)).not.toThrow();
- });
-
- it("should handle non-object payload gracefully", () => {
- expect(() => stripCocoonPayload("string")).not.toThrow();
- expect(() => stripCocoonPayload(42)).not.toThrow();
- expect(() => stripCocoonPayload(undefined)).not.toThrow();
- });
-});
-
-// ── parseToolCallsFromText ───────────────────────────────────────────
-
-describe("parseToolCallsFromText", () => {
- it("should parse a single tool call", () => {
- const text = `I'll search for that.
-
-{"name": "web_search", "arguments": {"query": "hello world"}}
-`;
- const calls = parseToolCallsFromText(text);
- expect(calls).toHaveLength(1);
- expect(calls[0].type).toBe("toolCall");
- expect(calls[0].name).toBe("web_search");
- expect(calls[0].arguments).toEqual({ query: "hello world" });
- expect(calls[0].id).toMatch(/^cocoon_/);
- });
-
- it("should parse multiple tool calls", () => {
- const text = `
-{"name": "tool_a", "arguments": {"x": 1}}
-
-
-{"name": "tool_b", "arguments": {"y": 2}}
-`;
- const calls = parseToolCallsFromText(text);
- expect(calls).toHaveLength(2);
- expect(calls[0].name).toBe("tool_a");
- expect(calls[1].name).toBe("tool_b");
- });
-
- it("should handle nested JSON in arguments", () => {
- const text = `
-{"name": "send_msg", "arguments": {"text": "He said \\"hello\\"", "config": {"nested": true}}}
-`;
- const calls = parseToolCallsFromText(text);
- expect(calls).toHaveLength(1);
- expect(calls[0].name).toBe("send_msg");
- expect(calls[0].arguments.config).toEqual({ nested: true });
- });
-
- it("should strip blocks before parsing", () => {
- const text = `
-I need to search for this.
-
-
-{"name": "web_search", "arguments": {"query": "test"}}
-`;
- const calls = parseToolCallsFromText(text);
- expect(calls).toHaveLength(1);
- expect(calls[0].name).toBe("web_search");
- });
-
- it("should return empty array for no tool calls", () => {
- expect(parseToolCallsFromText("Just a regular response.")).toEqual([]);
- });
-
- it("should return empty array for malformed JSON", () => {
- const text = `
-{not valid json}
-`;
- const calls = parseToolCallsFromText(text);
- expect(calls).toEqual([]);
- });
-
- it("should skip tool calls without a name", () => {
- const text = `
-{"arguments": {"x": 1}}
-`;
- const calls = parseToolCallsFromText(text);
- expect(calls).toEqual([]);
- });
-
- it("should default arguments to empty object", () => {
- const text = `
-{"name": "simple_tool"}
-`;
- const calls = parseToolCallsFromText(text);
- expect(calls).toHaveLength(1);
- expect(calls[0].arguments).toEqual({});
- });
-
- it("should handle unclosed tool_call tag", () => {
- const text = `
-{"name": "broken", "arguments": {}}
-no closing tag`;
- const calls = parseToolCallsFromText(text);
- expect(calls).toEqual([]);
- });
-
- it("should handle empty tool_call tags", () => {
- const text = ``;
- const calls = parseToolCallsFromText(text);
- expect(calls).toEqual([]);
- });
-
- it("should generate unique IDs for each call", () => {
- const text = `
-{"name": "a", "arguments": {}}
-
-
-{"name": "b", "arguments": {}}
-`;
- const calls = parseToolCallsFromText(text);
- expect(calls[0].id).not.toBe(calls[1].id);
- });
-
- it("should handle arguments with special characters", () => {
- // JSON requires \" for quotes inside strings — use String.raw to preserve backslashes
- const text = String.raw`
-{"name": "send", "arguments": {"text": "Hello & \"friends\""}}
-`;
- const calls = parseToolCallsFromText(text);
- expect(calls).toHaveLength(1);
- expect(calls[0].arguments.text).toBe('Hello & "friends"');
- });
-});
-
-// ── extractPlainText ─────────────────────────────────────────────────
-
-describe("extractPlainText", () => {
- it("should remove tool_call blocks and keep surrounding text", () => {
- const text = `Hello world.
-
-{"name": "test", "arguments": {}}
-
-Goodbye.`;
- expect(extractPlainText(text)).toBe("Hello world.\n\nGoodbye.");
- });
-
- it("should remove multiple tool_call blocks", () => {
- const text = `AXBYC`;
- expect(extractPlainText(text)).toBe("ABC");
- });
-
- it("should remove think blocks", () => {
- const text = `reasoning hereThe answer is 42.`;
- expect(extractPlainText(text)).toBe("The answer is 42.");
- });
-
- it("should remove both think and tool_call blocks", () => {
- const text = `hmmHello{"name":"x","arguments":{}} world`;
- expect(extractPlainText(text)).toBe("Hello world");
- });
-
- it("should return empty string for pure tool calls", () => {
- const text = `
-{"name": "a", "arguments": {}}
-`;
- expect(extractPlainText(text)).toBe("");
- });
-
- it("should handle text with no special blocks", () => {
- expect(extractPlainText("Just regular text.")).toBe("Just regular text.");
- });
-
- it("should handle consecutive tool_call blocks", () => {
- const text = `ABC`;
- expect(extractPlainText(text)).toBe("");
- });
-});
-
-// ── wrapToolResult ───────────────────────────────────────────────────
-
-describe("wrapToolResult", () => {
- it("should wrap in tool_response with CDATA", () => {
- const result = wrapToolResult("success: true");
- expect(result).toBe(`\n\n`);
- });
-
- it("should escape ]]> in content (CDATA injection)", () => {
- const result = wrapToolResult("data with ]]> inside");
- expect(result).toBe(
- `\n inside]]>\n`
- );
- });
-
- it("should handle empty result", () => {
- const result = wrapToolResult("");
- expect(result).toBe(`\n\n`);
- });
-
- it("should handle result with XML-like content", () => {
- const result = wrapToolResult('Hello & World
');
- expect(result).toContain('Hello & World
');
- });
-
- it("should handle multiple ]]> sequences", () => {
- const result = wrapToolResult("a]]>b]]>c");
- // Each ]]> gets split
- expect(result).not.toContain("]]>]]>");
- // But the structure should be valid CDATA
- expect(result).toContain("");
- expect(result).toContain("");
- });
-
- it("should handle large content", () => {
- const bigContent = "x".repeat(100000);
- const result = wrapToolResult(bigContent);
- expect(result).toContain(bigContent);
- });
-});
diff --git a/src/cocoon/tool-adapter.ts b/src/cocoon/tool-adapter.ts
deleted file mode 100644
index 781479ed..00000000
--- a/src/cocoon/tool-adapter.ts
+++ /dev/null
@@ -1,246 +0,0 @@
-/**
- * Cocoon Tool Adapter
- *
- * Translation layer for tool calling via Cocoon proxy.
- * The proxy doesn't support the OpenAI `tools` parameter, so we:
- * 1. Inject tool definitions into the system prompt (Qwen3 Hermes format)
- * 2. Strip unsupported fields from the request body
- * 3. Parse XML tags from the model's text response
- */
-
-import { randomUUID } from "crypto";
-import { createLogger } from "../utils/logger.js";
-import type { Tool, UserMessage, ToolResultMessage } from "@mariozechner/pi-ai";
-
-const log = createLogger("Cocoon");
-
-// ── System Prompt Injection ──────────────────────────────────────────
-
-const TOOL_PREAMBLE = `
-
-# Tools
-
-You may call one or more functions to assist with the user query.
-
-You are provided with function signatures within XML tags:
-
-`;
-
-const TOOL_POSTAMBLE = `
-
-For each function call, return a json object with function name and arguments within XML tags:
-
-{"name": , "arguments": }
-`;
-
-/**
- * Append Qwen3-style tool definitions to the system prompt.
- */
-export function injectToolsIntoSystemPrompt(systemPrompt: string, tools: Tool[]): string {
- if (!tools || tools.length === 0) return systemPrompt;
-
- const toolLines = tools.map((t) =>
- JSON.stringify({
- type: "function",
- function: {
- name: t.name,
- description: t.description,
- parameters: t.parameters,
- },
- })
- );
-
- return systemPrompt + TOOL_PREAMBLE + toolLines.join("\n") + "\n" + TOOL_POSTAMBLE;
-}
-
-// ── Payload Stripping (onPayload callback) ───────────────────────────
-
-/** Fields the Cocoon proxy rejects as "unknown option". */
-const UNSUPPORTED_FIELDS = ["tools", "tool_choice", "store", "reasoning_effort", "stream_options"];
-
-/**
- * pi-ai `onPayload` callback — mutates the request body to remove
- * fields that the Cocoon proxy doesn't understand, and adds
- * Qwen3-recommended parameters.
- */
-export function stripCocoonPayload(payload: unknown): void {
- if (typeof payload !== "object" || payload === null) return;
- const obj = payload as Record;
- for (const field of UNSUPPORTED_FIELDS) {
- delete obj[field];
- }
- // Qwen3 recommended: reduce repetitions (e.g. "How can I assist you?")
- obj.presence_penalty = obj.presence_penalty ?? 1.5;
-}
-
-// ── Response Parsing ─────────────────────────────────────────────────
-
-const TOOL_CALL_OPEN = "";
-const TOOL_CALL_CLOSE = "";
-const THINK_RE = /[\s\S]*?<\/think>/g;
-
-export interface SyntheticToolCall {
- type: "toolCall";
- id: string;
- name: string;
- arguments: Record;
-}
-
-/**
- * Extract a complete JSON object from text starting at `startIndex`,
- * handling nested braces correctly.
- */
-function extractJsonObject(
- text: string,
- startIndex: number
-): { json: string; endIndex: number } | null {
- let braceCount = 0;
- let inString = false;
- let escaped = false;
-
- for (let i = startIndex; i < text.length; i++) {
- const ch = text[i];
- if (escaped) {
- escaped = false;
- continue;
- }
- if (ch === "\\" && inString) {
- escaped = true;
- continue;
- }
- if (ch === '"') {
- inString = !inString;
- continue;
- }
- if (!inString) {
- if (ch === "{") braceCount++;
- if (ch === "}") {
- braceCount--;
- if (braceCount === 0) {
- return { json: text.slice(startIndex, i + 1), endIndex: i };
- }
- }
- }
- }
- return null;
-}
-
-/**
- * Parse `` blocks from the model's text response.
- * Uses balanced brace parsing to handle nested JSON in arguments.
- * Returns an array of synthetic ToolCall objects compatible with pi-ai.
- */
-export function parseToolCallsFromText(
- text: string,
- allowedTools?: Set
-): SyntheticToolCall[] {
- // Strip blocks before parsing
- const cleaned = text.replace(THINK_RE, "").trim();
-
- const calls: SyntheticToolCall[] = [];
- let searchFrom = 0;
-
- while (true) {
- const openIdx = cleaned.indexOf(TOOL_CALL_OPEN, searchFrom);
- if (openIdx === -1) break;
-
- const contentStart = openIdx + TOOL_CALL_OPEN.length;
- const closeIdx = cleaned.indexOf(TOOL_CALL_CLOSE, contentStart);
- if (closeIdx === -1) break;
-
- // Find the first '{' inside the tag content
- const braceStart = cleaned.indexOf("{", contentStart);
- if (braceStart === -1 || braceStart >= closeIdx) {
- searchFrom = closeIdx + TOOL_CALL_CLOSE.length;
- continue;
- }
-
- // Extract balanced JSON object
- const extracted = extractJsonObject(cleaned, braceStart);
- if (extracted) {
- try {
- const parsed = JSON.parse(extracted.json);
- if (parsed.name && typeof parsed.name === "string") {
- if (allowedTools && !allowedTools.has(parsed.name)) {
- log.warn(`Cocoon: rejected tool call "${parsed.name}" — not in allowed set`);
- } else {
- calls.push({
- type: "toolCall",
- id: `cocoon_${randomUUID()}`,
- name: parsed.name,
- arguments: parsed.arguments ?? {},
- });
- }
- }
- } catch (e) {
- log.debug(`Failed to parse tool call JSON: ${String(e)}`);
- log.debug(`Raw: ${extracted.json.slice(0, 200)}`);
- }
- }
-
- searchFrom = closeIdx + TOOL_CALL_CLOSE.length;
- }
-
- return calls;
-}
-
-/**
- * Extract the plain text content from a response, stripping
- * and blocks.
- */
-export function extractPlainText(text: string): string {
- let result = text.replace(THINK_RE, "");
-
- // Remove all ... blocks
- let searchFrom = 0;
- while (true) {
- const openIdx = result.indexOf(TOOL_CALL_OPEN, searchFrom);
- if (openIdx === -1) break;
- const closeIdx = result.indexOf(TOOL_CALL_CLOSE, openIdx);
- if (closeIdx === -1) break;
- result = result.slice(0, openIdx) + result.slice(closeIdx + TOOL_CALL_CLOSE.length);
- searchFrom = openIdx;
- }
-
- return result.trim();
-}
-
-// ── Tool Result Formatting ───────────────────────────────────────────
-
-/**
- * Wrap a tool result string in `` tags for Qwen3.
- * Uses CDATA to prevent XML injection from result content.
- */
-export function wrapToolResult(resultText: string): string {
- const safe = resultText.replace(/]]>/g, "]]]]>");
- return `\n\n`;
-}
-
-/**
- * Build the message that carries a tool result back to the model. Cocoon has no
- * native tool-result role, so its results are wrapped as a plain user message;
- * every other provider uses the standard toolResult message. Centralising the
- * branch here keeps the cocoon-specific shape out of the agentic loop.
- */
-export function buildToolResultMessage(
- provider: string,
- block: { id: string; name: string },
- resultText: string,
- isError: boolean
-): UserMessage | ToolResultMessage {
- if (provider === "cocoon") {
- return {
- role: "user",
- content: [{ type: "text", text: wrapToolResult(resultText) }],
- timestamp: Date.now(),
- };
- }
- return {
- role: "toolResult",
- toolCallId: block.id,
- toolName: block.name,
- content: [{ type: "text", text: resultText }],
- isError,
- timestamp: Date.now(),
- };
-}
diff --git a/src/config/__tests__/configurable-keys.test.ts b/src/config/__tests__/configurable-keys.test.ts
index 1d5081e8..2b7be541 100644
--- a/src/config/__tests__/configurable-keys.test.ts
+++ b/src/config/__tests__/configurable-keys.test.ts
@@ -150,8 +150,8 @@ describe("CONFIGURABLE_KEYS — new scalar entries", () => {
});
});
- describe("cocoon.port", () => {
- const meta = CONFIGURABLE_KEYS["cocoon.port"];
+ describe("gocoon.port", () => {
+ const meta = CONFIGURABLE_KEYS["gocoon.port"];
it("accepts 1-65535", () => {
expect(meta.validate("10000")).toBeUndefined();
diff --git a/src/config/__tests__/loader.test.ts b/src/config/__tests__/loader.test.ts
index 106d6c80..4d61de59 100644
--- a/src/config/__tests__/loader.test.ts
+++ b/src/config/__tests__/loader.test.ts
@@ -151,6 +151,19 @@ market:
deprecated_field: "should be ignored"
`;
+// Config with the removed 'cocoon' provider (replaced by gocoon in 0.9.0)
+const LEGACY_COCOON = `
+agent:
+ api_key: sk-ant-test
+ provider: cocoon
+telegram:
+ api_id: 12345
+ api_hash: abcdef
+ phone: "+1234567890"
+cocoon:
+ port: 9999
+`;
+
// Config for non-anthropic provider (should auto-set model)
const OPENAI_CONFIG = `
agent:
@@ -563,6 +576,15 @@ storage:
expect((config as any).market).toBeUndefined();
});
+ it("should migrate the removed 'cocoon' provider to 'gocoon' and carry its port", () => {
+ writeTestConfig(LEGACY_COCOON);
+
+ const config = loadConfig(TEST_CONFIG_PATH);
+
+ expect(config.agent.provider).toBe("gocoon");
+ expect(config.gocoon?.port).toBe(9999);
+ });
+
it("should accept config with extra unknown fields", () => {
const configWithExtra = `
agent:
diff --git a/src/config/configurable-keys.ts b/src/config/configurable-keys.ts
index 4c4612c9..6d77ffb7 100644
--- a/src/config/configurable-keys.ts
+++ b/src/config/configurable-keys.ts
@@ -205,11 +205,11 @@ export const CONFIGURABLE_KEYS: Record = {
mask: identity,
parse: identity,
},
- "cocoon.port": {
+ "gocoon.port": {
type: "number",
category: "Agent",
- label: "Cocoon Port",
- description: "Cocoon proxy port (requires restart)",
+ label: "Gocoon Port",
+ description: "gocoon-runner port (requires restart)",
sensitive: false,
hotReload: "restart",
validate: numberInRange(1, 65535),
@@ -677,21 +677,6 @@ export const CONFIGURABLE_KEYS: Record = {
},
};
-// ── Category order for frontend grouping ───────────────────────────────
-
-export const CATEGORY_ORDER: ConfigCategory[] = [
- "API Keys",
- "Agent",
- "Session",
- "Telegram",
- "Embedding",
- "WebUI",
- "Deals",
- "TON Proxy",
- "Coding Agent",
- "Developer",
-];
-
// ── Dot-notation helpers ───────────────────────────────────────────────
const FORBIDDEN_SEGMENTS = new Set(["__proto__", "constructor", "prototype"]);
diff --git a/src/config/loader.ts b/src/config/loader.ts
index f80800a6..c5fb7811 100644
--- a/src/config/loader.ts
+++ b/src/config/loader.ts
@@ -59,6 +59,27 @@ export function loadConfig(configPath: string = DEFAULT_CONFIG_PATH): Config {
rawAgent.provider = "anthropic";
}
+ // Backward compatibility: the 'cocoon' provider (a proxy to an external
+ // cocoon-cli) was replaced by the native 'gocoon' provider in 0.9.0. Migrate
+ // so existing configs keep loading. gocoon runs its own channel, so the user
+ // must run `teleton gocoon init` and fund it before use.
+ if (rawAgent && rawAgent.provider === "cocoon") {
+ log.warn(
+ "Provider 'cocoon' was removed in 0.9.0; migrating to the native 'gocoon' provider. " +
+ "Run 'teleton gocoon init' and fund the channel before use."
+ );
+ rawAgent.provider = "gocoon";
+ // Carry a custom port from the old top-level cocoon block (the schema is
+ // non-strict and would otherwise drop it).
+ const rawObj = raw as Record;
+ const oldCocoon = rawObj.cocoon as { port?: number } | undefined;
+ if (oldCocoon?.port != null) {
+ const gocoon = (rawObj.gocoon as Record | undefined) ?? {};
+ if (gocoon.port == null) gocoon.port = oldCocoon.port;
+ rawObj.gocoon = gocoon;
+ }
+ }
+
const result = ConfigSchema.safeParse(raw);
if (!result.success) {
throw new Error(`Invalid config: ${result.error.message}`);
diff --git a/src/config/model-catalog.ts b/src/config/model-catalog.ts
index 09514238..66b5e0c7 100644
--- a/src/config/model-catalog.ts
+++ b/src/config/model-catalog.ts
@@ -262,6 +262,13 @@ export const MODEL_OPTIONS: Record = {
description: "Reasoning, 204K ctx, $0.30/M",
},
],
+ gocoon: [
+ {
+ value: "Qwen/Qwen3-32B",
+ name: "Qwen3-32B",
+ description: "Decentralized inference on TON",
+ },
+ ],
};
/** Get models for a provider (codex → openai-codex) */
diff --git a/src/config/providers.ts b/src/config/providers.ts
index d3a1d8fa..c494b824 100644
--- a/src/config/providers.ts
+++ b/src/config/providers.ts
@@ -12,7 +12,7 @@ export type SupportedProvider =
| "zai"
| "minimax"
| "huggingface"
- | "cocoon"
+ | "gocoon"
| "local";
export interface ProviderMetadata {
@@ -185,17 +185,17 @@ const PROVIDER_REGISTRY: Record = {
toolLimit: 128,
piAiProvider: "huggingface",
},
- cocoon: {
- id: "cocoon",
- displayName: "Cocoon Network (Decentralized)",
+ gocoon: {
+ id: "gocoon",
+ displayName: "Gocoon (Decentralized, TON)",
envVar: "",
keyPrefix: null,
- keyHint: "No API key needed — pays in TON",
- consoleUrl: "https://cocoon.network",
+ keyHint: "No API key, pays in TON",
+ consoleUrl: "https://github.com/TONresistor/gocoon",
defaultModel: "Qwen/Qwen3-32B",
utilityModel: "Qwen/Qwen3-32B",
toolLimit: 128,
- piAiProvider: "cocoon",
+ piAiProvider: "gocoon",
},
local: {
id: "local",
@@ -235,7 +235,7 @@ export const SUPPORTED_PROVIDER_IDS = Object.keys(PROVIDER_REGISTRY) as [
export function validateApiKeyFormat(provider: SupportedProvider, key: string): string | undefined {
const meta = PROVIDER_REGISTRY[provider];
if (!meta) return `Unknown provider: ${provider}`;
- if (provider === "cocoon" || provider === "local" || provider === "codex") return undefined;
+ if (provider === "gocoon" || provider === "local" || provider === "codex") return undefined;
if (!key || key.trim().length === 0) return "API key is required";
if (meta.keyPrefix && !key.startsWith(meta.keyPrefix)) {
return `Invalid format (should start with ${meta.keyPrefix})`;
diff --git a/src/config/schema.ts b/src/config/schema.ts
index 689bda22..9ee81129 100644
--- a/src/config/schema.ts
+++ b/src/config/schema.ts
@@ -262,7 +262,7 @@ export const McpConfigSchema = _McpObject.default(_McpObject.parse({}));
const _ToolSearchObject = z.object({
enabled: z
.boolean()
- .default(false)
+ .default(true)
.describe("Enable ToolSearch mode: core tools + meta-tool replaces RAG pre-selection"),
});
export const ToolSearchConfigSchema = _ToolSearchObject.default(_ToolSearchObject.parse({}));
@@ -346,7 +346,7 @@ export const ConfigSchema = z.object({
logging: LoggingConfigSchema,
dev: DevConfigSchema,
tool_rag: ToolRagConfigSchema,
- tool_search: ToolSearchConfigSchema.optional(),
+ tool_search: ToolSearchConfigSchema,
capabilities: CapabilitiesConfigSchema,
api: ApiConfigSchema.optional(),
ton_proxy: TonProxyConfigSchema,
@@ -356,17 +356,21 @@ export const ConfigSchema = z.object({
.record(z.string(), z.unknown())
.default({})
.describe("Per-plugin config (key = plugin name with underscores)"),
- cocoon: z
+ gocoon: z
.object({
port: z
.number()
.min(1)
.max(65535)
.default(10000)
- .describe("HTTP port of the cocoon-cli proxy"),
+ .describe("HTTP port of the gocoon-runner OpenAI-compatible API"),
+ auto_start: z
+ .boolean()
+ .optional()
+ .describe("Auto-install and supervise the gocoon-runner on start (default: true)"),
})
.optional()
- .describe("Cocoon Network — expects external cocoon-cli running on this port"),
+ .describe("Gocoon: pure-Go COCOON client (decentralized LLM on TON)"),
tonapi_key: z
.string()
.optional()
@@ -384,19 +388,11 @@ export const ConfigSchema = z.object({
export type Config = z.infer;
export type AgentConfig = z.infer;
export type TelegramConfig = z.infer;
-export type StorageConfig = z.infer;
export type SessionResetPolicy = z.infer;
export type DealsConfig = z.infer;
export type WebUIConfig = z.infer;
-export type EmbeddingConfig = z.infer;
-export type LoggingConfig = z.infer;
-export type DevConfig = z.infer;
export type McpConfig = z.infer;
-export type ToolRagConfig = z.infer;
-export type ToolSearchConfig = z.infer;
export type McpServerConfig = z.infer;
-export type CapabilitiesConfig = z.infer;
export type TonProxyConfig = z.infer;
export type ApiConfig = z.infer;
export type ExecConfig = z.infer;
-export type HeartbeatConfig = z.infer;
diff --git a/src/constants/limits.ts b/src/constants/limits.ts
index d72d8d30..26caab1f 100644
--- a/src/constants/limits.ts
+++ b/src/constants/limits.ts
@@ -79,7 +79,6 @@ export const WEB_SEARCH_MAX_RESULTS = 10; // max allowed count
export const TOOL_CONCURRENCY_LIMIT = 3;
// ─── Tool RAG ──────────────────────────────────────────────────
-export const TOOL_RAG_DEFAULT_TOP_K = 35;
export const TOOL_RAG_MIN_SCORE = 0.1;
export const TOOL_RAG_VECTOR_WEIGHT = 0.6;
export const TOOL_RAG_KEYWORD_WEIGHT = 0.4;
diff --git a/src/constants/timeouts.ts b/src/constants/timeouts.ts
index 7d718e63..b8984fb2 100644
--- a/src/constants/timeouts.ts
+++ b/src/constants/timeouts.ts
@@ -1,6 +1,5 @@
/** TTS generation timeout */
export const TTS_TIMEOUT_MS = 30_000;
-export const ONBOARDING_PROMPT_TIMEOUT_MS = 120_000;
export const BATCH_TRIGGER_DELAY_MS = 500;
export const DEFAULT_FETCH_TIMEOUT_MS = 15_000;
export const RETRY_DEFAULT_MAX_ATTEMPTS = 3;
diff --git a/src/deals/gift-detector.ts b/src/deals/gift-detector.ts
index 5c37c87a..3cb3a16b 100644
--- a/src/deals/gift-detector.ts
+++ b/src/deals/gift-detector.ts
@@ -79,18 +79,4 @@ export class GiftDetector {
return [];
}
}
-
- /**
- * Reset cache for a specific user (useful after trades)
- */
- resetCache(userId: number): void {
- this.seenGifts.delete(userId);
- }
-
- /**
- * Clear all caches
- */
- clearAll(): void {
- this.seenGifts.clear();
- }
}
diff --git a/src/deals/utils.ts b/src/deals/utils.ts
index 1ff7e69b..d08205eb 100644
--- a/src/deals/utils.ts
+++ b/src/deals/utils.ts
@@ -2,7 +2,6 @@
* Utility functions for deals system
*/
-import type { Deal } from "./types.js";
import { DEALS_CONFIG } from "./config.js";
/**
@@ -39,13 +38,6 @@ export function calculateExpiry(): number {
return Math.floor(Date.now() / 1000) + DEALS_CONFIG.expirySeconds;
}
-/**
- * Check if deal has expired
- */
-export function isDealExpired(deal: Deal): boolean {
- return deal.expires_at < Math.floor(Date.now() / 1000);
-}
-
/**
* Format deal proposal message with inline buttons
*/
diff --git a/src/events/bus.ts b/src/events/bus.ts
deleted file mode 100644
index db480d7a..00000000
--- a/src/events/bus.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { EventEmitter } from "events";
-
-export interface EventMap {
- "bridge:connected": { mode: "user" | "bot" };
- "bridge:disconnected": { mode: "user" | "bot" };
- "mode:changed": { from: "user" | "bot"; to: "user" | "bot" };
- "config:updated": { key: string; value: unknown };
- "tools:changed": { removed: string[]; added: string[] };
-}
-
-export class EventBus extends EventEmitter {
- on(event: K, handler: (payload: EventMap[K]) => void): this {
- return super.on(event, handler);
- }
-
- emit(event: K, payload: EventMap[K]): boolean {
- return super.emit(event, payload);
- }
-
- off(event: K, handler: (payload: EventMap[K]) => void): this {
- return super.off(event, handler);
- }
-}
-
-// Singleton instance
-export const eventBus = new EventBus();
diff --git a/src/gocoon/__tests__/sse-proxy.test.ts b/src/gocoon/__tests__/sse-proxy.test.ts
new file mode 100644
index 00000000..3ccae719
--- /dev/null
+++ b/src/gocoon/__tests__/sse-proxy.test.ts
@@ -0,0 +1,53 @@
+import { describe, it, expect } from "vitest";
+import { completionToSse } from "../sse-proxy.js";
+
+// Parse the `data: {...}` SSE lines back into objects for assertions.
+function parseEvents(events: string[]): any[] {
+ return events.map((e) => JSON.parse(e.replace(/^data: /, "").trim()));
+}
+
+describe("completionToSse", () => {
+ it("frames a chat completion into a delta chunk plus a usage chunk", () => {
+ const completion = JSON.stringify({
+ id: "c1",
+ created: 1,
+ model: "m",
+ choices: [{ index: 0, message: { role: "assistant", content: "hi" }, finish_reason: "stop" }],
+ usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 },
+ });
+ const evs = parseEvents(completionToSse(completion));
+ expect(evs).toHaveLength(2);
+ expect(evs[0].choices[0].delta).toEqual({ role: "assistant", content: "hi" });
+ expect(evs[0].choices[0].finish_reason).toBe("stop");
+ expect(evs[1].choices).toEqual([]);
+ expect(evs[1].usage.total_tokens).toBe(4);
+ });
+
+ it("emits only a delta chunk when usage is absent", () => {
+ const evs = parseEvents(
+ completionToSse(
+ JSON.stringify({ id: "c2", choices: [{ index: 0, message: { content: "x" } }] })
+ )
+ );
+ expect(evs).toHaveLength(1);
+ expect(evs[0].choices[0].delta.content).toBe("x");
+ });
+
+ it("surfaces a runner error envelope as an SSE error event, not an empty stream", () => {
+ // Regression: a 200 {error} envelope used to be framed into an empty,
+ // zero-token success stream, silently hiding the runner failure.
+ const evs = parseEvents(
+ completionToSse(JSON.stringify({ error: { message: "no workers available" } }))
+ );
+ expect(evs).toHaveLength(1);
+ expect(evs[0].error.message).toBe("no workers available");
+ expect(evs[0].choices).toBeUndefined();
+ });
+
+ it("turns a non-JSON upstream body into an error event carrying a snippet", () => {
+ const evs = parseEvents(completionToSse("upstream 502 bad gateway "));
+ expect(evs).toHaveLength(1);
+ expect(evs[0].error.message).toContain("non-JSON upstream body");
+ expect(evs[0].error.message).toContain("upstream 502");
+ });
+});
diff --git a/src/gocoon/cli.ts b/src/gocoon/cli.ts
new file mode 100644
index 00000000..54792039
--- /dev/null
+++ b/src/gocoon/cli.ts
@@ -0,0 +1,189 @@
+import { execFile, spawn } from "child_process";
+import { promisify } from "util";
+import { existsSync, mkdirSync } from "fs";
+import { createLogger } from "../utils/logger.js";
+import { fetchWithTimeout } from "../utils/fetch.js";
+import { ensureGocoonBinaries } from "./installer.js";
+import {
+ clientConfigPath,
+ gocoonDataDir,
+ runnerBaseUrl,
+ tonConfigPath,
+ walletPath,
+} from "./paths.js";
+
+const log = createLogger("gocoon");
+const execFileAsync = promisify(execFile);
+const MAX_BUFFER = 8 * 1024 * 1024;
+
+// Run a gocoon subcommand and capture stdout (for --json / short commands).
+export async function runGocoon(args: string[], timeoutMs = 60_000): Promise {
+ const { gocoon } = await ensureGocoonBinaries();
+ const { stdout } = await execFileAsync(gocoon, args, {
+ timeout: timeoutMs,
+ maxBuffer: MAX_BUFFER,
+ });
+ return stdout;
+}
+
+// Run a long-running gocoon subcommand, forwarding output lines. Resolves on exit 0.
+export async function streamGocoon(args: string[], onLine?: (line: string) => void): Promise {
+ const { gocoon } = await ensureGocoonBinaries();
+ await new Promise((resolve, reject) => {
+ const child = spawn(gocoon, args, { stdio: ["ignore", "pipe", "pipe"] });
+ const emit = (buf: Buffer): void => {
+ const line = buf.toString().trimEnd();
+ if (line) (onLine ?? ((l) => log.debug(l)))(line);
+ };
+ child.stdout?.on("data", emit);
+ child.stderr?.on("data", emit);
+ child.on("error", reject);
+ child.on("exit", (code) =>
+ code === 0
+ ? resolve()
+ : reject(new Error(`gocoon ${args[0]} exited with code ${code ?? "null"}`))
+ );
+ });
+}
+
+function parseJson(out: string): Record {
+ const start = out.indexOf("{");
+ const end = out.lastIndexOf("}");
+ if (start === -1 || end < start) {
+ throw new Error(`gocoon: expected JSON output, got: ${out.slice(0, 200)}`);
+ }
+ return JSON.parse(out.slice(start, end + 1)) as Record;
+}
+
+export interface InitSummary {
+ fundAddress: string;
+ ownerAddress: string;
+ recommendedFundingTon: string;
+ recommendedFundingNano: string;
+ walletPath: string;
+ configPath: string;
+}
+
+export function walletExists(): boolean {
+ return existsSync(walletPath()) && existsSync(clientConfigPath());
+}
+
+// Create the COCOON wallet, or reuse the existing one (stable funding address).
+export async function gocoonInit(): Promise {
+ if (walletExists()) {
+ const info = await walletInfo();
+ return {
+ fundAddress: info.fundAddress,
+ ownerAddress: info.ownerAddress,
+ recommendedFundingTon: info.recommendedFundingTon,
+ recommendedFundingNano: "",
+ walletPath: walletPath(),
+ configPath: clientConfigPath(),
+ };
+ }
+ mkdirSync(gocoonDataDir(), { recursive: true });
+ const j = parseJson(
+ await runGocoon(["init", "--dir", gocoonDataDir(), "--json", "--force"], 120_000)
+ );
+ return {
+ fundAddress: String(j.fund_address ?? ""),
+ ownerAddress: String(j.owner_address ?? ""),
+ recommendedFundingTon: String(j.recommended_funding_ton ?? "20"),
+ recommendedFundingNano: String(j.recommended_funding_nano ?? "20000000000"),
+ walletPath: String(j.wallet_path ?? walletPath()),
+ configPath: String(j.config_path ?? clientConfigPath()),
+ };
+}
+
+export interface WalletInfo {
+ ownerAddress: string;
+ fundAddress: string;
+ balanceNano: bigint;
+ balanceTon: string;
+ funded: boolean;
+ recommendedFundingTon: string;
+}
+
+export async function walletInfo(): Promise {
+ const j = parseJson(
+ await runGocoon([
+ "wallet",
+ "info",
+ "--wallet",
+ walletPath(),
+ "--config",
+ clientConfigPath(),
+ "--json",
+ ])
+ );
+ return {
+ ownerAddress: String(j.owner_address ?? ""),
+ fundAddress: String(j.fund_address ?? ""),
+ balanceNano: BigInt(String(j.balance_nano ?? "0")),
+ balanceTon: String(j.balance_ton ?? "0"),
+ funded: Boolean(j.funded),
+ recommendedFundingTon: String(j.recommended_funding_ton ?? "20"),
+ };
+}
+
+export interface ChannelInfo {
+ stateName: string;
+ stakeNano: bigint;
+ balanceNano: bigint;
+}
+
+// Read a channel's on-chain state directly (no runner needed). The contract
+// account stays "active" after a cooperative close (it keeps a little storage
+// TON), so callers must check stateName, not the account status, to tell a
+// closed channel from a live one. Returns null if it can't be read.
+export async function channelInfoOnChain(clientSC: string): Promise {
+ try {
+ const j = parseJson(
+ await runGocoon([
+ "channel",
+ "info",
+ "--client-sc",
+ clientSC,
+ "--config",
+ clientConfigPath(),
+ "--ton-config",
+ tonConfigPath(),
+ "--json",
+ ])
+ );
+ return {
+ stateName: String(j.state_name ?? ""),
+ stakeNano: BigInt(String(j.stake_nano ?? "0")),
+ balanceNano: BigInt(String(j.balance_nano ?? "0")),
+ };
+ } catch {
+ return null;
+ }
+}
+
+export async function waitFunded(onLine?: (line: string) => void): Promise {
+ await streamGocoon(
+ ["wallet", "wait-funded", "--wallet", walletPath(), "--config", clientConfigPath()],
+ onLine
+ );
+}
+
+// Read the active proxy's client_sc from the running runner. Needed by topup/close.
+export async function fetchClientSC(port: number): Promise {
+ const res = await fetchWithTimeout(`${runnerBaseUrl(port)}/jsonstats`, { timeoutMs: 3_000 });
+ if (!res.ok) {
+ throw new Error(`runner /jsonstats returned HTTP ${res.status} (is the gocoon runner active?)`);
+ }
+ const j = (await res.json()) as { proxies?: { sc_address?: string }[] };
+ const addr = j.proxies?.[0]?.sc_address?.trim();
+ if (!addr) throw new Error("runner has no proxy/client_sc yet; wait for discovery and retry");
+ return addr;
+}
+
+// Decimal TON string to integer nanoTON (max 9 decimals).
+export function tonToNano(ton: string): string {
+ const s = ton.trim();
+ if (!/^\d+(\.\d{1,9})?$/.test(s)) throw new Error(`invalid TON amount: "${ton}"`);
+ const [int, frac = ""] = s.split(".");
+ return (BigInt(int) * 1_000_000_000n + BigInt(frac.padEnd(9, "0"))).toString();
+}
diff --git a/src/gocoon/index.ts b/src/gocoon/index.ts
new file mode 100644
index 00000000..0a241c10
--- /dev/null
+++ b/src/gocoon/index.ts
@@ -0,0 +1,30 @@
+export {
+ ensureGocoonBinaries,
+ isInstalled,
+ detectPlatform,
+ type GocoonBinaries,
+} from "./installer.js";
+export { GocoonSupervisor, type SupervisorOptions } from "./supervisor.js";
+export {
+ init,
+ waitFunded,
+ walletInfo,
+ topup,
+ withdrawAll,
+ resetWallet,
+ tonToNano,
+ type InitSummary,
+ type WalletInfo,
+ type ProgressSink,
+ type GocoonProgress,
+ type GocoonStage,
+ type GocoonStatus,
+} from "./lifecycle.js";
+export {
+ GOCOON_VERSION,
+ GOCOON_DEFAULT_PORT,
+ gocoonDataDir,
+ clientConfigPath,
+ runnerBaseUrl,
+} from "./paths.js";
+export { GocoonSseProxy, completionToSse, type GocoonSseProxyOptions } from "./sse-proxy.js";
diff --git a/src/gocoon/installer.ts b/src/gocoon/installer.ts
new file mode 100644
index 00000000..bdfd55cb
--- /dev/null
+++ b/src/gocoon/installer.ts
@@ -0,0 +1,159 @@
+import { createHash } from "crypto";
+import { execFileSync } from "child_process";
+import {
+ chmodSync,
+ existsSync,
+ mkdirSync,
+ readFileSync,
+ renameSync,
+ rmSync,
+ writeFileSync,
+} from "fs";
+import { join } from "path";
+import { createLogger } from "../utils/logger.js";
+import { fetchWithTimeout } from "../utils/fetch.js";
+import { GOCOON_VERSION, binDir, gocoonBin, runnerBin, versionSentinel } from "./paths.js";
+
+const log = createLogger("gocoon");
+
+const REPO = "TONresistor/gocoon";
+const DOWNLOAD_TIMEOUT_MS = 120_000;
+
+interface Platform {
+ os: "darwin" | "linux" | "windows";
+ arch: "amd64" | "arm64";
+}
+
+export function detectPlatform(): Platform {
+ const osMap: Record = {
+ darwin: "darwin",
+ linux: "linux",
+ win32: "windows",
+ };
+ const archMap: Record = { x64: "amd64", arm64: "arm64" };
+ const os = osMap[process.platform];
+ const arch = archMap[process.arch];
+ if (!os || !arch) {
+ throw new Error(`gocoon: unsupported platform ${process.platform}/${process.arch}`);
+ }
+ return { os, arch };
+}
+
+export interface GocoonBinaries {
+ gocoon: string;
+ runner: string;
+}
+
+function sha256File(p: string): string {
+ return createHash("sha256").update(readFileSync(p)).digest("hex");
+}
+
+// The sentinel is JSON { version, gocoon, runner } recording each binary's
+// sha256 at install time. Legacy installs wrote a plain version string; those
+// parse to null so they get re-verified (and re-downloaded) once.
+function readSentinel(): { version: string; gocoon: string; runner: string } | null {
+ try {
+ const j = JSON.parse(readFileSync(versionSentinel(), "utf-8")) as Record;
+ if (
+ typeof j.version === "string" &&
+ typeof j.gocoon === "string" &&
+ typeof j.runner === "string"
+ ) {
+ return { version: j.version, gocoon: j.gocoon, runner: j.runner };
+ }
+ } catch {
+ /* missing or legacy plain-string sentinel */
+ }
+ return null;
+}
+
+// Cheap presence check for status display: pinned version + both files exist.
+export function isInstalled(): boolean {
+ const s = readSentinel();
+ return !!s && s.version === GOCOON_VERSION && existsSync(gocoonBin()) && existsSync(runnerBin());
+}
+
+// Full integrity check: the on-disk binaries still match the sha256 recorded at
+// install. Catches an out-of-band swap/corruption that the version string alone
+// would miss, so we never launch an unverified runner against a live channel.
+function binariesVerified(): boolean {
+ const s = readSentinel();
+ if (!s || s.version !== GOCOON_VERSION) return false;
+ if (!existsSync(gocoonBin()) || !existsSync(runnerBin())) return false;
+ try {
+ return sha256File(gocoonBin()) === s.gocoon && sha256File(runnerBin()) === s.runner;
+ } catch {
+ return false;
+ }
+}
+
+// Download the pinned release, verify its SHA-256, extract both binaries into
+// ~/.teleton/bin. Idempotent via the version sentinel.
+export async function ensureGocoonBinaries(): Promise {
+ const out: GocoonBinaries = { gocoon: gocoonBin(), runner: runnerBin() };
+ if (binariesVerified()) return out;
+
+ const { os, arch } = detectPlatform();
+ const archive = `gocoon-${GOCOON_VERSION}-${os}-${arch}.tar.gz`;
+ const base = `https://github.com/${REPO}/releases/download/${GOCOON_VERSION}`;
+ log.info(`Installing gocoon ${GOCOON_VERSION} (${os}/${arch})`);
+
+ const [tar, shaLine] = await Promise.all([
+ fetchBuffer(`${base}/${archive}`),
+ fetchText(`${base}/${archive}.sha256`),
+ ]);
+
+ const expected = shaLine.trim().split(/\s+/)[0]?.toLowerCase();
+ const actual = createHash("sha256").update(tar).digest("hex");
+ if (!expected || expected !== actual) {
+ throw new Error(
+ `gocoon checksum mismatch for ${archive} (expected ${expected}, got ${actual})`
+ );
+ }
+
+ mkdirSync(binDir(), { recursive: true });
+ // Temp dir under binDir so the final rename stays on one filesystem (no EXDEV).
+ const tmp = join(binDir(), `.extract-${process.pid}`);
+ rmSync(tmp, { recursive: true, force: true });
+ mkdirSync(tmp, { recursive: true });
+ const tarPath = join(tmp, archive);
+ writeFileSync(tarPath, tar);
+ try {
+ execFileSync("tar", ["-xzf", tarPath, "-C", tmp], { stdio: "pipe" });
+ const ext = os === "windows" ? ".exe" : "";
+ for (const [name, dest] of [
+ ["gocoon", out.gocoon],
+ ["gocoon-runner", out.runner],
+ ] as const) {
+ const src = join(tmp, name + ext);
+ if (!existsSync(src)) throw new Error(`gocoon: archive ${archive} is missing ${name}${ext}`);
+ renameSync(src, dest);
+ if (os !== "windows") chmodSync(dest, 0o755);
+ }
+ } finally {
+ rmSync(tmp, { recursive: true, force: true });
+ }
+
+ writeFileSync(
+ versionSentinel(),
+ JSON.stringify({
+ version: GOCOON_VERSION,
+ gocoon: sha256File(out.gocoon),
+ runner: sha256File(out.runner),
+ })
+ );
+ log.info(`gocoon ${GOCOON_VERSION} installed`);
+ return out;
+}
+
+async function fetchBuffer(url: string): Promise {
+ const res = await fetchWithTimeout(url, { timeoutMs: DOWNLOAD_TIMEOUT_MS });
+ if (!res.ok) throw new Error(`gocoon: download failed ${url} (HTTP ${res.status})`);
+ return Buffer.from(await res.arrayBuffer());
+}
+
+async function fetchText(url: string): Promise {
+ const res = await fetchWithTimeout(url, { timeoutMs: DOWNLOAD_TIMEOUT_MS });
+ if (!res.ok) throw new Error(`gocoon: download failed ${url} (HTTP ${res.status})`);
+ return res.text();
+}
diff --git a/src/gocoon/lifecycle.ts b/src/gocoon/lifecycle.ts
new file mode 100644
index 00000000..285cefec
--- /dev/null
+++ b/src/gocoon/lifecycle.ts
@@ -0,0 +1,417 @@
+import { spawn, type ChildProcess } from "child_process";
+import { rmSync } from "fs";
+import { Address, fromNano, internal, SendMode } from "@ton/core";
+import { createLogger } from "../utils/logger.js";
+import { getErrorMessage } from "../utils/errors.js";
+import { tonapiFetch } from "../constants/api-endpoints.js";
+import { getCachedTonClient } from "../ton/wallet-service.js";
+import { openWallet } from "../ton/wallet-open.js";
+import { sendWalletTx } from "../ton/confirm.js";
+import { withTxLock } from "../ton/tx-lock.js";
+import { ensureGocoonBinaries } from "./installer.js";
+import {
+ channelInfoOnChain,
+ fetchClientSC,
+ gocoonInit,
+ streamGocoon,
+ tonToNano,
+ waitFunded,
+ walletInfo,
+ type InitSummary,
+ type WalletInfo,
+} from "./cli.js";
+import { killProcessGroup, waitReady } from "./supervisor.js";
+import {
+ GOCOON_DEFAULT_PORT,
+ clientConfigPath,
+ gocoonDataDir,
+ runnerBaseUrl,
+ runnerBin,
+ walletPath,
+} from "./paths.js";
+
+const log = createLogger("gocoon");
+
+export { gocoonInit as init, waitFunded, walletInfo, tonToNano };
+export type { InitSummary, WalletInfo };
+
+export type GocoonStage =
+ | "resolve"
+ | "find_channel"
+ | "spawn_runner"
+ | "close_channel"
+ | "wait_refund"
+ | "withdraw_cocoon"
+ | "withdraw_agent"
+ | "complete";
+
+export type GocoonStatus = "started" | "ok" | "info" | "warn" | "skipped" | "error";
+
+export interface GocoonProgress {
+ stage: GocoonStage;
+ status: GocoonStatus;
+ message: string;
+ at: number;
+}
+
+export type ProgressSink = (e: GocoonProgress) => void;
+
+const REFUND_TIMEOUT_MS = 180_000;
+const REFUND_MIN_DELTA_NANO = 5_000_000_000n;
+const MIN_COCOON_WITHDRAW_NANO = 50_000_000n;
+const AGENT_FEE_BUFFER_NANO = 10_000_000n;
+// Above this leftover balance, reset refuses: we never delete the mnemonic of a
+// wallet that still controls funds.
+const RESET_MAX_DUST_NANO = 100_000_000n;
+const COCOON_CLIENT_OPS = new Set([
+ "CocoonOwnerClientRegister",
+ "CocoonExtClientTopUp",
+ "CocoonOwnerRequestRefund",
+ "CocoonOwnerWithdraw",
+]);
+
+// Thrown by findClientSC ONLY when the wallet has no cocoon_client interaction
+// on chain (no channel was ever staked). Transient/HTTP failures throw plain
+// Errors instead, so callers can abort rather than mistake a lookup failure for
+// "no channel" and drain the liquid wallet while leaving the stake locked.
+class ChannelNotFoundError extends Error {}
+
+// Add TON to the open channel. The runner must be active (reads client_sc from /jsonstats).
+export async function topup(
+ amountTon: string,
+ port: number = GOCOON_DEFAULT_PORT,
+ onLine?: (line: string) => void
+): Promise {
+ const nano = tonToNano(amountTon);
+ const clientSC = await fetchClientSC(port);
+ log.info(`Topping up channel with ${amountTon} TON (${nano} nanoTON)`);
+ await streamGocoon(["channel", "topup", "--amount", nano, "--client-sc", clientSC], onLine);
+}
+
+// Close the channel, wait for the refund, then withdraw the COCOON and agent
+// wallets to destination. The runner must not be running.
+export async function withdrawAll(
+ destination: string,
+ sink: ProgressSink = () => {},
+ port: number = GOCOON_DEFAULT_PORT
+): Promise {
+ const emit = (stage: GocoonStage, status: GocoonStatus, message: string): void =>
+ sink({ stage, status, message, at: Date.now() });
+
+ emit("resolve", "started", "Resolving destination");
+ const dest = await resolveDest(destination);
+ emit("resolve", "ok", `Destination: ${dest.label}`);
+
+ if (await isRunnerUp(port)) {
+ throw new Error("the gocoon runner is active; stop teleton first, then retry the withdraw");
+ }
+
+ const cocoon = await walletInfo();
+ // COCOON channel ops are executed by the fund (node) wallet, not the owner
+ // wallet — scan fund_address (the prior owner-address scan silently skipped
+ // the close and left the stake locked).
+ emit("find_channel", "started", `Looking for a channel via ${shortAddr(cocoon.fundAddress)}`);
+ let clientSC: string | null = null;
+ try {
+ clientSC = await findClientSC(cocoon.fundAddress);
+ } catch (err) {
+ if (err instanceof ChannelNotFoundError) {
+ emit(
+ "find_channel",
+ "skipped",
+ "No channel was ever staked; withdrawing wallet balance only"
+ );
+ } else {
+ // A transient lookup failure (tonapi rate-limit/5xx, network) must NOT be
+ // treated as "no channel" — that would drain the liquid wallet and leave
+ // the staked TON locked while reporting success. Abort so the user retries.
+ emit("find_channel", "error", `Channel lookup failed: ${getErrorMessage(err)}`);
+ throw new Error(
+ `could not check for an open channel (${getErrorMessage(err)}); aborting so the staked TON is not left locked. Retry the withdraw.`
+ );
+ }
+ }
+
+ if (clientSC) {
+ // Decide liveness from the channel STATE, not the account status: a
+ // cooperatively-closed cocoon_client account stays "active" (it holds
+ // storage TON). Re-closing a closed channel would hang the refund wait.
+ const ch = await channelInfoOnChain(clientSC);
+ if (!ch) {
+ emit("find_channel", "error", "Could not read the channel state");
+ throw new Error(
+ "located a channel but could not read its state; aborting so the staked TON is not left locked. Retry the withdraw."
+ );
+ }
+ const live = ch.stateName !== "closed" && ch.stakeNano > 0n;
+ if (live) {
+ emit("find_channel", "ok", "Channel located (open) — closing");
+ emit("spawn_runner", "started", "Starting gocoon-runner (transient)");
+ const runner = await spawnTransientRunner();
+ try {
+ await waitReady(`${runnerBaseUrl(port)}/jsonstats`, 30_000);
+ emit("spawn_runner", "ok", "Runner ready");
+
+ emit("close_channel", "started", "Closing channel");
+ await streamGocoon([
+ "channel",
+ "close",
+ "--client-sc",
+ clientSC,
+ "--url",
+ runnerBaseUrl(port),
+ ]);
+ emit("close_channel", "ok", "Channel close transaction sent");
+
+ emit(
+ "wait_refund",
+ "started",
+ "Waiting for the staked TON to return (cooperative close, up to 3 min)"
+ );
+ const landed = await waitForRefund(REFUND_TIMEOUT_MS);
+ if (!landed) {
+ // The close IS on chain; the stake returns once the node co-signs or
+ // via the ~12h unilateral delay. Leave the wallets untouched so a
+ // later re-run sweeps everything cleanly (the re-run is idempotent).
+ emit(
+ "wait_refund",
+ "warn",
+ "Refund not received within 3 min. The channel close is on chain; the stake returns once the node co-signs or via the ~12h fallback. Re-run the withdraw later to finish."
+ );
+ emit(
+ "complete",
+ "warn",
+ "Channel closed; refund pending. Re-run the withdraw later to sweep the funds (your TON is safe)."
+ );
+ return;
+ }
+ emit("wait_refund", "ok", "Refund landed on chain");
+ } finally {
+ if (runner.pid != null) killProcessGroup(runner.pid);
+ }
+ } else {
+ emit("find_channel", "ok", "Channel already closed — withdrawing the remaining balance");
+ }
+ }
+
+ const balance = (await walletInfo()).balanceNano;
+ if (balance > MIN_COCOON_WITHDRAW_NANO) {
+ emit("withdraw_cocoon", "started", `Withdrawing COCOON wallet to ${dest.label}`);
+ await streamGocoon([
+ "wallet",
+ "withdraw",
+ "--wallet",
+ walletPath(),
+ "--config",
+ clientConfigPath(),
+ "--to",
+ dest.address.toString({ bounceable: false }),
+ "--timeout",
+ "10m",
+ ]);
+ emit("withdraw_cocoon", "ok", "COCOON wallet withdrawn");
+ } else {
+ emit("withdraw_cocoon", "skipped", `COCOON wallet too low (${balance} nanoTON), skipping`);
+ }
+
+ emit("withdraw_agent", "started", `Withdrawing agent wallet to ${dest.label}`);
+ await withdrawAgentWallet(dest.address, emit);
+
+ emit("complete", "ok", `All funds sent to ${dest.label}`);
+}
+
+// Delete the local COCOON wallet + config so the next init generates a fresh
+// owner/node wallet (gocoonInit reuses wallet.json when present). Binaries live
+// in binDir() and are kept. Run after a full withdraw. Guard: refuses while the
+// runner is up, or (without force) while the wallet still holds funds or an
+// active channel exists, so we never destroy keys that still control value.
+export async function resetWallet(
+ opts: { force?: boolean } = {},
+ port: number = GOCOON_DEFAULT_PORT
+): Promise {
+ if (await isRunnerUp(port)) {
+ throw new Error("the gocoon runner is active; stop it first, then retry the reset");
+ }
+ if (!opts.force) {
+ let info: WalletInfo | null = null;
+ try {
+ info = await walletInfo();
+ } catch {
+ info = null; // no wallet on disk; nothing to guard
+ }
+ if (info) {
+ if (info.balanceNano > RESET_MAX_DUST_NANO) {
+ throw new Error(
+ `fund wallet still holds ${info.balanceTon} TON; withdraw first (or reset with force)`
+ );
+ }
+ if (info.fundAddress) {
+ let liveChannel = false;
+ try {
+ const clientSC = await findClientSC(info.fundAddress);
+ const ch = await channelInfoOnChain(clientSC);
+ // findClientSC also returns a closed channel's contract (its account
+ // stays active to hold storage TON), so check the channel state, not
+ // the account status: only a not-closed channel that still holds
+ // stake controls recoverable value and must block the reset.
+ liveChannel = !!ch && ch.stateName !== "closed" && ch.stakeNano > 0n;
+ } catch (err) {
+ if (!(err instanceof ChannelNotFoundError)) {
+ // Could not verify there is no live channel (transient lookup
+ // failure). Refuse rather than risk deleting keys to a live stake.
+ throw new Error(
+ `could not verify the channel state (${getErrorMessage(err)}); refusing to reset. Retry, or reset with force if you are sure.`
+ );
+ }
+ liveChannel = false; // ChannelNotFoundError: no channel was ever staked
+ }
+ if (liveChannel) {
+ throw new Error("a funded channel still exists; withdraw first (or reset with force)");
+ }
+ }
+ }
+ }
+ rmSync(gocoonDataDir(), { recursive: true, force: true });
+}
+
+interface ResolvedDest {
+ address: Address;
+ label: string;
+}
+
+// Accept a TON address or a .ton domain.
+async function resolveDest(raw: string): Promise {
+ const s = raw.trim();
+ if (!s) throw new Error("empty destination");
+ if (s.toLowerCase().endsWith(".ton")) {
+ const res = await tonapiFetch(`/dns/${encodeURIComponent(s.toLowerCase())}/resolve`);
+ if (!res.ok) throw new Error(`tonapi dns resolve ${s} returned HTTP ${res.status}`);
+ const j = (await res.json()) as { wallet?: { address?: string } };
+ const addr = j.wallet?.address;
+ if (!addr) throw new Error(`${s} has no wallet record`);
+ return { address: Address.parse(addr), label: `${s} (${shortAddr(addr)})` };
+ }
+ try {
+ return { address: Address.parse(s), label: s };
+ } catch {
+ throw new Error(`not a TON address or .ton domain: "${raw}"`);
+ }
+}
+
+async function isRunnerUp(port: number): Promise {
+ try {
+ await fetch(`${runnerBaseUrl(port)}/jsonstats`, { signal: AbortSignal.timeout(800) });
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+// Find the client_sc this fund wallet interacted with on chain. Returns the
+// client_sc address; throws ChannelNotFoundError if the wallet never opened a
+// channel, and a plain Error for transient/HTTP failures so callers can abort
+// rather than mistake a lookup failure for "no channel". Open-vs-closed liveness
+// is decided by the caller via channelInfoOnChain.
+async function findClientSC(walletAddr: string): Promise {
+ const res = await tonapiFetch(`/accounts/${encodeURIComponent(walletAddr)}/events?limit=50`);
+ if (!res.ok) throw new Error(`tonapi events returned HTTP ${res.status}`);
+ const j = (await res.json()) as {
+ events?: {
+ actions?: {
+ type?: string;
+ SmartContractExec?: {
+ executor?: { address?: string };
+ contract?: { address?: string };
+ operation?: string;
+ };
+ }[];
+ }[];
+ };
+
+ const me = normalizeRaw(walletAddr);
+ let candidate: string | undefined;
+ for (const event of j.events ?? []) {
+ for (const action of event.actions ?? []) {
+ const exec = action.SmartContractExec;
+ if (action.type !== "SmartContractExec" || !exec) continue;
+ if (normalizeRaw(exec.executor?.address ?? "") !== me) continue;
+ if (!COCOON_CLIENT_OPS.has(exec.operation ?? "")) continue;
+ candidate = exec.contract?.address;
+ break;
+ }
+ if (candidate) break;
+ }
+ if (!candidate) {
+ throw new ChannelNotFoundError(
+ "no cocoon_client interaction found; channel may never have been staked"
+ );
+ }
+
+ return Address.parse(candidate).toString({ bounceable: false });
+}
+
+// Poll the COCOON wallet balance until it grows by at least 5 TON (the refund).
+// Returns true once the refund lands, false if it has not within timeoutMs (the
+// caller handles the slow/unilateral path without hard-failing the withdraw).
+async function waitForRefund(timeoutMs: number): Promise {
+ const baseline = (await walletInfo()).balanceNano;
+ const deadline = Date.now() + timeoutMs;
+ while (Date.now() < deadline) {
+ await sleep(5_000);
+ try {
+ const now = (await walletInfo()).balanceNano;
+ if (now > baseline && now - baseline >= REFUND_MIN_DELTA_NANO) return true;
+ } catch {
+ /* transient RPC error */
+ }
+ }
+ return false;
+}
+
+// Send (balance - 0.01 TON) from teleton's agent wallet to dest. Skips if empty.
+async function withdrawAgentWallet(
+ dest: Address,
+ emit: (stage: GocoonStage, status: GocoonStatus, message: string) => void
+): Promise {
+ const client = await getCachedTonClient();
+ const opened = await openWallet(client);
+ if (!opened) {
+ emit("withdraw_agent", "skipped", "No agent wallet on disk, skipping");
+ return;
+ }
+ const balance = await client.getBalance(opened.contract.address);
+ const send = balance - AGENT_FEE_BUFFER_NANO;
+ if (send <= 0n) {
+ emit("withdraw_agent", "skipped", `Agent wallet too low (${balance} nanoTON), skipping`);
+ return;
+ }
+ const sent = await withTxLock(() =>
+ sendWalletTx(client, opened.contract, {
+ secretKey: opened.keyPair.secretKey,
+ messages: [internal({ to: dest, value: send, bounce: false, body: "withdraw" })],
+ sendMode: SendMode.PAY_GAS_SEPARATELY,
+ })
+ );
+ if (!sent) throw new Error("agent wallet transfer was not confirmed on-chain");
+ emit("withdraw_agent", "ok", `Agent wallet withdrawn (${fromNano(send)} TON sent)`);
+}
+
+async function spawnTransientRunner(): Promise {
+ await ensureGocoonBinaries();
+ return spawn(runnerBin(), ["--config", clientConfigPath()], { detached: true, stdio: "ignore" });
+}
+
+function normalizeRaw(s: string): string {
+ try {
+ const a = Address.parse(s);
+ return `${a.workChain}:${a.hash.toString("hex")}`.toLowerCase();
+ } catch {
+ return s.toLowerCase();
+ }
+}
+
+function shortAddr(s: string): string {
+ return s.length > 18 ? `${s.slice(0, 8)}...${s.slice(-6)}` : s;
+}
+
+const sleep = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms));
diff --git a/src/gocoon/paths.ts b/src/gocoon/paths.ts
new file mode 100644
index 00000000..69047b68
--- /dev/null
+++ b/src/gocoon/paths.ts
@@ -0,0 +1,21 @@
+import { join } from "path";
+import { TELETON_ROOT } from "../workspace/paths.js";
+
+// Pinned release. The installer verifies the SHA-256 of this exact tag.
+export const GOCOON_VERSION = "v0.2.0";
+export const GOCOON_DEFAULT_PORT = 10000;
+
+const exe = (name: string): string => (process.platform === "win32" ? `${name}.exe` : name);
+
+export const binDir = (): string => join(TELETON_ROOT, "bin");
+export const gocoonBin = (): string => join(binDir(), exe("gocoon"));
+export const runnerBin = (): string => join(binDir(), exe("gocoon-runner"));
+export const versionSentinel = (): string => join(binDir(), ".gocoon-version");
+
+export const gocoonDataDir = (): string => join(TELETON_ROOT, "gocoon");
+export const walletPath = (): string => join(gocoonDataDir(), "wallet.json");
+export const clientConfigPath = (): string => join(gocoonDataDir(), "client-config.json");
+export const tonConfigPath = (): string => join(gocoonDataDir(), "ton-config.json");
+
+export const runnerBaseUrl = (port: number = GOCOON_DEFAULT_PORT): string =>
+ `http://127.0.0.1:${port}`;
diff --git a/src/gocoon/sse-proxy.ts b/src/gocoon/sse-proxy.ts
new file mode 100644
index 00000000..b1af20c9
--- /dev/null
+++ b/src/gocoon/sse-proxy.ts
@@ -0,0 +1,155 @@
+import http from "node:http";
+import { createLogger } from "../utils/logger.js";
+
+const log = createLogger("gocoon-sse");
+
+export interface GocoonSseProxyOptions {
+ runnerPort: number;
+ host?: string;
+}
+
+// GocoonSseProxy sits in front of the gocoon runner. pi-ai always sends
+// stream:true and only parses Server-Sent Events, but the runner returns a
+// single JSON document. This proxy forwards requests to the runner and, when
+// the client asked for streaming, frames the runner's JSON reply as SSE so
+// pi-ai parses it. The gocoon runner itself stays untouched.
+export class GocoonSseProxy {
+ private server: http.Server | null = null;
+ private readonly runnerPort: number;
+ private readonly host: string;
+ port = 0;
+
+ constructor(opts: GocoonSseProxyOptions) {
+ this.runnerPort = opts.runnerPort;
+ this.host = opts.host ?? "127.0.0.1";
+ }
+
+ async start(): Promise {
+ const server = http.createServer((req, res) => {
+ void this.handle(req, res);
+ });
+ this.server = server;
+ await new Promise((resolve, reject) => {
+ server.once("error", reject);
+ server.listen(0, this.host, () => {
+ const addr = server.address();
+ this.port = typeof addr === "object" && addr ? addr.port : 0;
+ resolve();
+ });
+ });
+ log.info(`SSE proxy on ${this.host}:${this.port} -> runner ${this.runnerPort}`);
+ }
+
+ stop(): void {
+ this.server?.close();
+ this.server = null;
+ }
+
+ private async handle(req: http.IncomingMessage, res: http.ServerResponse): Promise {
+ const chunks: Buffer[] = [];
+ for await (const c of req) chunks.push(c as Buffer);
+ const body = Buffer.concat(chunks);
+ const wantsStream =
+ req.method === "POST" &&
+ /\/v1\/chat\/completions$/.test(req.url ?? "") &&
+ requestWantsStream(body);
+
+ let upstream: Response;
+ try {
+ upstream = await fetch(`http://127.0.0.1:${this.runnerPort}${req.url ?? ""}`, {
+ method: req.method,
+ headers: { "content-type": req.headers["content-type"] ?? "application/json" },
+ body: req.method === "GET" || req.method === "HEAD" ? undefined : body,
+ });
+ } catch (err) {
+ res.writeHead(502, { "content-type": "application/json" });
+ res.end(JSON.stringify({ error: { message: `gocoon sse-proxy: ${String(err)}` } }));
+ return;
+ }
+
+ const ctype = upstream.headers.get("content-type") ?? "";
+ // Frame a single JSON completion as SSE only when the client wanted streaming.
+ if (wantsStream && ctype.includes("application/json")) {
+ const text = await upstream.text();
+ // A runner error (non-2xx) must surface as a real error, not be reframed
+ // into an empty 200 SSE stream — which the OpenAI SDK parses as a
+ // successful zero-token reply, hiding "no workers / channel out of balance
+ // / model not served" and letting the agent retry silently.
+ if (!upstream.ok) {
+ res.writeHead(upstream.status, { "content-type": "application/json" });
+ res.end(
+ text ||
+ JSON.stringify({ error: { message: `gocoon runner returned HTTP ${upstream.status}` } })
+ );
+ return;
+ }
+ res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache" });
+ for (const ev of completionToSse(text)) res.write(ev);
+ res.write("data: [DONE]\n\n");
+ res.end();
+ return;
+ }
+ // Pass everything else through unchanged (GET /v1/models, already-SSE, non-stream).
+ const buf = Buffer.from(await upstream.arrayBuffer());
+ res.writeHead(upstream.status, { "content-type": ctype || "application/json" });
+ res.end(buf);
+ }
+}
+
+function requestWantsStream(body: Buffer): boolean {
+ try {
+ return (JSON.parse(body.toString("utf8")) as { stream?: boolean }).stream === true;
+ } catch {
+ return false;
+ }
+}
+
+// completionToSse converts an OpenAI chat.completion document into the SSE
+// "data:" events of a stream: one chunk carrying the message as a delta, then
+// a final chunk carrying usage with no choices.
+export function completionToSse(completion: string): string[] {
+ let doc: Record;
+ try {
+ doc = JSON.parse(completion) as Record;
+ } catch {
+ // Non-JSON upstream body labelled application/json: emit a structured error
+ // so the SDK throws a clean APIError instead of an opaque SyntaxError.
+ const snippet = completion.slice(0, 200).replace(/\s+/g, " ").trim();
+ return [
+ `data: ${JSON.stringify({ error: { message: `gocoon sse-proxy: non-JSON upstream body: ${snippet}` } })}\n\n`,
+ ];
+ }
+ // A 200 error envelope must surface as a thrown error, not an empty stream.
+ if (doc.error) {
+ return [`data: ${JSON.stringify({ error: doc.error })}\n\n`];
+ }
+ const choices = Array.isArray(doc.choices) ? (doc.choices as Record[]) : [];
+ const base = () => ({
+ id: doc.id,
+ object: "chat.completion.chunk",
+ created: doc.created,
+ model: doc.model,
+ });
+
+ const deltaChoices = choices.map((choice, i) => {
+ const msg = (choice.message ?? {}) as Record;
+ const delta: Record = {};
+ if (msg.role !== undefined) delta.role = msg.role;
+ if (msg.content !== undefined) delta.content = msg.content;
+ if (Array.isArray(msg.tool_calls)) {
+ delta.tool_calls = (msg.tool_calls as Record[]).map((tc, idx) => ({
+ index: idx,
+ ...tc,
+ }));
+ }
+ const out: Record = { index: i, delta };
+ if (choice.finish_reason !== undefined) out.finish_reason = choice.finish_reason;
+ return out;
+ });
+
+ const events = [`data: ${JSON.stringify({ ...base(), choices: deltaChoices })}\n\n`];
+ if (doc.usage) {
+ events.push(`data: ${JSON.stringify({ ...base(), choices: [], usage: doc.usage })}\n\n`);
+ }
+ return events;
+}
diff --git a/src/gocoon/supervisor.ts b/src/gocoon/supervisor.ts
new file mode 100644
index 00000000..2477dff8
--- /dev/null
+++ b/src/gocoon/supervisor.ts
@@ -0,0 +1,175 @@
+import { execFileSync } from "child_process";
+import { type ChildProcess, spawn } from "child_process";
+import { createLogger } from "../utils/logger.js";
+import { getErrorMessage } from "../utils/errors.js";
+import { runnerBin } from "./paths.js";
+
+const log = createLogger("gocoon");
+
+export interface SupervisorOptions {
+ configPath: string;
+ healthUrl: string;
+ startGraceMs?: number;
+ verbosity?: number;
+ backoffCapMs?: number;
+ onLog?: (line: string) => void;
+}
+
+const READY_PROBE_TIMEOUT_MS = 2_000;
+const READY_POLL_INTERVAL_MS = 250;
+const KILL_GRACE_MS = 5_000;
+const MAX_RESTART_FAILURES = 5;
+
+// Spawns gocoon-runner, waits for the health URL, then auto-restarts on crash
+// with exponential backoff until stop() is called.
+export class GocoonSupervisor {
+ private child: ChildProcess | null = null;
+ private stopped = false;
+ private runningFlag = false;
+ private failures = 0;
+ private backoffMs = 1_000;
+
+ private readonly configPath: string;
+ private readonly healthUrl: string;
+ private readonly startGraceMs: number;
+ private readonly verbosity: number;
+ private readonly backoffCapMs: number;
+ private readonly onLog: (line: string) => void;
+
+ constructor(opts: SupervisorOptions) {
+ this.configPath = opts.configPath;
+ this.healthUrl = opts.healthUrl;
+ this.startGraceMs = opts.startGraceMs ?? 30_000;
+ this.verbosity = opts.verbosity ?? 0;
+ this.backoffCapMs = opts.backoffCapMs ?? 60_000;
+ this.onLog = opts.onLog ?? ((line) => log.debug(line));
+ }
+
+ async start(): Promise {
+ this.stopped = false;
+ this.failures = 0;
+ this.backoffMs = 1_000;
+ this.spawnChild();
+ try {
+ await waitReady(this.healthUrl, this.startGraceMs);
+ } catch (err) {
+ this.stopped = true; // prevent the exit handler from respawning
+ this.killChild();
+ throw new Error(`gocoon-runner did not become healthy: ${getErrorMessage(err)}`);
+ }
+ log.info(`gocoon-runner ready (pid ${this.child?.pid ?? "?"})`);
+ }
+
+ stop(): void {
+ if (this.stopped) return;
+ this.stopped = true;
+ this.killChild();
+ }
+
+ isRunning(): boolean {
+ return this.runningFlag;
+ }
+
+ private spawnChild(): void {
+ const args = ["--config", this.configPath];
+ if (this.verbosity > 0) args.push(`-v${this.verbosity}`);
+ const child = spawn(runnerBin(), args, { detached: true, stdio: ["ignore", "pipe", "pipe"] });
+ this.child = child;
+ this.runningFlag = true;
+ log.info(`gocoon-runner started (pid ${child.pid ?? "?"})`);
+ const onData = (buf: Buffer): void => {
+ const line = buf.toString().trimEnd();
+ if (line) this.onLog(line);
+ };
+ child.stdout?.on("data", onData);
+ child.stderr?.on("data", onData);
+ child.on("error", (err) => log.error(`gocoon-runner process error: ${getErrorMessage(err)}`));
+ child.on("exit", (code, signal) => this.onChildExit(code, signal));
+ }
+
+ private onChildExit(code: number | null, signal: NodeJS.Signals | null): void {
+ this.runningFlag = false;
+ if (this.stopped) {
+ log.info("gocoon-runner stopped");
+ return;
+ }
+ log.warn(
+ `gocoon-runner exited (code=${code ?? "null"} signal=${signal ?? "null"}); restarting in ${this.backoffMs}ms`
+ );
+ setTimeout(() => void this.restart(), this.backoffMs);
+ }
+
+ private async restart(): Promise {
+ if (this.stopped) return;
+ try {
+ this.spawnChild();
+ await waitReady(this.healthUrl, this.startGraceMs);
+ this.failures = 0;
+ this.backoffMs = 1_000;
+ log.info("gocoon-runner recovered");
+ } catch (err) {
+ this.failures += 1;
+ this.backoffMs = Math.min(this.backoffMs * 2, this.backoffCapMs);
+ if (this.failures >= MAX_RESTART_FAILURES) {
+ this.stopped = true;
+ this.killChild();
+ log.error(`gocoon-runner failed ${this.failures} times, giving up`);
+ return;
+ }
+ log.error(
+ `gocoon-runner restart unhealthy (${this.failures}/${MAX_RESTART_FAILURES}): ${getErrorMessage(err)}`
+ );
+ this.killChild(); // its exit event reschedules the next restart
+ }
+ }
+
+ private killChild(): void {
+ const child = this.child;
+ this.child = null;
+ if (child?.pid != null) killProcessGroup(child.pid);
+ }
+}
+
+// SIGTERM the whole process group, then SIGKILL after graceMs. taskkill on Windows.
+export function killProcessGroup(pid: number, graceMs = KILL_GRACE_MS): void {
+ if (process.platform === "win32") {
+ try {
+ execFileSync("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore" });
+ } catch {
+ /* gone */
+ }
+ return;
+ }
+ try {
+ process.kill(-pid, "SIGTERM");
+ setTimeout(() => {
+ try {
+ process.kill(-pid, "SIGKILL");
+ } catch {
+ /* gone */
+ }
+ }, graceMs);
+ } catch {
+ try {
+ process.kill(pid, "SIGKILL");
+ } catch {
+ /* gone */
+ }
+ }
+}
+
+export async function waitReady(url: string, timeoutMs: number): Promise {
+ const deadline = Date.now() + timeoutMs;
+ while (Date.now() < deadline) {
+ try {
+ const res = await fetch(url, { signal: AbortSignal.timeout(READY_PROBE_TIMEOUT_MS) });
+ if (res.ok) return;
+ } catch {
+ /* not up yet */
+ }
+ await sleep(READY_POLL_INTERVAL_MS);
+ }
+ throw new Error(`health ${url} not ready within ${timeoutMs}ms`);
+}
+
+const sleep = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms));
diff --git a/src/index.ts b/src/index.ts
index 80e26294..673f07f8 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -7,7 +7,6 @@ import type { TelegramMessage } from "./telegram/bridge.js";
import type { ITelegramBridge } from "./telegram/bridge-interface.js";
import { isBotBridge, isUserBridge } from "./telegram/bridge-guards.js";
import { createBridge } from "./telegram/factory.js";
-import { eventBus } from "./events/bus.js";
import { MessageHandler } from "./telegram/handlers.js";
import { AdminHandler } from "./telegram/admin.js";
import { MessageDebouncer } from "./telegram/debounce.js";
@@ -18,6 +17,8 @@ import { setTonapiKey } from "./constants/api-endpoints.js";
import { setToncenterApiKey } from "./ton/endpoint.js";
import { TELETON_ROOT } from "./workspace/paths.js";
import { join } from "path";
+import { existsSync } from "fs";
+import type { GocoonSupervisor, GocoonSseProxy } from "./gocoon/index.js";
import { ToolRegistry } from "./agent/tools/registry.js";
import { registerAllTools } from "./agent/tools/register-all.js";
import { type PluginModuleWithHooks } from "./agent/tools/plugin-loader.js";
@@ -67,6 +68,8 @@ export class TeletonApp {
private webuiServer: WebUIServer | null = null;
private apiServer: ApiServer | null = null;
private pluginWatcher: PluginWatcher | null = null;
+ private gocoonSupervisor: GocoonSupervisor | null = null;
+ private gocoonProxy: GocoonSseProxy | null = null;
private mcpConnections: McpConnection[] = [];
private callbackHandlerRegistered = false;
private messageHandlersRegistered = false;
@@ -134,9 +137,40 @@ export class TeletonApp {
rewireHooks: () => this.wirePluginEventHooks(),
},
userHookEvaluator: this.userHookEvaluator,
+ gocoonControl: {
+ stopRunner: () => this.stopGocoonRunner(),
+ },
};
}
+ /**
+ * Stop the supervised gocoon runner + SSE proxy. A withdraw refuses to run
+ * while the runner is active, so the Gocoon page calls this first. The agent
+ * stays up; gocoon inference is unavailable until the next restart.
+ */
+ stopGocoonRunner(): boolean {
+ let stopped = false;
+ if (this.gocoonProxy) {
+ try {
+ this.gocoonProxy.stop();
+ } catch (error: unknown) {
+ log.error({ err: error }, "gocoon sse-proxy stop failed");
+ }
+ this.gocoonProxy = null;
+ stopped = true;
+ }
+ if (this.gocoonSupervisor) {
+ try {
+ this.gocoonSupervisor.stop();
+ } catch (error: unknown) {
+ log.error({ err: error }, "gocoon supervisor stop failed");
+ }
+ this.gocoonSupervisor = null;
+ stopped = true;
+ }
+ return stopped;
+ }
+
constructor(configPath?: string) {
this.configPath = configPath ?? getDefaultConfigPath();
this.config = loadConfig(this.configPath);
@@ -429,7 +463,7 @@ ${blue} ┌──────────────────────
// Initialize context builder for RAG search in agent
this.agent.initializeContextBuilder(this.memory.embedder, getDatabase().isVectorSearchReady());
- // Register provider-specific models (Cocoon / local LLM)
+ // Register provider-specific models (gocoon / local LLM)
await this.initializeProviders();
// Connect to Telegram
@@ -437,7 +471,6 @@ ${blue} ┌──────────────────────
if (!this.bridge.isAvailable()) {
throw new Error("Failed to connect to Telegram");
}
- eventBus.emit("bridge:connected", { mode: this.config.telegram.mode });
await this.resolveOwnerInfo();
const ownUserId = this.bridge.getOwnUserId();
if (ownUserId) {
@@ -669,24 +702,64 @@ ${blue} ┌──────────────────────
}
/**
- * Register provider-specific models (Cocoon Network / local LLM).
+ * Register provider-specific models (gocoon / local LLM).
*/
private async initializeProviders(): Promise {
- if (this.config.agent.provider === "cocoon") {
+ if (this.config.agent.provider === "gocoon") {
+ const port = this.config.gocoon?.port ?? 10000;
+ const autoStart = this.config.gocoon?.auto_start ?? true;
try {
- const { registerCocoonModels } = await import("./agent/client.js");
- const port = this.config.cocoon?.port ?? 10000;
- const models = await registerCocoonModels(port);
+ if (autoStart) {
+ const {
+ ensureGocoonBinaries,
+ GocoonSupervisor,
+ runnerBaseUrl,
+ clientConfigPath,
+ walletInfo,
+ } = await import("./gocoon/index.js");
+ if (!existsSync(clientConfigPath())) {
+ throw new Error(
+ "gocoon is not set up yet; run `teleton gocoon init` (or use the Gocoon page) first"
+ );
+ }
+ await ensureGocoonBinaries();
+ // Opening the channel needs free TON on-chain; fail clearly instead of a health timeout.
+ const wallet = await walletInfo();
+ if (wallet.balanceNano < 2_000_000_000n) {
+ throw new Error(
+ `COCOON wallet has ${wallet.balanceTon} TON; gocoon needs at least 2 TON free to open the channel. ` +
+ `Fund ${wallet.fundAddress} (Gocoon page or \`teleton gocoon init\`), then restart.`
+ );
+ }
+ this.gocoonSupervisor = new GocoonSupervisor({
+ configPath: clientConfigPath(),
+ healthUrl: `${runnerBaseUrl(port)}/v1/models`,
+ startGraceMs: 60_000, // first on-chain channel registration can take ~60s
+ });
+ await this.gocoonSupervisor.start();
+ log.info(`Gocoon runner started on port ${port}`);
+ }
+ // pi-ai always streams and only parses SSE; the gocoon runner returns a
+ // single JSON document. Front the runner with a local proxy that frames
+ // its reply as SSE so streaming clients parse it. Keeps gocoon untouched.
+ const { GocoonSseProxy } = await import("./gocoon/index.js");
+ this.gocoonProxy = new GocoonSseProxy({ runnerPort: port });
+ await this.gocoonProxy.start();
+ const { registerGocoonModels } = await import("./agent/client.js");
+ const models = await registerGocoonModels(this.gocoonProxy.port);
if (models.length === 0) {
throw new Error(`No models found on port ${port}`);
}
- log.info(`Cocoon Network ready — ${models.length} model(s) on port ${port}`);
+ log.info(
+ `Gocoon ready: ${models.length} model(s) (runner ${port}, sse-proxy ${this.gocoonProxy.port})`
+ );
} catch (error: unknown) {
- log.error(
- `Cocoon Network unavailable on port ${this.config.cocoon?.port ?? 10000}: ${getErrorMessage(error)}`
+ // Non-fatal: keep the agent and WebUI alive so gocoon can be installed and
+ // funded from the Gocoon page, then a restart activates it.
+ log.warn(`Gocoon not ready: ${getErrorMessage(error)}`);
+ log.warn(
+ "Agent is up but can't chat until gocoon is funded. Open the Gocoon page (or run `teleton gocoon init`), then restart."
);
- log.error("Start the Cocoon client first: cocoon start");
- throw new Error(`Cocoon Network unavailable: ${getErrorMessage(error)}`);
}
}
@@ -1092,6 +1165,22 @@ ${blue} ┌──────────────────────
}
}
+ // Stop the gocoon SSE proxy and runner (when teleton supervises them)
+ if (this.gocoonProxy) {
+ try {
+ this.gocoonProxy.stop();
+ } catch (error: unknown) {
+ log.error({ err: error }, "gocoon sse-proxy stop failed");
+ }
+ }
+ if (this.gocoonSupervisor) {
+ try {
+ this.gocoonSupervisor.stop();
+ } catch (error: unknown) {
+ log.error({ err: error }, "gocoon supervisor stop failed");
+ }
+ }
+
// Close MCP connections
if (this.mcpConnections.length > 0) {
try {
diff --git a/src/memory/__tests__/observation-masking.test.ts b/src/memory/__tests__/observation-masking.test.ts
index 894b82a8..33ac5015 100644
--- a/src/memory/__tests__/observation-masking.test.ts
+++ b/src/memory/__tests__/observation-masking.test.ts
@@ -23,14 +23,6 @@ function makeUserMsg(text: string): UserMessage {
return { role: "user", content: [{ type: "text", text }], timestamp: Date.now() };
}
-function makeCocoonToolResult(text: string): UserMessage {
- return {
- role: "user",
- content: [{ type: "text", text: `${text}` }],
- timestamp: Date.now(),
- };
-}
-
const SHORT_RESULT = JSON.stringify({ success: true, data: { message: "Done" } });
const LONG_RESULT = JSON.stringify({
success: true,
@@ -181,19 +173,6 @@ describe("maskOldToolResults — inter-iteration truncation", () => {
expect(result[0]).toBe(messages[0]); // kept intact
});
- it("truncates Cocoon-style tool results", () => {
- const config: MaskingConfig = { ...DEFAULT_MASKING_CONFIG, keepRecentCount: 20 };
- const longCocoon = "x".repeat(5000);
- const messages: Message[] = [
- makeCocoonToolResult(longCocoon), // index 0, prev iteration
- makeToolResult("current", SHORT_RESULT), // index 1, current
- ];
- const result = maskOldToolResults(messages, { config, currentIterationStartIndex: 1 });
- const text = (result[0] as UserMessage).content[0];
- expect(text.type === "text" && text.text.length).toBeLessThan(longCocoon.length + 30);
- expect(text.type === "text" && text.text).toContain("truncated");
- });
-
it("uses summary field for truncation when available", () => {
const config: MaskingConfig = { ...DEFAULT_MASKING_CONFIG, keepRecentCount: 20 };
const withSummary = JSON.stringify({
diff --git a/src/memory/agent/index.ts b/src/memory/agent/index.ts
index eb994be6..61f9044a 100644
--- a/src/memory/agent/index.ts
+++ b/src/memory/agent/index.ts
@@ -1,3 +1,2 @@
export * from "./knowledge.js";
-export * from "./sessions.js";
export * from "./tasks.js";
diff --git a/src/memory/agent/sessions.ts b/src/memory/agent/sessions.ts
deleted file mode 100644
index ec2cd63c..00000000
--- a/src/memory/agent/sessions.ts
+++ /dev/null
@@ -1,198 +0,0 @@
-import type Database from "better-sqlite3";
-import type { EmbeddingProvider } from "../embeddings/provider.js";
-import { hashText, serializeEmbedding } from "../embeddings/index.js";
-import { randomUUID } from "crypto";
-import type { SessionRow } from "../types/db-rows.js";
-import { createLogger } from "../../utils/logger.js";
-
-const log = createLogger("Memory");
-
-export interface Session {
- id: string;
- chatId?: string;
- startedAt: Date;
- endedAt?: Date;
- summary?: string;
- messageCount: number;
- tokensUsed: number;
-}
-
-export interface SessionEntry {
- role: "user" | "assistant";
- content: string;
- timestamp: Date;
-}
-
-export class SessionStore {
- constructor(
- private db: Database.Database,
- private embedder: EmbeddingProvider,
- private vectorEnabled: boolean
- ) {}
-
- createSession(chatId?: string): Session {
- const id = randomUUID();
- const now = Math.floor(Date.now() / 1000);
-
- this.db
- .prepare(
- `
- INSERT INTO sessions (id, chat_id, started_at, message_count, tokens_used)
- VALUES (?, ?, ?, 0, 0)
- `
- )
- .run(id, chatId ?? null, now);
-
- return {
- id,
- chatId,
- startedAt: new Date(now * 1000),
- messageCount: 0,
- tokensUsed: 0,
- };
- }
-
- endSession(sessionId: string, summary: string, tokensUsed: number = 0): void {
- const now = Math.floor(Date.now() / 1000);
-
- this.db
- .prepare(
- `
- UPDATE sessions
- SET ended_at = ?, summary = ?, tokens_used = ?
- WHERE id = ?
- `
- )
- .run(now, summary, tokensUsed, sessionId);
- }
-
- incrementMessageCount(sessionId: string, count: number = 1): void {
- this.db
- .prepare(
- `
- UPDATE sessions
- SET message_count = message_count + ?
- WHERE id = ?
- `
- )
- .run(count, sessionId);
- }
-
- getSession(id: string): Session | undefined {
- const row = this.db.prepare(`SELECT * FROM sessions WHERE id = ?`).get(id) as
- | SessionRow
- | undefined;
-
- if (!row) return undefined;
-
- return {
- id: row.id,
- chatId: row.chat_id,
- startedAt: new Date(row.started_at * 1000),
- endedAt: row.ended_at ? new Date(row.ended_at * 1000) : undefined,
- summary: row.summary ?? undefined,
- messageCount: row.message_count,
- tokensUsed: row.tokens_used,
- };
- }
-
- getActiveSessions(): Session[] {
- const rows = this.db
- .prepare(
- `
- SELECT * FROM sessions
- WHERE ended_at IS NULL
- ORDER BY started_at DESC
- `
- )
- .all() as SessionRow[];
-
- return rows.map((row) => ({
- id: row.id,
- chatId: row.chat_id,
- startedAt: new Date(row.started_at * 1000),
- endedAt: undefined,
- summary: row.summary ?? undefined,
- messageCount: row.message_count,
- tokensUsed: row.tokens_used,
- }));
- }
-
- getSessionsByChat(chatId: string, limit: number = 50): Session[] {
- const rows = this.db
- .prepare(
- `
- SELECT * FROM sessions
- WHERE chat_id = ?
- ORDER BY started_at DESC
- LIMIT ?
- `
- )
- .all(chatId, limit) as SessionRow[];
-
- return rows.map((row) => ({
- id: row.id,
- chatId: row.chat_id,
- startedAt: new Date(row.started_at * 1000),
- endedAt: row.ended_at ? new Date(row.ended_at * 1000) : undefined,
- summary: row.summary ?? undefined,
- messageCount: row.message_count,
- tokensUsed: row.tokens_used,
- }));
- }
-
- /**
- * Index a session for search after ending.
- * Creates a knowledge entry from the session summary for future retrieval.
- */
- async indexSession(sessionId: string): Promise {
- const session = this.getSession(sessionId);
- if (!session || !session.summary) return;
-
- try {
- const knowledgeId = `session:${sessionId}`;
- const text = `Session from ${session.startedAt.toISOString()}:\n${session.summary}`;
- const hash = hashText(text);
-
- let embedding: number[] | null = null;
- if (this.vectorEnabled) {
- embedding = await this.embedder.embedQuery(text);
- }
-
- this.db
- .prepare(
- `
- INSERT INTO knowledge (id, source, path, text, hash, created_at, updated_at)
- VALUES (?, 'session', ?, ?, ?, unixepoch(), unixepoch())
- ON CONFLICT(id) DO UPDATE SET
- text = excluded.text,
- hash = excluded.hash,
- updated_at = excluded.updated_at
- `
- )
- .run(knowledgeId, sessionId, text, hash);
-
- if (embedding && this.vectorEnabled) {
- const embeddingBuffer = serializeEmbedding(embedding);
-
- this.db.prepare(`DELETE FROM knowledge_vec WHERE id = ?`).run(knowledgeId);
- this.db
- .prepare(`INSERT INTO knowledge_vec (id, embedding) VALUES (?, ?)`)
- .run(knowledgeId, embeddingBuffer);
- }
-
- log.info(`Indexed session ${sessionId} to knowledge base`);
- } catch (error) {
- log.error({ err: error }, "Error indexing session");
- }
- }
-
- deleteSession(sessionId: string): void {
- const knowledgeId = `session:${sessionId}`;
- if (this.vectorEnabled) {
- this.db.prepare(`DELETE FROM knowledge_vec WHERE id = ?`).run(knowledgeId);
- }
- this.db.prepare(`DELETE FROM sessions WHERE id = ?`).run(sessionId);
- this.db.prepare(`DELETE FROM knowledge WHERE id = ?`).run(knowledgeId);
- }
-}
diff --git a/src/memory/ai-summarization.ts b/src/memory/ai-summarization.ts
index e6a9354c..d0ce0493 100644
--- a/src/memory/ai-summarization.ts
+++ b/src/memory/ai-summarization.ts
@@ -16,13 +16,6 @@ import { createLogger } from "../utils/logger.js";
const log = createLogger("Memory");
-export interface SummarizationConfig {
- apiKey: string;
- contextWindow: number;
- maxSummaryTokens: number;
- maxChunkTokens: number;
-}
-
export interface SummarizationResult {
summary: string;
tokensUsed: number;
diff --git a/src/memory/daily-logs.ts b/src/memory/daily-logs.ts
index 0b3c87c9..f153c1a1 100644
--- a/src/memory/daily-logs.ts
+++ b/src/memory/daily-logs.ts
@@ -146,8 +146,3 @@ export function cleanupOldDailyLogs(maxAgeDays = 60): number {
return deleted;
}
-
-export function writeConversationMilestone(chatId: string, topic: string, details: string): void {
- const content = `### Conversation Milestone\n\n**Chat**: ${chatId}\n**Topic**: ${topic}\n\n${details}`;
- appendToDailyLog(content);
-}
diff --git a/src/memory/envelope.ts b/src/memory/envelope.ts
index 2896278d..d7400706 100644
--- a/src/memory/envelope.ts
+++ b/src/memory/envelope.ts
@@ -140,17 +140,3 @@ export function formatMessageEnvelope(params: EnvelopeParams): string {
}
return `${header} ${body}`;
}
-
-export function formatMessageEnvelopeSimple(params: {
- senderId?: string;
- senderName?: string;
- body: string;
- isGroup: boolean;
-}): string {
- if (!params.isGroup) {
- return params.body;
- }
-
- const sender = params.senderName || (params.senderId ? `user:${params.senderId}` : "unknown");
- return `${sender}: ${params.body}`;
-}
diff --git a/src/memory/feed/messages.ts b/src/memory/feed/messages.ts
index bbc49006..761da6b9 100644
--- a/src/memory/feed/messages.ts
+++ b/src/memory/feed/messages.ts
@@ -90,12 +90,6 @@ export class MessageStore {
})();
}
- pruneOldMessages(maxAgeDays = 90): number {
- const cutoffSec = Math.floor(Date.now() / 1000) - maxAgeDays * 86_400;
- const result = this.db.prepare("DELETE FROM tg_messages WHERE timestamp < ?").run(cutoffSec);
- return result.changes;
- }
-
getRecentMessages(chatId: string, limit: number = 20): TelegramMessage[] {
const rows = this.db
.prepare(
diff --git a/src/memory/observation-masking.ts b/src/memory/observation-masking.ts
index df8b70ab..5786b1bd 100644
--- a/src/memory/observation-masking.ts
+++ b/src/memory/observation-masking.ts
@@ -1,4 +1,4 @@
-import type { Message, ToolResultMessage, UserMessage, TextContent } from "@mariozechner/pi-ai";
+import type { Message, ToolResultMessage, TextContent } from "@mariozechner/pi-ai";
import type { ToolRegistry } from "../agent/tools/registry.js";
import {
MASKING_KEEP_RECENT_COUNT,
@@ -26,12 +26,6 @@ export interface MaskingOptions {
currentIterationStartIndex?: number;
}
-/** Detect Cocoon-style tool results (UserMessage with `` CDATA). */
-const isCocoonToolResult = (msg: Message): boolean =>
- msg.role === "user" &&
- Array.isArray(msg.content) &&
- msg.content.some((c) => c.type === "text" && c.text.includes(""));
-
/** Check if a tool result should be exempt from masking/truncation. */
function isExempt(
toolMsg: ToolResultMessage,
@@ -82,7 +76,7 @@ export function maskOldToolResults(messages: Message[], options?: MaskingOptions
const toolResults = messages
.map((msg, index) => ({ msg, index }))
- .filter(({ msg }) => msg.role === "toolResult" || isCocoonToolResult(msg));
+ .filter(({ msg }) => msg.role === "toolResult");
// Quick exit: nothing to mask or truncate
const needsMasking = toolResults.length > config.keepRecentCount;
@@ -98,14 +92,6 @@ export function maskOldToolResults(messages: Message[], options?: MaskingOptions
const toMask = toolResults.slice(0, -config.keepRecentCount);
for (const { msg, index } of toMask) {
- if (isCocoonToolResult(msg)) {
- result[index] = {
- ...msg,
- content: [{ type: "text" as const, text: "[Tool response masked]" }],
- } as UserMessage;
- continue;
- }
-
const toolMsg = msg as ToolResultMessage;
if (isExempt(toolMsg, config, toolRegistry)) continue;
@@ -142,24 +128,6 @@ export function maskOldToolResults(messages: Message[], options?: MaskingOptions
// Never truncate results from the current iteration
if (index >= iterStart) continue;
- if (isCocoonToolResult(msg)) {
- const userMsg = msg as UserMessage;
- if (!Array.isArray(userMsg.content)) continue;
- const textBlock = userMsg.content.find((c): c is TextContent => c.type === "text");
- if (textBlock && textBlock.text.length > config.truncationThreshold) {
- result[index] = {
- ...userMsg,
- content: [
- {
- type: "text" as const,
- text: truncateToolResult(textBlock.text, config.truncationKeepChars),
- },
- ],
- } as UserMessage;
- }
- continue;
- }
-
const toolMsg = msg as ToolResultMessage;
if (isExempt(toolMsg, config, toolRegistry)) continue;
@@ -180,31 +148,3 @@ export function maskOldToolResults(messages: Message[], options?: MaskingOptions
return result;
}
-
-export function calculateMaskingSavings(
- originalMessages: Message[],
- maskedMessages: Message[]
-): { originalChars: number; maskedChars: number; savings: number } {
- const countChars = (messages: Message[]): number => {
- let total = 0;
- for (const msg of messages) {
- if (msg.role === "toolResult" || isCocoonToolResult(msg)) {
- for (const block of msg.content) {
- if (typeof block !== "string" && block.type === "text") {
- total += block.text.length;
- }
- }
- }
- }
- return total;
- };
-
- const originalChars = countChars(originalMessages);
- const maskedChars = countChars(maskedMessages);
-
- return {
- originalChars,
- maskedChars,
- savings: originalChars - maskedChars,
- };
-}
diff --git a/src/memory/tool-config.ts b/src/memory/tool-config.ts
index 84ed10a9..28a246f4 100644
--- a/src/memory/tool-config.ts
+++ b/src/memory/tool-config.ts
@@ -100,10 +100,3 @@ export function initializeToolConfig(
).run(toolName, legacyEnabled, legacyScope, level);
}
}
-
-/**
- * Delete tool configuration (reverts to defaults)
- */
-export function deleteToolConfig(db: Database.Database, toolName: string): void {
- db.prepare(`DELETE FROM tool_config WHERE tool_name = ?`).run(toolName);
-}
diff --git a/src/memory/types/db-rows.ts b/src/memory/types/db-rows.ts
index 3ba3b25c..7f073a8f 100644
--- a/src/memory/types/db-rows.ts
+++ b/src/memory/types/db-rows.ts
@@ -20,24 +20,6 @@ export interface TaskRow {
scheduled_message_id: number | null;
}
-export interface SessionRow {
- id: string;
- chat_id: string;
- started_at: number;
- updated_at: number;
- ended_at: number | null;
- summary: string | null;
- message_count: number;
- tokens_used: number;
- last_message_id: number | null;
- last_channel: string | null;
- last_to: string | null;
- context_tokens: number | null;
- model: string | null;
- provider: string | null;
- last_reset_date: string | null;
-}
-
export interface TgUserRow {
id: string;
username: string | null;
diff --git a/src/providers/codex-credentials.ts b/src/providers/codex-credentials.ts
index e341085f..05711e0b 100644
--- a/src/providers/codex-credentials.ts
+++ b/src/providers/codex-credentials.ts
@@ -144,8 +144,3 @@ export async function refreshCodexApiKey(): Promise {
export function isCodexTokenValid(): boolean {
return tokenCache.isValid();
}
-
-/** Reset internal cache — exposed for testing only */
-export function _resetCache(): void {
- tokenCache.reset();
-}
diff --git a/src/providers/model-resolver.ts b/src/providers/model-resolver.ts
index f42994db..81eb79c3 100644
--- a/src/providers/model-resolver.ts
+++ b/src/providers/model-resolver.ts
@@ -7,10 +7,10 @@ const log = createLogger("LLM");
const modelCache = new Map>();
-const COCOON_MODELS: Record> = {};
+const GOCOON_MODELS: Record> = {};
-/** Register models discovered from a running Cocoon client */
-export async function registerCocoonModels(httpPort: number): Promise {
+/** Register models discovered from a running gocoon-runner (native OpenAI-compatible API). */
+export async function registerGocoonModels(httpPort: number): Promise {
try {
const res = await fetchWithTimeout(`http://localhost:${httpPort}/v1/models`, {
timeoutMs: 3000,
@@ -25,11 +25,11 @@ export async function registerCocoonModels(httpPort: number): Promise
const ids: string[] = [];
for (const m of models) {
const id = m.id || m.name || String(m);
- COCOON_MODELS[id] = {
+ GOCOON_MODELS[id] = {
id,
name: id,
api: "openai-completions",
- provider: "cocoon",
+ provider: "gocoon",
baseUrl: `http://localhost:${httpPort}/v1`,
reasoning: false,
input: ["text"],
@@ -40,6 +40,8 @@ export async function registerCocoonModels(httpPort: number): Promise
supportsStore: false,
supportsDeveloperRole: false,
supportsReasoningEffort: false,
+ supportsStrictMode: false,
+ maxTokensField: "max_tokens",
},
};
ids.push(id);
@@ -47,7 +49,7 @@ export async function registerCocoonModels(httpPort: number): Promise
return ids;
} catch (error) {
if (error instanceof Error && error.name === "TimeoutError") {
- log.warn({ port: httpPort }, "Cocoon /v1/models timed out after 3s, returning empty list");
+ log.warn({ port: httpPort }, "gocoon /v1/models timed out after 3s, returning empty list");
}
return [];
}
@@ -116,17 +118,19 @@ export function getProviderModel(provider: SupportedProvider, modelId: string):
const meta = getProviderMetadata(provider);
- if (meta.piAiProvider === "cocoon") {
- let model = COCOON_MODELS[modelId];
+ if (meta.piAiProvider === "gocoon") {
+ let model = GOCOON_MODELS[modelId];
if (!model) {
- model = Object.values(COCOON_MODELS)[0];
- if (model) log.warn(`Cocoon model "${modelId}" not found, using "${model.id}"`);
+ // Fall back to the provider default (a served model), not the first registered
+ // one, which may be an unusable model with no workers.
+ model = GOCOON_MODELS[meta.defaultModel] ?? Object.values(GOCOON_MODELS)[0];
+ if (model) log.warn(`gocoon model "${modelId}" not found, using "${model.id}"`);
}
if (model) {
modelCache.set(cacheKey, model);
return model;
}
- throw new Error("No Cocoon models available. Is the cocoon client running?");
+ throw new Error("No gocoon models available. Is the gocoon runner running?");
}
if (meta.piAiProvider === "local") {
diff --git a/src/services/tts.ts b/src/services/tts.ts
index 8ea81e75..ded16feb 100644
--- a/src/services/tts.ts
+++ b/src/services/tts.ts
@@ -396,36 +396,3 @@ async function generateElevenLabsTTS(text: string, voiceId: string): Promise {
- return new Promise((resolve, reject) => {
- const proc = spawn("edge-tts", ["--list-voices"], {
- stdio: ["pipe", "pipe", "pipe"],
- });
-
- let stdout = "";
- proc.stdout?.on("data", (data) => {
- stdout += data.toString();
- });
-
- proc.on("close", (code) => {
- if (code === 0) {
- // Parse voice names from output
- const voices = stdout
- .split("\n")
- .filter((line) => line.startsWith("Name:"))
- .map((line) => line.replace("Name: ", "").trim());
- resolve(voices);
- } else {
- reject(new Error("Failed to list voices"));
- }
- });
-
- proc.on("error", (err) => {
- reject(err);
- });
- });
-}
diff --git a/src/session/store.ts b/src/session/store.ts
index 094ba97c..96778fe6 100644
--- a/src/session/store.ts
+++ b/src/session/store.ts
@@ -239,24 +239,6 @@ export function updateSession(
.get(sessionKey) as SessionRow;
return rowToSession(updated);
}
-export function incrementMessageCount(chatId: string): void {
- const db = getDb();
- const sessionKey = sessionKeyFor(chatId);
-
- const result = db
- .prepare(
- `UPDATE sessions SET message_count = message_count + 1, updated_at = ? WHERE chat_id = ?`
- )
- .run(Date.now(), sessionKey);
-
- // If no row existed, create the session first then increment
- if (result.changes === 0) {
- getOrCreateSession(chatId);
- db.prepare(
- `UPDATE sessions SET message_count = message_count + 1, updated_at = ? WHERE chat_id = ?`
- ).run(Date.now(), sessionKey);
- }
-}
export function getSession(chatId: string): SessionEntry | null {
const db = getDb();
const sessionKey = sessionKeyFor(chatId);
diff --git a/src/session/transcript.ts b/src/session/transcript.ts
index cf6aeafc..9ad2bd7d 100644
--- a/src/session/transcript.ts
+++ b/src/session/transcript.ts
@@ -168,33 +168,6 @@ export function transcriptExists(sessionId: string): boolean {
return existsSync(getTranscriptPath(sessionId));
}
-export function getTranscriptSize(sessionId: string): number {
- try {
- const messages = readTranscript(sessionId);
- return messages.length;
- } catch {
- return 0;
- }
-}
-
-export function deleteTranscript(sessionId: string): boolean {
- const transcriptPath = getTranscriptPath(sessionId);
-
- if (!existsSync(transcriptPath)) {
- return false;
- }
-
- try {
- unlinkSync(transcriptPath);
- transcriptCache.delete(sessionId);
- log.info(`Deleted transcript: ${sessionId}`);
- return true;
- } catch (error) {
- log.error({ err: error }, `Failed to delete transcript ${sessionId}`);
- return false;
- }
-}
-
/**
* Archive a transcript (rename with timestamped .archived suffix).
*/
diff --git a/src/telegram/callbacks/handler.ts b/src/telegram/callbacks/handler.ts
deleted file mode 100644
index 5e102885..00000000
--- a/src/telegram/callbacks/handler.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-import type { ITelegramBridge } from "../bridge-interface.js";
-import { isUserBridge } from "../bridge-guards.js";
-import { createLogger } from "../../utils/logger.js";
-
-const log = createLogger("Telegram");
-
-export type CallbackHandler = (data: {
- action: string;
- params: string[];
- queryId: bigint;
- chatId: string;
- messageId: number;
- userId: number;
-}) => Promise;
-
-export class CallbackQueryHandler {
- private handlers: Map = new Map();
-
- constructor(private bridge: ITelegramBridge) {}
-
- register(actionPrefix: string, handler: CallbackHandler): void {
- this.handlers.set(actionPrefix, handler);
- }
-
- async handle(event: {
- queryId: bigint;
- data?: Buffer;
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- peer type varies across GramJS update shapes
- peer?: any;
- chatInstance?: string;
- msgId?: number;
- userId: bigint;
- }): Promise {
- try {
- const queryId = event.queryId;
- const data = event.data?.toString() || "";
- const chatId = event.peer?.toString() || event.chatInstance?.toString() || "";
- const messageId = event.msgId || 0;
- const userId = Number(event.userId);
-
- log.info(`[Callback] Received: data="${data}" from user ${userId} in chat ${chatId}`);
-
- const parts = data.split(":");
- const action = parts[0];
- const params = parts.slice(1);
-
- const handler = this.handlers.get(action);
- if (!handler) {
- log.warn(`No handler for callback action: ${action}`);
- await this.answerCallback(queryId, "Unknown action");
- return;
- }
-
- await handler({
- action,
- params,
- queryId,
- chatId,
- messageId,
- userId,
- });
- } catch (error) {
- log.error({ err: error }, "Error handling callback query");
- if (event?.queryId) {
- await this.answerCallback(event.queryId, "An error occurred. Please try again.");
- }
- }
- }
-
- private async answerCallback(queryId: bigint, message?: string, alert = false): Promise {
- try {
- if (isUserBridge(this.bridge)) {
- await this.bridge.getClient().answerCallbackQuery(queryId, { message, alert });
- }
- } catch (error) {
- log.error({ err: error }, "Error answering callback");
- }
- }
-}
diff --git a/src/telegram/callbacks/index.ts b/src/telegram/callbacks/index.ts
deleted file mode 100644
index 4d8079e7..00000000
--- a/src/telegram/callbacks/index.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export type {
- CallbackQuery,
- InlineButton,
- CallbackHandler as CallbackHandlerFunc,
-} from "./types.js";
-export { CallbackQueryHandler } from "./handler.js";
-export { initializeCallbackRouter } from "./router.js";
diff --git a/src/telegram/callbacks/router.ts b/src/telegram/callbacks/router.ts
deleted file mode 100644
index 7a26e18f..00000000
--- a/src/telegram/callbacks/router.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { CallbackQueryHandler } from "./handler.js";
-import type { ITelegramBridge } from "../bridge-interface.js";
-
-export function initializeCallbackRouter(bridge: ITelegramBridge): CallbackQueryHandler {
- const handler = new CallbackQueryHandler(bridge);
- return handler;
-}
diff --git a/src/telegram/callbacks/types.ts b/src/telegram/callbacks/types.ts
deleted file mode 100644
index cf7c7bb5..00000000
--- a/src/telegram/callbacks/types.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-export interface CallbackQuery {
- id: string;
- data: string;
- chatId: string;
- messageId: number;
- userId: number;
-}
-
-export interface InlineButton {
- text: string;
- callback_data: string;
-}
-
-export type CallbackHandler = (query: CallbackQuery) => Promise;
diff --git a/src/telegram/client.ts b/src/telegram/client.ts
index 2b473085..3036c918 100644
--- a/src/telegram/client.ts
+++ b/src/telegram/client.ts
@@ -371,21 +371,6 @@ export class TelegramUserClient {
}
}
- async resolveUsername(username: string): Promise {
- const clean = username.replace("@", "");
- try {
- // Call ResolveUsername directly — bypasses GramJS's VALID_USERNAME_RE
- // which rejects collectible usernames shorter than 5 chars.
- const result = await this.client.invoke(
- new Api.contacts.ResolveUsername({ username: clean })
- );
- return result.users[0] || result.chats[0];
- } catch (error: unknown) {
- log.error({ err: error }, `Failed to resolve username ${clean}`);
- return undefined;
- }
- }
-
async getEntity(entity: string): Promise {
return await this.client.getEntity(entity);
}
diff --git a/src/telegram/debounce.ts b/src/telegram/debounce.ts
index f1e16990..dbe65af5 100644
--- a/src/telegram/debounce.ts
+++ b/src/telegram/debounce.ts
@@ -134,10 +134,6 @@ export class MessageDebouncer {
}
}
- getBufferDepth(chatId: string): number {
- return this.buffers.get(chatId)?.messages.length ?? 0;
- }
-
async flushAll(): Promise {
const keys = Array.from(this.buffers.keys());
for (const key of keys) {
diff --git a/src/telegram/offset-store.ts b/src/telegram/offset-store.ts
index 3576c650..a93d6277 100644
--- a/src/telegram/offset-store.ts
+++ b/src/telegram/offset-store.ts
@@ -94,10 +94,3 @@ export function writeOffset(messageId: number, chatId?: string): void {
saveState(state);
}
}
-
-/**
- * Get all chat offsets (for debugging)
- */
-export function getAllOffsets(): Record {
- return loadState().perChat;
-}
diff --git a/src/ton/dex-constants.ts b/src/ton/dex-constants.ts
index 73052b9a..6b43110a 100644
--- a/src/ton/dex-constants.ts
+++ b/src/ton/dex-constants.ts
@@ -18,8 +18,6 @@ export const DEDUST_GAS = {
SWAP_TON_TO_JETTON: "0.25",
// Jetton to any asset swap
SWAP_JETTON_TO_ANY: "0.3",
- // Extra gas for multi-hop swaps
- MULTIHOP_EXTRA: "0.1",
// Forward gas for jetton transfers
FORWARD_GAS: "0.2",
};
diff --git a/src/utils/logger.ts b/src/utils/logger.ts
index e814de62..348fa0bb 100644
--- a/src/utils/logger.ts
+++ b/src/utils/logger.ts
@@ -16,14 +16,6 @@ export function addLogListener(fn: LogListener): () => void {
return () => listeners.delete(fn);
}
-export function removeLogListener(fn: LogListener): void {
- listeners.delete(fn);
-}
-
-export function clearLogListeners(): void {
- listeners.clear();
-}
-
// ── Pino level → WebUI LogEntry level mapping ─────────────────────────
const LEVEL_MAP: Record = {
10: "log", // trace → log
@@ -163,9 +155,6 @@ export function createLogger(module: string): pino.Logger {
return rootLogger.child({ module });
}
-/** The root pino logger (no module prefix). */
-export const logger = rootLogger;
-
/**
* Apply logging config from YAML (called after config load in TonnetApp).
* Wires config.logging.level to the live logger.
@@ -201,22 +190,10 @@ export function setLogLevel(level: LogLevel): void {
_verbose = level === "debug" || level === "trace";
}
-/**
- * Get current log level.
- */
-export function getLogLevel(): string {
- return rootLogger.level;
-}
-
// ── Backward compatibility ────────────────────────────────────────────
let _verbose = rootLogger.isLevelEnabled("debug");
-/** @deprecated Use createLogger(module).debug() instead */
-export function verbose(...args: unknown[]): void {
- if (_verbose) rootLogger.debug(args.map(String).join(" "));
-}
-
/** @deprecated Use setLogLevel("debug") / setLogLevel("info") instead */
export function setVerbose(v: boolean): void {
setLogLevel(v ? "debug" : "info");
diff --git a/src/webui/__tests__/setup-routes.test.ts b/src/webui/__tests__/setup-routes.test.ts
index e468c9ba..42f385a9 100644
--- a/src/webui/__tests__/setup-routes.test.ts
+++ b/src/webui/__tests__/setup-routes.test.ts
@@ -58,8 +58,8 @@ vi.mock("../../config/providers.js", () => ({
consoleUrl: "https://console.anthropic.com/",
},
{
- id: "cocoon",
- displayName: "Cocoon Network",
+ id: "gocoon",
+ displayName: "Gocoon",
defaultModel: "auto",
utilityModel: "auto",
toolLimit: null,
@@ -182,8 +182,8 @@ describe("Setup API Routes", () => {
consoleUrl: "https://console.anthropic.com/",
},
{
- id: "cocoon",
- displayName: "Cocoon Network",
+ id: "gocoon",
+ displayName: "Gocoon",
defaultModel: "auto",
utilityModel: "auto",
toolLimit: null,
@@ -289,7 +289,7 @@ describe("Setup API Routes", () => {
expect(data.data).toHaveLength(2);
expect(data.data[0].id).toBe("anthropic");
expect(data.data[0].requiresApiKey).toBe(true);
- expect(data.data[1].id).toBe("cocoon");
+ expect(data.data[1].id).toBe("gocoon");
expect(data.data[1].requiresApiKey).toBe(false);
});
});
@@ -887,18 +887,18 @@ describe("Setup API Routes", () => {
expect(ConfigSchema.parse).toHaveBeenCalled();
});
- it("includes cocoon config when provided", async () => {
+ it("includes gocoon config when provided", async () => {
const input = {
...validInput,
- cocoon: { endpoint: "https://cocoon.network" },
+ gocoon: { port: 10000 },
};
const res = await post(app, "/config/save", input);
expect(res.status).toBe(200);
- // Check that writeFileSync was called with YAML containing cocoon
+ // Check that writeFileSync was called with YAML containing gocoon
const writeCall = (writeFileSync as Mock).mock.calls[0];
- expect(writeCall[1]).toContain("cocoon");
+ expect(writeCall[1]).toContain("gocoon");
});
it("returns 400 on Zod validation failure", async () => {
diff --git a/src/webui/log-interceptor.ts b/src/webui/log-interceptor.ts
index 35718012..a77a86f5 100644
--- a/src/webui/log-interceptor.ts
+++ b/src/webui/log-interceptor.ts
@@ -6,7 +6,7 @@
* keeping the same API for routes/logs.ts and the SSE stream.
*/
import type { LogEntry } from "./types.js";
-import { addLogListener, clearLogListeners } from "../utils/logger.js";
+import { addLogListener } from "../utils/logger.js";
type LogListener = (entry: LogEntry) => void;
@@ -40,22 +40,6 @@ class LogInterceptor {
this.cleanups.delete(listener);
};
}
-
- removeListener(listener: LogListener): void {
- const cleanup = this.cleanups.get(listener);
- if (cleanup) {
- cleanup();
- this.cleanups.delete(listener);
- }
- }
-
- clear(): void {
- for (const cleanup of this.cleanups.values()) {
- cleanup();
- }
- this.cleanups.clear();
- clearLogListeners();
- }
}
// Singleton instance
diff --git a/src/webui/routes/config.ts b/src/webui/routes/config.ts
index 8d92aa7c..ac17ba90 100644
--- a/src/webui/routes/config.ts
+++ b/src/webui/routes/config.ts
@@ -335,7 +335,8 @@ export function createConfigRoutes(deps: WebUIServerDeps) {
const provider = c.req.param("provider");
try {
const meta = getProviderMetadata(provider as SupportedProvider);
- const needsKey = provider !== "cocoon" && provider !== "local";
+ // codex authenticates via the Codex CLI (~/.codex/auth.json), no key to paste.
+ const needsKey = provider !== "gocoon" && provider !== "local" && provider !== "codex";
return c.json({
success: true,
data: {
diff --git a/src/webui/routes/gocoon.ts b/src/webui/routes/gocoon.ts
new file mode 100644
index 00000000..e851d125
--- /dev/null
+++ b/src/webui/routes/gocoon.ts
@@ -0,0 +1,175 @@
+import { Hono } from "hono";
+import type { WebUIServerDeps, APIResponse } from "../types.js";
+import { getErrorMessage } from "../../utils/errors.js";
+import {
+ ensureGocoonBinaries,
+ init,
+ isInstalled,
+ resetWallet,
+ runnerBaseUrl,
+ topup,
+ walletInfo,
+ withdrawAll,
+ GOCOON_DEFAULT_PORT,
+ GOCOON_VERSION,
+ type GocoonProgress,
+} from "../../gocoon/index.js";
+
+// gocoon management routes (install/setup/top-up/withdraw), thin shells over the
+// same lifecycle the CLI uses. Mounted at /api/gocoon and /v1/gocoon.
+export function createGocoonRoutes(deps: WebUIServerDeps) {
+ const app = new Hono();
+ const port = (): number => deps.agent.getConfig().gocoon?.port ?? GOCOON_DEFAULT_PORT;
+
+ let withdraw: {
+ running: boolean;
+ done: boolean;
+ events: GocoonProgress[];
+ error?: string;
+ } | null = null;
+
+ app.get("/status", async (c) => {
+ const data: Record = {
+ installed: isInstalled(),
+ version: isInstalled() ? GOCOON_VERSION : null,
+ };
+ try {
+ const w = await walletInfo();
+ data.wallet = {
+ fundAddress: w.fundAddress,
+ ownerAddress: w.ownerAddress,
+ balanceTon: w.balanceTon,
+ balanceNano: w.balanceNano.toString(),
+ funded: w.funded,
+ recommendedFundingTon: w.recommendedFundingTon,
+ };
+ } catch {
+ data.wallet = null;
+ }
+ data.runner = await fetch(`${runnerBaseUrl(port())}/jsonstats`, {
+ signal: AbortSignal.timeout(800),
+ })
+ .then(() => true)
+ .catch(() => false);
+ return c.json({ success: true, data } as APIResponse);
+ });
+
+ app.post("/install", async (c) => {
+ try {
+ await ensureGocoonBinaries();
+ return c.json({ success: true, data: { version: GOCOON_VERSION } } as APIResponse);
+ } catch (err) {
+ return c.json({ success: false, error: getErrorMessage(err) } as APIResponse, 500);
+ }
+ });
+
+ app.post("/init", async (c) => {
+ try {
+ const s = await init();
+ return c.json({
+ success: true,
+ data: { fundAddress: s.fundAddress, recommendedFundingTon: s.recommendedFundingTon },
+ } as APIResponse);
+ } catch (err) {
+ return c.json({ success: false, error: getErrorMessage(err) } as APIResponse, 500);
+ }
+ });
+
+ // Poll this until funded flips true after the user sends the TON.
+ app.get("/balance", async (c) => {
+ try {
+ const w = await walletInfo();
+ return c.json({
+ success: true,
+ data: { balanceTon: w.balanceTon, balanceNano: w.balanceNano.toString(), funded: w.funded },
+ } as APIResponse);
+ } catch (err) {
+ return c.json({ success: false, error: getErrorMessage(err) } as APIResponse, 400);
+ }
+ });
+
+ app.post("/topup", async (c) => {
+ try {
+ const body = (await c.req.json().catch(() => ({}))) as { amount?: string | number };
+ const amount = String(body.amount ?? "").trim();
+ if (!amount)
+ return c.json({ success: false, error: "amount (TON) is required" } as APIResponse, 400);
+ await topup(amount, port());
+ return c.json({ success: true, data: { amount } } as APIResponse);
+ } catch (err) {
+ return c.json({ success: false, error: getErrorMessage(err) } as APIResponse, 400);
+ }
+ });
+
+ // Async: POST starts a withdraw, GET polls its progress.
+ app.post("/withdraw", async (c) => {
+ if (withdraw?.running) {
+ return c.json(
+ { success: false, error: "a withdraw is already in progress" } as APIResponse,
+ 409
+ );
+ }
+ const body = (await c.req.json().catch(() => ({}))) as { destination?: string };
+ const destination = String(body.destination ?? "").trim();
+ if (!destination)
+ return c.json({ success: false, error: "destination is required" } as APIResponse, 400);
+
+ const job: { running: boolean; done: boolean; events: GocoonProgress[]; error?: string } = {
+ running: true,
+ done: false,
+ events: [],
+ };
+ withdraw = job;
+ void withdrawAll(destination, (e) => job.events.push(e), port())
+ .then(() => {
+ job.running = false;
+ job.done = true;
+ })
+ .catch((err) => {
+ job.running = false;
+ job.done = true;
+ job.error = getErrorMessage(err);
+ });
+ return c.json({ success: true, data: { started: true } } as APIResponse);
+ });
+
+ app.get("/withdraw", (c) =>
+ c.json({
+ success: true,
+ data: withdraw ?? { running: false, done: false, events: [] },
+ } as APIResponse)
+ );
+
+ // Stop teleton's supervised runner so a withdraw can proceed (withdrawAll
+ // refuses while the runner is active). Waits for the port to go quiet so the
+ // caller knows it's safe to start the withdraw.
+ app.post("/runner/stop", async (c) => {
+ const wasRunning = deps.gocoonControl?.stopRunner() ?? false;
+ const deadline = Date.now() + 8000;
+ while (Date.now() < deadline) {
+ const up = await fetch(`${runnerBaseUrl(port())}/jsonstats`, {
+ signal: AbortSignal.timeout(800),
+ })
+ .then(() => true)
+ .catch(() => false);
+ if (!up) break;
+ await new Promise((r) => setTimeout(r, 400));
+ }
+ return c.json({ success: true, data: { stopped: wasRunning } } as APIResponse);
+ });
+
+ // Wipe the local COCOON wallet + config so the next setup creates a fresh
+ // wallet. Guarded in resetWallet: refuses while the runner is up, or while
+ // funds / an active channel remain (unless force).
+ app.post("/reset", async (c) => {
+ try {
+ const body = (await c.req.json().catch(() => ({}))) as { force?: boolean };
+ await resetWallet({ force: Boolean(body.force) }, port());
+ return c.json({ success: true, data: { reset: true } } as APIResponse);
+ } catch (err) {
+ return c.json({ success: false, error: getErrorMessage(err) } as APIResponse, 400);
+ }
+ });
+
+ return app;
+}
diff --git a/src/webui/routes/setup.ts b/src/webui/routes/setup.ts
index 41932985..898ff6e1 100644
--- a/src/webui/routes/setup.ts
+++ b/src/webui/routes/setup.ts
@@ -92,7 +92,7 @@ export function createSetupRoutes(options?: { keyHash?: string }): Hono {
toolLimit: p.toolLimit,
keyPrefix: p.keyPrefix,
consoleUrl: p.consoleUrl,
- requiresApiKey: p.id !== "cocoon" && p.id !== "local",
+ requiresApiKey: p.id !== "gocoon" && p.id !== "local",
requiresBaseUrl: p.id === "local",
}));
return c.json({ success: true, data: providers });
@@ -542,7 +542,7 @@ export function createSetupRoutes(options?: { keyHash?: string }): Hono {
},
mcp: { servers: {} },
plugins: {},
- ...(input.cocoon ? { cocoon: input.cocoon } : {}),
+ ...(input.gocoon ? { gocoon: input.gocoon } : {}),
...(input.tonapi_key ? { tonapi_key: input.tonapi_key } : {}),
...(input.toncenter_api_key ? { toncenter_api_key: input.toncenter_api_key } : {}),
...(input.tavily_api_key ? { tavily_api_key: input.tavily_api_key } : {}),
diff --git a/src/webui/routes/shared.ts b/src/webui/routes/shared.ts
index 58bbde12..36224b7a 100644
--- a/src/webui/routes/shared.ts
+++ b/src/webui/routes/shared.ts
@@ -14,6 +14,7 @@ import { createConfigRoutes } from "./config.js";
import { createMarketplaceRoutes } from "./marketplace.js";
import { createHooksRoutes } from "./hooks.js";
import { createTonProxyRoutes } from "./ton-proxy.js";
+import { createGocoonRoutes } from "./gocoon.js";
/** A route factory shared by the WebUI and Management API servers. */
export type RouteFactory = (deps: WebUIServerDeps) => Hono;
@@ -42,4 +43,5 @@ export const SHARED_ROUTE_FACTORIES: ReadonlyArray<[string, RouteFactory]> = [
["marketplace", createMarketplaceRoutes],
["hooks", createHooksRoutes],
["ton-proxy", createTonProxyRoutes],
+ ["gocoon", createGocoonRoutes],
];
diff --git a/src/webui/types.ts b/src/webui/types.ts
index 5963dbe9..95680589 100644
--- a/src/webui/types.ts
+++ b/src/webui/types.ts
@@ -43,6 +43,8 @@ export interface WebUIServerDeps {
lifecycle?: AgentLifecycle;
marketplace?: MarketplaceDeps;
userHookEvaluator?: UserHookEvaluator | null;
+ /** Stop the supervised gocoon runner + proxy so a withdraw can close the channel. */
+ gocoonControl?: { stopRunner: () => boolean };
}
// ── Marketplace types ───────────────────────────────────────────────
@@ -121,15 +123,6 @@ export interface ModuleInfo {
isPlugin: boolean;
}
-export interface PluginManifest {
- name: string;
- version: string;
- author?: string;
- description?: string;
- dependencies?: string[];
- sdkVersion?: string;
-}
-
export interface MemorySearchResult {
id: string;
text: string;
diff --git a/src/workspace/index.ts b/src/workspace/index.ts
index 7951176f..28ffbd4e 100644
--- a/src/workspace/index.ts
+++ b/src/workspace/index.ts
@@ -26,8 +26,6 @@ export {
ensureWorkspace,
isNewWorkspace,
loadTemplate,
- writeFileIfMissing,
- getWorkspaceStats,
type Workspace,
type WorkspaceConfig,
} from "./manager.js";
diff --git a/src/workspace/manager.ts b/src/workspace/manager.ts
index 84ff3ae3..c52e3c0e 100644
--- a/src/workspace/manager.ts
+++ b/src/workspace/manager.ts
@@ -1,6 +1,6 @@
// src/workspace/manager.ts
-import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync } from "fs";
+import { existsSync, mkdirSync, readFileSync, copyFileSync } from "fs";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
import { TELETON_ROOT, WORKSPACE_ROOT, WORKSPACE_PATHS } from "./paths.js";
@@ -154,38 +154,3 @@ export function loadTemplate(name: string): string {
}
return readFileSync(templatePath, "utf-8");
}
-
-/**
- * Write file only if it doesn't exist
- */
-export function writeFileIfMissing(path: string, content: string): void {
- if (!existsSync(path)) {
- const dir = dirname(path);
- if (!existsSync(dir)) {
- mkdirSync(dir, { recursive: true });
- }
- writeFileSync(path, content, "utf-8");
- }
-}
-
-/**
- * Get workspace stats
- */
-export function getWorkspaceStats(workspace: Workspace): {
- exists: boolean;
- hasConfig: boolean;
- hasTemplates: boolean;
- hasSession: boolean;
- hasWallet: boolean;
-} {
- return {
- exists: existsSync(workspace.workspace),
- hasConfig: existsSync(workspace.configPath),
- hasTemplates:
- existsSync(workspace.soulPath) &&
- existsSync(workspace.memoryPath) &&
- existsSync(workspace.identityPath),
- hasSession: existsSync(workspace.sessionPath),
- hasWallet: existsSync(workspace.walletPath),
- };
-}
diff --git a/web/package-lock.json b/web/package-lock.json
index c2b84878..705cb0a2 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -26,13 +26,13 @@
}
},
"node_modules/@babel/code-frame": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
- "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/helper-validator-identifier": "^7.29.7",
"js-tokens": "^4.0.0",
"picocolors": "^1.1.1"
},
@@ -41,9 +41,9 @@
}
},
"node_modules/@babel/compat-data": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
- "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
+ "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -51,21 +51,21 @@
}
},
"node_modules/@babel/core": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
- "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
+ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.29.0",
- "@babel/generator": "^7.29.0",
- "@babel/helper-compilation-targets": "^7.28.6",
- "@babel/helper-module-transforms": "^7.28.6",
- "@babel/helpers": "^7.28.6",
- "@babel/parser": "^7.29.0",
- "@babel/template": "^7.28.6",
- "@babel/traverse": "^7.29.0",
- "@babel/types": "^7.29.0",
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helpers": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7",
"@jridgewell/remapping": "^2.3.5",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
@@ -82,14 +82,14 @@
}
},
"node_modules/@babel/generator": {
- "version": "7.29.1",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
- "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
+ "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/parser": "^7.29.0",
- "@babel/types": "^7.29.0",
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7",
"@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2"
@@ -99,14 +99,14 @@
}
},
"node_modules/@babel/helper-compilation-targets": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
- "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
+ "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/compat-data": "^7.28.6",
- "@babel/helper-validator-option": "^7.27.1",
+ "@babel/compat-data": "^7.29.7",
+ "@babel/helper-validator-option": "^7.29.7",
"browserslist": "^4.24.0",
"lru-cache": "^5.1.1",
"semver": "^6.3.1"
@@ -116,9 +116,9 @@
}
},
"node_modules/@babel/helper-globals": {
- "version": "7.28.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
- "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
+ "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -126,29 +126,29 @@
}
},
"node_modules/@babel/helper-module-imports": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
- "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
+ "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/traverse": "^7.28.6",
- "@babel/types": "^7.28.6"
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-transforms": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
- "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
+ "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/helper-module-imports": "^7.28.6",
- "@babel/helper-validator-identifier": "^7.28.5",
- "@babel/traverse": "^7.28.6"
+ "@babel/helper-module-imports": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
@@ -168,9 +168,9 @@
}
},
"node_modules/@babel/helper-string-parser": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
- "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -178,9 +178,9 @@
}
},
"node_modules/@babel/helper-validator-identifier": {
- "version": "7.28.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
- "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -188,9 +188,9 @@
}
},
"node_modules/@babel/helper-validator-option": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
- "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
+ "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -198,27 +198,27 @@
}
},
"node_modules/@babel/helpers": {
- "version": "7.29.2",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
- "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
+ "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/template": "^7.28.6",
- "@babel/types": "^7.29.0"
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
- "version": "7.29.2",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
- "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
+ "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.29.0"
+ "@babel/types": "^7.29.7"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -260,33 +260,33 @@
}
},
"node_modules/@babel/template": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
- "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
+ "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.28.6",
- "@babel/parser": "^7.28.6",
- "@babel/types": "^7.28.6"
+ "@babel/code-frame": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
- "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
+ "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.29.0",
- "@babel/generator": "^7.29.0",
- "@babel/helper-globals": "^7.28.0",
- "@babel/parser": "^7.29.0",
- "@babel/template": "^7.28.6",
- "@babel/types": "^7.29.0",
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-globals": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7",
"debug": "^4.3.1"
},
"engines": {
@@ -294,14 +294,14 @@
}
},
"node_modules/@babel/types": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
- "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
+ "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/helper-string-parser": "^7.27.1",
- "@babel/helper-validator-identifier": "^7.28.5"
+ "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
@@ -800,9 +800,9 @@
}
},
"node_modules/@remix-run/router": {
- "version": "1.23.2",
- "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
- "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==",
+ "version": "1.23.3",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.3.tgz",
+ "integrity": "sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
@@ -1328,9 +1328,9 @@
}
},
"node_modules/baseline-browser-mapping": {
- "version": "2.10.24",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.24.tgz",
- "integrity": "sha512-I2NkZOOrj2XuguvWCK6OVh9GavsNjZjK908Rq3mIBK25+GD8vPX5w2WdxVqnQ7xx3SrZJiCiZFu+/Oz50oSYSA==",
+ "version": "2.10.38",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz",
+ "integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -1375,9 +1375,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001791",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz",
- "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==",
+ "version": "1.0.30001799",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz",
+ "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==",
"dev": true,
"funding": [
{
@@ -1521,9 +1521,9 @@
}
},
"node_modules/electron-to-chromium": {
- "version": "1.5.344",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz",
- "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==",
+ "version": "1.5.376",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.376.tgz",
+ "integrity": "sha512-cUVA7/RvbFTEuw/i3obUwDTRIXojaxkResf+ibByPFxjc6XK3VNtcQXV0NSbAlJ0FMjcJGgftVVB4Qo184EXvA==",
"dev": true,
"license": "ISC"
},
@@ -2728,11 +2728,14 @@
}
},
"node_modules/node-releases": {
- "version": "2.0.38",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz",
- "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==",
+ "version": "2.0.48",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.48.tgz",
+ "integrity": "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
},
"node_modules/parse-entities": {
"version": "4.0.2",
@@ -2890,12 +2893,12 @@
}
},
"node_modules/react-router": {
- "version": "6.30.3",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz",
- "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==",
+ "version": "6.30.4",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.4.tgz",
+ "integrity": "sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==",
"license": "MIT",
"dependencies": {
- "@remix-run/router": "1.23.2"
+ "@remix-run/router": "1.23.3"
},
"engines": {
"node": ">=14.0.0"
@@ -2905,13 +2908,13 @@
}
},
"node_modules/react-router-dom": {
- "version": "6.30.3",
- "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz",
- "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
+ "version": "6.30.4",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.4.tgz",
+ "integrity": "sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==",
"license": "MIT",
"dependencies": {
- "@remix-run/router": "1.23.2",
- "react-router": "6.30.3"
+ "@remix-run/router": "1.23.3",
+ "react-router": "6.30.4"
},
"engines": {
"node": ">=14.0.0"
@@ -3316,9 +3319,9 @@
}
},
"node_modules/vite": {
- "version": "6.4.2",
- "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
- "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
+ "version": "6.4.3",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
+ "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==",
"dev": true,
"license": "MIT",
"dependencies": {
diff --git a/web/src/App.tsx b/web/src/App.tsx
index 5756204f..3cbffa64 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -20,6 +20,7 @@ const Hooks = React.lazy(() => import('./pages/Hooks').then(m => ({ default: m.H
const Logs = React.lazy(() => import('./pages/Logs').then(m => ({ default: m.Logs })));
const Conversations = React.lazy(() => import('./pages/Conversations').then(m => ({ default: m.Conversations })));
const Wallet = React.lazy(() => import('./pages/Wallet').then(m => ({ default: m.Wallet })));
+const Gocoon = React.lazy(() => import('./pages/Gocoon').then(m => ({ default: m.Gocoon })));
function App() {
// Setup route bypasses auth entirely
@@ -151,6 +152,7 @@ function RoutedContent() {
} />
} />
} />
+ } />
} />
} />
} />
diff --git a/web/src/components/AllowLists.tsx b/web/src/components/AllowLists.tsx
index 944b30e4..5e230434 100644
--- a/web/src/components/AllowLists.tsx
+++ b/web/src/components/AllowLists.tsx
@@ -41,8 +41,6 @@ export function AllowLists({ getLocal, onSave }: AllowListsProps) {
return (
-
-
value={tab}
onChange={setTab}
diff --git a/web/src/components/GocoonPanel.tsx b/web/src/components/GocoonPanel.tsx
new file mode 100644
index 00000000..0d551712
--- /dev/null
+++ b/web/src/components/GocoonPanel.tsx
@@ -0,0 +1,484 @@
+import { useEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react';
+import { api } from '../lib/api';
+import { errMsg } from '../lib/utils';
+import { toast } from '../lib/toast';
+import { RefreshButton } from './RefreshButton';
+
+interface Wallet {
+ fundAddress: string;
+ ownerAddress: string;
+ balanceTon: string;
+ balanceNano: string;
+ funded: boolean;
+ recommendedFundingTon: string;
+}
+interface GocoonStatus {
+ installed: boolean;
+ version: string | null;
+ wallet: Wallet | null;
+ runner: boolean;
+}
+type WEvent = { stage: string; status: string; message: string };
+
+// 1 TON: above this free balance the wallet is treated as provisioned, since the
+// stake (~15 TON) drops the free balance below gocoon's "funded" threshold.
+const PROVISIONED_NANO = 1_000_000_000n;
+
+// Client-side sanity check only; the server resolves the destination authoritatively.
+const RAW_TON = /^(?:EQ|UQ|kQ|0Q)[A-Za-z0-9_-]{46}$/;
+const TON_DOMAIN = /^(?:[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?\.)+ton$/i;
+const looksLikeTONDest = (s: string): boolean => {
+ const t = s.trim();
+ return RAW_TON.test(t) || TON_DOMAIN.test(t);
+};
+
+function GemIcon({ className }: { className?: string }) {
+ return (
+
+ );
+}
+function CopyIcon() {
+ return (
+
+ );
+}
+function ExternalIcon() {
+ return (
+
+ );
+}
+
+const successBadge: CSSProperties = {
+ color: 'var(--success)',
+ borderColor: 'color-mix(in srgb, var(--success) 32%, transparent)',
+};
+
+export function GocoonPanel() {
+ const [status, setStatus] = useState(null);
+ const [busy, setBusy] = useState(null);
+ const [topupAmount, setTopupAmount] = useState('');
+ const [withdrawDest, setWithdrawDest] = useState('');
+ const [confirmText, setConfirmText] = useState('');
+ const [resetText, setResetText] = useState('');
+ const [withdrawRunning, setWithdrawRunning] = useState(false);
+ const [events, setEvents] = useState([]);
+ const pollRef = useRef | null>(null);
+
+ const refresh = async (): Promise => {
+ try {
+ const r = await api.gocoonStatus();
+ if (r.success) setStatus(r.data);
+ } catch {
+ /* keep last known status */
+ }
+ };
+
+ const startWithdrawPoll = (): void => {
+ if (pollRef.current) clearInterval(pollRef.current);
+ pollRef.current = setInterval(async () => {
+ const s = await api.gocoonWithdrawStatus();
+ if (!s.success) return;
+ setEvents(s.data.events.map((e) => ({ stage: e.stage, status: e.status, message: e.message })));
+ if (s.data.done) {
+ if (pollRef.current) clearInterval(pollRef.current);
+ pollRef.current = null;
+ setWithdrawRunning(false);
+ setConfirmText('');
+ if (s.data.error) toast.error(s.data.error);
+ else toast.success('Withdraw complete');
+ void refresh();
+ }
+ }, 1500);
+ };
+
+ useEffect(() => {
+ void refresh();
+ // Resume an in-flight withdraw after navigation / reload (the job lives server-side).
+ void (async () => {
+ try {
+ const s = await api.gocoonWithdrawStatus();
+ if (!s.success) return;
+ if (s.data.events.length > 0)
+ setEvents(s.data.events.map((e) => ({ stage: e.stage, status: e.status, message: e.message })));
+ if (s.data.running) {
+ setWithdrawRunning(true);
+ startWithdrawPoll();
+ }
+ } catch {
+ /* no in-flight withdraw */
+ }
+ })();
+ return () => {
+ if (pollRef.current) clearInterval(pollRef.current);
+ };
+ }, []);
+
+ const w = status?.wallet ?? null;
+ const running = !!status?.runner;
+ const balNano = w ? BigInt(w.balanceNano || '0') : 0n;
+ const hasFunds = !!w && (w.funded || balNano > PROVISIONED_NANO);
+ const awaitingDeposit = !!status?.installed && !!w && !running && !hasFunds;
+ const destValid = looksLikeTONDest(withdrawDest);
+
+ // While waiting for the first deposit, poll so the balance is auto-detected.
+ useEffect(() => {
+ if (!awaitingDeposit) return;
+ const t = setInterval(() => void refresh(), 5000);
+ return () => clearInterval(t);
+ }, [awaitingDeposit]);
+
+ const run = async (label: string, fn: () => Promise): Promise => {
+ setBusy(label);
+ try {
+ await fn();
+ } catch (err) {
+ toast.error(errMsg(err));
+ } finally {
+ setBusy(null);
+ }
+ };
+
+ const onInstall = (): Promise =>
+ run('install', async () => {
+ const r = await api.gocoonInstall();
+ toast.success(`Installed gocoon ${r.data.version}`);
+ await refresh();
+ });
+
+ const onSetup = (): Promise =>
+ run('setup', async () => {
+ await api.gocoonInit();
+ await refresh();
+ });
+
+ const onTopup = (): Promise =>
+ run('topup', async () => {
+ if (!topupAmount.trim()) return;
+ await api.gocoonTopup(topupAmount.trim());
+ toast.success(`Topped up ${topupAmount} TON`);
+ setTopupAmount('');
+ await refresh();
+ });
+
+ const onWithdraw = (): Promise =>
+ run('withdraw', async () => {
+ const dest = withdrawDest.trim();
+ if (!dest || !looksLikeTONDest(dest)) return;
+ await api.gocoonWithdrawStart(dest);
+ setEvents([]);
+ setWithdrawRunning(true);
+ startWithdrawPoll();
+ });
+
+ const onStopRunner = (): Promise =>
+ run('stopRunner', async () => {
+ await api.gocoonRunnerStop();
+ await refresh();
+ });
+
+ const onReset = (): Promise =>
+ run('reset', async () => {
+ await api.gocoonReset();
+ setResetText('');
+ toast.success('Wallet reset, create a fresh one below');
+ await refresh();
+ });
+
+ const copy = (s: string): void => {
+ void navigator.clipboard?.writeText(s).then(() => toast.success('Address copied')).catch(() => {});
+ };
+
+ if (!status) {
+ return (
+
+ );
+ }
+
+ const heroBadge: { text: string; cls?: string; style?: CSSProperties } = running
+ ? { text: 'Running', style: successBadge }
+ : hasFunds
+ ? { text: 'Ready', style: successBadge }
+ : { text: 'Awaiting funds', cls: 'warn' };
+
+ return (
+ <>
+
+
+ {!status.installed ? (
+
gocoon is not installed yet
+ ) : !w ? (
+
No funding wallet yet, create one below
+ ) : (
+ <>
+
+ COCOON Wallet
+
+ {heroBadge.text}
+
+
+
+ {w.balanceTon}
+ TON
+
+
+
{w.fundAddress}
+
+
+
+
+
+ >
+ )}
+
+
+
+
+
+
+ {!status.installed && (
+
+ {busy === 'install' ? 'Installing...' : 'Install gocoon'}
+
+ }
+ />
+ )}
+ {status.installed && !w && (
+
+ {busy === 'setup' ? 'Creating...' : 'Create funding wallet'}
+
+ }
+ />
+ )}
+ {awaitingDeposit && (
+
+ Waiting for funds
+
+ }
+ />
+ )}
+ {!running && hasFunds && (
+ Funded}
+ />
+ )}
+ {running && (
+ live}
+ />
+ )}
+
+
+
+ Runner{' '}
+ {running ? 'running' : 'stopped'}
+
+ {status.installed && status.version && gocoon {status.version}}
+
+ void refresh()} />
+
+
+
+
+
+ {w && (
+
+
+ Manage
+
+
+ {hasFunds && (
+
+
+
+ setTopupAmount(e.target.value)}
+ />
+
+
+
+ {running
+ ? 'Adds stake to the channel.'
+ : 'Start the agent first — top up needs the runner active.'}
+
+
+ )}
+
+
+
+ {withdrawRunning ? (
+
+ Withdraw in progress (~3 min): closing channel and draining wallets.
+
+ ) : (
+ <>
+ {running && (
+
+
+ The agent is running. Stop it before withdrawing.
+
+
+
+ )}
+
setWithdrawDest(e.target.value)}
+ />
+ {withdrawDest.trim() && !destValid && (
+
+ Doesn't look like a TON address or .ton domain.
+
+ )}
+
setConfirmText(e.target.value)}
+ style={{ marginTop: 8 }}
+ />
+
+
+ Closes the channel and drains the COCOON + agent wallets to the address above. Irreversible.
+
+ >
+ )}
+ {events.length > 0 && (
+
+ {events.map((e, i) => (
+
+ {e.status === 'ok' ? '[ok]' : e.status === 'error' ? '[x]' : '[..]'} [{e.stage}] {e.message}
+
+ ))}
+
+ )}
+
+
+ {!hasFunds && !running && (
+
+
+
setResetText(e.target.value)}
+ />
+
+
+ Deletes the local COCOON wallet + config so a fresh one is created on the next setup. Withdraw everything first. Irreversible.
+
+
+ )}
+
+
+ )}
+ >
+ );
+}
+
+function Steps({ installed, hasWallet, funded, running }: {
+ installed: boolean;
+ hasWallet: boolean;
+ funded: boolean;
+ running: boolean;
+}) {
+ const steps = [
+ { label: 'Install', done: installed },
+ { label: 'Wallet', done: hasWallet },
+ { label: 'Fund', done: funded },
+ { label: 'Run', done: running },
+ ];
+ const current = steps.findIndex((s) => !s.done);
+ return (
+
+ {steps.map((s, i) => {
+ const state = s.done ? 'done' : i === current ? 'current' : 'pending';
+ return (
+
+ {s.done ? '✓' : i + 1}
+ {s.label}
+ {i < steps.length - 1 && }
+
+ );
+ })}
+
+ );
+}
+
+function Action({ title, hint, right }: { title: string; hint: string; right: ReactNode }) {
+ return (
+
+ );
+}
diff --git a/web/src/components/Layout.tsx b/web/src/components/Layout.tsx
index 6f1fc7b6..e082989e 100644
--- a/web/src/components/Layout.tsx
+++ b/web/src/components/Layout.tsx
@@ -70,6 +70,16 @@ function IconWallet() {
);
}
+function IconGocoon() {
+ return (
+
+ );
+}
+
function IconWorkspace() {
return (
)}
- {selected && !selected.requiresApiKey && selected.id === 'cocoon' && (
+ {selected && !selected.requiresApiKey && selected.id === 'gocoon' && (
- Cocoon Network uses a local proxy. No API key required.
+ Gocoon runs a local decentralized LLM on TON. No API key required.
-
+
onChange({ ...data, cocoonPort: parseInt(e.target.value) || 0 })}
+ value={data.gocoonPort}
+ onChange={(e) => onChange({ ...data, gocoonPort: parseInt(e.target.value) || 0 })}
min={1}
max={65535}
className="w-full"
/>
- Port where the Cocoon client proxy is running (1-65535).
+ Port where the gocoon runner is listening (1-65535).
@@ -182,7 +182,7 @@ export function ProviderStep({ data, onChange }: StepProps) {
)}
- {selected && selected.id !== 'cocoon' && selected.id !== 'local' && (
+ {selected && selected.id !== 'gocoon' && selected.id !== 'local' && (
{loadingModels ? (
diff --git a/web/src/components/setup/SetupContext.tsx b/web/src/components/setup/SetupContext.tsx
index af65838a..aecd6c3f 100644
--- a/web/src/components/setup/SetupContext.tsx
+++ b/web/src/components/setup/SetupContext.tsx
@@ -27,7 +27,7 @@ export interface WizardData {
agentName: string;
provider: string;
apiKey: string;
- cocoonPort: number;
+ gocoonPort: number;
localUrl: string;
apiId: number;
apiHash: string;
@@ -71,7 +71,7 @@ const DEFAULTS: WizardData = {
agentName: 'Nova',
provider: '',
apiKey: '',
- cocoonPort: 11435,
+ gocoonPort: 10000,
localUrl: 'http://localhost:11434/v1',
apiId: 0,
apiHash: '',
@@ -113,8 +113,8 @@ function validateStep(step: number, data: WizardData): boolean {
return data.riskAccepted;
case 1:
if (!data.provider) return false;
- if (data.provider === 'cocoon') {
- return data.cocoonPort >= 1 && data.cocoonPort <= 65535;
+ if (data.provider === 'gocoon') {
+ return data.gocoonPort >= 1 && data.gocoonPort <= 65535;
}
if (data.provider === 'local') {
try { new URL(data.localUrl); return true; }
@@ -123,7 +123,7 @@ function validateStep(step: number, data: WizardData): boolean {
return data.apiKey.length > 0;
case 2: {
// Config
- if (data.provider !== 'cocoon' && data.provider !== 'local') {
+ if (data.provider !== 'gocoon' && data.provider !== 'local') {
const modelValue = data.model === '__custom__' ? data.customModel : data.model;
if (!modelValue) return false;
}
@@ -204,7 +204,7 @@ export function SetupProvider({ children }: { children: ReactNode }) {
return {
agent: {
provider: data.provider,
- ...(data.provider !== 'cocoon' && data.provider !== 'local' && data.apiKey ? { api_key: data.apiKey } : {}),
+ ...(data.provider !== 'gocoon' && data.provider !== 'local' && data.apiKey ? { api_key: data.apiKey } : {}),
...(data.provider === 'local' ? { base_url: data.localUrl } : {}),
...(resolvedModel ? { model: resolvedModel } : {}),
max_agentic_iterations: data.maxIterations,
@@ -222,7 +222,7 @@ export function SetupProvider({ children }: { children: ReactNode }) {
...(data.botToken ? { bot_token: data.botToken } : {}),
...(data.botUsername ? { bot_username: data.botUsername } : {}),
},
- ...(data.provider === 'cocoon' ? { cocoon: { port: data.cocoonPort } } : {}),
+ ...(data.provider === 'gocoon' ? { gocoon: { port: data.gocoonPort } } : {}),
deals: {
enabled: !!data.botToken,
...(data.customizeThresholds
diff --git a/web/src/index.css b/web/src/index.css
index d1d9c897..a7fe05e6 100644
--- a/web/src/index.css
+++ b/web/src/index.css
@@ -371,8 +371,6 @@ body {
.dashboard-root {
display: flex;
flex-direction: column;
- height: calc(100vh - 64px);
- min-height: 0;
}
.dashboard-settings {
@@ -2869,61 +2867,110 @@ button.btn-destructive:hover { background: var(--red); opacity: 0.9; }
.task-detail-actions { display: flex; gap: 8px; margin-top: 2px; }
/* ── Dashboard ── */
-.dash-hero { display: flex; align-items: center; gap: 14px; padding: 16px 18px; }
-.dash-orb { position: relative; width: 12px; height: 12px; border-radius: 50%; background: var(--green); flex-shrink: 0; }
+/* ── Command-center top row ── */
+.dash-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px; align-items: stretch; }
+@media (max-width: 720px) { .dash-grid { grid-template-columns: 1fr; } }
+
+/* Card header: title + optional description, with right-aligned status/action */
+.dash-head { display: flex; align-items: center; gap: 11px; margin-bottom: 18px; }
+.dash-head-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
+.dash-head-title { font-size: var(--font-base); font-weight: 600; letter-spacing: -0.2px; line-height: 1.15; color: var(--text-primary); }
+.dash-head-desc { font-size: var(--font-xs); color: var(--text-secondary); }
+.dash-head-right { margin-left: auto; display: flex; align-items: center; flex-shrink: 0; }
+
+/* Live status badge */
+.dash-status {
+ display: inline-flex; align-items: center; gap: 6px;
+ font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;
+ color: var(--green); background: var(--green-dim); padding: 4px 10px; border-radius: var(--radius-pill);
+}
+.dash-orb { position: relative; width: 7px; height: 7px; border-radius: 50%; background: var(--green); flex-shrink: 0; }
.dash-orb::after {
content: ''; position: absolute; inset: 0; border-radius: 50%; background: var(--green);
animation: orb-ping 1.9s ease-out infinite;
}
@keyframes orb-ping { 0% { opacity: 0.55; transform: scale(1); } 100% { opacity: 0; transform: scale(3.4); } }
-.dash-hero-main { flex: 1; min-width: 0; }
-.dash-hero-title { display: flex; align-items: center; gap: 10px; font-size: var(--font-lg); font-weight: 600; color: var(--text-primary); }
-.dash-hero-state {
+
+/* Agent card */
+.dash-agent { display: flex; flex-direction: column; }
+.dash-agent-model-name { display: block; font-size: var(--font-2xl); font-weight: 700; letter-spacing: -0.6px; line-height: 1.1; color: var(--text-primary); }
+.dash-agent-provider { display: block; margin-top: 5px; font-size: var(--font-sm); color: var(--text-secondary); }
+.dash-agent-selects { display: flex; gap: 12px; margin-top: auto; padding-top: 18px; border-top: 1px solid var(--border-glass); }
+.dash-hero-field { display: flex; flex-direction: column; gap: 6px; flex: 1; min-width: 0; }
+.dash-hero-label {
font-size: var(--font-xs); font-weight: 600; text-transform: uppercase; letter-spacing: 0.4px;
- color: var(--green); background: var(--green-dim); padding: 2px 8px; border-radius: var(--radius-pill);
+ color: var(--text-tertiary); padding-left: 2px;
+}
+
+/* Usage card */
+.dash-usage { display: flex; flex-direction: column; }
+.dash-usage-hero { display: flex; align-items: baseline; gap: 10px; }
+.dash-usage-num { font-size: var(--font-2xl); font-weight: 700; letter-spacing: -0.8px; line-height: 1; color: var(--text-primary); font-variant-numeric: tabular-nums; }
+.dash-usage-cost { font-size: var(--font-sm); color: var(--text-secondary); }
+.dash-metrics { display: flex; margin-top: auto; padding-top: 18px; border-top: 1px solid var(--border-glass); }
+.dash-metric {
+ flex: 1; display: flex; flex-direction: column; gap: 3px; align-items: flex-start; text-align: left;
+ height: auto; padding: 0 14px; background: none; color: inherit; border: none; border-radius: 0;
+}
+.dash-metric:first-child { padding-left: 0; }
+.dash-metric + .dash-metric { border-left: 1px solid var(--border-glass); }
+.dash-metric.clickable:hover .dash-metric-v { color: var(--accent); }
+.dash-metric-v { font-size: var(--font-xl); font-weight: 600; color: var(--text-primary); font-variant-numeric: tabular-nums; }
+.dash-metric-k { font-size: var(--font-xs); color: var(--text-tertiary); text-transform: uppercase; letter-spacing: 0.4px; }
+
+/* GRAM chip in the usage header */
+.dash-gram {
+ display: inline-flex; align-items: center; gap: 6px;
+ height: auto; padding: 5px 10px; background: var(--accent-subtle); color: var(--text-primary);
+ border: 1px solid transparent; border-radius: var(--radius-pill);
+ transition: border-color var(--duration-color) var(--easing-standard);
}
-.dash-hero-sub { font-size: var(--font-sm); color: var(--text-secondary); margin-top: 3px; }
-.dash-hero-selects { display: flex; gap: 12px; flex-shrink: 0; }
-.dash-hero-field { display: flex; flex-direction: column; gap: 5px; width: 168px; }
-.dash-hero-field.model { width: 232px; }
-.dash-hero-label {
- font-size: var(--font-xs);
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.3px;
- color: var(--text-tertiary);
- padding-left: 2px;
+.dash-gram:hover { background: var(--accent-subtle); border-color: var(--accent); }
+.dash-gram-glyph { width: 15px; height: 15px; color: var(--accent); flex-shrink: 0; }
+.dash-gram-amt { font-size: var(--font-sm); font-weight: 600; color: var(--text-primary); font-variant-numeric: tabular-nums; }
+.dash-gram-unit { font-size: var(--font-xs); font-weight: 600; color: var(--accent); }
+
+/* Recent activity */
+.dash-activity { display: flex; flex-direction: column; }
+.dash-activity-sub { font-size: var(--font-sm); color: var(--text-tertiary); }
+.dash-activity-list { display: flex; flex-direction: column; }
+.dash-activity-row {
+ display: flex; align-items: center; gap: 12px; width: 100%; text-align: left;
+ height: auto; padding: 8px 8px; background: none; color: inherit;
+ border: none; border-radius: var(--radius-sm);
+ transition: background var(--duration-color) var(--easing-standard);
}
-
-/* Compact menu-bar-style stats strip */
-.dash-statbar {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- gap: 2px 6px;
- padding: 6px 10px;
- margin-bottom: 16px;
- background: var(--bg-secondary);
- border: 1px solid var(--border-glass);
- border-radius: var(--radius-pill);
+.dash-activity-row:hover { background: var(--glass-thin); }
+.dash-activity-ava {
+ width: 34px; height: 34px; border-radius: 50%; flex-shrink: 0;
+ display: flex; align-items: center; justify-content: center;
+ font-size: var(--font-sm); font-weight: 600; color: var(--accent); background: var(--accent-dim);
}
-.stat-item {
- display: inline-flex;
- align-items: baseline;
- gap: 5px;
- white-space: nowrap;
- padding: 3px 9px;
- border-radius: var(--radius-pill);
+.dash-activity-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
+.dash-activity-name { font-size: var(--font-md); font-weight: 500; color: var(--text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.dash-activity-snip { font-size: var(--font-sm); color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.dash-activity-time { font-size: var(--font-xs); color: var(--text-tertiary); flex-shrink: 0; white-space: nowrap; font-variant-numeric: tabular-nums; }
+.dash-activity-all {
+ align-self: flex-start; margin-top: 6px; height: auto; padding: 6px 10px;
+ font-size: var(--font-sm); font-weight: 500; color: var(--accent);
+ background: none; border: none; border-radius: var(--radius-sm);
}
-.stat-item .stat-v { font-size: var(--font-sm); font-weight: 600; color: var(--text-primary); }
-.stat-item .stat-v.mono { font-family: var(--font-mono); }
-.stat-item .stat-k { font-size: var(--font-xs); color: var(--text-tertiary); text-transform: uppercase; letter-spacing: 0.3px; }
-.stat-item.clickable { cursor: pointer; transition: background var(--duration-color) var(--easing-standard); }
-.stat-item.clickable:hover { background: var(--glass-thin); }
-.stat-item.clickable:hover .stat-k { color: var(--text-secondary); }
+.dash-activity-all:hover { background: var(--accent-subtle); }
+
+.dashboard-settings { margin-bottom: 16px; align-items: stretch; }
+
+/* Settings cards */
+.dash-settings { display: flex; flex-direction: column; }
+.dash-settings .allowlist-rows { min-height: 72px; }
-.dashboard-settings { margin-bottom: 16px; }
-.dash-policy { display: grid; gap: 16px; margin-top: 16px; }
+/* Access Policy: stacked label + pill rows, divided */
+.dash-policy { display: flex; flex-direction: column; flex: 1; }
+.dash-policy-row { display: flex; flex-direction: column; gap: 10px; padding: 16px 0; }
+.dash-policy-row + .dash-policy-row { border-top: 1px solid var(--border-glass); }
+.dash-policy-label {
+ display: flex; align-items: center; gap: 6px;
+ font-size: var(--font-md); font-weight: 500; color: var(--text-primary);
+}
/* ── Allow Lists — iOS grouped list, one ID per row (Figma 6023:2101) ── */
.dash-card-fill { display: flex; flex-direction: column; }
@@ -3195,3 +3242,85 @@ select:focus-visible,
color: var(--accent);
box-shadow: var(--shadow-sm);
}
+
+/* Gocoon page */
+.gocoon-steps {
+ display: flex;
+ align-items: center;
+ padding: var(--space-md) var(--space-lg);
+ margin-bottom: var(--space-lg);
+ background: var(--bg-secondary);
+ border: 1px solid var(--border-glass);
+ border-radius: var(--radius-card);
+}
+.gocoon-step {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex: 1;
+ min-width: 0;
+}
+.gocoon-step:last-child {
+ flex: 0 0 auto;
+}
+.gocoon-step-dot {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+ border-radius: var(--radius-pill);
+ font-size: var(--font-xs);
+ font-weight: 700;
+ flex-shrink: 0;
+ border: 1.5px solid var(--border);
+ background: var(--bg-secondary);
+ color: var(--text-tertiary);
+ transition: all var(--duration-color) var(--easing-standard);
+}
+.gocoon-step-dot.done {
+ background: var(--accent);
+ border-color: var(--accent);
+ color: var(--text-on-accent);
+}
+.gocoon-step-dot.current {
+ border-color: var(--accent);
+ color: var(--accent);
+}
+.gocoon-step-label {
+ font-size: var(--font-sm);
+ font-weight: 600;
+ color: var(--text-tertiary);
+ white-space: nowrap;
+}
+.gocoon-step-label.done,
+.gocoon-step-label.current {
+ color: var(--text-primary);
+}
+.gocoon-step-line {
+ flex: 1;
+ height: 1.5px;
+ margin: 0 10px;
+ min-width: 14px;
+ background: var(--border);
+ border-radius: 2px;
+}
+.gocoon-step-line.done {
+ background: var(--accent);
+}
+.gocoon-action {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--space-lg);
+}
+.gocoon-meta {
+ display: flex;
+ align-items: center;
+ gap: var(--space-md);
+ margin-top: var(--space-lg);
+ padding-top: var(--space-md);
+ border-top: 1px solid var(--border-glass);
+ font-size: var(--font-sm);
+ color: var(--text-secondary);
+}
diff --git a/web/src/lib/a11y.ts b/web/src/lib/a11y.ts
deleted file mode 100644
index 43d20e13..00000000
--- a/web/src/lib/a11y.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import type { KeyboardEvent } from 'react';
-
-/**
- * Returns the a11y props needed to make a non-button element keyboard-activatable.
- * Handles Enter and Space exactly like a native button would.
- *
- * Usage:
- *
- */
-export function expandableRowProps(onActivate: () => void): {
- onKeyDown: (e: KeyboardEvent) => void;
- tabIndex: number;
- role: string;
-} {
- return {
- onKeyDown: (e: KeyboardEvent) => {
- if (e.key === 'Enter' || e.key === ' ') {
- e.preventDefault();
- onActivate();
- }
- },
- tabIndex: 0,
- role: 'button',
- };
-}
diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts
index 07441445..ab444656 100644
--- a/web/src/lib/api.ts
+++ b/web/src/lib/api.ts
@@ -81,7 +81,7 @@ export interface SetupConfig {
bot_token?: string;
bot_username?: string;
};
- cocoon?: { port: number };
+ gocoon?: { port: number };
deals?: { enabled?: boolean; buy_max_floor_percent?: number; sell_min_floor_percent?: number };
tonapi_key?: string;
toncenter_api_key?: string;
@@ -392,6 +392,32 @@ export const api = {
return fetchAPI>('/status');
},
+ // gocoon: decentralized LLM on TON
+ async gocoonStatus() {
+ return fetchAPI>('/gocoon/status');
+ },
+ async gocoonInstall() {
+ return fetchAPI>('/gocoon/install', { method: 'POST' });
+ },
+ async gocoonInit() {
+ return fetchAPI>('/gocoon/init', { method: 'POST' });
+ },
+ async gocoonTopup(amount: string) {
+ return fetchAPI>('/gocoon/topup', { method: 'POST', body: JSON.stringify({ amount }) });
+ },
+ async gocoonWithdrawStart(destination: string) {
+ return fetchAPI>('/gocoon/withdraw', { method: 'POST', body: JSON.stringify({ destination }) });
+ },
+ async gocoonWithdrawStatus() {
+ return fetchAPI>('/gocoon/withdraw');
+ },
+ async gocoonRunnerStop() {
+ return fetchAPI>('/gocoon/runner/stop', { method: 'POST' });
+ },
+ async gocoonReset() {
+ return fetchAPI>('/gocoon/reset', { method: 'POST' });
+ },
+
async getTools() {
return fetchAPI>('/tools');
},
@@ -443,10 +469,6 @@ export const api = {
return fetchAPI>('/plugins');
},
- async getPluginPriorities() {
- return fetchAPI>>('/plugins/priorities');
- },
-
async setPluginPriority(pluginName: string, priority: number) {
return fetchAPI>('/plugins/priorities', {
method: 'POST',
@@ -454,12 +476,6 @@ export const api = {
});
},
- async resetPluginPriority(pluginName: string) {
- return fetchAPI>(`/plugins/priorities/${encodeURIComponent(pluginName)}`, {
- method: 'DELETE',
- });
- },
-
async getToolRag() {
return fetchAPI>('/tools/rag');
},
@@ -551,10 +567,6 @@ export const api = {
return fetchAPI>(`/tasks${qs}`);
},
- async tasksGet(id: string) {
- return fetchAPI>(`/tasks/${id}`);
- },
-
async tasksDelete(_id: string) {
return fetchAPI>(`/tasks/${_id}`, { method: 'DELETE' });
},
@@ -570,10 +582,6 @@ export const api = {
});
},
- async tasksCleanDone() {
- return fetchAPI>('/tasks/clean-done', { method: 'POST' });
- },
-
async getConfigKeys() {
return fetchAPI>('/config');
},
@@ -585,12 +593,6 @@ export const api = {
});
},
- async unsetConfigKey(key: string) {
- return fetchAPI>(`/config/${key}`, {
- method: 'DELETE',
- });
- },
-
async getModelsForProvider(provider: string) {
return fetchAPI>>(`/config/models/${encodeURIComponent(provider)}`);
},
diff --git a/web/src/lib/log-store.ts b/web/src/lib/log-store.ts
index 7c7b4597..13f6ffb1 100644
--- a/web/src/lib/log-store.ts
+++ b/web/src/lib/log-store.ts
@@ -1,7 +1,5 @@
import { api, LogEntry } from './api';
-export type { LogEntry };
-
type Listener = () => void;
const MAX_LOGS = 2000;
diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts
index fc1485d1..dd86ef46 100644
--- a/web/src/lib/utils.ts
+++ b/web/src/lib/utils.ts
@@ -9,6 +9,17 @@ export function formatDate(input: string | number | null | undefined, epochScale
return date.toLocaleDateString('fr-FR', { year: 'numeric', month: 'short', day: 'numeric' });
}
+/** Compact relative time from a unix-seconds epoch: now, 5m, 3h, 2d, else date. */
+export function timeAgo(epochSeconds: number | null | undefined): string {
+ if (epochSeconds == null) return '—';
+ const diff = Date.now() / 1000 - epochSeconds;
+ if (diff < 45) return 'now';
+ if (diff < 3600) return `${Math.round(diff / 60)}m`;
+ if (diff < 86400) return `${Math.round(diff / 3600)}h`;
+ if (diff < 604800) return `${Math.round(diff / 86400)}d`;
+ return formatDate(epochSeconds, 1000);
+}
+
export function formatDateTime(input: string | number | null | undefined, epochScale = 1): string {
if (input == null) return '\u2014';
const date = typeof input === 'number' ? new Date(input * epochScale) : new Date(input);
diff --git a/web/src/pages/Config.tsx b/web/src/pages/Config.tsx
index 3e486484..fdb3da61 100644
--- a/web/src/pages/Config.tsx
+++ b/web/src/pages/Config.tsx
@@ -143,29 +143,6 @@ export function Config() {
handleProviderCancel={config.handleProviderCancel}
/>
-
- {config.getLocal('agent.provider') === 'cocoon' && (
- <>
- Cocoon
-
- config.setLocal('cocoon.port', v)}
- onSave={(v) => config.saveConfig('cocoon.port', v)}
- onCancel={() => config.cancelLocal('cocoon.port')}
- min={1}
- max={65535}
- placeholder="11434"
- hotReload="restart"
- />
-
- >
- )}
>
)}
diff --git a/web/src/pages/Dashboard.tsx b/web/src/pages/Dashboard.tsx
index 0dba2c0a..f66c4a13 100644
--- a/web/src/pages/Dashboard.tsx
+++ b/web/src/pages/Dashboard.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useState } from 'react';
+import { useEffect, useState, type ReactNode } from 'react';
import { useNavigate } from 'react-router-dom';
import { useConfigState } from '../hooks/useConfigState';
import { POLICY_OPTIONS } from '../components/TelegramSettingsPanel';
@@ -8,9 +8,10 @@ import { PillTabs } from '../components/PillTabs';
import { InfoTip } from '../components/InfoTip';
import { Select } from '../components/Select';
import { ProviderSwitchZone, PROVIDER_OPTIONS, PROVIDER_LABELS } from '../components/ProviderControl';
-import { api, StatusData } from '../lib/api';
-import { errMsg } from '../lib/utils';
-import { Skeleton } from '../components/Skeleton';
+import { api, StatusData, ConversationChat } from '../lib/api';
+import { errMsg, timeAgo } from '../lib/utils';
+import { Skeleton, SkeletonRows } from '../components/Skeleton';
+import { EmptyState } from '../components/EmptyState';
import { Alert } from '../components/Alert';
const PLATFORM_LABEL: Record = { darwin: 'macOS', linux: 'Linux', win32: 'Windows' };
@@ -20,18 +21,54 @@ function fmtUptime(sec: number): string {
return `${Math.floor(sec / 3600)}h ${Math.floor((sec % 3600) / 60)}m`;
}
-function StatItem({ label, value, mono, to }: { label: string; value: string | number; mono?: boolean; to?: string }) {
+function providerLabel(provider: string): string {
+ const i = PROVIDER_OPTIONS.indexOf(provider);
+ return i >= 0 ? PROVIDER_LABELS[i] : provider;
+}
+
+function CardHead({ title, desc, right }: { title: string; desc?: string; right?: ReactNode }) {
+ return (
+
+
+ {title}
+ {desc && {desc}}
+
+ {right &&
{right}
}
+
+ );
+}
+
+function StatusBadge() {
+ return (
+
+
+ Running
+
+ );
+}
+
+function GramGlyph() {
+ return (
+
+ );
+}
+
+function Metric({ label, value, to }: { label: string; value: string | number; to?: string }) {
const navigate = useNavigate();
const clickable = !!to;
return (
- navigate(to) : undefined}
- {...(clickable ? { role: 'button', tabIndex: 0, onKeyDown: (e: React.KeyboardEvent) => { if (e.key === 'Enter') navigate(to); } } : {})}
>
- {value}
- {label}
-
+ {value}
+ {label}
+
);
}
@@ -45,6 +82,7 @@ export function Dashboard() {
handleProviderChange, handleProviderConfirm, handleProviderCancel,
loadData,
} = useConfigState();
+ const navigate = useNavigate();
const handleArraySave = async (key: string, values: string[]) => {
try {
@@ -55,14 +93,20 @@ export function Dashboard() {
}
};
- // Poll /api/status every 10s for live metrics (tokens, uptime).
+ // Live metrics (tokens, uptime) + wallet balance + recent chats.
const [liveStatus, setLiveStatus] = useState(null);
const [balance, setBalance] = useState(null);
+ const [recent, setRecent] = useState(null);
useEffect(() => {
let active = true;
const poll = () => api.getStatus().then((r) => { if (active) setLiveStatus(r.data); }).catch(() => {});
const id = setInterval(poll, 10_000);
api.getWallet().then((r) => { if (active) setBalance(r.data?.balance ?? null); }).catch(() => {});
+ api.getConversations().then((r) => {
+ if (!active) return;
+ const chats = (r.data ?? []).slice().sort((a, b) => (b.last_message_at ?? 0) - (a.last_message_at ?? 0));
+ setRecent(chats);
+ }).catch(() => {});
return () => { active = false; clearInterval(id); };
}, []);
@@ -70,8 +114,11 @@ export function Dashboard() {
return (
);
}
@@ -79,6 +126,11 @@ export function Dashboard() {
const s = liveStatus ?? status;
const platform = s.platform ? (PLATFORM_LABEL[s.platform] ?? s.platform) : null;
+ const provider = pendingProvider ?? getLocal('agent.provider');
+ const modelLabel = modelOptions.find((m) => m.value === getLocal('agent.model'))?.name ?? getLocal('agent.model');
+ const tokens = s.tokenUsage ? `${(s.tokenUsage.totalTokens / 1000).toFixed(1)}K` : '0';
+ const cost = s.tokenUsage ? `$${s.tokenUsage.totalCost.toFixed(3)}` : '$0.000';
+ const recentTop = (recent ?? []).slice(0, 7);
return (
@@ -86,42 +138,59 @@ export function Dashboard() {
{error &&
setError(null)} style={{ marginBottom: '14px' }} />}
- {/* ── Status hero ── */}
-
-
-
-
- Agent
-
Running
+
+ {/* ── Agent ── */}
+
+
} />
+
+ {modelLabel}
+
+ {[providerLabel(provider), `up ${fmtUptime(s.uptime)}`, platform].filter(Boolean).join(' · ')}
+
-
- {[`up ${fmtUptime(s.uptime)}`, platform].filter(Boolean).join(' · ')}
+
+
+ Provider
+
+
+
+ Model
+
-
-
-
Provider
-
+
+ {/* ── Usage ── */}
+
+
navigate('/wallet')}>
+
+ {balance ?? '—'}
+ GRAM
+
+ }
+ />
+
+ {tokens}
+ {cost} spent
-
-
Model
-
{pendingProvider && pendingMeta && (
-
+
)}
- {/* ── Metrics ── */}
-
-
-
-
-
-
-
-
-
+ {/* ── Recent activity ── */}
+
+
{stats.messages.toLocaleString()} messages}
+ />
+ {recent === null ? (
+
+ ) : recentTop.length === 0 ? (
+
+ ) : (
+ <>
+
+ {recentTop.map((c) => {
+ const name = c.title || c.username || c.id;
+ return (
+
+ );
+ })}
+
+ {recent.length > recentTop.length && (
+
+ )}
+ >
+ )}
- {/* ── Settings (side by side) ── */}
+ {/* ── Settings ── */}
-
-
+
+
-
-
+
+
saveConfig('telegram.dm_policy', v)} ariaLabel="DM policy" />
-
-
+
+
saveConfig('telegram.group_policy', v)} ariaLabel="Group policy" />
-
+
{s.platform === 'linux' && (
diff --git a/web/src/pages/Gocoon.tsx b/web/src/pages/Gocoon.tsx
new file mode 100644
index 00000000..bdc237f6
--- /dev/null
+++ b/web/src/pages/Gocoon.tsx
@@ -0,0 +1,13 @@
+import { GocoonPanel } from '../components/GocoonPanel';
+
+export function Gocoon() {
+ return (
+
+
+
Gocoon
+
Decentralized LLM on TON. Install, fund, top up, and withdraw.
+
+
+
+ );
+}
diff --git a/web/src/pages/Setup.tsx b/web/src/pages/Setup.tsx
index b3471732..ab8e7bd0 100644
--- a/web/src/pages/Setup.tsx
+++ b/web/src/pages/Setup.tsx
@@ -10,7 +10,7 @@ import { getSteps, useSetup } from '../components/setup/SetupContext';
import { setup } from '../lib/api';
// Re-export types for step components that import from here
-export type { WizardData, StepProps } from '../components/setup/SetupContext';
+export type { StepProps } from '../components/setup/SetupContext';
const ALL_STEP_COMPONENTS: Record
= {
welcome: WelcomeStep,