@indigoai-us/hq-cli 5.275.0 → 5.277.0
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 +112 -0
- package/README.md +12 -0
- package/assets/bot-workers/setup/context/USER-GUIDE.md +4 -4
- package/assets/scaffold/core/scripts/hq-delegate-pickup.sh +99 -0
- package/assets/scaffold/core/scripts/hq-delegate-repo.sh +158 -0
- package/assets/scaffold/core/scripts/hq-delegate-resolve.sh +145 -0
- package/assets/scaffold/core/scripts/hq-delegate-secrets.sh +129 -0
- package/assets/scaffold/core/scripts/hq-delegate-transfer.sh +175 -0
- package/assets/scaffold/core/scripts/lib/secret-patterns.sh +58 -0
- package/dist/command-catalog.generated.d.ts +239 -1
- package/dist/command-catalog.generated.js +302 -1
- package/dist/commands/core-checkpoint.d.ts +4 -2
- package/dist/commands/core-checkpoint.js +34 -3
- package/dist/commands/core-command-catalog.d.ts +4 -2
- package/dist/commands/core-command-catalog.js +5 -2
- package/dist/commands/core-delegate.d.ts +6 -0
- package/dist/commands/core-delegate.js +987 -0
- package/dist/commands/core-ontology.js +3 -3
- package/dist/commands/core.js +11 -1
- package/dist/commands/daemon.d.ts +4 -0
- package/dist/commands/daemon.js +175 -2
- package/dist/commands/dm.d.ts +2 -0
- package/dist/commands/dm.js +11 -4
- package/dist/commands/feedback.d.ts +6 -0
- package/dist/commands/feedback.js +86 -37
- package/dist/commands/lanes-accounts.js +44 -2
- package/dist/commands/lanes-message.js +69 -12
- package/dist/commands/lanes-resume.js +2 -0
- package/dist/commands/lanes-shared.d.ts +3 -0
- package/dist/commands/lanes-shared.js +12 -2
- package/dist/commands/lanes.d.ts +5 -1
- package/dist/commands/lanes.js +402 -49
- package/dist/commands/run-feature-telemetry.d.ts +3 -0
- package/dist/commands/run-feature-telemetry.js +6 -0
- package/dist/commands/run.js +3 -2
- package/dist/commands/secrets.d.ts +16 -1
- package/dist/commands/secrets.js +80 -45
- package/dist/commands/sync-mode.d.ts +11 -0
- package/dist/commands/sync-mode.js +30 -0
- package/dist/commands/whoami.d.ts +4 -6
- package/dist/commands/whoami.js +39 -8
- package/dist/fast-core.d.ts +6 -7
- package/dist/fast-core.js +22 -8
- package/dist/fast-scaffold.d.ts +33 -7
- package/dist/fast-scaffold.js +104 -8
- package/dist/index.js +32 -1
- package/dist/lanes-drain-hook.js +154 -7
- package/dist/lib/agent-kit/run/inbox.js +3 -2
- package/dist/lib/billing/plan-lock.d.ts +3 -1
- package/dist/lib/billing/plan-lock.js +7 -1
- package/dist/lib/checkpoint-ontology/apply.d.ts +6 -0
- package/dist/lib/checkpoint-ontology/apply.js +60 -3
- package/dist/lib/checkpoint-ontology/company.d.ts +8 -4
- package/dist/lib/checkpoint-ontology/company.js +15 -7
- package/dist/lib/checkpoint-ontology/plan.d.ts +36 -0
- package/dist/lib/checkpoint-ontology/plan.js +66 -4
- package/dist/lib/checkpoint-ontology/queued-touches.d.ts +32 -0
- package/dist/lib/checkpoint-ontology/queued-touches.js +193 -0
- package/dist/lib/checkpoint-ontology/tree.d.ts +2 -0
- package/dist/lib/checkpoint-ontology/tree.js +34 -0
- package/dist/lib/core-utils/jq-json-serializer.d.ts +5 -0
- package/dist/lib/core-utils/jq-json-serializer.js +10 -0
- package/dist/lib/core-utils/secret-patterns.d.ts +41 -0
- package/dist/lib/core-utils/secret-patterns.js +33 -0
- package/dist/lib/core-utils/timeout-guard-native.d.ts +7 -0
- package/dist/lib/core-utils/timeout-guard-native.js +40 -0
- package/dist/lib/core-utils/timeout-guard.d.ts +3 -3
- package/dist/lib/core-utils/timeout-guard.js +28 -13
- package/dist/lib/daemon/catalog.d.ts +18 -2
- package/dist/lib/daemon/catalog.js +18 -2
- package/dist/lib/daemon/config.d.ts +6 -0
- package/dist/lib/daemon/config.js +15 -2
- package/dist/lib/daemon/requests.d.ts +4 -0
- package/dist/lib/daemon/requests.js +2 -0
- package/dist/lib/daemon/run.js +94 -8
- package/dist/lib/daemon/supervisor.d.ts +8 -0
- package/dist/lib/daemon/supervisor.js +101 -1
- package/dist/lib/daemon/sync-runner.d.ts +58 -0
- package/dist/lib/daemon/sync-runner.js +277 -0
- package/dist/lib/doctor/__testing__/run-doctor.d.ts +8 -0
- package/dist/lib/doctor/__testing__/run-doctor.js +52 -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/flag-registry.d.ts +12 -7
- package/dist/lib/flag-registry.js +66 -75
- package/dist/lib/flag-snapshot-file.d.ts +20 -0
- package/dist/lib/flag-snapshot-file.js +91 -0
- package/dist/lib/lanes/claude-balance.d.ts +6 -1
- package/dist/lib/lanes/claude-balance.js +46 -8
- package/dist/lib/lanes/config.js +15 -0
- package/dist/lib/lanes/hire-grant-dispatch.d.ts +2 -0
- package/dist/lib/lanes/hire-grant-dispatch.js +41 -0
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/keep-alive.d.ts +3 -0
- package/dist/lib/lanes/keep-alive.js +3 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +4 -0
- package/dist/lib/lanes/outpost-dispatch.js +11 -2
- package/dist/lib/lanes/reconcile.js +20 -4
- package/dist/lib/lanes/spawn.js +17 -10
- package/dist/lib/lanes/store.js +45 -1
- package/dist/lib/lanes/types.d.ts +30 -1
- package/dist/lib/lanes/types.js +2 -2
- package/dist/lib/lanes/watcher.js +3 -2
- package/dist/lib/process-flag-company-context.d.ts +8 -0
- package/dist/lib/process-flag-company-context.js +67 -0
- package/dist/main.js +3 -1
- package/dist/native-fast-route.d.ts +3 -0
- package/dist/native-fast-route.js +39 -0
- package/dist/secrets-env-fast-path.d.ts +5 -0
- package/dist/secrets-env-fast-path.js +16 -0
- package/dist/secrets-env-fast-route.d.ts +18 -0
- package/dist/secrets-env-fast-route.js +54 -0
- package/dist/utils/cli-telemetry.d.ts +26 -2
- package/dist/utils/cli-telemetry.js +178 -3
- package/dist/utils/hq-root-resolver.d.ts +13 -0
- package/dist/utils/hq-root-resolver.js +103 -0
- package/dist/utils/scaffold-fast-import-guard.js +6 -1
- package/dist/utils/vault-api.d.ts +8 -0
- package/dist/utils/vault-api.js +13 -1
- package/package.json +2 -2
|
@@ -12,7 +12,7 @@ import * as path from "node:path";
|
|
|
12
12
|
import { resolveLiveRoot } from "../utils/hq-roots.js";
|
|
13
13
|
import { describeCoreCommand } from "./core-command-catalog.js";
|
|
14
14
|
import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
|
|
15
|
-
import { MULTI_WRITER_FLAG, planOntologyRun, } from "../lib/checkpoint-ontology/plan.js";
|
|
15
|
+
import { MULTI_WRITER_FLAG, planOntologyRun, payloadOntologyCompany, } from "../lib/checkpoint-ontology/plan.js";
|
|
16
16
|
import { applyOntologyRun, ApplyError } from "../lib/checkpoint-ontology/apply.js";
|
|
17
17
|
import { resolveOntologyCompany, WriteBoundsError } from "../lib/checkpoint-ontology/company.js";
|
|
18
18
|
import { findDuplicateGroups, summarizeMergeBack, } from "../lib/checkpoint-ontology/multi-writer.js";
|
|
@@ -85,8 +85,8 @@ export async function runOntologyPlan(hqRoot, runDirOption, deps = {}) {
|
|
|
85
85
|
}
|
|
86
86
|
export function runOntologyApply(hqRoot, runDirOption, deps = {}) {
|
|
87
87
|
const runDir = requireRunDir(runDirOption);
|
|
88
|
-
const company = readPayload(runDir).
|
|
89
|
-
if (
|
|
88
|
+
const company = payloadOntologyCompany(readPayload(runDir)).slug;
|
|
89
|
+
if (company === null) {
|
|
90
90
|
throw new OntologyUsageError("ontology: the run payload names no company");
|
|
91
91
|
}
|
|
92
92
|
const report = applyOntologyRun({ hqRoot, runDir, company, now: deps.now?.() });
|
package/dist/commands/core.js
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
*/
|
|
31
31
|
import { Option } from "commander";
|
|
32
32
|
import { registerCoreCheckpointCommand } from "./core-checkpoint.js";
|
|
33
|
+
import { registerCoreDelegateCommand } from "./core-delegate.js";
|
|
33
34
|
import { registerCoreOntologyCommand } from "./core-ontology.js";
|
|
34
35
|
import { registerCoreCheckpointGateSettingCommand } from "./core-checkpoint-gate-setting.js";
|
|
35
36
|
import { resolveLiveRoot } from "../utils/hq-roots.js";
|
|
@@ -213,6 +214,14 @@ export const SCAFFOLD_ONLY_ASSETS = [
|
|
|
213
214
|
{ asset: "core/scripts/lib/session-scope-capability.sh", root: "live", summary: "Session scope-capability lib sourced by hq-session" },
|
|
214
215
|
{ asset: "core/scripts/lib/session-id.sh", root: "live", summary: "Session-id resolver lib sourced by hq-session" },
|
|
215
216
|
{ asset: "core/scripts/lib/work-mesh-enqueue.sh", root: "live", summary: "Work Mesh Live spool enqueue lib sourced by hooks" },
|
|
217
|
+
// Wave 1a delegate shells remain bundled as the default-off rollback path.
|
|
218
|
+
// Their roots mirror the original scripts; pickup does not read an HQ root.
|
|
219
|
+
{ asset: "core/scripts/hq-delegate-resolve.sh", root: "live", summary: "Rollback shell for native delegate resolve" },
|
|
220
|
+
{ asset: "core/scripts/hq-delegate-pickup.sh", root: "cwd", summary: "Rollback shell for native delegate pickup (root independent)" },
|
|
221
|
+
{ asset: "core/scripts/hq-delegate-repo.sh", root: "live", summary: "Rollback shell for native delegate repo" },
|
|
222
|
+
{ asset: "core/scripts/hq-delegate-secrets.sh", root: "live", summary: "Rollback shell for native delegate secrets" },
|
|
223
|
+
{ asset: "core/scripts/hq-delegate-transfer.sh", root: "live", summary: "Rollback shell for native delegate transfer" },
|
|
224
|
+
{ asset: "core/scripts/lib/secret-patterns.sh", root: "live", summary: "Secret-pattern library sourced by the delegate rollback shells" },
|
|
216
225
|
...NATIVE_UTILITY_COMMANDS.map(({ asset, root, summary }) => ({
|
|
217
226
|
asset,
|
|
218
227
|
root,
|
|
@@ -288,6 +297,7 @@ export function registerCoreCommands(program) {
|
|
|
288
297
|
registerCoreCheckpointCommand(core);
|
|
289
298
|
registerCoreOntologyCommand(core);
|
|
290
299
|
registerCoreCheckpointGateSettingCommand(core);
|
|
300
|
+
registerCoreDelegateCommand(core);
|
|
291
301
|
// These command names intentionally own native implementations. Their
|
|
292
302
|
// similarly named scaffold assets are forwarders, so sending them through
|
|
293
303
|
// runEntry would recurse back into this command forever.
|
|
@@ -461,7 +471,7 @@ export function registerCoreCommands(program) {
|
|
|
461
471
|
const chunks = [];
|
|
462
472
|
for await (const chunk of process.stdin)
|
|
463
473
|
chunks.push(chunk);
|
|
464
|
-
const code = timeoutGuardCommand(Buffer.concat(chunks).toString("utf8"));
|
|
474
|
+
const code = await timeoutGuardCommand(Buffer.concat(chunks).toString("utf8"));
|
|
465
475
|
if (code !== 0)
|
|
466
476
|
process.exit(code);
|
|
467
477
|
});
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import type { Command } from "commander";
|
|
17
17
|
import { type LegacyUnit } from "../lib/daemon/legacy.js";
|
|
18
18
|
import type { HostApp } from "../lib/daemon/supervisor.js";
|
|
19
|
+
import { type SyncModeVaultClient } from "./sync-mode.js";
|
|
19
20
|
import type { RunCommand } from "../lib/service-manager/launchd.js";
|
|
20
21
|
export interface DaemonCommandDeps {
|
|
21
22
|
env: NodeJS.ProcessEnv;
|
|
@@ -38,9 +39,12 @@ export interface DaemonCommandDeps {
|
|
|
38
39
|
userHome: (user: string) => string;
|
|
39
40
|
/** The pid is a live `hq daemon run`. */
|
|
40
41
|
daemonAlive: (pid: number) => boolean;
|
|
42
|
+
/** Test seam for the company sync-mode commands. */
|
|
43
|
+
syncModeClient?: () => Promise<SyncModeVaultClient>;
|
|
41
44
|
windowsUser?: string;
|
|
42
45
|
windowsHeadlessLauncher?: string;
|
|
43
46
|
systemUnitDir?: string;
|
|
47
|
+
systemRecordPath?: string;
|
|
44
48
|
}
|
|
45
49
|
/**
|
|
46
50
|
* The HQ folder install records: --hq-root, else the CLI's shared resolution
|
package/dist/commands/daemon.js
CHANGED
|
@@ -18,7 +18,7 @@ import * as os from "node:os";
|
|
|
18
18
|
import * as path from "node:path";
|
|
19
19
|
import { major as semverMajor, valid as validSemver } from "semver";
|
|
20
20
|
import { DEFAULT_SERVICES, SERVICE_DESCRIPTIONS, enabledServices, hqCloudUnits } from "../lib/daemon/catalog.js";
|
|
21
|
-
import { applyServiceToggle, readDaemonConfig, writeDaemonConfig } from "../lib/daemon/config.js";
|
|
21
|
+
import { applyServiceToggle, effectiveSyncOwner, readDaemonConfig, writeDaemonConfig } from "../lib/daemon/config.js";
|
|
22
22
|
import { daemonUnitInstalled } from "../lib/daemon/handoff.js";
|
|
23
23
|
import { adoptLegacyUnits, readAdoptedUnits, restoreLegacyUnits } from "../lib/daemon/migrate.js";
|
|
24
24
|
import { ROOT_REFUSAL, SYSTEM_UNIT_DIR, daemonRootRefusal, installDaemonUnit, installScopeFor, uninstallDaemonUnit, } from "../lib/daemon/install.js";
|
|
@@ -27,7 +27,10 @@ import { isDaemonProcess, runningDaemonPid } from "../lib/daemon/lock.js";
|
|
|
27
27
|
import { bundledHqCloud } from "../lib/daemon/hq-cloud.js";
|
|
28
28
|
import { MACHINE_TYPE_MARKER_PATH, detectMachineType, homeFromPasswd, } from "../lib/daemon/machine-type.js";
|
|
29
29
|
import { daemonPaths } from "../lib/daemon/paths.js";
|
|
30
|
+
import { processCommandLine } from "../lib/daemon/lock.js";
|
|
30
31
|
import { writeControlRequest } from "../lib/daemon/requests.js";
|
|
32
|
+
import { isSyncRunnerCommandLine, readWatchRunnerOwnerStatus, watchRunnerOwnerStatusPath } from "../lib/daemon/sync-runner.js";
|
|
33
|
+
import { getSyncModeForCompany, setSyncMode } from "./sync-mode.js";
|
|
31
34
|
import { resolveDefaultHqRoot } from "../utils/cognito-session.js";
|
|
32
35
|
class DaemonCommandError extends Error {
|
|
33
36
|
}
|
|
@@ -96,6 +99,8 @@ async function loadDefaults(over) {
|
|
|
96
99
|
windowsUser: over.windowsUser ?? (platform === "win32" ? mesh.resolveWindowsUser(env) : ""),
|
|
97
100
|
windowsHeadlessLauncher: over.windowsHeadlessLauncher ?? (platform === "win32" ? mesh.resolveWindowsHeadlessLauncher(env) : undefined),
|
|
98
101
|
systemUnitDir: over.systemUnitDir ?? SYSTEM_UNIT_DIR,
|
|
102
|
+
syncModeClient: over.syncModeClient,
|
|
103
|
+
systemRecordPath: over.systemRecordPath ?? path.join(SYSTEM_ADOPTED_ROOT, "adopted.json"),
|
|
99
104
|
};
|
|
100
105
|
}
|
|
101
106
|
function detect(d, flag, config) {
|
|
@@ -250,7 +255,7 @@ function migrateDeps(d, paths) {
|
|
|
250
255
|
recordPath: path.join(paths.root, "adopted.json"),
|
|
251
256
|
// Box system units: root-owned, outside the login user's daemon folder (which handOver chowns to that user).
|
|
252
257
|
systemAdoptedDir: path.join(SYSTEM_ADOPTED_ROOT, "adopted"),
|
|
253
|
-
systemRecordPath: path.join(SYSTEM_ADOPTED_ROOT, "adopted.json"),
|
|
258
|
+
systemRecordPath: d.systemRecordPath ?? path.join(SYSTEM_ADOPTED_ROOT, "adopted.json"),
|
|
254
259
|
systemUnitDir: d.systemUnitDir ?? SYSTEM_UNIT_DIR,
|
|
255
260
|
};
|
|
256
261
|
}
|
|
@@ -372,6 +377,135 @@ function queueRequest(d, request, what) {
|
|
|
372
377
|
const running = runningDaemonPid(paths.lockPath, d.daemonAlive) !== null;
|
|
373
378
|
d.out(running ? `Asked the daemon to ${what}.` : `Queued "${what}"; the daemon is not running, so it applies when it starts.`);
|
|
374
379
|
}
|
|
380
|
+
function daemonSyncModeClient(d) {
|
|
381
|
+
if (d.syncModeClient)
|
|
382
|
+
return d.syncModeClient();
|
|
383
|
+
return (async () => {
|
|
384
|
+
const [{ VaultClient }, { buildVaultConfig, ensureCognitoToken }] = await Promise.all([
|
|
385
|
+
import("@indigoai-us/hq-cloud"),
|
|
386
|
+
import("../utils/cognito-session.js"),
|
|
387
|
+
]);
|
|
388
|
+
const token = await ensureCognitoToken();
|
|
389
|
+
return new VaultClient(buildVaultConfig(token));
|
|
390
|
+
})();
|
|
391
|
+
}
|
|
392
|
+
function daemonSyncStatus(d) {
|
|
393
|
+
const host = currentHost(d);
|
|
394
|
+
const { paths } = host;
|
|
395
|
+
const config = readDaemonConfig(paths.configPath);
|
|
396
|
+
const daemonPid = runningDaemonPid(paths.lockPath, d.daemonAlive);
|
|
397
|
+
const daemonState = daemonPid === null ? null : readState(paths);
|
|
398
|
+
const syncUnit = daemonState?.units.find((unit) => unit.id === "sync");
|
|
399
|
+
const hqRoot = config.hqRoot ?? d.env.HQ_ROOT?.trim();
|
|
400
|
+
let ownerStatusCandidate = null;
|
|
401
|
+
if (hqRoot) {
|
|
402
|
+
try {
|
|
403
|
+
ownerStatusCandidate = readWatchRunnerOwnerStatus(watchRunnerOwnerStatusPath(hqRoot, d.home, d.env));
|
|
404
|
+
}
|
|
405
|
+
catch (error) {
|
|
406
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
407
|
+
d.err(`Could not read sync runner owner status; treating it as unavailable: ${detail}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
const ownerCommandLine = ownerStatusCandidate
|
|
411
|
+
? processCommandLine(ownerStatusCandidate.pid, { platform: d.platform, run: d.run })
|
|
412
|
+
: null;
|
|
413
|
+
const ownerStatus = ownerCommandLine && isSyncRunnerCommandLine(ownerCommandLine)
|
|
414
|
+
? ownerStatusCandidate
|
|
415
|
+
: null;
|
|
416
|
+
return {
|
|
417
|
+
running: syncUnit?.status === "running",
|
|
418
|
+
paused: config.syncPaused === true,
|
|
419
|
+
syncOwner: effectiveSyncOwner(config, host.detected.type),
|
|
420
|
+
owner: ownerStatus?.owner ?? (syncUnit?.status === "running" ? "hq-daemon" : null),
|
|
421
|
+
runnerPid: ownerStatus?.pid ?? syncUnit?.pid ?? null,
|
|
422
|
+
hqCloudVersion: ownerStatus?.version ?? null,
|
|
423
|
+
lastHeartbeat: syncUnit?.lastHeartbeatAt ?? ownerStatus?.heartbeatAt ?? null,
|
|
424
|
+
lastPassResult: syncUnit?.lastPassResult ?? null,
|
|
425
|
+
unitStatus: syncUnit?.status ?? "not-running",
|
|
426
|
+
reason: syncUnit?.reason ?? null,
|
|
427
|
+
logPath: paths.unitLogPath("sync"),
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
async function runDaemonSyncStatus(d, opts) {
|
|
431
|
+
const status = daemonSyncStatus(d);
|
|
432
|
+
if (opts.json) {
|
|
433
|
+
d.out(JSON.stringify(status, null, 2));
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
d.out(`Sync ${status.running ? "running" : status.unitStatus}${status.paused ? " (paused)" : ""}.`);
|
|
437
|
+
d.out(`Configured sync owner: ${status.syncOwner}.`);
|
|
438
|
+
d.out(`Owner: ${status.owner ?? "none"}; runner pid: ${status.runnerPid ?? "none"}; hq-cloud: ${status.hqCloudVersion ?? "unknown"}.`);
|
|
439
|
+
if (status.reason)
|
|
440
|
+
d.out(`Reason: ${status.reason}`);
|
|
441
|
+
d.out(`Log: ${status.logPath}`);
|
|
442
|
+
}
|
|
443
|
+
function queueDaemonSyncControl(d, request) {
|
|
444
|
+
const host = currentHost(d);
|
|
445
|
+
writeControlRequest(host.paths.requestsDir, request);
|
|
446
|
+
handOver(d, host);
|
|
447
|
+
return runningDaemonPid(host.paths.lockPath, d.daemonAlive) !== null;
|
|
448
|
+
}
|
|
449
|
+
async function runDaemonSyncNow(d, opts) {
|
|
450
|
+
const host = currentHost(d);
|
|
451
|
+
const config = readDaemonConfig(host.paths.configPath);
|
|
452
|
+
if (config.syncPaused)
|
|
453
|
+
throw new DaemonCommandError("sync is paused; run `hq daemon sync resume` first");
|
|
454
|
+
if (!enabledServices(host.detected.type, config).includes("sync")) {
|
|
455
|
+
throw new DaemonCommandError("sync is disabled for this machine; run `hq daemon enable sync` first");
|
|
456
|
+
}
|
|
457
|
+
const running = queueDaemonSyncControl(d, { action: "restart", unit: "sync" });
|
|
458
|
+
const result = { accepted: true, action: "restart-sync-runner", daemonRunning: running };
|
|
459
|
+
d.out(opts.json ? JSON.stringify(result, null, 2) : running ? "Asked the daemon to start a fresh sync pass." : "Queued a fresh sync pass for the next daemon start.");
|
|
460
|
+
}
|
|
461
|
+
async function runDaemonSyncPause(d, paused, opts) {
|
|
462
|
+
const host = currentHost(d);
|
|
463
|
+
const config = readDaemonConfig(host.paths.configPath);
|
|
464
|
+
if (config.syncPaused !== paused)
|
|
465
|
+
writeDaemonConfig(host.paths.configPath, { ...config, syncPaused: paused });
|
|
466
|
+
const daemonRunning = queueDaemonSyncControl(d, { action: "reconcile" });
|
|
467
|
+
const result = { paused, persisted: true, daemonRunning };
|
|
468
|
+
d.out(opts.json ? JSON.stringify(result, null, 2) : `Sync ${paused ? "paused" : "resumed"}${daemonRunning ? "" : "; the setting will apply when the daemon starts"}.`);
|
|
469
|
+
}
|
|
470
|
+
async function runDaemonSyncTakeOwnership(d, opts) {
|
|
471
|
+
const host = currentHost(d);
|
|
472
|
+
const config = readDaemonConfig(host.paths.configPath);
|
|
473
|
+
if (config.syncOwner !== "daemon")
|
|
474
|
+
writeDaemonConfig(host.paths.configPath, { ...config, syncOwner: "daemon" });
|
|
475
|
+
const daemonRunning = queueDaemonSyncControl(d, { action: "reconcile" });
|
|
476
|
+
const result = { syncOwner: "daemon", persisted: true, daemonRunning };
|
|
477
|
+
d.out(opts.json ? JSON.stringify(result, null, 2) : `Daemon sync ownership persisted${daemonRunning ? "; asked the daemon to reconcile" : "; it applies when the daemon starts"}.`);
|
|
478
|
+
}
|
|
479
|
+
async function runDaemonSyncModeGet(d, companySlug, opts) {
|
|
480
|
+
const result = await getSyncModeForCompany({ companySlug, vaultClient: await daemonSyncModeClient(d) });
|
|
481
|
+
const value = {
|
|
482
|
+
companySlug: result.companySlug,
|
|
483
|
+
membershipId: result.membershipId,
|
|
484
|
+
mode: result.config.syncMode,
|
|
485
|
+
isDefault: result.config.isDefault,
|
|
486
|
+
...(result.config.updatedAt ? { updatedAt: result.config.updatedAt } : {}),
|
|
487
|
+
};
|
|
488
|
+
d.out(opts.json ? JSON.stringify(value, null, 2) : `${value.companySlug}: ${value.mode}${value.isDefault ? " (default)" : ""}`);
|
|
489
|
+
}
|
|
490
|
+
async function runDaemonSyncModeSet(d, companySlug, mode, opts) {
|
|
491
|
+
if (mode !== "all" && mode !== "shared") {
|
|
492
|
+
throw new DaemonCommandError(`Invalid sync mode '${mode}'. Use all or shared.`);
|
|
493
|
+
}
|
|
494
|
+
const result = await setSyncMode({
|
|
495
|
+
mode,
|
|
496
|
+
companySlug,
|
|
497
|
+
vaultClient: await daemonSyncModeClient(d),
|
|
498
|
+
});
|
|
499
|
+
const value = {
|
|
500
|
+
companySlug: result.companySlug,
|
|
501
|
+
membershipId: result.membershipId,
|
|
502
|
+
previousMode: result.previousMode,
|
|
503
|
+
mode: result.newMode,
|
|
504
|
+
isDefault: result.newConfig.isDefault,
|
|
505
|
+
...(result.newConfig.updatedAt ? { updatedAt: result.newConfig.updatedAt } : {}),
|
|
506
|
+
};
|
|
507
|
+
d.out(opts.json ? JSON.stringify(value, null, 2) : `${value.companySlug}: ${value.previousMode} → ${value.mode}`);
|
|
508
|
+
}
|
|
375
509
|
async function runToggle(d, action, services) {
|
|
376
510
|
const host = currentHost(d);
|
|
377
511
|
const { paths } = host;
|
|
@@ -608,6 +742,45 @@ export function registerDaemonCommand(program, over = {}) {
|
|
|
608
742
|
.description("Show the machine type and each service's state")
|
|
609
743
|
.option("--json", "Print machine-readable JSON")
|
|
610
744
|
.action(wrap((d, opts) => runStatus(d, opts)));
|
|
745
|
+
const sync = daemon
|
|
746
|
+
.command("sync")
|
|
747
|
+
.description("Inspect and control the daemon's company sync runner");
|
|
748
|
+
sync
|
|
749
|
+
.command("status")
|
|
750
|
+
.description("Show sync ownership, heartbeat, last pass and log path")
|
|
751
|
+
.option("--json", "Print machine-readable JSON")
|
|
752
|
+
.action(wrap((d, opts) => runDaemonSyncStatus(d, opts)));
|
|
753
|
+
sync
|
|
754
|
+
.command("now")
|
|
755
|
+
.description("Start a fresh sync pass immediately")
|
|
756
|
+
.option("--json", "Print machine-readable JSON")
|
|
757
|
+
.action(wrap((d, opts) => runDaemonSyncNow(d, opts)));
|
|
758
|
+
sync
|
|
759
|
+
.command("pause")
|
|
760
|
+
.description("Pause daemon sync until resumed")
|
|
761
|
+
.option("--json", "Print machine-readable JSON")
|
|
762
|
+
.action(wrap((d, opts) => runDaemonSyncPause(d, true, opts)));
|
|
763
|
+
sync
|
|
764
|
+
.command("resume")
|
|
765
|
+
.description("Resume daemon sync after a pause")
|
|
766
|
+
.option("--json", "Print machine-readable JSON")
|
|
767
|
+
.action(wrap((d, opts) => runDaemonSyncPause(d, false, opts)));
|
|
768
|
+
sync
|
|
769
|
+
.command("take-ownership")
|
|
770
|
+
.description("Persist daemon ownership of desktop sync and reconcile")
|
|
771
|
+
.option("--json", "Print machine-readable JSON")
|
|
772
|
+
.action(wrap((d, opts) => runDaemonSyncTakeOwnership(d, opts)));
|
|
773
|
+
const mode = sync.command("mode").description("Read or change a company's sync footprint");
|
|
774
|
+
mode
|
|
775
|
+
.command("get <company>")
|
|
776
|
+
.description("Read a company's current all/shared sync mode")
|
|
777
|
+
.option("--json", "Print machine-readable JSON")
|
|
778
|
+
.action(wrap((d, company, opts) => runDaemonSyncModeGet(d, company, opts)));
|
|
779
|
+
mode
|
|
780
|
+
.command("set <company> <mode>")
|
|
781
|
+
.description("Set a company's sync mode to all or shared")
|
|
782
|
+
.option("--json", "Print machine-readable JSON")
|
|
783
|
+
.action(wrap((d, company, mode, opts) => runDaemonSyncModeSet(d, company, mode, opts)));
|
|
611
784
|
daemon
|
|
612
785
|
.command("logs [unit]")
|
|
613
786
|
.description("Print the supervisor log, or one service's log")
|
package/dist/commands/dm.d.ts
CHANGED
|
@@ -368,5 +368,7 @@ export declare function agentCallerUid(token: string, machine?: () => boolean):
|
|
|
368
368
|
export declare const AGENT_THREAD_UNAVAILABLE: string;
|
|
369
369
|
/** One-line summary of an agent inbox item (DM, email, … — shapes vary by channel). Pure. */
|
|
370
370
|
export declare function formatAgentInboxItem(item: Record<string, unknown>): string;
|
|
371
|
+
/** Remove delegated execution context before agent inbox items can be emitted. */
|
|
372
|
+
export declare function redactAgentInboxItem<T extends object>(item: T): Omit<T, "actingContext">;
|
|
371
373
|
export declare function registerDmCommand(program: Command): void;
|
|
372
374
|
//# sourceMappingURL=dm.d.ts.map
|
package/dist/commands/dm.js
CHANGED
|
@@ -1064,16 +1064,22 @@ export function formatAgentInboxItem(item) {
|
|
|
1064
1064
|
const text = str("text") || str("body") || str("subject");
|
|
1065
1065
|
return `${chalk.bold(from)} ${chalk.dim(`[${channel}] ${id}${when ? ` ${when}` : ""}`)}\n ${firstLine(text)}`;
|
|
1066
1066
|
}
|
|
1067
|
+
/** Remove delegated execution context before agent inbox items can be emitted. */
|
|
1068
|
+
export function redactAgentInboxItem(item) {
|
|
1069
|
+
const { actingContext: _actingContext, ...safe } = item;
|
|
1070
|
+
return safe;
|
|
1071
|
+
}
|
|
1067
1072
|
async function runAgentInbox(token, agentUid, opts) {
|
|
1068
1073
|
const route = `/v1/agents/${encodeURIComponent(agentUid)}/inbox`;
|
|
1069
1074
|
const res = await vaultApiFetch({ token, path: route });
|
|
1070
1075
|
if (!res.ok) {
|
|
1071
|
-
|
|
1072
|
-
|
|
1076
|
+
// Error bodies are not constrained to the success response shape. Do not
|
|
1077
|
+
// echo them: a proxy or partial failure could include inbox items.
|
|
1078
|
+
console.error(chalk.red(`GET ${route} → ${res.status}`));
|
|
1073
1079
|
process.exit(1);
|
|
1074
1080
|
}
|
|
1075
1081
|
const data = (await res.json().catch(() => ({})));
|
|
1076
|
-
let items = Array.isArray(data.messages) ? data.messages : [];
|
|
1082
|
+
let items = (Array.isArray(data.messages) ? data.messages : []).map(redactAgentInboxItem);
|
|
1077
1083
|
if (opts.limit && Number(opts.limit) > 0)
|
|
1078
1084
|
items = items.slice(0, Number(opts.limit));
|
|
1079
1085
|
if (opts.json) {
|
|
@@ -1251,7 +1257,8 @@ async function runDmChannel(target, opts) {
|
|
|
1251
1257
|
}
|
|
1252
1258
|
const data = (await res.json());
|
|
1253
1259
|
const messages = data.messages ?? [];
|
|
1254
|
-
const
|
|
1260
|
+
const agentCaller = agentCallerUid(token) !== null;
|
|
1261
|
+
const ordered = [...messages].reverse().map((message) => agentCaller ? redactAgentInboxItem(message) : message); // oldest-first for reading; agent JSON must not expose delegated context
|
|
1255
1262
|
if (opts.json) {
|
|
1256
1263
|
console.log(JSON.stringify(ordered, null, 2));
|
|
1257
1264
|
}
|
|
@@ -3,6 +3,8 @@ import { type DiagnosticsBlob } from "../utils/feedback-diagnostics.js";
|
|
|
3
3
|
import { collectFeedbackLogs, feedbackLogsEnabled } from "../utils/feedback-logs.js";
|
|
4
4
|
import { type LogBundleRef } from "../utils/feedback-log-bundle.js";
|
|
5
5
|
export declare const BODY_MAX_BYTES: number;
|
|
6
|
+
/** Validate fields that must be valid before feedback attachments are uploaded. */
|
|
7
|
+
export declare function validateFeedbackContent(title: string, body: string): void;
|
|
6
8
|
/**
|
|
7
9
|
* Mirror of the server's `MAX_BODY_BYTES` in hq-pro
|
|
8
10
|
* `src/vault-service/handlers/feedback.ts`, which rejects the WHOLE request
|
|
@@ -36,6 +38,10 @@ export interface FeedbackSubmitOptions {
|
|
|
36
38
|
* stays a summary and this carries the rest.
|
|
37
39
|
*/
|
|
38
40
|
logBundle?: LogBundleRef;
|
|
41
|
+
/** Diagnostics collected before uploads by the command path. */
|
|
42
|
+
diagnostics?: DiagnosticsBlob;
|
|
43
|
+
/** Precomputed setting so environment lookup happens before attachments upload. */
|
|
44
|
+
logsEnabled?: boolean;
|
|
39
45
|
}
|
|
40
46
|
export declare function readBodyFile(bodyFile: string, stdin?: NodeJS.ReadableStream): Promise<string>;
|
|
41
47
|
/**
|
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
4
|
-
import { vaultApiFetch } from "../utils/vault-api.js";
|
|
4
|
+
import { assertVaultApiRequestSupported, vaultApiFetch, } from "../utils/vault-api.js";
|
|
5
5
|
import { collectDiagnostics } from "../utils/feedback-diagnostics.js";
|
|
6
6
|
import { MAX_SCREENSHOTS, uploadScreenshots } from "../utils/feedback-screenshots.js";
|
|
7
7
|
import { FEEDBACK_LOGS_MAX_TOTAL_BYTES, FEEDBACK_LOGS_MIN_USEFUL_BYTES, collectFeedbackLogs, feedbackLogsEnabled, } from "../utils/feedback-logs.js";
|
|
8
8
|
import { uploadLogBundle } from "../utils/feedback-log-bundle.js";
|
|
9
9
|
export const BODY_MAX_BYTES = 64 * 1024;
|
|
10
|
+
/** Validate fields that must be valid before feedback attachments are uploaded. */
|
|
11
|
+
export function validateFeedbackContent(title, body) {
|
|
12
|
+
if (title.trim().length === 0) {
|
|
13
|
+
throw new Error("title must not be empty. Provide a short, non-whitespace title via --title.");
|
|
14
|
+
}
|
|
15
|
+
if (body.trim().length === 0) {
|
|
16
|
+
throw new Error("body must not be empty. Provide at least one non-whitespace character.");
|
|
17
|
+
}
|
|
18
|
+
const bodyBytes = Buffer.byteLength(body, "utf8");
|
|
19
|
+
if (bodyBytes > BODY_MAX_BYTES) {
|
|
20
|
+
throw new Error(`Body exceeds 64 KiB limit (${bodyBytes} bytes). Reduce the body size before submitting.`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
10
23
|
/**
|
|
11
24
|
* Mirror of the server's `MAX_BODY_BYTES` in hq-pro
|
|
12
25
|
* `src/vault-service/handlers/feedback.ts`, which rejects the WHOLE request
|
|
@@ -16,12 +29,36 @@ export const BODY_MAX_BYTES = 64 * 1024;
|
|
|
16
29
|
* logs are budgeted against whatever headroom is left between them.
|
|
17
30
|
*/
|
|
18
31
|
export const REQUEST_MAX_BYTES = 64 * 1024;
|
|
32
|
+
/** Reject JSON envelopes that hq-pro would refuse with HTTP 413. */
|
|
33
|
+
function validateFeedbackRequestSize(requestBody) {
|
|
34
|
+
const requestBytes = Buffer.byteLength(JSON.stringify(requestBody), "utf8");
|
|
35
|
+
if (requestBytes > REQUEST_MAX_BYTES) {
|
|
36
|
+
throw new Error(`Request exceeds 64 KiB limit (${requestBytes} bytes). Reduce the feedback content before submitting.`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
19
39
|
/**
|
|
20
40
|
* Headroom held back for the `"logs":{...}` JSON envelope itself — its keys,
|
|
21
41
|
* braces, and the escaping of the tail text. The final size is re-measured
|
|
22
42
|
* after attaching, so this reserve only needs to be roughly right.
|
|
23
43
|
*/
|
|
24
44
|
const LOGS_ENVELOPE_RESERVE_BYTES = 1024;
|
|
45
|
+
function buildFeedbackRequestBody(opts) {
|
|
46
|
+
const requestBody = {
|
|
47
|
+
type: opts.type,
|
|
48
|
+
title: opts.title,
|
|
49
|
+
body: opts.body,
|
|
50
|
+
diagnostics: opts.diagnostics,
|
|
51
|
+
};
|
|
52
|
+
if (opts.company) {
|
|
53
|
+
requestBody.company = opts.company;
|
|
54
|
+
}
|
|
55
|
+
if (opts.screenshots && opts.screenshots.length > 0) {
|
|
56
|
+
requestBody.screenshots = opts.screenshots;
|
|
57
|
+
}
|
|
58
|
+
if (opts.logBundle)
|
|
59
|
+
requestBody.logBundle = opts.logBundle;
|
|
60
|
+
return requestBody;
|
|
61
|
+
}
|
|
25
62
|
export async function readBodyFile(bodyFile, stdin) {
|
|
26
63
|
if (bodyFile === "-") {
|
|
27
64
|
const stream = stdin ?? process.stdin;
|
|
@@ -79,48 +116,32 @@ export function attachDebugLogs(requestBody, diagnostics, enabled, deps = {}) {
|
|
|
79
116
|
}
|
|
80
117
|
}
|
|
81
118
|
export async function submitFeedback(opts) {
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
// warning (HQ-AB). Catch it here so the caller gets a clear, actionable error
|
|
88
|
-
// and the bad request never reaches the server.
|
|
89
|
-
if (opts.title.trim().length === 0) {
|
|
90
|
-
throw new Error("title must not be empty. Provide a short, non-whitespace title via --title.");
|
|
91
|
-
}
|
|
92
|
-
if (opts.body.trim().length === 0) {
|
|
93
|
-
throw new Error("body must not be empty. Provide at least one non-whitespace character.");
|
|
94
|
-
}
|
|
95
|
-
const bodyBytes = Buffer.byteLength(opts.body, "utf8");
|
|
96
|
-
if (bodyBytes > BODY_MAX_BYTES) {
|
|
97
|
-
throw new Error(`Body exceeds 64 KiB limit (${bodyBytes} bytes). Reduce the body size before submitting.`);
|
|
98
|
-
}
|
|
99
|
-
const diagnostics = collectDiagnostics();
|
|
100
|
-
const requestBody = {
|
|
119
|
+
// Keep submitFeedback safe for direct callers as well as the command path,
|
|
120
|
+
// which invokes the same validator before uploading any attachments.
|
|
121
|
+
validateFeedbackContent(opts.title, opts.body);
|
|
122
|
+
const diagnostics = opts.diagnostics ?? collectDiagnostics();
|
|
123
|
+
const requestBody = buildFeedbackRequestBody({
|
|
101
124
|
type: opts.type,
|
|
102
125
|
title: opts.title,
|
|
103
126
|
body: opts.body,
|
|
127
|
+
company: opts.company,
|
|
104
128
|
diagnostics,
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
// The bundle reference is four small fields, so unlike the inline logs below
|
|
113
|
-
// it needs no budgeting — but it is added BEFORE attachDebugLogs so those
|
|
114
|
-
// bytes are counted against the inline budget rather than overrunning it.
|
|
115
|
-
if (opts.logBundle) {
|
|
116
|
-
requestBody.logBundle = opts.logBundle;
|
|
117
|
-
}
|
|
129
|
+
screenshots: opts.screenshots,
|
|
130
|
+
logBundle: opts.logBundle,
|
|
131
|
+
});
|
|
132
|
+
// Direct callers can supply attachment references; ensure the complete base
|
|
133
|
+
// envelope still fits before adding optional inline logs or making a request.
|
|
134
|
+
validateFeedbackRequestSize(requestBody);
|
|
118
135
|
// Debug logs are attached LAST and sized against the headroom the rest of
|
|
119
136
|
// the request left behind. The server 413s the whole body over 64 KiB, so a
|
|
120
137
|
// fixed log budget would turn a previously-working large-body submission
|
|
121
138
|
// into a hard failure. Measuring the real serialized envelope — the exact
|
|
122
139
|
// bytes vaultApiFetch will send — is what makes that impossible.
|
|
123
|
-
attachDebugLogs(requestBody, diagnostics, opts.includeLogs !== false
|
|
140
|
+
attachDebugLogs(requestBody, diagnostics, opts.includeLogs !== false, {
|
|
141
|
+
envEnabled: opts.logsEnabled === undefined
|
|
142
|
+
? feedbackLogsEnabled
|
|
143
|
+
: () => opts.logsEnabled === true,
|
|
144
|
+
});
|
|
124
145
|
const res = await vaultApiFetch({
|
|
125
146
|
token: opts.token,
|
|
126
147
|
path: "/v1/feedback",
|
|
@@ -155,8 +176,34 @@ function registerSubcommand(feedbackCmd, type) {
|
|
|
155
176
|
try {
|
|
156
177
|
const token = await ensureCognitoToken({ interactive: false });
|
|
157
178
|
const body = await readBodyFile(opts.bodyFile);
|
|
158
|
-
|
|
159
|
-
//
|
|
179
|
+
validateFeedbackContent(opts.title, body);
|
|
180
|
+
// Resolve every required local input before creating S3 objects. These
|
|
181
|
+
// diagnostics are passed through so submitFeedback does not collect
|
|
182
|
+
// them again after attachments have been uploaded.
|
|
183
|
+
const diagnostics = collectDiagnostics();
|
|
184
|
+
const logsEnabled = opts.logs !== false && feedbackLogsEnabled();
|
|
185
|
+
// Validate the serialized server request envelope before screenshots
|
|
186
|
+
// or the log bundle can create remote objects. Attachment references
|
|
187
|
+
// are added and checked again inside submitFeedback after upload.
|
|
188
|
+
validateFeedbackRequestSize(buildFeedbackRequestBody({
|
|
189
|
+
type,
|
|
190
|
+
title: opts.title,
|
|
191
|
+
body,
|
|
192
|
+
company: opts.company,
|
|
193
|
+
diagnostics,
|
|
194
|
+
}));
|
|
195
|
+
// vaultApiFetch can reject an hqk_ token or an invalid API base URL
|
|
196
|
+
// locally before fetch. Check the mandatory POST, and the screenshot
|
|
197
|
+
// presign when requested, before any upload can create an orphan.
|
|
198
|
+
assertVaultApiRequestSupported({ token, path: "/v1/feedback" });
|
|
199
|
+
if ((opts.screenshot ?? []).length > 0) {
|
|
200
|
+
assertVaultApiRequestSupported({
|
|
201
|
+
token,
|
|
202
|
+
path: "/v1/feedback/screenshots/presign",
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
// Upload screenshots (direct-to-S3 via presigned PUT) before the
|
|
206
|
+
// submission, so the row references uploaded objects.
|
|
160
207
|
const screenshots = await uploadScreenshots({
|
|
161
208
|
paths: opts.screenshot ?? [],
|
|
162
209
|
token,
|
|
@@ -167,7 +214,7 @@ function registerSubcommand(feedbackCmd, type) {
|
|
|
167
214
|
// this resolves undefined and the report still carries inline logs.
|
|
168
215
|
const logBundle = await uploadLogBundle({
|
|
169
216
|
token,
|
|
170
|
-
enabled:
|
|
217
|
+
enabled: logsEnabled,
|
|
171
218
|
});
|
|
172
219
|
const result = await submitFeedback({
|
|
173
220
|
type,
|
|
@@ -178,6 +225,8 @@ function registerSubcommand(feedbackCmd, type) {
|
|
|
178
225
|
screenshots,
|
|
179
226
|
includeLogs: opts.logs !== false,
|
|
180
227
|
logBundle,
|
|
228
|
+
diagnostics,
|
|
229
|
+
logsEnabled,
|
|
181
230
|
});
|
|
182
231
|
console.log(`Submitted: ${result.id}`);
|
|
183
232
|
}
|