@aight-cool/aight-utils 0.1.14 → 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 +2 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/bootstrap.ts +20 -12
- package/src/config.ts +15 -31
- package/src/items.ts +25 -1
- package/src/notif-prefs.ts +115 -0
- package/src/push-hook.ts +28 -2
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
|
},
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/src/bootstrap.ts
CHANGED
|
@@ -6,6 +6,16 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
|
6
6
|
|
|
7
7
|
const AIGHT_MD = `# Aight Integration
|
|
8
8
|
|
|
9
|
+
## ⚠️ Shortcuts Protocol (CRITICAL — follow exactly)
|
|
10
|
+
|
|
11
|
+
When you receive a message starting with "shortcut:", you MUST reply with ONLY a JSON object — no explanation, no markdown fences, no other text:
|
|
12
|
+
{"short_name": "Example Name", "emoji": "🎯"}
|
|
13
|
+
Rules:
|
|
14
|
+
- short_name: 2-4 words, Title Case, describing what the shortcut does
|
|
15
|
+
- emoji: a single relevant emoji
|
|
16
|
+
- Do NOT answer the question in the shortcut text. Just label it.
|
|
17
|
+
- Reply with NOTHING else — just the raw JSON object
|
|
18
|
+
|
|
9
19
|
Aight is the iOS app the user is chatting through. It connects to the OpenClaw gateway running on their machine, giving them a native mobile interface for their AI agent.
|
|
10
20
|
|
|
11
21
|
## What You Can Do (via Aight)
|
|
@@ -189,6 +199,7 @@ When you receive a message prefixed with \`[Group Chat: "Name" — Members: ...]
|
|
|
189
199
|
[Group Chat: "Name" — Members: emoji Name (@username), ...]
|
|
190
200
|
[Recent messages]
|
|
191
201
|
emoji SenderName: message text
|
|
202
|
+
emoji SenderName: [your message at HH:MM]
|
|
192
203
|
emoji SenderName: message text
|
|
193
204
|
...
|
|
194
205
|
|
|
@@ -196,15 +207,18 @@ emoji SenderName: message text
|
|
|
196
207
|
The user's actual message
|
|
197
208
|
\`\`\`
|
|
198
209
|
|
|
210
|
+
**Your own messages are stubbed.** To save tokens, the app replaces the body of your own messages in the recent messages block with \`[your message at HH:MM]\`. You already have the full text in your session history, so no information is lost. Other agents' and the user's messages are shown in full.
|
|
211
|
+
|
|
199
212
|
Rules:
|
|
200
213
|
- To address another agent, **@mention them** in your reply text. The app routes automatically.
|
|
201
214
|
- Do **NOT** use \`sessions_send\` — just @mention in your message.
|
|
202
215
|
- Recent messages provide conversational context — the gateway session has full history.
|
|
203
216
|
- \`[Your turn]\` marks the boundary between context and the new message you should respond to.
|
|
217
|
+
- If you need to recall what you said at a specific time, check your own session history — it has the full text.
|
|
204
218
|
|
|
205
|
-
##
|
|
219
|
+
## Group Chat — Task Protocol
|
|
206
220
|
|
|
207
|
-
When a task is posted in
|
|
221
|
+
When a task is posted in any group chat, follow these rules **without exception:**
|
|
208
222
|
|
|
209
223
|
### 1. Claim Immediately
|
|
210
224
|
When you start working on something, say so in the group chat. No silent pickups.
|
|
@@ -232,15 +246,7 @@ If an agent goes silent for 5+ minutes during an active task, any other agent (o
|
|
|
232
246
|
|
|
233
247
|
## Shortcuts (Aight App)
|
|
234
248
|
|
|
235
|
-
|
|
236
|
-
Reply with ONLY a JSON object — no markdown, no explanation:
|
|
237
|
-
\`\`\`
|
|
238
|
-
{"short_name": "Example Name", "emoji": "🎯"}
|
|
239
|
-
\`\`\`
|
|
240
|
-
Rules:
|
|
241
|
-
- short_name: 2-4 words, Title Case, describing what the shortcut does
|
|
242
|
-
- emoji: a single relevant emoji
|
|
243
|
-
- Reply with NOTHING else — just the JSON object
|
|
249
|
+
See the Shortcuts Protocol at the top of this document.
|
|
244
250
|
|
|
245
251
|
`;
|
|
246
252
|
|
|
@@ -250,7 +256,9 @@ export function registerBootstrap(api: OpenClawPluginApi) {
|
|
|
250
256
|
api.logger.info(`[aight-utils] api.on type: ${typeof api.on}`);
|
|
251
257
|
api.on("before_agent_start", (_event: unknown, ctx: { sessionKey?: string }) => {
|
|
252
258
|
// Inject AIGHT.md context into every agent session
|
|
253
|
-
api.logger.info(
|
|
259
|
+
api.logger.info(
|
|
260
|
+
`[aight-utils] Bootstrap: injecting AIGHT.md into session ${ctx?.sessionKey}`,
|
|
261
|
+
);
|
|
254
262
|
return { systemPrompt: AIGHT_MD };
|
|
255
263
|
});
|
|
256
264
|
api.logger.info("[aight-utils] Bootstrap hook registered (before_agent_start)");
|
package/src/config.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface AightConfig {
|
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
/**
|
|
19
|
+
/** Strict allowlist of keys this plugin owns — anything else is rejected */
|
|
20
20
|
const PLUGIN_CONFIG_KEYS = new Set(["push", "today"]);
|
|
21
21
|
|
|
22
22
|
/** Secret keys that must never be returned to clients via RPC */
|
|
@@ -53,25 +53,23 @@ export function registerConfig(api: OpenClawPluginApi) {
|
|
|
53
53
|
return;
|
|
54
54
|
}
|
|
55
55
|
try {
|
|
56
|
+
// Reject any keys not in the plugin allowlist — never touch root gateway config
|
|
57
|
+
const incoming = params as Record<string, unknown>;
|
|
58
|
+
for (const key of Object.keys(incoming)) {
|
|
59
|
+
if (!PLUGIN_CONFIG_KEYS.has(key)) {
|
|
60
|
+
respond(false, { error: `Key "${key}" is not allowed in config.patch` });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
56
65
|
// Load current config
|
|
57
66
|
const currentConfig = await api.runtime.config.loadConfig();
|
|
58
67
|
const pluginEntry = (currentConfig as any)?.plugins?.entries?.["aight-utils"] ?? {};
|
|
59
68
|
const currentPluginConfig = pluginEntry.config ?? {};
|
|
60
69
|
|
|
61
|
-
//
|
|
62
|
-
const pluginPatch: Record<string, unknown> = {};
|
|
63
|
-
const rootPatch: Record<string, unknown> = {};
|
|
64
|
-
for (const [key, value] of Object.entries(params as Record<string, unknown>)) {
|
|
65
|
-
if (PLUGIN_CONFIG_KEYS.has(key)) {
|
|
66
|
-
pluginPatch[key] = value;
|
|
67
|
-
} else {
|
|
68
|
-
rootPatch[key] = value;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Deep merge plugin-level keys into plugin config
|
|
70
|
+
// Deep merge allowed plugin-level keys into plugin config
|
|
73
71
|
const merged = { ...currentPluginConfig };
|
|
74
|
-
for (const [key, value] of Object.entries(
|
|
72
|
+
for (const [key, value] of Object.entries(incoming)) {
|
|
75
73
|
if (
|
|
76
74
|
value &&
|
|
77
75
|
typeof value === "object" &&
|
|
@@ -85,8 +83,9 @@ export function registerConfig(api: OpenClawPluginApi) {
|
|
|
85
83
|
}
|
|
86
84
|
}
|
|
87
85
|
|
|
88
|
-
// Build updated config
|
|
89
|
-
|
|
86
|
+
// Build updated config — only the plugin's own config section is modified;
|
|
87
|
+
// root gateway config is preserved as-is and never overwritten by client input.
|
|
88
|
+
const updatedConfig: Record<string, unknown> = {
|
|
90
89
|
...(currentConfig as Record<string, unknown>),
|
|
91
90
|
plugins: {
|
|
92
91
|
...((currentConfig as any)?.plugins ?? {}),
|
|
@@ -100,21 +99,6 @@ export function registerConfig(api: OpenClawPluginApi) {
|
|
|
100
99
|
},
|
|
101
100
|
};
|
|
102
101
|
|
|
103
|
-
// Deep merge root-level keys into the gateway config
|
|
104
|
-
for (const [key, value] of Object.entries(rootPatch)) {
|
|
105
|
-
if (
|
|
106
|
-
value &&
|
|
107
|
-
typeof value === "object" &&
|
|
108
|
-
!Array.isArray(value) &&
|
|
109
|
-
updatedConfig[key] &&
|
|
110
|
-
typeof updatedConfig[key] === "object"
|
|
111
|
-
) {
|
|
112
|
-
updatedConfig[key] = { ...(updatedConfig[key] as Record<string, unknown>), ...value };
|
|
113
|
-
} else {
|
|
114
|
-
updatedConfig[key] = value;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
102
|
await api.runtime.config.writeConfigFile(updatedConfig as any);
|
|
119
103
|
respond(true, { ok: true, config: getClientSafeConfig(merged as AightConfig) });
|
|
120
104
|
} catch (err) {
|
package/src/items.ts
CHANGED
|
@@ -150,13 +150,37 @@ export const AightItemToolParams = Type.Object({
|
|
|
150
150
|
|
|
151
151
|
// ── Registration ──
|
|
152
152
|
|
|
153
|
+
/** Strip heavy fields for list responses — detail is fetched via aight.items.get */
|
|
154
|
+
function toLightItem(item: Item): Omit<Item, "description" | "metadata" | "url"> {
|
|
155
|
+
const { description: _d, metadata: _m, url: _u, ...light } = item;
|
|
156
|
+
return light;
|
|
157
|
+
}
|
|
158
|
+
|
|
153
159
|
export function registerItems(api: OpenClawPluginApi) {
|
|
154
160
|
api.registerGatewayMethod(
|
|
155
161
|
"aight.items.list",
|
|
156
162
|
({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
157
163
|
const filters: ListFilters =
|
|
158
164
|
params && typeof params === "object" ? (params as ListFilters) : {};
|
|
159
|
-
respond(true, { items: listItems(filters) });
|
|
165
|
+
respond(true, { items: listItems(filters).map(toLightItem) });
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
api.registerGatewayMethod(
|
|
170
|
+
"aight.items.get",
|
|
171
|
+
({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
172
|
+
const id = typeof params?.id === "string" ? params.id : "";
|
|
173
|
+
if (!id) {
|
|
174
|
+
respond(false, { error: "id required" });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const all = loadItems();
|
|
178
|
+
const item = all.find((i) => i.id === id);
|
|
179
|
+
if (!item || item.status === "deleted") {
|
|
180
|
+
respond(false, { error: "item not found" });
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
respond(true, { item });
|
|
160
184
|
},
|
|
161
185
|
);
|
|
162
186
|
|
|
@@ -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)}`,
|