process-watchdog 1.0.0 → 1.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.
Files changed (40) hide show
  1. package/README.md +115 -0
  2. package/dashboard/watchdog.html +406 -0
  3. package/dist/api/routes.d.ts.map +1 -1
  4. package/dist/api/routes.js +45 -0
  5. package/dist/api/routes.js.map +1 -1
  6. package/dist/index.js +5 -5
  7. package/dist/index.js.map +1 -1
  8. package/dist/platform/index.d.ts.map +1 -1
  9. package/dist/platform/index.js +7 -1
  10. package/dist/platform/index.js.map +1 -1
  11. package/dist/platform/linux.d.ts +14 -0
  12. package/dist/platform/linux.d.ts.map +1 -0
  13. package/dist/platform/linux.js +235 -0
  14. package/dist/platform/linux.js.map +1 -0
  15. package/dist/platform/macos.d.ts +14 -0
  16. package/dist/platform/macos.d.ts.map +1 -0
  17. package/dist/platform/macos.js +255 -0
  18. package/dist/platform/macos.js.map +1 -0
  19. package/dist/plugins/plugin-loader.d.ts +1 -1
  20. package/dist/plugins/plugin-loader.d.ts.map +1 -1
  21. package/dist/plugins/plugin-loader.js +46 -1
  22. package/dist/plugins/plugin-loader.js.map +1 -1
  23. package/dist/plugins/process-guard.d.ts.map +1 -1
  24. package/dist/plugins/process-guard.js +0 -15
  25. package/dist/plugins/process-guard.js.map +1 -1
  26. package/package.json +1 -1
  27. package/src/api/routes.ts +56 -0
  28. package/src/index.ts +5 -5
  29. package/src/platform/index.ts +7 -1
  30. package/src/platform/linux.ts +255 -0
  31. package/src/platform/macos.ts +259 -0
  32. package/src/plugins/plugin-loader.ts +66 -1
  33. package/src/plugins/process-guard.ts +0 -15
  34. package/tests/plugins/cpu-monitor.test.ts +5 -5
  35. package/tests/plugins/disk-health.test.ts +1 -1
  36. package/tests/plugins/fixtures/broken-plugin.js +5 -0
  37. package/tests/plugins/fixtures/custom-plugin.js +36 -0
  38. package/tests/plugins/fixtures/named-plugin.js +31 -0
  39. package/tests/plugins/plugin-loader.test.ts +124 -7
  40. package/tests/plugins/process-guard.test.ts +1 -1
