@indigoai-us/hq-cli 5.345.155 → 5.345.159

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 (169) hide show
  1. package/CHANGELOG.md +289 -0
  2. package/assets/scaffold/core/scripts/rebuild-company-knowledge-index.sh +110 -2
  3. package/assets/scaffold/core/scripts/rebuild-projects-index.sh +101 -5
  4. package/assets/scaffold/core/scripts/worktree.sh +4 -0
  5. package/dist/command-catalog.generated.d.ts +536 -7
  6. package/dist/command-catalog.generated.js +676 -24
  7. package/dist/command-registration-plan.d.ts +18 -0
  8. package/dist/command-registration-plan.js +4 -0
  9. package/dist/commands/_patterns.d.ts +17 -0
  10. package/dist/commands/_patterns.js +22 -0
  11. package/dist/commands/agent-inbox.js +2 -2
  12. package/dist/commands/agent-kit.js +8 -6
  13. package/dist/commands/bot.js +7 -4
  14. package/dist/commands/channel-suggest.d.ts +56 -0
  15. package/dist/commands/channel-suggest.js +170 -0
  16. package/dist/commands/channels.js +7 -1
  17. package/dist/commands/cloud.d.ts +45 -0
  18. package/dist/commands/cloud.js +150 -10
  19. package/dist/commands/company.js +4 -3
  20. package/dist/commands/core-checkpoint-native.d.ts +18 -0
  21. package/dist/commands/core-checkpoint-native.js +67 -2
  22. package/dist/commands/core-pipeline-driver-native.d.ts +12 -1
  23. package/dist/commands/core-pipeline-driver-native.js +131 -5
  24. package/dist/commands/core.js +13 -1
  25. package/dist/commands/crm.js +83 -6
  26. package/dist/commands/deploy-access.d.ts +33 -0
  27. package/dist/commands/deploy-access.js +322 -0
  28. package/dist/commands/dm-attach.d.ts +106 -0
  29. package/dist/commands/dm-attach.js +184 -0
  30. package/dist/commands/dm.d.ts +37 -0
  31. package/dist/commands/dm.js +172 -10
  32. package/dist/commands/feedback-show.d.ts +40 -0
  33. package/dist/commands/feedback-show.js +261 -0
  34. package/dist/commands/feedback.d.ts +21 -0
  35. package/dist/commands/feedback.js +152 -2
  36. package/dist/commands/files-browse.d.ts +16 -0
  37. package/dist/commands/files-browse.js +52 -1
  38. package/dist/commands/files.d.ts +24 -0
  39. package/dist/commands/files.js +52 -1
  40. package/dist/commands/integrations-connect.js +7 -3
  41. package/dist/commands/integrations-core.d.ts +13 -1
  42. package/dist/commands/integrations-core.js +49 -4
  43. package/dist/commands/integrations.js +4 -2
  44. package/dist/commands/lanes-accounts.js +16 -1
  45. package/dist/commands/lanes-loop.js +14 -4
  46. package/dist/commands/lanes-message.js +6 -4
  47. package/dist/commands/lanes-resume.js +36 -13
  48. package/dist/commands/lanes-shared.d.ts +5 -0
  49. package/dist/commands/lanes-shared.js +13 -3
  50. package/dist/commands/lanes-watch.js +1 -0
  51. package/dist/commands/lanes.d.ts +20 -1
  52. package/dist/commands/lanes.js +201 -51
  53. package/dist/commands/mesh.js +149 -1
  54. package/dist/commands/monitor.d.ts +1 -0
  55. package/dist/commands/monitor.js +4 -1
  56. package/dist/commands/repo.d.ts +41 -0
  57. package/dist/commands/repo.js +331 -0
  58. package/dist/commands/scan.d.ts +3 -1
  59. package/dist/commands/scan.js +7 -3
  60. package/dist/commands/secrets.d.ts +37 -0
  61. package/dist/commands/secrets.js +185 -16
  62. package/dist/commands/signals.js +17 -5
  63. package/dist/commands/staff/listings.d.ts +41 -0
  64. package/dist/commands/staff/listings.js +160 -0
  65. package/dist/commands/staff/reset-test-account.d.ts +3 -2
  66. package/dist/commands/staff/reset-test-account.js +4 -3
  67. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2629 -1036
  68. package/dist/hq-cloud-sync-runner/package.json +1 -1
  69. package/dist/lib/agent-kit/inbox-state.d.ts +7 -0
  70. package/dist/lib/agent-kit/inbox-state.js +5 -1
  71. package/dist/lib/agent-kit/mcp/tools.js +2 -1
  72. package/dist/lib/agent-kit/run/inbox.js +2 -1
  73. package/dist/lib/agent-kit/wake-secrets.d.ts +7 -0
  74. package/dist/lib/agent-kit/wake-secrets.js +32 -0
  75. package/dist/lib/agent-kit/wake.d.ts +4 -0
  76. package/dist/lib/agent-kit/wake.js +49 -3
  77. package/dist/lib/bot/inflight.d.ts +7 -1
  78. package/dist/lib/bot/inflight.js +10 -2
  79. package/dist/lib/bot/prompt.js +3 -0
  80. package/dist/lib/bot/run.js +72 -20
  81. package/dist/lib/bot/session.d.ts +14 -0
  82. package/dist/lib/bot/session.js +70 -0
  83. package/dist/lib/capacity/collectors/claude-oauth.d.ts +6 -0
  84. package/dist/lib/capacity/collectors/claude-oauth.js +12 -0
  85. package/dist/lib/capacity/lane-login.d.ts +6 -0
  86. package/dist/lib/capacity/lane-login.js +5 -0
  87. package/dist/lib/core-utils/worktree.js +2 -0
  88. package/dist/lib/daemon/exit-cause.d.ts +43 -0
  89. package/dist/lib/daemon/exit-cause.js +88 -0
  90. package/dist/lib/daemon/handoff.d.ts +2 -0
  91. package/dist/lib/daemon/handoff.js +6 -1
  92. package/dist/lib/daemon/hq-child-bootstrap.mjs +20 -3
  93. package/dist/lib/daemon/paths.d.ts +2 -0
  94. package/dist/lib/daemon/paths.js +1 -0
  95. package/dist/lib/daemon/run.js +13 -0
  96. package/dist/lib/daemon/supervisor.d.ts +54 -1
  97. package/dist/lib/daemon/supervisor.js +297 -21
  98. package/dist/lib/daemon/unit-holds.d.ts +22 -0
  99. package/dist/lib/daemon/unit-holds.js +86 -0
  100. package/dist/lib/doctor/checks/sync-health.d.ts +9 -0
  101. package/dist/lib/doctor/checks/sync-health.js +66 -1
  102. package/dist/lib/doctor/company-resolution-state.d.ts +30 -0
  103. package/dist/lib/doctor/company-resolution-state.js +89 -0
  104. package/dist/lib/doctor/fix/apply.d.ts +36 -6
  105. package/dist/lib/doctor/fix/apply.js +128 -12
  106. package/dist/lib/doctor/types.d.ts +1 -1
  107. package/dist/lib/doctor/types.js +1 -0
  108. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  109. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  110. package/dist/lib/hook-engine/resolve.js +6 -2
  111. package/dist/lib/index-render/company-knowledge.d.ts +1 -1
  112. package/dist/lib/index-render/company-knowledge.js +4 -5
  113. package/dist/lib/index-render/index.js +44 -3
  114. package/dist/lib/index-render/projects.d.ts +1 -1
  115. package/dist/lib/index-render/projects.js +5 -5
  116. package/dist/lib/lanes/account-company.d.ts +28 -0
  117. package/dist/lib/lanes/account-company.js +53 -0
  118. package/dist/lib/lanes/accounts.d.ts +22 -3
  119. package/dist/lib/lanes/accounts.js +38 -2
  120. package/dist/lib/lanes/claude-balance.d.ts +6 -1
  121. package/dist/lib/lanes/claude-balance.js +6 -1
  122. package/dist/lib/lanes/claude-credential.d.ts +78 -0
  123. package/dist/lib/lanes/claude-credential.js +163 -0
  124. package/dist/lib/lanes/compaction.d.ts +2 -0
  125. package/dist/lib/lanes/compaction.js +9 -0
  126. package/dist/lib/lanes/config.js +6 -0
  127. package/dist/lib/lanes/envelope-repair-dispatch.js +2 -1
  128. package/dist/lib/lanes/hooks-install.js +2 -1
  129. package/dist/lib/lanes/host-capacity.d.ts +2 -0
  130. package/dist/lib/lanes/host-capacity.js +1 -0
  131. package/dist/lib/lanes/keep-alive.d.ts +1 -1
  132. package/dist/lib/lanes/keep-alive.js +3 -1
  133. package/dist/lib/lanes/links.js +10 -4
  134. package/dist/lib/lanes/loop.js +16 -1
  135. package/dist/lib/lanes/node-binary.d.ts +17 -0
  136. package/dist/lib/lanes/node-binary.js +30 -0
  137. package/dist/lib/lanes/outpost-dispatch.d.ts +2 -1
  138. package/dist/lib/lanes/outpost-dispatch.js +1 -0
  139. package/dist/lib/lanes/reconcile.js +4 -0
  140. package/dist/lib/lanes/remote-control-lane.js +2 -0
  141. package/dist/lib/lanes/spawn.d.ts +3 -0
  142. package/dist/lib/lanes/spawn.js +119 -9
  143. package/dist/lib/lanes/store.d.ts +6 -0
  144. package/dist/lib/lanes/store.js +107 -18
  145. package/dist/lib/lanes/types.d.ts +2 -0
  146. package/dist/lib/lanes/watch.js +28 -5
  147. package/dist/lib/lanes/watcher-main.js +4 -3
  148. package/dist/lib/lanes/watcher.d.ts +4 -0
  149. package/dist/lib/lanes/watcher.js +110 -21
  150. package/dist/lib/mesh/api.d.ts +32 -0
  151. package/dist/lib/mesh/api.js +75 -0
  152. package/dist/lib/secret-scan/detect.js +9 -0
  153. package/dist/lib/secret-scan/repo-config.d.ts +41 -0
  154. package/dist/lib/secret-scan/repo-config.js +237 -0
  155. package/dist/lib/secret-scan/rules.d.ts +7 -1
  156. package/dist/lib/secret-scan/rules.js +3 -1
  157. package/dist/lib/secret-scan/staged.d.ts +5 -0
  158. package/dist/lib/secret-scan/staged.js +19 -2
  159. package/dist/lib/secret-scan/types.d.ts +2 -0
  160. package/dist/lib/sync/server-owned-local-only.d.ts +29 -0
  161. package/dist/lib/sync/server-owned-local-only.js +75 -0
  162. package/dist/sentry-before-send.js +1 -0
  163. package/dist/utils/access-denied-hint.js +6 -0
  164. package/dist/utils/client-health.d.ts +34 -0
  165. package/dist/utils/client-health.js +96 -3
  166. package/dist/utils/package-use-lease.js +27 -9
  167. package/dist/utils/version-gate.d.ts +7 -0
  168. package/dist/utils/version-gate.js +66 -0
  169. package/package.json +8 -8
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import chalk from "chalk";
3
- import { ensureCognitoToken } from "../utils/cognito-session.js";
3
+ import { ensureCognitoToken, loadCachedTokens } from "../utils/cognito-session.js";
4
4
  import { assertVaultApiRequestSupported, vaultApiFetch, } from "../utils/vault-api.js";
