@indigoai-us/hq-cli 5.345.164 → 5.345.166

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 (35) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/assets/scaffold/core/scripts/rebuild-all-indexes.sh +11 -0
  3. package/assets/scaffold/core/scripts/rebuild-orchestrator-index.sh +11 -0
  4. package/assets/scaffold/core/scripts/rebuild-public-knowledge-index.sh +11 -0
  5. package/assets/scaffold/core/scripts/rebuild-reports-index.sh +11 -0
  6. package/assets/scaffold/core/scripts/rebuild-social-drafts-index.sh +11 -0
  7. package/assets/scaffold/core/scripts/rebuild-threads-index.sh +11 -0
  8. package/assets/scaffold/core/scripts/rebuild-workers-index.sh +11 -0
  9. package/dist/command-catalog.generated.d.ts +66 -0
  10. package/dist/command-catalog.generated.js +82 -0
  11. package/dist/commands/billing-sponsor.d.ts +20 -0
  12. package/dist/commands/billing-sponsor.js +116 -0
  13. package/dist/commands/integrations-api.d.ts +15 -0
  14. package/dist/commands/integrations-api.js +27 -0
  15. package/dist/commands/integrations-manage.js +4 -2
  16. package/dist/commands/integrations.js +9 -5
  17. package/dist/commands/scan.d.ts +6 -0
  18. package/dist/commands/scan.js +11 -1
  19. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +210 -197
  20. package/dist/hq-cloud-sync-runner/package.json +1 -1
  21. package/dist/lib/doctor/checks/sync-runtime.d.ts +7 -2
  22. package/dist/lib/doctor/checks/sync-runtime.js +1 -3
  23. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  24. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  25. package/dist/lib/index-render/index.d.ts +5 -0
  26. package/dist/lib/index-render/index.js +22 -1
  27. package/dist/lib/secret-scan/detect.js +2 -0
  28. package/dist/lib/secret-scan/probes.d.ts +13 -0
  29. package/dist/lib/secret-scan/probes.js +69 -0
  30. package/dist/lib/secret-scan/repo-config.d.ts +24 -0
  31. package/dist/lib/secret-scan/repo-config.js +159 -29
  32. package/dist/lib/secret-scan/rules.d.ts +2 -0
  33. package/dist/lib/secret-scan/staged.d.ts +15 -1
  34. package/dist/lib/secret-scan/staged.js +46 -4
  35. package/package.json +8 -8
