@indigoai-us/hq-cli 5.202.0 → 5.203.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +24 -0
  3. package/assets/lanes-hooks/hq-lanes-drain.mjs +36 -15
  4. package/dist/command-catalog.generated.d.ts +168 -0
  5. package/dist/command-catalog.generated.js +214 -0
  6. package/dist/command-registration-plan.d.ts +6 -0
  7. package/dist/command-registration-plan.js +1 -0
  8. package/dist/commands/integrations-connect.d.ts +38 -0
  9. package/dist/commands/integrations-connect.js +39 -38
  10. package/dist/commands/integrations-core.d.ts +4 -6
  11. package/dist/commands/integrations-core.js +15 -12
  12. package/dist/commands/integrations-import.js +8 -6
  13. package/dist/commands/integrations-manage.js +8 -6
  14. package/dist/commands/integrations.js +9 -5
  15. package/dist/commands/lanes.d.ts +1 -0
  16. package/dist/commands/lanes.js +150 -83
  17. package/dist/commands/monitor.d.ts +58 -0
  18. package/dist/commands/monitor.js +965 -0
  19. package/dist/lib/doctor/checks/integrations.js +11 -0
  20. package/dist/lib/lanes/delivery-core.d.ts +15 -0
  21. package/dist/lib/lanes/delivery-core.js +21 -0
  22. package/dist/lib/lanes/delivery.d.ts +21 -5
  23. package/dist/lib/lanes/delivery.js +71 -7
  24. package/dist/lib/lanes/dropbox.d.ts +13 -1
  25. package/dist/lib/lanes/dropbox.js +61 -5
  26. package/dist/lib/lanes/index.d.ts +1 -1
  27. package/dist/lib/lanes/index.js +1 -1
  28. package/dist/lib/lanes/move-account.js +1 -1
  29. package/dist/lib/lanes/spawn.d.ts +6 -3
  30. package/dist/lib/lanes/spawn.js +41 -8
  31. package/dist/lib/lanes/types.d.ts +3 -0
  32. package/dist/lib/monitor.d.ts +153 -0
  33. package/dist/lib/monitor.js +519 -0
  34. package/dist/main.js +40 -28
  35. package/dist/utils/cli-network-lifecycle.d.ts +4 -0
  36. package/dist/utils/cli-network-lifecycle.js +11 -0
  37. package/dist/utils/redact-error-text.js +2 -1
  38. package/dist/utils/redact-url.d.ts +14 -0
  39. package/dist/utils/redact-url.js +218 -0
  40. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -53,6 +53,7 @@ import { registerCommandsWithRecovery } from "./startup-registration.js";
53
53
  import { installTreeTornCaptureContext, installTreeTornStderrLine, isInstallTreeTornError, } from "./utils/install-tree-torn.js";
54
54
  import { isVaultAccessDeniedError, vaultAccessDeniedMessage, } from "./utils/vault-access-denied-error.js";
55
55
  import { fallbackOperatorMessage, unexpectedCliErrorMessage } from "./utils/unexpected-cli-error.js";
