@indigoai-us/hq-cli 5.345.159 → 5.345.161

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 (104) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/assets/scaffold/core/scripts/session-journal.sh +116 -24
  3. package/dist/command-catalog.generated.d.ts +222 -9
  4. package/dist/command-catalog.generated.js +273 -9
  5. package/dist/commands/access.d.ts +6 -0
  6. package/dist/commands/access.js +18 -1
  7. package/dist/commands/acl-lock.d.ts +41 -0
  8. package/dist/commands/acl-lock.js +28 -0
  9. package/dist/commands/agent-enroll.d.ts +2 -0
  10. package/dist/commands/agent-enroll.js +9 -4
  11. package/dist/commands/agents.js +5 -11
  12. package/dist/commands/auth.js +6 -5
  13. package/dist/commands/billing-sponsor.d.ts +4 -4
  14. package/dist/commands/billing-sponsor.js +11 -11
  15. package/dist/commands/billing-usage.d.ts +22 -8
  16. package/dist/commands/billing-usage.js +57 -22
  17. package/dist/commands/billing.d.ts +7 -7
  18. package/dist/commands/billing.js +13 -13
  19. package/dist/commands/cloud-provision.js +2 -2
  20. package/dist/commands/cloud.js +7 -7
  21. package/dist/commands/core-session-helper-runtime.d.ts +1 -0
  22. package/dist/commands/core-session-journal-native.d.ts +1 -0
  23. package/dist/commands/core-session-journal-native.js +158 -34
  24. package/dist/commands/db-remote-deps.js +12 -1
  25. package/dist/commands/feedback.d.ts +12 -0
  26. package/dist/commands/feedback.js +52 -0
  27. package/dist/commands/files-browse.d.ts +8 -0
  28. package/dist/commands/files-browse.js +83 -1
  29. package/dist/commands/files.js +19 -7
  30. package/dist/commands/install-global.d.ts +2 -0
  31. package/dist/commands/install-global.js +24 -1
  32. package/dist/commands/integrations-connect.js +1 -0
  33. package/dist/commands/integrations-manage.d.ts +2 -0
  34. package/dist/commands/integrations-manage.js +6 -4
  35. package/dist/commands/lanes-link.js +5 -3
  36. package/dist/commands/lanes-session-start.d.ts +12 -0
  37. package/dist/commands/lanes-session-start.js +93 -12
  38. package/dist/commands/meetings.d.ts +15 -4
  39. package/dist/commands/meetings.js +58 -19
  40. package/dist/commands/skill.js +6 -6
  41. package/dist/commands/whoami.js +1 -1
  42. package/dist/hook-engine-path.js +13 -2
  43. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +3236 -1988
  44. package/dist/hq-cloud-sync-runner/package.json +1 -1
  45. package/dist/index.js +21 -0
  46. package/dist/lanes-session-start-argv.d.ts +7 -0
  47. package/dist/lanes-session-start-argv.js +9 -0
  48. package/dist/lib/billing/plan-limit-refusals.d.ts +13 -3
  49. package/dist/lib/billing/plan-limit-refusals.js +26 -3
  50. package/dist/lib/billing/plan-limit-upgrade-url.d.ts +8 -0
  51. package/dist/lib/billing/plan-limit-upgrade-url.js +18 -0
  52. package/dist/lib/billing/plan-lock.d.ts +22 -6
  53. package/dist/lib/billing/plan-lock.js +58 -30
  54. package/dist/lib/billing/usage-cost.d.ts +24 -2
  55. package/dist/lib/billing/usage-cost.js +103 -3
  56. package/dist/lib/billing/usage-only.d.ts +37 -1
  57. package/dist/lib/billing/usage-only.js +89 -4
  58. package/dist/lib/billing/usage-pause.d.ts +27 -0
  59. package/dist/lib/billing/usage-pause.js +50 -0
  60. package/dist/lib/bot/log.d.ts +3 -2
  61. package/dist/lib/bot/progress.d.ts +23 -1
  62. package/dist/lib/bot/progress.js +43 -5
  63. package/dist/lib/bot/run.js +2 -0
  64. package/dist/lib/bot/runtime/claude.d.ts +12 -0
  65. package/dist/lib/bot/runtime/claude.js +20 -0
  66. package/dist/lib/bot/runtime/index.d.ts +10 -1
  67. package/dist/lib/bot/runtime/index.js +14 -2
  68. package/dist/lib/bot/runtime/types.d.ts +8 -0
  69. package/dist/lib/db/control-plane.d.ts +5 -0
  70. package/dist/lib/db/control-plane.js +10 -0
  71. package/dist/lib/doctor/checks/ps1-shims.d.ts +21 -0
  72. package/dist/lib/doctor/checks/ps1-shims.js +82 -0
  73. package/dist/lib/doctor/fix/apply.d.ts +13 -1
  74. package/dist/lib/doctor/fix/apply.js +38 -3
  75. package/dist/lib/doctor/fix/remediation.d.ts +9 -1
  76. package/dist/lib/doctor/fix/remediation.js +22 -0
  77. package/dist/lib/doctor/registry.js +2 -0
  78. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  79. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  80. package/dist/lib/hook-engine/resolve.d.ts +14 -0
  81. package/dist/lib/hook-engine/resolve.js +55 -1
  82. package/dist/lib/lanes/links.d.ts +2 -1
  83. package/dist/lib/lanes/links.js +235 -17
  84. package/dist/lib/lanes/registry-flag.d.ts +24 -0
  85. package/dist/lib/lanes/registry-flag.js +72 -6
  86. package/dist/lib/lanes/store.d.ts +4 -2
  87. package/dist/lib/lanes/store.js +8 -8
  88. package/dist/lib/plan-limit-nag.d.ts +0 -16
  89. package/dist/lib/plan-limit-nag.js +17 -18
  90. package/dist/lib/windows/ps1-shims.d.ts +60 -0
  91. package/dist/lib/windows/ps1-shims.js +152 -0
  92. package/dist/main.d.ts +0 -2
  93. package/dist/main.js +19 -57
  94. package/dist/utils/plan-gate-error.d.ts +30 -14
  95. package/dist/utils/plan-gate-error.js +60 -46
  96. package/dist/utils/self-update.d.ts +2 -0
  97. package/dist/utils/self-update.js +8 -0
  98. package/dist/utils/staged-npm-install.js +13 -2
  99. package/dist/utils/team-upgrade.d.ts +19 -6
  100. package/dist/utils/team-upgrade.js +37 -39
  101. package/dist/utils/token-keychain.d.ts +11 -8
  102. package/dist/utils/token-keychain.js +36 -15
  103. package/package.json +8 -8
  104. package/scripts/install-hook-engine.mjs +12 -0
