@indigoai-us/hq-cli 5.345.156 → 5.345.160

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 (188) hide show
  1. package/CHANGELOG.md +255 -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 +535 -10
  6. package/dist/command-catalog.generated.js +678 -31
  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/access.d.ts +6 -0
  12. package/dist/commands/access.js +18 -1
  13. package/dist/commands/acl-lock.d.ts +41 -0
  14. package/dist/commands/acl-lock.js +28 -0
  15. package/dist/commands/agent-enroll.d.ts +2 -0
  16. package/dist/commands/agent-enroll.js +9 -4
  17. package/dist/commands/agent-kit.js +8 -6
  18. package/dist/commands/agents.js +5 -11
  19. package/dist/commands/billing-sponsor.d.ts +4 -4
  20. package/dist/commands/billing-sponsor.js +11 -11
  21. package/dist/commands/billing-usage.d.ts +22 -8
  22. package/dist/commands/billing-usage.js +57 -22
  23. package/dist/commands/billing.d.ts +7 -7
  24. package/dist/commands/billing.js +13 -13
  25. package/dist/commands/bot.js +7 -4
  26. package/dist/commands/channel-suggest.d.ts +56 -0
  27. package/dist/commands/channel-suggest.js +170 -0
  28. package/dist/commands/channels.js +7 -1
  29. package/dist/commands/cloud-provision.js +2 -2
  30. package/dist/commands/cloud.d.ts +45 -0
  31. package/dist/commands/cloud.js +157 -17
  32. package/dist/commands/company.js +4 -3
  33. package/dist/commands/core-checkpoint-native.d.ts +18 -0
  34. package/dist/commands/core-checkpoint-native.js +67 -2
  35. package/dist/commands/core-pipeline-driver-native.d.ts +12 -1
  36. package/dist/commands/core-pipeline-driver-native.js +131 -5
  37. package/dist/commands/core.js +12 -0
  38. package/dist/commands/crm.js +83 -6
  39. package/dist/commands/db-remote-deps.js +12 -1
  40. package/dist/commands/deploy-access.d.ts +33 -0
  41. package/dist/commands/deploy-access.js +322 -0
  42. package/dist/commands/dm-attach.d.ts +106 -0
  43. package/dist/commands/dm-attach.js +184 -0
  44. package/dist/commands/dm.d.ts +37 -0
  45. package/dist/commands/dm.js +172 -10
  46. package/dist/commands/feedback-show.d.ts +40 -0
  47. package/dist/commands/feedback-show.js +261 -0
  48. package/dist/commands/feedback.d.ts +21 -0
  49. package/dist/commands/feedback.js +152 -2
  50. package/dist/commands/files.d.ts +24 -0
  51. package/dist/commands/files.js +71 -8
  52. package/dist/commands/integrations-connect.js +7 -3
  53. package/dist/commands/lanes-accounts.js +16 -1
  54. package/dist/commands/lanes-loop.js +14 -4
  55. package/dist/commands/lanes-message.js +6 -4
  56. package/dist/commands/lanes-resume.js +36 -13
  57. package/dist/commands/lanes-shared.d.ts +5 -0
  58. package/dist/commands/lanes-shared.js +13 -3
  59. package/dist/commands/lanes-watch.js +1 -0
  60. package/dist/commands/lanes.d.ts +20 -1
  61. package/dist/commands/lanes.js +201 -51
  62. package/dist/commands/meetings.d.ts +4 -4
  63. package/dist/commands/meetings.js +17 -8
  64. package/dist/commands/mesh.js +149 -1
  65. package/dist/commands/monitor.d.ts +1 -0
  66. package/dist/commands/monitor.js +4 -1
  67. package/dist/commands/repo.d.ts +41 -0
  68. package/dist/commands/repo.js +331 -0
  69. package/dist/commands/scan.d.ts +3 -1
  70. package/dist/commands/scan.js +7 -3
  71. package/dist/commands/secrets.d.ts +21 -0
  72. package/dist/commands/secrets.js +114 -14
  73. package/dist/commands/signals.js +17 -5
  74. package/dist/commands/skill.js +6 -6
  75. package/dist/commands/staff/listings.d.ts +41 -0
  76. package/dist/commands/staff/listings.js +160 -0
  77. package/dist/commands/staff/reset-test-account.d.ts +3 -2
  78. package/dist/commands/staff/reset-test-account.js +4 -3
  79. package/dist/commands/whoami.js +1 -1
  80. package/dist/hook-engine-path.js +13 -2
  81. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +3392 -1243
  82. package/dist/hq-cloud-sync-runner/package.json +1 -1
  83. package/dist/lib/agent-kit/run/inbox.js +2 -1
  84. package/dist/lib/agent-kit/wake-secrets.d.ts +7 -0
  85. package/dist/lib/agent-kit/wake-secrets.js +32 -0
  86. package/dist/lib/agent-kit/wake.d.ts +4 -0
  87. package/dist/lib/agent-kit/wake.js +49 -3
  88. package/dist/lib/billing/plan-limit-refusals.d.ts +13 -3
  89. package/dist/lib/billing/plan-limit-refusals.js +26 -3
  90. package/dist/lib/billing/plan-limit-upgrade-url.d.ts +8 -0
  91. package/dist/lib/billing/plan-limit-upgrade-url.js +18 -0
  92. package/dist/lib/billing/plan-lock.d.ts +22 -6
  93. package/dist/lib/billing/plan-lock.js +58 -30
  94. package/dist/lib/billing/usage-cost.d.ts +24 -2
  95. package/dist/lib/billing/usage-cost.js +103 -3
  96. package/dist/lib/billing/usage-only.d.ts +37 -1
  97. package/dist/lib/billing/usage-only.js +89 -4
  98. package/dist/lib/billing/usage-pause.d.ts +27 -0
  99. package/dist/lib/billing/usage-pause.js +50 -0
  100. package/dist/lib/bot/inflight.d.ts +7 -1
  101. package/dist/lib/bot/inflight.js +10 -2
  102. package/dist/lib/bot/prompt.js +3 -0
  103. package/dist/lib/bot/run.js +72 -20
  104. package/dist/lib/bot/session.d.ts +14 -0
  105. package/dist/lib/bot/session.js +70 -0
  106. package/dist/lib/capacity/collectors/claude-oauth.d.ts +6 -0
  107. package/dist/lib/capacity/collectors/claude-oauth.js +12 -0
  108. package/dist/lib/capacity/lane-login.d.ts +6 -0
  109. package/dist/lib/capacity/lane-login.js +5 -0
  110. package/dist/lib/core-utils/worktree.js +2 -0
  111. package/dist/lib/daemon/exit-cause.d.ts +43 -0
  112. package/dist/lib/daemon/exit-cause.js +88 -0
  113. package/dist/lib/daemon/handoff.d.ts +2 -0
  114. package/dist/lib/daemon/handoff.js +6 -1
  115. package/dist/lib/daemon/hq-child-bootstrap.mjs +20 -3
  116. package/dist/lib/daemon/paths.d.ts +2 -0
  117. package/dist/lib/daemon/paths.js +1 -0
  118. package/dist/lib/daemon/run.js +13 -0
  119. package/dist/lib/daemon/supervisor.d.ts +54 -1
  120. package/dist/lib/daemon/supervisor.js +297 -21
  121. package/dist/lib/daemon/unit-holds.d.ts +22 -0
  122. package/dist/lib/daemon/unit-holds.js +86 -0
  123. package/dist/lib/db/control-plane.d.ts +5 -0
  124. package/dist/lib/db/control-plane.js +10 -0
  125. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  126. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  127. package/dist/lib/hook-engine/resolve.d.ts +14 -0
  128. package/dist/lib/hook-engine/resolve.js +55 -1
  129. package/dist/lib/index-render/company-knowledge.js +1 -3
  130. package/dist/lib/index-render/projects.js +1 -3
  131. package/dist/lib/lanes/account-company.d.ts +28 -0
  132. package/dist/lib/lanes/account-company.js +53 -0
  133. package/dist/lib/lanes/accounts.d.ts +22 -3
  134. package/dist/lib/lanes/accounts.js +38 -2
  135. package/dist/lib/lanes/claude-balance.d.ts +6 -1
  136. package/dist/lib/lanes/claude-balance.js +6 -1
  137. package/dist/lib/lanes/claude-credential.d.ts +78 -0
  138. package/dist/lib/lanes/claude-credential.js +163 -0
  139. package/dist/lib/lanes/compaction.d.ts +2 -0
  140. package/dist/lib/lanes/compaction.js +9 -0
  141. package/dist/lib/lanes/config.js +6 -0
  142. package/dist/lib/lanes/envelope-repair-dispatch.js +2 -1
  143. package/dist/lib/lanes/hooks-install.js +2 -1
  144. package/dist/lib/lanes/host-capacity.d.ts +2 -0
  145. package/dist/lib/lanes/host-capacity.js +1 -0
  146. package/dist/lib/lanes/keep-alive.d.ts +1 -1
  147. package/dist/lib/lanes/keep-alive.js +3 -1
  148. package/dist/lib/lanes/links.js +10 -4
  149. package/dist/lib/lanes/loop.js +16 -1
  150. package/dist/lib/lanes/node-binary.d.ts +17 -0
  151. package/dist/lib/lanes/node-binary.js +30 -0
  152. package/dist/lib/lanes/outpost-dispatch.d.ts +2 -1
  153. package/dist/lib/lanes/outpost-dispatch.js +1 -0
  154. package/dist/lib/lanes/reconcile.js +4 -0
  155. package/dist/lib/lanes/remote-control-lane.js +2 -0
  156. package/dist/lib/lanes/spawn.d.ts +3 -0
  157. package/dist/lib/lanes/spawn.js +119 -9
  158. package/dist/lib/lanes/store.d.ts +6 -0
  159. package/dist/lib/lanes/store.js +107 -18
  160. package/dist/lib/lanes/types.d.ts +2 -0
  161. package/dist/lib/lanes/watch.js +28 -5
  162. package/dist/lib/lanes/watcher-main.js +4 -3
  163. package/dist/lib/lanes/watcher.d.ts +4 -0
  164. package/dist/lib/lanes/watcher.js +110 -21
  165. package/dist/lib/mesh/api.d.ts +32 -0
  166. package/dist/lib/mesh/api.js +75 -0
  167. package/dist/lib/plan-limit-nag.d.ts +0 -16
  168. package/dist/lib/plan-limit-nag.js +17 -18
  169. package/dist/lib/secret-scan/detect.js +9 -0
  170. package/dist/lib/secret-scan/repo-config.d.ts +41 -0
  171. package/dist/lib/secret-scan/repo-config.js +237 -0
  172. package/dist/lib/secret-scan/rules.d.ts +7 -1
  173. package/dist/lib/secret-scan/rules.js +3 -1
  174. package/dist/lib/secret-scan/staged.d.ts +5 -0
  175. package/dist/lib/secret-scan/staged.js +19 -2
  176. package/dist/lib/secret-scan/types.d.ts +2 -0
  177. package/dist/lib/sync/server-owned-local-only.d.ts +29 -0
  178. package/dist/lib/sync/server-owned-local-only.js +75 -0
  179. package/dist/main.d.ts +0 -2
  180. package/dist/main.js +19 -57
  181. package/dist/utils/package-use-lease.js +27 -9
  182. package/dist/utils/plan-gate-error.d.ts +30 -14
  183. package/dist/utils/plan-gate-error.js +60 -46
  184. package/dist/utils/team-upgrade.d.ts +19 -6
  185. package/dist/utils/team-upgrade.js +37 -39
  186. package/dist/utils/version-gate.d.ts +7 -0
  187. package/dist/utils/version-gate.js +66 -0
  188. package/package.json +8 -8
