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/commands.ts DELETED
@@ -1,346 +0,0 @@
1
- import type { PluginContext, Agent, Issue } from "@paperclipai/plugin-sdk";
2
- import { sendMessage, escapeMarkdownV2, sendChatAction } from "./telegram-api.js";
3
- import { METRIC_NAMES } from "./constants.js";
4
- import { handleAcpCommand } from "./acp-bridge.js";
5
-
6
- type BotCommand = {
7
- command: string;
8
- description: string;
9
- };
10
-
11
- export const BOT_COMMANDS: BotCommand[] = [
12
- { command: "status", description: "Company health: active agents, open issues" },
13
- { command: "issues", description: "List open issues (optionally by project)" },
14
- { command: "agents", description: "List agents with current status" },
15
- { command: "approve", description: "Approve a pending request by ID" },
16
- { command: "help", description: "Show available commands" },
17
- { command: "acp", description: "Manage agent sessions (spawn, status, cancel, close)" },
18
- { command: "commands", description: "Manage custom workflow commands (list, import, run, delete)" },
19
- ];
20
-
21
- export async function handleCommand(
22
- ctx: PluginContext,
23
- token: string,
24
- chatId: string,
25
- command: string,
26
- args: string,
27
- messageThreadId?: number,
28
- baseUrl?: string,
29
- ): Promise<void> {
30
- await ctx.metrics.write(METRIC_NAMES.commandsHandled, 1);
31
-
32
- switch (command) {
33
- case "status":
34
- await handleStatus(ctx, token, chatId, messageThreadId);
35
- break;
36
- case "issues":
37
- await handleIssues(ctx, token, chatId, args, messageThreadId);
38
- break;
39
- case "agents":
40
- await handleAgents(ctx, token, chatId, messageThreadId);
41
- break;
42
- case "approve":
43
- await handleApprove(ctx, token, chatId, args, messageThreadId, baseUrl);
44
- break;
45
- case "help":
46
- await handleHelp(ctx, token, chatId, messageThreadId);
47
- break;
48
- case "connect":
49
- await handleConnect(ctx, token, chatId, args, messageThreadId);
50
- break;
51
- case "connect-topic":
52
- await handleConnectTopic(ctx, token, chatId, args, messageThreadId);
53
- break;
54
- case "acp":
55
- await handleAcpCommand(ctx, token, chatId, args, messageThreadId);
56
- break;
57
- default:
58
- await sendMessage(ctx, token, chatId, `Unknown command: /${command}. Try /help`, {
59
- messageThreadId,
60
- });
61
- }
62
- }
63
-
64
- async function handleStatus(
65
- ctx: PluginContext,
66
- token: string,
67
- chatId: string,
68
- messageThreadId?: number,
69
- ): Promise<void> {
70
- await sendChatAction(ctx, token, chatId);
71
-
72
- try {
73
- const companyId = await resolveCompanyId(ctx, chatId);
74
- const agents = await ctx.agents.list({ companyId });
75
- const activeAgents = agents.filter((a: Agent) => a.status === "active");
76
- const issues = await ctx.issues.list({ companyId, limit: 10 });
77
- const doneIssues = issues.filter((i: Issue) => i.status === "done");
78
-
79
- const lines = [
80
- escapeMarkdownV2("📊") + " *Paperclip Status*",
81
- "",
82
- `${escapeMarkdownV2("🤖")} Active agents: *${activeAgents.length}*/${escapeMarkdownV2(String(agents.length))}`,
83
- `${escapeMarkdownV2("📋")} Recent issues: *${escapeMarkdownV2(String(issues.length))}* \\(${escapeMarkdownV2(String(doneIssues.length))} done\\)`,
84
- ];
85
-
86
- await sendMessage(ctx, token, chatId, lines.join("\n"), {
87
- parseMode: "MarkdownV2",
88
- messageThreadId,
89
- });
90
- } catch {
91
- await sendMessage(ctx, token, chatId, escapeMarkdownV2("📊") + " *Paperclip Status*\n\n" + escapeMarkdownV2("Could not fetch status. Make sure this chat is linked to a company with /connect."), {
92
- parseMode: "MarkdownV2",
93
- messageThreadId,
94
- });
95
- }
96
- }
97
-
98
- async function handleIssues(
99
- ctx: PluginContext,
100
- token: string,
101
- chatId: string,
102
- projectFilter: string,
103
- messageThreadId?: number,
104
- ): Promise<void> {
105
- await sendChatAction(ctx, token, chatId);
106
-
107
- try {
108
- const companyId = await resolveCompanyId(ctx, chatId);
109
- const issues = await ctx.issues.list({ companyId, limit: 10 });
110
- const filtered = projectFilter
111
- ? issues.filter((i: Issue) => {
112
- const projName = i.project?.name ?? "";
113
- return projName.toLowerCase().includes(projectFilter.toLowerCase());
114
- })
115
- : issues;
116
-
117
- if (filtered.length === 0) {
118
- const filter = projectFilter ? ` for project "${projectFilter}"` : "";
119
- await sendMessage(ctx, token, chatId, `No issues found${filter}.`, { messageThreadId });
120
- return;
121
- }
122
-
123
- const statusEmoji: Record<string, string> = { done: "✅", todo: "📋", in_progress: "🔄", backlog: "📥" };
124
- const lines = [escapeMarkdownV2("📋") + " *Open Issues*", ""];
125
- for (const issue of filtered) {
126
- const emoji = statusEmoji[issue.status] ?? "📋";
127
- const id = issue.identifier ?? issue.id;
128
- lines.push(`${escapeMarkdownV2(emoji)} ${escapeMarkdownV2(id)} \\- ${escapeMarkdownV2(issue.title)}`);
129
- }
130
-
131
- await sendMessage(ctx, token, chatId, lines.join("\n"), {
132
- parseMode: "MarkdownV2",
133
- messageThreadId,
134
- });
135
- } catch {
136
- const filter = projectFilter ? ` for project "${projectFilter}"` : "";
137
- await sendMessage(
138
- ctx,
139
- token,
140
- chatId,
141
- `Could not fetch issues${filter}. Make sure this chat is linked with /connect.`,
142
- { messageThreadId },
143
- );
144
- }
145
- }
146
-
147
- async function handleAgents(
148
- ctx: PluginContext,
149
- token: string,
150
- chatId: string,
151
- messageThreadId?: number,
152
- ): Promise<void> {
153
- await sendChatAction(ctx, token, chatId);
154
-
155
- try {
156
- const companyId = await resolveCompanyId(ctx, chatId);
157
- const agents = await ctx.agents.list({ companyId });
158
-
159
- if (agents.length === 0) {
160
- await sendMessage(ctx, token, chatId, "No agents found.", { messageThreadId });
161
- return;
162
- }
163
-
164
- const statusEmoji: Record<string, string> = { active: "🟢", error: "🔴", paused: "🟡", idle: "⚪", running: "🔵" };
165
- const lines = [escapeMarkdownV2("🤖") + " *Agents*", ""];
166
- for (const agent of agents) {
167
- const emoji = statusEmoji[agent.status] ?? "⚪";
168
- lines.push(`${escapeMarkdownV2(emoji)} *${escapeMarkdownV2(agent.name)}* \\- ${escapeMarkdownV2(agent.status)}`);
169
- }
170
-
171
- await sendMessage(ctx, token, chatId, lines.join("\n"), {
172
- parseMode: "MarkdownV2",
173
- messageThreadId,
174
- });
175
- } catch {
176
- await sendMessage(
177
- ctx,
178
- token,
179
- chatId,
180
- "Could not fetch agents. Make sure this chat is linked with /connect.",
181
- { messageThreadId },
182
- );
183
- }
184
- }
185
-
186
- async function handleApprove(
187
- ctx: PluginContext,
188
- token: string,
189
- chatId: string,
190
- approvalId: string,
191
- messageThreadId?: number,
192
- baseUrl: string = "http://localhost:3100",
193
- ): Promise<void> {
194
- if (!approvalId.trim()) {
195
- await sendMessage(ctx, token, chatId, "Usage: /approve <approval-id>", {
196
- messageThreadId,
197
- });
198
- return;
199
- }
200
-
201
- try {
202
- await ctx.http.fetch(
203
- `${baseUrl}/api/approvals/${approvalId.trim()}/approve`,
204
- {
205
- method: "POST",
206
- headers: { "Content-Type": "application/json" },
207
- body: JSON.stringify({ decidedByUserId: `telegram:${chatId}` }),
208
- },
209
- );
210
-
211
- await sendMessage(
212
- ctx,
213
- token,
214
- chatId,
215
- `${escapeMarkdownV2("✅")} *Approved*: \`${escapeMarkdownV2(approvalId.trim())}\``,
216
- { parseMode: "MarkdownV2", messageThreadId },
217
- );
218
- } catch (err) {
219
- await sendMessage(
220
- ctx,
221
- token,
222
- chatId,
223
- `Failed to approve ${approvalId}: ${err instanceof Error ? err.message : String(err)}`,
224
- { messageThreadId },
225
- );
226
- }
227
- }
228
-
229
- async function handleHelp(
230
- ctx: PluginContext,
231
- token: string,
232
- chatId: string,
233
- messageThreadId?: number,
234
- ): Promise<void> {
235
- const lines = [
236
- escapeMarkdownV2("📎") + " *Paperclip Bot Commands*",
237
- "",
238
- ...BOT_COMMANDS.map(
239
- (cmd) => `/${escapeMarkdownV2(cmd.command)} \\- ${escapeMarkdownV2(cmd.description)}`,
240
- ),
241
- "",
242
- `/${escapeMarkdownV2("connect")} \\- ${escapeMarkdownV2("Link this chat to a Paperclip company")}`,
243
- `/${escapeMarkdownV2("connect-topic")} \\- ${escapeMarkdownV2("Map a project to a forum topic")}`,
244
- ];
245
-
246
- await sendMessage(ctx, token, chatId, lines.join("\n"), {
247
- parseMode: "MarkdownV2",
248
- messageThreadId,
249
- });
250
- }
251
-
252
- async function handleConnect(
253
- ctx: PluginContext,
254
- token: string,
255
- chatId: string,
256
- companyName: string,
257
- messageThreadId?: number,
258
- ): Promise<void> {
259
- if (!companyName.trim()) {
260
- await sendMessage(ctx, token, chatId, "Usage: /connect <company-name>", {
261
- messageThreadId,
262
- });
263
- return;
264
- }
265
-
266
- await ctx.state.set(
267
- { scopeKind: "instance", stateKey: `chat_${chatId}` },
268
- { companyName: companyName.trim(), linkedAt: new Date().toISOString() },
269
- );
270
-
271
- await sendMessage(
272
- ctx,
273
- token,
274
- chatId,
275
- `${escapeMarkdownV2("🔗")} ${escapeMarkdownV2("Linked this chat to company:")} *${escapeMarkdownV2(companyName.trim())}*`,
276
- { parseMode: "MarkdownV2", messageThreadId },
277
- );
278
-
279
- ctx.logger.info("Chat linked to company", { chatId, companyName: companyName.trim() });
280
- }
281
-
282
- export async function handleConnectTopic(
283
- ctx: PluginContext,
284
- token: string,
285
- chatId: string,
286
- args: string,
287
- messageThreadId?: number,
288
- ): Promise<void> {
289
- const parts = args.trim().split(/\s+/);
290
- if (parts.length < 2) {
291
- await sendMessage(ctx, token, chatId, "Usage: /connect\\-topic <project\\-name> <topic\\-id>", {
292
- parseMode: "MarkdownV2",
293
- messageThreadId,
294
- });
295
- return;
296
- }
297
-
298
- const topicId = parts.pop()!;
299
- const projectName = parts.join(" ");
300
-
301
- const existing = (await ctx.state.get({
302
- scopeKind: "instance",
303
- stateKey: `topic-map-${chatId}`,
304
- })) as Record<string, string> | null;
305
-
306
- const topicMap = existing ?? {};
307
- topicMap[projectName] = topicId;
308
-
309
- await ctx.state.set(
310
- { scopeKind: "instance", stateKey: `topic-map-${chatId}` },
311
- topicMap,
312
- );
313
-
314
- await sendMessage(
315
- ctx,
316
- token,
317
- chatId,
318
- `${escapeMarkdownV2("🔗")} ${escapeMarkdownV2(`Mapped project "${projectName}" to topic ${topicId}`)}`,
319
- { parseMode: "MarkdownV2", messageThreadId },
320
- );
321
-
322
- ctx.logger.info("Topic mapped", { chatId, projectName, topicId });
323
- }
324
-
325
- export async function getTopicForProject(
326
- ctx: PluginContext,
327
- chatId: string,
328
- projectName?: string,
329
- ): Promise<number | undefined> {
330
- if (!projectName) return undefined;
331
- const topicMap = (await ctx.state.get({
332
- scopeKind: "instance",
333
- stateKey: `topic-map-${chatId}`,
334
- })) as Record<string, string> | null;
335
- if (!topicMap) return undefined;
336
- const topicId = topicMap[projectName];
337
- return topicId ? Number(topicId) : undefined;
338
- }
339
-
340
- async function resolveCompanyId(ctx: PluginContext, chatId: string): Promise<string> {
341
- const mapping = await ctx.state.get({
342
- scopeKind: "instance",
343
- stateKey: `chat_${chatId}`,
344
- }) as { companyName: string } | null;
345
- return mapping?.companyName ?? chatId;
346
- }
package/src/constants.ts DELETED
@@ -1,51 +0,0 @@
1
- export const PLUGIN_ID = "paperclip-plugin-telegram";
2
- export const PLUGIN_VERSION = "0.2.0";
3
-
4
- export const DEFAULT_CONFIG = {
5
- telegramBotTokenRef: "",
6
- defaultChatId: "",
7
- approvalsChatId: "",
8
- errorsChatId: "",
9
- paperclipBaseUrl: "http://localhost:3100",
10
- notifyOnIssueCreated: true,
11
- notifyOnIssueDone: true,
12
- notifyOnApprovalCreated: true,
13
- notifyOnAgentError: true,
14
- enableCommands: true,
15
- enableInbound: true,
16
- dailyDigestEnabled: false,
17
- dailyDigestTime: "09:00",
18
- topicRouting: false,
19
- escalationChatId: "",
20
- escalationTimeoutMs: 900000,
21
- escalationDefaultAction: "defer",
22
- escalationHoldMessage: "Let me check on that - I'll get back to you shortly.",
23
- // Phase 3: Media Pipeline
24
- briefAgentId: "",
25
- briefAgentChatIds: [] as string[],
26
- transcriptionApiKeyRef: "",
27
- // Phase 5: Proactive Suggestions
28
- maxSuggestionsPerHourPerCompany: 10,
29
- watchDeduplicationWindowMs: 86400000, // 24h
30
- } as const;
31
-
32
- export const MAX_AGENTS_PER_THREAD = 5;
33
- export const MAX_CONVERSATION_TURNS = 50;
34
- export const DEFAULT_CONVERSATION_TURNS = 10;
35
-
36
- export const METRIC_NAMES = {
37
- sent: "telegram_notifications_sent",
38
- failed: "telegram_notification_failures",
39
- commandsHandled: "telegram_commands_handled",
40
- inboundRouted: "telegram_inbound_routed",
41
- escalationsCreated: "telegram_escalations_created",
42
- escalationsResolved: "telegram_escalations_resolved",
43
- escalationsTimedOut: "telegram_escalations_timed_out",
44
- mediaProcessed: "telegram_media_processed",
45
- commandsExecuted: "telegram_custom_commands_executed",
46
- suggestionsEmitted: "telegram_suggestions_emitted",
47
- } as const;
48
-
49
- // Cross-plugin ACP event names
50
- export const ACP_SPAWN_EVENT = "acp-spawn";
51
- export const ACP_OUTPUT_EVENT = "plugin.paperclip-plugin-acp.output";