pi-telegram-plus 0.0.1 → 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,5 +1,7 @@
1
1
  # pi-telegram-plus
2
2
 
3
+ ## Overview
4
+
3
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.**
4
6
 
5
7
  `pi-telegram-plus` is a pi extension that turns Telegram into a full-featured remote control surface for the pi coding agent. It's not just a notification bot — it mirrors the core pi TUI experience into Telegram, with interactive menus, inline keyboards, file attachments, and live agent output rendering.
@@ -13,27 +15,15 @@
13
15
  - **Multi-instance safe** — file-based polling lock prevents multiple pi instances from racing on the same bot token
14
16
  - **Automatic reconnection** — exponential backoff on transient failures
15
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
16
21
 
17
22
  ### 🎮 Full Session Control
18
- All pi session lifecycle commands available via Telegram:
19
-
20
- | Command | Description |
21
- |---------|-------------|
22
- | `/new` | Start a new session |
23
- | `/fork` | Fork from a previous user message |
24
- | `/clone` | Clone at a previous user message |
25
- | `/tree` | Navigate session tree |
26
- | `/resume` | Resume a previous session |
27
- | `/compact` | Compact session context |
28
- | `/name` | Set or show session name |
29
- | `/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.
30
24
 
31
25
  ### 🧠 Model & Authentication Management
32
- - `/model` View available models / switch current model via interactive selection
33
- - `/scoped-models` — Toggle scoped model sets
34
- - `/thinking` — Adjust thinking level (off/minimal/low/medium/high/xhigh)
35
- - `/login` — OAuth or API key authentication with full interactive flow
36
- - `/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.
37
27
 
38
28
  ### 📨 Message Modes
39
29
  Two modes for handling incoming messages while the agent is running:
@@ -50,12 +40,31 @@ Full interactive UI components built on inline keyboards:
50
40
  - **InputSecret** — same as Input, but the prompt message is auto-deleted after reply to protect sensitive data
51
41
  - **Select** — paginated option list with Prev/Next navigation
52
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.
53
60
 
54
61
  ### 🎨 Message Rendering
55
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
56
64
  - **Tool execution rendering** — Configurable level (`hidden` / `brief` / `full`) for tool call visibility
57
65
  - **Thinking rendering** — Configurable level (`hidden` / `brief` / `full`) for agent thinking blocks
58
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
59
68
  - **Image output** — Automatically sends agent-generated images as Telegram photos
60
69
 
61
70
  ### 📎 File Attachments
@@ -72,25 +81,65 @@ Full interactive UI components built on inline keyboards:
72
81
  - Reports saved paths back to the user
73
82
  - Handles name sanitization and deduplication
74
83
 
75
- ### ⚙️ 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:**
76
130
 
77
131
  | Command | Description |
78
132
  |---------|-------------|
79
- | `/tg-setup` | Configure Telegram bot token (first-time setup) |
80
- | `/tg-connect` | Enable / start Telegram connection |
81
- | `/tg-disconnect` | Disable / stop connection (keeps token) |
82
133
  | `/tg-config` | Configure rendering levels and message mode |
83
- | `/tg-bind-cwd` | Bind a workspace directory to a separate bot token |
84
- | `/tg-unbind-cwd` | Remove workspace bot binding |
85
- | `/tg-list` | List all Telegram bot bindings |
86
134
 
87
- ### 🔧 Utility Commands
135
+ ### Utility Commands
88
136
 
89
137
  | Command | Description |
90
138
  |---------|-------------|
91
139
  | `/cwd` | Show current working directory |
92
140
  | `/cd` | Switch pi working directory |
93
141
  | `/stop` | Abort the current agent turn |
142
+ | `/status` | Show runtime snapshot (workspace, model, context, messages) |
94
143
  | `/debug` | Show debug info (model, thinking, streaming, entries) |
95
144
  | `/settings` | Open settings menu |
96
145
  | `/copy` | Copy last assistant text |
@@ -104,215 +153,39 @@ Full interactive UI components built on inline keyboards:
104
153
 
105
154
  ---
106
155
 
