pi-all-notification 1.0.2 → 1.0.4
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 +4 -8
- package/extensions/index.ts +31 -18
- package/extensions/settle.ts +49 -0
- package/package.json +8 -2
- package/tests/settle.test.ts +49 -0
package/README.md
CHANGED
|
@@ -16,19 +16,15 @@ pi 多渠道通知扩展 — macOS 桌面通知 / Bark 推送 / 自定义 Webhoo
|
|
|
16
16
|
pi install npm:pi-all-notification
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
或通过 git:
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
pi install git:github.com/RealAlexandreAI/pi-all-notification
|
|
23
|
-
```
|
|
24
|
-
|
|
25
19
|
重启 pi(或 `/reload`)。
|
|
26
20
|
|
|
27
21
|
## 使用
|
|
28
22
|
|
|
29
23
|
### Agent 自动触发
|
|
30
24
|
|
|
31
|
-
|
|
25
|
+
一轮运行**最终结束**时自动通知(Pi 的 `agent_settled` 事件)。自然结束发 `✅ Complete`,被用户取消发 `⏹ Cancelled`,两者不会混为一谈。可在设置面板 toggle 触发时机。
|
|
26
|
+
|
|
27
|
+
> 用的是 `agent_settled` 而不是 `agent_end`:`agent_end` 之后还可能跟着重试、压缩恢复或排队的消息,在那里发通知既可能过早,也无法区分「完成」和「被取消」。
|
|
32
28
|
|
|
33
29
|
### Agent 手动调用
|
|
34
30
|
|
|
@@ -111,7 +107,7 @@ Webhook 支持 GET 和 POST,Body 模板支持变量替换:
|
|
|
111
107
|
|
|
112
108
|
| 事件 | 默认 | 说明 |
|
|
113
109
|
|------|------|------|
|
|
114
|
-
| `agent_end` | ✅ |
|
|
110
|
+
| `agent_end` | ✅ | 一轮运行结束(自然完成 `✅` 或被取消 `⏹`) |
|
|
115
111
|
| `agent_start` | ⬜ | Agent 开始处理 |
|
|
116
112
|
| `tool_error` | ⬜ | 工具执行出错 |
|
|
117
113
|
|
package/extensions/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
18
18
|
import { join, dirname } from "node:path";
|
|
19
19
|
import { homedir } from "node:os";
|
|
20
20
|
import { execSync } from "node:child_process";
|
|
21
|
+
import { describeTask, settleNotification } from "./settle.ts";
|
|
21
22
|
|
|
22
23
|
// ─── Types ────────────────────────────────────────────────────
|
|
23
24
|
|
|
@@ -46,6 +47,10 @@ interface WebhookConfig extends ChannelConfig {
|
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
interface TriggerConfig {
|
|
50
|
+
/**
|
|
51
|
+
* Notify when a run settles: naturally (`✅ Complete`) or cancelled (`⏹ Cancelled`).
|
|
52
|
+
* Named `agent_end` for backward compatibility with existing config files.
|
|
53
|
+
*/
|
|
49
54
|
agent_end: boolean;
|
|
50
55
|
agent_start: boolean;
|
|
51
56
|
tool_error: boolean;
|
|
@@ -252,6 +257,14 @@ function settingLabel(emoji: string, name: string, enabled: boolean, extra?: str
|
|
|
252
257
|
|
|
253
258
|
export default function (pi: ExtensionAPI) {
|
|
254
259
|
let projectName = "";
|
|
260
|
+
/**
|
|
261
|
+
* The prompt that started the current run, for the completion notification.
|
|
262
|
+
*
|
|
263
|
+
* `agent_settled` carries no messages, so the text is kept from
|
|
264
|
+
* `before_agent_start` instead; `agent_end` used to read it off
|
|
265
|
+
* `event.messages`, which `agent_settled` does not have.
|
|
266
|
+
*/
|
|
267
|
+
let lastTaskDescription: string | null = null;
|
|
255
268
|
|
|
256
269
|
// ── notify_user agent tool ────────────────────────────
|
|
257
270
|
pi.registerTool({
|
|
@@ -681,7 +694,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
681
694
|
while (true) {
|
|
682
695
|
const t = cfg.triggers;
|
|
683
696
|
const items = [
|
|
684
|
-
{ label: settingLabel("🏁", "agent_end (
|
|
697
|
+
{ label: settingLabel("🏁", "agent_end (run settled: complete or cancelled)", t.agent_end), value: "agent_end" },
|
|
685
698
|
{ label: settingLabel("🚀", "agent_start (processing begins)", t.agent_start), value: "agent_start" },
|
|
686
699
|
{ label: settingLabel("❌", "tool_error (tool execution error)", t.tool_error), value: "tool_error" },
|
|
687
700
|
{ label: "← Back", value: "back" },
|
|
@@ -747,6 +760,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
747
760
|
projectName = ctx.cwd.split(/[/\\]/).pop() || "";
|
|
748
761
|
});
|
|
749
762
|
|
|
763
|
+
// Capture the prompt for the completion notification: `agent_settled` carries
|
|
764
|
+
// no messages, and this is the last point the prompt text is available.
|
|
765
|
+
pi.on("before_agent_start", (event, _ctx) => {
|
|
766
|
+
lastTaskDescription = describeTask((event as { prompt?: unknown }).prompt);
|
|
767
|
+
});
|
|
768
|
+
|
|
750
769
|
pi.on("agent_start", (_event, ctx) => {
|
|
751
770
|
projectName = ctx.cwd.split(/[/\\]/).pop() || "";
|
|
752
771
|
const cfg = loadConfig();
|
|
@@ -762,28 +781,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
762
781
|
fireAll(cfg, vars);
|
|
763
782
|
});
|
|
764
783
|
|
|
765
|
-
|
|
784
|
+
// `agent_settled` is Pi's final notification boundary and is the only event
|
|
785
|
+
// that says whether the run was cancelled. `agent_end` fires earlier and can
|
|
786
|
+
// still be followed by retries, recovery or queued work, so a notification
|
|
787
|
+
// sent there can be both premature and wrong about how the run finished.
|
|
788
|
+
pi.on("agent_settled", (event, _ctx) => {
|
|
766
789
|
const cfg = loadConfig();
|
|
767
790
|
if (!cfg.triggers.agent_end) return;
|
|
768
791
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
const raw = typeof m.content === "string" ? m.content.trim() : "";
|
|
775
|
-
const firstLine = raw.split(/\n/)[0] || "";
|
|
776
|
-
taskDesc = firstLine.length > 40 ? firstLine.slice(0, 37) + "..." : firstLine;
|
|
777
|
-
break;
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
|
|
792
|
+
const decision = settleNotification(
|
|
793
|
+
event as { aborted?: unknown },
|
|
794
|
+
projectName,
|
|
795
|
+
lastTaskDescription,
|
|
796
|
+
);
|
|
782
797
|
const vars: NotificationVars = {
|
|
783
|
-
|
|
784
|
-
message: taskDesc,
|
|
798
|
+
...decision,
|
|
785
799
|
project: projectName,
|
|
786
|
-
emoji: "✅",
|
|
787
800
|
timestamp: new Date().toISOString(),
|
|
788
801
|
};
|
|
789
802
|
fireAll(cfg, vars);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a settled run should notify about.
|
|
3
|
+
*
|
|
4
|
+
* Pi's `agent_settled` is the final notification boundary and the only event
|
|
5
|
+
* that reports whether the run was aborted. `agent_end` fires earlier and may
|
|
6
|
+
* still be followed by retries, recovery or queued work, so a notification sent
|
|
7
|
+
* there can be both premature and wrong about how the run finished. This module
|
|
8
|
+
* holds the decision alone, so it is testable without loading the extension.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface SettleNotification {
|
|
12
|
+
title: string;
|
|
13
|
+
message: string;
|
|
14
|
+
emoji: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** A run's prompt, collapsed to one short line for a notification body. */
|
|
18
|
+
export function describeTask(prompt: unknown): string | null {
|
|
19
|
+
if (typeof prompt !== "string") return null;
|
|
20
|
+
const firstLine = prompt.trim().split(/\n/)[0] ?? "";
|
|
21
|
+
if (!firstLine) return null;
|
|
22
|
+
return firstLine.length > 40 ? `${firstLine.slice(0, 37)}...` : firstLine;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The notification for a settled run.
|
|
27
|
+
*
|
|
28
|
+
* A cancelled run says so instead of reporting success: `aborted` distinguishes
|
|
29
|
+
* a run the person stopped from one that finished on its own, and calling a
|
|
30
|
+
* cancellation "Complete" is the bug this guards.
|
|
31
|
+
*/
|
|
32
|
+
export function settleNotification(
|
|
33
|
+
event: { aborted?: unknown },
|
|
34
|
+
projectName: string,
|
|
35
|
+
taskDescription: string | null,
|
|
36
|
+
): SettleNotification {
|
|
37
|
+
if (event?.aborted === true) {
|
|
38
|
+
return {
|
|
39
|
+
title: "pi ⏹ Cancelled",
|
|
40
|
+
message: `Run cancelled — ${projectName}`,
|
|
41
|
+
emoji: "⏹",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
title: "pi ✅ Complete",
|
|
46
|
+
message: taskDescription ?? projectName,
|
|
47
|
+
emoji: "✅",
|
|
48
|
+
};
|
|
49
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-all-notification",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Multi-channel notification for pi: macOS desktop, Bark push, custom webhook",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -19,7 +19,13 @@
|
|
|
19
19
|
"./extensions/index.ts"
|
|
20
20
|
]
|
|
21
21
|
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"test": "node --import tsx --test tests/*.test.ts"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"tsx": "^4.23.15"
|
|
27
|
+
},
|
|
22
28
|
"peerDependencies": {
|
|
23
|
-
"@earendil-works/pi-coding-agent": "
|
|
29
|
+
"@earendil-works/pi-coding-agent": "^1.1.0"
|
|
24
30
|
}
|
|
25
31
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { describeTask, settleNotification } from "../extensions/settle.ts";
|
|
4
|
+
|
|
5
|
+
describe("describeTask", () => {
|
|
6
|
+
it("takes the first line of the prompt", () => {
|
|
7
|
+
assert.equal(describeTask("Fix the build\nand then tag it"), "Fix the build");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("truncates a long first line", () => {
|
|
11
|
+
const long = "a".repeat(100);
|
|
12
|
+
const result = describeTask(long);
|
|
13
|
+
assert.equal(result, `${"a".repeat(37)}...`);
|
|
14
|
+
assert.equal(result?.length, 40);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("returns null for empty or non-string prompts", () => {
|
|
18
|
+
assert.equal(describeTask(""), null);
|
|
19
|
+
assert.equal(describeTask(" \n "), null);
|
|
20
|
+
assert.equal(describeTask(undefined), null);
|
|
21
|
+
assert.equal(describeTask({ text: "hi" }), null);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("settleNotification", () => {
|
|
26
|
+
it("reports a natural finish as complete", () => {
|
|
27
|
+
const note = settleNotification({ aborted: false }, "pi-plugins", "Run the tests");
|
|
28
|
+
assert.equal(note.emoji, "✅");
|
|
29
|
+
assert.match(note.title, /Complete/);
|
|
30
|
+
assert.equal(note.message, "Run the tests");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("reports a cancelled run as cancelled, not complete", () => {
|
|
34
|
+
const note = settleNotification({ aborted: true }, "pi-plugins", "Run the tests");
|
|
35
|
+
assert.equal(note.emoji, "⏹");
|
|
36
|
+
assert.match(note.title, /Cancelled/);
|
|
37
|
+
assert.match(note.message, /cancelled/i);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("falls back to the project name when no prompt was captured", () => {
|
|
41
|
+
const note = settleNotification({ aborted: false }, "pi-plugins", null);
|
|
42
|
+
assert.equal(note.message, "pi-plugins");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("treats an absent aborted flag as a natural finish", () => {
|
|
46
|
+
const note = settleNotification({}, "pi-plugins", "task");
|
|
47
|
+
assert.equal(note.emoji, "✅");
|
|
48
|
+
});
|
|
49
|
+
});
|