@indigoai-us/hq-cli 5.103.29 → 5.103.31

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,10 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.103.31] — 2026-08-29
6
+
7
+ ## [5.103.30] — 2026-08-29
8
+
5
9
  ## [5.103.29] — 2026-08-29
6
10
 
7
11
  ## [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-]*$/;
@@ -95,6 +97,21 @@ export function readCompanyPrefix(hqRoot, companySlug) {
95
97
  return undefined;
96
98
  }
97
99
  }
100
+ /**
101
+ * A caller-input condition inside `resolveSkillUid` — the target has no
102
+ * SKILL.md, or the resolved SKILL.md carries no registered skill_uid — is the
103
+ * user's request/state, not an hq-cli defect. Mark it `expected` so the
104
+ * top-level boundary prints the actionable message and skips Sentry, exactly as
105
+ * `skillApiError` does for the remote 4xx siblings (HQ-CLI-Z). Redact at the
106
+ * throw site: the boundary's expected branch prints `err.message` verbatim, so
107
+ * these caller-supplied paths must be scrubbed here, not only on the fallback
108
+ * path they use today.
109
+ */
110
+ function localSkillError(message) {
111
+ return Object.assign(new Error(redactErrorText(message) || message), {
112
+ expected: true,
113
+ });
114
+ }
98
115
  /** Resolve a UID directly, or read the immutable UID from a local SKILL.md. */
99
116
  export function resolveSkillUid(target, cwd) {
100
117
  if (SKILL_UID_PATTERN.test(target))
@@ -104,11 +121,11 @@ export function resolveSkillUid(target, cwd) {
104
121
  filePath = path.join(filePath, "SKILL.md");
105
122
  }
106
123
  if (!fs.existsSync(filePath)) {
107
- throw new Error(`No SKILL.md found at '${target}'. Pass a SKILL.md path, its directory, or a skl_… uid.`);
124
+ throw localSkillError(`No SKILL.md found at '${target}'. Pass a SKILL.md path, its directory, or a skl_… uid.`);
108
125
  }
109
126
  const uid = parseSkillUid(fs.readFileSync(filePath, "utf8"));
110
127
  if (!uid) {
111
- throw new Error(`The SKILL.md at '${filePath}' has no registered skill_uid. Register and stamp it with 'hq skill create <slug> --company <company>', then retry.`);
128
+ throw localSkillError(`The SKILL.md at '${filePath}' has no registered skill_uid. Register and stamp it with 'hq skill create <slug> --company <company>', then retry.`);
112
129
  }
113
130
  return uid;
114
131
  }
@@ -179,6 +196,60 @@ export function mapSkillError(status, body) {
179
196
  return `Server error: ${server || status}`;
180
197
  return server || `Request failed (${status})`;
181
198
  }
199
+ /**
200
+ * Type a failed skills-API response by its HTTP status so the top-level error
201
+ * boundary can tell a correctly-denied client 4xx (ordinary caller state) from a
202
+ * genuine fault (an hq-cli defect or a server error). The message is built from
203
+ * the UNCHANGED `mapSkillError(status, body)` mapping; only the error's CLASS
204
+ * varies:
205
+ *
206
+ * 401 (human) -> AuthError (HQ-CLI-8: the caller runs `hq login`)
207
+ * 401 (machine) -> unmarked Error (a company agent never runs `hq login`)
208
+ * 403, 404, 409, 429 -> ExpectedUserError (HQ-CLI-6: printed, exit 1, not captured)
209
+ * everything else -> unmarked Error (still captured to Sentry)
210
+ *
211
+ * The 4xx allowlist is closed and ENUMERATED, not a `status < 500` range: a 400
212
+ * or 422 means the CLI itself built a malformed request, and every 5xx is a real
213
+ * server fault — both must keep reaching Sentry so a genuine defect is never
214
+ * hidden behind a "caller error" label (HQ-CLI-Z, Sentry 7694457056). Before
215
+ * this, both call sites threw a bare `new Error(...)`, so a correctly-denied 403
216
+ * (e.g. an agent principal with no person entity) fell through the boundary's
217
+ * closed allowlist and filed a crash report for what hq-pro itself deliberately
218
+ * exempts from Sentry capture.
219
+ *
220
+ * `opts.machineIdentity` distinguishes a company agent (`isMachineIdentity()`).
221
+ * A machine session mints automatically and is never repaired by `hq login`, so
222
+ * a machine 401 is a disabled-credential or backend-authorizer fault, not
223
+ * login-repairable caller state — it stays an unmarked, reportable Error rather
224
+ * than a suppressed `AuthError`.
225
+ *
226
+ * The mapped server text is scrubbed and length-bounded through the shared
227
+ * `redactErrorText` chain BEFORE it is placed on any error, because the
228
+ * boundary's AuthError/expected branches print `err.message` verbatim — without
229
+ * this, an upstream 4xx diagnostic would bypass the output-safety boundary that
230
+ * the unmarked path still gets from `fallbackOperatorMessage`.
231
+ *
232
+ * Additive remedy for the observed condition only: a 403 whose machine-readable
233
+ * body `code` is NO_PERSON_ENTITY gets a CLI-actionable `hq onboard` sentence
234
+ * appended (wording precedent: src/commands/cloud-provision.ts). Keyed on
235
+ * `body.code`, never on the prose, so the message is unchanged when the field is
236
+ * absent.
237
+ */
238
+ export function skillApiError(status, body, opts = {}) {
239
+ let message = redactErrorText(mapSkillError(status, body)) || `Request failed (${status})`;
240
+ if (status === 403 && body.code === "NO_PERSON_ENTITY") {
241
+ message += " Run `hq onboard` first to create your HQ identity, then retry.";
242
+ }
243
+ if (status === 401) {
244
+ return opts.machineIdentity ? new Error(message) : new AuthError(message);
245
+ }
246
+ if (status === 403 || status === 404 || status === 409 || status === 429) {
247
+ return Object.assign(new Error(message), {
248
+ expected: true,
249
+ });
250
+ }
251
+ return new Error(message);
252
+ }
182
253
  export function registerSkillCommand(program, deps = {}) {
183
254
  const ensureToken = deps.ensureToken ?? ensureCognitoToken;
184
255
  const apiFetch = deps.apiFetch ?? vaultApiFetch;
@@ -245,7 +316,9 @@ export function registerSkillCommand(program, deps = {}) {
245
316
  });
246
317
  if (!response.ok) {
247
318
  const body = (await response.json().catch(() => ({})));
248
- throw new Error(mapSkillError(response.status, body));
319
+ throw skillApiError(response.status, body, {
320
+ machineIdentity: isMachineIdentity(),
321
+ });
249
322
  }
250
323
  const registered = (await response.json());
251
324
  if (typeof registered.skillUid !== "string" ||
@@ -316,7 +389,9 @@ export function registerSkillCommand(program, deps = {}) {
316
389
  });
317
390
  if (!response.ok) {
318
391
  const body = (await response.json().catch(() => ({})));
319
- throw new Error(mapSkillError(response.status, body));
392
+ throw skillApiError(response.status, body, {
393
+ machineIdentity: isMachineIdentity(),
394
+ });
320
395
  }
321
396
  const posted = (await response.json());
322
397
  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.31",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {