@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 +3 -0
- package/dist/command-catalog.generated.d.ts +8 -0
- package/dist/command-catalog.generated.js +10 -0
- package/dist/commands/integrations-core.d.ts +40 -0
- package/dist/commands/integrations-core.js +90 -0
- package/dist/commands/integrations.d.ts +2 -2
- package/dist/commands/integrations.js +27 -3
- package/package.json +1 -1
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);
|