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.
Files changed (60) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/LICENSE +21 -0
  3. package/README.md +132 -0
  4. package/assets/audio/bip-bop-01.mp3 +0 -0
  5. package/assets/audio/bip-bop-03.mp3 +0 -0
  6. package/assets/audio/nope-03.mp3 +0 -0
  7. package/assets/audio/staplebops-06.mp3 +0 -0
  8. package/assets/audio/yup-01.mp3 +0 -0
  9. package/dist/audio.d.ts +4 -0
  10. package/dist/audio.d.ts.map +1 -0
  11. package/dist/audio.js +55 -0
  12. package/dist/audio.js.map +1 -0
  13. package/dist/config.d.ts +6 -0
  14. package/dist/config.d.ts.map +1 -0
  15. package/dist/config.js +47 -0
  16. package/dist/config.js.map +1 -0
  17. package/dist/constants.d.ts +17 -0
  18. package/dist/constants.d.ts.map +1 -0
  19. package/dist/constants.js +40 -0
  20. package/dist/constants.js.map +1 -0
  21. package/dist/dedup.d.ts +3 -0
  22. package/dist/dedup.d.ts.map +1 -0
  23. package/dist/dedup.js +57 -0
  24. package/dist/dedup.js.map +1 -0
  25. package/dist/env.d.ts +2 -0
  26. package/dist/env.d.ts.map +1 -0
  27. package/dist/env.js +4 -0
  28. package/dist/env.js.map +1 -0
  29. package/dist/events.d.ts +9 -0
  30. package/dist/events.d.ts.map +1 -0
  31. package/dist/events.js +52 -0
  32. package/dist/events.js.map +1 -0
  33. package/dist/index.d.ts +19 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +119 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/notify.d.ts +3 -0
  38. package/dist/notify.d.ts.map +1 -0
  39. package/dist/notify.js +23 -0
  40. package/dist/notify.js.map +1 -0
  41. package/dist/types.d.ts +37 -0
  42. package/dist/types.d.ts.map +1 -0
  43. package/dist/types.js +2 -0
  44. package/dist/types.js.map +1 -0
  45. package/dist/v1.d.ts +21 -0
  46. package/dist/v1.d.ts.map +1 -0
  47. package/dist/v1.js +133 -0
  48. package/dist/v1.js.map +1 -0
  49. package/index.js +4 -0
  50. package/package.json +80 -0
  51. package/src/audio.ts +50 -0
  52. package/src/config.ts +50 -0
  53. package/src/constants.ts +43 -0
  54. package/src/dedup.ts +58 -0
  55. package/src/env.ts +3 -0
  56. package/src/events.ts +58 -0
  57. package/src/index.ts +127 -0
  58. package/src/notify.ts +25 -0
  59. package/src/types.ts +39 -0
  60. 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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"}
@@ -0,0 +1,3 @@
1
+ import type { ResolvedOpts } from "./types.js";
2
+ export declare function notify(title: string, content: string, nid: string, soundName: string, opts: ResolvedOpts): Promise<void>;
3
+ //# sourceMappingURL=notify.d.ts.map
@@ -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"}
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -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
@@ -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
@@ -0,0 +1,4 @@
1
+ export { default } from "./dist/v1.js"
2
+ export { DEFAULTS, PRIORITY_BY_KIND, SOUND_FILES, VIBRATE_PATTERNS } from "./dist/constants.js"
3
+ export { default as V1 } from "./dist/v1.js"
4
+ export { default as V2 } from "./dist/index.js"
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
+ }
@@ -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
+ }