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 +54 -11
- package/dist/config.d.ts +1 -0
- package/dist/config.js +24 -6
- package/dist/exec.d.ts +4 -0
- package/dist/exec.js +20 -0
- package/dist/index.js +58 -13
- package/dist/notify.d.ts +1 -0
- package/dist/notify.js +2 -1
- package/package.json +19 -4
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
|
|
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 `
|
|
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
|
-
| `
|
|
39
|
-
| `
|
|
40
|
-
| `
|
|
41
|
-
| `
|
|
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
|
|
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
|
|
66
|
-
export
|
|
67
|
-
export
|
|
68
|
-
export
|
|
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
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.
|
|
20
|
+
const topic = env.OPENCODE_NTFY_TOPIC;
|
|
4
21
|
if (!topic) {
|
|
5
|
-
throw new Error("
|
|
22
|
+
throw new Error("OPENCODE_NTFY_TOPIC environment variable is required");
|
|
6
23
|
}
|
|
7
|
-
const priority = env.
|
|
24
|
+
const priority = env.OPENCODE_NTFY_PRIORITY || "default";
|
|
8
25
|
if (!VALID_PRIORITIES.includes(priority)) {
|
|
9
|
-
throw new Error(`
|
|
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.
|
|
14
|
-
token: env.
|
|
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
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.
|
|
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
|
-
|
|
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: ${
|
|
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
|
-
?
|
|
53
|
+
? String(error.data.message)
|
|
25
54
|
: "";
|
|
26
|
-
|
|
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: ${
|
|
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
|
|
65
|
+
const permissionType = props.permission || "";
|
|
35
66
|
const patterns = props.patterns?.join(", ") || "";
|
|
36
|
-
const
|
|
37
|
-
|
|
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
|
|
75
|
+
await resolveAndSend($, config, "PERMISSION", vars, {
|
|
40
76
|
title: `${project} - Permission Requested`,
|
|
41
|
-
message: `Event: permission.asked\nProject: ${project}\nTime: ${
|
|
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
|
-
|
|
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: ${
|
|
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
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.
|
|
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
|
-
"
|
|
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
|
}
|