@indigoai-us/hq-cli 5.119.5 → 5.119.7

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,24 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.119.7] — 2026-09-17
6
+
7
+ ### Fixed
8
+
9
+ - When the kit cannot install its `@reboot` entry because the host has no
10
+ `crontab` at all, the install now says so and names the package to install,
11
+ instead of reporting a raw ENOENT the operator cannot act on. A minimal
12
+ container image routinely ships without cron; the Muse pilot hit this the
13
+ first time the reboot entry was attempted.
14
+
15
+ ## [5.119.6] — 2026-09-17
16
+
17
+ ### Fixed
18
+
19
+ - Slack token paste links now use the company's canonical Console slug. Links
20
+ built with an internal company ID redirected to the company home and lost the
21
+ agent setup form.
22
+
5
23
  ## [5.119.5] — 2026-09-17
6
24
 
7
25
  ### Fixed
@@ -443,7 +443,7 @@ function currentDeviceSignInAction(status) {
443
443
  code,
444
444
  };
445
445
  }
446
- function pendingSlackAction(status) {
446
+ function pendingSlackAction(status, companySlug) {
447
447
  const agent = recordValue(status.agent);
448
448
  const configuredSlack = recordValue(recordValue(agent?.channels)?.slack);
449
449
  const appId = nonEmptyString(configuredSlack?.appId);
@@ -455,6 +455,9 @@ function pendingSlackAction(status) {
455
455
  new URL(tokenPage).hostname === "api.slack.com" &&
456
456
  agentUid && AGENT_UID_PATTERN.test(agentUid) &&
457
457
  companyUid && /^cmp_[A-Za-z0-9_-]+$/.test(companyUid)) {
458
+ const companyUrlKey = companySlug && /^[a-z0-9][a-z0-9-]*$/.test(companySlug)
459
+ ? companySlug
460
+ : companyUid;
458
461
  return {
459
462
  type: "slack-app-token",
460
463
  title: `Finish ${nonEmptyString(agent?.name) ?? "the agent"}'s Slack connection`,
@@ -462,7 +465,7 @@ function pendingSlackAction(status) {
462
465
  instruction: "Create an app-level token with the connections:write scope, then paste it into HQ. HQ verifies the token and resumes setup; if Slack asks you to install the app, follow the install link shown there.",
463
466
  url: `https://api.slack.com/apps/${appId}/general`,
464
467
  urlLabel: "Get token",
465
- pasteUrl: `https://hq.computer/companies/${encodeURIComponent(companyUid)}/agents?setup=${encodeURIComponent(agentUid)}`,
468
+ pasteUrl: `https://hq.computer/companies/${encodeURIComponent(companyUrlKey)}/agents?setup=${encodeURIComponent(agentUid)}`,
466
469
  };
467
470
  }
468
471
  const diagnostics = recordValue(agent?.channelDiagnostics);
@@ -496,12 +499,40 @@ function pendingSlackAction(status) {
496
499
  * status fields into a list so another operator action can be added without
497
500
  * changing the renderers or command control flow.
498
501
  */
499
- function pendingOperatorActions(status) {
502
+ function pendingOperatorActions(status, companySlug) {
500
503
  return [
501
504
  currentDeviceSignInAction(status),
502
- pendingSlackAction(status),
505
+ pendingSlackAction(status, companySlug),
503
506
  ].filter((action) => action !== null);
504
507
  }
508
+ async function consoleCompanySlug(token, status, companyRef) {
509
+ if (companyRef && /^[a-z0-9][a-z0-9-]*$/.test(companyRef))
510
+ return companyRef;
511
+ const agent = recordValue(status.agent);
512
+ const slack = recordValue(recordValue(agent?.channels)?.slack);
513
+ if (!slack?.appTokenPendingUrl)
514
+ return undefined;
515
+ const companyUid = nonEmptyString(agent?.companyUid);
516
+ if (!companyUid)
517
+ return undefined;
518
+ try {
519
+ const response = await vaultApiFetch({
520
+ token,
521
+ path: "/membership/me",
522
+ signal: AbortSignal.timeout(10_000),
523
+ });
524
+ if (!response.ok)
525
+ return undefined;
526
+ const body = recordValue(await response.json());
527
+ const memberships = Array.isArray(body?.memberships) ? body.memberships : [];
528
+ const membership = memberships.map(recordValue).find((item) => item?.companyUid === companyUid && item?.status === "active");
529
+ const slug = nonEmptyString(membership?.companySlug);
530
+ return slug && /^[a-z0-9][a-z0-9-]*$/.test(slug) ? slug : undefined;
531
+ }
532
+ catch {
533
+ return undefined;
534
+ }
535
+ }
505
536
  function pendingActionsJson(actions) {
506
537
  return actions.map(({ type, title, instruction, url, pasteUrl, code }) => ({
507
538
  type,
@@ -1190,7 +1221,9 @@ export function registerAgentsCommand(program) {
1190
1221
  const status = identity
1191
1222
  ? await getAgentStatus(token, identity.uid)
1192
1223
  : null;
1193
- const actions = status ? pendingOperatorActions(status) : [];
1224
+ const actions = status
1225
+ ? pendingOperatorActions(status, await consoleCompanySlug(token, status, companyOf(this)))
1226
+ : [];
1194
1227
  const slackInstall = actions.find((action) => action.type === "slack-install");
1195
1228
  if (opts.json) {
1196
1229
  console.log(JSON.stringify({
@@ -1311,7 +1344,7 @@ export function registerAgentsCommand(program) {
1311
1344
  const token = (await resolveVaultCredential()).token;
1312
1345
  const agentUid = await resolveAgentUid(token, agent, companyOf(this));
1313
1346
  const status = await getAgentStatus(token, agentUid);
1314
- const actions = pendingOperatorActions(status);
1347
+ const actions = pendingOperatorActions(status, await consoleCompanySlug(token, status, companyOf(this)));
1315
1348
  if (opts.json) {
1316
1349
  process.stdout.write(JSON.stringify({
1317
1350
  ...status,
@@ -80,19 +80,5 @@ export interface RebootCronResult {
80
80
  }
81
81
  /** Drop any previously managed block, leaving every other entry untouched. */
82
82
  export declare function stripManagedCron(existing: string): string;
83
- /**
84
- * Install the @reboot entry that brings the fallback supervisor back after a
85
- * restart.
86
- *
87
- * Printing the line and trusting it to be pasted is what the installer used to
88
- * do, and it is how the Muse pilot lost its messages: the host rebooted, the
89
- * supervisor died with it, nothing restarted it, and the bot went quiet while
90
- * every surface still reported healthy. A supervisor that does not survive a
91
- * reboot is not a supervisor, so the entry is now written for real.
92
- *
93
- * Failure is NOT fatal — a host with no crontab is still a working kit until
94
- * it restarts — but it is reported, so the caller can tell the operator the
95
- * one thing they must then do by hand.
96
- */
97
83
  export declare function ensureRebootCrontab(paths: Pick<AgentKitPaths, "agentDir" | "logsDir">, host: ServiceHostPaths, run?: CronRunner): RebootCronResult;
98
84
  //# sourceMappingURL=fallback.d.ts.map
@@ -161,6 +161,10 @@ export function stripManagedCron(existing) {
161
161
  * it restarts — but it is reported, so the caller can tell the operator the
162
162
  * one thing they must then do by hand.
163
163
  */
164
+ /** No cron on the host: spawn failed outright, or the shell could not find it. */
165
+ function isMissingCrontab(r) {
166
+ return r.status === 127 || /\bENOENT\b|command not found|not found/i.test(r.stderr);
167
+ }
164
168
  export function ensureRebootCrontab(paths, host, run) {
165
169
  const exec = run ??
166
170
  ((args, input) => {
@@ -170,6 +174,17 @@ export function ensureRebootCrontab(paths, host, run) {
170
174
  return { status: r.status ?? 1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
171
175
  });
172
176
  const listed = exec(["-l"]);
177
+ // A minimal host image often ships without cron at all, which is not an
178
+ // error the operator can act on unless it is named. Observed on the Muse
179
+ // pilot: the reboot entry could not be written because the cron package was
180
+ // simply not installed, and the raw ENOENT did not say so.
181
+ if (isMissingCrontab(listed)) {
182
+ return {
183
+ installed: false,
184
+ reason: "the `crontab` command is not installed on this host (install your distribution's cron package, " +
185
+ "for example `apt-get install -y cron` or `apk add busybox-cron`), then run `hq agent kit install` again",
186
+ };
187
+ }
173
188
  // An empty crontab exits non-zero with "no crontab for <user>"; that is not
174
189
  // a failure, it just means there is nothing to preserve. Anything else is.
175
190
  const noCrontabYet = listed.status !== 0 && /no crontab for/i.test(listed.stderr);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.119.5",
3
+ "version": "5.119.7",
4
4
  "description": "HQ by Indigo management CLI \u2014 modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {