@indigoai-us/hq-cli 5.345.177 → 5.345.179

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 (100) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/dist/command-catalog.generated.d.ts +79 -1
  3. package/dist/command-catalog.generated.js +96 -1
  4. package/dist/commands/bot-jobs.d.ts +11 -9
  5. package/dist/commands/bot-jobs.js +56 -21
  6. package/dist/commands/bot.d.ts +13 -0
  7. package/dist/commands/bot.js +230 -21
  8. package/dist/commands/cloud.d.ts +49 -0
  9. package/dist/commands/cloud.js +155 -15
  10. package/dist/commands/daemon.d.ts +2 -0
  11. package/dist/commands/daemon.js +15 -4
  12. package/dist/commands/dm-attach.js +15 -3
  13. package/dist/commands/integrations-core.d.ts +2 -0
  14. package/dist/commands/integrations-core.js +1 -0
  15. package/dist/commands/library-add.d.ts +9 -0
  16. package/dist/commands/library-add.js +19 -2
  17. package/dist/commands/run.js +9 -1
  18. package/dist/commands/sources.d.ts +89 -0
  19. package/dist/commands/sources.js +363 -1
  20. package/dist/lib/bot/config.d.ts +2 -0
  21. package/dist/lib/bot/fleet-default-flag.d.ts +31 -0
  22. package/dist/lib/bot/fleet-default-flag.js +127 -0
  23. package/dist/lib/bot/fleet-local.d.ts +30 -1
  24. package/dist/lib/bot/fleet-local.js +86 -11
  25. package/dist/lib/bot/index.d.ts +1 -0
  26. package/dist/lib/bot/index.js +1 -0
  27. package/dist/lib/bot/jobs/hot-signal-sync.d.ts +109 -0
  28. package/dist/lib/bot/jobs/hot-signal-sync.js +177 -0
  29. package/dist/lib/bot/local-config.js +1 -1
  30. package/dist/lib/bot/local-jobs.d.ts +40 -4
  31. package/dist/lib/bot/local-jobs.js +72 -5
  32. package/dist/lib/bot/run.d.ts +15 -0
  33. package/dist/lib/bot/run.js +61 -0
  34. package/dist/lib/daemon/catalog.d.ts +2 -0
  35. package/dist/lib/daemon/catalog.js +7 -2
  36. package/dist/lib/daemon/config.d.ts +2 -1
  37. package/dist/lib/daemon/config.js +4 -3
  38. package/dist/lib/daemon/handoff.d.ts +4 -0
  39. package/dist/lib/daemon/handoff.js +6 -2
  40. package/dist/lib/daemon/install.d.ts +70 -2
  41. package/dist/lib/daemon/install.js +185 -27
  42. package/dist/lib/daemon/legacy.d.ts +9 -3
  43. package/dist/lib/daemon/legacy.js +11 -4
  44. package/dist/lib/daemon/migrate.js +1 -1
  45. package/dist/lib/daemon/run.d.ts +3 -2
  46. package/dist/lib/daemon/run.js +10 -6
  47. package/dist/lib/doctor/checks/anywhere.d.ts +6 -0
  48. package/dist/lib/doctor/checks/anywhere.js +35 -6
  49. package/dist/lib/hot-channels/adapters/__fixtures__/dummy.d.ts +33 -0
  50. package/dist/lib/hot-channels/adapters/__fixtures__/dummy.js +53 -0
  51. package/dist/lib/hot-channels/adapters/__fixtures__/meeting-providers.d.ts +38 -0
  52. package/dist/lib/hot-channels/adapters/__fixtures__/meeting-providers.js +64 -0
  53. package/dist/lib/hot-channels/adapters/attio.d.ts +67 -0
  54. package/dist/lib/hot-channels/adapters/attio.js +374 -0
  55. package/dist/lib/hot-channels/adapters/gmail.d.ts +111 -0
  56. package/dist/lib/hot-channels/adapters/gmail.js +413 -0
  57. package/dist/lib/hot-channels/adapters/index.d.ts +23 -0
  58. package/dist/lib/hot-channels/adapters/index.js +39 -0
  59. package/dist/lib/hot-channels/adapters/linear.d.ts +79 -0
  60. package/dist/lib/hot-channels/adapters/linear.js +321 -0
  61. package/dist/lib/hot-channels/adapters/meeting-providers.d.ts +86 -0
  62. package/dist/lib/hot-channels/adapters/meeting-providers.js +253 -0
  63. package/dist/lib/hot-channels/adapters/meetings.d.ts +81 -0
  64. package/dist/lib/hot-channels/adapters/meetings.js +523 -0
  65. package/dist/lib/hot-channels/adapters/slack.d.ts +70 -0
  66. package/dist/lib/hot-channels/adapters/slack.js +277 -0
  67. package/dist/lib/hot-channels/alert.d.ts +78 -0
  68. package/dist/lib/hot-channels/alert.js +179 -0
  69. package/dist/lib/hot-channels/audience.d.ts +15 -0
  70. package/dist/lib/hot-channels/audience.js +25 -0
  71. package/dist/lib/hot-channels/contract.d.ts +140 -0
  72. package/dist/lib/hot-channels/contract.js +20 -0
  73. package/dist/lib/hot-channels/cursors.d.ts +41 -0
  74. package/dist/lib/hot-channels/cursors.js +84 -0
  75. package/dist/lib/hot-channels/index.d.ts +10 -0
  76. package/dist/lib/hot-channels/index.js +10 -0
  77. package/dist/lib/hot-channels/landing.d.ts +83 -0
  78. package/dist/lib/hot-channels/landing.js +175 -0
  79. package/dist/lib/hot-channels/ledger.d.ts +101 -0
  80. package/dist/lib/hot-channels/ledger.js +186 -0
  81. package/dist/lib/hot-channels/loop.d.ts +74 -0
  82. package/dist/lib/hot-channels/loop.js +219 -0
  83. package/dist/lib/hot-channels/prompts/signals-pass.md +22 -0
  84. package/dist/lib/hot-channels/registry.d.ts +28 -0
  85. package/dist/lib/hot-channels/registry.js +121 -0
  86. package/dist/lib/hot-channels/run.d.ts +137 -0
  87. package/dist/lib/hot-channels/run.js +324 -0
  88. package/dist/lib/hot-channels/signals-pass.d.ts +150 -0
  89. package/dist/lib/hot-channels/signals-pass.js +581 -0
  90. package/dist/lib/hot-channels/sync-lock.d.ts +51 -0
  91. package/dist/lib/hot-channels/sync-lock.js +224 -0
  92. package/dist/lib/install/hook-target.d.ts +6 -2
  93. package/dist/lib/install/hook-target.js +9 -4
  94. package/dist/lib/lanes/loop.d.ts +10 -0
  95. package/dist/lib/lanes/loop.js +18 -1
  96. package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +1 -0
  97. package/dist/lib/mesh/live/daemon/work-subscribe.js +26 -6
  98. package/dist/run/hq-plugin.d.ts +3 -0
  99. package/dist/run/hq-plugin.js +18 -0
  100. package/package.json +7 -7