@@ -1,3 +1,7 @@
1
+ import { readdirSync } from 'node:fs';
2
+ import { existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import os from 'node:os';
1
5
  import { ProcessGuardPlugin } from './process-guard.js';
2
6
  import { MemoryMonitorPlugin } from './memory-monitor.js';
3
7
  import { DiskHealthPlugin } from './disk-health.js';
@@ -10,8 +14,9 @@ const pluginRegistry = {
10
14
  'startup-optimizer': StartupOptimizerPlugin,
11
15
  'cpu-monitor': CpuMonitorPlugin,
12
16
  };
13
- export function loadPlugins(config) {
17
+ export async function loadPlugins(config, customPluginDir) {
14
18
  const plugins = [];
19
+ // Load built-in plugins from the registry
15
20
  for (const [name, PluginClass] of Object.entries(pluginRegistry)) {
16
21
  const pluginConfig = config.plugins[name];
17
22
  if (!pluginConfig || !pluginConfig.enabled)
@@ -20,6 +25,46 @@ export function loadPlugins(config) {
20
25
  plugin.configure(pluginConfig);
21
26
  plugins.push(plugin);
22
27
  }
28
+ // Load custom plugins from ~/.aidev/watchdog-plugins/ (or override for testing)
29
+ const resolvedCustomDir = customPluginDir ?? join(os.homedir(), '.aidev', 'watchdog-plugins');
30
+ if (existsSync(resolvedCustomDir)) {
31
+ let entries;
32
+ try {
33
+ entries = readdirSync(resolvedCustomDir).filter((f) => f.endsWith('.js'));
34
+ }
35
+ catch {
36
+ entries = [];
37
+ }
38
+ for (const filename of entries) {
39
+ const filePath = join(resolvedCustomDir, filename);
40
+ try {
41
+ // Dynamic import to support both `export default` and `export { Plugin }`
42
+ const mod = await import(filePath);
43
+ const PluginClass = mod.default ?? mod.Plugin;
44
+ if (typeof PluginClass !== 'function') {
45
+ console.warn(`[watchdog] Custom plugin "${filename}" has no default or Plugin export — skipping.`);
46
+ continue;
47
+ }
48
+ const plugin = new PluginClass();
49
+ if (typeof plugin.name !== 'string' ||
50
+ typeof plugin.check !== 'function' ||
51
+ typeof plugin.fix !== 'function' ||
52
+ typeof plugin.configure !== 'function') {
53
+ console.warn(`[watchdog] Custom plugin "${filename}" does not implement WatchdogPlugin — skipping.`);
54
+ continue;
55
+ }
56
+ const pluginConfig = config.plugins[plugin.name];
57
+ if (pluginConfig) {
58
+ plugin.configure(pluginConfig);
59
+ }
60
+ plugins.push(plugin);
61
+ console.log(`[watchdog] Loaded custom plugin: ${plugin.name} (${filename})`);
62
+ }
63
+ catch (err) {
64
+ console.error(`[watchdog] Failed to load custom plugin "${filename}": ${err.message}`);
65
+ }
66
+ }
67
+ }
23
68
  return plugins;
24
69
  }
25
70
  //# sourceMappingURL=plugin-loader.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"plugin-loader.js","sourceRoot":"","sources":["../../src/plugins/plugin-loader.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEpD,MAAM,cAAc,GAA6C;IAC/D,eAAe,EAAE,kBAAkB;IACnC,gBAAgB,EAAE,mBAAmB;IACrC,aAAa,EAAE,gBAAgB;IAC/B,mBAAmB,EAAE,sBAAsB;IAC3C,aAAa,EAAE,gBAAgB;CAChC,CAAC;AAEF,MAAM,UAAU,WAAW,CAAC,MAAsB;IAChD,MAAM,OAAO,GAAqB,EAAE,CAAC;IACrC,KAAK,MAAM,CAAC,IAAI,EAAE,WAAW,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,cAAc,CAAC,EAAE,CAAC;QACjE,MAAM,YAAY,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC1C,IAAI,CAAC,YAAY,IAAI,CAAC,YAAY,CAAC,OAAO;YAAE,SAAS;QACrD,MAAM,MAAM,GAAG,IAAI,WAAW,EAAE,CAAC;QACjC,MAAM,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC;QAC/B,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACvB,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
1
+ {"version":3,"file":"plugin-loader.js","sourceRoot":"","sources":["../../src/plugins/plugin-loader.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"process-guard.d.ts","sourceRoot":"","sources":["../../src/plugins/process-guard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAe7F,qBAAa,kBAAmB,YAAW,cAAc;IACvD,IAAI,SAAmB;IACvB,WAAW,SAA+D;IAC1E,eAAe,SAAU;IACzB,OAAO,CAAC,MAAM,CAKZ;IAEF,SAAS,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI;IAI7B,KAAK,IAAI,OAAO,CAAC,WAAW,CAAC;IAwD7B,GAAG,IAAI,OAAO,CAAC,SAAS,CAAC;CA8EhC"}
1
+ {"version":3,"file":"process-guard.d.ts","sourceRoot":"","sources":["../../src/plugins/process-guard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAe7F,qBAAa,kBAAmB,YAAW,cAAc;IACvD,IAAI,SAAmB;IACvB,WAAW,SAA+D;IAC1E,eAAe,SAAU;IACzB,OAAO,CAAC,MAAM,CAKZ;IAEF,SAAS,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI;IAI7B,KAAK,IAAI,OAAO,CAAC,WAAW,CAAC;IAwD7B,GAAG,IAAI,OAAO,CAAC,SAAS,CAAC;CA+DhC"}
@@ -103,21 +103,6 @@ export class ProcessGuardPlugin {
103
103
  actionsKept.push(`Killed ${killed}/${orphans.length} orphaned ${name} processes`);
104
104
  }
105
105
  }
106
- // Best-effort: clean up low-CPU conhost processes (skip any that were running at snapshot time)
107
- try {
108
- const conhosts = await safeGetProcessesByName(platform, 'conhost');
109
- for (const proc of conhosts) {
110
- if (proc.cpu < 0.1 && !protectedPids.has(proc.pid)) {
111
- const success = await platform.killProcess(proc.pid);
112
- if (success) {
113
- totalMemoryFreedMB += proc.memoryMB ?? 0;
114
- }
115
- }
116
- }
117
- }
118
- catch {
119
- // best-effort, ignore errors
120
- }
121
106
  // Capture after metrics (best-effort — system may be in transition after kills)
122
107
  let afterListeningPids;
123
108
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"process-guard.js","sourceRoot":"","sources":["../../src/plugins/process-guard.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"process-guard.js","sourceRoot":"","sources":["../../src/plugins/process-guard.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "process-watchdog",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Modular PC health agent for the aidev.com.au ecosystem",
5
5
  "type": "module",
6
6
  "bin": {
package/src/api/routes.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  import { Router } from 'express';
2
+ import { writeFile, mkdir } from 'fs/promises';
3
+ import { join } from 'path';
4
+ import os from 'os';
2
5
  import { HistoryStore } from '../store/history.js';
3
6
  import { WatchdogPlugin, HealthStatus } from '../plugins/plugin.interface.js';
4
7
  import { WatchdogConfig } from '../config.js';
@@ -119,5 +122,58 @@ export function createRoutes(
119
122
  res.json(config);
120
123
  });
121
124
 
125
+ // PUT /config — deep-merge partial config and persist to ~/.aidev/watchdog.json
126
+ router.put('/config', async (req, res) => {
127
+ const body = req.body;
128
+ if (!body || typeof body !== 'object' || Array.isArray(body)) {
129
+ res.status(400).json({ error: 'Request body must be a JSON object' });
130
+ return;
131
+ }
132
+
133
+ try {
134
+ // Deep-merge the incoming partial config into the live config object
135
+ function isPlainObject(v: unknown): v is Record<string, unknown> {
136
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
137
+ }
138
+
139
+ function deepMerge(
140
+ target: Record<string, unknown>,
141
+ source: Record<string, unknown>,
142
+ ): Record<string, unknown> {
143
+ const result: Record<string, unknown> = { ...target };
144
+ for (const key of Object.keys(source)) {
145
+ const src = source[key];
146
+ const tgt = result[key];
147
+ if (isPlainObject(src) && isPlainObject(tgt)) {
148
+ result[key] = deepMerge(tgt, src);
149
+ } else {
150
+ result[key] = src;
151
+ }
152
+ }
153
+ return result;
154
+ }
155
+
156
+ const merged = deepMerge(
157
+ config as unknown as Record<string, unknown>,
158
+ body as Record<string, unknown>,
159
+ ) as unknown as WatchdogConfig;
160
+
161
+ // Apply the merged values back onto the live config object so GET /config
162
+ // reflects the change immediately (without restart)
163
+ Object.assign(config, merged);
164
+
165
+ // Persist user overrides to ~/.aidev/watchdog.json
166
+ const aidevDir = join(os.homedir(), '.aidev');
167
+ await mkdir(aidevDir, { recursive: true });
168
+ const userConfigPath = join(aidevDir, 'watchdog.json');
169
+ await writeFile(userConfigPath, JSON.stringify(body, null, 2), 'utf-8');
170
+
171
+ res.json(config);
172
+ } catch (err) {
173
+ const message = err instanceof Error ? err.message : String(err);
174
+ res.status(500).json({ error: message });
175
+ }
176
+ });
177
+
122
178
  return router;
123
179
  }
package/src/index.ts CHANGED
@@ -23,7 +23,7 @@ program
23
23
  .description('Load config, start scheduler, and watch continuously')
24
24
  .action(async () => {
25
25
  const config = await loadConfig();
26
- const plugins = loadPlugins(config);
26
+ const plugins = await loadPlugins(config);
27
27
 
28
28
  const home = process.env.HOME || process.env.USERPROFILE || '.';
29
29
  const aidevDir = join(home, '.aidev');
@@ -58,7 +58,7 @@ program
58
58
  .description('Run check() on each plugin and print a summary line')
59
59
  .action(async () => {
60
60
  const config = await loadConfig();
61
- const plugins = loadPlugins(config);
61
+ const plugins = await loadPlugins(config);
62
62
 
63
63
  for (const plugin of plugins) {
64
64
  try {
@@ -84,7 +84,7 @@ program
84
84
  .description('Run check() for all or a specific plugin and print detailed metrics')
85
85
  .action(async (pluginArg?: string) => {
86
86
  const config = await loadConfig();
87
- const plugins = loadPlugins(config);
87
+ const plugins = await loadPlugins(config);
88
88
 
89
89
  const targets = pluginArg
90
90
  ? plugins.filter((p) => p.name === pluginArg)
@@ -125,7 +125,7 @@ program
125
125
  .description('Run fix() for all or a specific plugin and print actions and resources freed')
126
126
  .action(async (pluginArg?: string) => {
127
127
  const config = await loadConfig();
128
- const plugins = loadPlugins(config);
128
+ const plugins = await loadPlugins(config);
129
129
 
130
130
  const targets = pluginArg
131
131
  ? plugins.filter((p) => p.name === pluginArg)
@@ -171,7 +171,7 @@ apiCmd
171
171
  .description('Start the HTTP API server')
172
172
  .action(async () => {
173
173
  const config = await loadConfig();
174
- const plugins = loadPlugins(config);
174
+ const plugins = await loadPlugins(config);
175
175
 
176
176
  const home = process.env.HOME || process.env.USERPROFILE || '.';
177
177
  const aidevDir = join(home, '.aidev');
@@ -1,12 +1,18 @@
1
1
  import { PlatformAdapter } from './platform.interface.js';
2
2
  import { WindowsPlatform } from './windows.js';
3
+ import { MacOSPlatform } from './macos.js';
4
+ import { LinuxPlatform } from './linux.js';
3
5
 
4
6
  export function getPlatform(): PlatformAdapter {
5
7
  switch (process.platform) {
6
8
  case 'win32':
7
9
  return new WindowsPlatform();
10
+ case 'darwin':
11
+ return new MacOSPlatform();
12
+ case 'linux':
13
+ return new LinuxPlatform();
8
14
  default:
9
- throw new Error(`Unsupported platform: ${process.platform}. Windows support only in v1.`);
15
+ throw new Error(`Unsupported platform: ${process.platform}`);
10
16
  }
11
17
  }
12
18
 
@@ -0,0 +1,255 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import * as fs from 'node:fs';
3
+ import * as os from 'node:os';
4
+ import * as path from 'node:path';
5
+ import type {
6
+ PlatformAdapter,
7
+ ProcessInfo,
8
+ MemoryInfo,
9
+ DiskInfo,
10
+ CpuSample,
11
+ StartupItem,
12
+ } from './platform.interface.js';
13
+
14
+ function run(cmd: string, args: string[], timeoutMs = 60000): string {
15
+ return execFileSync(cmd, args, { encoding: 'utf-8', timeout: timeoutMs }).trim();
16
+ }
17
+
18
+ /** Read /proc/stat and return raw CPU counters for all CPUs combined (first line). */
19
+ function readProcStatCpu(): {
20
+ user: number;
21
+ nice: number;
22
+ system: number;
23
+ idle: number;
24
+ iowait: number;
25
+ irq: number;
26
+ softirq: number;
27
+ total: number;
28
+ } {
29
+ const stat = fs.readFileSync('/proc/stat', 'utf-8');
30
+ const cpuLine = stat.split('\n').find((l) => l.startsWith('cpu '));
31
+ if (!cpuLine) throw new Error('Cannot parse /proc/stat');
32
+ // cpu user nice system idle iowait irq softirq steal guest guest_nice
33
+ const parts = cpuLine.trim().split(/\s+/).slice(1).map(Number);
34
+ const [user = 0, nice = 0, system = 0, idle = 0, iowait = 0, irq = 0, softirq = 0] = parts;
35
+ const total = parts.reduce((a, b) => a + b, 0);
36
+ return { user, nice, system, idle, iowait, irq, softirq, total };
37
+ }
38
+
39
+ export class LinuxPlatform implements PlatformAdapter {
40
+ async getProcesses(): Promise<ProcessInfo[]> {
41
+ // ps aux columns: USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
42
+ const raw = run('ps', ['aux']);
43
+ const lines = raw.split('\n').slice(1); // skip header
44
+ const result: ProcessInfo[] = [];
45
+ for (const line of lines) {
46
+ if (!line.trim()) continue;
47
+ const parts = line.trim().split(/\s+/);
48
+ if (parts.length < 11) continue;
49
+ const pid = parseInt(parts[1], 10);
50
+ const cpuPct = parseFloat(parts[2]) || 0;
51
+ const rssKB = parseInt(parts[5], 10) || 0;
52
+ const name = parts.slice(10).join(' ');
53
+ if (isNaN(pid)) continue;
54
+ result.push({
55
+ pid,
56
+ name,
57
+ memoryMB: Math.round(rssKB / 1024),
58
+ cpu: cpuPct,
59
+ responding: true,
60
+ });
61
+ }
62
+ return result;
63
+ }
64
+
65
+ async getProcessesByName(name: string): Promise<ProcessInfo[]> {
66
+ // pgrep -a <name> => "<pid> <full command>"
67
+ let pgrepOutput = '';
68
+ try {
69
+ pgrepOutput = run('pgrep', ['-a', name]);
70
+ } catch {
71
+ // pgrep exits non-zero when no matches
72
+ return [];
73
+ }
74
+
75
+ const pids: number[] = [];
76
+ for (const line of pgrepOutput.split('\n')) {
77
+ if (!line.trim()) continue;
78
+ const pid = parseInt(line.trim().split(/\s+/)[0], 10);
79
+ if (!isNaN(pid)) pids.push(pid);
80
+ }
81
+ if (pids.length === 0) return [];
82
+
83
+ const allProcs = await this.getProcesses();
84
+ return allProcs.filter((p) => pids.includes(p.pid));
85
+ }
86
+
87
+ async getListeningPids(): Promise<number[]> {
88
+ // ss -tlnp — shows TCP listening sockets with process info
89
+ // State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
90
+ // LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=1234,fd=3))
91
+ let raw = '';
92
+ try {
93
+ raw = run('ss', ['-tlnp']);
94
+ } catch {
95
+ return [];
96
+ }
97
+ const pids = new Set<number>();
98
+ for (const line of raw.split('\n').slice(1)) {
99
+ // Extract pid=NNNN from users:((...,pid=NNNN,...))
100
+ const matches = line.matchAll(/pid=(\d+)/g);
101
+ for (const m of matches) {
102
+ const pid = parseInt(m[1], 10);
103
+ if (!isNaN(pid)) pids.add(pid);
104
+ }
105
+ }
106
+ return Array.from(pids);
107
+ }
108
+
109
+ async killProcess(pid: number): Promise<boolean> {
110
+ try {
111
+ run('kill', ['-9', String(pid)]);
112
+ return true;
113
+ } catch {
114
+ return false;
115
+ }
116
+ }
117
+
118
+ async getMemoryInfo(): Promise<MemoryInfo> {
119
+ // /proc/meminfo keys are in kB
120
+ const raw = fs.readFileSync('/proc/meminfo', 'utf-8');
121
+ const getValue = (key: string): number => {
122
+ const match = raw.match(new RegExp(`^${key}:\\s+(\\d+)`, 'm'));
123
+ return match ? parseInt(match[1], 10) : 0;
124
+ };
125
+
126
+ const totalKB = getValue('MemTotal');
127
+ const freeKB = getValue('MemFree');
128
+ const buffersKB = getValue('Buffers');
129
+ const cachedKB = getValue('Cached'); // includes "Cached" but not "SwapCached"
130
+ const sreclaimKB = getValue('SReclaimable');
131
+ const swapTotalKB = getValue('SwapTotal');
132
+ const swapFreeKB = getValue('SwapFree');
133
+
134
+ const totalMB = Math.round(totalKB / 1024);
135
+ // Available memory = free + buffers + cached (page cache) + slab reclaimable
136
+ const availableKB = freeKB + buffersKB + cachedKB + sreclaimKB;
137
+ const freeMB = Math.round(availableKB / 1024);
138
+ const usedMB = totalMB - freeMB;
139
+ const usedPct = totalMB > 0 ? Math.round((usedMB / totalMB) * 100) : 0;
140
+ const pageFileUsageMB = Math.round((swapTotalKB - swapFreeKB) / 1024);
141
+
142
+ return { totalMB, freeMB, usedPct, pageFileUsageMB };
143
+ }
144
+
145
+ async getDiskInfo(): Promise<DiskInfo[]> {
146
+ // df --block-size=G produces sizes in gigabytes (with trailing G suffix)
147
+ let raw = '';
148
+ try {
149
+ raw = run('df', ['--block-size=G']);
150
+ } catch {
151
+ return [];
152
+ }
153
+ const result: DiskInfo[] = [];
154
+ for (const line of raw.split('\n').slice(1)) {
155
+ if (!line.trim()) continue;
156
+ const parts = line.trim().split(/\s+/);
157
+ if (parts.length < 6) continue;
158
+ // Strip the trailing "G" added by --block-size=G
159
+ const totalGB = parseInt(parts[1].replace('G', ''), 10);
160
+ const usedGB = parseInt(parts[2].replace('G', ''), 10);
161
+ const freeGB = parseInt(parts[3].replace('G', ''), 10);
162
+ const drive = parts[0];
163
+ if (isNaN(totalGB) || totalGB <= 0) continue;
164
+ const usedPct = Math.round((usedGB / totalGB) * 100);
165
+ result.push({ drive, totalGB, freeGB, usedPct });
166
+ }
167
+ return result;
168
+ }
169
+
170
+ async getCpuUsage(samples: number, intervalMs: number): Promise<CpuSample[]> {
171
+ const result: CpuSample[] = [];
172
+ const count = Math.max(1, samples);
173
+
174
+ let prev = readProcStatCpu();
175
+
176
+ for (let i = 0; i < count; i++) {
177
+ await new Promise<void>((resolve) => setTimeout(resolve, intervalMs));
178
+
179
+ try {
180
+ const curr = readProcStatCpu();
181
+ const totalDelta = curr.total - prev.total;
182
+ const idleDelta = curr.idle - prev.idle + (curr.iowait - prev.iowait);
183
+ const usedPct =
184
+ totalDelta > 0 ? Math.round(((totalDelta - idleDelta) / totalDelta) * 100) : 0;
185
+ result.push({ usedPct, timestamp: new Date() });
186
+ prev = curr;
187
+ } catch {
188
+ result.push({ usedPct: 0, timestamp: new Date() });
189
+ }
190
+ }
191
+ return result;
192
+ }
193
+
194
+ async getStartupItems(): Promise<StartupItem[]> {
195
+ const autostartDir = path.join(os.homedir(), '.config', 'autostart');
196
+ const items: StartupItem[] = [];
197
+
198
+ let files: string[] = [];
199
+ try {
200
+ files = fs.readdirSync(autostartDir).filter((f) => f.endsWith('.desktop'));
201
+ } catch {
202
+ return items;
203
+ }
204
+
205
+ for (const file of files) {
206
+ const fullPath = path.join(autostartDir, file);
207
+ let name = path.basename(file, '.desktop');
208
+ let command = '';
209
+ let enabled = true;
210
+
211
+ try {
212
+ const content = fs.readFileSync(fullPath, 'utf-8');
213
+ for (const line of content.split('\n')) {
214
+ const trimmed = line.trim();
215
+ if (trimmed.startsWith('Name=')) {
216
+ name = trimmed.slice('Name='.length);
217
+ } else if (trimmed.startsWith('Exec=')) {
218
+ command = trimmed.slice('Exec='.length);
219
+ } else if (trimmed.toLowerCase() === 'x-gnome-autostart-enabled=false') {
220
+ enabled = false;
221
+ } else if (trimmed.startsWith('Hidden=') && trimmed.split('=')[1]?.toLowerCase() === 'true') {
222
+ enabled = false;
223
+ }
224
+ }
225
+ } catch {
226
+ // Failed to read .desktop file — use filename-based defaults
227
+ }
228
+
229
+ items.push({ name, command, location: fullPath, enabled });
230
+ }
231
+ return items;
232
+ }
233
+
234
+ async disableStartupItem(name: string): Promise<boolean> {
235
+ const autostartDir = path.join(os.homedir(), '.config', 'autostart');
236
+ // Accept both the entry name and the filename (with or without .desktop)
237
+ const desktopName = name.endsWith('.desktop') ? name : `${name}.desktop`;
238
+ const desktopPath = path.join(autostartDir, desktopName);
239
+
240
+ try {
241
+ fs.unlinkSync(desktopPath);
242
+ return true;
243
+ } catch {
244
+ return false;
245
+ }
246
+ }
247
+
248
+ getTempDirs(): string[] {
249
+ return [
250
+ '/tmp',
251
+ path.join(os.homedir(), '.cache'),
252
+ path.join(os.homedir(), '.npm', '_cacache'),
253
+ ];
254
+ }
255
+ }