@indigoai-us/hq-cli 5.162.0 → 5.164.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 (71) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/command-catalog.generated.d.ts +188 -0
  3. package/dist/command-catalog.generated.js +235 -0
  4. package/dist/command-registration-plan.d.ts +7 -0
  5. package/dist/command-registration-plan.js +1 -0
  6. package/dist/commands/agent-enroll.d.ts +6 -1
  7. package/dist/commands/agent-enroll.js +30 -3
  8. package/dist/commands/agents.js +15 -22
  9. package/dist/commands/bot.js +56 -6
  10. package/dist/commands/daemon.d.ts +53 -0
  11. package/dist/commands/daemon.js +554 -0
  12. package/dist/commands/lanes.js +7 -0
  13. package/dist/commands/meetings.js +18 -16
  14. package/dist/commands/mesh.js +9 -0
  15. package/dist/lib/daemon/catalog.d.ts +85 -0
  16. package/dist/lib/daemon/catalog.js +274 -0
  17. package/dist/lib/daemon/config.d.ts +26 -0
  18. package/dist/lib/daemon/config.js +85 -0
  19. package/dist/lib/daemon/handoff.d.ts +53 -0
  20. package/dist/lib/daemon/handoff.js +136 -0
  21. package/dist/lib/daemon/install.d.ts +76 -0
  22. package/dist/lib/daemon/install.js +299 -0
  23. package/dist/lib/daemon/legacy.d.ts +35 -0
  24. package/dist/lib/daemon/legacy.js +120 -0
  25. package/dist/lib/daemon/lock.d.ts +47 -0
  26. package/dist/lib/daemon/lock.js +138 -0
  27. package/dist/lib/daemon/machine-type.d.ts +48 -0
  28. package/dist/lib/daemon/machine-type.js +139 -0
  29. package/dist/lib/daemon/migrate.d.ts +61 -0
  30. package/dist/lib/daemon/migrate.js +238 -0
  31. package/dist/lib/daemon/paths.d.ts +22 -0
  32. package/dist/lib/daemon/paths.js +42 -0
  33. package/dist/lib/daemon/remote-control.d.ts +35 -0
  34. package/dist/lib/daemon/remote-control.js +47 -0
  35. package/dist/lib/daemon/requests.d.ts +24 -0
  36. package/dist/lib/daemon/requests.js +63 -0
  37. package/dist/lib/daemon/run.d.ts +41 -0
  38. package/dist/lib/daemon/run.js +319 -0
  39. package/dist/lib/daemon/supervisor.d.ts +87 -0
  40. package/dist/lib/daemon/supervisor.js +523 -0
  41. package/dist/lib/daemon/updater.d.ts +39 -0
  42. package/dist/lib/daemon/updater.js +103 -0
  43. package/dist/lib/flag-registry.d.ts +7 -1
  44. package/dist/lib/flag-registry.js +8 -0
  45. package/dist/lib/mesh/live/backoff.d.ts +2 -2
  46. package/dist/lib/mesh/live/backoff.js +16 -3
  47. package/dist/lib/mesh/live/daemon/run.d.ts +2 -2
  48. package/dist/lib/mesh/live/daemon/run.js +5 -2
  49. package/dist/lib/mesh/live/flush.d.ts +3 -2
  50. package/dist/lib/mesh/live/flush.js +34 -7
  51. package/dist/lib/mesh/live/session-events-client.d.ts +2 -0
  52. package/dist/lib/mesh/live/session-events-client.js +25 -0
  53. package/dist/lib/service-manager/systemd.d.ts +2 -2
  54. package/dist/lib/service-manager/systemd.js +1 -1
  55. package/dist/lib/service-manager/types.d.ts +2 -0
  56. package/dist/main.d.ts +6 -0
  57. package/dist/main.js +53 -13
  58. package/dist/sentry.js +5 -1
  59. package/dist/startup-registration.d.ts +3 -1
  60. package/dist/startup-registration.js +25 -4
  61. package/dist/utils/cognito-session.d.ts +4 -0
  62. package/dist/utils/cognito-session.js +11 -10
  63. package/dist/utils/install-tree-torn.d.ts +29 -10
  64. package/dist/utils/install-tree-torn.js +262 -29
  65. package/dist/utils/plan-gate-error.d.ts +27 -13
  66. package/dist/utils/plan-gate-error.js +133 -58
  67. package/dist/utils/self-update.d.ts +9 -0
  68. package/dist/utils/self-update.js +15 -2
  69. package/dist/utils/sentry-fingerprint.d.ts +7 -1
  70. package/dist/utils/sentry-fingerprint.js +95 -15
  71. package/package.json +1 -1
