@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.
- package/CHANGELOG.md +50 -0
- package/dist/command-catalog.generated.d.ts +79 -1
- package/dist/command-catalog.generated.js +96 -1
- package/dist/commands/bot-jobs.d.ts +11 -9
- package/dist/commands/bot-jobs.js +56 -21
- package/dist/commands/bot.d.ts +13 -0
- package/dist/commands/bot.js +230 -21
- package/dist/commands/cloud.d.ts +49 -0
- package/dist/commands/cloud.js +155 -15
- package/dist/commands/daemon.d.ts +2 -0
- package/dist/commands/daemon.js +15 -4
- package/dist/commands/dm-attach.js +15 -3
- package/dist/commands/integrations-core.d.ts +2 -0
- package/dist/commands/integrations-core.js +1 -0
- package/dist/commands/library-add.d.ts +9 -0
- package/dist/commands/library-add.js +19 -2
- package/dist/commands/run.js +9 -1
- package/dist/commands/sources.d.ts +89 -0
- package/dist/commands/sources.js +363 -1
- package/dist/lib/bot/config.d.ts +2 -0
- package/dist/lib/bot/fleet-default-flag.d.ts +31 -0
- package/dist/lib/bot/fleet-default-flag.js +127 -0
- package/dist/lib/bot/fleet-local.d.ts +30 -1
- package/dist/lib/bot/fleet-local.js +86 -11
- package/dist/lib/bot/index.d.ts +1 -0
- package/dist/lib/bot/index.js +1 -0
- package/dist/lib/bot/jobs/hot-signal-sync.d.ts +109 -0
- package/dist/lib/bot/jobs/hot-signal-sync.js +177 -0
- package/dist/lib/bot/local-config.js +1 -1
- package/dist/lib/bot/local-jobs.d.ts +40 -4
- package/dist/lib/bot/local-jobs.js +72 -5
- package/dist/lib/bot/run.d.ts +15 -0
- package/dist/lib/bot/run.js +61 -0
- package/dist/lib/daemon/catalog.d.ts +2 -0
- package/dist/lib/daemon/catalog.js +7 -2
- package/dist/lib/daemon/config.d.ts +2 -1
- package/dist/lib/daemon/config.js +4 -3
- package/dist/lib/daemon/handoff.d.ts +4 -0
- package/dist/lib/daemon/handoff.js +6 -2
- package/dist/lib/daemon/install.d.ts +70 -2
- package/dist/lib/daemon/install.js +185 -27
- package/dist/lib/daemon/legacy.d.ts +9 -3
- package/dist/lib/daemon/legacy.js +11 -4
- package/dist/lib/daemon/migrate.js +1 -1
- package/dist/lib/daemon/run.d.ts +3 -2
- package/dist/lib/daemon/run.js +10 -6
- package/dist/lib/doctor/checks/anywhere.d.ts +6 -0
- package/dist/lib/doctor/checks/anywhere.js +35 -6
- package/dist/lib/hot-channels/adapters/__fixtures__/dummy.d.ts +33 -0
- package/dist/lib/hot-channels/adapters/__fixtures__/dummy.js +53 -0
- package/dist/lib/hot-channels/adapters/__fixtures__/meeting-providers.d.ts +38 -0
- package/dist/lib/hot-channels/adapters/__fixtures__/meeting-providers.js +64 -0
- package/dist/lib/hot-channels/adapters/attio.d.ts +67 -0
- package/dist/lib/hot-channels/adapters/attio.js +374 -0
- package/dist/lib/hot-channels/adapters/gmail.d.ts +111 -0
- package/dist/lib/hot-channels/adapters/gmail.js +413 -0
- package/dist/lib/hot-channels/adapters/index.d.ts +23 -0
- package/dist/lib/hot-channels/adapters/index.js +39 -0
- package/dist/lib/hot-channels/adapters/linear.d.ts +79 -0
- package/dist/lib/hot-channels/adapters/linear.js +321 -0
- package/dist/lib/hot-channels/adapters/meeting-providers.d.ts +86 -0
- package/dist/lib/hot-channels/adapters/meeting-providers.js +253 -0
- package/dist/lib/hot-channels/adapters/meetings.d.ts +81 -0
- package/dist/lib/hot-channels/adapters/meetings.js +523 -0
- package/dist/lib/hot-channels/adapters/slack.d.ts +70 -0
- package/dist/lib/hot-channels/adapters/slack.js +277 -0
- package/dist/lib/hot-channels/alert.d.ts +78 -0
- package/dist/lib/hot-channels/alert.js +179 -0
- package/dist/lib/hot-channels/audience.d.ts +15 -0
- package/dist/lib/hot-channels/audience.js +25 -0
- package/dist/lib/hot-channels/contract.d.ts +140 -0
- package/dist/lib/hot-channels/contract.js +20 -0
- package/dist/lib/hot-channels/cursors.d.ts +41 -0
- package/dist/lib/hot-channels/cursors.js +84 -0
- package/dist/lib/hot-channels/index.d.ts +10 -0
- package/dist/lib/hot-channels/index.js +10 -0
- package/dist/lib/hot-channels/landing.d.ts +83 -0
- package/dist/lib/hot-channels/landing.js +175 -0
- package/dist/lib/hot-channels/ledger.d.ts +101 -0
- package/dist/lib/hot-channels/ledger.js +186 -0
- package/dist/lib/hot-channels/loop.d.ts +74 -0
- package/dist/lib/hot-channels/loop.js +219 -0
- package/dist/lib/hot-channels/prompts/signals-pass.md +22 -0
- package/dist/lib/hot-channels/registry.d.ts +28 -0
- package/dist/lib/hot-channels/registry.js +121 -0
- package/dist/lib/hot-channels/run.d.ts +137 -0
- package/dist/lib/hot-channels/run.js +324 -0
- package/dist/lib/hot-channels/signals-pass.d.ts +150 -0
- package/dist/lib/hot-channels/signals-pass.js +581 -0
- package/dist/lib/hot-channels/sync-lock.d.ts +51 -0
- package/dist/lib/hot-channels/sync-lock.js +224 -0
- package/dist/lib/install/hook-target.d.ts +6 -2
- package/dist/lib/install/hook-target.js +9 -4
- package/dist/lib/lanes/loop.d.ts +10 -0
- package/dist/lib/lanes/loop.js +18 -1
- package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +1 -0
- package/dist/lib/mesh/live/daemon/work-subscribe.js +26 -6
- package/dist/run/hq-plugin.d.ts +3 -0
- package/dist/run/hq-plugin.js +18 -0
- package/package.json +7 -7
package/dist/commands/cloud.js
CHANGED
|
@@ -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 =
|
|
1295
|
+
scopeVaultConfig = buildSyncVaultConfig(accessToken);
|
|
1296
|
+
runVaultConfig = scopeVaultConfig;
|
|
1176
1297
|
}
|
|
1177
1298
|
else {
|
|
1178
1299
|
const accessToken = await ensureSyncCognitoToken();
|
|
1179
|
-
vaultConfig =
|
|
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 =
|
|
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 =
|
|
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:"),
|
|
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 =
|
|
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:"),
|
|
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 =
|
|
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:"),
|
|
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 =
|
|
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:"),
|
|
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;
|
package/dist/commands/daemon.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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 '${
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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:
|
|
540
|
+
resolve({ status, headers: out, ...(errorBody ? parseS3Error(errorBody) : {}) });
|
|
524
541
|
});
|
|
525
542
|
res.on("error", reject);
|
|
526
543
|
});
|
package/dist/commands/run.js
CHANGED
|
@@ -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
|