@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 +4 -0
- package/dist/commands/skill.d.ts +42 -0
- package/dist/commands/skill.js +80 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/dist/commands/skill.d.ts
CHANGED
|
@@ -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;
|
package/dist/commands/skill.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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}`));
|