@neuraiproject/neurai-depin-terminal 1.0.0 → 2.0.0
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 +37 -8
- package/dist/index.cjs +143 -184
- package/package.json +12 -12
- package/src/config/ConfigManager.js +43 -26
- package/src/constants.js +18 -5
- package/src/domain/messageTypes.js +26 -0
- package/src/errors.js +17 -0
- package/src/index.js +129 -30
- package/src/messaging/MessagePoller.js +132 -3
- package/src/messaging/MessageSender.js +99 -41
- package/src/messaging/MessageStore.js +26 -0
- package/src/messaging/RecipientDirectory.js +186 -0
- package/src/ui/CharsmUI.js +690 -0
- package/src/ui/RecipientSelector.js +114 -0
- package/src/ui/TabManager.js +162 -0
- package/src/ui/render.js +173 -0
- package/src/utils.js +304 -43
- package/src/wallet/WalletManager.js +5 -6
- package/src/ui/TerminalUI.js +0 -272
- package/src/ui/components/ErrorOverlay.js +0 -99
- package/src/ui/components/InputBox.js +0 -63
- package/src/ui/components/MessageBox.js +0 -51
- package/src/ui/components/StatusBar.js +0 -32
- package/src/ui/components/TopBar.js +0 -63
package/src/utils.js
CHANGED
|
@@ -75,10 +75,10 @@ export function truncate(str, length, suffix = '...') {
|
|
|
75
75
|
*/
|
|
76
76
|
export function formatTimestamp(timestamp, timezone = 'UTC') {
|
|
77
77
|
const date = timestamp instanceof Date ? timestamp : new Date(timestamp * 1000);
|
|
78
|
-
|
|
78
|
+
|
|
79
79
|
// Handle UTC explicitly
|
|
80
80
|
if (timezone === 'UTC') {
|
|
81
|
-
return date.toLocaleTimeString('en-US', {
|
|
81
|
+
return date.toLocaleTimeString('en-US', {
|
|
82
82
|
timeZone: 'UTC',
|
|
83
83
|
hour12: false,
|
|
84
84
|
hour: '2-digit',
|
|
@@ -93,7 +93,7 @@ export function formatTimestamp(timestamp, timezone = 'UTC') {
|
|
|
93
93
|
// Create a new date shifted by the offset hours
|
|
94
94
|
// We use UTC for display to avoid local system timezone interference
|
|
95
95
|
const shiftedDate = new Date(date.getTime() + (offset * 60 * 60 * 1000));
|
|
96
|
-
return shiftedDate.toLocaleTimeString('en-US', {
|
|
96
|
+
return shiftedDate.toLocaleTimeString('en-US', {
|
|
97
97
|
timeZone: 'UTC',
|
|
98
98
|
hour12: false,
|
|
99
99
|
hour: '2-digit',
|
|
@@ -103,7 +103,7 @@ export function formatTimestamp(timestamp, timezone = 'UTC') {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
// Fallback to UTC
|
|
106
|
-
return date.toLocaleTimeString('en-US', {
|
|
106
|
+
return date.toLocaleTimeString('en-US', {
|
|
107
107
|
timeZone: 'UTC',
|
|
108
108
|
hour12: false,
|
|
109
109
|
hour: '2-digit',
|
|
@@ -123,53 +123,260 @@ export function createMessageKey(hash, signature) {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
/**
|
|
126
|
-
*
|
|
126
|
+
* Drain stdin until silence is detected
|
|
127
|
+
* @param {Object} stdin - Stdin stream
|
|
128
|
+
* @param {number} silenceMs - Milliseconds of silence to wait for (default: 300)
|
|
129
|
+
* @param {number} maxWaitMs - Maximum time to wait in total (default: 2000)
|
|
130
|
+
* @returns {Promise<void>}
|
|
131
|
+
*/
|
|
132
|
+
export async function drainInput(stdin, silenceMs = 300, maxWaitMs = 2000) {
|
|
133
|
+
// If not TTY, we can't really drain in the same way, but we can try small read
|
|
134
|
+
if (!stdin.isTTY) {
|
|
135
|
+
if (stdin.readableLength > 0) stdin.read();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Enable raw mode to catch all chars
|
|
140
|
+
try {
|
|
141
|
+
stdin.setRawMode(true);
|
|
142
|
+
} catch (e) {
|
|
143
|
+
// Ignore
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
await new Promise(resolve => {
|
|
147
|
+
stdin.resume();
|
|
148
|
+
|
|
149
|
+
let silenceTimer;
|
|
150
|
+
let maxTimer;
|
|
151
|
+
|
|
152
|
+
const cleanup = () => {
|
|
153
|
+
if (silenceTimer) clearTimeout(silenceTimer);
|
|
154
|
+
if (maxTimer) clearTimeout(maxTimer);
|
|
155
|
+
stdin.removeAllListeners('data');
|
|
156
|
+
|
|
157
|
+
// Pause so we don't eat future input intended for others
|
|
158
|
+
stdin.pause();
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
stdin.setRawMode(false);
|
|
162
|
+
} catch (e) {
|
|
163
|
+
// Ignore
|
|
164
|
+
}
|
|
165
|
+
resolve();
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const resetSilenceTimer = () => {
|
|
169
|
+
if (silenceTimer) clearTimeout(silenceTimer);
|
|
170
|
+
silenceTimer = setTimeout(cleanup, silenceMs);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// If we hit the max wait time, just force proceed
|
|
174
|
+
maxTimer = setTimeout(cleanup, maxWaitMs);
|
|
175
|
+
|
|
176
|
+
// Listen for ANY data and drain it
|
|
177
|
+
stdin.on('data', () => {
|
|
178
|
+
resetSilenceTimer();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Start the initial timer
|
|
182
|
+
resetSilenceTimer();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Final sanity check drain
|
|
186
|
+
if (stdin.readableLength > 0) {
|
|
187
|
+
stdin.read();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Read password from stdin with character masking (pure implementation without readline)
|
|
127
193
|
* @param {string} prompt - Prompt to display
|
|
128
194
|
* @param {string} [maskChar='*'] - Character to display for each typed character
|
|
129
195
|
* @returns {Promise<string>} The entered password
|
|
130
196
|
*/
|
|
131
|
-
export function readPassword(prompt, maskChar = '*') {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
197
|
+
export async function readPassword(prompt, maskChar = '*') {
|
|
198
|
+
const stdin = process.stdin;
|
|
199
|
+
|
|
200
|
+
// Use robust drain before starting
|
|
201
|
+
await drainInput(stdin, 200, 1000);
|
|
202
|
+
|
|
203
|
+
return new Promise((resolve, reject) => {
|
|
204
|
+
let onDataHandler = null;
|
|
205
|
+
|
|
206
|
+
// Comprehensive cleanup function to ensure stdin is in pristine state
|
|
207
|
+
const cleanup = (removeListener = true) => {
|
|
208
|
+
if (removeListener && onDataHandler) {
|
|
209
|
+
stdin.removeListener('data', onDataHandler);
|
|
210
|
+
onDataHandler = null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Remove ALL listeners to avoid any residual state
|
|
214
|
+
stdin.removeAllListeners('data');
|
|
215
|
+
|
|
216
|
+
if (stdin.isTTY) {
|
|
217
|
+
try {
|
|
218
|
+
stdin.setRawMode(false);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
// Ignore raw mode reset failures
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// DON'T pause stdin - leave it ready for next use
|
|
225
|
+
// CharsmUI will manage its own resume/pause
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// Ensure stdin is in correct initial state
|
|
229
|
+
if (!stdin.isTTY) {
|
|
230
|
+
reject(new Error('stdin is not a TTY'));
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Start fresh
|
|
235
|
+
stdin.removeAllListeners('data');
|
|
236
|
+
stdin.removeAllListeners('keypress');
|
|
136
237
|
stdin.resume();
|
|
238
|
+
|
|
239
|
+
// Set raw mode for character-by-character input
|
|
240
|
+
try {
|
|
241
|
+
stdin.setRawMode(true);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
reject(new Error(`Failed to set raw mode: ${error.message}`));
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
137
247
|
stdin.setEncoding('utf8');
|
|
248
|
+
|
|
249
|
+
// Always mask password (show asterisks)
|
|
250
|
+
const shouldMask = true;
|
|
138
251
|
let password = '';
|
|
252
|
+
let done = false;
|
|
253
|
+
let escapeState = 'normal';
|
|
139
254
|
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
resolve(password);
|
|
150
|
-
break;
|
|
255
|
+
const finish = () => {
|
|
256
|
+
if (done) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
done = true;
|
|
260
|
+
cleanup(true);
|
|
261
|
+
process.stdout.write('\n');
|
|
262
|
+
resolve(password);
|
|
263
|
+
};
|
|
151
264
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
265
|
+
onDataHandler = (chunk) => {
|
|
266
|
+
for (const ch of chunk) {
|
|
267
|
+
if (done) {
|
|
155
268
|
break;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const codePoint = ch.charCodeAt(0);
|
|
156
272
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (
|
|
160
|
-
|
|
161
|
-
|
|
273
|
+
// State machine for filtering ANSI escape sequences
|
|
274
|
+
if (escapeState === 'esc') {
|
|
275
|
+
if (ch === '[') {
|
|
276
|
+
escapeState = 'csi';
|
|
277
|
+
} else if (ch === ']') {
|
|
278
|
+
escapeState = 'osc';
|
|
279
|
+
} else {
|
|
280
|
+
escapeState = 'normal';
|
|
162
281
|
}
|
|
163
|
-
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
164
284
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
285
|
+
if (escapeState === 'csi') {
|
|
286
|
+
if (ch >= '@' && ch <= '~') {
|
|
287
|
+
escapeState = 'normal';
|
|
288
|
+
}
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (escapeState === 'osc') {
|
|
293
|
+
if (ch === '\x07') {
|
|
294
|
+
escapeState = 'normal';
|
|
295
|
+
} else if (codePoint === 0x9c) {
|
|
296
|
+
escapeState = 'normal';
|
|
297
|
+
} else if (ch === '\x1b') {
|
|
298
|
+
escapeState = 'osc-esc';
|
|
299
|
+
}
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (escapeState === 'osc-esc') {
|
|
304
|
+
if (ch === '\\') {
|
|
305
|
+
escapeState = 'normal';
|
|
306
|
+
} else if (ch !== '\x1b') {
|
|
307
|
+
escapeState = 'osc';
|
|
308
|
+
}
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Process actual characters
|
|
313
|
+
switch (ch) {
|
|
314
|
+
case KEY_CODES.ENTER:
|
|
315
|
+
case KEY_CODES.CARRIAGE_RETURN:
|
|
316
|
+
case KEY_CODES.CTRL_D:
|
|
317
|
+
finish();
|
|
318
|
+
return;
|
|
319
|
+
|
|
320
|
+
case KEY_CODES.CTRL_C:
|
|
321
|
+
cleanup(true);
|
|
322
|
+
process.stdout.write('\n');
|
|
323
|
+
resetTerminal();
|
|
324
|
+
process.exit(0);
|
|
325
|
+
|
|
326
|
+
case KEY_CODES.BACKSPACE:
|
|
327
|
+
case KEY_CODES.BACKSPACE_ALT:
|
|
328
|
+
if (password.length > 0) {
|
|
329
|
+
password = password.slice(0, -1);
|
|
330
|
+
if (shouldMask) {
|
|
331
|
+
process.stdout.write(TERMINAL.BACKSPACE);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
break;
|
|
335
|
+
|
|
336
|
+
default:
|
|
337
|
+
// Start of escape sequence
|
|
338
|
+
if (ch === '\x1b') {
|
|
339
|
+
escapeState = 'esc';
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// C1 control characters (ignore)
|
|
344
|
+
if (codePoint === 0x9b) {
|
|
345
|
+
escapeState = 'csi';
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
if (codePoint === 0x9d) {
|
|
349
|
+
escapeState = 'osc';
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
if (codePoint >= 0x80 && codePoint <= 0x9f) {
|
|
353
|
+
// Ignore C1 control characters
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// C0 control characters (ignore except Enter, Backspace, Ctrl+C handled above)
|
|
358
|
+
if (codePoint < 0x20) {
|
|
359
|
+
// Ignore C0 control characters
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Valid printable character
|
|
364
|
+
password += ch;
|
|
365
|
+
if (shouldMask) {
|
|
366
|
+
process.stdout.write(maskChar);
|
|
367
|
+
}
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
169
370
|
}
|
|
170
371
|
};
|
|
171
372
|
|
|
172
|
-
|
|
373
|
+
// Attach listener FIRST so it can filter any incoming ANSI sequences
|
|
374
|
+
stdin.on('data', onDataHandler);
|
|
375
|
+
|
|
376
|
+
// Show prompt after a tiny delay
|
|
377
|
+
setTimeout(() => {
|
|
378
|
+
process.stdout.write(prompt);
|
|
379
|
+
}, 10);
|
|
173
380
|
});
|
|
174
381
|
}
|
|
175
382
|
|
|
@@ -186,12 +393,12 @@ export async function withSuppressedConsole(fn) {
|
|
|
186
393
|
const originalStdoutWrite = process.stdout.write;
|
|
187
394
|
const originalStderrWrite = process.stderr.write;
|
|
188
395
|
|
|
189
|
-
console.log = () => {};
|
|
190
|
-
console.warn = () => {};
|
|
191
|
-
console.error = () => {};
|
|
192
|
-
console.info = () => {};
|
|
193
|
-
process.stdout.write = () => {};
|
|
194
|
-
process.stderr.write = () => {};
|
|
396
|
+
console.log = () => { };
|
|
397
|
+
console.warn = () => { };
|
|
398
|
+
console.error = () => { };
|
|
399
|
+
console.info = () => { };
|
|
400
|
+
process.stdout.write = () => { };
|
|
401
|
+
process.stderr.write = () => { };
|
|
195
402
|
|
|
196
403
|
try {
|
|
197
404
|
return await fn();
|
|
@@ -213,6 +420,12 @@ export function resetTerminal() {
|
|
|
213
420
|
if (process.stdout.isTTY) {
|
|
214
421
|
try {
|
|
215
422
|
process.stdout.write(TERMINAL.EXIT_ALT_SCREEN);
|
|
423
|
+
// Disable mouse reporting (1000, 1002, 1003, 1006, 1015)
|
|
424
|
+
process.stdout.write('\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?1015l');
|
|
425
|
+
// Disable bracketed paste (2004)
|
|
426
|
+
process.stdout.write('\x1b[?2004l');
|
|
427
|
+
// Disable focus tracking (1004)
|
|
428
|
+
process.stdout.write('\x1b[?1004l');
|
|
216
429
|
process.stdout.write(TERMINAL.SHOW_CURSOR);
|
|
217
430
|
process.stdout.write(TERMINAL.RESET_ATTRIBUTES);
|
|
218
431
|
process.stdout.write(TERMINAL.NEW_LINE);
|
|
@@ -231,6 +444,54 @@ export function resetTerminal() {
|
|
|
231
444
|
}
|
|
232
445
|
}
|
|
233
446
|
|
|
447
|
+
/**
|
|
448
|
+
* Emergency terminal cleanup
|
|
449
|
+
* Called at startup to ensure terminal is in a clean state
|
|
450
|
+
*/
|
|
451
|
+
export function emergencyTerminalCleanup() {
|
|
452
|
+
if (process.stdin.isTTY) {
|
|
453
|
+
try {
|
|
454
|
+
// Remove any stale listeners
|
|
455
|
+
process.stdin.removeAllListeners('keypress');
|
|
456
|
+
process.stdin.removeAllListeners('data');
|
|
457
|
+
|
|
458
|
+
// Ensure raw mode is off
|
|
459
|
+
process.stdin.setRawMode(false);
|
|
460
|
+
|
|
461
|
+
// Resume stdin to allow reading buffered data
|
|
462
|
+
process.stdin.resume();
|
|
463
|
+
|
|
464
|
+
// Aggressively flush all buffered data (may need multiple reads)
|
|
465
|
+
let flushed = 0;
|
|
466
|
+
while (process.stdin.readableLength > 0 && flushed < 10) {
|
|
467
|
+
process.stdin.read();
|
|
468
|
+
flushed++;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Now pause for normal operation
|
|
472
|
+
process.stdin.pause();
|
|
473
|
+
} catch (err) {
|
|
474
|
+
// Ignore errors during cleanup
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (process.stdout.isTTY) {
|
|
479
|
+
try {
|
|
480
|
+
// Reset terminal attributes
|
|
481
|
+
process.stdout.write(TERMINAL.RESET_ATTRIBUTES);
|
|
482
|
+
// Disable mouse reporting (1000, 1002, 1003, 1006, 1015)
|
|
483
|
+
process.stdout.write('\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?1015l');
|
|
484
|
+
// Disable bracketed paste (2004)
|
|
485
|
+
process.stdout.write('\x1b[?2004l');
|
|
486
|
+
// Disable focus tracking (1004)
|
|
487
|
+
process.stdout.write('\x1b[?1004l');
|
|
488
|
+
process.stdout.write(TERMINAL.SHOW_CURSOR);
|
|
489
|
+
} catch (err) {
|
|
490
|
+
// Ignore errors
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
234
495
|
/**
|
|
235
496
|
* Parse RPC host from URL for display
|
|
236
497
|
* @param {string} url - Full RPC URL
|
|
@@ -277,8 +538,8 @@ export function validatePassword(password, minLength, maxLength) {
|
|
|
277
538
|
*/
|
|
278
539
|
export function isPubkeyRevealed(pubkeyResponse) {
|
|
279
540
|
return pubkeyResponse?.pubkey &&
|
|
280
|
-
|
|
281
|
-
|
|
541
|
+
pubkeyResponse?.revealed === 1 &&
|
|
542
|
+
pubkeyResponse.pubkey.trim().length > 0;
|
|
282
543
|
}
|
|
283
544
|
|
|
284
545
|
/**
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import NeuraiKey from '@neuraiproject/neurai-key';
|
|
8
|
-
import secp256k1 from 'secp256k1';
|
|
9
8
|
import {
|
|
10
9
|
ADDRESS,
|
|
11
10
|
ERROR_MESSAGES
|
|
@@ -32,7 +31,7 @@ export class WalletManager {
|
|
|
32
31
|
|
|
33
32
|
/**
|
|
34
33
|
* Derive address and public key from WIF private key
|
|
35
|
-
* Uses NeuraiKey to get address and
|
|
34
|
+
* Uses NeuraiKey to get address and derive compressed public key
|
|
36
35
|
* @returns {Promise<void>}
|
|
37
36
|
* @throws {WalletError} If key derivation fails
|
|
38
37
|
*/
|
|
@@ -46,10 +45,10 @@ export class WalletManager {
|
|
|
46
45
|
this.address = keyInfo.address;
|
|
47
46
|
this.privateKeyHex = keyInfo.privateKey;
|
|
48
47
|
|
|
49
|
-
// Derive compressed public key from
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
// Derive compressed public key from WIF
|
|
49
|
+
this.publicKey = await withSuppressedConsole(() => {
|
|
50
|
+
return NeuraiKey.getPubkeyByWIF(this.config.network, this.config.privateKey);
|
|
51
|
+
});
|
|
53
52
|
} catch (error) {
|
|
54
53
|
throw new WalletError(`${ERROR_MESSAGES.INVALID_WIF}: ${error.message}`);
|
|
55
54
|
}
|