thinkncollab-shell 0.0.6 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Readme.md +322 -179
- package/bin/thinknsh.js +3 -3
- package/package.json +1 -1
- package/src/commands/auth/whoami.js +44 -39
- package/src/commands/index.js +7 -0
- package/src/commands/tasks/complete.js +1 -1
- package/src/commands/tasks/task-status.js +61 -0
- package/src/commands/tasks/tasks.js +165 -56
- package/src/core/api-client.js +64 -28
- package/src/core/project-config.js +1 -1
- package/src/core/shell.js +68 -49
- package/src/core/websocket.js +1 -1
package/bin/thinknsh.js
CHANGED
|
@@ -46,9 +46,9 @@ if (args.includes('--version') || args.includes('-v')) {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
const shell = new ThinkNCollabShell({
|
|
49
|
-
serverUrl: process.env.THINKNCOLLAB_SERVER || '
|
|
50
|
-
apiUrl: process.env.THINKNCOLLAB_API_URL || '
|
|
51
|
-
wsUrl: process.env.THINKNCOLLAB_WS_URL || '
|
|
49
|
+
serverUrl: process.env.THINKNCOLLAB_SERVER || 'http://localhost:3001', // ← /thinknsh hata
|
|
50
|
+
apiUrl: process.env.THINKNCOLLAB_API_URL || 'http://localhost:3001',
|
|
51
|
+
wsUrl: process.env.THINKNCOLLAB_WS_URL || 'http://localhost:3001',
|
|
52
52
|
autoConnect: process.env.THINKNCOLLAB_AUTO_CONNECT === 'true'
|
|
53
53
|
});
|
|
54
54
|
|
package/package.json
CHANGED
|
@@ -1,47 +1,52 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Whoami Command
|
|
3
|
-
*
|
|
4
|
-
* Usage: whoami
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
1
|
const chalk = require('chalk');
|
|
8
2
|
|
|
9
3
|
module.exports = {
|
|
10
4
|
name: 'whoami',
|
|
11
5
|
description: 'Show current user information',
|
|
12
6
|
aliases: ['user', 'me'],
|
|
13
|
-
requiresAuth:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
7
|
+
requiresAuth: false,
|
|
8
|
+
|
|
9
|
+
async execute(args, shell) {
|
|
10
|
+
const user = shell.api.getUser();
|
|
11
|
+
const room = shell.ws.getCurrentRoom?.();
|
|
12
|
+
const session = shell.api.session;
|
|
13
|
+
|
|
14
|
+
// ✅ Server se last session history fetch karo
|
|
15
|
+
let lastSession = null;
|
|
16
|
+
try {
|
|
17
|
+
const history = await shell.api._request('GET', '/thinknsh/session/history');
|
|
18
|
+
// Current session chhod ke last wali lo
|
|
19
|
+
const prev = history.find(s => s._id !== session?.shellSessionId);
|
|
20
|
+
if (prev) lastSession = prev;
|
|
21
|
+
} catch {}
|
|
22
|
+
|
|
23
|
+
console.log(chalk.cyan('\n👤 User Information:'));
|
|
24
|
+
console.log(chalk.dim('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
25
|
+
console.log(` ${chalk.yellow('Name:')} ${user.name || 'Not set'}`);
|
|
26
|
+
console.log(` ${chalk.yellow('Email:')} ${user.email}`);
|
|
27
|
+
console.log(` ${chalk.yellow('User ID:')} ${user._id || 'N/A'}`);
|
|
28
|
+
console.log(` ${chalk.yellow('Type:')} ${user.userType || 'User'}`);
|
|
29
|
+
console.log(` ${chalk.yellow('Status:')} ${shell.ws.isConnected?.() ? chalk.green('● Online') : chalk.gray('○ Offline')}`);
|
|
30
|
+
|
|
31
|
+
if (room) {
|
|
32
|
+
console.log(` ${chalk.yellow('Room:')} ${chalk.cyan(room)}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (session?.timestamp) {
|
|
36
|
+
console.log(` ${chalk.yellow('Login At:')} ${chalk.dim(new Date(session.timestamp).toLocaleString())}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ✅ Server se actual logout time
|
|
40
|
+
if (lastSession?.logoutAt) {
|
|
41
|
+
console.log(` ${chalk.yellow('Last Logout:')} ${chalk.dim(new Date(lastSession.logoutAt).toLocaleString())}`);
|
|
42
|
+
} else if (lastSession?.lastSeenAt) {
|
|
43
|
+
console.log(` ${chalk.yellow('Last Seen:')} ${chalk.dim(new Date(lastSession.lastSeenAt).toLocaleString())} ${chalk.red('(crash?)')}`);
|
|
46
44
|
}
|
|
45
|
+
|
|
46
|
+
if (lastSession?.machineId) {
|
|
47
|
+
console.log(` ${chalk.yellow('Last Machine:')} ${chalk.dim(lastSession.machineId)}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log();
|
|
51
|
+
}
|
|
47
52
|
};
|
package/src/commands/index.js
CHANGED
|
@@ -51,13 +51,20 @@ const load = (path, category) => {
|
|
|
51
51
|
// Tasks
|
|
52
52
|
load('./tasks/tasks', 'tasks');
|
|
53
53
|
load('./tasks/task', 'tasks');
|
|
54
|
+
load('./tasks/task-status', 'tasks');
|
|
54
55
|
// load('./tasks/start', 'tasks');
|
|
55
56
|
load('./tasks/complete', 'tasks');
|
|
56
57
|
// load('./tasks/comment', 'tasks');
|
|
57
58
|
load('./tasks/create-task', 'tasks');
|
|
59
|
+
|
|
58
60
|
|
|
59
61
|
// Terminal
|
|
60
62
|
load('./terminal/share', 'terminal');
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
register(command, category) {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// src/commands/tasks/task-status.js
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
|
|
4
|
+
const STATUS_MAP = {
|
|
5
|
+
accept: 'accepted',
|
|
6
|
+
start: 'inprogress',
|
|
7
|
+
complete: 'completed',
|
|
8
|
+
reject: 'rejected',
|
|
9
|
+
done: 'completed',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
name: 'task-status',
|
|
14
|
+
description: 'Update your task status',
|
|
15
|
+
aliases: ['accept', 'start', 'complete', 'reject', 'done'],
|
|
16
|
+
requiresAuth: true,
|
|
17
|
+
requiresRoom: true,
|
|
18
|
+
|
|
19
|
+
async execute(args, shell) {
|
|
20
|
+
const roomId = shell.ws.getCurrentRoom();
|
|
21
|
+
const taskId = args[0];
|
|
22
|
+
// invoked as 'accept <id>', 'start <id>', etc.
|
|
23
|
+
const verb = shell._lastCommand || 'task-status';
|
|
24
|
+
const status = STATUS_MAP[verb] || args[1];
|
|
25
|
+
|
|
26
|
+
if (!taskId) {
|
|
27
|
+
console.log(chalk.yellow(' Usage: accept <taskId> | start <taskId> | complete <taskId>'));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (!status) {
|
|
31
|
+
console.log(chalk.yellow(' Usage: task-status <taskId> <accepted|inprogress|completed|rejected>'));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const statusColors = {
|
|
36
|
+
accepted: chalk.green,
|
|
37
|
+
inprogress: chalk.blue,
|
|
38
|
+
completed: chalk.green,
|
|
39
|
+
rejected: chalk.red,
|
|
40
|
+
};
|
|
41
|
+
const col = statusColors[status] || chalk.white;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const result = await shell.api._request(
|
|
45
|
+
'POST',
|
|
46
|
+
`/thinknsh/${roomId}/tasks/${taskId}/status`,
|
|
47
|
+
{ status }
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
console.log(col(`\n ✓ Task "${result.taskTitle}" → ${status}`));
|
|
51
|
+
if (result.allCompleted) {
|
|
52
|
+
console.log(chalk.green(' 🎉 All assignees done — task fully completed!\n'));
|
|
53
|
+
} else {
|
|
54
|
+
console.log('');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.log(chalk.red(`\n ❌ ${err.message}\n`));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
@@ -1,74 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/commands/tasks/tasks.js — List my tasks + suggestions in current room
|
|
3
|
+
*/
|
|
1
4
|
const chalk = require('chalk');
|
|
2
5
|
|
|
3
6
|
module.exports = {
|
|
4
|
-
name:
|
|
5
|
-
description:
|
|
6
|
-
aliases:
|
|
7
|
+
name: 'tasks',
|
|
8
|
+
description: 'List your tasks + suggested tasks in the current room',
|
|
9
|
+
aliases: ['mytasks', 'my-tasks'],
|
|
7
10
|
requiresAuth: true,
|
|
11
|
+
requiresRoom: true,
|
|
8
12
|
|
|
9
13
|
async execute(args, shell) {
|
|
10
|
-
const user
|
|
11
|
-
const
|
|
12
|
-
|
|
14
|
+
const user = shell.api.getUser();
|
|
15
|
+
const roomId = shell.ws.getCurrentRoom();
|
|
16
|
+
|
|
17
|
+
if (!user) { console.log(chalk.red('❌ Please login first')); return; }
|
|
18
|
+
if (!roomId) { console.log(chalk.yellow('⚠️ Join a room first: join <room-id>')); return; }
|
|
19
|
+
|
|
20
|
+
// ── Flags ────────────────────────────────────────────────────────────
|
|
21
|
+
const showAll = args.includes('--all') || args.includes('-a');
|
|
22
|
+
const noSuggestions = args.includes('--no-suggest') || args.includes('-ns');
|
|
23
|
+
const filterStatus = args.find(a => ['pending','inprogress','review','completed','reopened'].includes(a));
|
|
24
|
+
const topN = args.find(a => /^\d+$/.test(a)) || 5;
|
|
25
|
+
|
|
26
|
+
// ── Color maps ───────────────────────────────────────────────────────
|
|
27
|
+
const stColors = {
|
|
28
|
+
pending: chalk.yellow,
|
|
29
|
+
inprogress: chalk.blue,
|
|
30
|
+
review: chalk.magenta,
|
|
31
|
+
completed: chalk.green,
|
|
32
|
+
reopened: chalk.cyan,
|
|
33
|
+
};
|
|
34
|
+
const prColors = {
|
|
35
|
+
critical: chalk.red,
|
|
36
|
+
high: chalk.hex('#FF6B35'),
|
|
37
|
+
medium: chalk.yellow,
|
|
38
|
+
low: chalk.green,
|
|
39
|
+
};
|
|
40
|
+
const scoreColor = s => s >= 80 ? chalk.red(s) : s >= 60 ? chalk.yellow(s) : chalk.green(s);
|
|
41
|
+
const plColor = pl => ({
|
|
42
|
+
CRITICAL: chalk.red,
|
|
43
|
+
HIGH: chalk.hex('#FF6B35'),
|
|
44
|
+
MEDIUM: chalk.yellow,
|
|
45
|
+
LOW: chalk.green,
|
|
46
|
+
'VERY LOW': chalk.dim,
|
|
47
|
+
}[pl] || chalk.white);
|
|
48
|
+
|
|
49
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
50
|
+
const fmtDue = dueDate => {
|
|
51
|
+
if (!dueDate) return '';
|
|
52
|
+
const d = new Date(dueDate);
|
|
53
|
+
const days = Math.ceil((d - new Date().setHours(0,0,0,0)) / 86400000);
|
|
54
|
+
const str = d.toLocaleDateString('en-IN', { day:'2-digit', month:'short' });
|
|
55
|
+
if (days < 0) return chalk.red(` due:${str}(!)`);
|
|
56
|
+
if (days === 0) return chalk.yellow(` due:TODAY`);
|
|
57
|
+
if (days === 1) return chalk.yellow(` due:${str}`);
|
|
58
|
+
return chalk.dim(` due:${str}`);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const divider = (color = chalk.cyan) => color(' ' + '─'.repeat(58));
|
|
62
|
+
const subline = (label, val) =>
|
|
63
|
+
` ${chalk.dim(label+':')} ${chalk.dim(val)}`;
|
|
64
|
+
|
|
65
|
+
// ── Fetch ────────────────────────────────────────────────────────────
|
|
66
|
+
process.stdout.write(chalk.dim(' Fetching tasks...\r'));
|
|
67
|
+
|
|
68
|
+
let result;
|
|
13
69
|
try {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// FIX: getCurrentRoom() lives on shell.ws, not shell.api
|
|
20
|
-
const roomId = shell.ws.getCurrentRoom();
|
|
21
|
-
|
|
22
|
-
if (!roomId) {
|
|
23
|
-
console.log(chalk.yellow('⚠️ Please join a room first: join <room-id>'));
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (!shell.api) {
|
|
28
|
-
console.log(chalk.red('❌ API not initialized'));
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
console.log(chalk.dim(' Fetching tasks...'));
|
|
33
|
-
|
|
34
|
-
// FIX: pass `roomId` (was passing undefined `roomId` from shell.api.getCurrentRoom())
|
|
35
|
-
const result = await shell.api.mytasks(user, token, roomId);
|
|
36
|
-
|
|
37
|
-
if (!result || !result.tasks || result.tasks.length === 0) {
|
|
38
|
-
console.log(chalk.yellow(' No tasks found in this room.'));
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
console.log(chalk.cyan(`\n📋 Your Tasks in room: ${chalk.white(roomId)}\n`));
|
|
43
|
-
console.log(chalk.dim('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
44
|
-
|
|
45
|
-
result.tasks.forEach((task, index) => {
|
|
46
|
-
const statusColor = {
|
|
47
|
-
completed: chalk.green,
|
|
48
|
-
in_progress: chalk.yellow,
|
|
49
|
-
pending: chalk.dim,
|
|
50
|
-
}[task.status] || chalk.white;
|
|
51
|
-
|
|
52
|
-
const status = statusColor(`[${task.status || 'pending'}]`);
|
|
53
|
-
const title = chalk.white(task.title || task.name || 'Untitled');
|
|
54
|
-
const due = task.dueDate ? chalk.dim(` — due: ${new Date(task.dueDate).toLocaleDateString()}`) : '';
|
|
70
|
+
result = await shell.api._request('GET', `/thinknsh/${roomId}/tasks?n=${topN}`);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
console.log(chalk.red(`\n ❌ ${err.message}\n`));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
55
75
|
|
|
56
|
-
|
|
76
|
+
process.stdout.write(' '.repeat(30) + '\r'); // clear spinner line
|
|
77
|
+
|
|
78
|
+
// ════════════════════════════════════════════════════════════════════
|
|
79
|
+
// MY TASKS
|
|
80
|
+
// ════════════════════════════════════════════════════════════════════
|
|
81
|
+
let tasks = result.tasks || [];
|
|
82
|
+
|
|
83
|
+
if (filterStatus) tasks = tasks.filter(t => t.status === filterStatus);
|
|
84
|
+
if (!showAll) tasks = tasks.filter(t => !['completed','done'].includes(t.status));
|
|
85
|
+
|
|
86
|
+
console.log(divider());
|
|
87
|
+
console.log(chalk.cyan(` MY TASKS`) + chalk.dim(` (${tasks.length} active${result.total > tasks.length ? ` of ${result.total}` : ''})`));
|
|
88
|
+
console.log(divider());
|
|
89
|
+
|
|
90
|
+
if (!tasks.length) {
|
|
91
|
+
console.log(chalk.dim('\n No assigned tasks.\n'));
|
|
92
|
+
} else {
|
|
93
|
+
tasks.forEach((task, i) => {
|
|
94
|
+
const sc = stColors[task.status] || chalk.white;
|
|
95
|
+
const pc = prColors[task.priority] || chalk.white;
|
|
96
|
+
|
|
97
|
+
const num = chalk.dim(` ${String(i+1).padStart(2)}.`);
|
|
98
|
+
const status = sc(`[${(task.status || 'unknown').padEnd(10)}]`);
|
|
99
|
+
const title = task.status === 'completed'
|
|
100
|
+
? chalk.dim(task.title)
|
|
101
|
+
: chalk.white(task.title);
|
|
102
|
+
const due = fmtDue(task.dueDate);
|
|
103
|
+
const myStatus = task.myStatus && task.myStatus !== task.status
|
|
104
|
+
? chalk.dim(` (${task.myStatus})`)
|
|
105
|
+
: '';
|
|
106
|
+
const col = task.column ? chalk.dim(` [${task.column}]`) : '';
|
|
107
|
+
|
|
108
|
+
console.log(`\n${num} ${status} ${title}`);
|
|
109
|
+
console.log(subline('id', task._id) +
|
|
110
|
+
` ${chalk.dim('priority:')} ${pc(task.priority)}` +
|
|
111
|
+
due + myStatus + col);
|
|
57
112
|
|
|
58
113
|
if (task.description) {
|
|
59
|
-
|
|
114
|
+
const desc = task.description.length > 90
|
|
115
|
+
? task.description.slice(0, 87) + '...'
|
|
116
|
+
: task.description;
|
|
117
|
+
console.log(` ${chalk.dim(desc)}`);
|
|
60
118
|
}
|
|
61
119
|
});
|
|
120
|
+
}
|
|
62
121
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
122
|
+
// ════════════════════════════════════════════════════════════════════
|
|
123
|
+
// SUGGESTIONS
|
|
124
|
+
// ════════════════════════════════════════════════════════════════════
|
|
125
|
+
if (!noSuggestions) {
|
|
126
|
+
const suggestions = result.suggestions?.tasks || [];
|
|
127
|
+
const insights = result.suggestions?.insights || {};
|
|
128
|
+
const meta = result.suggestions?.metadata || {};
|
|
129
|
+
|
|
130
|
+
console.log('\n' + divider(chalk.magenta));
|
|
131
|
+
console.log(chalk.magenta(` SUGGESTED FOR YOU`) +
|
|
132
|
+
chalk.dim(` (top ${suggestions.length}${meta.totalScored ? ` of ${meta.totalScored} available` : ''})`));
|
|
133
|
+
console.log(divider(chalk.magenta));
|
|
134
|
+
|
|
135
|
+
if (!suggestions.length) {
|
|
136
|
+
console.log(chalk.dim('\n No suggestions — all tasks are assigned.\n'));
|
|
69
137
|
} else {
|
|
70
|
-
|
|
138
|
+
suggestions.forEach((task, i) => {
|
|
139
|
+
const pc = prColors[task.priority] || chalk.white;
|
|
140
|
+
const plc = plColor(task.priorityLevel);
|
|
141
|
+
const due = fmtDue(task.dueDate);
|
|
142
|
+
const col = task.column ? chalk.dim(` [${task.column}]`) : '';
|
|
143
|
+
const assignees = task.assigneeCount
|
|
144
|
+
? chalk.dim(` · ${task.assigneeCount} assigned`)
|
|
145
|
+
: chalk.dim(' · unassigned');
|
|
146
|
+
const urgentTag = task.isOverdue
|
|
147
|
+
? chalk.red(' ⚠ OVERDUE')
|
|
148
|
+
: task.isUrgent ? chalk.yellow(' ⏰ URGENT') : '';
|
|
149
|
+
|
|
150
|
+
const num = chalk.dim(` ${String(i+1).padStart(2)}.`);
|
|
151
|
+
const score = `[score:${scoreColor(task.score)}]`;
|
|
152
|
+
const level = plc(`[${task.priorityLevel}]`);
|
|
153
|
+
|
|
154
|
+
console.log(`\n${num} ${score} ${level} ${chalk.white(task.title)}${urgentTag}`);
|
|
155
|
+
console.log(subline('id', task._id) +
|
|
156
|
+
` ${chalk.dim('priority:')} ${pc(task.priority)}` +
|
|
157
|
+
due + col + assignees);
|
|
158
|
+
|
|
159
|
+
if (task.priorityReason) {
|
|
160
|
+
console.log(` ${chalk.dim('↳ ' + task.priorityReason)}`);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// ── Insights bar ─────────────────────────────────────────────
|
|
165
|
+
console.log('\n' + divider(chalk.dim));
|
|
166
|
+
if (insights.focusRecommendation) {
|
|
167
|
+
console.log(` ${chalk.magenta('💡')} ${chalk.white(insights.focusRecommendation)}`);
|
|
168
|
+
}
|
|
169
|
+
if (insights.estimatedTime) {
|
|
170
|
+
console.log(` ${chalk.dim('⏱ Est. time:')} ${chalk.white(insights.estimatedTime)}`);
|
|
171
|
+
}
|
|
172
|
+
if (meta.scoreRange) {
|
|
173
|
+
console.log(` ${chalk.dim('📊 Score range:')} ${chalk.white(`${meta.scoreRange.min}–${meta.scoreRange.max}`)} ${chalk.dim('avg:')} ${chalk.white(meta.scoreRange.avg)}`);
|
|
174
|
+
}
|
|
71
175
|
}
|
|
72
176
|
}
|
|
177
|
+
|
|
178
|
+
// ── Footer ───────────────────────────────────────────────────────────
|
|
179
|
+
console.log('\n' + divider());
|
|
180
|
+
console.log(chalk.dim(' task <id> · complete <id> · tasks --all · tasks 10 · tasks --no-suggest'));
|
|
181
|
+
console.log(divider() + '\n');
|
|
73
182
|
}
|
|
74
183
|
};
|
package/src/core/api-client.js
CHANGED
|
@@ -11,8 +11,8 @@ const os = require('os');
|
|
|
11
11
|
class ThinkNCollabAPI {
|
|
12
12
|
|
|
13
13
|
constructor(options = {}) {
|
|
14
|
-
this.apiUrl = (options.apiUrl || '
|
|
15
|
-
this.wsUrl = (options.wsUrl || '
|
|
14
|
+
this.apiUrl = (options.apiUrl || 'http://localhost:3001').replace(/\/$/, '');
|
|
15
|
+
this.wsUrl = (options.wsUrl || 'http://localhost:3001').replace(/\/$/, '');
|
|
16
16
|
this.session = null;
|
|
17
17
|
this.sessionPath = path.join(os.homedir(), '.thinkncollab', 'session.json');
|
|
18
18
|
this._loadSession();
|
|
@@ -115,37 +115,73 @@ class ThinkNCollabAPI {
|
|
|
115
115
|
|
|
116
116
|
// ─── Auth ─────────────────────────────────────────────────────────────────
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
118
|
+
async login(email, password) {
|
|
119
|
+
const result = await this._request('POST', '/thinknsh/login', null, {
|
|
120
|
+
'x-email': email,
|
|
121
|
+
'x-password': password,
|
|
122
|
+
'x-machine-id': os.hostname()
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
this._saveSession({
|
|
126
|
+
token: result.shellToken,
|
|
127
|
+
shellSessionId: result.shellSessionId, // ✅ add karo
|
|
128
|
+
user: {
|
|
129
|
+
_id: result._id,
|
|
130
|
+
userId: result._id,
|
|
131
|
+
email: result.email,
|
|
132
|
+
name: result.name,
|
|
133
|
+
userType: result.userType || result.model || 'User',
|
|
134
|
+
},
|
|
135
|
+
timestamp: new Date().toISOString()
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
token: result.shellToken,
|
|
140
|
+
user: this.getUser()
|
|
141
|
+
};
|
|
142
|
+
}
|
|
124
143
|
|
|
125
|
-
|
|
126
|
-
this._saveSession({
|
|
127
|
-
token: result.shellToken,
|
|
128
|
-
user: {
|
|
129
|
-
_id: result._id,
|
|
130
|
-
userId: result._id, // explicit — ws needs this
|
|
131
|
-
email: result.email,
|
|
132
|
-
name: result.name,
|
|
133
|
-
userType: result.userType || result.model || 'User',
|
|
134
|
-
},
|
|
135
|
-
timestamp: new Date().toISOString()
|
|
136
|
-
});
|
|
144
|
+
// ─── Session event logger ─────────────────────────────────────────────────
|
|
137
145
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
146
|
+
async logEvent(type, roomId = null, meta = {}) {
|
|
147
|
+
const sessionId = this.session?.shellSessionId;
|
|
148
|
+
if (!sessionId) return;
|
|
149
|
+
await this._request('POST', '/thinknsh/session/event', {
|
|
150
|
+
sessionId, type, roomId, meta
|
|
151
|
+
}).catch(() => {}); // silently fail — never block shell
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ─── Heartbeat — keeps lastSeenAt fresh while shell is open ──────────────
|
|
155
|
+
|
|
156
|
+
startHeartbeat(intervalMs = 60000) {
|
|
157
|
+
if (this._heartbeatTimer) return; // already running
|
|
158
|
+
this._heartbeatTimer = setInterval(() => {
|
|
159
|
+
const sessionId = this.session?.shellSessionId;
|
|
160
|
+
if (!sessionId) return;
|
|
161
|
+
this._request('POST', '/thinknsh/session/event', {
|
|
162
|
+
sessionId,
|
|
163
|
+
type: 'heartbeat',
|
|
164
|
+
meta: { ts: new Date().toISOString() }
|
|
165
|
+
}).catch(() => {});
|
|
166
|
+
}, intervalMs);
|
|
167
|
+
}
|
|
143
168
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
169
|
+
stopHeartbeat() {
|
|
170
|
+
if (this._heartbeatTimer) {
|
|
171
|
+
clearInterval(this._heartbeatTimer);
|
|
172
|
+
this._heartbeatTimer = null;
|
|
147
173
|
}
|
|
174
|
+
}
|
|
148
175
|
|
|
176
|
+
async logout() {
|
|
177
|
+
const sessionId = this.session?.shellSessionId;
|
|
178
|
+
if (sessionId) {
|
|
179
|
+
await this._request('POST', '/thinknsh/session/end', { sessionId }).catch(() => {});
|
|
180
|
+
}
|
|
181
|
+
this.stopHeartbeat();
|
|
182
|
+
this._clearSession();
|
|
183
|
+
return { success: true };
|
|
184
|
+
}
|
|
149
185
|
async whoami() {
|
|
150
186
|
if (!this.isAuthenticated()) throw new Error('Not logged in');
|
|
151
187
|
return this.getUser();
|
|
@@ -18,7 +18,7 @@ const fs = require('fs-extra');
|
|
|
18
18
|
const path = require('path');
|
|
19
19
|
const os = require('os');
|
|
20
20
|
|
|
21
|
-
const CONFIG_FILE = '
|
|
21
|
+
const CONFIG_FILE = 'tncproject';
|
|
22
22
|
const MAGIC = Buffer.from('TNCP');
|
|
23
23
|
const ROOM_ID_SIZE = 24;
|
|
24
24
|
const MIN_SIZE = 4 + 1 + ROOM_ID_SIZE + 12 + 16 + 2; // 59 bytes minimum
|