pi-harness-runtime 1.1.145 → 1.1.147

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 (176) hide show
  1. package/README.md +113 -0
  2. package/index.ts +136 -6
  3. package/package.json +2 -2
  4. package/packages/a2a-adapter/package.json +1 -1
  5. package/packages/architecture-generator/package.json +1 -1
  6. package/packages/auth/package.json +1 -1
  7. package/packages/autonomous-refactor/package.json +1 -1
  8. package/packages/autonomous-runtime/package.json +1 -1
  9. package/packages/cache-strategy/package.json +1 -1
  10. package/packages/capability-registry/package.json +1 -1
  11. package/packages/checkpoint/package.json +1 -1
  12. package/packages/cli-plugin-sdk/package.json +1 -1
  13. package/packages/clipboard/package.json +1 -1
  14. package/packages/clipboard-plugin/package.json +1 -1
  15. package/packages/code-generation/package.json +1 -1
  16. package/packages/code-review/package.json +1 -1
  17. package/packages/codex-adapter/package.json +1 -1
  18. package/packages/codex-watcher/package.json +1 -1
  19. package/packages/config-capture/package.json +1 -1
  20. package/packages/context-compiler/package.json +1 -1
  21. package/packages/context-discovery/package.json +1 -1
  22. package/packages/context-manager/package.json +1 -1
  23. package/packages/cookie-sanitizer/package.json +1 -1
  24. package/packages/cost-optimizer/package.json +1 -1
  25. package/packages/dependency-analyzer/package.json +1 -1
  26. package/packages/django-plugin/package.json +1 -1
  27. package/packages/doc-generator/package.json +1 -1
  28. package/packages/evaluation-engine/package.json +1 -1
  29. package/packages/evaluation-runner/package.json +1 -1
  30. package/packages/event-bus/package.json +1 -1
  31. package/packages/event-store/package.json +1 -1
  32. package/packages/experience-replay/package.json +1 -1
  33. package/packages/feedback-collector/package.json +1 -1
  34. package/packages/file-copy-helper/package.json +1 -1
  35. package/packages/framework-detector/package.json +1 -1
  36. package/packages/framework-plugin-sdk/package.json +1 -1
  37. package/packages/frappe-plugin/package.json +1 -1
  38. package/packages/generic-web-plugin/package.json +1 -1
  39. package/packages/health-monitor/package.json +1 -1
  40. package/packages/intent-analyzer/package.json +1 -1
  41. package/packages/jev-judge/package.json +1 -1
  42. package/packages/knowledge-graph/package.json +1 -1
  43. package/packages/knowledge-retrieval/package.json +1 -1
  44. package/packages/laravel-plugin/package.json +1 -1
  45. package/packages/learning-engine/package.json +1 -1
  46. package/packages/mcp-adapter/package.json +1 -1
  47. package/packages/memory-engine/package.json +1 -1
  48. package/packages/milestone-manager/package.json +1 -1
  49. package/packages/model-registry/package.json +1 -1
  50. package/packages/nextjs-plugin/package.json +1 -1
  51. package/packages/notification/dist/src/adapters/email-adapter.d.ts.map +1 -0
  52. package/packages/notification/dist/src/adapters/email-adapter.js.map +1 -0
  53. package/packages/notification/dist/src/adapters/line-adapter.d.ts.map +1 -0
  54. package/packages/notification/dist/src/adapters/line-adapter.js.map +1 -0
  55. package/packages/notification/dist/src/adapters/ntfy-adapter.d.ts.map +1 -0
  56. package/packages/notification/dist/src/adapters/ntfy-adapter.js.map +1 -0
  57. package/packages/notification/dist/src/adapters/telegram-adapter.d.ts +56 -0
  58. package/packages/notification/dist/src/adapters/telegram-adapter.d.ts.map +1 -0
  59. package/packages/notification/dist/src/adapters/telegram-adapter.js +251 -0
  60. package/packages/notification/dist/src/adapters/telegram-adapter.js.map +1 -0
  61. package/packages/notification/dist/src/adapters/webhook-adapter.d.ts.map +1 -0
  62. package/packages/notification/dist/src/adapters/webhook-adapter.js.map +1 -0
  63. package/packages/notification/dist/src/base-adapter.d.ts.map +1 -0
  64. package/packages/notification/dist/src/base-adapter.js.map +1 -0
  65. package/packages/notification/dist/{index.d.ts → src/index.d.ts} +2 -0
  66. package/packages/notification/dist/src/index.d.ts.map +1 -0
  67. package/packages/notification/dist/{index.js → src/index.js} +2 -0
  68. package/packages/notification/dist/src/index.js.map +1 -0
  69. package/packages/notification/dist/src/notification-center.d.ts +89 -0
  70. package/packages/notification/dist/src/notification-center.d.ts.map +1 -0
  71. package/packages/notification/dist/{notification-center.js → src/notification-center.js} +103 -1
  72. package/packages/notification/dist/src/notification-center.js.map +1 -0
  73. package/packages/notification/dist/src/notification-policy.d.ts +66 -0
  74. package/packages/notification/dist/src/notification-policy.d.ts.map +1 -0
  75. package/packages/notification/dist/src/notification-policy.js +153 -0
  76. package/packages/notification/dist/src/notification-policy.js.map +1 -0
  77. package/packages/notification/dist/src/telegram-webhook-handler.d.ts +86 -0
  78. package/packages/notification/dist/src/telegram-webhook-handler.d.ts.map +1 -0
  79. package/packages/notification/dist/src/telegram-webhook-handler.js +149 -0
  80. package/packages/notification/dist/src/telegram-webhook-handler.js.map +1 -0
  81. package/packages/notification/dist/{types.d.ts → src/types.d.ts} +37 -0
  82. package/packages/notification/dist/src/types.d.ts.map +1 -0
  83. package/packages/notification/dist/src/types.js.map +1 -0
  84. package/packages/notification/package.json +1 -1
  85. package/packages/notification/src/adapters/telegram-adapter.ts +187 -5
  86. package/packages/notification/src/index.ts +2 -0
  87. package/packages/notification/src/notification-center.ts +139 -0
  88. package/packages/notification/src/notification-policy.ts +203 -0
  89. package/packages/notification/src/telegram-webhook-handler.ts +211 -0
  90. package/packages/notification/src/types.ts +42 -0
  91. package/packages/observability/package.json +1 -1
  92. package/packages/okf-indexer/package.json +1 -1
  93. package/packages/paperclip-integration/dist/index.d.ts.map +1 -1
  94. package/packages/paperclip-integration/dist/index.js +30 -18
  95. package/packages/paperclip-integration/dist/index.js.map +1 -1
  96. package/packages/paperclip-integration/package.json +1 -1
  97. package/packages/paperclip-integration/src/index.ts +35 -20
  98. package/packages/performance-optimizer/package.json +1 -1
  99. package/packages/privilege-broker/package.json +1 -1
  100. package/packages/project-analyzer/package.json +1 -1
  101. package/packages/project-bootstrap/package.json +1 -1
  102. package/packages/projection-engine/package.json +1 -1
  103. package/packages/prompt-compiler/package.json +1 -1
  104. package/packages/prompt-versioning/package.json +1 -1
  105. package/packages/provider-adapter-sdk/package.json +1 -1
  106. package/packages/provider-router/package.json +1 -1
  107. package/packages/provider-selector/package.json +1 -1
  108. package/packages/providers/package.json +1 -1
  109. package/packages/qdrant-skills/package.json +1 -1
  110. package/packages/quota-manager/package.json +1 -1
  111. package/packages/rate-limiter/package.json +1 -1
  112. package/packages/react-vite-plugin/package.json +1 -1
  113. package/packages/release-manager/package.json +1 -1
  114. package/packages/requirement-compiler/package.json +1 -1
  115. package/packages/runtime/package.json +1 -1
  116. package/packages/scheduler/package.json +1 -1
  117. package/packages/scheduler-adapter/package.json +1 -1
  118. package/packages/session/package.json +1 -1
  119. package/packages/session-api/package.json +1 -1
  120. package/packages/session-export/package.json +1 -1
  121. package/packages/shared-context/package.json +1 -1
  122. package/packages/skill-mcp-client/package.json +1 -1
  123. package/packages/skill-registry/package.json +1 -1
  124. package/packages/skills/package.json +1 -1
  125. package/packages/sprint-planner/package.json +1 -1
  126. package/packages/subscription-engine/package.json +1 -1
  127. package/packages/task-compiler/package.json +1 -1
  128. package/packages/tencentdb-memory/package.json +1 -1
  129. package/packages/tencentdb-sync/package.json +1 -1
  130. package/packages/test-data-generator/package.json +1 -1
  131. package/packages/test-generator/package.json +1 -1
  132. package/packages/todo-bd-sync/package.json +1 -1
  133. package/packages/token-estimation/package.json +1 -1
  134. package/packages/token-optimizer/package.json +1 -1
  135. package/packages/trajectory/package.json +1 -1
  136. package/packages/tui/package.json +1 -1
  137. package/packages/types/package.json +1 -1
  138. package/packages/workflow-events/package.json +1 -1
  139. package/packages/workspace-scanner/package.json +1 -1
  140. package/packages/worktree/package.json +1 -1
  141. package/packages/write-review/package.json +1 -1
  142. package/scripts/telegram-callback-processor.ts +180 -0
  143. package/scripts/telegram-webhook-server.ts +387 -0
  144. package/scripts/test-telegram-approval.ts +64 -0
  145. package/packages/notification/dist/adapters/email-adapter.d.ts.map +0 -1
  146. package/packages/notification/dist/adapters/email-adapter.js.map +0 -1
  147. package/packages/notification/dist/adapters/line-adapter.d.ts.map +0 -1
  148. package/packages/notification/dist/adapters/line-adapter.js.map +0 -1
  149. package/packages/notification/dist/adapters/ntfy-adapter.d.ts.map +0 -1
  150. package/packages/notification/dist/adapters/ntfy-adapter.js.map +0 -1
  151. package/packages/notification/dist/adapters/telegram-adapter.d.ts +0 -19
  152. package/packages/notification/dist/adapters/telegram-adapter.d.ts.map +0 -1
  153. package/packages/notification/dist/adapters/telegram-adapter.js +0 -97
  154. package/packages/notification/dist/adapters/telegram-adapter.js.map +0 -1
  155. package/packages/notification/dist/adapters/webhook-adapter.d.ts.map +0 -1
  156. package/packages/notification/dist/adapters/webhook-adapter.js.map +0 -1
  157. package/packages/notification/dist/base-adapter.d.ts.map +0 -1
  158. package/packages/notification/dist/base-adapter.js.map +0 -1
  159. package/packages/notification/dist/index.d.ts.map +0 -1
  160. package/packages/notification/dist/index.js.map +0 -1
  161. package/packages/notification/dist/notification-center.d.ts +0 -50
  162. package/packages/notification/dist/notification-center.d.ts.map +0 -1
  163. package/packages/notification/dist/notification-center.js.map +0 -1
  164. package/packages/notification/dist/types.d.ts.map +0 -1
  165. package/packages/notification/dist/types.js.map +0 -1
  166. /package/packages/notification/dist/{adapters → src/adapters}/email-adapter.d.ts +0 -0
  167. /package/packages/notification/dist/{adapters → src/adapters}/email-adapter.js +0 -0
  168. /package/packages/notification/dist/{adapters → src/adapters}/line-adapter.d.ts +0 -0
  169. /package/packages/notification/dist/{adapters → src/adapters}/line-adapter.js +0 -0
  170. /package/packages/notification/dist/{adapters → src/adapters}/ntfy-adapter.d.ts +0 -0
  171. /package/packages/notification/dist/{adapters → src/adapters}/ntfy-adapter.js +0 -0
  172. /package/packages/notification/dist/{adapters → src/adapters}/webhook-adapter.d.ts +0 -0
  173. /package/packages/notification/dist/{adapters → src/adapters}/webhook-adapter.js +0 -0
  174. /package/packages/notification/dist/{base-adapter.d.ts → src/base-adapter.d.ts} +0 -0
  175. /package/packages/notification/dist/{base-adapter.js → src/base-adapter.js} +0 -0
  176. /package/packages/notification/dist/{types.js → src/types.js} +0 -0