package/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.166] — 2026-10-24
6
+ - `hq integrations approve` now says why an approved call did not run. When an app refuses it, the CLI prints the app's own reason, such as Slack's refusal to send into a Slack Connect channel. Before this it printed only "failed (rejected)". With an older server it prints the rejection code instead. `hq integrations pending --history` adds a `Why:` line for decided calls that did not run. The approve command printed after a queued call keeps `--company` when you passed it. Before printing, the reason is cleaned of terminal escape codes, credentials and credential-bearing links, and it is capped at 500 characters. Feedback 15e7593c-50e5-4e6a-ae3a-342d618363c0.
7
+ - The bundled sync engine now uses hq-cloud 6.18.101, the current npm `latest`.
8
+ A 403 from the flags endpoint (for example a stale company in the local
9
+ manifest) is now held as `false` for an hour instead of being re-read and
10
+ logged on every telemetry pass (hq-cloud #1046).
11
+ - `hq doctor` no longer counts runner telemetry errors as sync receiver permission refusals. A failed outcome-flag read ("hq-flags: resolve returned 403") for a company you are not a member of used to inflate `sync.runtime.receiver-permission`; only the receiver's own events are counted now.
12
+ - The bundled shell fallbacks for `rebuild-index` all, orchestrator, public-knowledge, reports, social-drafts, threads, and workers now print the same usage as the native command for `--help`/`-h` and exit without writing. Before, they ignored the flag and rebuilt the index, which failed the published core command parity check.
13
+
14
+ ## [5.345.165] — 2026-10-23
15
+ - `hq billing sponsor grant <email|prs_id> --company <slug>` lets a company owner or admin put a member's personal plan on the company bill without a request first; `--all-members` does it for every active member and reports each person as covered, already covered, or skipped. `hq billing sponsor ungrant <email|prs_id>` takes it off again. hq-pro checks the owner/admin role and active membership.
16
+ - `hq scan secrets <files...>` takes `--repo <dir>`, which applies that repo's committed `.gitleaks.toml` allowlists to a file scan, matching each file by its path relative to the repo, as a staged scan does. `--snapshot-root <dir>` (with `--repo`) matches files laid out as `<dir>/<commit>/<repo path>`, the history copies `commit-secret-scan.sh pr-check` writes, as `<repo path>`. Findings still name the files as given. Before this, a file scan ignored the repo's allowlists, so pr-check failed on false positives the commit gate already allowed. Without `--repo` a file scan is unchanged.
17
+ - `--allowlist-rev <rev>` (with `--repo`) reads `.gitleaks.toml` from that commit instead of HEAD; pr-check passes its base branch, so a branch cannot exempt its own earlier commits by adding an allowlist later. An unknown rev exits 2.
18
+ - A file that resolves (after `..` and symlinks) outside the repo and the snapshot root gets no repo allowlist, nor does a snapshot file without a commit folder.
19
+ - A file scan with `--repo` honors only exact-value exemptions. Each allowlist needs `condition = "AND"` and specific `paths`, and lists in `stopwords` the exact secret values it allows, such as known manifest digests (at least 16 characters, no spaces). A detected secret is allowed only when it equals one of them, so other secrets in the same file are still reported. An allowlist with `regexes`, `regexTarget`, `targetRules` or `commits`, or with paths alone, is refused and the scan exits 2, because patterns, alone or combined across entries, can hide a real credential. Staged scans keep the full allowlist format; there each allowlist with content checks is judged by running the real detector on a fixed set of realistic credentials and refused if it hides any of them.
20
+ - An allowlist validation error never prints an exemption value. It names the entry and position (for example `stopwords[0]`) with the value's length and a short SHA-256 prefix, so a credential typed into an allowlist by mistake does not reach terminal or CI logs.
21
+
5
22
  ## [5.345.164] — 2026-10-23
6
23
  - `hq billing status` and `hq billing usage --card` now show trial usage against the $500 trial cap, the $10 included usage, the spend limit and a prompt to add a card when usage passes the included amount. The plan line says "unavailable right now" instead of guessing Starter, invoice-billed companies read as a flat plan billed by invoice, and a failed read of a scheduled switch is reported.
7
24
  - `hq deploy domains` refuses a name that is really an IPv4 address. Inputs such as `1.2`, `0x7f.1` and `1.2.3.4` were rewritten to an address and accepted; a numeric last label is now refused before any request.
@@ -11,6 +11,17 @@
11
11
 
12
12
  set -uo pipefail
13
13
 
14
+ # Help must never write: print the same usage as `hq core rebuild-index all --help`.
15
+ for arg in "$@"; do
16
+ if [[ "$arg" == "--help" || "$arg" == "-h" ]]; then
17
+ printf '%s\n' \
18
+ 'Usage: hq core rebuild-index all [--accept]' \
19
+ '' \
20
+ 'Regenerate every INDEX.md across the HQ tree. Help performs no writes.'
21
+ exit 0
22
+ fi
23
+ done
24
+
14
25
  HQ_ROOT="${HQ_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
15
26
  cd "$HQ_ROOT"
16
27
 
@@ -12,6 +12,17 @@
12
12
 
13
13
  set -euo pipefail
14
14
 
15
+ # Help must never write: print the same usage as `hq core rebuild-index orchestrator --help`.
16
+ for arg in "$@"; do
17
+ if [[ "$arg" == "--help" || "$arg" == "-h" ]]; then
18
+ printf '%s\n' \
19
+ 'Usage: hq core rebuild-index orchestrator [--accept]' \
20
+ '' \
21
+ 'Regenerate the orchestrator workspace INDEX.md. Help performs no writes.'
22
+ exit 0
23
+ fi
24
+ done
25
+
15
26
  HQ_ROOT="${HQ_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
16
27
  cd "$HQ_ROOT"
17
28
 
@@ -7,6 +7,17 @@
7
7
 
8
8
  set -euo pipefail
9
9
 
10
+ # Help must never write: print the same usage as `hq core rebuild-index public-knowledge --help`.
11
+ for arg in "$@"; do
12
+ if [[ "$arg" == "--help" || "$arg" == "-h" ]]; then
13
+ printf '%s\n' \
14
+ 'Usage: hq core rebuild-index public-knowledge [--accept]' \
15
+ '' \
16
+ 'Regenerate core/knowledge/public/INDEX.md. Help performs no writes.'
17
+ exit 0
18
+ fi
19
+ done
20
+
10
21
  HQ_ROOT="${HQ_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
11
22
  cd "$HQ_ROOT"
12
23
 
@@ -8,6 +8,17 @@
8
8
 
9
9
  set -euo pipefail
10
10
 
11
+ # Help must never write: print the same usage as `hq core rebuild-index reports --help`.
12
+ for arg in "$@"; do
13
+ if [[ "$arg" == "--help" || "$arg" == "-h" ]]; then
14
+ printf '%s\n' \
15
+ 'Usage: hq core rebuild-index reports [--accept]' \
16
+ '' \
17
+ 'Regenerate the reports INDEX.md. Help performs no writes.'
18
+ exit 0
19
+ fi
20
+ done
21
+
11
22
  HQ_ROOT="${HQ_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
12
23
  cd "$HQ_ROOT"
13
24
 
@@ -8,6 +8,17 @@
8
8
 
9
9
  set -euo pipefail
10
10
 
11
+ # Help must never write: print the same usage as `hq core rebuild-index social-drafts --help`.
12
+ for arg in "$@"; do
13
+ if [[ "$arg" == "--help" || "$arg" == "-h" ]]; then
14
+ printf '%s\n' \
15
+ 'Usage: hq core rebuild-index social-drafts [--accept]' \
16
+ '' \
17
+ 'Regenerate workspace/social-drafts/INDEX.md. Help performs no writes.'
18
+ exit 0
19
+ fi
20
+ done
21
+
11
22
  HQ_ROOT="${HQ_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
12
23
  # shellcheck source=core/scripts/lib/portable.sh
13
24
  . "$HQ_ROOT/core/scripts/lib/portable.sh"
@@ -11,6 +11,17 @@
11
11
 
12
12
  set -euo pipefail
13
13
 
14
+ # Help must never write: print the same usage as `hq core rebuild-index threads --help`.
15
+ for arg in "$@"; do
16
+ if [[ "$arg" == "--help" || "$arg" == "-h" ]]; then
17
+ printf '%s\n' \
18
+ 'Usage: hq core rebuild-index threads [--accept]' \
19
+ '' \
20
+ 'Regenerate the session-threads INDEX.md. Help performs no writes.'
21
+ exit 0
22
+ fi
23
+ done
24
+
14
25
  HQ_ROOT="${HQ_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
15
26
  cd "$HQ_ROOT"
16
27
 
@@ -7,6 +7,17 @@
7
7
 
8
8
  set -euo pipefail
9
9
 
10
+ # Help must never write: print the same usage as `hq core rebuild-index workers --help`.
11
+ for arg in "$@"; do
12
+ if [[ "$arg" == "--help" || "$arg" == "-h" ]]; then
13
+ printf '%s\n' \
14
+ 'Usage: hq core rebuild-index workers [--accept]' \
15
+ '' \
16
+ 'Regenerate the workers INDEX.md. Help performs no writes.'
17
+ exit 0
18
+ fi
19
+ done
20
+
10
21
  HQ_ROOT="${HQ_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
11
22
  cd "$HQ_ROOT"
12
23
 
@@ -6277,6 +6277,18 @@ export declare const COMMAND_CATALOG: readonly [{
6277
6277
  readonly flags: "--json";
6278
6278
  readonly description: "Print findings as JSON";
6279
6279
  readonly hidden: false;
6280
+ }, {
6281
+ readonly flags: "--repo <dir>";
6282
+ readonly description: "With files: apply this repo's committed .gitleaks.toml allowlists, matching repo-relative paths";
6283
+ readonly hidden: false;
6284
+ }, {
6285
+ readonly flags: "--snapshot-root <dir>";
6286
+ readonly description: "With --repo: files under <dir>/<commit>/<path> are matched as <path>";
6287
+ readonly hidden: false;
6288
+ }, {
6289
+ readonly flags: "--allowlist-rev <rev>";
6290
+ readonly description: "With --repo: read .gitleaks.toml from this commit instead of HEAD";
6291
+ readonly hidden: false;
6280
6292
  }];
6281
6293
  readonly subcommands: readonly [];
6282
6294
  }];
