@sunilaleti/hx 1.0.0 → 1.1.1

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/CHANGELOG.md CHANGED
@@ -28,6 +28,40 @@ the project follows [semantic versioning](https://semver.org/).
28
28
 
29
29
  ---
30
30
 
31
+ ## [1.1.1] - 2026-10-31
32
+
33
+ ### Fixed
34
+
35
+ - HTTPS failed on macOS for every host - OpenRouter, ChatGPT, Tavily, npm - with "unable to get
36
+ local issuer certificate". HX verified against the OS trust store alone, and on macOS Node reads
37
+ only the certificates trusted by hand there, not the system roots. HX now trusts Node's own
38
+ roots and the OS store together, so a corporate CA in the keychain still works.
39
+ - A certificate HX will not accept is reported with what to do about it - add the CA to the
40
+ system trust store or set `HX_CA_BUNDLE` - instead of Node's advice to restart it with
41
+ `--use-system-ca`, and is not retried: another attempt meets the same certificate.
42
+
43
+ ---
44
+
45
+ ## [1.1.0] - 2026-10-31
46
+
47
+ ### Added
48
+
49
+ - HX updates itself, as Claude Code does. About once a day a session checks npm for a newer
50
+ release, installs it in the background with `npm install --global`, and says to restart.
51
+ `hx update` (or `hx upgrade`) updates now. Turn the background update off with
52
+ `"autoUpdates": false` in settings or `HX_DISABLE_AUTOUPDATER=1`. Only an npm install updates
53
+ itself; `hx -p` never does.
54
+ 1.0.0 has no updater: install 1.1.0 once with `npm install --global @sunilaleti/hx@latest`, and
55
+ it keeps itself current from there.
56
+
57
+ ### Changed
58
+
59
+ - CI runs the suite twice instead of four times - Linux and macOS, both on Node 24 - and
60
+ publishes with the npm that ships with Node 24. Node 22, the oldest HX supports, is checked
61
+ by installing the package on it and running `hx --version`.
62
+
63
+ ---
64
+
31
65
  ## [1.0.0] - 2026-10-31
32
66
 
33
67
  ### Changed
@@ -1103,7 +1137,9 @@ First release, published to PyPI as [`hx-cli`](https://pypi.org/project/hx-cli/)
1103
1137
  - `/configure` and `hx auth` for the OpenRouter key, `hx upgrade` for
1104
1138
  self-update, and `install.sh` bootstrapping uv with a pinned Python.
1105
1139
 
1106
- [Unreleased]: https://github.com/aletisunil/hx/compare/v1.0.0...HEAD
1140
+ [Unreleased]: https://github.com/aletisunil/hx/compare/v1.1.1...HEAD
1141
+ [1.1.1]: https://github.com/aletisunil/hx/compare/v1.1.0...v1.1.1
1142
+ [1.1.0]: https://github.com/aletisunil/hx/compare/v1.0.0...v1.1.0
1107
1143
  [1.0.0]: https://github.com/aletisunil/hx/compare/v0.3.0-beta.1...v1.0.0
1108
1144
  [0.3.0-beta.1]: https://github.com/aletisunil/hx/compare/v0.2.20...v0.3.0-beta.1
1109
1145
  [0.2.20]: https://github.com/aletisunil/hx/compare/v0.2.19...v0.2.20
package/README.md CHANGED
@@ -16,8 +16,13 @@ npm install --global @sunilaleti/hx
16
16
  ```
17
17
 
18
18
  HX runs on [Node.js](https://nodejs.org/) 22.15 or newer, on macOS and Linux.
19
- Run the same command again to update. Then run `hx`: the first time, it opens
20
- with the HX logo and says to type `/login`.
19
+ Then run `hx`: the first time, it opens with the HX logo and says to type
20
+ `/login`.
21
+
22
+ HX keeps itself up to date, as Claude Code does. About once a day, a session
23
+ checks npm for a newer release, installs it in the background, and says to
24
+ restart. Run `hx update` to update now. To turn the background update off, set
25
+ `"autoUpdates": false` in `~/.hx/settings.json`, or `HX_DISABLE_AUTOUPDATER=1`.
21
26
 
22
27
  Or let the install script check your Node.js version and run that command:
23
28
 
@@ -288,9 +293,9 @@ number.
288
293
  Credits are *not* in the status bar's cost, which prices tokens: `/cost`
289
294
  under-reports a session that searched.
290
295
 
291
- **Behind a TLS-inspecting proxy.** HX verifies certificates against the
292
- operating system's trust store, so a corporate CA the machine already trusts
293
- needs no setup. When the CA lives only in a file, name it:
296
+ **Behind a TLS-inspecting proxy.** HX verifies certificates against Node's own
297
+ roots and the operating system's trust store together, so a corporate CA the
298
+ machine already trusts needs no setup. When the CA lives only in a file, name it:
294
299
 
295
300
  ```sh
296
301
  export HX_CA_BUNDLE=/path/to/corp-ca.pem # SSL_CERT_FILE and REQUESTS_CA_BUNDLE also work
@@ -349,6 +354,7 @@ hx auth login [provider] # sign in (openrouter, openai-codex, devin, claude-c
349
354
  hx auth logout <provider> # forget a stored credential
350
355
  hx docs [section] # the manual; no argument lists its sections
351
356
  hx changelog [version] # what shipped in each version
357
+ hx update # install the newest release now (also: hx upgrade)
352
358
  ```
353
359
 
354
360
  Print mode is the scriptable one: stdout carries only the assistant's text, so
@@ -663,6 +669,7 @@ along with the `.hx/.gitignore` it added to hide it.
663
669
  {
664
670
  "theme": "dark", // dark | light | ansi, or a file in ~/.hx/themes
665
671
  "quiet_startup": false, // skip the startup header
672
+ "autoUpdates": true, // install new releases in the background, once a day
666
673
 
667
674
  "models": {
668
675
  "model": "anthropic/claude-sonnet-4.5",
package/dist/hx.js CHANGED
@@ -534,10 +534,8 @@ function tlsAgentOptions() {
534
534
  const bundle = caBundle();
535
535
  let ca;
536
536
  if (bundle !== null) ca = certificates(readText(bundle));
537
- else {
538
- ca = tls.getCACertificates("system");
539
- if (ca.length === 0) ca = tls.getCACertificates("default");
540
- }
537
+ else
538
+ ca = [.../* @__PURE__ */ new Set([...tls.getCACertificates("default"), ...tls.getCACertificates("system")])];
541
539
  tlsOptions = { ca, ALPNProtocols: ["http/1.1"] };
542
540
  }
543
541
  return tlsOptions;
@@ -582,12 +580,10 @@ function withTlsHint(message2) {
582
580
  return `${rstrip(rstrip(message2), ".")}. ${hint2}`;
583
581
  }
584
582
  function describe(error) {
583
+ const certificate = certificateFailure(error);
584
+ if (certificate !== null) return certificate;
585
585
  for (const err of chain(error)) {
586
586
  const code2 = err.code ?? "";
587
- if (VERIFY_CODES.has(code2)) {
588
- const reason = flatten(err.message || "certificate verify failed");
589
- return `TLS certificate verification failed (${reason}). ${tlsHint(reason)}`;
590
- }
591
587
  if (code2.startsWith("ERR_SSL") || code2.startsWith("ERR_TLS")) {
592
588
  return `TLS handshake failed: ${flatten(err.message)}.`;
593
589
  }
@@ -599,6 +595,19 @@ function describe(error) {
599
595
  const failure2 = error;
600
596
  return withTlsHint(`${failure2?.name ?? "Error"}: ${flatten(String(failure2?.message ?? error))}`);
601
597
  }
598
+ function certificateFailure(error) {
599
+ for (const err of chain(error)) {
600
+ if (!VERIFY_CODES.has(err.code ?? "")) continue;
601
+ const reason = flatten(
602
+ (err.message || "certificate verify failed").replace(
603
+ /;\s*if the root CA[\s\S]*$/,
604
+ ""
605
+ )
606
+ );
607
+ return `TLS certificate verification failed (${reason}). ${tlsHint(reason)}`;
608
+ }
609
+ return null;
610
+ }
602
611
  function chain(error) {
603
612
  const seen = [];
604
613
  let current = error;
@@ -1725,11 +1734,17 @@ function streamEnd(stopReason, fields = {}) {
1725
1734
  planWindows: fields.planWindows ?? []
1726
1735
  };
1727
1736
  }
1737
+ function unreached(error) {
1738
+ const certificate = certificateFailure(error);
1739
+ if (certificate !== null) return new ProviderError(certificate);
1740
+ return new ProviderError(error.message, { retryable: true });
1741
+ }
1728
1742
  var delta, ProviderError;
1729
1743
  var init_base = __esm({
1730
1744
  "src/providers/base.ts"() {
1731
1745
  "use strict";
1732
1746
  init_usage();
1747
+ init_net();
1733
1748
  delta = (fields) => ({
1734
1749
  kind: "delta",
1735
1750
  ...fields
@@ -3288,7 +3303,7 @@ var init_openrouter = __esm({
3288
3303
  });
3289
3304
  } catch (error) {
3290
3305
  if (signal.aborted) throw error;
3291
- throw new ProviderError(error.message, { retryable: true });
3306
+ throw unreached(error);
3292
3307
  }
3293
3308
  try {
3294
3309
  if (response.status >= 400) {
@@ -6351,7 +6366,8 @@ function buildSettings(data, cwd) {
6351
6366
  tui: { enterWhileBusy: busyRaw, fullscreen: truthy(tui.fullscreen ?? false) },
6352
6367
  tools: { hashline: truthy(pick(tools, "hashline", true)) },
6353
6368
  theme: pick(data, "theme", "dark"),
6354
- quietStartup: truthy(pick(data, "quietStartup", pick(data, "quiet_startup", false)))
6369
+ quietStartup: truthy(pick(data, "quietStartup", pick(data, "quiet_startup", false))),
6370
+ autoUpdates: truthy(pick(data, "autoUpdates", pick(data, "auto_updates", true)))
6355
6371
  };
6356
6372
  }
6357
6373
  function iterate(value) {
@@ -7189,7 +7205,7 @@ async function unaryCall(baseUrl, path, body, operation, timeout = 600, signal)
7189
7205
  [status3, payload] = await post(`${baseUrl}${path}`, body, UNARY_HEADERS, timeout, signal);
7190
7206
  } catch (error) {
7191
7207
  if (signal?.aborted) throw error;
7192
- throw new ProviderError(error.message, { retryable: true });
7208
+ throw unreached(error);
7193
7209
  }
7194
7210
  if (status3 >= 400) throw httpError(operation, status3, payload);
7195
7211
  return payload;
@@ -7485,7 +7501,7 @@ var init_devin2 = __esm({
7485
7501
  });
7486
7502
  } catch (error) {
7487
7503
  if (signal.aborted) throw error;
7488
- throw new ProviderError(error.message, { retryable: true });
7504
+ throw unreached(error);
7489
7505
  }
7490
7506
  try {
7491
7507
  if (response.status >= 400) {
@@ -67732,7 +67748,8 @@ init_net();
67732
67748
  init_py();
67733
67749
 
67734
67750
  // src/version.ts
67735
- var VERSION = "1.0.0";
67751
+ var VERSION = "1.1.1";
67752
+ var PACKAGE = "@sunilaleti/hx";
67736
67753
 
67737
67754
  // src/providers/codex.ts
67738
67755
  init_base();
@@ -67803,7 +67820,7 @@ var CodexProvider = class {
67803
67820
  });
67804
67821
  } catch (error) {
67805
67822
  if (signal.aborted) throw error;
67806
- throw new ProviderError(error.message, { retryable: true });
67823
+ throw unreached(error);
67807
67824
  }
67808
67825
  const windows = planWindows(response.headers);
67809
67826
  try {
@@ -75807,11 +75824,11 @@ var LANGUAGES = {
75807
75824
  ".go": GO,
75808
75825
  ".rs": RUST
75809
75826
  };
75810
- var PACKAGE = "@vscode/tree-sitter-wasm";
75827
+ var PACKAGE2 = "@vscode/tree-sitter-wasm";
75811
75828
  var load = createRequire(import.meta.url);
75812
75829
  function packageDir() {
75813
75830
  try {
75814
- return dirname(load.resolve(PACKAGE));
75831
+ return dirname(load.resolve(PACKAGE2));
75815
75832
  } catch {
75816
75833
  return null;
75817
75834
  }
@@ -75825,9 +75842,9 @@ function parserFor(grammar) {
75825
75842
  parser = (async () => {
75826
75843
  const directory = packageDir();
75827
75844
  if (directory === null)
75828
- throw new ToolError(`tree-sitter is not installed: npm install -g ${PACKAGE}`);
75845
+ throw new ToolError(`tree-sitter is not installed: npm install -g ${PACKAGE2}`);
75829
75846
  runtime ??= (async () => {
75830
- const module = load(PACKAGE);
75847
+ const module = load(PACKAGE2);
75831
75848
  await module.Parser.init({ locateFile: (file) => join(directory, file) });
75832
75849
  return module;
75833
75850
  })();
@@ -89678,6 +89695,17 @@ var HXSession = class {
89678
89695
  this.refreshStatus();
89679
89696
  if (this.loop.session.activeMessages().length) this.replayTranscript();
89680
89697
  if (this.extra.signIn) this.showSignIn(this.extra.signIn);
89698
+ void this.extra.update?.then((outcome2) => {
89699
+ if (outcome2 === null) return;
89700
+ const level = outcome2.ok ? "success" : "warning";
89701
+ if (this.view.transcript.blocks.length) {
89702
+ this.notice(outcome2.message, level);
89703
+ return;
89704
+ }
89705
+ this.view.transcript.add(new Spacer(1));
89706
+ this.view.transcript.add(new Notice(outcome2.message, level));
89707
+ this.runner.requestRender();
89708
+ });
89681
89709
  if (this.loop.permissions !== null)
89682
89710
  this.loop.permissions.asker = (request2) => this.askPermission(request2);
89683
89711
  const events = this.consumeEvents();
@@ -90680,6 +90708,145 @@ async function runSession(loop, bus, settings, extra = {}) {
90680
90708
  return session.relaunch;
90681
90709
  }
90682
90710
 
90711
+ // src/update.ts
90712
+ init_json();
90713
+ init_net();
90714
+ init_pathlib();
90715
+ init_paths();
90716
+ init_py();
90717
+ import { spawn as spawn10 } from "node:child_process";
90718
+ import { mkdirSync as mkdirSync15, openSync as openSync10, readFileSync as readFileSync10, realpathSync as realpathSync3, writeFileSync as writeFileSync12 } from "node:fs";
90719
+ init_which();
90720
+ var RELEASES_URL2 = `https://registry.npmjs.org/-/package/${PACKAGE}/dist-tags`;
90721
+ var RELEASES_ENV2 = "HX_RELEASES_URL";
90722
+ var DISABLE_ENV = "HX_DISABLE_AUTOUPDATER";
90723
+ var RELEASES_TIMEOUT2 = 5;
90724
+ var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
90725
+ var stateFile = () => join(userHome(), "update.json");
90726
+ async function latestRelease(signal) {
90727
+ const url = strip(process.env[RELEASES_ENV2] ?? "") || RELEASES_URL2;
90728
+ try {
90729
+ const response = await request(url, {
90730
+ timeout: RELEASES_TIMEOUT2,
90731
+ ...signal ? { signal } : {}
90732
+ });
90733
+ try {
90734
+ if (response.status < 200 || response.status >= 300) return null;
90735
+ const tags = await response.json();
90736
+ const latest = typeof tags === "object" && tags !== null ? tags.latest : null;
90737
+ return typeof latest === "string" && RELEASE2.test(latest) ? latest : null;
90738
+ } finally {
90739
+ response.close();
90740
+ }
90741
+ } catch {
90742
+ return null;
90743
+ }
90744
+ }
90745
+ var RELEASE2 = /^\d+\.\d+\.\d+$/;
90746
+ function compareReleases(a, b) {
90747
+ const left = a.split(".").map(Number);
90748
+ const right = b.split(".").map(Number);
90749
+ for (let index = 0; index < 3; index++) {
90750
+ const difference = (left[index] ?? 0) - (right[index] ?? 0);
90751
+ if (difference) return difference;
90752
+ }
90753
+ return 0;
90754
+ }
90755
+ function notAnNpmInstall() {
90756
+ const script = process.argv[1] ?? "";
90757
+ let real2;
90758
+ try {
90759
+ real2 = realpathSync3(script);
90760
+ } catch {
90761
+ return script;
90762
+ }
90763
+ return real2.includes(`/node_modules/${PACKAGE}/`) ? null : parent(parent(real2));
90764
+ }
90765
+ function npm() {
90766
+ return which("npm") ?? join(parent(process.execPath), "npm");
90767
+ }
90768
+ var INSTALL_COMMAND = `npm install --global ${PACKAGE}`;
90769
+ function runNpm(version, background) {
90770
+ const args = ["install", "--global", `${PACKAGE}@${version}`];
90771
+ let stdio = "inherit";
90772
+ if (background) {
90773
+ mkdirSync15(logsDir(), { recursive: true });
90774
+ const log = openSync10(join(logsDir(), "update.log"), "a");
90775
+ stdio = ["ignore", log, log];
90776
+ }
90777
+ return new Promise((resolve2) => {
90778
+ const child = spawn10(npm(), args, { stdio, detached: background });
90779
+ child.once("error", () => resolve2(null));
90780
+ child.once("exit", (code2, signal) => resolve2(code2 ?? (signal ? 1 : 0)));
90781
+ if (background) child.unref();
90782
+ });
90783
+ }
90784
+ async function runUpdate(print2) {
90785
+ const elsewhere2 = notAnNpmInstall();
90786
+ if (elsewhere2 !== null) {
90787
+ print2(
90788
+ `hx is running from ${elsewhere2}, not from an npm install, so it cannot update itself. Update a checkout with git pull and npm run build, or install the release: ${INSTALL_COMMAND}`,
90789
+ process.stderr
90790
+ );
90791
+ return 1;
90792
+ }
90793
+ const latest = await latestRelease();
90794
+ if (latest === null) {
90795
+ print2("Could not reach the npm registry to check for a newer hx.", process.stderr);
90796
+ return 1;
90797
+ }
90798
+ if (compareReleases(latest, VERSION) <= 0) {
90799
+ print2(`hx ${VERSION} is up to date.`);
90800
+ return 0;
90801
+ }
90802
+ print2(`Updating hx ${VERSION} to ${latest}...`);
90803
+ const status3 = await runNpm(latest, false);
90804
+ if (status3 === null) {
90805
+ print2(
90806
+ `npm is not installed, so hx cannot update itself. See https://nodejs.org/`,
90807
+ process.stderr
90808
+ );
90809
+ return 1;
90810
+ }
90811
+ if (status3 !== 0) {
90812
+ print2(`npm could not install ${PACKAGE}@${latest} (exit ${status3}).`, process.stderr);
90813
+ return 1;
90814
+ }
90815
+ print2(`Updated hx to ${latest}.`);
90816
+ return 0;
90817
+ }
90818
+ function lastChecked() {
90819
+ try {
90820
+ const state = JSON.parse(readFileSync10(stateFile(), "utf8"));
90821
+ const at = typeof state === "object" && state !== null ? state.checkedAt : 0;
90822
+ return typeof at === "number" ? at : 0;
90823
+ } catch {
90824
+ return 0;
90825
+ }
90826
+ }
90827
+ async function autoUpdate(enabled, signal) {
90828
+ const disabled = strip(process.env[DISABLE_ENV] ?? "").toLowerCase();
90829
+ if (!enabled || ["1", "true", "yes"].includes(disabled)) return null;
90830
+ if (notAnNpmInstall() !== null) return null;
90831
+ const now3 = Date.now();
90832
+ if (now3 - lastChecked() < CHECK_INTERVAL_MS) return null;
90833
+ try {
90834
+ writeFileSync12(stateFile(), `${dumps({ checkedAt: now3 })}
90835
+ `);
90836
+ } catch {
90837
+ return null;
90838
+ }
90839
+ const latest = await latestRelease(signal);
90840
+ if (signal.aborted || latest === null || compareReleases(latest, VERSION) <= 0) return null;
90841
+ const status3 = await runNpm(latest, true);
90842
+ if (status3 === 0) return { message: `Updated hx to ${latest}. Restart hx to use it.`, ok: true };
90843
+ const why = status3 === null ? "npm could not be started" : `npm exited ${status3}`;
90844
+ return {
90845
+ message: `Could not update hx to ${latest}: ${why}. Run hx update to see why.`,
90846
+ ok: false
90847
+ };
90848
+ }
90849
+
90683
90850
  // src/cli.ts
90684
90851
  var USAGE = `hx - an agent harness for the terminal
90685
90852
 
@@ -90695,6 +90862,7 @@ Usage:
90695
90862
  hx auth [set|clear] Show or change the OpenRouter API key
90696
90863
  hx docs [SECTION|--all] Print the manual, or one section of it
90697
90864
  hx changelog [VERSION] Print what shipped in each version
90865
+ hx update Update hx to the newest release
90698
90866
  hx --version Show version
90699
90867
  hx --help Show this message
90700
90868
 
@@ -90758,6 +90926,9 @@ ${USAGE}`, process.stderr);
90758
90926
  return runMcpCommand(parsed.rest);
90759
90927
  case "trace":
90760
90928
  return runTraceCommand(parsed);
90929
+ case "update":
90930
+ case "upgrade":
90931
+ return runUpdate(print);
90761
90932
  default:
90762
90933
  return runTuiCommand(parsed);
90763
90934
  }
@@ -90816,7 +90987,7 @@ function parseArgs(args) {
90816
90987
  }
90817
90988
  if (positional.length) {
90818
90989
  const [head4, ...tail3] = positional;
90819
- if (["resume", "prompt", "trace", "mcp", "auth"].includes(head4)) {
90990
+ if (["resume", "prompt", "trace", "mcp", "auth", "update", "upgrade"].includes(head4)) {
90820
90991
  parsed.command = head4;
90821
90992
  parsed.rest = tail3;
90822
90993
  if (head4 === "resume" && tail3.length) parsed.sessionId = tail3[0];
@@ -91505,7 +91676,9 @@ async function runTuiCommand(parsed) {
91505
91676
  if (moved) runtime2.notices.push(moved);
91506
91677
  const signIn2 = runtime2.provider instanceof DeferredProvider ? signInHint(runtime2.loop.model, runtime2.auth) : null;
91507
91678
  let relaunch = null;
91679
+ const sessionOver = new AbortController();
91508
91680
  try {
91681
+ const update = autoUpdate(runtime2.settings.autoUpdates, sessionOver.signal);
91509
91682
  await runtime2.connectMcp();
91510
91683
  await runtime2.refreshModelsIfStale();
91511
91684
  relaunch = await runSession(runtime2.loop, runtime2.bus, runtime2.settings, {
@@ -91519,9 +91692,11 @@ async function runTuiCommand(parsed) {
91519
91692
  notices: runtime2.notices,
91520
91693
  checkpoints: runtime2.checkpoints,
91521
91694
  renameLeft,
91522
- signIn: signIn2
91695
+ signIn: signIn2,
91696
+ update
91523
91697
  });
91524
91698
  } finally {
91699
+ sessionOver.abort();
91525
91700
  runtime2.bus.close();
91526
91701
  await runtime2.aclose();
91527
91702
  }