@pi-unipi/notify 0.1.5 → 0.1.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/README.md +24 -1
- package/commands.ts +130 -0
- package/events.ts +150 -3
- package/index.ts +6 -2
- package/package.json +2 -1
- package/platforms/ntfy.ts +48 -0
- package/settings.ts +26 -5
- package/skills/configure-notify/SKILL.md +25 -0
- package/summarize.ts +149 -0
- package/tools.ts +3 -3
- package/tui/ntfy-setup.ts +599 -0
- package/tui/recap-model-selector.ts +301 -0
- package/tui/settings-overlay.ts +68 -8
- package/types.ts +27 -1
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/notify — Recap Model Selector TUI
|
|
3
|
+
*
|
|
4
|
+
* Interactive overlay for selecting the recap summarization model.
|
|
5
|
+
* Uses the project-wide cached model list from ~/.unipi/config/models-cache.json.
|
|
6
|
+
*/
|
|
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";
|
|
11
|
+
import { readModelCache, type CachedModel } from "@pi-unipi/core";
|
|
12
|
+
import { loadConfig, saveConfig } from "../settings.js";
|
|
13
|
+
|
|
14
|
+
const DEFAULT_MODEL = "openrouter/openai/gpt-oss-20b";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Model selector overlay for recap model selection.
|
|
18
|
+
*/
|
|
19
|
+
export class RecapModelSelectorOverlay implements Component {
|
|
20
|
+
private models: CachedModel[] = [];
|
|
21
|
+
private filteredModels: CachedModel[] = [];
|
|
22
|
+
private selectedIndex = 0;
|
|
23
|
+
private filter = "";
|
|
24
|
+
private filterMode = false;
|
|
25
|
+
private saved = false;
|
|
26
|
+
private error: string | null = null;
|
|
27
|
+
onClose?: () => void;
|
|
28
|
+
requestRender?: () => void;
|
|
29
|
+
private theme: Theme | null = null;
|
|
30
|
+
|
|
31
|
+
constructor() {
|
|
32
|
+
// Load all cached models from project-wide cache
|
|
33
|
+
this.models = readModelCache();
|
|
34
|
+
this.applyFilter();
|
|
35
|
+
|
|
36
|
+
// Pre-select current config model
|
|
37
|
+
const config = loadConfig();
|
|
38
|
+
const currentModel = config.recap.model;
|
|
39
|
+
const idx = this.filteredModels.findIndex(
|
|
40
|
+
(m) => `${m.provider}/${m.id}` === currentModel
|
|
41
|
+
);
|
|
42
|
+
if (idx >= 0) this.selectedIndex = idx;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
setTheme(theme: Theme): void {
|
|
46
|
+
this.theme = theme;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
invalidate(): void {}
|
|
50
|
+
|
|
51
|
+
handleInput(data: string): void {
|
|
52
|
+
// Filter mode: type to search
|
|
53
|
+
if (this.filterMode) {
|
|
54
|
+
if (data === "\r") {
|
|
55
|
+
// Enter — exit filter mode
|
|
56
|
+
this.filterMode = false;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (data === "\x1b") {
|
|
60
|
+
// Escape — clear filter and exit filter mode
|
|
61
|
+
this.filter = "";
|
|
62
|
+
this.filterMode = false;
|
|
63
|
+
this.applyFilter();
|
|
64
|
+
this.selectedIndex = 0;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (data === "\x7f" || data === "\b") {
|
|
68
|
+
// Backspace
|
|
69
|
+
this.filter = this.filter.slice(0, -1);
|
|
70
|
+
this.applyFilter();
|
|
71
|
+
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredModels.length - 1));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (data.length === 1 && data >= " ") {
|
|
75
|
+
this.filter += data;
|
|
76
|
+
this.applyFilter();
|
|
77
|
+
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredModels.length - 1));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
switch (data) {
|
|
84
|
+
case "\x1b[A": // Up
|
|
85
|
+
case "k":
|
|
86
|
+
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
87
|
+
break;
|
|
88
|
+
case "\x1b[B": // Down
|
|
89
|
+
case "j":
|
|
90
|
+
this.selectedIndex = Math.min(
|
|
91
|
+
this.filteredModels.length - 1,
|
|
92
|
+
this.selectedIndex + 1
|
|
93
|
+
);
|
|
94
|
+
break;
|
|
95
|
+
case "/": // Start filter
|
|
96
|
+
this.filterMode = true;
|
|
97
|
+
this.filter = "";
|
|
98
|
+
break;
|
|
99
|
+
case "\r": // Enter — select and save
|
|
100
|
+
this.selectModel();
|
|
101
|
+
break;
|
|
102
|
+
case "\x1b": // Escape — close
|
|
103
|
+
this.onClose?.();
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private applyFilter(): void {
|
|
109
|
+
const q = this.filter.toLowerCase();
|
|
110
|
+
if (!q) {
|
|
111
|
+
this.filteredModels = [...this.models];
|
|
112
|
+
} else {
|
|
113
|
+
this.filteredModels = this.models.filter(
|
|
114
|
+
(m) =>
|
|
115
|
+
m.id.toLowerCase().includes(q) ||
|
|
116
|
+
(m.name?.toLowerCase().includes(q) ?? false)
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private selectModel(): void {
|
|
122
|
+
const model = this.filteredModels[this.selectedIndex];
|
|
123
|
+
if (!model) {
|
|
124
|
+
this.error = "No model selected";
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const modelRef = `${model.provider}/${model.id}`;
|
|
129
|
+
const config = loadConfig();
|
|
130
|
+
config.recap.model = modelRef;
|
|
131
|
+
saveConfig(config);
|
|
132
|
+
this.saved = true;
|
|
133
|
+
this.error = null;
|
|
134
|
+
setTimeout(() => this.onClose?.(), 500);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ─── Theme helpers ───────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
private fg(color: string, text: string): string {
|
|
140
|
+
if (this.theme) return this.theme.fg(color as any, text);
|
|
141
|
+
const c: Record<string, string> = {
|
|
142
|
+
accent: "\x1b[36m",
|
|
143
|
+
success: "\x1b[32m",
|
|
144
|
+
warning: "\x1b[33m",
|
|
145
|
+
error: "\x1b[31m",
|
|
146
|
+
dim: "\x1b[2m",
|
|
147
|
+
borderMuted: "\x1b[90m",
|
|
148
|
+
};
|
|
149
|
+
return `${c[color] ?? ""}${text}\x1b[0m`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private bold(text: string): string {
|
|
153
|
+
return this.theme ? this.theme.bold(text) : `\x1b[1m${text}\x1b[0m`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private frameLine(content: string, innerWidth: number): string {
|
|
157
|
+
const truncated = truncateToWidth(content, innerWidth, "");
|
|
158
|
+
const padding = Math.max(0, innerWidth - visibleWidth(truncated));
|
|
159
|
+
return `${this.fg("borderMuted", "│")}${truncated}${" ".repeat(padding)}${this.fg("borderMuted", "│")}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private ruleLine(innerWidth: number): string {
|
|
163
|
+
return this.fg("borderMuted", `├${"─".repeat(innerWidth)}┤`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private borderLine(innerWidth: number, edge: "top" | "bottom"): string {
|
|
167
|
+
const left = edge === "top" ? "┌" : "└";
|
|
168
|
+
const right = edge === "top" ? "┐" : "┘";
|
|
169
|
+
return this.fg("borderMuted", `${left}${"─".repeat(innerWidth)}${right}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
render(width: number): string[] {
|
|
173
|
+
const innerWidth = Math.max(40, width - 2);
|
|
174
|
+
const lines: string[] = [];
|
|
175
|
+
|
|
176
|
+
lines.push(this.borderLine(innerWidth, "top"));
|
|
177
|
+
lines.push(
|
|
178
|
+
this.frameLine(
|
|
179
|
+
this.fg("accent", this.bold("🤖 Recap Model Selector")),
|
|
180
|
+
innerWidth
|
|
181
|
+
)
|
|
182
|
+
);
|
|
183
|
+
lines.push(
|
|
184
|
+
this.frameLine(
|
|
185
|
+
this.fg("dim", "Select model for notification recaps"),
|
|
186
|
+
innerWidth
|
|
187
|
+
)
|
|
188
|
+
);
|
|
189
|
+
lines.push(this.ruleLine(innerWidth));
|
|
190
|
+
|
|
191
|
+
// Filter bar
|
|
192
|
+
if (this.filterMode) {
|
|
193
|
+
lines.push(
|
|
194
|
+
this.frameLine(
|
|
195
|
+
` ${this.fg("accent", "Filter:")} ${this.filter}${this.fg("accent", "█")}`,
|
|
196
|
+
innerWidth
|
|
197
|
+
)
|
|
198
|
+
);
|
|
199
|
+
} else if (this.filter) {
|
|
200
|
+
lines.push(
|
|
201
|
+
this.frameLine(
|
|
202
|
+
` ${this.fg("dim", "Filter:")} ${this.filter} ${this.fg("dim", "(press / to edit)")}`,
|
|
203
|
+
innerWidth
|
|
204
|
+
)
|
|
205
|
+
);
|
|
206
|
+
} else {
|
|
207
|
+
lines.push(
|
|
208
|
+
this.frameLine(
|
|
209
|
+
` ${this.fg("dim", `/${this.models.length} models · press / to filter`)}`,
|
|
210
|
+
innerWidth
|
|
211
|
+
)
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
lines.push(this.ruleLine(innerWidth));
|
|
215
|
+
|
|
216
|
+
// Model list
|
|
217
|
+
const terminalRows = process.stdout.rows ?? 30;
|
|
218
|
+
const maxVisible = Math.max(5, terminalRows - 14);
|
|
219
|
+
const startIdx = Math.max(
|
|
220
|
+
0,
|
|
221
|
+
this.selectedIndex - Math.floor(maxVisible / 2)
|
|
222
|
+
);
|
|
223
|
+
const endIdx = Math.min(
|
|
224
|
+
this.filteredModels.length,
|
|
225
|
+
startIdx + maxVisible
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
if (this.filteredModels.length === 0) {
|
|
229
|
+
lines.push(
|
|
230
|
+
this.frameLine(
|
|
231
|
+
` ${this.fg("dim", "No models found")}`,
|
|
232
|
+
innerWidth
|
|
233
|
+
)
|
|
234
|
+
);
|
|
235
|
+
} else {
|
|
236
|
+
for (let i = startIdx; i < endIdx; i++) {
|
|
237
|
+
const m = this.filteredModels[i];
|
|
238
|
+
const isSelected = i === this.selectedIndex;
|
|
239
|
+
const marker = isSelected ? this.fg("accent", "▸") : " ";
|
|
240
|
+
const label = m.name || m.id;
|
|
241
|
+
const fullRef = `${m.provider}/${m.id}`;
|
|
242
|
+
const isDefault = fullRef === DEFAULT_MODEL;
|
|
243
|
+
const defaultTag = isDefault
|
|
244
|
+
? ` ${this.fg("warning", "(default)")}`
|
|
245
|
+
: "";
|
|
246
|
+
|
|
247
|
+
const providerTag = this.fg("dim", `[${m.provider}]`);
|
|
248
|
+
const display = isSelected
|
|
249
|
+
? `${providerTag} ${this.bold(label)}${defaultTag}`
|
|
250
|
+
: `${providerTag} ${this.fg("dim", label)}${defaultTag}`;
|
|
251
|
+
|
|
252
|
+
lines.push(this.frameLine(` ${marker} ${display}`, innerWidth));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Scroll indicator
|
|
257
|
+
if (this.filteredModels.length > maxVisible) {
|
|
258
|
+
const pct = Math.round(
|
|
259
|
+
((this.selectedIndex + 1) / this.filteredModels.length) * 100
|
|
260
|
+
);
|
|
261
|
+
lines.push(
|
|
262
|
+
this.frameLine(
|
|
263
|
+
this.fg("dim", ` ${pct}% (${this.selectedIndex + 1}/${this.filteredModels.length})`),
|
|
264
|
+
innerWidth
|
|
265
|
+
)
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Status messages
|
|
270
|
+
if (this.error) {
|
|
271
|
+
lines.push(this.ruleLine(innerWidth));
|
|
272
|
+
lines.push(
|
|
273
|
+
this.frameLine(` ${this.fg("error", `⚠ ${this.error}`)}`, innerWidth)
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
if (this.saved) {
|
|
277
|
+
lines.push(this.ruleLine(innerWidth));
|
|
278
|
+
lines.push(
|
|
279
|
+
this.frameLine(
|
|
280
|
+
` ${this.fg("success", "✓ Model saved")}`,
|
|
281
|
+
innerWidth
|
|
282
|
+
)
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Footer
|
|
287
|
+
lines.push(this.ruleLine(innerWidth));
|
|
288
|
+
lines.push(
|
|
289
|
+
this.frameLine(
|
|
290
|
+
this.fg(
|
|
291
|
+
"dim",
|
|
292
|
+
"↑↓ navigate · / filter · Enter select · Esc cancel"
|
|
293
|
+
),
|
|
294
|
+
innerWidth
|
|
295
|
+
)
|
|
296
|
+
);
|
|
297
|
+
lines.push(this.borderLine(innerWidth, "bottom"));
|
|
298
|
+
|
|
299
|
+
return lines;
|
|
300
|
+
}
|
|
301
|
+
}
|
package/tui/settings-overlay.ts
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
import type { NotifyConfig } from "../types.js";
|
|
17
17
|
|
|
18
18
|
/** Section types */
|
|
19
|
-
type Section = "platforms" | "events";
|
|
19
|
+
type Section = "platforms" | "events" | "recap";
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Settings overlay component.
|
|
@@ -29,6 +29,8 @@ export class NotifySettingsOverlay implements Component {
|
|
|
29
29
|
private saved = false;
|
|
30
30
|
onClose?: () => void;
|
|
31
31
|
requestRender?: () => void;
|
|
32
|
+
/** Called when user presses M in recap section to open model selector */
|
|
33
|
+
onOpenModelSelector?: () => void;
|
|
32
34
|
private theme: Theme | null = null;
|
|
33
35
|
|
|
34
36
|
constructor() {
|
|
@@ -55,8 +57,17 @@ export class NotifySettingsOverlay implements Component {
|
|
|
55
57
|
this.toggleCurrent();
|
|
56
58
|
break;
|
|
57
59
|
case "\t": // Tab - switch section
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
{
|
|
61
|
+
const sections: Section[] = ["platforms", "events", "recap"];
|
|
62
|
+
const idx = sections.indexOf(this.section);
|
|
63
|
+
this.section = sections[(idx + 1) % sections.length];
|
|
64
|
+
this.selectedIndex = 0;
|
|
65
|
+
}
|
|
66
|
+
break;
|
|
67
|
+
case "m": // M - open model selector (only in recap section)
|
|
68
|
+
if (this.section === "recap") {
|
|
69
|
+
this.onOpenModelSelector?.();
|
|
70
|
+
}
|
|
60
71
|
break;
|
|
61
72
|
case "\r": // Enter - save
|
|
62
73
|
this.save();
|
|
@@ -68,21 +79,25 @@ export class NotifySettingsOverlay implements Component {
|
|
|
68
79
|
}
|
|
69
80
|
|
|
70
81
|
private get maxItems(): number {
|
|
71
|
-
if (this.section === "platforms") return
|
|
82
|
+
if (this.section === "platforms") return 4; // native, gotify, telegram, ntfy
|
|
83
|
+
if (this.section === "recap") return 1; // toggle
|
|
72
84
|
return Object.keys(this.config.events).length;
|
|
73
85
|
}
|
|
74
86
|
|
|
75
87
|
private toggleCurrent(): void {
|
|
76
88
|
if (this.section === "platforms") {
|
|
77
|
-
const platforms: Array<"native" | "gotify" | "telegram"> = [
|
|
89
|
+
const platforms: Array<"native" | "gotify" | "telegram" | "ntfy"> = [
|
|
78
90
|
"native",
|
|
79
91
|
"gotify",
|
|
80
92
|
"telegram",
|
|
93
|
+
"ntfy",
|
|
81
94
|
];
|
|
82
95
|
const key = platforms[this.selectedIndex];
|
|
83
96
|
if (key) {
|
|
84
97
|
this.config[key].enabled = !this.config[key].enabled;
|
|
85
98
|
}
|
|
99
|
+
} else if (this.section === "recap") {
|
|
100
|
+
this.config.recap.enabled = !this.config.recap.enabled;
|
|
86
101
|
} else {
|
|
87
102
|
const eventKeys = Object.keys(this.config.events);
|
|
88
103
|
const key = eventKeys[this.selectedIndex];
|
|
@@ -158,11 +173,17 @@ export class NotifySettingsOverlay implements Component {
|
|
|
158
173
|
this.section === "events"
|
|
159
174
|
? this.fg("accent", this.bold("[Events]"))
|
|
160
175
|
: this.fg("dim", "Events");
|
|
161
|
-
|
|
176
|
+
const recapTab =
|
|
177
|
+
this.section === "recap"
|
|
178
|
+
? this.fg("accent", this.bold("[Recap]"))
|
|
179
|
+
: this.fg("dim", "Recap");
|
|
180
|
+
lines.push(this.frameLine(` ${platformTab} ${eventsTab} ${recapTab}`, innerWidth));
|
|
162
181
|
lines.push(this.ruleLine(innerWidth));
|
|
163
182
|
|
|
164
183
|
if (this.section === "platforms") {
|
|
165
184
|
this.renderPlatforms(lines, innerWidth);
|
|
185
|
+
} else if (this.section === "recap") {
|
|
186
|
+
this.renderRecap(lines, innerWidth);
|
|
166
187
|
} else {
|
|
167
188
|
this.renderEvents(lines, innerWidth);
|
|
168
189
|
}
|
|
@@ -179,7 +200,10 @@ export class NotifySettingsOverlay implements Component {
|
|
|
179
200
|
|
|
180
201
|
// Footer
|
|
181
202
|
lines.push(this.ruleLine(innerWidth));
|
|
182
|
-
|
|
203
|
+
const footerHint = this.section === "recap"
|
|
204
|
+
? "↑↓ navigate · Space toggle · M change model · Tab switch · Enter save · Esc cancel"
|
|
205
|
+
: "↑↓ navigate · Space toggle · Tab switch · Enter save · Esc cancel";
|
|
206
|
+
lines.push(this.frameLine(this.fg("dim", footerHint), innerWidth));
|
|
183
207
|
lines.push(this.borderLine(innerWidth, "bottom"));
|
|
184
208
|
|
|
185
209
|
return lines;
|
|
@@ -187,7 +211,7 @@ export class NotifySettingsOverlay implements Component {
|
|
|
187
211
|
|
|
188
212
|
private renderPlatforms(lines: string[], innerWidth: number): void {
|
|
189
213
|
const platforms: Array<{
|
|
190
|
-
key: "native" | "gotify" | "telegram";
|
|
214
|
+
key: "native" | "gotify" | "telegram" | "ntfy";
|
|
191
215
|
label: string;
|
|
192
216
|
detail: string;
|
|
193
217
|
}> = [
|
|
@@ -210,6 +234,13 @@ export class NotifySettingsOverlay implements Component {
|
|
|
210
234
|
? "Bot configured"
|
|
211
235
|
: "Bot API notifications",
|
|
212
236
|
},
|
|
237
|
+
{
|
|
238
|
+
key: "ntfy",
|
|
239
|
+
label: "ntfy",
|
|
240
|
+
detail: this.config.ntfy.serverUrl
|
|
241
|
+
? `Server: ${this.config.ntfy.serverUrl}`
|
|
242
|
+
: "Self-hosted push service",
|
|
243
|
+
},
|
|
213
244
|
];
|
|
214
245
|
|
|
215
246
|
for (let i = 0; i < platforms.length; i++) {
|
|
@@ -248,4 +279,33 @@ export class NotifySettingsOverlay implements Component {
|
|
|
248
279
|
);
|
|
249
280
|
}
|
|
250
281
|
}
|
|
282
|
+
|
|
283
|
+
private renderRecap(lines: string[], innerWidth: number): void {
|
|
284
|
+
// Toggle
|
|
285
|
+
const isSelected = this.selectedIndex === 0;
|
|
286
|
+
const toggleOn = this.fg("success", "●");
|
|
287
|
+
const toggleOff = this.fg("dim", "○");
|
|
288
|
+
const toggle = this.config.recap.enabled ? toggleOn : toggleOff;
|
|
289
|
+
const label = isSelected
|
|
290
|
+
? this.bold("Enable Recap")
|
|
291
|
+
: this.fg("dim", "Enable Recap");
|
|
292
|
+
|
|
293
|
+
lines.push(
|
|
294
|
+
this.frameLine(
|
|
295
|
+
`${isSelected ? this.fg("accent", "▸") : " "} ${toggle} ${label}`,
|
|
296
|
+
innerWidth
|
|
297
|
+
)
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
// Current model display
|
|
301
|
+
const modelRef = this.config.recap.model;
|
|
302
|
+
const modelLabel = this.fg("dim", ` Model: ${modelRef}`);
|
|
303
|
+
lines.push(this.frameLine(modelLabel, innerWidth));
|
|
304
|
+
lines.push(
|
|
305
|
+
this.frameLine(
|
|
306
|
+
this.fg("dim", " Press M to change model"),
|
|
307
|
+
innerWidth
|
|
308
|
+
)
|
|
309
|
+
);
|
|
310
|
+
}
|
|
251
311
|
}
|
package/types.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
/** Supported notification platforms */
|
|
6
|
-
export type NotifyPlatform = "native" | "gotify" | "telegram";
|
|
6
|
+
export type NotifyPlatform = "native" | "gotify" | "telegram" | "ntfy";
|
|
7
7
|
|
|
8
8
|
/** Per-event notification configuration */
|
|
9
9
|
export interface EventNotifyConfig {
|
|
@@ -43,6 +43,28 @@ export interface TelegramConfig {
|
|
|
43
43
|
chatId?: string;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/** ntfy notification platform config */
|
|
47
|
+
export interface NtfyConfig {
|
|
48
|
+
/** Whether ntfy is enabled */
|
|
49
|
+
enabled: boolean;
|
|
50
|
+
/** ntfy server URL (default: https://ntfy.sh) */
|
|
51
|
+
serverUrl?: string;
|
|
52
|
+
/** ntfy topic to publish to */
|
|
53
|
+
topic?: string;
|
|
54
|
+
/** Optional access token for authenticated ntfy servers */
|
|
55
|
+
token?: string;
|
|
56
|
+
/** Priority level (1-5, default: 3) */
|
|
57
|
+
priority: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Recap notification config */
|
|
61
|
+
export interface RecapConfig {
|
|
62
|
+
/** Whether recap summarization is enabled */
|
|
63
|
+
enabled: boolean;
|
|
64
|
+
/** Model to use for recap (e.g. "openrouter/openai/gpt-oss-20b") */
|
|
65
|
+
model: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
46
68
|
/** Full notification configuration */
|
|
47
69
|
export interface NotifyConfig {
|
|
48
70
|
/** Global default platforms for all events */
|
|
@@ -55,6 +77,10 @@ export interface NotifyConfig {
|
|
|
55
77
|
gotify: GotifyConfig;
|
|
56
78
|
/** Telegram settings */
|
|
57
79
|
telegram: TelegramConfig;
|
|
80
|
+
/** ntfy settings */
|
|
81
|
+
ntfy: NtfyConfig;
|
|
82
|
+
/** Recap summarization settings */
|
|
83
|
+
recap: RecapConfig;
|
|
58
84
|
}
|
|
59
85
|
|
|
60
86
|
/** Parameters for the notify_user agent tool */
|