@indigoai-us/hq-cli 5.52.0 → 5.53.1

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.
@@ -468,21 +468,39 @@ export function registerFilesCommand(program: Command): Command {
468
468
  files
469
469
  .command("delete <prefix>")
470
470
  .description(
471
- "Delete vault objects under a prefix (bounded + scoped). Always previews the exact count first; prompts for confirmation unless --yes.",
471
+ "Delete vault objects under a prefix (bounded + scoped). Always previews the exact count first; prompts for confirmation unless --yes. Use --personal to target your own personal vault instead of a company.",
472
472
  )
473
473
  .option(
474
474
  "--dry-run",
475
475
  "List what WOULD be deleted without deleting anything",
476
476
  )
477
+ .option(
478
+ "--personal",
479
+ "Target your own personal vault instead of a company vault (mutually exclusive with --company)",
480
+ )
477
481
  .option("-y, --yes", "Skip the confirmation prompt (for scripts)")
478
482
  .action(
479
- async (prefix: string, opts: { dryRun?: boolean; yes?: boolean }) => {
483
+ async (
484
+ prefix: string,
485
+ opts: { dryRun?: boolean; yes?: boolean; personal?: boolean },
486
+ ) => {
480
487
  try {
488
+ const companySlug = files.opts().company as string | undefined;
489
+ const personal = opts.personal === true;
490
+ if (personal && companySlug) {
491
+ console.error(
492
+ chalk.red(
493
+ "Pass either --personal or --company, not both.",
494
+ ),
495
+ );
496
+ process.exit(1);
497
+ }
481
498
  await runFilesDelete({
482
499
  prefix,
483
500
  dryRun: opts.dryRun === true,
484
501
  yes: opts.yes === true,
485
- companySlug: files.opts().company as string | undefined,
502
+ personal,
503
+ companySlug,
486
504
  });
487
505
  } catch (err) {
488
506
  console.error(
@@ -783,10 +801,16 @@ function realConfirm(message: string): Promise<boolean> {
783
801
  /**
784
802
  * POST /v1/files/delete. Throws FilesDeleteHttpError on any non-2xx so the one
785
803
  * caller renders a single consistent error path.
804
+ *
805
+ * Scope is EITHER a company vault (`companyUid` set) OR the caller's personal
806
+ * vault (`personal: true`). For the personal case the server resolves the target
807
+ * person + bucket from the authenticated caller — we send NO uid, just the
808
+ * `personal` flag — so there is nothing for the client to get wrong or spoof.
786
809
  */
787
810
  async function callDeleteEndpoint(params: {
788
811
  token: string;
789
- companyUid: string;
812
+ companyUid?: string;
813
+ personal?: boolean;
790
814
  prefix: string;
791
815
  dryRun: boolean;
792
816
  }): Promise<FilesDeleteResponse> {
@@ -794,11 +818,17 @@ async function callDeleteEndpoint(params: {
794
818
  token: params.token,
795
819
  path: "/v1/files/delete",
796
820
  method: "POST",
797
- body: {
798
- company: params.companyUid,
799
- prefix: params.prefix,
800
- dryRun: params.dryRun,
801
- },
821
+ body: params.personal
822
+ ? {
823
+ personal: true,
824
+ prefix: params.prefix,
825
+ dryRun: params.dryRun,
826
+ }
827
+ : {
828
+ company: params.companyUid,
829
+ prefix: params.prefix,
830
+ dryRun: params.dryRun,
831
+ },
802
832
  });
803
833
  if (!res.ok) {
804
834
  const body = (await res.json().catch(() => ({}))) as Record<string, string>;
@@ -815,6 +845,8 @@ interface RunFilesDeleteParams {
815
845
  prefix: string;
816
846
  dryRun: boolean;
817
847
  yes: boolean;
848
+ /** Target the caller's personal vault instead of a company vault. */
849
+ personal?: boolean;
818
850
  companySlug: string | undefined;
819
851
  }
820
852
 
@@ -896,7 +928,14 @@ export async function runFilesDelete(
896
928
  }
897
929
 
898
930
  const token = await ensureCognitoToken();
899
- const companyUid = await getCompanyUid(token, params.companySlug);
931
+ // Personal scope resolves the vault server-side from the caller's identity —
932
+ // no company to look up. Company scope resolves the companyUid as before.
933
+ const companyUid = params.personal
934
+ ? undefined
935
+ : await getCompanyUid(token, params.companySlug);
936
+ const scopeArgs = params.personal
937
+ ? { personal: true as const }
938
+ : { companyUid };
900
939
 
901
940
  // 1. Always preview first — this is how we print the EXACT key count before
902
941
  // deleting anything (and the whole behavior of --dry-run).
@@ -904,7 +943,7 @@ export async function runFilesDelete(
904
943
  try {
905
944
  preview = await callDeleteEndpoint({
906
945
  token,
907
- companyUid,
946
+ ...scopeArgs,
908
947
  prefix: normalized,
909
948
  dryRun: true,
910
949
  });
@@ -956,7 +995,9 @@ export async function runFilesDelete(
956
995
 
957
996
  if (!params.yes) {
958
997
  const ok = await confirm(
959
- `Delete ${preview.matched} ${noun}? This removes them from the shared vault for everyone.`,
998
+ params.personal
999
+ ? `Delete ${preview.matched} ${noun}? This removes them from your personal vault.`
1000
+ : `Delete ${preview.matched} ${noun}? This removes them from the shared vault for everyone.`,
960
1001
  );
961
1002
  if (!ok) {
962
1003
  console.log(chalk.dim("Aborted — nothing was deleted."));
@@ -969,7 +1010,7 @@ export async function runFilesDelete(
969
1010
  try {
970
1011
  result = await callDeleteEndpoint({
971
1012
  token,
972
- companyUid,
1013
+ ...scopeArgs,
973
1014
  prefix: normalized,
974
1015
  dryRun: false,
975
1016
  });
@@ -1051,11 +1051,21 @@ describe("registerMembersCommand promote", () => {
1051
1051
  expect(logSpy).toHaveBeenCalledWith(
1052
1052
  expect.stringContaining("Updated role for 'alice@example.com' to admin"),
1053
1053
  );
1054
+ // Clarifies that admins already have full context access automatically
1055
+ // (no separate grant) and that sync mode governs local visibility.
1056
+ expect(logSpy).toHaveBeenCalledWith(
1057
+ expect.stringContaining(
1058
+ "Admins automatically have full read and write access",
1059
+ ),
1060
+ );
1061
+ expect(logSpy).toHaveBeenCalledWith(
1062
+ expect.stringContaining("governed by their sync mode"),
1063
+ );
1054
1064
  });
1055
1065
 
1056
1066
  it("forwards a personUid target and the guest role (beyond set-role's admin|member cap)", async () => {
1057
1067
  fetchSpy.mockResolvedValueOnce(jsonResponse(200, {}));
1058
- vi.spyOn(console, "log").mockImplementation(() => undefined);
1068
+ const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);
1059
1069
 
1060
1070
  await buildMembersProgram().parseAsync(
1061
1071
  ["members", "--company", "acme", "promote", "prs_bob", "guest"],
@@ -1070,6 +1080,11 @@ describe("registerMembersCommand promote", () => {
1070
1080
  membershipKey: "prs_bob#cmp_acme",
1071
1081
  newRole: "guest",
1072
1082
  });
1083
+ // The full-access clarification applies ONLY to the role-bypass roles
1084
+ // (owner/admin) — a guest/member promotion must not claim full access.
1085
+ expect(logSpy).not.toHaveBeenCalledWith(
1086
+ expect.stringContaining("full read and write access"),
1087
+ );
1073
1088
  });
1074
1089
 
1075
1090
  it("rejects an invalid role at the CLI and exits non-zero without a network call", async () => {
@@ -575,6 +575,19 @@ export function registerMembersCommand(program: Command): void {
575
575
  console.log(
576
576
  chalk.green(`Updated role for '${target}' to ${role}`),
577
577
  );
578
+ if (role === "owner" || role === "admin") {
579
+ // Clarify the common confusion: owners/admins already get full
580
+ // access to ALL company context automatically via the role bypass
581
+ // (hq-pro resolveEffectivePermission) — there is no separate context
582
+ // grant to give them. What they SEE locally is governed by their sync
583
+ // mode, not by access.
584
+ console.log(
585
+ chalk.dim(
586
+ `${role === "owner" ? "Owners" : "Admins"} automatically have full read and write access to all of this company's context — no separate context grant is needed.\n` +
587
+ `What they actually see synced to their machine is governed by their sync mode (\`hq sync mode\`), not by access.`,
588
+ ),
589
+ );
590
+ }
578
591
  } catch (err) {
579
592
  if (err instanceof InviteHttpError) {
580
593
  console.error(chalk.red(err.message));
package/src/index.ts CHANGED
@@ -48,6 +48,8 @@ import { registerSignalsCommand } from "./commands/signals.js";
48
48
  import { registerReindexCommand } from "./commands/reindex.js";
49
49
  import { registerRescueCommand } from "./commands/rescue.js";
50
50
  import { registerMcpCommand } from "./commands/mcp-status.js";
51
+ import { registerCrmCommand } from "./commands/crm.js";
52
+ import { registerCompanyCommand } from "./commands/company.js";
51
53
  import { sanitizeArgv } from "./utils/feedback-diagnostics.js";
52
54
  import { environmentalFsErrorMessage } from "./utils/environmental-error.js";
53
55
  import { isEpipe } from "./utils/epipe.js";
@@ -214,6 +216,15 @@ registerRescueCommand(program);
214
216
  // off the configs, NOT linkStatus), with secret-redacted output + `--json`.
215
217
  registerMcpCommand(program);
216
218
 
219
+ // Native CRM entity upsert (subcommand group — `hq crm entity upsert`). Wraps
220
+ // POST /crm/entities (the ontology write gate) so an authenticated company
221
+ // member can create/update canonical CRM entities in the company vault.
222
+ registerCrmCommand(program);
223
+
224
+ // Company settings (subcommand group — `hq company settings set`). Owner-only
225
+ // toggles for crmEnabled / ontologyEnabled via PUT /company-settings.
226
+ registerCompanyCommand(program);
227
+
217
228
  (async () => {
218
229
  try {
219
230
  Sentry.addBreadcrumb({