@indigoai-us/hq-cli 5.345.166 → 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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
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
+
5
9
  ## [5.345.166] — 2026-10-24
6
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.
7
11
  - The bundled sync engine now uses hq-cloud 6.18.101, the current npm `latest`.
@@ -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)"
@@ -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);
@@ -17,7 +17,7 @@
17
17
  */
18
18
  import { CLI_VERSION } from "../../../cli-version.js";
19
19
  import { peekIdToken } from "../../../utils/id-token.js";
20
- import { isRecipientUid } from "../../../lib/notify-recipient.js";
20
+ import { defaultAudienceFor, isAgentUid, isRecipientUid } from "../../../lib/notify-recipient.js";
21
21
  import { redactAgentInboxItem } from "../inbox-redact.js";
22
22
  import { markInboxDone, mergeAgentInbox, readDoneIds, summarizeInboxEntry } from "../inbox-state.js";
23
23
  import { AttachmentCompanyUnresolvedError, fetchChatAttachment } from "./attachments.js";
@@ -214,13 +214,13 @@ export function buildAgentMcpTools(clients) {
214
214
  },
215
215
  {
216
216
  name: "hq_dm_send",
217
- description: "Send an HQ message as this agent to a person (email or prs_…), an agent (agt_…), or a channel or group DM (chn_…). When replying to an inbox item that has a channelId, pass that channelId as `to` so the answer lands in the conversation it came from. Use audience='agent' for progress/status messages that only agents need to see; audience='human' for messages a person must read; audience='both' when unsure.",
217
+ description: "Send an HQ message as this agent to a person (email or prs_…), an agent (agt_…), or a channel or group DM (chn_…). When replying to an inbox item that has a channelId, pass that channelId as `to` so the answer lands in the conversation it came from. Use audience='agent' for progress/status messages that only agents need to see; audience='human' for messages a person must read; audience='both' when unsure. Default: human for a person, agent for an agent, a channel or a group.",
218
218
  inputSchema: {
219
219
  type: "object",
220
220
  properties: {
221
221
  to: { type: "string", description: "Email, prs_… or agt_… uid, or a chn_… channel id" },
222
222
  message: { type: "string" },
223
- audience: { type: "string", enum: ["human", "agent", "both"], description: "Who this message is for (default: human)" },
223
+ audience: { type: "string", enum: ["human", "agent", "both"], description: "Who this message is for (default: human for a person, agent for an agent, channel or group)" },
224
224
  },
225
225
  required: ["to", "message"],
226
226
  additionalProperties: false,
@@ -238,7 +238,7 @@ export function buildAgentMcpTools(clients) {
238
238
  const token = await clients.getToken();
239
239
  const res = await clients.apiJson(token, `/v1/notify/channels/${encodeURIComponent(to)}/messages`, {
240
240
  method: "POST",
241
- body: { body: message, ...(audience ? { audience } : {}) },
241
+ body: { body: message, audience: audience ?? defaultAudienceFor("channel") },
242
242
  });
243
243
  if (res.status < 200 || res.status >= 300) {
244
244
  return errorResult(`Channel send failed (${res.status}): ${describeError(res.body)}`);
@@ -252,8 +252,7 @@ export function buildAgentMcpTools(clients) {
252
252
  : (() => {
253
253
  throw new McpToolInputError('"to" must be an email, prs_… or agt_… uid, or a chn_… channel id');
254
254
  })();
255
- if (audience)
256
- body.audience = audience;
255
+ body.audience = audience ?? defaultAudienceFor(isAgentUid(to) ? "agent" : "person");
257
256
  const token = await clients.getToken();
258
257
  const res = await clients.apiJson(token, "/v1/notify/dm", { method: "POST", body });
259
258
  if (res.status < 200 || res.status >= 300) {
@@ -4,4 +4,19 @@ export declare const AGENT_UID_PATTERN: RegExp;
4
4
  export declare const EMAIL_PATTERN: RegExp;
5
5
  export declare function isRecipientUid(value: string): boolean;
6
6
  export declare function isAgentUid(value: string): boolean;
7
+ /** Who a notify message is for. */
8
+ export type DmAudience = "human" | "agent" | "both";
9
+ /** What a send is addressed to, for the default audience. */
10
+ export type DmRecipientKind = "person" | "agent" | "channel" | "group";
11
+ /**
12
+ * Classify a single 1:1 recipient (email, prs_ or agt_ uid) for the default
13
+ * audience. Anything that is not an agent uid is a person.
14
+ */
15
+ export declare function recipientKindOf(recipient: string): "person" | "agent";
16
+ /**
17
+ * Default audience by recipient: a 1:1 message to a person is for that
18
+ * person (`human`); a message to an agent, a channel or a group is tagged
19
+ * `agent` and folded away from people by default.
20
+ */
21
+ export declare function defaultAudienceFor(kind: DmRecipientKind): DmAudience;
7
22
  //# sourceMappingURL=notify-recipient.d.ts.map
@@ -8,4 +8,19 @@ export function isRecipientUid(value) {
8
8
  export function isAgentUid(value) {
9
9
  return AGENT_UID_PATTERN.test(value.trim());
10
10
  }
11
+ /**
12
+ * Classify a single 1:1 recipient (email, prs_ or agt_ uid) for the default
13
+ * audience. Anything that is not an agent uid is a person.
14
+ */
15
+ export function recipientKindOf(recipient) {
16
+ return /^agt_/.test(recipient.trim()) ? "agent" : "person";
17
+ }
18
+ /**
19
+ * Default audience by recipient: a 1:1 message to a person is for that
20
+ * person (`human`); a message to an agent, a channel or a group is tagged
21
+ * `agent` and folded away from people by default.
22
+ */
23
+ export function defaultAudienceFor(kind) {
24
+ return kind === "person" ? "human" : "agent";
25
+ }
11
26
  //# sourceMappingURL=notify-recipient.js.map
@@ -26,13 +26,19 @@
26
26
  * there, because a pattern can always be widened, alone or combined with other
27
27
  * entries, to hide a real credential.
28
28
  *
29
- * Fails closed: anything not acceptable is a ScanConfigError, and the scan
30
- * exits 2 rather than running with a guess.
29
+ * An allowlist that is not acceptable is an InvalidAllowlistError. The scan
30
+ * then warns and runs with no repo allowlist at all (stricter, never looser),
31
+ * so a rejected file cannot block the commit that replaces it. Other config
32
+ * problems, such as an unknown --allowlist-rev, are a ScanConfigError and the
33
+ * scan exits 2.
31
34
  */
32
35
  import { type Probe } from "./probes.js";
33
36
  import { type CompiledAllowlist } from "./rules.js";
34
37
  export declare class ScanConfigError extends Error {
35
38
  }
39
+ /** The repo's .gitleaks.toml cannot be used; the scan ignores it (see the top of the file). */
40
+ export declare class InvalidAllowlistError extends ScanConfigError {
41
+ }
36
42
  export declare const REPO_CONFIG_FILE = ".gitleaks.toml";
37
43
  /**
38
44
  * The first probe credential the allowlist would hide, judged as the detector
@@ -26,8 +26,11 @@
26
26
  * there, because a pattern can always be widened, alone or combined with other
27
27
  * entries, to hide a real credential.
28
28
  *
29
- * Fails closed: anything not acceptable is a ScanConfigError, and the scan
30
- * exits 2 rather than running with a guess.
29
+ * An allowlist that is not acceptable is an InvalidAllowlistError. The scan
30
+ * then warns and runs with no repo allowlist at all (stricter, never looser),
31
+ * so a rejected file cannot block the commit that replaces it. Other config
32
+ * problems, such as an unknown --allowlist-rev, are a ScanConfigError and the
33
+ * scan exits 2.
31
34
  */
32
35
  import { execFileSync } from "node:child_process";
33
36
  import { createHash } from "node:crypto";
@@ -38,6 +41,9 @@ import { credentialProbes } from "./probes.js";
38
41
  import { compileAllowlist, compiledRules } from "./rules.js";
39
42
  export class ScanConfigError extends Error {
40
43
  }
44
+ /** The repo's .gitleaks.toml cannot be used; the scan ignores it (see the top of the file). */
45
+ export class InvalidAllowlistError extends ScanConfigError {
46
+ }
41
47
  export const REPO_CONFIG_FILE = ".gitleaks.toml";
42
48
  const KEYS = new Set(["description", "condition", "regexTarget", "paths", "regexes", "stopwords", "commits", "targetRules"]);
43
49
  const MIN_PATH_PREFIX = 3;
@@ -68,7 +74,7 @@ export function suppressedProbe(al) {
68
74
  return probes.find((p, i) => probeBaseline[i] && scanFragment(probeFragment(p.line), rules).length === 0);
69
75
  }
70
76
  function fail(file, where, msg) {
71
- throw new ScanConfigError(`${file}: ${where}: ${msg}`);
77
+ throw new InvalidAllowlistError(`${file}: ${where}: ${msg}`);
72
78
  }
73
79
  function stringList(file, where, key, v) {
74
80
  if (v === undefined)
@@ -292,7 +298,7 @@ function parseToml(text, file) {
292
298
  return parse(text);
293
299
  }
294
300
  catch (err) {
295
- throw new ScanConfigError(`${file}: could not be parsed: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`);
301
+ throw new InvalidAllowlistError(`${file}: could not be parsed: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`);
296
302
  }
297
303
  }
298
304
  function git(repoRoot, args) {
@@ -313,10 +319,10 @@ function entry(out, blobField) {
313
319
  }
314
320
  function readEntry(repoRoot, e, where) {
315
321
  if (e.mode === "120000")
316
- throw new ScanConfigError(`${REPO_CONFIG_FILE} (${where}) is a symlink; commit a regular file`);
322
+ throw new InvalidAllowlistError(`${REPO_CONFIG_FILE} (${where}) is a symlink; commit a regular file`);
317
323
  const text = git(repoRoot, ["cat-file", "blob", e.blob]);
318
324
  if (text === null)
319
- throw new ScanConfigError(`${REPO_CONFIG_FILE} (${where}) could not be read from git`);
325
+ throw new InvalidAllowlistError(`${REPO_CONFIG_FILE} (${where}) could not be read from git`);
320
326
  return text;
321
327
  }
322
328
  /**
@@ -13,6 +13,12 @@ export interface ParsedDiff {
13
13
  export declare function parseUnifiedDiff(diff: string): ParsedDiff;
14
14
  export declare function stagedDiff(cwd: string): string;
15
15
  export declare function repoRoot(cwd: string): string;
16
+ /**
17
+ * The repo's allowlists, or none when its .gitleaks.toml is rejected. The
18
+ * warning is fixed text: a validation message can quote a path or other
19
+ * entry, and an entry can hold a credential.
20
+ */
21
+ export declare const ALLOWLIST_IGNORED_WARNING = "HQ secret scan: warning: the repo .gitleaks.toml allowlist was rejected; ignoring it and scanning with no repo allowlist";
16
22
  /**
17
23
  * Built-in rules plus the repo's own .gitleaks.toml allowlists. Diff paths are
18
24
  * relative to the repo root, as gitleaks' allowlist paths are.
@@ -5,7 +5,7 @@ import { execFileSync } from "node:child_process";
5
5
  import { readFileSync, realpathSync, statSync } from "node:fs";
6
6
  import { isAbsolute, relative, resolve, sep } from "node:path";
7
7
  import { scanFragment, scanPath } from "./detect.js";
8
- import { loadCommittedAllowlists, loadRepoAllowlists } from "./repo-config.js";
8
+ import { InvalidAllowlistError, loadCommittedAllowlists, loadRepoAllowlists } from "./repo-config.js";
9
9
  import { compiledRules } from "./rules.js";
10
10
  export class ScanInputError extends Error {
11
11
  }
@@ -101,13 +101,31 @@ export function repoRoot(cwd) {
101
101
  throw new ScanInputError(`could not find the repo root: ${detail || "git rev-parse failed"}`);
102
102
  }
103
103
  }
104
+ /**
105
+ * The repo's allowlists, or none when its .gitleaks.toml is rejected. The
106
+ * warning is fixed text: a validation message can quote a path or other
107
+ * entry, and an entry can hold a credential.
108
+ */
109
+ export const ALLOWLIST_IGNORED_WARNING = "HQ secret scan: warning: the repo .gitleaks.toml allowlist was rejected; ignoring it and scanning with no repo allowlist";
110
+ function repoAllowlistsOrNone(load) {
111
+ try {
112
+ return load();
113
+ }
114
+ catch (err) {
115
+ if (!(err instanceof InvalidAllowlistError))
116
+ throw err;
117
+ process.stderr.write(`${ALLOWLIST_IGNORED_WARNING}\n`);
118
+ return [];
119
+ }
120
+ }
104
121
  /**
105
122
  * Built-in rules plus the repo's own .gitleaks.toml allowlists. Diff paths are
106
123
  * relative to the repo root, as gitleaks' allowlist paths are.
107
124
  */
108
125
  export function scanStaged(cwd) {
109
126
  const diff = stagedDiff(cwd);
110
- const rules = { ...compiledRules(), repo: loadRepoAllowlists(repoRoot(cwd)) };
127
+ const root = repoRoot(cwd);
128
+ const rules = { ...compiledRules(), repo: repoAllowlistsOrNone(() => loadRepoAllowlists(root)) };
111
129
  const { fragments, binaryFiles } = parseUnifiedDiff(diff);
112
130
  return [...fragments.flatMap((f) => scanFragment(f, rules)), ...binaryFiles.flatMap((f) => scanPath(f, rules))];
113
131
  }
@@ -145,7 +163,7 @@ function matchPath(p, repo, snapshotRoot) {
145
163
  }
146
164
  export function scanFiles(paths, opts = {}) {
147
165
  const root = opts.repo !== undefined ? repoRoot(opts.repo) : undefined;
148
- const rules = root !== undefined ? { ...compiledRules(), repo: loadCommittedAllowlists(root, opts.allowlistRev) } : compiledRules();
166
+ const rules = root !== undefined ? { ...compiledRules(), repo: repoAllowlistsOrNone(() => loadCommittedAllowlists(root, opts.allowlistRev)) } : compiledRules();
149
167
  const repo = root !== undefined ? real(root) : undefined;
150
168
  const snapshotRoot = opts.snapshotRoot !== undefined ? real(opts.snapshotRoot) : undefined;
151
169
  const out = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.166",
3
+ "version": "5.345.168",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -114,11 +114,11 @@
114
114
  "*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}": "eslint --cache --no-error-on-unmatched-pattern"
115
115
  },
116
116
  "optionalDependencies": {
117
- "@indigoai-us/hq-hook-win32-x64": "5.345.166",
118
- "@indigoai-us/hq-hook-win32-arm64": "5.345.166",
119
- "@indigoai-us/hq-hook-darwin-x64": "5.345.166",
120
- "@indigoai-us/hq-hook-darwin-arm64": "5.345.166",
121
- "@indigoai-us/hq-hook-linux-x64": "5.345.166",
122
- "@indigoai-us/hq-hook-linux-arm64": "5.345.166"
117
+ "@indigoai-us/hq-hook-win32-x64": "5.345.168",
118
+ "@indigoai-us/hq-hook-win32-arm64": "5.345.168",
119
+ "@indigoai-us/hq-hook-darwin-x64": "5.345.168",
120
+ "@indigoai-us/hq-hook-darwin-arm64": "5.345.168",
121
+ "@indigoai-us/hq-hook-linux-x64": "5.345.168",
122
+ "@indigoai-us/hq-hook-linux-arm64": "5.345.168"
123
123
  }
124
124
  }