paperclip-plugin-telegram 0.2.0 → 0.2.2

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.
Files changed (62) hide show
  1. package/README.md +68 -4
  2. package/dist/acp-bridge.d.ts +34 -0
  3. package/dist/acp-bridge.js +805 -0
  4. package/dist/acp-bridge.js.map +1 -0
  5. package/dist/adapter.d.ts +35 -0
  6. package/dist/adapter.js +75 -0
  7. package/dist/adapter.js.map +1 -0
  8. package/dist/command-registry.d.ts +3 -0
  9. package/dist/command-registry.js +273 -0
  10. package/dist/command-registry.js.map +1 -0
  11. package/dist/commands.d.ts +10 -0
  12. package/dist/commands.js +213 -0
  13. package/dist/commands.js.map +1 -0
  14. package/dist/constants.d.ts +44 -0
  15. package/dist/constants.js +48 -0
  16. package/dist/constants.js.map +1 -0
  17. package/dist/escalation.d.ts +41 -0
  18. package/dist/escalation.js +254 -0
  19. package/dist/escalation.js.map +1 -0
  20. package/dist/formatters.d.ts +13 -0
  21. package/dist/formatters.js +130 -0
  22. package/dist/formatters.js.map +1 -0
  23. package/dist/index.js +4 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/manifest.d.ts +3 -0
  26. package/dist/manifest.js +230 -0
  27. package/dist/manifest.js.map +1 -0
  28. package/dist/media-pipeline.d.ts +46 -0
  29. package/dist/media-pipeline.js +161 -0
  30. package/dist/media-pipeline.js.map +1 -0
  31. package/dist/telegram-api.d.ts +28 -0
  32. package/dist/telegram-api.js +147 -0
  33. package/dist/telegram-api.js.map +1 -0
  34. package/dist/watch-registry.d.ts +9 -0
  35. package/dist/watch-registry.js +272 -0
  36. package/dist/watch-registry.js.map +1 -0
  37. package/dist/worker.d.ts +1 -0
  38. package/dist/worker.js +548 -0
  39. package/dist/worker.js.map +1 -0
  40. package/package.json +7 -3
  41. package/src/acp-bridge.ts +0 -1273
  42. package/src/adapter.ts +0 -129
  43. package/src/command-registry.ts +0 -482
  44. package/src/commands.ts +0 -346
  45. package/src/constants.ts +0 -51
  46. package/src/escalation.ts +0 -421
  47. package/src/formatters.ts +0 -148
  48. package/src/manifest.ts +0 -246
  49. package/src/media-pipeline.ts +0 -234
  50. package/src/telegram-api.ts +0 -202
  51. package/src/watch-registry.ts +0 -369
  52. package/src/worker.ts +0 -783
  53. package/tests/acp-bridge.test.ts +0 -314
  54. package/tests/command-registry.test.ts +0 -283
  55. package/tests/commands.test.ts +0 -213
  56. package/tests/escalation.test.ts +0 -550
  57. package/tests/formatters.test.ts +0 -185
  58. package/tests/media-pipeline.test.ts +0 -324
  59. package/tests/telegram-api.test.ts +0 -108
  60. package/tests/watch-registry.test.ts +0 -404
  61. package/tsconfig.json +0 -16
  62. /package/{src/index.ts → dist/index.d.ts} +0 -0
package/src/escalation.ts DELETED
@@ -1,421 +0,0 @@
1
- import type { PluginContext } from "@paperclipai/plugin-sdk";
2
- import { sendMessage, editMessage, escapeMarkdownV2, truncateAtWord } from "./telegram-api.js";
3
-
4
- export type EscalationReason =
5
- | "low_confidence"
6
- | "explicit_request"
7
- | "policy_violation"
8
- | "unknown_intent";
9
-
10
- export type EscalationEvent = {
11
- escalationId: string;
12
- agentId: string;
13
- companyId: string;
14
- reason: EscalationReason;
15
- context: {
16
- conversationHistory: Array<{ role: string; text: string }>;
17
- agentReasoning: string;
18
- suggestedActions: string[];
19
- suggestedReply?: string;
20
- confidenceScore?: number;
21
- };
22
- timeout: {
23
- durationMs: number;
24
- defaultAction: "defer" | "auto_reply" | "close";
25
- };
26
- originChatId?: string;
27
- originThreadId?: string;
28
- originMessageId?: string;
29
- // Transport info for routing replies back
30
- transport?: "native" | "acp";
31
- sessionId?: string;
32
- };
33
-
34
- export type EscalationResponse = {
35
- escalationId: string;
36
- responderId: string;
37
- responseText: string;
38
- action: "reply_to_customer" | "override_suggested" | "dismiss";
39
- };
40
-
41
- type StoredEscalation = {
42
- escalationId: string;
43
- agentId: string;
44
- companyId: string;
45
- reason: EscalationReason;
46
- agentReasoning: string;
47
- suggestedReply?: string;
48
- suggestedActions: string[];
49
- confidenceScore?: number;
50
- originChatId?: string;
51
- originThreadId?: string;
52
- originMessageId?: string;
53
- escalationChatId: string;
54
- escalationMessageId: string;
55
- status: "pending" | "resolved" | "timed_out";
56
- createdAt: string;
57
- timeoutAt: string;
58
- defaultAction: "defer" | "auto_reply" | "close";
59
- transport?: "native" | "acp";
60
- sessionId?: string;
61
- };
62
-
63
- const REASON_LABELS: Record<EscalationReason, string> = {
64
- low_confidence: "Low Confidence",
65
- explicit_request: "User Requested Human",
66
- policy_violation: "Policy Violation",
67
- unknown_intent: "Unknown Intent",
68
- };
69
-
70
- function esc(s: string): string {
71
- return escapeMarkdownV2(s);
72
- }
73
-
74
- export class EscalationManager {
75
- async create(
76
- ctx: PluginContext,
77
- token: string,
78
- event: EscalationEvent,
79
- escalationChatId: string,
80
- ): Promise<void> {
81
- const reasonLabel = REASON_LABELS[event.reason] ?? event.reason;
82
- const confidence = event.context.confidenceScore != null
83
- ? ` \\(${esc(String(Math.round(event.context.confidenceScore * 100)))}%\\)`
84
- : "";
85
-
86
- const lines: string[] = [
87
- `${esc("\u26a0\ufe0f")} *Escalation* \\- ${esc(reasonLabel)}${confidence}`,
88
- "",
89
- `*Agent:* ${esc(event.agentId)}`,
90
- `*Reason:* ${esc(event.context.agentReasoning ? truncateAtWord(event.context.agentReasoning, 500) : "No details provided")}`,
91
- ];
92
-
93
- if (event.context.suggestedActions.length > 0) {
94
- lines.push("");
95
- lines.push("*Suggested actions:*");
96
- for (const action of event.context.suggestedActions.slice(0, 5)) {
97
- lines.push(` ${esc("-")} ${esc(action)}`);
98
- }
99
- }
100
-
101
- if (event.context.suggestedReply) {
102
- lines.push("");
103
- lines.push("*Suggested reply:*");
104
- lines.push(`${esc(">")} ${esc(truncateAtWord(event.context.suggestedReply, 300))}`);
105
- }
106
-
107
- lines.push("");
108
- lines.push(`ID: \`${esc(event.escalationId)}\``);
109
-
110
- const buttons = [];
111
- if (event.context.suggestedReply) {
112
- buttons.push([
113
- { text: "Send Suggested Reply", callback_data: `esc_suggested_${event.escalationId}` },
114
- ]);
115
- }
116
- buttons.push([
117
- { text: "Reply", callback_data: `esc_reply_${event.escalationId}` },
118
- { text: "Override", callback_data: `esc_override_${event.escalationId}` },
119
- { text: "Dismiss", callback_data: `esc_dismiss_${event.escalationId}` },
120
- ]);
121
-
122
- const messageId = await sendMessage(ctx, token, escalationChatId, lines.join("\n"), {
123
- parseMode: "MarkdownV2",
124
- inlineKeyboard: buttons,
125
- });
126
-
127
- if (!messageId) {
128
- ctx.logger.error("Failed to send escalation message", { escalationId: event.escalationId });
129
- return;
130
- }
131
-
132
- const timeoutAt = new Date(Date.now() + event.timeout.durationMs).toISOString();
133
-
134
- const stored: StoredEscalation = {
135
- escalationId: event.escalationId,
136
- agentId: event.agentId,
137
- companyId: event.companyId,
138
- reason: event.reason,
139
- agentReasoning: event.context.agentReasoning,
140
- suggestedReply: event.context.suggestedReply,
141
- suggestedActions: event.context.suggestedActions,
142
- confidenceScore: event.context.confidenceScore,
143
- originChatId: event.originChatId,
144
- originThreadId: event.originThreadId,
145
- originMessageId: event.originMessageId,
146
- escalationChatId,
147
- escalationMessageId: String(messageId),
148
- status: "pending",
149
- createdAt: new Date().toISOString(),
150
- timeoutAt,
151
- defaultAction: event.timeout.defaultAction,
152
- transport: event.transport,
153
- sessionId: event.sessionId,
154
- };
155
-
156
- await ctx.state.set(
157
- { scopeKind: "instance", stateKey: `escalation_${event.escalationId}` },
158
- stored,
159
- );
160
-
161
- // Map the escalation message back so replies can be routed
162
- await ctx.state.set(
163
- { scopeKind: "instance", stateKey: `msg_${escalationChatId}_${messageId}` },
164
- {
165
- entityId: event.escalationId,
166
- entityType: "escalation",
167
- companyId: event.companyId,
168
- eventType: "escalation.created",
169
- },
170
- );
171
-
172
- // Track pending escalation IDs for timeout checks
173
- const pendingIds = (await ctx.state.get({
174
- scopeKind: "instance",
175
- stateKey: "escalation_pending_ids",
176
- }) as string[] | null) ?? [];
177
- pendingIds.push(event.escalationId);
178
- await ctx.state.set(
179
- { scopeKind: "instance", stateKey: "escalation_pending_ids" },
180
- pendingIds,
181
- );
182
-
183
- ctx.logger.info("Escalation created", {
184
- escalationId: event.escalationId,
185
- reason: event.reason,
186
- timeoutAt,
187
- });
188
- }
189
-
190
- async handleCallback(
191
- ctx: PluginContext,
192
- token: string,
193
- action: string,
194
- escalationId: string,
195
- actor: string,
196
- callbackQueryId: string,
197
- chatId: string | null,
198
- messageId: number | undefined,
199
- ): Promise<void> {
200
- const stored = await ctx.state.get({
201
- scopeKind: "instance",
202
- stateKey: `escalation_${escalationId}`,
203
- }) as StoredEscalation | null;
204
-
205
- if (!stored || stored.status !== "pending") {
206
- return;
207
- }
208
-
209
- switch (action) {
210
- case "suggested": {
211
- if (!stored.suggestedReply) break;
212
- await this.resolve(ctx, token, stored, {
213
- escalationId,
214
- responderId: `telegram:${actor}`,
215
- responseText: stored.suggestedReply,
216
- action: "reply_to_customer",
217
- });
218
- break;
219
- }
220
- case "reply": {
221
- if (chatId && messageId) {
222
- await editMessage(
223
- ctx,
224
- token,
225
- chatId,
226
- messageId,
227
- `${esc("\u26a0\ufe0f")} *Escalation* \\- *Awaiting your reply*\n\n${esc("Reply to this message with your response to the customer.")}`,
228
- { parseMode: "MarkdownV2" },
229
- );
230
- }
231
- break;
232
- }
233
- case "dismiss": {
234
- await this.resolve(ctx, token, stored, {
235
- escalationId,
236
- responderId: `telegram:${actor}`,
237
- responseText: "",
238
- action: "dismiss",
239
- });
240
- break;
241
- }
242
- case "override": {
243
- if (chatId && messageId) {
244
- await editMessage(
245
- ctx,
246
- token,
247
- chatId,
248
- messageId,
249
- `${esc("\u26a0\ufe0f")} *Escalation* \\- *Override mode*\n\n${esc("Reply to this message with your custom response.")}`,
250
- { parseMode: "MarkdownV2" },
251
- );
252
- }
253
- break;
254
- }
255
- }
256
- }
257
-
258
- async respond(
259
- ctx: PluginContext,
260
- token: string,
261
- escalationId: string,
262
- response: EscalationResponse,
263
- ): Promise<void> {
264
- const stored = await ctx.state.get({
265
- scopeKind: "instance",
266
- stateKey: `escalation_${escalationId}`,
267
- }) as StoredEscalation | null;
268
-
269
- if (!stored || stored.status !== "pending") {
270
- ctx.logger.warn("Escalation respond called for non-pending escalation", { escalationId });
271
- return;
272
- }
273
-
274
- await this.resolve(ctx, token, stored, response);
275
- }
276
-
277
- private async resolve(
278
- ctx: PluginContext,
279
- token: string,
280
- stored: StoredEscalation,
281
- response: EscalationResponse,
282
- ): Promise<void> {
283
- stored.status = "resolved";
284
- await ctx.state.set(
285
- { scopeKind: "instance", stateKey: `escalation_${stored.escalationId}` },
286
- stored,
287
- );
288
-
289
- await this.removePending(ctx, stored.escalationId);
290
-
291
- const statusLabel = response.action === "dismiss" ? "Dismissed" : "Resolved";
292
- await editMessage(
293
- ctx,
294
- token,
295
- stored.escalationChatId,
296
- Number(stored.escalationMessageId),
297
- `${esc("\u2705")} *Escalation ${statusLabel}* by ${esc(response.responderId)}\n\nID: \`${esc(stored.escalationId)}\``,
298
- { parseMode: "MarkdownV2" },
299
- );
300
-
301
- // Route reply back via the correct transport
302
- if (response.action === "reply_to_customer" && response.responseText) {
303
- if (stored.transport === "native" && stored.sessionId) {
304
- // Route back through native agent session
305
- try {
306
- await ctx.agents.sessions.sendMessage(stored.sessionId, stored.companyId, {
307
- prompt: `[Human escalation response] ${response.responseText}`,
308
- reason: "escalation_reply",
309
- });
310
- } catch (err) {
311
- ctx.logger.error("Failed to route escalation reply to native session", { error: String(err) });
312
- }
313
- } else if (stored.transport === "acp" && stored.sessionId) {
314
- // Route back via ACP event
315
- ctx.events.emit("acp-spawn", stored.companyId, {
316
- type: "message",
317
- sessionId: stored.sessionId,
318
- text: `[Human escalation response] ${response.responseText}`,
319
- });
320
- }
321
-
322
- // Also send to the originating Telegram chat if available
323
- if (stored.originChatId) {
324
- await sendMessage(ctx, token, stored.originChatId, esc(response.responseText), {
325
- parseMode: "MarkdownV2",
326
- messageThreadId: stored.originThreadId ? Number(stored.originThreadId) : undefined,
327
- replyToMessageId: stored.originMessageId ? Number(stored.originMessageId) : undefined,
328
- });
329
- }
330
- }
331
-
332
- // Emit resolution event - companyId is SECOND arg
333
- ctx.events.emit("escalation.resolved", stored.companyId, {
334
- escalationId: stored.escalationId,
335
- agentId: stored.agentId,
336
- responderId: response.responderId,
337
- responseText: response.responseText,
338
- action: response.action,
339
- });
340
-
341
- ctx.logger.info("Escalation resolved", {
342
- escalationId: stored.escalationId,
343
- action: response.action,
344
- responderId: response.responderId,
345
- });
346
- }
347
-
348
- async checkTimeouts(ctx: PluginContext, token: string): Promise<void> {
349
- const pendingIds = (await ctx.state.get({
350
- scopeKind: "instance",
351
- stateKey: "escalation_pending_ids",
352
- }) as string[] | null) ?? [];
353
-
354
- if (pendingIds.length === 0) return;
355
-
356
- const now = Date.now();
357
-
358
- for (const escalationId of pendingIds) {
359
- const stored = await ctx.state.get({
360
- scopeKind: "instance",
361
- stateKey: `escalation_${escalationId}`,
362
- }) as StoredEscalation | null;
363
-
364
- if (!stored || stored.status !== "pending") {
365
- await this.removePending(ctx, escalationId);
366
- continue;
367
- }
368
-
369
- const timeoutAt = new Date(stored.timeoutAt).getTime();
370
- if (now < timeoutAt) continue;
371
-
372
- ctx.logger.info("Escalation timed out", { escalationId, defaultAction: stored.defaultAction });
373
-
374
- stored.status = "timed_out";
375
- await ctx.state.set(
376
- { scopeKind: "instance", stateKey: `escalation_${escalationId}` },
377
- stored,
378
- );
379
-
380
- await this.removePending(ctx, escalationId);
381
-
382
- await editMessage(
383
- ctx,
384
- token,
385
- stored.escalationChatId,
386
- Number(stored.escalationMessageId),
387
- `${esc("\u23f0")} *Escalation Timed Out*\n\nDefault action: ${esc(stored.defaultAction)}\nID: \`${esc(escalationId)}\``,
388
- { parseMode: "MarkdownV2" },
389
- );
390
-
391
- // Emit timeout event - companyId is SECOND arg
392
- ctx.events.emit("escalation.timed_out", stored.companyId, {
393
- escalationId,
394
- agentId: stored.agentId,
395
- defaultAction: stored.defaultAction,
396
- suggestedReply: stored.suggestedReply,
397
- });
398
-
399
- if (stored.defaultAction === "auto_reply" && stored.suggestedReply && stored.originChatId) {
400
- await sendMessage(ctx, token, stored.originChatId, esc(stored.suggestedReply), {
401
- parseMode: "MarkdownV2",
402
- messageThreadId: stored.originThreadId ? Number(stored.originThreadId) : undefined,
403
- replyToMessageId: stored.originMessageId ? Number(stored.originMessageId) : undefined,
404
- });
405
- }
406
- }
407
- }
408
-
409
- private async removePending(ctx: PluginContext, escalationId: string): Promise<void> {
410
- const pendingIds = (await ctx.state.get({
411
- scopeKind: "instance",
412
- stateKey: "escalation_pending_ids",
413
- }) as string[] | null) ?? [];
414
-
415
- const updated = pendingIds.filter((id) => id !== escalationId);
416
- await ctx.state.set(
417
- { scopeKind: "instance", stateKey: "escalation_pending_ids" },
418
- updated,
419
- );
420
- }
421
- }
package/src/formatters.ts DELETED
@@ -1,148 +0,0 @@
1
- import type { PluginEvent } from "@paperclipai/plugin-sdk";
2
- import { escapeMarkdownV2, truncateAtWord } from "./telegram-api.js";
3
- import type { SendMessageOptions } from "./telegram-api.js";
4
-
5
- type Payload = Record<string, unknown>;
6
-
7
- type FormattedMessage = {
8
- text: string;
9
- options: SendMessageOptions;
10
- };
11
-
12
- function esc(s: string): string {
13
- return escapeMarkdownV2(s);
14
- }
15
-
16
- function bold(s: string): string {
17
- return `*${esc(s)}*`;
18
- }
19
-
20
- function code(s: string): string {
21
- return `\`${esc(s)}\``;
22
- }
23
-
24
- export function formatIssueCreated(event: PluginEvent): FormattedMessage {
25
- const p = event.payload as Payload;
26
- const identifier = String(p.identifier ?? event.entityId);
27
- const title = String(p.title ?? "Untitled");
28
- const status = p.status ? String(p.status) : null;
29
- const priority = p.priority ? String(p.priority) : null;
30
- const assigneeName = p.assigneeName ? String(p.assigneeName) : null;
31
- const projectName = p.projectName ? String(p.projectName) : null;
32
-
33
- const lines: string[] = [
34
- `${esc("📋")} ${bold("Issue Created")}: ${bold(identifier)}`,
35
- bold(title),
36
- ];
37
-
38
- const meta: string[] = [];
39
- if (status) meta.push(`Status: ${code(status)}`);
40
- if (priority) meta.push(`Priority: ${code(priority)}`);
41
- if (assigneeName) meta.push(`Assignee: ${esc(assigneeName)}`);
42
- if (projectName) meta.push(`Project: ${esc(projectName)}`);
43
- if (meta.length > 0) lines.push(meta.join(" \\| "));
44
-
45
- if (p.description) {
46
- const desc = truncateAtWord(String(p.description), 200);
47
- lines.push(`\n${esc(">")} ${esc(desc)}`);
48
- }
49
-
50
- return {
51
- text: lines.join("\n"),
52
- options: { parseMode: "MarkdownV2" },
53
- };
54
- }
55
-
56
- export function formatIssueDone(event: PluginEvent): FormattedMessage {
57
- const p = event.payload as Payload;
58
- const identifier = String(p.identifier ?? event.entityId);
59
- const title = String(p.title ?? "");
60
-
61
- return {
62
- text: [
63
- `${esc("✅")} ${bold("Issue Completed")}: ${bold(identifier)}`,
64
- `${bold(title)} ${esc("is now done.")}`,
65
- ].join("\n"),
66
- options: { parseMode: "MarkdownV2" },
67
- };
68
- }
69
-
70
- export function formatApprovalCreated(event: PluginEvent): FormattedMessage {
71
- const p = event.payload as Payload;
72
- const approvalType = String(p.type ?? "unknown");
73
- const approvalId = String(p.approvalId ?? event.entityId);
74
- const title = String(p.title ?? "Approval Requested");
75
- const description = p.description ? String(p.description) : null;
76
- const agentName = p.agentName ? String(p.agentName) : null;
77
-
78
- const lines: string[] = [
79
- `${esc("🔔")} ${bold("Approval Requested")}`,
80
- bold(title),
81
- ];
82
-
83
- if (agentName) lines.push(`Agent: ${esc(agentName)} \\| Type: ${code(approvalType)}`);
84
- if (description) lines.push(`\n${esc(truncateAtWord(description, 300))}`);
85
-
86
- // Add linked issues if present
87
- const linkedIssues = Array.isArray(p.linkedIssues) ? p.linkedIssues as Array<Payload> : [];
88
- if (linkedIssues.length > 0) {
89
- lines.push(`\n${bold(`Linked Issues (${String(linkedIssues.length)})`)}`);
90
- for (const issue of linkedIssues.slice(0, 5)) {
91
- const issueParts = [`${bold(String(issue.identifier ?? "?"))} ${esc(String(issue.title ?? ""))}`];
92
- const issueMeta: string[] = [];
93
- if (issue.status) issueMeta.push(String(issue.status));
94
- if (issue.priority) issueMeta.push(String(issue.priority));
95
- if (issue.assignee) issueMeta.push(`-> ${String(issue.assignee)}`);
96
- if (issueMeta.length > 0) issueParts.push(`\\(${esc(issueMeta.join(" | "))}\\)`);
97
- lines.push(issueParts.join(" "));
98
- }
99
- }
100
-
101
- return {
102
- text: lines.join("\n"),
103
- options: {
104
- parseMode: "MarkdownV2",
105
- inlineKeyboard: [
106
- [
107
- { text: "Approve", callback_data: `approve_${approvalId}` },
108
- { text: "Reject", callback_data: `reject_${approvalId}` },
109
- ],
110
- ],
111
- },
112
- };
113
- }
114
-
115
- export function formatAgentError(event: PluginEvent): FormattedMessage {
116
- const p = event.payload as Payload;
117
- const agentName = String(p.agentName ?? p.name ?? event.entityId);
118
- const errorMessage = String(p.error ?? p.message ?? "Unknown error");
119
-
120
- return {
121
- text: [
122
- `${esc("❌")} ${bold("Agent Error")}`,
123
- `${bold(agentName)} ${esc("encountered an error")}`,
124
- `\n${code(truncateAtWord(errorMessage, 500))}`,
125
- ].join("\n"),
126
- options: { parseMode: "MarkdownV2" },
127
- };
128
- }
129
-
130
- export function formatAgentRunStarted(event: PluginEvent): FormattedMessage {
131
- const p = event.payload as Payload;
132
- const agentName = String(p.agentName ?? event.entityId);
133
-
134
- return {
135
- text: `${esc("â–ļī¸")} ${bold(agentName)} ${esc("started a new run")}`,
136
- options: { parseMode: "MarkdownV2", disableNotification: true },
137
- };
138
- }
139
-
140
- export function formatAgentRunFinished(event: PluginEvent): FormattedMessage {
141
- const p = event.payload as Payload;
142
- const agentName = String(p.agentName ?? event.entityId);
143
-
144
- return {
145
- text: `${esc("âšī¸")} ${bold(agentName)} ${esc("completed successfully")}`,
146
- options: { parseMode: "MarkdownV2", disableNotification: true },
147
- };
148
- }