@bytesbrains/pi-telegram-bridge 1.0.0
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/AGENTS.md +170 -0
- package/LICENSE +21 -0
- package/README.md +136 -0
- package/package.json +51 -0
- package/src/helpers.ts +234 -0
- package/src/index.ts +381 -0
- package/src/tools/telegram.ts +27 -0
- package/src/types.ts +15 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Telegram Bridge — Agent Usage Guide
|
|
2
|
+
|
|
3
|
+
> You are an AI agent. Use telegram-bridge tools to communicate with humans via Telegram.
|
|
4
|
+
> Ask questions, send status updates, and check for incoming messages.
|
|
5
|
+
|
|
6
|
+
## Quickstart
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
telegram_status() # Check if bridge is configured
|
|
10
|
+
telegram_send(message="Hello! I'm working on the task.") # Send a status message
|
|
11
|
+
telegram_listen() # Check for new messages
|
|
12
|
+
telegram_ask(question="Should I proceed?", options=["Yes","No"]) # Ask and wait for reply
|
|
13
|
+
telegram_override(command="git push --force", reason="Force push detected") # Override blocked action
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Tools
|
|
17
|
+
|
|
18
|
+
### telegram_listen
|
|
19
|
+
|
|
20
|
+
Check for any new inbound messages from the human in the configured chat. Returns the latest message text, or indicates no new messages. The background listener automatically forwards messages, so this is for explicit polling.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
telegram_listen()
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### telegram_send
|
|
27
|
+
|
|
28
|
+
Send a one-way message. Use for:
|
|
29
|
+
- Status updates ("Build started", "Tests passed")
|
|
30
|
+
- Progress reports ("Step 3/5 complete")
|
|
31
|
+
- Notifications ("Deployment finished")
|
|
32
|
+
- Errors or alerts
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
telegram_send(message="✅ All tests passed. Ready to deploy.")
|
|
36
|
+
telegram_send(message="⚠️ Found 3 linting issues. Fixing now...")
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### telegram_ask
|
|
40
|
+
|
|
41
|
+
Ask a question and **WAIT** for a human reply. This tool **blocks** until the human responds or the timeout is reached. Use when you genuinely need human input — don't overuse it.
|
|
42
|
+
|
|
43
|
+
**Parameters:**
|
|
44
|
+
- `question` (required) — The question to ask
|
|
45
|
+
- `options` (optional) — Array of button options (default: `["Yes", "No", "Explain"]`)
|
|
46
|
+
- `timeoutMinutes` (optional) — How long to wait (default: 30 minutes)
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Simple yes/no
|
|
50
|
+
telegram_ask(question="Deploy v2.1.0 to production?")
|
|
51
|
+
|
|
52
|
+
# Custom options
|
|
53
|
+
telegram_ask(
|
|
54
|
+
question="Which environment should I target?",
|
|
55
|
+
options=["production", "staging", "development"]
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Short timeout
|
|
59
|
+
telegram_ask(
|
|
60
|
+
question="Approve this database migration?",
|
|
61
|
+
options=["Approve", "Reject"],
|
|
62
|
+
timeoutMinutes=5
|
|
63
|
+
)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Returns:**
|
|
67
|
+
- On reply: `Reply: "Yes"` with `details.answer = "Yes"`
|
|
68
|
+
- On timeout: `Timeout — proceeding autonomously` with `details.timedOut = true`
|
|
69
|
+
|
|
70
|
+
### telegram_override
|
|
71
|
+
|
|
72
|
+
Ask a human to approve or reject a **blocked action**. Use this when the supervisor blocks a dangerous operation (force push, destructive git ops, file deletion outside project boundary, etc.). This tool sends a structured message with the blocked command, reason, and context, then waits for the human to choose an action.
|
|
73
|
+
|
|
74
|
+
**Parameters:**
|
|
75
|
+
| Param | Required | Default | Description |
|
|
76
|
+
|-------|----------|---------|-------------|
|
|
77
|
+
| `command` | ✅ | — | The blocked command exactly as the supervisor reported it |
|
|
78
|
+
| `reason` | ✅ | — | Why the supervisor blocked it |
|
|
79
|
+
| `context` | ❌ | — | Additional context (what the user originally asked for) |
|
|
80
|
+
| `options` | ❌ | `["Yes, proceed", "No, cancel", "Explain more"]` | Custom button labels — 1st = proceed, 2nd = cancel, rest = explain |
|
|
81
|
+
| `timeoutMinutes` | ❌ | `30` | How long to wait for a response |
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# Force push override
|
|
85
|
+
telegram_override(
|
|
86
|
+
command="git push gitea feat/add-check --force",
|
|
87
|
+
reason="Force push can overwrite remote history"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Destructive operation with extra context
|
|
91
|
+
telegram_override(
|
|
92
|
+
command="rm -rf node_modules",
|
|
93
|
+
reason="Recursive deletion outside project boundary",
|
|
94
|
+
context="User requested a clean reinstall of dependencies"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Custom options
|
|
98
|
+
telegram_override(
|
|
99
|
+
command="chmod -R 777 /var/www",
|
|
100
|
+
reason="Broad permission changes detected",
|
|
101
|
+
options=["Yes, it's safe", "No, cancel", "Use 755 instead", "Explain more"],
|
|
102
|
+
timeoutMinutes=10
|
|
103
|
+
)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Returns `details`:**
|
|
107
|
+
| Field | Type | Description |
|
|
108
|
+
|-------|------|-------------|
|
|
109
|
+
| `choice` | `string` | The exact button label the human clicked |
|
|
110
|
+
| `action` | `"proceed"` \| `"abort"` \| `"explain"` | Mapped action based on which button was clicked |
|
|
111
|
+
| `command` | `string` | The original blocked command (echoed back) |
|
|
112
|
+
| `timedOut` | `boolean` | `true` if no response within timeout |
|
|
113
|
+
|
|
114
|
+
**Mandatory workflow when blocked:**
|
|
115
|
+
1. Supervisor blocks your command → you see the reason and blocked command
|
|
116
|
+
2. Call `telegram_override(command, reason, context)` to ask the human
|
|
117
|
+
3. Check `details.action`:
|
|
118
|
+
- **`"proceed"`** → call `supervisor_override(reason="<human approved>", command="<blocked command>")` then re-run
|
|
119
|
+
- **`"abort"`** → tell the user the action was cancelled: `telegram_send(message="Action cancelled as requested.")`
|
|
120
|
+
- **`"explain"`** → send more context via `telegram_send()`, then call `telegram_ask()` for follow-up
|
|
121
|
+
4. If **`details.timedOut === true`** → abort the action and tell the user: `telegram_send(message="No response received. Action aborted.")`
|
|
122
|
+
|
|
123
|
+
**On timeout:** Sends "⏰ No reply to override request. Aborting action." to Telegram and returns `details.timedOut = true`.
|
|
124
|
+
|
|
125
|
+
**Behavior on timeout:** The agent proceeds without human input. A "No reply" message is sent to Telegram.
|
|
126
|
+
|
|
127
|
+
### telegram_status
|
|
128
|
+
|
|
129
|
+
Check if the bridge is configured and the background listener is running.
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
telegram_status()
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Possible responses:**
|
|
136
|
+
- `Not configured. Set TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID.` — Missing env vars
|
|
137
|
+
- `Active. Bot: @mybot (listener: 🟢 running)` — Fully operational
|
|
138
|
+
- `Token set but unreachable.` — Network or auth issue
|
|
139
|
+
|
|
140
|
+
## Background Listener
|
|
141
|
+
|
|
142
|
+
A background listener starts automatically on session start and runs throughout the session. It:
|
|
143
|
+
- Polls Telegram every 5 seconds for new messages
|
|
144
|
+
- Forwards any non-bot text messages from the configured chat to the agent
|
|
145
|
+
- Sends a confirmation (`👂 Got it! Working on: ...`) to acknowledge receipt
|
|
146
|
+
- Stops cleanly on session shutdown
|
|
147
|
+
|
|
148
|
+
The listener ignores:
|
|
149
|
+
- Messages from bots (including itself)
|
|
150
|
+
- Messages from other chats
|
|
151
|
+
- Callback queries (button presses) — these are handled by `telegram_ask`
|
|
152
|
+
|
|
153
|
+
## Best Practices
|
|
154
|
+
|
|
155
|
+
1. **Use `telegram_send` for routine updates** — it's non-blocking
|
|
156
|
+
2. **Use `telegram_ask` sparingly** — only when you truly need human input
|
|
157
|
+
3. **Use `telegram_override` for blocked actions** — always ask before calling `supervisor_override`
|
|
158
|
+
4. **Set reasonable timeouts** — don't block the agent indefinitely
|
|
159
|
+
5. **Handle timeouts gracefully** — the agent should be able to proceed without human input
|
|
160
|
+
6. **Check `telegram_status` first** — verify the bridge is configured before sending messages
|
|
161
|
+
|
|
162
|
+
## Troubleshooting
|
|
163
|
+
|
|
164
|
+
| Problem | Solution |
|
|
165
|
+
|---------|----------|
|
|
166
|
+
| `telegram_status` says not configured | Set `TELEGRAM_BOT_TOKEN` and `TELEGRAM_CHAT_ID` env vars |
|
|
167
|
+
| Messages not being delivered | Check that the bot token is valid and the chat ID is correct |
|
|
168
|
+
| Bot unreachable | Check network connectivity and Telegram API status |
|
|
169
|
+
| Duplicate messages | The bridge tracks update IDs to avoid duplicates. If it persists, restart the session |
|
|
170
|
+
| Listener stops unexpectedly | Check logs for errors. The listener auto-recovers from transient failures |
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nandal
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# pi-telegram-bridge
|
|
2
|
+
|
|
3
|
+
Telegram bot bridge for pi agents — send messages, ask questions, and listen for human replies via Telegram.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pi install npm:@bytesbrains/pi-telegram-bridge
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Configuration
|
|
12
|
+
|
|
13
|
+
Set the following environment variables:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
export TELEGRAM_BOT_TOKEN="123456:ABC-DEF1234ghikl-zyx57W2v1u123ew11"
|
|
17
|
+
export TELEGRAM_CHAT_ID="123456789"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
| Variable | Required | Description |
|
|
21
|
+
|----------|----------|-------------|
|
|
22
|
+
| `TELEGRAM_BOT_TOKEN` | ✅ | Bot token from [@BotFather](https://t.me/BotFather) |
|
|
23
|
+
| `TELEGRAM_CHAT_ID` | ✅ | Target chat ID (your personal chat or a group) |
|
|
24
|
+
|
|
25
|
+
## Tools
|
|
26
|
+
|
|
27
|
+
### telegram_listen
|
|
28
|
+
|
|
29
|
+
Check for new inbound messages from the human.
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
telegram_listen()
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### telegram_send
|
|
36
|
+
|
|
37
|
+
Send a one-way message. Use for status updates and progress reports.
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
telegram_send(message="Build completed successfully ✅")
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### telegram_ask
|
|
44
|
+
|
|
45
|
+
Ask a question with inline keyboard options and **wait** for a human reply. Blocks until answered or timeout (default 30 min).
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
telegram_ask(question="Deploy to production?")
|
|
49
|
+
telegram_ask(question="Which branch?", options=["main", "staging", "dev"])
|
|
50
|
+
telegram_ask(question="Approve this change?", options=["Approve", "Reject", "Need changes"], timeoutMinutes=5)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### telegram_override
|
|
54
|
+
|
|
55
|
+
Ask a human to approve or reject a blocked action (force push, destructive git ops, file deletion, etc.). Designed for supervisor override flows. Returns `details.action = "proceed"|"abort"|"explain"` so the agent can call `supervisor_override()` or abort.
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
telegram_override(
|
|
59
|
+
command="git push gitea feat/add-check --force",
|
|
60
|
+
reason="Force push can overwrite remote history"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
telegram_override(
|
|
64
|
+
command="rm -rf node_modules",
|
|
65
|
+
reason="Recursive deletion outside project boundary",
|
|
66
|
+
context="User requested a clean reinstall of dependencies",
|
|
67
|
+
options=["Yes, it's fine", "No, cancel", "Use npx instead", "Explain more"],
|
|
68
|
+
timeoutMinutes=10
|
|
69
|
+
)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Parameters:**
|
|
73
|
+
| Param | Required | Default | Description |
|
|
74
|
+
|-------|----------|---------|-------------|
|
|
75
|
+
| `command` | ✅ | — | The blocked command or action |
|
|
76
|
+
| `reason` | ✅ | — | Why it was blocked by the supervisor |
|
|
77
|
+
| `context` | ❌ | — | Additional context (what the user asked for, etc.) |
|
|
78
|
+
| `options` | ❌ | `["Yes, proceed", "No, cancel", "Explain more"]` | Custom button labels |
|
|
79
|
+
| `timeoutMinutes` | ❌ | `30` | How long to wait for a response |
|
|
80
|
+
|
|
81
|
+
**Returns `details`:**
|
|
82
|
+
| Field | Type | Description |
|
|
83
|
+
|-------|------|-------------|
|
|
84
|
+
| `choice` | `string` | The exact button label the human clicked |
|
|
85
|
+
| `action` | `string` | `"proceed"` (first option), `"abort"` (second option), or `"explain"` (anything else) |
|
|
86
|
+
| `command` | `string` | The original blocked command |
|
|
87
|
+
| `timedOut` | `boolean` | Whether the request timed out |
|
|
88
|
+
|
|
89
|
+
**Agent workflow:**
|
|
90
|
+
1. Agent gets blocked by supervisor
|
|
91
|
+
2. Calls `telegram_override(command, reason, context)`
|
|
92
|
+
3. Checks `details.action`:
|
|
93
|
+
- `"proceed"` → call `supervisor_override(reason, command)`
|
|
94
|
+
- `"abort"` → tell user action was cancelled
|
|
95
|
+
- `"explain"` → call `telegram_send(message="...")` with more context, then re-ask
|
|
96
|
+
4. If `details.timedOut` → abort the action
|
|
97
|
+
|
|
98
|
+
### telegram_status
|
|
99
|
+
|
|
100
|
+
Check if the bridge is configured and running.
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
telegram_status()
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Background Listener
|
|
107
|
+
|
|
108
|
+
The extension starts a background listener on session start that polls for incoming Telegram messages. Any non-bot text message in the configured chat is forwarded to the agent as a user message. The agent responds with a confirmation.
|
|
109
|
+
|
|
110
|
+
The listener runs automatically — no manual setup needed.
|
|
111
|
+
|
|
112
|
+
## How It Works
|
|
113
|
+
|
|
114
|
+
1. **Session start**: The bridge restores the last processed update ID from session state and starts polling
|
|
115
|
+
2. **Inbound messages**: Non-bot text messages in the configured chat are forwarded to the agent
|
|
116
|
+
3. **Session end**: The listener stops cleanly on session shutdown
|
|
117
|
+
4. **Update ID is persisted** across sessions to avoid processing duplicate messages
|
|
118
|
+
|
|
119
|
+
## Get a Bot Token
|
|
120
|
+
|
|
121
|
+
1. Open Telegram and chat with [@BotFather](https://t.me/BotFather)
|
|
122
|
+
2. Send `/newbot` and follow the prompts
|
|
123
|
+
3. Copy the token and set `TELEGRAM_BOT_TOKEN`
|
|
124
|
+
4. Send a message to your bot, then visit:
|
|
125
|
+
`https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates`
|
|
126
|
+
5. Copy the `chat.id` from the response and set `TELEGRAM_CHAT_ID`
|
|
127
|
+
|
|
128
|
+
## Requirements
|
|
129
|
+
|
|
130
|
+
- Node.js >= 18
|
|
131
|
+
- A Telegram bot token
|
|
132
|
+
- A chat ID to send/receive messages
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bytesbrains/pi-telegram-bridge",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Telegram bot bridge for pi agents — send messages, ask questions, and listen for human replies via Telegram.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi-extension",
|
|
8
|
+
"telegram",
|
|
9
|
+
"bot",
|
|
10
|
+
"bridge",
|
|
11
|
+
"messaging"
|
|
12
|
+
],
|
|
13
|
+
"author": "nandal <nandal@users.noreply.github.com>",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/nandal/pi-ext.git",
|
|
17
|
+
"directory": "telegram-bridge"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/nandal/pi-ext/tree/main/telegram-bridge",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/nandal/pi-ext/issues"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"main": "./src/index.ts",
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"src/",
|
|
30
|
+
"README.md",
|
|
31
|
+
"AGENTS.md",
|
|
32
|
+
"LICENSE"
|
|
33
|
+
],
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
36
|
+
"typebox": "*"
|
|
37
|
+
},
|
|
38
|
+
"pi": {
|
|
39
|
+
"extensions": [
|
|
40
|
+
"./src/index.ts"
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"test": "vitest run",
|
|
45
|
+
"test:watch": "vitest"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"typescript": "^6.0.3",
|
|
49
|
+
"vitest": "^2.1.9"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-telegram-bridge — Telegram API Helpers
|
|
3
|
+
*/
|
|
4
|
+
const BASE_URL = "https://api.telegram.org/bot";
|
|
5
|
+
|
|
6
|
+
// ── Config ──────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
export function getToken(): string {
|
|
9
|
+
const tok = process.env.TELEGRAM_BOT_TOKEN;
|
|
10
|
+
if (!tok) throw new Error("TELEGRAM_BOT_TOKEN not set");
|
|
11
|
+
return tok;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getChatId(): string {
|
|
15
|
+
const id = process.env.TELEGRAM_CHAT_ID;
|
|
16
|
+
if (!id) throw new Error("TELEGRAM_CHAT_ID not set");
|
|
17
|
+
return id;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ── API ─────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
export async function telegramApi(
|
|
23
|
+
method: string,
|
|
24
|
+
body: Record<string, unknown>,
|
|
25
|
+
): Promise<unknown> {
|
|
26
|
+
const url = `${BASE_URL}${getToken()}/${method}`;
|
|
27
|
+
const res = await fetch(url, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: { "Content-Type": "application/json" },
|
|
30
|
+
body: JSON.stringify(body),
|
|
31
|
+
});
|
|
32
|
+
if (!res.ok) {
|
|
33
|
+
throw new Error(`Telegram API ${res.status}: ${await res.text()}`);
|
|
34
|
+
}
|
|
35
|
+
return res.json();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── Messaging ───────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
export async function sendMsg(
|
|
41
|
+
text: string,
|
|
42
|
+
replyMarkup?: object,
|
|
43
|
+
): Promise<number> {
|
|
44
|
+
const body: Record<string, unknown> = {
|
|
45
|
+
chat_id: getChatId(),
|
|
46
|
+
text,
|
|
47
|
+
parse_mode: "Markdown",
|
|
48
|
+
};
|
|
49
|
+
if (replyMarkup) body.reply_markup = JSON.stringify(replyMarkup);
|
|
50
|
+
const r = (await telegramApi("sendMessage", body)) as {
|
|
51
|
+
ok: boolean;
|
|
52
|
+
result: { message_id: number };
|
|
53
|
+
};
|
|
54
|
+
return r.result.message_id;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Polling ─────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
export async function pollReply(
|
|
60
|
+
sentId: number,
|
|
61
|
+
chatId: string,
|
|
62
|
+
timeoutMs: number,
|
|
63
|
+
): Promise<string | null> {
|
|
64
|
+
const token = getToken();
|
|
65
|
+
const deadline = Date.now() + timeoutMs;
|
|
66
|
+
|
|
67
|
+
// Drain pending updates to start fresh
|
|
68
|
+
let lastId = 0;
|
|
69
|
+
try {
|
|
70
|
+
const drain = await fetch(`${BASE_URL}${token}/getUpdates?timeout=0`);
|
|
71
|
+
if (drain.ok) {
|
|
72
|
+
const d = (await drain.json()) as {
|
|
73
|
+
ok: boolean;
|
|
74
|
+
result: Array<{ update_id: number }>;
|
|
75
|
+
};
|
|
76
|
+
if (d.ok && d.result.length > 0) {
|
|
77
|
+
lastId = d.result[d.result.length - 1].update_id;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
/* ignore drain errors */
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
while (Date.now() < deadline) {
|
|
85
|
+
const res = await fetch(
|
|
86
|
+
`${BASE_URL}${token}/getUpdates?offset=${lastId + 1}&timeout=5`,
|
|
87
|
+
);
|
|
88
|
+
if (!res.ok) {
|
|
89
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const data = (await res.json()) as {
|
|
93
|
+
ok: boolean;
|
|
94
|
+
result: Array<{
|
|
95
|
+
update_id: number;
|
|
96
|
+
message?: {
|
|
97
|
+
message_id: number;
|
|
98
|
+
chat: { id: number };
|
|
99
|
+
text?: string;
|
|
100
|
+
};
|
|
101
|
+
callback_query?: {
|
|
102
|
+
id: string;
|
|
103
|
+
message: { message_id: number; chat: { id: number } };
|
|
104
|
+
data: string;
|
|
105
|
+
};
|
|
106
|
+
}>;
|
|
107
|
+
};
|
|
108
|
+
if (!data.ok) {
|
|
109
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
for (const u of data.result) {
|
|
113
|
+
lastId = u.update_id;
|
|
114
|
+
// Check for text reply
|
|
115
|
+
if (
|
|
116
|
+
u.message &&
|
|
117
|
+
u.message.chat.id === Number(chatId) &&
|
|
118
|
+
u.message.message_id > sentId &&
|
|
119
|
+
u.message.text
|
|
120
|
+
) {
|
|
121
|
+
return u.message.text;
|
|
122
|
+
}
|
|
123
|
+
// Check for callback query (inline button)
|
|
124
|
+
if (
|
|
125
|
+
u.callback_query &&
|
|
126
|
+
u.callback_query.message.chat.id === Number(chatId)
|
|
127
|
+
) {
|
|
128
|
+
await telegramApi("answerCallbackQuery", {
|
|
129
|
+
callback_query_id: u.callback_query.id,
|
|
130
|
+
});
|
|
131
|
+
return u.callback_query.data;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
135
|
+
}
|
|
136
|
+
return null; // timeout
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── Listener State ──────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
export async function getBotId(): Promise<number | null> {
|
|
142
|
+
try {
|
|
143
|
+
const r = (await telegramApi("getMe", {})) as { result: { id: number } };
|
|
144
|
+
return r.result.id;
|
|
145
|
+
} catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function seedLastUpdateId(
|
|
151
|
+
lastUpdateId: number,
|
|
152
|
+
signal?: AbortSignal,
|
|
153
|
+
): Promise<number> {
|
|
154
|
+
if (lastUpdateId !== 0) return lastUpdateId;
|
|
155
|
+
const token = getToken();
|
|
156
|
+
try {
|
|
157
|
+
const res = await fetch(`${BASE_URL}${token}/getUpdates?timeout=0`, {
|
|
158
|
+
signal,
|
|
159
|
+
});
|
|
160
|
+
if (res.ok) {
|
|
161
|
+
const d = (await res.json()) as {
|
|
162
|
+
ok: boolean;
|
|
163
|
+
result: Array<{ update_id: number }>;
|
|
164
|
+
};
|
|
165
|
+
if (d.ok && d.result.length > 0) {
|
|
166
|
+
return d.result[d.result.length - 1].update_id;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
/* ignore */
|
|
171
|
+
}
|
|
172
|
+
return lastUpdateId;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function pollUpdates(
|
|
176
|
+
token: string,
|
|
177
|
+
chatId: string,
|
|
178
|
+
botId: number | null,
|
|
179
|
+
lastUpdateId: number,
|
|
180
|
+
signal: AbortSignal,
|
|
181
|
+
onMessage: (text: string) => void,
|
|
182
|
+
onUpdateId: (id: number) => void,
|
|
183
|
+
): Promise<void> {
|
|
184
|
+
while (!signal.aborted) {
|
|
185
|
+
try {
|
|
186
|
+
const res = await fetch(
|
|
187
|
+
`${BASE_URL}${token}/getUpdates?offset=${lastUpdateId + 1}&timeout=10`,
|
|
188
|
+
{ signal },
|
|
189
|
+
);
|
|
190
|
+
if (!res.ok) {
|
|
191
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const data = (await res.json()) as {
|
|
195
|
+
ok: boolean;
|
|
196
|
+
result: Array<{
|
|
197
|
+
update_id: number;
|
|
198
|
+
message?: {
|
|
199
|
+
message_id: number;
|
|
200
|
+
chat: { id: number };
|
|
201
|
+
from?: { id: number; is_bot?: boolean };
|
|
202
|
+
text?: string;
|
|
203
|
+
};
|
|
204
|
+
callback_query?: unknown;
|
|
205
|
+
}>;
|
|
206
|
+
};
|
|
207
|
+
if (!data.ok) {
|
|
208
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
for (const u of data.result) {
|
|
212
|
+
lastUpdateId = u.update_id;
|
|
213
|
+
onUpdateId(lastUpdateId);
|
|
214
|
+
// Skip callback queries (handled by telegram_ask)
|
|
215
|
+
if (u.callback_query) continue;
|
|
216
|
+
// Only process text messages from the configured chat, not from our bot
|
|
217
|
+
if (
|
|
218
|
+
u.message &&
|
|
219
|
+
u.message.text &&
|
|
220
|
+
String(u.message.chat.id) === chatId
|
|
221
|
+
) {
|
|
222
|
+
if (u.message.from?.is_bot || u.message.from?.id === botId) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
onMessage(u.message.text);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
} catch (e: unknown) {
|
|
229
|
+
if (e instanceof Error && e.name === "AbortError") break;
|
|
230
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
231
|
+
}
|
|
232
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
233
|
+
}
|
|
234
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-telegram-bridge — Telegram Bot Bridge
|
|
3
|
+
*
|
|
4
|
+
* Tools: telegram_listen, telegram_send, telegram_ask, telegram_override, telegram_status
|
|
5
|
+
* Config: TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID environment variables
|
|
6
|
+
*
|
|
7
|
+
* Starts a background listener on session start that polls for incoming
|
|
8
|
+
* messages and forwards them to the agent as user messages.
|
|
9
|
+
*/
|
|
10
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import {
|
|
12
|
+
getToken,
|
|
13
|
+
getChatId,
|
|
14
|
+
telegramApi,
|
|
15
|
+
sendMsg,
|
|
16
|
+
pollReply,
|
|
17
|
+
getBotId,
|
|
18
|
+
seedLastUpdateId,
|
|
19
|
+
pollUpdates,
|
|
20
|
+
} from "./helpers";
|
|
21
|
+
import {
|
|
22
|
+
listenSchema,
|
|
23
|
+
sendSchema,
|
|
24
|
+
askSchema,
|
|
25
|
+
statusSchema,
|
|
26
|
+
overrideSchema,
|
|
27
|
+
} from "./tools/telegram";
|
|
28
|
+
|
|
29
|
+
export default function telegramBridge(pi: ExtensionAPI) {
|
|
30
|
+
let botId: number | null = null;
|
|
31
|
+
let listenerAbort: AbortController | null = null;
|
|
32
|
+
let lastUpdateId = 0;
|
|
33
|
+
|
|
34
|
+
// ── Session lifecycle ───────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
37
|
+
// Restore lastUpdateId from session state
|
|
38
|
+
for (const entry of ctx.sessionManager.getEntries()) {
|
|
39
|
+
if (entry.type === "custom" && entry.customType === "tg-last-update") {
|
|
40
|
+
lastUpdateId = (entry.data as { update_id: number }).update_id;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Start background listener
|
|
44
|
+
startListener();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
pi.on("session_shutdown", () => {
|
|
48
|
+
listenerAbort?.abort();
|
|
49
|
+
listenerAbort = null;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// ── Background listener ─────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
async function startListener() {
|
|
55
|
+
if (listenerAbort) return; // already running
|
|
56
|
+
listenerAbort = new AbortController();
|
|
57
|
+
const signal = listenerAbort.signal;
|
|
58
|
+
const token = getToken();
|
|
59
|
+
const chatId = getChatId();
|
|
60
|
+
|
|
61
|
+
// Get bot's own ID once to filter out self-messages
|
|
62
|
+
if (!botId) {
|
|
63
|
+
botId = await getBotId();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Seed lastUpdateId
|
|
67
|
+
lastUpdateId = await seedLastUpdateId(lastUpdateId, signal);
|
|
68
|
+
|
|
69
|
+
// Fire and forget — poll in background
|
|
70
|
+
pollUpdates(
|
|
71
|
+
token,
|
|
72
|
+
chatId,
|
|
73
|
+
botId,
|
|
74
|
+
lastUpdateId,
|
|
75
|
+
signal,
|
|
76
|
+
// onMessage: forward to pi as user message
|
|
77
|
+
async (text: string) => {
|
|
78
|
+
pi.sendUserMessage(text);
|
|
79
|
+
await sendMsg(`👂 Got it! Working on: _${text.slice(0, 100)}_`);
|
|
80
|
+
},
|
|
81
|
+
// onUpdateId: persist offset
|
|
82
|
+
(id: number) => {
|
|
83
|
+
lastUpdateId = id;
|
|
84
|
+
pi.appendEntry("tg-last-update", { update_id: id });
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Tools ───────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
// telegram_listen
|
|
92
|
+
pi.registerTool({
|
|
93
|
+
name: "telegram_listen",
|
|
94
|
+
label: "Telegram Listen",
|
|
95
|
+
description:
|
|
96
|
+
"Check Telegram for any new inbound messages from the human. Returns the latest message text, or indicates no new messages.",
|
|
97
|
+
parameters: listenSchema,
|
|
98
|
+
async execute() {
|
|
99
|
+
try {
|
|
100
|
+
const token = getToken();
|
|
101
|
+
const chatId = getChatId();
|
|
102
|
+
if (!botId) botId = await getBotId();
|
|
103
|
+
|
|
104
|
+
const res = await fetch(
|
|
105
|
+
`https://api.telegram.org/bot${token}/getUpdates?offset=${lastUpdateId + 1}&timeout=5`,
|
|
106
|
+
);
|
|
107
|
+
if (!res.ok)
|
|
108
|
+
return {
|
|
109
|
+
content: [{ type: "text", text: "Could not reach Telegram." }],
|
|
110
|
+
isError: true,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const data = (await res.json()) as {
|
|
114
|
+
ok: boolean;
|
|
115
|
+
result: Array<{
|
|
116
|
+
update_id: number;
|
|
117
|
+
message?: {
|
|
118
|
+
message_id: number;
|
|
119
|
+
chat: { id: number };
|
|
120
|
+
from?: { id: number; is_bot?: boolean };
|
|
121
|
+
text?: string;
|
|
122
|
+
};
|
|
123
|
+
callback_query?: unknown;
|
|
124
|
+
}>;
|
|
125
|
+
};
|
|
126
|
+
if (!data.ok)
|
|
127
|
+
return {
|
|
128
|
+
content: [{ type: "text", text: "Telegram API error." }],
|
|
129
|
+
isError: true,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
for (const u of data.result) {
|
|
133
|
+
lastUpdateId = u.update_id;
|
|
134
|
+
pi.appendEntry("tg-last-update", { update_id: lastUpdateId });
|
|
135
|
+
if (u.callback_query) continue;
|
|
136
|
+
if (
|
|
137
|
+
u.message &&
|
|
138
|
+
u.message.text &&
|
|
139
|
+
String(u.message.chat.id) === chatId
|
|
140
|
+
) {
|
|
141
|
+
if (u.message.from?.is_bot || u.message.from?.id === botId)
|
|
142
|
+
continue;
|
|
143
|
+
return {
|
|
144
|
+
content: [
|
|
145
|
+
{
|
|
146
|
+
type: "text",
|
|
147
|
+
text: `📩 New message: "${u.message.text}"`,
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
details: { message: u.message.text },
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return { content: [{ type: "text", text: "No new messages." }] };
|
|
155
|
+
} catch (e: unknown) {
|
|
156
|
+
return {
|
|
157
|
+
content: [
|
|
158
|
+
{
|
|
159
|
+
type: "text",
|
|
160
|
+
text: `Failed: ${e instanceof Error ? e.message : e}`,
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
isError: true,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// telegram_send
|
|
170
|
+
pi.registerTool({
|
|
171
|
+
name: "telegram_send",
|
|
172
|
+
label: "Telegram Send",
|
|
173
|
+
description:
|
|
174
|
+
"Send a one-way message to Telegram. Use for status updates.",
|
|
175
|
+
parameters: sendSchema,
|
|
176
|
+
async execute(_id: string, params: { message: string }) {
|
|
177
|
+
try {
|
|
178
|
+
const mid = await sendMsg(params.message);
|
|
179
|
+
return { content: [{ type: "text", text: `Sent (id:${mid})` }] };
|
|
180
|
+
} catch (e: unknown) {
|
|
181
|
+
return {
|
|
182
|
+
content: [
|
|
183
|
+
{
|
|
184
|
+
type: "text",
|
|
185
|
+
text: `Failed: ${e instanceof Error ? e.message : e}`,
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
isError: true,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// telegram_ask
|
|
195
|
+
pi.registerTool({
|
|
196
|
+
name: "telegram_ask",
|
|
197
|
+
label: "Telegram Ask",
|
|
198
|
+
description:
|
|
199
|
+
"Ask a question on Telegram and WAIT for a human reply. BLOCKS until you answer. Use when you need human input.",
|
|
200
|
+
parameters: askSchema,
|
|
201
|
+
async execute(
|
|
202
|
+
_id: string,
|
|
203
|
+
params: {
|
|
204
|
+
question: string;
|
|
205
|
+
options?: string[];
|
|
206
|
+
timeoutMinutes?: number;
|
|
207
|
+
},
|
|
208
|
+
) {
|
|
209
|
+
try {
|
|
210
|
+
const chatId = getChatId();
|
|
211
|
+
const opts = params.options ?? ["Yes", "No", "Explain"];
|
|
212
|
+
const kb = opts.map((o: string) => [{ text: o, callback_data: o }]);
|
|
213
|
+
const timeoutMs = (params.timeoutMinutes ?? 30) * 60000;
|
|
214
|
+
const mid = await sendMsg(`❓ *${params.question}*`, {
|
|
215
|
+
inline_keyboard: kb,
|
|
216
|
+
});
|
|
217
|
+
const reply = await pollReply(mid, chatId, timeoutMs);
|
|
218
|
+
if (!reply) {
|
|
219
|
+
await sendMsg("⏰ No reply. Proceeding autonomously.");
|
|
220
|
+
return {
|
|
221
|
+
content: [
|
|
222
|
+
{ type: "text", text: "Timeout — proceeding autonomously" },
|
|
223
|
+
],
|
|
224
|
+
details: { timedOut: true },
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
content: [{ type: "text", text: `Reply: "${reply}"` }],
|
|
229
|
+
details: { answer: reply },
|
|
230
|
+
};
|
|
231
|
+
} catch (e: unknown) {
|
|
232
|
+
return {
|
|
233
|
+
content: [
|
|
234
|
+
{
|
|
235
|
+
type: "text",
|
|
236
|
+
text: `Failed: ${e instanceof Error ? e.message : e}`,
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
isError: true,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// telegram_override
|
|
246
|
+
pi.registerTool({
|
|
247
|
+
name: "telegram_override",
|
|
248
|
+
label: "Telegram Override",
|
|
249
|
+
description:
|
|
250
|
+
"Ask a human on Telegram to approve or reject a blocked action. " +
|
|
251
|
+
"Use when the supervisor blocks a dangerous command (force push, " +
|
|
252
|
+
"destructive git ops, file deletion, etc.) and you need human approval. " +
|
|
253
|
+
"Returns the human's choice so you can call supervisor_override or abort.",
|
|
254
|
+
parameters: overrideSchema,
|
|
255
|
+
async execute(
|
|
256
|
+
_id: string,
|
|
257
|
+
params: {
|
|
258
|
+
command: string;
|
|
259
|
+
reason: string;
|
|
260
|
+
context?: string;
|
|
261
|
+
options?: string[];
|
|
262
|
+
timeoutMinutes?: number;
|
|
263
|
+
},
|
|
264
|
+
) {
|
|
265
|
+
try {
|
|
266
|
+
const chatId = getChatId();
|
|
267
|
+
const opts = params.options ?? [
|
|
268
|
+
"Yes, proceed",
|
|
269
|
+
"No, cancel",
|
|
270
|
+
"Explain more",
|
|
271
|
+
];
|
|
272
|
+
const timeoutMs = (params.timeoutMinutes ?? 30) * 60000;
|
|
273
|
+
|
|
274
|
+
let message = `🛑 *Supervisor blocked an action*\n\n`;
|
|
275
|
+
message += `*Command:* \`${params.command.slice(0, 200)}\`\n`;
|
|
276
|
+
message += `*Reason:* ${params.reason.slice(0, 300)}\n`;
|
|
277
|
+
if (params.context) {
|
|
278
|
+
message += `*Context:* ${params.context.slice(0, 300)}\n`;
|
|
279
|
+
}
|
|
280
|
+
message += `\n_What should I do?_`;
|
|
281
|
+
|
|
282
|
+
const kb = opts.map((o: string) => [{ text: o, callback_data: o }]);
|
|
283
|
+
const mid = await sendMsg(message, { inline_keyboard: kb });
|
|
284
|
+
const reply = await pollReply(mid, chatId, timeoutMs);
|
|
285
|
+
|
|
286
|
+
if (!reply) {
|
|
287
|
+
await sendMsg("⏰ No reply to override request. Aborting action.");
|
|
288
|
+
return {
|
|
289
|
+
content: [
|
|
290
|
+
{
|
|
291
|
+
type: "text",
|
|
292
|
+
text: "Timeout — no human response. Action aborted.",
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
details: {
|
|
296
|
+
timedOut: true,
|
|
297
|
+
action: "abort",
|
|
298
|
+
command: params.command,
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Map the reply to an action
|
|
304
|
+
const choice = reply.trim();
|
|
305
|
+
let action: string;
|
|
306
|
+
if (choice === opts[0]) {
|
|
307
|
+
action = "proceed";
|
|
308
|
+
} else if (choice === opts[1]) {
|
|
309
|
+
action = "abort";
|
|
310
|
+
} else {
|
|
311
|
+
action = "explain";
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
await sendMsg(`✅ Choice received: _${choice}_`);
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
content: [
|
|
318
|
+
{
|
|
319
|
+
type: "text",
|
|
320
|
+
text: `Human chose: "${choice}" → action: ${action}`,
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
details: {
|
|
324
|
+
choice,
|
|
325
|
+
action,
|
|
326
|
+
command: params.command,
|
|
327
|
+
timedOut: false,
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
} catch (e: unknown) {
|
|
331
|
+
return {
|
|
332
|
+
content: [
|
|
333
|
+
{
|
|
334
|
+
type: "text",
|
|
335
|
+
text: `Failed: ${e instanceof Error ? e.message : e}`,
|
|
336
|
+
},
|
|
337
|
+
],
|
|
338
|
+
isError: true,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// telegram_status
|
|
345
|
+
pi.registerTool({
|
|
346
|
+
name: "telegram_status",
|
|
347
|
+
label: "Telegram Status",
|
|
348
|
+
description:
|
|
349
|
+
"Check if the Telegram bridge is configured and running.",
|
|
350
|
+
parameters: statusSchema,
|
|
351
|
+
async execute() {
|
|
352
|
+
if (!process.env.TELEGRAM_BOT_TOKEN || !process.env.TELEGRAM_CHAT_ID) {
|
|
353
|
+
return {
|
|
354
|
+
content: [
|
|
355
|
+
{
|
|
356
|
+
type: "text",
|
|
357
|
+
text: "Not configured. Set TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID.",
|
|
358
|
+
},
|
|
359
|
+
],
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
const r = (await telegramApi("getMe", {})) as {
|
|
364
|
+
result: { username: string };
|
|
365
|
+
};
|
|
366
|
+
return {
|
|
367
|
+
content: [
|
|
368
|
+
{
|
|
369
|
+
type: "text",
|
|
370
|
+
text: `Active. Bot: @${r.result.username} (listener: ${listenerAbort ? "🟢 running" : "🔴 stopped"})`,
|
|
371
|
+
},
|
|
372
|
+
],
|
|
373
|
+
};
|
|
374
|
+
} catch {
|
|
375
|
+
return {
|
|
376
|
+
content: [{ type: "text", text: "Token set but unreachable." }],
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-telegram-bridge — Tool Schemas
|
|
3
|
+
*
|
|
4
|
+
* Tool parameter schemas and metadata. Execute functions are wired in
|
|
5
|
+
* index.ts so they can capture the pi ExtensionAPI via closure.
|
|
6
|
+
*/
|
|
7
|
+
import { Type } from "typebox";
|
|
8
|
+
|
|
9
|
+
export const listenSchema = Type.Object({});
|
|
10
|
+
|
|
11
|
+
export const sendSchema = Type.Object({ message: Type.String() });
|
|
12
|
+
|
|
13
|
+
export const askSchema = Type.Object({
|
|
14
|
+
question: Type.String(),
|
|
15
|
+
options: Type.Optional(Type.Array(Type.String())),
|
|
16
|
+
timeoutMinutes: Type.Optional(Type.Number()),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const statusSchema = Type.Object({});
|
|
20
|
+
|
|
21
|
+
export const overrideSchema = Type.Object({
|
|
22
|
+
command: Type.String(),
|
|
23
|
+
reason: Type.String(),
|
|
24
|
+
context: Type.Optional(Type.String()),
|
|
25
|
+
options: Type.Optional(Type.Array(Type.String())),
|
|
26
|
+
timeoutMinutes: Type.Optional(Type.Number()),
|
|
27
|
+
});
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-telegram-bridge — Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface TelegramConfig {
|
|
6
|
+
/** Telegram Bot API token from @BotFather */
|
|
7
|
+
botToken: string;
|
|
8
|
+
/** Target chat ID to send messages to and listen from */
|
|
9
|
+
chatId: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Session state persisted across restarts */
|
|
13
|
+
export interface TelegramSessionState {
|
|
14
|
+
update_id: number;
|
|
15
|
+
}
|