@holdyourvoice/hyv 2.9.11 → 2.9.12

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/README.md CHANGED
@@ -13,6 +13,18 @@ npx @holdyourvoice/hyv scan draft.md
13
13
 
14
14
  ## install
15
15
 
16
+ **Recommended** (installs node automatically if missing):
17
+
18
+ ```bash
19
+ # mac / linux
20
+ curl -fsSL https://holdyourvoice.com/install.sh | bash
21
+
22
+ # windows (powershell)
23
+ irm https://holdyourvoice.com/install.ps1 | iex
24
+ ```
25
+
26
+ Already have node 18+?
27
+
16
28
  ```bash
17
29
  npm i -g @holdyourvoice/hyv
18
30
  ```
package/dist/index.js CHANGED
@@ -5385,7 +5385,7 @@ var init_free_paid = __esm({
5385
5385
  "Rich profile-aware rewrite prompts (hyv rewrite)",
5386
5386
  "Hybrid server analysis (hyv scan --server, MCP hyv_analyze)",
5387
5387
  "Multiple profiles + team voices (Team plan)",
5388
- "Dashboard at holdyourvoice.com/app"
5388
+ "Dashboard at holdyourvoice.com/dashboard"
5389
5389
  ];
5390
5390
  FREE_WEB_TOOLS = [
5391
5391
  { name: "All free tools", url: "https://holdyourvoice.com/tools" },
@@ -5406,7 +5406,7 @@ var init_free_paid = __esm({
5406
5406
  ];
5407
5407
  COMMUNITY_URL = "https://holdyourvoice.com/community";
5408
5408
  PRICING_URL = "https://holdyourvoice.com/#pricing";
5409
- DASHBOARD_BILLING_URL = "https://holdyourvoice.com/app/billing";
5409
+ DASHBOARD_BILLING_URL = "https://holdyourvoice.com/dashboard?tab=billing";
5410
5410
  }
5411
5411
  });
5412
5412
 
@@ -5710,7 +5710,7 @@ async function authenticateWithBrowser() {
5710
5710
  return authData;
5711
5711
  }
