@odience-network/paperclip-plugin-telegram-enhanced 0.2.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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +215 -0
  3. package/dist/acp-bridge.d.ts +35 -0
  4. package/dist/acp-bridge.js +891 -0
  5. package/dist/acp-bridge.js.map +1 -0
  6. package/dist/adapter.d.ts +35 -0
  7. package/dist/adapter.js +75 -0
  8. package/dist/adapter.js.map +1 -0
  9. package/dist/agent-labels.d.ts +12 -0
  10. package/dist/agent-labels.js +96 -0
  11. package/dist/agent-labels.js.map +1 -0
  12. package/dist/allowlist.d.ts +27 -0
  13. package/dist/allowlist.js +34 -0
  14. package/dist/allowlist.js.map +1 -0
  15. package/dist/approval-routing.d.ts +2 -0
  16. package/dist/approval-routing.js +7 -0
  17. package/dist/approval-routing.js.map +1 -0
  18. package/dist/command-registry.d.ts +3 -0
  19. package/dist/command-registry.js +268 -0
  20. package/dist/command-registry.js.map +1 -0
  21. package/dist/commands.d.ts +11 -0
  22. package/dist/commands.js +516 -0
  23. package/dist/commands.js.map +1 -0
  24. package/dist/constants.d.ts +76 -0
  25. package/dist/constants.js +71 -0
  26. package/dist/constants.js.map +1 -0
  27. package/dist/escalation.d.ts +42 -0
  28. package/dist/escalation.js +252 -0
  29. package/dist/escalation.js.map +1 -0
  30. package/dist/file-routing.d.ts +51 -0
  31. package/dist/file-routing.js +212 -0
  32. package/dist/file-routing.js.map +1 -0
  33. package/dist/formatters.d.ts +31 -0
  34. package/dist/formatters.js +336 -0
  35. package/dist/formatters.js.map +1 -0
  36. package/dist/index.d.ts +6 -0
  37. package/dist/index.js +4 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/interaction-delivery.d.ts +90 -0
  40. package/dist/interaction-delivery.js +142 -0
  41. package/dist/interaction-delivery.js.map +1 -0
  42. package/dist/manifest.d.ts +3 -0
  43. package/dist/manifest.js +111 -0
  44. package/dist/manifest.js.map +1 -0
  45. package/dist/media-pipeline.d.ts +47 -0
  46. package/dist/media-pipeline.js +162 -0
  47. package/dist/media-pipeline.js.map +1 -0
  48. package/dist/notification-filters.d.ts +23 -0
  49. package/dist/notification-filters.js +93 -0
  50. package/dist/notification-filters.js.map +1 -0
  51. package/dist/paperclip-api.d.ts +25 -0
  52. package/dist/paperclip-api.js +69 -0
  53. package/dist/paperclip-api.js.map +1 -0
  54. package/dist/polling-offset.d.ts +22 -0
  55. package/dist/polling-offset.js +68 -0
  56. package/dist/polling-offset.js.map +1 -0
  57. package/dist/secret-ref-validation.d.ts +7 -0
  58. package/dist/secret-ref-validation.js +49 -0
  59. package/dist/secret-ref-validation.js.map +1 -0
  60. package/dist/telegram-api.d.ts +40 -0
  61. package/dist/telegram-api.js +251 -0
  62. package/dist/telegram-api.js.map +1 -0
  63. package/dist/topic-projects.d.ts +2 -0
  64. package/dist/topic-projects.js +45 -0
  65. package/dist/topic-projects.js.map +1 -0
  66. package/dist/ui/index.d.ts +2 -0
  67. package/dist/ui/index.js +1446 -0
  68. package/dist/ui/index.js.map +1 -0
  69. package/dist/watch-registry.d.ts +9 -0
  70. package/dist/watch-registry.js +272 -0
  71. package/dist/watch-registry.js.map +1 -0
  72. package/dist/worker.d.ts +162 -0
  73. package/dist/worker.js +1520 -0
  74. package/dist/worker.js.map +1 -0
  75. package/package.json +59 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interaction-delivery.js","sourceRoot":"","sources":["../src/interaction-delivery.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { PaperclipPluginManifestV1 } from "@paperclipai/plugin-sdk";
