@pi-unipi/unipi 2.1.2 → 2.1.3

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 (41) hide show
  1. package/package.json +22 -22
  2. package/packages/ask-user/package.json +3 -3
  3. package/packages/autocomplete/package.json +1 -1
  4. package/packages/btw/package.json +2 -2
  5. package/packages/cocoindex/package.json +2 -2
  6. package/packages/compactor/package.json +4 -4
  7. package/packages/core/package.json +1 -1
  8. package/packages/footer/package.json +3 -3
  9. package/packages/info-screen/package.json +2 -2
  10. package/packages/input-shortcuts/package.json +3 -3
  11. package/packages/kanboard/package.json +2 -2
  12. package/packages/mcp/package.json +2 -2
  13. package/packages/memory/package.json +3 -3
  14. package/packages/milestone/package.json +2 -2
  15. package/packages/notify/README.md +2 -1
  16. package/packages/notify/events.ts +119 -81
  17. package/packages/notify/package.json +2 -2
  18. package/packages/notify/settings.ts +1 -0
  19. package/packages/notify/skills/configure-notify/SKILL.md +3 -1
  20. package/packages/notify/src/__tests__/event-bus.test.ts +3 -2
  21. package/packages/ralph/package.json +3 -3
  22. package/packages/subagents/package.json +5 -5
  23. package/packages/updater/package.json +3 -3
  24. package/packages/utility/package.json +2 -2
  25. package/packages/web-api/package.json +2 -2
  26. package/packages/workflow/package.json +2 -2
  27. package/packages/info-screen/tui/info-overlay.ts +0 -504
  28. package/packages/kanboard/tui/kanboard-overlay.ts +0 -300
  29. package/packages/memory/tui/settings-tui.ts +0 -281
  30. package/packages/notify/platforms/focus-win.ts +0 -123
  31. package/packages/notify/platforms/focus.ts +0 -33
  32. package/packages/notify/platforms/gotify.ts +0 -36
  33. package/packages/notify/platforms/native.ts +0 -79
  34. package/packages/notify/platforms/node-notifier.d.ts +0 -20
  35. package/packages/notify/platforms/ntfy.ts +0 -48
  36. package/packages/notify/platforms/telegram.ts +0 -77
  37. package/packages/notify/tui/gotify-setup.ts +0 -528
  38. package/packages/notify/tui/ntfy-setup.ts +0 -652
  39. package/packages/notify/tui/recap-model-selector.ts +0 -301
  40. package/packages/notify/tui/settings-overlay.ts +0 -352
  41. package/packages/notify/tui/telegram-setup.ts +0 -301
