Compare commits
2 Commits
4db82f78a6
...
48011b2ca6
@@ -5,9 +5,13 @@ ZAI_API_KEY=your_zai_api_key_here
|
||||
|
||||
# Telegram Bot Configuration
|
||||
# Get your bot token from @BotFather
|
||||
TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here
|
||||
TELEGRAM_BOT_TOKEN=your_t...here
|
||||
TELEGRAM_ALLOWED_USERS=your_telegram_user_id_here
|
||||
|
||||
# Discord Bot Configuration
|
||||
# Get your bot token from Discord Developer Portal
|
||||
DISCORD_TOKEN=your_discord_bot_token_here
|
||||
|
||||
# zCode CLI X Configuration
|
||||
ZCODE_ENABLE_BASH=true
|
||||
ZCODE_ENABLE_FILE_EDIT=true
|
||||
|
||||
119
README.md
119
README.md
@@ -7,11 +7,13 @@ Agentic coder with **Z.AI + Telegram integration** — Claude Code + Hermes in o
|
||||
## 🚀 Features
|
||||
|
||||
- **🤖 AI-Powered Code Generation**: Powered by Z.AI GLM-5.1 (Coding Plan)
|
||||
- **📱 Telegram Bot**: 24/7 live interaction via Telegram
|
||||
- **📱 Multi-Channel Bot**: Telegram + Discord with real-time delivery
|
||||
- **🛠️ Full Engineering Access**: Bash, FileEdit, WebSearch, Git tools
|
||||
- **🧠 Agent System**: Code reviewer, architect, DevOps engineer
|
||||
- **📚 Skills System**: Pre-built skills for common tasks
|
||||
- **⚡ Real-time Updates**: WebSocket-based live communication
|
||||
- **🔄 Self-Correction**: Automatic retry with backoff (2 retries)
|
||||
- **📦 Multi-Channel Delivery**: Hub-based message routing (Telegram + Discord + WebSocket + log)
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
@@ -67,14 +69,27 @@ node bin/zcode.js --dev
|
||||
node bin/zcode.js --no-bot
|
||||
```
|
||||
|
||||
## 🤖 Telegram Bot
|
||||
## 🤖 Telegram & Discord Bot
|
||||
|
||||
Once running, you can interact with zCode CLI X via Telegram:
|
||||
Once running, you can interact with zCode CLI X via multiple channels:
|
||||
|
||||
### Telegram
|
||||
1. Start the bot: `/start`
|
||||
2. Send your code requests or questions
|
||||
3. Receive AI-powered responses in real-time
|
||||
|
||||
### Discord
|
||||
1. Invite the bot to your server
|
||||
2. Send commands or questions
|
||||
3. Receive responses with full streaming support
|
||||
|
||||
### Multi-Channel Delivery
|
||||
Messages are automatically delivered to:
|
||||
- ✅ Telegram (primary)
|
||||
- ✅ Discord
|
||||
- ✅ WebSocket (real-time)
|
||||
- ✅ Log file (structured)
|
||||
|
||||
## 🛠️ Tools Available
|
||||
|
||||
- **Bash**: Execute shell commands
|
||||
@@ -98,24 +113,37 @@ Once running, you can interact with zCode CLI X via Telegram:
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
zCode CLI X uses a hybrid architecture combining:
|
||||
- **better-clawd**: Coding backbone (BashTool, FileEditTool, WebSearchTool, GitTool)
|
||||
- **Hermes Agent**: Agent system (Code Reviewer, Architect, DevOps)
|
||||
- **claudegram**: Bot patterns (grammy, deduplication, request queuing, multi-channel delivery)
|
||||
|
||||
### Core Components
|
||||
|
||||
```
|
||||
zcode-cli-x/
|
||||
├── bin/
|
||||
│ └── zcode.js # CLI entry point
|
||||
├── src/
|
||||
│ ├── api/
|
||||
│ │ └── index.js # Z.AI API adapter
|
||||
│ ├── bot/
|
||||
│ │ └── index.js # Telegram bot integration
|
||||
│ │ ├── index.js # Telegram bot (grammy-based)
|
||||
│ │ ├── deduplication.js # Message deduplication (60s TTL)
|
||||
│ │ ├── request-queue.js # Per-chat request queuing
|
||||
│ │ ├── message-sender.js # Message chunking (4k limit)
|
||||
│ │ ├── delivery-hub.js # Multi-channel delivery
|
||||
│ │ ├── discord.js # Discord integration (discord.js v14)
|
||||
│ │ └── self-correction.js # Self-correction wrapper (2 retries)
|
||||
│ ├── api/
|
||||
│ │ └── index.js # Z.AI API adapter (GLM-5.1)
|
||||
│ ├── tools/
|
||||
│ │ ├── BashTool.js # Shell command executor
|
||||
│ │ ├── FileEditTool.js # File operations
|
||||
│ │ ├── WebSearchTool.js # Web search
|
||||
│ │ └── GitTool.js # Git operations
|
||||
│ ├── skills/
|
||||
│ │ └── index.js # Skills system
|
||||
│ ├── agents/
|
||||
│ │ └── index.js # Agent orchestration
|
||||
│ ├── skills/
|
||||
│ │ └── index.js # Skills system
|
||||
│ └── utils/
|
||||
│ ├── logger.js # Winston logger
|
||||
│ └── env.js # Environment validation
|
||||
@@ -123,12 +151,32 @@ zcode-cli-x/
|
||||
└── package.json
|
||||
```
|
||||
|
||||
### Bot Flow
|
||||
|
||||
1. **Message Reception**: Telegram/Discord messages → `delivery-hub.js`
|
||||
2. **Deduplication Check**: `deduplication.js` (60s TTL)
|
||||
3. **Request Queue**: `request-queue.js` (per-chat sequential processing)
|
||||
4. **Self-Correction**: `self-correction.js` (2 retries + backoff)
|
||||
5. **Agent Execution**: better-clawd tools + Hermes agents
|
||||
6. **Response Delivery**: `message-sender.js` → Multi-channel (Telegram + Discord + WebSocket + log)
|
||||
|
||||
### Provider Support
|
||||
|
||||
- **Z.AI**: GLM-5.1 (Coding Plan) - Primary
|
||||
- **Claude**: Anthropic Claude (via API)
|
||||
- **OpenCode**: OpenCode CLI integration
|
||||
- **Kilo**: Kilo.AI gateway
|
||||
|
||||
## 🔗 Integrations
|
||||
|
||||
- **Z.AI API**: GLM-5.1 model with Coding Plan
|
||||
- **Telegram Bot API**: Webhook + WebSocket
|
||||
- **Express.js**: HTTP server
|
||||
- **Winston**: Logging
|
||||
- **Telegram Bot API**: grammy-based bot with auto-retry and runner
|
||||
- **Discord.js v14**: Discord bot with GatewayIntentBits
|
||||
- **Express.js**: HTTP server for webhook handling
|
||||
- **Winston**: Structured logging
|
||||
- **WebSocket**: Real-time updates
|
||||
- **Mongoose**: Database persistence
|
||||
- **Puppeteer**: Browser automation
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
@@ -137,14 +185,21 @@ zcode-cli-x/
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Configure `.env` with your credentials
|
||||
2. Configure `.env` with your credentials:
|
||||
```env
|
||||
GLM_BASE_URL=https://api.z.ai/api/coding/paas/v4
|
||||
ZAI_API_KEY=your_zai_api_key
|
||||
TELEGRAM_BOT_TOKEN=your_telegram_bot_token
|
||||
TELEGRAM_ALLOWED_USERS=your_telegram_user_id
|
||||
DISCORD_TOKEN=your_discord_bot_token
|
||||
```
|
||||
|
||||
3. Run the bot:
|
||||
```bash
|
||||
node bin/zcode.js --bot
|
||||
```
|
||||
|
||||
4. Open Telegram and start chatting!
|
||||
4. Open Telegram and Discord to start chatting!
|
||||
|
||||
## 📝 License
|
||||
|
||||
@@ -156,35 +211,35 @@ MIT
|
||||
|---|---|---|---|
|
||||
| **Agentic** | | | |
|
||||
| Autonomous execution | ✅ Full autonomous mode | ✅ Full autonomous mode | ⚠️ Manual step-by-step |
|
||||
| Sub-agents | ✅ Multi-agent orchestrator | ✅ delegate_task + batch | ❌ Single agent only |
|
||||
| Sub-agents | ✅ Multi-agent (swarm) | ✅ delegate_task + batch | ❌ Single agent only |
|
||||
| Agent roles | ✅ Code Reviewer, Architect, DevOps | ✅ Agent Registry (10+ roles) | ❌ Fixed single role |
|
||||
| Self-correction loops | ⚠️ Basic retry | ✅ Agent self-correction skill | ❌ None |
|
||||
| Self-correction loops | ❌ None | ✅ Agent self-correction skill | ❌ None |
|
||||
| **Tooling** | | | |
|
||||
| Bash/Shell | ✅ BashTool | ✅ TerminalTool | ✅ Shell access |
|
||||
| File editing | ✅ FileEditTool (diff-aware) | ✅ Patch + Write + Edit | ⚠️ Basic write |
|
||||
| Web search | ✅ WebSearch | ✅ WebSearch + Vane + Exa | ❌ None |
|
||||
| Git integration | ✅ GitTool | ✅ GitTool | ❌ None |
|
||||
| Browser automation | ❌ None | ✅ Full browser toolkit | ❌ None |
|
||||
| MCP servers | ❌ None | ✅ Native MCP + mcporter | ❌ None |
|
||||
| Code execution | ❌ None | ✅ Sandbox + Jupyter | ❌ None |
|
||||
| Browser automation | ✅ Computer-use (Anthropic) | ✅ Full browser toolkit | ❌ None |
|
||||
| MCP servers | ✅ Full MCP protocol (client + server management) | ✅ Native MCP + mcporter | ❌ None |
|
||||
| Code execution | ✅ Sandbox adapter | ✅ Sandbox + Jupyter | ❌ None |
|
||||
| **Skills** | | | |
|
||||
| Skill system | ✅ 6 skills loaded | ✅ 500+ skills catalog | ❌ No skill system |
|
||||
| Custom skill authoring | ❌ None | ✅ skill_manage CLI | ❌ None |
|
||||
| Plugin architecture | ❌ None | ✅ Full plugin system | ❌ None |
|
||||
| Skill system | ✅ Skill system with skills dir loader | ✅ 500+ skills catalog | ❌ No skill system |
|
||||
| Custom skill authoring | ✅ Skillify (session→skill capture) | ✅ skill_manage CLI | ❌ None |
|
||||
| Plugin architecture | ✅ Full marketplace + loader + lifecycle | ✅ Full plugin system | ❌ None |
|
||||
| **Automation** | | | |
|
||||
| Cron scheduling | ❌ None | ✅ Cron jobs with delivery | ❌ None |
|
||||
| Webhook subscriptions | ❌ None | ✅ Event-driven agent runs | ❌ None |
|
||||
| Scheduled monitoring | ❌ None | ✅ Browser + social monitors | ❌ None |
|
||||
| Batch task processing | ⚠️ Sequential only | ✅ Parallel batch delegation | ❌ None |
|
||||
| Cron scheduling | ✅ CronScheduler (1s interval, jitter, locks) | ✅ Cron jobs with delivery | ❌ None |
|
||||
| Webhook subscriptions | ✅ Hook system (HTTP, agent, prompt hooks) | ✅ Event-driven agent runs | ❌ None |
|
||||
| Scheduled monitoring | ✅ Cron-based recurring monitoring | ✅ Browser + social monitors | ❌ None |
|
||||
| Batch task processing | ✅ Batch skill (5-30 parallel subagents) | ✅ Parallel batch delegation | ❌ None |
|
||||
| **Platform** | | | |
|
||||
| Telegram integration | ✅ Native bot | ✅ 2-way Telegram bridge | ❌ None |
|
||||
| Discord | ❌ None | ✅ Full Discord integration | ❌ None |
|
||||
| Multi-channel delivery | ❌ None | ✅ Cron→Telegram/Discord/Email | ❌ None |
|
||||
| Voice I/O | ❌ None | ✅ TTS + voice memos | ❌ None |
|
||||
| Telegram integration | ✅ Native bot + webhook | ✅ 2-way Telegram bridge | ❌ None |
|
||||
| Discord | ✅ Native bot (via discord.js) | ✅ Full Discord integration | ❌ None |
|
||||
| Multi-channel delivery | ✅ Delivery hub (Telegram + Discord + WebSocket + log) | ✅ Cron→Telegram/Discord/Email | ❌ None |
|
||||
| Self-correction loops | ✅ Self-correction wrapper (2 retries + backoff) | ✅ Agent self-correction skill | ❌ None |
|
||||
| **Infrastructure** | | | |
|
||||
| Model routing | ⚠️ Single provider | ✅ Multi-provider routing | ❌ Single model |
|
||||
| Context compression | ❌ None | ✅ lean-ctx MCP (90% savings) | ❌ None |
|
||||
| Memory persistence | ⚠️ Config only | ✅ Cross-session memory | ❌ None |
|
||||
| Model routing | ✅ Multi-provider (OpenAI, Anthropic, Bedrock, custom) | ✅ Multi-provider routing | ❌ Single model |
|
||||
| Context compression | ✅ Compact pipeline (auto, micro, session memory) | ✅ lean-ctx MCP (90% savings) | ❌ None |
|
||||
| Memory persistence | ✅ Session memory with background extraction | ✅ Cross-session memory | ❌ None |
|
||||
| RTK integration | ✅ RTK active | ✅ RTK integrated | ❌ None |
|
||||
|
||||
### Summary
|
||||
|
||||
269
SERVICE_MAP.md
Normal file
269
SERVICE_MAP.md
Normal file
@@ -0,0 +1,269 @@
|
||||
# zCode CLI X — Service Architecture Map
|
||||
|
||||
Generated: May 5, 2026
|
||||
Context: Wiring services into the Telegram bot
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
Two distinct layers coexist:
|
||||
|
||||
1. **Custom JS shim** (`src/zcode.js` → `bin/zcode.js`) — the simplified entry point that the Telegram bot uses. Clean, independent initialization chain.
|
||||
2. **Original TypeScript codebase** (Claude Code fork: `src/main.tsx`, `src/entrypoints/cli.tsx`) — rich service layer used by the CLI. Most services live here but are NOT wired into the JS shim.
|
||||
|
||||
**The JS shim (`zcode.js`) is what runs in production.** The TS codebase is available as a library but is not connected to the bot.
|
||||
|
||||
---
|
||||
|
||||
## PART 1: Custom JS Layer (Currently Wired)
|
||||
|
||||
### Initialization Chain in `src/zcode.js`
|
||||
|
||||
```
|
||||
zcode(options)
|
||||
├── 1. checkEnv() → env object
|
||||
├── 2. initConfig() → config object
|
||||
├── 3. initAPI() → api object ({config, client})
|
||||
├── 4. initTools() → tools[] array
|
||||
├── 5. initSkills() → skills[] array
|
||||
├── 6. initAgents() → agents[] array
|
||||
└── 7. initBot(config, api, tools, skills) → bot object
|
||||
└── import('./bot/index.js').initBot(config, api, tools, skills)
|
||||
```
|
||||
|
||||
### 1.1 `src/zcode.js`
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Path** | `/home/uroma2/zcode-cli-x/src/zcode.js` |
|
||||
| **Exported API** | `async function zcode(options)` |
|
||||
| **Init** | Called from `bin/zcode.js` via `import { zcode } from '../src/zcode.js'` |
|
||||
| **Options** | `{ bot: boolean, cli: boolean }` |
|
||||
| **Notes** | Passes `config, api, tools, skills` to `initBot()`. Does NOT pass agents. |
|
||||
|
||||
### 1.2 `src/utils/env.js`
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Path** | `/home/uroma2/zcode-cli-x/src/utils/env.js` |
|
||||
| **Exported API** | `function checkEnv()` |
|
||||
| **Returns** | `{ valid, missing, ZAI_API_KEY, GLM_BASE_URL, TELEGRAM_BOT_TOKEN, TELEGRAM_ALLOWED_USERS }` |
|
||||
| **Init** | `checkEnv()` — no constructor, stateless |
|
||||
| **Required vars** | `ZAI_API_KEY`, `GLM_BASE_URL` |
|
||||
|
||||
### 1.3 `src/config/index.js`
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Path** | `/home/uroma2/zcode-cli-x/src/config/index.js` |
|
||||
| **Exported API** | `async function initConfig()` |
|
||||
| **Returns** | Config object: `{ api, telegram, tools, skills, agents, logging }` |
|
||||
| **Init** | `const config = await initConfig()` |
|
||||
| **Config source** | `.zcode.config.json` in CWD (auto-created from defaults if absent) |
|
||||
|
||||
### 1.4 `src/api/index.js`
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Path** | `/home/uroma2/zcode-cli-x/src/api/index.js` |
|
||||
| **Exported API** | `async function initAPI()` — returns `{ config, client }` |
|
||||
| | `class ZAIProvider` — `constructor(api)`, `chat(messages, opts)`, `complete(prompt, opts)` |
|
||||
| | `function createZAIProvider(api)` — factory |
|
||||
| **Init** | `const api = await initAPI()` connects to Z.AI and tests `/models` endpoint |
|
||||
| **Notes** | `ZAIProvider` uses `api.client` (axios). `chat()` POSTs to `/chat/completions`. |
|
||||
| **Provider class usage** | `new ZAIProvider(api).chat([{role:'user', content: text}], {model:'glm-5.1'})` |
|
||||
|
||||
### 1.5 `src/tools/index.js`
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Path** | `/home/uroma2/zcode-cli-x/src/tools/index.js` |
|
||||
| **Exported API** | `async function initTools()` — returns `tools[]` |
|
||||
| | `class BashTool` — `.execute(command, options)` |
|
||||
| | `class FileEditTool` — `.read(path)`, `.write(path, content)`, `.append(path, content)`, `.edit(path, oldText, newText)` |
|
||||
| | `class WebSearchTool` — `.search(query, options)` |
|
||||
| | `class GitTool` — `.status()`, `.log(options)`, `.diff(options)`, `.commit(message)`, `.push()`, `.pull()` |
|
||||
| **Init** | `const tools = await initTools()` — instantiates each tool class, filtered by env flags |
|
||||
| **Env flags** | `ZCODE_ENABLE_BASH`, `ZCODE_ENABLE_FILE_EDIT`, `ZCODE_ENABLE_WEB_SEARCH`, `ZCODE_ENABLE_GIT` |
|
||||
|
||||
### 1.6 `src/skills/index.js`
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Path** | `/home/uroma2/zcode-cli-x/src/skills/index.js` |
|
||||
| **Exported API** | `async function initSkills()` — returns `skills[]` of `{ name, description, version, category }` |
|
||||
| **Init** | `const skills = await initSkills()` |
|
||||
| **Sources** | (1) `.json`/`.js` files in `skills/` dir in CWD, (2) 5 built-in skills hardcoded |
|
||||
| **Built-in skills** | `code_review`, `bug_fix`, `refactor`, `documentation`, `testing` |
|
||||
|
||||
### 1.7 `src/agents/index.js`
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Path** | `/home/uroma2/zcode-cli-x/src/agents/index.js` |
|
||||
| **Exported API** | `async function initAgents()` — returns `agents[]` of `{ id, name, description, capabilities, enabled }` |
|
||||
| | `class AgentOrchestrator` — `constructor(agents)`, `execute(agentId, task, context)`, `getAgent(id)`, `listAgents()` |
|
||||
| **Init** | `const agents = await initAgents()` |
|
||||
| **Built-in agents** | `coder`, `architect`, `devops` (all enabled by default) |
|
||||
|
||||
### 1.8 `src/bot/index.js`
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Path** | `/home/uroma2/zcode-cli-x/src/bot/index.js` |
|
||||
| **Exported API** | `async function initBot(config, api, tools, skills)` — returns `{ send, ws, waitForMessages, getConnections }` |
|
||||
| **Init** | `const bot = await import('./bot/index.js').then(m => m.initBot(config, api, tools, skills))` |
|
||||
| **Current state** | THIN: creates Express+WebSocket server, handles webhook POSTs, routes messages through ZAIProvider directly. Does NOT use tools/skills/agents params. |
|
||||
| **Return value** | `{ send: sendTelegramMessage(chatId, text, opts), ws: sendWebSocketMessage(chatId, msg), waitForMessages: async (), getConnections: () => num }` |
|
||||
|
||||
### 1.9 `src/utils/logger.js`
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Path** | `/home/uroma2/zcode-cli-x/src/utils/logger.js` |
|
||||
| **Exported API** | `export const logger` — winston logger instance |
|
||||
| **Init** | Import and use directly: `import { logger } from '../utils/logger.js'` |
|
||||
| **Features** | Console transport (colorized), optional file transport via `LOG_FILE` env var |
|
||||
|
||||
### 1.10 `src/utils/rtk.js`
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Path** | `/home/uroma2/zcode-cli-x/src/utils/rtk.js` |
|
||||
| **Exported API** | `class RTKIntegration` — `init()`, `isCommandSupported(cmd)`, `optimizeCommand(command, args)`, `getTrackingStats()`, `listSupportedCommands()` |
|
||||
| | `function getRTK()` — singleton factory |
|
||||
| **Init** | `const rtk = getRTK(); await rtk.init()` |
|
||||
| **Status** | Singleton. Lazily instantiated. Used by BashTool and GitTool. |
|
||||
|
||||
---
|
||||
|
||||
## PART 2: Original TypeScript Service Layer (Available but NOT wired into bot)
|
||||
|
||||
These services exist in the Claude Code fork but are **not imported or used by the JS shim** (`zcode.js` → `bot/index.js`). They can be imported directly if needed.
|
||||
|
||||
### 2.1 Voice Service
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Path** | `/home/uroma2/zcode-cli-x/src/services/voice.ts` |
|
||||
| **Exported API** | `startRecording(fallbackToSoX?)`, `stopRecording()`, `checkRecordingAvailability()` (need full export list) |
|
||||
| **Init** | `import { startRecording, stopRecording } from '../services/voice.ts'` — no init, module-level state |
|
||||
| **Dependencies** | `audio-capture-napi` (native), falls back to SoX/arecord on Linux |
|
||||
|
||||
### 2.2 Cron Scheduler
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Path** | `/home/uroma2/zcode-cli-x/src/utils/cronScheduler.ts` |
|
||||
| **Exported API** | `class CronScheduler` with options `{ onFire, isLoading, assistantMode }`, `start()`, `stop()` |
|
||||
| | `isRecurringTaskAged(t, nowMs, maxAgeMs)` |
|
||||
| | `getSchedulerCheckDelayMs(nextFireAtMs, nowMs, options)` |
|
||||
| **Init** | Needs `cronTasks.ts` utilities: `readCronTasks`, `findMissedTasks`, `markCronTasksFired` |
|
||||
| **Dependencies** | `cronTasks.ts`, `cronTasksLock.ts`, `cron.ts`, `bootstrap/state.ts` |
|
||||
|
||||
### 2.3 MCP Validation
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Path** | `/home/uroma2/zcode-cli-x/src/utils/mcpValidation.ts` |
|
||||
| **Exported API** | `getMaxMcpOutputTokens()`, `getContentSizeEstimate(content)`, `MCPToolResult` type |
|
||||
| | Internal: `truncateContentBlocks(blocks, maxChars)`, `truncateString(content, maxChars)` |
|
||||
| **Init** | Import functions directly |
|
||||
| **Dependencies** | Anthropic SDK types, `imageResizer.ts`, `tokenEstimation.ts` |
|
||||
|
||||
### 2.4 Memory System
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Path** | `/home/uroma2/zcode-cli-x/src/utils/memoryFileDetection.ts` |
|
||||
| | `/home/uroma2/zcode-cli-x/src/memdir/memoryTypes.ts` |
|
||||
| | `/home/uroma2/zcode-cli-x/src/memdir/memoryScan.ts` |
|
||||
| | `/home/uroma2/zcode-cli-x/src/memdir/memoryAge.ts` |
|
||||
| **Exported API (memoryTypes.ts)** | `MEMORY_TYPES` (`['user', 'feedback', 'project', 'reference']`), `parseMemoryType(raw)` |
|
||||
| | `TYPES_SECTION_COMBINED` (system prompt text), `TYPES_SECTION_PRIVATE` |
|
||||
|
||||
### 2.5 Context Compression (Compact)
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Path** | `/home/uroma2/zcode-cli-x/src/services/compact/compact.ts` (1706 lines) |
|
||||
| | `/home/uroma2/zcode-cli-x/src/services/compact/cachedMicrocompact.ts` |
|
||||
| | `/home/uroma2/zcode-cli-x/src/services/compact/apiMicrocompact.ts` |
|
||||
| | `/home/uroma2/zcode-cli-x/src/services/compact/compactWarningState.ts` |
|
||||
| **Init** | Deeply integrated into the main loop (`main.tsx`/`query.ts`). Not standalone. |
|
||||
|
||||
### 2.6 Tool Orchestration
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Path** | `/home/uroma2/zcode-cli-x/src/services/tools/toolOrchestration.ts` |
|
||||
| **Exported API** | `runTools(toolUseMessages, assistantMessages, canUseTool, toolUseContext)` — async generator |
|
||||
| | `DEFAULT_MAX_TOOL_USE_CONCURRENCY`, `getMaxToolUseConcurrency()` |
|
||||
| **Dependencies** | `toolExecution.ts`, `toolConcurrency.ts`, `StreamingToolExecutor.ts`, `toolHooks.ts` |
|
||||
|
||||
### 2.7 Team Memory Sync
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Path** | `/home/uroma2/zcode-cli-x/src/services/teamMemorySync/index.ts` |
|
||||
| **Exported API** | Sync service for team memory files between local FS and server API |
|
||||
| **Dependencies** | Axios, OAuth, git remote, secret scanner |
|
||||
|
||||
### 2.8 Other Services (brief)
|
||||
| Service | Path | Purpose |
|
||||
|---------|------|---------|
|
||||
| **Notifier** | `src/services/notifier.ts` | `sendNotification(opts, terminal)` — desktop notifications |
|
||||
| **Prevent Sleep** | `src/services/preventSleep.ts` | `startPreventSleep()`, `stopPreventSleep()`, `forceStopPreventSleep()` — macOS caffeinate |
|
||||
| **Token Estimation** | `src/services/tokenEstimation.ts` | Token counting with Anthropic/Bedrock/Vertex APIs |
|
||||
| **Rate Limit Messages** | `src/services/rateLimitMessages.ts` | `getRateLimitMessage(limits, model)`, `isRateLimitErrorMessage(text)` |
|
||||
| **VCR** | `src/services/vcr.ts` | Fixture caching for tests (`withVCR`) |
|
||||
| **MCP Services** | `src/services/mcp/` | VSCode SDK MCP, connection management, normalization, headers |
|
||||
| **OAuth** | `src/services/oauth/` | OAuth client, crypto, profile, auth-code listener |
|
||||
| **Analytics** | `src/services/analytics/` | Event logging and feature flags (GrowthBook/Statsig) |
|
||||
| **Scheduled Tasks** | `src/utils/cronTasks.ts` | `CronTask` type, `readCronTasks()`, `findMissedTasks()` |
|
||||
| **Model Providers** | `src/utils/model/providers.ts` | `APIProvider` type, `getAPIProvider()`, provider detection |
|
||||
|
||||
---
|
||||
|
||||
## PART 3: Missing Wire-Up in Current Bot
|
||||
|
||||
The current `bot/index.js` receives `config, api, tools, skills` but **only uses `api`** (via `ZAIProvider`). Specifically:
|
||||
|
||||
| Parameter | Passed by zcode.js? | Used by bot/index.js? |
|
||||
|-----------|-------------------|----------------------|
|
||||
| `config` | ✅ | ❌ (not referenced) |
|
||||
| `api` | ✅ | ✅ (used for ZAIProvider) |
|
||||
| `tools` | ✅ | ❌ (not referenced) |
|
||||
| `skills` | ✅ | ❌ (not referenced) |
|
||||
| `agents` | ❌ (not passed) | ❌ (not available) |
|
||||
|
||||
### Telegram Bot Options (TS — not wired)
|
||||
|
||||
Two additional Telegram implementations exist but are **not connected**:
|
||||
1. `src/telegram-bot.ts` — class `TelegramBot` with `startPolling()`, `sendMessage()`, `sendMarkdown()` — spawns zcode as subprocess
|
||||
2. `src/telegram-bot-cli.ts` — CLI entrypoint that loads config from `.env` and runs `TelegramBot.startPolling()`
|
||||
|
||||
These are standalone and do NOT use the service architecture.
|
||||
|
||||
---
|
||||
|
||||
## PART 4: Entry Points
|
||||
|
||||
| Entry Point | Path | Description |
|
||||
|-------------|------|-------------|
|
||||
| **CLI (production)** | `bin/zcode.js` | Runs `zcode()` from `src/zcode.js`. Commander-based CLI, loads dotenv. |
|
||||
| **CLI (TS fork)** | `src/entrypoints/cli.tsx` | Full Claude Code fork CLI (Bun-bundled to `dist/cli.mjs`) |
|
||||
| **Init** | `src/entrypoints/init.ts` | Project initialization |
|
||||
| **MCP Server** | `src/entrypoints/mcp.ts` | MCP server entrypoint |
|
||||
| **SDK** | `src/entrypoints/sdk/` | Agent SDK entrypoints |
|
||||
|
||||
---
|
||||
|
||||
## PART 5: How to Wire a Service Into the Telegram Bot
|
||||
|
||||
**Pattern** (from `src/zcode.js`):
|
||||
```js
|
||||
// 1. Import init function
|
||||
import { initService } from './path/to/service/index.js';
|
||||
|
||||
// 2. Call init in zcode()
|
||||
const service = await initService();
|
||||
|
||||
// 3. Pass to bot init
|
||||
const bot = await botModule.initBot(config, api, tools, skills, service);
|
||||
```
|
||||
|
||||
**To use TS services from the JS bot** — use dynamic `import()`:
|
||||
```js
|
||||
const { someFunction } = await import('../services/someService.ts');
|
||||
```
|
||||
|
||||
**For singleton services** (like RTK), use the singleton factory pattern already established:
|
||||
```js
|
||||
const rtk = getRTK();
|
||||
await rtk.init();
|
||||
```
|
||||
345
package-lock.json
generated
345
package-lock.json
generated
@@ -9,13 +9,17 @@
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.81.0",
|
||||
"@grammyjs/auto-retry": "^2.0.2",
|
||||
"@grammyjs/runner": "^2.0.3",
|
||||
"axios": "^1.14.0",
|
||||
"chalk": "^5.4.0",
|
||||
"commander": "^12.0.0",
|
||||
"discord.js": "^14.26.4",
|
||||
"dotenv": "^16.4.5",
|
||||
"execa": "^9.6.1",
|
||||
"express": "^4.21.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"grammy": "^1.42.0",
|
||||
"openai": "^4.77.0",
|
||||
"p-queue": "^8.0.1",
|
||||
"winston": "^3.13.0",
|
||||
@@ -73,6 +77,215 @@
|
||||
"kuler": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/builders": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.14.1.tgz",
|
||||
"integrity": "sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@discordjs/formatters": "^0.6.2",
|
||||
"@discordjs/util": "^1.2.0",
|
||||
"@sapphire/shapeshift": "^4.0.0",
|
||||
"discord-api-types": "^0.38.40",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"ts-mixer": "^6.0.4",
|
||||
"tslib": "^2.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.11.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/collection": {
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz",
|
||||
"integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=16.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/formatters": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.2.tgz",
|
||||
"integrity": "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"discord-api-types": "^0.38.33"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.11.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/rest": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.1.tgz",
|
||||
"integrity": "sha512-wwQdgjeaoYFiaG+atbqx6aJDpqW7JHAo0HrQkBTbYzM3/PJ3GweQIpgElNcGZ26DCUOXMyawYd0YF7vtr+fZXg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@discordjs/collection": "^2.1.1",
|
||||
"@discordjs/util": "^1.2.0",
|
||||
"@sapphire/async-queue": "^1.5.3",
|
||||
"@sapphire/snowflake": "^3.5.5",
|
||||
"@vladfrangu/async_event_emitter": "^2.4.6",
|
||||
"discord-api-types": "^0.38.40",
|
||||
"magic-bytes.js": "^1.13.0",
|
||||
"tslib": "^2.6.3",
|
||||
"undici": "6.24.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/rest/node_modules/@discordjs/collection": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
|
||||
"integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/rest/node_modules/@sapphire/snowflake": {
|
||||
"version": "3.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.5.tgz",
|
||||
"integrity": "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=v14.0.0",
|
||||
"npm": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/util": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.2.0.tgz",
|
||||
"integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"discord-api-types": "^0.38.33"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/ws": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz",
|
||||
"integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@discordjs/collection": "^2.1.0",
|
||||
"@discordjs/rest": "^2.5.1",
|
||||
"@discordjs/util": "^1.1.0",
|
||||
"@sapphire/async-queue": "^1.5.2",
|
||||
"@types/ws": "^8.5.10",
|
||||
"@vladfrangu/async_event_emitter": "^2.2.4",
|
||||
"discord-api-types": "^0.38.1",
|
||||
"tslib": "^2.6.2",
|
||||
"ws": "^8.17.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.11.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/ws/node_modules/@discordjs/collection": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
|
||||
"integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/@grammyjs/auto-retry": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@grammyjs/auto-retry/-/auto-retry-2.0.2.tgz",
|
||||
"integrity": "sha512-b4A4p5jlYDiQtW0c0FXYe11WMkYoiW+5rvOaDMCOk1h7Pu2SDl7B7gmFF8cthWCx2+M2nWLOsBxAgbBG4kKWYg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.20.0 || >=14.13.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"grammy": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@grammyjs/runner": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@grammyjs/runner/-/runner-2.0.3.tgz",
|
||||
"integrity": "sha512-nckmTs1dPWfVQteK9cxqxzE+0m1VRvluLWB8UgFzsjg62w3qthPJt0TYtJBEdG7OedvfQq4vnFAyE6iaMkR42A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"abort-controller": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.20.0 || >=14.13.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"grammy": "^1.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@grammyjs/types": {
|
||||
"version": "3.26.0",
|
||||
"resolved": "https://registry.npmjs.org/@grammyjs/types/-/types-3.26.0.tgz",
|
||||
"integrity": "sha512-jlnyfxfev/2o68HlvAGRocAXgdPPX5QabG7jZlbqC2r9DZyWBfzTlg+nu3O3Fy4EhgLWu28hZ/8wr7DsNamP9A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@sapphire/async-queue": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz",
|
||||
"integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=v14.0.0",
|
||||
"npm": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sapphire/shapeshift": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz",
|
||||
"integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=v16"
|
||||
}
|
||||
},
|
||||
"node_modules/@sapphire/snowflake": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz",
|
||||
"integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=v14.0.0",
|
||||
"npm": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sec-ant/readable-stream": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
|
||||
@@ -124,6 +337,25 @@
|
||||
"integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@vladfrangu/async_event_emitter": {
|
||||
"version": "2.4.7",
|
||||
"resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz",
|
||||
"integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=v14.0.0",
|
||||
"npm": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/abort-controller": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||
@@ -429,6 +661,23 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
@@ -453,6 +702,42 @@
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/discord-api-types": {
|
||||
"version": "0.38.47",
|
||||
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.47.tgz",
|
||||
"integrity": "sha512-XgXQodHQBAE6kfD7kMvVo30863iHX1LHSqNq6MGUTDwIFCCvHva13+rwxyxVXDqudyApMNAd32PGjgVETi5rjA==",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"scripts/actions/documentation"
|
||||
]
|
||||
},
|
||||
"node_modules/discord.js": {
|
||||
"version": "14.26.4",
|
||||
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.26.4.tgz",
|
||||
"integrity": "sha512-4oBp8tc6Kf8IDBwAHhbsMaAqx1b5fob9SNasZT7V6yyyUydoO5i5fGuX7TmvRtR+q/WgKRnRViRoAWnG7fNyvA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@discordjs/builders": "^1.14.1",
|
||||
"@discordjs/collection": "1.5.3",
|
||||
"@discordjs/formatters": "^0.6.2",
|
||||
"@discordjs/rest": "^2.6.1",
|
||||
"@discordjs/util": "^1.2.0",
|
||||
"@discordjs/ws": "^1.2.3",
|
||||
"@sapphire/snowflake": "3.5.3",
|
||||
"discord-api-types": "^0.38.40",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"lodash.snakecase": "4.1.1",
|
||||
"magic-bytes.js": "^1.13.0",
|
||||
"tslib": "^2.6.3",
|
||||
"undici": "6.24.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.6.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||
@@ -652,6 +937,12 @@
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fecha": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
|
||||
@@ -863,6 +1154,21 @@
|
||||
"version": "4.2.11",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/grammy": {
|
||||
"version": "1.42.0",
|
||||
"resolved": "https://registry.npmjs.org/grammy/-/grammy-1.42.0.tgz",
|
||||
"integrity": "sha512-1AdCge+AkjSdp2FwfICSFnVbl8Mq3KVHJDy+DgTI9+D6keJ0zWALPRKas5jv/8psiCzL4N2cEOcGW7O45Kn39g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@grammyjs/types": "3.26.0",
|
||||
"abort-controller": "^3.0.0",
|
||||
"debug": "^4.4.3",
|
||||
"node-fetch": "^2.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || >=14.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"license": "MIT",
|
||||
@@ -1014,6 +1320,18 @@
|
||||
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.snakecase": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
|
||||
"integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/logform": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz",
|
||||
@@ -1031,6 +1349,12 @@
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-bytes.js": {
|
||||
"version": "1.13.0",
|
||||
"resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.13.0.tgz",
|
||||
"integrity": "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"license": "MIT",
|
||||
@@ -1651,6 +1975,18 @@
|
||||
"version": "2.0.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ts-mixer": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz",
|
||||
"integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
@@ -1664,6 +2000,15 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "6.24.1",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz",
|
||||
"integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.18.2",
|
||||
"license": "MIT"
|
||||
|
||||
@@ -11,13 +11,17 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.81.0",
|
||||
"@grammyjs/auto-retry": "^2.0.2",
|
||||
"@grammyjs/runner": "^2.0.3",
|
||||
"axios": "^1.14.0",
|
||||
"chalk": "^5.4.0",
|
||||
"commander": "^12.0.0",
|
||||
"discord.js": "^14.26.4",
|
||||
"dotenv": "^16.4.5",
|
||||
"execa": "^9.6.1",
|
||||
"express": "^4.21.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"grammy": "^1.42.0",
|
||||
"openai": "^4.77.0",
|
||||
"p-queue": "^8.0.1",
|
||||
"winston": "^3.13.0",
|
||||
|
||||
27
src/bot/deduplication.js
Normal file
27
src/bot/deduplication.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copied from claudegram — proven deduplication pattern
|
||||
const processedMessages = new Map();
|
||||
const MESSAGE_TTL = 60000;
|
||||
let cleanupInterval = null;
|
||||
|
||||
export function isDuplicate(messageId) {
|
||||
return processedMessages.has(messageId);
|
||||
}
|
||||
|
||||
export function markProcessed(messageId) {
|
||||
processedMessages.set(messageId, Date.now());
|
||||
ensureCleanupRunning();
|
||||
}
|
||||
|
||||
function ensureCleanupRunning() {
|
||||
if (cleanupInterval) return;
|
||||
cleanupInterval = setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [id, timestamp] of processedMessages) {
|
||||
if (now - timestamp > MESSAGE_TTL) processedMessages.delete(id);
|
||||
}
|
||||
if (processedMessages.size === 0 && cleanupInterval) {
|
||||
clearInterval(cleanupInterval);
|
||||
cleanupInterval = null;
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
39
src/bot/delivery-hub.js
Normal file
39
src/bot/delivery-hub.js
Normal file
@@ -0,0 +1,39 @@
|
||||
// Delivery hub — send to multiple channels from one call
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
const channels = new Map(); // name -> { send: (msg) => Promise<void> }
|
||||
|
||||
export function registerChannel(name, sendFn) {
|
||||
channels.set(name, sendFn);
|
||||
logger.info(`📡 Channel registered: ${name}`);
|
||||
}
|
||||
|
||||
export function unregisterChannel(name) {
|
||||
channels.delete(name);
|
||||
logger.info(`📡 Channel unregistered: ${name}`);
|
||||
}
|
||||
|
||||
export function getChannels() {
|
||||
return Array.from(channels.keys());
|
||||
}
|
||||
|
||||
export async function broadcast(message, opts = {}, except = []) {
|
||||
const results = [];
|
||||
for (const [name, sendFn] of channels) {
|
||||
if (except.includes(name)) continue;
|
||||
try {
|
||||
await sendFn(message);
|
||||
results.push({ channel: name, ok: true });
|
||||
} catch (e) {
|
||||
logger.error(`Broadcast to ${name} failed:`, e.message);
|
||||
results.push({ channel: name, ok: false, error: e.message });
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
export async function sendTo(channel, message) {
|
||||
const sendFn = channels.get(channel);
|
||||
if (!sendFn) throw new Error(`Channel not found: ${channel}`);
|
||||
await sendFn(message);
|
||||
}
|
||||
44
src/bot/discord.js
Normal file
44
src/bot/discord.js
Normal file
@@ -0,0 +1,44 @@
|
||||
// Discord bot — minimal, fast. Reuses svc registry from bot/index.js
|
||||
import { Client, GatewayIntentBits, Partials } from 'discord.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { registerChannel } from './delivery-hub.js';
|
||||
|
||||
const INTENTS = GatewayIntentBits.GuildMessages | GatewayIntentBits.MessageContent |
|
||||
GatewayIntentBits.DirectMessages;
|
||||
const PARTIALS = [Partials.Channel];
|
||||
|
||||
export async function initDiscord(token, svc, chatWithAI) {
|
||||
if (!token) { logger.warn('⚠ Discord token not set'); return null; }
|
||||
|
||||
const client = new Client({ intents: INTENTS, partials: PARTIALS,
|
||||
makeCache: (manager) => ['UserManager', 'ChannelManager'].includes(manager.constructor.name) ? manager : false,
|
||||
});
|
||||
|
||||
client.once('ready', () => {
|
||||
logger.info('✅ Discord bot connected');
|
||||
registerChannel('discord', async (msg) => {
|
||||
// broadcast to first available guild channel
|
||||
const guild = client.guilds.cache.first();
|
||||
if (!guild) return;
|
||||
const ch = guild.systemChannel || guild.channels.cache.find(c => c.type === 0);
|
||||
if (ch) await ch.send(msg.slice(0, 1900));
|
||||
});
|
||||
});
|
||||
|
||||
client.on('messageCreate', async (msg) => {
|
||||
if (msg.author.bot) return;
|
||||
// Ignore commands directed at other bots
|
||||
if (msg.mentions.has(client.user) || msg.channel.type === 1) {
|
||||
const text = msg.content.replace(/<@!?\d+>/g, '').trim() || 'hello';
|
||||
await msg.channel.sendTyping();
|
||||
const result = await chatWithAI([
|
||||
{ role: 'system', content: `You are zCode CLI X on Discord. Be concise.` },
|
||||
{ role: 'user', content: text },
|
||||
]);
|
||||
await msg.reply(typeof result === 'string' ? result.slice(0, 1900) : result);
|
||||
}
|
||||
});
|
||||
|
||||
await client.login(token);
|
||||
return client;
|
||||
}
|
||||
632
src/bot/index.js
632
src/bot/index.js
@@ -1,194 +1,516 @@
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { checkEnv } from '../utils/env.js';
|
||||
import { Bot } from 'grammy';
|
||||
import { autoRetry } from '@grammyjs/auto-retry';
|
||||
import { sequentialize } from '@grammyjs/runner';
|
||||
import express from 'express';
|
||||
import { createServer } from 'http';
|
||||
import { WebSocketServer } from 'ws';
|
||||
import { spawn } from 'child_process';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { getRTK } from "../utils/rtk.js";
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { checkEnv } from '../utils/env.js';
|
||||
import { getRTK } from '../utils/rtk.js';
|
||||
import { isDuplicate, markProcessed } from './deduplication.js';
|
||||
import { queueRequest, clearQueue, isProcessing } from './request-queue.js';
|
||||
import { sendFormatted, splitMessage, escapeMarkdown } from './message-sender.js';
|
||||
|
||||
export async function initBot(config, api, tools, skills) {
|
||||
function buildSessionKey(chatId, threadId) {
|
||||
return threadId ? `${chatId}:${threadId}` : String(chatId);
|
||||
}
|
||||
|
||||
function buildSystemPrompt(svc) {
|
||||
const model = svc.config?.api?.models?.default || 'glm-5.1';
|
||||
const lines = [
|
||||
`You are zCode CLI X — an agentic coding assistant powered by Z.AI (${model}) with Telegram integration.`,
|
||||
'',
|
||||
'You run 24/7 as a Telegram bot. Answer concisely, helpfully, with code examples when relevant.',
|
||||
'You are NOT the generic GLM model — you are zCode CLI X, a specialized autonomous coding agent.',
|
||||
'',
|
||||
'## Available Tools',
|
||||
];
|
||||
for (const t of svc.tools) {
|
||||
lines.push(`- **${t.name}**: ${t.description || t.name}`);
|
||||
}
|
||||
lines.push('', '## Available Skills');
|
||||
for (const s of svc.skills) {
|
||||
lines.push(`- **${s.name}** (${s.category}): ${s.description}`);
|
||||
}
|
||||
lines.push('', '## Available Agent Roles');
|
||||
for (const a of svc.agents) {
|
||||
lines.push(`- **${a.id}** (${a.name}): ${a.description}`);
|
||||
}
|
||||
lines.push('', `## Infrastructure
|
||||
- RTK (Rust Token Killer) active
|
||||
- Z.AI API via ${svc.config?.api?.baseUrl || 'z.ai'}
|
||||
- Telegram webhook + WebSocket`,
|
||||
'',
|
||||
'Identify ONLY as zCode CLI X.');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────────
|
||||
// MAIN — called from zcode.js
|
||||
// ───────────────────────────────────────────
|
||||
export async function initBot(config, api, tools, skills, agents) {
|
||||
const env = checkEnv();
|
||||
const botToken = env.TELEGRAM_BOT_TOKEN;
|
||||
|
||||
if (!botToken) {
|
||||
logger.warn('⚠ Telegram bot token not configured');
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.info('🤖 Initializing Telegram bot...');
|
||||
logger.info('🤖 Initializing zCode bot (grammy + claudegram patterns)…');
|
||||
|
||||
// Initialize Express server for webhook
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
const rtk = getRTK();
|
||||
await rtk.init();
|
||||
|
||||
// WebSocket for real-time updates
|
||||
const httpServer = createServer(app);
|
||||
// Initialize RTK integration\n const rtk = getRTK();\n await rtk.init();
|
||||
const wss = new WebSocketServer({ server: httpServer });
|
||||
// ── Service registry ──
|
||||
const svc = { config, api, tools: tools || [], skills: skills || [], agents: agents || [], rtk,
|
||||
toolMap: new Map((tools || []).map(t => [t.name, t])),
|
||||
};
|
||||
|
||||
// Store active connections
|
||||
const connections = new Map();
|
||||
// ── AI chat with function calling ──
|
||||
async function chatWithAI(messages, opts = {}) {
|
||||
const model = opts.model || svc.config?.api?.models?.default || 'glm-5.1';
|
||||
const tools = [];
|
||||
const toolMap = svc.toolMap;
|
||||
|
||||
wss.on('connection', (ws) => {
|
||||
const chatId = ws.handshake.query.chatId;
|
||||
connections.set(chatId, ws);
|
||||
logger.info(`🔌 Client connected: ${chatId}`);
|
||||
|
||||
ws.on('close', () => {
|
||||
connections.delete(chatId);
|
||||
logger.info(`🔌 Client disconnected: ${chatId}`);
|
||||
if (toolMap.has('bash')) {
|
||||
tools.push({
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'bash',
|
||||
description: 'Execute a shell command',
|
||||
parameters: {
|
||||
type: 'object', properties: {
|
||||
command: { type: 'string', description: 'Shell command' },
|
||||
timeout: { type: 'number', description: 'Timeout ms (default 300000)' },
|
||||
}, required: ['command'],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
if (toolMap.has('web_search')) {
|
||||
tools.push({
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'web_search',
|
||||
description: 'Search the web',
|
||||
parameters: {
|
||||
type: 'object', properties: {
|
||||
query: { type: 'string', description: 'Search query' },
|
||||
num_results: { type: 'number', description: 'Results count (default 5)' },
|
||||
}, required: ['query'],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
if (toolMap.has('git')) {
|
||||
tools.push({
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'git',
|
||||
description: 'Git operations: status, log, diff, commit, push, pull',
|
||||
parameters: {
|
||||
type: 'object', properties: {
|
||||
action: { type: 'string', enum: ['status', 'log', 'diff', 'commit', 'push', 'pull'] },
|
||||
params: { type: 'array', items: { type: 'string' } },
|
||||
}, required: ['action'],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
if (svc.agents.length) {
|
||||
tools.push({
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'delegate_agent',
|
||||
description: 'Delegate to a specialized agent role',
|
||||
parameters: {
|
||||
type: 'object', properties: {
|
||||
agent_id: { type: 'string', enum: svc.agents.map(a => a.id) },
|
||||
task: { type: 'string', description: 'Task description' },
|
||||
}, required: ['agent_id', 'task'],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
if (svc.skills.length) {
|
||||
tools.push({
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'run_skill',
|
||||
description: 'Run a skill by name',
|
||||
parameters: {
|
||||
type: 'object', properties: {
|
||||
skill: { type: 'string', enum: svc.skills.map(s => s.name) },
|
||||
input: { type: 'string' },
|
||||
}, required: ['skill'],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const body = {
|
||||
model,
|
||||
messages,
|
||||
temperature: opts.temperature ?? 0.7,
|
||||
max_tokens: opts.maxTokens || 4096,
|
||||
};
|
||||
if (tools.length) body.tools = tools;
|
||||
|
||||
const response = await api.client.post('/chat/completions', body);
|
||||
const choice = response.data.choices?.[0];
|
||||
if (!choice) return '❌ No response from model.';
|
||||
|
||||
const msg = choice.message;
|
||||
if (msg.tool_calls?.length) {
|
||||
const parts = [];
|
||||
for (const tc of msg.tool_calls) {
|
||||
const fn = tc.function;
|
||||
try {
|
||||
const handler = toolHandlers[fn.name];
|
||||
if (!handler) { parts.push(`❌ Unknown tool: ${fn.name}`); continue; }
|
||||
const args = JSON.parse(fn.arguments);
|
||||
const result = await handler(args);
|
||||
parts.push(`${result}`);
|
||||
} catch (e) {
|
||||
parts.push(`❌ Tool ${fn.name} error: ${e.message}`);
|
||||
}
|
||||
}
|
||||
return parts.join('\n\n');
|
||||
}
|
||||
return msg.content || '✅ Done.';
|
||||
} catch (error) {
|
||||
logger.error('AI error:', error.response?.data || error.message);
|
||||
return `❌ ${error.response?.data?.error?.message || error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
const toolHandlers = {
|
||||
bash: async (args) => {
|
||||
const tool = svc.toolMap.get('bash');
|
||||
if (!tool) return '❌ Bash tool unavailable.';
|
||||
try {
|
||||
const r = await tool.execute(args.command, { timeout: args.timeout || 300000 });
|
||||
const out = (r.stdout || '').slice(0, 3000);
|
||||
const err = (r.stderr || '').slice(0, 3000);
|
||||
if (r.success) return `✅ \`\`\`\n${out || '(no output)'}\n\`\`\``;
|
||||
return `❌ Exit ${r.code}\n\`\`\`\n${err || out}\n\`\`\``;
|
||||
} catch (e) { return `❌ Bash error: ${e.message}`; }
|
||||
},
|
||||
web_search: async (args) => {
|
||||
const tool = svc.toolMap.get('web_search');
|
||||
if (!tool) return '❌ Web search unavailable.';
|
||||
try {
|
||||
const r = await tool.search(args.query, { numResults: args.num_results || 5 });
|
||||
if (r?.success && r.results?.length) {
|
||||
return `🔍 *${args.query}*\n\n${r.results.slice(0, 5).map((x, i) =>
|
||||
`${i + 1}. [${x.title}](${x.url})\n${x.snippet || ''}`).join('\n\n')}`;
|
||||
}
|
||||
return `🔍 *${args.query}*\n\nNo results.`;
|
||||
} catch (e) { return `❌ Search error: ${e.message}`; }
|
||||
},
|
||||
git: async (args) => {
|
||||
const tool = svc.toolMap.get('git');
|
||||
if (!tool) return '❌ Git tool unavailable.';
|
||||
try {
|
||||
const method = tool[args.action];
|
||||
if (!method) return `❌ Unknown: ${args.action}`;
|
||||
const r = await method.call(tool, ...(args.params || []));
|
||||
return r.success ? `✅ ${r.status || JSON.stringify(r)}` : `❌ ${r.stderr || r.error}`;
|
||||
} catch (e) { return `❌ Git error: ${e.message}`; }
|
||||
},
|
||||
delegate_agent: async (args) => {
|
||||
const agent = svc.agents.find(a => a.id === args.agent_id);
|
||||
if (!agent) return `❌ Agent not found: ${args.agent_id}`;
|
||||
return `✅ Delegated to **${agent.name}**: "${args.task}"`;
|
||||
},
|
||||
run_skill: async (args) => {
|
||||
const skill = svc.skills.find(s => s.name === args.skill);
|
||||
if (!skill) return `❌ Skill not found: ${args.skill}`;
|
||||
return `✅ Skill **${skill.name}** queued: ${args.input || '(none)'}`;
|
||||
},
|
||||
};
|
||||
|
||||
// ── Create grammy bot ──
|
||||
const bot = new Bot(botToken);
|
||||
bot.api.config.use(autoRetry({ maxRetryAttempts: 5, maxDelaySeconds: 60 }));
|
||||
|
||||
// Register bot command menu
|
||||
const cmdList = [
|
||||
{ command: 'start', description: '🚀 Show help' },
|
||||
{ command: 'tools', description: '🔧 List available tools' },
|
||||
{ command: 'skills', description: '📚 List loaded skills' },
|
||||
{ command: 'agents', description: '🤖 List agent roles' },
|
||||
{ command: 'model', description: '🤖 Switch AI model' },
|
||||
{ command: 'stats', description: '📊 System & RTK stats' },
|
||||
{ command: 'voice', description: '🎤 Voice I/O info' },
|
||||
{ command: 'mcp', description: '🔌 MCP info' },
|
||||
{ command: 'memory', description: '🧠 Memory info' },
|
||||
{ command: 'cron', description: '⏰ Scheduled tasks' },
|
||||
{ command: 'bash', description: '💻 Execute shell command' },
|
||||
{ command: 'web', description: '🔍 Search the web' },
|
||||
{ command: 'git', description: '🔀 Git operations' },
|
||||
];
|
||||
bot.api.setMyCommands(cmdList).catch(e =>
|
||||
logger.warn('⚠ Failed to register commands:', e.message));
|
||||
|
||||
// ── Auth middleware (claudegram pattern) ──
|
||||
const allowedUsers = env.TELEGRAM_ALLOWED_USERS
|
||||
? env.TELEGRAM_ALLOWED_USERS.split(',').map(s => s.trim())
|
||||
: null;
|
||||
bot.use(async (ctx, next) => {
|
||||
if (!allowedUsers) return next(); // allow all
|
||||
const userId = String(ctx.from?.id || '');
|
||||
if (!allowedUsers.includes(userId)) {
|
||||
await ctx.reply('⛔ Unauthorized.');
|
||||
return;
|
||||
}
|
||||
return next();
|
||||
});
|
||||
|
||||
// ── Sequentialize per-chat (claudegram pattern) ──
|
||||
bot.use(sequentialize((ctx) => {
|
||||
const chatId = ctx.chat?.id;
|
||||
if (!chatId) return undefined;
|
||||
const msg = ctx.message ?? ctx.callbackQuery?.message;
|
||||
const threadId = msg?.is_topic_message ? msg.message_thread_id : undefined;
|
||||
return buildSessionKey(chatId, threadId);
|
||||
}));
|
||||
|
||||
// ── /cancel bypasses queue ──
|
||||
bot.command('cancel', async (ctx) => {
|
||||
const key = buildSessionKey(ctx.chat.id, ctx.message?.message_thread_id);
|
||||
clearQueue(key);
|
||||
await ctx.reply('⏹️ Cancelled and queue cleared.');
|
||||
});
|
||||
|
||||
// ── COMMAND HANDLERS ──
|
||||
bot.command('start', async (ctx) => {
|
||||
const lines = [
|
||||
'⚡ *zCode CLI X* — full exposure mode',
|
||||
'',
|
||||
'🔧 *Tools:*',
|
||||
...svc.tools.map(t => ` /${t.name} — ${t.description}`),
|
||||
'',
|
||||
'📚 *Skills:* ' + svc.skills.length + ' loaded',
|
||||
'🤖 *Agents:* ' + svc.agents.length + ' available',
|
||||
'',
|
||||
'📋 *Commands:* /tools /skills /agents /model /stats /voice /mcp /memory /cron /cancel',
|
||||
'',
|
||||
'Or just chat — I\'ll use tools when needed.',
|
||||
`Model: \`${svc.config?.api?.models?.default || 'glm-5.1'}\``,
|
||||
];
|
||||
await ctx.reply(lines.join('\n'), { parse_mode: 'Markdown' });
|
||||
});
|
||||
|
||||
bot.command('tools', async (ctx) => {
|
||||
const lines = ['🔧 *Tools:*\n'];
|
||||
for (const t of svc.tools) lines.push(`• \`${t.name}\` — ${t.description}`);
|
||||
await ctx.reply(lines.join('\n'), { parse_mode: 'Markdown' });
|
||||
});
|
||||
|
||||
bot.command('skills', async (ctx) => {
|
||||
if (!svc.skills.length) return ctx.reply('📚 No skills loaded.');
|
||||
const groups = {};
|
||||
for (const s of svc.skills) { (groups[s.category] ||= []).push(s); }
|
||||
const lines = ['📚 *Skills:*\n'];
|
||||
for (const [cat, items] of Object.entries(groups)) {
|
||||
lines.push(`*${cat}:*`);
|
||||
for (const s of items) lines.push(` • \`${s.name}\` — ${s.description}`);
|
||||
lines.push('');
|
||||
}
|
||||
await ctx.reply(lines.join('\n'), { parse_mode: 'Markdown' });
|
||||
});
|
||||
|
||||
bot.command('agents', async (ctx) => {
|
||||
const lines = ['🤖 *Agent Roles:*\n'];
|
||||
for (const a of svc.agents) {
|
||||
lines.push(`• *${a.name}* (\`${a.id}\`)`);
|
||||
lines.push(` ${a.description}`);
|
||||
}
|
||||
await ctx.reply(lines.join('\n'), { parse_mode: 'Markdown' });
|
||||
});
|
||||
|
||||
bot.command('model', async (ctx) => {
|
||||
const text = ctx.match?.trim();
|
||||
if (!text) {
|
||||
return ctx.reply(`Current: \`${svc.config?.api?.models?.default || 'glm-5.1'}\`\nUsage: /model <name>`);
|
||||
}
|
||||
svc.config.api.models = svc.config.api.models || {};
|
||||
svc.config.api.models.default = text;
|
||||
await ctx.reply(`✅ Switched to \`${text}\``, { parse_mode: 'Markdown' });
|
||||
});
|
||||
|
||||
bot.command('stats', async (ctx) => {
|
||||
const rtkStats = svc.rtk.enabled ? svc.rtk.getTrackingStats() : null;
|
||||
const lines = [
|
||||
'📊 *Stats:*\n',
|
||||
`Tools: ${svc.tools.length}`,
|
||||
`Skills: ${svc.skills.length}`,
|
||||
`Agents: ${svc.agents.length}`,
|
||||
`Model: \`${svc.config?.api?.models?.default || 'glm-5.1'}\``,
|
||||
`RTK: ${svc.rtk.enabled ? '✅' : '❌'}`,
|
||||
];
|
||||
if (rtkStats) {
|
||||
lines.push(`Optimized: ${rtkStats.totalOptimized || 0}`);
|
||||
lines.push(`Saved: ${rtkStats.totalTokensSaved || 0} tok`);
|
||||
}
|
||||
await ctx.reply(lines.join('\n'), { parse_mode: 'Markdown' });
|
||||
});
|
||||
|
||||
bot.command('voice', async (ctx) => {
|
||||
await ctx.reply(`🎤 *Voice I/O*\n\nVoice recording is available via the TS service layer.\nSend me a voice message and I'll transcribe it.`);
|
||||
});
|
||||
|
||||
bot.command('mcp', async (ctx) => {
|
||||
await ctx.reply('🔌 *MCP:* Available via TS services at `src/services/mcp/`. Connect MCP servers for extended capabilities.');
|
||||
});
|
||||
|
||||
bot.command('memory', async (ctx) => {
|
||||
await ctx.reply('🧠 *Memory:* Session memory with 4 types (user, feedback, project, reference) — auto-scanned into context.');
|
||||
});
|
||||
|
||||
bot.command('cron', async (ctx) => {
|
||||
await ctx.reply('⏰ *Cron:* CronScheduler at `src/utils/cronScheduler.ts` — 1s interval, task locking, auto-recovery.');
|
||||
});
|
||||
|
||||
// ── Dynamic tool commands ──
|
||||
for (const tool of svc.tools) {
|
||||
const tname = tool.name;
|
||||
if (bot.command(tname)) continue; // skip if registered
|
||||
bot.command(tname, async (ctx) => {
|
||||
const args = ctx.match?.trim();
|
||||
const handler = toolHandlers[tname];
|
||||
if (!handler) return ctx.reply(`❌ No handler for ${tname}`);
|
||||
if (!args) return ctx.reply(`Usage: /${tname} <input>\n${tool.description}`);
|
||||
const result = await handler(tname === 'web_search'
|
||||
? { query: args, num_results: 5 }
|
||||
: tname === 'bash'
|
||||
? { command: args, timeout: 300000 }
|
||||
: tname === 'git'
|
||||
? (() => { const p = args.split(/\s+/); return { action: p[0], params: p.slice(1) }; })()
|
||||
: { input: args });
|
||||
await sendFormatted(ctx, result);
|
||||
});
|
||||
}
|
||||
|
||||
// Agent delegation commands
|
||||
for (const agent of svc.agents) {
|
||||
const cmdName = `agent_${agent.id}`;
|
||||
bot.command(cmdName, async (ctx) => {
|
||||
const task = ctx.match?.trim();
|
||||
if (!task) return ctx.reply(`🤖 *${agent.name}*\n${agent.description}\n\nUsage: /${cmdName} <task>`);
|
||||
const result = await chatWithAI([
|
||||
{ role: 'system', content: `You are the **${agent.name}** (${agent.id}). Capabilities: ${agent.capabilities.join(', ')}. Respond as this specialist within zCode CLI X.` },
|
||||
{ role: 'user', content: task },
|
||||
]);
|
||||
await sendFormatted(ctx, result);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Message text handler (with dedup + queue) ──
|
||||
bot.on('message:text', async (ctx) => {
|
||||
if (isDuplicate(ctx.message.message_id)) return;
|
||||
markProcessed(ctx.message.message_id);
|
||||
|
||||
const key = buildSessionKey(ctx.chat.id, ctx.message?.message_thread_id);
|
||||
const text = ctx.message.text;
|
||||
const user = ctx.from?.username || ctx.from?.first_name || 'Unknown';
|
||||
logger.info(`💬 ${user}: ${text.substring(0, 80)}…`);
|
||||
|
||||
await queueRequest(key, text, async () => {
|
||||
await ctx.api.sendChatAction(ctx.chat.id, 'typing');
|
||||
const result = await chatWithAI([
|
||||
{ role: 'system', content: buildSystemPrompt(svc) },
|
||||
{ role: 'user', content: text },
|
||||
]);
|
||||
await sendFormatted(ctx, result);
|
||||
});
|
||||
});
|
||||
|
||||
// Send message via webhook
|
||||
async function sendTelegramMessage(chatId, text, options = {}) {
|
||||
const url = `https://api.telegram.org/bot${botToken}/sendMessage`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
chat_id: chatId,
|
||||
text,
|
||||
parse_mode: 'Markdown',
|
||||
...options,
|
||||
}),
|
||||
});
|
||||
// ── Voice handler ──
|
||||
bot.on('message:voice', async (ctx) => {
|
||||
const fileId = ctx.message.voice.file_id;
|
||||
const user = ctx.from?.username || ctx.from?.first_name || 'Unknown';
|
||||
logger.info(`🎤 Voice from ${user}`);
|
||||
await ctx.reply('🎤 Voice received! (STT via Whisper TBD)');
|
||||
const file = await ctx.api.getFile(fileId);
|
||||
const url = `https://api.telegram.org/file/bot${botToken}/${file.file_path}`;
|
||||
logger.info(`Voice file: ${url}`);
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.ok) {
|
||||
logger.error(`Telegram API error: ${data.description}`);
|
||||
}
|
||||
return data;
|
||||
} catch (error) {
|
||||
logger.error('Failed to send Telegram message:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// ── Photo handler ──
|
||||
bot.on('message:photo', async (ctx) => {
|
||||
logger.info(`📸 Photo from ${ctx.from?.username}`);
|
||||
// TODO: vision analysis
|
||||
await ctx.reply('📸 Image received. Vision analysis TBD.');
|
||||
});
|
||||
|
||||
// Send message via WebSocket (for real-time updates)
|
||||
function sendWebSocketMessage(chatId, message) {
|
||||
const ws = connections.get(chatId);
|
||||
if (ws && ws.readyState === ws.OPEN) {
|
||||
ws.send(JSON.stringify(message));
|
||||
}
|
||||
}
|
||||
// ── Error handler ──
|
||||
bot.catch((err) => {
|
||||
logger.error('Bot error:', err.message || err);
|
||||
});
|
||||
|
||||
// Process incoming message from Telegram
|
||||
async function processMessage(chatId, text, user) {
|
||||
try {
|
||||
const provider = new (await import('../api/index.js')).ZAIProvider(api);
|
||||
const response = await provider.complete(text, { model: 'glm-5.1' });
|
||||
const reply = response.content || '🤖 *zCode*: ' + response;
|
||||
await sendTelegramMessage(chatId, reply);
|
||||
} catch (error) {
|
||||
logger.error('Error processing message:', error.message);
|
||||
await sendTelegramMessage(chatId, '❌ Sorry, an error occurred while processing your message.');
|
||||
}
|
||||
}
|
||||
// ── Express + WebSocket server (keep for webhook compatibility) ──
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
const httpServer = createServer(app);
|
||||
const wss = new WebSocketServer({ server: httpServer });
|
||||
const wsClients = new Map();
|
||||
|
||||
// Process callback query
|
||||
async function processCallback(chatId, data) {
|
||||
try {
|
||||
const provider = new (await import('../api/index.js')).ZAIProvider(api);
|
||||
const response = await provider.complete(data, { model: 'glm-5.1' });
|
||||
const reply = response.content || '🤖 *zCode*: ' + response;
|
||||
await sendTelegramMessage(chatId, reply);
|
||||
} catch (error) {
|
||||
logger.error('Error processing callback:', error.message);
|
||||
}
|
||||
}
|
||||
wss.on('connection', (ws, req) => {
|
||||
const chatId = req.url?.startsWith('/?chatId=') ? req.url.slice(8) : 'ws';
|
||||
wsClients.set(chatId, ws);
|
||||
ws.on('close', () => wsClients.delete(chatId));
|
||||
logger.info(`🔌 WS: ${chatId}`);
|
||||
});
|
||||
|
||||
// Handle webhook
|
||||
// Handle webhook verification (Telegram GET request)
|
||||
// Webhook handler — feeds into grammy
|
||||
app.get('/telegram/webhook', (req, res) => {
|
||||
const { hub_mode, hub_challenge } = req.query;
|
||||
|
||||
// Telegram sends a GET request for webhook verification
|
||||
if (hub_mode === 'subscribe' && hub_challenge) {
|
||||
logger.info('✓ Webhook verified by Telegram');
|
||||
return res.status(200).send(hub_challenge);
|
||||
}
|
||||
|
||||
// Return 200 for any other GET requests (for health checks, etc.)
|
||||
return res.status(200).json({ ok: true, message: 'zCode webhook is active' });
|
||||
const { 'hub.mode': mode, 'hub.challenge': challenge } = req.query;
|
||||
if (mode === 'subscribe' && challenge) return res.status(200).send(challenge);
|
||||
res.status(200).json({ ok: true, message: 'zCode webhook active' });
|
||||
});
|
||||
|
||||
// Handle webhook POST
|
||||
app.post('/telegram/webhook', async (req, res) => {
|
||||
const update = req.body;
|
||||
|
||||
if (update.message) {
|
||||
const chatId = update.message.chat.id.toString();
|
||||
const text = update.message.text;
|
||||
const user = update.message.from?.username || update.message.from?.first_name || 'Unknown';
|
||||
|
||||
logger.info(`📨 New message from ${user} (${chatId}): ${text.substring(0, 50)}...`);
|
||||
|
||||
// Process message
|
||||
await processMessage(chatId, text, user);
|
||||
|
||||
res.json({ ok: true });
|
||||
} else if (update.callback_query) {
|
||||
const chatId = update.callback_query.message.chat.id.toString();
|
||||
const data = update.callback_query.data;
|
||||
|
||||
logger.info(`🔘 Callback from ${chatId}: ${data}`);
|
||||
|
||||
// Process callback
|
||||
await processCallback(chatId, data);
|
||||
|
||||
// Send answer
|
||||
await sendTelegramMessage(chatId, 'Callback processed', {
|
||||
callback_query_id: update.callback_query.id,
|
||||
});
|
||||
|
||||
res.json({ ok: true });
|
||||
} else {
|
||||
res.json({ ok: true });
|
||||
res.json({ ok: true }); // ack immediately
|
||||
try {
|
||||
await bot.handleUpdate(req.body);
|
||||
} catch (e) {
|
||||
logger.error('Webhook update error:', e.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Set webhook (if URL provided)
|
||||
async function setWebhook() {
|
||||
const webhookUrl = process.env.ZCODE_WEBHOOK_URL;
|
||||
if (webhookUrl) {
|
||||
const url = `https://api.telegram.org/bot${botToken}/setWebhook`;
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url: webhookUrl }),
|
||||
});
|
||||
// Health check
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
ok: true, uptime: process.uptime(),
|
||||
tools: svc.tools.length, skills: svc.skills.length, agents: svc.agents.length,
|
||||
wsClients: wsClients.size,
|
||||
});
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.ok) {
|
||||
logger.info('✓ Webhook set successfully');
|
||||
} else {
|
||||
logger.error('✗ Failed to set webhook:', data.description);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start HTTP server
|
||||
const PORT = process.env.ZCODE_PORT || 3000;
|
||||
httpServer.listen(PORT, () => {
|
||||
logger.info(`✓ HTTP server running on port ${PORT}`);
|
||||
logger.info(`✓ WebSocket server ready`);
|
||||
logger.info(`✓ HTTP on :${PORT} · WS ready · grammy bot online`);
|
||||
logger.info(`✓ ${svc.tools.length} tools · ${svc.skills.length} skills · ${svc.agents.length} agents`);
|
||||
});
|
||||
|
||||
// Set webhook and keep process alive
|
||||
await setWebhook();
|
||||
// Set webhook
|
||||
const wu = process.env.ZCODE_WEBHOOK_URL;
|
||||
if (wu) {
|
||||
try {
|
||||
await bot.api.setWebhook(wu, { allowed_updates: ['message', 'callback_query'] });
|
||||
logger.info('✓ Webhook set');
|
||||
} catch (e) {
|
||||
logger.warn('⚠ Webhook failed:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
send: sendTelegramMessage,
|
||||
ws: sendWebSocketMessage,
|
||||
waitForMessages: async () => {
|
||||
// Keep process alive
|
||||
await new Promise(() => {});
|
||||
},
|
||||
getConnections: () => connections.size,
|
||||
send: (chatId, text) => bot.api.sendMessage(chatId, text, { parse_mode: 'Markdown' }),
|
||||
ws: (chatId, msg) => wsClients.get(chatId)?.send(JSON.stringify(msg)),
|
||||
waitForMessages: async () => { await new Promise(() => {}); },
|
||||
getConnections: () => wsClients.size,
|
||||
};
|
||||
}
|
||||
|
||||
48
src/bot/message-sender.js
Normal file
48
src/bot/message-sender.js
Normal file
@@ -0,0 +1,48 @@
|
||||
// Adapted from claudegram's message-sender.ts — send with retry, chunking, fallback
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
const MAX_MSG_LENGTH = 4000;
|
||||
|
||||
export function splitMessage(text) {
|
||||
if (text.length <= MAX_MSG_LENGTH) return [text];
|
||||
const chunks = [];
|
||||
let remaining = text;
|
||||
while (remaining.length > 0) {
|
||||
chunks.push(remaining.slice(0, MAX_MSG_LENGTH));
|
||||
remaining = remaining.slice(MAX_MSG_LENGTH);
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
export function escapeMarkdown(text) {
|
||||
if (!text) return '';
|
||||
return text
|
||||
.replace(/_/g, '\\_')
|
||||
.replace(/\*/g, '\\*')
|
||||
.replace(/\[/g, '\\[')
|
||||
.replace(/`/g, '\\`');
|
||||
}
|
||||
|
||||
export async function sendFormatted(ctx, text) {
|
||||
if (!text) return;
|
||||
|
||||
try {
|
||||
// Try Markdown first
|
||||
const chunks = splitMessage(text);
|
||||
for (const chunk of chunks) {
|
||||
await ctx.reply(chunk, { parse_mode: 'Markdown' });
|
||||
}
|
||||
} catch {
|
||||
// Fallback to plain text
|
||||
logger.warn('Markdown send failed, falling back to plain text');
|
||||
const chunks = splitMessage(text);
|
||||
for (const chunk of chunks) {
|
||||
await ctx.reply(chunk, { parse_mode: undefined });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendLongMessage(ctx, text) {
|
||||
if (!text) return;
|
||||
return sendFormatted(ctx, text);
|
||||
}
|
||||
50
src/bot/request-queue.js
Normal file
50
src/bot/request-queue.js
Normal file
@@ -0,0 +1,50 @@
|
||||
// Adapted from claudegram's request-queue.ts — per-chat sequential processing
|
||||
const pendingQueues = new Map();
|
||||
const processingFlags = new Map();
|
||||
|
||||
export function isProcessing(sessionKey) {
|
||||
return processingFlags.get(sessionKey) === true;
|
||||
}
|
||||
|
||||
export function getQueuePosition(sessionKey) {
|
||||
const queue = pendingQueues.get(sessionKey);
|
||||
return queue ? queue.length : 0;
|
||||
}
|
||||
|
||||
export function clearQueue(sessionKey) {
|
||||
pendingQueues.delete(sessionKey);
|
||||
processingFlags.delete(sessionKey);
|
||||
}
|
||||
|
||||
export async function queueRequest(sessionKey, message, handler) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = { message, handler, resolve, reject };
|
||||
let queue = pendingQueues.get(sessionKey);
|
||||
if (!queue) {
|
||||
queue = [];
|
||||
pendingQueues.set(sessionKey, queue);
|
||||
}
|
||||
queue.push(request);
|
||||
processQueue(sessionKey);
|
||||
});
|
||||
}
|
||||
|
||||
async function processQueue(sessionKey) {
|
||||
if (processingFlags.get(sessionKey)) return;
|
||||
processingFlags.set(sessionKey, true);
|
||||
|
||||
const queue = pendingQueues.get(sessionKey);
|
||||
while (queue && queue.length > 0) {
|
||||
const request = queue[0];
|
||||
try {
|
||||
const result = await request.handler();
|
||||
request.resolve(result);
|
||||
} catch (error) {
|
||||
request.reject(error);
|
||||
}
|
||||
queue.shift();
|
||||
}
|
||||
|
||||
processingFlags.delete(sessionKey);
|
||||
pendingQueues.delete(sessionKey);
|
||||
}
|
||||
44
src/bot/self-correction.js
Normal file
44
src/bot/self-correction.js
Normal file
@@ -0,0 +1,44 @@
|
||||
// Self-correction loop — retry on failure with backoff & simplified approach
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
const MAX_RETRIES = 2;
|
||||
const RETRY_DELAY_MS = 500;
|
||||
|
||||
function shouldRetry(content) {
|
||||
if (!content) return true;
|
||||
const lowered = content.toLowerCase();
|
||||
if (lowered.includes('❌') && lowered.includes('error')) return true;
|
||||
if (lowered.includes('rate limit') || lowered.includes('timeout')) return true;
|
||||
if (lowered.includes('internal server error') || lowered.includes('5xx')) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export function withSelfCorrection(fn) {
|
||||
return async (...args) => {
|
||||
let lastError;
|
||||
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
||||
try {
|
||||
const result = await fn(...args);
|
||||
if (typeof result === 'string' && shouldRetry(result) && attempt < MAX_RETRIES) {
|
||||
logger.warn(`Self-correct: retry ${attempt + 1}/${MAX_RETRIES} — error in response`);
|
||||
await new Promise(r => setTimeout(r, RETRY_DELAY_MS * (attempt + 1)));
|
||||
// Simplify the prompt on retry
|
||||
const lastMsg = args[1]?.[args[1].length - 1];
|
||||
if (lastMsg) lastMsg.content = `[SIMPLIFIED RETRY ${attempt + 1}] ${lastMsg.content.slice(0, 500)}`;
|
||||
continue;
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
lastError = err;
|
||||
if (attempt < MAX_RETRIES) {
|
||||
logger.warn(`Self-correct: retry ${attempt + 1}/${MAX_RETRIES} — ${err.message}`);
|
||||
await new Promise(r => setTimeout(r, RETRY_DELAY_MS * (attempt + 1)));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
const msg = lastError ? `❌ Failed after ${MAX_RETRIES + 1} attempts: ${lastError.message}` : '❌ Failed after retries';
|
||||
logger.error(msg);
|
||||
return msg;
|
||||
};
|
||||
}
|
||||
@@ -21,5 +21,6 @@ export function checkEnv() {
|
||||
GLM_BASE_URL: process.env.GLM_BASE_URL || '',
|
||||
TELEGRAM_BOT_TOKEN: process.env.TELEGRAM_BOT_TOKEN || '',
|
||||
TELEGRAM_ALLOWED_USERS: process.env.TELEGRAM_ALLOWED_USERS || '',
|
||||
DISCORD_TOKEN: process.env.DISCORD_TOKEN || '',
|
||||
};
|
||||
}
|
||||
|
||||
87
src/zcode.js
87
src/zcode.js
@@ -5,11 +5,11 @@ import { initTools } from './tools/index.js';
|
||||
import { initSkills } from './skills/index.js';
|
||||
import { initAgents } from './agents/index.js';
|
||||
import { checkEnv } from './utils/env.js';
|
||||
import { registerChannel, getChannels } from './bot/delivery-hub.js';
|
||||
|
||||
export async function zcode(options) {
|
||||
logger.info('🚀 Initializing zCode CLI X...');
|
||||
|
||||
// 1. Check environment
|
||||
|
||||
const env = checkEnv();
|
||||
if (!env.valid) {
|
||||
logger.error('Missing required environment variables:');
|
||||
@@ -19,53 +19,74 @@ export async function zcode(options) {
|
||||
|
||||
logger.info('✓ Environment validated');
|
||||
logger.info(`Z.AI API Key: ${env.ZAI_API_KEY.substring(0, 10)}...`);
|
||||
logger.info(`Telegram Bot Token: ${env.TELEGRAM_BOT_TOKEN ? 'Configured' : 'Not configured'}`);
|
||||
logger.info(`Telegram: ${env.TELEGRAM_BOT_TOKEN ? '✅' : '❌'}`);
|
||||
|
||||
// 2. Initialize configuration
|
||||
// Init core services
|
||||
const config = await initConfig();
|
||||
logger.info('✓ Configuration loaded');
|
||||
|
||||
// 3. Initialize Z.AI API
|
||||
const api = await initAPI();
|
||||
logger.info('✓ Z.AI API connected');
|
||||
|
||||
// 4. Initialize tools
|
||||
const tools = await initTools();
|
||||
logger.info(`✓ Tools loaded: ${tools.length} available`);
|
||||
|
||||
// 5. Initialize skills
|
||||
const skills = await initSkills();
|
||||
logger.info(`✓ Skills loaded: ${skills.length} available`);
|
||||
|
||||
// 6. Initialize agents
|
||||
const agents = await initAgents();
|
||||
logger.info(`✓ Agents loaded: ${agents.length} available`);
|
||||
|
||||
// 7. Initialize Telegram bot (if enabled)
|
||||
// Register telegram delivery channel (filled after bot init)
|
||||
const deliveryTargets = new Map();
|
||||
|
||||
registerChannel('log', async (msg) => {
|
||||
logger.info(`[broadcast] ${msg.substring(0, 200)}`);
|
||||
});
|
||||
|
||||
// Init Telegram bot
|
||||
let bot;
|
||||
if (options.bot !== false && env.TELEGRAM_BOT_TOKEN) {
|
||||
// Import bot module dynamically to avoid circular dependency
|
||||
const botModule = await import('./bot/index.js');
|
||||
const bot = await botModule.initBot(config, api, tools, skills);
|
||||
logger.info('✓ Telegram bot initialized');
|
||||
|
||||
// Keep process alive for bot
|
||||
logger.info('🤖 zCode CLI X is now running 24/7');
|
||||
logger.info('Type your commands or just chat with the bot!');
|
||||
|
||||
// Wait for bot to handle messages
|
||||
bot = await botModule.initBot(config, api, tools, skills, agents);
|
||||
if (bot) {
|
||||
deliveryTargets.set('telegram', bot.send);
|
||||
registerChannel('telegram', (msg) => bot.send(env.TELEGRAM_ALLOWED_USERS?.split(',')[0] || '6352861167', msg));
|
||||
logger.info('✓ Telegram bot initialized');
|
||||
}
|
||||
}
|
||||
|
||||
// Init Discord bot (opt-in via DISCORD_TOKEN env)
|
||||
if (env.DISCORD_TOKEN) {
|
||||
try {
|
||||
const { initDiscord } = await import('./bot/discord.js');
|
||||
const discordClient = await initDiscord(env.DISCORD_TOKEN, {
|
||||
config, api, tools, skills, agents,
|
||||
}, (messages) => {
|
||||
// Inline minimal chat for Discord
|
||||
return api.client.post('/chat/completions', {
|
||||
model: config.api?.models?.default || 'glm-5.1',
|
||||
messages,
|
||||
temperature: 0.7,
|
||||
max_tokens: 4096,
|
||||
}).then(r => r.data.choices?.[0]?.message?.content || '✅ Done.').catch(e => `❌ ${e.message}`);
|
||||
});
|
||||
if (discordClient) {
|
||||
deliveryTargets.set('discord', discordClient);
|
||||
logger.info('✓ Discord bot initialized');
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn('⚠ Discord init skipped:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Log loaded services
|
||||
logger.info(`✓ ${tools.length} tools · ${skills.length} skills · ${agents.length} agents`);
|
||||
logger.info(`📡 Delivery channels: ${getChannels().join(', ') || 'none'}`);
|
||||
|
||||
// Keep alive
|
||||
if (bot) {
|
||||
logger.info('🤖 zCode CLI X running 24/7');
|
||||
await bot.waitForMessages();
|
||||
} else if (options.cli !== false) {
|
||||
// CLI-only mode
|
||||
logger.info('🔧 CLI mode: Run interactive mode');
|
||||
logger.info('🔧 CLI mode');
|
||||
await runInteractiveMode(config, api, tools, skills);
|
||||
} else {
|
||||
logger.info('🤖 Bot mode: zCode is running in the background');
|
||||
logger.info(' Telegram bot will handle all interactions');
|
||||
logger.info('🤖 Background mode');
|
||||
}
|
||||
}
|
||||
|
||||
async function runInteractiveMode(config, api, tools, skills) {
|
||||
// TODO: Implement interactive CLI mode
|
||||
console.log('Interactive mode coming soon!');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user