2
+ declare const manifest: PaperclipPluginManifestV1;
3
+ export default manifest;
@@ -0,0 +1,111 @@
1
+ import { PLUGIN_ID, PLUGIN_VERSION } from "./constants.js";
2
+ const manifest = {
3
+ id: PLUGIN_ID,
4
+ apiVersion: 1,
5
+ version: PLUGIN_VERSION,
6
+ displayName: "Telegram Bot",
7
+ description: "Bidirectional Telegram integration: push notifications, bot commands, escalation to humans, multi-agent sessions (native + ACP), media pipeline with transcription, custom workflow commands, and proactive suggestion watches.",
8
+ author: "Odience Network",
9
+ categories: ["connector", "automation"],
10
+ capabilities: [
11
+ "companies.read",
12
+ "projects.read",
13
+ "issues.read",
14
+ "issues.create",
15
+ "issues.update",
16
+ "issue.comments.read",
17
+ "issue.comments.create",
18
+ "agents.read",
19
+ "agents.invoke",
20
+ "agent.sessions.create",
21
+ "agent.sessions.list",
22
+ "agent.sessions.send",
23
+ "agent.sessions.close",
24
+ "agent.tools.register",
25
+ "events.subscribe",
26
+ "events.emit",
27
+ "plugin.state.read",
28
+ "plugin.state.write",
29
+ "http.outbound",
30
+ "secrets.read-ref",
31
+ "activity.log.write",
32
+ "metrics.write",
33
+ "jobs.schedule",
34
+ "instance.settings.register",
35
+ "ui.page.register",
36
+ ],
37
+ entrypoints: {
38
+ worker: "./dist/worker.js",
39
+ ui: "./dist/ui",
40
+ },
41
+ ui: {
42
+ slots: [
43
+ {
44
+ type: "settingsPage",
45
+ id: "telegram-settings",
46
+ displayName: "Telegram Settings",
47
+ exportName: "TelegramSettingsPage",
48
+ },
49
+ ],
50
+ },
51
+ jobs: [
52
+ {
53
+ jobKey: "telegram-daily-digest",
54
+ displayName: "Telegram Digest",
55
+ description: "Send a summary of agent activity to Telegram (daily or bidaily).",
56
+ schedule: "0 * * * *",
57
+ },
58
+ {
59
+ jobKey: "check-escalation-timeouts",
60
+ displayName: "Check Escalation Timeouts",
61
+ description: "Check for timed-out escalations and apply default actions.",
62
+ schedule: "* * * * *",
63
+ },
64
+ {
65
+ jobKey: "check-watches",
66
+ displayName: "Check Proactive Watches",
67
+ description: "Evaluate registered watches and send suggestions when conditions are met.",
68
+ schedule: "*/15 * * * *",
69
+ },
70
+ ],
71
+ tools: [
72
+ {
73
+ name: "escalate_to_human",
74
+ displayName: "Escalate to Human",
75
+ description: "Escalate a conversation to a human when you cannot handle it confidently",
76
+ parametersSchema: { type: "object" },
77
+ },
78
+ {
79
+ name: "handoff_to_agent",
80
+ displayName: "Handoff to Agent",
81
+ description: "Hand off work to another agent in this thread",
82
+ parametersSchema: { type: "object" },
83
+ },
84
+ {
85
+ name: "discuss_with_agent",
86
+ displayName: "Discuss with Agent",
87
+ description: "Start a back-and-forth conversation with another agent",
88
+ parametersSchema: { type: "object" },
89
+ },
90
+ {
91
+ name: "register_watch",
92
+ displayName: "Register Watch",
93
+ description: "Register a proactive watch that monitors entities and sends suggestions",
94
+ parametersSchema: { type: "object" },
95
+ },
96
+ {
97
+ name: "send_to_telegram",
98
+ displayName: "Send Telegram Message",
99
+ description: "Send text and Markdown content to a Telegram chat, with optional thread and project-key file routing.",
100
+ parametersSchema: { type: "object" },
101
+ },
102
+ {
103
+ name: "send_file_to_telegram",
104
+ displayName: "Send File to Telegram",
105
+ description: "Deprecated compatibility alias for send_to_telegram.",
106
+ parametersSchema: { type: "object" },
107
+ },
108
+ ],
109
+ };
110
+ export default manifest;
111
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAE3D,MAAM,QAAQ,GAA8B;IAC1C,EAAE,EAAE,SAAS;IACb,UAAU,EAAE,CAAC;IACb,OAAO,EAAE,cAAc;IACvB,WAAW,EAAE,cAAc;IAC3B,WAAW,EACT,iOAAiO;IACnO,MAAM,EAAE,iBAAiB;IACzB,UAAU,EAAE,CAAC,WAAW,EAAE,YAAY,CAAC;IACvC,YAAY,EAAE;QACZ,gBAAgB;QAChB,eAAe;QACf,aAAa;QACb,eAAe;QACf,eAAe;QACf,qBAAqB;QACrB,uBAAuB;QACvB,aAAa;QACb,eAAe;QACf,uBAAuB;QACvB,qBAAqB;QACrB,qBAAqB;QACrB,sBAAsB;QACtB,sBAAsB;QACtB,kBAAkB;QAClB,aAAa;QACb,mBAAmB;QACnB,oBAAoB;QACpB,eAAe;QACf,kBAAkB;QAClB,oBAAoB;QACpB,eAAe;QACf,eAAe;QACf,4BAA4B;QAC5B,kBAAkB;KACnB;IACD,WAAW,EAAE;QACX,MAAM,EAAE,kBAAkB;QAC1B,EAAE,EAAE,WAAW;KAChB;IACD,EAAE,EAAE;QACF,KAAK,EAAE;YACL;gBACE,IAAI,EAAE,cAAc;gBACpB,EAAE,EAAE,mBAAmB;gBACvB,WAAW,EAAE,mBAAmB;gBAChC,UAAU,EAAE,sBAAsB;aACnC;SACF;KACF;IACD,IAAI,EAAE;QACJ;YACE,MAAM,EAAE,uBAAuB;YAC/B,WAAW,EAAE,iBAAiB;YAC9B,WAAW,EAAE,kEAAkE;YAC/E,QAAQ,EAAE,WAAW;SACtB;QACD;YACE,MAAM,EAAE,2BAA2B;YACnC,WAAW,EAAE,2BAA2B;YACxC,WAAW,EAAE,4DAA4D;YACzE,QAAQ,EAAE,WAAW;SACtB;QACD;YACE,MAAM,EAAE,eAAe;YACvB,WAAW,EAAE,yBAAyB;YACtC,WAAW,EAAE,2EAA2E;YACxF,QAAQ,EAAE,cAAc;SACzB;KACF;IACD,KAAK,EAAE;QACL;YACE,IAAI,EAAE,mBAAmB;YACzB,WAAW,EAAE,mBAAmB;YAChC,WAAW,EAAE,0EAA0E;YACvF,gBAAgB,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;SACrC;QACD;YACE,IAAI,EAAE,kBAAkB;YACxB,WAAW,EAAE,kBAAkB;YAC/B,WAAW,EAAE,+CAA+C;YAC5D,gBAAgB,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;SACrC;QACD;YACE,IAAI,EAAE,oBAAoB;YAC1B,WAAW,EAAE,oBAAoB;YACjC,WAAW,EAAE,wDAAwD;YACrE,gBAAgB,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;SACrC;QACD;YACE,IAAI,EAAE,gBAAgB;YACtB,WAAW,EAAE,gBAAgB;YAC7B,WAAW,EAAE,yEAAyE;YACtF,gBAAgB,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;SACrC;QACD;YACE,IAAI,EAAE,kBAAkB;YACxB,WAAW,EAAE,uBAAuB;YACpC,WAAW,EAAE,uGAAuG;YACpH,gBAAgB,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;SACrC;QACD;YACE,IAAI,EAAE,uBAAuB;YAC7B,WAAW,EAAE,uBAAuB;YACpC,WAAW,EAAE,sDAAsD;YACnE,gBAAgB,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;SACrC;KACF;CACF,CAAC;AAEF,eAAe,QAAQ,CAAC"}
@@ -0,0 +1,47 @@
1
+ import type { PluginContext } from "@paperclipai/plugin-sdk";
2
+ type MediaConfig = {
3
+ briefAgentId: string;
4
+ briefAgentChatIds: string[];
5
+ transcriptionApiKeyRef: string;
6
+ publicUrl?: string;
7
+ };
8
+ type TelegramMediaMessage = {
9
+ message_id: number;
10
+ chat: {
11
+ id: number;
12
+ };
13
+ message_thread_id?: number;
14
+ from?: {
15
+ id: number;
16
+ username?: string;
17
+ first_name?: string;
18
+ };
19
+ voice?: {
20
+ file_id: string;
21
+ duration: number;
22
+ mime_type?: string;
23
+ };
24
+ audio?: {
25
+ file_id: string;
26
+ duration: number;
27
+ title?: string;
28
+ mime_type?: string;
29
+ };
30
+ video_note?: {
31
+ file_id: string;
32
+ duration: number;
33
+ };
34
+ document?: {
35
+ file_id: string;
36
+ file_name?: string;
37
+ mime_type?: string;
38
+ };
39
+ photo?: Array<{
40
+ file_id: string;
41
+ width: number;
42
+ height: number;
43
+ }>;
44
+ caption?: string;
45
+ };
46
+ export declare function handleMediaMessage(ctx: PluginContext, token: string, msg: TelegramMediaMessage, config: MediaConfig, companyId: string): Promise<boolean>;
47
+ export {};
@@ -0,0 +1,162 @@
1
+ import { sendMessage, escapeMarkdownV2, sendChatAction } from "./telegram-api.js";
2
+ import { METRIC_NAMES } from "./constants.js";
3
+ import { getSessions, wakeAgentWithIssue } from "./acp-bridge.js";
4
+ import { resolveMappedProjectIdForTopic } from "./topic-projects.js";
5
+ const TELEGRAM_API = "https://api.telegram.org";
6
+ export async function handleMediaMessage(ctx, token, msg, config, companyId) {
7
+ const chatId = String(msg.chat.id);
8
+ const threadId = msg.message_thread_id;
9
+ // Determine if this is an intake channel (brief agent) or an agent thread
10
+ const isIntakeChannel = config.briefAgentChatIds.includes(chatId);
11
+ const hasActiveSession = threadId
12
+ ? (await getSessions(ctx, chatId, threadId)).some((s) => s.status === "active")
13
+ : false;
14
+ if (!isIntakeChannel && !hasActiveSession) {
15
+ return false; // Not a media-relevant context
16
+ }
17
+ await sendChatAction(ctx, token, chatId);
18
+ // Extract media file info
19
+ const fileId = extractFileId(msg);
20
+ if (!fileId)
21
+ return false;
22
+ const isAudio = !!(msg.voice || msg.audio || msg.video_note);
23
+ let textContent = msg.caption ?? "";
24
+ // Transcribe audio/voice if applicable
25
+ if (isAudio && config.transcriptionApiKeyRef) {
26
+ try {
27
+ const transcription = await transcribeAudio(ctx, token, fileId, config.transcriptionApiKeyRef);
28
+ if (transcription) {
29
+ textContent = transcription;
30
+ // Send transcription preview
31
+ await sendMessage(ctx, token, chatId, `${escapeMarkdownV2("\ud83c\udfa4")} *Transcription:*\n${escapeMarkdownV2(textContent.slice(0, 500))}${textContent.length > 500 ? escapeMarkdownV2("...") : ""}`, {
32
+ parseMode: "MarkdownV2",
33
+ messageThreadId: threadId,
34
+ replyToMessageId: msg.message_id,
35
+ });
36
+ }
37
+ }
38
+ catch (err) {
39
+ ctx.logger.error("Transcription failed", { error: String(err) });
40
+ textContent = msg.caption ?? "[Audio - transcription failed]";
41
+ }
42
+ }
43
+ if (isIntakeChannel && config.briefAgentId) {
44
+ // Route to Brief Agent via one-shot invocation
45
+ try {
46
+ const prompt = buildBriefPrompt(msg, textContent);
47
+ const { runId } = await ctx.agents.invoke(config.briefAgentId, companyId, {
48
+ prompt,
49
+ reason: "media_intake",
50
+ });
51
+ const hasPublicUrl = config.publicUrl && config.publicUrl.startsWith("https://");
52
+ const inlineKeyboard = hasPublicUrl
53
+ ? [[{ text: "View Run ↗", url: `${config.publicUrl}/agents/${config.briefAgentId}/runs/${runId}` }]]
54
+ : undefined;
55
+ await sendMessage(ctx, token, chatId, `${escapeMarkdownV2("\ud83d\udcdd")} Media sent to Brief Agent \\(run: \`${escapeMarkdownV2(runId)}\`\\)`, {
56
+ parseMode: "MarkdownV2",
57
+ messageThreadId: threadId,
58
+ replyToMessageId: msg.message_id,
59
+ inlineKeyboard,
60
+ });
61
+ ctx.logger.info("Media routed to brief agent", { runId, briefAgentId: config.briefAgentId });
62
+ }
63
+ catch (err) {
64
+ ctx.logger.error("Failed to invoke brief agent", { error: String(err) });
65
+ await sendMessage(ctx, token, chatId, `Failed to route media to brief agent: ${String(err)}`, { messageThreadId: threadId });
66
+ }
67
+ }
68
+ else if (hasActiveSession && threadId) {
69
+ // Route to the active agent in the thread
70
+ const sessions = await getSessions(ctx, chatId, threadId);
71
+ const activeSessions = sessions.filter((s) => s.status === "active");
72
+ const target = activeSessions.sort((a, b) => new Date(b.lastActivityAt).getTime() - new Date(a.lastActivityAt).getTime())[0];
73
+ if (target) {
74
+ const mediaLabel = isAudio ? "Audio message" : msg.photo ? "Photo" : "Document";
75
+ const prompt = `[${mediaLabel}] ${textContent || "(no caption)"}`;
76
+ const projectId = await resolveMappedProjectIdForTopic(ctx, chatId, companyId, threadId);
77
+ if (target.transport === "native") {
78
+ await wakeAgentWithIssue(ctx, target.agentId, companyId, prompt, "media_message", projectId);
79
+ }
80
+ else {
81
+ ctx.events.emit("acp-spawn", companyId, {
82
+ type: "message",
83
+ sessionId: target.sessionId,
84
+ chatId,
85
+ threadId,
86
+ text: prompt,
87
+ });
88
+ }
89
+ }
90
+ }
91
+ await ctx.metrics.write(METRIC_NAMES.mediaProcessed, 1);
92
+ return true;
93
+ }
94
+ function extractFileId(msg) {
95
+ if (msg.voice)
96
+ return msg.voice.file_id;
97
+ if (msg.audio)
98
+ return msg.audio.file_id;
99
+ if (msg.video_note)
100
+ return msg.video_note.file_id;
101
+ if (msg.document)
102
+ return msg.document.file_id;
103
+ if (msg.photo && msg.photo.length > 0) {
104
+ // Use the largest photo
105
+ return msg.photo.sort((a, b) => b.width * b.height - a.width * a.height)[0].file_id;
106
+ }
107
+ return null;
108
+ }
109
+ async function transcribeAudio(ctx, botToken, fileId, transcriptionApiKeyRef) {
110
+ // 1. Get file path from Telegram (JSON response — ctx.http.fetch works fine for this)
111
+ const fileRes = await ctx.http.fetch(`${TELEGRAM_API}/bot${botToken}/getFile?file_id=${fileId}`, { method: "GET" });
112
+ const fileData = (await fileRes.json());
113
+ if (!fileData.ok || !fileData.result?.file_path) {
114
+ ctx.logger.error("Failed to get file path from Telegram", { fileId });
115
+ return null;
116
+ }
117
+ // 2. Download binary audio using native fetch (bypasses RPC bridge UTF-8 corruption)
118
+ const downloadUrl = `${TELEGRAM_API}/file/bot${botToken}/${fileData.result.file_path}`;
119
+ const audioRes = await fetch(downloadUrl);
120
+ const audioBuffer = Buffer.from(await audioRes.arrayBuffer());
121
+ // 3. Resolve the OpenAI API key from Paperclip secrets
122
+ const apiKey = await ctx.secrets.resolve(transcriptionApiKeyRef);
123
+ // 4. Build multipart form data manually (native FormData + Blob works in Node 18+)
124
+ const formData = new FormData();
125
+ formData.append("file", new Blob([audioBuffer], { type: "audio/ogg" }), "audio.ogg");
126
+ formData.append("model", "whisper-1");
127
+ // 5. Send to Whisper API using native fetch (FormData can't be serialized through RPC bridge)
128
+ const whisperRes = await fetch("https://api.openai.com/v1/audio/transcriptions", {
129
+ method: "POST",
130
+ headers: {
131
+ Authorization: `Bearer ${apiKey}`,
132
+ },
133
+ body: formData,
134
+ });
135
+ const whisperData = (await whisperRes.json());
136
+ return whisperData.text ?? null;
137
+ }
138
+ function buildBriefPrompt(msg, textContent) {
139
+ const parts = [];
140
+ if (msg.voice) {
141
+ parts.push(`[Voice message, ${msg.voice.duration}s]`);
142
+ }
143
+ else if (msg.audio) {
144
+ parts.push(`[Audio: ${msg.audio.title ?? "untitled"}, ${msg.audio.duration}s]`);
145
+ }
146
+ else if (msg.video_note) {
147
+ parts.push(`[Video note, ${msg.video_note.duration}s]`);
148
+ }
149
+ else if (msg.document) {
150
+ parts.push(`[Document: ${msg.document.file_name ?? "unknown"}]`);
151
+ }
152
+ else if (msg.photo) {
153
+ parts.push("[Photo]");
154
+ }
155
+ if (textContent) {
156
+ parts.push(textContent);
157
+ }
158
+ const sender = msg.from?.username ?? msg.from?.first_name ?? "unknown";
159
+ parts.push(`\nFrom: ${sender}`);
160
+ return parts.join("\n");
161
+ }
162
+ //# sourceMappingURL=media-pipeline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-pipeline.js","sourceRoot":"","sources":["../src/media-pipeline.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import type { PluginEvent } from "@paperclipai/plugin-sdk";
2
+ /**
3
+ * Forward an `issue.updated` event as a "blocked" notification only when the issue
4
+ * transitioned to `status === "blocked"` AND it is owned by a human/board user
5
+ * (`assigneeUserId` is non-null/non-empty). Agent-only blocked issues are noise.
6
+ */
7
+ export declare function shouldNotifyIssueBlocked(event: PluginEvent, enabled: boolean): boolean;
8
+ /**
9
+ * Normalize a configured board-usernames value (array or comma/whitespace-separated
10
+ * string) into a deduped list of lowercase handles with any leading `@` stripped.
11
+ */
12
+ export declare function parseBoardUsernames(value: unknown): string[];
13
+ /**
14
+ * Word-boundary-aware, case-insensitive `@username` matcher. `@board` matches
15
+ * "ping @board please" but not "@boardroom" (trailing boundary) nor "me@board.com"
16
+ * (leading boundary — avoids email false positives).
17
+ */
18
+ export declare function matchesBoardMention(text: string, usernames: string[]): boolean;
19
+ /**
20
+ * Forward an `issue.comment.created` event only when the comment body @-mentions one
21
+ * of the configured board usernames.
22
+ */
23
+ export declare function shouldNotifyBoardMention(event: PluginEvent, enabled: boolean, boardUsernames: string[]): boolean;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Anti-flood notification filters.
3
+ *
4
+ * Ported from tue-Jonas/paperclip-plugin-telegram (TWB-94, commit 03b6e99) — see
5
+ * TWB-FORK.md. The fork diverged from our v0.3.0 line (it removed the settings UI
6
+ * and secret-ref validation we keep), so the *logic* is re-implemented here against
7
+ * our worker rather than applying the fork's branch diff.
8
+ *
9
+ * These predicates gate noisy notifications behind config flags so the board chat
10
+ * only hears signal:
11
+ * - issue blocked: forward `issue.updated` only when the issue is genuinely blocked
12
+ * AND a human/board (assigneeUserId) owns it.
13
+ * - board mention: forward `issue.comment.created` only when a configured board
14
+ * username is @-mentioned (word-boundary aware, case-insensitive).
15
+ *
16
+ * Note: run-started / run-finished gating (the other half of TWB-94) already exists
17
+ * in our worker post-v0.3.0 via `notifyOnAgentRunStarted` / `notifyOnAgentRunFinished`
18
+ * (both default off), so it is not re-implemented here.
19
+ */
20
+ function isNonEmptyValue(value) {
21
+ if (value === null || value === undefined)
22
+ return false;
23
+ if (typeof value === "string")
24
+ return value.trim().length > 0;
25
+ return true;
26
+ }
27
+ /**
28
+ * Forward an `issue.updated` event as a "blocked" notification only when the issue
29
+ * transitioned to `status === "blocked"` AND it is owned by a human/board user
30
+ * (`assigneeUserId` is non-null/non-empty). Agent-only blocked issues are noise.
31
+ */
32
+ export function shouldNotifyIssueBlocked(event, enabled) {
33
+ if (!enabled)
34
+ return false;
35
+ const payload = event.payload;
36
+ if (payload.status !== "blocked")
37
+ return false;
38
+ return isNonEmptyValue(payload.assigneeUserId);
39
+ }
40
+ /**
41
+ * Normalize a configured board-usernames value (array or comma/whitespace-separated
42
+ * string) into a deduped list of lowercase handles with any leading `@` stripped.
43
+ */
44
+ export function parseBoardUsernames(value) {
45
+ const raw = Array.isArray(value)
46
+ ? value.map((entry) => String(entry))
47
+ : typeof value === "string"
48
+ ? value.split(/[\s,]+/)
49
+ : [];
50
+ const seen = new Set();
51
+ for (const entry of raw) {
52
+ const handle = entry.trim().replace(/^@+/, "").toLowerCase();
53
+ if (handle)
54
+ seen.add(handle);
55
+ }
56
+ return [...seen];
57
+ }
58
+ function escapeRegExp(value) {
59
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
60
+ }
61
+ /**
62
+ * Word-boundary-aware, case-insensitive `@username` matcher. `@board` matches
63
+ * "ping @board please" but not "@boardroom" (trailing boundary) nor "me@board.com"
64
+ * (leading boundary — avoids email false positives).
65
+ */
66
+ export function matchesBoardMention(text, usernames) {
67
+ if (!text || usernames.length === 0)
68
+ return false;
69
+ for (const handle of usernames) {
70
+ const re = new RegExp(`(?<![a-z0-9_])@${escapeRegExp(handle)}(?![a-z0-9_])`, "i");
71
+ if (re.test(text))
72
+ return true;
73
+ }
74
+ return false;
75
+ }
76
+ function extractCommentBody(payload) {
77
+ const candidate = (typeof payload.body === "string" && payload.body) ||
78
+ (typeof payload.comment === "string" && payload.comment) ||
79
+ (typeof payload.text === "string" && payload.text) ||
80
+ "";
81
+ return candidate;
82
+ }
83
+ /**
84
+ * Forward an `issue.comment.created` event only when the comment body @-mentions one
85
+ * of the configured board usernames.
86
+ */
87
+ export function shouldNotifyBoardMention(event, enabled, boardUsernames) {
88
+ if (!enabled || boardUsernames.length === 0)
89
+ return false;
90
+ const payload = event.payload;
91
+ return matchesBoardMention(extractCommentBody(payload), boardUsernames);
92
+ }
93
+ //# sourceMappingURL=notification-filters.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notification-filters.js","sourceRoot":"","sources":["../src/notification-filters.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import type { PluginContext } from "@paperclipai/plugin-sdk";
2
+ /**
3
+ * Structured error for non-2xx Paperclip API responses. Ported from tue-Jonas
4
+ * `PaperclipApiError` (TWX-328) onto our worker/commands layout: it preserves
5
+ * the original throw message so existing callers/tests keep matching on
6
+ * "Paperclip API request failed with <status>" while also exposing `status` and
7
+ * `detail` so callers can classify conflicts (e.g. stale decision callbacks).
8
+ */
9
+ export declare class PaperclipApiError extends Error {
10
+ readonly status: number;
11
+ readonly detail: string;
12
+ constructor(status: number, detail: string);
13
+ }
14
+ /**
15
+ * Classifies "the decision is no longer actionable" conflicts so a stale inline
16
+ * button press (TWX-328) is acknowledged gracefully instead of surfacing a raw
17
+ * API error. Covers both shapes seen across Paperclip surfaces:
18
+ * - interaction conflicts: 409 "Interaction has already been resolved"
19
+ * - approval conflicts: 422 "Only pending or revision requested approvals
20
+ * can be approved/rejected" (an approval decided through another channel,
21
+ * expired, or already resolved in the opposite direction).
22
+ */
23
+ export declare function isAlreadyResolvedConflict(error: unknown): boolean;
24
+ export declare function fetchPaperclipApi(ctx: PluginContext, url: string, init?: RequestInit): Promise<Response>;
25
+ export declare function buildPaperclipAuthHeaders(boardApiToken?: string): Record<string, string>;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Structured error for non-2xx Paperclip API responses. Ported from tue-Jonas
3
+ * `PaperclipApiError` (TWX-328) onto our worker/commands layout: it preserves
4
+ * the original throw message so existing callers/tests keep matching on
5
+ * "Paperclip API request failed with <status>" while also exposing `status` and
6
+ * `detail` so callers can classify conflicts (e.g. stale decision callbacks).
7
+ */
8
+ export class PaperclipApiError extends Error {
9
+ status;
10
+ detail;
11
+ constructor(status, detail) {
12
+ const suffix = detail ? `: ${detail}` : "";
13
+ super(`Paperclip API request failed with ${status}${suffix}`);
14
+ this.name = "PaperclipApiError";
15
+ this.status = status;
16
+ this.detail = detail;
17
+ }
18
+ }
19
+ /**
20
+ * Classifies "the decision is no longer actionable" conflicts so a stale inline
21
+ * button press (TWX-328) is acknowledged gracefully instead of surfacing a raw
22
+ * API error. Covers both shapes seen across Paperclip surfaces:
23
+ * - interaction conflicts: 409 "Interaction has already been resolved"
24
+ * - approval conflicts: 422 "Only pending or revision requested approvals
25
+ * can be approved/rejected" (an approval decided through another channel,
26
+ * expired, or already resolved in the opposite direction).
27
+ */
28
+ export function isAlreadyResolvedConflict(error) {
29
+ const record = error && typeof error === "object" ? error : null;
30
+ const status = typeof record?.status === "number" ? record.status : null;
31
+ const detail = typeof record?.detail === "string" ? record.detail : "";
32
+ if (status !== 409 && status !== 422)
33
+ return false;
34
+ return /already been (?:resolved|decided)|already (?:resolved|decided)|only pending(?: or revision requested)? approvals can be/i.test(detail);
35
+ }
36
+ function isLoopbackUrl(url) {
37
+ try {
38
+ const parsed = new URL(url);
39
+ return ["localhost", "127.0.0.1", "::1", "[::1]"].includes(parsed.hostname);
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ }
45
+ export async function fetchPaperclipApi(ctx, url, init) {
46
+ const response = isLoopbackUrl(url)
47
+ ? await fetch(url, init)
48
+ : await ctx.http.fetch(url, init);
49
+ if (!response.ok) {
50
+ let body = "";
51
+ try {
52
+ body = await response.text();
53
+ }
54
+ catch {
55
+ body = "";
56
+ }
57
+ const detail = body ? body.slice(0, 300) : "";
58
+ throw new PaperclipApiError(response.status, detail);
59
+ }
60
+ return response;
61
+ }
62
+ export function buildPaperclipAuthHeaders(boardApiToken) {
63
+ return boardApiToken
64
+ ? {
65
+ Authorization: `Bearer ${boardApiToken}`,
66
+ }
67
+ : {};
68
+ }
69
+ //# sourceMappingURL=paperclip-api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paperclip-api.js","sourceRoot":"","sources":["../src/paperclip-api.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IACjC,MAAM,CAAS;IACf,MAAM,CAAS;IAExB,YAAY,MAAc,EAAE,MAAc;QACxC,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,KAAK,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3C,KAAK,CAAC,qCAAqC,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;QAC9D,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;QAChC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;CACF;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,yBAAyB,CAAC,KAAc;IACtD,MAAM,MAAM,GACV,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAE,KAAiC,CAAC,CAAC,CAAC,IAAI,CAAC;IACjF,MAAM,MAAM,GAAG,OAAO,MAAM,EAAE,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;IACzE,MAAM,MAAM,GAAG,OAAO,MAAM,EAAE,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IACvE,IAAI,MAAM,KAAK,GAAG,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,KAAK,CAAC;IACnD,OAAO,0HAA0H,CAAC,IAAI,CACpI,MAAM,CACP,CAAC;AACJ,CAAC;AAED,SAAS,aAAa,CAAC,GAAW;IAChC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;QAC5B,OAAO,CAAC,WAAW,EAAE,WAAW,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC9E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,GAAkB,EAClB,GAAW,EACX,IAAkB;IAElB,MAAM,QAAQ,GAAG,aAAa,CAAC,GAAG,CAAC;QACjC,CAAC,CAAC,MAAM,KAAK,CAAC,GAAG,EAAE,IAAI,CAAC;QACxB,CAAC,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAEpC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,IAAI,IAAI,GAAG,EAAE,CAAC;QACd,IAAI,CAAC;YACH,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QAC/B,CAAC;QAAC,MAAM,CAAC;YACP,IAAI,GAAG,EAAE,CAAC;QACZ,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9C,MAAM,IAAI,iBAAiB,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvD,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,yBAAyB,CACvC,aAAsB;IAEtB,OAAO,aAAa;QAClB,CAAC,CAAC;YACE,aAAa,EAAE,UAAU,aAAa,EAAE;SACzC;QACH,CAAC,CAAC,EAAE,CAAC;AACT,CAAC"}
@@ -0,0 +1,22 @@
1
+ import type { PluginContext } from "@paperclipai/plugin-sdk";
2
+ export declare const TELEGRAM_LAST_UPDATE_ID_STATE_KEY = "telegram-last-update-id";
3
+ export declare function getPersistedTelegramUpdateOffset(ctx: PluginContext): Promise<number>;
4
+ export declare function persistTelegramUpdateOffset(ctx: PluginContext, updateId: number): Promise<void>;
5
+ type PollingLogger = Pick<PluginContext["logger"], "error">;
6
+ export declare function handleTelegramUpdateThenPersistOffset(options: {
7
+ updateId: number;
8
+ lastUpdateId: number;
9
+ handleUpdate: () => Promise<void>;
10
+ persistOffset: (updateId: number) => Promise<void>;
11
+ logger: PollingLogger;
12
+ }): Promise<number>;
13
+ export declare function processTelegramUpdateBatch<TUpdate extends {
14
+ update_id: number;
15
+ }>(options: {
16
+ updates: TUpdate[];
17
+ lastUpdateId: number;
18
+ handleUpdate: (update: TUpdate) => Promise<void>;
19
+ persistOffset: (updateId: number) => Promise<void>;
20
+ logger: PollingLogger;
21
+ }): Promise<number>;
22
+ export {};