@@ -29,7 +29,44 @@ export interface TelegramConfig {
29
29
  botToken: string;
30
30
  chatId: string;
31
31
  parseMode?: "MarkdownV2" | "HTML" | "Markdown";
32
+ enableInlineKeyboard?: boolean;
33
+ actionButtons?: InlineKeyboardButton[];
32
34
  }
35
+ /**
36
+ * Inline keyboard button for Telegram interactive messages
37
+ */
38
+ export interface InlineKeyboardButton {
39
+ /** Button text displayed to user */
40
+ text: string;
41
+ /** Callback data sent when button is clicked */
42
+ callbackData: string;
43
+ /** Optional URL to open when button is clicked (mutually exclusive with callbackData) */
44
+ url?: string;
45
+ }
46
+ /**
47
+ * Callback query received from Telegram inline keyboard
48
+ */
49
+ export interface TelegramCallbackQuery {
50
+ id: string;
51
+ from: {
52
+ id: number;
53
+ is_bot: boolean;
54
+ first_name: string;
55
+ username?: string;
56
+ };
57
+ chat_instance: string;
58
+ data: string;
59
+ message?: {
60
+ chat: {
61
+ id: number;
62
+ };
63
+ message_id: number;
64
+ };
65
+ }
66
+ /**
67
+ * Callback handler function type
68
+ */
69
+ export type TelegramCallbackHandler = (callbackData: string, query: TelegramCallbackQuery) => Promise<void> | void;
33
70
  export interface NtfyConfig {
34
71
  server: string;
35
72
  topic: string;
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/notification",
3
- "version": "1.1.145",
3
+ "version": "1.1.147",
4
4
  "description": "pi-harness-runtime — notification",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -7,8 +7,11 @@
7
7
  /// <reference types="node" />
8
8
 
9
9
  import type {
10
+ InlineKeyboardButton,
10
11
  NotificationPayload,
11
12
  NotificationResult,
13
+ TelegramCallbackHandler,
14
+ TelegramCallbackQuery,
12
15
  TelegramConfig,
13
16
  } from "../types.js";
14
17
  import { BaseChannelAdapter } from "../base-adapter.js";
@@ -17,11 +20,27 @@ export class TelegramAdapter extends BaseChannelAdapter {
17
20
  readonly id = "telegram";
18
21
  readonly type = "telegram";
19
22
  private _botUsername: string | undefined;
23
+ private _callbackHandler: TelegramCallbackHandler | undefined;
24
+ private _webhookSecret: string | undefined;
20
25
 
21
26
  constructor(config: TelegramConfig) {
22
27
  super({ id: "telegram", type: "telegram", enabled: true, config });
23
28
  }
24
29
 
30
+ /**
31
+ * Register a callback handler for inline keyboard button clicks
32
+ */
33
+ setCallbackHandler(handler: TelegramCallbackHandler): void {
34
+ this._callbackHandler = handler;
35
+ }
36
+
37
+ /**
38
+ * Set webhook secret for verification
39
+ */
40
+ setWebhookSecret(secret: string): void {
41
+ this._webhookSecret = secret;
42
+ }
43
+
25
44
  get botUsername(): string | undefined {
26
45
  return this._botUsername;
27
46
  }
@@ -43,21 +62,32 @@ return this._botUsername;
43
62
  }
44
63
  }
