@indigoai-us/hq-cli 5.345.57 → 5.345.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.58] — 2026-10-09
6
+ - hq access now accepts `--as <member>` to view another member's access, read-only.
7
+
5
8
  ## [5.345.57] — 2026-10-09
6
9
  - Add `hq lanes create --loop` and `hq lanes enqueue` for persistent worker lanes that process FIFO phase envelopes, return generic results or section 8 handoffs, reuse sessions per story, retry one stall, and park a second stall for the senior. Loop behavior uses the default-on `lanes.conduct-loop` hq-flags kill switch. See `docs/lanes.md` for command output and exit codes.
7
10
 
@@ -2871,6 +2871,10 @@ export declare const COMMAND_CATALOG: readonly [{
2871
2871
  readonly flags: "--company <slug>";
2872
2872
  readonly description: "Company slug";
2873
2873
  readonly hidden: false;
2874
+ }, {
2875
+ readonly flags: "--as <person-uid>";
2876
+ readonly description: "Check a person's access (exact prs_ or agt_ UID; self or owner/admin)";
2877
+ readonly hidden: false;
2874
2878
  }, {
2875
2879
  readonly flags: "--json";
2876
2880
  readonly description: "Print a single JSON object";
@@ -3462,6 +3462,11 @@ export const COMMAND_CATALOG = [
3462
3462
  "description": "Company slug",
3463
3463
  "hidden": false
3464
3464
  },
3465
+ {
3466
+ "flags": "--as <person-uid>",
3467
+ "description": "Check a person's access (exact prs_ or agt_ UID; self or owner/admin)",
3468
+ "hidden": false
3469
+ },
3465
3470
  {
3466
3471
  "flags": "--json",
3467
3472
  "description": "Print a single JSON object",
@@ -135,6 +135,15 @@ export interface RunAccessResult {
135
135
  alreadyAskedAt?: string;
136
136
  requestNote?: string;
137
137
  }
138
+ export interface AccessAsResult {
139
+ personUid: string;
140
+ company: string;
141
+ path: string;
142
+ permission: "read" | "write" | "admin" | null;
143
+ canRead: boolean;
144
+ roleBypass: boolean;
145
+ exitCode: 0 | 3;
146
+ }
138
147
  export declare function outcomeExitCode(outcome: AccessOutcome): 0 | 2 | 3 | 4;
139
148
  export declare function formatAccessOutcome(result: RunAccessResult): string;
140
149
  export declare function resolveGrantor(tree: AccessAclTree | null, members: AccessMember[]): AccessGrantor | null;
@@ -149,6 +158,11 @@ export declare function resolveAccessCompany(target: string, opts?: {
149
158
  company?: string;
150
159
  hqRoot?: string;
151
160
  }): string | undefined;
161
+ export declare function runAccessForPerson(rawPath: string, personUid: string, opts?: {
162
+ company?: string;
163
+ hqRoot?: string;
164
+ }): Promise<AccessAsResult>;
165
+ export declare function formatAccessForPerson(result: AccessAsResult): string;
152
166
  export declare function runAccessForPath(path: string, opts?: {
153
167
  company?: string;
154
168
  hqRoot?: string;
@@ -20,7 +20,7 @@ import * as readline from "node:readline";
20
20
  import { ListObjectsV2Command, } from "@aws-sdk/client-s3";
21
21
  import { DEFAULT_HQ_ROOT } from "../utils/cognito-session.js";
22
22
  import { getCompanyUid, vaultApiFetch, resolveCallerUid } from "../utils/vault-api.js";
23
- import { parseCompanySlugFromPath, resolveBrowseSession, makeCompanyPresignFactory, runGet, } from "./files-browse.js";
23
+ import { parseCompanySlugFromPath, classifyAclSource, resolveBrowseSession, makeCompanyPresignFactory, runGet, toBucketRelative, } from "./files-browse.js";
24
24
  import { listJournals, syncDoctor, } from "@indigoai-us/hq-cloud";
25
25
  import { listActiveMembers } from "./members.js";
26
26
  import { buildDmBody } from "./dm.js";
@@ -87,6 +87,10 @@ function normalizeTarget(raw, slug) {
87
87
  t = t.slice(anchor.length);
88
88
  return t;
89
89
  }
90
+ function fileGrantCoversPath(grantPath, targetPath) {
91
+ return classifyAclSource(targetPath, [grantPath]) !== "role-bypass";
92
+ }
93
+ const FILE_PERMISSION_RANK = { read: 1, write: 2, admin: 3 };
90
94
  function httpStatusOf(err) {
91
95
  if (!err || typeof err !== "object")
92
96
  return undefined;
@@ -637,6 +641,7 @@ export function registerAccessCommand(program) {
637
641
  .command("access <path-or-query>")
638
642
  .description("Tell whether a vault file exists and whether you can read it. Never a bare not-found. Exit codes: 0 local/not-synced, 2 never-existed, 3 no-access, 4 ambiguous.")
639
643
  .option("--company <slug>", "Company slug")
644
+ .option("--as <person-uid>", "Check a person's access (exact prs_ or agt_ UID; self or owner/admin)")
640
645
  .option("--json", "Print a single JSON object")
641
646
  .option("--no-fix", "Diagnose only: do not fetch, pin, or repair sync")
642
647
  .option("--yes", "Answer the one send-confirmation yes (hosts must pass this explicitly)")
@@ -650,6 +655,12 @@ Exit codes:
650
655
  `)
651
656
  .action(async (target, opts) => {
652
657
  try {
658
+ if (opts.as) {
659
+ const result = await runAccessForPerson(target, opts.as, opts);
660
+ console.log(opts.json ? JSON.stringify(result) : formatAccessForPerson(result));
661
+ process.exitCode = result.exitCode;
662
+ return;
663
+ }
653
664
  const result = await runAccessForPath(target, opts);
654
665
  process.exitCode = result.exitCode;
655
666
  }
@@ -669,6 +680,63 @@ export function resolveAccessCompany(target, opts) {
669
680
  const hqRoot = opts?.hqRoot ?? DEFAULT_HQ_ROOT;
670
681
  return opts?.company ?? slugFromTarget(target) ?? readActiveCompany(hqRoot);
671
682
  }
683
+ export async function runAccessForPerson(rawPath, personUid, opts) {
684
+ const uid = personUid.trim();
685
+ if (!/^(?:prs|agt)_[A-Za-z0-9_-]+$/.test(uid)) {
686
+ throw new Error("Pass the person's exact prs_ or agt_ UID to --as.");
687
+ }
688
+ const pathSlug = slugFromTarget(rawPath);
689
+ if (opts?.company && pathSlug && opts.company !== pathSlug) {
690
+ throw new Error("The --company value does not match the company in the path.");
691
+ }
692
+ const company = resolveAccessCompany(rawPath, opts);
693
+ if (!company)
694
+ throw new Error("Pass --company <slug> or an anchored company path.");
695
+ const relativePath = normalizeTarget(rawPath, company);
696
+ if (!relativePath || relativePath.split(/[\\/]/).includes("..")) {
697
+ throw new Error("Pass a company-relative path without parent-directory segments.");
698
+ }
699
+ const { token } = await resolveBrowseSession();
700
+ const companyUid = await getCompanyUid(token, company);
701
+ const response = await vaultApiFetch({
702
+ token,
703
+ path: `/files/${encodeURIComponent(companyUid)}/members/${encodeURIComponent(uid)}/access`,
704
+ method: "GET",
705
+ });
706
+ if (!response.ok) {
707
+ if (response.status === 403) {
708
+ throw new Error("Only the person themself or a company owner/admin can check this access.");
709
+ }
710
+ if (response.status === 404) {
711
+ throw new Error("That person is not an active member of this company.");
712
+ }
713
+ throw new Error(`Member access check failed (${response.status}).`);
714
+ }
715
+ const projection = (await response.json());
716
+ if (projection.personUid !== uid || !projection.files) {
717
+ throw new Error("The member access response did not match the requested person.");
718
+ }
719
+ const roleBypass = projection.files.roleBypass === true;
720
+ const grants = Array.isArray(projection.files.grants) ? projection.files.grants : [];
721
+ const matchingPermissions = grants
722
+ .filter((grant) => typeof grant.path === "string" && typeof grant.permission === "string" &&
723
+ grant.permission in FILE_PERMISSION_RANK &&
724
+ fileGrantCoversPath(toBucketRelative(grant.path, company), relativePath))
725
+ .map((grant) => grant.permission);
726
+ const permission = roleBypass ? "admin" : matchingPermissions.reduce((best, candidate) => best === null || FILE_PERMISSION_RANK[candidate] > FILE_PERMISSION_RANK[best] ? candidate : best, null);
727
+ return {
728
+ personUid: uid,
729
+ company,
730
+ path: `companies/${company}/${relativePath}`,
731
+ permission,
732
+ canRead: permission !== null,
733
+ roleBypass,
734
+ exitCode: permission === null ? 3 : 0,
735
+ };
736
+ }
737
+ export function formatAccessForPerson(result) {
738
+ return `${result.personUid} has ${result.permission ?? "no access"} access to ${result.path}.`;
739
+ }
672
740
  export async function runAccessForPath(path, opts) {
673
741
  const target = path;
674
742
  const hqRoot = opts?.hqRoot ?? DEFAULT_HQ_ROOT;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.57",
3
+ "version": "5.345.58",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {