@indigoai-us/hq-cli 5.47.15 → 5.47.17

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.
Files changed (39) hide show
  1. package/.github/workflows/publish.yml +2 -2
  2. package/dist/commands/feedback.d.ts +2 -0
  3. package/dist/commands/feedback.js +24 -2
  4. package/dist/commands/files.d.ts +19 -0
  5. package/dist/commands/files.js +37 -3
  6. package/dist/commands/people.d.ts +22 -0
  7. package/dist/commands/people.js +187 -0
  8. package/dist/commands/secrets.js +25 -2
  9. package/dist/index.js +6 -2
  10. package/dist/run/hq-plugin.js +9 -2
  11. package/dist/sentry-dsn.generated.d.ts +1 -1
  12. package/dist/sentry-dsn.generated.js +1 -1
  13. package/dist/utils/feedback-diagnostics.d.ts +7 -0
  14. package/dist/utils/feedback-diagnostics.js +4 -2
  15. package/dist/utils/feedback-screenshots.d.ts +23 -0
  16. package/dist/utils/feedback-screenshots.js +98 -0
  17. package/dist/utils/feedback-versions.d.ts +34 -0
  18. package/dist/utils/feedback-versions.js +50 -0
  19. package/dist/utils/people.d.ts +99 -0
  20. package/dist/utils/people.js +168 -0
  21. package/package.json +1 -1
  22. package/src/commands/feedback.test.ts +44 -0
  23. package/src/commands/feedback.ts +46 -13
  24. package/src/commands/files-delete.test.ts +132 -0
  25. package/src/commands/files.ts +42 -1
  26. package/src/commands/people.test.ts +426 -0
  27. package/src/commands/people.ts +240 -0
  28. package/src/commands/secrets.test.ts +80 -0
  29. package/src/commands/secrets.ts +35 -0
  30. package/src/index.ts +4 -0
  31. package/src/run/hq-plugin.test.ts +39 -0
  32. package/src/run/hq-plugin.ts +7 -0
  33. package/src/utils/feedback-diagnostics.test.ts +11 -0
  34. package/src/utils/feedback-diagnostics.ts +8 -0
  35. package/src/utils/feedback-screenshots.test.ts +134 -0
  36. package/src/utils/feedback-screenshots.ts +124 -0
  37. package/src/utils/feedback-versions.test.ts +98 -0
  38. package/src/utils/feedback-versions.ts +68 -0
  39. package/src/utils/people.ts +222 -0
@@ -104,8 +104,8 @@ jobs:
104
104
  fi
105
105
  export SENTRY_AUTH_TOKEN
106
106
  VER=$(jq -r .version package.json)
107
- npx -y @sentry/cli@^2 sourcemaps upload --release "hq-cli@$VER" dist/
107
+ npx -y @sentry/cli@^2 sourcemaps upload --release "hq-cli@$VER" dist/ node_modules/@indigoai-us/hq-cloud/dist/
108
108
  env:
109
109
  SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
110
110
  SENTRY_ORG: indigo-d0
111
- SENTRY_PROJECT: hq
111
+ SENTRY_PROJECT: hq-cli
@@ -9,6 +9,8 @@ export interface FeedbackSubmitOptions {
9
9
  body: string;
10
10
  company?: string;
11
11
  token: string;
12
+ /** S3 object keys of already-uploaded screenshots (see uploadScreenshots). */
13
+ screenshots?: string[];
12
14
  }
13
15
  export declare function readBodyFile(bodyFile: string, stdin?: NodeJS.ReadableStream): Promise<string>;
14
16
  export declare function submitFeedback(opts: FeedbackSubmitOptions): Promise<FeedbackResult>;
@@ -1,10 +1,11 @@
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]="3e8d95ee-bfb4-5ca3-8177-7bc3ca618bd4")}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]="ee78c7b6-6e93-5217-8a1d-ddc51afcf9c3")}catch(e){}}();
3
3
  import * as fs from "node:fs";
4
4
  import chalk from "chalk";
5
5
  import { ensureCognitoToken } from "../utils/cognito-session.js";
6
6
  import { vaultApiFetch } from "../utils/vault-api.js";
7
7
  import { collectDiagnostics } from "../utils/feedback-diagnostics.js";
8
+ import { MAX_SCREENSHOTS, uploadScreenshots } from "../utils/feedback-screenshots.js";
8
9
  export const BODY_MAX_BYTES = 64 * 1024;
