loopwork 0.3.0 → 0.3.1

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 (46) hide show
  1. package/bin/loopwork +0 -0
  2. package/package.json +48 -4
  3. package/src/backends/github.ts +6 -3
  4. package/src/backends/json.ts +28 -10
  5. package/src/commands/run.ts +2 -2
  6. package/src/contracts/config.ts +3 -75
  7. package/src/contracts/index.ts +0 -6
  8. package/src/core/cli.ts +25 -16
  9. package/src/core/state.ts +10 -4
  10. package/src/core/utils.ts +10 -4
  11. package/src/monitor/index.ts +56 -34
  12. package/src/plugins/index.ts +9 -131
  13. package/examples/README.md +0 -70
  14. package/examples/basic-json-backend/.specs/tasks/TASK-001.md +0 -22
  15. package/examples/basic-json-backend/.specs/tasks/TASK-002.md +0 -23
  16. package/examples/basic-json-backend/.specs/tasks/TASK-003.md +0 -37
  17. package/examples/basic-json-backend/.specs/tasks/tasks.json +0 -19
  18. package/examples/basic-json-backend/README.md +0 -32
  19. package/examples/basic-json-backend/TESTING.md +0 -184
  20. package/examples/basic-json-backend/hello.test.ts +0 -9
  21. package/examples/basic-json-backend/hello.ts +0 -3
  22. package/examples/basic-json-backend/loopwork.config.js +0 -35
  23. package/examples/basic-json-backend/math.test.ts +0 -29
  24. package/examples/basic-json-backend/math.ts +0 -3
  25. package/examples/basic-json-backend/package.json +0 -15
  26. package/examples/basic-json-backend/quick-start.sh +0 -80
  27. package/loopwork.config.ts +0 -164
  28. package/src/plugins/asana.ts +0 -192
  29. package/src/plugins/cost-tracking.ts +0 -402
  30. package/src/plugins/discord.ts +0 -269
  31. package/src/plugins/everhour.ts +0 -335
  32. package/src/plugins/telegram/bot.ts +0 -517
  33. package/src/plugins/telegram/index.ts +0 -6
  34. package/src/plugins/telegram/notifications.ts +0 -198
  35. package/src/plugins/todoist.ts +0 -261
  36. package/test/backends.test.ts +0 -929
  37. package/test/cli.test.ts +0 -145
  38. package/test/config.test.ts +0 -90
  39. package/test/e2e.test.ts +0 -458
  40. package/test/github-tasks.test.ts +0 -191
  41. package/test/loopwork-config-types.test.ts +0 -288
  42. package/test/monitor.test.ts +0 -123
  43. package/test/plugins.test.ts +0 -1175
  44. package/test/state.test.ts +0 -295
  45. package/test/utils.test.ts +0 -60
  46. package/tsconfig.json +0 -20