@@ -8820,6 +8832,60 @@ export declare const COMMAND_CATALOG: readonly [{
8820
8832
  readonly hidden: false;
8821
8833
  }];
8822
8834
  readonly subcommands: readonly [];
8835
+ }, {
8836
+ readonly name: "grant";
8837
+ readonly description: "Admin: put a member's personal plan on the company bill, no request needed";
8838
+ readonly aliases: readonly [];
8839
+ readonly hidden: false;
8840
+ readonly helpOption: true;
8841
+ readonly helpCommand: false;
8842
+ readonly usage: "[options] [person]";
8843
+ readonly helpText: readonly [];
8844
+ readonly arguments: readonly [{
8845
+ readonly name: "person";
8846
+ readonly required: false;
8847
+ readonly variadic: false;
8848
+ readonly description: "Member email or prs_ id";
8849
+ }];
8850
+ readonly options: readonly [{
8851
+ readonly flags: "--company <slug>";
8852
+ readonly description: "Company slug (resolves to companyUid)";
8853
+ readonly hidden: false;
8854
+ }, {
8855
+ readonly flags: "--all-members";
8856
+ readonly description: "Sponsor every active member of the company";
8857
+ readonly hidden: false;
8858
+ }, {
8859
+ readonly flags: "--json";
8860
+ readonly description: "Emit raw JSON";
8861
+ readonly hidden: false;
8862
+ }];
8863
+ readonly subcommands: readonly [];
8864
+ }, {
8865
+ readonly name: "ungrant";
8866
+ readonly description: "Admin: take a member's personal plan off the company bill";
8867
+ readonly aliases: readonly [];
8868
+ readonly hidden: false;
8869
+ readonly helpOption: true;
8870
+ readonly helpCommand: false;
8871
+ readonly usage: "[options] <person>";
8872
+ readonly helpText: readonly [];
8873
+ readonly arguments: readonly [{
8874
+ readonly name: "person";
8875
+ readonly required: true;
8876
+ readonly variadic: false;
8877
+ readonly description: "Member email or prs_ id";
8878
+ }];
8879
+ readonly options: readonly [{
8880
+ readonly flags: "--company <slug>";
8881
+ readonly description: "Company slug (resolves to companyUid)";
8882
+ readonly hidden: false;
8883
+ }, {
8884
+ readonly flags: "--json";
8885
+ readonly description: "Emit raw JSON";
8886
+ readonly hidden: false;
8887
+ }];
8888
+ readonly subcommands: readonly [];
8823
8889
  }];
8824
8890
  }, {
8825
8891
  readonly name: "usage";
@@ -7634,6 +7634,21 @@ export const COMMAND_CATALOG = [
7634
7634
  "flags": "--json",
7635
7635
  "description": "Print findings as JSON",
7636
7636
  "hidden": false
7637
+ },
7638
+ {
7639
+ "flags": "--repo <dir>",
7640
+ "description": "With files: apply this repo's committed .gitleaks.toml allowlists, matching repo-relative paths",
7641
+ "hidden": false
7642
+ },
7643
+ {
7644
+ "flags": "--snapshot-root <dir>",
7645
+ "description": "With --repo: files under <dir>/<commit>/<path> are matched as <path>",
7646
+ "hidden": false
7647
+ },
7648
+ {
7649
+ "flags": "--allowlist-rev <rev>",
7650
+ "description": "With --repo: read .gitleaks.toml from this commit instead of HEAD",
7651
+ "hidden": false
7637
7652
  }
7638
7653
  ],
7639
7654
  "subcommands": []
@@ -10757,6 +10772,73 @@ export const COMMAND_CATALOG = [
10757
10772
  }
10758
10773
  ],
10759
10774
  "subcommands": []
10775
+ },
10776
+ {
10777
+ "name": "grant",
10778
+ "description": "Admin: put a member's personal plan on the company bill, no request needed",
10779
+ "aliases": [],
10780
+ "hidden": false,
10781
+ "helpOption": true,
10782
+ "helpCommand": false,
10783
+ "usage": "[options] [person]",
10784
+ "helpText": [],
10785
+ "arguments": [
10786
+ {
10787
+ "name": "person",
10788
+ "required": false,
10789
+ "variadic": false,
10790
+ "description": "Member email or prs_ id"
10791
+ }
10792
+ ],
10793
+ "options": [
10794
+ {
10795
+ "flags": "--company <slug>",
10796
+ "description": "Company slug (resolves to companyUid)",
10797
+ "hidden": false
10798
+ },
10799
+ {
10800
+ "flags": "--all-members",
10801
+ "description": "Sponsor every active member of the company",
10802
+ "hidden": false
10803
+ },
10804
+ {
10805
+ "flags": "--json",
10806
+ "description": "Emit raw JSON",
10807
+ "hidden": false
10808
+ }
10809
+ ],
10810
+ "subcommands": []
10811
+ },
10812
+ {
10813
+ "name": "ungrant",
10814
+ "description": "Admin: take a member's personal plan off the company bill",
10815
+ "aliases": [],
10816
+ "hidden": false,
10817
+ "helpOption": true,
10818
+ "helpCommand": false,
10819
+ "usage": "[options] <person>",
10820
+ "helpText": [],
10821
+ "arguments": [
10822
+ {
10823
+ "name": "person",
10824
+ "required": true,
10825
+ "variadic": false,
10826
+ "description": "Member email or prs_ id"
10827
+ }
10828
+ ],
10829
+ "options": [
10830
+ {
10831
+ "flags": "--company <slug>",
10832
+ "description": "Company slug (resolves to companyUid)",
10833
+ "hidden": false
10834
+ },
10835
+ {
10836
+ "flags": "--json",
10837
+ "description": "Emit raw JSON",
10838
+ "hidden": false
10839
+ }
10840
+ ],
10841
+ "subcommands": []
10760
10842
  }