9
10
  export async function readBodyFile(bodyFile, stdin) {
10
11
  if (bodyFile === "-") {
@@ -25,6 +26,16 @@ export async function readBodyFile(bodyFile, stdin) {
25
26
  return fs.promises.readFile(bodyFile, "utf-8");
26
27
  }
27
28
  export async function submitFeedback(opts) {
29
+ // Validate the title locally, symmetric with the body check below. Commander's
30
+ // `requiredOption("--title")` only requires the flag to be PRESENT — an empty
31
+ // or whitespace-only value (`--title ""`, or a title the /hq-bug skill derived
32
+ // to nothing) passes the flag check, then the server rejects it with a 400
33
+ // "title (non-empty string) is required" that floods Sentry as a context-free
34
+ // warning (HQ-AB). Catch it here so the caller gets a clear, actionable error
35
+ // and the bad request never reaches the server.
36
+ if (opts.title.trim().length === 0) {
37
+ throw new Error("title must not be empty. Provide a short, non-whitespace title via --title.");
38
+ }
28
39
  if (opts.body.trim().length === 0) {
29
40
  throw new Error("body must not be empty. Provide at least one non-whitespace character.");
30
41
  }
@@ -42,6 +53,9 @@ export async function submitFeedback(opts) {
42
53
  if (opts.company) {
43
54
  requestBody.company = opts.company;
44
55
  }
56
+ if (opts.screenshots && opts.screenshots.length > 0) {
57
+ requestBody.screenshots = opts.screenshots;
58
+ }
45
59
  const res = await vaultApiFetch({
46
60
  token: opts.token,
47
61
  path: "/v1/feedback",
@@ -68,16 +82,24 @@ function registerSubcommand(feedbackCmd, type) {
68
82
  .requiredOption("--title <text>", "Short title for the report")
69
83
  .requiredOption("--body-file <path>", "Path to a markdown file with the body; use - to read from stdin")
70
84
  .option("--company <slug>", "Company slug to associate with the report")
85
+ .option("--screenshot <path>", `Attach a screenshot (repeatable, up to ${MAX_SCREENSHOTS}; .png/.jpg/.jpeg/.webp/.gif)`, (value, prev) => [...prev, value], [])
71
86
  .action(async (opts) => {
72
87
  try {
73
88
  const token = await ensureCognitoToken({ interactive: false });
74
89
  const body = await readBodyFile(opts.bodyFile);
90
+ // Validate + upload screenshots (direct-to-S3 via presigned PUT)
91
+ // before submitting, so the row references uploaded objects.
92
+ const screenshots = await uploadScreenshots({
93
+ paths: opts.screenshot ?? [],
94
+ token,
95
+ });
75
96
  const result = await submitFeedback({
76
97
  type,
77
98
  title: opts.title,
78
99
  body,
79
100
  company: opts.company,
80
101
  token,
102
+ screenshots,
81
103
  });
82
104
  console.log(`Submitted: ${result.id}`);
83
105
  }
@@ -95,4 +117,4 @@ export function registerFeedbackCommand(program) {
95
117
  registerSubcommand(feedbackCmd, "feature");
96
118
  }
97
119
  //# sourceMappingURL=feedback.js.map
98
- //# debugId=3e8d95ee-bfb4-5ca3-8177-7bc3ca618bd4
120
+ //# debugId=ee78c7b6-6e93-5217-8a1d-ddc51afcf9c3
@@ -68,6 +68,25 @@ interface RunFilesDeleteParams {
68
68
  yes: boolean;
69
69
  companySlug: string | undefined;
70
70
  }
71
+ /**
72
+ * The vault bucket is already company-scoped, so a delete prefix must be
73
+ * BUCKET-RELATIVE (e.g. `projects/foo/*`). A caller who pastes an HQ *local*
74
+ * tree path (`companies/<slug>/projects/foo`) over-prefixes it; the server then
75
+ * rejects it with INVALID_PREFIX_COMPANIES_SCOPED (HTTP 400), the source of the
76
+ * recurring Sentry warning HQ-8F. Strip a redundant leading `companies/<slug>/`
77
+ * so the local-looking path is normalized to the bucket-relative key the vault
78
+ * actually stores. Returns the stripped slug for a one-line notice, or null when
79
+ * there was nothing to strip. Pure → unit-testable.
80
+ *
81
+ * This runs UPSTREAM of the server's exact-vs-glob branch, so it covers both
82
+ * the glob spelling (`companies/<slug>/projects/foo/*` → `validatePrefix`,
83
+ * HQ-8F) and the EXACT-key spelling (`companies/<slug>/notes/foo.md` →
84
+ * `validateObjectKey`, HQ-CA) with the same normalization.
85
+ */
86
+ export declare function stripRedundantCompanyScope(prefix: string): {
87
+ prefix: string;
88
+ strippedSlug: string;
89
+ } | null;
71
90
  export declare function runFilesDelete(params: RunFilesDeleteParams, deps?: {
72
91
  confirm?: ConfirmFn;
73
92
  }): Promise<void>;
@@ -1,5 +1,5 @@
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]="5f5242f3-b24f-58b0-a3ee-381c42c755fc")}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]="bc564dd4-d8c1-5d09-ad49-3bcd9f7328a6")}catch(e){}}();
3
3
  import chalk from "chalk";
4
4
  import open from "open";
5
5
  import * as readline from "node:readline";
@@ -566,13 +566,47 @@ function printKeyPreview(resp) {
566
566
  console.log(chalk.dim(` … and ${resp.matched - resp.keys.length} more`));
567
567
  }
568
568
  }
569
+ /**
570
+ * The vault bucket is already company-scoped, so a delete prefix must be
571
+ * BUCKET-RELATIVE (e.g. `projects/foo/*`). A caller who pastes an HQ *local*
572
+ * tree path (`companies/<slug>/projects/foo`) over-prefixes it; the server then
573
+ * rejects it with INVALID_PREFIX_COMPANIES_SCOPED (HTTP 400), the source of the
574
+ * recurring Sentry warning HQ-8F. Strip a redundant leading `companies/<slug>/`
575
+ * so the local-looking path is normalized to the bucket-relative key the vault
576
+ * actually stores. Returns the stripped slug for a one-line notice, or null when
577
+ * there was nothing to strip. Pure → unit-testable.
578
+ *
579
+ * This runs UPSTREAM of the server's exact-vs-glob branch, so it covers both
580
+ * the glob spelling (`companies/<slug>/projects/foo/*` → `validatePrefix`,
581
+ * HQ-8F) and the EXACT-key spelling (`companies/<slug>/notes/foo.md` →
582
+ * `validateObjectKey`, HQ-CA) with the same normalization.
583
+ */
584
+ export function stripRedundantCompanyScope(prefix) {
585
+ const m = /^companies\/([^/]+)(?:\/(.*))?$/.exec(prefix);
586
+ if (!m)
587
+ return null;
588
+ return { prefix: m[2] ?? "", strippedSlug: m[1] };
589
+ }
569
590
  export async function runFilesDelete(params, deps = {}) {
570
591
  const confirm = deps.confirm ?? realConfirm;
592
+ // The vault is already company-scoped — a `companies/<slug>/` prefix is the HQ
593
+ // LOCAL tree layout, not a vault key, and the server 400s it (HQ-8F). Strip it
594
+ // here so a pasted local path is gracefully normalized to bucket-relative
595
+ // BEFORE the dry-run/preview (so the operator still sees the exact keys and
596
+ // confirms the right target). If stripping empties the prefix, the root-reject
597
+ // below catches it with a clear message.
598
+ const scope = stripRedundantCompanyScope(params.prefix);
599
+ if (scope) {
600
+ console.error(chalk.yellow(`Note: stripped redundant 'companies/${scope.strippedSlug}/' — the vault ` +
601
+ `is already company-scoped; using bucket-relative ` +
602
+ `'${scope.prefix || "(root)"}'.`));
603
+ }
604
+ const rawPrefix = scope ? scope.prefix : params.prefix;
571
605
  // Normalize exactly as the share/unshare/acl paths do (trailing `/` → `/*`),
572
606
  // then reject the root/empty prefix CLIENT-side so a typo never reaches the
573
607
  // server as a vault-wide delete. The server enforces this too (defense in
574
608
  // depth), but failing fast here is clearer and avoids a wasted round-trip.
575
- const normalized = normalizeFilePrefix(params.prefix);
609
+ const normalized = normalizeFilePrefix(rawPrefix);
576
610
  if (normalized === "" || normalized === "*" || normalized === "/*") {
577
611
  console.error(chalk.red("Refusing to delete the vault root. Pass a bounded prefix (e.g. 'projects/foo/' or 'projects/foo/*') or an exact key."));
578
612
  process.exit(1);
@@ -648,4 +682,4 @@ export async function runFilesDelete(params, deps = {}) {
648
682
  }
649
683
  }
650
684
  //# sourceMappingURL=files.js.map
651
- //# debugId=5f5242f3-b24f-58b0-a3ee-381c42c755fc
685
+ //# debugId=bc564dd4-d8c1-5d09-ad49-3bcd9f7328a6
@@ -0,0 +1,22 @@
1
+ /**
2
+ * `hq people` — read a company's people (membership) records from the local HQ
3
+ * tree and search/resolve over them.
4
+ *
5
+ * hq people list [--company <slug>] [--json]
6
+ * hq people search <keyword> [--company <slug>] [--json]
7
+ * hq people resolve <name> [--company <slug>] [--json]
8
+ *
9
+ * Source of truth is `companies/<company>/people/<person>/meta.yaml`. Every
10
+ * subcommand operates on exactly ONE company (the active company, or the one
11
+ * named by `--company`); nothing reads across company boundaries.
12
+ */
13
+ import { Command } from "commander";
14
+ /**
15
+ * Resolve the single company to operate on. Explicit `--company` always wins
16
+ * (after a path-safety check). Otherwise the active company is inferred from
17
+ * `companies/manifest.yaml`: if exactly one company exists it's used; if several
18
+ * do, the caller must disambiguate with `--company`.
19
+ */
20
+ export declare function resolveCompanySlug(hqRoot: string, explicit: string | undefined): string;
21
+ export declare function registerPeopleCommand(program: Command): void;
22
+ //# sourceMappingURL=people.d.ts.map
@@ -0,0 +1,187 @@
1
+ /**
2
+ * `hq people` — read a company's people (membership) records from the local HQ
3
+ * tree and search/resolve over them.
4
+ *
5
+ * hq people list [--company <slug>] [--json]
6
+ * hq people search <keyword> [--company <slug>] [--json]
7
+ * hq people resolve <name> [--company <slug>] [--json]
8
+ *
9
+ * Source of truth is `companies/<company>/people/<person>/meta.yaml`. Every
10
+ * subcommand operates on exactly ONE company (the active company, or the one
11
+ * named by `--company`); nothing reads across company boundaries.
12
+ */
13
+
14
+ !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]="076b7f64-6626-5733-8992-ab9361a3e31d")}catch(e){}}();
15
+ import * as fs from "fs";
16
+ import { Option } from "commander";
17
+ import chalk from "chalk";
18
+ import * as yaml from "js-yaml";
19
+ import { findHqRoot } from "../utils/manifest.js";
20
+ import { manifestPath } from "./cloud-provision.js";
21
+ import { assertSafeCompanySlug, listCompanyPeople, searchPeople, resolveNameToEmail, companyPeopleDir, } from "../utils/people.js";
22
+ /** Companies that still exist (anything not explicitly `status: archived`). */
23
+ function activeCompanySlugs(manifest) {
24
+ const companies = manifest.companies ?? {};
25
+ return Object.entries(companies)
26
+ .filter(([, entry]) => (entry?.status ?? "active") !== "archived")
27
+ .map(([slug]) => slug)
28
+ .sort();
29
+ }
30
+ /**
31
+ * Resolve the single company to operate on. Explicit `--company` always wins
32
+ * (after a path-safety check). Otherwise the active company is inferred from
33
+ * `companies/manifest.yaml`: if exactly one company exists it's used; if several
34
+ * do, the caller must disambiguate with `--company`.
35
+ */
36
+ export function resolveCompanySlug(hqRoot, explicit) {
37
+ if (explicit) {
38
+ assertSafeCompanySlug(explicit);
39
+ return explicit;
40
+ }
41
+ const mPath = manifestPath(hqRoot);
42
+ if (!fs.existsSync(mPath)) {
43
+ throw new Error("Could not determine the active company — companies/manifest.yaml not found. " +
44
+ "Re-run with --company <slug>.");
45
+ }
46
+ let manifest;
47
+ try {
48
+ manifest = yaml.load(fs.readFileSync(mPath, "utf-8"));
49
+ }
50
+ catch (err) {
51
+ throw new Error(`companies/manifest.yaml is malformed: ${err instanceof Error ? err.message : String(err)}`);
52
+ }
53
+ const slugs = activeCompanySlugs(manifest ?? {});
54
+ if (slugs.length === 1)
55
+ return slugs[0];
56
+ if (slugs.length === 0) {
57
+ throw new Error("No companies found in companies/manifest.yaml — re-run with --company <slug>.");
58
+ }
59
+ throw new Error("Multiple companies found — re-run with --company <slug> to pick one:\n" +
60
+ slugs.map((s) => ` --company ${s}`).join("\n"));
61
+ }
62
+ /** Resolve the HQ root: explicit override (tests) → cwd walk-up. */
63
+ function resolveHqRoot(opts) {
64
+ return opts.hqRoot ?? findHqRoot();
65
+ }
66
+ function printPeopleTable(people) {
67
+ const nameW = Math.max(4, ...people.map((p) => p.name.length));
68
+ const emailW = Math.max(5, ...people.map((p) => (p.email ?? "—").length));
69
+ const roleW = Math.max(4, ...people.map((p) => (p.role ?? "—").length));
70
+ console.log(chalk.bold([
71
+ "NAME".padEnd(nameW),
72
+ "EMAIL".padEnd(emailW),
73
+ "ROLE".padEnd(roleW),
74
+ "TYPE",
75
+ ].join(" ")));
76
+ for (const p of people) {
77
+ console.log([
78
+ p.name.padEnd(nameW),
79
+ (p.email ?? "—").padEnd(emailW),
80
+ (p.role ?? "—").padEnd(roleW),
81
+ p.type ?? "—",
82
+ ].join(" "));
83
+ }
84
+ }
85
+ function fail(message) {
86
+ console.error(chalk.red(message));
87
+ process.exit(1);
88
+ }
89
+ export function registerPeopleCommand(program) {
90
+ const people = program
91
+ .command("people")
92
+ .description("List, search, and resolve a company's people (from companies/<co>/people)")
93
+ .option("--company <slug>", "Company slug to scope to (defaults to the active company)")
94
+ // Hidden escape hatch for tests / non-standard layouts — point the reader at
95
+ // an explicit HQ tree root instead of walking up from cwd.
96
+ .addOption(new Option("--hq-root <path>", "Override the HQ tree root (advanced)").hideHelp());
97
+ people
98
+ .command("list")
99
+ .description("List all people recorded for the company")
100
+ .option("--json", "Output JSON instead of a table")
101
+ .action((opts) => {
102
+ try {
103
+ const scope = people.opts();
104
+ const hqRoot = resolveHqRoot(scope);
105
+ const slug = resolveCompanySlug(hqRoot, scope.company);
106
+ const records = listCompanyPeople(hqRoot, slug);
107
+ if (opts.json) {
108
+ console.log(JSON.stringify(records, null, 2));
109
+ return;
110
+ }
111
+ if (records.length === 0) {
112
+ console.log(chalk.gray(`No people recorded for '${slug}' (looked in ${companyPeopleDir(hqRoot, slug)}).`));
113
+ return;
114
+ }
115
+ printPeopleTable(records);
116
+ }
117
+ catch (err) {
118
+ fail(err instanceof Error ? err.message : String(err));
119
+ }
120
+ });
121
+ people
122
+ .command("search <keyword>")
123
+ .description("Keyword search over people names and emails")
124
+ .option("--json", "Output JSON instead of a table")
125
+ .action((keyword, opts) => {
126
+ try {
127
+ const scope = people.opts();
128
+ const hqRoot = resolveHqRoot(scope);
129
+ const slug = resolveCompanySlug(hqRoot, scope.company);
130
+ const matches = searchPeople(listCompanyPeople(hqRoot, slug), keyword);
131
+ if (opts.json) {
132
+ console.log(JSON.stringify(matches, null, 2));
133
+ return;
134
+ }
135
+ if (matches.length === 0) {
136
+ console.log(chalk.gray(`No people in '${slug}' match "${keyword}".`));
137
+ return;
138
+ }
139
+ printPeopleTable(matches);
140
+ }
141
+ catch (err) {
142
+ fail(err instanceof Error ? err.message : String(err));
143
+ }
144
+ });
145
+ people
146
+ .command("resolve <name>")
147
+ .description("Resolve a person name to their email address")
148
+ .option("--json", "Output JSON instead of plain text")
149
+ .action((name, opts) => {
150
+ try {
151
+ const scope = people.opts();
152
+ const hqRoot = resolveHqRoot(scope);
153
+ const slug = resolveCompanySlug(hqRoot, scope.company);
154
+ const result = resolveNameToEmail(listCompanyPeople(hqRoot, slug), name);
155
+ if (opts.json) {
156
+ console.log(JSON.stringify(result, null, 2));
157
+ if (result.status === "found")
158
+ return;
159
+ process.exit(1);
160
+ }
161
+ switch (result.status) {
162
+ case "found":
163
+ // Bare email on stdout so callers can capture it directly.
164
+ console.log(result.email);
165
+ return;
166
+ case "no_email":
167
+ fail(`Found '${result.person.name}' in '${slug}' but no email is recorded for them.`);
168
+ break;
169
+ case "ambiguous":
170
+ console.error(chalk.yellow(`"${name}" matches ${result.matches.length} people in '${slug}' — be more specific:`));
171
+ for (const m of result.matches) {
172
+ console.error(` ${m.name}${m.email ? ` <${m.email}>` : ""}`);
173
+ }
174
+ process.exit(1);
175
+ break;
176
+ case "not_found":
177
+ fail(`No person matching "${name}" found in '${slug}'.`);
178
+ break;
179
+ }
180
+ }
181
+ catch (err) {
182
+ fail(err instanceof Error ? err.message : String(err));
183
+ }
184
+ });
185
+ }
186
+ //# sourceMappingURL=people.js.map
187
+ //# debugId=076b7f64-6626-5733-8992-ab9361a3e31d
@@ -1,5 +1,5 @@
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]="d01cb1a1-b851-5ffe-9608-e505e55dcdc8")}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]="3049e431-c5db-5430-90fe-82866c35e95b")}catch(e){}}();
3
3
  import chalk from "chalk";