5712
5712
  async function openAuthenticatedDashboard(opts = {}) {
5713
- const nextPath = opts.next || "/app/billing";
5713
+ const nextPath = opts.next || "/dashboard?tab=billing";
5714
5714
  try {
5715
5715
  const response = await authenticatedRequest(cliApiUrl("/cli/auth/web-handoff"), {
5716
5716
  method: "POST",
@@ -5725,7 +5725,7 @@ async function openAuthenticatedDashboard(opts = {}) {
5725
5725
  }
5726
5726
  } catch {
5727
5727
  }
5728
- const billingUrl = nextPath === "/app/billing" ? DASHBOARD_BILLING_URL : assertSafeOpenUrl(`https://holdyourvoice.com${nextPath}`);
5728
+ const billingUrl = nextPath === "/dashboard?tab=billing" || nextPath === "/app/billing" ? DASHBOARD_BILLING_URL : assertSafeOpenUrl(`https://holdyourvoice.com${nextPath}`);
5729
5729
  await (0, import_open.default)(billingUrl);
5730
5730
  }
5731
5731
  async function refreshToken(tokenOverride) {
@@ -5883,7 +5883,7 @@ async function request2(method, path27, body) {
5883
5883
  if (res.status === 401)
5884
5884
  throw new Error("your session expired. run: hyv init to sign in again.");
5885
5885
  if (res.status === 403)
5886
- throw new Error("you don't have access to this feature. check your plan at holdyourvoice.com/app/billing");
5886
+ throw new Error("you don't have access to this feature. check your plan at holdyourvoice.com/dashboard?tab=billing");
5887
5887
  if (!res.ok) {
5888
5888
  throw new Error(`something went wrong (${res.status}). try again or contact support.`);
5889
5889
  }
@@ -11956,7 +11956,7 @@ async function stepSignup(profileName) {
11956
11956
  ));
11957
11957
  }
11958
11958
  console.log(import_chalk12.default.cyan("\n opening billing in your dashboard ($1 first month)..."));
11959
- await withSpinner("opening billing\u2026", () => openAuthenticatedDashboard({ next: "/app/billing" }));
11959
+ await withSpinner("opening billing\u2026", () => openAuthenticatedDashboard({ next: "/dashboard?tab=billing" }));
11960
11960
  await briefPause();
11961
11961
  markStepComplete("signup");
11962
11962
  console.log(import_chalk12.default.dim("\n you're signed in \u2014 pick a plan in billing, no second login.\n"));
@@ -15624,7 +15624,7 @@ function registerInitCommand(program3) {
15624
15624
  } else if (options?.browser === false) {
15625
15625
  console.log(import_chalk4.default.red("License key required when --no-browser is set."));
15626
15626
  console.log("\nUsage: hyv init <license-key>");
15627
- console.log("\nGenerate a license key at: https://holdyourvoice.com/app");
15627
+ console.log("\nGenerate a license key at: https://holdyourvoice.com/dashboard");
15628
15628
  process.exit(1);
15629
15629
  } else {
15630
15630
  authData = await authenticateWithBrowser();
@@ -16997,7 +16997,7 @@ async function showPlan() {
16997
16997
  async function upgradePlan() {
16998
16998
  console.log(import_chalk14.default.cyan("\nOpening billing in your dashboard ($1 first month)...\n"));
16999
16999
  try {
17000
- await openAuthenticatedDashboard({ next: "/app/billing" });
17000
+ await openAuthenticatedDashboard({ next: "/dashboard?tab=billing" });
17001
17001
  console.log(import_chalk14.default.green("\n\u2713 Dashboard opened \u2014 you're already signed in"));
17002
17002
  console.log(import_chalk14.default.dim("Pick a plan in billing. No second login."));
17003
17003
  } catch {
@@ -17530,6 +17530,33 @@ function claudeDesktopDir() {
17530
17530
  return path18.join(HOME, ".config", "Claude");
17531
17531
  return path18.join(HOME, "Library", "Application Support", "Claude");
17532
17532
  }
17533
+ function windsurfRulePath() {
17534
+ const dirs = [
17535
+ path18.join(HOME, ".windsurf"),
17536
+ IS_WIN ? path18.join(HOME, "AppData", "Roaming", "Windsurf") : path18.join(HOME, "Library", "Application Support", "Windsurf")
17537
+ ];
17538
+ for (const dir of dirs) {
17539
+ const file = path18.join(dir, "rules", "hyv.md");
17540
+ if (fs19.existsSync(file))
17541
+ return file;
17542
+ }
17543
+ return null;
17544
+ }
17545
+ function opencodeAgentsPath() {
17546
+ return path18.join(HOME, ".config", "opencode", "AGENTS.md");
17547
+ }
17548
+ function readOpencodeHyv() {
17549
+ const file = path18.join(HOME, ".config", "opencode", "opencode.jsonc");
17550
+ if (!fs19.existsSync(file))
17551
+ return false;
17552
+ try {
17553
+ const raw = fs19.readFileSync(file, "utf-8").replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
17554
+ const cfg = JSON.parse(raw);
17555
+ return Boolean(cfg.mcp?.hyv);
17556
+ } catch {
17557
+ return false;
17558
+ }
17559
+ }
17533
17560
  function isOwnerOnlyFile(filePath) {
17534
17561
  try {
17535
17562
  if (!fs19.existsSync(filePath))
@@ -17551,7 +17578,7 @@ function readMcpHyv(configFile) {
17551
17578
  }
17552
17579
  }
17553
17580
  function registerDoctorCommand(program3) {
17554
- program3.command("doctor").description("Diagnose CLI health: engine, cache, auth, agents").option("--fix-agents", "Re-run agent config copy (idempotent)").action(async (opts) => {
17581
+ program3.command("doctor").description("Diagnose CLI health: engine, cache, auth, agents").option("--fix-agents", "Re-run agent config copy (idempotent)").option("--verify-hosts", "Spawn MCP using each host config and report pass/fail").action(async (opts) => {
17555
17582
  console.log(import_chalk18.default.bold("\nhold your voice \u2014 doctor\n"));
17556
17583
  let issues = 0;
17557
17584
  let fixed = 0;
@@ -17660,14 +17687,20 @@ function registerDoctorCommand(program3) {
17660
17687
  console.log(import_chalk18.default.dim(" run: rm ~/.cursor/rules/hyv.md (or hyv doctor --fix-agents)"));
17661
17688
  issues++;
17662
17689
  }
17690
+ const agMcpFile = path18.join(HOME, ".gemini", "config", "mcp_config.json");
17663
17691
  const agentChecks = [
17664
17692
  { name: "claude desktop mcp", ok: readMcpHyv(path18.join(claudeDesktopDir(), "claude_desktop_config.json")) },
17665
17693
  { name: "cursor mcp", ok: readMcpHyv(path18.join(HOME, ".cursor", "mcp.json")) },
17666
17694
  { name: "cursor rule", ok: fs19.existsSync(cursorRule) },
17695
+ { name: "antigravity mcp", ok: readMcpHyv(agMcpFile) },
17696
+ { name: "opencode mcp", ok: readOpencodeHyv() },
17697
+ { name: "opencode agents", ok: fs19.existsSync(opencodeAgentsPath()) && fs19.readFileSync(opencodeAgentsPath(), "utf-8").includes("hyv") },
17698
+ { name: "windsurf rule", ok: Boolean(windsurfRulePath()) },
17667
17699
  { name: "claude code command", ok: fs19.existsSync(path18.join(HOME, ".claude", "commands", "hyv.md")) },
17668
17700
  { name: "claude code skill", ok: fs19.existsSync(path18.join(HOME, ".claude", "skills", "hold-your-voice", "SKILL.md")) },
17669
17701
  { name: "codex agents", ok: fs19.existsSync(path18.join(HOME, ".codex", "AGENTS.md")) && fs19.readFileSync(path18.join(HOME, ".codex", "AGENTS.md"), "utf-8").includes("hyv") },
17670
- { name: "command code skill", ok: fs19.existsSync(path18.join(HOME, ".commandcode", "skills", "hyv", "SKILL.md")) }
17702
+ { name: "command code skill", ok: fs19.existsSync(path18.join(HOME, ".commandcode", "skills", "hyv", "SKILL.md")) },
17703
+ { name: "chatgpt connector guide", ok: fs19.existsSync(path18.join(HOME, ".chatgpt", "hyv-mcp-connector.txt")) }
17671
17704
  ];
17672
17705
  for (const agent of agentChecks) {
17673
17706
  if (agent.ok) {
@@ -17718,6 +17751,31 @@ function registerDoctorCommand(program3) {
17718
17751
  console.log(import_chalk18.default.red(` \u2717 mcp stdio probe failed: ${err.message}`));
17719
17752
  issues++;
17720
17753
  }
17754
+ if (opts.verifyHosts) {
17755
+ console.log(import_chalk18.default.dim("verifying mcp hosts (spawns subprocess per config)..."));
17756
+ try {
17757
+ const pkgDir = path18.resolve(__dirname, "..");
17758
+ const { verifyAgentHosts } = require(path18.join(pkgDir, "scripts", "verify-agent-hosts.js"));
17759
+ const hosts = await verifyAgentHosts();
17760
+ for (const host of hosts) {
17761
+ if (host.kind === "manual") {
17762
+ console.log(import_chalk18.default.yellow(` \u25CB ${host.label} \u2014 ${host.detail}`));
17763
+ continue;
17764
+ }
17765
+ if (host.ok) {
17766
+ console.log(import_chalk18.default.green(` \u2713 ${host.label}${host.detail ? ` (${host.detail})` : ""}`));
17767
+ } else if (host.kind === "file") {
17768
+ console.log(import_chalk18.default.dim(` - ${host.label} \u2014 ${host.detail || "missing"}`));
17769
+ } else {
17770
+ console.log(import_chalk18.default.red(` \u2717 ${host.label} \u2014 ${host.detail || "failed"}`));
17771
+ issues++;
17772
+ }
17773
+ }
17774
+ } catch (err) {
17775
+ console.log(import_chalk18.default.red(` \u2717 host verification failed: ${err.message}`));
17776
+ issues++;
17777
+ }
17778
+ }
17721
17779
  console.log("");
17722
17780
  if (issues === 0 && fixed === 0) {
17723
17781
  console.log(import_chalk18.default.green("\u2713 everything looks good!"));
@@ -18579,11 +18637,11 @@ function registerDemoCommand(program3) {
18579
18637
  // src/commands/open.ts
18580
18638
  var import_chalk29 = __toESM(require_source());
18581
18639
  var PAGES = {
18582
- dashboard: "https://holdyourvoice.com/app",
18583
- profiles: "https://holdyourvoice.com/app",
18584
- pricing: "https://holdyourvoice.com/app/billing",
18585
- settings: "https://holdyourvoice.com/app/settings",
18586
- billing: "https://holdyourvoice.com/app/billing"
18640
+ dashboard: "https://holdyourvoice.com/dashboard",
18641
+ profiles: "https://holdyourvoice.com/dashboard?tab=profiles",
18642
+ pricing: "https://holdyourvoice.com/dashboard?tab=billing",
18643
+ settings: "https://holdyourvoice.com/dashboard",
18644
+ billing: "https://holdyourvoice.com/dashboard?tab=billing"
18587
18645
  };
18588
18646
  function registerOpenCommand(program3) {
18589
18647
  program3.command("open").description("Open the web dashboard in your browser").option("--page <path>", "Page: dashboard, profiles, pricing, settings", "dashboard").option("--profile <name>", "Deep-link to a specific profile").option("--no-browser", "Print URL only, don't open").action(async (options) => {
@@ -19506,8 +19564,16 @@ function printMcpSetup() {
19506
19564
  console.log(import_chalk32.default.dim(" Instructions: ~/.codex/AGENTS.md (merged on install)\n"));
19507
19565
  console.log(import_chalk32.default.bold("Command Code"));
19508
19566
  console.log(import_chalk32.default.dim(" Skill: ~/.commandcode/skills/hyv/SKILL.md\n"));
19509
- console.log(import_chalk32.default.bold("ChatGPT"));
19567
+ console.log(import_chalk32.default.bold("ChatGPT Desktop (manual connector)"));
19568
+ console.log(import_chalk32.default.dim(" Settings \u2192 Connectors \u2192 add connector"));
19569
+ console.log(import_chalk32.default.dim(" Name: hold your voice | Command: hyv | Arguments: mcp"));
19570
+ console.log(import_chalk32.default.dim(" Guide: ~/.chatgpt/hyv-mcp-connector.txt (after hyv doctor --fix-agents)"));
19510
19571
  console.log(import_chalk32.default.dim(" hyv mcp --setup-chatgpt\n"));
19572
+ console.log(import_chalk32.default.bold("Antigravity"));
19573
+ console.log(import_chalk32.default.dim(" MCP: ~/.gemini/config/mcp_config.json \u2192 mcpServers.hyv (auto via postinstall)\n"));
19574
+ console.log(import_chalk32.default.bold("OpenCode"));
19575
+ console.log(import_chalk32.default.dim(" MCP: ~/.config/opencode/opencode.jsonc \u2192 mcp.hyv (auto via postinstall)"));
19576
+ console.log(import_chalk32.default.dim(" Rules: ~/.config/opencode/AGENTS.md\n"));
19511
19577
  console.log(import_chalk32.default.bold("Auto-configure"));
19512
19578
  console.log(import_chalk32.default.dim(" hyv doctor --fix-agents"));
19513
19579
  console.log(import_chalk32.default.dim(" HYV_AUTO_CONFIGURE_AGENTS=0 npm i -g @holdyourvoice/hyv (skip)\n"));
@@ -19680,7 +19746,7 @@ async function exportCommand(format, opts) {
19680
19746
  } else {
19681
19747
  console.log(`
19682
19748
  ${c.red("\u2717")} no voice profiles found`);
19683
- console.log(` ${c.dim("create one at")} ${c.cyan("https://holdyourvoice.com/app")}`);
19749
+ console.log(` ${c.dim("create one at")} ${c.cyan("https://holdyourvoice.com/dashboard")}`);
19684
19750
  }
19685
19751
  process.exit(1);
19686
19752
  return;
@@ -19780,15 +19846,24 @@ program2.command("mcp").description("Start MCP server (for Claude Desktop and ot
19780
19846
  return;
19781
19847
  }
19782
19848
  if (opts.setupChatgpt) {
19783
- console.log(import_chalk33.default.bold("\nhold your voice \u2014 chatgpt setup\n"));
19784
- console.log("To connect HYV to ChatGPT:");
19785
- console.log(import_chalk33.default.dim(" 1. Go to ") + import_chalk33.default.cyan("https://chatgpt.com/#settings/Connectors"));
19786
- console.log(import_chalk33.default.dim(" 2. Add a new connector"));
19787
- console.log(import_chalk33.default.dim(" 3. For local MCP, use: ") + import_chalk33.default.cyan("hyv mcp"));
19788
- console.log(import_chalk33.default.dim(" 4. ChatGPT Desktop supports stdio MCP servers"));
19849
+ const home = require("os").homedir();
19850
+ const guide = require("path").join(home, ".chatgpt", "hyv-mcp-connector.txt");
19851
+ console.log(import_chalk33.default.bold("\nhold your voice \u2014 chatgpt desktop mcp setup\n"));
19852
+ console.log("ChatGPT has no auto-config file. Add this connector once in the desktop app:\n");
19853
+ console.log(import_chalk33.default.dim(" 1. Install hyv: ") + import_chalk33.default.cyan("npm i -g @holdyourvoice/hyv@latest && hyv welcome"));
19854
+ console.log(import_chalk33.default.dim(" 2. Open ") + import_chalk33.default.cyan("https://chatgpt.com/#settings/Connectors"));
19855
+ console.log(import_chalk33.default.dim(" 3. Add connector"));
19856
+ console.log(import_chalk33.default.dim(" 4. Name: ") + import_chalk33.default.cyan("hold your voice"));
19857
+ console.log(import_chalk33.default.dim(" 5. Command: ") + import_chalk33.default.cyan("hyv"));
19858
+ console.log(import_chalk33.default.dim(" 6. Arguments: ") + import_chalk33.default.cyan("mcp"));
19859
+ console.log(import_chalk33.default.dim(" 7. Save, restart ChatGPT Desktop, ask: scan this with hold your voice"));
19789
19860
  console.log("");
19790
- console.log(import_chalk33.default.dim("Note: The remote HTTP MCP endpoint is not yet available."));
19791
- console.log(import_chalk33.default.dim("Use the local stdio MCP server with Claude Desktop or Claude Code instead."));
19861
+ if (require("fs").existsSync(guide)) {
19862
+ console.log(import_chalk33.default.dim(`Full guide written to: ${guide}`));
19863
+ } else {
19864
+ console.log(import_chalk33.default.dim("Run hyv doctor --fix-agents to write ~/.chatgpt/hyv-mcp-connector.txt"));
19865
+ }
19866
+ console.log(import_chalk33.default.dim("\nBrowser chatgpt cannot use local MCP \u2014 desktop app only."));
19792
19867
  return;
19793
19868
  }
19794
19869
  startMcpServer();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holdyourvoice/hyv",
3
- "version": "2.9.11",
3
+ "version": "2.9.12",
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": {
@@ -59,6 +59,8 @@
59
59
  "dist/",
60
60
  "scripts/postinstall.js",
61
61
  "scripts/postinstall-lib.js",
62
+ "scripts/install.sh",
63
+ "scripts/install.ps1",
62
64
  "scripts/check-no-duplicates.js",
63
65
  "assets/",
64
66
  "skills/",
@@ -0,0 +1,89 @@
1
+ # hold your voice — install node (if needed) + @holdyourvoice/hyv
2
+ param(
3
+ [switch]$EnsureNodeOnly
4
+ )
5
+
6
+ $ErrorActionPreference = 'Stop'
7
+
8
+ $HyvPkg = '@holdyourvoice/hyv@latest'
9
+ $MinNodeMajor = 18
10
+
11
+ function Write-Log([string]$Message) { Write-Host " $Message" }
12
+
13
+ function Test-NodeOk {
14
+ if (-not (Get-Command node -ErrorAction SilentlyContinue)) { return $false }
15
+ $major = [int](node -p "parseInt(process.versions.node.split('.')[0], 10)")
16
+ return $major -ge $MinNodeMajor
17
+ }
18
+
19
+ function Refresh-Path {
20
+ $machine = [Environment]::GetEnvironmentVariable('Path', 'Machine')
21
+ $user = [Environment]::GetEnvironmentVariable('Path', 'User')
22
+ if ($machine -or $user) {
23
+ $env:Path = @($machine, $user) -join ';'
24
+ }
25
+ }
26
+
27
+ function Install-Node {
28
+ if (Get-Command winget -ErrorAction SilentlyContinue) {
29
+ Write-Log 'installing node via winget...'
30
+ winget install OpenJS.NodeJS.LTS --accept-package-agreements --accept-source-agreements
31
+ Refresh-Path
32
+ return
33
+ }
34
+ if (Get-Command choco -ErrorAction SilentlyContinue) {
35
+ Write-Log 'installing node via chocolatey...'
36
+ choco install nodejs-lts -y
37
+ Refresh-Path
38
+ return
39
+ }
40
+ $lts = '22.16.0'
41
+ $msi = "node-v$lts-x64.msi"
42
+ $url = "https://nodejs.org/dist/v$lts/$msi"
43
+ $tmp = Join-Path $env:TEMP "hyv-node-$msi"
44
+ Write-Log "downloading node $lts..."
45
+ Invoke-WebRequest -Uri $url -OutFile $tmp -UseBasicParsing
46
+ Write-Log 'installing node...'
47
+ Start-Process msiexec.exe -ArgumentList "/i `"$tmp`" /qn" -Wait
48
+ Refresh-Path
49
+ }
50
+
51
+ function Ensure-Node {
52
+ Refresh-Path
53
+ if (Test-NodeOk) {
54
+ Write-Log "node $(node -v) found"
55
+ return
56
+ }
57
+ Write-Log "node $MinNodeMajor+ not found — installing automatically..."
58
+ Install-Node
59
+ Refresh-Path
60
+ if (-not (Test-NodeOk)) {
61
+ throw 'node install finished but node is still not on PATH — open a new terminal and run this script again'
62
+ }
63
+ Write-Log "node $(node -v) ready"
64
+ }
65
+
66
+ function Install-Hyv {
67
+ if (-not (Get-Command npm -ErrorAction SilentlyContinue)) {
68
+ throw 'npm not found after node install'
69
+ }
70
+ Write-Log 'installing hold your voice...'
71
+ npm i -g $HyvPkg
72
+ Refresh-Path
73
+ if (Get-Command hyv -ErrorAction SilentlyContinue) {
74
+ Write-Log 'running hyv welcome...'
75
+ hyv welcome
76
+ } else {
77
+ Write-Host ' ! hyv installed but not on PATH — open a new terminal and run: hyv welcome' -ForegroundColor Yellow
78
+ }
79
+ }
80
+
81
+ Write-Host ''
82
+ Write-Host 'hold your voice — installer'
83
+ Write-Host ''
84
+ Ensure-Node
85
+ if ($EnsureNodeOnly) { exit 0 }
86
+ Install-Hyv
87
+ Write-Host ''
88
+ Write-Host ' ✓ done — hyv is ready'
89
+ Write-Host ''
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env bash
2
+ # hold your voice — install node (if needed) + @holdyourvoice/hyv
3
+ set -euo pipefail
4
+
5
+ HYV_PKG="@holdyourvoice/hyv@latest"
6
+ MIN_NODE_MAJOR=18
7
+ NODE_LTS="22.16.0"
8
+ ENSURE_NODE_ONLY=0
9
+
10
+ for arg in "$@"; do
11
+ case "$arg" in
12
+ --ensure-node-only) ENSURE_NODE_ONLY=1 ;;
13
+ esac
14
+ done
15
+
16
+ log() { printf ' %s\n' "$*"; }
17
+ warn() { printf ' ! %s\n' "$*" >&2; }
18
+
19
+ node_major() {
20
+ node -p "parseInt(process.versions.node.split('.')[0], 10)" 2>/dev/null || echo 0
21
+ }
22
+
23
+ node_ok() {
24
+ command -v node >/dev/null 2>&1 || return 1
25
+ [[ "$(node_major)" -ge "$MIN_NODE_MAJOR" ]]
26
+ }
27
+
28
+ refresh_path() {
29
+ export PATH="/usr/local/bin:/opt/homebrew/bin:${HOME}/.local/share/fnm:${PATH}"
30
+ hash -r 2>/dev/null || true
31
+ }
32
+
33
+ install_node_fnm() {
34
+ export FNM_DIR="${FNM_DIR:-${HOME}/.local/share/fnm}"
35
+ export FNM_VERSION="${FNM_VERSION:-v1.38.1}"
36
+ mkdir -p "$FNM_DIR"
37
+ if ! command -v fnm >/dev/null 2>&1; then
38
+ log "installing fnm (user-level node manager)..."
39
+ curl -fsSL https://fnm.vercel.app/install | bash -s -- --install-no-use
40
+ fi
41
+ refresh_path
42
+ if ! command -v fnm >/dev/null 2>&1 && [[ -x "${FNM_DIR}/fnm" ]]; then
43
+ export PATH="${FNM_DIR}:${PATH}"
44
+ fi
45
+ command -v fnm >/dev/null 2>&1 || return 1
46
+ eval "$(fnm env)"
47
+ fnm install "$NODE_LTS"
48
+ fnm default "$NODE_LTS"
49
+ eval "$(fnm env)"
50
+ }
51
+
52
+ install_node_mac() {
53
+ if command -v brew >/dev/null 2>&1; then
54
+ log "installing node via homebrew..."
55
+ brew install node
56
+ refresh_path
57
+ return 0
58
+ fi
59
+ if install_node_fnm; then
60
+ refresh_path
61
+ return 0
62
+ fi
63
+ local arch pkg
64
+ arch="$(uname -m)"
65
+ case "$arch" in
66
+ arm64) pkg="node-v${NODE_LTS}.pkg" ;;
67
+ x86_64) pkg="node-v${NODE_LTS}.pkg" ;;
68
+ *) warn "unsupported mac architecture: $arch"; return 1 ;;
69
+ esac
70
+ local url="https://nodejs.org/dist/v${NODE_LTS}/${pkg}"
71
+ local tmp="/tmp/hyv-node-v${NODE_LTS}.pkg"
72
+ log "downloading node ${NODE_LTS}..."
73
+ curl -fsSL "$url" -o "$tmp"
74
+ log "installing node (may ask for your password)..."
75
+ sudo installer -pkg "$tmp" -target /
76
+ refresh_path
77
+ }
78
+
79
+ install_node_linux() {
80
+ if install_node_fnm; then
81
+ refresh_path
82
+ return 0
83
+ fi
84
+ if command -v apt-get >/dev/null 2>&1; then
85
+ log "installing node via apt (nodesource)..."
86
+ curl -fsSL "https://deb.nodesource.com/setup_${NODE_LTS%%.*}.x" | sudo -E bash -
87
+ sudo apt-get install -y nodejs
88
+ refresh_path
89
+ return 0
90
+ fi
91
+ if command -v dnf >/dev/null 2>&1; then
92
+ log "installing node via dnf..."
93
+ sudo dnf install -y nodejs npm
94
+ refresh_path
95
+ return 0
96
+ fi
97
+ warn "could not auto-install node on this linux distro — install node ${MIN_NODE_MAJOR}+ manually"
98
+ return 1
99
+ }
100
+
101
+ install_node() {
102
+ case "$(uname -s)" in
103
+ Darwin) install_node_mac ;;
104
+ Linux) install_node_linux ;;
105
+ *)
106
+ warn "unsupported platform for auto node install"
107
+ return 1
108
+ ;;
109
+ esac
110
+ }
111
+
112
+ ensure_node() {
113
+ refresh_path
114
+ if node_ok; then
115
+ log "node $(node -v) found"
116
+ return 0
117
+ fi
118
+ log "node ${MIN_NODE_MAJOR}+ not found — installing automatically..."
119
+ install_node
120
+ refresh_path
121
+ if node_ok; then
122
+ log "node $(node -v) ready"
123
+ return 0
124
+ fi
125
+ warn "node install finished but node is still not on PATH — open a new terminal and run this script again"
126
+ return 1
127
+ }
128
+
129
+ install_hyv() {
130
+ if ! command -v npm >/dev/null 2>&1; then
131
+ warn "npm not found after node install"
132
+ return 1
133
+ fi
134
+ log "installing hold your voice..."
135
+ npm i -g "$HYV_PKG"
136
+ refresh_path
137
+ if command -v hyv >/dev/null 2>&1; then
138
+ log "running hyv welcome..."
139
+ hyv welcome
140
+ else
141
+ warn "hyv installed but not on PATH — open a new terminal and run: hyv welcome"
142
+ fi
143
+ }
144
+
145
+ main() {
146
+ printf '\nhold your voice — installer\n\n'
147
+ ensure_node
148
+ if [[ "$ENSURE_NODE_ONLY" -eq 1 ]]; then
149
+ exit 0
150
+ fi
151
+ install_hyv
152
+ printf '\n ✓ done — hyv is ready\n\n'
153
+ }
154
+
155
+ main "$@"
@@ -83,16 +83,79 @@ function resolveHyvMcpCommand(pkgDir) {
83
83
  return { command: 'hyv', args: ['mcp'] };
84
84
  }
85
85
 
86
- function mergeJsonConfigFile(configFile, mutator, { backup = true } = {}) {
86
+ /** OpenCode local MCP uses a command array: [node, entry, mcp] */
87
+ function resolveHyvMcpCommandArray(pkgDir) {
88
+ const { command, args } = resolveHyvMcpCommand(pkgDir);
89
+ return [command, ...args];
90
+ }
91
+
92
+ function parseJsonc(text) {
93
+ const noBlock = text.replace(/\/\*[\s\S]*?\*\//g, '');
94
+ const noLine = noBlock.replace(/^\s*\/\/.*$/gm, '');
95
+ return JSON.parse(noLine);
96
+ }
97
+
98
+ function stringifyJsonc(config, originalText) {
99
+ const usesTrailingComma = /,\s*[}\]]/.test(originalText || '');
100
+ const space = (originalText || '').includes(': ') ? 2 : 2;
101
+ let out = JSON.stringify(config, null, space);
102
+ if (usesTrailingComma) {
103
+ out = out.replace(/,\n(\s*[}\]])/g, ',\n$1');
104
+ }
105
+ return out.endsWith('\n') ? out : `${out}\n`;
106
+ }
107
+
108
+ function readJsonObjectFromFile(file, parser) {
109
+ if (!fs.existsSync(file)) return { config: {}, original: '' };
110
+ const original = fs.readFileSync(file, 'utf-8');
111
+ if (!original.trim()) return { config: {}, original };
112
+ try {
113
+ const parsed = parser(original);
114
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
115
+ return { ok: false, reason: 'parse-error', error: 'expected json object' };
116
+ }
117
+ return { config: parsed, original };
118
+ } catch (err) {
119
+ return { ok: false, reason: 'parse-error', error: err.message };
120
+ }
121
+ }
122
+
123
+ function mergeJsoncConfigFile(configFile, mutator, { backup = true } = {}) {
87
124
  fs.mkdirSync(path.dirname(configFile), { recursive: true });
88
- let config = {};
89
- if (fs.existsSync(configFile)) {
125
+ const read = readJsonObjectFromFile(configFile, parseJsonc);
126
+ if (read.ok === false) return read;
127
+ let { config, original } = read;
128
+ const next = mutator(config) || config;
129
+ if (backup && fs.existsSync(configFile)) {
90
130
  try {
91
- config = JSON.parse(fs.readFileSync(configFile, 'utf-8'));
92
- } catch (err) {
93
- return { ok: false, reason: 'parse-error', error: err.message };
131
+ fs.copyFileSync(configFile, `${configFile}.hyv.bak`);
132
+ } catch {
133
+ // non-fatal
94
134
  }
95
135
  }
136
+ fs.writeFileSync(configFile, original ? stringifyJsonc(next, original) : `${JSON.stringify(next, null, 2)}\n`);
137
+ return { ok: true };
138
+ }
139
+
140
+ function antigravityMcpConfigPath(home, isWin) {
141
+ return path.join(home, '.gemini', 'config', 'mcp_config.json');
142
+ }
143
+
144
+ function opencodeConfigDir(home, isWin) {
145
+ if (isWin) return path.join(home, '.config', 'opencode');
146
+ if (process.platform === 'linux') return path.join(home, '.config', 'opencode');
147
+ return path.join(home, '.config', 'opencode');
148
+ }
149
+
150
+ function chatgptHelperDir(home) {
151
+ return path.join(home, '.chatgpt');
152
+ }
153
+
154
+ function mergeJsonConfigFile(configFile, mutator, { backup = true } = {}) {
155
+ fs.mkdirSync(path.dirname(configFile), { recursive: true });
156
+ const read = readJsonObjectFromFile(configFile, (text) => JSON.parse(text));
157
+ if (read.ok === false) return read;
158
+ let { config, original } = read;
96
159
  const next = mutator(config) || config;
97
160
  if (backup && fs.existsSync(configFile)) {
98
161
  try {
@@ -101,7 +164,7 @@ function mergeJsonConfigFile(configFile, mutator, { backup = true } = {}) {
101
164
  // non-fatal
102
165
  }
103
166
  }
104
- fs.writeFileSync(configFile, JSON.stringify(next, null, 2));
167
+ fs.writeFileSync(configFile, `${JSON.stringify(next, null, 2)}\n`);
105
168
  return { ok: true };
106
169
  }
107
170
 
@@ -274,6 +337,93 @@ function setupAgents({ pkgDir, home = require('os').homedir(), quiet = false })
274
337
  warnings.push(`command code: ${err.message}`);
275
338
  }
276
339
 
340
+ // Antigravity — ~/.gemini/config/mcp_config.json (absolute paths for GUI launch)
341
+ try {
342
+ const agFile = antigravityMcpConfigPath(home, isWin);
343
+ const mcpCmd = resolveHyvMcpCommand(pkgDir);
344
+ const result = mergeJsonConfigFile(agFile, (config) => {
345
+ if (!config.mcpServers) config.mcpServers = {};
346
+ if (!config.mcpServers.hyv) {
347
+ config.mcpServers.hyv = { command: mcpCmd.command, args: mcpCmd.args };
348
+ configured.push('antigravity mcp');
349
+ }
350
+ return config;
351
+ });
352
+ if (!result.ok) warnings.push(`antigravity: could not update mcp config (${result.reason})`);
353
+ } catch (err) {
354
+ warnings.push(`antigravity: ${err.message}`);
355
+ }
356
+
357
+ // OpenCode — ~/.config/opencode/opencode.jsonc + AGENTS.md
358
+ try {
359
+ const ocDir = opencodeConfigDir(home, isWin);
360
+ const ocFile = path.join(ocDir, 'opencode.jsonc');
361
+ const cmdArr = resolveHyvMcpCommandArray(pkgDir);
362
+ const ocResult = mergeJsoncConfigFile(ocFile, (config) => {
363
+ if (!config.mcp) config.mcp = {};
364
+ if (!config.mcp.hyv) {
365
+ config.mcp.hyv = { type: 'local', command: cmdArr, enabled: true };
366
+ configured.push('opencode mcp');
367
+ }
368
+ return config;
369
+ });
370
+ if (!ocResult.ok) warnings.push(`opencode: could not update opencode.jsonc (${ocResult.reason})`);
371
+
372
+ const agentsFile = path.join(ocDir, 'AGENTS.md');
373
+ const genericSrc = path.join(pkgDir, 'agents', 'generic.md');
374
+ let existing = '';
375
+ if (fs.existsSync(agentsFile)) existing = fs.readFileSync(agentsFile, 'utf-8');
376
+ const addition = fs.existsSync(genericSrc) ? fs.readFileSync(genericSrc, 'utf-8') : '';
377
+ if (addition && shouldUpgradeAgent(agentsFile, agentsMarker, pkgVersion)) {
378
+ const merged = mergeAgentsMd(existing, addition, 'hold-your-voice');
379
+ fs.mkdirSync(ocDir, { recursive: true });
380
+ fs.writeFileSync(agentsFile, merged);
381
+ configured.push('opencode agents');
382
+ } else if (!fs.existsSync(agentsFile) && addition) {
383
+ fs.mkdirSync(ocDir, { recursive: true });
384
+ fs.writeFileSync(agentsFile, `${addition.trim()}\n`);
385
+ configured.push('opencode agents');
386
+ }
387
+ } catch (err) {
388
+ warnings.push(`opencode: ${err.message}`);
389
+ }
390
+
391
+ // ChatGPT — connector helper (UI has no config file; write exact values for manual add)
392
+ try {
393
+ const cgDir = chatgptHelperDir(home);
394
+ fs.mkdirSync(cgDir, { recursive: true });
395
+ const mcpCmd = resolveHyvMcpCommand(pkgDir);
396
+ const connectorGuide = [
397
+ 'hold your voice — chatgpt desktop mcp connector',
398
+ '',
399
+ 'chatgpt has no auto-config file. add this connector once in the chatgpt desktop app:',
400
+ '',
401
+ '1. open chatgpt desktop (not the browser)',
402
+ '2. settings → connectors → add connector',
403
+ '3. name: hold your voice',
404
+ '4. command: hyv',
405
+ '5. arguments: mcp',
406
+ '',
407
+ 'if the connector fails to start, use absolute paths instead:',
408
+ ` command: ${mcpCmd.command}`,
409
+ ` arguments: ${mcpCmd.args.join(' ')}`,
410
+ '',
411
+ 'then restart chatgpt desktop and ask: scan this with hold your voice',
412
+ '',
413
+ 'more help: hyv mcp --setup-chatgpt',
414
+ '',
415
+ ].join('\n');
416
+ const guideFile = path.join(cgDir, 'hyv-mcp-connector.txt');
417
+ fs.writeFileSync(guideFile, connectorGuide);
418
+ configured.push('chatgpt connector guide');
419
+
420
+ const chatgptSrc = path.join(pkgDir, 'agents', 'chatgpt.md');
421
+ const instrFile = path.join(cgDir, 'hyv-instructions.txt');
422
+ if (fs.existsSync(chatgptSrc)) installAgent(chatgptSrc, instrFile);
423
+ } catch (err) {
424
+ warnings.push(`chatgpt: ${err.message}`);
425
+ }
426
+
277
427
  // Generic reference copy
278
428
  try {
279
429
  const genericSrc = path.join(pkgDir, 'agents', 'generic.md');
@@ -300,9 +450,15 @@ module.exports = {
300
450
  hasCompletedOnboarding,
301
451
  markOnboardingComplete,
302
452
  resolveHyvMcpCommand,
453
+ resolveHyvMcpCommandArray,
454
+ parseJsonc,
303
455
  mergeJsonConfigFile,
456
+ mergeJsoncConfigFile,
304
457
  toCursorMdc,
305
458
  toWindsurfRule,
306
459
  setupAgents,
307
460
  claudeDesktopDir,
461
+ antigravityMcpConfigPath,
462
+ opencodeConfigDir,
463
+ chatgptHelperDir,
308
464
  };
@@ -19,6 +19,7 @@ function print(msg) {
19
19
  print('');
20
20
  print(' ✓ hold your voice installed — free local scan ready');
21
21
  print(' next: hyv welcome | hyv scan draft.md | hyv mcp --setup');
22
+ print(' no node yet? curl -fsSL https://holdyourvoice.com/install.sh | bash');
22
23
  print('');
23
24
 
24
25
  const { configured, warnings } = setupAgents({ pkgDir, quiet });