@aight-cool/aight-utils 0.1.17 → 0.1.18

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/src/items.ts DELETED
@@ -1,252 +0,0 @@
1
- /**
2
- * Items Store — aight.items.list, aight.items.upsert, aight.items.delete + aight_item tool
3
- */
4
-
5
- import { Type, type Static } from "@sinclair/typebox";
6
- import * as fs from "node:fs";
7
- import * as path from "node:path";
8
- import * as os from "node:os";
9
- import type { OpenClawPluginApi, GatewayRequestHandlerOptions } from "openclaw/plugin-sdk";
10
-
11
- // ── Types ──
12
-
13
- export const ItemType = Type.Union([
14
- Type.Literal("trigger"),
15
- Type.Literal("item"),
16
- Type.Literal("process"),
17
- ]);
18
-
19
- export const ItemStatus = Type.Union([
20
- Type.Literal("active"),
21
- Type.Literal("done"),
22
- Type.Literal("fired"),
23
- Type.Literal("cancelled"),
24
- Type.Literal("deleted"),
25
- ]);
26
-
27
- export const ItemSchema = Type.Object({
28
- id: Type.String({ description: "Unique item ID" }),
29
- type: ItemType,
30
- title: Type.String({ description: "Display title" }),
31
- status: Type.Optional(ItemStatus),
32
- labels: Type.Optional(Type.Array(Type.String())),
33
- scheduledFor: Type.Optional(Type.String({ description: "ISO 8601 datetime for triggers" })),
34
- description: Type.Optional(Type.String()),
35
- url: Type.Optional(Type.String({ description: "Related URL (PR, issue, etc.)" })),
36
- metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
37
- createdAt: Type.Optional(Type.String()),
38
- updatedAt: Type.Optional(Type.String()),
39
- });
40
-
41
- export type Item = Static<typeof ItemSchema>;
42
-
43
- // ── Store ──
44
-
45
- const STORE_DIR = path.join(os.homedir(), ".openclaw", "aight");
46
- const STORE_FILE = path.join(STORE_DIR, "items.json");
47
-
48
- export function loadItems(): Item[] {
49
- try {
50
- if (!fs.existsSync(STORE_FILE)) return [];
51
- const raw = fs.readFileSync(STORE_FILE, "utf-8");
52
- const parsed = JSON.parse(raw);
53
- return Array.isArray(parsed) ? parsed : [];
54
- } catch {
55
- return [];
56
- }
57
- }
58
-
59
- export function saveItems(items: Item[]): void {
60
- fs.mkdirSync(STORE_DIR, { recursive: true });
61
- fs.writeFileSync(STORE_FILE, JSON.stringify(items, null, 2), { encoding: "utf-8", mode: 0o600 });
62
- }
63
-
64
- const MAX_TITLE_LEN = 500;
65
- const MAX_DESC_LEN = 5000;
66
- const MAX_ITEMS = 10000;
67
-
68
- export function upsertItem(item: Item): Item {
69
- if (item.title && item.title.length > MAX_TITLE_LEN) {
70
- throw new Error(`title exceeds max length of ${MAX_TITLE_LEN}`);
71
- }
72
- if (item.description && item.description.length > MAX_DESC_LEN) {
73
- throw new Error(`description exceeds max length of ${MAX_DESC_LEN}`);
74
- }
75
- const items = loadItems();
76
- const now = new Date().toISOString();
77
- const idx = items.findIndex((i) => i.id === item.id);
78
- if (idx < 0 && items.length >= MAX_ITEMS) {
79
- throw new Error(`item store full (max ${MAX_ITEMS})`);
80
- }
81
- const merged: Item = {
82
- ...item,
83
- status: item.status ?? "active",
84
- updatedAt: now,
85
- createdAt: idx >= 0 ? items[idx].createdAt : now,
86
- };
87
- if (idx >= 0) {
88
- items[idx] = merged;
89
- } else {
90
- items.push(merged);
91
- }
92
- saveItems(items);
93
- return merged;
94
- }
95
-
96
- export function deleteItem(id: string): boolean {
97
- const items = loadItems();
98
- const idx = items.findIndex((i) => i.id === id);
99
- if (idx < 0) return false;
100
- items[idx] = { ...items[idx], status: "deleted", updatedAt: new Date().toISOString() };
101
- saveItems(items);
102
- return true;
103
- }
104
-
105
- export interface ListFilters {
106
- type?: string;
107
- labels?: string[];
108
- status?: string;
109
- from?: string;
110
- to?: string;
111
- }
112
-
113
- export function listItems(filters: ListFilters = {}): Item[] {
114
- let items = loadItems().filter((i) => i.status !== "deleted");
115
-
116
- if (filters.type) {
117
- items = items.filter((i) => i.type === filters.type);
118
- }
119
- if (filters.status) {
120
- items = items.filter((i) => i.status === filters.status);
121
- }
122
- if (filters.labels && filters.labels.length > 0) {
123
- items = items.filter((i) => filters.labels!.some((l) => i.labels?.includes(l)));
124
- }
125
- if (filters.from) {
126
- const fromTs = new Date(filters.from).getTime();
127
- items = items.filter((i) => {
128
- if (!i.scheduledFor) return true;
129
- return new Date(i.scheduledFor).getTime() >= fromTs;
130
- });
131
- }
132
- if (filters.to) {
133
- const toTs = new Date(filters.to).getTime();
134
- items = items.filter((i) => {
135
- if (!i.scheduledFor) return true;
136
- return new Date(i.scheduledFor).getTime() <= toTs;
137
- });
138
- }
139
-
140
- return items;
141
- }
142
-
143
- // ── Tool schema ──
144
-
145
- export const AightItemToolParams = Type.Object({
146
- action: Type.Union([Type.Literal("create"), Type.Literal("update"), Type.Literal("delete")]),
147
- item: Type.Optional(ItemSchema),
148
- id: Type.Optional(Type.String({ description: "Item ID (for delete)" })),
149
- });
150
-
151
- // ── Registration ──
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
-
159
- export function registerItems(api: OpenClawPluginApi) {
160
- api.registerGatewayMethod(
161
- "aight.items.list",
162
- ({ params, respond }: GatewayRequestHandlerOptions) => {
163
- const filters: ListFilters =
164
- params && typeof params === "object" ? (params as ListFilters) : {};
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 });
184
- },
185
- );
186
-
187
- api.registerGatewayMethod(
188
- "aight.items.upsert",
189
- ({ params, respond }: GatewayRequestHandlerOptions) => {
190
- if (
191
- !params ||
192
- typeof params !== "object" ||
193
- !("id" in params) ||
194
- !("type" in params) ||
195
- !("title" in params)
196
- ) {
197
- respond(false, { error: "item must have id, type, and title" });
198
- return;
199
- }
200
- const item = upsertItem(params as Item);
201
- respond(true, { item });
202
- },
203
- );
204
-
205
- api.registerGatewayMethod(
206
- "aight.items.delete",
207
- ({ params, respond }: GatewayRequestHandlerOptions) => {
208
- const id = typeof params?.id === "string" ? params.id : "";
209
- if (!id) {
210
- respond(false, { error: "id required" });
211
- return;
212
- }
213
- const ok = deleteItem(id);
214
- respond(true, { ok, id });
215
- },
216
- );
217
-
218
- api.registerTool({
219
- name: "aight_item",
220
- label: "Aight Item",
221
- description:
222
- "Create, update, or delete a structured item in the Aight Today view. " +
223
- "Use for reminders, tasks, events, deadlines, and process tracking. " +
224
- "Parse natural language dates/times before calling (e.g. 'tomorrow at 3pm' → ISO 8601).",
225
- parameters: AightItemToolParams,
226
- async execute(_toolCallId: string, params: any) {
227
- const json = (payload: unknown) => ({
228
- content: [{ type: "text" as const, text: JSON.stringify(payload, null, 2) }],
229
- details: payload,
230
- });
231
-
232
- try {
233
- if (params.action === "delete") {
234
- const id = params.id ?? params.item?.id;
235
- if (!id) throw new Error("id required for delete");
236
- const ok = deleteItem(id);
237
- return json({ ok, id });
238
- }
239
-
240
- if (!params.item) throw new Error("item required for create/update");
241
- if (!params.item.id || !params.item.type || !params.item.title) {
242
- throw new Error("item must have id, type, and title");
243
- }
244
-
245
- const item = upsertItem(params.item);
246
- return json({ action: params.action, item });
247
- } catch (err) {
248
- return json({ error: err instanceof Error ? err.message : String(err) });
249
- }
250
- },
251
- });
252
- }
@@ -1,115 +0,0 @@
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 DELETED
@@ -1,162 +0,0 @@
1
- /**
2
- * Push notification hook — sends push on agent_end.
3
- */
4
-
5
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
6
- import { getPluginConfig } from "./config.js";
7
- import { sendPush, loadTokens, unregisterToken } from "./push.js";
8
- import { loadGroupName } from "./groups.js";
9
- import { shouldSendPush } from "./notif-prefs.js";
10
-
11
- export function registerPushHook(api: OpenClawPluginApi) {
12
- try {
13
- api.on("agent_end", async (event, ctx) => {
14
- api.logger.info(
15
- `[aight-utils] agent_end fired session=${ctx.sessionKey} agent=${ctx.agentId}`,
16
- );
17
- const tokens = loadTokens();
18
- if (tokens.length === 0) return;
19
-
20
- // Skip hidden/internal sessions — no push notifications for config,
21
- // security, voice, sub-agent, or other background sessions.
22
- const sk = ctx.sessionKey ?? "";
23
- if (
24
- sk.endsWith(":aight-config") ||
25
- sk.endsWith(":aight-pentest") ||
26
- sk.endsWith(":speak") ||
27
- sk.endsWith(":structured_content") ||
28
- sk.endsWith(":main") ||
29
- sk.includes("subagent") ||
30
- sk.includes("security-audit") ||
31
- sk.includes("_skill-audit-") ||
32
- sk.includes("_ensure-skill-defender") ||
33
- sk.endsWith("security-fix") ||
34
- sk.endsWith("skill-scan")
35
- ) {
36
- api.logger.info(`[aight-utils] Skipping hidden session push: ${sk}`);
37
- return;
38
- }
39
-
40
- const msgs = event.messages ?? [];
41
- api.logger.info(
42
- `[aight-utils] messages count=${msgs.length} roles=${msgs.map((m: any) => m.role).join(",")}`,
43
- );
44
-
45
- // Extract last assistant message - try string and array content formats
46
- let preview = "";
47
- for (let i = msgs.length - 1; i >= 0; i--) {
48
- const m = msgs[i] as any;
49
- if (m.role === "assistant") {
50
- if (typeof m.content === "string" && m.content.trim()) {
51
- preview = m.content.slice(0, 200);
52
- break;
53
- }
54
- if (Array.isArray(m.content)) {
55
- const textBlock = m.content.find(
56
- (b: any) => b.type === "text" && typeof b.text === "string",
57
- );
58
- if (textBlock) {
59
- preview = textBlock.text.slice(0, 200);
60
- break;
61
- }
62
- }
63
- }
64
- }
65
-
66
- if (!preview) {
67
- api.logger.info(`[aight-utils] No preview found, skipping push`);
68
- return;
69
- }
70
-
71
- // Skip hidden/internal sessions (aight-config, structured_content, etc.)
72
- const sessionKey_ = ctx.sessionKey ?? "";
73
- const HIDDEN_SESSIONS = ["aight-config", "structured_content"];
74
- if (HIDDEN_SESSIONS.some((h) => sessionKey_.includes(h))) {
75
- api.logger.info(`[aight-utils] Skipping hidden session: ${sessionKey_}`);
76
- return;
77
- }
78
-
79
- // Skip internal/meta responses
80
- const skip = ["NO_REPLY", "REPLY_SKIP", "ANNOUNCE_SKIP", "HEARTBEAT_OK"];
81
- if (skip.includes(preview.trim())) {
82
- api.logger.info(`[aight-utils] Skipping meta response: ${preview.trim()}`);
83
- return;
84
- }
85
-
86
- const freshConfig = getPluginConfig(api);
87
-
88
- // Resolve display name from gateway config agent list
89
- const agentId = ctx.agentId ?? "agent";
90
- const agents = (api.config as any)?.agents?.list ?? [];
91
- const agent = agents.find((a: any) => a.id === agentId);
92
- const displayName = agent?.name ?? agent?.identity?.name ?? agentId;
93
-
94
- // Resolve group chat name for push subtitle (WhatsApp-style layout)
95
- const pushTitle = displayName;
96
- let pushSubtitle: string | undefined;
97
- const sessionKey = ctx.sessionKey ?? "";
98
- if (sessionKey.includes(":group-chat:")) {
99
- const groupId = sessionKey.split(":group-chat:")[1];
100
- if (groupId) {
101
- const groupName = loadGroupName(api, groupId);
102
- if (groupName) {
103
- pushSubtitle = groupName;
104
- }
105
- }
106
- }
107
-
108
- const cleanBody = preview.trim().replace(/\n+/g, " ").trim();
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
-
118
- for (const device of tokens) {
119
- if (!device.sendKey) continue;
120
- try {
121
- const pushResult = await sendPush(
122
- device.deviceId,
123
- {
124
- title: pushTitle.trim(),
125
- subtitle: pushSubtitle,
126
- body: cleanBody,
127
- data: { sessionKey: ctx.sessionKey, agentId },
128
- },
129
- freshConfig,
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
- }
148
- } catch (err) {
149
- api.logger.warn(
150
- `[aight-utils] Push failed: ${err instanceof Error ? err.message : String(err)}`,
151
- );
152
- }
153
- }
154
- });
155
-
156
- api.logger.info("[aight-utils] Push hook registered (agent_end)");
157
- } catch (err) {
158
- api.logger.error(
159
- `[aight-utils] Failed to register push hook: ${err instanceof Error ? err.message : String(err)}`,
160
- );
161
- }
162
- }