@@ -1,5 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import { randomUUID } from "node:crypto";
3
4
  import { fileURLToPath } from "node:url";
4
5
  const USAGE = `session-journal.sh: usage:
5
6
  session-journal.sh write "<title>" [--files f1,f2,...] [--body-file PATH] [--session KEY]
@@ -163,6 +164,58 @@ function isAgedPidlessCounterLock(lock, now) {
163
164
  return false;
164
165
  }
165
166
  }
167
+ function reclaimStaleCounterTempDirs(lock, now) {
168
+ const parent = path.dirname(lock);
169
+ // Publishers create <lock>.tmp.XXXXXX, matching the shell helper. Older native
170
+ // builds used <lock>.tmp-XXXXXX, so reclaim aged orphans with either prefix.
171
+ const prefixes = [`${path.basename(lock)}.tmp.`, `${path.basename(lock)}.tmp-`];
172
+ let names;
173
+ try {
174
+ names = fs.readdirSync(parent);
175
+ }
176
+ catch {
177
+ return;
178
+ }
179
+ for (const name of names) {
180
+ if (!prefixes.some((prefix) => name.startsWith(prefix)))
181
+ continue;
182
+ const temp = path.join(parent, name);
183
+ if (!isAgedPidlessCounterLock(temp, now))
184
+ continue;
185
+ const read = (file) => {
186
+ try {
187
+ return fs.readFileSync(path.join(temp, file), "utf8").trim();
188
+ }
189
+ catch {
190
+ return "";
191
+ }
192
+ };
193
+ const owner = read("pid");
194
+ const windowsOwner = read("winpid");
195
+ const token = read("owner-token");
196
+ if (owner && !/^\d+$/u.test(owner))
197
+ continue;
198
+ if (windowsOwner && !/^\d+$/u.test(windowsOwner))
199
+ continue;
200
+ if (owner && Number(owner) > 1 && counterLockOwnerIsAlive(temp, Number(owner)))
201
+ continue;
202
+ if (!owner && windowsOwner && Number(windowsOwner) > 1 && processIsAlive(Number(windowsOwner)))
203
+ continue;
204
+ if (read("pid") !== owner || read("winpid") !== windowsOwner || read("owner-token") !== token
205
+ || !isAgedPidlessCounterLock(temp, now))
206
+ continue;
207
+ if (owner && counterLockOwnerIsAlive(temp, Number(owner)))
208
+ continue;
209
+ if (windowsOwner && Number(windowsOwner) > 1 && processIsAlive(Number(windowsOwner)))
210
+ continue;
211
+ const stale = `${temp}.stale-${process.pid}-${Math.random().toString(16).slice(2)}`;
212
+ try {
213
+ fs.renameSync(temp, stale);
214
+ fs.rmSync(stale, { recursive: true, force: true });
215
+ }
216
+ catch { /* another waiter cleaned it or the owner resumed */ }
217
+ }
218
+ }
166
219
  function reclaimStaleCounterLock(lock, now) {
167
220
  let rawOwner;
168
221
  try {
@@ -184,6 +237,11 @@ function reclaimStaleCounterLock(lock, now) {
184
237
  return false;
185
238
  const owner = rawOwner ? Number.parseInt(rawOwner, 10) : 0;
186
239
  const windowsOwner = rawWindowsPid ? Number.parseInt(rawWindowsPid, 10) : 0;
240
+ let observedToken = "";
241
+ try {
242
+ observedToken = fs.readFileSync(path.join(lock, "owner-token"), "utf8").trim();
243
+ }
244
+ catch { /* legacy lock */ }
187
245
  if (rawOwner && (owner <= 1 || counterLockOwnerIsAlive(lock, owner)))
188
246
  return false;
189
247
  if (!rawOwner && windowsOwner > 1 && processIsAlive(windowsOwner))
@@ -219,7 +277,12 @@ function reclaimStaleCounterLock(lock, now) {
219
277
  catch {
220
278
  currentRawWindowsPid = "";
221
279
  }
222
- const sameOwner = currentRawOwner === rawOwner && currentRawWindowsPid === rawWindowsPid;
280
+ let currentToken = "";
281
+ try {
282
+ currentToken = fs.readFileSync(path.join(lock, "owner-token"), "utf8").trim();
283
+ }
284
+ catch { /* legacy lock */ }
285
+ const sameOwner = currentRawOwner === rawOwner && currentRawWindowsPid === rawWindowsPid && currentToken === observedToken;
223
286
  const stillStale = rawOwner
224
287
  ? sameOwner && !counterLockOwnerIsAlive(lock, owner)
225
288
  : sameOwner && !(windowsOwner > 1 && processIsAlive(windowsOwner)) && isAgedPidlessCounterLock(lock, now);
@@ -239,7 +302,15 @@ function reclaimStaleCounterLock(lock, now) {
239
302
  catch { /* best effort */ }
240
303
  return false;
241
304
  }
242
- function withCounterLock(lock, env, writers, now, action) {
305
+ function readLockToken(lock) {
306
+ try {
307
+ return fs.readFileSync(path.join(lock, "owner-token"), "utf8").trim();
308
+ }
309
+ catch {
310
+ return "";
311
+ }
312
+ }
313
+ function withCounterLock(lock, env, writers, now, action, beforeCounterAction) {
243
314
  let i = 0;
244
315
  let acquired = false;
245
316
  let tries = env.HQ_JOURNAL_COUNTER_LOCK_TRIES ?? "250";
@@ -247,22 +318,82 @@ function withCounterLock(lock, env, writers, now, action) {
247
318
  tries = "250";
248
319
  const retryLimit = Number.parseInt(tries, 10);
249
320
  while (i < retryLimit) {
321
+ reclaimStaleCounterTempDirs(lock, now);
250
322
  try {
251
- fs.mkdirSync(lock);
252
- acquired = true;
323
+ const temp = fs.mkdtempSync(`${lock}.tmp.`);
324
+ const ownerToken = randomUUID();
253
325
  try {
254
326
  if (process.platform === "win32")
255
- fs.writeFileSync(path.join(lock, "winpid"), `${process.pid}\n`, { flag: "wx" });
256
- fs.writeFileSync(path.join(lock, "pid"), `${process.pid}\n`, { flag: "wx" });
327
+ fs.writeFileSync(path.join(temp, "winpid"), `${process.pid}\n`, { flag: "wx" });
328
+ fs.writeFileSync(path.join(temp, "pid"), `${process.pid}\n`, { flag: "wx" });
329
+ fs.writeFileSync(path.join(temp, "owner-token"), `${ownerToken}\n`, { flag: "wx" });
330
+ const publishClaim = `${lock}.publish`;
331
+ try {
332
+ fs.mkdirSync(publishClaim);
333
+ }
334
+ catch (error) {
335
+ try {
336
+ fs.rmSync(temp, { recursive: true, force: true });
337
+ }
338
+ catch { /* best effort */ }
339
+ if (error.code === "EEXIST" && reclaimStaleCounterLock(publishClaim, now))
340
+ continue;
341
+ throw error;
342
+ }
343
+ try {
344
+ fs.writeFileSync(path.join(publishClaim, "owner-token"), `${ownerToken}\n`, { flag: "wx" });
345
+ fs.writeFileSync(path.join(publishClaim, "pid"), `${process.pid}\n`, { flag: "wx" });
346
+ // renameSync can replace an existing empty PID-less directory on
347
+ // POSIX. Refuse a pre-existing lock while publishers are serialized.
348
+ if (readLockToken(publishClaim) !== ownerToken || fs.existsSync(lock))
349
+ throw new Error("counter lock already exists");
350
+ fs.renameSync(temp, lock);
351
+ }
352
+ finally {
353
+ if (readLockToken(publishClaim) === ownerToken)
354
+ fs.rmSync(publishClaim, { recursive: true, force: true });
355
+ }
356
+ acquired = true;
257
357
  }
258
- catch {
358
+ catch (error) {
359
+ try {
360
+ fs.rmSync(temp, { recursive: true, force: true });
361
+ }
362
+ catch { /* best effort */ }
363
+ throw error;
364
+ }
365
+ if (!acquired)
366
+ continue;
367
+ if (readLockToken(lock) !== ownerToken) {
368
+ acquired = false;
369
+ i++;
370
+ continue;
371
+ }
372
+ beforeCounterAction?.();
373
+ if (readLockToken(lock) !== ownerToken) {
374
+ acquired = false;
375
+ i++;
376
+ continue;
377
+ }
378
+ const completed = action(ownerToken);
379
+ if (!completed) {
380
+ if (readLockToken(lock) === ownerToken) {
381
+ try {
382
+ fs.rmSync(lock, { recursive: true, force: true });
383
+ }
384
+ catch { /* best effort */ }
385
+ }
386
+ acquired = false;
387
+ i++;
388
+ continue;
389
+ }
390
+ if (readLockToken(lock) === ownerToken) {
259
391
  try {
260
392
  fs.rmSync(lock, { recursive: true, force: true });
261
393
  }
262
394
  catch { /* best effort */ }
263
- return;
264
395
  }
265
- break;
396
+ return;
266
397
  }
267
398
  catch {
268
399
  if (reclaimStaleCounterLock(lock, now))
@@ -276,22 +407,6 @@ function withCounterLock(lock, env, writers, now, action) {
276
407
  writers.stderr("session-journal: tool-counter: lock remained busy; skipped update\n");
277
408
  return;
278
409
  }
279
- try {
280
- action();
281
- }
282
- finally {
283
- let owner = 0;
284
- try {
285
- owner = Number.parseInt(fs.readFileSync(path.join(lock, "pid"), "utf8"), 10) || 0;
286
- }
287
- catch { /* lock disappeared */ }
288
- if (owner === process.pid) {
289
- try {
290
- fs.rmSync(lock, { recursive: true, force: true });
291
- }
292
- catch { /* best effort */ }
293
- }
294
- }
295
410
  }
296
411
  function nextSequence(dir) {
297
412
  let names = [];
@@ -313,7 +428,7 @@ function options(args, valueFlags) {
313
428
  }
314
429
  return values;
315
430
  }
316
- function runToolCounter(root, action, sessionArg, env, now, writers) {
431
+ function runToolCounter(root, action, sessionArg, env, now, writers, beforeCounterAction) {
317
432
  const rawKey = sessionArg || env.HQ_JOURNAL_SESSION || env.HQ_HOOK_SESSION_ID || "";
318
433
  const key = counterKey(rawKey);
319
434
  const date = now().toISOString().slice(0, 10);
@@ -335,27 +450,36 @@ function runToolCounter(root, action, sessionArg, env, now, writers) {
335
450
  writers.stderr(`${sessionJournalAsset}: line ${line}: ${printableCounter}: Is a directory\n`);
336
451
  };
337
452
  if (action === "increment") {
338
- withCounterLock(lock, env, writers, now, () => {
453
+ withCounterLock(lock, env, writers, now, (ownerToken) => {
454
+ if (readLockToken(lock) !== ownerToken)
455
+ return false;
456
+ const next = readCounter(counter) + 1;
457
+ if (readLockToken(lock) !== ownerToken)
458
+ return false;
339
459
  try {
340
- fs.writeFileSync(counter, `${readCounter(counter) + 1}\n`);
460
+ fs.writeFileSync(counter, `${next}\n`);
341
461
  }
342
462
  catch (error) {
343
- reportDirectoryWriteFailure(error, 457);
463
+ reportDirectoryWriteFailure(error, 545);
344
464
  }
345
- });
465
+ return true;
466
+ }, beforeCounterAction);
346
467
  }
347
468
  else if (action === "read") {
348
469
  writers.stdout(`${readCounter(counter)}\n`);
349
470
  }
350
471
  else if (action === "reset") {
351
- withCounterLock(lock, env, writers, now, () => {
472
+ withCounterLock(lock, env, writers, now, (ownerToken) => {
473
+ if (readLockToken(lock) !== ownerToken)
474
+ return false;
352
475
  try {
353
476
  fs.writeFileSync(counter, "0\n");
354
477
  }
355
478
  catch (error) {
356
- reportDirectoryWriteFailure(error, 465);
479
+ reportDirectoryWriteFailure(error, 556);
357
480
  }
358
- });
481
+ return true;
482
+ }, beforeCounterAction);
359
483
  }
360
484
  else {
361
485
  writers.stderr("session-journal: tool-counter: subcommand required (increment|read|reset)\n");
@@ -519,7 +643,7 @@ function executeSessionJournal(args, cwd, hqRoot, dependencies, writers) {
519
643
  if (command === "tool-counter") {
520
644
  const action = rest[0] ?? "";
521
645
  const session = options(rest.slice(1), ["--session"]).get("--session") ?? "";
522
- runToolCounter(root, action, session, runtime.env, runtime.now, writers);
646
+ runToolCounter(root, action, session, runtime.env, runtime.now, writers, dependencies.beforeCounterAction);
523
647
  return 0;
524
648
  }
525
649
  writers.stderr(USAGE);
@@ -74,7 +74,18 @@ export function describeRemoteError(error) {
74
74
  return "Remote databases are not available yet.";
75
75
  }
76
76
  if (status === 402 || code === "PLAN_REQUIRED") {
77
- return "Remote databases require the Team plan.";
77
+ // US-024: the server's usage pause message, else its own sentence. The
78
+ // CLI names no plan: the flat plans are closed.
79
+ const pause = error.usagePause;
80
+ if (pause && typeof pause.message === "string" && pause.message) {
81
+ return typeof pause.actionUrl === "string" && pause.actionUrl
82
+ ? `${pause.message}\nBilling: ${pause.actionUrl}`
83
+ : pause.message;
84
+ }
85
+ const serverMessage = error.serverMessage;
86
+ if (typeof serverMessage === "string" && serverMessage)
87
+ return serverMessage;
88
+ return "Remote databases are not available on this company's current billing. Run `hq billing status` to check it.";
78
89
  }
79
90
  if (status === 403)
80
91
  return "You are not a member of this company, or this app belongs to another company.";
@@ -91,5 +91,17 @@ export declare function postFeedbackUpdate(opts: {
91
91
  publicUrl: string | null;
92
92
  slack: string | null;
93
93
  }>;
94
+ /** Mirror of hq-pro normalizeAssignee: a prs_ uid as-is, an email lowercased. */
95
+ export declare function normalizeFeedbackAssignee(value: string): string;
96
+ /**
97
+ * Reassign a ticket. A person's own login may move a ticket that another
98
+ * person claimed; agent and operator service tokens get 409
99
+ * TICKET_CLAIMED_BY_HUMAN from the server for those tickets.
100
+ */
101
+ export declare function assignFeedback(opts: {
102
+ id: string;
103
+ token: string;
104
+ assignee: string;
105
+ }): Promise<void>;
94
106
  export declare function registerFeedbackCommand(program: Command): void;
95
107
  //# sourceMappingURL=feedback.d.ts.map
@@ -285,6 +285,57 @@ export async function postFeedbackUpdate(opts) {
285
285
  slack: typeof data.publicUpdateSlack === "string" ? data.publicUpdateSlack : null,
286
286
  };
287
287
  }
288
+ const ASSIGNEE_RE = /^(?:prs_[A-Za-z0-9]+|[^\s@]+@[^\s@]+\.[^\s@]+)$/;
289
+ /** Mirror of hq-pro normalizeAssignee: a prs_ uid as-is, an email lowercased. */
290
+ export function normalizeFeedbackAssignee(value) {
291
+ const trimmed = value.trim();
292
+ if (!ASSIGNEE_RE.test(trimmed)) {
293
+ throw new Error(`Invalid --to '${value}': expected an email address or a prs_ uid.`);
294
+ }
295
+ return trimmed.startsWith("prs_") ? trimmed : trimmed.toLowerCase();
296
+ }
297
+ /**
298
+ * Reassign a ticket. A person's own login may move a ticket that another
299
+ * person claimed; agent and operator service tokens get 409
300
+ * TICKET_CLAIMED_BY_HUMAN from the server for those tickets.
301
+ */
302
+ export async function assignFeedback(opts) {
303
+ const res = await vaultApiFetch({
304
+ token: opts.token,
305
+ path: `/v1/feedback/${opts.id}`,
306
+ method: "PATCH",
307
+ body: { assignee: opts.assignee },
308
+ });
309
+ if (res.ok)
310
+ return;
311
+ const data = (await res.json().catch(() => ({})));
312
+ const message = typeof data.error === "string" ? data.error : res.statusText;
313
+ if (data.code === "TICKET_CLAIMED_BY_HUMAN") {
314
+ throw new Error(`${message} Run hq feedback assign from your own hq login (hq login), not an agent or operator token.`);
315
+ }
316
+ throw new Error(`Failed to assign ${opts.id}: ${message}`);
317
+ }
318
+ function registerAssignSubcommand(feedbackCmd) {
319
+ feedbackCmd
320
+ .command("assign")
321
+ .description("Reassign a ticket to a person, including one another person has claimed")
322
+ .argument("<id>", "Feedback id (feedback_<uuid>)")
323
+ .requiredOption("--to <assignee>", "New assignee: an email address or a prs_ uid")
324
+ .action(async (id, opts) => {
325
+ try {
326
+ assertFeedbackId(id);
327
+ const assignee = normalizeFeedbackAssignee(opts.to);
328
+ const token = await resolveStaffToken();
329
+ assertVaultApiRequestSupported({ token, path: `/v1/feedback/${id}` });
330
+ await assignFeedback({ id, token, assignee });
331
+ console.log(`Assigned: ${id} -> ${assignee}`);
332
+ }
333
+ catch (err) {
334
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
335
+ process.exit(1);
336
+ }
337
+ });
338
+ }
288
339
  function registerLinkSubcommand(feedbackCmd) {
289
340
  feedbackCmd
290
341
  .command("link")
@@ -447,5 +498,6 @@ export function registerFeedbackCommand(program) {
447
498
  registerLinkSubcommand(feedbackCmd);
448
499
  registerUpdateSubcommand(feedbackCmd);
449
500
  registerFeedbackShowCommand(feedbackCmd);
501
+ registerAssignSubcommand(feedbackCmd);
450
502
  }
451
503
  //# sourceMappingURL=feedback.js.map
@@ -459,6 +459,14 @@ export declare const STORAGE_DOWNLOAD_FAILED_CODE = "FILES_STORAGE_DOWNLOAD_FAIL
459
459
  * firewall that does not allow the bucket host, and we say so.
460
460
  */
461
461
  export declare function presignedDownloadError(key: string, url: string, dl: Response): Promise<Error>;
462
+ /**
463
+ * Turn a presigned download that never got an HTTP response (connection
464
+ * refused or reset, DNS failure, timeout, or a TLS failure) into an error that
465
+ * names the storage host and tells the operator to allow it. Returns
466
+ * undefined for any other error so the caller rethrows it unchanged. Only the
467
+ * URL host is used; the signed query string never reaches the message.
468
+ */
469
+ export declare function presignedDownloadTransportError(key: string, url: string, err: unknown): Error | undefined;
462
470
  export declare function createCompanyPresignClient(input: {
463
471
  token: string;
464
472
  companyUid: string;
@@ -760,6 +760,82 @@ export async function presignedDownloadError(key, url, dl) {
760
760
  ...(storageCode ? { storageCode } : {}),
761
761
  });
762
762
  }
763
+ /** Socket-level error codes that mean the storage host could not be reached. */
764
+ const TRANSPORT_BLOCK_CODES = new Set([
765
+ "ECONNREFUSED",
766
+ "ECONNRESET",
767
+ "ETIMEDOUT",
768
+ "EHOSTUNREACH",
769
+ "ENETUNREACH",
770
+ "EPIPE",
771
+ "ENOTFOUND",
772
+ "EAI_AGAIN",
773
+ "UND_ERR_CONNECT_TIMEOUT",
774
+ "UND_ERR_HEADERS_TIMEOUT",
775
+ "UND_ERR_SOCKET",
776
+ ]);
777
+ /** TLS failures, usually a TLS-inspecting proxy whose certificate Node does not trust. */
778
+ const TLS_BLOCK_CODES = new Set([
779
+ "CERT_HAS_EXPIRED",
780
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
781
+ "SELF_SIGNED_CERT_IN_CHAIN",
782
+ "UNABLE_TO_GET_ISSUER_CERT",
783
+ "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
784
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
785
+ "ERR_TLS_CERT_ALTNAME_INVALID",
786
+ "ERR_SSL_WRONG_VERSION_NUMBER",
787
+ "EPROTO",
788
+ ]);
789
+ function errorChainCode(err) {
790
+ let cur = err;
791
+ for (let depth = 0; depth < 5 && cur && typeof cur === "object"; depth++) {
792
+ const code = cur.code;
793
+ if (typeof code === "string" && code.length > 0)
794
+ return code;
795
+ cur = cur.cause;
796
+ }
797
+ return undefined;
798
+ }
799
+ /**
800
+ * Turn a presigned download that never got an HTTP response (connection
801
+ * refused or reset, DNS failure, timeout, or a TLS failure) into an error that
802
+ * names the storage host and tells the operator to allow it. Returns
803
+ * undefined for any other error so the caller rethrows it unchanged. Only the
804
+ * URL host is used; the signed query string never reaches the message.
805
+ */
806
+ export function presignedDownloadTransportError(key, url, err) {
807
+ const name = err?.name;
808
+ const timedOut = name === "TimeoutError";
809
+ const code = timedOut ? "TIMEOUT" : errorChainCode(err);
810
+ if (!code)
811
+ return undefined;
812
+ const tls = TLS_BLOCK_CODES.has(code) || code.startsWith("ERR_TLS_") || code.startsWith("ERR_SSL_");
813
+ if (!timedOut && !tls && !TRANSPORT_BLOCK_CODES.has(code))
814
+ return undefined;
815
+ let host = "the storage host";
816
+ try {
817
+ host = new URL(url).host;
818
+ }
819
+ catch {
820
+ // keep the generic label
821
+ }
822
+ const what = tls
823
+ ? `the TLS connection to ${host} failed (${code}). A proxy that inspects HTTPS traffic is the usual cause`
824
+ : timedOut
825
+ ? `the connection to ${host} timed out. A firewall or egress rule that drops traffic is the usual cause`
826
+ : `the connection to ${host} failed (${code}). A firewall, proxy or egress rule on this network is the usual cause`;
827
+ const message = `Failed to download '${key}': ${what}. Your file access was granted; the ` +
828
+ `network blocked the storage host. Ask your network operator to allow ` +
829
+ `outbound HTTPS (TCP 443) to ${host} (HQ vault files are served from ` +
830
+ `*.s3.us-east-1.amazonaws.com), then retry.`;
831
+ return Object.assign(new Error(message, { cause: err }), {
832
+ code: STORAGE_DOWNLOAD_FAILED_CODE,
833
+ key,
834
+ blockedHost: host,
835
+ transportCode: code,
836
+ fromStorage: false,
837
+ });
838
+ }
763
839
  export function createCompanyPresignClient(input) {
764
840
  const { token, companyUid, companySlug, baseUrl } = input;
765
841
  const denied = (message, key, extra) => Object.assign(new Error(message), {
@@ -829,7 +905,13 @@ export function createCompanyPresignClient(input) {
829
905
  }
830
906
  throw new Error(message);
831
907
  }
832
- const dl = await fetch(first.url, { signal: AbortSignal.timeout(120_000) });
908
+ let dl;
909
+ try {
910
+ dl = await fetch(first.url, { signal: AbortSignal.timeout(120_000) });
911
+ }
912
+ catch (err) {
913
+ throw presignedDownloadTransportError(key, first.url, err) ?? err;
914
+ }
833
915
  if (!dl.ok) {
834
916
  throw await presignedDownloadError(key, first.url, dl);
835
917
  }
@@ -1,4 +1,5 @@
1
1
  import chalk from "chalk";
2
+ import { aclLockedBy, describeAclLock } from "./acl-lock.js";
2
3
  import open from "open";
3
4
  import * as readline from "node:readline";
4
5
  import { resolveVaultCredential } from "../utils/resolve-vault-credential.js";
@@ -489,17 +490,21 @@ export function registerFilesCommand(program) {
489
490
  throw new Error('Invalid ACL tree response');
490
491
  }
491
492
  const row = tree.directRow;
493
+ // The most-specific row has no grants, so broader grants do not apply.
494
+ const lockedBy = aclLockedBy(tree);
492
495
  if (opts.json) {
493
- console.log(JSON.stringify({ ...tree, schemaVersion: 1, companyUid, exists: row !== null }, null, 2));
496
+ console.log(JSON.stringify({ ...tree, schemaVersion: 1, companyUid, exists: row !== null, lockedBy }, null, 2));
494
497
  return;
495
498
  }
496
499
  // No own row AND nothing inherited or granted below — preserve the
497
500
  // original "no ACL record" exit path.
498
- if (!row && tree.inherited.length === 0 && tree.children.length === 0) {
501
+ if (!row && !lockedBy && tree.inherited.length === 0 && tree.children.length === 0) {
499
502
  console.error(chalk.red(`No ACL record exists for '${canonicalPrefix}'`));
500
503
  process.exit(1);
501
504
  }
502
- const aclStatus = row?.open ? "open" : "restricted";
505
+ const aclStatus = lockedBy
506
+ ? lockedBy.readUnknown ? "locked unless open" : "locked"
507
+ : row?.open ? "open" : "restricted";
503
508
  console.log(chalk.green(`ACL for ${displayAclPattern(tree.prefix)} (${aclStatus})`));
504
509
  if (isPrivateFolderPattern(tree.prefix)) {
505
510
  console.log(chalk.gray(privateFolderExplanation(tree.prefix)));
@@ -507,9 +512,12 @@ export function registerFilesCommand(program) {
507
512
  if (row) {
508
513
  console.log(`Creator: ${row.creatorUid}`);
509
514
  }
510
- else {
515
+ else if (!lockedBy) {
511
516
  console.log(chalk.gray("No direct ACL row — access flows from the inherited/descendant grants below."));
512
517
  }
518
+ if (lockedBy) {
519
+ console.log(chalk.yellow(describeAclLock(lockedBy, displayAclPattern(lockedBy.path))));
520
+ }
513
521
  if (tree.effectivePermission) {
514
522
  console.log(`Your effective permission: ${tree.effectivePermission}`);
515
523
  }
@@ -560,9 +568,9 @@ export function registerFilesCommand(program) {
560
568
  const directEntries = tree.direct;
561
569
  if (directEntries.length === 0) {
562
570
  if (row?.open) {
563
- console.log(chalk.gray("Open ACL — all active members have read access."));
571
+ console.log(chalk.gray("No grants on this row (open: broader grants still apply). All active members have at least read access."));
564
572
  }
565
- else if (row) {
573
+ else if (row && !lockedBy) {
566
574
  console.log(chalk.gray("No explicit grants on this prefix — only creator has access."));
567
575
  }
568
576
  }
@@ -571,7 +579,11 @@ export function registerFilesCommand(program) {
571
579
  printEntryTable(directEntries, false);
572
580
  }
573
581
  if (tree.inherited.length > 0) {
574
- console.log("\nInherited (granted on an ancestor prefix):");
582
+ console.log(lockedBy
583
+ ? lockedBy.readUnknown
584
+ ? `\nGranted on ancestor prefixes (apply only if ${displayAclPattern(lockedBy.path)} is open):`
585
+ : `\nGranted on ancestor prefixes (do not apply: locked by ${displayAclPattern(lockedBy.path)}):`
586
+ : "\nInherited (granted on an ancestor prefix):");
575
587
  printEntryTable(tree.inherited, true);
576
588
  }
577
589
  if (tree.children.length > 0) {
@@ -83,6 +83,8 @@ export interface GlobalCommandOpts {
83
83
  trustCodexHooks?: (cwd: string, keys: readonly string[]) => Promise<RuntimeHookTrustResult>;
84
84
  /** Test seam: is the npm hq.cmd folder on the Windows PATH? Null off Windows. */
85
85
  shimPathCheck?: () => ShimPathCheck | null;
86
+ /** Test seam: the Windows npm prefix holding hq.cmd. Null off Windows. */
87
+ ps1ShimDir?: () => string | null;
86
88
  }
87
89
  export declare function resolveInstallRoot(explicit: string | undefined, options?: {
88
90
  env?: NodeJS.ProcessEnv;
@@ -27,7 +27,8 @@ import { readCompaniesManifestMap } from '../lib/work-context/repo-remote.js';
27
27
  import { resolveDefaultHqRoot } from '../utils/hq-root-resolver.js';
28
28
  import { trustCodexUserHooks } from '../utils/hook-trust.js';
29
29
  import { assertProviderWrappersInstallable, installProviderWrappers, providerWrapperInstallPaths, removeProviderWrappers } from '../lib/install/provider-wrappers.js';
30
- import { checkShimOnPath, ensureShimOnUserPath, shimPathWarning } from '../lib/windows/user-path.js';
30
+ import { checkShimOnPath, ensureShimOnUserPath, npmShimDir, shimPathWarning } from '../lib/windows/user-path.js';
31
+ import { removeHqCliPs1Shims } from '../lib/windows/ps1-shims.js';
31
32
  import { addAgentsMdBlock, addCodexHooks, classifyLegacySkills, codexAdapterPath, codexCharterSource, codexHookTrustKeys, codexTargets, hasAllCodexHooks, HQ_OWNED_MARKER, LEGACY_BACKUP_NAME, LEGACY_SKILLS_NAME, removeAgentsMdBlock, removeCodexHooks, } from '../lib/install/codex-targets.js';
32
33
  import { addClaudeMdBlock, addHqHooks, charterPath, claudeTargets, hasAllHqHooks, HQ_ANYWHERE_PACK, HQ_MCP_SERVER_NAME, hqSkillsSource, lineDiff, masterHookPath, shimPath, MCP_SUBPROCESS_PATH, removeClaudeMdBlock, removeHqHooks, SKILL_LINK_PREFIX, } from '../lib/install/claude-targets.js';
33
34
  export { hookTargetFor, isDaemonInstalled } from '../lib/install/hook-target.js';
@@ -1156,6 +1157,24 @@ export function resolveInstallRoot(explicit, options = {}) {
1156
1157
  throw Object.assign(new Error('Run from inside your HQ checkout, or pass --hq-root <dir>.'), { expected: true });
1157
1158
  }
1158
1159
  }
1160
+ function defaultPs1ShimDir() {
1161
+ return process.platform === 'win32' ? npmShimDir(process.argv[1], fs.existsSync) : null;
1162
+ }
1163
+ /**
1164
+ * PowerShell picks npm's hq.ps1 over hq.cmd, and a Restricted execution policy
1165
+ * blocks it. Remove only npm's own hq-cli launchers; never change the policy.
1166
+ */
1167
+ function removePs1ShimsReport(dir) {
1168
+ const result = removeHqCliPs1Shims(dir);
1169
+ const lines = [];
1170
+ if (result.removed.length > 0) {
1171
+ lines.push(` windows: removed ${result.removed.map((f) => path.win32.basename(f)).join(', ')} so PowerShell now runs hq.cmd.`);
1172
+ }
1173
+ for (const failure of result.failed) {
1174
+ lines.push(` windows: could not remove ${failure.file} (${failure.reason}). Run hq.cmd instead of hq in PowerShell.`);
1175
+ }
1176
+ return lines;
1177
+ }
1159
1178
  /** Entry used by `hq install --global` (wired from pkg-install.ts). */
1160
1179
  export async function runInstallGlobal(opts) {
1161
1180
  const runtime = assertRuntime(opts.runtime);
@@ -1201,6 +1220,10 @@ export async function runInstallGlobal(opts) {
1201
1220
  if (shimPath)
1202
1221
  for (const line of shimPathWarning(shimPath))
1203
1222
  (opts.log ?? console.log)(line);
1223
+ const ps1Dir = (opts.ps1ShimDir ?? defaultPs1ShimDir)();
1224
+ if (ps1Dir)
1225
+ for (const line of removePs1ShimsReport(ps1Dir))
1226
+ (opts.log ?? console.log)(line);
1204
1227
  }
1205
1228
  }
1206
1229
  /**
@@ -1449,6 +1449,7 @@ export function registerConnectCommands(integrations) {
1449
1449
  .option("--auth <mode>", "Force the auth mode: none, key, or oauth (default: detect)")
1450
1450
  .option("--client-id <id>", "Client id of an OAuth app you registered with the provider yourself")
1451
1451
  .option("--client-secret-stdin", "Read that app's client secret from stdin")
1452
+ .option("--scopes <list>", "Permissions to request, space- or comma-separated (default: the permissions the app was approved for last time)")
1452
1453
  .option("--no-browser", "Print the sign-in URL instead of opening a browser")
1453
1454
  .option("--timeout <seconds>", "How long to wait for a browser sign-in (default 300)")
1454
1455
  .option("--connect", "For a revoked row, run `connect <domain>` to re-add and revive it")
@@ -26,6 +26,8 @@ interface Principal {
26
26
  * guessing — a misparsed principal silently grants access to the wrong party.
27
27
  */
28
28
  export declare function parsePrincipal(raw: string): Principal;
29
+ /** `--with` / `--to` / `--from` help. Groups resolve membership at call time. */
30
+ export declare const PRINCIPAL_HELP = "Email, uid, vault group (grp_\u2026), or `everyone`";
29
31
  export declare function registerManageCommands(integrations: Command): void;
30
32
  /** Order-insensitive identity of an allowlist, for change detection. */
31
33
  export declare function allowlistFingerprint(grants: WriteAllowlistGrant[]): string;