@holdyourvoice/hyv 2.9.10 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holdyourvoice/hyv",
3
- "version": "2.9.10",
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 });