@sunilaleti/hx 1.0.0 → 1.1.0

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,26 @@ the project follows [semantic versioning](https://semver.org/).
28
28
 
29
29
  ---
30
30
 
31
+ ## [1.1.0] - 2026-10-31
32
+
33
+ ### Added
34
+
35
+ - HX updates itself, as Claude Code does. About once a day a session checks npm for a newer
36
+ release, installs it in the background with `npm install --global`, and says to restart.
37
+ `hx update` (or `hx upgrade`) updates now. Turn the background update off with
38
+ `"autoUpdates": false` in settings or `HX_DISABLE_AUTOUPDATER=1`. Only an npm install updates
39
+ itself; `hx -p` never does.
40
+ 1.0.0 has no updater: install 1.1.0 once with `npm install --global @sunilaleti/hx@latest`, and
41
+ it keeps itself current from there.
42
+
43
+ ### Changed
44
+
45
+ - CI runs the suite twice instead of four times - Linux and macOS, both on Node 24 - and
46
+ publishes with the npm that ships with Node 24. Node 22, the oldest HX supports, is checked
47
+ by installing the package on it and running `hx --version`.
48
+
49
+ ---
50
+
31
51
  ## [1.0.0] - 2026-10-31
32
52
 
33
53
  ### Changed
@@ -1103,7 +1123,8 @@ First release, published to PyPI as [`hx-cli`](https://pypi.org/project/hx-cli/)
1103
1123
  - `/configure` and `hx auth` for the OpenRouter key, `hx upgrade` for
1104
1124
  self-update, and `install.sh` bootstrapping uv with a pinned Python.
1105
1125
 
1106
- [Unreleased]: https://github.com/aletisunil/hx/compare/v1.0.0...HEAD
1126
+ [Unreleased]: https://github.com/aletisunil/hx/compare/v1.1.0...HEAD
1127
+ [1.1.0]: https://github.com/aletisunil/hx/compare/v1.0.0...v1.1.0
1107
1128
  [1.0.0]: https://github.com/aletisunil/hx/compare/v0.3.0-beta.1...v1.0.0
1108
1129
  [0.3.0-beta.1]: https://github.com/aletisunil/hx/compare/v0.2.20...v0.3.0-beta.1
1109
1130
  [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
 
@@ -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
@@ -6351,7 +6351,8 @@ function buildSettings(data, cwd) {
6351
6351
  tui: { enterWhileBusy: busyRaw, fullscreen: truthy(tui.fullscreen ?? false) },
6352
6352
  tools: { hashline: truthy(pick(tools, "hashline", true)) },
6353
6353
  theme: pick(data, "theme", "dark"),
6354
- quietStartup: truthy(pick(data, "quietStartup", pick(data, "quiet_startup", false)))
6354
+ quietStartup: truthy(pick(data, "quietStartup", pick(data, "quiet_startup", false))),
6355
+ autoUpdates: truthy(pick(data, "autoUpdates", pick(data, "auto_updates", true)))
6355
6356
  };
6356
6357
  }
6357
6358
  function iterate(value) {
@@ -67732,7 +67733,8 @@ init_net();
67732
67733
  init_py();
67733
67734
 
67734
67735
  // src/version.ts
67735
- var VERSION = "1.0.0";
67736
+ var VERSION = "1.1.0";
67737
+ var PACKAGE = "@sunilaleti/hx";
67736
67738
 
67737
67739
  // src/providers/codex.ts
67738
67740
  init_base();
@@ -75807,11 +75809,11 @@ var LANGUAGES = {
75807
75809
  ".go": GO,
75808
75810
  ".rs": RUST
75809
75811
  };
75810
- var PACKAGE = "@vscode/tree-sitter-wasm";
75812
+ var PACKAGE2 = "@vscode/tree-sitter-wasm";
75811
75813
  var load = createRequire(import.meta.url);
75812
75814
  function packageDir() {
75813
75815
  try {
75814
- return dirname(load.resolve(PACKAGE));
75816
+ return dirname(load.resolve(PACKAGE2));
75815
75817
  } catch {
75816
75818
  return null;
75817
75819
  }
@@ -75825,9 +75827,9 @@ function parserFor(grammar) {
75825
75827
  parser = (async () => {
75826
75828
  const directory = packageDir();
75827
75829
  if (directory === null)
75828
- throw new ToolError(`tree-sitter is not installed: npm install -g ${PACKAGE}`);
75830
+ throw new ToolError(`tree-sitter is not installed: npm install -g ${PACKAGE2}`);
75829
75831
  runtime ??= (async () => {
75830
- const module = load(PACKAGE);
75832
+ const module = load(PACKAGE2);
75831
75833
  await module.Parser.init({ locateFile: (file) => join(directory, file) });
75832
75834
  return module;
75833
75835
  })();
@@ -89678,6 +89680,17 @@ var HXSession = class {
89678
89680
  this.refreshStatus();
89679
89681
  if (this.loop.session.activeMessages().length) this.replayTranscript();
89680
89682
  if (this.extra.signIn) this.showSignIn(this.extra.signIn);
89683
+ void this.extra.update?.then((outcome2) => {
89684
+ if (outcome2 === null) return;
89685
+ const level = outcome2.ok ? "success" : "warning";
89686
+ if (this.view.transcript.blocks.length) {
89687
+ this.notice(outcome2.message, level);
89688
+ return;
89689
+ }
89690
+ this.view.transcript.add(new Spacer(1));
89691
+ this.view.transcript.add(new Notice(outcome2.message, level));
89692
+ this.runner.requestRender();
89693
+ });
89681
89694
  if (this.loop.permissions !== null)
89682
89695
  this.loop.permissions.asker = (request2) => this.askPermission(request2);
89683
89696
  const events = this.consumeEvents();
@@ -90680,6 +90693,145 @@ async function runSession(loop, bus, settings, extra = {}) {
90680
90693
  return session.relaunch;
90681
90694
  }
90682
90695
 
90696
+ // src/update.ts
90697
+ init_json();
90698
+ init_net();
90699
+ init_pathlib();
90700
+ init_paths();
90701
+ init_py();
90702
+ import { spawn as spawn10 } from "node:child_process";
90703
+ import { mkdirSync as mkdirSync15, openSync as openSync10, readFileSync as readFileSync10, realpathSync as realpathSync3, writeFileSync as writeFileSync12 } from "node:fs";
90704
+ init_which();
90705
+ var RELEASES_URL2 = `https://registry.npmjs.org/-/package/${PACKAGE}/dist-tags`;
90706
+ var RELEASES_ENV2 = "HX_RELEASES_URL";
90707
+ var DISABLE_ENV = "HX_DISABLE_AUTOUPDATER";
90708
+ var RELEASES_TIMEOUT2 = 5;
90709
+ var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
90710
+ var stateFile = () => join(userHome(), "update.json");
90711
+ async function latestRelease(signal) {
90712
+ const url = strip(process.env[RELEASES_ENV2] ?? "") || RELEASES_URL2;
90713
+ try {
90714
+ const response = await request(url, {
90715
+ timeout: RELEASES_TIMEOUT2,
90716
+ ...signal ? { signal } : {}
90717
+ });
90718
+ try {
90719
+ if (response.status < 200 || response.status >= 300) return null;
90720
+ const tags = await response.json();
90721
+ const latest = typeof tags === "object" && tags !== null ? tags.latest : null;
90722
+ return typeof latest === "string" && RELEASE2.test(latest) ? latest : null;
90723
+ } finally {
90724
+ response.close();
90725
+ }
90726
+ } catch {
90727
+ return null;
90728
+ }
90729
+ }
90730
+ var RELEASE2 = /^\d+\.\d+\.\d+$/;
90731
+ function compareReleases(a, b) {
90732
+ const left = a.split(".").map(Number);
90733
+ const right = b.split(".").map(Number);
90734
+ for (let index = 0; index < 3; index++) {
90735
+ const difference = (left[index] ?? 0) - (right[index] ?? 0);
90736
+ if (difference) return difference;
90737
+ }
90738
+ return 0;
90739
+ }
90740
+ function notAnNpmInstall() {
90741
+ const script = process.argv[1] ?? "";
90742
+ let real2;
90743
+ try {
90744
+ real2 = realpathSync3(script);
90745
+ } catch {
90746
+ return script;
90747
+ }
90748
+ return real2.includes(`/node_modules/${PACKAGE}/`) ? null : parent(parent(real2));
90749
+ }
90750
+ function npm() {
90751
+ return which("npm") ?? join(parent(process.execPath), "npm");
90752
+ }
90753
+ var INSTALL_COMMAND = `npm install --global ${PACKAGE}`;
90754
+ function runNpm(version, background) {
90755
+ const args = ["install", "--global", `${PACKAGE}@${version}`];
90756
+ let stdio = "inherit";
90757
+ if (background) {
90758
+ mkdirSync15(logsDir(), { recursive: true });
90759
+ const log = openSync10(join(logsDir(), "update.log"), "a");
90760
+ stdio = ["ignore", log, log];
90761
+ }
90762
+ return new Promise((resolve2) => {
90763
+ const child = spawn10(npm(), args, { stdio, detached: background });
90764
+ child.once("error", () => resolve2(null));
90765
+ child.once("exit", (code2, signal) => resolve2(code2 ?? (signal ? 1 : 0)));
90766
+ if (background) child.unref();
90767
+ });
90768
+ }
90769
+ async function runUpdate(print2) {
90770
+ const elsewhere2 = notAnNpmInstall();
90771
+ if (elsewhere2 !== null) {
90772
+ print2(
90773
+ `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}`,
90774
+ process.stderr
90775
+ );
90776
+ return 1;
90777
+ }
90778
+ const latest = await latestRelease();
90779
+ if (latest === null) {
90780
+ print2("Could not reach the npm registry to check for a newer hx.", process.stderr);
90781
+ return 1;
90782
+ }
90783
+ if (compareReleases(latest, VERSION) <= 0) {
90784
+ print2(`hx ${VERSION} is up to date.`);
90785
+ return 0;
90786
+ }
90787
+ print2(`Updating hx ${VERSION} to ${latest}...`);
90788
+ const status3 = await runNpm(latest, false);
90789
+ if (status3 === null) {
90790
+ print2(
90791
+ `npm is not installed, so hx cannot update itself. See https://nodejs.org/`,
90792
+ process.stderr
90793
+ );
90794
+ return 1;
90795
+ }
90796
+ if (status3 !== 0) {
90797
+ print2(`npm could not install ${PACKAGE}@${latest} (exit ${status3}).`, process.stderr);
90798
+ return 1;
90799
+ }
90800
+ print2(`Updated hx to ${latest}.`);
90801
+ return 0;
90802
+ }
90803
+ function lastChecked() {
90804
+ try {
90805
+ const state = JSON.parse(readFileSync10(stateFile(), "utf8"));
90806
+ const at = typeof state === "object" && state !== null ? state.checkedAt : 0;
90807
+ return typeof at === "number" ? at : 0;
90808
+ } catch {
90809
+ return 0;
90810
+ }
90811
+ }
90812
+ async function autoUpdate(enabled, signal) {
90813
+ const disabled = strip(process.env[DISABLE_ENV] ?? "").toLowerCase();
90814
+ if (!enabled || ["1", "true", "yes"].includes(disabled)) return null;
90815
+ if (notAnNpmInstall() !== null) return null;
90816
+ const now3 = Date.now();
90817
+ if (now3 - lastChecked() < CHECK_INTERVAL_MS) return null;
90818
+ try {
90819
+ writeFileSync12(stateFile(), `${dumps({ checkedAt: now3 })}
90820
+ `);
90821
+ } catch {
90822
+ return null;
90823
+ }
90824
+ const latest = await latestRelease(signal);
90825
+ if (signal.aborted || latest === null || compareReleases(latest, VERSION) <= 0) return null;
90826
+ const status3 = await runNpm(latest, true);
90827
+ if (status3 === 0) return { message: `Updated hx to ${latest}. Restart hx to use it.`, ok: true };
90828
+ const why = status3 === null ? "npm could not be started" : `npm exited ${status3}`;
90829
+ return {
90830
+ message: `Could not update hx to ${latest}: ${why}. Run hx update to see why.`,
90831
+ ok: false
90832
+ };
90833
+ }
90834
+
90683
90835
  // src/cli.ts
90684
90836
  var USAGE = `hx - an agent harness for the terminal
90685
90837
 
@@ -90695,6 +90847,7 @@ Usage:
90695
90847
  hx auth [set|clear] Show or change the OpenRouter API key
90696
90848
  hx docs [SECTION|--all] Print the manual, or one section of it
90697
90849
  hx changelog [VERSION] Print what shipped in each version
90850
+ hx update Update hx to the newest release
90698
90851
  hx --version Show version
90699
90852
  hx --help Show this message
90700
90853
 
@@ -90758,6 +90911,9 @@ ${USAGE}`, process.stderr);
90758
90911
  return runMcpCommand(parsed.rest);
90759
90912
  case "trace":
90760
90913
  return runTraceCommand(parsed);
90914
+ case "update":
90915
+ case "upgrade":
90916
+ return runUpdate(print);
90761
90917
  default:
90762
90918
  return runTuiCommand(parsed);
90763
90919
  }
@@ -90816,7 +90972,7 @@ function parseArgs(args) {
90816
90972
  }
90817
90973
  if (positional.length) {
90818
90974
  const [head4, ...tail3] = positional;
90819
- if (["resume", "prompt", "trace", "mcp", "auth"].includes(head4)) {
90975
+ if (["resume", "prompt", "trace", "mcp", "auth", "update", "upgrade"].includes(head4)) {
90820
90976
  parsed.command = head4;
90821
90977
  parsed.rest = tail3;
90822
90978
  if (head4 === "resume" && tail3.length) parsed.sessionId = tail3[0];
@@ -91505,7 +91661,9 @@ async function runTuiCommand(parsed) {
91505
91661
  if (moved) runtime2.notices.push(moved);
91506
91662
  const signIn2 = runtime2.provider instanceof DeferredProvider ? signInHint(runtime2.loop.model, runtime2.auth) : null;
91507
91663
  let relaunch = null;
91664
+ const sessionOver = new AbortController();
91508
91665
  try {
91666
+ const update = autoUpdate(runtime2.settings.autoUpdates, sessionOver.signal);
91509
91667
  await runtime2.connectMcp();
91510
91668
  await runtime2.refreshModelsIfStale();
91511
91669
  relaunch = await runSession(runtime2.loop, runtime2.bus, runtime2.settings, {
@@ -91519,9 +91677,11 @@ async function runTuiCommand(parsed) {
91519
91677
  notices: runtime2.notices,
91520
91678
  checkpoints: runtime2.checkpoints,
91521
91679
  renameLeft,
91522
- signIn: signIn2
91680
+ signIn: signIn2,
91681
+ update
91523
91682
  });
91524
91683
  } finally {
91684
+ sessionOver.abort();
91525
91685
  runtime2.bus.close();
91526
91686
  await runtime2.aclose();
91527
91687
  }