thinkncollab-shell 0.0.1 → 0.0.3
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/thinknsh.js +3 -2
- package/package.json +8 -5
- package/src/commands/auth/login.js +77 -0
- package/src/commands/auth/logout.js +32 -0
- package/src/commands/auth/whoami.js +47 -0
- package/src/commands/index.js +104 -0
- package/src/commands/message/say.js +43 -0
- package/src/commands/room/join.js +68 -0
- package/src/commands/room/leave.js +33 -0
- package/src/commands/room/rooms.js +66 -0
- package/src/commands/shell/status.js +73 -0
- package/src/commands/team/invite.js +46 -0
- package/src/commands/team/myteam.js +78 -0
- package/src/commands/team/teammates.js +52 -0
- package/src/core/api-client.js +156 -0
- package/src/core/notification-window.js +135 -0
- package/src/core/shell.js +416 -855
- package/src/core/websocket.js +230 -292
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MyTeam Command
|
|
3
|
+
*
|
|
4
|
+
* Usage: myteam
|
|
5
|
+
* myteam [team-id]
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const chalk = require('chalk');
|
|
9
|
+
const Table = require('cli-table3');
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
name: 'myteam',
|
|
13
|
+
description: 'Show team information and members',
|
|
14
|
+
aliases: ['team', 'org'],
|
|
15
|
+
requiresAuth: true,
|
|
16
|
+
|
|
17
|
+
async execute(args, shell) {
|
|
18
|
+
const teamId = args[0] || shell.api.getUser()?.team;
|
|
19
|
+
|
|
20
|
+
if (!teamId) {
|
|
21
|
+
console.log(chalk.yellow('⚠️ You are not part of any team'));
|
|
22
|
+
console.log(chalk.dim('💡 Ask your team admin for an invite'));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
console.log(chalk.cyan(`\n👥 Team Information: ${teamId}`));
|
|
27
|
+
console.log(chalk.dim('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const teamInfo = await shell.api.getTeamInfo(teamId);
|
|
31
|
+
|
|
32
|
+
// Team details
|
|
33
|
+
console.log(` ${chalk.yellow('Name:')} ${teamInfo.name}`);
|
|
34
|
+
console.log(` ${chalk.yellow('Members:')} ${teamInfo.memberCount}`);
|
|
35
|
+
console.log(` ${chalk.yellow('Rooms:')} ${teamInfo.roomCount}`);
|
|
36
|
+
console.log(` ${chalk.yellow('Created:')} ${new Date(teamInfo.createdAt).toLocaleDateString()}`);
|
|
37
|
+
|
|
38
|
+
if (teamInfo.description) {
|
|
39
|
+
console.log(` ${chalk.yellow('Description:')} ${teamInfo.description}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log();
|
|
43
|
+
|
|
44
|
+
// Team members table
|
|
45
|
+
const table = new Table({
|
|
46
|
+
head: ['Status', 'Name', 'Email', 'Role', 'Current Room'],
|
|
47
|
+
colWidths: [8, 20, 25, 12, 20]
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
teamInfo.members.forEach(member => {
|
|
51
|
+
const status = member.online ? chalk.green('●') : chalk.gray('○');
|
|
52
|
+
const room = member.currentRoom || '-';
|
|
53
|
+
table.push([
|
|
54
|
+
status,
|
|
55
|
+
member.name || member.email.split('@')[0],
|
|
56
|
+
member.email,
|
|
57
|
+
member.role || 'member',
|
|
58
|
+
room
|
|
59
|
+
]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
console.log(table.toString());
|
|
63
|
+
console.log();
|
|
64
|
+
|
|
65
|
+
// Team activity
|
|
66
|
+
if (teamInfo.recentActivity && teamInfo.recentActivity.length > 0) {
|
|
67
|
+
console.log(chalk.cyan('📊 Recent Activity:'));
|
|
68
|
+
teamInfo.recentActivity.slice(0, 5).forEach(activity => {
|
|
69
|
+
const time = new Date(activity.timestamp).toLocaleTimeString();
|
|
70
|
+
console.log(chalk.dim(` [${time}] ${activity.user}: ${activity.action}`));
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.log(chalk.red(`❌ Failed to fetch team info: ${error.message}`));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Teammates Command
|
|
3
|
+
*
|
|
4
|
+
* Usage: teammates
|
|
5
|
+
* teammates [name]
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const chalk = require('chalk');
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
name: 'teammates',
|
|
12
|
+
description: 'List online teammates',
|
|
13
|
+
aliases: ['members', 'online'],
|
|
14
|
+
requiresAuth: true,
|
|
15
|
+
|
|
16
|
+
async execute(args, shell) {
|
|
17
|
+
const filter = args[0] ? args[0].toLowerCase() : null;
|
|
18
|
+
|
|
19
|
+
console.log(chalk.cyan('\n👥 Online Teammates:'));
|
|
20
|
+
console.log(chalk.dim('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const teammates = await shell.api.getTeammates();
|
|
24
|
+
|
|
25
|
+
const online = teammates.filter(t => t.online);
|
|
26
|
+
const offline = teammates.filter(t => !t.online);
|
|
27
|
+
|
|
28
|
+
if (online.length === 0) {
|
|
29
|
+
console.log(chalk.yellow(' No teammates online'));
|
|
30
|
+
} else {
|
|
31
|
+
online
|
|
32
|
+
.filter(t => !filter || t.name.toLowerCase().includes(filter) || t.email.toLowerCase().includes(filter))
|
|
33
|
+
.forEach(t => {
|
|
34
|
+
const typing = t.typing ? chalk.green(' (typing...)') : '';
|
|
35
|
+
const room = t.currentRoom ? chalk.dim(` in ${t.currentRoom}`) : '';
|
|
36
|
+
console.log(` ${chalk.green('●')} ${chalk.white(t.name || t.email)}${typing}${room}`);
|
|
37
|
+
|
|
38
|
+
if (t.status && t.status !== 'online') {
|
|
39
|
+
console.log(chalk.dim(` ${t.status}`));
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (offline.length > 0 && !filter) {
|
|
45
|
+
console.log(chalk.dim(`\n ... and ${offline.length} offline`));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.log(chalk.red(`❌ Failed to fetch teammates: ${error.message}`));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ThinkNCollab API Client
|
|
3
|
+
* Handles HTTP requests to the backend
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const https = require('https');
|
|
7
|
+
const http = require('http');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const fs = require('fs-extra');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
|
|
12
|
+
class ThinkNCollabAPI {
|
|
13
|
+
|
|
14
|
+
constructor(options = {}) {
|
|
15
|
+
this.apiUrl = (options.apiUrl || 'https://thinkncollab.com').replace(/\/$/, '');
|
|
16
|
+
this.wsUrl = (options.wsUrl || 'https://thinkncollab.com').replace(/\/$/, '');
|
|
17
|
+
this.session = null;
|
|
18
|
+
this.sessionPath = path.join(os.homedir(), '.thinkncollab', 'session.json');
|
|
19
|
+
|
|
20
|
+
// Load existing session on startup
|
|
21
|
+
this._loadSession();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ─── Session management ───────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
_loadSession() {
|
|
27
|
+
try {
|
|
28
|
+
if (fs.existsSync(this.sessionPath)) {
|
|
29
|
+
this.session = fs.readJsonSync(this.sessionPath);
|
|
30
|
+
}
|
|
31
|
+
} catch {}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
_saveSession(session) {
|
|
35
|
+
try {
|
|
36
|
+
fs.ensureDirSync(path.dirname(this.sessionPath));
|
|
37
|
+
fs.writeJsonSync(this.sessionPath, session);
|
|
38
|
+
this.session = session;
|
|
39
|
+
} catch {}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
_clearSession() {
|
|
43
|
+
try {
|
|
44
|
+
fs.removeSync(this.sessionPath);
|
|
45
|
+
this.session = null;
|
|
46
|
+
} catch {}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
isAuthenticated() {
|
|
50
|
+
return !!(this.session?.token);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getToken() {
|
|
54
|
+
return this.session?.token || null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getUser() {
|
|
58
|
+
return this.session?.user || null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ─── HTTP helper ──────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
_request(method, endpoint, body = null, headers = {}) {
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
const url = new URL(this.apiUrl + endpoint);
|
|
66
|
+
const isHttps = url.protocol === 'https:';
|
|
67
|
+
const lib = isHttps ? https : http;
|
|
68
|
+
|
|
69
|
+
const bodyStr = body ? JSON.stringify(body) : null;
|
|
70
|
+
|
|
71
|
+
const options = {
|
|
72
|
+
hostname: url.hostname,
|
|
73
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
74
|
+
path: url.pathname + url.search,
|
|
75
|
+
method,
|
|
76
|
+
headers: {
|
|
77
|
+
'Content-Type': 'application/json',
|
|
78
|
+
'Accept': 'application/json',
|
|
79
|
+
...headers,
|
|
80
|
+
...(bodyStr ? { 'Content-Length': Buffer.byteLength(bodyStr) } : {})
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const req = lib.request(options, (res) => {
|
|
85
|
+
let data = '';
|
|
86
|
+
res.on('data', chunk => data += chunk);
|
|
87
|
+
res.on('end', () => {
|
|
88
|
+
try {
|
|
89
|
+
const parsed = JSON.parse(data);
|
|
90
|
+
if (res.statusCode >= 400) {
|
|
91
|
+
reject(new Error(parsed.error || parsed.message || `HTTP ${res.statusCode}`));
|
|
92
|
+
} else {
|
|
93
|
+
resolve(parsed);
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
reject(new Error(`Invalid response: ${data}`));
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
req.on('error', (err) => {
|
|
102
|
+
if (err.code === 'ECONNREFUSED') {
|
|
103
|
+
reject(new Error(`Cannot connect to server at ${this.apiUrl} — is it running?`));
|
|
104
|
+
} else {
|
|
105
|
+
reject(err);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (bodyStr) req.write(bodyStr);
|
|
110
|
+
req.end();
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ─── Auth API ─────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
async login(email, password) {
|
|
117
|
+
// Your backend reads from headers
|
|
118
|
+
const result = await this._request('POST', '/thinknsh/login', null, {
|
|
119
|
+
'x-email': email,
|
|
120
|
+
'x-password': password,
|
|
121
|
+
'x-machine-id': require('os').hostname()
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Save session
|
|
125
|
+
this._saveSession({
|
|
126
|
+
token: result.shellToken,
|
|
127
|
+
user: {
|
|
128
|
+
_id: result._id,
|
|
129
|
+
email: result.email,
|
|
130
|
+
name: result.name,
|
|
131
|
+
},
|
|
132
|
+
timestamp: new Date().toISOString()
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
token: result.shellToken,
|
|
137
|
+
user: {
|
|
138
|
+
_id: result._id,
|
|
139
|
+
email: result.email,
|
|
140
|
+
name: result.name,
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async logout() {
|
|
146
|
+
this._clearSession();
|
|
147
|
+
return { success: true };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async whoami() {
|
|
151
|
+
if (!this.isAuthenticated()) throw new Error('Not logged in');
|
|
152
|
+
return this.getUser();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = ThinkNCollabAPI;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ThinkNCollab — Notification Window
|
|
4
|
+
* Spawned automatically by the main shell as a separate terminal process.
|
|
5
|
+
* Connects to the main shell via TCP and displays live notifications.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const net = require('net');
|
|
9
|
+
const chalk = require('chalk');
|
|
10
|
+
|
|
11
|
+
const PORT = parseInt(process.env.THINKNSH_NOTIFY_PORT || '7379');
|
|
12
|
+
const HOST = '127.0.0.1';
|
|
13
|
+
|
|
14
|
+
// ── UI ────────────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
function clearScreen() { process.stdout.write('\x1Bc'); }
|
|
17
|
+
|
|
18
|
+
function drawHeader() {
|
|
19
|
+
const width = process.stdout.columns || 80;
|
|
20
|
+
const title = ' 🔔 ThinkNCollab — Live Notifications ';
|
|
21
|
+
const line = '━'.repeat(width);
|
|
22
|
+
console.log(chalk.cyan(line));
|
|
23
|
+
console.log(chalk.cyan.bold(title.padStart(Math.floor((width + title.length) / 2))));
|
|
24
|
+
console.log(chalk.cyan(line));
|
|
25
|
+
console.log(chalk.dim(` Port: ${PORT} | Notifications will appear here in real time.`));
|
|
26
|
+
console.log(chalk.dim(' Close this window or press Ctrl+C to stop.\n'));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function beep() {
|
|
30
|
+
// Terminal bell — works on Windows CMD, macOS Terminal, Linux
|
|
31
|
+
process.stdout.write('\x07');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function printNotification(data) {
|
|
35
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
36
|
+
const ts = chalk.dim(`[${timestamp}]`);
|
|
37
|
+
|
|
38
|
+
switch (data.type) {
|
|
39
|
+
case 'task_assigned':
|
|
40
|
+
beep();
|
|
41
|
+
console.log(`${ts} 📋 ${chalk.yellow('Task assigned →')} ${chalk.white(data.taskTitle)} ${chalk.dim('by')} ${chalk.cyan(data.assignedBy)}`);
|
|
42
|
+
break;
|
|
43
|
+
case 'task_started':
|
|
44
|
+
console.log(`${ts} ⚡ ${chalk.green(data.userName)} started working on ${chalk.white(data.taskTitle)}`);
|
|
45
|
+
break;
|
|
46
|
+
case 'task_completed':
|
|
47
|
+
beep();
|
|
48
|
+
console.log(`${ts} ✅ ${chalk.green(data.userName)} completed ${chalk.white(data.taskTitle)}`);
|
|
49
|
+
break;
|
|
50
|
+
case 'message':
|
|
51
|
+
beep();
|
|
52
|
+
console.log(`${ts} 💬 ${chalk.cyan(data.from)}: ${data.text}`);
|
|
53
|
+
break;
|
|
54
|
+
case 'userJoined':
|
|
55
|
+
console.log(`${ts} 👤 ${chalk.green(data.username)} joined`);
|
|
56
|
+
break;
|
|
57
|
+
case 'userLeft':
|
|
58
|
+
console.log(`${ts} 👤 ${chalk.yellow(data.username)} left`);
|
|
59
|
+
break;
|
|
60
|
+
case 'connected':
|
|
61
|
+
console.log(`${ts} ${chalk.green('✅ Shell connected')}`);
|
|
62
|
+
break;
|
|
63
|
+
case 'disconnected':
|
|
64
|
+
console.log(`${ts} ${chalk.yellow('⚠️ Shell disconnected')}`);
|
|
65
|
+
break;
|
|
66
|
+
case 'notification': {
|
|
67
|
+
beep();
|
|
68
|
+
const colors = { success: chalk.green, warning: chalk.yellow, error: chalk.red, info: chalk.blue };
|
|
69
|
+
const color = colors[data.level] || chalk.blue;
|
|
70
|
+
console.log(`${ts} 📢 ${color(`${data.title}: ${data.message}`)}`);
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
case 'typing':
|
|
74
|
+
process.stdout.write(`\r${chalk.dim(`✏️ ${data.username} is typing... `)}`);
|
|
75
|
+
break;
|
|
76
|
+
case 'stoppedTyping':
|
|
77
|
+
process.stdout.write('\r' + ' '.repeat(40) + '\r');
|
|
78
|
+
break;
|
|
79
|
+
default:
|
|
80
|
+
console.log(`${ts} ${chalk.white(JSON.stringify(data))}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── TCP Client ────────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
function connect() {
|
|
87
|
+
clearScreen();
|
|
88
|
+
drawHeader();
|
|
89
|
+
console.log(chalk.dim(` Connecting to shell on port ${PORT}...`));
|
|
90
|
+
|
|
91
|
+
const client = new net.Socket();
|
|
92
|
+
let buffer = '';
|
|
93
|
+
|
|
94
|
+
client.connect(PORT, HOST, () => {
|
|
95
|
+
console.log(chalk.green(' ✅ Connected to shell!\n'));
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
client.on('data', (chunk) => {
|
|
99
|
+
buffer += chunk.toString();
|
|
100
|
+
const lines = buffer.split('\n');
|
|
101
|
+
buffer = lines.pop();
|
|
102
|
+
|
|
103
|
+
lines.forEach(line => {
|
|
104
|
+
if (!line.trim()) return;
|
|
105
|
+
try {
|
|
106
|
+
const data = JSON.parse(line);
|
|
107
|
+
printNotification(data);
|
|
108
|
+
} catch {
|
|
109
|
+
console.log(chalk.dim(line));
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
client.on('close', () => {
|
|
115
|
+
console.log(chalk.yellow('\n Shell disconnected. Retrying in 3s...'));
|
|
116
|
+
setTimeout(connect, 3000);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
client.on('error', (err) => {
|
|
120
|
+
if (err.code === 'ECONNREFUSED') {
|
|
121
|
+
console.log(chalk.dim(` Waiting for shell on port ${PORT}...`));
|
|
122
|
+
} else {
|
|
123
|
+
console.log(chalk.dim(` Connection error: ${err.message}`));
|
|
124
|
+
}
|
|
125
|
+
setTimeout(connect, 3000);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── Ctrl+C ────────────────────────────────────────────────────────────────────
|
|
130
|
+
process.on('SIGINT', () => {
|
|
131
|
+
console.log(chalk.yellow('\n Notification window closed.'));
|
|
132
|
+
process.exit(0);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
connect();
|