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.
- package/bin/thinknagent.js +203 -44
- package/lib/app.js +327 -0
- package/lib/connect.js +2 -5
- package/lib/daemon.js +193 -0
- package/lib/logwatcher.js +3 -1
- package/lib/metrics.js +31 -0
- package/lib/shell.js +45 -28
- package/lib/supervisor.js +58 -0
- package/package.json +1 -1
- package/thinknagent.sh +560 -0
package/bin/thinknagent.js
CHANGED
|
@@ -18,32 +18,45 @@ program
|
|
|
18
18
|
// ── init ─────────────────────────────────────────────────────────────────────
|
|
19
19
|
program
|
|
20
20
|
.command('init')
|
|
21
|
-
.description('Register this server with ThinkNCollab')
|
|
22
|
-
.
|
|
23
|
-
.
|
|
24
|
-
.option('--
|
|
25
|
-
.option('--gpu',
|
|
26
|
-
.option('--logs <paths>',
|
|
27
|
-
.option('--app-path <path>',
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
console.log(chalk.
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
40
|
-
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.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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 --
|
|
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
|
|
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
|
-
|
|
101
|
-
|
|
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
|
|
108
|
-
console.log(` Server
|
|
109
|
-
console.log(`
|
|
110
|
-
console.log(`
|
|
111
|
-
console.log(`
|
|
112
|
-
console.log(`
|
|
113
|
-
console.log(`
|
|
114
|
-
console.log(`
|
|
115
|
-
console.log(`
|
|
116
|
-
console.log(`
|
|
117
|
-
console.log(chalk.gray('
|
|
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
|
|
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!
|
|
117
|
-
this.
|
|
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 }) => {
|