4
4
  import * as readline from "node:readline";
5
5
  import { spawn } from "node:child_process";
@@ -251,6 +251,11 @@ export async function loadRevealedSecrets(token, companyUid, keys, usage) {
251
251
  if (!res.ok) {
252
252
  const body = (await res.json().catch(() => ({})));
253
253
  const message = extractApiMessage(body, res.statusText);
254
+ // High-security ("nuclear") refusal surfaced at the batch level (rather
255
+ // than per-name): point the caller at the proxy and never leak plaintext.
256
+ if (body.code === "high_security_denied" || body.highSecurity === true) {
257
+ throw new Error("A requested secret is high-security and cannot be injected locally — it can only be used through the HQ secret proxy, which keeps the plaintext server-side.");
258
+ }
254
259
  if (res.status >= 400 &&
255
260
  res.status < 500 &&
256
261
  typeof body.code === "string") {
@@ -280,6 +285,15 @@ export async function loadRevealedSecrets(token, companyUid, keys, usage) {
280
285
  if (resolved.has(key))
281
286
  continue;
282
287
  const err = errorsByName.get(key);
288
+ // High-security ("nuclear") secret: the server refuses to vend it on the
289
+ // local-injection (batch-load) path — per-name code `high_security_denied`,
290
+ // no plaintext returned. Every caller of loadRevealedSecrets injects or
291
+ // prints the plaintext locally (`secrets get --reveal`, `secrets exec`,
292
+ // `secrets env`), so a high-security secret can NEVER be used here. Surface
293
+ // a clear, actionable error pointing at the proxy instead of a raw failure.
294
+ if (err?.code === "high_security_denied") {
295
+ throw new Error(`Secret '${key}' is high-security and cannot be injected locally — it can only be used through the HQ secret proxy, which keeps the plaintext server-side.`);
296
+ }
283
297
  const reason = err?.code === "not_found"
284
298
  ? "Secret not found"
285
299
  : err?.code === "forbidden"
@@ -364,6 +378,15 @@ export function registerSecretsCommand(program) {
364
378
  });
365
379
  if (!res.ok) {
366
380
  const body = (await res.json().catch(() => ({})));
381
+ // High-security ("nuclear") secret: the server refuses to reveal it on
382
+ // the local-injection path (403, no plaintext). Surface a clear,
383
+ // actionable error pointing the user at the proxy rather than a raw
384
+ // 4xx — the value can ONLY be used through the server-side proxy.
385
+ if (res.status === 403 && body.highSecurity === true) {
386
+ console.error(chalk.red(`Secret '${name}' is high-security and cannot be revealed locally.`));
387
+ console.error(chalk.dim(" It can only be used through the HQ secret proxy, which keeps the plaintext server-side."));
388
+ process.exit(1);
389
+ }
367
390
  console.error(chalk.red(`Failed to get secret: ${extractApiMessage(body, res.statusText)}`));
368
391
  process.exit(1);
369
392
  }
@@ -1125,4 +1148,4 @@ export function registerSecretsCommand(program) {
1125
1148
  });
1126
1149
  }
1127
1150
  //# sourceMappingURL=secrets.js.map
1128
- //# debugId=d01cb1a1-b851-5ffe-9608-e505e55dcdc8
1151
+ //# debugId=3049e431-c5db-5430-90fe-82866c35e95b
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]="964b8c71-056c-51fb-893a-d51f8f6ad5d9")}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]="aeb143c7-7e19-5999-b6cb-790fc79034cc")}catch(e){}}();
7
7
  import { Command } from "commander";
