thinknagent 0.1.20 → 0.1.23

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/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/logwatcher.js CHANGED
@@ -4,6 +4,8 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const chokidar = require('chokidar');
6
6
  const { EventEmitter } = require('events');
7
+ const { encryptE2EE } = require('./e2ee');
8
+ const store = require('./store');
7
9
 
8
10
  const BUFFER_LINES = 100;
9
11
  const CHUNK_DELAY = 50;
@@ -24,7 +26,7 @@ class LogWatcher extends EventEmitter {
24
26
  for (const p of this.logPaths) {
25
27
  this._watch(p);
26
28
  }
27
- console.log(`[logs] Watching ${this.logPaths.length} file(s)`);
29
+ console.log(`[logs] Watching ${this.logPaths.length} file(s) (AES-256 E2EE active)`);
28
30
  }
29
31
 
30
32
  stop() {
@@ -66,37 +68,47 @@ class LogWatcher extends EventEmitter {
66
68
  });
67
69
 
68
70
  watcher.on('change', (fpath, stats) => {
69
- const newSize = stats ? stats.size : fs.statSync(absPath).size;
71
+ try {
72
+ if (!fs.existsSync(absPath)) return;
73
+ const newSize = stats ? stats.size : fs.statSync(absPath).size;
70
74
 
71
- if (newSize < state.size) {
72
- // log rotated — reset
73
- state.size = 0;
74
- }
75
+ if (newSize < state.size) {
76
+ // log rotated — reset
77
+ state.size = 0;
78
+ }
79
+
80
+ const newBytes = newSize - state.size;
81
+ if (newBytes <= 0) return;
75
82
 
76
- const newBytes = newSize - state.size;
77
- if (newBytes <= 0) return;
78
-
79
- const buf = Buffer.alloc(newBytes);
80
- const fd = fs.openSync(absPath, 'r');
81
- fs.readSync(fd, buf, 0, newBytes, state.size);
82
- fs.closeSync(fd);
83
- state.size = newSize;
84
-
85
- const newLines = buf.toString('utf8').split('\n').filter(Boolean);
86
- state.lines.push(...newLines);
87
-
88
- clearTimeout(state.timer);
89
- state.timer = setTimeout(() => {
90
- const toSend = state.lines.splice(0);
91
- if (toSend.length) {
92
- this.conn.emit('agent:logs', {
93
- file: absPath,
94
- lines: toSend.map(l => ({ ts: Date.now(), text: l })),
95
- });
83
+ const buf = Buffer.alloc(newBytes);
84
+ const fd = fs.openSync(absPath, 'r');
85
+ try {
86
+ fs.readSync(fd, buf, 0, newBytes, state.size);
87
+ } finally {
88
+ fs.closeSync(fd);
96
89
  }
97
- }, CHUNK_DELAY);
90
+ state.size = newSize;
91
+
92
+ const newLines = buf.toString('utf8').split('\n').filter(Boolean);
93
+ state.lines.push(...newLines);
94
+
95
+ clearTimeout(state.timer);
96
+ state.timer = setTimeout(() => {
97
+ const toSend = state.lines.splice(0);
98
+ if (toSend.length) {
99
+ const roomId = this.conn?.roomId || store.get('roomId') || '';
100
+ this.conn.emit('agent:logs', {
101
+ file: absPath,
102
+ lines: toSend.map(l => ({ ts: Date.now(), text: encryptE2EE(l, roomId) })),
103
+ });
104
+ }
105
+ }, CHUNK_DELAY);
106
+ } catch (err) {
107
+ console.error(`[logs] Error reading log slice from ${absPath}:`, err.message);
108
+ }
98
109
  });
99
110
 
111
+
100
112
  watcher.on('error', (err) => {
101
113
  console.error(`[logs] Watch error on ${absPath}:`, err.message);
102
114
  });
@@ -107,12 +119,17 @@ class LogWatcher extends EventEmitter {
107
119
 
108
120
  _sendTail(filePath) {
109
121
  try {
110
- const content = fs.readFileSync(filePath, 'utf8');
122
+ const absPath = filePath.startsWith('~')
123
+ ? path.join(process.env.HOME || '/', filePath.slice(1))
124
+ : path.resolve(filePath);
125
+ if (!fs.existsSync(absPath)) return;
126
+ const content = fs.readFileSync(absPath, 'utf8');
111
127
  const lines = content.split('\n').filter(Boolean).slice(-BUFFER_LINES);
112
128
  if (lines.length) {
129
+ const roomId = this.conn?.roomId || store.get('roomId') || '';
113
130
  this.conn.emit('agent:logs:tail', {
114
131
  file: filePath,
115
- lines: lines.map(l => ({ ts: null, text: l })),
132
+ lines: lines.map(l => ({ ts: null, text: encryptE2EE(l, roomId) })),
116
133
  });
117
134
  }
118
135
  } catch (err) {
package/lib/metrics.js CHANGED
@@ -2,25 +2,32 @@
2
2
 
3
3
  const si = require('systeminformation');
4
4
 
5
- const POLL_INTERVAL = 5000;
5
+ const DEFAULT_POLL_INTERVAL = 1000;
6
6
 
7
7
  class MetricsPoller {
8
- constructor({ connection, gpu = false, onMetricsEmit = null }) {
8
+ constructor({ connection, gpu = false, interval = DEFAULT_POLL_INTERVAL, onMetricsEmit = null }) {
9
9
  this.conn = connection;
10
10
  this.gpu = gpu;
11
+ this.interval = Math.max(500, interval || DEFAULT_POLL_INTERVAL);
11
12
  this.onMetricsEmit = onMetricsEmit; // alert engine callback — no monkey-patch needed
12
13
  this._timer = null;
14
+ this._isPolling = false;
13
15
  this._history = [];
14
16
  }
15
17
 
16
18
  start() {
17
19
  this._poll();
18
- this._timer = setInterval(() => this._poll(), POLL_INTERVAL);
19
- console.log('[metrics] Poller started');
20
+ this._timer = setInterval(() => {
21
+ if (!this._isPolling) {
22
+ this._poll();
23
+ }
24
+ }, this.interval);
25
+ console.log(`[metrics] Poller started (interval: ${this.interval}ms)`);
20
26
  }
21
27
 
22
28
  stop() {
23
29
  if (this._timer) clearInterval(this._timer);
30
+ this._timer = null;
24
31
  console.log('[metrics] Poller stopped');
25
32
  }
26
33
 
@@ -29,6 +36,8 @@ class MetricsPoller {
29
36
  }
30
37
 
31
38
  async _poll() {
39
+ if (this._isPolling) return;
40
+ this._isPolling = true;
32
41
  try {
33
42
  const [cpu, mem, disk, net, procs] = await Promise.all([
34
43
  si.currentLoad(),
@@ -119,6 +128,62 @@ class MetricsPoller {
119
128
  }
120
129
  }
121
130
 
131
+ // ─── Automated Spike Diagnostic & Root Cause Engine ────────────────────────
132
+ const curCpuUsage = parseFloat(cpu.currentLoad.toFixed(1));
133
+ const curMemPct = parseFloat(((mem.used / mem.total) * 100).toFixed(1));
134
+ const sortedProcs = (procs.list || []).slice().sort((a, b) => (b.pmem || 0) - (a.pmem || 0));
135
+ const topMemProc = sortedProcs[0] || null;
136
+ const topCpuProc = (procs.list || []).slice().sort((a, b) => (b.pcpu || 0) - (a.pcpu || 0))[0] || null;
137
+
138
+ let diagnostics = {
139
+ hasSpike: false,
140
+ type: 'nominal',
141
+ culprit: null,
142
+ deltaPct: 0,
143
+ explanation: 'System resource levels nominal and stable.'
144
+ };
145
+
146
+ const prevMem = this._lastMemPct !== undefined ? this._lastMemPct : curMemPct;
147
+ const prevCpu = this._lastCpuUsage !== undefined ? this._lastCpuUsage : curCpuUsage;
148
+ const memDelta = parseFloat((curMemPct - prevMem).toFixed(1));
149
+ const cpuDelta = parseFloat((curCpuUsage - prevCpu).toFixed(1));
150
+ this._lastMemPct = curMemPct;
151
+ this._lastCpuUsage = curCpuUsage;
152
+
153
+ if (curMemPct >= 80 || memDelta >= 3.5) {
154
+ diagnostics.hasSpike = true;
155
+ diagnostics.type = 'memory';
156
+ diagnostics.deltaPct = memDelta;
157
+ if (topMemProc) {
158
+ const procMemMB = Math.round(((topMemProc.pmem || 0) / 100) * (mem.total / (1024 * 1024)));
159
+ diagnostics.culprit = {
160
+ pid: topMemProc.pid,
161
+ name: topMemProc.name,
162
+ command: topMemProc.command ? topMemProc.command.slice(0, 80) : topMemProc.name,
163
+ memPct: parseFloat((topMemProc.pmem || 0).toFixed(1)),
164
+ memMB: procMemMB,
165
+ cpu: parseFloat((topMemProc.pcpu || 0).toFixed(1))
166
+ };
167
+ diagnostics.explanation = `Memory surge detected (${curMemPct}% RAM, delta: ${memDelta >= 0 ? '+' : ''}${memDelta}%). Primary consumer is "${topMemProc.name}" (PID ${topMemProc.pid}) utilizing ${diagnostics.culprit.memPct}% (${procMemMB} MB). Probable cause: Rapid memory allocation, large dataset buffer in memory, or memory leak.`;
168
+ }
169
+ } else if (curCpuUsage >= 80 || cpuDelta >= 25.0) {
170
+ diagnostics.hasSpike = true;
171
+ diagnostics.type = 'cpu';
172
+ diagnostics.deltaPct = cpuDelta;
173
+ if (topCpuProc) {
174
+ diagnostics.culprit = {
175
+ pid: topCpuProc.pid,
176
+ name: topCpuProc.name,
177
+ command: topCpuProc.command ? topCpuProc.command.slice(0, 80) : topCpuProc.name,
178
+ cpu: parseFloat((topCpuProc.pcpu || 0).toFixed(1)),
179
+ memPct: parseFloat((topCpuProc.pmem || 0).toFixed(1))
180
+ };
181
+ diagnostics.explanation = `High CPU workload spike detected (${curCpuUsage}% CPU, delta: ${cpuDelta >= 0 ? '+' : ''}${cpuDelta}%). Primary consumer is "${topCpuProc.name}" (PID ${topCpuProc.pid}) utilizing ${diagnostics.culprit.cpu}% CPU. Probable cause: Intensive computation, complex query/loop execution, or heavy background processing.`;
182
+ }
183
+ }
184
+
185
+ payload.diagnostics = diagnostics;
186
+
122
187
  this._history.push({ ts: payload.ts, cpu: payload.cpu.usage, mem: payload.memory.usedPct });
123
188
  if (this._history.length > 60) this._history.shift();
124
189
  payload.history = this._history;
@@ -129,6 +194,8 @@ class MetricsPoller {
129
194
 
130
195
  } catch (err) {
131
196
  console.error('[metrics] Poll error:', err.message);
197
+ } finally {
198
+ this._isPolling = false;
132
199
  }
133
200
  }
134
201
  }
package/lib/shell.js CHANGED
@@ -11,6 +11,7 @@ try {
11
11
  const PTY_ENV_ALLOWLIST = new Set([
12
12
  'PATH', 'HOME', 'SHELL', 'TERM', 'LANG', 'LC_ALL', 'LC_CTYPE',
13
13
  'USER', 'LOGNAME', 'HOSTNAME', 'TZ', 'COLORTERM', 'DISPLAY',
14
+ 'NVM_DIR', 'NODE_PATH', 'PM2_HOME', 'EDITOR', 'VISUAL', 'CI'
14
15
  ]);
15
16
 
16
17
  function buildSafeEnv() {
@@ -18,6 +19,19 @@ function buildSafeEnv() {
18
19
  for (const key of PTY_ENV_ALLOWLIST) {
19
20
  if (process.env[key] !== undefined) safe[key] = process.env[key];
20
21
  }
22
+ const home = process.env.HOME || '/home/ubuntu';
23
+ const extraPaths = [
24
+ `${home}/.nvm/versions/node/$(ls ${home}/.nvm/versions/node 2>/dev/null | tail -n 1)/bin`,
25
+ `${home}/.npm-global/bin`,
26
+ `${home}/.local/bin`,
27
+ '/usr/local/bin',
28
+ '/usr/bin',
29
+ '/bin',
30
+ '/usr/sbin',
31
+ '/sbin'
32
+ ].join(':');
33
+
34
+ safe.PATH = safe.PATH ? `${safe.PATH}:${extraPaths}` : extraPaths;
21
35
  safe.TERM = 'xterm-256color';
22
36
  safe.THINKNCOLLAB_AGENT = '1';
23
37
  return safe;
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "thinknagent",
3
- "version": "0.1.20",
4
- "description": "ThinkNCollab server agent — metrics, logs, alerts, shell bridge",
3
+ "version": "0.1.23",
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>",
7
7
  "bin": {
8
- "thinknagent": "bin/thinknagent.js"
8
+ "thinknagent": "bin/thinknagent.js",
9
+ "thinkncollab-mcp": "bin/thinkncollab-mcp.js"
9
10
  },
10
11
  "scripts": {
11
12
  "start": "node bin/thinknagent.js start",
package/install/setup.sh DELETED
@@ -1,90 +0,0 @@
1
- #!/usr/bin/env bash
2
- # ThinkNCollab Agent — installer
3
- # Usage: curl -fsSL https://thinkncollab.com/install-agent.sh | bash -s -- --server https://thinkncollab.com --name my-server --room <roomId>
4
- set -euo pipefail
5
-
6
- SERVER=""
7
- NAME=""
8
- ROOM=""
9
- GPU=false
10
- LOGS=""
11
- SYSTEMD=false
12
-
13
- while [[ $# -gt 0 ]]; do
14
- case $1 in
15
- --server) SERVER="$2"; shift 2 ;;
16
- --name) NAME="$2"; shift 2 ;;
17
- --room) ROOM="$2"; shift 2 ;;
18
- --gpu) GPU=true; shift ;;
19
- --logs) LOGS="$2"; shift 2 ;;
20
- --systemd) SYSTEMD=true; shift ;;
21
- *) echo "Unknown option: $1"; exit 1 ;;
22
- esac
23
- done
24
-
25
- [[ -z "$SERVER" ]] && echo "Error: --server required" && exit 1
26
- [[ -z "$NAME" ]] && echo "Error: --name required" && exit 1
27
- [[ -z "$ROOM" ]] && echo "Error: --room required" && exit 1
28
-
29
- echo ""
30
- echo " ThinkNCollab Agent Installer"
31
- echo " ──────────────────────────────"
32
-
33
- NODE_VER=$(node --version 2>/dev/null | cut -d. -f1 | tr -d 'v' || echo "0")
34
- if [[ "$NODE_VER" -lt 18 ]]; then
35
- echo " Error: Node.js 18+ required (found: $(node --version 2>/dev/null || echo 'not found'))"
36
- exit 1
37
- fi
38
- echo " Node.js : $(node --version) ✓"
39
-
40
- echo " Installing thinknagent..."
41
- npm install -g thinknagent --silent
42
-
43
- GPU_FLAG=""
44
- $GPU && GPU_FLAG="--gpu"
45
-
46
- LOGS_FLAG=""
47
- [[ -n "$LOGS" ]] && LOGS_FLAG="--logs $LOGS"
48
-
49
- thinknagent init --server "$SERVER" --name "$NAME" --room "$ROOM" $GPU_FLAG $LOGS_FLAG
50
-
51
- if $SYSTEMD; then
52
- echo ""
53
- echo " Setting up systemd service..."
54
- id thinknagent &>/dev/null || useradd --system --no-create-home thinknagent
55
- AGENT_BIN="$(which thinknagent)"
56
- cat > /etc/systemd/system/thinknagent.service << SVCEOF
57
- [Unit]
58
- Description=ThinkNCollab Agent
59
- After=network-online.target
60
- Wants=network-online.target
61
-
62
- [Service]
63
- User=thinknagent
64
- Group=thinknagent
65
- ExecStart=${AGENT_BIN} start
66
- Restart=on-failure
67
- RestartSec=10
68
- Environment=NODE_ENV=production
69
- StandardOutput=journal
70
- StandardError=journal
71
- SyslogIdentifier=thinknagent
72
-
73
- [Install]
74
- WantedBy=multi-user.target
75
- SVCEOF
76
- systemctl daemon-reload
77
- systemctl enable thinknagent
78
- systemctl start thinknagent
79
- echo " systemd service: enabled + started ✓"
80
- echo " Logs: journalctl -u thinknagent -f"
81
- else
82
- echo ""
83
- echo " To start now : thinknagent start"
84
- echo " To run on boot : Re-run this script with --systemd flag"
85
- echo " Or with pm2 : pm2 start \$(which thinknagent) -- start && pm2 save"
86
- fi
87
-
88
- echo ""
89
- echo " ✓ Done. Open ThinkNCollab and approve this agent in your room's DevOps Wall."
90
- echo ""