@pi-unipi/notify 2.0.4 → 2.0.7
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/ask-user-prompt-message.ts +75 -0
- package/events.ts +54 -6
- package/package.json +6 -1
|
@@ -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/events.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/notify",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.7",
|
|
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
|
"license": "MIT",
|
|
@@ -18,12 +18,17 @@
|
|
|
18
18
|
"notify",
|
|
19
19
|
"notifications"
|
|
20
20
|
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "tsc --noEmit && node --experimental-strip-types --test src/__tests__/*.test.ts",
|
|
23
|
+
"typecheck": "tsc --noEmit"
|
|
24
|
+
},
|
|
21
25
|
"files": [
|
|
22
26
|
"index.ts",
|
|
23
27
|
"tools.ts",
|
|
24
28
|
"commands.ts",
|
|
25
29
|
"settings.ts",
|
|
26
30
|
"events.ts",
|
|
31
|
+
"ask-user-prompt-message.ts",
|
|
27
32
|
"ntfy-config.ts",
|
|
28
33
|
"summarize.ts",
|
|
29
34
|
"types.ts",
|