@holdyourvoice/hyv 2.8.7 → 2.8.9

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/dist/index.js CHANGED
@@ -973,7 +973,7 @@ var require_command = __commonJS({
973
973
  "node_modules/commander/lib/command.js"(exports2) {
974
974
  var EventEmitter = require("node:events").EventEmitter;
975
975
  var childProcess = require("node:child_process");
976
- var path23 = require("node:path");
976
+ var path24 = require("node:path");
977
977
  var fs25 = require("node:fs");
978
978
  var process2 = require("node:process");
979
979
  var { Argument: Argument2, humanReadableArgName } = require_argument();
@@ -1916,10 +1916,10 @@ Expecting one of '${allowedValues.join("', '")}'`);
1916
1916
  let launchWithNode = false;
1917
1917
  const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
1918
1918
  function findFile(baseDir, baseName) {
1919
- const localBin = path23.resolve(baseDir, baseName);
1919
+ const localBin = path24.resolve(baseDir, baseName);
1920
1920
  if (fs25.existsSync(localBin))
1921
1921
  return localBin;
1922
- if (sourceExt.includes(path23.extname(baseName)))
1922
+ if (sourceExt.includes(path24.extname(baseName)))
1923
1923
  return void 0;
1924
1924
  const foundExt = sourceExt.find(
1925
1925
  (ext) => fs25.existsSync(`${localBin}${ext}`)
@@ -1939,17 +1939,17 @@ Expecting one of '${allowedValues.join("', '")}'`);
1939
1939
  } catch (err) {
1940
1940
  resolvedScriptPath = this._scriptPath;
1941
1941
  }
1942
- executableDir = path23.resolve(
1943
- path23.dirname(resolvedScriptPath),
1942
+ executableDir = path24.resolve(
1943
+ path24.dirname(resolvedScriptPath),
1944
1944
  executableDir
1945
1945
  );
1946
1946
  }
1947
1947
  if (executableDir) {
1948
1948
  let localFile = findFile(executableDir, executableFile);
1949
1949
  if (!localFile && !subcommand._executableFile && this._scriptPath) {
1950
- const legacyName = path23.basename(
1950
+ const legacyName = path24.basename(
1951
1951
  this._scriptPath,
1952
- path23.extname(this._scriptPath)
1952
+ path24.extname(this._scriptPath)
1953
1953
  );
1954
1954
  if (legacyName !== this._name) {
1955
1955
  localFile = findFile(
@@ -1960,7 +1960,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1960
1960
  }
1961
1961
  executableFile = localFile || executableFile;
1962
1962
  }
1963
- launchWithNode = sourceExt.includes(path23.extname(executableFile));
1963
+ launchWithNode = sourceExt.includes(path24.extname(executableFile));
1964
1964
  let proc;
1965
1965
  if (process2.platform !== "win32") {
1966
1966
  if (launchWithNode) {
@@ -2817,7 +2817,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2817
2817
  * @return {Command}
2818
2818
  */
2819
2819
  nameFromFilename(filename) {
2820
- this._name = path23.basename(filename, path23.extname(filename));
2820
+ this._name = path24.basename(filename, path24.extname(filename));
2821
2821
  return this;
2822
2822
  }
2823
2823
  /**
@@ -2831,10 +2831,10 @@ Expecting one of '${allowedValues.join("', '")}'`);
2831
2831
  * @param {string} [path]
2832
2832
  * @return {(string|null|Command)}
2833
2833
  */
2834
- executableDir(path24) {
2835
- if (path24 === void 0)
2834
+ executableDir(path25) {
2835
+ if (path25 === void 0)
2836
2836
  return this._executableDir;
2837
- this._executableDir = path24;
2837
+ this._executableDir = path25;
2838
2838
  return this;
2839
2839
  }
2840
2840
  /**
@@ -3934,15 +3934,15 @@ var require_route = __commonJS({
3934
3934
  };
3935
3935
  }
3936
3936
  function wrapConversion(toModel, graph) {
3937
- const path23 = [graph[toModel].parent, toModel];
3937
+ const path24 = [graph[toModel].parent, toModel];
3938
3938
  let fn = conversions[graph[toModel].parent][toModel];
3939
3939
  let cur = graph[toModel].parent;
3940
3940
  while (graph[cur].parent) {
3941
- path23.unshift(graph[cur].parent);
3941
+ path24.unshift(graph[cur].parent);
3942
3942
  fn = link(conversions[graph[cur].parent][cur], fn);
3943
3943
  cur = graph[cur].parent;
3944
3944
  }
3945
- fn.conversion = path23;
3945
+ fn.conversion = path24;
3946
3946
  return fn;
3947
3947
  }
3948
3948
  module2.exports = function(fromModel) {
@@ -4381,14 +4381,14 @@ var require_templates = __commonJS({
4381
4381
  }
4382
4382
  return results;
4383
4383
  }
4384
- function buildStyle(chalk31, styles) {
4384
+ function buildStyle(chalk32, styles) {
4385
4385
  const enabled = {};
4386
4386
  for (const layer of styles) {
4387
4387
  for (const style of layer.styles) {
4388
4388
  enabled[style[0]] = layer.inverse ? null : style.slice(1);
4389
4389
  }
4390
4390
  }
4391
- let current = chalk31;
4391
+ let current = chalk32;
4392
4392
  for (const [styleName, styles2] of Object.entries(enabled)) {
4393
4393
  if (!Array.isArray(styles2)) {
4394
4394
  continue;
@@ -4400,7 +4400,7 @@ var require_templates = __commonJS({
4400
4400
  }
4401
4401
  return current;
4402
4402
  }
4403
- module2.exports = (chalk31, temporary) => {
4403
+ module2.exports = (chalk32, temporary) => {
4404
4404
  const styles = [];
4405
4405
  const chunks = [];
4406
4406
  let chunk = [];
@@ -4410,13 +4410,13 @@ var require_templates = __commonJS({
4410
4410
  } else if (style) {
4411
4411
  const string = chunk.join("");
4412
4412
  chunk = [];
4413
- chunks.push(styles.length === 0 ? string : buildStyle(chalk31, styles)(string));
4413
+ chunks.push(styles.length === 0 ? string : buildStyle(chalk32, styles)(string));
4414
4414
  styles.push({ inverse, styles: parseStyle(style) });
4415
4415
  } else if (close) {
4416
4416
  if (styles.length === 0) {
4417
4417
  throw new Error("Found extraneous } in Chalk template literal");
4418
4418
  }
4419
- chunks.push(buildStyle(chalk31, styles)(chunk.join("")));
4419
+ chunks.push(buildStyle(chalk32, styles)(chunk.join("")));
4420
4420
  chunk = [];
4421
4421
  styles.pop();
4422
4422
  } else {
@@ -4464,16 +4464,16 @@ var require_source = __commonJS({
4464
4464
  }
4465
4465
  };
4466
4466
  var chalkFactory = (options) => {
4467
- const chalk32 = {};
4468
- applyOptions(chalk32, options);
4469
- chalk32.template = (...arguments_) => chalkTag(chalk32.template, ...arguments_);
4470
- Object.setPrototypeOf(chalk32, Chalk.prototype);
4471
- Object.setPrototypeOf(chalk32.template, chalk32);
4472
- chalk32.template.constructor = () => {
4467
+ const chalk33 = {};
4468
+ applyOptions(chalk33, options);
4469
+ chalk33.template = (...arguments_) => chalkTag(chalk33.template, ...arguments_);
4470
+ Object.setPrototypeOf(chalk33, Chalk.prototype);
4471
+ Object.setPrototypeOf(chalk33.template, chalk33);
4472
+ chalk33.template.constructor = () => {
4473
4473
  throw new Error("`chalk.constructor()` is deprecated. Use `new chalk.Instance()` instead.");
4474
4474
  };
4475
- chalk32.template.Instance = ChalkClass;
4476
- return chalk32.template;
4475
+ chalk33.template.Instance = ChalkClass;
4476
+ return chalk33.template;
4477
4477
  };
4478
4478
  function Chalk(options) {
4479
4479
  return chalkFactory(options);
@@ -4584,7 +4584,7 @@ var require_source = __commonJS({
4584
4584
  return openAll + string + closeAll;
4585
4585
  };
4586
4586
  var template;
4587
- var chalkTag = (chalk32, ...strings) => {
4587
+ var chalkTag = (chalk33, ...strings) => {
4588
4588
  const [firstString] = strings;
4589
4589
  if (!isArray(firstString) || !isArray(firstString.raw)) {
4590
4590
  return strings.join(" ");
@@ -4600,26 +4600,109 @@ var require_source = __commonJS({
4600
4600
  if (template === void 0) {
4601
4601
  template = require_templates();
4602
4602
  }
4603
- return template(chalk32, parts.join(""));
4603
+ return template(chalk33, parts.join(""));
4604
4604
  };
4605
4605
  Object.defineProperties(Chalk.prototype, styles);
4606
- var chalk31 = Chalk();
4607
- chalk31.supportsColor = stdoutColor;
4608
- chalk31.stderr = Chalk({ level: stderrColor ? stderrColor.level : 0 });
4609
- chalk31.stderr.supportsColor = stderrColor;
4610
- module2.exports = chalk31;
4606
+ var chalk32 = Chalk();
4607
+ chalk32.supportsColor = stdoutColor;
4608
+ chalk32.stderr = Chalk({ level: stderrColor ? stderrColor.level : 0 });
4609
+ chalk32.stderr.supportsColor = stderrColor;
4610
+ module2.exports = chalk32;
4611
4611
  }
4612
4612
  });
4613
4613
 
4614
4614
  // src/lib/config.ts
4615
+ function validateApiBase(raw) {
4616
+ const trimmed = raw.replace(/\/$/, "");
4617
+ let parsed;
4618
+ try {
4619
+ parsed = new URL(trimmed);
4620
+ } catch {
4621
+ throw new Error(`Invalid HYV_API_URL: ${raw}`);
4622
+ }
4623
+ if (!ALLOWED_API_HOSTS.has(parsed.hostname)) {
4624
+ throw new Error(`HYV_API_URL host not allowed: ${parsed.hostname}`);
4625
+ }
4626
+ if (parsed.hostname !== "localhost" && parsed.hostname !== "127.0.0.1" && parsed.protocol !== "https:") {
4627
+ throw new Error("HYV_API_URL must use https://");
4628
+ }
4629
+ return trimmed;
4630
+ }
4631
+ function cliApiUrl(apiPath) {
4632
+ const p = apiPath.startsWith("/") ? apiPath : `/${apiPath}`;
4633
+ return `${API_BASE}${p}`;
4634
+ }
4635
+ function assertSafeOpenUrl(url) {
4636
+ let parsed;
4637
+ try {
4638
+ parsed = new URL(url);
4639
+ } catch {
4640
+ throw new Error("Invalid URL");
4641
+ }
4642
+ if (parsed.protocol !== "https:") {
4643
+ throw new Error("Only https:// URLs can be opened");
4644
+ }
4645
+ if (!ALLOWED_API_HOSTS.has(parsed.hostname)) {
4646
+ throw new Error(`URL host not allowed: ${parsed.hostname}`);
4647
+ }
4648
+ return url;
4649
+ }
4650
+ function assertSafeProfileName(name) {
4651
+ const normalized = String(name || "").trim();
4652
+ if (!normalized)
4653
+ throw new Error("Profile name is required");
4654
+ if (/[/\\]|\.\.|\u0000/.test(normalized)) {
4655
+ throw new Error("Invalid profile name: use letters, numbers, hyphens, and underscores only");
4656
+ }
4657
+ if (!PROFILE_NAME_RE.test(normalized)) {
4658
+ throw new Error("Invalid profile name: use letters, numbers, hyphens, and underscores only");
4659
+ }
4660
+ return normalized;
4661
+ }
4662
+ function profilePathForName(name) {
4663
+ const safe = assertSafeProfileName(name);
4664
+ ensureHyvDir();
4665
+ const profilePath = path.resolve(PROFILES_DIR, `${safe}.md`);
4666
+ const profilesRoot = path.resolve(PROFILES_DIR) + path.sep;
4667
+ if (!profilePath.startsWith(profilesRoot)) {
4668
+ throw new Error("Invalid profile name");
4669
+ }
4670
+ return profilePath;
4671
+ }
4615
4672
  function ensureHyvDir() {
4616
4673
  const dirs = [HYV_DIR, PROFILES_DIR, CACHE_DIR, QUEUE_DIR];
4617
4674
  for (const dir of dirs) {
4618
4675
  if (!fs.existsSync(dir)) {
4619
4676
  fs.mkdirSync(dir, { recursive: true, mode: 448 });
4677
+ } else {
4678
+ try {
4679
+ fs.chmodSync(dir, 448);
4680
+ } catch {
4681
+ }
4620
4682
  }
4621
4683
  }
4622
4684
  }
4685
+ function appendSecureLine(filePath, line, dir) {
4686
+ if (dir) {
4687
+ if (!fs.existsSync(dir))
4688
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
4689
+ else {
4690
+ try {
4691
+ fs.chmodSync(dir, 448);
4692
+ } catch {
4693
+ }
4694
+ }
4695
+ }
4696
+ if (!fs.existsSync(filePath)) {
4697
+ fs.writeFileSync(filePath, line, { mode: 384 });
4698
+ return;
4699
+ }
4700
+ try {
4701
+ fs.chmodSync(filePath, 384);
4702
+ } catch {
4703
+ }
4704
+ fs.appendFileSync(filePath, line);
4705
+ }
4623
4706
  function isInitialized() {
4624
4707
  return fs.existsSync(AUTH_FILE);
4625
4708
  }
@@ -4673,18 +4756,19 @@ function listCachedProfiles() {
4673
4756
  }
4674
4757
  }
4675
4758
  function readCachedProfile(name) {
4676
- const profilePath = path.join(PROFILES_DIR, `${name}.md`);
4759
+ const profilePath = profilePathForName(name);
4677
4760
  try {
4678
4761
  if (!fs.existsSync(profilePath))
4679
4762
  return null;
4680
4763
  return fs.readFileSync(profilePath, "utf-8");
4681
- } catch {
4764
+ } catch (err) {
4765
+ if (err.message?.includes("Invalid profile name"))
4766
+ throw err;
4682
4767
  return null;
4683
4768
  }
4684
4769
  }
4685
4770
  function writeCachedProfile(name, content) {
4686
- ensureHyvDir();
4687
- const profilePath = path.join(PROFILES_DIR, `${name}.md`);
4771
+ const profilePath = profilePathForName(name);
4688
4772
  fs.writeFileSync(profilePath, content, { mode: 384 });
4689
4773
  }
4690
4774
  function getQueuedSignals() {
@@ -4704,7 +4788,7 @@ function queueSignal(signal) {
4704
4788
  ensureHyvDir();
4705
4789
  const id = Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
4706
4790
  const filePath = path.join(QUEUE_DIR, `${id}.json`);
4707
- fs.writeFileSync(filePath, JSON.stringify(signal, null, 2));
4791
+ fs.writeFileSync(filePath, JSON.stringify(signal, null, 2), { mode: 384 });
4708
4792
  }
4709
4793
  function saveLastEditSession(session) {
4710
4794
  ensureHyvDir();
@@ -4720,7 +4804,7 @@ function readLastEditSession() {
4720
4804
  return null;
4721
4805
  }
4722
4806
  }
4723
- var fs, path, os, HYV_DIR, AUTH_FILE, CONFIG_FILE, PROFILES_DIR, CACHE_DIR, QUEUE_DIR, LAST_SESSION_FILE, API_BASE;
4807
+ var fs, path, os, HYV_DIR, AUTH_FILE, CONFIG_FILE, PROFILES_DIR, CACHE_DIR, QUEUE_DIR, LAST_SESSION_FILE, ALLOWED_API_HOSTS, API_BASE, PROFILE_NAME_RE;
4724
4808
  var init_config = __esm({
4725
4809
  "src/lib/config.ts"() {
4726
4810
  "use strict";
@@ -4734,7 +4818,15 @@ var init_config = __esm({
4734
4818
  CACHE_DIR = path.join(HYV_DIR, "cache");
4735
4819
  QUEUE_DIR = path.join(HYV_DIR, "queue");
4736
4820
  LAST_SESSION_FILE = path.join(HYV_DIR, "last-session.json");
4737
- API_BASE = process.env.HYV_API_URL || "https://holdyourvoice.com";
4821
+ ALLOWED_API_HOSTS = /* @__PURE__ */ new Set([
4822
+ "holdyourvoice.com",
4823
+ "www.holdyourvoice.com",
4824
+ "staging.holdyourvoice.com",
4825
+ "localhost",
4826
+ "127.0.0.1"
4827
+ ]);
4828
+ API_BASE = validateApiBase(process.env.HYV_API_URL || "https://holdyourvoice.com");
4829
+ PROFILE_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,62}$/i;
4738
4830
  }
4739
4831
  });
4740
4832
 
@@ -4825,13 +4917,13 @@ var require_define_lazy_prop = __commonJS({
4825
4917
  // node_modules/open/index.js
4826
4918
  var require_open = __commonJS({
4827
4919
  "node_modules/open/index.js"(exports2, module2) {
4828
- var path23 = require("path");
4920
+ var path24 = require("path");
4829
4921
  var childProcess = require("child_process");
4830
4922
  var { promises: fs25, constants: fsConstants } = require("fs");
4831
4923
  var isWsl = require_is_wsl();
4832
4924
  var isDocker = require_is_docker();
4833
4925
  var defineLazyProperty = require_define_lazy_prop();
4834
- var localXdgOpenPath = path23.join(__dirname, "xdg-open");
4926
+ var localXdgOpenPath = path24.join(__dirname, "xdg-open");
4835
4927
  var { platform, arch } = process;
4836
4928
  var hasContainerEnv = () => {
4837
4929
  try {
@@ -4988,14 +5080,14 @@ var require_open = __commonJS({
4988
5080
  }
4989
5081
  const subprocess = childProcess.spawn(command, cliArguments, childProcessOptions);
4990
5082
  if (options.wait) {
4991
- return new Promise((resolve12, reject) => {
5083
+ return new Promise((resolve14, reject) => {
4992
5084
  subprocess.once("error", reject);
4993
5085
  subprocess.once("close", (exitCode) => {
4994
5086
  if (!options.allowNonzeroExitCode && exitCode > 0) {
4995
5087
  reject(new Error(`Exited with code ${exitCode}`));
4996
5088
  return;
4997
5089
  }
4998
- resolve12(subprocess);
5090
+ resolve14(subprocess);
4999
5091
  });
5000
5092
  });
5001
5093
  }
@@ -5077,6 +5169,108 @@ var require_open = __commonJS({
5077
5169
  }
5078
5170
  });
5079
5171
 
5172
+ // src/lib/version.ts
5173
+ function pkgRoot() {
5174
+ const candidates = [
5175
+ path2.resolve(__dirname, ".."),
5176
+ // dist/ or src/lib/ → cli/
5177
+ path2.resolve(__dirname, "..", "..")
5178
+ // src/lib/ → cli/
5179
+ ];
5180
+ for (const root of candidates) {
5181
+ if (fs2.existsSync(path2.join(root, "package.json")))
5182
+ return root;
5183
+ }
5184
+ return path2.resolve(__dirname, "..");
5185
+ }
5186
+ function getCliVersion() {
5187
+ try {
5188
+ const pkg = JSON.parse(fs2.readFileSync(path2.join(PKG_ROOT, "package.json"), "utf-8"));
5189
+ return pkg.version || "unknown";
5190
+ } catch {
5191
+ return "unknown";
5192
+ }
5193
+ }
5194
+ function getRulesVersion() {
5195
+ try {
5196
+ if (fs2.existsSync(RULES_MANIFEST)) {
5197
+ return JSON.parse(fs2.readFileSync(RULES_MANIFEST, "utf-8")).version || "unknown";
5198
+ }
5199
+ } catch {
5200
+ }
5201
+ return "unknown";
5202
+ }
5203
+ function getEngineLabel() {
5204
+ return `hyv v${getCliVersion()} / rules v${getRulesVersion()}`;
5205
+ }
5206
+ function compareSemver(a, b) {
5207
+ const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
5208
+ const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
5209
+ const len = Math.max(pa.length, pb.length);
5210
+ for (let i = 0; i < len; i++) {
5211
+ const da = pa[i] ?? 0;
5212
+ const db = pb[i] ?? 0;
5213
+ if (da > db)
5214
+ return 1;
5215
+ if (da < db)
5216
+ return -1;
5217
+ }
5218
+ return 0;
5219
+ }
5220
+ function fetchLatestNpmVersion(timeoutMs = 8e3) {
5221
+ return new Promise((resolve14) => {
5222
+ (0, import_child_process.execFile)(
5223
+ "npm",
5224
+ ["view", "@holdyourvoice/hyv", "version"],
5225
+ { timeout: timeoutMs, encoding: "utf-8" },
5226
+ (err, stdout) => {
5227
+ if (err || !stdout?.trim())
5228
+ resolve14(null);
5229
+ else
5230
+ resolve14(stdout.trim());
5231
+ }
5232
+ );
5233
+ });
5234
+ }
5235
+ var fs2, path2, import_child_process, PKG_ROOT, RULES_MANIFEST;
5236
+ var init_version = __esm({
5237
+ "src/lib/version.ts"() {
5238
+ "use strict";
5239
+ fs2 = __toESM(require("fs"));
5240
+ path2 = __toESM(require("path"));
5241
+ import_child_process = require("child_process");
5242
+ PKG_ROOT = pkgRoot();
5243
+ RULES_MANIFEST = path2.join(PKG_ROOT, "assets", "detection-rules.json");
5244
+ }
5245
+ });
5246
+
5247
+ // src/lib/auth-refresh.ts
5248
+ function tokenExpiresAtMs(expiresAt) {
5249
+ if (!expiresAt)
5250
+ return null;
5251
+ const ms = new Date(expiresAt).getTime();
5252
+ return Number.isFinite(ms) ? ms : null;
5253
+ }
5254
+ function shouldRefreshToken(expiresAt, now = Date.now()) {
5255
+ const expiresMs = tokenExpiresAtMs(expiresAt);
5256
+ if (expiresMs === null)
5257
+ return false;
5258
+ return expiresMs <= now + REFRESH_LEAD_MS;
5259
+ }
5260
+ function isTokenExpired(expiresAt, now = Date.now()) {
5261
+ const expiresMs = tokenExpiresAtMs(expiresAt);
5262
+ if (expiresMs === null)
5263
+ return false;
5264
+ return expiresMs < now;
5265
+ }
5266
+ var REFRESH_LEAD_MS;
5267
+ var init_auth_refresh = __esm({
5268
+ "src/lib/auth-refresh.ts"() {
5269
+ "use strict";
5270
+ REFRESH_LEAD_MS = 5 * 60 * 1e3;
5271
+ }
5272
+ });
5273
+
5080
5274
  // src/lib/auth.ts
5081
5275
  var auth_exports = {};
5082
5276
  __export(auth_exports, {
@@ -5084,10 +5278,20 @@ __export(auth_exports, {
5084
5278
  authenticateWithLicense: () => authenticateWithLicense,
5085
5279
  authenticatedRequest: () => authenticatedRequest,
5086
5280
  checkSession: () => checkSession,
5087
- refreshToken: () => refreshToken
5281
+ getValidToken: () => getValidToken,
5282
+ refreshToken: () => refreshToken,
5283
+ verifyOAuthState: () => verifyOAuthState
5088
5284
  });
5285
+ function verifyOAuthState(received, expected) {
5286
+ if (!received || !expected || received !== expected) {
5287
+ throw new Error("OAuth state mismatch \u2014 possible CSRF attempt");
5288
+ }
5289
+ }
5290
+ function escapeHtml(value) {
5291
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
5292
+ }
5089
5293
  function request(url, options = {}) {
5090
- return new Promise((resolve12, reject) => {
5294
+ return new Promise((resolve14, reject) => {
5091
5295
  const urlObj = new URL(url);
5092
5296
  const isHttps = urlObj.protocol === "https:";
5093
5297
  const client = isHttps ? https : http;
@@ -5098,7 +5302,7 @@ function request(url, options = {}) {
5098
5302
  method: options.method || "GET",
5099
5303
  headers: {
5100
5304
  "Content-Type": "application/json",
5101
- "User-Agent": "hyv-cli/2.3.1",
5305
+ "User-Agent": `hyv-cli/${getCliVersion()}`,
5102
5306
  ...options.headers
5103
5307
  },
5104
5308
  timeout: options.timeout || 3e4
@@ -5108,9 +5312,9 @@ function request(url, options = {}) {
5108
5312
  res.on("data", (chunk) => data += chunk);
5109
5313
  res.on("end", () => {
5110
5314
  try {
5111
- resolve12({ status: res.statusCode || 0, data: JSON.parse(data) });
5315
+ resolve14({ status: res.statusCode || 0, data: JSON.parse(data) });
5112
5316
  } catch {
5113
- resolve12({ status: res.statusCode || 0, data });
5317
+ resolve14({ status: res.statusCode || 0, data });
5114
5318
  }
5115
5319
  });
5116
5320
  });
@@ -5125,8 +5329,21 @@ function request(url, options = {}) {
5125
5329
  req.end();
5126
5330
  });
5127
5331
  }
5332
+ async function getValidToken() {
5333
+ const auth = readAuth();
5334
+ if (!auth?.token)
5335
+ return null;
5336
+ if (shouldRefreshToken(auth.expires_at)) {
5337
+ const ok = await refreshToken(auth.token);
5338
+ if (ok)
5339
+ return getToken();
5340
+ if (isTokenExpired(auth.expires_at))
5341
+ return null;
5342
+ }
5343
+ return auth.token;
5344
+ }
5128
5345
  async function authenticatedRequest(url, options = {}) {
5129
- const token = getToken();
5346
+ const token = await getValidToken();
5130
5347
  if (!token) {
5131
5348
  throw new Error("Not authenticated. Run `hyv init` first.");
5132
5349
  }
@@ -5155,9 +5372,9 @@ async function authenticateWithLicense(licenseKey) {
5155
5372
  }
5156
5373
  async function authenticateWithBrowser() {
5157
5374
  const server = http.createServer();
5158
- const port = await new Promise((resolve12) => {
5375
+ const port = await new Promise((resolve14) => {
5159
5376
  server.listen(0, "127.0.0.1", () => {
5160
- resolve12(server.address().port);
5377
+ resolve14(server.address().port);
5161
5378
  });
5162
5379
  });
5163
5380
  const redirectUri = `http://127.0.0.1:${port}/callback`;
@@ -5169,10 +5386,14 @@ async function authenticateWithBrowser() {
5169
5386
  server.close();
5170
5387
  throw new Error("Failed to start authentication flow");
5171
5388
  }
5172
- const { auth_url } = response.data;
5389
+ const { auth_url, state: expectedState } = response.data;
5390
+ if (!expectedState) {
5391
+ server.close();
5392
+ throw new Error("Authentication server did not return OAuth state");
5393
+ }
5173
5394
  console.log(import_chalk.default.cyan("\nOpening browser for authentication..."));
5174
- await (0, import_open.default)(auth_url);
5175
- const authData = await new Promise((resolve12, reject) => {
5395
+ await (0, import_open.default)(assertSafeOpenUrl(auth_url));
5396
+ const authData = await new Promise((resolve14, reject) => {
5176
5397
  const timeout = setTimeout(() => {
5177
5398
  server.close();
5178
5399
  reject(new Error("Authentication timeout. Please try again."));
@@ -5190,6 +5411,16 @@ async function authenticateWithBrowser() {
5190
5411
  reject(new Error("Invalid callback parameters"));
5191
5412
  return;
5192
5413
  }
5414
+ try {
5415
+ verifyOAuthState(state, expectedState);
5416
+ } catch (stateErr) {
5417
+ res.writeHead(400, { "Content-Type": "text/html" });
5418
+ res.end("<h1>Authentication failed</h1><p>Invalid OAuth state.</p>");
5419
+ clearTimeout(timeout);
5420
+ server.close();
5421
+ reject(stateErr);
5422
+ return;
5423
+ }
5193
5424
  try {
5194
5425
  const callbackResponse = await request(`${API_BASE}/cli/auth/callback`, {
5195
5426
  method: "POST",
@@ -5210,10 +5441,10 @@ async function authenticateWithBrowser() {
5210
5441
  `);
5211
5442
  clearTimeout(timeout);
5212
5443
  server.close();
5213
- resolve12(data);
5444
+ resolve14(data);
5214
5445
  } catch (error) {
5215
5446
  res.writeHead(500, { "Content-Type": "text/html" });
5216
- res.end(`<h1>Authentication failed</h1><p>${error.message}</p>`);
5447
+ res.end(`<h1>Authentication failed</h1><p>${escapeHtml(error.message)}</p>`);
5217
5448
  clearTimeout(timeout);
5218
5449
  server.close();
5219
5450
  reject(error);
@@ -5227,8 +5458,8 @@ async function authenticateWithBrowser() {
5227
5458
  writeAuth(authData);
5228
5459
  return authData;
5229
5460
  }
5230
- async function refreshToken() {
5231
- const token = getToken();
5461
+ async function refreshToken(tokenOverride) {
5462
+ const token = tokenOverride || readAuth()?.token;
5232
5463
  if (!token)
5233
5464
  return false;
5234
5465
  try {
@@ -5254,7 +5485,7 @@ async function refreshToken() {
5254
5485
  }
5255
5486
  }
5256
5487
  async function checkSession() {
5257
- const token = getToken();
5488
+ const token = await getValidToken();
5258
5489
  if (!token)
5259
5490
  return { valid: false };
5260
5491
  try {
@@ -5280,6 +5511,8 @@ var init_auth = __esm({
5280
5511
  import_chalk = __toESM(require_source());
5281
5512
  import_open = __toESM(require_open());
5282
5513
  init_config();
5514
+ init_version();
5515
+ init_auth_refresh();
5283
5516
  }
5284
5517
  });
5285
5518
 
@@ -5413,25 +5646,23 @@ function recordEvent(event, meta) {
5413
5646
  if (!isTelemetryEnabled())
5414
5647
  return;
5415
5648
  try {
5416
- if (!fs2.existsSync(TELEMETRY_DIR))
5417
- fs2.mkdirSync(TELEMETRY_DIR, { recursive: true });
5418
- fs2.appendFileSync(TELEMETRY_FILE, JSON.stringify({
5649
+ appendSecureLine(TELEMETRY_FILE, JSON.stringify({
5419
5650
  event,
5420
5651
  ts: (/* @__PURE__ */ new Date()).toISOString(),
5421
5652
  ...meta
5422
- }) + "\n");
5653
+ }) + "\n", TELEMETRY_DIR);
5423
5654
  } catch {
5424
5655
  }
5425
5656
  }
5426
- var fs2, path2, os2, TELEMETRY_DIR, TELEMETRY_FILE;
5657
+ var path3, os2, TELEMETRY_DIR, TELEMETRY_FILE;
5427
5658
  var init_telemetry = __esm({
5428
5659
  "src/lib/telemetry.ts"() {
5429
5660
  "use strict";
5430
- fs2 = __toESM(require("fs"));
5431
- path2 = __toESM(require("path"));
5661
+ path3 = __toESM(require("path"));
5432
5662
  os2 = __toESM(require("os"));
5433
- TELEMETRY_DIR = path2.join(os2.homedir(), ".hyv", "telemetry");
5434
- TELEMETRY_FILE = path2.join(TELEMETRY_DIR, "events.jsonl");
5663
+ init_config();
5664
+ TELEMETRY_DIR = path3.join(os2.homedir(), ".hyv", "telemetry");
5665
+ TELEMETRY_FILE = path3.join(TELEMETRY_DIR, "events.jsonl");
5435
5666
  }
5436
5667
  });
5437
5668
 
@@ -5442,11 +5673,11 @@ __export(api_exports, {
5442
5673
  apiPost: () => apiPost,
5443
5674
  requireSubscription: () => requireSubscription
5444
5675
  });
5445
- async function request2(method, path23, body) {
5446
- const token = getToken();
5676
+ async function request2(method, path24, body) {
5677
+ const token = await getValidToken();
5447
5678
  if (!token)
5448
5679
  throw new Error("you're not signed in yet. run: hyv init");
5449
- const url = `${API_BASE}${path23}`;
5680
+ const url = `${API_BASE}${path24}`;
5450
5681
  const opts = {
5451
5682
  method,
5452
5683
  headers: {
@@ -5471,11 +5702,11 @@ async function request2(method, path23, body) {
5471
5702
  }
5472
5703
  return res.json();
5473
5704
  }
5474
- function apiGet(path23) {
5475
- return request2("GET", path23);
5705
+ function apiGet(path24) {
5706
+ return request2("GET", path24);
5476
5707
  }
5477
- function apiPost(path23, body) {
5478
- return request2("POST", path23, body);
5708
+ function apiPost(path24, body) {
5709
+ return request2("POST", path24, body);
5479
5710
  }
5480
5711
  async function requireSubscription() {
5481
5712
  const { requirePaidFeature: requirePaidFeature2 } = await Promise.resolve().then(() => (init_access(), access_exports));
@@ -5485,6 +5716,7 @@ var init_api = __esm({
5485
5716
  "src/lib/api.ts"() {
5486
5717
  "use strict";
5487
5718
  init_config();
5719
+ init_auth();
5488
5720
  }
5489
5721
  });
5490
5722
 
@@ -5679,7 +5911,7 @@ async function refreshInBackground(slug) {
5679
5911
  }
5680
5912
  }
5681
5913
  function getDiskCachePath(slug) {
5682
- return path3.join(DISK_CACHE_DIR, `${slug.replace(/[^a-z0-9-]/gi, "_")}.json`);
5914
+ return path4.join(DISK_CACHE_DIR, `${slug.replace(/[^a-z0-9-]/gi, "_")}.json`);
5683
5915
  }
5684
5916
  function loadFromDiskCache(slug) {
5685
5917
  try {
@@ -5718,18 +5950,18 @@ function buildMinimalProfile(slug, body) {
5718
5950
  cadence: {}
5719
5951
  };
5720
5952
  }
5721
- var fs3, path3, os3, CACHE_TTL, memoryCache, DISK_CACHE_DIR;
5953
+ var fs3, path4, os3, CACHE_TTL, memoryCache, DISK_CACHE_DIR;
5722
5954
  var init_profile = __esm({
5723
5955
  "src/lib/profile.ts"() {
5724
5956
  "use strict";
5725
5957
  init_api();
5726
5958
  init_config();
5727
5959
  fs3 = __toESM(require("fs"));
5728
- path3 = __toESM(require("path"));
5960
+ path4 = __toESM(require("path"));
5729
5961
  os3 = __toESM(require("os"));
5730
5962
  CACHE_TTL = 5 * 60 * 1e3;
5731
5963
  memoryCache = /* @__PURE__ */ new Map();
5732
- DISK_CACHE_DIR = path3.join(os3.homedir(), ".hyv", "cache", "profiles");
5964
+ DISK_CACHE_DIR = path4.join(os3.homedir(), ".hyv", "cache", "profiles");
5733
5965
  }
5734
5966
  });
5735
5967
 
@@ -5744,7 +5976,7 @@ __export(local_profile_exports, {
5744
5976
  loadProfileFromDiskCache: () => loadProfileFromDiskCache
5745
5977
  });
5746
5978
  function cachePathForSlug(slug) {
5747
- return path4.join(DISK_CACHE_DIR2, `${slug.replace(/[^a-z0-9-]/gi, "_")}.json`);
5979
+ return path5.join(DISK_CACHE_DIR2, `${slug.replace(/[^a-z0-9-]/gi, "_")}.json`);
5748
5980
  }
5749
5981
  function loadProfileFromDiskCache(slug) {
5750
5982
  try {
@@ -5844,17 +6076,17 @@ function hasRichProfile(profile) {
5844
6076
  return false;
5845
6077
  return (profile.never_list?.length || 0) > 0 || (profile.learned_patterns?.length || 0) > 0 || (profile.drift_snapshot?.length || 0) > 0 || (profile.voice_rules?.length || 0) > 0;
5846
6078
  }
5847
- var fs4, path4, os4, DISK_CACHE_DIR2;
6079
+ var fs4, path5, os4, DISK_CACHE_DIR2;
5848
6080
  var init_local_profile = __esm({
5849
6081
  "src/lib/local-profile.ts"() {
5850
6082
  "use strict";
5851
6083
  fs4 = __toESM(require("fs"));
5852
- path4 = __toESM(require("path"));
6084
+ path5 = __toESM(require("path"));
5853
6085
  os4 = __toESM(require("os"));
5854
6086
  init_config();
5855
6087
  init_config();
5856
6088
  init_profile();
5857
- DISK_CACHE_DIR2 = path4.join(os4.homedir(), ".hyv", "cache", "profiles");
6089
+ DISK_CACHE_DIR2 = path5.join(os4.homedir(), ".hyv", "cache", "profiles");
5858
6090
  }
5859
6091
  });
5860
6092
 
@@ -5946,19 +6178,19 @@ var init_profile_parse = __esm({
5946
6178
  function loadSupplementalPatterns() {
5947
6179
  const p = path8.join(CACHE_DIR, "supplemental-rules.json");
5948
6180
  try {
5949
- if (!fs8.existsSync(p))
6181
+ if (!fs7.existsSync(p))
5950
6182
  return [];
5951
- const data = JSON.parse(fs8.readFileSync(p, "utf-8"));
6183
+ const data = JSON.parse(fs7.readFileSync(p, "utf-8"));
5952
6184
  return Array.isArray(data.patterns) ? data.patterns : [];
5953
6185
  } catch {
5954
6186
  return [];
5955
6187
  }
5956
6188
  }
5957
- var fs8, path8, BUNDLED_MANIFEST;
6189
+ var fs7, path8, BUNDLED_MANIFEST;
5958
6190
  var init_rules_loader = __esm({
5959
6191
  "src/lib/rules-loader.ts"() {
5960
6192
  "use strict";
5961
- fs8 = __toESM(require("fs"));
6193
+ fs7 = __toESM(require("fs"));
5962
6194
  path8 = __toESM(require("path"));
5963
6195
  init_config();
5964
6196
  BUNDLED_MANIFEST = path8.resolve(__dirname, "..", "..", "assets", "detection-rules.json");
@@ -7047,7 +7279,7 @@ function runPipeline(text, profile, applyFixes = false) {
7047
7279
  }
7048
7280
  function readText(source) {
7049
7281
  const fs25 = require("fs");
7050
- const path23 = require("path");
7282
+ const path24 = require("path");
7051
7283
  if (source === "-") {
7052
7284
  if (process.stdin.isTTY) {
7053
7285
  console.error("No input provided. Pipe content or specify a file.");
@@ -7055,7 +7287,7 @@ function readText(source) {
7055
7287
  }
7056
7288
  return { text: fs25.readFileSync(0, "utf-8"), path: "stdin" };
7057
7289
  }
7058
- const resolved = path23.resolve(source);
7290
+ const resolved = path24.resolve(source);
7059
7291
  if (!fs25.existsSync(resolved)) {
7060
7292
  console.error(`File not found: ${resolved}`);
7061
7293
  process.exit(1);
@@ -10561,7 +10793,7 @@ var {
10561
10793
  } = import_index.default;
10562
10794
 
10563
10795
  // src/index.ts
10564
- var import_chalk30 = __toESM(require_source());
10796
+ var import_chalk31 = __toESM(require_source());
10565
10797
 
10566
10798
  // src/commands/init.ts
10567
10799
  var import_chalk4 = __toESM(require_source());
@@ -10733,79 +10965,11 @@ init_config();
10733
10965
  init_auth();
10734
10966
  init_access();
10735
10967
  init_local_profile();
10736
-
10737
- // src/lib/version.ts
10738
- var fs5 = __toESM(require("fs"));
10739
- var path5 = __toESM(require("path"));
10740
- var import_child_process = require("child_process");
10741
- function pkgRoot() {
10742
- const candidates = [
10743
- path5.resolve(__dirname, ".."),
10744
- // dist/ or src/lib/ → cli/
10745
- path5.resolve(__dirname, "..", "..")
10746
- // src/lib/ → cli/
10747
- ];
10748
- for (const root of candidates) {
10749
- if (fs5.existsSync(path5.join(root, "package.json")))
10750
- return root;
10751
- }
10752
- return path5.resolve(__dirname, "..");
10753
- }
10754
- var PKG_ROOT = pkgRoot();
10755
- var RULES_MANIFEST = path5.join(PKG_ROOT, "assets", "detection-rules.json");
10756
- function getCliVersion() {
10757
- try {
10758
- const pkg = JSON.parse(fs5.readFileSync(path5.join(PKG_ROOT, "package.json"), "utf-8"));
10759
- return pkg.version || "unknown";
10760
- } catch {
10761
- return "unknown";
10762
- }
10763
- }
10764
- function getRulesVersion() {
10765
- try {
10766
- if (fs5.existsSync(RULES_MANIFEST)) {
10767
- return JSON.parse(fs5.readFileSync(RULES_MANIFEST, "utf-8")).version || "unknown";
10768
- }
10769
- } catch {
10770
- }
10771
- return "unknown";
10772
- }
10773
- function getEngineLabel() {
10774
- return `hyv v${getCliVersion()} / rules v${getRulesVersion()}`;
10775
- }
10776
- function compareSemver(a, b) {
10777
- const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
10778
- const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
10779
- const len = Math.max(pa.length, pb.length);
10780
- for (let i = 0; i < len; i++) {
10781
- const da = pa[i] ?? 0;
10782
- const db = pb[i] ?? 0;
10783
- if (da > db)
10784
- return 1;
10785
- if (da < db)
10786
- return -1;
10787
- }
10788
- return 0;
10789
- }
10790
- function fetchLatestNpmVersion(timeoutMs = 8e3) {
10791
- return new Promise((resolve12) => {
10792
- (0, import_child_process.execFile)(
10793
- "npm",
10794
- ["view", "@holdyourvoice/hyv", "version"],
10795
- { timeout: timeoutMs, encoding: "utf-8" },
10796
- (err, stdout) => {
10797
- if (err || !stdout?.trim())
10798
- resolve12(null);
10799
- else
10800
- resolve12(stdout.trim());
10801
- }
10802
- );
10803
- });
10804
- }
10968
+ init_version();
10805
10969
 
10806
10970
  // src/lib/queue-sync.ts
10807
10971
  var import_chalk6 = __toESM(require_source());
10808
- var fs6 = __toESM(require("fs"));
10972
+ var fs5 = __toESM(require("fs"));
10809
10973
  var path6 = __toESM(require("path"));
10810
10974
  init_config();
10811
10975
  init_auth();
@@ -10815,34 +10979,34 @@ async function flushQueuedSignals() {
10815
10979
  return { sent: 0, failed: 0 };
10816
10980
  let sent = 0;
10817
10981
  let failed = 0;
10818
- if (!fs6.existsSync(QUEUE_DIR))
10982
+ if (!fs5.existsSync(QUEUE_DIR))
10819
10983
  return { sent: 0, failed: 0 };
10820
- const files = fs6.readdirSync(QUEUE_DIR).filter((f) => f.endsWith(".json"));
10984
+ const files = fs5.readdirSync(QUEUE_DIR).filter((f) => f.endsWith(".json"));
10821
10985
  for (const file of files) {
10822
10986
  const filePath = path6.join(QUEUE_DIR, file);
10823
10987
  try {
10824
- const signal = JSON.parse(fs6.readFileSync(filePath, "utf-8"));
10988
+ const signal = JSON.parse(fs5.readFileSync(filePath, "utf-8"));
10825
10989
  let ok = false;
10826
10990
  if (signal.type === "reinforce") {
10827
- const res = await authenticatedRequest("https://holdyourvoice.com/cli/learning/reinforce", {
10991
+ const res = await authenticatedRequest(cliApiUrl("/cli/learning/reinforce"), {
10828
10992
  method: "POST",
10829
10993
  body: {
10830
10994
  profile_id: signal.profile,
10831
- original_text: signal.report?.session?.original_path,
10832
- accepted_text: signal.report?.session?.accepted_path,
10995
+ original_text: signal.original_text ?? signal.report?.original_text,
10996
+ accepted_text: signal.accepted_text ?? signal.report?.accepted_text,
10833
10997
  signal_report: signal.report
10834
10998
  }
10835
10999
  });
10836
11000
  ok = res.status === 200;
10837
11001
  } else if (signal.type === "add_instruction") {
10838
- const res = await authenticatedRequest("https://holdyourvoice.com/cli/learning/add", {
11002
+ const res = await authenticatedRequest(cliApiUrl("/cli/learning/add"), {
10839
11003
  method: "POST",
10840
11004
  body: { profile_id: signal.profile, instruction: signal.instruction }
10841
11005
  });
10842
11006
  ok = res.status === 200;
10843
11007
  }
10844
11008
  if (ok) {
10845
- fs6.unlinkSync(filePath);
11009
+ fs5.unlinkSync(filePath);
10846
11010
  sent++;
10847
11011
  } else {
10848
11012
  failed++;
@@ -10971,7 +11135,7 @@ async function printEvolutionSummary(slug) {
10971
11135
  return;
10972
11136
  try {
10973
11137
  const res = await authenticatedRequest(
10974
- `https://holdyourvoice.com/cli/profiles/${encodeURIComponent(slug)}/evolution`,
11138
+ cliApiUrl(`/cli/profiles/${encodeURIComponent(slug)}/evolution`),
10975
11139
  { method: "GET" }
10976
11140
  );
10977
11141
  if (res.status !== 200 || !res.data)
@@ -10999,7 +11163,7 @@ init_config();
10999
11163
  init_auth();
11000
11164
  init_access();
11001
11165
  init_profile();
11002
- var fs7 = __toESM(require("fs"));
11166
+ var fs6 = __toESM(require("fs"));
11003
11167
  var path7 = __toESM(require("path"));
11004
11168
  function registerSyncCommand(program3) {
11005
11169
  program3.command("sync").description("Sync profiles and rules from server").option("--force", "Force re-download even if cache is fresh").action(async (options) => {
@@ -11007,7 +11171,7 @@ function registerSyncCommand(program3) {
11007
11171
  await requirePaidFeature("profiles");
11008
11172
  console.log(import_chalk8.default.cyan("Syncing profiles and rules..."));
11009
11173
  const response = await authenticatedRequest(
11010
- "https://holdyourvoice.com/cli/sync",
11174
+ cliApiUrl("/cli/sync"),
11011
11175
  { method: "GET" }
11012
11176
  );
11013
11177
  if (response.status !== 200) {
@@ -11023,12 +11187,12 @@ function registerSyncCommand(program3) {
11023
11187
  profileCount++;
11024
11188
  }
11025
11189
  const rulesPath = path7.join(CACHE_DIR, "rules.md");
11026
- fs7.writeFileSync(rulesPath, data.rules, { mode: 384 });
11190
+ fs6.writeFileSync(rulesPath, data.rules, { mode: 384 });
11027
11191
  const promptPath = path7.join(CACHE_DIR, "prompt-template.md");
11028
- fs7.writeFileSync(promptPath, data.prompt_template, { mode: 384 });
11192
+ fs6.writeFileSync(promptPath, data.prompt_template, { mode: 384 });
11029
11193
  if (data.detection_rules) {
11030
11194
  const rulesJson = path7.join(CACHE_DIR, "detection-rules.json");
11031
- fs7.writeFileSync(rulesJson, JSON.stringify(data.detection_rules, null, 2), { mode: 384 });
11195
+ fs6.writeFileSync(rulesJson, JSON.stringify(data.detection_rules, null, 2), { mode: 384 });
11032
11196
  }
11033
11197
  for (const profile of data.profiles) {
11034
11198
  try {
@@ -11044,7 +11208,7 @@ function registerSyncCommand(program3) {
11044
11208
  console.log(import_chalk8.default.yellow(` ! ${queueResult.failed} queued signal(s) could not send`));
11045
11209
  }
11046
11210
  const metaPath = path7.join(CACHE_DIR, "sync-meta.json");
11047
- fs7.writeFileSync(metaPath, JSON.stringify({
11211
+ fs6.writeFileSync(metaPath, JSON.stringify({
11048
11212
  synced_at: data.synced_at,
11049
11213
  profile_count: profileCount,
11050
11214
  plan: data.plan
@@ -11120,7 +11284,7 @@ async function showProfileHistory(slug) {
11120
11284
  return;
11121
11285
  }
11122
11286
  try {
11123
- const res = await authenticatedRequest2(`https://holdyourvoice.com/cli/profiles/${encodeURIComponent(slug)}/evolution`, { method: "GET" });
11287
+ const res = await authenticatedRequest2(cliApiUrl(`/cli/profiles/${encodeURIComponent(slug)}/evolution`), { method: "GET" });
11124
11288
  if (res.status !== 200) {
11125
11289
  console.log(import_chalk8.default.red("Could not load profile history."));
11126
11290
  return;
@@ -11149,12 +11313,12 @@ Profile evolution: ${slug}
11149
11313
 
11150
11314
  // src/commands/rewrite.ts
11151
11315
  var import_chalk9 = __toESM(require_source());
11152
- var fs10 = __toESM(require("fs"));
11316
+ var fs9 = __toESM(require("fs"));
11153
11317
  var path10 = __toESM(require("path"));
11154
11318
  init_pipeline();
11155
11319
 
11156
11320
  // src/lib/prompt.ts
11157
- var fs9 = __toESM(require("fs"));
11321
+ var fs8 = __toESM(require("fs"));
11158
11322
  var path9 = __toESM(require("path"));
11159
11323
  init_config();
11160
11324
  init_signals();
@@ -11162,8 +11326,8 @@ init_profile_parse();
11162
11326
  function loadPromptTemplate() {
11163
11327
  const templatePath = path9.join(CACHE_DIR, "prompt-template.md");
11164
11328
  try {
11165
- if (fs9.existsSync(templatePath)) {
11166
- return fs9.readFileSync(templatePath, "utf-8");
11329
+ if (fs8.existsSync(templatePath)) {
11330
+ return fs8.readFileSync(templatePath, "utf-8");
11167
11331
  }
11168
11332
  } catch {
11169
11333
  }
@@ -11229,10 +11393,10 @@ function loadProfile(name) {
11229
11393
  }
11230
11394
  }
11231
11395
  try {
11232
- if (!fs9.existsSync(PROFILES_DIR)) {
11396
+ if (!fs8.existsSync(PROFILES_DIR)) {
11233
11397
  return null;
11234
11398
  }
11235
- const profiles = fs9.readdirSync(PROFILES_DIR).filter((f) => f.endsWith(".md"));
11399
+ const profiles = fs8.readdirSync(PROFILES_DIR).filter((f) => f.endsWith(".md"));
11236
11400
  if (profiles.length > 0) {
11237
11401
  const defaultProfile = profiles[0].replace(".md", "");
11238
11402
  const content = readCachedProfile(defaultProfile);
@@ -11364,7 +11528,7 @@ Draft: ${draftPath}`));
11364
11528
  }
11365
11529
  if (options.output) {
11366
11530
  const outputPath = path10.resolve(options.output);
11367
- fs10.writeFileSync(outputPath, promptResult.prompt);
11531
+ fs9.writeFileSync(outputPath, promptResult.prompt);
11368
11532
  console.log(import_chalk9.default.green(`
11369
11533
  \u2713 Prompt written to ${outputPath}`));
11370
11534
  } else {
@@ -11384,7 +11548,7 @@ Draft: ${draftPath}`));
11384
11548
 
11385
11549
  // src/commands/learning.ts
11386
11550
  var import_chalk10 = __toESM(require_source());
11387
- var fs11 = __toESM(require("fs"));
11551
+ var fs10 = __toESM(require("fs"));
11388
11552
  var path11 = __toESM(require("path"));
11389
11553
 
11390
11554
  // src/lib/patterns.ts
@@ -11889,16 +12053,16 @@ function registerLearningCommands(program3) {
11889
12053
  }
11890
12054
  origPath = path11.resolve(original);
11891
12055
  editPath = path11.resolve(edited);
11892
- if (!fs11.existsSync(origPath)) {
12056
+ if (!fs10.existsSync(origPath)) {
11893
12057
  console.error(import_chalk10.default.red(`Original file not found: ${origPath}`));
11894
12058
  process.exit(1);
11895
12059
  }
11896
- if (!fs11.existsSync(editPath)) {
12060
+ if (!fs10.existsSync(editPath)) {
11897
12061
  console.error(import_chalk10.default.red(`Edited file not found: ${editPath}`));
11898
12062
  process.exit(1);
11899
12063
  }
11900
- origText = fs11.readFileSync(origPath, "utf-8");
11901
- editText = fs11.readFileSync(editPath, "utf-8");
12064
+ origText = fs10.readFileSync(origPath, "utf-8");
12065
+ editText = fs10.readFileSync(editPath, "utf-8");
11902
12066
  saveLastEditSession({
11903
12067
  original_path: origPath,
11904
12068
  edited_path: editPath,
@@ -11914,6 +12078,8 @@ function registerLearningCommands(program3) {
11914
12078
  queueSignal({
11915
12079
  type: "reinforce",
11916
12080
  profile: options.profile,
12081
+ original_text: origText,
12082
+ accepted_text: editText,
11917
12083
  report,
11918
12084
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
11919
12085
  });
@@ -11927,6 +12093,8 @@ ${err.message}`));
11927
12093
  queueSignal({
11928
12094
  type: "reinforce",
11929
12095
  profile: options.profile,
12096
+ original_text: origText,
12097
+ accepted_text: editText,
11930
12098
  report,
11931
12099
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
11932
12100
  });
@@ -11939,6 +12107,8 @@ ${err.message}`));
11939
12107
  queueSignal({
11940
12108
  type: "reinforce",
11941
12109
  profile: options.profile,
12110
+ original_text: origText,
12111
+ accepted_text: editText,
11942
12112
  report,
11943
12113
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
11944
12114
  });
@@ -11946,7 +12116,7 @@ ${err.message}`));
11946
12116
  }
11947
12117
  console.log(import_chalk10.default.cyan("\nSending to server..."));
11948
12118
  const response = await authenticatedRequest(
11949
- "https://holdyourvoice.com/cli/learning/reinforce",
12119
+ cliApiUrl("/cli/learning/reinforce"),
11950
12120
  {
11951
12121
  method: "POST",
11952
12122
  body: {
@@ -11968,6 +12138,8 @@ ${err.message}`));
11968
12138
  queueSignal({
11969
12139
  type: "reinforce",
11970
12140
  profile: options.profile,
12141
+ original_text: origText,
12142
+ accepted_text: editText,
11971
12143
  report,
11972
12144
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
11973
12145
  });
@@ -11994,7 +12166,7 @@ ${err.message}`));
11994
12166
  }
11995
12167
  console.log(import_chalk10.default.cyan("Adding instruction..."));
11996
12168
  const response = await authenticatedRequest(
11997
- "https://holdyourvoice.com/cli/learning/add",
12169
+ cliApiUrl("/cli/learning/add"),
11998
12170
  {
11999
12171
  method: "POST",
12000
12172
  body: {
@@ -12057,7 +12229,7 @@ function printProfileImpact(impact, data) {
12057
12229
 
12058
12230
  // src/commands/onboarding.ts
12059
12231
  var import_chalk11 = __toESM(require_source());
12060
- var fs12 = __toESM(require("fs"));
12232
+ var fs11 = __toESM(require("fs"));
12061
12233
  var path12 = __toESM(require("path"));
12062
12234
  init_config();
12063
12235
  init_auth();
@@ -12133,6 +12305,7 @@ function extractStats(samples) {
12133
12305
  function registerOnboardingCommands(program3) {
12134
12306
  program3.command("new").description("Create a new voice profile").argument("<name>", "Profile name").option("--from-samples <dir>", "Create from writing samples").option("--from-llm", "Generate extraction prompt for LLM").action(async (name, options) => {
12135
12307
  try {
12308
+ assertSafeProfileName(name);
12136
12309
  const token = getToken();
12137
12310
  if (options.fromSamples) {
12138
12311
  await createFromSamples(name, options.fromSamples, token);
@@ -12147,6 +12320,7 @@ Error: ${error.message}`));
12147
12320
  process.exit(1);
12148
12321
  }
12149
12322
  });
12323
+ registerImportCommand(program3);
12150
12324
  }
12151
12325
  async function flashcardOnboarding(name, token) {
12152
12326
  const { printWelcome: printWelcome2 } = await Promise.resolve().then(() => (init_welcome(), welcome_exports));
@@ -12170,7 +12344,7 @@ Industry: ${industry}
12170
12344
  if (token) {
12171
12345
  console.log(import_chalk11.default.cyan("\nGenerating profile on server..."));
12172
12346
  const response = await authenticatedRequest(
12173
- "https://holdyourvoice.com/cli/profiles/new",
12347
+ cliApiUrl("/cli/profiles/new"),
12174
12348
  {
12175
12349
  method: "POST",
12176
12350
  body: {
@@ -12207,20 +12381,20 @@ Industry: ${industry}
12207
12381
  }
12208
12382
  async function createFromSamples(name, sampleDir, token) {
12209
12383
  const dirPath = path12.resolve(sampleDir);
12210
- if (!fs12.existsSync(dirPath)) {
12384
+ if (!fs11.existsSync(dirPath)) {
12211
12385
  throw new Error(`Directory not found: ${dirPath}`);
12212
12386
  }
12213
12387
  console.log(import_chalk11.default.bold(`
12214
12388
  Creating voice profile: ${name}`));
12215
12389
  console.log(import_chalk11.default.dim(`Reading samples from: ${dirPath}
12216
12390
  `));
12217
- const files = fs12.readdirSync(dirPath).filter((f) => f.endsWith(".md") || f.endsWith(".txt")).map((f) => path12.join(dirPath, f));
12391
+ const files = fs11.readdirSync(dirPath).filter((f) => f.endsWith(".md") || f.endsWith(".txt")).map((f) => path12.join(dirPath, f));
12218
12392
  if (files.length === 0) {
12219
12393
  throw new Error("No .md or .txt files found in directory");
12220
12394
  }
12221
12395
  const samples = [];
12222
12396
  for (const file of files) {
12223
- const text = fs12.readFileSync(file, "utf-8");
12397
+ const text = fs11.readFileSync(file, "utf-8");
12224
12398
  if (text.trim().length > 0) {
12225
12399
  samples.push({ path: file, text });
12226
12400
  console.log(import_chalk11.default.dim(` \u2022 ${path12.basename(file)} (${words(text).length} words)`));
@@ -12240,7 +12414,7 @@ Stats extracted:`));
12240
12414
  if (token) {
12241
12415
  console.log(import_chalk11.default.cyan("\nGenerating profile on server..."));
12242
12416
  const response = await authenticatedRequest(
12243
- "https://holdyourvoice.com/cli/profiles/new",
12417
+ cliApiUrl("/cli/profiles/new"),
12244
12418
  {
12245
12419
  method: "POST",
12246
12420
  body: {
@@ -12290,17 +12464,37 @@ hyv import ${name} <file.md>
12290
12464
  Or paste the profile content and I'll save it.`;
12291
12465
  console.log(prompt);
12292
12466
  }
12467
+ function registerImportCommand(program3) {
12468
+ program3.command("import").description("Import a voice profile from file").argument("<name>", "Profile name").argument("<file>", "Profile markdown file").action(async (name, file) => {
12469
+ try {
12470
+ assertSafeProfileName(name);
12471
+ const filePath = path12.resolve(file);
12472
+ if (!fs11.existsSync(filePath)) {
12473
+ console.error(import_chalk11.default.red(`File not found: ${filePath}`));
12474
+ process.exit(1);
12475
+ }
12476
+ const content = fs11.readFileSync(filePath, "utf-8");
12477
+ ensureHyvDir();
12478
+ writeCachedProfile(name, content);
12479
+ console.log(import_chalk11.default.green(`
12480
+ \u2713 Profile imported: ${name}`));
12481
+ } catch (error) {
12482
+ console.error(import_chalk11.default.red(`Error: ${error.message}`));
12483
+ process.exit(1);
12484
+ }
12485
+ });
12486
+ }
12293
12487
  function askQuestion(question) {
12294
- return new Promise((resolve12) => {
12295
- const readline = require("readline");
12296
- const rl = readline.createInterface({
12488
+ return new Promise((resolve14) => {
12489
+ const readline2 = require("readline");
12490
+ const rl = readline2.createInterface({
12297
12491
  input: process.stdin,
12298
12492
  output: process.stdout
12299
12493
  });
12300
12494
  rl.question(import_chalk11.default.cyan(` ${question}
12301
12495
  > `), (answer) => {
12302
12496
  rl.close();
12303
- resolve12(answer.trim());
12497
+ resolve14(answer.trim());
12304
12498
  });
12305
12499
  });
12306
12500
  }
@@ -12404,7 +12598,7 @@ Error: ${error.message}`));
12404
12598
  async function showPlan() {
12405
12599
  console.log(import_chalk12.default.bold("\nSubscription Plan\n"));
12406
12600
  const response = await authenticatedRequest(
12407
- "https://holdyourvoice.com/cli/heartbeat",
12601
+ cliApiUrl("/cli/heartbeat"),
12408
12602
  { method: "GET" }
12409
12603
  );
12410
12604
  if (response.status !== 200) {
@@ -12448,7 +12642,7 @@ async function showPlan() {
12448
12642
  async function upgradePlan() {
12449
12643
  console.log(import_chalk12.default.cyan("\nOpening checkout...\n"));
12450
12644
  const response = await authenticatedRequest(
12451
- "https://holdyourvoice.com/cli/subscribe",
12645
+ cliApiUrl("/cli/subscribe"),
12452
12646
  {
12453
12647
  method: "POST",
12454
12648
  body: { plan: "individual" }
@@ -12459,7 +12653,7 @@ async function upgradePlan() {
12459
12653
  const checkoutUrl = data.checkout_url;
12460
12654
  if (checkoutUrl) {
12461
12655
  console.log(import_chalk12.default.dim("Opening browser..."));
12462
- await (0, import_open2.default)(checkoutUrl);
12656
+ await (0, import_open2.default)(assertSafeOpenUrl(checkoutUrl));
12463
12657
  console.log(import_chalk12.default.green("\n\u2713 Checkout opened in browser"));
12464
12658
  console.log(import_chalk12.default.dim("Complete the checkout to activate your plan."));
12465
12659
  } else {
@@ -12473,7 +12667,7 @@ async function upgradePlan() {
12473
12667
  async function openBillingPortal() {
12474
12668
  console.log(import_chalk12.default.cyan("\nOpening billing portal...\n"));
12475
12669
  const response = await authenticatedRequest(
12476
- "https://holdyourvoice.com/cli/subscribe/manage",
12670
+ cliApiUrl("/cli/subscribe/manage"),
12477
12671
  { method: "POST" }
12478
12672
  );
12479
12673
  if (response.status === 200) {
@@ -12481,7 +12675,7 @@ async function openBillingPortal() {
12481
12675
  const portalUrl = data.portal_url;
12482
12676
  if (portalUrl) {
12483
12677
  console.log(import_chalk12.default.dim("Opening browser..."));
12484
- await (0, import_open2.default)(portalUrl);
12678
+ await (0, import_open2.default)(assertSafeOpenUrl(portalUrl));
12485
12679
  console.log(import_chalk12.default.green("\n\u2713 Billing portal opened"));
12486
12680
  } else {
12487
12681
  console.log(import_chalk12.default.yellow("No portal URL received."));
@@ -12663,7 +12857,7 @@ async function runHybridAnalysis(text, profile, opts = {}) {
12663
12857
 
12664
12858
  // src/commands/history.ts
12665
12859
  var import_chalk14 = __toESM(require_source());
12666
- var fs13 = __toESM(require("fs"));
12860
+ var fs12 = __toESM(require("fs"));
12667
12861
  var path13 = __toESM(require("path"));
12668
12862
  init_config();
12669
12863
  var HISTORY_DIR = path13.join(HYV_DIR, "history");
@@ -12671,17 +12865,15 @@ var HISTORY_FILE = path13.join(HISTORY_DIR, "scans.jsonl");
12671
12865
  function logScan(entry) {
12672
12866
  try {
12673
12867
  ensureHyvDir();
12674
- if (!fs13.existsSync(HISTORY_DIR))
12675
- fs13.mkdirSync(HISTORY_DIR, { recursive: true });
12676
- fs13.appendFileSync(HISTORY_FILE, JSON.stringify(entry) + "\n");
12868
+ appendSecureLine(HISTORY_FILE, JSON.stringify(entry) + "\n", HISTORY_DIR);
12677
12869
  } catch {
12678
12870
  }
12679
12871
  }
12680
12872
  function readHistory() {
12681
12873
  try {
12682
- if (!fs13.existsSync(HISTORY_FILE))
12874
+ if (!fs12.existsSync(HISTORY_FILE))
12683
12875
  return [];
12684
- const lines = fs13.readFileSync(HISTORY_FILE, "utf-8").trim().split("\n").filter(Boolean);
12876
+ const lines = fs12.readFileSync(HISTORY_FILE, "utf-8").trim().split("\n").filter(Boolean);
12685
12877
  return lines.map((l) => JSON.parse(l));
12686
12878
  } catch {
12687
12879
  return [];
@@ -12713,8 +12905,8 @@ function registerHistoryCommand(program3) {
12713
12905
  program3.command("history").description("Show past scan scores and track improvement").option("--limit <n>", "Number of entries to show", "20").option("--file <path>", "Filter by file path").option("--since <date>", "Show entries since (e.g., 7d, 1m, 2024-01-01)").option("--chart", "Show ASCII sparkline chart").option("--clear", "Clear history").option("--format <type>", "Output format (text, json)", "text").action(async (options) => {
12714
12906
  try {
12715
12907
  if (options.clear) {
12716
- if (fs13.existsSync(HISTORY_FILE)) {
12717
- fs13.unlinkSync(HISTORY_FILE);
12908
+ if (fs12.existsSync(HISTORY_FILE)) {
12909
+ fs12.unlinkSync(HISTORY_FILE);
12718
12910
  }
12719
12911
  console.log(import_chalk14.default.green("\n\u2713 History cleared"));
12720
12912
  return;
@@ -12869,7 +13061,7 @@ hyv scan ${filePath}`));
12869
13061
  hasProfile: !!profile
12870
13062
  });
12871
13063
  if (options.failOnHit && signals.length > 0) {
12872
- process.exit(1);
13064
+ process.exit(2);
12873
13065
  }
12874
13066
  } catch (error) {
12875
13067
  console.error(import_chalk15.default.red(`Error: ${error.message}`));
@@ -12881,14 +13073,141 @@ hyv scan ${filePath}`));
12881
13073
  // src/commands/doctor.ts
12882
13074
  var import_chalk16 = __toESM(require_source());
12883
13075
  var fs14 = __toESM(require("fs"));
12884
- var path14 = __toESM(require("path"));
13076
+ var path15 = __toESM(require("path"));
12885
13077
  var os5 = __toESM(require("os"));
12886
13078
  init_config();
12887
13079
  init_auth();
12888
13080
  init_access();
12889
13081
  init_local_profile();
13082
+ init_version();
13083
+
13084
+ // src/lib/mcp-stdio-test.ts
13085
+ var import_child_process2 = require("child_process");
13086
+
13087
+ // src/lib/cli-entry.ts
13088
+ var fs13 = __toESM(require("fs"));
13089
+ var path14 = __toESM(require("path"));
13090
+ function resolveCliEntry() {
13091
+ const candidates = [
13092
+ path14.resolve(process.argv[1] || ""),
13093
+ path14.resolve(__dirname, "index.js"),
13094
+ path14.resolve(__dirname, "..", "dist", "index.js")
13095
+ ];
13096
+ return candidates.find((p) => p && fs13.existsSync(p)) || null;
13097
+ }
13098
+ function mcpServerCommand() {
13099
+ const entry = resolveCliEntry();
13100
+ if (entry) {
13101
+ return { command: process.execPath, args: [entry, "mcp"] };
13102
+ }
13103
+ return { command: "hyv", args: ["mcp"] };
13104
+ }
13105
+ function mcpServerSnippet() {
13106
+ const { command, args: args2 } = mcpServerCommand();
13107
+ return JSON.stringify({ command, args: args2 }, null, 2);
13108
+ }
13109
+
13110
+ // src/lib/mcp-stdio-test.ts
13111
+ async function testMcpStdioSubprocess() {
13112
+ const entry = resolveCliEntry();
13113
+ if (!entry)
13114
+ return { ok: false };
13115
+ return new Promise((resolve14) => {
13116
+ const child = (0, import_child_process2.spawn)(process.execPath, [entry, "mcp"], {
13117
+ stdio: ["pipe", "pipe", "pipe"],
13118
+ env: { ...process.env, HYV_POSTINSTALL_QUIET: "1" }
13119
+ });
13120
+ let buffer = "";
13121
+ let settled = false;
13122
+ const finish = (ok, toolCount) => {
13123
+ if (settled)
13124
+ return;
13125
+ settled = true;
13126
+ clearTimeout(timeout);
13127
+ try {
13128
+ child.kill();
13129
+ } catch {
13130
+ }
13131
+ resolve14({ ok, toolCount });
13132
+ };
13133
+ const timeout = setTimeout(() => finish(false), 1e4);
13134
+ const handleLine = (line) => {
13135
+ const trimmed = line.trim();
13136
+ if (!trimmed.startsWith("{"))
13137
+ return;
13138
+ try {
13139
+ const msg = JSON.parse(trimmed);
13140
+ if (msg.id === 2 && Array.isArray(msg.result?.tools) && msg.result.tools.length > 0) {
13141
+ finish(true, msg.result.tools.length);
13142
+ }
13143
+ } catch {
13144
+ }
13145
+ };
13146
+ child.stdout.on("data", (chunk) => {
13147
+ buffer += chunk.toString();
13148
+ const lines = buffer.split("\n");
13149
+ buffer = lines.pop() || "";
13150
+ for (const line of lines)
13151
+ handleLine(line);
13152
+ });
13153
+ child.on("error", () => finish(false));
13154
+ child.on("exit", () => {
13155
+ if (!settled) {
13156
+ handleLine(buffer);
13157
+ finish(buffer.includes("hyv_scan"));
13158
+ }
13159
+ });
13160
+ setTimeout(() => {
13161
+ const send2 = (payload) => {
13162
+ child.stdin.write(`${JSON.stringify(payload)}
13163
+ `);
13164
+ };
13165
+ send2({
13166
+ jsonrpc: "2.0",
13167
+ id: 1,
13168
+ method: "initialize",
13169
+ params: {
13170
+ protocolVersion: "2024-11-05",
13171
+ capabilities: {},
13172
+ clientInfo: { name: "hyv-self-test", version: "1.0" }
13173
+ }
13174
+ });
13175
+ send2({ jsonrpc: "2.0", method: "notifications/initialized" });
13176
+ send2({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} });
13177
+ }, 500);
13178
+ });
13179
+ }
13180
+
13181
+ // src/commands/doctor.ts
12890
13182
  var HOME = os5.homedir();
12891
13183
  var IS_WIN = process.platform === "win32";
13184
+ function claudeDesktopDir() {
13185
+ if (IS_WIN)
13186
+ return path15.join(HOME, "AppData", "Roaming", "Claude");
13187
+ if (process.platform === "linux")
13188
+ return path15.join(HOME, ".config", "Claude");
13189
+ return path15.join(HOME, "Library", "Application Support", "Claude");
13190
+ }
13191
+ function isOwnerOnlyFile(filePath) {
13192
+ try {
13193
+ if (!fs14.existsSync(filePath))
13194
+ return true;
13195
+ const mode = fs14.statSync(filePath).mode & 511;
13196
+ return (mode & 63) === 0;
13197
+ } catch {
13198
+ return false;
13199
+ }
13200
+ }
13201
+ function readMcpHyv(configFile) {
13202
+ try {
13203
+ if (!fs14.existsSync(configFile))
13204
+ return false;
13205
+ const cfg = JSON.parse(fs14.readFileSync(configFile, "utf-8"));
13206
+ return Boolean(cfg.mcpServers?.hyv);
13207
+ } catch {
13208
+ return false;
13209
+ }
13210
+ }
12892
13211
  function registerDoctorCommand(program3) {
12893
13212
  program3.command("doctor").description("Diagnose CLI health: engine, cache, auth, agents").option("--fix-agents", "Re-run agent config copy (idempotent)").action(async (opts) => {
12894
13213
  console.log(import_chalk16.default.bold("\nhold your voice \u2014 doctor\n"));
@@ -12915,12 +13234,12 @@ function registerDoctorCommand(program3) {
12915
13234
  console.log(import_chalk16.default.green(" \u2713 .hyv directory exists"));
12916
13235
  } else {
12917
13236
  console.log(import_chalk16.default.yellow(" ! .hyv directory missing \u2014 creating..."));
12918
- fs14.mkdirSync(HYV_DIR, { recursive: true });
13237
+ fs14.mkdirSync(HYV_DIR, { recursive: true, mode: 448 });
12919
13238
  fixed++;
12920
13239
  }
12921
13240
  console.log(import_chalk16.default.dim("checking cache..."));
12922
13241
  const diskProfiles = listDiskCachedProfiles();
12923
- const syncMeta = path14.join(CACHE_DIR, "sync-meta.json");
13242
+ const syncMeta = path15.join(CACHE_DIR, "sync-meta.json");
12924
13243
  if (diskProfiles.length > 0 || fs14.existsSync(syncMeta)) {
12925
13244
  console.log(import_chalk16.default.green(` \u2713 profile cache (${diskProfiles.length} full profile(s))`));
12926
13245
  if (fs14.existsSync(syncMeta)) {
@@ -12933,6 +13252,26 @@ function registerDoctorCommand(program3) {
12933
13252
  } else {
12934
13253
  console.log(import_chalk16.default.dim(" - no full profile cache (free local engine still works)"));
12935
13254
  }
13255
+ console.log(import_chalk16.default.dim("checking file permissions..."));
13256
+ if (fs14.existsSync(HYV_DIR)) {
13257
+ const hyvMode = fs14.statSync(HYV_DIR).mode & 511;
13258
+ if ((hyvMode & 63) === 0) {
13259
+ console.log(import_chalk16.default.green(" \u2713 .hyv directory permissions"));
13260
+ } else {
13261
+ console.log(import_chalk16.default.yellow(" ! .hyv directory is world/group accessible"));
13262
+ console.log(import_chalk16.default.dim(" run: chmod 700 ~/.hyv"));
13263
+ issues++;
13264
+ }
13265
+ }
13266
+ if (fs14.existsSync(AUTH_FILE)) {
13267
+ if (isOwnerOnlyFile(AUTH_FILE)) {
13268
+ console.log(import_chalk16.default.green(" \u2713 auth.json permissions"));
13269
+ } else {
13270
+ console.log(import_chalk16.default.yellow(" ! auth.json is world/group readable"));
13271
+ console.log(import_chalk16.default.dim(" run: chmod 600 ~/.hyv/auth.json"));
13272
+ issues++;
13273
+ }
13274
+ }
12936
13275
  console.log(import_chalk16.default.dim("checking authentication..."));
12937
13276
  if (isInitialized()) {
12938
13277
  const auth = readAuth();
@@ -12957,7 +13296,7 @@ function registerDoctorCommand(program3) {
12957
13296
  console.log(import_chalk16.default.dim(" run: hyv init for profiles + learning"));
12958
13297
  }
12959
13298
  console.log(import_chalk16.default.dim("checking voice profile..."));
12960
- const voiceMd = path14.join(HYV_DIR, "voice.md");
13299
+ const voiceMd = path15.join(HYV_DIR, "voice.md");
12961
13300
  const hasVoiceMd = fs14.existsSync(voiceMd) && fs14.readFileSync(voiceMd, "utf-8").trim().length > 50;
12962
13301
  const profileFiles = fs14.existsSync(PROFILES_DIR) ? fs14.readdirSync(PROFILES_DIR).filter((f) => f.endsWith(".md") && f !== "voice.md") : [];
12963
13302
  if (hasVoiceMd || profileFiles.length > 0 || diskProfiles.length > 0) {
@@ -12972,15 +13311,24 @@ function registerDoctorCommand(program3) {
12972
13311
  console.log(import_chalk16.default.dim(" run: hyv new <name> or hyv init"));
12973
13312
  }
12974
13313
  console.log(import_chalk16.default.dim("checking agent configurations..."));
12975
- const cwd = process.cwd();
12976
- const agents = [
12977
- { name: "claude desktop", file: path14.join(IS_WIN ? path14.join(HOME, "AppData", "Roaming", "Claude") : path14.join(HOME, "Library", "Application Support", "Claude"), "claude_desktop_config.json") },
12978
- { name: "claude code", file: path14.join(HOME, ".claude", "commands", "hyv.md") },
12979
- { name: "cursor", file: path14.join(HOME, ".cursor", "rules", "hyv.md") },
12980
- { name: "codex (project)", file: path14.join(cwd, ".codex", "instructions.md") }
13314
+ const cursorLegacyRule = path15.join(HOME, ".cursor", "rules", "hyv.md");
13315
+ const cursorRule = path15.join(HOME, ".cursor", "rules", "hyv.mdc");
13316
+ if (fs14.existsSync(cursorLegacyRule) && fs14.existsSync(cursorRule)) {
13317
+ console.log(import_chalk16.default.yellow(" ! stale cursor rule ~/.cursor/rules/hyv.md (use hyv.mdc)"));
13318
+ console.log(import_chalk16.default.dim(" run: rm ~/.cursor/rules/hyv.md (or hyv doctor --fix-agents)"));
13319
+ issues++;
13320
+ }
13321
+ const agentChecks = [
13322
+ { name: "claude desktop mcp", ok: readMcpHyv(path15.join(claudeDesktopDir(), "claude_desktop_config.json")) },
13323
+ { name: "cursor mcp", ok: readMcpHyv(path15.join(HOME, ".cursor", "mcp.json")) },
13324
+ { name: "cursor rule", ok: fs14.existsSync(cursorRule) },
13325
+ { name: "claude code command", ok: fs14.existsSync(path15.join(HOME, ".claude", "commands", "hyv.md")) },
13326
+ { name: "claude code skill", ok: fs14.existsSync(path15.join(HOME, ".claude", "skills", "hold-your-voice", "SKILL.md")) },
13327
+ { name: "codex agents", ok: fs14.existsSync(path15.join(HOME, ".codex", "AGENTS.md")) && fs14.readFileSync(path15.join(HOME, ".codex", "AGENTS.md"), "utf-8").includes("hyv") },
13328
+ { name: "command code skill", ok: fs14.existsSync(path15.join(HOME, ".commandcode", "skills", "hyv", "SKILL.md")) }
12981
13329
  ];
12982
- for (const agent of agents) {
12983
- if (fs14.existsSync(agent.file)) {
13330
+ for (const agent of agentChecks) {
13331
+ if (agent.ok) {
12984
13332
  console.log(import_chalk16.default.green(` \u2713 ${agent.name}`));
12985
13333
  } else {
12986
13334
  console.log(import_chalk16.default.dim(` - ${agent.name} not configured`));
@@ -12988,35 +13336,45 @@ function registerDoctorCommand(program3) {
12988
13336
  }
12989
13337
  if (opts.fixAgents) {
12990
13338
  try {
12991
- const postinstall = path14.resolve(__dirname, "..", "..", "scripts", "postinstall.js");
12992
- if (fs14.existsSync(postinstall)) {
12993
- require(postinstall);
12994
- console.log(import_chalk16.default.green(" \u2713 re-ran postinstall agent setup"));
12995
- fixed++;
12996
- }
12997
- } catch {
12998
- console.log(import_chalk16.default.yellow(" ! could not re-run postinstall"));
13339
+ const pkgDir = path15.resolve(__dirname, "..");
13340
+ const { setupAgents } = require(path15.join(pkgDir, "scripts", "postinstall-lib.js"));
13341
+ const result = setupAgents({ pkgDir, quiet: true });
13342
+ console.log(import_chalk16.default.green(` \u2713 re-ran agent setup (${result.configured.join(", ") || "no changes"})`));
13343
+ if (result.warnings.length) {
13344
+ console.log(import_chalk16.default.yellow(` notes: ${result.warnings.join("; ")}`));
13345
+ }
13346
+ fixed++;
13347
+ } catch (err) {
13348
+ console.log(import_chalk16.default.yellow(` ! could not re-run agent setup: ${err.message}`));
12999
13349
  }
13000
13350
  }
13001
13351
  console.log(import_chalk16.default.dim("checking mcp server..."));
13002
- const claudeConfigFile = path14.join(
13003
- IS_WIN ? path14.join(HOME, "AppData", "Roaming", "Claude") : path14.join(HOME, "Library", "Application Support", "Claude"),
13004
- "claude_desktop_config.json"
13005
- );
13006
- if (fs14.existsSync(claudeConfigFile)) {
13007
- try {
13008
- const cfg = JSON.parse(fs14.readFileSync(claudeConfigFile, "utf-8"));
13009
- if (cfg.mcpServers?.hyv) {
13010
- console.log(import_chalk16.default.green(" \u2713 mcp configured for claude desktop"));
13011
- } else {
13012
- console.log(import_chalk16.default.yellow(" ! mcp not configured \u2014 run: hyv init"));
13013
- issues++;
13014
- }
13015
- } catch {
13016
- console.log(import_chalk16.default.yellow(" ! could not read claude desktop config"));
13017
- }
13352
+ const claudeMcp = readMcpHyv(path15.join(claudeDesktopDir(), "claude_desktop_config.json"));
13353
+ const cursorMcp = readMcpHyv(path15.join(HOME, ".cursor", "mcp.json"));
13354
+ if (claudeMcp || cursorMcp) {
13355
+ if (claudeMcp)
13356
+ console.log(import_chalk16.default.green(" \u2713 mcp configured for claude desktop"));
13357
+ if (cursorMcp)
13358
+ console.log(import_chalk16.default.green(" \u2713 mcp configured for cursor"));
13018
13359
  } else {
13019
- console.log(import_chalk16.default.dim(" - claude desktop not found"));
13360
+ console.log(import_chalk16.default.yellow(" ! mcp not configured \u2014 run: hyv doctor --fix-agents or hyv mcp --setup"));
13361
+ issues++;
13362
+ }
13363
+ try {
13364
+ const stdio = await testMcpStdioSubprocess();
13365
+ if (stdio.ok && (stdio.toolCount || 0) >= 10) {
13366
+ console.log(import_chalk16.default.green(` \u2713 mcp stdio subprocess healthy (${stdio.toolCount} tools)`));
13367
+ } else if (stdio.ok) {
13368
+ console.log(import_chalk16.default.yellow(` ! mcp stdio ok but only ${stdio.toolCount || 0} tools`));
13369
+ issues++;
13370
+ } else {
13371
+ console.log(import_chalk16.default.red(" \u2717 mcp stdio subprocess failed"));
13372
+ console.log(import_chalk16.default.dim(" run: hyv mcp --test"));
13373
+ issues++;
13374
+ }
13375
+ } catch (err) {
13376
+ console.log(import_chalk16.default.red(` \u2717 mcp stdio probe failed: ${err.message}`));
13377
+ issues++;
13020
13378
  }
13021
13379
  console.log("");
13022
13380
  if (issues === 0 && fixed === 0) {
@@ -13065,7 +13423,7 @@ function registerRenameCommand(program3) {
13065
13423
  return;
13066
13424
  }
13067
13425
  const response = await authenticatedRequest(
13068
- "https://holdyourvoice.com/cli/profiles/rename",
13426
+ cliApiUrl("/cli/profiles/rename"),
13069
13427
  {
13070
13428
  method: "POST",
13071
13429
  body: {
@@ -13094,15 +13452,51 @@ error: ${error.message}
13094
13452
  }
13095
13453
 
13096
13454
  // src/commands/fix.ts
13097
- var import_chalk18 = __toESM(require_source());
13098
- var fs15 = __toESM(require("fs"));
13099
- var path15 = __toESM(require("path"));
13455
+ var import_chalk19 = __toESM(require_source());
13100
13456
  init_pipeline();
13101
13457
  init_local_profile();
13102
13458
  init_access();
13103
13459
  init_config();
13460
+
13461
+ // src/lib/destructive-write.ts
13462
+ var fs15 = __toESM(require("fs"));
13463
+ var path16 = __toESM(require("path"));
13464
+ var readline = __toESM(require("readline"));
13465
+ var import_chalk18 = __toESM(require_source());
13466
+ async function confirmDestructiveWrite(options) {
13467
+ if (options.yes)
13468
+ return true;
13469
+ const label = options.target ? `${options.action} (${options.target})` : options.action;
13470
+ if (!process.stdin.isTTY) {
13471
+ console.error(import_chalk18.default.red("\nRefusing destructive write without confirmation (non-interactive)."));
13472
+ console.error(import_chalk18.default.dim(" Re-run with --yes to create .bak backups and proceed.\n"));
13473
+ return false;
13474
+ }
13475
+ const question = `
13476
+ ${label}
13477
+ A .bak backup will be created. Proceed? [y/N] `;
13478
+ const answer = await new Promise((resolve14) => {
13479
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
13480
+ rl.question(question, (value) => {
13481
+ rl.close();
13482
+ resolve14(value.trim().toLowerCase());
13483
+ });
13484
+ });
13485
+ return answer === "y" || answer === "yes";
13486
+ }
13487
+ function writeInPlaceWithBackup(filePath, content) {
13488
+ const backupPath = filePath + ".bak";
13489
+ fs15.copyFileSync(filePath, backupPath);
13490
+ fs15.writeFileSync(filePath, content);
13491
+ return backupPath;
13492
+ }
13493
+ function backupBasename(filePath) {
13494
+ return path16.basename(filePath) + ".bak";
13495
+ }
13496
+
13497
+ // src/commands/fix.ts
13104
13498
  function registerFixCommand(program3) {
13105
- program3.command("fix").description("Apply auto-fixes (word swaps, connector removals) without an LLM").argument("<file>", "File to fix (or - for stdin)").option("--dry-run", "Show changes without writing").option("-i, --in-place", "Write fixes back to the file").option("--profile <name>", "Voice profile for never-list checks").option("--ignore <rules>", "Comma-separated rule IDs to skip").option("--format <type>", "Output format (text or json)", "text").action(async (file, options) => {
13499
+ program3.command("fix").description("Apply auto-fixes (word swaps, connector removals) without an LLM").argument("<file>", "File to fix (or - for stdin)").option("--dry-run", "Show changes without writing").option("-i, --in-place", "Write fixes back to the file").option("-y, --yes", "Confirm in-place write without prompting (creates .bak backup)").option("--profile <name>", "Voice profile for never-list checks").option("--ignore <rules>", "Comma-separated rule IDs to skip").option("--format <type>", "Output format (text or json)", "text").action(async (file, options) => {
13106
13500
  try {
13107
13501
  const profile = await loadProfileForCommand(options.profile);
13108
13502
  const { text, path: filePath } = readText(file);
@@ -13111,9 +13505,9 @@ function registerFixCommand(program3) {
13111
13505
  if (options.format === "json") {
13112
13506
  console.log(JSON.stringify({ file: filePath, autoFixes: 0, llmIssues: result.stats.needsLLM, changes: [] }));
13113
13507
  } else {
13114
- console.log(import_chalk18.default.green("\n\u2713 No auto-fixable issues found."));
13508
+ console.log(import_chalk19.default.green("\n\u2713 No auto-fixable issues found."));
13115
13509
  if (result.stats.needsLLM > 0) {
13116
- console.log(import_chalk18.default.dim(` ${result.stats.needsLLM} issues need LLM rewrite \u2014 run: hyv rewrite ${file}`));
13510
+ console.log(import_chalk19.default.dim(` ${result.stats.needsLLM} issues need LLM rewrite \u2014 run: hyv rewrite ${file}`));
13117
13511
  }
13118
13512
  }
13119
13513
  return;
@@ -13127,24 +13521,30 @@ function registerFixCommand(program3) {
13127
13521
  fixed: options.dryRun ? void 0 : result.fixed
13128
13522
  }, null, 2));
13129
13523
  } else {
13130
- console.log(import_chalk18.default.dim(`
13524
+ console.log(import_chalk19.default.dim(`
13131
13525
  hyv fix ${filePath}
13132
13526
  `));
13133
13527
  for (const change of result.changes) {
13134
- console.log(import_chalk18.default.dim(` Line ${change.line}: `) + import_chalk18.default.red(change.before) + import_chalk18.default.dim(" \u2192 ") + import_chalk18.default.green(change.after));
13528
+ console.log(import_chalk19.default.dim(` Line ${change.line}: `) + import_chalk19.default.red(change.before) + import_chalk19.default.dim(" \u2192 ") + import_chalk19.default.green(change.after));
13135
13529
  }
13136
- console.log(import_chalk18.default.green(`
13530
+ console.log(import_chalk19.default.green(`
13137
13531
  \u2713 ${result.changes.length} auto-fix${result.changes.length === 1 ? "" : "es"} applied`));
13138
13532
  if (result.stats.needsLLM > 0) {
13139
- console.log(import_chalk18.default.dim(` ${result.stats.needsLLM} issues need LLM rewrite \u2014 run: hyv rewrite ${file}`));
13533
+ console.log(import_chalk19.default.dim(` ${result.stats.needsLLM} issues need LLM rewrite \u2014 run: hyv rewrite ${file}`));
13140
13534
  }
13141
13535
  }
13142
13536
  if (!options.dryRun) {
13143
13537
  if (options.inPlace && filePath !== "stdin") {
13144
- const backupPath = filePath + ".bak";
13145
- fs15.copyFileSync(filePath, backupPath);
13146
- fs15.writeFileSync(filePath, result.fixed);
13147
- console.log(import_chalk18.default.dim(` Written to ${filePath} (backup: ${path15.basename(backupPath)})`));
13538
+ const confirmed = await confirmDestructiveWrite({
13539
+ yes: options.yes,
13540
+ action: "Apply auto-fixes in-place",
13541
+ target: filePath
13542
+ });
13543
+ if (!confirmed) {
13544
+ process.exit(1);
13545
+ }
13546
+ const backupPath = writeInPlaceWithBackup(filePath, result.fixed);
13547
+ console.log(import_chalk19.default.dim(` Written to ${filePath} (backup: ${backupBasename(filePath)})`));
13148
13548
  saveLastEditSession({
13149
13549
  original_path: filePath,
13150
13550
  edited_path: filePath,
@@ -13152,7 +13552,7 @@ hyv fix ${filePath}
13152
13552
  edited_text: result.fixed,
13153
13553
  profile: options.profile
13154
13554
  });
13155
- console.log(import_chalk18.default.dim(" Tip: hyv reinforce --last to teach your profile from this edit"));
13555
+ console.log(import_chalk19.default.dim(" Tip: hyv reinforce --last to teach your profile from this edit"));
13156
13556
  } else if (filePath === "stdin" || !options.inPlace) {
13157
13557
  process.stdout.write("\n" + result.fixed + "\n");
13158
13558
  if (filePath !== "stdin") {
@@ -13168,14 +13568,14 @@ hyv fix ${filePath}
13168
13568
  }
13169
13569
  await maybeShowLimitedModeHint(!!profile);
13170
13570
  } catch (error) {
13171
- console.error(import_chalk18.default.red(`Error: ${error.message}`));
13571
+ console.error(import_chalk19.default.red(`Error: ${error.message}`));
13172
13572
  process.exit(1);
13173
13573
  }
13174
13574
  });
13175
13575
  }
13176
13576
 
13177
13577
  // src/commands/check.ts
13178
- var import_chalk19 = __toESM(require_source());
13578
+ var import_chalk20 = __toESM(require_source());
13179
13579
  init_pipeline();
13180
13580
  init_local_profile();
13181
13581
  init_access();
@@ -13187,18 +13587,18 @@ function registerCheckCommand(program3) {
13187
13587
  if (text === "-") {
13188
13588
  const fs26 = require("fs");
13189
13589
  if (process.stdin.isTTY) {
13190
- console.error(import_chalk19.default.red("No input provided. Pipe content or pass text as argument."));
13590
+ console.error(import_chalk20.default.red("No input provided. Pipe content or pass text as argument."));
13191
13591
  process.exit(1);
13192
13592
  }
13193
13593
  inputText = fs26.readFileSync(0, "utf-8");
13194
13594
  }
13195
13595
  if (!inputText.trim()) {
13196
- console.error(import_chalk19.default.red("No text provided."));
13596
+ console.error(import_chalk20.default.red("No text provided."));
13197
13597
  process.exit(1);
13198
13598
  }
13199
13599
  const fs25 = require("fs");
13200
13600
  if (text !== "-" && fs25.existsSync(text)) {
13201
- console.log(import_chalk19.default.yellow(`"${text}" looks like a file. Did you mean: hyv scan ${text}`));
13601
+ console.log(import_chalk20.default.yellow(`"${text}" looks like a file. Did you mean: hyv scan ${text}`));
13202
13602
  process.exit(1);
13203
13603
  }
13204
13604
  const result = runPipeline(inputText, profile, false);
@@ -13222,30 +13622,30 @@ function registerCheckCommand(program3) {
13222
13622
  }, null, 2));
13223
13623
  } else {
13224
13624
  if (result.stats.totalSignals === 0) {
13225
- console.log(import_chalk19.default.green("\n\u2713 Clean \u2014 no AI patterns found."));
13226
- console.log(import_chalk19.default.dim(` score: ${result.score}/100`));
13625
+ console.log(import_chalk20.default.green("\n\u2713 Clean \u2014 no AI patterns found."));
13626
+ console.log(import_chalk20.default.dim(` score: ${result.score}/100`));
13227
13627
  } else {
13228
13628
  console.log("");
13229
13629
  printGroupedSignals(result.signalMap.signals.slice(0, 15), profile);
13230
13630
  if (result.signalMap.signals.length > 15) {
13231
- console.log(import_chalk19.default.dim(` ... and ${result.signalMap.signals.length - 15} more`));
13631
+ console.log(import_chalk20.default.dim(` ... and ${result.signalMap.signals.length - 15} more`));
13232
13632
  }
13233
- console.log(import_chalk19.default.yellow(`
13633
+ console.log(import_chalk20.default.yellow(`
13234
13634
  ${result.stats.totalSignals} issues (${result.stats.red} red, ${result.stats.yellow} yellow)`));
13235
- console.log(import_chalk19.default.dim(` score: ${result.score}/100`));
13635
+ console.log(import_chalk20.default.dim(` score: ${result.score}/100`));
13236
13636
  }
13237
13637
  await maybeShowLimitedModeHint(!!profile);
13238
13638
  }
13239
13639
  process.exit(result.stats.totalSignals > 0 ? 1 : 0);
13240
13640
  } catch (error) {
13241
- console.error(import_chalk19.default.red(`Error: ${error.message}`));
13641
+ console.error(import_chalk20.default.red(`Error: ${error.message}`));
13242
13642
  process.exit(1);
13243
13643
  }
13244
13644
  });
13245
13645
  }
13246
13646
 
13247
13647
  // src/commands/score.ts
13248
- var import_chalk20 = __toESM(require_source());
13648
+ var import_chalk21 = __toESM(require_source());
13249
13649
  init_pipeline();
13250
13650
  init_local_profile();
13251
13651
  function registerScoreCommand(program3) {
@@ -13270,14 +13670,14 @@ function registerScoreCommand(program3) {
13270
13670
  process.exit(1);
13271
13671
  }
13272
13672
  } catch (error) {
13273
- console.error(import_chalk20.default.red(`Error: ${error.message}`));
13673
+ console.error(import_chalk21.default.red(`Error: ${error.message}`));
13274
13674
  process.exit(1);
13275
13675
  }
13276
13676
  });
13277
13677
  }
13278
13678
 
13279
13679
  // src/commands/diff.ts
13280
- var import_chalk21 = __toESM(require_source());
13680
+ var import_chalk22 = __toESM(require_source());
13281
13681
  init_pipeline();
13282
13682
  init_local_profile();
13283
13683
  function registerDiffCommand(program3) {
@@ -13287,9 +13687,9 @@ function registerDiffCommand(program3) {
13287
13687
  const { text, path: filePath } = readText(file);
13288
13688
  const result = runPipeline(text, profile, true);
13289
13689
  if (result.changes.length === 0) {
13290
- console.log(import_chalk21.default.green("\n\u2713 No auto-fixable changes."));
13690
+ console.log(import_chalk22.default.green("\n\u2713 No auto-fixable changes."));
13291
13691
  if (result.stats.needsLLM > 0) {
13292
- console.log(import_chalk21.default.dim(` ${result.stats.needsLLM} issues need LLM rewrite`));
13692
+ console.log(import_chalk22.default.dim(` ${result.stats.needsLLM} issues need LLM rewrite`));
13293
13693
  }
13294
13694
  return;
13295
13695
  }
@@ -13304,42 +13704,42 @@ function registerDiffCommand(program3) {
13304
13704
  const originalLines = text.split("\n");
13305
13705
  const fixedLines = result.fixed.split("\n");
13306
13706
  const contextN = parseInt(options.context, 10);
13307
- console.log(import_chalk21.default.dim(`--- ${filePath}`));
13308
- console.log(import_chalk21.default.dim(`+++ ${filePath} (fixed)`));
13707
+ console.log(import_chalk22.default.dim(`--- ${filePath}`));
13708
+ console.log(import_chalk22.default.dim(`+++ ${filePath} (fixed)`));
13309
13709
  for (const change of result.changes) {
13310
13710
  const lineIdx = change.line - 1;
13311
13711
  const start = Math.max(0, lineIdx - contextN);
13312
13712
  const end = Math.min(originalLines.length, lineIdx + contextN + 1);
13313
- console.log(import_chalk21.default.dim(`@@ -${start + 1},${end - start} +${start + 1},${end - start} @@`));
13713
+ console.log(import_chalk22.default.dim(`@@ -${start + 1},${end - start} +${start + 1},${end - start} @@`));
13314
13714
  for (let i = start; i < end; i++) {
13315
13715
  if (i === lineIdx) {
13316
- console.log(import_chalk21.default.red(`-${originalLines[i]}`));
13317
- console.log(import_chalk21.default.green(`+${fixedLines[i]}`));
13716
+ console.log(import_chalk22.default.red(`-${originalLines[i]}`));
13717
+ console.log(import_chalk22.default.green(`+${fixedLines[i]}`));
13318
13718
  } else {
13319
- console.log(import_chalk21.default.dim(` ${originalLines[i]}`));
13719
+ console.log(import_chalk22.default.dim(` ${originalLines[i]}`));
13320
13720
  }
13321
13721
  }
13322
13722
  }
13323
- console.log(import_chalk21.default.green(`
13723
+ console.log(import_chalk22.default.green(`
13324
13724
  ${result.changes.length} auto-fix${result.changes.length === 1 ? "" : "es"} available`));
13325
- console.log(import_chalk21.default.dim(` run: hyv fix ${file} -i to apply`));
13725
+ console.log(import_chalk22.default.dim(` run: hyv fix ${file} -i to apply`));
13326
13726
  if (options.apply && filePath !== "stdin") {
13327
13727
  const fs25 = require("fs");
13328
- const path23 = require("path");
13728
+ const path24 = require("path");
13329
13729
  const backupPath = filePath + ".bak";
13330
13730
  fs25.copyFileSync(filePath, backupPath);
13331
13731
  fs25.writeFileSync(filePath, result.fixed);
13332
- console.log(import_chalk21.default.green(` \u2713 Applied. Backup: ${path23.basename(backupPath)}`));
13732
+ console.log(import_chalk22.default.green(` \u2713 Applied. Backup: ${path24.basename(backupPath)}`));
13333
13733
  }
13334
13734
  } catch (error) {
13335
- console.error(import_chalk21.default.red(`Error: ${error.message}`));
13735
+ console.error(import_chalk22.default.red(`Error: ${error.message}`));
13336
13736
  process.exit(1);
13337
13737
  }
13338
13738
  });
13339
13739
  }
13340
13740
 
13341
13741
  // src/commands/rules.ts
13342
- var import_chalk22 = __toESM(require_source());
13742
+ var import_chalk23 = __toESM(require_source());
13343
13743
  init_config();
13344
13744
  var RULE_CATALOG = [
13345
13745
  // AI Overused Words
@@ -13433,7 +13833,7 @@ function registerRulesCommand(program3) {
13433
13833
  for (const id of ids)
13434
13834
  disabledRules.delete(id);
13435
13835
  writeConfig({ ...config, disabled_rules: [...disabledRules] });
13436
- console.log(import_chalk22.default.green(`
13836
+ console.log(import_chalk23.default.green(`
13437
13837
  \u2713 Enabled: ${ids.join(", ")}`));
13438
13838
  return;
13439
13839
  }
@@ -13442,13 +13842,13 @@ function registerRulesCommand(program3) {
13442
13842
  for (const id of ids)
13443
13843
  disabledRules.add(id);
13444
13844
  writeConfig({ ...config, disabled_rules: [...disabledRules] });
13445
- console.log(import_chalk22.default.green(`
13845
+ console.log(import_chalk23.default.green(`
13446
13846
  \u2713 Disabled: ${ids.join(", ")}`));
13447
13847
  return;
13448
13848
  }
13449
13849
  if (options.reset) {
13450
13850
  writeConfig({ ...config, disabled_rules: [] });
13451
- console.log(import_chalk22.default.green("\n\u2713 All rules reset to default (enabled)"));
13851
+ console.log(import_chalk23.default.green("\n\u2713 All rules reset to default (enabled)"));
13452
13852
  return;
13453
13853
  }
13454
13854
  let rules = [...RULE_CATALOG];
@@ -13476,40 +13876,40 @@ function registerRulesCommand(program3) {
13476
13876
  }
13477
13877
  console.log("");
13478
13878
  for (const [cat, catRules] of categories) {
13479
- console.log(import_chalk22.default.bold(` ${cat} rules (${catRules.length})
13879
+ console.log(import_chalk23.default.bold(` ${cat} rules (${catRules.length})
13480
13880
  `));
13481
- console.log(import_chalk22.default.dim(" ID Sev AutoFix Status"));
13482
- console.log(import_chalk22.default.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
13881
+ console.log(import_chalk23.default.dim(" ID Sev AutoFix Status"));
13882
+ console.log(import_chalk23.default.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
13483
13883
  for (const rule of catRules) {
13484
- const sev = rule.severity === "red" ? import_chalk22.default.red("red") : import_chalk22.default.yellow("yel");
13485
- const fix = rule.autoFixable ? import_chalk22.default.green(" \u2713") : import_chalk22.default.dim(" \u2717");
13486
- const enabled2 = disabledRules.has(rule.id) ? import_chalk22.default.red(" \u2717 disabled") : import_chalk22.default.green(" \u2713 enabled");
13884
+ const sev = rule.severity === "red" ? import_chalk23.default.red("red") : import_chalk23.default.yellow("yel");
13885
+ const fix = rule.autoFixable ? import_chalk23.default.green(" \u2713") : import_chalk23.default.dim(" \u2717");
13886
+ const enabled2 = disabledRules.has(rule.id) ? import_chalk23.default.red(" \u2717 disabled") : import_chalk23.default.green(" \u2713 enabled");
13487
13887
  const id = rule.id.padEnd(24);
13488
- console.log(` ${import_chalk22.default.dim(id)} ${sev} ${fix} ${enabled2}`);
13888
+ console.log(` ${import_chalk23.default.dim(id)} ${sev} ${fix} ${enabled2}`);
13489
13889
  }
13490
13890
  console.log("");
13491
13891
  }
13492
13892
  const enabled = rules.filter((r) => !disabledRules.has(r.id)).length;
13493
13893
  const disabled = rules.length - enabled;
13494
- console.log(import_chalk22.default.dim(` ${rules.length} rules, ${enabled} enabled, ${disabled} disabled`));
13495
- console.log(import_chalk22.default.dim(` toggle: hyv rules --disable <id>`));
13496
- console.log(import_chalk22.default.dim(` reset: hyv rules --reset
13894
+ console.log(import_chalk23.default.dim(` ${rules.length} rules, ${enabled} enabled, ${disabled} disabled`));
13895
+ console.log(import_chalk23.default.dim(` toggle: hyv rules --disable <id>`));
13896
+ console.log(import_chalk23.default.dim(` reset: hyv rules --reset
13497
13897
  `));
13498
13898
  } catch (error) {
13499
- console.error(import_chalk22.default.red(`Error: ${error.message}`));
13899
+ console.error(import_chalk23.default.red(`Error: ${error.message}`));
13500
13900
  process.exit(1);
13501
13901
  }
13502
13902
  });
13503
13903
  }
13504
13904
 
13505
13905
  // src/commands/batch.ts
13506
- var import_chalk23 = __toESM(require_source());
13906
+ var import_chalk24 = __toESM(require_source());
13507
13907
  var fs16 = __toESM(require("fs"));
13508
- var path16 = __toESM(require("path"));
13908
+ var path17 = __toESM(require("path"));
13509
13909
  init_pipeline();
13510
13910
  init_local_profile();
13511
13911
  function registerBatchCommand(program3) {
13512
- program3.command("batch").description("Scan or fix multiple files matching a glob").argument("<pattern>", 'Glob pattern (e.g., "posts/**/*.md")').option("--fix", "Apply auto-fixes (default: scan only)").option("-i, --in-place", "Write fixes back to files").option("--threshold <n>", "Fail if any file score < threshold").option("--fail-on-hit", "Exit non-zero if any file has issues").option("--sort <field>", "Sort by: issues, score, name", "issues").option("--format <type>", "Output format (text, json, csv)", "text").option("--profile <name>", "Voice profile").option("--ignore <patterns>", "Comma-separated glob ignores").action(async (pattern, options) => {
13912
+ program3.command("batch").description("Scan or fix multiple files matching a glob").argument("<pattern>", 'Glob pattern (e.g., "posts/**/*.md")').option("--fix", "Apply auto-fixes (default: scan only)").option("-i, --in-place", "Write fixes back to files").option("-y, --yes", "Confirm destructive in-place fixes without prompting").option("--threshold <n>", "Fail if any file score < threshold").option("--fail-on-hit", "Exit with code 2 if any file has issues").option("--sort <field>", "Sort by: issues, score, name", "issues").option("--format <type>", "Output format (text, json, csv)", "text").option("--profile <name>", "Voice profile").option("--ignore <patterns>", "Comma-separated glob ignores").action(async (pattern, options) => {
13513
13913
  try {
13514
13914
  const profile = await loadProfileForCommand(options.profile);
13515
13915
  const glob = require_index_min();
@@ -13518,18 +13918,27 @@ function registerBatchCommand(program3) {
13518
13918
  nodir: true
13519
13919
  });
13520
13920
  if (files.length === 0) {
13521
- console.log(import_chalk23.default.yellow(`
13921
+ console.log(import_chalk24.default.yellow(`
13522
13922
  No files matching: ${pattern}`));
13523
13923
  return;
13524
13924
  }
13925
+ if (options.fix && options.inPlace) {
13926
+ const confirmed = await confirmDestructiveWrite({
13927
+ yes: options.yes,
13928
+ action: `Apply auto-fixes in-place to ${files.length} file(s)`,
13929
+ target: pattern
13930
+ });
13931
+ if (!confirmed)
13932
+ process.exit(1);
13933
+ }
13525
13934
  if (options.format === "text") {
13526
- console.log(import_chalk23.default.dim(`
13935
+ console.log(import_chalk24.default.dim(`
13527
13936
  scanning ${files.length} file${files.length === 1 ? "" : "s"}...
13528
13937
  `));
13529
13938
  }
13530
13939
  const results = [];
13531
13940
  for (const file of files) {
13532
- const absPath = path16.resolve(file);
13941
+ const absPath = path17.resolve(file);
13533
13942
  if (!fs16.existsSync(absPath))
13534
13943
  continue;
13535
13944
  const text = fs16.readFileSync(absPath, "utf-8");
@@ -13543,9 +13952,7 @@ No files matching: ${pattern}`));
13543
13952
  autoFixes: result.changes.length
13544
13953
  });
13545
13954
  if (options.fix && options.inPlace && result.changes.length > 0) {
13546
- const backupPath = absPath + ".bak";
13547
- fs16.copyFileSync(absPath, backupPath);
13548
- fs16.writeFileSync(absPath, result.fixed);
13955
+ writeInPlaceWithBackup(absPath, result.fixed);
13549
13956
  }
13550
13957
  }
13551
13958
  if (options.sort === "score") {
@@ -13564,98 +13971,110 @@ No files matching: ${pattern}`));
13564
13971
  }
13565
13972
  } else {
13566
13973
  for (const r of results) {
13567
- const icon = r.issues > 0 ? import_chalk23.default.red("\u25CF") : import_chalk23.default.green("\u25CB");
13568
- const score = r.score < 60 ? import_chalk23.default.red(r.score) : r.score < 80 ? import_chalk23.default.yellow(r.score) : import_chalk23.default.green(r.score);
13569
- const issueStr = r.issues > 0 ? import_chalk23.default.red(`${r.issues} issues`) : import_chalk23.default.green("clean");
13974
+ const icon = r.issues > 0 ? import_chalk24.default.red("\u25CF") : import_chalk24.default.green("\u25CB");
13975
+ const score = r.score < 60 ? import_chalk24.default.red(r.score) : r.score < 80 ? import_chalk24.default.yellow(r.score) : import_chalk24.default.green(r.score);
13976
+ const issueStr = r.issues > 0 ? import_chalk24.default.red(`${r.issues} issues`) : import_chalk24.default.green("clean");
13570
13977
  console.log(` ${icon} ${r.file.padEnd(40)} ${issueStr.padEnd(20)} score: ${score}`);
13571
13978
  }
13572
13979
  const withIssues = results.filter((r) => r.issues > 0).length;
13573
13980
  const totalIssues = results.reduce((sum, r) => sum + r.issues, 0);
13574
13981
  const worst = results.reduce((min, r) => r.score < min.score ? r : min, results[0]);
13575
- console.log(import_chalk23.default.dim(`
13982
+ console.log(import_chalk24.default.dim(`
13576
13983
  \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`));
13577
- console.log(import_chalk23.default.dim(` ${results.length} files, ${withIssues} with issues, ${totalIssues} total issues`));
13984
+ console.log(import_chalk24.default.dim(` ${results.length} files, ${withIssues} with issues, ${totalIssues} total issues`));
13578
13985
  if (withIssues > 0) {
13579
- console.log(import_chalk23.default.dim(` worst: ${worst.file} (${worst.score}/100)`));
13986
+ console.log(import_chalk24.default.dim(` worst: ${worst.file} (${worst.score}/100)`));
13580
13987
  }
13581
13988
  if (options.fix && options.inPlace) {
13582
13989
  const fixed = results.filter((r) => r.autoFixes > 0);
13583
- console.log(import_chalk23.default.green(`
13990
+ console.log(import_chalk24.default.green(`
13584
13991
  \u2713 ${fixed.reduce((sum, r) => sum + r.autoFixes, 0)} auto-fixes applied across ${fixed.length} files`));
13585
13992
  }
13586
13993
  }
13587
13994
  const threshold = options.threshold ? parseInt(options.threshold, 10) : 0;
13588
- const hasFailures = results.some(
13589
- (r) => options.failOnHit && r.issues > 0 || threshold > 0 && r.score < threshold
13590
- );
13591
- if (hasFailures)
13995
+ const failOnHit = results.some((r) => options.failOnHit && r.issues > 0);
13996
+ const belowThreshold = results.some((r) => threshold > 0 && r.score < threshold);
13997
+ if (failOnHit)
13998
+ process.exit(2);
13999
+ if (belowThreshold)
13592
14000
  process.exit(1);
13593
14001
  } catch (error) {
13594
- console.error(import_chalk23.default.red(`Error: ${error.message}`));
14002
+ console.error(import_chalk24.default.red(`Error: ${error.message}`));
13595
14003
  process.exit(1);
13596
14004
  }
13597
14005
  });
13598
14006
  }
13599
14007
 
13600
14008
  // src/commands/watch.ts
13601
- var import_chalk24 = __toESM(require_source());
14009
+ var import_chalk25 = __toESM(require_source());
13602
14010
  var fs17 = __toESM(require("fs"));
13603
- var path17 = __toESM(require("path"));
14011
+ var path18 = __toESM(require("path"));
13604
14012
  init_pipeline();
13605
14013
  init_local_profile();
13606
14014
  function registerWatchCommand(program3) {
13607
- program3.command("watch").description("Watch a file and re-scan on every save").argument("<file>", "File to watch").option("--command <cmd>", "What to run on change: scan, score, fix", "scan").option("--debounce <ms>", "Delay after save before scanning", "300").option("--notify", "OS notification when issues found").option("--profile <name>", "Voice profile").action(async (file, options) => {
14015
+ program3.command("watch").description("Watch a file and re-scan on every save").argument("<file>", "File to watch").option("--command <cmd>", "What to run on change: scan, score, fix", "scan").option("--debounce <ms>", "Delay after save before scanning", "300").option("--notify", "OS notification when issues found").option("-y, --yes", "Confirm destructive auto-fix writes without prompting").option("--profile <name>", "Voice profile").action(async (file, options) => {
13608
14016
  try {
13609
14017
  const profile = await loadProfileForCommand(options.profile);
13610
- const absPath = path17.resolve(file);
14018
+ const absPath = path18.resolve(file);
13611
14019
  if (!fs17.existsSync(absPath)) {
13612
- console.error(import_chalk24.default.red(`File not found: ${absPath}`));
14020
+ console.error(import_chalk25.default.red(`File not found: ${absPath}`));
13613
14021
  process.exit(1);
13614
14022
  }
13615
- console.log(import_chalk24.default.dim(`
14023
+ if (options.command === "fix") {
14024
+ const confirmed = await confirmDestructiveWrite({
14025
+ yes: options.yes,
14026
+ action: "Auto-fix on every save",
14027
+ target: absPath
14028
+ });
14029
+ if (!confirmed)
14030
+ process.exit(1);
14031
+ }
14032
+ console.log(import_chalk25.default.dim(`
13616
14033
  watching ${absPath}`));
13617
- console.log(import_chalk24.default.dim(`command: ${options.command} debounce: ${options.debounce}ms`));
13618
- console.log(import_chalk24.default.dim("ctrl+c to stop\n"));
14034
+ console.log(import_chalk25.default.dim(`command: ${options.command} debounce: ${options.debounce}ms`));
14035
+ console.log(import_chalk25.default.dim("ctrl+c to stop\n"));
13619
14036
  let debounceTimer = null;
13620
14037
  const debounceMs = parseInt(options.debounce, 10);
13621
14038
  let scanCount = 0;
13622
14039
  let lastScore = 0;
14040
+ let ignoreWatchUntil = 0;
13623
14041
  const runScan = () => {
13624
14042
  const text = fs17.readFileSync(absPath, "utf-8");
13625
14043
  const result = runPipeline(text, profile, options.command === "fix");
13626
14044
  scanCount++;
13627
14045
  const now = (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false });
13628
- console.log(import_chalk24.default.dim(`[${now}] saved \u2014 ${options.command}ing...`));
14046
+ console.log(import_chalk25.default.dim(`[${now}] saved \u2014 ${options.command}ing...`));
13629
14047
  if (options.command === "score") {
13630
14048
  const score = result.score;
13631
- const color = score < 60 ? import_chalk24.default.red : score < 80 ? import_chalk24.default.yellow : import_chalk24.default.green;
14049
+ const color = score < 60 ? import_chalk25.default.red : score < 80 ? import_chalk25.default.yellow : import_chalk25.default.green;
13632
14050
  console.log(` ${color(score + "/100")}`);
13633
14051
  if (lastScore > 0 && score !== lastScore) {
13634
14052
  const delta = score - lastScore;
13635
- console.log(import_chalk24.default.dim(` ${delta > 0 ? "\u2191" : "\u2193"} ${Math.abs(delta)} from last scan`));
14053
+ console.log(import_chalk25.default.dim(` ${delta > 0 ? "\u2191" : "\u2193"} ${Math.abs(delta)} from last scan`));
13636
14054
  }
13637
14055
  lastScore = score;
13638
14056
  } else if (options.command === "fix") {
13639
14057
  if (result.changes.length > 0) {
13640
14058
  for (const change of result.changes) {
13641
- console.log(import_chalk24.default.dim(` Line ${change.line}: `) + import_chalk24.default.red(change.before) + import_chalk24.default.dim(" \u2192 ") + import_chalk24.default.green(change.after));
14059
+ console.log(import_chalk25.default.dim(` Line ${change.line}: `) + import_chalk25.default.red(change.before) + import_chalk25.default.dim(" \u2192 ") + import_chalk25.default.green(change.after));
13642
14060
  }
13643
- fs17.writeFileSync(absPath, result.fixed);
13644
- console.log(import_chalk24.default.green(` \u2713 ${result.changes.length} auto-fixes applied`));
14061
+ ignoreWatchUntil = Date.now() + debounceMs + 200;
14062
+ writeInPlaceWithBackup(absPath, result.fixed);
14063
+ console.log(import_chalk25.default.green(` \u2713 ${result.changes.length} auto-fixes applied`));
13645
14064
  } else {
13646
- console.log(import_chalk24.default.green(" \u2713 no auto-fixable issues"));
14065
+ console.log(import_chalk25.default.green(" \u2713 no auto-fixable issues"));
13647
14066
  }
13648
14067
  } else {
13649
14068
  if (result.stats.totalSignals === 0) {
13650
- console.log(import_chalk24.default.green(` \u2713 clean \u2014 score: ${result.score}/100`));
14069
+ console.log(import_chalk25.default.green(` \u2713 clean \u2014 score: ${result.score}/100`));
13651
14070
  } else {
13652
- console.log(import_chalk24.default.yellow(` ${result.stats.totalSignals} issues (${result.stats.red} red, ${result.stats.yellow} yellow) score: ${result.score}/100`));
14071
+ console.log(import_chalk25.default.yellow(` ${result.stats.totalSignals} issues (${result.stats.red} red, ${result.stats.yellow} yellow) score: ${result.score}/100`));
13653
14072
  for (const signal of result.signalMap.signals.slice(0, 3)) {
13654
- const sev = signal.severity === "red" ? import_chalk24.default.red("\u25CF") : import_chalk24.default.yellow("\u25CB");
14073
+ const sev = signal.severity === "red" ? import_chalk25.default.red("\u25CF") : import_chalk25.default.yellow("\u25CB");
13655
14074
  console.log(` ${sev} line ${signal.line}: ${signal.id} \u2014 ${signal.suggestion}`);
13656
14075
  }
13657
14076
  if (result.signalMap.signals.length > 3) {
13658
- console.log(import_chalk24.default.dim(` ... and ${result.signalMap.signals.length - 3} more`));
14077
+ console.log(import_chalk25.default.dim(` ... and ${result.signalMap.signals.length - 3} more`));
13659
14078
  }
13660
14079
  }
13661
14080
  }
@@ -13665,26 +14084,28 @@ watching ${absPath}`));
13665
14084
  fs17.watch(absPath, (eventType) => {
13666
14085
  if (eventType !== "change")
13667
14086
  return;
14087
+ if (Date.now() < ignoreWatchUntil)
14088
+ return;
13668
14089
  if (debounceTimer)
13669
14090
  clearTimeout(debounceTimer);
13670
14091
  debounceTimer = setTimeout(runScan, debounceMs);
13671
14092
  });
13672
14093
  process.on("SIGINT", () => {
13673
- console.log(import_chalk24.default.dim(`
14094
+ console.log(import_chalk25.default.dim(`
13674
14095
  ${scanCount} scans completed. exiting.`));
13675
14096
  process.exit(0);
13676
14097
  });
13677
14098
  await new Promise(() => {
13678
14099
  });
13679
14100
  } catch (error) {
13680
- console.error(import_chalk24.default.red(`Error: ${error.message}`));
14101
+ console.error(import_chalk25.default.red(`Error: ${error.message}`));
13681
14102
  process.exit(1);
13682
14103
  }
13683
14104
  });
13684
14105
  }
13685
14106
 
13686
14107
  // src/commands/demo.ts
13687
- var import_chalk25 = __toESM(require_source());
14108
+ var import_chalk26 = __toESM(require_source());
13688
14109
  var fs18 = __toESM(require("fs"));
13689
14110
  var SAMPLES = {
13690
14111
  linkedin: {
@@ -13775,46 +14196,46 @@ function registerDemoCommand(program3) {
13775
14196
  const style = options.clean ? "clean" : options.style;
13776
14197
  const sample = SAMPLES[style];
13777
14198
  if (!sample) {
13778
- console.error(import_chalk25.default.red(`Unknown style: ${options.style}. Valid: ${Object.keys(SAMPLES).filter((k) => k !== "clean").join(", ")}`));
14199
+ console.error(import_chalk26.default.red(`Unknown style: ${options.style}. Valid: ${Object.keys(SAMPLES).filter((k) => k !== "clean").join(", ")}`));
13779
14200
  process.exit(1);
13780
14201
  }
13781
14202
  if (options.output) {
13782
14203
  const outputPath = require("path").resolve(options.output);
13783
14204
  fs18.writeFileSync(outputPath, sample.text);
13784
- console.log(import_chalk25.default.green(`
14205
+ console.log(import_chalk26.default.green(`
13785
14206
  \u2713 Sample written to ${outputPath}`));
13786
14207
  if (sample.patterns.length > 0) {
13787
- console.log(import_chalk25.default.dim(` Contains ${sample.patterns.length} AI patterns: ${sample.patterns.slice(0, 5).join(", ")}...`));
14208
+ console.log(import_chalk26.default.dim(` Contains ${sample.patterns.length} AI patterns: ${sample.patterns.slice(0, 5).join(", ")}...`));
13788
14209
  }
13789
- console.log(import_chalk25.default.dim(`
14210
+ console.log(import_chalk26.default.dim(`
13790
14211
  Scan it: hyv scan ${options.output}`));
13791
- console.log(import_chalk25.default.dim(` Fix it: hyv fix ${options.output}`));
14212
+ console.log(import_chalk26.default.dim(` Fix it: hyv fix ${options.output}`));
13792
14213
  } else {
13793
- console.log(import_chalk25.default.bold(`
14214
+ console.log(import_chalk26.default.bold(`
13794
14215
  \u2500\u2500\u2500 sample (${style}) \u2500\u2500\u2500
13795
14216
  `));
13796
14217
  console.log(sample.text);
13797
- console.log(import_chalk25.default.dim(`
14218
+ console.log(import_chalk26.default.dim(`
13798
14219
  \u2500\u2500\u2500 end \u2500\u2500\u2500`));
13799
14220
  if (sample.patterns.length > 0) {
13800
- console.log(import_chalk25.default.dim(`
14221
+ console.log(import_chalk26.default.dim(`
13801
14222
  ${sample.patterns.length} AI patterns embedded: ${sample.patterns.slice(0, 8).join(", ")}...`));
13802
14223
  }
13803
- console.log(import_chalk25.default.dim(`
14224
+ console.log(import_chalk26.default.dim(`
13804
14225
  Save to file: hyv demo --output demo.md`));
13805
- console.log(import_chalk25.default.dim(` Scan it: hyv scan demo.md`));
13806
- console.log(import_chalk25.default.dim(` Fix it: hyv fix demo.md
14226
+ console.log(import_chalk26.default.dim(` Scan it: hyv scan demo.md`));
14227
+ console.log(import_chalk26.default.dim(` Fix it: hyv fix demo.md
13807
14228
  `));
13808
14229
  }
13809
14230
  } catch (error) {
13810
- console.error(import_chalk25.default.red(`Error: ${error.message}`));
14231
+ console.error(import_chalk26.default.red(`Error: ${error.message}`));
13811
14232
  process.exit(1);
13812
14233
  }
13813
14234
  });
13814
14235
  }
13815
14236
 
13816
14237
  // src/commands/open.ts
13817
- var import_chalk26 = __toESM(require_source());
14238
+ var import_chalk27 = __toESM(require_source());
13818
14239
  var PAGES = {
13819
14240
  dashboard: "https://holdyourvoice.com/dashboard",
13820
14241
  profiles: "https://holdyourvoice.com/dashboard",
@@ -13831,12 +14252,12 @@ function registerOpenCommand(program3) {
13831
14252
  return;
13832
14253
  }
13833
14254
  const open3 = (await Promise.resolve().then(() => __toESM(require_open()))).default;
13834
- console.log(import_chalk26.default.dim(`
14255
+ console.log(import_chalk27.default.dim(`
13835
14256
  Opening ${url}...`));
13836
14257
  await open3(url);
13837
- console.log(import_chalk26.default.green(" \u2713 Opened in browser\n"));
14258
+ console.log(import_chalk27.default.green(" \u2713 Opened in browser\n"));
13838
14259
  } catch (error) {
13839
- console.error(import_chalk26.default.red(`Error: ${error.message}`));
14260
+ console.error(import_chalk27.default.red(`Error: ${error.message}`));
13840
14261
  process.exit(1);
13841
14262
  }
13842
14263
  });
@@ -13847,10 +14268,10 @@ init_welcome();
13847
14268
 
13848
14269
  // src/lib/onboarding.ts
13849
14270
  var fs19 = __toESM(require("fs"));
13850
- var path18 = __toESM(require("path"));
14271
+ var path19 = __toESM(require("path"));
13851
14272
  var os6 = __toESM(require("os"));
13852
- var hyvDir = path18.join(os6.homedir(), ".hyv");
13853
- var onboardingFile = path18.join(hyvDir, "onboarding-complete.json");
14273
+ var hyvDir = path19.join(os6.homedir(), ".hyv");
14274
+ var onboardingFile = path19.join(hyvDir, "onboarding-complete.json");
13854
14275
  function hasCompletedOnboarding() {
13855
14276
  try {
13856
14277
  return fs19.existsSync(onboardingFile);
@@ -13869,11 +14290,11 @@ function markOnboardingComplete(version) {
13869
14290
 
13870
14291
  // src/commands/welcome.ts
13871
14292
  var fs20 = __toESM(require("fs"));
13872
- var path19 = __toESM(require("path"));
14293
+ var path20 = __toESM(require("path"));
13873
14294
  function registerWelcomeCommand(program3) {
13874
14295
  const pkgVersion3 = (() => {
13875
14296
  try {
13876
- const pkgPath3 = path19.resolve(__dirname, "..", "..", "package.json");
14297
+ const pkgPath3 = path20.resolve(__dirname, "..", "package.json");
13877
14298
  return JSON.parse(fs20.readFileSync(pkgPath3, "utf-8")).version;
13878
14299
  } catch {
13879
14300
  return "0.0.0";
@@ -13898,7 +14319,7 @@ function getWelcomeText() {
13898
14319
  }
13899
14320
 
13900
14321
  // src/commands/content.ts
13901
- var import_chalk27 = __toESM(require_source());
14322
+ var import_chalk28 = __toESM(require_source());
13902
14323
  init_free_paid();
13903
14324
  var TEMPLATES = {
13904
14325
  cursor: {
@@ -13972,59 +14393,60 @@ ${COMMUNITY_URL}`
13972
14393
  function registerContentCommand(program3) {
13973
14394
  program3.command("content").description("Blog outlines, CI snippets, and share templates").argument("[topic]", "cursor | agents | ci | share", "cursor").option("--list", "List available templates").action((topic, opts) => {
13974
14395
  if (opts.list) {
13975
- console.log(import_chalk27.default.bold("\nhyv content templates\n"));
14396
+ console.log(import_chalk28.default.bold("\nhyv content templates\n"));
13976
14397
  for (const [key, t2] of Object.entries(TEMPLATES)) {
13977
- console.log(import_chalk27.default.dim(` ${key}`) + ` \u2014 ${t2.title}`);
14398
+ console.log(import_chalk28.default.dim(` ${key}`) + ` \u2014 ${t2.title}`);
13978
14399
  }
13979
14400
  console.log("");
13980
14401
  return;
13981
14402
  }
13982
14403
  const t = TEMPLATES[topic] || TEMPLATES.cursor;
13983
- console.log(import_chalk27.default.bold(`
14404
+ console.log(import_chalk28.default.bold(`
13984
14405
  ${t.title}
13985
14406
  `));
13986
14407
  console.log(t.body);
13987
- console.log(import_chalk27.default.dim("\nBlog: https://holdyourvoice.com/blog\n"));
14408
+ console.log(import_chalk28.default.dim("\nBlog: https://holdyourvoice.com/blog\n"));
13988
14409
  });
13989
14410
  }
13990
14411
 
13991
14412
  // src/commands/upgrade.ts
13992
- var import_chalk28 = __toESM(require_source());
13993
- var import_child_process2 = require("child_process");
14413
+ var import_chalk29 = __toESM(require_source());
14414
+ var import_child_process3 = require("child_process");
14415
+ init_version();
13994
14416
  function registerUpgradeCommand(program3) {
13995
14417
  program3.command("upgrade").description("Upgrade to the latest @holdyourvoice/hyv").option("--check", "Only check if an update is available").action(async (opts) => {
13996
14418
  const current = getCliVersion();
13997
- console.log(import_chalk28.default.dim(`
14419
+ console.log(import_chalk29.default.dim(`
13998
14420
  Current: ${getEngineLabel()}
13999
14421
  `));
14000
14422
  let latest = current;
14001
14423
  try {
14002
- const out = (0, import_child_process2.execSync)("npm view @holdyourvoice/hyv version", { encoding: "utf-8", timeout: 15e3 });
14424
+ const out = (0, import_child_process3.execSync)("npm view @holdyourvoice/hyv version", { encoding: "utf-8", timeout: 15e3 });
14003
14425
  latest = out.trim();
14004
14426
  } catch {
14005
- console.log(import_chalk28.default.yellow("Could not reach npm registry. Try: npm i -g @holdyourvoice/hyv@latest"));
14427
+ console.log(import_chalk29.default.yellow("Could not reach npm registry. Try: npm i -g @holdyourvoice/hyv@latest"));
14006
14428
  return;
14007
14429
  }
14008
14430
  const cmp = compareSemver(current, latest);
14009
14431
  if (cmp === 0) {
14010
- console.log(import_chalk28.default.green("\u2713 You are on the latest version."));
14432
+ console.log(import_chalk29.default.green("\u2713 You are on the latest version."));
14011
14433
  return;
14012
14434
  }
14013
14435
  if (cmp > 0) {
14014
- console.log(import_chalk28.default.green(`\u2713 You are ahead of npm (published: ${latest}).`));
14436
+ console.log(import_chalk29.default.green(`\u2713 You are ahead of npm (published: ${latest}).`));
14015
14437
  return;
14016
14438
  }
14017
- console.log(import_chalk28.default.cyan(`Update available: ${current} \u2192 ${latest}`));
14439
+ console.log(import_chalk29.default.cyan(`Update available: ${current} \u2192 ${latest}`));
14018
14440
  if (opts.check) {
14019
- console.log(import_chalk28.default.dim("\nRun: hyv upgrade or npm i -g @holdyourvoice/hyv@latest\n"));
14441
+ console.log(import_chalk29.default.dim("\nRun: hyv upgrade or npm i -g @holdyourvoice/hyv@latest\n"));
14020
14442
  return;
14021
14443
  }
14022
- console.log(import_chalk28.default.dim("Installing..."));
14444
+ console.log(import_chalk29.default.dim("Installing..."));
14023
14445
  try {
14024
- (0, import_child_process2.execSync)("npm i -g @holdyourvoice/hyv@latest", { stdio: "inherit" });
14025
- console.log(import_chalk28.default.green("\n\u2713 Upgraded successfully. Restart your terminal.\n"));
14446
+ (0, import_child_process3.execSync)("npm i -g @holdyourvoice/hyv@latest", { stdio: "inherit" });
14447
+ console.log(import_chalk29.default.green("\n\u2713 Upgraded successfully. Restart your terminal.\n"));
14026
14448
  } catch {
14027
- console.log(import_chalk28.default.red("\nUpgrade failed. Run manually: npm i -g @holdyourvoice/hyv@latest\n"));
14449
+ console.log(import_chalk29.default.red("\nUpgrade failed. Run manually: npm i -g @holdyourvoice/hyv@latest\n"));
14028
14450
  process.exit(1);
14029
14451
  }
14030
14452
  });
@@ -14032,7 +14454,7 @@ Current: ${getEngineLabel()}
14032
14454
 
14033
14455
  // src/mcp.ts
14034
14456
  var fs21 = __toESM(require("fs"));
14035
- var path20 = __toESM(require("path"));
14457
+ var path21 = __toESM(require("path"));
14036
14458
  var os7 = __toESM(require("os"));
14037
14459
  init_classifier();
14038
14460
  init_autofix();
@@ -14044,9 +14466,29 @@ init_welcome();
14044
14466
  init_config();
14045
14467
  init_telemetry();
14046
14468
  init_access();
14047
- var VOICE_MD = path20.join(os7.homedir(), ".hyv", "voice.md");
14469
+ var VOICE_MD = path21.join(os7.homedir(), ".hyv", "voice.md");
14048
14470
  var MAX_RESPONSE_CHARS = 12e4;
14049
- var pkgPath = path20.resolve(__dirname, "..", "package.json");
14471
+ var MAX_SCAN_FILE_BYTES = 2 * 1024 * 1024;
14472
+ function readScanFile(filePath) {
14473
+ const cwdReal = fs21.realpathSync(process.cwd());
14474
+ const resolved = fs21.realpathSync(path21.resolve(filePath));
14475
+ if (!resolved.startsWith(cwdReal + path21.sep) && resolved !== cwdReal) {
14476
+ throw new Error(`file must be in the current working directory (${cwdReal})`);
14477
+ }
14478
+ const stat = fs21.statSync(resolved);
14479
+ if (stat.size > MAX_SCAN_FILE_BYTES) {
14480
+ throw new Error(`file too large (max ${MAX_SCAN_FILE_BYTES} bytes)`);
14481
+ }
14482
+ return fs21.readFileSync(resolved, "utf-8");
14483
+ }
14484
+ function toolResultPayload(result) {
14485
+ const isError = result.startsWith("Error:") || result.startsWith("Unknown tool:");
14486
+ return {
14487
+ content: [{ type: "text", text: truncateResponse(result) }],
14488
+ ...isError ? { isError: true } : {}
14489
+ };
14490
+ }
14491
+ var pkgPath = path21.resolve(__dirname, "..", "package.json");
14050
14492
  var pkgVersion = (() => {
14051
14493
  try {
14052
14494
  return JSON.parse(fs21.readFileSync(pkgPath, "utf-8")).version;
@@ -14054,8 +14496,14 @@ var pkgVersion = (() => {
14054
14496
  return "2.7.1";
14055
14497
  }
14056
14498
  })();
14057
- async function resolveProfile(slug) {
14058
- return loadProfileForCommand(slug, { allowServerFetch: true });
14499
+ async function resolveProfile(slug, allowServerFetch = false) {
14500
+ const profile = await loadProfileForCommand(slug, { allowServerFetch });
14501
+ if (slug && !profile) {
14502
+ const { requirePaidFeature: requirePaidFeature2 } = await Promise.resolve().then(() => (init_access(), access_exports));
14503
+ await requirePaidFeature2("premiumPrompts");
14504
+ return loadProfileForCommand(slug, { allowServerFetch: true });
14505
+ }
14506
+ return profile;
14059
14507
  }
14060
14508
  function jsonRpcOk(id, result) {
14061
14509
  return JSON.stringify({ jsonrpc: "2.0", id, result });
@@ -14116,14 +14564,11 @@ async function toolScan(args2) {
14116
14564
  let scanText2 = text;
14117
14565
  const profile = await resolveProfile(args2.profile);
14118
14566
  if (filePath) {
14119
- const resolved = path20.resolve(filePath);
14120
- const cwd = process.cwd();
14121
- if (!resolved.startsWith(cwd + path20.sep) && resolved !== cwd) {
14122
- return `Error: file must be in the current working directory (${cwd})`;
14567
+ try {
14568
+ scanText2 = readScanFile(filePath);
14569
+ } catch (err) {
14570
+ return `Error: ${err.message}`;
14123
14571
  }
14124
- if (!fs21.existsSync(resolved))
14125
- return `Error: file not found: ${filePath}`;
14126
- scanText2 = fs21.readFileSync(resolved, "utf-8");
14127
14572
  }
14128
14573
  const result = runPipeline(scanText2, profile, false);
14129
14574
  if (result.stats.totalSignals === 0) {
@@ -14135,13 +14580,19 @@ async function toolScan(args2) {
14135
14580
  const lines = [
14136
14581
  `Score: ${result.score}/100. Found ${result.stats.totalSignals} issues (${result.stats.red} red, ${result.stats.yellow} yellow):`
14137
14582
  ];
14583
+ const shown = /* @__PURE__ */ new Set();
14138
14584
  if (profile && profileIssues.length > 0) {
14139
14585
  lines.push(`Profile-specific (${profileIssues.length}):`);
14140
14586
  for (const s of profileIssues.slice(0, 5)) {
14587
+ const key = `${s.line}:${s.id}`;
14588
+ shown.add(key);
14141
14589
  lines.push(` line ${s.line}: [${s.severity}] ${s.id} \u2014 ${s.suggestion}`);
14142
14590
  }
14143
14591
  }
14144
14592
  for (const s of result.signalMap.signals.slice(0, 15)) {
14593
+ const key = `${s.line}:${s.id}`;
14594
+ if (shown.has(key))
14595
+ continue;
14145
14596
  lines.push(`line ${s.line}: [${s.severity}] ${s.id} \u2014 ${s.suggestion}`);
14146
14597
  }
14147
14598
  return lines.join("\n");
@@ -14284,7 +14735,7 @@ async function toolAnalyze(args2) {
14284
14735
  return "Error: no text provided";
14285
14736
  try {
14286
14737
  const profile = await resolveProfile(args2.profile);
14287
- const preferServer = args2.prefer_server !== false;
14738
+ const preferServer = args2.prefer_server === true;
14288
14739
  const result = await runHybridAnalysis(text, profile, {
14289
14740
  preferServer,
14290
14741
  profileSlug: args2.profile || profile?.slug
@@ -14407,7 +14858,7 @@ var TOOLS = [
14407
14858
  properties: {
14408
14859
  text: { type: "string", description: "Text to analyze" },
14409
14860
  profile: { type: "string", description: "Voice profile slug" },
14410
- prefer_server: { type: "boolean", description: "Use server hybrid engine when paid (default true)" }
14861
+ prefer_server: { type: "boolean", description: "Use server hybrid engine when paid (default false)" }
14411
14862
  },
14412
14863
  required: ["text"]
14413
14864
  }
@@ -14599,9 +15050,12 @@ async function handleRequest(line) {
14599
15050
  mcpLog("error", `tool ${toolName}: ${err.message}`);
14600
15051
  recordEvent("mcp_tool_error", { tool: toolName, error: err.message });
14601
15052
  }
14602
- send(jsonRpcOk(id, { content: [{ type: "text", text: truncateResponse(result) }] }));
15053
+ send(jsonRpcOk(id, toolResultPayload(result)));
14603
15054
  break;
14604
15055
  }
15056
+ case "ping":
15057
+ send(jsonRpcOk(id, {}));
15058
+ break;
14605
15059
  default:
14606
15060
  if (id !== void 0) {
14607
15061
  send(jsonRpcError(id, -32601, `Method not found: ${method}`));
@@ -14621,16 +15075,17 @@ async function startMcpServer() {
14621
15075
  }
14622
15076
  let buffer = "";
14623
15077
  process.stdin.setEncoding("utf-8");
15078
+ let requestChain = Promise.resolve();
15079
+ const enqueueRequest = (line) => {
15080
+ requestChain = requestChain.then(() => handleRequest(line)).catch((err) => mcpLog("error", err.message));
15081
+ };
14624
15082
  process.stdin.on("data", (chunk) => {
14625
15083
  buffer += chunk;
14626
15084
  const lines = buffer.split("\n");
14627
15085
  buffer = lines.pop() || "";
14628
15086
  for (const line of lines) {
14629
- if (line.trim()) {
14630
- handleRequest(line.trim()).catch((err) => {
14631
- mcpLog("error", err.message);
14632
- });
14633
- }
15087
+ if (line.trim())
15088
+ enqueueRequest(line.trim());
14634
15089
  }
14635
15090
  });
14636
15091
  process.stdin.on("end", () => process.exit(0));
@@ -14642,104 +15097,137 @@ async function startMcpServer() {
14642
15097
  }
14643
15098
 
14644
15099
  // src/lib/mcp-setup.ts
14645
- var import_chalk29 = __toESM(require_source());
15100
+ var import_chalk30 = __toESM(require_source());
14646
15101
  var fs22 = __toESM(require("fs"));
14647
- var path21 = __toESM(require("path"));
15102
+ var path22 = __toESM(require("path"));
14648
15103
  var os8 = __toESM(require("os"));
15104
+ init_version();
14649
15105
  var HOME2 = os8.homedir();
14650
15106
  var IS_WIN2 = process.platform === "win32";
15107
+ function claudeDesktopConfigPath() {
15108
+ if (IS_WIN2)
15109
+ return path22.join(HOME2, "AppData", "Roaming", "Claude", "claude_desktop_config.json");
15110
+ if (process.platform === "linux")
15111
+ return path22.join(HOME2, ".config", "Claude", "claude_desktop_config.json");
15112
+ return path22.join(HOME2, "Library", "Application Support", "Claude", "claude_desktop_config.json");
15113
+ }
14651
15114
  function printMcpSetup() {
14652
- const claudeConfig = IS_WIN2 ? path21.join(HOME2, "AppData", "Roaming", "Claude", "claude_desktop_config.json") : path21.join(HOME2, "Library", "Application Support", "Claude", "claude_desktop_config.json");
14653
- console.log(import_chalk29.default.bold("\nhold your voice \u2014 mcp setup\n"));
14654
- console.log(import_chalk29.default.dim(` ${getEngineLabel()}
15115
+ console.log(import_chalk30.default.bold("\nhold your voice \u2014 mcp setup\n"));
15116
+ console.log(import_chalk30.default.dim(` ${getEngineLabel()}
14655
15117
  `));
14656
- console.log(import_chalk29.default.bold("Claude Desktop"));
14657
- console.log(import_chalk29.default.dim(" Add to claude_desktop_config.json \u2192 mcpServers:"));
14658
- console.log(import_chalk29.default.cyan(' "hyv": { "command": "hyv", "args": ["mcp"] }'));
14659
- console.log(import_chalk29.default.dim(` Config file: ${claudeConfig}
15118
+ console.log(import_chalk30.default.bold("Claude Desktop"));
15119
+ console.log(import_chalk30.default.dim(" Add to claude_desktop_config.json \u2192 mcpServers.hyv:"));
15120
+ console.log(import_chalk30.default.cyan(` ${mcpServerSnippet().split("\n").join("\n ")}`));
15121
+ console.log(import_chalk30.default.dim(` Config: ${claudeDesktopConfigPath()}
14660
15122
  `));
14661
- console.log(import_chalk29.default.bold("Cursor"));
14662
- console.log(import_chalk29.default.dim(" Rule file: ~/.cursor/rules/hyv.md"));
14663
- console.log(import_chalk29.default.dim(" Run: hyv doctor --fix-agents to copy latest instructions\n"));
14664
- console.log(import_chalk29.default.bold("Claude Code"));
14665
- console.log(import_chalk29.default.dim(" Command: ~/.claude/commands/hyv.md\n"));
14666
- console.log(import_chalk29.default.bold("Windsurf / Codex / ChatGPT"));
14667
- console.log(import_chalk29.default.dim(" hyv doctor --fix-agents copies agent files from the package\n"));
14668
- console.log(import_chalk29.default.bold("Verify"));
14669
- console.log(import_chalk29.default.dim(" hyv mcp --test"));
14670
- console.log(import_chalk29.default.dim(" HYV_TELEMETRY=1 hyv mcp (optional usage logging to ~/.hyv/telemetry/)\n"));
15123
+ console.log(import_chalk30.default.bold("Cursor"));
15124
+ console.log(import_chalk30.default.dim(" MCP: ~/.cursor/mcp.json \u2192 mcpServers.hyv (same JSON as above)"));
15125
+ console.log(import_chalk30.default.dim(" Rule: ~/.cursor/rules/hyv.mdc (alwaysApply \u2014 auto via postinstall)\n"));
15126
+ console.log(import_chalk30.default.bold("Claude Code"));
15127
+ console.log(import_chalk30.default.dim(" Command: ~/.claude/commands/hyv.md"));
15128
+ console.log(import_chalk30.default.dim(" Skill: ~/.claude/skills/hold-your-voice/SKILL.md\n"));
15129
+ console.log(import_chalk30.default.bold("Windsurf"));
15130
+ console.log(import_chalk30.default.dim(" Rule: ~/.windsurf/rules/hyv.md (trigger: always_on)\n"));
15131
+ console.log(import_chalk30.default.bold("Codex"));
15132
+ console.log(import_chalk30.default.dim(" Instructions: ~/.codex/AGENTS.md (merged on install)\n"));
15133
+ console.log(import_chalk30.default.bold("Command Code"));
15134
+ console.log(import_chalk30.default.dim(" Skill: ~/.commandcode/skills/hyv/SKILL.md\n"));
15135
+ console.log(import_chalk30.default.bold("ChatGPT"));
15136
+ console.log(import_chalk30.default.dim(" hyv mcp --setup-chatgpt\n"));
15137
+ console.log(import_chalk30.default.bold("Auto-configure"));
15138
+ console.log(import_chalk30.default.dim(" hyv doctor --fix-agents"));
15139
+ console.log(import_chalk30.default.dim(" HYV_AUTO_CONFIGURE_AGENTS=0 npm i -g @holdyourvoice/hyv (skip)\n"));
15140
+ console.log(import_chalk30.default.bold("Verify"));
15141
+ console.log(import_chalk30.default.dim(" hyv mcp --test"));
15142
+ console.log(import_chalk30.default.dim(" HYV_TELEMETRY=1 hyv mcp (optional usage logging to ~/.hyv/telemetry/)\n"));
14671
15143
  }
14672
15144
  async function runMcpSelfTest() {
14673
- console.log(import_chalk29.default.bold("\nhold your voice \u2014 mcp self-test\n"));
14674
- console.log(import_chalk29.default.dim(` ${getEngineLabel()}
15145
+ console.log(import_chalk30.default.bold("\nhold your voice \u2014 mcp self-test\n"));
15146
+ console.log(import_chalk30.default.dim(` ${getEngineLabel()}
14675
15147
  `));
14676
15148
  let passed = 0;
14677
15149
  let failed = 0;
14678
15150
  const tools = getMcpToolNames();
14679
15151
  if (tools.length >= 10) {
14680
- console.log(import_chalk29.default.green(` \u2713 ${tools.length} MCP tools registered`));
15152
+ console.log(import_chalk30.default.green(` \u2713 ${tools.length} MCP tools registered`));
14681
15153
  passed++;
14682
15154
  } else {
14683
- console.log(import_chalk29.default.red(` \u2717 expected 10+ tools, got ${tools.length}`));
15155
+ console.log(import_chalk30.default.red(` \u2717 expected 10+ tools, got ${tools.length}`));
14684
15156
  failed++;
14685
15157
  }
14686
- const required = ["hyv_welcome", "hyv_scan", "hyv_analyze", "hyv_clean", "hyv_validate"];
15158
+ const required = ["hyv_welcome", "hyv_scan", "hyv_analyze", "hyv_clean", "hyv_validate", "hyv_list_free_tools"];
14687
15159
  for (const name of required) {
14688
15160
  if (tools.includes(name)) {
14689
- console.log(import_chalk29.default.green(` \u2713 tool: ${name}`));
15161
+ console.log(import_chalk30.default.green(` \u2713 tool: ${name}`));
14690
15162
  passed++;
14691
15163
  } else {
14692
- console.log(import_chalk29.default.red(` \u2717 missing tool: ${name}`));
15164
+ console.log(import_chalk30.default.red(` \u2717 missing tool: ${name}`));
14693
15165
  failed++;
14694
15166
  }
14695
15167
  }
14696
15168
  try {
14697
15169
  const welcome = await invokeMcpTool("hyv_welcome", {});
14698
15170
  if (welcome.includes("Hold Your Voice") || welcome.includes("hold your voice")) {
14699
- console.log(import_chalk29.default.green(" \u2713 hyv_welcome responds"));
15171
+ console.log(import_chalk30.default.green(" \u2713 hyv_welcome responds"));
14700
15172
  passed++;
14701
15173
  } else {
14702
- console.log(import_chalk29.default.red(" \u2717 hyv_welcome unexpected output"));
15174
+ console.log(import_chalk30.default.red(" \u2717 hyv_welcome unexpected output"));
14703
15175
  failed++;
14704
15176
  }
14705
15177
  } catch (e) {
14706
- console.log(import_chalk29.default.red(` \u2717 hyv_welcome failed: ${e.message}`));
15178
+ console.log(import_chalk30.default.red(` \u2717 hyv_welcome failed: ${e.message}`));
14707
15179
  failed++;
14708
15180
  }
14709
15181
  try {
14710
15182
  const demo = await invokeMcpTool("hyv_demo", {});
14711
15183
  if (demo.includes("Score") || demo.includes("issues")) {
14712
- console.log(import_chalk29.default.green(" \u2713 hyv_demo pipeline works"));
15184
+ console.log(import_chalk30.default.green(" \u2713 hyv_demo pipeline works"));
14713
15185
  passed++;
14714
15186
  } else {
14715
- console.log(import_chalk29.default.red(" \u2717 hyv_demo unexpected output"));
15187
+ console.log(import_chalk30.default.red(" \u2717 hyv_demo unexpected output"));
14716
15188
  failed++;
14717
15189
  }
14718
15190
  } catch (e) {
14719
- console.log(import_chalk29.default.red(` \u2717 hyv_demo failed: ${e.message}`));
15191
+ console.log(import_chalk30.default.red(` \u2717 hyv_demo failed: ${e.message}`));
14720
15192
  failed++;
14721
15193
  }
14722
- const voiceMd = path21.join(HOME2, ".hyv", "voice.md");
15194
+ const voiceMd = path22.join(HOME2, ".hyv", "voice.md");
14723
15195
  if (fs22.existsSync(voiceMd)) {
14724
- console.log(import_chalk29.default.green(" \u2713 voice profile found"));
15196
+ console.log(import_chalk30.default.green(" \u2713 voice profile found"));
14725
15197
  passed++;
14726
15198
  } else {
14727
- console.log(import_chalk29.default.dim(" - no voice.md (free local engine still works)"));
15199
+ console.log(import_chalk30.default.dim(" - no voice.md (free local engine still works)"));
14728
15200
  }
14729
15201
  if (getDemoText().length > 100) {
14730
- console.log(import_chalk29.default.green(" \u2713 demo content available"));
15202
+ console.log(import_chalk30.default.green(" \u2713 demo content available"));
14731
15203
  passed++;
14732
15204
  } else {
14733
- console.log(import_chalk29.default.red(" \u2717 demo content missing"));
15205
+ console.log(import_chalk30.default.red(" \u2717 demo content missing"));
15206
+ failed++;
15207
+ }
15208
+ try {
15209
+ const stdio = await testMcpStdioSubprocess();
15210
+ if (stdio.ok && (stdio.toolCount || 0) >= 10) {
15211
+ console.log(import_chalk30.default.green(` \u2713 stdio MCP subprocess responds (${stdio.toolCount} tools)`));
15212
+ passed++;
15213
+ } else if (stdio.ok) {
15214
+ console.log(import_chalk30.default.yellow(` ! stdio MCP subprocess ok but only ${stdio.toolCount || 0} tools`));
15215
+ failed++;
15216
+ } else {
15217
+ console.log(import_chalk30.default.red(" \u2717 stdio MCP subprocess failed"));
15218
+ failed++;
15219
+ }
15220
+ } catch (e) {
15221
+ console.log(import_chalk30.default.red(` \u2717 stdio MCP subprocess failed: ${e.message}`));
14734
15222
  failed++;
14735
15223
  }
14736
15224
  console.log("");
14737
15225
  if (failed === 0) {
14738
- console.log(import_chalk29.default.green(`\u2713 all checks passed (${passed})`));
14739
- console.log(import_chalk29.default.dim("\nStart server: hyv mcp\n"));
15226
+ console.log(import_chalk30.default.green(`\u2713 all checks passed (${passed})`));
15227
+ console.log(import_chalk30.default.dim("\nStart server: hyv mcp\n"));
14740
15228
  return true;
14741
15229
  }
14742
- console.log(import_chalk29.default.yellow(`! ${failed} check(s) failed, ${passed} passed`));
15230
+ console.log(import_chalk30.default.yellow(`! ${failed} check(s) failed, ${passed} passed`));
14743
15231
  return false;
14744
15232
  }
14745
15233
 
@@ -14869,9 +15357,9 @@ async function exportCommand(format, opts) {
14869
15357
  init_access();
14870
15358
  init_welcome();
14871
15359
  var fs24 = __toESM(require("fs"));
14872
- var path22 = __toESM(require("path"));
15360
+ var path23 = __toESM(require("path"));
14873
15361
  var program2 = new Command();
14874
- var pkgPath2 = path22.resolve(__dirname, "..", "package.json");
15362
+ var pkgPath2 = path23.resolve(__dirname, "..", "package.json");
14875
15363
  var pkgVersion2 = (() => {
14876
15364
  try {
14877
15365
  return JSON.parse(fs24.readFileSync(pkgPath2, "utf-8")).version;
@@ -14918,15 +15406,15 @@ program2.command("mcp").description("Start MCP server (for Claude Desktop and ot
14918
15406
  return;
14919
15407
  }
14920
15408
  if (opts.setupChatgpt) {
14921
- console.log(import_chalk30.default.bold("\nhold your voice \u2014 chatgpt setup\n"));
15409
+ console.log(import_chalk31.default.bold("\nhold your voice \u2014 chatgpt setup\n"));
14922
15410
  console.log("To connect HYV to ChatGPT:");
14923
- console.log(import_chalk30.default.dim(" 1. Go to ") + import_chalk30.default.cyan("https://chatgpt.com/#settings/Connectors"));
14924
- console.log(import_chalk30.default.dim(" 2. Add a new connector"));
14925
- console.log(import_chalk30.default.dim(" 3. For local MCP, use: ") + import_chalk30.default.cyan("hyv mcp"));
14926
- console.log(import_chalk30.default.dim(" 4. ChatGPT Desktop supports stdio MCP servers"));
15411
+ console.log(import_chalk31.default.dim(" 1. Go to ") + import_chalk31.default.cyan("https://chatgpt.com/#settings/Connectors"));
15412
+ console.log(import_chalk31.default.dim(" 2. Add a new connector"));
15413
+ console.log(import_chalk31.default.dim(" 3. For local MCP, use: ") + import_chalk31.default.cyan("hyv mcp"));
15414
+ console.log(import_chalk31.default.dim(" 4. ChatGPT Desktop supports stdio MCP servers"));
14927
15415
  console.log("");
14928
- console.log(import_chalk30.default.dim("Note: The remote HTTP MCP endpoint is not yet available."));
14929
- console.log(import_chalk30.default.dim("Use the local stdio MCP server with Claude Desktop or Claude Code instead."));
15416
+ console.log(import_chalk31.default.dim("Note: The remote HTTP MCP endpoint is not yet available."));
15417
+ console.log(import_chalk31.default.dim("Use the local stdio MCP server with Claude Desktop or Claude Code instead."));
14930
15418
  return;
14931
15419
  }
14932
15420
  startMcpServer();