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.
Files changed (34) hide show
  1. package/.agentvibes/config.json +23 -13
  2. package/.claude/commands/agent-vibes/verbosity.md +98 -89
  3. package/.claude/config/audio-effects.cfg +6 -1
  4. package/.claude/hooks/bmad-speak.sh +2 -2
  5. package/.claude/hooks/piper-download-voices.sh +233 -225
  6. package/.claude/hooks/piper-installer.sh +1 -1
  7. package/.claude/hooks/piper-voice-manager.sh +125 -0
  8. package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +97 -90
  9. package/.claude/hooks/play-tts-enhanced.sh +1 -1
  10. package/.claude/hooks/play-tts-piper.sh +16 -5
  11. package/.claude/hooks/play-tts-ssh-remote.sh +168 -167
  12. package/.claude/hooks/play-tts.sh +31 -9
  13. package/.claude/hooks/session-start-tts.sh +4 -1
  14. package/.claude/hooks/stop-tts.sh +1 -1
  15. package/.claude/hooks/verbosity-manager.sh +185 -178
  16. package/.claude/hooks-windows/download-extra-voices.ps1 +243 -185
  17. package/.claude/hooks-windows/play-tts-piper.ps1 +7 -2
  18. package/.claude/hooks-windows/play-tts.ps1 +219 -65
  19. package/.claude/hooks-windows/session-start-tts.ps1 +2 -1
  20. package/.claude/hooks-windows/verbosity-manager.ps1 +126 -119
  21. package/README.md +24 -1
  22. package/RELEASE_NOTES.md +113 -0
  23. package/bin/agentvibes-voice-browser.js +1939 -1840
  24. package/mcp-server/server.py +75 -25
  25. package/package.json +1 -1
  26. package/src/console/tabs/receiver-tab.js +1527 -1483
  27. package/src/console/tabs/settings-tab.js +2 -2
  28. package/src/console/tabs/setup-tab.js +122 -20
  29. package/src/console/tabs/voices-tab.js +130 -13
  30. package/src/i18n/en.js +202 -202
  31. package/src/installer.js +29 -25
  32. package/src/services/llm-provider-service.js +114 -11
  33. package/src/services/verbosity-service.js +159 -157
  34. package/templates/agentvibes-receiver.sh +3 -2
@@ -27,8 +27,66 @@ export const PROVIDERS = [
27
27
  name: 'OpenAI Codex',
28
28
  desc: 'OpenAI CLI agent — .codex/config.toml + AGENTS.md',
29
29
  },
30
+ {
31
+ id: 'default',
32
+ name: 'Default (Fallback)',
33
+ desc: 'Used when any tool calls TTS without identifying its LLM',
34
+ // No install/uninstall — this is a config-only entry
35
+ isDefault: true,
36
+ },
30
37
  ];
31
38
 
39
+ const DEFAULT_LLM_CONFIGS = {
40
+ // Fallback used when play-tts is invoked with no -llm flag. Pretext is
41
+ // empty by default — users edit it via Setup → Default → Configure. When
42
+ // empty, no prefix is prepended at all.
43
+ default: {
44
+ effects: 'light',
45
+ bgTrack: '',
46
+ bgVolume: '0.15',
47
+ voice: 'en_US-lessac-high',
48
+ pretext: '',
49
+ ttsEngine: 'piper',
50
+ },
51
+ 'claude-code': {
52
+ effects: 'light',
53
+ bgTrack: 'agent_vibes_chillwave_v2_loop.mp3',
54
+ bgVolume: '0.15',
55
+ voice: 'en_US-lessac-high',
56
+ pretext: 'Claude Code here',
57
+ ttsEngine: 'piper',
58
+ },
59
+ copilot: {
60
+ effects: 'light',
61
+ bgTrack: 'agent_vibes_bossa_nova_v2_loop.mp3',
62
+ bgVolume: '0.15',
63
+ voice: 'en_US-libritts-high::Anna-11',
64
+ pretext: 'Copilot here',
65
+ ttsEngine: 'piper',
66
+ },
67
+ codex: {
68
+ effects: 'light',
69
+ bgTrack: 'agent_vibes_chillwave_v2_loop.mp3',
70
+ bgVolume: '0.15',
71
+ // NOTE: lessac-medium appears to silently fail to synthesize on some
72
+ // Windows Piper installs (loads the model, exits with no output).
73
+ // lessac-high works reliably, so use it as the default for codex.
74
+ voice: 'en_US-lessac-high',
75
+ pretext: 'Codex here',
76
+ ttsEngine: 'piper',
77
+ },
78
+ };
79
+
80
+ function ensureDefaultLlmConfigSync(llmKey, targetDir) {
81
+ const existing = loadLlmConfigSync(llmKey, targetDir);
82
+ if (existing.sourcePath) return;
83
+
84
+ const defaults = DEFAULT_LLM_CONFIGS[llmKey];
85
+ if (!defaults) return;
86
+
87
+ saveLlmConfigSync(llmKey, defaults, targetDir);
88
+ }
89
+
32
90
  // ── Provider install-checks ─────────────────────────────────────────────────