@@ -12,27 +12,27 @@
12
12
  *
13
13
  * Tenant boundary: the company comes from the caller's own active memberships
14
14
  * (`/membership/me`); a slug outside them is refused before any read. The
15
- * usage-limits routes are member-gated on the server as well. With no cloud
15
+ * company statement is owner-gated on the server as well. With no cloud
16
16
  * company (or `--personal`) it reads the caller's personal scope.
17
17
  *
18
- * Usage numbers come through `readMonthUsage` (src/lib/billing/usage-cost.ts),
19
- * the single seam to switch to the US-005 daily rollup once it has a read route.
18
+ * Usage numbers come through `readMonthUsage` (src/lib/billing/usage-cost.ts)
19
+ * from the usage statement, `GET /v1/billing/usage` (hq-pro US-006). Only a
20
+ * server without that route (404) falls back to the usage-limits readings.
20
21
  *
21
22
  * Only an account on usage pricing gets costs. The plan comes from the
22
- * server: `pricingPlan` / `usageOnly` on the usage-limits body (hq-pro #5042,
23
- * computed with `isOnUsagePricing`). Only when that body does not say, the
24
- * billing summary the command already reads for the trial. A flat-plan account (for example a Workforce
25
- * company that subscribed before the 2026-10-16 cutover) and an account the
26
- * server says nothing about get readings only, with no rate, cost or
23
+ * server's shared plan status (`billingState`, hq-pro #5088) on the billing
24
+ * summary, with the legacy `pricingPlan` / `usageOnly` fields for a server
25
+ * that does not send it. An account on an earlier flat plan and an account
26
+ * the server says nothing about get readings only, with no rate, cost or
27
27
  * projected bill.
