@indigoai-us/hq-cli 5.290.0 → 5.292.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +66 -3
- package/assets/scaffold/core/scripts/handoff-finalize.sh +18 -0
- package/assets/scaffold/core/scripts/handoff-post.sh +71 -6
- package/assets/scaffold/core/scripts/hook-lib.sh +345 -50
- package/assets/scaffold/core/scripts/hq-delegate-bundle.sh +307 -0
- package/assets/scaffold/core/scripts/hq-delegate-grant.sh +226 -0
- package/assets/scaffold/core/scripts/hq-delegate-publish.sh +91 -0
- package/assets/scaffold/core/scripts/hq-delegate-send.sh +259 -0
- package/assets/scaffold/core/scripts/hq-delegate-verify.sh +213 -0
- package/assets/scaffold/core/scripts/hq-job-notify.sh +389 -0
- package/assets/scaffold/core/scripts/hq-job-probe.sh +835 -0
- package/assets/scaffold/core/scripts/hq-job-remediate.sh +359 -0
- package/assets/scaffold/core/scripts/hq-job-run.sh +1025 -0
- package/assets/scaffold/core/scripts/hq-session.sh +182 -21
- package/assets/scaffold/core/scripts/jobs-schedule-parse.sh +310 -0
- package/assets/scaffold/core/scripts/jobs-validate.sh +451 -0
- package/assets/scaffold/core/scripts/lib/session-id.sh +3 -2
- package/assets/scaffold/core/scripts/lib/work-mesh-enqueue.sh +2 -1
- package/assets/scaffold/core/scripts/outpost-jobs-reconcile.sh +830 -0
- package/dist/command-catalog.generated.d.ts +165 -1
- package/dist/command-catalog.generated.js +205 -1
- package/dist/commands/cloud.d.ts +26 -14
- package/dist/commands/cloud.js +207 -56
- package/dist/commands/core-delegate-w1b.d.ts +8 -0
- package/dist/commands/core-delegate-w1b.js +1049 -0
- package/dist/commands/core-delegate.d.ts +28 -0
- package/dist/commands/core-delegate.js +15 -13
- package/dist/commands/core-handoff.js +152 -23
- package/dist/commands/core-jobs-native.d.ts +23 -0
- package/dist/commands/core-jobs-native.js +1331 -0
- package/dist/commands/core-native-jobs-flag.d.ts +4 -0
- package/dist/commands/core-native-jobs-flag.js +7 -0
- package/dist/commands/core-ontology.d.ts +22 -4
- package/dist/commands/core-ontology.js +90 -8
- package/dist/commands/core-policy.js +24 -5
- package/dist/commands/core.js +17 -0
- package/dist/commands/integrations-core.js +7 -0
- package/dist/commands/lanes.js +111 -35
- package/dist/commands/members.js +5 -0
- package/dist/commands/mesh.js +46 -25
- package/dist/commands/personal-targeted-push-flag.d.ts +4 -0
- package/dist/commands/personal-targeted-push-flag.js +7 -0
- package/dist/commands/personal-targeted-push-paths.d.ts +3 -0
- package/dist/commands/personal-targeted-push-paths.js +67 -0
- package/dist/commands/secrets.js +9 -0
- package/dist/commands/skill.d.ts +3 -0
- package/dist/commands/skill.js +28 -0
- package/dist/fast-core.js +1 -0
- package/dist/lib/billing/free-month.d.ts +18 -0
- package/dist/lib/billing/free-month.js +31 -0
- package/dist/lib/billing/plan-limit-refusals.d.ts +69 -0
- package/dist/lib/billing/plan-limit-refusals.js +187 -0
- package/dist/lib/billing/plan-lock.d.ts +2 -6
- package/dist/lib/billing/plan-lock.js +5 -16
- package/dist/lib/checkpoint-ontology/drop-aliases.d.ts +41 -0
- package/dist/lib/checkpoint-ontology/drop-aliases.js +76 -0
- package/dist/lib/checkpoint-ontology/entity-file.d.ts +6 -3
- package/dist/lib/checkpoint-ontology/entity-file.js +12 -6
- package/dist/lib/checkpoint-ontology/merge-copies.d.ts +27 -8
- package/dist/lib/checkpoint-ontology/merge-copies.js +64 -97
- package/dist/lib/core-utils/secret-patterns.d.ts +3 -1
- package/dist/lib/core-utils/secret-patterns.js +3 -2
- package/dist/lib/daemon/flag.d.ts +2 -0
- package/dist/lib/daemon/flag.js +2 -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/lanes/autopilot.d.ts +69 -0
- package/dist/lib/lanes/autopilot.js +418 -0
- package/dist/lib/lanes/mesh-events.js +2 -0
- package/dist/lib/lanes/store.js +11 -6
- package/dist/lib/mesh/api.js +4 -0
- package/dist/lib/mesh/live/daemon/state.d.ts +7 -1
- package/dist/lib/mesh/live/daemon/state.js +34 -2
- package/dist/main.js +23 -12
- package/dist/utils/plan-gate-error.d.ts +24 -0
- package/dist/utils/plan-gate-error.js +219 -9
- package/dist/utils/scaffold-sibling-references.js +4 -4
- package/dist/utils/self-update.d.ts +6 -0
- package/dist/utils/self-update.js +28 -6
- package/dist/utils/staged-install.d.ts +34 -0
- package/dist/utils/staged-install.js +296 -0
- package/package.json +2 -2
- package/scripts/mesh-daemon-if-stale.mjs +4 -0
package/dist/commands/mesh.js
CHANGED
|
@@ -11,7 +11,7 @@ import { readFileSync } from "node:fs";
|
|
|
11
11
|
import { findHqRoot } from "../utils/manifest.js";
|
|
12
12
|
import { CLI_VERSION } from "../cli-version.js";
|
|
13
13
|
import chalk from "chalk";
|
|
14
|
-
import {
|
|
14
|
+
import { ensureCognitoToken, isExpiring, isMachineIdentity, loadCachedTokens, } from "../utils/cognito-session.js";
|
|
15
15
|
import * as readline from "node:readline/promises";
|
|
16
16
|
import { STORY_STATUSES, appendThreadEvent, ensureProjectThread, eventPayload, getProjectView, listActiveMembershipCompanies, listActiveThreads, patchStory, putProjectView, registerMeshProject, resolveActiveMembershipCompany, resolveMeshPrincipalUid, warmMeshConversationCache, } from "../lib/mesh/api.js";
|
|
17
17
|
import { closeSupersededProjectThread, ensureMissingBoardStories, ensureProjectBoard, boardStoryStatus, formatStoryStatusChanges, mergeProjectStories, prdSyncEventsThatChangeBoard, } from "../lib/mesh/project-board.js";
|
|
@@ -26,7 +26,9 @@ import { CLI_KIND_TO_SCHEMA, resolveEnqueueSessionId, } from "../lib/mesh/live/i
|
|
|
26
26
|
import { flushSessionEvents } from "../lib/mesh/live/flush.js";
|
|
27
27
|
import { backfillHeldSessions } from "../lib/mesh/live/backfill-held.js";
|
|
28
28
|
import { createSessionEventsPoster, resolveVaultApiBase, } from "../lib/mesh/live/session-events-client.js";
|
|
29
|
-
const
|
|
29
|
+
const ONLINE_MEMBERSHIP_VALIDATION_TIMEOUT_MS = 5_000;
|
|
30
|
+
// Leave 500 ms of headroom inside SessionStart's 2 s process-group limit.
|
|
31
|
+
const OFFLINE_MEMBERSHIP_VALIDATION_TIMEOUT_MS = 1_500;
|
|
30
32
|
import { buildEmitEvidence, machineEmitDisposition } from "../lib/mesh/live/emit-evidence.js";
|
|
31
33
|
import { appendHeldLine } from "../lib/mesh/live/spool.js";
|
|
32
34
|
import { createEmitPoster, buildMeshEmitEvent } from "../lib/mesh/live/emit-client.js";
|
|
@@ -860,29 +862,45 @@ async function runContextReconcile(opts) {
|
|
|
860
862
|
let offline = Boolean(opts.offline);
|
|
861
863
|
let token;
|
|
862
864
|
try {
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
865
|
+
if (offline) {
|
|
866
|
+
if (isMachineIdentity()) {
|
|
867
|
+
token = await ensureCognitoToken({ tokenSource: "machine", interactive: false });
|
|
868
|
+
}
|
|
869
|
+
else {
|
|
870
|
+
const cached = loadCachedTokens();
|
|
871
|
+
if (cached && !isExpiring(cached, 120)) {
|
|
872
|
+
token = cached.accessToken;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
else {
|
|
877
|
+
token = await requireToken();
|
|
878
|
+
}
|
|
879
|
+
if (token) {
|
|
880
|
+
const sessionToken = token;
|
|
881
|
+
const membershipValidationTimeoutMs = offline
|
|
882
|
+
? OFFLINE_MEMBERSHIP_VALIDATION_TIMEOUT_MS
|
|
883
|
+
: ONLINE_MEMBERSHIP_VALIDATION_TIMEOUT_MS;
|
|
884
|
+
validateMembership = async (candidate) => {
|
|
885
|
+
const membership = await new Promise((resolve, reject) => {
|
|
886
|
+
const timer = setTimeout(() => reject(new Error("membership validation timed out")), membershipValidationTimeoutMs);
|
|
887
|
+
void resolveActiveMembershipCompany(sessionToken, candidate).then((value) => {
|
|
888
|
+
clearTimeout(timer);
|
|
889
|
+
resolve(value);
|
|
890
|
+
}, (error) => {
|
|
891
|
+
clearTimeout(timer);
|
|
892
|
+
reject(error);
|
|
893
|
+
});
|
|
876
894
|
});
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
895
|
+
if (!membership || typeof membership.companyUid !== "string" || !membership.companyUid) {
|
|
896
|
+
return false;
|
|
897
|
+
}
|
|
898
|
+
return { uid: membership.companyUid };
|
|
899
|
+
};
|
|
900
|
+
if (!offline) {
|
|
901
|
+
deliver = createWorkSessionDeliverer({ token: sessionToken });
|
|
902
|
+
fetchCandidates = createCandidatesFetcher({ token: sessionToken });
|
|
880
903
|
}
|
|
881
|
-
return { uid: membership.companyUid };
|
|
882
|
-
};
|
|
883
|
-
if (!offline) {
|
|
884
|
-
deliver = createWorkSessionDeliverer({ token: sessionToken });
|
|
885
|
-
fetchCandidates = createCandidatesFetcher({ token: sessionToken });
|
|
886
904
|
}
|
|
887
905
|
}
|
|
888
906
|
catch {
|
|
@@ -900,7 +918,10 @@ async function runContextReconcile(opts) {
|
|
|
900
918
|
log: (message) => console.error(chalk.dim(message)),
|
|
901
919
|
});
|
|
902
920
|
if (activation.status === "needs_choice") {
|
|
903
|
-
const interactive = !
|
|
921
|
+
const interactive = !offline &&
|
|
922
|
+
!opts.machine &&
|
|
923
|
+
Boolean(process.stdin.isTTY && process.stdout.isTTY) &&
|
|
924
|
+
!isMachineIdentity();
|
|
904
925
|
if (interactive) {
|
|
905
926
|
const choices = activation.memberships
|
|
906
927
|
.map((membership, index) => `${index + 1}. ${membership.slug}`)
|
|
@@ -920,7 +941,7 @@ async function runContextReconcile(opts) {
|
|
|
920
941
|
rl.close();
|
|
921
942
|
}
|
|
922
943
|
}
|
|
923
|
-
else if (!opts.machine) {
|
|
944
|
+
else if (!offline && !opts.machine) {
|
|
924
945
|
console.error(chalk.yellow("Multiple active memberships. Run `hq mesh context default set <slug>` to choose this device's default company."));
|
|
925
946
|
}
|
|
926
947
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const CLI_PERSONAL_TARGETED_PUSH_PATHS_FLAG = "cli.personal-targeted-push-paths";
|
|
2
|
+
/** The established full-scope behavior remains in force until an explicit hq-flags enablement. */
|
|
3
|
+
export declare function personalTargetedPushPathsEnabled(): boolean;
|
|
4
|
+
//# sourceMappingURL=personal-targeted-push-flag.d.ts.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { resolveProcessFlagGate } from "../lib/flag-registry.js";
|
|
2
|
+
export const CLI_PERSONAL_TARGETED_PUSH_PATHS_FLAG = "cli.personal-targeted-push-paths";
|
|
3
|
+
/** The established full-scope behavior remains in force until an explicit hq-flags enablement. */
|
|
4
|
+
export function personalTargetedPushPathsEnabled() {
|
|
5
|
+
return resolveProcessFlagGate(CLI_PERSONAL_TARGETED_PUSH_PATHS_FLAG, { fallback: false }, () => false);
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=personal-targeted-push-flag.js.map
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Resolve explicit --personal paths against the same roots as a full personal push. */
|
|
2
|
+
export declare function resolveExplicitPersonalPushPaths(hqRoot: string, requestedPaths: readonly string[], cwd?: string): string[];
|
|
3
|
+
//# sourceMappingURL=personal-targeted-push-paths.d.ts.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { computePersonalVaultPaths, matchPersonalVaultExclusion, } from "@indigoai-us/hq-cloud";
|
|
4
|
+
function isWithin(parent, candidate) {
|
|
5
|
+
const relative = path.relative(parent, candidate);
|
|
6
|
+
return relative === "" ||
|
|
7
|
+
(relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
8
|
+
}
|
|
9
|
+
function rejectPath(input, reason) {
|
|
10
|
+
throw new Error(`--personal cannot push path "${input}": ${reason}`);
|
|
11
|
+
}
|
|
12
|
+
/** Resolve explicit --personal paths against the same roots as a full personal push. */
|
|
13
|
+
export function resolveExplicitPersonalPushPaths(hqRoot, requestedPaths, cwd = process.cwd()) {
|
|
14
|
+
let realRoot;
|
|
15
|
+
try {
|
|
16
|
+
realRoot = fs.realpathSync.native(hqRoot);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
const code = error instanceof Error && "code" in error ? String(error.code) : "unavailable";
|
|
20
|
+
throw new Error(`--personal cannot resolve HQ root "${hqRoot}" (${code})`);
|
|
21
|
+
}
|
|
22
|
+
const allowedRoots = computePersonalVaultPaths(realRoot)
|
|
23
|
+
.map((allowedPath) => {
|
|
24
|
+
try {
|
|
25
|
+
return fs.realpathSync.native(allowedPath);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
const code = error instanceof Error && "code" in error ? String(error.code) : "unavailable";
|
|
29
|
+
throw new Error(`--personal personal-vault scope changed at "${allowedPath}" (${code})`);
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
.filter((allowedPath) => isWithin(realRoot, allowedPath));
|
|
33
|
+
const resolved = [];
|
|
34
|
+
for (const input of requestedPaths) {
|
|
35
|
+
const normalizedInput = path.resolve(cwd, input);
|
|
36
|
+
let realPath;
|
|
37
|
+
try {
|
|
38
|
+
realPath = fs.realpathSync.native(normalizedInput);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
const code = error instanceof Error && "code" in error ? String(error.code) : "unavailable";
|
|
42
|
+
rejectPath(input, `path cannot be resolved (${code})`);
|
|
43
|
+
}
|
|
44
|
+
if (!isWithin(realRoot, realPath)) {
|
|
45
|
+
rejectPath(input, "resolved path escapes the HQ root");
|
|
46
|
+
}
|
|
47
|
+
let isDirectory;
|
|
48
|
+
try {
|
|
49
|
+
isDirectory = fs.statSync(realPath).isDirectory();
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
const code = error instanceof Error && "code" in error ? String(error.code) : "unavailable";
|
|
53
|
+
rejectPath(input, `path cannot be inspected (${code})`);
|
|
54
|
+
}
|
|
55
|
+
const relativePath = path.relative(realRoot, realPath).split(path.sep).join("/");
|
|
56
|
+
const exclusion = matchPersonalVaultExclusion(relativePath, isDirectory);
|
|
57
|
+
if (exclusion) {
|
|
58
|
+
rejectPath(input, `excluded by personal-vault rule ${exclusion.id}`);
|
|
59
|
+
}
|
|
60
|
+
if (!allowedRoots.some((allowedRoot) => isWithin(allowedRoot, realPath))) {
|
|
61
|
+
rejectPath(input, "resolved path is outside the personal-vault paths");
|
|
62
|
+
}
|
|
63
|
+
resolved.push(realPath);
|
|
64
|
+
}
|
|
65
|
+
return resolved;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=personal-targeted-push-paths.js.map
|
package/dist/commands/secrets.js
CHANGED
|
@@ -5,6 +5,7 @@ import { cognitoCachePrincipalUid, ensureCognitoToken } from "../utils/cognito-s
|
|
|
5
5
|
import { isAuthError } from "../utils/auth-error.js";
|
|
6
6
|
import { DEFAULT_SECRETS_CACHE_TTL_MS, hasCacheEntry, readCache, writeCache, removeCacheEntry, clearAllCache, } from "../utils/secrets-cache.js";
|
|
7
7
|
import { computeSha256 } from "../utils/integrity.js";
|
|
8
|
+
import { isPlanGateError } from "../utils/plan-gate-error.js";
|
|
8
9
|
import { SECRET_NAME_PATTERN, GROUP_ID_PATTERN, EMAIL_PATTERN } from "./_patterns.js";
|
|
9
10
|
import { describeSecretsScope, formatSecretSaved, formatSecretsListEmpty, formatSecretsListHeader, } from "./secrets-scope.js";
|
|
10
11
|
import { vaultApiFetch, getCompanyUid, getEntityUid, looksLikeCompanyUid, } from "../utils/vault-api.js";
|
|
@@ -988,6 +989,11 @@ export function registerSecretsCommand(program) {
|
|
|
988
989
|
}
|
|
989
990
|
}
|
|
990
991
|
catch (err) {
|
|
992
|
+
// A plan-limit refusal goes to main.ts's shared renderer, which prints
|
|
993
|
+
// the reason, usage, fix options and upgrade link, offers the upgrade
|
|
994
|
+
// interactively, and exits non-zero.
|
|
995
|
+
if (isPlanGateError(err))
|
|
996
|
+
throw err;
|
|
991
997
|
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
992
998
|
process.exit(1);
|
|
993
999
|
}
|
|
@@ -1650,6 +1656,9 @@ export function registerSecretsCommand(program) {
|
|
|
1650
1656
|
console.log(chalk.dim(" One-time use — link is invalidated after first submission."));
|
|
1651
1657
|
}
|
|
1652
1658
|
catch (err) {
|
|
1659
|
+
// Same shared plan-limit renderer as `secrets set` (see main.ts).
|
|
1660
|
+
if (isPlanGateError(err))
|
|
1661
|
+
throw err;
|
|
1653
1662
|
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
1654
1663
|
process.exit(1);
|
|
1655
1664
|
}
|
package/dist/commands/skill.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { Command } from "commander";
|
|
|
18
18
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
19
19
|
import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
|
|
20
20
|
import { surfaceCompanySkill } from "../lib/company-skill-wrapper.js";
|
|
21
|
+
import { type PlanLimitRefusalSummary } from "../lib/billing/plan-limit-refusals.js";
|
|
21
22
|
export declare const SKILL_UID_PATTERN: RegExp;
|
|
22
23
|
export declare const SKILL_SLUG_PATTERN: RegExp;
|
|
23
24
|
interface SkillSyncInput {
|
|
@@ -30,6 +31,8 @@ interface SkillSyncResult {
|
|
|
30
31
|
filesUploaded: number;
|
|
31
32
|
filesSkipped: number;
|
|
32
33
|
aborted: boolean;
|
|
34
|
+
/** Set when a plan limit refused the upload; absent or null otherwise. */
|
|
35
|
+
planLimit?: PlanLimitRefusalSummary | null;
|
|
33
36
|
}
|
|
34
37
|
export declare function parseSkillUid(markdown: string): string | undefined;
|
|
35
38
|
export declare function readActiveCompanySlug(hqRoot: string): string | undefined;
|
package/dist/commands/skill.js
CHANGED
|
@@ -27,25 +27,47 @@ import { surfaceCompanySkill } from "../lib/company-skill-wrapper.js";
|
|
|
27
27
|
import { AuthError } from "../utils/auth-error.js";
|
|
28
28
|
import { redactErrorText } from "../utils/redact-error-text.js";
|
|
29
29
|
import { stampVaultAccessDenied, vaultAccessDeniedDiagnostics, } from "../utils/vault-access-denied-error.js";
|
|
30
|
+
import { PlanGateError } from "../utils/plan-gate-error.js";
|
|
31
|
+
import { createPlanLimitRefusalTracker, trackingProgressRecorder, } from "../lib/billing/plan-limit-refusals.js";
|
|
30
32
|
export const SKILL_UID_PATTERN = /^skl_[A-Za-z0-9]+$/;
|
|
31
33
|
export const SKILL_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
32
34
|
const COMPANY_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
33
35
|
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
|
|
34
36
|
const SHA256_HEX_PATTERN = /^[0-9a-f]{64}$/;
|
|
35
37
|
async function defaultSyncFile(input) {
|
|
38
|
+
// The runner reports a plan-limit refusal as an event, not a throw, and still
|
|
39
|
+
// returns normally. Track it so the command cannot claim "Sync: complete".
|
|
40
|
+
const tracker = createPlanLimitRefusalTracker({ companySlug: input.companySlug });
|
|
36
41
|
const result = await share({
|
|
37
42
|
paths: [input.filePath],
|
|
38
43
|
company: input.companySlug,
|
|
39
44
|
hqRoot: input.hqRoot,
|
|
40
45
|
vaultConfig: buildVaultConfig(input.token),
|
|
41
46
|
onConflict: "abort",
|
|
47
|
+
progressRecorder: trackingProgressRecorder(tracker, input.companySlug),
|
|
42
48
|
});
|
|
43
49
|
return {
|
|
44
50
|
filesUploaded: result.filesUploaded,
|
|
45
51
|
filesSkipped: result.filesSkipped,
|
|
46
52
|
aborted: result.aborted,
|
|
53
|
+
planLimit: tracker.summary(),
|
|
47
54
|
};
|
|
48
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* The upload of a stamped skill file was refused by a plan limit. Registration
|
|
58
|
+
* and the local stamp already succeeded, so say that on stderr, then return a
|
|
59
|
+
* typed plan gate for main.ts to render (reason and upgrade link) and exit
|
|
60
|
+
* non-zero. Never "Skill ready … Sync: complete".
|
|
61
|
+
*/
|
|
62
|
+
function skillUploadRefused(skillUid, filePath, companySlug, refused) {
|
|
63
|
+
console.warn(chalk.yellow(`⚠ Skill ${skillUid} is stamped locally at '${filePath}', but the upload was refused by your plan limit.`));
|
|
64
|
+
return new PlanGateError("PLAN_LIMIT_EXCEEDED", {
|
|
65
|
+
reason: `${refused.message ?? "New files are paused by your plan limit."} ` +
|
|
66
|
+
"The skill file was not uploaded. Once the limit is sorted, upload it with: " +
|
|
67
|
+
`hq sync push --company ${companySlug} '${filePath}'`,
|
|
68
|
+
...(refused.upgradeUrl ? { upgradeUrl: refused.upgradeUrl } : {}),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
49
71
|
export function parseSkillUid(markdown) {
|
|
50
72
|
const match = markdown.match(FRONTMATTER_RE);
|
|
51
73
|
if (!match)
|
|
@@ -575,6 +597,9 @@ export function registerSkillCommand(program, deps = {}) {
|
|
|
575
597
|
// Sentry rather than filing a crash on its first occurrence.
|
|
576
598
|
throw localSkillError(`Skill ${registered.skillUid} is stamped locally at '${filePath}', but sync aborted because the remote file conflicts.`);
|
|
577
599
|
}
|
|
600
|
+
if (syncResult.planLimit) {
|
|
601
|
+
throw skillUploadRefused(registered.skillUid, filePath, companySlug, syncResult.planLimit);
|
|
602
|
+
}
|
|
578
603
|
}
|
|
579
604
|
console.log(chalk.green(`Skill ready: ${registered.skillUid}`));
|
|
580
605
|
console.log(` File: ${filePath}`);
|
|
@@ -739,6 +764,9 @@ export function registerSkillCommand(program, deps = {}) {
|
|
|
739
764
|
if (syncResult.aborted) {
|
|
740
765
|
throw localSkillError(`Skill ${skillUid} is stamped locally at '${canonicalPath}', but sync aborted because the remote file conflicts.`);
|
|
741
766
|
}
|
|
767
|
+
if (syncResult.planLimit) {
|
|
768
|
+
throw skillUploadRefused(skillUid, canonicalPath, companySlug, syncResult.planLimit);
|
|
769
|
+
}
|
|
742
770
|
}
|
|
743
771
|
console.log(chalk.green(`Skill promoted: ${skillUid}`));
|
|
744
772
|
console.log(` File: ${canonicalPath}`);
|
package/dist/fast-core.js
CHANGED
|
@@ -54,6 +54,7 @@ export async function runFastCore(argv) {
|
|
|
54
54
|
args: parsed.args,
|
|
55
55
|
cwd: process.cwd(),
|
|
56
56
|
hqRoot,
|
|
57
|
+
env: parsed.entry.name === "hq-session" ? { HQ_HQ_SESSION_NO_CLI: "1" } : undefined,
|
|
57
58
|
});
|
|
58
59
|
// Propagate verbatim: these scripts use their exit code as their interface.
|
|
59
60
|
process.exit(code);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The free-month offer hq-pro attaches to Starter plan-limit payloads (the
|
|
3
|
+
* usage-limits nag and the hard-stop 402 body). Kept in a leaf module with no
|
|
4
|
+
* imports so the plan-gate error decoder can reuse the one parser without
|
|
5
|
+
* pulling in the HTTP layer that itself imports the decoder.
|
|
6
|
+
*/
|
|
7
|
+
export interface PlanLimitFreeMonth {
|
|
8
|
+
eligible: boolean;
|
|
9
|
+
trialDays?: number;
|
|
10
|
+
url?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Parse `freeMonth` off a server payload. Anything but an explicit
|
|
14
|
+
* `eligible: true` is "no offer": an absent or malformed field never invents
|
|
15
|
+
* one.
|
|
16
|
+
*/
|
|
17
|
+
export declare function parseFreeMonth(value: unknown): PlanLimitFreeMonth | undefined;
|
|
18
|
+
//# sourceMappingURL=free-month.d.ts.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The free-month offer hq-pro attaches to Starter plan-limit payloads (the
|
|
3
|
+
* usage-limits nag and the hard-stop 402 body). Kept in a leaf module with no
|
|
4
|
+
* imports so the plan-gate error decoder can reuse the one parser without
|
|
5
|
+
* pulling in the HTTP layer that itself imports the decoder.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Parse `freeMonth` off a server payload. Anything but an explicit
|
|
9
|
+
* `eligible: true` is "no offer": an absent or malformed field never invents
|
|
10
|
+
* one.
|
|
11
|
+
*/
|
|
12
|
+
export function parseFreeMonth(value) {
|
|
13
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
const rec = value;
|
|
17
|
+
if (rec.eligible !== true)
|
|
18
|
+
return undefined;
|
|
19
|
+
const url = typeof rec.url === "string" && rec.url.trim().length > 0
|
|
20
|
+
? rec.url.trim()
|
|
21
|
+
: undefined;
|
|
22
|
+
const trialDays = typeof rec.trialDays === "number" && Number.isFinite(rec.trialDays)
|
|
23
|
+
? rec.trialDays
|
|
24
|
+
: undefined;
|
|
25
|
+
return {
|
|
26
|
+
eligible: true,
|
|
27
|
+
...(trialDays !== undefined ? { trialDays } : {}),
|
|
28
|
+
...(url ? { url } : {}),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=free-month.js.map
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collect the files a sync push was refused by a plan limit, from the surfaces
|
|
3
|
+
* the pinned `@indigoai-us/hq-cloud` runner already has, so `hq sync push`,
|
|
4
|
+
* `hq sync now` and `hq skill create` never print a success line over a
|
|
5
|
+
* refusal.
|
|
6
|
+
*
|
|
7
|
+
* The runner reports a refusal two ways today:
|
|
8
|
+
*
|
|
9
|
+
* 1. A `{ type: "plan-limit", upgradeUrl }` event, emitted once per push pass
|
|
10
|
+
* when a presign row carried a code it recognises. It names no path.
|
|
11
|
+
* 2. A per-file `{ type: "error", path, message }` event for a 402 it does not
|
|
12
|
+
* recognise (hq-pro's hard-stop body). The message is hq-cloud's
|
|
13
|
+
* `describeError` string, which carries `http=402` and, when the server
|
|
14
|
+
* sent one, `code=…`.
|
|
15
|
+
*
|
|
16
|
+
* `@indigoai-us/hq-cloud` 6.18.17 (US-012) recognises the hard-stop body,
|
|
17
|
+
* names each refused path on the `plan-limit` event (`paths`, `count`) and
|
|
18
|
+
* stops re-sending paused keys. The `path`/`paths`/`count` reads below consume
|
|
19
|
+
* that event; the `error`-message parsing stays for runners older than
|
|
20
|
+
* 6.18.17.
|
|
21
|
+
*/
|
|
22
|
+
import { type SyncProgressEvent } from "@indigoai-us/hq-cloud";
|
|
23
|
+
/** What one push leg was refused. `null` from the tracker means nothing was. */
|
|
24
|
+
export interface PlanLimitRefusalSummary {
|
|
25
|
+
/**
|
|
26
|
+
* Refused files the runner reported. A `plan-limit` event with no path or
|
|
27
|
+
* count still means at least one file, so this is never 0.
|
|
28
|
+
*/
|
|
29
|
+
count: number;
|
|
30
|
+
/** Vault-relative paths the runner named, de-duplicated, in report order. */
|
|
31
|
+
paths: string[];
|
|
32
|
+
/** Server sentence from a refused file's error, when one was carried. */
|
|
33
|
+
message?: string;
|
|
34
|
+
/** Server-selected (or company billing fallback) upgrade link. */
|
|
35
|
+
upgradeUrl?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface PlanLimitRefusalTracker {
|
|
38
|
+
observe(event: SyncProgressEvent): void;
|
|
39
|
+
summary(): PlanLimitRefusalSummary | null;
|
|
40
|
+
}
|
|
41
|
+
/** True when a runner `error` message is a plan-limit (402) refusal. */
|
|
42
|
+
export declare function isPlanLimitErrorMessage(message: unknown): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* @param opts.companySlug the push target's slug, used for the company billing
|
|
45
|
+
* link when the runner did not carry one. Omit for a personal-vault leg.
|
|
46
|
+
*/
|
|
47
|
+
export declare function createPlanLimitRefusalTracker(opts?: {
|
|
48
|
+
companySlug?: string;
|
|
49
|
+
}): PlanLimitRefusalTracker;
|
|
50
|
+
/**
|
|
51
|
+
* The `progressRecorder` for a share() call: feeds the tracker and still writes
|
|
52
|
+
* the shared progress snapshot, exactly as share()'s default recorder does.
|
|
53
|
+
* Passing the recorder rather than `onEvent` keeps share()'s own console output
|
|
54
|
+
* unchanged. `company` is the snapshot label: the target slug, or null for the
|
|
55
|
+
* personal vault.
|
|
56
|
+
*/
|
|
57
|
+
export declare function trackingProgressRecorder(tracker: PlanLimitRefusalTracker, company: string | null): (event: SyncProgressEvent) => void;
|
|
58
|
+
/** Add a later leg's refusals to an earlier total. */
|
|
59
|
+
export declare function mergePlanLimitRefusals(a: PlanLimitRefusalSummary | null | undefined, b: PlanLimitRefusalSummary | null | undefined): PlanLimitRefusalSummary | null;
|
|
60
|
+
/**
|
|
61
|
+
* Plain lines describing a refusal: what was not uploaded, why, and the
|
|
62
|
+
* upgrade link. Colour and the leading status line are the caller's.
|
|
63
|
+
*/
|
|
64
|
+
export declare function renderPlanLimitRefusalLines(summary: PlanLimitRefusalSummary, indent?: string): string[];
|
|
65
|
+
/** "1 new file" / "3 new files", or "new files" when only a notice said so. */
|
|
66
|
+
export declare function describeRefusedCount(summary: PlanLimitRefusalSummary): string;
|
|
67
|
+
/** {@link describeRefusedCount} with its verb: "1 new file was", "new files were". */
|
|
68
|
+
export declare function describeRefusedFiles(summary: PlanLimitRefusalSummary): string;
|
|
69
|
+
//# sourceMappingURL=plan-limit-refusals.d.ts.map
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collect the files a sync push was refused by a plan limit, from the surfaces
|
|
3
|
+
* the pinned `@indigoai-us/hq-cloud` runner already has, so `hq sync push`,
|
|
4
|
+
* `hq sync now` and `hq skill create` never print a success line over a
|
|
5
|
+
* refusal.
|
|
6
|
+
*
|
|
7
|
+
* The runner reports a refusal two ways today:
|
|
8
|
+
*
|
|
9
|
+
* 1. A `{ type: "plan-limit", upgradeUrl }` event, emitted once per push pass
|
|
10
|
+
* when a presign row carried a code it recognises. It names no path.
|
|
11
|
+
* 2. A per-file `{ type: "error", path, message }` event for a 402 it does not
|
|
12
|
+
* recognise (hq-pro's hard-stop body). The message is hq-cloud's
|
|
13
|
+
* `describeError` string, which carries `http=402` and, when the server
|
|
14
|
+
* sent one, `code=…`.
|
|
15
|
+
*
|
|
16
|
+
* `@indigoai-us/hq-cloud` 6.18.17 (US-012) recognises the hard-stop body,
|
|
17
|
+
* names each refused path on the `plan-limit` event (`paths`, `count`) and
|
|
18
|
+
* stops re-sending paused keys. The `path`/`paths`/`count` reads below consume
|
|
19
|
+
* that event; the `error`-message parsing stays for runners older than
|
|
20
|
+
* 6.18.17.
|
|
21
|
+
*/
|
|
22
|
+
import { createSyncProgressRecorder, } from "@indigoai-us/hq-cloud";
|
|
23
|
+
import { planLimitUpgradeFallbackUrl } from "./plan-limit-upgrade-url.js";
|
|
24
|
+
/** Upgrade destination when neither the event nor the error named one. */
|
|
25
|
+
const FALLBACK_UPGRADE_URL = "https://hq.computer/billing";
|
|
26
|
+
const PLAN_LIMIT_CODE_TOKEN = /\bcode=(?:PLAN_LIMIT_EXCEEDED|PLAN_LIMIT_REACHED|plan_limit_reached)\b/;
|
|
27
|
+
const HTTP_402_TOKEN = /\bhttp=402\b/;
|
|
28
|
+
const DIAGNOSTIC_TOKEN = /^(?:[A-Za-z]+Error|code=\S+|path=\S+|http=\d+|syscall=\S+|cause=\S+|causeHttp=\d+|host=\S+)$/;
|
|
29
|
+
/** True when a runner `error` message is a plan-limit (402) refusal. */
|
|
30
|
+
export function isPlanLimitErrorMessage(message) {
|
|
31
|
+
return (typeof message === "string" &&
|
|
32
|
+
(PLAN_LIMIT_CODE_TOKEN.test(message) || HTTP_402_TOKEN.test(message)));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The human sentence at the end of a `describeError` string, after its
|
|
36
|
+
* `Name code=… path=… http=…` diagnostic prefix. Everything up to the `http=`
|
|
37
|
+
* token is dropped first, because a `path=` value may itself contain spaces.
|
|
38
|
+
* Undefined when nothing but diagnostics (or a machine token) is left.
|
|
39
|
+
*/
|
|
40
|
+
function serverSentence(message) {
|
|
41
|
+
const http = /\bhttp=\d+\s+/.exec(message);
|
|
42
|
+
const rest = http ? message.slice(http.index + http[0].length) : message;
|
|
43
|
+
const words = rest.trim().split(/\s+/);
|
|
44
|
+
let index = 0;
|
|
45
|
+
while (index < words.length && DIAGNOSTIC_TOKEN.test(words[index]))
|
|
46
|
+
index++;
|
|
47
|
+
const sentence = words.slice(index).join(" ").trim();
|
|
48
|
+
if (!sentence || sentence === "plan_limit_reached")
|
|
49
|
+
return undefined;
|
|
50
|
+
return sentence;
|
|
51
|
+
}
|
|
52
|
+
function safeHttpsUrl(value) {
|
|
53
|
+
if (typeof value !== "string" || value.trim() !== value)
|
|
54
|
+
return undefined;
|
|
55
|
+
try {
|
|
56
|
+
const url = new URL(value);
|
|
57
|
+
return url.protocol === "https:" && !url.username && !url.password
|
|
58
|
+
? value
|
|
59
|
+
: undefined;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* @param opts.companySlug the push target's slug, used for the company billing
|
|
67
|
+
* link when the runner did not carry one. Omit for a personal-vault leg.
|
|
68
|
+
*/
|
|
69
|
+
export function createPlanLimitRefusalTracker(opts = {}) {
|
|
70
|
+
const paths = [];
|
|
71
|
+
let noticeSeen = false;
|
|
72
|
+
let reportedCount = 0;
|
|
73
|
+
let message;
|
|
74
|
+
let upgradeUrl;
|
|
75
|
+
const addPath = (value) => {
|
|
76
|
+
if (typeof value === "string" && value && !paths.includes(value)) {
|
|
77
|
+
paths.push(value);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
return {
|
|
81
|
+
observe(event) {
|
|
82
|
+
const record = event;
|
|
83
|
+
if (event.type === "plan-limit") {
|
|
84
|
+
noticeSeen = true;
|
|
85
|
+
upgradeUrl ??= safeHttpsUrl(record.upgradeUrl);
|
|
86
|
+
addPath(record.path);
|
|
87
|
+
if (Array.isArray(record.paths))
|
|
88
|
+
record.paths.forEach(addPath);
|
|
89
|
+
if (typeof record.count === "number" && Number.isFinite(record.count)) {
|
|
90
|
+
reportedCount += Math.max(0, Math.trunc(record.count));
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (event.type === "error" &&
|
|
95
|
+
record.diagnostic !== true &&
|
|
96
|
+
isPlanLimitErrorMessage(record.message)) {
|
|
97
|
+
noticeSeen = true;
|
|
98
|
+
addPath(record.path);
|
|
99
|
+
message ??= serverSentence(record.message);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
summary() {
|
|
103
|
+
if (!noticeSeen)
|
|
104
|
+
return null;
|
|
105
|
+
const fallbackUrl = opts.companySlug
|
|
106
|
+
? planLimitUpgradeFallbackUrl(opts.companySlug, FALLBACK_UPGRADE_URL)
|
|
107
|
+
: undefined;
|
|
108
|
+
const url = upgradeUrl ?? fallbackUrl;
|
|
109
|
+
return {
|
|
110
|
+
count: Math.max(1, paths.length, reportedCount),
|
|
111
|
+
paths: [...paths],
|
|
112
|
+
...(message ? { message } : {}),
|
|
113
|
+
...(url ? { upgradeUrl: url } : {}),
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The `progressRecorder` for a share() call: feeds the tracker and still writes
|
|
120
|
+
* the shared progress snapshot, exactly as share()'s default recorder does.
|
|
121
|
+
* Passing the recorder rather than `onEvent` keeps share()'s own console output
|
|
122
|
+
* unchanged. `company` is the snapshot label: the target slug, or null for the
|
|
123
|
+
* personal vault.
|
|
124
|
+
*/
|
|
125
|
+
export function trackingProgressRecorder(tracker, company) {
|
|
126
|
+
const record = createSyncProgressRecorder({ company, phase: "push" });
|
|
127
|
+
return (event) => {
|
|
128
|
+
tracker.observe(event);
|
|
129
|
+
record(event);
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/** Add a later leg's refusals to an earlier total. */
|
|
133
|
+
export function mergePlanLimitRefusals(a, b) {
|
|
134
|
+
if (!a)
|
|
135
|
+
return b ?? null;
|
|
136
|
+
if (!b)
|
|
137
|
+
return a;
|
|
138
|
+
const paths = [...a.paths];
|
|
139
|
+
for (const p of b.paths)
|
|
140
|
+
if (!paths.includes(p))
|
|
141
|
+
paths.push(p);
|
|
142
|
+
const message = a.message ?? b.message;
|
|
143
|
+
const upgradeUrl = a.upgradeUrl ?? b.upgradeUrl;
|
|
144
|
+
return {
|
|
145
|
+
count: a.count + b.count,
|
|
146
|
+
paths,
|
|
147
|
+
...(message ? { message } : {}),
|
|
148
|
+
...(upgradeUrl ? { upgradeUrl } : {}),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/** How many named paths to print before summarising the rest. */
|
|
152
|
+
const MAX_LISTED_PATHS = 20;
|
|
153
|
+
/**
|
|
154
|
+
* Plain lines describing a refusal: what was not uploaded, why, and the
|
|
155
|
+
* upgrade link. Colour and the leading status line are the caller's.
|
|
156
|
+
*/
|
|
157
|
+
export function renderPlanLimitRefusalLines(summary, indent = " ") {
|
|
158
|
+
const lines = [];
|
|
159
|
+
lines.push(`${indent}${summary.message ?? "New files are paused by your plan limit."}`);
|
|
160
|
+
if (summary.paths.length > 0) {
|
|
161
|
+
lines.push(`${indent}Not uploaded:`);
|
|
162
|
+
for (const p of summary.paths.slice(0, MAX_LISTED_PATHS)) {
|
|
163
|
+
lines.push(`${indent} - ${p}`);
|
|
164
|
+
}
|
|
165
|
+
const unnamed = summary.count - Math.min(summary.paths.length, MAX_LISTED_PATHS);
|
|
166
|
+
if (unnamed > 0)
|
|
167
|
+
lines.push(`${indent} … and ${unnamed} more`);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
lines.push(`${indent}New files stay on this machine and were not uploaded.`);
|
|
171
|
+
}
|
|
172
|
+
if (summary.upgradeUrl)
|
|
173
|
+
lines.push(`${indent}Upgrade: ${summary.upgradeUrl}`);
|
|
174
|
+
return lines;
|
|
175
|
+
}
|
|
176
|
+
/** "1 new file" / "3 new files", or "new files" when only a notice said so. */
|
|
177
|
+
export function describeRefusedCount(summary) {
|
|
178
|
+
if (summary.paths.length === 0 && summary.count <= 1)
|
|
179
|
+
return "new files";
|
|
180
|
+
return `${summary.count} new file${summary.count === 1 ? "" : "s"}`;
|
|
181
|
+
}
|
|
182
|
+
/** {@link describeRefusedCount} with its verb: "1 new file was", "new files were". */
|
|
183
|
+
export function describeRefusedFiles(summary) {
|
|
184
|
+
const subject = describeRefusedCount(summary);
|
|
185
|
+
return `${subject} ${subject === "1 new file" ? "was" : "were"}`;
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=plan-limit-refusals.js.map
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
* An absent field is UNKNOWN, so nothing here ever infers over-limit from
|
|
21
21
|
* missing data (hq-absent-field-never-means-constraining-value).
|
|
22
22
|
*/
|
|
23
|
+
import { parseFreeMonth, type PlanLimitFreeMonth } from "./free-month.js";
|
|
24
|
+
export { parseFreeMonth, type PlanLimitFreeMonth };
|
|
23
25
|
/**
|
|
24
26
|
* Dimensions the bind notice names with a remedy. Owner decision 7 (2026-09-17)
|
|
25
27
|
* fixes the nagging set at these four: `deployments` and `storageBytes` are
|
|
@@ -38,11 +40,6 @@ export interface PlanLimitMembers {
|
|
|
38
40
|
}
|
|
39
41
|
/** @deprecated US-040 — alias of {@link PlanLimitMembers}. */
|
|
40
42
|
export type PlanLockMembers = PlanLimitMembers;
|
|
41
|
-
export interface PlanLimitFreeMonth {
|
|
42
|
-
eligible: boolean;
|
|
43
|
-
trialDays?: number;
|
|
44
|
-
url?: string;
|
|
45
|
-
}
|
|
46
43
|
export interface PlanLimitStatus {
|
|
47
44
|
companySlug: string;
|
|
48
45
|
companyUid?: string;
|
|
@@ -78,7 +75,6 @@ export declare const STARTER_AGENT_TARGET = 0;
|
|
|
78
75
|
export declare const DEFAULT_UPGRADE_URL = "https://hq.computer/billing";
|
|
79
76
|
/** The paid plan the nag sends owners to. Copy lives in ONE place. */
|
|
80
77
|
export declare const WORKFORCE_PLAN_LABEL = "HQ Workforce ($500/mo)";
|
|
81
|
-
export declare function parseFreeMonth(value: unknown): PlanLimitFreeMonth | undefined;
|
|
82
78
|
/**
|
|
83
79
|
* Pick the membership row for `companySlug` (or `cmp_…` uid) out of a decoded
|
|
84
80
|
* `/membership/me` body. No matching row is UNKNOWN. Does not read `planLock`.
|