process-watchdog 1.0.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.
Files changed (117) hide show
  1. package/config/default.json +16 -0
  2. package/dashboard/watchdog.html +856 -0
  3. package/dist/api/routes.d.ts +6 -0
  4. package/dist/api/routes.d.ts.map +1 -0
  5. package/dist/api/routes.js +105 -0
  6. package/dist/api/routes.js.map +1 -0
  7. package/dist/api/server.d.ts +10 -0
  8. package/dist/api/server.d.ts.map +1 -0
  9. package/dist/api/server.js +16 -0
  10. package/dist/api/server.js.map +1 -0
  11. package/dist/config.d.ts +27 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/config.js +45 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/daemon/processwatchdog.err.log +973 -0
  16. package/dist/daemon/processwatchdog.exe +0 -0
  17. package/dist/daemon/processwatchdog.exe.config +6 -0
  18. package/dist/daemon/processwatchdog.out.log +2 -0
  19. package/dist/daemon/processwatchdog.wrapper.log +18 -0
  20. package/dist/daemon/processwatchdog.xml +30 -0
  21. package/dist/index.d.ts +3 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +203 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/installer/service.d.ts +3 -0
  26. package/dist/installer/service.d.ts.map +1 -0
  27. package/dist/installer/service.js +41 -0
  28. package/dist/installer/service.js.map +1 -0
  29. package/dist/integrations/mah.d.ts +3 -0
  30. package/dist/integrations/mah.d.ts.map +1 -0
  31. package/dist/integrations/mah.js +22 -0
  32. package/dist/integrations/mah.js.map +1 -0
  33. package/dist/integrations/total-recall.d.ts +3 -0
  34. package/dist/integrations/total-recall.d.ts.map +1 -0
  35. package/dist/integrations/total-recall.js +22 -0
  36. package/dist/integrations/total-recall.js.map +1 -0
  37. package/dist/platform/index.d.ts +4 -0
  38. package/dist/platform/index.d.ts.map +1 -0
  39. package/dist/platform/index.js +10 -0
  40. package/dist/platform/index.js.map +1 -0
  41. package/dist/platform/platform.interface.d.ts +42 -0
  42. package/dist/platform/platform.interface.d.ts.map +1 -0
  43. package/dist/platform/platform.interface.js +2 -0
  44. package/dist/platform/platform.interface.js.map +1 -0
  45. package/dist/platform/windows.d.ts +14 -0
  46. package/dist/platform/windows.d.ts.map +1 -0
  47. package/dist/platform/windows.js +162 -0
  48. package/dist/platform/windows.js.map +1 -0
  49. package/dist/plugins/cpu-monitor.d.ts +11 -0
  50. package/dist/plugins/cpu-monitor.d.ts.map +1 -0
  51. package/dist/plugins/cpu-monitor.js +57 -0
  52. package/dist/plugins/cpu-monitor.js.map +1 -0
  53. package/dist/plugins/disk-health.d.ts +11 -0
  54. package/dist/plugins/disk-health.d.ts.map +1 -0
  55. package/dist/plugins/disk-health.js +111 -0
  56. package/dist/plugins/disk-health.js.map +1 -0
  57. package/dist/plugins/memory-monitor.d.ts +11 -0
  58. package/dist/plugins/memory-monitor.d.ts.map +1 -0
  59. package/dist/plugins/memory-monitor.js +61 -0
  60. package/dist/plugins/memory-monitor.js.map +1 -0
  61. package/dist/plugins/plugin-loader.d.ts +4 -0
  62. package/dist/plugins/plugin-loader.d.ts.map +1 -0
  63. package/dist/plugins/plugin-loader.js +25 -0
  64. package/dist/plugins/plugin-loader.js.map +1 -0
  65. package/dist/plugins/plugin.interface.d.ts +28 -0
  66. package/dist/plugins/plugin.interface.d.ts.map +1 -0
  67. package/dist/plugins/plugin.interface.js +2 -0
  68. package/dist/plugins/plugin.interface.js.map +1 -0
  69. package/dist/plugins/process-guard.d.ts +11 -0
  70. package/dist/plugins/process-guard.d.ts.map +1 -0
  71. package/dist/plugins/process-guard.js +139 -0
  72. package/dist/plugins/process-guard.js.map +1 -0
  73. package/dist/plugins/startup-optimizer.d.ts +11 -0
  74. package/dist/plugins/startup-optimizer.d.ts.map +1 -0
  75. package/dist/plugins/startup-optimizer.js +78 -0
  76. package/dist/plugins/startup-optimizer.js.map +1 -0
  77. package/dist/scheduler.d.ts +16 -0
  78. package/dist/scheduler.d.ts.map +1 -0
  79. package/dist/scheduler.js +46 -0
  80. package/dist/scheduler.js.map +1 -0
  81. package/dist/store/history.d.ts +20 -0
  82. package/dist/store/history.d.ts.map +1 -0
  83. package/dist/store/history.js +60 -0
  84. package/dist/store/history.js.map +1 -0
  85. package/package.json +35 -0
  86. package/src/api/routes.ts +123 -0
  87. package/src/api/server.ts +20 -0
  88. package/src/config.ts +78 -0
  89. package/src/index.ts +228 -0
  90. package/src/installer/service.ts +50 -0
  91. package/src/integrations/mah.ts +22 -0
  92. package/src/integrations/total-recall.ts +27 -0
  93. package/src/platform/index.ts +13 -0
  94. package/src/platform/platform.interface.ts +46 -0
  95. package/src/platform/windows.ts +242 -0
  96. package/src/plugins/cpu-monitor.ts +67 -0
  97. package/src/plugins/disk-health.ts +128 -0
  98. package/src/plugins/memory-monitor.ts +70 -0
  99. package/src/plugins/plugin-loader.ts +27 -0
  100. package/src/plugins/plugin.interface.ts +31 -0
  101. package/src/plugins/process-guard.ts +165 -0
  102. package/src/plugins/startup-optimizer.ts +103 -0
  103. package/src/scheduler.ts +53 -0
  104. package/src/store/history.ts +90 -0
  105. package/tests/api/routes.test.ts +113 -0
  106. package/tests/config.test.ts +24 -0
  107. package/tests/platform/windows.test.ts +59 -0
  108. package/tests/plugins/cpu-monitor.test.ts +69 -0
  109. package/tests/plugins/disk-health.test.ts +69 -0
  110. package/tests/plugins/memory-monitor.test.ts +57 -0
  111. package/tests/plugins/plugin-loader.test.ts +35 -0
  112. package/tests/plugins/process-guard.test.ts +40 -0
  113. package/tests/plugins/startup-optimizer.test.ts +50 -0
  114. package/tests/scheduler.test.ts +69 -0
  115. package/tests/store/history.test.ts +89 -0
  116. package/tsconfig.json +18 -0
  117. package/vitest.config.ts +10 -0
