@holdyourvoice/hyv 2.9.14 → 2.9.15

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holdyourvoice/hyv",
3
- "version": "2.9.14",
3
+ "version": "2.9.15",
4
4
  "description": "Free local AI writing scan for cursor & claude. MCP server, 220+ pattern detection, voice profiles. npx @holdyourvoice/hyv welcome",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -89,6 +89,18 @@ function resolveHyvMcpCommandArray(pkgDir) {
89
89
  return [command, ...args];
90
90
  }
91
91
 
92
+ function mcpJsonEntriesMatch(existing, desired) {
93
+ if (!existing || typeof existing !== 'object') return false;
94
+ const a = existing.args || [];
95
+ const b = desired.args || [];
96
+ return existing.command === desired.command && JSON.stringify(a) === JSON.stringify(b);
97
+ }
98
+
99
+ function mcpCommandArrayMatches(existing, desired) {
100
+ if (!Array.isArray(existing) || !Array.isArray(desired)) return false;
101
+ return JSON.stringify(existing) === JSON.stringify(desired);
102
+ }
103
+
92
104
  function parseJsonc(text) {
93
105
  const noBlock = text.replace(/\/\*[\s\S]*?\*\//g, '');
94
106
  const noLine = noBlock.replace(/^\s*\/\/.*$/gm, '');
@@ -151,6 +163,226 @@ function chatgptHelperDir(home) {
151
163
  return path.join(home, '.chatgpt');
152
164
  }
153
165
 
166
+ function macAppInstalled(appName, home) {
167
+ if (process.platform !== 'darwin') return false;
168
+ const roots = ['/Applications', path.join(home, 'Applications')];
169
+ return roots.some((root) => fs.existsSync(path.join(root, `${appName}.app`)));
170
+ }
171
+
172
+ function dirExists(dir) {
173
+ try {
174
+ return fs.existsSync(dir) && fs.statSync(dir).isDirectory();
175
+ } catch {
176
+ return false;
177
+ }
178
+ }
179
+
180
+ function fileExists(file) {
181
+ try {
182
+ return fs.existsSync(file);
183
+ } catch {
184
+ return false;
185
+ }
186
+ }
187
+
188
+ function commandOnPath(cmd) {
189
+ const pathVar = process.env.PATH || '';
190
+ const exts = process.platform === 'win32' ? (process.env.PATHEXT || '.EXE;.CMD;.BAT').split(';') : [''];
191
+ for (const entry of pathVar.split(path.delimiter)) {
192
+ if (!entry) continue;
193
+ for (const ext of exts) {
194
+ const candidate = path.join(entry, process.platform === 'win32' ? `${cmd}${ext}` : cmd);
195
+ try {
196
+ if (fs.existsSync(candidate)) return true;
197
+ } catch {
198
+ // ignore
199
+ }
200
+ }
201
+ }
202
+ return false;
203
+ }
204
+
205
+ /**
206
+ * Detect locally installed AI apps that hyv can integrate with.
207
+ * @returns {{ id: string, label: string, reason: string, integration: 'mcp'|'rules'|'manual' }[]}
208
+ */
209
+ function detectInstalledAgents(home = require('os').homedir(), isWin = process.platform === 'win32') {
210
+ const detected = [];
211
+
212
+ const claudeDir = claudeDesktopDir(home, isWin);
213
+ if (
214
+ dirExists(claudeDir) ||
215
+ fileExists(path.join(claudeDir, 'claude_desktop_config.json')) ||
216
+ macAppInstalled('Claude', home)
217
+ ) {
218
+ detected.push({
219
+ id: 'claude-desktop',
220
+ label: 'claude desktop',
221
+ reason: dirExists(claudeDir) ? 'config directory found' : 'claude desktop app found',
222
+ integration: 'mcp',
223
+ });
224
+ }
225
+
226
+ const cursorDir = path.join(home, '.cursor');
227
+ if (dirExists(cursorDir) || macAppInstalled('Cursor', home)) {
228
+ detected.push({
229
+ id: 'cursor',
230
+ label: 'cursor',
231
+ reason: dirExists(cursorDir) ? '~/.cursor found' : 'cursor app found',
232
+ integration: 'mcp',
233
+ });
234
+ }
235
+
236
+ const claudeCodeDir = path.join(home, '.claude');
237
+ if (dirExists(claudeCodeDir) || commandOnPath('claude')) {
238
+ detected.push({
239
+ id: 'claude-code',
240
+ label: 'claude code',
241
+ reason: dirExists(claudeCodeDir) ? '~/.claude found' : 'claude cli on path',
242
+ integration: 'rules',
243
+ });
244
+ }
245
+
246
+ const windsurfDirs = [
247
+ path.join(home, '.windsurf'),
248
+ isWin ? path.join(home, 'AppData', 'Roaming', 'Windsurf') : path.join(home, 'Library', 'Application Support', 'Windsurf'),
249
+ ].filter(Boolean);
250
+ if (windsurfDirs.some(dirExists) || macAppInstalled('Windsurf', home)) {
251
+ detected.push({
252
+ id: 'windsurf',
253
+ label: 'windsurf',
254
+ reason: windsurfDirs.some(dirExists) ? 'windsurf config found' : 'windsurf app found',
255
+ integration: 'rules',
256
+ });
257
+ }
258
+
259
+ if (dirExists(path.join(home, '.codex')) || commandOnPath('codex')) {
260
+ detected.push({
261
+ id: 'codex',
262
+ label: 'codex',
263
+ reason: dirExists(path.join(home, '.codex')) ? '~/.codex found' : 'codex cli on path',
264
+ integration: 'rules',
265
+ });
266
+ }
267
+
268
+ if (dirExists(path.join(home, '.commandcode'))) {
269
+ detected.push({
270
+ id: 'command-code',
271
+ label: 'command code',
272
+ reason: '~/.commandcode found',
273
+ integration: 'rules',
274
+ });
275
+ }
276
+
277
+ const agDir = path.dirname(antigravityMcpConfigPath(home, isWin));
278
+ if (dirExists(agDir) || macAppInstalled('Antigravity', home)) {
279
+ detected.push({
280
+ id: 'antigravity',
281
+ label: 'antigravity',
282
+ reason: dirExists(agDir) ? '~/.gemini/config found' : 'antigravity app found',
283
+ integration: 'mcp',
284
+ });
285
+ }
286
+
287
+ const ocDir = opencodeConfigDir(home, isWin);
288
+ if (dirExists(ocDir) || commandOnPath('opencode')) {
289
+ detected.push({
290
+ id: 'opencode',
291
+ label: 'opencode',
292
+ reason: dirExists(ocDir) ? 'opencode config found' : 'opencode cli on path',
293
+ integration: 'mcp',
294
+ });
295
+ }
296
+
297
+ const chatgptDir = chatgptHelperDir(home);
298
+ if (dirExists(chatgptDir) || macAppInstalled('ChatGPT', home)) {
299
+ detected.push({
300
+ id: 'chatgpt',
301
+ label: 'chatgpt desktop',
302
+ reason: dirExists(chatgptDir) ? '~/.chatgpt found' : 'chatgpt app found',
303
+ integration: 'manual',
304
+ });
305
+ }
306
+
307
+ return detected;
308
+ }
309
+
310
+ function agentIdForConfiguredLabel(label) {
311
+ const map = {
312
+ 'claude desktop': 'claude-desktop',
313
+ 'claude desktop mcp': 'claude-desktop',
314
+ cursor: 'cursor',
315
+ 'cursor mcp': 'cursor',
316
+ 'claude code': 'claude-code',
317
+ 'claude code skill': 'claude-code',
318
+ windsurf: 'windsurf',
319
+ codex: 'codex',
320
+ 'command code': 'command-code',
321
+ 'antigravity mcp': 'antigravity',
322
+ 'opencode mcp': 'opencode',
323
+ 'opencode agents': 'opencode',
324
+ 'chatgpt connector guide': 'chatgpt',
325
+ };
326
+ return map[label] || null;
327
+ }
328
+
329
+ function isAgentIdEnabled(agentId, onlyDetected, detectedIds) {
330
+ if (!onlyDetected) return true;
331
+ return detectedIds.includes(agentId);
332
+ }
333
+
334
+ function readMcpHyvEntry(configFile) {
335
+ if (!fileExists(configFile)) return null;
336
+ try {
337
+ const cfg = JSON.parse(fs.readFileSync(configFile, 'utf-8'));
338
+ return cfg?.mcpServers?.hyv || null;
339
+ } catch {
340
+ return null;
341
+ }
342
+ }
343
+
344
+ function isAgentIntegrated(agentId, home = require('os').homedir(), isWin = process.platform === 'win32') {
345
+ switch (agentId) {
346
+ case 'claude-desktop':
347
+ return Boolean(readMcpHyvEntry(path.join(claudeDesktopDir(home, isWin), 'claude_desktop_config.json')));
348
+ case 'cursor':
349
+ return Boolean(readMcpHyvEntry(path.join(home, '.cursor', 'mcp.json')))
350
+ && fileExists(path.join(home, '.cursor', 'rules', 'hyv.mdc'));
351
+ case 'claude-code':
352
+ return fileExists(path.join(home, '.claude', 'commands', 'hyv.md'))
353
+ && fileExists(path.join(home, '.claude', 'skills', 'hold-your-voice', 'SKILL.md'));
354
+ case 'windsurf': {
355
+ const dirs = [
356
+ path.join(home, '.windsurf'),
357
+ isWin ? path.join(home, 'AppData', 'Roaming', 'Windsurf') : path.join(home, 'Library', 'Application Support', 'Windsurf'),
358
+ ].filter(Boolean);
359
+ return dirs.some((dir) => fileExists(path.join(dir, 'rules', 'hyv.md')));
360
+ }
361
+ case 'codex': {
362
+ const agents = path.join(home, '.codex', 'AGENTS.md');
363
+ return fileExists(agents) && fs.readFileSync(agents, 'utf-8').includes('hyv');
364
+ }
365
+ case 'command-code':
366
+ return fileExists(path.join(home, '.commandcode', 'skills', 'hyv', 'SKILL.md'));
367
+ case 'antigravity':
368
+ return Boolean(readMcpHyvEntry(antigravityMcpConfigPath(home, isWin)));
369
+ case 'opencode': {
370
+ const ocFile = path.join(opencodeConfigDir(home, isWin), 'opencode.jsonc');
371
+ if (!fileExists(ocFile)) return false;
372
+ try {
373
+ const cfg = parseJsonc(fs.readFileSync(ocFile, 'utf-8'));
374
+ return Boolean(cfg.mcp?.hyv);
375
+ } catch {
376
+ return false;
377
+ }
378
+ }
379
+ case 'chatgpt':
380
+ return fileExists(path.join(chatgptHelperDir(home), 'hyv-mcp-connector.txt'));
381
+ default:
382
+ return false;
383
+ }
384
+ }
385
+
154
386
  function mergeJsonConfigFile(configFile, mutator, { backup = true } = {}) {
155
387
  fs.mkdirSync(path.dirname(configFile), { recursive: true });
156
388
  const read = readJsonObjectFromFile(configFile, (text) => JSON.parse(text));
@@ -207,20 +439,35 @@ function claudeDesktopDir(home, isWin) {
207
439
 
208
440
  /**
209
441
  * Configure MCP + agent instructions for detected IDEs.
210
- * @returns {{ configured: string[], warnings: string[], pkgVersion: string }}
442
+ * @returns {{ configured: string[], warnings: string[], pkgVersion: string, detected: object[], skipped: string[] }}
211
443
  */
212
- function setupAgents({ pkgDir, home = require('os').homedir(), quiet = false }) {
444
+ function setupAgents({
445
+ pkgDir,
446
+ home = require('os').homedir(),
447
+ quiet = false,
448
+ onlyDetected = false,
449
+ force = false,
450
+ } = {}) {
213
451
  const configured = [];
214
452
  const warnings = [];
453
+ const skipped = [];
215
454
  const isWin = process.platform === 'win32';
216
455
  const hyvDir = path.join(home, '.hyv');
217
456
  const agentsMarker = path.join(hyvDir, 'agents-version.json');
218
457
  const pkgVersion = readPkgVersion(pkgDir);
219
458
  const mcpCmd = resolveHyvMcpCommand(pkgDir);
220
459
  const autoConfigure = process.env.HYV_AUTO_CONFIGURE_AGENTS !== '0';
460
+ const detected = detectInstalledAgents(home, isWin);
461
+ const detectedIds = detected.map((entry) => entry.id);
221
462
 
222
463
  if (!autoConfigure) {
223
- return { configured, warnings: ['HYV_AUTO_CONFIGURE_AGENTS=0 — skipped agent setup'], pkgVersion };
464
+ return {
465
+ configured,
466
+ warnings: ['HYV_AUTO_CONFIGURE_AGENTS=0 — skipped agent setup'],
467
+ pkgVersion,
468
+ detected,
469
+ skipped: detectedIds,
470
+ };
224
471
  }
225
472
 
226
473
  function installAgent(src, dest, transform) {
@@ -229,11 +476,16 @@ function setupAgents({ pkgDir, home = require('os').homedir(), quiet = false })
229
476
  }
230
477
 
231
478
  function setupMcpJson(configFile, label) {
479
+ const desired = { command: mcpCmd.command, args: mcpCmd.args };
232
480
  const result = mergeJsonConfigFile(configFile, (config) => {
233
481
  if (!config.mcpServers) config.mcpServers = {};
234
- if (!config.mcpServers.hyv) {
235
- config.mcpServers.hyv = { command: mcpCmd.command, args: mcpCmd.args };
482
+ const existing = config.mcpServers.hyv;
483
+ if (!existing) {
484
+ config.mcpServers.hyv = desired;
236
485
  configured.push(`${label} mcp`);
486
+ } else if (force || !mcpJsonEntriesMatch(existing, desired)) {
487
+ config.mcpServers.hyv = desired;
488
+ configured.push(`${label} mcp (updated)`);
237
489
  }
238
490
  return config;
239
491
  });
@@ -242,18 +494,22 @@ function setupAgents({ pkgDir, home = require('os').homedir(), quiet = false })
242
494
  }
243
495
 
244
496
  // Claude Desktop MCP
245
- try {
246
- const claudeDir = claudeDesktopDir(home, isWin);
247
- const configFile = path.join(claudeDir, 'claude_desktop_config.json');
248
- if (fs.existsSync(claudeDir) || autoConfigure) {
497
+ if (isAgentIdEnabled('claude-desktop', onlyDetected, detectedIds)) {
498
+ try {
499
+ const claudeDir = claudeDesktopDir(home, isWin);
500
+ const configFile = path.join(claudeDir, 'claude_desktop_config.json');
249
501
  setupMcpJson(configFile, 'claude desktop');
502
+ } catch (err) {
503
+ warnings.push(`claude desktop: ${err.message}`);
250
504
  }
251
- } catch (err) {
252
- warnings.push(`claude desktop: ${err.message}`);
505
+ } else if (onlyDetected) {
506
+ skipped.push('claude-desktop');
253
507
  }
254
508
 
255
509
  // Cursor — global MCP + always-on rule (.mdc)
256
- try {
510
+ if (!isAgentIdEnabled('cursor', onlyDetected, detectedIds)) {
511
+ if (onlyDetected) skipped.push('cursor');
512
+ } else try {
257
513
  const cursorDir = path.join(home, '.cursor');
258
514
  fs.mkdirSync(cursorDir, { recursive: true });
259
515
  const mcpFile = path.join(cursorDir, 'mcp.json');
@@ -271,7 +527,9 @@ function setupAgents({ pkgDir, home = require('os').homedir(), quiet = false })
271
527
  }
272
528
 
273
529
  // Claude Code — slash command + skill
274
- try {
530
+ if (!isAgentIdEnabled('claude-code', onlyDetected, detectedIds)) {
531
+ if (onlyDetected) skipped.push('claude-code');
532
+ } else try {
275
533
  const cmdDir = path.join(home, '.claude', 'commands');
276
534
  const skillDir = path.join(home, '.claude', 'skills', 'hold-your-voice');
277
535
  fs.mkdirSync(cmdDir, { recursive: true });
@@ -287,7 +545,9 @@ function setupAgents({ pkgDir, home = require('os').homedir(), quiet = false })
287
545
  }
288
546
 
289
547
  // Windsurf — rules with frontmatter
290
- try {
548
+ if (!isAgentIdEnabled('windsurf', onlyDetected, detectedIds)) {
549
+ if (onlyDetected) skipped.push('windsurf');
550
+ } else try {
291
551
  const wsDirs = [
292
552
  isWin ? path.join(home, 'AppData', 'Roaming', 'Windsurf') : path.join(home, '.windsurf'),
293
553
  isWin ? null : path.join(home, 'Library', 'Application Support', 'Windsurf'),
@@ -306,7 +566,9 @@ function setupAgents({ pkgDir, home = require('os').homedir(), quiet = false })
306
566
  }
307
567
 
308
568
  // Codex — ~/.codex/AGENTS.md
309
- try {
569
+ if (!isAgentIdEnabled('codex', onlyDetected, detectedIds)) {
570
+ if (onlyDetected) skipped.push('codex');
571
+ } else try {
310
572
  const codexDir = path.join(home, '.codex');
311
573
  fs.mkdirSync(codexDir, { recursive: true });
312
574
  const agentsFile = path.join(codexDir, 'AGENTS.md');
@@ -327,7 +589,9 @@ function setupAgents({ pkgDir, home = require('os').homedir(), quiet = false })
327
589
  }
328
590
 
329
591
  // Command Code skill
330
- try {
592
+ if (!isAgentIdEnabled('command-code', onlyDetected, detectedIds)) {
593
+ if (onlyDetected) skipped.push('command-code');
594
+ } else try {
331
595
  const ccSkillDir = path.join(home, '.commandcode', 'skills', 'hyv');
332
596
  fs.mkdirSync(ccSkillDir, { recursive: true });
333
597
  const skillSrc = path.join(pkgDir, 'skills', 'hold-your-voice', 'SKILL.md');
@@ -338,14 +602,21 @@ function setupAgents({ pkgDir, home = require('os').homedir(), quiet = false })
338
602
  }
339
603
 
340
604
  // Antigravity — ~/.gemini/config/mcp_config.json (absolute paths for GUI launch)
341
- try {
605
+ if (!isAgentIdEnabled('antigravity', onlyDetected, detectedIds)) {
606
+ if (onlyDetected) skipped.push('antigravity');
607
+ } else try {
342
608
  const agFile = antigravityMcpConfigPath(home, isWin);
343
609
  const mcpCmd = resolveHyvMcpCommand(pkgDir);
610
+ const desired = { command: mcpCmd.command, args: mcpCmd.args };
344
611
  const result = mergeJsonConfigFile(agFile, (config) => {
345
612
  if (!config.mcpServers) config.mcpServers = {};
346
- if (!config.mcpServers.hyv) {
347
- config.mcpServers.hyv = { command: mcpCmd.command, args: mcpCmd.args };
613
+ const existing = config.mcpServers.hyv;
614
+ if (!existing) {
615
+ config.mcpServers.hyv = desired;
348
616
  configured.push('antigravity mcp');
617
+ } else if (force || !mcpJsonEntriesMatch(existing, desired)) {
618
+ config.mcpServers.hyv = desired;
619
+ configured.push('antigravity mcp (updated)');
349
620
  }
350
621
  return config;
351
622
  });
@@ -355,15 +626,22 @@ function setupAgents({ pkgDir, home = require('os').homedir(), quiet = false })
355
626
  }
356
627
 
357
628
  // OpenCode — ~/.config/opencode/opencode.jsonc + AGENTS.md
358
- try {
629
+ if (!isAgentIdEnabled('opencode', onlyDetected, detectedIds)) {
630
+ if (onlyDetected) skipped.push('opencode');
631
+ } else try {
359
632
  const ocDir = opencodeConfigDir(home, isWin);
360
633
  const ocFile = path.join(ocDir, 'opencode.jsonc');
361
634
  const cmdArr = resolveHyvMcpCommandArray(pkgDir);
362
635
  const ocResult = mergeJsoncConfigFile(ocFile, (config) => {
363
636
  if (!config.mcp) config.mcp = {};
364
- if (!config.mcp.hyv) {
365
- config.mcp.hyv = { type: 'local', command: cmdArr, enabled: true };
637
+ const existing = config.mcp.hyv;
638
+ const desired = { type: 'local', command: cmdArr, enabled: true };
639
+ if (!existing) {
640
+ config.mcp.hyv = desired;
366
641
  configured.push('opencode mcp');
642
+ } else if (force || !mcpCommandArrayMatches(existing.command, cmdArr)) {
643
+ config.mcp.hyv = { ...existing, ...desired, command: cmdArr };
644
+ configured.push('opencode mcp (updated)');
367
645
  }
368
646
  return config;
369
647
  });
@@ -389,7 +667,9 @@ function setupAgents({ pkgDir, home = require('os').homedir(), quiet = false })
389
667
  }
390
668
 
391
669
  // ChatGPT — connector helper (UI has no config file; write exact values for manual add)
392
- try {
670
+ if (!isAgentIdEnabled('chatgpt', onlyDetected, detectedIds)) {
671
+ if (onlyDetected) skipped.push('chatgpt');
672
+ } else try {
393
673
  const cgDir = chatgptHelperDir(home);
394
674
  fs.mkdirSync(cgDir, { recursive: true });
395
675
  const mcpCmd = resolveHyvMcpCommand(pkgDir);
@@ -424,7 +704,7 @@ function setupAgents({ pkgDir, home = require('os').homedir(), quiet = false })
424
704
  warnings.push(`chatgpt: ${err.message}`);
425
705
  }
426
706
 
427
- // Generic reference copy
707
+ // Generic reference copy (always when auto-configure runs)
428
708
  try {
429
709
  const genericSrc = path.join(pkgDir, 'agents', 'generic.md');
430
710
  const genericDest = path.join(hyvDir, 'agents', 'generic.md');
@@ -437,7 +717,31 @@ function setupAgents({ pkgDir, home = require('os').homedir(), quiet = false })
437
717
  if (!quiet && warnings.length) {
438
718
  for (const w of warnings) process.stderr.write(`[hyv postinstall] warning: ${w}\n`);
439
719
  }
440
- return { configured: [...new Set(configured)], warnings, pkgVersion };
720
+ return {
721
+ configured: [...new Set(configured)],
722
+ warnings,
723
+ pkgVersion,
724
+ detected,
725
+ skipped: [...new Set(skipped)],
726
+ };
727
+ }
728
+
729
+ /**
730
+ * Detect installed apps and configure hyv MCP/rules for each one.
731
+ * @returns {{ detected: object[], configured: string[], warnings: string[], skipped: string[] }}
732
+ */
733
+ function integrateDetectedAgents({ pkgDir, home = require('os').homedir(), quiet = false, force = false } = {}) {
734
+ const detected = detectInstalledAgents(home);
735
+ if (detected.length === 0) {
736
+ return { detected, configured: [], warnings: [], skipped: [] };
737
+ }
738
+ const result = setupAgents({ pkgDir, home, quiet, onlyDetected: true, force });
739
+ return {
740
+ detected: result.detected || detected,
741
+ configured: result.configured || [],
742
+ warnings: result.warnings || [],
743
+ skipped: result.skipped || [],
744
+ };
441
745
  }
442
746
 
443
747
  module.exports = {
@@ -457,6 +761,10 @@ module.exports = {
457
761
  toCursorMdc,
458
762
  toWindsurfRule,
459
763
  setupAgents,
764
+ detectInstalledAgents,
765
+ integrateDetectedAgents,
766
+ agentIdForConfiguredLabel,
767
+ isAgentIntegrated,
460
768
  claudeDesktopDir,
461
769
  antigravityMcpConfigPath,
462
770
  opencodeConfigDir,