opencode-ntfy.sh 0.1.0 → 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 CHANGED
@@ -7,14 +7,16 @@ or desktop when it needs your attention.
7
7
 
8
8
  ## Notifications
9
9
 
10
- The plugin sends notifications for two events:
10
+ The plugin sends notifications for three events:
11
11
 
12
12
  - **Session Idle** -- The AI agent has finished its work and is waiting for
13
13
  input. Includes the project name and timestamp.
14
14
  - **Session Error** -- The session encountered an error. Includes the project
15
15
  name, timestamp, and error message (when available).
16
+ - **Permission Asked** -- The agent needs permission to perform an action.
17
+ Includes the project name, timestamp, permission type, and patterns.
16
18
 
17
- If `NTFY_TOPIC` is not set, the plugin does nothing.
19
+ If `OPENCODE_NTFY_TOPIC` is not set, the plugin does nothing.
18
20
 
19
21
  ## Install
20
22
 
@@ -35,10 +37,51 @@ Configuration is done through environment variables.
35
37
 
36
38
  | Variable | Required | Default | Description |
37
39
  |---|---|---|---|
38
- | `NTFY_TOPIC` | Yes | -- | The ntfy.sh topic to publish to. |
39
- | `NTFY_SERVER` | No | `https://ntfy.sh` | The ntfy server URL. Set this to use a self-hosted instance. |
40
- | `NTFY_TOKEN` | No | -- | Bearer token for authenticated topics. |
41
- | `NTFY_PRIORITY` | No | `default` | Notification priority. One of: `min`, `low`, `default`, `high`, `max`. |
40
+ | `OPENCODE_NTFY_TOPIC` | Yes | -- | The ntfy.sh topic to publish to. |
41
+ | `OPENCODE_NTFY_SERVER` | No | `https://ntfy.sh` | The ntfy server URL. Set this to use a self-hosted instance. |
42
+ | `OPENCODE_NTFY_TOKEN` | No | -- | Bearer token for authenticated topics. |
43
+ | `OPENCODE_NTFY_PRIORITY` | No | `default` | Global notification priority. One of: `min`, `low`, `default`, `high`, `max`. |
44
+
45
+ ### Custom Notification Commands
46
+
47
+ Each notification field (title, message, tags, priority) can be customized
48
+ per event by setting an environment variable containing a shell command. The
49
+ command's stdout (trimmed) is used as the field value. If the command is not
50
+ set or fails, the hardcoded default is used silently.
51
+
52
+ Before execution, template variables in the command string are substituted
53
+ with their values. Unset variables are substituted with empty strings.
54
+
55
+ #### Per-Event Environment Variables
56
+
57
+ | Event | Title | Message | Tags | Priority |
58
+ |---|---|---|---|---|
59
+ | `session.idle` | `OPENCODE_NTFY_SESSION_IDLE_TITLE_CMD` | `OPENCODE_NTFY_SESSION_IDLE_MESSAGE_CMD` | `OPENCODE_NTFY_SESSION_IDLE_TAGS_CMD` | `OPENCODE_NTFY_SESSION_IDLE_PRIORITY_CMD` |
60
+ | `session.error` | `OPENCODE_NTFY_SESSION_ERROR_TITLE_CMD` | `OPENCODE_NTFY_SESSION_ERROR_MESSAGE_CMD` | `OPENCODE_NTFY_SESSION_ERROR_TAGS_CMD` | `OPENCODE_NTFY_SESSION_ERROR_PRIORITY_CMD` |
61
+ | `permission.asked` | `OPENCODE_NTFY_PERMISSION_TITLE_CMD` | `OPENCODE_NTFY_PERMISSION_MESSAGE_CMD` | `OPENCODE_NTFY_PERMISSION_TAGS_CMD` | `OPENCODE_NTFY_PERMISSION_PRIORITY_CMD` |
62
+
63
+ #### Template Variables
64
+
65
+ | Variable | Available In | Description |
66
+ |---|---|---|
67
+ | `${event}` | All events | The event type string (e.g., `session.idle`) |
68
+ | `${time}` | All events | ISO 8601 timestamp |
69
+ | `${error}` | `session.error` only | The error message (empty string for other events) |
70
+ | `${permission_type}` | `permission.asked` only | The permission type (empty string for other events) |
71
+ | `${permission_patterns}` | `permission.asked` only | Comma-separated list of patterns (empty string for other events) |
72
+
73
+ #### Example
74
+
75
+ ```sh
76
+ # Custom title for idle notifications
77
+ export OPENCODE_NTFY_SESSION_IDLE_TITLE_CMD='echo "${event} is done"'
78
+
79
+ # Custom message with timestamp
80
+ export OPENCODE_NTFY_SESSION_ERROR_MESSAGE_CMD='echo "Error at ${time}: ${error}"'
81
+
82
+ # Override priority for permission requests
83
+ export OPENCODE_NTFY_PERMISSION_PRIORITY_CMD='echo "high"'
84
+ ```
42
85
 
