@holdyourvoice/hyv 2.8.7 → 2.8.8

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
@@ -974,7 +974,7 @@ var require_command = __commonJS({
974
974
  var EventEmitter = require("node:events").EventEmitter;
975
975
  var childProcess = require("node:child_process");
976
976
  var path23 = require("node:path");
977
- var fs25 = require("node:fs");
977
+ var fs24 = require("node:fs");
978
978
  var process2 = require("node:process");
979
979
  var { Argument: Argument2, humanReadableArgName } = require_argument();
980
980
  var { CommanderError: CommanderError2 } = require_error();
@@ -1917,12 +1917,12 @@ Expecting one of '${allowedValues.join("', '")}'`);
1917
1917
  const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
1918
1918
  function findFile(baseDir, baseName) {
1919
1919
  const localBin = path23.resolve(baseDir, baseName);
1920
- if (fs25.existsSync(localBin))
1920
+ if (fs24.existsSync(localBin))
1921
1921
  return localBin;
1922
1922
  if (sourceExt.includes(path23.extname(baseName)))
1923
1923
  return void 0;
1924
1924
  const foundExt = sourceExt.find(
1925
- (ext) => fs25.existsSync(`${localBin}${ext}`)
1925
+ (ext) => fs24.existsSync(`${localBin}${ext}`)
1926
1926
  );
1927
1927
  if (foundExt)
1928
1928
  return `${localBin}${foundExt}`;
@@ -1935,7 +1935,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1935
1935
  if (this._scriptPath) {
1936
1936
  let resolvedScriptPath;
1937
1937
  try {
1938
- resolvedScriptPath = fs25.realpathSync(this._scriptPath);
1938
+ resolvedScriptPath = fs24.realpathSync(this._scriptPath);
1939
1939
  } catch (err) {
1940
1940
  resolvedScriptPath = this._scriptPath;
1941
1941
  }
@@ -4612,14 +4612,97 @@ var require_source = __commonJS({
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
 
@@ -4742,11 +4834,11 @@ var init_config = __esm({
4742
4834
  var require_is_docker = __commonJS({
4743
4835
  "node_modules/is-docker/index.js"(exports2, module2) {
4744
4836
  "use strict";
4745
- var fs25 = require("fs");
4837
+ var fs24 = require("fs");
4746
4838
  var isDocker;
4747
4839
  function hasDockerEnv() {
4748
4840
  try {
4749
- fs25.statSync("/.dockerenv");
4841
+ fs24.statSync("/.dockerenv");
4750
4842
  return true;
4751
4843
  } catch (_) {
4752
4844
  return false;
@@ -4754,7 +4846,7 @@ var require_is_docker = __commonJS({
4754
4846
  }
4755
4847
  function hasDockerCGroup() {
4756
4848
  try {
4757
- return fs25.readFileSync("/proc/self/cgroup", "utf8").includes("docker");
4849
+ return fs24.readFileSync("/proc/self/cgroup", "utf8").includes("docker");
4758
4850
  } catch (_) {
4759
4851
  return false;
4760
4852
  }
@@ -4773,7 +4865,7 @@ var require_is_wsl = __commonJS({
4773
4865
  "node_modules/is-wsl/index.js"(exports2, module2) {
4774
4866
  "use strict";
4775
4867
  var os9 = require("os");
4776
- var fs25 = require("fs");
4868
+ var fs24 = require("fs");
4777
4869
  var isDocker = require_is_docker();
4778
4870
  var isWsl = () => {
4779
4871
  if (process.platform !== "linux") {
@@ -4786,7 +4878,7 @@ var require_is_wsl = __commonJS({
4786
4878
  return true;
4787
4879
  }
4788
4880
  try {
4789
- return fs25.readFileSync("/proc/version", "utf8").toLowerCase().includes("microsoft") ? !isDocker() : false;
4881
+ return fs24.readFileSync("/proc/version", "utf8").toLowerCase().includes("microsoft") ? !isDocker() : false;
4790
4882
  } catch (_) {
4791
4883
  return false;
4792
4884
  }
@@ -4827,7 +4919,7 @@ var require_open = __commonJS({
4827
4919
  "node_modules/open/index.js"(exports2, module2) {
4828
4920
  var path23 = require("path");
4829
4921
  var childProcess = require("child_process");
4830
- var { promises: fs25, constants: fsConstants } = require("fs");
4922
+ var { promises: fs24, 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();
@@ -4835,7 +4927,7 @@ var require_open = __commonJS({
4835
4927
  var { platform, arch } = process;
4836
4928
  var hasContainerEnv = () => {
4837
4929
  try {
4838
- fs25.statSync("/run/.containerenv");
4930
+ fs24.statSync("/run/.containerenv");
4839
4931
  return true;
4840
4932
  } catch {
4841
4933
  return false;
@@ -4858,14 +4950,14 @@ var require_open = __commonJS({
4858
4950
  const configFilePath = "/etc/wsl.conf";
4859
4951
  let isConfigFileExists = false;
4860
4952
  try {
4861
- await fs25.access(configFilePath, fsConstants.F_OK);
4953
+ await fs24.access(configFilePath, fsConstants.F_OK);
4862
4954
  isConfigFileExists = true;
4863
4955
  } catch {
4864
4956
  }
4865
4957
  if (!isConfigFileExists) {
4866
4958
  return defaultMountPoint;
4867
4959
  }
4868
- const configContent = await fs25.readFile(configFilePath, { encoding: "utf8" });
4960
+ const configContent = await fs24.readFile(configFilePath, { encoding: "utf8" });
4869
4961
  const configMountPoint = /(?<!#.*)root\s*=\s*(?<mountPoint>.*)/g.exec(configContent);
4870
4962
  if (!configMountPoint) {
4871
4963
  return defaultMountPoint;
@@ -4965,7 +5057,7 @@ var require_open = __commonJS({
4965
5057
  const isBundled = !__dirname || __dirname === "/";
4966
5058
  let exeLocalXdgOpen = false;
4967
5059
  try {
4968
- await fs25.access(localXdgOpenPath, fsConstants.X_OK);
5060
+ await fs24.access(localXdgOpenPath, fsConstants.X_OK);
4969
5061
  exeLocalXdgOpen = true;
4970
5062
  } catch {
4971
5063
  }
@@ -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,81 @@ 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
+
5080
5247
  // src/lib/auth.ts
5081
5248
  var auth_exports = {};
5082
5249
  __export(auth_exports, {
@@ -5084,10 +5251,14 @@ __export(auth_exports, {
5084
5251
  authenticateWithLicense: () => authenticateWithLicense,
5085
5252
  authenticatedRequest: () => authenticatedRequest,
5086
5253
  checkSession: () => checkSession,
5254
+ getValidToken: () => getValidToken,
5087
5255
  refreshToken: () => refreshToken
5088
5256
  });
5257
+ function escapeHtml(value) {
5258
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
5259
+ }
5089
5260
  function request(url, options = {}) {
5090
- return new Promise((resolve12, reject) => {
5261
+ return new Promise((resolve14, reject) => {
5091
5262
  const urlObj = new URL(url);
5092
5263
  const isHttps = urlObj.protocol === "https:";
5093
5264
  const client = isHttps ? https : http;
@@ -5098,7 +5269,7 @@ function request(url, options = {}) {
5098
5269
  method: options.method || "GET",
5099
5270
  headers: {
5100
5271
  "Content-Type": "application/json",
5101
- "User-Agent": "hyv-cli/2.3.1",
5272
+ "User-Agent": `hyv-cli/${getCliVersion()}`,
5102
5273
  ...options.headers
5103
5274
  },
5104
5275
  timeout: options.timeout || 3e4
@@ -5108,9 +5279,9 @@ function request(url, options = {}) {
5108
5279
  res.on("data", (chunk) => data += chunk);
5109
5280
  res.on("end", () => {
5110
5281
  try {
5111
- resolve12({ status: res.statusCode || 0, data: JSON.parse(data) });
5282
+ resolve14({ status: res.statusCode || 0, data: JSON.parse(data) });
5112
5283
  } catch {
5113
- resolve12({ status: res.statusCode || 0, data });
5284
+ resolve14({ status: res.statusCode || 0, data });
5114
5285
  }
5115
5286
  });
5116
5287
  });
@@ -5125,8 +5296,25 @@ function request(url, options = {}) {
5125
5296
  req.end();
5126
5297
  });
5127
5298
  }
5299
+ async function getValidToken() {
5300
+ const auth = readAuth();
5301
+ if (!auth?.token)
5302
+ return null;
5303
+ if (auth.expires_at) {
5304
+ const expires = new Date(auth.expires_at);
5305
+ const refreshBy = Date.now() + 5 * 60 * 1e3;
5306
+ if (expires.getTime() <= refreshBy) {
5307
+ const ok = await refreshToken(auth.token);
5308
+ if (ok)
5309
+ return getToken();
5310
+ if (expires.getTime() < Date.now())
5311
+ return null;
5312
+ }
5313
+ }
5314
+ return auth.token;
5315
+ }
5128
5316
  async function authenticatedRequest(url, options = {}) {
5129
- const token = getToken();
5317
+ const token = await getValidToken();
5130
5318
  if (!token) {
5131
5319
  throw new Error("Not authenticated. Run `hyv init` first.");
5132
5320
  }
@@ -5155,9 +5343,9 @@ async function authenticateWithLicense(licenseKey) {
5155
5343
  }
5156
5344
  async function authenticateWithBrowser() {
5157
5345
  const server = http.createServer();
5158
- const port = await new Promise((resolve12) => {
5346
+ const port = await new Promise((resolve14) => {
5159
5347
  server.listen(0, "127.0.0.1", () => {
5160
- resolve12(server.address().port);
5348
+ resolve14(server.address().port);
5161
5349
  });
5162
5350
  });
5163
5351
  const redirectUri = `http://127.0.0.1:${port}/callback`;
@@ -5169,10 +5357,14 @@ async function authenticateWithBrowser() {
5169
5357
  server.close();
5170
5358
  throw new Error("Failed to start authentication flow");
5171
5359
  }
5172
- const { auth_url } = response.data;
5360
+ const { auth_url, state: expectedState } = response.data;
5361
+ if (!expectedState) {
5362
+ server.close();
5363
+ throw new Error("Authentication server did not return OAuth state");
5364
+ }
5173
5365
  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) => {
5366
+ await (0, import_open.default)(assertSafeOpenUrl(auth_url));
5367
+ const authData = await new Promise((resolve14, reject) => {
5176
5368
  const timeout = setTimeout(() => {
5177
5369
  server.close();
5178
5370
  reject(new Error("Authentication timeout. Please try again."));
@@ -5190,6 +5382,14 @@ async function authenticateWithBrowser() {
5190
5382
  reject(new Error("Invalid callback parameters"));
5191
5383
  return;
5192
5384
  }
5385
+ if (state !== expectedState) {
5386
+ res.writeHead(400, { "Content-Type": "text/html" });
5387
+ res.end("<h1>Authentication failed</h1><p>Invalid OAuth state.</p>");
5388
+ clearTimeout(timeout);
5389
+ server.close();
5390
+ reject(new Error("OAuth state mismatch \u2014 possible CSRF attempt"));
5391
+ return;
5392
+ }
5193
5393
  try {
5194
5394
  const callbackResponse = await request(`${API_BASE}/cli/auth/callback`, {
5195
5395
  method: "POST",
@@ -5210,10 +5410,10 @@ async function authenticateWithBrowser() {
5210
5410
  `);
5211
5411
  clearTimeout(timeout);
5212
5412
  server.close();
5213
- resolve12(data);
5413
+ resolve14(data);
5214
5414
  } catch (error) {
5215
5415
  res.writeHead(500, { "Content-Type": "text/html" });
5216
- res.end(`<h1>Authentication failed</h1><p>${error.message}</p>`);
5416
+ res.end(`<h1>Authentication failed</h1><p>${escapeHtml(error.message)}</p>`);
5217
5417
  clearTimeout(timeout);
5218
5418
  server.close();
5219
5419
  reject(error);
@@ -5227,8 +5427,8 @@ async function authenticateWithBrowser() {
5227
5427
  writeAuth(authData);
5228
5428
  return authData;
5229
5429
  }
5230
- async function refreshToken() {
5231
- const token = getToken();
5430
+ async function refreshToken(tokenOverride) {
5431
+ const token = tokenOverride || readAuth()?.token;
5232
5432
  if (!token)
5233
5433
  return false;
5234
5434
  try {
@@ -5254,7 +5454,7 @@ async function refreshToken() {
5254
5454
  }
5255
5455
  }
5256
5456
  async function checkSession() {
5257
- const token = getToken();
5457
+ const token = await getValidToken();
5258
5458
  if (!token)
5259
5459
  return { valid: false };
5260
5460
  try {
@@ -5280,6 +5480,7 @@ var init_auth = __esm({
5280
5480
  import_chalk = __toESM(require_source());
5281
5481
  import_open = __toESM(require_open());
5282
5482
  init_config();
5483
+ init_version();
5283
5484
  }
5284
5485
  });
5285
5486
 
@@ -5413,25 +5614,23 @@ function recordEvent(event, meta) {
5413
5614
  if (!isTelemetryEnabled())
5414
5615
  return;
5415
5616
  try {
5416
- if (!fs2.existsSync(TELEMETRY_DIR))
5417
- fs2.mkdirSync(TELEMETRY_DIR, { recursive: true });
5418
- fs2.appendFileSync(TELEMETRY_FILE, JSON.stringify({
5617
+ appendSecureLine(TELEMETRY_FILE, JSON.stringify({
5419
5618
  event,
5420
5619
  ts: (/* @__PURE__ */ new Date()).toISOString(),
5421
5620
  ...meta
5422
- }) + "\n");
5621
+ }) + "\n", TELEMETRY_DIR);
5423
5622
  } catch {
5424
5623
  }
5425
5624
  }
5426
- var fs2, path2, os2, TELEMETRY_DIR, TELEMETRY_FILE;
5625
+ var path3, os2, TELEMETRY_DIR, TELEMETRY_FILE;
5427
5626
  var init_telemetry = __esm({
5428
5627
  "src/lib/telemetry.ts"() {
5429
5628
  "use strict";
5430
- fs2 = __toESM(require("fs"));
5431
- path2 = __toESM(require("path"));
5629
+ path3 = __toESM(require("path"));
5432
5630
  os2 = __toESM(require("os"));
5433
- TELEMETRY_DIR = path2.join(os2.homedir(), ".hyv", "telemetry");
5434
- TELEMETRY_FILE = path2.join(TELEMETRY_DIR, "events.jsonl");
5631
+ init_config();
5632
+ TELEMETRY_DIR = path3.join(os2.homedir(), ".hyv", "telemetry");
5633
+ TELEMETRY_FILE = path3.join(TELEMETRY_DIR, "events.jsonl");
5435
5634
  }
5436
5635
  });
5437
5636
 
@@ -5443,7 +5642,7 @@ __export(api_exports, {
5443
5642
  requireSubscription: () => requireSubscription
5444
5643
  });
5445
5644
  async function request2(method, path23, body) {
5446
- const token = getToken();
5645
+ const token = await getValidToken();
5447
5646
  if (!token)
5448
5647
  throw new Error("you're not signed in yet. run: hyv init");
5449
5648
  const url = `${API_BASE}${path23}`;
@@ -5485,6 +5684,7 @@ var init_api = __esm({
5485
5684
  "src/lib/api.ts"() {
5486
5685
  "use strict";
5487
5686
  init_config();
5687
+ init_auth();
5488
5688
  }
5489
5689
  });
5490
5690
 
@@ -5679,7 +5879,7 @@ async function refreshInBackground(slug) {
5679
5879
  }
5680
5880
  }
5681
5881
  function getDiskCachePath(slug) {
5682
- return path3.join(DISK_CACHE_DIR, `${slug.replace(/[^a-z0-9-]/gi, "_")}.json`);
5882
+ return path4.join(DISK_CACHE_DIR, `${slug.replace(/[^a-z0-9-]/gi, "_")}.json`);
5683
5883
  }
5684
5884
  function loadFromDiskCache(slug) {
5685
5885
  try {
@@ -5718,18 +5918,18 @@ function buildMinimalProfile(slug, body) {
5718
5918
  cadence: {}
5719
5919
  };
5720
5920
  }
5721
- var fs3, path3, os3, CACHE_TTL, memoryCache, DISK_CACHE_DIR;
5921
+ var fs3, path4, os3, CACHE_TTL, memoryCache, DISK_CACHE_DIR;
5722
5922
  var init_profile = __esm({
5723
5923
  "src/lib/profile.ts"() {
5724
5924
  "use strict";
5725
5925
  init_api();
5726
5926
  init_config();
5727
5927
  fs3 = __toESM(require("fs"));
5728
- path3 = __toESM(require("path"));
5928
+ path4 = __toESM(require("path"));
5729
5929
  os3 = __toESM(require("os"));
5730
5930
  CACHE_TTL = 5 * 60 * 1e3;
5731
5931
  memoryCache = /* @__PURE__ */ new Map();
5732
- DISK_CACHE_DIR = path3.join(os3.homedir(), ".hyv", "cache", "profiles");
5932
+ DISK_CACHE_DIR = path4.join(os3.homedir(), ".hyv", "cache", "profiles");
5733
5933
  }
5734
5934
  });
5735
5935
 
@@ -5744,7 +5944,7 @@ __export(local_profile_exports, {
5744
5944
  loadProfileFromDiskCache: () => loadProfileFromDiskCache
5745
5945
  });
5746
5946
  function cachePathForSlug(slug) {
5747
- return path4.join(DISK_CACHE_DIR2, `${slug.replace(/[^a-z0-9-]/gi, "_")}.json`);
5947
+ return path5.join(DISK_CACHE_DIR2, `${slug.replace(/[^a-z0-9-]/gi, "_")}.json`);
5748
5948
  }
5749
5949
  function loadProfileFromDiskCache(slug) {
5750
5950
  try {
@@ -5844,17 +6044,17 @@ function hasRichProfile(profile) {
5844
6044
  return false;
5845
6045
  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
6046
  }
5847
- var fs4, path4, os4, DISK_CACHE_DIR2;
6047
+ var fs4, path5, os4, DISK_CACHE_DIR2;
5848
6048
  var init_local_profile = __esm({
5849
6049
  "src/lib/local-profile.ts"() {
5850
6050
  "use strict";
5851
6051
  fs4 = __toESM(require("fs"));
5852
- path4 = __toESM(require("path"));
6052
+ path5 = __toESM(require("path"));
5853
6053
  os4 = __toESM(require("os"));
5854
6054
  init_config();
5855
6055
  init_config();
5856
6056
  init_profile();
5857
- DISK_CACHE_DIR2 = path4.join(os4.homedir(), ".hyv", "cache", "profiles");
6057
+ DISK_CACHE_DIR2 = path5.join(os4.homedir(), ".hyv", "cache", "profiles");
5858
6058
  }
5859
6059
  });
5860
6060
 
@@ -5946,19 +6146,19 @@ var init_profile_parse = __esm({
5946
6146
  function loadSupplementalPatterns() {
5947
6147
  const p = path8.join(CACHE_DIR, "supplemental-rules.json");
5948
6148
  try {
5949
- if (!fs8.existsSync(p))
6149
+ if (!fs7.existsSync(p))
5950
6150
  return [];
5951
- const data = JSON.parse(fs8.readFileSync(p, "utf-8"));
6151
+ const data = JSON.parse(fs7.readFileSync(p, "utf-8"));
5952
6152
  return Array.isArray(data.patterns) ? data.patterns : [];
5953
6153
  } catch {
5954
6154
  return [];
5955
6155
  }
5956
6156
  }
5957
- var fs8, path8, BUNDLED_MANIFEST;
6157
+ var fs7, path8, BUNDLED_MANIFEST;
5958
6158
  var init_rules_loader = __esm({
5959
6159
  "src/lib/rules-loader.ts"() {
5960
6160
  "use strict";
5961
- fs8 = __toESM(require("fs"));
6161
+ fs7 = __toESM(require("fs"));
5962
6162
  path8 = __toESM(require("path"));
5963
6163
  init_config();
5964
6164
  BUNDLED_MANIFEST = path8.resolve(__dirname, "..", "..", "assets", "detection-rules.json");
@@ -7046,26 +7246,26 @@ function runPipeline(text, profile, applyFixes = false) {
7046
7246
  };
7047
7247
  }
7048
7248
  function readText(source) {
7049
- const fs25 = require("fs");
7249
+ const fs24 = require("fs");
7050
7250
  const path23 = require("path");
7051
7251
  if (source === "-") {
7052
7252
  if (process.stdin.isTTY) {
7053
7253
  console.error("No input provided. Pipe content or specify a file.");
7054
7254
  process.exit(1);
7055
7255
  }
7056
- return { text: fs25.readFileSync(0, "utf-8"), path: "stdin" };
7256
+ return { text: fs24.readFileSync(0, "utf-8"), path: "stdin" };
7057
7257
  }
7058
7258
  const resolved = path23.resolve(source);
7059
- if (!fs25.existsSync(resolved)) {
7259
+ if (!fs24.existsSync(resolved)) {
7060
7260
  console.error(`File not found: ${resolved}`);
7061
7261
  process.exit(1);
7062
7262
  }
7063
- const stat = fs25.statSync(resolved);
7263
+ const stat = fs24.statSync(resolved);
7064
7264
  if (stat.isDirectory()) {
7065
7265
  console.error(`${resolved} is a directory, not a file.`);
7066
7266
  process.exit(1);
7067
7267
  }
7068
- return { text: fs25.readFileSync(resolved, "utf-8"), path: resolved };
7268
+ return { text: fs24.readFileSync(resolved, "utf-8"), path: resolved };
7069
7269
  }
7070
7270
  var init_pipeline = __esm({
7071
7271
  "src/lib/pipeline.ts"() {
@@ -8073,7 +8273,7 @@ globstar while`, t, d, e, u, m), this.matchOne(t.slice(d), e.slice(u), s))
8073
8273
  g.minimatch.escape = vi.escape;
8074
8274
  g.minimatch.unescape = Ei.unescape;
8075
8275
  });
8076
- var fs25 = R((Wt) => {
8276
+ var fs24 = R((Wt) => {
8077
8277
  "use strict";
8078
8278
  Object.defineProperty(Wt, "__esModule", { value: true });
8079
8279
  Wt.LRUCache = void 0;
@@ -9042,7 +9242,7 @@ globstar while`, t, d, e, u, m), this.matchOne(t.slice(d), e.slice(u), s))
9042
9242
  };
9043
9243
  Object.defineProperty(_, "__esModule", { value: true });
9044
9244
  _.PathScurry = _.Path = _.PathScurryDarwin = _.PathScurryPosix = _.PathScurryWin32 = _.PathScurryBase = _.PathPosix = _.PathWin32 = _.PathBase = _.ChildrenCache = _.ResolveCache = void 0;
9045
- var Qt = fs25(), Yt = require("node:path"), yr = require("node:url"), pt = require("fs"), Sr = br(require("node:fs")), vr = pt.realpathSync.native, Ht = require("node:fs/promises"), bs = Oe(), mt = { lstatSync: pt.lstatSync, readdir: pt.readdir, readdirSync: pt.readdirSync, readlinkSync: pt.readlinkSync, realpathSync: vr, promises: { lstat: Ht.lstat, readdir: Ht.readdir, readlink: Ht.readlink, realpath: Ht.realpath } }, _s = (n) => !n || n === mt || n === Sr ? mt : { ...mt, ...n, promises: { ...mt.promises, ...n.promises || {} } }, Os = /^\\\\\?\\([a-z]:)\\?$/i, Er = (n) => n.replace(/\//g, "\\").replace(Os, "$1\\"), _r = /[\\\/]/, N = 0, xs = 1, Ts = 2, G = 4, Cs = 6, Rs = 8, Q = 10, As = 12, j = 15, dt = ~j, xe = 16, ys = 32, gt = 64, W = 128, Vt = 256, Xt = 512, Ss = gt | W | Xt, Or = 1023, Te = (n) => n.isFile() ? Rs : n.isDirectory() ? G : n.isSymbolicLink() ? Q : n.isCharacterDevice() ? Ts : n.isBlockDevice() ? Cs : n.isSocket() ? As : n.isFIFO() ? xs : N, vs = new Qt.LRUCache({ max: 2 ** 12 }), wt = (n) => {
9245
+ var Qt = fs24(), Yt = require("node:path"), yr = require("node:url"), pt = require("fs"), Sr = br(require("node:fs")), vr = pt.realpathSync.native, Ht = require("node:fs/promises"), bs = Oe(), mt = { lstatSync: pt.lstatSync, readdir: pt.readdir, readdirSync: pt.readdirSync, readlinkSync: pt.readlinkSync, realpathSync: vr, promises: { lstat: Ht.lstat, readdir: Ht.readdir, readlink: Ht.readlink, realpath: Ht.realpath } }, _s = (n) => !n || n === mt || n === Sr ? mt : { ...mt, ...n, promises: { ...mt.promises, ...n.promises || {} } }, Os = /^\\\\\?\\([a-z]:)\\?$/i, Er = (n) => n.replace(/\//g, "\\").replace(Os, "$1\\"), _r = /[\\\/]/, N = 0, xs = 1, Ts = 2, G = 4, Cs = 6, Rs = 8, Q = 10, As = 12, j = 15, dt = ~j, xe = 16, ys = 32, gt = 64, W = 128, Vt = 256, Xt = 512, Ss = gt | W | Xt, Or = 1023, Te = (n) => n.isFile() ? Rs : n.isDirectory() ? G : n.isSymbolicLink() ? Q : n.isCharacterDevice() ? Ts : n.isBlockDevice() ? Cs : n.isSocket() ? As : n.isFIFO() ? xs : N, vs = new Qt.LRUCache({ max: 2 ** 12 }), wt = (n) => {
9046
9246
  let t = vs.get(n);
9047
9247
  if (t)
9048
9248
  return t;
@@ -10733,79 +10933,11 @@ init_config();
10733
10933
  init_auth();
10734
10934
  init_access();
10735
10935
  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
- }
10936
+ init_version();
10805
10937
 
10806
10938
  // src/lib/queue-sync.ts
10807
10939
  var import_chalk6 = __toESM(require_source());
10808
- var fs6 = __toESM(require("fs"));
10940
+ var fs5 = __toESM(require("fs"));
10809
10941
  var path6 = __toESM(require("path"));
10810
10942
  init_config();
10811
10943
  init_auth();
@@ -10815,34 +10947,34 @@ async function flushQueuedSignals() {
10815
10947
  return { sent: 0, failed: 0 };
10816
10948
  let sent = 0;
10817
10949
  let failed = 0;
10818
- if (!fs6.existsSync(QUEUE_DIR))
10950
+ if (!fs5.existsSync(QUEUE_DIR))
10819
10951
  return { sent: 0, failed: 0 };
10820
- const files = fs6.readdirSync(QUEUE_DIR).filter((f) => f.endsWith(".json"));
10952
+ const files = fs5.readdirSync(QUEUE_DIR).filter((f) => f.endsWith(".json"));
10821
10953
  for (const file of files) {
10822
10954
  const filePath = path6.join(QUEUE_DIR, file);
10823
10955
  try {
10824
- const signal = JSON.parse(fs6.readFileSync(filePath, "utf-8"));
10956
+ const signal = JSON.parse(fs5.readFileSync(filePath, "utf-8"));
10825
10957
  let ok = false;
10826
10958
  if (signal.type === "reinforce") {
10827
- const res = await authenticatedRequest("https://holdyourvoice.com/cli/learning/reinforce", {
10959
+ const res = await authenticatedRequest(cliApiUrl("/cli/learning/reinforce"), {
10828
10960
  method: "POST",
10829
10961
  body: {
10830
10962
  profile_id: signal.profile,
10831
- original_text: signal.report?.session?.original_path,
10832
- accepted_text: signal.report?.session?.accepted_path,
10963
+ original_text: signal.original_text ?? signal.report?.original_text,
10964
+ accepted_text: signal.accepted_text ?? signal.report?.accepted_text,
10833
10965
  signal_report: signal.report
10834
10966
  }
10835
10967
  });
10836
10968
  ok = res.status === 200;
10837
10969
  } else if (signal.type === "add_instruction") {
10838
- const res = await authenticatedRequest("https://holdyourvoice.com/cli/learning/add", {
10970
+ const res = await authenticatedRequest(cliApiUrl("/cli/learning/add"), {
10839
10971
  method: "POST",
10840
10972
  body: { profile_id: signal.profile, instruction: signal.instruction }
10841
10973
  });
10842
10974
  ok = res.status === 200;
10843
10975
  }
10844
10976
  if (ok) {
10845
- fs6.unlinkSync(filePath);
10977
+ fs5.unlinkSync(filePath);
10846
10978
  sent++;
10847
10979
  } else {
10848
10980
  failed++;
@@ -10971,7 +11103,7 @@ async function printEvolutionSummary(slug) {
10971
11103
  return;
10972
11104
  try {
10973
11105
  const res = await authenticatedRequest(
10974
- `https://holdyourvoice.com/cli/profiles/${encodeURIComponent(slug)}/evolution`,
11106
+ cliApiUrl(`/cli/profiles/${encodeURIComponent(slug)}/evolution`),
10975
11107
  { method: "GET" }
10976
11108
  );
10977
11109
  if (res.status !== 200 || !res.data)
@@ -10999,7 +11131,7 @@ init_config();
10999
11131
  init_auth();
11000
11132
  init_access();
11001
11133
  init_profile();
11002
- var fs7 = __toESM(require("fs"));
11134
+ var fs6 = __toESM(require("fs"));
11003
11135
  var path7 = __toESM(require("path"));
11004
11136
  function registerSyncCommand(program3) {
11005
11137
  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 +11139,7 @@ function registerSyncCommand(program3) {
11007
11139
  await requirePaidFeature("profiles");
11008
11140
  console.log(import_chalk8.default.cyan("Syncing profiles and rules..."));
11009
11141
  const response = await authenticatedRequest(
11010
- "https://holdyourvoice.com/cli/sync",
11142
+ cliApiUrl("/cli/sync"),
11011
11143
  { method: "GET" }
11012
11144
  );
11013
11145
  if (response.status !== 200) {
@@ -11023,12 +11155,12 @@ function registerSyncCommand(program3) {
11023
11155
  profileCount++;
11024
11156
  }
11025
11157
  const rulesPath = path7.join(CACHE_DIR, "rules.md");
11026
- fs7.writeFileSync(rulesPath, data.rules, { mode: 384 });
11158
+ fs6.writeFileSync(rulesPath, data.rules, { mode: 384 });
11027
11159
  const promptPath = path7.join(CACHE_DIR, "prompt-template.md");
11028
- fs7.writeFileSync(promptPath, data.prompt_template, { mode: 384 });
11160
+ fs6.writeFileSync(promptPath, data.prompt_template, { mode: 384 });
11029
11161
  if (data.detection_rules) {
11030
11162
  const rulesJson = path7.join(CACHE_DIR, "detection-rules.json");
11031
- fs7.writeFileSync(rulesJson, JSON.stringify(data.detection_rules, null, 2), { mode: 384 });
11163
+ fs6.writeFileSync(rulesJson, JSON.stringify(data.detection_rules, null, 2), { mode: 384 });
11032
11164
  }
11033
11165
  for (const profile of data.profiles) {
11034
11166
  try {
@@ -11044,7 +11176,7 @@ function registerSyncCommand(program3) {
11044
11176
  console.log(import_chalk8.default.yellow(` ! ${queueResult.failed} queued signal(s) could not send`));
11045
11177
  }
11046
11178
  const metaPath = path7.join(CACHE_DIR, "sync-meta.json");
11047
- fs7.writeFileSync(metaPath, JSON.stringify({
11179
+ fs6.writeFileSync(metaPath, JSON.stringify({
11048
11180
  synced_at: data.synced_at,
11049
11181
  profile_count: profileCount,
11050
11182
  plan: data.plan
@@ -11120,7 +11252,7 @@ async function showProfileHistory(slug) {
11120
11252
  return;
11121
11253
  }
11122
11254
  try {
11123
- const res = await authenticatedRequest2(`https://holdyourvoice.com/cli/profiles/${encodeURIComponent(slug)}/evolution`, { method: "GET" });
11255
+ const res = await authenticatedRequest2(cliApiUrl(`/cli/profiles/${encodeURIComponent(slug)}/evolution`), { method: "GET" });
11124
11256
  if (res.status !== 200) {
11125
11257
  console.log(import_chalk8.default.red("Could not load profile history."));
11126
11258
  return;
@@ -11149,12 +11281,12 @@ Profile evolution: ${slug}
11149
11281
 
11150
11282
  // src/commands/rewrite.ts
11151
11283
  var import_chalk9 = __toESM(require_source());
11152
- var fs10 = __toESM(require("fs"));
11284
+ var fs9 = __toESM(require("fs"));
11153
11285
  var path10 = __toESM(require("path"));
11154
11286
  init_pipeline();
11155
11287
 
11156
11288
  // src/lib/prompt.ts
11157
- var fs9 = __toESM(require("fs"));
11289
+ var fs8 = __toESM(require("fs"));
11158
11290
  var path9 = __toESM(require("path"));
11159
11291
  init_config();
11160
11292
  init_signals();
@@ -11162,8 +11294,8 @@ init_profile_parse();
11162
11294
  function loadPromptTemplate() {
11163
11295
  const templatePath = path9.join(CACHE_DIR, "prompt-template.md");
11164
11296
  try {
11165
- if (fs9.existsSync(templatePath)) {
11166
- return fs9.readFileSync(templatePath, "utf-8");
11297
+ if (fs8.existsSync(templatePath)) {
11298
+ return fs8.readFileSync(templatePath, "utf-8");
11167
11299
  }
11168
11300
  } catch {
11169
11301
  }
@@ -11229,10 +11361,10 @@ function loadProfile(name) {
11229
11361
  }
11230
11362
  }
11231
11363
  try {
11232
- if (!fs9.existsSync(PROFILES_DIR)) {
11364
+ if (!fs8.existsSync(PROFILES_DIR)) {
11233
11365
  return null;
11234
11366
  }
11235
- const profiles = fs9.readdirSync(PROFILES_DIR).filter((f) => f.endsWith(".md"));
11367
+ const profiles = fs8.readdirSync(PROFILES_DIR).filter((f) => f.endsWith(".md"));
11236
11368
  if (profiles.length > 0) {
11237
11369
  const defaultProfile = profiles[0].replace(".md", "");
11238
11370
  const content = readCachedProfile(defaultProfile);
@@ -11364,7 +11496,7 @@ Draft: ${draftPath}`));
11364
11496
  }
11365
11497
  if (options.output) {
11366
11498
  const outputPath = path10.resolve(options.output);
11367
- fs10.writeFileSync(outputPath, promptResult.prompt);
11499
+ fs9.writeFileSync(outputPath, promptResult.prompt);
11368
11500
  console.log(import_chalk9.default.green(`
11369
11501
  \u2713 Prompt written to ${outputPath}`));
11370
11502
  } else {
@@ -11384,7 +11516,7 @@ Draft: ${draftPath}`));
11384
11516
 
11385
11517
  // src/commands/learning.ts
11386
11518
  var import_chalk10 = __toESM(require_source());
11387
- var fs11 = __toESM(require("fs"));
11519
+ var fs10 = __toESM(require("fs"));
11388
11520
  var path11 = __toESM(require("path"));
11389
11521
 
11390
11522
  // src/lib/patterns.ts
@@ -11889,16 +12021,16 @@ function registerLearningCommands(program3) {
11889
12021
  }
11890
12022
  origPath = path11.resolve(original);
11891
12023
  editPath = path11.resolve(edited);
11892
- if (!fs11.existsSync(origPath)) {
12024
+ if (!fs10.existsSync(origPath)) {
11893
12025
  console.error(import_chalk10.default.red(`Original file not found: ${origPath}`));
11894
12026
  process.exit(1);
11895
12027
  }
11896
- if (!fs11.existsSync(editPath)) {
12028
+ if (!fs10.existsSync(editPath)) {
11897
12029
  console.error(import_chalk10.default.red(`Edited file not found: ${editPath}`));
11898
12030
  process.exit(1);
11899
12031
  }
11900
- origText = fs11.readFileSync(origPath, "utf-8");
11901
- editText = fs11.readFileSync(editPath, "utf-8");
12032
+ origText = fs10.readFileSync(origPath, "utf-8");
12033
+ editText = fs10.readFileSync(editPath, "utf-8");
11902
12034
  saveLastEditSession({
11903
12035
  original_path: origPath,
11904
12036
  edited_path: editPath,
@@ -11914,6 +12046,8 @@ function registerLearningCommands(program3) {
11914
12046
  queueSignal({
11915
12047
  type: "reinforce",
11916
12048
  profile: options.profile,
12049
+ original_text: origText,
12050
+ accepted_text: editText,
11917
12051
  report,
11918
12052
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
11919
12053
  });
@@ -11927,6 +12061,8 @@ ${err.message}`));
11927
12061
  queueSignal({
11928
12062
  type: "reinforce",
11929
12063
  profile: options.profile,
12064
+ original_text: origText,
12065
+ accepted_text: editText,
11930
12066
  report,
11931
12067
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
11932
12068
  });
@@ -11939,6 +12075,8 @@ ${err.message}`));
11939
12075
  queueSignal({
11940
12076
  type: "reinforce",
11941
12077
  profile: options.profile,
12078
+ original_text: origText,
12079
+ accepted_text: editText,
11942
12080
  report,
11943
12081
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
11944
12082
  });
@@ -11946,7 +12084,7 @@ ${err.message}`));
11946
12084
  }
11947
12085
  console.log(import_chalk10.default.cyan("\nSending to server..."));
11948
12086
  const response = await authenticatedRequest(
11949
- "https://holdyourvoice.com/cli/learning/reinforce",
12087
+ cliApiUrl("/cli/learning/reinforce"),
11950
12088
  {
11951
12089
  method: "POST",
11952
12090
  body: {
@@ -11968,6 +12106,8 @@ ${err.message}`));
11968
12106
  queueSignal({
11969
12107
  type: "reinforce",
11970
12108
  profile: options.profile,
12109
+ original_text: origText,
12110
+ accepted_text: editText,
11971
12111
  report,
11972
12112
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
11973
12113
  });
@@ -11994,7 +12134,7 @@ ${err.message}`));
11994
12134
  }
11995
12135
  console.log(import_chalk10.default.cyan("Adding instruction..."));
11996
12136
  const response = await authenticatedRequest(
11997
- "https://holdyourvoice.com/cli/learning/add",
12137
+ cliApiUrl("/cli/learning/add"),
11998
12138
  {
11999
12139
  method: "POST",
12000
12140
  body: {
@@ -12057,7 +12197,7 @@ function printProfileImpact(impact, data) {
12057
12197
 
12058
12198
  // src/commands/onboarding.ts
12059
12199
  var import_chalk11 = __toESM(require_source());
12060
- var fs12 = __toESM(require("fs"));
12200
+ var fs11 = __toESM(require("fs"));
12061
12201
  var path12 = __toESM(require("path"));
12062
12202
  init_config();
12063
12203
  init_auth();
@@ -12133,6 +12273,7 @@ function extractStats(samples) {
12133
12273
  function registerOnboardingCommands(program3) {
12134
12274
  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
12275
  try {
12276
+ assertSafeProfileName(name);
12136
12277
  const token = getToken();
12137
12278
  if (options.fromSamples) {
12138
12279
  await createFromSamples(name, options.fromSamples, token);
@@ -12147,6 +12288,7 @@ Error: ${error.message}`));
12147
12288
  process.exit(1);
12148
12289
  }
12149
12290
  });
12291
+ registerImportCommand(program3);
12150
12292
  }
12151
12293
  async function flashcardOnboarding(name, token) {
12152
12294
  const { printWelcome: printWelcome2 } = await Promise.resolve().then(() => (init_welcome(), welcome_exports));
@@ -12170,7 +12312,7 @@ Industry: ${industry}
12170
12312
  if (token) {
12171
12313
  console.log(import_chalk11.default.cyan("\nGenerating profile on server..."));
12172
12314
  const response = await authenticatedRequest(
12173
- "https://holdyourvoice.com/cli/profiles/new",
12315
+ cliApiUrl("/cli/profiles/new"),
12174
12316
  {
12175
12317
  method: "POST",
12176
12318
  body: {
@@ -12207,20 +12349,20 @@ Industry: ${industry}
12207
12349
  }
12208
12350
  async function createFromSamples(name, sampleDir, token) {
12209
12351
  const dirPath = path12.resolve(sampleDir);
12210
- if (!fs12.existsSync(dirPath)) {
12352
+ if (!fs11.existsSync(dirPath)) {
12211
12353
  throw new Error(`Directory not found: ${dirPath}`);
12212
12354
  }
12213
12355
  console.log(import_chalk11.default.bold(`
12214
12356
  Creating voice profile: ${name}`));
12215
12357
  console.log(import_chalk11.default.dim(`Reading samples from: ${dirPath}
12216
12358
  `));
12217
- const files = fs12.readdirSync(dirPath).filter((f) => f.endsWith(".md") || f.endsWith(".txt")).map((f) => path12.join(dirPath, f));
12359
+ const files = fs11.readdirSync(dirPath).filter((f) => f.endsWith(".md") || f.endsWith(".txt")).map((f) => path12.join(dirPath, f));
12218
12360
  if (files.length === 0) {
12219
12361
  throw new Error("No .md or .txt files found in directory");
12220
12362
  }
12221
12363
  const samples = [];
12222
12364
  for (const file of files) {
12223
- const text = fs12.readFileSync(file, "utf-8");
12365
+ const text = fs11.readFileSync(file, "utf-8");
12224
12366
  if (text.trim().length > 0) {
12225
12367
  samples.push({ path: file, text });
12226
12368
  console.log(import_chalk11.default.dim(` \u2022 ${path12.basename(file)} (${words(text).length} words)`));
@@ -12240,7 +12382,7 @@ Stats extracted:`));
12240
12382
  if (token) {
12241
12383
  console.log(import_chalk11.default.cyan("\nGenerating profile on server..."));
12242
12384
  const response = await authenticatedRequest(
12243
- "https://holdyourvoice.com/cli/profiles/new",
12385
+ cliApiUrl("/cli/profiles/new"),
12244
12386
  {
12245
12387
  method: "POST",
12246
12388
  body: {
@@ -12290,8 +12432,28 @@ hyv import ${name} <file.md>
12290
12432
  Or paste the profile content and I'll save it.`;
12291
12433
  console.log(prompt);
12292
12434
  }
12435
+ function registerImportCommand(program3) {
12436
+ program3.command("import").description("Import a voice profile from file").argument("<name>", "Profile name").argument("<file>", "Profile markdown file").action(async (name, file) => {
12437
+ try {
12438
+ assertSafeProfileName(name);
12439
+ const filePath = path12.resolve(file);
12440
+ if (!fs11.existsSync(filePath)) {
12441
+ console.error(import_chalk11.default.red(`File not found: ${filePath}`));
12442
+ process.exit(1);
12443
+ }
12444
+ const content = fs11.readFileSync(filePath, "utf-8");
12445
+ ensureHyvDir();
12446
+ writeCachedProfile(name, content);
12447
+ console.log(import_chalk11.default.green(`
12448
+ \u2713 Profile imported: ${name}`));
12449
+ } catch (error) {
12450
+ console.error(import_chalk11.default.red(`Error: ${error.message}`));
12451
+ process.exit(1);
12452
+ }
12453
+ });
12454
+ }
12293
12455
  function askQuestion(question) {
12294
- return new Promise((resolve12) => {
12456
+ return new Promise((resolve14) => {
12295
12457
  const readline = require("readline");
12296
12458
  const rl = readline.createInterface({
12297
12459
  input: process.stdin,
@@ -12300,7 +12462,7 @@ function askQuestion(question) {
12300
12462
  rl.question(import_chalk11.default.cyan(` ${question}
12301
12463
  > `), (answer) => {
12302
12464
  rl.close();
12303
- resolve12(answer.trim());
12465
+ resolve14(answer.trim());
12304
12466
  });
12305
12467
  });
12306
12468
  }
@@ -12404,7 +12566,7 @@ Error: ${error.message}`));
12404
12566
  async function showPlan() {
12405
12567
  console.log(import_chalk12.default.bold("\nSubscription Plan\n"));
12406
12568
  const response = await authenticatedRequest(
12407
- "https://holdyourvoice.com/cli/heartbeat",
12569
+ cliApiUrl("/cli/heartbeat"),
12408
12570
  { method: "GET" }
12409
12571
  );
12410
12572
  if (response.status !== 200) {
@@ -12448,7 +12610,7 @@ async function showPlan() {
12448
12610
  async function upgradePlan() {
12449
12611
  console.log(import_chalk12.default.cyan("\nOpening checkout...\n"));
12450
12612
  const response = await authenticatedRequest(
12451
- "https://holdyourvoice.com/cli/subscribe",
12613
+ cliApiUrl("/cli/subscribe"),
12452
12614
  {
12453
12615
  method: "POST",
12454
12616
  body: { plan: "individual" }
@@ -12459,7 +12621,7 @@ async function upgradePlan() {
12459
12621
  const checkoutUrl = data.checkout_url;
12460
12622
  if (checkoutUrl) {
12461
12623
  console.log(import_chalk12.default.dim("Opening browser..."));
12462
- await (0, import_open2.default)(checkoutUrl);
12624
+ await (0, import_open2.default)(assertSafeOpenUrl(checkoutUrl));
12463
12625
  console.log(import_chalk12.default.green("\n\u2713 Checkout opened in browser"));
12464
12626
  console.log(import_chalk12.default.dim("Complete the checkout to activate your plan."));
12465
12627
  } else {
@@ -12473,7 +12635,7 @@ async function upgradePlan() {
12473
12635
  async function openBillingPortal() {
12474
12636
  console.log(import_chalk12.default.cyan("\nOpening billing portal...\n"));
12475
12637
  const response = await authenticatedRequest(
12476
- "https://holdyourvoice.com/cli/subscribe/manage",
12638
+ cliApiUrl("/cli/subscribe/manage"),
12477
12639
  { method: "POST" }
12478
12640
  );
12479
12641
  if (response.status === 200) {
@@ -12481,7 +12643,7 @@ async function openBillingPortal() {
12481
12643
  const portalUrl = data.portal_url;
12482
12644
  if (portalUrl) {
12483
12645
  console.log(import_chalk12.default.dim("Opening browser..."));
12484
- await (0, import_open2.default)(portalUrl);
12646
+ await (0, import_open2.default)(assertSafeOpenUrl(portalUrl));
12485
12647
  console.log(import_chalk12.default.green("\n\u2713 Billing portal opened"));
12486
12648
  } else {
12487
12649
  console.log(import_chalk12.default.yellow("No portal URL received."));
@@ -12663,7 +12825,7 @@ async function runHybridAnalysis(text, profile, opts = {}) {
12663
12825
 
12664
12826
  // src/commands/history.ts
12665
12827
  var import_chalk14 = __toESM(require_source());
12666
- var fs13 = __toESM(require("fs"));
12828
+ var fs12 = __toESM(require("fs"));
12667
12829
  var path13 = __toESM(require("path"));
12668
12830
  init_config();
12669
12831
  var HISTORY_DIR = path13.join(HYV_DIR, "history");
@@ -12671,17 +12833,15 @@ var HISTORY_FILE = path13.join(HISTORY_DIR, "scans.jsonl");
12671
12833
  function logScan(entry) {
12672
12834
  try {
12673
12835
  ensureHyvDir();
12674
- if (!fs13.existsSync(HISTORY_DIR))
12675
- fs13.mkdirSync(HISTORY_DIR, { recursive: true });
12676
- fs13.appendFileSync(HISTORY_FILE, JSON.stringify(entry) + "\n");
12836
+ appendSecureLine(HISTORY_FILE, JSON.stringify(entry) + "\n", HISTORY_DIR);
12677
12837
  } catch {
12678
12838
  }
12679
12839
  }
12680
12840
  function readHistory() {
12681
12841
  try {
12682
- if (!fs13.existsSync(HISTORY_FILE))
12842
+ if (!fs12.existsSync(HISTORY_FILE))
12683
12843
  return [];
12684
- const lines = fs13.readFileSync(HISTORY_FILE, "utf-8").trim().split("\n").filter(Boolean);
12844
+ const lines = fs12.readFileSync(HISTORY_FILE, "utf-8").trim().split("\n").filter(Boolean);
12685
12845
  return lines.map((l) => JSON.parse(l));
12686
12846
  } catch {
12687
12847
  return [];
@@ -12713,8 +12873,8 @@ function registerHistoryCommand(program3) {
12713
12873
  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
12874
  try {
12715
12875
  if (options.clear) {
12716
- if (fs13.existsSync(HISTORY_FILE)) {
12717
- fs13.unlinkSync(HISTORY_FILE);
12876
+ if (fs12.existsSync(HISTORY_FILE)) {
12877
+ fs12.unlinkSync(HISTORY_FILE);
12718
12878
  }
12719
12879
  console.log(import_chalk14.default.green("\n\u2713 History cleared"));
12720
12880
  return;
@@ -12869,7 +13029,7 @@ hyv scan ${filePath}`));
12869
13029
  hasProfile: !!profile
12870
13030
  });
12871
13031
  if (options.failOnHit && signals.length > 0) {
12872
- process.exit(1);
13032
+ process.exit(2);
12873
13033
  }
12874
13034
  } catch (error) {
12875
13035
  console.error(import_chalk15.default.red(`Error: ${error.message}`));
@@ -12880,15 +13040,33 @@ hyv scan ${filePath}`));
12880
13040
 
12881
13041
  // src/commands/doctor.ts
12882
13042
  var import_chalk16 = __toESM(require_source());
12883
- var fs14 = __toESM(require("fs"));
13043
+ var fs13 = __toESM(require("fs"));
12884
13044
  var path14 = __toESM(require("path"));
12885
13045
  var os5 = __toESM(require("os"));
12886
13046
  init_config();
12887
13047
  init_auth();
12888
13048
  init_access();
12889
13049
  init_local_profile();
13050
+ init_version();
12890
13051
  var HOME = os5.homedir();
12891
13052
  var IS_WIN = process.platform === "win32";
13053
+ function claudeDesktopDir() {
13054
+ if (IS_WIN)
13055
+ return path14.join(HOME, "AppData", "Roaming", "Claude");
13056
+ if (process.platform === "linux")
13057
+ return path14.join(HOME, ".config", "Claude");
13058
+ return path14.join(HOME, "Library", "Application Support", "Claude");
13059
+ }
13060
+ function readMcpHyv(configFile) {
13061
+ try {
13062
+ if (!fs13.existsSync(configFile))
13063
+ return false;
13064
+ const cfg = JSON.parse(fs13.readFileSync(configFile, "utf-8"));
13065
+ return Boolean(cfg.mcpServers?.hyv);
13066
+ } catch {
13067
+ return false;
13068
+ }
13069
+ }
12892
13070
  function registerDoctorCommand(program3) {
12893
13071
  program3.command("doctor").description("Diagnose CLI health: engine, cache, auth, agents").option("--fix-agents", "Re-run agent config copy (idempotent)").action(async (opts) => {
12894
13072
  console.log(import_chalk16.default.bold("\nhold your voice \u2014 doctor\n"));
@@ -12904,28 +13082,28 @@ function registerDoctorCommand(program3) {
12904
13082
  console.log(import_chalk16.default.dim("tip: run hyv welcome for free capabilities\n"));
12905
13083
  console.log(import_chalk16.default.dim("checking cli installation..."));
12906
13084
  const cliPath = process.argv[1];
12907
- if (cliPath && fs14.existsSync(cliPath)) {
13085
+ if (cliPath && fs13.existsSync(cliPath)) {
12908
13086
  console.log(import_chalk16.default.green(" \u2713 cli installed"));
12909
13087
  } else {
12910
13088
  console.log(import_chalk16.default.red(" \u2717 cli not found"));
12911
13089
  issues++;
12912
13090
  }
12913
13091
  console.log(import_chalk16.default.dim("checking .hyv directory..."));
12914
- if (fs14.existsSync(HYV_DIR)) {
13092
+ if (fs13.existsSync(HYV_DIR)) {
12915
13093
  console.log(import_chalk16.default.green(" \u2713 .hyv directory exists"));
12916
13094
  } else {
12917
13095
  console.log(import_chalk16.default.yellow(" ! .hyv directory missing \u2014 creating..."));
12918
- fs14.mkdirSync(HYV_DIR, { recursive: true });
13096
+ fs13.mkdirSync(HYV_DIR, { recursive: true, mode: 448 });
12919
13097
  fixed++;
12920
13098
  }
12921
13099
  console.log(import_chalk16.default.dim("checking cache..."));
12922
13100
  const diskProfiles = listDiskCachedProfiles();
12923
13101
  const syncMeta = path14.join(CACHE_DIR, "sync-meta.json");
12924
- if (diskProfiles.length > 0 || fs14.existsSync(syncMeta)) {
13102
+ if (diskProfiles.length > 0 || fs13.existsSync(syncMeta)) {
12925
13103
  console.log(import_chalk16.default.green(` \u2713 profile cache (${diskProfiles.length} full profile(s))`));
12926
- if (fs14.existsSync(syncMeta)) {
13104
+ if (fs13.existsSync(syncMeta)) {
12927
13105
  try {
12928
- const meta = JSON.parse(fs14.readFileSync(syncMeta, "utf-8"));
13106
+ const meta = JSON.parse(fs13.readFileSync(syncMeta, "utf-8"));
12929
13107
  console.log(import_chalk16.default.dim(` last sync: ${meta.synced_at || "unknown"}`));
12930
13108
  } catch {
12931
13109
  }
@@ -12958,8 +13136,8 @@ function registerDoctorCommand(program3) {
12958
13136
  }
12959
13137
  console.log(import_chalk16.default.dim("checking voice profile..."));
12960
13138
  const voiceMd = path14.join(HYV_DIR, "voice.md");
12961
- const hasVoiceMd = fs14.existsSync(voiceMd) && fs14.readFileSync(voiceMd, "utf-8").trim().length > 50;
12962
- const profileFiles = fs14.existsSync(PROFILES_DIR) ? fs14.readdirSync(PROFILES_DIR).filter((f) => f.endsWith(".md") && f !== "voice.md") : [];
13139
+ const hasVoiceMd = fs13.existsSync(voiceMd) && fs13.readFileSync(voiceMd, "utf-8").trim().length > 50;
13140
+ const profileFiles = fs13.existsSync(PROFILES_DIR) ? fs13.readdirSync(PROFILES_DIR).filter((f) => f.endsWith(".md") && f !== "voice.md") : [];
12963
13141
  if (hasVoiceMd || profileFiles.length > 0 || diskProfiles.length > 0) {
12964
13142
  if (hasVoiceMd)
12965
13143
  console.log(import_chalk16.default.green(" \u2713 voice.md exists"));
@@ -12972,15 +13150,17 @@ function registerDoctorCommand(program3) {
12972
13150
  console.log(import_chalk16.default.dim(" run: hyv new <name> or hyv init"));
12973
13151
  }
12974
13152
  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") }
13153
+ const agentChecks = [
13154
+ { name: "claude desktop mcp", ok: readMcpHyv(path14.join(claudeDesktopDir(), "claude_desktop_config.json")) },
13155
+ { name: "cursor mcp", ok: readMcpHyv(path14.join(HOME, ".cursor", "mcp.json")) },
13156
+ { name: "cursor rule", ok: fs13.existsSync(path14.join(HOME, ".cursor", "rules", "hyv.mdc")) },
13157
+ { name: "claude code command", ok: fs13.existsSync(path14.join(HOME, ".claude", "commands", "hyv.md")) },
13158
+ { name: "claude code skill", ok: fs13.existsSync(path14.join(HOME, ".claude", "skills", "hold-your-voice", "SKILL.md")) },
13159
+ { name: "codex agents", ok: fs13.existsSync(path14.join(HOME, ".codex", "AGENTS.md")) && fs13.readFileSync(path14.join(HOME, ".codex", "AGENTS.md"), "utf-8").includes("hyv") },
13160
+ { name: "command code skill", ok: fs13.existsSync(path14.join(HOME, ".commandcode", "skills", "hyv", "SKILL.md")) }
12981
13161
  ];
12982
- for (const agent of agents) {
12983
- if (fs14.existsSync(agent.file)) {
13162
+ for (const agent of agentChecks) {
13163
+ if (agent.ok) {
12984
13164
  console.log(import_chalk16.default.green(` \u2713 ${agent.name}`));
12985
13165
  } else {
12986
13166
  console.log(import_chalk16.default.dim(` - ${agent.name} not configured`));
@@ -12988,35 +13168,29 @@ function registerDoctorCommand(program3) {
12988
13168
  }
12989
13169
  if (opts.fixAgents) {
12990
13170
  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"));
13171
+ const pkgDir = path14.resolve(__dirname, "..");
13172
+ const { setupAgents } = require(path14.join(pkgDir, "scripts", "postinstall-lib.js"));
13173
+ const result = setupAgents({ pkgDir, quiet: true });
13174
+ console.log(import_chalk16.default.green(` \u2713 re-ran agent setup (${result.configured.join(", ") || "no changes"})`));
13175
+ if (result.warnings.length) {
13176
+ console.log(import_chalk16.default.yellow(` notes: ${result.warnings.join("; ")}`));
13177
+ }
13178
+ fixed++;
13179
+ } catch (err) {
13180
+ console.log(import_chalk16.default.yellow(` ! could not re-run agent setup: ${err.message}`));
12999
13181
  }
13000
13182
  }
13001
13183
  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
- }
13184
+ const claudeMcp = readMcpHyv(path14.join(claudeDesktopDir(), "claude_desktop_config.json"));
13185
+ const cursorMcp = readMcpHyv(path14.join(HOME, ".cursor", "mcp.json"));
13186
+ if (claudeMcp || cursorMcp) {
13187
+ if (claudeMcp)
13188
+ console.log(import_chalk16.default.green(" \u2713 mcp configured for claude desktop"));
13189
+ if (cursorMcp)
13190
+ console.log(import_chalk16.default.green(" \u2713 mcp configured for cursor"));
13018
13191
  } else {
13019
- console.log(import_chalk16.default.dim(" - claude desktop not found"));
13192
+ console.log(import_chalk16.default.yellow(" ! mcp not configured \u2014 run: hyv doctor --fix-agents or hyv mcp --setup"));
13193
+ issues++;
13020
13194
  }
13021
13195
  console.log("");
13022
13196
  if (issues === 0 && fixed === 0) {
@@ -13065,7 +13239,7 @@ function registerRenameCommand(program3) {
13065
13239
  return;
13066
13240
  }
13067
13241
  const response = await authenticatedRequest(
13068
- "https://holdyourvoice.com/cli/profiles/rename",
13242
+ cliApiUrl("/cli/profiles/rename"),
13069
13243
  {
13070
13244
  method: "POST",
13071
13245
  body: {
@@ -13095,7 +13269,7 @@ error: ${error.message}
13095
13269
 
13096
13270
  // src/commands/fix.ts
13097
13271
  var import_chalk18 = __toESM(require_source());
13098
- var fs15 = __toESM(require("fs"));
13272
+ var fs14 = __toESM(require("fs"));
13099
13273
  var path15 = __toESM(require("path"));
13100
13274
  init_pipeline();
13101
13275
  init_local_profile();
@@ -13142,8 +13316,8 @@ hyv fix ${filePath}
13142
13316
  if (!options.dryRun) {
13143
13317
  if (options.inPlace && filePath !== "stdin") {
13144
13318
  const backupPath = filePath + ".bak";
13145
- fs15.copyFileSync(filePath, backupPath);
13146
- fs15.writeFileSync(filePath, result.fixed);
13319
+ fs14.copyFileSync(filePath, backupPath);
13320
+ fs14.writeFileSync(filePath, result.fixed);
13147
13321
  console.log(import_chalk18.default.dim(` Written to ${filePath} (backup: ${path15.basename(backupPath)})`));
13148
13322
  saveLastEditSession({
13149
13323
  original_path: filePath,
@@ -13185,19 +13359,19 @@ function registerCheckCommand(program3) {
13185
13359
  const profile = await loadProfileForCommand(options.profile);
13186
13360
  let inputText = text;
13187
13361
  if (text === "-") {
13188
- const fs26 = require("fs");
13362
+ const fs25 = require("fs");
13189
13363
  if (process.stdin.isTTY) {
13190
13364
  console.error(import_chalk19.default.red("No input provided. Pipe content or pass text as argument."));
13191
13365
  process.exit(1);
13192
13366
  }
13193
- inputText = fs26.readFileSync(0, "utf-8");
13367
+ inputText = fs25.readFileSync(0, "utf-8");
13194
13368
  }
13195
13369
  if (!inputText.trim()) {
13196
13370
  console.error(import_chalk19.default.red("No text provided."));
13197
13371
  process.exit(1);
13198
13372
  }
13199
- const fs25 = require("fs");
13200
- if (text !== "-" && fs25.existsSync(text)) {
13373
+ const fs24 = require("fs");
13374
+ if (text !== "-" && fs24.existsSync(text)) {
13201
13375
  console.log(import_chalk19.default.yellow(`"${text}" looks like a file. Did you mean: hyv scan ${text}`));
13202
13376
  process.exit(1);
13203
13377
  }
@@ -13324,11 +13498,11 @@ function registerDiffCommand(program3) {
13324
13498
  ${result.changes.length} auto-fix${result.changes.length === 1 ? "" : "es"} available`));
13325
13499
  console.log(import_chalk21.default.dim(` run: hyv fix ${file} -i to apply`));
13326
13500
  if (options.apply && filePath !== "stdin") {
13327
- const fs25 = require("fs");
13501
+ const fs24 = require("fs");
13328
13502
  const path23 = require("path");
13329
13503
  const backupPath = filePath + ".bak";
13330
- fs25.copyFileSync(filePath, backupPath);
13331
- fs25.writeFileSync(filePath, result.fixed);
13504
+ fs24.copyFileSync(filePath, backupPath);
13505
+ fs24.writeFileSync(filePath, result.fixed);
13332
13506
  console.log(import_chalk21.default.green(` \u2713 Applied. Backup: ${path23.basename(backupPath)}`));
13333
13507
  }
13334
13508
  } catch (error) {
@@ -13504,12 +13678,12 @@ function registerRulesCommand(program3) {
13504
13678
 
13505
13679
  // src/commands/batch.ts
13506
13680
  var import_chalk23 = __toESM(require_source());
13507
- var fs16 = __toESM(require("fs"));
13681
+ var fs15 = __toESM(require("fs"));
13508
13682
  var path16 = __toESM(require("path"));
13509
13683
  init_pipeline();
13510
13684
  init_local_profile();
13511
13685
  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) => {
13686
+ 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 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) => {
13513
13687
  try {
13514
13688
  const profile = await loadProfileForCommand(options.profile);
13515
13689
  const glob = require_index_min();
@@ -13522,6 +13696,11 @@ function registerBatchCommand(program3) {
13522
13696
  No files matching: ${pattern}`));
13523
13697
  return;
13524
13698
  }
13699
+ if (options.fix && options.inPlace && !options.yes) {
13700
+ console.error(import_chalk23.default.red("\nRefusing to write fixes in-place without confirmation."));
13701
+ console.error(import_chalk23.default.dim(" Re-run with --yes to create .bak backups and apply fixes.\n"));
13702
+ process.exit(1);
13703
+ }
13525
13704
  if (options.format === "text") {
13526
13705
  console.log(import_chalk23.default.dim(`
13527
13706
  scanning ${files.length} file${files.length === 1 ? "" : "s"}...
@@ -13530,9 +13709,9 @@ No files matching: ${pattern}`));
13530
13709
  const results = [];
13531
13710
  for (const file of files) {
13532
13711
  const absPath = path16.resolve(file);
13533
- if (!fs16.existsSync(absPath))
13712
+ if (!fs15.existsSync(absPath))
13534
13713
  continue;
13535
- const text = fs16.readFileSync(absPath, "utf-8");
13714
+ const text = fs15.readFileSync(absPath, "utf-8");
13536
13715
  const result = runPipeline(text, profile, options.fix || false);
13537
13716
  results.push({
13538
13717
  file,
@@ -13544,8 +13723,8 @@ No files matching: ${pattern}`));
13544
13723
  });
13545
13724
  if (options.fix && options.inPlace && result.changes.length > 0) {
13546
13725
  const backupPath = absPath + ".bak";
13547
- fs16.copyFileSync(absPath, backupPath);
13548
- fs16.writeFileSync(absPath, result.fixed);
13726
+ fs15.copyFileSync(absPath, backupPath);
13727
+ fs15.writeFileSync(absPath, result.fixed);
13549
13728
  }
13550
13729
  }
13551
13730
  if (options.sort === "score") {
@@ -13585,10 +13764,11 @@ No files matching: ${pattern}`));
13585
13764
  }
13586
13765
  }
13587
13766
  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)
13767
+ const failOnHit = results.some((r) => options.failOnHit && r.issues > 0);
13768
+ const belowThreshold = results.some((r) => threshold > 0 && r.score < threshold);
13769
+ if (failOnHit)
13770
+ process.exit(2);
13771
+ if (belowThreshold)
13592
13772
  process.exit(1);
13593
13773
  } catch (error) {
13594
13774
  console.error(import_chalk23.default.red(`Error: ${error.message}`));
@@ -13599,19 +13779,24 @@ No files matching: ${pattern}`));
13599
13779
 
13600
13780
  // src/commands/watch.ts
13601
13781
  var import_chalk24 = __toESM(require_source());
13602
- var fs17 = __toESM(require("fs"));
13782
+ var fs16 = __toESM(require("fs"));
13603
13783
  var path17 = __toESM(require("path"));
13604
13784
  init_pipeline();
13605
13785
  init_local_profile();
13606
13786
  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) => {
13787
+ 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
13788
  try {
13609
13789
  const profile = await loadProfileForCommand(options.profile);
13610
13790
  const absPath = path17.resolve(file);
13611
- if (!fs17.existsSync(absPath)) {
13791
+ if (!fs16.existsSync(absPath)) {
13612
13792
  console.error(import_chalk24.default.red(`File not found: ${absPath}`));
13613
13793
  process.exit(1);
13614
13794
  }
13795
+ if (options.command === "fix" && !options.yes) {
13796
+ console.error(import_chalk24.default.red("\nRefusing to auto-fix on save without confirmation."));
13797
+ console.error(import_chalk24.default.dim(" Re-run with --yes to create .bak backups before each fix.\n"));
13798
+ process.exit(1);
13799
+ }
13615
13800
  console.log(import_chalk24.default.dim(`
13616
13801
  watching ${absPath}`));
13617
13802
  console.log(import_chalk24.default.dim(`command: ${options.command} debounce: ${options.debounce}ms`));
@@ -13621,7 +13806,7 @@ watching ${absPath}`));
13621
13806
  let scanCount = 0;
13622
13807
  let lastScore = 0;
13623
13808
  const runScan = () => {
13624
- const text = fs17.readFileSync(absPath, "utf-8");
13809
+ const text = fs16.readFileSync(absPath, "utf-8");
13625
13810
  const result = runPipeline(text, profile, options.command === "fix");
13626
13811
  scanCount++;
13627
13812
  const now = (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false });
@@ -13640,7 +13825,9 @@ watching ${absPath}`));
13640
13825
  for (const change of result.changes) {
13641
13826
  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));
13642
13827
  }
13643
- fs17.writeFileSync(absPath, result.fixed);
13828
+ const backupPath = absPath + ".bak";
13829
+ fs16.copyFileSync(absPath, backupPath);
13830
+ fs16.writeFileSync(absPath, result.fixed);
13644
13831
  console.log(import_chalk24.default.green(` \u2713 ${result.changes.length} auto-fixes applied`));
13645
13832
  } else {
13646
13833
  console.log(import_chalk24.default.green(" \u2713 no auto-fixable issues"));
@@ -13662,7 +13849,7 @@ watching ${absPath}`));
13662
13849
  console.log("");
13663
13850
  };
13664
13851
  runScan();
13665
- fs17.watch(absPath, (eventType) => {
13852
+ fs16.watch(absPath, (eventType) => {
13666
13853
  if (eventType !== "change")
13667
13854
  return;
13668
13855
  if (debounceTimer)
@@ -13685,7 +13872,7 @@ watching ${absPath}`));
13685
13872
 
13686
13873
  // src/commands/demo.ts
13687
13874
  var import_chalk25 = __toESM(require_source());
13688
- var fs18 = __toESM(require("fs"));
13875
+ var fs17 = __toESM(require("fs"));
13689
13876
  var SAMPLES = {
13690
13877
  linkedin: {
13691
13878
  text: `In today's fast-paced digital landscape, it's important to note that leveraging the right tools is not just nice to have, but essential for success. Let's delve into the holistic approach that will transform your workflow.
@@ -13780,7 +13967,7 @@ function registerDemoCommand(program3) {
13780
13967
  }
13781
13968
  if (options.output) {
13782
13969
  const outputPath = require("path").resolve(options.output);
13783
- fs18.writeFileSync(outputPath, sample.text);
13970
+ fs17.writeFileSync(outputPath, sample.text);
13784
13971
  console.log(import_chalk25.default.green(`
13785
13972
  \u2713 Sample written to ${outputPath}`));
13786
13973
  if (sample.patterns.length > 0) {
@@ -13846,35 +14033,35 @@ function registerOpenCommand(program3) {
13846
14033
  init_welcome();
13847
14034
 
13848
14035
  // src/lib/onboarding.ts
13849
- var fs19 = __toESM(require("fs"));
14036
+ var fs18 = __toESM(require("fs"));
13850
14037
  var path18 = __toESM(require("path"));
13851
14038
  var os6 = __toESM(require("os"));
13852
14039
  var hyvDir = path18.join(os6.homedir(), ".hyv");
13853
14040
  var onboardingFile = path18.join(hyvDir, "onboarding-complete.json");
13854
14041
  function hasCompletedOnboarding() {
13855
14042
  try {
13856
- return fs19.existsSync(onboardingFile);
14043
+ return fs18.existsSync(onboardingFile);
13857
14044
  } catch {
13858
14045
  return false;
13859
14046
  }
13860
14047
  }
13861
14048
  function markOnboardingComplete(version) {
13862
- if (!fs19.existsSync(hyvDir))
13863
- fs19.mkdirSync(hyvDir, { recursive: true });
13864
- fs19.writeFileSync(
14049
+ if (!fs18.existsSync(hyvDir))
14050
+ fs18.mkdirSync(hyvDir, { recursive: true });
14051
+ fs18.writeFileSync(
13865
14052
  onboardingFile,
13866
14053
  JSON.stringify({ version, completed_at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)
13867
14054
  );
13868
14055
  }
13869
14056
 
13870
14057
  // src/commands/welcome.ts
13871
- var fs20 = __toESM(require("fs"));
14058
+ var fs19 = __toESM(require("fs"));
13872
14059
  var path19 = __toESM(require("path"));
13873
14060
  function registerWelcomeCommand(program3) {
13874
14061
  const pkgVersion3 = (() => {
13875
14062
  try {
13876
- const pkgPath3 = path19.resolve(__dirname, "..", "..", "package.json");
13877
- return JSON.parse(fs20.readFileSync(pkgPath3, "utf-8")).version;
14063
+ const pkgPath3 = path19.resolve(__dirname, "..", "package.json");
14064
+ return JSON.parse(fs19.readFileSync(pkgPath3, "utf-8")).version;
13878
14065
  } catch {
13879
14066
  return "0.0.0";
13880
14067
  }
@@ -13991,6 +14178,7 @@ ${t.title}
13991
14178
  // src/commands/upgrade.ts
13992
14179
  var import_chalk28 = __toESM(require_source());
13993
14180
  var import_child_process2 = require("child_process");
14181
+ init_version();
13994
14182
  function registerUpgradeCommand(program3) {
13995
14183
  program3.command("upgrade").description("Upgrade to the latest @holdyourvoice/hyv").option("--check", "Only check if an update is available").action(async (opts) => {
13996
14184
  const current = getCliVersion();
@@ -14031,7 +14219,7 @@ Current: ${getEngineLabel()}
14031
14219
  }
14032
14220
 
14033
14221
  // src/mcp.ts
14034
- var fs21 = __toESM(require("fs"));
14222
+ var fs20 = __toESM(require("fs"));
14035
14223
  var path20 = __toESM(require("path"));
14036
14224
  var os7 = __toESM(require("os"));
14037
14225
  init_classifier();
@@ -14046,16 +14234,42 @@ init_telemetry();
14046
14234
  init_access();
14047
14235
  var VOICE_MD = path20.join(os7.homedir(), ".hyv", "voice.md");
14048
14236
  var MAX_RESPONSE_CHARS = 12e4;
14237
+ var MAX_SCAN_FILE_BYTES = 2 * 1024 * 1024;
14238
+ function readScanFile(filePath) {
14239
+ const cwdReal = fs20.realpathSync(process.cwd());
14240
+ const resolved = fs20.realpathSync(path20.resolve(filePath));
14241
+ if (!resolved.startsWith(cwdReal + path20.sep) && resolved !== cwdReal) {
14242
+ throw new Error(`file must be in the current working directory (${cwdReal})`);
14243
+ }
14244
+ const stat = fs20.statSync(resolved);
14245
+ if (stat.size > MAX_SCAN_FILE_BYTES) {
14246
+ throw new Error(`file too large (max ${MAX_SCAN_FILE_BYTES} bytes)`);
14247
+ }
14248
+ return fs20.readFileSync(resolved, "utf-8");
14249
+ }
14250
+ function toolResultPayload(result) {
14251
+ const isError = result.startsWith("Error:") || result.startsWith("Unknown tool:");
14252
+ return {
14253
+ content: [{ type: "text", text: truncateResponse(result) }],
14254
+ ...isError ? { isError: true } : {}
14255
+ };
14256
+ }
14049
14257
  var pkgPath = path20.resolve(__dirname, "..", "package.json");
14050
14258
  var pkgVersion = (() => {
14051
14259
  try {
14052
- return JSON.parse(fs21.readFileSync(pkgPath, "utf-8")).version;
14260
+ return JSON.parse(fs20.readFileSync(pkgPath, "utf-8")).version;
14053
14261
  } catch {
14054
14262
  return "2.7.1";
14055
14263
  }
14056
14264
  })();
14057
- async function resolveProfile(slug) {
14058
- return loadProfileForCommand(slug, { allowServerFetch: true });
14265
+ async function resolveProfile(slug, allowServerFetch = false) {
14266
+ const profile = await loadProfileForCommand(slug, { allowServerFetch });
14267
+ if (slug && !profile) {
14268
+ const { requirePaidFeature: requirePaidFeature2 } = await Promise.resolve().then(() => (init_access(), access_exports));
14269
+ await requirePaidFeature2("premiumPrompts");
14270
+ return loadProfileForCommand(slug, { allowServerFetch: true });
14271
+ }
14272
+ return profile;
14059
14273
  }
14060
14274
  function jsonRpcOk(id, result) {
14061
14275
  return JSON.stringify({ jsonrpc: "2.0", id, result });
@@ -14116,14 +14330,11 @@ async function toolScan(args2) {
14116
14330
  let scanText2 = text;
14117
14331
  const profile = await resolveProfile(args2.profile);
14118
14332
  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})`;
14333
+ try {
14334
+ scanText2 = readScanFile(filePath);
14335
+ } catch (err) {
14336
+ return `Error: ${err.message}`;
14123
14337
  }
14124
- if (!fs21.existsSync(resolved))
14125
- return `Error: file not found: ${filePath}`;
14126
- scanText2 = fs21.readFileSync(resolved, "utf-8");
14127
14338
  }
14128
14339
  const result = runPipeline(scanText2, profile, false);
14129
14340
  if (result.stats.totalSignals === 0) {
@@ -14135,13 +14346,19 @@ async function toolScan(args2) {
14135
14346
  const lines = [
14136
14347
  `Score: ${result.score}/100. Found ${result.stats.totalSignals} issues (${result.stats.red} red, ${result.stats.yellow} yellow):`
14137
14348
  ];
14349
+ const shown = /* @__PURE__ */ new Set();
14138
14350
  if (profile && profileIssues.length > 0) {
14139
14351
  lines.push(`Profile-specific (${profileIssues.length}):`);
14140
14352
  for (const s of profileIssues.slice(0, 5)) {
14353
+ const key = `${s.line}:${s.id}`;
14354
+ shown.add(key);
14141
14355
  lines.push(` line ${s.line}: [${s.severity}] ${s.id} \u2014 ${s.suggestion}`);
14142
14356
  }
14143
14357
  }
14144
14358
  for (const s of result.signalMap.signals.slice(0, 15)) {
14359
+ const key = `${s.line}:${s.id}`;
14360
+ if (shown.has(key))
14361
+ continue;
14145
14362
  lines.push(`line ${s.line}: [${s.severity}] ${s.id} \u2014 ${s.suggestion}`);
14146
14363
  }
14147
14364
  return lines.join("\n");
@@ -14284,7 +14501,7 @@ async function toolAnalyze(args2) {
14284
14501
  return "Error: no text provided";
14285
14502
  try {
14286
14503
  const profile = await resolveProfile(args2.profile);
14287
- const preferServer = args2.prefer_server !== false;
14504
+ const preferServer = args2.prefer_server === true;
14288
14505
  const result = await runHybridAnalysis(text, profile, {
14289
14506
  preferServer,
14290
14507
  profileSlug: args2.profile || profile?.slug
@@ -14407,7 +14624,7 @@ var TOOLS = [
14407
14624
  properties: {
14408
14625
  text: { type: "string", description: "Text to analyze" },
14409
14626
  profile: { type: "string", description: "Voice profile slug" },
14410
- prefer_server: { type: "boolean", description: "Use server hybrid engine when paid (default true)" }
14627
+ prefer_server: { type: "boolean", description: "Use server hybrid engine when paid (default false)" }
14411
14628
  },
14412
14629
  required: ["text"]
14413
14630
  }
@@ -14599,9 +14816,12 @@ async function handleRequest(line) {
14599
14816
  mcpLog("error", `tool ${toolName}: ${err.message}`);
14600
14817
  recordEvent("mcp_tool_error", { tool: toolName, error: err.message });
14601
14818
  }
14602
- send(jsonRpcOk(id, { content: [{ type: "text", text: truncateResponse(result) }] }));
14819
+ send(jsonRpcOk(id, toolResultPayload(result)));
14603
14820
  break;
14604
14821
  }
14822
+ case "ping":
14823
+ send(jsonRpcOk(id, {}));
14824
+ break;
14605
14825
  default:
14606
14826
  if (id !== void 0) {
14607
14827
  send(jsonRpcError(id, -32601, `Method not found: ${method}`));
@@ -14611,7 +14831,7 @@ async function handleRequest(line) {
14611
14831
  }
14612
14832
  async function startMcpServer() {
14613
14833
  const access = await getAccessState().catch(() => null);
14614
- if (fs21.existsSync(VOICE_MD)) {
14834
+ if (fs20.existsSync(VOICE_MD)) {
14615
14835
  mcpLog("info", `voice profile: ${VOICE_MD}`);
14616
14836
  } else {
14617
14837
  mcpLog("info", "free local engine ready \u2014 no voice profile yet");
@@ -14621,16 +14841,17 @@ async function startMcpServer() {
14621
14841
  }
14622
14842
  let buffer = "";
14623
14843
  process.stdin.setEncoding("utf-8");
14844
+ let requestChain = Promise.resolve();
14845
+ const enqueueRequest = (line) => {
14846
+ requestChain = requestChain.then(() => handleRequest(line)).catch((err) => mcpLog("error", err.message));
14847
+ };
14624
14848
  process.stdin.on("data", (chunk) => {
14625
14849
  buffer += chunk;
14626
14850
  const lines = buffer.split("\n");
14627
14851
  buffer = lines.pop() || "";
14628
14852
  for (const line of lines) {
14629
- if (line.trim()) {
14630
- handleRequest(line.trim()).catch((err) => {
14631
- mcpLog("error", err.message);
14632
- });
14633
- }
14853
+ if (line.trim())
14854
+ enqueueRequest(line.trim());
14634
14855
  }
14635
14856
  });
14636
14857
  process.stdin.on("end", () => process.exit(0));
@@ -14643,32 +14864,135 @@ async function startMcpServer() {
14643
14864
 
14644
14865
  // src/lib/mcp-setup.ts
14645
14866
  var import_chalk29 = __toESM(require_source());
14646
- var fs22 = __toESM(require("fs"));
14867
+ var fs21 = __toESM(require("fs"));
14647
14868
  var path21 = __toESM(require("path"));
14648
14869
  var os8 = __toESM(require("os"));
14870
+ var import_child_process3 = require("child_process");
14871
+ init_version();
14649
14872
  var HOME2 = os8.homedir();
14650
14873
  var IS_WIN2 = process.platform === "win32";
14874
+ function claudeDesktopConfigPath() {
14875
+ if (IS_WIN2)
14876
+ return path21.join(HOME2, "AppData", "Roaming", "Claude", "claude_desktop_config.json");
14877
+ if (process.platform === "linux")
14878
+ return path21.join(HOME2, ".config", "Claude", "claude_desktop_config.json");
14879
+ return path21.join(HOME2, "Library", "Application Support", "Claude", "claude_desktop_config.json");
14880
+ }
14881
+ function mcpSnippet() {
14882
+ const entry = [
14883
+ path21.resolve(process.argv[1] || ""),
14884
+ path21.resolve(__dirname, "index.js"),
14885
+ path21.resolve(__dirname, "..", "dist", "index.js")
14886
+ ].find((p) => p && fs21.existsSync(p));
14887
+ if (entry) {
14888
+ return JSON.stringify({ command: process.execPath, args: [entry, "mcp"] }, null, 2);
14889
+ }
14890
+ return JSON.stringify({ command: "hyv", args: ["mcp"] }, null, 2);
14891
+ }
14651
14892
  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
14893
  console.log(import_chalk29.default.bold("\nhold your voice \u2014 mcp setup\n"));
14654
14894
  console.log(import_chalk29.default.dim(` ${getEngineLabel()}
14655
14895
  `));
14656
14896
  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}
14897
+ console.log(import_chalk29.default.dim(" Add to claude_desktop_config.json \u2192 mcpServers.hyv:"));
14898
+ console.log(import_chalk29.default.cyan(` ${mcpSnippet().split("\n").join("\n ")}`));
14899
+ console.log(import_chalk29.default.dim(` Config: ${claudeDesktopConfigPath()}
14660
14900
  `));
14661
14901
  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"));
14902
+ console.log(import_chalk29.default.dim(" MCP: ~/.cursor/mcp.json \u2192 mcpServers.hyv (same JSON as above)"));
14903
+ console.log(import_chalk29.default.dim(" Rule: ~/.cursor/rules/hyv.mdc (alwaysApply \u2014 auto via postinstall)\n"));
14664
14904
  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"));
14905
+ console.log(import_chalk29.default.dim(" Command: ~/.claude/commands/hyv.md"));
14906
+ console.log(import_chalk29.default.dim(" Skill: ~/.claude/skills/hold-your-voice/SKILL.md\n"));
14907
+ console.log(import_chalk29.default.bold("Windsurf"));
14908
+ console.log(import_chalk29.default.dim(" Rule: ~/.windsurf/rules/hyv.md (trigger: always_on)\n"));
14909
+ console.log(import_chalk29.default.bold("Codex"));
14910
+ console.log(import_chalk29.default.dim(" Instructions: ~/.codex/AGENTS.md (merged on install)\n"));
14911
+ console.log(import_chalk29.default.bold("Command Code"));
14912
+ console.log(import_chalk29.default.dim(" Skill: ~/.commandcode/skills/hyv/SKILL.md\n"));
14913
+ console.log(import_chalk29.default.bold("ChatGPT"));
14914
+ console.log(import_chalk29.default.dim(" hyv mcp --setup-chatgpt\n"));
14915
+ console.log(import_chalk29.default.bold("Auto-configure"));
14916
+ console.log(import_chalk29.default.dim(" hyv doctor --fix-agents"));
14917
+ console.log(import_chalk29.default.dim(" HYV_AUTO_CONFIGURE_AGENTS=0 npm i -g @holdyourvoice/hyv (skip)\n"));
14668
14918
  console.log(import_chalk29.default.bold("Verify"));
14669
14919
  console.log(import_chalk29.default.dim(" hyv mcp --test"));
14670
14920
  console.log(import_chalk29.default.dim(" HYV_TELEMETRY=1 hyv mcp (optional usage logging to ~/.hyv/telemetry/)\n"));
14671
14921
  }
14922
+ async function testMcpStdioSubprocess() {
14923
+ const candidates = [
14924
+ path21.resolve(process.argv[1] || ""),
14925
+ path21.resolve(__dirname, "index.js"),
14926
+ path21.resolve(__dirname, "..", "dist", "index.js")
14927
+ ];
14928
+ const entry = candidates.find((p) => p && fs21.existsSync(p));
14929
+ if (!entry)
14930
+ return { ok: false };
14931
+ return new Promise((resolve14) => {
14932
+ const child = (0, import_child_process3.spawn)(process.execPath, [entry, "mcp"], {
14933
+ stdio: ["pipe", "pipe", "pipe"],
14934
+ env: { ...process.env, HYV_POSTINSTALL_QUIET: "1" }
14935
+ });
14936
+ let buffer = "";
14937
+ let settled = false;
14938
+ const finish = (ok, toolCount) => {
14939
+ if (settled)
14940
+ return;
14941
+ settled = true;
14942
+ clearTimeout(timeout);
14943
+ try {
14944
+ child.kill();
14945
+ } catch {
14946
+ }
14947
+ resolve14({ ok, toolCount });
14948
+ };
14949
+ const timeout = setTimeout(() => finish(false), 1e4);
14950
+ const handleLine = (line) => {
14951
+ const trimmed = line.trim();
14952
+ if (!trimmed.startsWith("{"))
14953
+ return;
14954
+ try {
14955
+ const msg = JSON.parse(trimmed);
14956
+ if (msg.id === 2 && Array.isArray(msg.result?.tools) && msg.result.tools.length > 0) {
14957
+ finish(true, msg.result.tools.length);
14958
+ }
14959
+ } catch {
14960
+ }
14961
+ };
14962
+ child.stdout.on("data", (chunk) => {
14963
+ buffer += chunk.toString();
14964
+ const lines = buffer.split("\n");
14965
+ buffer = lines.pop() || "";
14966
+ for (const line of lines)
14967
+ handleLine(line);
14968
+ });
14969
+ child.on("error", () => finish(false));
14970
+ child.on("exit", () => {
14971
+ if (!settled) {
14972
+ handleLine(buffer);
14973
+ finish(buffer.includes("hyv_scan"));
14974
+ }
14975
+ });
14976
+ setTimeout(() => {
14977
+ const send2 = (payload) => {
14978
+ child.stdin.write(`${JSON.stringify(payload)}
14979
+ `);
14980
+ };
14981
+ send2({
14982
+ jsonrpc: "2.0",
14983
+ id: 1,
14984
+ method: "initialize",
14985
+ params: {
14986
+ protocolVersion: "2024-11-05",
14987
+ capabilities: {},
14988
+ clientInfo: { name: "hyv-self-test", version: "1.0" }
14989
+ }
14990
+ });
14991
+ send2({ jsonrpc: "2.0", method: "notifications/initialized" });
14992
+ send2({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} });
14993
+ }, 500);
14994
+ });
14995
+ }
14672
14996
  async function runMcpSelfTest() {
14673
14997
  console.log(import_chalk29.default.bold("\nhold your voice \u2014 mcp self-test\n"));
14674
14998
  console.log(import_chalk29.default.dim(` ${getEngineLabel()}
@@ -14683,7 +15007,7 @@ async function runMcpSelfTest() {
14683
15007
  console.log(import_chalk29.default.red(` \u2717 expected 10+ tools, got ${tools.length}`));
14684
15008
  failed++;
14685
15009
  }
14686
- const required = ["hyv_welcome", "hyv_scan", "hyv_analyze", "hyv_clean", "hyv_validate"];
15010
+ const required = ["hyv_welcome", "hyv_scan", "hyv_analyze", "hyv_clean", "hyv_validate", "hyv_list_free_tools"];
14687
15011
  for (const name of required) {
14688
15012
  if (tools.includes(name)) {
14689
15013
  console.log(import_chalk29.default.green(` \u2713 tool: ${name}`));
@@ -14720,7 +15044,7 @@ async function runMcpSelfTest() {
14720
15044
  failed++;
14721
15045
  }
14722
15046
  const voiceMd = path21.join(HOME2, ".hyv", "voice.md");
14723
- if (fs22.existsSync(voiceMd)) {
15047
+ if (fs21.existsSync(voiceMd)) {
14724
15048
  console.log(import_chalk29.default.green(" \u2713 voice profile found"));
14725
15049
  passed++;
14726
15050
  } else {
@@ -14733,6 +15057,22 @@ async function runMcpSelfTest() {
14733
15057
  console.log(import_chalk29.default.red(" \u2717 demo content missing"));
14734
15058
  failed++;
14735
15059
  }
15060
+ try {
15061
+ const stdio = await testMcpStdioSubprocess();
15062
+ if (stdio.ok && (stdio.toolCount || 0) >= 10) {
15063
+ console.log(import_chalk29.default.green(` \u2713 stdio MCP subprocess responds (${stdio.toolCount} tools)`));
15064
+ passed++;
15065
+ } else if (stdio.ok) {
15066
+ console.log(import_chalk29.default.yellow(` ! stdio MCP subprocess ok but only ${stdio.toolCount || 0} tools`));
15067
+ failed++;
15068
+ } else {
15069
+ console.log(import_chalk29.default.red(" \u2717 stdio MCP subprocess failed"));
15070
+ failed++;
15071
+ }
15072
+ } catch (e) {
15073
+ console.log(import_chalk29.default.red(` \u2717 stdio MCP subprocess failed: ${e.message}`));
15074
+ failed++;
15075
+ }
14736
15076
  console.log("");
14737
15077
  if (failed === 0) {
14738
15078
  console.log(import_chalk29.default.green(`\u2713 all checks passed (${passed})`));
@@ -14744,7 +15084,7 @@ async function runMcpSelfTest() {
14744
15084
  }
14745
15085
 
14746
15086
  // src/commands/export.ts
14747
- var fs23 = __toESM(require("fs"));
15087
+ var fs22 = __toESM(require("fs"));
14748
15088
  init_api();
14749
15089
  var FORMATS = {
14750
15090
  claude: {
@@ -14831,7 +15171,7 @@ async function exportCommand(format, opts) {
14831
15171
  const prompt = fmt.wrap(detail.profile, detail.body);
14832
15172
  if (opts.output) {
14833
15173
  const outFile = opts.output.replace("{name}", profile.slug || profile.name);
14834
- fs23.writeFileSync(outFile, prompt);
15174
+ fs22.writeFileSync(outFile, prompt);
14835
15175
  results.push({ profile: profile.name, file: outFile });
14836
15176
  } else {
14837
15177
  results.push({ profile: profile.name, prompt });
@@ -14868,13 +15208,13 @@ async function exportCommand(format, opts) {
14868
15208
  // src/index.ts
14869
15209
  init_access();
14870
15210
  init_welcome();
14871
- var fs24 = __toESM(require("fs"));
15211
+ var fs23 = __toESM(require("fs"));
14872
15212
  var path22 = __toESM(require("path"));
14873
15213
  var program2 = new Command();
14874
15214
  var pkgPath2 = path22.resolve(__dirname, "..", "package.json");
14875
15215
  var pkgVersion2 = (() => {
14876
15216
  try {
14877
- return JSON.parse(fs24.readFileSync(pkgPath2, "utf-8")).version;
15217
+ return JSON.parse(fs23.readFileSync(pkgPath2, "utf-8")).version;
14878
15218
  } catch {
14879
15219
  return "2.7.1";
14880
15220
  }