opencode-termux-notify 0.1.0
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/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +132 -0
- package/assets/audio/bip-bop-01.mp3 +0 -0
- package/assets/audio/bip-bop-03.mp3 +0 -0
- package/assets/audio/nope-03.mp3 +0 -0
- package/assets/audio/staplebops-06.mp3 +0 -0
- package/assets/audio/yup-01.mp3 +0 -0
- package/dist/audio.d.ts +4 -0
- package/dist/audio.d.ts.map +1 -0
- package/dist/audio.js +55 -0
- package/dist/audio.js.map +1 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +47 -0
- package/dist/config.js.map +1 -0
- package/dist/constants.d.ts +17 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +40 -0
- package/dist/constants.js.map +1 -0
- package/dist/dedup.d.ts +3 -0
- package/dist/dedup.d.ts.map +1 -0
- package/dist/dedup.js +57 -0
- package/dist/dedup.js.map +1 -0
- package/dist/env.d.ts +2 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +4 -0
- package/dist/env.js.map +1 -0
- package/dist/events.d.ts +9 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +52 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +119 -0
- package/dist/index.js.map +1 -0
- package/dist/notify.d.ts +3 -0
- package/dist/notify.d.ts.map +1 -0
- package/dist/notify.js +23 -0
- package/dist/notify.js.map +1 -0
- package/dist/types.d.ts +37 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/v1.d.ts +21 -0
- package/dist/v1.d.ts.map +1 -0
- package/dist/v1.js +133 -0
- package/dist/v1.js.map +1 -0
- package/index.js +4 -0
- package/package.json +80 -0
- package/src/audio.ts +50 -0
- package/src/config.ts +50 -0
- package/src/constants.ts +43 -0
- package/src/dedup.ts +58 -0
- package/src/env.ts +3 -0
- package/src/events.ts +58 -0
- package/src/index.ts +127 -0
- package/src/notify.ts +25 -0
- package/src/types.ts +39 -0
- package/src/v1.ts +140 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opencode-termux-notify — Termux-native notifications for OpenCode (V2)
|
|
3
|
+
*
|
|
4
|
+
* 6 builtin attention sounds:
|
|
5
|
+
* default → bip-bop-01.mp3
|
|
6
|
+
* question → bip-bop-03.mp3
|
|
7
|
+
* permission → staplebops-06.mp3
|
|
8
|
+
* error → nope-03.mp3
|
|
9
|
+
* done → bip-bop-01.mp3
|
|
10
|
+
* subagent_done → yup-01.mp3
|
|
11
|
+
*
|
|
12
|
+
* Audio bundled at assets/audio/*.mp3, played via `termux-media-player play <file>`
|
|
13
|
+
* (termux-notification --sound is boolean only — no file arg)
|
|
14
|
+
*/
|
|
15
|
+
import { define } from "@opencode-ai/plugin/v2/promise";
|
|
16
|
+
import { playAudio } from "./audio.js";
|
|
17
|
+
import { getEnabledKinds, getContent, getTitle, resolveOpts } from "./config.js";
|
|
18
|
+
import { shouldNotifyShared } from "./dedup.js";
|
|
19
|
+
import { isTermuxEnvironment } from "./env.js";
|
|
20
|
+
import { classifyEvent } from "./events.js";
|
|
21
|
+
import { notify } from "./notify.js";
|
|
22
|
+
export { DEFAULTS, PRIORITY_BY_KIND, SOUND_FILES, VIBRATE_PATTERNS } from "./constants.js";
|
|
23
|
+
export default define({
|
|
24
|
+
id: "termux-notify",
|
|
25
|
+
setup: async (ctx) => {
|
|
26
|
+
const userOpts = (ctx.options ?? {});
|
|
27
|
+
const opts = resolveOpts(userOpts);
|
|
28
|
+
const enabledKinds = getEnabledKinds(userOpts);
|
|
29
|
+
if (opts.requireTermux && !isTermuxEnvironment()) {
|
|
30
|
+
console.warn("[termux-notify] Not in Termux (TERMUX_VERSION/PREFIX missing) — plugin will still listen but notifications may fail. Set { requireTermux:false } to silence this.");
|
|
31
|
+
}
|
|
32
|
+
if (!ctx?.event?.subscribe) {
|
|
33
|
+
console.error("[termux-notify] ctx.event.subscribe not available — check OpenCode version >= 1.18");
|
|
34
|
+
return () => { };
|
|
35
|
+
}
|
|
36
|
+
const controller = new AbortController();
|
|
37
|
+
const events = ctx.event.subscribe({ signal: controller.signal });
|
|
38
|
+
const active = new Set();
|
|
39
|
+
const errored = new Set();
|
|
40
|
+
void (async () => {
|
|
41
|
+
try {
|
|
42
|
+
for await (const event of events) {
|
|
43
|
+
if (!event?.type)
|
|
44
|
+
continue;
|
|
45
|
+
if (event.type === "session.status") {
|
|
46
|
+
const sid = event.properties?.sessionID;
|
|
47
|
+
if (!sid)
|
|
48
|
+
continue;
|
|
49
|
+
const st = event.properties?.status?.type;
|
|
50
|
+
if (st === "busy" || st === "retry") {
|
|
51
|
+
active.add(sid);
|
|
52
|
+
errored.delete(sid);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (st !== "idle")
|
|
56
|
+
continue;
|
|
57
|
+
if (!active.has(sid))
|
|
58
|
+
continue;
|
|
59
|
+
active.delete(sid);
|
|
60
|
+
if (errored.has(sid)) {
|
|
61
|
+
errored.delete(sid);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (event.type === "session.error") {
|
|
66
|
+
const sid = event.properties?.sessionID;
|
|
67
|
+
if (sid && active.has(sid))
|
|
68
|
+
errored.add(sid);
|
|
69
|
+
}
|
|
70
|
+
const classified = await classifyEvent(event, ctx);
|
|
71
|
+
if (!classified)
|
|
72
|
+
continue;
|
|
73
|
+
const { kind, sound, sessionID } = classified;
|
|
74
|
+
if (!enabledKinds.has(kind) && !enabledKinds.has(sound))
|
|
75
|
+
continue;
|
|
76
|
+
const evtId = classified.evtId;
|
|
77
|
+
const sessionKey = `${sessionID}:${kind}`;
|
|
78
|
+
const ok = await shouldNotifyShared(evtId, sessionKey, opts);
|
|
79
|
+
if (!ok)
|
|
80
|
+
continue;
|
|
81
|
+
const nid = `opencode-${sessionID}-${kind}`;
|
|
82
|
+
let sessionName = sessionID.slice(0, 8);
|
|
83
|
+
try {
|
|
84
|
+
if (ctx?.session?.get) {
|
|
85
|
+
const info = await ctx.session.get({ sessionID });
|
|
86
|
+
const raw = info?.info || info?.data || info;
|
|
87
|
+
const candidate = raw?.title || raw?.slug || raw?.summary?.title || raw?.id;
|
|
88
|
+
if (candidate && typeof candidate === "string" && candidate.trim()) {
|
|
89
|
+
sessionName = candidate.trim().slice(0, 40);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch { }
|
|
94
|
+
const title = getTitle(kind, sessionName, userOpts);
|
|
95
|
+
const content = getContent(kind, userOpts);
|
|
96
|
+
const notifySubagents = userOpts.notifySubagents !== false;
|
|
97
|
+
if (kind === "subagent_done" && !notifySubagents) {
|
|
98
|
+
await playAudio(sound, opts);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
await notify(title, content, nid, sound, opts);
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
106
|
+
console.error(`[termux-notify] Failed to send ${kind} notification:`, msg);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
const err = e;
|
|
112
|
+
if (err?.name !== "AbortError")
|
|
113
|
+
console.error("[termux-notify] event loop error:", e);
|
|
114
|
+
}
|
|
115
|
+
})();
|
|
116
|
+
return () => controller.abort();
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/notify.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notify.d.ts","sourceRoot":"","sources":["../src/notify.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE9C,wBAAsB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAmB9H"}
|
package/dist/notify.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { VIBRATE_PATTERNS, PRIORITY_BY_KIND } from "./constants.js";
|
|
3
|
+
import { playAudio } from "./audio.js";
|
|
4
|
+
export async function notify(title, content, nid, soundName, opts) {
|
|
5
|
+
const vibrate = opts.vibrate ? VIBRATE_PATTERNS[soundName] || VIBRATE_PATTERNS.default : undefined;
|
|
6
|
+
const priority = opts.priority || PRIORITY_BY_KIND[soundName] || "high";
|
|
7
|
+
const baseArgs = ["--id", nid, "--title", title, "--content", content, "--priority", priority];
|
|
8
|
+
if (vibrate)
|
|
9
|
+
baseArgs.push("--vibrate", vibrate);
|
|
10
|
+
const notifPromise = new Promise((resolve, reject) => {
|
|
11
|
+
const p = spawn(opts.bin, baseArgs, { stdio: "ignore" });
|
|
12
|
+
p.on("error", reject);
|
|
13
|
+
p.on("close", (code) => (code === 0 ? resolve() : reject(new Error(`termux-notification exit ${code}`))));
|
|
14
|
+
});
|
|
15
|
+
const audioPromise = playAudio(soundName, opts);
|
|
16
|
+
try {
|
|
17
|
+
await notifPromise;
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
void audioPromise.catch(() => { });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=notify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notify.js","sourceRoot":"","sources":["../src/notify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAA;AAC1C,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AACnE,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAGtC,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,KAAa,EAAE,OAAe,EAAE,GAAW,EAAE,SAAiB,EAAE,IAAkB;IAC7G,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,gBAAgB,CAAC,SAAS,CAAC,IAAI,gBAAgB,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAA;IAClG,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,gBAAgB,CAAC,SAAS,CAAC,IAAI,MAAM,CAAA;IACvE,MAAM,QAAQ,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,YAAY,EAAE,QAAQ,CAAC,CAAA;IAC9F,IAAI,OAAO;QAAE,QAAQ,CAAC,IAAI,CAAC,WAAW,EAAE,OAAO,CAAC,CAAA;IAEhD,MAAM,YAAY,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACzD,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAA;QACxD,CAAC,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;QACrB,CAAC,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAmB,EAAE,EAAE,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,4BAA4B,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;IAC1H,CAAC,CAAC,CAAA;IAEF,MAAM,YAAY,GAAG,SAAS,CAAC,SAAS,EAAE,IAAI,CAAC,CAAA;IAE/C,IAAI,CAAC;QACH,MAAM,YAAY,CAAA;IACpB,CAAC;YAAS,CAAC;QACT,KAAK,YAAY,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;IACnC,CAAC;AACH,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { DEFAULTS } from "./constants.js";
|
|
2
|
+
export interface TermuxNotifyOptions {
|
|
3
|
+
bin?: string;
|
|
4
|
+
mediaBin?: string;
|
|
5
|
+
sharedPath?: string;
|
|
6
|
+
seenTTL?: number;
|
|
7
|
+
sessionCooldown?: number;
|
|
8
|
+
globalCooldown?: number;
|
|
9
|
+
sound?: boolean;
|
|
10
|
+
playSound?: boolean;
|
|
11
|
+
vibrate?: boolean;
|
|
12
|
+
requireTermux?: boolean;
|
|
13
|
+
priority?: "high" | "low" | "default" | "max" | (string & {});
|
|
14
|
+
kinds?: Array<"default" | "question" | "permission" | "error" | "done" | "subagent_done" | "idle">;
|
|
15
|
+
notifySubagents?: boolean;
|
|
16
|
+
title_default?: string;
|
|
17
|
+
title_done?: string;
|
|
18
|
+
title_subagent_done?: string;
|
|
19
|
+
title_question?: string;
|
|
20
|
+
title_permission?: string;
|
|
21
|
+
title_error?: string;
|
|
22
|
+
content_default?: string;
|
|
23
|
+
content_done?: string;
|
|
24
|
+
content_subagent_done?: string;
|
|
25
|
+
content_question?: string;
|
|
26
|
+
content_permission?: string;
|
|
27
|
+
content_error?: string;
|
|
28
|
+
vibrateIdle?: string;
|
|
29
|
+
vibrateError?: string;
|
|
30
|
+
titleIdle?: string;
|
|
31
|
+
titleError?: string;
|
|
32
|
+
contentIdle?: string;
|
|
33
|
+
contentError?: string;
|
|
34
|
+
}
|
|
35
|
+
export type NotifyKind = "default" | "question" | "permission" | "error" | "done" | "subagent_done";
|
|
36
|
+
export type ResolvedOpts = typeof DEFAULTS;
|
|
37
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAE9C,MAAM,WAAW,mBAAmB;IAClC,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,QAAQ,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,SAAS,GAAG,KAAK,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAA;IAC7D,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,GAAG,UAAU,GAAG,YAAY,GAAG,OAAO,GAAG,MAAM,GAAG,eAAe,GAAG,MAAM,CAAC,CAAA;IAClG,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAA;IAEtB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,UAAU,GAAG,YAAY,GAAG,OAAO,GAAG,MAAM,GAAG,eAAe,CAAA;AACnG,MAAM,MAAM,YAAY,GAAG,OAAO,QAAQ,CAAA"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/dist/v1.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opencode-termux-notify — Termux-native notifications for OpenCode (V1)
|
|
3
|
+
*
|
|
4
|
+
* V1 plugin API: export const TermuxNotify: Plugin = async (input, options) => ({ event })
|
|
5
|
+
* Loaded via `plugin: ["opencode-termux-notify"]` or `["opencode-termux-notify/v1"]`
|
|
6
|
+
* V2 is available at `opencode-termux-notify/v2`
|
|
7
|
+
*
|
|
8
|
+
* 6 builtin attention sounds:
|
|
9
|
+
* default → bip-bop-01.mp3
|
|
10
|
+
* question → bip-bop-03.mp3
|
|
11
|
+
* permission → staplebops-06.mp3
|
|
12
|
+
* error → nope-03.mp3
|
|
13
|
+
* done → bip-bop-01.mp3
|
|
14
|
+
* subagent_done → yup-01.mp3
|
|
15
|
+
*/
|
|
16
|
+
import type { Plugin } from "@opencode-ai/plugin";
|
|
17
|
+
export type { TermuxNotifyOptions } from "./types.js";
|
|
18
|
+
export { DEFAULTS, PRIORITY_BY_KIND, SOUND_FILES, VIBRATE_PATTERNS } from "./constants.js";
|
|
19
|
+
export declare const TermuxNotify: Plugin;
|
|
20
|
+
export default TermuxNotify;
|
|
21
|
+
//# sourceMappingURL=v1.d.ts.map
|
package/dist/v1.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"v1.d.ts","sourceRoot":"","sources":["../src/v1.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AASjD,YAAY,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AACrD,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AAE1F,eAAO,MAAM,YAAY,EAAE,MA6G1B,CAAA;eAEc,YAAY"}
|
package/dist/v1.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opencode-termux-notify — Termux-native notifications for OpenCode (V1)
|
|
3
|
+
*
|
|
4
|
+
* V1 plugin API: export const TermuxNotify: Plugin = async (input, options) => ({ event })
|
|
5
|
+
* Loaded via `plugin: ["opencode-termux-notify"]` or `["opencode-termux-notify/v1"]`
|
|
6
|
+
* V2 is available at `opencode-termux-notify/v2`
|
|
7
|
+
*
|
|
8
|
+
* 6 builtin attention sounds:
|
|
9
|
+
* default → bip-bop-01.mp3
|
|
10
|
+
* question → bip-bop-03.mp3
|
|
11
|
+
* permission → staplebops-06.mp3
|
|
12
|
+
* error → nope-03.mp3
|
|
13
|
+
* done → bip-bop-01.mp3
|
|
14
|
+
* subagent_done → yup-01.mp3
|
|
15
|
+
*/
|
|
16
|
+
import { playAudio } from "./audio.js";
|
|
17
|
+
import { getEnabledKinds, getContent, getTitle, resolveOpts } from "./config.js";
|
|
18
|
+
import { shouldNotifyShared } from "./dedup.js";
|
|
19
|
+
import { isTermuxEnvironment } from "./env.js";
|
|
20
|
+
import { classifyEvent } from "./events.js";
|
|
21
|
+
import { notify } from "./notify.js";
|
|
22
|
+
export { DEFAULTS, PRIORITY_BY_KIND, SOUND_FILES, VIBRATE_PATTERNS } from "./constants.js";
|
|
23
|
+
export const TermuxNotify = async (input, options) => {
|
|
24
|
+
const userOpts = (options ?? {});
|
|
25
|
+
const opts = resolveOpts(userOpts);
|
|
26
|
+
const enabledKinds = getEnabledKinds(userOpts);
|
|
27
|
+
if (opts.requireTermux && !isTermuxEnvironment()) {
|
|
28
|
+
console.warn("[termux-notify] Not in Termux (TERMUX_VERSION/PREFIX missing) — plugin will still listen but notifications may fail. Set { requireTermux:false } to silence this.");
|
|
29
|
+
}
|
|
30
|
+
const active = new Set();
|
|
31
|
+
const errored = new Set();
|
|
32
|
+
// Adapter so classifyEvent and session-title logic can use V2-style ctx.session.get
|
|
33
|
+
// V1 client uses client.session.get({ path: { id } }) -> { data: Session }
|
|
34
|
+
const ctxAdapter = {
|
|
35
|
+
session: {
|
|
36
|
+
get: async ({ sessionID }) => {
|
|
37
|
+
try {
|
|
38
|
+
const client = input.client;
|
|
39
|
+
if (!client?.session?.get)
|
|
40
|
+
return undefined;
|
|
41
|
+
let raw;
|
|
42
|
+
// try V2-style first, then V1-style
|
|
43
|
+
try {
|
|
44
|
+
raw = await client.session.get({ sessionID });
|
|
45
|
+
if (raw && (raw.info || raw.data || raw.title || raw.id))
|
|
46
|
+
return raw;
|
|
47
|
+
}
|
|
48
|
+
catch { }
|
|
49
|
+
try {
|
|
50
|
+
raw = await client.session.get({ path: { id: sessionID } });
|
|
51
|
+
}
|
|
52
|
+
catch { }
|
|
53
|
+
// SDK gen returns { data, ... } or direct
|
|
54
|
+
const data = raw?.data ?? raw;
|
|
55
|
+
return data;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
return {
|
|
64
|
+
event: async ({ event }) => {
|
|
65
|
+
if (!event?.type)
|
|
66
|
+
return;
|
|
67
|
+
// replicate V2 status tracking to avoid duplicate idle notifications after errors
|
|
68
|
+
if (event.type === "session.status") {
|
|
69
|
+
const sid = event.properties?.sessionID;
|
|
70
|
+
if (!sid)
|
|
71
|
+
return;
|
|
72
|
+
const st = event.properties?.status?.type;
|
|
73
|
+
if (st === "busy" || st === "retry") {
|
|
74
|
+
active.add(sid);
|
|
75
|
+
errored.delete(sid);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (st !== "idle")
|
|
79
|
+
return;
|
|
80
|
+
if (!active.has(sid))
|
|
81
|
+
return;
|
|
82
|
+
active.delete(sid);
|
|
83
|
+
if (errored.has(sid)) {
|
|
84
|
+
errored.delete(sid);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (event.type === "session.error") {
|
|
89
|
+
const sid = event.properties?.sessionID;
|
|
90
|
+
if (sid && active.has(sid))
|
|
91
|
+
errored.add(sid);
|
|
92
|
+
}
|
|
93
|
+
const classified = await classifyEvent(event, ctxAdapter);
|
|
94
|
+
if (!classified)
|
|
95
|
+
return;
|
|
96
|
+
const { kind, sound, sessionID } = classified;
|
|
97
|
+
if (!enabledKinds.has(kind) && !enabledKinds.has(sound))
|
|
98
|
+
return;
|
|
99
|
+
const evtId = classified.evtId;
|
|
100
|
+
const sessionKey = `${sessionID}:${kind}`;
|
|
101
|
+
const ok = await shouldNotifyShared(evtId, sessionKey, opts);
|
|
102
|
+
if (!ok)
|
|
103
|
+
return;
|
|
104
|
+
const nid = `opencode-${sessionID}-${kind}`;
|
|
105
|
+
let sessionName = sessionID.slice(0, 8);
|
|
106
|
+
try {
|
|
107
|
+
const info = await ctxAdapter.session.get({ sessionID });
|
|
108
|
+
const raw = info?.info || info?.data || info;
|
|
109
|
+
const candidate = raw?.title || raw?.slug || raw?.summary?.title || raw?.id;
|
|
110
|
+
if (candidate && typeof candidate === "string" && candidate.trim()) {
|
|
111
|
+
sessionName = candidate.trim().slice(0, 40);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch { }
|
|
115
|
+
const title = getTitle(kind, sessionName, userOpts);
|
|
116
|
+
const content = getContent(kind, userOpts);
|
|
117
|
+
const notifySubagents = userOpts.notifySubagents !== false;
|
|
118
|
+
if (kind === "subagent_done" && !notifySubagents) {
|
|
119
|
+
await playAudio(sound, opts);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
await notify(title, content, nid, sound, opts);
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
127
|
+
console.error(`[termux-notify] Failed to send ${kind} notification:`, msg);
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
export default TermuxNotify;
|
|
133
|
+
//# sourceMappingURL=v1.js.map
|
package/dist/v1.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"v1.js","sourceRoot":"","sources":["../src/v1.ts"],"names":[],"mappings":"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"}
|
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/package.json",
|
|
3
|
+
"name": "opencode-termux-notify",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Termux-native notifications for OpenCode — 6 attention sounds (default/question/permission/error/done/subagent_done) via termux-notification + termux-media-player, with cross-instance deduplication.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"opencode",
|
|
9
|
+
"opencode-plugin",
|
|
10
|
+
"termux",
|
|
11
|
+
"android",
|
|
12
|
+
"notification",
|
|
13
|
+
"termux-notification",
|
|
14
|
+
"ai",
|
|
15
|
+
"agent"
|
|
16
|
+
],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "Victozee26 <https://github.com/Victozee26>",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/Victozee26/opencode-termux-notify.git"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/Victozee26/opencode-termux-notify/issues"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/Victozee26/opencode-termux-notify#readme",
|
|
27
|
+
"funding": {
|
|
28
|
+
"type": "github",
|
|
29
|
+
"url": "https://github.com/sponsors/Victozee26"
|
|
30
|
+
},
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"import": "./dist/v1.js",
|
|
34
|
+
"types": "./dist/v1.d.ts"
|
|
35
|
+
},
|
|
36
|
+
"./v1": {
|
|
37
|
+
"import": "./dist/v1.js",
|
|
38
|
+
"types": "./dist/v1.d.ts"
|
|
39
|
+
},
|
|
40
|
+
"./v2": {
|
|
41
|
+
"import": "./dist/index.js",
|
|
42
|
+
"types": "./dist/index.d.ts"
|
|
43
|
+
},
|
|
44
|
+
"./package.json": "./package.json"
|
|
45
|
+
},
|
|
46
|
+
"main": "./dist/v1.js",
|
|
47
|
+
"types": "./dist/v1.d.ts",
|
|
48
|
+
"files": [
|
|
49
|
+
"dist/",
|
|
50
|
+
"src/",
|
|
51
|
+
"assets/",
|
|
52
|
+
"index.js",
|
|
53
|
+
"README.md",
|
|
54
|
+
"LICENSE",
|
|
55
|
+
"CHANGELOG.md"
|
|
56
|
+
],
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": ">=20"
|
|
59
|
+
},
|
|
60
|
+
"publishConfig": {
|
|
61
|
+
"access": "public"
|
|
62
|
+
},
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"@opencode-ai/plugin": "^1.18.27"
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"@types/node": "^26.4.1",
|
|
68
|
+
"typescript": "^7.0.2"
|
|
69
|
+
},
|
|
70
|
+
"scripts": {
|
|
71
|
+
"typecheck": "tsc --noEmit",
|
|
72
|
+
"build": "tsc --project tsconfig.json 2>&1 | head -n 50; echo \"built to dist/\"",
|
|
73
|
+
"prepublishOnly": "npm run typecheck && npm run build",
|
|
74
|
+
"pack:dry": "npm pack --dry-run",
|
|
75
|
+
"test": "node --test src/index.test.js 2>&1 | head -n 100; echo \"no tests yet — see CONTRIBUTING\""
|
|
76
|
+
},
|
|
77
|
+
"opencode": {
|
|
78
|
+
"plugin": true
|
|
79
|
+
}
|
|
80
|
+
}
|
package/src/audio.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { existsSync } from "node:fs"
|
|
2
|
+
import { spawn } from "node:child_process"
|
|
3
|
+
import { join, dirname } from "node:path"
|
|
4
|
+
import { fileURLToPath } from "node:url"
|
|
5
|
+
import { SOUND_FILES } from "./constants.js"
|
|
6
|
+
import type { ResolvedOpts } from "./types.js"
|
|
7
|
+
|
|
8
|
+
export function resolveAudioPath(file: string): string {
|
|
9
|
+
try {
|
|
10
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
11
|
+
const candidates = [
|
|
12
|
+
join(here, "../assets/audio", file),
|
|
13
|
+
join(here, "assets/audio", file),
|
|
14
|
+
join(here, "../../assets/audio", file),
|
|
15
|
+
join(here, "../src/assets/audio", file),
|
|
16
|
+
join(process.cwd(), "assets/audio", file),
|
|
17
|
+
join(process.cwd(), "src/assets/audio", file),
|
|
18
|
+
]
|
|
19
|
+
for (const p of candidates) if (existsSync(p)) return p
|
|
20
|
+
return candidates[0]!
|
|
21
|
+
} catch {
|
|
22
|
+
return file
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function playAudio(soundName: string, opts: ResolvedOpts): Promise<void> {
|
|
27
|
+
if (!opts.playSound) return
|
|
28
|
+
const file = SOUND_FILES[soundName]
|
|
29
|
+
if (!file) return
|
|
30
|
+
const path = resolveAudioPath(file)
|
|
31
|
+
if (!existsSync(path)) {
|
|
32
|
+
console.warn(`[termux-notify] audio file not found: ${path} for ${soundName}`)
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
const bin = existsSync(opts.mediaBin) ? opts.mediaBin : "termux-media-player"
|
|
36
|
+
await new Promise<void>((resolve) => {
|
|
37
|
+
const p = spawn(bin, ["play", path], { stdio: "ignore" })
|
|
38
|
+
p.on("error", (err) => {
|
|
39
|
+
console.error(`[termux-notify] media play failed for ${soundName}:`, (err as Error).message)
|
|
40
|
+
resolve()
|
|
41
|
+
})
|
|
42
|
+
p.on("close", () => resolve())
|
|
43
|
+
setTimeout(() => {
|
|
44
|
+
try {
|
|
45
|
+
;(p as any).unref?.()
|
|
46
|
+
} catch {}
|
|
47
|
+
resolve()
|
|
48
|
+
}, 3000)
|
|
49
|
+
})
|
|
50
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { DEFAULTS } from "./constants.js"
|
|
2
|
+
import type { ResolvedOpts, TermuxNotifyOptions } from "./types.js"
|
|
3
|
+
|
|
4
|
+
export function resolveOpts(userOpts: TermuxNotifyOptions): ResolvedOpts {
|
|
5
|
+
const opts: ResolvedOpts = { ...DEFAULTS, ...userOpts } as ResolvedOpts
|
|
6
|
+
if (typeof userOpts.sharedPath === "string" && userOpts.sharedPath.trim()) opts.sharedPath = userOpts.sharedPath
|
|
7
|
+
if (typeof (userOpts as any).bin === "string" && (userOpts as any).bin.trim()) opts.bin = (userOpts as any).bin
|
|
8
|
+
if (typeof userOpts.mediaBin === "string" && userOpts.mediaBin.trim()) opts.mediaBin = userOpts.mediaBin
|
|
9
|
+
return opts
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getEnabledKinds(userOpts: TermuxNotifyOptions): Set<string> {
|
|
13
|
+
const enabledKinds = new Set<string>(
|
|
14
|
+
Array.isArray(userOpts.kinds) && userOpts.kinds.length
|
|
15
|
+
? (userOpts.kinds as string[])
|
|
16
|
+
: ["default", "question", "permission", "error", "done", "subagent_done"],
|
|
17
|
+
)
|
|
18
|
+
if (enabledKinds.has("idle")) {
|
|
19
|
+
enabledKinds.delete("idle")
|
|
20
|
+
enabledKinds.add("done")
|
|
21
|
+
enabledKinds.add("default")
|
|
22
|
+
}
|
|
23
|
+
return enabledKinds
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getTitle(kind: string, sessionName: string, userOpts: TermuxNotifyOptions): string {
|
|
27
|
+
const titles: Record<string, string> = {
|
|
28
|
+
default: `OpenCode — ${sessionName}`,
|
|
29
|
+
done: `OpenCode — ${sessionName}`,
|
|
30
|
+
subagent_done: `OpenCode (subagent) — ${sessionName}`,
|
|
31
|
+
question: `OpenCode question — ${sessionName}`,
|
|
32
|
+
permission: `OpenCode permission — ${sessionName}`,
|
|
33
|
+
error: `OpenCode error — ${sessionName}`,
|
|
34
|
+
}
|
|
35
|
+
const key = `title_${kind}` as keyof TermuxNotifyOptions
|
|
36
|
+
return typeof (userOpts as any)[key] === "string" ? (userOpts as any)[key] : titles[kind] || titles.default!
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getContent(kind: string, userOpts: TermuxNotifyOptions): string {
|
|
40
|
+
const contents: Record<string, string> = {
|
|
41
|
+
default: "Session finished",
|
|
42
|
+
done: "Session done",
|
|
43
|
+
subagent_done: "Subagent done",
|
|
44
|
+
question: "Question needs input",
|
|
45
|
+
permission: "Permission needs input",
|
|
46
|
+
error: "Session errored",
|
|
47
|
+
}
|
|
48
|
+
const key = `content_${kind}` as keyof TermuxNotifyOptions
|
|
49
|
+
return typeof (userOpts as any)[key] === "string" ? (userOpts as any)[key] : contents[kind] || contents.default!
|
|
50
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { tmpdir } from "node:os"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
|
|
4
|
+
export const SOUND_FILES: Record<string, string> = {
|
|
5
|
+
default: "bip-bop-01.mp3",
|
|
6
|
+
question: "bip-bop-03.mp3",
|
|
7
|
+
permission: "staplebops-06.mp3",
|
|
8
|
+
error: "nope-03.mp3",
|
|
9
|
+
done: "bip-bop-01.mp3",
|
|
10
|
+
subagent_done: "yup-01.mp3",
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const VIBRATE_PATTERNS: Record<string, string> = {
|
|
14
|
+
default: "400,200,400",
|
|
15
|
+
done: "400,200,400",
|
|
16
|
+
subagent_done: "200,100,200",
|
|
17
|
+
question: "500,250,500",
|
|
18
|
+
permission: "600,200,600",
|
|
19
|
+
error: "800,300,800,300,800",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const PRIORITY_BY_KIND: Record<string, string> = {
|
|
23
|
+
default: "high",
|
|
24
|
+
done: "high",
|
|
25
|
+
subagent_done: "default",
|
|
26
|
+
question: "high",
|
|
27
|
+
permission: "high",
|
|
28
|
+
error: "high",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const DEFAULTS = {
|
|
32
|
+
bin: "/data/data/com.termux/files/usr/bin/termux-notification",
|
|
33
|
+
mediaBin: "/data/data/com.termux/files/usr/bin/termux-media-player",
|
|
34
|
+
sharedPath: join(tmpdir(), "termux-notify-shared.json"),
|
|
35
|
+
seenTTL: 60_000,
|
|
36
|
+
sessionCooldown: 5_000,
|
|
37
|
+
globalCooldown: 1_000,
|
|
38
|
+
sound: true,
|
|
39
|
+
playSound: true,
|
|
40
|
+
vibrate: true,
|
|
41
|
+
requireTermux: true,
|
|
42
|
+
priority: undefined as string | undefined,
|
|
43
|
+
}
|