@indigoai-us/hq-cli 5.343.1 → 5.344.1
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 +7 -0
- package/dist/command-catalog.generated.d.ts +14 -3
- package/dist/command-catalog.generated.js +18 -3
- package/dist/commands/cloud.js +2 -2
- package/dist/commands/login.js +12 -1
- package/dist/commands/logout.js +10 -0
- package/dist/commands/mcp-serve.js +4 -3
- package/dist/commands/mesh.d.ts +27 -1
- package/dist/commands/mesh.js +123 -12
- package/dist/lib/work-context/config.d.ts +66 -0
- package/dist/lib/work-context/config.js +142 -0
- package/dist/lib/work-context/person-identity.d.ts +16 -0
- package/dist/lib/work-context/person-identity.js +39 -0
- package/dist/utils/client-health.d.ts +7 -3
- package/dist/utils/client-health.js +30 -6
- package/dist/utils/package-use-lease.d.ts +7 -2
- package/dist/utils/package-use-lease.js +39 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.344.1] — 2026-10-05
|
|
6
|
+
- Release the MCP server's package lease after its resident modules preload and record a bounded command purpose on CLI package leases.
|
|
7
|
+
|
|
8
|
+
## [5.344.0] — 2026-10-04
|
|
9
|
+
- Include a bounded reason in sync-failure client-health reports.
|
|
10
|
+
- Add an explicit opt-in to count usage from HQ sessions with no company toward the default company: `hq mesh context default set` asks in a terminal (default No), takes `--count-usage` / `--no-count-usage`, and writes `usageAttribution` (company and signed-in person) to the work-context config. Changing or clearing the default, logging out, or signing in as another account withdraws it; `get` shows it; an automatic default never opts in.
|
|
11
|
+
|
|
5
12
|
## [5.343.1] — 2026-10-04
|
|
6
13
|
|
|
7
14
|
- Include the bounded hq-cli version in `cli_feature_used` telemetry so feature-use rows can be grouped by release.
|
|
@@ -9678,7 +9678,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
9678
9678
|
readonly options: readonly [];
|
|
9679
9679
|
readonly subcommands: readonly [{
|
|
9680
9680
|
readonly name: "get";
|
|
9681
|
-
readonly description: "Show the device default company (never activeCompany)";
|
|
9681
|
+
readonly description: "Show the device default company (never activeCompany) and whether usage is counted toward it";
|
|
9682
9682
|
readonly aliases: readonly [];
|
|
9683
9683
|
readonly hidden: false;
|
|
9684
9684
|
readonly helpOption: true;
|
|
@@ -9700,7 +9700,10 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
9700
9700
|
readonly helpOption: true;
|
|
9701
9701
|
readonly helpCommand: false;
|
|
9702
9702
|
readonly usage: "[options] <slug>";
|
|
9703
|
-
readonly helpText: readonly [
|
|
9703
|
+
readonly helpText: readonly [{
|
|
9704
|
+
readonly position: "after";
|
|
9705
|
+
readonly text: "\nWith neither --count-usage nor --no-count-usage, an interactive terminal asks (default No);\notherwise an existing opt-in is kept only when it is for the same company.\nChanging to a different company always stops counting usage toward the old one.";
|
|
9706
|
+
}];
|
|
9704
9707
|
readonly arguments: readonly [{
|
|
9705
9708
|
readonly name: "slug";
|
|
9706
9709
|
readonly required: true;
|
|
@@ -9715,6 +9718,14 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
9715
9718
|
readonly flags: "--repair-held-with-default";
|
|
9716
9719
|
readonly description: "Persist opt-in for lower-confidence historical held-session repair";
|
|
9717
9720
|
readonly hidden: false;
|
|
9721
|
+
}, {
|
|
9722
|
+
readonly flags: "--count-usage";
|
|
9723
|
+
readonly description: "Count usage from HQ sessions with no company toward this company (its owner and admins can see it)";
|
|
9724
|
+
readonly hidden: false;
|
|
9725
|
+
}, {
|
|
9726
|
+
readonly flags: "--no-count-usage";
|
|
9727
|
+
readonly description: "Do not count usage from HQ sessions with no company toward this company";
|
|
9728
|
+
readonly hidden: false;
|
|
9718
9729
|
}, {
|
|
9719
9730
|
readonly flags: "--company <slug|uid>";
|
|
9720
9731
|
readonly description: "Deprecated no-op; the selected company is verified directly";
|
|
@@ -9727,7 +9738,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
9727
9738
|
readonly subcommands: readonly [];
|
|
9728
9739
|
}, {
|
|
9729
9740
|
readonly name: "clear";
|
|
9730
|
-
readonly description: "Clear the device default company";
|
|
9741
|
+
readonly description: "Clear the device default company and any opt-in to count usage toward it";
|
|
9731
9742
|
readonly aliases: readonly [];
|
|
9732
9743
|
readonly hidden: false;
|
|
9733
9744
|
readonly helpOption: true;
|
|
@@ -11737,7 +11737,7 @@ export const COMMAND_CATALOG = [
|
|
|
11737
11737
|
"subcommands": [
|
|
11738
11738
|
{
|
|
11739
11739
|
"name": "get",
|
|
11740
|
-
"description": "Show the device default company (never activeCompany)",
|
|
11740
|
+
"description": "Show the device default company (never activeCompany) and whether usage is counted toward it",
|
|
11741
11741
|
"aliases": [],
|
|
11742
11742
|
"hidden": false,
|
|
11743
11743
|
"helpOption": true,
|
|
@@ -11762,7 +11762,12 @@ export const COMMAND_CATALOG = [
|
|
|
11762
11762
|
"helpOption": true,
|
|
11763
11763
|
"helpCommand": false,
|
|
11764
11764
|
"usage": "[options] <slug>",
|
|
11765
|
-
"helpText": [
|
|
11765
|
+
"helpText": [
|
|
11766
|
+
{
|
|
11767
|
+
"position": "after",
|
|
11768
|
+
"text": "\nWith neither --count-usage nor --no-count-usage, an interactive terminal asks (default No);\notherwise an existing opt-in is kept only when it is for the same company.\nChanging to a different company always stops counting usage toward the old one."
|
|
11769
|
+
}
|
|
11770
|
+
],
|
|
11766
11771
|
"arguments": [
|
|
11767
11772
|
{
|
|
11768
11773
|
"name": "slug",
|
|
@@ -11782,6 +11787,16 @@ export const COMMAND_CATALOG = [
|
|
|
11782
11787
|
"description": "Persist opt-in for lower-confidence historical held-session repair",
|
|
11783
11788
|
"hidden": false
|
|
11784
11789
|
},
|
|
11790
|
+
{
|
|
11791
|
+
"flags": "--count-usage",
|
|
11792
|
+
"description": "Count usage from HQ sessions with no company toward this company (its owner and admins can see it)",
|
|
11793
|
+
"hidden": false
|
|
11794
|
+
},
|
|
11795
|
+
{
|
|
11796
|
+
"flags": "--no-count-usage",
|
|
11797
|
+
"description": "Do not count usage from HQ sessions with no company toward this company",
|
|
11798
|
+
"hidden": false
|
|
11799
|
+
},
|
|
11785
11800
|
{
|
|
11786
11801
|
"flags": "--company <slug|uid>",
|
|
11787
11802
|
"description": "Deprecated no-op; the selected company is verified directly",
|
|
@@ -11797,7 +11812,7 @@ export const COMMAND_CATALOG = [
|
|
|
11797
11812
|
},
|
|
11798
11813
|
{
|
|
11799
11814
|
"name": "clear",
|
|
11800
|
-
"description": "Clear the device default company",
|
|
11815
|
+
"description": "Clear the device default company and any opt-in to count usage toward it",
|
|
11801
11816
|
"aliases": [],
|
|
11802
11817
|
"hidden": false,
|
|
11803
11818
|
"helpOption": true,
|
package/dist/commands/cloud.js
CHANGED
|
@@ -20,7 +20,7 @@ import { share, sync, getStateDir, listJournals, VaultClient, computePersonalVau
|
|
|
20
20
|
import { DEFAULT_HQ_ROOT, ensureCognitoToken, buildVaultConfig, loadCachedTokens, } from "../utils/cognito-session.js";
|
|
21
21
|
import { resolveUploadAuthor } from "../utils/upload-author.js";
|
|
22
22
|
import { companyFolderExceedsThreshold, emitNarrowHint, isStrictRefusal, resolveBannerLevel, resolveNarrowHintPresentationLevel, resolveNarrowHintMinBytes, } from "../lib/narrow-hint-banner.js";
|
|
23
|
-
import { beginSyncHealthReport, } from "../utils/client-health.js";
|
|
23
|
+
import { beginSyncHealthReport, classifySyncFailure, } from "../utils/client-health.js";
|
|
24
24
|
import { companySlugFromSyncPaths, warnAboutInvalidOntologyEntities, } from "../lib/sync-invalid-entity-warning.js";
|
|
25
25
|
import { isWorkBearingSyncResult, recordFirstWorkBearingSync, } from "../lib/first-sync-next-step.js";
|
|
26
26
|
import { maybeShowCliInviteNudge } from "../lib/cli-invite-nudge.js";
|
|
@@ -125,7 +125,7 @@ export async function withSyncHealthReport(run) {
|
|
|
125
125
|
await health.succeeded();
|
|
126
126
|
}
|
|
127
127
|
else {
|
|
128
|
-
await health.failed();
|
|
128
|
+
await health.failed(classifySyncFailure(err));
|
|
129
129
|
}
|
|
130
130
|
if (err instanceof SyncExitError)
|
|
131
131
|
process.exit(err.code);
|
package/dist/commands/login.js
CHANGED
|
@@ -8,7 +8,8 @@ import { browserLogin, loadCachedTokens, isExpiring } from '@indigoai-us/hq-clou
|
|
|
8
8
|
import { cognitoConfigForLoginProvider } from '../utils/login-provider.js';
|
|
9
9
|
import * as readline from "node:readline/promises";
|
|
10
10
|
import { listActiveMembershipCompanies } from "../lib/mesh/api.js";
|
|
11
|
-
import { setDefaultCompany } from "../lib/work-context/config.js";
|
|
11
|
+
import { retainUsageAttributionForPerson, setDefaultCompany } from "../lib/work-context/config.js";
|
|
12
|
+
import { personUidFromTokens } from "../lib/work-context/person-identity.js";
|
|
12
13
|
import { ensureDefaultCompany } from "../lib/work-context/default-company.js";
|
|
13
14
|
import { workContextRoot } from "../lib/work-context/paths.js";
|
|
14
15
|
function peekIdToken(idToken) {
|
|
@@ -62,6 +63,16 @@ export function registerLoginCommand(program) {
|
|
|
62
63
|
process.exitCode = 1;
|
|
63
64
|
return;
|
|
64
65
|
}
|
|
66
|
+
try {
|
|
67
|
+
// The usage-attribution opt-in belongs to the person who gave it. A
|
|
68
|
+
// different (or unknown) account signing in on this OS user drops it.
|
|
69
|
+
retainUsageAttributionForPerson(personUidFromTokens(loadCachedTokens()), {
|
|
70
|
+
root: workContextRoot(),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
/* fail closed below: readers also require the signed-in person to match */
|
|
75
|
+
}
|
|
65
76
|
try {
|
|
66
77
|
const root = workContextRoot();
|
|
67
78
|
const activation = await ensureDefaultCompany({
|
package/dist/commands/logout.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import { clearCachedTokens, loadCachedTokens } from '@indigoai-us/hq-cloud';
|
|
6
|
+
import { retainUsageAttributionForPerson } from '../lib/work-context/config.js';
|
|
7
|
+
import { workContextRoot } from '../lib/work-context/paths.js';
|
|
6
8
|
export function registerLogoutCommand(program) {
|
|
7
9
|
program
|
|
8
10
|
.command('logout')
|
|
@@ -11,6 +13,14 @@ export function registerLogoutCommand(program) {
|
|
|
11
13
|
try {
|
|
12
14
|
const { clearMcpOAuthCaches } = await import('../lib/daemon/mcp-oauth.js');
|
|
13
15
|
clearMcpOAuthCaches();
|
|
16
|
+
// Signing out withdraws the opt-in to count no-company usage toward the
|
|
17
|
+
// default company; the next account must opt in for itself.
|
|
18
|
+
try {
|
|
19
|
+
retainUsageAttributionForPerson(null, { root: workContextRoot() });
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
console.error(chalk.yellow('Could not withdraw the usage opt-in:'), error instanceof Error ? error.message : 'Unknown error');
|
|
23
|
+
}
|
|
14
24
|
const existing = loadCachedTokens();
|
|
15
25
|
if (!existing) {
|
|
16
26
|
console.log(chalk.yellow('No cached HQ session'));
|
|
@@ -73,11 +73,12 @@ export async function runMcpServe(opts = {}) {
|
|
|
73
73
|
process.emitWarning("hq-cli resident package preload completion failed (class=Error, module=unknown)");
|
|
74
74
|
});
|
|
75
75
|
};
|
|
76
|
+
// Release before the stdio read can remain open indefinitely. The request
|
|
77
|
+
// handlers and their declared lazy modules are loaded before this point.
|
|
78
|
+
startResidentPreload();
|
|
76
79
|
const send = (msg) => {
|
|
77
80
|
const response = `${JSON.stringify({ jsonrpc: "2.0", ...msg })}\n`;
|
|
78
|
-
|
|
79
|
-
return output.write(response);
|
|
80
|
-
return output.write(response, startResidentPreload);
|
|
81
|
+
return output.write(response);
|
|
81
82
|
};
|
|
82
83
|
const reply = (id, result) => send({ id, result });
|
|
83
84
|
const replyError = (id, code, message) => send({ id, error: { code, message } });
|
package/dist/commands/mesh.d.ts
CHANGED
|
@@ -10,6 +10,30 @@ import { type MeshCompany, type MeshThread } from "../lib/mesh/api.js";
|
|
|
10
10
|
import { type DaemonInstallPlatform, type InstallPaths } from "../lib/mesh/live/daemon/index.js";
|
|
11
11
|
import { type RunCommand } from "../lib/service-manager/launchd.js";
|
|
12
12
|
export declare function formatCheckLines(threads: MeshThread[], company: MeshCompany, projectId?: string): string[];
|
|
13
|
+
/** Test seam for the usage-attribution question asked by `default set`. */
|
|
14
|
+
export interface DefaultCompanyPromptDeps {
|
|
15
|
+
/** True when stdin and stdout are a terminal (a person can answer). */
|
|
16
|
+
isInteractive?: () => boolean;
|
|
17
|
+
/** Ask a yes/no question; resolves true only for an explicit yes. */
|
|
18
|
+
confirm?: (question: string) => Promise<boolean>;
|
|
19
|
+
}
|
|
20
|
+
export declare function usageOptInExplainer(company: string): string;
|
|
21
|
+
export declare function usageOptInQuestion(company: string): string;
|
|
22
|
+
export declare function usageCountedLine(company: string): string;
|
|
23
|
+
export declare function usageNotCountedLine(company: string): string;
|
|
24
|
+
/** Only "y" or "yes" (any case, surrounding space ignored) is a yes. EOF (null) is no. */
|
|
25
|
+
export declare function parseYesNoAnswer(answer: string | null): boolean;
|
|
26
|
+
/** A person can answer only when both stdin and stdout are a terminal. */
|
|
27
|
+
export declare function isInteractiveTerminal(stdin?: {
|
|
28
|
+
isTTY?: boolean;
|
|
29
|
+
}, stdout?: {
|
|
30
|
+
isTTY?: boolean;
|
|
31
|
+
}): boolean;
|
|
32
|
+
/** Ask one yes/no question. Resolves false on EOF or a closed input. */
|
|
33
|
+
export declare function askYesNo(question: string, io?: {
|
|
34
|
+
input?: NodeJS.ReadableStream;
|
|
35
|
+
output?: NodeJS.WritableStream;
|
|
36
|
+
}): Promise<boolean>;
|
|
13
37
|
/** Injectable confirmation seam for `hq mesh context correct` (stubbed in tests). */
|
|
14
38
|
export type MeshConfirmFn = (message: string) => Promise<boolean>;
|
|
15
39
|
/** Exported for confirmation-gate unit tests (US-017B). */
|
|
@@ -25,7 +49,9 @@ export declare function runContextCorrect(opts: {
|
|
|
25
49
|
}, deps?: {
|
|
26
50
|
confirm?: MeshConfirmFn;
|
|
27
51
|
}): Promise<void>;
|
|
28
|
-
export declare function registerMeshCommand(program: Command
|
|
52
|
+
export declare function registerMeshCommand(program: Command, deps?: {
|
|
53
|
+
defaultCompanyPrompt?: DefaultCompanyPromptDeps;
|
|
54
|
+
}): void;
|
|
29
55
|
export declare function runDaemonStatus(opts: {
|
|
30
56
|
json?: boolean;
|
|
31
57
|
}, deps?: {
|
package/dist/commands/mesh.js
CHANGED
|
@@ -18,9 +18,10 @@ import { closeSupersededProjectThread, ensureMissingBoardStories, ensureProjectB
|
|
|
18
18
|
import { mapSourceStoryStatus } from "../lib/mesh/story-status.js";
|
|
19
19
|
import { buildPreflightFixtures } from "../lib/work-context/preflight-contract.js";
|
|
20
20
|
import { createCandidatesFetcher, createMigratePoster, createOrganizePoster, createWorkSessionDeliverer, fetchCompanyLive, formatCompanyLiveTable, openMeshTransport, probeMigrationCapabilityForMemberships, requireToken, } from "../lib/mesh/client.js";
|
|
21
|
-
import { clearDefaultCompany, isDefaultCompanyDisabled, readDeviceConfig, recordMigrationCapabilitySnapshot, setRepairHeldWithDefault, setDefaultCompany, } from "../lib/work-context/config.js";
|
|
21
|
+
import { clearDefaultCompany, effectiveUsageAttribution, isDefaultCompanyDisabled, readDeviceConfig, recordMigrationCapabilitySnapshot, setRepairHeldWithDefault, setDefaultCompany, setUsageAttribution, } from "../lib/work-context/config.js";
|
|
22
22
|
import { ensureDefaultCompany } from "../lib/work-context/default-company.js";
|
|
23
|
-
import {
|
|
23
|
+
import { personUidFromTokens } from "../lib/work-context/person-identity.js";
|
|
24
|
+
import { DefaultCompanyUnavailableError, WorkContextError, } from "../lib/work-context/errors.js";
|
|
24
25
|
import { isValidSessionId } from "../lib/mesh/live/session-identity.js";
|
|
25
26
|
import { CLI_KIND_TO_SCHEMA, resolveEnqueueSessionId, } from "../lib/mesh/live/index.js";
|
|
26
27
|
import { flushSessionEvents } from "../lib/mesh/live/flush.js";
|
|
@@ -1170,6 +1171,58 @@ async function runContextBackfillHeld(opts) {
|
|
|
1170
1171
|
"the mesh daemon's held retry will re-attribute and post those events.");
|
|
1171
1172
|
}
|
|
1172
1173
|
}
|
|
1174
|
+
export function usageOptInExplainer(company) {
|
|
1175
|
+
return `Opting in counts usage from HQ sessions started with no company toward ${company}. Sessions bound to a company count toward that company either way.`;
|
|
1176
|
+
}
|
|
1177
|
+
export function usageOptInQuestion(company) {
|
|
1178
|
+
return `Count usage from HQ sessions with no company toward ${company}? Your company's owner and admins will see that usage, including tokens, models, and the repo and branch you worked in. [y/N] `;
|
|
1179
|
+
}
|
|
1180
|
+
export function usageCountedLine(company) {
|
|
1181
|
+
return `Usage from HQ sessions with no company is counted toward ${company}.`;
|
|
1182
|
+
}
|
|
1183
|
+
export function usageNotCountedLine(company) {
|
|
1184
|
+
return `Usage from HQ sessions with no company is not counted toward ${company}. To opt in, run: hq mesh context default set ${company} --count-usage`;
|
|
1185
|
+
}
|
|
1186
|
+
/** Only "y" or "yes" (any case, surrounding space ignored) is a yes. EOF (null) is no. */
|
|
1187
|
+
export function parseYesNoAnswer(answer) {
|
|
1188
|
+
return answer !== null && /^y(es)?$/i.test(answer.trim());
|
|
1189
|
+
}
|
|
1190
|
+
/** A person can answer only when both stdin and stdout are a terminal. */
|
|
1191
|
+
export function isInteractiveTerminal(stdin = process.stdin, stdout = process.stdout) {
|
|
1192
|
+
return stdin.isTTY === true && stdout.isTTY === true;
|
|
1193
|
+
}
|
|
1194
|
+
/** Ask one yes/no question. Resolves false on EOF or a closed input. */
|
|
1195
|
+
export async function askYesNo(question, io = {}) {
|
|
1196
|
+
const rl = readline.createInterface({
|
|
1197
|
+
input: io.input ?? process.stdin,
|
|
1198
|
+
output: io.output ?? process.stderr,
|
|
1199
|
+
terminal: false,
|
|
1200
|
+
});
|
|
1201
|
+
const closed = new Promise((resolve) => rl.once("close", () => resolve(null)));
|
|
1202
|
+
try {
|
|
1203
|
+
const answer = await Promise.race([
|
|
1204
|
+
rl.question(question).catch(() => null),
|
|
1205
|
+
closed,
|
|
1206
|
+
]);
|
|
1207
|
+
return parseYesNoAnswer(answer);
|
|
1208
|
+
}
|
|
1209
|
+
finally {
|
|
1210
|
+
rl.close();
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
/** The signed-in person's prs_ uid from the cached Cognito session, or null. */
|
|
1214
|
+
function currentPersonUid() {
|
|
1215
|
+
try {
|
|
1216
|
+
return personUidFromTokens(loadCachedTokens());
|
|
1217
|
+
}
|
|
1218
|
+
catch {
|
|
1219
|
+
return null;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
function usageAttributionJson(cfg, personUid) {
|
|
1223
|
+
const effective = effectiveUsageAttribution(cfg, personUid);
|
|
1224
|
+
return { countUsage: effective !== null, usageAttribution: effective };
|
|
1225
|
+
}
|
|
1173
1226
|
async function runContextDefaultGet(opts) {
|
|
1174
1227
|
const root = workContextHomeRoot();
|
|
1175
1228
|
const cfg = readDeviceConfig({ root });
|
|
@@ -1189,6 +1242,7 @@ async function runContextDefaultGet(opts) {
|
|
|
1189
1242
|
needsChoice: !current && !disabled && memberships.length > 1,
|
|
1190
1243
|
memberships,
|
|
1191
1244
|
repairHeldWithDefault: cfg.repairHeldWithDefault === true,
|
|
1245
|
+
...usageAttributionJson(cfg, currentPersonUid()),
|
|
1192
1246
|
}, null, 2));
|
|
1193
1247
|
return;
|
|
1194
1248
|
}
|
|
@@ -1201,10 +1255,20 @@ async function runContextDefaultGet(opts) {
|
|
|
1201
1255
|
return;
|
|
1202
1256
|
}
|
|
1203
1257
|
console.log(`Default company: ${current.slug}${current.uid ? ` (${current.uid})` : ""} enabled=${current.enabled} source=${source}`);
|
|
1258
|
+
console.log(effectiveUsageAttribution(cfg, currentPersonUid())
|
|
1259
|
+
? usageCountedLine(current.slug)
|
|
1260
|
+
: usageNotCountedLine(current.slug));
|
|
1204
1261
|
}
|
|
1205
|
-
async function runContextDefaultSet(slug, opts) {
|
|
1262
|
+
async function runContextDefaultSet(slug, opts, promptDeps = {}) {
|
|
1206
1263
|
const root = workContextHomeRoot();
|
|
1207
1264
|
void opts.company; // retained for CLI compatibility
|
|
1265
|
+
const usageChoice = opts.countUsage === true
|
|
1266
|
+
? "opt-in"
|
|
1267
|
+
: opts.countUsage === false
|
|
1268
|
+
? "opt-out"
|
|
1269
|
+
: "keep-if-same";
|
|
1270
|
+
const personUid = currentPersonUid();
|
|
1271
|
+
const priorOptIn = effectiveUsageAttribution(readDeviceConfig({ root }), personUid);
|
|
1208
1272
|
let token;
|
|
1209
1273
|
try {
|
|
1210
1274
|
token = await requireToken();
|
|
@@ -1244,6 +1308,8 @@ async function runContextDefaultSet(slug, opts) {
|
|
|
1244
1308
|
const cfg = await setDefaultCompany(slug, {
|
|
1245
1309
|
root,
|
|
1246
1310
|
migrationCapability,
|
|
1311
|
+
usageAttribution: usageChoice,
|
|
1312
|
+
personUid,
|
|
1247
1313
|
validateMembership: async (candidate) => {
|
|
1248
1314
|
if (!token) {
|
|
1249
1315
|
throw new DefaultCompanyUnavailableError(`Cannot verify membership for company "${candidate}" (no Cognito session; offline or not logged in)`);
|
|
@@ -1261,19 +1327,55 @@ async function runContextDefaultSet(slug, opts) {
|
|
|
1261
1327
|
return { uid: membership.companyUid };
|
|
1262
1328
|
},
|
|
1263
1329
|
});
|
|
1264
|
-
|
|
1330
|
+
let configured = opts.repairHeldWithDefault
|
|
1265
1331
|
? setRepairHeldWithDefault(true, { root })
|
|
1266
1332
|
: cfg;
|
|
1333
|
+
const stored = configured.defaultCompany;
|
|
1334
|
+
const company = stored?.slug ?? slug;
|
|
1267
1335
|
if (opts.json) {
|
|
1268
|
-
|
|
1336
|
+
// Machine output never prompts; the opt-in changes only with a flag.
|
|
1337
|
+
console.log(JSON.stringify({
|
|
1338
|
+
ok: true,
|
|
1339
|
+
config: configured,
|
|
1340
|
+
migrationWarning,
|
|
1341
|
+
...usageAttributionJson(configured, personUid),
|
|
1342
|
+
}, null, 2));
|
|
1269
1343
|
return;
|
|
1270
1344
|
}
|
|
1271
|
-
|
|
1272
|
-
|
|
1345
|
+
console.log(`Default company set to ${company}${stored?.uid ? ` (${stored.uid})` : ""}`);
|
|
1346
|
+
if (priorOptIn && priorOptIn.companyUid !== stored?.uid) {
|
|
1347
|
+
console.log("Usage is no longer counted toward the previous default company.");
|
|
1348
|
+
}
|
|
1349
|
+
const interactive = (promptDeps.isInteractive ??
|
|
1350
|
+
(() => isInteractiveTerminal()))();
|
|
1351
|
+
if (usageChoice === "keep-if-same" &&
|
|
1352
|
+
interactive &&
|
|
1353
|
+
personUid &&
|
|
1354
|
+
!effectiveUsageAttribution(configured, personUid) &&
|
|
1355
|
+
stored?.uid?.startsWith("cmp_")) {
|
|
1356
|
+
console.error(usageOptInExplainer(company));
|
|
1357
|
+
let yes = false;
|
|
1358
|
+
try {
|
|
1359
|
+
yes = await (promptDeps.confirm ?? askYesNo)(usageOptInQuestion(company));
|
|
1360
|
+
}
|
|
1361
|
+
catch {
|
|
1362
|
+
yes = false;
|
|
1363
|
+
}
|
|
1364
|
+
if (yes)
|
|
1365
|
+
configured = setUsageAttribution({ companyUid: stored.uid, personUid }, { root });
|
|
1366
|
+
}
|
|
1367
|
+
console.log(effectiveUsageAttribution(configured, personUid)
|
|
1368
|
+
? usageCountedLine(company)
|
|
1369
|
+
: usageNotCountedLine(company));
|
|
1273
1370
|
if (migrationWarning)
|
|
1274
1371
|
console.error(chalk.yellow(migrationWarning));
|
|
1275
1372
|
}
|
|
1276
1373
|
catch (err) {
|
|
1374
|
+
if (err instanceof WorkContextError && err.code === "UsageAttributionUnavailable") {
|
|
1375
|
+
console.error(chalk.red(`${err.code}: ${err.message}`));
|
|
1376
|
+
process.exitCode = 1;
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1277
1379
|
if (err instanceof DefaultCompanyUnavailableError) {
|
|
1278
1380
|
console.error(chalk.red(`${err.code}: ${err.message}`));
|
|
1279
1381
|
process.exitCode = 1;
|
|
@@ -1396,12 +1498,16 @@ export async function runContextCorrect(opts, deps = {}) {
|
|
|
1396
1498
|
}
|
|
1397
1499
|
async function runContextDefaultClear(opts) {
|
|
1398
1500
|
const root = workContextHomeRoot();
|
|
1501
|
+
const hadOptIn = readDeviceConfig({ root }).usageAttribution !== undefined;
|
|
1399
1502
|
const cfg = clearDefaultCompany({ root });
|
|
1400
1503
|
if (opts.json) {
|
|
1401
|
-
console.log(JSON.stringify({ ok: true, config: cfg }, null, 2));
|
|
1504
|
+
console.log(JSON.stringify({ ok: true, config: cfg, ...usageAttributionJson(cfg, currentPersonUid()) }, null, 2));
|
|
1402
1505
|
return;
|
|
1403
1506
|
}
|
|
1404
1507
|
console.log("Default company cleared.");
|
|
1508
|
+
if (hadOptIn) {
|
|
1509
|
+
console.log("Usage from HQ sessions with no company is no longer counted toward any company.");
|
|
1510
|
+
}
|
|
1405
1511
|
}
|
|
1406
1512
|
async function runContextRequeue(opts) {
|
|
1407
1513
|
const root = workContextHomeRoot();
|
|
@@ -1854,7 +1960,7 @@ function addSharedFlags(cmd) {
|
|
|
1854
1960
|
.option("--thread-id <id>", "Explicit work thread id")
|
|
1855
1961
|
.option("--json", "Print machine-readable JSON");
|
|
1856
1962
|
}
|
|
1857
|
-
export function registerMeshCommand(program) {
|
|
1963
|
+
export function registerMeshCommand(program, deps = {}) {
|
|
1858
1964
|
const mesh = program
|
|
1859
1965
|
.command("mesh")
|
|
1860
1966
|
.description("Work mesh — register project work, report progress, and warm ~/.hq/work-mesh/cache");
|
|
@@ -2030,7 +2136,7 @@ export function registerMeshCommand(program) {
|
|
|
2030
2136
|
.description("Manage the device-local default company preference");
|
|
2031
2137
|
def
|
|
2032
2138
|
.command("get")
|
|
2033
|
-
.description("Show the device default company (never activeCompany)")
|
|
2139
|
+
.description("Show the device default company (never activeCompany) and whether usage is counted toward it")
|
|
2034
2140
|
.option("--json", "Print machine-readable JSON")
|
|
2035
2141
|
.action((opts) => wrap(() => runContextDefaultGet(opts))());
|
|
2036
2142
|
def
|
|
@@ -2039,12 +2145,17 @@ export function registerMeshCommand(program) {
|
|
|
2039
2145
|
.argument("<slug>", "Company slug")
|
|
2040
2146
|
.option("--allow-without-migration", "Deprecated compatibility no-op; selected-company membership is always verified")
|
|
2041
2147
|
.option("--repair-held-with-default", "Persist opt-in for lower-confidence historical held-session repair")
|
|
2148
|
+
.option("--count-usage", "Count usage from HQ sessions with no company toward this company (its owner and admins can see it)")
|
|
2149
|
+
.option("--no-count-usage", "Do not count usage from HQ sessions with no company toward this company")
|
|
2042
2150
|
.option("--company <slug|uid>", "Deprecated no-op; the selected company is verified directly")
|
|
2043
2151
|
.option("--json", "Print machine-readable JSON")
|
|
2044
|
-
.
|
|
2152
|
+
.addHelpText("after", "\nWith neither --count-usage nor --no-count-usage, an interactive terminal asks (default No);\n" +
|
|
2153
|
+
"otherwise an existing opt-in is kept only when it is for the same company.\n" +
|
|
2154
|
+
"Changing to a different company always stops counting usage toward the old one.")
|
|
2155
|
+
.action((slug, opts) => wrap(() => runContextDefaultSet(slug, opts, deps.defaultCompanyPrompt))());
|
|
2045
2156
|
def
|
|
2046
2157
|
.command("clear")
|
|
2047
|
-
.description("Clear the device default company")
|
|
2158
|
+
.description("Clear the device default company and any opt-in to count usage toward it")
|
|
2048
2159
|
.option("--json", "Print machine-readable JSON")
|
|
2049
2160
|
.action((opts) => wrap(() => runContextDefaultClear(opts))());
|
|
2050
2161
|
context
|
|
@@ -44,10 +44,41 @@ export interface RepoCompanyMapping {
|
|
|
44
44
|
updatedAt: string;
|
|
45
45
|
}
|
|
46
46
|
type RepoCompanyMap = Record<string, RepoCompanyMapping>;
|
|
47
|
+
/**
|
|
48
|
+
* Explicit opt-in to count usage from HQ sessions with no company toward the
|
|
49
|
+
* device default company. hq-cloud's usage sync reads this field and credits
|
|
50
|
+
* the default company only when mode is exactly "default-company", companyUid
|
|
51
|
+
* starts with "cmp_" and equals the default company's uid, and optedInAt
|
|
52
|
+
* parses as a date. Company owners and admins can see company-attributed
|
|
53
|
+
* usage, so this is never written without the person's explicit consent.
|
|
54
|
+
*/
|
|
55
|
+
export interface UsageAttributionOptIn {
|
|
56
|
+
mode: "default-company";
|
|
57
|
+
companyUid: string;
|
|
58
|
+
/**
|
|
59
|
+
* The signed-in person who consented: the `custom:entityUid` claim (prs_...)
|
|
60
|
+
* of the cached Cognito person session. Consent never carries to another
|
|
61
|
+
* account on the same OS user.
|
|
62
|
+
*/
|
|
63
|
+
personUid: string;
|
|
64
|
+
optedInAt: string;
|
|
65
|
+
}
|
|
66
|
+
/** A person principal uid, as carried in the `custom:entityUid` token claim. */
|
|
67
|
+
export declare function isPersonUid(value: unknown): value is string;
|
|
68
|
+
/** How `setDefaultCompany` treats the usage-attribution opt-in. */
|
|
69
|
+
export type UsageAttributionChoice =
|
|
70
|
+
/** Write an opt-in for the newly set company (requires its uid). */
|
|
71
|
+
"opt-in"
|
|
72
|
+
/** Remove any opt-in. */
|
|
73
|
+
| "opt-out"
|
|
74
|
+
/** Keep an existing opt-in only when it is for the same company uid. */
|
|
75
|
+
| "keep-if-same";
|
|
47
76
|
export interface WorkContextDeviceConfig {
|
|
48
77
|
schemaVersion: typeof DEVICE_CONFIG_SCHEMA_VERSION;
|
|
49
78
|
/** Explicit device default. Never copied from activeCompany. */
|
|
50
79
|
defaultCompany?: DeviceDefaultCompany | null;
|
|
80
|
+
/** Opt-in to count no-company HQ session usage toward the default company. */
|
|
81
|
+
usageAttribution?: UsageAttributionOptIn;
|
|
51
82
|
/** Marks a null default written by the automatic-activation implementation. */
|
|
52
83
|
defaultCompanyAutomaticEligible?: true;
|
|
53
84
|
/** Last-known migration capability across memberships (doctor reports this). */
|
|
@@ -75,12 +106,31 @@ export interface DeviceConfigDeps {
|
|
|
75
106
|
*/
|
|
76
107
|
validateMembership?: (slug: string) => Promise<MembershipValidationResult> | MembershipValidationResult;
|
|
77
108
|
now?: () => Date;
|
|
109
|
+
/**
|
|
110
|
+
* Usage-attribution opt-in handling for setDefaultCompany. Defaults to
|
|
111
|
+
* "keep-if-same": an existing opt-in survives only when it is for the same
|
|
112
|
+
* company uid, so changing the default never carries consent across companies.
|
|
113
|
+
*/
|
|
114
|
+
usageAttribution?: UsageAttributionChoice;
|
|
115
|
+
/**
|
|
116
|
+
* The signed-in person (prs_...). Required to write an opt-in; an existing
|
|
117
|
+
* opt-in is kept only when it was given by this person.
|
|
118
|
+
*/
|
|
119
|
+
personUid?: string | null;
|
|
78
120
|
}
|
|
79
121
|
/** Acquire an interprocess config lock; exported for direct lock regression tests. */
|
|
80
122
|
export declare function acquireDeviceConfigLock(root: string, options?: {
|
|
81
123
|
timeoutMs?: number;
|
|
82
124
|
staleMs?: number;
|
|
83
125
|
}): () => void;
|
|
126
|
+
/** Parse a raw usageAttribution value; anything malformed is dropped (fail closed). */
|
|
127
|
+
export declare function sanitizeUsageAttribution(raw: unknown): UsageAttributionOptIn | undefined;
|
|
128
|
+
/**
|
|
129
|
+
* The opt-in that is in effect: present, for the uid of the current enabled
|
|
130
|
+
* default company, and given by the signed-in person. Mirrors the hq-cloud
|
|
131
|
+
* reader. No signed-in person means no opt-in.
|
|
132
|
+
*/
|
|
133
|
+
export declare function effectiveUsageAttribution(cfg: Pick<WorkContextDeviceConfig, "defaultCompany" | "usageAttribution">, personUid: string | null | undefined): UsageAttributionOptIn | null;
|
|
84
134
|
/** Injection points for assertSafeConfigPath (tests only). */
|
|
85
135
|
export interface SafeConfigPathIo {
|
|
86
136
|
platform?: NodeJS.Platform;
|
|
@@ -104,6 +154,22 @@ export declare function getDefaultCompany(deps: Pick<DeviceConfigDeps, "root">):
|
|
|
104
154
|
/** A clear is a durable opt-out, not an invitation for automatic reactivation. */
|
|
105
155
|
export declare function isDefaultCompanyDisabled(deps: Pick<DeviceConfigDeps, "root">): boolean;
|
|
106
156
|
export declare function setDefaultCompany(slug: string, deps: DeviceConfigDeps): Promise<WorkContextDeviceConfig>;
|
|
157
|
+
/**
|
|
158
|
+
* Record or remove the usage-attribution opt-in for the current default
|
|
159
|
+
* company. Opting in requires an enabled, explicitly chosen default with a
|
|
160
|
+
* cmp_ uid that equals `companyUid` (the company the person was asked about);
|
|
161
|
+
* an automatic single-company default is never opted in.
|
|
162
|
+
*/
|
|
163
|
+
export declare function setUsageAttribution(optIn: {
|
|
164
|
+
companyUid: string;
|
|
165
|
+
personUid: string;
|
|
166
|
+
} | null, deps: Pick<DeviceConfigDeps, "root" | "now">): WorkContextDeviceConfig;
|
|
167
|
+
/**
|
|
168
|
+
* Drop the usage-attribution opt-in unless it was given by `personUid`. Called
|
|
169
|
+
* on login (identity may have changed) and logout (null: always drop). A
|
|
170
|
+
* config with no opt-in, or one already matching, is left untouched.
|
|
171
|
+
*/
|
|
172
|
+
export declare function retainUsageAttributionForPerson(personUid: string | null | undefined, deps: Pick<DeviceConfigDeps, "root" | "now">): boolean;
|
|
107
173
|
export declare function clearDefaultCompany(deps: DeviceConfigDeps): WorkContextDeviceConfig;
|
|
108
174
|
/**
|
|
109
175
|
* Read the persisted company mapping for a repo identity key (gap 4).
|
|
@@ -9,6 +9,10 @@ import { DefaultCompanyUnavailableError, UnsafeConfigPathError, WorkContextError
|
|
|
9
9
|
import { workContextConfigPath } from "./paths.js";
|
|
10
10
|
import { assertWindowsOwnerOnlyAcl } from "./windows-acl.js";
|
|
11
11
|
const DEVICE_CONFIG_SCHEMA_VERSION = 1;
|
|
12
|
+
/** A person principal uid, as carried in the `custom:entityUid` token claim. */
|
|
13
|
+
export function isPersonUid(value) {
|
|
14
|
+
return typeof value === "string" && /^prs_[A-Za-z0-9]+$/.test(value);
|
|
15
|
+
}
|
|
12
16
|
const CONFIG_LOCK_TIMEOUT_MS = 5_000;
|
|
13
17
|
const CONFIG_LOCK_RETRY_MS = 10;
|
|
14
18
|
function waitForConfigLock(ms) {
|
|
@@ -146,6 +150,44 @@ function sanitizeRepoCompanyMap(raw) {
|
|
|
146
150
|
}
|
|
147
151
|
return Object.keys(out).length > 0 ? out : null;
|
|
148
152
|
}
|
|
153
|
+
/** Parse a raw usageAttribution value; anything malformed is dropped (fail closed). */
|
|
154
|
+
export function sanitizeUsageAttribution(raw) {
|
|
155
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
156
|
+
return undefined;
|
|
157
|
+
const v = raw;
|
|
158
|
+
if (v.mode !== "default-company")
|
|
159
|
+
return undefined;
|
|
160
|
+
const companyUid = typeof v.companyUid === "string" ? v.companyUid.trim() : "";
|
|
161
|
+
const personUid = typeof v.personUid === "string" ? v.personUid.trim() : "";
|
|
162
|
+
const optedInAt = typeof v.optedInAt === "string" ? v.optedInAt.trim() : "";
|
|
163
|
+
if (!companyUid.startsWith("cmp_") ||
|
|
164
|
+
!isPersonUid(personUid) ||
|
|
165
|
+
!optedInAt ||
|
|
166
|
+
!Number.isFinite(Date.parse(optedInAt))) {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
return { mode: "default-company", companyUid, personUid, optedInAt };
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The opt-in that is in effect: present, for the uid of the current enabled
|
|
173
|
+
* default company, and given by the signed-in person. Mirrors the hq-cloud
|
|
174
|
+
* reader. No signed-in person means no opt-in.
|
|
175
|
+
*/
|
|
176
|
+
export function effectiveUsageAttribution(cfg, personUid) {
|
|
177
|
+
const opt = cfg.usageAttribution;
|
|
178
|
+
if (!opt || !isPersonUid(personUid) || opt.personUid !== personUid)
|
|
179
|
+
return null;
|
|
180
|
+
const def = cfg.defaultCompany;
|
|
181
|
+
if (!opt || !def || def.enabled !== true || def.source === "disabled")
|
|
182
|
+
return null;
|
|
183
|
+
if (!def.uid || def.uid !== opt.companyUid)
|
|
184
|
+
return null;
|
|
185
|
+
return opt;
|
|
186
|
+
}
|
|
187
|
+
/** Keep `usageAttribution` next to `defaultCompany`, omitting it when absent. */
|
|
188
|
+
function usageAttributionField(value) {
|
|
189
|
+
return value ? { usageAttribution: value } : {};
|
|
190
|
+
}
|
|
149
191
|
/**
|
|
150
192
|
* Refuse a config path that is a symlink (every platform) or writable by
|
|
151
193
|
* anyone other than the owner.
|
|
@@ -216,6 +258,7 @@ export function readDeviceConfig(deps) {
|
|
|
216
258
|
defaultCompany: legacyNullDefault
|
|
217
259
|
? { slug: "", enabled: false, source: "disabled" }
|
|
218
260
|
: raw.defaultCompany ?? null,
|
|
261
|
+
...usageAttributionField(sanitizeUsageAttribution(raw.usageAttribution)),
|
|
219
262
|
migrationCapability: raw.migrationCapability ?? null,
|
|
220
263
|
repoCompanyMap: sanitizeRepoCompanyMap(raw.repoCompanyMap),
|
|
221
264
|
defaultCompanyMemberships: sanitizeMembershipSnapshot(raw.defaultCompanyMemberships),
|
|
@@ -287,9 +330,36 @@ export async function setDefaultCompany(slug, deps) {
|
|
|
287
330
|
};
|
|
288
331
|
if (resolvedUid)
|
|
289
332
|
defaultCompany.uid = resolvedUid;
|
|
333
|
+
const choice = deps.usageAttribution ?? "keep-if-same";
|
|
334
|
+
let usageAttribution;
|
|
335
|
+
const personUid = isPersonUid(deps.personUid) ? deps.personUid : undefined;
|
|
336
|
+
if (choice === "opt-in") {
|
|
337
|
+
if (!resolvedUid?.startsWith("cmp_")) {
|
|
338
|
+
throw new WorkContextError("UsageAttributionUnavailable", `Cannot count usage toward "${trimmed}": its company uid is unknown`);
|
|
339
|
+
}
|
|
340
|
+
if (!personUid) {
|
|
341
|
+
throw new WorkContextError("UsageAttributionUnavailable", `Cannot count usage toward "${trimmed}": the signed-in person could not be determined`);
|
|
342
|
+
}
|
|
343
|
+
usageAttribution = {
|
|
344
|
+
mode: "default-company",
|
|
345
|
+
companyUid: resolvedUid,
|
|
346
|
+
personUid,
|
|
347
|
+
optedInAt: now().toISOString(),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
else if (choice === "keep-if-same") {
|
|
351
|
+
usageAttribution =
|
|
352
|
+
resolvedUid &&
|
|
353
|
+
personUid &&
|
|
354
|
+
prior.usageAttribution?.companyUid === resolvedUid &&
|
|
355
|
+
prior.usageAttribution.personUid === personUid
|
|
356
|
+
? prior.usageAttribution
|
|
357
|
+
: undefined;
|
|
358
|
+
}
|
|
290
359
|
const next = {
|
|
291
360
|
schemaVersion: DEVICE_CONFIG_SCHEMA_VERSION,
|
|
292
361
|
defaultCompany,
|
|
362
|
+
...usageAttributionField(usageAttribution),
|
|
293
363
|
migrationCapability: prior.migrationCapability ?? null,
|
|
294
364
|
repoCompanyMap: prior.repoCompanyMap ?? null,
|
|
295
365
|
defaultCompanyMemberships: prior.defaultCompanyMemberships ?? null,
|
|
@@ -300,6 +370,69 @@ export async function setDefaultCompany(slug, deps) {
|
|
|
300
370
|
return next;
|
|
301
371
|
});
|
|
302
372
|
}
|
|
373
|
+
/**
|
|
374
|
+
* Record or remove the usage-attribution opt-in for the current default
|
|
375
|
+
* company. Opting in requires an enabled, explicitly chosen default with a
|
|
376
|
+
* cmp_ uid that equals `companyUid` (the company the person was asked about);
|
|
377
|
+
* an automatic single-company default is never opted in.
|
|
378
|
+
*/
|
|
379
|
+
export function setUsageAttribution(optIn, deps) {
|
|
380
|
+
const now = deps.now ?? (() => new Date());
|
|
381
|
+
return withDeviceConfigLock(deps.root, () => {
|
|
382
|
+
const prior = readDeviceConfig(deps);
|
|
383
|
+
let usageAttribution;
|
|
384
|
+
if (optIn) {
|
|
385
|
+
const def = prior.defaultCompany;
|
|
386
|
+
if (!def ||
|
|
387
|
+
def.enabled !== true ||
|
|
388
|
+
def.source !== "chosen" ||
|
|
389
|
+
!def.uid?.startsWith("cmp_") ||
|
|
390
|
+
def.uid !== optIn.companyUid ||
|
|
391
|
+
!isPersonUid(optIn.personUid)) {
|
|
392
|
+
throw new WorkContextError("UsageAttributionUnavailable", "Usage can only be counted toward a default company you chose on this device");
|
|
393
|
+
}
|
|
394
|
+
usageAttribution = {
|
|
395
|
+
mode: "default-company",
|
|
396
|
+
companyUid: def.uid,
|
|
397
|
+
personUid: optIn.personUid,
|
|
398
|
+
optedInAt: now().toISOString(),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
const next = {
|
|
402
|
+
schemaVersion: DEVICE_CONFIG_SCHEMA_VERSION,
|
|
403
|
+
defaultCompany: prior.defaultCompany ?? null,
|
|
404
|
+
...usageAttributionField(usageAttribution),
|
|
405
|
+
migrationCapability: prior.migrationCapability ?? null,
|
|
406
|
+
repoCompanyMap: prior.repoCompanyMap ?? null,
|
|
407
|
+
defaultCompanyMemberships: prior.defaultCompanyMemberships ?? null,
|
|
408
|
+
repairHeldWithDefault: prior.repairHeldWithDefault === true,
|
|
409
|
+
updatedAt: now().toISOString(),
|
|
410
|
+
};
|
|
411
|
+
writeDeviceConfig({ root: deps.root }, next);
|
|
412
|
+
return next;
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Drop the usage-attribution opt-in unless it was given by `personUid`. Called
|
|
417
|
+
* on login (identity may have changed) and logout (null: always drop). A
|
|
418
|
+
* config with no opt-in, or one already matching, is left untouched.
|
|
419
|
+
*/
|
|
420
|
+
export function retainUsageAttributionForPerson(personUid, deps) {
|
|
421
|
+
if (!fs.existsSync(workContextConfigPath(deps.root)))
|
|
422
|
+
return false;
|
|
423
|
+
return withDeviceConfigLock(deps.root, () => {
|
|
424
|
+
const prior = readDeviceConfig(deps);
|
|
425
|
+
const opt = prior.usageAttribution;
|
|
426
|
+
if (!opt)
|
|
427
|
+
return false;
|
|
428
|
+
if (isPersonUid(personUid) && opt.personUid === personUid)
|
|
429
|
+
return false;
|
|
430
|
+
const now = deps.now ?? (() => new Date());
|
|
431
|
+
const { usageAttribution: _dropped, ...rest } = prior;
|
|
432
|
+
writeDeviceConfig({ root: deps.root }, { ...rest, updatedAt: now().toISOString() });
|
|
433
|
+
return true;
|
|
434
|
+
});
|
|
435
|
+
}
|
|
303
436
|
export function clearDefaultCompany(deps) {
|
|
304
437
|
const now = deps.now ?? (() => new Date());
|
|
305
438
|
return withDeviceConfigLock(deps.root, () => {
|
|
@@ -307,6 +440,8 @@ export function clearDefaultCompany(deps) {
|
|
|
307
440
|
const next = {
|
|
308
441
|
schemaVersion: DEVICE_CONFIG_SCHEMA_VERSION,
|
|
309
442
|
defaultCompany: { slug: "", enabled: false, source: "disabled" },
|
|
443
|
+
// usageAttribution is intentionally omitted: clearing the default also
|
|
444
|
+
// withdraws the opt-in to count usage toward it.
|
|
310
445
|
migrationCapability: prior.migrationCapability ?? null,
|
|
311
446
|
repoCompanyMap: prior.repoCompanyMap ?? null,
|
|
312
447
|
defaultCompanyMemberships: prior.defaultCompanyMemberships ?? null,
|
|
@@ -355,6 +490,7 @@ export function setRepoCompany(key, mapping, deps) {
|
|
|
355
490
|
const next = {
|
|
356
491
|
schemaVersion: DEVICE_CONFIG_SCHEMA_VERSION,
|
|
357
492
|
defaultCompany: prior.defaultCompany ?? null,
|
|
493
|
+
...usageAttributionField(prior.usageAttribution),
|
|
358
494
|
migrationCapability: prior.migrationCapability ?? null,
|
|
359
495
|
repoCompanyMap: nextMap,
|
|
360
496
|
defaultCompanyMemberships: prior.defaultCompanyMemberships ?? null,
|
|
@@ -373,6 +509,7 @@ export function recordMigrationCapabilitySnapshot(snapshot, deps) {
|
|
|
373
509
|
const next = {
|
|
374
510
|
schemaVersion: DEVICE_CONFIG_SCHEMA_VERSION,
|
|
375
511
|
defaultCompany: prior.defaultCompany ?? null,
|
|
512
|
+
...usageAttributionField(prior.usageAttribution),
|
|
376
513
|
migrationCapability: {
|
|
377
514
|
unlocked: snapshot.unlocked,
|
|
378
515
|
offline: snapshot.offline,
|
|
@@ -414,6 +551,7 @@ export function recordDefaultCompanyMemberships(snapshot, deps) {
|
|
|
414
551
|
const next = {
|
|
415
552
|
schemaVersion: DEVICE_CONFIG_SCHEMA_VERSION,
|
|
416
553
|
defaultCompany: prior.defaultCompany ?? null,
|
|
554
|
+
...usageAttributionField(prior.usageAttribution),
|
|
417
555
|
migrationCapability: prior.migrationCapability ?? null,
|
|
418
556
|
repoCompanyMap: prior.repoCompanyMap ?? null,
|
|
419
557
|
defaultCompanyMemberships: {
|
|
@@ -435,6 +573,7 @@ export function invalidateDefaultCompanyMemberships(deps) {
|
|
|
435
573
|
const next = {
|
|
436
574
|
schemaVersion: DEVICE_CONFIG_SCHEMA_VERSION,
|
|
437
575
|
defaultCompany: prior.defaultCompany ?? null,
|
|
576
|
+
...usageAttributionField(prior.usageAttribution),
|
|
438
577
|
migrationCapability: prior.migrationCapability ?? null,
|
|
439
578
|
repoCompanyMap: prior.repoCompanyMap ?? null,
|
|
440
579
|
defaultCompanyMemberships: null,
|
|
@@ -468,6 +607,8 @@ export function setAutoDefaultCompany(membership, deps) {
|
|
|
468
607
|
enabled: true,
|
|
469
608
|
source: "auto_single",
|
|
470
609
|
},
|
|
610
|
+
// usageAttribution is intentionally omitted: an automatic default is
|
|
611
|
+
// never opted in to usage attribution.
|
|
471
612
|
migrationCapability: prior.migrationCapability ?? null,
|
|
472
613
|
repoCompanyMap: prior.repoCompanyMap ?? null,
|
|
473
614
|
defaultCompanyMemberships: prior.defaultCompanyMemberships ?? null,
|
|
@@ -485,6 +626,7 @@ export function setRepairHeldWithDefault(enabled, deps) {
|
|
|
485
626
|
const next = {
|
|
486
627
|
schemaVersion: DEVICE_CONFIG_SCHEMA_VERSION,
|
|
487
628
|
defaultCompany: prior.defaultCompany ?? null,
|
|
629
|
+
...usageAttributionField(prior.usageAttribution),
|
|
488
630
|
migrationCapability: prior.migrationCapability ?? null,
|
|
489
631
|
repoCompanyMap: prior.repoCompanyMap ?? null,
|
|
490
632
|
defaultCompanyMemberships: prior.defaultCompanyMemberships ?? null,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signed-in person identity for the usage-attribution opt-in.
|
|
3
|
+
*
|
|
4
|
+
* Source: the cached Cognito person session (`~/.hq/cognito-tokens.json`,
|
|
5
|
+
* read through `loadCachedTokens`). The person uid is the `custom:entityUid`
|
|
6
|
+
* claim of the ID token (falling back to the access token), and only a
|
|
7
|
+
* `prs_` value counts. Agent (`agt_`) or unknown identities return null, so
|
|
8
|
+
* no opt-in is written or kept for them. Claims are read without
|
|
9
|
+
* verification; this decides only whether local consent belongs to the
|
|
10
|
+
* account that is signed in, never authorization.
|
|
11
|
+
*/
|
|
12
|
+
export declare function personUidFromTokens(tokens: {
|
|
13
|
+
idToken?: string;
|
|
14
|
+
accessToken?: string;
|
|
15
|
+
} | null | undefined): string | null;
|
|
16
|
+
//# sourceMappingURL=person-identity.d.ts.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signed-in person identity for the usage-attribution opt-in.
|
|
3
|
+
*
|
|
4
|
+
* Source: the cached Cognito person session (`~/.hq/cognito-tokens.json`,
|
|
5
|
+
* read through `loadCachedTokens`). The person uid is the `custom:entityUid`
|
|
6
|
+
* claim of the ID token (falling back to the access token), and only a
|
|
7
|
+
* `prs_` value counts. Agent (`agt_`) or unknown identities return null, so
|
|
8
|
+
* no opt-in is written or kept for them. Claims are read without
|
|
9
|
+
* verification; this decides only whether local consent belongs to the
|
|
10
|
+
* account that is signed in, never authorization.
|
|
11
|
+
*/
|
|
12
|
+
import { isPersonUid } from "./config.js";
|
|
13
|
+
function claimsOf(token) {
|
|
14
|
+
if (typeof token !== "string")
|
|
15
|
+
return null;
|
|
16
|
+
const part = token.split(".")[1];
|
|
17
|
+
if (!part)
|
|
18
|
+
return null;
|
|
19
|
+
try {
|
|
20
|
+
const parsed = JSON.parse(Buffer.from(part, "base64url").toString("utf8"));
|
|
21
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
22
|
+
? parsed
|
|
23
|
+
: null;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function personUidFromTokens(tokens) {
|
|
30
|
+
if (!tokens)
|
|
31
|
+
return null;
|
|
32
|
+
for (const token of [tokens.idToken, tokens.accessToken]) {
|
|
33
|
+
const uid = claimsOf(token)?.["custom:entityUid"];
|
|
34
|
+
if (isPersonUid(uid))
|
|
35
|
+
return uid;
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=person-identity.js.map
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
*/
|
|
33
33
|
import { isExpiring, loadCachedTokens } from "./cognito-session.js";
|
|
34
34
|
import { type VersionInfo } from "./feedback-versions.js";
|
|
35
|
-
import { type ClientHealthArch, type ClientHealthHeartbeat, type ClientHealthLocalFilesOverview, type ClientHealthPlatform } from "./client-health-contract.js";
|
|
35
|
+
import { type ClientHealthArch, type ClientHealthFailureReason, type ClientHealthHeartbeat, type ClientHealthLocalFilesOverview, type ClientHealthPlatform } from "./client-health-contract.js";
|
|
36
36
|
export declare const CLIENT_HEALTH_STATE_FILE = "cli-client-health.json";
|
|
37
37
|
/**
|
|
38
38
|
* Single-flight marker for the journal observation below. Held only for the
|
|
@@ -141,6 +141,8 @@ export interface BuildCliHeartbeatInput {
|
|
|
141
141
|
now: Date;
|
|
142
142
|
platform?: ClientHealthPlatform | null;
|
|
143
143
|
arch?: ClientHealthArch | null;
|
|
144
|
+
/** Closed, typed reason from the terminal sync runner; never free-form text. */
|
|
145
|
+
failureReason?: ClientHealthFailureReason;
|
|
144
146
|
/**
|
|
145
147
|
* Bounded local log/journal file facts (US-016). Only ever attached for
|
|
146
148
|
* `sync_success` / `sync_failure` kinds — support asked for this on the
|
|
@@ -148,6 +150,8 @@ export interface BuildCliHeartbeatInput {
|
|
|
148
150
|
*/
|
|
149
151
|
localFilesOverview?: ClientHealthLocalFilesOverview | null;
|
|
150
152
|
}
|
|
153
|
+
/** Classify sync failures using typed error identity/code fields only. */
|
|
154
|
+
export declare function classifySyncFailure(err: unknown): ClientHealthFailureReason;
|
|
151
155
|
/**
|
|
152
156
|
* Build one contract-valid CLI heartbeat, or null when this environment
|
|
153
157
|
* cannot be represented in the closed contract enums (unknown platform/arch)
|
|
@@ -184,7 +188,7 @@ export interface ClientHealthDeps {
|
|
|
184
188
|
* ignores its abort signal) must never delay a command past the bound. On
|
|
185
189
|
* timeout the pending work is abandoned silently.
|
|
186
190
|
*/
|
|
187
|
-
export declare function contributeClientHealth(kind: ClientHealthEventKind, deps?: ClientHealthDeps): Promise<void>;
|
|
191
|
+
export declare function contributeClientHealth(kind: ClientHealthEventKind, deps?: ClientHealthDeps, failureReason?: ClientHealthFailureReason): Promise<void>;
|
|
188
192
|
/**
|
|
189
193
|
* Fire-once-per-process invocation heartbeat (commander preAction seam, next
|
|
190
194
|
* to `emitCliSessionStarted`). Reports the real CLI + Core versions, platform,
|
|
@@ -194,7 +198,7 @@ export declare function reportCliClientHealthInvocation(deps?: ClientHealthDeps)
|
|
|
194
198
|
export interface SyncHealthReport {
|
|
195
199
|
/** Report the terminal outcome. Bounded, swallowed, safe to await. */
|
|
196
200
|
succeeded(): Promise<void>;
|
|
197
|
-
failed(): Promise<void>;
|
|
201
|
+
failed(reason?: ClientHealthFailureReason): Promise<void>;
|
|
198
202
|
}
|
|
199
203
|
/**
|
|
200
204
|
* Report a CLI sync command run: an attempt heartbeat now (fire-and-forget)
|
|
@@ -37,6 +37,7 @@ import { getStateDir, listJournals } from "@indigoai-us/hq-cloud";
|
|
|
37
37
|
import { isExpiring, isMachineIdentity, loadCachedTokens, } from "./cognito-session.js";
|
|
38
38
|
import { vaultApiFetch } from "./vault-api.js";
|
|
39
39
|
import { collectVersions } from "./feedback-versions.js";
|
|
40
|
+
import { isAuthError } from "./auth-error.js";
|
|
40
41
|
import { CLIENT_HEALTH_CONTRACT_VERSION, parseClientHealthHeartbeat, } from "./client-health-contract.js";
|
|
41
42
|
import { collectLocalFilesOverview } from "./local-files-overview.js";
|
|
42
43
|
/** Same bound as CLI telemetry: a heartbeat may never stall a command. */
|
|
@@ -314,6 +315,25 @@ function acquireJournalObservationLock(stateDir, nowMs, staleMs) {
|
|
|
314
315
|
}
|
|
315
316
|
};
|
|
316
317
|
}
|
|
318
|
+
/** Classify sync failures using typed error identity/code fields only. */
|
|
319
|
+
export function classifySyncFailure(err) {
|
|
320
|
+
if (isAuthError(err))
|
|
321
|
+
return "AUTH_EXPIRED";
|
|
322
|
+
let current = err;
|
|
323
|
+
for (let depth = 0; depth < 3; depth += 1) {
|
|
324
|
+
if (current === null || typeof current !== "object")
|
|
325
|
+
break;
|
|
326
|
+
const value = current;
|
|
327
|
+
if (value.code === "ENOSPC" || value.code === "EDQUOT") {
|
|
328
|
+
return "DISK_FULL";
|
|
329
|
+
}
|
|
330
|
+
if (value.code === "EACCES" || value.code === "EPERM") {
|
|
331
|
+
return "PERMISSION_DENIED";
|
|
332
|
+
}
|
|
333
|
+
current = value.cause;
|
|
334
|
+
}
|
|
335
|
+
return "RUNNER_FAILED";
|
|
336
|
+
}
|
|
317
337
|
/**
|
|
318
338
|
* Build one contract-valid CLI heartbeat, or null when this environment
|
|
319
339
|
* cannot be represented in the closed contract enums (unknown platform/arch)
|
|
@@ -388,6 +408,9 @@ export function buildCliHeartbeat(input) {
|
|
|
388
408
|
if (input.state.lastSyncAttemptAt !== undefined) {
|
|
389
409
|
heartbeat.lastSyncAttemptAt = input.state.lastSyncAttemptAt;
|
|
390
410
|
}
|
|
411
|
+
if (input.kind === "sync_failure" && input.failureReason !== undefined) {
|
|
412
|
+
heartbeat.failureReason = input.failureReason;
|
|
413
|
+
}
|
|
391
414
|
if (lastSyncSuccessAt !== null) {
|
|
392
415
|
heartbeat.lastSyncSuccessAt = lastSyncSuccessAt;
|
|
393
416
|
}
|
|
@@ -435,10 +458,10 @@ function settleWithin(work, ms) {
|
|
|
435
458
|
* ignores its abort signal) must never delay a command past the bound. On
|
|
436
459
|
* timeout the pending work is abandoned silently.
|
|
437
460
|
*/
|
|
438
|
-
export async function contributeClientHealth(kind, deps = {}) {
|
|
439
|
-
await settleWithin(contributeClientHealthUnbounded(kind, deps), deps.timeoutMs ?? CLIENT_HEALTH_TIMEOUT_MS);
|
|
461
|
+
export async function contributeClientHealth(kind, deps = {}, failureReason) {
|
|
462
|
+
await settleWithin(contributeClientHealthUnbounded(kind, deps, failureReason), deps.timeoutMs ?? CLIENT_HEALTH_TIMEOUT_MS);
|
|
440
463
|
}
|
|
441
|
-
async function contributeClientHealthUnbounded(kind, deps) {
|
|
464
|
+
async function contributeClientHealthUnbounded(kind, deps, failureReason) {
|
|
442
465
|
try {
|
|
443
466
|
const now = (deps.now ?? (() => new Date()))();
|
|
444
467
|
const stateDir = (deps.stateDir ?? getStateDir)();
|
|
@@ -546,6 +569,7 @@ async function contributeClientHealthUnbounded(kind, deps) {
|
|
|
546
569
|
journalLastSyncAt,
|
|
547
570
|
now,
|
|
548
571
|
localFilesOverview,
|
|
572
|
+
failureReason,
|
|
549
573
|
});
|
|
550
574
|
if (!heartbeat)
|
|
551
575
|
return;
|
|
@@ -587,12 +611,12 @@ export function reportCliClientHealthInvocation(deps) {
|
|
|
587
611
|
*/
|
|
588
612
|
export function beginSyncHealthReport(deps = {}) {
|
|
589
613
|
const attempt = contributeClientHealth("sync_attempt", deps);
|
|
590
|
-
const finish = (kind) => attempt
|
|
591
|
-
.then(() => contributeClientHealth(kind, deps))
|
|
614
|
+
const finish = (kind, failureReason) => attempt
|
|
615
|
+
.then(() => contributeClientHealth(kind, deps, failureReason))
|
|
592
616
|
.catch(() => undefined);
|
|
593
617
|
return {
|
|
594
618
|
succeeded: () => finish("sync_success"),
|
|
595
|
-
failed: () => finish("sync_failure"),
|
|
619
|
+
failed: (reason) => finish("sync_failure", reason),
|
|
596
620
|
};
|
|
597
621
|
}
|
|
598
622
|
//# sourceMappingURL=client-health.js.map
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export declare const PACKAGE_USE_LEASE_HANDOFF_ENV = "HQ_CLI_PACKAGE_USE_LEASE_HANDOFF";
|
|
2
|
+
export declare const PACKAGE_USE_LEASE_PURPOSES: readonly ["command", "resident-preload", "mcp-serve", "hook", "daemon"];
|
|
3
|
+
export type PackageUseLeasePurpose = (typeof PACKAGE_USE_LEASE_PURPOSES)[number];
|
|
2
4
|
export declare class PackageUseUpdatePendingError extends Error {
|
|
3
5
|
constructor(message: string);
|
|
4
6
|
}
|
|
@@ -10,7 +12,10 @@ export interface PackageUseLeaseRecord {
|
|
|
10
12
|
pid: number;
|
|
11
13
|
start_time_ms: number;
|
|
12
14
|
hq_version: string;
|
|
15
|
+
purpose?: PackageUseLeasePurpose;
|
|
13
16
|
}
|
|
17
|
+
/** Map only fixed command words to a bounded lease purpose; never persist argv values. */
|
|
18
|
+
export declare function packageUseLeasePurposeForArgv(argv: readonly string[]): PackageUseLeasePurpose;
|
|
14
19
|
export interface PackageUseLeaseHandoff {
|
|
15
20
|
readonly envValue: string;
|
|
16
21
|
waitForChild(pid: number, childAlive: () => boolean): Promise<boolean>;
|
|
@@ -20,7 +25,7 @@ export declare function npmPrefix(root: string, platform?: NodeJS.Platform): str
|
|
|
20
25
|
export declare function packageUseLeaseDirectory(prefix: string, stateDirectory?: string): string;
|
|
21
26
|
export declare function packageUseUpdateRequestPath(prefix: string, stateDirectory?: string): string;
|
|
22
27
|
declare function waitForUpdateRequest(requestPath: string, timeoutMs: number): void;
|
|
23
|
-
export declare function acquirePackageUseLease(prefix: string, hqVersion: string, stateDirectory?: string): PackageUseLease;
|
|
28
|
+
export declare function acquirePackageUseLease(prefix: string, hqVersion: string, stateDirectory?: string, purpose?: PackageUseLeasePurpose): PackageUseLease;
|
|
24
29
|
/**
|
|
25
30
|
* Create a bounded, per-reexec rendezvous in the current lease directory.
|
|
26
31
|
* The child confirms readiness only after publishing its own atomic lease.
|
|
@@ -29,7 +34,7 @@ export declare function createPackageUseLeaseHandoff(lease: PackageUseLease, tim
|
|
|
29
34
|
export declare function createCurrentPackageUseLeaseHandoff(timeoutMs?: number): PackageUseLeaseHandoff | null;
|
|
30
35
|
/** Publish this process's lease identity to a parent waiting in the same prefix. */
|
|
31
36
|
export declare function publishPackageUseLeaseHandoff(lease: PackageUseLease, env?: NodeJS.ProcessEnv): boolean;
|
|
32
|
-
export declare function acquireCurrentPackageUseLease(): PackageUseLease | null;
|
|
37
|
+
export declare function acquireCurrentPackageUseLease(argv?: readonly string[]): PackageUseLease | null;
|
|
33
38
|
/** Release this process's package-use lease after a long-running command has loaded its package modules. */
|
|
34
39
|
export declare function releaseCurrentPackageUseLease(): void;
|
|
35
40
|
/** The lease remains held through command execution and is released on process exit. */
|
|
@@ -8,12 +8,47 @@ const CLI_NAME = "@indigoai-us/hq-cli";
|
|
|
8
8
|
const UPDATE_REQUEST = "update.pending.json";
|
|
9
9
|
export const PACKAGE_USE_LEASE_HANDOFF_ENV = "HQ_CLI_PACKAGE_USE_LEASE_HANDOFF";
|
|
10
10
|
const HANDOFF_TIMEOUT_MS = 2_000;
|
|
11
|
+
export const PACKAGE_USE_LEASE_PURPOSES = [
|
|
12
|
+
"command",
|
|
13
|
+
"resident-preload",
|
|
14
|
+
"mcp-serve",
|
|
15
|
+
"hook",
|
|
16
|
+
"daemon",
|
|
17
|
+
];
|
|
11
18
|
export class PackageUseUpdatePendingError extends Error {
|
|
12
19
|
constructor(message) {
|
|
13
20
|
super(message);
|
|
14
21
|
this.name = "PackageUseUpdatePendingError";
|
|
15
22
|
}
|
|
16
23
|
}
|
|
24
|
+
/** Map only fixed command words to a bounded lease purpose; never persist argv values. */
|
|
25
|
+
export function packageUseLeasePurposeForArgv(argv) {
|
|
26
|
+
const args = argv.slice(2);
|
|
27
|
+
if (args[0] === "mcp" && args[1] === "serve")
|
|
28
|
+
return "mcp-serve";
|
|
29
|
+
if ((args[0] === "daemon" && args[1] === "run") ||
|
|
30
|
+
(args[0] === "mesh" && args[1] === "daemon" && args[2] === "run"))
|
|
31
|
+
return "daemon";
|
|
32
|
+
if (args[0] === "core" && ["hq-session", "checkpoint-stop-gate"].includes(args[1] ?? "")) {
|
|
33
|
+
return "hook";
|
|
34
|
+
}
|
|
35
|
+
const hasOption = (...options) => options.some((option) => args.includes(option));
|
|
36
|
+
if (args[0] === "login" ||
|
|
37
|
+
(args[0] === "auth" && args[1] === "login") ||
|
|
38
|
+
(args[0] === "agents" && ["kit", "mcp"].includes(args[1] ?? "")) ||
|
|
39
|
+
(args[0] === "agents" && ["terminal", "message"].includes(args[1] ?? "") &&
|
|
40
|
+
(args[1] === "terminal" || hasOption("--wait"))) ||
|
|
41
|
+
(args[0] === "bot" && (args[1] === "run" || (args[1] === "logs" && hasOption("--follow", "-f")))) ||
|
|
42
|
+
(args[0] === "daemon" && args[1] === "logs" && hasOption("--follow", "-f")) ||
|
|
43
|
+
(args[0] === "integrations" && args[1] === "connect") ||
|
|
44
|
+
(args[0] === "lanes" && ["watch", "message", "resume"].includes(args[1] ?? "")) ||
|
|
45
|
+
(args[0] === "monitor" && ["wait", "_run"].includes(args[1] ?? "")) ||
|
|
46
|
+
(args[0] === "pack" && args[1] === "install") ||
|
|
47
|
+
(args[0] === "pkg" && args[1] === "update") ||
|
|
48
|
+
(args[0] === "secrets" && args[1] === "sandbox"))
|
|
49
|
+
return "resident-preload";
|
|
50
|
+
return "command";
|
|
51
|
+
}
|
|
17
52
|
let currentPackageUseLease = null;
|
|
18
53
|
function errorLabel(error) {
|
|
19
54
|
if (error && typeof error === "object") {
|
|
@@ -131,7 +166,7 @@ function waitForUpdateRequest(requestPath, timeoutMs) {
|
|
|
131
166
|
Atomics.wait(sleeper, 0, 0, 100);
|
|
132
167
|
}
|
|
133
168
|
}
|
|
134
|
-
export function acquirePackageUseLease(prefix, hqVersion, stateDirectory) {
|
|
169
|
+
export function acquirePackageUseLease(prefix, hqVersion, stateDirectory, purpose = "command") {
|
|
135
170
|
const directory = packageUseLeaseDirectory(prefix, stateDirectory);
|
|
136
171
|
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
137
172
|
const requestPath = path.join(directory, UPDATE_REQUEST);
|
|
@@ -139,6 +174,7 @@ export function acquirePackageUseLease(prefix, hqVersion, stateDirectory) {
|
|
|
139
174
|
pid: process.pid,
|
|
140
175
|
start_time_ms: Math.floor(performance.timeOrigin),
|
|
141
176
|
hq_version: hqVersion,
|
|
177
|
+
purpose,
|
|
142
178
|
};
|
|
143
179
|
const leasePath = path.join(directory, `${record.pid}-${record.start_time_ms}.json`);
|
|
144
180
|
const tempPath = `${leasePath}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
@@ -272,7 +308,7 @@ function exists(file) {
|
|
|
272
308
|
throw error;
|
|
273
309
|
}
|
|
274
310
|
}
|
|
275
|
-
export function acquireCurrentPackageUseLease() {
|
|
311
|
+
export function acquireCurrentPackageUseLease(argv = process.argv) {
|
|
276
312
|
const pkg = findCliPackage(path.dirname(fileURLToPath(import.meta.url)));
|
|
277
313
|
if (!pkg || isPnpmGlobal(pkg.root) || isBunGlobal(pkg.root))
|
|
278
314
|
return null;
|
|
@@ -285,7 +321,7 @@ export function acquireCurrentPackageUseLease() {
|
|
|
285
321
|
if (nodeModules > 0 && parts[nodeModules - 1] !== "lib")
|
|
286
322
|
return null;
|
|
287
323
|
}
|
|
288
|
-
const lease = acquirePackageUseLease(prefix, pkg.version);
|
|
324
|
+
const lease = acquirePackageUseLease(prefix, pkg.version, undefined, packageUseLeasePurposeForArgv(argv));
|
|
289
325
|
currentPackageUseLease = lease;
|
|
290
326
|
publishPackageUseLeaseHandoff(lease);
|
|
291
327
|
return lease;
|