@indigoai-us/hq-cli 5.51.0 → 5.53.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
@@ -2,6 +2,47 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.53.0]
6
+
7
+ ### Added
8
+
9
+ - **`hq crm entity upsert` — write a client record into the native CRM from the
10
+ terminal.** Posts to `POST /crm/entities`, which runs through the ontology
11
+ write-gate (tenant-isolated, `crmEnabled`-gated) and refreshes the company's
12
+ CRM projection. `--company`, `--type` (`contact|deal|contract|invoice|company`)
13
+ and `--name` are required in flag form; `--attio-id|--stripe-id|--pandadoc-id|
14
+ --neon-id` populate `external_ids`; `--json <file|->` passes a full entity or
15
+ array instead. Surfaces a `CRM_DISABLED` 403 with an enable hint. This is the
16
+ build-native path for companies with no external CRM.
17
+ - **`hq company settings set` — toggle company settings from the CLI.** Calls
18
+ `PUT /company-settings` with `--crm-enabled <true|false>` and/or
19
+ `--ontology-enabled <true|false>` (at least one required; owner-gated
20
+ server-side). `--crm-enabled true` is how `/crm-setup` activates the native
21
+ CRM for a company.
22
+
23
+ ## [5.52.0]
24
+
25
+ ### Added
26
+
27
+ - **`hq members promote <target> <newRole>` (alias `set-role`) — change a
28
+ member's role from the CLI.** A discoverable runner for role changes via
29
+ `POST /membership/role`, replacing the undiscoverable, `admin|member`-only
30
+ `set-role` (now kept as an alias). Supports the full role set
31
+ (`owner|admin|member|guest`); `<target>` may be an email, a `prs_` personUid,
32
+ or a full membership key (same resolver as `revoke`). It is a GENERAL role
33
+ change — it can promote OR demote — and authorization (owner-or-admin; only
34
+ an owner may set a target to owner or change an owner) is enforced
35
+ server-side.
36
+ - **`hq files share --full` — glob-safe whole-vault access.** Granting prefixes
37
+ one at a time hit `PolicyBudgetExceeded` after a few (each prefix is a
38
+ distinct ARN in the member's DEFLATE-packed inline STS session policy), and
39
+ the intended single-`*` wildcard escape hatch was unreachable because an
40
+ unquoted `*` expands to local filenames and fails the one-prefix check.
41
+ `--full` is a glob-safe flag that performs the coalesced whole-vault wildcard
42
+ grant in a single policy entry.
43
+
44
+ ## [5.51.0]
45
+
5
46
  ### Fixed
6
47
 
7
48
  - **`hq secrets exec` / `hq secrets env` load via the batch endpoint, killing
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export declare function registerCompanyCommand(program: Command): void;
3
+ //# sourceMappingURL=company.d.ts.map
@@ -0,0 +1,82 @@
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]="c0b7f9ed-8ddb-5422-8872-f85b6b406af6")}catch(e){}}();
3
+ import chalk from "chalk";
4
+ import { ensureCognitoToken } from "../utils/cognito-session.js";
5
+ import { vaultApiFetch, getEntityUid } from "../utils/vault-api.js";
6
+ /**
7
+ * Parse a `--flag <true|false>` string into a boolean, or throw. Commander
8
+ * passes the raw string; we validate strictly so a typo never silently writes
9
+ * the wrong value.
10
+ */
11
+ function parseBoolFlag(value, flag) {
12
+ const v = value.trim().toLowerCase();
13
+ if (v === "true")
14
+ return true;
15
+ if (v === "false")
16
+ return false;
17
+ throw new Error(`${flag} must be 'true' or 'false' (got '${value}')`);
18
+ }
19
+ export function registerCompanyCommand(program) {
20
+ const company = program
21
+ .command("company")
22
+ .description("Company-level settings")
23
+ .option("--company <slug>", "Company slug (resolves to companyUid)");
24
+ const settings = company
25
+ .command("settings")
26
+ .description("Per-company settings (owner-only)");
27
+ settings
28
+ .command("set")
29
+ .description("Set per-company settings (PUT /company-settings). Require at least one " +
30
+ "of --crm-enabled / --ontology-enabled.")
31
+ .option("--crm-enabled <true|false>", "Enable/disable the native CRM for this company")
32
+ .option("--ontology-enabled <true|false>", "Enable/disable the ontology gardener for this company")
33
+ .action(async (opts) => {
34
+ try {
35
+ const companySlug = company.opts().company;
36
+ if (!companySlug) {
37
+ console.error(chalk.red("Error: --company <slug> is required."));
38
+ process.exit(1);
39
+ }
40
+ if (opts.crmEnabled === undefined && opts.ontologyEnabled === undefined) {
41
+ console.error(chalk.red("Error: provide at least one of --crm-enabled / --ontology-enabled."));
42
+ process.exit(1);
43
+ }
44
+ const body = {};
45
+ if (opts.crmEnabled !== undefined) {
46
+ body.crmEnabled = parseBoolFlag(opts.crmEnabled, "--crm-enabled");
47
+ }
48
+ if (opts.ontologyEnabled !== undefined) {
49
+ body.ontologyEnabled = parseBoolFlag(opts.ontologyEnabled, "--ontology-enabled");
50
+ }
51
+ const token = await ensureCognitoToken();
52
+ const companyUid = await getEntityUid(token, { companySlug });
53
+ body.companyUid = companyUid;
54
+ const res = await vaultApiFetch({
55
+ token,
56
+ path: "/company-settings",
57
+ method: "PUT",
58
+ body,
59
+ });
60
+ if (!res.ok) {
61
+ const errBody = (await res.json().catch(() => ({})));
62
+ if (res.status === 403) {
63
+ console.error(chalk.red("Requires owner role on this company to change its settings."));
64
+ process.exit(1);
65
+ }
66
+ console.error(chalk.red(`Failed to update company settings: ${errBody.error ?? res.statusText}`));
67
+ process.exit(1);
68
+ }
69
+ const applied = Object.entries(body)
70
+ .filter(([k]) => k !== "companyUid")
71
+ .map(([k, v]) => `${k}=${v}`)
72
+ .join(", ");
73
+ console.log(chalk.green(`Company settings updated for ${companySlug}: ${applied}`));
74
+ }
75
+ catch (err) {
76
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
77
+ process.exit(1);
78
+ }
79
+ });
80
+ }
81
+ //# sourceMappingURL=company.js.map
82
+ //# debugId=c0b7f9ed-8ddb-5422-8872-f85b6b406af6
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export declare function registerCrmCommand(program: Command): void;
3
+ //# sourceMappingURL=crm.d.ts.map
@@ -0,0 +1,162 @@
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]="056ad757-a0da-5318-b97f-21ae6529f648")}catch(e){}}();
3
+ import chalk from "chalk";
4
+ import { readFileSync } from "node:fs";
5
+ import { ensureCognitoToken } from "../utils/cognito-session.js";
6
+ import { vaultApiFetch, getEntityUid } from "../utils/vault-api.js";
7
+ /**
8
+ * `hq crm entity upsert` — agent-facing controlled entity upsert
9
+ * (hq-native-crm US-002). Wraps `POST /crm/entities`, which fronts the ontology
10
+ * write gate so an authenticated company MEMBER (or higher) can create/update
11
+ * canonical CRM entities in their company's vault.
12
+ *
13
+ * Conventions mirror `secrets.ts`: ensureCognitoToken() → getEntityUid() →
14
+ * vaultApiFetch() → error-check → chalk output.
15
+ */
16
+ /** The CRM/ontology entity types the upsert route accepts via `--type`. */
17
+ const CRM_ENTITY_TYPES = [
18
+ "contact",
19
+ "deal",
20
+ "contract",
21
+ "invoice",
22
+ "company",
23
+ ];
24
+ /**
25
+ * Read the `--json <file|->` payload: a single entity object or an array of
26
+ * entities. `-` reads from stdin. Returns the entities array (always
27
+ * normalized to an array) or throws a descriptive Error.
28
+ */
29
+ function readJsonEntities(source) {
30
+ const raw = source === "-"
31
+ ? readFileSync(0, "utf8") // fd 0 = stdin
32
+ : readFileSync(source, "utf8");
33
+ let parsed;
34
+ try {
35
+ parsed = JSON.parse(raw);
36
+ }
37
+ catch (err) {
38
+ throw new Error(`--json payload is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
39
+ }
40
+ const entities = Array.isArray(parsed) ? parsed : [parsed];
41
+ if (entities.length === 0) {
42
+ throw new Error("--json payload contained no entities");
43
+ }
44
+ for (const e of entities) {
45
+ if (!e || typeof e !== "object") {
46
+ throw new Error("--json entities must be objects");
47
+ }
48
+ const obj = e;
49
+ if (typeof obj.type !== "string" || typeof obj.canonical_name !== "string") {
50
+ throw new Error("each --json entity needs a string `type` and `canonical_name`");
51
+ }
52
+ }
53
+ return entities;
54
+ }
55
+ /**
56
+ * Build a single entity from the flag form (`--type` + `--name` + optional
57
+ * external-id flags). The route fills the additive defaults (aliases/domain/
58
+ * relationships/confidence) when omitted, but we send a complete minimal shape.
59
+ */
60
+ function buildEntityFromFlags(opts) {
61
+ const externalIds = {};
62
+ if (opts.attioId)
63
+ externalIds.attio = opts.attioId;
64
+ if (opts.stripeId)
65
+ externalIds.stripe = opts.stripeId;
66
+ if (opts.pandadocId)
67
+ externalIds.pandadoc = opts.pandadocId;
68
+ if (opts.neonId)
69
+ externalIds.neon = opts.neonId;
70
+ const entity = {
71
+ type: opts.type,
72
+ canonical_name: opts.name,
73
+ aliases: [],
74
+ confidence: 1,
75
+ domain: [],
76
+ relationships: [],
77
+ };
78
+ if (Object.keys(externalIds).length > 0) {
79
+ entity.external_ids = externalIds;
80
+ }
81
+ return entity;
82
+ }
83
+ export function registerCrmCommand(program) {
84
+ const crm = program
85
+ .command("crm")
86
+ .description("Native CRM — upsert canonical entities into a company vault")
87
+ .option("--company <slug>", "Company slug (resolves to companyUid)");
88
+ const entity = crm
89
+ .command("entity")
90
+ .description("CRM entity operations");
91
+ entity
92
+ .command("upsert")
93
+ .description("Create or update a canonical CRM entity (POST /crm/entities). " +
94
+ "Use --type/--name (+ optional external-id flags) or --json <file|-> " +
95
+ "to pass a full entity/array.")
96
+ .option("--type <type>", `Entity type: ${CRM_ENTITY_TYPES.join(" | ")}`)
97
+ .option("--name <canonical_name>", "Canonical entity name")
98
+ .option("--attio-id <id>", "Attio record id → external_ids.attio")
99
+ .option("--stripe-id <id>", "Stripe object id → external_ids.stripe")
100
+ .option("--pandadoc-id <id>", "PandaDoc document id → external_ids.pandadoc")
101
+ .option("--neon-id <id>", "Neon row id → external_ids.neon")
102
+ .option("--json <file|->", "Path to a JSON file (or '-' for stdin) with a full entity object or array")
103
+ .action(async (opts) => {
104
+ try {
105
+ const companySlug = crm.opts().company;
106
+ if (!companySlug) {
107
+ console.error(chalk.red("Error: --company <slug> is required."));
108
+ process.exit(1);
109
+ }
110
+ // Resolve the entity payload: --json is the alternative to flags.
111
+ let entities;
112
+ if (opts.json) {
113
+ entities = readJsonEntities(opts.json);
114
+ }
115
+ else {
116
+ if (!opts.type) {
117
+ console.error(chalk.red(`Error: --type <${CRM_ENTITY_TYPES.join("|")}> is required (or pass --json).`));
118
+ process.exit(1);
119
+ }
120
+ if (!CRM_ENTITY_TYPES.includes(opts.type)) {
121
+ console.error(chalk.red(`Error: --type must be one of ${CRM_ENTITY_TYPES.join(", ")}.`));
122
+ process.exit(1);
123
+ }
124
+ if (!opts.name) {
125
+ console.error(chalk.red("Error: --name <canonical_name> is required (or pass --json)."));
126
+ process.exit(1);
127
+ }
128
+ entities = [buildEntityFromFlags(opts)];
129
+ }
130
+ const token = await ensureCognitoToken();
131
+ const companyUid = await getEntityUid(token, { companySlug });
132
+ const res = await vaultApiFetch({
133
+ token,
134
+ path: "/crm/entities",
135
+ method: "POST",
136
+ body: { companyUid, entities },
137
+ });
138
+ if (!res.ok) {
139
+ const body = (await res.json().catch(() => ({})));
140
+ // CrmDisabledError surfaces as a 403 with a helpful message.
141
+ if (res.status === 403 && body.code === "CRM_DISABLED") {
142
+ console.error(chalk.red("CRM is not enabled for this company. Enable it first: " +
143
+ `hq company settings set --company ${companySlug} --crm-enabled true`));
144
+ process.exit(1);
145
+ }
146
+ console.error(chalk.red(`Failed to upsert entities: ${body.error ?? res.statusText}`));
147
+ process.exit(1);
148
+ }
149
+ const result = (await res.json().catch(() => ({})));
150
+ console.log(chalk.green(`Upsert ${result.status ?? "ok"}: ` +
151
+ `${result.created ?? 0} created, ` +
152
+ `${result.updated ?? 0} updated, ` +
153
+ `${result.skipped ?? 0} skipped.`));
154
+ }
155
+ catch (err) {
156
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
157
+ process.exit(1);
158
+ }
159
+ });
160
+ }
161
+ //# sourceMappingURL=crm.js.map
162
+ //# debugId=056ad757-a0da-5318-b97f-21ae6529f648
@@ -66,6 +66,8 @@ interface RunFilesDeleteParams {
66
66
  prefix: string;
67
67
  dryRun: boolean;
68
68
  yes: boolean;
69
+ /** Target the caller's personal vault instead of a company vault. */
70
+ personal?: boolean;
69
71
  companySlug: string | undefined;
70
72
  }
71
73
  /**
@@ -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]="bc564dd4-d8c1-5d09-ad49-3bcd9f7328a6")}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]="af29c08f-dbeb-5406-8deb-fdc263354b4a")}catch(e){}}();
3
3
  import chalk from "chalk";
4
4
  import open from "open";
5
5
  import * as readline from "node:readline";
@@ -99,10 +99,35 @@ export function registerFilesCommand(program) {
99
99
  .description("Share file paths. Without --with: mint a share-session URL and open it in the browser. With --with: grant access directly to a person, group, or @all.")
100
100
  .option("--with <principal>", "Email address, group id, or '@all' to share with every active company member")
101
101
  .option("--permission <level>", "Permission level (only with --with): read | write")
102
+ .option("--full", "Grant access to the ENTIRE vault (the '*' wildcard prefix) — no need to quote a glob. Requires --with; defaults to write permission.")
102
103
  .option("--expires <duration>", "Token expiry duration for share-session URL (e.g. 15m, 1h, 24h). Default 15m. Max 24h.")
103
104
  .option("--no-open", "Print the share-session URL but do not launch the browser")
104
105
  .action(async (paths, opts) => {
105
106
  try {
107
+ // Full-vault grant: a glob-safe affordance for "give this person the
108
+ // whole vault" so admins never have to quote a `*` (an unquoted glob
109
+ // expands to local filenames and instantly fails the one-prefix
110
+ // check). Maps to the single `*` wildcard grant, which the server
111
+ // coalesces to one policy entry — sidestepping the per-prefix STS
112
+ // session-policy budget. Defaults to write permission.
113
+ if (opts.full) {
114
+ if (opts.with === undefined) {
115
+ console.error(chalk.red("--full grants whole-vault access to a principal and requires --with <principal>."));
116
+ process.exit(1);
117
+ }
118
+ if (paths && paths.length > 0) {
119
+ console.error(chalk.red("--full grants the entire vault; do not also pass file paths."));
120
+ process.exit(1);
121
+ }
122
+ await runDirectGrant({
123
+ prefix: "*",
124
+ principal: opts.with,
125
+ permission: opts.permission ?? "write",
126
+ companySlug: files.opts().company,
127
+ fullVault: true,
128
+ });
129
+ return;
130
+ }
106
131
  if (!paths || paths.length === 0) {
107
132
  console.error(chalk.red("usage: hq files share <paths...> [--with <principal>]"));
108
133
  process.exit(1);
@@ -331,16 +356,24 @@ export function registerFilesCommand(program) {
331
356
  });
332
357
  files
333
358
  .command("delete <prefix>")
334
- .description("Delete vault objects under a prefix (bounded + scoped). Always previews the exact count first; prompts for confirmation unless --yes.")
359
+ .description("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.")
335
360
  .option("--dry-run", "List what WOULD be deleted without deleting anything")
361
+ .option("--personal", "Target your own personal vault instead of a company vault (mutually exclusive with --company)")
336
362
  .option("-y, --yes", "Skip the confirmation prompt (for scripts)")
337
363
  .action(async (prefix, opts) => {
338
364
  try {
365
+ const companySlug = files.opts().company;
366
+ const personal = opts.personal === true;
367
+ if (personal && companySlug) {
368
+ console.error(chalk.red("Pass either --personal or --company, not both."));
369
+ process.exit(1);
370
+ }
339
371
  await runFilesDelete({
340
372
  prefix,
341
373
  dryRun: opts.dryRun === true,
342
374
  yes: opts.yes === true,
343
- companySlug: files.opts().company,
375
+ personal,
376
+ companySlug,
344
377
  });
345
378
  }
346
379
  catch (err) {
@@ -437,7 +470,12 @@ async function runDirectGrant(params) {
437
470
  const data = (await res.json());
438
471
  const printedPrefix = data.acl?.path ?? data.acl?.prefix ?? canonicalPrefix;
439
472
  const verb = autoCreated ? "Created ACL and granted" : "Granted";
440
- console.log(chalk.green(`${verb} ${params.permission} on ${printedPrefix} to ${principalLabel}`));
473
+ if (params.fullVault) {
474
+ console.log(chalk.green(`${verb} ${params.permission} on the ENTIRE vault to ${principalLabel}`));
475
+ }
476
+ else {
477
+ console.log(chalk.green(`${verb} ${params.permission} on ${printedPrefix} to ${principalLabel}`));
478
+ }
441
479
  }
442
480
  async function runShareSession(params) {
443
481
  // Normalize every path through the shared prefix helper so a trailing
@@ -533,17 +571,28 @@ function realConfirm(message) {
533
571
  /**
534
572
  * POST /v1/files/delete. Throws FilesDeleteHttpError on any non-2xx so the one
535
573
  * caller renders a single consistent error path.
574
+ *
575
+ * Scope is EITHER a company vault (`companyUid` set) OR the caller's personal
576
+ * vault (`personal: true`). For the personal case the server resolves the target
577
+ * person + bucket from the authenticated caller — we send NO uid, just the
578
+ * `personal` flag — so there is nothing for the client to get wrong or spoof.
536
579
  */
537
580
  async function callDeleteEndpoint(params) {
538
581
  const res = await vaultApiFetch({
539
582
  token: params.token,
540
583
  path: "/v1/files/delete",
541
584
  method: "POST",
542
- body: {
543
- company: params.companyUid,
544
- prefix: params.prefix,
545
- dryRun: params.dryRun,
546
- },
585
+ body: params.personal
586
+ ? {
587
+ personal: true,
588
+ prefix: params.prefix,
589
+ dryRun: params.dryRun,
590
+ }
591
+ : {
592
+ company: params.companyUid,
593
+ prefix: params.prefix,
594
+ dryRun: params.dryRun,
595
+ },
547
596
  });
548
597
  if (!res.ok) {
549
598
  const body = (await res.json().catch(() => ({})));
@@ -612,14 +661,21 @@ export async function runFilesDelete(params, deps = {}) {
612
661
  process.exit(1);
613
662
  }
614
663
  const token = await ensureCognitoToken();
615
- const companyUid = await getCompanyUid(token, params.companySlug);
664
+ // Personal scope resolves the vault server-side from the caller's identity —
665
+ // no company to look up. Company scope resolves the companyUid as before.
666
+ const companyUid = params.personal
667
+ ? undefined
668
+ : await getCompanyUid(token, params.companySlug);
669
+ const scopeArgs = params.personal
670
+ ? { personal: true }
671
+ : { companyUid };
616
672
  // 1. Always preview first — this is how we print the EXACT key count before
617
673
  // deleting anything (and the whole behavior of --dry-run).
618
674
  let preview;
619
675
  try {
620
676
  preview = await callDeleteEndpoint({
621
677
  token,
622
- companyUid,
678
+ ...scopeArgs,
623
679
  prefix: normalized,
624
680
  dryRun: true,
625
681
  });
@@ -652,7 +708,9 @@ export async function runFilesDelete(params, deps = {}) {
652
708
  console.log(chalk.dim(` (${preview.skipped} more under this prefix you can't delete will be left untouched)`));
653
709
  }
654
710
  if (!params.yes) {
655
- const ok = await confirm(`Delete ${preview.matched} ${noun}? This removes them from the shared vault for everyone.`);
711
+ const ok = await confirm(params.personal
712
+ ? `Delete ${preview.matched} ${noun}? This removes them from your personal vault.`
713
+ : `Delete ${preview.matched} ${noun}? This removes them from the shared vault for everyone.`);
656
714
  if (!ok) {
657
715
  console.log(chalk.dim("Aborted — nothing was deleted."));
658
716
  return;
@@ -663,7 +721,7 @@ export async function runFilesDelete(params, deps = {}) {
663
721
  try {
664
722
  result = await callDeleteEndpoint({
665
723
  token,
666
- companyUid,
724
+ ...scopeArgs,
667
725
  prefix: normalized,
668
726
  dryRun: false,
669
727
  });
@@ -682,4 +740,4 @@ export async function runFilesDelete(params, deps = {}) {
682
740
  }
683
741
  }
684
742
  //# sourceMappingURL=files.js.map
685
- //# debugId=bc564dd4-d8c1-5d09-ad49-3bcd9f7328a6
743
+ //# debugId=af29c08f-dbeb-5406-8deb-fdc263354b4a
@@ -149,5 +149,15 @@ export declare function listActiveMembers(token: string, companyUid: string): Pr
149
149
  */
150
150
  export declare function resolveRevokeTargetToMembershipKey(arg: string, companyUid: string): string;
151
151
  export declare function revokeInvite(token: string, tokenOrKey: string, companyUid: string): Promise<void>;
152
+ /**
153
+ * Change a member's role via `POST /membership/role`, accepting the FULL role
154
+ * set (owner|admin|member|guest). This is a GENERAL role change — it can promote
155
+ * OR demote. Authorization (owner-or-admin `changeRoles`, owner-only
156
+ * promote-to-owner / change-an-owner) is enforced SERVER-side; this function
157
+ * only validates the role string locally and surfaces the server's error.
158
+ * Role string is validated BEFORE any network call so callers/tests can rely on
159
+ * a synchronous-shaped rejection for a bad role.
160
+ */
161
+ export declare function changeMemberRole(token: string, companyUid: string, membershipKey: string, newRole: Role): Promise<void>;
152
162
  export declare function registerMembersCommand(program: Command): void;
153
163
  //# sourceMappingURL=members.d.ts.map
@@ -1,12 +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]="8244d720-094e-5d4e-b813-75086ed0849d")}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]="83a54ed2-3f52-5b6f-a784-09f78f488ddd")}catch(e){}}();
3
3
  import chalk from "chalk";
4
4
  import { ensureCognitoToken } from "../utils/cognito-session.js";
5
5
  import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
6
6
  const EMAIL_PATTERN = /^[^\s]+@[^\s]+$/;
7
7
  const PERSON_UID_PATTERN = /^prs_[A-Za-z0-9_-]+$/;
8
8
  export const VALID_ROLES = new Set(["owner", "admin", "member", "guest"]);
9
- const VALID_MEMBER_SET_ROLES = new Set(["admin", "member"]);
10
9
  export function detectTarget(target) {
11
10
  if (EMAIL_PATTERN.test(target)) {
12
11
  return { type: "email", value: target.trim().toLowerCase() };
@@ -265,9 +264,18 @@ export async function revokeInvite(token, tokenOrKey, companyUid) {
265
264
  throw new InviteHttpError(res.status, err.message ?? err.error ?? res.statusText, err.code);
266
265
  }
267
266
  }
268
- async function setMemberRole(token, companyUid, membershipKey, newRole) {
269
- if (!VALID_MEMBER_SET_ROLES.has(newRole)) {
270
- throw new Error(`Invalid role '${newRole}': must be one of admin, member`);
267
+ /**
268
+ * Change a member's role via `POST /membership/role`, accepting the FULL role
269
+ * set (owner|admin|member|guest). This is a GENERAL role change it can promote
270
+ * OR demote. Authorization (owner-or-admin `changeRoles`, owner-only
271
+ * promote-to-owner / change-an-owner) is enforced SERVER-side; this function
272
+ * only validates the role string locally and surfaces the server's error.
273
+ * Role string is validated BEFORE any network call so callers/tests can rely on
274
+ * a synchronous-shaped rejection for a bad role.
275
+ */
276
+ export async function changeMemberRole(token, companyUid, membershipKey, newRole) {
277
+ if (!VALID_ROLES.has(newRole)) {
278
+ throw new Error(`Invalid role '${newRole}': must be one of owner, admin, member, guest`);
271
279
  }
272
280
  const res = await vaultApiFetch({
273
281
  token,
@@ -285,16 +293,29 @@ export function registerMembersCommand(program) {
285
293
  .command("members")
286
294
  .description("Manage company memberships and invites")
287
295
  .option("--company <slug>", "Company slug (resolves to companyUid)");
296
+ // Canonical role-change command. `promote` is the discoverable verb users
297
+ // reach for; `set-role` is kept as an alias for the older name. The route is a
298
+ // GENERAL role change, so the help text and success message are honest that it
299
+ // can demote as well as promote. Authorization is enforced server-side.
288
300
  members
289
- .command("set-role <membershipKey> <role>")
290
- .description("Change a member or agent role to admin or member")
291
- .action(async (membershipKey, role) => {
301
+ .command("promote <target> <newRole>")
302
+ .alias("set-role")
303
+ .description("Change a member's role (owner|admin|member|guest) promotes OR demotes. " +
304
+ "<target> may be an email, a prs_ personUid, or a full membership key. " +
305
+ "Owner-or-admin only; setting a target to owner (or changing an owner) is owner-only. Server-enforced.")
306
+ .action(async (target, newRole) => {
292
307
  try {
308
+ const role = newRole.trim().toLowerCase();
309
+ if (!VALID_ROLES.has(role)) {
310
+ console.error(chalk.red(`Invalid role '${newRole}': must be one of owner, admin, member, guest`));
311
+ process.exit(1);
312
+ }
293
313
  const token = await ensureCognitoToken();
294
314
  const companySlug = members.opts().company;
295
315
  const companyUid = await getCompanyUid(token, companySlug);
296
- await setMemberRole(token, companyUid, membershipKey, role);
297
- console.log(chalk.green(`Updated role for '${membershipKey}' to ${role}`));
316
+ const membershipKey = resolveRevokeTargetToMembershipKey(target, companyUid);
317
+ await changeMemberRole(token, companyUid, membershipKey, role);
318
+ console.log(chalk.green(`Updated role for '${target}' to ${role}`));
298
319
  }
299
320
  catch (err) {
300
321
  if (err instanceof InviteHttpError) {
@@ -534,4 +555,4 @@ export function registerMembersCommand(program) {
534
555
  });
535
556
  }
536
557
  //# sourceMappingURL=members.js.map
537
- //# debugId=8244d720-094e-5d4e-b813-75086ed0849d
558
+ //# debugId=83a54ed2-3f52-5b6f-a784-09f78f488ddd
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@
5
5
  // MUST be first: guard the Node version before any dependency that needs a
6
6
  // Node 20+ API (e.g. util.styleText) or a newer native ABI is evaluated.
7
7
 
8
- !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]="82c44c14-a953-5644-9bf2-dd2c6d487d63")}catch(e){}}();
8
+ !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]="6ad01242-c165-5be2-bb31-e929c0b38f5e")}catch(e){}}();
9
9
  import "./node-preflight.js";
10
10
  import { Command } from "commander";
11
11
  import { initSentry, Sentry } from "./sentry.js";
@@ -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";
@@ -173,6 +175,13 @@ registerRescueCommand(program);
173
175
  // provenance-based status across BOTH Claude + Codex runtimes (reads `_hqPack`
174
176
  // off the configs, NOT linkStatus), with secret-redacted output + `--json`.
175
177
  registerMcpCommand(program);
178
+ // Native CRM entity upsert (subcommand group — `hq crm entity upsert`). Wraps
179
+ // POST /crm/entities (the ontology write gate) so an authenticated company
180
+ // member can create/update canonical CRM entities in the company vault.
181
+ registerCrmCommand(program);
182
+ // Company settings (subcommand group — `hq company settings set`). Owner-only
183
+ // toggles for crmEnabled / ontologyEnabled via PUT /company-settings.
184
+ registerCompanyCommand(program);
176
185
  (async () => {
177
186
  try {
178
187
  Sentry.addBreadcrumb({
@@ -233,4 +242,4 @@ registerMcpCommand(program);
233
242
  }
234
243
  })();
235
244
  //# sourceMappingURL=index.js.map
236
- //# debugId=82c44c14-a953-5644-9bf2-dd2c6d487d63
245
+ //# debugId=6ad01242-c165-5be2-bb31-e929c0b38f5e
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.51.0",
3
+ "version": "5.53.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {