@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 +12 -0
- package/dist/index.js +100 -25
- package/package.json +3 -1
- package/scripts/install.ps1 +89 -0
- package/scripts/install.sh +155 -0
- package/scripts/postinstall-lib.js +163 -7
- package/scripts/postinstall.js +1 -0
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/
|
|
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/
|
|
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 || "/
|
|
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/
|
|
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: "/
|
|
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/
|
|
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: "/
|
|
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/
|
|
18583
|
-
profiles: "https://holdyourvoice.com/
|
|
18584
|
-
pricing: "https://holdyourvoice.com/
|
|
18585
|
-
settings: "https://holdyourvoice.com/
|
|
18586
|
-
billing: "https://holdyourvoice.com/
|
|
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/
|
|
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
|
-
|
|
19784
|
-
|
|
19785
|
-
console.log(import_chalk33.default.
|
|
19786
|
-
console.log(
|
|
19787
|
-
console.log(import_chalk33.default.dim("
|
|
19788
|
-
console.log(import_chalk33.default.dim("
|
|
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
|
-
|
|
19791
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
if (
|
|
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
|
-
|
|
92
|
-
} catch
|
|
93
|
-
|
|
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
|
};
|
package/scripts/postinstall.js
CHANGED
|
@@ -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 });
|