@indigoai-us/hq-cli 5.101.7 → 5.103.0

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.
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Native-client OAuth loopback listener for `hq integrations connect`.
3
+ *
4
+ * An OAuth-protected MCP server signs the admin in through a browser, so the
5
+ * authorization code comes back as an HTTP redirect. RFC 8252 §7.3 defines the
6
+ * native-app answer: bind an ephemeral port on the loopback interface and use
7
+ * `http://127.0.0.1:<port>/…` as the redirect URI. hq-pro validates that shape
8
+ * on `/oauth/start` (see `isCliLoopbackRedirectUri`) and keeps everything that
9
+ * matters server-side — the PKCE verifier, the single-use state row, and the
10
+ * code exchange — so a code captured here is useless on its own.
11
+ *
12
+ * Ordering matters: the listener must be bound BEFORE `/oauth/start` is called,
13
+ * because the port is part of the redirect URI hq-pro registers with the remote
14
+ * authorization server. Bind → start → open browser → await → exchange.
15
+ */
16
+ import { createServer } from "node:http";
17
+ import { IntegrationsCliError } from "./integrations-core.js";
18
+ /**
19
+ * The one callback path hq-pro admits for a loopback redirect. Must stay
20
+ * byte-identical to hq-pro's `CLI_LOOPBACK_REDIRECT_PATH`; a drift here reads
21
+ * as `OAUTH_REDIRECT_URI_NOT_ALLOWED` at connect time.
22
+ */
23
+ export const LOOPBACK_CALLBACK_PATH = "/hq/integrations/oauth/callback";
24
+ /** How long to wait for the browser round trip before giving the port back. */
25
+ const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
26
+ function respond(res, status, title, detail) {
27
+ const body = `<!doctype html><meta charset="utf-8"><title>${title}</title>
28
+ <body style="font:15px system-ui;margin:4rem auto;max-width:32rem;color:#111">
29
+ <h1 style="font-size:1.1rem;font-weight:600">${title}</h1><p>${detail}</p></body>`;
30
+ res.writeHead(status, {
31
+ "content-type": "text/html; charset=utf-8",
32
+ // The page is a terminal handoff, never something to keep or re-fetch.
33
+ "cache-control": "no-store",
34
+ });
35
+ res.end(body);
36
+ }
37
+ /**
38
+ * Bind a loopback listener on an ephemeral port and return its redirect URI.
39
+ *
40
+ * Binds to `127.0.0.1` explicitly rather than the default wildcard: a wildcard
41
+ * bind would expose the callback to the local network for the life of the
42
+ * sign-in, and the whole reason hq-pro admits this URI is that it cannot leave
43
+ * the machine.
44
+ */
45
+ export async function startLoopbackListener(opts = {}) {
46
+ let onCallback = null;
47
+ /**
48
+ * A callback that arrived before anything was waiting for it. The browser is
49
+ * opened before `waitForCode` is awaited, and a redirect can complete inside
50
+ * that window — dropping it would hang the sign-in until the timeout for no
51
+ * reason. Buffering makes the listener correct regardless of ordering.
52
+ */
53
+ let buffered = null;
54
+ /** Latched once a result is delivered OR buffered, so only the first wins. */
55
+ let settled = false;
56
+ const server = createServer((req, res) => {
57
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
58
+ if (url.pathname !== LOOPBACK_CALLBACK_PATH) {
59
+ respond(res, 404, "Not found", "This address only handles HQ sign-in callbacks.");
60
+ return;
61
+ }
62
+ const error = url.searchParams.get("error") ?? undefined;
63
+ const code = url.searchParams.get("code") ?? undefined;
64
+ const state = url.searchParams.get("state") ?? undefined;
65
+ if (error) {
66
+ respond(res, 400, "Sign-in cancelled", "You can close this tab and return to your terminal.");
67
+ }
68
+ else if (!code || !state) {
69
+ respond(res, 400, "Sign-in incomplete", "The provider did not send a code. Try again from your terminal.");
70
+ }
71
+ else {
72
+ // NOT "Connected": at this point the state has not been checked and the
73
+ // code has not been exchanged, either of which can still fail. The
74
+ // terminal is the only place that knows the real outcome, so the page
75
+ // reports receipt and sends the person back there.
76
+ respond(res, 200, "Sign-in received", "You can close this tab — check your terminal for the result.");
77
+ }
78
+ // Deliver at most once: a refresh or a duplicate redirect must not race a
79
+ // second exchange against a state row that is already consumed.
80
+ if (settled)
81
+ return;
82
+ settled = true;
83
+ const deliver = onCallback;
84
+ onCallback = null;
85
+ if (deliver)
86
+ deliver({ code, state, error });
87
+ else
88
+ buffered = { code, state, error };
89
+ });
90
+ await new Promise((resolve, reject) => {
91
+ server.once("error", reject);
92
+ server.listen(0, "127.0.0.1", () => {
93
+ server.removeListener("error", reject);
94
+ resolve();
95
+ });
96
+ });
97
+ const address = server.address();
98
+ if (!address || typeof address !== "object") {
99
+ server.close();
100
+ throw new IntegrationsCliError("Could not open a local port to finish sign-in. Retry, or use --no-browser to connect from the console.", { expected: true });
101
+ }
102
+ const redirectUri = `http://127.0.0.1:${address.port}${LOOPBACK_CALLBACK_PATH}`;
103
+ let timer = null;
104
+ const close = () => {
105
+ if (timer)
106
+ clearTimeout(timer);
107
+ timer = null;
108
+ onCallback = null;
109
+ server.close();
110
+ // The socket keeps the event loop alive; hq-cli exits by draining it.
111
+ server.unref();
112
+ };
113
+ return {
114
+ redirectUri,
115
+ close,
116
+ waitForCode(expectedState) {
117
+ return new Promise((resolve, reject) => {
118
+ const deliver = (result) => {
119
+ if (timer)
120
+ clearTimeout(timer);
121
+ timer = null;
122
+ const { code, state, error } = result;
123
+ if (error) {
124
+ reject(new IntegrationsCliError(`Sign-in was declined (${error}).`, { expected: true }));
125
+ return;
126
+ }
127
+ if (!code || !state) {
128
+ reject(new IntegrationsCliError("The provider redirected back without an authorization code. Try connecting again.", { expected: true }));
129
+ return;
130
+ }
131
+ // The state hq-pro minted is the anti-CSRF binding. A mismatch means
132
+ // this callback belongs to a different sign-in (or was injected), so
133
+ // refuse rather than forwarding someone else's code for exchange.
134
+ if (state !== expectedState) {
135
+ reject(new IntegrationsCliError("The sign-in response did not match the request. Nothing was connected — try again.", { expected: true }));
136
+ return;
137
+ }
138
+ resolve(code);
139
+ };
140
+ // The redirect may already have landed while the browser was opening.
141
+ if (buffered) {
142
+ const result = buffered;
143
+ buffered = null;
144
+ deliver(result);
145
+ return;
146
+ }
147
+ timer = setTimeout(() => {
148
+ close();
149
+ reject(new IntegrationsCliError("Timed out waiting for the browser sign-in. Run the connect command again.", { expected: true }));
150
+ }, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
151
+ // Don't let a pending sign-in wait keep an otherwise-finished process
152
+ // alive: the explicit timeout above is the bound, not the event loop.
153
+ timer.unref?.();
154
+ onCallback = deliver;
155
+ });
156
+ },
157
+ };
158
+ }
159
+ //# sourceMappingURL=integrations-oauth.js.map
@@ -1,21 +1,34 @@
1
1
  /**
2
- * `hq integrations` subcommand group — use company-connected apps (the
3
- * integration factory's governed gateway) from any HQ-authenticated session.
2
+ * `hq integrations` subcommand group — the full lifecycle of a company's
3
+ * connected apps, from any HQ-authenticated session.
4
4
  *
5
- * The console connects apps (e.g. Linear via OAuth sign-in); cloud agents get
6
- * their tools natively through hq-pro's integration MCP gateway
7
- * (POST /v1/integrations/mcp). This command gives LOCAL sessions (Claude
8
- * Code / Codex chats, scripts) the same first-class path no hand-crafted
9
- * authenticated curl calls.
5
+ * Cloud agents get connected apps natively through hq-pro's integration MCP
6
+ * gateway (POST /v1/integrations/mcp). This command group gives LOCAL sessions
7
+ * (Claude Code / Codex chats, scripts) the same first-class path no
8
+ * hand-crafted authenticated curl calls, and no bouncing to the console for
9
+ * things a terminal can do.
10
10
  *
11
- * Subcommands:
11
+ * Find and add:
12
+ * hq integrations catalog [query] Apps you can connect.
13
+ * hq integrations inspect <app> What an app exposes.
14
+ * hq integrations discover <docsUrl> Find a server from its docs page.
15
+ * hq integrations connect <app> Connect it (no-auth, key, OAuth).
16
+ * hq integrations reconnect [app] Re-authenticate a broken app.
17
+ *
18
+ * Use:
12
19
  * hq integrations list Connected apps for the company.
20
+ * hq integrations show [app] One app in full.
13
21
  * hq integrations tools --provider <p> What the connected app can do.
14
22
  * hq integrations call <tool> --provider <p> --args '<json>'
15
- * Invoke one of the app's tools.
16
- * hq integrations approve|reject <queueId> --provider <p>
17
- * Decide a queued (approval-gated)
18
- * call; approve executes it.
23
+ * hq integrations pending Calls awaiting approval.
24
+ * hq integrations approve|reject <queueId> Decide a queued call.
25
+ *
26
+ * Govern and remove:
27
+ * hq integrations policy [app] --set <m> Approval setting for changes.
28
+ * hq integrations grants|grant|ungrant Per-tool approval exceptions.
29
+ * hq integrations access|share|unshare Who may use the app.
30
+ * hq integrations audit Recent activity.
31
+ * hq integrations disconnect [app] Remove it and its credentials.
19
32
  *
20
33
  * Governance: reads flow freely; calls that can change the app are subject to
21
34
  * the connection's write policy (default: a person approves first). A queued
@@ -24,64 +37,14 @@
24
37
  * Auth: Cognito ID token via the shared session cache (`hq auth refresh`
25
38
  * semantics); company resolves like every other command — `--company <slug>`
26
39
  * or the caller's single active membership. Never hardcoded.
40
+ *
41
+ * Shared primitives (error taxonomy, HTTP guards, connection resolution) live
42
+ * in `integrations-core.ts` and are re-exported below so existing importers
43
+ * keep working; the verb implementations live in `integrations-connect.ts` and
44
+ * `integrations-manage.ts`.
27
45
  */
28
46
  import { Command } from "commander";
29
- interface AdminConnection {
30
- id: string;
31
- provider: string;
32
- status: string;
33
- writePolicy?: string;
34
- installation?: {
35
- displayName?: string;
36
- domain?: string;
37
- status?: string;
38
- } | null;
39
- }
40
- interface GatewayMessage {
41
- result?: unknown;
42
- error?: {
43
- code?: number;
44
- message?: string;
45
- };
46
- }
47
- export declare class IntegrationsCliError extends Error {
48
- /**
49
- * True when the error is the caller's request/state/permission (a client 4xx
50
- * or a local input/usage error) rather than an hq-cli defect. Expected errors
51
- * are printed to the user but skipped for Sentry capture (HQ-CLI-6). Defaults
52
- * to false so an unclassified error still reaches Sentry.
53
- */
54
- readonly expected: boolean;
55
- constructor(message: string, opts?: {
56
- expected?: boolean;
57
- });
58
- }
59
- /** "factory:linear" → "linear"; mirrors hq-pro's factoryToolPrefix. */
60
- export declare function toolPrefixForProvider(provider: string): string;
61
- export declare function fetchConnections(token: string, companyUid: string): Promise<AdminConnection[]>;
62
- /**
63
- * Resolve one connection by `--connection acct_…` or `--provider linear`
64
- * (matches `factory:<slug>` and bare provider ids, case-insensitive). Errors
65
- * list what IS connected so the fix is one command away.
66
- */
67
- export declare function selectConnection(connections: AdminConnection[], opts: {
68
- connection?: string;
69
- provider?: string;
70
- }): AdminConnection;
71
- export declare function callGateway(token: string, params: Record<string, unknown>): Promise<GatewayMessage>;
72
- /**
73
- * Gateway results arrive MCP-style: `{ content: [{ type: "text", text }] }`
74
- * where `text` is the provider's JSON. Unwrap to the inner payload; fall back
75
- * to the raw result when the shape differs.
76
- */
77
- export declare function unwrapGatewayResult(result: unknown): unknown;
78
- interface QueuedOutcome {
79
- queuedForApproval: true;
80
- queueId: string;
81
- connectionId: string;
82
- expiresAt?: string;
83
- }
84
- export declare function queuedOutcome(payload: unknown): QueuedOutcome | null;
47
+ export { IntegrationsCliError, callGateway, fetchConnections, queuedOutcome, selectConnection, toolPrefixForProvider, unwrapGatewayResult, } from "./integrations-core.js";
48
+ export type { AdminConnection } from "./integrations-core.js";
85
49
  export declare function registerIntegrationsCommand(program: Command): void;
86
- export {};
87
50
  //# sourceMappingURL=integrations.d.ts.map
@@ -1,21 +1,34 @@
1
1
  /**
2
- * `hq integrations` subcommand group — use company-connected apps (the
3
- * integration factory's governed gateway) from any HQ-authenticated session.
2
+ * `hq integrations` subcommand group — the full lifecycle of a company's
3
+ * connected apps, from any HQ-authenticated session.
4
4
  *
5
- * The console connects apps (e.g. Linear via OAuth sign-in); cloud agents get
6
- * their tools natively through hq-pro's integration MCP gateway
7
- * (POST /v1/integrations/mcp). This command gives LOCAL sessions (Claude
8
- * Code / Codex chats, scripts) the same first-class path no hand-crafted
9
- * authenticated curl calls.
5
+ * Cloud agents get connected apps natively through hq-pro's integration MCP
6
+ * gateway (POST /v1/integrations/mcp). This command group gives LOCAL sessions
7
+ * (Claude Code / Codex chats, scripts) the same first-class path no
8
+ * hand-crafted authenticated curl calls, and no bouncing to the console for
9
+ * things a terminal can do.
10
10
  *
11
- * Subcommands:
11
+ * Find and add:
12
+ * hq integrations catalog [query] Apps you can connect.
13
+ * hq integrations inspect <app> What an app exposes.
14
+ * hq integrations discover <docsUrl> Find a server from its docs page.
15
+ * hq integrations connect <app> Connect it (no-auth, key, OAuth).
16
+ * hq integrations reconnect [app] Re-authenticate a broken app.
17
+ *
18
+ * Use:
12
19
  * hq integrations list Connected apps for the company.
20
+ * hq integrations show [app] One app in full.
13
21
  * hq integrations tools --provider <p> What the connected app can do.
14
22
  * hq integrations call <tool> --provider <p> --args '<json>'
15
- * Invoke one of the app's tools.
16
- * hq integrations approve|reject <queueId> --provider <p>
17
- * Decide a queued (approval-gated)
18
- * call; approve executes it.
23
+ * hq integrations pending Calls awaiting approval.
24
+ * hq integrations approve|reject <queueId> Decide a queued call.
25
+ *
26
+ * Govern and remove:
27
+ * hq integrations policy [app] --set <m> Approval setting for changes.
28
+ * hq integrations grants|grant|ungrant Per-tool approval exceptions.
29
+ * hq integrations access|share|unshare Who may use the app.
30
+ * hq integrations audit Recent activity.
31
+ * hq integrations disconnect [app] Remove it and its credentials.
19
32
  *
20
33
  * Governance: reads flow freely; calls that can change the app are subject to
21
34
  * the connection's write policy (default: a person approves first). A queued
@@ -24,259 +37,24 @@
24
37
  * Auth: Cognito ID token via the shared session cache (`hq auth refresh`
25
38
  * semantics); company resolves like every other command — `--company <slug>`
26
39
  * or the caller's single active membership. Never hardcoded.
40
+ *
41
+ * Shared primitives (error taxonomy, HTTP guards, connection resolution) live
42
+ * in `integrations-core.ts` and are re-exported below so existing importers
43
+ * keep working; the verb implementations live in `integrations-connect.ts` and
44
+ * `integrations-manage.ts`.
27
45
  */
28
46
  import { randomUUID } from "node:crypto";
29
47
  import chalk from "chalk";
30
48
  import { ensureCognitoIdToken } from "../utils/cognito-session.js";
31
49
  import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
32
- import { AuthError } from "../utils/auth-error.js";
33
- import { redactErrorText } from "../utils/redact-error-text.js";
34
- export class IntegrationsCliError extends Error {
35
- /**
36
- * True when the error is the caller's request/state/permission (a client 4xx
37
- * or a local input/usage error) rather than an hq-cli defect. Expected errors
38
- * are printed to the user but skipped for Sentry capture (HQ-CLI-6). Defaults
39
- * to false so an unclassified error still reaches Sentry.
40
- */
41
- expected;
42
- constructor(message, opts = {}) {
43
- super(message);
44
- this.name = "IntegrationsCliError";
45
- this.expected = opts.expected ?? false;
46
- }
47
- }
48
- /**
49
- * A client 4xx is the caller's request/state/permission (bad params, stale
50
- * queueId, a non-owner approving) — expected and user-facing, not a bug. A 5xx
51
- * (or a 2xx protocol violation) is a genuine server/unknown fault worth a Sentry
52
- * crash report.
53
- */
54
- function isClientError(status) {
55
- return status >= 400 && status < 500;
56
- }
57
- /**
58
- * Statuses that mean HQ's integration gateway (or the third-party provider
59
- * behind it) could not serve this request right now — an UPSTREAM AVAILABILITY
60
- * event, not an hq-cli defect and not something the caller did wrong.
61
- *
62
- * HQ-CLI-F: the 500/503s in Sentry correlate request-for-request with the
63
- * hq-pro `IntegrationMcpFunction` Lambda hitting its 30s timeout while waiting
64
- * on a provider (CloudWatch `integration_mcp_audit event=provider_error`, and
65
- * the same spikes counted in the AWS/Lambda `Errors` metric). The event is
66
- * already recorded first-party, in the project that owns the fix; mirroring it
67
- * into hq-cli's tracker is duplicate, unactionable noise. 429 is included
68
- * because a rate-limited call is the same "retry in a moment" outcome (it was
69
- * already `expected` via `isClientError`; only its wording changes here).
70
- */
71
- function isUpstreamUnavailable(status) {
72
- return status === 429 || status === 500 || status === 502 || status === 503 || status === 504;
73
- }
74
- /** Actionable wording for an upstream-availability status. */
75
- function upstreamUnavailableMessage(status) {
76
- return status === 429
77
- ? `HQ's integration gateway is rate-limiting this request (HTTP 429). Wait a moment and retry.`
78
- : `HQ's integration gateway is temporarily unavailable (HTTP ${status}). ` +
79
- `This is a service-side hiccup, not a problem with your command — retry in a moment.`;
80
- }
81
- /**
82
- * Shared non-2xx guard for every integration-gateway call site. Raises the
83
- * expected, actionable upstream-availability error when the status says the
84
- * service is down or throttling; returns otherwise so the caller keeps its own
85
- * status-specific message and `expected` classification unchanged.
86
- */
87
- function raiseIfUpstreamUnavailable(res) {
88
- if (isUpstreamUnavailable(res.status)) {
89
- throw new IntegrationsCliError(upstreamUnavailableMessage(res.status), {
90
- expected: true,
91
- });
92
- }
93
- }
94
- // The integration gateway answers `POST /v1/integrations/mcp` JSON-RPC-style: a
95
- // transport failure is a non-2xx HTTP status, but a GOVERNED refusal arrives as
96
- // HTTP 200 carrying a JSON-RPC `error` object (mirrors hq-pro's
97
- // integration-mcp/server.ts error mapping). These caller-side codes are the
98
- // JSON-RPC analog of a client 4xx — the caller's request/state/permission,
99
- // expected and actionable, not an hq-cli defect — so they are printed to the
100
- // user and skipped for Sentry capture (HQ-CLI-B):
101
- // -32003 UNAUTHORIZED — connection-level access denial, e.g. the
102
- // "You do not have access to this integration. Ask its
103
- // owner to share it with you." (IntegrationAccessDenied)
104
- // that flooded Sentry, plus ConnectionNotFound / a
105
- // read-only share rejecting a write.
106
- // -32602 INVALID_PARAMS — an unknown tool or unsupported provider for the
107
- // connection (a bad request the caller can correct).
108
- // -32050 PROVIDER_ERROR — a THIRD-PARTY provider fault, surfaced verbatim.
109
- // hq-pro mints this code for EVERY provider fault
110
- // and for nothing else: `integration-mcp/server.ts`
111
- // maps an `IntegrationMcpError` with status
112
- // 'provider_error' to -32050, raised by
113
- // `integration-mcp/dispatch.ts` for ProviderTimeout,
114
- // ProviderRateLimited, ProviderParseError,
115
- // ProviderWriteUnknown and TokenRefreshFailed.
116
- // NOTE the remote server's OWN JSON-RPC code is
117
- // embedded in the message TEXT ("Remote MCP request
118
- // failed (-32602): …"); the wire code hq-cli sees is
119
- // always -32050, so the codes above never match it.
120
- // Every occurrence is already recorded first-party
121
- // in hq-pro — `integration_mcp_audit
122
- // event=provider_error` with reason/provider/tool,
123
- // an `integration_mcp_health_signal` metric, and
124
- // hq-pro's own Sentry project — so hq-cli reporting
125
- // it again is duplicate noise in the wrong tracker,
126
- // filed against a codebase that cannot fix it
127
- // (HQ-CLI-F). Should hq-pro ever reuse -32050 for
128
- // an hq-pro-side fault, the mapping site named above
129
- // is where that change is traceable; -32603 below
130
- // remains the code for hq-pro's own faults.
131
- // Everything else stays unexpected so a genuine fault still reaches Sentry:
132
- // INTERNAL_ERROR (-32603), CONFLICT (-32009, which the gateway also raises for
133
- // a confirm queue being unavailable or an owner notification failing — real
134
- // backend faults worth a report), METHOD_NOT_FOUND / PARSE_ERROR, and any
135
- // absent or unrecognized code.
136
- const EXPECTED_GATEWAY_ERROR_CODES = new Set([-32003, -32602, -32050]);
137
- function isExpectedGatewayError(code) {
138
- return code != null && EXPECTED_GATEWAY_ERROR_CODES.has(code);
139
- }
140
- // A 401 from ANY integration-gateway vault call means the caller's HQ session
141
- // is expired or missing — an expected auth state fixed by `hq login`, not an
142
- // hq-cli defect. Raise the same typed AuthError the vault company-resolution
143
- // paths use (HQ-CLI-8) so the top-level handler prints one actionable message
144
- // and skips Sentry, instead of surfacing the opaque, unactionable
145
- // "Integration gateway request failed (HTTP 401)" that shipped as a fatal from
146
- // `callGateway` (HQ-CLI-9). Non-401 statuses keep their existing behavior:
147
- // other 4xx stay expected client errors, 5xx still report.
148
- function raiseIfUnauthorized(res) {
149
- if (res.status === 401)
150
- throw new AuthError();
151
- }
152
- /** "factory:linear" → "linear"; mirrors hq-pro's factoryToolPrefix. */
153
- export function toolPrefixForProvider(provider) {
154
- return provider
155
- .replace(/^factory:/, "")
156
- .trim()
157
- .toLowerCase()
158
- .replace(/[^a-z0-9]+/g, ".");
159
- }
160
- export async function fetchConnections(token, companyUid) {
161
- const res = await vaultApiFetch({
162
- token,
163
- path: "/v1/integrations/admin",
164
- query: { companyUid },
165
- });
166
- if (!res.ok) {
167
- raiseIfUnauthorized(res);
168
- raiseIfUpstreamUnavailable(res);
169
- const body = (await res.json().catch(() => ({})));
170
- throw new IntegrationsCliError(body.error ?? `Failed to list integrations (HTTP ${res.status})`, { expected: isClientError(res.status) });
171
- }
172
- const data = (await res.json());
173
- return data.connections ?? [];
174
- }
175
- /**
176
- * Resolve one connection by `--connection acct_…` or `--provider linear`
177
- * (matches `factory:<slug>` and bare provider ids, case-insensitive). Errors
178
- * list what IS connected so the fix is one command away.
179
- */
180
- export function selectConnection(connections, opts) {
181
- const active = connections.filter((c) => c.status !== "revoked");
182
- if (opts.connection) {
183
- const match = connections.find((c) => c.id === opts.connection);
184
- if (!match) {
185
- throw new IntegrationsCliError(`No connection '${opts.connection}'. Run \`hq integrations list\` to see connected apps.`, { expected: true });
186
- }
187
- return match;
188
- }
189
- if (opts.provider) {
190
- const want = opts.provider.trim().toLowerCase();
191
- const match = active.find((c) => {
192
- const bare = c.provider.replace(/^factory:/, "").toLowerCase();
193
- return bare === want || c.provider.toLowerCase() === want;
194
- });
195
- if (!match) {
196
- const available = active
197
- .map((c) => c.provider.replace(/^factory:/, ""))
198
- .join(", ");
199
- throw new IntegrationsCliError(`No connected app matches '${opts.provider}'.` +
200
- (available ? ` Connected: ${available}.` : " Nothing is connected yet — connect apps on the console Integrations page."), { expected: true });
201
- }
202
- return match;
203
- }
204
- if (active.length === 1)
205
- return active[0];
206
- if (active.length === 0) {
207
- throw new IntegrationsCliError("No connected apps yet. Connect one on the console Integrations page, then retry.", { expected: true });
208
- }
209
- throw new IntegrationsCliError(`Multiple apps are connected — pick one with --provider:\n` +
210
- active.map((c) => ` --provider ${c.provider.replace(/^factory:/, "")}`).join("\n"), { expected: true });
211
- }
212
- export async function callGateway(token, params) {
213
- const res = await vaultApiFetch({
214
- token,
215
- path: "/v1/integrations/mcp",
216
- method: "POST",
217
- body: {
218
- jsonrpc: "2.0",
219
- id: `hq-cli-${randomUUID()}`,
220
- method: "tools/call",
221
- params,
222
- },
223
- });
224
- const message = (await res.json().catch(() => null));
225
- if (!res.ok || !message) {
226
- raiseIfUnauthorized(res);
227
- raiseIfUpstreamUnavailable(res);
228
- throw new IntegrationsCliError(`Integration gateway request failed (HTTP ${res.status}).`, { expected: isClientError(res.status) });
229
- }
230
- if (message.error) {
231
- // The gateway's error text is minted UPSTREAM (hq-pro, the integration
232
- // factory, and beyond it the third-party provider), so it is untrusted:
233
- // scrub credentials and bound the length here, at the throw site, because
234
- // an `expected` error is printed straight from `err.message` by the
235
- // top-level handler and never passes through `unexpectedCliErrorMessage`.
236
- // The chain is idempotent, so the unexpected path scrubbing again is a
237
- // no-op. This preserves PR #298's user-visible diagnostic; it only makes
238
- // it safe on the newly-expected path.
239
- throw new IntegrationsCliError(redactErrorText(message.error.message ?? "") ||
240
- "Integration gateway returned an error.", { expected: isExpectedGatewayError(message.error.code) });
241
- }
242
- return message;
243
- }
244
- /**
245
- * Gateway results arrive MCP-style: `{ content: [{ type: "text", text }] }`
246
- * where `text` is the provider's JSON. Unwrap to the inner payload; fall back
247
- * to the raw result when the shape differs.
248
- */
249
- export function unwrapGatewayResult(result) {
250
- if (result && typeof result === "object" && Array.isArray(result.content)) {
251
- const content = result.content;
252
- const text = content.find((c) => c.type === "text")?.text;
253
- if (typeof text === "string") {
254
- try {
255
- return JSON.parse(text);
256
- }
257
- catch {
258
- return text;
259
- }
260
- }
261
- }
262
- return result;
263
- }
264
- export function queuedOutcome(payload) {
265
- if (payload &&
266
- typeof payload === "object" &&
267
- payload.queuedForApproval === true &&
268
- typeof payload.queueId === "string") {
269
- return payload;
270
- }
271
- return null;
272
- }
273
- function printJson(value) {
274
- console.log(JSON.stringify(value, null, 2));
275
- }
50
+ import { IntegrationsCliError, bareProvider, callGateway, fetchConnections, isClientError, printJson, raiseIfUnauthorized, raiseIfUpstreamUnavailable, selectConnection, toolPrefixForProvider, unwrapGatewayResult, queuedOutcome, } from "./integrations-core.js";
51
+ import { registerConnectCommands } from "./integrations-connect.js";
52
+ import { registerManageCommands } from "./integrations-manage.js";
53
+ export { IntegrationsCliError, callGateway, fetchConnections, queuedOutcome, selectConnection, toolPrefixForProvider, unwrapGatewayResult, } from "./integrations-core.js";
276
54
  export function registerIntegrationsCommand(program) {
277
55
  const integrations = program
278
56
  .command("integrations")
279
- .description("Use company-connected apps (Linear, Notion, …) through HQ's governed integration gateway");
57
+ .description("Connect, govern, and use company apps (Linear, Notion, …) through HQ's governed integration gateway");
280
58
  integrations
281
59
  .command("list")
282
60
  .description("List the company's connected apps")
@@ -291,11 +69,11 @@ export function registerIntegrationsCommand(program) {
291
69
  return;
292
70
  }
293
71
  if (connections.length === 0) {
294
- console.log("No apps connected yet. Connect one on the console Integrations page.");
72
+ console.log("No apps connected yet. Browse them with `hq integrations catalog`, then connect one with `hq integrations connect <app>`.");
295
73
  return;
296
74
  }
297
75
  for (const c of connections) {
298
- const name = c.installation?.displayName ?? c.provider.replace(/^factory:/, "");
76
+ const name = c.installation?.displayName ?? bareProvider(c.provider);
299
77
  const flags = [
300
78
  c.status,
301
79
  c.writePolicy ? `writes: ${c.writePolicy}` : null,
@@ -303,7 +81,7 @@ export function registerIntegrationsCommand(program) {
303
81
  ]
304
82
  .filter(Boolean)
305
83
  .join(" · ");
306
- console.log(`${chalk.bold(name)} (${c.provider.replace(/^factory:/, "")}) ${chalk.dim(flags)}`);
84
+ console.log(`${chalk.bold(name)} (${bareProvider(c.provider)}) ${chalk.dim(flags)}`);
307
85
  console.log(chalk.dim(` connection: ${c.id}`));
308
86
  }
309
87
  });
@@ -338,7 +116,7 @@ export function registerIntegrationsCommand(program) {
338
116
  const label = tool.title && tool.title !== tool.name ? ` ${chalk.dim(tool.title)}` : "";
339
117
  console.log(`${chalk.bold(tool.name)}${label}`);
340
118
  }
341
- console.log(chalk.dim(`\n${tools.length} tools. Call one with: hq integrations call <tool> --provider ${connection.provider.replace(/^factory:/, "")} --args '<json>'`));
119
+ console.log(chalk.dim(`\n${tools.length} tools. Call one with: hq integrations call <tool> --provider ${bareProvider(connection.provider)} --args '<json>'`));
342
120
  });
343
121
  integrations
344
122
  .command("call <tool>")
@@ -385,7 +163,7 @@ export function registerIntegrationsCommand(program) {
385
163
  return;
386
164
  }
387
165
  console.log(chalk.yellow("Queued for approval — this call can change the app, so a company owner decides first."));
388
- console.log(`Approve with:\n hq integrations approve ${queued.queueId} --provider ${connection.provider.replace(/^factory:/, "")}`);
166
+ console.log(`Approve with:\n hq integrations approve ${queued.queueId} --provider ${bareProvider(connection.provider)}`);
389
167
  if (queued.expiresAt) {
390
168
  console.log(chalk.dim(`Expires ${queued.expiresAt}`));
391
169
  }
@@ -431,5 +209,7 @@ export function registerIntegrationsCommand(program) {
431
209
  }
432
210
  });
433
211
  }
212
+ registerConnectCommands(integrations);
213
+ registerManageCommands(integrations);
434
214
  }
435
215
  //# sourceMappingURL=integrations.js.map
@@ -229,7 +229,7 @@ export function registerReindexCommand(program) {
229
229
  program
230
230
  .command('reindex')
231
231
  .alias('master-sync')
232
- .description('Surface namespaced skills, regenerate the workers registry, and trust HQ hooks for Codex and Grok')
232
+ .description('Surface namespaced skills, materialize legacy knowledge repos, regenerate the workers registry, and trust HQ hooks for Codex and Grok')
233
233
  .option('--repo-root <path>', 'HQ root to operate on (defaults to the current directory)')
234
234
  .option('--from-hook', 'Invoked from a Claude/Codex lifecycle hook: never wait on the per-root operation lock — if a sync/rescue holds it, skip this reindex instead of blocking the session (equivalent to --lock-timeout 0)')
235
235
  .option('--lock-timeout <seconds>', 'Bound the wait for the per-root operation lock (seconds). 0 = refuse immediately; omitted = wait indefinitely (the interactive default). --from-hook implies 0; an explicit --lock-timeout wins.')