@indigoai-us/hq-cli 5.103.26 → 5.103.28

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.103.28] — 2026-08-28
6
+
7
+ ## [5.103.27] — 2026-08-28
8
+
5
9
  ## [5.103.26] — 2026-08-27
6
10
 
7
11
  ### Changed
@@ -272,15 +272,11 @@ async function runGroupSend(recipients, message) {
272
272
  for (const r of recipients) {
273
273
  const rc = detectRecipient(r);
274
274
  if (!rc) {
275
- console.error(chalk.red(`Invalid recipient '${r}': each must be an email address or a personUid (prs_…).`));
276
- process.exit(1);
277
- }
278
- // Group DMs are channels — agents don't participate in channels (their
279
- // DM surface is 1:1 via the durable box inbox). DM an agent directly.
280
- if (rc.toPersonUid?.startsWith("agt_")) {
281
- console.error(chalk.red(`Agents can't join group DMs yet — DM '${r}' directly: hq dm ${r} "<message>".`));
275
+ console.error(chalk.red(`Invalid recipient '${r}': each must be an email address, a personUid (prs_…), or an agentUid (agt_…).`));
282
276
  process.exit(1);
283
277
  }
278
+ // People (prs_) and agents (agt_) are both valid group participants —
279
+ // the server (handleCreateGroupDm) accepts agt_* uids like any other.
284
280
  participants.push(rc.toEmail ?? rc.toPersonUid);
285
281
  }
286
282
  if (participants.length < 2) {
@@ -8,6 +8,7 @@
8
8
  * Sentry live in exactly one place. `integrations.ts` re-exports this module's
9
9
  * public surface, so existing importers keep working unchanged.
10
10
  */
11
+ export { bareProvider } from "../lib/integrations/provider-slug.js";
11
12
  /** Write policy on a connection — mirrors hq-pro's `WritePolicy`. */
12
13
  export type WritePolicy = "auto-allow" | "confirm" | "deny";
13
14
  /** Who a grant points at — mirrors hq-pro's `AclGranteeType`. */
@@ -199,8 +200,6 @@ export declare function raiseIfUnauthorized(res: Response): void;
199
200
  export declare function raiseForResponse(res: Response, fallback: string): Promise<never>;
200
201
  /** "factory:linear" → "linear"; mirrors hq-pro's factoryToolPrefix. */
201
202
  export declare function toolPrefixForProvider(provider: string): string;
202
- /** "factory:linear" → "linear", for display and for `--provider` echoes. */
203
- export declare function bareProvider(provider: string): string;
204
203
  /**
205
204
  * Read the whole admin surface: connections with their governance state, the
206
205
  * viewer's role, and the recent audit feed. Several verbs need more than the
@@ -264,5 +263,18 @@ interface QueuedOutcome {
264
263
  }
265
264
  export declare function queuedOutcome(payload: unknown): QueuedOutcome | null;
266
265
  export declare function printJson(value: unknown): void;
267
- export {};
266
+ /**
267
+ * Print why a connection is unusable and how to repair it, directly beneath its
268
+ * status line.
269
+ *
270
+ * `list` and `show` used to print a bare `error`, leaving the diagnosis the
271
+ * control plane had already sent reachable only through a separate `hq doctor`
272
+ * run. Classification is shared with that check rather than duplicated, so the
273
+ * two surfaces cannot drift into disagreeing about the same row.
274
+ *
275
+ * Healthy and revoked rows print nothing: revoked tombstones carry their own
276
+ * re-add wording, and a repair path printed against a working connection sends
277
+ * a user to fix something that is not broken.
278
+ */
279
+ export declare function printHealthNotice(connection: AdminConnection): void;
268
280
  //# sourceMappingURL=integrations-core.d.ts.map
@@ -9,9 +9,13 @@
9
9
  * public surface, so existing importers keep working unchanged.
10
10
  */
11
11
  import { randomUUID } from "node:crypto";
12
+ import chalk from "chalk";
12
13
  import { vaultApiFetch } from "../utils/vault-api.js";
13
14
  import { AuthError } from "../utils/auth-error.js";
14
15
  import { redactErrorText } from "../utils/redact-error-text.js";
16
+ import { connectionHealthNotice } from "../lib/integrations/health.js";
17
+ import { bareProvider } from "../lib/integrations/provider-slug.js";
18
+ export { bareProvider } from "../lib/integrations/provider-slug.js";
15
19
  export class IntegrationsCliError extends Error {
16
20
  /**
17
21
  * True when the error is the caller's request/state/permission (a client 4xx
@@ -196,9 +200,6 @@ export function toolPrefixForProvider(provider) {
196
200
  .replace(/[^a-z0-9]+/g, ".");
197
201
  }
198
202
  /** "factory:linear" → "linear", for display and for `--provider` echoes. */
199
- export function bareProvider(provider) {
200
- return provider.replace(/^factory:/, "");
201
- }
202
203
  /** Turn a factory display name into the human-friendly slug people type. */
203
204
  function humanSlug(value) {
204
205
  return value
@@ -441,4 +442,25 @@ export function queuedOutcome(payload) {
441
442
  export function printJson(value) {
442
443
  console.log(JSON.stringify(value, null, 2));
443
444
  }
445
+ /**
446
+ * Print why a connection is unusable and how to repair it, directly beneath its
447
+ * status line.
448
+ *
449
+ * `list` and `show` used to print a bare `error`, leaving the diagnosis the
450
+ * control plane had already sent reachable only through a separate `hq doctor`
451
+ * run. Classification is shared with that check rather than duplicated, so the
452
+ * two surfaces cannot drift into disagreeing about the same row.
453
+ *
454
+ * Healthy and revoked rows print nothing: revoked tombstones carry their own
455
+ * re-add wording, and a repair path printed against a working connection sends
456
+ * a user to fix something that is not broken.
457
+ */
458
+ export function printHealthNotice(connection) {
459
+ const notice = connectionHealthNotice(connection);
460
+ if (!notice)
461
+ return;
462
+ console.log(chalk.yellow(` ${notice.headline}`));
463
+ if (notice.remediation)
464
+ console.log(chalk.yellow(` ${notice.remediation}`));
465
+ }
444
466
  //# sourceMappingURL=integrations-core.js.map
@@ -16,7 +16,7 @@ import readline from "node:readline";
16
16
  import chalk from "chalk";
17
17
  import { ensureCognitoIdToken } from "../utils/cognito-session.js";
18
18
  import { getCompanyUid } from "../utils/vault-api.js";
19
- import { IntegrationsCliError, bareProvider, fetchConnections, fetchAdminSurface, printJson, revokedConnectionDetails, resolveConnection, selectConnection, } from "./integrations-core.js";
19
+ import { IntegrationsCliError, bareProvider, printHealthNotice, fetchConnections, fetchAdminSurface, printJson, revokedConnectionDetails, resolveConnection, selectConnection, } from "./integrations-core.js";
20
20
  import { fetchPendingApprovals, getConnectionAccess, mutateConnectionAccess, purgeConnection, setReadSafe, uninstallIntegration, updateGovernance, } from "./integrations-api.js";
21
21
  const WRITE_POLICIES = ["auto-allow", "confirm", "deny"];
22
22
  const PERMISSIONS = ["read", "write", "admin"];
@@ -238,6 +238,9 @@ export function registerManageCommands(integrations) {
238
238
  if (install?.status === "needs_credentials") {
239
239
  console.log(chalk.yellow(" Needs sign-in — run `hq integrations reconnect` to fix it."));
240
240
  }
241
+ // Last, so the diagnosis and its fix path are the final thing on screen
242
+ // rather than buried above the sharing and ownership lines.
243
+ printHealthNotice(connection);
241
244
  });
242
245
  integrations
243
246
  .command("policy [app]")
@@ -52,7 +52,7 @@ import { randomUUID } from "node:crypto";
52
52
  import chalk from "chalk";
53
53
  import { ensureCognitoIdToken } from "../utils/cognito-session.js";
54
54
  import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
55
- import { IntegrationsCliError, bareProvider, callGateway, fetchConnections, isClientError, printJson, raiseIfUnauthorized, raiseIfUpstreamUnavailable, gatewayResultIsError, selectConnection, revokedConnectionDetails, toolPrefixForProvider, unwrapGatewayResult, queuedOutcome, } from "./integrations-core.js";
55
+ import { IntegrationsCliError, bareProvider, callGateway, fetchConnections, isClientError, printHealthNotice, printJson, raiseIfUnauthorized, raiseIfUpstreamUnavailable, gatewayResultIsError, selectConnection, revokedConnectionDetails, toolPrefixForProvider, unwrapGatewayResult, queuedOutcome, } from "./integrations-core.js";
56
56
  import { registerConnectCommands } from "./integrations-connect.js";
57
57
  import { registerImportCommands } from "./integrations-import.js";
58
58
  import { registerManageCommands } from "./integrations-manage.js";
@@ -190,6 +190,7 @@ export function registerIntegrationsCommand(program) {
190
190
  .join(" · ");
191
191
  console.log(`${chalk.bold(connectionLabel(c))} ${chalk.dim(flags)}`);
192
192
  console.log(chalk.dim(` connection: ${c.id}`));
193
+ printHealthNotice(c);
193
194
  }
194
195
  });
195
196
  registerImportCommands(integrations);
@@ -7,41 +7,26 @@
7
7
  * stored health signals only: a recorded rejection overrides a misleading
8
8
  * `connected` status, but a clean connected row is not live-provider proof.
9
9
  */
10
- import { type AdminConnection } from "../../../commands/integrations-core.js";
10
+ import { type IntegrationConnection } from "../../integrations/health.js";
11
11
  import type { CheckContext, CheckFamily, CheckResult } from "../types.js";
12
+ /**
13
+ * Health classification is shared with `hq integrations list`/`show` and lives
14
+ * in `lib/integrations/health.ts`. Re-exported here so the doctor family stays
15
+ * the single import surface for callers that already depend on it.
16
+ */
17
+ export { classifyConnection } from "../../integrations/health.js";
18
+ export type { IntegrationConnection } from "../../integrations/health.js";
12
19
  export declare const INTEGRATIONS_FAMILY_ID = "integrations";
13
20
  export declare const INTEGRATIONS_PREFIX = "integrations";
14
- /** Additive health fields returned by newer control planes. */
15
- export interface IntegrationConnection extends AdminConnection {
16
- errorReason?: string;
17
- degradedReason?: string;
18
- needsReauthReason?: string;
19
- /** Server-derived remediation flow; optional for older control planes. */
20
- fix_path?: string;
21
- /** Server-derived remediation class; optional for older control planes. */
22
- fix_kind?: string;
23
- }
24
21
  export interface IntegrationsDoctorDeps {
25
22
  ensureToken?: () => Promise<string>;
26
23
  resolveCompany?: (token: string, company: string | undefined) => Promise<string>;
27
24
  listConnections?: (token: string, companyUid: string) => Promise<IntegrationConnection[]>;
28
25
  }
29
- type FindingKind = "reconnect" | "re-add" | "contact-admin" | "provider-blocked" | "retryable" | "hq-configuration";
30
- interface Finding {
31
- provider: string;
32
- connectionId: string;
33
- kind: FindingKind;
34
- message: string;
35
- /** Optional HQ-authored remediation returned by the control plane. */
36
- remediation?: string;
37
- }
38
26
  /**
39
27
  * Expected session/company prerequisites degrade to NA instead of crashing or
40
28
  * becoming a false connection failure. An unreadable inventory remains UNKNOWN.
41
29
  */
42
30
  export declare function checkIntegrations(context: CheckContext, deps?: IntegrationsDoctorDeps): Promise<CheckResult[]>;
43
- /** Classify without echoing untrusted provider text, which may contain secrets. */
44
- export declare function classifyConnection(connection: IntegrationConnection): Finding[];
45
31
  export declare const integrationsFamily: CheckFamily;
46
- export {};
47
32
  //# sourceMappingURL=integrations.d.ts.map
@@ -11,20 +11,16 @@ import { ensureCognitoIdToken } from "../../../utils/cognito-session.js";
11
11
  import { isAuthError } from "../../../utils/auth-error.js";
12
12
  import { isCompanySelectionError } from "../../../utils/company-selection-error.js";
13
13
  import { getCompanyUid } from "../../../utils/vault-api.js";
14
- import { bareProvider, fetchConnections, IntegrationsCliError, } from "../../../commands/integrations-core.js";
14
+ import { fetchConnections, IntegrationsCliError } from "../../../commands/integrations-core.js";
15
+ import { classifyConnection } from "../../integrations/health.js";
16
+ /**
17
+ * Health classification is shared with `hq integrations list`/`show` and lives
18
+ * in `lib/integrations/health.ts`. Re-exported here so the doctor family stays
19
+ * the single import surface for callers that already depend on it.
20
+ */
21
+ export { classifyConnection } from "../../integrations/health.js";
15
22
  export const INTEGRATIONS_FAMILY_ID = "integrations";
16
23
  export const INTEGRATIONS_PREFIX = "integrations";
17
- const RECONNECT_REASON_CODES = new Set([
18
- "oauth_refresh_invalid_grant",
19
- "oauth_refresh_unavailable",
20
- "token_refresh_failed",
21
- "credentials_rejected",
22
- ]);
23
- const RETRYABLE_REASON_CODES = new Set([
24
- "oauth_refresh_transient",
25
- "oauth_refresh_write_conflict",
26
- ]);
27
- const HQ_CONFIGURATION_REASON_CODE = "oauth_client_secret_unavailable";
28
24
  /**
29
25
  * Expected session/company prerequisites degrade to NA instead of crashing or
30
26
  * becoming a false connection failure. An unreadable inventory remains UNKNOWN.
@@ -93,144 +89,6 @@ export async function checkIntegrations(context, deps = {}) {
93
89
  }
94
90
  return groupFindings(findings, context.company);
95
91
  }
96
- /** Classify without echoing untrusted provider text, which may contain secrets. */
97
- export function classifyConnection(connection) {
98
- if (connection.status === "revoked")
99
- return [];
100
- const provider = bareProvider(connection.provider);
101
- const reason = recordedReason(connection);
102
- const connectedWithRecordedFailure = connection.status === "connected" && reason !== "";
103
- const knownReasonCode = knownReasonCodeFor(connection);
104
- if (knownReasonCode) {
105
- const finding = findingForKnownReasonCode(connection, provider, knownReasonCode);
106
- // Retryable and HQ-configuration reason codes diagnose conditions that a
107
- // caller-specific remediation cannot change. Reconnect-class codes defer
108
- // to the server's credential/role-aware remediation classification.
109
- return [withServerRemediation(connection, RECONNECT_REASON_CODES.has(knownReasonCode)
110
- ? findingForServerFixKind(finding, connection.fix_kind)
111
- : finding)];
112
- }
113
- // A stale remediation value does not make a clean connected row unhealthy.
114
- if (!connectedWithRecordedFailure && connection.status === "connected")
115
- return [];
116
- if (isProviderBlocked(reason) || connection.status === "degraded") {
117
- return [withServerRemediation(connection, {
118
- provider,
119
- connectionId: connection.id,
120
- kind: "provider-blocked",
121
- message: connectedWithRecordedFailure
122
- ? "reports connected, but recorded provider health says access is blocked upstream"
123
- : "provider-side access is blocked or unavailable",
124
- })];
125
- }
126
- const fallback = genericReconnectFinding(connection, provider);
127
- const serverClassified = findingForServerFixKind(fallback, connection.fix_kind);
128
- if (serverClassified.kind !== "reconnect") {
129
- return [withServerRemediation(connection, serverClassified)];
130
- }
131
- if (isTokenRefreshFailure(reason) || isCredentialFailure(reason)) {
132
- const problem = isTokenRefreshFailure(reason)
133
- ? "token refresh failed"
134
- : connectedWithRecordedFailure
135
- ? "reports connected, but the provider rejected the stored credentials"
136
- : "the provider rejected the stored credentials";
137
- return [withServerRemediation(connection, {
138
- provider,
139
- connectionId: connection.id,
140
- kind: "reconnect",
141
- message: problem,
142
- })];
143
- }
144
- if (connection.status === "needs-reauth" || connection.status === "error") {
145
- return [withServerRemediation(connection, fallback)];
146
- }
147
- if (connection.status !== "connected") {
148
- return [withServerRemediation(connection, {
149
- provider,
150
- connectionId: connection.id,
151
- kind: "reconnect",
152
- message: `reports an unrecognized non-healthy status (${connection.status})`,
153
- })];
154
- }
155
- return [];
156
- }
157
- function genericReconnectFinding(connection, provider) {
158
- return {
159
- provider,
160
- connectionId: connection.id,
161
- kind: "reconnect",
162
- message: connection.status === "needs-reauth"
163
- ? "needs re-authentication"
164
- : "is in an error state",
165
- };
166
- }
167
- /** Only values emitted by hq-pro affect the classification; future values fall back. */
168
- function findingForServerFixKind(fallback, fixKind) {
169
- switch (fixKind) {
170
- case "re-add":
171
- return {
172
- ...fallback,
173
- kind: "re-add",
174
- message: "requires its API key to be re-entered",
175
- };
176
- case "contact-admin":
177
- return {
178
- ...fallback,
179
- kind: "contact-admin",
180
- message: "must be repaired by a company owner or admin",
181
- };
182
- case "reconnect":
183
- default:
184
- return fallback;
185
- }
186
- }
187
- /**
188
- * A fix path is server-authored, role- and credential-aware wording. It cannot
189
- * make a connection unhealthy: the caller must first have classified a health
190
- * signal, so a stale fix path on an otherwise clean connected row is ignored.
191
- */
192
- function withServerRemediation(connection, finding) {
193
- return connection.fix_path
194
- ? { ...finding, remediation: connection.fix_path }
195
- : finding;
196
- }
197
- /**
198
- * These are a closed hq-pro contract. Exact matching deliberately comes before
199
- * the legacy text heuristics, which remain below for old and unknown rows.
200
- */
201
- function knownReasonCodeFor(connection) {
202
- return [connection.errorReason, connection.needsReauthReason, connection.degradedReason]
203
- .find((value) => (typeof value === "string" &&
204
- (RECONNECT_REASON_CODES.has(value) ||
205
- RETRYABLE_REASON_CODES.has(value) ||
206
- value === HQ_CONFIGURATION_REASON_CODE)));
207
- }
208
- function findingForKnownReasonCode(connection, provider, code) {
209
- if (RECONNECT_REASON_CODES.has(code)) {
210
- return {
211
- provider,
212
- connectionId: connection.id,
213
- kind: "reconnect",
214
- message: "stored credentials need re-authentication",
215
- };
216
- }
217
- if (RETRYABLE_REASON_CODES.has(code)) {
218
- return {
219
- provider,
220
- connectionId: connection.id,
221
- kind: "retryable",
222
- message: code === "oauth_refresh_write_conflict"
223
- ? "token refresh lost a concurrent write race; the stored credential remains intact"
224
- : "token refresh is temporarily unavailable; the stored credential remains intact",
225
- };
226
- }
227
- return {
228
- provider,
229
- connectionId: connection.id,
230
- kind: "hq-configuration",
231
- message: "the HQ OAuth client secret is unavailable",
232
- };
233
- }
234
92
  function groupFindings(findings, company) {
235
93
  const groups = new Map();
236
94
  for (const finding of findings) {
@@ -288,6 +146,15 @@ function resultForGroup(entries, company) {
288
146
  ...(serverRemediation ? { remediation: serverRemediation } : {}),
289
147
  };
290
148
  }
149
+ if (first.kind === "wait") {
150
+ return {
151
+ status: "WARN",
152
+ checkId: `${INTEGRATIONS_PREFIX}.wait.${first.provider}`,
153
+ target: namedConnections,
154
+ message: `${first.provider}: ${count} ${plural} ${first.message}. Wait for it to complete, then retry the operation.`,
155
+ remediation: serverRemediation ?? "Wait for the Factory installation to complete, then retry the operation.",
156
+ };
157
+ }
291
158
  if (first.kind === "hq-configuration") {
292
159
  return {
293
160
  status: "FAIL",
@@ -298,6 +165,36 @@ function resultForGroup(entries, company) {
298
165
  };
299
166
  }
300
167
  const companyArg = company ? ` --company ${company}` : "";
168
+ if (first.kind === "undiagnosed") {
169
+ // The control plane reported a fault and explained none of it. Printing the
170
+ // reconnect command here would be the same guess `classifyConnection`
171
+ // refuses to make: it may cost the reader a working credential and, when it
172
+ // does not help, leaves them with nothing else to try. Name what HQ knows,
173
+ // point at the one command that can show more, and let a human decide.
174
+ //
175
+ // One `show` per listed connection, not just the first. Grouping keys on
176
+ // the message, and every undiagnosed row carries the same HQ-authored
177
+ // message whatever its unrecognized reason code was — so a single command
178
+ // would leave the rest of the group uninspected with their differing
179
+ // hidden reasons unread. `show` resolves exactly one connection.
180
+ const inspect = preview
181
+ .map((id) => `hq integrations show --connection ${id}${companyArg}`)
182
+ .join("; ");
183
+ return {
184
+ status: "FAIL",
185
+ checkId: `${INTEGRATIONS_PREFIX}.undiagnosed.${first.provider}`,
186
+ target: namedConnections,
187
+ // The caution belongs only where nothing supplied a fix. With a
188
+ // server-authored remediation printed below, warning that reconnecting
189
+ // may not help would contradict the very instruction being given.
190
+ message: serverRemediation
191
+ ? `${first.provider}: ${count} ${plural} ${first.message}.`
192
+ : `${first.provider}: ${count} ${plural} ${first.message}. HQ has no diagnosis for this, so reconnecting may not be the fix.`,
193
+ remediation: serverRemediation ?? (overflow > 0
194
+ ? `Inspect the listed connections, then repeat for the remaining ${overflow}: ${inspect}`
195
+ : inspect),
196
+ };
197
+ }
301
198
  const remediation = preview
302
199
  .map((id) => `hq integrations reconnect --connection ${id}${companyArg}`)
303
200
  .join("; ");
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Connection health classification, shared by every surface that has to explain
3
+ * why an integration is not usable.
4
+ *
5
+ * This lived inside the `hq doctor` integrations check until `list`/`show` had
6
+ * to answer the same question. It is deliberately presentation-free and makes
7
+ * no network calls: it classifies stored health signals only, so a caller can
8
+ * render a finding without deciding what the underlying codes mean. Keeping one
9
+ * copy is the point — `hq doctor` and `hq integrations show` disagreeing about
10
+ * whether a row needs a reconnect is exactly the confusion this family exists
11
+ * to remove.
12
+ *
13
+ * It never echoes untrusted provider text: upstream messages can carry tokens or
14
+ * secret-bearing URLs, so every user-visible string here is HQ-authored, and the
15
+ * only externally-sourced string that reaches a user is the server's own
16
+ * `fix_path`.
17
+ */
18
+ import type { AdminConnection } from "../../commands/integrations-core.js";
19
+ /** Additive health fields returned by newer control planes. */
20
+ export interface IntegrationConnection extends AdminConnection {
21
+ errorReason?: string;
22
+ degradedReason?: string;
23
+ needsReauthReason?: string;
24
+ /** Server-derived remediation flow; optional for older control planes. */
25
+ fix_path?: string;
26
+ /** Server-derived remediation class; optional for older control planes. */
27
+ fix_kind?: string;
28
+ }
29
+ /**
30
+ * Remediation class — WHO has to act and WHAT they have to do.
31
+ *
32
+ * `undiagnosed` is the console's twin member (`_connection-health.ts` `FixKind`)
33
+ * and lands here to close that divergence. Before it, an unhealthy row the
34
+ * server described with neither a recognised reason code NOR a `fix_kind` fell
35
+ * back to `reconnect`, so `hq doctor` told a person to sign in again on the
36
+ * strength of no diagnosis at all — advice that can make them discard a
37
+ * credential that was never the problem, and that leaves them no better off
38
+ * when it does not help. An admission is more useful than a guess.
39
+ *
40
+ * A server that DID say `fix_kind: "reconnect"` still classifies as
41
+ * `reconnect`: that is a diagnosis, not an absence of one.
42
+ */
43
+ export type FindingKind = "reconnect" | "re-add" | "contact-admin" | "wait" | "provider-blocked" | "retryable" | "hq-configuration" | "undiagnosed";
44
+ export interface Finding {
45
+ provider: string;
46
+ connectionId: string;
47
+ kind: FindingKind;
48
+ message: string;
49
+ /** Optional HQ-authored remediation returned by the control plane. */
50
+ remediation?: string;
51
+ }
52
+ /** Classify without echoing untrusted provider text, which may contain secrets. */
53
+ export declare function classifyConnection(connection: IntegrationConnection): Finding[];
54
+ /**
55
+ * What a human should be told about one unhealthy connection.
56
+ *
57
+ * `null` means there is nothing to say: the row is healthy, or it is a revoked
58
+ * tombstone, which every surface already explains through its own re-add path.
59
+ */
60
+ export interface ConnectionHealthNotice {
61
+ kind: FindingKind;
62
+ /** HQ-authored, provider-prefixed sentence — safe to print verbatim. */
63
+ headline: string;
64
+ /** The server's role- and credential-aware fix path, when it sent one. */
65
+ remediation?: string;
66
+ }
67
+ /**
68
+ * A status alone ("error") tells a user nothing they can act on. The control
69
+ * plane already sends the diagnosis and the fix path on every non-healthy row;
70
+ * this turns those into the two lines a surface should print next to the status.
71
+ *
72
+ * Revoked rows return `null` on purpose. They are tombstones rather than broken
73
+ * connections, and `revokedConnectionDetails` already produces the correct
74
+ * re-add wording for them — classifying them here would print two competing
75
+ * recovery paths for the same row.
76
+ */
77
+ export declare function connectionHealthNotice(connection: IntegrationConnection): ConnectionHealthNotice | null;
78
+ //# sourceMappingURL=health.d.ts.map