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
@@ -1,404 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import { handleRegisterWatch, checkWatches } from "../src/watch-registry.js";
3
- import type { PluginContext } from "@paperclipai/plugin-sdk";
4
-
5
- let sentMessages: Array<{ chatId: string; text: string; options?: Record<string, unknown> }> = [];
6
- let stateStore: Record<string, unknown> = {};
7
-
8
- vi.mock("../src/telegram-api.js", async () => {
9
- const actual = await vi.importActual("../src/telegram-api.js") as Record<string, unknown>;
10
- return {
11
- ...actual,
12
- sendMessage: vi.fn(async (_ctx: unknown, _token: string, chatId: string, text: string, options?: Record<string, unknown>) => {
13
- sentMessages.push({ chatId, text, options });
14
- return 1;
15
- }),
16
- };
17
- });
18
-
19
- function mockCtx(overrides: Record<string, unknown> = {}): PluginContext {
20
- return {
21
- http: { fetch: vi.fn() },
22
- metrics: { write: vi.fn() },
23
- state: {
24
- get: vi.fn(async (key: { stateKey: string }) => stateStore[key.stateKey] ?? null),
25
- set: vi.fn(async (key: { stateKey: string }, value: unknown) => {
26
- stateStore[key.stateKey] = value;
27
- }),
28
- },
29
- logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
30
- companies: {
31
- list: vi.fn().mockResolvedValue([{ id: "co-1" }]),
32
- },
33
- issues: {
34
- list: vi.fn().mockResolvedValue([]),
35
- },
36
- agents: {
37
- list: vi.fn().mockResolvedValue([]),
38
- },
39
- ...overrides,
40
- } as unknown as PluginContext;
41
- }
42
-
43
- beforeEach(() => {
44
- sentMessages = [];
45
- stateStore = {};
46
- });
47
-
48
- describe("handleRegisterWatch - storage and retrieval", () => {
49
- it("registers a custom watch and stores it", async () => {
50
- const ctx = mockCtx();
51
- const result = await handleRegisterWatch(ctx, {
52
- name: "Invoice Alert",
53
- description: "Alert on overdue invoices",
54
- entityType: "custom",
55
- conditions: [{ field: "status", operator: "eq", value: "overdue" }],
56
- template: "Invoice {{entityId}} is overdue",
57
- chatId: "chat-1",
58
- }, "co-1");
59
-
60
- expect(result.content).toBeDefined();
61
- const parsed = JSON.parse(result.content!);
62
- expect(parsed.status).toBe("registered");
63
- expect(parsed.watchId).toBeDefined();
64
- expect(parsed.name).toBe("Invoice Alert");
65
-
66
- // Check it was stored
67
- const watches = stateStore["watches_co-1"] as Array<{ name: string }>;
68
- expect(watches).toHaveLength(1);
69
- expect(watches[0].name).toBe("Invoice Alert");
70
- });
71
-
72
- it("registers a watch using built-in template", async () => {
73
- const ctx = mockCtx();
74
- const result = await handleRegisterWatch(ctx, {
75
- builtinTemplate: "invoice-overdue",
76
- chatId: "chat-1",
77
- }, "co-1");
78
-
79
- expect(result.content).toBeDefined();
80
- const parsed = JSON.parse(result.content!);
81
- expect(parsed.status).toBe("registered");
82
- expect(parsed.name).toBe("Invoice Overdue");
83
- });
84
-
85
- it("returns error when no name or builtinTemplate", async () => {
86
- const ctx = mockCtx();
87
- const result = await handleRegisterWatch(ctx, {
88
- chatId: "chat-1",
89
- }, "co-1");
90
-
91
- expect(result.error).toContain("Either 'name' or 'builtinTemplate' is required");
92
- });
93
-
94
- it("returns error when chatId is missing", async () => {
95
- const ctx = mockCtx();
96
- const result = await handleRegisterWatch(ctx, {
97
- name: "test",
98
- template: "test",
99
- }, "co-1");
100
-
101
- expect(result.error).toContain("chatId");
102
- });
103
-
104
- it("returns error when custom watch has no template", async () => {
105
- const ctx = mockCtx();
106
- const result = await handleRegisterWatch(ctx, {
107
- name: "test",
108
- chatId: "chat-1",
109
- }, "co-1");
110
-
111
- expect(result.error).toContain("template");
112
- });
113
-
114
- it("appends to existing watch registry", async () => {
115
- stateStore["watches_co-1"] = [{
116
- watchId: "w1",
117
- name: "Existing",
118
- description: "",
119
- entityType: "custom",
120
- conditions: [],
121
- template: "t",
122
- chatId: "c",
123
- companyId: "co-1",
124
- createdBy: "test",
125
- createdAt: "2026-01-01",
126
- }];
127
- const ctx = mockCtx();
128
- await handleRegisterWatch(ctx, {
129
- name: "New Watch",
130
- template: "new template",
131
- chatId: "chat-1",
132
- }, "co-1");
133
-
134
- const watches = stateStore["watches_co-1"] as unknown[];
135
- expect(watches).toHaveLength(2);
136
- });
137
- });
138
-
139
- describe("checkWatches - rate limiting", () => {
140
- it("suppresses when hourly count exceeds max", async () => {
141
- const hourKey = `suggestion_hourly_co-1_${new Date().toISOString().slice(0, 13)}`;
142
- stateStore[hourKey] = 11; // over limit of 10
143
-
144
- stateStore["watches_co-1"] = [{
145
- watchId: "w1",
146
- name: "Test",
147
- description: "",
148
- entityType: "issue",
149
- conditions: [],
150
- template: "Alert: {{id}}",
151
- chatId: "chat-1",
152
- companyId: "co-1",
153
- createdBy: "agent",
154
- createdAt: "2026-01-01",
155
- }];
156
-
157
- const ctx = mockCtx({
158
- issues: { list: vi.fn().mockResolvedValue([{ id: "i1", title: "Test", status: "open" }]) },
159
- });
160
-
161
- await checkWatches(ctx, "token", {
162
- maxSuggestionsPerHourPerCompany: 10,
163
- watchDeduplicationWindowMs: 86400000,
164
- });
165
-
166
- // Rate limited, so no messages sent
167
- expect(sentMessages.length).toBe(0);
168
- });
169
-
170
- it("sends suggestion when under rate limit", async () => {
171
- stateStore["watches_co-1"] = [{
172
- watchId: "w1",
173
- name: "Active Issues",
174
- description: "",
175
- entityType: "issue",
176
- conditions: [{ field: "status", operator: "eq", value: "open" }],
177
- template: "Issue {{id}} needs attention",
178
- chatId: "chat-1",
179
- companyId: "co-1",
180
- createdBy: "agent",
181
- createdAt: "2026-01-01",
182
- }];
183
-
184
- const ctx = mockCtx({
185
- issues: { list: vi.fn().mockResolvedValue([{ id: "i1", title: "Test", status: "open" }]) },
186
- });
187
-
188
- await checkWatches(ctx, "token", {
189
- maxSuggestionsPerHourPerCompany: 10,
190
- watchDeduplicationWindowMs: 86400000,
191
- });
192
-
193
- expect(sentMessages.length).toBe(1);
194
- expect(sentMessages[0].text).toContain("needs attention");
195
- });
196
- });
197
-
198
- describe("checkWatches - deduplication", () => {
199
- it("suppresses same watch+entity within dedup window", async () => {
200
- stateStore["watches_co-1"] = [{
201
- watchId: "w1",
202
- name: "Active Issues",
203
- description: "",
204
- entityType: "issue",
205
- conditions: [{ field: "status", operator: "eq", value: "open" }],
206
- template: "Issue {{id}} needs attention",
207
- chatId: "chat-1",
208
- companyId: "co-1",
209
- createdBy: "agent",
210
- createdAt: "2026-01-01",
211
- }];
212
-
213
- // Mark as already sent within the window
214
- stateStore["suggestion_log_w1_i1"] = {
215
- watchId: "w1",
216
- entityId: "i1",
217
- sentAt: new Date().toISOString(), // just now
218
- };
219
-
220
- const ctx = mockCtx({
221
- issues: { list: vi.fn().mockResolvedValue([{ id: "i1", title: "Test", status: "open" }]) },
222
- });
223
-
224
- await checkWatches(ctx, "token", {
225
- maxSuggestionsPerHourPerCompany: 10,
226
- watchDeduplicationWindowMs: 86400000, // 24h
227
- });
228
-
229
- expect(sentMessages.length).toBe(0);
230
- });
231
-
232
- it("allows same watch+entity after dedup window expires", async () => {
233
- stateStore["watches_co-1"] = [{
234
- watchId: "w1",
235
- name: "Active Issues",
236
- description: "",
237
- entityType: "issue",
238
- conditions: [{ field: "status", operator: "eq", value: "open" }],
239
- template: "Issue {{id}} needs attention",
240
- chatId: "chat-1",
241
- companyId: "co-1",
242
- createdBy: "agent",
243
- createdAt: "2026-01-01",
244
- }];
245
-
246
- // Sent 25 hours ago - outside 24h window
247
- stateStore["suggestion_log_w1_i1"] = {
248
- watchId: "w1",
249
- entityId: "i1",
250
- sentAt: new Date(Date.now() - 25 * 60 * 60 * 1000).toISOString(),
251
- };
252
-
253
- const ctx = mockCtx({
254
- issues: { list: vi.fn().mockResolvedValue([{ id: "i1", title: "Test", status: "open" }]) },
255
- });
256
-
257
- await checkWatches(ctx, "token", {
258
- maxSuggestionsPerHourPerCompany: 10,
259
- watchDeduplicationWindowMs: 86400000,
260
- });
261
-
262
- expect(sentMessages.length).toBe(1);
263
- });
264
- });
265
-
266
- describe("Built-in template loading", () => {
267
- it("loads invoice-overdue template with correct conditions", async () => {
268
- const ctx = mockCtx();
269
- const result = await handleRegisterWatch(ctx, {
270
- builtinTemplate: "invoice-overdue",
271
- chatId: "chat-1",
272
- }, "co-1");
273
-
274
- const watches = stateStore["watches_co-1"] as Array<{
275
- name: string;
276
- conditions: Array<{ field: string; operator: string }>;
277
- }>;
278
- expect(watches[0].name).toBe("Invoice Overdue");
279
- expect(watches[0].conditions.length).toBe(2);
280
- expect(watches[0].conditions[0].field).toBe("dueDate");
281
- expect(watches[0].conditions[1].field).toBe("status");
282
- });
283
-
284
- it("loads lead-stale template with correct conditions", async () => {
285
- const ctx = mockCtx();
286
- const result = await handleRegisterWatch(ctx, {
287
- builtinTemplate: "lead-stale",
288
- chatId: "chat-1",
289
- }, "co-1");
290
-
291
- const watches = stateStore["watches_co-1"] as Array<{
292
- name: string;
293
- conditions: Array<{ field: string; operator: string }>;
294
- }>;
295
- expect(watches[0].name).toBe("Stale Lead");
296
- expect(watches[0].conditions.length).toBe(2);
297
- expect(watches[0].conditions[0].field).toBe("lastActivityAt");
298
- expect(watches[0].conditions[1].field).toBe("status");
299
- });
300
-
301
- it("returns error for unknown built-in template (falls through to custom)", async () => {
302
- const ctx = mockCtx();
303
- const result = await handleRegisterWatch(ctx, {
304
- builtinTemplate: "nonexistent-template",
305
- chatId: "chat-1",
306
- }, "co-1");
307
-
308
- // Will fail because no name and no valid builtin
309
- expect(result.error).toContain("template");
310
- });
311
- });
312
-
313
- describe("Watch condition matching", () => {
314
- it("matches issues with eq condition", async () => {
315
- stateStore["watches_co-1"] = [{
316
- watchId: "w1",
317
- name: "Open Issues",
318
- description: "",
319
- entityType: "issue",
320
- conditions: [{ field: "status", operator: "eq", value: "open" }],
321
- template: "Issue {{id}} is open",
322
- chatId: "chat-1",
323
- companyId: "co-1",
324
- createdBy: "agent",
325
- createdAt: "2026-01-01",
326
- }];
327
-
328
- const ctx = mockCtx({
329
- issues: { list: vi.fn().mockResolvedValue([
330
- { id: "i1", status: "open" },
331
- { id: "i2", status: "closed" },
332
- ]) },
333
- });
334
-
335
- await checkWatches(ctx, "token", {
336
- maxSuggestionsPerHourPerCompany: 10,
337
- watchDeduplicationWindowMs: 86400000,
338
- });
339
-
340
- // Only i1 should match
341
- expect(sentMessages.length).toBe(1);
342
- expect(sentMessages[0].text).toContain("i1");
343
- });
344
-
345
- it("matches issues with ne condition", async () => {
346
- stateStore["watches_co-1"] = [{
347
- watchId: "w1",
348
- name: "Non-closed Issues",
349
- description: "",
350
- entityType: "issue",
351
- conditions: [{ field: "status", operator: "ne", value: "closed" }],
352
- template: "Issue {{id}} is not closed",
353
- chatId: "chat-1",
354
- companyId: "co-1",
355
- createdBy: "agent",
356
- createdAt: "2026-01-01",
357
- }];
358
-
359
- const ctx = mockCtx({
360
- issues: { list: vi.fn().mockResolvedValue([
361
- { id: "i1", status: "open" },
362
- { id: "i2", status: "closed" },
363
- ]) },
364
- });
365
-
366
- await checkWatches(ctx, "token", {
367
- maxSuggestionsPerHourPerCompany: 10,
368
- watchDeduplicationWindowMs: 86400000,
369
- });
370
-
371
- expect(sentMessages.length).toBe(1);
372
- expect(sentMessages[0].text).toContain("i1");
373
- });
374
-
375
- it("matches issues with contains condition", async () => {
376
- stateStore["watches_co-1"] = [{
377
- watchId: "w1",
378
- name: "Bug Issues",
379
- description: "",
380
- entityType: "issue",
381
- conditions: [{ field: "title", operator: "contains", value: "bug" }],
382
- template: "Bug: {{id}}",
383
- chatId: "chat-1",
384
- companyId: "co-1",
385
- createdBy: "agent",
386
- createdAt: "2026-01-01",
387
- }];
388
-
389
- const ctx = mockCtx({
390
- issues: { list: vi.fn().mockResolvedValue([
391
- { id: "i1", title: "Fix bug in login" },
392
- { id: "i2", title: "Add feature" },
393
- ]) },
394
- });
395
-
396
- await checkWatches(ctx, "token", {
397
- maxSuggestionsPerHourPerCompany: 10,
398
- watchDeduplicationWindowMs: 86400000,
399
- });
400
-
401
- expect(sentMessages.length).toBe(1);
402
- expect(sentMessages[0].text).toContain("i1");
403
- });
404
- });
package/tsconfig.json DELETED
@@ -1,16 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "Node16",
5
- "moduleResolution": "Node16",
6
- "outDir": "./dist",
7
- "rootDir": "./src",
8
- "strict": true,
9
- "esModuleInterop": true,
10
- "skipLibCheck": true,
11
- "declaration": true,
12
- "sourceMap": true
13
- },
14
- "include": ["src/**/*.ts"],
15
- "exclude": ["node_modules", "dist"]
16
- }
File without changes