thinkncollab-shell 0.0.5 → 0.0.6

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,6 +1,5 @@
1
1
  /**
2
- * ThinkNCollab API Client
3
- * Handles HTTP requests to the backend
2
+ * src/core/api-client.js
4
3
  */
5
4
 
6
5
  const https = require('https');
@@ -12,22 +11,19 @@ const os = require('os');
12
11
  class ThinkNCollabAPI {
13
12
 
14
13
  constructor(options = {}) {
15
- this.apiUrl = (options.apiUrl || 'https://thinkncollab.com').replace(/\/$/, '');
16
- this.wsUrl = (options.wsUrl || 'https://thinkncollab.com').replace(/\/$/, '');
17
- this.session = null;
14
+ this.apiUrl = (options.apiUrl || 'https://thinkncollab.com').replace(/\/$/, '');
15
+ this.wsUrl = (options.wsUrl || 'https://thinkncollab.com').replace(/\/$/, '');
16
+ this.session = null;
18
17
  this.sessionPath = path.join(os.homedir(), '.thinkncollab', 'session.json');
19
-
20
- // Load existing session on startup
21
18
  this._loadSession();
22
19
  }
23
20
 
24
- // ─── Session management ───────────────────────────────────────────────────
21
+ // ─── Session ──────────────────────────────────────────────────────────────
25
22
 
26
23
  _loadSession() {
27
24
  try {
28
- if (fs.existsSync(this.sessionPath)) {
25
+ if (fs.existsSync(this.sessionPath))
29
26
  this.session = fs.readJsonSync(this.sessionPath);
30
- }
31
27
  } catch {}
32
28
  }
33
29
 
@@ -40,33 +36,40 @@ class ThinkNCollabAPI {
40
36
  }
41
37
 
42
38
  _clearSession() {
43
- try {
44
- fs.removeSync(this.sessionPath);
45
- this.session = null;
46
- } catch {}
39
+ try { fs.removeSync(this.sessionPath); } catch {}
40
+ this.session = null;
47
41
  }
48
42
 
49
- isAuthenticated() {
50
- return !!(this.session?.token);
51
- }
52
-
53
- getToken() {
54
- return this.session?.token || null;
55
- }
43
+ isAuthenticated() { return !!(this.session?.token); }
44
+ getToken() { return this.session?.token || null; }
56
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
+ */
57
52
  getUser() {
58
- return this.session?.user || null;
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
+ };
59
63
  }
60
64
 
61
65
  // ─── HTTP helper ──────────────────────────────────────────────────────────
62
66
 
63
67
  _request(method, endpoint, body = null, headers = {}) {
64
68
  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;
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;
70
73
 
71
74
  const options = {
72
75
  hostname: url.hostname,
@@ -74,8 +77,9 @@ class ThinkNCollabAPI {
74
77
  path: url.pathname + url.search,
75
78
  method,
76
79
  headers: {
77
- 'Content-Type': 'application/json',
78
- 'Accept': 'application/json',
80
+ 'Content-Type': 'application/json',
81
+ 'Accept': 'application/json',
82
+ ...(this.session?.token ? { 'x-shell-token': this.session.token } : {}),
79
83
  ...headers,
80
84
  ...(bodyStr ? { 'Content-Length': Buffer.byteLength(bodyStr) } : {})
81
85
  }
@@ -87,23 +91,21 @@ class ThinkNCollabAPI {
87
91
  res.on('end', () => {
88
92
  try {
89
93
  const parsed = JSON.parse(data);
90
- if (res.statusCode >= 400) {
94
+ if (res.statusCode >= 400)
91
95
  reject(new Error(parsed.error || parsed.message || `HTTP ${res.statusCode}`));
92
- } else {
96
+ else
93
97
  resolve(parsed);
94
- }
95
98
  } catch {
96
- reject(new Error(`Invalid response: ${data}`));
99
+ reject(new Error(`Invalid response: ${data.slice(0, 100)}`));
97
100
  }
98
101
  });
99
102
  });
100
103
 
101
104
  req.on('error', (err) => {
102
- if (err.code === 'ECONNREFUSED') {
105
+ if (err.code === 'ECONNREFUSED')
103
106
  reject(new Error(`Cannot connect to server at ${this.apiUrl} — is it running?`));
104
- } else {
107
+ else
105
108
  reject(err);
106
- }
107
109
  });
108
110
 
109
111
  if (bodyStr) req.write(bodyStr);
@@ -111,34 +113,31 @@ class ThinkNCollabAPI {
111
113
  });
112
114
  }
113
115
 
114
- // ─── Auth API ─────────────────────────────────────────────────────────────
116
+ // ─── Auth ─────────────────────────────────────────────────────────────────
115
117
 
116
118
  async login(email, password) {
117
- // Your backend reads from headers
118
119
  const result = await this._request('POST', '/thinknsh/login', null, {
119
- 'x-email': email,
120
- 'x-password': password,
121
- 'x-machine-id': require('os').hostname()
120
+ 'x-email': email,
121
+ 'x-password': password,
122
+ 'x-machine-id': os.hostname()
122
123
  });
123
124
 
124
- // Save session
125
+ // Normalise: store with BOTH _id and userId so getUser() always works
125
126
  this._saveSession({
126
127
  token: result.shellToken,
127
128
  user: {
128
- _id: result._id,
129
- email: result.email,
130
- name: result.name,
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',
131
134
  },
132
135
  timestamp: new Date().toISOString()
133
136
  });
134
137
 
135
138
  return {
136
139
  token: result.shellToken,
137
- user: {
138
- _id: result._id,
139
- email: result.email,
140
- name: result.name,
141
- }
140
+ user: this.getUser() // return normalised shape
142
141
  };
143
142
  }
144
143
 
@@ -151,6 +150,61 @@ class ThinkNCollabAPI {
151
150
  if (!this.isAuthenticated()) throw new Error('Not logged in');
152
151
  return this.getUser();
153
152
  }
153
+
154
+ // ─── Rooms ────────────────────────────────────────────────────────────────
155
+
156
+ /**
157
+ * joinRoom — validates membership server-side and returns room meta.
158
+ * Called by commands/room/join.js BEFORE the WebSocket thinknsh:join emit.
159
+ */
160
+ async joinRoom(roomId, password) {
161
+ if (!roomId) throw new Error('roomId is required');
162
+
163
+ const result = await this._request('POST', `/thinknsh/rooms/${roomId}/join`,
164
+ password ? { password } : null
165
+ );
166
+
167
+ // Normalise response — server may return different shapes
168
+ return {
169
+ room: result.room || result,
170
+ participants: result.participants || result.members || [],
171
+ recentMessages: result.recentMessages || result.history || [],
172
+ };
173
+ }
174
+
175
+ /**
176
+ * getRooms — list rooms the user belongs to
177
+ */
178
+ async getRooms() {
179
+ return this._request('GET', '/thinknsh/rooms');
180
+ }
181
+
182
+ // ─── Tasks ────────────────────────────────────────────────────────────────
183
+
184
+ async mytasks(roomId, limit) {
185
+ if (!roomId) throw new Error('roomId is required');
186
+ const user = this.getUser();
187
+ let ep = `/thinknsh/tasks?roomId=${encodeURIComponent(roomId)}`;
188
+ if (user?._id) ep += `&userId=${encodeURIComponent(user._id)}`;
189
+ if (limit) ep += `&n=${limit}`;
190
+ return this._request('GET', ep);
191
+ }
192
+
193
+ /**
194
+ * Generic request wrapper for task command modules that call
195
+ * shell.api.request(method, path, { params, body })
196
+ */
197
+ async request(method, endpoint, options = {}) {
198
+ let ep = endpoint;
199
+
200
+ // Append query params
201
+ if (options.params && Object.keys(options.params).length) {
202
+ const qs = new URLSearchParams(options.params).toString();
203
+ ep += (ep.includes('?') ? '&' : '?') + qs;
204
+ }
205
+
206
+ return this._request(method.toUpperCase(), ep, options.body || null);
207
+ }
154
208
  }
155
209
 
156
210
  module.exports = ThinkNCollabAPI;
@@ -93,7 +93,7 @@ function connect() {
93
93
  let buffer = '';
94
94
 
95
95
  client.connect(PORT, HOST, () => {
96
- console.log(chalk.green(' ✅ Connected to shell!\n'));
96
+ console.log(chalk.green(' Connected to shell!\n'));
97
97
  });
98
98
 
99
99
  client.on('data', (chunk) => {
@@ -0,0 +1,100 @@
1
+ /**
2
+ * src/core/project-config.js
3
+ *
4
+ * Shell-side reader for .tncproject binary files.
5
+ * NO crypto code — shell only reads the plaintext roomId prefix,
6
+ * then sends the raw file to the server for decryption.
7
+ *
8
+ * File layout (written by server):
9
+ * [4 bytes] "TNCP" magic
10
+ * [1 byte] version 0x01
11
+ * [24 bytes] roomId ASCII hex ← only readable part
12
+ * [12 bytes] AES-GCM IV ← opaque
13
+ * [16 bytes] AES-GCM auth tag ← opaque
14
+ * [N bytes] encrypted payload ← opaque
15
+ */
16
+
17
+ const fs = require('fs-extra');
18
+ const path = require('path');
19
+ const os = require('os');
20
+
21
+ const CONFIG_FILE = '.tncproject';
22
+ const MAGIC = Buffer.from('TNCP');
23
+ const ROOM_ID_SIZE = 24;
24
+ const MIN_SIZE = 4 + 1 + ROOM_ID_SIZE + 12 + 16 + 2; // 59 bytes minimum
25
+
26
+ function findConfigFile(startDir = process.cwd()) {
27
+ let dir = path.resolve(startDir);
28
+ const root = path.parse(dir).root;
29
+
30
+ while (true) {
31
+ const candidate = path.join(dir, CONFIG_FILE);
32
+ if (fs.existsSync(candidate)) return candidate;
33
+ if (dir === root || dir === os.homedir()) break;
34
+ dir = path.dirname(dir);
35
+ }
36
+ return null;
37
+ }
38
+
39
+ /**
40
+ * Read and lightly validate a .tncproject file.
41
+ * Returns { filePath, roomId, fileBase64 } — no decryption.
42
+ *
43
+ * @throws if file is missing, wrong format, or roomId looks wrong
44
+ */
45
+ function loadProjectConfig(startDir = process.cwd()) {
46
+ const filePath = findConfigFile(startDir);
47
+
48
+ if (!filePath) {
49
+ throw new Error(
50
+ 'No .tncproject file found in this directory or any parent.\n' +
51
+ ' Download it from: ThinkNCollab → Room → Project Config'
52
+ );
53
+ }
54
+
55
+ const buf = fs.readFileSync(filePath);
56
+
57
+ if (buf.length < MIN_SIZE) {
58
+ throw new Error(
59
+ 'Invalid .tncproject — file too small or corrupted.\n' +
60
+ ' Re-download it from the dashboard.'
61
+ );
62
+ }
63
+
64
+ if (!buf.slice(0, 4).equals(MAGIC)) {
65
+ throw new Error(
66
+ 'Not a ThinkNCollab project config file.\n' +
67
+ ' Re-download from the dashboard.'
68
+ );
69
+ }
70
+
71
+ const version = buf.readUInt8(4);
72
+ if (version !== 0x01) {
73
+ throw new Error(
74
+ `Unsupported .tncproject version (${version}).\n` +
75
+ ' Update thinkncollab-shell: npm update -g thinkncollab-shell'
76
+ );
77
+ }
78
+
79
+ // Read roomId from plaintext prefix — bytes 5..28
80
+ const roomId = buf.slice(5, 5 + ROOM_ID_SIZE).toString('ascii').trim();
81
+
82
+ if (!/^[a-f0-9]{24}$/i.test(roomId)) {
83
+ throw new Error(
84
+ 'Corrupted .tncproject — invalid roomId.\n' +
85
+ ' Re-download it from the dashboard.'
86
+ );
87
+ }
88
+
89
+ return {
90
+ filePath,
91
+ roomId,
92
+ fileBase64: buf.toString('base64'), // sent as-is to server
93
+ };
94
+ }
95
+
96
+ function hasProjectConfig(startDir = process.cwd()) {
97
+ return !!findConfigFile(startDir);
98
+ }
99
+
100
+ module.exports = { loadProjectConfig, hasProjectConfig, CONFIG_FILE };