56
+ import { runGlobalNetworkLifecycle } from "./utils/cli-network-lifecycle.js";
56
57
  /**
57
58
  * Total time one CLI invocation may await foreground network work. This spans
58
59
  * the hard version decision, pre-action observability, and release-health
@@ -152,7 +153,7 @@ export function handleStreamError(err, deps = defaultStreamErrorDependencies) {
152
153
  }
153
154
  process.stdout.on("error", (err) => handleStreamError(err));
154
155
  process.stderr.on("error", (err) => handleStreamError(err));
155
- initSentry();
156
+ runGlobalNetworkLifecycle(process.argv, () => initSentry());
156
157
  const program = new Command();
157
158
  program
158
159
  .name("hq")
@@ -166,13 +167,15 @@ program.hook("preAction", async () => {
166
167
  // regardless, because the timer cannot be serviced while the event loop is
167
168
  // blocked. Keep anything added here asynchronous, or separately cheap: this
168
169
  // hook is on the path of EVERY hq command.
169
- const budget = activeForegroundNetworkBudget;
170
- if (!budget)
171
- return;
172
- await budget.waitFor(OBSERVABILITY_FOREGROUND_WAIT_MS, FINALIZATION_FOREGROUND_WAIT_MS, (timeoutMs) => Promise.all([
173
- emitCliSessionStarted(timeoutMs),
174
- reportCliClientHealthInvocation({ timeoutMs }),
175
- ]));
170
+ await runGlobalNetworkLifecycle(process.argv, async () => {
171
+ const budget = activeForegroundNetworkBudget;
172
+ if (!budget)
173
+ return;
174
+ await budget.waitFor(OBSERVABILITY_FOREGROUND_WAIT_MS, FINALIZATION_FOREGROUND_WAIT_MS, (timeoutMs) => Promise.all([
175
+ emitCliSessionStarted(timeoutMs),
176
+ reportCliClientHealthInvocation({ timeoutMs }),
177
+ ]));
178
+ });
176
179
  });
177
180
  export async function runCli() {
178
181
  const foregroundNetworkBudget = new ForegroundNetworkBudget();
@@ -180,17 +183,19 @@ export async function runCli() {
180
183
  // Begin one registry refresh without awaiting it. Every command gate reads
181
184
  // only the client's held snapshot and falls back locally, so an offline or
182
185
  // slow registry cannot delay command parsing or alter a command failure.
183
- void kickFlagRegistryReadiness();
186
+ runGlobalNetworkLifecycle(process.argv, () => {
187
+ void kickFlagRegistryReadiness();
188
+ });
184
189
  // Set when a self-update re-exec'd this command on a newer CLI: the child
185
190
  // already did the work, so this process only has to carry its exit status
186
191
  // out (after the finally block's telemetry, hence not process.exit here).
187
192
  let reexecStatus = null;
188
193
  try {
189
- Sentry.addBreadcrumb({
194
+ runGlobalNetworkLifecycle(process.argv, () => Sentry.addBreadcrumb({
190
195
  category: "command",
191
196
  message: sanitizeArgv(process.argv.slice(2)).join(" "),
192
197
  level: "info",
193
- });
198
+ }));
194
199
  // Version handling, both halves. Skipped for inspection flags
195
200
  // (`--version`, `--help`) so users debugging a broken install can still
196
201
  // introspect what they have, and silent on any failure — neither half may
@@ -204,7 +209,9 @@ export async function runCli() {
204
209
  // it (hq-pro's `updateRecommended` and the cached npm latest from
205
210
  // `version-check.ts`); whichever fires first re-execs, and the child
206
211
  // carries a guard env so it can never update again.
207
- if (!shouldSkipGate(process.argv)) {
212
+ const versionAction = await runGlobalNetworkLifecycle(process.argv, async () => {
213
+ if (shouldSkipGate(process.argv))
214
+ return "continue";
208
215
  const gate = (await foregroundNetworkBudget.waitFor(VERSION_GATE_FOREGROUND_WAIT_MS, FINALIZATION_FOREGROUND_WAIT_MS, (timeoutMs) => enforceVersionGate(async (decision) => {
209
216
  const outcome = await autoUpdateAndReexec(process.argv, decision.latestVersion);
210
217
  if (outcome.action === "reexec")
@@ -212,16 +219,19 @@ export async function runCli() {
212
219
  return outcome.action === "reexec";
213
220
  }, { timeoutMs }))) ?? "continue";
214
221
  if (gate === "reexec")
215
- return;
222
+ return "reexec";
216
223
  const cachedLatest = staleAgainstCachedLatest();
217
224
  if (cachedLatest) {
218
225
  const outcome = await autoUpdateAndReexec(process.argv, cachedLatest);
219
226
  if (outcome.action === "reexec") {
220
227
  reexecStatus = outcome.reexecStatus ?? 0;
221
- return;
228
+ return "reexec";
222
229
  }
223
230
  }
224
- }
231
+ return "continue";
232
+ });
233
+ if (versionAction === "reexec")
234
+ return;
225
235
  // Register every contributor for the selected root only. Root help and
226
236
  // unknown-command recovery use generated metadata, so they keep the full
227
237
  // Commander surface without evaluating every command implementation.
@@ -256,19 +266,21 @@ export async function runCli() {
256
266
  finally {
257
267
  // Plan-limit nag (US-016): stderr-only, never throws, never touches
258
268
  // process.exitCode — safe to run after exit codes have been set.
259
- emitPlanLimitNag();
260
- // Release health: finalize the per-run session before the flush.
261
- finishSentrySession();
262
- // Neither task may turn a successful command into Node's
263
- // `unsettled top-level await` exit. They are observability-only after the
264
- // command has completed, so a bounded best-effort wait is the terminal
265
- // lifecycle boundary for this invocation. The budget reserves this 500 ms
266
- // slice even when the command itself was long-running.
267
- const releaseHealthTimeoutMs = foregroundNetworkBudget.timeoutFor(FINALIZATION_FOREGROUND_WAIT_MS);
268
- await settleWithin([
269
- refreshVersionCache({ timeoutMs: releaseHealthTimeoutMs }),
270
- Sentry.flush(releaseHealthTimeoutMs),
271
- ], releaseHealthTimeoutMs);
269
+ await runGlobalNetworkLifecycle(process.argv, async () => {
270
+ emitPlanLimitNag();
271
+ // Release health: finalize the per-run session before the flush.
272
+ finishSentrySession();
273
+ // Neither task may turn a successful command into Node's
274
+ // `unsettled top-level await` exit. They are observability-only after the
275
+ // command has completed, so a bounded best-effort wait is the terminal
276
+ // lifecycle boundary for this invocation. The budget reserves this 500 ms
277
+ // slice even when the command itself was long-running.
278
+ const releaseHealthTimeoutMs = foregroundNetworkBudget.timeoutFor(FINALIZATION_FOREGROUND_WAIT_MS);
279
+ await settleWithin([
280
+ refreshVersionCache({ timeoutMs: releaseHealthTimeoutMs }),
281
+ Sentry.flush(releaseHealthTimeoutMs),
282
+ ], releaseHealthTimeoutMs);
283
+ });
272
284
  // Last, so it wins over anything the (skipped) command path would have
273
285
  // set: the re-exec'd child's status IS this invocation's result.
274
286
  if (reexecStatus !== null)
@@ -0,0 +1,4 @@
1
+ export declare function isMonitorFastPathCommand(argv: readonly string[]): boolean;
2
+ /** Skip process-wide network lifecycle work for hooks and detached monitor supervisors. */
3
+ export declare function runGlobalNetworkLifecycle<T>(argv: readonly string[], operation: () => T): T | undefined;
4
+ //# sourceMappingURL=cli-network-lifecycle.d.ts.map
@@ -0,0 +1,11 @@
1
+ const MONITOR_HIDDEN_NETWORK_BYPASS = new Set(["drain", "wait", "enabled", "_run"]);
2
+ export function isMonitorFastPathCommand(argv) {
3
+ return argv[2] === "monitor" && MONITOR_HIDDEN_NETWORK_BYPASS.has(argv[3] ?? "");
4
+ }
5
+ /** Skip process-wide network lifecycle work for hooks and detached monitor supervisors. */
6
+ export function runGlobalNetworkLifecycle(argv, operation) {
7
+ if (isMonitorFastPathCommand(argv))
8
+ return undefined;
9
+ return operation();
10
+ }
11
+ //# sourceMappingURL=cli-network-lifecycle.js.map
@@ -9,6 +9,7 @@
9
9
  // expected branch instead, which prints `err.message` verbatim. Rather than
