agent-noti 1.4.0 → 1.4.2
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 +5 -1
- package/bin/cli.mjs +19 -4
- package/bin/play.mjs +32 -3
- package/bin/stamp.mjs +13 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -102,9 +102,11 @@ agent-noti ntfy
|
|
|
102
102
|
This opens an interactive TUI where you can:
|
|
103
103
|
- Set your ntfy server and topic
|
|
104
104
|
- Toggle which events send push notifications (task complete / approval needed)
|
|
105
|
-
- Set priority level
|
|
105
|
+
- Set priority level and time threshold
|
|
106
106
|
- Send a test notification
|
|
107
107
|
|
|
108
|
+
The **threshold** setting (in minutes) skips push notifications for quick tasks. For example, set it to `5` and you'll only get pinged if the agent ran for at least 5 minutes. Set to `0` to always notify.
|
|
109
|
+
|
|
108
110
|
On first run you'll be prompted for a topic name. Subscribe to the same topic in the [ntfy app](https://ntfy.sh) (Android/iOS/web) to receive notifications.
|
|
109
111
|
|
|
110
112
|
### Quick test
|
|
@@ -123,6 +125,7 @@ Sends a test notification to your configured topic without opening the interacti
|
|
|
123
125
|
| `ntfy.server` | `https://ntfy.sh` | ntfy server URL |
|
|
124
126
|
| `ntfy.topic` | — | Your topic name (required) |
|
|
125
127
|
| `ntfy.priority` | `default` | `min`, `low`, `default`, `high`, `urgent` |
|
|
128
|
+
| `ntfy.threshold` | `0` | Min minutes before notifying (0 = always) |
|
|
126
129
|
| `ntfy.idle` | `true` | Notify on task complete |
|
|
127
130
|
| `ntfy.input` | `true` | Notify on approval needed |
|
|
128
131
|
|
|
@@ -141,6 +144,7 @@ All settings are stored in `~/.agent-noti/config.json`:
|
|
|
141
144
|
"server": "https://ntfy.sh",
|
|
142
145
|
"topic": "my-agent-alerts",
|
|
143
146
|
"priority": "default",
|
|
147
|
+
"threshold": 5,
|
|
144
148
|
"idle": true,
|
|
145
149
|
"input": true
|
|
146
150
|
}
|
package/bin/cli.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { homedir, platform } from "os";
|
|
|
9
9
|
|
|
10
10
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
11
|
const PLAY_SCRIPT = join(__dirname, "play.mjs");
|
|
12
|
+
const STAMP_SCRIPT = join(__dirname, "stamp.mjs");
|
|
12
13
|
const CODEX_NOTIFY_SCRIPT = join(__dirname, "codex-notify.mjs");
|
|
13
14
|
const SOUNDS_DIR = join(__dirname, "..", "sounds");
|
|
14
15
|
|
|
@@ -98,9 +99,11 @@ function writeConfig(config) {
|
|
|
98
99
|
function buildClaudeHooks() {
|
|
99
100
|
const idle = { type: "command", command: `node "${PLAY_SCRIPT}" idle` };
|
|
100
101
|
const input = { type: "command", command: `node "${PLAY_SCRIPT}" input` };
|
|
102
|
+
const stamp = { type: "command", command: `node "${STAMP_SCRIPT}"` };
|
|
101
103
|
return {
|
|
102
104
|
Stop: [{ hooks: [idle], metadata: { id: HOOK_ID } }],
|
|
103
105
|
PermissionRequest: [{ hooks: [input], metadata: { id: HOOK_ID } }],
|
|
106
|
+
UserPromptSubmit: [{ hooks: [stamp], metadata: { id: HOOK_ID } }],
|
|
104
107
|
};
|
|
105
108
|
}
|
|
106
109
|
|
|
@@ -631,6 +634,7 @@ function ntfy() {
|
|
|
631
634
|
server: setup.server,
|
|
632
635
|
topic: setup.topic,
|
|
633
636
|
priority: "default",
|
|
637
|
+
threshold: 0,
|
|
634
638
|
idle: true,
|
|
635
639
|
input: true,
|
|
636
640
|
};
|
|
@@ -641,17 +645,21 @@ function ntfy() {
|
|
|
641
645
|
const rows = ["idle", "input"];
|
|
642
646
|
let selected = 0;
|
|
643
647
|
let statusMsg = "";
|
|
644
|
-
const totalLines =
|
|
648
|
+
const totalLines = 13;
|
|
645
649
|
|
|
646
650
|
function render(firstTime) {
|
|
647
651
|
if (!firstTime) process.stdout.write(`\x1b[${totalLines}A`);
|
|
648
652
|
|
|
653
|
+
const thresh = ntfyConf.threshold ?? 0;
|
|
654
|
+
const threshLabel = thresh === 0 ? "off (always notify)" : `${thresh} min`;
|
|
655
|
+
|
|
649
656
|
process.stdout.write("\x1b[2K\n");
|
|
650
657
|
process.stdout.write(`\x1b[2K \x1b[1mntfy.sh push notifications\x1b[0m\n`);
|
|
651
658
|
process.stdout.write("\x1b[2K\n");
|
|
652
|
-
process.stdout.write(`\x1b[2K Server:
|
|
653
|
-
process.stdout.write(`\x1b[2K Topic:
|
|
654
|
-
process.stdout.write(`\x1b[2K Priority:
|
|
659
|
+
process.stdout.write(`\x1b[2K Server: \x1b[36m${ntfyConf.server || "https://ntfy.sh"}\x1b[0m\n`);
|
|
660
|
+
process.stdout.write(`\x1b[2K Topic: \x1b[36m${ntfyConf.topic}\x1b[0m\n`);
|
|
661
|
+
process.stdout.write(`\x1b[2K Priority: \x1b[36m${ntfyConf.priority || "default"}\x1b[0m\n`);
|
|
662
|
+
process.stdout.write(`\x1b[2K Threshold: \x1b[36m${threshLabel}\x1b[0m\n`);
|
|
655
663
|
process.stdout.write("\x1b[2K\n");
|
|
656
664
|
|
|
657
665
|
for (let i = 0; i < rows.length; i++) {
|
|
@@ -701,6 +709,13 @@ function ntfy() {
|
|
|
701
709
|
const newPri = await ntfyPrompt(`Priority [${NTFY_PRIORITIES.join("/")}] (${NTFY_PRIORITIES[priIdx]}): `);
|
|
702
710
|
if (newPri && NTFY_PRIORITIES.includes(newPri)) ntfyConf.priority = newPri;
|
|
703
711
|
|
|
712
|
+
const curThresh = ntfyConf.threshold ?? 0;
|
|
713
|
+
const newThresh = await ntfyPrompt(`Threshold in minutes, 0=off (${curThresh}): `);
|
|
714
|
+
if (newThresh !== "") {
|
|
715
|
+
const val = parseInt(newThresh, 10);
|
|
716
|
+
if (!isNaN(val) && val >= 0) ntfyConf.threshold = val;
|
|
717
|
+
}
|
|
718
|
+
|
|
704
719
|
// Re-enter raw mode and re-render
|
|
705
720
|
stdin.setRawMode(true);
|
|
706
721
|
stdin.resume();
|
package/bin/play.mjs
CHANGED
|
@@ -13,11 +13,14 @@ import { execFile, exec } from "child_process";
|
|
|
13
13
|
import { join, dirname } from "path";
|
|
14
14
|
import { fileURLToPath } from "url";
|
|
15
15
|
import { platform, homedir } from "os";
|
|
16
|
-
import { existsSync, readFileSync } from "fs";
|
|
16
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
17
17
|
|
|
18
18
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
19
|
const SOUNDS_DIR = join(__dirname, "..", "sounds");
|
|
20
|
-
const
|
|
20
|
+
const CONFIG_DIR = join(homedir(), ".agent-noti");
|
|
21
|
+
const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
22
|
+
const LAST_PROMPT_PATH = join(CONFIG_DIR, "last-prompt");
|
|
23
|
+
const LAST_EVENT_PATH = join(CONFIG_DIR, "last-event");
|
|
21
24
|
|
|
22
25
|
const EVENTS = ["idle", "input"];
|
|
23
26
|
|
|
@@ -107,7 +110,33 @@ async function sendNtfy(event, config) {
|
|
|
107
110
|
|
|
108
111
|
// Send ntfy push notification for actual events (not --file previews)
|
|
109
112
|
if (EVENTS.includes(arg)) {
|
|
110
|
-
|
|
113
|
+
const threshold = config.ntfy?.threshold ?? 0; // minutes, 0 = always
|
|
114
|
+
let shouldSend = true;
|
|
115
|
+
|
|
116
|
+
if (threshold > 0) {
|
|
117
|
+
// Claude Code: PromptSubmit hook writes last-prompt (exact task start)
|
|
118
|
+
// Codex fallback: use last-event (time since previous event)
|
|
119
|
+
const tsFile = existsSync(LAST_PROMPT_PATH) ? LAST_PROMPT_PATH : LAST_EVENT_PATH;
|
|
120
|
+
try {
|
|
121
|
+
if (existsSync(tsFile)) {
|
|
122
|
+
const started = parseInt(readFileSync(tsFile, "utf-8"), 10);
|
|
123
|
+
if (!isNaN(started)) {
|
|
124
|
+
const elapsed = (Date.now() - started) / 60000;
|
|
125
|
+
shouldSend = elapsed >= threshold;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} catch {}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (shouldSend) {
|
|
132
|
+
await sendNtfy(arg, config);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Write last-event timestamp (Codex fallback for threshold)
|
|
136
|
+
try {
|
|
137
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
138
|
+
writeFileSync(LAST_EVENT_PATH, String(Date.now()));
|
|
139
|
+
} catch {}
|
|
111
140
|
}
|
|
112
141
|
|
|
113
142
|
// Mute check (skip for --file, which is used by picker previews)
|
package/bin/stamp.mjs
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Records the current timestamp to ~/.agent-noti/last-prompt.
|
|
4
|
+
* Called by the PromptSubmit hook so play.mjs can measure task duration.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { writeFileSync, mkdirSync } from "fs";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import { homedir } from "os";
|
|
10
|
+
|
|
11
|
+
const dir = join(homedir(), ".agent-noti");
|
|
12
|
+
mkdirSync(dir, { recursive: true });
|
|
13
|
+
writeFileSync(join(dir, "last-prompt"), String(Date.now()));
|