@indigoai-us/hq-cli 5.345.147 → 5.345.149

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 (168) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +2 -0
  3. package/assets/bot-workers/setup/worker.yaml +11 -0
  4. package/dist/command-catalog.generated.d.ts +341 -11
  5. package/dist/command-catalog.generated.js +421 -11
  6. package/dist/command-help.js +2 -1
  7. package/dist/commands/access.d.ts +45 -2
  8. package/dist/commands/access.js +537 -223
  9. package/dist/commands/agent-inbox.js +2 -1
  10. package/dist/commands/billing-usage.d.ts +33 -2
  11. package/dist/commands/billing-usage.js +75 -4
  12. package/dist/commands/bot.js +2 -2
  13. package/dist/commands/channels.d.ts +27 -0
  14. package/dist/commands/channels.js +108 -4
  15. package/dist/commands/cloud.js +31 -1
  16. package/dist/commands/core-refresh-vault-access-native.d.ts +33 -0
  17. package/dist/commands/core-refresh-vault-access-native.js +356 -28
  18. package/dist/commands/core-work-mesh-bind-trusted-native.d.ts +9 -0
  19. package/dist/commands/core-work-mesh-bind-trusted-native.js +26 -19
  20. package/dist/commands/core.js +9 -0
  21. package/dist/commands/daemon.d.ts +7 -0
  22. package/dist/commands/daemon.js +37 -3
  23. package/dist/commands/dm.js +18 -3
  24. package/dist/commands/doctor.d.ts +11 -1
  25. package/dist/commands/doctor.js +56 -1
  26. package/dist/commands/install-global.d.ts +8 -8
  27. package/dist/commands/install-global.js +139 -61
  28. package/dist/commands/lanes-shared.d.ts +3 -1
  29. package/dist/commands/lanes.d.ts +4 -4
  30. package/dist/commands/lanes.js +380 -25
  31. package/dist/commands/library-add.d.ts +31 -1
  32. package/dist/commands/library-add.js +69 -2
  33. package/dist/commands/library.js +37 -3
  34. package/dist/commands/pack-install.d.ts +2 -2
  35. package/dist/commands/pack-install.js +133 -35
  36. package/dist/commands/reindex.js +19 -0
  37. package/dist/commands/sync-manifest.d.ts +2 -9
  38. package/dist/commands/sync-manifest.js +4 -15
  39. package/dist/hook-engine-path.d.ts +4 -0
  40. package/dist/hook-engine-path.js +43 -0
  41. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2783 -2354
  42. package/dist/hq-cloud-sync-runner/package.json +1 -1
  43. package/dist/index.d.ts +10 -0
  44. package/dist/index.js +76 -5
  45. package/dist/lazy-commands.js +2 -0
  46. package/dist/lib/agent-kit/inbox-redact.d.ts +44 -0
  47. package/dist/lib/agent-kit/inbox-redact.js +361 -0
  48. package/dist/lib/agent-kit/inbox-state.js +11 -2
  49. package/dist/lib/agent-kit/mcp/tools.js +2 -1
  50. package/dist/lib/agent-kit/run/inbox.js +36 -7
  51. package/dist/lib/anywhere-runtime-flag-key.d.ts +3 -0
  52. package/dist/lib/anywhere-runtime-flag-key.js +9 -0
  53. package/dist/lib/billing/usage-cost.d.ts +31 -1
  54. package/dist/lib/billing/usage-cost.js +46 -0
  55. package/dist/lib/company-skill-wrapper.js +6 -4
  56. package/dist/lib/daemon/install.d.ts +47 -1
  57. package/dist/lib/daemon/install.js +181 -15
  58. package/dist/lib/daemon/lock.d.ts +32 -0
  59. package/dist/lib/daemon/lock.js +55 -2
  60. package/dist/lib/db/local.d.ts +2 -2
  61. package/dist/lib/db/local.js +2 -2
  62. package/dist/lib/db/sql.js +2 -2
  63. package/dist/lib/db/sqlite-binding.d.ts +22 -0
  64. package/dist/lib/db/sqlite-binding.js +168 -0
  65. package/dist/lib/doctor/__testing__/run-doctor.js +4 -0
  66. package/dist/lib/doctor/checks/anywhere.d.ts +15 -0
  67. package/dist/lib/doctor/checks/anywhere.js +77 -11
  68. package/dist/lib/doctor/checks/cli-path.d.ts +12 -0
  69. package/dist/lib/doctor/checks/cli-path.js +46 -0
  70. package/dist/lib/doctor/checks/hook-engine.d.ts +4 -0
  71. package/dist/lib/doctor/checks/hook-engine.js +17 -0
  72. package/dist/lib/doctor/checks/sync-health.d.ts +7 -0
  73. package/dist/lib/doctor/checks/sync-health.js +38 -2
  74. package/dist/lib/doctor/checks/temp-homes.d.ts +19 -0
  75. package/dist/lib/doctor/checks/temp-homes.js +54 -0
  76. package/dist/lib/doctor/fix/apply.d.ts +90 -1
  77. package/dist/lib/doctor/fix/apply.js +176 -10
  78. package/dist/lib/doctor/fix/json-output.d.ts +4 -1
  79. package/dist/lib/doctor/fix/json-output.js +6 -0
  80. package/dist/lib/doctor/fix/remediation.d.ts +7 -1
  81. package/dist/lib/doctor/fix/remediation.js +23 -1
  82. package/dist/lib/doctor/registry.js +10 -0
  83. package/dist/lib/doctor/temp-home-sweeper.d.ts +149 -0
  84. package/dist/lib/doctor/temp-home-sweeper.js +418 -0
  85. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  86. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  87. package/dist/lib/flag-registry.js +9 -1
  88. package/dist/lib/flag-snapshot-file.js +8 -3
  89. package/dist/lib/hook-engine/reconcile.d.ts +9 -0
  90. package/dist/lib/hook-engine/reconcile.js +134 -0
  91. package/dist/lib/hook-engine/registration.d.ts +17 -0
  92. package/dist/lib/hook-engine/registration.js +50 -0
  93. package/dist/lib/hook-engine/resolve.d.ts +31 -0
  94. package/dist/lib/hook-engine/resolve.js +134 -0
  95. package/dist/lib/install/claude-targets.d.ts +2 -1
  96. package/dist/lib/install/claude-targets.js +8 -3
  97. package/dist/lib/install/codex-targets.d.ts +3 -0
  98. package/dist/lib/install/codex-targets.js +15 -4
  99. package/dist/lib/install/hook-target.d.ts +9 -0
  100. package/dist/lib/install/hook-target.js +39 -0
  101. package/dist/lib/lanes/admission-host.d.ts +1 -0
  102. package/dist/lib/lanes/admission-host.js +8 -1
  103. package/dist/lib/lanes/admission.d.ts +4 -0
  104. package/dist/lib/lanes/admission.js +6 -2
  105. package/dist/lib/lanes/envelope-repair-dispatch.d.ts +5 -0
  106. package/dist/lib/lanes/envelope-repair-dispatch.js +12 -1
  107. package/dist/lib/lanes/index.d.ts +2 -2
  108. package/dist/lib/lanes/index.js +1 -1
  109. package/dist/lib/lanes/keep-alive.js +4 -1
  110. package/dist/lib/lanes/local-host-identity.d.ts +40 -0
  111. package/dist/lib/lanes/local-host-identity.js +314 -0
  112. package/dist/lib/lanes/reaper.d.ts +1 -1
  113. package/dist/lib/lanes/reaper.js +7 -0
  114. package/dist/lib/lanes/reconcile.d.ts +4 -0
  115. package/dist/lib/lanes/reconcile.js +16 -8
  116. package/dist/lib/lanes/spawn.d.ts +20 -0
  117. package/dist/lib/lanes/spawn.js +48 -2
  118. package/dist/lib/lanes/status-fields.js +6 -1
  119. package/dist/lib/lanes/store.js +8 -1
  120. package/dist/lib/lanes/throttle.d.ts +49 -6
  121. package/dist/lib/lanes/throttle.js +154 -17
  122. package/dist/lib/lanes/types.d.ts +22 -1
  123. package/dist/lib/lanes/types.js +27 -3
  124. package/dist/lib/lanes/watcher.js +27 -2
  125. package/dist/lib/lanes/worker-continuity.d.ts +2 -0
  126. package/dist/lib/lanes/worker-continuity.js +18 -11
  127. package/dist/lib/manifest-disabled.d.ts +13 -0
  128. package/dist/lib/manifest-disabled.js +19 -0
  129. package/dist/lib/service-manager/launchd.d.ts +1 -0
  130. package/dist/lib/service-manager/launchd.js +1 -1
  131. package/dist/lib/windows/console-encoding.d.ts +24 -0
  132. package/dist/lib/windows/console-encoding.js +83 -0
  133. package/dist/lib/windows/install-followup.d.ts +35 -0
  134. package/dist/lib/windows/install-followup.js +142 -0
  135. package/dist/lib/windows/user-path.d.ts +125 -0
  136. package/dist/lib/windows/user-path.js +275 -0
  137. package/dist/lib/work-context/auto-bind.d.ts +1 -1
  138. package/dist/lib/work-context/auto-bind.js +22 -3
  139. package/dist/lib/work-context/lane-bind.d.ts +31 -0
  140. package/dist/lib/work-context/lane-bind.js +101 -0
  141. package/dist/lib/work-context/organize.js +1 -0
  142. package/dist/lib/work-context/prd-sync.js +7 -0
  143. package/dist/lib/work-context/project.d.ts +36 -10
  144. package/dist/lib/work-context/project.js +130 -41
  145. package/dist/lib/work-context/reconcile.d.ts +6 -0
  146. package/dist/lib/work-context/reconcile.js +25 -5
  147. package/dist/lib/work-context/state.d.ts +6 -0
  148. package/dist/lib/work-context/state.js +29 -1
  149. package/dist/main.js +2 -1
  150. package/dist/unknown-command.d.ts +18 -0
  151. package/dist/unknown-command.js +88 -0
  152. package/dist/utils/cognito-session.d.ts +8 -0
  153. package/dist/utils/cognito-session.js +14 -2
  154. package/dist/utils/copy-tree.d.ts +61 -0
  155. package/dist/utils/copy-tree.js +249 -0
  156. package/dist/utils/environmental-error.js +7 -3
  157. package/dist/utils/hook-trust.d.ts +9 -0
  158. package/dist/utils/hook-trust.js +30 -2
  159. package/dist/utils/npm-invocation.d.ts +27 -0
  160. package/dist/utils/npm-invocation.js +54 -0
  161. package/dist/utils/qmd-native-binding-error.js +3 -1
  162. package/dist/utils/resolve-executable.d.ts +2 -0
  163. package/dist/utils/resolve-executable.js +6 -2
  164. package/dist/utils/safe-link.d.ts +50 -0
  165. package/dist/utils/safe-link.js +300 -0
  166. package/package.json +17 -5
  167. package/scripts/hook-engine-manifest.mjs +22 -0
  168. package/scripts/install-hook-engine.mjs +10 -0