10
10
  // duplicate the chain at the second print site, it lives here and both callers
11
11
  // share it: provider-minted text is untrusted and must never be printed raw.
12
+ import { redactUrlsInText } from "./redact-url.js";
12
13
  /** Upper bound on any redacted diagnostic — a message, not a payload dump. */
13
14
  export const REDACTED_TEXT_MAX_LENGTH = 1_000;
14
15
  /**
@@ -18,7 +19,7 @@ export const REDACTED_TEXT_MAX_LENGTH = 1_000;
18
19
  * their own fallback wording.
19
20
  */
20
21
  export function redactErrorText(text) {
21
- return text
22
+ return redactUrlsInText(text)
22
23
  .replace(/-----BEGIN[^-]+PRIVATE KEY-----[\s\S]*?-----END[^-]+PRIVATE KEY-----/g, "[REDACTED]")
23
24
  .replace(/\p{Cc}/gu, " ")
24
25
  .replace(/\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/gi, "[REDACTED]")
@@ -0,0 +1,14 @@
1
+ /** Marker used when a value claims to be a URL but cannot be parsed safely. */
2
+ export declare const REDACTED_URL = "[REDACTED]";
3
+ /**
4
+ * Redact the values of secret-looking URL parameters while retaining URL
5
+ * structure and safe query values. An invalid URL or malformed encoded
6
+ * parameter name is replaced in full because its secret boundaries are
7
+ * unknowable.
8
+ */
9
+ export declare function redactSensitiveUrl(value: string): string;
10
+ /** Redact URLs embedded in an error or human-readable sentence. */
11
+ export declare function redactUrlsInText(text: string): string;
12
+ /** Clone a JSON-like value with all URL-valued fields and embedded URLs safe. */
13
+ export declare function redactSensitiveUrls(value: unknown): unknown;
14
+ //# sourceMappingURL=redact-url.d.ts.map
@@ -0,0 +1,218 @@
1
+ /** Marker used when a value claims to be a URL but cannot be parsed safely. */
2
+ export const REDACTED_URL = "[REDACTED]";
3
+ const EXACT_SECRET_PARAMETER_NAMES = new Set([
4
+ "key",
5
+ "apikey",
6
+ "api_key",
7
+ "x-api-key",
8
+ "xmcpkey",
9
+ "token",
10
+ "access_token",
11
+ "refresh_token",
12
+ "secret",
13
+ "client_secret",
14
+ "password",
15
+ "pass",
16
+ "pwd",
17
+ "auth",
18
+ "authorization",
19
+ "sig",
20
+ "signature",
21
+ "session",
22
+ "code",
23
+ "credential",
24
+ "credentials",
25
+ "authcode",
26
+ "authorizationcode",
27
+ "oauthcode",
28
+ "clientcode",
29
+ "verificationcode",
30
+ "confirmationcode",
31
+ "resetcode",
32
+ "activationcode",
33
+ "invitecode",
34
+ "devicecode",
35
+ "pairingcode",
36
+ "onetimecode",
37
+ "otpcode",
38
+ "auth_code",
39
+ "authorization_code",
40
+ "oauth_code",
41
+ "client_code",
42
+ "verification_code",
43
+ "confirmation_code",
44
+ "reset_code",
45
+ "activation_code",
46
+ "invite_code",
47
+ "device_code",
48
+ "pairing_code",
49
+ "one_time_code",
50
+ "otp_code",
51
+ ]);
52
+ const UNAMBIGUOUS_SECRET_PARAMETER_SUFFIXES = [
53
+ "key",
54
+ "token",
55
+ "secret",
56
+ "password",
57
+ "signature",
58
+ "credential",
59
+ "credentials",
60
+ ];
61
+ const AMBIGUOUS_SECRET_PARAMETER_SUFFIXES = ["pass", "pwd", "sig", "auth", "session"];
62
+ const SECRET_PATH_LABEL = /^(?:access[_-]?token|refresh[_-]?token|id[_-]?token|api[_-]?key|x?mcpkey|client[_-]?secret|token|key|secret|password|pass|pwd|auth|authorization|sig|signature|credential|credentials|session)[=_-].+$/i;
63
+ function hasParameterBoundaryBeforeSuffix(name, suffix) {
64
+ const suffixStart = name.length - suffix.length;
65
+ if (suffixStart <= 0)
66
+ return true;
67
+ return /[-_.]/.test(name[suffixStart - 1]) || /[A-Z]/.test(name[suffixStart]);
68
+ }
69
+ function isSecretParameterName(name) {
70
+ const normalized = name.toLowerCase();
71
+ return (EXACT_SECRET_PARAMETER_NAMES.has(normalized) ||
72
+ UNAMBIGUOUS_SECRET_PARAMETER_SUFFIXES.some((suffix) => normalized.endsWith(suffix)) ||
73
+ AMBIGUOUS_SECRET_PARAMETER_SUFFIXES.some((suffix) => normalized.endsWith(suffix) && hasParameterBoundaryBeforeSuffix(name, suffix)));
74
+ }
75
+ function isOpaqueCapabilitySegment(segment) {
76
+ if (SECRET_PATH_LABEL.test(segment))
77
+ return true;
78
+ const length = segment.length;
79
+ const hasLetter = /[A-Za-z]/.test(segment);
80
+ const hasDigit = /\d/.test(segment);
81
+ const isLongHex = length >= 24 && /^[A-Fa-f0-9]+$/.test(segment);
82
+ const isLongBase64Url = length >= 32 &&
83
+ /^[A-Za-z0-9_-]+$/.test(segment) &&
84
+ hasLetter &&
85
+ (hasDigit || /[A-Z_-]/.test(segment));
86
+ return (length >= 20 && hasLetter && hasDigit) || isLongHex || isLongBase64Url;
87
+ }
88
+ /** Mask opaque credential-like path segments while keeping ordinary routes. */
89
+ function redactPathSegments(pathname) {
90
+ const segments = pathname.split("/");
91
+ const redacted = segments.map((segment) => {
92
+ let decoded;
93
+ try {
94
+ decoded = decodeURIComponent(segment);
95
+ }
96
+ catch (error) {
97
+ if (error instanceof URIError)
98
+ return null;
99
+ throw error;
100
+ }
101
+ return isOpaqueCapabilitySegment(decoded) ? "REDACTED" : segment;
102
+ });
103
+ return redacted.some((segment) => segment === null) ? null : redacted.join("/");
104
+ }
105
+ /**
106
+ * Redact the values of secret-looking URL parameters while retaining URL
107
+ * structure and safe query values. An invalid URL or malformed encoded
108
+ * parameter name is replaced in full because its secret boundaries are
109
+ * unknowable.
110
+ */
111
+ export function redactSensitiveUrl(value) {
112
+ let url;
113
+ try {
114
+ url = new URL(value);
115
+ }
116
+ catch (error) {
117
+ if (error instanceof TypeError)
118
+ return REDACTED_URL;
119
+ throw error;
120
+ }
121
+ const hasUserinfo = url.username !== "" || url.password !== "";
122
+ url.username = "";
123
+ url.password = "";
124
+ const pathname = redactPathSegments(url.pathname);
125
+ if (pathname === null)
126
+ return REDACTED_URL;
127
+ url.pathname = pathname;
128
+ const query = redactParameterList(url.search.slice(1));
129
+ if (query === null)
130
+ return REDACTED_URL;
131
+ url.search = query === "" ? "" : `?${query}`;
132
+ const fragment = redactFragment(url.hash.slice(1));
133
+ if (fragment === null)
134
+ return REDACTED_URL;
135
+ url.hash = fragment === "" ? "" : `#${fragment}`;
136
+ const serialized = url.toString();
137
+ if (!hasUserinfo)
138
+ return serialized;
139
+ const authorityStart = serialized.indexOf("://");
140
+ if (authorityStart === -1)
141
+ return serialized;
142
+ const hostStart = authorityStart + 3;
143
+ return `${serialized.slice(0, hostStart)}***@${serialized.slice(hostStart)}`;
144
+ }
145
+ /** Redact secret parameters in an ampersand-separated query or fragment. */
146
+ function redactParameterList(raw) {
147
+ if (raw === "")
148
+ return "";
149
+ const parts = raw.split("&");
150
+ const redacted = [];
151
+ for (const part of parts) {
152
+ const equalsAt = part.indexOf("=");
153
+ const encodedName = equalsAt === -1 ? part : part.slice(0, equalsAt);
154
+ let name;
155
+ try {
156
+ name = decodeURIComponent(encodedName.replace(/\+/g, " "));
157
+ }
158
+ catch (error) {
159
+ if (error instanceof URIError)
160
+ return null;
161
+ throw error;
162
+ }
163
+ if (isSecretParameterName(name)) {
164
+ redacted.push(`${encodedName}=REDACTED`);
165
+ }
166
+ else {
167
+ redacted.push(part);
168
+ }
169
+ }
170
+ return redacted.join("&");
171
+ }
172
+ /** Redact query-like fields in both `#access_token=...` and `#/route?...`. */
173
+ function redactFragment(fragment) {
174
+ if (fragment === "")
175
+ return "";
176
+ const queryStart = fragment.indexOf("?");
177
+ if (queryStart !== -1) {
178
+ const query = redactParameterList(fragment.slice(queryStart + 1));
179
+ return query === null ? null : `${fragment.slice(0, queryStart + 1)}${query}`;
180
+ }
181
+ if (fragment.includes("="))
182
+ return redactParameterList(fragment);
183
+ return fragment;
184
+ }
185
+ const URL_IN_TEXT = /\b[a-z][a-z\d+.-]*:\/\/[^\s<>"'`]+/gi;
186
+ const TRAILING_SENTENCE_PUNCTUATION = /[),.;!?]+$/;
187
+ /** Redact URLs embedded in an error or human-readable sentence. */
188
+ export function redactUrlsInText(text) {
189
+ return text.replace(URL_IN_TEXT, (candidate) => {
190
+ const punctuation = TRAILING_SENTENCE_PUNCTUATION.exec(candidate)?.[0] ?? "";
191
+ const url = punctuation ? candidate.slice(0, -punctuation.length) : candidate;
192
+ return `${redactSensitiveUrl(url)}${punctuation}`;
193
+ });
194
+ }
195
+ const URL_PROPERTY = /(?:^|[-_.])(?:url|uri|endpoint)$/i;
196
+ const CAMEL_URL_PROPERTY = /(?:Url|Uri|Endpoint)$/;
197
+ const ABSOLUTE_URL = /^[a-z][a-z\d+.-]*:\/\//i;
198
+ function isUrlProperty(key) {
199
+ return URL_PROPERTY.test(key) || CAMEL_URL_PROPERTY.test(key);
200
+ }
201
+ /** Clone a JSON-like value with all URL-valued fields and embedded URLs safe. */
202
+ export function redactSensitiveUrls(value) {
203
+ if (typeof value === "string")
204
+ return redactUrlsInText(value);
205
+ if (Array.isArray(value))
206
+ return value.map((item) => redactSensitiveUrls(item));
207
+ if (!value || typeof value !== "object")
208
+ return value;
209
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
210
+ key,
211
+ typeof item === "string" && isUrlProperty(key)
212
+ ? ABSOLUTE_URL.test(item)
213
+ ? redactSensitiveUrl(item)
214
+ : redactUrlsInText(item)
215
+ : redactSensitiveUrls(item),
216
+ ]));
217
+ }
218
+ //# sourceMappingURL=redact-url.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.202.0",
3
+ "version": "5.203.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {