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 CHANGED
@@ -1,10 +1,6 @@
1
1
  # pi-telegram-plus
2
2
 
3
- <p>
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
- - `/model` View available models / switch current model via interactive selection
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
- ### ⚙️ Telegram-Specific Commands
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
- ### 🔧 Utility Commands
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
- ## Installation
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
- ## Development
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
- ```bash
298
- # Type checking
299
- npm run typecheck
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
- # Run tests
302
- npm test
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
- # Watch mode
305
- npm run test:watch
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
- ## Security
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
- ## License
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
- MIT
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(() => undefined);
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 { /* non-critical */ }
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 { /* non-critical */ }
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();
@@ -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(() => undefined);
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(() => undefined);
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
  }
@@ -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(() => undefined);
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(() => undefined);
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(() => undefined);
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 { /* connection errors reported via polling onError */ }
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(() => undefined);
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(() => undefined);
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(() => undefined);
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(() => undefined);
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 {