@@ -22,7 +22,7 @@ import * as path from "path";
22
22
  import { share, getStateDir, listJournals, createIgnoreFilter, VaultClient, computePersonalVaultPaths, PERSONAL_VAULT_JOURNAL_SLUG, resolvePullScope, readSyncProgress, syncDoctor, } from "@indigoai-us/hq-cloud";
23
23
  import { syncLeg } from "../utils/sync-operation-lock.js";
24
24
  import { SYNC_WATCHDOG_EXIT_CODE, SYNC_WATCHDOG_REPORT_GRACE_MS, SyncWatchdogTimeoutError, createSnapshotProgressProbe, resolveSyncWatchdogLimits, withSyncWatchdog, } from "../utils/sync-watchdog.js";
25
- import { DEFAULT_HQ_ROOT, ensureCognitoToken, buildVaultConfig, loadCachedTokens, } from "../utils/cognito-session.js";
25
+ import { DEFAULT_HQ_ROOT, ensureCognitoToken, buildVaultConfig, IdentitySwapRefusal, cognitoCachePrincipalUid, isDefinitivePersonAuthRejection, isIdentitySwapRefusal, loadCachedTokens, } from "../utils/cognito-session.js";
26
26
  import { resolveUploadAuthor } from "../utils/upload-author.js";
27
27
  import { companyFolderExceedsThreshold, emitNarrowHint, isStrictRefusal, resolveBannerLevel, resolveNarrowHintPresentationLevel, resolveNarrowHintMinBytes, } from "../lib/narrow-hint-banner.js";
28
28
  import { beginSyncHealthReport, classifySyncFailure, } from "../utils/client-health.js";
@@ -66,12 +66,130 @@ async function ensureSyncCognitoToken() {
66
66
  interactive: process.stdin.isTTY === true && process.stdout.isTTY === true,
67
67
  });
68
68
  }
69
+ /** Shown when the session expires during a sync and cannot be renewed. */
70
+ export const SYNC_SESSION_EXPIRED_MESSAGE = "Your HQ session expired during sync and could not be refreshed. Run `hq login`, then rerun the sync.";
71
+ /** Shown when the refresh failed for a network or server reason. */
72
+ export const SYNC_REFRESH_UNAVAILABLE_MESSAGE = "Could not refresh your HQ session during sync because the sign-in service did not respond. Check your connection, then rerun the sync.";
73
+ /** Shown when the start token carries no principal to bind the run to. */
74
+ export const SYNC_NO_IDENTITY_MESSAGE = "The HQ session token does not identify an account, so the sync did not start. Run `hq login`, then rerun the sync.";
75
+ /** Shown when the token would change to a different account mid-run. */
76
+ export const SYNC_IDENTITY_CHANGED_MESSAGE = "The signed-in HQ account changed while this sync was running, so the sync stopped instead of continuing as a different account. Rerun the sync as the account you want.";
77
+ /**
78
+ * The session token expired during a sync run and the non-interactive
79
+ * refresh failed. `kind` separates a rejected session (sign in again) from a
80
+ * network or server failure (retry later).
81
+ */
82
+ export class SyncSessionExpiredError extends Error {
83
+ kind;
84
+ reason;
85
+ constructor(kind, reason) {
86
+ const base = kind === "reauth" ? SYNC_SESSION_EXPIRED_MESSAGE : SYNC_REFRESH_UNAVAILABLE_MESSAGE;
87
+ super(reason ? `${base} (${reason})` : base);
88
+ this.kind = kind;
89
+ this.reason = reason;
90
+ this.name = "SyncSessionExpiredError";
91
+ }
92
+ }
93
+ /**
94
+ * Per-config record of the last token-function failure. hq-cloud's one-shot
95
+ * 401 retry swallows getter errors and rethrows the original 401, so
96
+ * {@link formatSyncFailure} reads this (given the run's config) to report the
97
+ * real cause. Keyed by config so two runs in one process never share a record.
98
+ */
99
+ const syncAuthFailures = new WeakMap();
100
+ /** The last token failure recorded by a {@link buildSyncVaultConfig} config. */
101
+ export function syncAuthFailureFor(config) {
102
+ return config ? (syncAuthFailures.get(config)?.() ?? null) : null;
103
+ }
104
+ function classifyRefreshFailure(err) {
105
+ const reason = err instanceof Error ? err.message : String(err);
106
+ const reauth = isDefinitivePersonAuthRejection(err) ||
107
+ /No valid HQ session|Run `hq login`/i.test(reason);
108
+ return new SyncSessionExpiredError(reauth ? "reauth" : "unavailable", reason);
109
+ }
110
+ /**
111
+ * Vault config for a sync run whose token stays valid for the whole run.
112
+ *
113
+ * `initialToken` is resolved (and, on a TTY, logged in) before the run starts.
114
+ * The config carries a getter instead of that string, so every vault request
115
+ * re-reads the cached session and refreshes it when it is close to expiry.
116
+ * When a request still gets a 401, hq-cloud's VaultClient calls the getter
117
+ * once more and retries once if the token changed (no loop). A multi-company
118
+ * `hq sync pull --all` can outlast the one-hour Cognito access token; a
119
+ * captured string made every later target fail with 401 (feedback_3d3fab0b).
120
+ *
121
+ * The getter is bound to the principal of `initialToken`. If a later token
122
+ * belongs to a different principal (a mid-run `hq login` as another account,
123
+ * or the machine-identity fallback after a rejected person refresh), it throws
124
+ * an {@link IdentitySwapRefusal} and the run stops. It never continues as a
125
+ * different principal.
126
+ *
127
+ * Mid-run refreshes never open a browser.
128
+ */
129
+ export function buildSyncVaultConfig(initialToken, deps = {}) {
130
+ const ensureToken = deps.ensureToken ?? (() => ensureCognitoToken({ interactive: false }));
131
+ const runPrincipal = cognitoCachePrincipalUid(initialToken);
132
+ // A start token with no sub/entityUid cannot be bound to a principal, and a
133
+ // null binding would accept any later identity-less token. Refuse up front.
134
+ if (runPrincipal === null) {
135
+ throw new IdentitySwapRefusal(SYNC_NO_IDENTITY_MESSAGE);
136
+ }
137
+ let refused = null;
138
+ let lastSyncAuthFailure = null;
139
+ const authToken = async () => {
140
+ // Once refused, stay refused for the rest of the run.
141
+ if (refused)
142
+ throw refused;
143
+ let token;
144
+ try {
145
+ token = await ensureToken();
146
+ }
147
+ catch (err) {
148
+ const failure = isIdentitySwapRefusal(err) ? err : classifyRefreshFailure(err);
149
+ if (isIdentitySwapRefusal(failure))
150
+ refused = failure;
151
+ lastSyncAuthFailure = failure;
152
+ throw failure;
153
+ }
154
+ if (cognitoCachePrincipalUid(token) !== runPrincipal) {
155
+ refused = new IdentitySwapRefusal(SYNC_IDENTITY_CHANGED_MESSAGE);
156
+ lastSyncAuthFailure = refused;
157
+ throw refused;
158
+ }
159
+ lastSyncAuthFailure = null;
160
+ return token;
161
+ };
162
+ const config = { ...buildVaultConfig(initialToken), authToken };
163
+ syncAuthFailures.set(config, () => lastSyncAuthFailure);
164
+ return config;
165
+ }
166
+ function isVaultAuthRejection(err) {
167
+ // Duck-typed (VaultAuthError sets statusCode 401) so this stays compatible
168
+ // across independently released hq-cloud versions.
169
+ if (typeof err !== "object" || err === null)
170
+ return false;
171
+ const e = err;
172
+ return e.name === "VaultAuthError" || e.statusCode === 401;
173
+ }
69
174
  /**
70
175
  * Preserve the vault's typed write-scope refusal at the CLI boundary. This is
71
176
  * deliberately duck-typed so hq-cli remains compatible while hq-cloud and the
72
177
  * CLI are released independently.
73
178
  */
