@indigoai-us/hq-cli 5.345.90 → 5.345.91
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.91] — 2026-10-12
|
|
6
|
+
- Add published E2E checks comparing native commands with bundled shell scripts for qmd background reindex, hq-session, checkpoint stop gate, and worker skill commands.
|
|
7
|
+
|
|
8
|
+
- `hq integrations connect <app> --auth key` (and `--token-stdin`) now
|
|
9
|
+
sends the access key to the app's MCP endpoint. Catalog Slack used to
|
|
10
|
+
ignore the key and only `--mcp-url` connected. If the server says the
|
|
11
|
+
app needs a key and cannot sign HQ in automatically, the command
|
|
12
|
+
collects the key instead of stopping.
|
|
13
|
+
|
|
5
14
|
## [5.345.90] — 2026-10-12
|
|
6
15
|
- The setup bot that ships with hq-cli (used before an HQ has synced its core workers) now reads the HQ desktop app's first-run handoff, the same as hq-core's copy. It treats the one note the app sends when its visual setup closes as its kickoff, never offers or repeats an import the app already did, and never runs a second scan. It also no longer asks what the person already said: told "I run an independent software engineering company", it sets up for a software services firm instead of asking "for clients, or your own product?".
|
|
7
16
|
|
|
@@ -15,6 +15,12 @@
|
|
|
15
15
|
* install and treats hq-pro's `INTEGRATION_FACTORY_OAUTH_REQUIRED` as the
|
|
16
16
|
* signal to switch into the browser flow. That way a catalog row that says
|
|
17
17
|
* "key" but is really OAuth-protected still connects on the first try.
|
|
18
|
+
*
|
|
19
|
+
* A pasted key is different. Catalog/domain installs can drop `--auth key` /
|
|
20
|
+
* `--token-stdin` (hq-pro only forwards a bearer token on the direct MCP URL
|
|
21
|
+
* path, and an OAuth catalog row short-circuits to sign-in). When the caller
|
|
22
|
+
* supplied a key, or the server says the endpoint needs one and cannot
|
|
23
|
+
* register HQ, retry against the inspected MCP URL with the key.
|
|
18
24
|
*/
|
|
19
25
|
import { Command } from "commander";
|
|
20
26
|
import { type ResidentCommandLeaseOptions } from "../utils/resident-package-preload.js";
|
|
@@ -15,6 +15,12 @@
|
|
|
15
15
|
* install and treats hq-pro's `INTEGRATION_FACTORY_OAUTH_REQUIRED` as the
|
|
16
16
|
* signal to switch into the browser flow. That way a catalog row that says
|
|
17
17
|
* "key" but is really OAuth-protected still connects on the first try.
|
|
18
|
+
*
|
|
19
|
+
* A pasted key is different. Catalog/domain installs can drop `--auth key` /
|
|
20
|
+
* `--token-stdin` (hq-pro only forwards a bearer token on the direct MCP URL
|
|
21
|
+
* path, and an OAuth catalog row short-circuits to sign-in). When the caller
|
|
22
|
+
* supplied a key, or the server says the endpoint needs one and cannot
|
|
23
|
+
* register HQ, retry against the inspected MCP URL with the key.
|
|
18
24
|
*/
|
|
19
25
|
import chalk from "chalk";
|
|
20
26
|
import open from "open";
|
|
@@ -30,6 +36,8 @@ import { completeOAuth, discoverDocs, installIntegration, listCatalog, pullBluep
|
|
|
30
36
|
import { OAUTH_LOOPBACK_TIMEOUT_CODE, startLoopbackListener, } from "./integrations-oauth.js";
|
|
31
37
|
/** hq-pro's machine code for "this endpoint needs a browser sign-in". */
|
|
32
38
|
const OAUTH_REQUIRED_CODE = "INTEGRATION_FACTORY_OAUTH_REQUIRED";
|
|
39
|
+
/** hq-pro's machine code for "this MCP endpoint refused the auth we sent". */
|
|
40
|
+
const DIRECT_MCP_AUTH_REJECTED_CODE = "DIRECT_MCP_AUTH_REJECTED";
|
|
33
41
|
function warnBrowserOpenFailure(error) {
|
|
34
42
|
console.warn("[hq-cli:integrations-connect] browser open failed", safeCaughtError(error));
|
|
35
43
|
}
|
|
@@ -51,6 +59,17 @@ function isOAuthRequiredError(err) {
|
|
|
51
59
|
return (err instanceof IntegrationsCliError &&
|
|
52
60
|
(err.code === OAUTH_REQUIRED_CODE || err.oauthProtected === true));
|
|
53
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* The server probed the MCP endpoint, got an OAuth challenge, and determined
|
|
64
|
+
* it cannot self-register HQ (no DCR). The recovery is a pasted access key,
|
|
65
|
+
* not a browser sign-in. `oauthProtected: true` is the opposite signal and
|
|
66
|
+
* must keep taking the sign-in branch.
|
|
67
|
+
*/
|
|
68
|
+
function isAccessKeyRequiredError(err) {
|
|
69
|
+
return (err instanceof IntegrationsCliError &&
|
|
70
|
+
err.code === DIRECT_MCP_AUTH_REJECTED_CODE &&
|
|
71
|
+
err.oauthProtected !== true);
|
|
72
|
+
}
|
|
54
73
|
/**
|
|
55
74
|
* hq-pro will not take our loopback callback, for either of the two reasons it
|
|
56
75
|
* can refuse one:
|
|
@@ -894,6 +913,59 @@ async function completeCredentialIfNeeded(token, companyUid, target, opts, resul
|
|
|
894
913
|
bearerToken,
|
|
895
914
|
});
|
|
896
915
|
}
|
|
916
|
+
function mcpUrlFromBlueprint(blueprint) {
|
|
917
|
+
const surfaces = [
|
|
918
|
+
...(blueprint.recommendedSurface ? [blueprint.recommendedSurface] : []),
|
|
919
|
+
...blueprint.surfaces,
|
|
920
|
+
];
|
|
921
|
+
for (const surface of surfaces) {
|
|
922
|
+
const url = surface.url?.trim();
|
|
923
|
+
if (url && /^https:\/\//i.test(url))
|
|
924
|
+
return url;
|
|
925
|
+
}
|
|
926
|
+
return undefined;
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Catalog/domain/query installs are the path that drops a pasted key. The
|
|
930
|
+
* working connect is the same one `--mcp-url` already uses: a direct MCP
|
|
931
|
+
* endpoint plus `authMode: bearer`.
|
|
932
|
+
*/
|
|
933
|
+
async function targetForAccessKeyConnect(token, companyUid, target, opts) {
|
|
934
|
+
if (target.ref.mcpUrl)
|
|
935
|
+
return { ...target, authClass: "key" };
|
|
936
|
+
const blueprint = await pullBlueprint(token, companyUid, {
|
|
937
|
+
...(target.ref.domain ? { domain: target.ref.domain } : {}),
|
|
938
|
+
...(target.ref.query ? { query: target.ref.query } : {}),
|
|
939
|
+
...(target.ref.catalogEntryId ? { catalogEntryId: target.ref.catalogEntryId } : {}),
|
|
940
|
+
});
|
|
941
|
+
const mcpUrl = mcpUrlFromBlueprint(blueprint);
|
|
942
|
+
if (!mcpUrl) {
|
|
943
|
+
throw new IntegrationsCliError(`${target.label} needs an access key. Re-run with --mcp-url <endpoint> --auth key --token-stdin.`, { expected: true });
|
|
944
|
+
}
|
|
945
|
+
return {
|
|
946
|
+
ref: {
|
|
947
|
+
mcpUrl,
|
|
948
|
+
...(opts.provider ? { provider: opts.provider } : blueprint.provider ? { provider: blueprint.provider } : {}),
|
|
949
|
+
...(opts.displayName
|
|
950
|
+
? { displayName: opts.displayName }
|
|
951
|
+
: blueprint.displayName
|
|
952
|
+
? { displayName: blueprint.displayName }
|
|
953
|
+
: {}),
|
|
954
|
+
},
|
|
955
|
+
authClass: "key",
|
|
956
|
+
label: target.label,
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
function bearerInstallInput(target, opts, bearerToken) {
|
|
960
|
+
return {
|
|
961
|
+
...target.ref,
|
|
962
|
+
authMode: "bearer",
|
|
963
|
+
bearerToken,
|
|
964
|
+
...(opts.authHeader
|
|
965
|
+
? { authScheme: { placement: "header", header: opts.authHeader } }
|
|
966
|
+
: {}),
|
|
967
|
+
};
|
|
968
|
+
}
|
|
897
969
|
/** Print the outcome of a successful connect. */
|
|
898
970
|
function reportInstall(result, opts, expectedRevivedConnectionId) {
|
|
899
971
|
if (opts.json) {
|
|
@@ -923,17 +995,34 @@ async function connectApp(token, companyUid, app, opts, expectedRevivedConnectio
|
|
|
923
995
|
: opts.auth ?? target.authClass;
|
|
924
996
|
assertUserClientApplicable(opts, target, authMode);
|
|
925
997
|
assertScopesApplicable(opts, target, authMode);
|
|
926
|
-
if (authMode === "oauth") {
|
|
927
|
-
const result = await connectViaOAuth(token, companyUid, target, opts);
|
|
928
|
-
if (result)
|
|
929
|
-
reportInstall(result, opts, expectedRevivedConnectionId);
|
|
930
|
-
return;
|
|
931
|
-
}
|
|
932
998
|
// A key is only collected when something already says one is needed, or
|
|
933
999
|
// the caller supplied one — otherwise a no-auth app would pointlessly
|
|
934
|
-
// prompt.
|
|
1000
|
+
// prompt. Compute this BEFORE the OAuth branch: a catalog row marked
|
|
1001
|
+
// oauth plus `--token-stdin` used to skip the key entirely.
|
|
935
1002
|
const wantsKey = authMode === "key" || Boolean(opts.token || opts.tokenStdin);
|
|
1003
|
+
const finishWithAccessKey = async (existingKey) => {
|
|
1004
|
+
const keyTarget = await targetForAccessKeyConnect(token, companyUid, target, opts);
|
|
1005
|
+
const bearerToken = existingKey ?? (await resolveKey(opts, keyTarget.label));
|
|
1006
|
+
const result = await installIntegration(token, companyUid, bearerInstallInput(keyTarget, opts, bearerToken));
|
|
1007
|
+
reportInstall(await completeCredentialIfNeeded(token, companyUid, keyTarget, opts, result), opts, expectedRevivedConnectionId);
|
|
1008
|
+
};
|
|
1009
|
+
if (authMode === "oauth" && !wantsKey) {
|
|
1010
|
+
try {
|
|
1011
|
+
const result = await connectViaOAuth(token, companyUid, target, opts);
|
|
1012
|
+
if (result)
|
|
1013
|
+
reportInstall(result, opts, expectedRevivedConnectionId);
|
|
1014
|
+
}
|
|
1015
|
+
catch (err) {
|
|
1016
|
+
if (isAccessKeyRequiredError(err)) {
|
|
1017
|
+
await finishWithAccessKey();
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
throw err;
|
|
1021
|
+
}
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
936
1024
|
const bearerToken = wantsKey ? await resolveKey(opts, target.label) : undefined;
|
|
1025
|
+
const alreadyDirectKeyInstall = Boolean(target.ref.mcpUrl && bearerToken);
|
|
937
1026
|
try {
|
|
938
1027
|
const result = await installIntegration(token, companyUid, {
|
|
939
1028
|
...target.ref,
|
|
@@ -952,13 +1041,28 @@ async function connectApp(token, companyUid, app, opts, expectedRevivedConnectio
|
|
|
952
1041
|
reportInstall(await completeCredentialIfNeeded(token, companyUid, target, opts, result), opts, expectedRevivedConnectionId);
|
|
953
1042
|
}
|
|
954
1043
|
catch (err) {
|
|
1044
|
+
const retryAsKey = !alreadyDirectKeyInstall &&
|
|
1045
|
+
(isAccessKeyRequiredError(err) || (isOAuthRequiredError(err) && wantsKey));
|
|
1046
|
+
if (retryAsKey) {
|
|
1047
|
+
await finishWithAccessKey(bearerToken);
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
955
1050
|
// Server-authoritative detection: the endpoint turned out to be
|
|
956
1051
|
// OAuth-protected, so run the browser flow instead of making the
|
|
957
1052
|
// caller re-issue the command with --auth oauth.
|
|
958
1053
|
if (isOAuthRequiredError(err)) {
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1054
|
+
try {
|
|
1055
|
+
const result = await connectViaOAuth(token, companyUid, target, opts);
|
|
1056
|
+
if (result)
|
|
1057
|
+
reportInstall(result, opts, expectedRevivedConnectionId);
|
|
1058
|
+
}
|
|
1059
|
+
catch (oauthErr) {
|
|
1060
|
+
if (isAccessKeyRequiredError(oauthErr)) {
|
|
1061
|
+
await finishWithAccessKey(bearerToken);
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
throw oauthErr;
|
|
1065
|
+
}
|
|
962
1066
|
return;
|
|
963
1067
|
}
|
|
964
1068
|
throw err;
|