8
8
  import { initSentry, Sentry } from "./sentry.js";
9
9
  import { registerAddCommand } from "./commands/add.js";
@@ -36,6 +36,7 @@ import { registerGroupGrantsCommand } from "./commands/group-grants.js";
36
36
  import { registerFilesCommand } from "./commands/files.js";
37
37
  import { registerFilesBrowseCommands } from "./commands/files-browse.js";
38
38
  import { registerMembersCommand } from "./commands/members.js";
39
+ import { registerPeopleCommand } from "./commands/people.js";
39
40
  import { registerDmCommand } from "./commands/dm.js";
40
41
  import { registerFeedbackCommand } from "./commands/feedback.js";
41
42
  import { registerMeetingsCommand } from "./commands/meetings.js";
@@ -134,6 +135,9 @@ const filesCmd = registerFilesCommand(program);
134
135
  registerFilesBrowseCommands(filesCmd);
135
136
  // Membership management (subcommand group — hq members invite|list|revoke)
136
137
  registerMembersCommand(program);
138
+ // People directory (subcommand group — hq people list|search|resolve), reading
139
+ // the local companies/<co>/people store scoped to one company.
140
+ registerPeopleCommand(program);
137
141
  registerDmCommand(program);
138
142
  // Onboarding (top-level — Cognito + vault-service provisioning)