10761
10843
  ]
10762
10844
  },
@@ -13,6 +13,13 @@
13
13
  * hq billing sponsor decline <id> — admin: say no
14
14
  * hq billing sponsor revoke <id> — admin: end an active sponsorship
15
15
  * hq billing sponsor withdraw <id> — me: take back my own request
16
+ * hq billing sponsor grant <person> --company <slug>
17
+ * — admin: sponsor a member's plan
18
+ * directly, no request needed
19
+ * hq billing sponsor grant --all-members --company <slug>
20
+ * — admin: sponsor every active member
21
+ * hq billing sponsor ungrant <person> --company <slug>
22
+ * — admin: end a direct sponsorship
16
23
  *
17
24
  * Every subcommand talks to the SAME hq-pro routes the console uses
18
25
  * (`/v1/billing/sponsorship/...`). hq-pro is the authoritative gate on all of
@@ -82,6 +89,19 @@ export interface SponsorshipSelfState {
82
89
  }
83
90
  /** Plain label for a request status. Never shows the raw enum. */
84
91
  export declare function statusLabel(status: string): string;
92
+ /** Per-person outcome of an owner/admin grant, as hq-pro returns it. */
93
+ export interface PlanGrantResult {
94
+ personUid: string;
95
+ resourceId: string;
96
+ outcome: "sponsored" | "already_sponsored" | "sponsored_by_another_company" | "usage_pricing" | "not_a_member";
97
+ }
98
+ /** Plain line for one grant outcome. */
99
+ export declare function grantOutcomeLabel(outcome: PlanGrantResult["outcome"]): string;
100
+ /**
101
+ * Turn `<person>` into a person uid: a `prs_` uid passes through, an email is
102
+ * looked up in the company's active members. hq-pro re-checks membership.
103
+ */
104
+ export declare function resolveMemberPersonUid(token: string, companyUid: string, person: string): Promise<string>;
85
105
  export declare function registerBillingSponsorCommands(billing: Command): void;
86
106
  export {};
87
107
  //# sourceMappingURL=billing-sponsor.d.ts.map
@@ -13,6 +13,13 @@
13
13
  * hq billing sponsor decline <id> — admin: say no
14
14
  * hq billing sponsor revoke <id> — admin: end an active sponsorship
15
15
  * hq billing sponsor withdraw <id> — me: take back my own request
16
+ * hq billing sponsor grant <person> --company <slug>
17
+ * — admin: sponsor a member's plan
18
+ * directly, no request needed
19
+ * hq billing sponsor grant --all-members --company <slug>
20
+ * — admin: sponsor every active member
21
+ * hq billing sponsor ungrant <person> --company <slug>
22
+ * — admin: end a direct sponsorship
16
23
  *
17
24
  * Every subcommand talks to the SAME hq-pro routes the console uses
18
25
  * (`/v1/billing/sponsorship/...`). hq-pro is the authoritative gate on all of
