paperclip-plugin-telegram 0.2.1 → 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 (61) hide show
  1. package/dist/acp-bridge.d.ts +34 -0
  2. package/dist/acp-bridge.js +805 -0
  3. package/dist/acp-bridge.js.map +1 -0
  4. package/dist/adapter.d.ts +35 -0
  5. package/dist/adapter.js +75 -0
  6. package/dist/adapter.js.map +1 -0
  7. package/dist/command-registry.d.ts +3 -0
  8. package/dist/command-registry.js +273 -0
  9. package/dist/command-registry.js.map +1 -0
  10. package/dist/commands.d.ts +10 -0
  11. package/dist/commands.js +213 -0
  12. package/dist/commands.js.map +1 -0
  13. package/dist/constants.d.ts +44 -0
  14. package/dist/constants.js +48 -0
  15. package/dist/constants.js.map +1 -0
  16. package/dist/escalation.d.ts +41 -0
  17. package/dist/escalation.js +254 -0
  18. package/dist/escalation.js.map +1 -0
  19. package/dist/formatters.d.ts +13 -0
  20. package/dist/formatters.js +130 -0
  21. package/dist/formatters.js.map +1 -0
  22. package/dist/index.js +4 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/manifest.d.ts +3 -0
  25. package/dist/manifest.js +230 -0
  26. package/dist/manifest.js.map +1 -0
  27. package/dist/media-pipeline.d.ts +46 -0
  28. package/dist/media-pipeline.js +161 -0
  29. package/dist/media-pipeline.js.map +1 -0
  30. package/dist/telegram-api.d.ts +28 -0
  31. package/dist/telegram-api.js +147 -0
  32. package/dist/telegram-api.js.map +1 -0
  33. package/dist/watch-registry.d.ts +9 -0
  34. package/dist/watch-registry.js +272 -0
  35. package/dist/watch-registry.js.map +1 -0
  36. package/dist/worker.d.ts +1 -0
  37. package/dist/worker.js +548 -0
  38. package/dist/worker.js.map +1 -0
  39. package/package.json +7 -3
  40. package/src/acp-bridge.ts +0 -1273
  41. package/src/adapter.ts +0 -129
  42. package/src/command-registry.ts +0 -482
  43. package/src/commands.ts +0 -346
  44. package/src/constants.ts +0 -51
  45. package/src/escalation.ts +0 -421
  46. package/src/formatters.ts +0 -148
  47. package/src/manifest.ts +0 -246
  48. package/src/media-pipeline.ts +0 -234
  49. package/src/telegram-api.ts +0 -202
  50. package/src/watch-registry.ts +0 -369
  51. package/src/worker.ts +0 -783
  52. package/tests/acp-bridge.test.ts +0 -314
  53. package/tests/command-registry.test.ts +0 -283
  54. package/tests/commands.test.ts +0 -213
  55. package/tests/escalation.test.ts +0 -550
  56. package/tests/formatters.test.ts +0 -185
  57. package/tests/media-pipeline.test.ts +0 -324
  58. package/tests/telegram-api.test.ts +0 -108
  59. package/tests/watch-registry.test.ts +0 -404
  60. package/tsconfig.json +0 -16
  61. /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