139
143
  registerOnboardCommand(program);
@@ -180,4 +184,4 @@ registerRescueCommand(program);
180
184
  }
181
185
  })();
182
186
  //# sourceMappingURL=index.js.map
183
- //# debugId=964b8c71-056c-51fb-893a-d51f8f6ad5d9
187
+ //# debugId=aeb143c7-7e19-5999-b6cb-790fc79034cc
@@ -1,5 +1,5 @@
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]="63aeb21a-b1e7-5bff-8c7d-560fad3cc0dd")}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]="20ece834-f9b5-5295-8620-87eb4f13ba7c")}catch(e){}}();
3
3
  import { ResolutionError } from 'varlock/plugin-lib';
4
4
  import { DEFAULT_SECRETS_CACHE_TTL_MS, readCache, writeCache, } from '../utils/secrets-cache.js';
5
5
  function normalizeCacheTtlMs(cacheTtlMs) {
@@ -59,6 +59,13 @@ export function installHqPlugin(graph /* EnvGraph */, opts) {
59
59
  if (err.code === 'not_found') {
60
60
  throw new ResolutionError(`Secret "${secretName}" does not exist in company`);
61
61
  }
62
+ // High-security ("nuclear") secret: the server refuses to vend it on
63
+ // the local-injection path. It can ONLY be used through the
64
+ // server-side proxy, so `hq run` (which injects plaintext into the
65
+ // child env) can never load it. Surface a clear, actionable error.
66
+ if (err.code === 'high_security_denied') {
67
+ throw new ResolutionError(`Secret "${secretName}" is high-security and cannot be injected locally — it can only be used via the HQ secret proxy (POST /secrets/{companyUid}/proxy/{path}), which keeps the plaintext server-side. Remove it from this schema's locally-injected vars.`);
68
+ }
62
69
  throw new ResolutionError(`Failed to load secret "${secretName}": ${err.message ?? err.code}`);
63
70
  }
64
71
  // Sentinel-check style throughout: `readCache` returns `string | null`
@@ -158,4 +165,4 @@ export async function prewarmHqSecrets(graph /* EnvGraph */, opts, state) {
158
165
  state.uid = uid;
159
166
  }
160
167
  //# sourceMappingURL=hq-plugin.js.map
161
- //# debugId=63aeb21a-b1e7-5bff-8c7d-560fad3cc0dd
168
+ //# debugId=20ece834-f9b5-5295-8620-87eb4f13ba7c
@@ -1,2 +1,2 @@
1
- export declare const BUNDLED_DSN = "https://ed8c1f7624b67945b43d485bbf98d0f7@o4507292345892864.ingest.us.sentry.io/4511263744786432";
1
+ export declare const BUNDLED_DSN = "https://467061dc39fb64644f5640a82d8949cf@o4507292345892864.ingest.us.sentry.io/4511597516226560";
2
2
  //# sourceMappingURL=sentry-dsn.generated.d.ts.map
@@ -1,5 +1,5 @@
1
1
 
2
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]="55f2362e-5cf7-5dac-b6a3-ef0f9ccde645")}catch(e){}}();
3
- export const BUNDLED_DSN = "https://ed8c1f7624b67945b43d485bbf98d0f7@o4507292345892864.ingest.us.sentry.io/4511263744786432";
3
+ export const BUNDLED_DSN = "https://467061dc39fb64644f5640a82d8949cf@o4507292345892864.ingest.us.sentry.io/4511597516226560";
4
4
  //# sourceMappingURL=sentry-dsn.generated.js.map