45
64
 
65
+ /**
66
+ * Send a message with optional inline keyboard buttons
67
+ */
46
68
  async send(payload: NotificationPayload): Promise<NotificationResult> {
47
69
  try {
48
70
  const cfg = this.config.config as TelegramConfig;
49
71
  const message = this.formatMessage(payload);
50
72
 
73
+ // Build request body
74
+ const body: Record<string, unknown> = {
75
+ chat_id: cfg.chatId,
76
+ text: message,
77
+ parse_mode: cfg.parseMode ?? "MarkdownV2",
78
+ };
79
+
80
+ // Add inline keyboard if enabled and buttons are configured
81
+ if (cfg.enableInlineKeyboard && (cfg.actionButtons?.length ?? 0) > 0) {
82
+ body.reply_markup = this.buildInlineKeyboard(cfg.actionButtons!);
83
+ }
84
+
51
85
  const response = await fetch(
52
86
  `https://api.telegram.org/bot${cfg.botToken}/sendMessage`,
53
87
  {
54
88
  method: "POST",
55
89
  headers: { "Content-Type": "application/json" },
56
- body: JSON.stringify({
57
- chat_id: cfg.chatId,
58
- text: message,
59
- parse_mode: cfg.parseMode ?? "MarkdownV2",
60
- }),
90
+ body: JSON.stringify(body),
61
91
  },
62
92
  );
63
93
 
@@ -80,6 +110,158 @@ return this._botUsername;
80
110
  }
81
111
  }