28
28
  */
29
29
  import chalk from "chalk";
30
30
  import { ensureCognitoToken } from "../utils/cognito-session.js";
31
31
  import { vaultApiFetch, vaultApiFetchPublic } from "../utils/vault-api.js";
32
- import { buildReadingsReport, buildReport, formatReport, readMonthUsage, } from "../lib/billing/usage-cost.js";
32
+ import { buildReadingsReport, buildReport, formatReport, readMonthUsage, usageLimitsSource, usageStatementSource, } from "../lib/billing/usage-cost.js";
33
33
  import { ratesFromPricingStatement } from "../lib/billing/usage-rate-card.js";
34
34
  import { fetchUsageCard, formatUsageCardLines } from "../lib/billing/usage-card.js";
35
- import { fetchCompanyBillingSummary, pricingPlanFromBody, usageTrialActiveFromBody, } from "../lib/billing/usage-only.js";
35
+ import { fetchCompanyBillingSummary, pricingPlanFromBody, hasBillingStateField, usageTrialActiveFromBody, } from "../lib/billing/usage-only.js";
36
36
  export class UsageScopeError extends Error {
37
37
  }
38
38
  /** Pick the scope. Never reaches outside the caller's own memberships. */
@@ -67,7 +67,7 @@ function asRecord(value) {
67
67
  /** The plan on a usage-limits body: `pricingPlan`, else the `usageOnly` / `onUsagePricing` boolean. */
68
68
  export function pricingPlanFromUsageLimits(body) {
69
69
  const plan = pricingPlanFromBody(body);
70
- if (plan)
70
+ if (plan || hasBillingStateField(body))
71
71
  return plan;
72
72
  const rec = asRecord(body);
73
73
  const flag = typeof rec?.onUsagePricing === "boolean" ? rec.onUsagePricing : rec?.usageOnly;
@@ -85,9 +85,12 @@ export function pricingPlanFromSummary(body) {
85
85
  const rec = asRecord(body);
86
86
  if (!rec)
87
87
  return null;
88
- // `plan: "usage"` only means "no Workforce subscription", not usage pricing,
89
- // so it never turns pricing on. Without pricingPlan, show no cost.
90
- return pricingPlanFromBody(rec) ?? "current";
88
+ // `plan: "usage"` only means "no flat-plan subscription", not usage pricing,
89
+ // so it never turns pricing on. A server that sends billingState answers
90
+ // from it alone (unknown stays unknown); an older one without pricingPlan
91
+ // shows no cost.
92
+ const plan = pricingPlanFromBody(rec);
93
+ return hasBillingStateField(rec) ? plan : plan ?? "current";
91
94
  }
92
95
  /**
93
96
  * True: price the meters. False: flat plan. Null: unknown, show no cost.
@@ -101,20 +104,44 @@ export function resolveOnUsagePricing(limitsPlan, summaryPlan) {
101
104
  return null;
102
105
  return plan === "usage-only";
103
106
  }
107
+ /** Thrown when the company statement is refused to a caller who is not its owner. */
108
+ export class UsageStatementForbiddenError extends Error {
109
+ }
110
+ /**
111
+ * Read the statement, or say the caller should use the legacy readings: null
112
+ * means the server has no statement route (404) or no reader was given.
113
+ */
114
+ async function readStatementBody(scope, deps) {
115
+ if (!deps.fetchUsageStatement)
116
+ return null;
117
+ const res = await deps.fetchUsageStatement(scope);
118
+ if (res.status === 404)
119
+ return null;
120
+ if (res.status === 403 && scope.kind === "company") {
121
+ throw new UsageStatementForbiddenError(`Only the owner of '${scope.companySlug}' can see its usage statement. ` +
122
+ "Run `hq billing usage --personal` for your own usage.");
123
+ }
124
+ return { body: res.status >= 200 && res.status < 300 ? res.body : null };
125
+ }
104
126
  export async function runBillingUsage(opts, deps) {
105
127
  const memberships = await deps.listCompanies();
106
128
  const scope = resolveUsageScope(memberships, opts);
107
129
  const membership = scope.kind === "company" ? memberships.find((m) => m.uid === scope.companyUid) : undefined;
108
130
  let limitsBody = null;
131
+ const statement = await readStatementBody(scope, deps);
132
+ const usageDeps = {
133
+ fetchUsageLimits: async (s) => (limitsBody = await deps.fetchUsageLimits(s)),
134
+ now: deps.now,
135
+ ...(deps.readTrialActive ? { readTrialActive: deps.readTrialActive } : {}),
136
+ ...(opts.storageScan !== false && deps.readStorageSplit && membership
137
+ ? { readStorageSplit: () => deps.readStorageSplit(membership, { fresh: opts.fresh === true }) }
138
+ : {}),
139
+ };
140
+ const source = statement
141
+ ? usageStatementSource(async () => statement.body)
142
+ : usageLimitsSource(usageDeps);
109
143
  const [usage, pricing] = await Promise.all([
110
- readMonthUsage(scope, {
111
- fetchUsageLimits: async (s) => (limitsBody = await deps.fetchUsageLimits(s)),
112
- now: deps.now,
113
- ...(deps.readTrialActive ? { readTrialActive: deps.readTrialActive } : {}),
114
- ...(opts.storageScan !== false && deps.readStorageSplit && membership
115
- ? { readStorageSplit: () => deps.readStorageSplit(membership, { fresh: opts.fresh === true }) }
116
- : {}),
117
- }),
144
+ readMonthUsage(scope, usageDeps, source),
118
145
  deps.fetchPricing().catch(() => null),
119
146
  ]);
120
147
  const limitsPlan = pricingPlanFromUsageLimits(limitsBody);
@@ -142,6 +169,14 @@ function productionDeps() {
142
169
  const { companiesFromMemberships } = await import("./storage.js");
143
170
  return companiesFromMemberships(await res.json());
144
171
  },
172
+ async fetchUsageStatement(scope) {
173
+ const res = await vaultApiFetch(scope.kind === "company"
174
+ ? { token: await token(), path: "/v1/billing/usage", query: { companyUid: scope.companyUid } }
175
+ : { token: await token(), path: "/v1/billing/usage", query: { scope: "person" } });
176
+ if (!res.ok)
177
+ return { status: res.status, body: null };
178
+ return { status: res.status, body: await res.json().catch(() => null) };
179
+ },
145
180
  async fetchUsageLimits(scope) {
146
181
  const res = await vaultApiFetch(scope.kind === "company"
147
182
  ? { token: await token(), path: "/v1/billing/usage-limits", query: { companyUid: scope.companyUid } }
@@ -13,16 +13,16 @@
13
13
  * hq billing checkout [--company <slug>] [--personal] — mint a card-capture link
14
14
  * hq billing usage [--company <slug>] [--personal] — usage, projected bill,
15
15
  * drivers (read-only)
16
- * hq billing upgrade [--company <slug>] — Workforce checkout
17
- * (legacy plans only)
16
+ * hq billing upgrade [--company <slug>] — the switch to usage
17
+ * pricing (earlier plans)
18
18
  * hq billing sponsor … — ask a company to cover
19
- * your $50 plan, or
19
+ * your personal plan, or
20
20
  * answer such a request
21
21
  *
22
- * Usage pricing (US-012): a company whose billing summary says
23
- * `pricingPlan: "usage-only"` has no plan to upgrade to. `hq billing upgrade`
24
- * tells it so and points at `hq billing usage` instead of opening Workforce
25
- * checkout. Legacy companies keep the Workforce flow unchanged.
22
+ * Usage pricing (US-012, US-024): a company on usage pricing has no plan to
23
+ * upgrade to; `hq billing upgrade` tells it so and points at
24
+ * `hq billing usage`. Every other company is pointed at the switch to usage
25
+ * pricing: the flat-plan checkout is closed, so the CLI never opens it.
26
26
  */
27
27
  import { Command } from "commander";
28
28
  interface DatabaseBilling {
@@ -13,22 +13,22 @@
13
13
  * hq billing checkout [--company <slug>] [--personal] — mint a card-capture link
14
14
  * hq billing usage [--company <slug>] [--personal] — usage, projected bill,
15
15
  * drivers (read-only)
16
- * hq billing upgrade [--company <slug>] — Workforce checkout
17
- * (legacy plans only)
16
+ * hq billing upgrade [--company <slug>] — the switch to usage
17
+ * pricing (earlier plans)
18
18
  * hq billing sponsor … — ask a company to cover
19
- * your $50 plan, or
19
+ * your personal plan, or
20
20
  * answer such a request
21
21
  *
22
- * Usage pricing (US-012): a company whose billing summary says
23
- * `pricingPlan: "usage-only"` has no plan to upgrade to. `hq billing upgrade`
24
- * tells it so and points at `hq billing usage` instead of opening Workforce
25
- * checkout. Legacy companies keep the Workforce flow unchanged.
22
+ * Usage pricing (US-012, US-024): a company on usage pricing has no plan to
23
+ * upgrade to; `hq billing upgrade` tells it so and points at
24
+ * `hq billing usage`. Every other company is pointed at the switch to usage
25
+ * pricing: the flat-plan checkout is closed, so the CLI never opens it.
26
26
  */
27
27
  import chalk from "chalk";
28
28
  import { ensureCognitoToken } from "../utils/cognito-session.js";
29
29
  import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
30
30
  import { mintPaymentLink } from "../utils/billing-gate.js";
31
- import { upgradeToTeam } from "../utils/team-upgrade.js";
31
+ import { openUsageSwitch } from "../utils/team-upgrade.js";
32
32
  import { registerBillingSponsorCommands } from "./billing-sponsor.js";
33
33
  import { fetchCompanyPricingPlan, isUsageOnly, usageOnlyUpgradeLines, } from "../lib/billing/usage-only.js";
34
34
  import { fetchUsageCard } from "../lib/billing/usage-card.js";
@@ -157,10 +157,10 @@ export function registerBillingCommand(program) {
157
157
  });
158
158
  billing
159
159
  .command("upgrade")
160
- .description("Open Stripe Checkout to upgrade a company on an earlier plan (usage-pricing companies have no plan to upgrade)")
160
+ .description("Open the switch to usage pricing for a company on an earlier plan (usage-pricing companies have nothing to change)")
161
161
  .option("--company <slug>", "Company slug (resolves to companyUid)")
162
- .option("--no-browser", "Print the Stripe Checkout URL instead of opening it")
163
- .option("--json", "Emit the Checkout URL as JSON")
162
+ .option("--no-browser", "Print the URL instead of opening it")
163
+ .option("--json", "Emit the URL as JSON")
164
164
  .action(async function (opts) {
165
165
  try {
166
166
  const token = await ensureCognitoToken();
@@ -177,8 +177,8 @@ export function registerBillingCommand(program) {
177
177
  console.log(line);
178
178
  return;
179
179
  }
180
- await upgradeToTeam({
181
- companyUid,
180
+ await openUsageSwitch({
181
+ companySlug: companyOf(this),
182
182
  noBrowser: opts.browser === false,
183
183
  json: opts.json,
184
184
  });
@@ -44,7 +44,7 @@ import { DEFAULT_COGNITO, ensureCognitoToken, resolveDefaultHqRoot, } from "../u
44
44
  import { peekHqApiKey } from "../utils/resolve-vault-credential.js";
45
45
  import { getCompanyUid, resolveCallerPersonUid } from "../utils/vault-api.js";
46
46
  import { resolveHqBinary } from "../lib/mesh/live/daemon/install.js";
47
- import { askDaemonToStart, askDaemonToStop, daemonOwnsProcess, daemonSupervises, daemonWillSupervise, handOffInstallHere, } from "../lib/daemon/handoff.js";
47
+ import { askDaemonToStart, askDaemonToStop, daemonOwnsProcess, daemonHoldReason, daemonSupervises, daemonWillSupervise, handOffInstallHere, } from "../lib/daemon/handoff.js";
48
48
  import { daemonPaths } from "../lib/daemon/paths.js";
49
49
  import { launchdLabel } from "../lib/bot/paths.js";
50
50
  import { isAncestorProcess } from "../lib/bot/self-process.js";
@@ -286,9 +286,11 @@ async function startBot(name, dir, config, output, extra = {}) {
286
286
  const d = botDaemonStatus(paths, daemonDeps());
287
287
  const hqDaemon = daemonPaths();
288
288
  let how;
289
+ let supervised = false;
289
290
  // A bot created since the daemon last read its plan is the daemon's too,
290
291
  // unless its own LaunchAgent runs it.
291
292
  if (daemonSupervises(hqDaemon, `bots:${name}`) || (!d.installed && daemonWillSupervise(hqDaemon, `bots:${name}`))) {
293
+ supervised = true;
292
294
  // `hq daemon` runs this bot; starting a copy here would run it twice.
293
295
  const before = readBotStatus(dir);
294
296
  if (before && isPidAlive(before.pid) && before.state === "running") {
@@ -322,18 +324,19 @@ async function startBot(name, dir, config, output, extra = {}) {
322
324
  return !!s && s.state === "running" && isPidAlive(s.pid);
323
325
  }, 15_000);
324
326
  const status = readBotStatus(dir);
327
+ const lastError = (!running && supervised ? daemonHoldReason(hqDaemon, `bots:${name}`) : null) ?? status?.lastError ?? null;
325
328
  if (output === "silent")
326
329
  return { running, how };
327
330
  if (output === "json") {
328
- console.log(JSON.stringify({ ok: running, name, ...extra, running, how, state: status?.state ?? "unknown", pid: status?.pid ?? null, lastError: status?.lastError ?? null }, null, 2));
331
+ console.log(JSON.stringify({ ok: running, name, ...extra, running, how, state: status?.state ?? "unknown", pid: status?.pid ?? null, lastError }, null, 2));
329
332
  return { running, how };
330
333
  }
331
334
  if (running)
332
335
  console.log(chalk.green(`${name} is running`) + chalk.dim(` (${how})`));
333
336
  else {
334
337
  console.log(chalk.yellow(`${name} did not report running within 15s`) + chalk.dim(` (${how})`));
335
- if (status?.lastError)
336
- console.log(chalk.dim(` last error: ${status.lastError}`));
338
+ if (lastError)
339
+ console.log(chalk.dim(` last error: ${lastError}`));
337
340
  console.log(chalk.dim(` logs: hq bot logs ${name}`));
338
341
  }
339
342
  return { running, how };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Near-match suggestions for a channel name that resolved to nothing.
3
+ *
4
+ * `hq dm channel <name>`, `hq dm <channel> "msg"`, `hq channels members` and
5
+ * `hq files share --channel` all resolve a channel by exact slug or display
6
+ * name. When that misses, the user usually typed something close to a real
7
+ * channel (for example "desktop-agent-coordination" for the project channel
8
+ * "project-desktop-agent-creation-1331ed7f"). This module ranks the caller's
9
+ * channels against the typed name so the error can list the closest few.
10
+ *
11
+ * Pure: no I/O, so it is unit-testable.
12
+ */
13
+ /** The fields the ranker reads. Structurally compatible with ChannelSummary. */
14
+ export interface SuggestableChannel {
15
+ channelId: string;
16
+ name?: string;
17
+ slug?: string;
18
+ scope?: string;
19
+ }
20
+ export interface ChannelSuggestion {
21
+ id: string;
22
+ name: string;
23
+ }
24
+ /** Most suggestions an error ever lists. */
25
+ export declare const MAX_CHANNEL_SUGGESTIONS = 3;
26
+ /**
27
+ * Tokens used for comparison. Drops a leading "project" token and a trailing
28
+ * 8-hex id, which every project channel carries and which would otherwise
29
+ * dominate the comparison. Falls back to the raw tokens when stripping would
30
+ * leave nothing (a channel literally named "project").
31
+ */
32
+ export declare function comparableTokens(value: string): string[];
33
+ /** Levenshtein distance over code points (so astral characters count once). */
34
+ export declare function editDistance(a: string, b: string): number;
35
+ /**
36
+ * Rank the caller's channels against a name that matched nothing and return
37
+ * up to `max` close ones, best first. Group DMs are unnamed and cannot be
38
+ * addressed by name, so they are never suggested. Each channel is compared by
39
+ * both its slug and its display name; the better score wins.
40
+ */
41
+ export declare function suggestChannels(channels: readonly SuggestableChannel[], query: string, max?: number): ChannelSuggestion[];
42
+ /**
43
+ * Append a "Did you mean" block to a not-found message. Returns the message
44
+ * unchanged when there are no suggestions.
45
+ */
46
+ export declare function withChannelSuggestions(message: string, suggestions: readonly ChannelSuggestion[]): string;
47
+ /**
48
+ * Thrown by channel resolvers when a name or id matches nothing. Carries the
49
+ * near-match suggestions so a --json caller can emit them structurally.
50
+ */
51
+ export declare class ChannelNotFoundError extends Error {
52
+ readonly baseMessage: string;
53
+ readonly suggestions: ChannelSuggestion[];
54
+ constructor(baseMessage: string, suggestions: ChannelSuggestion[]);
55
+ }
56
+ //# sourceMappingURL=channel-suggest.d.ts.map
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Near-match suggestions for a channel name that resolved to nothing.
3
+ *
4
+ * `hq dm channel <name>`, `hq dm <channel> "msg"`, `hq channels members` and
5
+ * `hq files share --channel` all resolve a channel by exact slug or display
6
+ * name. When that misses, the user usually typed something close to a real
7
+ * channel (for example "desktop-agent-coordination" for the project channel
8
+ * "project-desktop-agent-creation-1331ed7f"). This module ranks the caller's
9
+ * channels against the typed name so the error can list the closest few.
10
+ *
11
+ * Pure: no I/O, so it is unit-testable.
12
+ */
13
+ /** Most suggestions an error ever lists. */
14
+ export const MAX_CHANNEL_SUGGESTIONS = 3;
15
+ /**
16
+ * Comparison strings are capped so a very long name cannot make the edit
17
+ * distance (quadratic) expensive across a large channel list.
18
+ */
19
+ const MAX_COMPARE_LENGTH = 64;
20
+ /** Lowercase, Unicode-normalised tokens split on '-', '_' and whitespace. */
21
+ function rawTokens(value) {
22
+ return value
23
+ .normalize("NFKC")
24
+ .toLowerCase()
25
+ .trim()
26
+ .replace(/^#/, "")
27
+ .split(/[\s_-]+/u)
28
+ .filter((token) => token.length > 0);
29
+ }
30
+ /**
31
+ * Tokens used for comparison. Drops a leading "project" token and a trailing
32
+ * 8-hex id, which every project channel carries and which would otherwise
33
+ * dominate the comparison. Falls back to the raw tokens when stripping would
34
+ * leave nothing (a channel literally named "project").
35
+ */
36
+ export function comparableTokens(value) {
37
+ const tokens = rawTokens(value);
38
+ let stripped = tokens;
39
+ if (stripped[0] === "project")
40
+ stripped = stripped.slice(1);
41
+ const last = stripped[stripped.length - 1];
42
+ if (last !== undefined && /^[0-9a-f]{8}$/.test(last)) {
43
+ stripped = stripped.slice(0, -1);
44
+ }
45
+ return stripped.length > 0 ? stripped : tokens;
46
+ }
47
+ function sharedPrefixLength(a, b) {
48
+ const max = Math.min(a.length, b.length);
49
+ let i = 0;
50
+ while (i < max && a[i] === b[i])
51
+ i += 1;
52
+ return i;
53
+ }
54
+ /** Levenshtein distance over code points (so astral characters count once). */
55
+ export function editDistance(a, b) {
56
+ const left = Array.from(a);
57
+ const right = Array.from(b);
58
+ if (left.length === 0)
59
+ return right.length;
60
+ if (right.length === 0)
61
+ return left.length;
62
+ let previous = Array.from({ length: right.length + 1 }, (_, j) => j);
63
+ for (let i = 1; i <= left.length; i += 1) {
64
+ const current = [i];
65
+ for (let j = 1; j <= right.length; j += 1) {
66
+ const cost = left[i - 1] === right[j - 1] ? 0 : 1;
67
+ current[j] = Math.min(previous[j] + 1, current[j - 1] + 1, previous[j - 1] + cost);
68
+ }
69
+ previous = current;
70
+ }
71
+ return previous[right.length];
72
+ }
73
+ function toComparable(value) {
74
+ const tokens = comparableTokens(value);
75
+ return {
76
+ tokens: new Set(tokens),
77
+ joined: Array.from(tokens.join("-")).slice(0, MAX_COMPARE_LENGTH).join(""),
78
+ };
79
+ }
80
+ /**
81
+ * Similarity in [0, 1], or 0 when the two are not close enough to suggest.
82
+ * A candidate qualifies when it shares a token, shares a prefix of at least
83
+ * three characters, or is within 40% edit distance of the query.
84
+ */
85
+ function similarity(query, candidate) {
86
+ if (!query.joined || !candidate.joined)
87
+ return 0;
88
+ let shared = 0;
89
+ for (const token of query.tokens)
90
+ if (candidate.tokens.has(token))
91
+ shared += 1;
92
+ const union = query.tokens.size + candidate.tokens.size - shared;
93
+ const tokenScore = union > 0 ? shared / union : 0;
94
+ const prefix = sharedPrefixLength(query.joined, candidate.joined);
95
+ const longest = Math.max(Array.from(query.joined).length, Array.from(candidate.joined).length);
96
+ const prefixScore = longest > 0 ? prefix / longest : 0;
97
+ const editScore = longest > 0
98
+ ? 1 - editDistance(query.joined, candidate.joined) / longest
99
+ : 0;
100
+ const close = shared > 0 || prefix >= 3 || editScore >= 0.6;
101
+ if (!close)
102
+ return 0;
103
+ return 0.5 * tokenScore + 0.2 * prefixScore + 0.3 * editScore;
104
+ }
105
+ /**
106
+ * Rank the caller's channels against a name that matched nothing and return
107
+ * up to `max` close ones, best first. Group DMs are unnamed and cannot be
108
+ * addressed by name, so they are never suggested. Each channel is compared by
109
+ * both its slug and its display name; the better score wins.
110
+ */
111
+ export function suggestChannels(channels, query, max = MAX_CHANNEL_SUGGESTIONS) {
112
+ if (max <= 0)
113
+ return [];
114
+ const target = toComparable(query);
115
+ if (!target.joined)
116
+ return [];
117
+ const scored = [];
118
+ for (const channel of channels) {
119
+ if (channel.scope === "group")
120
+ continue;
121
+ const slug = channel.slug?.trim() ?? "";
122
+ const name = channel.name?.trim() ?? "";
123
+ if (!slug && !name)
124
+ continue;
125
+ const score = Math.max(slug ? similarity(target, toComparable(slug)) : 0, name ? similarity(target, toComparable(name)) : 0);
126
+ if (score <= 0)
127
+ continue;
128
+ scored.push({
129
+ suggestion: { id: channel.channelId, name: slug || name },
130
+ score,
131
+ });
132
+ }
133
+ scored.sort((a, b) => b.score - a.score ||
134
+ a.suggestion.name.localeCompare(b.suggestion.name) ||
135
+ a.suggestion.id.localeCompare(b.suggestion.id));
136
+ return scored.slice(0, max).map((entry) => entry.suggestion);
137
+ }
138
+ /**
139
+ * Strip control and bidi-override characters so a server-supplied name cannot
140
+ * drive the terminal or visually reorder the line.
141
+ */
142
+ function printable(value) {
143
+ // eslint-disable-next-line no-control-regex
144
+ return value.replace(/[\u0000-\u001f\u007f-\u009f\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, "");
145
+ }
146
+ /**
147
+ * Append a "Did you mean" block to a not-found message. Returns the message
148
+ * unchanged when there are no suggestions.
149
+ */
150
+ export function withChannelSuggestions(message, suggestions) {
151
+ if (suggestions.length === 0)
152
+ return message;
153
+ const lines = suggestions.map((s) => ` ${printable(s.name)} (${printable(s.id)})`);
154
+ return `${message}\nDid you mean:\n${lines.join("\n")}`;
155
+ }
156
+ /**
157
+ * Thrown by channel resolvers when a name or id matches nothing. Carries the
158
+ * near-match suggestions so a --json caller can emit them structurally.
159
+ */
160
+ export class ChannelNotFoundError extends Error {
161
+ baseMessage;
162
+ suggestions;
163
+ constructor(baseMessage, suggestions) {
164
+ super(withChannelSuggestions(baseMessage, suggestions));
165
+ this.name = "ChannelNotFoundError";
166
+ this.baseMessage = baseMessage;
167
+ this.suggestions = suggestions;
168
+ }
169
+ }
170
+ //# sourceMappingURL=channel-suggest.js.map
@@ -1,6 +1,7 @@
1
1
  import chalk from "chalk";
2
2
  import { ensureCognitoToken } from "../utils/cognito-session.js";
3
3
  import { vaultApiFetch } from "../utils/vault-api.js";
4
+ import { suggestChannels, withChannelSuggestions } from "./channel-suggest.js";
4
5
  import { callerFromToken, channelSlug, fetchChannelMembers, fetchChannels, matchChannelsByName, } from "./dm.js";
5
6
  /**
6
7
  * Human label for a channel row. Named channels (personal/company) show their
@@ -189,7 +190,12 @@ async function runChannelsMembers(channelName, opts) {
189
190
  const channels = await fetchChannels(token);
190
191
  const matches = matchChannelsByName(channels, channelName);
191
192
  if (matches.length === 0) {
192
- console.error(chalk.red(`No channel named '${channelName}' — run \`hq channels\` to see your channels.`));
193
+ const message = `No channel named '${channelName}' — run \`hq channels\` to see your channels.`;
194
+ const suggestions = suggestChannels(channels, channelName);
195
+ if (opts.json) {
196
+ console.log(JSON.stringify({ ok: false, error: message, suggestions }, null, 2));
197
+ }
198
+ console.error(chalk.red(withChannelSuggestions(message, suggestions)));
193
199
  process.exit(1);
194
200
  }
195
201
  if (matches.length > 1) {
@@ -484,8 +484,8 @@ async function defaultRunInitialSync(args) {
484
484
  if (event.type === "plan-limit") {
485
485
  failureCount += Math.max(1, event.count ?? 0, event.paths?.length ?? 0);
486
486
  firstFailure ??= {
487
- path: event.paths?.[0] ?? "plan limit",
488
- message: event.message ?? "plan limit refused upload",
487
+ path: event.paths?.[0] ?? "new files",
488
+ message: event.message ?? "new files are paused for this account",
489
489
  };
490
490
  return;
491
491
  }
@@ -69,6 +69,32 @@ export declare function scopeExcludedExitCode(count: number): 0 | 1;
69
69
  export declare function scopeExcludedWarning(count: number): string | null;
70
70
  /** Actionable recovery for files refused because the vault baseline is stale. */
71
71
  export declare function staleUploadRefusalLines(count: number, paths?: readonly string[]): string[];
72
+ /**
73
+ * Upload-failure fields newer hq-cloud ShareResult carries. Read through
74
+ * readUploadFailures so this CLI also builds against a release without them.
75
+ */
76
+ export interface UploadFailureCounts {
77
+ filesUploadFailed?: number;
78
+ uploadFailedPaths?: string[];
79
+ }
80
+ export declare function readUploadFailures(result: object): {
81
+ count: number;
82
+ paths: string[];
83
+ };
84
+ /**
85
+ * Files the vault accepted in the plan but refused or failed at upload
86
+ * (feedback_943456f4). Without these lines a push that uploaded nothing
87
+ * printed "✓ Pushed 0" and exited 0.
88
+ */
89
+ export declare function uploadFailedLines(count: number, paths?: readonly string[]): string[];
90
+ /**
91
+ * share() option that turns a user-named path an ignore rule drops into an
92
+ * "unreachable path" error instead of a silent 0-file success
93
+ * (feedback_63bfc302, feedback_8c294805). Spread as a plain record so this
94
+ * builds against an hq-cloud release whose ShareOptions predates the field;
95
+ * older releases ignore it.
96
+ */
97
+ export declare function namedPathShareOptions(userNamedPaths: boolean): Record<string, boolean>;
72
98
  /**
73
99
  * Optional detail line listing the effective write prefixes when a push
74
100
  * dropped files — helps debug ACL vs scope-excluded mismatches (feedback_7e9c535e).
@@ -241,6 +267,8 @@ export interface ShareCallResult {
241
267
  filesExcludedByScope: number;
242
268
  filesRefusedStale?: number;
243
269
  filesRefusedStalePaths?: string[];
270
+ filesUploadFailed?: number;
271
+ uploadFailedPaths?: string[];
244
272
  conflictPaths: string[];
245
273
  aborted: boolean;
246
274
  /** Files this leg's plan limit refused; absent or null when none were. */
@@ -286,6 +314,8 @@ export interface PushAllResult {
286
314
  filesExcludedByScope: number;
287
315
  filesRefusedStale?: number;
288
316
  filesRefusedStalePaths?: string[];
317
+ filesUploadFailed?: number;
318
+ uploadFailedPaths?: string[];
289
319
  errors: Array<{
290
320
  company: string;
291
321
  message: string;
@@ -418,4 +448,19 @@ export declare function printReadOnlyLocalOnly(hqRoot: string, journals: Readonl
418
448
  files?: Record<string, unknown>;
419
449
  };
420
450
  }>, log?: (line: string) => void): number;
451
+ /**
452
+ * `hq sync status`: list local files under server-owned folders (sources/,
453
+ * signals/, ontology/ outside entities/) that are not in the sync journal.
454
+ * The server pipeline writes these folders; a client push refuses them and
455
+ * the recurring runner skips them quietly, so a file written locally there
456
+ * never reaches the vault and `pendingFiles: 0` still reports a clean tree
457
+ * (feedback_0414d71c). Uses only the journals already loaded, with no
458
+ * network calls. Prints nothing when nothing applies.
459
+ */
460
+ export declare function printServerOwnedLocalOnly(hqRoot: string, journals: ReadonlyArray<{
461
+ slug: string;
462
+ journal: {
463
+ files?: Record<string, unknown>;
464
+ };
465
+ }>, log?: (line: string) => void): number;
421
466
  //# sourceMappingURL=cloud.d.ts.map