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/Readme.md +87 -279
- package/bin/thinknagent.js +24 -1
- package/bin/thinkncollab-mcp.js +387 -0
- package/lib/agent.js +41 -7
- package/lib/alerts.js +12 -10
- package/lib/connect.js +18 -0
- package/lib/daemon.js +5 -4
- package/lib/e2ee.js +45 -0
- package/lib/history.js +143 -0
- package/lib/logwatcher.js +46 -29
- package/lib/metrics.js +71 -4
- package/lib/shell.js +14 -0
- package/package.json +4 -3
- package/install/setup.sh +0 -90
- package/lib/app.js +0 -327
- package/thinknagent.sh +0 -554
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
|
-
|
|
71
|
+
try {
|
|
72
|
+
if (!fs.existsSync(absPath)) return;
|
|
73
|
+
const newSize = stats ? stats.size : fs.statSync(absPath).size;
|
|
70
74
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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(() =>
|
|
19
|
-
|
|
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.
|
|
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 ""
|