@@ -35,6 +42,7 @@ import { diagnosticsEnabled } from "../lib/diagnostics.js";
35
42
  import { ensureCognitoToken } from "../utils/cognito-session.js";
36
43
  import { isCompanySelectionError } from "../utils/company-selection-error.js";
37
44
  import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
45
+ import { listActiveMembers } from "./members.js";
38
46
  import { fetchCompanyPricingPlan, fetchPersonPricingPlan, isUsageOnly, usageOnlySponsorLines, } from "../lib/billing/usage-only.js";
39
47
  /** The request kind this command files. Matches hq-pro's contract. */
40
48
  export const PLAN_RESOURCE = "plan";
@@ -90,6 +98,9 @@ export function messageForError(status, body) {
90
98
  if (status === 403) {
91
99
  return "You're not allowed to do that. Only an owner or admin of the company can answer a sponsorship request.";
92
100
  }
101
+ if (status === 404 && code === "NOT_A_MEMBER") {
102
+ return "That person isn't an active member of this company.";
103
+ }
93
104
  if (status === 404)
94
105
  return "No sponsorship request with that id.";
95
106
  if (status === 409 && code === "DUPLICATE_PENDING_REQUEST") {
@@ -168,6 +179,41 @@ async function transition(requestId, op) {
168
179
  });
169
180
  return { ...data.request, code: data.code, addCardUrl: data.addCardUrl };
170
181
  }
182
+ /** Plain line for one grant outcome. */
183
+ export function grantOutcomeLabel(outcome) {
184
+ switch (outcome) {
185
+ case "sponsored":
186
+ return "Now on the company bill";
187
+ case "already_sponsored":
188
+ return "Already on the company bill";
189
+ case "sponsored_by_another_company":
190
+ return "Skipped: another company already covers them";
191
+ case "usage_pricing":
192
+ return "Skipped: on usage pricing, no personal plan to cover";
193
+ case "not_a_member":
194
+ return "Skipped: not an active member";
195
+ default:
196
+ return outcome;
197
+ }
198
+ }
199
+ /**
200
+ * Turn `<person>` into a person uid: a `prs_` uid passes through, an email is
201
+ * looked up in the company's active members. hq-pro re-checks membership.
202
+ */
203
+ export async function resolveMemberPersonUid(token, companyUid, person) {
204
+ const trimmed = person.trim();
205
+ if (/^prs_[A-Za-z0-9]+$/.test(trimmed))
206
+ return trimmed;
207
+ const email = trimmed.toLowerCase();
208
+ if (!/^[^\s]+@[^\s]+$/.test(email)) {
209
+ throw new SponsorError("Pass the person's email or their prs_ id.");
210
+ }
211
+ const members = await listActiveMembers(token, companyUid);
212
+ const match = members.find((m) => m.personEmail?.trim().toLowerCase() === email);
213
+ if (match?.personUid)
214
+ return match.personUid;
215
+ throw new SponsorError(`No active member has the email ${email}. Run \`hq members list\`.`);
216
+ }
171
217
  /** Whether the sponsoring company is on usage pricing. Unknown counts as no. */
172
218
  async function companyIsUsageOnly(companyUid) {
173
219
  if (!companyUid)
@@ -402,5 +448,75 @@ export function registerBillingSponsorCommands(billing) {
402
448
  fail(err);
403
449
  }
404
450
  });
