agentvibes 5.1.3 → 5.2.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/.agentvibes/config.json +23 -13
- package/.claude/commands/agent-vibes/verbosity.md +98 -89
- package/.claude/config/audio-effects.cfg +6 -1
- package/.claude/hooks/bmad-speak.sh +2 -2
- package/.claude/hooks/piper-download-voices.sh +233 -225
- package/.claude/hooks/piper-installer.sh +1 -1
- package/.claude/hooks/piper-voice-manager.sh +125 -0
- package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +97 -90
- package/.claude/hooks/play-tts-enhanced.sh +1 -1
- package/.claude/hooks/play-tts-piper.sh +16 -5
- package/.claude/hooks/play-tts-ssh-remote.sh +168 -167
- package/.claude/hooks/play-tts.sh +31 -9
- package/.claude/hooks/session-start-tts.sh +4 -1
- package/.claude/hooks/stop-tts.sh +1 -1
- package/.claude/hooks/verbosity-manager.sh +185 -178
- package/.claude/hooks-windows/download-extra-voices.ps1 +243 -185
- package/.claude/hooks-windows/play-tts-piper.ps1 +7 -2
- package/.claude/hooks-windows/play-tts.ps1 +219 -65
- package/.claude/hooks-windows/session-start-tts.ps1 +2 -1
- package/.claude/hooks-windows/verbosity-manager.ps1 +126 -119
- package/README.md +24 -1
- package/RELEASE_NOTES.md +113 -0
- package/bin/agentvibes-voice-browser.js +1939 -1840
- package/mcp-server/server.py +75 -25
- package/package.json +1 -1
- package/src/console/tabs/receiver-tab.js +1527 -1483
- package/src/console/tabs/settings-tab.js +2 -2
- package/src/console/tabs/setup-tab.js +122 -20
- package/src/console/tabs/voices-tab.js +130 -13
- package/src/i18n/en.js +202 -202
- package/src/installer.js +29 -25
- package/src/services/llm-provider-service.js +114 -11
- package/src/services/verbosity-service.js +159 -157
- package/templates/agentvibes-receiver.sh +3 -2
|
@@ -1,1483 +1,1527 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AgentVibes TUI Console — Receiver Tab
|
|
3
|
-
* SSH Receiver — setup, enable/disable, and live message monitor.
|
|
4
|
-
*
|
|
5
|
-
* Implements the Tab Component Contract:
|
|
6
|
-
* createReceiverTab(screen, services) → { box, show, hide, onFocus, onBlur, getFooterText, getFooterColor }
|
|
7
|
-
*
|
|
8
|
-
* Uses scrollable text boxes (not lists) so users can highlight and copy
|
|
9
|
-
* with their mouse in the terminal.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync, chmodSync, unlinkSync, watchFile, unwatchFile } from 'node:fs';
|
|
13
|
-
import { execSync, spawnSync, spawn } from 'node:child_process';
|
|
14
|
-
import path from 'node:path';
|
|
15
|
-
import { homedir } from 'node:os';
|
|
16
|
-
import { fileURLToPath } from 'node:url';
|
|
17
|
-
import { t } from '../../i18n/strings.js';
|
|
18
|
-
|
|
19
|
-
const IS_TEST = process.env.AGENTVIBES_TEST_MODE === 'true';
|
|
20
|
-
|
|
21
|
-
let blessed;
|
|
22
|
-
if (!IS_TEST) {
|
|
23
|
-
const { default: b } = await import('blessed');
|
|
24
|
-
blessed = b;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
|
|
29
|
-
const COLORS = {
|
|
30
|
-
contentBg: '#0a0e1a',
|
|
31
|
-
sectionHdr: '#00897b',
|
|
32
|
-
labelFg: '#e3f2fd',
|
|
33
|
-
valueFg: '#ffff00',
|
|
34
|
-
activeFg: '#80cbc4',
|
|
35
|
-
borderFg: '#00897b',
|
|
36
|
-
footerBg: '#00897b',
|
|
37
|
-
noticeFg: '#90a4ae',
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
const FOOTER_TEXT = 'SSH Receiver [Q] Quit';
|
|
41
|
-
|
|
42
|
-
// ---------------------------------------------------------------------------
|
|
43
|
-
|
|
44
|
-
function createTestStub() {
|
|
45
|
-
return {
|
|
46
|
-
box: {},
|
|
47
|
-
show: () => {},
|
|
48
|
-
hide: () => {},
|
|
49
|
-
onFocus: () => {},
|
|
50
|
-
onBlur: () => {},
|
|
51
|
-
getFooterText: () => FOOTER_TEXT,
|
|
52
|
-
getFooterColor: () => COLORS.footerBg,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// ---------------------------------------------------------------------------
|
|
57
|
-
|
|
58
|
-
const _thisDir = IS_TEST ? '' : path.dirname(fileURLToPath(import.meta.url));
|
|
59
|
-
const TEMPLATE_PATH = IS_TEST ? '' : path.resolve(_thisDir, '..', '..', '..', 'templates', 'agentvibes-receiver.sh');
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Get the machine's Tailscale IP (if available) and SSH port.
|
|
63
|
-
*/
|
|
64
|
-
function _getNetworkInfo() {
|
|
65
|
-
let tailscaleIp = '';
|
|
66
|
-
let localIp = '';
|
|
67
|
-
let sshPort = '22';
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
state.
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
path.join(
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
'',
|
|
237
|
-
'
|
|
238
|
-
'
|
|
239
|
-
'
|
|
240
|
-
' ' + check(state.
|
|
241
|
-
' ' + check(state.
|
|
242
|
-
' ' + check(state.
|
|
243
|
-
' ' + check(state.
|
|
244
|
-
' ' + check(state.
|
|
245
|
-
'',
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
'
|
|
267
|
-
'',
|
|
268
|
-
|
|
269
|
-
'
|
|
270
|
-
'',
|
|
271
|
-
'
|
|
272
|
-
'
|
|
273
|
-
'',
|
|
274
|
-
'',
|
|
275
|
-
'
|
|
276
|
-
'
|
|
277
|
-
'
|
|
278
|
-
'',
|
|
279
|
-
'
|
|
280
|
-
'
|
|
281
|
-
'
|
|
282
|
-
'',
|
|
283
|
-
'
|
|
284
|
-
'',
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
'
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
'
|
|
307
|
-
'
|
|
308
|
-
'',
|
|
309
|
-
'
|
|
310
|
-
'
|
|
311
|
-
'',
|
|
312
|
-
'
|
|
313
|
-
'
|
|
314
|
-
'',
|
|
315
|
-
'',
|
|
316
|
-
'
|
|
317
|
-
'
|
|
318
|
-
'
|
|
319
|
-
'',
|
|
320
|
-
'
|
|
321
|
-
'
|
|
322
|
-
'
|
|
323
|
-
'',
|
|
324
|
-
'
|
|
325
|
-
'
|
|
326
|
-
'',
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
'
|
|
336
|
-
'
|
|
337
|
-
'',
|
|
338
|
-
|
|
339
|
-
'',
|
|
340
|
-
'
|
|
341
|
-
'
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
'',
|
|
351
|
-
'
|
|
352
|
-
'
|
|
353
|
-
|
|
354
|
-
'',
|
|
355
|
-
'
|
|
356
|
-
'
|
|
357
|
-
'
|
|
358
|
-
'
|
|
359
|
-
'',
|
|
360
|
-
'
|
|
361
|
-
'',
|
|
362
|
-
'
|
|
363
|
-
'(
|
|
364
|
-
'',
|
|
365
|
-
'',
|
|
366
|
-
'============================================================',
|
|
367
|
-
'
|
|
368
|
-
'============================================================',
|
|
369
|
-
'
|
|
370
|
-
'Receiver
|
|
371
|
-
'',
|
|
372
|
-
'
|
|
373
|
-
'',
|
|
374
|
-
'
|
|
375
|
-
'
|
|
376
|
-
'
|
|
377
|
-
'',
|
|
378
|
-
'
|
|
379
|
-
'
|
|
380
|
-
'',
|
|
381
|
-
'
|
|
382
|
-
'
|
|
383
|
-
'
|
|
384
|
-
'',
|
|
385
|
-
'
|
|
386
|
-
'',
|
|
387
|
-
'
|
|
388
|
-
'
|
|
389
|
-
'',
|
|
390
|
-
'
|
|
391
|
-
'
|
|
392
|
-
'
|
|
393
|
-
'
|
|
394
|
-
'
|
|
395
|
-
'',
|
|
396
|
-
'
|
|
397
|
-
'',
|
|
398
|
-
'
|
|
399
|
-
'
|
|
400
|
-
'',
|
|
401
|
-
'
|
|
402
|
-
'
|
|
403
|
-
'
|
|
404
|
-
'
|
|
405
|
-
'
|
|
406
|
-
'
|
|
407
|
-
'',
|
|
408
|
-
'
|
|
409
|
-
'',
|
|
410
|
-
'
|
|
411
|
-
'
|
|
412
|
-
'
|
|
413
|
-
'
|
|
414
|
-
'
|
|
415
|
-
'
|
|
416
|
-
'
|
|
417
|
-
'',
|
|
418
|
-
'',
|
|
419
|
-
'
|
|
420
|
-
'
|
|
421
|
-
'
|
|
422
|
-
'',
|
|
423
|
-
'
|
|
424
|
-
'',
|
|
425
|
-
'
|
|
426
|
-
'
|
|
427
|
-
'
|
|
428
|
-
'
|
|
429
|
-
'
|
|
430
|
-
'',
|
|
431
|
-
'
|
|
432
|
-
'
|
|
433
|
-
'',
|
|
434
|
-
'
|
|
435
|
-
'
|
|
436
|
-
'',
|
|
437
|
-
'',
|
|
438
|
-
'
|
|
439
|
-
'
|
|
440
|
-
'
|
|
441
|
-
'',
|
|
442
|
-
'
|
|
443
|
-
'',
|
|
444
|
-
'
|
|
445
|
-
'
|
|
446
|
-
'',
|
|
447
|
-
'
|
|
448
|
-
'
|
|
449
|
-
'',
|
|
450
|
-
'
|
|
451
|
-
'
|
|
452
|
-
'',
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
'
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
'
|
|
474
|
-
'
|
|
475
|
-
'
|
|
476
|
-
'',
|
|
477
|
-
|
|
478
|
-
'
|
|
479
|
-
'',
|
|
480
|
-
'',
|
|
481
|
-
'============================================================',
|
|
482
|
-
'
|
|
483
|
-
'
|
|
484
|
-
'',
|
|
485
|
-
'
|
|
486
|
-
'
|
|
487
|
-
'
|
|
488
|
-
'
|
|
489
|
-
'',
|
|
490
|
-
'
|
|
491
|
-
'',
|
|
492
|
-
'
|
|
493
|
-
'
|
|
494
|
-
'
|
|
495
|
-
'
|
|
496
|
-
'
|
|
497
|
-
'
|
|
498
|
-
'
|
|
499
|
-
'
|
|
500
|
-
'',
|
|
501
|
-
'',
|
|
502
|
-
'
|
|
503
|
-
'
|
|
504
|
-
'
|
|
505
|
-
'
|
|
506
|
-
'
|
|
507
|
-
'
|
|
508
|
-
'',
|
|
509
|
-
'
|
|
510
|
-
'',
|
|
511
|
-
'
|
|
512
|
-
'
|
|
513
|
-
'
|
|
514
|
-
'
|
|
515
|
-
'',
|
|
516
|
-
'
|
|
517
|
-
'',
|
|
518
|
-
'
|
|
519
|
-
'',
|
|
520
|
-
'
|
|
521
|
-
'',
|
|
522
|
-
'
|
|
523
|
-
'
|
|
524
|
-
'
|
|
525
|
-
'
|
|
526
|
-
'
|
|
527
|
-
'
|
|
528
|
-
'
|
|
529
|
-
'
|
|
530
|
-
'',
|
|
531
|
-
'
|
|
532
|
-
'
|
|
533
|
-
'
|
|
534
|
-
'
|
|
535
|
-
'',
|
|
536
|
-
'
|
|
537
|
-
'',
|
|
538
|
-
'
|
|
539
|
-
'',
|
|
540
|
-
'
|
|
541
|
-
'
|
|
542
|
-
'',
|
|
543
|
-
'
|
|
544
|
-
'',
|
|
545
|
-
'
|
|
546
|
-
'',
|
|
547
|
-
'
|
|
548
|
-
'
|
|
549
|
-
'
|
|
550
|
-
'
|
|
551
|
-
'
|
|
552
|
-
'
|
|
553
|
-
'
|
|
554
|
-
'
|
|
555
|
-
'
|
|
556
|
-
'
|
|
557
|
-
'
|
|
558
|
-
'
|
|
559
|
-
'
|
|
560
|
-
'
|
|
561
|
-
'
|
|
562
|
-
'
|
|
563
|
-
' sudo
|
|
564
|
-
' sudo
|
|
565
|
-
'',
|
|
566
|
-
'
|
|
567
|
-
' sudo
|
|
568
|
-
' sudo
|
|
569
|
-
'
|
|
570
|
-
' sudo
|
|
571
|
-
'
|
|
572
|
-
' #
|
|
573
|
-
'',
|
|
574
|
-
'
|
|
575
|
-
'
|
|
576
|
-
'
|
|
577
|
-
'
|
|
578
|
-
'',
|
|
579
|
-
'
|
|
580
|
-
'',
|
|
581
|
-
'
|
|
582
|
-
'',
|
|
583
|
-
'
|
|
584
|
-
'
|
|
585
|
-
'
|
|
586
|
-
'
|
|
587
|
-
'',
|
|
588
|
-
'
|
|
589
|
-
'
|
|
590
|
-
'
|
|
591
|
-
'',
|
|
592
|
-
'
|
|
593
|
-
'
|
|
594
|
-
'',
|
|
595
|
-
'
|
|
596
|
-
'',
|
|
597
|
-
'
|
|
598
|
-
'
|
|
599
|
-
'',
|
|
600
|
-
'
|
|
601
|
-
'',
|
|
602
|
-
'
|
|
603
|
-
'
|
|
604
|
-
'
|
|
605
|
-
'
|
|
606
|
-
'
|
|
607
|
-
'
|
|
608
|
-
'
|
|
609
|
-
'',
|
|
610
|
-
'
|
|
611
|
-
'
|
|
612
|
-
'
|
|
613
|
-
'
|
|
614
|
-
'
|
|
615
|
-
'
|
|
616
|
-
'',
|
|
617
|
-
'
|
|
618
|
-
'
|
|
619
|
-
'
|
|
620
|
-
'
|
|
621
|
-
'
|
|
622
|
-
'
|
|
623
|
-
'',
|
|
624
|
-
'
|
|
625
|
-
'
|
|
626
|
-
'
|
|
627
|
-
'
|
|
628
|
-
'',
|
|
629
|
-
'
|
|
630
|
-
'
|
|
631
|
-
'
|
|
632
|
-
'
|
|
633
|
-
'',
|
|
634
|
-
'
|
|
635
|
-
'
|
|
636
|
-
'
|
|
637
|
-
'
|
|
638
|
-
'
|
|
639
|
-
'
|
|
640
|
-
'',
|
|
641
|
-
'
|
|
642
|
-
'
|
|
643
|
-
'
|
|
644
|
-
'
|
|
645
|
-
'',
|
|
646
|
-
'
|
|
647
|
-
'
|
|
648
|
-
'
|
|
649
|
-
'
|
|
650
|
-
'',
|
|
651
|
-
'
|
|
652
|
-
'',
|
|
653
|
-
'
|
|
654
|
-
'
|
|
655
|
-
'',
|
|
656
|
-
'
|
|
657
|
-
'
|
|
658
|
-
'
|
|
659
|
-
'',
|
|
660
|
-
'',
|
|
661
|
-
'
|
|
662
|
-
'
|
|
663
|
-
'
|
|
664
|
-
'
|
|
665
|
-
'
|
|
666
|
-
'',
|
|
667
|
-
'
|
|
668
|
-
'',
|
|
669
|
-
'
|
|
670
|
-
'',
|
|
671
|
-
'
|
|
672
|
-
'
|
|
673
|
-
'
|
|
674
|
-
'
|
|
675
|
-
'
|
|
676
|
-
'',
|
|
677
|
-
'2
|
|
678
|
-
'',
|
|
679
|
-
'
|
|
680
|
-
'',
|
|
681
|
-
'
|
|
682
|
-
'',
|
|
683
|
-
'
|
|
684
|
-
'
|
|
685
|
-
'',
|
|
686
|
-
'
|
|
687
|
-
'
|
|
688
|
-
'
|
|
689
|
-
'',
|
|
690
|
-
'',
|
|
691
|
-
'
|
|
692
|
-
'
|
|
693
|
-
'
|
|
694
|
-
'',
|
|
695
|
-
'
|
|
696
|
-
'',
|
|
697
|
-
'
|
|
698
|
-
'
|
|
699
|
-
'
|
|
700
|
-
'
|
|
701
|
-
'
|
|
702
|
-
'
|
|
703
|
-
'
|
|
704
|
-
'
|
|
705
|
-
'',
|
|
706
|
-
'
|
|
707
|
-
'',
|
|
708
|
-
'
|
|
709
|
-
'
|
|
710
|
-
'
|
|
711
|
-
'',
|
|
712
|
-
'
|
|
713
|
-
'
|
|
714
|
-
'
|
|
715
|
-
'
|
|
716
|
-
'',
|
|
717
|
-
'
|
|
718
|
-
'
|
|
719
|
-
'
|
|
720
|
-
'',
|
|
721
|
-
'
|
|
722
|
-
'',
|
|
723
|
-
'
|
|
724
|
-
'
|
|
725
|
-
'
|
|
726
|
-
'
|
|
727
|
-
'',
|
|
728
|
-
'
|
|
729
|
-
'
|
|
730
|
-
'
|
|
731
|
-
'
|
|
732
|
-
'
|
|
733
|
-
'',
|
|
734
|
-
'',
|
|
735
|
-
'
|
|
736
|
-
'
|
|
737
|
-
'
|
|
738
|
-
'',
|
|
739
|
-
'
|
|
740
|
-
'
|
|
741
|
-
'
|
|
742
|
-
'
|
|
743
|
-
'',
|
|
744
|
-
'
|
|
745
|
-
'
|
|
746
|
-
'
|
|
747
|
-
'
|
|
748
|
-
'',
|
|
749
|
-
'
|
|
750
|
-
'
|
|
751
|
-
'
|
|
752
|
-
'
|
|
753
|
-
'
|
|
754
|
-
'
|
|
755
|
-
'',
|
|
756
|
-
'
|
|
757
|
-
' - Check:
|
|
758
|
-
'
|
|
759
|
-
'
|
|
760
|
-
'',
|
|
761
|
-
'
|
|
762
|
-
' -
|
|
763
|
-
'
|
|
764
|
-
'
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
const
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
const
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
tags: true,
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
parent:
|
|
854
|
-
top:
|
|
855
|
-
|
|
856
|
-
tags: true,
|
|
857
|
-
content: '',
|
|
858
|
-
style: {
|
|
859
|
-
});
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
style: {
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
let
|
|
993
|
-
const
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
music
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
const
|
|
1102
|
-
const
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
const
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
const
|
|
1121
|
-
const
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
//
|
|
1216
|
-
const
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
for (const msg of
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
box.key(['
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
let
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1
|
+
/**
|
|
2
|
+
* AgentVibes TUI Console — Receiver Tab
|
|
3
|
+
* SSH Receiver — setup, enable/disable, and live message monitor.
|
|
4
|
+
*
|
|
5
|
+
* Implements the Tab Component Contract:
|
|
6
|
+
* createReceiverTab(screen, services) → { box, show, hide, onFocus, onBlur, getFooterText, getFooterColor }
|
|
7
|
+
*
|
|
8
|
+
* Uses scrollable text boxes (not lists) so users can highlight and copy
|
|
9
|
+
* with their mouse in the terminal.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync, chmodSync, unlinkSync, watchFile, unwatchFile } from 'node:fs';
|
|
13
|
+
import { execSync, spawnSync, spawn } from 'node:child_process';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { homedir } from 'node:os';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { t } from '../../i18n/strings.js';
|
|
18
|
+
|
|
19
|
+
const IS_TEST = process.env.AGENTVIBES_TEST_MODE === 'true';
|
|
20
|
+
|
|
21
|
+
let blessed;
|
|
22
|
+
if (!IS_TEST) {
|
|
23
|
+
const { default: b } = await import('blessed');
|
|
24
|
+
blessed = b;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
const COLORS = {
|
|
30
|
+
contentBg: '#0a0e1a',
|
|
31
|
+
sectionHdr: '#00897b',
|
|
32
|
+
labelFg: '#e3f2fd',
|
|
33
|
+
valueFg: '#ffff00',
|
|
34
|
+
activeFg: '#80cbc4',
|
|
35
|
+
borderFg: '#00897b',
|
|
36
|
+
footerBg: '#00897b',
|
|
37
|
+
noticeFg: '#90a4ae',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const FOOTER_TEXT = 'SSH Receiver [Q] Quit';
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
function createTestStub() {
|
|
45
|
+
return {
|
|
46
|
+
box: {},
|
|
47
|
+
show: () => {},
|
|
48
|
+
hide: () => {},
|
|
49
|
+
onFocus: () => {},
|
|
50
|
+
onBlur: () => {},
|
|
51
|
+
getFooterText: () => FOOTER_TEXT,
|
|
52
|
+
getFooterColor: () => COLORS.footerBg,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
const _thisDir = IS_TEST ? '' : path.dirname(fileURLToPath(import.meta.url));
|
|
59
|
+
const TEMPLATE_PATH = IS_TEST ? '' : path.resolve(_thisDir, '..', '..', '..', 'templates', 'agentvibes-receiver.sh');
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get the machine's Tailscale IP (if available) and SSH port.
|
|
63
|
+
*/
|
|
64
|
+
function _getNetworkInfo() {
|
|
65
|
+
let tailscaleIp = '';
|
|
66
|
+
let localIp = '';
|
|
67
|
+
let sshPort = '22';
|
|
68
|
+
const isWin = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
|
|
69
|
+
try {
|
|
70
|
+
tailscaleIp = execSync(isWin ? 'tailscale ip -4' : 'tailscale ip -4 2>/dev/null',
|
|
71
|
+
{ timeout: 3000, stdio: 'pipe' }).toString().trim();
|
|
72
|
+
} catch { /* tailscale not installed */ }
|
|
73
|
+
try {
|
|
74
|
+
if (isWin) {
|
|
75
|
+
// Use PowerShell to get local IP on Windows
|
|
76
|
+
localIp = execSync('powershell -NoProfile -Command "(Get-NetIPAddress -AddressFamily IPv4 | Where-Object { $_.InterfaceAlias -notmatch \'Loopback\' } | Select-Object -First 1).IPAddress"',
|
|
77
|
+
{ timeout: 5000, stdio: 'pipe' }).toString().trim();
|
|
78
|
+
} else {
|
|
79
|
+
localIp = execSync("hostname -I 2>/dev/null | awk '{print $1}'", { timeout: 3000, stdio: 'pipe' }).toString().trim();
|
|
80
|
+
}
|
|
81
|
+
} catch { /* ignore */ }
|
|
82
|
+
try {
|
|
83
|
+
if (isWin) {
|
|
84
|
+
// Windows SSH port from sshd_config
|
|
85
|
+
const sshdConf = readFileSync('C:\\ProgramData\\ssh\\sshd_config', 'utf-8');
|
|
86
|
+
const m = sshdConf.match(/^Port\s+(\d+)/m);
|
|
87
|
+
if (m) sshPort = m[1];
|
|
88
|
+
} else {
|
|
89
|
+
const portLine = execSync("grep -E '^Port ' /etc/ssh/sshd_config 2>/dev/null || echo 'Port 22'", { timeout: 3000, stdio: 'pipe' }).toString().trim();
|
|
90
|
+
const m = portLine.match(/^Port\s+(\d+)/);
|
|
91
|
+
if (m) sshPort = m[1];
|
|
92
|
+
}
|
|
93
|
+
} catch { /* default 22 */ }
|
|
94
|
+
return { tailscaleIp, localIp, sshPort };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Detect current receiver setup state — returns an object with boolean checks.
|
|
99
|
+
* Used to determine whether instructions should show full setup or just verification.
|
|
100
|
+
*/
|
|
101
|
+
function _detectSetupState() {
|
|
102
|
+
const isWin = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
|
|
103
|
+
const state = {
|
|
104
|
+
receiverUserExists: false,
|
|
105
|
+
receiverScriptInstalled: false,
|
|
106
|
+
voiceModelsPresent: false,
|
|
107
|
+
pipewireTcpConfigured: false,
|
|
108
|
+
flatVolumesDisabled: false,
|
|
109
|
+
pulseCookieShared: false,
|
|
110
|
+
forceCommandConfigured: false,
|
|
111
|
+
tcpModuleLoaded: false,
|
|
112
|
+
isWindows: isWin,
|
|
113
|
+
sshdRunning: false,
|
|
114
|
+
ffmpegInstalled: false,
|
|
115
|
+
piperInstalled: false,
|
|
116
|
+
};
|
|
117
|
+
try {
|
|
118
|
+
if (isWin) {
|
|
119
|
+
// Windows detection
|
|
120
|
+
const home = homedir();
|
|
121
|
+
state.receiverScriptInstalled = existsSync(path.join(home, '.agentvibes', 'play-remote.ps1'));
|
|
122
|
+
state.receiverUserExists = true; // Windows uses the current user, no separate user needed
|
|
123
|
+
|
|
124
|
+
// Check voice models
|
|
125
|
+
const voicesDir = path.join(home, '.claude', 'piper-voices');
|
|
126
|
+
try {
|
|
127
|
+
const files = require('fs').readdirSync(voicesDir).filter(f => f.endsWith('.onnx'));
|
|
128
|
+
state.voiceModelsPresent = files.length > 0;
|
|
129
|
+
} catch { /* no voices */ }
|
|
130
|
+
|
|
131
|
+
// Check sshd running
|
|
132
|
+
try {
|
|
133
|
+
const svc = execSync('powershell -NoProfile -Command "(Get-Service sshd -EA SilentlyContinue).Status"',
|
|
134
|
+
{ timeout: 5000, stdio: 'pipe' }).toString().trim();
|
|
135
|
+
state.sshdRunning = svc === 'Running';
|
|
136
|
+
} catch { /* sshd not installed */ }
|
|
137
|
+
|
|
138
|
+
// Check ForceCommand in Windows sshd_config
|
|
139
|
+
try {
|
|
140
|
+
const sshdConf = readFileSync('C:\\ProgramData\\ssh\\sshd_config', 'utf-8');
|
|
141
|
+
state.forceCommandConfigured = sshdConf.includes('ForceCommand') && sshdConf.includes('play-remote.ps1');
|
|
142
|
+
} catch { /* no read access */ }
|
|
143
|
+
|
|
144
|
+
// Check ffmpeg
|
|
145
|
+
try {
|
|
146
|
+
execSync('where ffmpeg', { timeout: 3000, stdio: 'pipe' });
|
|
147
|
+
state.ffmpegInstalled = true;
|
|
148
|
+
} catch { /* not found */ }
|
|
149
|
+
|
|
150
|
+
// Check piper
|
|
151
|
+
try {
|
|
152
|
+
execSync('where piper', { timeout: 3000, stdio: 'pipe' });
|
|
153
|
+
state.piperInstalled = true;
|
|
154
|
+
} catch {
|
|
155
|
+
const piperPath = path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Piper', 'piper.exe');
|
|
156
|
+
state.piperInstalled = existsSync(piperPath);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Windows doesn't need PipeWire/PulseAudio — mark as N/A
|
|
160
|
+
state.pipewireTcpConfigured = true;
|
|
161
|
+
state.flatVolumesDisabled = true;
|
|
162
|
+
state.pulseCookieShared = true;
|
|
163
|
+
state.tcpModuleLoaded = true;
|
|
164
|
+
} else {
|
|
165
|
+
// Linux/macOS detection (original)
|
|
166
|
+
let receiverHome = '';
|
|
167
|
+
try {
|
|
168
|
+
execSync('id agentvibes-receiver', { timeout: 3000, stdio: 'pipe' });
|
|
169
|
+
state.receiverUserExists = true;
|
|
170
|
+
try {
|
|
171
|
+
receiverHome = execSync("getent passwd agentvibes-receiver 2>/dev/null | cut -d: -f6 || echo '/home/agentvibes-receiver'",
|
|
172
|
+
{ timeout: 3000, stdio: 'pipe' }).toString().trim();
|
|
173
|
+
} catch { receiverHome = '/home/agentvibes-receiver'; }
|
|
174
|
+
} catch { /* user does not exist */ }
|
|
175
|
+
|
|
176
|
+
if (receiverHome) {
|
|
177
|
+
state.receiverScriptInstalled = existsSync(path.join(receiverHome, '.agentvibes/play-remote.sh'));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (receiverHome) {
|
|
181
|
+
try {
|
|
182
|
+
const voices = execSync(`ls ${receiverHome}/.claude/piper-voices/*.onnx 2>/dev/null | wc -l`,
|
|
183
|
+
{ timeout: 3000, stdio: 'pipe' }).toString().trim();
|
|
184
|
+
state.voiceModelsPresent = parseInt(voices, 10) > 0;
|
|
185
|
+
} catch { /* no access or no voices */ }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const home = homedir();
|
|
189
|
+
state.pipewireTcpConfigured = existsSync(
|
|
190
|
+
path.join(home, '.config/pipewire/pipewire-pulse.conf.d/agentvibes-tcp.conf'));
|
|
191
|
+
state.flatVolumesDisabled = existsSync(
|
|
192
|
+
path.join(home, '.config/pipewire/pipewire-pulse.conf.d/no-flat-volumes.conf'));
|
|
193
|
+
|
|
194
|
+
if (receiverHome) {
|
|
195
|
+
state.pulseCookieShared = existsSync(path.join(receiverHome, '.config/pulse/cookie'));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const sshdConf = readFileSync('/etc/ssh/sshd_config', 'utf-8');
|
|
200
|
+
state.forceCommandConfigured = sshdConf.includes('Match User agentvibes-receiver');
|
|
201
|
+
} catch { /* no read access */ }
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const modules = execSync('pactl list modules short 2>/dev/null', { timeout: 3000, stdio: 'pipe' }).toString();
|
|
205
|
+
state.tcpModuleLoaded = modules.includes('module-native-protocol-tcp');
|
|
206
|
+
} catch { /* pactl not available */ }
|
|
207
|
+
}
|
|
208
|
+
} catch { /* detection failed, assume not set up */ }
|
|
209
|
+
return state;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Build detailed setup instructions (cross-platform).
|
|
214
|
+
* Organized: explanation → server instructions (for copying) → local setup.
|
|
215
|
+
* Designed to be self-contained so an AI agent can execute all steps.
|
|
216
|
+
* Detects existing setup and shows verification-only instructions when ready.
|
|
217
|
+
*/
|
|
218
|
+
function _buildDetailedInstructions(receiverAlias, receiverScript, networkInfo) {
|
|
219
|
+
// Show detected values as hints but always use placeholders in instructions
|
|
220
|
+
// so the AI agent asks the user to confirm/provide their actual values
|
|
221
|
+
const detectedIp = networkInfo.tailscaleIp || networkInfo.localIp || '';
|
|
222
|
+
const detectedPort = networkInfo.sshPort || '22';
|
|
223
|
+
const state = _detectSetupState();
|
|
224
|
+
const isWin = state.isWindows;
|
|
225
|
+
const allReady = isWin
|
|
226
|
+
? (state.receiverScriptInstalled && state.voiceModelsPresent &&
|
|
227
|
+
state.sshdRunning && state.forceCommandConfigured)
|
|
228
|
+
: (state.receiverUserExists && state.receiverScriptInstalled &&
|
|
229
|
+
state.voiceModelsPresent && state.pipewireTcpConfigured &&
|
|
230
|
+
state.flatVolumesDisabled && state.pulseCookieShared &&
|
|
231
|
+
state.forceCommandConfigured && state.tcpModuleLoaded);
|
|
232
|
+
|
|
233
|
+
// Build status header showing what's detected
|
|
234
|
+
const check = (ok) => ok ? '[OK]' : '[--]';
|
|
235
|
+
const statusLines = isWin ? [
|
|
236
|
+
'============================================================',
|
|
237
|
+
'SETUP STATUS — Windows (auto-detected)',
|
|
238
|
+
'============================================================',
|
|
239
|
+
'',
|
|
240
|
+
' ' + check(state.sshdRunning) + ' OpenSSH Server running',
|
|
241
|
+
' ' + check(state.forceCommandConfigured) + ' SSH ForceCommand configured',
|
|
242
|
+
' ' + check(state.receiverScriptInstalled) + ' Receiver script (play-remote.ps1)',
|
|
243
|
+
' ' + check(state.voiceModelsPresent) + ' Piper voice models installed',
|
|
244
|
+
' ' + check(state.piperInstalled) + ' Piper TTS installed',
|
|
245
|
+
' ' + check(state.ffmpegInstalled) + ' ffmpeg installed (background music)',
|
|
246
|
+
'',
|
|
247
|
+
] : [
|
|
248
|
+
'============================================================',
|
|
249
|
+
'SETUP STATUS (auto-detected)',
|
|
250
|
+
'============================================================',
|
|
251
|
+
'',
|
|
252
|
+
' ' + check(state.receiverUserExists) + ' Receiver user (agentvibes-receiver)',
|
|
253
|
+
' ' + check(state.receiverScriptInstalled) + ' Receiver script (play-remote.sh)',
|
|
254
|
+
' ' + check(state.voiceModelsPresent) + ' Voice models copied',
|
|
255
|
+
' ' + check(state.pipewireTcpConfigured) + ' PipeWire TCP audio (port 34567)',
|
|
256
|
+
' ' + check(state.flatVolumesDisabled) + ' Flat-volumes disabled',
|
|
257
|
+
' ' + check(state.pulseCookieShared) + ' PulseAudio cookie shared',
|
|
258
|
+
' ' + check(state.forceCommandConfigured) + ' SSH ForceCommand configured',
|
|
259
|
+
' ' + check(state.tcpModuleLoaded) + ' TCP audio module loaded',
|
|
260
|
+
'',
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
if (allReady) {
|
|
264
|
+
if (isWin) {
|
|
265
|
+
return [
|
|
266
|
+
'Press [A] to copy all text to your clipboard.',
|
|
267
|
+
'',
|
|
268
|
+
...statusLines,
|
|
269
|
+
'All checks passed! Windows receiver is ready.',
|
|
270
|
+
'',
|
|
271
|
+
'============================================================',
|
|
272
|
+
'SERVER SETUP (the remote machine running Claude)',
|
|
273
|
+
'============================================================',
|
|
274
|
+
'',
|
|
275
|
+
' 1. Add SSH alias (~/.ssh/config on the server):',
|
|
276
|
+
'',
|
|
277
|
+
' Host <RECEIVER_NAME>',
|
|
278
|
+
' HostName ' + (detectedIp || '<RECEIVER_IP>'),
|
|
279
|
+
' Port 45123',
|
|
280
|
+
' User ' + (process.env.USERNAME || '<WINDOWS_USER>'),
|
|
281
|
+
' IdentityFile ~/.ssh/id_ed25519',
|
|
282
|
+
'',
|
|
283
|
+
' 2. Tell AgentVibes where to send TTS:',
|
|
284
|
+
' echo "<RECEIVER_NAME>" > .claude/ssh-remote-host.txt',
|
|
285
|
+
'',
|
|
286
|
+
' 3. Switch to ssh-remote provider:',
|
|
287
|
+
' echo "ssh-remote" > .claude/tts-provider.txt',
|
|
288
|
+
'',
|
|
289
|
+
'',
|
|
290
|
+
'============================================================',
|
|
291
|
+
'VERIFICATION',
|
|
292
|
+
'============================================================',
|
|
293
|
+
'',
|
|
294
|
+
'From the server:',
|
|
295
|
+
' echo \'{"text":"Hello","voice":"en_US-lessac-medium"}\' \\',
|
|
296
|
+
' | base64 | xargs ssh <RECEIVER_NAME>',
|
|
297
|
+
'',
|
|
298
|
+
'Check log: type %USERPROFILE%\\.agentvibes\\receiver.log',
|
|
299
|
+
'',
|
|
300
|
+
].join('\n');
|
|
301
|
+
}
|
|
302
|
+
return [
|
|
303
|
+
'Press [A] to copy all text to your clipboard.',
|
|
304
|
+
'',
|
|
305
|
+
...statusLines,
|
|
306
|
+
'All checks passed! Setup is complete.',
|
|
307
|
+
'Below are verification tests and server-side instructions.',
|
|
308
|
+
'',
|
|
309
|
+
'',
|
|
310
|
+
'============================================================',
|
|
311
|
+
'SERVER SETUP (the remote machine running Claude)',
|
|
312
|
+
'============================================================',
|
|
313
|
+
'Sender hook: .claude/hooks/play-tts-ssh-remote.sh',
|
|
314
|
+
'',
|
|
315
|
+
'1. Add an SSH alias for the receiver (~/.ssh/config):',
|
|
316
|
+
'',
|
|
317
|
+
' Host <RECEIVER_NAME>',
|
|
318
|
+
' HostName <RECEIVER_IP>',
|
|
319
|
+
detectedIp ? ' # detected: ' + detectedIp : '',
|
|
320
|
+
' Port ' + detectedPort,
|
|
321
|
+
' User agentvibes-receiver',
|
|
322
|
+
' IdentityFile ~/.ssh/id_ed25519',
|
|
323
|
+
'',
|
|
324
|
+
'2. Tell AgentVibes where to send TTS:',
|
|
325
|
+
' echo "<RECEIVER_NAME>" > .claude/ssh-remote-host.txt',
|
|
326
|
+
'',
|
|
327
|
+
'3. Switch to ssh-remote provider:',
|
|
328
|
+
' echo "ssh-remote" > .claude/tts-provider.txt',
|
|
329
|
+
'',
|
|
330
|
+
'',
|
|
331
|
+
'============================================================',
|
|
332
|
+
'VERIFICATION',
|
|
333
|
+
'============================================================',
|
|
334
|
+
'',
|
|
335
|
+
'Test from server:',
|
|
336
|
+
' echo \'{"text":"Hello","voice":"en_US-lessac-medium"}\' \\',
|
|
337
|
+
' | base64 | xargs ssh <RECEIVER_NAME>',
|
|
338
|
+
'',
|
|
339
|
+
'Full pipeline:',
|
|
340
|
+
' bash .claude/hooks/play-tts.sh "Receiver test complete"',
|
|
341
|
+
'',
|
|
342
|
+
].join('\n');
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Full setup instructions (when not everything is detected)
|
|
346
|
+
|
|
347
|
+
// ---- WINDOWS INSTRUCTIONS ----
|
|
348
|
+
if (isWin) {
|
|
349
|
+
return [
|
|
350
|
+
'Press [A] to copy all text to your clipboard.',
|
|
351
|
+
'Give this to an AI agent on your server to set up the sender.',
|
|
352
|
+
'',
|
|
353
|
+
...statusLines,
|
|
354
|
+
'',
|
|
355
|
+
'============================================================',
|
|
356
|
+
'BEFORE YOU BEGIN — Ask the user for these values:',
|
|
357
|
+
'============================================================',
|
|
358
|
+
'',
|
|
359
|
+
' RECEIVER_IP: This Windows machine\'s Tailscale IP',
|
|
360
|
+
detectedIp ? ' (detected: ' + detectedIp + ' — ask user to confirm)' : ' (run: tailscale ip -4)',
|
|
361
|
+
' RECEIVER_PORT: 45123 (hardened non-standard port)',
|
|
362
|
+
' RECEIVER_NAME: SSH alias name (default: my-receiver)',
|
|
363
|
+
' WINDOWS_USER: Windows username (e.g. Paul)',
|
|
364
|
+
'',
|
|
365
|
+
'',
|
|
366
|
+
'============================================================',
|
|
367
|
+
'WHAT IS SSH RECEIVER?',
|
|
368
|
+
'============================================================',
|
|
369
|
+
'',
|
|
370
|
+
'AgentVibes SSH Receiver lets remote servers (cloud VPS, dev',
|
|
371
|
+
'servers) send TTS audio to this Windows machine. The server',
|
|
372
|
+
'sends text + voice config over SSH, and this machine generates',
|
|
373
|
+
'and plays audio locally through its speakers.',
|
|
374
|
+
'',
|
|
375
|
+
'Server AI --[SSH/Tailscale]--> Windows --[Piper+ffmpeg]--> Speakers',
|
|
376
|
+
'',
|
|
377
|
+
'Security: SSH is hardened with key-only auth, ForceCommand',
|
|
378
|
+
'(no shell access), non-standard port, Tailscale-only binding.',
|
|
379
|
+
'',
|
|
380
|
+
'',
|
|
381
|
+
'============================================================',
|
|
382
|
+
'PART 1: WINDOWS RECEIVER SETUP (this machine)',
|
|
383
|
+
'============================================================',
|
|
384
|
+
'Setup script: setup-ssh-receiver.ps1',
|
|
385
|
+
'Receiver script: templates/agentvibes-receiver.ps1',
|
|
386
|
+
'',
|
|
387
|
+
'Step 1: Install prerequisites (if not already done)',
|
|
388
|
+
'',
|
|
389
|
+
' a) Install Tailscale (for secure networking):',
|
|
390
|
+
' winget install --id Tailscale.Tailscale -e',
|
|
391
|
+
' Then sign in with your Tailscale account.',
|
|
392
|
+
'',
|
|
393
|
+
' b) Install OpenSSH Server (admin PowerShell):',
|
|
394
|
+
' Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0',
|
|
395
|
+
'',
|
|
396
|
+
' c) Install Piper TTS and ffmpeg (if not installed):',
|
|
397
|
+
' Run the AgentVibes installer: node bin/agent-vibes install',
|
|
398
|
+
' It will check and install ffmpeg automatically.',
|
|
399
|
+
'',
|
|
400
|
+
'Step 2: Run the automated setup script (admin PowerShell)',
|
|
401
|
+
'',
|
|
402
|
+
' cd C:\\path\\to\\AgentVibes',
|
|
403
|
+
' powershell -ExecutionPolicy Bypass -File setup-ssh-receiver.ps1',
|
|
404
|
+
'',
|
|
405
|
+
' This script handles everything:',
|
|
406
|
+
' - Deploys hardened sshd_config (port 45123, key-only, ForceCommand)',
|
|
407
|
+
' - Installs receiver script to ~/.agentvibes/play-remote.ps1',
|
|
408
|
+
' - Adds firewall rule (Tailscale IPs only)',
|
|
409
|
+
' - Restarts sshd',
|
|
410
|
+
'',
|
|
411
|
+
'Step 3: Add the sender\'s SSH public key',
|
|
412
|
+
'',
|
|
413
|
+
' Get the public key from your server:',
|
|
414
|
+
' ssh your-server "cat ~/.ssh/id_ed25519.pub"',
|
|
415
|
+
'',
|
|
416
|
+
' Then in admin PowerShell on this Windows machine:',
|
|
417
|
+
' Set-Content -Path "C:\\ProgramData\\ssh\\administrators_authorized_keys" `',
|
|
418
|
+
' -Value "paste-the-public-key-here"',
|
|
419
|
+
' cmd /c \'icacls C:\\ProgramData\\ssh\\administrators_authorized_keys `',
|
|
420
|
+
' /inheritance:r /grant "SYSTEM:F" /grant "BUILTIN\\Administrators:F"\'',
|
|
421
|
+
' Restart-Service sshd',
|
|
422
|
+
'',
|
|
423
|
+
'Step 4: Security hardening details',
|
|
424
|
+
'',
|
|
425
|
+
' The setup script configures:',
|
|
426
|
+
' Port: 45123 (non-standard)',
|
|
427
|
+
' ListenAddress: Tailscale IP only (not 0.0.0.0)',
|
|
428
|
+
' Auth: SSH key only (no passwords)',
|
|
429
|
+
' ForceCommand: Can ONLY run the receiver script (no shell)',
|
|
430
|
+
' Forwarding: All disabled (TCP, agent, X11, tunnel)',
|
|
431
|
+
' Firewall: Port 45123 from 100.0.0.0/8 (Tailscale) only',
|
|
432
|
+
'',
|
|
433
|
+
'',
|
|
434
|
+
'============================================================',
|
|
435
|
+
'PART 2: SERVER SETUP (the remote machine running Claude)',
|
|
436
|
+
'============================================================',
|
|
437
|
+
'',
|
|
438
|
+
' 1. Add SSH alias (~/.ssh/config on the server):',
|
|
439
|
+
'',
|
|
440
|
+
' Host <RECEIVER_NAME>',
|
|
441
|
+
' HostName <RECEIVER_IP>',
|
|
442
|
+
' Port 45123',
|
|
443
|
+
' User <WINDOWS_USER>',
|
|
444
|
+
' IdentityFile ~/.ssh/id_ed25519',
|
|
445
|
+
'',
|
|
446
|
+
' 2. Tell AgentVibes where to send TTS:',
|
|
447
|
+
' echo "<RECEIVER_NAME>" > .claude/ssh-remote-host.txt',
|
|
448
|
+
'',
|
|
449
|
+
' 3. Switch to ssh-remote provider:',
|
|
450
|
+
' echo "ssh-remote" > .claude/tts-provider.txt',
|
|
451
|
+
'',
|
|
452
|
+
'',
|
|
453
|
+
'============================================================',
|
|
454
|
+
'PART 3: VERIFICATION',
|
|
455
|
+
'============================================================',
|
|
456
|
+
'',
|
|
457
|
+
'Test from the server:',
|
|
458
|
+
'',
|
|
459
|
+
' echo \'{"text":"Hello from server","voice":"en_US-lessac-medium"}\' \\',
|
|
460
|
+
' | base64 | xargs ssh <RECEIVER_NAME>',
|
|
461
|
+
'',
|
|
462
|
+
' Expected: Audio plays on Windows speakers.',
|
|
463
|
+
' Check log: type %USERPROFILE%\\.agentvibes\\receiver.log',
|
|
464
|
+
'',
|
|
465
|
+
'Test full AgentVibes pipeline:',
|
|
466
|
+
' bash .claude/hooks/play-tts.sh "Receiver test complete"',
|
|
467
|
+
'',
|
|
468
|
+
].join('\n');
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ---- LINUX/macOS INSTRUCTIONS (original) ----
|
|
472
|
+
return [
|
|
473
|
+
'Press [A] to copy all text to your clipboard.',
|
|
474
|
+
'Give this to an AI agent on your server AND local machine',
|
|
475
|
+
'to set up and verify the SSH receiver end-to-end.',
|
|
476
|
+
'',
|
|
477
|
+
...statusLines,
|
|
478
|
+
'',
|
|
479
|
+
'============================================================',
|
|
480
|
+
'BEFORE YOU BEGIN — Ask the user for these values:',
|
|
481
|
+
'============================================================',
|
|
482
|
+
'',
|
|
483
|
+
' RECEIVER_IP: The receiver machine IP or Tailscale hostname',
|
|
484
|
+
detectedIp ? ' (detected: ' + detectedIp + ' — ask user to confirm)' : ' (could not auto-detect — user must provide)',
|
|
485
|
+
' RECEIVER_PORT: The SSH port on the receiver machine',
|
|
486
|
+
' (detected: ' + detectedPort + ' — ask user to confirm)',
|
|
487
|
+
' RECEIVER_NAME: SSH alias name (default: my-receiver)',
|
|
488
|
+
' TTS_PROVIDER: Which TTS engine on the receiver?',
|
|
489
|
+
' Options: piper (default, Linux), soprano (neural TTS, any platform),',
|
|
490
|
+
' macos (macOS say), windows-sapi (Windows SAPI)',
|
|
491
|
+
'',
|
|
492
|
+
'Replace <RECEIVER_IP>, <RECEIVER_PORT>, <RECEIVER_NAME>,',
|
|
493
|
+
'and <TTS_PROVIDER> in commands below with user-provided values.',
|
|
494
|
+
'',
|
|
495
|
+
'',
|
|
496
|
+
'============================================================',
|
|
497
|
+
'WHAT IS SSH RECEIVER?',
|
|
498
|
+
'============================================================',
|
|
499
|
+
'',
|
|
500
|
+
'AgentVibes SSH Receiver lets remote servers (cloud VPS, dev',
|
|
501
|
+
'servers) send TTS audio to your local machine. The server AI',
|
|
502
|
+
'sends text + voice config over SSH, and this machine generates',
|
|
503
|
+
'and plays audio locally through its speakers.',
|
|
504
|
+
'',
|
|
505
|
+
'Server AI --[SSH payload]--> This Machine --[piper+sox+ffmpeg]--> Speakers',
|
|
506
|
+
'',
|
|
507
|
+
'Prerequisites:',
|
|
508
|
+
' - Both machines can reach each other via SSH (Tailscale VPN',
|
|
509
|
+
' recommended for easy setup: https://tailscale.com/download)',
|
|
510
|
+
' - A TTS engine on the receiver (this machine):',
|
|
511
|
+
' Linux: piper-tts (default, offline, fast)',
|
|
512
|
+
' Windows: soprano or windows-sapi (uses system voices)',
|
|
513
|
+
' macOS: macos (uses built-in say command)',
|
|
514
|
+
' - sox and ffmpeg recommended for audio effects and music',
|
|
515
|
+
'',
|
|
516
|
+
'',
|
|
517
|
+
'============================================================',
|
|
518
|
+
'PART 1: LOCAL SETUP (this machine — the receiver)',
|
|
519
|
+
'============================================================',
|
|
520
|
+
'Setup script: scripts/setup-receiver-user.sh',
|
|
521
|
+
'Receiver template: templates/agentvibes-receiver.sh',
|
|
522
|
+
'Sender hook: .claude/hooks/play-tts-ssh-remote.sh',
|
|
523
|
+
'',
|
|
524
|
+
'Run these steps on the machine that has speakers attached.',
|
|
525
|
+
'',
|
|
526
|
+
'IMPORTANT: Steps that require sudo cannot be run by an AI',
|
|
527
|
+
'agent. The USER must run sudo commands in a separate terminal.',
|
|
528
|
+
'The AI agent should tell the user what to run, then verify',
|
|
529
|
+
'the results using the detection checks shown above.',
|
|
530
|
+
'',
|
|
531
|
+
'--- Option A: Automated Setup (recommended) ---',
|
|
532
|
+
'',
|
|
533
|
+
'Ask the USER to run this in a separate terminal:',
|
|
534
|
+
'',
|
|
535
|
+
' sudo bash /path/to/AgentVibes/scripts/setup-receiver-user.sh',
|
|
536
|
+
'',
|
|
537
|
+
'This single script handles everything:',
|
|
538
|
+
' - Creates agentvibes-receiver user (groups: audio + your group)',
|
|
539
|
+
' - Copies piper voice models and music tracks',
|
|
540
|
+
' - Installs the receiver script (play-remote.sh)',
|
|
541
|
+
' - Configures PipeWire TCP audio on localhost:34567',
|
|
542
|
+
' - Disables flat-volumes (prevents volume hijacking)',
|
|
543
|
+
' - Shares PulseAudio cookie for cross-user auth',
|
|
544
|
+
' - Tests audio playback',
|
|
545
|
+
'',
|
|
546
|
+
'After the user confirms it ran successfully, verify with:',
|
|
547
|
+
' id agentvibes-receiver # user exists?',
|
|
548
|
+
' ls /home/agentvibes-receiver/.claude/piper-voices/*.onnx # voices?',
|
|
549
|
+
' pactl list modules short | grep tcp # TCP module?',
|
|
550
|
+
'',
|
|
551
|
+
'Then skip to Step 3 (ForceCommand) below.',
|
|
552
|
+
'',
|
|
553
|
+
'--- Option B: Manual Setup (step by step) ---',
|
|
554
|
+
'',
|
|
555
|
+
'Step 1: Enable receiver script',
|
|
556
|
+
' Press [E] in this tab (installs play-remote.sh to ~/.agentvibes/)',
|
|
557
|
+
'',
|
|
558
|
+
'Step 2: Create the receiver user',
|
|
559
|
+
'',
|
|
560
|
+
' Ask the USER to run these sudo commands in a terminal:',
|
|
561
|
+
'',
|
|
562
|
+
' Linux/WSL:',
|
|
563
|
+
' sudo useradd -m -s /bin/bash agentvibes-receiver',
|
|
564
|
+
' sudo usermod -aG audio,$(id -gn) agentvibes-receiver',
|
|
565
|
+
' # Create directories for voices and music:',
|
|
566
|
+
' sudo mkdir -p /home/agentvibes-receiver/.claude/piper-voices',
|
|
567
|
+
' sudo mkdir -p /home/agentvibes-receiver/.claude/audio/tracks',
|
|
568
|
+
' sudo mkdir -p /home/agentvibes-receiver/.agentvibes',
|
|
569
|
+
' # Copy voice models (required for TTS):',
|
|
570
|
+
' sudo cp ~/.claude/piper-voices/*.onnx /home/agentvibes-receiver/.claude/piper-voices/',
|
|
571
|
+
' sudo cp ~/.claude/piper-voices/*.onnx.json /home/agentvibes-receiver/.claude/piper-voices/',
|
|
572
|
+
' # Copy music tracks (optional, for background music):',
|
|
573
|
+
' sudo cp ~/.claude/audio/tracks/*.mp3 /home/agentvibes-receiver/.claude/audio/tracks/ 2>/dev/null',
|
|
574
|
+
' # Install receiver script:',
|
|
575
|
+
' sudo cp ~/.agentvibes/play-remote.sh /home/agentvibes-receiver/.agentvibes/play-remote.sh',
|
|
576
|
+
' sudo chmod +x /home/agentvibes-receiver/.agentvibes/play-remote.sh',
|
|
577
|
+
' # Fix ownership:',
|
|
578
|
+
' sudo chown -R agentvibes-receiver:agentvibes-receiver /home/agentvibes-receiver/.claude',
|
|
579
|
+
' sudo chown -R agentvibes-receiver:agentvibes-receiver /home/agentvibes-receiver/.agentvibes',
|
|
580
|
+
'',
|
|
581
|
+
' macOS:',
|
|
582
|
+
' sudo dscl . -create /Users/agentvibes-receiver',
|
|
583
|
+
' sudo dscl . -create /Users/agentvibes-receiver UserShell /bin/bash',
|
|
584
|
+
' sudo dscl . -create /Users/agentvibes-receiver NFSHomeDirectory /Users/agentvibes-receiver',
|
|
585
|
+
' sudo mkdir -p /Users/agentvibes-receiver',
|
|
586
|
+
' # Enable Remote Login in System Settings > General > Sharing',
|
|
587
|
+
' # Then copy voices/script as above (adjust paths to /Users/)',
|
|
588
|
+
'',
|
|
589
|
+
' AI agent: After user confirms, verify:',
|
|
590
|
+
' id agentvibes-receiver # user exists?',
|
|
591
|
+
' ls /home/agentvibes-receiver/.agentvibes/play-remote.sh # script?',
|
|
592
|
+
' ls /home/agentvibes-receiver/.claude/piper-voices/*.onnx # voices?',
|
|
593
|
+
'',
|
|
594
|
+
'Step 3: ForceCommand (recommended — limits SSH to audio only)',
|
|
595
|
+
'',
|
|
596
|
+
' Ask the USER to add this to /etc/ssh/sshd_config:',
|
|
597
|
+
'',
|
|
598
|
+
' Match User agentvibes-receiver',
|
|
599
|
+
' ForceCommand ' + receiverScript,
|
|
600
|
+
' PasswordAuthentication no',
|
|
601
|
+
' PermitTTY no',
|
|
602
|
+
'',
|
|
603
|
+
' Then ask user to reload sshd:',
|
|
604
|
+
' Linux/WSL: sudo systemctl reload sshd',
|
|
605
|
+
' macOS: sudo launchctl kickstart -k system/com.openssh.sshd',
|
|
606
|
+
'',
|
|
607
|
+
' AI agent: Verify (no sudo needed):',
|
|
608
|
+
' grep "Match User agentvibes-receiver" /etc/ssh/sshd_config',
|
|
609
|
+
'',
|
|
610
|
+
'Step 4: Audio access (required for dedicated user)',
|
|
611
|
+
'',
|
|
612
|
+
' The receiver user runs as a different UID and cannot access',
|
|
613
|
+
' your audio via Unix sockets. TCP localhost solves this.',
|
|
614
|
+
'',
|
|
615
|
+
' Linux (PipeWire — most modern distros):',
|
|
616
|
+
'',
|
|
617
|
+
' a) Enable TCP audio listener (AI agent CAN do this — no sudo):',
|
|
618
|
+
' mkdir -p ~/.config/pipewire/pipewire-pulse.conf.d',
|
|
619
|
+
' cat > ~/.config/pipewire/pipewire-pulse.conf.d/agentvibes-tcp.conf << \'EOF\'',
|
|
620
|
+
' pulse.cmd = [',
|
|
621
|
+
' { cmd = "load-module" args = "module-native-protocol-tcp auth-cookie-enabled=1 auth-anonymous=0 listen=127.0.0.1 port=34567" }',
|
|
622
|
+
' ]',
|
|
623
|
+
' EOF',
|
|
624
|
+
'',
|
|
625
|
+
' b) Disable flat-volumes (AI agent CAN do this — no sudo):',
|
|
626
|
+
' cat > ~/.config/pipewire/pipewire-pulse.conf.d/no-flat-volumes.conf << \'EOF\'',
|
|
627
|
+
' pulse.properties = {',
|
|
628
|
+
' pulse.flat.volumes = false',
|
|
629
|
+
' }',
|
|
630
|
+
' EOF',
|
|
631
|
+
'',
|
|
632
|
+
' c) Share PulseAudio cookie (REQUIRES sudo — ask USER):',
|
|
633
|
+
' sudo mkdir -p /home/agentvibes-receiver/.config/pulse',
|
|
634
|
+
' sudo cp ~/.config/pulse/cookie /home/agentvibes-receiver/.config/pulse/',
|
|
635
|
+
' sudo chown -R agentvibes-receiver:agentvibes-receiver \\',
|
|
636
|
+
' /home/agentvibes-receiver/.config/pulse',
|
|
637
|
+
' sudo chmod 600 /home/agentvibes-receiver/.config/pulse/cookie',
|
|
638
|
+
'',
|
|
639
|
+
' d) Load TCP module now (AI agent CAN do this — no sudo):',
|
|
640
|
+
' pactl load-module module-native-protocol-tcp \\',
|
|
641
|
+
' auth-cookie-enabled=1 auth-anonymous=0 \\',
|
|
642
|
+
' listen=127.0.0.1 port=34567',
|
|
643
|
+
'',
|
|
644
|
+
' AI agent: Verify audio setup:',
|
|
645
|
+
' pactl list modules short | grep tcp # TCP loaded?',
|
|
646
|
+
' ls /home/agentvibes-receiver/.config/pulse/cookie # cookie?',
|
|
647
|
+
' PULSE_SERVER=tcp:127.0.0.1:34567 pactl info # TCP works?',
|
|
648
|
+
'',
|
|
649
|
+
' Linux (PulseAudio — older distros):',
|
|
650
|
+
' # Add to /etc/pulse/default.pa or ~/.config/pulse/default.pa:',
|
|
651
|
+
' load-module module-native-protocol-tcp auth-cookie-enabled=1 \\',
|
|
652
|
+
' auth-anonymous=0 listen=127.0.0.1 port=34567',
|
|
653
|
+
' # Then share the cookie as above (step c — requires sudo)',
|
|
654
|
+
' # Restart: pulseaudio --kill && pulseaudio --start',
|
|
655
|
+
'',
|
|
656
|
+
' macOS:',
|
|
657
|
+
' # macOS uses coreaudiod which is system-wide.',
|
|
658
|
+
' # The receiver user should have audio access if in the',
|
|
659
|
+
' # "audio" group. No TCP workaround needed.',
|
|
660
|
+
'',
|
|
661
|
+
' WSL2:',
|
|
662
|
+
' # Audio routes through WSLg PulseServer at /mnt/wslg/PulseServer.',
|
|
663
|
+
' # Set in receiver script: export PULSE_SERVER=unix:/mnt/wslg/PulseServer',
|
|
664
|
+
' # Cross-user access may require the TCP approach above.',
|
|
665
|
+
'',
|
|
666
|
+
'Step 5: Add server SSH key',
|
|
667
|
+
'',
|
|
668
|
+
' On the server, generate a key if needed:',
|
|
669
|
+
' ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N ""',
|
|
670
|
+
'',
|
|
671
|
+
' Copy the public key to the receiver:',
|
|
672
|
+
' ssh-copy-id -i ~/.ssh/id_ed25519.pub \\',
|
|
673
|
+
' agentvibes-receiver@<RECEIVER_IP>',
|
|
674
|
+
'',
|
|
675
|
+
'',
|
|
676
|
+
'============================================================',
|
|
677
|
+
'PART 2: SERVER SETUP (the remote machine running Claude)',
|
|
678
|
+
'============================================================',
|
|
679
|
+
'Sender hook: .claude/hooks/play-tts-ssh-remote.sh',
|
|
680
|
+
'Config file: .agentvibes/config/agentvibes.json',
|
|
681
|
+
'',
|
|
682
|
+
'Run these steps on the remote server that needs TTS.',
|
|
683
|
+
'',
|
|
684
|
+
'1. Add an SSH alias for the receiver (~/.ssh/config):',
|
|
685
|
+
'',
|
|
686
|
+
' Host <RECEIVER_NAME>',
|
|
687
|
+
' HostName <RECEIVER_IP>',
|
|
688
|
+
' Port <RECEIVER_PORT>',
|
|
689
|
+
' User agentvibes-receiver',
|
|
690
|
+
' IdentityFile ~/.ssh/id_ed25519',
|
|
691
|
+
'',
|
|
692
|
+
'2. Tell AgentVibes where to send TTS:',
|
|
693
|
+
'',
|
|
694
|
+
' echo "<RECEIVER_NAME>" > .claude/ssh-remote-host.txt',
|
|
695
|
+
'',
|
|
696
|
+
'3. Switch to the ssh-remote provider:',
|
|
697
|
+
'',
|
|
698
|
+
' # In .agentvibes/config/agentvibes.json set "provider": "ssh-remote"',
|
|
699
|
+
' # Or run: agentvibes provider switch ssh-remote',
|
|
700
|
+
'',
|
|
701
|
+
'The sender hook at .claude/hooks/play-tts-ssh-remote.sh',
|
|
702
|
+
'bundles voice, effects, and music into a single JSON payload',
|
|
703
|
+
'and sends it over SSH. No TTS software needed on the server.',
|
|
704
|
+
'',
|
|
705
|
+
'',
|
|
706
|
+
'============================================================',
|
|
707
|
+
'PART 3: VERIFICATION (test end-to-end)',
|
|
708
|
+
'============================================================',
|
|
709
|
+
'',
|
|
710
|
+
'Use tmux to test both sides simultaneously:',
|
|
711
|
+
'',
|
|
712
|
+
' tmux new-session -d -s agentvibes-verify',
|
|
713
|
+
' # Left pane: watch receiver log on LOCAL machine',
|
|
714
|
+
' tmux send-keys "tail -f /home/agentvibes-receiver/.agentvibes/receiver.log \\',
|
|
715
|
+
' || tail -f ~/.agentvibes/receiver.log" Enter',
|
|
716
|
+
' # Right pane: send test from SERVER',
|
|
717
|
+
' tmux split-window -h',
|
|
718
|
+
' tmux send-keys "ssh <your-server>" Enter',
|
|
719
|
+
' tmux attach -t agentvibes-verify',
|
|
720
|
+
'',
|
|
721
|
+
'Then in the server pane, run these tests in order:',
|
|
722
|
+
'',
|
|
723
|
+
'Test 1 — SSH connectivity:',
|
|
724
|
+
' ssh <RECEIVER_NAME> "echo hello"',
|
|
725
|
+
' # Expected: ForceCommand runs, you see RECEIVED in the log pane',
|
|
726
|
+
'',
|
|
727
|
+
'Test 2 — TTS from server:',
|
|
728
|
+
' echo \'{"text":"Hello from server test","voice":"en_US-lessac-medium"}\' \\',
|
|
729
|
+
' | base64 | xargs ssh <RECEIVER_NAME>',
|
|
730
|
+
' # Expected: Audio plays on receiver speakers, log shows DONE',
|
|
731
|
+
'',
|
|
732
|
+
'Test 3 — Full AgentVibes pipeline:',
|
|
733
|
+
' bash .claude/hooks/play-tts.sh "Testing AgentVibes receiver"',
|
|
734
|
+
' # Expected: TTS with configured voice, effects, and music',
|
|
735
|
+
'',
|
|
736
|
+
'Or test locally on the receiver machine without SSH:',
|
|
737
|
+
'',
|
|
738
|
+
' sudo -u agentvibes-receiver \\',
|
|
739
|
+
' PULSE_SERVER=tcp:127.0.0.1:34567 \\',
|
|
740
|
+
' paplay /usr/share/sounds/freedesktop/stereo/bell.oga',
|
|
741
|
+
' # Expected: Bell sound plays through your speakers',
|
|
742
|
+
'',
|
|
743
|
+
' sudo -u agentvibes-receiver \\',
|
|
744
|
+
' PULSE_SERVER=tcp:127.0.0.1:34567 \\',
|
|
745
|
+
' /home/agentvibes-receiver/.agentvibes/play-remote.sh \\',
|
|
746
|
+
' "$(echo \'{"text":"Local pipeline test","voice":"en_US-lessac-medium"}\' | base64)"',
|
|
747
|
+
' # Expected: TTS audio plays, receiver.log shows RECEIVED → PLAYING → DONE',
|
|
748
|
+
'',
|
|
749
|
+
'',
|
|
750
|
+
'============================================================',
|
|
751
|
+
'TROUBLESHOOTING',
|
|
752
|
+
'============================================================',
|
|
753
|
+
'',
|
|
754
|
+
'SSH connection refused:',
|
|
755
|
+
' - Check sshd is running: systemctl status sshd',
|
|
756
|
+
' - Check firewall allows <RECEIVER_PORT>: sudo ufw status',
|
|
757
|
+
' - Check authorized_keys: cat /home/agentvibes-receiver/.ssh/authorized_keys',
|
|
758
|
+
'',
|
|
759
|
+
'No audio / connection refused on audio:',
|
|
760
|
+
' - Check TCP module: pactl list modules short | grep tcp',
|
|
761
|
+
' - Check cookie exists: ls -la /home/agentvibes-receiver/.config/pulse/cookie',
|
|
762
|
+
' - Test TCP directly: PULSE_SERVER=tcp:127.0.0.1:34567 pactl info',
|
|
763
|
+
'',
|
|
764
|
+
'Volume hijacked / wrong speaker:',
|
|
765
|
+
' - Verify flat-volumes disabled:',
|
|
766
|
+
' cat ~/.config/pipewire/pipewire-pulse.conf.d/no-flat-volumes.conf',
|
|
767
|
+
' - Select specific sink: echo "sink_name" > \\',
|
|
768
|
+
' /home/agentvibes-receiver/.agentvibes/receiver-sink.txt',
|
|
769
|
+
' - List available sinks: pactl list sinks short',
|
|
770
|
+
'',
|
|
771
|
+
'No voice models:',
|
|
772
|
+
' - Check: ls /home/agentvibes-receiver/.claude/piper-voices/*.onnx',
|
|
773
|
+
' - Re-copy: sudo cp ~/.claude/piper-voices/*.onnx* \\',
|
|
774
|
+
' /home/agentvibes-receiver/.claude/piper-voices/',
|
|
775
|
+
'',
|
|
776
|
+
'ForceCommand not working:',
|
|
777
|
+
' - Check sshd_config syntax: sudo sshd -t',
|
|
778
|
+
' - Reload sshd: sudo systemctl reload sshd',
|
|
779
|
+
' - Test manually: ssh agentvibes-receiver@localhost',
|
|
780
|
+
].join('\n');
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
export function createReceiverTab(screen, services) {
|
|
784
|
+
if (IS_TEST) return createTestStub();
|
|
785
|
+
|
|
786
|
+
const { languageService, focusMainTabBar } = services || {};
|
|
787
|
+
const _tl = (key) => languageService ? languageService.t(key) : t('en', key);
|
|
788
|
+
|
|
789
|
+
const AGENTVIBES_DIR = path.join(homedir(), '.agentvibes');
|
|
790
|
+
const RECEIVER_SCRIPT = path.join(AGENTVIBES_DIR, 'play-remote.sh');
|
|
791
|
+
const RECEIVER_ALIAS = 'my-receiver';
|
|
792
|
+
|
|
793
|
+
// Log file: check receiver user's home first, fall back to current user's
|
|
794
|
+
const RECEIVER_USER_LOG = '/home/agentvibes-receiver/.agentvibes/receiver.log';
|
|
795
|
+
const LOCAL_LOG = path.join(AGENTVIBES_DIR, 'receiver.log');
|
|
796
|
+
const LOG_FILE = existsSync(RECEIVER_USER_LOG) ? RECEIVER_USER_LOG : LOCAL_LOG;
|
|
797
|
+
|
|
798
|
+
// Sink config — shared with receiver script via receiver user's home
|
|
799
|
+
const RECEIVER_SINK_FILE = '/home/agentvibes-receiver/.agentvibes/receiver-sink.txt';
|
|
800
|
+
const LOCAL_SINK_FILE = path.join(AGENTVIBES_DIR, 'receiver-sink.txt');
|
|
801
|
+
const SINK_FILE = existsSync('/home/agentvibes-receiver/.agentvibes') ? RECEIVER_SINK_FILE : LOCAL_SINK_FILE;
|
|
802
|
+
|
|
803
|
+
// -------------------------------------------------------------------------
|
|
804
|
+
// Container
|
|
805
|
+
|
|
806
|
+
const box = blessed.box({
|
|
807
|
+
parent: screen,
|
|
808
|
+
top: 5,
|
|
809
|
+
left: 0,
|
|
810
|
+
width: '100%',
|
|
811
|
+
bottom: 2,
|
|
812
|
+
hidden: true,
|
|
813
|
+
keys: true,
|
|
814
|
+
style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
|
|
815
|
+
border: { type: 'line' },
|
|
816
|
+
borderStyle: { fg: COLORS.borderFg },
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
box.key(['escape'], () => {
|
|
820
|
+
if (typeof focusMainTabBar === 'function') { focusMainTabBar(); screen.render(); }
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
// -------------------------------------------------------------------------
|
|
824
|
+
// Description text (collapsible)
|
|
825
|
+
|
|
826
|
+
const descBox = blessed.box({
|
|
827
|
+
parent: box,
|
|
828
|
+
top: 0,
|
|
829
|
+
left: 2,
|
|
830
|
+
width: '96%',
|
|
831
|
+
height: 9,
|
|
832
|
+
tags: true,
|
|
833
|
+
hidden: true,
|
|
834
|
+
border: { type: 'line' },
|
|
835
|
+
label: ` {bold}${_tl('receiverWhatIsTitle')}{/bold} `,
|
|
836
|
+
style: {
|
|
837
|
+
fg: COLORS.labelFg,
|
|
838
|
+
bg: '#111827',
|
|
839
|
+
border: { fg: COLORS.sectionHdr },
|
|
840
|
+
},
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
const descText = blessed.text({
|
|
844
|
+
parent: descBox,
|
|
845
|
+
top: 0,
|
|
846
|
+
left: 1,
|
|
847
|
+
tags: true,
|
|
848
|
+
content: _tl('receiverDesc'),
|
|
849
|
+
style: { fg: '#b0bec5', bg: '#111827' },
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
blessed.text({
|
|
853
|
+
parent: descBox,
|
|
854
|
+
top: 6,
|
|
855
|
+
right: 2,
|
|
856
|
+
tags: true,
|
|
857
|
+
content: '{#90a4ae-fg}Press {bold}[?]{/bold} to close{/#90a4ae-fg}',
|
|
858
|
+
style: { bg: '#111827' },
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
// -------------------------------------------------------------------------
|
|
862
|
+
// Top: actions row + status row + info row + feedback
|
|
863
|
+
// Positions are dynamic — shift down when description is open
|
|
864
|
+
|
|
865
|
+
const _topOffset = () => _showDescription ? 10 : 0;
|
|
866
|
+
|
|
867
|
+
const actionsLine = blessed.text({
|
|
868
|
+
parent: box,
|
|
869
|
+
top: 0, // updated dynamically
|
|
870
|
+
left: 4,
|
|
871
|
+
tags: true,
|
|
872
|
+
content: '',
|
|
873
|
+
style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
const statusLine = blessed.text({
|
|
877
|
+
parent: box,
|
|
878
|
+
top: 1, // updated dynamically
|
|
879
|
+
left: 4,
|
|
880
|
+
tags: true,
|
|
881
|
+
content: '',
|
|
882
|
+
style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
const infoLine = blessed.text({
|
|
886
|
+
parent: box,
|
|
887
|
+
top: 2, // updated dynamically
|
|
888
|
+
left: 4,
|
|
889
|
+
tags: true,
|
|
890
|
+
content: '',
|
|
891
|
+
style: { fg: COLORS.noticeFg, bg: COLORS.contentBg },
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
const feedbackLine = blessed.text({
|
|
895
|
+
parent: box,
|
|
896
|
+
top: 3, // updated dynamically
|
|
897
|
+
left: 4,
|
|
898
|
+
tags: true,
|
|
899
|
+
content: '',
|
|
900
|
+
style: { fg: COLORS.noticeFg, bg: COLORS.contentBg },
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
// -------------------------------------------------------------------------
|
|
904
|
+
// Separator + section label + main content
|
|
905
|
+
|
|
906
|
+
const separatorLine = blessed.text({
|
|
907
|
+
parent: box,
|
|
908
|
+
top: 5, // updated dynamically
|
|
909
|
+
left: 2,
|
|
910
|
+
content: `{${COLORS.sectionHdr}-fg}${'─'.repeat(68)}{/${COLORS.sectionHdr}-fg}`,
|
|
911
|
+
tags: true,
|
|
912
|
+
style: { bg: COLORS.contentBg },
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
const sectionLabel = blessed.text({
|
|
916
|
+
parent: box,
|
|
917
|
+
top: 5, // updated dynamically
|
|
918
|
+
left: 4,
|
|
919
|
+
tags: true,
|
|
920
|
+
content: '',
|
|
921
|
+
style: { bg: COLORS.contentBg },
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
const contentBox = blessed.box({
|
|
925
|
+
parent: box,
|
|
926
|
+
top: 7, // updated dynamically
|
|
927
|
+
left: 2,
|
|
928
|
+
width: '96%',
|
|
929
|
+
bottom: 2,
|
|
930
|
+
tags: true,
|
|
931
|
+
scrollable: true,
|
|
932
|
+
alwaysScroll: true,
|
|
933
|
+
scrollbar: { ch: '│', style: { fg: COLORS.sectionHdr } },
|
|
934
|
+
border: { type: 'line' },
|
|
935
|
+
focusable: true,
|
|
936
|
+
style: {
|
|
937
|
+
fg: COLORS.labelFg,
|
|
938
|
+
bg: COLORS.contentBg,
|
|
939
|
+
border: { fg: COLORS.borderFg },
|
|
940
|
+
},
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
// -------------------------------------------------------------------------
|
|
944
|
+
// State
|
|
945
|
+
|
|
946
|
+
let _messages = [];
|
|
947
|
+
let _watchActive = false;
|
|
948
|
+
let _showDetails = null; // null = auto (based on receiver state), true/false = manual override
|
|
949
|
+
let _showDescription = true; // Show description on first visit
|
|
950
|
+
|
|
951
|
+
// -------------------------------------------------------------------------
|
|
952
|
+
// Receiver management
|
|
953
|
+
|
|
954
|
+
function _isReceiverEnabled() {
|
|
955
|
+
return existsSync(RECEIVER_SCRIPT);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function _enableReceiver() {
|
|
959
|
+
try {
|
|
960
|
+
mkdirSync(AGENTVIBES_DIR, { recursive: true, mode: 0o700 });
|
|
961
|
+
if (existsSync(TEMPLATE_PATH)) {
|
|
962
|
+
copyFileSync(TEMPLATE_PATH, RECEIVER_SCRIPT);
|
|
963
|
+
chmodSync(RECEIVER_SCRIPT, 0o755);
|
|
964
|
+
return true;
|
|
965
|
+
}
|
|
966
|
+
return false;
|
|
967
|
+
} catch {
|
|
968
|
+
return false;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function _disableReceiver() {
|
|
973
|
+
try {
|
|
974
|
+
unlinkSync(RECEIVER_SCRIPT);
|
|
975
|
+
return true;
|
|
976
|
+
} catch {
|
|
977
|
+
return false;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Send a test TTS message from this machine to the receiver via SSH.
|
|
983
|
+
* Mirrors the payload format used by play-tts-ssh-remote.sh.
|
|
984
|
+
*/
|
|
985
|
+
function _sendTest() {
|
|
986
|
+
// Read SSH host
|
|
987
|
+
const projectRoot = path.resolve(_thisDir, '..', '..', '..');
|
|
988
|
+
const hostPaths = [
|
|
989
|
+
path.join(projectRoot, '.claude', 'ssh-remote-host.txt'),
|
|
990
|
+
path.join(homedir(), '.claude', 'ssh-remote-host.txt'),
|
|
991
|
+
];
|
|
992
|
+
let sshHost = '';
|
|
993
|
+
for (const p of hostPaths) {
|
|
994
|
+
try { sshHost = readFileSync(p, 'utf-8').trim(); break; } catch { /* next */ }
|
|
995
|
+
}
|
|
996
|
+
if (!sshHost) {
|
|
997
|
+
_showFeedback('{red-fg}No SSH host configured — set .claude/ssh-remote-host.txt{/red-fg}');
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
// Validate host format
|
|
1001
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(sshHost)) {
|
|
1002
|
+
_showFeedback('{red-fg}Invalid SSH host format{/red-fg}');
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// Read voice (best-effort, fall back to default)
|
|
1007
|
+
let voice = 'en_US-lessac-medium';
|
|
1008
|
+
const voicePaths = [
|
|
1009
|
+
path.join(projectRoot, '.claude', 'tts-voice.txt'),
|
|
1010
|
+
path.join(homedir(), '.agentvibes', 'config', 'voice.txt'),
|
|
1011
|
+
];
|
|
1012
|
+
for (const p of voicePaths) {
|
|
1013
|
+
try { const v = readFileSync(p, 'utf-8').trim(); if (v) { voice = v; break; } } catch { /* next */ }
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
const payload = JSON.stringify({
|
|
1017
|
+
text: 'AgentVibes receiver test — if you hear this, it works!',
|
|
1018
|
+
voice,
|
|
1019
|
+
effects: '',
|
|
1020
|
+
music: '',
|
|
1021
|
+
volume: '0.10',
|
|
1022
|
+
project: 'agentvibes-tui',
|
|
1023
|
+
pretext: '',
|
|
1024
|
+
speed: '1.0',
|
|
1025
|
+
provider: 'piper',
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
const encoded = Buffer.from(payload).toString('base64');
|
|
1029
|
+
|
|
1030
|
+
_showFeedback('{yellow-fg}Sending test to ' + sshHost + '...{/yellow-fg}');
|
|
1031
|
+
screen.render();
|
|
1032
|
+
|
|
1033
|
+
// Fire SSH in background — don't block the TUI
|
|
1034
|
+
const child = spawn('ssh', ['-o', 'ConnectTimeout=5', sshHost, encoded], {
|
|
1035
|
+
stdio: 'ignore',
|
|
1036
|
+
detached: true,
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
child.on('close', (code) => {
|
|
1040
|
+
if (code === 0) {
|
|
1041
|
+
_showFeedback('{green-fg}Test sent! Check receiver for audio playback.{/green-fg}');
|
|
1042
|
+
} else {
|
|
1043
|
+
_showFeedback(`{red-fg}SSH failed (exit ${code}) — check host and key config{/red-fg}`);
|
|
1044
|
+
}
|
|
1045
|
+
screen.render();
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
child.on('error', (err) => {
|
|
1049
|
+
_showFeedback(`{red-fg}SSH error: ${err.message}{/red-fg}`);
|
|
1050
|
+
screen.render();
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
child.unref();
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// -------------------------------------------------------------------------
|
|
1057
|
+
// Log parsing
|
|
1058
|
+
|
|
1059
|
+
function _parseLogFile() {
|
|
1060
|
+
if (!existsSync(LOG_FILE)) return [];
|
|
1061
|
+
try {
|
|
1062
|
+
const content = readFileSync(LOG_FILE, 'utf-8');
|
|
1063
|
+
const lines = content.trim().split('\n').filter(l => l.length > 0);
|
|
1064
|
+
return lines
|
|
1065
|
+
.filter(line => line.includes('|')) // Skip v1 format lines
|
|
1066
|
+
.map(line => {
|
|
1067
|
+
const parts = line.split('|');
|
|
1068
|
+
// Extract music from detail field (e.g., "effects=none music=track.mp3")
|
|
1069
|
+
const detail = parts[5] || '';
|
|
1070
|
+
const musicMatch = detail.match(/music=(\S+)/);
|
|
1071
|
+
const musicRaw = musicMatch ? musicMatch[1] : '';
|
|
1072
|
+
// Convert filename to friendly name: "agentvibes_soft_flamenco_loop.mp3" → "Soft Flamenco Loop"
|
|
1073
|
+
let music = '';
|
|
1074
|
+
if (musicRaw && musicRaw !== 'none') {
|
|
1075
|
+
music = musicRaw
|
|
1076
|
+
.replace(/\.[^.]+$/, '') // strip extension
|
|
1077
|
+
.replace(/^agent_?vibes_/i, '') // strip agent_vibes_ or agentvibes_ prefix
|
|
1078
|
+
.replace(/_?loop$/i, '') // strip _loop suffix
|
|
1079
|
+
.replace(/_v\d+$/i, '') // strip _v1, _v2 etc
|
|
1080
|
+
.replace(/_/g, ' ') // underscores to spaces
|
|
1081
|
+
.replace(/\b\w/g, c => c.toUpperCase()); // title case
|
|
1082
|
+
}
|
|
1083
|
+
return {
|
|
1084
|
+
timestamp: parts[0] || '',
|
|
1085
|
+
status: parts[1] || '',
|
|
1086
|
+
project: parts[2] || 'unknown',
|
|
1087
|
+
voice: parts[3] || '',
|
|
1088
|
+
textPreview: parts[4] || '',
|
|
1089
|
+
detail,
|
|
1090
|
+
music,
|
|
1091
|
+
ip: parts[6] || '',
|
|
1092
|
+
logId: parts[7] || '',
|
|
1093
|
+
};
|
|
1094
|
+
});
|
|
1095
|
+
} catch {
|
|
1096
|
+
return [];
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function _formatMessage(msg) {
|
|
1101
|
+
const [date = '', time = ''] = (msg.timestamp || '').split('T');
|
|
1102
|
+
const statusRaw = msg.status === 'DONE' ? 'OK ' :
|
|
1103
|
+
msg.status === 'ERROR' ? 'ERR ' :
|
|
1104
|
+
msg.status === 'PLAYING' ? 'PLAY' :
|
|
1105
|
+
msg.status === 'RECEIVED' ? 'RECV' :
|
|
1106
|
+
msg.status === 'WARN' ? 'WARN' :
|
|
1107
|
+
msg.status.substring(0, 4).padEnd(4);
|
|
1108
|
+
// Color-coded status
|
|
1109
|
+
const statusColor = msg.status === 'DONE' ? 'green' :
|
|
1110
|
+
msg.status === 'ERROR' ? 'red' :
|
|
1111
|
+
msg.status === 'WARN' ? 'yellow' :
|
|
1112
|
+
msg.status === 'PLAYING' ? 'cyan' : 'white';
|
|
1113
|
+
const status = `{${statusColor}-fg}${statusRaw}{/${statusColor}-fg}`;
|
|
1114
|
+
const logId = `{#607d8b-fg}${(msg.logId || '—').padEnd(5)}{/#607d8b-fg}`;
|
|
1115
|
+
const ip = `{#ce93d8-fg}${(msg.ip || '—').substring(0, 15).padEnd(15)}{/#ce93d8-fg}`;
|
|
1116
|
+
const project = `{#4fc3f7-fg}${msg.project.substring(0, 12).padEnd(12)}{/#4fc3f7-fg}`;
|
|
1117
|
+
const voice = `{#ffb74d-fg}${msg.voice.substring(0, 18).padEnd(18)}{/#ffb74d-fg}`;
|
|
1118
|
+
const music = `{#a5d6a7-fg}${(msg.music || '—').substring(0, 15).padEnd(15)}{/#a5d6a7-fg}`;
|
|
1119
|
+
// Parse playback detail (sink, vol, pulse) from PLAYING log line
|
|
1120
|
+
const pd = msg.playDetail || '';
|
|
1121
|
+
const sinkMatch = pd.match(/sink=(\S+)/);
|
|
1122
|
+
const volMatch = pd.match(/vol=(\S+)/);
|
|
1123
|
+
const sinkName = sinkMatch ? sinkMatch[1].replace(/^alsa_output\./, '').substring(0, 20) : '—';
|
|
1124
|
+
const vol = volMatch ? volMatch[1] : '—';
|
|
1125
|
+
const sink = `{#b39ddb-fg}${sinkName.padEnd(20)}{/#b39ddb-fg}`;
|
|
1126
|
+
const volume = `{#ef9a9a-fg}${vol.padEnd(5)}{/#ef9a9a-fg}`;
|
|
1127
|
+
const text = `{red-fg}${msg.textPreview}{/red-fg}`;
|
|
1128
|
+
return `${logId} {#90a4ae-fg}${date} ${time}{/#90a4ae-fg} ${status} ${ip} ${project} ${voice} ${sink} ${volume} ${music} ${text}`;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// -------------------------------------------------------------------------
|
|
1132
|
+
// Health check
|
|
1133
|
+
|
|
1134
|
+
function _getToolChecks() {
|
|
1135
|
+
const checks = [];
|
|
1136
|
+
const _isWinCheck = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
|
|
1137
|
+
const cmdCheck = (cmd) => {
|
|
1138
|
+
try {
|
|
1139
|
+
execSync(_isWinCheck ? `where ${cmd}` : `command -v ${cmd}`, { stdio: 'pipe', timeout: 3000 });
|
|
1140
|
+
return true;
|
|
1141
|
+
} catch {
|
|
1142
|
+
return false;
|
|
1143
|
+
}
|
|
1144
|
+
};
|
|
1145
|
+
|
|
1146
|
+
checks.push(cmdCheck('piper') ? '{green-fg}piper{/green-fg}' : '{red-fg}piper{/red-fg}');
|
|
1147
|
+
checks.push(cmdCheck('sox') ? '{green-fg}sox{/green-fg}' : '{yellow-fg}sox{/yellow-fg}');
|
|
1148
|
+
checks.push(cmdCheck('ffmpeg') ? '{green-fg}ffmpeg{/green-fg}' : '{yellow-fg}ffmpeg{/yellow-fg}');
|
|
1149
|
+
|
|
1150
|
+
let player = 'none';
|
|
1151
|
+
for (const p of ['pw-play', 'paplay', 'aplay']) {
|
|
1152
|
+
if (cmdCheck(p)) { player = p; break; }
|
|
1153
|
+
}
|
|
1154
|
+
checks.push(player !== 'none' ? `{green-fg}${player}{/green-fg}` : '{red-fg}no player{/red-fg}');
|
|
1155
|
+
return checks.join(' ');
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// -------------------------------------------------------------------------
|
|
1159
|
+
// Feedback flash (shows a message for 3 seconds)
|
|
1160
|
+
|
|
1161
|
+
let _feedbackTimer = null;
|
|
1162
|
+
function _showFeedback(msg) {
|
|
1163
|
+
feedbackLine.setContent(' ' + msg);
|
|
1164
|
+
screen.render();
|
|
1165
|
+
if (_feedbackTimer) clearTimeout(_feedbackTimer);
|
|
1166
|
+
_feedbackTimer = setTimeout(() => {
|
|
1167
|
+
_updateFeedbackDefault();
|
|
1168
|
+
screen.render();
|
|
1169
|
+
}, 3000);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
function _updateFeedbackDefault() {
|
|
1173
|
+
feedbackLine.setContent('');
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// -------------------------------------------------------------------------
|
|
1177
|
+
// Refresh display
|
|
1178
|
+
|
|
1179
|
+
// Cache network info and tool checks (refresh every 30s, not every render)
|
|
1180
|
+
let _networkInfo = { tailscaleIp: '', localIp: '', sshPort: '22' };
|
|
1181
|
+
let _toolChecksCache = '';
|
|
1182
|
+
let _lastCacheTime = 0;
|
|
1183
|
+
const CACHE_TTL_MS = 30000;
|
|
1184
|
+
|
|
1185
|
+
function _refreshCachedInfo() {
|
|
1186
|
+
const now = Date.now();
|
|
1187
|
+
if (now - _lastCacheTime > CACHE_TTL_MS) {
|
|
1188
|
+
_networkInfo = _getNetworkInfo();
|
|
1189
|
+
_toolChecksCache = _getToolChecks();
|
|
1190
|
+
_lastCacheTime = now;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
function refreshDisplay() {
|
|
1195
|
+
const enabled = _isReceiverEnabled();
|
|
1196
|
+
_refreshCachedInfo();
|
|
1197
|
+
|
|
1198
|
+
// Toggle description box
|
|
1199
|
+
if (_showDescription) {
|
|
1200
|
+
descBox.show();
|
|
1201
|
+
} else {
|
|
1202
|
+
descBox.hide();
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// Dynamic positioning based on description visibility
|
|
1206
|
+
const offset = _showDescription ? 10 : 0;
|
|
1207
|
+
actionsLine.top = offset;
|
|
1208
|
+
statusLine.top = offset + 1;
|
|
1209
|
+
infoLine.top = offset + 2;
|
|
1210
|
+
feedbackLine.top = offset + 3;
|
|
1211
|
+
separatorLine.top = offset + 5;
|
|
1212
|
+
sectionLabel.top = offset + 5;
|
|
1213
|
+
contentBox.top = offset + 7;
|
|
1214
|
+
|
|
1215
|
+
// Auto-show instructions when receiver isn't set up; manual toggle overrides
|
|
1216
|
+
const showingDetails = _showDetails !== null ? _showDetails : !enabled;
|
|
1217
|
+
|
|
1218
|
+
// Actions row — each action a different color
|
|
1219
|
+
const enableLabel = enabled
|
|
1220
|
+
? '{#ef5350-fg}{bold}[E]{/bold} Turn Off{/#ef5350-fg}'
|
|
1221
|
+
: '{#66bb6a-fg}{bold}[E]{/bold} Turn On{/#66bb6a-fg}';
|
|
1222
|
+
const speakerKey = '{#ce93d8-fg}{bold}[O]{/bold} Speaker{/#ce93d8-fg}';
|
|
1223
|
+
const detailLabel = showingDetails
|
|
1224
|
+
? '{#4fc3f7-fg}{bold}[D]{/bold} Messages{/#4fc3f7-fg}'
|
|
1225
|
+
: '{#4fc3f7-fg}{bold}[D]{/bold} Setup Guide{/#4fc3f7-fg}';
|
|
1226
|
+
const testKey = '{#ffd54f-fg}{bold}[P]{/bold} Test{/#ffd54f-fg}';
|
|
1227
|
+
const clearKey = '{#ffb74d-fg}{bold}[C]{/bold} Clear Log{/#ffb74d-fg}';
|
|
1228
|
+
const copyKey = showingDetails ? '{#a5d6a7-fg}{bold}[A]{/bold} Copy Setup{/#a5d6a7-fg}' : '';
|
|
1229
|
+
const descLabel = _showDescription
|
|
1230
|
+
? '{#90a4ae-fg}{bold}[?]{/bold} Hide Info{/#90a4ae-fg}'
|
|
1231
|
+
: '{#90a4ae-fg}{bold}[?]{/bold} What is this?{/#90a4ae-fg}';
|
|
1232
|
+
const actions = [enableLabel, speakerKey, testKey, detailLabel, clearKey, copyKey, descLabel].filter(Boolean);
|
|
1233
|
+
actionsLine.setContent(' ' + actions.join(' '));
|
|
1234
|
+
|
|
1235
|
+
// Status + Speaker
|
|
1236
|
+
const statusIcon = enabled ? '{green-fg}● ON{/green-fg}' : '{yellow-fg}● OFF{/yellow-fg}';
|
|
1237
|
+
let speakerDisplay = '{#90a4ae-fg}(default){/#90a4ae-fg}';
|
|
1238
|
+
try {
|
|
1239
|
+
const configured = readFileSync(SINK_FILE, 'utf-8').trim();
|
|
1240
|
+
if (configured) speakerDisplay = `{bold}${configured.replace(/^alsa_output\./, '')}{/bold}`;
|
|
1241
|
+
} catch { /* no config */ }
|
|
1242
|
+
statusLine.setContent(` Status: ${statusIcon} Speaker: ${speakerDisplay}`);
|
|
1243
|
+
|
|
1244
|
+
// Network + tools + log — IP yellow, port cyan
|
|
1245
|
+
const ipDisplay = _networkInfo.tailscaleIp || _networkInfo.localIp || 'unknown';
|
|
1246
|
+
infoLine.setContent(` IP: {yellow-fg}{bold}${ipDisplay}{/bold}{/yellow-fg} Port: {#4fc3f7-fg}{bold}${_networkInfo.sshPort}{/bold}{/#4fc3f7-fg} Tools: ${_toolChecksCache} Log: {#90a4ae-fg}${LOG_FILE}{/#90a4ae-fg}`);
|
|
1247
|
+
|
|
1248
|
+
_updateFeedbackDefault();
|
|
1249
|
+
|
|
1250
|
+
// Main content
|
|
1251
|
+
_messages = _parseLogFile();
|
|
1252
|
+
|
|
1253
|
+
if (showingDetails) {
|
|
1254
|
+
sectionLabel.setContent(`{${COLORS.sectionHdr}-fg} Setup Instructions {/${COLORS.sectionHdr}-fg}`);
|
|
1255
|
+
const copyHint = '{#a5d6a7-fg}Press {bold}[A]{/bold} to copy these instructions to clipboard{/#a5d6a7-fg}\n';
|
|
1256
|
+
contentBox.setContent(copyHint + _buildDetailedInstructions(RECEIVER_ALIAS, RECEIVER_SCRIPT, _networkInfo));
|
|
1257
|
+
} else {
|
|
1258
|
+
sectionLabel.setContent(`{${COLORS.sectionHdr}-fg} Messages {/${COLORS.sectionHdr}-fg}`);
|
|
1259
|
+
|
|
1260
|
+
if (_messages.length === 0) {
|
|
1261
|
+
const text = [
|
|
1262
|
+
'No messages received yet. Waiting for SSH TTS requests...',
|
|
1263
|
+
'',
|
|
1264
|
+
'Press [D] above for setup guide.',
|
|
1265
|
+
].join('\n');
|
|
1266
|
+
contentBox.setContent(text);
|
|
1267
|
+
} else {
|
|
1268
|
+
const header = `{#607d8b-fg}${'ID'.padEnd(5)}{/#607d8b-fg} {#90a4ae-fg}${'DATE'.padEnd(10)} ${'TIME'.padEnd(8)}{/#90a4ae-fg} {bold}${'STAT'.padEnd(4)}{/bold} {#ce93d8-fg}${'IP'.padEnd(15)}{/#ce93d8-fg} {#4fc3f7-fg}${'PROJECT'.padEnd(12)}{/#4fc3f7-fg} {#ffb74d-fg}${'VOICE'.padEnd(18)}{/#ffb74d-fg} {#b39ddb-fg}${'SPEAKER'.padEnd(20)}{/#b39ddb-fg} {#ef9a9a-fg}${'VOL'.padEnd(5)}{/#ef9a9a-fg} {#a5d6a7-fg}${'MUSIC'.padEnd(15)}{/#a5d6a7-fg} {red-fg}TEXT{/red-fg}`;
|
|
1269
|
+
const separator = '─'.repeat(78);
|
|
1270
|
+
const lines = [header, separator];
|
|
1271
|
+
// Group log lines per request — show one row with final status
|
|
1272
|
+
// Each request produces RECEIVED → PLAYING → DONE/ERROR
|
|
1273
|
+
const grouped = [];
|
|
1274
|
+
let current = null;
|
|
1275
|
+
for (const msg of _messages) {
|
|
1276
|
+
if (msg.status === 'RECEIVED') {
|
|
1277
|
+
current = { ...msg };
|
|
1278
|
+
} else if (current && msg.status === 'PLAYING') {
|
|
1279
|
+
// Merge PLAYING detail (sink, vol, pulse) into grouped row
|
|
1280
|
+
current.playDetail = msg.detail;
|
|
1281
|
+
} else if (current && (msg.status === 'DONE' || msg.status === 'ERROR' || msg.status === 'WARN')) {
|
|
1282
|
+
current.status = msg.status;
|
|
1283
|
+
current.timestamp = msg.timestamp;
|
|
1284
|
+
grouped.push(current);
|
|
1285
|
+
current = null;
|
|
1286
|
+
} else if (!current && (msg.status === 'DONE' || msg.status === 'ERROR')) {
|
|
1287
|
+
// Orphaned status — show as-is
|
|
1288
|
+
grouped.push(msg);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
// If a request is still in-progress, show it
|
|
1292
|
+
if (current) {
|
|
1293
|
+
grouped.push(current);
|
|
1294
|
+
}
|
|
1295
|
+
const recent = grouped.slice(-50).reverse();
|
|
1296
|
+
for (const msg of recent) {
|
|
1297
|
+
lines.push(_formatMessage(msg));
|
|
1298
|
+
}
|
|
1299
|
+
contentBox.setContent(lines.join('\n'));
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
contentBox.scrollTo(0);
|
|
1304
|
+
screen.render();
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// -------------------------------------------------------------------------
|
|
1308
|
+
// File watcher
|
|
1309
|
+
|
|
1310
|
+
function _startWatching() {
|
|
1311
|
+
if (_watchActive) return;
|
|
1312
|
+
_watchActive = true;
|
|
1313
|
+
try {
|
|
1314
|
+
watchFile(LOG_FILE, { interval: 2000 }, () => refreshDisplay());
|
|
1315
|
+
} catch { /* file may not exist yet */ }
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
function _stopWatching() {
|
|
1319
|
+
if (!_watchActive) return;
|
|
1320
|
+
_watchActive = false;
|
|
1321
|
+
try { unwatchFile(LOG_FILE); } catch { /* ignore */ }
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// -------------------------------------------------------------------------
|
|
1325
|
+
// Scroll bindings
|
|
1326
|
+
box.key(['up'], () => { contentBox.scroll(-1); screen.render(); });
|
|
1327
|
+
box.key(['down'], () => { contentBox.scroll(1); screen.render(); });
|
|
1328
|
+
box.key(['pageup'], () => { contentBox.scroll(-contentBox.height); screen.render(); });
|
|
1329
|
+
box.key(['pagedown'], () => { contentBox.scroll(contentBox.height); screen.render(); });
|
|
1330
|
+
|
|
1331
|
+
// -------------------------------------------------------------------------
|
|
1332
|
+
// Action key bindings
|
|
1333
|
+
|
|
1334
|
+
box.key(['e', 'E'], () => {
|
|
1335
|
+
if (_isReceiverEnabled()) {
|
|
1336
|
+
_disableReceiver();
|
|
1337
|
+
_showFeedback('{yellow-fg}Receiver disabled{/yellow-fg}');
|
|
1338
|
+
} else {
|
|
1339
|
+
if (_enableReceiver()) {
|
|
1340
|
+
_showFeedback('{green-fg}Receiver enabled! play-remote.sh installed.{/green-fg}');
|
|
1341
|
+
} else {
|
|
1342
|
+
_showFeedback('{red-fg}Failed to enable — template not found{/red-fg}');
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
refreshDisplay();
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
box.key(['d', 'D'], () => {
|
|
1349
|
+
// Manual override: toggle between instructions and messages
|
|
1350
|
+
const currentlyShowing = _showDetails !== null ? _showDetails : !_isReceiverEnabled();
|
|
1351
|
+
_showDetails = !currentlyShowing;
|
|
1352
|
+
refreshDisplay();
|
|
1353
|
+
});
|
|
1354
|
+
|
|
1355
|
+
box.key(['a', 'A'], () => {
|
|
1356
|
+
// Copy setup instructions to clipboard (only available when instructions are shown)
|
|
1357
|
+
const _currentlyShowingDetails = _showDetails !== null ? _showDetails : !_isReceiverEnabled();
|
|
1358
|
+
if (!_currentlyShowingDetails) {
|
|
1359
|
+
_showFeedback('{yellow-fg}Nothing to copy — setup instructions not shown{/yellow-fg}');
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
_refreshCachedInfo();
|
|
1363
|
+
const text = _buildDetailedInstructions(RECEIVER_ALIAS, RECEIVER_SCRIPT, _networkInfo)
|
|
1364
|
+
.replace(/\{[^}]*\}/g, '')
|
|
1365
|
+
// eslint-disable-next-line no-control-regex
|
|
1366
|
+
.replace(/\x1b\[[0-9;]*m/g, '');
|
|
1367
|
+
// Try platform-appropriate clipboard command
|
|
1368
|
+
const _isWin = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
|
|
1369
|
+
let copied = false;
|
|
1370
|
+
if (_isWin) {
|
|
1371
|
+
// Windows clip.exe mangles UTF-8 — use temp file + PowerShell with explicit encoding
|
|
1372
|
+
const tmpFile = path.join(AGENTVIBES_DIR, 'clip-tmp.txt');
|
|
1373
|
+
try {
|
|
1374
|
+
mkdirSync(AGENTVIBES_DIR, { recursive: true });
|
|
1375
|
+
writeFileSync(tmpFile, '\ufeff' + text, 'utf-8');
|
|
1376
|
+
const psCmd = `Get-Content -Path "${tmpFile.replace(/\\/g, '/')}" -Encoding UTF8 -Raw | Set-Clipboard; Remove-Item "${tmpFile.replace(/\\/g, '/')}"`;
|
|
1377
|
+
const r = spawnSync('powershell', ['-NoProfile', '-Command', psCmd], { timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
1378
|
+
if (r.status === 0) {
|
|
1379
|
+
_showFeedback('{green-fg}Copied to clipboard!{/green-fg}');
|
|
1380
|
+
copied = true;
|
|
1381
|
+
}
|
|
1382
|
+
} catch { /* fall through to file fallback */ }
|
|
1383
|
+
} else {
|
|
1384
|
+
const clipCmds = [['xclip', ['-selection', 'clipboard']], ['xsel', ['--clipboard', '--input']], ['wl-copy', []], ['pbcopy', []]];
|
|
1385
|
+
for (const [cmd, args] of clipCmds) {
|
|
1386
|
+
const r = spawnSync(cmd, args, { input: text, timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
1387
|
+
if (r.status === 0) {
|
|
1388
|
+
_showFeedback('{green-fg}Copied to clipboard!{/green-fg}');
|
|
1389
|
+
copied = true;
|
|
1390
|
+
break;
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
if (!copied) {
|
|
1395
|
+
// Last resort: save to file
|
|
1396
|
+
const filePath = path.join(AGENTVIBES_DIR, 'receiver-clipboard.txt');
|
|
1397
|
+
try {
|
|
1398
|
+
mkdirSync(AGENTVIBES_DIR, { recursive: true });
|
|
1399
|
+
writeFileSync(filePath, text + '\n');
|
|
1400
|
+
_showFeedback(`{yellow-fg}Saved to ${filePath}{/yellow-fg}`);
|
|
1401
|
+
} catch {
|
|
1402
|
+
_showFeedback('{red-fg}Failed to copy{/red-fg}');
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
});
|
|
1406
|
+
|
|
1407
|
+
box.key(['?'], () => {
|
|
1408
|
+
_showDescription = !_showDescription;
|
|
1409
|
+
refreshDisplay();
|
|
1410
|
+
});
|
|
1411
|
+
|
|
1412
|
+
box.key(['o', 'O'], () => {
|
|
1413
|
+
// List available audio sinks and let user pick one
|
|
1414
|
+
let sinks;
|
|
1415
|
+
try {
|
|
1416
|
+
const out = execSync('pactl --server=tcp:127.0.0.1:34567 list sinks short 2>/dev/null || pactl list sinks short 2>/dev/null', { timeout: 5000 }).toString().trim();
|
|
1417
|
+
sinks = out.split('\n').filter(l => l.length > 0).map(line => {
|
|
1418
|
+
const parts = line.split('\t');
|
|
1419
|
+
return { id: parts[0], name: parts[1] || '', driver: parts[2] || '', state: parts[4] || '' };
|
|
1420
|
+
});
|
|
1421
|
+
} catch {
|
|
1422
|
+
_showFeedback('{red-fg}Failed to list audio outputs{/red-fg}');
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
if (sinks.length === 0) {
|
|
1426
|
+
_showFeedback('{red-fg}No audio outputs found{/red-fg}');
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// Read current configured sink
|
|
1431
|
+
let currentSink = '';
|
|
1432
|
+
try { currentSink = readFileSync(SINK_FILE, 'utf-8').trim(); } catch { /* none set */ }
|
|
1433
|
+
|
|
1434
|
+
const sinkList = blessed.list({
|
|
1435
|
+
parent: screen,
|
|
1436
|
+
top: 'center',
|
|
1437
|
+
left: 'center',
|
|
1438
|
+
width: '80%',
|
|
1439
|
+
height: Math.min(sinks.length + 4, 20),
|
|
1440
|
+
tags: true,
|
|
1441
|
+
border: { type: 'line' },
|
|
1442
|
+
label: ' Select Audio Output (Enter to confirm, Esc to cancel) ',
|
|
1443
|
+
style: {
|
|
1444
|
+
fg: COLORS.labelFg,
|
|
1445
|
+
bg: '#1a1a2e',
|
|
1446
|
+
border: { fg: COLORS.sectionHdr },
|
|
1447
|
+
selected: { fg: '#000000', bg: '#80cbc4' },
|
|
1448
|
+
item: { fg: COLORS.labelFg, bg: '#1a1a2e' },
|
|
1449
|
+
},
|
|
1450
|
+
keys: true,
|
|
1451
|
+
vi: true,
|
|
1452
|
+
items: sinks.map(s => {
|
|
1453
|
+
const marker = s.name === currentSink ? ' {green-fg}◆{/green-fg}' : ' ';
|
|
1454
|
+
const stateColor = s.state === 'RUNNING' ? 'green' : s.state === 'SUSPENDED' ? 'yellow' : 'gray';
|
|
1455
|
+
// Strip alsa_output. prefix for readability
|
|
1456
|
+
const shortName = s.name.replace(/^alsa_output\./, '');
|
|
1457
|
+
return `${marker} {bold}${shortName}{/bold} {${stateColor}-fg}${s.state}{/${stateColor}-fg}`;
|
|
1458
|
+
}),
|
|
1459
|
+
});
|
|
1460
|
+
|
|
1461
|
+
sinkList.focus();
|
|
1462
|
+
screen.render();
|
|
1463
|
+
|
|
1464
|
+
sinkList.on('select', (_item, index) => {
|
|
1465
|
+
const chosen = sinks[index].name;
|
|
1466
|
+
try {
|
|
1467
|
+
writeFileSync(SINK_FILE, chosen + '\n');
|
|
1468
|
+
// Also write to receiver user's config if accessible
|
|
1469
|
+
if (SINK_FILE !== RECEIVER_SINK_FILE) {
|
|
1470
|
+
try { writeFileSync(RECEIVER_SINK_FILE, chosen + '\n'); } catch { /* no access */ }
|
|
1471
|
+
}
|
|
1472
|
+
_showFeedback(`{green-fg}Speaker set: ${chosen.replace(/^alsa_output\./, '')}{/green-fg}`);
|
|
1473
|
+
} catch (e) {
|
|
1474
|
+
_showFeedback(`{red-fg}Failed to save speaker: ${e.message}{/red-fg}`);
|
|
1475
|
+
}
|
|
1476
|
+
sinkList.destroy();
|
|
1477
|
+
box.focus();
|
|
1478
|
+
refreshDisplay();
|
|
1479
|
+
});
|
|
1480
|
+
|
|
1481
|
+
sinkList.key(['escape', 'q'], () => {
|
|
1482
|
+
sinkList.destroy();
|
|
1483
|
+
box.focus();
|
|
1484
|
+
screen.render();
|
|
1485
|
+
});
|
|
1486
|
+
});
|
|
1487
|
+
|
|
1488
|
+
box.key(['p', 'P'], () => {
|
|
1489
|
+
_sendTest();
|
|
1490
|
+
});
|
|
1491
|
+
|
|
1492
|
+
box.key(['c', 'C'], () => {
|
|
1493
|
+
try { writeFileSync(LOG_FILE, ''); } catch { /* ignore */ }
|
|
1494
|
+
_showFeedback('{green-fg}Log cleared{/green-fg}');
|
|
1495
|
+
refreshDisplay();
|
|
1496
|
+
});
|
|
1497
|
+
|
|
1498
|
+
// -------------------------------------------------------------------------
|
|
1499
|
+
// Language change handler
|
|
1500
|
+
|
|
1501
|
+
if (languageService) {
|
|
1502
|
+
languageService.onChange(() => {
|
|
1503
|
+
descBox.setLabel(` {bold}${_tl('receiverWhatIsTitle')}{/bold} `);
|
|
1504
|
+
descText.setContent(_tl('receiverDesc'));
|
|
1505
|
+
screen.render();
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
// Tab Component Contract
|
|
1510
|
+
|
|
1511
|
+
return {
|
|
1512
|
+
box,
|
|
1513
|
+
show() {
|
|
1514
|
+
box.show();
|
|
1515
|
+
refreshDisplay();
|
|
1516
|
+
_startWatching();
|
|
1517
|
+
},
|
|
1518
|
+
hide() {
|
|
1519
|
+
box.hide();
|
|
1520
|
+
_stopWatching();
|
|
1521
|
+
},
|
|
1522
|
+
onFocus() { box.focus(); },
|
|
1523
|
+
onBlur() {},
|
|
1524
|
+
getFooterText: () => _tl('receiverFooter'),
|
|
1525
|
+
getFooterColor: () => COLORS.footerBg,
|
|
1526
|
+
};
|
|
1527
|
+
}
|