82
112
 
113
+ /**
114
+ * Build inline keyboard markup from button configuration
115
+ */
116
+ private buildInlineKeyboard(
117
+ buttons: InlineKeyboardButton[],
118
+ ): { inline_keyboard: Array<Array<{ text: string; callback_data?: string; url?: string }>> } {
119
+ // Group buttons into rows of up to 3 buttons each (Telegram limit)
120
+ const ROW_SIZE = 3;
121
+ const rows: Array<Array<{ text: string; callback_data?: string; url?: string }>> = [];
122
+
123
+ for (let i = 0; i < buttons.length; i += ROW_SIZE) {
124
+ const row = buttons.slice(i, i + ROW_SIZE).map((btn) => ({
125
+ text: btn.text,
126
+ ...(btn.url ? { url: btn.url } : { callback_data: btn.callbackData }),
127
+ }));
128
+ rows.push(row);
129
+ }
130
+
131
+ return { inline_keyboard: rows };
132
+ }
133
+
134
+ /**
135
+ * Process incoming webhook update from Telegram
136
+ */
137
+ async handleWebhookUpdate(update: Record<string, unknown>): Promise<void> {
138
+ // Verify secret token if set
139
+ if (this._webhookSecret) {
140
+ const secretToken = (update as { secret_token?: string }).secret_token;
141
+ if (secretToken !== this._webhookSecret) {
142
+ console.error("[TelegramAdapter] Invalid webhook secret");
143
+ return;
144
+ }
145
+ }
146
+
147
+ // Handle callback query (button click)
148
+ const callbackQuery = update.callback_query as Record<string, unknown> | undefined;
149
+ if (callbackQuery && this._callbackHandler) {
150
+ const query = this.parseCallbackQuery(callbackQuery);
151
+ if (query) {
152
+ await this._callbackHandler(query.data, query);
153
+ await this.answerCallbackQuery(query.id);
154
+ }
155
+ }
156
+
157
+ // Handle regular messages (future: user can type commands)
158
+ const message = update.message as Record<string, unknown> | undefined;
159
+ if (message) {
160
+ console.log("[TelegramAdapter] Received message:", message.text);
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Parse callback query from webhook update
166
+ */
167
+ private parseCallbackQuery(
168
+ raw: Record<string, unknown>,
169
+ ): TelegramCallbackQuery | null {
170
+ try {
171
+ const from = raw.from as Record<string, unknown>;
172
+ const message = raw.message as Record<string, unknown> | undefined;
173
+ const chat = message?.chat as Record<string, unknown> | undefined;
174
+
175
+ return {
176
+ id: String(raw.id),
177
+ from: {
178
+ id: Number(from?.id),
179
+ is_bot: Boolean(from?.is_bot),
180
+ first_name: String(from?.first_name ?? ""),
181
+ username: from?.username ? String(from.username) : undefined,
182
+ },
183
+ chat_instance: String(raw.chat_instance ?? ""),
184
+ data: String(raw.data ?? ""),
185
+ message: message
186
+ ? {
187
+ chat: { id: Number(chat?.id) },
188
+ message_id: Number(message?.message_id),
189
+ }
190
+ : undefined,
191
+ };
192
+ } catch {
193
+ console.error("[TelegramAdapter] Failed to parse callback query");
194
+ return null;
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Answer a callback query to remove loading state
200
+ */
201
+ private async answerCallbackQuery(callbackQueryId: string): Promise<void> {
202
+ try {
203
+ const cfg = this.config.config as TelegramConfig;
204
+ await fetch(
205
+ `https://api.telegram.org/bot${cfg.botToken}/answerCallbackQuery`,
206
+ {
207
+ method: "POST",
208
+ headers: { "Content-Type": "application/json" },
209
+ body: JSON.stringify({ callback_query_id: callbackQueryId }),
210
+ },
211
+ );
212
+ } catch (error) {
213
+ console.error("[TelegramAdapter] Failed to answer callback query:", error);
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Setup webhook for receiving updates
219
+ */
220
+ async setupWebhook(webhookUrl: string): Promise<boolean> {
221
+ try {
222
+ const cfg = this.config.config as TelegramConfig;
223
+ const response = await fetch(
224
+ `https://api.telegram.org/bot${cfg.botToken}/setWebhook`,
225
+ {
226
+ method: "POST",
227
+ headers: { "Content-Type": "application/json" },
228
+ body: JSON.stringify({
229
+ url: webhookUrl,
230
+ secret_token: this._webhookSecret,
231
+ }),
232
+ },
233
+ );
234
+
235
+ if (!response.ok) {
236
+ console.error("[TelegramAdapter] Webhook setup failed:", await response.text());
237
+ return false;
238
+ }
239
+
240
+ console.log(`[TelegramAdapter] Webhook set to: ${webhookUrl}`);
241
+ return true;
242
+ } catch (error) {
243
+ console.error("[TelegramAdapter] Webhook setup error:", error);
244
+ return false;
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Get current webhook info
250
+ */
251
+ async getWebhookInfo(): Promise<Record<string, unknown> | null> {
252
+ try {
253
+ const cfg = this.config.config as TelegramConfig;
254
+ const response = await fetch(
255
+ `https://api.telegram.org/bot${cfg.botToken}/getWebhookInfo`,
256
+ );
257
+
258
+ if (!response.ok) return null;
259
+ return (await response.json()) as Record<string, unknown>;
260
+ } catch {
261
+ return null;
262
+ }
263
+ }
264
+
83
265
  private formatMessage(payload: NotificationPayload): string {
84
266
  const emoji = this.getEmoji(payload.event);
85
267
  const title = `${emoji} ${payload.title}`;
@@ -7,4 +7,6 @@
7
7
  export * from "./notification-center.js";
8
8
  export * from "./base-adapter.js";
9
9
  export * from "./types.js";
10
+ export * from "./telegram-webhook-handler.js";
10
11
  export { LineAdapter } from "./adapters/line-adapter.js";
12
+ export { TelegramAdapter } from "./adapters/telegram-adapter.js";
@@ -10,15 +10,19 @@
10
10
  */
11
11
 
12
12
  import type {
13
+ InlineKeyboardButton,
13
14
  NotificationEvent,
14
15
  NotificationPayload,
15
16
  NotificationConfig,
16
17
  NotificationChannelConfig,
17
18
  NotificationResult,
18
19
  NotificationContext,
20
+ TelegramCallbackHandler,
21
+ TelegramCallbackQuery,
19
22
  } from "./types.js";
20
23
  import type { ChannelAdapter } from "./base-adapter.js";
21
24
  import { TelegramAdapter } from "./adapters/telegram-adapter.js";
25
+ import { buildCallbackData, parseCallbackData, CallbackActions } from "./telegram-webhook-handler.js";
22
26
  import { LineAdapter } from "./adapters/line-adapter.js";
23
27
  import { NtfyAdapter } from "./adapters/ntfy-adapter.js";
24
28
  import { EmailAdapter } from "./adapters/email-adapter.js";
@@ -158,6 +162,132 @@ export class NotificationCenter {
158
162
  return undefined;
159
163
  }
160
164
 
165
+ /**
166
+ * Get Telegram adapter for advanced operations (webhook setup, etc.)
167
+ */
168
+ getTelegramAdapter(): TelegramAdapter | undefined {
169
+ const adapter = this.adapters.get("telegram");
170
+ if (adapter instanceof TelegramAdapter) {
171
+ return adapter;
172
+ }
173
+ return undefined;
174
+ }
175
+
176
+ /**
177
+ * Set callback handler for Telegram inline keyboard button clicks
178
+ */
179
+ setTelegramCallbackHandler(handler: TelegramCallbackHandler): void {
180
+ const adapter = this.adapters.get("telegram");
181
+ if (adapter instanceof TelegramAdapter) {
182
+ adapter.setCallbackHandler(handler);
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Set webhook secret for Telegram updates verification
188
+ */
189
+ setTelegramWebhookSecret(secret: string): void {
190
+ const adapter = this.adapters.get("telegram");
191
+ if (adapter instanceof TelegramAdapter) {
192
+ adapter.setWebhookSecret(secret);
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Setup Telegram webhook for receiving updates
198
+ */
199
+ async setupTelegramWebhook(webhookUrl: string): Promise<boolean> {
200
+ const adapter = this.adapters.get("telegram");
201
+ if (adapter instanceof TelegramAdapter) {
202
+ return await adapter.setupWebhook(webhookUrl);
203
+ }
204
+ return false;
205
+ }
206
+
207
+ /**
208
+ * Send notification with interactive Yes/No buttons
209
+ */
210
+ async notifyWithApproval(
211
+ event: NotificationEvent,
212
+ context: NotificationContext,
213
+ options?: {
214
+ /** Custom button labels */
215
+ approveLabel?: string;
216
+ rejectLabel?: string;
217
+ /** Additional details for the message */
218
+ additionalDetails?: Record<string, unknown>;
219
+ },
220
+ ): Promise<NotificationResult[]> {
221
+ const approveLabel = options?.approveLabel ?? "Yes";
222
+ const rejectLabel = options?.rejectLabel ?? "No";
223
+
224
+ // Build buttons
225
+ const buttons: InlineKeyboardButton[] = [
226
+ { text: `\u2705 ${approveLabel}`, callbackData: buildCallbackData(CallbackActions.APPROVE, context.jobId) },
227
+ { text: `\u274C ${rejectLabel}`, callbackData: buildCallbackData(CallbackActions.REJECT, context.jobId) },
228
+ ];
229
+
230
+ // Send to each channel with buttons (Telegram only)
231
+ const results: NotificationResult[] = [];
232
+
233
+ for (const [id, adapter] of this.adapters.entries()) {
234
+ if (adapter instanceof TelegramAdapter) {
235
+ try {
236
+ const payload = this.buildPayload(event, context, {
237
+ enableInlineKeyboard: true,
238
+ actionButtons: buttons,
239
+ ...(options?.additionalDetails && { additionalDetails: options.additionalDetails }),
240
+ });
241
+ const result = await adapter.send(payload);
242
+ results.push(result);
243
+ } catch (error) {
244
+ results.push({ success: false, channel: id, error: String(error) });
245
+ }
246
+ }
247
+ }
248
+
249
+ return results;
250
+ }
251
+
252
+ /**
253
+ * Send interactive notification with custom buttons
254
+ */
255
+ async notifyWithButtons(
256
+ event: NotificationEvent,
257
+ context: NotificationContext,
258
+ buttons: Array<{ text: string; action: string; targetId: string; url?: string }>,
259
+ options?: {
260
+ /** Additional details for the message */
261
+ additionalDetails?: Record<string, unknown>;
262
+ },
263
+ ): Promise<NotificationResult[]> {
264
+ const results: NotificationResult[] = [];
265
+
266
+ for (const [id, adapter] of this.adapters.entries()) {
267
+ if (adapter instanceof TelegramAdapter) {
268
+ try {
269
+ const formattedButtons: InlineKeyboardButton[] = buttons.map((b) => ({
270
+ text: b.text,
271
+ callbackData: buildCallbackData(b.action, b.targetId),
272
+ url: b.url,
273
+ }));
274
+
275
+ const payload = this.buildPayload(event, context, {
276
+ enableInlineKeyboard: true,
277
+ actionButtons: formattedButtons,
278
+ ...(options?.additionalDetails && { additionalDetails: options.additionalDetails }),
279
+ });
280
+ const result = await adapter.send(payload);
281
+ results.push(result);
282
+ } catch (error) {
283
+ results.push({ success: false, channel: id, error: String(error) });
284
+ }
285
+ }
286
+ }
287
+
288
+ return results;
289
+ }
290
+
161
291
  // --- Private Methods ------------------------------------------------
162
292
 
163
293
  private createAdapter(
@@ -192,6 +322,11 @@ export class NotificationCenter {
192
322
  private buildPayload(
193
323
  event: NotificationEvent,
194
324
  context: NotificationContext,
325
+ options?: {
326
+ enableInlineKeyboard?: boolean;
327
+ actionButtons?: InlineKeyboardButton[];
328
+ additionalDetails?: Record<string, unknown>;
329
+ },
195
330
  ): NotificationPayload {
196
331
  const { title, message } = this.getEventContent(event, context);
197
332
 
@@ -207,6 +342,10 @@ export class NotificationCenter {
207
342
  ...(context.taskId && { taskId: context.taskId }),
208
343
  ...(context.taskTitle && { taskTitle: context.taskTitle }),
209
344
  ...(context.error && { error: context.error }),
345
+ // Pass inline keyboard options to Telegram adapter
346
+ ...(options?.enableInlineKeyboard && { _enableInlineKeyboard: true }),
347
+ ...(options?.actionButtons && { _actionButtons: options.actionButtons }),
348
+ ...(options?.additionalDetails && options.additionalDetails),
210
349
  },
211
350
  };
212
351
  }
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Notification Policy Engine for intelligent routing
3
+ *
4
+ * Decides:
5
+ * 1. Should we notify at all?
6
+ * 2. What channel(s)?
7
+ * 3. Should we include approval buttons?
8
+ * 4. What's the priority level?
9
+ *
10
+ * Can be extended with Jev for AI-powered decisions:
11
+ * import { Jev } from "@type-safe/ai";
12
+ */
13
+
14
+ export interface NotificationContext {
15
+ event: string;
16
+ jobId?: string;
17
+ requirement?: string;
18
+ error?: string;
19
+ severity?: "low" | "medium" | "high" | "critical";
20
+ requiresApproval?: boolean;
21
+ userId?: number;
22
+ timestamp?: string;
23
+ }
24
+
25
+ export interface NotificationPolicy {
26
+ shouldNotify: boolean;
27
+ channels: ("telegram" | "tui" | "log")[];
28
+ includeButtons: boolean;
29
+ priority: "low" | "normal" | "high" | "urgent";
30
+ message?: string;
31
+ }
32
+
33
+ export interface UserPreferences {
34
+ telegramEnabled: boolean;
35
+ telegramChatId?: string;
36
+ telegramBotToken?: string;
37
+ tuiEnabled: boolean;
38
+ notifyOnError: boolean;
39
+ notifyOnCompletion: boolean;
40
+ notifyOnApprovalRequired: boolean;
41
+ }
42
+
43
+ // Default preferences
44
+ const DEFAULT_PREFERENCES: UserPreferences = {
45
+ telegramEnabled: false,
46
+ tuiEnabled: true,
47
+ notifyOnError: true,
48
+ notifyOnCompletion: false,
49
+ notifyOnApprovalRequired: true,
50
+ };
51
+
52
+ /**
53
+ * Events that require user approval
54
+ */
55
+ const APPROVAL_REQUIRED_EVENTS = new Set([
56
+ "HumanReviewNeeded",
57
+ "WaitingForUserInput",
58
+ "ApprovalRequired",
59
+ "ConfirmDeploy",
60
+ "ConfirmDelete",
61
+ "ConfirmCriticalAction",
62
+ ]);
63
+
64
+ /**
65
+ * High-priority events (always notify)
66
+ */
67
+ const HIGH_PRIORITY_EVENTS = new Set([
68
+ "QuotaExhausted",
69
+ "CriticalError",
70
+ "HumanReviewNeeded",
71
+ "WaitingForUserInput",
72
+ "JobFailed",
73
+ "SystemAlert",
74
+ ]);
75
+
76
+ /**
77
+ * Notification policy engine
78
+ */
79
+ export class NotificationPolicyEngine {
80
+ private preferences: UserPreferences;
81
+
82
+ constructor(preferences?: Partial<UserPreferences>) {
83
+ this.preferences = { ...DEFAULT_PREFERENCES, ...preferences };
84
+ }
85
+
86
+ /**
87
+ * Update user preferences
88
+ */
89
+ updatePreferences(prefs: Partial<UserPreferences>): void {
90
+ this.preferences = { ...this.preferences, ...prefs };
91
+ }
92
+
93
+ /**
94
+ * Evaluate notification policy
95
+ */
96
+ evaluate(context: NotificationContext): NotificationPolicy {
97
+ const event = context.event;
98
+ const severity = context.severity || this.inferSeverity(event);
99
+ const needsApproval = APPROVAL_REQUIRED_EVENTS.has(event);
100
+
101
+ // Build channels
102
+ const channels: ("telegram" | "tui" | "log")[] = [];
103
+
104
+ if (this.preferences.telegramEnabled && this.preferences.telegramChatId) {
105
+ channels.push("telegram");
106
+ }
107
+
108
+ if (this.preferences.tuiEnabled) {
109
+ channels.push("tui");
110
+ }
111
+
112
+ // Decide based on event type and severity
113
+ let shouldNotify = false;
114
+ let includeButtons = false;
115
+ let priority: NotificationPolicy["priority"] = "normal";
116
+
117
+ switch (event) {
118
+ case "HumanReviewNeeded":
119
+ case "WaitingForUserInput":
120
+ case "ApprovalRequired":
121
+ shouldNotify = this.preferences.notifyOnApprovalRequired;
122
+ includeButtons = true;
123
+ priority = "high";
124
+ break;
125
+
126
+ case "CriticalError":
127
+ case "SystemAlert":
128
+ shouldNotify = true;
129
+ includeButtons = needsApproval;
130
+ priority = "urgent";
131
+ break;
132
+
133
+ case "QuotaExhausted":
134
+ shouldNotify = true;
135
+ includeButtons = false;
136
+ priority = "high";
137
+ break;
138
+
139
+ case "JobCompleted":
140
+ shouldNotify = this.preferences.notifyOnCompletion;
141
+ includeButtons = false;
142
+ priority = "low";
143
+ break;
144
+
145
+ case "Error":
146
+ case "JobFailed":
147
+ shouldNotify = this.preferences.notifyOnError;
148
+ includeButtons = needsApproval;
149
+ priority = severity === "high" || severity === "critical" ? "high" : "normal";
150
+ break;
151
+
152
+ default:
153
+ shouldNotify = severity === "high" || severity === "critical";
154
+ includeButtons = needsApproval;
155
+ priority = severity === "critical" ? "urgent" : severity === "high" ? "high" : "low";
156
+ }
157
+
158
+ // Always notify for high-priority events
159
+ if (HIGH_PRIORITY_EVENTS.has(event)) {
160
+ shouldNotify = true;
161
+ if (priority !== "urgent") {
162
+ priority = "high";
163
+ }
164
+ }
165
+
166
+ return {
167
+ shouldNotify,
168
+ channels: channels.length > 0 ? channels : ["log"],
169
+ includeButtons,
170
+ priority,
171
+ };
172
+ }
173
+
174
+ /**
175
+ * Infer severity from event name
176
+ */
177
+ private inferSeverity(event: string): "low" | "medium" | "high" | "critical" {
178
+ if (event.includes("Critical") || event.includes("Emergency")) {
179
+ return "critical";
180
+ }
181
+ if (event.includes("Error") || event.includes("Failed") || event.includes("Exhausted")) {
182
+ return "high";
183
+ }
184
+ if (event.includes("Warning") || event.includes("Alert")) {
185
+ return "medium";
186
+ }
187
+ return "low";
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Check if an event requires approval buttons
193
+ */
194
+ export function requiresApproval(event: string): boolean {
195
+ return APPROVAL_REQUIRED_EVENTS.has(event);
196
+ }
197
+
198
+ /**
199
+ * Check if an event is high priority
200
+ */
201
+ export function isHighPriority(event: string): boolean {
202
+ return HIGH_PRIORITY_EVENTS.has(event);
203
+ }