@indigoai-us/hq-cli 5.345.191 → 5.345.194
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 +431 -0
- package/assets/bot-workers/setup/worker.yaml +91 -34
- package/dist/command-catalog.generated.d.ts +145 -19
- package/dist/command-catalog.generated.js +184 -21
- package/dist/commands/bot-jobs.d.ts +21 -18
- package/dist/commands/bot-jobs.js +127 -39
- package/dist/commands/bot.d.ts +4 -3
- package/dist/commands/bot.js +265 -140
- package/dist/commands/core-jobs-schedule.js +10 -0
- package/dist/commands/daemon.js +37 -0
- package/dist/commands/files.d.ts +41 -6
- package/dist/commands/files.js +108 -27
- package/dist/commands/pack-start-bot.d.ts +4 -6
- package/dist/commands/pack-start-bot.js +4 -8
- package/dist/commands/signals-run.d.ts +46 -0
- package/dist/commands/signals-run.js +180 -0
- package/dist/commands/signals.d.ts +1 -0
- package/dist/commands/signals.js +3 -0
- package/dist/commands/sources.d.ts +58 -0
- package/dist/commands/sources.js +294 -26
- package/dist/commands/staff/reset-test-account.d.ts +10 -0
- package/dist/commands/staff/reset-test-account.js +18 -2
- package/dist/commands/whoami.d.ts +7 -0
- package/dist/commands/whoami.js +35 -2
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +31171 -52512
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/bot/api.d.ts +3 -2
- package/dist/lib/bot/config.d.ts +2 -0
- package/dist/lib/bot/fleet-cron.d.ts +121 -0
- package/dist/lib/bot/fleet-cron.js +319 -0
- package/dist/lib/bot/fleet-local.d.ts +93 -1
- package/dist/lib/bot/fleet-local.js +546 -34
- package/dist/lib/bot/fleet-owner-companies.d.ts +12 -0
- package/dist/lib/bot/fleet-owner-companies.js +30 -0
- package/dist/lib/bot/fleet-watch.d.ts +25 -0
- package/dist/lib/bot/fleet-watch.js +51 -0
- package/dist/lib/bot/index.d.ts +1 -1
- package/dist/lib/bot/index.js +1 -1
- package/dist/lib/bot/job-run.d.ts +5 -4
- package/dist/lib/bot/job-run.js +5 -4
- package/dist/lib/bot/jobs/hot-signal-sync.d.ts +57 -3
- package/dist/lib/bot/jobs/hot-signal-sync.js +163 -4
- package/dist/lib/bot/local-jobs.d.ts +13 -2
- package/dist/lib/bot/local-jobs.js +17 -4
- package/dist/lib/bot/progress.d.ts +14 -0
- package/dist/lib/bot/progress.js +47 -1
- package/dist/lib/bot/run.d.ts +55 -2
- package/dist/lib/bot/run.js +192 -10
- package/dist/lib/bot/setup-tool-offer.d.ts +1 -0
- package/dist/lib/bot/setup-tool-offer.js +16 -0
- package/dist/lib/bot/signals-engine-flag.d.ts +18 -0
- package/dist/lib/bot/signals-engine-flag.js +30 -0
- package/dist/lib/daemon/catalog.d.ts +4 -0
- package/dist/lib/daemon/catalog.js +29 -9
- package/dist/lib/daemon/legacy.d.ts +6 -0
- package/dist/lib/daemon/legacy.js +16 -10
- package/dist/lib/daemon/run.d.ts +1 -0
- package/dist/lib/daemon/run.js +10 -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 +13 -1
- package/dist/lib/flag-registry.js +46 -0
- package/dist/lib/hot-channels/adapters/attio.d.ts +75 -2
- package/dist/lib/hot-channels/adapters/attio.js +367 -29
- package/dist/lib/hot-channels/adapters/gmail.d.ts +14 -0
- package/dist/lib/hot-channels/adapters/gmail.js +59 -2
- package/dist/lib/hot-channels/adapters/linear.d.ts +24 -0
- package/dist/lib/hot-channels/adapters/linear.js +71 -1
- package/dist/lib/hot-channels/adapters/meetings.js +4 -1
- package/dist/lib/hot-channels/adapters/slack.js +7 -3
- package/dist/lib/hot-channels/alert.js +1 -1
- package/dist/lib/hot-channels/contract.d.ts +11 -0
- package/dist/lib/hot-channels/contract.js +1 -0
- package/dist/lib/hot-channels/cursors.js +1 -1
- package/dist/lib/hot-channels/landing.d.ts +6 -2
- package/dist/lib/hot-channels/landing.js +13 -6
- package/dist/lib/hot-channels/ledger.js +2 -0
- package/dist/lib/hot-channels/loop.d.ts +6 -0
- package/dist/lib/hot-channels/loop.js +19 -4
- package/dist/lib/hot-channels/registry.js +9 -1
- package/dist/lib/hot-channels/run.d.ts +18 -0
- package/dist/lib/hot-channels/run.js +15 -4
- package/dist/lib/hot-channels/signals-pass.d.ts +34 -2
- package/dist/lib/hot-channels/signals-pass.js +149 -38
- package/dist/lib/hot-channels/signals-redo.d.ts +60 -0
- package/dist/lib/hot-channels/signals-redo.js +207 -0
- package/dist/lib/hot-channels/sync-lock.js +22 -6
- package/dist/lib/mesh/live/daemon/install.d.ts +22 -0
- package/dist/lib/mesh/live/daemon/install.js +117 -30
- package/dist/lib/signals/adapters-catalog.d.ts +77 -0
- package/dist/lib/signals/adapters-catalog.js +194 -0
- package/dist/lib/signals/engine.d.ts +202 -0
- package/dist/lib/signals/engine.js +504 -0
- package/dist/lib/signals/failure-alert.d.ts +89 -0
- package/dist/lib/signals/failure-alert.js +175 -0
- package/dist/lib/signals/local-date.d.ts +3 -0
- package/dist/lib/signals/local-date.js +6 -0
- package/dist/lib/signals/projection.d.ts +42 -0
- package/dist/lib/signals/projection.js +67 -0
- package/dist/lib/signals/reply.d.ts +112 -0
- package/dist/lib/signals/reply.js +368 -0
- package/dist/lib/signals/run-lock.d.ts +36 -0
- package/dist/lib/signals/run-lock.js +148 -0
- package/dist/lib/signals/run-record.d.ts +24 -0
- package/dist/lib/signals/run-record.js +82 -0
- package/dist/lib/signals/sanitize.d.ts +105 -0
- package/dist/lib/signals/sanitize.js +439 -0
- package/dist/lib/signals/settings.d.ts +10 -0
- package/dist/lib/signals/settings.js +48 -0
- package/dist/lib/signals/steps/adapters.d.ts +14 -0
- package/dist/lib/signals/steps/adapters.js +156 -0
- package/dist/lib/signals/steps/entity-similarity.d.ts +10 -0
- package/dist/lib/signals/steps/entity-similarity.js +47 -0
- package/dist/lib/signals/steps/extract.d.ts +118 -0
- package/dist/lib/signals/steps/extract.js +793 -0
- package/dist/lib/signals/steps/garden.d.ts +24 -0
- package/dist/lib/signals/steps/garden.js +250 -0
- package/dist/lib/signals/steps/types.d.ts +117 -0
- package/dist/lib/signals/steps/types.js +4 -0
- package/dist/lib/signals/steps/upload.d.ts +43 -0
- package/dist/lib/signals/steps/upload.js +324 -0
- package/dist/lib/signals/write-guard.d.ts +63 -0
- package/dist/lib/signals/write-guard.js +287 -0
- package/dist/utils/resident-package-preload.js +16 -0
- package/dist/utils/vault-api.d.ts +1 -0
- package/dist/utils/vault-api.js +1 -1
- package/package.json +9 -9
- package/dist/lib/bot/fleet-default-flag.d.ts +0 -31
- package/dist/lib/bot/fleet-default-flag.js +0 -127
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
import { Command } from "commander";
|
|
24
24
|
import { type RunLedgerAdapterSummary, type RunLedgerEntry, type RunTrigger, type SignalsGeneratedFn } from "../lib/hot-channels/ledger.js";
|
|
25
25
|
import type { HotChannelRegistry } from "../lib/hot-channels/registry.js";
|
|
26
|
+
import { type AdapterFlagReader } from "../lib/signals/adapters-catalog.js";
|
|
26
27
|
import { type SelectionLoader } from "../lib/hot-channels/selection.js";
|
|
28
|
+
import { type FleetCronOps } from "../lib/bot/fleet-cron.js";
|
|
27
29
|
import { type ScheduleReport } from "../lib/hot-channels/schedule.js";
|
|
28
30
|
import { type OnDemandSyncDeps, type OnDemandSyncReport } from "../lib/hot-channels/run.js";
|
|
29
31
|
type ListFormat = "table" | "json";
|
|
@@ -74,6 +76,13 @@ export interface CompanyHotChannelSyncInput {
|
|
|
74
76
|
hqRoot: string;
|
|
75
77
|
company: string;
|
|
76
78
|
adapter?: string;
|
|
79
|
+
/** Registered adapter ids allowed to run; see OnDemandSyncInput.onlyAdapters. */
|
|
80
|
+
onlyAdapters?: readonly string[];
|
|
81
|
+
/**
|
|
82
|
+
* Company flag reader for the adapter gate. Defaults to the shared hq-flags
|
|
83
|
+
* evaluator for this company with the signed-in session.
|
|
84
|
+
*/
|
|
85
|
+
isFlagOn?: AdapterFlagReader;
|
|
77
86
|
registry?: HotChannelRegistry;
|
|
78
87
|
now?: () => Date;
|
|
79
88
|
/** `scheduled` for the bot's hot-signal-sync job; `on-demand` by default. */
|
|
@@ -119,6 +128,53 @@ export declare function runCompanyHotChannelSync(input: CompanyHotChannelSyncInp
|
|
|
119
128
|
* line under `companies/{company}/sources/`, and nothing else.
|
|
120
129
|
*/
|
|
121
130
|
export declare function runSync(options: SyncOptions, overrides?: SyncOverrides): Promise<OnDemandSyncReport>;
|
|
131
|
+
export interface SignalsRedoOptions {
|
|
132
|
+
company?: string;
|
|
133
|
+
since?: string;
|
|
134
|
+
key?: string[];
|
|
135
|
+
channel?: string;
|
|
136
|
+
maxDocs?: number;
|
|
137
|
+
dryRun?: boolean;
|
|
138
|
+
json?: boolean;
|
|
139
|
+
hqRoot: string;
|
|
140
|
+
}
|
|
141
|
+
export interface SignalsRedoOverrides {
|
|
142
|
+
now?: () => Date;
|
|
143
|
+
/** The signed-in person; defaults to the local Cognito session. */
|
|
144
|
+
author?: {
|
|
145
|
+
email: string;
|
|
146
|
+
} | null;
|
|
147
|
+
/** The signals pass; defaults to the local `claude -p` pass with `maxDocs`. */
|
|
148
|
+
createPass?: (maxDocs: number) => SignalsGeneratedFn;
|
|
149
|
+
worklistScript?: string;
|
|
150
|
+
}
|
|
151
|
+
export interface SignalsRedoReport {
|
|
152
|
+
company: string;
|
|
153
|
+
dryRun: boolean;
|
|
154
|
+
/** Keys queued per channel, by how they were queued. */
|
|
155
|
+
queued: {
|
|
156
|
+
pending: Record<string, string[]>;
|
|
157
|
+
worklist: Record<string, string[]>;
|
|
158
|
+
};
|
|
159
|
+
signalsWritten: number;
|
|
160
|
+
/** Keys still waiting in each channel's pending file after the run. */
|
|
161
|
+
pendingLeft: Record<string, number>;
|
|
162
|
+
notices: string[];
|
|
163
|
+
error: string | null;
|
|
164
|
+
exitCode: number;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* `hq sources signals`: queue landed documents for the signals pass and run
|
|
168
|
+
* the pass now, under the same per-person lock as `hq sources sync`. The pass
|
|
169
|
+
* keeps its cap (`--max-docs`, default 20); what the cap leaves stays queued
|
|
170
|
+
* and the next run (this command or the daily sync) picks it up.
|
|
171
|
+
*/
|
|
172
|
+
export declare function runSignalsRedo(options: SignalsRedoOptions, overrides?: SignalsRedoOverrides): Promise<SignalsRedoReport>;
|
|
173
|
+
/** A usage or setup failure `runSignalsRedo` already printed; the exit code is set. */
|
|
174
|
+
export declare class SignalsRedoExit extends Error {
|
|
175
|
+
readonly exitCode: number;
|
|
176
|
+
constructor(message: string, exitCode: number);
|
|
177
|
+
}
|
|
122
178
|
export interface ScheduleOptions {
|
|
123
179
|
company?: string;
|
|
124
180
|
at?: string;
|
|
@@ -131,6 +187,8 @@ export interface ScheduleOverrides {
|
|
|
131
187
|
botsRoot?: string;
|
|
132
188
|
/** Whose ledger to read; defaults to the signed-in person. */
|
|
133
189
|
email?: string;
|
|
190
|
+
/** The HQ Fleet Runtime schedule of one bot; defaults to the bot's own runtime. */
|
|
191
|
+
fleetOps?: (agentUid: string) => FleetCronOps;
|
|
134
192
|
}
|
|
135
193
|
export declare class NoHotSignalSyncJobError extends Error {
|
|
136
194
|
readonly company: string;
|
package/dist/commands/sources.js
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
import { randomUUID } from "node:crypto";
|
|
24
24
|
import * as fs from "node:fs";
|
|
25
25
|
import * as path from "node:path";
|
|
26
|
+
import { InvalidArgumentError } from "commander";
|
|
26
27
|
import chalk from "chalk";
|
|
27
28
|
import { SOURCE_CHANNELS, assertSourceChannel, resolveEntity, listAvailableEntities, listSources, getSource, VaultClient, } from "@indigoai-us/hq-cloud";
|
|
28
29
|
import { ensureCognitoToken, ensureCognitoIdToken, buildVaultConfig, DEFAULT_COGNITO, DEFAULT_HQ_ROOT, } from "../utils/cognito-session.js";
|
|
@@ -34,16 +35,20 @@ import { resolveUploadAuthor } from "../utils/upload-author.js";
|
|
|
34
35
|
import { createSignalsAlertChannelPoster, raiseSignalsFailureAlerts } from "../lib/hot-channels/alert.js";
|
|
35
36
|
import { audienceKey } from "../lib/hot-channels/audience.js";
|
|
36
37
|
import { appendRunLedgerEntry, ledgerDay, readRunLedger, summarizeRunLedger, } from "../lib/hot-channels/ledger.js";
|
|
37
|
-
import { createClaudeSignalsPass } from "../lib/hot-channels/signals-pass.js";
|
|
38
|
+
import { createClaudeSignalsPass, DEFAULT_MAX_SIGNAL_DOCS, readPendingSignals, SignalsPassError, } from "../lib/hot-channels/signals-pass.js";
|
|
39
|
+
import { backlogRunResult, parseSince, queueSignalsRedo, selectSignalsRedo, SignalsRedoUsageError, } from "../lib/hot-channels/signals-redo.js";
|
|
38
40
|
import { assertSafeSegment } from "../lib/hot-channels/landing.js";
|
|
39
41
|
import { acquireSyncLock, SYNC_LOCK_BUSY_REASON } from "../lib/hot-channels/sync-lock.js";
|
|
40
42
|
import { createBuiltinHotChannelRegistry } from "../lib/hot-channels/adapters/index.js";
|
|
43
|
+
import { companyAdapterFlagReader, gateAdapters, readSignalsAdaptersCatalog, } from "../lib/signals/adapters-catalog.js";
|
|
41
44
|
import { createSelectionLoader } from "../lib/hot-channels/selection.js";
|
|
42
45
|
import { createPersonVaultReader } from "../lib/hot-channels/person-vault-reader.js";
|
|
43
46
|
import { S3Client } from "@aws-sdk/client-s3";
|
|
44
47
|
import { resolveCanonicalPersonUid } from "./cloud.js";
|
|
45
48
|
import { botsRoot as defaultBotsRoot } from "../lib/bot/paths.js";
|
|
46
49
|
import { ensureHotSignalSyncJob } from "../lib/bot/jobs/hot-signal-sync.js";
|
|
50
|
+
import { companyOfHotSignalSyncJob, fleetCronOps } from "../lib/bot/fleet-cron.js";
|
|
51
|
+
import { HQ_FLEET_RUNTIME_ENGINE_ID, readBotConfig } from "../lib/bot/config.js";
|
|
47
52
|
import { findHotSignalSyncJobs, lastRunsPerAdapter, parseDailyAt, renderScheduleReport, rescheduleDaily, toJobView, } from "../lib/hot-channels/schedule.js";
|
|
48
53
|
import { listActiveMembers } from "./members.js";
|
|
49
54
|
import { runOnDemandSync, } from "../lib/hot-channels/run.js";
|
|
@@ -485,32 +490,78 @@ async function skippedReport(input, vaultRoot, personKey, personUid, now) {
|
|
|
485
490
|
runId: line.runId,
|
|
486
491
|
signInRequired: false,
|
|
487
492
|
skipped: true,
|
|
493
|
+
personKey,
|
|
488
494
|
outcomes: [],
|
|
489
495
|
entries: [line],
|
|
490
496
|
notices: [`Another hot-channel sync for ${input.company} is already running, so this one was skipped.`],
|
|
491
497
|
exitCode: 0,
|
|
492
498
|
};
|
|
493
499
|
}
|
|
500
|
+
/**
|
|
501
|
+
* One `gated` outcome per adapter the local gate turned off (yaml or company
|
|
502
|
+
* flag). With `--adapter`, only that adapter's. Nothing is fetched for them.
|
|
503
|
+
*/
|
|
504
|
+
function gatedAdapterOutcomes(registry, gating, wanted) {
|
|
505
|
+
const want = wanted?.toLowerCase();
|
|
506
|
+
const out = [];
|
|
507
|
+
for (const adapter of registry.list()) {
|
|
508
|
+
if (!(adapter.id in gating.skips))
|
|
509
|
+
continue;
|
|
510
|
+
const provider = adapter.tokenScope.provider || adapter.id;
|
|
511
|
+
if (want !== undefined && adapter.id !== want && provider.toLowerCase() !== want)
|
|
512
|
+
continue;
|
|
513
|
+
const flag = gating.offFlags[adapter.id];
|
|
514
|
+
out.push({
|
|
515
|
+
adapter: adapter.id,
|
|
516
|
+
provider,
|
|
517
|
+
connectionId: null,
|
|
518
|
+
appName: null,
|
|
519
|
+
status: "gated",
|
|
520
|
+
landed: 0,
|
|
521
|
+
duplicates: 0,
|
|
522
|
+
message: flag
|
|
523
|
+
? `${adapter.id} is not enabled for this company (flag ${flag} is off)`
|
|
524
|
+
: `${adapter.id} is turned off in core/settings/signals-adapters.yaml`,
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
return out;
|
|
528
|
+
}
|
|
494
529
|
async function runLockedCompanySync(input, vaultRoot, personKey, author, session, lockHeld) {
|
|
495
530
|
const deps = gatewaySyncDeps(session.token, session.companyUid);
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
531
|
+
const registry = input.registry ??
|
|
532
|
+
createBuiltinHotChannelRegistry({ vaultRoot, identities: await meetingIdentities(input, session, author.email) });
|
|
533
|
+
// The adapter gate for every caller: signals-adapters.yaml plus the company
|
|
534
|
+
// flags, read through hq-flags.
|
|
535
|
+
const gating = await gateAdapters(registry.list().map((a) => a.id), readSignalsAdaptersCatalog(input.hqRoot), input.isFlagOn ??
|
|
536
|
+
companyAdapterFlagReader(session.companyUid, session.token, { hqRoot: input.hqRoot, company: input.company }));
|
|
537
|
+
const gated = gatedAdapterOutcomes(registry, gating, input.adapter);
|
|
538
|
+
const onlyAdapters = (input.onlyAdapters ?? gating.enabled).filter((id) => gating.enabled.includes(id));
|
|
539
|
+
let report;
|
|
540
|
+
if (input.adapter !== undefined && gated.length > 0) {
|
|
541
|
+
// `--adapter` named an adapter the gate turned off: there is nothing to run.
|
|
542
|
+
report = { runId: null, signInRequired: false, personKey, outcomes: [], entries: [], notices: [], exitCode: 0 };
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
// A fresh loader per run: selections are cached for this run only.
|
|
546
|
+
const selections = input.createSelections ? input.createSelections() : defaultSelections();
|
|
547
|
+
report = await runOnDemandSync({
|
|
548
|
+
...(selections ? { selections } : {}),
|
|
549
|
+
deps,
|
|
550
|
+
registry,
|
|
551
|
+
vaultRoot,
|
|
552
|
+
personUid: author.userSub,
|
|
553
|
+
personKey,
|
|
554
|
+
...(input.adapter !== undefined ? { adapter: input.adapter } : {}),
|
|
555
|
+
...(input.onlyAdapters !== undefined || Object.keys(gating.skips).length > 0 ? { onlyAdapters } : {}),
|
|
556
|
+
...(input.now ? { now: input.now } : {}),
|
|
557
|
+
...(input.trigger ? { trigger: input.trigger } : {}),
|
|
558
|
+
...(input.catchUp === undefined ? {} : { catchUp: input.catchUp }),
|
|
559
|
+
...(input.firstRun ? { firstRun: true } : {}),
|
|
560
|
+
...(input.signalsGenerated ? { signalsGenerated: input.signalsGenerated } : {}),
|
|
561
|
+
beforeWrite: lockHeld,
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
report.outcomes.push(...gated);
|
|
514
565
|
if (input.signalsAlert) {
|
|
515
566
|
const postCompanyChannel = input.signalsAlert.postCompanyChannel ??
|
|
516
567
|
createSignalsAlertChannelPoster({ hqRoot: input.hqRoot, company: input.company, deps });
|
|
@@ -593,6 +644,188 @@ function renderSyncTable(company, report) {
|
|
|
593
644
|
}
|
|
594
645
|
return lines.join("\n");
|
|
595
646
|
}
|
|
647
|
+
/**
|
|
648
|
+
* `hq sources signals`: queue landed documents for the signals pass and run
|
|
649
|
+
* the pass now, under the same per-person lock as `hq sources sync`. The pass
|
|
650
|
+
* keeps its cap (`--max-docs`, default 20); what the cap leaves stays queued
|
|
651
|
+
* and the next run (this command or the daily sync) picks it up.
|
|
652
|
+
*/
|
|
653
|
+
export async function runSignalsRedo(options, overrides = {}) {
|
|
654
|
+
const company = options.company ?? readActiveCompanySlug(options.hqRoot);
|
|
655
|
+
const fail = (message, exitCode) => {
|
|
656
|
+
process.stderr.write(chalk.red(`✗ ${message}\n`));
|
|
657
|
+
process.exitCode = exitCode;
|
|
658
|
+
throw new SignalsRedoExit(message, exitCode);
|
|
659
|
+
};
|
|
660
|
+
if (!company)
|
|
661
|
+
return fail("No company specified. Pass --company <slug> or set activeCompany in .hq/config.json.", 2);
|
|
662
|
+
assertSafeSegment(company, "company");
|
|
663
|
+
if (options.since === undefined && (options.key ?? []).length === 0)
|
|
664
|
+
return fail("Pass --since <date> or --key <sources/channel/id.md>.", 2);
|
|
665
|
+
const vaultRoot = path.join(options.hqRoot, "companies", company);
|
|
666
|
+
if (!fs.existsSync(vaultRoot))
|
|
667
|
+
return fail(`No company directory: companies/${company}`, 2);
|
|
668
|
+
const maxDocs = options.maxDocs ?? DEFAULT_MAX_SIGNAL_DOCS;
|
|
669
|
+
let selection;
|
|
670
|
+
try {
|
|
671
|
+
selection = await selectSignalsRedo({
|
|
672
|
+
vaultRoot,
|
|
673
|
+
...(options.key ? { keys: options.key } : {}),
|
|
674
|
+
...(options.since !== undefined ? { since: parseSince(options.since) } : {}),
|
|
675
|
+
...(options.channel !== undefined ? { channel: options.channel } : {}),
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
catch (err) {
|
|
679
|
+
if (err instanceof SignalsRedoUsageError)
|
|
680
|
+
return fail(err.message, 2);
|
|
681
|
+
throw err;
|
|
682
|
+
}
|
|
683
|
+
const report = {
|
|
684
|
+
company,
|
|
685
|
+
dryRun: options.dryRun === true,
|
|
686
|
+
queued: { pending: Object.fromEntries(selection.pending), worklist: Object.fromEntries(selection.worklist) },
|
|
687
|
+
signalsWritten: 0,
|
|
688
|
+
pendingLeft: {},
|
|
689
|
+
notices: [...selection.notices],
|
|
690
|
+
error: null,
|
|
691
|
+
exitCode: 0,
|
|
692
|
+
};
|
|
693
|
+
const channels = [...new Set([...selection.pending.keys(), ...selection.worklist.keys()])];
|
|
694
|
+
if (!report.dryRun && channels.length > 0) {
|
|
695
|
+
const author = overrides.author !== undefined ? overrides.author : resolveUploadAuthor();
|
|
696
|
+
if (!author)
|
|
697
|
+
return fail(`${SIGN_IN_PROMPT} (the run takes your hot-channel sync lock)`, 1);
|
|
698
|
+
const lock = await acquireSyncLock(vaultRoot, audienceKey([author.email]), overrides.now ? { now: overrides.now } : {});
|
|
699
|
+
if (!lock)
|
|
700
|
+
return fail(`Another hot-channel sync for ${company} is running. Try again when it finishes.`, 1);
|
|
701
|
+
try {
|
|
702
|
+
await queueSignalsRedo({
|
|
703
|
+
hqRoot: options.hqRoot,
|
|
704
|
+
company,
|
|
705
|
+
selection,
|
|
706
|
+
beforeWrite: lock.assertHeld,
|
|
707
|
+
...(overrides.worklistScript ? { worklistScript: overrides.worklistScript } : {}),
|
|
708
|
+
});
|
|
709
|
+
const pass = overrides.createPass
|
|
710
|
+
? overrides.createPass(maxDocs)
|
|
711
|
+
: createClaudeSignalsPass({ hqRoot: options.hqRoot, company, maxDocs });
|
|
712
|
+
for (const channel of channels) {
|
|
713
|
+
report.signalsWritten += await pass(backlogRunResult(channel), { beforeWrite: lock.assertHeld });
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
catch (err) {
|
|
717
|
+
report.error = err instanceof SignalsPassError ? err.code : err instanceof Error ? err.message : String(err);
|
|
718
|
+
report.exitCode = 1;
|
|
719
|
+
}
|
|
720
|
+
finally {
|
|
721
|
+
await lock.release();
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
for (const channel of selection.pending.keys()) {
|
|
725
|
+
report.pendingLeft[channel] = (await readPendingSignals(vaultRoot, channel)).length;
|
|
726
|
+
}
|
|
727
|
+
if (options.json) {
|
|
728
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
729
|
+
}
|
|
730
|
+
else {
|
|
731
|
+
process.stdout.write(`${renderSignalsRedo(report)}\n`);
|
|
732
|
+
}
|
|
733
|
+
if (report.exitCode !== 0)
|
|
734
|
+
process.exitCode = report.exitCode;
|
|
735
|
+
return report;
|
|
736
|
+
}
|
|
737
|
+
/** A usage or setup failure `runSignalsRedo` already printed; the exit code is set. */
|
|
738
|
+
export class SignalsRedoExit extends Error {
|
|
739
|
+
exitCode;
|
|
740
|
+
constructor(message, exitCode) {
|
|
741
|
+
super(message);
|
|
742
|
+
this.name = "SignalsRedoExit";
|
|
743
|
+
this.exitCode = exitCode;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
function renderSignalsRedo(report) {
|
|
747
|
+
const lines = [`${report.dryRun ? "Would queue" : "Queued"} for signals in ${report.company}:`];
|
|
748
|
+
const groups = [
|
|
749
|
+
...Object.entries(report.queued.pending).map(([channel, keys]) => ({ channel, keys, how: "pending" })),
|
|
750
|
+
...Object.entries(report.queued.worklist).map(([channel, keys]) => ({ channel, keys, how: "worklist --redo" })),
|
|
751
|
+
];
|
|
752
|
+
if (groups.length === 0)
|
|
753
|
+
lines.push(" nothing (no matching landed document without signals)");
|
|
754
|
+
for (const group of groups) {
|
|
755
|
+
lines.push(` sources/${group.channel}: ${group.keys.length} (${group.how})`);
|
|
756
|
+
for (const key of group.keys)
|
|
757
|
+
lines.push(` ${key}`);
|
|
758
|
+
}
|
|
759
|
+
lines.push(...report.notices);
|
|
760
|
+
if (!report.dryRun && groups.length > 0) {
|
|
761
|
+
lines.push("", `${report.signalsWritten} signal(s) written.`);
|
|
762
|
+
for (const [channel, left] of Object.entries(report.pendingLeft)) {
|
|
763
|
+
if (left > 0)
|
|
764
|
+
lines.push(`${left} document(s) still queued in sources/${channel}; the next run picks them up.`);
|
|
765
|
+
}
|
|
766
|
+
if (report.error)
|
|
767
|
+
lines.push(chalk.red(`The signals pass stopped: ${report.error}. Queued documents stay queued.`));
|
|
768
|
+
}
|
|
769
|
+
return lines.join("\n");
|
|
770
|
+
}
|
|
771
|
+
function collectKey(value, previous = []) {
|
|
772
|
+
return [...previous, value];
|
|
773
|
+
}
|
|
774
|
+
function parseMaxDocs(value) {
|
|
775
|
+
const n = Number(value);
|
|
776
|
+
if (!Number.isInteger(n) || n <= 0)
|
|
777
|
+
throw new InvalidArgumentError("must be a positive integer");
|
|
778
|
+
return n;
|
|
779
|
+
}
|
|
780
|
+
function fleetBotsIn(root) {
|
|
781
|
+
let names;
|
|
782
|
+
try {
|
|
783
|
+
names = fs.readdirSync(root).sort();
|
|
784
|
+
}
|
|
785
|
+
catch {
|
|
786
|
+
return [];
|
|
787
|
+
}
|
|
788
|
+
const bots = [];
|
|
789
|
+
for (const botName of names) {
|
|
790
|
+
const config = readBotConfig(path.join(root, botName));
|
|
791
|
+
if (config && config.engine?.id === HQ_FLEET_RUNTIME_ENGINE_ID)
|
|
792
|
+
bots.push({ botName, agentUid: config.agentUid });
|
|
793
|
+
}
|
|
794
|
+
return bots;
|
|
795
|
+
}
|
|
796
|
+
function findFleetHotSignalSyncJob(root, company, opsFor) {
|
|
797
|
+
for (const bot of fleetBotsIn(root)) {
|
|
798
|
+
let ops;
|
|
799
|
+
let jobs;
|
|
800
|
+
try {
|
|
801
|
+
ops = opsFor(bot.agentUid);
|
|
802
|
+
jobs = ops.list();
|
|
803
|
+
}
|
|
804
|
+
catch {
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
const job = jobs.find((candidate) => companyOfHotSignalSyncJob(candidate) === company);
|
|
808
|
+
if (job)
|
|
809
|
+
return { botName: bot.botName, ops, job };
|
|
810
|
+
}
|
|
811
|
+
return null;
|
|
812
|
+
}
|
|
813
|
+
function fleetDailyAt(expr) {
|
|
814
|
+
const match = /^(\d{1,2}) (\d{1,2}) \* \* \*$/.exec((expr ?? "").trim());
|
|
815
|
+
return match ? `${match[2].padStart(2, "0")}:${match[1].padStart(2, "0")}` : null;
|
|
816
|
+
}
|
|
817
|
+
function fleetJobView(found) {
|
|
818
|
+
const expr = found.job.schedule?.expr ?? "";
|
|
819
|
+
return {
|
|
820
|
+
botName: found.botName,
|
|
821
|
+
jobId: found.job.id,
|
|
822
|
+
cron: expr,
|
|
823
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
|
|
824
|
+
dailyAt: fleetDailyAt(expr),
|
|
825
|
+
nextRunAt: found.job.next_run_at ?? null,
|
|
826
|
+
lastRunAt: found.job.last_run_at ?? null,
|
|
827
|
+
};
|
|
828
|
+
}
|
|
596
829
|
export class NoHotSignalSyncJobError extends Error {
|
|
597
830
|
company;
|
|
598
831
|
constructor(company) {
|
|
@@ -618,7 +851,22 @@ export async function runSchedule(options, overrides = {}) {
|
|
|
618
851
|
// Validate before touching any job file.
|
|
619
852
|
if (options.at !== undefined)
|
|
620
853
|
parseDailyAt(options.at);
|
|
621
|
-
|
|
854
|
+
const botsRootDir = overrides.botsRoot ?? defaultBotsRoot();
|
|
855
|
+
const fleetFound = findFleetHotSignalSyncJob(botsRootDir, company, overrides.fleetOps ?? fleetCronOps);
|
|
856
|
+
if (fleetFound) {
|
|
857
|
+
if (options.at !== undefined) {
|
|
858
|
+
const { hour, minute } = parseDailyAt(options.at);
|
|
859
|
+
const updated = fleetFound.ops.reschedule(fleetFound.job.id, `${minute} ${hour} * * *`);
|
|
860
|
+
if (!updated)
|
|
861
|
+
throw new NoHotSignalSyncJobError(company);
|
|
862
|
+
fleetFound.job = updated;
|
|
863
|
+
}
|
|
864
|
+
return finishScheduleReport(options, overrides, company, fleetJobView(fleetFound), [], now);
|
|
865
|
+
}
|
|
866
|
+
if (options.at !== undefined && fleetBotsIn(botsRootDir).some((bot) => bot.botName === "setup")) {
|
|
867
|
+
throw new Error(`The setup bot runs on the HQ Fleet Runtime and has no hot-signal-sync job for ${company} yet. Start it once with \`hq bot start setup\` so it adds the job, then set the time again.`);
|
|
868
|
+
}
|
|
869
|
+
let found = findHotSignalSyncJobs(botsRootDir, company);
|
|
622
870
|
if (options.at !== undefined) {
|
|
623
871
|
if (found.length === 0) {
|
|
624
872
|
// Setting a time turns the daily sync on: register it on the personal setup bot.
|
|
@@ -638,20 +886,19 @@ export async function runSchedule(options, overrides = {}) {
|
|
|
638
886
|
throw new NoHotSignalSyncJobError(company);
|
|
639
887
|
found = rescheduleDaily(found, options.at, now);
|
|
640
888
|
}
|
|
889
|
+
return finishScheduleReport(options, overrides, company, found[0] ? toJobView(found[0], now) : null, found.slice(1).map((f) => f.botName), now);
|
|
890
|
+
}
|
|
891
|
+
async function finishScheduleReport(options, overrides, company, job, extraBots, now) {
|
|
641
892
|
const email = overrides.email ?? resolveUploadAuthor()?.email;
|
|
642
893
|
const lastRuns = email
|
|
643
894
|
? await lastRunsPerAdapter(path.join(options.hqRoot, "companies", company), audienceKey([email]), now)
|
|
644
895
|
: [];
|
|
645
|
-
const report = {
|
|
646
|
-
company,
|
|
647
|
-
job: found[0] ? toJobView(found[0], now) : null,
|
|
648
|
-
lastRuns,
|
|
649
|
-
};
|
|
896
|
+
const report = { company, job, lastRuns };
|
|
650
897
|
if (options.json) {
|
|
651
898
|
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
652
899
|
}
|
|
653
900
|
else {
|
|
654
|
-
process.stdout.write(renderScheduleReport(report,
|
|
901
|
+
process.stdout.write(renderScheduleReport(report, extraBots) + "\n");
|
|
655
902
|
if (!email)
|
|
656
903
|
process.stdout.write("Sign in (hq login) to see recent runs.\n");
|
|
657
904
|
}
|
|
@@ -730,6 +977,27 @@ export function registerSourcesCommand(program) {
|
|
|
730
977
|
.action(withErrorHandling(async (options) => {
|
|
731
978
|
await runSchedule(options);
|
|
732
979
|
}));
|
|
980
|
+
sources
|
|
981
|
+
.command("signals")
|
|
982
|
+
.description("Process landed sources for signals again: queue them for the signals pass and run it now (the pass's document cap still applies)")
|
|
983
|
+
.option("--company <slug>", "Company slug (defaults to .hq/config.json activeCompany)")
|
|
984
|
+
.option("--since <date>", "Landed documents from this date on (YYYY-MM-DD is local midnight) that have no signals yet")
|
|
985
|
+
.option("--key <key>", "A landed document to process again, signals or not (sources/<channel>/<id>.md; repeatable)", collectKey)
|
|
986
|
+
.option("--channel <channel>", "Limit --since to one channel")
|
|
987
|
+
.option("--max-docs <n>", `Documents sent to claude in this run (default ${DEFAULT_MAX_SIGNAL_DOCS})`, parseMaxDocs)
|
|
988
|
+
.option("--dry-run", "Print what would be queued; queue nothing and call no model")
|
|
989
|
+
.option("--json", "Print the report as JSON")
|
|
990
|
+
.option("--hq-root <path>", `Local HQ tree root (default: ${DEFAULT_HQ_ROOT})`, DEFAULT_HQ_ROOT)
|
|
991
|
+
.action(withErrorHandling(async (options) => {
|
|
992
|
+
try {
|
|
993
|
+
await runSignalsRedo(options);
|
|
994
|
+
}
|
|
995
|
+
catch (err) {
|
|
996
|
+
// Usage and setup failures are already printed with their exit code.
|
|
997
|
+
if (!(err instanceof SignalsRedoExit))
|
|
998
|
+
throw err;
|
|
999
|
+
}
|
|
1000
|
+
}));
|
|
733
1001
|
sources
|
|
734
1002
|
.command("sync")
|
|
735
1003
|
.description("Run every hot channel now: each adapter once per connection you can use")
|
|
@@ -24,6 +24,12 @@ interface PlanCompany {
|
|
|
24
24
|
slug: string;
|
|
25
25
|
memberCount: number;
|
|
26
26
|
}
|
|
27
|
+
interface PlanCompanyHandoff {
|
|
28
|
+
companyUid: string;
|
|
29
|
+
companySlug: string;
|
|
30
|
+
toPersonUid: string;
|
|
31
|
+
toEmail: string;
|
|
32
|
+
}
|
|
27
33
|
interface Plan {
|
|
28
34
|
target: {
|
|
29
35
|
input: string;
|
|
@@ -32,6 +38,8 @@ interface Plan {
|
|
|
32
38
|
personUid: string | null;
|
|
33
39
|
cognitoUsers: PlanCognitoUser[];
|
|
34
40
|
ownedCompanies: PlanCompany[];
|
|
41
|
+
/** Present only when the request opted in to the handoff. */
|
|
42
|
+
sharedCompanyHandoffs?: PlanCompanyHandoff[];
|
|
35
43
|
personalVault: {
|
|
36
44
|
bucketName: string | null;
|
|
37
45
|
kmsKeyAlias: string | null;
|
|
@@ -89,6 +97,8 @@ export interface RunResetTestAccountParams {
|
|
|
89
97
|
execute: boolean;
|
|
90
98
|
/** Skip the typed-confirmation prompt on --execute (for scripts). */
|
|
91
99
|
yes?: boolean;
|
|
100
|
+
/** Keep a company with real members and hand it to one, instead of refusing. */
|
|
101
|
+
handoffSharedCompanies?: boolean;
|
|
92
102
|
}
|
|
93
103
|
export interface RunResetTestAccountDeps {
|
|
94
104
|
prompt?: ConfirmFn;
|
|
@@ -76,6 +76,13 @@ export function renderPlan(plan) {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
lines.push("");
|
|
79
|
+
if (plan.sharedCompanyHandoffs && plan.sharedCompanyHandoffs.length > 0) {
|
|
80
|
+
lines.push(chalk.bold(" Companies kept and handed over"));
|
|
81
|
+
for (const h of plan.sharedCompanyHandoffs) {
|
|
82
|
+
lines.push(` handoff: ${h.companySlug} to ${h.toEmail}`);
|
|
83
|
+
}
|
|
84
|
+
lines.push("");
|
|
85
|
+
}
|
|
79
86
|
lines.push(chalk.bold(" Personal vault"));
|
|
80
87
|
lines.push(` bucket: ${plan.personalVault.bucketName ?? chalk.dim("(none)")}`);
|
|
81
88
|
lines.push(` kms alias: ${plan.personalVault.kmsKeyAlias ?? chalk.dim("(none)")}`);
|
|
@@ -166,13 +173,17 @@ export async function runResetTestAccount(params, deps = {}) {
|
|
|
166
173
|
throw new Error("reset-test-account requires a super-admin (staff) HQ session. " +
|
|
167
174
|
"Sign in with an @getindigo.ai account and retry.");
|
|
168
175
|
}
|
|
176
|
+
// Absent unless asked for, so the server's refusal stays the default.
|
|
177
|
+
const handoff = params.handoffSharedCompanies === true
|
|
178
|
+
? { handoffSharedCompanies: true }
|
|
179
|
+
: {};
|
|
169
180
|
// Dry-run first. The AC calls out "Without --execute, runs dry-run and
|
|
170
181
|
// prints the plan" — so a bare invocation always ends here.
|
|
171
182
|
const dryRunRes = await vaultApiFetch({
|
|
172
183
|
token,
|
|
173
184
|
path: "/v1/admin/test-account/reset",
|
|
174
185
|
method: "POST",
|
|
175
|
-
body: { email, dryRun: true },
|
|
186
|
+
body: { email, dryRun: true, ...handoff },
|
|
176
187
|
});
|
|
177
188
|
const dryRunBody = await readBody(dryRunRes);
|
|
178
189
|
if (!dryRunRes.ok) {
|
|
@@ -221,7 +232,7 @@ export async function runResetTestAccount(params, deps = {}) {
|
|
|
221
232
|
token,
|
|
222
233
|
path: "/v1/admin/test-account/reset",
|
|
223
234
|
method: "POST",
|
|
224
|
-
body: { email, dryRun: false, confirmEmail },
|
|
235
|
+
body: { email, dryRun: false, confirmEmail, ...handoff },
|
|
225
236
|
});
|
|
226
237
|
const executeBody = await readBody(executeRes);
|
|
227
238
|
// The handler answers 200 (all-ok) and 207 (best-effort partial). Anything
|
|
@@ -234,6 +245,9 @@ export async function runResetTestAccount(params, deps = {}) {
|
|
|
234
245
|
throw new Error("server did not return an execute result");
|
|
235
246
|
}
|
|
236
247
|
console.log(renderExecuteSummary(result));
|
|
248
|
+
for (const h of plan.sharedCompanyHandoffs ?? []) {
|
|
249
|
+
console.log(` handoff: ${h.companySlug} to ${h.toEmail}`);
|
|
250
|
+
}
|
|
237
251
|
if (!result.ok) {
|
|
238
252
|
// Non-zero exit so a scripted caller can react. The summary line above
|
|
239
253
|
// already shows which step failed.
|
|
@@ -253,12 +267,14 @@ export function registerResetTestAccountCommand(staff, deps = {}) {
|
|
|
253
267
|
.argument("<email>", "Designated test-account email (must be in the registry)")
|
|
254
268
|
.option("--execute", "Actually delete artifacts. Prompts for a typed-email confirmation.")
|
|
255
269
|
.option("-y, --yes", "Skip the typed-email confirmation prompt (still sends confirmEmail).")
|
|
270
|
+
.option("--handoff-shared-companies", "Keep a company with real members and hand it to one, instead of refusing the reset.")
|
|
256
271
|
.action(async (email, opts) => {
|
|
257
272
|
try {
|
|
258
273
|
await runResetTestAccount({
|
|
259
274
|
email,
|
|
260
275
|
execute: opts.execute === true,
|
|
261
276
|
yes: opts.yes === true,
|
|
277
|
+
handoffSharedCompanies: opts.handoffSharedCompanies === true,
|
|
262
278
|
}, deps);
|
|
263
279
|
}
|
|
264
280
|
catch (err) {
|
|
@@ -21,6 +21,13 @@ export interface WhoamiDisplayIdentity {
|
|
|
21
21
|
* email with the token subject of the delegating machine.
|
|
22
22
|
*/
|
|
23
23
|
export declare function resolveWhoamiIdentity(identity: WhoamiTokenIdentity): WhoamiDisplayIdentity;
|
|
24
|
+
/**
|
|
25
|
+
* Bound on the person-uid lookup, so `--json` callers with short timeouts still
|
|
26
|
+
* get an answer. Kept at 1s: with every endpoint stalled, a whole `whoami
|
|
27
|
+
* --json` must still finish inside the foreground network budget plus startup
|
|
28
|
+
* (foreground-network-budget.test.ts); 3s overran it.
|
|
29
|
+
*/
|
|
30
|
+
export declare const PERSON_UID_LOOKUP_TIMEOUT_MS = 1000;
|
|
24
31
|
export declare function renderWhoamiPlanBlock(status: PlanLimitStatus, opts?: {
|
|
25
32
|
inviteTeammateEnabled?: boolean;
|
|
26
33
|
}): string | null;
|
package/dist/commands/whoami.js
CHANGED
|
@@ -8,6 +8,7 @@ import { ensureCognitoToken, loadMachineCachedTokens, resolveCognitoTokenSource,
|
|
|
8
8
|
import { colorizePlanLimitNotice, fetchPlanLimitStatus, renderPlanLine, renderPlanLimitNotice, } from "../lib/billing/plan-lock.js";
|
|
9
9
|
import { resolveCompanyFlagGate, } from "../lib/flag-registry.js";
|
|
10
10
|
import { queuePlanLimitInviteTeammateExposure } from "../utils/cli-telemetry.js";
|
|
11
|
+
import { resolveCallerPersonUid } from "../utils/vault-api.js";
|
|
11
12
|
export const CLI_LIMIT_INVITE_TEAMMATE_FLAG = "cli.limit-invite-teammate-v1";
|
|
12
13
|
/**
|
|
13
14
|
* Resolves the person represented by an ID token without pairing a delegated
|
|
@@ -44,6 +45,36 @@ function peekIdToken(idToken) {
|
|
|
44
45
|
: undefined,
|
|
45
46
|
};
|
|
46
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Bound on the person-uid lookup, so `--json` callers with short timeouts still
|
|
50
|
+
* get an answer. Kept at 1s: with every endpoint stalled, a whole `whoami
|
|
51
|
+
* --json` must still finish inside the foreground network budget plus startup
|
|
52
|
+
* (foreground-network-budget.test.ts); 3s overran it.
|
|
53
|
+
*/
|
|
54
|
+
export const PERSON_UID_LOOKUP_TIMEOUT_MS = 1_000;
|
|
55
|
+
/**
|
|
56
|
+
* The signed-in person's canonical `prs_*` uid for `whoami --json`. Person ID
|
|
57
|
+
* tokens carry no `custom:entityType`/`custom:entityUid` claims, so the token
|
|
58
|
+
* alone almost never has it; the vault's person-entity lookup does (the same
|
|
59
|
+
* rule `hq secrets --personal` uses). Best-effort: no session, a timeout or a
|
|
60
|
+
* network failure resolve to null and the rest of the identity still prints.
|
|
61
|
+
*/
|
|
62
|
+
async function readPersonUid(identity) {
|
|
63
|
+
if (identity.entityType === 'person' && identity.entityUid)
|
|
64
|
+
return identity.entityUid;
|
|
65
|
+
try {
|
|
66
|
+
const token = await ensureCognitoToken({ interactive: false });
|
|
67
|
+
if (!token)
|
|
68
|
+
return null;
|
|
69
|
+
return await resolveCallerPersonUid(token, undefined, {
|
|
70
|
+
command: 'hq whoami',
|
|
71
|
+
signal: AbortSignal.timeout(PERSON_UID_LOOKUP_TIMEOUT_MS),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
47
78
|
/**
|
|
48
79
|
* starter-plan-hard-limits / US-011 — read the workspace plan-limit nag for
|
|
49
80
|
* `--company <slug>` (the bind notice).
|
|
@@ -148,9 +179,11 @@ export function registerWhoamiCommand(program) {
|
|
|
148
179
|
: boundLegacyAgent;
|
|
149
180
|
// planLimits is additive: absent-or-null means UNKNOWN.
|
|
150
181
|
const planLimits = await readPlanLimits(opts.company);
|
|
182
|
+
const authenticated = machine ? Boolean(machineCreds) : Boolean(cached && !isExpiring(cached, 0));
|
|
183
|
+
const personUid = !machine && authenticated ? await readPersonUid(identity) : null;
|
|
151
184
|
console.log(JSON.stringify({
|
|
152
185
|
schemaVersion: 1,
|
|
153
|
-
authenticated
|
|
186
|
+
authenticated,
|
|
154
187
|
tokenSource: machine ? 'machine' : 'person',
|
|
155
188
|
email: machine ? null : displayedIdentity.email ?? null,
|
|
156
189
|
// Cognito subject of the person shown in `email` (null for machine
|
|
@@ -162,7 +195,7 @@ export function registerWhoamiCommand(program) {
|
|
|
162
195
|
? identity.delegatedEmail : null,
|
|
163
196
|
delegatedSub: identity.delegatedEmail !== undefined && identity.delegatedSub !== undefined
|
|
164
197
|
? identity.delegatedSub : null,
|
|
165
|
-
personUid
|
|
198
|
+
personUid,
|
|
166
199
|
agentUid,
|
|
167
200
|
username: machineCreds?.username ?? null,
|
|
168
201
|
planLimits,
|