@indigoai-us/hq-cli 5.345.31 → 5.345.32

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,9 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.32] — 2026-10-06
6
+ - `hq integrations list --usable` lists only the apps you can use right now: connected, shared with you under the same access rule the HQ gateway applies, and backed by a provider the gateway has registered. Each row shows the app name, its website domain, and the flag that selects exactly that connection in `hq integrations tools` and `call` (`--provider <slug>`, or `--connection <id>` when two connections share a provider). `--json` returns `{companyUid, viewerAccessKnown, apps}`. `--no-login` makes `list` fail instead of opening a browser sign-in, for hooks and scripts.
7
+
5
8
  ## [5.345.31] — 2026-10-06
6
9
  - HQ Anywhere commands now require both the operator runtime switch and the signed-in person's `hq-anywhere` setting. `hq doctor` reports the setting state and access stays off when it cannot be read.
7
10
 
@@ -4054,6 +4054,14 @@ export declare const COMMAND_CATALOG: readonly [{
4054
4054
  readonly flags: "--json";
4055
4055
  readonly description: "Machine-readable output";
4056
4056
  readonly hidden: false;
4057
+ }, {
4058
+ readonly flags: "--usable";
4059
+ readonly description: "Only apps you can use right now (connected and shared with you), with the --provider value to call them";
4060
+ readonly hidden: false;
4061
+ }, {
4062
+ readonly flags: "--no-login";
4063
+ readonly description: "Fail instead of opening a browser sign-in when there is no HQ session";
4064
+ readonly hidden: false;
4057
4065
  }];
4058
4066
  readonly subcommands: readonly [];
4059
4067
  }, {
@@ -4897,6 +4897,16 @@ export const COMMAND_CATALOG = [
4897
4897
  "flags": "--json",
4898
4898
  "description": "Machine-readable output",
4899
4899
  "hidden": false
4900
+ },
4901
+ {
4902
+ "flags": "--usable",
4903
+ "description": "Only apps you can use right now (connected and shared with you), with the --provider value to call them",
4904
+ "hidden": false
4905
+ },
4906
+ {
4907
+ "flags": "--no-login",
4908
+ "description": "Fail instead of opening a browser sign-in when there is no HQ session",
4909
+ "hidden": false
4900
4910
  }
4901
4911
  ],
4902
4912
  "subcommands": []
@@ -225,6 +225,46 @@ export declare function toolPrefixForProvider(provider: string): string;
225
225
  */
226
226
  export declare function fetchAdminSurface(token: string, companyUid: string): Promise<AdminSurface>;
227
227
  export declare function fetchConnections(token: string, companyUid: string): Promise<AdminConnection[]>;