5
5
  import { collectDiagnostics } from "../utils/feedback-diagnostics.js";
6
6
  import { MAX_SCREENSHOTS, uploadScreenshots } from "../utils/feedback-screenshots.js";
@@ -8,6 +8,7 @@ import { FEEDBACK_LOGS_MAX_TOTAL_BYTES, FEEDBACK_LOGS_MIN_USEFUL_BYTES, collectF
8
8
  import { uploadLogBundle } from "../utils/feedback-log-bundle.js";
9
9
  import { resolveProcessFlagGate } from "../lib/flag-registry.js";
10
10
  import { cachedLatestVersion } from "../utils/version-check.js";
11
+ import { registerFeedbackShowCommand } from "./feedback-show.js";
11
12
  export const FEEDBACK_LATEST_VERSION_FLAG = "feedback.latest-version";
12
13
  export const FEEDBACK_LATEST_VERSION_FLAG_FALLBACK = false;
13
14
  export const FEEDBACK_REPORTER_KIND_FLAG = "feedback.reporter-kind";
@@ -199,7 +200,151 @@ export async function submitFeedback(opts) {
199
200
  throw new Error(`Failed to submit feedback: ${errMsg}`);
200
201
  }
201
202
  const data = (await res.json());
202
- return { id: data.id };
203
+ return {
204
+ id: data.id,
205
+ ...(typeof data.publicUrl === "string" && data.publicUrl.length > 0
206
+ ? { publicUrl: data.publicUrl }
207
+ : {}),
208
+ };
209
+ }
210
+ // ---------------------------------------------------------------------------
211
+ // Public ticket pages: `hq feedback link <id>` and `hq feedback update <id>`
212
+ // ---------------------------------------------------------------------------
213
+ /** Customer-visible states accepted by `hq feedback update --state`. */
214
+ export const PUBLIC_TICKET_STATES = [
215
+ "triaged",
216
+ "in-progress",
217
+ "shipped",
218
+ "verified",
219
+ "resolved",
220
+ ];
221
+ const FEEDBACK_ID_RE = /^feedback_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
222
+ export function assertFeedbackId(id) {
223
+ if (!FEEDBACK_ID_RE.test(id)) {
224
+ throw new Error(`Invalid feedback id '${id}': expected the full feedback_<uuid> id printed by hq feedback.`);
225
+ }
226
+ }
227
+ /**
228
+ * The staff feedback routes gate on the id token's email claim, so send the
229
+ * cached id token when a person session has one. Machine identities already
230
+ * get their id token back from ensureCognitoToken.
231
+ */
232
+ async function resolveStaffToken() {
233
+ const token = await ensureCognitoToken({ interactive: false });
234
+ const cached = loadCachedTokens();
235
+ return typeof cached?.idToken === "string" && cached.idToken.length > 0
236
+ ? cached.idToken
237
+ : token;
238
+ }
239
+ async function readFeedbackError(res) {
240
+ const data = await res.json().catch(() => ({}));
241
+ return data &&
242
+ typeof data === "object" &&
243
+ !Array.isArray(data) &&
244
+ typeof data.error === "string"
245
+ ? data.error
246
+ : res.statusText;
247
+ }
248
+ function publicUrlFrom(data) {
249
+ if (!data || typeof data !== "object" || Array.isArray(data))
250
+ return null;
251
+ const url = data.publicUrl;
252
+ return typeof url === "string" && url.length > 0 ? url : null;
253
+ }
254
+ /** Resolve (and lazily mint) the public tracking link for a ticket. */
255
+ export async function fetchFeedbackLink(opts) {
256
+ const res = await vaultApiFetch({
257
+ token: opts.token,
258
+ path: `/v1/feedback/${opts.id}`,
259
+ method: "PATCH",
260
+ body: { ensurePublicToken: true },
261
+ });
262
+ if (!res.ok) {
263
+ throw new Error(`Failed to get the public link for ${opts.id}: ${await readFeedbackError(res)}`);
264
+ }
265
+ const url = publicUrlFrom(await res.json());
266
+ if (!url) {
267
+ throw new Error(`The server did not return a public link for ${opts.id}. Public ticket pages may not be deployed yet.`);
268
+ }
269
+ return url;
270
+ }
271
+ /** Post a customer-visible update; the server also mirrors it into #hq-feedback. */
272
+ export async function postFeedbackUpdate(opts) {
273
+ const res = await vaultApiFetch({
274
+ token: opts.token,
275
+ path: `/v1/feedback/${opts.id}`,
276
+ method: "PATCH",
277
+ body: { publicUpdate: { state: opts.state, note: opts.note } },
278
+ });
279
+ if (!res.ok) {
280
+ throw new Error(`Failed to update ${opts.id}: ${await readFeedbackError(res)}`);
281
+ }
282
+ const data = (await res.json());
283
+ return {
284
+ publicUrl: publicUrlFrom(data),
285
+ slack: typeof data.publicUpdateSlack === "string" ? data.publicUpdateSlack : null,
286
+ };
287
+ }
288
+ function registerLinkSubcommand(feedbackCmd) {
289
+ feedbackCmd
290
+ .command("link")
291
+ .description("Print the public tracking link for a ticket (status.hqforwork.com)")
292
+ .argument("<id>", "Feedback id (feedback_<uuid>)")
293
+ .action(async (id) => {
294
+ try {
295
+ assertFeedbackId(id);
296
+ const token = await resolveStaffToken();
297
+ assertVaultApiRequestSupported({ token, path: `/v1/feedback/${id}` });
298
+ const url = await fetchFeedbackLink({ id, token });
299
+ console.log(`Track: ${url}`);
300
+ }
301
+ catch (err) {
302
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
303
+ process.exit(1);
304
+ }
305
+ });
306
+ }
307
+ function registerUpdateSubcommand(feedbackCmd) {
308
+ feedbackCmd
309
+ .command("update")
310
+ .description("Post a customer-visible status update on a ticket's public page and its #hq-feedback thread")
311
+ .argument("<id>", "Feedback id (feedback_<uuid>)")
312
+ .requiredOption("--state <state>", `One of: ${PUBLIC_TICKET_STATES.join(", ")}`)
313
+ .requiredOption("--note <text>", "What the customer should read (plain text, up to 2000 characters)")
314
+ .action(async (id, opts) => {
315
+ try {
316
+ assertFeedbackId(id);
317
+ if (!PUBLIC_TICKET_STATES.includes(opts.state)) {
318
+ throw new Error(`Invalid --state '${opts.state}': expected one of ${PUBLIC_TICKET_STATES.join(", ")}.`);
319
+ }
320
+ const note = opts.note.trim();
321
+ if (note.length === 0)
322
+ throw new Error("--note must not be empty.");
323
+ if (note.length > 2000)
324
+ throw new Error("--note must be at most 2000 characters.");
325
+ const token = await resolveStaffToken();
326
+ assertVaultApiRequestSupported({ token, path: `/v1/feedback/${id}` });
327
+ const result = await postFeedbackUpdate({
328
+ id,
329
+ token,
330
+ state: opts.state,
331
+ note,
332
+ });
333
+ console.log(`Updated: ${id} -> ${opts.state}`);
334
+ if (result.publicUrl)
335
+ console.log(`Track: ${result.publicUrl}`);
336
+ if (result.slack === "failed") {
337
+ console.error(chalk.yellow("Warning:"), "the update was saved but the #hq-feedback thread post failed.");
338
+ }
339
+ else if (result.slack === "skipped") {
340
+ console.error(chalk.yellow("Note:"), "the ticket has no #hq-feedback thread, so nothing was posted to Slack.");
341
+ }
342
+ }
343
+ catch (err) {
344
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
345
+ process.exit(1);
346
+ }
347
+ });
203
348
  }
