@aight-cool/aight-utils 0.1.15 → 0.1.17

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/index.ts CHANGED
@@ -14,6 +14,7 @@ import { registerPushHook } from "./src/push-hook.js";
14
14
  import { registerHealth } from "./src/health.js";
15
15
  import { registerVersion } from "./src/version.js";
16
16
  import { registerGroupRpc } from "./src/groups.js";
17
+ import { registerNotifPrefsRPC } from "./src/notif-prefs.js";
17
18
 
18
19
  const aightPlugin = {
19
20
  id: "aight-utils",
@@ -68,6 +69,7 @@ const aightPlugin = {
68
69
  registerHealth(api);
69
70
  registerVersion(api);
70
71
  registerGroupRpc(api);
72
+ registerNotifPrefsRPC(api);
71
73
 
72
74
  api.logger.info("[aight-utils] Plugin loaded");
73
75
  },
@@ -2,7 +2,7 @@
2
2
  "id": "aight-utils",
3
3
  "name": "Aight App Utils",
4
4
  "description": "Aight App: Push notifications, Today items, config RPC, and agent bootstrap",
5
- "version": "0.1.15",
5
+ "version": "0.1.17",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aight-cool/aight-utils",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "OpenClaw gateway plugin for Aight App: push notifications, Today items, config RPC, and agent bootstrap",
5
5
  "type": "module",
6
6
  "files": [
package/src/bootstrap.ts CHANGED
@@ -256,7 +256,9 @@ export function registerBootstrap(api: OpenClawPluginApi) {
256
256
  api.logger.info(`[aight-utils] api.on type: ${typeof api.on}`);
257
257
  api.on("before_agent_start", (_event: unknown, ctx: { sessionKey?: string }) => {
258
258
  // Inject AIGHT.md context into every agent session
259
- api.logger.info(`[aight-utils] Bootstrap: injecting AIGHT.md into session ${ctx?.sessionKey}`);
259
+ api.logger.info(
260
+ `[aight-utils] Bootstrap: injecting AIGHT.md into session ${ctx?.sessionKey}`,
261
+ );
260
262
  return { systemPrompt: AIGHT_MD };
261
263
  });
262
264
  api.logger.info("[aight-utils] Bootstrap hook registered (before_agent_start)");
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Notification Preferences — gateway-side storage for push suppression.
3
+ *
4
+ * The app syncs notification preferences here via RPC. The push hook
5
+ * checks these prefs before sending to the relay — if a category is
6
+ * muted, the push never leaves the user's machine.
7
+ *
8
+ * RPC methods:
9
+ * aight.notif.setPrefs — update preferences
10
+ * aight.notif.getPrefs — read current preferences
11
+ */
12
+
13
+ import * as fs from "node:fs";
14
+ import * as path from "node:path";
15
+ import * as os from "node:os";
16
+ import type { OpenClawPluginApi, GatewayRequestHandlerOptions } from "openclaw/plugin-sdk";
17
+
18
+ // ── Types ──
19
+
20
+ export interface NotifPrefs {
21
+ globalMute: boolean;
22
+ muteUntil: string | null;
23
+ agentReplies: boolean;
24
+ groupChat: boolean;
25
+ cron: boolean;
26
+ }
27
+
28
+ const DEFAULTS: NotifPrefs = {
29
+ globalMute: false,
30
+ muteUntil: null,
31
+ agentReplies: true,
32
+ groupChat: true,
33
+ cron: false,
34
+ };
35
+
36
+ // ── Storage ──
37
+
38
+ const PREFS_DIR = path.join(os.homedir(), ".openclaw", "aight");
39
+ const PREFS_FILE = path.join(PREFS_DIR, "notif-prefs.json");
40
+
41
+ export function loadNotifPrefs(): NotifPrefs {
42
+ try {
43
+ if (!fs.existsSync(PREFS_FILE)) return { ...DEFAULTS };
44
+ const raw = fs.readFileSync(PREFS_FILE, "utf-8");
45
+ const parsed = JSON.parse(raw);
46
+ return { ...DEFAULTS, ...parsed };
47
+ } catch {
48
+ return { ...DEFAULTS };
49
+ }
50
+ }
51
+
52
+ function saveNotifPrefs(prefs: NotifPrefs): void {
53
+ fs.mkdirSync(PREFS_DIR, { recursive: true, mode: 0o700 });
54
+ fs.writeFileSync(PREFS_FILE, JSON.stringify(prefs, null, 2), "utf-8");
55
+ }
56
+
57
+ // ── Classification (matches app-side logic) ──
58
+
59
+ export function classifySessionKey(sessionKey: string): "agentReplies" | "groupChat" | "cron" {
60
+ if (sessionKey.includes(":cron:")) return "cron";
61
+ if (sessionKey.includes(":group-chat:")) return "groupChat";
62
+ return "agentReplies";
63
+ }
64
+
65
+ /**
66
+ * Check if a push for this sessionKey should be sent.
67
+ */
68
+ export function shouldSendPush(sessionKey: string): boolean {
69
+ if (!sessionKey) return true; // fail open
70
+
71
+ const prefs = loadNotifPrefs();
72
+
73
+ // Global mute
74
+ if (prefs.globalMute) {
75
+ if (!prefs.muteUntil) return false; // indefinite
76
+ const expiry = new Date(prefs.muteUntil).getTime();
77
+ if (Date.now() < expiry) return false;
78
+ // Mute expired — clear it
79
+ prefs.globalMute = false;
80
+ prefs.muteUntil = null;
81
+ saveNotifPrefs(prefs);
82
+ }
83
+
84
+ // Category check
85
+ const category = classifySessionKey(sessionKey);
86
+ return prefs[category];
87
+ }
88
+
89
+ // ── RPC Registration ──
90
+
91
+ export function registerNotifPrefsRPC(api: OpenClawPluginApi) {
92
+ api.registerGatewayMethod("aight.notif.getPrefs", ({ respond }: GatewayRequestHandlerOptions) => {
93
+ respond(true, loadNotifPrefs());
94
+ });
95
+
96
+ api.registerGatewayMethod(
97
+ "aight.notif.setPrefs",
98
+ ({ params, respond }: GatewayRequestHandlerOptions) => {
99
+ const update = (params ?? {}) as Partial<NotifPrefs>;
100
+ const current = loadNotifPrefs();
101
+
102
+ if (typeof update.globalMute === "boolean") current.globalMute = update.globalMute;
103
+ if (typeof update.agentReplies === "boolean") current.agentReplies = update.agentReplies;
104
+ if (typeof update.groupChat === "boolean") current.groupChat = update.groupChat;
105
+ if (typeof update.cron === "boolean") current.cron = update.cron;
106
+ if (update.muteUntil !== undefined) current.muteUntil = update.muteUntil;
107
+
108
+ saveNotifPrefs(current);
109
+ api.logger.info(`[aight-utils] Notification prefs updated: ${JSON.stringify(current)}`);
110
+ respond(true, current);
111
+ },
112
+ );
113
+
114
+ api.logger.info("[aight-utils] Notification prefs RPC registered");
115
+ }
package/src/push-hook.ts CHANGED
@@ -4,8 +4,9 @@
4
4
 
5
5
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
6
6
  import { getPluginConfig } from "./config.js";
7
- import { sendPush, loadTokens } from "./push.js";
7
+ import { sendPush, loadTokens, unregisterToken } from "./push.js";
8
8
  import { loadGroupName } from "./groups.js";
9
+ import { shouldSendPush } from "./notif-prefs.js";
9
10
 
10
11
  export function registerPushHook(api: OpenClawPluginApi) {
11
12
  try {
@@ -106,10 +107,18 @@ export function registerPushHook(api: OpenClawPluginApi) {
106
107
 
107
108
  const cleanBody = preview.trim().replace(/\n+/g, " ").trim();
108
109
 
110
+ // ── Notification preference gate ──
111
+ // Check if this category is muted — if so, don't send the push at all.
112
+ const sk_ = ctx.sessionKey ?? "";
113
+ if (!shouldSendPush(sk_)) {
114
+ api.logger.info(`[aight-utils] Push suppressed by notification prefs: ${sk_}`);
115
+ return;
116
+ }
117
+
109
118
  for (const device of tokens) {
110
119
  if (!device.sendKey) continue;
111
120
  try {
112
- await sendPush(
121
+ const pushResult = await sendPush(
113
122
  device.deviceId,
114
123
  {
115
124
  title: pushTitle.trim(),
@@ -119,6 +128,23 @@ export function registerPushHook(api: OpenClawPluginApi) {
119
128
  },
120
129
  freshConfig,
121
130
  );
131
+ api.logger.info(
132
+ `[aight-utils] Push sent: session=${ctx.sessionKey} device=${device.deviceId} ok=${pushResult.ok}${pushResult.error ? ` error=${pushResult.error}` : ""}`,
133
+ );
134
+
135
+ // Auto-prune stale tokens — if the relay rejects the token, remove it
136
+ if (!pushResult.ok && pushResult.error) {
137
+ const err = pushResult.error.toLowerCase();
138
+ if (
139
+ err.includes("baddevicetoken") ||
140
+ err.includes("unregistered") ||
141
+ err.includes("devicetokennotfortopic") ||
142
+ err.includes("expired")
143
+ ) {
144
+ api.logger.info(`[aight-utils] Pruning stale device token: ${device.deviceId}`);
145
+ unregisterToken(device.deviceId);
146
+ }
147
+ }
122
148
  } catch (err) {
123
149
  api.logger.warn(
124
150
  `[aight-utils] Push failed: ${err instanceof Error ? err.message : String(err)}`,