@bytesbrains/pi-telegram-bridge 1.1.3 → 1.1.4

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/src/templates.ts CHANGED
@@ -8,685 +8,774 @@
8
8
 
9
9
  // ── Helpers ─────────────────────────────────────────────────────────
10
10
 
11
- const BAR = "━".repeat(30);
11
+ const BAR = "━".repeat(20);
12
12
 
13
13
  function bold(s: string) {
14
- return `<b>${s}</b>`;
14
+ return `<b>${s}</b>`;
15
15
  }
16
16
  function italic(s: string) {
17
- return `<i>${s}</i>`;
17
+ return `<i>${s}</i>`;
18
18
  }
19
19
  function code(s: string) {
20
- return `<code>${s}</code>`;
20
+ return `<code>${s}</code>`;
21
21
  }
22
22
  function strike(s: string) {
23
- return `<s>${s}</s>`;
23
+ return `<s>${s}</s>`;
24
24
  }
25
25
  function labelValue(label: string, value: string) {
26
- return `${bold(label + ":")} ${value}`;
26
+ return `${bold(label + ":")} ${value}`;
27
27
  }
28
28
  function link(url: string, text: string) {
29
- return `<a href="${url}">${text}</a>`;
29
+ return `<a href="${url}">${text}</a>`;
30
30
  }
31
31
  function progressBar(pct: number, width = 12) {
32
- const filled = Math.round((pct / 100) * width);
33
- return `<code>${"█".repeat(filled)}${"░".repeat(width - filled)}</code> ${pct}%`;
32
+ const filled = Math.round((pct / 100) * width);
33
+ return `<code>${"█".repeat(filled)}${"░".repeat(width - filled)}</code> ${pct}%`;
34
34
  }
35
35
 
36
36
  // ── CI/CD Status Icons ─────────────────────────────────────────────
37
37
 
38
38
  function jobIcon(status: string): string {
39
- const map: Record<string, string> = {
40
- success: "✅",
41
- failure: "❌",
42
- cancelled: "⚪",
43
- skipped: "⏭️",
44
- running: "🔵",
45
- queued: "⏳",
46
- pending: "🟡",
47
- in_progress: "🔵",
48
- };
49
- return map[status] ?? "❓";
39
+ const map: Record<string, string> = {
40
+ success: "✅",
41
+ failure: "❌",
42
+ cancelled: "⚪",
43
+ skipped: "⏭️",
44
+ running: "🔵",
45
+ queued: "⏳",
46
+ pending: "🟡",
47
+ in_progress: "🔵",
48
+ };
49
+ return map[status] ?? "❓";
50
50
  }
51
51
 
52
52
  // ── Issue Tray (compact, swipe-friendly) ───────────────────────────
53
53
 
54
54
  export interface IssueCard {
55
- number: number;
56
- title: string;
57
- state: "open" | "closed";
58
- status?: string;
59
- assignee?: string;
60
- labels?: string[];
61
- milestone?: string;
62
- repo: { owner: string; name: string };
63
- branch?: string;
64
- pr?: { number: number; status: string };
65
- url?: string;
55
+ number: number;
56
+ title: string;
57
+ state: "open" | "closed";
58
+ status?: string;
59
+ assignee?: string;
60
+ labels?: string[];
61
+ milestone?: string;
62
+ repo: { owner: string; name: string };
63
+ branch?: string;
64
+ pr?: { number: number; status: string };
65
+ url?: string;
66
66
  }
67
67
 
68
68
  export function issueCard(i: IssueCard): string {
69
- const icon = i.state === "closed" ? "✅" : i.status === "blocked" ? "🚫" : "📋";
70
- const closed = i.state === "closed";
71
-
72
- let msg = `${icon} ${bold(`Issue #${i.number}`)}${closed ? strike("") : ""}: ${closed ? strike(i.title) : bold(i.title)}\n`;
73
- msg += `${BAR}\n`;
74
-
75
- if (i.status) {
76
- const statusIcon = i.status === "in progress" ? "🔄" : i.status === "blocked" ? "🚫" : "📌";
77
- msg += labelValue("Status", `${closed ? strike(`${statusIcon} ${i.status}`) : `${statusIcon} ${i.status}`}`) + "\n";
78
- }
79
- if (i.assignee) msg += labelValue("Assignee", code(i.assignee)) + "\n";
80
- if (i.labels && i.labels.length > 0) {
81
- msg += labelValue("Labels", i.labels.map((l) => code(l)).join(" ")) + "\n";
82
- }
83
- if (i.milestone) msg += labelValue("Milestone", italic(i.milestone)) + "\n";
84
- msg += labelValue("Repo", italic(`${i.repo.owner}/${i.repo.name}`)) + "\n";
85
- msg += `${BAR}\n`;
86
- if (i.branch) msg += labelValue("Branch", code(i.branch)) + "\n";
87
- if (i.pr && i.url) {
88
- msg += labelValue("PR", `${link(i.url, `#${i.pr.number}`)} — ${i.pr.status}`) + "\n";
89
- } else if (i.pr) {
90
- msg += labelValue("PR", `#${i.pr.number} — ${i.pr.status}`) + "\n";
91
- }
92
- if (i.url) msg += `\n${link(i.url, "View on GitHub")}`;
93
- return msg;
69
+ const icon =
70
+ i.state === "closed" ? "✅" : i.status === "blocked" ? "🚫" : "📋";
71
+ const closed = i.state === "closed";
72
+
73
+ let msg = `${icon} ${bold(`Issue #${i.number}`)}${closed ? strike("") : ""}: ${closed ? strike(i.title) : bold(i.title)}\n`;
74
+ msg += `${BAR}\n`;
75
+
76
+ if (i.status) {
77
+ const statusIcon =
78
+ i.status === "in progress" ? "🔄" : i.status === "blocked" ? "🚫" : "📌";
79
+ msg +=
80
+ labelValue(
81
+ "Status",
82
+ `${closed ? strike(`${statusIcon} ${i.status}`) : `${statusIcon} ${i.status}`}`,
83
+ ) + "\n";
84
+ }
85
+ if (i.assignee) msg += labelValue("Assignee", code(i.assignee)) + "\n";
86
+ if (i.labels && i.labels.length > 0) {
87
+ msg += labelValue("Labels", i.labels.map((l) => code(l)).join(" ")) + "\n";
88
+ }
89
+ if (i.milestone) msg += labelValue("Milestone", italic(i.milestone)) + "\n";
90
+ msg += labelValue("Repo", italic(`${i.repo.owner}/${i.repo.name}`)) + "\n";
91
+ msg += `${BAR}\n`;
92
+ if (i.branch) msg += labelValue("Branch", code(i.branch)) + "\n";
93
+ if (i.pr && i.url) {
94
+ msg +=
95
+ labelValue("PR", `${link(i.url, `#${i.pr.number}`)} — ${i.pr.status}`) +
96
+ "\n";
97
+ } else if (i.pr) {
98
+ msg += labelValue("PR", `#${i.pr.number} — ${i.pr.status}`) + "\n";
99
+ }
100
+ if (i.url) msg += `\n${link(i.url, "View on GitHub")}`;
101
+ return msg;
94
102
  }
95
103
 
96
104
  // ── PR Status Card ──────────────────────────────────────────────────
97
105
 
98
106
  export interface PRCard {
99
- number: number;
100
- title: string;
101
- state: "open" | "closed" | "merged" | "draft";
102
- author: string;
103
- base: string;
104
- head: string;
105
- repo: { owner: string; name: string };
106
- ci?: { passed: number; failed: number; total: number; status?: string };
107
- reviews?: { approved: number; requested: number; changesRequested: number };
108
- files?: { added: number; removed: number; changed: number };
109
- breaking?: boolean;
110
- draft?: boolean;
111
- url?: string;
107
+ number: number;
108
+ title: string;
109
+ state: "open" | "closed" | "merged" | "draft";
110
+ author: string;
111
+ base: string;
112
+ head: string;
113
+ repo: { owner: string; name: string };
114
+ ci?: { passed: number; failed: number; total: number; status?: string };
115
+ reviews?: { approved: number; requested: number; changesRequested: number };
116
+ files?: { added: number; removed: number; changed: number };
117
+ breaking?: boolean;
118
+ draft?: boolean;
119
+ url?: string;
112
120
  }
113
121
 
114
122
  export function prCard(pr: PRCard): string {
115
- const stateIcon =
116
- pr.state === "merged" ? "💜" : pr.state === "closed" ? "⚫" : pr.draft ? "📝" : "🔀";
117
- const stateBadge =
118
- pr.state === "merged"
119
- ? "💜 Merged"
120
- : pr.state === "closed"
121
- ? "⚫ Closed"
122
- : pr.draft
123
- ? "📝 Draft"
124
- : "🟡 Open";
125
-
126
- let msg = `${stateIcon} ${bold(`PR #${pr.number}`)}: ${italic(pr.title)}\n`;
127
- msg += `${BAR}\n`;
128
- msg += labelValue("Status", stateBadge) + "\n";
129
- msg += labelValue("Author", code(pr.author)) + "\n";
130
- msg += labelValue("Branch", `${code(pr.base)} ← ${code(pr.head)}`) + "\n";
131
-
132
- if (pr.ci) {
133
- const icon = pr.ci.status
134
- ? jobIcon(pr.ci.status)
135
- : pr.ci.failed === 0
136
- ? ""
137
- : "❌";
138
- msg +=
139
- labelValue(
140
- "CI",
141
- `${icon} ${pr.ci.passed}/${pr.ci.total} checks passed` +
142
- (pr.ci.failed > 0 ? ` · ${pr.ci.failed} failed` : ""),
143
- ) + "\n";
144
- }
145
-
146
- if (pr.reviews) {
147
- const parts: string[] = [];
148
- if (pr.reviews.approved > 0) parts.push(`✅ ${pr.reviews.approved} approved`);
149
- if (pr.reviews.changesRequested > 0)
150
- parts.push(`🔴 ${pr.reviews.changesRequested} changes req`);
151
- if (pr.reviews.requested > 0) parts.push(`⏳ ${pr.reviews.requested} pending`);
152
- msg += labelValue("Reviews", parts.join(" · ")) + "\n";
153
- }
154
-
155
- if (pr.files) {
156
- msg +=
157
- labelValue(
158
- "Files",
159
- `${pr.files.changed} files (+${pr.files.added} −${pr.files.removed})`,
160
- ) + "\n";
161
- }
162
- if (pr.breaking) msg += labelValue("Breaking", "⚠️ Yes") + "\n";
163
-
164
- msg += `${BAR}\n`;
165
- if (pr.url) msg += link(pr.url, "View on GitHub");
166
- return msg;
123
+ const stateIcon =
124
+ pr.state === "merged"
125
+ ? "💜"
126
+ : pr.state === "closed"
127
+ ? ""
128
+ : pr.draft
129
+ ? "📝"
130
+ : "🔀";
131
+ const stateBadge =
132
+ pr.state === "merged"
133
+ ? "💜 Merged"
134
+ : pr.state === "closed"
135
+ ? "⚫ Closed"
136
+ : pr.draft
137
+ ? "📝 Draft"
138
+ : "🟡 Open";
139
+
140
+ let msg = `${stateIcon} ${bold(`PR #${pr.number}`)}: ${italic(pr.title)}\n`;
141
+ msg += `${BAR}\n`;
142
+ msg += labelValue("Status", stateBadge) + "\n";
143
+ msg += labelValue("Author", code(pr.author)) + "\n";
144
+ msg += labelValue("Branch", `${code(pr.base)} ← ${code(pr.head)}`) + "\n";
145
+
146
+ if (pr.ci) {
147
+ const icon = pr.ci.status
148
+ ? jobIcon(pr.ci.status)
149
+ : pr.ci.failed === 0
150
+ ? ""
151
+ : "";
152
+ msg +=
153
+ labelValue(
154
+ "CI",
155
+ `${icon} ${pr.ci.passed}/${pr.ci.total} checks passed` +
156
+ (pr.ci.failed > 0 ? ` · ${pr.ci.failed} failed` : ""),
157
+ ) + "\n";
158
+ }
159
+
160
+ if (pr.reviews) {
161
+ const parts: string[] = [];
162
+ if (pr.reviews.approved > 0)
163
+ parts.push(`✅ ${pr.reviews.approved} approved`);
164
+ if (pr.reviews.changesRequested > 0)
165
+ parts.push(`🔴 ${pr.reviews.changesRequested} changes req`);
166
+ if (pr.reviews.requested > 0)
167
+ parts.push(`⏳ ${pr.reviews.requested} pending`);
168
+ msg += labelValue("Reviews", parts.join(" · ")) + "\n";
169
+ }
170
+
171
+ if (pr.files) {
172
+ msg +=
173
+ labelValue(
174
+ "Files",
175
+ `${pr.files.changed} files (+${pr.files.added} −${pr.files.removed})`,
176
+ ) + "\n";
177
+ }
178
+ if (pr.breaking) msg += labelValue("Breaking", "⚠️ Yes") + "\n";
179
+
180
+ msg += `${BAR}\n`;
181
+ if (pr.url) msg += link(pr.url, "View on GitHub");
182
+ return msg;
167
183
  }
168
184
 
169
185
  // ── Work / Task Progress ────────────────────────────────────────────
170
186
 
171
187
  export interface TaskProgress {
172
- title: string;
173
- emoji?: string;
174
- status: "started" | "in-progress" | "paused" | "blocked" | "complete";
175
- progress?: number;
176
- elapsed?: string;
177
- eta?: string;
178
- details?: string[];
179
- metrics?: Record<string, string>;
188
+ title: string;
189
+ emoji?: string;
190
+ status: "started" | "in-progress" | "paused" | "blocked" | "complete";
191
+ progress?: number;
192
+ elapsed?: string;
193
+ eta?: string;
194
+ details?: string[];
195
+ metrics?: Record<string, string>;
180
196
  }
181
197
 
182
198
  export function taskProgress(t: TaskProgress): string {
183
- const icons: Record<string, string> = {
184
- started: "🟢",
185
- "in-progress": "🔵",
186
- paused: "🟡",
187
- blocked: "🚫",
188
- complete: "✅",
189
- };
190
- const emoji = t.emoji ?? "🛠️";
191
- const icon = icons[t.status] ?? "🔵";
192
-
193
- let msg = `${emoji} ${bold(t.title)}\n`;
194
- msg += `${BAR}\n`;
195
- msg += labelValue("Status", `${icon} ${t.status}`) + "\n";
196
-
197
- if (t.progress !== undefined) {
198
- msg += labelValue("Progress", progressBar(t.progress)) + "\n";
199
- }
200
- if (t.elapsed) msg += labelValue("Elapsed", code(t.elapsed)) + "\n";
201
- if (t.eta) msg += labelValue("ETA", code(t.eta)) + "\n";
202
-
203
- if (t.details && t.details.length > 0) {
204
- msg += `${BAR}\n`;
205
- for (const d of t.details) {
206
- msg += ` ${d}\n`;
207
- }
208
- }
209
-
210
- if (t.metrics && Object.keys(t.metrics).length > 0) {
211
- msg += `${BAR}\n`;
212
- for (const [k, v] of Object.entries(t.metrics)) {
213
- msg += labelValue(k, code(v)) + "\n";
214
- }
215
- }
216
- return msg;
199
+ const icons: Record<string, string> = {
200
+ started: "🟢",
201
+ "in-progress": "🔵",
202
+ paused: "🟡",
203
+ blocked: "🚫",
204
+ complete: "✅",
205
+ };
206
+ const emoji = t.emoji ?? "🛠️";
207
+ const icon = icons[t.status] ?? "🔵";
208
+
209
+ let msg = `${emoji} ${bold(t.title)}\n`;
210
+ msg += `${BAR}\n`;
211
+ msg += labelValue("Status", `${icon} ${t.status}`) + "\n";
212
+
213
+ if (t.progress !== undefined) {
214
+ msg += labelValue("Progress", progressBar(t.progress)) + "\n";
215
+ }
216
+ if (t.elapsed) msg += labelValue("Elapsed", code(t.elapsed)) + "\n";
217
+ if (t.eta) msg += labelValue("ETA", code(t.eta)) + "\n";
218
+
219
+ if (t.details && t.details.length > 0) {
220
+ msg += `${BAR}\n`;
221
+ for (const d of t.details) {
222
+ msg += ` ${d}\n`;
223
+ }
224
+ }
225
+
226
+ if (t.metrics && Object.keys(t.metrics).length > 0) {
227
+ msg += `${BAR}\n`;
228
+ for (const [k, v] of Object.entries(t.metrics)) {
229
+ msg += labelValue(k, code(v)) + "\n";
230
+ }
231
+ }
232
+ return msg;
217
233
  }
218
234
 
219
235
  // ── CI Pipeline ─────────────────────────────────────────────────────
220
236
 
221
237
  export interface CIPipeline {
222
- workflow: string;
223
- runNumber: number;
224
- branch: string;
225
- event: string;
226
- jobs: Array<{ name: string; status: string; duration?: string }>;
227
- triggeredBy?: string;
228
- url?: string;
238
+ workflow: string;
239
+ runNumber: number;
240
+ branch: string;
241
+ event: string;
242
+ jobs: Array<{ name: string; status: string; duration?: string }>;
243
+ triggeredBy?: string;
244
+ url?: string;
229
245
  }
230
246
 
231
247
  export function ciPipeline(p: CIPipeline): string {
232
- let msg = `🔄 ${bold(p.workflow)}\n`;
233
- msg += `${BAR}\n`;
234
- msg += labelValue("Run", `#${p.runNumber}`) + "\n";
235
- msg += labelValue("Branch", code(p.branch)) + "\n";
236
- msg += labelValue("Trigger", p.event) + "\n";
237
- if (p.triggeredBy) msg += labelValue("By", code(p.triggeredBy)) + "\n";
238
- msg += `${BAR}\n`;
248
+ let msg = `🔄 ${bold(p.workflow)}\n`;
249
+ msg += `${BAR}\n`;
250
+ msg += labelValue("Run", `#${p.runNumber}`) + "\n";
251
+ msg += labelValue("Branch", code(p.branch)) + "\n";
252
+ msg += labelValue("Trigger", p.event) + "\n";
253
+ if (p.triggeredBy) msg += labelValue("By", code(p.triggeredBy)) + "\n";
254
+ msg += `${BAR}\n`;
239
255
 
240
- for (const j of p.jobs) {
241
- const dur = j.duration ? ` (${j.duration})` : "";
242
- msg += `${jobIcon(j.status)} ${code(j.name)}${dur}\n`;
243
- }
256
+ for (const j of p.jobs) {
257
+ const dur = j.duration ? ` (${j.duration})` : "";
258
+ msg += `${jobIcon(j.status)} ${code(j.name)}${dur}\n`;
259
+ }
244
260
 
245
- if (p.url) msg += `\n${link(p.url, "View Pipeline")}`;
246
- return msg;
261
+ if (p.url) msg += `\n${link(p.url, "View Pipeline")}`;
262
+ return msg;
247
263
  }
248
264
 
249
265
  // ── Agent Session Card ──────────────────────────────────────────────
250
266
 
251
267
  export interface SessionCard {
252
- session: string;
253
- branch?: string;
254
- model?: string;
255
- turns?: number;
256
- tokens?: { used: number; total: number };
257
- duration?: string;
258
- status: "idle" | "streaming" | "tool-exec" | "waiting";
268
+ session: string;
269
+ branch?: string;
270
+ model?: string;
271
+ turns?: number;
272
+ tokens?: { used: number; total: number };
273
+ duration?: string;
274
+ status: "idle" | "streaming" | "tool-exec" | "waiting";
259
275
  }
260
276
 
261
277
  export function sessionCard(s: SessionCard): string {
262
- const statusIcons: Record<string, string> = {
263
- idle: "⏸️",
264
- streaming: "💬",
265
- "tool-exec": "🔧",
266
- waiting: "⏳",
267
- };
268
- const icon = statusIcons[s.status] ?? "🤖";
269
-
270
- let msg = `${icon} ${bold("Agent Session")}\n`;
271
- msg += `${BAR}\n`;
272
- msg += labelValue("Session", code(s.session)) + "\n";
273
- if (s.branch) msg += labelValue("Branch", code(s.branch)) + "\n";
274
- if (s.model) msg += labelValue("Model", italic(s.model)) + "\n";
275
- if (s.turns !== undefined) msg += labelValue("Turns", `${s.turns}`) + "\n";
276
- if (s.tokens) {
277
- const pct = Math.round((s.tokens.used / s.tokens.total) * 100);
278
- msg += labelValue("Tokens", `${s.tokens.used.toLocaleString()} / ${s.tokens.total.toLocaleString()} (${pct}%)`) + "\n";
279
- }
280
- if (s.duration) msg += labelValue("Duration", code(s.duration)) + "\n";
281
- return msg;
278
+ const statusIcons: Record<string, string> = {
279
+ idle: "⏸️",
280
+ streaming: "💬",
281
+ "tool-exec": "🔧",
282
+ waiting: "⏳",
283
+ };
284
+ const icon = statusIcons[s.status] ?? "🤖";
285
+
286
+ let msg = `${icon} ${bold("Agent Session")}\n`;
287
+ msg += `${BAR}\n`;
288
+ msg += labelValue("Session", code(s.session)) + "\n";
289
+ if (s.branch) msg += labelValue("Branch", code(s.branch)) + "\n";
290
+ if (s.model) msg += labelValue("Model", italic(s.model)) + "\n";
291
+ if (s.turns !== undefined) msg += labelValue("Turns", `${s.turns}`) + "\n";
292
+ if (s.tokens) {
293
+ const pct = Math.round((s.tokens.used / s.tokens.total) * 100);
294
+ msg +=
295
+ labelValue(
296
+ "Tokens",
297
+ `${s.tokens.used.toLocaleString()} / ${s.tokens.total.toLocaleString()} (${pct}%)`,
298
+ ) + "\n";
299
+ }
300
+ if (s.duration) msg += labelValue("Duration", code(s.duration)) + "\n";
301
+ return msg;
282
302
  }
283
303
 
284
304
  // ── Alert / Error ───────────────────────────────────────────────────
285
305
 
286
306
  export interface AlertCard {
287
- level: "info" | "warning" | "error" | "critical";
288
- title: string;
289
- source?: string;
290
- detail?: string;
291
- action?: string;
292
- stack?: string;
307
+ level: "info" | "warning" | "error" | "critical";
308
+ title: string;
309
+ source?: string;
310
+ detail?: string;
311
+ action?: string;
312
+ stack?: string;
293
313
  }
294
314
 
295
315
  export function alertCard(a: AlertCard): string {
296
- const icons: Record<string, string> = {
297
- info: "ℹ️",
298
- warning: "⚠️",
299
- error: "🚨",
300
- critical: "🛑",
301
- };
302
- const icon = icons[a.level] ?? "📢";
303
-
304
- let msg = `${icon} ${bold(a.title)}\n`;
305
- msg += `${BAR}\n`;
306
- if (a.source) msg += labelValue("Source", code(a.source)) + "\n";
307
- if (a.detail) msg += labelValue("Detail", italic(a.detail)) + "\n";
308
- if (a.action) msg += labelValue("Action", bold(a.action)) + "\n";
309
- if (a.stack) {
310
- msg += `\n${code(a.stack.slice(0, 300))}\n`;
311
- }
312
- return msg;
316
+ const icons: Record<string, string> = {
317
+ info: "ℹ️",
318
+ warning: "⚠️",
319
+ error: "🚨",
320
+ critical: "🛑",
321
+ };
322
+ const icon = icons[a.level] ?? "📢";
323
+
324
+ let msg = `${icon} ${bold(a.title)}\n`;
325
+ msg += `${BAR}\n`;
326
+ if (a.source) msg += labelValue("Source", code(a.source)) + "\n";
327
+ if (a.detail) msg += labelValue("Detail", italic(a.detail)) + "\n";
328
+ if (a.action) msg += labelValue("Action", bold(a.action)) + "\n";
329
+ if (a.stack) {
330
+ msg += `\n${code(a.stack.slice(0, 300))}\n`;
331
+ }
332
+ return msg;
313
333
  }
314
334
 
315
335
  // ── Factory / Orchestrator ──────────────────────────────────────────
316
336
 
317
337
  export interface FactoryJobCard {
318
- jobId: string;
319
- mode: "single" | "chain" | "parallel";
320
- status: "pending" | "running" | "success" | "failure" | "cancelled";
321
- agent?: string;
322
- task?: string;
323
- environment?: string;
324
- duration?: string;
325
- worker?: string;
338
+ jobId: string;
339
+ mode: "single" | "chain" | "parallel";
340
+ status: "pending" | "running" | "success" | "failure" | "cancelled";
341
+ agent?: string;
342
+ task?: string;
343
+ environment?: string;
344
+ duration?: string;
345
+ worker?: string;
326
346
  }
327
347
 
328
348
  export function factoryJobCard(j: FactoryJobCard): string {
329
- const icons: Record<string, string> = {
330
- pending: "⏳",
331
- running: "🔵",
332
- success: "✅",
333
- failure: "❌",
334
- cancelled: "⚪",
335
- };
336
- const icon = icons[j.status] ?? "❓";
337
-
338
- let msg = `${icon} ${bold("Factory Job")} ${code(j.jobId)}\n`;
339
- msg += `${BAR}\n`;
340
- msg += labelValue("Status", `${icon} ${j.status}`) + "\n";
341
- msg += labelValue("Mode", code(j.mode)) + "\n";
342
- if (j.agent) msg += labelValue("Agent", code(j.agent)) + "\n";
343
- if (j.task) msg += labelValue("Task", italic(j.task.slice(0, 120))) + "\n";
344
- if (j.environment) msg += labelValue("Env", code(j.environment)) + "\n";
345
- if (j.worker) msg += labelValue("Worker", code(j.worker)) + "\n";
346
- if (j.duration) msg += labelValue("Duration", code(j.duration)) + "\n";
347
- return msg;
349
+ const icons: Record<string, string> = {
350
+ pending: "⏳",
351
+ running: "🔵",
352
+ success: "✅",
353
+ failure: "❌",
354
+ cancelled: "⚪",
355
+ };
356
+ const icon = icons[j.status] ?? "❓";
357
+
358
+ let msg = `${icon} ${bold("Factory Job")} ${code(j.jobId)}\n`;
359
+ msg += `${BAR}\n`;
360
+ msg += labelValue("Status", `${icon} ${j.status}`) + "\n";
361
+ msg += labelValue("Mode", code(j.mode)) + "\n";
362
+ if (j.agent) msg += labelValue("Agent", code(j.agent)) + "\n";
363
+ if (j.task) msg += labelValue("Task", italic(j.task.slice(0, 120))) + "\n";
364
+ if (j.environment) msg += labelValue("Env", code(j.environment)) + "\n";
365
+ if (j.worker) msg += labelValue("Worker", code(j.worker)) + "\n";
366
+ if (j.duration) msg += labelValue("Duration", code(j.duration)) + "\n";
367
+ return msg;
348
368
  }
349
369
 
350
370
  // ── Multi-Item Summaries ────────────────────────────────────────────
351
371
 
352
372
  export interface SummaryHeader {
353
- title: string;
354
- subtitle?: string;
355
- items?: Array<{ icon: string; label: string; value: string }>;
373
+ title: string;
374
+ subtitle?: string;
375
+ items?: Array<{ icon: string; label: string; value: string }>;
356
376
  }
357
377
 
358
378
  export function summaryHeader(s: SummaryHeader): string {
359
- let msg = `${bold(s.title)}\n`;
360
- if (s.subtitle) msg += `${italic(s.subtitle)}\n`;
361
- msg += `${BAR}\n`;
362
- if (s.items) {
363
- for (const item of s.items) {
364
- msg += `${item.icon} ${labelValue(item.label, code(item.value))}\n`;
365
- }
366
- }
367
- return msg;
379
+ let msg = `${bold(s.title)}\n`;
380
+ if (s.subtitle) msg += `${italic(s.subtitle)}\n`;
381
+ msg += `${BAR}\n`;
382
+ if (s.items) {
383
+ for (const item of s.items) {
384
+ msg += `${item.icon} ${labelValue(item.label, code(item.value))}\n`;
385
+ }
386
+ }
387
+ return msg;
368
388
  }
369
389
 
370
390
  // ── Quick Acknowledgment ────────────────────────────────────────────
371
391
 
372
392
  export function ack(text: string, emoji = "✅"): string {
373
- return `${emoji} ${italic(text)}`;
393
+ return `${emoji} ${italic(text)}`;
374
394
  }
375
395
 
376
396
  // ── Decision Card ───────────────────────────────────────────────────
377
397
 
378
398
  export interface DecisionCard {
379
- question: string;
380
- context: string;
381
- options: string[];
382
- risk: "low" | "medium" | "high" | "critical";
383
- command?: string;
384
- reason?: string;
399
+ question: string;
400
+ context: string;
401
+ options: string[];
402
+ risk: "low" | "medium" | "high" | "critical";
403
+ command?: string;
404
+ reason?: string;
385
405
  }
386
406
 
387
407
  export function decisionCard(d: DecisionCard): string {
388
- const riskIcons: Record<string, string> = {
389
- low: "🟢",
390
- medium: "🟡",
391
- high: "🟠",
392
- critical: "🔴",
393
- };
394
-
395
- let msg = `🛑 ${bold("Decision Required")}\n`;
396
- msg += `${BAR}\n`;
397
- msg += labelValue("Risk", `${riskIcons[d.risk]} ${d.risk}`) + "\n";
398
- msg += labelValue("Context", italic(d.context)) + "\n";
399
- if (d.command) msg += labelValue("Command", code(d.command)) + "\n";
400
- if (d.reason) msg += labelValue("Reason", d.reason) + "\n";
401
- msg += `\n${bold("❓ " + d.question)}\n`;
402
- msg += `\n${d.options.map((o, i) => `${i + 1}. ${o}`).join("\n")}`;
403
- return msg;
408
+ const riskIcons: Record<string, string> = {
409
+ low: "🟢",
410
+ medium: "🟡",
411
+ high: "🟠",
412
+ critical: "🔴",
413
+ };
414
+
415
+ let msg = `🛑 ${bold("Decision Required")}\n`;
416
+ msg += `${BAR}\n`;
417
+ msg += labelValue("Risk", `${riskIcons[d.risk]} ${d.risk}`) + "\n";
418
+ msg += labelValue("Context", italic(d.context)) + "\n";
419
+ if (d.command) msg += labelValue("Command", code(d.command)) + "\n";
420
+ if (d.reason) msg += labelValue("Reason", d.reason) + "\n";
421
+ msg += `\n${bold("❓ " + d.question)}\n`;
422
+ msg += `\n${d.options.map((o, i) => `${i + 1}. ${o}`).join("\n")}`;
423
+ return msg;
404
424
  }
405
425
 
406
426
  // ── Composite: Daily Standup / Status Roundup ───────────────────────
407
427
 
408
428
  export interface StandupCard {
409
- date: string;
410
- agent: string;
411
- workedOn: string[];
412
- nextUp: string[];
413
- blockers: string[];
414
- metrics?: Record<string, string>;
429
+ date: string;
430
+ agent: string;
431
+ workedOn: string[];
432
+ nextUp: string[];
433
+ blockers: string[];
434
+ metrics?: Record<string, string>;
415
435
  }
416
436
 
417
437
  export function standupCard(s: StandupCard): string {
418
- let msg = `📊 ${bold("Status Roundup")}\n`;
419
- msg += `${BAR}\n`;
420
- msg += labelValue("Date", code(s.date)) + "\n";
421
- msg += labelValue("Agent", code(s.agent)) + "\n";
422
- msg += `${BAR}\n`;
423
-
424
- msg += `${bold("✅ Completed")}\n`;
425
- for (const item of s.workedOn) {
426
- msg += ` • ${item}\n`;
427
- }
428
- msg += `\n${bold("🔄 Next")}\n`;
429
- for (const item of s.nextUp) {
430
- msg += ` • ${item}\n`;
431
- }
432
- if (s.blockers.length > 0) {
433
- msg += `\n${bold("🚫 Blockers")}\n`;
434
- for (const item of s.blockers) {
435
- msg += ` • ${item}\n`;
436
- }
437
- }
438
- if (s.metrics && Object.keys(s.metrics).length > 0) {
439
- msg += `${BAR}\n`;
440
- for (const [k, v] of Object.entries(s.metrics)) {
441
- msg += labelValue(k, code(v)) + "\n";
442
- }
443
- }
444
- return msg;
438
+ let msg = `📊 ${bold("Status Roundup")}\n`;
439
+ msg += `${BAR}\n`;
440
+ msg += labelValue("Date", code(s.date)) + "\n";
441
+ msg += labelValue("Agent", code(s.agent)) + "\n";
442
+ msg += `${BAR}\n`;
443
+
444
+ msg += `${bold("✅ Completed")}\n`;
445
+ for (const item of s.workedOn) {
446
+ msg += ` • ${item}\n`;
447
+ }
448
+ msg += `\n${bold("🔄 Next")}\n`;
449
+ for (const item of s.nextUp) {
450
+ msg += ` • ${item}\n`;
451
+ }
452
+ if (s.blockers.length > 0) {
453
+ msg += `\n${bold("🚫 Blockers")}\n`;
454
+ for (const item of s.blockers) {
455
+ msg += ` • ${item}\n`;
456
+ }
457
+ }
458
+ if (s.metrics && Object.keys(s.metrics).length > 0) {
459
+ msg += `${BAR}\n`;
460
+ for (const [k, v] of Object.entries(s.metrics)) {
461
+ msg += labelValue(k, code(v)) + "\n";
462
+ }
463
+ }
464
+ return msg;
445
465
  }
446
466
 
447
467
  // ── Diff / Change Summary ───────────────────────────────────────────
448
468
 
449
469
  export interface DiffSummary {
450
- title: string;
451
- stats: { added: number; removed: number; files: number };
452
- highlights: string[];
453
- url?: string;
470
+ title: string;
471
+ stats: { added: number; removed: number; files: number };
472
+ highlights: string[];
473
+ url?: string;
454
474
  }
455
475
 
456
476
  export function diffSummary(d: DiffSummary): string {
457
- let msg = `📝 ${bold(d.title)}\n`;
458
- msg += `${BAR}\n`;
459
- msg +=
460
- labelValue(
461
- "Changes",
462
- `+${d.stats.added} −${d.stats.removed} in ${d.stats.files} files`,
463
- ) + "\n";
464
- if (d.highlights.length > 0) {
465
- msg += `${BAR}\n`;
466
- for (const h of d.highlights) {
467
- msg += ` • ${h}\n`;
468
- }
469
- }
470
- if (d.url) msg += `\n${link(d.url, "View Diff")}`;
471
- return msg;
477
+ let msg = `📝 ${bold(d.title)}\n`;
478
+ msg += `${BAR}\n`;
479
+ msg +=
480
+ labelValue(
481
+ "Changes",
482
+ `+${d.stats.added} −${d.stats.removed} in ${d.stats.files} files`,
483
+ ) + "\n";
484
+ if (d.highlights.length > 0) {
485
+ msg += `${BAR}\n`;
486
+ for (const h of d.highlights) {
487
+ msg += ` • ${h}\n`;
488
+ }
489
+ }
490
+ if (d.url) msg += `\n${link(d.url, "View Diff")}`;
491
+ return msg;
472
492
  }
473
493
 
474
494
  // ── Quick Pickers (for telegram_ask buttons) ────────────────────────
475
495
 
476
496
  export function quickActions(label: string, actions: string[]): string {
477
- return `${bold(label)}\n${actions.map((a, i) => `${i + 1}. ${code(a)}`).join(" \n")}`;
497
+ return `${bold(label)}\n${actions.map((a, i) => `${i + 1}. ${code(a)}`).join(" \n")}`;
478
498
  }
479
499
 
480
500
  export function yesNoExplain(prompt: string): string {
481
- return `❓ ${bold(prompt)}\n\nPick an option below:`;
501
+ return `❓ ${bold(prompt)}\n\nPick an option below:`;
482
502
  }
483
503
 
484
504
  // ── Build Notification (main dispatch for telegram_notify tool) ────
485
505
 
486
506
  export interface NotifyInput {
487
- kind: "issue" | "pr" | "task" | "pipeline" | "session" | "alert" | "factory-job" | "standup" | "diff" | "decision" | "ack";
488
- issueNumber?: number; issueTitle?: string; issueState?: "open" | "closed";
489
- issueStatus?: string; issueAssignee?: string; issueLabels?: string[];
490
- issueMilestone?: string; issueRepoOwner?: string; issueRepoName?: string;
491
- issueBranch?: string; issuePRNumber?: number; issuePRStatus?: string;
492
- issueUrl?: string;
493
- prNumber?: number; prTitle?: string; prState?: "open" | "closed" | "merged" | "draft";
494
- prAuthor?: string; prBase?: string; prHead?: string;
495
- prRepoOwner?: string; prRepoName?: string;
496
- prCIPassed?: number; prCIFailed?: number; prCITotal?: number; prCIStatus?: string;
497
- prReviewsApproved?: number; prReviewsRequested?: number; prReviewsChanges?: number;
498
- prFilesAdded?: number; prFilesRemoved?: number; prFilesChanged?: number;
499
- prBreaking?: boolean; prDraft?: boolean; prUrl?: string;
500
- taskTitle?: string; taskEmoji?: string;
501
- taskStatus?: "started" | "in-progress" | "paused" | "blocked" | "complete";
502
- taskProgress?: number; taskElapsed?: string; taskEta?: string;
503
- taskDetails?: string[]; taskMetrics?: Record<string, string>;
504
- pipelineWorkflow?: string; pipelineRunNumber?: number;
505
- pipelineBranch?: string; pipelineEvent?: string;
506
- pipelineJobs?: Array<{ name: string; status: string; duration?: string }>;
507
- pipelineTriggeredBy?: string; pipelineUrl?: string;
508
- sessionName?: string; sessionBranch?: string; sessionModel?: string;
509
- sessionTurns?: number; sessionTokensUsed?: number; sessionTokensTotal?: number;
510
- sessionDuration?: string; sessionStatus?: "idle" | "streaming" | "tool-exec" | "waiting";
511
- alertLevel?: "info" | "warning" | "error" | "critical";
512
- alertTitle?: string; alertSource?: string; alertDetail?: string;
513
- alertAction?: string; alertStack?: string;
514
- jobId?: string; jobMode?: "single" | "chain" | "parallel";
515
- jobStatus?: "pending" | "running" | "success" | "failure" | "cancelled";
516
- jobAgent?: string; jobTask?: string; jobEnvironment?: string;
517
- jobDuration?: string; jobWorker?: string;
518
- standupDate?: string; standupAgent?: string;
519
- standupWorkedOn?: string[]; standupNextUp?: string[];
520
- standupBlockers?: string[]; standupMetrics?: Record<string, string>;
521
- diffTitle?: string; diffAdded?: number; diffRemoved?: number;
522
- diffFiles?: number; diffHighlights?: string[]; diffUrl?: string;
523
- decisionQuestion?: string; decisionContext?: string;
524
- decisionOptions?: string[]; decisionRisk?: "low" | "medium" | "high" | "critical";
525
- decisionCommand?: string; decisionReason?: string;
526
- ackText?: string; ackEmoji?: string;
507
+ kind:
508
+ | "issue"
509
+ | "pr"
510
+ | "task"
511
+ | "pipeline"
512
+ | "session"
513
+ | "alert"
514
+ | "factory-job"
515
+ | "standup"
516
+ | "diff"
517
+ | "decision"
518
+ | "ack";
519
+ issueNumber?: number;
520
+ issueTitle?: string;
521
+ issueState?: "open" | "closed";
522
+ issueStatus?: string;
523
+ issueAssignee?: string;
524
+ issueLabels?: string[];
525
+ issueMilestone?: string;
526
+ issueRepoOwner?: string;
527
+ issueRepoName?: string;
528
+ issueBranch?: string;
529
+ issuePRNumber?: number;
530
+ issuePRStatus?: string;
531
+ issueUrl?: string;
532
+ prNumber?: number;
533
+ prTitle?: string;
534
+ prState?: "open" | "closed" | "merged" | "draft";
535
+ prAuthor?: string;
536
+ prBase?: string;
537
+ prHead?: string;
538
+ prRepoOwner?: string;
539
+ prRepoName?: string;
540
+ prCIPassed?: number;
541
+ prCIFailed?: number;
542
+ prCITotal?: number;
543
+ prCIStatus?: string;
544
+ prReviewsApproved?: number;
545
+ prReviewsRequested?: number;
546
+ prReviewsChanges?: number;
547
+ prFilesAdded?: number;
548
+ prFilesRemoved?: number;
549
+ prFilesChanged?: number;
550
+ prBreaking?: boolean;
551
+ prDraft?: boolean;
552
+ prUrl?: string;
553
+ taskTitle?: string;
554
+ taskEmoji?: string;
555
+ taskStatus?: "started" | "in-progress" | "paused" | "blocked" | "complete";
556
+ taskProgress?: number;
557
+ taskElapsed?: string;
558
+ taskEta?: string;
559
+ taskDetails?: string[];
560
+ taskMetrics?: Record<string, string>;
561
+ pipelineWorkflow?: string;
562
+ pipelineRunNumber?: number;
563
+ pipelineBranch?: string;
564
+ pipelineEvent?: string;
565
+ pipelineJobs?: Array<{ name: string; status: string; duration?: string }>;
566
+ pipelineTriggeredBy?: string;
567
+ pipelineUrl?: string;
568
+ sessionName?: string;
569
+ sessionBranch?: string;
570
+ sessionModel?: string;
571
+ sessionTurns?: number;
572
+ sessionTokensUsed?: number;
573
+ sessionTokensTotal?: number;
574
+ sessionDuration?: string;
575
+ sessionStatus?: "idle" | "streaming" | "tool-exec" | "waiting";
576
+ alertLevel?: "info" | "warning" | "error" | "critical";
577
+ alertTitle?: string;
578
+ alertSource?: string;
579
+ alertDetail?: string;
580
+ alertAction?: string;
581
+ alertStack?: string;
582
+ jobId?: string;
583
+ jobMode?: "single" | "chain" | "parallel";
584
+ jobStatus?: "pending" | "running" | "success" | "failure" | "cancelled";
585
+ jobAgent?: string;
586
+ jobTask?: string;
587
+ jobEnvironment?: string;
588
+ jobDuration?: string;
589
+ jobWorker?: string;
590
+ standupDate?: string;
591
+ standupAgent?: string;
592
+ standupWorkedOn?: string[];
593
+ standupNextUp?: string[];
594
+ standupBlockers?: string[];
595
+ standupMetrics?: Record<string, string>;
596
+ diffTitle?: string;
597
+ diffAdded?: number;
598
+ diffRemoved?: number;
599
+ diffFiles?: number;
600
+ diffHighlights?: string[];
601
+ diffUrl?: string;
602
+ decisionQuestion?: string;
603
+ decisionContext?: string;
604
+ decisionOptions?: string[];
605
+ decisionRisk?: "low" | "medium" | "high" | "critical";
606
+ decisionCommand?: string;
607
+ decisionReason?: string;
608
+ ackText?: string;
609
+ ackEmoji?: string;
527
610
  }
528
611
 
529
612
  export function buildNotification(input: NotifyInput): string {
530
- switch (input.kind) {
531
- case "issue":
532
- return issueCard({
533
- number: input.issueNumber ?? 0,
534
- title: input.issueTitle ?? "Unknown",
535
- state: input.issueState ?? "open",
536
- status: input.issueStatus,
537
- assignee: input.issueAssignee,
538
- labels: input.issueLabels,
539
- milestone: input.issueMilestone,
540
- repo: {
541
- owner: input.issueRepoOwner ?? "unknown",
542
- name: input.issueRepoName ?? "unknown",
543
- },
544
- branch: input.issueBranch,
545
- pr: input.issuePRNumber
546
- ? { number: input.issuePRNumber, status: input.issuePRStatus ?? "unknown" }
547
- : undefined,
548
- url: input.issueUrl,
549
- });
550
-
551
- case "pr":
552
- return prCard({
553
- number: input.prNumber ?? 0,
554
- title: input.prTitle ?? "Unknown",
555
- state: input.prState ?? "open",
556
- author: input.prAuthor ?? "unknown",
557
- base: input.prBase ?? "main",
558
- head: input.prHead ?? "unknown",
559
- repo: {
560
- owner: input.prRepoOwner ?? "unknown",
561
- name: input.prRepoName ?? "unknown",
562
- },
563
- ci:
564
- input.prCITotal !== undefined
565
- ? {
566
- passed: input.prCIPassed ?? 0,
567
- failed: input.prCIFailed ?? 0,
568
- total: input.prCITotal,
569
- status: input.prCIStatus,
570
- }
571
- : undefined,
572
- reviews:
573
- input.prReviewsApproved !== undefined ||
574
- input.prReviewsRequested !== undefined ||
575
- input.prReviewsChanges !== undefined
576
- ? {
577
- approved: input.prReviewsApproved ?? 0,
578
- requested: input.prReviewsRequested ?? 0,
579
- changesRequested: input.prReviewsChanges ?? 0,
580
- }
581
- : undefined,
582
- files:
583
- input.prFilesChanged !== undefined
584
- ? {
585
- added: input.prFilesAdded ?? 0,
586
- removed: input.prFilesRemoved ?? 0,
587
- changed: input.prFilesChanged,
588
- }
589
- : undefined,
590
- breaking: input.prBreaking,
591
- draft: input.prDraft,
592
- url: input.prUrl,
593
- });
594
-
595
- case "task":
596
- return taskProgress({
597
- title: input.taskTitle ?? "Task",
598
- emoji: input.taskEmoji,
599
- status: input.taskStatus ?? "started",
600
- progress: input.taskProgress,
601
- elapsed: input.taskElapsed,
602
- eta: input.taskEta,
603
- details: input.taskDetails,
604
- metrics: input.taskMetrics,
605
- });
606
-
607
- case "pipeline":
608
- return ciPipeline({
609
- workflow: input.pipelineWorkflow ?? "CI",
610
- runNumber: input.pipelineRunNumber ?? 0,
611
- branch: input.pipelineBranch ?? "unknown",
612
- event: input.pipelineEvent ?? "push",
613
- jobs: input.pipelineJobs ?? [],
614
- triggeredBy: input.pipelineTriggeredBy,
615
- url: input.pipelineUrl,
616
- });
617
-
618
- case "session":
619
- return sessionCard({
620
- session: input.sessionName ?? "unknown",
621
- branch: input.sessionBranch,
622
- model: input.sessionModel,
623
- turns: input.sessionTurns,
624
- tokens:
625
- input.sessionTokensTotal !== undefined
626
- ? { used: input.sessionTokensUsed ?? 0, total: input.sessionTokensTotal }
627
- : undefined,
628
- duration: input.sessionDuration,
629
- status: input.sessionStatus ?? "idle",
630
- });
631
-
632
- case "alert":
633
- return alertCard({
634
- level: input.alertLevel ?? "info",
635
- title: input.alertTitle ?? "Alert",
636
- source: input.alertSource,
637
- detail: input.alertDetail,
638
- action: input.alertAction,
639
- stack: input.alertStack,
640
- });
641
-
642
- case "factory-job":
643
- return factoryJobCard({
644
- jobId: input.jobId ?? "unknown",
645
- mode: input.jobMode ?? "single",
646
- status: input.jobStatus ?? "pending",
647
- agent: input.jobAgent,
648
- task: input.jobTask,
649
- environment: input.jobEnvironment,
650
- duration: input.jobDuration,
651
- worker: input.jobWorker,
652
- });
653
-
654
- case "standup":
655
- return standupCard({
656
- date: input.standupDate ?? "unknown",
657
- agent: input.standupAgent ?? "pi",
658
- workedOn: input.standupWorkedOn ?? [],
659
- nextUp: input.standupNextUp ?? [],
660
- blockers: input.standupBlockers ?? [],
661
- metrics: input.standupMetrics,
662
- });
663
-
664
- case "diff":
665
- return diffSummary({
666
- title: input.diffTitle ?? "Changes",
667
- stats: {
668
- added: input.diffAdded ?? 0,
669
- removed: input.diffRemoved ?? 0,
670
- files: input.diffFiles ?? 0,
671
- },
672
- highlights: input.diffHighlights ?? [],
673
- url: input.diffUrl,
674
- });
675
-
676
- case "decision":
677
- return decisionCard({
678
- question: input.decisionQuestion ?? "Proceed?",
679
- context: input.decisionContext ?? "",
680
- options: input.decisionOptions ?? ["Yes", "No"],
681
- risk: input.decisionRisk ?? "medium",
682
- command: input.decisionCommand,
683
- reason: input.decisionReason,
684
- });
685
-
686
- case "ack":
687
- return ack(input.ackText ?? "Done", input.ackEmoji);
688
-
689
- default:
690
- return "Unknown notification kind";
691
- }
613
+ switch (input.kind) {
614
+ case "issue":
615
+ return issueCard({
616
+ number: input.issueNumber ?? 0,
617
+ title: input.issueTitle ?? "Unknown",
618
+ state: input.issueState ?? "open",
619
+ status: input.issueStatus,
620
+ assignee: input.issueAssignee,
621
+ labels: input.issueLabels,
622
+ milestone: input.issueMilestone,
623
+ repo: {
624
+ owner: input.issueRepoOwner ?? "unknown",
625
+ name: input.issueRepoName ?? "unknown",
626
+ },
627
+ branch: input.issueBranch,
628
+ pr: input.issuePRNumber
629
+ ? {
630
+ number: input.issuePRNumber,
631
+ status: input.issuePRStatus ?? "unknown",
632
+ }
633
+ : undefined,
634
+ url: input.issueUrl,
635
+ });
636
+
637
+ case "pr":
638
+ return prCard({
639
+ number: input.prNumber ?? 0,
640
+ title: input.prTitle ?? "Unknown",
641
+ state: input.prState ?? "open",
642
+ author: input.prAuthor ?? "unknown",
643
+ base: input.prBase ?? "main",
644
+ head: input.prHead ?? "unknown",
645
+ repo: {
646
+ owner: input.prRepoOwner ?? "unknown",
647
+ name: input.prRepoName ?? "unknown",
648
+ },
649
+ ci:
650
+ input.prCITotal !== undefined
651
+ ? {
652
+ passed: input.prCIPassed ?? 0,
653
+ failed: input.prCIFailed ?? 0,
654
+ total: input.prCITotal,
655
+ status: input.prCIStatus,
656
+ }
657
+ : undefined,
658
+ reviews:
659
+ input.prReviewsApproved !== undefined ||
660
+ input.prReviewsRequested !== undefined ||
661
+ input.prReviewsChanges !== undefined
662
+ ? {
663
+ approved: input.prReviewsApproved ?? 0,
664
+ requested: input.prReviewsRequested ?? 0,
665
+ changesRequested: input.prReviewsChanges ?? 0,
666
+ }
667
+ : undefined,
668
+ files:
669
+ input.prFilesChanged !== undefined
670
+ ? {
671
+ added: input.prFilesAdded ?? 0,
672
+ removed: input.prFilesRemoved ?? 0,
673
+ changed: input.prFilesChanged,
674
+ }
675
+ : undefined,
676
+ breaking: input.prBreaking,
677
+ draft: input.prDraft,
678
+ url: input.prUrl,
679
+ });
680
+
681
+ case "task":
682
+ return taskProgress({
683
+ title: input.taskTitle ?? "Task",
684
+ emoji: input.taskEmoji,
685
+ status: input.taskStatus ?? "started",
686
+ progress: input.taskProgress,
687
+ elapsed: input.taskElapsed,
688
+ eta: input.taskEta,
689
+ details: input.taskDetails,
690
+ metrics: input.taskMetrics,
691
+ });
692
+
693
+ case "pipeline":
694
+ return ciPipeline({
695
+ workflow: input.pipelineWorkflow ?? "CI",
696
+ runNumber: input.pipelineRunNumber ?? 0,
697
+ branch: input.pipelineBranch ?? "unknown",
698
+ event: input.pipelineEvent ?? "push",
699
+ jobs: input.pipelineJobs ?? [],
700
+ triggeredBy: input.pipelineTriggeredBy,
701
+ url: input.pipelineUrl,
702
+ });
703
+
704
+ case "session":
705
+ return sessionCard({
706
+ session: input.sessionName ?? "unknown",
707
+ branch: input.sessionBranch,
708
+ model: input.sessionModel,
709
+ turns: input.sessionTurns,
710
+ tokens:
711
+ input.sessionTokensTotal !== undefined
712
+ ? {
713
+ used: input.sessionTokensUsed ?? 0,
714
+ total: input.sessionTokensTotal,
715
+ }
716
+ : undefined,
717
+ duration: input.sessionDuration,
718
+ status: input.sessionStatus ?? "idle",
719
+ });
720
+
721
+ case "alert":
722
+ return alertCard({
723
+ level: input.alertLevel ?? "info",
724
+ title: input.alertTitle ?? "Alert",
725
+ source: input.alertSource,
726
+ detail: input.alertDetail,
727
+ action: input.alertAction,
728
+ stack: input.alertStack,
729
+ });
730
+
731
+ case "factory-job":
732
+ return factoryJobCard({
733
+ jobId: input.jobId ?? "unknown",
734
+ mode: input.jobMode ?? "single",
735
+ status: input.jobStatus ?? "pending",
736
+ agent: input.jobAgent,
737
+ task: input.jobTask,
738
+ environment: input.jobEnvironment,
739
+ duration: input.jobDuration,
740
+ worker: input.jobWorker,
741
+ });
742
+
743
+ case "standup":
744
+ return standupCard({
745
+ date: input.standupDate ?? "unknown",
746
+ agent: input.standupAgent ?? "pi",
747
+ workedOn: input.standupWorkedOn ?? [],
748
+ nextUp: input.standupNextUp ?? [],
749
+ blockers: input.standupBlockers ?? [],
750
+ metrics: input.standupMetrics,
751
+ });
752
+
753
+ case "diff":
754
+ return diffSummary({
755
+ title: input.diffTitle ?? "Changes",
756
+ stats: {
757
+ added: input.diffAdded ?? 0,
758
+ removed: input.diffRemoved ?? 0,
759
+ files: input.diffFiles ?? 0,
760
+ },
761
+ highlights: input.diffHighlights ?? [],
762
+ url: input.diffUrl,
763
+ });
764
+
765
+ case "decision":
766
+ return decisionCard({
767
+ question: input.decisionQuestion ?? "Proceed?",
768
+ context: input.decisionContext ?? "",
769
+ options: input.decisionOptions ?? ["Yes", "No"],
770
+ risk: input.decisionRisk ?? "medium",
771
+ command: input.decisionCommand,
772
+ reason: input.decisionReason,
773
+ });
774
+
775
+ case "ack":
776
+ return ack(input.ackText ?? "Done", input.ackEmoji);
777
+
778
+ default:
779
+ return "Unknown notification kind";
780
+ }
692
781
  }