@goodnesshq/opencode-notification 0.2.1 → 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.
@@ -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, dirname } from "node:path";
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
  },
32
+ telegram: {
33
+ enabled: true,
34
+ token: "",
35
+ chatId: "",
36
+ },
34
37
  ntfy: {
35
38
  enabled: true,
36
39
  server: "https://ntfy.sh",
37
40
  topic: "",
38
41
  },
39
- telegram: {
40
- enabled: false,
41
- token: "",
42
- chatId: "",
43
- },
44
- },
45
- overrides: {
46
- includeGroups: [],
47
- excludeGroups: [],
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
- await copySchema(cwd);
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,21 +381,38 @@ 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);
553
411
 
554
- const telegramEnabled = interactive
555
- ? await promptConfirm("Enable Telegram notifications?", false)
556
- : yesNo(await rl.question("Enable Telegram notifications? (y/N) [N] "), false);
557
-
558
412
  let globalConfig = await readJson(GLOBAL_CONFIG_PATH);
559
413
  if (!globalConfig) globalConfig = {};
560
414
  if (!globalConfig.ntfy) globalConfig.ntfy = {};
561
415
 
562
- let telegramToken = "";
563
- let telegramChatId = "";
564
-
565
416
  if (ntfyEnabled) {
566
417
  const serverInput = interactive
567
418
  ? await promptInput(
@@ -583,24 +434,6 @@ async function main() {
583
434
  if (topicInput.trim()) globalConfig.ntfy.topic = topicInput.trim();
584
435
  }
585
436
 
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
- const updatesUrl = `https://api.telegram.org/bot${telegramToken}/getUpdates`;
593
- console.log("\nFind your Telegram chat ID:");
594
- console.log(" 1) Send your bot a message in Telegram.");
595
- console.log(` 2) Open ${updatesUrl} in a browser.`);
596
- console.log(" 3) Use message.chat.id (or channel_post.chat.id for channels).\n");
597
- }
598
- const chatIdInput = interactive
599
- ? await promptInput("Telegram chat ID: ")
600
- : await rl.question("Telegram chat ID: ");
601
- telegramChatId = chatIdInput.trim();
602
- }
603
-
604
437
  const detailLevel = interactive
605
438
  ? await promptSelect({
606
439
  message: "Detail level:",
@@ -614,43 +447,6 @@ async function main() {
614
447
  ? "full"
615
448
  : "title-only";
616
449
 
617
- const adjustGroups = interactive
618
- ? await promptConfirm("Adjust event groups?", false)
619
- : yesNo(await rl.question("\nAdjust event groups? (y/N) [N] "), false);
620
- let includeGroups = [];
621
- let excludeGroups = [];
622
- if (adjustGroups) {
623
- const advanced = interactive
624
- ? await promptConfirm("Show advanced groups?", false)
625
- : yesNo(await rl.question("Show advanced groups? (y/N) [N] "), false);
626
- const allowed = advanced
627
- ? GROUPS_BASIC.concat(GROUPS_ADVANCED)
628
- : GROUPS_BASIC;
629
- if (interactive) {
630
- const choices = allowed.map((group) => ({ name: group, value: group }));
631
- includeGroups = await promptMultiSelect({
632
- message: "Include groups:",
633
- choices,
634
- defaultValues: [],
635
- });
636
- excludeGroups = await promptMultiSelect({
637
- message: "Exclude groups:",
638
- choices,
639
- defaultValues: [],
640
- });
641
- } else {
642
- console.log(`Available groups: ${allowed.join(", ")}`);
643
- const includeInput = await rl.question(
644
- "Include groups (comma-separated, blank to skip): ",
645
- );
646
- const excludeInput = await rl.question(
647
- "Exclude groups (comma-separated, blank to skip): ",
648
- );
649
- includeGroups = parseList(includeInput, allowed);
650
- excludeGroups = parseList(excludeInput, allowed);
651
- }
652
- }
653
-
654
450
  const installTN = interactive
655
451
  ? await promptConfirm("Use terminal-notifier if available?", true)
656
452
  : yesNo(
@@ -701,20 +497,16 @@ async function main() {
701
497
  enabled: macEnabled,
702
498
  method: installTN ? "auto" : "applescript",
703
499
  },
704
- ntfy: {
705
- enabled: ntfyEnabled,
706
- server: globalConfig.ntfy?.server || DEFAULTS.channels.ntfy.server,
707
- topic: globalConfig.ntfy?.topic || DEFAULTS.channels.ntfy.topic,
708
- },
709
500
  telegram: {
710
501
  enabled: telegramEnabled,
711
502
  token: telegramToken,
712
503
  chatId: telegramChatId,
713
504
  },
714
- },
715
- overrides: {
716
- includeGroups,
717
- excludeGroups,
505
+ ntfy: {
506
+ enabled: ntfyEnabled,
507
+ server: globalConfig.ntfy?.server || DEFAULTS.channels.ntfy.server,
508
+ topic: globalConfig.ntfy?.topic || DEFAULTS.channels.ntfy.topic,
509
+ },
718
510
  },
719
511
  };
720
512
 
@@ -729,7 +521,6 @@ async function main() {
729
521
  if (shouldWrite) {
730
522
  await ensureOpencodeDir(cwd);
731
523
  await writeJson(join(cwd, REPO_CONFIG_PATH), config);
732
- await copySchema(cwd);
733
524
  await ensureGitignore(cwd);
734
525
  }
735
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 Schema
30
+ ## Config
31
31
 
32
- The config is defined in `.opencode/oc-notify.schema.json` and supports:
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`, `ntfy`, and `telegram` settings
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
- "ntfy": { "enabled": true, "server": "https://ntfy.sh", "topic": "<your-topic>" },
63
- "telegram": { "enabled": false, "token": "", "chatId": "" }
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 (formerly verbose-style)
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` with includeGroups: `action_required`, `failures`
96
+ - `minimal` -> `custom`
124
97
  - `standard` -> `full`
125
- - `verbose` -> `custom` with includeGroups: `action_required`, `failures`, `change_summary`, `session_lifecycle`, `files`, `todos`, `vcs_worktree`, `pty`, `responses`
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)) || !(await fileExists(ASSET_SCHEMA))) {
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.1",
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: false,
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
- const groupSet = new Set(baseGroups);
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
- const schema = await loadSchema(root);
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
- }