@indigoai-us/hq-cli 5.105.1 → 5.106.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.
- package/CHANGELOG.md +25 -0
- package/dist/commands/agents.d.ts +1 -1
- package/dist/commands/agents.js +2 -2
- package/dist/commands/integrations-api.d.ts +26 -0
- package/dist/commands/integrations-connect.d.ts +20 -0
- package/dist/commands/integrations-connect.js +349 -17
- package/dist/commands/integrations-core.d.ts +11 -0
- package/dist/commands/integrations-core.js +33 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.106.0] — 2026-09-02
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- `hq integrations inspect` now accepts a pasted endpoint URL. Passing
|
|
10
|
+
`https://mcp.example.com/mcp` used to be treated as free-text search and
|
|
11
|
+
answered "No integrations.sh results found" for an app listed under exactly
|
|
12
|
+
that host; it is now looked up by host.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- The Connect-an-app catalog now says when a listed app's availability could
|
|
17
|
+
not be re-confirmed on the last sweep. Such apps are tagged
|
|
18
|
+
`not recently verified` so a stale entry reads as unconfirmed rather than as
|
|
19
|
+
freshly checked. The apps are still listed and still connectable.
|
|
20
|
+
|
|
21
|
+
## [5.105.2] — 2026-09-02
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- The bundled cloud sync engine now uses `@indigoai-us/hq-cloud` 6.16.3.
|
|
26
|
+
Session logs include hidden files, retain capture state on large first runs,
|
|
27
|
+
continue capturing after one bad file, and are saved only after capture
|
|
28
|
+
succeeds.
|
|
29
|
+
|
|
5
30
|
## [5.105.1] — 2026-08-31
|
|
6
31
|
|
|
7
32
|
### Fixed
|
|
@@ -133,7 +133,7 @@ export interface ProvisionAgentInput {
|
|
|
133
133
|
name: string;
|
|
134
134
|
slug: string;
|
|
135
135
|
codexAuthMode: "subscription" | "apiKey";
|
|
136
|
-
provider?: "codex" | "grok" | "claude";
|
|
136
|
+
provider?: "codex" | "grok" | "claude" | "agents-v2";
|
|
137
137
|
codexApiKey?: string;
|
|
138
138
|
idempotencyKey: string;
|
|
139
139
|
title?: string;
|
package/dist/commands/agents.js
CHANGED
|
@@ -45,7 +45,7 @@ export const VALID_EFFORTS = new Set([
|
|
|
45
45
|
/** Service-tier (speed) values hq-pro accepts on `runtime-config`. */
|
|
46
46
|
export const VALID_TIERS = new Set(["default", "priority"]);
|
|
47
47
|
/** Agent runtimes hq-pro accepts on `POST /v1/agents` (`AgentProvider`). */
|
|
48
|
-
export const VALID_PROVIDERS = new Set(["codex", "grok", "claude"]);
|
|
48
|
+
export const VALID_PROVIDERS = new Set(["codex", "grok", "claude", "agents-v2"]);
|
|
49
49
|
/** Auth modes hq-pro accepts on `POST /v1/agents` (`CodexAuthMode`). */
|
|
50
50
|
export const VALID_AUTH_MODES = new Set(["subscription", "apiKey"]);
|
|
51
51
|
/** Customer-facing agent size keys served by hq-pro's authoritative catalog. */
|
|
@@ -764,7 +764,7 @@ export function registerAgentsCommand(program) {
|
|
|
764
764
|
.description("Provision a new cloud agent (company-specific monthly price shown before creation)")
|
|
765
765
|
.option("--company <slug>", "Company slug (resolves to companyUid)")
|
|
766
766
|
.option("--slug <slug>", "Agent slug (defaults to a slug of <name>)")
|
|
767
|
-
.option("--provider <provider>", "Runtime: codex | grok | claude (default codex). claude is subscription-only")
|
|
767
|
+
.option("--provider <provider>", "Runtime: codex | grok | claude | agents-v2 (default codex). claude is subscription-only; agents-v2 boots its brain box (codex, or grok via a grok-* model) and receives the v2 runtime post-boot")
|
|
768
768
|
.option("--auth-mode <mode>", "Auth: subscription | apiKey (default subscription)", "subscription")
|
|
769
769
|
.option("--api-key-env <VAR>", "Env var holding the API key for --auth-mode apiKey (never pass the key as a flag)")
|
|
770
770
|
.option("--title <title>", "Org-chart job title")
|
|
@@ -22,6 +22,17 @@ export interface CatalogEntry {
|
|
|
22
22
|
*/
|
|
23
23
|
source?: "hq-discovered" | "integrations.sh" | "hq-recommended";
|
|
24
24
|
authClass?: "none" | "key" | "oauth";
|
|
25
|
+
/**
|
|
26
|
+
* Present when the app is listed on a CARRIED-FORWARD verdict: the latest
|
|
27
|
+
* catalog generation's probe answered HTTP 403, which establishes nothing,
|
|
28
|
+
* so the previous run's classification was kept rather than dropping the app
|
|
29
|
+
* on ambiguous evidence.
|
|
30
|
+
*
|
|
31
|
+
* The app is still offered — this qualifies the listing, it does not retract
|
|
32
|
+
* it. Absent (never `false`) on a freshly verified entry, so "no claim" and
|
|
33
|
+
* "claim is false" stay distinguishable.
|
|
34
|
+
*/
|
|
35
|
+
verificationCarriedForward?: true;
|
|
25
36
|
/** Opaque server-owned id; prefer it over echoing connection details back. */
|
|
26
37
|
entryId?: string;
|
|
27
38
|
}
|
|
@@ -186,6 +197,21 @@ export interface OAuthStartInput {
|
|
|
186
197
|
* omitting it pins the console callback instead.
|
|
187
198
|
*/
|
|
188
199
|
redirectUri?: string;
|
|
200
|
+
/**
|
|
201
|
+
* An OAuth app the admin registered with the provider themselves, for the
|
|
202
|
+
* providers that refuse to register HQ dynamically (they only issue
|
|
203
|
+
* confidential clients). hq-pro reads these ONLY on the direct `mcpUrl`
|
|
204
|
+
* path — a catalog, domain, or discovery-receipt connect keeps server-owned
|
|
205
|
+
* client resolution and drops them silently — so callers must pin an
|
|
206
|
+
* `mcpUrl` alongside them rather than hoping they were honoured.
|
|
207
|
+
*/
|
|
208
|
+
clientId?: string;
|
|
209
|
+
/**
|
|
210
|
+
* The registered app's secret, for a confidential client. Optional: a public
|
|
211
|
+
* client is a `clientId` on its own. hq-pro keeps it on the server-side
|
|
212
|
+
* state row and attaches it at code exchange; it is never logged.
|
|
213
|
+
*/
|
|
214
|
+
clientSecret?: string;
|
|
189
215
|
}
|
|
190
216
|
export declare function startOAuth(token: string, companyUid: string, input: OAuthStartInput): Promise<OAuthStartResult>;
|
|
191
217
|
export declare function completeOAuth(token: string, companyUid: string, input: {
|
|
@@ -26,5 +26,25 @@ import { Command } from "commander";
|
|
|
26
26
|
export declare function assertAuthMode(value: string | undefined): void;
|
|
27
27
|
/** Single-quote a value for a copy-pasteable shell command. */
|
|
28
28
|
export declare function shellQuote(value: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* The console that fronts the control plane this CLI is pointed at.
|
|
31
|
+
*
|
|
32
|
+
* A handoff must land on the console for the SAME backend, because that
|
|
33
|
+
* console starts its own OAuth flow against its own backend. Sending a staging
|
|
34
|
+
* session to the production console would connect the app in the wrong place —
|
|
35
|
+
* so when the control plane is overridden and its console cannot be derived,
|
|
36
|
+
* return undefined and say so rather than guess a host.
|
|
37
|
+
*
|
|
38
|
+
* `HQ_CONSOLE_URL` is the explicit override; otherwise the console is the
|
|
39
|
+
* control-plane host with its `hqapi.` prefix removed.
|
|
40
|
+
*/
|
|
41
|
+
export declare function consoleOrigin(vaultApiUrl?: string): string | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* The console's Integrations page for a company, the console root when the
|
|
44
|
+
* caller never named one (a single-membership session resolves the company
|
|
45
|
+
* server-side, so hq-cli holds a uid here, not a slug — and the console routes
|
|
46
|
+
* by slug), or undefined when the console for this control plane is unknown.
|
|
47
|
+
*/
|
|
48
|
+
export declare function consoleIntegrationsUrl(companySlug: string | undefined, origin: string | undefined): string | undefined;
|
|
29
49
|
export declare function registerConnectCommands(integrations: Command): void;
|
|
30
50
|
//# sourceMappingURL=integrations-connect.d.ts.map
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import chalk from "chalk";
|
|
20
20
|
import open from "open";
|
|
21
|
-
import { ensureCognitoIdToken } from "../utils/cognito-session.js";
|
|
21
|
+
import { DEFAULT_VAULT_API_URL, ensureCognitoIdToken, } from "../utils/cognito-session.js";
|
|
22
22
|
import { getCompanyUid } from "../utils/vault-api.js";
|
|
23
23
|
import { IntegrationsCliError, bareProvider, connectionDomain, printJson, revokedConnectionDetails, resolveConnection, } from "./integrations-core.js";
|
|
24
24
|
import { completeOAuth, discoverDocs, installIntegration, listCatalog, pullBlueprint, startOAuth, } from "./integrations-api.js";
|
|
@@ -61,6 +61,21 @@ const CONSOLE_HANDOFF_CODES = new Set([
|
|
|
61
61
|
"OAUTH_REDIRECT_URI_NOT_ALLOWED",
|
|
62
62
|
"OAUTH_LOOPBACK_NOT_SUPPORTED",
|
|
63
63
|
]);
|
|
64
|
+
/**
|
|
65
|
+
* Codes meaning "this provider will not register HQ automatically; an admin
|
|
66
|
+
* must supply their own OAuth app's credentials".
|
|
67
|
+
*
|
|
68
|
+
* hq-pro attaches `redirectUri` + `advancedClientSupported` to exactly these
|
|
69
|
+
* (`withAdvancedClientHint`), and the console turns that into its
|
|
70
|
+
* bring-your-own-credentials form. hq-cli has no flag for a client id or
|
|
71
|
+
* secret, so on its own it can only restate the refusal — which reads as a
|
|
72
|
+
* dead end even though the recovery exists one surface over. Route to the
|
|
73
|
+
* console instead.
|
|
74
|
+
*/
|
|
75
|
+
const BRING_YOUR_OWN_CLIENT_CODES = new Set([
|
|
76
|
+
"OAUTH_REGISTRATION_UNSUPPORTED",
|
|
77
|
+
"CLIENT_REGISTRATION_REFUSED",
|
|
78
|
+
]);
|
|
64
79
|
/**
|
|
65
80
|
* Looks-like-a-domain test for the positional `<app>` argument. Deliberately
|
|
66
81
|
* loose — hq-pro does the real resolution — but tight enough that `linear.app`
|
|
@@ -69,6 +84,27 @@ const CONSOLE_HANDOFF_CODES = new Set([
|
|
|
69
84
|
function looksLikeDomain(value) {
|
|
70
85
|
return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(value) && !value.includes(" ");
|
|
71
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* The host inside an endpoint URL, or the value unchanged when it is not one.
|
|
89
|
+
*
|
|
90
|
+
* `hq integrations inspect` is the "what does this expose before I connect it"
|
|
91
|
+
* command, so the natural thing to paste is the endpoint URL itself. Without
|
|
92
|
+
* this, `https://vyg.app/mcp` failed `looksLikeDomain` and went out as a free
|
|
93
|
+
* text SEARCH, which answered "No integrations.sh results found" for an app
|
|
94
|
+
* that is in the catalog under exactly that host. hq-pro accepts a URL-shaped
|
|
95
|
+
* domain, so the only thing missing was sending it as one.
|
|
96
|
+
*/
|
|
97
|
+
function hostFromEndpointUrl(value) {
|
|
98
|
+
const trimmed = value.trim();
|
|
99
|
+
if (!/^https?:\/\//i.test(trimmed))
|
|
100
|
+
return trimmed;
|
|
101
|
+
try {
|
|
102
|
+
return new URL(trimmed).hostname;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return trimmed;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
72
108
|
/**
|
|
73
109
|
* True when this shell is almost certainly on a different machine from the
|
|
74
110
|
* person's browser. Not a security boundary — just the signal that a loopback
|
|
@@ -325,6 +361,26 @@ async function findCatalogEntryByName(token, companyUid, name) {
|
|
|
325
361
|
return null;
|
|
326
362
|
}
|
|
327
363
|
}
|
|
364
|
+
/**
|
|
365
|
+
* Confirm the backend recognises this target, without connecting anything.
|
|
366
|
+
*
|
|
367
|
+
* `pullBlueprint` is the read-only half of the install: it takes the same
|
|
368
|
+
* domain / query / catalogEntryId handles and 404s an unknown one. It has no
|
|
369
|
+
* shape for the two remaining refs, and neither needs it — a discovery receipt
|
|
370
|
+
* was minted by `discoverDocs` moments earlier in `resolveTarget`, and an
|
|
371
|
+
* `--mcp-url` is an endpoint the caller typed rather than a handle the backend
|
|
372
|
+
* could confirm (the handoff prints it back so they can see what they gave).
|
|
373
|
+
*/
|
|
374
|
+
async function assertTargetResolvable(token, companyUid, target) {
|
|
375
|
+
const { domain, query, catalogEntryId } = target.ref;
|
|
376
|
+
if (!domain && !query && !catalogEntryId)
|
|
377
|
+
return;
|
|
378
|
+
await pullBlueprint(token, companyUid, {
|
|
379
|
+
...(domain ? { domain } : {}),
|
|
380
|
+
...(query ? { query } : {}),
|
|
381
|
+
...(catalogEntryId ? { catalogEntryId } : {}),
|
|
382
|
+
});
|
|
383
|
+
}
|
|
328
384
|
/**
|
|
329
385
|
* Run the browser sign-in and finish the install.
|
|
330
386
|
*
|
|
@@ -343,13 +399,26 @@ async function connectViaOAuth(token, companyUid, target, opts) {
|
|
|
343
399
|
});
|
|
344
400
|
}
|
|
345
401
|
const startInput = { ...target.ref };
|
|
402
|
+
// A user-registered app pins the console callback and skips the loopback
|
|
403
|
+
// entirely — see `userClientSignIn` for why an ephemeral port cannot be the
|
|
404
|
+
// redirect URI here.
|
|
405
|
+
const userClient = await resolveUserOAuthClient(opts, target.label);
|
|
406
|
+
if (userClient)
|
|
407
|
+
return await userClientSignIn(token, companyUid, target, opts, userClient);
|
|
346
408
|
// A loopback listener only works if the browser runs on THIS machine. Over
|
|
347
409
|
// SSH the person opens the printed URL on their workstation, so the provider
|
|
348
410
|
// redirects to the workstation's 127.0.0.1 while the listener sits on the
|
|
349
411
|
// remote host — the callback can never arrive and the command just times
|
|
350
412
|
// out. Go straight to the console handoff instead of failing slowly.
|
|
351
413
|
if (opts.browser === false && isRemoteShell()) {
|
|
352
|
-
|
|
414
|
+
// Every other route into the handoff has already put the target in front
|
|
415
|
+
// of the backend (`/oauth/start` rejects one it cannot resolve). This one
|
|
416
|
+
// returns before any request, so a target hq-pro would refuse — a stale
|
|
417
|
+
// `--entry-id`, an unmatched name that fell through to a raw query — would
|
|
418
|
+
// be answered with a confident "go finish it in the console" for an app
|
|
419
|
+
// that is not there. Ask the backend first.
|
|
420
|
+
await assertTargetResolvable(token, companyUid, target);
|
|
421
|
+
return await consoleHandoff(target, opts);
|
|
353
422
|
}
|
|
354
423
|
const listener = await startLoopbackListener(timeoutMs === undefined ? {} : { timeoutMs });
|
|
355
424
|
try {
|
|
@@ -361,13 +430,16 @@ async function connectViaOAuth(token, companyUid, target, opts) {
|
|
|
361
430
|
});
|
|
362
431
|
}
|
|
363
432
|
catch (err) {
|
|
364
|
-
if (!(err instanceof IntegrationsCliError) ||
|
|
365
|
-
!err.code ||
|
|
366
|
-
!CONSOLE_HANDOFF_CODES.has(err.code)) {
|
|
433
|
+
if (!(err instanceof IntegrationsCliError) || !err.code)
|
|
367
434
|
throw err;
|
|
435
|
+
if (BRING_YOUR_OWN_CLIENT_CODES.has(err.code)) {
|
|
436
|
+
listener.close();
|
|
437
|
+
return await bringYourOwnClientHandoff(target, opts, err.message, err.redirectUri);
|
|
368
438
|
}
|
|
439
|
+
if (!CONSOLE_HANDOFF_CODES.has(err.code))
|
|
440
|
+
throw err;
|
|
369
441
|
listener.close();
|
|
370
|
-
return await consoleHandoff(
|
|
442
|
+
return await consoleHandoff(target, opts);
|
|
371
443
|
}
|
|
372
444
|
// Arm the waiter BEFORE the browser opens. The listener buffers a redirect
|
|
373
445
|
// that beats it, but ordering it this way means the happy path never
|
|
@@ -391,20 +463,260 @@ async function connectViaOAuth(token, companyUid, target, opts) {
|
|
|
391
463
|
listener.close();
|
|
392
464
|
}
|
|
393
465
|
}
|
|
466
|
+
const PRODUCTION_CONSOLE_ORIGIN = "https://hq.computer";
|
|
467
|
+
const PRODUCTION_VAULT_API_HOST = "hqapi.hq.computer";
|
|
394
468
|
/**
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
469
|
+
* The console that fronts the control plane this CLI is pointed at.
|
|
470
|
+
*
|
|
471
|
+
* A handoff must land on the console for the SAME backend, because that
|
|
472
|
+
* console starts its own OAuth flow against its own backend. Sending a staging
|
|
473
|
+
* session to the production console would connect the app in the wrong place —
|
|
474
|
+
* so when the control plane is overridden and its console cannot be derived,
|
|
475
|
+
* return undefined and say so rather than guess a host.
|
|
476
|
+
*
|
|
477
|
+
* `HQ_CONSOLE_URL` is the explicit override; otherwise the console is the
|
|
478
|
+
* control-plane host with its `hqapi.` prefix removed.
|
|
399
479
|
*/
|
|
400
|
-
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
480
|
+
export function consoleOrigin(vaultApiUrl = DEFAULT_VAULT_API_URL) {
|
|
481
|
+
const explicit = process.env.HQ_CONSOLE_URL?.trim();
|
|
482
|
+
if (explicit)
|
|
483
|
+
return explicit.replace(/\/+$/, "");
|
|
484
|
+
let api;
|
|
485
|
+
try {
|
|
486
|
+
api = new URL(vaultApiUrl);
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
return undefined;
|
|
490
|
+
}
|
|
491
|
+
if (api.host === PRODUCTION_VAULT_API_HOST)
|
|
492
|
+
return PRODUCTION_CONSOLE_ORIGIN;
|
|
493
|
+
if (!api.hostname.startsWith("hqapi."))
|
|
494
|
+
return undefined;
|
|
495
|
+
return `${api.protocol}//${api.host.slice("hqapi.".length)}`;
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* The console's Integrations page for a company, the console root when the
|
|
499
|
+
* caller never named one (a single-membership session resolves the company
|
|
500
|
+
* server-side, so hq-cli holds a uid here, not a slug — and the console routes
|
|
501
|
+
* by slug), or undefined when the console for this control plane is unknown.
|
|
502
|
+
*/
|
|
503
|
+
export function consoleIntegrationsUrl(
|
|
504
|
+
// Required, deliberately: a default of `consoleOrigin()` would make an
|
|
505
|
+
// explicit `undefined` — exactly what an unresolvable control plane yields —
|
|
506
|
+
// silently fall back to the production console.
|
|
507
|
+
companySlug, origin) {
|
|
508
|
+
if (!origin)
|
|
509
|
+
return undefined;
|
|
510
|
+
const slug = companySlug?.trim();
|
|
511
|
+
// A `cmp_…` uid is not a slug; the console has no route for it.
|
|
512
|
+
if (!slug || slug.startsWith("cmp_"))
|
|
513
|
+
return origin;
|
|
514
|
+
return `${origin}/companies/${encodeURIComponent(slug)}/integrations`;
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* What the caller named, phrased for someone about to retype it in the console.
|
|
518
|
+
*
|
|
519
|
+
* A `--mcp-url` or `--docs-url` target has no catalog row to search for over
|
|
520
|
+
* there — `resolveTarget` reduces a docs page to an opaque discovery receipt
|
|
521
|
+
* the console cannot take — so unless the handoff carries the address back,
|
|
522
|
+
* the only copy of it is in the caller's scrollback. A catalog target needs no
|
|
523
|
+
* hint: its label IS what they type into the console's search box.
|
|
524
|
+
*/
|
|
525
|
+
function targetSelectionHint(opts) {
|
|
526
|
+
if (opts.mcpUrl)
|
|
527
|
+
return `Server address to enter there: ${opts.mcpUrl}`;
|
|
528
|
+
if (opts.docsUrl)
|
|
529
|
+
return `Found from these docs: ${opts.docsUrl}`;
|
|
530
|
+
if (opts.entryId)
|
|
531
|
+
return `Catalog entry: ${opts.entryId}`;
|
|
532
|
+
return undefined;
|
|
533
|
+
}
|
|
534
|
+
/** Shared tail of every console handoff: the page, then what to connect there. */
|
|
535
|
+
function printConsoleDestination(target, opts, url) {
|
|
536
|
+
if (url) {
|
|
537
|
+
console.error(`Open this page and connect ${target.label} from it:\n ${url}`);
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
console.error(`Open your HQ console's Integrations page and connect ${target.label} there. ` +
|
|
541
|
+
"(This session points at a control plane whose console address is not " +
|
|
542
|
+
"derivable — set HQ_CONSOLE_URL to name it.)");
|
|
543
|
+
}
|
|
544
|
+
const hint = targetSelectionHint(opts);
|
|
545
|
+
if (hint)
|
|
546
|
+
console.error(` ${hint}`);
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Fallback for a backend that will not accept a loopback callback: send the
|
|
550
|
+
* caller to the console and let it run the whole sign-in.
|
|
551
|
+
*
|
|
552
|
+
* This deliberately does NOT start an OAuth flow. It used to: it called
|
|
553
|
+
* `/oauth/start` against hq-pro's console callback and printed the vendor
|
|
554
|
+
* authorization URL. That URL could never complete. The console's callback
|
|
555
|
+
* route resolves which company to return to from a short-lived httpOnly cookie
|
|
556
|
+
* that only its own `startFactoryOAuthSignIn` server action sets, so a browser
|
|
557
|
+
* arriving from a CLI-minted URL carries no cookie, and the route — by design,
|
|
558
|
+
* rather than erroring — redirects to the console root. The user signed in
|
|
559
|
+
* successfully at the vendor every time and landed on the home page with no
|
|
560
|
+
* explanation, and the burned single-use state row made it look intermittent.
|
|
561
|
+
*
|
|
562
|
+
* Handing over the console page instead means the sign-in is started by the
|
|
563
|
+
* surface that can finish it. hq-cli still cannot report the outcome, so it
|
|
564
|
+
* says so rather than pretending.
|
|
565
|
+
*/
|
|
566
|
+
async function consoleHandoff(target, opts) {
|
|
567
|
+
const url = consoleIntegrationsUrl(opts.company, consoleOrigin());
|
|
568
|
+
console.error(chalk.yellow(`${target.label} signs in through the console, not the terminal — this HQ backend finishes these sign-ins there.`));
|
|
569
|
+
printConsoleDestination(target, opts, url);
|
|
570
|
+
if (url && opts.browser !== false)
|
|
571
|
+
await open(url).catch(() => { });
|
|
572
|
+
console.error(chalk.dim("When the browser says it connected, run `hq integrations list` to confirm."));
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* The provider refuses to register HQ automatically. That is recoverable — an
|
|
577
|
+
* admin registers their own OAuth app with the provider and pastes its
|
|
578
|
+
* credentials — but only the console can collect them, so say what is needed
|
|
579
|
+
* and hand the page over rather than restating the refusal and stopping.
|
|
580
|
+
*/
|
|
581
|
+
async function bringYourOwnClientHandoff(target, opts, reason, redirectUri) {
|
|
582
|
+
console.error(chalk.yellow(reason));
|
|
583
|
+
console.error(`${target.label} needs an OAuth app you register with the provider yourself.`);
|
|
584
|
+
// Prefer finishing here. The recovery needs two things the terminal can
|
|
585
|
+
// supply — the callback URL to register, and a client id + secret — and
|
|
586
|
+
// hq-pro sends the first one back on exactly these errors. Falling through
|
|
587
|
+
// to the console when either half is missing keeps the old behaviour rather
|
|
588
|
+
// than printing a half-command that cannot work.
|
|
589
|
+
const command = byoClientCommand(target, opts);
|
|
590
|
+
if (redirectUri && command) {
|
|
591
|
+
console.error("");
|
|
592
|
+
console.error(`In the provider's developer portal, register this callback URL:`);
|
|
593
|
+
console.error(` ${redirectUri}`);
|
|
594
|
+
console.error(`Then run:`);
|
|
595
|
+
console.error(` ${command}`);
|
|
596
|
+
console.error(chalk.dim("The secret is read from stdin, so it stays out of your shell history. " +
|
|
597
|
+
"Omit --client-secret-stdin for a public app that has no secret."));
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
const url = consoleIntegrationsUrl(opts.company, consoleOrigin());
|
|
601
|
+
console.error("The console shows the callback URL to register and takes the credentials.");
|
|
602
|
+
printConsoleDestination(target, opts, url);
|
|
603
|
+
if (url && opts.browser !== false)
|
|
604
|
+
await open(url).catch(() => { });
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* The copy-pasteable command that finishes a bring-your-own-app connect.
|
|
609
|
+
*
|
|
610
|
+
* Always `connect --mcp-url`, even when the caller reached here through
|
|
611
|
+
* `reconnect`: re-installing the same endpoint IS the reconnect, and the
|
|
612
|
+
* direct-endpoint path is the only one hq-pro reads user-supplied client
|
|
613
|
+
* credentials on. Null when the endpoint is unknown (a bare-name or domain
|
|
614
|
+
* connect that never resolved to a URL) — there is no honest command to print
|
|
615
|
+
* then, so the caller falls back to the console.
|
|
616
|
+
*/
|
|
617
|
+
function byoClientCommand(target, opts) {
|
|
618
|
+
const { mcpUrl, provider } = target.ref;
|
|
619
|
+
if (!mcpUrl)
|
|
620
|
+
return null;
|
|
621
|
+
const parts = ["hq integrations connect", `--mcp-url ${shellQuote(mcpUrl)}`];
|
|
622
|
+
if (opts.company)
|
|
623
|
+
parts.push(`--company ${shellQuote(opts.company)}`);
|
|
624
|
+
if (provider)
|
|
625
|
+
parts.push(`--provider ${shellQuote(provider)}`);
|
|
626
|
+
parts.push("--client-id <client-id-from-the-provider>", "--client-secret-stdin");
|
|
627
|
+
return parts.join(" ");
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Refuse a `--client-id` that hq-pro would silently ignore, before anything is
|
|
631
|
+
* installed.
|
|
632
|
+
*
|
|
633
|
+
* hq-pro reads user-supplied client credentials only on the direct `mcpUrl`
|
|
634
|
+
* path — on a catalog, domain, or discovery-receipt connect it nulls them out
|
|
635
|
+
* and resolves its own client instead. Passing them there would not fail: the
|
|
636
|
+
* connect would attempt dynamic registration exactly as before and come back
|
|
637
|
+
* with the same refusal, leaving the caller certain they had supplied
|
|
638
|
+
* credentials that were never read. Say so instead.
|
|
639
|
+
*/
|
|
640
|
+
function assertUserClientApplicable(opts, target, authMode) {
|
|
641
|
+
if (!opts.clientId && !opts.clientSecretStdin)
|
|
642
|
+
return;
|
|
643
|
+
if (!opts.clientId) {
|
|
644
|
+
throw new IntegrationsCliError("--client-secret-stdin needs --client-id — a secret identifies no app on its own.", { expected: true });
|
|
645
|
+
}
|
|
646
|
+
if (opts.token || opts.tokenStdin) {
|
|
647
|
+
throw new IntegrationsCliError("--client-id is for a browser sign-in and --token is for an API key; pass one or the other, not both.", { expected: true });
|
|
648
|
+
}
|
|
649
|
+
// Only an mode the caller or the catalog stated. An UNKNOWN mode is left
|
|
650
|
+
// alone on purpose: the direct-endpoint path routinely discovers it is
|
|
651
|
+
// OAuth-protected only when hq-pro says so, and rejecting here would block
|
|
652
|
+
// the exact providers this flag exists for.
|
|
653
|
+
if (authMode === "key" || authMode === "none") {
|
|
654
|
+
throw new IntegrationsCliError(`${target.label} does not use a browser sign-in, so --client-id does not apply to it.`, { expected: true });
|
|
655
|
+
}
|
|
656
|
+
if (!target.ref.mcpUrl) {
|
|
657
|
+
throw new IntegrationsCliError("Your own OAuth app can only be used with a specific endpoint. " +
|
|
658
|
+
"Re-run with --mcp-url <endpoint> (find it with `hq integrations inspect`), " +
|
|
659
|
+
"or connect it from the console.", { expected: true });
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Collect the user-registered OAuth app, if one was asked for.
|
|
664
|
+
*
|
|
665
|
+
* The secret has no argv flag by design. A client secret is a long-lived
|
|
666
|
+
* credential, and argv is visible in shell history and to `ps`; stdin covers
|
|
667
|
+
* the scripted case and a hidden prompt covers the interactive one, so there
|
|
668
|
+
* is no case left that an argv flag would serve.
|
|
669
|
+
*/
|
|
670
|
+
async function resolveUserOAuthClient(opts, appLabel) {
|
|
671
|
+
const clientId = opts.clientId?.trim();
|
|
672
|
+
if (!clientId)
|
|
673
|
+
return null;
|
|
674
|
+
if (opts.clientSecretStdin) {
|
|
675
|
+
const piped = (await readStdin()).trim();
|
|
676
|
+
if (!piped) {
|
|
677
|
+
throw new IntegrationsCliError("--client-secret-stdin was set but stdin was empty.", { expected: true });
|
|
678
|
+
}
|
|
679
|
+
return { clientId, clientSecret: piped };
|
|
680
|
+
}
|
|
681
|
+
if (process.stdin.isTTY) {
|
|
682
|
+
const value = (await promptForKey(`Client secret for ${appLabel} (leave blank if the app has none)`)).trim();
|
|
683
|
+
return value ? { clientId, clientSecret: value } : { clientId };
|
|
684
|
+
}
|
|
685
|
+
// Non-interactive and no secret piped: a public client is legitimate, so
|
|
686
|
+
// proceed with the id alone rather than inventing a blank secret.
|
|
687
|
+
return { clientId };
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Sign in with an OAuth app the admin registered themselves.
|
|
691
|
+
*
|
|
692
|
+
* No loopback listener, deliberately. The whole premise is that the person
|
|
693
|
+
* registered ONE redirect URI with the provider ahead of time, and an
|
|
694
|
+
* ephemeral loopback port can never be one of them — so this pins hq-pro's
|
|
695
|
+
* console callback by omitting `redirectUri`, and the sign-in completes on the
|
|
696
|
+
* console. hq-cli cannot observe that completion, so it says what it knows and
|
|
697
|
+
* points at the check, rather than reporting an install it never saw.
|
|
698
|
+
*
|
|
699
|
+
* This requires the console callback to be able to finish a flow it did not
|
|
700
|
+
* start. It could not until hq-pro/#2823 + hq-console/#783: the return company
|
|
701
|
+
* came from a cookie only the console's own start action set, so a CLI-minted
|
|
702
|
+
* URL arrived cookie-less and the route dropped the user on the console root
|
|
703
|
+
* with nothing connected — see `consoleHandoff` below, which exists because of
|
|
704
|
+
* exactly that. hq-pro now resolves the company from the state row when
|
|
705
|
+
* `companyUid` is omitted, so the callback completes and the console names the
|
|
706
|
+
* app it connected. Ship this only against a backend carrying both.
|
|
707
|
+
*/
|
|
708
|
+
async function userClientSignIn(token, companyUid, target, opts, client) {
|
|
709
|
+
const started = await startOAuth(token, companyUid, {
|
|
710
|
+
...target.ref,
|
|
711
|
+
clientId: client.clientId,
|
|
712
|
+
...(client.clientSecret ? { clientSecret: client.clientSecret } : {}),
|
|
713
|
+
});
|
|
714
|
+
console.error(`Open this URL to sign in to ${started.displayName || target.label}:`);
|
|
715
|
+
console.error(` ${started.authorizationUrl}`);
|
|
405
716
|
if (opts.browser !== false)
|
|
406
717
|
await open(started.authorizationUrl).catch(() => { });
|
|
407
|
-
console.error(chalk.dim("
|
|
718
|
+
console.error(chalk.dim("The browser finishes on the HQ console. When it says connected, " +
|
|
719
|
+
"run `hq integrations list` to confirm."));
|
|
408
720
|
return null;
|
|
409
721
|
}
|
|
410
722
|
/**
|
|
@@ -459,6 +771,7 @@ function reportInstall(result, opts, expectedRevivedConnectionId) {
|
|
|
459
771
|
async function connectApp(token, companyUid, app, opts, expectedRevivedConnectionId) {
|
|
460
772
|
const target = await resolveTarget(token, companyUid, app, opts, expectedRevivedConnectionId !== undefined);
|
|
461
773
|
const authMode = opts.auth ?? target.authClass;
|
|
774
|
+
assertUserClientApplicable(opts, target, authMode);
|
|
462
775
|
if (authMode === "oauth") {
|
|
463
776
|
const result = await connectViaOAuth(token, companyUid, target, opts);
|
|
464
777
|
if (result)
|
|
@@ -536,6 +849,11 @@ export function registerConnectCommands(integrations) {
|
|
|
536
849
|
entry.source === "hq-recommended" ? "recommended" : null,
|
|
537
850
|
entry.source === "hq-discovered" ? "community" : null,
|
|
538
851
|
entry.mcpReady ? null : "not one-click",
|
|
852
|
+
// The listing is a claim about connectability, and for these apps HQ
|
|
853
|
+
// could not re-confirm it: the last probe was inconclusive and the
|
|
854
|
+
// previous verdict was carried forward. Say so on the row rather
|
|
855
|
+
// than let it read the same as an app checked this morning.
|
|
856
|
+
entry.verificationCarriedForward ? "not recently verified" : null,
|
|
539
857
|
]
|
|
540
858
|
.filter(Boolean)
|
|
541
859
|
.join(" · ");
|
|
@@ -553,7 +871,10 @@ export function registerConnectCommands(integrations) {
|
|
|
553
871
|
.action(async (app, opts) => {
|
|
554
872
|
const token = await ensureCognitoIdToken();
|
|
555
873
|
const companyUid = await getCompanyUid(token, opts.company);
|
|
556
|
-
|
|
874
|
+
// A pasted endpoint URL names one host; look that host up rather than
|
|
875
|
+
// searching the catalog for the URL as free text.
|
|
876
|
+
const host = hostFromEndpointUrl(app);
|
|
877
|
+
const blueprint = await pullBlueprint(token, companyUid, looksLikeDomain(host) ? { domain: host } : { query: app });
|
|
557
878
|
if (opts.json) {
|
|
558
879
|
printJson(blueprint);
|
|
559
880
|
return;
|
|
@@ -609,6 +930,8 @@ export function registerConnectCommands(integrations) {
|
|
|
609
930
|
.option("--token <key>", "API key (prefer --token-stdin: --token lands in shell history)")
|
|
610
931
|
.option("--token-stdin", "Read the API key from stdin")
|
|
611
932
|
.option("--auth <mode>", "Force the auth mode: none, key, or oauth (default: detect)")
|
|
933
|
+
.option("--client-id <id>", "Client id of an OAuth app you registered with the provider yourself")
|
|
934
|
+
.option("--client-secret-stdin", "Read that app's client secret from stdin")
|
|
612
935
|
.option("--no-browser", "Print the sign-in URL instead of opening a browser")
|
|
613
936
|
.option("--timeout <seconds>", "How long to wait for a browser sign-in (default 300)")
|
|
614
937
|
.option("--json", "Machine-readable output")
|
|
@@ -627,6 +950,8 @@ export function registerConnectCommands(integrations) {
|
|
|
627
950
|
.option("--token <key>", "API key (prefer --token-stdin)")
|
|
628
951
|
.option("--token-stdin", "Read the API key from stdin")
|
|
629
952
|
.option("--auth <mode>", "Force the auth mode: none, key, or oauth (default: detect)")
|
|
953
|
+
.option("--client-id <id>", "Client id of an OAuth app you registered with the provider yourself")
|
|
954
|
+
.option("--client-secret-stdin", "Read that app's client secret from stdin")
|
|
630
955
|
.option("--no-browser", "Print the sign-in URL instead of opening a browser")
|
|
631
956
|
.option("--timeout <seconds>", "How long to wait for a browser sign-in (default 300)")
|
|
632
957
|
.option("--connect", "For a revoked row, run `connect <domain>` to re-add and revive it")
|
|
@@ -673,6 +998,13 @@ export function registerConnectCommands(integrations) {
|
|
|
673
998
|
...(opts.auth ? { authClass: opts.auth } : {}),
|
|
674
999
|
label: connection.installation?.displayName ?? bareProvider(connection.provider),
|
|
675
1000
|
};
|
|
1001
|
+
// Same gate `connectApp` applies. This path does not go through it —
|
|
1002
|
+
// it resolves the endpoint off the existing connection and branches
|
|
1003
|
+
// itself — so without this, flags the CLI promises to reject are
|
|
1004
|
+
// silently ignored instead: `--client-secret-stdin` with no id would
|
|
1005
|
+
// start an ordinary sign-in, and `--client-id` beside `--token` would
|
|
1006
|
+
// reinstall with the bearer token and drop the OAuth app.
|
|
1007
|
+
assertUserClientApplicable(opts, target, target.authClass);
|
|
676
1008
|
if (target.authClass === "oauth") {
|
|
677
1009
|
const result = await connectViaOAuth(token, companyUid, target, opts);
|
|
678
1010
|
if (result)
|
|
@@ -150,12 +150,23 @@ export declare class IntegrationsCliError extends Error {
|
|
|
150
150
|
* fingerprint as anything but the finite allowlist in sentry-fingerprint.ts.
|
|
151
151
|
*/
|
|
152
152
|
readonly rpcCode?: number;
|
|
153
|
+
/**
|
|
154
|
+
* HQ's own OAuth callback URL, carried on the errors that say a provider
|
|
155
|
+
* refused to register HQ automatically. It is what the admin must register
|
|
156
|
+
* with the provider before their own OAuth app can be used, and it is NOT a
|
|
157
|
+
* secret — every authorization server already receives it in the authorize
|
|
158
|
+
* URL. Taken from the response rather than assembled locally so the CLI
|
|
159
|
+
* prints the callback this backend will actually redirect to, instead of a
|
|
160
|
+
* guess at its route that would send the admin to register the wrong URL.
|
|
161
|
+
*/
|
|
162
|
+
readonly redirectUri?: string;
|
|
153
163
|
constructor(message: string, opts?: {
|
|
154
164
|
expected?: boolean;
|
|
155
165
|
code?: string;
|
|
156
166
|
status?: number;
|
|
157
167
|
oauthProtected?: boolean;
|
|
158
168
|
rpcCode?: number;
|
|
169
|
+
redirectUri?: string;
|
|
159
170
|
});
|
|
160
171
|
}
|
|
161
172
|
/**
|
|
@@ -51,6 +51,16 @@ export class IntegrationsCliError extends Error {
|
|
|
51
51
|
* fingerprint as anything but the finite allowlist in sentry-fingerprint.ts.
|
|
52
52
|
*/
|
|
53
53
|
rpcCode;
|
|
54
|
+
/**
|
|
55
|
+
* HQ's own OAuth callback URL, carried on the errors that say a provider
|
|
56
|
+
* refused to register HQ automatically. It is what the admin must register
|
|
57
|
+
* with the provider before their own OAuth app can be used, and it is NOT a
|
|
58
|
+
* secret — every authorization server already receives it in the authorize
|
|
59
|
+
* URL. Taken from the response rather than assembled locally so the CLI
|
|
60
|
+
* prints the callback this backend will actually redirect to, instead of a
|
|
61
|
+
* guess at its route that would send the admin to register the wrong URL.
|
|
62
|
+
*/
|
|
63
|
+
redirectUri;
|
|
54
64
|
constructor(message, opts = {}) {
|
|
55
65
|
super(message);
|
|
56
66
|
this.name = "IntegrationsCliError";
|
|
@@ -63,6 +73,8 @@ export class IntegrationsCliError extends Error {
|
|
|
63
73
|
this.oauthProtected = opts.oauthProtected;
|
|
64
74
|
if (opts.rpcCode !== undefined)
|
|
65
75
|
this.rpcCode = opts.rpcCode;
|
|
76
|
+
if (opts.redirectUri !== undefined)
|
|
77
|
+
this.redirectUri = opts.redirectUri;
|
|
66
78
|
}
|
|
67
79
|
}
|
|
68
80
|
/**
|
|
@@ -184,13 +196,34 @@ export async function raiseForResponse(res, fallback) {
|
|
|
184
196
|
// print verbatim — scrub credentials out before it can reach a terminal or a
|
|
185
197
|
// Sentry crash report (same reasoning as the gateway path below).
|
|
186
198
|
const detail = redactErrorText(body.error ?? "");
|
|
199
|
+
const redirectUri = httpsUrlOrNull(body.redirectUri);
|
|
187
200
|
throw new IntegrationsCliError(detail || `${fallback} (HTTP ${res.status})`, {
|
|
188
201
|
expected: isClientError(res.status),
|
|
189
202
|
status: res.status,
|
|
190
203
|
...(body.code ? { code: body.code } : {}),
|
|
191
204
|
...(body.oauthProtected === true ? { oauthProtected: true } : {}),
|
|
205
|
+
...(redirectUri ? { redirectUri } : {}),
|
|
192
206
|
});
|
|
193
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* Accept a response field only if it really is an absolute https URL.
|
|
210
|
+
*
|
|
211
|
+
* This one is printed as an instruction — "register THIS with the provider" —
|
|
212
|
+
* so a malformed or non-https value must be dropped rather than echoed: the
|
|
213
|
+
* caller's fallback (send them to the console, which shows the same callback)
|
|
214
|
+
* is correct, while a bad URL would have them register a callback that can
|
|
215
|
+
* never complete a sign-in.
|
|
216
|
+
*/
|
|
217
|
+
function httpsUrlOrNull(value) {
|
|
218
|
+
if (typeof value !== "string" || !value.trim())
|
|
219
|
+
return null;
|
|
220
|
+
try {
|
|
221
|
+
return new URL(value).protocol === "https:" ? value : null;
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
194
227
|
/** "factory:linear" → "linear"; mirrors hq-pro's factoryToolPrefix. */
|
|
195
228
|
export function toolPrefixForProvider(provider) {
|
|
196
229
|
return provider
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indigoai-us/hq-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.106.0",
|
|
4
4
|
"description": "HQ by Indigo management CLI — modules and cloud sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@aws-sdk/client-iot-data-plane": "^3.1096.0",
|
|
32
32
|
"@aws-sdk/client-s3": "^3.1049.0",
|
|
33
|
-
"@indigoai-us/hq-cloud": "~6.16.
|
|
33
|
+
"@indigoai-us/hq-cloud": "~6.16.3",
|
|
34
34
|
"@indigoai-us/hq-onboarding": "^0.1.0",
|
|
35
35
|
"@sentry/node": "^10.49.0",
|
|
36
36
|
"@tobilu/qmd": "2.5.3",
|