@indigoai-us/hq-cli 5.10.1 → 5.12.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 CHANGED
@@ -1,5 +1,53 @@
1
1
  # Changelog
2
2
 
3
+ ## [5.12.0] — 2026-05-08
4
+
5
+ ### Added
6
+
7
+ - **`hq feedback` command group** — two subcommands `hq feedback bug` and
8
+ `hq feedback feature` let any logged-in user submit structured feedback without
9
+ leaving the terminal. Submissions are JWT-authenticated via the existing Cognito
10
+ session (no additional login step).
11
+
12
+ - **`--body-file <path>` flag** — pass a pre-written markdown file as the feedback
13
+ body. Multi-line input without shell quoting headaches. Use `--body-file -` to read
14
+ from stdin (useful for piped workflows). Body input is file-based by design (no
15
+ inline string flag).
16
+
17
+ - **`--company <slug>` flag (optional)** — attaches a company context to the submission.
18
+ Resolved automatically from `core/scripts/hq-session.sh get company_slug` inside the
19
+ `/feedback` skill; the flag is omitted when the slug is empty.
20
+
21
+ - **Diagnostics blob** — every submission includes a structured diagnostics payload
22
+ (`cliVersion`, `nodeVersion`, `os.platform`, `os.release`, `command` argv array,
23
+ `cwd`, `recentSentryBreadcrumbs`). Purely plain CLI/process context — no GHQ-OS-aware
24
+ identifiers.
25
+
26
+ - **Slack relay** — the vault-service backend best-effort posts a formatted card to
27
+ `#hq-feedback` after writing the DynamoDB row. Relay is fire-and-forget; Slack
28
+ failures do not affect the HTTP response.
29
+
30
+ ### Example
31
+
32
+ ```bash
33
+ # Interactive: title prompted if not supplied
34
+ hq feedback bug --title "search broken on mobile"
35
+
36
+ # Scripted: body from a pre-written file
37
+ hq feedback feature --title "dark mode" --body-file /tmp/feature-body.md
38
+
39
+ # Piped: read body from stdin
40
+ echo "Steps to reproduce: ..." | hq feedback bug --title "crash on login" --body-file -
41
+ ```
42
+
43
+ ## [5.11.0] — 2026-05-07
44
+
45
+ ### Added
46
+
47
+ - **`--personal` flag for `hq secrets`** — pin a secret to the per-user store rather
48
+ than the company store. See commit `b5ec9bd` ("release: hq-cli@5.11.0 — --personal
49
+ flag for secrets") for details.
50
+
3
51
  ## [5.10.0] — 2026-05-04
4
52
 
5
53
  ### Added
@@ -0,0 +1,2 @@
1
+ export declare const CLI_VERSION = "5.12.0";
2
+ //# sourceMappingURL=cli-version.d.ts.map
@@ -0,0 +1,5 @@
1
+
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="f65738b2-fa67-5b68-a0c8-c73fba12c6c5")}catch(e){}}();
3
+ export const CLI_VERSION = "5.12.0";
4
+ //# sourceMappingURL=cli-version.js.map
5
+ //# debugId=f65738b2-fa67-5b68-a0c8-c73fba12c6c5
@@ -13,11 +13,11 @@
13
13
  * hq sync status — show local journal summary
14
14
  */
15
15
 
16
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="5ddbce6a-fdde-506e-8cdd-2e5b0ad20fff")}catch(e){}}();
16
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="213f074e-13c2-5b95-9519-813adf9adfa9")}catch(e){}}();
17
17
  import chalk from "chalk";
18
18
  import * as fs from "fs";
19
19
  import * as path from "path";
20
- import { share, sync, readJournal, getJournalPath, } from "@indigoai-us/hq-cloud";
20
+ import { share, sync, readJournal, getJournalPath, loadCachedTokens, } from "@indigoai-us/hq-cloud";
21
21
  import { DEFAULT_HQ_ROOT, ensureCognitoToken, buildVaultConfig, } from "../utils/cognito-session.js";
22
22
  export function registerCloudCommands(program) {
23
23
  program
@@ -85,6 +85,13 @@ export function registerCloudCommands(program) {
85
85
  const onEvent = jsonMode
86
86
  ? (event) => emitJson(event)
87
87
  : undefined;
88
+ // Stamp every uploaded object's S3 user metadata with the syncing
89
+ // user's Cognito identity (`Metadata['created-by']`). The hq-console
90
+ // vault UI's CREATED BY column reads this back via HEAD; without it,
91
+ // every row renders `—`. Resolved best-effort from the cached
92
+ // idToken — pre-vended `--creds-from-stdin` paths still get author
93
+ // attribution as long as the caller is logged in locally.
94
+ const author = resolveUploadAuthorFromCache();
88
95
  const result = await share({
89
96
  paths: targetPaths,
90
97
  company: options.company,
@@ -94,6 +101,7 @@ export function registerCloudCommands(program) {
94
101
  entityContext,
95
102
  hqRoot: options.hqRoot,
96
103
  onEvent,
104
+ ...(author ? { author } : {}),
97
105
  });
98
106
  if (jsonMode) {
99
107
  // Synthetic terminal event so subprocess consumers can read final
@@ -226,5 +234,36 @@ async function readAllStdin() {
226
234
  }
227
235
  return Buffer.concat(chunks).toString("utf8");
228
236
  }
237
+ /**
238
+ * Resolve the syncing user's `UploadAuthor` (sub + email) from the cached
239
+ * Cognito idToken. Returns `undefined` when no tokens are cached or the
240
+ * token is missing the required claims — share() then skips the metadata
241
+ * stamp gracefully (not an error).
242
+ *
243
+ * We deliberately decode the JWT here instead of verifying it: Cognito
244
+ * already verified at issuance, and we only use the public claims to
245
+ * label the upload's S3 user metadata (no auth decision rides on it).
246
+ */
247
+ function resolveUploadAuthorFromCache() {
248
+ const tokens = loadCachedTokens();
249
+ if (!tokens?.idToken)
250
+ return undefined;
251
+ const parts = tokens.idToken.split(".");
252
+ if (parts.length !== 3)
253
+ return undefined;
254
+ try {
255
+ const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
256
+ const padded = payload + "=".repeat((4 - (payload.length % 4)) % 4);
257
+ const json = Buffer.from(padded, "base64").toString("utf-8");
258
+ const claims = JSON.parse(json);
259
+ if (claims.sub && claims.email) {
260
+ return { userSub: claims.sub, email: claims.email };
261
+ }
262
+ return undefined;
263
+ }
264
+ catch {
265
+ return undefined;
266
+ }
267
+ }
229
268
  //# sourceMappingURL=cloud.js.map
230
- //# debugId=5ddbce6a-fdde-506e-8cdd-2e5b0ad20fff
269
+ //# debugId=213f074e-13c2-5b95-9519-813adf9adfa9
@@ -0,0 +1,16 @@
1
+ import { Command } from "commander";
2
+ export declare const BODY_MAX_BYTES: number;
3
+ export interface FeedbackResult {
4
+ id: string;
5
+ }
6
+ export interface FeedbackSubmitOptions {
7
+ type: "bug" | "feature";
8
+ title: string;
9
+ body: string;
10
+ company?: string;
11
+ token: string;
12
+ }
13
+ export declare function readBodyFile(bodyFile: string, stdin?: NodeJS.ReadableStream): Promise<string>;
14
+ export declare function submitFeedback(opts: FeedbackSubmitOptions): Promise<FeedbackResult>;
15
+ export declare function registerFeedbackCommand(program: Command): void;
16
+ //# sourceMappingURL=feedback.d.ts.map
@@ -0,0 +1,98 @@
1
+
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="3e8d95ee-bfb4-5ca3-8177-7bc3ca618bd4")}catch(e){}}();
3
+ import * as fs from "node:fs";
4
+ import chalk from "chalk";
5
+ import { ensureCognitoToken } from "../utils/cognito-session.js";
6
+ import { vaultApiFetch } from "../utils/vault-api.js";
7
+ import { collectDiagnostics } from "../utils/feedback-diagnostics.js";
8
+ export const BODY_MAX_BYTES = 64 * 1024;
9
+ export async function readBodyFile(bodyFile, stdin) {
10
+ if (bodyFile === "-") {
11
+ const stream = stdin ?? process.stdin;
12
+ if (stream.isTTY) {
13
+ throw new Error("--body-file - requires piped stdin (got interactive terminal).");
14
+ }
15
+ return new Promise((resolve, reject) => {
16
+ let data = "";
17
+ stream.setEncoding("utf8");
18
+ stream.on("data", (chunk) => {
19
+ data += chunk;
20
+ });
21
+ stream.on("end", () => resolve(data));
22
+ stream.on("error", reject);
23
+ });
24
+ }
25
+ return fs.promises.readFile(bodyFile, "utf-8");
26
+ }
27
+ export async function submitFeedback(opts) {
28
+ if (opts.body.trim().length === 0) {
29
+ throw new Error("body must not be empty. Provide at least one non-whitespace character.");
30
+ }
31
+ const bodyBytes = Buffer.byteLength(opts.body, "utf8");
32
+ if (bodyBytes > BODY_MAX_BYTES) {
33
+ throw new Error(`Body exceeds 64 KiB limit (${bodyBytes} bytes). Reduce the body size before submitting.`);
34
+ }
35
+ const diagnostics = collectDiagnostics();
36
+ const requestBody = {
37
+ type: opts.type,
38
+ title: opts.title,
39
+ body: opts.body,
40
+ diagnostics,
41
+ };
42
+ if (opts.company) {
43
+ requestBody.company = opts.company;
44
+ }
45
+ const res = await vaultApiFetch({
46
+ token: opts.token,
47
+ path: "/v1/feedback",
48
+ method: "POST",
49
+ body: requestBody,
50
+ });
51
+ if (!res.ok) {
52
+ const data = await res.json().catch(() => ({}));
53
+ const errMsg = data &&
54
+ typeof data === "object" &&
55
+ !Array.isArray(data) &&
56
+ typeof data.error === "string"
57
+ ? data.error
58
+ : res.statusText;
59
+ throw new Error(`Failed to submit feedback: ${errMsg}`);
60
+ }
61
+ const data = (await res.json());
62
+ return { id: data.id };
63
+ }
64
+ function registerSubcommand(feedbackCmd, type) {
65
+ feedbackCmd
66
+ .command(type)
67
+ .description(type === "bug" ? "Report a bug" : "Request a feature")
68
+ .requiredOption("--title <text>", "Short title for the report")
69
+ .requiredOption("--body-file <path>", "Path to a markdown file with the body; use - to read from stdin")
70
+ .option("--company <slug>", "Company slug to associate with the report")
71
+ .action(async (opts) => {
72
+ try {
73
+ const token = await ensureCognitoToken({ interactive: false });
74
+ const body = await readBodyFile(opts.bodyFile);
75
+ const result = await submitFeedback({
76
+ type,
77
+ title: opts.title,
78
+ body,
79
+ company: opts.company,
80
+ token,
81
+ });
82
+ console.log(`Submitted: ${result.id}`);
83
+ }
84
+ catch (err) {
85
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
86
+ process.exit(1);
87
+ }
88
+ });
89
+ }
90
+ export function registerFeedbackCommand(program) {
91
+ const feedbackCmd = program
92
+ .command("feedback")
93
+ .description("Submit a bug report or feature request to HQ");
94
+ registerSubcommand(feedbackCmd, "bug");
95
+ registerSubcommand(feedbackCmd, "feature");
96
+ }
97
+ //# sourceMappingURL=feedback.js.map
98
+ //# debugId=3e8d95ee-bfb4-5ca3-8177-7bc3ca618bd4
@@ -1,6 +1,6 @@
1
1
  import { Command } from "commander";