@@ -0,0 +1,242 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import type {
3
+ PlatformAdapter,
4
+ ProcessInfo,
5
+ MemoryInfo,
6
+ DiskInfo,
7
+ CpuSample,
8
+ StartupItem,
9
+ } from './platform.interface.js';
10
+
11
+ function ps(script: string): string {
12
+ return execFileSync(
13
+ 'powershell.exe',
14
+ ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', script],
15
+ {
16
+ encoding: 'utf-8',
17
+ timeout: 60000,
18
+ },
19
+ ).trim();
20
+ }
21
+
22
+ function psJson<T>(script: string): T {
23
+ const raw = ps(`${script} | ConvertTo-Json -Compress`);
24
+ if (!raw) return [] as unknown as T;
25
+ return JSON.parse(raw) as T;
26
+ }
27
+
28
+ export class WindowsPlatform implements PlatformAdapter {
29
+ async getProcesses(): Promise<ProcessInfo[]> {
30
+ type RawProcess = {
31
+ Id: number;
32
+ ProcessName: string;
33
+ memoryMB: number;
34
+ cpu: number | null;
35
+ Responding: boolean;
36
+ };
37
+
38
+ const result = psJson<RawProcess | RawProcess[]>(
39
+ `Get-Process -ErrorAction SilentlyContinue | Select-Object Id, ProcessName, @{N='memoryMB';E={[math]::Round($_.WorkingSet64/1MB)}}, @{N='cpu';E={$_.CPU}}, Responding`,
40
+ );
41
+ const arr = Array.isArray(result) ? result : [result];
42
+ return arr.map((p) => ({
43
+ pid: p.Id,
44
+ name: p.ProcessName,
45
+ memoryMB: p.memoryMB ?? 0,
46
+ cpu: p.cpu ?? 0,
47
+ responding: p.Responding ?? false,
48
+ }));
49
+ }
50
+
51
+ async getProcessesByName(name: string): Promise<ProcessInfo[]> {
52
+ type RawProcess = {
53
+ Id: number;
54
+ ProcessName: string;
55
+ memoryMB: number;
56
+ cpu: number | null;
57
+ Responding: boolean;
58
+ };
59
+
60
+ const result = psJson<RawProcess | RawProcess[]>(
61
+ `Get-Process ${name} -ErrorAction SilentlyContinue | Select-Object Id, ProcessName, @{N='memoryMB';E={[math]::Round($_.WorkingSet64/1MB)}}, @{N='cpu';E={$_.CPU}}, Responding`,
62
+ );
63
+ const arr = Array.isArray(result) ? result : [result];
64
+ return arr.map((p) => ({
65
+ pid: p.Id,
66
+ name: p.ProcessName,
67
+ memoryMB: p.memoryMB ?? 0,
68
+ cpu: p.cpu ?? 0,
69
+ responding: p.Responding ?? false,
70
+ }));
71
+ }
72
+
73
+ async getListeningPids(): Promise<number[]> {
74
+ type RawConn = { OwningProcess: number };
75
+ const result = psJson<RawConn | RawConn[]>(
76
+ `Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | Select-Object OwningProcess -Unique`,
77
+ );
78
+ if (!result) return [];
79
+ const arr = Array.isArray(result) ? result : [result];
80
+ return arr.map((c) => c.OwningProcess);
81
+ }
82
+
83
+ async killProcess(pid: number): Promise<boolean> {
84
+ try {
85
+ ps(`Stop-Process -Id ${pid} -Force -ErrorAction Stop`);
86
+ return true;
87
+ } catch {
88
+ return false;
89
+ }
90
+ }
91
+
92
+ async getMemoryInfo(): Promise<MemoryInfo> {
93
+ type RawOS = { TotalVisibleMemorySize: number; FreePhysicalMemory: number };
94
+ type RawPageFile = { CurrentUsage: number };
95
+
96
+ const osInfo = psJson<RawOS | RawOS[]>(
97
+ `Get-CimInstance Win32_OperatingSystem | Select-Object TotalVisibleMemorySize, FreePhysicalMemory`,
98
+ );
99
+ const osArr = Array.isArray(osInfo) ? osInfo : [osInfo];
100
+ const os = osArr[0];
101
+
102
+ const totalMB = Math.round(os.TotalVisibleMemorySize / 1024);
103
+ const freeMB = Math.round(os.FreePhysicalMemory / 1024);
104
+ const usedMB = totalMB - freeMB;
105
+ const usedPct = totalMB > 0 ? Math.round((usedMB / totalMB) * 100) : 0;
106
+
107
+ let pageFileUsageMB = 0;
108
+ try {
109
+ const pfInfo = psJson<RawPageFile | RawPageFile[]>(
110
+ `Get-CimInstance Win32_PageFileUsage | Select-Object CurrentUsage`,
111
+ );
112
+ if (pfInfo) {
113
+ const pfArr = Array.isArray(pfInfo) ? pfInfo : [pfInfo];
114
+ pageFileUsageMB = pfArr.reduce((sum, pf) => sum + (pf.CurrentUsage ?? 0), 0);
115
+ }
116
+ } catch {
117
+ // page file info is optional
118
+ }
119
+
120
+ return { totalMB, freeMB, usedPct, pageFileUsageMB };
121
+ }
122
+
123
+ async getDiskInfo(): Promise<DiskInfo[]> {
124
+ type RawDisk = { DeviceID: string; totalGB: number; freeGB: number };
125
+
126
+ const result = psJson<RawDisk | RawDisk[]>(
127
+ `Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" | Select-Object DeviceID, @{N='totalGB';E={[math]::Round($_.Size/1GB)}}, @{N='freeGB';E={[math]::Round($_.FreeSpace/1GB)}}`,
128
+ );
129
+ const arr = Array.isArray(result) ? result : [result];
130
+ return arr.map((d) => {
131
+ const usedGB = d.totalGB - d.freeGB;
132
+ const usedPct = d.totalGB > 0 ? Math.round((usedGB / d.totalGB) * 100) : 0;
133
+ return {
134
+ drive: d.DeviceID,
135
+ totalGB: d.totalGB,
136
+ freeGB: d.freeGB,
137
+ usedPct,
138
+ };
139
+ });
140
+ }
141
+
142
+ async getCpuUsage(samples: number, intervalMs: number): Promise<CpuSample[]> {
143
+ const intervalSec = Math.max(1, Math.round(intervalMs / 1000));
144
+
145
+ type RawCounter = {
146
+ Readings: string;
147
+ Timestamp: string;
148
+ };
149
+
150
+ type RawCounterSet = {
151
+ CounterSamples: Array<{ CookedValue: number; Timestamp: string }>;
152
+ };
153
+
154
+ try {
155
+ const raw = ps(
156
+ `$c = Get-Counter '\\Processor(_Total)\\% Processor Time' -SampleInterval ${intervalSec} -MaxSamples ${samples}; $c.CounterSamples | Select-Object CookedValue, Timestamp | ConvertTo-Json -Compress`,
157
+ );
158
+ if (!raw) return [];
159
+ const parsed = JSON.parse(raw) as
160
+ | { CookedValue: number; Timestamp: string }
161
+ | { CookedValue: number; Timestamp: string }[];
162
+ const arr = Array.isArray(parsed) ? parsed : [parsed];
163
+ return arr.map((s) => ({
164
+ usedPct: Math.round(s.CookedValue),
165
+ timestamp: new Date(s.Timestamp),
166
+ }));
167
+ } catch {
168
+ return [];
169
+ }
170
+ }
171
+
172
+ async getStartupItems(): Promise<StartupItem[]> {
173
+ const items: StartupItem[] = [];
174
+
175
+ // Read from registry run key
176
+ try {
177
+ const raw = ps(
178
+ `$reg = 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run'; if (Test-Path $reg) { Get-ItemProperty $reg | Select-Object * -ExcludeProperty PSPath,PSParentPath,PSChildName,PSDrive,PSProvider | ConvertTo-Json -Compress }`,
179
+ );
180
+ if (raw) {
181
+ const obj = JSON.parse(raw) as Record<string, string>;
182
+ for (const [name, command] of Object.entries(obj)) {
183
+ items.push({
184
+ name,
185
+ command,
186
+ location: 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run',
187
+ enabled: true,
188
+ });
189
+ }
190
+ }
191
+ } catch {
192
+ // registry read failed, continue
193
+ }
194
+
195
+ // Read from startup folder
196
+ try {
197
+ const startupFolder = ps(
198
+ `[Environment]::GetFolderPath('Startup')`,
199
+ );
200
+ if (startupFolder) {
201
+ const files = ps(
202
+ `$f = [Environment]::GetFolderPath('Startup'); if (Test-Path $f) { Get-ChildItem $f -File | Select-Object Name, FullName | ConvertTo-Json -Compress }`,
203
+ );
204
+ if (files) {
205
+ const parsed = JSON.parse(files) as
206
+ | { Name: string; FullName: string }
207
+ | { Name: string; FullName: string }[];
208
+ const arr = Array.isArray(parsed) ? parsed : [parsed];
209
+ for (const f of arr) {
210
+ items.push({
211
+ name: f.Name,
212
+ command: f.FullName,
213
+ location: startupFolder,
214
+ enabled: true,
215
+ });
216
+ }
217
+ }
218
+ }
219
+ } catch {
220
+ // startup folder read failed, continue
221
+ }
222
+
223
+ return items;
224
+ }
225
+
226
+ async disableStartupItem(name: string): Promise<boolean> {
227
+ try {
228
+ ps(
229
+ `Remove-ItemProperty -Path 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run' -Name '${name}' -ErrorAction Stop`,
230
+ );
231
+ return true;
232
+ } catch {
233
+ return false;
234
+ }
235
+ }
236
+
237
+ getTempDirs(): string[] {
238
+ return [process.env.TEMP, process.env.APPDATA ? process.env.APPDATA + '\\npm-cache' : undefined].filter(
239
+ (d): d is string => Boolean(d),
240
+ );
241
+ }
242
+ }
@@ -0,0 +1,67 @@
1
+ import { WatchdogPlugin, HealthCheck, FixResult, PluginConfig } from './plugin.interface.js';
2
+ import { getPlatform } from '../platform/index.js';
3
+
4
+ export class CpuMonitorPlugin implements WatchdogPlugin {
5
+ name = 'cpu-monitor';
6
+ description = 'Detect sustained high CPU usage and identify culprits';
7
+ defaultInterval = 120000;
8
+ private config: PluginConfig = {
9
+ enabled: true,
10
+ interval: 120000,
11
+ autoFix: false,
12
+ thresholds: { warningPct: 75, criticalPct: 90 },
13
+ };
14
+
15
+ configure(opts: PluginConfig): void {
16
+ this.config = { ...this.config, ...opts };
17
+ }
18
+
19
+ async check(): Promise<HealthCheck> {
20
+ const platform = getPlatform();
21
+ const samples = await platform.getCpuUsage(3, 2000);
22
+
23
+ const sampleCount = samples.length;
24
+ const cpuUsedPct =
25
+ sampleCount > 0
26
+ ? samples.reduce((sum, s) => sum + s.usedPct, 0) / sampleCount
27
+ : 0;
28
+
29
+ const warningPct = this.config.thresholds['warningPct'] ?? 75;
30
+ const criticalPct = this.config.thresholds['criticalPct'] ?? 90;
31
+
32
+ let status: HealthCheck['status'];
33
+ const suggestedActions: string[] = [];
34
+
35
+ if (cpuUsedPct >= criticalPct) {
36
+ status = 'critical';
37
+ suggestedActions.push('CPU critically high — investigate top CPU-consuming processes');
38
+ } else if (cpuUsedPct >= warningPct) {
39
+ status = 'warning';
40
+ suggestedActions.push('CPU usage elevated — monitor for sustained spikes');
41
+ } else {
42
+ status = 'healthy';
43
+ }
44
+
45
+ const message = `Average CPU usage: ${cpuUsedPct.toFixed(1)}% over ${sampleCount} sample(s)`;
46
+
47
+ return {
48
+ status,
49
+ metrics: { cpuUsedPct, sampleCount },
50
+ message,
51
+ suggestedActions,
52
+ };
53
+ }
54
+
55
+ async fix(): Promise<FixResult> {
56
+ // Alert-only: no automated process killing for CPU
57
+ const check = await this.check();
58
+ const metrics = check.metrics;
59
+
60
+ return {
61
+ actionsKept: ['CPU monitor is alert-only; no automated fix applied'],
62
+ resourcesFreed: '0 MB',
63
+ beforeMetrics: metrics,
64
+ afterMetrics: metrics,
65
+ };
66
+ }
67
+ }
@@ -0,0 +1,128 @@
1
+ import { WatchdogPlugin, HealthCheck, FixResult, PluginConfig } from './plugin.interface.js';
2
+ import { getPlatform } from '../platform/index.js';
3
+ import { readdirSync, statSync, unlinkSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+
6
+ export class DiskHealthPlugin implements WatchdogPlugin {
7
+ name = 'disk-health';
8
+ description = 'Monitor disk space, clean temporary files';
9
+ defaultInterval = 1800000;
10
+ private config: PluginConfig = {
11
+ enabled: true,
12
+ interval: 1800000,
13
+ autoFix: true,
14
+ thresholds: { warningPct: 80, criticalPct: 90, tempFileAgeDays: 7 },
15
+ };
16
+
17
+ configure(opts: PluginConfig): void {
18
+ this.config = { ...this.config, ...opts };
19
+ }
20
+
21
+ async check(): Promise<HealthCheck> {
22
+ const platform = getPlatform();
23
+ const disks = await platform.getDiskInfo();
24
+ const primary = disks[0];
25
+
26
+ if (!primary) {
27
+ return {
28
+ status: 'critical',
29
+ metrics: {},
30
+ message: 'No disk information available',
31
+ suggestedActions: [],
32
+ };
33
+ }
34
+
35
+ const diskUsedPct = primary.usedPct;
36
+ const diskFreeGB = primary.freeGB;
37
+ const diskTotalGB = primary.totalGB;
38
+
39
+ const warningPct = this.config.thresholds['warningPct'] ?? 80;
40
+ const criticalPct = this.config.thresholds['criticalPct'] ?? 90;
41
+
42
+ let status: HealthCheck['status'];
43
+ const suggestedActions: string[] = [];
44
+
45
+ if (diskUsedPct >= criticalPct) {
46
+ status = 'critical';
47
+ suggestedActions.push('Disk critically full — run fix() to clean temp files');
48
+ } else if (diskUsedPct >= warningPct) {
49
+ status = 'warning';
50
+ suggestedActions.push('Disk usage elevated — consider cleaning temp files');
51
+ } else {
52
+ status = 'healthy';
53
+ }
54
+
55
+ const message = `Disk ${primary.drive}: ${diskUsedPct.toFixed(1)}% used, ${diskFreeGB.toFixed(2)} GB free of ${diskTotalGB.toFixed(2)} GB`;
56
+
57
+ return {
58
+ status,
59
+ metrics: { diskUsedPct, diskFreeGB, diskTotalGB },
60
+ message,
61
+ suggestedActions,
62
+ };
63
+ }
64
+
65
+ async fix(): Promise<FixResult> {
66
+ const platform = getPlatform();
67
+
68
+ const disksBefore = await platform.getDiskInfo();
69
+ const primaryBefore = disksBefore[0];
70
+ const beforeMetrics: Record<string, number> = primaryBefore
71
+ ? {
72
+ diskUsedPct: primaryBefore.usedPct,
73
+ diskFreeGB: primaryBefore.freeGB,
74
+ diskTotalGB: primaryBefore.totalGB,
75
+ }
76
+ : {};
77
+
78
+ const tempDirs = platform.getTempDirs();
79
+ const tempFileAgeDays = this.config.thresholds['tempFileAgeDays'] ?? 7;
80
+ const ageThresholdMs = tempFileAgeDays * 24 * 60 * 60 * 1000;
81
+ const now = Date.now();
82
+
83
+ const actionsKept: string[] = [];
84
+ let filesDeleted = 0;
85
+
86
+ for (const dir of tempDirs) {
87
+ let entries: string[];
88
+ try {
89
+ entries = readdirSync(dir);
90
+ } catch {
91
+ continue;
92
+ }
93
+
94
+ for (const entry of entries) {
95
+ const filePath = join(dir, entry);
96
+ try {
97
+ const st = statSync(filePath);
98
+ if (st.isFile() && now - st.mtimeMs > ageThresholdMs) {
99
+ unlinkSync(filePath);
100
+ filesDeleted++;
101
+ }
102
+ } catch {
103
+ // skip files we can't stat or delete
104
+ }
105
+ }
106
+ }
107
+
108
+ if (filesDeleted > 0) {
109
+ actionsKept.push(`Deleted ${filesDeleted} temp file(s) older than ${tempFileAgeDays} days`);
110
+ }
111
+
112
+ const disksAfter = await platform.getDiskInfo();
113
+ const primaryAfter = disksAfter[0];
114
+ const afterMetrics: Record<string, number> = primaryAfter
115
+ ? {
116
+ diskUsedPct: primaryAfter.usedPct,
117
+ diskFreeGB: primaryAfter.freeGB,
118
+ diskTotalGB: primaryAfter.totalGB,
119
+ }
120
+ : {};
121
+
122
+ const freedGB =
123
+ primaryBefore && primaryAfter ? primaryAfter.freeGB - primaryBefore.freeGB : 0;
124
+ const resourcesFreed = `Freed ~${(freedGB * 1024).toFixed(0)} MB disk space`;
125
+
126
+ return { actionsKept, resourcesFreed, beforeMetrics, afterMetrics };
127
+ }
128
+ }
@@ -0,0 +1,70 @@
1
+ import { WatchdogPlugin, HealthCheck, FixResult, PluginConfig } from './plugin.interface.js';
2
+ import { getPlatform } from '../platform/index.js';
3
+
4
+ export class MemoryMonitorPlugin implements WatchdogPlugin {
5
+ name = 'memory-monitor';
6
+ description = 'Track RAM and page file usage, alert on high pressure';
7
+ defaultInterval = 120000;
8
+ private config: PluginConfig = {
9
+ enabled: true,
10
+ interval: 120000,
11
+ autoFix: false,
12
+ thresholds: { warningPct: 85, criticalPct: 95 },
13
+ };
14
+
15
+ configure(opts: PluginConfig): void {
16
+ this.config = { ...this.config, ...opts };
17
+ }
18
+
19
+ async check(): Promise<HealthCheck> {
20
+ const platform = getPlatform();
21
+ const mem = await platform.getMemoryInfo();
22
+
23
+ const ramUsedPct = mem.usedPct;
24
+ const ramFreeMB = mem.freeMB;
25
+ const ramTotalMB = mem.totalMB;
26
+ const pageFileMB = mem.pageFileUsageMB;
27
+
28
+ const metrics: Record<string, number> = {
29
+ ramUsedPct,
30
+ ramFreeMB,
31
+ ramTotalMB,
32
+ pageFileMB,
33
+ };
34
+
35
+ const warningPct = this.config.thresholds['warningPct'] ?? 85;
36
+ const criticalPct = this.config.thresholds['criticalPct'] ?? 95;
37
+
38
+ let status: HealthCheck['status'];
39
+ if (ramUsedPct >= criticalPct) {
40
+ status = 'critical';
41
+ } else if (ramUsedPct >= warningPct) {
42
+ status = 'warning';
43
+ } else {
44
+ status = 'healthy';
45
+ }
46
+
47
+ const message = `RAM: ${Math.round(ramUsedPct)}% used (${Math.round(ramFreeMB)} MB free of ${Math.round(ramTotalMB)} MB)`;
48
+
49
+ const suggestedActions: string[] = [];
50
+ if (status === 'critical') {
51
+ suggestedActions.push('RAM usage is critically high — consider closing applications or adding more memory');
52
+ } else if (status === 'warning') {
53
+ suggestedActions.push('RAM usage is elevated — monitor for further increases');
54
+ }
55
+
56
+ return { status, metrics, message, suggestedActions };
57
+ }
58
+
59
+ async fix(): Promise<FixResult> {
60
+ const result = await this.check();
61
+ const metrics = result.metrics;
62
+
63
+ return {
64
+ actionsKept: ['Alert pushed (no auto-fix for memory)'],
65
+ resourcesFreed: '0 MB (alert only)',
66
+ beforeMetrics: metrics,
67
+ afterMetrics: metrics,
68
+ };
69
+ }
70
+ }
@@ -0,0 +1,27 @@
1
+ import { WatchdogPlugin } from './plugin.interface.js';
2
+ import { WatchdogConfig } from '../config.js';
3
+ import { ProcessGuardPlugin } from './process-guard.js';
4
+ import { MemoryMonitorPlugin } from './memory-monitor.js';
5
+ import { DiskHealthPlugin } from './disk-health.js';
6
+ import { StartupOptimizerPlugin } from './startup-optimizer.js';
7
+ import { CpuMonitorPlugin } from './cpu-monitor.js';
8
+
9
+ const pluginRegistry: Record<string, new () => WatchdogPlugin> = {
10
+ 'process-guard': ProcessGuardPlugin,
11
+ 'memory-monitor': MemoryMonitorPlugin,
12
+ 'disk-health': DiskHealthPlugin,
13
+ 'startup-optimizer': StartupOptimizerPlugin,
14
+ 'cpu-monitor': CpuMonitorPlugin,
15
+ };
16
+
17
+ export function loadPlugins(config: WatchdogConfig): WatchdogPlugin[] {
18
+ const plugins: WatchdogPlugin[] = [];
19
+ for (const [name, PluginClass] of Object.entries(pluginRegistry)) {
20
+ const pluginConfig = config.plugins[name];
21
+ if (!pluginConfig || !pluginConfig.enabled) continue;
22
+ const plugin = new PluginClass();
23
+ plugin.configure(pluginConfig);
24
+ plugins.push(plugin);
25
+ }
26
+ return plugins;
27
+ }
@@ -0,0 +1,31 @@
1
+ export type HealthStatus = 'healthy' | 'warning' | 'critical';
2
+
3
+ export interface HealthCheck {
4
+ status: HealthStatus;
5
+ metrics: Record<string, number>;
6
+ message: string;
7
+ suggestedActions: string[];
8
+ }
9
+
10
+ export interface FixResult {
11
+ actionsKept: string[];
12
+ resourcesFreed: string;
13
+ beforeMetrics: Record<string, number>;
14
+ afterMetrics: Record<string, number>;
15
+ }
16
+
17
+ export interface PluginConfig {
18
+ enabled: boolean;
19
+ interval: number;
20
+ thresholds: Record<string, number>;
21
+ autoFix: boolean;
22
+ }
23
+
24
+ export interface WatchdogPlugin {
25
+ name: string;
26
+ description: string;
27
+ defaultInterval: number;
28
+ check(): Promise<HealthCheck>;
29
+ fix(): Promise<FixResult>;
30
+ configure(opts: PluginConfig): void;
31
+ }