@goodnesshq/opencode-notification 0.2.2 → 0.2.4
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/.opencode/notify-init.mjs +36 -244
- package/README.md +8 -36
- package/bin/ocn.mjs +1 -3
- package/package.json +1 -2
- package/plugins/opencode-notifications.mjs +3 -111
- package/.opencode/oc-notify.schema.json +0 -116
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFile, writeFile, mkdir, copyFile, stat } from "node:fs/promises";
|
|
3
|
-
import { join, basename
|
|
3
|
+
import { join, basename } from "node:path";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { createInterface } from "node:readline/promises";
|
|
6
6
|
import * as readline from "node:readline";
|
|
@@ -14,8 +14,6 @@ const GLOBAL_CONFIG_PATH = join(
|
|
|
14
14
|
"oc-notify.json",
|
|
15
15
|
);
|
|
16
16
|
const SCRIPT_PATH = fileURLToPath(import.meta.url);
|
|
17
|
-
const SCRIPT_DIR = dirname(SCRIPT_PATH);
|
|
18
|
-
const SCHEMA_SOURCE_PATH = join(SCRIPT_DIR, "oc-notify.schema.json");
|
|
19
17
|
|
|
20
18
|
const DEFAULTS = {
|
|
21
19
|
enabled: true,
|
|
@@ -31,44 +29,19 @@ const DEFAULTS = {
|
|
|
31
29
|
enabled: true,
|
|
32
30
|
method: "auto",
|
|
33
31
|
},
|
|
34
|
-
ntfy: {
|
|
35
|
-
enabled: true,
|
|
36
|
-
server: "https://ntfy.sh",
|
|
37
|
-
topic: "",
|
|
38
|
-
},
|
|
39
32
|
telegram: {
|
|
40
33
|
enabled: true,
|
|
41
34
|
token: "",
|
|
42
35
|
chatId: "",
|
|
43
36
|
},
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
37
|
+
ntfy: {
|
|
38
|
+
enabled: true,
|
|
39
|
+
server: "https://ntfy.sh",
|
|
40
|
+
topic: "",
|
|
41
|
+
},
|
|
48
42
|
},
|
|
49
43
|
};
|
|
50
44
|
|
|
51
|
-
const GROUPS_BASIC = [
|
|
52
|
-
"action_required",
|
|
53
|
-
"failures",
|
|
54
|
-
"change_summary",
|
|
55
|
-
"session_lifecycle",
|
|
56
|
-
"responses",
|
|
57
|
-
"vcs_worktree",
|
|
58
|
-
"todos",
|
|
59
|
-
];
|
|
60
|
-
|
|
61
|
-
const GROUPS_ADVANCED = [
|
|
62
|
-
"files",
|
|
63
|
-
"pty",
|
|
64
|
-
"commands_tools",
|
|
65
|
-
"message_lifecycle",
|
|
66
|
-
"message_parts",
|
|
67
|
-
"system",
|
|
68
|
-
"lsp",
|
|
69
|
-
"tui",
|
|
70
|
-
];
|
|
71
|
-
|
|
72
45
|
async function commandExists(command) {
|
|
73
46
|
try {
|
|
74
47
|
const { execSync } = await import("node:child_process");
|
|
@@ -87,16 +60,6 @@ function yesNo(value, fallback) {
|
|
|
87
60
|
return fallback;
|
|
88
61
|
}
|
|
89
62
|
|
|
90
|
-
function parseList(input, allowed) {
|
|
91
|
-
if (!input) return [];
|
|
92
|
-
const items = input
|
|
93
|
-
.split(",")
|
|
94
|
-
.map((item) => item.trim())
|
|
95
|
-
.filter(Boolean);
|
|
96
|
-
if (!allowed) return items;
|
|
97
|
-
return items.filter((item) => allowed.includes(item));
|
|
98
|
-
}
|
|
99
|
-
|
|
100
63
|
async function readJson(path) {
|
|
101
64
|
try {
|
|
102
65
|
const content = await readFile(path, "utf8");
|
|
@@ -124,15 +87,6 @@ async function ensureOpencodeDir(cwd) {
|
|
|
124
87
|
await mkdir(join(cwd, ".opencode"), { recursive: true });
|
|
125
88
|
}
|
|
126
89
|
|
|
127
|
-
async function copySchema(cwd) {
|
|
128
|
-
try {
|
|
129
|
-
await copyFile(
|
|
130
|
-
SCHEMA_SOURCE_PATH,
|
|
131
|
-
join(cwd, ".opencode", "oc-notify.schema.json"),
|
|
132
|
-
);
|
|
133
|
-
} catch {}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
90
|
async function ensureGitignore(cwd) {
|
|
137
91
|
const gitignorePath = join(cwd, ".gitignore");
|
|
138
92
|
const entry = ".opencode/oc-notify.json";
|
|
@@ -256,125 +210,6 @@ async function promptConfirm(message, defaultValue) {
|
|
|
256
210
|
});
|
|
257
211
|
}
|
|
258
212
|
|
|
259
|
-
async function promptMultiSelect({
|
|
260
|
-
message,
|
|
261
|
-
choices,
|
|
262
|
-
defaultValues = [],
|
|
263
|
-
pageSize = 12,
|
|
264
|
-
}) {
|
|
265
|
-
return withRawInput(
|
|
266
|
-
() =>
|
|
267
|
-
new Promise((resolve) => {
|
|
268
|
-
const state = { linesRendered: 0 };
|
|
269
|
-
let filter = "";
|
|
270
|
-
let cursor = 0;
|
|
271
|
-
const selected = new Set(defaultValues);
|
|
272
|
-
|
|
273
|
-
const getFiltered = () => {
|
|
274
|
-
if (!filter.trim()) return choices;
|
|
275
|
-
const term = filter.toLowerCase();
|
|
276
|
-
return choices.filter(
|
|
277
|
-
(choice) =>
|
|
278
|
-
choice.name.toLowerCase().includes(term) ||
|
|
279
|
-
choice.value.toLowerCase().includes(term),
|
|
280
|
-
);
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
const render = () => {
|
|
284
|
-
const filtered = getFiltered();
|
|
285
|
-
if (cursor >= filtered.length) cursor = Math.max(0, filtered.length - 1);
|
|
286
|
-
const maxStart = Math.max(0, filtered.length - pageSize);
|
|
287
|
-
const pageStart = Math.min(
|
|
288
|
-
maxStart,
|
|
289
|
-
Math.max(0, cursor - Math.floor(pageSize / 2)),
|
|
290
|
-
);
|
|
291
|
-
const page = filtered.slice(pageStart, pageStart + pageSize);
|
|
292
|
-
const lines = [];
|
|
293
|
-
const header = filter ? `${message} (filter: ${filter})` : message;
|
|
294
|
-
lines.push(header);
|
|
295
|
-
lines.push("Use \u2191/\u2193 to move, space to toggle, enter to confirm");
|
|
296
|
-
if (!page.length) {
|
|
297
|
-
lines.push(" (no matches)");
|
|
298
|
-
} else {
|
|
299
|
-
for (let index = 0; index < page.length; index += 1) {
|
|
300
|
-
const choice = page[index];
|
|
301
|
-
const absoluteIndex = pageStart + index;
|
|
302
|
-
const pointer = absoluteIndex === cursor ? ">" : " ";
|
|
303
|
-
const mark = selected.has(choice.value) ? "x" : " ";
|
|
304
|
-
lines.push(` ${pointer} [${mark}] ${choice.name}`);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
renderPrompt(lines, state);
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
const toggle = () => {
|
|
311
|
-
const filtered = getFiltered();
|
|
312
|
-
const choice = filtered[cursor];
|
|
313
|
-
if (!choice) return;
|
|
314
|
-
if (selected.has(choice.value)) selected.delete(choice.value);
|
|
315
|
-
else selected.add(choice.value);
|
|
316
|
-
};
|
|
317
|
-
|
|
318
|
-
const finish = () => {
|
|
319
|
-
const values = choices
|
|
320
|
-
.filter((choice) => selected.has(choice.value))
|
|
321
|
-
.map((choice) => choice.value);
|
|
322
|
-
renderPrompt([`${message} ${values.join(", ")}`], state);
|
|
323
|
-
process.stdout.write("\n");
|
|
324
|
-
cleanup();
|
|
325
|
-
resolve(values);
|
|
326
|
-
};
|
|
327
|
-
|
|
328
|
-
const onKey = (_char, key) => {
|
|
329
|
-
if (!key) return;
|
|
330
|
-
if (key.ctrl && key.name === "c") {
|
|
331
|
-
cleanup();
|
|
332
|
-
process.stdout.write("\n");
|
|
333
|
-
process.exit(1);
|
|
334
|
-
}
|
|
335
|
-
if (key.name === "up") {
|
|
336
|
-
cursor = Math.max(0, cursor - 1);
|
|
337
|
-
render();
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
if (key.name === "down") {
|
|
341
|
-
cursor = Math.min(getFiltered().length - 1, cursor + 1);
|
|
342
|
-
render();
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
if (key.name === "space") {
|
|
346
|
-
toggle();
|
|
347
|
-
render();
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
if (key.name === "return" || key.name === "enter") {
|
|
351
|
-
finish();
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
if (key.name === "backspace" || key.name === "delete") {
|
|
355
|
-
if (filter) {
|
|
356
|
-
filter = filter.slice(0, -1);
|
|
357
|
-
cursor = 0;
|
|
358
|
-
render();
|
|
359
|
-
}
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) {
|
|
363
|
-
filter += key.sequence;
|
|
364
|
-
cursor = 0;
|
|
365
|
-
render();
|
|
366
|
-
}
|
|
367
|
-
};
|
|
368
|
-
|
|
369
|
-
const cleanup = () => {
|
|
370
|
-
process.stdin.removeListener("keypress", onKey);
|
|
371
|
-
};
|
|
372
|
-
|
|
373
|
-
process.stdin.on("keypress", onKey);
|
|
374
|
-
render();
|
|
375
|
-
}),
|
|
376
|
-
);
|
|
377
|
-
}
|
|
378
213
|
|
|
379
214
|
async function sendTestNotification(config, repoName) {
|
|
380
215
|
const title = config.title || `${repoName}@test`;
|
|
@@ -471,8 +306,7 @@ async function main() {
|
|
|
471
306
|
if (args.includes("--install")) {
|
|
472
307
|
await ensureOpencodeDir(cwd);
|
|
473
308
|
await copyFile(SCRIPT_PATH, join(cwd, ".opencode", "notify-init.mjs"));
|
|
474
|
-
|
|
475
|
-
console.log("Installed .opencode/notify-init.mjs and schema in this repo.");
|
|
309
|
+
console.log("Installed .opencode/notify-init.mjs in this repo.");
|
|
476
310
|
return;
|
|
477
311
|
}
|
|
478
312
|
const interactive = isInteractive();
|
|
@@ -547,6 +381,30 @@ async function main() {
|
|
|
547
381
|
true,
|
|
548
382
|
);
|
|
549
383
|
|
|
384
|
+
const telegramEnabled = interactive
|
|
385
|
+
? await promptConfirm("Enable Telegram notifications?", true)
|
|
386
|
+
: yesNo(await rl.question("Enable Telegram notifications? (Y/n) [Y] "), true);
|
|
387
|
+
|
|
388
|
+
let telegramToken = "";
|
|
389
|
+
let telegramChatId = "";
|
|
390
|
+
|
|
391
|
+
if (telegramEnabled) {
|
|
392
|
+
const tokenInput = interactive
|
|
393
|
+
? await promptInput("Telegram bot token: ")
|
|
394
|
+
: await rl.question("Telegram bot token: ");
|
|
395
|
+
telegramToken = tokenInput.trim();
|
|
396
|
+
if (telegramToken) {
|
|
397
|
+
console.log("\nFind your Telegram chat ID:");
|
|
398
|
+
console.log(" - In Telegram, open the chat info and copy the Peer ID.");
|
|
399
|
+
console.log(" - For 1:1 chats, the chat ID is the Peer ID.");
|
|
400
|
+
console.log(" - For groups, prefix the Peer ID with '-'.\n");
|
|
401
|
+
}
|
|
402
|
+
const chatIdInput = interactive
|
|
403
|
+
? await promptInput("Telegram chat ID: ")
|
|
404
|
+
: await rl.question("Telegram chat ID: ");
|
|
405
|
+
telegramChatId = chatIdInput.trim();
|
|
406
|
+
}
|
|
407
|
+
|
|
550
408
|
const ntfyEnabled = interactive
|
|
551
409
|
? await promptConfirm("Enable ntfy notifications?", true)
|
|
552
410
|
: yesNo(await rl.question("Enable ntfy notifications? (Y/n) [Y] "), true);
|
|
@@ -576,30 +434,6 @@ async function main() {
|
|
|
576
434
|
if (topicInput.trim()) globalConfig.ntfy.topic = topicInput.trim();
|
|
577
435
|
}
|
|
578
436
|
|
|
579
|
-
const telegramEnabled = interactive
|
|
580
|
-
? await promptConfirm("Enable Telegram notifications?", true)
|
|
581
|
-
: yesNo(await rl.question("Enable Telegram notifications? (Y/n) [Y] "), true);
|
|
582
|
-
|
|
583
|
-
let telegramToken = "";
|
|
584
|
-
let telegramChatId = "";
|
|
585
|
-
|
|
586
|
-
if (telegramEnabled) {
|
|
587
|
-
const tokenInput = interactive
|
|
588
|
-
? await promptInput("Telegram bot token: ")
|
|
589
|
-
: await rl.question("Telegram bot token: ");
|
|
590
|
-
telegramToken = tokenInput.trim();
|
|
591
|
-
if (telegramToken) {
|
|
592
|
-
console.log("\nFind your Telegram chat ID:");
|
|
593
|
-
console.log(" - In Telegram, open the chat info and copy the Peer ID.");
|
|
594
|
-
console.log(" - For 1:1 chats, the chat ID is the Peer ID.");
|
|
595
|
-
console.log(" - For groups, prefix the Peer ID with '-'.\n");
|
|
596
|
-
}
|
|
597
|
-
const chatIdInput = interactive
|
|
598
|
-
? await promptInput("Telegram chat ID: ")
|
|
599
|
-
: await rl.question("Telegram chat ID: ");
|
|
600
|
-
telegramChatId = chatIdInput.trim();
|
|
601
|
-
}
|
|
602
|
-
|
|
603
437
|
const detailLevel = interactive
|
|
604
438
|
? await promptSelect({
|
|
605
439
|
message: "Detail level:",
|
|
@@ -613,43 +447,6 @@ async function main() {
|
|
|
613
447
|
? "full"
|
|
614
448
|
: "title-only";
|
|
615
449
|
|
|
616
|
-
const adjustGroups = interactive
|
|
617
|
-
? await promptConfirm("Adjust event groups?", false)
|
|
618
|
-
: yesNo(await rl.question("\nAdjust event groups? (y/N) [N] "), false);
|
|
619
|
-
let includeGroups = [];
|
|
620
|
-
let excludeGroups = [];
|
|
621
|
-
if (adjustGroups) {
|
|
622
|
-
const advanced = interactive
|
|
623
|
-
? await promptConfirm("Show advanced groups?", false)
|
|
624
|
-
: yesNo(await rl.question("Show advanced groups? (y/N) [N] "), false);
|
|
625
|
-
const allowed = advanced
|
|
626
|
-
? GROUPS_BASIC.concat(GROUPS_ADVANCED)
|
|
627
|
-
: GROUPS_BASIC;
|
|
628
|
-
if (interactive) {
|
|
629
|
-
const choices = allowed.map((group) => ({ name: group, value: group }));
|
|
630
|
-
includeGroups = await promptMultiSelect({
|
|
631
|
-
message: "Include groups:",
|
|
632
|
-
choices,
|
|
633
|
-
defaultValues: [],
|
|
634
|
-
});
|
|
635
|
-
excludeGroups = await promptMultiSelect({
|
|
636
|
-
message: "Exclude groups:",
|
|
637
|
-
choices,
|
|
638
|
-
defaultValues: [],
|
|
639
|
-
});
|
|
640
|
-
} else {
|
|
641
|
-
console.log(`Available groups: ${allowed.join(", ")}`);
|
|
642
|
-
const includeInput = await rl.question(
|
|
643
|
-
"Include groups (comma-separated, blank to skip): ",
|
|
644
|
-
);
|
|
645
|
-
const excludeInput = await rl.question(
|
|
646
|
-
"Exclude groups (comma-separated, blank to skip): ",
|
|
647
|
-
);
|
|
648
|
-
includeGroups = parseList(includeInput, allowed);
|
|
649
|
-
excludeGroups = parseList(excludeInput, allowed);
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
|
|
653
450
|
const installTN = interactive
|
|
654
451
|
? await promptConfirm("Use terminal-notifier if available?", true)
|
|
655
452
|
: yesNo(
|
|
@@ -700,20 +497,16 @@ async function main() {
|
|
|
700
497
|
enabled: macEnabled,
|
|
701
498
|
method: installTN ? "auto" : "applescript",
|
|
702
499
|
},
|
|
703
|
-
ntfy: {
|
|
704
|
-
enabled: ntfyEnabled,
|
|
705
|
-
server: globalConfig.ntfy?.server || DEFAULTS.channels.ntfy.server,
|
|
706
|
-
topic: globalConfig.ntfy?.topic || DEFAULTS.channels.ntfy.topic,
|
|
707
|
-
},
|
|
708
500
|
telegram: {
|
|
709
501
|
enabled: telegramEnabled,
|
|
710
502
|
token: telegramToken,
|
|
711
503
|
chatId: telegramChatId,
|
|
712
504
|
},
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
505
|
+
ntfy: {
|
|
506
|
+
enabled: ntfyEnabled,
|
|
507
|
+
server: globalConfig.ntfy?.server || DEFAULTS.channels.ntfy.server,
|
|
508
|
+
topic: globalConfig.ntfy?.topic || DEFAULTS.channels.ntfy.topic,
|
|
509
|
+
},
|
|
717
510
|
},
|
|
718
511
|
};
|
|
719
512
|
|
|
@@ -728,7 +521,6 @@ async function main() {
|
|
|
728
521
|
if (shouldWrite) {
|
|
729
522
|
await ensureOpencodeDir(cwd);
|
|
730
523
|
await writeJson(join(cwd, REPO_CONFIG_PATH), config);
|
|
731
|
-
await copySchema(cwd);
|
|
732
524
|
await ensureGitignore(cwd);
|
|
733
525
|
}
|
|
734
526
|
|
package/README.md
CHANGED
|
@@ -27,17 +27,16 @@ Before running the installer, create a unique ntfy topic and set up mobile notif
|
|
|
27
27
|
If you plan to use Telegram, see `docs/telegram-chat-id.md` for how to find your chat ID.
|
|
28
28
|
The installer uses an interactive, arrow-key prompt flow (with a non-interactive fallback in non-TTY environments).
|
|
29
29
|
|
|
30
|
-
## Config
|
|
30
|
+
## Config
|
|
31
31
|
|
|
32
|
-
The config
|
|
32
|
+
The repo config supports:
|
|
33
33
|
|
|
34
34
|
- `enabled`: master switch
|
|
35
35
|
- `title`: override or template (supports `{repo}` and `{branch}`)
|
|
36
36
|
- `tier`: `focus` | `full` | `custom`
|
|
37
37
|
- `detailLevel`: `full` | `title-only`
|
|
38
38
|
- `responseComplete`: trigger for completion notifications (default: `session.idle`)
|
|
39
|
-
- `channels`: `mac`, `
|
|
40
|
-
- `overrides`: include/exclude groups
|
|
39
|
+
- `channels`: `mac`, `telegram`, and `ntfy` settings
|
|
41
40
|
- `dedupe`: in-memory TTL settings
|
|
42
41
|
|
|
43
42
|
## Tier Defaults
|
|
@@ -59,8 +58,8 @@ The config is defined in `.opencode/oc-notify.schema.json` and supports:
|
|
|
59
58
|
},
|
|
60
59
|
"channels": {
|
|
61
60
|
"mac": { "enabled": true, "method": "auto" },
|
|
62
|
-
"
|
|
63
|
-
"
|
|
61
|
+
"telegram": { "enabled": true, "token": "", "chatId": "" },
|
|
62
|
+
"ntfy": { "enabled": true, "server": "https://ntfy.sh", "topic": "<your-topic>" }
|
|
64
63
|
}
|
|
65
64
|
}
|
|
66
65
|
```
|
|
@@ -78,7 +77,7 @@ The config is defined in `.opencode/oc-notify.schema.json` and supports:
|
|
|
78
77
|
}
|
|
79
78
|
```
|
|
80
79
|
|
|
81
|
-
### Custom
|
|
80
|
+
### Custom
|
|
82
81
|
```json
|
|
83
82
|
{
|
|
84
83
|
"enabled": true,
|
|
@@ -86,41 +85,14 @@ The config is defined in `.opencode/oc-notify.schema.json` and supports:
|
|
|
86
85
|
"responseComplete": {
|
|
87
86
|
"enabled": true,
|
|
88
87
|
"trigger": "message.part.updated"
|
|
89
|
-
},
|
|
90
|
-
"overrides": {
|
|
91
|
-
"includeGroups": [
|
|
92
|
-
"action_required",
|
|
93
|
-
"failures",
|
|
94
|
-
"change_summary",
|
|
95
|
-
"session_lifecycle",
|
|
96
|
-
"files",
|
|
97
|
-
"todos",
|
|
98
|
-
"vcs_worktree",
|
|
99
|
-
"pty",
|
|
100
|
-
"responses",
|
|
101
|
-
"commands_tools"
|
|
102
|
-
],
|
|
103
|
-
"excludeGroups": []
|
|
104
88
|
}
|
|
105
89
|
}
|
|
106
90
|
```
|
|
107
91
|
|
|
108
|
-
### Custom (curated)
|
|
109
|
-
```json
|
|
110
|
-
{
|
|
111
|
-
"enabled": true,
|
|
112
|
-
"tier": "custom",
|
|
113
|
-
"overrides": {
|
|
114
|
-
"includeGroups": ["action_required", "failures", "change_summary"],
|
|
115
|
-
"excludeGroups": ["session_lifecycle"]
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
92
|
## Migration from legacy tiers
|
|
120
93
|
|
|
121
94
|
Legacy tiers `minimal`, `standard`, and `verbose` are no longer supported. Update existing configs before notifications resume:
|
|
122
95
|
|
|
123
|
-
- `minimal` -> `custom`
|
|
96
|
+
- `minimal` -> `custom`
|
|
124
97
|
- `standard` -> `full`
|
|
125
|
-
- `verbose` -> `custom`
|
|
126
|
-
```
|
|
98
|
+
- `verbose` -> `custom`
|
package/bin/ocn.mjs
CHANGED
|
@@ -5,7 +5,6 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
|
|
6
6
|
const PACKAGE_ROOT = fileURLToPath(new URL("..", import.meta.url));
|
|
7
7
|
const ASSET_NOTIFY_INIT = join(PACKAGE_ROOT, ".opencode", "notify-init.mjs");
|
|
8
|
-
const ASSET_SCHEMA = join(PACKAGE_ROOT, ".opencode", "oc-notify.schema.json");
|
|
9
8
|
const PLUGIN_PATH = join(PACKAGE_ROOT, "plugins", "opencode-notifications.mjs");
|
|
10
9
|
const GLOBAL_PLUGIN_DIR = join(
|
|
11
10
|
process.env.HOME || "",
|
|
@@ -60,13 +59,12 @@ async function installAssets({ showNextSteps = true } = {}) {
|
|
|
60
59
|
const opencodeDir = join(cwd, ".opencode");
|
|
61
60
|
await ensureDir(opencodeDir);
|
|
62
61
|
|
|
63
|
-
if (!(await fileExists(ASSET_NOTIFY_INIT))
|
|
62
|
+
if (!(await fileExists(ASSET_NOTIFY_INIT))) {
|
|
64
63
|
console.error("ocn: installer assets missing from package");
|
|
65
64
|
process.exit(1);
|
|
66
65
|
}
|
|
67
66
|
|
|
68
67
|
await copyFile(ASSET_NOTIFY_INIT, join(opencodeDir, "notify-init.mjs"));
|
|
69
|
-
await copyFile(ASSET_SCHEMA, join(opencodeDir, "oc-notify.schema.json"));
|
|
70
68
|
|
|
71
69
|
if (showNextSteps) {
|
|
72
70
|
console.log(`Installed OpenCode Notifications installer in ${repoName}.`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodnesshq/opencode-notification",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Per-repo notification plugin for OpenCode with macOS, ntfy, and Telegram delivery.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
"bin/ocn.mjs",
|
|
11
11
|
"plugins/opencode-notifications.mjs",
|
|
12
12
|
".opencode/notify-init.mjs",
|
|
13
|
-
".opencode/oc-notify.schema.json",
|
|
14
13
|
"README.md"
|
|
15
14
|
],
|
|
16
15
|
"keywords": [
|
|
@@ -3,7 +3,6 @@ import { basename, join } from "node:path";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
|
|
5
5
|
const REPO_CONFIG_PATH = ".opencode/oc-notify.json";
|
|
6
|
-
const REPO_SCHEMA_PATH = ".opencode/oc-notify.schema.json";
|
|
7
6
|
const GLOBAL_CONFIG_PATH = join(homedir(), ".config", "opencode", "oc-notify.json");
|
|
8
7
|
|
|
9
8
|
const DEFAULTS = {
|
|
@@ -27,15 +26,11 @@ const DEFAULTS = {
|
|
|
27
26
|
topic: "",
|
|
28
27
|
},
|
|
29
28
|
telegram: {
|
|
30
|
-
enabled:
|
|
29
|
+
enabled: true,
|
|
31
30
|
token: "",
|
|
32
31
|
chatId: "",
|
|
33
32
|
},
|
|
34
33
|
},
|
|
35
|
-
overrides: {
|
|
36
|
-
includeGroups: [],
|
|
37
|
-
excludeGroups: [],
|
|
38
|
-
},
|
|
39
34
|
dedupe: {
|
|
40
35
|
ttlSeconds: 60,
|
|
41
36
|
},
|
|
@@ -107,10 +102,6 @@ function normalizeBool(value, fallback) {
|
|
|
107
102
|
return fallback;
|
|
108
103
|
}
|
|
109
104
|
|
|
110
|
-
function normalizeArray(value) {
|
|
111
|
-
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
112
|
-
}
|
|
113
|
-
|
|
114
105
|
function normalizeTier(value) {
|
|
115
106
|
if (value === undefined || value === null) {
|
|
116
107
|
return { value: DEFAULTS.tier, valid: true };
|
|
@@ -129,15 +120,6 @@ function normalizeMacMethod(value) {
|
|
|
129
120
|
return MAC_METHODS.has(value) ? value : DEFAULTS.channels.mac.method;
|
|
130
121
|
}
|
|
131
122
|
|
|
132
|
-
function sanitizeGroups(groups) {
|
|
133
|
-
const allowed = new Set(Object.keys(GROUPS));
|
|
134
|
-
const unique = new Set();
|
|
135
|
-
for (const group of groups) {
|
|
136
|
-
if (allowed.has(group)) unique.add(group);
|
|
137
|
-
}
|
|
138
|
-
return Array.from(unique);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
123
|
function normalizeConfig(raw, globalConfig) {
|
|
142
124
|
const source = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
143
125
|
const globalSource =
|
|
@@ -190,10 +172,6 @@ function normalizeConfig(raw, globalConfig) {
|
|
|
190
172
|
),
|
|
191
173
|
},
|
|
192
174
|
},
|
|
193
|
-
overrides: {
|
|
194
|
-
includeGroups: sanitizeGroups(normalizeArray(source.overrides?.includeGroups)),
|
|
195
|
-
excludeGroups: sanitizeGroups(normalizeArray(source.overrides?.excludeGroups)),
|
|
196
|
-
},
|
|
197
175
|
dedupe: {
|
|
198
176
|
ttlSeconds: Number.isFinite(source.dedupe?.ttlSeconds)
|
|
199
177
|
? Math.max(0, source.dedupe.ttlSeconds)
|
|
@@ -210,14 +188,7 @@ function normalizeConfig(raw, globalConfig) {
|
|
|
210
188
|
|
|
211
189
|
function buildGroupSet(config) {
|
|
212
190
|
const baseGroups = TIER_GROUPS[config.tier] ?? [];
|
|
213
|
-
|
|
214
|
-
for (const group of config.overrides.includeGroups) {
|
|
215
|
-
if (GROUPS[group]) groupSet.add(group);
|
|
216
|
-
}
|
|
217
|
-
for (const group of config.overrides.excludeGroups) {
|
|
218
|
-
groupSet.delete(group);
|
|
219
|
-
}
|
|
220
|
-
return groupSet;
|
|
191
|
+
return new Set(baseGroups);
|
|
221
192
|
}
|
|
222
193
|
|
|
223
194
|
function buildEventSet(groupSet) {
|
|
@@ -385,7 +356,6 @@ export default async function opencodeNotify(input) {
|
|
|
385
356
|
let terminalNotifierAvailable = null;
|
|
386
357
|
let globalConfigCache = { mtime: 0, data: null };
|
|
387
358
|
let repoConfigCache = { mtime: 0, data: null, errors: [], path: "" };
|
|
388
|
-
let schemaCache = { mtime: 0, data: null, root: "" };
|
|
389
359
|
let validationWarned = false;
|
|
390
360
|
const sessionTitles = new Map();
|
|
391
361
|
const dedupeStore = new Map();
|
|
@@ -415,22 +385,6 @@ export default async function opencodeNotify(input) {
|
|
|
415
385
|
}
|
|
416
386
|
}
|
|
417
387
|
|
|
418
|
-
async function loadSchema(root) {
|
|
419
|
-
if (!root) return null;
|
|
420
|
-
const path = join(root, REPO_SCHEMA_PATH);
|
|
421
|
-
try {
|
|
422
|
-
const info = await stat(path);
|
|
423
|
-
if (info.mtimeMs === schemaCache.mtime && schemaCache.data && schemaCache.root === root) {
|
|
424
|
-
return schemaCache.data;
|
|
425
|
-
}
|
|
426
|
-
const data = await loadJson(path);
|
|
427
|
-
schemaCache = { mtime: info.mtimeMs, data, root };
|
|
428
|
-
return data;
|
|
429
|
-
} catch {
|
|
430
|
-
return null;
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
388
|
async function fileExists(path) {
|
|
435
389
|
try {
|
|
436
390
|
await stat(path);
|
|
@@ -456,66 +410,6 @@ export default async function opencodeNotify(input) {
|
|
|
456
410
|
return "";
|
|
457
411
|
}
|
|
458
412
|
|
|
459
|
-
function validateSchema(schema, value, path = "$") {
|
|
460
|
-
const errors = [];
|
|
461
|
-
if (!schema || typeof schema !== "object") return errors;
|
|
462
|
-
|
|
463
|
-
if (schema.type === "object") {
|
|
464
|
-
const isObject = value && typeof value === "object" && !Array.isArray(value);
|
|
465
|
-
if (!isObject) {
|
|
466
|
-
errors.push(`${path} should be an object`);
|
|
467
|
-
return errors;
|
|
468
|
-
}
|
|
469
|
-
const props = schema.properties || {};
|
|
470
|
-
if (schema.additionalProperties === false) {
|
|
471
|
-
for (const key of Object.keys(value)) {
|
|
472
|
-
if (!props[key]) {
|
|
473
|
-
errors.push(`${path} has unknown property '${key}'`);
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
for (const [key, childSchema] of Object.entries(props)) {
|
|
478
|
-
if (value[key] !== undefined) {
|
|
479
|
-
errors.push(...validateSchema(childSchema, value[key], `${path}.${key}`));
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
if (schema.type === "array") {
|
|
485
|
-
if (!Array.isArray(value)) {
|
|
486
|
-
errors.push(`${path} should be an array`);
|
|
487
|
-
return errors;
|
|
488
|
-
}
|
|
489
|
-
if (schema.items) {
|
|
490
|
-
value.forEach((item, index) => {
|
|
491
|
-
errors.push(...validateSchema(schema.items, item, `${path}[${index}]`));
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
if (schema.type === "string") {
|
|
497
|
-
if (typeof value !== "string") errors.push(`${path} should be a string`);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
if (schema.type === "number") {
|
|
501
|
-
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
502
|
-
errors.push(`${path} should be a number`);
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
if (schema.type === "integer") {
|
|
507
|
-
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
508
|
-
errors.push(`${path} should be an integer`);
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
if (schema.enum && !schema.enum.includes(value)) {
|
|
513
|
-
errors.push(`${path} should be one of: ${schema.enum.join(", ")}`);
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
return errors;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
413
|
async function loadRepoConfig() {
|
|
520
414
|
const root = await resolveRepoRoot();
|
|
521
415
|
if (!root) return null;
|
|
@@ -530,9 +424,7 @@ export default async function opencodeNotify(input) {
|
|
|
530
424
|
return repoConfigCache;
|
|
531
425
|
}
|
|
532
426
|
const data = await loadJson(path);
|
|
533
|
-
|
|
534
|
-
const errors = schema ? validateSchema(schema, data) : [];
|
|
535
|
-
repoConfigCache = { mtime: info.mtimeMs, data, errors, path };
|
|
427
|
+
repoConfigCache = { mtime: info.mtimeMs, data, errors: [], path };
|
|
536
428
|
return repoConfigCache;
|
|
537
429
|
} catch {
|
|
538
430
|
return null;
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"title": "OpenCode Notifications Repo Config",
|
|
4
|
-
"type": "object",
|
|
5
|
-
"additionalProperties": false,
|
|
6
|
-
"properties": {
|
|
7
|
-
"version": {
|
|
8
|
-
"type": "integer",
|
|
9
|
-
"minimum": 1
|
|
10
|
-
},
|
|
11
|
-
"enabled": {
|
|
12
|
-
"type": "boolean"
|
|
13
|
-
},
|
|
14
|
-
"title": {
|
|
15
|
-
"type": "string"
|
|
16
|
-
},
|
|
17
|
-
"tier": {
|
|
18
|
-
"type": "string",
|
|
19
|
-
"enum": ["focus", "full", "custom"]
|
|
20
|
-
},
|
|
21
|
-
"detailLevel": {
|
|
22
|
-
"type": "string",
|
|
23
|
-
"enum": ["full", "title-only"]
|
|
24
|
-
},
|
|
25
|
-
"responseComplete": {
|
|
26
|
-
"type": "object",
|
|
27
|
-
"additionalProperties": false,
|
|
28
|
-
"properties": {
|
|
29
|
-
"enabled": {
|
|
30
|
-
"type": "boolean"
|
|
31
|
-
},
|
|
32
|
-
"trigger": {
|
|
33
|
-
"type": "string",
|
|
34
|
-
"enum": ["session.idle", "message.updated", "message.part.updated"]
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
},
|
|
38
|
-
"channels": {
|
|
39
|
-
"type": "object",
|
|
40
|
-
"additionalProperties": false,
|
|
41
|
-
"properties": {
|
|
42
|
-
"mac": {
|
|
43
|
-
"type": "object",
|
|
44
|
-
"additionalProperties": false,
|
|
45
|
-
"properties": {
|
|
46
|
-
"enabled": {
|
|
47
|
-
"type": "boolean"
|
|
48
|
-
},
|
|
49
|
-
"method": {
|
|
50
|
-
"type": "string",
|
|
51
|
-
"enum": ["auto", "applescript", "terminal-notifier"]
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
|
-
"ntfy": {
|
|
56
|
-
"type": "object",
|
|
57
|
-
"additionalProperties": false,
|
|
58
|
-
"properties": {
|
|
59
|
-
"enabled": {
|
|
60
|
-
"type": "boolean"
|
|
61
|
-
},
|
|
62
|
-
"server": {
|
|
63
|
-
"type": "string"
|
|
64
|
-
},
|
|
65
|
-
"topic": {
|
|
66
|
-
"type": "string"
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
},
|
|
70
|
-
"telegram": {
|
|
71
|
-
"type": "object",
|
|
72
|
-
"additionalProperties": false,
|
|
73
|
-
"properties": {
|
|
74
|
-
"enabled": {
|
|
75
|
-
"type": "boolean"
|
|
76
|
-
},
|
|
77
|
-
"token": {
|
|
78
|
-
"type": "string"
|
|
79
|
-
},
|
|
80
|
-
"chatId": {
|
|
81
|
-
"type": "string"
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
},
|
|
87
|
-
"overrides": {
|
|
88
|
-
"type": "object",
|
|
89
|
-
"additionalProperties": false,
|
|
90
|
-
"properties": {
|
|
91
|
-
"includeGroups": {
|
|
92
|
-
"type": "array",
|
|
93
|
-
"items": {
|
|
94
|
-
"type": "string"
|
|
95
|
-
}
|
|
96
|
-
},
|
|
97
|
-
"excludeGroups": {
|
|
98
|
-
"type": "array",
|
|
99
|
-
"items": {
|
|
100
|
-
"type": "string"
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
},
|
|
105
|
-
"dedupe": {
|
|
106
|
-
"type": "object",
|
|
107
|
-
"additionalProperties": false,
|
|
108
|
-
"properties": {
|
|
109
|
-
"ttlSeconds": {
|
|
110
|
-
"type": "number",
|
|
111
|
-
"minimum": 0
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|