thinknagent 0.1.18 → 0.1.19

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.
@@ -18,32 +18,45 @@ program
18
18
  // ── init ─────────────────────────────────────────────────────────────────────
19
19
  program
20
20
  .command('init')
21
- .description('Register this server with ThinkNCollab')
22
- .requiredOption('--server <url>', 'ThinkNCollab server URL (e.g. https://thinkncollab.com)')
23
- .requiredOption('--name <name>', 'Display name for this server on the DevOps Wall')
24
- .option('--room <roomId>', 'Room ID to connect to')
25
- .option('--gpu', 'Enable GPU metrics (requires nvidia-smi)')
26
- .option('--logs <paths>', 'Comma-separated log file paths to stream')
27
- .option('--app-path <path>', 'Path to the deployed application folder (to track version)')
21
+ .description('Register this server with ThinkNCollab DevOps Wall')
22
+ .option('--room <roomId>', 'Room ID to connect to (required)')
23
+ .option('--server <url>', 'ThinkNCollab server URL', 'https://thinkncollab.com')
24
+ .option('--name <name>', 'Display name for this server (default: hostname)')
25
+ .option('--gpu', 'Enable GPU metrics (requires nvidia-smi)')
26
+ .option('--logs <paths>', 'Comma-separated log file paths to stream')
27
+ .option('--app-path <path>', 'Path to the deployed application folder (to track version)')
28
+ .option('-f, --force', 'Force overwrite existing registration')
29
+ .option('-d, --daemon', 'Start background auto-restart daemon immediately after init')
28
30
  .action(async (opts) => {
29
- const existing = store.get('agentToken');
30
- if (existing) {
31
- console.log(chalk.yellow('Already registered. Run `thinknagent status` to check.'));
32
- console.log(chalk.gray('To re-register: thinknagent revoke && thinknagent init ...'));
33
- process.exit(0);
31
+ if (!opts.room) {
32
+ console.error(chalk.red('\n Error: --room <roomId> is required.'));
33
+ console.log(chalk.gray(' Usage: thinknagent init --room <roomId> [--name <name>] [--server <url>]\n'));
34
+ process.exit(1);
35
+ }
36
+
37
+ const os = require('os');
38
+ const existing = store.read();
39
+ if (existing.agentToken && !opts.force) {
40
+ console.log(chalk.yellow('\n Already registered for: ' + (existing.name || existing.agentId)));
41
+ console.log(chalk.gray(' To overwrite config, pass: --force (e.g. thinknagent init --room <roomId> --force)'));
42
+ console.log(chalk.gray(' Or check status: thinknagent status\n'));
43
+ return;
34
44
  }
35
45
 
36
- const agentId = uuid();
46
+ const agentId = existing.agentId || uuid();
47
+ const serverUrl = (opts.server || 'https://thinkncollab.com').replace(/\/$/, '');
48
+ const nodeName = opts.name || os.hostname();
49
+
37
50
  const cfg = {
51
+ ...existing,
38
52
  agentId,
39
- serverUrl: opts.server.replace(/\/$/, ''),
40
- name: opts.name,
41
- gpu: !!opts.gpu,
42
- logs: opts.logs ? opts.logs.split(',').map(s => s.trim()) : [],
53
+ serverUrl,
54
+ name: nodeName,
55
+ gpu: opts.gpu !== undefined ? !!opts.gpu : (existing.gpu || false),
56
+ logs: opts.logs ? opts.logs.split(',').map(s => s.trim()) : (existing.logs || []),
43
57
  roomId: opts.room,
44
- appPath: opts.appPath || null,
45
- alerts: [
46
- // sensible defaults — Owner can edit in browser
58
+ appPath: opts.appPath || existing.appPath || null,
59
+ alerts: existing.alerts || [
47
60
  { id: 'cpu-high', metric: 'cpu.usage', op: 'gt', value: 85, for: 60, severity: 'warning' },
48
61
  { id: 'cpu-crit', metric: 'cpu.usage', op: 'gt', value: 95, for: 30, severity: 'critical' },
49
62
  { id: 'mem-high', metric: 'memory.usedPct', op: 'gt', value: 85, for: 60, severity: 'warning' },
@@ -51,21 +64,34 @@ program
51
64
  ],
52
65
  };
53
66
 
67
+ if (opts.force) {
68
+ delete cfg.agentToken;
69
+ delete cfg.role;
70
+ }
71
+
54
72
  store.write(cfg);
55
73
 
56
74
  console.log(chalk.cyan('\n thinknagent') + chalk.gray(` v${require('../package.json').version}`));
57
75
  console.log(chalk.gray(' ─────────────────────────────────────────'));
58
76
  console.log(` Server : ${chalk.white(cfg.serverUrl)}`);
59
77
  console.log(` Name : ${chalk.white(cfg.name)}`);
78
+ console.log(` Room ID : ${chalk.white(cfg.roomId)}`);
60
79
  console.log(` Agent ID: ${chalk.white(agentId)}`);
61
80
  console.log(` GPU : ${cfg.gpu ? chalk.green('enabled') : chalk.gray('disabled')}`);
62
81
  console.log(` Logs : ${cfg.logs.length ? chalk.white(cfg.logs.join(', ')) : chalk.gray('none')}`);
63
82
  console.log(` App Path: ${cfg.appPath ? chalk.white(cfg.appPath) : chalk.gray('none')}`);
64
83
  console.log(chalk.gray(' ─────────────────────────────────────────'));
65
- console.log(chalk.yellow('\n Next step:'));
66
- console.log(' 1. Run: ' + chalk.cyan('thinknagent start'));
67
- console.log(' 2. An Owner of the target room must approve this agent in ThinkNCollab');
68
- console.log(' 3. Once approved, agent becomes active automatically\n');
84
+ console.log(chalk.green(' Configuration saved successfully!\n'));
85
+
86
+ if (opts.daemon) {
87
+ const DaemonManager = require('../lib/daemon');
88
+ new DaemonManager().startSupervisor();
89
+ } else {
90
+ console.log(chalk.yellow(' Next steps:'));
91
+ console.log(' • Start background auto-restart daemon : ' + chalk.cyan('thinknagent daemon start'));
92
+ console.log(' • Or run directly in terminal : ' + chalk.cyan('thinknagent start'));
93
+ console.log(' • Or launch local GUI app : ' + chalk.cyan('thinknagent app\n'));
94
+ }
69
95
  });
70
96
 
71
97
  // ── start ─────────────────────────────────────────────────────────────────────
@@ -77,44 +103,91 @@ program
77
103
  if (opts.dev) process.env.THINKNAGENT_DEV = '1';
78
104
 
79
105
  const cfg = store.read();
80
- if (!cfg.serverUrl) {
81
- console.error(chalk.red('Not initialized. Run: thinknagent init --server <url> --name <name>'));
106
+ if (!cfg.serverUrl || !cfg.roomId) {
107
+ console.error(chalk.red('\n Not initialized. Run: thinknagent init --room <roomId>'));
82
108
  process.exit(1);
83
109
  }
84
110
 
85
111
  console.log(chalk.cyan(`\n Starting thinknagent — ${cfg.name || cfg.agentId}`));
86
112
  if (!cfg.agentToken) {
87
- console.log(chalk.yellow(' Status: PENDING — waiting for Owner approval\n'));
113
+ console.log(chalk.yellow(' Status: PENDING — waiting for Owner approval in DevOps Wall\n'));
88
114
  }
89
115
 
90
116
  const agent = new Agent();
91
117
  agent.start();
92
118
  });
93
119
 
120
+ // ── stop ──────────────────────────────────────────────────────────────────────
121
+ program
122
+ .command('stop')
123
+ .description('Stop running thinknagent background daemon')
124
+ .action(() => {
125
+ const DaemonManager = require('../lib/daemon');
126
+ new DaemonManager().stopSupervisor();
127
+ });
128
+
129
+ // ── restart ───────────────────────────────────────────────────────────────────
130
+ program
131
+ .command('restart')
132
+ .description('Restart the background daemon')
133
+ .action(() => {
134
+ const DaemonManager = require('../lib/daemon');
135
+ const daemon = new DaemonManager();
136
+ daemon.stopSupervisor();
137
+ setTimeout(() => daemon.startSupervisor(), 1000);
138
+ });
139
+
94
140
  // ── status ────────────────────────────────────────────────────────────────────
95
141
  program
96
142
  .command('status')
97
- .description('Show current agent config and registration status')
143
+ .description('Show current agent config, approval, and daemon status')
98
144
  .action(() => {
145
+ const os = require('os');
99
146
  const cfg = store.read();
100
- if (!cfg.serverUrl) {
101
- console.log(chalk.gray('Not initialized.'));
147
+ const DaemonManager = require('../lib/daemon');
148
+ const daemon = new DaemonManager();
149
+ const isRunning = daemon.isRunning();
150
+
151
+ if (!cfg.serverUrl && !cfg.roomId) {
152
+ console.log(chalk.gray('\n thinknagent is not configured yet.'));
153
+ console.log(chalk.cyan(' Run: thinknagent init --room <roomId>\n'));
102
154
  return;
103
155
  }
104
156
 
105
- console.log(chalk.cyan('\n thinknagent status'));
106
- console.log(chalk.gray(' ────────────────────────────────'));
107
- console.log(` Name : ${chalk.white(cfg.name || '—')}`);
108
- console.log(` Server : ${chalk.white(cfg.serverUrl || '')}`);
109
- console.log(` Agent ID: ${chalk.white(cfg.agentId || '—')}`);
110
- console.log(` Role : ${chalk.white(cfg.role || '—')}`);
111
- console.log(` Room : ${chalk.white(cfg.roomId || '')}`);
112
- console.log(` Status : ${cfg.agentToken ? chalk.green('APPROVED') : chalk.yellow('PENDING')}`);
113
- console.log(` GPU : ${cfg.gpu ? chalk.green('enabled') : chalk.gray('disabled')}`);
114
- console.log(` Logs : ${(cfg.logs||[]).length ? cfg.logs.join(', ') : chalk.gray('none')}`);
115
- console.log(` App Path: ${cfg.appPath ? chalk.white(cfg.appPath) : chalk.gray('none')}`);
116
- console.log(` Alerts : ${(cfg.alerts||[]).length} rule(s)`);
117
- console.log(chalk.gray(' ────────────────────────────────\n'));
157
+ console.log(chalk.cyan('\n thinknagent status') + chalk.gray(` v${require('../package.json').version}`));
158
+ console.log(chalk.gray(' ─────────────────────────────────────────────'));
159
+ console.log(` Name : ${chalk.white(cfg.name || os.hostname())}`);
160
+ console.log(` Server : ${chalk.white(cfg.serverUrl || 'https://thinkncollab.com')}`);
161
+ console.log(` Room ID : ${chalk.white(cfg.roomId || '—')}`);
162
+ console.log(` Agent ID : ${chalk.white(cfg.agentId || '—')}`);
163
+ console.log(` Role : ${chalk.white(cfg.role || 'monitor')}`);
164
+ console.log(` Auth State : ${cfg.agentToken ? chalk.green('APPROVED (Active)') : chalk.yellow('PENDING (Waiting for Owner approval)')}`);
165
+ console.log(` Daemon : ${isRunning ? chalk.green(`RUNNING (PID ${daemon.getPid()})`) : chalk.gray('STOPPED')}`);
166
+ console.log(` GPU : ${cfg.gpu ? chalk.green('enabled') : chalk.gray('disabled')}`);
167
+ console.log(` Logs : ${(cfg.logs||[]).length ? cfg.logs.join(', ') : chalk.gray('none')}`);
168
+ console.log(` App Path : ${cfg.appPath ? chalk.white(cfg.appPath) : chalk.gray('none')}`);
169
+ console.log(chalk.gray(' ─────────────────────────────────────────────\n'));
170
+ });
171
+
172
+ // ── logs ──────────────────────────────────────────────────────────────────────
173
+ program
174
+ .command('logs')
175
+ .description('View recent daemon logs')
176
+ .option('-n, --lines <count>', 'Number of lines to view', '50')
177
+ .action((opts) => {
178
+ const os = require('os');
179
+ const fs = require('fs');
180
+ const path = require('path');
181
+ const logFile = path.join(os.homedir(), '.thinknagent', 'daemon.log');
182
+ if (!fs.existsSync(logFile)) {
183
+ console.log(chalk.gray('No daemon logs found at: ' + logFile));
184
+ return;
185
+ }
186
+ const lines = parseInt(opts.lines, 10) || 50;
187
+ const content = fs.readFileSync(logFile, 'utf8').trim().split('\n');
188
+ console.log(chalk.cyan(`\n --- thinknagent daemon logs (last ${lines} lines) ---`));
189
+ console.log(content.slice(-lines).join('\n'));
190
+ console.log(chalk.cyan(` --- end of logs ---\n`));
118
191
  });
119
192
 
120
193
  // ── revoke ────────────────────────────────────────────────────────────────────
@@ -122,8 +195,94 @@ program
122
195
  .command('revoke')
123
196
  .description('Remove all credentials from this server')
124
197
  .action(() => {
198
+ const DaemonManager = require('../lib/daemon');
199
+ new DaemonManager().stopSupervisor();
125
200
  store.clear();
126
- console.log(chalk.yellow(' Credentials cleared. Run `thinknagent init` to re-register.'));
201
+ console.log(chalk.yellow('\n Credentials cleared from ~/.thinknagent/'));
202
+ console.log(chalk.gray(' To re-register: thinknagent init --room <roomId>\n'));
203
+ });
204
+
205
+ // ── app (Local GUI / Web App) ────────────────────────────────────────────────
206
+ program
207
+ .command('app')
208
+ .description('Launch the ThinkNCollab Agent App (Local GUI Dashboard)')
209
+ .option('-p, --port <port>', 'Local port to run UI on', '4455')
210
+ .option('--no-open', 'Do not open browser automatically')
211
+ .action((opts) => {
212
+ const AgentApp = require('../lib/app');
213
+ const app = new AgentApp(parseInt(opts.port, 10));
214
+ app.start(opts.open !== false);
215
+ });
216
+
217
+ // ── daemon (Background Auto-Restart Service) ──────────────────────────────────
218
+ const daemonCmd = program
219
+ .command('daemon')
220
+ .description('Manage the background auto-restarting daemon');
221
+
222
+ daemonCmd
223
+ .command('start')
224
+ .description('Start the background daemon with auto-restart supervisor')
225
+ .action(() => {
226
+ const DaemonManager = require('../lib/daemon');
227
+ new DaemonManager().startSupervisor();
228
+ });
229
+
230
+ daemonCmd
231
+ .command('stop')
232
+ .description('Stop the background daemon supervisor')
233
+ .action(() => {
234
+ const DaemonManager = require('../lib/daemon');
235
+ new DaemonManager().stopSupervisor();
127
236
  });
128
237
 
238
+ daemonCmd
239
+ .command('restart')
240
+ .description('Restart the background daemon')
241
+ .action(() => {
242
+ const DaemonManager = require('../lib/daemon');
243
+ const daemon = new DaemonManager();
244
+ daemon.stopSupervisor();
245
+ setTimeout(() => daemon.startSupervisor(), 1000);
246
+ });
247
+
248
+ daemonCmd
249
+ .command('status')
250
+ .description('Check daemon process status')
251
+ .action(() => {
252
+ const DaemonManager = require('../lib/daemon');
253
+ new DaemonManager().getStatus();
254
+ });
255
+
256
+ daemonCmd
257
+ .command('install')
258
+ .description('Install as OS background service (Systemd on Linux, Launchd on macOS)')
259
+ .action(() => {
260
+ const DaemonManager = require('../lib/daemon');
261
+ new DaemonManager().installService();
262
+ });
263
+
264
+ // Default banner if no arguments
265
+ if (process.argv.length <= 2) {
266
+ const os = require('os');
267
+ const cfg = store.read();
268
+ const v = require('../package.json').version;
269
+ console.log(chalk.cyan(`\n ╔═══════════════════════════════════════════════════╗`));
270
+ console.log(chalk.cyan(` ║ ThinkNCollab Server Agent v${v} ║`));
271
+ console.log(chalk.cyan(` ╚═══════════════════════════════════════════════════╝`));
272
+ console.log(chalk.gray(` Zero-config server telemetry, terminal & DevOps sync\n`));
273
+
274
+ if (cfg.roomId) {
275
+ console.log(` Configured Room : ${chalk.green(cfg.roomId)} (${cfg.name || os.hostname()})`);
276
+ console.log(` Quick Commands :`);
277
+ console.log(` • ${chalk.cyan('thinknagent daemon start')} (Run in background with auto-restart)`);
278
+ console.log(` • ${chalk.cyan('thinknagent status')} (Check live approval & status)`);
279
+ console.log(` • ${chalk.cyan('thinknagent app')} (Open Desktop GUI dashboard)`);
280
+ console.log(` • ${chalk.cyan('thinknagent logs')} (View recent logs)\n`);
281
+ } else {
282
+ console.log(` Get Started:`);
283
+ console.log(` • ${chalk.cyan('thinknagent init --room <roomId>')} (Register this server)`);
284
+ console.log(` • ${chalk.cyan('thinknagent app')} (Launch local GUI setup)\n`);
285
+ }
286
+ }
287
+
129
288
  program.parse(process.argv);
package/lib/app.js ADDED
@@ -0,0 +1,327 @@
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;
package/lib/connect.js CHANGED
@@ -104,18 +104,15 @@ class Connection {
104
104
  });
105
105
 
106
106
  s.on('agent:approved', ({ agentToken, role, roomId }) => {
107
- // ✅ agentId yahan store mat karo — UUID already store mein sahi hai
108
107
  store.set('agentToken', agentToken);
109
108
  store.set('role', role);
110
109
  store.set('roomId', roomId);
111
- // agentId touch mat karo!
112
110
 
113
111
  this.agentToken = agentToken;
114
112
  this.role = role;
115
113
 
116
- console.log(`[thinknagent] Approved! Reconnecting with token...`);
117
- this.socket.disconnect();
118
- setTimeout(() => this.socket.connect(), 500);
114
+ console.log(`[thinknagent] Approved! Active session ready. Role: ${role} | Room: ${roomId}`);
115
+ this.onReady?.({ role, roomId });
119
116
  });
120
117
  // Server says: "Owner rejected this agent"
121
118
  s.on('agent:rejected', ({ reason }) => {