@@ -1,517 +0,0 @@
1
- /**
2
- * Telegram Bot for Loopwork Task Management
3
- *
4
- * Provides interactive task management through Telegram commands.
5
- *
6
- * Commands:
7
- * /tasks - List pending tasks
8
- * /task <id> - Get task details
9
- * /complete <id> - Mark task as completed
10
- * /fail <id> <reason> - Mark task as failed
11
- * /reset <id> - Reset task to pending
12
- * /status - Get loop status
13
- * /help - Show available commands
14
- *
15
- * Setup:
16
- * 1. Create a bot with @BotFather
17
- * 2. Set TELEGRAM_BOT_TOKEN env var
18
- * 3. Set TELEGRAM_CHAT_ID env var (your chat ID)
19
- * 4. Run: bun run src/telegram-bot.ts
20
- */
21
-
22
- import { createBackend, type TaskBackend, type Task } from '../../backends'
23
- import type { BackendConfig } from '../../backends/types'
24
-
25
- interface TelegramUpdate {
26
- update_id: number
27
- message?: {
28
- message_id: number
29
- from: { id: number; username?: string }
30
- chat: { id: number }
31
- text?: string
32
- date: number
33
- }
34
- }
35
-
36
- interface TelegramResponse {
37
- ok: boolean
38
- result?: TelegramUpdate[]
39
- description?: string
40
- }
41
-
42
- export class TelegramTaskBot {
43
- private botToken: string
44
- private allowedChatId: string
45
- private backend: TaskBackend
46
- private lastUpdateId = 0
47
- private running = false
48
-
49
- constructor(config: {
50
- botToken?: string
51
- chatId?: string
52
- backend?: BackendConfig
53
- } = {}) {
54
- this.botToken = config.botToken || process.env.TELEGRAM_BOT_TOKEN || ''
55
- this.allowedChatId = config.chatId || process.env.TELEGRAM_CHAT_ID || ''
56
-
57
- if (!this.botToken) {
58
- throw new Error('TELEGRAM_BOT_TOKEN is required')
59
- }
60
- if (!this.allowedChatId) {
61
- throw new Error('TELEGRAM_CHAT_ID is required')
62
- }
63
-
64
- // Auto-detect backend
65
- const backendConfig: BackendConfig = config.backend || {
66
- type: 'json',
67
- tasksFile: '.specs/tasks/tasks.json',
68
- }
69
- this.backend = createBackend(backendConfig)
70
- }
71
-
72
- /**
73
- * Send a message to the configured chat
74
- */
75
- async sendMessage(text: string, parseMode: 'HTML' | 'Markdown' = 'HTML'): Promise<boolean> {
76
- try {
77
- const url = `https://api.telegram.org/bot${this.botToken}/sendMessage`
78
- const response = await fetch(url, {
79
- method: 'POST',
80
- headers: { 'Content-Type': 'application/json' },
81
- body: JSON.stringify({
82
- chat_id: this.allowedChatId,
83
- text,
84
- parse_mode: parseMode,
85
- }),
86
- })
87
- return response.ok
88
- } catch {
89
- return false
90
- }
91
- }
92
-
93
- /**
94
- * Get updates from Telegram
95
- */
96
- private async getUpdates(): Promise<TelegramUpdate[]> {
97
- try {
98
- const url = `https://api.telegram.org/bot${this.botToken}/getUpdates?offset=${this.lastUpdateId + 1}&timeout=30`
99
- const response = await fetch(url)
100
- const data = await response.json() as TelegramResponse
101
-
102
- if (data.ok && data.result) {
103
- return data.result
104
- }
105
- return []
106
- } catch {
107
- return []
108
- }
109
- }
110
-
111
- /**
112
- * Handle a command
113
- */
114
- private async handleCommand(chatId: number, text: string): Promise<string> {
115
- // Security check - only allow configured chat
116
- if (String(chatId) !== this.allowedChatId) {
117
- return '⛔ Unauthorized'
118
- }
119
-
120
- const parts = text.trim().split(/\s+/)
121
- const command = parts[0].toLowerCase()
122
-
123
- switch (command) {
124
- case '/tasks':
125
- case '/list':
126
- return this.handleListTasks()
127
-
128
- case '/task':
129
- return this.handleGetTask(parts[1])
130
-
131
- case '/complete':
132
- case '/done':
133
- return this.handleCompleteTask(parts[1])
134
-
135
- case '/fail':
136
- return this.handleFailTask(parts[1], parts.slice(2).join(' '))
137
-
138
- case '/reset':
139
- return this.handleResetTask(parts[1])
140
-
141
- case '/status':
142
- return this.handleStatus()
143
-
144
- case '/new':
145
- case '/create':
146
- return this.handleCreateTask(parts.slice(1).join(' '))
147
-
148
- case '/subtask':
149
- return this.handleCreateSubTask(parts[1], parts.slice(2).join(' '))
150
-
151
- case '/priority':
152
- return this.handleSetPriority(parts[1], parts[2] as 'high' | 'medium' | 'low')
153
-
154
- case '/help':
155
- case '/start':
156
- return this.handleHelp()
157
-
158
- default:
159
- return `Unknown command: ${command}\nUse /help for available commands.`
160
- }
161
- }
162
-
163
- private async handleListTasks(): Promise<string> {
164
- const tasks = await this.backend.listPendingTasks()
165
-
166
- if (tasks.length === 0) {
167
- return '✅ No pending tasks!'
168
- }
169
-
170
- const priorityEmoji: Record<string, string> = {
171
- high: '🔴',
172
- medium: '🟡',
173
- low: '🟢',
174
- }
175
-
176
- const lines = tasks.slice(0, 10).map(t => {
177
- const emoji = priorityEmoji[t.priority] || '⚪'
178
- const title = t.title.length > 40 ? t.title.slice(0, 37) + '...' : t.title
179
- return `${emoji} <code>${t.id}</code>\n ${escapeHtml(title)}`
180
- })
181
-
182
- let response = `📋 <b>Pending Tasks (${tasks.length})</b>\n\n${lines.join('\n\n')}`
183
-
184
- if (tasks.length > 10) {
185
- response += `\n\n<i>...and ${tasks.length - 10} more</i>`
186
- }
187
-
188
- return response
189
- }
190
-
191
- private async handleGetTask(taskId?: string): Promise<string> {
192
- if (!taskId) {
193
- return '❌ Usage: /task <task-id>'
194
- }
195
-
196
- const task = await this.backend.getTask(taskId)
197
-
198
- if (!task) {
199
- return `❌ Task not found: ${taskId}`
200
- }
201
-
202
- const statusEmoji: Record<string, string> = {
203
- pending: '⏳',
204
- 'in-progress': '🔄',
205
- completed: '✅',
206
- failed: '❌',
207
- }
208
-
209
- const lines = [
210
- `${statusEmoji[task.status] || '❓'} <b>${escapeHtml(task.title)}</b>`,
211
- '',
212
- `<b>ID:</b> <code>${task.id}</code>`,
213
- `<b>Status:</b> ${task.status}`,
214
- `<b>Priority:</b> ${task.priority}`,
215
- ]
216
-
217
- if (task.feature) {
218
- lines.push(`<b>Feature:</b> ${escapeHtml(task.feature)}`)
219
- }
220
-
221
- if (task.parentId) {
222
- lines.push(`<b>Parent:</b> <code>${task.parentId}</code>`)
223
- }
224
-
225
- if (task.dependsOn && task.dependsOn.length > 0) {
226
- lines.push(`<b>Depends on:</b> ${task.dependsOn.map(d => `<code>${d}</code>`).join(', ')}`)
227
- }
228
-
229
- // Show truncated description
230
- if (task.description) {
231
- const desc = task.description.length > 500
232
- ? task.description.slice(0, 497) + '...'
233
- : task.description
234
- lines.push('', '<b>Description:</b>', escapeHtml(desc))
235
- }
236
-
237
- return lines.join('\n')
238
- }
239
-
240
- private async handleCompleteTask(taskId?: string): Promise<string> {
241
- if (!taskId) {
242
- return '❌ Usage: /complete <task-id>'
243
- }
244
-
245
- const task = await this.backend.getTask(taskId)
246
- if (!task) {
247
- return `❌ Task not found: ${taskId}`
248
- }
249
-
250
- const result = await this.backend.markCompleted(taskId, 'Completed via Telegram')
251
-
252
- if (result.success) {
253
- return `✅ Task ${taskId} marked as completed!`
254
- }
255
-
256
- return `❌ Failed to complete task: ${result.error || 'Unknown error'}`
257
- }
258
-
259
- private async handleFailTask(taskId?: string, reason?: string): Promise<string> {
260
- if (!taskId) {
261
- return '❌ Usage: /fail <task-id> [reason]'
262
- }
263
-
264
- const task = await this.backend.getTask(taskId)
265
- if (!task) {
266
- return `❌ Task not found: ${taskId}`
267
- }
268
-
269
- const result = await this.backend.markFailed(taskId, reason || 'Marked failed via Telegram')
270
-
271
- if (result.success) {
272
- return `❌ Task ${taskId} marked as failed`
273
- }
274
-
275
- return `❌ Failed to update task: ${result.error || 'Unknown error'}`
276
- }
277
-
278
- private async handleResetTask(taskId?: string): Promise<string> {
279
- if (!taskId) {
280
- return '❌ Usage: /reset <task-id>'
281
- }
282
-
283
- const task = await this.backend.getTask(taskId)
284
- if (!task) {
285
- return `❌ Task not found: ${taskId}`
286
- }
287
-
288
- const result = await this.backend.resetToPending(taskId)
289
-
290
- if (result.success) {
291
- return `🔄 Task ${taskId} reset to pending`
292
- }
293
-
294
- return `❌ Failed to reset task: ${result.error || 'Unknown error'}`
295
- }
296
-
297
- private async handleStatus(): Promise<string> {
298
- const ping = await this.backend.ping()
299
- const pending = await this.backend.countPending()
300
-
301
- const lines = [
302
- '📊 <b>Loopwork Status</b>',
303
- '',
304
- `<b>Backend:</b> ${this.backend.name}`,
305
- `<b>Health:</b> ${ping.ok ? '✅ OK' : '❌ Error'}`,
306
- `<b>Latency:</b> ${ping.latencyMs}ms`,
307
- `<b>Pending Tasks:</b> ${pending}`,
308
- ]
309
-
310
- if (ping.error) {
311
- lines.push(`<b>Error:</b> ${escapeHtml(ping.error)}`)
312
- }
313
-
314
- return lines.join('\n')
315
- }
316
-
317
- private async handleCreateTask(titleAndDesc: string): Promise<string> {
318
- if (!titleAndDesc.trim()) {
319
- return `❌ Usage: /new <title>
320
-
321
- Or with description:
322
- /new <title>
323
- <description on next lines>
324
-
325
- Example:
326
- /new Add user authentication
327
- Implement login/logout with JWT tokens`
328
- }
329
-
330
- if (!this.backend.createTask) {
331
- return '❌ This backend does not support task creation'
332
- }
333
-
334
- // Parse title and description (first line is title, rest is description)
335
- const lines = titleAndDesc.split('\n')
336
- const title = lines[0].trim()
337
- const description = lines.slice(1).join('\n').trim()
338
-
339
- try {
340
- const task = await this.backend.createTask({
341
- title,
342
- description: description || title,
343
- priority: 'medium',
344
- })
345
-
346
- return `✅ Task created!
347
-
348
- <b>ID:</b> <code>${task.id}</code>
349
- <b>Title:</b> ${escapeHtml(task.title)}
350
- <b>Priority:</b> ${task.priority}
351
-
352
- Use /task ${task.id} to view details`
353
- } catch (e: any) {
354
- return `❌ Failed to create task: ${escapeHtml(e.message)}`
355
- }
356
- }
357
-
358
- private async handleCreateSubTask(parentId: string | undefined, titleAndDesc: string): Promise<string> {
359
- if (!parentId || !titleAndDesc.trim()) {
360
- return `❌ Usage: /subtask <parent-id> <title>
361
-
362
- Example:
363
- /subtask TASK-001 Add login form validation`
364
- }
365
-
366
- if (!this.backend.createSubTask) {
367
- return '❌ This backend does not support sub-task creation'
368
- }
369
-
370
- // Check parent exists
371
- const parent = await this.backend.getTask(parentId)
372
- if (!parent) {
373
- return `❌ Parent task not found: ${parentId}`
374
- }
375
-
376
- const lines = titleAndDesc.split('\n')
377
- const title = lines[0].trim()
378
- const description = lines.slice(1).join('\n').trim()
379
-
380
- try {
381
- const task = await this.backend.createSubTask(parentId, {
382
- title,
383
- description: description || title,
384
- priority: parent.priority,
385
- feature: parent.feature,
386
- })
387
-
388
- return `✅ Sub-task created!
389
-
390
- <b>ID:</b> <code>${task.id}</code>
391
- <b>Parent:</b> <code>${parentId}</code>
392
- <b>Title:</b> ${escapeHtml(task.title)}
393
-
394
- Use /task ${task.id} to view details`
395
- } catch (e: any) {
396
- return `❌ Failed to create sub-task: ${escapeHtml(e.message)}`
397
- }
398
- }
399
-
400
- private async handleSetPriority(taskId: string | undefined, priority: 'high' | 'medium' | 'low' | undefined): Promise<string> {
401
- if (!taskId || !priority) {
402
- return `❌ Usage: /priority <task-id> <high|medium|low>
403
-
404
- Example:
405
- /priority TASK-001 high`
406
- }
407
-
408
- if (!['high', 'medium', 'low'].includes(priority)) {
409
- return `❌ Invalid priority: ${priority}\nUse: high, medium, or low`
410
- }
411
-
412
- const task = await this.backend.getTask(taskId)
413
- if (!task) {
414
- return `❌ Task not found: ${taskId}`
415
- }
416
-
417
- if (!this.backend.setPriority) {
418
- return '❌ This backend does not support priority changes'
419
- }
420
-
421
- const oldPriority = task.priority
422
- const result = await this.backend.setPriority(taskId, priority)
423
-
424
- if (result.success) {
425
- const priorityEmoji: Record<string, string> = {
426
- high: '🔴',
427
- medium: '🟡',
428
- low: '🟢',
429
- }
430
- return `✅ Priority updated!
431
-
432
- <b>Task:</b> <code>${taskId}</code>
433
- <b>Old:</b> ${priorityEmoji[oldPriority] || '⚪'} ${oldPriority}
434
- <b>New:</b> ${priorityEmoji[priority] || '⚪'} ${priority}`
435
- }
436
-
437
- return `❌ Failed to update priority: ${result.error || 'Unknown error'}`
438
- }
439
-
440
- private handleHelp(): string {
441
- return `🤖 <b>Loopwork Task Bot</b>
442
-
443
- <b>View Tasks:</b>
444
- /tasks - List pending tasks
445
- /task &lt;id&gt; - Get task details
446
- /status - Get backend status
447
-
448
- <b>Create Tasks:</b>
449
- /new &lt;title&gt; - Create new task
450
- /subtask &lt;parent&gt; &lt;title&gt; - Create sub-task
451
-
452
- <b>Update Tasks:</b>
453
- /complete &lt;id&gt; - Mark complete
454
- /fail &lt;id&gt; [reason] - Mark failed
455
- /reset &lt;id&gt; - Reset to pending
456
- /priority &lt;id&gt; &lt;high|medium|low&gt; - Set priority
457
-
458
- <b>Backend:</b> ${this.backend.name}`
459
- }
460
-
461
- /**
462
- * Start the bot polling loop
463
- */
464
- async start(): Promise<void> {
465
- if (this.running) return
466
-
467
- this.running = true
468
- console.log('🤖 Telegram bot started')
469
- await this.sendMessage('🤖 Loopwork bot is now online!\n\nUse /help for commands.')
470
-
471
- while (this.running) {
472
- try {
473
- const updates = await this.getUpdates()
474
-
475
- for (const update of updates) {
476
- this.lastUpdateId = update.update_id
477
-
478
- if (update.message?.text) {
479
- const response = await this.handleCommand(
480
- update.message.chat.id,
481
- update.message.text
482
- )
483
- await this.sendMessage(response)
484
- }
485
- }
486
- } catch (e: any) {
487
- console.error('Bot error:', e.message)
488
- await new Promise(r => setTimeout(r, 5000))
489
- }
490
- }
491
- }
492
-
493
- /**
494
- * Stop the bot
495
- */
496
- stop(): void {
497
- this.running = false
498
- }
499
- }
500
-
501
- function escapeHtml(text: string): string {
502
- return text
503
- .replace(/&/g, '&amp;')
504
- .replace(/</g, '&lt;')
505
- .replace(/>/g, '&gt;')
506
- }
507
-
508
- // CLI entry point
509
- if (import.meta.main) {
510
- const bot = new TelegramTaskBot()
511
- bot.start().catch(console.error)
512
-
513
- process.on('SIGINT', () => {
514
- bot.stop()
515
- process.exit(0)
516
- })
517
- }
@@ -1,6 +0,0 @@
1
- /**
2
- * Telegram Plugin
3
- */
4
-
5
- export * from './notifications'
6
- export * from './bot'
@@ -1,198 +0,0 @@
1
- /**
2
- * Telegram Notification Plugin for Loopwork
3
- *
4
- * Sends task status notifications to a Telegram chat via the Bot API.
5
- *
6
- * Setup:
7
- * 1. Create a bot with @BotFather and get the token
8
- * 2. Get your chat ID (can use @userinfobot)
9
- * 3. Set TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID env vars
10
- */
11
-
12
- import type { LoopworkPlugin, PluginTask, LoopStats, PluginTaskResult } from '../../contracts'
13
-
14
- export type NotificationLevel = 'info' | 'success' | 'warning' | 'error'
15
-
16
- export interface NotificationPayload {
17
- title: string
18
- message: string
19
- level: NotificationLevel
20
- taskId?: string
21
- }
22
-
23
- export interface NotificationPlugin {
24
- metadata: {
25
- name: string
26
- version: string
27
- type: 'notification'
28
- description: string
29
- }
30
- isConfigured(): boolean
31
- send(payload: NotificationPayload): Promise<{ success: boolean; error?: string }>
32
- ping(): Promise<{ ok: boolean; error?: string }>
33
- }
34
-
35
- export interface TelegramConfig {
36
- botToken: string
37
- chatId: string
38
- parseMode?: 'HTML' | 'Markdown' | 'MarkdownV2'
39
- disableNotification?: boolean
40
- }
41
-
42
- const LEVEL_EMOJI: Record<NotificationLevel, string> = {
43
- info: 'ℹ️',
44
- success: '✅',
45
- warning: '⚠️',
46
- error: '❌',
47
- }
48
-
49
- /**
50
- * Create a Telegram notification plugin
51
- */
52
- export function createTelegramPlugin(config?: Partial<TelegramConfig>): NotificationPlugin {
53
- const botToken = config?.botToken || process.env.TELEGRAM_BOT_TOKEN || ''
54
- const chatId = config?.chatId || process.env.TELEGRAM_CHAT_ID || ''
55
- const parseMode = config?.parseMode || 'HTML'
56
- const disableNotification = config?.disableNotification ?? false
57
-
58
- return {
59
- metadata: {
60
- name: 'telegram',
61
- version: '1.0.0',
62
- type: 'notification',
63
- description: 'Send notifications to Telegram',
64
- },
65
-
66
- isConfigured(): boolean {
67
- return Boolean(botToken && chatId)
68
- },
69
-
70
- async send(payload: NotificationPayload): Promise<{ success: boolean; error?: string }> {
71
- if (!this.isConfigured()) {
72
- return { success: false, error: 'Telegram not configured (missing TELEGRAM_BOT_TOKEN or TELEGRAM_CHAT_ID)' }
73
- }
74
-
75
- const emoji = LEVEL_EMOJI[payload.level]
76
- let text = `${emoji} <b>${escapeHtml(payload.title)}</b>\n\n${escapeHtml(payload.message)}`
77
-
78
- if (payload.taskId) {
79
- text += `\n\n<code>Task: ${escapeHtml(payload.taskId)}</code>`
80
- }
81
-
82
- try {
83
- const url = `https://api.telegram.org/bot${botToken}/sendMessage`
84
- const response = await fetch(url, {
85
- method: 'POST',
86
- headers: { 'Content-Type': 'application/json' },
87
- body: JSON.stringify({
88
- chat_id: chatId,
89
- text,
90
- parse_mode: parseMode,
91
- disable_notification: disableNotification,
92
- }),
93
- })
94
-
95
- if (!response.ok) {
96
- const data = await response.json().catch(() => ({})) as { description?: string }
97
- return { success: false, error: data.description || `HTTP ${response.status}` }
98
- }
99
-
100
- return { success: true }
101
- } catch (e: any) {
102
- return { success: false, error: e.message }
103
- }
104
- },
105
-
106
- async ping(): Promise<{ ok: boolean; error?: string }> {
107
- if (!this.isConfigured()) {
108
- return { ok: false, error: 'Not configured' }
109
- }
110
-
111
- try {
112
- const url = `https://api.telegram.org/bot${botToken}/getMe`
113
- const response = await fetch(url)
114
-
115
- if (!response.ok) {
116
- return { ok: false, error: `HTTP ${response.status}` }
117
- }
118
-
119
- const data = await response.json() as { ok: boolean; result?: { username: string } }
120
- if (data.ok) {
121
- return { ok: true }
122
- }
123
-
124
- return { ok: false, error: 'Invalid response' }
125
- } catch (e: any) {
126
- return { ok: false, error: e.message }
127
- }
128
- },
129
- }
130
- }
131
-
132
- /**
133
- * Escape HTML special characters for Telegram
134
- */
135
- function escapeHtml(text: string): string {
136
- return text
137
- .replace(/&/g, '&amp;')
138
- .replace(/</g, '&lt;')
139
- .replace(/>/g, '&gt;')
140
- }
141
-
142
- /**
143
- * Create a hook plugin that sends Telegram notifications on task events
144
- */
145
- export function createTelegramHookPlugin(config?: Partial<TelegramConfig>): LoopworkPlugin {
146
- const telegram = createTelegramPlugin(config)
147
-
148
- return {
149
- name: 'telegram-hooks',
150
-
151
- async onTaskStart(task: PluginTask) {
152
- if (!telegram.isConfigured()) return
153
-
154
- await telegram.send({
155
- title: 'Task Started',
156
- message: `${task.title}`,
157
- level: 'info',
158
- taskId: task.id,
159
- })
160
- },
161
-
162
- async onTaskComplete(task: PluginTask, result: PluginTaskResult) {
163
- if (!telegram.isConfigured()) return
164
-
165
- const durationMin = Math.round(result.duration / 60)
166
- await telegram.send({
167
- title: 'Task Completed',
168
- message: `${task.title}\n\nDuration: ${durationMin} minutes`,
169
- level: 'success',
170
- taskId: task.id,
171
- })
172
- },
173
-
174
- async onTaskFailed(task: PluginTask, error: string) {
175
- if (!telegram.isConfigured()) return
176
-
177
- await telegram.send({
178
- title: 'Task Failed',
179
- message: `${task.title}\n\nError: ${error.slice(0, 200)}`,
180
- level: 'error',
181
- taskId: task.id,
182
- })
183
- },
184
-
185
- async onLoopEnd(stats: LoopStats) {
186
- if (!telegram.isConfigured()) return
187
-
188
- const durationMin = Math.round(stats.duration / 60)
189
- const level = stats.failed > 0 ? 'warning' : 'success'
190
-
191
- await telegram.send({
192
- title: 'Loop Completed',
193
- message: `Completed: ${stats.completed}\nFailed: ${stats.failed}\nDuration: ${durationMin} minutes`,
194
- level,
195
- })
196
- },
197
- }
198
- }