@pi-unipi/notify 2.1.3 → 2.2.1

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/README.md CHANGED
@@ -28,6 +28,8 @@ Notify subscribes to Pi lifecycle events and routes notifications based on your
28
28
  | `agent_settled` | Off | Agent fully settles after retries, compaction, and queued continuations |
29
29
  | `memory_consolidated` | Off | Memory auto-saved |
30
30
  | `session_shutdown` | Off | Session ends |
31
+ | `ask_user_prompt` | Off | Agent asked a question and is waiting for an answer |
32
+ | `permission_request` | Off | A permission prompt is about to be shown (requires [`@gotgenes/pi-permission-system`](https://www.npmjs.com/package/@gotgenes/pi-permission-system)) |
31
33
 
32
34
  Notify registers with the info-screen dashboard, showing enabled platforms and last notification time. The footer subscribes to `NOTIFICATION_SENT` events to display notification stats.
33
35
 
package/events.ts CHANGED
@@ -14,6 +14,7 @@ import { sendGotifyNotification } from "./platforms/gotify.js";
14
14
  import { sendTelegramNotification } from "./platforms/telegram.js";
15
15
  import { sendNtfyNotification } from "./platforms/ntfy.js";
16
16
  import { buildAskUserPromptMessage } from "./ask-user-prompt-message.js";
17
+ import { buildPermissionPromptMessage } from "./permission-prompt-message.js";
17
18
  import { summarizeLastMessage } from "./summarize.js";
18
19
 
19
20
  // Event emitted by @juicesharp/rpiv-ask-user-question before showing its UI.
@@ -21,6 +22,13 @@ import { summarizeLastMessage } from "./summarize.js";
21
22
  // `./events` contract in npm.
22
23
  const ASK_USER_PROMPT_EVENT = "rpiv:ask-user:prompt" as const;
23
24
 
25
+ // Event emitted by @gotgenes/pi-permission-system immediately before the
26
+ // user-facing permission UI is invoked. Fires only for prompts a human must
27
+ // answer — policy auto-allow/deny and session approvals do not emit it.
28
+ // Kept as a local string (like the rpiv event above) because it belongs to a
29
+ // third-party package rather than the unipi event contract.
30
+ const PERMISSION_UI_PROMPT_EVENT = "permissions:ui_prompt" as const;
31
+
24
32
  /** Stored session context for modelRegistry access */
25
33
  let sessionCtx: ExtensionContext | null = null;
26
34
 
@@ -58,6 +66,7 @@ export const BUILTIN_EVENTS: Record<
58
66
  memory_consolidated: { hook: UNIPI_EVENTS.MEMORY_CONSOLIDATED, label: "Memory Saved" },
59
67
  session_shutdown: { hook: "session_shutdown", label: "Session End" },
60
68
  ask_user_prompt: { hook: UNIPI_EVENTS.ASK_USER_PROMPT, label: "Question Asked" },
69
+ permission_request: { hook: PERMISSION_UI_PROMPT_EVENT, label: "Permission Request" },
61
70
  };
62
71
 
63
72
  /**
@@ -298,6 +307,8 @@ function buildEventMessage(eventKey: string, payload: unknown): string {
298
307
  return "Session ending";
299
308
  case "ask_user_prompt":
300
309
  return buildAskUserPromptMessage(payload);
310
+ case "permission_request":
311
+ return buildPermissionPromptMessage(payload);
301
312
  default:
302
313
  return p.message ? String(p.message) : "Event occurred";
303
314
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/notify",
3
- "version": "2.1.3",
3
+ "version": "2.2.1",
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/**/*",
@@ -42,7 +34,7 @@
42
34
  "access": "public"
43
35
  },
44
36
  "dependencies": {
45
- "@pi-unipi/core": "*",
37
+ "@pi-unipi/core": "2.2.0",
46
38
  "node-notifier": "^10.0.1"
47
39
  },
48
40
  "peerDependencies": {
@@ -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
+ }
package/settings.ts CHANGED
@@ -28,6 +28,7 @@ export const DEFAULT_CONFIG: NotifyConfig = {
28
28
  memory_consolidated: { enabled: false, platforms: [] },
29
29
  session_shutdown: { enabled: false, platforms: [] },
30
30
  ask_user_prompt: { enabled: false, platforms: [] },
31
+ permission_request: { enabled: false, platforms: [] },
31
32
  },
32
33
  native: {
33
34
  enabled: true,
@@ -36,7 +36,9 @@ Help users configure the `@pi-unipi/notify` notification system.
36
36
  "agent_end": { "enabled": false, "platforms": [] },
37
37
  "agent_settled": { "enabled": false, "platforms": [] },
38
38
  "memory_consolidated": { "enabled": false, "platforms": [] },
39
- "session_shutdown": { "enabled": false, "platforms": [] }
39
+ "session_shutdown": { "enabled": false, "platforms": [] },
40
+ "ask_user_prompt": { "enabled": false, "platforms": [] },
41
+ "permission_request": { "enabled": false, "platforms": [] }
40
42
  },
