@holdyourvoice/hyv 2.9.2 → 2.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/index.js +70 -100
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  All notable CLI changes. Also mirrored to [holdyourvoice.com/changelog](https://holdyourvoice.com/changelog) for user-facing releases.
4
4
 
5
+ ## [2.9.4] — 2026-06-12
6
+
7
+ ### Fixed
8
+ - Welcome step 4 — one sign-in only; opens dashboard billing tab (no second Google login on marketing site)
9
+ - `hyv plan --upgrade` opens authenticated dashboard billing instead of public pricing page
10
+
11
+ ### Changed
12
+ - Step 4 copy tightened; spinners while account + profile sync run
13
+
14
+ ## [2.9.3] — 2026-06-12
15
+
16
+ ### Fixed
17
+ - Browser signup OAuth — clearer error when Google denies; server fix for `redirect_uri_mismatch` (requires API deploy)
18
+
5
19
  ## [2.9.2] — 2026-06-12
6
20
 
7
21
  ### Changed
package/dist/index.js CHANGED
@@ -4612,40 +4612,6 @@ var require_source = __commonJS({
4612
4612
  });
4613
4613
 
4614
4614
  // src/lib/config.ts
4615
- var config_exports = {};
4616
- __export(config_exports, {
4617
- API_BASE: () => API_BASE,
4618
- AUTH_FILE: () => AUTH_FILE,
4619
- CACHE_DIR: () => CACHE_DIR,
4620
- CONFIG_FILE: () => CONFIG_FILE,
4621
- HYV_DIR: () => HYV_DIR,
4622
- LAST_SESSION_FILE: () => LAST_SESSION_FILE,
4623
- PROFILES_DIR: () => PROFILES_DIR,
4624
- QUEUE_DIR: () => QUEUE_DIR,
4625
- appendSecureLine: () => appendSecureLine,
4626
- assertSafeOAuthUrl: () => assertSafeOAuthUrl,
4627
- assertSafeOpenUrl: () => assertSafeOpenUrl,
4628
- assertSafeProfileName: () => assertSafeProfileName,
4629
- clearAuth: () => clearAuth,
4630
- clearQueuedSignals: () => clearQueuedSignals,
4631
- cliApiUrl: () => cliApiUrl,
4632
- ensureHyvDir: () => ensureHyvDir,
4633
- getQueuedSignals: () => getQueuedSignals,
4634
- getToken: () => getToken,
4635
- isInitialized: () => isInitialized,
4636
- listCachedProfiles: () => listCachedProfiles,
4637
- profilePathForName: () => profilePathForName,
4638
- queueSignal: () => queueSignal,
4639
- readAuth: () => readAuth,
4640
- readCachedProfile: () => readCachedProfile,
4641
- readConfig: () => readConfig,
4642
- readLastEditSession: () => readLastEditSession,
4643
- saveLastEditSession: () => saveLastEditSession,
4644
- writeAuth: () => writeAuth,
4645
- writeCachedProfile: () => writeCachedProfile,
4646
- writeConfig: () => writeConfig,
4647
- writeSecureFile: () => writeSecureFile
4648
- });
4649
4615
  function validateApiBase(raw) {
4650
4616
  const trimmed = raw.replace(/\/$/, "");
4651
4617
  let parsed;
@@ -4666,7 +4632,7 @@ function cliApiUrl(apiPath) {
4666
4632
  const p = apiPath.startsWith("/") ? apiPath : `/${apiPath}`;
4667
4633
  return `${API_BASE}${p}`;
4668
4634
  }
4669
- function assertSafeOpenUrl(url) {
4635
+ function assertSafeOpenUrl2(url) {
4670
4636
  let parsed;
4671
4637
  try {
4672
4638
  parsed = new URL(url);
@@ -4737,10 +4703,6 @@ function ensureHyvDir() {
4737
4703
  }
4738
4704
  }
4739
4705
  }
4740
- function writeSecureFile(filePath, content) {
4741
- ensureHyvDir();
4742
- fs.writeFileSync(filePath, content, { mode: 384 });
4743
- }
4744
4706
  function appendSecureLine(filePath, line, dir) {
4745
4707
  if (dir) {
4746
4708
  if (!fs.existsSync(dir))
@@ -4779,11 +4741,6 @@ function writeAuth(auth) {
4779
4741
  ensureHyvDir();
4780
4742
  fs.writeFileSync(AUTH_FILE, JSON.stringify(auth, null, 2), { mode: 384 });
4781
4743
  }
4782
- function clearAuth() {
4783
- if (fs.existsSync(AUTH_FILE)) {
4784
- fs.unlinkSync(AUTH_FILE);
4785
- }
4786
- }
4787
4744
  function readConfig() {
4788
4745
  try {
4789
4746
  if (!fs.existsSync(CONFIG_FILE))
@@ -4854,17 +4811,6 @@ function queueSignal(signal) {
4854
4811
  const filePath = path.join(QUEUE_DIR, `${id}.json`);
4855
4812
  fs.writeFileSync(filePath, JSON.stringify(signal, null, 2), { mode: 384 });
4856
4813
  }
4857
- function clearQueuedSignals() {
4858
- try {
4859
- if (!fs.existsSync(QUEUE_DIR))
4860
- return;
4861
- const files = fs.readdirSync(QUEUE_DIR).filter((f) => f.endsWith(".json"));
4862
- for (const f of files) {
4863
- fs.unlinkSync(path.join(QUEUE_DIR, f));
4864
- }
4865
- } catch {
4866
- }
4867
- }
4868
4814
  function saveLastEditSession(session) {
4869
4815
  ensureHyvDir();
4870
4816
  const data = { ...session, saved_at: (/* @__PURE__ */ new Date()).toISOString() };
@@ -5355,6 +5301,7 @@ __export(auth_exports, {
5355
5301
  authenticatedRequest: () => authenticatedRequest,
5356
5302
  checkSession: () => checkSession,
5357
5303
  getValidToken: () => getValidToken,
5304
+ openAuthenticatedDashboard: () => openAuthenticatedDashboard,
5358
5305
  refreshToken: () => refreshToken,
5359
5306
  verifyOAuthState: () => verifyOAuthState
5360
5307
  });
@@ -5479,6 +5426,15 @@ async function authenticateWithBrowser() {
5479
5426
  if (url.pathname === "/callback") {
5480
5427
  const code = url.searchParams.get("code");
5481
5428
  const state = url.searchParams.get("state");
5429
+ const oauthError = url.searchParams.get("error");
5430
+ if (oauthError) {
5431
+ res.writeHead(400, { "Content-Type": "text/html" });
5432
+ res.end(`<h1>Authentication failed</h1><p>${escapeHtml(oauthError)}</p>`);
5433
+ clearTimeout(timeout);
5434
+ server.close();
5435
+ reject(new Error(`Google sign-in failed: ${oauthError}`));
5436
+ return;
5437
+ }
5482
5438
  if (!code || !state) {
5483
5439
  res.writeHead(400, { "Content-Type": "text/html" });
5484
5440
  res.end("<h1>Authentication failed</h1><p>Missing code or state.</p>");
@@ -5534,6 +5490,23 @@ async function authenticateWithBrowser() {
5534
5490
  writeAuth(authData);
5535
5491
  return authData;
5536
5492
  }
5493
+ async function openAuthenticatedDashboard(opts = {}) {
5494
+ const response = await authenticatedRequest(cliApiUrl("/cli/auth/web-handoff"), {
5495
+ method: "POST",
5496
+ body: {
5497
+ next: opts.next || "/dashboard",
5498
+ tab: opts.tab || "billing"
5499
+ }
5500
+ });
5501
+ if (response.status !== 200) {
5502
+ throw new Error("Could not open dashboard \u2014 try https://holdyourvoice.com/dashboard");
5503
+ }
5504
+ const { url } = response.data;
5505
+ if (!url) {
5506
+ throw new Error("Dashboard handoff URL missing");
5507
+ }
5508
+ await (0, import_open.default)(assertSafeOpenUrl(url));
5509
+ }
5537
5510
  async function refreshToken(tokenOverride) {
5538
5511
  const token = tokenOverride || readAuth()?.token;
5539
5512
  if (!token)
@@ -5625,7 +5598,7 @@ function printFreePaidMatrix(opts = {}) {
5625
5598
  First month $1 \u2192 ${PRICING_URL}`));
5626
5599
  console.log(import_chalk2.default.dim(" hyv init | hyv plan --upgrade\n"));
5627
5600
  }
5628
- var import_chalk2, NPX_EXAMPLES, FREE_CLI_COMMANDS, PAID_FEATURES, FREE_WEB_TOOLS, COMMUNITY_URL, PRICING_URL;
5601
+ var import_chalk2, NPX_EXAMPLES, FREE_CLI_COMMANDS, PAID_FEATURES, FREE_WEB_TOOLS, COMMUNITY_URL, PRICING_URL, DASHBOARD_BILLING_URL;
5629
5602
  var init_free_paid = __esm({
5630
5603
  "src/lib/free-paid.ts"() {
5631
5604
  "use strict";
@@ -5673,6 +5646,7 @@ var init_free_paid = __esm({
5673
5646
  ];
5674
5647
  COMMUNITY_URL = "https://holdyourvoice.com/community";
5675
5648
  PRICING_URL = "https://holdyourvoice.com/#pricing";
5649
+ DASHBOARD_BILLING_URL = "https://holdyourvoice.com/dashboard?tab=billing";
5676
5650
  }
5677
5651
  });
5678
5652
 
@@ -11443,12 +11417,12 @@ function buildStepGuide(step, profileName) {
11443
11417
  return [
11444
11418
  "### Step 4 \u2014 save & unlock",
11445
11419
  "",
11446
- "Explain warmly: their profile is local-only until they sign up. A free account backs it up and syncs across machines.",
11420
+ "Explain warmly: local profile until signup. One browser sign-in via CLI \u2014 then open dashboard billing (no second login).",
11447
11421
  "Paid unlock is **$1 for the first month** \u2014 profiles that learn, hybrid rewrite, dashboard.",
11448
11422
  "Scanning/fix/MCP stay free forever without an account.",
11449
11423
  "",
11450
- "1. `hyv init` \u2014 browser signup",
11451
- "2. `hyv plan --upgrade` \u2014 $1 first month",
11424
+ "1. `hyv init` or welcome step 4 \u2014 single Google sign-in from terminal",
11425
+ "2. Dashboard opens on billing tab \u2014 user picks plan there",
11452
11426
  "",
11453
11427
  `Pricing: ${PRICING_URL}`
11454
11428
  ].join("\n");
@@ -11775,14 +11749,13 @@ async function stepTest(profileName) {
11775
11749
  }
11776
11750
  async function stepSignup(profileName) {
11777
11751
  console.log(import_chalk12.default.bold("\nstep 4 \xB7 save & unlock\n"));
11778
- console.log(" your voice profile is saved locally \u2014 that's enough to scan and test.");
11779
- console.log(" but it won't follow you to another machine, and it won't get sharper");
11780
- console.log(" every time you rewrite.\n");
11781
- console.log(" a free account backs up your profile and syncs it everywhere you use hyv.");
11782
- console.log(" upgrade when you're ready: " + import_chalk12.default.bold("$1 for your first month") + ", then full");
11783
- console.log(" access to profiles that learn, hybrid rewrite, and your dashboard.\n");
11752
+ console.log(" your profile works on this machine. scan anything, anytime \u2014 free forever.");
11753
+ console.log("");
11754
+ console.log(" create a free account to back it up and sync everywhere.");
11755
+ console.log(" then unlock learning for " + import_chalk12.default.bold("$1 your first month") + " \u2014 profiles that");
11756
+ console.log(" get sharper every rewrite, hybrid rewrites, and your dashboard.\n");
11784
11757
  console.log(import_chalk12.default.dim(" free forever (no account): scan, fix, check, mcp"));
11785
- console.log(import_chalk12.default.dim(" paid unlock: learning loop, rich rewrites, sync across devices\n"));
11758
+ console.log(import_chalk12.default.dim(" $1 first month: learning loop, rich rewrites, sync across devices\n"));
11786
11759
  const ready = await askYesNo(" create your free account now? ($1 first month to unlock everything)");
11787
11760
  if (!ready) {
11788
11761
  console.log(import_chalk12.default.dim("\n no rush \u2014 your profile stays on this machine."));
@@ -11794,18 +11767,25 @@ async function stepSignup(profileName) {
11794
11767
  return;
11795
11768
  }
11796
11769
  if (!isInitialized() || !getToken()) {
11797
- console.log(import_chalk12.default.cyan("\n opening browser for signup...\n"));
11798
- await authenticateWithBrowser();
11770
+ console.log(import_chalk12.default.cyan("\n opening browser for signup (one sign-in)...\n"));
11771
+ await withSpinner("creating your account\u2026", () => authenticateWithBrowser());
11772
+ await briefPause();
11773
+ console.log(import_chalk12.default.green(" \u2713 account created"));
11774
+ } else {
11775
+ console.log(import_chalk12.default.dim("\n already signed in \u2014 syncing profile..."));
11799
11776
  }
11800
11777
  const content = fs13.readFileSync(
11801
11778
  path13.join(os7.homedir(), ".hyv", "profiles", `${profileName}.md`),
11802
11779
  "utf-8"
11803
11780
  );
11804
11781
  try {
11805
- const response = await authenticatedRequest(cliApiUrl("/cli/profiles/new"), {
11806
- method: "POST",
11807
- body: { name: profileName, content, source: "welcome" }
11808
- });
11782
+ const response = await withSpinner(
11783
+ "syncing profile\u2026",
11784
+ () => authenticatedRequest(cliApiUrl("/cli/profiles/new"), {
11785
+ method: "POST",
11786
+ body: { name: profileName, content, source: "welcome" }
11787
+ })
11788
+ );
11809
11789
  if (response.status === 200) {
11810
11790
  console.log(import_chalk12.default.green(" \u2713 profile synced to your account"));
11811
11791
  } else {
@@ -11814,12 +11794,15 @@ async function stepSignup(profileName) {
11814
11794
  } catch {
11815
11795
  console.log(import_chalk12.default.yellow(" profile saved locally \u2014 run `hyv sync` after you upgrade"));
11816
11796
  }
11817
- console.log(import_chalk12.default.cyan("\n opening pricing \u2014 $1 first month to unlock learning + sync..."));
11818
- const { default: open3 } = await Promise.resolve().then(() => __toESM(require_open()));
11819
- const { assertSafeOpenUrl: assertSafeOpenUrl2 } = await Promise.resolve().then(() => (init_config(), config_exports));
11820
- await open3(assertSafeOpenUrl2(PRICING_URL));
11797
+ try {
11798
+ console.log(import_chalk12.default.cyan("\n opening billing in your dashboard ($1 first month)..."));
11799
+ await withSpinner("opening dashboard\u2026", () => openAuthenticatedDashboard({ tab: "billing" }));
11800
+ await briefPause();
11801
+ } catch {
11802
+ console.log(import_chalk12.default.yellow(" could not auto-open dashboard \u2014 visit holdyourvoice.com/dashboard"));
11803
+ }
11821
11804
  markStepComplete("signup");
11822
- console.log(import_chalk12.default.dim("\n or run: hyv plan --upgrade\n"));
11805
+ console.log(import_chalk12.default.dim("\n you're signed in \u2014 pick a plan in billing, no second login.\n"));
11823
11806
  }
11824
11807
  async function runInteractiveWelcome() {
11825
11808
  recordEvent("welcome_interactive");
@@ -16845,28 +16828,15 @@ async function showPlan() {
16845
16828
  }
16846
16829
  }
16847
16830
  async function upgradePlan() {
16848
- console.log(import_chalk14.default.cyan("\nOpening checkout...\n"));
16849
- const response = await authenticatedRequest(
16850
- cliApiUrl("/cli/subscribe"),
16851
- {
16852
- method: "POST",
16853
- body: { plan: "individual" }
16854
- }
16855
- );
16856
- if (response.status === 200) {
16857
- const data = response.data;
16858
- const checkoutUrl = data.checkout_url;
16859
- if (checkoutUrl) {
16860
- console.log(import_chalk14.default.dim("Opening browser..."));
16861
- await (0, import_open2.default)(assertSafeOpenUrl(checkoutUrl));
16862
- console.log(import_chalk14.default.green("\n\u2713 Checkout opened in browser"));
16863
- console.log(import_chalk14.default.dim("Complete the checkout to activate your plan."));
16864
- } else {
16865
- console.log(import_chalk14.default.yellow("No checkout URL received."));
16866
- }
16867
- } else {
16868
- console.log(import_chalk14.default.yellow("Could not create checkout session."));
16869
- console.log(import_chalk14.default.dim("Visit https://holdyourvoice.com/pricing to subscribe."));
16831
+ console.log(import_chalk14.default.cyan("\nOpening billing in your dashboard ($1 first month)...\n"));
16832
+ try {
16833
+ await openAuthenticatedDashboard({ tab: "billing" });
16834
+ console.log(import_chalk14.default.green("\n\u2713 Dashboard opened \u2014 you're already signed in"));
16835
+ console.log(import_chalk14.default.dim("Pick a plan in billing. No second login."));
16836
+ } catch {
16837
+ console.log(import_chalk14.default.yellow("Could not open dashboard automatically."));
16838
+ console.log(import_chalk14.default.dim(`Visit ${DASHBOARD_BILLING_URL} after running hyv init`));
16839
+ console.log(import_chalk14.default.dim(`Or see plans: ${PRICING_URL}`));
16870
16840
  }
16871
16841
  }
16872
16842
  async function openBillingPortal() {
@@ -16880,7 +16850,7 @@ async function openBillingPortal() {
16880
16850
  const portalUrl = data.portal_url;
16881
16851
  if (portalUrl) {
16882
16852
  console.log(import_chalk14.default.dim("Opening browser..."));
16883
- await (0, import_open2.default)(assertSafeOpenUrl(portalUrl));
16853
+ await (0, import_open2.default)(assertSafeOpenUrl2(portalUrl));
16884
16854
  console.log(import_chalk14.default.green("\n\u2713 Billing portal opened"));
16885
16855
  } else {
16886
16856
  console.log(import_chalk14.default.yellow("No portal URL received."));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holdyourvoice/hyv",
3
- "version": "2.9.2",
3
+ "version": "2.9.4",
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": {