@@ -8,6 +8,7 @@ import chalk from "chalk";
8
8
  import * as os from "node:os";
9
9
  import { ensureCognitoToken } from "../utils/cognito-session.js";
10
10
  import { vaultApiFetch } from "../utils/vault-api.js";
11
+ import { redactAgentInboxItem } from "../lib/agent-kit/inbox-redact.js";
11
12
  import { markInboxDone, mergeAgentInbox, readDoneIds, summarizeInboxEntry } from "../lib/agent-kit/inbox-state.js";
12
13
  import { agentKitPaths } from "../lib/agent-kit/paths.js";
13
14
  import { requireExternalCreds } from "./agent-kit.js";
@@ -36,7 +37,7 @@ export function registerAgentInboxCommand(agent) {
36
37
  });
37
38
  const rows = entries
38
39
  .filter((e) => opts.all || !done.has(e.id))
39
- .map((e) => summarizeInboxEntry(e, done.has(e.id)));
40
+ .map((e) => summarizeInboxEntry(redactAgentInboxItem(e), done.has(e.id)));
40
41
  if (opts.json) {
41
42
  console.log(JSON.stringify({ count: rows.length, messages: rows, ...(note ? { note } : {}) }, null, 2));
42
43
  return;
@@ -17,9 +17,18 @@
17
17
  *
18
18
  * Usage numbers come through `readMonthUsage` (src/lib/billing/usage-cost.ts),
19
19
  * the single seam to switch to the US-005 daily rollup once it has a read route.
20
+ *
21
+ * 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
27
+ * projected bill.
20
28
  */
21
29
  import type { Command } from "commander";
22
- import { type ReadMonthUsageDeps, type StorageSplit, type UsageReport, type UsageScope } from "../lib/billing/usage-cost.js";
30
+ import { type BillingUsageReport, type ReadMonthUsageDeps, type StorageSplit, type UsageScope } from "../lib/billing/usage-cost.js";
31
+ import { type PricingPlan } from "../lib/billing/usage-only.js";
23
32
  export interface MembershipRef {
24
33
  uid: string;
25
34
  slug: string;
@@ -34,6 +43,12 @@ export interface BillingUsageDeps {
34
43
  fetchPricing(): Promise<unknown | null>;
35
44
  /** Whether the usage trial is running for the scope. Null = unknown. */
36
45
  readTrialActive?: (scope: UsageScope) => Promise<boolean | null>;
46
+ /**
47
+ * The billing summary body for the scope (company: `/v1/billing/summary`,
48
+ * person: `/v1/billing/person/summary`). Null when it did not answer. Used
49
+ * for the plan when usage-limits does not carry it (older server).
50
+ */
51
+ fetchBillingSummary?: (scope: UsageScope) => Promise<unknown | null>;
37
52
  now(): Date;
38
53
  }
39
54
  export interface BillingUsageOptions {
@@ -46,7 +61,23 @@ export declare class UsageScopeError extends Error {
46
61
  }
47
62
  /** Pick the scope. Never reaches outside the caller's own memberships. */
48
63
  export declare function resolveUsageScope(memberships: MembershipRef[], opts: BillingUsageOptions): UsageScope;
49
- export declare function runBillingUsage(opts: BillingUsageOptions, deps: BillingUsageDeps): Promise<UsageReport>;
64
+ /** The plan on a usage-limits body: `pricingPlan`, else the `usageOnly` / `onUsagePricing` boolean. */
65
+ export declare function pricingPlanFromUsageLimits(body: unknown): PricingPlan | null;
66
+ /**
67
+ * The plan on a billing summary body. A summary that answered without saying
68
+ * usage-only is a flat plan: the person summary carries `personalUsage` only
69
+ * for usage pricing, and a company summary from a server older than usage
70
+ * pricing has no `pricingPlan` at all.
71
+ */
72
+ export declare function pricingPlanFromSummary(body: unknown): PricingPlan | null;
73
+ /**
74
+ * True: price the meters. False: flat plan. Null: unknown, show no cost.
75
+ * When usage-limits answers, only its answer counts: the server computes it
76
+ * with `isOnUsagePricing`, the source of truth. The summary's plan is used
77
+ * only when usage-limits does not say.
78
+ */
79
+ export declare function resolveOnUsagePricing(limitsPlan: PricingPlan | null, summaryPlan: PricingPlan | null): boolean | null;
80
+ export declare function runBillingUsage(opts: BillingUsageOptions, deps: BillingUsageDeps): Promise<BillingUsageReport>;
50
81
  /**
51
82
  * `hq billing usage --card`: the card from GET /v1/pricing, or the bundled
52
83
  * card when the feed has none. Needs no sign-in.
@@ -17,14 +17,22 @@
17
17
  *
18
18
  * Usage numbers come through `readMonthUsage` (src/lib/billing/usage-cost.ts),
19
19
  * the single seam to switch to the US-005 daily rollup once it has a read route.
20
+ *
21
+ * 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
27
+ * projected bill.
20
28
  */
21
29
  import chalk from "chalk";
22
30
  import { ensureCognitoToken } from "../utils/cognito-session.js";
23
31
  import { vaultApiFetch, vaultApiFetchPublic } from "../utils/vault-api.js";
24
- import { buildReport, formatReport, readMonthUsage, } from "../lib/billing/usage-cost.js";
32
+ import { buildReadingsReport, buildReport, formatReport, readMonthUsage, } from "../lib/billing/usage-cost.js";
25
33
  import { ratesFromPricingStatement } from "../lib/billing/usage-rate-card.js";
26
34
  import { fetchUsageCard, formatUsageCardLines } from "../lib/billing/usage-card.js";
27
- import { fetchCompanyBillingSummary, usageTrialActiveFromBody } from "../lib/billing/usage-only.js";
35
+ import { fetchCompanyBillingSummary, pricingPlanFromBody, usageTrialActiveFromBody, } from "../lib/billing/usage-only.js";
28
36
  export class UsageScopeError extends Error {
29
37
  }
30
38
  /** Pick the scope. Never reaches outside the caller's own memberships. */
@@ -51,13 +59,56 @@ export function resolveUsageScope(memberships, opts) {
51
59
  memberships.map((m) => ` --company ${m.slug}`).join("\n") +
52
60
  "\n --personal");
53
61
  }
62
+ function asRecord(value) {
63
+ return value !== null && typeof value === "object" && !Array.isArray(value)
64
+ ? value
65
+ : null;
66
+ }
67
+ /** The plan on a usage-limits body: `pricingPlan`, else the `usageOnly` / `onUsagePricing` boolean. */
68
+ export function pricingPlanFromUsageLimits(body) {
69
+ const plan = pricingPlanFromBody(body);
70
+ if (plan)
71
+ return plan;
72
+ const rec = asRecord(body);
73
+ const flag = typeof rec?.onUsagePricing === "boolean" ? rec.onUsagePricing : rec?.usageOnly;
74
+ if (typeof flag === "boolean")
75
+ return flag ? "usage-only" : "current";
76
+ return null;
77
+ }
78
+ /**
79
+ * The plan on a billing summary body. A summary that answered without saying
80
+ * usage-only is a flat plan: the person summary carries `personalUsage` only
81
+ * for usage pricing, and a company summary from a server older than usage
82
+ * pricing has no `pricingPlan` at all.
83
+ */
84
+ export function pricingPlanFromSummary(body) {
85
+ const rec = asRecord(body);
86
+ if (!rec)
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";
91
+ }
92
+ /**
93
+ * True: price the meters. False: flat plan. Null: unknown, show no cost.
94
+ * When usage-limits answers, only its answer counts: the server computes it
95
+ * with `isOnUsagePricing`, the source of truth. The summary's plan is used
96
+ * only when usage-limits does not say.
97
+ */
98
+ export function resolveOnUsagePricing(limitsPlan, summaryPlan) {
99
+ const plan = limitsPlan ?? summaryPlan;
100
+ if (plan === null)
101
+ return null;
102
+ return plan === "usage-only";
103
+ }
54
104
  export async function runBillingUsage(opts, deps) {
55
105
  const memberships = await deps.listCompanies();
56
106
  const scope = resolveUsageScope(memberships, opts);
57
107
  const membership = scope.kind === "company" ? memberships.find((m) => m.uid === scope.companyUid) : undefined;
108
+ let limitsBody = null;
58
109
  const [usage, pricing] = await Promise.all([
59
110
  readMonthUsage(scope, {
60
- fetchUsageLimits: deps.fetchUsageLimits,
111
+ fetchUsageLimits: async (s) => (limitsBody = await deps.fetchUsageLimits(s)),
61
112
  now: deps.now,
62
113
  ...(deps.readTrialActive ? { readTrialActive: deps.readTrialActive } : {}),
63
114
  ...(opts.storageScan !== false && deps.readStorageSplit && membership
@@ -66,7 +117,18 @@ export async function runBillingUsage(opts, deps) {
66
117
  }),
67
118
  deps.fetchPricing().catch(() => null),
68
119
  ]);
69
- return buildReport(usage, ratesFromPricingStatement(pricing));
120
+ const limitsPlan = pricingPlanFromUsageLimits(limitsBody);
121
+ const summary = limitsPlan === null && deps.fetchBillingSummary
122
+ ? await deps.fetchBillingSummary(scope).catch((err) => {
123
+ // stderr keeps --json output on stdout parseable.
124
+ console.error(`Could not read the billing summary: ${err instanceof Error ? err.message : String(err)}`);
125
+ return null;
126
+ })
127
+ : null;
128
+ const onUsagePricing = resolveOnUsagePricing(limitsPlan, pricingPlanFromSummary(summary));
129
+ if (onUsagePricing !== true)
130
+ return buildReadingsReport(usage, onUsagePricing);
131
+ return { ...buildReport(usage, ratesFromPricingStatement(pricing)), onUsagePricing: true };
70
132
  }
71
133
  // ── Production deps ─────────────────────────────────────────────────────────
72
134
  function productionDeps() {
@@ -117,6 +179,15 @@ function productionDeps() {
117
179
  const body = await fetchCompanyBillingSummary(await token(), scope.companyUid);
118
180
  return body === null ? null : usageTrialActiveFromBody(body);
119
181
  },
182
+ async fetchBillingSummary(scope) {
183
+ // Company: the same cached read as readTrialActive, so no second call.
184
+ if (scope.kind === "company")
185
+ return fetchCompanyBillingSummary(await token(), scope.companyUid);
186
+ const res = await vaultApiFetch({ token: await token(), path: "/v1/billing/person/summary" });
187
+ if (!res.ok)
188
+ return null;
189
+ return res.json().catch(() => null);
190
+ },
120
191
  now: () => new Date(),
121
192
  };
122
193
  }
@@ -475,7 +475,7 @@ export function resolveCreateKind(opts, worker) {
475
475
  return { kind, companies: [] };
476
476
  }
477
477
  if (isSetup) {
478
- throw Object.assign(new Error("HQ's setup bot is always a personal bot; drop --kind company."), { expected: true });
478
+ throw Object.assign(new Error("HQ's assistant is always a personal bot; drop --kind company."), { expected: true });
479
479
  }
480
480
  const companies = requested.length > 0 ? requested : normalizeBotCompanies(worker?.company ? [worker.company] : []);
481
481
  if (companies.length === 0) {
@@ -638,7 +638,7 @@ async function runBotCreateLocked(name, opts) {
638
638
  const { kind, companies } = resolveCreateKind(opts, worker);
639
639
  const isSetupBot = name === RESERVED_SETUP_BOT_NAME && kind === "personal" && worker?.id === SETUP_BOT_WORKER_ID;
640
640
  if (name === RESERVED_SETUP_BOT_NAME && kind === "personal" && !isSetupBot) {
641
- throw Object.assign(new Error(`The name ${RESERVED_SETUP_BOT_NAME} is reserved for HQ's setup bot; create it with --worker ${RESERVED_SETUP_BOT_NAME}.`), { expected: true });
641
+ throw Object.assign(new Error(`The name ${RESERVED_SETUP_BOT_NAME} is reserved for HQ's assistant; create it with --worker ${RESERVED_SETUP_BOT_NAME}.`), { expected: true });
642
642
  }
643
643
  const existing = readBotConfig(dir);
644
644
  if (existing) {
@@ -12,5 +12,32 @@ export declare function describeChannel(c: ChannelSummary): string;
12
12
  * fetched list) so the formatting is unit-testable without network/auth.
13
13
  */
14
14
  export declare function formatChannelsList(channels: ChannelSummary[]): string;
15
+ /** Activity fields the server returns on each channel row. */
16
+ type ChannelWithActivity = ChannelSummary & {
17
+ lastActivityAt?: string;
18
+ lastMessageAt?: string;
19
+ lastHumanMessageAt?: string;
20
+ unreadCount?: number;
21
+ };
22
+ /**
23
+ * Latest activity time on a channel row, as epoch ms, or 0 when the channel
24
+ * has never had activity. lastActivityAt moves on new messages (work-session
25
+ * status cards do not move it); lastMessageAt and lastHumanMessageAt are read
26
+ * as fallbacks for rows that carry only those.
27
+ * Pure → unit-testable.
28
+ */
29
+ export declare function channelActivityMs(c: ChannelWithActivity): number;
30
+ /**
31
+ * Parse `--active-since`: an ISO timestamp, or a duration back from now such
32
+ * as `30m`, `2h`, `1d`, `90s`. Returns epoch ms, or null when unreadable.
33
+ */
34
+ export declare function parseActiveSince(value: string, now?: number): number | null;
35
+ /**
36
+ * Channels with activity at or after `sinceMs`, newest first. With no bound,
37
+ * every channel sorted newest first (never-active channels last, in their
38
+ * original order). Pure → unit-testable.
39
+ */
40
+ export declare function channelsByActivity<T extends ChannelWithActivity>(channels: T[], sinceMs?: number): T[];
15
41
  export declare function registerChannelsCommand(program: Command): void;
42
+ export {};
16
43
  //# sourceMappingURL=channels.d.ts.map
@@ -67,7 +67,64 @@ function formatChannelMembers(members, json) {
67
67
  .map((member) => `${member.displayName ?? ""}\t${member.personUid}`)
68
68
  .join("\n");
69
69
  }
70
- async function runChannelsList() {
70
+ /**
71
+ * Latest activity time on a channel row, as epoch ms, or 0 when the channel
72
+ * has never had activity. lastActivityAt moves on new messages (work-session
73
+ * status cards do not move it); lastMessageAt and lastHumanMessageAt are read
74
+ * as fallbacks for rows that carry only those.
75
+ * Pure → unit-testable.
76
+ */
77
+ export function channelActivityMs(c) {
78
+ let best = 0;
79
+ for (const v of [c.lastActivityAt, c.lastMessageAt, c.lastHumanMessageAt]) {
80
+ const ms = typeof v === "string" ? Date.parse(v) : NaN;
81
+ if (Number.isFinite(ms) && ms > best)
82
+ best = ms;
83
+ }
84
+ return best;
85
+ }
86
+ /**
87
+ * Parse `--active-since`: an ISO timestamp, or a duration back from now such
88
+ * as `30m`, `2h`, `1d`, `90s`. Returns epoch ms, or null when unreadable.
89
+ */
90
+ export function parseActiveSince(value, now = Date.now()) {
91
+ const v = value.trim();
92
+ const rel = /^(\d+)\s*([smhd])$/i.exec(v);
93
+ if (rel) {
94
+ const unit = { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000 }[rel[2].toLowerCase()];
95
+ return now - Number(rel[1]) * unit;
96
+ }
97
+ if (!/^\d{4}-\d{2}-\d{2}/.test(v))
98
+ return null;
99
+ const ms = Date.parse(v);
100
+ return Number.isFinite(ms) ? ms : null;
101
+ }
102
+ /**
103
+ * Channels with activity at or after `sinceMs`, newest first. With no bound,
104
+ * every channel sorted newest first (never-active channels last, in their
105
+ * original order). Pure → unit-testable.
106
+ */
107
+ export function channelsByActivity(channels, sinceMs) {
108
+ return channels
109
+ .map((c, i) => ({ c, i, at: channelActivityMs(c) }))
110
+ .filter(({ at }) => sinceMs === undefined || (at > 0 && at >= sinceMs))
111
+ .sort((a, b) => b.at - a.at || a.i - b.i)
112
+ .map(({ c }) => c);
113
+ }
114
+ async function runChannelsList(opts = {}) {
115
+ let sinceMs;
116
+ if (opts.activeSince !== undefined) {
117
+ const parsed = parseActiveSince(opts.activeSince);
118
+ if (parsed === null) {
119
+ console.error(chalk.red(`--active-since must be an ISO time (2026-10-19T17:00:00Z) or a duration (30m, 2h, 1d); got '${opts.activeSince}'.`));
120
+ process.exit(2);
121
+ }
122
+ sinceMs = parsed;
123
+ }
124
+ if (opts.sort !== undefined && opts.sort !== "activity" && opts.sort !== "default") {
125
+ console.error(chalk.red(`--sort must be 'activity' or 'default'; got '${opts.sort}'.`));
126
+ process.exit(2);
127
+ }
71
128
  try {
72
129
  const token = await ensureCognitoToken();
73
130
  const res = await vaultApiFetch({ token, path: "/v1/notify/channels" });
@@ -82,7 +139,44 @@ async function runChannelsList() {
82
139
  process.exit(1);
83
140
  }
84
141
  const data = (await res.json());
85
- console.log(formatChannelsList(data.channels ?? []));
142
+ let list = data.channels ?? [];
143
+ if (sinceMs !== undefined || opts.sort === "activity") {
144
+ list = channelsByActivity(list, sinceMs);
145
+ }
146
+ console.log(opts.json ? JSON.stringify(list, null, 2) : formatChannelsList(list));
147
+ }
148
+ catch (err) {
149
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
150
+ process.exit(1);
151
+ }
152
+ }
153
+ /**
154
+ * `hq channels changes` — one call to the channel directory feed
155
+ * (GET /v1/notify/channels?cursor). No cursor, or an expired one, returns a
156
+ * full snapshot in `rows` with `snapshot: true` (or `reset: true`); a valid
157
+ * cursor returns only the channels that changed since it in `changed`, plus
158
+ * `removedChannelIds`. Every response carries the successor `cursor`
159
+ * (single-use, 15-minute TTL). Prints the response as JSON.
160
+ */
161
+ async function runChannelsChanges(opts) {
162
+ try {
163
+ const token = await ensureCognitoToken();
164
+ const res = await vaultApiFetch({
165
+ token,
166
+ path: "/v1/notify/channels",
167
+ query: { cursor: opts.cursor ?? "" },
168
+ });
169
+ if (!res.ok) {
170
+ if (res.status === 401) {
171
+ console.error(chalk.red("Not authenticated — run `hq login` and try again."));
172
+ }
173
+ else {
174
+ const err = (await res.json().catch(() => ({})));
175
+ console.error(chalk.red(`Could not read channel changes: ${err.error ?? err.message ?? res.statusText}`));
176
+ }
177
+ process.exit(1);
178
+ }
179
+ console.log(JSON.stringify(await res.json(), null, 2));
86
180
  }
87
181
  catch (err) {
88
182
  console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
@@ -119,8 +213,18 @@ export function registerChannelsCommand(program) {
119
213
  channels
120
214
  .command("list", { isDefault: true })
121
215
  .description("List your DM channels — named channels and group DMs.")
122
- .action(async () => {
123
- await runChannelsList();
216
+ .option("--json", "Output the channel array as JSON, including activity fields.")
217
+ .option("--active-since <when>", "Only channels with activity at or after an ISO time or a duration back from now (30m, 2h, 1d); newest first.")
218
+ .option("--sort <order>", "'activity' lists the most recently active channels first.")
219
+ .action(async (opts) => {
220
+ await runChannelsList(opts);
221
+ });
222
+ channels
223
+ .command("changes")
224
+ .description("Print the channels that changed since a feed cursor, as JSON (no cursor → full snapshot).")
225
+ .option("--cursor <token>", "Cursor from the previous `hq channels changes` response.")
226
+ .action(async (opts) => {
227
+ await runChannelsChanges(opts);
124
228
  });
125
229
  channels
126
230
  .command("members <channel>")
@@ -1074,7 +1074,8 @@ export function registerCloudCommands(program) {
1074
1074
  }
1075
1075
  }
1076
1076
  else {
1077
- log(chalk.green(`\n✓ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ${result.filesSkipped} skipped)`));
1077
+ const skipSummary = formatPushSkipSummary(result);
1078
+ log(chalk.green(`\n✓ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ${skipSummary})`));
1078
1079
  }
1079
1080
  await syncHealth.succeeded();
1080
1081
  const exitCode = scopeExcludedExitCode(result.filesExcludedByScope + staleRefusalCount);
@@ -2058,6 +2059,35 @@ function scopeLabel(slug) {
2058
2059
  return "personal (companies/personal)";
2059
2060
  return slug;
2060
2061
  }
2062
+ function formatPushSkipSummary(result) {
2063
+ const detail = result.filesSkippedDetail;
2064
+ if (!detail)
2065
+ return `${result.filesSkipped} skipped`;
2066
+ const parts = [];
2067
+ if (detail.upToDate > 0)
2068
+ parts.push(`${detail.upToDate} already up to date`);
2069
+ const otherTotal = detail.conflict + detail.brokenLink + detail.other;
2070
+ if (otherTotal > 0) {
2071
+ const named = [];
2072
+ if (detail.conflict > 0) {
2073
+ named.push(`${detail.conflict} conflict${detail.conflict === 1 ? "" : "s"}`);
2074
+ }
2075
+ if (detail.brokenLink > 0) {
2076
+ named.push(`${detail.brokenLink} broken link${detail.brokenLink === 1 ? "" : "s"}`);
2077
+ }
2078
+ if (detail.other > 0)
2079
+ named.push(`${detail.other} other`);
2080
+ if (detail.conflict > 0 || detail.brokenLink > 0) {
2081
+ parts.push(`${otherTotal} skipped (${named.join(", ")})`);
2082
+ }
2083
+ else {
2084
+ parts.push(`${otherTotal} skipped`);
2085
+ }
2086
+ }
2087
+ if (parts.length === 0)
2088
+ return `${result.filesSkipped} skipped`;
2089
+ return parts.join(", ");
2090
+ }
2061
2091
  function formatBytes(bytes) {
2062
2092
  if (bytes === 0)
2063
2093
  return "0 B";
@@ -7,6 +7,7 @@ export type HqResult = {
7
7
  stdout: string;
8
8
  stderr: string;
9
9
  missing?: boolean;
10
+ timedOut?: boolean;
10
11
  };
11
12
  export type RefreshVaultAccessDependencies = {
12
13
  cwd: string;
@@ -14,8 +15,40 @@ export type RefreshVaultAccessDependencies = {
14
15
  now: () => Date;
15
16
  hqAvailable: boolean;
16
17
  runHq?: (args: string[], cwd: string) => HqResult;
18
+ /** How long to wait for another refresh to release the manifest lock. */
19
+ lockWaitMs?: number;
17
20
  };
21
+ /**
22
+ * Per-call ceiling for each child `hq` call. SessionStart runs this helper, so
23
+ * a stalled vault request must not outlive the session that started it.
24
+ */
25
+ export declare const DEFAULT_HQ_CALL_TIMEOUT_MS = 30000;
26
+ /**
27
+ * Run one child `hq` call with a timeout and with browser sign-in disabled.
28
+ * Without `HQ_NO_BROWSER_LOGIN` (read by cognito-session's
29
+ * resolveInteractiveLogin) a signed-out child opens a browser login and
30
+ * waits on it indefinitely. On Windows the call runs under a supervisor so a
31
+ * timeout ends the cmd.exe shim and every process under it.
32
+ */
33
+ /** Windows npm shims run hq under cmd.exe, so only there does a plain kill leave the real hq running. */
34
+ export declare function usesTreeKillByDefault(platform?: NodeJS.Platform): boolean;
35
+ export declare function runHqChild(args: string[], cwd: string, env: NodeJS.ProcessEnv, killTree?: boolean): HqResult;
18
36
  export declare function createRefreshVaultAccessDependencies(cwd: string, env: NodeJS.ProcessEnv, now: () => Date): RefreshVaultAccessDependencies;
19
37
  export declare function runRefreshVaultAccess(args: string[], deps: RefreshVaultAccessDependencies): UtilityOutput;
38
+ /** Test seam: runs after a waiter has judged the lock stale and before it takes the lock over. */
39
+ export interface ManifestLockHooks {
40
+ afterStaleCheck?: () => void;
41
+ }
42
+ /**
43
+ * Take the manifest lock and return its owner token, or undefined when the
44
+ * lock stays busy past `waitMs`. The lock file is created by hard-linking a
45
+ * file that already holds the token, so it never exists without an owner. A
46
+ * stale lock (older than LOCK_STALE_MS, or whose holder on this host has
47
+ * exited) is taken over through removeLockIf, so two waiters that both judge
48
+ * it stale cannot remove each other's new lock.
49
+ */
50
+ export declare function acquireManifestLock(lock: string, waitMs: number, hooks?: ManifestLockHooks): string | undefined;
51
+ /** Release the lock if `token` still owns it. Returns false when another process has taken it over. */
52
+ export declare function releaseManifestLock(lock: string, token: string): boolean;
20
53
  export declare function registerCoreRefreshVaultAccessNative(core: Command): void;
21
54
  //# sourceMappingURL=core-refresh-vault-access-native.d.ts.map