@aight-cool/aight-utils 0.1.0
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/LICENSE +21 -0
- package/README.md +242 -0
- package/hooks/aight-bootstrap/HOOK.md +20 -0
- package/hooks/aight-bootstrap/handler.ts +34 -0
- package/index.ts +74 -0
- package/openclaw.plugin.json +67 -0
- package/package.json +57 -0
- package/src/bootstrap.ts +88 -0
- package/src/config.ts +113 -0
- package/src/defaults.ts +5 -0
- package/src/health.ts +95 -0
- package/src/items.ts +228 -0
- package/src/push-hook.ts +90 -0
- package/src/push.ts +240 -0
- package/src/reminders.ts +91 -0
- package/src/version.ts +63 -0
package/src/config.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config RPC module — aight.config.get, aight.config.patch, aight.status
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { OpenClawPluginApi, GatewayRequestHandlerOptions } from "openclaw/plugin-sdk";
|
|
6
|
+
import { DEFAULT_PUSH_MODE } from "./defaults.js";
|
|
7
|
+
|
|
8
|
+
export interface AightConfig {
|
|
9
|
+
push?: {
|
|
10
|
+
mode?: "private" | "rich";
|
|
11
|
+
relayUrl?: string;
|
|
12
|
+
relaySecret?: string;
|
|
13
|
+
};
|
|
14
|
+
today?: {
|
|
15
|
+
enabled?: boolean;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Secret keys that must never be returned to clients via RPC */
|
|
20
|
+
const SECRET_KEYS: string[] = ["relaySecret"];
|
|
21
|
+
|
|
22
|
+
/** Returns a sanitized copy of the config with secrets redacted */
|
|
23
|
+
export function getClientSafeConfig(config: AightConfig): Record<string, unknown> {
|
|
24
|
+
const safe = JSON.parse(JSON.stringify(config));
|
|
25
|
+
if (safe.push) {
|
|
26
|
+
for (const key of SECRET_KEYS) {
|
|
27
|
+
if (key in safe.push) {
|
|
28
|
+
safe.push[key] = safe.push[key] ? "[REDACTED]" : undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return safe;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getPluginConfig(api: OpenClawPluginApi): AightConfig {
|
|
36
|
+
const raw = api.pluginConfig;
|
|
37
|
+
return raw && typeof raw === "object" && !Array.isArray(raw) ? (raw as AightConfig) : {};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function registerConfig(api: OpenClawPluginApi) {
|
|
41
|
+
api.registerGatewayMethod("aight.config.get", ({ respond }: GatewayRequestHandlerOptions) => {
|
|
42
|
+
respond(true, getClientSafeConfig(getPluginConfig(api)));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
api.registerGatewayMethod(
|
|
46
|
+
"aight.config.patch",
|
|
47
|
+
async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
48
|
+
if (!params || typeof params !== "object") {
|
|
49
|
+
respond(false, { error: "params must be an object" });
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
// Load current config, merge patch into plugin config, write back
|
|
54
|
+
const currentConfig = await api.runtime.config.loadConfig();
|
|
55
|
+
const pluginEntry = (currentConfig as any)?.plugins?.entries?.["aight-utils"] ?? {};
|
|
56
|
+
const currentPluginConfig = pluginEntry.config ?? {};
|
|
57
|
+
|
|
58
|
+
// Deep merge the patch into plugin config
|
|
59
|
+
const merged = { ...currentPluginConfig };
|
|
60
|
+
for (const [key, value] of Object.entries(params as Record<string, unknown>)) {
|
|
61
|
+
if (
|
|
62
|
+
value &&
|
|
63
|
+
typeof value === "object" &&
|
|
64
|
+
!Array.isArray(value) &&
|
|
65
|
+
merged[key] &&
|
|
66
|
+
typeof merged[key] === "object"
|
|
67
|
+
) {
|
|
68
|
+
merged[key] = { ...merged[key], ...value };
|
|
69
|
+
} else {
|
|
70
|
+
merged[key] = value;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Write updated config
|
|
75
|
+
const updatedConfig = {
|
|
76
|
+
...(currentConfig as Record<string, unknown>),
|
|
77
|
+
plugins: {
|
|
78
|
+
...((currentConfig as any)?.plugins ?? {}),
|
|
79
|
+
entries: {
|
|
80
|
+
...((currentConfig as any)?.plugins?.entries ?? {}),
|
|
81
|
+
"aight-utils": {
|
|
82
|
+
...pluginEntry,
|
|
83
|
+
config: merged,
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
await api.runtime.config.writeConfigFile(updatedConfig as any);
|
|
90
|
+
respond(true, { ok: true, config: getClientSafeConfig(merged as AightConfig) });
|
|
91
|
+
} catch (err) {
|
|
92
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
93
|
+
api.logger.error(`[aight-utils] config.patch failed: ${msg}`);
|
|
94
|
+
respond(false, { error: msg });
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
api.registerGatewayMethod("aight.status", ({ respond }: GatewayRequestHandlerOptions) => {
|
|
100
|
+
const cfg = getPluginConfig(api);
|
|
101
|
+
respond(true, {
|
|
102
|
+
ok: true,
|
|
103
|
+
version: "0.1.0",
|
|
104
|
+
push: {
|
|
105
|
+
mode: cfg.push?.mode ?? DEFAULT_PUSH_MODE,
|
|
106
|
+
relayUrl: cfg.push?.relayUrl ?? "https://push.aight.app",
|
|
107
|
+
},
|
|
108
|
+
today: {
|
|
109
|
+
enabled: cfg.today?.enabled ?? true,
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
package/src/defaults.ts
ADDED
package/src/health.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System health RPC — aight.health
|
|
3
|
+
*
|
|
4
|
+
* Returns memory, CPU, disk stats without needing an LLM call.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { execSync } from "child_process";
|
|
8
|
+
import { platform } from "os";
|
|
9
|
+
import type { OpenClawPluginApi, GatewayRequestHandlerOptions } from "openclaw/plugin-sdk";
|
|
10
|
+
|
|
11
|
+
interface HealthStats {
|
|
12
|
+
memory: { usedGB: number; totalGB: number; percent: number } | null;
|
|
13
|
+
cpu: { percent: number } | null;
|
|
14
|
+
disk: { usedGB: number; totalGB: number; percent: number } | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getStats(): HealthStats {
|
|
18
|
+
const os = platform();
|
|
19
|
+
let memory: HealthStats["memory"] = null;
|
|
20
|
+
let cpu: HealthStats["cpu"] = null;
|
|
21
|
+
let disk: HealthStats["disk"] = null;
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
if (os === "darwin") {
|
|
25
|
+
// Memory
|
|
26
|
+
const totalBytes = parseInt(
|
|
27
|
+
execSync("/usr/sbin/sysctl -n hw.memsize", { encoding: "utf8", timeout: 5000 }).trim(),
|
|
28
|
+
);
|
|
29
|
+
const top = execSync("top -l 1 -n 0", { encoding: "utf8", timeout: 5000 });
|
|
30
|
+
const memMatch = top.match(/PhysMem:\s*(\d+)([GM])\s*used/);
|
|
31
|
+
const usedGB = memMatch ? parseInt(memMatch[1]) * (memMatch[2] === "G" ? 1 : 1 / 1024) : 0;
|
|
32
|
+
const totalGB = totalBytes / 1073741824;
|
|
33
|
+
memory = {
|
|
34
|
+
usedGB: Math.round(usedGB * 10) / 10,
|
|
35
|
+
totalGB: Math.round(totalGB * 10) / 10,
|
|
36
|
+
percent: Math.round((usedGB / totalGB) * 100),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// CPU
|
|
40
|
+
const idleMatch = top.match(/([\d.]+)% idle/);
|
|
41
|
+
cpu = {
|
|
42
|
+
percent: idleMatch ? Math.round(100 - parseFloat(idleMatch[1])) : 0,
|
|
43
|
+
};
|
|
44
|
+
} else {
|
|
45
|
+
// Linux memory
|
|
46
|
+
const meminfo = execSync("cat /proc/meminfo", { encoding: "utf8", timeout: 5000 });
|
|
47
|
+
const totalKB = parseInt(meminfo.match(/MemTotal:\s*(\d+)/)?.[1] ?? "0");
|
|
48
|
+
const availKB = parseInt(meminfo.match(/MemAvailable:\s*(\d+)/)?.[1] ?? "0");
|
|
49
|
+
const totalGB = (totalKB * 1024) / 1073741824;
|
|
50
|
+
const usedGB = ((totalKB - availKB) * 1024) / 1073741824;
|
|
51
|
+
memory = {
|
|
52
|
+
usedGB: Math.round(usedGB * 10) / 10,
|
|
53
|
+
totalGB: Math.round(totalGB * 10) / 10,
|
|
54
|
+
percent: Math.round((usedGB / totalGB) * 100),
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Linux CPU
|
|
58
|
+
const loadavg = execSync("cat /proc/loadavg", { encoding: "utf8", timeout: 5000 });
|
|
59
|
+
const load1m = parseFloat(loadavg.split(" ")[0]);
|
|
60
|
+
const cores = parseInt(execSync("nproc", { encoding: "utf8", timeout: 5000 }).trim());
|
|
61
|
+
cpu = { percent: Math.round(Math.min((load1m / cores) * 100, 100)) };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Disk (cross-platform via df)
|
|
65
|
+
const df = execSync("df -k /", { encoding: "utf8", timeout: 5000 });
|
|
66
|
+
const dfLine = df.split("\n")[1];
|
|
67
|
+
const dfParts = dfLine?.split(/\s+/);
|
|
68
|
+
if (dfParts && dfParts.length >= 4) {
|
|
69
|
+
const totalKB = parseInt(dfParts[1]);
|
|
70
|
+
const usedKB = parseInt(dfParts[2]);
|
|
71
|
+
disk = {
|
|
72
|
+
totalGB: Math.round(totalKB / 1048576),
|
|
73
|
+
usedGB: Math.round(usedKB / 1048576),
|
|
74
|
+
percent: Math.round((usedKB / totalKB) * 100),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
// Best effort — return whatever we got
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { memory, cpu, disk };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function registerHealth(api: OpenClawPluginApi) {
|
|
85
|
+
api.registerGatewayMethod("aight.health", async ({ respond }: GatewayRequestHandlerOptions) => {
|
|
86
|
+
try {
|
|
87
|
+
const stats = getStats();
|
|
88
|
+
respond(true, stats);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
respond(false, {
|
|
91
|
+
error: err instanceof Error ? err.message : String(err),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
package/src/items.ts
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
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
|
+
export function registerItems(api: OpenClawPluginApi) {
|
|
154
|
+
api.registerGatewayMethod(
|
|
155
|
+
"aight.items.list",
|
|
156
|
+
({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
157
|
+
const filters: ListFilters =
|
|
158
|
+
params && typeof params === "object" ? (params as ListFilters) : {};
|
|
159
|
+
respond(true, { items: listItems(filters) });
|
|
160
|
+
},
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
api.registerGatewayMethod(
|
|
164
|
+
"aight.items.upsert",
|
|
165
|
+
({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
166
|
+
if (
|
|
167
|
+
!params ||
|
|
168
|
+
typeof params !== "object" ||
|
|
169
|
+
!("id" in params) ||
|
|
170
|
+
!("type" in params) ||
|
|
171
|
+
!("title" in params)
|
|
172
|
+
) {
|
|
173
|
+
respond(false, { error: "item must have id, type, and title" });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const item = upsertItem(params as Item);
|
|
177
|
+
respond(true, { item });
|
|
178
|
+
},
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
api.registerGatewayMethod(
|
|
182
|
+
"aight.items.delete",
|
|
183
|
+
({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
184
|
+
const id = typeof params?.id === "string" ? params.id : "";
|
|
185
|
+
if (!id) {
|
|
186
|
+
respond(false, { error: "id required" });
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const ok = deleteItem(id);
|
|
190
|
+
respond(true, { ok, id });
|
|
191
|
+
},
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
api.registerTool({
|
|
195
|
+
name: "aight_item",
|
|
196
|
+
label: "Aight Item",
|
|
197
|
+
description:
|
|
198
|
+
"Create, update, or delete a structured item in the Aight Today view. " +
|
|
199
|
+
"Use for reminders, tasks, events, deadlines, and process tracking. " +
|
|
200
|
+
"Parse natural language dates/times before calling (e.g. 'tomorrow at 3pm' → ISO 8601).",
|
|
201
|
+
parameters: AightItemToolParams,
|
|
202
|
+
async execute(_toolCallId: string, params: any) {
|
|
203
|
+
const json = (payload: unknown) => ({
|
|
204
|
+
content: [{ type: "text" as const, text: JSON.stringify(payload, null, 2) }],
|
|
205
|
+
details: payload,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
if (params.action === "delete") {
|
|
210
|
+
const id = params.id ?? params.item?.id;
|
|
211
|
+
if (!id) throw new Error("id required for delete");
|
|
212
|
+
const ok = deleteItem(id);
|
|
213
|
+
return json({ ok, id });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!params.item) throw new Error("item required for create/update");
|
|
217
|
+
if (!params.item.id || !params.item.type || !params.item.title) {
|
|
218
|
+
throw new Error("item must have id, type, and title");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const item = upsertItem(params.item);
|
|
222
|
+
return json({ action: params.action, item });
|
|
223
|
+
} catch (err) {
|
|
224
|
+
return json({ error: err instanceof Error ? err.message : String(err) });
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
}
|
package/src/push-hook.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
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
|
+
|
|
9
|
+
export function registerPushHook(api: OpenClawPluginApi) {
|
|
10
|
+
try {
|
|
11
|
+
api.on("agent_end", async (event, ctx) => {
|
|
12
|
+
api.logger.info(
|
|
13
|
+
`[aight-utils] agent_end fired session=${ctx.sessionKey} agent=${ctx.agentId}`,
|
|
14
|
+
);
|
|
15
|
+
const tokens = loadTokens();
|
|
16
|
+
if (tokens.length === 0) return;
|
|
17
|
+
|
|
18
|
+
const msgs = event.messages ?? [];
|
|
19
|
+
api.logger.info(
|
|
20
|
+
`[aight-utils] messages count=${msgs.length} roles=${msgs.map((m: any) => m.role).join(",")}`,
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
// Extract last assistant message - try string and array content formats
|
|
24
|
+
let preview = "";
|
|
25
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
26
|
+
const m = msgs[i] as any;
|
|
27
|
+
if (m.role === "assistant") {
|
|
28
|
+
if (typeof m.content === "string" && m.content.trim()) {
|
|
29
|
+
preview = m.content.slice(0, 200);
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
if (Array.isArray(m.content)) {
|
|
33
|
+
const textBlock = m.content.find(
|
|
34
|
+
(b: any) => b.type === "text" && typeof b.text === "string",
|
|
35
|
+
);
|
|
36
|
+
if (textBlock) {
|
|
37
|
+
preview = textBlock.text.slice(0, 200);
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!preview) {
|
|
45
|
+
api.logger.info(`[aight-utils] No preview found, skipping push`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Skip internal/meta responses
|
|
50
|
+
const skip = ["NO_REPLY", "REPLY_SKIP", "ANNOUNCE_SKIP", "HEARTBEAT_OK"];
|
|
51
|
+
if (skip.includes(preview.trim())) {
|
|
52
|
+
api.logger.info(`[aight-utils] Skipping meta response: ${preview.trim()}`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const freshConfig = getPluginConfig(api);
|
|
57
|
+
|
|
58
|
+
// Resolve display name from gateway config agent list
|
|
59
|
+
const agentId = ctx.agentId ?? "agent";
|
|
60
|
+
const agents = (api.config as any)?.agents?.list ?? [];
|
|
61
|
+
const agent = agents.find((a: any) => a.id === agentId);
|
|
62
|
+
const displayName = agent?.name ?? agent?.identity?.name ?? agentId;
|
|
63
|
+
|
|
64
|
+
for (const device of tokens) {
|
|
65
|
+
if (!device.sendKey) continue;
|
|
66
|
+
try {
|
|
67
|
+
await sendPush(
|
|
68
|
+
device.deviceId,
|
|
69
|
+
{
|
|
70
|
+
title: displayName,
|
|
71
|
+
body: preview,
|
|
72
|
+
data: { sessionKey: ctx.sessionKey, agentId },
|
|
73
|
+
},
|
|
74
|
+
freshConfig,
|
|
75
|
+
);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
api.logger.warn(
|
|
78
|
+
`[aight-utils] Push failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
api.logger.info("[aight-utils] Push hook registered (agent_end)");
|
|
85
|
+
} catch (err) {
|
|
86
|
+
api.logger.error(
|
|
87
|
+
`[aight-utils] Failed to register push hook: ${err instanceof Error ? err.message : String(err)}`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|