@aight-cool/aight-utils 0.1.15 → 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
- }
package/src/push-hook.ts DELETED
@@ -1,136 +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 } from "./push.js";
8
- import { loadGroupName } from "./groups.js";
9
-
10
- export function registerPushHook(api: OpenClawPluginApi) {
11
- try {
12
- api.on("agent_end", async (event, ctx) => {
13
- api.logger.info(
14
- `[aight-utils] agent_end fired session=${ctx.sessionKey} agent=${ctx.agentId}`,
15
- );
16
- const tokens = loadTokens();
17
- if (tokens.length === 0) return;
18
-
19
- // Skip hidden/internal sessions — no push notifications for config,
20
- // security, voice, sub-agent, or other background sessions.
21
- const sk = ctx.sessionKey ?? "";
22
- if (
23
- sk.endsWith(":aight-config") ||
24
- sk.endsWith(":aight-pentest") ||
25
- sk.endsWith(":speak") ||
26
- sk.endsWith(":structured_content") ||
27
- sk.endsWith(":main") ||
28
- sk.includes("subagent") ||
29
- sk.includes("security-audit") ||
30
- sk.includes("_skill-audit-") ||
31
- sk.includes("_ensure-skill-defender") ||
32
- sk.endsWith("security-fix") ||
33
- sk.endsWith("skill-scan")
34
- ) {
35
- api.logger.info(`[aight-utils] Skipping hidden session push: ${sk}`);
36
- return;
37
- }
38
-
39
- const msgs = event.messages ?? [];
40
- api.logger.info(
41
- `[aight-utils] messages count=${msgs.length} roles=${msgs.map((m: any) => m.role).join(",")}`,
42
- );
43
-
44
- // Extract last assistant message - try string and array content formats
45
- let preview = "";
46
- for (let i = msgs.length - 1; i >= 0; i--) {
47
- const m = msgs[i] as any;
48
- if (m.role === "assistant") {
49
- if (typeof m.content === "string" && m.content.trim()) {
50
- preview = m.content.slice(0, 200);
51
- break;
52
- }
53
- if (Array.isArray(m.content)) {
54
- const textBlock = m.content.find(
55
- (b: any) => b.type === "text" && typeof b.text === "string",
56
- );
57
- if (textBlock) {
58
- preview = textBlock.text.slice(0, 200);
59
- break;
60
- }
61
- }
62
- }
63
- }
64
-
65
- if (!preview) {
66
- api.logger.info(`[aight-utils] No preview found, skipping push`);
67
- return;
68
- }
69
-
70
- // Skip hidden/internal sessions (aight-config, structured_content, etc.)
71
- const sessionKey_ = ctx.sessionKey ?? "";
72
- const HIDDEN_SESSIONS = ["aight-config", "structured_content"];
73
- if (HIDDEN_SESSIONS.some((h) => sessionKey_.includes(h))) {
74
- api.logger.info(`[aight-utils] Skipping hidden session: ${sessionKey_}`);
75
- return;
76
- }
77
-
78
- // Skip internal/meta responses
79
- const skip = ["NO_REPLY", "REPLY_SKIP", "ANNOUNCE_SKIP", "HEARTBEAT_OK"];
80
- if (skip.includes(preview.trim())) {
81
- api.logger.info(`[aight-utils] Skipping meta response: ${preview.trim()}`);
82
- return;
83
- }
84
-
85
- const freshConfig = getPluginConfig(api);
86
-
87
- // Resolve display name from gateway config agent list
88
- const agentId = ctx.agentId ?? "agent";
89
- const agents = (api.config as any)?.agents?.list ?? [];
90
- const agent = agents.find((a: any) => a.id === agentId);
91
- const displayName = agent?.name ?? agent?.identity?.name ?? agentId;
92
-
93
- // Resolve group chat name for push subtitle (WhatsApp-style layout)
94
- const pushTitle = displayName;
95
- let pushSubtitle: string | undefined;
96
- const sessionKey = ctx.sessionKey ?? "";
97
- if (sessionKey.includes(":group-chat:")) {
98
- const groupId = sessionKey.split(":group-chat:")[1];
99
- if (groupId) {
100
- const groupName = loadGroupName(api, groupId);
101
- if (groupName) {
102
- pushSubtitle = groupName;
103
- }
104
- }
105
- }
106
-
107
- const cleanBody = preview.trim().replace(/\n+/g, " ").trim();
108
-
109
- for (const device of tokens) {
110
- if (!device.sendKey) continue;
111
- try {
112
- await sendPush(
113
- device.deviceId,
114
- {
115
- title: pushTitle.trim(),
116
- subtitle: pushSubtitle,
117
- body: cleanBody,
118
- data: { sessionKey: ctx.sessionKey, agentId },
119
- },
120
- freshConfig,
121
- );
122
- } catch (err) {
123
- api.logger.warn(
124
- `[aight-utils] Push failed: ${err instanceof Error ? err.message : String(err)}`,
125
- );
126
- }
127
- }
128
- });
129
-
130
- api.logger.info("[aight-utils] Push hook registered (agent_end)");
131
- } catch (err) {
132
- api.logger.error(
133
- `[aight-utils] Failed to register push hook: ${err instanceof Error ? err.message : String(err)}`,
134
- );
135
- }
136
- }
package/src/push.ts DELETED
@@ -1,242 +0,0 @@
1
- /**
2
- * Push Notifications — aight.push.register, aight.push.unregister, sendPush()
3
- *
4
- * Auth model: the relay has a master secret. During device registration, the
5
- * plugin calls relay /register with the device push token. The relay returns
6
- * a sendKey = HMAC(masterSecret, pushToken). The plugin stores the sendKey
7
- * alongside the device token. When sending a push, the plugin includes the
8
- * sendKey. The relay recomputes the HMAC and verifies — zero state, no shared secrets.
9
- */
10
-
11
- import * as fs from "node:fs";
12
- import * as path from "node:path";
13
- import * as os from "node:os";
14
- import type { OpenClawPluginApi, GatewayRequestHandlerOptions } from "openclaw/plugin-sdk";
15
- import type { AightConfig } from "./config.js";
16
- import { DEFAULT_RELAY_URL, DEFAULT_PUSH_MODE } from "./defaults.js";
17
-
18
- // ── Device Token Store ──
19
-
20
- export interface DeviceToken {
21
- deviceId: string;
22
- pushToken: string;
23
- platform: "ios" | "android";
24
- sandbox?: boolean;
25
- sendKey?: string;
26
- registeredAt: string;
27
- }
28
-
29
- const TOKEN_DIR = path.join(os.homedir(), ".openclaw", "aight");
30
- const TOKEN_FILE = path.join(TOKEN_DIR, "devices.json");
31
-
32
- export function loadTokens(): DeviceToken[] {
33
- try {
34
- if (!fs.existsSync(TOKEN_FILE)) return [];
35
- const raw = fs.readFileSync(TOKEN_FILE, "utf-8");
36
- const parsed = JSON.parse(raw);
37
- return Array.isArray(parsed) ? parsed : [];
38
- } catch {
39
- return [];
40
- }
41
- }
42
-
43
- export function saveTokens(tokens: DeviceToken[]): void {
44
- fs.mkdirSync(TOKEN_DIR, { recursive: true, mode: 0o700 });
45
- fs.writeFileSync(TOKEN_FILE, JSON.stringify(tokens, null, 2), { encoding: "utf-8", mode: 0o600 });
46
- }
47
-
48
- export function registerToken(token: DeviceToken): void {
49
- const tokens = loadTokens();
50
- const idx = tokens.findIndex((t) => t.deviceId === token.deviceId);
51
- if (idx >= 0) {
52
- tokens[idx] = token;
53
- } else {
54
- tokens.push(token);
55
- }
56
- saveTokens(tokens);
57
- }
58
-
59
- export function unregisterToken(deviceId: string): boolean {
60
- const tokens = loadTokens();
61
- const filtered = tokens.filter((t) => t.deviceId !== deviceId);
62
- if (filtered.length === tokens.length) return false;
63
- saveTokens(filtered);
64
- return true;
65
- }
66
-
67
- // ── Relay Registration (get sendKey) ──
68
-
69
- async function obtainSendKey(relayUrl: string, pushToken: string): Promise<string> {
70
- const res = await fetch(`${relayUrl}/register`, {
71
- method: "POST",
72
- headers: { "Content-Type": "application/json" },
73
- body: JSON.stringify({ token: pushToken }),
74
- });
75
-
76
- if (!res.ok) {
77
- const text = await res.text();
78
- throw new Error(`Relay /register returned ${res.status}: ${text}`);
79
- }
80
-
81
- const data = (await res.json()) as { ok: boolean; sendKey: string };
82
- if (!data.sendKey) throw new Error("Relay did not return a sendKey");
83
- return data.sendKey;
84
- }
85
-
86
- // ── Push Sending ──
87
-
88
- export interface PushPayload {
89
- title?: string;
90
- subtitle?: string;
91
- body?: string;
92
- data?: Record<string, unknown>;
93
- silent?: boolean;
94
- }
95
-
96
- export async function sendPush(
97
- deviceId: string,
98
- payload: PushPayload,
99
- config: AightConfig,
100
- ): Promise<{ ok: boolean; error?: string }> {
101
- const tokens = loadTokens();
102
- const device = tokens.find((t) => t.deviceId === deviceId);
103
- if (!device) {
104
- return { ok: false, error: `No device token for ${deviceId}` };
105
- }
106
- if (!device.sendKey) {
107
- return { ok: false, error: `No sendKey for device ${deviceId} — re-register to obtain one` };
108
- }
109
-
110
- const relayUrl = config.push?.relayUrl ?? DEFAULT_RELAY_URL;
111
- const mode = config.push?.mode ?? DEFAULT_PUSH_MODE;
112
-
113
- const pushBody: Record<string, unknown> = {
114
- token: device.pushToken,
115
- sendKey: device.sendKey,
116
- platform: device.platform,
117
- sandbox: device.sandbox ?? false,
118
- };
119
-
120
- if (mode === "rich" && !payload.silent) {
121
- pushBody.title = payload.title;
122
- if (payload.subtitle) pushBody.subtitle = payload.subtitle;
123
- pushBody.body = payload.body;
124
- }
125
-
126
- if (payload.data) {
127
- pushBody.data = payload.data;
128
- }
129
-
130
- try {
131
- const res = await fetch(`${relayUrl}/send`, {
132
- method: "POST",
133
- headers: { "Content-Type": "application/json" },
134
- body: JSON.stringify(pushBody),
135
- });
136
-
137
- if (!res.ok) {
138
- const text = await res.text();
139
- return { ok: false, error: `Relay returned ${res.status}: ${text}` };
140
- }
141
-
142
- return { ok: true };
143
- } catch (err) {
144
- return { ok: false, error: err instanceof Error ? err.message : String(err) };
145
- }
146
- }
147
-
148
- // ── Registration ──
149
-
150
- export function registerPush(api: OpenClawPluginApi, _config: AightConfig) {
151
- api.registerGatewayMethod(
152
- "aight.push.register",
153
- ({ params, respond }: GatewayRequestHandlerOptions) => {
154
- if (
155
- !params ||
156
- typeof params !== "object" ||
157
- typeof params.deviceId !== "string" ||
158
- typeof params.pushToken !== "string" ||
159
- (params.platform !== "ios" && params.platform !== "android")
160
- ) {
161
- respond(false, { error: "deviceId, pushToken, and platform (ios|android) required" });
162
- return;
163
- }
164
-
165
- const deviceId = params.deviceId as string;
166
- const pushToken = params.pushToken as string;
167
- const platform = params.platform as "ios" | "android";
168
- const sandbox = !!params.sandbox;
169
-
170
- // Register immediately (respond fast)
171
- registerToken({
172
- deviceId,
173
- pushToken,
174
- platform,
175
- sandbox,
176
- registeredAt: new Date().toISOString(),
177
- });
178
-
179
- respond(true, { ok: true, deviceId });
180
- api.logger.info(`[aight-utils] Push token registered for device ${deviceId}`);
181
-
182
- // Obtain sendKey from relay in background
183
- const relayUrl = _config.push?.relayUrl ?? DEFAULT_RELAY_URL;
184
- obtainSendKey(relayUrl, pushToken)
185
- .then((sendKey) => {
186
- // Update the token with the sendKey
187
- registerToken({
188
- deviceId,
189
- pushToken,
190
- platform,
191
- sandbox,
192
- sendKey,
193
- registeredAt: new Date().toISOString(),
194
- });
195
- api.logger.info(`[aight-utils] sendKey obtained for device ${deviceId}`);
196
- })
197
- .catch((err) => {
198
- const msg = err instanceof Error ? err.message : String(err);
199
- api.logger.warn(`[aight-utils] Failed to obtain sendKey: ${msg}`);
200
- });
201
- },
202
- );
203
-
204
- api.registerGatewayMethod(
205
- "aight.push.unregister",
206
- ({ params, respond }: GatewayRequestHandlerOptions) => {
207
- const deviceId = typeof params?.deviceId === "string" ? params.deviceId : "";
208
- if (!deviceId) {
209
- respond(false, { error: "deviceId required" });
210
- return;
211
- }
212
-
213
- const ok = unregisterToken(deviceId);
214
- respond(true, { ok, deviceId });
215
- },
216
- );
217
-
218
- api.registerGatewayMethod(
219
- "aight.push.test",
220
- async ({ params, respond }: GatewayRequestHandlerOptions) => {
221
- const deviceId = typeof params?.deviceId === "string" ? params.deviceId : "";
222
- const tokens = loadTokens();
223
- const device = deviceId ? tokens.find((t) => t.deviceId === deviceId) : tokens[0];
224
- if (!device) {
225
- respond(false, { error: "No registered device" });
226
- return;
227
- }
228
-
229
- // Test always sends a visible push regardless of privacy mode
230
- const result = await sendPush(
231
- device.deviceId,
232
- {
233
- title: "Aight 🤙",
234
- body: "Push notifications are working!",
235
- },
236
- { ..._config, push: { ..._config.push, mode: "rich" } },
237
- );
238
-
239
- respond(true, result);
240
- },
241
- );
242
- }