-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathstore.ts
More file actions
104 lines (89 loc) · 3.32 KB
/
Copy pathstore.ts
File metadata and controls
104 lines (89 loc) · 3.32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
import { promises as fs } from 'node:fs';
import { dirname, join } from 'node:path';
import { randomUUID } from 'node:crypto';
import type { AlertRule, AlertsFile } from './types.js';
import { DEFAULT_COOLDOWN_MS } from './types.js';
import { GEMINI_MCP_DIR, isENOENT, writeAtomic } from './paths.js';
export const DEFAULT_ALERTS_DIR = GEMINI_MCP_DIR;
export const DEFAULT_ALERTS_FILE = join(DEFAULT_ALERTS_DIR, 'alerts.json');
const EMPTY_FILE: AlertsFile = { version: 1, rules: [] };
export type CreateRuleInput = Omit<AlertRule, 'id' | 'createdAt' | 'lastFiredAt'> &
Partial<Pick<AlertRule, 'id' | 'createdAt' | 'lastFiredAt'>>;
export type UpdateRulePatch = Partial<
Pick<AlertRule, 'name' | 'enabled' | 'oneShot' | 'cooldownMs' | 'params' | 'lastFiredAt'>
>;
export interface AlertStoreOptions {
filePath?: string;
}
export class AlertStore {
readonly filePath: string;
private readonly dir: string;
constructor(opts: AlertStoreOptions = {}) {
this.filePath = opts.filePath ?? DEFAULT_ALERTS_FILE;
this.dir = dirname(this.filePath);
}
async list(): Promise<AlertRule[]> {
const file = await this.read();
return file.rules;
}
async get(id: string): Promise<AlertRule | undefined> {
const file = await this.read();
return file.rules.find((r) => r.id === id);
}
async create(input: CreateRuleInput): Promise<AlertRule> {
const rule: AlertRule = {
id: input.id ?? randomUUID(),
name: input.name,
category: input.category,
enabled: input.enabled,
oneShot: input.oneShot,
cooldownMs: input.cooldownMs ?? DEFAULT_COOLDOWN_MS,
params: input.params,
createdAt: input.createdAt ?? new Date().toISOString(),
lastFiredAt: input.lastFiredAt ?? null,
};
const file = await this.read();
if (file.rules.some((r) => r.id === rule.id)) {
throw new Error(`Alert rule already exists: ${rule.id}`);
}
file.rules.push(rule);
await this.write(file);
return rule;
}
async update(id: string, patch: UpdateRulePatch): Promise<AlertRule> {
const file = await this.read();
const idx = file.rules.findIndex((r) => r.id === id);
if (idx === -1) throw new Error(`Alert rule not found: ${id}`);
file.rules[idx] = { ...file.rules[idx], ...patch };
await this.write(file);
return file.rules[idx];
}
async delete(id: string): Promise<boolean> {
const file = await this.read();
const before = file.rules.length;
file.rules = file.rules.filter((r) => r.id !== id);
if (file.rules.length === before) return false;
await this.write(file);
return true;
}
async markFired(id: string, firedAt: string = new Date().toISOString()): Promise<AlertRule> {
return this.update(id, { lastFiredAt: firedAt });
}
async read(): Promise<AlertsFile> {
try {
const raw = await fs.readFile(this.filePath, 'utf8');
const parsed = JSON.parse(raw) as AlertsFile;
if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.rules)) {
throw new Error('Malformed alerts file');
}
return parsed;
} catch (err) {
if (isENOENT(err)) return structuredClone(EMPTY_FILE);
throw err;
}
}
async write(file: AlertsFile): Promise<void> {
await fs.mkdir(this.dir, { recursive: true });
await writeAtomic(this.filePath, `${JSON.stringify(file, null, 2)}\n`);
}
}