@indigoai-us/hq-cli 5.342.9 → 5.342.11
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 +7 -0
- package/dist/commands/auth.js +3 -0
- package/dist/commands/integrations-core.d.ts +3 -0
- package/dist/commands/integrations-core.js +36 -3
- package/dist/commands/login.js +13 -0
- package/dist/lib/flag-registry.js +10 -5
- package/dist/main.js +65 -4
- package/dist/utils/resident-package-preload.d.ts +1 -1
- package/dist/utils/resident-package-preload.js +42 -0
- package/dist/utils/version-gate.js +20 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.342.11] — 2026-10-04
|
|
6
|
+
- Release the CLI package-use lease before interactive login waits after resident modules preload.
|
|
7
|
+
- Show the cached version update nudge after interactive commands when a newer cached release remains and its feature flag is enabled; avoid waiting for registry readiness when there is no nudge to show or repeating guidance after the updater already reported its result.
|
|
8
|
+
|
|
9
|
+
## [5.342.10] — 2026-10-04
|
|
10
|
+
- Attach bounded status, gateway error-class, and integrations subcommand tags to captured gateway faults.
|
|
11
|
+
|
|
5
12
|
## [5.342.9] — 2026-10-04
|
|
6
13
|
- Child wrappers now release their package-use lease after startup modules are preloaded, so long-running child processes do not block CLI updates.
|
|
7
14
|
- Keep nested HQ child leases and materialize core-job post-exit helpers before releasing the parent package lease.
|
package/dist/commands/auth.js
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* session is kept valid by `hq auth refresh` / `hq-auth-refresh` and consumed
|
|
15
15
|
* by the deploy + sync skills.
|
|
16
16
|
*/
|
|
17
|
+
import { releaseCurrentPackageUseLease, releasePackageUseLeaseAfterResidentPreload } from "../utils/package-use-lease.js";
|
|
18
|
+
import { preloadResidentPackageModules, reportResidentPackagePreloadFailure } from "../utils/resident-package-preload.js";
|
|
17
19
|
import * as fs from "node:fs";
|
|
18
20
|
import { open, realpath } from "node:fs/promises";
|
|
19
21
|
import path from "node:path";
|
|
@@ -231,6 +233,7 @@ export function registerAuthCommands(program) {
|
|
|
231
233
|
}
|
|
232
234
|
}
|
|
233
235
|
const selection = resolveLoginProviderSelection(options.provider, process.env, rememberedProvider);
|
|
236
|
+
await releasePackageUseLeaseAfterResidentPreload(() => preloadResidentPackageModules("auth-login"), reportResidentPackagePreloadFailure, releaseCurrentPackageUseLease);
|
|
234
237
|
const tokens = await browserLogin(selection.config);
|
|
235
238
|
try {
|
|
236
239
|
storeLastLoginProvider(providerFromIdToken(tokens.idToken) ?? selection.providerToRemember);
|
|
@@ -150,6 +150,8 @@ 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
|
+
/** Fixed vocabulary for the integrations subcommand that called the gateway. */
|
|
154
|
+
readonly gatewaySubcommand?: "tools_list" | "tools_call" | "other";
|
|
153
155
|
/**
|
|
154
156
|
* HQ's own OAuth callback URL, carried on the errors that say a provider
|
|
155
157
|
* refused to register HQ automatically. The provider must register the exact
|
|
@@ -164,6 +166,7 @@ export declare class IntegrationsCliError extends Error {
|
|
|
164
166
|
status?: number;
|
|
165
167
|
oauthProtected?: boolean;
|
|
166
168
|
rpcCode?: number;
|
|
169
|
+
gatewaySubcommand?: "tools_list" | "tools_call" | "other";
|
|
167
170
|
redirectUri?: string;
|
|
168
171
|
});
|
|
169
172
|
}
|
|
@@ -55,6 +55,8 @@ export class IntegrationsCliError extends Error {
|
|
|
55
55
|
* fingerprint as anything but the finite allowlist in sentry-fingerprint.ts.
|
|
56
56
|
*/
|
|
57
57
|
rpcCode;
|
|
58
|
+
/** Fixed vocabulary for the integrations subcommand that called the gateway. */
|
|
59
|
+
gatewaySubcommand;
|
|
58
60
|
/**
|
|
59
61
|
* HQ's own OAuth callback URL, carried on the errors that say a provider
|
|
60
62
|
* refused to register HQ automatically. The provider must register the exact
|
|
@@ -75,6 +77,8 @@ export class IntegrationsCliError extends Error {
|
|
|
75
77
|
this.oauthProtected = opts.oauthProtected;
|
|
76
78
|
if (opts.rpcCode !== undefined)
|
|
77
79
|
this.rpcCode = opts.rpcCode;
|
|
80
|
+
if (opts.gatewaySubcommand !== undefined)
|
|
81
|
+
this.gatewaySubcommand = opts.gatewaySubcommand;
|
|
78
82
|
if (opts.redirectUri !== undefined) {
|
|
79
83
|
Object.defineProperty(this, "redirectUri", {
|
|
80
84
|
value: opts.redirectUri,
|
|
@@ -175,6 +179,18 @@ const EXPECTED_GATEWAY_ERROR_CODES = new Set([-32003, -32602, -32050]);
|
|
|
175
179
|
function isExpectedGatewayError(code) {
|
|
176
180
|
return code != null && EXPECTED_GATEWAY_ERROR_CODES.has(code);
|
|
177
181
|
}
|
|
182
|
+
function expectedGatewayErrorFallback(code) {
|
|
183
|
+
switch (code) {
|
|
184
|
+
case -32003:
|
|
185
|
+
return "You do not have access to this integration. Ask its owner to share it with you.";
|
|
186
|
+
case -32602:
|
|
187
|
+
return "The integration rejected this request. Check the tool name and arguments.";
|
|
188
|
+
case -32050:
|
|
189
|
+
return "The connected provider could not complete this request. Wait a moment and try again.";
|
|
190
|
+
default:
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
178
194
|
// A 401 from ANY integration-gateway vault call means the caller's HQ session
|
|
179
195
|
// is expired or missing — an expected auth state fixed by `hq login`, not an
|
|
180
196
|
// hq-cli defect. Raise the same typed AuthError the vault company-resolution
|
|
@@ -399,7 +415,18 @@ export async function resolveConnection(token, companyUid, app, opts, recoveryOp
|
|
|
399
415
|
}
|
|
400
416
|
return selectConnection(connections, opts);
|
|
401
417
|
}
|
|
418
|
+
function gatewaySubcommandFor(params) {
|
|
419
|
+
const name = params.name;
|
|
420
|
+
if (typeof name !== "string")
|
|
421
|
+
return "other";
|
|
422
|
+
if (name.endsWith(".mcp.tools.list"))
|
|
423
|
+
return "tools_list";
|
|
424
|
+
if (name.endsWith(".mcp.tools.call"))
|
|
425
|
+
return "tools_call";
|
|
426
|
+
return "other";
|
|
427
|
+
}
|
|
402
428
|
export async function callGateway(token, params, idempotencyKey) {
|
|
429
|
+
const gatewaySubcommand = gatewaySubcommandFor(params);
|
|
403
430
|
const deadline = Date.now() + ASYNC_GATEWAY_DEADLINE_MS;
|
|
404
431
|
const res = await vaultApiFetch({
|
|
405
432
|
token,
|
|
@@ -414,6 +441,7 @@ export async function callGateway(token, params, idempotencyKey) {
|
|
|
414
441
|
});
|
|
415
442
|
const message = (await res.json().catch(() => null));
|
|
416
443
|
let terminalMessage = message;
|
|
444
|
+
let terminalStatus = res.status;
|
|
417
445
|
if (res.status === 202) {
|
|
418
446
|
const accepted = message;
|
|
419
447
|
const jobId = accepted && typeof accepted.jobId === "string" ? accepted.jobId : "";
|
|
@@ -427,7 +455,9 @@ export async function callGateway(token, params, idempotencyKey) {
|
|
|
427
455
|
if (!idempotencyKey) {
|
|
428
456
|
throw new IntegrationsCliError(`Integration gateway accepted async job ${jobId} without an idempotency key.`, { expected: false, status: 202 });
|
|
429
457
|
}
|
|
430
|
-
|
|
458
|
+
const terminal = await pollAsyncGatewayJob(token, companyUid, jobId, idempotencyKey, deadline);
|
|
459
|
+
terminalMessage = terminal.message;
|
|
460
|
+
terminalStatus = terminal.status;
|
|
431
461
|
}
|
|
432
462
|
if (res.status !== 202 && (!res.ok || !terminalMessage)) {
|
|
433
463
|
raiseIfUnauthorized(res);
|
|
@@ -437,7 +467,7 @@ export async function callGateway(token, params, idempotencyKey) {
|
|
|
437
467
|
// reaches the Sentry fingerprint's HTTP bucket instead of colliding under
|
|
438
468
|
// default grouping. Same reasoning as raiseForResponse, which already sets
|
|
439
469
|
// `status`. This does not change the `expected` decision above.
|
|
440
|
-
throw new IntegrationsCliError(`Integration gateway request failed (HTTP ${res.status}).`, { expected: isClientError(res.status), status: res.status });
|
|
470
|
+
throw new IntegrationsCliError(`Integration gateway request failed (HTTP ${res.status}).`, { expected: isClientError(res.status), status: res.status, gatewaySubcommand });
|
|
441
471
|
}
|
|
442
472
|
if (terminalMessage?.error) {
|
|
443
473
|
// The gateway's error text is minted UPSTREAM (hq-pro, the integration
|
|
@@ -449,8 +479,11 @@ export async function callGateway(token, params, idempotencyKey) {
|
|
|
449
479
|
// no-op. This preserves PR #298's user-visible diagnostic; it only makes
|
|
450
480
|
// it safe on the newly-expected path.
|
|
451
481
|
throw new IntegrationsCliError(redactErrorText(terminalMessage.error.message ?? "") ||
|
|
482
|
+
expectedGatewayErrorFallback(terminalMessage.error.code) ||
|
|
452
483
|
"Integration gateway returned an error.", {
|
|
453
484
|
expected: isExpectedGatewayError(terminalMessage.error.code),
|
|
485
|
+
status: terminalStatus,
|
|
486
|
+
gatewaySubcommand,
|
|
454
487
|
// Carry the protocol code to the capture site as a bounded grouping
|
|
455
488
|
// discriminator (HQ-CLI collision, Sentry 7642756130). This does not
|
|
456
489
|
// alter the `expected` decision above; it only labels the error.
|
|
@@ -518,7 +551,7 @@ async function pollAsyncGatewayJob(token, companyUid, jobId, idempotencyKey, dea
|
|
|
518
551
|
if (!hasJsonRpcOutcome) {
|
|
519
552
|
throw new IntegrationsCliError(`Integration job ${jobId} returned an invalid JSON-RPC response.`, { expected: false });
|
|
520
553
|
}
|
|
521
|
-
return response;
|
|
554
|
+
return { message: response, status: res.status };
|
|
522
555
|
}
|
|
523
556
|
if (poll.status !== "pending") {
|
|
524
557
|
throw new IntegrationsCliError(`Integration job ${jobId} returned unknown status '${String(poll.status)}'.`, { expected: true });
|
package/dist/commands/login.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* hq login — opens browser for Cognito auth.
|
|
3
3
|
*/
|
|
4
|
+
import { releaseCurrentPackageUseLease, releasePackageUseLeaseAfterResidentPreload } from "../utils/package-use-lease.js";
|
|
5
|
+
import { preloadResidentPackageModules, reportResidentPackagePreloadFailure } from "../utils/resident-package-preload.js";
|
|
4
6
|
import chalk from 'chalk';
|
|
5
7
|
import { browserLogin, loadCachedTokens, isExpiring } from '@indigoai-us/hq-cloud';
|
|
6
8
|
import { cognitoConfigForLoginProvider } from '../utils/login-provider.js';
|
|
@@ -30,6 +32,15 @@ export function registerLoginCommand(program) {
|
|
|
30
32
|
.option('--provider <provider>', 'OAuth provider to use: google, microsoft, or picker')
|
|
31
33
|
.action(async (options) => {
|
|
32
34
|
let accessToken;
|
|
35
|
+
let packageUseLeaseReleased = false;
|
|
36
|
+
const releaseForInteractiveWait = async () => {
|
|
37
|
+
if (packageUseLeaseReleased)
|
|
38
|
+
return;
|
|
39
|
+
await releasePackageUseLeaseAfterResidentPreload(async () => {
|
|
40
|
+
await preloadResidentPackageModules("login");
|
|
41
|
+
packageUseLeaseReleased = true;
|
|
42
|
+
}, reportResidentPackagePreloadFailure, releaseCurrentPackageUseLease);
|
|
43
|
+
};
|
|
33
44
|
try {
|
|
34
45
|
const existing = loadCachedTokens();
|
|
35
46
|
if (existing && !isExpiring(existing, 120)) {
|
|
@@ -38,6 +49,7 @@ export function registerLoginCommand(program) {
|
|
|
38
49
|
accessToken = existing.accessToken;
|
|
39
50
|
}
|
|
40
51
|
else {
|
|
52
|
+
await releaseForInteractiveWait();
|
|
41
53
|
console.log('Opening browser for authentication...');
|
|
42
54
|
const tokens = await browserLogin(cognitoConfigForLoginProvider(options.provider));
|
|
43
55
|
const who = peekIdToken(tokens.idToken).email ?? 'HQ';
|
|
@@ -63,6 +75,7 @@ export function registerLoginCommand(program) {
|
|
|
63
75
|
.join("\n");
|
|
64
76
|
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
65
77
|
try {
|
|
78
|
+
await releaseForInteractiveWait();
|
|
66
79
|
const answer = await rl.question(`Choose this device's default company:\n${choices}\nSelection: `);
|
|
67
80
|
const selected = activation.memberships[Number.parseInt(answer, 10) - 1];
|
|
68
81
|
if (selected) {
|
|
@@ -47,6 +47,7 @@ let refreshCompletion = Promise.resolve();
|
|
|
47
47
|
let completeRefresh = null;
|
|
48
48
|
let refreshWorker = null;
|
|
49
49
|
let refreshCompanyUid;
|
|
50
|
+
let refreshHasCompanyContext = false;
|
|
50
51
|
let refreshPendingSnapshot = null;
|
|
51
52
|
let refreshSentry;
|
|
52
53
|
/** Wait briefly for the current company refresh to persist or report completion. */
|
|
@@ -144,13 +145,12 @@ export function kickFlagRegistryReadiness(dependencies = {}) {
|
|
|
144
145
|
completeRefresh = null;
|
|
145
146
|
refreshWorker = null;
|
|
146
147
|
refreshCompanyUid = companyUid;
|
|
148
|
+
refreshHasCompanyContext = Boolean(companyUid || company.companySlug);
|
|
147
149
|
refreshPendingSnapshot = null;
|
|
148
150
|
refreshSentry = sentry;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
});
|
|
153
|
-
}
|
|
151
|
+
refreshCompletion = new Promise((resolve) => {
|
|
152
|
+
completeRefresh = resolve;
|
|
153
|
+
});
|
|
154
154
|
const finishRefresh = () => {
|
|
155
155
|
completeRefresh?.();
|
|
156
156
|
completeRefresh = null;
|
|
@@ -220,6 +220,10 @@ export function kickFlagRegistryReadiness(dependencies = {}) {
|
|
|
220
220
|
refreshPendingSnapshot = null;
|
|
221
221
|
finishRefresh();
|
|
222
222
|
}
|
|
223
|
+
else if (!refreshHasCompanyContext) {
|
|
224
|
+
refreshPendingSnapshot = null;
|
|
225
|
+
finishRefresh();
|
|
226
|
+
}
|
|
223
227
|
}
|
|
224
228
|
else {
|
|
225
229
|
finishRefresh();
|
|
@@ -316,6 +320,7 @@ export function _resetFlagRegistryForTests() {
|
|
|
316
320
|
completeRefresh = null;
|
|
317
321
|
refreshWorker = null;
|
|
318
322
|
refreshCompanyUid = undefined;
|
|
323
|
+
refreshHasCompanyContext = false;
|
|
319
324
|
refreshPendingSnapshot = null;
|
|
320
325
|
refreshSentry = undefined;
|
|
321
326
|
}
|
package/dist/main.js
CHANGED
|
@@ -41,16 +41,17 @@ import { DEFAULT_COGNITO, isMachineIdentity, loadMachineCachedTokens, } from "./
|
|
|
41
41
|
import { isCompanySelectionError } from "./utils/company-selection-error.js";
|
|
42
42
|
import { canOfferTeamUpgrade, formatPlanGateError, offerTeamUpgrade, offerUpgradeLink, planGateErrorFromVaultError, } from "./utils/plan-gate-error.js";
|
|
43
43
|
import { upgradeToTeam } from "./utils/team-upgrade.js";
|
|
44
|
-
import { printVersionAndNudge, refreshVersionCache, staleAgainstCachedLatest, } from "./utils/version-check.js";
|
|
45
|
-
import { enforceVersionGate, shouldSkipGate, } from "./utils/version-gate.js";
|
|
44
|
+
import { printCachedVersionNudge, printVersionAndNudge, refreshVersionCache, staleAgainstCachedLatest, } from "./utils/version-check.js";
|
|
45
|
+
import { enforceVersionGate, isLocalDependencyInstall, resolveRunningInstall, shouldSkipGate, } from "./utils/version-gate.js";
|
|
46
46
|
import { autoUpdateAndReexec } from "./utils/self-update.js";
|
|
47
47
|
import { CLI_VERSION } from "./cli-version.js";
|
|
48
48
|
import { findLazyCommand, registerCommandCatalog } from "./lazy-commands.js";
|
|
49
49
|
import { emitCliFirstSyncNextStepOutcome, emitCliLimitNoticeEngagement, emitCliLimitNoticeExposure, emitCliInviteNudgeEvent, emitCliSessionStarted, emitPlanLimitInviteTeammateExposure, isCliLimitResourceKind, queueCliLimitNoticeEngagement, queueCliLimitNoticeExposure, takeFirstSyncStepOutcomes, takeCliLimitNoticeEngagements, takeCliLimitNoticeExposures, takeCliInviteNudgeEvents, takePlanLimitInviteTeammateExposures, maybeShowFirstSyncNextStep, } from "./utils/cli-telemetry.js";
|
|
50
50
|
import { reportCliClientHealthInvocation } from "./utils/client-health.js";
|
|
51
51
|
import { settleWithin } from "./utils/settle-with-timeout.js";
|
|
52
|
+
import { isVersionRequest } from "./version-request.js";
|
|
52
53
|
import { emitPlanLimitNag } from "./lib/plan-limit-nag.js";
|
|
53
|
-
import { kickFlagRegistryReadiness } from "./lib/flag-registry.js";
|
|
54
|
+
import { kickFlagRegistryReadiness, waitForFlagRegistryRefresh, } from "./lib/flag-registry.js";
|
|
54
55
|
import { isPackageRootResolutionError, packageRootCaptureContext, } from "./utils/package-root-diagnostics.js";
|
|
55
56
|
import { registerCommandsWithRecovery } from "./startup-registration.js";
|
|
56
57
|
import { releasePackageUseLeaseAfterCommandLoad } from "./utils/package-use-lease.js";
|
|
@@ -185,6 +186,8 @@ export async function runCli() {
|
|
|
185
186
|
// out (after the finally block's telemetry, hence not process.exit here).
|
|
186
187
|
let reexecStatus = null;
|
|
187
188
|
let versionNudgeHandled = false;
|
|
189
|
+
let versionUpdateHandled = false;
|
|
190
|
+
let commandParsed = false;
|
|
188
191
|
try {
|
|
189
192
|
runGlobalNetworkLifecycle(process.argv, () => Sentry.addBreadcrumb({
|
|
190
193
|
category: "command",
|
|
@@ -209,6 +212,9 @@ export async function runCli() {
|
|
|
209
212
|
return "continue";
|
|
210
213
|
const gate = (await foregroundNetworkBudget.waitFor(VERSION_GATE_FOREGROUND_WAIT_MS, FINALIZATION_FOREGROUND_WAIT_MS, (timeoutMs) => enforceVersionGate(async (decision) => {
|
|
211
214
|
const outcome = await autoUpdateAndReexec(process.argv, decision.latestVersion);
|
|
215
|
+
if (outcome.action === "update-failed" || outcome.action === "updated-no-reexec") {
|
|
216
|
+
versionUpdateHandled = true;
|
|
217
|
+
}
|
|
212
218
|
if (outcome.action === "reexec")
|
|
213
219
|
reexecStatus = outcome.reexecStatus ?? 0;
|
|
214
220
|
return outcome.action === "reexec";
|
|
@@ -218,6 +224,9 @@ export async function runCli() {
|
|
|
218
224
|
const cachedLatest = staleAgainstCachedLatest();
|
|
219
225
|
if (cachedLatest) {
|
|
220
226
|
const outcome = await autoUpdateAndReexec(process.argv, cachedLatest);
|
|
227
|
+
if (outcome.action === "update-failed" || outcome.action === "updated-no-reexec") {
|
|
228
|
+
versionUpdateHandled = true;
|
|
229
|
+
}
|
|
221
230
|
if (outcome.action === "reexec") {
|
|
222
231
|
reexecStatus = outcome.reexecStatus ?? 0;
|
|
223
232
|
return "reexec";
|
|
@@ -264,6 +273,7 @@ export async function runCli() {
|
|
|
264
273
|
}
|
|
265
274
|
await releasePackageUseLeaseAfterCommandLoad(process.argv);
|
|
266
275
|
await program.parseAsync();
|
|
276
|
+
commandParsed = true;
|
|
267
277
|
maybeShowFirstSyncNextStep();
|
|
268
278
|
}
|
|
269
279
|
catch (err) {
|
|
@@ -287,10 +297,26 @@ export async function runCli() {
|
|
|
287
297
|
const inviteTeammateExposureFlushes = takePlanLimitInviteTeammateExposures().map((companyUid) => emitPlanLimitInviteTeammateExposure(companyUid, releaseHealthTimeoutMs));
|
|
288
298
|
const limitNoticeExposureFlushes = takeCliLimitNoticeExposures().map((exposure) => emitCliLimitNoticeExposure(exposure, releaseHealthTimeoutMs));
|
|
289
299
|
const limitNoticeEngagementFlushes = takeCliLimitNoticeEngagements().map((engagement) => emitCliLimitNoticeEngagement(engagement, releaseHealthTimeoutMs));
|
|
300
|
+
const commandArgs = process.argv.slice(2);
|
|
301
|
+
const canShowCachedVersionNudge = commandParsed &&
|
|
302
|
+
!versionNudgeHandled &&
|
|
303
|
+
!versionUpdateHandled &&
|
|
304
|
+
process.stdout.isTTY === true &&
|
|
305
|
+
!commandArgs.includes("--json") &&
|
|
306
|
+
!isVersionRequest(process.argv) &&
|
|
307
|
+
!shouldSkipGate(process.argv) &&
|
|
308
|
+
!isLocalDependencyInstall(resolveRunningInstall());
|
|
309
|
+
const cachedVersionNudge = canShowCachedVersionNudge && staleAgainstCachedLatest() !== null
|
|
310
|
+
? (async () => {
|
|
311
|
+
await waitForFlagRegistryRefresh(releaseHealthTimeoutMs);
|
|
312
|
+
await printCachedVersionNudge(process.stderr);
|
|
313
|
+
})()
|
|
314
|
+
: undefined;
|
|
290
315
|
await settleWithin([
|
|
291
316
|
...(versionNudgeHandled
|
|
292
317
|
? []
|
|
293
318
|
: [refreshVersionCache({ timeoutMs: releaseHealthTimeoutMs })]),
|
|
319
|
+
...(cachedVersionNudge ? [cachedVersionNudge] : []),
|
|
294
320
|
Sentry.flush(releaseHealthTimeoutMs),
|
|
295
321
|
...firstSyncOutcomeFlushes,
|
|
296
322
|
...inviteNudgeOutcomeFlushes,
|
|
@@ -335,6 +361,39 @@ function qmdSpawnFailureCaptureContext(err) {
|
|
|
335
361
|
return undefined;
|
|
336
362
|
return { contexts: { qmd_spawn_failure: diagnostics } };
|
|
337
363
|
}
|
|
364
|
+
const GATEWAY_RPC_CODE_TAGS = new Map([
|
|
365
|
+
[-32603, "internal_error"],
|
|
366
|
+
[-32009, "conflict"],
|
|
367
|
+
[-32003, "unauthorized"],
|
|
368
|
+
[-32602, "invalid_params"],
|
|
369
|
+
[-32050, "provider_error"],
|
|
370
|
+
]);
|
|
371
|
+
function integrationsGatewayCaptureContext(err) {
|
|
372
|
+
if (err === null || typeof err !== "object")
|
|
373
|
+
return undefined;
|
|
374
|
+
const record = err;
|
|
375
|
+
const subcommand = record.gatewaySubcommand;
|
|
376
|
+
if (subcommand === undefined)
|
|
377
|
+
return undefined;
|
|
378
|
+
const subcommandTag = subcommand === "tools_list" || subcommand === "tools_call"
|
|
379
|
+
? subcommand
|
|
380
|
+
: "other";
|
|
381
|
+
const status = record.status;
|
|
382
|
+
const statusTag = typeof status === "number" && Number.isInteger(status) && status >= 100 && status <= 599
|
|
383
|
+
? String(status)
|
|
384
|
+
: status === undefined ? "unknown" : "other";
|
|
385
|
+
const rpcCode = record.rpcCode;
|
|
386
|
+
const codeTag = typeof rpcCode === "number" && Number.isInteger(rpcCode)
|
|
387
|
+
? GATEWAY_RPC_CODE_TAGS.get(rpcCode) ?? "other"
|
|
388
|
+
: rpcCode === undefined ? "missing" : "other";
|
|
389
|
+
return {
|
|
390
|
+
tags: {
|
|
391
|
+
integration_gateway_status: statusTag,
|
|
392
|
+
integration_gateway_code: codeTag,
|
|
393
|
+
integration_subcommand: subcommandTag,
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
}
|
|
338
397
|
/** Classify a top-level failure without making the CLI process boundary opaque to tests. */
|
|
339
398
|
export async function handleTopLevelError(err, deps = defaultTopLevelErrorDependencies) {
|
|
340
399
|
// A typed plan gate, or an hq-cloud vault error carrying a plan-limit
|
|
@@ -853,9 +912,11 @@ export async function handleTopLevelError(err, deps = defaultTopLevelErrorDepend
|
|
|
853
912
|
const spawnContext = qmdSpawnFailureCaptureContext(err);
|
|
854
913
|
const installContext = incompleteInstallCaptureContext(err);
|
|
855
914
|
const qmdContext = qmdExitCaptureContext(err);
|
|
856
|
-
|
|
915
|
+
const gatewayContext = integrationsGatewayCaptureContext(err);
|
|
916
|
+
if (spawnContext || installContext || qmdContext || gatewayContext) {
|
|
857
917
|
deps.sentry.captureException(err, {
|
|
858
918
|
...qmdContext,
|
|
919
|
+
...(gatewayContext ? { tags: gatewayContext.tags } : {}),
|
|
859
920
|
...((spawnContext || installContext)
|
|
860
921
|
? { contexts: { ...spawnContext?.contexts, ...installContext } }
|
|
861
922
|
: {}),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ResidentPackagePath = "bot" | "
|
|
1
|
+
export type ResidentPackagePath = "auth-login" | "bot" | "childWrapper" | "coreJobRun" | "login" | "mcp" | "daemon";
|
|
2
2
|
/** Package imports reachable lazily from each resident command's runtime graph. */
|
|
3
3
|
export declare const RESIDENT_PACKAGE_MODULES: Record<ResidentPackagePath, readonly string[]>;
|
|
4
4
|
export declare class ResidentPackagePreloadError extends Error {
|
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
/** Package imports reachable lazily from each resident command's runtime graph. */
|
|
2
2
|
export const RESIDENT_PACKAGE_MODULES = {
|
|
3
|
+
login: [
|
|
4
|
+
"../lib/mesh/api.js",
|
|
5
|
+
"../lib/work-context/config.js",
|
|
6
|
+
"../lib/work-context/default-company.js",
|
|
7
|
+
"../lib/work-context/paths.js",
|
|
8
|
+
"../lib/plan-limit-nag.js",
|
|
9
|
+
"../utils/plan-gate-error.js",
|
|
10
|
+
"../sentry.js",
|
|
11
|
+
"open",
|
|
12
|
+
],
|
|
13
|
+
"auth-login": [
|
|
14
|
+
"../lib/auth-login-provider-state.js",
|
|
15
|
+
"../lib/daemon/mcp-oauth.js",
|
|
16
|
+
"../lib/plan-limit-nag.js",
|
|
17
|
+
"../utils/plan-gate-error.js",
|
|
18
|
+
"../sentry.js",
|
|
19
|
+
"open",
|
|
20
|
+
],
|
|
3
21
|
bot: [
|
|
4
22
|
"../lib/flag-registry.js",
|
|
5
23
|
"../commands/access.js",
|
|
@@ -24,6 +42,30 @@ export const RESIDENT_PACKAGE_MODULES = {
|
|
|
24
42
|
],
|
|
25
43
|
};
|
|
26
44
|
const LOADERS = {
|
|
45
|
+
login: [
|
|
46
|
+
() => import("../lib/mesh/api.js"),
|
|
47
|
+
() => import("../lib/work-context/config.js"),
|
|
48
|
+
() => import("../lib/work-context/default-company.js"),
|
|
49
|
+
() => import("../lib/work-context/paths.js"),
|
|
50
|
+
() => import("../lib/plan-limit-nag.js"),
|
|
51
|
+
() => import("../utils/plan-gate-error.js"),
|
|
52
|
+
async () => {
|
|
53
|
+
const { preloadSentrySdk } = await import("../sentry.js");
|
|
54
|
+
preloadSentrySdk();
|
|
55
|
+
},
|
|
56
|
+
() => import("open"),
|
|
57
|
+
],
|
|
58
|
+
"auth-login": [
|
|
59
|
+
() => import("../lib/auth-login-provider-state.js"),
|
|
60
|
+
() => import("../lib/daemon/mcp-oauth.js"),
|
|
61
|
+
() => import("../lib/plan-limit-nag.js"),
|
|
62
|
+
() => import("../utils/plan-gate-error.js"),
|
|
63
|
+
async () => {
|
|
64
|
+
const { preloadSentrySdk } = await import("../sentry.js");
|
|
65
|
+
preloadSentrySdk();
|
|
66
|
+
},
|
|
67
|
+
() => import("open"),
|
|
68
|
+
],
|
|
27
69
|
bot: [
|
|
28
70
|
() => import("../lib/flag-registry.js"),
|
|
29
71
|
() => import("../commands/access.js"),
|
|
@@ -37,6 +37,7 @@ import { fileURLToPath } from "node:url";
|
|
|
37
37
|
import chalk from "chalk";
|
|
38
38
|
import semver from "semver";
|
|
39
39
|
import { CLI_NAME, CLI_VERSION } from "../cli-version.js";
|
|
40
|
+
import { isVersionRequest } from "../version-request.js";
|
|
40
41
|
import { Sentry } from "../sentry.js";
|
|
41
42
|
import { isCliTelemetryDisabled } from "./cli-telemetry.js";
|
|
42
43
|
import { DEFAULT_VAULT_API_URL } from "./cognito-session.js";
|
|
@@ -315,11 +316,6 @@ export function isLocalDependencyPackageDir(pkgDir, platform = process.platform)
|
|
|
315
316
|
// `<dir>/lib/node_modules` while the `.bin` shim keeps resolving
|
|
316
317
|
// `<dir>/node_modules`.
|
|
317
318
|
//
|
|
318
|
-
// Windows is the exception and must keep the old behaviour: its global
|
|
319
|
-
// layout is `<prefix>\node_modules` with no `lib` segment, so the same test
|
|
320
|
-
// would misread a genuine global install as local.
|
|
321
|
-
if (platform === "win32")
|
|
322
|
-
return false;
|
|
323
319
|
const segments = pkgDir
|
|
324
320
|
.replace(/\\/g, "/")
|
|
325
321
|
.replace(/\/+$/, "")
|
|
@@ -328,6 +324,23 @@ export function isLocalDependencyPackageDir(pkgDir, platform = process.platform)
|
|
|
328
324
|
const nodeModulesIndex = segments.lastIndexOf("node_modules");
|
|
329
325
|
if (nodeModulesIndex <= 0)
|
|
330
326
|
return false;
|
|
327
|
+
if (platform === "win32") {
|
|
328
|
+
const prefix = segments.slice(0, nodeModulesIndex).join("/");
|
|
329
|
+
const normalize = (value) => value.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
330
|
+
const configuredPrefix = process.env.npm_config_prefix ?? process.env.NPM_CONFIG_PREFIX;
|
|
331
|
+
if (configuredPrefix && normalize(configuredPrefix) === normalize(prefix))
|
|
332
|
+
return false;
|
|
333
|
+
const appData = process.env.APPDATA;
|
|
334
|
+
const defaultPrefix = appData
|
|
335
|
+
? path.join(appData, "npm")
|
|
336
|
+
: path.join(os.homedir(), "AppData", "Roaming", "npm");
|
|
337
|
+
if (normalize(defaultPrefix) === normalize(prefix))
|
|
338
|
+
return false;
|
|
339
|
+
const pathEntries = (process.env.PATH ?? "").split(";");
|
|
340
|
+
if (pathEntries.some((entry) => normalize(entry) === normalize(prefix)))
|
|
341
|
+
return false;
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
331
344
|
return segments[nodeModulesIndex - 1] !== "lib";
|
|
332
345
|
}
|
|
333
346
|
export function isLocalDependencyInstall(install, platform = process.platform) {
|
|
@@ -1588,12 +1601,11 @@ export async function enforceVersionGate(onUpdateRecommended, options = {}) {
|
|
|
1588
1601
|
* force-upgraded.
|
|
1589
1602
|
*/
|
|
1590
1603
|
export function shouldSkipGate(argv) {
|
|
1591
|
-
return argv.some((a) => a === "--version" ||
|
|
1604
|
+
return isVersionRequest(argv) || argv.some((a) => a === "--version" ||
|
|
1592
1605
|
a === "-V" ||
|
|
1593
1606
|
a === "-v" ||
|
|
1594
1607
|
a === "--help" ||
|
|
1595
|
-
a === "-h"
|
|
1596
|
-
a === "version");
|
|
1608
|
+
a === "-h");
|
|
1597
1609
|
}
|
|
1598
1610
|
export const __test__ = {
|
|
1599
1611
|
CLIENT_ID,
|