74
179
  export function formatSyncFailure(err, ctx = {}) {
180
+ if (err instanceof SyncSessionExpiredError || isIdentitySwapRefusal(err)) {
181
+ return err.message;
182
+ }
183
+ if (isVaultAuthRejection(err)) {
184
+ // hq-cloud's 401 retry swallowed the token-function error; report it.
185
+ const recorded = syncAuthFailureFor(ctx.vaultConfig);
186
+ if (recorded)
187
+ return recorded.message;
188
+ // A 401 that survived the one refresh-and-retry: the session is no longer
189
+ // accepted, so the fix is a fresh sign-in.
190
+ const base = err instanceof Error ? err.message : String(err);
191
+ return `${base}. ${SYNC_SESSION_EXPIRED_MESSAGE}`;
192
+ }
75
193
  const unreachable = formatUnreachablePushPaths(err, ctx);
76
194
  if (unreachable)
77
195
  return unreachable;
@@ -694,7 +812,7 @@ export async function pullAll(options, deps) {
694
812
  }
695
813
  }
696
814
  catch (err) {
697
- const message = formatSyncFailure(err);
815
+ const message = (deps.formatFailure ?? formatSyncFailure)(err);
698
816
  result.errors.push({ company: entry.slug, message });
699
817
  result.perCompany.push({ slug: entry.slug, error: message });
700
818
  }
@@ -823,7 +941,7 @@ export async function pushAll(options, deps) {
823
941
  });
824
942
  }
825
943
  catch (err) {
826
- const message = formatSyncFailure(err);
944
+ const message = (deps.formatFailure ?? formatSyncFailure)(err);
827
945
  result.errors.push({ company: entry.slug, message });
828
946
  result.perCompany.push({ slug: entry.slug, error: message });
829
947
  }