33
91
 
34
92
  export async function checkClaudeInstalled(targetDir) {
@@ -73,12 +131,24 @@ export async function checkCodexInstalled(targetDir) {
73
131
  export async function installClaudeMcp(targetDir) {
74
132
  const mcpConfigPath = path.join(targetDir, '.mcp.json');
75
133
 
134
+ // The agentvibes server entry for Claude Code's .mcp.json.
135
+ //
136
+ // IMPORTANT: no `env.AGENTVIBES_LLM` block here. GitHub Copilot CLI
137
+ // also reads project-level `.mcp.json` with precedence over its own
138
+ // `~/.copilot/mcp-config.json` — so if we set `AGENTVIBES_LLM=claude-code`
139
+ // in `.mcp.json`, Copilot CLI picks up that value too and mis-routes.
140
+ // Instead, the MCP server (mcp-server/server.py) auto-detects Claude
141
+ // Code via the `CLAUDECODE=1` env var that Claude Code sets on every
142
+ // subprocess it spawns. Copilot CLI does NOT set that var, so its
143
+ // spawned MCP server correctly falls back to its own config.
144
+ const agentvibesServer = {
145
+ command: 'npx',
146
+ args: ['-y', '--package=agentvibes', 'agentvibes-mcp-server'],
147
+ };
148
+
76
149
  const mcpConfig = {
77
150
  mcpServers: {
78
- agentvibes: {
79
- command: 'npx',
80
- args: ['-y', '--package=agentvibes', 'agentvibes-mcp-server'],
81
- },
151
+ agentvibes: agentvibesServer,
82
152
  },
83
153
  };
84
154
 
@@ -86,15 +156,15 @@ export async function installClaudeMcp(targetDir) {
86
156
  let mcpCreated = false;
87
157
  try {
88
158
  await fs.access(mcpConfigPath);
89
- // Already exists — merge agentvibes key if missing
159
+ // Already exists — merge / upgrade the agentvibes entry. This also
160
+ // STRIPS any stale AGENTVIBES_LLM env block left over from v5.1.2..4
161
+ // so Copilot CLI stops mis-routing.
90
162
  try {
91
163
  const existing = JSON.parse(await fs.readFile(mcpConfigPath, 'utf8'));
92
- if (!existing.mcpServers?.agentvibes) {
93
- existing.mcpServers = existing.mcpServers || {};
94
- existing.mcpServers.agentvibes = mcpConfig.mcpServers.agentvibes;
95
- await fs.writeFile(mcpConfigPath, JSON.stringify(existing, null, 2) + '\n');
96
- mcpCreated = true;
97
- }
164
+ existing.mcpServers = existing.mcpServers || {};
165
+ existing.mcpServers.agentvibes = { ...agentvibesServer };
166
+ await fs.writeFile(mcpConfigPath, JSON.stringify(existing, null, 2) + '\n');
167
+ mcpCreated = true;
98
168
  } catch { /* parse error — don't corrupt */ }
99
169
  } catch {
100
170
  // File doesn't exist — create it
@@ -112,6 +182,7 @@ export async function installClaudeMcp(targetDir) {
112
182
  await installer.copyPluginFiles(targetDir, silentSpinner);
113
183
  await installer.copyBmadConfigFiles(targetDir, silentSpinner);
114
184
  await installer.copyBackgroundMusicFiles(targetDir, silentSpinner);
185
+ ensureDefaultLlmConfigSync('claude-code', targetDir);
115
186
 
116
187
  return { success: true, mcpCreated };
117
188
  } catch (err) {
@@ -247,6 +318,37 @@ export async function installCopilotMcp(targetDir) {
247
318
 
248
319
  mcpConfig.servers.agentvibes = agentvibesServer;
249
320
  await fs.writeFile(mcpJsonPath, JSON.stringify(mcpConfig, null, 2) + '\n');
321
+
322
+ // Also write ~/.copilot/mcp-config.json so the GitHub Copilot CLI
323
+ // (different product from VS Code Copilot Chat!) can find the
324
+ // agentvibes MCP server. VS Code reads .vscode/mcp.json, but the
325
+ // CLI reads ONLY from ~/.copilot/mcp-config.json per docs:
326
+ // https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers
327
+ try {
328
+ const copilotHome = process.env.COPILOT_HOME ||
329
+ path.join(process.env.USERPROFILE || process.env.HOME || '', '.copilot');
330
+ const copilotMcpPath = path.join(copilotHome, 'mcp-config.json');
331
+ await fs.mkdir(copilotHome, { recursive: true });
332
+ let cliConfig = { mcpServers: {} };
333
+ try {
334
+ const existingCli = await fs.readFile(copilotMcpPath, 'utf8');
335
+ const parsedCli = JSON.parse(existingCli);
336
+ if (parsedCli && typeof parsedCli === 'object') {
337
+ cliConfig = parsedCli;
338
+ if (!cliConfig.mcpServers) cliConfig.mcpServers = {};
339
+ }
340
+ } catch { /* new file */ }
341
+ cliConfig.mcpServers.agentvibes = {
342
+ type: 'local',
343
+ command: 'npx',
344
+ args: ['-y', '--package=agentvibes', 'agentvibes-mcp-server'],
345
+ env: { AGENTVIBES_LLM: 'copilot' },
346
+ tools: ['*'],
347
+ };
348
+ await fs.writeFile(copilotMcpPath, JSON.stringify(cliConfig, null, 2) + '\n');
349
+ } catch { /* best effort — CLI might not be installed */ }
350
+
351
+ ensureDefaultLlmConfigSync('copilot', targetDir);
250
352
  return { success: true };
251
353
  } catch (err) {
252
354
  return { success: false, error: err.message };
@@ -300,6 +402,7 @@ export async function installCodexMcp(targetDir) {
300
402
  try { existing = await fs.readFile(tomlPath, 'utf8'); } catch { /* new file */ }
301
403
  const content = buildCodexToml(existing);
302
404
  await fs.writeFile(tomlPath, content);
405
+ ensureDefaultLlmConfigSync('codex', targetDir);
303
406
  return { success: true };
304
407
  } catch (err) {
305
408
  return { success: false, error: err.message };
@@ -1,157 +1,159 @@
1
- /**
2
- * AgentVibes VerbosityService
3
- * Epic 10: Stories 10.1-10.4
4
- *
5
- * Centralises verbosity logic for 5 levels: minimal, low, medium, high, custom.
6
- *
7
- * Hook types:
8
- * - 'prompt-submit' — fires when user submits a prompt
9
- * - 'response-complete' — fires when Claude finishes responding
10
- */
11
-
12
- // ---------------------------------------------------------------------------
13
-
14
- /**
15
- * Ordered verbosity levels from quietest to most verbose.
16
- * @type {string[]}
17
- */
18
- export const VERBOSITY_LEVELS = Object.freeze(['minimal', 'low', 'medium', 'high', 'custom']);
19
-
20
- // Per-level shouldSpeak configuration (fixed levels only; custom reads from config)
21
- const LEVEL_SPEAK = Object.freeze({
22
- minimal: { 'prompt-submit': false, 'response-complete': true },
23
- low: { 'prompt-submit': false, 'response-complete': true },
24
- medium: { 'prompt-submit': true, 'response-complete': true },
25
- high: { 'prompt-submit': true, 'response-complete': true },
26
- });
27
-
28
- // Per-level static messages (null = use hook default message)
29
- const LEVEL_MESSAGES = Object.freeze({
30
- minimal: {
31
- 'prompt-submit': null,
32
- 'response-complete': 'Claude is ready for your input',
33
- },
34
- low: {
35
- 'prompt-submit': null,
36
- 'response-complete': null, // built dynamically: "Done. <summary>"
37
- },
38
- medium: { 'prompt-submit': null, 'response-complete': null },
39
- high: { 'prompt-submit': null, 'response-complete': null },
40
- });
41
-
42
- // ---------------------------------------------------------------------------
43
-
44
- export class VerbosityService {
45
- /**
46
- * @param {object} configService - AgentVibes ConfigService instance
47
- */
48
- constructor(configService) {
49
- this._config = configService;
50
- this._migrated = false; // tracks whether migration ran in this session
51
- }
52
-
53
- /**
54
- * Returns the current verbosity level.
55
- * Defaults to 'high' if not configured or unrecognised.
56
- * @returns {'minimal'|'low'|'medium'|'high'|'custom'}
57
- */
58
- getLevel() {
59
- const cfg = this._config.getConfig();
60
- const level = cfg.verbosity;
61
- return VERBOSITY_LEVELS.includes(level) ? level : 'high';
62
- }
63
-
64
- /**
65
- * Returns whether TTS should speak for the given hook type.
66
- *
67
- * @param {'prompt-submit'|'response-complete'} hookType
68
- * @returns {boolean}
69
- */
70
- shouldSpeak(hookType) {
71
- const level = this.getLevel();
72
- if (level === 'custom') {
73
- return this._customShouldSpeak(hookType);
74
- }
75
- const table = LEVEL_SPEAK[level] ?? LEVEL_SPEAK.high;
76
- return table[hookType] ?? true;
77
- }
78
-
79
- /**
80
- * Returns the message for the given hook type and context.
81
- * Returns null to use the hook's own default message.
82
- *
83
- * @param {'prompt-submit'|'response-complete'} hookType
84
- * @param {object} context - { summary?: string }
85
- * @returns {string|null}
86
- */
87
- getMessage(hookType, context) {
88
- const level = this.getLevel();
89
- if (level === 'high' || level === 'medium') return null;
90
-
91
- if (level === 'low' && hookType === 'response-complete') {
92
- const summary = context?.summary ?? '';
93
- const trimmed = summary.slice(0, 30);
94
- return trimmed.length > 0 ? `Done. ${trimmed}` : 'Done';
95
- }
96
-
97
- if (level === 'custom') return null; // custom uses hook defaults
98
-
99
- return LEVEL_MESSAGES[level]?.[hookType] ?? null;
100
- }
101
-
102
- /**
103
- * Checks if migration is needed (old 'low' → 'medium').
104
- * If needed and not already done, performs migration and returns true.
105
- * @returns {boolean} true if migration was performed, false otherwise
106
- */
107
- checkMigration() {
108
- const cfg = this._config.getConfig();
109
- if (cfg.verbosityMigrated) return false;
110
- if (cfg.verbosity !== 'low') return false;
111
-
112
- // Migrate: old 'low' users get 'medium' in new system
113
- this._config.set('verbosity', 'medium');
114
- this._config.set('verbosityMigrated', true);
115
- this._migrated = true;
116
- return true;
117
- }
118
-
119
- /**
120
- * Returns true if a migration notice should be shown to the user.
121
- * @returns {boolean}
122
- */
123
- needsMigrationNotice() {
124
- if (!this._migrated) return false;
125
- const cfg = this._config.getConfig();
126
- return !cfg.migrationNoticeDismissed;
127
- }
128
-
129
- /**
130
- * Marks the migration notice as dismissed.
131
- */
132
- dismissMigrationNotice() {
133
- this._config.set('migrationNoticeDismissed', true);
134
- }
135
-
136
- // ---------------------------------------------------------------------------
137
- // Private
138
-
139
- /**
140
- * shouldSpeak for CUSTOM level — reads per-hook toggles from config.customVerbosity.
141
- * @param {string} hookType
142
- * @returns {boolean}
143
- */
144
- _customShouldSpeak(hookType) {
145
- const cfg = this._config.getConfig();
146
- const custom = cfg.customVerbosity ?? {};
147
- const keyMap = {
148
- 'prompt-submit': 'promptSubmit',
149
- 'response-complete': 'responseComplete',
150
- };
151
- const key = keyMap[hookType];
152
- if (key === undefined) return true;
153
- return custom[key] !== false; // default true if not configured
154
- }
155
- }
156
-
157
- export default VerbosityService;
1
+ /**
2
+ * AgentVibes VerbosityService
3
+ * Epic 10: Stories 10.1-10.4
4
+ *
5
+ * Centralises verbosity logic for 5 levels: minimal, low, medium, high, custom.
6
+ *
7
+ * Hook types:
8
+ * - 'prompt-submit' — fires when user submits a prompt
9
+ * - 'response-complete' — fires when Claude finishes responding
10
+ */
11
+
12
+ // ---------------------------------------------------------------------------
13
+
14
+ /**
15
+ * Ordered verbosity levels from quietest to most verbose.
16
+ * @type {string[]}
17
+ */
18
+ export const VERBOSITY_LEVELS = Object.freeze(['minimal', 'low', 'medium', 'high', 'caveman', 'custom']);
19
+
20
+ // Per-level shouldSpeak configuration (fixed levels only; custom reads from config)
21
+ const LEVEL_SPEAK = Object.freeze({
22
+ minimal: { 'prompt-submit': false, 'response-complete': true },
23
+ low: { 'prompt-submit': false, 'response-complete': true },
24
+ medium: { 'prompt-submit': true, 'response-complete': true },
25
+ high: { 'prompt-submit': true, 'response-complete': true },
26
+ caveman: { 'prompt-submit': true, 'response-complete': true },
27
+ });
28
+
29
+ // Per-level static messages (null = use hook default message)
30
+ const LEVEL_MESSAGES = Object.freeze({
31
+ minimal: {
32
+ 'prompt-submit': null,
33
+ 'response-complete': 'Claude is ready for your input',
34
+ },
35
+ low: {
36
+ 'prompt-submit': null,
37
+ 'response-complete': null, // built dynamically: "Done. <summary>"
38
+ },
39
+ medium: { 'prompt-submit': null, 'response-complete': null },
40
+ high: { 'prompt-submit': null, 'response-complete': null },
41
+ caveman: { 'prompt-submit': null, 'response-complete': null },
42
+ });
43
+
44
+ // ---------------------------------------------------------------------------
45
+
46
+ export class VerbosityService {
47
+ /**
48
+ * @param {object} configService - AgentVibes ConfigService instance
49
+ */
50
+ constructor(configService) {
51
+ this._config = configService;
52
+ this._migrated = false; // tracks whether migration ran in this session
53
+ }
54
+
55
+ /**
56
+ * Returns the current verbosity level.
57
+ * Defaults to 'high' if not configured or unrecognised.
58
+ * @returns {'minimal'|'low'|'medium'|'high'|'custom'}
59
+ */
60
+ getLevel() {
61
+ const cfg = this._config.getConfig();
62
+ const level = cfg.verbosity;
63
+ return VERBOSITY_LEVELS.includes(level) ? level : 'high';
64
+ }
65
+
66
+ /**
67
+ * Returns whether TTS should speak for the given hook type.
68
+ *
69
+ * @param {'prompt-submit'|'response-complete'} hookType
70
+ * @returns {boolean}
71
+ */
72
+ shouldSpeak(hookType) {
73
+ const level = this.getLevel();
74
+ if (level === 'custom') {
75
+ return this._customShouldSpeak(hookType);
76
+ }
77
+ const table = LEVEL_SPEAK[level] ?? LEVEL_SPEAK.high;
78
+ return table[hookType] ?? true;
79
+ }
80
+
81
+ /**
82
+ * Returns the message for the given hook type and context.
83
+ * Returns null to use the hook's own default message.
84
+ *
85
+ * @param {'prompt-submit'|'response-complete'} hookType
86
+ * @param {object} context - { summary?: string }
87
+ * @returns {string|null}
88
+ */
89
+ getMessage(hookType, context) {
90
+ const level = this.getLevel();
91
+ if (level === 'high' || level === 'medium' || level === 'caveman') return null;
92
+
93
+ if (level === 'low' && hookType === 'response-complete') {
94
+ const summary = context?.summary ?? '';
95
+ const trimmed = summary.slice(0, 30);
96
+ return trimmed.length > 0 ? `Done. ${trimmed}` : 'Done';
97
+ }
98
+
99
+ if (level === 'custom') return null; // custom uses hook defaults
100
+
101
+ return LEVEL_MESSAGES[level]?.[hookType] ?? null;
102
+ }
103
+
104
+ /**
105
+ * Checks if migration is needed (old 'low' → 'medium').
106
+ * If needed and not already done, performs migration and returns true.
107
+ * @returns {boolean} true if migration was performed, false otherwise
108
+ */
109
+ checkMigration() {
110
+ const cfg = this._config.getConfig();
111
+ if (cfg.verbosityMigrated) return false;
112
+ if (cfg.verbosity !== 'low') return false;
113
+
114
+ // Migrate: old 'low' users get 'medium' in new system
115
+ this._config.set('verbosity', 'medium');
116
+ this._config.set('verbosityMigrated', true);
117
+ this._migrated = true;
118
+ return true;
119
+ }
120
+
121
+ /**
122
+ * Returns true if a migration notice should be shown to the user.
123
+ * @returns {boolean}
124
+ */
125
+ needsMigrationNotice() {
126
+ if (!this._migrated) return false;
127
+ const cfg = this._config.getConfig();
128
+ return !cfg.migrationNoticeDismissed;
129
+ }
130
+
131
+ /**
132
+ * Marks the migration notice as dismissed.
133
+ */
134
+ dismissMigrationNotice() {
135
+ this._config.set('migrationNoticeDismissed', true);
136
+ }
137
+
138
+ // ---------------------------------------------------------------------------
139
+ // Private
140
+
141
+ /**
142
+ * shouldSpeak for CUSTOM level — reads per-hook toggles from config.customVerbosity.
143
+ * @param {string} hookType
144
+ * @returns {boolean}
145
+ */
146
+ _customShouldSpeak(hookType) {
147
+ const cfg = this._config.getConfig();
148
+ const custom = cfg.customVerbosity ?? {};
149
+ const keyMap = {
150
+ 'prompt-submit': 'promptSubmit',
151
+ 'response-complete': 'responseComplete',
152
+ };
153
+ const key = keyMap[hookType];
154
+ if (key === undefined) return true;
155
+ return custom[key] !== false; // default true if not configured
156
+ }
157
+ }
158
+
159
+ export default VerbosityService;
@@ -201,8 +201,9 @@ if [[ -z "$TEXT" ]]; then
201
201
  exit 1
202
202
  fi
203
203
 
204
- # SECURITY: Validate voice format (alphanumeric, hyphens, underscores only)
205
- if [[ ! "$VOICE" =~ ^[a-zA-Z0-9_-]+$ ]]; then
204
+ # SECURITY: Validate voice format (allow :: for multi-speaker, . for locale, space for names)
205
+ _voice_re='^[a-zA-Z0-9_.: -]+$'
206
+ if [[ ! "$VOICE" =~ $_voice_re ]]; then
206
207
  echo "Error: Invalid voice format" >&2
207
208
  exit 1
208
209
  fi