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
package/src/config.ts ADDED
@@ -0,0 +1,78 @@
1
+ import { readFile } from 'fs/promises';
2
+ import { existsSync } from 'fs';
3
+ import { fileURLToPath } from 'url';
4
+ import { resolve, dirname, join } from 'path';
5
+ import os from 'os';
6
+
7
+ export interface WatchdogConfig {
8
+ port: number;
9
+ logLevel: string;
10
+ historyRetentionDays: number;
11
+ integrations: {
12
+ totalRecall: { enabled: boolean; url: string };
13
+ mah: { enabled: boolean; url: string };
14
+ };
15
+ plugins: Record<string, {
16
+ enabled: boolean;
17
+ interval: number;
18
+ autoFix: boolean;
19
+ thresholds: Record<string, number>;
20
+ }>;
21
+ }
22
+
23
+ type DeepPartial<T> = {
24
+ [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
25
+ };
26
+
27
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
28
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
29
+ }
30
+
31
+ function deepMerge<T extends Record<string, unknown>>(
32
+ target: T,
33
+ source: Record<string, unknown>,
34
+ ): T {
35
+ const result: Record<string, unknown> = { ...target };
36
+ for (const key of Object.keys(source)) {
37
+ const srcVal = source[key];
38
+ const tgtVal = result[key];
39
+ if (isPlainObject(srcVal) && isPlainObject(tgtVal)) {
40
+ result[key] = deepMerge(tgtVal, srcVal);
41
+ } else {
42
+ result[key] = srcVal;
43
+ }
44
+ }
45
+ return result as T;
46
+ }
47
+
48
+ const __filename = fileURLToPath(import.meta.url);
49
+ const __dirname = dirname(__filename);
50
+ // src/config.ts lives in <project-root>/src/, so project root is one level up
51
+ const PROJECT_ROOT = resolve(__dirname, '..');
52
+
53
+ export async function loadConfig(
54
+ overrides?: DeepPartial<WatchdogConfig>,
55
+ ): Promise<WatchdogConfig> {
56
+ // 1. Read config/default.json from the project root
57
+ const defaultPath = join(PROJECT_ROOT, 'config', 'default.json');
58
+ const defaultRaw = await readFile(defaultPath, 'utf-8');
59
+ let config = JSON.parse(defaultRaw) as WatchdogConfig;
60
+
61
+ // 2. Read ~/.aidev/watchdog.json if it exists
62
+ const userConfigPath = join(os.homedir(), '.aidev', 'watchdog.json');
63
+ if (existsSync(userConfigPath)) {
64
+ const userRaw = await readFile(userConfigPath, 'utf-8');
65
+ const userConfig = JSON.parse(userRaw) as Record<string, unknown>;
66
+ config = deepMerge(config as unknown as Record<string, unknown>, userConfig) as unknown as WatchdogConfig;
67
+ }
68
+
69
+ // 3. Apply caller overrides
70
+ if (overrides) {
71
+ config = deepMerge(
72
+ config as unknown as Record<string, unknown>,
73
+ overrides as Record<string, unknown>,
74
+ ) as unknown as WatchdogConfig;
75
+ }
76
+
77
+ return config;
78
+ }
package/src/index.ts ADDED
@@ -0,0 +1,228 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { mkdirSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+
6
+ import { loadConfig } from './config.js';
7
+ import { loadPlugins } from './plugins/plugin-loader.js';
8
+ import { Scheduler } from './scheduler.js';
9
+ import { HistoryStore } from './store/history.js';
10
+
11
+ const program = new Command();
12
+
13
+ program
14
+ .name('watchdog')
15
+ .description('AI Dev process watchdog — monitor and auto-fix system health')
16
+ .version('1.0.0');
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // watchdog start
20
+ // ---------------------------------------------------------------------------
21
+ program
22
+ .command('start')
23
+ .description('Load config, start scheduler, and watch continuously')
24
+ .action(async () => {
25
+ const config = await loadConfig();
26
+ const plugins = loadPlugins(config);
27
+
28
+ const home = process.env.HOME || process.env.USERPROFILE || '.';
29
+ const aidevDir = join(home, '.aidev');
30
+ mkdirSync(aidevDir, { recursive: true });
31
+ const dbPath = join(aidevDir, 'watchdog.db');
32
+
33
+ const store = new HistoryStore(dbPath);
34
+
35
+ const autoFix: Record<string, boolean> = {};
36
+ for (const [name, cfg] of Object.entries(config.plugins)) {
37
+ autoFix[name] = cfg.autoFix;
38
+ }
39
+
40
+ const scheduler = new Scheduler(plugins, { autoFix });
41
+ scheduler.start();
42
+
43
+ console.log(`[watchdog] Started with ${plugins.length} plugin(s). Press Ctrl+C to stop.`);
44
+
45
+ process.on('SIGINT', () => {
46
+ console.log('\n[watchdog] Stopping…');
47
+ scheduler.stop();
48
+ store.close();
49
+ process.exit(0);
50
+ });
51
+ });
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // watchdog status
55
+ // ---------------------------------------------------------------------------
56
+ program
57
+ .command('status')
58
+ .description('Run check() on each plugin and print a summary line')
59
+ .action(async () => {
60
+ const config = await loadConfig();
61
+ const plugins = loadPlugins(config);
62
+
63
+ for (const plugin of plugins) {
64
+ try {
65
+ const result = await plugin.check();
66
+ const label =
67
+ result.status === 'healthy'
68
+ ? 'OK'
69
+ : result.status === 'warning'
70
+ ? 'WARN'
71
+ : 'CRIT';
72
+ console.log(`[${label}] ${plugin.name}: ${result.message}`);
73
+ } catch (err) {
74
+ console.error(`[ERR] ${plugin.name}: ${(err as Error).message}`);
75
+ }
76
+ }
77
+ });
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // watchdog check [plugin]
81
+ // ---------------------------------------------------------------------------
82
+ program
83
+ .command('check [plugin]')
84
+ .description('Run check() for all or a specific plugin and print detailed metrics')
85
+ .action(async (pluginArg?: string) => {
86
+ const config = await loadConfig();
87
+ const plugins = loadPlugins(config);
88
+
89
+ const targets = pluginArg
90
+ ? plugins.filter((p) => p.name === pluginArg)
91
+ : plugins;
92
+
93
+ if (pluginArg && targets.length === 0) {
94
+ console.error(`No plugin found with name "${pluginArg}"`);
95
+ process.exit(1);
96
+ }
97
+
98
+ for (const plugin of targets) {
99
+ try {
100
+ const result = await plugin.check();
101
+ console.log(`\n--- ${plugin.name} ---`);
102
+ console.log(`Status : ${result.status}`);
103
+ console.log(`Message: ${result.message}`);
104
+ console.log('Metrics:');
105
+ for (const [key, value] of Object.entries(result.metrics)) {
106
+ console.log(` ${key}: ${value}`);
107
+ }
108
+ if (result.suggestedActions.length > 0) {
109
+ console.log('Suggested actions:');
110
+ for (const action of result.suggestedActions) {
111
+ console.log(` - ${action}`);
112
+ }
113
+ }
114
+ } catch (err) {
115
+ console.error(`[ERR] ${plugin.name}: ${(err as Error).message}`);
116
+ }
117
+ }
118
+ });
119
+
120
+ // ---------------------------------------------------------------------------
121
+ // watchdog fix [plugin]
122
+ // ---------------------------------------------------------------------------
123
+ program
124
+ .command('fix [plugin]')
125
+ .description('Run fix() for all or a specific plugin and print actions and resources freed')
126
+ .action(async (pluginArg?: string) => {
127
+ const config = await loadConfig();
128
+ const plugins = loadPlugins(config);
129
+
130
+ const targets = pluginArg
131
+ ? plugins.filter((p) => p.name === pluginArg)
132
+ : plugins;
133
+
134
+ if (pluginArg && targets.length === 0) {
135
+ console.error(`No plugin found with name "${pluginArg}"`);
136
+ process.exit(1);
137
+ }
138
+
139
+ for (const plugin of targets) {
140
+ try {
141
+ const result = await plugin.fix();
142
+ console.log(`\n--- ${plugin.name} ---`);
143
+ console.log(`Resources freed: ${result.resourcesFreed}`);
144
+ if (result.actionsKept.length > 0) {
145
+ console.log('Actions taken:');
146
+ for (const action of result.actionsKept) {
147
+ console.log(` - ${action}`);
148
+ }
149
+ }
150
+ console.log('Before metrics:');
151
+ for (const [key, value] of Object.entries(result.beforeMetrics)) {
152
+ console.log(` ${key}: ${value}`);
153
+ }
154
+ console.log('After metrics:');
155
+ for (const [key, value] of Object.entries(result.afterMetrics)) {
156
+ console.log(` ${key}: ${value}`);
157
+ }
158
+ } catch (err) {
159
+ console.error(`[ERR] ${plugin.name}: ${(err as Error).message}`);
160
+ }
161
+ }
162
+ });
163
+
164
+ // ---------------------------------------------------------------------------
165
+ // watchdog api start
166
+ // ---------------------------------------------------------------------------
167
+ const apiCmd = program.command('api').description('API server commands');
168
+
169
+ apiCmd
170
+ .command('start')
171
+ .description('Start the HTTP API server')
172
+ .action(async () => {
173
+ const config = await loadConfig();
174
+ const plugins = loadPlugins(config);
175
+
176
+ const home = process.env.HOME || process.env.USERPROFILE || '.';
177
+ const aidevDir = join(home, '.aidev');
178
+ mkdirSync(aidevDir, { recursive: true });
179
+ const dbPath = join(aidevDir, 'watchdog.db');
180
+
181
+ const store = new HistoryStore(dbPath);
182
+
183
+ // Dynamic import so that missing api/server.ts does not break the whole CLI at load time
184
+ const { startServer } = await import('./api/server.js');
185
+ startServer(store, plugins, config);
186
+
187
+ console.log(`[watchdog] API server started on port ${config.port}. Press Ctrl+C to stop.`);
188
+
189
+ process.on('SIGINT', () => {
190
+ console.log('\n[watchdog] Shutting down API server…');
191
+ store.close();
192
+ process.exit(0);
193
+ });
194
+ });
195
+
196
+ // ---------------------------------------------------------------------------
197
+ // watchdog install-service
198
+ // ---------------------------------------------------------------------------
199
+ program
200
+ .command('install-service')
201
+ .description('Install watchdog as a system service')
202
+ .action(async () => {
203
+ try {
204
+ const { installService } = await import('./installer/service.js');
205
+ await installService();
206
+ } catch (err) {
207
+ console.error('[watchdog] install-service failed:', (err as Error).message);
208
+ process.exit(1);
209
+ }
210
+ });
211
+
212
+ // ---------------------------------------------------------------------------
213
+ // watchdog uninstall-service
214
+ // ---------------------------------------------------------------------------
215
+ program
216
+ .command('uninstall-service')
217
+ .description('Uninstall the watchdog system service')
218
+ .action(async () => {
219
+ try {
220
+ const { uninstallService } = await import('./installer/service.js');
221
+ await uninstallService();
222
+ } catch (err) {
223
+ console.error('[watchdog] uninstall-service failed:', (err as Error).message);
224
+ process.exit(1);
225
+ }
226
+ });
227
+
228
+ program.parseAsync(process.argv);
@@ -0,0 +1,50 @@
1
+ import { join } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+
4
+ const __dirname = fileURLToPath(new URL('.', import.meta.url));
5
+
6
+ export async function installService(): Promise<void> {
7
+ if (process.platform !== 'win32') {
8
+ console.error('Service installation currently supports Windows only.');
9
+ process.exit(1);
10
+ }
11
+
12
+ const { Service } = await import('node-windows');
13
+ const svc = new Service({
14
+ name: 'ProcessWatchdog',
15
+ description: 'Process Watchdog — PC health monitoring agent',
16
+ script: join(__dirname, '..', 'index.js'),
17
+ nodeOptions: [],
18
+ scriptOptions: 'start',
19
+ } as ConstructorParameters<typeof Service>[0]);
20
+
21
+ svc.on('install', () => {
22
+ svc.start();
23
+ console.log('[watchdog] Service installed and started');
24
+ });
25
+
26
+ svc.on('alreadyinstalled', () => {
27
+ console.log('[watchdog] Service already installed');
28
+ });
29
+
30
+ svc.install();
31
+ }
32
+
33
+ export async function uninstallService(): Promise<void> {
34
+ if (process.platform !== 'win32') {
35
+ console.error('Service uninstallation currently supports Windows only.');
36
+ process.exit(1);
37
+ }
38
+
39
+ const { Service } = await import('node-windows');
40
+ const svc = new Service({
41
+ name: 'ProcessWatchdog',
42
+ script: join(__dirname, '..', 'index.js'),
43
+ });
44
+
45
+ svc.on('uninstall', () => {
46
+ console.log('[watchdog] Service uninstalled');
47
+ });
48
+
49
+ svc.uninstall();
50
+ }
@@ -0,0 +1,22 @@
1
+ import { WatchdogConfig } from '../config.js';
2
+
3
+ export async function registerProduct(config: WatchdogConfig): Promise<boolean> {
4
+ if (!config.integrations.mah.enabled) return false;
5
+
6
+ try {
7
+ const response = await fetch(`${config.integrations.mah.url}/api/v1/products`, {
8
+ method: 'POST',
9
+ headers: { 'Content-Type': 'application/json' },
10
+ body: JSON.stringify({
11
+ name: 'Process Watchdog',
12
+ runtime: 'mah-native',
13
+ description: 'Modular PC health agent with plugin architecture',
14
+ port: config.port,
15
+ healthEndpoint: `http://localhost:${config.port}/api/v1/health`,
16
+ }),
17
+ });
18
+ return response.ok;
19
+ } catch {
20
+ return false;
21
+ }
22
+ }
@@ -0,0 +1,27 @@
1
+ import { WatchdogConfig } from '../config.js';
2
+
3
+ export async function pushAlert(
4
+ config: WatchdogConfig,
5
+ plugin: string,
6
+ message: string,
7
+ severity: 'info' | 'warning' | 'critical',
8
+ ): Promise<boolean> {
9
+ if (!config.integrations.totalRecall.enabled) return false;
10
+
11
+ try {
12
+ const response = await fetch(`${config.integrations.totalRecall.url}/broadcast`, {
13
+ method: 'POST',
14
+ headers: { 'Content-Type': 'application/json' },
15
+ body: JSON.stringify({
16
+ source: 'process-watchdog',
17
+ plugin,
18
+ message,
19
+ severity,
20
+ timestamp: new Date().toISOString(),
21
+ }),
22
+ });
23
+ return response.ok;
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
@@ -0,0 +1,13 @@
1
+ import { PlatformAdapter } from './platform.interface.js';
2
+ import { WindowsPlatform } from './windows.js';
3
+
4
+ export function getPlatform(): PlatformAdapter {
5
+ switch (process.platform) {
6
+ case 'win32':
7
+ return new WindowsPlatform();
8
+ default:
9
+ throw new Error(`Unsupported platform: ${process.platform}. Windows support only in v1.`);
10
+ }
11
+ }
12
+
13
+ export type { PlatformAdapter, ProcessInfo, MemoryInfo, DiskInfo, CpuSample, StartupItem } from './platform.interface.js';
@@ -0,0 +1,46 @@
1
+ export interface ProcessInfo {
2
+ pid: number;
3
+ name: string;
4
+ memoryMB: number;
5
+ cpu: number;
6
+ responding: boolean;
7
+ }
8
+
9
+ export interface MemoryInfo {
10
+ totalMB: number;
11
+ freeMB: number;
12
+ usedPct: number;
13
+ pageFileUsageMB: number;
14
+ }
15
+
16
+ export interface DiskInfo {
17
+ drive: string;
18
+ totalGB: number;
19
+ freeGB: number;
20
+ usedPct: number;
21
+ }
22
+
23
+ export interface CpuSample {
24
+ usedPct: number;
25
+ timestamp: Date;
26
+ }
27
+
28
+ export interface StartupItem {
29
+ name: string;
30
+ command: string;
31
+ location: string;
32
+ enabled: boolean;
33
+ }
34
+
35
+ export interface PlatformAdapter {
36
+ getProcesses(): Promise<ProcessInfo[]>;
37
+ getProcessesByName(name: string): Promise<ProcessInfo[]>;
38
+ getListeningPids(): Promise<number[]>;
39
+ killProcess(pid: number): Promise<boolean>;
40
+ getMemoryInfo(): Promise<MemoryInfo>;
41
+ getDiskInfo(): Promise<DiskInfo[]>;
42
+ getCpuUsage(samples: number, intervalMs: number): Promise<CpuSample[]>;
43
+ getStartupItems(): Promise<StartupItem[]>;
44
+ disableStartupItem(name: string): Promise<boolean>;
45
+ getTempDirs(): string[];
46
+ }