thinkncollab-shell 0.0.6 โ†’ 0.0.8

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 CHANGED
@@ -46,7 +46,7 @@ if (args.includes('--version') || args.includes('-v')) {
46
46
  }
47
47
 
48
48
  const shell = new ThinkNCollabShell({
49
- serverUrl: process.env.THINKNCOLLAB_SERVER || 'https://thinkncollab.com', // โ† /thinknsh hata
49
+ serverUrl: process.env.THINKNCOLLAB_SERVER || 'https://thinkncollab.com',
50
50
  apiUrl: process.env.THINKNCOLLAB_API_URL || 'https://thinkncollab.com',
51
51
  wsUrl: process.env.THINKNCOLLAB_WS_URL || 'https://thinkncollab.com',
52
52
  autoConnect: process.env.THINKNCOLLAB_AUTO_CONNECT === 'true'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkncollab-shell",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "ThinkNCollab Custom Shell with WebSocket collaboration",
5
5
  "main": "src/core/shell.js",
6
6
  "bin": {
@@ -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: true,
14
-
15
- async execute(args, shell) {
16
- const user = shell.api.getUser();
17
- const room = shell.api.getCurrentRoom();
18
-
19
- console.log(chalk.cyan('\n๐Ÿ‘ค User Information:'));
20
- console.log(chalk.dim('โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”'));
21
- console.log(` ${chalk.yellow('Email:')} ${user.email}`);
22
- console.log(` ${chalk.yellow('Name:')} ${user.name || 'Not set'}`);
23
- console.log(` ${chalk.yellow('User ID:')} ${user.id || 'N/A'}`);
24
-
25
- if (user.team) {
26
- console.log(` ${chalk.yellow('Team:')} ${user.team}`);
27
- }
28
-
29
- if (user.role) {
30
- console.log(` ${chalk.yellow('Role:')} ${user.role}`);
31
- }
32
-
33
- console.log(` ${chalk.yellow('Status:')} ${shell.api.isConnected() ? chalk.green('Online') : chalk.gray('Offline')}`);
34
-
35
- if (room) {
36
- console.log(` ${chalk.yellow('Room:')} ${chalk.cyan(room.name)} (${room.id})`);
37
- }
38
-
39
- // Show last login if available
40
- if (user.lastLogin) {
41
- const lastLogin = new Date(user.lastLogin).toLocaleString();
42
- console.log(` ${chalk.yellow('Last Login:')} ${lastLogin}`);
43
- }
44
-
45
- console.log();
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
  };
@@ -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) {
@@ -6,7 +6,7 @@ const chalk = require('chalk');
6
6
  module.exports = {
7
7
  name: 'complete',
8
8
  description: 'Mark a task as completed',
9
- aliases: ['done', 'finish'],
9
+ aliases: ['done', 'finish', 'complete'],
10
10
  requiresAuth: true,
11
11
 
12
12
  async execute(args, shell) {
@@ -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: 'myTask',
5
- description: 'My Tasks in the Room',
6
- aliases: ['mytasks'],
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 = shell.api.getUser();
11
- const token = shell.api.getToken();
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
- if (!user || !token) {
15
- console.log(chalk.red('โŒ Please login first: login'));
16
- return;
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
- console.log(` ${chalk.dim(`${index + 1}.`)} ${status} ${title}${due}`);
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
- console.log(` ${chalk.dim(task.description)}`);
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
- console.log(chalk.dim('\nโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”'));
64
- console.log(chalk.green(` โœ… ${result.tasks.length} task(s) fetched successfully.\n`));
65
-
66
- } catch (err) {
67
- if (err.name === 'ExitPromptError' || err.message?.includes('force closed')) {
68
- console.log(chalk.yellow('โš ๏ธ Cancelled'));
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
- console.log(chalk.red(`โŒ Fetching tasks failed: ${err.message}`));
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
  };
@@ -115,37 +115,73 @@ class ThinkNCollabAPI {
115
115
 
116
116
  // โ”€โ”€โ”€ Auth โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
117
117
 
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
- });
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
- // Normalise: store with BOTH _id and userId so getUser() always works
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
- return {
139
- token: result.shellToken,
140
- user: this.getUser() // return normalised shape
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
- async logout() {
145
- this._clearSession();
146
- return { success: true };
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 = '.tncproject';
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
@@ -40,7 +40,7 @@ function findConfigFile(startDir = process.cwd()) {
40
40
  * Read and lightly validate a .tncproject file.
41
41
  * Returns { filePath, roomId, fileBase64 } โ€” no decryption.
42
42
  *
43
- * @throws if file is missing, wrong format, or roomId looks wrong
43
+ * @throws if file is missing, wrong format, or roomId looks wrong http://localhost:3001
44
44
  */
45
45
  function loadProjectConfig(startDir = process.cwd()) {
46
46
  const filePath = findConfigFile(startDir);