451
+ sponsor
452
+ .command("grant")
453
+ .argument("[person]", "Member email or prs_ id")
454
+ .description("Admin: put a member's personal plan on the company bill, no request needed")
455
+ .option("--company <slug>", "Company slug (resolves to companyUid)")
456
+ .option("--all-members", "Sponsor every active member of the company")
457
+ .option("--json", "Emit raw JSON")
458
+ .action(async function (person, opts) {
459
+ try {
460
+ if (Boolean(person) === Boolean(opts.allMembers)) {
461
+ throw new SponsorError("Name one person, or pass --all-members.");
462
+ }
463
+ const token = await ensureCognitoToken();
464
+ const named = opts.company ?? billing.opts().company;
465
+ const companyUid = await getCompanyUid(token, named);
466
+ const body = { companyUid };
467
+ if (person)
468
+ body.personUid = await resolveMemberPersonUid(token, companyUid, person);
469
+ else
470
+ body.allMembers = true;
471
+ const data = await callApi(token, "/v1/billing/sponsorship/plans", { method: "POST", body });
472
+ if (opts.json) {
473
+ process.stdout.write(JSON.stringify(data, null, 2) + "\n");
474
+ return;
475
+ }
476
+ if (data.results.length === 0) {
477
+ console.log("No active members to sponsor.");
478
+ return;
479
+ }
480
+ for (const r of data.results) {
481
+ console.log(` ${chalk.bold(r.personUid)} ${grantOutcomeLabel(r.outcome)}`);
482
+ }
483
+ const added = data.results.filter((r) => r.outcome === "sponsored").length;
484
+ console.log(added === 1
485
+ ? "1 personal plan moved onto the company bill."
486
+ : `${added} personal plans moved onto the company bill.`);
487
+ const single = person ? data.results[0] : undefined;
488
+ if (single && single.outcome !== "sponsored" && single.outcome !== "already_sponsored") {
489
+ process.exitCode = 1;
490
+ }
491
+ }
492
+ catch (err) {
493
+ fail(err);
494
+ }
495
+ });
496
+ sponsor
497
+ .command("ungrant")
498
+ .argument("<person>", "Member email or prs_ id")
499
+ .description("Admin: take a member's personal plan off the company bill")
500
+ .option("--company <slug>", "Company slug (resolves to companyUid)")
501
+ .option("--json", "Emit raw JSON")
502
+ .action(async function (person, opts) {
503
+ try {
504
+ const token = await ensureCognitoToken();
505
+ const named = opts.company ?? billing.opts().company;
506
+ const companyUid = await getCompanyUid(token, named);
507
+ const personUid = await resolveMemberPersonUid(token, companyUid, person);
508
+ const data = await callApi(token, "/v1/billing/sponsorship/plans", { method: "DELETE", body: { companyUid, personUid } });
509
+ if (opts.json) {
510
+ process.stdout.write(JSON.stringify(data, null, 2) + "\n");
511
+ return;
512
+ }
513
+ console.log(data.revoked
514
+ ? "Ended. Their personal plan goes back to them from the next bill."
515
+ : "This company wasn't covering their plan. Nothing changed.");
516
+ }
517
+ catch (err) {
518
+ fail(err);
519
+ }
520
+ });
405
521
  }
406
522
  //# sourceMappingURL=billing-sponsor.js.map
@@ -292,6 +292,10 @@ export interface PendingApproval {
292
292
  status?: string;
293
293
  /** Owner-facing outcome projected by hq-pro for decided rows. */
294
294
  outcome?: string;
295
+ /** Closed rejection code for a rejected row (e.g. provider_tool_error); older servers omit it. */
296
+ rejectionReason?: string | null;
297
+ /** Plain reason a decided row did not run, such as the app's own refusal text; older servers omit it. */
298
+ failureMessage?: string | null;
295
299
  }
296
300
  /**
297
301
  * Read open approvals from hq-pro's confirm-queue state (not the audit feed).
@@ -307,4 +311,15 @@ export declare function mutateConnectionAccess(token: string, companyUid: string
307
311
  granteeId?: string;
308
312
  permission?: Permission;
309
313
  }): Promise<ConnectionAccess>;
314
+ /** Same cap the server applies to a failure reason; the CLI enforces it for older or malformed responses. */
315
+ export declare const TERMINAL_REASON_MAX_CHARS = 500;
316
+ /**
317
+ * Server-supplied text that reaches the terminal (a failure reason quoting an
318
+ * app's error). ANSI escapes, control and invisible formatting characters are
319
+ * removed, then the same credential-redaction chain the gateway diagnostics use
320
+ * (`redactErrorText`: credential URLs, bearer values, token assignments, known
321
+ * token shapes) is applied, and the result is capped. The server redacts too;
322
+ * this keeps an older or misbehaving server from printing a secret.
323
+ */
324
+ export declare function terminalSafeText(text: string): string;
310
325
  //# sourceMappingURL=integrations-api.d.ts.map
@@ -8,6 +8,7 @@
8
8
  * is optional and read defensively.
9
9
  */
10
10
  import { vaultApiFetch } from "../utils/vault-api.js";
11
+ import { redactErrorText } from "../utils/redact-error-text.js";
11
12
  import { IntegrationsCliError, raiseForResponse, } from "./integrations-core.js";
12
13
  export async function listCatalog(token, companyUid, opts = {}) {
13
14
  const query = {
@@ -188,4 +189,30 @@ export async function mutateConnectionAccess(token, companyUid, action, input) {
188
189
  }
189
190
  return (await res.json());
190
191
  }
192
+ // eslint-disable-next-line no-control-regex
193
+ const TERMINAL_ESCAPES = /\u001b\[[0-?]*[ -/]*[@-~]|\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)?|\u001b[@-_]/g;
194
+ // eslint-disable-next-line no-control-regex
195
+ const TERMINAL_CONTROL_CHARS = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g;
196
+ /** Bidirectional overrides, isolates, marks and zero-width characters, which can reorder or hide text. */
197
+ const TERMINAL_INVISIBLE_FORMAT_CHARS = /[\u200b-\u200f\u202a-\u202e\u2060-\u2069\ufeff]/g;
198
+ /** Same cap the server applies to a failure reason; the CLI enforces it for older or malformed responses. */
199
+ export const TERMINAL_REASON_MAX_CHARS = 500;
200
+ /**
201
+ * Server-supplied text that reaches the terminal (a failure reason quoting an
202
+ * app's error). ANSI escapes, control and invisible formatting characters are
203
+ * removed, then the same credential-redaction chain the gateway diagnostics use
204
+ * (`redactErrorText`: credential URLs, bearer values, token assignments, known
205
+ * token shapes) is applied, and the result is capped. The server redacts too;
206
+ * this keeps an older or misbehaving server from printing a secret.
207
+ */
208
+ export function terminalSafeText(text) {
209
+ const visible = text
210
+ .replace(TERMINAL_ESCAPES, "")
211
+ .replace(TERMINAL_CONTROL_CHARS, "")
212
+ .replace(TERMINAL_INVISIBLE_FORMAT_CHARS, "");
213
+ const redacted = redactErrorText(visible);
214
+ return redacted.length > TERMINAL_REASON_MAX_CHARS
215
+ ? `${redacted.slice(0, TERMINAL_REASON_MAX_CHARS - 1)}…`
216
+ : redacted;
217
+ }
191
218
  //# sourceMappingURL=integrations-api.js.map
@@ -18,7 +18,7 @@ import { ensureCognitoIdToken } from "../utils/cognito-session.js";
18
18
  import { getCompanyUid } from "../utils/vault-api.js";
19
19
  import { redactSensitiveUrl, redactUrlsInText } from "../utils/redact-url.js";
20
20
  import { IntegrationsCliError, bareProvider, printHealthNotice, fetchConnections, fetchAdminSurface, fetchAuditPage, AUDIT_PAGE_MAX, printJson, revokedConnectionDetails, resolveConnection, selectConnection, } from "./integrations-core.js";
21
- import { fetchPendingApprovals, getConnectionAccess, mutateConnectionAccess, purgeConnection, setReadSafe, uninstallIntegration, updateGovernance, } from "./integrations-api.js";
21
+ import { fetchPendingApprovals, getConnectionAccess, mutateConnectionAccess, purgeConnection, setReadSafe, terminalSafeText, uninstallIntegration, updateGovernance, } from "./integrations-api.js";
22
22
  const WRITE_POLICIES = ["auto-allow", "confirm", "deny"];
23
23
  const PERMISSIONS = ["read", "write", "admin"];
24
24
  /** Plain-English gloss for each write policy, used in every policy readout. */
@@ -617,9 +617,11 @@ export function registerManageCommands(integrations) {
617
617
  console.log(`${chalk.bold(label)} ${chalk.dim(`${who} · ${row.createdAt}`)}`);
618
618
  if (opts.history) {
619
619
  console.log(chalk.dim(` ${row.queueId} · ${row.status ?? row.outcome ?? "status unavailable"}`));
620
+ if (row.failureMessage)
621
+ console.log(chalk.dim(` Why: ${terminalSafeText(row.failureMessage)}`));
620
622
  }
621
623
  else {
622
- console.log(chalk.dim(` hq integrations approve ${row.queueId}${row.provider ? ` --provider ${bareProvider(row.provider)}` : ""}`));
624
+ console.log(chalk.dim(` hq integrations approve ${row.queueId}${row.provider ? ` --provider ${bareProvider(row.provider)}` : ""}${opts.company ? ` --company ${opts.company}` : ""}`));
623
625
  }
624
626
  }
625
627
  });