agentvibes 5.10.1 → 5.11.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 +15 -1
- package/.agentvibes/install-manifest.json +342 -0
- package/.claude/audio/ui/CREDITS.txt +16 -0
- package/.claude/audio/ui/bling-success.wav +0 -0
- package/.claude/commands/agent-vibes/receiver.md +64 -0
- package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
- package/.claude/commands/agent-vibes-rdp.md +24 -24
- package/.claude/config/audio-effects.cfg +5 -4
- package/.claude/config/audio-effects.cfg.sample +52 -52
- package/.claude/config/background-music-enabled.txt +1 -1
- package/.claude/config/background-music-position.txt +27 -0
- package/.claude/config/background-music-volume.txt +1 -0
- package/.claude/config/background-music.cfg +1 -0
- package/.claude/config/background-music.txt +1 -0
- package/.claude/config/language.txt +1 -0
- package/.claude/config/personality.txt +1 -0
- package/.claude/config/reverb-level.txt +1 -0
- package/.claude/config/tts-speech-rate.txt +1 -0
- package/.claude/config/tts-verbosity.txt +1 -0
- package/.claude/docs/TERMUX_SETUP.md +408 -408
- package/.claude/github-star-reminder.txt +1 -1
- package/.claude/hooks/audio-cache-utils.sh +0 -0
- package/.claude/hooks/audio-processor.sh +57 -18
- package/.claude/hooks/background-music-manager.sh +0 -0
- package/.claude/hooks/bmad-party-speak.sh +0 -0
- package/.claude/hooks/bmad-speak-enhanced.sh +0 -0
- package/.claude/hooks/bmad-speak.sh +0 -0
- package/.claude/hooks/bmad-tts-injector.sh +0 -0
- package/.claude/hooks/bmad-voice-manager.sh +0 -0
- package/.claude/hooks/clawdbot-receiver-SECURE.sh +0 -0
- package/.claude/hooks/clawdbot-receiver.sh +0 -0
- package/.claude/hooks/clean-audio-cache.sh +0 -0
- package/.claude/hooks/cleanup-cache.sh +0 -0
- package/.claude/hooks/configure-rdp-mode.sh +0 -0
- package/.claude/hooks/download-extra-voices.sh +0 -0
- package/.claude/hooks/effects-manager.sh +0 -0
- package/.claude/hooks/github-star-reminder.sh +0 -0
- package/.claude/hooks/kokoro-installer.sh +117 -0
- package/.claude/hooks/kokoro-server.py +219 -0
- package/.claude/hooks/kokoro-tts.py +141 -0
- package/.claude/hooks/language-manager.sh +0 -0
- package/.claude/hooks/learn-manager.sh +0 -0
- package/.claude/hooks/macos-voice-manager.sh +0 -0
- package/.claude/hooks/migrate-background-music.sh +0 -0
- package/.claude/hooks/migrate-to-agentvibes.sh +0 -0
- package/.claude/hooks/optimize-background-music.sh +0 -0
- package/.claude/hooks/path-resolver.sh +0 -0
- package/.claude/hooks/personality-manager.sh +0 -0
- package/.claude/hooks/piper-download-voices.sh +39 -16
- package/.claude/hooks/piper-installer.sh +11 -6
- package/.claude/hooks/piper-multispeaker-registry.sh +0 -0
- package/.claude/hooks/piper-voice-manager.sh +6 -6
- package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +3 -1
- package/.claude/hooks/play-tts-agentvibes-receiver.sh +1 -0
- package/.claude/hooks/play-tts-elevenlabs.sh +360 -0
- package/.claude/hooks/play-tts-enhanced.sh +0 -0
- package/.claude/hooks/play-tts-kokoro.sh +127 -0
- package/.claude/hooks/play-tts-macos.sh +0 -0
- package/.claude/hooks/play-tts-piper.sh +0 -0
- package/.claude/hooks/play-tts-soprano.sh +0 -0
- package/.claude/hooks/play-tts-ssh-remote.sh +9 -2
- package/.claude/hooks/play-tts-termux-ssh.sh +3 -1
- package/.claude/hooks/play-tts-windows-receiver.sh +0 -0
- package/.claude/hooks/play-tts.sh +78 -2
- package/.claude/hooks/prepare-release.sh +0 -0
- package/.claude/hooks/provider-commands.sh +71 -22
- package/.claude/hooks/provider-manager.sh +15 -7
- package/.claude/hooks/replay-target-audio.sh +0 -0
- package/.claude/hooks/requirements.txt +6 -6
- package/.claude/hooks/sentiment-manager.sh +0 -0
- package/.claude/hooks/session-start-tts.sh +0 -0
- package/.claude/hooks/soprano-gradio-synth.py +139 -139
- package/.claude/hooks/speed-manager.sh +0 -0
- package/.claude/hooks/stop-tts.sh +0 -0
- package/.claude/hooks/termux-installer.sh +0 -0
- package/.claude/hooks/translate-manager.sh +0 -0
- package/.claude/hooks/translator.py +237 -237
- package/.claude/hooks/tts-queue-worker.sh +0 -0
- package/.claude/hooks/tts-queue.sh +0 -0
- package/.claude/hooks/verbosity-manager.sh +0 -0
- package/.claude/hooks/voice-manager.sh +0 -0
- package/.claude/hooks-windows/audio-cache-utils.ps1.user.bak +119 -0
- package/.claude/hooks-windows/kokoro-server.py +219 -0
- package/.claude/hooks-windows/kokoro-tts.py +107 -0
- package/.claude/hooks-windows/play-tts-kokoro.ps1 +191 -0
- package/.claude/hooks-windows/play-tts.ps1 +29 -1
- package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
- package/.claude/hooks-windows/soprano-gradio-synth.py.user.bak +153 -0
- package/.claude/piper-voices-dir.txt +1 -0
- package/.claude/verbosity.txt +1 -1
- package/.clawdbot/README.md +105 -105
- package/.mcp.json +6 -19
- package/README.md +12 -86
- package/RELEASE_NOTES.md +60 -0
- package/WINDOWS-SETUP.md +208 -208
- package/bin/agent-vibes +39 -39
- package/bin/agentvibes-voice-browser.js +0 -0
- package/bin/agentvibes.js +0 -0
- package/bin/mcp-server.js +121 -121
- package/bin/mcp-server.sh +0 -0
- package/bin/test-bmad-pr +78 -78
- package/mcp-server/QUICK_START.md +203 -203
- package/mcp-server/README.md +345 -345
- package/mcp-server/WINDOWS_SETUP.md +0 -0
- package/mcp-server/examples/claude_desktop_config.json +11 -11
- package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
- package/mcp-server/examples/custom_instructions.md +169 -169
- package/mcp-server/install-deps.js +0 -0
- package/mcp-server/server.py +1807 -1807
- package/mcp-server/test_server.py +0 -0
- package/package.json +4 -3
- package/src/cli/list-personalities.js +110 -110
- package/src/cli/list-voices.js +114 -114
- package/src/commands/bmad-voices.js +394 -394
- package/src/commands/install-mcp.js +3 -3
- package/src/console/app.js +25 -9
- package/src/console/audio-env.js +85 -1
- package/src/console/brand-colors.js +13 -13
- package/src/console/constants/personalities.js +44 -44
- package/src/console/navigation.js +4 -0
- package/src/console/tabs/agents-tab.js +12 -6
- package/src/console/tabs/help-tab.js +314 -314
- package/src/console/tabs/music-tab.js +7 -25
- package/src/console/tabs/readme-tab.js +272 -272
- package/src/console/tabs/setup-tab.js +1704 -120
- package/src/console/tabs/voices-tab.js +72 -88
- package/src/console/widgets/destroy-list.js +25 -25
- package/src/console/widgets/format-utils.js +14 -2
- package/src/console/widgets/help-bar.js +55 -0
- package/src/console/widgets/notice.js +55 -55
- package/src/console/widgets/reverb-picker.js +429 -41
- package/src/console/widgets/track-picker.js +60 -51
- package/src/i18n/de.js +2 -1
- package/src/i18n/en.js +1 -0
- package/src/i18n/es.js +2 -1
- package/src/i18n/fr.js +2 -1
- package/src/i18n/hi.js +2 -1
- package/src/i18n/ja.js +2 -1
- package/src/i18n/ko.js +2 -1
- package/src/i18n/pt.js +2 -1
- package/src/i18n/strings.js +9 -9
- package/src/i18n/zh-CN.js +2 -1
- package/src/installer/language-screen.js +31 -31
- package/src/installer/music-file-input.js +304 -304
- package/src/installer.js +95 -7
- package/src/services/config-service.js +264 -264
- package/src/services/language-service.js +47 -47
- package/src/services/provider-service.js +35 -0
- package/src/services/provider-voice-catalog.js +126 -0
- package/src/services/tts-engine-service.js +51 -2
- package/src/utils/audio-duration-validator.js +53 -10
- package/src/utils/audio-format-validator.js +277 -277
- package/src/utils/dependency-checker.js +469 -469
- package/src/utils/file-ownership-verifier.js +358 -358
- package/src/utils/music-file-validator.js +285 -285
- package/src/utils/preview-list-prompt.js +144 -136
- package/src/utils/provider-validator.js +70 -0
- package/src/utils/secure-music-storage.js +412 -412
- package/templates/agentvibes-receiver.sh +235 -231
- package/templates/audio/welcome-music.mp3 +0 -0
|
@@ -13,8 +13,7 @@ import fs from 'node:fs';
|
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import os from 'node:os';
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
|
-
import {
|
|
17
|
-
import { buildAudioEnv, detectMp3Player } from '../audio-env.js';
|
|
16
|
+
import { buildAudioEnv, spawnMp3Player } from '../audio-env.js';
|
|
18
17
|
import { t } from '../../i18n/strings.js';
|
|
19
18
|
|
|
20
19
|
// Package-relative tracks dir — used as fallback when cwd has no .claude/audio/tracks/
|
|
@@ -542,28 +541,14 @@ export function createMusicTab(screen, services) {
|
|
|
542
541
|
let _playingProcess = null;
|
|
543
542
|
let _playingTrackId = null;
|
|
544
543
|
|
|
545
|
-
// Kill the entire process group so child audio processes (ffplay, play, mpg123) all die
|
|
546
544
|
function _killPlayingProcess() {
|
|
547
545
|
if (_playingProcess) {
|
|
548
|
-
|
|
549
|
-
try {
|
|
550
|
-
if (_isWin) {
|
|
551
|
-
// Windows: kill the process tree via taskkill (process group kill doesn't work)
|
|
552
|
-
spawn('taskkill', ['/F', '/T', '/PID', String(_playingProcess.pid)], { // NOSONAR
|
|
553
|
-
stdio: 'ignore', windowsHide: true,
|
|
554
|
-
});
|
|
555
|
-
} else {
|
|
556
|
-
process.kill(-_playingProcess.pid, 'SIGTERM');
|
|
557
|
-
}
|
|
558
|
-
} catch (e) {
|
|
559
|
-
if (e.code !== 'ESRCH') { /* ignore */ }
|
|
560
|
-
}
|
|
546
|
+
_playingProcess.kill();
|
|
561
547
|
_playingProcess = null;
|
|
562
548
|
}
|
|
563
549
|
}
|
|
564
550
|
|
|
565
551
|
const _spawnEnv = buildAudioEnv();
|
|
566
|
-
const _detectedPlayer = detectMp3Player(_spawnEnv);
|
|
567
552
|
|
|
568
553
|
process.on('exit', () => { _killPlayingProcess(); });
|
|
569
554
|
|
|
@@ -594,7 +579,8 @@ export function createMusicTab(screen, services) {
|
|
|
594
579
|
_killPlayingProcess();
|
|
595
580
|
_playingTrackId = null;
|
|
596
581
|
|
|
597
|
-
|
|
582
|
+
const proc = spawnMp3Player(trackPath, _spawnEnv);
|
|
583
|
+
if (!proc) {
|
|
598
584
|
const installHint = process.platform === 'win32'
|
|
599
585
|
? 'No MP3 player found. Install ffmpeg: winget install ffmpeg'
|
|
600
586
|
: 'No MP3 player found. Install ffmpeg: sudo apt install ffmpeg';
|
|
@@ -604,18 +590,14 @@ export function createMusicTab(screen, services) {
|
|
|
604
590
|
return;
|
|
605
591
|
}
|
|
606
592
|
|
|
607
|
-
|
|
608
|
-
// Spawn the detected player directly (no sh -c chain — avoids VLC/cvlc stderr issues)
|
|
609
|
-
_playingProcess = spawn(_detectedPlayer.bin, _detectedPlayer.args(trackPath), {
|
|
610
|
-
stdio: 'ignore', detached: !_isWin, windowsHide: true, env: _spawnEnv,
|
|
611
|
-
});
|
|
593
|
+
_playingProcess = proc;
|
|
612
594
|
_playingTrackId = trackId;
|
|
613
595
|
|
|
614
596
|
const label = _allTracks.find(t => t.id === trackId)?.label ?? formatTrackLabel(trackId);
|
|
615
597
|
previewLine.setContent(`{${COLORS.playingFg}-fg}♪ Previewing: ${label} (Space again to stop){/${COLORS.playingFg}-fg}`);
|
|
616
598
|
screen.render();
|
|
617
599
|
|
|
618
|
-
|
|
600
|
+
proc.on('exit', () => {
|
|
619
601
|
if (_playingTrackId === trackId) {
|
|
620
602
|
_playingTrackId = null;
|
|
621
603
|
_playingProcess = null;
|
|
@@ -624,7 +606,7 @@ export function createMusicTab(screen, services) {
|
|
|
624
606
|
}
|
|
625
607
|
});
|
|
626
608
|
|
|
627
|
-
|
|
609
|
+
proc.on('error', () => {
|
|
628
610
|
if (_playingTrackId === trackId) {
|
|
629
611
|
_killPlayingProcess();
|
|
630
612
|
_playingTrackId = null;
|
|
@@ -1,272 +1,272 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AgentVibes TUI Console — Readme Tab
|
|
3
|
-
* Epic 13: Story 13.2
|
|
4
|
-
*
|
|
5
|
-
* Implements the Tab Component Contract:
|
|
6
|
-
* createReadmeTab(screen, services) → { box, show, hide, onFocus, onBlur, getFooterText, getFooterColor }
|
|
7
|
-
*
|
|
8
|
-
* Features: renders README.md with styled markdown, scrolling, search.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import fs from 'node:fs';
|
|
12
|
-
import path from 'node:path';
|
|
13
|
-
import { t } from '../../i18n/strings.js';
|
|
14
|
-
|
|
15
|
-
const IS_TEST = process.env.AGENTVIBES_TEST_MODE === 'true';
|
|
16
|
-
|
|
17
|
-
let blessed;
|
|
18
|
-
if (!IS_TEST) {
|
|
19
|
-
const { default: b } = await import('blessed');
|
|
20
|
-
blessed = b;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
|
|
25
|
-
const COLORS = {
|
|
26
|
-
contentBg: '#0a0e1a',
|
|
27
|
-
h1Fg: '#69f0ae', // Green — H1
|
|
28
|
-
h2Fg: '#82b1ff', // Blue — H2
|
|
29
|
-
h3Fg: '#80d8ff', // Light blue — H3
|
|
30
|
-
codeFg: '#ffff00', // Yellow — code spans
|
|
31
|
-
boldFg: '#e3f2fd', // Bright — bold text
|
|
32
|
-
quoteFg: '#90a4ae', // Gray — blockquotes
|
|
33
|
-
labelFg: '#e3f2fd',
|
|
34
|
-
borderFg: '#455a64',
|
|
35
|
-
footerBg: '#455a64', // Dark gray — Readme tab footer
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const _FOOTER_TEXT_EN = '[↑↓/jk] Scroll [PgUp/PgDn] Page [/] Search [S/V/M/A/R] Tab [Q] Quit';
|
|
39
|
-
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
|
-
// Markdown renderer (story 13.2)
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Render a single markdown line to blessed tagged string.
|
|
45
|
-
* Handles: H1/H2/H3, bold (**), code spans (`), blockquotes (>), plain text.
|
|
46
|
-
*
|
|
47
|
-
* @param {string} line
|
|
48
|
-
* @returns {string}
|
|
49
|
-
*/
|
|
50
|
-
export function renderMarkdownLine(line) {
|
|
51
|
-
// H1
|
|
52
|
-
if (/^# /.test(line)) {
|
|
53
|
-
const text = line.replace(/^# /, '');
|
|
54
|
-
return `{bold}{${COLORS.h1Fg}-fg}${text}{/${COLORS.h1Fg}-fg}{/bold}`;
|
|
55
|
-
}
|
|
56
|
-
// H2
|
|
57
|
-
if (/^## /.test(line)) {
|
|
58
|
-
const text = line.replace(/^## /, '');
|
|
59
|
-
return `{bold}{${COLORS.h2Fg}-fg}${text}{/${COLORS.h2Fg}-fg}{/bold}`;
|
|
60
|
-
}
|
|
61
|
-
// H3
|
|
62
|
-
if (/^### /.test(line)) {
|
|
63
|
-
const text = line.replace(/^### /, '');
|
|
64
|
-
return `{${COLORS.h3Fg}-fg}${text}{/${COLORS.h3Fg}-fg}`;
|
|
65
|
-
}
|
|
66
|
-
// Blockquote
|
|
67
|
-
if (/^> /.test(line)) {
|
|
68
|
-
const text = line.replace(/^> /, '');
|
|
69
|
-
return `{${COLORS.quoteFg}-fg}│ ${text}{/${COLORS.quoteFg}-fg}`;
|
|
70
|
-
}
|
|
71
|
-
// Horizontal rule
|
|
72
|
-
if (/^---+$/.test(line.trim())) {
|
|
73
|
-
return `{${COLORS.quoteFg}-fg}${'─'.repeat(66)}{/${COLORS.quoteFg}-fg}`;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Inline: code spans, bold
|
|
77
|
-
let result = line;
|
|
78
|
-
// Escape existing blessed tags (literal braces in content)
|
|
79
|
-
// Code spans: `text`
|
|
80
|
-
result = result.replace(/`([^`]+)`/g, `{${COLORS.codeFg}-fg}$1{/${COLORS.codeFg}-fg}`);
|
|
81
|
-
// Bold: **text**
|
|
82
|
-
result = result.replace(/\*\*([^*]+)\*\*/g, `{bold}$1{/bold}`);
|
|
83
|
-
// Italic: *text*
|
|
84
|
-
result = result.replace(/\*([^*]+)\*/g, `{${COLORS.h3Fg}-fg}$1{/${COLORS.h3Fg}-fg}`);
|
|
85
|
-
|
|
86
|
-
return result;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Render full markdown content to blessed tagged multi-line string.
|
|
91
|
-
* @param {string} markdown
|
|
92
|
-
* @returns {string}
|
|
93
|
-
*/
|
|
94
|
-
export function renderMarkdown(markdown) {
|
|
95
|
-
return markdown
|
|
96
|
-
.split('\n')
|
|
97
|
-
.map(line => renderMarkdownLine(line))
|
|
98
|
-
.join('\n');
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// ---------------------------------------------------------------------------
|
|
102
|
-
|
|
103
|
-
function createTestStub() {
|
|
104
|
-
return {
|
|
105
|
-
box: {},
|
|
106
|
-
show: () => {},
|
|
107
|
-
hide: () => {},
|
|
108
|
-
onFocus: () => {},
|
|
109
|
-
onBlur: () => {},
|
|
110
|
-
getFooterText: () => _FOOTER_TEXT_EN,
|
|
111
|
-
getFooterColor: () => COLORS.footerBg,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// ---------------------------------------------------------------------------
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Create the Readme tab component.
|
|
119
|
-
*
|
|
120
|
-
* @param {object} screen - Blessed screen instance
|
|
121
|
-
* @param {object} services
|
|
122
|
-
* @returns {{ box, show, hide, onFocus, onBlur, getFooterText, getFooterColor }}
|
|
123
|
-
*/
|
|
124
|
-
export function createReadmeTab(screen, services) {
|
|
125
|
-
if (IS_TEST) return createTestStub();
|
|
126
|
-
|
|
127
|
-
const { focusMainTabBar, languageService } = services;
|
|
128
|
-
const _tl = (key) => languageService ? languageService.t(key) : t('en', key);
|
|
129
|
-
|
|
130
|
-
// -------------------------------------------------------------------------
|
|
131
|
-
// Container
|
|
132
|
-
|
|
133
|
-
const box = blessed.box({
|
|
134
|
-
parent: screen,
|
|
135
|
-
top: 5,
|
|
136
|
-
left: 0,
|
|
137
|
-
width: '100%',
|
|
138
|
-
bottom: 2,
|
|
139
|
-
hidden: true,
|
|
140
|
-
style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
|
|
141
|
-
border: { type: 'line' },
|
|
142
|
-
borderStyle: { fg: COLORS.borderFg },
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
// -------------------------------------------------------------------------
|
|
146
|
-
// Load README.md
|
|
147
|
-
|
|
148
|
-
function _loadReadme() {
|
|
149
|
-
// Package root — works whether installed globally (node_modules/.bin) or run from source
|
|
150
|
-
const pkgRoot = path.resolve(new URL(import.meta.url).pathname, '..', '..', '..', '..');
|
|
151
|
-
const candidates = [
|
|
152
|
-
path.resolve(process.cwd(), 'README.md'),
|
|
153
|
-
path.resolve(process.cwd(), 'readme.md'),
|
|
154
|
-
path.resolve(pkgRoot, 'README.md'), // AgentVibes package README fallback
|
|
155
|
-
];
|
|
156
|
-
for (const p of candidates) {
|
|
157
|
-
if (fs.existsSync(p)) {
|
|
158
|
-
try {
|
|
159
|
-
return fs.readFileSync(p, 'utf8');
|
|
160
|
-
} catch {
|
|
161
|
-
// Unreadable
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
return `# README\n\n${_tl('readmeNotFound')}`;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// -------------------------------------------------------------------------
|
|
169
|
-
// Scrollable content
|
|
170
|
-
|
|
171
|
-
const scrollBox = blessed.box({
|
|
172
|
-
parent: box,
|
|
173
|
-
top: 1,
|
|
174
|
-
left: 2,
|
|
175
|
-
width: '96%',
|
|
176
|
-
bottom: 4,
|
|
177
|
-
scrollable: true,
|
|
178
|
-
alwaysScroll: true,
|
|
179
|
-
tags: true,
|
|
180
|
-
keys: true,
|
|
181
|
-
vi: true,
|
|
182
|
-
mouse: true,
|
|
183
|
-
scrollbar: { ch: '│', style: { fg: COLORS.borderFg } },
|
|
184
|
-
content: '',
|
|
185
|
-
style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
// Scroll indicator
|
|
189
|
-
const scrollIndicator = blessed.text({
|
|
190
|
-
parent: box,
|
|
191
|
-
bottom: 2,
|
|
192
|
-
right: 2,
|
|
193
|
-
content: '',
|
|
194
|
-
tags: true,
|
|
195
|
-
style: { fg: COLORS.quoteFg, bg: COLORS.contentBg },
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
// -------------------------------------------------------------------------
|
|
199
|
-
// Render
|
|
200
|
-
|
|
201
|
-
function refreshContent() {
|
|
202
|
-
const markdown = _loadReadme();
|
|
203
|
-
const rendered = renderMarkdown(markdown);
|
|
204
|
-
scrollBox.setContent(rendered);
|
|
205
|
-
scrollIndicator.setContent(`{#607d8b-fg}${_tl('readmeScrollMore')}{/#607d8b-fg}`);
|
|
206
|
-
screen.render();
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Scroll events update indicator
|
|
210
|
-
scrollBox.on('scroll', () => {
|
|
211
|
-
const atBottom = scrollBox.getScrollPerc() >= 99;
|
|
212
|
-
scrollIndicator.setContent(
|
|
213
|
-
atBottom ? '' : `{#607d8b-fg}${_tl('readmeScrollMore')}{/#607d8b-fg}`
|
|
214
|
-
);
|
|
215
|
-
screen.render();
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
// PgUp/PgDn
|
|
219
|
-
scrollBox.key(['pageup'], () => {
|
|
220
|
-
scrollBox.scroll(-10);
|
|
221
|
-
screen.render();
|
|
222
|
-
});
|
|
223
|
-
scrollBox.key(['pagedown'], () => {
|
|
224
|
-
scrollBox.scroll(10);
|
|
225
|
-
screen.render();
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
// [↑] at top of content → jump to main header tab bar
|
|
229
|
-
scrollBox.key(['up'], () => {
|
|
230
|
-
if (scrollBox.getScroll() === 0 && typeof focusMainTabBar === 'function') {
|
|
231
|
-
focusMainTabBar();
|
|
232
|
-
}
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
// Escape → return to header tab bar
|
|
236
|
-
scrollBox.key(['escape'], () => {
|
|
237
|
-
if (typeof focusMainTabBar === 'function') { focusMainTabBar(); screen.render(); }
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
// -------------------------------------------------------------------------
|
|
241
|
-
// Tab Component Contract
|
|
242
|
-
|
|
243
|
-
return {
|
|
244
|
-
box,
|
|
245
|
-
|
|
246
|
-
show() {
|
|
247
|
-
box.show();
|
|
248
|
-
refreshContent();
|
|
249
|
-
screen.render();
|
|
250
|
-
},
|
|
251
|
-
|
|
252
|
-
hide() {
|
|
253
|
-
box.hide();
|
|
254
|
-
screen.render();
|
|
255
|
-
},
|
|
256
|
-
|
|
257
|
-
onFocus() {
|
|
258
|
-
scrollBox.focus();
|
|
259
|
-
screen.render();
|
|
260
|
-
},
|
|
261
|
-
|
|
262
|
-
onBlur() {},
|
|
263
|
-
|
|
264
|
-
getFooterText() {
|
|
265
|
-
return _tl('readmeFooter');
|
|
266
|
-
},
|
|
267
|
-
|
|
268
|
-
getFooterColor() {
|
|
269
|
-
return COLORS.footerBg;
|
|
270
|
-
},
|
|
271
|
-
};
|
|
272
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* AgentVibes TUI Console — Readme Tab
|
|
3
|
+
* Epic 13: Story 13.2
|
|
4
|
+
*
|
|
5
|
+
* Implements the Tab Component Contract:
|
|
6
|
+
* createReadmeTab(screen, services) → { box, show, hide, onFocus, onBlur, getFooterText, getFooterColor }
|
|
7
|
+
*
|
|
8
|
+
* Features: renders README.md with styled markdown, scrolling, search.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { t } from '../../i18n/strings.js';
|
|
14
|
+
|
|
15
|
+
const IS_TEST = process.env.AGENTVIBES_TEST_MODE === 'true';
|
|
16
|
+
|
|
17
|
+
let blessed;
|
|
18
|
+
if (!IS_TEST) {
|
|
19
|
+
const { default: b } = await import('blessed');
|
|
20
|
+
blessed = b;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
const COLORS = {
|
|
26
|
+
contentBg: '#0a0e1a',
|
|
27
|
+
h1Fg: '#69f0ae', // Green — H1
|
|
28
|
+
h2Fg: '#82b1ff', // Blue — H2
|
|
29
|
+
h3Fg: '#80d8ff', // Light blue — H3
|
|
30
|
+
codeFg: '#ffff00', // Yellow — code spans
|
|
31
|
+
boldFg: '#e3f2fd', // Bright — bold text
|
|
32
|
+
quoteFg: '#90a4ae', // Gray — blockquotes
|
|
33
|
+
labelFg: '#e3f2fd',
|
|
34
|
+
borderFg: '#455a64',
|
|
35
|
+
footerBg: '#455a64', // Dark gray — Readme tab footer
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const _FOOTER_TEXT_EN = '[↑↓/jk] Scroll [PgUp/PgDn] Page [/] Search [S/V/M/A/R] Tab [Q] Quit';
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Markdown renderer (story 13.2)
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Render a single markdown line to blessed tagged string.
|
|
45
|
+
* Handles: H1/H2/H3, bold (**), code spans (`), blockquotes (>), plain text.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} line
|
|
48
|
+
* @returns {string}
|
|
49
|
+
*/
|
|
50
|
+
export function renderMarkdownLine(line) {
|
|
51
|
+
// H1
|
|
52
|
+
if (/^# /.test(line)) {
|
|
53
|
+
const text = line.replace(/^# /, '');
|
|
54
|
+
return `{bold}{${COLORS.h1Fg}-fg}${text}{/${COLORS.h1Fg}-fg}{/bold}`;
|
|
55
|
+
}
|
|
56
|
+
// H2
|
|
57
|
+
if (/^## /.test(line)) {
|
|
58
|
+
const text = line.replace(/^## /, '');
|
|
59
|
+
return `{bold}{${COLORS.h2Fg}-fg}${text}{/${COLORS.h2Fg}-fg}{/bold}`;
|
|
60
|
+
}
|
|
61
|
+
// H3
|
|
62
|
+
if (/^### /.test(line)) {
|
|
63
|
+
const text = line.replace(/^### /, '');
|
|
64
|
+
return `{${COLORS.h3Fg}-fg}${text}{/${COLORS.h3Fg}-fg}`;
|
|
65
|
+
}
|
|
66
|
+
// Blockquote
|
|
67
|
+
if (/^> /.test(line)) {
|
|
68
|
+
const text = line.replace(/^> /, '');
|
|
69
|
+
return `{${COLORS.quoteFg}-fg}│ ${text}{/${COLORS.quoteFg}-fg}`;
|
|
70
|
+
}
|
|
71
|
+
// Horizontal rule
|
|
72
|
+
if (/^---+$/.test(line.trim())) {
|
|
73
|
+
return `{${COLORS.quoteFg}-fg}${'─'.repeat(66)}{/${COLORS.quoteFg}-fg}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Inline: code spans, bold
|
|
77
|
+
let result = line;
|
|
78
|
+
// Escape existing blessed tags (literal braces in content)
|
|
79
|
+
// Code spans: `text`
|
|
80
|
+
result = result.replace(/`([^`]+)`/g, `{${COLORS.codeFg}-fg}$1{/${COLORS.codeFg}-fg}`);
|
|
81
|
+
// Bold: **text**
|
|
82
|
+
result = result.replace(/\*\*([^*]+)\*\*/g, `{bold}$1{/bold}`);
|
|
83
|
+
// Italic: *text*
|
|
84
|
+
result = result.replace(/\*([^*]+)\*/g, `{${COLORS.h3Fg}-fg}$1{/${COLORS.h3Fg}-fg}`);
|
|
85
|
+
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Render full markdown content to blessed tagged multi-line string.
|
|
91
|
+
* @param {string} markdown
|
|
92
|
+
* @returns {string}
|
|
93
|
+
*/
|
|
94
|
+
export function renderMarkdown(markdown) {
|
|
95
|
+
return markdown
|
|
96
|
+
.split('\n')
|
|
97
|
+
.map(line => renderMarkdownLine(line))
|
|
98
|
+
.join('\n');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
function createTestStub() {
|
|
104
|
+
return {
|
|
105
|
+
box: {},
|
|
106
|
+
show: () => {},
|
|
107
|
+
hide: () => {},
|
|
108
|
+
onFocus: () => {},
|
|
109
|
+
onBlur: () => {},
|
|
110
|
+
getFooterText: () => _FOOTER_TEXT_EN,
|
|
111
|
+
getFooterColor: () => COLORS.footerBg,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Create the Readme tab component.
|
|
119
|
+
*
|
|
120
|
+
* @param {object} screen - Blessed screen instance
|
|
121
|
+
* @param {object} services
|
|
122
|
+
* @returns {{ box, show, hide, onFocus, onBlur, getFooterText, getFooterColor }}
|
|
123
|
+
*/
|
|
124
|
+
export function createReadmeTab(screen, services) {
|
|
125
|
+
if (IS_TEST) return createTestStub();
|
|
126
|
+
|
|
127
|
+
const { focusMainTabBar, languageService } = services;
|
|
128
|
+
const _tl = (key) => languageService ? languageService.t(key) : t('en', key);
|
|
129
|
+
|
|
130
|
+
// -------------------------------------------------------------------------
|
|
131
|
+
// Container
|
|
132
|
+
|
|
133
|
+
const box = blessed.box({
|
|
134
|
+
parent: screen,
|
|
135
|
+
top: 5,
|
|
136
|
+
left: 0,
|
|
137
|
+
width: '100%',
|
|
138
|
+
bottom: 2,
|
|
139
|
+
hidden: true,
|
|
140
|
+
style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
|
|
141
|
+
border: { type: 'line' },
|
|
142
|
+
borderStyle: { fg: COLORS.borderFg },
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// -------------------------------------------------------------------------
|
|
146
|
+
// Load README.md
|
|
147
|
+
|
|
148
|
+
function _loadReadme() {
|
|
149
|
+
// Package root — works whether installed globally (node_modules/.bin) or run from source
|
|
150
|
+
const pkgRoot = path.resolve(new URL(import.meta.url).pathname, '..', '..', '..', '..');
|
|
151
|
+
const candidates = [
|
|
152
|
+
path.resolve(process.cwd(), 'README.md'),
|
|
153
|
+
path.resolve(process.cwd(), 'readme.md'),
|
|
154
|
+
path.resolve(pkgRoot, 'README.md'), // AgentVibes package README fallback
|
|
155
|
+
];
|
|
156
|
+
for (const p of candidates) {
|
|
157
|
+
if (fs.existsSync(p)) {
|
|
158
|
+
try {
|
|
159
|
+
return fs.readFileSync(p, 'utf8');
|
|
160
|
+
} catch {
|
|
161
|
+
// Unreadable
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return `# README\n\n${_tl('readmeNotFound')}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// -------------------------------------------------------------------------
|
|
169
|
+
// Scrollable content
|
|
170
|
+
|
|
171
|
+
const scrollBox = blessed.box({
|
|
172
|
+
parent: box,
|
|
173
|
+
top: 1,
|
|
174
|
+
left: 2,
|
|
175
|
+
width: '96%',
|
|
176
|
+
bottom: 4,
|
|
177
|
+
scrollable: true,
|
|
178
|
+
alwaysScroll: true,
|
|
179
|
+
tags: true,
|
|
180
|
+
keys: true,
|
|
181
|
+
vi: true,
|
|
182
|
+
mouse: true,
|
|
183
|
+
scrollbar: { ch: '│', style: { fg: COLORS.borderFg } },
|
|
184
|
+
content: '',
|
|
185
|
+
style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Scroll indicator
|
|
189
|
+
const scrollIndicator = blessed.text({
|
|
190
|
+
parent: box,
|
|
191
|
+
bottom: 2,
|
|
192
|
+
right: 2,
|
|
193
|
+
content: '',
|
|
194
|
+
tags: true,
|
|
195
|
+
style: { fg: COLORS.quoteFg, bg: COLORS.contentBg },
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// -------------------------------------------------------------------------
|
|
199
|
+
// Render
|
|
200
|
+
|
|
201
|
+
function refreshContent() {
|
|
202
|
+
const markdown = _loadReadme();
|
|
203
|
+
const rendered = renderMarkdown(markdown);
|
|
204
|
+
scrollBox.setContent(rendered);
|
|
205
|
+
scrollIndicator.setContent(`{#607d8b-fg}${_tl('readmeScrollMore')}{/#607d8b-fg}`);
|
|
206
|
+
screen.render();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Scroll events update indicator
|
|
210
|
+
scrollBox.on('scroll', () => {
|
|
211
|
+
const atBottom = scrollBox.getScrollPerc() >= 99;
|
|
212
|
+
scrollIndicator.setContent(
|
|
213
|
+
atBottom ? '' : `{#607d8b-fg}${_tl('readmeScrollMore')}{/#607d8b-fg}`
|
|
214
|
+
);
|
|
215
|
+
screen.render();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// PgUp/PgDn
|
|
219
|
+
scrollBox.key(['pageup'], () => {
|
|
220
|
+
scrollBox.scroll(-10);
|
|
221
|
+
screen.render();
|
|
222
|
+
});
|
|
223
|
+
scrollBox.key(['pagedown'], () => {
|
|
224
|
+
scrollBox.scroll(10);
|
|
225
|
+
screen.render();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// [↑] at top of content → jump to main header tab bar
|
|
229
|
+
scrollBox.key(['up'], () => {
|
|
230
|
+
if (scrollBox.getScroll() === 0 && typeof focusMainTabBar === 'function') {
|
|
231
|
+
focusMainTabBar();
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Escape → return to header tab bar
|
|
236
|
+
scrollBox.key(['escape'], () => {
|
|
237
|
+
if (typeof focusMainTabBar === 'function') { focusMainTabBar(); screen.render(); }
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// -------------------------------------------------------------------------
|
|
241
|
+
// Tab Component Contract
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
box,
|
|
245
|
+
|
|
246
|
+
show() {
|
|
247
|
+
box.show();
|
|
248
|
+
refreshContent();
|
|
249
|
+
screen.render();
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
hide() {
|
|
253
|
+
box.hide();
|
|
254
|
+
screen.render();
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
onFocus() {
|
|
258
|
+
scrollBox.focus();
|
|
259
|
+
screen.render();
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
onBlur() {},
|
|
263
|
+
|
|
264
|
+
getFooterText() {
|
|
265
|
+
return _tl('readmeFooter');
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
getFooterColor() {
|
|
269
|
+
return COLORS.footerBg;
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
}
|