@@ -1126,6 +1244,8 @@ export function registerCloudCommands(program) {
1126
1244
  // Assigned right before the sync engine runs, so option-validation
1127
1245
  // failures never count as a sync attempt (US-003).
1128
1246
  let syncHealth;
1247
+ /** The run's vault config, so the catch can report its recorded token failure. */
1248
+ let runVaultConfig;
1129
1249
  let explicitPersonalPaths;
1130
1250
  // Company folder the push resolves relative paths against; names the
1131
1251
  // .hqinclude line and the blocked path in failure output.
@@ -1172,11 +1292,13 @@ export function registerCloudCommands(program) {
1172
1292
  // Upload creds are pre-vended in entityContext, but prefix filtering
1173
1293
  // still needs vault-service membership/grant resolution.
1174
1294
  const accessToken = await ensureSyncCognitoToken();
1175
- scopeVaultConfig = buildVaultConfig(accessToken);
1295
+ scopeVaultConfig = buildSyncVaultConfig(accessToken);
1296
+ runVaultConfig = scopeVaultConfig;
1176
1297
  }
1177
1298
  else {
1178
1299
  const accessToken = await ensureSyncCognitoToken();
1179
- vaultConfig = buildVaultConfig(accessToken);
1300
+ vaultConfig = buildSyncVaultConfig(accessToken);
1301
+ runVaultConfig = vaultConfig;
1180
1302
  scopeVaultConfig = vaultConfig;
1181
1303
  }
1182
1304
  // Resolve the target. For `--personal`, look up the caller's
@@ -1426,6 +1548,7 @@ export function registerCloudCommands(program) {
1426
1548
  const message = formatSyncFailure(err, {
1427
1549
  hqRoot: options.hqRoot,
1428
1550
  companySlug: pushCompanySlug,
1551
+ vaultConfig: runVaultConfig,
1429
1552
  });
1430
1553
  if (jsonMode) {
1431
1554
  // In JSON mode, the parent process is parsing stderr for ndjson —
@@ -1493,12 +1616,15 @@ export function registerCloudCommands(program) {
1493
1616
  return;
1494
1617
  }
1495
1618
  let syncHealth;
1619
+ /** The run's vault config, so the catch can report its recorded token failure. */
1620
+ let runVaultConfig;
1496
1621
  try {
1497
1622
  console.log(chalk.bold("\nHQ Sync — Pull"));
1498
1623
  console.log(` HQ root: ${options.hqRoot}`);
1499
1624
  console.log(` Company: ${options.company ?? "(from .hq/config.json)"}\n`);
1500
1625
  const accessToken = await ensureSyncCognitoToken();
1501
- const vaultConfig = buildVaultConfig(accessToken);
1626
+ const vaultConfig = buildSyncVaultConfig(accessToken);
1627
+ runVaultConfig = vaultConfig;
1502
1628
  // US-011 (2026-05-21 fix): resolve the caller's sync-config for
1503
1629
  // the targeted membership BEFORE the pull, so we can (a) emit the
1504
1630
  // narrow-hint banner after success if still on all-mode and
@@ -1581,7 +1707,7 @@ export function registerCloudCommands(program) {
1581
1707
  catch (err) {
1582
1708
  if (syncHealth)
1583
1709
  await syncHealth.failed();
1584
- const message = formatSyncFailure(err);
1710
+ const message = formatSyncFailure(err, { vaultConfig: runVaultConfig });
1585
1711
  console.error(chalk.red("\n✗ Pull failed:"), message);
1586
1712
  process.exit(1);
1587
1713
  }
@@ -1843,9 +1969,12 @@ async function runPullAll(hqRoot, onConflict, modeAllOverride, skipPersonal, for
1843
1969
  logVaultSkips(skipPersonal, skipCompanies);
1844
1970
  console.log("");
1845
1971
  let result;
1972
+ /** The run's vault config, so the catch can report its recorded token failure. */
1973
+ let runVaultConfig;
1846
1974
  try {
1847
1975
  const accessToken = await ensureSyncCognitoToken();
1848
- const vaultConfig = buildVaultConfig(accessToken);
1976
+ const vaultConfig = buildSyncVaultConfig(accessToken);
1977
+ runVaultConfig = vaultConfig;
1849
1978
  const realClient = new VaultClient(vaultConfig);
1850
1979
  const adapter = {
1851
1980
  listMyMemberships: () => realClient.listMyMemberships(),
@@ -1871,6 +2000,7 @@ async function runPullAll(hqRoot, onConflict, modeAllOverride, skipPersonal, for
1871
2000
  ...(skipCompanies && skipCompanies.size > 0 ? { skipCompanies } : {}),
1872
2001
  }, {
1873
2002
  vaultClient: adapter,
2003
+ formatFailure: (err) => formatSyncFailure(err, { vaultConfig }),
1874
2004
  // DEV-1768: resolve each company's REAL pull scope (mode + prefixSet)
1875
2005
  // via the shared resolver, so the actual pull is scoped — the CLI no
1876
2006
  // longer seeds an all-mode PullRecord that wedges the menubar runner.
@@ -1898,7 +2028,7 @@ async function runPullAll(hqRoot, onConflict, modeAllOverride, skipPersonal, for
1898
2028
  });
1899
2029
  }
1900
2030
  catch (err) {
1901
- console.error(chalk.red("\n✗ Pull-all failed:"), describeSyncError(err));
2031
+ console.error(chalk.red("\n✗ Pull-all failed:"), formatSyncFailure(err, { vaultConfig: runVaultConfig }));
1902
2032
  // Unwind (never process.exit) so withSyncHealthReport records the
1903
2033
  // terminal sync_failure before exiting on our behalf.
1904
2034
  throw new SyncExitError(1);
@@ -1937,9 +2067,12 @@ export async function runPullPersonal(hqRoot, onConflict) {
1937
2067
  console.log(chalk.bold("\nHQ Sync — Pull (personal)"));
1938
2068
  console.log(` HQ root: ${hqRoot}`);
1939
2069
  console.log(` Strategy: ${onConflict ?? "(interactive)"}\n`);
2070
+ /** The run's vault config, so the catch can report its recorded token failure. */
2071
+ let runVaultConfig;
1940
2072
  try {
1941
2073
  const accessToken = await ensureSyncCognitoToken();
1942
- const vaultConfig = buildVaultConfig(accessToken);
2074
+ const vaultConfig = buildSyncVaultConfig(accessToken);
2075
+ runVaultConfig = vaultConfig;
1943
2076
  const client = new VaultClient(vaultConfig);
1944
2077
  const personalUid = await resolveCanonicalPersonUid({
1945
2078
  listMyMemberships: () => client.listMyMemberships(),
@@ -1967,7 +2100,7 @@ export async function runPullPersonal(hqRoot, onConflict) {
1967
2100
  catch (err) {
1968
2101
  if (err instanceof SyncExitError)
1969
2102
  throw err;
1970
- console.error(chalk.red("\n✗ Pull (personal) failed:"), describeSyncError(err));
2103
+ console.error(chalk.red("\n✗ Pull (personal) failed:"), formatSyncFailure(err, { vaultConfig: runVaultConfig }));
1971
2104
  throw new SyncExitError(1);
1972
2105
  }
1973
2106
  }
@@ -1983,9 +2116,12 @@ async function runPushAll(hqRoot, message, onConflict, skipPersonal, skipCompani
1983
2116
  logVaultSkips(skipPersonal, skipCompanies);
1984
2117
  console.log("");
1985
2118
  let result;
2119
+ /** The run's vault config, so the catch can report its recorded token failure. */
2120
+ let runVaultConfig;
1986
2121
  try {
1987
2122
  const accessToken = await ensureSyncCognitoToken();
1988
- const vaultConfig = buildVaultConfig(accessToken);
2123
+ const vaultConfig = buildSyncVaultConfig(accessToken);
2124
+ runVaultConfig = vaultConfig;
1989
2125
  const realClient = new VaultClient(vaultConfig);
1990
2126
  const author = resolveUploadAuthor();
1991
2127
  const adapter = {
@@ -2008,6 +2144,7 @@ async function runPushAll(hqRoot, message, onConflict, skipPersonal, skipCompani
2008
2144
  ...(skipCompanies && skipCompanies.size > 0 ? { skipCompanies } : {}),
2009
2145
  }, {
2010
2146
  vaultClient: adapter,
2147
+ formatFailure: (err) => formatSyncFailure(err, { vaultConfig }),
2011
2148
  resolveScope: (companyUid, slug) => resolvePullScope(realClient, companyUid, slug, hqRoot),
2012
2149
  beforeShare: async ({ slug, companyUid, companyRoot }) => warnAboutInvalidOntologyEntities({
2013
2150
  hqRoot,
@@ -2052,7 +2189,7 @@ async function runPushAll(hqRoot, message, onConflict, skipPersonal, skipCompani
2052
2189
  });
2053
2190
  }
2054
2191
  catch (err) {
2055
- console.error(chalk.red("\n✗ Push-all failed:"), describeSyncError(err));
2192
+ console.error(chalk.red("\n✗ Push-all failed:"), formatSyncFailure(err, { vaultConfig: runVaultConfig }));
2056
2193
  // Unwind (never process.exit) so withSyncHealthReport records the
2057
2194
  // terminal sync_failure before exiting on our behalf.
2058
2195
  throw new SyncExitError(1);
@@ -2149,9 +2286,12 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
2149
2286
  console.log(` HQ root: ${hqRoot}`);
2150
2287
  console.log(` Target: ${personal ? "(personal)" : (company ?? "(active company)")}`);
2151
2288
  console.log(` Strategy: ${onConflict ?? "(interactive)"}\n`);
2289
+ /** The run's vault config, so the catch can report its recorded token failure. */
2290
+ let runVaultConfig;
2152
2291
  try {
2153
2292
  const accessToken = await ensureSyncCognitoToken();
2154
- const vaultConfig = buildVaultConfig(accessToken);
2293
+ const vaultConfig = buildSyncVaultConfig(accessToken);
2294
+ runVaultConfig = vaultConfig;
2155
2295
  const author = resolveUploadAuthor();
2156
2296
  // Resolve the target. For --personal, look up the canonical person and
2157
2297
  // route paths/journal through personalMode. Otherwise use the company arg
@@ -2403,7 +2543,7 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
2403
2543
  catch (err) {
2404
2544
  if (err instanceof SyncExitError)
2405
2545
  throw err;
2406
- console.error(chalk.red("\n✗ Sync now failed:"), describeSyncError(err));
2546
+ console.error(chalk.red("\n✗ Sync now failed:"), formatSyncFailure(err, { vaultConfig: runVaultConfig }));
2407
2547
  throw new SyncExitError(1);
2408
2548
  }
2409
2549
  }
@@ -59,6 +59,8 @@ export interface DaemonCommandDeps {
59
59
  /** Reads stdin for the one-shot daemon machine login; never reads env or argv credentials. */
60
60
  readStdin?: () => Promise<string>;
61
61
  windowsUser?: string;
62
+ /** The Windows user's SID; the daemon's Scheduled Task name carries it so each user on a PC has their own task. */
63
+ windowsSid?: string;
62
64
  windowsHeadlessLauncher?: string;
63
65
  systemUnitDir?: string;
64
66
  systemRecordPath?: string;
@@ -24,7 +24,7 @@ import { DEFAULT_SERVICES, SERVICE_DESCRIPTIONS, enabledServices, hqCloudUnits }
24
24
  import { applyServiceToggle, effectiveSyncOwner, readDaemonConfig, writeDaemonConfig } from "../lib/daemon/config.js";
25
25
  import { daemonUnitInstalled } from "../lib/daemon/handoff.js";
26
26
  import { adoptLegacyUnits, readAdoptedUnits, restoreLegacyUnits } from "../lib/daemon/migrate.js";
27
- import { DAEMON_WINDOWS_TASK, ROOT_REFUSAL, SYSTEM_UNIT_DIR, WINDOWS_START_WAIT_MS, daemonRootRefusal, installDaemonUnit, installScopeFor, uninstallDaemonUnit, windowsInteractiveSession, } from "../lib/daemon/install.js";
27
+ import { ROOT_REFUSAL, SYSTEM_UNIT_DIR, WINDOWS_START_WAIT_MS, daemonRootRefusal, installDaemonUnit, installScopeFor, classifyWindowsTask, legacyWindowsTask, readWindowsSid, uninstallDaemonUnit, windowsInteractiveSession, windowsTaskName, } from "../lib/daemon/install.js";
28
28
  import { describeLegacyUnit, findLegacyUnits } from "../lib/daemon/legacy.js";
29
29
  import { isDaemonProcess, liveDaemonLock, runningDaemonPid } from "../lib/daemon/lock.js";
30
30
  import { bundledHqCloud, managedHqCloud, managedHqCloudRoot, readHqCloudCopy, readHqCloudUpdateState } from "../lib/daemon/hq-cloud.js";
@@ -132,7 +132,10 @@ async function loadDefaults(over) {
132
132
  existsSync: fs.existsSync,
133
133
  listDir: (dir) => fs.readdirSync(dir),
134
134
  launchdJobLoaded: (label) => run("launchctl", ["print", `gui/${process.getuid?.() ?? 0}/${label}`]).status === 0,
135
- windowsTaskExists: (task) => run("schtasks.exe", ["/Query", "/TN", task]).status === 0,
135
+ windowsTaskOwner: (task) => classifyWindowsTask(run, task, {
136
+ windowsSid: over.windowsSid ?? readWindowsSid(run),
137
+ windowsUser: over.windowsUser ?? mesh.resolveWindowsUser(env),
138
+ }).state,
136
139
  })),
137
140
  resolveHqRoot: over.resolveHqRoot ?? ((flag) => resolveDaemonHqRoot(flag, { env, cwd: process.cwd(), home })),
138
141
  userHome: over.userHome ?? ((user) => loginHome(user, run)),
@@ -140,6 +143,7 @@ async function loadDefaults(over) {
140
143
  sleep: over.sleep,
141
144
  interactiveSession: over.interactiveSession,
142
145
  windowsUser: over.windowsUser ?? (platform === "win32" ? mesh.resolveWindowsUser(env) : ""),
146
+ windowsSid: over.windowsSid ?? (platform === "win32" ? readWindowsSid(run) : undefined),
143
147
  windowsHeadlessLauncher: over.windowsHeadlessLauncher ?? (platform === "win32" ? mesh.resolveWindowsHeadlessLauncher(env) : undefined),
144
148
  systemUnitDir: over.systemUnitDir ?? SYSTEM_UNIT_DIR,
145
149
  syncModeClient: over.syncModeClient,
@@ -196,6 +200,7 @@ function hostFor(d, paths, pathEnv) {
196
200
  uid: d.uid,
197
201
  isRoot: d.isRoot,
198
202
  windowsUser: d.windowsUser ?? "",
203
+ windowsSid: d.windowsSid,
199
204
  windowsHeadlessLauncher: d.windowsHeadlessLauncher,
200
205
  systemUnitDir: d.systemUnitDir ?? SYSTEM_UNIT_DIR,
201
206
  env,
@@ -306,6 +311,8 @@ async function runInstall(d, opts) {
306
311
  runningDaemon: () => liveDaemonLock(paths.lockPath, d.daemonAlive),
307
312
  });
308
313
  handOver(d, { paths, detected, runAsUser });
314
+ if (result.notice)
315
+ d.out(result.notice);
309
316
  if (result.warning)
310
317
  d.err(result.warning);
311
318
  if (result.kind === "manual") {
@@ -333,7 +340,7 @@ async function runInstall(d, opts) {
333
340
  ? `Installed ${result.unitPath}, but the daemon did not start within ${seconds} seconds although you are signed in to Windows.`
334
341
  : `Installed ${result.unitPath}, but the daemon did not start within ${seconds} seconds, and HQ could not check whether you are signed in to Windows.`,
335
342
  "Check it with: hq daemon status",
336
- `The task's last result: powershell -NoProfile -Command "(Get-ScheduledTaskInfo -TaskName '${DAEMON_WINDOWS_TASK}').LastTaskResult"`,
343
+ `The task's last result: powershell -NoProfile -Command "(Get-ScheduledTaskInfo -TaskName '${windowsTaskName({ windowsSid: d.windowsSid, windowsUser: d.windowsUser ?? "" })}').LastTaskResult"`,
337
344
  ].join("\n"));
338
345
  }
339
346
  if (!result.running) {
@@ -386,6 +393,8 @@ async function runUninstall(d, opts) {
386
393
  }
387
394
  const result = uninstallDaemonUnit(hostFor(d, paths, false), { scope: installScopeFor(detected.type) }, { run: d.run });
388
395
  d.out(result.removed ? `Removed ${result.unitPath}` : "hq daemon was not installed.");
396
+ if (result.notice)
397
+ d.out(result.notice);
389
398
  if (result.warning)
390
399
  d.err(result.warning);
391
400
  if (opts.restoreLegacy) {
@@ -869,7 +878,9 @@ async function runMigrate(d, opts) {
869
878
  platform: d.platform,
870
879
  home: d.home,
871
880
  systemUnitDir: d.systemUnitDir,
881
+ windowsTaskName: windowsTaskName({ windowsSid: d.windowsSid, windowsUser: d.windowsUser ?? "" }),
872
882
  windowsTaskExists: (task) => d.run("schtasks.exe", ["/Query", "/TN", task]).status === 0,
883
+ windowsLegacyTaskIsMine: () => legacyWindowsTask(d.run, { windowsSid: d.windowsSid, windowsUser: d.windowsUser ?? "" }).state === "mine",
873
884
  });
874
885
  if (!installed) {
875
886
  throw new DaemonCommandError("hq daemon is not installed here yet. Run `hq daemon install` first, then `hq daemon migrate --apply`.");
@@ -893,7 +904,7 @@ async function runMigrate(d, opts) {
893
904
  if (u.migratable)
894
905
  d.out(`Would take over ${u.unitId} from ${describeLegacyUnit(u)}${u.path ? ` (${u.path})` : ""}`);
895
906
  else
896
- d.out(`Leaving ${u.unitId} to ${describeLegacyUnit(u)} (owned by the HQ desktop app)`);
907
+ d.out(`Leaving ${u.unitId} to ${describeLegacyUnit(u)} (${u.notMineReason ?? "owned by the HQ desktop app"})`);
897
908
  }
898
909
  d.out("Nothing was changed. `hq daemon migrate --apply` takes them over.");
899
910
  }
@@ -18,7 +18,8 @@
18
18
  *
19
19
  * Network is injected (`presign`, `put`) so the sequence is unit-testable.
20
20
  */
21
- import { createReadStream, statSync } from "node:fs";
21
+ import { readFileSync, statSync } from "node:fs";
22
+ import { Readable } from "node:stream";
22
23
  import path from "node:path";
23
24
  import { vaultApiFetch } from "../utils/vault-api.js";
24
25
  import { contentTypeFor, streamPut } from "./library-add.js";
@@ -89,7 +90,17 @@ export function validateAttachmentPaths(paths, stat = statSync) {
89
90
  }
90
91
  export const defaultAttachmentUploadDeps = {
91
92
  presign: (token, body) => vaultApiFetch({ token, path: "/v1/files/presign", method: "POST", body }),
92
- put: (file, url, headers) => streamPut(url, headers, createReadStream(file.path), file.sizeBytes),
93
+ // Read the bytes once and declare their real length. Streaming with the
94
+ // size from validateAttachmentPaths' earlier stat sent a Content-Length the
95
+ // body could fall short of (file rewritten or truncated in between); S3 then
96
+ // waits ~20s and answers 400 RequestTimeout. Attachments are capped at 25 MB.
97
+ put: (file, url, headers) => {
98
+ const bytes = readFileSync(file.path);
99
+ if (bytes.length > MAX_DM_ATTACHMENT_BYTES) {
100
+ return Promise.reject(new Error(`Attachment too large: ${file.path} grew past the 25 MB limit after it was checked.`));
101
+ }
102
+ return streamPut(url, headers, Readable.from([bytes]), bytes.length);
103
+ },
93
104
  };
94
105
  /**
95
106
  * Presign-put every file into `scopeUid`, then return the `attachments[]`
@@ -138,7 +149,8 @@ export async function uploadDmAttachments(args) {
138
149
  }
139
150
  const put = await deps.put(p.file, row.url, headers);
140
151
  if (put.status < 200 || put.status >= 300) {
141
- throw new Error(`Upload failed for ${p.file.path} (HTTP ${put.status}). The message was not sent.`);
152
+ const reason = [put.errorCode, put.errorMessage].filter(Boolean).join(": ");
153
+ throw new Error(`Upload failed for ${p.file.path} (HTTP ${put.status}${reason ? ` ${reason}` : ""}). The message was not sent.`);
142
154
  }
143
155
  out.push({
144
156
  vaultPath: p.key,
@@ -299,6 +299,8 @@ export interface UsableApp {
299
299
  * provider, else `--connection <id>`.
300
300
  */
301
301
  selector: string;
302
+ /** The caller created this connection (`viewerAccess.isCreator`). */
303
+ ownedByCaller: boolean;
302
304
  }
303
305
  export interface UsableAppsResult {
304
306
  companyUid: string;
@@ -529,6 +529,7 @@ export async function fetchUsableApps(token, companyUid) {
529
529
  status: row.status,
530
530
  providerArg,
531
531
  selector: shared ? `--connection ${row.id}` : `--provider ${providerArg}`,
532
+ ownedByCaller: row.viewerAccess?.isCreator === true,
532
533
  };
533
534
  })
534
535
  .sort((a, b) => compareNames(a.name, b.name))
@@ -19,7 +19,16 @@ export interface OpenedSource {
19
19
  export interface PutResponse {
20
20
  status: number;
21
21
  headers: Record<string, string | undefined>;
22
+ /** S3 `<Code>` from a non-2xx response body (e.g. `RequestTimeout`), when present. */
23
+ errorCode?: string;
24
+ /** S3 `<Message>` from a non-2xx response body, when present. */
25
+ errorMessage?: string;
22
26
  }
27
+ /** Pull `<Code>` and `<Message>` out of an S3 XML error body. Pure. */
28
+ export declare function parseS3Error(body: string): {
29
+ errorCode?: string;
30
+ errorMessage?: string;
31
+ };
23
32
  export interface LibraryAddDeps {
24
33
  hqRoot: string;
25
34
  now: () => Date;
@@ -33,6 +33,14 @@ import { CONTENT_SHA256_METADATA_KEY, PLACEHOLDER_SUFFIX, companyOffloadKey, con
33
33
  import { formatPlanGateError, formatUpgradeRefusal, planGateErrorFromResponse, } from "../utils/plan-gate-error.js";
34
34
  /** Metadata key hq-pro binds to `checksumSha256` (sync's content hash). */
35
35
  const CONTENT_SHA256_META_KEY = CONTENT_SHA256_METADATA_KEY;
36
+ /** Cap on how much of a non-2xx PUT response body is kept for the error reason. */
37
+ const PUT_ERROR_BODY_LIMIT = 8 * 1024;
38
+ /** Pull `<Code>` and `<Message>` out of an S3 XML error body. Pure. */
39
+ export function parseS3Error(body) {
40
+ const code = /<Code>([^<]{1,200})<\/Code>/.exec(body)?.[1]?.trim();
41
+ const message = /<Message>([^<]{1,500})<\/Message>/.exec(body)?.[1]?.trim();
42
+ return { ...(code ? { errorCode: code } : {}), ...(message ? { errorMessage: message } : {}) };
43
+ }
36
44
  const MEDIA_TYPES = {
37
45
  ".png": "image/png",
38
46
  ".jpg": "image/jpeg",
@@ -515,12 +523,21 @@ export function streamPut(url, headers, body, size) {
515
523
  return new Promise((resolve, reject) => {
516
524
  const target = new URL(url);
517
525
  const request = (target.protocol === "http:" ? http : https).request(target, { method: "PUT", headers: { ...headers, "content-length": String(size) } }, (res) => {
518
- res.resume();
526
+ const status = res.statusCode ?? 0;
527
+ // Keep the start of a failure body so the caller can name S3's reason
528
+ // (RequestTimeout, EntityTooLarge, ...) instead of a bare status.
529
+ let errorBody = "";
530
+ res.setEncoding("utf8");
531
+ res.on("data", (chunk) => {
532
+ if ((status < 200 || status >= 300) && errorBody.length < PUT_ERROR_BODY_LIMIT) {
533
+ errorBody += chunk.slice(0, PUT_ERROR_BODY_LIMIT - errorBody.length);
534
+ }
535
+ });
519
536
  res.on("end", () => {
520
537
  const out = {};
521
538
  for (const [k, v] of Object.entries(res.headers))
522
539
  out[k] = Array.isArray(v) ? v.join(", ") : v;
523
- resolve({ status: res.statusCode ?? 0, headers: out });
540
+ resolve({ status, headers: out, ...(errorBody ? parseS3Error(errorBody) : {}) });
524
541
  });
525
542
  res.on("error", reject);
526
543
  });
@@ -10,7 +10,7 @@ import { computeSha256 } from '../utils/integrity.js';
10
10
  import { isUnavailableSecretLoadHttpStatus, SecretLoadUnavailableError, } from '../utils/secret-load-availability.js';
11
11
  import { vaultApiFetch, getCompanyUid } from '../utils/vault-api.js';
12
12
  import { discoverSchemas } from '../run/discover-schemas.js';
13
- import { installHqPlugin, prewarmHqSecrets } from '../run/hq-plugin.js';
13
+ import { installHqPlugin, itemIsOptional, prewarmHqSecrets } from '../run/hq-plugin.js';
14
14
  import { assertEnvGraphLoaded, resolveEnvValuesOrThrow, EnvGraphLoadError, EnvResolutionError, } from '../run/env-graph-guard.js';
15
15
  import { hadFatalRejection } from '../unhandled-rejection-boundary.js';
16
16
  import { withRunSecretReadTelemetry } from './run-feature-telemetry.js';
@@ -223,6 +223,14 @@ export function registerRunCommand(program) {
223
223
  const schemaItems = Object.entries(graph.configSchema);
224
224
  const itemMessages = (wantWarnings) => schemaItems.flatMap(([k, item]) => (item.errors ?? [])
225
225
  .filter((e) => Boolean(e?.isWarning) === wantWarnings)
226
+ .filter((e) => {
227
+ if (wantWarnings)
228
+ return true;
229
+ if (!itemIsOptional(item))
230
+ return true;
231
+ const msg = e?.message ?? '';
232
+ return !/No read permission|does not exist in company|not returned by vault/.test(msg);
233
+ })
226
234
  .map((e) => ` ${k}: ${e?.message ?? String(e)}`));
227
235
  const warningMsgs = itemMessages(true);
228
236
  if (warningMsgs.length > 0) {
@@ -11,6 +11,8 @@
11
11
  * hq sources get Fetch one source by id.
12
12
  * hq sources channels Print the canonical SOURCE_CHANNELS enum.
13
13
  * hq sources entities List entities the caller has access to.
14
+ * hq sources runs Show the local hot-channel run ledger for one day.
15
+ * hq sources sync Run every hot channel now for the signed-in person.
14
16
  *
15
17
  * Defaults:
16
18
  * --entity falls back to .hq/config.json activeCompany (per sync.ts).
@@ -18,5 +20,92 @@
18
20
  * --limit 50.
19
21
  */
20
22
  import { Command } from "commander";
23
+ import { type RunLedgerAdapterSummary, type RunLedgerEntry, type RunTrigger, type SignalsGeneratedFn } from "../lib/hot-channels/ledger.js";
24
+ import type { HotChannelRegistry } from "../lib/hot-channels/registry.js";
25
+ import { type OnDemandSyncDeps, type OnDemandSyncReport } from "../lib/hot-channels/run.js";
26
+ type ListFormat = "table" | "json";
27
+ export interface RunsOptions {
28
+ today?: boolean;
29
+ date?: string;
30
+ company?: string;
31
+ email?: string;
32
+ format?: ListFormat;
33
+ hqRoot: string;
34
+ }
35
+ export interface RunsReport {
36
+ company: string;
37
+ personKey: string;
38
+ day: string;
39
+ adapters: RunLedgerAdapterSummary[];
40
+ entries: RunLedgerEntry[];
41
+ malformed: number;
42
+ }
43
+ /**
44
+ * Reads one day's run ledger (`sources/_runs/@{personKey}/{day}.jsonl` in the
45
+ * company folder of the local HQ tree). The person key is
46
+ * `audienceKey([email])` of the signed-in person. No network calls.
47
+ */
48
+ export declare function runRuns(options: RunsOptions, now?: Date): Promise<RunsReport>;
49
+ export interface SyncOptions {
50
+ adapter?: string;
51
+ company?: string;
52
+ hqRoot: string;
53
+ json?: boolean;
54
+ }
55
+ export interface SyncOverrides {
56
+ /** Adapters to run. This build registers none, so the default is empty. */
57
+ registry?: HotChannelRegistry;
58
+ now?: () => Date;
59
+ /** The signals pass; defaults to the local `claude -p` pass under the user's own login. */
60
+ signalsGenerated?: SignalsGeneratedFn;
61
+ }
62
+ /**
63
+ * Network access for one sync: the catalog, the person's usable connections,
64
+ * and tool calls through the HQ gateway. Integration tokens stay server-side;
65
+ * the gateway routes each call by connection id.
66
+ */
67
+ export declare function gatewaySyncDeps(token: string, companyUid: string): OnDemandSyncDeps;
68
+ export interface CompanyHotChannelSyncInput {
69
+ hqRoot: string;
70
+ company: string;
71
+ adapter?: string;
72
+ registry?: HotChannelRegistry;
73
+ now?: () => Date;
74
+ /** `scheduled` for the bot's hot-signal-sync job; `on-demand` by default. */
75
+ trigger?: RunTrigger;
76
+ catchUp?: boolean;
77
+ signalsGenerated?: SignalsGeneratedFn;
78
+ /** The signed-in person's `prs_` person uid in this company; defaults to a roster lookup by email. */
79
+ resolvePersonUid?: (token: string, companyUid: string, email: string) => Promise<string | null>;
80
+ /**
81
+ * Where the signals-failure alert goes (US-012). Unset = no alert check.
82
+ * The company-channel post defaults to the Slack channel in the company's
83
+ * `settings/signals/preferences.yaml`, through the HQ integrations gateway.
84
+ */
85
+ signalsAlert?: SignalsAlertSenders;
86
+ }
87
+ export interface SignalsAlertSenders {
88
+ /** The primary alert to the owner; throws when it was not delivered. */
89
+ sendOwnerDm: (text: string) => Promise<void>;
90
+ postCompanyChannel?: (text: string) => Promise<void>;
91
+ log?: (message: string) => void;
92
+ }
93
+ /**
94
+ * One hot-channel sync for the signed-in person in one company: each adapter
95
+ * once per usable connection. Writes under `companies/{company}/`: landed
96
+ * sources in `sources/`, index rows in `sources/_index/`, cursors in
97
+ * `sources/_cursors/`, one ledger line per adapter in `sources/_runs/`, and
98
+ * signals in `signals/@{key}/`. `hq sources sync` and the bot's scheduled
99
+ * hot-signal-sync job both call it. Only one run per person per company goes
100
+ * at a time: a second one writes a `skipped` ledger line and lands nothing.
101
+ */
102
+ export declare function runCompanyHotChannelSync(input: CompanyHotChannelSyncInput): Promise<OnDemandSyncReport>;
103
+ /**
104
+ * Run every hot channel for the signed-in person now: each adapter once per
105
+ * usable connection. Writes cursors, landed sources and an `on-demand` ledger
106
+ * line under `companies/{company}/sources/`, and nothing else.
107
+ */
108
+ export declare function runSync(options: SyncOptions, overrides?: SyncOverrides): Promise<OnDemandSyncReport>;
21
109
  export declare function registerSourcesCommand(program: Command): void;
110
+ export {};
22
111
  //# sourceMappingURL=sources.d.ts.map