43
86
  ### Subscribing to notifications
44
87
 
@@ -55,17 +98,17 @@ To receive notifications, subscribe to your topic using any
55
98
  ### Example
56
99
 
57
100
  ```sh
58
- export NTFY_TOPIC="my-opencode-notifications"
101
+ export OPENCODE_NTFY_TOPIC="my-opencode-notifications"
59
102
  opencode
60
103
  ```
61
104
 
62
105
  With authentication and a self-hosted server:
63
106
 
64
107
  ```sh
65
- export NTFY_TOPIC="my-opencode-notifications"
66
- export NTFY_SERVER="https://ntfy.example.com"
67
- export NTFY_TOKEN="tk_mytoken"
68
- export NTFY_PRIORITY="high"
108
+ export OPENCODE_NTFY_TOPIC="my-opencode-notifications"
109
+ export OPENCODE_NTFY_SERVER="https://ntfy.example.com"
110
+ export OPENCODE_NTFY_TOKEN="tk_mytoken"
111
+ export OPENCODE_NTFY_PRIORITY="high"
69
112
  opencode
70
113
  ```
71
114
 
package/dist/config.d.ts CHANGED
@@ -3,5 +3,6 @@ export interface NtfyConfig {
3
3
  server: string;
4
4
  token?: string;
5
5
  priority: string;
6
+ iconUrl: string;
6
7
  }
7
8
  export declare function loadConfig(env: Record<string, string | undefined>): NtfyConfig;
package/dist/config.js CHANGED
@@ -1,17 +1,35 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join, dirname } from "node:path";
3
+ import { fileURLToPath } from "node:url";
1
4
  const VALID_PRIORITIES = ["min", "low", "default", "high", "max"];
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
7
+ const PACKAGE_VERSION = pkg.version;
8
+ const BASE_ICON_URL = `https://raw.githubusercontent.com/lannuttia/opencode-ntfy.sh/v${PACKAGE_VERSION}/assets`;
9
+ function resolveIconUrl(env) {
10
+ const mode = env.OPENCODE_NTFY_ICON_MODE === "light" ? "light" : "dark";
11
+ if (mode === "light" && env.OPENCODE_NTFY_ICON_LIGHT) {
12
+ return env.OPENCODE_NTFY_ICON_LIGHT;
13
+ }
14
+ if (mode === "dark" && env.OPENCODE_NTFY_ICON_DARK) {
15
+ return env.OPENCODE_NTFY_ICON_DARK;
16
+ }
17
+ return `${BASE_ICON_URL}/opencode-icon-${mode}.png`;
18
+ }
2
19
  export function loadConfig(env) {
3
- const topic = env.NTFY_TOPIC;
20
+ const topic = env.OPENCODE_NTFY_TOPIC;
4
21
  if (!topic) {
5
- throw new Error("NTFY_TOPIC environment variable is required");
22
+ throw new Error("OPENCODE_NTFY_TOPIC environment variable is required");
6
23
  }
7
- const priority = env.NTFY_PRIORITY || "default";
24
+ const priority = env.OPENCODE_NTFY_PRIORITY || "default";
8
25
  if (!VALID_PRIORITIES.includes(priority)) {
9
- throw new Error(`NTFY_PRIORITY must be one of: ${VALID_PRIORITIES.join(", ")}`);
26
+ throw new Error(`OPENCODE_NTFY_PRIORITY must be one of: ${VALID_PRIORITIES.join(", ")}`);
10
27
  }
11
28
  return {
12
29
  topic,
13
- server: env.NTFY_SERVER || "https://ntfy.sh",
14
- token: env.NTFY_TOKEN,
30
+ server: env.OPENCODE_NTFY_SERVER || "https://ntfy.sh",
31
+ token: env.OPENCODE_NTFY_TOKEN,
15
32
  priority,
33
+ iconUrl: resolveIconUrl(env),
16
34
  };
17
35
  }
