@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 +18 -0
- package/dist/commands/agents.js +39 -6
- package/dist/lib/agent-kit/fallback.d.ts +0 -14
- package/dist/lib/agent-kit/fallback.js +15 -0
- package/package.json +1 -1
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
|
package/dist/commands/agents.js
CHANGED
|
@@ -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(
|
|
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
|
|
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);
|