paperclip-plugin-telegram 0.2.3 → 0.2.7
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/LICENSE +21 -0
- package/README.md +113 -88
- package/dist/acp-bridge.js +138 -49
- package/dist/acp-bridge.js.map +1 -1
- package/dist/command-registry.js +11 -16
- package/dist/command-registry.js.map +1 -1
- package/dist/commands.d.ts +1 -1
- package/dist/commands.js +59 -22
- package/dist/commands.js.map +1 -1
- package/dist/constants.d.ts +5 -2
- package/dist/constants.js +5 -2
- package/dist/constants.js.map +1 -1
- package/dist/formatters.d.ts +10 -6
- package/dist/formatters.js +95 -23
- package/dist/formatters.js.map +1 -1
- package/dist/manifest.js +67 -38
- package/dist/manifest.js.map +1 -1
- package/dist/media-pipeline.d.ts +1 -0
- package/dist/media-pipeline.js +14 -7
- package/dist/media-pipeline.js.map +1 -1
- package/dist/telegram-api.d.ts +3 -0
- package/dist/telegram-api.js +26 -0
- package/dist/telegram-api.js.map +1 -1
- package/dist/worker.js +186 -49
- package/dist/worker.js.map +1 -1
- package/package.json +10 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Matt Van Horn
|
|
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
CHANGED
|
@@ -1,27 +1,66 @@
|
|
|
1
1
|
# paperclip-plugin-telegram
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/paperclip-plugin-telegram)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
5
|
|
|
5
|
-
Bidirectional Telegram integration for [Paperclip](https://github.com/paperclipai/paperclip). Push agent notifications to Telegram, receive bot commands, approve requests with inline buttons, and
|
|
6
|
+
Bidirectional Telegram integration for [Paperclip](https://github.com/paperclipai/paperclip). Push agent notifications to Telegram, receive bot commands, approve requests with inline buttons, gather community signals, run multi-agent sessions in threads, process media attachments, register custom commands, and deploy proactive agent suggestions.
|
|
6
7
|
|
|
7
8
|
Built on the Paperclip plugin SDK and the domain event bridge ([PR #909](https://github.com/paperclipai/paperclip/pull/909)).
|
|
8
9
|
|
|
9
|
-
##
|
|
10
|
+
## Why this exists
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
Multiple Paperclip users asked for notifications on the same day the plugin system shipped (2026-03-14):
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
- **Bot commands**: `/status`, `/issues`, `/agents`, `/approve`, `/help`, `/connect`, `/connect-topic`, `/acp`, `/commands`
|
|
15
|
-
- **Inline buttons** for approve/reject on approval notifications
|
|
16
|
-
- **Inbound routing**: reply to a notification in Telegram and it becomes an issue comment in Paperclip
|
|
17
|
-
- **Per-company chat routing** with `/connect` command
|
|
18
|
-
- **Forum topic routing**: map Telegram topics to Paperclip projects
|
|
19
|
-
- **Daily digest**: scheduled summary of agent activity
|
|
20
|
-
- **MarkdownV2 formatting** with automatic plain text fallback
|
|
21
|
-
- **ACP bridge**: `/acp spawn`, `/acp status`, `/acp cancel`, `/acp close` for managing agent sessions
|
|
14
|
+
> "is there a way to have codex/claude check paperclip to see when tasks are done without me prompting it?" - @Choose Liberty, Discord #dev
|
|
22
15
|
|
|
23
|
-
|
|
16
|
+
> "basically to have it 'let me know when its done'" - @Choose Liberty, Discord #dev
|
|
17
|
+
|
|
18
|
+
> "can claude code check paperclip to see when tasks are done" - @Nascozz, Discord #dev
|
|
19
|
+
|
|
20
|
+
@dotta (maintainer) responded: "we're also adding issue-changed hooks for plugins so when that lands someone could [make notifications]." The event bridge ([PR #909](https://github.com/paperclipai/paperclip/pull/909)) shipped that same day. @Ryze said "Really excited by the plugins. I had developed a custom plugin bridge that I will now deprecate and migrate over to the new supported plugin system."
|
|
21
|
+
|
|
22
|
+
This is that plugin.
|
|
23
|
+
|
|
24
|
+
## What it does
|
|
25
|
+
|
|
26
|
+
### Notifications (MarkdownV2 with plain text fallback)
|
|
27
|
+
|
|
28
|
+
- **Issue created** - Title, description, status, priority, assignee, project fields, and a "View Issue" link
|
|
29
|
+
- **Issue done** - Completion confirmation with status fields
|
|
30
|
+
- **Approval requested** - Interactive **Approve** and **Reject** inline buttons. Click to act without leaving Telegram.
|
|
31
|
+
- **Agent error** - Error message with warning indicator
|
|
32
|
+
- **Agent run started/finished** - Lifecycle notifications
|
|
24
33
|
|
|
34
|
+
### Interactive approvals
|
|
35
|
+
- Approve/reject inline buttons on every approval notification
|
|
36
|
+
- Clicking a button calls the Paperclip API and updates the Telegram message inline
|
|
37
|
+
- Callback query acknowledgment with result text
|
|
38
|
+
|
|
39
|
+
### Per-type chat routing
|
|
40
|
+
- `approvalsChatId` - Dedicated chat for approval notifications
|
|
41
|
+
- `errorsChatId` - Dedicated chat for agent errors
|
|
42
|
+
- `escalationChatId` - Dedicated chat for agent escalations
|
|
43
|
+
- Falls back to `defaultChatId` when per-type chats aren't configured
|
|
44
|
+
- Per-company overrides via `/connect`
|
|
45
|
+
|
|
46
|
+
### Bot commands
|
|
47
|
+
- `/status` - Show active agents and recent completions
|
|
48
|
+
- `/issues` - List open issues
|
|
49
|
+
- `/agents` - List agents with status indicators
|
|
50
|
+
- `/approve <id>` - Approve a pending approval
|
|
51
|
+
- `/help` - Display all available commands
|
|
52
|
+
- `/connect <company>` - Link this chat to a Paperclip company
|
|
53
|
+
- `/connect_topic <project-name> <topic-id>` - Map a forum topic to a Paperclip project
|
|
54
|
+
- `/acp spawn <agent>` - Start a new agent session in the current thread
|
|
55
|
+
- `/acp status` - Check ACP session status
|
|
56
|
+
- `/acp cancel` - Cancel a running ACP session
|
|
57
|
+
- `/acp close` - Close a completed ACP session
|
|
58
|
+
- `/commands import <json>` - Import a workflow command
|
|
59
|
+
- `/commands list` - List registered workflow commands
|
|
60
|
+
- `/commands run <name> [args]` - Execute a workflow command
|
|
61
|
+
- `/commands delete <name>` - Delete a workflow command
|
|
62
|
+
|
|
63
|
+
### Phase 1: HITL Escalation
|
|
25
64
|
- Agents call `escalate_to_human` tool when stuck (low confidence, user request, policy violation, unknown intent)
|
|
26
65
|
- Escalation posted to dedicated channel with conversation context, suggested reply, and confidence score
|
|
27
66
|
- Inline buttons: Send Suggested Reply, Reply, Override, Dismiss
|
|
@@ -30,8 +69,7 @@ Built on the Paperclip plugin SDK and the domain event bridge ([PR #909](https:/
|
|
|
30
69
|
- Reply routing back to originating chat via native or ACP transport
|
|
31
70
|
|
|
32
71
|
### Phase 2: Multi-Agent Group Threads
|
|
33
|
-
|
|
34
|
-
- Multiple agents per thread (up to 5 configurable via `MAX_AGENTS_PER_THREAD`)
|
|
72
|
+
- Multiple agents per thread (up to 5 configurable via `maxAgentsPerThread`)
|
|
35
73
|
- `@mention` routing: address a specific agent by name in a multi-agent thread
|
|
36
74
|
- Reply-to routing: reply to an agent's message to route to that agent
|
|
37
75
|
- Fallback routing: most recently active agent receives unaddressed messages
|
|
@@ -44,14 +82,12 @@ Built on the Paperclip plugin SDK and the domain event bridge ([PR #909](https:/
|
|
|
44
82
|
- Auto-spawn on handoff/discuss if target agent isn't already in the thread
|
|
45
83
|
|
|
46
84
|
### Phase 3: Media-to-Task Pipeline
|
|
47
|
-
|
|
48
85
|
- Voice messages, audio, video notes, documents, and photos routed to agents
|
|
49
86
|
- Voice/audio transcription via Whisper API with transcription preview posted back
|
|
50
87
|
- **Brief Agent**: media sent to intake channels is forwarded to a configurable Brief Agent for triage
|
|
51
88
|
- Media in active agent threads is routed to the active session (native or ACP)
|
|
52
89
|
|
|
53
90
|
### Phase 4: Custom Workflow Commands
|
|
54
|
-
|
|
55
91
|
- `/commands import <json>` - import a multi-step workflow as a custom slash command
|
|
56
92
|
- `/commands list` - show all registered custom commands
|
|
57
93
|
- `/commands run <name> [args]` - execute a workflow
|
|
@@ -62,7 +98,6 @@ Built on the Paperclip plugin SDK and the domain event bridge ([PR #909](https:/
|
|
|
62
98
|
- Per-company command registry
|
|
63
99
|
|
|
64
100
|
### Phase 5: Proactive Agent Suggestions
|
|
65
|
-
|
|
66
101
|
- Agents call `register_watch` tool to set up condition-based monitors
|
|
67
102
|
- Watch conditions: `gt`, `lt`, `eq`, `ne`, `contains`, `exists` operators on entity fields
|
|
68
103
|
- Watches evaluate against issues, agents, or custom state-stored data
|
|
@@ -72,25 +107,24 @@ Built on the Paperclip plugin SDK and the domain event bridge ([PR #909](https:/
|
|
|
72
107
|
- Deduplication: same watch+entity won't re-fire within a configurable window (default 24h)
|
|
73
108
|
- Scheduled job checks all watches periodically
|
|
74
109
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
2. Run `/newbot` and follow the prompts
|
|
81
|
-
3. Save the bot token
|
|
82
|
-
|
|
83
|
-
### 2. Get your chat ID
|
|
84
|
-
|
|
85
|
-
Send a message to your bot, then run:
|
|
110
|
+
### Reply routing
|
|
111
|
+
- Reply to any bot notification to route your message back to Paperclip
|
|
112
|
+
- Replies to issue notifications create issue comments automatically
|
|
113
|
+
- Replies to escalation notifications resolve the escalation as a human reply
|
|
114
|
+
- Enable/disable with `enableInbound` config toggle (default: true)
|
|
86
115
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
116
|
+
### Daily digest
|
|
117
|
+
- Configurable digest summaries posted to your Telegram chats
|
|
118
|
+
- Modes: `daily` (once), `bidaily` (twice), `tridaily` (three times per day)
|
|
119
|
+
- Configure via the `digestMode` config setting
|
|
120
|
+
- Includes: tasks completed/created, active agents, in-progress/review/blocked issues
|
|
90
121
|
|
|
91
|
-
|
|
122
|
+
### Forum topic routing
|
|
123
|
+
- Map Telegram forum topics to Paperclip projects via `/connect_topic`
|
|
124
|
+
- Notifications for a project are routed to its mapped topic
|
|
125
|
+
- Requires a group with forum topics enabled
|
|
92
126
|
|
|
93
|
-
|
|
127
|
+
## Install
|
|
94
128
|
|
|
95
129
|
```bash
|
|
96
130
|
npm install paperclip-plugin-telegram
|
|
@@ -104,69 +138,51 @@ curl -X POST http://127.0.0.1:3100/api/plugins/install \
|
|
|
104
138
|
-d '{"packageName":"paperclip-plugin-telegram"}'
|
|
105
139
|
```
|
|
106
140
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
The bot token field requires a Paperclip secret reference (a UUID), not the raw token.
|
|
110
|
-
|
|
111
|
-
1. In Paperclip, go to **Settings → Secrets → Create new secret**
|
|
112
|
-
2. Paste your Telegram bot token as the secret value and save
|
|
113
|
-
3. Copy the UUID of the created secret
|
|
114
|
-
4. Use that UUID in the `telegramBotTokenRef` field below
|
|
115
|
-
|
|
116
|
-
Do the same for `transcriptionApiKeyRef` if you want Whisper transcription.
|
|
141
|
+
## Setup
|
|
117
142
|
|
|
118
|
-
|
|
143
|
+
1. Open Telegram and chat with [@BotFather](https://t.me/BotFather)
|
|
144
|
+
2. Run `/newbot` and follow the prompts to create a bot
|
|
145
|
+
3. Save the bot token
|
|
146
|
+
4. Send a message to your bot, then run `curl "https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates"` and find the `chat.id` field
|
|
147
|
+
5. In Paperclip, go to **Settings -> Secrets -> Create new secret**, paste your bot token as the secret value, and copy the resulting UUID
|
|
148
|
+
6. Configure the plugin with the secret UUID in `telegramBotTokenRef` and your chat ID in `defaultChatId`
|
|
119
149
|
|
|
120
|
-
|
|
150
|
+
## Configuration
|
|
121
151
|
|
|
122
152
|
| Setting | Required | Description |
|
|
123
153
|
|---------|----------|-------------|
|
|
124
|
-
| `telegramBotTokenRef` | Yes | Secret UUID for your bot token
|
|
125
|
-
| `defaultChatId` |
|
|
154
|
+
| `telegramBotTokenRef` | Yes | Secret UUID for your bot token |
|
|
155
|
+
| `defaultChatId` | No | Fallback chat ID for notifications |
|
|
126
156
|
| `approvalsChatId` | No | Separate chat for approvals |
|
|
127
157
|
| `errorsChatId` | No | Separate chat for errors |
|
|
158
|
+
| `escalationChatId` | No | Dedicated chat for agent escalations |
|
|
159
|
+
| `paperclipBaseUrl` | No | Internal Paperclip API URL (default: http://localhost:3100) |
|
|
160
|
+
| `paperclipPublicUrl` | No | Public URL for issue links in messages |
|
|
128
161
|
| `enableCommands` | No | Enable bot commands (default: true) |
|
|
129
162
|
| `enableInbound` | No | Route Telegram replies to issues (default: true) |
|
|
130
|
-
| `
|
|
131
|
-
| `
|
|
132
|
-
| `
|
|
163
|
+
| `topicRouting` | No | Map forum topics to projects (default: false) |
|
|
164
|
+
| `digestMode` | No | Digest frequency: off, daily, bidaily, tridaily (default: off) |
|
|
165
|
+
| `dailyDigestTime` | No | UTC time for digest, HH:MM (default: 09:00) |
|
|
166
|
+
| `bidailySecondTime` | No | Second digest time for bidaily mode (default: 17:00) |
|
|
167
|
+
| `tridailyTimes` | No | Comma-separated HH:MM times for tridaily (default: 07:00,13:00,19:00) |
|
|
133
168
|
| `escalationTimeoutMs` | No | Timeout before default action fires (default: 900000 / 15 min) |
|
|
134
|
-
| `escalationDefaultAction` | No | Action on timeout: `defer`, `
|
|
135
|
-
| `escalationHoldMessage` | No | Message sent to customer while waiting
|
|
136
|
-
| `
|
|
137
|
-
| `
|
|
138
|
-
| `
|
|
169
|
+
| `escalationDefaultAction` | No | Action on timeout: `defer`, `auto_reply`, `close` (default: `defer`) |
|
|
170
|
+
| `escalationHoldMessage` | No | Message sent to customer while waiting |
|
|
171
|
+
| `maxAgentsPerThread` | No | Max concurrent agents per thread (default: 5) |
|
|
172
|
+
| `briefAgentId` | No | Agent ID for media intake Brief Agent |
|
|
173
|
+
| `briefAgentChatIds` | No | Chat IDs that act as media intake channels |
|
|
174
|
+
| `transcriptionApiKeyRef` | No | Secret reference to OpenAI API key for Whisper |
|
|
139
175
|
| `maxSuggestionsPerHourPerCompany` | No | Rate limit for proactive suggestions (default: 10) |
|
|
140
|
-
| `watchDeduplicationWindowMs` | No |
|
|
141
|
-
|
|
142
|
-
### 6. Add bot to group (optional)
|
|
143
|
-
|
|
144
|
-
If using a group chat:
|
|
145
|
-
1. Add the bot to your Telegram group
|
|
146
|
-
2. Disable privacy mode via BotFather (`/setprivacy` -> Disable) if you want the bot to see all messages
|
|
147
|
-
3. Run `/connect <company-name>` in the group
|
|
148
|
-
|
|
149
|
-
## How it works
|
|
150
|
-
|
|
151
|
-
### Notifications (outbound)
|
|
152
|
-
|
|
153
|
-
When Paperclip events fire (issue created, approval needed, agent error), the plugin formats them as Telegram messages with MarkdownV2 and sends to the configured chat.
|
|
154
|
-
|
|
155
|
-
Approval notifications include inline Approve/Reject buttons. Clicking a button calls the Paperclip API directly and updates the message to show the resolution.
|
|
156
|
-
|
|
157
|
-
### Commands (inbound)
|
|
176
|
+
| `watchDeduplicationWindowMs` | No | Suppress duplicate watch suggestions within this window (default: 86400000 / 24h) |
|
|
158
177
|
|
|
159
|
-
|
|
178
|
+
## Agent tools
|
|
160
179
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
### Reply routing
|
|
168
|
-
|
|
169
|
-
When a user replies to a bot notification, the plugin looks up which Paperclip entity that notification was about and posts the reply as an issue comment. This turns Telegram threads into a natural conversation interface for Paperclip issues.
|
|
180
|
+
| Tool | Phase | Description |
|
|
181
|
+
|------|-------|-------------|
|
|
182
|
+
| `escalate_to_human` | 1 | Escalate a conversation to a human when confidence is low |
|
|
183
|
+
| `handoff_to_agent` | 2 | Hand off work to another agent in this thread |
|
|
184
|
+
| `discuss_with_agent` | 2 | Start a back-and-forth conversation with another agent |
|
|
185
|
+
| `register_watch` | 5 | Register a proactive watch that monitors entities and sends suggestions |
|
|
170
186
|
|
|
171
187
|
## Comparison with PR #407
|
|
172
188
|
|
|
@@ -192,7 +208,7 @@ When a user replies to a bot notification, the plugin looks up which Paperclip e
|
|
|
192
208
|
|
|
193
209
|
The `telegramBotTokenRef` and `transcriptionApiKeyRef` fields now require a Paperclip secret reference (a UUID), not the raw token value. If you previously entered your raw bot token in the field, follow these steps to migrate:
|
|
194
210
|
|
|
195
|
-
1. Go to **Settings
|
|
211
|
+
1. Go to **Settings -> Secrets -> Create new secret**
|
|
196
212
|
2. Paste your Telegram bot token as the secret value and save
|
|
197
213
|
3. Copy the resulting UUID
|
|
198
214
|
4. Open **Plugin Settings for Telegram Bot** and paste the UUID into "Telegram Bot Token"
|
|
@@ -204,13 +220,18 @@ The plugin will fail to activate if a raw token (non-UUID) is entered in the fie
|
|
|
204
220
|
|
|
205
221
|
```bash
|
|
206
222
|
pnpm install
|
|
207
|
-
pnpm build
|
|
208
223
|
pnpm typecheck
|
|
224
|
+
pnpm test
|
|
225
|
+
pnpm build
|
|
209
226
|
```
|
|
210
227
|
|
|
211
|
-
|
|
228
|
+
~80 tests covering notifications, approvals, escalation, session registry, media pipeline, custom commands, proactive suggestions, MarkdownV2 formatting, and bot commands.
|
|
212
229
|
|
|
213
|
-
|
|
230
|
+
## Contributing
|
|
231
|
+
|
|
232
|
+
Issues and PRs welcome at [github.com/mvanhorn/paperclip-plugin-telegram](https://github.com/mvanhorn/paperclip-plugin-telegram).
|
|
233
|
+
|
|
234
|
+
Auto-publishes to npm on push to `main` via OIDC trusted publishing.
|
|
214
235
|
|
|
215
236
|
## Credits
|
|
216
237
|
|
|
@@ -219,3 +240,7 @@ MIT
|
|
|
219
240
|
[@leeknowsai](https://github.com/leeknowsai) - Worker bootstrap patterns adapted from the Discord plugin.
|
|
220
241
|
|
|
221
242
|
Inspired by [OpenClaw's Telegram integration](https://github.com/openclaw/openclaw) (grammY, bidirectional messaging, inline buttons) and adapted for the Paperclip plugin SDK.
|
|
243
|
+
|
|
244
|
+
## License
|
|
245
|
+
|
|
246
|
+
MIT
|
package/dist/acp-bridge.js
CHANGED
|
@@ -35,6 +35,24 @@ export async function handleAcpCommand(ctx, token, chatId, args, messageThreadId
|
|
|
35
35
|
].join("\n"), { parseMode: "MarkdownV2", messageThreadId });
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
+
// --- Agent name resolution ---
|
|
39
|
+
/**
|
|
40
|
+
* Resolve an agent by name/urlKey (case-insensitive).
|
|
41
|
+
* The plugin SDK's `agents.get()` requires a UUID, so we list all agents
|
|
42
|
+
* and match by name or urlKey.
|
|
43
|
+
*/
|
|
44
|
+
async function resolveAgentByName(ctx, name, companyId) {
|
|
45
|
+
try {
|
|
46
|
+
const allAgents = await ctx.agents.list({ companyId });
|
|
47
|
+
const lower = name.toLowerCase();
|
|
48
|
+
const match = allAgents.find((a) => a.name?.toLowerCase() === lower ||
|
|
49
|
+
a.urlKey?.toLowerCase() === lower);
|
|
50
|
+
return match ? { id: match.id, name: match.name } : null;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
38
56
|
// --- Spawn (multi-agent aware, native-first) ---
|
|
39
57
|
async function handleAcpSpawn(ctx, token, chatId, agentName, messageThreadId, companyId) {
|
|
40
58
|
if (!agentName.trim()) {
|
|
@@ -58,15 +76,14 @@ async function handleAcpSpawn(ctx, token, chatId, agentName, messageThreadId, co
|
|
|
58
76
|
const trimmedName = agentName.trim();
|
|
59
77
|
const displayName = trimmedName.charAt(0).toUpperCase() + trimmedName.slice(1);
|
|
60
78
|
const resolvedCompanyId = companyId ?? await resolveCompanyIdFromChat(ctx, chatId);
|
|
61
|
-
// Try native session first:
|
|
79
|
+
// Try native session first: resolve agent by name, then create session
|
|
62
80
|
let transport = "acp";
|
|
63
81
|
let sessionId;
|
|
64
82
|
let agentId = "";
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
agentId = agent.id;
|
|
83
|
+
const resolved = await resolveAgentByName(ctx, trimmedName, resolvedCompanyId);
|
|
84
|
+
if (resolved) {
|
|
85
|
+
try {
|
|
86
|
+
agentId = resolved.id;
|
|
70
87
|
const session = await ctx.agents.sessions.create(agentId, resolvedCompanyId, {
|
|
71
88
|
reason: `Telegram thread ${chatId}/${messageThreadId}`,
|
|
72
89
|
});
|
|
@@ -74,12 +91,11 @@ async function handleAcpSpawn(ctx, token, chatId, agentName, messageThreadId, co
|
|
|
74
91
|
transport = "native";
|
|
75
92
|
ctx.logger.info("Created native agent session", { agentId, sessionId });
|
|
76
93
|
}
|
|
77
|
-
|
|
94
|
+
catch {
|
|
78
95
|
sessionId = `acp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
79
96
|
}
|
|
80
97
|
}
|
|
81
|
-
|
|
82
|
-
// Agent not found in Paperclip - fall back to ACP
|
|
98
|
+
else {
|
|
83
99
|
sessionId = `acp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
84
100
|
}
|
|
85
101
|
const now = new Date().toISOString();
|
|
@@ -297,26 +313,56 @@ export async function routeMessageToAgent(ctx, token, chatId, threadId, text, re
|
|
|
297
313
|
await ctx.agents.sessions.sendMessage(targetSession.sessionId, resolvedCompanyId, {
|
|
298
314
|
prompt: text,
|
|
299
315
|
reason: "telegram_message",
|
|
300
|
-
onEvent: (
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
316
|
+
onEvent: (() => {
|
|
317
|
+
// Buffer assistant text and send only the final response
|
|
318
|
+
const assistantTextBuffer = [];
|
|
319
|
+
return (event) => {
|
|
320
|
+
if (event.eventType === "chunk" && event.message) {
|
|
321
|
+
const msg = event.message;
|
|
322
|
+
if (msg.startsWith("{")) {
|
|
323
|
+
try {
|
|
324
|
+
const parsed = JSON.parse(msg);
|
|
325
|
+
// Collect only assistant text content
|
|
326
|
+
if (parsed.type === "assistant" && parsed.message?.content) {
|
|
327
|
+
const textParts = parsed.message.content
|
|
328
|
+
.filter((c) => c.type === "text" && c.text)
|
|
329
|
+
.map((c) => c.text);
|
|
330
|
+
if (textParts.length > 0) {
|
|
331
|
+
assistantTextBuffer.push(textParts.join("\n"));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
// Not JSON — ignore non-structured output
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// Drop all non-JSON chunks (system messages like "run started", "adapter invocation", etc.)
|
|
340
|
+
}
|
|
341
|
+
else if (event.eventType === "done") {
|
|
342
|
+
const finalText = assistantTextBuffer.length > 0
|
|
343
|
+
? assistantTextBuffer.join("\n\n")
|
|
344
|
+
: "";
|
|
345
|
+
if (finalText) {
|
|
346
|
+
handleAcpOutput(ctx, token, {
|
|
347
|
+
sessionId: targetSession.sessionId,
|
|
348
|
+
chatId,
|
|
349
|
+
threadId,
|
|
350
|
+
text: finalText,
|
|
351
|
+
done: true,
|
|
352
|
+
}).catch((err) => ctx.logger.error("Native output handler error", { error: String(err) }));
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
handleAcpOutput(ctx, token, {
|
|
356
|
+
sessionId: targetSession.sessionId,
|
|
357
|
+
chatId,
|
|
358
|
+
threadId,
|
|
359
|
+
text: event.message ?? "Run completed",
|
|
360
|
+
done: true,
|
|
361
|
+
}).catch((err) => ctx.logger.error("Native output handler error", { error: String(err) }));
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
})(),
|
|
320
366
|
});
|
|
321
367
|
}
|
|
322
368
|
catch (err) {
|
|
@@ -432,19 +478,62 @@ async function flushOutputQueue(ctx, token, chatId, threadId) {
|
|
|
432
478
|
}
|
|
433
479
|
}
|
|
434
480
|
}
|
|
481
|
+
// --- Markdown to Telegram HTML ---
|
|
482
|
+
function escapeHtml(text) {
|
|
483
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
484
|
+
}
|
|
485
|
+
function markdownToTelegramHtml(text) {
|
|
486
|
+
let html = escapeHtml(text);
|
|
487
|
+
// Bold: **text** → <b>text</b>
|
|
488
|
+
html = html.replace(/\*\*(.+?)\*\*/g, "<b>$1</b>");
|
|
489
|
+
// Italic: _text_ (but not in the middle of words)
|
|
490
|
+
html = html.replace(/(?<!\w)_(.+?)_(?!\w)/g, "<i>$1</i>");
|
|
491
|
+
// Inline code: `text` → <code>text</code>
|
|
492
|
+
html = html.replace(/`([^`]+)`/g, "<code>$1</code>");
|
|
493
|
+
// Code blocks: ```text``` → <pre>text</pre>
|
|
494
|
+
html = html.replace(/```(?:\w*\n)?([\s\S]*?)```/g, "<pre>$1</pre>");
|
|
495
|
+
return html;
|
|
496
|
+
}
|
|
435
497
|
// --- Send labeled output ---
|
|
498
|
+
const TELEGRAM_MAX_LENGTH = 4000; // Leave room for prefix/label overhead
|
|
436
499
|
async function sendLabeledOutput(ctx, token, chatId, threadId, sessionId, displayName, text, done) {
|
|
437
500
|
const prefix = done
|
|
438
501
|
? escapeMarkdownV2("\u2705")
|
|
439
502
|
: escapeMarkdownV2("\ud83e\udd16");
|
|
440
503
|
const label = `*\\[${escapeMarkdownV2(displayName)}\\]*`;
|
|
441
|
-
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
|
|
504
|
+
// Split long text into chunks to stay within Telegram's 4096 char limit
|
|
505
|
+
const chunks = [];
|
|
506
|
+
if (text.length <= TELEGRAM_MAX_LENGTH) {
|
|
507
|
+
chunks.push(text);
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
let remaining = text;
|
|
511
|
+
while (remaining.length > 0) {
|
|
512
|
+
if (remaining.length <= TELEGRAM_MAX_LENGTH) {
|
|
513
|
+
chunks.push(remaining);
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
// Try to split at a newline boundary
|
|
517
|
+
let splitAt = remaining.lastIndexOf("\n", TELEGRAM_MAX_LENGTH);
|
|
518
|
+
if (splitAt <= 0)
|
|
519
|
+
splitAt = TELEGRAM_MAX_LENGTH;
|
|
520
|
+
chunks.push(remaining.slice(0, splitAt));
|
|
521
|
+
remaining = remaining.slice(splitAt).replace(/^\n/, "");
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
525
|
+
const isLast = i === chunks.length - 1;
|
|
526
|
+
// Convert agent Markdown to Telegram HTML for proper rendering
|
|
527
|
+
const doneEmoji = done ? "\u2705" : "\ud83e\udd16";
|
|
528
|
+
const chunkPrefix = `${doneEmoji} <b>[${escapeHtml(displayName)}]</b> `;
|
|
529
|
+
const formatted = `${chunkPrefix}${markdownToTelegramHtml(chunks[i])}`;
|
|
530
|
+
const messageId = await sendMessage(ctx, token, chatId, formatted, {
|
|
531
|
+
parseMode: "HTML",
|
|
532
|
+
messageThreadId: threadId,
|
|
533
|
+
});
|
|
534
|
+
if (messageId && isLast) {
|
|
535
|
+
await ctx.state.set({ scopeKind: "instance", stateKey: `agent_msg_${chatId}_${messageId}` }, { sessionId });
|
|
536
|
+
}
|
|
448
537
|
}
|
|
449
538
|
}
|
|
450
539
|
// --- Handoff tool handler ---
|
|
@@ -532,21 +621,21 @@ async function executeHandoff(ctx, token, chatId, threadId, targetAgent, context
|
|
|
532
621
|
let transport = "acp";
|
|
533
622
|
let sessionId;
|
|
534
623
|
let agentId = "";
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
agentId =
|
|
624
|
+
const resolved = await resolveAgentByName(ctx, targetAgent, companyId);
|
|
625
|
+
if (resolved) {
|
|
626
|
+
try {
|
|
627
|
+
agentId = resolved.id;
|
|
539
628
|
const session = await ctx.agents.sessions.create(agentId, companyId, {
|
|
540
629
|
reason: `Handoff from Telegram thread ${chatId}/${threadId}`,
|
|
541
630
|
});
|
|
542
631
|
sessionId = session.sessionId;
|
|
543
632
|
transport = "native";
|
|
544
633
|
}
|
|
545
|
-
|
|
634
|
+
catch {
|
|
546
635
|
sessionId = `acp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
547
636
|
}
|
|
548
637
|
}
|
|
549
|
-
|
|
638
|
+
else {
|
|
550
639
|
sessionId = `acp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
551
640
|
}
|
|
552
641
|
const displayName = targetAgent.charAt(0).toUpperCase() + targetAgent.slice(1);
|
|
@@ -618,21 +707,21 @@ export async function handleDiscussToolCall(ctx, token, params, companyId, sourc
|
|
|
618
707
|
let transport = "acp";
|
|
619
708
|
let sessionId;
|
|
620
709
|
let agentId = "";
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
agentId =
|
|
710
|
+
const resolved = await resolveAgentByName(ctx, targetAgent, companyId);
|
|
711
|
+
if (resolved) {
|
|
712
|
+
try {
|
|
713
|
+
agentId = resolved.id;
|
|
625
714
|
const session = await ctx.agents.sessions.create(agentId, companyId, {
|
|
626
715
|
reason: `Discussion from Telegram thread ${chatId}/${threadId}`,
|
|
627
716
|
});
|
|
628
717
|
sessionId = session.sessionId;
|
|
629
718
|
transport = "native";
|
|
630
719
|
}
|
|
631
|
-
|
|
720
|
+
catch {
|
|
632
721
|
sessionId = `acp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
633
722
|
}
|
|
634
723
|
}
|
|
635
|
-
|
|
724
|
+
else {
|
|
636
725
|
sessionId = `acp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
637
726
|
}
|
|
638
727
|
const displayName = targetAgent.charAt(0).toUpperCase() + targetAgent.slice(1);
|
|
@@ -791,7 +880,7 @@ async function resolveCompanyIdFromChat(ctx, chatId) {
|
|
|
791
880
|
scopeKind: "instance",
|
|
792
881
|
stateKey: `chat_${chatId}`,
|
|
793
882
|
});
|
|
794
|
-
return mapping?.companyName ?? chatId;
|
|
883
|
+
return mapping?.companyId ?? mapping?.companyName ?? chatId;
|
|
795
884
|
}
|
|
796
885
|
function simpleHash(text) {
|
|
797
886
|
let hash = 0;
|