thinknagent 0.1.22 → 0.1.24

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/lib/agent.js CHANGED
@@ -6,6 +6,7 @@ const MetricsPoller = require('./metrics');
6
6
  const LogWatcher = require('./logwatcher');
7
7
  const AlertEngine = require('./alerts');
8
8
  const ShellBridge = require('./shell');
9
+ const HistoryManager = require('./history');
9
10
  const store = require('./store');
10
11
  const chokidar = require('chokidar');
11
12
  const fs = require('fs');
@@ -79,12 +80,16 @@ class Agent {
79
80
  });
80
81
 
81
82
  this.alerts.conn = this.conn;
83
+ this.history = new HistoryManager();
82
84
 
83
85
  this.metrics = new MetricsPoller({
84
86
  connection: this.conn,
85
87
  gpu: cfg.gpu || false,
86
88
  interval: cfg.interval || 1000,
87
- onMetricsEmit: (payload) => this.alerts.evaluate(payload),
89
+ onMetricsEmit: (payload) => {
90
+ this.alerts.evaluate(payload);
91
+ this.history.recordSnapshot(payload);
92
+ },
88
93
  });
89
94
 
90
95
  this.logs = new LogWatcher({
@@ -117,6 +122,17 @@ class Agent {
117
122
  this.metrics.pollNow();
118
123
  });
119
124
 
125
+ this.conn.socket.on('agent:get_history', ({ hours, requestId }) => {
126
+ const targetHours = typeof hours === 'number' ? hours : 72;
127
+ console.log(`[agent] Historical metrics requested (${targetHours}h)`);
128
+ const historyData = this.history.getHistory(targetHours);
129
+ this.conn.socket.emit('agent:history_data', {
130
+ requestId,
131
+ hours: targetHours,
132
+ history: historyData
133
+ });
134
+ });
135
+
120
136
  this.conn.socket.on('agent:send_logs', () => {
121
137
  console.log('[agent] Logs refresh requested');
122
138
  if (this.logs && this.logs.logPaths) {
package/lib/alerts.js CHANGED
@@ -44,7 +44,7 @@ class AlertEngine {
44
44
  const elapsed = (now - state.firstTriggeredAt) / 1000; // seconds
45
45
  if (elapsed >= rule.for && !state.fired) {
46
46
  state.fired = true;
47
- this._fire(rule, val);
47
+ this._fire(rule, val, metrics);
48
48
  }
49
49
  } else {
50
50
  // Condition cleared
@@ -57,17 +57,19 @@ class AlertEngine {
57
57
  }
58
58
  }
59
59
 
60
- _fire(rule, currentValue) {
60
+ _fire(rule, currentValue, metrics = null) {
61
61
  console.warn(`[alerts] FIRING: ${rule.id} — ${rule.metric} ${rule.op} ${rule.value} (current: ${currentValue})`);
62
62
  this.conn.emit('agent:alert', {
63
- id: rule.id,
64
- metric: rule.metric,
65
- op: rule.op,
66
- threshold: rule.value,
67
- current: currentValue,
68
- severity: rule.severity || 'warning',
69
- status: 'active',
70
- firedAt: Date.now(),
63
+ id: rule.id,
64
+ metric: rule.metric,
65
+ op: rule.op,
66
+ threshold: rule.value,
67
+ current: currentValue,
68
+ severity: rule.severity || 'warning',
69
+ status: 'active',
70
+ firedAt: Date.now(),
71
+ diagnostics: metrics?.diagnostics || null,
72
+ topProcesses: metrics?.processes?.top || [],
71
73
  });
72
74
  }
73
75
 
package/lib/connect.js CHANGED
@@ -143,6 +143,24 @@ s.on('agent:approved', ({ agentToken, role, roomId }) => {
143
143
  process.exit(0);
144
144
  });
145
145
 
146
+ // ── Periodic HMAC Challenge-Response Zero-Trust Protocol ───────────────────
147
+ s.on('agent:auth_challenge', ({ challenge, ts }) => {
148
+ const cfg = store.read();
149
+ const token = cfg.agentToken || this.agentToken;
150
+ const crypto = require('crypto');
151
+ if (!token) return;
152
+
153
+ const raw = `${challenge}:${ts}:${cfg.agentId}`;
154
+ const signature = crypto.createHmac('sha256', token).update(raw).digest('hex');
155
+
156
+ s.emit('agent:auth_challenge_response', {
157
+ challenge,
158
+ ts,
159
+ signature,
160
+ agentId: cfg.agentId
161
+ });
162
+ });
163
+
146
164
  s.on('connect_error', (err) => {
147
165
  console.error(`[thinknagent] Connection error: ${err.message}`);
148
166
  });
package/lib/history.js ADDED
@@ -0,0 +1,143 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ const HOME = os.homedir();
8
+ const CONFIG_DIR = path.join(HOME, '.thinknagent');
9
+ const HISTORY_DIR = path.join(CONFIG_DIR, 'history');
10
+ const RETENTION_DAYS = 3;
11
+ const SNAPSHOT_INTERVAL_MS = 30000; // Snapshot to disk every 30 seconds
12
+
13
+ class HistoryManager {
14
+ constructor() {
15
+ this._ensureDir();
16
+ this._lastSnapshotTime = 0;
17
+ }
18
+
19
+ _ensureDir() {
20
+ try {
21
+ if (!fs.existsSync(HISTORY_DIR)) {
22
+ fs.mkdirSync(HISTORY_DIR, { recursive: true, mode: 0o700 });
23
+ }
24
+ } catch (err) {
25
+ console.warn('[history] Failed to create history directory:', err.message);
26
+ }
27
+ }
28
+
29
+ _getDateKey(timestamp = Date.now()) {
30
+ const d = new Date(timestamp);
31
+ const y = d.getUTCFullYear();
32
+ const m = String(d.getUTCMonth() + 1).padStart(2, '0');
33
+ const day = String(d.getUTCDate()).padStart(2, '0');
34
+ return `${y}-${m}-${day}`;
35
+ }
36
+
37
+ _getFilePathForDate(dateKey) {
38
+ return path.join(HISTORY_DIR, `metrics_${dateKey}.jsonl`);
39
+ }
40
+
41
+ // Called on every metrics tick (we sample every 30s to disk)
42
+ recordSnapshot(metrics) {
43
+ const now = Date.now();
44
+ if (now - this._lastSnapshotTime < SNAPSHOT_INTERVAL_MS) {
45
+ return;
46
+ }
47
+ this._lastSnapshotTime = now;
48
+
49
+ try {
50
+ const dateKey = this._getDateKey(now);
51
+ const filePath = this._getFilePathForDate(dateKey);
52
+
53
+ const entry = {
54
+ ts: now,
55
+ cpu: metrics.cpu?.usage ?? 0,
56
+ load: metrics.cpu?.loadAvg ?? 0,
57
+ mem: metrics.memory?.usedPct ?? 0,
58
+ memUsedMB: Math.round((metrics.memory?.used || 0) / (1024 * 1024)),
59
+ rxSec: metrics.network?.[0]?.rxSec ?? 0,
60
+ txSec: metrics.network?.[0]?.txSec ?? 0,
61
+ procs: metrics.processes?.total ?? 0,
62
+ diag: metrics.diagnostics?.hasSpike ? metrics.diagnostics : undefined,
63
+ };
64
+
65
+ // Append atomic line to daily disk file (survives crashes, restarts, reboots)
66
+ fs.appendFileSync(filePath, JSON.stringify(entry) + '\n', { encoding: 'utf8', mode: 0o600 });
67
+
68
+ // Run daily pruning check
69
+ this._pruneOldFiles();
70
+ } catch (err) {
71
+ console.error('[history] Error writing snapshot to disk:', err.message);
72
+ }
73
+ }
74
+
75
+ // Deletes any history files older than RETENTION_DAYS (3 days)
76
+ _pruneOldFiles() {
77
+ try {
78
+ const files = fs.readdirSync(HISTORY_DIR);
79
+ const cutoffTime = Date.now() - (RETENTION_DAYS * 24 * 60 * 60 * 1000);
80
+ const cutoffDateKey = this._getDateKey(cutoffTime);
81
+
82
+ for (const file of files) {
83
+ if (!file.startsWith('metrics_') || !file.endsWith('.jsonl')) continue;
84
+ const fileDateKey = file.replace('metrics_', '').replace('.jsonl', '');
85
+ if (fileDateKey < cutoffDateKey) {
86
+ try {
87
+ fs.unlinkSync(path.join(HISTORY_DIR, file));
88
+ console.log(`[history] Pruned old history file: ${file}`);
89
+ } catch (e) {}
90
+ }
91
+ }
92
+ } catch (err) {
93
+ // ignore
94
+ }
95
+ }
96
+
97
+ // Retrieve historical data across the last N hours (up to 72 hours / 3 days)
98
+ getHistory(hours = 72) {
99
+ const minTimestamp = Date.now() - (hours * 60 * 60 * 1000);
100
+ const records = [];
101
+
102
+ try {
103
+ if (!fs.existsSync(HISTORY_DIR)) return [];
104
+
105
+ const files = fs.readdirSync(HISTORY_DIR)
106
+ .filter(f => f.startsWith('metrics_') && f.endsWith('.jsonl'))
107
+ .sort(); // ascending date order
108
+
109
+ for (const file of files) {
110
+ const fullPath = path.join(HISTORY_DIR, file);
111
+ const content = fs.readFileSync(fullPath, 'utf8');
112
+ const lines = content.split('\n');
113
+
114
+ for (const line of lines) {
115
+ if (!line.trim()) continue;
116
+ try {
117
+ const data = JSON.parse(line);
118
+ if (data.ts >= minTimestamp) {
119
+ records.push(data);
120
+ }
121
+ } catch (e) {}
122
+ }
123
+ }
124
+
125
+ // If more than 300 points requested, downsample evenly to keep UI charts silky smooth
126
+ if (records.length > 300) {
127
+ const step = Math.ceil(records.length / 300);
128
+ const sampled = [];
129
+ for (let i = 0; i < records.length; i += step) {
130
+ sampled.push(records[i]);
131
+ }
132
+ return sampled;
133
+ }
134
+
135
+ return records;
136
+ } catch (err) {
137
+ console.error('[history] Error reading history from disk:', err.message);
138
+ return [];
139
+ }
140
+ }
141
+ }
142
+
143
+ module.exports = HistoryManager;
package/lib/metrics.js CHANGED
@@ -47,6 +47,12 @@ class MetricsPoller {
47
47
  si.processes(),
48
48
  ]);
49
49
 
50
+ // True active process memory (excluding OS buffer / page cache)
51
+ const realUsedBytes = (typeof mem.available === 'number' && mem.available > 0)
52
+ ? (mem.total - mem.available)
53
+ : (mem.active || (mem.used - (mem.buffcache || 0)));
54
+ const realUsedPct = parseFloat(Math.min(100, Math.max(0, (realUsedBytes / mem.total) * 100)).toFixed(1));
55
+
50
56
  const payload = {
51
57
  ts: Date.now(),
52
58
  cpu: {
@@ -55,10 +61,15 @@ class MetricsPoller {
55
61
  loadAvg: cpu.avgLoad ?? 0,
56
62
  },
57
63
  memory: {
58
- total: mem.total,
59
- used: mem.used,
60
- free: mem.free,
61
- usedPct: parseFloat(((mem.used / mem.total) * 100).toFixed(1)),
64
+ total: mem.total,
65
+ active: realUsedBytes,
66
+ activePct: realUsedPct,
67
+ buffcache: mem.buffcache || Math.max(0, mem.used - realUsedBytes),
68
+ rawUsed: mem.used,
69
+ rawUsedPct: parseFloat(((mem.used / mem.total) * 100).toFixed(1)),
70
+ free: mem.available || mem.free,
71
+ used: realUsedBytes,
72
+ usedPct: realUsedPct,
62
73
  },
63
74
  disk: disk.map(d => ({
64
75
  fs: d.fs,
@@ -128,6 +139,79 @@ class MetricsPoller {
128
139
  }
129
140
  }
130
141
 
142
+ // ─── Automated Spike Diagnostic & Root Cause Engine ────────────────────────
143
+ const curCpuUsage = parseFloat(cpu.currentLoad.toFixed(1));
144
+ const curMemPct = realUsedPct;
145
+
146
+ // Filter real user/application processes with actual resource footprint
147
+ const sortedMemProcs = (procs.list || [])
148
+ .filter(p => p && p.pid > 1 && (p.pmem > 0.5 || p.pcpu > 0.5))
149
+ .sort((a, b) => (b.pmem || 0) - (a.pmem || 0));
150
+ const topMemProc = sortedMemProcs[0] || (procs.list || []).sort((a, b) => (b.pmem || 0) - (a.pmem || 0))[0] || null;
151
+
152
+ const sortedCpuProcs = (procs.list || [])
153
+ .filter(p => p && p.pid > 1 && (p.pcpu > 0.5 || p.pmem > 0.5))
154
+ .sort((a, b) => (b.pcpu || 0) - (a.pcpu || 0));
155
+ const topCpuProc = sortedCpuProcs[0] || (procs.list || []).sort((a, b) => (b.pcpu || 0) - (a.pcpu || 0))[0] || null;
156
+
157
+ let diagnostics = {
158
+ hasSpike: false,
159
+ type: 'nominal',
160
+ culprit: null,
161
+ deltaPct: 0,
162
+ explanation: 'System resource levels nominal and stable.'
163
+ };
164
+
165
+ const prevMem = this._lastMemPct !== undefined ? this._lastMemPct : curMemPct;
166
+ const prevCpu = this._lastCpuUsage !== undefined ? this._lastCpuUsage : curCpuUsage;
167
+ const memDelta = parseFloat((curMemPct - prevMem).toFixed(1));
168
+ const cpuDelta = parseFloat((curCpuUsage - prevCpu).toFixed(1));
169
+ this._lastMemPct = curMemPct;
170
+ this._lastCpuUsage = curCpuUsage;
171
+
172
+ const topMemMB = topMemProc ? Math.round(((topMemProc.pmem || 0) / 100) * (mem.total / (1024 * 1024))) : 0;
173
+ const topMemPct = topMemProc ? parseFloat((topMemProc.pmem || 0).toFixed(1)) : 0;
174
+ const hasRealMemCulprit = topMemProc && (topMemPct >= 3.0 || topMemMB >= 35);
175
+
176
+ // Trigger spike only on sudden surge OR critically high memory with an identified culprit
177
+ if (memDelta >= 5.0 || (curMemPct >= 90 && hasRealMemCulprit)) {
178
+ diagnostics.hasSpike = true;
179
+ diagnostics.type = 'memory';
180
+ diagnostics.deltaPct = memDelta;
181
+ if (hasRealMemCulprit) {
182
+ diagnostics.culprit = {
183
+ pid: topMemProc.pid,
184
+ name: topMemProc.name,
185
+ command: topMemProc.command ? topMemProc.command.slice(0, 80) : topMemProc.name,
186
+ memPct: topMemPct,
187
+ memMB: topMemMB,
188
+ cpu: parseFloat((topMemProc.pcpu || 0).toFixed(1))
189
+ };
190
+ diagnostics.explanation = `Memory surge detected (${curMemPct}% RAM, delta: ${memDelta >= 0 ? '+' : ''}${memDelta}%). Primary consumer is "${topMemProc.name}" (PID ${topMemProc.pid}) utilizing ${topMemPct}% (${topMemMB} MB). Probable cause: Rapid memory allocation, large dataset buffer in memory, or memory leak.`;
191
+ } else {
192
+ diagnostics.explanation = `Memory load is elevated (${curMemPct}% RAM, delta: ${memDelta >= 0 ? '+' : ''}${memDelta}%), distributed across system caches and background services. No single runaway process.`;
193
+ }
194
+ } else if (cpuDelta >= 30.0 || (curCpuUsage >= 85 && topCpuProc && (topCpuProc.pcpu || 0) >= 10.0)) {
195
+ const topCpuVal = topCpuProc ? parseFloat((topCpuProc.pcpu || 0).toFixed(1)) : 0;
196
+ diagnostics.hasSpike = true;
197
+ diagnostics.type = 'cpu';
198
+ diagnostics.deltaPct = cpuDelta;
199
+ if (topCpuProc && topCpuVal >= 5.0) {
200
+ diagnostics.culprit = {
201
+ pid: topCpuProc.pid,
202
+ name: topCpuProc.name,
203
+ command: topCpuProc.command ? topCpuProc.command.slice(0, 80) : topCpuProc.name,
204
+ cpu: topCpuVal,
205
+ memPct: parseFloat((topCpuProc.pmem || 0).toFixed(1))
206
+ };
207
+ diagnostics.explanation = `High CPU workload spike detected (${curCpuUsage}% CPU, delta: ${cpuDelta >= 0 ? '+' : ''}${cpuDelta}%). Primary consumer is "${topCpuProc.name}" (PID ${topCpuProc.pid}) utilizing ${topCpuVal}% CPU. Probable cause: Intensive computation, complex query/loop execution, or heavy background processing.`;
208
+ } else {
209
+ diagnostics.explanation = `High CPU utilization (${curCpuUsage}% CPU, delta: ${cpuDelta >= 0 ? '+' : ''}${cpuDelta}%) spread across multiple threads.`;
210
+ }
211
+ }
212
+
213
+ payload.diagnostics = diagnostics;
214
+
131
215
  this._history.push({ ts: payload.ts, cpu: payload.cpu.usage, mem: payload.memory.usedPct });
132
216
  if (this._history.length > 60) this._history.shift();
133
217
  payload.history = this._history;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinknagent",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "description": "ThinkNCollab server agent & MCP server — metrics, logs, alerts, shell bridge, AI planning",
5
5
  "main": "lib/agent.js",
6
6
  "author": "ThinkNCollab Team <raman@thinkncollab.com>",