@pi-unipi/notify 2.0.5 → 2.0.8

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.
@@ -0,0 +1,75 @@
1
+ /**
2
+ * @pi-unipi/notify — Internal helper: build notification message from
3
+ * ask-user prompt event payloads.
4
+ *
5
+ * Supports both UniPi's flat `unipi:ask-user:prompt` payload and the
6
+ * lossless `rpiv:ask-user:prompt` questionnaire projection.
7
+ *
8
+ * @internal — not part of the public API. Shared by the event listener and tests.
9
+ */
10
+
11
+ export interface AskUserPromptEventPayload {
12
+ questions: ReadonlyArray<AskUserPromptQuestion>;
13
+ }
14
+
15
+ export interface AskUserPromptQuestion {
16
+ question: string;
17
+ header: string;
18
+ multiSelect: boolean;
19
+ options: ReadonlyArray<AskUserPromptOption>;
20
+ }
21
+
22
+ export interface AskUserPromptOption {
23
+ label: string;
24
+ description: string;
25
+ hasPreview: boolean;
26
+ }
27
+
28
+ function isRecord(value: unknown): value is Record<string, unknown> {
29
+ return value !== null && typeof value === "object";
30
+ }
31
+
32
+ function nonEmptyString(value: unknown, fallback: string): string {
33
+ return typeof value === "string" && value.trim().length > 0 ? value : fallback;
34
+ }
35
+
36
+ function buildFlatPromptMessage(payload: Record<string, unknown>): string {
37
+ const question = nonEmptyString(payload.question, "A question");
38
+ const context = nonEmptyString(payload.context, "");
39
+ return context ? `Agent asks: ${question} — ${context}` : `Agent asks: ${question}`;
40
+ }
41
+
42
+ /** Build a human-readable notification message from an ask-user prompt payload. */
43
+ export function buildAskUserPromptMessage(payload: unknown): string {
44
+ const p = isRecord(payload) ? payload : {};
45
+
46
+ const questions = Array.isArray(p.questions)
47
+ ? p.questions.filter(isRecord)
48
+ : [];
49
+
50
+ if (questions.length === 0 && ("question" in p || "context" in p)) {
51
+ return buildFlatPromptMessage(p);
52
+ }
53
+
54
+ const firstQ = questions[0];
55
+
56
+ const baseQuestion = firstQ
57
+ ? nonEmptyString(firstQ.question, "A question")
58
+ : "A question";
59
+
60
+ const suffix = questions.length > 1 ? ` (+${questions.length - 1} more)` : "";
61
+
62
+ const optionLabels =
63
+ firstQ && Array.isArray(firstQ.options)
64
+ ? firstQ.options
65
+ .filter(isRecord)
66
+ .map((o) => nonEmptyString(o.label, ""))
67
+ .filter((label) => label.length > 0)
68
+ : [];
69
+
70
+ const options = optionLabels.join(", ");
71
+
72
+ return options
73
+ ? `Agent asks: ${baseQuestion}${suffix} — ${options}`
74
+ : `Agent asks: ${baseQuestion}${suffix}`;
75
+ }
package/commands.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * Registers slash commands for notification configuration and testing.
5
5
  */
6
6
 
7
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
7
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
8
8
  import { UNIPI_PREFIX } from "@pi-unipi/core";
9
9
  import { NOTIFY_COMMANDS } from "@pi-unipi/core";
10
10
  import { NotifySettingsOverlay } from "./tui/settings-overlay.js";
package/events.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * Supports built-in events and dynamic discovery via MODULE_READY.
6
6
  */
7
7
 
8
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
8
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
9
9
  import { UNIPI_EVENTS, emitEvent } from "@pi-unipi/core";
10
10
  import type { NotifyConfig, NotifyPlatform, NotifyDispatchResult } from "./types.js";
11
11
  import { loadNtfyConfig } from "./ntfy-config.js";
@@ -13,11 +13,28 @@ import { sendNativeNotification, SuppressedError } from "./platforms/native.js";
13
13
  import { sendGotifyNotification } from "./platforms/gotify.js";
14
14
  import { sendTelegramNotification } from "./platforms/telegram.js";
15
15
  import { sendNtfyNotification } from "./platforms/ntfy.js";
16
+ import { buildAskUserPromptMessage } from "./ask-user-prompt-message.js";
16
17
  import { summarizeLastMessage } from "./summarize.js";
17
18
 
19
+ // Event emitted by @juicesharp/rpiv-ask-user-question before showing its UI.
20
+ // Keep this as a local string until that package publishes an importable
21
+ // `./events` contract in npm.
22
+ const ASK_USER_PROMPT_EVENT = "rpiv:ask-user:prompt" as const;
23
+
18
24
  /** Stored session context for modelRegistry access */
19
25
  let sessionCtx: ExtensionContext | null = null;
20
26
 
