@pi-unipi/unipi 2.1.3 → 2.2.0

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 (90) hide show
  1. package/README.md +6 -2
  2. package/package.json +23 -22
  3. package/packages/ask-user/ask-ui.ts +45 -31
  4. package/packages/ask-user/launcher-ui.ts +26 -22
  5. package/packages/ask-user/package.json +2 -2
  6. package/packages/ask-user/settings-tui.ts +22 -9
  7. package/packages/autocomplete/package.json +1 -1
  8. package/packages/autocomplete/src/constants.ts +5 -0
  9. package/packages/btw/package.json +2 -2
  10. package/packages/cocoindex/package.json +2 -2
  11. package/packages/compactor/package.json +3 -3
  12. package/packages/compactor/src/tui/settings-overlay.ts +2 -1
  13. package/packages/core/constants.ts +18 -0
  14. package/packages/core/index.ts +1 -0
  15. package/packages/core/package.json +1 -1
  16. package/packages/core/tui-width.ts +127 -0
  17. package/packages/footer/package.json +2 -2
  18. package/packages/footer/src/tui/settings-tui.ts +2 -1
  19. package/packages/image/README.md +82 -0
  20. package/packages/image/index.ts +1 -0
  21. package/packages/image/package.json +57 -0
  22. package/packages/image/skills/image/SKILL.md +73 -0
  23. package/packages/image/src/commands.ts +17 -0
  24. package/packages/image/src/generate.ts +201 -0
  25. package/packages/image/src/image-source.ts +204 -0
  26. package/packages/image/src/index.ts +99 -0
  27. package/packages/image/src/models.ts +290 -0
  28. package/packages/image/src/recognize.ts +223 -0
  29. package/packages/image/src/settings.ts +149 -0
  30. package/packages/image/src/tools.ts +296 -0
  31. package/packages/image/src/tui/model-selector.ts +279 -0
  32. package/packages/image/src/tui/settings-dialog.ts +236 -0
  33. package/packages/info-screen/package.json +2 -2
  34. package/packages/info-screen/tui/info-overlay.ts +505 -0
  35. package/packages/input-shortcuts/package.json +2 -2
  36. package/packages/kanboard/package.json +2 -2
  37. package/packages/kanboard/tui/kanboard-overlay.ts +300 -0
  38. package/packages/mcp/package.json +2 -2
  39. package/packages/mcp/src/tui/add-overlay.ts +10 -7
  40. package/packages/mcp/src/tui/settings-overlay.ts +10 -7
  41. package/packages/memory/package.json +3 -3
  42. package/packages/memory/tui/settings-tui.ts +281 -0
  43. package/packages/milestone/package.json +2 -2
  44. package/packages/notify/README.md +2 -0
  45. package/packages/notify/events.ts +11 -0
  46. package/packages/notify/package.json +2 -2
  47. package/packages/notify/permission-prompt-message.ts +95 -0
  48. package/packages/notify/platforms/focus-win.ts +123 -0
  49. package/packages/notify/platforms/focus.ts +33 -0
  50. package/packages/notify/platforms/gotify.ts +36 -0
  51. package/packages/notify/platforms/native.ts +79 -0
  52. package/packages/notify/platforms/node-notifier.d.ts +20 -0
  53. package/packages/notify/platforms/ntfy.ts +48 -0
  54. package/packages/notify/platforms/telegram.ts +77 -0
  55. package/packages/notify/settings.ts +1 -0
  56. package/packages/notify/skills/configure-notify/SKILL.md +28 -1
  57. package/packages/notify/src/__tests__/permission-prompt-message.test.ts +172 -0
  58. package/packages/notify/tui/gotify-setup.ts +529 -0
  59. package/packages/notify/tui/ntfy-setup.ts +653 -0
  60. package/packages/notify/tui/recap-model-selector.ts +301 -0
  61. package/packages/notify/tui/settings-overlay.ts +353 -0
  62. package/packages/notify/tui/telegram-setup.ts +302 -0
  63. package/packages/ralph/package.json +3 -3
  64. package/packages/subagents/package.json +4 -4
  65. package/packages/subagents/src/__tests__/shutdown-stale-ctx.test.ts +185 -0
  66. package/packages/subagents/src/index.ts +52 -10
  67. package/packages/unipi/index.ts +2 -0
  68. package/packages/updater/package.json +2 -2
  69. package/packages/updater/src/tui/changelog-overlay.ts +2 -2
  70. package/packages/updater/src/tui/readme-overlay.ts +2 -1
  71. package/packages/updater/src/tui/update-overlay.ts +2 -1
  72. package/packages/utility/package.json +2 -2
  73. package/packages/utility/src/tui/badge-settings-tui.ts +2 -2
  74. package/packages/utility/src/tui/util-settings-tui.ts +2 -2
  75. package/packages/web-api/README.md +28 -11
  76. package/packages/web-api/package.json +8 -2
  77. package/packages/web-api/skills/web/SKILL.md +36 -9
  78. package/packages/web-api/src/index.ts +15 -0
  79. package/packages/web-api/src/providers/duckduckgo.ts +71 -27
  80. package/packages/web-api/src/providers/firecrawl.ts +1 -1
  81. package/packages/web-api/src/providers/jina-reader.ts +1 -1
  82. package/packages/web-api/src/providers/jina-search.ts +1 -1
  83. package/packages/web-api/src/providers/perplexity.ts +2 -2
  84. package/packages/web-api/src/providers/serpapi.ts +1 -1
  85. package/packages/web-api/src/providers/tavily.ts +1 -1
  86. package/packages/web-api/src/providers/wigolo-client.ts +196 -0
  87. package/packages/web-api/src/providers/wigolo.ts +117 -0
  88. package/packages/web-api/src/settings.ts +1 -0
  89. package/packages/web-api/src/tools.ts +93 -36
  90. package/packages/workflow/package.json +2 -2
@@ -0,0 +1,529 @@
1
+ /**
2
+ * @pi-unipi/notify — Gotify Setup TUI Component
3
+ *
4
+ * Interactive overlay for setting up Gotify push notifications.
5
+ * Guides user through server URL, app token, and priority configuration.
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 { sendGotifyNotification } from "../platforms/gotify.js";
13
+ import { updateConfig, loadConfig } from "../settings.js";
14
+ import { boxInnerWidth } from "@pi-unipi/core";
15
+
16
+ type SetupPhase =
17
+ | "instructions"
18
+ | "server-url"
19
+ | "app-token"
20
+ | "priority"
21
+ | "testing"
22
+ | "success"
23
+ | "error"
24
+ | "test-failed";
25
+
26
+ /**
27
+ * Gotify setup overlay component.
28
+ */
29
+ export class GotifySetupOverlay implements Component {
30
+ private phase: SetupPhase = "instructions";
31
+ private serverUrl = "";
32
+ private appToken = "";
33
+ private priority = "5";
34
+ private error: string | null = null;
35
+ private testError: string | null = null;
36
+ private isInPaste = false;
37
+ private pasteBuffer = "";
38
+ private pasteTarget: "server-url" | "app-token" = "server-url";
39
+ onClose?: () => void;
40
+ requestRender?: () => void;
41
+ private theme: Theme | null = null;
42
+
43
+ constructor() {
44
+ // Pre-fill from existing config if available
45
+ const config = loadConfig();
46
+ if (config.gotify.serverUrl) this.serverUrl = config.gotify.serverUrl;
47
+ if (config.gotify.appToken) this.appToken = config.gotify.appToken;
48
+ if (config.gotify.priority) this.priority = String(config.gotify.priority);
49
+ }
50
+
51
+ setTheme(theme: Theme): void {
52
+ this.theme = theme;
53
+ }
54
+
55
+ invalidate(): void {}
56
+
57
+ handleInput(data: string): void {
58
+ switch (this.phase) {
59
+ case "instructions":
60
+ if (data === "\r" || data === " ") {
61
+ this.phase = this.serverUrl ? "app-token" : "server-url";
62
+ } else if (data === "\x1b") {
63
+ this.onClose?.();
64
+ }
65
+ break;
66
+
67
+ case "server-url":
68
+ this.handleTextInput(data, "server-url", () => {
69
+ this.phase = "app-token";
70
+ });
71
+ break;
72
+
73
+ case "app-token":
74
+ this.handleTextInput(data, "app-token", () => {
75
+ this.phase = "priority";
76
+ });
77
+ break;
78
+
79
+ case "priority":
80
+ if (data === "\r" && this.isValidPriority()) {
81
+ this.testConnection();
82
+ } else if (data === "\x1b") {
83
+ this.onClose?.();
84
+ } else if (data === "\x7f" || data === "\b") {
85
+ this.priority = this.priority.slice(0, -1);
86
+ } else {
87
+ const ch = data.replace(/[^\d]/g, "");
88
+ if (ch && this.priority.length < 2) {
89
+ this.priority += ch;
90
+ }
91
+ }
92
+ break;
93
+
94
+ case "testing":
95
+ if (data === "\x1b") {
96
+ this.onClose?.();
97
+ }
98
+ break;
99
+
100
+ case "success":
101
+ case "error":
102
+ case "test-failed":
103
+ if (data === "\r" || data === " " || data === "\x1b") {
104
+ this.onClose?.();
105
+ }
106
+ break;
107
+ }
108
+ }
109
+
110
+ private handleTextInput(
111
+ data: string,
112
+ target: "server-url" | "app-token",
113
+ onEnter: () => void
114
+ ): void {
115
+ // Handle bracketed paste mode
116
+ if (this.isInPaste) {
117
+ this.pasteBuffer += data;
118
+ const endIndex = this.pasteBuffer.indexOf("\x1b[201~");
119
+ if (endIndex !== -1) {
120
+ const pasteContent = this.pasteBuffer.substring(0, endIndex).trim();
121
+ if (target === "server-url") {
122
+ this.serverUrl = pasteContent;
123
+ } else {
124
+ this.appToken = pasteContent;
125
+ }
126
+ this.isInPaste = false;
127
+ this.pasteBuffer = "";
128
+ }
129
+ return;
130
+ }
131
+ // Detect start of bracketed paste
132
+ if (data.includes("\x1b[200~")) {
133
+ this.isInPaste = true;
134
+ this.pasteTarget = target;
135
+ this.pasteBuffer = data.replace("\x1b[200~", "");
136
+ return;
137
+ }
138
+ if (data === "\r") {
139
+ onEnter();
140
+ } else if (data === "\x1b") {
141
+ this.onClose?.();
142
+ } else if (data === "\x7f" || data === "\b") {
143
+ if (target === "server-url") {
144
+ this.serverUrl = this.serverUrl.slice(0, -1);
145
+ } else {
146
+ this.appToken = this.appToken.slice(0, -1);
147
+ }
148
+ } else {
149
+ // Ignore escape sequences
150
+ if (data.startsWith("\x1b[")) return;
151
+ if (target === "server-url") {
152
+ this.serverUrl += data;
153
+ } else {
154
+ this.appToken += data;
155
+ }
156
+ }
157
+ }
158
+
159
+ private isValidPriority(): boolean {
160
+ const num = parseInt(this.priority, 10);
161
+ return !isNaN(num) && num >= 1 && num <= 10;
162
+ }
163
+
164
+ private async testConnection(): Promise<void> {
165
+ this.phase = "testing";
166
+ this.requestRender?.();
167
+
168
+ try {
169
+ await sendGotifyNotification(
170
+ this.serverUrl.replace(/\/$/, ""),
171
+ this.appToken,
172
+ "Pi — Setup Test",
173
+ `Gotify configured successfully at ${new Date().toLocaleTimeString()}`,
174
+ parseInt(this.priority, 10) || 5
175
+ );
176
+ this.saveConfig();
177
+ this.phase = "success";
178
+ this.requestRender?.();
179
+ setTimeout(() => this.onClose?.(), 1500);
180
+ } catch (err) {
181
+ this.testError = err instanceof Error ? err.message : String(err);
182
+ this.phase = "test-failed";
183
+ this.requestRender?.();
184
+ }
185
+ }
186
+
187
+ private saveConfig(): void {
188
+ updateConfig({
189
+ gotify: {
190
+ enabled: true,
191
+ serverUrl: this.serverUrl.replace(/\/$/, ""),
192
+ appToken: this.appToken,
193
+ priority: parseInt(this.priority, 10) || 5,
194
+ },
195
+ });
196
+ }
197
+
198
+ // ─── Theme helpers ───────────────────────────────────────────────────
199
+
200
+ private fg(color: string, text: string): string {
201
+ if (this.theme) return this.theme.fg(color as any, text);
202
+ const c: Record<string, string> = {
203
+ accent: "\x1b[36m",
204
+ success: "\x1b[32m",
205
+ warning: "\x1b[33m",
206
+ error: "\x1b[31m",
207
+ dim: "\x1b[2m",
208
+ borderMuted: "\x1b[90m",
209
+ };
210
+ return `${c[color] ?? ""}${text}\x1b[0m`;
211
+ }
212
+
213
+ private bold(text: string): string {
214
+ return this.theme ? this.theme.bold(text) : `\x1b[1m${text}\x1b[0m`;
215
+ }
216
+
217
+ private frameLine(content: string, innerWidth: number): string {
218
+ const truncated = truncateToWidth(content, innerWidth, "");
219
+ const padding = Math.max(0, innerWidth - visibleWidth(truncated));
220
+ return `${this.fg("borderMuted", "│")}${truncated}${" ".repeat(padding)}${this.fg("borderMuted", "│")}`;
221
+ }
222
+
223
+ private ruleLine(innerWidth: number): string {
224
+ return this.fg("borderMuted", `├${"─".repeat(innerWidth)}┤`);
225
+ }
226
+
227
+ private borderLine(innerWidth: number, edge: "top" | "bottom"): string {
228
+ const left = edge === "top" ? "┌" : "└";
229
+ const right = edge === "top" ? "┐" : "┘";
230
+ return this.fg("borderMuted", `${left}${"─".repeat(innerWidth)}${right}`);
231
+ }
232
+
233
+ private maskToken(token: string): string {
234
+ if (token.length <= 8) return token;
235
+ return token.slice(0, 4) + "•".repeat(token.length - 8) + token.slice(-4);
236
+ }
237
+
238
+ render(width: number): string[] {
239
+ const innerWidth = boxInnerWidth(width);
240
+ const lines: string[] = [];
241
+
242
+ lines.push(this.borderLine(innerWidth, "top"));
243
+ lines.push(
244
+ this.frameLine(
245
+ this.fg("accent", this.bold("📡 Gotify Setup")),
246
+ innerWidth
247
+ )
248
+ );
249
+ lines.push(this.ruleLine(innerWidth));
250
+
251
+ switch (this.phase) {
252
+ case "instructions":
253
+ lines.push(
254
+ this.frameLine(
255
+ this.fg("dim", "Set up Gotify push notifications:"),
256
+ innerWidth
257
+ )
258
+ );
259
+ lines.push(this.frameLine("", innerWidth));
260
+ lines.push(
261
+ this.frameLine(
262
+ ` ${this.bold("1.")} Run a Gotify server (or use an existing one)`,
263
+ innerWidth
264
+ )
265
+ );
266
+ lines.push(
267
+ this.frameLine(
268
+ ` ${this.fg("dim", "See: https://gotify.net/docs/install")}`,
269
+ innerWidth
270
+ )
271
+ );
272
+ lines.push(this.frameLine("", innerWidth));
273
+ lines.push(
274
+ this.frameLine(
275
+ ` ${this.bold("2.")} Open your Gotify web UI`,
276
+ innerWidth
277
+ )
278
+ );
279
+ lines.push(
280
+ this.frameLine(
281
+ ` Go to ${this.fg("accent", "Apps")} → Create Application`,
282
+ innerWidth
283
+ )
284
+ );
285
+ lines.push(
286
+ this.frameLine(
287
+ ` Copy the ${this.fg("accent", "app token")}`,
288
+ innerWidth
289
+ )
290
+ );
291
+ lines.push(this.frameLine("", innerWidth));
292
+ lines.push(
293
+ this.frameLine(
294
+ ` ${this.bold("3.")} Enter your server URL and app token below`,
295
+ innerWidth
296
+ )
297
+ );
298
+ if (this.serverUrl) {
299
+ lines.push(
300
+ this.frameLine(
301
+ ` ${this.fg("success", "✓")} Server URL pre-filled from existing config`,
302
+ innerWidth
303
+ )
304
+ );
305
+ }
306
+ if (this.appToken) {
307
+ lines.push(
308
+ this.frameLine(
309
+ ` ${this.fg("success", "✓")} App token pre-filled from existing config`,
310
+ innerWidth
311
+ )
312
+ );
313
+ }
314
+ lines.push(this.ruleLine(innerWidth));
315
+ lines.push(
316
+ this.frameLine(
317
+ this.fg("dim", "Press Enter to continue, Esc to cancel"),
318
+ innerWidth
319
+ )
320
+ );
321
+ break;
322
+
323
+ case "server-url":
324
+ lines.push(
325
+ this.frameLine(
326
+ this.fg("dim", "Enter your Gotify server URL:"),
327
+ innerWidth
328
+ )
329
+ );
330
+ lines.push(this.frameLine("", innerWidth));
331
+ lines.push(
332
+ this.frameLine(
333
+ ` ${this.fg("accent", this.bold(this.serverUrl || " "))}${this.fg("dim", "█")}`,
334
+ innerWidth
335
+ )
336
+ );
337
+ lines.push(this.frameLine("", innerWidth));
338
+ lines.push(
339
+ this.frameLine(
340
+ this.fg("dim", "Example: https://gotify.example.com"),
341
+ innerWidth
342
+ )
343
+ );
344
+ lines.push(this.ruleLine(innerWidth));
345
+ lines.push(
346
+ this.frameLine(
347
+ this.fg("dim", "Enter to continue · Esc to cancel"),
348
+ innerWidth
349
+ )
350
+ );
351
+ break;
352
+
353
+ case "app-token":
354
+ lines.push(
355
+ this.frameLine(
356
+ this.fg("dim", "Enter your app token:"),
357
+ innerWidth
358
+ )
359
+ );
360
+ lines.push(this.frameLine("", innerWidth));
361
+ const displayToken = this.appToken
362
+ ? this.fg("accent", this.bold(this.maskToken(this.appToken)))
363
+ : " ";
364
+ lines.push(
365
+ this.frameLine(
366
+ ` ${displayToken}${this.fg("dim", "█")}`,
367
+ innerWidth
368
+ )
369
+ );
370
+ lines.push(this.frameLine("", innerWidth));
371
+ lines.push(
372
+ this.frameLine(
373
+ this.fg("dim", "Found in Gotify → Apps → your app"),
374
+ innerWidth
375
+ )
376
+ );
377
+ lines.push(this.ruleLine(innerWidth));
378
+ lines.push(
379
+ this.frameLine(
380
+ this.fg("dim", "Enter to continue · Esc to cancel"),
381
+ innerWidth
382
+ )
383
+ );
384
+ break;
385
+
386
+ case "priority":
387
+ lines.push(
388
+ this.frameLine(
389
+ this.fg("dim", "Set notification priority (1-10):"),
390
+ innerWidth
391
+ )
392
+ );
393
+ lines.push(this.frameLine("", innerWidth));
394
+ lines.push(
395
+ this.frameLine(
396
+ ` ${this.fg("accent", this.bold(this.priority || " "))}${this.fg("dim", "█")}`,
397
+ innerWidth
398
+ )
399
+ );
400
+ lines.push(this.frameLine("", innerWidth));
401
+ lines.push(
402
+ this.frameLine(
403
+ ` ${this.fg("dim", "1")} = low · ${this.fg("dim", "5")} = normal · ${this.fg("dim", "10")} = high`,
404
+ innerWidth
405
+ )
406
+ );
407
+ lines.push(this.ruleLine(innerWidth));
408
+ lines.push(
409
+ this.frameLine(
410
+ this.fg("dim", "Enter to test connection · Esc to cancel"),
411
+ innerWidth
412
+ )
413
+ );
414
+ break;
415
+
416
+ case "testing":
417
+ lines.push(this.frameLine("", innerWidth));
418
+ lines.push(
419
+ this.frameLine(
420
+ ` ${this.fg("accent", "⠋")} ${this.bold("Testing connection...")}`,
421
+ innerWidth
422
+ )
423
+ );
424
+ lines.push(this.frameLine("", innerWidth));
425
+ lines.push(
426
+ this.frameLine(
427
+ ` ${this.fg("dim", `Sending test to ${this.serverUrl}`)}`,
428
+ innerWidth
429
+ )
430
+ );
431
+ lines.push(this.ruleLine(innerWidth));
432
+ lines.push(
433
+ this.frameLine(
434
+ this.fg("dim", "Esc to cancel"),
435
+ innerWidth
436
+ )
437
+ );
438
+ break;
439
+
440
+ case "success":
441
+ lines.push(this.frameLine("", innerWidth));
442
+ lines.push(
443
+ this.frameLine(
444
+ ` ${this.fg("success", "✓ Gotify configured successfully!")}`,
445
+ innerWidth
446
+ )
447
+ );
448
+ lines.push(this.frameLine("", innerWidth));
449
+ lines.push(
450
+ this.frameLine(
451
+ ` ${this.fg("dim", `Server: ${this.serverUrl}`)}`,
452
+ innerWidth
453
+ )
454
+ );
455
+ lines.push(
456
+ this.frameLine(
457
+ ` ${this.fg("dim", `Priority: ${this.priority}`)}`,
458
+ innerWidth
459
+ )
460
+ );
461
+ lines.push(this.ruleLine(innerWidth));
462
+ lines.push(
463
+ this.frameLine(
464
+ this.fg("dim", "Closing..."),
465
+ innerWidth
466
+ )
467
+ );
468
+ break;
469
+
470
+ case "test-failed":
471
+ lines.push(this.frameLine("", innerWidth));
472
+ lines.push(
473
+ this.frameLine(
474
+ ` ${this.fg("error", "✗ Connection test failed")}`,
475
+ innerWidth
476
+ )
477
+ );
478
+ lines.push(this.frameLine("", innerWidth));
479
+ lines.push(
480
+ this.frameLine(
481
+ ` ${this.fg("dim", this.testError || "Unknown error")}`,
482
+ innerWidth
483
+ )
484
+ );
485
+ lines.push(this.frameLine("", innerWidth));
486
+ lines.push(
487
+ this.frameLine(
488
+ ` ${this.fg("dim", "Check your server URL and app token")}`,
489
+ innerWidth
490
+ )
491
+ );
492
+ lines.push(this.ruleLine(innerWidth));
493
+ lines.push(
494
+ this.frameLine(
495
+ this.fg("dim", "Press Enter to close"),
496
+ innerWidth
497
+ )
498
+ );
499
+ break;
500
+
501
+ case "error":
502
+ lines.push(this.frameLine("", innerWidth));
503
+ lines.push(
504
+ this.frameLine(
505
+ ` ${this.fg("error", "✗ Setup failed")}`,
506
+ innerWidth
507
+ )
508
+ );
509
+ lines.push(this.frameLine("", innerWidth));
510
+ lines.push(
511
+ this.frameLine(
512
+ ` ${this.fg("dim", this.error || "Unknown error")}`,
513
+ innerWidth
514
+ )
515
+ );
516
+ lines.push(this.ruleLine(innerWidth));
517
+ lines.push(
518
+ this.frameLine(
519
+ this.fg("dim", "Press Enter to close"),
520
+ innerWidth
521
+ )
522
+ );
523
+ break;
524
+ }
525
+
526
+ lines.push(this.borderLine(innerWidth, "bottom"));
527
+ return lines;
528
+ }
529
+ }