agentvibes 3.5.9 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/.agentvibes/bmad/bmad-voices-enabled.flag +0 -0
  2. package/.agentvibes/bmad/bmad-voices.md +69 -0
  3. package/.claude/config/audio-effects.cfg +1 -1
  4. package/.claude/config/background-music-position.txt +1 -27
  5. package/.claude/github-star-reminder.txt +1 -1
  6. package/.claude/hooks/audio-processor.sh +32 -17
  7. package/.claude/hooks/bmad-speak-enhanced.sh +5 -5
  8. package/.claude/hooks/bmad-speak.sh +4 -4
  9. package/.claude/hooks/bmad-voice-manager.sh +8 -8
  10. package/.claude/hooks/clawdbot-receiver-SECURE.sh +23 -25
  11. package/.claude/hooks/clawdbot-receiver.sh +28 -4
  12. package/.claude/hooks/language-manager.sh +1 -1
  13. package/.claude/hooks/path-resolver.sh +60 -0
  14. package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +90 -0
  15. package/.claude/hooks/play-tts-piper.sh +82 -24
  16. package/.claude/hooks/play-tts-ssh-remote.sh +13 -15
  17. package/.claude/hooks/play-tts.sh +16 -5
  18. package/.claude/hooks/session-start-tts.sh +26 -56
  19. package/.claude/hooks/soprano-gradio-synth.py +1 -1
  20. package/.claude/hooks/verbosity-manager.sh +10 -4
  21. package/.claude/settings.json +1 -1
  22. package/CLAUDE.md +129 -104
  23. package/README.md +418 -10
  24. package/RELEASE_NOTES.md +60 -1036
  25. package/bin/agentvibes-voice-browser.js +1827 -0
  26. package/bin/agentvibes.js +100 -0
  27. package/mcp-server/server.py +67 -3
  28. package/package.json +11 -2
  29. package/src/console/app.js +806 -0
  30. package/src/console/audio-env.js +123 -0
  31. package/src/console/brand-colors.js +13 -0
  32. package/src/console/footer-config.js +42 -0
  33. package/src/console/modals/.gitkeep +0 -0
  34. package/src/console/modals/modal-overlay.js +247 -0
  35. package/src/console/navigation.js +60 -0
  36. package/src/console/tabs/.gitkeep +0 -0
  37. package/src/console/tabs/agents-tab.js +369 -0
  38. package/src/console/tabs/help-tab.js +261 -0
  39. package/src/console/tabs/install-tab.js +990 -0
  40. package/src/console/tabs/music-tab.js +997 -0
  41. package/src/console/tabs/placeholder-tab.js +45 -0
  42. package/src/console/tabs/readme-tab.js +267 -0
  43. package/src/console/tabs/settings-tab.js +3949 -0
  44. package/src/console/tabs/voices-tab.js +1574 -0
  45. package/src/installer/music-file-input.js +304 -0
  46. package/src/installer.js +1353 -676
  47. package/src/services/.gitkeep +0 -0
  48. package/src/services/agent-voice-store.js +163 -0
  49. package/src/services/config-service.js +240 -0
  50. package/src/services/navigation-service.js +123 -0
  51. package/src/services/provider-service.js +132 -0
  52. package/src/services/verbosity-service.js +157 -0
  53. package/src/utils/audio-duration-validator.js +298 -0
  54. package/src/utils/audio-format-validator.js +277 -0
  55. package/src/utils/dependency-checker.js +3 -3
  56. package/src/utils/file-ownership-verifier.js +358 -0
  57. package/src/utils/music-file-validator.js +275 -0
  58. package/src/utils/preview-list-prompt.js +136 -0
  59. package/src/utils/provider-validator.js +144 -132
  60. package/src/utils/secure-music-storage.js +412 -0
  61. package/templates/agentvibes-receiver.sh +11 -7
  62. package/voice-assignments.json +8245 -0
  63. package/.claude/config/background-music-volume.txt +0 -1
  64. package/.claude/config/background-music.cfg +0 -1
  65. package/.claude/config/background-music.txt +0 -1
  66. package/.claude/config/tts-speech-rate.txt +0 -1
  67. package/.claude/config/tts-verbosity.txt +0 -1
  68. package/.claude/hooks/bmad-party-manager.sh +0 -225
  69. package/.claude/hooks/stop.sh +0 -38
  70. package/.claude/piper-voices-dir.txt +0 -1
  71. package/.mcp.json +0 -34
@@ -0,0 +1,123 @@
1
+ /**
2
+ * AgentVibes — Cross-platform audio environment helpers.
3
+ *
4
+ * Provides PULSE_SERVER-safe environment and MP3/WAV player detection
5
+ * that works on native Linux, WSL2, macOS, and Windows.
6
+ */
7
+
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import os from 'node:os';
11
+ import { spawnSync } from 'node:child_process';
12
+
13
+ /**
14
+ * Build a spawn environment with correct PULSE_SERVER handling.
15
+ *
16
+ * - If PULSE_SERVER is already set in the environment, keep it.
17
+ * - On WSL2, set it to the wslg PulseServer socket if it exists.
18
+ * - On native Linux / macOS / Windows, do NOT set it (use system default).
19
+ *
20
+ * Also extends PATH so pipx-installed binaries (piper) are found.
21
+ *
22
+ * @returns {Object} Environment object safe to pass to child_process.spawn
23
+ */
24
+ export function buildAudioEnv() {
25
+ const env = {
26
+ ...process.env,
27
+ PATH: [process.env.PATH, path.join(os.homedir(), '.local', 'bin'), '/usr/local/bin']
28
+ .filter(Boolean).join(process.platform === 'win32' ? ';' : ':'),
29
+ };
30
+
31
+ if (process.env.PULSE_SERVER) {
32
+ env.PULSE_SERVER = process.env.PULSE_SERVER;
33
+ } else if (fs.existsSync('/mnt/wslg/PulseServer')) {
34
+ env.PULSE_SERVER = 'unix:/mnt/wslg/PulseServer';
35
+ }
36
+ // else: leave PULSE_SERVER unset — native PulseAudio/PipeWire uses its default socket
37
+
38
+ return env;
39
+ }
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Player detection
43
+
44
+ /** @typedef {{ bin: string, args: (file: string) => string[] }} Player */
45
+
46
+ /** MP3-capable players in preference order */
47
+ const MP3_PLAYERS = [
48
+ { bin: 'ffplay', args: (f) => ['-nodisp', '-autoexit', '-loglevel', 'quiet', f] },
49
+ { bin: 'play', args: (f) => [f] }, // sox
50
+ { bin: 'mpg123', args: (f) => ['-q', f] },
51
+ { bin: 'cvlc', args: (f) => ['--play-and-exit', '--no-video', f] },
52
+ { bin: 'mpv', args: (f) => ['--no-video', '--really-quiet', f] },
53
+ { bin: 'afplay', args: (f) => [f] }, // macOS
54
+ ];
55
+
56
+ /** WAV-capable players in preference order */
57
+ const WAV_PLAYERS = [
58
+ { bin: 'aplay', args: (f) => [f] }, // ALSA (Linux)
59
+ { bin: 'paplay', args: (f) => [f] }, // PulseAudio
60
+ { bin: 'play', args: (f) => [f] }, // sox
61
+ { bin: 'ffplay', args: (f) => ['-nodisp', '-autoexit', '-loglevel', 'quiet', f] },
62
+ { bin: 'afplay', args: (f) => [f] }, // macOS
63
+ { bin: 'mpv', args: (f) => ['--no-video', '--really-quiet', f] },
64
+ { bin: 'cvlc', args: (f) => ['--play-and-exit', '--no-video', f] },
65
+ ];
66
+
67
+ /** Windows players — use PowerShell's SoundPlayer for WAV */
68
+ const WIN_WAV_PLAYER = {
69
+ bin: 'powershell',
70
+ args: (f) => ['-NoProfile', '-Command',
71
+ `(New-Object Media.SoundPlayer '${f.replace(/'/g, "''")}').PlaySync()`],
72
+ };
73
+
74
+ /**
75
+ * Detect the first available player from a list.
76
+ * Caches results per list so `which` is only called once per binary.
77
+ *
78
+ * @param {Player[]} players
79
+ * @param {Object} env - Environment to use for `which`
80
+ * @returns {Player|null}
81
+ */
82
+ const _cache = new Map();
83
+ function _detect(players, env) {
84
+ for (const p of players) {
85
+ if (_cache.has(p.bin)) {
86
+ if (_cache.get(p.bin)) return p;
87
+ continue;
88
+ }
89
+ const r = spawnSync('which', [p.bin], { stdio: 'pipe', env });
90
+ const found = r.status === 0;
91
+ _cache.set(p.bin, found);
92
+ if (found) return p;
93
+ }
94
+ return null;
95
+ }
96
+
97
+ /**
98
+ * Detect the best available MP3 player.
99
+ * On Windows, falls back to ffplay/mpv if installed, otherwise null.
100
+ *
101
+ * @param {Object} [env] - Environment (defaults to buildAudioEnv())
102
+ * @returns {Player|null}
103
+ */
104
+ export function detectMp3Player(env) {
105
+ env = env || buildAudioEnv();
106
+ return _detect(MP3_PLAYERS, env);
107
+ }
108
+
109
+ /**
110
+ * Detect the best available WAV player.
111
+ * On Windows, uses PowerShell SoundPlayer as built-in fallback.
112
+ *
113
+ * @param {Object} [env] - Environment (defaults to buildAudioEnv())
114
+ * @returns {Player|null}
115
+ */
116
+ export function detectWavPlayer(env) {
117
+ env = env || buildAudioEnv();
118
+ if (process.platform === 'win32') {
119
+ // Try cross-platform players first, fall back to PowerShell
120
+ return _detect(WAV_PLAYERS, env) || WIN_WAV_PLAYER;
121
+ }
122
+ return _detect(WAV_PLAYERS, env);
123
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * AgentVibes TUI — Brand Color Constants
3
+ *
4
+ * Single source of truth for the two primary brand colors.
5
+ * Change BRAND_PINK or BRAND_BLUE here to update every modal title,
6
+ * button, and the "Vibes" logotype across the entire TUI.
7
+ */
8
+
9
+ /** Magenta-pink used for modal titles and the "Vibes" logotype. */
10
+ export const BRAND_PINK = '#f06292'; // Light magenta — Pink 300
11
+
12
+ /** Indigo blue used for default button backgrounds and primary accents. */
13
+ export const BRAND_BLUE = '#3949ab';
@@ -0,0 +1,42 @@
1
+ /**
2
+ * AgentVibes TUI Console — Context Footer Configuration
3
+ * Story 6.3: Color-Coded Context Footer System
4
+ *
5
+ * Per-tab footer colors and keyboard shortcut hint text.
6
+ * Keys are styled in magenta ({#ff00ff-fg}{bold}); actions follow in plain text.
7
+ */
8
+
9
+ /** Fallback footer background color */
10
+ export const DEFAULT_FOOTER_COLOR = '#1a237e';
11
+
12
+ /** Helper: wrap a key label in magenta bold tags */
13
+ function key(label) {
14
+ return `{#ff00ff-fg}{bold}[${label}]{/bold}{/#ff00ff-fg}`;
15
+ }
16
+
17
+ export const FOOTER_CONFIG = {
18
+ settings: {
19
+ color: '#2196f3',
20
+ text: ` ${key('↑↓')} Navigate ${key('←→')} Same Row ${key('Enter')} Activate ${key('Space')} Preview ${key('Esc')} Cancel`,
21
+ },
22
+ voices: {
23
+ color: '#00bcd4',
24
+ text: ` ${key('1-6')} Sort ${key('/')} Search ${key('P')} Provider ${key('F')} Favorites ${key('Space')} Preview ${key('*')} Fav ${key('I')} Install`,
25
+ },
26
+ music: {
27
+ color: '#ff9800',
28
+ text: ` ${key('Space')} Preview ${key('Enter')} Select ${key('M')} Toggle ${key('*')} Fav ${key('F')} Filter ${key('↑↓')} Navigate`,
29
+ },
30
+ readme: {
31
+ color: '#455a64',
32
+ text: ` ${key('↑↓')} Scroll ${key('PgUp/PgDn')} Page ${key('Home/End')} Jump`,
33
+ },
34
+ help: {
35
+ color: '#607d8b',
36
+ text: ` ${key('↑↓')} Scroll ${key('Q')} Quit`,
37
+ },
38
+ install: {
39
+ color: '#1a237e',
40
+ text: ` ${key('↑↓')} Navigate ${key('Enter')} Select ${key('Esc')} Back`,
41
+ },
42
+ };
File without changes
@@ -0,0 +1,247 @@
1
+ /**
2
+ * AgentVibes TUI Console — Modal Overlay Component
3
+ * Story 6.4: Modal Overlay Component
4
+ *
5
+ * Provides a reusable dim-overlay + centered-container modal for the TUI.
6
+ * Used by stories 7.7 (Personality), 7.8 (Voice), 7.9 (Music) selector modals.
7
+ *
8
+ * Integration with NavigationService:
9
+ * - openModal() sets isModalOpen() = true (blocks S/V/M/A/R/H/I/T shortcuts)
10
+ * - closeModal() restores modal-closed state
11
+ * - pushFocus/popFocus manage keyboard focus across open/close
12
+ */
13
+
14
+ import blessed from 'blessed';
15
+
16
+ // Brand colours consistent with app.js and UX design plan
17
+ const COLORS = {
18
+ headerBg: '#1a237e',
19
+ contentBg: '#0a0e1a',
20
+ activeTab: '#3949ab',
21
+ textWhite: 'white',
22
+ overlayBg: '#000000',
23
+ };
24
+
25
+ export class ModalOverlay {
26
+ /**
27
+ * @param {object} screen - Blessed screen instance (or stub in tests)
28
+ * @param {NavigationService} nav - Navigation service for modal state management
29
+ * @param {object} [opts={}]
30
+ * @param {string} [opts.title='Modal'] - Default modal title
31
+ */
32
+ constructor(screen, nav, opts = {}) {
33
+ this._screen = screen;
34
+ this._nav = nav;
35
+ this._onApply = null;
36
+ this._focusedButton = 'apply'; // M1: tracks which footer button has focus ('apply'|'cancel')
37
+
38
+ const title = opts.title ?? 'Modal';
39
+ this._title = title; // L1: stored for external access
40
+
41
+ // Detect test/stub environment: real blessed screens have a .program property.
42
+ // When using a stub screen (no .program), create lightweight object stubs
43
+ // instead of real blessed widgets to avoid "No active screen." errors.
44
+ const isTestMode = !screen.program;
45
+
46
+ if (isTestMode) {
47
+ this._overlay = { hidden: true };
48
+ this._container = { hidden: true, key: () => {}, focus: () => {} };
49
+ this._titleBar = { setContent: () => {} };
50
+ this._contentBox = { hidden: true };
51
+ this._footerBar = { hidden: true, setContent: () => {} }; // setContent needed by _updateFooter
52
+ } else {
53
+ // Full-screen dim overlay — sits beneath the modal container
54
+ this._overlay = blessed.box({
55
+ top: 0,
56
+ left: 0,
57
+ width: '100%',
58
+ height: '100%',
59
+ style: { bg: COLORS.overlayBg },
60
+ hidden: true,
61
+ });
62
+
63
+ // Centered modal container with border
64
+ this._container = blessed.box({
65
+ top: 'center',
66
+ left: 'center',
67
+ width: '60%',
68
+ height: '60%',
69
+ border: { type: 'line' },
70
+ style: {
71
+ fg: COLORS.textWhite,
72
+ bg: COLORS.contentBg,
73
+ border: { fg: COLORS.activeTab },
74
+ },
75
+ hidden: true,
76
+ keys: true,
77
+ mouse: true,
78
+ });
79
+
80
+ // Title bar — top row of container
81
+ this._titleBar = blessed.box({
82
+ parent: this._container,
83
+ top: 0,
84
+ left: 0,
85
+ width: '100%',
86
+ height: 1,
87
+ content: ` ${title}`,
88
+ tags: true,
89
+ style: {
90
+ fg: COLORS.textWhite,
91
+ bg: COLORS.headerBg,
92
+ },
93
+ });
94
+
95
+ // Scrollable content area — callers append their UI here via getContentBox()
96
+ this._contentBox = blessed.box({
97
+ parent: this._container,
98
+ top: 1,
99
+ left: 0,
100
+ width: '100%',
101
+ bottom: 2,
102
+ scrollable: true,
103
+ alwaysScroll: true,
104
+ keys: true,
105
+ style: {
106
+ fg: COLORS.textWhite,
107
+ bg: COLORS.contentBg,
108
+ },
109
+ });
110
+
111
+ // Footer strip with focusable Apply / Cancel buttons
112
+ this._footerBar = blessed.box({
113
+ parent: this._container,
114
+ bottom: 0,
115
+ left: 0,
116
+ width: '100%',
117
+ height: 2,
118
+ content: ' ► {bold}[Apply]{/bold} [Cancel]',
119
+ tags: true,
120
+ style: {
121
+ fg: COLORS.textWhite,
122
+ bg: COLORS.headerBg,
123
+ },
124
+ });
125
+
126
+ // Append to screen in z-order: overlay first (bottom), container on top
127
+ this._screen.append(this._overlay);
128
+ this._screen.append(this._container);
129
+
130
+ // M1: Tab cycles focus indicator between Apply and Cancel
131
+ this._container.key(['tab'], () => {
132
+ this._focusedButton = this._focusedButton === 'apply' ? 'cancel' : 'apply';
133
+ this._updateFooter();
134
+ this._screen.render();
135
+ });
136
+
137
+ // Enter activates the currently focused footer button (M1: checks _focusedButton)
138
+ this._container.key(['enter'], () => {
139
+ if (this._focusedButton === 'apply') {
140
+ this._handleApply();
141
+ } else {
142
+ this.close();
143
+ }
144
+ });
145
+
146
+ this._container.key(['escape'], () => this.close());
147
+ }
148
+ }
149
+
150
+ // ---------------------------------------------------------------------------
151
+ // Public API
152
+
153
+ /**
154
+ * Open the modal overlay.
155
+ * Shows the dim overlay + container, updates NavigationService modal state,
156
+ * pushes current focus to the focus stack, and renders the screen.
157
+ * @param {Function|null} [onApply] - Called when user activates Apply
158
+ */
159
+ open(onApply = null) {
160
+ // H2: Guard against double-open — prevents stacked pushFocus calls
161
+ if (this._nav.isModalOpen()) return;
162
+
163
+ this._onApply = onApply;
164
+ this._focusedButton = 'apply'; // Reset focus to Apply on each open
165
+ this._overlay.hidden = false;
166
+ this._container.hidden = false;
167
+
168
+ this._nav.openModal(() => {
169
+ // Push current focus so we can restore on close
170
+ this._nav.pushFocus(this._screen.focused);
171
+ this._container.focus();
172
+ });
173
+
174
+ this._updateFooter();
175
+ this._screen.render();
176
+ }
177
+
178
+ /**
179
+ * Close the modal overlay.
180
+ * Hides overlay + container, restores NavigationService modal state,
181
+ * pops saved focus, and renders the screen.
182
+ *
183
+ * Guard: uses widget-state (overlay.hidden) rather than isModalOpen().
184
+ * Reason: Blessed.js fires multiple Esc handlers in registration order.
185
+ * navigation.js registers its handler first and calls nav.closeModal()
186
+ * before app.js calls modal.close() — so isModalOpen() is already false
187
+ * when we arrive here, causing the old guard to return early and leave
188
+ * the overlay/container visible on screen.
189
+ */
190
+ close() {
191
+ // H1: Widget-state guard — safe against dual-handler Esc ordering
192
+ if (this._overlay.hidden) return;
193
+ this._overlay.hidden = true;
194
+ this._container.hidden = true;
195
+ // H1: navigation.js may have already called closeModal() — only call if still open
196
+ if (this._nav.isModalOpen()) this._nav.closeModal();
197
+ const prev = this._nav.popFocus();
198
+ if (prev && typeof prev.focus === 'function') prev.focus();
199
+ this._screen.render();
200
+ }
201
+
202
+ /**
203
+ * Returns the inner content box so callers (7.7-7.9) can append their UI.
204
+ * @returns {object} Blessed box (or stub in tests)
205
+ */
206
+ getContentBox() {
207
+ return this._contentBox;
208
+ }
209
+
210
+ /**
211
+ * Update the modal title bar text.
212
+ * @param {string} text
213
+ */
214
+ setTitle(text) {
215
+ this._titleBar.setContent(` ${text}`);
216
+ }
217
+
218
+ // ---------------------------------------------------------------------------
219
+ // Private
220
+
221
+ /**
222
+ * M1: Render the footer with a ► cursor on the currently focused button.
223
+ */
224
+ _updateFooter() {
225
+ const applyLabel = this._focusedButton === 'apply' ? '► {bold}[Apply]{/bold}' : ' [Apply]';
226
+ const cancelLabel = this._focusedButton === 'cancel' ? '► {bold}[Cancel]{/bold}' : ' [Cancel]';
227
+ this._footerBar.setContent(` ${applyLabel} ${cancelLabel}`);
228
+ }
229
+
230
+ _handleApply() {
231
+ if (typeof this._onApply === 'function') {
232
+ this._onApply();
233
+ }
234
+ this.close();
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Factory function for creating a ModalOverlay.
240
+ * @param {object} screen - Blessed screen
241
+ * @param {NavigationService} nav - Navigation service
242
+ * @param {object} [opts={}]
243
+ * @returns {ModalOverlay}
244
+ */
245
+ export function createModalOverlay(screen, nav, opts = {}) {
246
+ return new ModalOverlay(screen, nav, opts);
247
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * AgentVibes TUI Console — Global Keyboard Navigation
3
+ * Story 6.2: Tab Bar & Global Keyboard Navigation
4
+ *
5
+ * Registers all global key bindings on the Blessed screen.
6
+ * Tab shortcuts (S/V/M/A/R/H/I) are blocked when a modal is open.
7
+ */
8
+
9
+ /** Map of key → tab ID for global tab shortcut keys */
10
+ const KEY_TO_TAB = {
11
+ 's': 'settings', 'S': 'settings',
12
+ 'v': 'voices', 'V': 'voices',
13
+ 'm': 'music', 'M': 'music',
14
+ 'r': 'readme', 'R': 'readme',
15
+ 'h': 'help', 'H': 'help',
16
+ 'i': 'install', 'I': 'install',
17
+ };
18
+
19
+ /**
20
+ * Register all global keyboard navigation handlers on the Blessed screen.
21
+ *
22
+ * Handlers registered:
23
+ * S/V/M/A/R/H/I → switchTab (blocked when modal is open)
24
+ * Tab / T/t → cycleTab forward (blocked when modal is open)
25
+ * Shift+Tab → cycleTab backward (blocked when modal is open)
26
+ * Escape → closeModal (only when modal is open)
27
+ *
28
+ * Arrow keys (left/right) are intentionally NOT used for tab cycling —
29
+ * individual tabs use left/right for in-element navigation (e.g. row siblings).
30
+ *
31
+ * NOTE: Q / Ctrl+C are already registered in app.js (_registerHandlers).
32
+ * Do NOT re-register them here — that would stack duplicate quit handlers.
33
+ *
34
+ * @param {object} screen - Blessed screen instance (or stub in tests)
35
+ * @param {import('../services/navigation-service.js').NavigationService} navigationService
36
+ */
37
+ export function setupNavigation(screen, navigationService) {
38
+ // Tab switching shortcuts — one handler per key (both cases)
39
+ for (const [key, tabId] of Object.entries(KEY_TO_TAB)) {
40
+ screen.key([key], () => {
41
+ if (!navigationService.isModalOpen()) {
42
+ navigationService.switchTab(tabId);
43
+ }
44
+ });
45
+ }
46
+
47
+ // T → cycle to next tab (Tab itself is handled by the tab bar and footer only)
48
+ screen.key(['t', 'T'], () => {
49
+ if (!navigationService.isModalOpen()) {
50
+ navigationService.cycleTab();
51
+ }
52
+ });
53
+
54
+ // Escape — close modal (story 6.4 will expand modal handling)
55
+ screen.key(['escape'], () => {
56
+ if (navigationService.isModalOpen()) {
57
+ navigationService.closeModal();
58
+ }
59
+ });
60
+ }
File without changes