@indigoai-us/hq-cli 5.345.169 → 5.345.171
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 +69 -0
- package/assets/scaffold/core/scripts/hq-delegate-send.sh +1 -1
- package/dist/command-catalog.generated.d.ts +181 -9
- package/dist/command-catalog.generated.js +224 -10
- package/dist/commands/access.d.ts +32 -1
- package/dist/commands/access.js +119 -5
- package/dist/commands/agent-kit.d.ts +1 -1
- package/dist/commands/agent-kit.js +3 -1
- package/dist/commands/agents.js +2 -1
- package/dist/commands/bot.d.ts +3 -0
- package/dist/commands/bot.js +141 -14
- package/dist/commands/cloud.d.ts +78 -1
- package/dist/commands/cloud.js +274 -9
- package/dist/commands/core-company-index-scope-flag.d.ts +8 -1
- package/dist/commands/core-company-index-scope-flag.js +9 -2
- package/dist/commands/core-delegate-w1b.js +5 -2
- package/dist/commands/core-dm-bind-native.js +19 -4
- package/dist/commands/core-rebuild-index-fast.js +24 -6
- package/dist/commands/core.js +18 -6
- package/dist/commands/dm.d.ts +17 -3
- package/dist/commands/dm.js +121 -26
- package/dist/commands/feedback-triage.d.ts +43 -1
- package/dist/commands/feedback-triage.js +123 -23
- package/dist/commands/feedback.d.ts +40 -1
- package/dist/commands/feedback.js +184 -4
- package/dist/commands/files.d.ts +42 -0
- package/dist/commands/files.js +223 -30
- package/dist/commands/integrations-api.d.ts +4 -0
- package/dist/commands/integrations-api.js +18 -2
- package/dist/commands/integrations-connect.d.ts +10 -3
- package/dist/commands/integrations-connect.js +70 -6
- package/dist/commands/integrations-core.d.ts +23 -0
- package/dist/commands/integrations-core.js +100 -0
- package/dist/commands/integrations-manage.js +8 -1
- package/dist/commands/integrations.js +49 -5
- package/dist/commands/lanes-loop.d.ts +20 -0
- package/dist/commands/lanes-loop.js +69 -11
- package/dist/commands/lanes.js +9 -1
- package/dist/commands/mesh.js +6 -0
- package/dist/commands/rescue.d.ts +9 -0
- package/dist/commands/rescue.js +31 -0
- package/dist/commands/run.js +115 -40
- package/dist/commands/scan.d.ts +5 -1
- package/dist/commands/scan.js +16 -4
- package/dist/commands/secrets.js +50 -9
- package/dist/commands/sync-manifest.d.ts +16 -0
- package/dist/commands/sync-manifest.js +17 -1
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2367 -2109
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/agent-kit/inbox-state.d.ts +17 -0
- package/dist/lib/agent-kit/inbox-state.js +41 -20
- package/dist/lib/agent-kit/kit-config.d.ts +2 -0
- package/dist/lib/agent-kit/kit-config.js +6 -0
- package/dist/lib/agent-kit/mcp/tools.js +12 -5
- package/dist/lib/agent-kit/run/heartbeat.d.ts +6 -3
- package/dist/lib/agent-kit/run/heartbeat.js +6 -3
- package/dist/lib/agent-kit/run/inbox.d.ts +8 -3
- package/dist/lib/agent-kit/run/inbox.js +42 -10
- package/dist/lib/agent-kit/run/request-timeout.d.ts +26 -0
- package/dist/lib/agent-kit/run/request-timeout.js +51 -0
- package/dist/lib/bot/api.d.ts +1 -1
- package/dist/lib/bot/config.d.ts +10 -0
- package/dist/lib/bot/config.js +17 -1
- package/dist/lib/bot/decisions.d.ts +62 -0
- package/dist/lib/bot/decisions.js +228 -0
- package/dist/lib/bot/fleet-local.d.ts +127 -0
- package/dist/lib/bot/fleet-local.js +638 -0
- package/dist/lib/bot/index.d.ts +2 -0
- package/dist/lib/bot/index.js +2 -0
- package/dist/lib/bot/local-config.d.ts +3 -2
- package/dist/lib/bot/local-config.js +5 -2
- package/dist/lib/bot/promote.d.ts +10 -0
- package/dist/lib/bot/promote.js +3 -2
- package/dist/lib/bot/run.d.ts +7 -0
- package/dist/lib/bot/run.js +104 -1
- package/dist/lib/bot/runtime/index.js +8 -4
- package/dist/lib/bot/runtime/scratch-dir.d.ts +24 -0
- package/dist/lib/bot/runtime/scratch-dir.js +41 -0
- package/dist/lib/caller-principal.d.ts +4 -0
- package/dist/lib/caller-principal.js +40 -0
- package/dist/lib/capacity/collectors/grok.js +8 -2
- package/dist/lib/cli-flag-keys.d.ts +2 -0
- package/dist/lib/cli-flag-keys.js +2 -0
- package/dist/lib/daemon/catalog.d.ts +4 -0
- package/dist/lib/daemon/catalog.js +63 -29
- package/dist/lib/daemon/config.d.ts +7 -0
- package/dist/lib/daemon/config.js +32 -0
- package/dist/lib/daemon/run.d.ts +3 -2
- package/dist/lib/daemon/run.js +23 -3
- package/dist/lib/daemon/supervisor.d.ts +1 -0
- package/dist/lib/daemon/supervisor.js +4 -0
- package/dist/lib/daemon/sync-core-canary.d.ts +12 -0
- package/dist/lib/daemon/sync-core-canary.js +61 -0
- package/dist/lib/doctor/checks/release-links.d.ts +62 -1
- package/dist/lib/doctor/checks/release-links.js +188 -2
- package/dist/lib/doctor/checks/sync-health.d.ts +32 -0
- package/dist/lib/doctor/checks/sync-health.js +54 -1
- package/dist/lib/doctor/fix/apply.d.ts +26 -4
- package/dist/lib/doctor/fix/apply.js +149 -11
- package/dist/lib/doctor/fix/remediation.d.ts +8 -1
- package/dist/lib/doctor/fix/remediation.js +22 -0
- package/dist/lib/doctor/types.d.ts +1 -1
- package/dist/lib/doctor/types.js +1 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/index-render/index.d.ts +9 -0
- package/dist/lib/index-render/index.js +65 -5
- package/dist/lib/index-render/session-scope.d.ts +69 -0
- package/dist/lib/index-render/session-scope.js +279 -0
- package/dist/lib/lanes/admission.js +7 -20
- package/dist/lib/lanes/reconcile.js +13 -15
- package/dist/lib/lanes/stale-lock.d.ts +50 -0
- package/dist/lib/lanes/stale-lock.js +105 -0
- package/dist/lib/lanes/store.js +11 -76
- package/dist/lib/mesh/live/daemon/work-subscribe.js +31 -0
- package/dist/lib/notify-recipient.d.ts +10 -4
- package/dist/lib/notify-recipient.js +16 -4
- package/dist/lib/secret-input-link.d.ts +42 -0
- package/dist/lib/secret-input-link.js +93 -0
- package/dist/lib/secret-scan/range.d.ts +57 -0
- package/dist/lib/secret-scan/range.js +266 -0
- package/dist/lib/secret-scan/repo-config.d.ts +18 -3
- package/dist/lib/secret-scan/repo-config.js +135 -2
- package/dist/lib/work-context/organize.d.ts +7 -0
- package/dist/lib/work-context/organize.js +21 -1
- package/dist/run/script-bound-schema.d.ts +27 -0
- package/dist/run/script-bound-schema.js +97 -0
- package/dist/secrets-env-action.d.ts +2 -0
- package/dist/secrets-env-action.js +7 -2
- package/dist/sentry-before-send.js +1 -0
- package/dist/utils/cognito-session.d.ts +3 -30
- package/dist/utils/cognito-session.js +2 -100
- package/dist/utils/machine-principal-state.d.ts +38 -0
- package/dist/utils/machine-principal-state.js +110 -0
- package/dist/utils/safe-link.js +9 -0
- package/dist/utils/script-bound-exec.d.ts +158 -0
- package/dist/utils/script-bound-exec.js +568 -0
- package/dist/utils/sync-watchdog.d.ts +92 -0
- package/dist/utils/sync-watchdog.js +250 -0
- package/package.json +8 -8
package/dist/commands/cloud.js
CHANGED
|
@@ -18,8 +18,9 @@ import { SERVER_OWNED_FOLDERS, findServerOwnedLocalOnly, } from "../lib/sync/ser
|
|
|
18
18
|
import chalk from "chalk";
|
|
19
19
|
import * as fs from "fs";
|
|
20
20
|
import * as path from "path";
|
|
21
|
-
import { share, getStateDir, listJournals, createIgnoreFilter, VaultClient, computePersonalVaultPaths, PERSONAL_VAULT_JOURNAL_SLUG, resolvePullScope, syncDoctor, } from "@indigoai-us/hq-cloud";
|
|
21
|
+
import { share, getStateDir, listJournals, createIgnoreFilter, VaultClient, computePersonalVaultPaths, PERSONAL_VAULT_JOURNAL_SLUG, resolvePullScope, readSyncProgress, syncDoctor, } from "@indigoai-us/hq-cloud";
|
|
22
22
|
import { syncLeg } from "../utils/sync-operation-lock.js";
|
|
23
|
+
import { SYNC_WATCHDOG_EXIT_CODE, SYNC_WATCHDOG_REPORT_GRACE_MS, SyncWatchdogTimeoutError, createSnapshotProgressProbe, resolveSyncWatchdogLimits, withSyncWatchdog, } from "../utils/sync-watchdog.js";
|
|
23
24
|
import { DEFAULT_HQ_ROOT, ensureCognitoToken, buildVaultConfig, loadCachedTokens, } from "../utils/cognito-session.js";
|
|
24
25
|
import { resolveUploadAuthor } from "../utils/upload-author.js";
|
|
25
26
|
import { companyFolderExceedsThreshold, emitNarrowHint, isStrictRefusal, resolveBannerLevel, resolveNarrowHintPresentationLevel, resolveNarrowHintMinBytes, } from "../lib/narrow-hint-banner.js";
|
|
@@ -69,7 +70,10 @@ async function ensureSyncCognitoToken() {
|
|
|
69
70
|
* deliberately duck-typed so hq-cli remains compatible while hq-cloud and the
|
|
70
71
|
* CLI are released independently.
|
|
71
72
|
*/
|
|
72
|
-
export function formatSyncFailure(err) {
|
|
73
|
+
export function formatSyncFailure(err, ctx = {}) {
|
|
74
|
+
const unreachable = formatUnreachablePushPaths(err, ctx);
|
|
75
|
+
if (unreachable)
|
|
76
|
+
return unreachable;
|
|
73
77
|
// A plan-limit refusal the runner threw (a 402 on the whole request) renders
|
|
74
78
|
// with the same reason, fix options and upgrade link as every other refusal.
|
|
75
79
|
const planGate = planGateErrorFromVaultError(err);
|
|
@@ -105,6 +109,150 @@ export function formatSyncFailure(err) {
|
|
|
105
109
|
}
|
|
106
110
|
return err instanceof Error ? err.message : String(err);
|
|
107
111
|
}
|
|
112
|
+
function asUnreachablePushPaths(err) {
|
|
113
|
+
if (!(err instanceof Error) || err.name !== "UnreachablePushPathsError")
|
|
114
|
+
return null;
|
|
115
|
+
const candidate = err;
|
|
116
|
+
if (!Array.isArray(candidate.paths))
|
|
117
|
+
return null;
|
|
118
|
+
if (typeof candidate.reasons !== "object" || candidate.reasons === null)
|
|
119
|
+
return null;
|
|
120
|
+
const paths = candidate.paths.filter((p) => typeof p === "string");
|
|
121
|
+
return {
|
|
122
|
+
message: err.message,
|
|
123
|
+
paths,
|
|
124
|
+
reasons: candidate.reasons,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The `.hqinclude` line that admits a named path: its HQ-relative spelling,
|
|
129
|
+
* with a trailing slash for a folder. Resolved the way the push resolver
|
|
130
|
+
* reads a named path (absolute, HQ root, the company folder, then the current
|
|
131
|
+
* directory); a path that resolves nowhere inside the HQ tree is suggested as
|
|
132
|
+
* typed.
|
|
133
|
+
*/
|
|
134
|
+
export function hqincludeEntryFor(namedPath, ctx = {}) {
|
|
135
|
+
const toEntry = (rel, isDir) => {
|
|
136
|
+
const posix = rel.split(path.sep).join("/").replace(/\/+$/, "");
|
|
137
|
+
return isDir ? posix + "/" : posix;
|
|
138
|
+
};
|
|
139
|
+
const hqRoot = ctx.hqRoot ? path.resolve(ctx.hqRoot) : undefined;
|
|
140
|
+
if (hqRoot) {
|
|
141
|
+
const candidates = path.isAbsolute(namedPath)
|
|
142
|
+
? [namedPath]
|
|
143
|
+
: [
|
|
144
|
+
path.join(hqRoot, namedPath),
|
|
145
|
+
...(ctx.companySlug
|
|
146
|
+
? [path.join(hqRoot, "companies", ctx.companySlug, namedPath)]
|
|
147
|
+
: []),
|
|
148
|
+
path.resolve(ctx.cwd ?? process.cwd(), namedPath),
|
|
149
|
+
];
|
|
150
|
+
for (const candidate of candidates) {
|
|
151
|
+
const rel = path.relative(hqRoot, candidate);
|
|
152
|
+
if (rel === "" ||
|
|
153
|
+
rel === ".." ||
|
|
154
|
+
rel.startsWith(".." + path.sep) ||
|
|
155
|
+
path.isAbsolute(rel)) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
let stat;
|
|
159
|
+
try {
|
|
160
|
+
stat = fs.statSync(candidate);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
stat = undefined;
|
|
164
|
+
}
|
|
165
|
+
if (stat)
|
|
166
|
+
return toEntry(rel, stat.isDirectory());
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return toEntry(namedPath.replace(/^\.\//, ""), namedPath.endsWith("/"));
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Re-render hq-cloud's UnreachablePushPathsError with the real blocker per
|
|
173
|
+
* path (feedback_6afa6ce4). The upstream message always ends with advice
|
|
174
|
+
* about symlinks, so a path left out of `.hqinclude` read as a symlink
|
|
175
|
+
* problem. This names the `.hqinclude` line to add, and keeps the symlink
|
|
176
|
+
* advice only for reasons that are about links or paths outside the company
|
|
177
|
+
* folder. Returns null for any other error.
|
|
178
|
+
*/
|
|
179
|
+
export function formatUnreachablePushPaths(err, ctx = {}) {
|
|
180
|
+
const parsed = asUnreachablePushPaths(err);
|
|
181
|
+
if (!parsed || parsed.paths.length === 0)
|
|
182
|
+
return null;
|
|
183
|
+
const upstreamLines = parsed.message.split("\n");
|
|
184
|
+
const upstreamLine = (p) => upstreamLines.find((l) => l.startsWith(" · " + p + " — "));
|
|
185
|
+
const ruleFor = (p) => {
|
|
186
|
+
const line = upstreamLine(p);
|
|
187
|
+
if (!line)
|
|
188
|
+
return undefined;
|
|
189
|
+
return line.slice((" · " + p + " — ").length).replace(/; it was not uploaded$/, "");
|
|
190
|
+
};
|
|
191
|
+
const includeEntries = [];
|
|
192
|
+
let linkAdvice = false;
|
|
193
|
+
const lines = parsed.paths.map((p) => {
|
|
194
|
+
const reason = parsed.reasons[p];
|
|
195
|
+
if (reason === "ignored") {
|
|
196
|
+
const rule = ruleFor(p) ?? "excluded by a sync ignore rule";
|
|
197
|
+
if (rule === "not listed in .hqinclude") {
|
|
198
|
+
const entry = hqincludeEntryFor(p, ctx);
|
|
199
|
+
includeEntries.push(entry);
|
|
200
|
+
return " · " + p + " — not listed in .hqinclude, so sync skips it. Add this line to .hqinclude: " + entry;
|
|
201
|
+
}
|
|
202
|
+
return " · " + p + " — " + rule + "; it was not uploaded";
|
|
203
|
+
}
|
|
204
|
+
if (reason === "outside-company" || reason === "unreadable-link") {
|
|
205
|
+
linkAdvice = true;
|
|
206
|
+
}
|
|
207
|
+
return upstreamLine(p) ?? " · " + p + " — could not be pushed (" + String(reason) + ")";
|
|
208
|
+
});
|
|
209
|
+
const count = parsed.paths.length;
|
|
210
|
+
const out = [
|
|
211
|
+
count + " named path" + (count === 1 ? "" : "s") + " could not be pushed; nothing was uploaded.",
|
|
212
|
+
...lines,
|
|
213
|
+
];
|
|
214
|
+
if (includeEntries.length > 0) {
|
|
215
|
+
out.push("Your HQ root has a .hqinclude allowlist, so only listed paths sync. Add the line" +
|
|
216
|
+
(includeEntries.length === 1 ? "" : "s") +
|
|
217
|
+
" above to .hqinclude at the HQ root, then push again.");
|
|
218
|
+
}
|
|
219
|
+
if (linkAdvice) {
|
|
220
|
+
out.push("A path reached through a symlink is only pushable when it stays inside the HQ tree " +
|
|
221
|
+
"(e.g. companies/<slug>/knowledge → repos/private/knowledge-<slug>); one that points " +
|
|
222
|
+
"outside HQ has to sync through whatever owns it, not the vault.");
|
|
223
|
+
}
|
|
224
|
+
return out.join("\n");
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Lines naming the blocker when a push with user-named paths uploaded nothing
|
|
228
|
+
* and every not-uploaded file fell outside write scope (feedback_5baccb9e).
|
|
229
|
+
* The summary before this gave a count and the granted prefixes; this names
|
|
230
|
+
* each requested path as the thing no write grant covers and points at
|
|
231
|
+
* `hq access`, which reports the ACL row that blocks it.
|
|
232
|
+
*/
|
|
233
|
+
export function explicitPushBlockedLines(input) {
|
|
234
|
+
if (!input.userNamedPaths || input.namedPaths.length === 0)
|
|
235
|
+
return [];
|
|
236
|
+
if (input.filesUploaded > 0 || input.filesExcludedByScope <= 0)
|
|
237
|
+
return [];
|
|
238
|
+
if (input.otherNotUploaded > 0)
|
|
239
|
+
return [];
|
|
240
|
+
const keys = input.namedPaths.map((p) => hqincludeEntryFor(p, {
|
|
241
|
+
hqRoot: input.hqRoot,
|
|
242
|
+
cwd: input.cwd,
|
|
243
|
+
companySlug: input.companySlug,
|
|
244
|
+
}).replace(/\/$/, ""));
|
|
245
|
+
const lines = [
|
|
246
|
+
"✗ None of the paths you named were uploaded: every file in them is outside your write access.",
|
|
247
|
+
];
|
|
248
|
+
for (const key of keys) {
|
|
249
|
+
lines.push(input.prefixSet === undefined
|
|
250
|
+
? " · " + key + " — the server refused writes here (an ACL on this path or a parent blocks you)"
|
|
251
|
+
: " · " + key + " — no write prefix you hold covers this path");
|
|
252
|
+
}
|
|
253
|
+
lines.push(" Run `hq access " + keys[0] + "` to see which ACL row blocks the write, then ask its creator or a company admin for write access.");
|
|
254
|
+
return lines;
|
|
255
|
+
}
|
|
108
256
|
/**
|
|
109
257
|
* US-003: run one sync fanout under a client-health attempt/outcome report.
|
|
110
258
|
* The report is best-effort, bounded, and swallowed — it can only append a
|
|
@@ -153,6 +301,41 @@ export function scopeExcludedWarning(count) {
|
|
|
153
301
|
`They were skipped, not synced. Re-run with --json to list them, or ` +
|
|
154
302
|
`ask an admin to grant you write on those paths.`);
|
|
155
303
|
}
|
|
304
|
+
/**
|
|
305
|
+
* Counts the files share() held back because this sync scope is fenced after an account
|
|
306
|
+
* switch on this computer (feedback_a69e12ba). share() reports the hold only as
|
|
307
|
+
* an `identity-switch-push-held` event and leaves the files out of every
|
|
308
|
+
* result counter, so without this the push printed "Pushed 0 file(s) (0 B,
|
|
309
|
+
* 0 skipped)" as a success while the vault kept the old copy.
|
|
310
|
+
*
|
|
311
|
+
* Each event carries the size of one fenced hold, so the tracker keeps the
|
|
312
|
+
* largest count it sees instead of a sum: a repeated emit for the same hold
|
|
313
|
+
* cannot inflate the total. A missing, non-numeric or negative count reads as 0.
|
|
314
|
+
*/
|
|
315
|
+
export function createIdentityHeldTracker() {
|
|
316
|
+
let held = 0;
|
|
317
|
+
return {
|
|
318
|
+
observe(event) {
|
|
319
|
+
if (event.type !== "identity-switch-push-held")
|
|
320
|
+
return;
|
|
321
|
+
const n = typeof event.count === "number" && Number.isFinite(event.count)
|
|
322
|
+
? Math.max(0, Math.trunc(event.count))
|
|
323
|
+
: 0;
|
|
324
|
+
held = Math.max(held, n);
|
|
325
|
+
},
|
|
326
|
+
count: () => held,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
/** Recovery lines for files held by the account-switch push fence. */
|
|
330
|
+
export function identityHeldLines(count) {
|
|
331
|
+
if (count <= 0)
|
|
332
|
+
return [];
|
|
333
|
+
return [
|
|
334
|
+
`⚠ ${count} file(s) were NOT uploaded: uploads for this company are held on this computer ` +
|
|
335
|
+
`after an account sign-in change. Run \`hq sync held\` to see what is held, then ` +
|
|
336
|
+
`\`hq sync release-held\` to upload it.`,
|
|
337
|
+
];
|
|
338
|
+
}
|
|
156
339
|
/** Actionable recovery for files refused because the vault baseline is stale. */
|
|
157
340
|
export function staleUploadRefusalLines(count, paths = []) {
|
|
158
341
|
if (count <= 0)
|
|
@@ -263,6 +446,34 @@ function resolveDeletePolicy() {
|
|
|
263
446
|
return "currency-gated";
|
|
264
447
|
}
|
|
265
448
|
const DEFAULT_SYNC_LOCK_TIMEOUT_SECONDS = 300;
|
|
449
|
+
/**
|
|
450
|
+
* feedback_b7c00a62: run a `sync now` runner under the watchdog (see
|
|
451
|
+
* `utils/sync-watchdog.ts` for the limits). A timeout prints a clear message
|
|
452
|
+
* and unwinds as a failed {@link SyncExitError}, so {@link withSyncHealthReport}
|
|
453
|
+
* records the failure and exits 124. The failure report gets at most
|
|
454
|
+
* {@link SYNC_WATCHDOG_REPORT_GRACE_MS}; after that the process exits 124
|
|
455
|
+
* regardless, and the worker backstop stays armed behind both.
|
|
456
|
+
*/
|
|
457
|
+
export async function runUnderSyncWatchdog(limits, label, run, watchdog = withSyncWatchdog, exit = (code) => process.exit(code)) {
|
|
458
|
+
try {
|
|
459
|
+
return await watchdog(run, {
|
|
460
|
+
idleMs: limits.idleSeconds * 1000,
|
|
461
|
+
maxRuntimeMs: limits.maxRuntimeSeconds * 1000,
|
|
462
|
+
label,
|
|
463
|
+
probeProgress: createSnapshotProgressProbe(readSyncProgress),
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
catch (err) {
|
|
467
|
+
if (err instanceof SyncWatchdogTimeoutError) {
|
|
468
|
+
console.error(chalk.red("\n\u2717 " + err.message));
|
|
469
|
+
process.exitCode = SYNC_WATCHDOG_EXIT_CODE;
|
|
470
|
+
const forceExit = setTimeout(() => exit(SYNC_WATCHDOG_EXIT_CODE), SYNC_WATCHDOG_REPORT_GRACE_MS);
|
|
471
|
+
forceExit.unref();
|
|
472
|
+
throw new SyncExitError(SYNC_WATCHDOG_EXIT_CODE);
|
|
473
|
+
}
|
|
474
|
+
throw err;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
266
477
|
/**
|
|
267
478
|
* Bound foreground waits for a watcher/manual sync that currently owns the
|
|
268
479
|
* per-root operation lock. The cloud engine reads this environment value on
|
|
@@ -912,6 +1123,9 @@ export function registerCloudCommands(program) {
|
|
|
912
1123
|
// failures never count as a sync attempt (US-003).
|
|
913
1124
|
let syncHealth;
|
|
914
1125
|
let explicitPersonalPaths;
|
|
1126
|
+
// Company folder the push resolves relative paths against; names the
|
|
1127
|
+
// .hqinclude line and the blocked path in failure output.
|
|
1128
|
+
let pushCompanySlug;
|
|
915
1129
|
try {
|
|
916
1130
|
if (options.personal && options.credsFromStdin) {
|
|
917
1131
|
throw new Error("`--personal` cannot be combined with --creds-from-stdin: " +
|
|
@@ -1028,6 +1242,15 @@ export function registerCloudCommands(program) {
|
|
|
1028
1242
|
companyRef: targetCompany ?? readActiveCompany(options.hqRoot),
|
|
1029
1243
|
});
|
|
1030
1244
|
const planLimitTracker = createPlanLimitRefusalTracker(companySlugForBilling(progressCompany));
|
|
1245
|
+
// A cmp_ uid target has no slug in progressCompany; fall back to
|
|
1246
|
+
// the entity slug, then the company folder the paths sit in.
|
|
1247
|
+
pushCompanySlug =
|
|
1248
|
+
companySlugForBilling(progressCompany).companySlug ??
|
|
1249
|
+
(entityContext && !entityContext.uid.startsWith("prs_")
|
|
1250
|
+
? entityContext.slug
|
|
1251
|
+
: undefined) ??
|
|
1252
|
+
(personalMode ? undefined : companySlugFromSyncPaths(options.hqRoot, targetPaths));
|
|
1253
|
+
const identityHeld = createIdentityHeldTracker();
|
|
1031
1254
|
syncHealth = beginSyncHealthReport();
|
|
1032
1255
|
if (!personalMode) {
|
|
1033
1256
|
const companyUid = entityContext?.uid ??
|
|
@@ -1055,7 +1278,10 @@ export function registerCloudCommands(program) {
|
|
|
1055
1278
|
}),
|
|
1056
1279
|
hqRoot: options.hqRoot,
|
|
1057
1280
|
onEvent,
|
|
1058
|
-
progressRecorder:
|
|
1281
|
+
progressRecorder: ((record) => (event) => {
|
|
1282
|
+
identityHeld.observe(event);
|
|
1283
|
+
record(event);
|
|
1284
|
+
})(trackingProgressRecorder(planLimitTracker, progressCompany)),
|
|
1059
1285
|
...(personalMode ? { personalMode: true } : {}),
|
|
1060
1286
|
...(journalSlug !== undefined ? { journalSlug } : {}),
|
|
1061
1287
|
...(author ? { author } : {}),
|
|
@@ -1063,6 +1289,7 @@ export function registerCloudCommands(program) {
|
|
|
1063
1289
|
...namedPathShareOptions(userNamedPaths),
|
|
1064
1290
|
});
|
|
1065
1291
|
const planLimitRefused = planLimitTracker.summary();
|
|
1292
|
+
const identityHeldCount = identityHeld.count();
|
|
1066
1293
|
const staleRefusalCount = result.filesRefusedStale ?? 0;
|
|
1067
1294
|
const uploadFailed = readUploadFailures(result);
|
|
1068
1295
|
const refusalRecovery = [
|
|
@@ -1075,6 +1302,9 @@ export function registerCloudCommands(program) {
|
|
|
1075
1302
|
uploadFailed.count > 0
|
|
1076
1303
|
? "Push again; the files that failed are still on this machine."
|
|
1077
1304
|
: null,
|
|
1305
|
+
identityHeldCount > 0
|
|
1306
|
+
? "Run `hq sync held`, then `hq sync release-held` to upload the held files."
|
|
1307
|
+
: null,
|
|
1078
1308
|
]
|
|
1079
1309
|
.filter((message) => message !== null)
|
|
1080
1310
|
.join(" ");
|
|
@@ -1095,6 +1325,7 @@ export function registerCloudCommands(program) {
|
|
|
1095
1325
|
filesExcludedByScope: result.filesExcludedByScope,
|
|
1096
1326
|
filesRefusedStale: staleRefusalCount,
|
|
1097
1327
|
filesRefusedStalePaths: result.filesRefusedStalePaths ?? [],
|
|
1328
|
+
filesHeldByIdentityFence: identityHeldCount,
|
|
1098
1329
|
filesUploadFailed: uploadFailed.count,
|
|
1099
1330
|
uploadFailedPaths: uploadFailed.paths,
|
|
1100
1331
|
...(refusalRecovery ? { refusalRecovery } : {}),
|
|
@@ -1115,8 +1346,8 @@ export function registerCloudCommands(program) {
|
|
|
1115
1346
|
// Never a success line over a refusal: say what was pushed, what
|
|
1116
1347
|
// was refused and how to fix it, then exit non-zero. The sync
|
|
1117
1348
|
// itself worked, so client health records a success. `--json`
|
|
1118
|
-
// callers read `planLimitRefused` on the complete event
|
|
1119
|
-
//
|
|
1349
|
+
// callers also read `planLimitRefused` on the complete event; the
|
|
1350
|
+
// exit status is non-zero in both modes.
|
|
1120
1351
|
log(chalk.yellow(`\n⚠ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ` +
|
|
1121
1352
|
`${result.filesSkipped} skipped), but ${describeRefusedFiles(planLimitRefused)} ` +
|
|
1122
1353
|
"refused: new files are paused for this account."));
|
|
@@ -1132,23 +1363,42 @@ export function registerCloudCommands(program) {
|
|
|
1132
1363
|
for (const line of uploadFailedLines(uploadFailed.count, uploadFailed.paths)) {
|
|
1133
1364
|
log(chalk.yellow(line));
|
|
1134
1365
|
}
|
|
1366
|
+
for (const line of identityHeldLines(identityHeldCount)) {
|
|
1367
|
+
log(chalk.yellow(line));
|
|
1368
|
+
}
|
|
1135
1369
|
await syncHealth.succeeded();
|
|
1136
1370
|
process.exit(1);
|
|
1137
1371
|
return;
|
|
1138
1372
|
}
|
|
1139
|
-
const notUploadedCount = result.filesExcludedByScope + staleRefusalCount + uploadFailed.count
|
|
1373
|
+
const notUploadedCount = result.filesExcludedByScope + staleRefusalCount + uploadFailed.count +
|
|
1374
|
+
identityHeldCount;
|
|
1140
1375
|
if (notUploadedCount > 0) {
|
|
1141
1376
|
log(chalk.yellow(`\n⚠ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ` +
|
|
1142
1377
|
`${result.filesSkipped} skipped, ${notUploadedCount} files not uploaded)`));
|
|
1143
1378
|
for (const line of scopeExcludedLines(result.filesExcludedByScope, pushPrefixSet)) {
|
|
1144
1379
|
log(chalk.yellow(line));
|
|
1145
1380
|
}
|
|
1381
|
+
for (const line of explicitPushBlockedLines({
|
|
1382
|
+
userNamedPaths,
|
|
1383
|
+
namedPaths: targetPaths,
|
|
1384
|
+
filesUploaded: result.filesUploaded,
|
|
1385
|
+
filesExcludedByScope: result.filesExcludedByScope,
|
|
1386
|
+
otherNotUploaded: staleRefusalCount + uploadFailed.count + identityHeldCount,
|
|
1387
|
+
prefixSet: pushPrefixSet,
|
|
1388
|
+
hqRoot: options.hqRoot,
|
|
1389
|
+
companySlug: pushCompanySlug,
|
|
1390
|
+
})) {
|
|
1391
|
+
log(chalk.red(line));
|
|
1392
|
+
}
|
|
1146
1393
|
for (const line of staleUploadRefusalLines(staleRefusalCount, result.filesRefusedStalePaths)) {
|
|
1147
1394
|
log(chalk.yellow(line));
|
|
1148
1395
|
}
|
|
1149
1396
|
for (const line of uploadFailedLines(uploadFailed.count, uploadFailed.paths)) {
|
|
1150
1397
|
log(chalk.yellow(line));
|
|
1151
1398
|
}
|
|
1399
|
+
for (const line of identityHeldLines(identityHeldCount)) {
|
|
1400
|
+
log(chalk.yellow(line));
|
|
1401
|
+
}
|
|
1152
1402
|
}
|
|
1153
1403
|
else {
|
|
1154
1404
|
const skipSummary = formatPushSkipSummary(result);
|
|
@@ -1169,7 +1419,10 @@ export function registerCloudCommands(program) {
|
|
|
1169
1419
|
}
|
|
1170
1420
|
if (syncHealth)
|
|
1171
1421
|
await syncHealth.failed();
|
|
1172
|
-
const message = formatSyncFailure(err
|
|
1422
|
+
const message = formatSyncFailure(err, {
|
|
1423
|
+
hqRoot: options.hqRoot,
|
|
1424
|
+
companySlug: pushCompanySlug,
|
|
1425
|
+
});
|
|
1173
1426
|
if (jsonMode) {
|
|
1174
1427
|
// In JSON mode, the parent process is parsing stderr for ndjson —
|
|
1175
1428
|
// human-formatted error lines would corrupt the stream. Emit a
|
|
@@ -1458,6 +1711,14 @@ export function registerCloudCommands(program) {
|
|
|
1458
1711
|
.option("--force-scope-shrink", "When a scope shrink would un-track locally-modified files, proceed " +
|
|
1459
1712
|
"anyway. Dirty files are KEPT on disk (only un-tracked); clean " +
|
|
1460
1713
|
"out-of-scope files are quarantined under .hq/scope-quarantine/.")
|
|
1714
|
+
.option("--max-idle <seconds>", "For scheduled or unattended runs: stop with exit code " + SYNC_WATCHDOG_EXIT_CODE +
|
|
1715
|
+
" after this many seconds with no file planned, transferred or skipped. The remote " +
|
|
1716
|
+
"listing and planning phase of a leg reports no progress, so set this longer than " +
|
|
1717
|
+
"your slowest planning phase. Laptop sleep does not count. " +
|
|
1718
|
+
"(default: HQ_SYNC_MAX_IDLE, else off)")
|
|
1719
|
+
.option("--max-runtime <seconds>", "For scheduled or unattended runs: stop with exit code " + SYNC_WATCHDOG_EXIT_CODE +
|
|
1720
|
+
" if the run has not finished after this many seconds, even while it is making progress. " +
|
|
1721
|
+
"Laptop sleep does not count. (default: HQ_SYNC_MAX_RUNTIME, else off)")
|
|
1461
1722
|
.action(async (options) => {
|
|
1462
1723
|
const skips = refuseSyncForOffVaults("now", {
|
|
1463
1724
|
all: options.all,
|
|
@@ -1468,15 +1729,19 @@ export function registerCloudCommands(program) {
|
|
|
1468
1729
|
try {
|
|
1469
1730
|
configureSyncLockTimeout(options.lockTimeout);
|
|
1470
1731
|
assertSingleSelector(options, "now");
|
|
1732
|
+
const watchdogLimits = resolveSyncWatchdogLimits({
|
|
1733
|
+
maxIdle: options.maxIdle,
|
|
1734
|
+
maxRuntime: options.maxRuntime,
|
|
1735
|
+
});
|
|
1471
1736
|
if (options.all) {
|
|
1472
1737
|
// `options.personal === false` is Commander's auto-negation
|
|
1473
1738
|
// of `--personal`; in `--all` mode that means "drop the
|
|
1474
1739
|
// personal leg from both legs of the bidirectional fanout"
|
|
1475
1740
|
// (see `--no-personal` option above).
|
|
1476
|
-
await withSyncHealthReport(() => runNowAll(options.hqRoot, options.message, options.onConflict, options.modeAll === true, skips.skipPersonal, options.forceScopeShrink === true, skips.skipCompanies));
|
|
1741
|
+
await withSyncHealthReport(() => runUnderSyncWatchdog(watchdogLimits, "hq sync now --all", () => runNowAll(options.hqRoot, options.message, options.onConflict, options.modeAll === true, skips.skipPersonal, options.forceScopeShrink === true, skips.skipCompanies)));
|
|
1477
1742
|
return;
|
|
1478
1743
|
}
|
|
1479
|
-
await withSyncHealthReport(() => runNowSingle(options.hqRoot, options.company, options.personal === true, options.message, options.onConflict, options.modeAll === true, options.forceScopeShrink === true));
|
|
1744
|
+
await withSyncHealthReport(() => runUnderSyncWatchdog(watchdogLimits, "hq sync now", () => runNowSingle(options.hqRoot, options.company, options.personal === true, options.message, options.onConflict, options.modeAll === true, options.forceScopeShrink === true)));
|
|
1480
1745
|
}
|
|
1481
1746
|
catch (err) {
|
|
1482
1747
|
console.error(chalk.red("\n✗ Sync now failed:"), err instanceof Error ? err.message : String(err));
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
import { CORE_COMPANY_INDEX_SCOPE_FLAG } from "../lib/cli-flag-keys.js";
|
|
2
|
+
export { CORE_COMPANY_INDEX_SCOPE_FLAG };
|
|
3
|
+
/**
|
|
4
|
+
* Kill switch for company-scoped index rebuilds, default ON
|
|
5
|
+
* (indigo-feature-flags-default-on-as-fail-safes). An absent, archived or
|
|
6
|
+
* unreadable flag row keeps scoping on; only an explicit false turns it off,
|
|
7
|
+
* and then `companies` rebuilds every company as it did before scoping.
|
|
8
|
+
*/
|
|
2
9
|
export declare function coreCompanyIndexScopeEnabled(): boolean;
|
|
3
10
|
//# sourceMappingURL=core-company-index-scope-flag.d.ts.map
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { resolveProcessFlagGate } from "../lib/flag-registry.js";
|
|
2
|
-
|
|
2
|
+
import { CORE_COMPANY_INDEX_SCOPE_FLAG } from "../lib/cli-flag-keys.js";
|
|
3
|
+
export { CORE_COMPANY_INDEX_SCOPE_FLAG };
|
|
4
|
+
/**
|
|
5
|
+
* Kill switch for company-scoped index rebuilds, default ON
|
|
6
|
+
* (indigo-feature-flags-default-on-as-fail-safes). An absent, archived or
|
|
7
|
+
* unreadable flag row keeps scoping on; only an explicit false turns it off,
|
|
8
|
+
* and then `companies` rebuilds every company as it did before scoping.
|
|
9
|
+
*/
|
|
3
10
|
export function coreCompanyIndexScopeEnabled() {
|
|
4
|
-
return resolveProcessFlagGate(CORE_COMPANY_INDEX_SCOPE_FLAG, { fallback:
|
|
11
|
+
return resolveProcessFlagGate(CORE_COMPANY_INDEX_SCOPE_FLAG, { fallback: true }, () => true, undefined, { acceptStaleSnapshot: true, killSwitchWithoutCompany: true });
|
|
5
12
|
}
|
|
6
13
|
//# sourceMappingURL=core-company-index-scope-flag.js.map
|
|
@@ -3,6 +3,7 @@ import { createHash, randomInt } from "node:crypto";
|
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as os from "node:os";
|
|
5
5
|
import * as path from "node:path";
|
|
6
|
+
import { callerIsMachinePrincipal } from "../lib/caller-principal.js";
|
|
6
7
|
import { serializeJqJson } from "../lib/core-utils/jq-json-serializer.js";
|
|
7
8
|
import { shellDisplayPath } from "../lib/core-utils/shell-display-path.js";
|
|
8
9
|
import { matchingSecretPatterns } from "../lib/core-utils/secret-patterns.js";
|
|
@@ -1079,8 +1080,10 @@ function runSend(args, context) {
|
|
|
1079
1080
|
catch {
|
|
1080
1081
|
return fail("send", "could not persist sending state; no DM attempted");
|
|
1081
1082
|
}
|
|
1082
|
-
// The pickup DM is read by the person and acted on by their agent.
|
|
1083
|
-
|
|
1083
|
+
// The pickup DM is read by the person and acted on by their agent. Only an
|
|
1084
|
+
// agent or bot may tag it `both`; a person login sends `human`.
|
|
1085
|
+
const audience = callerIsMachinePrincipal() ? "both" : "human";
|
|
1086
|
+
const dm = runHq(["dm", principal, headline, "--audience", audience, "--prompt-file", prompt, "--details-file", brief], context);
|
|
1084
1087
|
if (dm.code !== 0)
|
|
1085
1088
|
return fail("send", "hq dm delivery outcome is uncertain; status is sending, inspect DM history before retrying");
|
|
1086
1089
|
const event = /eventId[: ]+([A-Za-z0-9_-]+)/.exec(dm.stdout ?? "")?.[1] ?? "";
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
+
import { tokenIsMachinePrincipal } from "../lib/caller-principal.js";
|
|
5
6
|
import { spawnSync } from "node:child_process";
|
|
6
7
|
import { resolveWindowsExecutable } from "../lib/bot/runtime/windows.js";
|
|
7
8
|
import { resolveLiveRoot } from "../utils/hq-roots.js";
|
|
@@ -329,9 +330,12 @@ function latestCliEventId(d, ch) {
|
|
|
329
330
|
});
|
|
330
331
|
return String(jqAlternative(rows.at(-1)?.eventId, jqAlternative(rows.at(-1)?.messageId, "")));
|
|
331
332
|
}
|
|
332
|
-
|
|
333
|
+
/** The ID token every human-API request in this file sends as its bearer. */
|
|
334
|
+
function humanApiToken(d) {
|
|
335
|
+
return JSON.parse(d.fs.readFileSync(path.join(d.home, ".hq/cognito-tokens.json"), "utf8")).idToken;
|
|
336
|
+
}
|
|
337
|
+
async function sendHuman(d, ch, payload, token = humanApiToken(d)) {
|
|
333
338
|
const config = JSON.parse(d.fs.readFileSync(path.join(d.home, ".hq/config.json"), "utf8"));
|
|
334
|
-
const token = JSON.parse(d.fs.readFileSync(path.join(d.home, ".hq/cognito-tokens.json"), "utf8")).idToken;
|
|
335
339
|
const headers = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
|
|
336
340
|
const channelsResponse = await d.fetch(`${config.vaultApiUrl}/v1/notify/channels`, { headers, signal: AbortSignal.timeout(DM_BIND_API_TIMEOUT_MS) });
|
|
337
341
|
const channels = await channelsResponse.json();
|
|
@@ -596,11 +600,22 @@ async function post(d, args) {
|
|
|
596
600
|
const mentions = mentionsJson(d, sid, names);
|
|
597
601
|
if (!mentions.length)
|
|
598
602
|
return fail("no mention resolved to a channel member id — not posting an update that notifies nobody");
|
|
599
|
-
|
|
603
|
+
// People never send agent-tagged messages: a person login posts `human`.
|
|
604
|
+
// The audience is read from the token this request sends as its bearer, so
|
|
605
|
+
// a box with machine creds plus a person cache cannot tag a person-token
|
|
606
|
+
// post `agent` (the server rejects that with 403).
|
|
607
|
+
let token;
|
|
608
|
+
try {
|
|
609
|
+
token = humanApiToken(d);
|
|
610
|
+
}
|
|
611
|
+
catch (error) {
|
|
612
|
+
return utilityError(`hq-dm-bind: ${error.message}\n`, 1);
|
|
613
|
+
}
|
|
614
|
+
const payload = { body: postedMessage, mentions, audience: tokenIsMachinePrincipal(token) ? "agent" : "human" };
|
|
600
615
|
if (root)
|
|
601
616
|
payload.rootEventId = root;
|
|
602
617
|
try {
|
|
603
|
-
eventId = await sendHuman(d, ch, payload);
|
|
618
|
+
eventId = await sendHuman(d, ch, payload, token);
|
|
604
619
|
}
|
|
605
620
|
catch (error) {
|
|
606
621
|
return utilityError(`hq-dm-bind: ${error.message}\n`, 1);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** Isolated fast-entry adapter for rebuild-index native renderers. */
|
|
2
2
|
import { CompanyIndexUserError, renderIndexTarget } from "../lib/index-render/index.js";
|
|
3
|
+
import { planScopedRebuild } from "../lib/index-render/session-scope.js";
|
|
4
|
+
import { CORE_COMPANY_INDEX_SCOPE_FLAG } from "../lib/cli-flag-keys.js";
|
|
3
5
|
const targets = new Set([
|
|
4
6
|
"all", "companies", "company-knowledge", "orchestrator", "projects",
|
|
5
7
|
"public-knowledge", "reports", "social-drafts", "threads", "workers",
|
|
@@ -10,7 +12,10 @@ const targets = new Set([
|
|
|
10
12
|
*/
|
|
11
13
|
export async function runRebuildIndexFast(args, _stdin, _cwd, hqRoot) {
|
|
12
14
|
const [target, ...rest] = args;
|
|
13
|
-
|
|
15
|
+
// The only argument the fast route takes is --all-companies; anything else
|
|
16
|
+
// needs the full graph's argument handling.
|
|
17
|
+
const allCompanies = rest.length === 1 && rest[0] === "--all-companies";
|
|
18
|
+
if (!target || !targets.has(target) || (rest.length > 0 && !allCompanies) || !hqRoot)
|
|
14
19
|
return undefined;
|
|
15
20
|
const logs = [];
|
|
16
21
|
const flushLogs = () => {
|
|
@@ -19,11 +24,24 @@ export async function runRebuildIndexFast(args, _stdin, _cwd, hqRoot) {
|
|
|
19
24
|
};
|
|
20
25
|
try {
|
|
21
26
|
const refused = [];
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
// Same session company scope as the full command, resolved before any
|
|
28
|
+
// write. A refusal falls back to the full graph, which reports it.
|
|
29
|
+
// The full flag registry is off-limits on the fast route; read the
|
|
30
|
+
// default-on kill switch from the lightweight snapshot reader, and only for
|
|
31
|
+
// the targets that need it.
|
|
32
|
+
const companyIndexScopeEnabled = target === "companies" || target === "all"
|
|
33
|
+
? await (await import("../native-fast-route.js")).fastFlagEnabledDefaultOn(CORE_COMPANY_INDEX_SCOPE_FLAG)
|
|
34
|
+
: true;
|
|
35
|
+
const plan = planScopedRebuild(target, rest, hqRoot, process.env, { companyIndexScopeEnabled });
|
|
36
|
+
if (plan.warning)
|
|
37
|
+
logs.push(plan.warning);
|
|
38
|
+
for (const run of plan.runs) {
|
|
39
|
+
renderIndexTarget(run.target, {
|
|
40
|
+
root: hqRoot,
|
|
41
|
+
log: (message) => logs.push(message),
|
|
42
|
+
refused,
|
|
43
|
+
}, run.args);
|
|
44
|
+
}
|
|
27
45
|
if (refused.length > 0)
|
|
28
46
|
return undefined;
|
|
29
47
|
flushLogs();
|
package/dist/commands/core.js
CHANGED
|
@@ -57,6 +57,7 @@ import { registerCoreWorkflowRunCommand } from "./core-workflow-run-command.js";
|
|
|
57
57
|
import { resolveLiveRoot } from "../utils/hq-roots.js";
|
|
58
58
|
import { interpreterFor, runBundledScript } from "../utils/run-bundled-script.js";
|
|
59
59
|
import { CompanyIndexUserError, renderIndexTarget } from "../lib/index-render/index.js";
|
|
60
|
+
import { planScopedRebuild, SESSION_SCOPE_HELP } from "../lib/index-render/session-scope.js";
|
|
60
61
|
import { generateWorkersRegistry } from "../lib/workers-registry/index.js";
|
|
61
62
|
import { scanPackages } from "../lib/scan-packages/index.js";
|
|
62
63
|
import { detectStaleCorePolicyMirror } from "../lib/core-utils/detect-stale-core-policy-mirror.js";
|
|
@@ -671,6 +672,7 @@ export function registerCoreCommands(program) {
|
|
|
671
672
|
const wantsHelp = rendererArgs.some((arg) => arg === "--help" || arg === "-h");
|
|
672
673
|
if (wantsHelp && ["companies", "company-knowledge", "projects"].includes(entry.renderer)) {
|
|
673
674
|
renderIndexTarget(entry.renderer, { root: "", output: (message) => process.stdout.write(message) }, rendererArgs);
|
|
675
|
+
process.stdout.write(SESSION_SCOPE_HELP);
|
|
674
676
|
return;
|
|
675
677
|
}
|
|
676
678
|
if (wantsHelp) {
|
|
@@ -686,12 +688,22 @@ export function registerCoreCommands(program) {
|
|
|
686
688
|
const hqRoot = resolveLiveRoot({ hqRoot: scope.hqRoot });
|
|
687
689
|
const refused = [];
|
|
688
690
|
try {
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
}
|
|
691
|
+
// Resolve the session's company scope before any write: a bound
|
|
692
|
+
// session rebuilds only its bound companies, and an unbound one must
|
|
693
|
+
// name a company or pass --all-companies.
|
|
694
|
+
const plan = planScopedRebuild(entry.renderer, rendererArgs, hqRoot, process.env, {
|
|
695
|
+
companyIndexScopeEnabled: coreCompanyIndexScopeEnabled(),
|
|
696
|
+
});
|
|
697
|
+
if (plan.warning)
|
|
698
|
+
process.stderr.write(`${plan.warning}\n`);
|
|
699
|
+
for (const run of plan.runs) {
|
|
700
|
+
renderIndexTarget(run.target, {
|
|
701
|
+
root: hqRoot,
|
|
702
|
+
log: (message) => process.stderr.write(`${message}\n`),
|
|
703
|
+
force,
|
|
704
|
+
refused,
|
|
705
|
+
}, run.args);
|
|
706
|
+
}
|
|
695
707
|
}
|
|
696
708
|
catch (error) {
|
|
697
709
|
if (error instanceof CompanyIndexUserError)
|
package/dist/commands/dm.d.ts
CHANGED
|
@@ -77,6 +77,14 @@ export interface DmContact {
|
|
|
77
77
|
* eba06f59). Returns null when nothing or more than one contact matches. Pure.
|
|
78
78
|
*/
|
|
79
79
|
export declare function matchContactByName(contacts: DmContact[], name: string): DmContact | null;
|
|
80
|
+
/**
|
|
81
|
+
* Every distinct contact (person or bot) whose display name matches `name`,
|
|
82
|
+
* using the same rule as matchContactByName. A leading '@' is the explicit
|
|
83
|
+
* person/bot selector and is ignored for matching. Pure.
|
|
84
|
+
*/
|
|
85
|
+
export declare function matchContactsByName(contacts: DmContact[], name: string): DmContact[];
|
|
86
|
+
/** One line per ambiguous contact match, for a refusal message. Pure. */
|
|
87
|
+
export declare function formatContactMatches(matches: DmContact[]): string;
|
|
80
88
|
/**
|
|
81
89
|
* The confirmation line for a channel send. When the typed target is not the
|
|
82
90
|
* channel's own display name (for example `hq dm setup` matching a channel
|
|
@@ -236,15 +244,21 @@ export declare function fetchChannels(token: string): Promise<ChannelSummary[]>;
|
|
|
236
244
|
* post itself passes. Used to resolve `@name` mentions into participant uids.
|
|
237
245
|
*/
|
|
238
246
|
export declare function fetchChannelMembers(token: string, channelId: string): Promise<ChannelMember[]>;
|
|
247
|
+
/** An explicit agent/both audience from a person login. */
|
|
248
|
+
export declare class PersonAudienceError extends Error {
|
|
249
|
+
constructor();
|
|
250
|
+
}
|
|
239
251
|
/**
|
|
240
252
|
* Resolve the audience for a send. The default comes from the recipient kind
|
|
241
|
-
* (`defaultAudienceFor`
|
|
242
|
-
* `--
|
|
253
|
+
* and the caller (`defaultAudienceFor`: always `human` for a person login);
|
|
254
|
+
* `--for-human` maps to `human`; an explicit `--audience` wins over
|
|
255
|
+
* `--for-human` (with a warning) and is validated. A person login passing
|
|
256
|
+
* `--audience agent|both` throws PersonAudienceError.
|
|
243
257
|
*/
|
|
244
258
|
export declare function resolveSendAudience(opts: {
|
|
245
259
|
audience?: string;
|
|
246
260
|
forHuman?: boolean;
|
|
247
|
-
}, recipientKind: DmRecipientKind, warn?: (msg: string) => void): string;
|
|
261
|
+
}, recipientKind: DmRecipientKind, warn?: (msg: string) => void, machineCaller?: boolean): string;
|
|
248
262
|
/** One incoming DM as returned by GET /v1/notify/inbox. */
|
|
249
263
|
export interface DmInboxEvent {
|
|
250
264
|
eventId: string;
|