pi-telegram-plus 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -232
- package/index.ts +17 -5
- package/lib/attachments.ts +7 -3
- package/lib/commands/session.ts +4 -1
- package/lib/commands/telegram-commands.ts +7 -4
- package/lib/config.ts +6 -3
- package/lib/controller.ts +53 -14
- package/lib/custom-dialogs.ts +668 -0
- package/lib/heartbeat.ts +5 -1
- package/lib/logger.ts +304 -0
- package/lib/markdown.ts +267 -61
- package/lib/menu-commands.ts +4 -1
- package/lib/polling.ts +13 -8
- package/lib/renderer.ts +141 -14
- package/lib/telegram-api.ts +83 -28
- package/lib/telegram-ui.ts +89 -10
- package/lib/text-split.ts +216 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
# pi-telegram-plus
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
<a href="https://www.npmjs.com/package/pi-telegram-plus"><img src="https://img.shields.io/npm/v/pi-telegram-plus?style=flat-square&logo=npm" alt="npm" /></a>
|
|
5
|
-
<a href="https://github.com/jalyfeng/pi-telegram-plus"><img src="https://img.shields.io/github/license/jalyfeng/pi-telegram-plus?style=flat-square" alt="license" /></a>
|
|
6
|
-
<img src="https://img.shields.io/badge/node-%3E%3D22.19.0-339933?style=flat-square&logo=node.js" alt="node" />
|
|
7
|
-
</p>
|
|
3
|
+
## Overview
|
|
8
4
|
|
|
9
5
|
**Full Telegram control of [pi coding agent](https://github.com/earendil-works/pi-coding-agent) — commands, interactive UI, model/session management, file transfer, and real-time streaming output, all from Telegram.**
|
|
10
6
|
|
|
@@ -19,27 +15,15 @@
|
|
|
19
15
|
- **Multi-instance safe** — file-based polling lock prevents multiple pi instances from racing on the same bot token
|
|
20
16
|
- **Automatic reconnection** — exponential backoff on transient failures
|
|
21
17
|
- **Bot command menu sync** — automatically syncs available commands to Telegram's BotMenu (up to 100 commands)
|
|
18
|
+
- **Authorized user** — the first Telegram user to message the bot is auto-authorized and persisted; all other users are rejected (reset by removing the binding and re-setting up)
|
|
19
|
+
- **TUI status line** — a `telegram+` indicator in the pi status bar shows connection state (connected / active / awaiting pairing / disconnected / not configured / error)
|
|
20
|
+
- **Typing indicator** — sends `typing` chat-action pulses while a turn is active so the Telegram chat shows the bot is working
|
|
22
21
|
|
|
23
22
|
### 🎮 Full Session Control
|
|
24
|
-
All pi session lifecycle commands available via Telegram
|
|
25
|
-
|
|
26
|
-
| Command | Description |
|
|
27
|
-
|---------|-------------|
|
|
28
|
-
| `/new` | Start a new session |
|
|
29
|
-
| `/fork` | Fork from a previous user message |
|
|
30
|
-
| `/clone` | Clone at a previous user message |
|
|
31
|
-
| `/tree` | Navigate session tree |
|
|
32
|
-
| `/resume` | Resume a previous session |
|
|
33
|
-
| `/compact` | Compact session context |
|
|
34
|
-
| `/name` | Set or show session name |
|
|
35
|
-
| `/session` | Show session statistics |
|
|
23
|
+
All pi session lifecycle commands are available via Telegram — start, fork, clone, navigate, resume, compact, rename, and inspect sessions. See **Session Control Commands** in the Usage Guide.
|
|
36
24
|
|
|
37
25
|
### 🧠 Model & Authentication Management
|
|
38
|
-
|
|
39
|
-
- `/scoped-models` — Toggle scoped model sets
|
|
40
|
-
- `/thinking` — Adjust thinking level (off/minimal/low/medium/high/xhigh)
|
|
41
|
-
- `/login` — OAuth or API key authentication with full interactive flow
|
|
42
|
-
- `/logout` — Remove stored credentials
|
|
26
|
+
Switch the current model, toggle scoped model sets, adjust the thinking level, and complete OAuth or API key authentication with full interactive flows — all from Telegram. See **Model & Authentication Commands** in the Usage Guide.
|
|
43
27
|
|
|
44
28
|
### 📨 Message Modes
|
|
45
29
|
Two modes for handling incoming messages while the agent is running:
|
|
@@ -56,12 +40,31 @@ Full interactive UI components built on inline keyboards:
|
|
|
56
40
|
- **InputSecret** — same as Input, but the prompt message is auto-deleted after reply to protect sensitive data
|
|
57
41
|
- **Select** — paginated option list with Prev/Next navigation
|
|
58
42
|
- **Editor** — multi-line text input prompt
|
|
43
|
+
- **Custom (third-party)** — `ctx.ui.custom(factory)` dialogs from extensions like [`@capyup/pi-goal`](https://www.npmjs.com/package/@capyup/pi-goal) are bridged to inline buttons. See **Third-party Dialog Support** below.
|
|
44
|
+
|
|
45
|
+
### 🧩 Third-party Dialog Support
|
|
46
|
+
|
|
47
|
+
Third-party extensions that use `ctx.ui.custom(factory)` (such as [`@capyup/pi-goal`](https://www.npmjs.com/package/@capyup/pi-goal))
|
|
48
|
+
are bridged to Telegram inline buttons so remote turns can interact with them:
|
|
49
|
+
|
|
50
|
+
| Scenario | Telegram behavior |
|
|
51
|
+
|----------|-------------------|
|
|
52
|
+
| **pi-goal draft confirmation** (`propose_goal_draft` → `showProposalDialog`) | Shows ✅ Confirm / 💬 Continue chatting buttons. Confirm creates the goal; Continue lets the agent keep refining; `/stop` or timeout cancels. |
|
|
53
|
+
| **pi-goal `goal_question`** (single question) | Shows the question text, option buttons (paginated if needed) as toggle buttons (☐/☑, multi-select), a ✏️ Type answer button for free-text entry, and Cancel. A ✓ Submit button appears once at least one option is selected (or a custom answer is given); the selected options are joined into a single string answer. Free-text entry finalizes immediately. |
|
|
54
|
+
| **pi-goal `goal_questionnaire`** (multi-question) | Drives the opaque questionnaire component: cycles tabs to extract every question, then presents one question at a time with option buttons as multi-select toggles (☐/☑), ◀ Tab / Tab ▶ navigation between questions (no forced auto-advance on option pick), ✏️ Type for free-text entry, and Cancel. A ✓ Submit button only appears once every question is answered; before that the message shows a `Still to answer: …` placeholder. Per-question selections are joined into single-string answers. Falls back to a `cancelled` degrade only if the component lacks the expected `handleInput`/`render` API. |
|
|
55
|
+
| **Unknown `custom()` components** | Auto-dismissed with a ⚠️ notification and a `cancelled` result, so the agent continues gracefully (never hangs or throws). |
|
|
56
|
+
|
|
57
|
+
**Interactive modals are Telegram-only during a Telegram turn.** Interactive modals (confirm, select, input, editor, custom) are bridged to Telegram inline buttons for the remote user and are NOT also rendered in the local TUI. `ExtensionUIContext.custom` and the other modals expose no external cancel handle, so mirroring a modal into the TUI during a remote turn would leave the local TUI stuck at the dialog once the Telegram side resolves. Local TUI turns never enter the Telegram UI swap, so they keep using the real TUI UIContext and are completely unaffected. Persistent/stateful UI (goal widget, status line, working indicator, footer/header) is forwarded to the TUI base so the local TUI always shows accurate state. Editor operations (paste, set/get text) are no-ops during Telegram turns, so a remote turn never touches the local editor.
|
|
58
|
+
|
|
59
|
+
> **Command-triggered turns are held to the end of the chain.** Commands like `/sisyphus` and `/goals` enqueue the agent turn fire-and-forget via `pi.sendUserMessage` and return immediately; the actual turn (and the `goal_question` / `goal_questionnaire` / `propose_goal_draft` dialogs it raises) runs afterward. The controller keeps the Telegram UI swap active across that enqueued turn and any pi-goal auto-continue chain (waiting for the agent to go idle through a small grace window), so every dialog in the chain bridges to Telegram instead of rendering to the local TUI. The hold is skipped when a local turn was already streaming, so it never hijacks an active local session.
|
|
59
60
|
|
|
60
61
|
### 🎨 Message Rendering
|
|
61
62
|
- **Markdown → Telegram HTML** — Full conversion via `marked` (tables, code blocks, blockquotes, lists, inline formatting)
|
|
63
|
+
- **Mobile-first table rendering** — box-drawing and card layouts with pseudo-table protection and header repetition across split chunks, so wide tables stay readable on phone screens
|
|
62
64
|
- **Tool execution rendering** — Configurable level (`hidden` / `brief` / `full`) for tool call visibility
|
|
63
65
|
- **Thinking rendering** — Configurable level (`hidden` / `brief` / `full`) for agent thinking blocks
|
|
64
66
|
- **Output splitting** — Safe UTF-8-aware splitting at Telegram's 4096-byte limit
|
|
67
|
+
- **Oversized code blocks** — automatically sent as downloadable files instead of being split across many `<pre>` messages
|
|
65
68
|
- **Image output** — Automatically sends agent-generated images as Telegram photos
|
|
66
69
|
|
|
67
70
|
### 📎 File Attachments
|
|
@@ -78,19 +81,58 @@ Full interactive UI components built on inline keyboards:
|
|
|
78
81
|
- Reports saved paths back to the user
|
|
79
82
|
- Handles name sanitization and deduplication
|
|
80
83
|
|
|
81
|
-
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Usage Guide
|
|
87
|
+
|
|
88
|
+
### Session Control Commands
|
|
89
|
+
|
|
90
|
+
| Command | Description |
|
|
91
|
+
|---------|-------------|
|
|
92
|
+
| `/new` | Start a new session |
|
|
93
|
+
| `/fork` | Fork from a previous user message |
|
|
94
|
+
| `/clone` | Clone at a previous user message |
|
|
95
|
+
| `/tree` | Navigate session tree |
|
|
96
|
+
| `/resume` | Resume a previous session |
|
|
97
|
+
| `/compact` | Compact session context |
|
|
98
|
+
| `/name` | Set or show session name |
|
|
99
|
+
| `/session` | Show session statistics |
|
|
100
|
+
|
|
101
|
+
### Model & Authentication Commands
|
|
102
|
+
|
|
103
|
+
- `/model` — View available models / switch current model via interactive selection
|
|
104
|
+
- `/scoped-models` — Toggle scoped model sets
|
|
105
|
+
- `/thinking` — Adjust thinking level (off/minimal/low/medium/high/xhigh)
|
|
106
|
+
- `/login` — OAuth or API key authentication with full interactive flow
|
|
107
|
+
- `/logout` — Remove stored credentials
|
|
108
|
+
|
|
109
|
+
### Telegram Connection Commands
|
|
110
|
+
|
|
111
|
+
**Global scope** (a single bot token shared across all workspaces):
|
|
112
|
+
|
|
113
|
+
| Command | Description |
|
|
114
|
+
|---------|-------------|
|
|
115
|
+
| `/tg-global-setup` | Configure the global bot token and connect (first-time setup) |
|
|
116
|
+
| `/tg-global-connect` | Enable / start the global bot connection |
|
|
117
|
+
| `/tg-global-disconnect` | Disable / stop the global bot (keeps the token) |
|
|
118
|
+
|
|
119
|
+
**Workspace scope** (per-directory bot token; overrides global when bound):
|
|
120
|
+
|
|
121
|
+
| Command | Description |
|
|
122
|
+
|---------|-------------|
|
|
123
|
+
| `/tg-bind-cwd` | Bind the current directory to its own bot token |
|
|
124
|
+
| `/tg-cwd-connect` | Enable the bot for the current directory |
|
|
125
|
+
| `/tg-cwd-disconnect` | Disable the bot for the current directory |
|
|
126
|
+
| `/tg-unbind-cwd` | Remove the current directory's bot binding |
|
|
127
|
+
| `/tg-list` | List all bot bindings (global + workspace) |
|
|
128
|
+
|
|
129
|
+
**Shared:**
|
|
82
130
|
|
|
83
131
|
| Command | Description |
|
|
84
132
|
|---------|-------------|
|
|
85
|
-
| `/tg-setup` | Configure Telegram bot token (first-time setup) |
|
|
86
|
-
| `/tg-connect` | Enable / start Telegram connection |
|
|
87
|
-
| `/tg-disconnect` | Disable / stop connection (keeps token) |
|
|
88
133
|
| `/tg-config` | Configure rendering levels and message mode |
|
|
89
|
-
| `/tg-bind-cwd` | Bind a workspace directory to a separate bot token |
|
|
90
|
-
| `/tg-unbind-cwd` | Remove workspace bot binding |
|
|
91
|
-
| `/tg-list` | List all Telegram bot bindings |
|
|
92
134
|
|
|
93
|
-
###
|
|
135
|
+
### Utility Commands
|
|
94
136
|
|
|
95
137
|
| Command | Description |
|
|
96
138
|
|---------|-------------|
|
|
@@ -111,213 +153,39 @@ Full interactive UI components built on inline keyboards:
|
|
|
111
153
|
|
|
112
154
|
---
|
|
113
155
|
|
|
114
|
-
##
|
|
115
|
-
|
|
116
|
-
### Prerequisites
|
|
117
|
-
|
|
118
|
-
- Node.js >= 22.19.0
|
|
119
|
-
- [pi coding agent](https://github.com/earendil-works/pi-coding-agent) installed globally
|
|
120
|
-
- A Telegram bot token from [@BotFather](https://t.me/BotFather)
|
|
121
|
-
|
|
122
|
-
### Installation
|
|
123
|
-
|
|
124
|
-
**Install via npm (recommended):**
|
|
125
|
-
|
|
126
|
-
```bash
|
|
127
|
-
pi install npm:pi-telegram-plus
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
**Or install from source:**
|
|
131
|
-
|
|
132
|
-
```bash
|
|
133
|
-
git clone https://github.com/jalyfeng/pi-telegram-plus.git
|
|
134
|
-
cd pi-telegram-plus
|
|
135
|
-
npm install
|
|
136
|
-
pi packages add .
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
3. **Start pi and configure your Telegram bot:**
|
|
140
|
-
|
|
141
|
-
```bash
|
|
142
|
-
pi
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
Inside pi, run:
|
|
146
|
-
|
|
147
|
-
```
|
|
148
|
-
/tg-setup
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
Paste your bot token when prompted. The bot will connect automatically.
|
|
152
|
-
|
|
153
|
-
> **Note:** On first message from any user, pi-telegram-plus automatically authorizes that user ID. Only that user can control the bot afterward.
|
|
154
|
-
|
|
155
|
-
---
|
|
156
|
-
|
|
157
|
-
## Configuration
|
|
158
|
-
|
|
159
|
-
### Per-Workspace Bot Tokens
|
|
160
|
-
|
|
161
|
-
You can bind different bot tokens to different directories:
|
|
162
|
-
|
|
163
|
-
```
|
|
164
|
-
/tg-bind-cwd /path/to/project
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
This creates a workspace-specific binding in `~/.pi/agent/tg.json`. Workspace bindings take priority over the global configuration.
|
|
168
|
-
|
|
169
|
-
### Rendering & Mode Settings
|
|
170
|
-
|
|
171
|
-
Via interactive menu:
|
|
172
|
-
|
|
173
|
-
```
|
|
174
|
-
/tg-config
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
Or directly:
|
|
178
|
-
|
|
179
|
-
```
|
|
180
|
-
/tg-config tool brief # Tool rendering: hidden | brief | full
|
|
181
|
-
/tg-config thinking full # Thinking rendering: hidden | brief | full
|
|
182
|
-
/tg-config mode steer # Message mode: queue | steer
|
|
183
|
-
/tg-config retry 3 # API retry count: 0-10
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
---
|
|
187
|
-
|
|
188
|
-
## How It Works
|
|
189
|
-
|
|
190
|
-
```
|
|
191
|
-
┌─────────────────┐ Long Polling ┌──────────────────────┐
|
|
192
|
-
│ Telegram Bot │ ◄──────────────────► │ pi-telegram-plus │
|
|
193
|
-
│ (api.telegram) │ Callback Queries │ (pi extension) │
|
|
194
|
-
└─────────────────┘ │ │
|
|
195
|
-
▲ │ ┌────────────────┐ │
|
|
196
|
-
│ Messages / Files │ │ Controller │ │
|
|
197
|
-
│ │ │ ├─ handleMsg │ │
|
|
198
|
-
┌─────────┐ │ │ ├─ handleCB │ │
|
|
199
|
-
│ User │ │ │ └─ runPrompt │ │
|
|
200
|
-
│ (Telegram)│ │ │ │ │
|
|
201
|
-
└─────────┘ │ │ ┌─────────────┐ │ │
|
|
202
|
-
▲ │ │ │ TelegramUI │ │ │
|
|
203
|
-
│ Streaming Output │ │ │ (interactive)│ │ │
|
|
204
|
-
│ Live Tool/Thinking Events │ │ └─────────────┘ │ │
|
|
205
|
-
│ File Attachments (tg_attach) │ │ │ │
|
|
206
|
-
└───────────────────────────────────┤ ┌─────────────┐ │ │
|
|
207
|
-
│ │ Renderer │ │ │
|
|
208
|
-
│ │ (Markdown→ │ │ │
|
|
209
|
-
│ │ Telegram │ │ │
|
|
210
|
-
│ │ HTML) │ │ │
|
|
211
|
-
│ └─────────────┘ │ │
|
|
212
|
-
│ │ │ │
|
|
213
|
-
│ ▼ │ │
|
|
214
|
-
│ ┌─────────────┐ │ │
|
|
215
|
-
│ │ pi Agent │ │ │
|
|
216
|
-
│ │ (session, │ │ │
|
|
217
|
-
│ │ models, │ │ │
|
|
218
|
-
│ │ tools, ...) │ │ │
|
|
219
|
-
│ └─────────────┘ │ │
|
|
220
|
-
└──────────────────────┘
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
### Architecture Overview
|
|
224
|
-
|
|
225
|
-
- **`index.ts`** — Extension entry point. Wires all modules together and handles `session_start`/`session_shutdown` lifecycle events.
|
|
226
|
-
- **`lib/telegram-api.ts`** — Raw Telegram Bot API client with retry logic and file upload/download.
|
|
227
|
-
- **`lib/polling.ts`** — Long polling loop with multi-instance file lock. Re-reads config while holding the lock to prevent offset regressions.
|
|
228
|
-
- **`lib/controller.ts`** — Message routing: slash commands, text prompts, media attachments, callback queries.
|
|
229
|
-
- **`lib/telegram-ui.ts`** — Interactive UI layer: notify, confirm, input, select (pagination), editor.
|
|
230
|
-
- **`lib/renderer.ts`** — Hooks into agent lifecycle events (`agent_start`, `tool_execution_*`, `message_end`) and streams rendered output to Telegram.
|
|
231
|
-
- **`lib/markdown.ts`** — Custom `marked` renderer that converts Markdown to Telegram-compatible HTML.
|
|
232
|
-
- **`lib/config.ts`** — Configuration persistence with file locking for concurrent-safe writes. Supports global + workspace scopes.
|
|
233
|
-
- **`lib/attachments.ts`** — `tg_attach` tool registration and outbound attachment delivery.
|
|
234
|
-
- **`lib/commands/`** — Command handler modules: model, session, auth (login/logout), lifecycle, settings, tg-config, telegram-commands.
|
|
235
|
-
- **`lib/heartbeat.ts`** — Periodic typing indicator while processing.
|
|
236
|
-
- **`lib/status.ts`** — TUI status line integration showing connection state.
|
|
237
|
-
- **`lib/session-capture.ts`** — Monkeys patches `AgentSession.bindExtensions` to intercept session lifecycle and capture the active session reference.
|
|
238
|
-
- **`lib/menu-commands.ts`** — Builds and syncs the Telegram BotMenu command list.
|
|
239
|
-
- **`lib/callback-protocol.ts`** — Encodes/decodes inline button callback data.
|
|
240
|
-
|
|
241
|
-
---
|
|
242
|
-
|
|
243
|
-
## Project Structure
|
|
244
|
-
|
|
245
|
-
```
|
|
246
|
-
pi-telegram-plus/
|
|
247
|
-
├── index.ts # Extension entry point
|
|
248
|
-
├── pi-host.d.ts # Type augmentation for @earendil-works/pi-ai
|
|
249
|
-
├── package.json
|
|
250
|
-
├── tsconfig.json
|
|
251
|
-
├── vitest.config.ts
|
|
252
|
-
├── .pi/
|
|
253
|
-
│ └── settings.json # pi package registration
|
|
254
|
-
└── lib/
|
|
255
|
-
├── types.ts # All TypeScript interfaces & types
|
|
256
|
-
├── telegram-api.ts # Telegram Bot HTTP API client
|
|
257
|
-
├── polling.ts # Long polling with multi-instance lock
|
|
258
|
-
├── controller.ts # Message router & prompt executor
|
|
259
|
-
├── telegram-ui.ts # Interactive UI (notify, confirm, input, select)
|
|
260
|
-
├── renderer.ts # Agent event → Telegram output renderer
|
|
261
|
-
├── markdown.ts # Markdown → Telegram HTML converter
|
|
262
|
-
├── html.ts # HTML escaping utilities
|
|
263
|
-
├── text-split.ts # UTF-8-safe text splitter for 4096 byte limit
|
|
264
|
-
├── command-parser.ts # Slash command parser & bot-username normalizer
|
|
265
|
-
├── config.ts # Configuration store (global + workspace scopes)
|
|
266
|
-
├── attachments.ts # Outbound file attachment tool (tg_attach)
|
|
267
|
-
├── heartbeat.ts # Typing indicator pulse
|
|
268
|
-
├── status.ts # TUI status line formatter
|
|
269
|
-
├── session-capture.ts # Agent session capture & handler patching
|
|
270
|
-
├── menu-commands.ts # Telegram BotMenu sync
|
|
271
|
-
├── callback-protocol.ts # UI callback encoding/decoding
|
|
272
|
-
├── commands/
|
|
273
|
-
│ ├── register.ts # Command registry aggregator
|
|
274
|
-
│ ├── model.ts # /model, /scoped-models, /thinking
|
|
275
|
-
│ ├── session.ts # /new, /fork, /clone, /tree, /resume, /cd, /cwd, /name, /session
|
|
276
|
-
│ ├── auth.ts # /login (OAuth + API key), /logout
|
|
277
|
-
│ ├── info.ts # /copy, /export, /import, /share, /status, /changelog, /hotkeys, /debug
|
|
278
|
-
│ ├── lifecycle.ts # /compact, /reload, /stop, /quit
|
|
279
|
-
│ ├── settings.ts # /settings menu
|
|
280
|
-
│ ├── tg-config.ts # /tg-config
|
|
281
|
-
│ └── telegram-commands.ts # /tg-setup, /tg-connect, /tg-disconnect, /tg-bind-cwd, /tg-list
|
|
282
|
-
└── __tests__/
|
|
283
|
-
├── attachments.test.ts
|
|
284
|
-
├── callback-protocol.test.ts
|
|
285
|
-
├── config.test.ts
|
|
286
|
-
├── controller.test.ts
|
|
287
|
-
├── html.test.ts
|
|
288
|
-
├── markdown.test.ts
|
|
289
|
-
├── status.test.ts
|
|
290
|
-
└── text-split.test.ts
|
|
291
|
-
```
|
|
156
|
+
## Troubleshooting
|
|
292
157
|
|
|
293
|
-
|
|
158
|
+
Common issues and diagnostic steps. The extension writes a structured JSON Lines log to `<agent dir>/logs/pi-telegram-plus-YYYY-MM-DD.log` (default `~/.pi/agent/logs/`). Set `PI_TELEGRAM_PLUS_LOG_LEVEL=debug|info|warn|error` to control verbosity. See [docs/logging.md](docs/logging.md) for the full logging design.
|
|
294
159
|
|
|
295
|
-
|
|
160
|
+
### The bot does not respond to my messages
|
|
161
|
+
- Verify the bot token is correct: run `/tg-global-setup` (global) or `/tg-bind-cwd` (workspace) and re-paste the token from [@BotFather](https://t.me/BotFather).
|
|
162
|
+
- Confirm the bot is connected: `/tg-list` should show the binding as enabled. If not, run `/tg-global-connect` or `/tg-cwd-connect`.
|
|
163
|
+
- Make sure you are the authorized user. The **first** Telegram user to message the bot is auto-authorized; all others are rejected. To reset authorization, remove the binding and re-setup.
|
|
164
|
+
- Check that no other pi instance is polling the same token. The file-based polling lock prevents races, but a stuck lock file can block a new instance — restart pi or remove the stale lock if needed.
|
|
296
165
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
166
|
+
### Messages arrive but the agent output is not streamed
|
|
167
|
+
- Confirm pi has an active model and valid credentials: run `/model` and `/status` from Telegram.
|
|
168
|
+
- If `tool` / `thinking` rendering is set to `hidden`, output may look silent. Run `/tg-config tool brief` and `/tg-config thinking brief` to surface activity.
|
|
169
|
+
- Long single messages may exceed Telegram's 4096-byte limit; the extension splits them automatically, but if delivery still fails, check your network and the pi log for upstream API errors.
|
|
300
170
|
|
|
301
|
-
|
|
302
|
-
|
|
171
|
+
### Interactive dialogs (Select / Confirm / Input / Editor) do not appear
|
|
172
|
+
- Inline keyboards require a recent Telegram client; update your Telegram app.
|
|
173
|
+
- Inline keyboards are removed once the pending dialog resolves or is cancelled (e.g. via `/stop` or timeout). Re-trigger the action to get a fresh keyboard.
|
|
174
|
+
- For third-party `custom()` dialogs (pi-goal), ensure the producing extension is loaded (`/reload`) and that the component exposes the expected `handleInput`/`render` API. Unknown shapes are auto-dismissed as `cancelled`.
|
|
303
175
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
176
|
+
### `/tg-global-*` or `/tg-bind-cwd` commands are missing
|
|
177
|
+
- The extension must be registered as a pi package. Re-run `pi install npm:pi-telegram-plus` (or `pi packages add .` from source) and restart pi.
|
|
178
|
+
- Run `/reload` to refresh command registration without a full restart.
|
|
307
179
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
- **No secrets in source code** — All bot tokens and API keys are read from `~/.pi/agent/tg.json` and `~/.pi/agent/auth.json` at runtime.
|
|
313
|
-
- **File permissions** — Configuration files are created with `chmod 600` (owner-only access).
|
|
314
|
-
- **Sensitive input protection** — API key inputs use `inputSecret` which automatically deletes the user's message after reading.
|
|
315
|
-
- **Attachment path safety** — Outbound `tg_attach` blocks sensitive paths (`/etc`, `~/.ssh`, etc.).
|
|
316
|
-
- **Polling lock** — File-based lock prevents multiple pi instances from polling the same bot token simultaneously.
|
|
317
|
-
- **First-user authorization** — The first Telegram user to message the bot is automatically authorized; subsequent users are rejected.
|
|
318
|
-
|
|
319
|
-
---
|
|
180
|
+
### File attachments fail to send or are rejected
|
|
181
|
+
- Outbound `tg_attach` blocks sensitive paths (`/etc`, `~/.ssh`, etc.). Move the file to a non-sensitive location and retry.
|
|
182
|
+
- Default upload size limit is 50 MB. Files exceeding it are rejected; reduce the file size or split the content.
|
|
183
|
+
- For download failures (Telegram → working directory), check that the working directory is writable and that the filename was sanitized correctly. Saved paths are reported back in the chat.
|
|
320
184
|
|
|
321
|
-
|
|
185
|
+
### Polling reconnects repeatedly or reports transient failures
|
|
186
|
+
- The extension uses exponential backoff on transient errors. If failures persist, verify network reachability to `api.telegram.org` and that the bot token has not been revoked in BotFather.
|
|
187
|
+
- A revoked/regenerated token will keep failing until you re-run `/tg-global-setup` with the new token.
|
|
322
188
|
|
|
323
|
-
|
|
189
|
+
### Configuration changes are not picked up
|
|
190
|
+
- Per-workspace bindings live in `~/.pi/agent/tg.json`. After editing by hand, run `/reload` (or restart pi) so the extension re-reads config.
|
|
191
|
+
- Workspace bindings override the global token. If the wrong bot responds, run `/tg-list` and `/tg-unbind-cwd` to clear the unintended override.
|
package/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
-
import { extname, resolve } from "node:path";
|
|
2
|
+
import { extname, join, resolve } from "node:path";
|
|
3
3
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { registerTelegramAttachmentTool } from "./lib/attachments.ts";
|
|
5
|
-
import { readResolvedTelegramConfig, writeResolvedTelegramConfig } from "./lib/config.ts";
|
|
5
|
+
import { readResolvedTelegramConfig, writeResolvedTelegramConfig, getAgentDir } from "./lib/config.ts";
|
|
6
6
|
import { createTelegramController, type TelegramCommandHandler } from "./lib/controller.ts";
|
|
7
7
|
import { createHeartbeat } from "./lib/heartbeat.ts";
|
|
8
8
|
import { registerTelegramRenderer } from "./lib/renderer.ts";
|
|
@@ -11,11 +11,15 @@ import { createTelegramTransport, downloadTelegramFile, getTelegramBotUsername,
|
|
|
11
11
|
import { createTelegramUiRuntime } from "./lib/telegram-ui.ts";
|
|
12
12
|
import { formatTelegramStatusLine, clearTelegramStatus, TELEGRAM_STATUS_KEY } from "./lib/status.ts";
|
|
13
13
|
import { createTelegramPollingRuntime } from "./lib/polling.ts";
|
|
14
|
+
import { initLogger, log, type LogLevel } from "./lib/logger.ts";
|
|
15
|
+
|
|
14
16
|
import { registerAllCommands } from "./lib/commands/register.ts";
|
|
15
17
|
import { registerTelegramCommands } from "./lib/commands/telegram-commands.ts";
|
|
16
18
|
import { syncTelegramCommands } from "./lib/menu-commands.ts";
|
|
17
19
|
import type { ResolvedTelegramConfig, TelegramConfig, TelegramTurn } from "./lib/types.ts";
|
|
18
20
|
|
|
21
|
+
const indexLog = log.child("index");
|
|
22
|
+
|
|
19
23
|
type TelegramPlusRuntimeState = {
|
|
20
24
|
dispose?: () => void;
|
|
21
25
|
};
|
|
@@ -30,6 +34,14 @@ function getTelegramPlusRuntimeState(): TelegramPlusRuntimeState {
|
|
|
30
34
|
|
|
31
35
|
export default function piTelegramPlus(pi: ExtensionAPI): void {
|
|
32
36
|
installAgentSessionCapture();
|
|
37
|
+
// Initialize file logging first, before any subsystem can emit. Log directory
|
|
38
|
+
// lives under the pi agent cache dir alongside tg.json; level is overridable
|
|
39
|
+
// via PI_TELEGRAM_PLUS_LOG_LEVEL (debug/info/warn/error). See lib/logger.ts.
|
|
40
|
+
const envLevel = process.env.PI_TELEGRAM_PLUS_LOG_LEVEL?.toLowerCase();
|
|
41
|
+
const level: LogLevel = (envLevel === "debug" || envLevel === "info" || envLevel === "warn" || envLevel === "error")
|
|
42
|
+
? envLevel
|
|
43
|
+
: "info";
|
|
44
|
+
initLogger({ dir: join(getAgentDir(), "logs"), level });
|
|
33
45
|
const runtimeState = getTelegramPlusRuntimeState();
|
|
34
46
|
runtimeState.dispose?.();
|
|
35
47
|
|
|
@@ -256,7 +268,7 @@ export default function piTelegramPlus(pi: ExtensionAPI): void {
|
|
|
256
268
|
}
|
|
257
269
|
const chatId = config.activeChatId;
|
|
258
270
|
if (chatId !== undefined && config.botToken) {
|
|
259
|
-
transport.sendText(chatId, `<b>error</b>\nTelegram polling failed`).catch(()
|
|
271
|
+
transport.sendText(chatId, `<b>error</b>\nTelegram polling failed`).catch(log.child("polling").swallow("error", "sendText polling-failure notice failed", { chatId }));
|
|
260
272
|
} else {
|
|
261
273
|
getActiveSession()?.extensionRunner.getUIContext().notify(`Telegram polling failed: ${message}`, "error");
|
|
262
274
|
}
|
|
@@ -318,10 +330,10 @@ export default function piTelegramPlus(pi: ExtensionAPI): void {
|
|
|
318
330
|
config = { ...config, botUsername };
|
|
319
331
|
await persistCurrentConfig(config);
|
|
320
332
|
}
|
|
321
|
-
} catch {
|
|
333
|
+
} catch (err) { indexLog.debug("resolve botUsername on startup failed (non-critical)", { err }); }
|
|
322
334
|
}
|
|
323
335
|
if (config.botToken && isTelegramEnabled() && !polling.isActive()) polling.start();
|
|
324
|
-
try { await syncTelegramCommands(config.botToken, pi); } catch {
|
|
336
|
+
try { await syncTelegramCommands(config.botToken, pi); } catch (err) { indexLog.debug("syncTelegramCommands on startup failed (non-critical)", { err }); }
|
|
325
337
|
lastStatusError = undefined;
|
|
326
338
|
heartbeat.startStatusHeartbeat(refreshStatus);
|
|
327
339
|
refreshStatus();
|
package/lib/attachments.ts
CHANGED
|
@@ -3,6 +3,9 @@ import { basename, resolve } from "node:path";
|
|
|
3
3
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { Type } from "typebox";
|
|
5
5
|
import type { TelegramTransport, TelegramTurn } from "./types.ts";
|
|
6
|
+
import { log } from "./logger.ts";
|
|
7
|
+
|
|
8
|
+
const attachLog = log.child("attachments");
|
|
6
9
|
|
|
7
10
|
const MAX_ATTACHMENTS_PER_TURN = 10;
|
|
8
11
|
const DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
|
|
@@ -58,7 +61,8 @@ async function sendTelegramAttachment(
|
|
|
58
61
|
await transport.sendChatAction(chatId, "upload_photo");
|
|
59
62
|
await transport.sendPhoto(chatId, attachment.path, attachment.fileName, true);
|
|
60
63
|
return;
|
|
61
|
-
} catch {
|
|
64
|
+
} catch (err) {
|
|
65
|
+
attachLog.warn("sendPhoto failed; falling back to sendDocument", { chatId, fileName: attachment.fileName, err });
|
|
62
66
|
await transport.sendChatAction(chatId, "upload_document");
|
|
63
67
|
await transport.sendDocument(chatId, attachment.path, attachment.fileName);
|
|
64
68
|
return;
|
|
@@ -125,7 +129,7 @@ export function registerTelegramAttachmentTool(
|
|
|
125
129
|
for (const attachment of pendingAttachments) {
|
|
126
130
|
await sendTelegramAttachment(chatId, attachment, deps.transport, maxBytes, async (message) => {
|
|
127
131
|
failed.push(message);
|
|
128
|
-
await deps.transport.sendText(chatId, message).catch((
|
|
132
|
+
await deps.transport.sendText(chatId, message).catch(attachLog.swallow("warn", "sendText attachment error notice failed", { chatId }));
|
|
129
133
|
});
|
|
130
134
|
}
|
|
131
135
|
return {
|
|
@@ -148,7 +152,7 @@ export async function sendQueuedTelegramAttachments(
|
|
|
148
152
|
const maxBytes = outboundAttachmentLimit();
|
|
149
153
|
for (const attachment of turn.queuedAttachments) {
|
|
150
154
|
await sendTelegramAttachment(turn.chatId, attachment, transport, maxBytes, async (message) => {
|
|
151
|
-
await transport.sendText(turn.chatId, message).catch((
|
|
155
|
+
await transport.sendText(turn.chatId, message).catch(attachLog.swallow("warn", "sendText attachment error notice failed", { chatId: turn.chatId }));
|
|
152
156
|
});
|
|
153
157
|
}
|
|
154
158
|
}
|
package/lib/commands/session.ts
CHANGED
|
@@ -5,6 +5,9 @@ import { SessionManager, type SessionEntry } from "@earendil-works/pi-coding-age
|
|
|
5
5
|
import type { SessionNameDeps, CommandRegistry } from "./register.ts";
|
|
6
6
|
import { escapeHtml } from "../html.ts";
|
|
7
7
|
import type { CapturedAgentSession } from "../types.ts";
|
|
8
|
+
import { log } from "../logger.ts";
|
|
9
|
+
|
|
10
|
+
const sessionLog = log.child("session");
|
|
8
11
|
|
|
9
12
|
function truncate(text: string, max: number): string {
|
|
10
13
|
return text.length <= max ? text : text.slice(0, max - 1) + "…";
|
|
@@ -187,7 +190,7 @@ export function registerSessionCommands(
|
|
|
187
190
|
if (!rawPath) return;
|
|
188
191
|
|
|
189
192
|
const targetCwd = resolveTargetCwd(rawPath, ctx.cwd);
|
|
190
|
-
const info = await stat(targetCwd).catch(()
|
|
193
|
+
const info = await stat(targetCwd).catch(sessionLog.swallow("debug", "stat target cwd failed (treated as not-a-directory)", { targetCwd }));
|
|
191
194
|
if (!info?.isDirectory()) {
|
|
192
195
|
ui.notify(`Not a directory: ${escapeHtml(targetCwd)}`, "error");
|
|
193
196
|
return;
|
|
@@ -4,6 +4,9 @@ import { escapeHtml } from "../html.ts";
|
|
|
4
4
|
import { getTelegramBotUsername } from "../telegram-api.ts";
|
|
5
5
|
import type { ResolvedTelegramConfig, TelegramConfig, TelegramTransport } from "../types.ts";
|
|
6
6
|
import type { TelegramPollingRuntime } from "../polling.ts";
|
|
7
|
+
import { log } from "../logger.ts";
|
|
8
|
+
|
|
9
|
+
const tgCmdLog = log.child("tg-commands");
|
|
7
10
|
|
|
8
11
|
export type TelegramCommandDeps = {
|
|
9
12
|
getConfig: () => TelegramConfig;
|
|
@@ -40,7 +43,7 @@ async function configureGlobalTelegramToken(
|
|
|
40
43
|
): Promise<boolean> {
|
|
41
44
|
const token = await (ui.inputSecret?.("Telegram bot token") ?? ui.input("Telegram bot token"));
|
|
42
45
|
if (!token) return false;
|
|
43
|
-
const botUsername = await getTelegramBotUsername(token).catch((
|
|
46
|
+
const botUsername = await getTelegramBotUsername(token).catch(tgCmdLog.swallow("warn", "getTelegramBotUsername failed during global token setup"));
|
|
44
47
|
if (!deps.getResolvedConfig()) {
|
|
45
48
|
deps.switchResolvedConfig({ store: { version: 2, global: {}, workspaces: [] }, scope: "global", config: {} });
|
|
46
49
|
}
|
|
@@ -72,10 +75,10 @@ async function handleTgGlobalConnect(
|
|
|
72
75
|
if (!deps.getConfig().botToken) {
|
|
73
76
|
if (!(await configureGlobalTelegramToken(ui, deps))) return;
|
|
74
77
|
} else {
|
|
75
|
-
const botUsername = deps.getConfig().botUsername ?? await getTelegramBotUsername(deps.getConfig().botToken!).catch((
|
|
78
|
+
const botUsername = deps.getConfig().botUsername ?? await getTelegramBotUsername(deps.getConfig().botToken!).catch(tgCmdLog.swallow("warn", "getTelegramBotUsername failed during global connect"));
|
|
76
79
|
await globalConnectAndStart(deps, deps.getConfig().botToken!, botUsername);
|
|
77
80
|
}
|
|
78
|
-
} catch {
|
|
81
|
+
} catch (err) { tgCmdLog.debug("global connect swallowed error (reported via polling onError)", { err }); }
|
|
79
82
|
deps.clearStatusError();
|
|
80
83
|
deps.startStatusHeartbeat();
|
|
81
84
|
ui.notify("Telegram global bot connected.", "info");
|
|
@@ -128,7 +131,7 @@ export function registerTelegramCommands(
|
|
|
128
131
|
const workspacePath = resolve(args.trim() || ctx.cwd || process.cwd());
|
|
129
132
|
const token = await (ui.inputSecret?.(`Telegram bot token for ${workspacePath}`) ?? ui.input(`Telegram bot token for ${workspacePath}`));
|
|
130
133
|
if (!token) return;
|
|
131
|
-
const botUsername = await getTelegramBotUsername(token).catch((
|
|
134
|
+
const botUsername = await getTelegramBotUsername(token).catch(tgCmdLog.swallow("warn", "getTelegramBotUsername failed during workspace token setup", { workspacePath }));
|
|
132
135
|
await deps.getPolling().stop();
|
|
133
136
|
deps.switchResolvedConfig(await bindWorkspaceTelegramConfig(workspacePath, {
|
|
134
137
|
botToken: token,
|
package/lib/config.ts
CHANGED
|
@@ -3,6 +3,9 @@ import { chmod, mkdir, readFile, rm, rmdir, stat, writeFile } from "node:fs/prom
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join, relative, resolve } from "node:path";
|
|
5
5
|
import type { ResolvedTelegramConfig, TelegramConfig, TelegramConfigStore, TelegramWorkspaceConfig } from "./types.ts";
|
|
6
|
+
import { log } from "./logger.ts";
|
|
7
|
+
|
|
8
|
+
const configLog = log.child("config");
|
|
6
9
|
|
|
7
10
|
export function getAgentDir(): string {
|
|
8
11
|
return process.env.PI_CODING_AGENT_DIR
|
|
@@ -33,7 +36,7 @@ async function withTelegramConfigLock<T>(run: () => Promise<T>): Promise<T> {
|
|
|
33
36
|
if (code !== "EEXIST") throw error;
|
|
34
37
|
const age = Date.now() - (await stat(lockPath).then((s) => s.mtimeMs).catch(() => Date.now()));
|
|
35
38
|
if (age > 30_000 || Date.now() - started > 10_000) {
|
|
36
|
-
await rm(lockPath, { recursive: true, force: true }).catch((
|
|
39
|
+
await rm(lockPath, { recursive: true, force: true }).catch(configLog.swallow("warn", "rm stale config lock failed", { lockPath }));
|
|
37
40
|
continue;
|
|
38
41
|
}
|
|
39
42
|
await sleep(50);
|
|
@@ -42,7 +45,7 @@ async function withTelegramConfigLock<T>(run: () => Promise<T>): Promise<T> {
|
|
|
42
45
|
try {
|
|
43
46
|
return await run();
|
|
44
47
|
} finally {
|
|
45
|
-
await rmdir(lockPath).catch((
|
|
48
|
+
await rmdir(lockPath).catch(configLog.swallow("warn", "rmdir config lock failed on release", { lockPath }));
|
|
46
49
|
}
|
|
47
50
|
}
|
|
48
51
|
|
|
@@ -73,7 +76,7 @@ export async function writeTelegramConfigStore(store: TelegramConfigStore): Prom
|
|
|
73
76
|
workspaces: store.workspaces ?? [],
|
|
74
77
|
};
|
|
75
78
|
await writeFile(path, JSON.stringify(normalized, null, 2) + "\n", { mode: 0o600 });
|
|
76
|
-
await chmod(path, 0o600).catch((
|
|
79
|
+
await chmod(path, 0o600).catch(configLog.swallow("warn", "chmod config file failed", { path }));
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
function normalizePath(path: string): string {
|