package/dist/exec.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import type { PluginInput } from "@opencode-ai/plugin";
2
+ type BunShell = PluginInput["$"];
3
+ export declare function resolveField($: BunShell, commandTemplate: string | undefined, variables: Record<string, string>, fallback: string): Promise<string>;
4
+ export {};
package/dist/exec.js ADDED
@@ -0,0 +1,20 @@
1
+ function substituteVariables(template, variables) {
2
+ return template.replace(/\$\{(\w+)\}/g, (_, name) => variables[name] ?? "");
3
+ }
4
+ export async function resolveField($, commandTemplate, variables, fallback) {
5
+ if (!commandTemplate) {
6
+ return fallback;
7
+ }
8
+ try {
9
+ const command = substituteVariables(commandTemplate, variables);
10
+ const result = await $ `${{ raw: command }}`.nothrow().quiet();
11
+ if (result.exitCode !== 0) {
12
+ return fallback;
13
+ }
14
+ const text = result.text().trim();
15
+ return text || fallback;
16
+ }
17
+ catch {
18
+ return fallback;
19
+ }
20
+ }
package/dist/index.js CHANGED
@@ -1,52 +1,97 @@
1
1
  import { loadConfig } from "./config.js";
2
2
  import { sendNotification } from "./notify.js";
3
+ import { resolveField } from "./exec.js";
3
4
  function getProjectName(directory) {
4
5
  return directory.split("/").pop() || directory;
5
6
  }
