@indigoai-us/hq-cli 5.103.29 → 5.103.30

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,8 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.103.30] — 2026-08-29
6
+
5
7
  ## [5.103.29] — 2026-08-29
6
8
 
7
9
  ## [5.103.28] — 2026-08-28
@@ -40,6 +40,48 @@ export declare function makeSkillTemplate(input: {
40
40
  /** Replace SKILL.md without exposing a partially-written identity to agents. */
41
41
  export declare function writeSkillFileAtomically(filePath: string, content: string): void;
42
42
  export declare function mapSkillError(status: number, body: Record<string, unknown>): string;
43
+ /**
44
+ * Type a failed skills-API response by its HTTP status so the top-level error
45
+ * boundary can tell a correctly-denied client 4xx (ordinary caller state) from a
46
+ * genuine fault (an hq-cli defect or a server error). The message is built from
47
+ * the UNCHANGED `mapSkillError(status, body)` mapping; only the error's CLASS
48
+ * varies:
49
+ *
50
+ * 401 (human) -> AuthError (HQ-CLI-8: the caller runs `hq login`)
51
+ * 401 (machine) -> unmarked Error (a company agent never runs `hq login`)
52
+ * 403, 404, 409, 429 -> ExpectedUserError (HQ-CLI-6: printed, exit 1, not captured)
53
+ * everything else -> unmarked Error (still captured to Sentry)
54
+ *
55
+ * The 4xx allowlist is closed and ENUMERATED, not a `status < 500` range: a 400
56
+ * or 422 means the CLI itself built a malformed request, and every 5xx is a real
57
+ * server fault — both must keep reaching Sentry so a genuine defect is never
58
+ * hidden behind a "caller error" label (HQ-CLI-Z, Sentry 7694457056). Before
59
+ * this, both call sites threw a bare `new Error(...)`, so a correctly-denied 403
60
+ * (e.g. an agent principal with no person entity) fell through the boundary's
61
+ * closed allowlist and filed a crash report for what hq-pro itself deliberately
62
+ * exempts from Sentry capture.
63
+ *
64
+ * `opts.machineIdentity` distinguishes a company agent (`isMachineIdentity()`).
65
+ * A machine session mints automatically and is never repaired by `hq login`, so
66
+ * a machine 401 is a disabled-credential or backend-authorizer fault, not
67
+ * login-repairable caller state — it stays an unmarked, reportable Error rather
68
+ * than a suppressed `AuthError`.
69
+ *
70
+ * The mapped server text is scrubbed and length-bounded through the shared
71
+ * `redactErrorText` chain BEFORE it is placed on any error, because the
72
+ * boundary's AuthError/expected branches print `err.message` verbatim — without
73
+ * this, an upstream 4xx diagnostic would bypass the output-safety boundary that
74
+ * the unmarked path still gets from `fallbackOperatorMessage`.
75
+ *
76
+ * Additive remedy for the observed condition only: a 403 whose machine-readable
77
+ * body `code` is NO_PERSON_ENTITY gets a CLI-actionable `hq onboard` sentence
78
+ * appended (wording precedent: src/commands/cloud-provision.ts). Keyed on
79
+ * `body.code`, never on the prose, so the message is unchanged when the field is
80
+ * absent.
81
+ */
82
+ export declare function skillApiError(status: number, body: Record<string, unknown>, opts?: {
83
+ machineIdentity?: boolean;
84
+ }): Error;
43
85
  interface SkillCommandDeps {
44
86
  ensureToken?: typeof ensureCognitoToken;
45
87
  apiFetch?: typeof vaultApiFetch;
@@ -13,9 +13,11 @@ import * as path from "node:path";
13
13
  import chalk from "chalk";
14
14
  import yaml from "js-yaml";
15
15
  import { share } from "@indigoai-us/hq-cloud";
16
- import { ensureCognitoToken, DEFAULT_HQ_ROOT, buildVaultConfig, } from "../utils/cognito-session.js";
16
+ import { ensureCognitoToken, DEFAULT_HQ_ROOT, buildVaultConfig, isMachineIdentity, } from "../utils/cognito-session.js";
17
17
  import { vaultApiFetch } from "../utils/vault-api.js";
18
18
  import { surfaceCompanySkill } from "../lib/company-skill-wrapper.js";
19
+ import { AuthError } from "../utils/auth-error.js";
20
+ import { redactErrorText } from "../utils/redact-error-text.js";
19
21
  export const SKILL_UID_PATTERN = /^skl_[A-Za-z0-9]+$/;
20
22
  export const SKILL_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
21
23
  const COMPANY_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
@@ -179,6 +181,60 @@ export function mapSkillError(status, body) {
179
181
  return `Server error: ${server || status}`;
180
182
  return server || `Request failed (${status})`;
181
183
  }
184
+ /**
185
+ * Type a failed skills-API response by its HTTP status so the top-level error
186
+ * boundary can tell a correctly-denied client 4xx (ordinary caller state) from a
187
+ * genuine fault (an hq-cli defect or a server error). The message is built from
188
+ * the UNCHANGED `mapSkillError(status, body)` mapping; only the error's CLASS
189
+ * varies:
190
+ *
191
+ * 401 (human) -> AuthError (HQ-CLI-8: the caller runs `hq login`)
192
+ * 401 (machine) -> unmarked Error (a company agent never runs `hq login`)
193
+ * 403, 404, 409, 429 -> ExpectedUserError (HQ-CLI-6: printed, exit 1, not captured)
194
+ * everything else -> unmarked Error (still captured to Sentry)
195
+ *
196
+ * The 4xx allowlist is closed and ENUMERATED, not a `status < 500` range: a 400
197
+ * or 422 means the CLI itself built a malformed request, and every 5xx is a real
198
+ * server fault — both must keep reaching Sentry so a genuine defect is never
199
+ * hidden behind a "caller error" label (HQ-CLI-Z, Sentry 7694457056). Before
200
+ * this, both call sites threw a bare `new Error(...)`, so a correctly-denied 403
201
+ * (e.g. an agent principal with no person entity) fell through the boundary's
202
+ * closed allowlist and filed a crash report for what hq-pro itself deliberately
203
+ * exempts from Sentry capture.
204
+ *
205
+ * `opts.machineIdentity` distinguishes a company agent (`isMachineIdentity()`).
206
+ * A machine session mints automatically and is never repaired by `hq login`, so
207
+ * a machine 401 is a disabled-credential or backend-authorizer fault, not
208
+ * login-repairable caller state — it stays an unmarked, reportable Error rather
209
+ * than a suppressed `AuthError`.
210
+ *
211
+ * The mapped server text is scrubbed and length-bounded through the shared
212
+ * `redactErrorText` chain BEFORE it is placed on any error, because the
213
+ * boundary's AuthError/expected branches print `err.message` verbatim — without
214
+ * this, an upstream 4xx diagnostic would bypass the output-safety boundary that
215
+ * the unmarked path still gets from `fallbackOperatorMessage`.
216
+ *
217
+ * Additive remedy for the observed condition only: a 403 whose machine-readable
218
+ * body `code` is NO_PERSON_ENTITY gets a CLI-actionable `hq onboard` sentence
219
+ * appended (wording precedent: src/commands/cloud-provision.ts). Keyed on
220
+ * `body.code`, never on the prose, so the message is unchanged when the field is
221
+ * absent.
222
+ */
223
+ export function skillApiError(status, body, opts = {}) {
224
+ let message = redactErrorText(mapSkillError(status, body)) || `Request failed (${status})`;
225
+ if (status === 403 && body.code === "NO_PERSON_ENTITY") {
226
+ message += " Run `hq onboard` first to create your HQ identity, then retry.";
227
+ }
228
+ if (status === 401) {
229
+ return opts.machineIdentity ? new Error(message) : new AuthError(message);
230
+ }
231
+ if (status === 403 || status === 404 || status === 409 || status === 429) {
232
+ return Object.assign(new Error(message), {
233
+ expected: true,
234
+ });
235
+ }
236
+ return new Error(message);
237
+ }
182
238
  export function registerSkillCommand(program, deps = {}) {
183
239
  const ensureToken = deps.ensureToken ?? ensureCognitoToken;
184
240
  const apiFetch = deps.apiFetch ?? vaultApiFetch;
@@ -245,7 +301,9 @@ export function registerSkillCommand(program, deps = {}) {
245
301
  });
246
302
  if (!response.ok) {
247
303
  const body = (await response.json().catch(() => ({})));
248
- throw new Error(mapSkillError(response.status, body));
304
+ throw skillApiError(response.status, body, {
305
+ machineIdentity: isMachineIdentity(),
306
+ });
249
307
  }
250
308
  const registered = (await response.json());
251
309
  if (typeof registered.skillUid !== "string" ||
@@ -316,7 +374,9 @@ export function registerSkillCommand(program, deps = {}) {
316
374
  });
317
375
  if (!response.ok) {
318
376
  const body = (await response.json().catch(() => ({})));
319
- throw new Error(mapSkillError(response.status, body));
377
+ throw skillApiError(response.status, body, {
378
+ machineIdentity: isMachineIdentity(),
379
+ });
320
380
  }
321
381
  const posted = (await response.json());
322
382
  console.log(chalk.green(`Improvement posted: ${posted.commentId}`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.103.29",
3
+ "version": "5.103.30",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {