@pi-unipi/unipi 0.1.13 → 0.1.14
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 +3 -1
- package/package.json +1 -1
- package/packages/autocomplete/src/constants.ts +5 -1
- package/packages/autocomplete/src/provider.ts +43 -5
- package/packages/compactor/src/session/db.ts +5 -1
- package/packages/info-screen/index.ts +42 -13
- package/packages/mcp/src/bridge/registry.ts +1 -3
- package/packages/mcp/src/index.ts +7 -13
- package/packages/memory/index.ts +9 -12
- package/packages/notify/README.md +24 -1
- package/packages/notify/index.ts +6 -2
- package/packages/notify/skills/configure-notify/SKILL.md +25 -0
- package/packages/subagents/src/__tests__/badge-generation.test.ts +17 -1
- package/packages/subagents/src/agent-runner.ts +19 -11
- package/packages/utility/src/index.ts +57 -8
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ pi install npm:@pi-unipi/kanboard
|
|
|
41
41
|
| `@pi-unipi/btw` | Parallel side conversations with `/btw` |
|
|
42
42
|
| `@pi-unipi/web-api` | Web search, read, and summarize with provider selection |
|
|
43
43
|
| `@pi-unipi/compactor` | Session compaction, context management, batch execution |
|
|
44
|
-
| `@pi-unipi/notify` | Cross-platform notifications (native, Gotify, Telegram) |
|
|
44
|
+
| `@pi-unipi/notify` | Cross-platform notifications (native, Gotify, Telegram, ntfy) |
|
|
45
45
|
| `@pi-unipi/utility` | Environment info, diagnostics, settings inspector, cleanup |
|
|
46
46
|
| `@pi-unipi/mcp` | MCP server discovery, connection, and tool integration |
|
|
47
47
|
| `@pi-unipi/ask-user` | Structured user input with options and freeform text |
|
|
@@ -145,6 +145,8 @@ pi install npm:@pi-unipi/kanboard
|
|
|
145
145
|
| `/unipi:notify-settings` | Configure notification platforms |
|
|
146
146
|
| `/unipi:notify-set-gotify` | Set Gotify server config |
|
|
147
147
|
| `/unipi:notify-set-tg` | Set Telegram bot config |
|
|
148
|
+
| `/unipi:notify-set-ntfy` | Set ntfy topic and server |
|
|
149
|
+
| `/unipi:notify-recap-model` | Set model for notification recaps |
|
|
148
150
|
| `/unipi:notify-test` | Test notification delivery |
|
|
149
151
|
|
|
150
152
|
### Utility (`/unipi:*`)
|
package/package.json
CHANGED
|
@@ -132,11 +132,13 @@ export const COMMAND_REGISTRY: Record<string, string> = {
|
|
|
132
132
|
"unipi:milestone-onboard": "milestone",
|
|
133
133
|
"unipi:milestone-update": "milestone",
|
|
134
134
|
|
|
135
|
-
// notify (
|
|
135
|
+
// notify (6 commands)
|
|
136
136
|
"unipi:notify-settings": "notify",
|
|
137
137
|
"unipi:notify-set-gotify": "notify",
|
|
138
138
|
"unipi:notify-set-tg": "notify",
|
|
139
|
+
"unipi:notify-set-ntfy": "notify",
|
|
139
140
|
"unipi:notify-test": "notify",
|
|
141
|
+
"unipi:notify-recap-model": "notify",
|
|
140
142
|
|
|
141
143
|
// kanboard (3 commands)
|
|
142
144
|
"unipi:kanboard": "kanboard",
|
|
@@ -219,7 +221,9 @@ export const COMMAND_DESCRIPTIONS: Record<string, string> = {
|
|
|
219
221
|
"unipi:notify-settings": "Configure notification platforms and events",
|
|
220
222
|
"unipi:notify-set-gotify": "Set up Gotify push notifications",
|
|
221
223
|
"unipi:notify-set-tg": "Set up Telegram bot notifications",
|
|
224
|
+
"unipi:notify-set-ntfy": "Set up ntfy push notifications",
|
|
222
225
|
"unipi:notify-test": "Test all enabled notification platforms",
|
|
226
|
+
"unipi:notify-recap-model": "Select model for notification recaps",
|
|
223
227
|
|
|
224
228
|
"unipi:milestone-onboard": "Create MILESTONES.md from existing workflow docs",
|
|
225
229
|
"unipi:milestone-update": "Sync MILESTONES.md with completed work",
|
|
@@ -274,16 +274,54 @@ export function createEnchantedProvider(
|
|
|
274
274
|
descriptionOverrides,
|
|
275
275
|
);
|
|
276
276
|
|
|
277
|
-
// If no unipi items match,
|
|
277
|
+
// If no unipi items match, handle skill vs system items
|
|
278
278
|
if (enhancedUnipiItems.length === 0) {
|
|
279
|
-
|
|
280
|
-
|
|
279
|
+
if (nonUnipiItems.length === 0) return null;
|
|
280
|
+
|
|
281
|
+
// Check if user explicitly typed /skill: prefix
|
|
282
|
+
const isSkillQuery = effectivePrefix.replace(/^\//, "").toLowerCase().startsWith("skill:");
|
|
283
|
+
|
|
284
|
+
if (isSkillQuery) {
|
|
285
|
+
// User wants skill commands — return them
|
|
286
|
+
return { items: nonUnipiItems, prefix: effectivePrefix };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Otherwise, filter out skill commands from suggestions
|
|
290
|
+
const systemOnly = nonUnipiItems.filter(item => !item.value.startsWith("skill:"));
|
|
291
|
+
return systemOnly.length > 0
|
|
292
|
+
? { items: systemOnly, prefix: effectivePrefix }
|
|
281
293
|
: null;
|
|
282
294
|
}
|
|
283
295
|
|
|
284
|
-
//
|
|
296
|
+
// Separate non-unipi items into system commands and skill commands
|
|
297
|
+
const systemItems: AutocompleteItem[] = [];
|
|
298
|
+
const skillItems: AutocompleteItem[] = [];
|
|
299
|
+
|
|
300
|
+
for (const item of nonUnipiItems) {
|
|
301
|
+
if (item.value.startsWith("skill:")) {
|
|
302
|
+
skillItems.push(item);
|
|
303
|
+
} else {
|
|
304
|
+
systemItems.push(item);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Check if user explicitly typed /skill: prefix
|
|
309
|
+
const isExplicitSkillQuery = effectivePrefix.replace(/^\//, "").toLowerCase().startsWith("skill:");
|
|
310
|
+
|
|
311
|
+
// Build final list based on query context
|
|
312
|
+
let finalItems: AutocompleteItem[];
|
|
313
|
+
|
|
314
|
+
if (isExplicitSkillQuery) {
|
|
315
|
+
// User explicitly wants skill commands — show them first
|
|
316
|
+
finalItems = [...skillItems, ...enhancedUnipiItems, ...systemItems];
|
|
317
|
+
} else {
|
|
318
|
+
// Default: unipi commands first, then system commands, hide skill commands
|
|
319
|
+
// (skill commands are redundant when unipi equivalents exist)
|
|
320
|
+
finalItems = [...enhancedUnipiItems, ...systemItems];
|
|
321
|
+
}
|
|
322
|
+
|
|
285
323
|
return {
|
|
286
|
-
items:
|
|
324
|
+
items: finalItems,
|
|
287
325
|
prefix: effectivePrefix,
|
|
288
326
|
};
|
|
289
327
|
},
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import { execFileSync } from "node:child_process";
|
|
7
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
7
8
|
import { homedir } from "node:os";
|
|
8
|
-
import { join } from "node:path";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
9
10
|
import type { SessionEvent, StoredEvent, SessionMeta, ResumeRow } from "../types.js";
|
|
10
11
|
|
|
11
12
|
export function getWorktreeSuffix(): string {
|
|
@@ -76,6 +77,9 @@ export class SessionDB {
|
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
async init(): Promise<void> {
|
|
80
|
+
const dir = dirname(this.dbPath);
|
|
81
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
82
|
+
|
|
79
83
|
const sqlite: any = await getSQLite();
|
|
80
84
|
// Handle different SQLite API shapes:
|
|
81
85
|
// - bun:sqlite exports Database as a named export
|
|
@@ -32,27 +32,56 @@ export default function (pi: ExtensionAPI) {
|
|
|
32
32
|
// Start load tracking
|
|
33
33
|
startLoadTracking();
|
|
34
34
|
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
// Debounced MODULE_READY handling — batch module announcements
|
|
36
|
+
// to prevent layout shift from rapid per-module cache invalidation.
|
|
37
|
+
let moduleReadyBatch: Array<{ name: string; version: string; tools?: string[]; loadTimeMs?: number }> = [];
|
|
38
|
+
let moduleReadyTimer: ReturnType<typeof setTimeout> | null = null;
|
|
39
|
+
const MODULE_READY_DEBOUNCE_MS = 150;
|
|
40
|
+
|
|
41
|
+
function flushModuleReadyBatch(): void {
|
|
42
|
+
const batch = moduleReadyBatch;
|
|
43
|
+
moduleReadyBatch = [];
|
|
44
|
+
moduleReadyTimer = null;
|
|
45
|
+
|
|
46
|
+
if (batch.length === 0) return;
|
|
47
|
+
|
|
48
|
+
// Track all modules and tools
|
|
49
|
+
let hasTools = false;
|
|
50
|
+
for (const event of batch) {
|
|
38
51
|
trackModule(event.name, event.version || "unknown");
|
|
39
52
|
recordLoadTime(event.name, "module", event.loadTimeMs);
|
|
40
|
-
|
|
41
|
-
// Invalidate overview so next fetch picks up new module list
|
|
42
|
-
infoRegistry.invalidateCache("overview");
|
|
43
|
-
|
|
44
|
-
// Trigger background refresh of overview — subscribers will re-render
|
|
45
|
-
infoRegistry.getGroupData("overview");
|
|
46
|
-
|
|
47
53
|
if (event.tools && Array.isArray(event.tools)) {
|
|
48
54
|
for (const tool of event.tools) {
|
|
49
55
|
trackTool(tool, event.name);
|
|
50
56
|
}
|
|
51
|
-
|
|
52
|
-
infoRegistry.invalidateCache("tools");
|
|
53
|
-
infoRegistry.getGroupData("tools");
|
|
57
|
+
hasTools = true;
|
|
54
58
|
}
|
|
55
59
|
}
|
|
60
|
+
|
|
61
|
+
// Single cache invalidation for all modules
|
|
62
|
+
infoRegistry.invalidateCache("overview");
|
|
63
|
+
infoRegistry.getGroupData("overview");
|
|
64
|
+
|
|
65
|
+
if (hasTools) {
|
|
66
|
+
infoRegistry.invalidateCache("tools");
|
|
67
|
+
infoRegistry.getGroupData("tools");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Listen for module announcements — track and trigger reactive updates
|
|
72
|
+
pi.events.on(UNIPI_EVENTS.MODULE_READY, (event: any) => {
|
|
73
|
+
if (event.name && event.name !== MODULES.INFO_SCREEN) {
|
|
74
|
+
moduleReadyBatch.push({
|
|
75
|
+
name: event.name,
|
|
76
|
+
version: event.version,
|
|
77
|
+
tools: event.tools,
|
|
78
|
+
loadTimeMs: event.loadTimeMs,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Debounce: wait for more modules to arrive, then flush once
|
|
82
|
+
if (moduleReadyTimer) clearTimeout(moduleReadyTimer);
|
|
83
|
+
moduleReadyTimer = setTimeout(flushModuleReadyBatch, MODULE_READY_DEBOUNCE_MS);
|
|
84
|
+
}
|
|
56
85
|
});
|
|
57
86
|
|
|
58
87
|
pi.events.on(UNIPI_EVENTS.INFO_GROUP_REGISTERED, (_event: any) => {
|
|
@@ -107,9 +107,7 @@ export class ServerRegistry {
|
|
|
107
107
|
safeEnv[k] = typeof v === "string" ? v : String(v);
|
|
108
108
|
}
|
|
109
109
|
} else {
|
|
110
|
-
|
|
111
|
-
`[MCP] Server '${name}': env is not an object (${typeof def.env}), skipping env vars`,
|
|
112
|
-
);
|
|
110
|
+
// Env config invalid — silently skip env vars.
|
|
113
111
|
}
|
|
114
112
|
}
|
|
115
113
|
|
|
@@ -75,11 +75,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
75
75
|
try {
|
|
76
76
|
const result = loadAndResolve(cwd);
|
|
77
77
|
servers = result.servers;
|
|
78
|
-
} catch (
|
|
79
|
-
|
|
80
|
-
"[MCP] Failed to load config:",
|
|
81
|
-
err instanceof Error ? err.message : err,
|
|
82
|
-
);
|
|
78
|
+
} catch (_err) {
|
|
79
|
+
// Config load failure — servers will be empty, visible via /unipi:mcp-status.
|
|
83
80
|
}
|
|
84
81
|
|
|
85
82
|
// Start enabled servers (parallel, non-blocking errors)
|
|
@@ -88,14 +85,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
88
85
|
.map(async (server) => {
|
|
89
86
|
try {
|
|
90
87
|
await registry!.startServer(server);
|
|
91
|
-
console.log
|
|
92
|
-
|
|
93
|
-
);
|
|
88
|
+
// Removed console.log — startup logs cause layout shift in TUI.
|
|
89
|
+
// Server status visible via /unipi:mcp-status or info screen.
|
|
94
90
|
} catch (err) {
|
|
95
|
-
console.error
|
|
96
|
-
|
|
97
|
-
err instanceof Error ? err.message : err,
|
|
98
|
-
);
|
|
91
|
+
// Removed console.error — errors surfaced via info-screen MCP group.
|
|
92
|
+
// Server failure tracked in registry state.
|
|
99
93
|
}
|
|
100
94
|
});
|
|
101
95
|
|
|
@@ -138,7 +132,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
138
132
|
},
|
|
139
133
|
};
|
|
140
134
|
} catch (err) {
|
|
141
|
-
console.error
|
|
135
|
+
// Removed console.error — info-screen shows "?" on error.
|
|
142
136
|
return {
|
|
143
137
|
total: { value: "?" },
|
|
144
138
|
active: { value: "?" },
|
package/packages/memory/index.ts
CHANGED
|
@@ -78,11 +78,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
78
78
|
|
|
79
79
|
// Sync any orphaned markdown files into the database
|
|
80
80
|
const synced = projectStorage.syncOrphanedFiles();
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
console.warn("[unipi/memory] Failed to initialize storage, running without memory:", (err as any)?.message ?? err);
|
|
81
|
+
// Removed console.warn — orphaned file sync is informational only.
|
|
82
|
+
// Visible via memory tool list or info-screen memory group.
|
|
83
|
+
} catch (_err) {
|
|
84
|
+
// Memory init failure — running without memory. Silent startup.
|
|
86
85
|
projectStorage = null;
|
|
87
86
|
}
|
|
88
87
|
|
|
@@ -113,7 +112,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
113
112
|
// Register info group
|
|
114
113
|
const registry = getInfoRegistry();
|
|
115
114
|
if (registry) {
|
|
116
|
-
console.debug("[memory] Registering info group");
|
|
117
115
|
registry.registerGroup({
|
|
118
116
|
id: "memory",
|
|
119
117
|
name: "Memory",
|
|
@@ -143,8 +141,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
143
141
|
try {
|
|
144
142
|
projectMemories = projectStorage.listAll();
|
|
145
143
|
allMemories = listAllProjects();
|
|
146
|
-
} catch (
|
|
147
|
-
|
|
144
|
+
} catch (_err) {
|
|
145
|
+
// Info panel data unavailable — shows empty values.
|
|
148
146
|
}
|
|
149
147
|
const uniqueProjects = [...new Set(allMemories.map((m) => m.project))];
|
|
150
148
|
|
|
@@ -175,8 +173,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
175
173
|
try {
|
|
176
174
|
projectCount = projectStorage?.listAll()?.length ?? 0;
|
|
177
175
|
projectCountAll = listAllProjects().length;
|
|
178
|
-
} catch (
|
|
179
|
-
|
|
176
|
+
} catch (_err) {
|
|
177
|
+
// Count unavailable — status bar shows 0.
|
|
180
178
|
}
|
|
181
179
|
const vecReady = isEmbeddingReady();
|
|
182
180
|
const vecIcon = vecReady ? "⚡" : "📝";
|
|
@@ -196,8 +194,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
196
194
|
let projectMemories: Array<{ id: string; title: string; type: string }> = [];
|
|
197
195
|
try {
|
|
198
196
|
projectMemories = projectStorage.listAll();
|
|
199
|
-
} catch (
|
|
200
|
-
console.warn("[unipi/memory] Failed to list memories for recall:", err);
|
|
197
|
+
} catch (_err) {
|
|
201
198
|
recallDone = true; // Skip recall on error
|
|
202
199
|
return;
|
|
203
200
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @pi-unipi/notify
|
|
2
2
|
|
|
3
|
-
Cross-platform notification extension for Pi. Sends push notifications to native OS, Gotify, and
|
|
3
|
+
Cross-platform notification extension for Pi. Sends push notifications to native OS, Gotify, Telegram, and ntfy when agent lifecycle events occur.
|
|
4
4
|
|
|
5
5
|
## What it does
|
|
6
6
|
|
|
@@ -52,12 +52,35 @@ This guides you through:
|
|
|
52
52
|
2. Pasting the bot token
|
|
53
53
|
3. Auto-detecting your chat ID
|
|
54
54
|
|
|
55
|
+
### ntfy
|
|
56
|
+
|
|
57
|
+
HTTP-based pub-sub notifications via [ntfy.sh](https://ntfy.sh) or self-hosted. Run setup command:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
/unipi:notify-set-ntfy
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Or configure manually:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"ntfy": {
|
|
68
|
+
"enabled": true,
|
|
69
|
+
"serverUrl": "https://ntfy.sh",
|
|
70
|
+
"topic": "your-topic-name",
|
|
71
|
+
"priority": 3
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
55
76
|
## Commands
|
|
56
77
|
|
|
57
78
|
| Command | Description |
|
|
58
79
|
|---------|-------------|
|
|
59
80
|
| `/unipi:notify-settings` | Open settings overlay to configure platforms and events |
|
|
81
|
+
| `/unipi:notify-set-gotify` | Configure Gotify server connection |
|
|
60
82
|
| `/unipi:notify-set-tg` | Interactive Telegram bot setup |
|
|
83
|
+
| `/unipi:notify-set-ntfy` | Configure ntfy topic and server |
|
|
61
84
|
| `/unipi:notify-test` | Send test notification to all enabled platforms |
|
|
62
85
|
|
|
63
86
|
## Agent Tool
|
package/packages/notify/index.ts
CHANGED
|
@@ -19,6 +19,8 @@ import { loadConfig } from "./settings.js";
|
|
|
19
19
|
import {
|
|
20
20
|
registerEventListeners,
|
|
21
21
|
unregisterEventListeners,
|
|
22
|
+
setSessionContext,
|
|
23
|
+
clearSessionContext,
|
|
22
24
|
} from "./events.js";
|
|
23
25
|
|
|
24
26
|
/** Package version */
|
|
@@ -38,20 +40,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
38
40
|
registerNotifyCommands(pi);
|
|
39
41
|
|
|
40
42
|
// Session lifecycle — register events and announce module
|
|
41
|
-
pi.on("session_start", async () => {
|
|
43
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
44
|
+
setSessionContext(ctx);
|
|
42
45
|
const config = loadConfig();
|
|
43
46
|
registerEventListeners(pi, config);
|
|
44
47
|
|
|
45
48
|
emitEvent(pi, UNIPI_EVENTS.MODULE_READY, {
|
|
46
49
|
name: MODULES.NOTIFY,
|
|
47
50
|
version: VERSION,
|
|
48
|
-
commands: ["unipi:notify-settings", "unipi:notify-set-gotify", "unipi:notify-set-tg", "unipi:notify-test"],
|
|
51
|
+
commands: ["unipi:notify-settings", "unipi:notify-set-gotify", "unipi:notify-set-tg", "unipi:notify-set-ntfy", "unipi:notify-test", "unipi:notify-recap-model"],
|
|
49
52
|
tools: [NOTIFY_TOOLS.NOTIFY_USER],
|
|
50
53
|
});
|
|
51
54
|
});
|
|
52
55
|
|
|
53
56
|
// Cleanup on session shutdown
|
|
54
57
|
pi.on("session_shutdown", async () => {
|
|
58
|
+
clearSessionContext();
|
|
55
59
|
unregisterEventListeners();
|
|
56
60
|
});
|
|
57
61
|
}
|
|
@@ -47,6 +47,13 @@ Help users configure the `@pi-unipi/notify` notification system.
|
|
|
47
47
|
"enabled": false,
|
|
48
48
|
"botToken": null,
|
|
49
49
|
"chatId": null
|
|
50
|
+
},
|
|
51
|
+
"ntfy": {
|
|
52
|
+
"enabled": false,
|
|
53
|
+
"serverUrl": "https://ntfy.sh",
|
|
54
|
+
"topic": null,
|
|
55
|
+
"token": null,
|
|
56
|
+
"priority": 3
|
|
50
57
|
}
|
|
51
58
|
}
|
|
52
59
|
```
|
|
@@ -75,6 +82,20 @@ Bot API notifications. Requires:
|
|
|
75
82
|
- `botToken` — From @BotFather
|
|
76
83
|
- `chatId` — Auto-detected by `/unipi:notify-set-tg`
|
|
77
84
|
|
|
85
|
+
### ntfy (default: disabled)
|
|
86
|
+
|
|
87
|
+
Simple HTTP-based pub-sub notification service. Supports public [ntfy.sh](https://ntfy.sh) and self-hosted instances.
|
|
88
|
+
Requires:
|
|
89
|
+
- `serverUrl` — ntfy server URL (default: `https://ntfy.sh`)
|
|
90
|
+
- `topic` — Topic name to publish to (acts as a channel)
|
|
91
|
+
- `token` — Optional access token for authenticated servers
|
|
92
|
+
- `priority` — 1-5 (default: 3)
|
|
93
|
+
|
|
94
|
+
**Setup options:**
|
|
95
|
+
1. **Interactive overlay:** Run `/unipi:notify-set-ntfy` for guided setup with connection test
|
|
96
|
+
2. **Manual config:** Edit `config.json` directly with the fields above
|
|
97
|
+
3. **Agent can write config:** Read the current config, merge changes, write back
|
|
98
|
+
|
|
78
99
|
## Commands
|
|
79
100
|
|
|
80
101
|
| Command | Description |
|
|
@@ -82,6 +103,7 @@ Bot API notifications. Requires:
|
|
|
82
103
|
| `/unipi:notify-settings` | TUI overlay to toggle platforms and events |
|
|
83
104
|
| `/unipi:notify-set-gotify` | Interactive Gotify setup wizard |
|
|
84
105
|
| `/unipi:notify-set-tg` | Interactive Telegram setup wizard |
|
|
106
|
+
| `/unipi:notify-set-ntfy` | Interactive ntfy setup wizard |
|
|
85
107
|
| `/unipi:notify-test` | Send test notification to all enabled platforms |
|
|
86
108
|
|
|
87
109
|
## Events
|
|
@@ -125,6 +147,7 @@ Read the JSON, make changes, write it back. Example:
|
|
|
125
147
|
|
|
126
148
|
For Gotify: suggest running `/unipi:notify-set-gotify`
|
|
127
149
|
For Telegram: suggest running `/unipi:notify-set-tg`
|
|
150
|
+
For ntfy: suggest running `/unipi:notify-set-ntfy`
|
|
128
151
|
For general settings: suggest `/unipi:notify-settings`
|
|
129
152
|
|
|
130
153
|
## Validation rules
|
|
@@ -132,3 +155,5 @@ For general settings: suggest `/unipi:notify-settings`
|
|
|
132
155
|
- Gotify: `serverUrl` and `appToken` required when enabled
|
|
133
156
|
- Gotify: `priority` must be 1-10
|
|
134
157
|
- Telegram: `botToken` and `chatId` required when enabled
|
|
158
|
+
- ntfy: `serverUrl` and `topic` required when enabled
|
|
159
|
+
- ntfy: `priority` must be 1-5
|
|
@@ -205,6 +205,7 @@ describe("Badge generation — event bus (CRITICAL FIX)", () => {
|
|
|
205
205
|
const validLifecycleEvents = [
|
|
206
206
|
"session_start", "session_shutdown", "input",
|
|
207
207
|
"tool_call", "tool_execution_start",
|
|
208
|
+
"agent_end", "before_agent_start",
|
|
208
209
|
];
|
|
209
210
|
|
|
210
211
|
// Check that pi.on() is only used with lifecycle events
|
|
@@ -228,11 +229,26 @@ describe("Badge generation — event bus (CRITICAL FIX)", () => {
|
|
|
228
229
|
// ─── Test: Event flow ──────────────────────────────────────────────
|
|
229
230
|
|
|
230
231
|
describe("Badge generation — event flow", () => {
|
|
231
|
-
it("utility emits BADGE_GENERATE_REQUEST
|
|
232
|
+
it("utility emits BADGE_GENERATE_REQUEST after agent responds (deferred from input)", () => {
|
|
232
233
|
const src = readSource("packages/utility/src/index.ts");
|
|
233
234
|
|
|
235
|
+
// BADGE_GENERATE_REQUEST should be emitted in agent_end handler, not input
|
|
234
236
|
assert.ok(src.includes("BADGE_GENERATE_REQUEST"));
|
|
235
237
|
assert.ok(src.includes('source: "input-hook"'));
|
|
238
|
+
|
|
239
|
+
// input handler should NOT emit BADGE_GENERATE_REQUEST directly
|
|
240
|
+
const inputBlock = src.match(/pi\.on\("input"[\s\S]*?(?=pi\.on\(|$)/)?.[0] ?? "";
|
|
241
|
+
assert.ok(
|
|
242
|
+
!inputBlock.includes("BADGE_GENERATE_REQUEST"),
|
|
243
|
+
"input handler should NOT emit BADGE_GENERATE_REQUEST — deferred to agent_end",
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
// agent_end handler should emit BADGE_GENERATE_REQUEST
|
|
247
|
+
const agentEndBlock = src.match(/pi\.on\("agent_end"[\s\S]*?(?=pi\.on\(|$)/)?.[0] ?? "";
|
|
248
|
+
assert.ok(
|
|
249
|
+
agentEndBlock.includes("BADGE_GENERATE_REQUEST"),
|
|
250
|
+
"agent_end handler should emit BADGE_GENERATE_REQUEST with full conversation context",
|
|
251
|
+
);
|
|
236
252
|
});
|
|
237
253
|
|
|
238
254
|
it("BADGE_GENERATE_REQUEST event is defined in core", () => {
|
|
@@ -172,12 +172,16 @@ export async function runAgent(
|
|
|
172
172
|
let toolNames = getToolNamesForType(type, agentConfig);
|
|
173
173
|
|
|
174
174
|
// Create resource loader
|
|
175
|
+
// Respect agentConfig.extensions/skills flags: if explicitly false, skip loading.
|
|
176
|
+
// This prevents explore/work agents from loading all parent extensions.
|
|
175
177
|
const agentDir = getAgentDir();
|
|
178
|
+
const skipExtensions = options.isolated || agentConfig?.extensions === false;
|
|
179
|
+
const skipSkills = options.isolated || agentConfig?.skills === false;
|
|
176
180
|
const loader = new DefaultResourceLoader({
|
|
177
181
|
cwd: effectiveCwd,
|
|
178
182
|
agentDir,
|
|
179
|
-
noExtensions:
|
|
180
|
-
noSkills:
|
|
183
|
+
noExtensions: skipExtensions,
|
|
184
|
+
noSkills: skipSkills,
|
|
181
185
|
noPromptTemplates: true,
|
|
182
186
|
noThemes: true,
|
|
183
187
|
noContextFiles: true,
|
|
@@ -213,15 +217,19 @@ export async function runAgent(
|
|
|
213
217
|
});
|
|
214
218
|
session.setActiveToolsByName(activeTools);
|
|
215
219
|
|
|
216
|
-
// Bind extensions
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
220
|
+
// Bind extensions — only if extensions were loaded.
|
|
221
|
+
// Skipping for agents with extensions: false avoids firing session_start
|
|
222
|
+
// on an empty extension set, preventing unnecessary MODULE_READY cascade.
|
|
223
|
+
if (!skipExtensions) {
|
|
224
|
+
await session.bindExtensions({
|
|
225
|
+
onError: (err) => {
|
|
226
|
+
options.onToolActivity?.({
|
|
227
|
+
type: "end",
|
|
228
|
+
toolName: `extension-error:${err.extensionPath}`,
|
|
229
|
+
});
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
}
|
|
225
233
|
|
|
226
234
|
options.onSessionCreated?.(session);
|
|
227
235
|
|
|
@@ -38,6 +38,12 @@ const VERSION = getPackageVersion(new URL(".", import.meta.url).pathname);
|
|
|
38
38
|
/** Whether we've seen the first user message (for auto badge generation) */
|
|
39
39
|
let firstMessageSeen = false;
|
|
40
40
|
|
|
41
|
+
/** Stored user text from first input, used to build conversation summary after agent responds */
|
|
42
|
+
let firstUserText = "";
|
|
43
|
+
|
|
44
|
+
/** Stored UI context from first input, used to show badge overlay after agent responds */
|
|
45
|
+
let firstInputCtx: any = null;
|
|
46
|
+
|
|
41
47
|
/** All commands registered by this module */
|
|
42
48
|
const ALL_COMMANDS = [
|
|
43
49
|
UTILITY_COMMANDS.CONTINUE,
|
|
@@ -106,7 +112,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
106
112
|
}
|
|
107
113
|
});
|
|
108
114
|
|
|
109
|
-
// First-message hook:
|
|
115
|
+
// First-message hook: capture user text for deferred badge generation
|
|
110
116
|
pi.on("input", async (_event: any, ctx: any) => {
|
|
111
117
|
// Only trigger on first user message
|
|
112
118
|
if (firstMessageSeen) return;
|
|
@@ -120,8 +126,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
120
126
|
const sessionName = pi.getSessionName?.();
|
|
121
127
|
if (sessionName) return;
|
|
122
128
|
|
|
123
|
-
//
|
|
124
|
-
|
|
129
|
+
// Store first message text for later use in agent_end
|
|
130
|
+
firstUserText = typeof _event?.content === "string"
|
|
125
131
|
? _event.content
|
|
126
132
|
: Array.isArray(_event?.content)
|
|
127
133
|
? _event.content
|
|
@@ -130,16 +136,57 @@ export default function (pi: ExtensionAPI) {
|
|
|
130
136
|
.join(" ")
|
|
131
137
|
: "";
|
|
132
138
|
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
139
|
+
// Store ctx for badge overlay show after agent responds
|
|
140
|
+
firstInputCtx = ctx;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// After agent completes first response, generate badge name with full conversation context
|
|
144
|
+
pi.on("agent_end", async (event: any, _ctx: any) => {
|
|
145
|
+
// Only act if we captured a first input and are waiting for badge generation
|
|
146
|
+
if (!firstInputCtx) return;
|
|
147
|
+
const ctx = firstInputCtx;
|
|
148
|
+
firstInputCtx = null; // consume — only trigger once
|
|
149
|
+
|
|
150
|
+
// Check if a name was already set (e.g. manually) in the meantime
|
|
151
|
+
const sessionName = pi.getSessionName?.();
|
|
152
|
+
if (sessionName) return;
|
|
138
153
|
|
|
139
154
|
// Show badge overlay if UI available
|
|
140
155
|
if (ctx?.hasUI && !nameBadgeState.isVisible()) {
|
|
141
156
|
await nameBadgeState.show(pi, ctx);
|
|
142
157
|
}
|
|
158
|
+
|
|
159
|
+
// Build conversation summary from full message history (user + assistant)
|
|
160
|
+
const messages: any[] = event?.messages ?? [];
|
|
161
|
+
const summaryParts: string[] = [];
|
|
162
|
+
|
|
163
|
+
// Include the user's first message
|
|
164
|
+
if (firstUserText) {
|
|
165
|
+
summaryParts.push(`User: ${firstUserText}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Include assistant's response text
|
|
169
|
+
const assistantMsgs = messages.filter((m: any) => m.role === "assistant");
|
|
170
|
+
for (const msg of assistantMsgs) {
|
|
171
|
+
if (Array.isArray(msg.content)) {
|
|
172
|
+
const textParts = msg.content
|
|
173
|
+
.filter((c: any) => c.type === "text")
|
|
174
|
+
.map((c: any) => c.text)
|
|
175
|
+
.join(" ");
|
|
176
|
+
if (textParts) summaryParts.push(`Assistant: ${textParts}`);
|
|
177
|
+
} else if (typeof msg.content === "string" && msg.content) {
|
|
178
|
+
summaryParts.push(`Assistant: ${msg.content}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Truncate to reasonable size
|
|
183
|
+
const conversationSummary = summaryParts.join("\n").slice(0, 800);
|
|
184
|
+
|
|
185
|
+
// Emit event for subagents to spawn background agent
|
|
186
|
+
emitEvent(pi, UNIPI_EVENTS.BADGE_GENERATE_REQUEST, {
|
|
187
|
+
source: "input-hook",
|
|
188
|
+
conversationSummary,
|
|
189
|
+
});
|
|
143
190
|
});
|
|
144
191
|
|
|
145
192
|
// Track command usage
|
|
@@ -153,6 +200,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
153
200
|
pi.on("session_shutdown", async () => {
|
|
154
201
|
nameBadgeState.hide();
|
|
155
202
|
firstMessageSeen = false;
|
|
203
|
+
firstUserText = "";
|
|
204
|
+
firstInputCtx = null;
|
|
156
205
|
await lifecycle.shutdown("session_shutdown");
|
|
157
206
|
});
|
|
158
207
|
}
|