@indigoai-us/hq-cli 5.103.20 → 5.103.22
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 +26 -0
- package/dist/commands/agents.d.ts +23 -0
- package/dist/commands/agents.js +95 -9
- package/dist/commands/auth.d.ts +1 -1
- package/dist/commands/auth.js +2 -2
- package/dist/commands/integrations-api.d.ts +29 -0
- package/dist/commands/integrations-api.js +35 -0
- package/dist/commands/integrations-connect.js +89 -43
- package/dist/commands/integrations-core.d.ts +24 -2
- package/dist/commands/integrations-core.js +88 -12
- package/dist/commands/integrations-import.d.ts +24 -0
- package/dist/commands/integrations-import.js +364 -0
- package/dist/commands/integrations-manage.d.ts +1 -1
- package/dist/commands/integrations-manage.js +159 -4
- package/dist/commands/integrations.d.ts +3 -0
- package/dist/commands/integrations.js +143 -9
- package/dist/commands/mcp-registration.d.ts +2 -2
- package/dist/commands/mcp-registration.js +8 -6
- package/dist/utils/vault-api.d.ts +12 -0
- package/dist/utils/vault-api.js +13 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `hq integrations show | policy | grants | grant | ungrant | access | share |
|
|
3
|
-
* unshare | audit | pending | disconnect`.
|
|
3
|
+
* unshare | audit | pending | disconnect | purge`.
|
|
4
4
|
*
|
|
5
5
|
* The govern-and-remove half of the lifecycle. Two different permission
|
|
6
6
|
* surfaces live here and are easy to confuse, so they get separate verbs:
|
|
@@ -16,8 +16,8 @@ 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, fetchAdminSurface, printJson, resolveConnection, selectConnection, } from "./integrations-core.js";
|
|
20
|
-
import { fetchPendingApprovals, getConnectionAccess, mutateConnectionAccess, uninstallIntegration, updateGovernance, } from "./integrations-api.js";
|
|
19
|
+
import { IntegrationsCliError, bareProvider, fetchConnections, fetchAdminSurface, printJson, revokedConnectionDetails, resolveConnection, selectConnection, } from "./integrations-core.js";
|
|
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"];
|
|
23
23
|
/** Plain-English gloss for each write policy, used in every policy readout. */
|
|
@@ -97,6 +97,67 @@ function confirm(message) {
|
|
|
97
97
|
function connectionLabel(connection) {
|
|
98
98
|
return connection.installation?.displayName ?? bareProvider(connection.provider);
|
|
99
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* The category of match must mirror `selectConnection`: an exact provider id
|
|
102
|
+
* takes precedence over a display name, which takes precedence over its human
|
|
103
|
+
* slug. Unlike ordinary verbs, purge deliberately operates on historical
|
|
104
|
+
* revoked rows, so it must not inherit selectConnection's active-first policy.
|
|
105
|
+
*/
|
|
106
|
+
function purgeSelectorMatches(connections, selector) {
|
|
107
|
+
if (selector.connection) {
|
|
108
|
+
return connections.filter((connection) => connection.id === selector.connection);
|
|
109
|
+
}
|
|
110
|
+
if (selector.provider) {
|
|
111
|
+
const want = selector.provider.trim().toLowerCase();
|
|
112
|
+
const wantHumanSlug = humanSlug(selector.provider);
|
|
113
|
+
const providerMatches = connections.filter((connection) => bareProvider(connection.provider).toLowerCase() === want ||
|
|
114
|
+
connection.provider.toLowerCase() === want);
|
|
115
|
+
if (providerMatches.length > 0)
|
|
116
|
+
return providerMatches;
|
|
117
|
+
const displayNameMatches = connections.filter((connection) => connection.installation?.displayName?.trim().toLowerCase() === want);
|
|
118
|
+
if (displayNameMatches.length > 0)
|
|
119
|
+
return displayNameMatches;
|
|
120
|
+
if (wantHumanSlug) {
|
|
121
|
+
return connections.filter((connection) => {
|
|
122
|
+
const displayName = connection.installation?.displayName;
|
|
123
|
+
return displayName !== undefined && humanSlug(displayName) === wantHumanSlug;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
return connections;
|
|
129
|
+
}
|
|
130
|
+
function humanSlug(value) {
|
|
131
|
+
return value
|
|
132
|
+
.trim()
|
|
133
|
+
.toLowerCase()
|
|
134
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
135
|
+
.replace(/^-+|-+$/g, "");
|
|
136
|
+
}
|
|
137
|
+
function purgeSelector(app, opts) {
|
|
138
|
+
if (app && !opts.provider && !opts.connection) {
|
|
139
|
+
return app.startsWith("acct_") ? { connection: app } : { provider: app };
|
|
140
|
+
}
|
|
141
|
+
return opts;
|
|
142
|
+
}
|
|
143
|
+
async function resolveRevokedPurgeConnection(token, companyUid, app, opts) {
|
|
144
|
+
const selector = purgeSelector(app, opts);
|
|
145
|
+
const connections = await fetchConnections(token, companyUid);
|
|
146
|
+
const matches = purgeSelectorMatches(connections, selector);
|
|
147
|
+
const revoked = matches.filter((connection) => connection.status === "revoked");
|
|
148
|
+
if (revoked.length === 1)
|
|
149
|
+
return revoked[0];
|
|
150
|
+
if (revoked.length > 1) {
|
|
151
|
+
const selected = selector.connection ?? selector.provider ?? "the supplied selector";
|
|
152
|
+
throw new IntegrationsCliError(`Multiple revoked connections match '${selected}'. Use --connection <acct_id> to choose one:\n` +
|
|
153
|
+
revoked.map((connection) => ` --connection ${connection.id} (${connectionLabel(connection)})`).join("\n"), { expected: true });
|
|
154
|
+
}
|
|
155
|
+
if (matches.length > 0) {
|
|
156
|
+
throw new IntegrationsCliError(`${connectionLabel(matches[0])} is still connected. Disconnect it first, then purge it.`, { expected: true });
|
|
157
|
+
}
|
|
158
|
+
// Keep the existing command group's not-found wording and error taxonomy.
|
|
159
|
+
return selectConnection(connections, selector);
|
|
160
|
+
}
|
|
100
161
|
/**
|
|
101
162
|
* Governance writes are owner-only on hq-pro AND the allowlist PATCH is a
|
|
102
163
|
* whole-list REPLACE. A non-owner reads an identity-redacted grant list, so
|
|
@@ -144,7 +205,9 @@ export function registerManageCommands(integrations) {
|
|
|
144
205
|
? selectConnection(surface.connections, app.startsWith("acct_") ? { connection: app } : { provider: app })
|
|
145
206
|
: selectConnection(surface.connections, opts);
|
|
146
207
|
if (opts.json) {
|
|
147
|
-
printJson(connection
|
|
208
|
+
printJson(connection.status === "revoked"
|
|
209
|
+
? { ...connection, ...revokedConnectionDetails(connection, opts.company) }
|
|
210
|
+
: connection);
|
|
148
211
|
return;
|
|
149
212
|
}
|
|
150
213
|
const install = connection.installation;
|
|
@@ -153,6 +216,11 @@ export function registerManageCommands(integrations) {
|
|
|
153
216
|
if (install?.id)
|
|
154
217
|
console.log(chalk.dim(` installation: ${install.id}`));
|
|
155
218
|
console.log(chalk.dim(` status: ${connection.status}`));
|
|
219
|
+
if (connection.status === "revoked") {
|
|
220
|
+
const details = revokedConnectionDetails(connection, opts.company);
|
|
221
|
+
console.log(chalk.yellow(` Revoked — ${details.reason}`));
|
|
222
|
+
console.log(chalk.yellow(` Re-add: ${details.fixPath}`));
|
|
223
|
+
}
|
|
156
224
|
if (install?.domain)
|
|
157
225
|
console.log(chalk.dim(` domain: ${install.domain}`));
|
|
158
226
|
if (install?.surface?.url)
|
|
@@ -218,6 +286,47 @@ export function registerManageCommands(integrations) {
|
|
|
218
286
|
}
|
|
219
287
|
console.log(chalk.green(`${connectionLabel(connection)}: ${writePolicy} — ${POLICY_BLURB[writePolicy]}`));
|
|
220
288
|
});
|
|
289
|
+
integrations
|
|
290
|
+
.command("read-safe <tool>")
|
|
291
|
+
.description("Mark a read-only tool as safe to run without approval (owner only)")
|
|
292
|
+
.option("--company <slug>", "Company slug, e.g. indigo")
|
|
293
|
+
.option("--provider <slug>", "Connected app (e.g. linear)")
|
|
294
|
+
.option("--connection <id>", "Connection id (acct_…)")
|
|
295
|
+
.option("--unset", "Remove the read-safe override")
|
|
296
|
+
.option("--off", "Alias for --unset")
|
|
297
|
+
.option("--json", "Machine-readable output")
|
|
298
|
+
.action(async (tool, opts) => {
|
|
299
|
+
const toolName = tool.trim();
|
|
300
|
+
if (!toolName) {
|
|
301
|
+
throw new IntegrationsCliError("Tool name cannot be empty.", { expected: true });
|
|
302
|
+
}
|
|
303
|
+
const token = await ensureCognitoIdToken();
|
|
304
|
+
const companyUid = await getCompanyUid(token, opts.company);
|
|
305
|
+
const connection = await resolveConnection(token, companyUid, undefined, opts);
|
|
306
|
+
const readSafe = !(opts.unset || opts.off);
|
|
307
|
+
try {
|
|
308
|
+
await setReadSafe(token, companyUid, connection.id, toolName, readSafe);
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
if (error instanceof IntegrationsCliError) {
|
|
312
|
+
if (error.status === 403) {
|
|
313
|
+
throw new IntegrationsCliError("This command is owner only. Ask a company owner to manage read-safe tools.", { expected: true });
|
|
314
|
+
}
|
|
315
|
+
if (readSafe && (error.status === 409 || error.status === 422)) {
|
|
316
|
+
throw new IntegrationsCliError(`${toolName} is a write tool and can't be marked read-safe (its changes always need approval).`, { expected: true });
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
throw error;
|
|
320
|
+
}
|
|
321
|
+
const outcome = { connectionId: connection.id, toolName, readSafe };
|
|
322
|
+
if (opts.json) {
|
|
323
|
+
printJson(outcome);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
console.log(chalk.green(readSafe
|
|
327
|
+
? `${toolName} on ${connectionLabel(connection)} is now read-safe and skips approval.`
|
|
328
|
+
: `${toolName} on ${connectionLabel(connection)} is no longer read-safe; its override was removed.`));
|
|
329
|
+
});
|
|
221
330
|
integrations
|
|
222
331
|
.command("grants [app]")
|
|
223
332
|
.description("Show who is authorized to run an app's change-making tools")
|
|
@@ -518,6 +627,52 @@ export function registerManageCommands(integrations) {
|
|
|
518
627
|
}
|
|
519
628
|
console.log(chalk.green(`Disconnected ${connectionLabel(connection)}.`));
|
|
520
629
|
});
|
|
630
|
+
integrations
|
|
631
|
+
.command("purge [app]")
|
|
632
|
+
.description("Permanently remove a revoked connection from the list (owner only)")
|
|
633
|
+
.option("--company <slug>", "Company slug, e.g. indigo")
|
|
634
|
+
.option("--provider <slug>", "Connected app (e.g. linear)")
|
|
635
|
+
.option("--connection <id>", "Connection id (acct_…)")
|
|
636
|
+
.option("--yes", "Skip the permanent-removal confirmation prompt")
|
|
637
|
+
.option("--json", "Machine-readable output")
|
|
638
|
+
.action(async (app, opts) => {
|
|
639
|
+
const token = await ensureCognitoIdToken();
|
|
640
|
+
const companyUid = await getCompanyUid(token, opts.company);
|
|
641
|
+
const connection = await resolveRevokedPurgeConnection(token, companyUid, app, opts);
|
|
642
|
+
const label = connectionLabel(connection);
|
|
643
|
+
if (!opts.yes) {
|
|
644
|
+
// Warning + prompt go to stderr: with --json, stdout must stay parseable.
|
|
645
|
+
console.error(`Purging ${chalk.bold(label)} permanently removes its revoked connection record. It cannot be restored.`);
|
|
646
|
+
const ok = await confirm(`Permanently purge ${label}?`);
|
|
647
|
+
if (!ok) {
|
|
648
|
+
// A non-TTY run lands here too: refusing is the only safe default
|
|
649
|
+
// when nobody can confirm a permanent deletion.
|
|
650
|
+
throw new IntegrationsCliError("Not purged. Re-run with --yes if you are sure.", { expected: true });
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
try {
|
|
654
|
+
await purgeConnection(token, companyUid, connection.id);
|
|
655
|
+
}
|
|
656
|
+
catch (error) {
|
|
657
|
+
if (error instanceof IntegrationsCliError && error.status === 409) {
|
|
658
|
+
throw new IntegrationsCliError(`${label} is still connected. Disconnect it first, then purge it.`, { expected: true, status: 409, code: error.code });
|
|
659
|
+
}
|
|
660
|
+
if (error instanceof IntegrationsCliError && error.status === 403) {
|
|
661
|
+
throw new IntegrationsCliError("Purging a connection is owner only. Ask a company owner to run this.", { expected: true, status: 403, code: error.code });
|
|
662
|
+
}
|
|
663
|
+
throw error;
|
|
664
|
+
}
|
|
665
|
+
const outcome = {
|
|
666
|
+
connectionId: connection.id,
|
|
667
|
+
provider: bareProvider(connection.provider),
|
|
668
|
+
status: "purged",
|
|
669
|
+
};
|
|
670
|
+
if (opts.json) {
|
|
671
|
+
printJson(outcome);
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
console.log(chalk.green(`Purged ${label}. It will no longer appear in \`hq integrations list\`.`));
|
|
675
|
+
});
|
|
521
676
|
}
|
|
522
677
|
/** Order-insensitive identity of an allowlist, for change detection. */
|
|
523
678
|
export function allowlistFingerprint(grants) {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* hq integrations discover <docsUrl> Find a server from its docs page.
|
|
15
15
|
* hq integrations connect <app> Connect it (no-auth, key, OAuth).
|
|
16
16
|
* hq integrations reconnect [app] Re-authenticate a broken app.
|
|
17
|
+
* hq integrations import Import Claude Desktop connectors.
|
|
17
18
|
*
|
|
18
19
|
* Use:
|
|
19
20
|
* hq integrations list --company indigo Connected apps for a company.
|
|
@@ -25,10 +26,12 @@
|
|
|
25
26
|
*
|
|
26
27
|
* Govern and remove:
|
|
27
28
|
* hq integrations policy [app] --set <m> Approval setting for changes.
|
|
29
|
+
* hq integrations read-safe <tool> Mark/unmark a trusted read-only tool.
|
|
28
30
|
* hq integrations grants|grant|ungrant Per-tool authorization to run change-making tools.
|
|
29
31
|
* hq integrations access|share|unshare Who may use the app.
|
|
30
32
|
* hq integrations audit Recent activity.
|
|
31
33
|
* hq integrations disconnect [app] Remove it and its credentials.
|
|
34
|
+
* hq integrations purge [app] Permanently remove a revoked connection row.
|
|
32
35
|
*
|
|
33
36
|
* Governance: reads flow freely; calls that can change the app are subject to
|
|
34
37
|
* the connection's write policy (default: a person approves first). A queued
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* hq integrations discover <docsUrl> Find a server from its docs page.
|
|
15
15
|
* hq integrations connect <app> Connect it (no-auth, key, OAuth).
|
|
16
16
|
* hq integrations reconnect [app] Re-authenticate a broken app.
|
|
17
|
+
* hq integrations import Import Claude Desktop connectors.
|
|
17
18
|
*
|
|
18
19
|
* Use:
|
|
19
20
|
* hq integrations list --company indigo Connected apps for a company.
|
|
@@ -25,10 +26,12 @@
|
|
|
25
26
|
*
|
|
26
27
|
* Govern and remove:
|
|
27
28
|
* hq integrations policy [app] --set <m> Approval setting for changes.
|
|
29
|
+
* hq integrations read-safe <tool> Mark/unmark a trusted read-only tool.
|
|
28
30
|
* hq integrations grants|grant|ungrant Per-tool authorization to run change-making tools.
|
|
29
31
|
* hq integrations access|share|unshare Who may use the app.
|
|
30
32
|
* hq integrations audit Recent activity.
|
|
31
33
|
* hq integrations disconnect [app] Remove it and its credentials.
|
|
34
|
+
* hq integrations purge [app] Permanently remove a revoked connection row.
|
|
32
35
|
*
|
|
33
36
|
* Governance: reads flow freely; calls that can change the app are subject to
|
|
34
37
|
* the connection's write policy (default: a person approves first). A queued
|
|
@@ -49,10 +52,113 @@ import { randomUUID } from "node:crypto";
|
|
|
49
52
|
import chalk from "chalk";
|
|
50
53
|
import { ensureCognitoIdToken } from "../utils/cognito-session.js";
|
|
51
54
|
import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
|
|
52
|
-
import { IntegrationsCliError, bareProvider, callGateway, fetchConnections, isClientError, printJson, raiseIfUnauthorized, raiseIfUpstreamUnavailable, gatewayResultIsError, selectConnection, toolPrefixForProvider, unwrapGatewayResult, queuedOutcome, } from "./integrations-core.js";
|
|
55
|
+
import { IntegrationsCliError, bareProvider, callGateway, fetchConnections, isClientError, printJson, raiseIfUnauthorized, raiseIfUpstreamUnavailable, gatewayResultIsError, selectConnection, revokedConnectionDetails, toolPrefixForProvider, unwrapGatewayResult, queuedOutcome, } from "./integrations-core.js";
|
|
53
56
|
import { registerConnectCommands } from "./integrations-connect.js";
|
|
57
|
+
import { registerImportCommands } from "./integrations-import.js";
|
|
54
58
|
import { registerManageCommands } from "./integrations-manage.js";
|
|
55
59
|
export { IntegrationsCliError, callGateway, fetchConnections, queuedOutcome, selectConnection, toolPrefixForProvider, unwrapGatewayResult, } from "./integrations-core.js";
|
|
60
|
+
/**
|
|
61
|
+
* Gateway metadata is additive and may be absent when talking to an older
|
|
62
|
+
* gateway. Parse it defensively so an unknown or malformed value never breaks
|
|
63
|
+
* tool discovery.
|
|
64
|
+
*/
|
|
65
|
+
function effectiveToolMode(tool) {
|
|
66
|
+
const raw = tool._meta?.["hq/effective-mode"];
|
|
67
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
68
|
+
return null;
|
|
69
|
+
const mode = raw.mode;
|
|
70
|
+
const approvalRequired = raw.approvalRequired;
|
|
71
|
+
if ((mode !== "read" && mode !== "write") || typeof approvalRequired !== "boolean") {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const denied = raw.denied;
|
|
75
|
+
const policy = raw.policy;
|
|
76
|
+
return {
|
|
77
|
+
mode,
|
|
78
|
+
approvalRequired,
|
|
79
|
+
...(denied === true ? { denied: true } : {}),
|
|
80
|
+
...(policy === "deny" ? { policy: "deny" } : {}),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function toolBadge(tool) {
|
|
84
|
+
const effectiveMode = effectiveToolMode(tool);
|
|
85
|
+
if (!effectiveMode)
|
|
86
|
+
return null;
|
|
87
|
+
if (effectiveMode.mode === "read")
|
|
88
|
+
return "read";
|
|
89
|
+
if (effectiveMode.denied)
|
|
90
|
+
return "denied";
|
|
91
|
+
return effectiveMode.approvalRequired ? "approval-required" : "write";
|
|
92
|
+
}
|
|
93
|
+
function formatToolBadge(tool) {
|
|
94
|
+
const badge = toolBadge(tool);
|
|
95
|
+
if (!badge)
|
|
96
|
+
return "";
|
|
97
|
+
const label = `[${badge}]`;
|
|
98
|
+
switch (badge) {
|
|
99
|
+
case "read":
|
|
100
|
+
return chalk.green(label);
|
|
101
|
+
case "approval-required":
|
|
102
|
+
return chalk.yellow(label);
|
|
103
|
+
case "denied":
|
|
104
|
+
return chalk.red(label);
|
|
105
|
+
case "write":
|
|
106
|
+
return chalk.blue(label);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** Add gateway policy metadata where scripts can consume it directly. */
|
|
110
|
+
function toolJsonView(tool) {
|
|
111
|
+
const effectiveMode = effectiveToolMode(tool);
|
|
112
|
+
return effectiveMode ? { ...tool, effectiveMode } : tool;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The gateway relays MCP tool schemas verbatim. Keep the default listing
|
|
116
|
+
* compact, but make required inputs visible before a caller has to discover
|
|
117
|
+
* them through a validation error.
|
|
118
|
+
*/
|
|
119
|
+
function requiredInputHint(inputSchema) {
|
|
120
|
+
if (!inputSchema || typeof inputSchema !== "object")
|
|
121
|
+
return null;
|
|
122
|
+
const required = inputSchema.required;
|
|
123
|
+
if (!Array.isArray(required))
|
|
124
|
+
return null;
|
|
125
|
+
const names = required.filter((name) => typeof name === "string");
|
|
126
|
+
return names.length > 0 ? `requires: ${names.join(", ")}` : null;
|
|
127
|
+
}
|
|
128
|
+
/** The user-facing slug stays first; the factory's display name is additive. */
|
|
129
|
+
function connectionLabel(connection) {
|
|
130
|
+
const slug = bareProvider(connection.provider);
|
|
131
|
+
const displayName = connection.installation?.displayName?.trim();
|
|
132
|
+
return displayName && displayName.toLowerCase() !== slug.toLowerCase()
|
|
133
|
+
? `${slug} (${displayName})`
|
|
134
|
+
: slug;
|
|
135
|
+
}
|
|
136
|
+
function printToolSchema(tool) {
|
|
137
|
+
if (tool.inputSchema === undefined) {
|
|
138
|
+
console.log(chalk.dim(" Input schema was not reported by this app."));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const schema = JSON.stringify(tool.inputSchema, null, 2);
|
|
142
|
+
console.log(chalk.dim(" Input schema:"));
|
|
143
|
+
console.log(chalk.dim(schema.split("\n").map((line) => ` ${line}`).join("\n")));
|
|
144
|
+
}
|
|
145
|
+
function reportRevokedConnection(connection, opts) {
|
|
146
|
+
if (connection.status !== "revoked")
|
|
147
|
+
return false;
|
|
148
|
+
const details = revokedConnectionDetails(connection, opts.company);
|
|
149
|
+
if (opts.json) {
|
|
150
|
+
printJson(details);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
console.log(chalk.yellow(`${connectionLabel(connection)}: status=revoked`));
|
|
154
|
+
console.log(chalk.yellow(` ${details.reason}`));
|
|
155
|
+
console.log(` Re-add: ${details.fixPath}`);
|
|
156
|
+
}
|
|
157
|
+
if (opts.fail) {
|
|
158
|
+
throw new IntegrationsCliError(`status=revoked. ${details.reason} Re-add with: ${details.fixPath}`, { expected: true });
|
|
159
|
+
}
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
56
162
|
export function registerIntegrationsCommand(program) {
|
|
57
163
|
const integrations = program
|
|
58
164
|
.command("integrations")
|
|
@@ -75,7 +181,6 @@ export function registerIntegrationsCommand(program) {
|
|
|
75
181
|
return;
|
|
76
182
|
}
|
|
77
183
|
for (const c of connections) {
|
|
78
|
-
const name = c.installation?.displayName ?? bareProvider(c.provider);
|
|
79
184
|
const flags = [
|
|
80
185
|
c.status,
|
|
81
186
|
c.writePolicy ? `writes: ${c.writePolicy}` : null,
|
|
@@ -83,21 +188,25 @@ export function registerIntegrationsCommand(program) {
|
|
|
83
188
|
]
|
|
84
189
|
.filter(Boolean)
|
|
85
190
|
.join(" · ");
|
|
86
|
-
console.log(`${chalk.bold(
|
|
191
|
+
console.log(`${chalk.bold(connectionLabel(c))} ${chalk.dim(flags)}`);
|
|
87
192
|
console.log(chalk.dim(` connection: ${c.id}`));
|
|
88
193
|
}
|
|
89
194
|
});
|
|
195
|
+
registerImportCommands(integrations);
|
|
90
196
|
integrations
|
|
91
197
|
.command("tools")
|
|
92
198
|
.description("List what a connected app can do")
|
|
93
199
|
.option("--provider <slug>", "Connected app (e.g. linear)")
|
|
94
200
|
.option("--connection <id>", "Connection id (acct_…)")
|
|
95
201
|
.option("--company <slug>", "Company slug, e.g. indigo")
|
|
202
|
+
.option("--describe <tool>", "Print the full input schema for one tool")
|
|
96
203
|
.option("--json", "Machine-readable output")
|
|
97
204
|
.action(async (opts) => {
|
|
98
205
|
const token = await ensureCognitoIdToken();
|
|
99
206
|
const companyUid = await getCompanyUid(token, opts.company);
|
|
100
207
|
const connection = selectConnection(await fetchConnections(token, companyUid), opts);
|
|
208
|
+
if (reportRevokedConnection(connection, opts))
|
|
209
|
+
return;
|
|
101
210
|
const prefix = toolPrefixForProvider(connection.provider);
|
|
102
211
|
const message = await callGateway(token, {
|
|
103
212
|
companyUid,
|
|
@@ -105,20 +214,43 @@ export function registerIntegrationsCommand(program) {
|
|
|
105
214
|
arguments: { companyUid, connectionId: connection.id },
|
|
106
215
|
});
|
|
107
216
|
const payload = unwrapGatewayResult(message.result);
|
|
108
|
-
if (opts.json) {
|
|
109
|
-
printJson(payload);
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
217
|
const tools = payload?.tools ?? [];
|
|
113
218
|
if (tools.length === 0) {
|
|
219
|
+
if (opts.json) {
|
|
220
|
+
printJson(payload);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
114
223
|
console.log("The app reported no tools.");
|
|
115
224
|
return;
|
|
116
225
|
}
|
|
226
|
+
if (opts.describe) {
|
|
227
|
+
const tool = tools.find((candidate) => candidate.name === opts.describe);
|
|
228
|
+
if (!tool) {
|
|
229
|
+
throw new IntegrationsCliError(`No tool named '${opts.describe}' on ${connectionLabel(connection)}.`, { expected: true });
|
|
230
|
+
}
|
|
231
|
+
if (opts.json) {
|
|
232
|
+
printJson(toolJsonView(tool));
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const badge = formatToolBadge(tool);
|
|
236
|
+
console.log(`${chalk.bold(tool.name)}${badge ? ` ${badge}` : ""} ${chalk.dim(`on ${connectionLabel(connection)}`)}`);
|
|
237
|
+
if (tool.description)
|
|
238
|
+
console.log(chalk.dim(` ${tool.description}`));
|
|
239
|
+
printToolSchema(tool);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (opts.json) {
|
|
243
|
+
printJson({ ...payload, tools: tools.map(toolJsonView) });
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
console.log(chalk.dim(`App: ${connectionLabel(connection)}`));
|
|
117
247
|
for (const tool of tools) {
|
|
118
248
|
const label = tool.title && tool.title !== tool.name ? ` ${chalk.dim(tool.title)}` : "";
|
|
119
|
-
|
|
249
|
+
const required = requiredInputHint(tool.inputSchema);
|
|
250
|
+
const badge = formatToolBadge(tool);
|
|
251
|
+
console.log(`${chalk.bold(tool.name)}${badge ? ` ${badge}` : ""}${label}${required ? ` ${chalk.dim(required)}` : ""}`);
|
|
120
252
|
}
|
|
121
|
-
console.log(chalk.dim(`\n${tools.length} tools.
|
|
253
|
+
console.log(chalk.dim(`\n${tools.length} tools. Describe inputs: hq integrations tools --provider ${bareProvider(connection.provider)} --describe <tool>\nCall one with: hq integrations call <tool> --provider ${bareProvider(connection.provider)} --args '<json>'`));
|
|
122
254
|
});
|
|
123
255
|
integrations
|
|
124
256
|
.command("call <tool>")
|
|
@@ -144,6 +276,8 @@ export function registerIntegrationsCommand(program) {
|
|
|
144
276
|
const token = await ensureCognitoIdToken();
|
|
145
277
|
const companyUid = await getCompanyUid(token, opts.company);
|
|
146
278
|
const connection = selectConnection(await fetchConnections(token, companyUid), opts);
|
|
279
|
+
if (reportRevokedConnection(connection, { ...opts, fail: true }))
|
|
280
|
+
return;
|
|
147
281
|
const prefix = toolPrefixForProvider(connection.provider);
|
|
148
282
|
const idempotencyKey = opts.idempotencyKey ?? `hq-cli-${randomUUID()}`;
|
|
149
283
|
const message = await callGateway(token, {
|
|
@@ -471,7 +471,7 @@ export declare const claudeConfigFormat: ConfigFormat<Record<string, unknown>>;
|
|
|
471
471
|
/**
|
|
472
472
|
* Build the Claude server definition emitted into `mcpServers.<name>` from a
|
|
473
473
|
* manifest: pass the transport fields through, resolve `${secret:}` in every
|
|
474
|
-
* header/env value (recording the plaintexts in `secretSink` for redaction), and
|
|
474
|
+
* argument/header/env value (recording the plaintexts in `secretSink` for redaction), and
|
|
475
475
|
* stamp `_hqPack` provenance. The output object's key order is deterministic so a
|
|
476
476
|
* re-run produces a byte-identical def (idempotency depends on stable serialize).
|
|
477
477
|
*/
|
|
@@ -673,7 +673,7 @@ export declare const codexConfigFormat: ConfigFormat<CodexTomlDoc>;
|
|
|
673
673
|
export declare function appendCodexTable(originalText: string, name: string, def: TomlTable): string;
|
|
674
674
|
/**
|
|
675
675
|
* Build the Codex server table emitted as `[mcp_servers.<name>]`: pass the
|
|
676
|
-
* transport fields through, resolve `${secret:}` in every header/env value
|
|
676
|
+
* transport fields through, resolve `${secret:}` in every argument/header/env value
|
|
677
677
|
* (recording plaintexts in `secretSink` for redaction), stamp `_hqPack` provenance,
|
|
678
678
|
* and write the per-tool `approval_mode` from the manifest where present (the Codex-
|
|
679
679
|
* specific field — `[mcp_servers.<name>.tools.<t>]` with `approval_mode = "…"`).
|
|
@@ -934,7 +934,7 @@ export const claudeConfigFormat = {
|
|
|
934
934
|
/**
|
|
935
935
|
* Build the Claude server definition emitted into `mcpServers.<name>` from a
|
|
936
936
|
* manifest: pass the transport fields through, resolve `${secret:}` in every
|
|
937
|
-
* header/env value (recording the plaintexts in `secretSink` for redaction), and
|
|
937
|
+
* argument/header/env value (recording the plaintexts in `secretSink` for redaction), and
|
|
938
938
|
* stamp `_hqPack` provenance. The output object's key order is deterministic so a
|
|
939
939
|
* re-run produces a byte-identical def (idempotency depends on stable serialize).
|
|
940
940
|
*/
|
|
@@ -944,8 +944,9 @@ export function buildClaudeServerDef(manifest, pack, resolve, secretSink) {
|
|
|
944
944
|
def.url = manifest.url;
|
|
945
945
|
if (manifest.command !== undefined)
|
|
946
946
|
def.command = manifest.command;
|
|
947
|
-
if (manifest.args !== undefined)
|
|
948
|
-
def.args = manifest.args;
|
|
947
|
+
if (manifest.args !== undefined) {
|
|
948
|
+
def.args = manifest.args.map((arg) => resolveSecretRefs(arg, resolve, secretSink));
|
|
949
|
+
}
|
|
949
950
|
if (manifest.headers !== undefined) {
|
|
950
951
|
def.headers = resolveStringMap(manifest.headers, resolve, secretSink);
|
|
951
952
|
}
|
|
@@ -1371,7 +1372,7 @@ export function appendCodexTable(originalText, name, def) {
|
|
|
1371
1372
|
}
|
|
1372
1373
|
/**
|
|
1373
1374
|
* Build the Codex server table emitted as `[mcp_servers.<name>]`: pass the
|
|
1374
|
-
* transport fields through, resolve `${secret:}` in every header/env value
|
|
1375
|
+
* transport fields through, resolve `${secret:}` in every argument/header/env value
|
|
1375
1376
|
* (recording plaintexts in `secretSink` for redaction), stamp `_hqPack` provenance,
|
|
1376
1377
|
* and write the per-tool `approval_mode` from the manifest where present (the Codex-
|
|
1377
1378
|
* specific field — `[mcp_servers.<name>.tools.<t>]` with `approval_mode = "…"`).
|
|
@@ -1384,8 +1385,9 @@ export function buildCodexServerDef(manifest, pack, resolve, secretSink) {
|
|
|
1384
1385
|
def.url = manifest.url;
|
|
1385
1386
|
if (manifest.command !== undefined)
|
|
1386
1387
|
def.command = manifest.command;
|
|
1387
|
-
if (manifest.args !== undefined)
|
|
1388
|
-
def.args = manifest.args;
|
|
1388
|
+
if (manifest.args !== undefined) {
|
|
1389
|
+
def.args = manifest.args.map((arg) => resolveSecretRefs(arg, resolve, secretSink));
|
|
1390
|
+
}
|
|
1389
1391
|
if (manifest.headers !== undefined) {
|
|
1390
1392
|
def.headers = resolveStringMap(manifest.headers, resolve, secretSink);
|
|
1391
1393
|
}
|
|
@@ -34,6 +34,18 @@ export interface VaultApiOptions {
|
|
|
34
34
|
* network call; this stays the backstop for a new call site that forgets to.
|
|
35
35
|
*/
|
|
36
36
|
export declare function rewritePathForApiKey(path: string): string | null;
|
|
37
|
+
/**
|
|
38
|
+
* Client-family identifier sent on every authenticated vault-API request.
|
|
39
|
+
*
|
|
40
|
+
* hq-pro reads this exact header into its closed `RosterClientFamily` enum
|
|
41
|
+
* (`roster-client-family.ts`) and tags each agents-request Sentry event with it
|
|
42
|
+
* (`handler.ts` setSentryClientFamily). Sending it means an event this CLI
|
|
43
|
+
* raises reads `hq_client=hq_cli` and names its origin instead of `absent` — the
|
|
44
|
+
* missing attribution behind Sentry hq-pro 7617401436. It is a fixed literal,
|
|
45
|
+
* never caller-supplied text, used only for telemetry/attribution: no
|
|
46
|
+
* authorization or request signing depends on it.
|
|
47
|
+
*/
|
|
48
|
+
export declare const HQ_CLIENT_NAME = "@indigoai-us/hq-cli";
|
|
37
49
|
export declare function vaultApiFetch(opts: VaultApiOptions): Promise<Response>;
|
|
38
50
|
/**
|
|
39
51
|
* Public (NONE-auth) GET against the vault API — no bearer token. The
|
package/dist/utils/vault-api.js
CHANGED
|
@@ -124,6 +124,18 @@ export function rewritePathForApiKey(path) {
|
|
|
124
124
|
}
|
|
125
125
|
return null;
|
|
126
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Client-family identifier sent on every authenticated vault-API request.
|
|
129
|
+
*
|
|
130
|
+
* hq-pro reads this exact header into its closed `RosterClientFamily` enum
|
|
131
|
+
* (`roster-client-family.ts`) and tags each agents-request Sentry event with it
|
|
132
|
+
* (`handler.ts` setSentryClientFamily). Sending it means an event this CLI
|
|
133
|
+
* raises reads `hq_client=hq_cli` and names its origin instead of `absent` — the
|
|
134
|
+
* missing attribution behind Sentry hq-pro 7617401436. It is a fixed literal,
|
|
135
|
+
* never caller-supplied text, used only for telemetry/attribution: no
|
|
136
|
+
* authorization or request signing depends on it.
|
|
137
|
+
*/
|
|
138
|
+
export const HQ_CLIENT_NAME = "@indigoai-us/hq-cli";
|
|
127
139
|
export async function vaultApiFetch(opts) {
|
|
128
140
|
let path = opts.path;
|
|
129
141
|
if (opts.token.startsWith("hqk_")) {
|
|
@@ -156,6 +168,7 @@ export async function vaultApiFetch(opts) {
|
|
|
156
168
|
headers: {
|
|
157
169
|
Authorization: `Bearer ${opts.token}`,
|
|
158
170
|
'Content-Type': 'application/json',
|
|
171
|
+
'x-hq-client-name': HQ_CLIENT_NAME,
|
|
159
172
|
},
|
|
160
173
|
body: opts.body ? JSON.stringify(opts.body) : undefined,
|
|
161
174
|
signal: opts.signal,
|