thinknagent 0.1.19 → 0.1.22

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/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(),
@@ -129,6 +138,8 @@ class MetricsPoller {
129
138
 
130
139
  } catch (err) {
131
140
  console.error('[metrics] Poll error:', err.message);
141
+ } finally {
142
+ this._isPolling = false;
132
143
  }
133
144
  }
134
145
  }
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.19",
4
- "description": "ThinkNCollab server agent — metrics, logs, alerts, shell bridge",
3
+ "version": "0.1.22",
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 ""
package/lib/app.js DELETED
@@ -1,327 +0,0 @@
1
- 'use strict';
2
-
3
- const http = require('http');
4
- const path = require('path');
5
- const os = require('os');
6
- const { exec } = require('child_process');
7
- const store = require('./store');
8
- const Agent = require('./agent');
9
- const si = require('systeminformation');
10
-
11
- class AgentApp {
12
- constructor(port = 4455) {
13
- this.port = port;
14
- this.server = null;
15
- this.agentInstance = null;
16
- }
17
-
18
- start(autoOpen = true) {
19
- this.server = http.createServer((req, res) => this._handleRequest(req, res));
20
- this.server.listen(this.port, '127.0.0.1', () => {
21
- const url = `http://localhost:${this.port}`;
22
- console.log(`\n \x1b[32m✔\x1b[0m ThinkNCollab Agent App is running at: \x1b[36m${url}\x1b[0m\n`);
23
- if (autoOpen) {
24
- this._openBrowser(url);
25
- }
26
- });
27
-
28
- // Auto-start agent if already initialized
29
- const cfg = store.read();
30
- if (cfg.serverUrl && cfg.roomId) {
31
- try {
32
- this.agentInstance = new Agent();
33
- this.agentInstance.start();
34
- } catch (err) {
35
- console.warn('[app] Auto-start agent notice:', err.message);
36
- }
37
- }
38
- }
39
-
40
- _openBrowser(url) {
41
- const startCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
42
- exec(`${startCmd} ${url}`, () => {});
43
- }
44
-
45
- async _handleRequest(req, res) {
46
- const url = new URL(req.url, `http://${req.headers.host}`);
47
-
48
- // API: Status & Telemetry
49
- if (url.pathname === '/api/status' && req.method === 'GET') {
50
- try {
51
- const [cpu, mem, disk] = await Promise.all([
52
- si.currentLoad(),
53
- si.mem(),
54
- si.fsSize()
55
- ]);
56
- const cfg = store.read();
57
- res.writeHead(200, { 'Content-Type': 'application/json' });
58
- return res.end(JSON.stringify({
59
- success: true,
60
- config: {
61
- name: cfg.name || os.hostname(),
62
- serverUrl: cfg.serverUrl || '',
63
- roomId: cfg.roomId || '',
64
- agentId: cfg.agentId || '',
65
- role: cfg.role || 'monitor',
66
- status: cfg.agentToken ? 'approved' : cfg.agentId ? 'pending' : 'not_initialized'
67
- },
68
- telemetry: {
69
- hostname: os.hostname(),
70
- platform: `${os.type()} ${os.release()} (${os.arch()})`,
71
- uptime: os.uptime(),
72
- cpuPercent: parseFloat(cpu.currentLoad.toFixed(1)),
73
- cores: cpu.cpus?.length || os.cpus().length,
74
- memoryTotalMB: Math.round(mem.total / 1048576),
75
- memoryUsedMB: Math.round(mem.used / 1048576),
76
- memoryUsedPct: parseFloat(((mem.used / mem.total) * 100).toFixed(1)),
77
- disk: disk.map(d => ({
78
- mount: d.mount,
79
- sizeGB: (d.size / 1073741824).toFixed(1),
80
- usedPct: parseFloat((d.use || 0).toFixed(1))
81
- }))
82
- }
83
- }));
84
- } catch (err) {
85
- res.writeHead(500, { 'Content-Type': 'application/json' });
86
- return res.end(JSON.stringify({ error: err.message }));
87
- }
88
- }
89
-
90
- // API: Connect / Configure
91
- if (url.pathname === '/api/connect' && req.method === 'POST') {
92
- let body = '';
93
- req.on('data', chunk => body += chunk);
94
- req.on('end', () => {
95
- try {
96
- const data = JSON.parse(body);
97
- const { serverUrl, name, roomId } = data;
98
- if (!serverUrl || !roomId) {
99
- res.writeHead(400, { 'Content-Type': 'application/json' });
100
- return res.end(JSON.stringify({ error: 'Server URL and Room ID are required.' }));
101
- }
102
-
103
- const { v4: uuid } = require('uuid');
104
- const existing = store.read();
105
- const agentId = existing.agentId || uuid();
106
-
107
- store.write({
108
- ...existing,
109
- serverUrl: serverUrl.replace(/\/$/, ''),
110
- name: name || os.hostname(),
111
- roomId,
112
- agentId
113
- });
114
-
115
- // Restart Agent instance
116
- if (this.agentInstance) {
117
- try { this.agentInstance._shutdown('reconnect'); } catch (e) {}
118
- }
119
- this.agentInstance = new Agent();
120
- this.agentInstance.start();
121
-
122
- res.writeHead(200, { 'Content-Type': 'application/json' });
123
- return res.end(JSON.stringify({ success: true, message: 'Agent connected! Check DevOps Wall for approval.' }));
124
- } catch (err) {
125
- res.writeHead(500, { 'Content-Type': 'application/json' });
126
- return res.end(JSON.stringify({ error: err.message }));
127
- }
128
- });
129
- return;
130
- }
131
-
132
- // Serve Local App UI
133
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
134
- res.end(this._getHtml());
135
- }
136
-
137
- _getHtml() {
138
- return `<!DOCTYPE html>
139
- <html lang="en">
140
- <head>
141
- <meta charset="UTF-8">
142
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
143
- <title>ThinkNCollab Agent App</title>
144
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" />
145
- <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&family=Syne:wght@400;600;700;800&display=swap" rel="stylesheet">
146
- <style>
147
- * { margin:0; padding:0; box-sizing:border-box; }
148
- :root {
149
- --bg: #090910; --bg2: #12121e; --card: #161626; --border: rgba(255,255,255,0.08);
150
- --accent: #00ff88; --accent-glow: rgba(0,255,136,0.2); --text: #f1f5f9; --text2: #94a3b8; --text3: #475569;
151
- --orange: #f97316; --red: #ef4444;
152
- --font: 'Syne', sans-serif; --mono: 'JetBrains Mono', monospace;
153
- }
154
- body { background: var(--bg); color: var(--text); font-family: var(--font); min-height: 100vh; display: flex; flex-direction: column; }
155
- header { height: 60px; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; padding: 0 24px; background: rgba(18,18,30,0.8); backdrop-filter: blur(12px); }
156
- .brand { display: flex; align-items: center; gap: 10px; font-weight: 800; font-size: 16px; letter-spacing: -0.02em; }
157
- .brand-dot { color: var(--accent); }
158
- .badge { font-family: var(--mono); font-size: 11px; padding: 4px 10px; border-radius: 20px; font-weight: 600; display: flex; align-items: center; gap: 6px; }
159
- .badge.online { background: rgba(0,255,136,0.12); color: var(--accent); border: 1px solid rgba(0,255,136,0.3); }
160
- .badge.pending { background: rgba(249,115,22,0.12); color: var(--orange); border: 1px solid rgba(249,115,22,0.3); }
161
- .badge-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; animation: pulse 2s infinite; }
162
- @keyframes pulse { 0%,100%{opacity:1;} 50%{opacity:0.3;} }
163
- .container { max-width: 960px; margin: 0 auto; width: 100%; padding: 24px; flex: 1; display: flex; flex-direction: column; gap: 20px; }
164
- .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 14px; }
165
- .card { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 18px; }
166
- .card-title { font-size: 11px; font-family: var(--mono); color: var(--text2); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 6px; display: flex; align-items: center; gap: 6px; }
167
- .metric-val { font-size: 26px; font-weight: 800; font-family: var(--mono); color: var(--accent); }
168
- .metric-sub { font-size: 11px; color: var(--text3); font-family: var(--mono); margin-top: 4px; }
169
- .form-group { margin-bottom: 12px; }
170
- .form-group label { display: block; font-size: 11px; font-family: var(--mono); color: var(--text2); margin-bottom: 4px; }
171
- .form-group input { width: 100%; background: var(--bg2); border: 1px solid var(--border); color: #fff; padding: 8px 12px; border-radius: 6px; font-size: 12px; font-family: var(--mono); }
172
- .form-group input:focus { outline: none; border-color: var(--accent); }
173
- .btn { background: var(--accent); color: #05140c; border: none; padding: 10px 18px; border-radius: 6px; font-size: 12px; font-weight: 700; cursor: pointer; font-family: var(--font); transition: all 0.15s; }
174
- .btn:hover { box-shadow: 0 0 15px var(--accent-glow); transform: translateY(-1px); }
175
- .toast { position: fixed; bottom: 20px; right: 20px; background: var(--card); border: 1px solid var(--accent); padding: 12px 20px; border-radius: 8px; font-size: 12px; color: #fff; display: none; z-index: 100; }
176
- </style>
177
- </head>
178
- <body>
179
- <header>
180
- <div class="brand">
181
- <i class="fa-solid fa-server" style="color:var(--accent);"></i>
182
- ThinkNCollab Agent App<span class="brand-dot">.</span>
183
- </div>
184
- <div id="status-badge" class="badge pending">
185
- <div class="badge-dot"></div>
186
- <span id="status-text">INITIALIZING</span>
187
- </div>
188
- </header>
189
-
190
- <div class="container">
191
- <div class="grid">
192
- <div class="card">
193
- <div class="card-title"><i class="fa-solid fa-microchip"></i> CPU Usage</div>
194
- <div class="metric-val" id="val-cpu">--%</div>
195
- <div class="metric-sub" id="val-cores">-- Cores</div>
196
- </div>
197
- <div class="card">
198
- <div class="card-title"><i class="fa-solid fa-memory"></i> Memory</div>
199
- <div class="metric-val" id="val-mem">--%</div>
200
- <div class="metric-sub" id="val-mem-detail">-- / -- MB</div>
201
- </div>
202
- <div class="card">
203
- <div class="card-title"><i class="fa-solid fa-hard-drive"></i> Primary Disk</div>
204
- <div class="metric-val" id="val-disk">--%</div>
205
- <div class="metric-sub" id="val-disk-detail">-- GB</div>
206
- </div>
207
- <div class="card">
208
- <div class="card-title"><i class="fa-solid fa-clock"></i> Node Uptime</div>
209
- <div class="metric-val" id="val-uptime" style="font-size:18px;">--</div>
210
- <div class="metric-sub" id="val-platform">--</div>
211
- </div>
212
- </div>
213
-
214
- <div class="card">
215
- <div style="font-size:14px; font-weight:700; margin-bottom:14px; display:flex; align-items:center; gap:8px;">
216
- <i class="fa-solid fa-link" style="color:var(--accent);"></i> Connect to ThinkNCollab DevOps Wall
217
- </div>
218
- <div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(220px, 1fr)); gap:12px;">
219
- <div class="form-group">
220
- <label>ThinkNCollab Server URL</label>
221
- <input type="text" id="cfg-server" placeholder="https://thinkncollab.com" value="https://thinkncollab.com" />
222
- </div>
223
- <div class="form-group">
224
- <label>Target Room ID (from URL)</label>
225
- <input type="text" id="cfg-room" placeholder="e.g. 6a318170496c7b00a7f74260" />
226
- </div>
227
- <div class="form-group">
228
- <label>Node Display Name</label>
229
- <input type="text" id="cfg-name" placeholder="prod-web-01" />
230
- </div>
231
- </div>
232
- <div style="display:flex; justify-content:flex-end; margin-top:8px;">
233
- <button class="btn" onclick="saveConnection()"><i class="fa-solid fa-paper-plane"></i> Connect Node</button>
234
- </div>
235
- </div>
236
- </div>
237
-
238
- <div id="toast" class="toast"></div>
239
-
240
- <script>
241
- async function fetchStatus() {
242
- try {
243
- const res = await fetch('/api/status');
244
- const data = await res.json();
245
- if (data.success) {
246
- const t = data.telemetry;
247
- const c = data.config;
248
-
249
- document.getElementById('val-cpu').textContent = t.cpuPercent + '%';
250
- document.getElementById('val-cores').textContent = t.cores + ' Cores';
251
- document.getElementById('val-mem').textContent = t.memoryUsedPct + '%';
252
- document.getElementById('val-mem-detail').textContent = t.memoryUsedMB + ' / ' + t.memoryTotalMB + ' MB';
253
- if (t.disk && t.disk[0]) {
254
- document.getElementById('val-disk').textContent = t.disk[0].usedPct + '%';
255
- document.getElementById('val-disk-detail').textContent = t.disk[0].sizeGB + ' GB';
256
- }
257
- const upHours = Math.floor(t.uptime / 3600);
258
- const upMins = Math.floor((t.uptime % 3600) / 60);
259
- document.getElementById('val-uptime').textContent = upHours + 'h ' + upMins + 'm';
260
- document.getElementById('val-platform').textContent = t.platform;
261
-
262
- const badge = document.getElementById('status-badge');
263
- const badgeText = document.getElementById('status-text');
264
- if (c.status === 'approved') {
265
- badge.className = 'badge online';
266
- badgeText.textContent = 'ONLINE (APPROVED)';
267
- } else if (c.status === 'pending') {
268
- badge.className = 'badge pending';
269
- badgeText.textContent = 'PENDING APPROVAL';
270
- } else {
271
- badge.className = 'badge pending';
272
- badgeText.textContent = 'NOT CONFIGURED';
273
- }
274
-
275
- if (c.serverUrl && !document.getElementById('cfg-server').value) document.getElementById('cfg-server').value = c.serverUrl;
276
- if (c.roomId && !document.getElementById('cfg-room').value) document.getElementById('cfg-room').value = c.roomId;
277
- if (c.name && !document.getElementById('cfg-name').value) document.getElementById('cfg-name').value = c.name;
278
- }
279
- } catch (err) {
280
- console.error('Status fetch error:', err);
281
- }
282
- }
283
-
284
- async function saveConnection() {
285
- const serverUrl = document.getElementById('cfg-server').value.trim();
286
- const roomId = document.getElementById('cfg-room').value.trim();
287
- const name = document.getElementById('cfg-name').value.trim();
288
-
289
- if (!serverUrl || !roomId) {
290
- showToast('Server URL and Room ID are required!');
291
- return;
292
- }
293
-
294
- try {
295
- const res = await fetch('/api/connect', {
296
- method: 'POST',
297
- headers: { 'Content-Type': 'application/json' },
298
- body: JSON.stringify({ serverUrl, roomId, name })
299
- });
300
- const d = await res.json();
301
- if (d.success) {
302
- showToast(d.message || 'Connected successfully!');
303
- fetchStatus();
304
- } else {
305
- showToast(d.error || 'Failed to connect');
306
- }
307
- } catch (err) {
308
- showToast('Connection error: ' + err.message);
309
- }
310
- }
311
-
312
- function showToast(msg) {
313
- const t = document.getElementById('toast');
314
- t.textContent = msg;
315
- t.style.display = 'block';
316
- setTimeout(() => t.style.display = 'none', 4000);
317
- }
318
-
319
- fetchStatus();
320
- setInterval(fetchStatus, 3000);
321
- </script>
322
- </body>
323
- </html>`;
324
- }
325
- }
326
-
327
- module.exports = AgentApp;