@indigoai-us/hq-cli 5.171.0 → 5.172.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
@@ -2,6 +2,19 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.172.0] — 2026-09-24
6
+
7
+ ### Changed
8
+
9
+ - The `hq daemon` updater keeps hq-cloud current as well. Every 6 hours it
10
+ installs the newest hq-cloud with the same major version into
11
+ `~/.hq/daemon/hq-cloud`, and it restarts sync and the outpost heartbeat
12
+ right after an update. The daemon runs sync, and the heartbeat when that
13
+ hq-cloud has `hq-outpost-heartbeat`, from whichever copy is newer: this one
14
+ or the one bundled with the CLI.
15
+ - `hq daemon` sync pushes local changes as they happen (`--event-push`), the
16
+ same as outpost sync does today.
17
+
5
18
  ## [5.171.0] — 2026-09-24
6
19
 
7
20
  ### Fixed
@@ -16,7 +16,7 @@
16
16
  import * as fs from "node:fs";
17
17
  import * as os from "node:os";
18
18
  import * as path from "node:path";
19
- import { DEFAULT_SERVICES, SERVICE_DESCRIPTIONS, enabledServices } from "../lib/daemon/catalog.js";
19
+ import { DEFAULT_SERVICES, SERVICE_DESCRIPTIONS, enabledServices, hqCloudUnits } from "../lib/daemon/catalog.js";
20
20
  import { applyServiceToggle, readDaemonConfig, writeDaemonConfig } from "../lib/daemon/config.js";
21
21
  import { daemonUnitInstalled } from "../lib/daemon/handoff.js";
22
22
  import { adoptLegacyUnits, readAdoptedUnits, restoreLegacyUnits } from "../lib/daemon/migrate.js";
@@ -418,19 +418,23 @@ async function runMigrate(d, opts) {
418
418
  d.out("Nothing was changed. `hq daemon migrate --apply` takes them over.");
419
419
  }
420
420
  async function runServiceSelfUpdate(d) {
421
- const [{ daemonSelfUpdate }, { hostToolUpdater, runDaemonUpdater, updaterEnv }, { TOOL_NAMES }, { cliSelfUpdatable }] = await Promise.all([
421
+ const [{ daemonSelfUpdate }, { hostToolUpdater, runDaemonUpdater, updaterEnv }, { TOOL_NAMES }, { cliSelfUpdatable }, { updateManagedHqCloud },] = await Promise.all([
422
422
  import("../utils/self-update.js"),
423
423
  import("../lib/daemon/updater.js"),
424
424
  import("../lib/daemon/tools.js"),
425
425
  import("../lib/daemon/run.js"),
426
+ import("../lib/daemon/hq-cloud.js"),
426
427
  ]);
427
- const { paths } = currentHost(d);
428
+ const { paths, detected } = currentHost(d);
428
429
  const env = updaterEnv(d.env);
429
430
  const result = await runDaemonUpdater({
430
431
  // Same guard as every self-update: never replace a dev checkout or a project's copy with a global install.
431
432
  updateCli: cliSelfUpdatable() ? () => daemonSelfUpdate({ env }) : null,
432
433
  tools: TOOL_NAMES.map((name) => hostToolUpdater(name, env, d.home)),
434
+ updateHqCloud: async () => updateManagedHqCloud(paths.root, env),
435
+ hqCloudUnits: hqCloudUnits(detected.type),
433
436
  requestDaemonRestart: () => writeControlRequest(paths.requestsDir, { action: "restart-daemon" }),
437
+ requestRestart: (unit) => writeControlRequest(paths.requestsDir, { action: "restart", unit }),
434
438
  log: d.out,
435
439
  });
436
440
  if (!result.ok)
@@ -62,6 +62,8 @@ export interface PlanContext {
62
62
  hqRoot: string | undefined;
63
63
  /** hq-cloud's sync-runner.js, or null when it cannot be found. */
64
64
  syncRunnerPath: string | null;
65
+ /** hq-cloud's `hq-outpost-heartbeat` bin, when the hq-cloud in use has one. */
66
+ heartbeatRunnerPath: string | null;
65
67
  signedIn: boolean;
66
68
  /** ~/.hq-agent/kit.json exists. */
67
69
  kitConfigured: boolean;
@@ -86,6 +88,12 @@ export interface PlanContext {
86
88
  /** The box's Codex remote-control runner script, when it has one. */
87
89
  codexRunnerScript: string | null;
88
90
  }
91
+ /**
92
+ * The units that run hq-cloud's code on a machine type, restarted after the
93
+ * updater installs a new hq-cloud. Only outposts and self-hosted machines run
94
+ * the outpost heartbeat; agent machines run the agent kit's own.
95
+ */
96
+ export declare function hqCloudUnits(machineType: MachineType): ServiceName[];
89
97
  /** Written by outpost provisioning; the daemon runs it rather than re-implementing Codex login and relay setup. */
90
98
  export declare const CODEX_RUNNER_SCRIPT = "/usr/local/bin/outpost-codex-runner.sh";
91
99
  /** The outpost's Claude runner script, read for the session name it uses. */
@@ -96,6 +96,14 @@ function firstBlock(checks) {
96
96
  const hit = checks.find(([blocked]) => blocked);
97
97
  return hit ? { state: "waiting", reason: hit[1] } : undefined;
98
98
  }
99
+ /**
100
+ * The units that run hq-cloud's code on a machine type, restarted after the
101
+ * updater installs a new hq-cloud. Only outposts and self-hosted machines run
102
+ * the outpost heartbeat; agent machines run the agent kit's own.
103
+ */
104
+ export function hqCloudUnits(machineType) {
105
+ return machineType === "outpost" || machineType === "self-hosted" ? ["sync", "heartbeat"] : ["sync"];
106
+ }
99
107
  /** Written by outpost provisioning; the daemon runs it rather than re-implementing Codex login and relay setup. */
100
108
  export const CODEX_RUNNER_SCRIPT = "/usr/local/bin/outpost-codex-runner.sh";
101
109
  /** The outpost's Claude runner script, read for the session name it uses. */
@@ -113,7 +121,7 @@ function planService(service, ctx) {
113
121
  command: {
114
122
  type: "node",
115
123
  script: ctx.syncRunnerPath,
116
- args: ["--companies", "--direction", "both", "--on-conflict", "keep", "--hq-root", ctx.hqRoot ?? "", "--watch"],
124
+ args: ["--companies", "--direction", "both", "--on-conflict", "keep", "--hq-root", ctx.hqRoot ?? "", "--watch", "--event-push"],
117
125
  },
118
126
  });
119
127
  unit.blocked = firstBlock([
@@ -141,7 +149,10 @@ function planService(service, ctx) {
141
149
  }
142
150
  return [
143
151
  base(service, {
144
- command: { type: "hq", args: ["outposts", "heartbeat", "--home", ctx.home] },
152
+ // hq-cloud's own heartbeat picks up hq-cloud updates without a CLI release.
153
+ command: ctx.heartbeatRunnerPath
154
+ ? { type: "node", script: ctx.heartbeatRunnerPath, args: ["--home", ctx.home] }
155
+ : { type: "hq", args: ["outposts", "heartbeat", "--home", ctx.home] },
145
156
  blocked: firstBlock([[!ctx.signedIn, NOT_SIGNED_IN]]),
146
157
  }),
147
158
  ];
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Which hq-cloud the daemon runs sync and the outpost heartbeat from.
3
+ *
4
+ * The hq CLI ships with a copy of hq-cloud, but hq-cloud releases more often
5
+ * than the CLI. The updater keeps a newer copy of the same major version in
6
+ * `<daemon dir>/hq-cloud`, and the daemon runs whichever of the two is newer.
7
+ * A different major version is never used: it may not match this CLI.
8
+ */
9
+ export declare const HQ_CLOUD_PACKAGE = "@indigoai-us/hq-cloud";
10
+ export interface HqCloudCopy {
11
+ /** The package folder (the one holding its package.json). */
12
+ root: string;
13
+ version: string;
14
+ }
15
+ export interface PickedHqCloud extends HqCloudCopy {
16
+ source: "bundled" | "managed";
17
+ }
18
+ export declare function managedHqCloudDir(daemonRoot: string): string;
19
+ /** The package folder inside the updater's install prefix. */
20
+ export declare function managedHqCloudRoot(daemonRoot: string): string;
21
+ /**
22
+ * `usable` says whether a copy has what the daemon runs; a newer managed copy
23
+ * that fails it (an interrupted or damaged install) is passed over.
24
+ */
25
+ export declare function pickHqCloud(bundled: HqCloudCopy | null, managed: HqCloudCopy | null, usable?: (copy: HqCloudCopy) => boolean): PickedHqCloud | null;
26
+ export interface HqCloudRunners {
27
+ syncRunner: string | null;
28
+ /** `hq-outpost-heartbeat`, which older hq-cloud releases do not have. */
29
+ heartbeat: string | null;
30
+ }
31
+ export declare function hqCloudRunners(root: string, exists?: (p: string) => boolean): HqCloudRunners;
32
+ /** Install the newest release of the bundled major version, never older than the bundled copy. */
33
+ export declare function hqCloudInstallCommand(prefix: string, bundledVersion: string, platform?: NodeJS.Platform): {
34
+ file: string;
35
+ args: string[];
36
+ shell: boolean;
37
+ };
38
+ export declare function readHqCloudCopy(root: string): HqCloudCopy | null;
39
+ /** The copy bundled with this CLI. */
40
+ export declare function bundledHqCloud(): HqCloudCopy | null;
41
+ /** The hq-cloud the daemon should run right now. */
42
+ export declare function currentHqCloud(daemonRoot: string): PickedHqCloud | null;
43
+ export interface HqCloudUpdateOutcome {
44
+ ok: boolean;
45
+ from: string | null;
46
+ to: string | null;
47
+ detail?: string;
48
+ }
49
+ /** Install or update the updater's copy, as this (the login) user. */
50
+ export declare function updateManagedHqCloud(daemonRoot: string, env: NodeJS.ProcessEnv): HqCloudUpdateOutcome;
51
+ //# sourceMappingURL=hq-cloud.d.ts.map
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Which hq-cloud the daemon runs sync and the outpost heartbeat from.
3
+ *
4
+ * The hq CLI ships with a copy of hq-cloud, but hq-cloud releases more often
5
+ * than the CLI. The updater keeps a newer copy of the same major version in
6
+ * `<daemon dir>/hq-cloud`, and the daemon runs whichever of the two is newer.
7
+ * A different major version is never used: it may not match this CLI.
8
+ */
9
+ import { spawnSync } from "node:child_process";
10
+ import * as fs from "node:fs";
11
+ import { createRequire } from "node:module";
12
+ import * as path from "node:path";
13
+ import { buildSpawnPlan } from "../../utils/windows-spawn.js";
14
+ export const HQ_CLOUD_PACKAGE = "@indigoai-us/hq-cloud";
15
+ export function managedHqCloudDir(daemonRoot) {
16
+ return path.join(daemonRoot, "hq-cloud");
17
+ }
18
+ /** The package folder inside the updater's install prefix. */
19
+ export function managedHqCloudRoot(daemonRoot) {
20
+ return path.join(managedHqCloudDir(daemonRoot), "node_modules", ...HQ_CLOUD_PACKAGE.split("/"));
21
+ }
22
+ function parseVersion(v) {
23
+ const m = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(v.trim());
24
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
25
+ }
26
+ function newer(a, b) {
27
+ for (let i = 0; i < 3; i++) {
28
+ if (a[i] !== b[i])
29
+ return a[i] > b[i];
30
+ }
31
+ return false;
32
+ }
33
+ /**
34
+ * `usable` says whether a copy has what the daemon runs; a newer managed copy
35
+ * that fails it (an interrupted or damaged install) is passed over.
36
+ */
37
+ export function pickHqCloud(bundled, managed, usable = () => true) {
38
+ if (!bundled)
39
+ return null;
40
+ const b = parseVersion(bundled.version);
41
+ const m = managed ? parseVersion(managed.version) : null;
42
+ if (managed && b && m && m[0] === b[0] && newer(m, b) && usable(managed))
43
+ return { ...managed, source: "managed" };
44
+ return { ...bundled, source: "bundled" };
45
+ }
46
+ export function hqCloudRunners(root, exists = fs.existsSync) {
47
+ const bin = (name) => {
48
+ const p = path.join(root, "dist", "bin", name);
49
+ return exists(p) ? p : null;
50
+ };
51
+ return { syncRunner: bin("sync-runner.js"), heartbeat: bin("outpost-heartbeat.js") };
52
+ }
53
+ /** Install the newest release of the bundled major version, never older than the bundled copy. */
54
+ export function hqCloudInstallCommand(prefix, bundledVersion, platform = process.platform) {
55
+ // npm is a .cmd on Windows and needs the shell, so every argument is quoted for it.
56
+ const plan = buildSpawnPlan("npm", ["install", "--prefix", prefix, "--no-audit", "--no-fund", "--omit=dev", `${HQ_CLOUD_PACKAGE}@^${bundledVersion}`], platform);
57
+ return { file: plan.cmd, args: plan.args, shell: plan.shell };
58
+ }
59
+ export function readHqCloudCopy(root) {
60
+ try {
61
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
62
+ return typeof pkg.version === "string" ? { root, version: pkg.version } : null;
63
+ }
64
+ catch {
65
+ return null;
66
+ }
67
+ }
68
+ /** The copy bundled with this CLI. */
69
+ export function bundledHqCloud() {
70
+ try {
71
+ const require = createRequire(import.meta.url);
72
+ return readHqCloudCopy(path.dirname(require.resolve(`${HQ_CLOUD_PACKAGE}/package.json`)));
73
+ }
74
+ catch {
75
+ return null;
76
+ }
77
+ }
78
+ /** The hq-cloud the daemon should run right now. */
79
+ export function currentHqCloud(daemonRoot) {
80
+ return pickHqCloud(bundledHqCloud(), readHqCloudCopy(managedHqCloudRoot(daemonRoot)), (copy) => hqCloudRunners(copy.root).syncRunner !== null);
81
+ }
82
+ /** Install or update the updater's copy, as this (the login) user. */
83
+ export function updateManagedHqCloud(daemonRoot, env) {
84
+ const bundled = bundledHqCloud();
85
+ const from = currentHqCloud(daemonRoot)?.version ?? null;
86
+ if (!bundled)
87
+ return { ok: false, from, to: null, detail: "this CLI install has no bundled hq-cloud to match" };
88
+ const prefix = managedHqCloudDir(daemonRoot);
89
+ try {
90
+ fs.mkdirSync(prefix, { recursive: true, mode: 0o700 });
91
+ }
92
+ catch (err) {
93
+ return { ok: false, from, to: null, detail: `could not create ${prefix}: ${err instanceof Error ? err.message : String(err)}` };
94
+ }
95
+ const cmd = hqCloudInstallCommand(prefix, bundled.version);
96
+ const r = spawnSync(cmd.file, cmd.args, {
97
+ encoding: "utf8",
98
+ env,
99
+ timeout: 10 * 60_000,
100
+ stdio: ["ignore", "pipe", "pipe"],
101
+ windowsHide: true,
102
+ shell: cmd.shell,
103
+ });
104
+ if (r.status !== 0) {
105
+ const tail = `${r.stderr ?? ""}\n${r.stdout ?? ""}`.trim().split("\n").slice(-3).join(" ").slice(0, 400);
106
+ return { ok: false, from, to: null, detail: tail || (r.error ? r.error.message : `npm exited ${r.status ?? "by signal"}`) };
107
+ }
108
+ return { ok: true, from, to: currentHqCloud(daemonRoot)?.version ?? null };
109
+ }
110
+ //# sourceMappingURL=hq-cloud.js.map
@@ -23,6 +23,7 @@ import { daemonRootRefusal } from "./install.js";
23
23
  import { detectMachineType } from "./machine-type.js";
24
24
  import { appendPrivate, daemonPaths, ensurePrivateDir, openPrivateAppend } from "./paths.js";
25
25
  import { takeControlRequests } from "./requests.js";
26
+ import { currentHqCloud, hqCloudRunners } from "./hq-cloud.js";
26
27
  import { runnerSessionName } from "./remote-control.js";
27
28
  import { STOP_ESCALATION_MS, createSupervisor } from "./supervisor.js";
28
29
  import { userToolDirs } from "./tools.js";
@@ -230,8 +231,7 @@ export async function runDaemon(opts) {
230
231
  return 1;
231
232
  }
232
233
  log("info", `starting hq daemon (pid ${process.pid}) as ${machineType} (${detected.detail})`);
233
- const syncRunnerPath = resolveSyncRunnerPath();
234
- if (!syncRunnerPath)
234
+ if (!resolveSyncRunnerPath())
235
235
  log("warn", "the hq-cloud sync runner was not found in this CLI install; sync is unavailable");
236
236
  const updatable = cliSelfUpdatable();
237
237
  const hostName = os.hostname().split(".")[0] || "hq-host";
@@ -247,12 +247,16 @@ export async function runDaemon(opts) {
247
247
  const legacy = legacyUnits();
248
248
  const configured = config.hqRoot ?? env.HQ_ROOT?.trim() ?? undefined;
249
249
  const hqRoot = configured && fs.existsSync(configured) ? configured : undefined;
250
+ // Re-picked every reconcile, so a newer hq-cloud from the updater is used as soon as it lands.
251
+ const hqCloud = currentHqCloud(paths.root);
252
+ const runners = hqCloud ? hqCloudRunners(hqCloud.root) : { syncRunner: null, heartbeat: null };
250
253
  return planUnits(enabledServices(machineType, config), {
251
254
  machineType,
252
255
  platform,
253
256
  home,
254
257
  hqRoot,
255
- syncRunnerPath,
258
+ syncRunnerPath: runners.syncRunner,
259
+ heartbeatRunnerPath: runners.heartbeat,
256
260
  signedIn: signedIn(env, home, log),
257
261
  kitConfigured: fs.existsSync(agentKitPaths(home, env).kitConfigPath),
258
262
  desktopAppManagesSync: machineType === "desktop" &&
@@ -8,11 +8,15 @@
8
8
  * 2. Update the Claude, Codex and Grok CLIs the login user installed, each
9
9
  * through its own update route. A copy this user cannot write (root's
10
10
  * under /usr) is reported and left alone.
11
- * 3. After a CLI update, ask the daemon to restart itself so the supervisor and
11
+ * 3. Install or update the daemon's own hq-cloud (sync and the outpost
12
+ * heartbeat run from it when it is newer than the CLI's bundled copy), and
13
+ * restart sync and the heartbeat right after it changes.
14
+ * 4. After a CLI update, ask the daemon to restart itself so the supervisor and
12
15
  * every service run the new version. The remote-control hosts notice an
13
16
  * updated Claude or Codex on disk themselves and restart once idle.
14
17
  */
15
18
  import type { SelfUpdateOutcome } from "../../utils/self-update.js";
19
+ import type { HqCloudUpdateOutcome } from "./hq-cloud.js";
16
20
  import { type ToolInstall, type ToolName } from "./tools.js";
17
21
  export type { ToolInstall } from "./tools.js";
18
22
  export interface ToolUpdater {
@@ -27,7 +31,13 @@ export interface UpdaterDeps {
27
31
  /** Null when this CLI is not a global install the updater may replace. */
28
32
  updateCli: (() => Promise<Pick<SelfUpdateOutcome, "action" | "latest">>) | null;
29
33
  tools: readonly ToolUpdater[];
34
+ /** Install or update the daemon's own hq-cloud; null where it has none. */
35
+ updateHqCloud: (() => Promise<HqCloudUpdateOutcome>) | null;
36
+ /** The units on this machine that run hq-cloud code, restarted after it updates. */
37
+ hqCloudUnits: readonly string[];
30
38
  requestDaemonRestart: () => void;
39
+ /** Restart one unit (a restart request to the running daemon). */
40
+ requestRestart: (unit: string) => void;
31
41
  log: (line: string) => void;
32
42
  }
33
43
  export interface UpdaterResult {
@@ -8,7 +8,10 @@
8
8
  * 2. Update the Claude, Codex and Grok CLIs the login user installed, each
9
9
  * through its own update route. A copy this user cannot write (root's
10
10
  * under /usr) is reported and left alone.
11
- * 3. After a CLI update, ask the daemon to restart itself so the supervisor and
11
+ * 3. Install or update the daemon's own hq-cloud (sync and the outpost
12
+ * heartbeat run from it when it is newer than the CLI's bundled copy), and
13
+ * restart sync and the heartbeat right after it changes.
14
+ * 4. After a CLI update, ask the daemon to restart itself so the supervisor and
12
15
  * every service run the new version. The remote-control hosts notice an
13
16
  * updated Claude or Codex on disk themselves and restart once idle.
14
17
  */
@@ -39,6 +42,26 @@ export async function runDaemonUpdater(deps) {
39
42
  deps.log(`hq CLI: ${cli.action === "current" ? "up to date" : cli.action}`);
40
43
  }
41
44
  }
45
+ if (deps.updateHqCloud) {
46
+ const cloud = await deps.updateHqCloud().catch((err) => ({
47
+ ok: false,
48
+ from: null,
49
+ to: null,
50
+ detail: err instanceof Error ? err.message : String(err),
51
+ }));
52
+ if (!cloud.ok) {
53
+ deps.log(`hq-cloud update failed: ${cloud.detail ?? "unknown error"}`);
54
+ ok = false;
55
+ }
56
+ else if (cloud.to && cloud.to !== cloud.from) {
57
+ deps.log(`hq-cloud updated ${cloud.from ?? "(none)"} -> ${cloud.to}; restarting ${deps.hqCloudUnits.join(" and ")} onto it`);
58
+ for (const unit of deps.hqCloudUnits)
59
+ deps.requestRestart(unit);
60
+ }
61
+ else {
62
+ deps.log(`hq-cloud ${cloud.to ?? cloud.from ?? "unknown version"}: up to date`);
63
+ }
64
+ }
42
65
  const updated = [];
43
66
  for (const tool of deps.tools) {
44
67
  const outcome = updateTool(tool, deps.log);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.171.0",
3
+ "version": "5.172.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {