agent-noti 1.4.0 → 1.4.1

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 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
+ PromptSubmit: [{ 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 = 12;
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: \x1b[36m${ntfyConf.server || "https://ntfy.sh"}\x1b[0m\n`);
653
- process.stdout.write(`\x1b[2K Topic: \x1b[36m${ntfyConf.topic}\x1b[0m\n`);
654
- process.stdout.write(`\x1b[2K Priority: \x1b[36m${ntfyConf.priority || "default"}\x1b[0m\n`);
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 CONFIG_PATH = join(homedir(), ".agent-noti", "config.json");
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
- await sendNtfy(arg, config);
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()));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-noti",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "Audio notifications for Claude Code & Codex — customizable sound themes",
5
5
  "bin": {
6
6
  "agent-noti": "./bin/cli.mjs"