5
5
  //# debugId=55f2362e-5cf7-5dac-b6a3-ef0f9ccde645
@@ -1,3 +1,4 @@
1
+ import { type VersionInfo } from "./feedback-versions.js";
1
2
  export interface GitContext {
2
3
  branch: string | null;
3
4
  head: string | null;
@@ -6,6 +7,12 @@ export interface GitContext {
6
7
  }
7
8
  export interface DiagnosticsBlob {
8
9
  cliVersion: string;
10
+ /**
11
+ * The hq-cli, hq-core, and hq-sync versions from the submitter's
12
+ * environment. `cliVersion` above is retained for back-compat; new
13
+ * consumers should read `versions` (which carries core + sync too).
14
+ */
15
+ versions: VersionInfo;
9
16
  nodeVersion: string;
10
17
  os: {
11
18
  platform: string;
@@ -1,9 +1,10 @@
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]="ee39c63b-ddaa-553d-b5db-77633d323d88")}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]="ae4c924b-fe3d-558c-8b44-e1bac1f5a73e")}catch(e){}}();
3
3
  import * as os from "os";
4
4
  import { execFileSync } from "child_process";
5
5
  import { getRecentBreadcrumbs } from "./breadcrumb-buffer.js";
6
6
  import { CLI_VERSION } from "../cli-version.js";