@@ -1,652 +0,0 @@
1
- /**
2
- * @pi-unipi/notify — ntfy Setup TUI Component
3
- *
4
- * Interactive overlay for setting up ntfy push notifications.
5
- * Guides user through server URL, topic, optional token, and priority.
6
- * Tests connection before saving.
7
- */
8
-
9
- import type { Component } from "@earendil-works/pi-tui";
10
- import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
11
- import type { Theme } from "@earendil-works/pi-coding-agent";
12
- import { sendNtfyNotification } from "../platforms/ntfy.js";
13
- import { loadNtfyConfig, saveNtfyConfig, getNtfyConfigScope } from "../ntfy-config.js";
14
-
15
- type SetupPhase =
16
- | "instructions"
17
- | "scope"
18
- | "server-url"
19
- | "topic"
20
- | "token"
21
- | "priority"
22
- | "testing"
23
- | "success"
24
- | "error"
25
- | "test-failed";
26
-
27
- /**
28
- * ntfy setup overlay component.
29
- */
30
- export class NtfySetupOverlay implements Component {
31
- private phase: SetupPhase = "instructions";
32
- private scope: "global" | "project" = "global";
33
- private scopeIndex = 0; // 0 = global, 1 = project
34
- private serverUrl = "";
35
- private topic = "";
36
- private token = "";
37
- private priority = "3";
38
- private error: string | null = null;
39
- private testError: string | null = null;
40
- private isInPaste = false;
41
- private pasteBuffer = "";
42
- onClose?: () => void;
43
- requestRender?: () => void;
44
- private theme: Theme | null = null;
45
-
46
- constructor() {
47
- // Determine current scope and pre-fill from resolved config
48
- const cwd = process.cwd();
49
- const existingScope = getNtfyConfigScope(cwd);
50
- if (existingScope !== "none") {
51
- this.scope = existingScope;
52
- this.scopeIndex = existingScope === "project" ? 1 : 0;
53
- }
54
- const config = loadNtfyConfig(cwd);
55
- if (config.serverUrl) this.serverUrl = config.serverUrl;
56
- if (config.topic) this.topic = config.topic;
57
- if (config.token) this.token = config.token;
58
- if (config.priority) this.priority = String(config.priority);
59
- }
60
-
61
- setTheme(theme: Theme): void {
62
- this.theme = theme;
63
- }
64
-
65
- invalidate(): void {}
66
-
67
- handleInput(data: string): void {
68
- switch (this.phase) {
69
- case "instructions":
70
- if (data === "\r" || data === " ") {
71
- this.phase = "scope";
72
- } else if (data === "\x1b") {
73
- this.onClose?.();
74
- }
75
- break;
76
-
77
- case "scope":
78
- if (data === "\x1b[A" || data === "k") {
79
- // Up
80
- this.scopeIndex = Math.max(0, this.scopeIndex - 1);
81
- } else if (data === "\x1b[B" || data === "j") {
82
- // Down
83
- this.scopeIndex = Math.min(1, this.scopeIndex + 1);
84
- } else if (data === "\r" || data === " ") {
85
- this.scope = this.scopeIndex === 1 ? "project" : "global";
86
- this.phase = this.serverUrl ? "topic" : "server-url";
87
- } else if (data === "\x1b") {
88
- this.onClose?.();
89
- }
90
- break;
91
-
92
- case "server-url":
93
- this.handleTextInput(data, "server-url", () => {
94
- if (!this.serverUrl) {
95
- this.serverUrl = "https://ntfy.sh";
96
- }
97
- this.phase = "topic";
98
- });
99
- break;
100
-
101
- case "topic":
102
- this.handleTextInput(data, "topic", () => {
103
- this.phase = "token";
104
- });
105
- break;
106
-
107
- case "token":
108
- this.handleTextInput(data, "token", () => {
109
- this.phase = "priority";
110
- });
111
- break;
112
-
113
- case "priority":
114
- if (data === "\r" && this.isValidPriority()) {
115
- this.testConnection();
116
- } else if (data === "\x1b") {
117
- this.onClose?.();
118
- } else if (data === "\x7f" || data === "\b") {
119
- this.priority = this.priority.slice(0, -1);
120
- } else {
121
- const ch = data.replace(/[^\d]/g, "");
122
- if (ch && this.priority.length < 1) {
123
- this.priority += ch;
124
- }
125
- }
126
- break;
127
-
128
- case "testing":
129
- if (data === "\x1b") {
130
- this.onClose?.();
131
- }
132
- break;
133
-
134
- case "success":
135
- case "error":
136
- case "test-failed":
137
- if (data === "\r" || data === " " || data === "\x1b") {
138
- this.onClose?.();
139
- }
140
- break;
141
- }
142
- }
143
-
144
- private handleTextInput(
145
- data: string,
146
- target: "server-url" | "topic" | "token",
147
- onEnter: () => void
148
- ): void {
149
- // Handle bracketed paste mode
150
- if (this.isInPaste) {
151
- this.pasteBuffer += data;
152
- const endIndex = this.pasteBuffer.indexOf("\x1b[201~");
153
- if (endIndex !== -1) {
154
- const pasteContent = this.pasteBuffer.substring(0, endIndex).trim();
155
- if (target === "server-url") {
156
- this.serverUrl = pasteContent;
157
- } else if (target === "topic") {
158
- this.topic = pasteContent;
159
- } else {
160
- this.token = pasteContent;
161
- }
162
- this.isInPaste = false;
163
- this.pasteBuffer = "";
164
- }
165
- return;
166
- }
167
- // Detect start of bracketed paste
168
- if (data.includes("\x1b[200~")) {
169
- this.isInPaste = true;
170
- this.pasteBuffer = data.replace("\x1b[200~", "");
171
- return;
172
- }
173
- if (data === "\r") {
174
- onEnter();
175
- } else if (data === "\x1b") {
176
- // Escape during token phase — skip token (optional field)
177
- if (target === "token") {
178
- this.phase = "priority";
179
- } else {
180
- this.onClose?.();
181
- }
182
- } else if (data === "\x7f" || data === "\b") {
183
- if (target === "server-url") {
184
- this.serverUrl = this.serverUrl.slice(0, -1);
185
- } else if (target === "topic") {
186
- this.topic = this.topic.slice(0, -1);
187
- } else {
188
- this.token = this.token.slice(0, -1);
189
- }
190
- } else {
191
- // Ignore escape sequences
192
- if (data.startsWith("\x1b[")) return;
193
- if (target === "server-url") {
194
- this.serverUrl += data;
195
- } else if (target === "topic") {
196
- this.topic += data;
197
- } else {
198
- this.token += data;
199
- }
200
- }
201
- }
202
-
203
- private isValidPriority(): boolean {
204
- const num = parseInt(this.priority, 10);
205
- return !isNaN(num) && num >= 1 && num <= 5;
206
- }
207
-
208
- private async testConnection(): Promise<void> {
209
- this.phase = "testing";
210
- this.requestRender?.();
211
-
212
- try {
213
- await sendNtfyNotification(
214
- this.serverUrl.replace(/\/$/, ""),
215
- this.topic,
216
- "Pi — Setup Test",
217
- `ntfy configured successfully at ${new Date().toLocaleTimeString()}`,
218
- parseInt(this.priority, 10) || 3,
219
- this.token || undefined
220
- );
221
- this.saveConfig();
222
- this.phase = "success";
223
- this.requestRender?.();
224
- setTimeout(() => this.onClose?.(), 1500);
225
- } catch (err) {
226
- this.testError = err instanceof Error ? err.message : String(err);
227
- this.phase = "test-failed";
228
- this.requestRender?.();
229
- }
230
- }
231
-
232
- private saveConfig(): void {
233
- const cwd = process.cwd();
234
- saveNtfyConfig(this.scope, cwd, {
235
- enabled: true,
236
- serverUrl: this.serverUrl.replace(/\/$/, ""),
237
- topic: this.topic,
238
- token: this.token || undefined,
239
- priority: parseInt(this.priority, 10) || 3,
240
- });
241
- }
242
-
243
- // ─── Theme helpers ───────────────────────────────────────────────────
244
-
245
- private fg(color: string, text: string): string {
246
- if (this.theme) return this.theme.fg(color as any, text);
247
- const c: Record<string, string> = {
248
- accent: "\x1b[36m",
249
- success: "\x1b[32m",
250
- warning: "\x1b[33m",
251
- error: "\x1b[31m",
252
- dim: "\x1b[2m",
253
- borderMuted: "\x1b[90m",
254
- };
255
- return `${c[color] ?? ""}${text}\x1b[0m`;
256
- }
257
-
258
- private bold(text: string): string {
259
- return this.theme ? this.theme.bold(text) : `\x1b[1m${text}\x1b[0m`;
260
- }
261
-
262
- private frameLine(content: string, innerWidth: number): string {
263
- const truncated = truncateToWidth(content, innerWidth, "");
264
- const padding = Math.max(0, innerWidth - visibleWidth(truncated));
265
- return `${this.fg("borderMuted", "│")}${truncated}${" ".repeat(padding)}${this.fg("borderMuted", "│")}`;
266
- }
267
-
268
- private ruleLine(innerWidth: number): string {
269
- return this.fg("borderMuted", `├${"─".repeat(innerWidth)}┤`);
270
- }
271
-
272
- private borderLine(innerWidth: number, edge: "top" | "bottom"): string {
273
- const left = edge === "top" ? "┌" : "└";
274
- const right = edge === "top" ? "┐" : "┘";
275
- return this.fg("borderMuted", `${left}${"─".repeat(innerWidth)}${right}`);
276
- }
277
-
278
- private maskToken(token: string): string {
279
- if (token.length <= 8) return token;
280
- return token.slice(0, 4) + "•".repeat(token.length - 8) + token.slice(-4);
281
- }
282
-
283
- render(width: number): string[] {
284
- const innerWidth = Math.max(22, width - 2);
285
- const lines: string[] = [];
286
-
287
- lines.push(this.borderLine(innerWidth, "top"));
288
- lines.push(
289
- this.frameLine(
290
- this.fg("accent", this.bold("📢 ntfy Setup")),
291
- innerWidth
292
- )
293
- );
294
- lines.push(this.ruleLine(innerWidth));
295
-
296
- switch (this.phase) {
297
- case "instructions":
298
- lines.push(
299
- this.frameLine(
300
- this.fg("dim", "Set up ntfy push notifications:"),
301
- innerWidth
302
- )
303
- );
304
- lines.push(this.frameLine("", innerWidth));
305
- lines.push(
306
- this.frameLine(
307
- ` ${this.bold("1.")} ntfy is a simple HTTP-based notification service`,
308
- innerWidth
309
- )
310
- );
311
- lines.push(
312
- this.frameLine(
313
- ` ${this.fg("dim", "Public: https://ntfy.sh | Self-hosted: any ntfy server")}`,
314
- innerWidth
315
- )
316
- );
317
- lines.push(this.frameLine("", innerWidth));
318
- lines.push(
319
- this.frameLine(
320
- ` ${this.bold("2.")} Choose a topic (acts as a channel name)`,
321
- innerWidth
322
- )
323
- );
324
- lines.push(
325
- this.frameLine(
326
- ` ${this.fg("dim", "Subscribe to the topic in the ntfy app or web UI")}`,
327
- innerWidth
328
- )
329
- );
330
- lines.push(this.frameLine("", innerWidth));
331
- lines.push(
332
- this.frameLine(
333
- ` ${this.bold("3.")} Optionally set an access token for private servers`,
334
- innerWidth
335
- )
336
- );
337
- if (this.serverUrl) {
338
- lines.push(
339
- this.frameLine(
340
- ` ${this.fg("success", "✓")} Server URL pre-filled from existing config`,
341
- innerWidth
342
- )
343
- );
344
- }
345
- if (this.topic) {
346
- lines.push(
347
- this.frameLine(
348
- ` ${this.fg("success", "✓")} Topic pre-filled from existing config`,
349
- innerWidth
350
- )
351
- );
352
- }
353
- lines.push(this.ruleLine(innerWidth));
354
- lines.push(
355
- this.frameLine(
356
- this.fg("dim", "Press Enter to continue, Esc to cancel"),
357
- innerWidth
358
- )
359
- );
360
- break;
361
-
362
- case "scope": {
363
- lines.push(
364
- this.frameLine(
365
- this.fg("dim", "Where should this config be saved?"),
366
- innerWidth
367
- )
368
- );
369
- lines.push(this.frameLine("", innerWidth));
370
- const options = ["Global (all projects)", "Project (this project only)"];
371
- for (let i = 0; i < options.length; i++) {
372
- const isSelected = i === this.scopeIndex;
373
- const label = isSelected ? this.bold(options[i]) : this.fg("dim", options[i]);
374
- lines.push(
375
- this.frameLine(
376
- ` ${isSelected ? this.fg("accent", "▸") : " "} ${label}`,
377
- innerWidth
378
- )
379
- );
380
- }
381
- lines.push(this.frameLine("", innerWidth));
382
- lines.push(this.ruleLine(innerWidth));
383
- lines.push(
384
- this.frameLine(
385
- this.fg("dim", "↑↓ select · Enter confirm · Esc cancel"),
386
- innerWidth
387
- )
388
- );
389
- break;
390
- }
391
-
392
- case "server-url":
393
- lines.push(
394
- this.frameLine(
395
- this.fg("dim", "Enter ntfy server URL:"),
396
- innerWidth
397
- )
398
- );
399
- lines.push(this.frameLine("", innerWidth));
400
- lines.push(
401
- this.frameLine(
402
- ` ${this.fg("accent", this.bold(this.serverUrl || " "))}${this.fg("dim", "█")}`,
403
- innerWidth
404
- )
405
- );
406
- lines.push(this.frameLine("", innerWidth));
407
- lines.push(
408
- this.frameLine(
409
- this.fg("dim", "Default: https://ntfy.sh (public)"),
410
- innerWidth
411
- )
412
- );
413
- lines.push(
414
- this.frameLine(
415
- this.fg("dim", "Leave empty and press Enter for default"),
416
- innerWidth
417
- )
418
- );
419
- lines.push(this.ruleLine(innerWidth));
420
- lines.push(
421
- this.frameLine(
422
- this.fg("dim", "Enter to continue · Esc to cancel"),
423
- innerWidth
424
- )
425
- );
426
- break;
427
-
428
- case "topic":
429
- lines.push(
430
- this.frameLine(
431
- this.fg("dim", "Enter topic name:"),
432
- innerWidth
433
- )
434
- );
435
- lines.push(this.frameLine("", innerWidth));
436
- lines.push(
437
- this.frameLine(
438
- ` ${this.fg("accent", this.bold(this.topic || " "))}${this.fg("dim", "█")}`,
439
- innerWidth
440
- )
441
- );
442
- lines.push(this.frameLine("", innerWidth));
443
- lines.push(
444
- this.frameLine(
445
- this.fg("dim", "e.g. my-pi-notifications, project-alerts"),
446
- innerWidth
447
- )
448
- );
449
- lines.push(
450
- this.frameLine(
451
- this.fg("dim", "Pick something unique if using public ntfy.sh"),
452
- innerWidth
453
- )
454
- );
455
- lines.push(this.ruleLine(innerWidth));
456
- lines.push(
457
- this.frameLine(
458
- this.fg("dim", "Enter to continue · Esc to cancel"),
459
- innerWidth
460
- )
461
- );
462
- break;
463
-
464
- case "token":
465
- lines.push(
466
- this.frameLine(
467
- this.fg("dim", "Enter access token (optional):"),
468
- innerWidth
469
- )
470
- );
471
- lines.push(this.frameLine("", innerWidth));
472
- const displayToken = this.token
473
- ? this.fg("accent", this.bold(this.maskToken(this.token)))
474
- : " ";
475
- lines.push(
476
- this.frameLine(
477
- ` ${displayToken}${this.fg("dim", "█")}`,
478
- innerWidth
479
- )
480
- );
481
- lines.push(this.frameLine("", innerWidth));
482
- lines.push(
483
- this.frameLine(
484
- this.fg("dim", "Only needed for private/authenticated ntfy servers"),
485
- innerWidth
486
- )
487
- );
488
- lines.push(
489
- this.frameLine(
490
- this.fg("dim", "Not needed for public ntfy.sh"),
491
- innerWidth
492
- )
493
- );
494
- lines.push(this.ruleLine(innerWidth));
495
- lines.push(
496
- this.frameLine(
497
- this.fg("dim", "Enter to continue · Esc to skip"),
498
- innerWidth
499
- )
500
- );
501
- break;
502
-
503
- case "priority":
504
- lines.push(
505
- this.frameLine(
506
- this.fg("dim", "Set notification priority (1-5):"),
507
- innerWidth
508
- )
509
- );
510
- lines.push(this.frameLine("", innerWidth));
511
- lines.push(
512
- this.frameLine(
513
- ` ${this.fg("accent", this.bold(this.priority || " "))}${this.fg("dim", "█")}`,
514
- innerWidth
515
- )
516
- );
517
- lines.push(this.frameLine("", innerWidth));
518
- lines.push(
519
- this.frameLine(
520
- ` ${this.fg("dim", "1")} = min · ${this.fg("dim", "3")} = default · ${this.fg("dim", "5")} = max`,
521
- innerWidth
522
- )
523
- );
524
- lines.push(this.ruleLine(innerWidth));
525
- lines.push(
526
- this.frameLine(
527
- this.fg("dim", "Enter to test connection · Esc to cancel"),
528
- innerWidth
529
- )
530
- );
531
- break;
532
-
533
- case "testing":
534
- lines.push(this.frameLine("", innerWidth));
535
- lines.push(
536
- this.frameLine(
537
- ` ${this.fg("accent", "⠋")} ${this.bold("Testing connection...")}`,
538
- innerWidth
539
- )
540
- );
541
- lines.push(this.frameLine("", innerWidth));
542
- lines.push(
543
- this.frameLine(
544
- ` ${this.fg("dim", `Sending test to ${this.serverUrl}/${this.topic}`)}`,
545
- innerWidth
546
- )
547
- );
548
- lines.push(this.ruleLine(innerWidth));
549
- lines.push(
550
- this.frameLine(
551
- this.fg("dim", "Esc to cancel"),
552
- innerWidth
553
- )
554
- );
555
- break;
556
-
557
- case "success":
558
- lines.push(this.frameLine("", innerWidth));
559
- lines.push(
560
- this.frameLine(
561
- ` ${this.fg("success", "✓ ntfy configured successfully!")}`,
562
- innerWidth
563
- )
564
- );
565
- lines.push(this.frameLine("", innerWidth));
566
- lines.push(
567
- this.frameLine(
568
- ` ${this.fg("dim", `Server: ${this.serverUrl}`)}`,
569
- innerWidth
570
- )
571
- );
572
- lines.push(
573
- this.frameLine(
574
- ` ${this.fg("dim", `Topic: ${this.topic}`)}`,
575
- innerWidth
576
- )
577
- );
578
- lines.push(
579
- this.frameLine(
580
- ` ${this.fg("dim", `Priority: ${this.priority}`)}`,
581
- innerWidth
582
- )
583
- );
584
- lines.push(this.ruleLine(innerWidth));
585
- lines.push(
586
- this.frameLine(
587
- this.fg("dim", "Closing..."),
588
- innerWidth
589
- )
590
- );
591
- break;
592
-
593
- case "test-failed":
594
- lines.push(this.frameLine("", innerWidth));
595
- lines.push(
596
- this.frameLine(
597
- ` ${this.fg("error", "✗ Connection test failed")}`,
598
- innerWidth
599
- )
600
- );
601
- lines.push(this.frameLine("", innerWidth));
602
- lines.push(
603
- this.frameLine(
604
- ` ${this.fg("dim", this.testError || "Unknown error")}`,
605
- innerWidth
606
- )
607
- );
608
- lines.push(this.frameLine("", innerWidth));
609
- lines.push(
610
- this.frameLine(
611
- ` ${this.fg("dim", "Check your server URL and topic")}`,
612
- innerWidth
613
- )
614
- );
615
- lines.push(this.ruleLine(innerWidth));
616
- lines.push(
617
- this.frameLine(
618
- this.fg("dim", "Press Enter to close"),
619
- innerWidth
620
- )
621
- );
622
- break;
623
-
624
- case "error":
625
- lines.push(this.frameLine("", innerWidth));
626
- lines.push(
627
- this.frameLine(
628
- ` ${this.fg("error", "✗ Setup failed")}`,
629
- innerWidth
630
- )
631
- );
632
- lines.push(this.frameLine("", innerWidth));
633
- lines.push(
634
- this.frameLine(
635
- ` ${this.fg("dim", this.error || "Unknown error")}`,
636
- innerWidth
637
- )
638
- );
639
- lines.push(this.ruleLine(innerWidth));
640
- lines.push(
641
- this.frameLine(
642
- this.fg("dim", "Press Enter to close"),
643
- innerWidth
644
- )
645
- );
646
- break;
647
- }
648
-
649
- lines.push(this.borderLine(innerWidth, "bottom"));
650
- return lines;
651
- }
652
- }