41
43
  "native": {
42
44
  "enabled": true,
@@ -155,9 +157,34 @@ ntfy uses dedicated `ntfy.json` files at both global and project scope, with ful
155
157
  | `agent_settled` | Off | Agent fully settles after retries, compaction, and queued continuations |
156
158
  | `memory_consolidated` | Off | Memory auto-saved |
157
159
  | `session_shutdown` | Off | Session ends |
160
+ | `ask_user_prompt` | Off | Agent asked a question and is waiting for an answer |
161
+ | `permission_request` | Off | A permission prompt is about to be shown |
158
162
 
159
163
  Each event can override `platforms` — empty array means use `defaultPlatforms`.
160
164
 
165
+ ### `permission_request`
166
+
167
+ Fires on the `permissions:ui_prompt` broadcast from
168
+ [`@gotgenes/pi-permission-system`](https://www.npmjs.com/package/@gotgenes/pi-permission-system),
169
+ emitted immediately before the user-facing permission UI is invoked. Policy
170
+ auto-allow, policy deny, session approvals, and infrastructure auto-allowed
171
+ requests do **not** emit it, so this event does not produce notification spam.
172
+ Forwarded subagent prompts are handled too — the parent UI session emits the
173
+ event right before showing the forwarded dialog, and the notification is
174
+ suffixed with `(forwarded)`.
175
+
176
+ The notification is formatted defensively from the payload's `agentName`,
177
+ `surface`, `value` and `message` fields:
178
+
179
+ ```text
180
+ Pi — Permission Request
181
+ Current agent requested bash 'npm test'. Allow this command?
182
+ ```
183
+
184
+ Enable it when you run Pi in a background pane and don't want to miss a
185
+ permission prompt. If the permission system is not installed the event simply
186
+ never fires.
187
+
161
188
  ## Agent workflow
162
189
 
163
190
  ### Reading current config
@@ -11,6 +11,7 @@ import { 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";
14
+ import { boxInnerWidth } from "@pi-unipi/core";
14
15
 
15
16
  type SetupPhase =
16
17
  | "instructions"
@@ -235,7 +236,7 @@ export class GotifySetupOverlay implements Component {
235
236
  }
236
237
 
237
238
  render(width: number): string[] {
238
- const innerWidth = Math.max(22, width - 2);
239
+ const innerWidth = boxInnerWidth(width);
239
240
  const lines: string[] = [];
240
241
 
241
242
  lines.push(this.borderLine(innerWidth, "top"));
package/tui/ntfy-setup.ts CHANGED
@@ -11,6 +11,7 @@ import { 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";
14
+ import { boxInnerWidth } from "@pi-unipi/core";
14
15
 
15
16
  type SetupPhase =
16
17
  | "instructions"
@@ -281,7 +282,7 @@ export class NtfySetupOverlay implements Component {
281
282
  }
282
283
 
283
284
  render(width: number): string[] {
284
- const innerWidth = Math.max(22, width - 2);
285
+ const innerWidth = boxInnerWidth(width);
285
286
  const lines: string[] = [];
286
287
 
287
288
  lines.push(this.borderLine(innerWidth, "top"));
@@ -8,7 +8,7 @@
8
8
  import type { Component } from "@earendil-works/pi-tui";
9
9
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
10
10
  import type { Theme } from "@earendil-works/pi-coding-agent";
11
- import { readModelCache, type CachedModel } from "@pi-unipi/core";
11
+ import { readModelCache, type CachedModel, boxInnerWidth } from "@pi-unipi/core";
12
12
  import { loadConfig, saveConfig } from "../settings.js";
13
13
 
14
14
  const DEFAULT_MODEL = "openrouter/openai/gpt-oss-20b";
@@ -170,7 +170,7 @@ export class RecapModelSelectorOverlay implements Component {
170
170
  }
171
171
 
172
172
  render(width: number): string[] {
173
- const innerWidth = Math.max(40, width - 2);
173
+ const innerWidth = boxInnerWidth(width);
174
174
  const lines: string[] = [];
175
175
 
176
176
  lines.push(this.borderLine(innerWidth, "top"));
@@ -15,6 +15,7 @@ import {
15
15
  } from "../settings.js";
16
16
  import { loadNtfyConfig, saveNtfyConfig, getNtfyConfigScope } from "../ntfy-config.js";
17
17
  import type { NotifyConfig, NtfyConfig } from "../types.js";
18
+ import { boxInnerWidth } from "@pi-unipi/core";
18
19
 
19
20
  /** Section types */
20
21
  type Section = "platforms" | "events" | "recap";
@@ -174,7 +175,7 @@ export class NotifySettingsOverlay implements Component {
174
175
  }
175
176
 
176
177
  render(width: number): string[] {
177
- const innerWidth = Math.max(22, width - 2);
178
+ const innerWidth = boxInnerWidth(width);
178
179
  const lines: string[] = [];
179
180
 
180
181
  lines.push(this.borderLine(innerWidth, "top"));
@@ -10,6 +10,7 @@ import { 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";
13
+ import { boxInnerWidth } from "@pi-unipi/core";
13
14
 
14
15
  type SetupPhase = "instructions" | "token" | "polling" | "success" | "error" | "timeout";
15
16
 
@@ -224,7 +225,7 @@ export class TelegramSetupOverlay implements Component {
224
225
  }
225
226
 
226
227
  render(width: number): string[] {
227
- const innerWidth = Math.max(22, width - 2);
228
+ const innerWidth = boxInnerWidth(width);
228
229
  const lines: string[] = [];
229
230
 
230
231
  lines.push(this.borderLine(innerWidth, "top"));