107
- ## Installation
108
-
109
- ### Prerequisites
110
-
111
- - Node.js >= 22.19.0
112
- - [pi coding agent](https://github.com/earendil-works/pi-coding-agent) installed globally
113
- - A Telegram bot token from [@BotFather](https://t.me/BotFather)
114
-
115
- ### Setup
116
-
117
- 1. **Clone or create the extension directory:**
118
-
119
- ```bash
120
- git clone <repo-url> pi-telegram-plus
121
- cd pi-telegram-plus
122
- npm install
123
- ```
124
-
125
- 2. **Configure pi to load the extension:**
126
-
127
- ```bash
128
- # Inside the pi-telegram-plus directory
129
- pi packages add .
130
- ```
131
-
132
- This registers the extension via `.pi/settings.json`.
133
-
134
- 3. **Start pi and configure your Telegram bot:**
135
-
136
- ```bash
137
- pi
138
- ```
139
-
140
- Inside pi, run:
141
-
142
- ```
143
- /tg-setup
144
- ```
145
-
146
- Paste your bot token when prompted. The bot will connect automatically.
147
-
148
- > **Note:** On first message from any user, pi-telegram-plus automatically authorizes that user ID. Only that user can control the bot afterward.
149
-
150
- ---
156
+ ## Troubleshooting
151
157
 
152
- ## Configuration
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.
153
159
 
154
- ### Per-Workspace Bot Tokens
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.
155
165
 
156
- You can bind different bot tokens to different directories:
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.
157
170
 
158
- ```
159
- /tg-bind-cwd /path/to/project
160
- ```
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`.
161
175
 
162
- This creates a workspace-specific binding in `~/.pi/agent/tg.json`. Workspace bindings take priority over the global configuration.
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.
163
179
 
164
- ### Rendering & Mode Settings
165
-
166
- Via interactive menu:
167
-
168
- ```
169
- /tg-config
170
- ```
171
-
172
- Or directly:
173
-
174
- ```
175
- /tg-config tool brief # Tool rendering: hidden | brief | full
176
- /tg-config thinking full # Thinking rendering: hidden | brief | full
177
- /tg-config mode steer # Message mode: queue | steer
178
- /tg-config retry 3 # API retry count: 0-10
179
- ```
180
-
181
- ---
182
-
183
- ## How It Works
184
-
185
- ```
186
- ┌─────────────────┐ Long Polling ┌──────────────────────┐
187
- │ Telegram Bot │ ◄──────────────────► │ pi-telegram-plus │
188
- │ (api.telegram) │ Callback Queries │ (pi extension) │
189
- └─────────────────┘ │ │
190
- ▲ │ ┌────────────────┐ │
191
- │ Messages / Files │ │ Controller │ │
192
- │ │ │ ├─ handleMsg │ │
193
- ┌─────────┐ │ │ ├─ handleCB │ │
194
- │ User │ │ │ └─ runPrompt │ │
195
- │ (Telegram)│ │ │ │ │
196
- └─────────┘ │ │ ┌─────────────┐ │ │
197
- ▲ │ │ │ TelegramUI │ │ │
198
- │ Streaming Output │ │ │ (interactive)│ │ │
199
- │ Live Tool/Thinking Events │ │ └─────────────┘ │ │
200
- │ File Attachments (tg_attach) │ │ │ │
201
- └───────────────────────────────────┤ ┌─────────────┐ │ │
202
- │ │ Renderer │ │ │
203
- │ │ (Markdown→ │ │ │
204
- │ │ Telegram │ │ │
205
- │ │ HTML) │ │ │
206
- │ └─────────────┘ │ │
207
- │ │ │ │
208
- │ ▼ │ │
209
- │ ┌─────────────┐ │ │
210
- │ │ pi Agent │ │ │
211
- │ │ (session, │ │ │
212
- │ │ models, │ │ │
213
- │ │ tools, ...) │ │ │
214
- │ └─────────────┘ │ │
215
- └──────────────────────┘
216
- ```
217
-
218
- ### Architecture Overview
219
-
220
- - **`index.ts`** — Extension entry point. Wires all modules together and handles `session_start`/`session_shutdown` lifecycle events.
221
- - **`lib/telegram-api.ts`** — Raw Telegram Bot API client with retry logic and file upload/download.
222
- - **`lib/polling.ts`** — Long polling loop with multi-instance file lock. Re-reads config while holding the lock to prevent offset regressions.
223
- - **`lib/controller.ts`** — Message routing: slash commands, text prompts, media attachments, callback queries.
224
- - **`lib/telegram-ui.ts`** — Interactive UI layer: notify, confirm, input, select (pagination), editor.
225
- - **`lib/renderer.ts`** — Hooks into agent lifecycle events (`agent_start`, `tool_execution_*`, `message_end`) and streams rendered output to Telegram.
226
- - **`lib/markdown.ts`** — Custom `marked` renderer that converts Markdown to Telegram-compatible HTML.
227
- - **`lib/config.ts`** — Configuration persistence with file locking for concurrent-safe writes. Supports global + workspace scopes.
228
- - **`lib/attachments.ts`** — `tg_attach` tool registration and outbound attachment delivery.
229
- - **`lib/commands/`** — Command handler modules: model, session, auth (login/logout), lifecycle, settings, tg-config, telegram-commands.
230
- - **`lib/heartbeat.ts`** — Periodic typing indicator while processing.
231
- - **`lib/status.ts`** — TUI status line integration showing connection state.
232
- - **`lib/session-capture.ts`** — Monkeys patches `AgentSession.bindExtensions` to intercept session lifecycle and capture the active session reference.
233
- - **`lib/menu-commands.ts`** — Builds and syncs the Telegram BotMenu command list.
234
- - **`lib/callback-protocol.ts`** — Encodes/decodes inline button callback data.
235
-
236
- ---
237
-
238
- ## Project Structure
239
-
240
- ```
241
- pi-telegram-plus/
242
- ├── index.ts # Extension entry point
243
- ├── pi-host.d.ts # Type augmentation for @earendil-works/pi-ai
244
- ├── package.json
245
- ├── tsconfig.json
246
- ├── vitest.config.ts
247
- ├── .pi/
248
- │ └── settings.json # pi package registration
249
- └── lib/
250
- ├── types.ts # All TypeScript interfaces & types
251
- ├── telegram-api.ts # Telegram Bot HTTP API client
252
- ├── polling.ts # Long polling with multi-instance lock
253
- ├── controller.ts # Message router & prompt executor
254
- ├── telegram-ui.ts # Interactive UI (notify, confirm, input, select)
255
- ├── renderer.ts # Agent event → Telegram output renderer
256
- ├── markdown.ts # Markdown → Telegram HTML converter
257
- ├── html.ts # HTML escaping utilities
258
- ├── text-split.ts # UTF-8-safe text splitter for 4096 byte limit
259
- ├── command-parser.ts # Slash command parser & bot-username normalizer
260
- ├── config.ts # Configuration store (global + workspace scopes)
261
- ├── attachments.ts # Outbound file attachment tool (tg_attach)
262
- ├── heartbeat.ts # Typing indicator pulse
263
- ├── status.ts # TUI status line formatter
264
- ├── session-capture.ts # Agent session capture & handler patching
265
- ├── menu-commands.ts # Telegram BotMenu sync
266
- ├── callback-protocol.ts # UI callback encoding/decoding
267
- ├── commands/
268
- │ ├── register.ts # Command registry aggregator
269
- │ ├── model.ts # /model, /scoped-models, /thinking
270
- │ ├── session.ts # /new, /fork, /clone, /tree, /resume, /cd, /cwd, /name, /session
271
- │ ├── auth.ts # /login (OAuth + API key), /logout
272
- │ ├── info.ts # /copy, /export, /import, /share, /changelog, /hotkeys, /debug
273
- │ ├── lifecycle.ts # /compact, /reload, /stop, /quit
274
- │ ├── settings.ts # /settings menu
275
- │ ├── tg-config.ts # /tg-config
276
- │ └── telegram-commands.ts # /tg-setup, /tg-connect, /tg-disconnect, /tg-bind-cwd, /tg-list
277
- └── __tests__/
278
- ├── attachments.test.ts
279
- ├── callback-protocol.test.ts
280
- ├── config.test.ts
281
- ├── controller.test.ts
282
- ├── html.test.ts
283
- ├── markdown.test.ts
284
- ├── status.test.ts
285
- └── text-split.test.ts
286
- ```
287
-
288
- ---
289
-
290
- ## Development
291
-
292
- ```bash
293
- # Type checking
294
- npm run typecheck
295
-
296
- # Run tests
297
- npm test
298
-
299
- # Watch mode
300
- npm run test:watch
301
- ```
302
-
303
- ---
304
-
305
- ## Security
306
-
307
- - **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.
308
- - **File permissions** — Configuration files are created with `chmod 600` (owner-only access).
309
- - **Sensitive input protection** — API key inputs use `inputSecret` which automatically deletes the user's message after reading.
310
- - **Attachment path safety** — Outbound `tg_attach` blocks sensitive paths (`/etc`, `~/.ssh`, etc.).
311
- - **Polling lock** — File-based lock prevents multiple pi instances from polling the same bot token simultaneously.
312
- - **First-user authorization** — The first Telegram user to message the bot is automatically authorized; subsequent users are rejected.
313
-
314
- ---
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.
315
184
 
316
- ## 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.
317
188
 
318
- 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
 
@@ -88,7 +100,11 @@ export default function piTelegramPlus(pi: ExtensionAPI): void {
88
100
  refreshStatus();
89
101
  };
90
102
 
91
- const isTelegramEnabled = (): boolean => config.telegramEnabled ?? resolvedConfig?.scope !== "global";
103
+ const isTelegramEnabled = (): boolean => {
104
+ if (config.telegramEnabled !== undefined) return config.telegramEnabled;
105
+ // Default: workspace binding implies intent to use; global requires explicit enable.
106
+ return resolvedConfig?.scope === "workspace";
107
+ };
92
108
 
93
109
  const transport = createTelegramTransport(() => config);
94
110
  const ui = createTelegramUiRuntime({
@@ -121,11 +137,11 @@ export default function piTelegramPlus(pi: ExtensionAPI): void {
121
137
  // Pi built-in commands (model, session, new, etc.) are already registered by pi core.
122
138
  const TUI_VISIBLE_COMMANDS = new Set([
123
139
  // tg-* commands
124
- "tg-setup", "tg-connect", "tg-disconnect", "tg-config",
125
- "tg-bind-cwd", "tg-unbind-cwd", "tg-list",
140
+ "tg-global-setup", "tg-global-connect", "tg-global-disconnect", "tg-config",
141
+ "tg-bind-cwd", "tg-unbind-cwd", "tg-cwd-connect", "tg-cwd-disconnect", "tg-list",
126
142
  // other pi-telegram-plus custom commands (TUI-only command list excludes /import, which is now
127
143
  // a built-in pi command; keep Telegram handler registration only.
128
- "cwd", "cd", "thinking", "stop", "debug",
144
+ "cwd", "cd", "status", "thinking", "stop", "debug",
129
145
  ]);
130
146
 
131
147
  registerAllCommands({
@@ -135,7 +151,10 @@ export default function piTelegramPlus(pi: ExtensionAPI): void {
135
151
  pi.registerCommand(name, { description: options.description, handler: options.handler });
136
152
  }
137
153
  },
138
- }, sessionDeps, sessionNameDeps, tgConfigDeps);
154
+ }, sessionDeps, sessionNameDeps, tgConfigDeps, {
155
+ getTransport: () => transport,
156
+ getActiveChatId: () => config.activeChatId,
157
+ });
139
158
 
140
159
  registerTelegramCommands({
141
160
  registerCommand: (name: string, options: { description?: string; handler: TelegramCommandHandler }) => {
@@ -249,7 +268,7 @@ export default function piTelegramPlus(pi: ExtensionAPI): void {
249
268
  }
250
269
  const chatId = config.activeChatId;
251
270
  if (chatId !== undefined && config.botToken) {
252
- 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 }));
253
272
  } else {
254
273
  getActiveSession()?.extensionRunner.getUIContext().notify(`Telegram polling failed: ${message}`, "error");
255
274
  }
@@ -300,7 +319,7 @@ export default function piTelegramPlus(pi: ExtensionAPI): void {
300
319
  } catch (error) {
301
320
  switchResolvedConfig({ store: { version: 2, global: {}, workspaces: [] }, scope: "global", config: {} });
302
321
  getActiveSession()?.extensionRunner.getUIContext().notify(
303
- `Telegram config is not v2 yet. Run /tg-setup to recreate it. ${error instanceof Error ? error.message : String(error)}`,
322
+ `Telegram config is not v2 yet. Run /tg-global-setup to recreate it. ${error instanceof Error ? error.message : String(error)}`,
304
323
  "error",
305
324
  );
306
325
  }
@@ -311,10 +330,10 @@ export default function piTelegramPlus(pi: ExtensionAPI): void {
311
330
  config = { ...config, botUsername };
312
331
  await persistCurrentConfig(config);
313
332
  }
314
- } catch { /* non-critical */ }
333
+ } catch (err) { indexLog.debug("resolve botUsername on startup failed (non-critical)", { err }); }
315
334
  }
316
335
  if (config.botToken && isTelegramEnabled() && !polling.isActive()) polling.start();
317
- 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 }); }
318
337
  lastStatusError = undefined;
319
338
  heartbeat.startStatusHeartbeat(refreshStatus);
320
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
  }