@@ -30,6 +30,7 @@
30
30
  * are only READ (their exported symbols), never modified.
31
31
  */
32
32
  import * as nodeFs from "node:fs";
33
+ import { homedir } from "node:os";
33
34
  import * as path from "node:path";
34
35
  import { fileURLToPath } from "node:url";
35
36
  import { CLI_NAME } from "../cli-version.js";
@@ -637,6 +638,125 @@ function packageRootHealthy(packageRoot, fs) {
637
638
  const manifest = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf-8"));
638
639
  return manifest.name === CLI_NAME;
639
640
  }
641
+ function isWithinPath(candidate, root) {
642
+ const relative = path.relative(root, candidate);
643
+ return relative === "" ||
644
+ (!path.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`));
645
+ }
646
+ /** Paths whose presence, metadata, or immediate entries show activity for this import. */
647
+ function installTreeActivityPaths(target, packageRoot) {
648
+ const root = packageRoot ? path.resolve(packageRoot) : null;
649
+ const paths = new Set();
650
+ const directories = new Set();
651
+ const addPath = (candidate) => {
652
+ const resolved = path.resolve(candidate);
653
+ paths.add(resolved);
654
+ directories.add(path.dirname(resolved));
655
+ if (!root || !isWithinPath(resolved, root))
656
+ return;
657
+ let current = path.dirname(resolved);
658
+ for (;;) {
659
+ paths.add(current);
660
+ directories.add(current);
661
+ if (current === root)
662
+ break;
663
+ const parent = path.dirname(current);
664
+ if (parent === current || !isWithinPath(parent, root))
665
+ break;
666
+ current = parent;
667
+ }
668
+ };
669
+ if (root) {
670
+ for (const candidate of [
671
+ root,
672
+ path.join(root, "package.json"),
673
+ path.join(root, "dist"),
674
+ path.join(root, "node_modules"),
675
+ ]) {
676
+ addPath(candidate);
677
+ directories.add(candidate);
678
+ }
679
+ }
680
+ if (target.kind === "path" || target.kind === "exists") {
681
+ addPath(target.path);
682
+ }
683
+ else if (target.kind === "relative") {
684
+ const base = path.resolve(target.from, target.specifier);
685
+ const candidates = [
686
+ base,
687
+ ...CJS_FILE_EXTENSIONS.map((ext) => base + ext),
688
+ ...CJS_FILE_EXTENSIONS.map((ext) => path.join(base, `index${ext}`)),
689
+ ];
690
+ for (const candidate of candidates)
691
+ addPath(candidate);
692
+ }
693
+ else if (target.kind === "package") {
694
+ let from = path.resolve(target.from);
695
+ let ancestorCount = 0;
696
+ for (;;) {
697
+ if (root && !isWithinPath(from, root))
698
+ break;
699
+ addPath(path.join(from, "node_modules", target.name));
700
+ if (from === root)
701
+ break;
702
+ ancestorCount += 1;
703
+ if (!root && ancestorCount >= 12)
704
+ break;
705
+ const parent = path.dirname(from);
706
+ if (parent === from)
707
+ break;
708
+ from = parent;
709
+ }
710
+ }
711
+ return {
712
+ paths: [...paths].sort(),
713
+ directories: [...directories].sort(),
714
+ };
715
+ }
716
+ /** A local-only snapshot of the missing target and directories an installer may change. */
717
+ function installTreeActivitySignature(target, packageRoot, fs) {
718
+ const activity = installTreeActivityPaths(target, packageRoot);
719
+ const snapshots = [];
720
+ let observed = true;
721
+ const errorCode = (err) => {
722
+ if (err !== null && typeof err === "object" && "code" in err && typeof err.code === "string") {
723
+ return err.code;
724
+ }
725
+ return "error";
726
+ };
727
+ for (const candidate of activity.paths) {
728
+ try {
729
+ if (!fs.existsSync(candidate)) {
730
+ snapshots.push([candidate, "missing"]);
731
+ continue;
732
+ }
733
+ const stat = fs.statSync(candidate);
734
+ snapshots.push([
735
+ candidate,
736
+ stat.isDirectory() ? "directory" : "file",
737
+ stat.size ?? null,
738
+ stat.mtimeMs ?? null,
739
+ ]);
740
+ }
741
+ catch (err) {
742
+ observed = false;
743
+ snapshots.push([candidate, `unreadable:${errorCode(err)}`]);
744
+ }
745
+ }
746
+ for (const directory of activity.directories) {
747
+ try {
748
+ if (!fs.existsSync(directory) || !fs.statSync(directory).isDirectory())
749
+ continue;
750
+ snapshots.push([directory, "entries", fs.readdirSync(directory).sort()]);
751
+ }
752
+ catch (err) {
753
+ observed = false;
754
+ snapshots.push([directory, `entries-unreadable:${errorCode(err)}`]);
755
+ }
756
+ }
757
+ return { value: JSON.stringify(snapshots), observed };
758
+ }
759
+ export const DEFAULT_INSTALL_TREE_STALE_PROBE_MS = 1_500;
640
760
  /**
641
761
  * Poll until the install tree has been continuously READY for `quietMs`, or the
642
762
  * wait exhausts its bounds. READY means: (i) no FRESH update lock held by another
@@ -645,14 +765,14 @@ function packageRootHealthy(packageRoot, fs) {
645
765
  * `.<leaf>-<rand>` retired sibling remains beside it. Every filesystem error makes
646
766
  * its condition "not ready" rather than throwing.
647
767
  *
648
- * The wait is LOCK-AWARE and bounded on TWO independent axes, so a live
649
- * cooperating writer (the desktop app, the box's update timer, another `hq`)
650
- * cannot make a legitimately-in-progress reinstall look like a torn install:
768
+ * The wait is LOCK-AWARE and bounded, so a live cooperating writer (the desktop
769
+ * app, the box's update timer, another `hq`) cannot make a legitimately-in-progress
770
+ * reinstall look stale:
651
771
  * - UNLOCKED time — wall time observed with no fresh foreign lock — is bounded
652
- * by `deadlineMs`. That is the budget for the TREE to settle once no writer is
653
- * visible; with no lock ever seen, unlocked time equals total wait time and
654
- * the behaviour is byte-for-byte the pre-lock-aware one (unsettled at exactly
655
- * `deadlineMs`, and `deadlineMs === 0` evaluates readiness exactly once).
772
+ * by `deadlineMs`. If the target is still missing, no lock or retired sibling
773
+ * is present, and the observed tree stays unchanged for `staleProbeMs`, the
774
+ * wait returns `staleTree` early. Any observed tree change keeps the ordinary
775
+ * settle deadline in force. `deadlineMs === 0` evaluates readiness once.
656
776
  * - LOCKED time — wall time observed under a fresh foreign lock held by a live
657
777
  * pid — is bounded SEPARATELY by `lockWaitMs` (default the lock contract's own
658
778
  * 10-minute staleness window) and does NOT consume the settle budget. When
@@ -665,7 +785,8 @@ function packageRootHealthy(packageRoot, fs) {
665
785
  * began, so a starved event loop only makes the wait coarser, never unbounded. A
666
786
  * stale lock (dead pid, older than the staleness window, unparseable) or our own
667
787
  * pid never extends anything. The whole wait is bounded (≤ `deadlineMs +
668
- * lockWaitMs`), so the caller can never hang.
788
+ * lockWaitMs`), and the unchanged-tree path normally returns after the much
789
+ * shorter `staleProbeMs` observation window.
669
790
  */
670
791
  export async function waitForInstallTreeSettled(args) {
671
792
  const now = args.now ?? Date.now;
@@ -676,7 +797,11 @@ export async function waitForInstallTreeSettled(args) {
676
797
  const quietMs = args.quietMs ?? 1500;
677
798
  const deadlineMs = args.deadlineMs;
678
799
  const lockWaitMs = args.lockWaitMs ?? DEFAULT_INSTALL_SETTLE_LOCK_WAIT_MS;
800
+ const staleProbeMs = Math.max(0, args.staleProbeMs ?? DEFAULT_INSTALL_TREE_STALE_PROBE_MS);
679
801
  const start = now();
802
+ let lastActivitySnapshot = installTreeActivitySignature(args.target, args.packageRoot, fs);
803
+ let staleObservationStartedAt = lastActivitySnapshot.observed ? start : null;
804
+ let sawTreeChange = false;
680
805
  let readySince = null;
681
806
  let sawLock = false;
682
807
  let sawRetired = false;
@@ -700,6 +825,32 @@ export async function waitForInstallTreeSettled(args) {
700
825
  sawLock = true;
701
826
  lockTool = boundedDiagnosticValue(lock.tool, LOCK_TOOL_BYTES);
702
827
  }
828
+ if (!sawTreeChange) {
829
+ const activitySnapshot = installTreeActivitySignature(args.target, args.packageRoot, fs);
830
+ if (!activitySnapshot.observed) {
831
+ staleObservationStartedAt = null;
832
+ }
833
+ else if (!lastActivitySnapshot.observed) {
834
+ staleObservationStartedAt = t;
835
+ }
836
+ else if (activitySnapshot.value !== lastActivitySnapshot.value) {
837
+ sawTreeChange = true;
838
+ staleObservationStartedAt = null;
839
+ }
840
+ lastActivitySnapshot = activitySnapshot;
841
+ }
842
+ let retiredPresent = false;
843
+ let retiredCheckFailed = false;
844
+ if (args.packageRoot) {
845
+ try {
846
+ retiredPresent = retiredSiblingPresent(args.packageRoot, fs);
847
+ if (retiredPresent)
848
+ sawRetired = true;
849
+ }
850
+ catch {
851
+ retiredCheckFailed = true;
852
+ }
853
+ }
703
854
  let ready = !foreignLockHeld && installTargetPresent(args.target, fs);
704
855
  if (ready && args.packageRoot) {
705
856
  try {
@@ -709,17 +860,8 @@ export async function waitForInstallTreeSettled(args) {
709
860
  catch {
710
861
  ready = false;
711
862
  }
712
- if (ready) {
713
- try {
714
- if (retiredSiblingPresent(args.packageRoot, fs)) {
715
- sawRetired = true;
716
- ready = false;
717
- }
718
- }
719
- catch {
720
- ready = false;
721
- }
722
- }
863
+ if (ready && (retiredPresent || retiredCheckFailed))
864
+ ready = false;
723
865
  }
724
866
  if (ready) {
725
867
  if (readySince === null)
@@ -731,7 +873,38 @@ export async function waitForInstallTreeSettled(args) {
731
873
  const waitedMs = t - start;
732
874
  if (ready && (deadlineMs === 0 || t - readySince >= quietMs)) {
733
875
  // Settling requires no foreign lock, so it is never held at the end here.
734
- return { settled: true, waitedMs, sawLock, sawRetired, lockHeldAtEnd: false, lockedMs, lockTool };
876
+ return {
877
+ settled: true,
878
+ waitedMs,
879
+ sawLock,
880
+ sawRetired,
881
+ lockHeldAtEnd: false,
882
+ lockedMs,
883
+ lockTool,
884
+ sawTreeChange,
885
+ staleTree: false,
886
+ };
887
+ }
888
+ if (!ready &&
889
+ !foreignLockHeld &&
890
+ !retiredPresent &&
891
+ !retiredCheckFailed &&
892
+ !sawLock &&
893
+ !sawRetired &&
894
+ !sawTreeChange &&
895
+ staleObservationStartedAt !== null &&
896
+ t - staleObservationStartedAt >= staleProbeMs) {
897
+ return {
898
+ settled: false,
899
+ waitedMs,
900
+ sawLock,
901
+ sawRetired,
902
+ lockHeldAtEnd: false,
903
+ lockedMs,
904
+ lockTool,
905
+ sawTreeChange,
906
+ staleTree: true,
907
+ };
735
908
  }
736
909
  // Give up when the TREE has had a full unlocked budget with no writer visible,
737
910
  // OR a writer is STILL holding the lock and has held it past its own staleness
@@ -742,7 +915,17 @@ export async function waitForInstallTreeSettled(args) {
742
915
  // `deadlineMs` 0 makes the unlocked test fire on the first poll (evaluate-once),
743
916
  // matching the pre-lock-aware contract; `lockHeldAtEnd` distinguishes the causes.
744
917
  if (unlockedMs >= deadlineMs || (foreignLockHeld && lockedMs >= lockWaitMs)) {
745
- return { settled: false, waitedMs, sawLock, sawRetired, lockHeldAtEnd: foreignLockHeld, lockedMs, lockTool };
918
+ return {
919
+ settled: false,
920
+ waitedMs,
921
+ sawLock,
922
+ sawRetired,
923
+ lockHeldAtEnd: foreignLockHeld,
924
+ lockedMs,
925
+ lockTool,
926
+ sawTreeChange,
927
+ staleTree: false,
928
+ };
746
929
  }
747
930
  // The first poll at which the wait has reached `deadlineMs` while a lock is
748
931
  // still held is exactly where the pre-lock-aware wait gave up — announce the
@@ -808,7 +991,7 @@ export function installRewriteInProgress(args) {
808
991
  }
809
992
  }
810
993
  /** The fixed, input-free carrier message (grouping cardinality stays bounded). */
811
- const INSTALL_TREE_TORN_MESSAGE = "hq-cli install tree was being rewritten while this command started";
994
+ const INSTALL_TREE_TORN_MESSAGE = "hq-cli could not load a module from its install tree and automatic recovery did not complete";
812
995
  /**
813
996
  * A torn-install failure that stays VISIBLE in Sentry with bounded, hq-derived
814
997
  * diagnostics. Fixed `name`/`message` (no argv, path, or free text in either),
@@ -822,6 +1005,8 @@ export class InstallTreeTornError extends Error {
822
1005
  super(INSTALL_TREE_TORN_MESSAGE);
823
1006
  this.name = "InstallTreeTornError";
824
1007
  this.cause = init.cause;
1008
+ const sawTreeChange = init.sawTreeChange ?? false;
1009
+ const treeState = init.treeState ?? (init.outcome === "stale_tree" ? "stale" : "in_flight");
825
1010
  this.diagnostics = {
826
1011
  dialect: init.classified.dialect,
827
1012
  code: init.classified.code,
@@ -829,10 +1014,12 @@ export class InstallTreeTornError extends Error {
829
1014
  importer: boundedDiagnosticValue(init.classified.importer, IMPORTER_BYTES),
830
1015
  packageRoot: boundedDiagnosticValue(init.packageRoot ?? "", PACKAGE_ROOT_BYTES),
831
1016
  outcome: init.outcome,
1017
+ treeState,
832
1018
  attempt: init.attempt,
833
1019
  waitedMs: init.waitedMs,
834
1020
  sawLock: init.sawLock,
835
1021
  sawRetired: init.sawRetired,
1022
+ sawTreeChange,
836
1023
  lockHeldAtEnd: init.lockHeldAtEnd ?? false,
837
1024
  lockedMs: init.lockedMs ?? 0,
838
1025
  lockTool: boundedDiagnosticValue(init.lockTool ?? "", LOCK_TOOL_BYTES),
@@ -846,8 +1033,45 @@ export function isInstallTreeTornError(err) {
846
1033
  return err instanceof InstallTreeTornError;
847
1034
  }
848
1035
  /** The exact Sentry `contexts` payload for a captured torn-install failure. */
849
- export function installTreeTornCaptureContext(err) {
850
- return { contexts: { install_tree_torn: err.diagnostics } };
1036
+ export function installTreeTornCaptureContext(err, homeDirectory = homedir()) {
1037
+ const diagnostics = err.diagnostics;
1038
+ return {
1039
+ contexts: {
1040
+ install_tree_torn: {
1041
+ ...diagnostics,
1042
+ specifier: scrubHomeDirectoryPrefix(diagnostics.specifier, homeDirectory),
1043
+ importer: scrubHomeDirectoryPrefix(diagnostics.importer, homeDirectory),
1044
+ packageRoot: scrubHomeDirectoryPrefix(diagnostics.packageRoot, homeDirectory),
1045
+ },
1046
+ },
1047
+ };
1048
+ }
1049
+ function looksLikeWindowsPath(value) {
1050
+ return /^[a-zA-Z]:[\\/]/.test(value) || /^\\\\/.test(value);
1051
+ }
1052
+ /** Replace a home-directory prefix without matching similarly-named sibling folders. */
1053
+ export function scrubHomeDirectoryPrefix(value, homeDirectory = homedir()) {
1054
+ if (!value || !homeDirectory)
1055
+ return value;
1056
+ if (looksLikeWindowsPath(value) || looksLikeWindowsPath(homeDirectory)) {
1057
+ const home = path.win32.normalize(homeDirectory).replace(/[\\/]+$/, "");
1058
+ const normalized = path.win32.normalize(value);
1059
+ const foldedHome = home.toLowerCase();
1060
+ const foldedValue = normalized.toLowerCase();
1061
+ if (foldedValue === foldedHome)
1062
+ return "~";
1063
+ const prefix = `${foldedHome}\\`;
1064
+ if (foldedValue.startsWith(prefix))
1065
+ return `~\\${normalized.slice(home.length + 1)}`;
1066
+ return value;
1067
+ }
1068
+ const home = path.posix.normalize(homeDirectory).replace(/\/+$/, "");
1069
+ const normalized = path.posix.normalize(value);
1070
+ if (normalized === home)
1071
+ return "~";
1072
+ if (normalized.startsWith(`${home}/`))
1073
+ return `~/${normalized.slice(home.length + 1)}`;
1074
+ return value;
851
1075
  }
852
1076
  /**
853
1077
  * The fixed, input-free operator remedy. It names the likely writers (so a
@@ -855,10 +1079,15 @@ export function installTreeTornCaptureContext(err) {
855
1079
  * and interpolates NOTHING — the only per-event value is the bounded specifier
856
1080
  * appended by {@link installTreeTornStderrLine}.
857
1081
  */
858
- export const INSTALL_TREE_TORN_REMEDY = "the hq-cli install was being updated by another process (the box's hq-cli-update " +
859
- "timer, the desktop app, or another hq command) while this command started. " +
860
- "Re-run your command; if it keeps failing, reinstall with " +
861
- "`npm i -g @indigoai-us/hq-cli` (or `pnpm add -g @indigoai-us/hq-cli`).";
1082
+ export const INSTALL_TREE_TORN_REMEDY = "hq-cli could not finish automatic install-tree recovery. Re-run your command; " +
1083
+ "if it still fails, reinstall with `npm install -g @indigoai-us/hq-cli@latest` " +
1084
+ "(or `pnpm add -g @indigoai-us/hq-cli@latest` / `bun add -g @indigoai-us/hq-cli@latest` " +
1085
+ "when that manager owns your install).";
1086
+ /** Fixed repair guidance for an unchanged tree with no active updater signal. */
1087
+ export const INSTALL_TREE_STALE_REMEDY = "hq-cli's install tree is incomplete and no active updater was detected. " +
1088
+ "Reinstall with `npm install -g @indigoai-us/hq-cli@latest` " +
1089
+ "(or `pnpm add -g @indigoai-us/hq-cli@latest` / `bun add -g @indigoai-us/hq-cli@latest` " +
1090
+ "when that manager owns your install), then run the command again.";
862
1091
  /**
863
1092
  * The fixed, input-free remedy for the 'lock-held' outcome: a cooperating writer
864
1093
  * held the shared update lock past its own staleness ceiling. Telling the user to
@@ -877,7 +1106,11 @@ export const INSTALL_TREE_LOCK_HELD_REMEDY = "another hq updater (the desktop ap
877
1106
  * {@link INSTALL_TREE_TORN_REMEDY} byte-for-byte.
878
1107
  */
879
1108
  export function installTreeTornStderrLine(err) {
880
- const remedy = err.diagnostics.outcome === "lock-held" ? INSTALL_TREE_LOCK_HELD_REMEDY : INSTALL_TREE_TORN_REMEDY;
1109
+ const remedy = err.diagnostics.outcome === "lock-held"
1110
+ ? INSTALL_TREE_LOCK_HELD_REMEDY
1111
+ : err.diagnostics.outcome === "stale_tree"
1112
+ ? INSTALL_TREE_STALE_REMEDY
1113
+ : INSTALL_TREE_TORN_REMEDY;
881
1114
  const specifier = err.diagnostics.specifier;
882
1115
  return specifier ? `${remedy} (missing: ${specifier})` : remedy;
883
1116
  }
@@ -1,30 +1,44 @@
1
- export type PlanGateCode = "PLAN_LIMIT_EXCEEDED" | "PLAN_REQUIRED"
2
- /**
3
- * US-054: a PERSONAL scope asked for a feature the paid Individual plan
4
- * includes and the free tier does not. A different plan, a different price
5
- * and a different remedy from the company codes above, so it is its own
6
- * code rather than a reuse of `PLAN_REQUIRED` with company copy.
7
- */
8
- | "PERSONAL_PLAN_REQUIRED";
1
+ export type PlanGateCode = "PLAN_LIMIT_EXCEEDED" | "PLAN_REQUIRED" | "MEETING_PLAN_REQUIRED"
2
+ /** A PERSONAL scope asked for a feature the paid Individual plan includes. */
3
+ | "PERSONAL_PLAN_REQUIRED"
4
+ /** External agent enrollment was denied by the company plan. */
5
+ | "BILLING_REQUIRED";
9
6
  export interface PlanGateDetails {
10
7
  resource?: string;
11
- /** The refused feature, on a `PERSONAL_PLAN_REQUIRED` denial. */
12
8
  feature?: string;
13
9
  used?: number;
14
10
  limit?: number;
15
11
  upgradeUrl?: string;
16
- /** Best-effort target captured by the shared HTTP client for interactive checkout. */
12
+ reason?: string;
13
+ /** Company target from request context or the server's UID-keyed billing URL. */
17
14
  companyUid?: string;
18
15
  }
16
+ /** One renderer for server-selected upgrade links across CLI plan refusals. */
17
+ export declare function formatUpgradeRefusal(reason: string, upgradeUrl: string | undefined, options?: {
18
+ urlOnly?: boolean;
19
+ }): string;
20
+ /** Recover a company UID only from the server's UID-keyed billing callback. */
21
+ export declare function companyUidFromPlanUpgradeUrl(value: unknown): string | undefined;
19
22
  export declare class PlanGateError extends Error {
20
23
  readonly code: PlanGateCode;
21
24
  readonly details: PlanGateDetails;
22
25
  constructor(code: PlanGateCode, details: PlanGateDetails);
23
26
  }
24
27
  export declare function isPlanGateError(err: unknown): err is PlanGateError;
25
- export declare function formatPlanGateError(err: PlanGateError): string;
28
+ /** Plain, stable CLI copy for plan-gated resource creation. */
29
+ export declare function formatPlanGateError(err: PlanGateError, options?: {
30
+ urlOnly?: boolean;
31
+ }): string;
26
32
  export declare function canOfferTeamUpgrade(argv?: readonly string[]): boolean;
27
- /** Ask once, only on a real interactive terminal. */
33
+ export interface UpgradeLinkOfferDependencies {
34
+ ask?: (question: string) => Promise<string>;
35
+ openUrl?: (url: string) => Promise<void>;
36
+ argv?: readonly string[];
37
+ interactive?: boolean;
38
+ }
39
+ /** Ask before opening the exact URL returned by the server. */
40
+ export declare function offerUpgradeLink(url: string, deps?: UpgradeLinkOfferDependencies): Promise<boolean>;
41
+ /** Legacy checkout callback retained for callers outside the shared refusal paths. */
28
42
  export declare function offerTeamUpgrade(onConfirm: () => Promise<void>, deps?: {
29
43
  ask?: (question: string) => Promise<string>;
30
44
  argv?: readonly string[];
@@ -33,7 +47,7 @@ export declare function offerTeamUpgrade(onConfirm: () => Promise<void>, deps?:
33
47
  /** Decode an already-read JSON envelope from either shared HTTP client. */
34
48
  export declare function planGateErrorFromPayload(status: number, body: unknown, companyUid?: string): PlanGateError | null;
35
49
  /**
36
- * Decode only the two deliberate plan-gate envelopes. Malformed, unrelated,
50
+ * Decode the supported plan-gate envelopes. Malformed, unrelated,
37
51
  * or non-402 responses retain their existing command-specific handling.
38
52
  */
39
53
  export declare function planGateErrorFromResponse(response: Response, companyUid?: string): Promise<{