228
+ /**
229
+ * One app the caller can use right now through the HQ gateway. Field names
230
+ * follow the bot `company_apps` projection (name, provider, domain, status),
231
+ * so session context and bot context describe an app the same way. No
232
+ * endpoint URLs, credentials, scopes, or creator identities.
233
+ */
234
+ export interface UsableApp {
235
+ connectionId: string;
236
+ /** Display name, stripped of control characters and capped in length. */
237
+ name: string;
238
+ provider: string;
239
+ domain: string | null;
240
+ status: string;
241
+ /** Bare provider slug, e.g. `notion`. */
242
+ providerArg: string;
243
+ /**
244
+ * The flag that resolves to exactly this connection in `hq integrations
245
+ * tools|call`: `--provider <slug>` when no other active row shares the
246
+ * provider, else `--connection <id>`.
247
+ */
248
+ selector: string;
249
+ }
250
+ export interface UsableAppsResult {
251
+ companyUid: string;
252
+ /**
253
+ * False when the server ignored `view=summary` (an older deploy). Its rows
254
+ * carry no `viewerAccess`, so nothing can be confirmed usable and `apps` is
255
+ * empty rather than guessed.
256
+ */
257
+ viewerAccessKnown: boolean;
258
+ apps: UsableApp[];
259
+ }
260
+ /**
261
+ * The apps the caller may use, from `GET /v1/integrations/admin?view=summary`.
262
+ * A row counts only when all of these hold, mirroring the gateway's checks:
263
+ * the server's per-caller ACL decision (`viewerAccess.usable`) is true, the
264
+ * status is one the gateway serves, and the gateway has a provider registered
265
+ * for it. Fails closed: a row without `viewerAccess` is not listed.
266
+ */
267
+ export declare function fetchUsableApps(token: string, companyUid: string): Promise<UsableAppsResult>;
228
268
  /**
229
269
  * Resolve one connection by `--connection acct_…` or `--provider linear`
230
270
  * (matches `factory:<slug>`, bare provider ids, and an installation's human
@@ -355,6 +355,96 @@ export async function fetchAdminSurface(token, companyUid) {
355
355
  export async function fetchConnections(token, companyUid) {
356
356
  return (await fetchAdminSurface(token, companyUid)).connections;
357
357
  }
358
+ /**
359
+ * Statuses the gateway serves. `needs-attention` is derived by the summary
360
+ * view from a stored `connected` row whose token renewal is failing, so the
361
+ * gateway still dispatches to it.
362
+ */
363
+ const GATEWAY_SERVED_STATUSES = new Set(["connected", "needs-attention"]);
364
+ /**
365
+ * Providers hq-pro registers statically (`createIntegrationProviderRegistry`).
366
+ * Every other provider is registered per company from a factory installation
367
+ * keyed by its `runtimeProviderId`; a connection with neither is refused by the
368
+ * gateway as `ProviderUnsupported`.
369
+ */
370
+ const STATIC_GATEWAY_PROVIDERS = new Set(["google-workspace", "linear", "slack"]);
371
+ const MAX_APP_NAME_LENGTH = 60;
372
+ /**
373
+ * Display names are set by whoever installed the app and end up in agent
374
+ * context, so keep them to one short line of printable text.
375
+ */
376
+ function safeAppName(raw, fallback) {
377
+ const cleaned = (raw ?? "")
378
+ .replace(/[\p{Cc}\p{Cf}\u2028\u2029]/gu, " ")
379
+ .replace(/\s+/g, " ")
380
+ .trim();
381
+ if (!cleaned)
382
+ return fallback;
383
+ const chars = Array.from(cleaned);
384
+ return chars.length > MAX_APP_NAME_LENGTH
385
+ ? `${chars.slice(0, MAX_APP_NAME_LENGTH - 1).join("").trimEnd()}…`
386
+ : cleaned;
387
+ }
388
+ /** Case-insensitive code-point order, so the list reads the same in every locale. */
389
+ function compareNames(a, b) {
390
+ const x = a.toLowerCase();
391
+ const y = b.toLowerCase();
392
+ return x < y ? -1 : x > y ? 1 : 0;
393
+ }
394
+ function gatewayHasProvider(row) {
395
+ if (STATIC_GATEWAY_PROVIDERS.has(row.provider))
396
+ return true;
397
+ return row.installation?.runtimeProviderId === row.provider;
398
+ }
399
+ /**
400
+ * The apps the caller may use, from `GET /v1/integrations/admin?view=summary`.
401
+ * A row counts only when all of these hold, mirroring the gateway's checks:
402
+ * the server's per-caller ACL decision (`viewerAccess.usable`) is true, the
403
+ * status is one the gateway serves, and the gateway has a provider registered
404
+ * for it. Fails closed: a row without `viewerAccess` is not listed.
405
+ */
406
+ export async function fetchUsableApps(token, companyUid) {
407
+ const res = await vaultApiFetch({
408
+ token,
409
+ path: "/v1/integrations/admin",
410
+ query: { companyUid, view: "summary" },
411
+ });
412
+ if (!res.ok)
413
+ await raiseForResponse(res, "Failed to list integrations");
414
+ const data = (await res.json());
415
+ const viewerAccessKnown = data.view === "summary";
416
+ const rows = Array.isArray(data.connections) ? data.connections : [];
417
+ // `--provider` picks the first non-revoked row with that provider, whoever
418
+ // may use it, so a shared provider needs `--connection` to be exact.
419
+ const activeByProvider = new Map();
420
+ for (const row of rows) {
421
+ if (row.status === "revoked")
422
+ continue;
423
+ const key = bareProvider(row.provider).toLowerCase();
424
+ activeByProvider.set(key, (activeByProvider.get(key) ?? 0) + 1);
425
+ }
426
+ const apps = viewerAccessKnown
427
+ ? rows
428
+ .filter((row) => row.viewerAccess?.usable === true &&
429
+ GATEWAY_SERVED_STATUSES.has(row.status) &&
430
+ gatewayHasProvider(row))
431
+ .map((row) => {
432
+ const providerArg = bareProvider(row.provider);
433
+ const shared = (activeByProvider.get(providerArg.toLowerCase()) ?? 0) > 1;
434
+ return {
435
+ connectionId: row.id,
436
+ name: safeAppName(row.installation?.displayName, providerArg),
437
+ provider: row.provider,
438
+ domain: typeof row.domain === "string" && row.domain ? row.domain : null,
439
+ status: row.status,
440
+ providerArg,
441
+ selector: shared ? `--connection ${row.id}` : `--provider ${providerArg}`,
442
+ };
443
+ })
444
+ .sort((a, b) => compareNames(a.name, b.name))
445
+ : [];
446
+ return { companyUid: data.companyUid ?? companyUid, viewerAccessKnown, apps };
447
+ }
358
448
  /**
359
449
  * Resolve one connection by `--connection acct_…` or `--provider linear`
360
450
  * (matches `factory:<slug>`, bare provider ids, and an installation's human
@@ -51,8 +51,8 @@
51
51
  */
52
52
  import { Command } from "commander";
53
53
  import { type PendingApproval } from "./integrations-api.js";
54
- export { IntegrationsCliError, callGateway, fetchConnections, queuedOutcome, selectConnection, toolPrefixForProvider, unwrapGatewayResult, } from "./integrations-core.js";
55
- export type { AdminConnection } from "./integrations-core.js";
54
+ export { IntegrationsCliError, callGateway, fetchConnections, fetchUsableApps, queuedOutcome, selectConnection, toolPrefixForProvider, unwrapGatewayResult, } from "./integrations-core.js";
55
+ export type { AdminConnection, UsableApp, UsableAppsResult } from "./integrations-core.js";
56
56
  /** Wait for a decided queue item without resending the approve request. */
57
57
  export declare function waitForApprovalTerminal(token: string, companyUid: string, queueId: string, options?: {
58
58
  timeoutMs?: number;
@@ -54,12 +54,12 @@ import chalk from "chalk";
54
54
  import { ensureCognitoIdToken } from "../utils/cognito-session.js";
55
55
  import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
56
56
  import { redactSensitiveUrl, redactSensitiveUrls, redactUrlsInText } from "../utils/redact-url.js";
57
- import { IntegrationsCliError, bareProvider, callGateway, fetchConnections, isClientError, printHealthNotice, printJson, printIntegrationCallResultJson, raiseIfUnauthorized, raiseIfUpstreamUnavailable, gatewayResultIsError, selectConnection, revokedConnectionDetails, toolPrefixForProvider, unwrapGatewayResult, queuedOutcome, } from "./integrations-core.js";
57
+ import { IntegrationsCliError, bareProvider, callGateway, fetchConnections, fetchUsableApps, isClientError, printHealthNotice, printJson, printIntegrationCallResultJson, raiseIfUnauthorized, raiseIfUpstreamUnavailable, gatewayResultIsError, selectConnection, revokedConnectionDetails, toolPrefixForProvider, unwrapGatewayResult, queuedOutcome, } from "./integrations-core.js";
58
58
  import { registerConnectCommands } from "./integrations-connect.js";
59
59
  import { registerImportCommands } from "./integrations-import.js";
60
60
  import { registerManageCommands } from "./integrations-manage.js";
61
61
  import { fetchPendingApprovals } from "./integrations-api.js";
62
- export { IntegrationsCliError, callGateway, fetchConnections, queuedOutcome, selectConnection, toolPrefixForProvider, unwrapGatewayResult, } from "./integrations-core.js";
62
+ export { IntegrationsCliError, callGateway, fetchConnections, fetchUsableApps, queuedOutcome, selectConnection, toolPrefixForProvider, unwrapGatewayResult, } from "./integrations-core.js";
63
63
  /**
64
64
  * Gateway metadata is additive and may be absent when talking to an older
65
65
  * gateway. Parse it defensively so an unknown or malformed value never breaks
@@ -220,9 +220,33 @@ export function registerIntegrationsCommand(program) {
220
220
  .description("List the company's connected apps")
221
221
  .option("--company <slug>", "Company slug, e.g. indigo (defaults to your single active company)")
222
222
  .option("--json", "Machine-readable output")
223
+ .option("--usable", "Only apps you can use right now (connected and shared with you), with the --provider value to call them")
224
+ .option("--no-login", "Fail instead of opening a browser sign-in when there is no HQ session")
223
225
  .action(async (opts) => {
224
- const token = await ensureCognitoIdToken();
226
+ const token = await ensureCognitoIdToken(opts.login === false ? { interactive: false } : {});
225
227
  const companyUid = await getCompanyUid(token, opts.company);
228
+ if (opts.usable) {
229
+ const result = await fetchUsableApps(token, companyUid);
230
+ if (opts.json) {
231
+ printJson(result);
232
+ return;
233
+ }
234
+ if (!result.viewerAccessKnown) {
235
+ console.log("This HQ server cannot report which apps are shared with you yet. Run `hq integrations list` to see every connected app.");
236
+ return;
237
+ }
238
+ if (result.apps.length === 0) {
239
+ console.log("No connected apps are available to you in this company. Run `hq integrations list` to see what is connected, or ask an admin to share one.");
240
+ return;
241
+ }
242
+ for (const app of result.apps) {
243
+ const where = app.domain ? ` (${app.domain})` : "";
244
+ const flag = app.status === "needs-attention" ? chalk.yellow(" needs attention") : "";
245
+ console.log(`${chalk.bold(app.name)}${chalk.dim(where)}${flag}`);
246
+ console.log(chalk.dim(` hq integrations tools ${app.selector}`));
247
+ }
248
+ return;
249
+ }
226
250
  const connections = await fetchConnections(token, companyUid);
227
251
  if (opts.json) {
228
252
  printJson(connections);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.31",
3
+ "version": "5.345.32",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {