@indigoai-us/hq-cli 5.345.170 → 5.345.172

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 (142) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/assets/scaffold/core/scripts/hq-delegate-send.sh +1 -1
  3. package/dist/command-catalog.generated.d.ts +206 -7
  4. package/dist/command-catalog.generated.js +252 -8
  5. package/dist/command-registration-plan.d.ts +7 -0
  6. package/dist/command-registration-plan.js +1 -0
  7. package/dist/commands/access.d.ts +32 -1
  8. package/dist/commands/access.js +112 -3
  9. package/dist/commands/agent-kit.d.ts +1 -1
  10. package/dist/commands/agent-kit.js +3 -1
  11. package/dist/commands/agents.js +2 -1
  12. package/dist/commands/bot.d.ts +3 -0
  13. package/dist/commands/bot.js +179 -18
  14. package/dist/commands/cloud.d.ts +58 -1
  15. package/dist/commands/cloud.js +228 -10
  16. package/dist/commands/core-company-index-scope-flag.d.ts +8 -1
  17. package/dist/commands/core-company-index-scope-flag.js +9 -2
  18. package/dist/commands/core-delegate-w1b.js +5 -2
  19. package/dist/commands/core-dm-bind-native.js +19 -4
  20. package/dist/commands/core-rebuild-index-fast.js +24 -6
  21. package/dist/commands/core.js +18 -6
  22. package/dist/commands/daemon.js +16 -1
  23. package/dist/commands/dm.d.ts +17 -3
  24. package/dist/commands/dm.js +121 -26
  25. package/dist/commands/files.d.ts +1 -0
  26. package/dist/commands/files.js +22 -4
  27. package/dist/commands/integrations-api.d.ts +4 -0
  28. package/dist/commands/integrations-api.js +18 -2
  29. package/dist/commands/integrations-connect.d.ts +10 -3
  30. package/dist/commands/integrations-connect.js +70 -6
  31. package/dist/commands/integrations-core.d.ts +23 -0
  32. package/dist/commands/integrations-core.js +100 -0
  33. package/dist/commands/integrations-manage.js +8 -1
  34. package/dist/commands/integrations.js +49 -5
  35. package/dist/commands/lanes-loop.d.ts +20 -0
  36. package/dist/commands/lanes-loop.js +69 -11
  37. package/dist/commands/lanes.js +9 -1
  38. package/dist/commands/rescue.d.ts +9 -0
  39. package/dist/commands/rescue.js +31 -0
  40. package/dist/commands/run.js +115 -40
  41. package/dist/commands/scan.d.ts +5 -1
  42. package/dist/commands/scan.js +16 -4
  43. package/dist/commands/secrets.js +50 -9
  44. package/dist/commands/sync-core.d.ts +22 -0
  45. package/dist/commands/sync-core.js +124 -0
  46. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2447 -2135
  47. package/dist/hq-cloud-sync-runner/package.json +1 -1
  48. package/dist/lib/agent-kit/inbox-state.d.ts +17 -0
  49. package/dist/lib/agent-kit/inbox-state.js +41 -20
  50. package/dist/lib/agent-kit/kit-config.d.ts +2 -0
  51. package/dist/lib/agent-kit/kit-config.js +6 -0
  52. package/dist/lib/agent-kit/mcp/tools.js +12 -5
  53. package/dist/lib/agent-kit/run/heartbeat.d.ts +6 -3
  54. package/dist/lib/agent-kit/run/heartbeat.js +6 -3
  55. package/dist/lib/agent-kit/run/inbox.d.ts +8 -3
  56. package/dist/lib/agent-kit/run/inbox.js +42 -10
  57. package/dist/lib/agent-kit/run/request-timeout.d.ts +26 -0
  58. package/dist/lib/agent-kit/run/request-timeout.js +51 -0
  59. package/dist/lib/bot/api.d.ts +1 -1
  60. package/dist/lib/bot/config.d.ts +10 -0
  61. package/dist/lib/bot/config.js +17 -1
  62. package/dist/lib/bot/fleet-local.d.ts +142 -0
  63. package/dist/lib/bot/fleet-local.js +689 -0
  64. package/dist/lib/bot/fleet-python.d.ts +47 -0
  65. package/dist/lib/bot/fleet-python.js +218 -0
  66. package/dist/lib/bot/index.d.ts +2 -0
  67. package/dist/lib/bot/index.js +2 -0
  68. package/dist/lib/bot/local-config.d.ts +3 -2
  69. package/dist/lib/bot/local-config.js +5 -2
  70. package/dist/lib/bot/promote.d.ts +10 -0
  71. package/dist/lib/bot/promote.js +3 -2
  72. package/dist/lib/bot/prompt.d.ts +6 -1
  73. package/dist/lib/bot/prompt.js +8 -3
  74. package/dist/lib/bot/runtime/index.js +8 -4
  75. package/dist/lib/bot/runtime/scratch-dir.d.ts +24 -0
  76. package/dist/lib/bot/runtime/scratch-dir.js +41 -0
  77. package/dist/lib/caller-principal.d.ts +4 -0
  78. package/dist/lib/caller-principal.js +40 -0
  79. package/dist/lib/capacity/collectors/grok.js +8 -2
  80. package/dist/lib/cli-flag-keys.d.ts +2 -0
  81. package/dist/lib/cli-flag-keys.js +2 -0
  82. package/dist/lib/daemon/catalog.d.ts +12 -4
  83. package/dist/lib/daemon/catalog.js +70 -24
  84. package/dist/lib/daemon/config.d.ts +0 -7
  85. package/dist/lib/daemon/config.js +0 -32
  86. package/dist/lib/daemon/run.d.ts +5 -1
  87. package/dist/lib/daemon/run.js +48 -11
  88. package/dist/lib/daemon/supervisor.js +3 -1
  89. package/dist/lib/doctor/checks/release-links.d.ts +62 -1
  90. package/dist/lib/doctor/checks/release-links.js +188 -2
  91. package/dist/lib/doctor/fix/apply.js +54 -0
  92. package/dist/lib/doctor/fix/remediation.d.ts +8 -1
  93. package/dist/lib/doctor/fix/remediation.js +22 -0
  94. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  95. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  96. package/dist/lib/index-render/index.d.ts +9 -0
  97. package/dist/lib/index-render/index.js +65 -5
  98. package/dist/lib/index-render/session-scope.d.ts +69 -0
  99. package/dist/lib/index-render/session-scope.js +279 -0
  100. package/dist/lib/lanes/admission.js +7 -20
  101. package/dist/lib/lanes/reconcile.js +13 -15
  102. package/dist/lib/lanes/stale-lock.d.ts +50 -0
  103. package/dist/lib/lanes/stale-lock.js +105 -0
  104. package/dist/lib/lanes/store.js +11 -76
  105. package/dist/lib/notify-recipient.d.ts +10 -4
  106. package/dist/lib/notify-recipient.js +16 -4
  107. package/dist/lib/secret-input-link.d.ts +42 -0
  108. package/dist/lib/secret-input-link.js +93 -0
  109. package/dist/lib/secret-scan/range.d.ts +57 -0
  110. package/dist/lib/secret-scan/range.js +266 -0
  111. package/dist/lib/secret-scan/repo-config.d.ts +18 -3
  112. package/dist/lib/secret-scan/repo-config.js +135 -2
  113. package/dist/lib/sync-core/engine-home.d.ts +44 -0
  114. package/dist/lib/sync-core/engine-home.js +193 -0
  115. package/dist/lib/sync-core/flag.d.ts +17 -0
  116. package/dist/lib/sync-core/flag.js +47 -0
  117. package/dist/lib/sync-core/reconcile.d.ts +31 -0
  118. package/dist/lib/sync-core/reconcile.js +196 -0
  119. package/dist/lib/sync-core/release.d.ts +32 -0
  120. package/dist/lib/sync-core/release.js +140 -0
  121. package/dist/lib/sync-core/rollout-state.d.ts +24 -0
  122. package/dist/lib/sync-core/rollout-state.js +94 -0
  123. package/dist/lib/vault-sync-switches.d.ts +5 -0
  124. package/dist/lib/vault-sync-switches.js +43 -0
  125. package/dist/run/script-bound-schema.d.ts +27 -0
  126. package/dist/run/script-bound-schema.js +97 -0
  127. package/dist/secrets-env-action.d.ts +2 -0
  128. package/dist/secrets-env-action.js +7 -2
  129. package/dist/utils/cognito-session.d.ts +3 -30
  130. package/dist/utils/cognito-session.js +2 -100
  131. package/dist/utils/machine-principal-state.d.ts +38 -0
  132. package/dist/utils/machine-principal-state.js +110 -0
  133. package/dist/utils/remote-list-not-converging.d.ts +21 -0
  134. package/dist/utils/remote-list-not-converging.js +40 -0
  135. package/dist/utils/safe-link.js +9 -0
  136. package/dist/utils/script-bound-exec.d.ts +158 -0
  137. package/dist/utils/script-bound-exec.js +568 -0
  138. package/dist/utils/sync-watchdog.d.ts +92 -0
  139. package/dist/utils/sync-watchdog.js +250 -0
  140. package/package.json +8 -8
  141. package/dist/lib/daemon/sync-core-canary.d.ts +0 -9
  142. package/dist/lib/daemon/sync-core-canary.js +0 -56
@@ -5,7 +5,9 @@ import { resolveDestinationCompanyUid, vaultApiFetch } from "../utils/vault-api.
5
5
  import { isMachineIdentity } from "@indigoai-us/hq-cloud";
6
6
  import { peekIdToken } from "../utils/id-token.js";
7
7
  import { redactAgentInboxItem as redactInboxCredentials } from "../lib/agent-kit/inbox-redact.js";
8
- import { EMAIL_PATTERN, RECIPIENT_UID_PATTERN, defaultAudienceFor, recipientKindOf, } from "../lib/notify-recipient.js";
8
+ import { classifyAgentInboxBody } from "../lib/agent-kit/inbox-state.js";
9
+ import { EMAIL_PATTERN, RECIPIENT_UID_PATTERN, PERSON_AGENT_AUDIENCE_MESSAGE, PERSON_CANNOT_SEND_AGENT_CODE, callerMayUseAudience, defaultAudienceFor, recipientKindOf, } from "../lib/notify-recipient.js";
10
+ import { callerIsMachinePrincipal } from "../lib/caller-principal.js";
9
11
  export { defaultAudienceFor, recipientKindOf };
10
12
  import { dmForwardCliEnabled } from "./dm-forward-flag.js";
11
13
  import { attachmentScopeForChannel, formatUploadedAttachments, personalScopeForCaller, uploadDmAttachments, validateAttachmentPaths, } from "./dm-attach.js";
@@ -120,29 +122,45 @@ export function matchChannelsByName(channels, name) {
120
122
  * eba06f59). Returns null when nothing or more than one contact matches. Pure.
121
123
  */
122
124
  export function matchContactByName(contacts, name) {
123
- const target = name.trim().toLowerCase();
124
- const targetSlug = channelSlug(name);
125
+ const unique = matchContactsByName(contacts, name);
126
+ return unique.length === 1 ? unique[0] : null;
127
+ }
128
+ /**
129
+ * Every distinct contact (person or bot) whose display name matches `name`,
130
+ * using the same rule as matchContactByName. A leading '@' is the explicit
131
+ * person/bot selector and is ignored for matching. Pure.
132
+ */
133
+ export function matchContactsByName(contacts, name) {
134
+ const bare = name.trim().replace(/^@/, "");
135
+ const target = bare.toLowerCase();
136
+ const targetSlug = channelSlug(bare);
125
137
  if (!target)
126
- return null;
138
+ return [];
127
139
  const hits = contacts.filter((c) => {
128
140
  if (!c.personUid)
129
141
  return false;
130
142
  const dn = c.displayName?.trim().toLowerCase();
131
143
  return Boolean(dn && (dn === target || (targetSlug && channelSlug(dn) === targetSlug)));
132
144
  });
133
- const unique = [...new Map(hits.map((c) => [c.personUid, c])).values()];
134
- return unique.length === 1 ? unique[0] : null;
145
+ return [...new Map(hits.map((c) => [c.personUid, c])).values()];
146
+ }
147
+ /** One line per ambiguous contact match, for a refusal message. Pure. */
148
+ export function formatContactMatches(matches) {
149
+ return matches
150
+ .map((c) => ` ${c.displayName || "(no name)"}${c.email ? ` <${c.email}>` : ""} ${c.personUid}`)
151
+ .join("\n");
135
152
  }
153
+ /** Contacts, or null when the lookup failed (so callers can say so). */
136
154
  async function fetchContacts(token) {
137
155
  try {
138
156
  const res = await vaultApiFetch({ token, path: "/v1/notify/contacts" });
139
157
  if (!res.ok)
140
- return [];
158
+ return null;
141
159
  const data = (await res.json());
142
- return Array.isArray(data.contacts) ? data.contacts : [];
160
+ return Array.isArray(data.contacts) ? data.contacts : null;
143
161
  }
144
162
  catch {
145
- return [];
163
+ return null;
146
164
  }
147
165
  }
148
166
  /**
@@ -427,6 +445,8 @@ export async function resolveDmPeerUid(token, recipient) {
427
445
  function friendlyDmError(status, code, fallback) {
428
446
  if (status === 401)
429
447
  return "Not authenticated — run `hq login` and try again.";
448
+ if (code === PERSON_CANNOT_SEND_AGENT_CODE)
449
+ return fallback?.trim() || PERSON_AGENT_AUDIENCE_MESSAGE;
430
450
  if (status === 404 || code === "RECIPIENT_NOT_FOUND") {
431
451
  return "Recipient not found or not reachable — you can only DM someone you share an active company with.";
432
452
  }
@@ -818,29 +838,47 @@ function explicitAudience(opts) {
818
838
  }
819
839
  return explicit;
820
840
  }
841
+ /** An explicit agent/both audience from a person login. */
842
+ export class PersonAudienceError extends Error {
843
+ constructor() {
844
+ super(PERSON_AGENT_AUDIENCE_MESSAGE);
845
+ this.name = "PersonAudienceError";
846
+ }
847
+ }
821
848
  /**
822
849
  * Resolve the audience for a send. The default comes from the recipient kind
823
- * (`defaultAudienceFor`); `--for-human` maps to `human`; an explicit
824
- * `--audience` wins over `--for-human` (with a warning) and is validated.
850
+ * and the caller (`defaultAudienceFor`: always `human` for a person login);
851
+ * `--for-human` maps to `human`; an explicit `--audience` wins over
852
+ * `--for-human` (with a warning) and is validated. A person login passing
853
+ * `--audience agent|both` throws PersonAudienceError.
825
854
  */
826
- export function resolveSendAudience(opts, recipientKind, warn = (m) => console.error(chalk.yellow(m))) {
855
+ export function resolveSendAudience(opts, recipientKind, warn = (m) => console.error(chalk.yellow(m)), machineCaller = callerIsMachinePrincipal()) {
827
856
  const explicit = explicitAudience(opts);
857
+ if (!callerMayUseAudience(explicit, machineCaller))
858
+ throw new PersonAudienceError();
828
859
  if (explicit !== undefined) {
829
860
  if (opts.forHuman) {
830
861
  warn("Both --audience " + explicit + " and --for-human were passed; using --audience " + explicit + ".");
831
862
  }
832
863
  return explicit;
833
864
  }
834
- return opts.forHuman ? "human" : defaultAudienceFor(recipientKind);
865
+ return opts.forHuman ? "human" : defaultAudienceFor(recipientKind, machineCaller);
835
866
  }
836
867
  async function runDmSend(recipient, message, rawOpts) {
837
868
  // Validate an explicit --audience before any network call; the default is
838
869
  // applied once the recipient kind is known.
839
870
  explicitAudience(rawOpts);
871
+ // People never send agent-tagged messages: refuse before any network call.
872
+ const machineCaller = callerIsMachinePrincipal();
873
+ if (!callerMayUseAudience(rawOpts.audience, machineCaller)) {
874
+ console.error(chalk.red(PERSON_AGENT_AUDIENCE_MESSAGE));
875
+ process.exit(1);
876
+ return;
877
+ }
840
878
  const opts = { ...rawOpts };
841
879
  const withAudience = (kind) => ({
842
880
  ...rawOpts,
843
- audience: resolveSendAudience(rawOpts, kind),
881
+ audience: resolveSendAudience(rawOpts, kind, undefined, machineCaller),
844
882
  });
845
883
  // --channel <name> is an explicit channel target; the positional carries the
846
884
  // message (recipient slot), e.g. `hq dm --channel vyg-dev "hello"`.
@@ -874,16 +912,46 @@ async function runDmSend(recipient, message, rawOpts) {
874
912
  // A bare name or #hash addresses a named DM channel.
875
913
  const channelName = parseChannelName(recipient);
876
914
  if (channelName !== null) {
877
- // A bare word (no '#') may name a person as well as a channel. Prefer the
878
- // exact person and say so; '#name' or --channel always means the channel.
879
- const person = recipient.trim().startsWith("#")
880
- ? null
881
- : matchContactByName(await fetchContacts(await ensureCognitoToken()), channelName);
915
+ // A bare word may name a person or bot as well as a channel. Prefer the
916
+ // exact person/bot and say so; '#name' or --channel always means the
917
+ // channel, '@name' always means a person/bot. Several same-named
918
+ // contacts are ambiguous: refuse with the list rather than fall through
919
+ // to a channel of that name (feedback eba06f59).
920
+ const typed = recipient.trim();
921
+ const explicitPerson = typed.startsWith("@");
922
+ const bare = channelName.replace(/^@/, "");
923
+ let person = null;
924
+ if (!typed.startsWith("#")) {
925
+ const contacts = await fetchContacts(await ensureCognitoToken());
926
+ if (contacts === null) {
927
+ // Without contacts we cannot tell a person/bot from a channel, so a
928
+ // bare name must not guess the channel either.
929
+ console.error(chalk.red(`Could not load your contacts to resolve '${typed}'; nothing was sent. ` +
930
+ `Use their email or prs_/agt_ uid for a person or bot, or '#${bare}' or --channel ${bare} to post to the channel.`));
931
+ process.exit(1);
932
+ return;
933
+ }
934
+ else {
935
+ const matches = matchContactsByName(contacts, bare);
936
+ if (matches.length > 1) {
937
+ console.error(chalk.red(`'${bare}' matches ${matches.length} people or bots; nothing was sent. Use an email or uid:\n${formatContactMatches(matches)}\n` +
938
+ `Use '#${bare}' or --channel ${bare} for a channel.`));
939
+ process.exit(1);
940
+ return;
941
+ }
942
+ person = matches[0] ?? null;
943
+ }
944
+ if (!person && explicitPerson) {
945
+ console.error(chalk.red(`No person or bot named '${bare}' in your contacts; nothing was sent. Use '#${bare}' for a channel.`));
946
+ process.exit(1);
947
+ return;
948
+ }
949
+ }
882
950
  if (!person?.personUid) {
883
951
  await runChannelSend(channelName, message, withAudience("channel"));
884
952
  return;
885
953
  }
886
- console.log(chalk.dim(`Resolved '${channelName}' to person ${person.displayName || person.email || person.personUid} (${person.personUid}). Use '#${channelName}' or --channel ${channelName} to post to a channel instead.`));
954
+ console.log(chalk.dim(`Resolved '${bare}' to person ${person.displayName || person.email || person.personUid} (${person.personUid}). Use '#${bare}' or --channel ${bare} to post to a channel instead.`));
887
955
  recipient = person.personUid;
888
956
  }
889
957
  Object.assign(opts, withAudience(recipientKindOf(recipient)));
@@ -1492,23 +1560,46 @@ async function runAgentInbox(token, agentUid, opts) {
1492
1560
  console.error(chalk.red(`GET ${route} → ${res.status}`));
1493
1561
  process.exit(1);
1494
1562
  }
1495
- const data = (await res.json().catch(() => ({})));
1496
- let items = (Array.isArray(data.messages) ? data.messages : []).map(redactAgentInboxItem);
1563
+ // Same guard as `hq agent inbox` (hq-cli #2155): a 200 alone does not
1564
+ // certify the read. A decode failure stays undefined for the classifier;
1565
+ // parser errors and response bytes are never echoed.
1566
+ const classified = classifyAgentInboxBody(await res.json().catch(() => undefined));
1567
+ if (classified.status === "invalid") {
1568
+ console.error(chalk.red(`GET ${route} returned an invalid response; the inbox could not be read, and nothing was acknowledged.`));
1569
+ process.exit(1);
1570
+ return;
1571
+ }
1572
+ const partial = classified.status === "partial";
1573
+ if (partial) {
1574
+ console.error(chalk.yellow("The server inbox contained invalid messages; showing the valid ones only, which may be incomplete."));
1575
+ }
1576
+ let items = classified.items.map(redactAgentInboxItem);
1497
1577
  if (opts.limit && Number(opts.limit) > 0)
1498
1578
  items = items.slice(0, Number(opts.limit));
1499
1579
  if (opts.json) {
1500
1580
  console.log(JSON.stringify(items, null, 2));
1501
1581
  }
1502
1582
  else if (items.length === 0) {
1503
- console.log(chalk.dim("No pending inbox items."));
1583
+ console.log(chalk.dim(partial
1584
+ ? "No pending inbox items can be confirmed; the server inbox could not be read completely."
1585
+ : "No pending inbox items."));
1504
1586
  }
1505
1587
  else {
1506
1588
  console.log(chalk.green(`${items.length} pending inbox item${items.length === 1 ? "" : "s"}:`));
1507
1589
  for (const item of items)
1508
1590
  console.log(`\n${formatAgentInboxItem(item)}`);
1509
1591
  }
1510
- if (opts.markRead) {
1592
+ if (opts.markRead && partial) {
1593
+ // An incomplete read cannot show what --mark-read would hide, so ack nothing.
1594
+ const n = items.length;
1595
+ const ids = items.map((item) => item.id).join(", ");
1596
+ console.error(chalk.yellow(`--mark-read skipped: the inbox read was incomplete, so ${n} ${n === 1 ? "item was" : "items were"} not acked` +
1597
+ `${opts.limit ? " (count reflects --limit)" : ""}` +
1598
+ `${n > 0 ? `: ${ids}. Ack individually with \`hq agent inbox done <id>\`.` : "."}`));
1599
+ }
1600
+ if (opts.markRead && !partial) {
1511
1601
  let acked = 0;
1602
+ let failed = 0;
1512
1603
  for (const item of items) {
1513
1604
  const id = typeof item.messageId === "string" ? item.messageId : typeof item.id === "string" ? item.id : null;
1514
1605
  if (!id)
@@ -1516,9 +1607,13 @@ async function runAgentInbox(token, agentUid, opts) {
1516
1607
  const ack = await vaultApiFetch({ token, path: `${route}/${encodeURIComponent(id)}/ack`, method: "POST" });
1517
1608
  if (ack.ok)
1518
1609
  acked += 1;
1610
+ else
1611
+ failed += 1;
1519
1612
  }
1520
1613
  if (!opts.json && acked > 0)
1521
1614
  console.log(chalk.dim(`\nAcked ${acked}.`));
1615
+ if (failed > 0)
1616
+ console.error(chalk.yellow(`${failed} item${failed === 1 ? "" : "s"} could not be acked.`));
1522
1617
  }
1523
1618
  }
1524
1619
  async function runDmInbox(opts) {
@@ -1729,7 +1824,7 @@ async function runDmChannel(target, opts) {
1729
1824
  export function registerDmCommand(program) {
1730
1825
  const dm = program
1731
1826
  .command("dm")
1732
- .description("Send and read direct messages, and manage connection requests. Messages to a person send to them; messages to an agent, a channel or a group are tagged for agents and folded away from people by default. Pass --for-human or --audience human|agent|both to override.");
1827
+ .description("Send and read direct messages, and manage connection requests. Messages sent from a person login are always tagged for people. When an agent or bot sends, messages to an agent, a channel or a group are tagged for agents and folded away from people by default. Pass --for-human or --audience human to tag a message for people; --audience agent|both is for agents and bots only.");
1733
1828
  dm
1734
1829
  .command("send [recipient] [message]", { isDefault: true, hidden: true })
1735
1830
  .description('Send a direct message. RECIPIENT can be a person (email, personUid, or agentUid), a GROUP DM (comma-separated: "a@x.com,b@y.com"), or one of your DM CHANNELS by name — bare (hq dm vyg-dev "hi"), hash form (hq dm "#vyg-dev" "hi"), or via --channel (hq dm --channel vyg-dev "hi"). A person receives a DM as an HQ Sync notification; an agent receives it in its durable box inbox. If you aren\'t connected yet, it sends a connection request that holds your message. See your channels with `hq channels`.\n\nCHANNEL MENTIONS: HQ mentions are structured — the server never reads `@name` out of your text, so a channel message has to carry the participants alongside the body. When you write `@name` in a channel message, this command looks the name up in the channel roster (case-insensitive on display name and slug/handle; quote multi-word names as @"Jacob Posel") and sends it as a real mention, which notifies people and wakes agents. If a name is unknown or matches more than one member, the send FAILS with the candidates instead of posting something that mentions nobody. A bare "@" or an email address in your prose is never treated as a mention. Pass --no-mentions to post the text literally.\n\nCHANNEL THREADS: `hq dm <channel> --thread <eventId> "…"` replies under an existing root (the request carries rootEventId). `--thread last` replies to your most recent top-level message in that channel. Copy a root eventId from `hq dm channel <name>`, which prints each root\'s id and indents replies underneath.')
@@ -1742,7 +1837,7 @@ export function registerDmCommand(program) {
1742
1837
  .option("--details-file <path>", "Read the details from a file (1:1 DMs; channel sends also supported)")
1743
1838
  .option("--at <iso>", "Schedule delivery at an ISO8601 time (store-and-forward; delivered within ~60s of the time; 1:1 DMs only)")
1744
1839
  .option("--in <duration>", "Schedule delivery after a relative delay: 30s, 10m, 2h, 1d (1:1 DMs only)")
1745
- .option("--audience <human|agent|both>", "Tag who this message is for: human (a person must read it), agent (progress/status, hidden from humans by default), or both. Default: human for a person, agent for an agent, a channel or a group. Wins over --for-human.")
1840
+ .option("--audience <human|agent|both>", "Tag who this message is for: human (a person must read it), agent (progress/status, hidden from humans by default), or both. agent and both are for agents and bots only; a person login always sends human. Default for agents and bots: human for a person, agent for an agent, a channel or a group. Wins over --for-human.")
1746
1841
  .option("--for-human", "Tag the message for people (alias for --audience human).")
1747
1842
  .option("--attach <path>", "Upload a local file and attach it to the message (repeatable, up to 5 files, 25 MB each). 1:1 DMs and named channels; the file lands in the conversation's vault scope and the recipient can open it. Not available for group DMs or scheduled sends.", (value, previous = []) => [...previous, value])
1748
1843
  .action(async (recipient, message, opts) => {
@@ -78,6 +78,7 @@ export declare function registerFilesCommand(program: Command): Command;
78
78
  * whether it is a folder (`Makefile`, `bin/hq`); resolveBareSharePrefix asks
79
79
  * the vault.
80
80
  */
81
+ export declare function isSingleSegmentWildcardPrefix(prefix: string): boolean;
81
82
  export declare function looksLikeBareFolderPrefix(prefix: string): boolean;
82
83
  /**
83
84
  * Refuse a bare prefix that would become a malformed grant if it were
@@ -164,7 +164,7 @@ export function formatShareSessionError(err) {
164
164
  export function registerFilesCommand(program) {
165
165
  const files = program
166
166
  .command("files")
167
- .description("Manage file access controls in HQ vault. A trailing folder slash (foo/) is a private folder; the quoted glob 'foo/*' shares all children.")
167
+ .description("Manage file access controls in HQ vault. A trailing folder slash (foo/) is a private folder; the quoted glob 'foo/*' shares all children, and a quoted 'clients/*/reports' shares that folder under every client.")
168
168
  .option("--company <slug>", "Company slug (resolves to companyUid)");
169
169
  // An exported HQ_API_KEY must never fall back to the caller's Cognito session
170
170
  // (that would ignore the key's scope entirely), so every subcommand is gated.
@@ -193,7 +193,7 @@ export function registerFilesCommand(program) {
193
193
  });
194
194
  files
195
195
  .command("share [paths...]")
196
- .description("Share file paths. foo/ is a private folder (children stay creator-only); quote the shared-folder glob 'foo/*' so the shell passes it literally. Without --with: mint a share-session URL and open it in the browser. With --with: grant access directly to a person, group, or @all.")
196
+ .description("Share file paths. foo/ is a private folder (children stay creator-only); quote the shared-folder glob 'foo/*', or a one-folder wildcard such as 'clients/*/reports', so the shell passes it literally. Without --with: mint a share-session URL and open it in the browser. With --with: grant access directly to a person, group, or @all.")
197
197
  .option("--with <principal>", "Email address, group id, company Outpost id (out_...), or '@all' to share with every active company member")
198
198
  .option("--permission <level>", "Permission level (only with --with): read | write")
199
199
  .option("--exact", "With --with: grant exactly the object named by <prefix>, even when it looks like a folder (by default a bare folder name such as 'knowledge' is granted as the folder glob 'knowledge/*').")
@@ -763,6 +763,8 @@ export function registerFilesCommand(program) {
763
763
  async function warnIfEmptySharePrefix(params) {
764
764
  if (isPrivateFolderPattern(params.prefix))
765
765
  return;
766
+ if (isSingleSegmentWildcardPrefix(params.prefix))
767
+ return;
766
768
  let enabled;
767
769
  try {
768
770
  enabled = await resolveCompanyFlagGate(params.companyUid, params.token, FILES_SHARE_EMPTY_PREFIX_WARNING_FLAG, { fallback: false }, () => false);
@@ -879,9 +881,21 @@ async function resolveMentionPersonUid(mention, companySlug) {
879
881
  * whether it is a folder (`Makefile`, `bin/hq`); resolveBareSharePrefix asks
880
882
  * the vault.
881
883
  */
884
+ export function isSingleSegmentWildcardPrefix(prefix) {
885
+ const body = prefix.endsWith("/*") ? prefix.slice(0, -2) : prefix;
886
+ const segments = body.split("/");
887
+ if (segments.some((segment) => segment.length === 0))
888
+ return false;
889
+ if (segments.some((segment) => segment !== "*" && segment.includes("*")))
890
+ return false;
891
+ const wildcards = segments.filter((segment) => segment === "*").length;
892
+ return wildcards === 1 && wildcards < segments.length;
893
+ }
882
894
  export function looksLikeBareFolderPrefix(prefix) {
883
895
  if (prefix === "*" || prefix.endsWith("/") || prefix.endsWith("/*"))
884
896
  return false;
897
+ if (isSingleSegmentWildcardPrefix(prefix))
898
+ return false;
885
899
  const last = prefix.slice(prefix.lastIndexOf("/") + 1);
886
900
  return last.length > 0 && !last.includes(".");
887
901
  }
@@ -894,8 +908,12 @@ export function malformedBareSharePrefix(prefix) {
894
908
  if (!looksLikeBareFolderPrefix(prefix))
895
909
  return null;
896
910
  if (prefix.includes("*")) {
897
- return (`'${prefix}' has a '*' inside a path segment. Only a whole trailing '/*' segment ` +
898
- `(for example 'knowledge/*') or '*' alone (--full) is a glob.`);
911
+ const reason = prefix.split("/").some((segment) => segment !== "*" && segment.includes("*"))
912
+ ? "has a '*' inside a path segment"
913
+ : "has more than one '*' folder";
914
+ return (`'${prefix}' ${reason}. A '*' must be a whole folder name: once per path ` +
915
+ `(for example 'clients/*/reports'), as a trailing '/*' (for example 'knowledge/*'), ` +
916
+ `or '*' alone (--full).`);
899
917
  }
900
918
  if (prefix.split("/").some((segment) => segment.length === 0)) {
901
919
  return `'${prefix}' has an empty path segment. Pass a company-relative path such as 'knowledge'.`;
@@ -161,7 +161,10 @@ export interface InstallInput {
161
161
  placement: "header";
162
162
  header: string;
163
163
  };
164
+ scope?: ConnectScope;
164
165
  }
166
+ export type ConnectScope = "personal" | "company";
167
+ export declare function scopeUnavailableMessage(scope: ConnectScope): string;
165
168
  export declare function installIntegration(token: string, companyUid: string, input: InstallInput): Promise<InstallResult>;
166
169
  export declare function uninstallIntegration(token: string, companyUid: string, target: {
167
170
  installationId: string;
@@ -227,6 +230,7 @@ export interface OAuthStartInput {
227
230
  * direct `mcpUrl` path.
228
231
  */
229
232
  scopes?: string[];
233
+ scope?: ConnectScope;
230
234
  }
231
235
  export declare function startOAuth(token: string, companyUid: string, input: OAuthStartInput): Promise<OAuthStartResult>;
232
236
  export declare function completeOAuth(token: string, companyUid: string, input: {
@@ -53,6 +53,22 @@ export async function discoverDocs(token, companyUid, docsUrl) {
53
53
  await raiseForResponse(res, "Failed to read that documentation page");
54
54
  return (await res.json());
55
55
  }
56
+ export function scopeUnavailableMessage(scope) {
57
+ const flag = scope === "personal" ? "--personal" : "--company-owned";
58
+ return ("Personal and company integrations aren't turned on for this company yet. " +
59
+ `Run the command again without ${flag} to connect the usual way.`);
60
+ }
61
+ async function raiseForConnectResponse(res, fallback, scope) {
62
+ try {
63
+ return await raiseForResponse(res, fallback);
64
+ }
65
+ catch (error) {
66
+ if (scope && error instanceof IntegrationsCliError && error.status === 404 && !error.code) {
67
+ throw new IntegrationsCliError(scopeUnavailableMessage(scope), { expected: true, status: 404 });
68
+ }
69
+ throw error;
70
+ }
71
+ }
56
72
  export async function installIntegration(token, companyUid, input) {
57
73
  const res = await vaultApiFetch({
58
74
  token,
@@ -62,7 +78,7 @@ export async function installIntegration(token, companyUid, input) {
62
78
  });
63
79
  if (!res.ok) {
64
80
  try {
65
- await raiseForResponse(res, "Failed to connect the app");
81
+ await raiseForConnectResponse(res, "Failed to connect the app", input.scope);
66
82
  }
67
83
  catch (error) {
68
84
  if (error instanceof IntegrationsCliError &&
@@ -106,7 +122,7 @@ export async function startOAuth(token, companyUid, input) {
106
122
  body: { companyUid, ...input },
107
123
  });
108
124
  if (!res.ok)
109
- await raiseForResponse(res, "Failed to start sign-in");
125
+ await raiseForConnectResponse(res, "Failed to start sign-in", input.scope);
110
126
  return (await res.json());
111
127
  }
112
128
  export async function completeOAuth(token, companyUid, input) {
@@ -27,7 +27,7 @@ import { type ResidentCommandLeaseOptions } from "../utils/resident-package-prel
27
27
  import { consoleOrigin } from "../utils/console-origin.js";
28
28
  export { consoleOrigin };
29
29
  import type { AdminConnection } from "./integrations-core.js";
30
- import { type InstallInput } from "./integrations-api.js";
30
+ import { type ConnectScope, type InstallInput } from "./integrations-api.js";
31
31
  interface ConnectOptions {
32
32
  company?: string;
33
33
  entryId?: string;
@@ -54,7 +54,10 @@ interface ConnectOptions {
54
54
  * to a person and check `hq integrations list` afterwards.
55
55
  */
56
56
  linkOnly?: boolean;
57
+ personal?: boolean;
58
+ companyOwned?: boolean;
57
59
  }
60
+ export declare function connectScopeFromOptions(opts: Pick<ConnectOptions, "personal" | "companyOwned">): ConnectScope | undefined;
58
61
  /**
59
62
  * Validate `--auth` against the supported enum, rejecting an unknown value
60
63
  * before anything is installed. A finite provider/runtime option must never be
@@ -69,7 +72,7 @@ export declare function shellQuote(value: string): string;
69
72
  /** Everything the connect verbs need to name one app to hq-pro. */
70
73
  interface ConnectTarget {
71
74
  /** Install/OAuth identity fields, exactly as hq-pro expects them. */
72
- ref: Pick<InstallInput, "domain" | "query" | "catalogEntryId" | "discoveryReceiptId" | "mcpUrl" | "provider" | "displayName" | "docsUrl">;
75
+ ref: Pick<InstallInput, "domain" | "query" | "catalogEntryId" | "discoveryReceiptId" | "mcpUrl" | "provider" | "displayName" | "docsUrl" | "scope">;
73
76
  /** Best-known auth mode before hq-pro is asked; `undefined` = unknown. */
74
77
  authClass?: "none" | "key" | "oauth";
75
78
  /** Human label for prompts and success lines. */
@@ -109,6 +112,10 @@ export declare function bringYourOwnClientHandoff(target: ConnectTarget, opts: C
109
112
  * built, so the hint always names the URL Google will actually be sent.
110
113
  */
111
114
  export declare function googleClientTypeHint(clientId: string, authorizationUrl: string): string | null;
112
- export declare function findAlreadyConnected(connections: readonly AdminConnection[], app: string): AdminConnection | undefined;
115
+ export interface ConnectOwnership {
116
+ scope: ConnectScope;
117
+ viewerPersonUid?: string;
118
+ }
119
+ export declare function findAlreadyConnected(connections: readonly AdminConnection[], app: string, ownership?: ConnectOwnership): AdminConnection | undefined;
113
120
  export declare function registerConnectCommands(integrations: Command): void;
114
121
  //# sourceMappingURL=integrations-connect.d.ts.map
@@ -31,7 +31,7 @@ import { consoleOrigin } from "../utils/console-origin.js";
31
31
  export { consoleOrigin };
32
32
  import { getCompanyUid } from "../utils/vault-api.js";
33
33
  import { redactSensitiveUrl, redactUrlsInText } from "../utils/redact-url.js";
34
- import { IntegrationsCliError, bareProvider, connectionDomain, fetchAdminSurface, printJson, revokedConnectionDetails, resolveConnection, } from "./integrations-core.js";
34
+ import { IntegrationsCliError, bareProvider, connectionAccountLabel, connectionDomain, connectionOwnerLabel, fetchAdminSurface, printJson, revokedConnectionDetails, resolveConnection, } from "./integrations-core.js";
35
35
  import { completeOAuth, discoverDocs, installIntegration, listCatalog, pullBlueprint, startOAuth, } from "./integrations-api.js";
36
36
  import { OAUTH_LOOPBACK_TIMEOUT_CODE, startLoopbackListener, } from "./integrations-oauth.js";
37
37
  /** hq-pro's machine code for "this endpoint needs a browser sign-in". */
@@ -103,6 +103,19 @@ const BRING_YOUR_OWN_CLIENT_CODES = new Set([
103
103
  "OAUTH_REGISTRATION_UNSUPPORTED",
104
104
  "CLIENT_REGISTRATION_REFUSED",
105
105
  ]);
106
+ export function connectScopeFromOptions(opts) {
107
+ if (opts.personal && opts.companyOwned) {
108
+ throw new IntegrationsCliError("--personal and --company-owned cannot be used together; pick one.", { expected: true });
109
+ }
110
+ if (opts.personal)
111
+ return "personal";
112
+ if (opts.companyOwned)
113
+ return "company";
114
+ return undefined;
115
+ }
116
+ function scopeFlag(scope) {
117
+ return scope === "personal" ? "--personal" : "--company-owned";
118
+ }
106
119
  /**
107
120
  * Server-side limits, restated here so a typo fails in the terminal with a
108
121
  * sentence rather than as an opaque 400 three network hops away. Kept in step
@@ -758,6 +771,10 @@ async function consoleHandoff(target, opts) {
758
771
  if (opts.scopes) {
759
772
  console.error(chalk.yellow(`Note: --scopes cannot be carried into a console sign-in, so this connect will use ${target.label}'s default permissions.`));
760
773
  }
774
+ const scope = connectScopeFromOptions(opts);
775
+ if (scope) {
776
+ console.error(chalk.yellow(`Note: ${scopeFlag(scope)} cannot be carried into a console sign-in; choose who owns ${target.label} on the console page.`));
777
+ }
761
778
  if (url && opts.browser !== false)
762
779
  await open(url).catch((error) => warnBrowserOpenFailure(error));
763
780
  console.error(chalk.dim("When the browser says it connected, run `hq integrations list` to confirm."));
@@ -820,6 +837,9 @@ function byoClientCommand(target, opts) {
820
837
  // caller a line that reproduces the failure they are recovering from.
821
838
  if (opts.scopes)
822
839
  parts.push(`--scopes ${shellQuote(opts.scopes)}`);
840
+ const scope = connectScopeFromOptions(opts);
841
+ if (scope)
842
+ parts.push(scopeFlag(scope));
823
843
  return parts.join(" ");
824
844
  }
825
845
  /**
@@ -1035,6 +1055,7 @@ async function targetForAccessKeyConnect(token, companyUid, target, opts) {
1035
1055
  return {
1036
1056
  ref: {
1037
1057
  mcpUrl,
1058
+ ...(target.ref.scope ? { scope: target.ref.scope } : {}),
1038
1059
  ...(opts.provider ? { provider: opts.provider } : blueprint.provider ? { provider: blueprint.provider } : {}),
1039
1060
  ...(opts.displayName
1040
1061
  ? { displayName: opts.displayName }
@@ -1079,7 +1100,9 @@ function reportInstall(result, opts, expectedRevivedConnectionId) {
1079
1100
  * URL: hq-pro recognizes the domain and revives the revoked row in place.
1080
1101
  */
1081
1102
  async function connectApp(token, companyUid, app, opts, expectedRevivedConnectionId) {
1082
- const target = await resolveTarget(token, companyUid, app, opts, expectedRevivedConnectionId !== undefined);
1103
+ const resolved = await resolveTarget(token, companyUid, app, opts, expectedRevivedConnectionId !== undefined);
1104
+ const scope = connectScopeFromOptions(opts);
1105
+ const target = scope ? { ...resolved, ref: { ...resolved.ref, scope } } : resolved;
1083
1106
  const authMode = opts.authHeader
1084
1107
  ? "key"
1085
1108
  : opts.auth ?? target.authClass;
@@ -1205,7 +1228,16 @@ function isSettledInstallation(connection) {
1205
1228
  const status = connection.installation?.status?.trim().toLowerCase();
1206
1229
  return status === undefined || status === "" || status === "installed";
1207
1230
  }
1208
- export function findAlreadyConnected(connections, app) {
1231
+ function hasRequestedOwnership(connection, ownership) {
1232
+ const ownerType = connection.ownerType;
1233
+ if (ownership.scope === "personal") {
1234
+ return (ownerType === "person" &&
1235
+ Boolean(ownership.viewerPersonUid) &&
1236
+ connection.ownerId === ownership.viewerPersonUid);
1237
+ }
1238
+ return ownerType === undefined || ownerType === "company" || ownerType === "legacy";
1239
+ }
1240
+ export function findAlreadyConnected(connections, app, ownership) {
1209
1241
  const want = app.trim().toLowerCase();
1210
1242
  if (want === "")
1211
1243
  return undefined;
@@ -1215,6 +1247,8 @@ export function findAlreadyConnected(connections, app) {
1215
1247
  return false;
1216
1248
  if (!isSettledInstallation(connection))
1217
1249
  return false;
1250
+ if (ownership && !hasRequestedOwnership(connection, ownership))
1251
+ return false;
1218
1252
  const domain = (connectionDomain(connection) ?? "").toLowerCase();
1219
1253
  if (domain === want || domain.endsWith(`.${want}`))
1220
1254
  return true;
@@ -1254,22 +1288,31 @@ async function refuseIfAlreadyConnected(token, companyUid, app, opts) {
1254
1288
  // the deliberate "yes, add another one" escape hatch.
1255
1289
  if (!app || opts.force || opts.mcpUrl || opts.docsUrl || opts.entryId)
1256
1290
  return false;
1291
+ const requestedScope = connectScopeFromOptions(opts);
1257
1292
  let connections;
1258
1293
  let viewer;
1294
+ let personalConnect;
1259
1295
  try {
1260
1296
  const surface = await fetchAdminSurface(token, companyUid);
1261
1297
  connections = surface.connections ?? [];
1262
1298
  viewer = surface.viewer;
1299
+ personalConnect = surface.capabilities?.personalConnect === true;
1263
1300
  }
1264
1301
  catch {
1265
1302
  // Never let this convenience check block a connect. If the list cannot be
1266
1303
  // read, fall through and let the real flow report whatever is wrong.
1267
1304
  return false;
1268
1305
  }
1269
- const existing = findAlreadyConnected(connections, app);
1306
+ const scope = requestedScope ?? (personalConnect ? "personal" : undefined);
1307
+ const ownership = scope
1308
+ ? { scope, ...(viewer?.personUid ? { viewerPersonUid: viewer.personUid } : {}) }
1309
+ : undefined;
1310
+ const existing = findAlreadyConnected(connections, app, ownership);
1270
1311
  if (!existing)
1271
1312
  return false;
1272
1313
  const slug = bareProvider(existing.provider);
1314
+ const owner = connectionOwnerLabel(existing);
1315
+ const account = connectionAccountLabel(existing);
1273
1316
  if (opts.json) {
1274
1317
  printJson({
1275
1318
  ok: true,
@@ -1277,11 +1320,14 @@ async function refuseIfAlreadyConnected(token, companyUid, app, opts) {
1277
1320
  provider: slug,
1278
1321
  connection: existing.id,
1279
1322
  ...(connectionDomain(existing) ? { domain: connectionDomain(existing) } : {}),
1323
+ ...(existing.ownerType ? { ownerType: existing.ownerType } : {}),
1324
+ ...(existing.accountLabel ? { accountLabel: existing.accountLabel } : {}),
1280
1325
  });
1281
1326
  return true;
1282
1327
  }
1283
1328
  const label = redactUrlsInText(existing.installation?.displayName ?? slug);
1284
- console.log(chalk.green(`${label} is already connected.`));
1329
+ const detail = [owner, account].filter(Boolean).join(", ");
1330
+ console.log(chalk.green(`${label} is already connected${detail ? ` (${detail})` : ""}.`));
1285
1331
  if (connectedBySomeoneElse(existing, viewer)) {
1286
1332
  // Members may add apps and reconnect their own, but replacing a connection
1287
1333
  // someone else created stays with an owner or admin: the server refuses
@@ -1291,7 +1337,7 @@ async function refuseIfAlreadyConnected(token, companyUid, app, opts) {
1291
1337
  return true;
1292
1338
  }
1293
1339
  console.log(chalk.dim(` Sign in again: hq integrations reconnect --provider ${slug}\n` +
1294
- ` Add a second: hq integrations connect ${shellQuote(app)} --force`));
1340
+ ` Add a second: hq integrations connect ${shellQuote(app)}${requestedScope ? ` ${scopeFlag(requestedScope)}` : ""} --force`));
1295
1341
  return true;
1296
1342
  }
1297
1343
  export function registerConnectCommands(integrations) {
@@ -1425,8 +1471,11 @@ export function registerConnectCommands(integrations) {
1425
1471
  .option("--timeout <seconds>", "How long to wait for a browser sign-in (default 300)")
1426
1472
  .option("--link-only", "Print the sign-in link and exit without waiting; the sign-in finishes on the HQ console")
1427
1473
  .option("--force", "Connect again even if this app is already connected")
1474
+ .option("--personal", "Connect it as your own personal connection (not --company <slug>, which picks the company)")
1475
+ .option("--company-owned", "Connect it as a connection the whole company owns (needs permission to add company apps)")
1428
1476
  .option("--json", "Machine-readable output")
1429
1477
  .action(async (app, opts) => {
1478
+ connectScopeFromOptions(opts);
1430
1479
  assertAuthMode(opts.auth);
1431
1480
  assertAuthHeader(opts.authHeader);
1432
1481
  if (opts.authHeader && opts.auth !== undefined && opts.auth !== "key") {
@@ -1453,6 +1502,8 @@ export function registerConnectCommands(integrations) {
1453
1502
  .option("--no-browser", "Print the sign-in URL instead of opening a browser")
1454
1503
  .option("--timeout <seconds>", "How long to wait for a browser sign-in (default 300)")
1455
1504
  .option("--connect", "For a revoked row, run `connect <domain>` to re-add and revive it")
1505
+ .option("--personal", "With --connect: re-add it as your own personal connection")
1506
+ .option("--company-owned", "With --connect: re-add it as a connection the whole company owns")
1456
1507
  .option("--json", "Machine-readable output")
1457
1508
  .action(async (app, opts) => {
1458
1509
  // Same validation as `connect`. Without it a typo like `--auth oauth2`
@@ -1460,11 +1511,19 @@ export function registerConnectCommands(integrations) {
1460
1511
  // silently reinstalls with auto-detection — the caller believes they
1461
1512
  // forced a mode that was never applied.
1462
1513
  assertAuthMode(opts.auth);
1514
+ const scope = connectScopeFromOptions(opts);
1515
+ if (scope && !opts.connect) {
1516
+ throw new IntegrationsCliError(`${scopeFlag(scope)} only applies with --connect, when a revoked app is added again.`, { expected: true });
1517
+ }
1463
1518
  const token = await ensureCognitoIdToken();
1464
1519
  const companyUid = await getCompanyUid(token, opts.company);
1465
1520
  const connection = await resolveConnection(token, companyUid, app, opts, {
1466
1521
  allowSingleRevoked: Boolean(opts.connect),
1467
1522
  });
1523
+ if (scope && connection.status !== "revoked") {
1524
+ throw new IntegrationsCliError(`${scopeFlag(scope)} only applies when --connect adds a revoked app again; this one is ${connection.status}. ` +
1525
+ "A reconnect keeps the connection's current owner.", { expected: true });
1526
+ }
1468
1527
  if (connection.status === "revoked") {
1469
1528
  const details = revokedConnectionDetails(connection, opts.company);
1470
1529
  if (!opts.connect) {
@@ -1492,6 +1551,11 @@ export function registerConnectCommands(integrations) {
1492
1551
  ...(connection.installation?.displayName
1493
1552
  ? { displayName: connection.installation.displayName }
1494
1553
  : {}),
1554
+ ...(connection.ownerType === "company"
1555
+ ? { scope: "company" }
1556
+ : connection.ownerType === "person"
1557
+ ? { scope: "personal" }
1558
+ : {}),
1495
1559
  },
1496
1560
  ...(opts.auth ? { authClass: opts.auth } : {}),
1497
1561
  label: connection.installation?.displayName ?? bareProvider(connection.provider),