2
- import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
2
+ import { vaultApiFetch, getCompanyUid, getEntityUid } from "../utils/vault-api.js";
3
3
  export type { VaultApiOptions } from "../utils/vault-api.js";
4
- export { vaultApiFetch, getCompanyUid };
4
+ export { vaultApiFetch, getCompanyUid, getEntityUid };
5
5
  export declare function registerSecretsCommand(program: Command): void;
6
6
  //# sourceMappingURL=secrets.d.ts.map
@@ -1,13 +1,26 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="0239c476-98b8-53c0-9458-1c82dc0d26a9")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="217ceadb-01cd-5778-9cc0-6f6d895d8d67")}catch(e){}}();
3
3
  import chalk from "chalk";
4
4
  import * as readline from "node:readline";
5
5
  import { spawn } from "node:child_process";
6
6
  import { ensureCognitoToken } from "../utils/cognito-session.js";
7
7
  import { readCache, writeCache, removeCacheEntry, clearAllCache, } from "../utils/secrets-cache.js";
8
8
  import { SECRET_NAME_PATTERN, GROUP_ID_PATTERN } from "./_patterns.js";
9
- import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
10
- export { vaultApiFetch, getCompanyUid };
9
+ import { vaultApiFetch, getCompanyUid, getEntityUid, } from "../utils/vault-api.js";
10
+ export { vaultApiFetch, getCompanyUid, getEntityUid };
11
+ function scopeOpts(opts) {
12
+ if (opts.personal && opts.company) {
13
+ console.error(chalk.red("Error: --personal cannot be combined with --company."));
14
+ process.exit(1);
15
+ }
16
+ return { personal: !!opts.personal, companySlug: opts.company };
17
+ }
18
+ function rejectIfPersonal(opts, action) {
19
+ if (opts.personal) {
20
+ console.error(chalk.red(`Error: ${action} is not supported with --personal.`));
21
+ process.exit(1);
22
+ }
23
+ }
11
24
  function shellSingleQuote(value) {
12
25
  return "'" + value.replace(/'/g, "'\\''") + "'";
13
26
  }
@@ -115,7 +128,8 @@ export function registerSecretsCommand(program) {
115
128
  const secrets = program
116
129
  .command("secrets")
117
130
  .description("Manage secrets in HQ vault (SSM Parameter Store)")
118
- .option("--company <slug>", "Company slug (resolves to companyUid)");
131
+ .option("--company <slug>", "Company slug (resolves to companyUid)")
132
+ .option("--personal", "Operate on the caller's personal vault (no sharing)");
119
133
  secrets
120
134
  .command("set <name>")
121
135
  .description("Create or update a secret")
@@ -150,8 +164,7 @@ export function registerSecretsCommand(program) {
150
164
  process.exit(1);
151
165
  }
152
166
  const token = await ensureCognitoToken();
153
- const companySlug = secrets.opts().company;
154
- const companyUid = await getCompanyUid(token, companySlug);
167
+ const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
155
168
  const res = await vaultApiFetch({
156
169
  token,
157
170
  path: `/secrets/${encodeURIComponent(companyUid)}`,
@@ -178,8 +191,7 @@ export function registerSecretsCommand(program) {
178
191
  .action(async (name, opts) => {
179
192
  try {
180
193
  const token = await ensureCognitoToken();
181
- const companySlug = secrets.opts().company;
182
- const companyUid = await getCompanyUid(token, companySlug);
194
+ const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
183
195
  const query = {};
184
196
  if (opts.reveal) {
185
197
  query.reveal = "true";
@@ -233,8 +245,7 @@ export function registerSecretsCommand(program) {
233
245
  normalizedPrefix = normalized;
234
246
  }
235
247
  const token = await ensureCognitoToken();
236
- const companySlug = secrets.opts().company;
237
- const companyUid = await getCompanyUid(token, companySlug);
248
+ const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
238
249
  const query = {};
239
250
  if (normalizedPrefix) {
240
251
  query.prefix = normalizedPrefix;
@@ -298,8 +309,7 @@ export function registerSecretsCommand(program) {
298
309
  }
299
310
  }
300
311
  const token = await ensureCognitoToken();
301
- const companySlug = secrets.opts().company;
302
- const companyUid = await getCompanyUid(token, companySlug);
312
+ const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
303
313
  const res = await vaultApiFetch({
304
314
  token,
305
315
  path: buildSecretNamePath(companyUid, name),
@@ -350,8 +360,7 @@ export function registerSecretsCommand(program) {
350
360
  }
351
361
  }
352
362
  const token = await ensureCognitoToken();
353
- const companySlug = secrets.opts().company;
354
- const companyUid = await getCompanyUid(token, companySlug);
363
+ const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
355
364
  const revealed = await Promise.all(keys.map(async (key) => {
356
365
  const cached = readCache(companyUid, key);
357
366
  if (cached !== null) {
@@ -420,8 +429,7 @@ export function registerSecretsCommand(program) {
420
429
  }
421
430
  }
422
431
  const token = await ensureCognitoToken();
423
- const companySlug = secrets.opts().company;
424
- const companyUid = await getCompanyUid(token, companySlug);
432
+ const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
425
433
  const revealed = await Promise.all(keys.map(async (key) => {
426
434
  const cached = readCache(companyUid, key);
427
435
  if (cached !== null) {
@@ -459,6 +467,7 @@ export function registerSecretsCommand(program) {
459
467
  .option("--expires <duration>", "Token expiry duration (e.g. 24h, 2d, 30m)", "24h")
460
468
  .action(async (name, opts) => {
461
469
  try {
470
+ rejectIfPersonal(secrets.opts(), "generate-link");
462
471
  if (!SECRET_NAME_PATTERN.test(name)) {
463
472
  console.error(chalk.red(`Invalid secret name '${name}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_API_KEY or DEV/MY_KEY)`));
464
473
  process.exit(1);
@@ -474,8 +483,7 @@ export function registerSecretsCommand(program) {
474
483
  process.exit(1);
475
484
  }
476
485
  const token = await ensureCognitoToken();
477
- const companySlug = secrets.opts().company;
478
- const companyUid = await getCompanyUid(token, companySlug);
486
+ const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
479
487
  const res = await vaultApiFetch({
480
488
  token,
481
489
  path: buildSecretNamePath(companyUid, name),
@@ -507,6 +515,7 @@ export function registerSecretsCommand(program) {
507
515
  .requiredOption("--permission <level>", "Permission level: read | write | admin")
508
516
  .action(async (path, opts) => {
509
517
  try {
518
+ rejectIfPersonal(secrets.opts(), "share");
510
519
  if (!SECRET_NAME_PATTERN.test(path)) {
511
520
  console.error(chalk.red(`Invalid secret path '${path}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_KEY or PROD/DB_PASSWORD)`));
512
521
  process.exit(1);
@@ -523,8 +532,7 @@ export function registerSecretsCommand(program) {
523
532
  const granteeType = isEmail ? "email" : "group";
524
533
  const granteeId = opts.with;
525
534
  const token = await ensureCognitoToken();
526
- const companySlug = secrets.opts().company;
527
- const companyUid = await getCompanyUid(token, companySlug);
535
+ const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
528
536
  const res = await vaultApiFetch({
529
537
  token,
530
538
  path: `/secrets/${encodeURIComponent(companyUid)}/acl/grant`,
@@ -566,6 +574,7 @@ export function registerSecretsCommand(program) {
566
574
  .requiredOption("--from <principal>", "Email address or group id to remove")
567
575
  .action(async (path, opts) => {
568
576
  try {
577
+ rejectIfPersonal(secrets.opts(), "unshare");
569
578
  if (!SECRET_NAME_PATTERN.test(path)) {
570
579
  console.error(chalk.red(`Invalid secret path '${path}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_KEY or PROD/DB_PASSWORD)`));
571
580
  process.exit(1);
@@ -578,8 +587,7 @@ export function registerSecretsCommand(program) {
578
587
  const granteeType = isEmailFrom ? "email" : "group";
579
588
  const granteeId = opts.from;
580
589
  const token = await ensureCognitoToken();
581
- const companySlug = secrets.opts().company;
582
- const companyUid = await getCompanyUid(token, companySlug);
590
+ const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
583
591
  const res = await vaultApiFetch({
584
592
  token,
585
593
  path: `/secrets/${encodeURIComponent(companyUid)}/acl/revoke`,
@@ -618,13 +626,13 @@ export function registerSecretsCommand(program) {
618
626
  .description("Show the ACL (access control list) for a secret path")
619
627
  .action(async (path) => {
620
628
  try {
629
+ rejectIfPersonal(secrets.opts(), "acl");
621
630
  if (!SECRET_NAME_PATTERN.test(path)) {
622
631
  console.error(chalk.red(`Invalid secret path '${path}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_KEY or PROD/DB_PASSWORD)`));
623
632
  process.exit(1);
624
633
  }
625
634
  const token = await ensureCognitoToken();
626
- const companySlug = secrets.opts().company;
627
- const companyUid = await getCompanyUid(token, companySlug);
635
+ const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
628
636
  const secretPath = path;
629
637
  const res = await vaultApiFetch({
630
638
  token,
@@ -705,4 +713,4 @@ export function registerSecretsCommand(program) {
705
713
  });
706
714
  }
707
715
  //# sourceMappingURL=secrets.js.map
708
- //# debugId=0239c476-98b8-53c0-9458-1c82dc0d26a9
716
+ //# debugId=217ceadb-01cd-5778-9cc0-6f6d895d8d67
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * HQ CLI - Module management, package management, and cloud sync for HQ
4
4
  */
5
5
 
6
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="e834c407-cd5c-5dc1-af3b-7f041af651ef")}catch(e){}}();
6
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="65033669-3ba8-5aac-8a98-49827f627ff9")}catch(e){}}();
7
7
  import { Command } from "commander";
8
8
  import { initSentry, Sentry } from "./sentry.js";
9
9
  import { registerAddCommand } from "./commands/add.js";
@@ -28,12 +28,14 @@ import { registerRunCommand } from "./commands/run.js";
28
28
  import { registerGroupsCommand } from "./commands/groups.js";
29
29
  import { registerFilesCommand } from "./commands/files.js";
30
30
  import { registerMembersCommand } from "./commands/members.js";
31
+ import { registerFeedbackCommand } from "./commands/feedback.js";
32
+ import { sanitizeArgv } from "./utils/feedback-diagnostics.js";
31
33
  initSentry();
32
34
  const program = new Command();
33
35
  program
34
36
  .name("hq")
35
37
  .description("HQ management CLI — modules, packages, and cloud sync")
36
- .version("5.8.6");
38
+ .version("5.12.0");
37
39
  // Module management subcommand group
38
40
  const modulesCmd = program
39
41
  .command("modules")
@@ -86,8 +88,15 @@ registerFilesCommand(program);
86
88
  registerMembersCommand(program);
87
89
  // Onboarding (top-level — Cognito + vault-service provisioning)
88
90
  registerOnboardCommand(program);
91
+ // Feedback (subcommand group — hq feedback bug|feature)
92
+ registerFeedbackCommand(program);
89
93
  (async () => {
90
94
  try {
95
+ Sentry.addBreadcrumb({
96
+ category: "command",
97
+ message: sanitizeArgv(process.argv.slice(2)).join(" "),
98
+ level: "info",
99
+ });
91
100
  await program.parseAsync();
92
101
  }
93
102
  catch (err) {
@@ -99,4 +108,4 @@ registerOnboardCommand(program);
99
108
  }
100
109
  })();
101
110
  //# sourceMappingURL=index.js.map
102
- //# debugId=e834c407-cd5c-5dc1-af3b-7f041af651ef
111
+ //# debugId=65033669-3ba8-5aac-8a98-49827f627ff9
package/dist/sentry.js CHANGED
@@ -1,8 +1,9 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="9a31fb53-8cc1-5394-ac16-e96b82fa6caa")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="508ac88d-5683-5d1f-9857-80a8a74e03e2")}catch(e){}}();
3
3
  import * as Sentry from "@sentry/node";
4
4
  import { BUNDLED_DSN } from "./sentry-dsn.generated.js";
5
5
  import { beforeSend } from "./sentry-before-send.js";
6
+ import { beforeBreadcrumb } from "./utils/breadcrumb-buffer.js";
6
7
  export function initSentry() {
7
8
  const dsn = BUNDLED_DSN || process.env.SENTRY_DSN;
8
9
  if (!dsn)
@@ -15,8 +16,9 @@ export function initSentry() {
15
16
  tags: { repo: "hq-cli" },
16
17
  },
17
18
  beforeSend,
19
+ beforeBreadcrumb,
18
20
  });
19
21
  }
20
22
  export { Sentry };
21
23
  //# sourceMappingURL=sentry.js.map
22
- //# debugId=9a31fb53-8cc1-5394-ac16-e96b82fa6caa
24
+ //# debugId=508ac88d-5683-5d1f-9857-80a8a74e03e2
@@ -0,0 +1,4 @@
1
+ import type { Breadcrumb } from "@sentry/node";
2
+ export declare function beforeBreadcrumb(breadcrumb: Breadcrumb): Breadcrumb | null;
3
+ export declare function getRecentBreadcrumbs(): Breadcrumb[];
4
+ //# sourceMappingURL=breadcrumb-buffer.d.ts.map
@@ -0,0 +1,18 @@
1
+
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="6a54e8f0-10de-59c2-82d9-22de94d9e6f2")}catch(e){}}();
3
+ const BUFFER_SIZE = 20;
4
+ const _buffer = [];
5
+ // Sentry beforeBreadcrumb hook: records every breadcrumb in a ring buffer
6
+ // and returns it unchanged so Sentry still processes it normally.
7
+ export function beforeBreadcrumb(breadcrumb) {
8
+ _buffer.push(breadcrumb);
9
+ if (_buffer.length > BUFFER_SIZE) {
10
+ _buffer.shift();
11
+ }
12
+ return breadcrumb;
13
+ }
14
+ export function getRecentBreadcrumbs() {
15
+ return [..._buffer];
16
+ }
17
+ //# sourceMappingURL=breadcrumb-buffer.js.map
18
+ //# debugId=6a54e8f0-10de-59c2-82d9-22de94d9e6f2
@@ -0,0 +1,22 @@
1
+ export interface GitContext {
2
+ branch: string | null;
3
+ head: string | null;
4
+ dirty: boolean;
5
+ remoteUrl: string | null;
6
+ }
7
+ export interface DiagnosticsBlob {
8
+ cliVersion: string;
9
+ nodeVersion: string;
10
+ os: {
11
+ platform: string;
12
+ release: string;
13
+ arch: string;
14
+ };
15
+ command: string[];
16
+ cwd: string;
17
+ git: GitContext;
18
+ recentSentryBreadcrumbs: unknown[];
19
+ }
20
+ export declare function sanitizeArgv(argv: string[]): string[];
21
+ export declare function collectDiagnostics(): DiagnosticsBlob;
22
+ //# sourceMappingURL=feedback-diagnostics.d.ts.map
@@ -0,0 +1,95 @@
1
+
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="ee39c63b-ddaa-553d-b5db-77633d323d88")}catch(e){}}();
3
+ import * as os from "os";
4
+ import { execFileSync } from "child_process";
5
+ import { getRecentBreadcrumbs } from "./breadcrumb-buffer.js";
6
+ import { CLI_VERSION } from "../cli-version.js";
7
+ const SECRET_FLAGS = new Set([
8
+ "--token",
9
+ "--secret",
10
+ "--password",
11
+ "--key",
12
+ "--api-key",
13
+ "--access-token",
14
+ "--auth-token",
15
+ ]);
16
+ export function sanitizeArgv(argv) {
17
+ const result = [];
18
+ for (let i = 0; i < argv.length; i++) {
19
+ const arg = argv[i];
20
+ if (arg.startsWith("--") && arg.includes("=")) {
21
+ const eqIdx = arg.indexOf("=");
22
+ const flag = arg.slice(0, eqIdx);
23
+ if (SECRET_FLAGS.has(flag)) {
24
+ result.push(`${flag}=***`);
25
+ continue;
26
+ }
27
+ }
28
+ result.push(arg);
29
+ if (SECRET_FLAGS.has(arg) && i + 1 < argv.length) {
30
+ result.push("***");
31
+ i++;
32
+ }
33
+ }
34
+ return result;
35
+ }
36
+ function sanitizeRemoteUrl(url) {
37
+ return url.replace(/https?:\/\/[^@]+@/, "https://***@");
38
+ }
39
+ function runGit(args) {
40
+ return execFileSync("git", args, {
41
+ encoding: "utf-8",
42
+ timeout: 2000,
43
+ stdio: ["ignore", "pipe", "ignore"],
44
+ }).trim();
45
+ }
46
+ function collectGitContext() {
47
+ let branch = null;
48
+ let head = null;
49
+ let dirty = false;
50
+ let remoteUrl = null;
51
+ try {
52
+ branch = runGit(["rev-parse", "--abbrev-ref", "HEAD"]);
53
+ }
54
+ catch {
55
+ return { branch: null, head: null, dirty: false, remoteUrl: null };
56
+ }
57
+ try {
58
+ head = runGit(["rev-parse", "--short", "HEAD"]);
59
+ }
60
+ catch {
61
+ // best-effort
62
+ }
63
+ try {
64
+ const statusOut = runGit(["status", "--porcelain"]);
65
+ dirty = statusOut.length > 0;
66
+ }
67
+ catch {
68
+ // best-effort
69
+ }
70
+ try {
71
+ const raw = runGit(["remote", "get-url", "origin"]);
72
+ remoteUrl = sanitizeRemoteUrl(raw);
73
+ }
74
+ catch {
75
+ // no origin remote
76
+ }
77
+ return { branch, head, dirty, remoteUrl };
78
+ }
79
+ export function collectDiagnostics() {
80
+ return {
81
+ cliVersion: CLI_VERSION,
82
+ nodeVersion: process.version,
83
+ os: {
84
+ platform: os.platform(),
85
+ release: os.release(),
86
+ arch: os.arch(),
87
+ },
88
+ command: sanitizeArgv(process.argv.slice(2)),
89
+ cwd: process.cwd(),
90
+ git: collectGitContext(),
91
+ recentSentryBreadcrumbs: getRecentBreadcrumbs(),
92
+ };
93
+ }
94
+ //# sourceMappingURL=feedback-diagnostics.js.map
95
+ //# debugId=ee39c63b-ddaa-553d-b5db-77633d323d88
@@ -7,4 +7,9 @@ export interface VaultApiOptions {
7
7
  }
8
8
  export declare function vaultApiFetch(opts: VaultApiOptions): Promise<Response>;
9
9
  export declare function getCompanyUid(token: string, companySlug: string | undefined): Promise<string>;
10
+ export declare function resolveCallerPersonUid(token: string): Promise<string>;
11
+ export declare function getEntityUid(token: string, opts: {
12
+ personal?: boolean;
13
+ companySlug?: string;
14
+ }): Promise<string>;
10
15
  //# sourceMappingURL=vault-api.d.ts.map