@indigoai-us/hq-cli 5.103.33 → 5.104.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 +15 -0
- package/dist/commands/agents.d.ts +14 -0
- package/dist/commands/agents.js +91 -0
- package/dist/commands/skill.js +22 -5
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.104.0] — 2026-08-31
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `hq agents terminal <agent>` — open an interactive terminal (real TTY over
|
|
10
|
+
SSM Session Manager) on a fleet agent's box using HQ identity alone: no SSH,
|
|
11
|
+
no local AWS credentials. Owner/admin only; auto-starts a dormant box and
|
|
12
|
+
waits for it to come online. `--forward <port>` (with optional
|
|
13
|
+
`--local-port`) tunnels a port on the box to your machine instead of opening
|
|
14
|
+
a shell. Requires the AWS `session-manager-plugin` installed locally.
|
|
15
|
+
- `hq outposts terminal` — the same interactive terminal for personal
|
|
16
|
+
Outposts (via `@indigoai-us/hq-cloud`).
|
|
17
|
+
|
|
18
|
+
## [5.103.34] — 2026-08-30
|
|
19
|
+
|
|
5
20
|
## [5.103.33] — 2026-08-30
|
|
6
21
|
|
|
7
22
|
## [5.103.32] — 2026-08-29
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* the caller's single active membership (same as `members.ts`).
|
|
26
26
|
*/
|
|
27
27
|
import { Command } from "commander";
|
|
28
|
+
import { type TerminalSessionPayload } from "@indigoai-us/hq-cloud/outposts/node";
|
|
28
29
|
import { type BillingErrorPayload } from "../utils/billing-gate.js";
|
|
29
30
|
/** Reasoning-effort values hq-pro accepts on `runtime-config`. */
|
|
30
31
|
export declare const VALID_EFFORTS: Set<string>;
|
|
@@ -201,6 +202,19 @@ export declare function startStopAgent(token: string, agentUid: string, action:
|
|
|
201
202
|
uid: string;
|
|
202
203
|
runtime?: Record<string, unknown>;
|
|
203
204
|
}>;
|
|
205
|
+
/** `POST /v1/agents/{uid}/terminal` wire shape: vended session or 202 starting. */
|
|
206
|
+
export type AgentTerminalResponse = ({
|
|
207
|
+
ok: true;
|
|
208
|
+
uid: string;
|
|
209
|
+
mode: "shell" | "port-forward";
|
|
210
|
+
} & TerminalSessionPayload) | {
|
|
211
|
+
ok: false;
|
|
212
|
+
step: "starting";
|
|
213
|
+
state: "starting-instance" | "awaiting-ssm";
|
|
214
|
+
retryAfterSeconds?: number;
|
|
215
|
+
uid: string;
|
|
216
|
+
};
|
|
217
|
+
export declare function openAgentTerminal(token: string, agentUid: string, body: Record<string, unknown>): Promise<AgentTerminalResponse>;
|
|
204
218
|
export declare function retryAgent(token: string, agentUid: string): Promise<Record<string, unknown>>;
|
|
205
219
|
export declare function deprovisionAgent(token: string, agentUid: string): Promise<{
|
|
206
220
|
uid: string;
|
package/dist/commands/agents.js
CHANGED
|
@@ -30,6 +30,7 @@ import * as readline from "node:readline";
|
|
|
30
30
|
import { resolveVaultCredential } from "../utils/resolve-vault-credential.js";
|
|
31
31
|
import { gateApiKeyCapabilities } from "../utils/api-key-command-gate.js";
|
|
32
32
|
import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
|
|
33
|
+
import { launchSessionManagerPlugin, SessionManagerPluginLaunchError, TerminalSessionTimeoutError, waitForTerminalSession, } from "@indigoai-us/hq-cloud/outposts/node";
|
|
33
34
|
import { peekIdToken } from "../utils/id-token.js";
|
|
34
35
|
import { isPlanGateError } from "../utils/plan-gate-error.js";
|
|
35
36
|
import { confirmChargeOrExit, formatUsd, parseBillingPayload, surfaceBillingBlocked, } from "../utils/billing-gate.js";
|
|
@@ -299,6 +300,14 @@ export async function startStopAgent(token, agentUid, action) {
|
|
|
299
300
|
method: "POST",
|
|
300
301
|
});
|
|
301
302
|
}
|
|
303
|
+
export async function openAgentTerminal(token, agentUid, body) {
|
|
304
|
+
return agentsRequest({
|
|
305
|
+
token,
|
|
306
|
+
path: `/v1/agents/${encodeURIComponent(agentUid)}/terminal`,
|
|
307
|
+
method: "POST",
|
|
308
|
+
body,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
302
311
|
export async function retryAgent(token, agentUid) {
|
|
303
312
|
return agentsRequest({
|
|
304
313
|
token,
|
|
@@ -540,6 +549,17 @@ function sleep(ms) {
|
|
|
540
549
|
// ---------------------------------------------------------------------------
|
|
541
550
|
// Command registration
|
|
542
551
|
// ---------------------------------------------------------------------------
|
|
552
|
+
/** Parse an optional port flag: integer 1..65535 or exit(1) with the flag name. */
|
|
553
|
+
function parsePortOption(value, flag) {
|
|
554
|
+
if (value === undefined)
|
|
555
|
+
return undefined;
|
|
556
|
+
const n = Number(value);
|
|
557
|
+
if (!Number.isInteger(n) || n < 1 || n > 65_535) {
|
|
558
|
+
console.error(chalk.red(`${flag} must be an integer between 1 and 65535.`));
|
|
559
|
+
process.exit(1);
|
|
560
|
+
}
|
|
561
|
+
return n;
|
|
562
|
+
}
|
|
543
563
|
function fail(err) {
|
|
544
564
|
// Let the CLI boundary render and offer the one shared Team checkout flow.
|
|
545
565
|
// Local exits would otherwise prevent interactive plan-limit remediation.
|
|
@@ -578,6 +598,9 @@ export function registerAgentsCommand(program) {
|
|
|
578
598
|
bySubcommand: {
|
|
579
599
|
message: { capability: "agents:use", routeAvailable: false },
|
|
580
600
|
thread: { capability: "agents:use", routeAvailable: false },
|
|
601
|
+
// Interactive terminals are for humans: hq-pro serves no keyed
|
|
602
|
+
// `/v1/keys/agents/{uid}/terminal` route, so machine keys fail closed.
|
|
603
|
+
terminal: { capability: "agents:use", routeAvailable: false },
|
|
581
604
|
},
|
|
582
605
|
});
|
|
583
606
|
// `--company` may live on the group or the subcommand; the subcommand value
|
|
@@ -667,6 +690,74 @@ export function registerAgentsCommand(program) {
|
|
|
667
690
|
fail(err);
|
|
668
691
|
}
|
|
669
692
|
});
|
|
693
|
+
agents
|
|
694
|
+
.command("terminal <agent>")
|
|
695
|
+
.description("Open an interactive terminal on an agent's box — a real TTY over SSM Session Manager (no SSH, no local AWS credentials). " +
|
|
696
|
+
"Owner/admin only; requires the AWS session-manager-plugin installed locally. With --forward, tunnels a port on the box instead.")
|
|
697
|
+
.option("--company <slug>", "Company slug (to resolve an agent by name/slug)")
|
|
698
|
+
.option("--forward <port>", "Forward this port on the box to your machine instead of opening a shell")
|
|
699
|
+
.option("--local-port <port>", "Local port for --forward (defaults to the forwarded port)")
|
|
700
|
+
.action(async function (agent, opts) {
|
|
701
|
+
try {
|
|
702
|
+
const forwardPort = parsePortOption(opts.forward, "--forward");
|
|
703
|
+
const localPort = parsePortOption(opts.localPort, "--local-port");
|
|
704
|
+
if (localPort !== undefined && forwardPort === undefined) {
|
|
705
|
+
console.error(chalk.red("--local-port requires --forward <port>."));
|
|
706
|
+
process.exit(1);
|
|
707
|
+
}
|
|
708
|
+
// A shell needs a real TTY on both ends; a port-forward does not.
|
|
709
|
+
if (forwardPort === undefined && !process.stdin.isTTY) {
|
|
710
|
+
console.error(chalk.red("hq agents terminal needs an interactive terminal (stdin is not a TTY)."));
|
|
711
|
+
process.exit(1);
|
|
712
|
+
}
|
|
713
|
+
const token = (await resolveVaultCredential()).token;
|
|
714
|
+
const agentUid = await resolveAgentUid(token, agent, companyOf(this));
|
|
715
|
+
const body = forwardPort !== undefined
|
|
716
|
+
? {
|
|
717
|
+
mode: "port-forward",
|
|
718
|
+
portNumber: forwardPort,
|
|
719
|
+
...(localPort !== undefined ? { localPortNumber: localPort } : {}),
|
|
720
|
+
}
|
|
721
|
+
: { mode: "shell" };
|
|
722
|
+
// 202-retry loop: a dormant box is auto-started server-side; SSM
|
|
723
|
+
// registration after a cold start takes ~30-90s. Status lines go to
|
|
724
|
+
// stderr (once per state) so stdout stays the session's.
|
|
725
|
+
let lastState;
|
|
726
|
+
const session = await waitForTerminalSession(async () => {
|
|
727
|
+
const result = await openAgentTerminal(token, agentUid, body);
|
|
728
|
+
if (result.ok)
|
|
729
|
+
return { kind: "ready", session: result };
|
|
730
|
+
return {
|
|
731
|
+
kind: "starting",
|
|
732
|
+
state: result.state,
|
|
733
|
+
...(result.retryAfterSeconds !== undefined
|
|
734
|
+
? { retryAfterSeconds: result.retryAfterSeconds }
|
|
735
|
+
: {}),
|
|
736
|
+
};
|
|
737
|
+
}, {
|
|
738
|
+
onStatus: (state) => {
|
|
739
|
+
if (state === lastState)
|
|
740
|
+
return;
|
|
741
|
+
lastState = state;
|
|
742
|
+
console.error(chalk.dim(state === "starting-instance"
|
|
743
|
+
? `${agent}'s box is starting — waiting for it to come online (Ctrl-C to cancel)…`
|
|
744
|
+
: `${agent}'s box is up — waiting for its agent to register (Ctrl-C to cancel)…`));
|
|
745
|
+
},
|
|
746
|
+
});
|
|
747
|
+
console.error(chalk.dim(forwardPort !== undefined
|
|
748
|
+
? `Forwarding box port ${forwardPort} to localhost:${localPort ?? forwardPort} — Ctrl-C to end.`
|
|
749
|
+
: `Connected to ${agent}'s box — type 'exit' or Ctrl-D to leave.`));
|
|
750
|
+
process.exitCode = await launchSessionManagerPlugin(session);
|
|
751
|
+
}
|
|
752
|
+
catch (err) {
|
|
753
|
+
if (err instanceof SessionManagerPluginLaunchError ||
|
|
754
|
+
err instanceof TerminalSessionTimeoutError) {
|
|
755
|
+
console.error(chalk.red(err.message));
|
|
756
|
+
process.exit(1);
|
|
757
|
+
}
|
|
758
|
+
fail(err);
|
|
759
|
+
}
|
|
760
|
+
});
|
|
670
761
|
agents
|
|
671
762
|
.command("provision <name>")
|
|
672
763
|
.alias("new")
|
package/dist/commands/skill.js
CHANGED
|
@@ -136,11 +136,17 @@ export function resolveSkillUid(target, cwd) {
|
|
|
136
136
|
return uid;
|
|
137
137
|
}
|
|
138
138
|
export function canonicalCompanySkillPath(hqRoot, companySlug, skillSlug) {
|
|
139
|
+
// HQ-CLI-17 (Sentry 7699881899): both validators reject CALLER-supplied input
|
|
140
|
+
// — the `--company` slug and the `create <slug>` argument. A malformed value is
|
|
141
|
+
// the caller's request, not an hq-cli defect, so throw `localSkillError`
|
|
142
|
+
// (expected: true) and let the top-level boundary print the remedy and skip
|
|
143
|
+
// Sentry, exactly as the sibling caller-input throws in this module already do.
|
|
144
|
+
// The message text is unchanged; only its CLASS changes.
|
|
139
145
|
if (!COMPANY_SLUG_PATTERN.test(companySlug)) {
|
|
140
|
-
throw
|
|
146
|
+
throw localSkillError("Company slug must start with a lowercase letter or number and contain only lowercase letters, numbers, and hyphens.");
|
|
141
147
|
}
|
|
142
148
|
if (!SKILL_SLUG_PATTERN.test(skillSlug)) {
|
|
143
|
-
throw
|
|
149
|
+
throw localSkillError("Skill slug must start with a lowercase letter or number and contain only lowercase letters, numbers, and hyphens.");
|
|
144
150
|
}
|
|
145
151
|
return path.join(hqRoot, "companies", companySlug, "skills", skillSlug, "SKILL.md");
|
|
146
152
|
}
|
|
@@ -277,7 +283,10 @@ export function registerSkillCommand(program, deps = {}) {
|
|
|
277
283
|
.option("--surface-only", "Refresh local skill discovery without registration or sync")
|
|
278
284
|
.action(async (slug, opts) => {
|
|
279
285
|
if (!SKILL_SLUG_PATTERN.test(slug)) {
|
|
280
|
-
|
|
286
|
+
// HQ-CLI-17: the `create <slug>` argument is caller input; a malformed
|
|
287
|
+
// slug is a correctly-enforced refusal, not a crash. Mark it expected
|
|
288
|
+
// (via localSkillError) so it is printed and skipped for Sentry.
|
|
289
|
+
throw localSkillError("Skill slug must start with a lowercase letter or number and contain only lowercase letters, numbers, and hyphens.");
|
|
281
290
|
}
|
|
282
291
|
const parentOpts = skill.opts();
|
|
283
292
|
const resolvedRoot = path.resolve(parentOpts.hqRoot ?? hqRoot);
|
|
@@ -285,7 +294,13 @@ export function registerSkillCommand(program, deps = {}) {
|
|
|
285
294
|
const filePath = canonicalCompanySkillPath(resolvedRoot, companySlug, slug);
|
|
286
295
|
const legacyPrefix = readCompanyPrefix(resolvedRoot, companySlug);
|
|
287
296
|
if (fs.existsSync(filePath) && !fs.statSync(filePath).isFile()) {
|
|
288
|
-
|
|
297
|
+
// HQ-CLI-17: the target path is derived from caller input and its
|
|
298
|
+
// on-disk shape is the caller's own filesystem, not an hq-cli defect.
|
|
299
|
+
// The bare Error also embedded this absolute (home-relative) path, so it
|
|
300
|
+
// minted a NEW Sentry fingerprint per machine. Mark it expected so it is
|
|
301
|
+
// printed (with the path the caller needs) and never captured. Redaction
|
|
302
|
+
// in localSkillError scrubs credentials but leaves plain paths intact.
|
|
303
|
+
throw localSkillError(`Expected a SKILL.md file at '${filePath}'.`);
|
|
289
304
|
}
|
|
290
305
|
if (opts.surfaceOnly === true) {
|
|
291
306
|
// Discovery wrappers are execution surfaces. Never create one for an
|
|
@@ -396,8 +411,10 @@ export function registerSkillCommand(program, deps = {}) {
|
|
|
396
411
|
.requiredOption("-m, --message <text>", "The improvement to discuss")
|
|
397
412
|
.action(async (target, opts) => {
|
|
398
413
|
const message = opts.message.trim();
|
|
414
|
+
// HQ-CLI-17: a blank `-m/--message` is caller input, not an hq-cli defect.
|
|
415
|
+
// Mark it expected so it is printed and skipped for Sentry.
|
|
399
416
|
if (!message)
|
|
400
|
-
throw
|
|
417
|
+
throw localSkillError("An improvement message is required.");
|
|
401
418
|
const parentOpts = skill.opts();
|
|
402
419
|
const resolvedRoot = path.resolve(parentOpts.hqRoot ?? hqRoot);
|
|
403
420
|
const companySlug = resolveCompanySlug(parentOpts.company, resolvedRoot);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indigoai-us/hq-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.104.0",
|
|
4
4
|
"description": "HQ by Indigo management CLI — modules and cloud sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@aws-sdk/client-iot-data-plane": "^3.1096.0",
|
|
32
32
|
"@aws-sdk/client-s3": "^3.1049.0",
|
|
33
|
-
"@indigoai-us/hq-cloud": "~6.
|
|
33
|
+
"@indigoai-us/hq-cloud": "~6.16.0",
|
|
34
34
|
"@indigoai-us/hq-onboarding": "^0.1.0",
|
|
35
35
|
"@sentry/node": "^10.49.0",
|
|
36
36
|
"@tobilu/qmd": "2.5.3",
|