thinkncollab-shell 0.0.8 → 0.0.9

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.
@@ -1,246 +1,246 @@
1
- /**
2
- * src/core/api-client.js
3
- */
4
-
5
- const https = require('https');
6
- const http = require('http');
7
- const path = require('path');
8
- const fs = require('fs-extra');
9
- const os = require('os');
10
-
11
- class ThinkNCollabAPI {
12
-
13
- constructor(options = {}) {
14
- this.apiUrl = (options.apiUrl || 'https://thinkncollab.com').replace(/\/$/, '');
15
- this.wsUrl = (options.wsUrl || 'https://thinkncollab.com').replace(/\/$/, '');
16
- this.session = null;
17
- this.sessionPath = path.join(os.homedir(), '.thinkncollab', 'session.json');
18
- this._loadSession();
19
- }
20
-
21
- // ─── Session ──────────────────────────────────────────────────────────────
22
-
23
- _loadSession() {
24
- try {
25
- if (fs.existsSync(this.sessionPath))
26
- this.session = fs.readJsonSync(this.sessionPath);
27
- } catch {}
28
- }
29
-
30
- _saveSession(session) {
31
- try {
32
- fs.ensureDirSync(path.dirname(this.sessionPath));
33
- fs.writeJsonSync(this.sessionPath, session);
34
- this.session = session;
35
- } catch {}
36
- }
37
-
38
- _clearSession() {
39
- try { fs.removeSync(this.sessionPath); } catch {}
40
- this.session = null;
41
- }
42
-
43
- isAuthenticated() { return !!(this.session?.token); }
44
- getToken() { return this.session?.token || null; }
45
-
46
- /**
47
- * Returns user in a CONSISTENT shape used everywhere:
48
- * { _id, userId, email, name, userType }
49
- *
50
- * Both _id and userId are the same string so callers can use either.
51
- */
52
- getUser() {
53
- const u = this.session?.user;
54
- if (!u) return null;
55
- const id = (u._id || u.userId || '').toString();
56
- return {
57
- _id: id,
58
- userId: id, // ws.setUser / _emitJoin use userId
59
- email: u.email || '',
60
- name: u.name || '',
61
- userType: u.userType || u.model || 'User',
62
- };
63
- }
64
-
65
- // ─── HTTP helper ──────────────────────────────────────────────────────────
66
-
67
- _request(method, endpoint, body = null, headers = {}) {
68
- return new Promise((resolve, reject) => {
69
- const url = new URL(this.apiUrl + endpoint);
70
- const isHttps = url.protocol === 'https:';
71
- const lib = isHttps ? https : http;
72
- const bodyStr = body ? JSON.stringify(body) : null;
73
-
74
- const options = {
75
- hostname: url.hostname,
76
- port: url.port || (isHttps ? 443 : 80),
77
- path: url.pathname + url.search,
78
- method,
79
- headers: {
80
- 'Content-Type': 'application/json',
81
- 'Accept': 'application/json',
82
- ...(this.session?.token ? { 'x-shell-token': this.session.token } : {}),
83
- ...headers,
84
- ...(bodyStr ? { 'Content-Length': Buffer.byteLength(bodyStr) } : {})
85
- }
86
- };
87
-
88
- const req = lib.request(options, (res) => {
89
- let data = '';
90
- res.on('data', chunk => data += chunk);
91
- res.on('end', () => {
92
- try {
93
- const parsed = JSON.parse(data);
94
- if (res.statusCode >= 400)
95
- reject(new Error(parsed.error || parsed.message || `HTTP ${res.statusCode}`));
96
- else
97
- resolve(parsed);
98
- } catch {
99
- reject(new Error(`Invalid response: ${data.slice(0, 100)}`));
100
- }
101
- });
102
- });
103
-
104
- req.on('error', (err) => {
105
- if (err.code === 'ECONNREFUSED')
106
- reject(new Error(`Cannot connect to server at ${this.apiUrl} — is it running?`));
107
- else
108
- reject(err);
109
- });
110
-
111
- if (bodyStr) req.write(bodyStr);
112
- req.end();
113
- });
114
- }
115
-
116
- // ─── Auth ─────────────────────────────────────────────────────────────────
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
- });
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
- }
143
-
144
- // ─── Session event logger ─────────────────────────────────────────────────
145
-
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
- }
168
-
169
- stopHeartbeat() {
170
- if (this._heartbeatTimer) {
171
- clearInterval(this._heartbeatTimer);
172
- this._heartbeatTimer = null;
173
- }
174
- }
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
- }
185
- async whoami() {
186
- if (!this.isAuthenticated()) throw new Error('Not logged in');
187
- return this.getUser();
188
- }
189
-
190
- // ─── Rooms ────────────────────────────────────────────────────────────────
191
-
192
- /**
193
- * joinRoom — validates membership server-side and returns room meta.
194
- * Called by commands/room/join.js BEFORE the WebSocket thinknsh:join emit.
195
- */
196
- async joinRoom(roomId, password) {
197
- if (!roomId) throw new Error('roomId is required');
198
-
199
- const result = await this._request('POST', `/thinknsh/rooms/${roomId}/join`,
200
- password ? { password } : null
201
- );
202
-
203
- // Normalise response — server may return different shapes
204
- return {
205
- room: result.room || result,
206
- participants: result.participants || result.members || [],
207
- recentMessages: result.recentMessages || result.history || [],
208
- };
209
- }
210
-
211
- /**
212
- * getRooms — list rooms the user belongs to
213
- */
214
- async getRooms() {
215
- return this._request('GET', '/thinknsh/rooms');
216
- }
217
-
218
- // ─── Tasks ────────────────────────────────────────────────────────────────
219
-
220
- async mytasks(roomId, limit) {
221
- if (!roomId) throw new Error('roomId is required');
222
- const user = this.getUser();
223
- let ep = `/thinknsh/tasks?roomId=${encodeURIComponent(roomId)}`;
224
- if (user?._id) ep += `&userId=${encodeURIComponent(user._id)}`;
225
- if (limit) ep += `&n=${limit}`;
226
- return this._request('GET', ep);
227
- }
228
-
229
- /**
230
- * Generic request wrapper for task command modules that call
231
- * shell.api.request(method, path, { params, body })
232
- */
233
- async request(method, endpoint, options = {}) {
234
- let ep = endpoint;
235
-
236
- // Append query params
237
- if (options.params && Object.keys(options.params).length) {
238
- const qs = new URLSearchParams(options.params).toString();
239
- ep += (ep.includes('?') ? '&' : '?') + qs;
240
- }
241
-
242
- return this._request(method.toUpperCase(), ep, options.body || null);
243
- }
244
- }
245
-
1
+ /**
2
+ * src/core/api-client.js
3
+ */
4
+
5
+ const https = require('https');
6
+ const http = require('http');
7
+ const path = require('path');
8
+ const fs = require('fs-extra');
9
+ const os = require('os');
10
+
11
+ class ThinkNCollabAPI {
12
+
13
+ constructor(options = {}) {
14
+ this.apiUrl = (options.apiUrl || 'https://thinkncollab.com').replace(/\/$/, '');
15
+ this.wsUrl = (options.wsUrl || 'https://thinkncollab.com').replace(/\/$/, '');
16
+ this.session = null;
17
+ this.sessionPath = path.join(os.homedir(), '.thinkncollab', 'session.json');
18
+ this._loadSession();
19
+ }
20
+
21
+ // ─── Session ──────────────────────────────────────────────────────────────
22
+
23
+ _loadSession() {
24
+ try {
25
+ if (fs.existsSync(this.sessionPath))
26
+ this.session = fs.readJsonSync(this.sessionPath);
27
+ } catch {}
28
+ }
29
+
30
+ _saveSession(session) {
31
+ try {
32
+ fs.ensureDirSync(path.dirname(this.sessionPath));
33
+ fs.writeJsonSync(this.sessionPath, session);
34
+ this.session = session;
35
+ } catch {}
36
+ }
37
+
38
+ _clearSession() {
39
+ try { fs.removeSync(this.sessionPath); } catch {}
40
+ this.session = null;
41
+ }
42
+
43
+ isAuthenticated() { return !!(this.session?.token); }
44
+ getToken() { return this.session?.token || null; }
45
+
46
+ /*
47
+ Returns user in a CONSISTENT shape used everywhere:
48
+ { _id, userId, email, name, userType }
49
+
50
+ Both _id and userId are the same string so callers can use either.
51
+ */
52
+ getUser() {
53
+ const u = this.session?.user;
54
+ if (!u) return null;
55
+ const id = (u._id || u.userId || '').toString();
56
+ return {
57
+ _id: id,
58
+ userId: id, // ws.setUser / _emitJoin use userId
59
+ email: u.email || '',
60
+ name: u.name || '',
61
+ userType: u.userType || u.model || 'User',
62
+ };
63
+ }
64
+
65
+ // ─── HTTP helper ──────────────────────────────────────────────────────────
66
+
67
+ _request(method, endpoint, body = null, headers = {}) {
68
+ return new Promise((resolve, reject) => {
69
+ const url = new URL(this.apiUrl + endpoint);
70
+ const isHttps = url.protocol === 'https:';
71
+ const lib = isHttps ? https : http;
72
+ const bodyStr = body ? JSON.stringify(body) : null;
73
+
74
+ const options = {
75
+ hostname: url.hostname,
76
+ port: url.port || (isHttps ? 443 : 80),
77
+ path: url.pathname + url.search,
78
+ method,
79
+ headers: {
80
+ 'Content-Type': 'application/json',
81
+ 'Accept': 'application/json',
82
+ ...(this.session?.token ? { 'x-shell-token': this.session.token } : {}),
83
+ ...headers,
84
+ ...(bodyStr ? { 'Content-Length': Buffer.byteLength(bodyStr) } : {})
85
+ }
86
+ };
87
+
88
+ const req = lib.request(options, (res) => {
89
+ let data = '';
90
+ res.on('data', chunk => data += chunk);
91
+ res.on('end', () => {
92
+ try {
93
+ const parsed = JSON.parse(data);
94
+ if (res.statusCode >= 400)
95
+ reject(new Error(parsed.error || parsed.message || `HTTP ${res.statusCode}`));
96
+ else
97
+ resolve(parsed);
98
+ } catch {
99
+ reject(new Error(`Invalid response: ${data.slice(0, 100)}`));
100
+ }
101
+ });
102
+ });
103
+
104
+ req.on('error', (err) => {
105
+ if (err.code === 'ECONNREFUSED')
106
+ reject(new Error(`Cannot connect to server at ${this.apiUrl} — is it running?`));
107
+ else
108
+ reject(err);
109
+ });
110
+
111
+ if (bodyStr) req.write(bodyStr);
112
+ req.end();
113
+ });
114
+ }
115
+
116
+ // ─── Auth ─────────────────────────────────────────────────────────────────
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
+ });
124
+
125
+ this._saveSession({
126
+ token: result.shellToken,
127
+ shellSessionId: result.shellSessionId,
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
+ }
143
+
144
+ // ─── Session event logger ─────────────────────────────────────────────────
145
+
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
+ }
168
+
169
+ stopHeartbeat() {
170
+ if (this._heartbeatTimer) {
171
+ clearInterval(this._heartbeatTimer);
172
+ this._heartbeatTimer = null;
173
+ }
174
+ }
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
+ }
185
+ async whoami() {
186
+ if (!this.isAuthenticated()) throw new Error('Not logged in');
187
+ return this.getUser();
188
+ }
189
+
190
+ // ─── Rooms ────────────────────────────────────────────────────────────────
191
+
192
+ /**
193
+ * joinRoom — validates membership server-side and returns room meta.
194
+ * Called by commands/room/join.js BEFORE the WebSocket thinknsh:join emit.
195
+ */
196
+ async joinRoom(roomId, password) {
197
+ if (!roomId) throw new Error('roomId is required');
198
+
199
+ const result = await this._request('POST', `/thinknsh/rooms/${roomId}/join`,
200
+ password ? { password } : null
201
+ );
202
+
203
+ // Normalise response — server may return different shapes
204
+ return {
205
+ room: result.room || result,
206
+ participants: result.participants || result.members || [],
207
+ recentMessages: result.recentMessages || result.history || [],
208
+ };
209
+ }
210
+
211
+ /**
212
+ * getRooms — list rooms the user belongs to
213
+ */
214
+ async getRooms() {
215
+ return this._request('GET', '/thinknsh/rooms');
216
+ }
217
+
218
+ // ─── Tasks ────────────────────────────────────────────────────────────────
219
+
220
+ async mytasks(roomId, limit) {
221
+ if (!roomId) throw new Error('roomId is required');
222
+ const user = this.getUser();
223
+ let ep = `/thinknsh/tasks?roomId=${encodeURIComponent(roomId)}`;
224
+ if (user?._id) ep += `&userId=${encodeURIComponent(user._id)}`;
225
+ if (limit) ep += `&n=${limit}`;
226
+ return this._request('GET', ep);
227
+ }
228
+
229
+ /**
230
+ * Generic request wrapper for task command modules that call
231
+ * shell.api.request(method, path, { params, body })
232
+ */
233
+ async request(method, endpoint, options = {}) {
234
+ let ep = endpoint;
235
+
236
+ // Append query params
237
+ if (options.params && Object.keys(options.params).length) {
238
+ const qs = new URLSearchParams(options.params).toString();
239
+ ep += (ep.includes('?') ? '&' : '?') + qs;
240
+ }
241
+
242
+ return this._request(method.toUpperCase(), ep, options.body || null);
243
+ }
244
+ }
245
+
246
246
  module.exports = ThinkNCollabAPI;