7
+ async function resolveAndSend($, config, envPrefix, vars, defaults) {
8
+ const titleCmd = process.env[`OPENCODE_NTFY_${envPrefix}_TITLE_CMD`];
9
+ const messageCmd = process.env[`OPENCODE_NTFY_${envPrefix}_MESSAGE_CMD`];
10
+ const tagsCmd = process.env[`OPENCODE_NTFY_${envPrefix}_TAGS_CMD`];
11
+ const priorityCmd = process.env[`OPENCODE_NTFY_${envPrefix}_PRIORITY_CMD`];
12
+ const title = await resolveField($, titleCmd, vars, defaults.title);
13
+ const message = await resolveField($, messageCmd, vars, defaults.message);
14
+ const tags = await resolveField($, tagsCmd, vars, defaults.tags);
15
+ const priority = await resolveField($, priorityCmd, vars, config.priority);
16
+ await sendNotification(config, {
17
+ title,
18
+ message,
19
+ tags,
20
+ priority: priorityCmd ? priority : undefined,
21
+ });
22
+ }
23
+ function buildVars(event, time, extra = {}) {
24
+ return {
25
+ event,
26
+ time,
27
+ error: extra.error ?? "",
28
+ permission_type: extra.permission_type ?? "",
29
+ permission_patterns: extra.permission_patterns ?? "",
30
+ };
31
+ }
6
32
  export const plugin = async (input) => {
7
- if (!process.env.NTFY_TOPIC) {
33
+ if (!process.env.OPENCODE_NTFY_TOPIC) {
8
34
  return {};
9
35
  }
10
36
  const config = loadConfig(process.env);
11
37
  const project = getProjectName(input.directory);
38
+ const $ = input.$;
12
39
  return {
13
40
  event: async ({ event }) => {
14
41
  if (event.type === "session.idle") {
15
- await sendNotification(config, {
42
+ const time = new Date().toISOString();
43
+ const vars = buildVars("session.idle", time);
44
+ await resolveAndSend($, config, "SESSION_IDLE", vars, {
16
45
  title: `${project} - Session Idle`,
17
- message: `Event: session.idle\nProject: ${project}\nTime: ${new Date().toISOString()}`,
46
+ message: `Event: session.idle\nProject: ${project}\nTime: ${time}`,
18
47
  tags: "hourglass_done",
19
48
  });
20
49
  }
21
50
  else if (event.type === "session.error") {
22
51
  const error = event.properties.error;
23
52
  const errorMsg = error && "data" in error && "message" in error.data
24
- ? `\nError: ${error.data.message}`
53
+ ? String(error.data.message)
25
54
  : "";
26
- await sendNotification(config, {
55
+ const time = new Date().toISOString();
56
+ const vars = buildVars("session.error", time, { error: errorMsg });
57
+ await resolveAndSend($, config, "SESSION_ERROR", vars, {
27
58
  title: `${project} - Session Error`,
28
- message: `Event: session.error\nProject: ${project}\nTime: ${new Date().toISOString()}${errorMsg}`,
59
+ message: `Event: session.error\nProject: ${project}\nTime: ${time}${errorMsg ? `\nError: ${errorMsg}` : ""}`,
29
60
  tags: "warning",
30
61
  });
31
62
  }
32
63
  else if (event.type === "permission.asked") {
33
64
  const props = event.properties;
34
- const permission = props.permission || "";
65
+ const permissionType = props.permission || "";
35
66
  const patterns = props.patterns?.join(", ") || "";
36
- const detail = permission
37
- ? `\nPermission: ${permission}${patterns ? ` (${patterns})` : ""}`
67
+ const time = new Date().toISOString();
68
+ const vars = buildVars("permission.asked", time, {
69
+ permission_type: permissionType,
70
+ permission_patterns: patterns,
71
+ });
72
+ const detail = permissionType
73
+ ? `\nPermission: ${permissionType}${patterns ? ` (${patterns})` : ""}`
38
74
  : "";
39
- await sendNotification(config, {
75
+ await resolveAndSend($, config, "PERMISSION", vars, {
40
76
  title: `${project} - Permission Requested`,
41
- message: `Event: permission.asked\nProject: ${project}\nTime: ${new Date().toISOString()}${detail}`,
77
+ message: `Event: permission.asked\nProject: ${project}\nTime: ${time}${detail}`,
42
78
  tags: "lock",
43
79
  });
44
80
  }
45
81
  },
46
82
  "permission.ask": async (permission) => {
47
- await sendNotification(config, {
83
+ const time = new Date().toISOString();
84
+ const permissionType = permission.type || "";
85
+ const patterns = Array.isArray(permission.pattern)
86
+ ? permission.pattern.join(", ")
87
+ : permission.pattern || "";
88
+ const vars = buildVars("permission.asked", time, {
89
+ permission_type: permissionType,
90
+ permission_patterns: patterns,
91
+ });
92
+ await resolveAndSend($, config, "PERMISSION", vars, {
48
93
  title: `${project} - Permission Requested`,
49
- message: `Event: permission.asked\nProject: ${project}\nTime: ${new Date().toISOString()}\nPermission: ${permission.title}`,
94
+ message: `Event: permission.asked\nProject: ${project}\nTime: ${time}\nPermission: ${permission.title}`,
50
95
  tags: "lock",
51
96
  });
52
97
  },
package/dist/notify.d.ts CHANGED
@@ -3,5 +3,6 @@ export interface NotificationPayload {
3
3
  title: string;
4
4
  message: string;
5
5
  tags: string;
6
+ priority?: string;
6
7
  }
7
8
  export declare function sendNotification(config: NtfyConfig, payload: NotificationPayload): Promise<void>;
package/dist/notify.js CHANGED
@@ -2,8 +2,9 @@ export async function sendNotification(config, payload) {
2
2
  const url = `${config.server}/${config.topic}`;
3
3
  const headers = {
4
4
  Title: payload.title,
5
- Priority: config.priority,
5
+ Priority: payload.priority ?? config.priority,
6
6
  Tags: payload.tags,
7
+ "X-Icon": config.iconUrl,
7
8
  };
8
9
  if (config.token) {
9
10
  headers.Authorization = `Bearer ${config.token}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-ntfy.sh",
3
- "version": "0.1.0",
3
+ "version": "0.1.7",
4
4
  "description": "OpenCode plugin that sends push notifications via ntfy.sh",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -13,6 +13,7 @@
13
13
  },
14
14
  "scripts": {
15
15
  "build": "tsc",
16
+ "prepublishOnly": "npm run build",
16
17
  "test": "vitest run",
17
18
  "test:watch": "vitest"
18
19
  },
@@ -21,11 +22,25 @@
21
22
  "@types/node": "^25.2.3",
22
23
  "msw": "^2.12.10",
23
24
  "typescript": "^5.7.0",
24
- "vitest": "^3.0.0",
25
- "yaml": "^2.8.2"
25
+ "vitest": "^3.0.0"
26
26
  },
27
27
  "files": [
28
28
  "dist"
29
29
  ],
30
- "license": "MIT"
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/lannuttia/opencode-ntfy.sh.git"
33
+ },
34
+ "license": "MIT",
35
+ "keywords": [
36
+ "opencode",
37
+ "opencode-plugin",
38
+ "ntfy",
39
+ "ntfy.sh",
40
+ "notifications",
41
+ "push-notifications",
42
+ "ai",
43
+ "coding-assistant",
44
+ "plugin"
45
+ ]
31
46
  }