27
+ /** Unsubscribe functions for pi.events.on() listeners. Cleared before each registration to avoid accumulation across reloads. */
28
+ const unsubs: Array<() => void> = [];
29
+
30
+ /** Unregister all previously registered pi.events.on() listeners. */
31
+ function unregisterAll(): void {
32
+ for (const unsub of unsubs) {
33
+ try { unsub(); } catch { /* ignore */ }
34
+ }
35
+ unsubs.length = 0;
36
+ }
37
+
21
38
  /** Store session context (called from index.ts on session_start) */
22
39
  export function setSessionContext(ctx: ExtensionContext): void {
23
40
  sessionCtx = ctx;
@@ -42,6 +59,12 @@ export const BUILTIN_EVENTS: Record<
42
59
  ask_user_prompt: { hook: UNIPI_EVENTS.ASK_USER_PROMPT, label: "Question Asked" },
43
60
  };
44
61
 
62
+ /**
63
+ * Pi lifecycle event types (dispatched by ExtensionRunner).
64
+ * These must use pi.on() — not pi.events.on() — to receive events.
65
+ */
66
+ const LIFECYCLE_EVENTS = new Set(["agent_end", "session_shutdown"]);
67
+
45
68
  /**
46
69
  * Register event listeners for all enabled notification events.
47
70
  * Attaches listeners to pi hooks and routes notifications to platforms.
@@ -51,6 +74,9 @@ export function registerEventListeners(
51
74
  config: NotifyConfig,
52
75
  cwd: string
53
76
  ): void {
77
+ // Remove all previously registered EventBus listeners to prevent accumulation
78
+ // across reloads (EventBus persists but module instances are replaced).
79
+ unregisterAll();
54
80
  // Register built-in events (except agent_end which has custom logic)
55
81
  for (const [eventKey, def] of Object.entries(BUILTIN_EVENTS)) {
56
82
  if (eventKey === "agent_end") continue; // handled separately below
@@ -69,7 +95,29 @@ export function registerEventListeners(
69
95
  );
70
96
  };
71
97
 
72
- (pi as any).on(def.hook, handler);
98
+ // pi lifecycle events (agent_end, session_shutdown) are dispatched via
99
+ // ExtensionRunner — must use pi.on(). These are stored in
100
+ // extension.handlers and automatically replaced on reload, so they
101
+ // do NOT accumulate like EventBus listeners.
102
+ if (LIFECYCLE_EVENTS.has(eventKey)) {
103
+ (pi as any).on(def.hook, handler);
104
+ } else {
105
+ unsubs.push(pi.events.on(def.hook, handler));
106
+ }
107
+ }
108
+
109
+ // Listen for rpiv:ask-user:prompt from @juicesharp/rpiv-ask-user-question
110
+ const askUserConfig = config.events["ask_user_prompt"];
111
+ if (askUserConfig?.enabled) {
112
+ unsubs.push(pi.events.on(ASK_USER_PROMPT_EVENT, (payload: unknown) => {
113
+ const title = `Pi — ${BUILTIN_EVENTS.ask_user_prompt.label}`;
114
+ const message = buildAskUserPromptMessage(payload);
115
+ dispatchNotification(pi, title, message, askUserConfig.platforms, "ask_user_prompt", config, cwd).catch(
116
+ () => {
117
+ // Silently ignore — background notification failure is non-blocking.
118
+ }
119
+ );
120
+ }));
73
121
  }
74
122
 
75
123
  // agent_end — custom handler with session name and recap support
@@ -130,7 +178,7 @@ export function registerEventListeners(
130
178
  // For now, modules register their own events through MODULE_READY
131
179
  }
132
180
  };
133
- (pi as any).on(UNIPI_EVENTS.MODULE_READY, moduleHandler);
181
+ unsubs.push(pi.events.on(UNIPI_EVENTS.MODULE_READY, moduleHandler));
134
182
  }
135
183
 
136
184
  /** Get all platforms that are currently enabled in config */
@@ -144,7 +192,9 @@ function getEnabledPlatforms(config: NotifyConfig, ntfyEnabled: boolean): Notify
144
192
  }
145
193
 
146
194
  /** No-op — cleanup handled by session teardown */
147
- export function unregisterEventListeners(): void {}
195
+ export function unregisterEventListeners(): void {
196
+ unregisterAll();
197
+ }
148
198
 
149
199
  /**
150
200
  * Dispatch a notification to the configured platforms.
@@ -291,9 +341,7 @@ function buildEventMessage(eventKey: string, payload: unknown): string {
291
341
  case "session_shutdown":
292
342
  return "Session ending";
293
343
  case "ask_user_prompt":
294
- return p.context
295
- ? `Agent asks: ${String(p.question || "")} — ${String(p.context)}`
296
- : `Agent asks: ${String(p.question || "A question")}`;
344
+ return buildAskUserPromptMessage(payload);
297
345
  default:
298
346
  return p.message ? String(p.message) : "Event occurred";
299
347
  }
package/index.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * Bridges agent lifecycle events to external platforms (native OS, Gotify, Telegram).
6
6
  */
7
7
 
8
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
8
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
9
9
  import {
10
10
  UNIPI_EVENTS,
11
11
  MODULES,
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@pi-unipi/notify",
3
- "version": "2.0.5",
3
+ "version": "2.0.8",
4
4
  "description": "Cross-platform notification extension for Pi — native OS, Gotify, and Telegram notifications for agent lifecycle events",
5
5
  "type": "module",
6
+ "main": "index.ts",
6
7
  "license": "MIT",
7
8
  "author": "Neuron Mr White",
8
9
  "repository": {
@@ -18,12 +19,17 @@
18
19
  "notify",
19
20
  "notifications"
20
21
  ],
22
+ "scripts": {
23
+ "test": "tsc --noEmit && node --experimental-strip-types --test src/__tests__/*.test.ts",
24
+ "typecheck": "tsc --noEmit"
25
+ },
21
26
  "files": [
22
27
  "index.ts",
23
28
  "tools.ts",
24
29
  "commands.ts",
25
30
  "settings.ts",
26
31
  "events.ts",
32
+ "ask-user-prompt-message.ts",
27
33
  "ntfy-config.ts",
28
34
  "summarize.ts",
29
35
  "types.ts",
@@ -48,8 +54,8 @@
48
54
  "node-notifier": "^10.0.1"
49
55
  },
50
56
  "peerDependencies": {
51
- "@mariozechner/pi-coding-agent": "*",
52
- "@mariozechner/pi-tui": "*",
53
- "@sinclair/typebox": "*"
57
+ "@earendil-works/pi-coding-agent": "^0.75.5",
58
+ "@earendil-works/pi-tui": "^0.75.5",
59
+ "typebox": "^1.1.38"
54
60
  }
55
61
  }
package/tools.ts CHANGED
@@ -4,8 +4,8 @@
4
4
  * Registers the `notify_user` tool for ad-hoc notifications.
5
5
  */
6
6
 
7
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
8
- import { Type } from "@sinclair/typebox";
7
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
8
+ import { Type } from "typebox";
9
9
  import { NOTIFY_TOOLS } from "@pi-unipi/core";
10
10
  import { loadConfig } from "./settings.js";
11
11
  import { dispatchNotification } from "./events.js";
@@ -6,9 +6,9 @@
6
6
  * Tests connection before saving.
7
7
  */
8
8
 
9
- import type { Component } from "@mariozechner/pi-tui";
10
- import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
11
- import type { Theme } from "@mariozechner/pi-coding-agent";
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
12
  import { sendGotifyNotification } from "../platforms/gotify.js";
13
13
  import { updateConfig, loadConfig } from "../settings.js";
14
14
 
package/tui/ntfy-setup.ts CHANGED
@@ -6,9 +6,9 @@
6
6
  * Tests connection before saving.
7
7
  */
8
8
 
9
- import type { Component } from "@mariozechner/pi-tui";
10
- import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
11
- import type { Theme } from "@mariozechner/pi-coding-agent";
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
12
  import { sendNtfyNotification } from "../platforms/ntfy.js";
13
13
  import { loadNtfyConfig, saveNtfyConfig, getNtfyConfigScope } from "../ntfy-config.js";
14
14
 
@@ -5,9 +5,9 @@
5
5
  * Uses the project-wide cached model list from ~/.unipi/config/models-cache.json.
6
6
  */
7
7
 
8
- import type { Component } from "@mariozechner/pi-tui";
9
- import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
10
- import type { Theme } from "@mariozechner/pi-coding-agent";
8
+ import type { Component } from "@earendil-works/pi-tui";
9
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
10
+ import type { Theme } from "@earendil-works/pi-coding-agent";
11
11
  import { readModelCache, type CachedModel } from "@pi-unipi/core";
12
12
  import { loadConfig, saveConfig } from "../settings.js";
13
13
 
@@ -5,9 +5,9 @@
5
5
  * Allows toggling platforms, configuring credentials, and per-event settings.
6
6
  */
7
7
 
8
- import type { Component } from "@mariozechner/pi-tui";
9
- import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
10
- import type { Theme } from "@mariozechner/pi-coding-agent";
8
+ import type { Component } from "@earendil-works/pi-tui";
9
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
10
+ import type { Theme } from "@earendil-works/pi-coding-agent";
11
11
  import {
12
12
  loadConfig,
13
13
  saveConfig,
@@ -5,9 +5,9 @@
5
5
  * Guides user through BotFather flow and auto-detects chat ID.
6
6
  */
7
7
 
8
- import type { Component } from "@mariozechner/pi-tui";
9
- import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
10
- import type { Theme } from "@mariozechner/pi-coding-agent";
8
+ import type { Component } from "@earendil-works/pi-tui";
9
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
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
13