7
+ import { collectVersions } from "./feedback-versions.js";
7
8
  const SECRET_FLAGS = new Set([
8
9
  "--token",
9
10
  "--secret",
@@ -79,6 +80,7 @@ function collectGitContext() {
79
80
  export function collectDiagnostics() {
80
81
  return {
81
82
  cliVersion: CLI_VERSION,
83
+ versions: collectVersions(),
82
84
  nodeVersion: process.version,
83
85
  os: {
84
86
  platform: os.platform(),
@@ -92,4 +94,4 @@ export function collectDiagnostics() {
92
94
  };
93
95
  }
94
96
  //# sourceMappingURL=feedback-diagnostics.js.map
95
- //# debugId=ee39c63b-ddaa-553d-b5db-77633d323d88
97
+ //# debugId=ae4c924b-fe3d-558c-8b44-e1bac1f5a73e
@@ -0,0 +1,23 @@
1
+ export declare const MAX_SCREENSHOTS = 5;
2
+ export declare const MAX_SCREENSHOT_BYTES: number;
3
+ export declare function contentTypeForPath(filePath: string): string;
4
+ export interface ScreenshotInput {
5
+ path: string;
6
+ contentType: string;
7
+ bytes: Buffer;
8
+ }
9
+ /** Validate + read the given screenshot paths (count, type, existence, size). */
10
+ export declare function loadScreenshots(paths: string[]): ScreenshotInput[];
11
+ /**
12
+ * Validate the screenshot paths, request presigned PUT URLs from the feedback
13
+ * endpoint, upload each image direct to S3, and return the object keys to
14
+ * attach to the feedback submission. Returns [] for no screenshots.
15
+ *
16
+ * `fetchImpl` is injectable for tests; defaults to the global fetch.
17
+ */
18
+ export declare function uploadScreenshots(opts: {
19
+ paths: string[];
20
+ token: string;
21
+ fetchImpl?: typeof fetch;
22
+ }): Promise<string[]>;
23
+ //# sourceMappingURL=feedback-screenshots.d.ts.map