204
349
  function registerSubcommand(feedbackCmd, type) {
205
350
  feedbackCmd
@@ -284,6 +429,8 @@ function registerSubcommand(feedbackCmd, type) {
284
429
  logsEnabled,
285
430
  });
286
431
  console.log(`Submitted: ${result.id}`);
432
+ if (result.publicUrl)
433
+ console.log(`Track: ${result.publicUrl}`);
287
434
  }
288
435
  catch (err) {
289
436
  console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
@@ -297,5 +444,8 @@ export function registerFeedbackCommand(program) {
297
444
  .description("Submit a bug report or feature request to HQ");
298
445
  registerSubcommand(feedbackCmd, "bug");
299
446
  registerSubcommand(feedbackCmd, "feature");
447
+ registerLinkSubcommand(feedbackCmd);
448
+ registerUpdateSubcommand(feedbackCmd);
449
+ registerFeedbackShowCommand(feedbackCmd);
300
450
  }
301
451
  //# sourceMappingURL=feedback.js.map
@@ -443,6 +443,22 @@ export declare const PRESIGN_BUFFER_MAX_BYTES: number;
443
443
  * access token + companyUid are captured here; the orchestrator just calls
444
444
  * `send(...)` as if it held an S3 client.
445
445
  */
446
+ /** Error code for a presigned storage download that failed after the API authorized it. */
447
+ export declare const STORAGE_DOWNLOAD_FAILED_CODE = "FILES_STORAGE_DOWNLOAD_FAILED";
448
+ /**
449
+ * Build the error for a failed presigned download. The API already checked the
450
+ * caller's ACL before it signed the URL, so a failure here is a storage or
451
+ * network outcome, not a missing grant. The error carries no `status: 403` and
452
+ * a code that `isAccessDeniedError` treats as not an access denial, so the CLI
453
+ * does not send the caller to `hq access` (which only re-checks the ACL that
454
+ * already passed).
455
+ *
456
+ * A response that really came from S3 carries `x-amz-request-id`; we report
457
+ * S3's error code from the XML body. A response without it came from something
458
+ * between this machine and the storage host, usually a sandbox egress proxy or
459
+ * firewall that does not allow the bucket host, and we say so.
460
+ */
461
+ export declare function presignedDownloadError(key: string, url: string, dl: Response): Promise<Error>;
446
462
  export declare function createCompanyPresignClient(input: {
447
463
  token: string;
448
464
  companyUid: string;
@@ -709,6 +709,57 @@ export const PRESIGN_BUFFER_MAX_BYTES = 32 * 1024 * 1024;
709
709
  * access token + companyUid are captured here; the orchestrator just calls
710
710
  * `send(...)` as if it held an S3 client.
711
711
  */
712
+ /** Error code for a presigned storage download that failed after the API authorized it. */
713
+ export const STORAGE_DOWNLOAD_FAILED_CODE = "FILES_STORAGE_DOWNLOAD_FAILED";
714
+ /**
715
+ * Build the error for a failed presigned download. The API already checked the
716
+ * caller's ACL before it signed the URL, so a failure here is a storage or
717
+ * network outcome, not a missing grant. The error carries no `status: 403` and
718
+ * a code that `isAccessDeniedError` treats as not an access denial, so the CLI
719
+ * does not send the caller to `hq access` (which only re-checks the ACL that
720
+ * already passed).
721
+ *
722
+ * A response that really came from S3 carries `x-amz-request-id`; we report
723
+ * S3's error code from the XML body. A response without it came from something
724
+ * between this machine and the storage host, usually a sandbox egress proxy or
725
+ * firewall that does not allow the bucket host, and we say so.
726
+ */
727
+ export async function presignedDownloadError(key, url, dl) {
728
+ let host = "the storage host";
729
+ try {
730
+ host = new URL(url).host;
731
+ }
732
+ catch {
733
+ // keep the generic label
734
+ }
735
+ const requestId = dl.headers.get("x-amz-request-id");
736
+ const bodyText = (await dl.text().catch(() => "")).slice(0, 4096);
737
+ const fromStorage = requestId !== null && requestId.length > 0;
738
+ let message;
739
+ let storageCode;
740
+ if (fromStorage) {
741
+ storageCode = /<Code>([^<]{1,100})<\/Code>/.exec(bodyText)?.[1];
742
+ message =
743
+ `Failed to download '${key}' (HTTP ${dl.status}` +
744
+ (storageCode ? `, storage error ${storageCode}` : "") +
745
+ `, request id ${requestId}). Your file access was granted; the storage ` +
746
+ `service refused the signed download. Report this with: hq feedback bug`;
747
+ }
748
+ else {
749
+ message =
750
+ `Failed to download '${key}' (HTTP ${dl.status}). Your file access was ` +
751
+ `granted, but the response did not come from ${host}. A proxy or network ` +
752
+ `egress rule on this machine likely blocked it. Allow outbound HTTPS to ` +
753
+ `${host} (HQ vault files are served from *.s3.us-east-1.amazonaws.com).`;
754
+ }
755
+ return Object.assign(new Error(message), {
756
+ code: STORAGE_DOWNLOAD_FAILED_CODE,
757
+ key,
758
+ httpStatus: dl.status,
759
+ fromStorage,
760
+ ...(storageCode ? { storageCode } : {}),
761
+ });
762
+ }
712
763
  export function createCompanyPresignClient(input) {
713
764
  const { token, companyUid, companySlug, baseUrl } = input;
714
765
  const denied = (message, key, extra) => Object.assign(new Error(message), {
@@ -780,7 +831,7 @@ export function createCompanyPresignClient(input) {
780
831
  }
781
832
  const dl = await fetch(first.url, { signal: AbortSignal.timeout(120_000) });
782
833
  if (!dl.ok) {
783
- throw new Error(`Failed to download '${key}' (HTTP ${dl.status})`);
834
+ throw await presignedDownloadError(key, first.url, dl);
784
835
  }
785
836
  // HQ-CLI-5: when the response declares a length at or below the ceiling,
786
837
  // DRAIN it here — read the whole body before send() resolves and replay it
@@ -9,6 +9,30 @@ export declare const FILES_SHARE_EMPTY_PREFIX_WARNING_FLAG = "files.share-empty-
9
9
  export declare function parseDuration(input: string): number | null;
10
10
  /** PRD upper bound on browser-launch share-session expiry. */
11
11
  export declare const MAX_SHARE_SESSION_EXPIRY_MS: number;
12
+ /**
13
+ * A more specific ACL row under a prefix, as hq-pro reports it. File ACL
14
+ * resolution picks the single most specific row that covers a key, so each of
15
+ * these rows replaces the broader prefix's grants for the keys it covers
16
+ * (feedback_466fe8b8).
17
+ */
18
+ export interface AclMaskingRow {
19
+ path: string;
20
+ open: boolean;
21
+ creatorUid: string;
22
+ grantees: Array<{
23
+ granteeType: string;
24
+ granteeId: string;
25
+ permission: string;
26
+ }>;
27
+ }
28
+ /** The warning `hq files share` prints when a grant is shadowed by more specific rows. */
29
+ export declare function formatShadowedGrantWarning(input: {
30
+ prefix: string;
31
+ principal: string;
32
+ permission: string;
33
+ companySlug?: string;
34
+ maskedBy: AclMaskingRow[];
35
+ }): string;
12
36
  export interface ShareSessionResponse {
13
37
  url: string;
14
38
  token?: string;
@@ -9,6 +9,7 @@ import { looksLikeCompanyUid } from "../utils/vault-api.js";
9
9
  import { AuthError } from "../utils/auth-error.js";
10
10
  import { formatAccessDenied, offerAccessLadder, } from "../utils/access-denied-hint.js";
11
11
  import { fetchChannels, matchChannelsByName } from "./dm.js";
12
+ import { suggestChannels, withChannelSuggestions } from "./channel-suggest.js";
12
13
  import { resolveCompanySlug, resolvePersonWithRosterFallback, } from "./people.js";
13
14
  import { findHqRoot } from "../utils/manifest.js";
14
15
  import { maxLength } from "../utils/table-width.js";
@@ -55,6 +56,39 @@ function privateFolderExplanation(prefix) {
55
56
  return (`Private folder '${prefix}' — write can create only a new direct child. ` +
56
57
  "Children are creator-only; this grant does not read, list, overwrite, or delete them.");
57
58
  }
59
+ function describeMaskingGrantees(row) {
60
+ if (row.open)
61
+ return "open (all members read)";
62
+ if (row.grantees.length === 0)
63
+ return "none (creator only)";
64
+ return row.grantees
65
+ .map((g) => g.granteeType === "company-wide"
66
+ ? `everyone:${g.permission}`
67
+ : `${g.granteeId}:${g.permission}`)
68
+ .join(", ");
69
+ }
70
+ function printMaskingTable(rows) {
71
+ const PATH_W = Math.max(4, ...rows.map((r) => displayAclPattern(r.path).length));
72
+ const CREATOR_W = Math.max(7, ...rows.map((r) => r.creatorUid.length));
73
+ console.log(chalk.bold(["PATH".padEnd(PATH_W), "CREATOR".padEnd(CREATOR_W), "GRANTS"].join(" ")));
74
+ for (const row of rows) {
75
+ console.log([
76
+ displayAclPattern(row.path).padEnd(PATH_W),
77
+ row.creatorUid.padEnd(CREATOR_W),
78
+ describeMaskingGrantees(row),
79
+ ].join(" "));
80
+ }
81
+ }
82
+ /** The warning `hq files share` prints when a grant is shadowed by more specific rows. */
83
+ export function formatShadowedGrantWarning(input) {
84
+ const paths = input.maskedBy.map((row) => `'${row.path}'`).join(", ");
85
+ const company = input.companySlug ? ` --company ${input.companySlug}` : "";
86
+ return (`Warning: this grant takes no effect under ${input.maskedBy.length} more specific rule(s) ` +
87
+ `that do not include ${input.principal}: ${paths}. ` +
88
+ "Access under those paths is unchanged (the most specific rule wins). " +
89
+ `Share them too, for example: hq files share '${input.maskedBy[0]?.path ?? "<path>"}' --with ${input.principal} --permission ${input.permission}${company}` +
90
+ " — or remove the rule with hq files unshare.");
91
+ }
58
92
  function formatAclPatternConflict(prefix, serverMessage) {
59
93
  const privatePattern = prefix.endsWith("/*")
60
94
  ? prefix.slice(0, -1)
@@ -544,6 +578,14 @@ export function registerFilesCommand(program) {
544
578
  console.log("\nGranted on descendant prefixes (do not affect this prefix's access):");
545
579
  printEntryTable(tree.children, true);
546
580
  }
581
+ if (Array.isArray(tree.masking) && tree.masking.length > 0) {
582
+ console.log(`\nMore specific rules under ${displayAclPattern(tree.prefix)} (each replaces this prefix's grants for its own paths):`);
583
+ printMaskingTable(tree.masking);
584
+ if (tree.maskingTruncated) {
585
+ console.log(chalk.gray(" ...list truncated; use --json for the first page."));
586
+ }
587
+ console.log(chalk.gray(`A grant on ${displayAclPattern(tree.prefix)} does not reach these paths unless they grant the same principal. Share each path too, or remove its rule with hq files unshare.`));
588
+ }
547
589
  const allEntries = [...tree.direct, ...tree.inherited, ...tree.children];
548
590
  if (allEntries.some((entry) => entry.granteeType === "company-wide")) {
549
591
  console.log(chalk.gray("\nCompany-wide grants apply to every active member, including guests. Demoting someone to guest does not exclude them. Use hq members remove to end membership, or hq access <path> --as <email> to see effective access."));
@@ -704,7 +746,7 @@ async function resolveShareChannelTarget(params) {
704
746
  const channels = await fetchChannels(token);
705
747
  const matches = matchChannelsByName(channels, params.channelName);
706
748
  if (matches.length === 0) {
707
- console.error(chalk.red(`No channel named '${params.channelName}' — run \`hq channels\` to see your channels.`));
749
+ console.error(chalk.red(withChannelSuggestions(`No channel named '${params.channelName}' — run \`hq channels\` to see your channels.`, suggestChannels(channels, params.channelName))));
708
750
  process.exit(1);
709
751
  }
710
752
  if (matches.length > 1) {
@@ -894,6 +936,15 @@ async function runDirectGrant(params) {
894
936
  if (isPrivateFolderPattern(printedPrefix)) {
895
937
  console.log(chalk.gray(privateFolderExplanation(printedPrefix)));
896
938
  }
939
+ if (Array.isArray(data.maskedBy) && data.maskedBy.length > 0) {
940
+ console.warn(chalk.yellow(formatShadowedGrantWarning({
941
+ prefix: printedPrefix,
942
+ principal: isAll ? "@all" : principal,
943
+ permission: params.permission,
944
+ ...(params.companySlug ? { companySlug: params.companySlug } : {}),
945
+ maskedBy: data.maskedBy,
946
+ })));
947
+ }
897
948
  if (typeof data.record?.error === "string" &&
898
949
  typeof data.record.status === "number" &&
899
950
  data.record.status >= 400) {
@@ -548,12 +548,16 @@ async function connectViaOAuth(token, companyUid, target, opts) {
548
548
  ...target.ref,
549
549
  ...(requestedScopes ? { scopes: requestedScopes } : {}),
550
550
  };
551
- if (opts.linkOnly)
552
- return await linkOnlySignIn(token, companyUid, target, opts, startInput);
551
+ const userClient = await resolveUserOAuthClient(opts, target.label);
552
+ if (opts.linkOnly) {
553
+ return await linkOnlySignIn(token, companyUid, target, opts, {
554
+ ...startInput,
555
+ ...(userClient ?? {}),
556
+ });
557
+ }
553
558
  // A user-registered app pins the console callback and skips the loopback
554
559
  // entirely — see `userClientSignIn` for why an ephemeral port cannot be the
555
560
  // redirect URI here.
556
- const userClient = await resolveUserOAuthClient(opts, target.label);
557
561
  if (userClient)
558
562
  return await userClientSignIn(token, companyUid, target, opts, userClient, requestedScopes);
559
563
  // A loopback listener only works if the browser runs on THIS machine. Over
@@ -120,6 +120,16 @@ interface GatewayMessage {
120
120
  }
121
121
  export type GatewayErrorShape = "string_error" | "object_no_code" | "object_code" | "string_code" | "nested_code" | "empty" | "redacted_empty";
122
122
  export type GatewayFailureCode = "invalid_envelope";
123
+ /** Default client wait for a background (202) integration job. */
124
+ export declare const ASYNC_GATEWAY_DEADLINE_MS: number;
125
+ /** Upper bound for `hq integrations call --timeout`, in seconds. */
126
+ export declare const MAX_CALL_TIMEOUT_SECONDS: number;
127
+ /**
128
+ * Parse `--timeout <seconds>` for `hq integrations call` into milliseconds.
129
+ * Bounds how long the CLI waits for a background job; it never shortens the
130
+ * gateway's own limits.
131
+ */
132
+ export declare function parseCallTimeoutMs(raw: string | undefined): number;
123
133
  export declare class IntegrationsCliError extends Error {
124
134
  /**
125
135
  * True when the error is the caller's request/state/permission (a client 4xx
@@ -338,7 +348,9 @@ export declare function resolveConnection(token: string, companyUid: string, app
338
348
  }, recoveryOpts?: {
339
349
  allowSingleRevoked?: boolean;
340
350
  }): Promise<AdminConnection>;
341
- export declare function callGateway(token: string, params: Record<string, unknown>, idempotencyKey?: string, method?: "tools/call" | "tools/list"): Promise<GatewayMessage>;
351
+ export declare function callGateway(token: string, params: Record<string, unknown>, idempotencyKey?: string, method?: "tools/call" | "tools/list", opts?: {
352
+ waitMs?: number;
353
+ }): Promise<GatewayMessage>;
342
354
  /**
343
355
  * Gateway results arrive MCP-style: `{ content: [{ type: "text", text }] }`
344
356
  * where `text` is the provider's JSON. Unwrap to the inner payload; fall back
@@ -19,7 +19,43 @@ import { connectionHealthNotice } from "../lib/integrations/health.js";
19
19
  import { bareProvider } from "../lib/integrations/provider-slug.js";
20
20
  export { bareProvider } from "../lib/integrations/provider-slug.js";
21
21
  const ASYNC_GATEWAY_POLL_INTERVAL_MS = 1_000;
22
- const ASYNC_GATEWAY_DEADLINE_MS = 4 * 60_000 + 30_000;
22
+ /** Default client wait for a background (202) integration job. */
23
+ export const ASYNC_GATEWAY_DEADLINE_MS = 4 * 60_000 + 30_000;
24
+ /** Upper bound for `hq integrations call --timeout`, in seconds. */
25
+ export const MAX_CALL_TIMEOUT_SECONDS = 30 * 60;
26
+ /**
27
+ * Parse `--timeout <seconds>` for `hq integrations call` into milliseconds.
28
+ * Bounds how long the CLI waits for a background job; it never shortens the
29
+ * gateway's own limits.
30
+ */
31
+ export function parseCallTimeoutMs(raw) {
32
+ if (raw === undefined)
33
+ return ASYNC_GATEWAY_DEADLINE_MS;
34
+ const trimmed = raw.trim();
35
+ if (!/^\d+$/.test(trimmed)) {
36
+ throw new IntegrationsCliError(`--timeout must be a whole number of seconds (got "${raw}").`, { expected: true });
37
+ }
38
+ const seconds = Number(trimmed);
39
+ if (seconds < 1 || seconds > MAX_CALL_TIMEOUT_SECONDS) {
40
+ throw new IntegrationsCliError(`--timeout must be between 1 and ${MAX_CALL_TIMEOUT_SECONDS} seconds (got ${seconds}).`, { expected: true });
41
+ }
42
+ return seconds * 1_000;
43
+ }
44
+ /**
45
+ * Statuses the gateway returns when an inline (non-background) tools/call ran
46
+ * into the IntegrationMcpFunction 30s wall or its provider budget.
47
+ */
48
+ function isInlineTimeoutStatus(status) {
49
+ return status === 500 || status === 502 || status === 504;
50
+ }
51
+ function inlineTimeoutHint(status) {
52
+ return (`${upstreamUnavailableMessage(status)} ` +
53
+ "If this was a long-running call (for example a large analytical read), it may have hit the " +
54
+ "gateway's 30-second limit: the gateway ran it inline, not as a background job. Long calls " +
55
+ "run as background jobs only when async integration jobs are enabled for your company; ask " +
56
+ "HQ support to enable them. Once enabled, hq integrations call waits for the result " +
57
+ "automatically (see --timeout).");
58
+ }
23
59
  export class IntegrationsCliError extends Error {
24
60
  /**
25
61
  * True when the error is the caller's request/state/permission (a client 4xx
@@ -627,9 +663,9 @@ function gatewaySubcommandFor(params, method) {
627
663
  return "tools_call";
628
664
  return "other";
629
665
  }
630
- export async function callGateway(token, params, idempotencyKey, method = "tools/call") {
666
+ export async function callGateway(token, params, idempotencyKey, method = "tools/call", opts = {}) {
631
667
  const gatewaySubcommand = gatewaySubcommandFor(params, method);
632
- const deadline = Date.now() + ASYNC_GATEWAY_DEADLINE_MS;
668
+ const deadline = Date.now() + (opts.waitMs ?? ASYNC_GATEWAY_DEADLINE_MS);
633
669
  const res = await vaultApiFetch({
634
670
  token,
635
671
  path: "/v1/integrations/mcp",
@@ -663,6 +699,15 @@ export async function callGateway(token, params, idempotencyKey, method = "tools
663
699
  }
664
700
  if (res.status !== 202 && (!res.ok || !terminalMessage)) {
665
701
  raiseIfUnauthorized(res);
702
+ if (method === "tools/call" &&
703
+ params.acceptAsync === true &&
704
+ isInlineTimeoutStatus(res.status)) {
705
+ throw new IntegrationsCliError(inlineTimeoutHint(res.status), {
706
+ expected: true,
707
+ status: res.status,
708
+ gatewaySubcommand,
709
+ });
710
+ }
666
711
  raiseIfUpstreamUnavailable(res);
667
712
  // Carry the HTTP status so a reportable transport failure (e.g. a 501 that
668
713
  // is neither 401 nor an upstream-availability status, so it is captured)
@@ -773,7 +818,7 @@ async function pollAsyncGatewayJob(token, companyUid, jobId, idempotencyKey, dea
773
818
  break;
774
819
  await new Promise((resolve) => setTimeout(resolve, Math.min(ASYNC_GATEWAY_POLL_INTERVAL_MS, remainingMs)));
775
820
  }
776
- throw new IntegrationsCliError(`Timed out waiting for integration job ${jobId}; the server job may still finish. To safely retry, rerun with --idempotency-key ${idempotencyKey}.`, { expected: true });
821
+ throw new IntegrationsCliError(`Timed out waiting for integration job ${jobId}; the server job may still finish. To safely retry, rerun with --idempotency-key ${idempotencyKey} (add --timeout <seconds> to wait longer).`, { expected: true });
777
822
  }
778
823
  /**
779
824
  * Gateway results arrive MCP-style: `{ content: [{ type: "text", text }] }`
@@ -56,7 +56,7 @@ import chalk from "chalk";
56
56
  import { ensureCognitoIdToken } from "../utils/cognito-session.js";
57
57
  import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
58
58
  import { redactSensitiveUrl, redactSensitiveUrls, redactUrlsInText } from "../utils/redact-url.js";
59
- import { IntegrationsCliError, bareProvider, callGateway, fetchConnections, fetchUsableApps, isBuiltInGatewayProvider, isClientError, printHealthNotice, printJson, printIntegrationCallResultJson, raiseIfUnauthorized, raiseIfUpstreamUnavailable, gatewayResultIsError, selectConnection, revokedConnectionDetails, toolPrefixForProvider, unwrapGatewayResult, queuedOutcome, } from "./integrations-core.js";
59
+ import { IntegrationsCliError, bareProvider, callGateway, parseCallTimeoutMs, fetchConnections, fetchUsableApps, isBuiltInGatewayProvider, isClientError, printHealthNotice, printJson, printIntegrationCallResultJson, raiseIfUnauthorized, raiseIfUpstreamUnavailable, gatewayResultIsError, selectConnection, revokedConnectionDetails, toolPrefixForProvider, unwrapGatewayResult, queuedOutcome, } from "./integrations-core.js";
60
60
  import { registerConnectCommands } from "./integrations-connect.js";
61
61
  import { registerImportCommands } from "./integrations-import.js";
62
62
  import { registerManageCommands } from "./integrations-manage.js";
@@ -483,8 +483,10 @@ export function registerIntegrationsCommand(program) {
483
483
  .option("--args <json>", "Tool arguments as a JSON object (default {}); use - to read the JSON from stdin")
484
484
  .option("--args-file <path>", "Read tool arguments from a JSON file (use - for stdin); avoids shell quoting of apostrophes and parentheses")
485
485
  .option("--idempotency-key <key>", "Stable key so retries execute at most once (default: generated)")
486
+ .option("--timeout <seconds>", "How long to wait for a long call that runs as a background job (default 270, max 1800)")
486
487
  .option("--json", "Machine-readable output")
487
488
  .action(async (tool, opts) => {
489
+ const waitMs = parseCallTimeoutMs(opts.timeout);
488
490
  const parsedArgs = await resolveCallArgs(opts);
489
491
  const token = await ensureCognitoIdToken();
490
492
  const companyUid = await getCompanyUid(token, opts.company);
@@ -522,7 +524,7 @@ export function registerIntegrationsCommand(program) {
522
524
  },
523
525
  };
524
526
  }
525
- const message = await callGateway(token, gatewayParams, idempotencyKey);
527
+ const message = await callGateway(token, gatewayParams, idempotencyKey, "tools/call", { waitMs });
526
528
  const payload = unwrapGatewayResult(message.result);
527
529
  const queued = queuedOutcome(payload);
528
530
  if (queued) {
@@ -9,9 +9,11 @@ import { assertUniqueAccountHomes, providerCompactAfterMs } from "../lib/lanes/c
9
9
  import { resolveLaneEffort } from "../lib/lanes/effort.js";
10
10
  import { LanesError } from "../lib/lanes/errors.js";
11
11
  import { CAPACITY_ONLY_REASON } from "../lib/capacity/accounts.js";
12
+ import { claudeOpenCommand } from "../lib/capacity/ledger.js";
12
13
  import { accountQuotaView, clearAccountQuota, loadQuotaState } from "../lib/lanes/quota.js";
13
14
  import { resolveLaneModel } from "../lib/lanes/spawn.js";
14
15
  import { leadLaneIds } from "../lib/lanes/admission.js";
16
+ import { resolveLaneCompany } from "../lib/lanes/lane-company.js";
15
17
  export function createLaneAccountsHandlers(deps) {
16
18
  const { admitQueuedFromCtx, automaticBalanceMoveHistoryReason, ensureDir, laneWithRecoveredProviderSession, loadClaudeBalanceHostSnapshots, parseRemoteJson, prepareAccountMove, printJson, reconcileHostFromCtx, resolveCallerPath } = deps;
17
19
  const VENDOR_LOGIN = {
@@ -124,6 +126,7 @@ export function createLaneAccountsHandlers(deps) {
124
126
  hostname: ctx.hostname(),
125
127
  homedir: ctx.homedir,
126
128
  probe: ctx.probeProvider,
129
+ readClaudeCredential: ctx.readClaudeCredential,
127
130
  env: ctx.env,
128
131
  }).map((status) => {
129
132
  const accountCfg = config.providers[provider].accounts[status.id];
@@ -146,6 +149,10 @@ export function createLaneAccountsHandlers(deps) {
146
149
  error: "home_missing",
147
150
  fix_command: `hq lanes accounts set-home ${provider} ${status.id} <host-home>`,
148
151
  } : {}),
152
+ ...(provider === "claude" && status.sign_in_state === "expired" ? {
153
+ error: "sign_in_expired",
154
+ fix_command: `${claudeOpenCommand(status.home, ctx.homedir())} then run /login`,
155
+ } : {}),
149
156
  ...(provider === "claude" ? {
150
157
  identity: status.identity ?? null,
151
158
  credential_matches_interactive: differs === null ? null : !differs,
@@ -192,7 +199,11 @@ export function createLaneAccountsHandlers(deps) {
192
199
  const balanceGateEnabled = true;
193
200
  const recentUsageEnabled = await deps.readClaudeAccountBalanceSignalsEnabled(ctx);
194
201
  const loads = accountLoadsForReport({ hosts: loaded.snapshots, config, quotaState });
195
- const suggestions = planClaudeBalanceMoves({ hosts: loaded.snapshots, config });
202
+ const suggestions = planClaudeBalanceMoves({
203
+ hosts: loaded.snapshots,
204
+ config,
205
+ laneCompany: (lane) => resolveLaneCompany(hqRoot, lane).company,
206
+ });
196
207
  const selections = loaded.snapshots.map((host) => {
197
208
  try {
198
209
  const selection = selectClaudeAccount({
@@ -428,6 +439,7 @@ export function createLaneAccountsHandlers(deps) {
428
439
  existsSync: ctx.existsSync,
429
440
  hostname,
430
441
  probe: ctx.probeProvider,
442
+ readClaudeCredential: ctx.readClaudeCredential,
431
443
  env: ctx.env,
432
444
  });
433
445
  if (status.logged_in !== true) {
@@ -567,6 +579,7 @@ export function createLaneAccountsHandlers(deps) {
567
579
  existsSync: ctx.existsSync,
568
580
  hostname: ctx.hostname(),
569
581
  probe: ctx.probeProvider,
582
+ readClaudeCredential: ctx.readClaudeCredential,
570
583
  env: ctx.env,
571
584
  });
572
585
  if (status.logged_in !== true) {
@@ -584,6 +597,7 @@ export function createLaneAccountsHandlers(deps) {
584
597
  existsSync: ctx.existsSync,
585
598
  hostname: ctx.hostname(),
586
599
  probe: ctx.probeProvider,
600
+ readClaudeCredential: ctx.readClaudeCredential,
587
601
  env: ctx.env,
588
602
  }),
589
603
  });
@@ -613,6 +627,7 @@ export function createLaneAccountsHandlers(deps) {
613
627
  existsSync: ctx.existsSync,
614
628
  hostname: ctx.hostname(),
615
629
  probe: ctx.probeProvider,
630
+ readClaudeCredential: ctx.readClaudeCredential,
616
631
  env: ctx.env,
617
632
  }),
618
633
  });