@indigoai-us/hq-cli 5.345.165 → 5.345.168

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 (37) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/assets/scaffold/core/scripts/hq-delegate-send.sh +1 -1
  3. package/assets/scaffold/core/scripts/rebuild-all-indexes.sh +11 -0
  4. package/assets/scaffold/core/scripts/rebuild-orchestrator-index.sh +11 -0
  5. package/assets/scaffold/core/scripts/rebuild-public-knowledge-index.sh +11 -0
  6. package/assets/scaffold/core/scripts/rebuild-reports-index.sh +11 -0
  7. package/assets/scaffold/core/scripts/rebuild-social-drafts-index.sh +11 -0
  8. package/assets/scaffold/core/scripts/rebuild-threads-index.sh +11 -0
  9. package/assets/scaffold/core/scripts/rebuild-workers-index.sh +11 -0
  10. package/dist/command-catalog.generated.d.ts +6 -2
  11. package/dist/command-catalog.generated.js +7 -2
  12. package/dist/commands/access.js +2 -0
  13. package/dist/commands/agents.js +2 -2
  14. package/dist/commands/core-delegate-w1b.js +2 -1
  15. package/dist/commands/core-dm-bind-native.js +3 -1
  16. package/dist/commands/dm.d.ts +13 -2
  17. package/dist/commands/dm.js +42 -9
  18. package/dist/commands/integrations-api.d.ts +15 -0
  19. package/dist/commands/integrations-api.js +27 -0
  20. package/dist/commands/integrations-manage.js +4 -2
  21. package/dist/commands/integrations.js +9 -5
  22. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +210 -197
  23. package/dist/hq-cloud-sync-runner/package.json +1 -1
  24. package/dist/lib/agent-kit/mcp/tools.js +5 -6
  25. package/dist/lib/doctor/checks/sync-runtime.d.ts +7 -2
  26. package/dist/lib/doctor/checks/sync-runtime.js +1 -3
  27. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  28. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  29. package/dist/lib/index-render/index.d.ts +5 -0
  30. package/dist/lib/index-render/index.js +22 -1
  31. package/dist/lib/notify-recipient.d.ts +15 -0
  32. package/dist/lib/notify-recipient.js +15 -0
  33. package/dist/lib/secret-scan/repo-config.d.ts +8 -2
  34. package/dist/lib/secret-scan/repo-config.js +12 -6
  35. package/dist/lib/secret-scan/staged.d.ts +6 -0
  36. package/dist/lib/secret-scan/staged.js +21 -3
  37. package/package.json +8 -8
package/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.168] — 2026-10-24
6
+ - `hq dm` now picks the default audience from the recipient when no audience flag is passed. A 1:1 DM to a person (email, `prs_` uid, or a bare name that resolves to a person) sends `audience: "human"`. A 1:1 DM to an agent (`agt_` uid), a group DM and a channel post send `audience: "agent"`, so script and lane messages stay out of people's conversations. Scheduled sends (`--at`, `--in`) follow the same rule. Pass `--for-human` (alias for `--audience human`) to tag any message for people; an explicit `--audience human|agent|both` wins over `--for-human`, with a warning on stderr, and is validated on every path. `hq core delegate send` tags the pickup DM `audience: "both"`, `hq dm-bind post` keeps the channel default (`audience: "agent"`) for its automated status posts, and the agent MCP `hq_dm_send` tool, `hq agents message` and the `hq access` grant request apply the recipient rule. Reading commands (inbox, thread, channel) are unchanged, and `hq dm forward` does not set an audience.
7
+ - `hq scan secrets` no longer exits 2 when a repo's `.gitleaks.toml` allowlist is rejected. In staged and file scans it now prints a fixed warning that quotes no part of the file and scans with no repo allowlist, so a real key is still reported (exit 1) and a commit that replaces a bad allowlist is not blocked. An unknown `--allowlist-rev` still exits 2.
8
+
9
+ ## [5.345.166] — 2026-10-24
10
+ - `hq integrations approve` now says why an approved call did not run. When an app refuses it, the CLI prints the app's own reason, such as Slack's refusal to send into a Slack Connect channel. Before this it printed only "failed (rejected)". With an older server it prints the rejection code instead. `hq integrations pending --history` adds a `Why:` line for decided calls that did not run. The approve command printed after a queued call keeps `--company` when you passed it. Before printing, the reason is cleaned of terminal escape codes, credentials and credential-bearing links, and it is capped at 500 characters. Feedback 15e7593c-50e5-4e6a-ae3a-342d618363c0.
11
+ - The bundled sync engine now uses hq-cloud 6.18.101, the current npm `latest`.
12
+ A 403 from the flags endpoint (for example a stale company in the local
13
+ manifest) is now held as `false` for an hour instead of being re-read and
14
+ logged on every telemetry pass (hq-cloud #1046).
15
+ - `hq doctor` no longer counts runner telemetry errors as sync receiver permission refusals. A failed outcome-flag read ("hq-flags: resolve returned 403") for a company you are not a member of used to inflate `sync.runtime.receiver-permission`; only the receiver's own events are counted now.
16
+ - The bundled shell fallbacks for `rebuild-index` all, orchestrator, public-knowledge, reports, social-drafts, threads, and workers now print the same usage as the native command for `--help`/`-h` and exit without writing. Before, they ignored the flag and rebuilt the index, which failed the published core command parity check.
17
+
5
18
  ## [5.345.165] — 2026-10-23
6
19
  - `hq billing sponsor grant <email|prs_id> --company <slug>` lets a company owner or admin put a member's personal plan on the company bill without a request first; `--all-members` does it for every active member and reports each person as covered, already covered, or skipped. `hq billing sponsor ungrant <email|prs_id>` takes it off again. hq-pro checks the owner/admin role and active membership.
7
20
  - `hq scan secrets <files...>` takes `--repo <dir>`, which applies that repo's committed `.gitleaks.toml` allowlists to a file scan, matching each file by its path relative to the repo, as a staged scan does. `--snapshot-root <dir>` (with `--repo`) matches files laid out as `<dir>/<commit>/<repo path>`, the history copies `commit-secret-scan.sh pr-check` writes, as `<repo path>`. Findings still name the files as given. Before this, a file scan ignored the repo's allowlists, so pr-check failed on false positives the commit gate already allowed. Without `--repo` a file scan is unchanged.
@@ -283,7 +283,7 @@ if ! jq '.status = "sending"' "$MANIFEST" > "$TMP_MANIFEST" || ! mv "$TMP_MANIFE
283
283
  rm -f "$TMP_MANIFEST"
284
284
  die "could not persist sending state; no DM attempted"
285
285
  fi
286
- DM_OUT="$(hq dm "$PRINCIPAL" "$HEADLINE" --prompt-file "$PROMPT" --details-file "$BRIEF")" \
286
+ DM_OUT="$(hq dm "$PRINCIPAL" "$HEADLINE" --audience both --prompt-file "$PROMPT" --details-file "$BRIEF")" \
287
287
  || die "hq dm delivery outcome is uncertain; status is sending, inspect DM history before retrying"
288
288
 
289
289
  EVENT_ID="$(printf '%s' "$DM_OUT" | grep -oE 'eventId[: ]+[A-Za-z0-9_-]+' | head -1 | sed -E 's/eventId[: ]+//' || true)"
@@ -11,6 +11,17 @@
11
11
 
12
12
  set -uo pipefail
13
13
 
14
+ # Help must never write: print the same usage as `hq core rebuild-index all --help`.
15
+ for arg in "$@"; do
16
+ if [[ "$arg" == "--help" || "$arg" == "-h" ]]; then
17
+ printf '%s\n' \
18
+ 'Usage: hq core rebuild-index all [--accept]' \
19
+ '' \
20
+ 'Regenerate every INDEX.md across the HQ tree. Help performs no writes.'
21
+ exit 0
22
+ fi
23
+ done
24
+
14
25
  HQ_ROOT="${HQ_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
15
26
  cd "$HQ_ROOT"
16
27
 
@@ -12,6 +12,17 @@
12
12
 
13
13
  set -euo pipefail
14
14
 
15
+ # Help must never write: print the same usage as `hq core rebuild-index orchestrator --help`.
16
+ for arg in "$@"; do
17
+ if [[ "$arg" == "--help" || "$arg" == "-h" ]]; then
18
+ printf '%s\n' \
19
+ 'Usage: hq core rebuild-index orchestrator [--accept]' \
20
+ '' \
21
+ 'Regenerate the orchestrator workspace INDEX.md. Help performs no writes.'
22
+ exit 0
23
+ fi
24
+ done
25
+
15
26
  HQ_ROOT="${HQ_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
16
27
  cd "$HQ_ROOT"
17
28
 
@@ -7,6 +7,17 @@
7
7
 
8
8
  set -euo pipefail
9
9
 
10
+ # Help must never write: print the same usage as `hq core rebuild-index public-knowledge --help`.
11
+ for arg in "$@"; do
12
+ if [[ "$arg" == "--help" || "$arg" == "-h" ]]; then
13
+ printf '%s\n' \
14
+ 'Usage: hq core rebuild-index public-knowledge [--accept]' \
15
+ '' \
16
+ 'Regenerate core/knowledge/public/INDEX.md. Help performs no writes.'
17
+ exit 0
18
+ fi
19
+ done
20
+
10
21
  HQ_ROOT="${HQ_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
11
22
  cd "$HQ_ROOT"
12
23
 
@@ -8,6 +8,17 @@
8
8
 
9
9
  set -euo pipefail
10
10
 
11
+ # Help must never write: print the same usage as `hq core rebuild-index reports --help`.
12
+ for arg in "$@"; do
13
+ if [[ "$arg" == "--help" || "$arg" == "-h" ]]; then
14
+ printf '%s\n' \
15
+ 'Usage: hq core rebuild-index reports [--accept]' \
16
+ '' \
17
+ 'Regenerate the reports INDEX.md. Help performs no writes.'
18
+ exit 0
19
+ fi
20
+ done
21
+
11
22
  HQ_ROOT="${HQ_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
12
23
  cd "$HQ_ROOT"
13
24
 
@@ -8,6 +8,17 @@
8
8
 
9
9
  set -euo pipefail
10
10
 
11
+ # Help must never write: print the same usage as `hq core rebuild-index social-drafts --help`.
12
+ for arg in "$@"; do
13
+ if [[ "$arg" == "--help" || "$arg" == "-h" ]]; then
14
+ printf '%s\n' \
15
+ 'Usage: hq core rebuild-index social-drafts [--accept]' \
16
+ '' \
17
+ 'Regenerate workspace/social-drafts/INDEX.md. Help performs no writes.'
18
+ exit 0
19
+ fi
20
+ done
21
+
11
22
  HQ_ROOT="${HQ_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
12
23
  # shellcheck source=core/scripts/lib/portable.sh
13
24
  . "$HQ_ROOT/core/scripts/lib/portable.sh"
@@ -11,6 +11,17 @@
11
11
 
12
12
  set -euo pipefail
13
13
 
14
+ # Help must never write: print the same usage as `hq core rebuild-index threads --help`.
15
+ for arg in "$@"; do
16
+ if [[ "$arg" == "--help" || "$arg" == "-h" ]]; then
17
+ printf '%s\n' \
18
+ 'Usage: hq core rebuild-index threads [--accept]' \
19
+ '' \
20
+ 'Regenerate the session-threads INDEX.md. Help performs no writes.'
21
+ exit 0
22
+ fi
23
+ done
24
+
14
25
  HQ_ROOT="${HQ_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
15
26
  cd "$HQ_ROOT"
16
27
 
@@ -7,6 +7,17 @@
7
7
 
8
8
  set -euo pipefail
9
9
 
10
+ # Help must never write: print the same usage as `hq core rebuild-index workers --help`.
11
+ for arg in "$@"; do
12
+ if [[ "$arg" == "--help" || "$arg" == "-h" ]]; then
13
+ printf '%s\n' \
14
+ 'Usage: hq core rebuild-index workers [--accept]' \
15
+ '' \
16
+ 'Regenerate the workers INDEX.md. Help performs no writes.'
17
+ exit 0
18
+ fi
19
+ done
20
+
10
21
  HQ_ROOT="${HQ_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
11
22
  cd "$HQ_ROOT"
12
23
 
@@ -3987,7 +3987,7 @@ export declare const COMMAND_CATALOG: readonly [{
3987
3987
  }];
3988
3988
  }, {
3989
3989
  readonly name: "dm";
3990
- readonly description: "Send and read direct messages, and manage connection requests.";
3990
+ readonly 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.";
3991
3991
  readonly aliases: readonly [];
3992
3992
  readonly hidden: false;
3993
3993
  readonly helpOption: true;
@@ -4054,7 +4054,11 @@ export declare const COMMAND_CATALOG: readonly [{
4054
4054
  readonly hidden: false;
4055
4055
  }, {
4056
4056
  readonly flags: "--audience <human|agent|both>";
4057
- readonly description: "Tag who this message is for: human (a person must read it), agent (progress/status, hidden from humans by default), or both";
4057
+ readonly description: "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.";
4058
+ readonly hidden: false;
4059
+ }, {
4060
+ readonly flags: "--for-human";
4061
+ readonly description: "Tag the message for people (alias for --audience human).";
4058
4062
  readonly hidden: false;
4059
4063
  }, {
4060
4064
  readonly flags: "--attach <path>";
@@ -4827,7 +4827,7 @@ export const COMMAND_CATALOG = [
4827
4827
  },
4828
4828
  {
4829
4829
  "name": "dm",
4830
- "description": "Send and read direct messages, and manage connection requests.",
4830
+ "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.",
4831
4831
  "aliases": [],
4832
4832
  "hidden": false,
4833
4833
  "helpOption": true,
@@ -4908,7 +4908,12 @@ export const COMMAND_CATALOG = [
4908
4908
  },
4909
4909
  {
4910
4910
  "flags": "--audience <human|agent|both>",
4911
- "description": "Tag who this message is for: human (a person must read it), agent (progress/status, hidden from humans by default), or both",
4911
+ "description": "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.",
4912
+ "hidden": false
4913
+ },
4914
+ {
4915
+ "flags": "--for-human",
4916
+ "description": "Tag the message for people (alias for --audience human).",
4912
4917
  "hidden": false
4913
4918
  },
4914
4919
  {
@@ -25,6 +25,7 @@ import { listJournals, syncDoctor, } from "@indigoai-us/hq-cloud";
25
25
  import { listActiveMembers } from "./members.js";
26
26
  import { aclLockedBy } from "./acl-lock.js";
27
27
  import { buildDmBody } from "./dm.js";
28
+ import { defaultAudienceFor, recipientKindOf } from "../lib/notify-recipient.js";
28
29
  import { peekIdToken } from "../utils/id-token.js";
29
30
  import { findRecentAccessRequest, formatTimeAgo, recordAccessRequest, } from "../utils/access-requests.js";
30
31
  import { recordAccessOutcome } from "../utils/access-outcomes.js";
@@ -1155,6 +1156,7 @@ async function buildLiveAccessDeps(slug, hqRoot) {
1155
1156
  prompt: msg.prompt,
1156
1157
  details: msg.details,
1157
1158
  now: Date.now(),
1159
+ audience: defaultAudienceFor(recipientKindOf(msg.recipient)),
1158
1160
  });
1159
1161
  const res = await vaultApiFetch({
1160
1162
  token,
@@ -42,7 +42,7 @@ import { consoleOrigin } from "./integrations-connect.js";
42
42
  import { launchSessionManagerPlugin, SessionManagerPluginLaunchError, TerminalSessionTimeoutError, } from "@indigoai-us/hq-cloud/outposts/node";
43
43
  import { peekIdToken } from "../utils/id-token.js";
44
44
  import { canOfferTeamUpgrade, formatUpgradeRefusal, isPlanGateError, offerUpgradeLink, } from "../utils/plan-gate-error.js";
45
- import { AGENT_UID_PATTERN, isAgentUid } from "../lib/notify-recipient.js";
45
+ import { AGENT_UID_PATTERN, defaultAudienceFor, isAgentUid } from "../lib/notify-recipient.js";
46
46
  import { AGENT_MODEL_PROVIDERS, modelSelectionErrorMessage, readAgentModelCatalog, validateAgentModelSelection, } from "../lib/agents/model-catalog.js";
47
47
  import { fetchCompanyPricingPlan, isUsageOnly, usageOnlyBoxSentence, usageOnlyCardRequiredLines, } from "../lib/billing/usage-only.js";
48
48
  import { BUNDLED_USAGE_CARD, fetchUsageCard, } from "../lib/billing/usage-card.js";
@@ -1139,7 +1139,7 @@ export async function sendAgentDm(token, agentUid, message) {
1139
1139
  token,
1140
1140
  path: "/v1/notify/dm",
1141
1141
  method: "POST",
1142
- body: { toPersonUid: agentUid, body: message },
1142
+ body: { toPersonUid: agentUid, body: message, audience: defaultAudienceFor("agent") },
1143
1143
  });
1144
1144
  }
1145
1145
  /** Read the two-way DM conversation with an agent (most recent `limit`). */
@@ -1079,7 +1079,8 @@ function runSend(args, context) {
1079
1079
  catch {
1080
1080
  return fail("send", "could not persist sending state; no DM attempted");
1081
1081
  }
1082
- const dm = runHq(["dm", principal, headline, "--prompt-file", prompt, "--details-file", brief], context);
1082
+ // The pickup DM is read by the person and acted on by their agent.
1083
+ const dm = runHq(["dm", principal, headline, "--audience", "both", "--prompt-file", prompt, "--details-file", brief], context);
1083
1084
  if (dm.code !== 0)
1084
1085
  return fail("send", "hq dm delivery outcome is uncertain; status is sending, inspect DM history before retrying");
1085
1086
  const event = /eventId[: ]+([A-Za-z0-9_-]+)/.exec(dm.stdout ?? "")?.[1] ?? "";
@@ -579,6 +579,8 @@ async function post(d, args) {
579
579
  const versionError = requireCliVersion(d);
580
580
  if (versionError)
581
581
  return versionError;
582
+ // An automated status post keeps the channel default (agent audience); only a
583
+ // reply to a message a person wrote is for humans.
582
584
  const call = ["dm", ch, ...(root ? ["--thread", root] : []), cliMessage];
583
585
  const sent = d.runHq(call, { ...d.env, HQ_ROOT: d.root });
584
586
  if (sent.code !== 0)
@@ -594,7 +596,7 @@ async function post(d, args) {
594
596
  const mentions = mentionsJson(d, sid, names);
595
597
  if (!mentions.length)
596
598
  return fail("no mention resolved to a channel member id — not posting an update that notifies nobody");
597
- const payload = { body: postedMessage, mentions };
599
+ const payload = { body: postedMessage, mentions, audience: "agent" };
598
600
  if (root)
599
601
  payload.rootEventId = root;
600
602
  try {
@@ -1,4 +1,6 @@
1
1
  import { Command } from "commander";
2
+ import { defaultAudienceFor, recipientKindOf, type DmRecipientKind } from "../lib/notify-recipient.js";
3
+ export { defaultAudienceFor, recipientKindOf, type DmRecipientKind };
2
4
  import { type DmAttachmentWire } from "./dm-attach.js";
3
5
  export interface DmRecipient {
4
6
  toEmail?: string;
@@ -158,7 +160,8 @@ export declare function resolveMentions(tokens: string[], members: ChannelMember
158
160
  * Returns null on anything that doesn't match. Pure → unit-testable.
159
161
  */
160
162
  export declare function parseDuration(input: string): number | null;
161
- type DmAudience = "human" | "agent" | "both";
163
+ import type { DmAudience } from "../lib/notify-recipient.js";
164
+ export type { DmAudience };
162
165
  export interface DmSendBody {
163
166
  toEmail?: string;
164
167
  toPersonUid?: string;
@@ -233,6 +236,15 @@ export declare function fetchChannels(token: string): Promise<ChannelSummary[]>;
233
236
  * post itself passes. Used to resolve `@name` mentions into participant uids.
234
237
  */
235
238
  export declare function fetchChannelMembers(token: string, channelId: string): Promise<ChannelMember[]>;
239
+ /**
240
+ * Resolve the audience for a send. The default comes from the recipient kind
241
+ * (`defaultAudienceFor`); `--for-human` maps to `human`; an explicit
242
+ * `--audience` wins over `--for-human` (with a warning) and is validated.
243
+ */
244
+ export declare function resolveSendAudience(opts: {
245
+ audience?: string;
246
+ forHuman?: boolean;
247
+ }, recipientKind: DmRecipientKind, warn?: (msg: string) => void): string;
236
248
  /** One incoming DM as returned by GET /v1/notify/inbox. */
237
249
  export interface DmInboxEvent {
238
250
  eventId: string;
@@ -411,5 +423,4 @@ export declare function formatChannelTranscript(messages: ChannelMessageItem[],
411
423
  export declare function agentCallerUid(token: string, machine?: () => boolean): string | null;
412
424
  export declare const AGENT_THREAD_UNAVAILABLE: string;
413
425
  export declare function registerDmCommand(program: Command): void;
414
- export {};
415
426
  //# sourceMappingURL=dm.d.ts.map
@@ -5,7 +5,8 @@ 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 } from "../lib/notify-recipient.js";
8
+ import { EMAIL_PATTERN, RECIPIENT_UID_PATTERN, defaultAudienceFor, recipientKindOf, } from "../lib/notify-recipient.js";
9
+ export { defaultAudienceFor, recipientKindOf };
9
10
  import { dmForwardCliEnabled } from "./dm-forward-flag.js";
10
11
  import { attachmentScopeForChannel, formatUploadedAttachments, personalScopeForCaller, uploadDmAttachments, validateAttachmentPaths, } from "./dm-attach.js";
11
12
  import { ChannelNotFoundError, suggestChannels, withChannelSuggestions, } from "./channel-suggest.js";
@@ -555,7 +556,7 @@ async function runConnectionAction(action, identifier) {
555
556
  * the server's idempotent participant-key dedupe) a group channel, then posts
556
557
  * the message into it. Reuses the same vault API client + auth as the 1:1 path.
557
558
  */
558
- async function runGroupSend(recipients, message) {
559
+ async function runGroupSend(recipients, message, audience) {
559
560
  try {
560
561
  const participants = [];
561
562
  for (const r of recipients) {
@@ -601,7 +602,7 @@ async function runGroupSend(recipients, message) {
601
602
  token,
602
603
  path: `/v1/notify/channels/${encodeURIComponent(channelId)}/messages`,
603
604
  method: "POST",
604
- body: { body },
605
+ body: { body, ...(audience ? { audience } : {}) },
605
606
  });
606
607
  if (!sendRes.ok) {
607
608
  const err = (await sendRes.json().catch(() => ({})));
@@ -810,11 +811,41 @@ async function runChannelSend(channelName, message, opts) {
810
811
  process.exit(1);
811
812
  }
812
813
  }
813
- async function runDmSend(recipient, message, opts) {
814
+ function explicitAudience(opts) {
815
+ const explicit = opts.audience !== undefined && opts.audience !== "" ? opts.audience : undefined;
816
+ if (explicit !== undefined && !DM_AUDIENCE_VALUES.includes(explicit)) {
817
+ throw new Error("Invalid --audience '" + explicit + "': must be human, agent, or both.");
818
+ }
819
+ return explicit;
820
+ }
821
+ /**
822
+ * 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.
825
+ */
826
+ export function resolveSendAudience(opts, recipientKind, warn = (m) => console.error(chalk.yellow(m))) {
827
+ const explicit = explicitAudience(opts);
828
+ if (explicit !== undefined) {
829
+ if (opts.forHuman) {
830
+ warn("Both --audience " + explicit + " and --for-human were passed; using --audience " + explicit + ".");
831
+ }
832
+ return explicit;
833
+ }
834
+ return opts.forHuman ? "human" : defaultAudienceFor(recipientKind);
835
+ }
836
+ async function runDmSend(recipient, message, rawOpts) {
837
+ // Validate an explicit --audience before any network call; the default is
838
+ // applied once the recipient kind is known.
839
+ explicitAudience(rawOpts);
840
+ const opts = { ...rawOpts };
841
+ const withAudience = (kind) => ({
842
+ ...rawOpts,
843
+ audience: resolveSendAudience(rawOpts, kind),
844
+ });
814
845
  // --channel <name> is an explicit channel target; the positional carries the
815
846
  // message (recipient slot), e.g. `hq dm --channel vyg-dev "hello"`.
816
847
  if (opts.channel !== undefined) {
817
- await runChannelSend(opts.channel, message ?? recipient, opts);
848
+ await runChannelSend(opts.channel, message ?? recipient, withAudience("channel"));
818
849
  return;
819
850
  }
820
851
  if (recipient === undefined) {
@@ -837,7 +868,7 @@ async function runDmSend(recipient, message, opts) {
837
868
  process.exit(1);
838
869
  return;
839
870
  }
840
- await runGroupSend(group, message);
871
+ await runGroupSend(group, message, withAudience("group").audience);
841
872
  return;
842
873
  }
843
874
  // A bare name or #hash addresses a named DM channel.
@@ -849,12 +880,13 @@ async function runDmSend(recipient, message, opts) {
849
880
  ? null
850
881
  : matchContactByName(await fetchContacts(await ensureCognitoToken()), channelName);
851
882
  if (!person?.personUid) {
852
- await runChannelSend(channelName, message, opts);
883
+ await runChannelSend(channelName, message, withAudience("channel"));
853
884
  return;
854
885
  }
855
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.`));
856
887
  recipient = person.personUid;
857
888
  }
889
+ Object.assign(opts, withAudience(recipientKindOf(recipient)));
858
890
  if (opts.thread !== undefined) {
859
891
  console.error(chalk.red("--thread is only supported for channel messages (hq dm <channel> --thread <eventId|last> \"…\")."));
860
892
  process.exit(1);
@@ -1697,7 +1729,7 @@ async function runDmChannel(target, opts) {
1697
1729
  export function registerDmCommand(program) {
1698
1730
  const dm = program
1699
1731
  .command("dm")
1700
- .description("Send and read direct messages, and manage connection requests.");
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.");
1701
1733
  dm
1702
1734
  .command("send [recipient] [message]", { isDefault: true, hidden: true })
1703
1735
  .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.')
@@ -1710,7 +1742,8 @@ export function registerDmCommand(program) {
1710
1742
  .option("--details-file <path>", "Read the details from a file (1:1 DMs; channel sends also supported)")
1711
1743
  .option("--at <iso>", "Schedule delivery at an ISO8601 time (store-and-forward; delivered within ~60s of the time; 1:1 DMs only)")
1712
1744
  .option("--in <duration>", "Schedule delivery after a relative delay: 30s, 10m, 2h, 1d (1:1 DMs only)")
1713
- .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")
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.")
1746
+ .option("--for-human", "Tag the message for people (alias for --audience human).")
1714
1747
  .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])
1715
1748
  .action(async (recipient, message, opts) => {
1716
1749
  await runDmSend(recipient, message, opts);
@@ -292,6 +292,10 @@ export interface PendingApproval {
292
292
  status?: string;
293
293
  /** Owner-facing outcome projected by hq-pro for decided rows. */
294
294
  outcome?: string;
295
+ /** Closed rejection code for a rejected row (e.g. provider_tool_error); older servers omit it. */
296
+ rejectionReason?: string | null;
297
+ /** Plain reason a decided row did not run, such as the app's own refusal text; older servers omit it. */
298
+ failureMessage?: string | null;
295
299
  }
296
300
  /**
297
301
  * Read open approvals from hq-pro's confirm-queue state (not the audit feed).
@@ -307,4 +311,15 @@ export declare function mutateConnectionAccess(token: string, companyUid: string
307
311
  granteeId?: string;
308
312
  permission?: Permission;
309
313
  }): Promise<ConnectionAccess>;
314
+ /** Same cap the server applies to a failure reason; the CLI enforces it for older or malformed responses. */
315
+ export declare const TERMINAL_REASON_MAX_CHARS = 500;
316
+ /**
317
+ * Server-supplied text that reaches the terminal (a failure reason quoting an
318
+ * app's error). ANSI escapes, control and invisible formatting characters are
319
+ * removed, then the same credential-redaction chain the gateway diagnostics use
320
+ * (`redactErrorText`: credential URLs, bearer values, token assignments, known
321
+ * token shapes) is applied, and the result is capped. The server redacts too;
322
+ * this keeps an older or misbehaving server from printing a secret.
323
+ */
324
+ export declare function terminalSafeText(text: string): string;
310
325
  //# sourceMappingURL=integrations-api.d.ts.map
@@ -8,6 +8,7 @@
8
8
  * is optional and read defensively.
9
9
  */
10
10
  import { vaultApiFetch } from "../utils/vault-api.js";
11
+ import { redactErrorText } from "../utils/redact-error-text.js";
11
12
  import { IntegrationsCliError, raiseForResponse, } from "./integrations-core.js";
12
13
  export async function listCatalog(token, companyUid, opts = {}) {
13
14
  const query = {
@@ -188,4 +189,30 @@ export async function mutateConnectionAccess(token, companyUid, action, input) {
188
189
  }
189
190
  return (await res.json());
190
191
  }
192
+ // eslint-disable-next-line no-control-regex
193
+ const TERMINAL_ESCAPES = /\u001b\[[0-?]*[ -/]*[@-~]|\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)?|\u001b[@-_]/g;
194
+ // eslint-disable-next-line no-control-regex
195
+ const TERMINAL_CONTROL_CHARS = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g;
196
+ /** Bidirectional overrides, isolates, marks and zero-width characters, which can reorder or hide text. */
197
+ const TERMINAL_INVISIBLE_FORMAT_CHARS = /[\u200b-\u200f\u202a-\u202e\u2060-\u2069\ufeff]/g;
198
+ /** Same cap the server applies to a failure reason; the CLI enforces it for older or malformed responses. */
199
+ export const TERMINAL_REASON_MAX_CHARS = 500;
200
+ /**
201
+ * Server-supplied text that reaches the terminal (a failure reason quoting an
202
+ * app's error). ANSI escapes, control and invisible formatting characters are
203
+ * removed, then the same credential-redaction chain the gateway diagnostics use
204
+ * (`redactErrorText`: credential URLs, bearer values, token assignments, known
205
+ * token shapes) is applied, and the result is capped. The server redacts too;
206
+ * this keeps an older or misbehaving server from printing a secret.
207
+ */
208
+ export function terminalSafeText(text) {
209
+ const visible = text
210
+ .replace(TERMINAL_ESCAPES, "")
211
+ .replace(TERMINAL_CONTROL_CHARS, "")
212
+ .replace(TERMINAL_INVISIBLE_FORMAT_CHARS, "");
213
+ const redacted = redactErrorText(visible);
214
+ return redacted.length > TERMINAL_REASON_MAX_CHARS
215
+ ? `${redacted.slice(0, TERMINAL_REASON_MAX_CHARS - 1)}…`
216
+ : redacted;
217
+ }
191
218
  //# sourceMappingURL=integrations-api.js.map
@@ -18,7 +18,7 @@ import { ensureCognitoIdToken } from "../utils/cognito-session.js";
18
18
  import { getCompanyUid } from "../utils/vault-api.js";
19
19
  import { redactSensitiveUrl, redactUrlsInText } from "../utils/redact-url.js";
20
20
  import { IntegrationsCliError, bareProvider, printHealthNotice, fetchConnections, fetchAdminSurface, fetchAuditPage, AUDIT_PAGE_MAX, printJson, revokedConnectionDetails, resolveConnection, selectConnection, } from "./integrations-core.js";
21
- import { fetchPendingApprovals, getConnectionAccess, mutateConnectionAccess, purgeConnection, setReadSafe, uninstallIntegration, updateGovernance, } from "./integrations-api.js";
21
+ import { fetchPendingApprovals, getConnectionAccess, mutateConnectionAccess, purgeConnection, setReadSafe, terminalSafeText, uninstallIntegration, updateGovernance, } from "./integrations-api.js";
22
22
  const WRITE_POLICIES = ["auto-allow", "confirm", "deny"];
23
23
  const PERMISSIONS = ["read", "write", "admin"];
24
24
  /** Plain-English gloss for each write policy, used in every policy readout. */
@@ -617,9 +617,11 @@ export function registerManageCommands(integrations) {
617
617
  console.log(`${chalk.bold(label)} ${chalk.dim(`${who} · ${row.createdAt}`)}`);
618
618
  if (opts.history) {
619
619
  console.log(chalk.dim(` ${row.queueId} · ${row.status ?? row.outcome ?? "status unavailable"}`));
620
+ if (row.failureMessage)
621
+ console.log(chalk.dim(` Why: ${terminalSafeText(row.failureMessage)}`));
620
622
  }
621
623
  else {
622
- console.log(chalk.dim(` hq integrations approve ${row.queueId}${row.provider ? ` --provider ${bareProvider(row.provider)}` : ""}`));
624
+ console.log(chalk.dim(` hq integrations approve ${row.queueId}${row.provider ? ` --provider ${bareProvider(row.provider)}` : ""}${opts.company ? ` --company ${opts.company}` : ""}`));
623
625
  }
624
626
  }
625
627
  });
@@ -61,7 +61,7 @@ import { registerConnectCommands } from "./integrations-connect.js";
61
61
  import { registerImportCommands } from "./integrations-import.js";
62
62
  import { registerManageCommands } from "./integrations-manage.js";
63
63
  import { registerSlackCommands } from "./integrations-slack.js";
64
- import { fetchPendingApprovals } from "./integrations-api.js";
64
+ import { fetchPendingApprovals, terminalSafeText } from "./integrations-api.js";
65
65
  const CALL_ARGS_EXAMPLE = `--args '{"assignee":"me"}' or --args-file args.json`;
66
66
  function readStream(stream) {
67
67
  return new Promise((resolve, reject) => {
@@ -533,7 +533,7 @@ export function registerIntegrationsCommand(program) {
533
533
  return;
534
534
  }
535
535
  console.log(chalk.yellow("Queued for approval — this call can change the app, so a company owner decides first."));
536
- console.log(`Approve with:\n hq integrations approve ${queued.queueId} --provider ${bareProvider(connection.provider)}`);
536
+ console.log(`Approve with:\n hq integrations approve ${queued.queueId} --provider ${bareProvider(connection.provider)}${opts.company ? ` --company ${opts.company}` : ""}`);
537
537
  if (queued.expiresAt) {
538
538
  console.log(chalk.dim(`Expires ${queued.expiresAt}`));
539
539
  }
@@ -606,7 +606,9 @@ export function registerIntegrationsCommand(program) {
606
606
  const unknown = terminal.status === "write_unknown" || terminal.outcome === "unknown";
607
607
  console.log(chalk.red(unknown
608
608
  ? `The provider outcome is unknown for ${queueId}. Check the provider before taking any action; ${historyCommand} shows the latest queue state.`
609
- : `The approved call failed (${terminal.status ?? terminal.outcome}). See ${historyCommand} for its status.`));
609
+ : terminal.failureMessage
610
+ ? `The approved call did not run: ${terminalSafeText(terminal.failureMessage)}`
611
+ : `The approved call failed (${terminal.rejectionReason ?? terminal.status ?? terminal.outcome}). See ${historyCommand} for its status.`));
610
612
  }
611
613
  else if (terminal.status === "resumed" || terminal.status === "completed" || terminal.outcome === "ran") {
612
614
  console.log(chalk.green("Approved — the call completed."));
@@ -614,7 +616,7 @@ export function registerIntegrationsCommand(program) {
614
616
  printJson(unwrapGatewayResult(stored.result));
615
617
  }
616
618
  else if (ran) {
617
- console.log(chalk.yellow(`The call's result could not be read back. Run \`hq integrations approve ${queueId}\` again to fetch it; the call will not run twice.`));
619
+ console.log(chalk.yellow(`The call's result could not be read back. Run \`hq integrations approve ${queueId} --provider ${bareProvider(connection.provider)}${opts.company ? ` --company ${opts.company}` : ""}\` again to fetch it; the call will not run twice.`));
618
620
  }
619
621
  }
620
622
  else {
@@ -649,7 +651,9 @@ export function registerIntegrationsCommand(program) {
649
651
  if (failed) {
650
652
  const reason = body.rejectionReason ??
651
653
  (toolError ? "provider_tool_error" : body.outcome ?? body.status ?? "failed");
652
- console.log(chalk.red(`The approved call failed at the provider (${reason}).`));
654
+ console.log(chalk.red(body.failureMessage
655
+ ? `The approved call did not run: ${terminalSafeText(body.failureMessage)}`
656
+ : `The approved call failed at the provider (${reason}).`));
653
657
  }
654
658
  else if (body.status === "resumed" || body.status === "completed" || body.outcome === "ran") {
655
659
  console.log(chalk.green(`Approved — the call ran (status: ${body.status ?? body.outcome}).`));