@pi-unipi/notify 2.2.0 → 2.2.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/notify",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
4
4
  "description": "Cross-platform notification extension for Pi — native OS, Gotify, and Telegram notifications for agent lifecycle events",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -24,15 +24,7 @@
24
24
  "typecheck": "tsc --noEmit"
25
25
  },
26
26
  "files": [
27
- "index.ts",
28
- "tools.ts",
29
- "commands.ts",
30
- "settings.ts",
31
- "events.ts",
32
- "ask-user-prompt-message.ts",
33
- "ntfy-config.ts",
34
- "summarize.ts",
35
- "types.ts",
27
+ "*.ts",
36
28
  "platforms/*",
37
29
  "tui/*",
38
30
  "skills/**/*",
@@ -0,0 +1,95 @@
1
+ /**
2
+ * @pi-unipi/notify — Internal helper: build notification message from
3
+ * permission prompt event payloads.
4
+ *
5
+ * Handles the `permissions:ui_prompt` broadcast emitted by
6
+ * `@gotgenes/pi-permission-system` immediately before a human-facing
7
+ * permission prompt is shown.
8
+ *
9
+ * @internal — not part of the public API. Shared by the event listener and tests.
10
+ */
11
+
12
+ export interface PermissionPromptEventPayload {
13
+ /** Correlation id for the permission request */
14
+ requestId?: string;
15
+ /** Originating subsystem of the request */
16
+ source?: string;
17
+ /** Surface being requested — e.g. "bash", "mcp", "read", "edit" */
18
+ surface?: string | null;
19
+ /** Command / path / tool / skill being requested */
20
+ value?: string | null;
21
+ /** Current or requesting agent, when available */
22
+ agentName?: string | null;
23
+ /** Human-readable permission prompt text */
24
+ message?: string;
25
+ /** Present for forwarded subagent prompts */
26
+ forwarding?: unknown;
27
+ }
28
+
29
+ function isRecord(value: unknown): value is Record<string, unknown> {
30
+ return value !== null && typeof value === "object";
31
+ }
32
+
33
+ /** Return a trimmed non-empty string, or `undefined` for anything else. */
34
+ function cleanString(value: unknown): string | undefined {
35
+ if (typeof value !== "string") return undefined;
36
+ const trimmed = value.trim();
37
+ return trimmed.length > 0 ? trimmed : undefined;
38
+ }
39
+
40
+ /**
41
+ * `forwarding` is documented as "present for forwarded subagent prompts".
42
+ * Treat an explicitly falsy value (false / null / "" / 0) as not forwarded so
43
+ * that publishers which always include the key are handled correctly.
44
+ */
45
+ function isForwarded(value: unknown): boolean {
46
+ if (value === undefined || value === null) return false;
47
+ if (typeof value === "boolean") return value;
48
+ if (typeof value === "string") return value.trim().length > 0;
49
+ if (typeof value === "number") return value !== 0;
50
+ if (Array.isArray(value)) return value.length > 0;
51
+ if (isRecord(value)) return Object.keys(value).length > 0;
52
+ return true;
53
+ }
54
+
55
+ /**
56
+ * Build the "what was requested" clause from `agentName` / `surface` / `value`.
57
+ * Returns `undefined` when there is nothing meaningful to say.
58
+ */
59
+ function buildRequestClause(
60
+ agentName: string | undefined,
61
+ surface: string | undefined,
62
+ value: string | undefined,
63
+ ): string | undefined {
64
+ if (!surface && !value) return undefined;
65
+
66
+ const who = agentName ?? "Agent";
67
+
68
+ if (surface && value) return `${who} requested ${surface} '${value}'.`;
69
+ if (surface) return `${who} requested ${surface} access.`;
70
+ return `${who} requested '${value}'.`;
71
+ }
72
+
73
+ /** Build a human-readable notification message from a permission prompt payload. */
74
+ export function buildPermissionPromptMessage(payload: unknown): string {
75
+ const p = isRecord(payload) ? payload : {};
76
+
77
+ const agentName = cleanString(p.agentName);
78
+ const surface = cleanString(p.surface);
79
+ const value = cleanString(p.value);
80
+ const message = cleanString(p.message);
81
+
82
+ const parts: string[] = [];
83
+
84
+ const requestClause = buildRequestClause(agentName, surface, value);
85
+ if (requestClause) parts.push(requestClause);
86
+
87
+ // Only append the prompt text when it adds information beyond the clause.
88
+ if (message && message !== requestClause) parts.push(message);
89
+
90
+ if (parts.length === 0) parts.push("Pi is waiting for a permission decision.");
91
+
92
+ if (isForwarded(p.forwarding)) parts.push("(forwarded)");
93
+
94
+ return parts.join(" ");
95
+ }
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import type { Component } from "@earendil-works/pi-tui";
10
- import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
10
+ import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
11
11
  import type { Theme } from "@earendil-works/pi-coding-agent";
12
12
  import { sendGotifyNotification } from "../platforms/gotify.js";
13
13
  import { updateConfig, loadConfig } from "../settings.js";
@@ -59,7 +59,7 @@ export class GotifySetupOverlay implements Component {
59
59
  case "instructions":
60
60
  if (data === "\r" || data === " ") {
61
61
  this.phase = this.serverUrl ? "app-token" : "server-url";
62
- } else if (data === "\x1b") {
62
+ } else if (matchesKey(data, "escape")) {
63
63
  this.onClose?.();
64
64
  }
65
65
  break;
@@ -79,7 +79,7 @@ export class GotifySetupOverlay implements Component {
79
79
  case "priority":
80
80
  if (data === "\r" && this.isValidPriority()) {
81
81
  this.testConnection();
82
- } else if (data === "\x1b") {
82
+ } else if (matchesKey(data, "escape")) {
83
83
  this.onClose?.();
84
84
  } else if (data === "\x7f" || data === "\b") {
85
85
  this.priority = this.priority.slice(0, -1);
@@ -92,7 +92,7 @@ export class GotifySetupOverlay implements Component {
92
92
  break;
93
93
 
94
94
  case "testing":
95
- if (data === "\x1b") {
95
+ if (matchesKey(data, "escape")) {
96
96
  this.onClose?.();
97
97
  }
98
98
  break;
@@ -100,7 +100,7 @@ export class GotifySetupOverlay implements Component {
100
100
  case "success":
101
101
  case "error":
102
102
  case "test-failed":
103
- if (data === "\r" || data === " " || data === "\x1b") {
103
+ if (data === "\r" || data === " " || matchesKey(data, "escape")) {
104
104
  this.onClose?.();
105
105
  }
106
106
  break;
@@ -137,7 +137,7 @@ export class GotifySetupOverlay implements Component {
137
137
  }
138
138
  if (data === "\r") {
139
139
  onEnter();
140
- } else if (data === "\x1b") {
140
+ } else if (matchesKey(data, "escape")) {
141
141
  this.onClose?.();
142
142
  } else if (data === "\x7f" || data === "\b") {
143
143
  if (target === "server-url") {
package/tui/ntfy-setup.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import type { Component } from "@earendil-works/pi-tui";
10
- import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
10
+ import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
11
11
  import type { Theme } from "@earendil-works/pi-coding-agent";
12
12
  import { sendNtfyNotification } from "../platforms/ntfy.js";
13
13
  import { loadNtfyConfig, saveNtfyConfig, getNtfyConfigScope } from "../ntfy-config.js";
@@ -70,7 +70,7 @@ export class NtfySetupOverlay implements Component {
70
70
  case "instructions":
71
71
  if (data === "\r" || data === " ") {
72
72
  this.phase = "scope";
73
- } else if (data === "\x1b") {
73
+ } else if (matchesKey(data, "escape")) {
74
74
  this.onClose?.();
75
75
  }
76
76
  break;
@@ -85,7 +85,7 @@ export class NtfySetupOverlay implements Component {
85
85
  } else if (data === "\r" || data === " ") {
86
86
  this.scope = this.scopeIndex === 1 ? "project" : "global";
87
87
  this.phase = this.serverUrl ? "topic" : "server-url";
88
- } else if (data === "\x1b") {
88
+ } else if (matchesKey(data, "escape")) {
89
89
  this.onClose?.();
90
90
  }
91
91
  break;
@@ -114,7 +114,7 @@ export class NtfySetupOverlay implements Component {
114
114
  case "priority":
115
115
  if (data === "\r" && this.isValidPriority()) {
116
116
  this.testConnection();
117
- } else if (data === "\x1b") {
117
+ } else if (matchesKey(data, "escape")) {
118
118
  this.onClose?.();
119
119
  } else if (data === "\x7f" || data === "\b") {
120
120
  this.priority = this.priority.slice(0, -1);
@@ -127,7 +127,7 @@ export class NtfySetupOverlay implements Component {
127
127
  break;
128
128
 
129
129
  case "testing":
130
- if (data === "\x1b") {
130
+ if (matchesKey(data, "escape")) {
131
131
  this.onClose?.();
132
132
  }
133
133
  break;
@@ -135,7 +135,7 @@ export class NtfySetupOverlay implements Component {
135
135
  case "success":
136
136
  case "error":
137
137
  case "test-failed":
138
- if (data === "\r" || data === " " || data === "\x1b") {
138
+ if (data === "\r" || data === " " || matchesKey(data, "escape")) {
139
139
  this.onClose?.();
140
140
  }
141
141
  break;
@@ -173,7 +173,7 @@ export class NtfySetupOverlay implements Component {
173
173
  }
174
174
  if (data === "\r") {
175
175
  onEnter();
176
- } else if (data === "\x1b") {
176
+ } else if (matchesKey(data, "escape")) {
177
177
  // Escape during token phase — skip token (optional field)
178
178
  if (target === "token") {
179
179
  this.phase = "priority";
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { Component } from "@earendil-works/pi-tui";
9
- import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
9
+ import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
10
10
  import type { Theme } from "@earendil-works/pi-coding-agent";
11
11
  import { readModelCache, type CachedModel, boxInnerWidth } from "@pi-unipi/core";
12
12
  import { loadConfig, saveConfig } from "../settings.js";
@@ -56,7 +56,7 @@ export class RecapModelSelectorOverlay implements Component {
56
56
  this.filterMode = false;
57
57
  return;
58
58
  }
59
- if (data === "\x1b") {
59
+ if (matchesKey(data, "escape")) {
60
60
  // Escape — clear filter and exit filter mode
61
61
  this.filter = "";
62
62
  this.filterMode = false;
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { Component } from "@earendil-works/pi-tui";
9
- import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
9
+ import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
10
10
  import type { Theme } from "@earendil-works/pi-coding-agent";
11
11
  import { pollForChatId } from "../platforms/telegram.js";
12
12
  import { updateConfig } from "../settings.js";
@@ -48,7 +48,7 @@ export class TelegramSetupOverlay implements Component {
48
48
  case "instructions":
49
49
  if (data === "\r" || data === " ") {
50
50
  this.phase = "token";
51
- } else if (data === "\x1b") {
51
+ } else if (matchesKey(data, "escape")) {
52
52
  this.cleanup();
53
53
  this.onClose?.();
54
54
  }
@@ -75,7 +75,7 @@ export class TelegramSetupOverlay implements Component {
75
75
  }
76
76
  if (data === "\r" && this.botToken.length > 0) {
77
77
  this.startPolling();
78
- } else if (data === "\x1b") {
78
+ } else if (matchesKey(data, "escape")) {
79
79
  this.cleanup();
80
80
  this.onClose?.();
81
81
  } else if (data === "\x7f" || data === "\b") {
@@ -85,7 +85,7 @@ export class TelegramSetupOverlay implements Component {
85
85
  }
86
86
  break;
87
87
  case "polling":
88
- if (data === "\x1b") {
88
+ if (matchesKey(data, "escape")) {
89
89
  this.cleanup();
90
90
  this.onClose?.();
91
91
  }
@@ -93,7 +93,7 @@ export class TelegramSetupOverlay implements Component {
93
93
  case "success":
94
94
  case "error":
95
95
  case "timeout":
96
- if (data === "\r" || data === " " || data === "\x1b") {
96
+ if (data === "\r" || data === " " || matchesKey(data, "escape")) {
97
97
  this.cleanup();
98
98
  this.onClose?.();
99
99
  }