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.
- package/README.md +68 -4
- package/dist/acp-bridge.d.ts +34 -0
- package/dist/acp-bridge.js +805 -0
- package/dist/acp-bridge.js.map +1 -0
- package/dist/adapter.d.ts +35 -0
- package/dist/adapter.js +75 -0
- package/dist/adapter.js.map +1 -0
- package/dist/command-registry.d.ts +3 -0
- package/dist/command-registry.js +273 -0
- package/dist/command-registry.js.map +1 -0
- package/dist/commands.d.ts +10 -0
- package/dist/commands.js +213 -0
- package/dist/commands.js.map +1 -0
- package/dist/constants.d.ts +44 -0
- package/dist/constants.js +48 -0
- package/dist/constants.js.map +1 -0
- package/dist/escalation.d.ts +41 -0
- package/dist/escalation.js +254 -0
- package/dist/escalation.js.map +1 -0
- package/dist/formatters.d.ts +13 -0
- package/dist/formatters.js +130 -0
- package/dist/formatters.js.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.d.ts +3 -0
- package/dist/manifest.js +230 -0
- package/dist/manifest.js.map +1 -0
- package/dist/media-pipeline.d.ts +46 -0
- package/dist/media-pipeline.js +161 -0
- package/dist/media-pipeline.js.map +1 -0
- package/dist/telegram-api.d.ts +28 -0
- package/dist/telegram-api.js +147 -0
- package/dist/telegram-api.js.map +1 -0
- package/dist/watch-registry.d.ts +9 -0
- package/dist/watch-registry.js +272 -0
- package/dist/watch-registry.js.map +1 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +548 -0
- package/dist/worker.js.map +1 -0
- package/package.json +7 -3
- package/src/acp-bridge.ts +0 -1273
- package/src/adapter.ts +0 -129
- package/src/command-registry.ts +0 -482
- package/src/commands.ts +0 -346
- package/src/constants.ts +0 -51
- package/src/escalation.ts +0 -421
- package/src/formatters.ts +0 -148
- package/src/manifest.ts +0 -246
- package/src/media-pipeline.ts +0 -234
- package/src/telegram-api.ts +0 -202
- package/src/watch-registry.ts +0 -369
- package/src/worker.ts +0 -783
- package/tests/acp-bridge.test.ts +0 -314
- package/tests/command-registry.test.ts +0 -283
- package/tests/commands.test.ts +0 -213
- package/tests/escalation.test.ts +0 -550
- package/tests/formatters.test.ts +0 -185
- package/tests/media-pipeline.test.ts +0 -324
- package/tests/telegram-api.test.ts +0 -108
- package/tests/watch-registry.test.ts +0 -404
- package/tsconfig.json +0 -16
- /package/{src/index.ts → dist/index.d.ts} +0 -0
package/src/watch-registry.ts
DELETED
|
@@ -1,369 +0,0 @@
|
|
|
1
|
-
import type { PluginContext } from "@paperclipai/plugin-sdk";
|
|
2
|
-
import { sendMessage, escapeMarkdownV2 } from "./telegram-api.js";
|
|
3
|
-
import { METRIC_NAMES } from "./constants.js";
|
|
4
|
-
|
|
5
|
-
// --- Types ---
|
|
6
|
-
|
|
7
|
-
type WatchCondition = {
|
|
8
|
-
field: string;
|
|
9
|
-
operator: "gt" | "lt" | "eq" | "ne" | "contains" | "exists";
|
|
10
|
-
value: string | number | boolean;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
type Watch = {
|
|
14
|
-
watchId: string;
|
|
15
|
-
name: string;
|
|
16
|
-
description: string;
|
|
17
|
-
entityType: "issue" | "agent" | "company" | "custom";
|
|
18
|
-
conditions: WatchCondition[];
|
|
19
|
-
template: string; // Suggestion message template
|
|
20
|
-
chatId: string;
|
|
21
|
-
threadId?: number;
|
|
22
|
-
companyId: string;
|
|
23
|
-
createdBy: string;
|
|
24
|
-
createdAt: string;
|
|
25
|
-
lastTriggeredAt?: string;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
type SuggestionLog = {
|
|
29
|
-
watchId: string;
|
|
30
|
-
entityId: string;
|
|
31
|
-
sentAt: string;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
// Built-in watch templates
|
|
35
|
-
const BUILTIN_TEMPLATES: Record<string, Omit<Watch, "watchId" | "chatId" | "threadId" | "companyId" | "createdBy" | "createdAt">> = {
|
|
36
|
-
"invoice-overdue": {
|
|
37
|
-
name: "Invoice Overdue",
|
|
38
|
-
description: "Alert when invoices are past due",
|
|
39
|
-
entityType: "custom",
|
|
40
|
-
conditions: [
|
|
41
|
-
{ field: "dueDate", operator: "lt", value: "{{now}}" },
|
|
42
|
-
{ field: "status", operator: "ne", value: "paid" },
|
|
43
|
-
],
|
|
44
|
-
template: "Invoice {{entityId}} is overdue (due: {{dueDate}}). Consider sending a follow-up.",
|
|
45
|
-
},
|
|
46
|
-
"lead-stale": {
|
|
47
|
-
name: "Stale Lead",
|
|
48
|
-
description: "Alert when leads have no activity for 7+ days",
|
|
49
|
-
entityType: "custom",
|
|
50
|
-
conditions: [
|
|
51
|
-
{ field: "lastActivityAt", operator: "lt", value: "{{7daysAgo}}" },
|
|
52
|
-
{ field: "status", operator: "eq", value: "active" },
|
|
53
|
-
],
|
|
54
|
-
template: "Lead {{entityId}} has been inactive for 7+ days. Consider re-engagement.",
|
|
55
|
-
},
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
// --- Register watch tool handler ---
|
|
59
|
-
|
|
60
|
-
export async function handleRegisterWatch(
|
|
61
|
-
ctx: PluginContext,
|
|
62
|
-
params: Record<string, unknown>,
|
|
63
|
-
companyId: string,
|
|
64
|
-
): Promise<{ content?: string; error?: string }> {
|
|
65
|
-
const name = String(params.name ?? "");
|
|
66
|
-
const description = String(params.description ?? "");
|
|
67
|
-
const entityType = String(params.entityType ?? "custom") as Watch["entityType"];
|
|
68
|
-
const conditions = (params.conditions as WatchCondition[] | undefined) ?? [];
|
|
69
|
-
const template = String(params.template ?? "");
|
|
70
|
-
const chatId = String(params.chatId ?? "");
|
|
71
|
-
const threadId = params.threadId ? Number(params.threadId) : undefined;
|
|
72
|
-
const useBuiltin = params.builtinTemplate ? String(params.builtinTemplate) : undefined;
|
|
73
|
-
|
|
74
|
-
if (!name && !useBuiltin) {
|
|
75
|
-
return { error: "Either 'name' or 'builtinTemplate' is required" };
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (!chatId) {
|
|
79
|
-
return { error: "'chatId' is required" };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
let watch: Watch;
|
|
83
|
-
const watchId = `watch_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
84
|
-
|
|
85
|
-
if (useBuiltin && BUILTIN_TEMPLATES[useBuiltin]) {
|
|
86
|
-
const builtin = BUILTIN_TEMPLATES[useBuiltin]!;
|
|
87
|
-
watch = {
|
|
88
|
-
...builtin,
|
|
89
|
-
watchId,
|
|
90
|
-
chatId,
|
|
91
|
-
threadId,
|
|
92
|
-
companyId,
|
|
93
|
-
createdBy: "agent",
|
|
94
|
-
createdAt: new Date().toISOString(),
|
|
95
|
-
};
|
|
96
|
-
} else {
|
|
97
|
-
if (!template) {
|
|
98
|
-
return { error: "'template' is required for custom watches" };
|
|
99
|
-
}
|
|
100
|
-
watch = {
|
|
101
|
-
watchId,
|
|
102
|
-
name,
|
|
103
|
-
description,
|
|
104
|
-
entityType,
|
|
105
|
-
conditions,
|
|
106
|
-
template,
|
|
107
|
-
chatId,
|
|
108
|
-
threadId,
|
|
109
|
-
companyId,
|
|
110
|
-
createdBy: "agent",
|
|
111
|
-
createdAt: new Date().toISOString(),
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const watches = await getWatchRegistry(ctx, companyId);
|
|
116
|
-
watches.push(watch);
|
|
117
|
-
await saveWatchRegistry(ctx, companyId, watches);
|
|
118
|
-
|
|
119
|
-
return { content: JSON.stringify({ status: "registered", watchId, name: watch.name }) };
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// --- Check watches job ---
|
|
123
|
-
|
|
124
|
-
export async function checkWatches(
|
|
125
|
-
ctx: PluginContext,
|
|
126
|
-
token: string,
|
|
127
|
-
config: { maxSuggestionsPerHourPerCompany: number; watchDeduplicationWindowMs: number },
|
|
128
|
-
): Promise<void> {
|
|
129
|
-
// Get all companies that have watches
|
|
130
|
-
const companies = await ctx.companies.list();
|
|
131
|
-
|
|
132
|
-
for (const company of companies) {
|
|
133
|
-
try {
|
|
134
|
-
await checkWatchesForCompany(ctx, token, company.id, config);
|
|
135
|
-
} catch (err) {
|
|
136
|
-
ctx.logger.error("Watch check failed for company", { companyId: company.id, error: String(err) });
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
async function checkWatchesForCompany(
|
|
142
|
-
ctx: PluginContext,
|
|
143
|
-
token: string,
|
|
144
|
-
companyId: string,
|
|
145
|
-
config: { maxSuggestionsPerHourPerCompany: number; watchDeduplicationWindowMs: number },
|
|
146
|
-
): Promise<void> {
|
|
147
|
-
const watches = await getWatchRegistry(ctx, companyId);
|
|
148
|
-
if (watches.length === 0) return;
|
|
149
|
-
|
|
150
|
-
// Rate limiting: check how many suggestions we've sent this hour
|
|
151
|
-
const hourlyCount = await getHourlySuggestionCount(ctx, companyId);
|
|
152
|
-
if (hourlyCount >= config.maxSuggestionsPerHourPerCompany) {
|
|
153
|
-
ctx.logger.info("Watch suggestions rate-limited for company", { companyId, hourlyCount });
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
let sentThisRun = 0;
|
|
158
|
-
|
|
159
|
-
for (const watch of watches) {
|
|
160
|
-
if (hourlyCount + sentThisRun >= config.maxSuggestionsPerHourPerCompany) break;
|
|
161
|
-
|
|
162
|
-
try {
|
|
163
|
-
const entities = await evaluateWatch(ctx, watch, companyId);
|
|
164
|
-
|
|
165
|
-
for (const entity of entities) {
|
|
166
|
-
if (hourlyCount + sentThisRun >= config.maxSuggestionsPerHourPerCompany) break;
|
|
167
|
-
|
|
168
|
-
// Dedup check
|
|
169
|
-
const isDuplicate = await checkDedup(ctx, watch.watchId, entity.id, config.watchDeduplicationWindowMs);
|
|
170
|
-
if (isDuplicate) continue;
|
|
171
|
-
|
|
172
|
-
// Send suggestion
|
|
173
|
-
const message = interpolateTemplate(watch.template, entity);
|
|
174
|
-
await sendMessage(
|
|
175
|
-
ctx,
|
|
176
|
-
token,
|
|
177
|
-
watch.chatId,
|
|
178
|
-
`${escapeMarkdownV2("\ud83d\udca1")} *Suggestion:* ${escapeMarkdownV2(watch.name)}\n\n${escapeMarkdownV2(message)}`,
|
|
179
|
-
{
|
|
180
|
-
parseMode: "MarkdownV2",
|
|
181
|
-
messageThreadId: watch.threadId,
|
|
182
|
-
},
|
|
183
|
-
);
|
|
184
|
-
|
|
185
|
-
// Record dedup
|
|
186
|
-
await recordSuggestion(ctx, watch.watchId, entity.id);
|
|
187
|
-
sentThisRun++;
|
|
188
|
-
|
|
189
|
-
// Update watch last triggered
|
|
190
|
-
watch.lastTriggeredAt = new Date().toISOString();
|
|
191
|
-
}
|
|
192
|
-
} catch (err) {
|
|
193
|
-
ctx.logger.error("Watch evaluation failed", { watchId: watch.watchId, error: String(err) });
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Persist updated watches
|
|
198
|
-
if (sentThisRun > 0) {
|
|
199
|
-
await saveWatchRegistry(ctx, companyId, watches);
|
|
200
|
-
await ctx.metrics.write(METRIC_NAMES.suggestionsEmitted, sentThisRun);
|
|
201
|
-
await incrementHourlySuggestionCount(ctx, companyId, sentThisRun);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// --- Watch evaluation ---
|
|
206
|
-
|
|
207
|
-
async function evaluateWatch(
|
|
208
|
-
ctx: PluginContext,
|
|
209
|
-
watch: Watch,
|
|
210
|
-
companyId: string,
|
|
211
|
-
): Promise<Array<{ id: string; [key: string]: unknown }>> {
|
|
212
|
-
const matches: Array<{ id: string; [key: string]: unknown }> = [];
|
|
213
|
-
|
|
214
|
-
switch (watch.entityType) {
|
|
215
|
-
case "issue": {
|
|
216
|
-
const issues = await ctx.issues.list({ companyId, limit: 100 });
|
|
217
|
-
for (const issue of issues) {
|
|
218
|
-
const record = issue as unknown as Record<string, unknown>;
|
|
219
|
-
if (matchesConditions(record, watch.conditions)) {
|
|
220
|
-
matches.push({ id: issue.id, ...record });
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
break;
|
|
224
|
-
}
|
|
225
|
-
case "agent": {
|
|
226
|
-
const agents = await ctx.agents.list({ companyId });
|
|
227
|
-
for (const agent of agents) {
|
|
228
|
-
const record = agent as unknown as Record<string, unknown>;
|
|
229
|
-
if (matchesConditions(record, watch.conditions)) {
|
|
230
|
-
matches.push({ id: agent.id, ...record });
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
break;
|
|
234
|
-
}
|
|
235
|
-
case "custom": {
|
|
236
|
-
// Custom watches check state-stored data
|
|
237
|
-
const customData = await ctx.state.get({
|
|
238
|
-
scopeKind: "company",
|
|
239
|
-
scopeId: companyId,
|
|
240
|
-
stateKey: `watch_data_${watch.watchId}`,
|
|
241
|
-
}) as Array<Record<string, unknown>> | null;
|
|
242
|
-
|
|
243
|
-
if (customData) {
|
|
244
|
-
for (const item of customData) {
|
|
245
|
-
if (matchesConditions(item, watch.conditions)) {
|
|
246
|
-
matches.push({ id: String(item.id ?? "unknown"), ...item });
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
break;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
return matches;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function matchesConditions(record: Record<string, unknown>, conditions: WatchCondition[]): boolean {
|
|
258
|
-
const now = Date.now();
|
|
259
|
-
const sevenDaysAgo = now - 7 * 24 * 60 * 60 * 1000;
|
|
260
|
-
|
|
261
|
-
for (const condition of conditions) {
|
|
262
|
-
const fieldValue = record[condition.field];
|
|
263
|
-
|
|
264
|
-
let compareValue = condition.value;
|
|
265
|
-
if (compareValue === "{{now}}") compareValue = new Date().toISOString();
|
|
266
|
-
if (compareValue === "{{7daysAgo}}") compareValue = new Date(sevenDaysAgo).toISOString();
|
|
267
|
-
|
|
268
|
-
switch (condition.operator) {
|
|
269
|
-
case "eq":
|
|
270
|
-
if (fieldValue !== compareValue) return false;
|
|
271
|
-
break;
|
|
272
|
-
case "ne":
|
|
273
|
-
if (fieldValue === compareValue) return false;
|
|
274
|
-
break;
|
|
275
|
-
case "gt":
|
|
276
|
-
if (!(Number(fieldValue) > Number(compareValue))) return false;
|
|
277
|
-
break;
|
|
278
|
-
case "lt":
|
|
279
|
-
if (!(String(fieldValue) < String(compareValue))) return false;
|
|
280
|
-
break;
|
|
281
|
-
case "contains":
|
|
282
|
-
if (!String(fieldValue ?? "").includes(String(compareValue))) return false;
|
|
283
|
-
break;
|
|
284
|
-
case "exists":
|
|
285
|
-
if ((fieldValue == null) !== !compareValue) return false;
|
|
286
|
-
break;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
return true;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function interpolateTemplate(template: string, entity: Record<string, unknown>): string {
|
|
293
|
-
let result = template;
|
|
294
|
-
for (const [key, value] of Object.entries(entity)) {
|
|
295
|
-
result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), String(value ?? ""));
|
|
296
|
-
}
|
|
297
|
-
return result;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// --- Dedup & rate limiting ---
|
|
301
|
-
|
|
302
|
-
async function checkDedup(
|
|
303
|
-
ctx: PluginContext,
|
|
304
|
-
watchId: string,
|
|
305
|
-
entityId: string,
|
|
306
|
-
windowMs: number,
|
|
307
|
-
): Promise<boolean> {
|
|
308
|
-
const log = await ctx.state.get({
|
|
309
|
-
scopeKind: "instance",
|
|
310
|
-
stateKey: `suggestion_log_${watchId}_${entityId}`,
|
|
311
|
-
}) as SuggestionLog | null;
|
|
312
|
-
|
|
313
|
-
if (!log) return false;
|
|
314
|
-
|
|
315
|
-
const sentAt = new Date(log.sentAt).getTime();
|
|
316
|
-
return Date.now() - sentAt < windowMs;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
async function recordSuggestion(
|
|
320
|
-
ctx: PluginContext,
|
|
321
|
-
watchId: string,
|
|
322
|
-
entityId: string,
|
|
323
|
-
): Promise<void> {
|
|
324
|
-
const log: SuggestionLog = {
|
|
325
|
-
watchId,
|
|
326
|
-
entityId,
|
|
327
|
-
sentAt: new Date().toISOString(),
|
|
328
|
-
};
|
|
329
|
-
await ctx.state.set(
|
|
330
|
-
{ scopeKind: "instance", stateKey: `suggestion_log_${watchId}_${entityId}` },
|
|
331
|
-
log,
|
|
332
|
-
);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
async function getHourlySuggestionCount(ctx: PluginContext, companyId: string): Promise<number> {
|
|
336
|
-
const key = `suggestion_hourly_${companyId}_${new Date().toISOString().slice(0, 13)}`;
|
|
337
|
-
const count = await ctx.state.get({
|
|
338
|
-
scopeKind: "instance",
|
|
339
|
-
stateKey: key,
|
|
340
|
-
}) as number | null;
|
|
341
|
-
return count ?? 0;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
async function incrementHourlySuggestionCount(ctx: PluginContext, companyId: string, amount: number): Promise<void> {
|
|
345
|
-
const key = `suggestion_hourly_${companyId}_${new Date().toISOString().slice(0, 13)}`;
|
|
346
|
-
const current = await getHourlySuggestionCount(ctx, companyId);
|
|
347
|
-
await ctx.state.set(
|
|
348
|
-
{ scopeKind: "instance", stateKey: key },
|
|
349
|
-
current + amount,
|
|
350
|
-
);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// --- State helpers ---
|
|
354
|
-
|
|
355
|
-
async function getWatchRegistry(ctx: PluginContext, companyId: string): Promise<Watch[]> {
|
|
356
|
-
const watches = await ctx.state.get({
|
|
357
|
-
scopeKind: "company",
|
|
358
|
-
scopeId: companyId,
|
|
359
|
-
stateKey: `watches_${companyId}`,
|
|
360
|
-
}) as Watch[] | null;
|
|
361
|
-
return watches ?? [];
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
async function saveWatchRegistry(ctx: PluginContext, companyId: string, watches: Watch[]): Promise<void> {
|
|
365
|
-
await ctx.state.set(
|
|
366
|
-
{ scopeKind: "company", scopeId: companyId, stateKey: `watches_${companyId}` },
|
|
367
|
-
watches,
|
|
368
|
-
);
|
|
369
|
-
}
|