@indigoai-us/hq-cli 5.62.1 → 5.63.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/dist/commands/agents.d.ts +22 -1
- package/dist/commands/agents.js +98 -4
- package/dist/commands/billing.d.ts +17 -0
- package/dist/commands/billing.js +114 -0
- package/dist/commands/cloud.d.ts +11 -0
- package/dist/commands/cloud.js +40 -2
- package/dist/commands/members.d.ts +12 -1
- package/dist/commands/members.js +79 -12
- package/dist/commands/outposts.d.ts +18 -1
- package/dist/commands/outposts.js +91 -4
- package/dist/main.js +7 -2
- package/dist/utils/billing-gate.d.ts +77 -0
- package/dist/utils/billing-gate.js +127 -0
- package/package.json +2 -2
- package/src/commands/agents.test.ts +84 -0
- package/src/commands/agents.ts +157 -1
- package/src/commands/billing.test.ts +158 -0
- package/src/commands/billing.ts +146 -0
- package/src/commands/cloud.pull-all.test.ts +53 -0
- package/src/commands/cloud.ts +53 -0
- package/src/commands/members.test.ts +78 -0
- package/src/commands/members.ts +108 -11
- package/src/commands/outposts.test.ts +70 -0
- package/src/commands/outposts.ts +128 -2
- package/src/main.ts +6 -0
- package/src/utils/billing-gate.test.ts +95 -0
- package/src/utils/billing-gate.ts +182 -0
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* the caller's single active membership (same as `members.ts`).
|
|
23
23
|
*/
|
|
24
24
|
import { Command } from "commander";
|
|
25
|
+
import { type BillingErrorPayload } from "../utils/billing-gate.js";
|
|
25
26
|
/** Reasoning-effort values hq-pro accepts on `runtime-config`. */
|
|
26
27
|
export declare const VALID_EFFORTS: Set<string>;
|
|
27
28
|
/** Service-tier (speed) values hq-pro accepts on `runtime-config`. */
|
|
@@ -35,7 +36,9 @@ export declare const VALID_TIERS: Set<string>;
|
|
|
35
36
|
export declare class AgentsHttpError extends Error {
|
|
36
37
|
status: number;
|
|
37
38
|
code?: string;
|
|
38
|
-
|
|
39
|
+
/** hq-pro's billing envelope on a `402 billing_required` provision block. */
|
|
40
|
+
billing?: BillingErrorPayload;
|
|
41
|
+
constructor(status: number, message: string, code?: string, billing?: BillingErrorPayload);
|
|
39
42
|
}
|
|
40
43
|
/** Roster row from `GET /v1/agents` — a superset is returned; we keep what we render. */
|
|
41
44
|
export interface CompanyAgentView {
|
|
@@ -79,6 +82,24 @@ export declare function agentsRequest<T>(opts: {
|
|
|
79
82
|
body?: Record<string, unknown>;
|
|
80
83
|
query?: Record<string, string>;
|
|
81
84
|
}): Promise<T>;
|
|
85
|
+
/** Slugify an agent name into a lowercase, hyphen-separated slug. */
|
|
86
|
+
export declare function slugifyAgentName(name: string): string;
|
|
87
|
+
export interface ProvisionAgentInput {
|
|
88
|
+
companyUid: string;
|
|
89
|
+
name: string;
|
|
90
|
+
slug: string;
|
|
91
|
+
codexAuthMode: "subscription" | "apiKey";
|
|
92
|
+
provider?: "codex" | "grok";
|
|
93
|
+
codexApiKey?: string;
|
|
94
|
+
idempotencyKey: string;
|
|
95
|
+
title?: string;
|
|
96
|
+
description?: string;
|
|
97
|
+
}
|
|
98
|
+
export declare function provisionAgent(token: string, input: ProvisionAgentInput): Promise<{
|
|
99
|
+
uid?: string;
|
|
100
|
+
slug?: string;
|
|
101
|
+
[key: string]: unknown;
|
|
102
|
+
}>;
|
|
82
103
|
export declare function listAgents(token: string, companyUid: string): Promise<CompanyAgentView[]>;
|
|
83
104
|
export declare function getAgentStatus(token: string, agentUid: string): Promise<Record<string, unknown>>;
|
|
84
105
|
export declare function patchAgentProfile(token: string, agentUid: string, patch: ProfilePatch): Promise<{
|
package/dist/commands/agents.js
CHANGED
|
@@ -22,10 +22,12 @@
|
|
|
22
22
|
* the caller's single active membership (same as `members.ts`).
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
25
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="581b1950-a9bc-50a4-b16b-6e6649e9db30")}catch(e){}}();
|
|
26
26
|
import chalk from "chalk";
|
|
27
|
+
import { randomUUID } from "node:crypto";
|
|
27
28
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
28
29
|
import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
|
|
30
|
+
import { AGENT_PRICE_CENTS, confirmChargeOrExit, parseBillingPayload, surfaceBillingRequired, } from "../utils/billing-gate.js";
|
|
29
31
|
/** Reasoning-effort values hq-pro accepts on `runtime-config`. */
|
|
30
32
|
export const VALID_EFFORTS = new Set([
|
|
31
33
|
"minimal",
|
|
@@ -45,11 +47,14 @@ export const VALID_TIERS = new Set(["default", "priority"]);
|
|
|
45
47
|
export class AgentsHttpError extends Error {
|
|
46
48
|
status;
|
|
47
49
|
code;
|
|
48
|
-
|
|
50
|
+
/** hq-pro's billing envelope on a `402 billing_required` provision block. */
|
|
51
|
+
billing;
|
|
52
|
+
constructor(status, message, code, billing) {
|
|
49
53
|
super(message);
|
|
50
54
|
this.name = "AgentsHttpError";
|
|
51
55
|
this.status = status;
|
|
52
56
|
this.code = code;
|
|
57
|
+
this.billing = billing;
|
|
53
58
|
}
|
|
54
59
|
}
|
|
55
60
|
/**
|
|
@@ -61,10 +66,26 @@ export async function agentsRequest(opts) {
|
|
|
61
66
|
const res = await vaultApiFetch(opts);
|
|
62
67
|
if (!res.ok) {
|
|
63
68
|
const body = (await res.json().catch(() => ({})));
|
|
64
|
-
throw new AgentsHttpError(res.status, body.error ?? body.message ?? res.statusText, body.code);
|
|
69
|
+
throw new AgentsHttpError(res.status, body.error ?? body.message ?? res.statusText, body.code, parseBillingPayload(body));
|
|
65
70
|
}
|
|
66
71
|
return (await res.json());
|
|
67
72
|
}
|
|
73
|
+
/** Slugify an agent name into a lowercase, hyphen-separated slug. */
|
|
74
|
+
export function slugifyAgentName(name) {
|
|
75
|
+
return name
|
|
76
|
+
.toLowerCase()
|
|
77
|
+
.trim()
|
|
78
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
79
|
+
.replace(/^-+|-+$/g, "");
|
|
80
|
+
}
|
|
81
|
+
export async function provisionAgent(token, input) {
|
|
82
|
+
return agentsRequest({
|
|
83
|
+
token,
|
|
84
|
+
path: "/v1/agents",
|
|
85
|
+
method: "POST",
|
|
86
|
+
body: input,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
68
89
|
export async function listAgents(token, companyUid) {
|
|
69
90
|
const data = await agentsRequest({
|
|
70
91
|
token,
|
|
@@ -144,6 +165,79 @@ export function registerAgentsCommand(program) {
|
|
|
144
165
|
// `hq agents --company acme list`.
|
|
145
166
|
const companyOf = (sub) => sub.opts().company ??
|
|
146
167
|
agents.opts().company;
|
|
168
|
+
agents
|
|
169
|
+
.command("provision <name>")
|
|
170
|
+
.alias("new")
|
|
171
|
+
.description("Provision a new cloud agent ($100/month — requires --yes)")
|
|
172
|
+
.option("--company <slug>", "Company slug (resolves to companyUid)")
|
|
173
|
+
.option("--slug <slug>", "Agent slug (defaults to a slug of <name>)")
|
|
174
|
+
.option("--provider <provider>", "Runtime: codex | grok (default codex)")
|
|
175
|
+
.option("--auth-mode <mode>", "Codex auth: subscription | apiKey (default subscription)", "subscription")
|
|
176
|
+
.option("--api-key-env <VAR>", "Env var holding the API key for --auth-mode apiKey (never pass the key as a flag)")
|
|
177
|
+
.option("--title <title>", "Org-chart job title")
|
|
178
|
+
.option("--description <text>", "Short description / bio")
|
|
179
|
+
.option("--yes", "Confirm the $100/month charge (required to provision)")
|
|
180
|
+
.action(async function (name, opts) {
|
|
181
|
+
try {
|
|
182
|
+
const authMode = opts.authMode === "apiKey" ? "apiKey" : "subscription";
|
|
183
|
+
const provider = opts.provider === "grok" ? "grok" : opts.provider ? "codex" : undefined;
|
|
184
|
+
// Resolve the API key from the environment (never from a flag value) when
|
|
185
|
+
// apiKey mode is requested — validated BEFORE the paid gate so we don't
|
|
186
|
+
// charge and then fail.
|
|
187
|
+
let codexApiKey;
|
|
188
|
+
if (authMode === "apiKey") {
|
|
189
|
+
const envVar = opts.apiKeyEnv;
|
|
190
|
+
if (!envVar) {
|
|
191
|
+
console.error(chalk.red("--auth-mode apiKey requires --api-key-env <VAR> naming the env var that holds the key."));
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
codexApiKey = process.env[envVar];
|
|
195
|
+
if (!codexApiKey) {
|
|
196
|
+
console.error(chalk.red(`Env var ${envVar} is empty or unset — no API key to use.`));
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const token = await ensureCognitoToken();
|
|
201
|
+
const companyUid = await getCompanyUid(token, companyOf(this));
|
|
202
|
+
// Paid gate: print the monthly cost and require --yes before any call.
|
|
203
|
+
confirmChargeOrExit({
|
|
204
|
+
resource: "agent",
|
|
205
|
+
unitCents: AGENT_PRICE_CENTS,
|
|
206
|
+
yes: opts.yes,
|
|
207
|
+
});
|
|
208
|
+
const slug = opts.slug ?? slugifyAgentName(name);
|
|
209
|
+
try {
|
|
210
|
+
const result = await provisionAgent(token, {
|
|
211
|
+
companyUid,
|
|
212
|
+
name,
|
|
213
|
+
slug,
|
|
214
|
+
codexAuthMode: authMode,
|
|
215
|
+
...(provider ? { provider } : {}),
|
|
216
|
+
...(codexApiKey ? { codexApiKey } : {}),
|
|
217
|
+
idempotencyKey: `hq-cli-${randomUUID()}`,
|
|
218
|
+
...(opts.title ? { title: opts.title } : {}),
|
|
219
|
+
...(opts.description ? { description: opts.description } : {}),
|
|
220
|
+
});
|
|
221
|
+
const uid = typeof result.uid === "string" ? result.uid : slug;
|
|
222
|
+
console.log(chalk.green(`Provisioning started for agent "${name}".`));
|
|
223
|
+
console.log(chalk.dim(`Track setup: hq agents status ${uid} --company <slug>`));
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
// No card on file → surface the shareable payment link instead of an
|
|
227
|
+
// opaque 402, then exit non-zero so scripts can react.
|
|
228
|
+
if (err instanceof AgentsHttpError &&
|
|
229
|
+
err.status === 402 &&
|
|
230
|
+
err.billing) {
|
|
231
|
+
await surfaceBillingRequired(token, err.billing);
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
throw err;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
fail(err);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
147
241
|
agents
|
|
148
242
|
.command("list")
|
|
149
243
|
.description("List the company's agents")
|
|
@@ -382,4 +476,4 @@ export function registerAgentsCommand(program) {
|
|
|
382
476
|
});
|
|
383
477
|
}
|
|
384
478
|
//# sourceMappingURL=agents.js.map
|
|
385
|
-
//# debugId=
|
|
479
|
+
//# debugId=581b1950-a9bc-50a4-b16b-6e6649e9db30
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hq billing` — inspect billing state and mint a card-capture link from the
|
|
3
|
+
* terminal. Backs the paid-provisioning gate (agents & Outposts): a user can
|
|
4
|
+
* check whether a card is on file, and proactively hand out the same Stripe
|
|
5
|
+
* hosted card-capture link that a `402 billing_required` would surface.
|
|
6
|
+
*
|
|
7
|
+
* Targets the hq-pro billing control plane on `DEFAULT_VAULT_API_URL` via the
|
|
8
|
+
* shared `vaultApiFetch` helper — the same routes the console billing surfaces
|
|
9
|
+
* call.
|
|
10
|
+
*
|
|
11
|
+
* Subcommands:
|
|
12
|
+
* hq billing status [--company <slug>] — subscription + card on file
|
|
13
|
+
* hq billing checkout [--company <slug>] [--personal] — mint a card-capture link
|
|
14
|
+
*/
|
|
15
|
+
import { Command } from "commander";
|
|
16
|
+
export declare function registerBillingCommand(program: Command): void;
|
|
17
|
+
//# sourceMappingURL=billing.d.ts.map
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hq billing` — inspect billing state and mint a card-capture link from the
|
|
3
|
+
* terminal. Backs the paid-provisioning gate (agents & Outposts): a user can
|
|
4
|
+
* check whether a card is on file, and proactively hand out the same Stripe
|
|
5
|
+
* hosted card-capture link that a `402 billing_required` would surface.
|
|
6
|
+
*
|
|
7
|
+
* Targets the hq-pro billing control plane on `DEFAULT_VAULT_API_URL` via the
|
|
8
|
+
* shared `vaultApiFetch` helper — the same routes the console billing surfaces
|
|
9
|
+
* call.
|
|
10
|
+
*
|
|
11
|
+
* Subcommands:
|
|
12
|
+
* hq billing status [--company <slug>] — subscription + card on file
|
|
13
|
+
* hq billing checkout [--company <slug>] [--personal] — mint a card-capture link
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="9a5d87b5-94d2-5893-8c48-af8db719fa05")}catch(e){}}();
|
|
17
|
+
import chalk from "chalk";
|
|
18
|
+
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
19
|
+
import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
|
|
20
|
+
import { mintPaymentLink } from "../utils/billing-gate.js";
|
|
21
|
+
function fail(err) {
|
|
22
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
async function getBillingSummary(token, companyUid) {
|
|
26
|
+
const res = await vaultApiFetch({
|
|
27
|
+
token,
|
|
28
|
+
path: "/v1/billing/summary",
|
|
29
|
+
query: { companyUid },
|
|
30
|
+
});
|
|
31
|
+
if (!res.ok) {
|
|
32
|
+
const body = (await res.json().catch(() => ({})));
|
|
33
|
+
throw new Error(body.error ?? body.message ?? res.statusText);
|
|
34
|
+
}
|
|
35
|
+
return (await res.json());
|
|
36
|
+
}
|
|
37
|
+
export function registerBillingCommand(program) {
|
|
38
|
+
const billing = program
|
|
39
|
+
.command("billing")
|
|
40
|
+
.description("Inspect billing and mint a card-capture link")
|
|
41
|
+
.option("--company <slug>", "Company slug (resolves to companyUid)");
|
|
42
|
+
const companyOf = (sub) => sub.opts().company ??
|
|
43
|
+
billing.opts().company;
|
|
44
|
+
billing
|
|
45
|
+
.command("status")
|
|
46
|
+
.description("Show subscription status and whether a card is on file")
|
|
47
|
+
.option("--company <slug>", "Company slug (resolves to companyUid)")
|
|
48
|
+
.option("--json", "Emit raw JSON")
|
|
49
|
+
.action(async function (opts) {
|
|
50
|
+
try {
|
|
51
|
+
const token = await ensureCognitoToken();
|
|
52
|
+
const companyUid = await getCompanyUid(token, companyOf(this));
|
|
53
|
+
const summary = await getBillingSummary(token, companyUid);
|
|
54
|
+
if (opts.json) {
|
|
55
|
+
process.stdout.write(JSON.stringify(summary, null, 2) + "\n");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const sub = summary.subscriptionStatus ?? "none";
|
|
59
|
+
console.log(`${chalk.bold("Subscription")}: ${sub}`);
|
|
60
|
+
const pm = summary.defaultPaymentMethod;
|
|
61
|
+
if (pm?.present) {
|
|
62
|
+
const detail = pm.brand && pm.last4 ? `${pm.brand} ••••${pm.last4}` : "on file";
|
|
63
|
+
console.log(`${chalk.bold("Card")}: ${detail}`);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
console.log(`${chalk.bold("Card")}: ${chalk.yellow("no card on file")} — ` +
|
|
67
|
+
`run \`hq billing checkout\` to add one before provisioning paid resources.`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
fail(err);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
billing
|
|
75
|
+
.command("checkout")
|
|
76
|
+
.description("Mint a Stripe card-capture link (shareable) to add a card")
|
|
77
|
+
.option("--company <slug>", "Company slug (resolves to companyUid)")
|
|
78
|
+
.option("--personal", "Mint the link for your personal payer (Outposts) instead of a company")
|
|
79
|
+
.option("--json", "Emit raw JSON")
|
|
80
|
+
.action(async function (opts) {
|
|
81
|
+
try {
|
|
82
|
+
const token = await ensureCognitoToken();
|
|
83
|
+
let setup;
|
|
84
|
+
if (opts.personal) {
|
|
85
|
+
setup = {
|
|
86
|
+
payerType: "person",
|
|
87
|
+
path: "/v1/billing/checkout/person",
|
|
88
|
+
method: "POST",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
const companyUid = await getCompanyUid(token, companyOf(this));
|
|
93
|
+
setup = {
|
|
94
|
+
payerType: "company",
|
|
95
|
+
path: "/v1/billing/checkout/org",
|
|
96
|
+
method: "POST",
|
|
97
|
+
body: { companyUid },
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const url = await mintPaymentLink(token, setup);
|
|
101
|
+
if (opts.json) {
|
|
102
|
+
process.stdout.write(JSON.stringify({ url }, null, 2) + "\n");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
console.log("Add a card here (safe to share with whoever owns billing):\n " +
|
|
106
|
+
chalk.cyan(url));
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
fail(err);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=billing.js.map
|
|
114
|
+
//# debugId=9a5d87b5-94d2-5893-8c48-af8db719fa05
|
package/dist/commands/cloud.d.ts
CHANGED
|
@@ -66,6 +66,17 @@ export interface SyncCallOptions {
|
|
|
66
66
|
*/
|
|
67
67
|
syncMode?: SyncMode;
|
|
68
68
|
prefixSet?: string[];
|
|
69
|
+
/**
|
|
70
|
+
* Company-relative prefixes the pull must NOT materialize even when the
|
|
71
|
+
* caller's STS scope is wide enough to read them (sessions-exclusion /
|
|
72
|
+
* company-work-corpus). Derived by `resolvePullScope` in the hq-cloud release
|
|
73
|
+
* carrying the sessions/ pull-exclusion, forwarded to `sync()` here so the CLI
|
|
74
|
+
* honors the same "session transcripts never sync DOWN" invariant the
|
|
75
|
+
* background sync engine enforces. A company OWNER can read `sessions/` keys,
|
|
76
|
+
* so without this the CLI would bulk-download everyone's full-content session
|
|
77
|
+
* transcripts to local disk. Undefined/empty ⇒ nothing excluded (legacy).
|
|
78
|
+
*/
|
|
79
|
+
excludePrefixes?: string[];
|
|
69
80
|
/** Honor a `--force-scope-shrink` on a foreground pull (dirty files kept). */
|
|
70
81
|
forceScopeShrink?: boolean;
|
|
71
82
|
}
|
package/dist/commands/cloud.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* hq sync status — show local journal summary
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
16
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="5e2703d2-1874-5625-8508-67ff641ff1a6")}catch(e){}}();
|
|
17
17
|
import chalk from "chalk";
|
|
18
18
|
import * as fs from "fs";
|
|
19
19
|
import * as path from "path";
|
|
@@ -74,6 +74,25 @@ function pickCanonicalPerson(persons) {
|
|
|
74
74
|
return a.uid < b.uid ? -1 : 1;
|
|
75
75
|
})[0];
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Read the sessions-exclusion prefix set off a resolved {@link PullScope}.
|
|
79
|
+
*
|
|
80
|
+
* `resolvePullScope` in hq-cloud derives the sessions/ pull-exclusion and
|
|
81
|
+
* derives `excludePrefixes` (e.g. `["sessions/"]`) so a company OWNER — whose STS
|
|
82
|
+
* scope is wide enough to read `sessions/` keys — does not bulk-download
|
|
83
|
+
* everyone's full-content session transcripts to local disk, upholding the
|
|
84
|
+
* "session transcripts never sync DOWN" invariant hq-cloud enforces for the
|
|
85
|
+
* background engine. Keep the small runtime shape check even though PullScope
|
|
86
|
+
* now declares the field: it protects the CLI when an older external resolver
|
|
87
|
+
* implementation is injected by tests or downstream callers.
|
|
88
|
+
*/
|
|
89
|
+
function readScopeExcludePrefixes(scope) {
|
|
90
|
+
const raw = scope?.excludePrefixes;
|
|
91
|
+
if (!Array.isArray(raw))
|
|
92
|
+
return undefined;
|
|
93
|
+
const prefixes = raw.filter((p) => typeof p === "string" && p.length > 0);
|
|
94
|
+
return prefixes.length > 0 ? prefixes : undefined;
|
|
95
|
+
}
|
|
77
96
|
export async function pullAll(options, deps) {
|
|
78
97
|
const memberships = await deps.vaultClient.listMyMemberships();
|
|
79
98
|
const persons = await deps.vaultClient.listPersonEntities();
|
|
@@ -145,6 +164,12 @@ export async function pullAll(options, deps) {
|
|
|
145
164
|
if (scope.prefixSet !== undefined) {
|
|
146
165
|
entry.syncOptions.prefixSet = scope.prefixSet;
|
|
147
166
|
}
|
|
167
|
+
// Sessions-exclusion: thread the resolver's excludePrefixes into the
|
|
168
|
+
// pull so a wide-STS OWNER never bulk-downloads session transcripts.
|
|
169
|
+
const excludePrefixes = readScopeExcludePrefixes(scope);
|
|
170
|
+
if (excludePrefixes) {
|
|
171
|
+
entry.syncOptions.excludePrefixes = excludePrefixes;
|
|
172
|
+
}
|
|
148
173
|
}
|
|
149
174
|
catch {
|
|
150
175
|
resolvedMode = undefined;
|
|
@@ -742,6 +767,7 @@ export function registerCloudCommands(program) {
|
|
|
742
767
|
"to migrate, or re-run with --mode-all."));
|
|
743
768
|
process.exit(1);
|
|
744
769
|
}
|
|
770
|
+
const excludePrefixes = readScopeExcludePrefixes(pullScope);
|
|
745
771
|
const result = await sync({
|
|
746
772
|
company: options.company,
|
|
747
773
|
onConflict: options.onConflict,
|
|
@@ -753,6 +779,9 @@ export function registerCloudCommands(program) {
|
|
|
753
779
|
...(pullScope?.prefixSet !== undefined
|
|
754
780
|
? { prefixSet: pullScope.prefixSet }
|
|
755
781
|
: {}),
|
|
782
|
+
// Sessions-exclusion: never materialize excluded prefixes even for
|
|
783
|
+
// a wide-STS owner.
|
|
784
|
+
...(excludePrefixes ? { excludePrefixes } : {}),
|
|
756
785
|
...(options.forceScopeShrink ? { forceScopeShrink: true } : {}),
|
|
757
786
|
});
|
|
758
787
|
if (result.aborted) {
|
|
@@ -944,6 +973,11 @@ async function runPullAll(hqRoot, onConflict, modeAllOverride, skipPersonal, for
|
|
|
944
973
|
: {}),
|
|
945
974
|
...(opts.syncMode !== undefined ? { syncMode: opts.syncMode } : {}),
|
|
946
975
|
...(opts.prefixSet !== undefined ? { prefixSet: opts.prefixSet } : {}),
|
|
976
|
+
// Sessions-exclusion: forward the per-company excludePrefixes that
|
|
977
|
+
// pullAll stamped onto SyncCallOptions into the real hq-cloud sync().
|
|
978
|
+
...(opts.excludePrefixes?.length
|
|
979
|
+
? { excludePrefixes: opts.excludePrefixes }
|
|
980
|
+
: {}),
|
|
947
981
|
...(opts.forceScopeShrink ? { forceScopeShrink: true } : {}),
|
|
948
982
|
}),
|
|
949
983
|
});
|
|
@@ -1226,6 +1260,7 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
|
|
|
1226
1260
|
}
|
|
1227
1261
|
}
|
|
1228
1262
|
console.log(chalk.dim(" → pull leg"));
|
|
1263
|
+
const excludePrefixes = readScopeExcludePrefixes(pullScope);
|
|
1229
1264
|
const pullResult = await sync({
|
|
1230
1265
|
company: targetCompany,
|
|
1231
1266
|
vaultConfig,
|
|
@@ -1239,6 +1274,9 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
|
|
|
1239
1274
|
...(pullScope?.prefixSet !== undefined
|
|
1240
1275
|
? { prefixSet: pullScope.prefixSet }
|
|
1241
1276
|
: {}),
|
|
1277
|
+
// Sessions-exclusion: honor the resolver's excludePrefixes on the
|
|
1278
|
+
// `hq sync now` pull leg too.
|
|
1279
|
+
...(excludePrefixes ? { excludePrefixes } : {}),
|
|
1242
1280
|
...(forceScopeShrink ? { forceScopeShrink: true } : {}),
|
|
1243
1281
|
});
|
|
1244
1282
|
console.log(` ${pullResult.aborted ? chalk.yellow("⚠") : chalk.green("✓")} ` +
|
|
@@ -1368,4 +1406,4 @@ function resolveUploadAuthorFromCache() {
|
|
|
1368
1406
|
}
|
|
1369
1407
|
}
|
|
1370
1408
|
//# sourceMappingURL=cloud.js.map
|
|
1371
|
-
//# debugId=
|
|
1409
|
+
//# debugId=5e2703d2-1874-5625-8508-67ff641ff1a6
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
export declare const VALID_ROLES: Set<string>;
|
|
3
|
+
/** Roles allowed when inviting an existing fleet agent into a company. */
|
|
4
|
+
export declare const AGENT_INVITE_ROLES: Set<string>;
|
|
3
5
|
export type Role = "owner" | "admin" | "member" | "guest";
|
|
4
6
|
export interface PendingInvite {
|
|
5
7
|
membershipKey: string;
|
|
@@ -81,6 +83,13 @@ export interface InviteResult {
|
|
|
81
83
|
emailSkipped?: boolean;
|
|
82
84
|
/** Resend was attempted but failed — human-readable reason. */
|
|
83
85
|
emailError?: string;
|
|
86
|
+
/**
|
|
87
|
+
* Present when hq-pro treated the target as a fleet agent (guest invite).
|
|
88
|
+
* Always `false` on the agent path — host keeps billing.
|
|
89
|
+
*/
|
|
90
|
+
activationBilled?: boolean;
|
|
91
|
+
/** Host company uid when the server returned an agent guest-invite result. */
|
|
92
|
+
hostCompanyUid?: string;
|
|
84
93
|
}
|
|
85
94
|
export interface ResendInviteOptions {
|
|
86
95
|
inviteeEmail: string;
|
|
@@ -100,8 +109,10 @@ export interface ResendInviteResult {
|
|
|
100
109
|
emailError?: string;
|
|
101
110
|
}
|
|
102
111
|
export interface DetectedTarget {
|
|
103
|
-
type: "email" | "person";
|
|
112
|
+
type: "email" | "person" | "agent";
|
|
104
113
|
value: string;
|
|
114
|
+
/** True when the target is a fleet agent (uid or machine email). */
|
|
115
|
+
isAgent?: boolean;
|
|
105
116
|
}
|
|
106
117
|
export declare function detectTarget(target: string): DetectedTarget | null;
|
|
107
118
|
export declare function shortDate(iso: string): string;
|
package/dist/commands/members.js
CHANGED
|
@@ -1,17 +1,33 @@
|
|
|
1
1
|
|
|
2
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="09a00ce0-be62-5739-9816-042dda515518")}catch(e){}}();
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
5
5
|
import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
|
|
6
6
|
const EMAIL_PATTERN = /^[^\s]+@[^\s]+$/;
|
|
7
7
|
const PERSON_UID_PATTERN = /^prs_[A-Za-z0-9_-]+$/;
|
|
8
|
+
const AGENT_UID_PATTERN = /^agt_[A-Za-z0-9_-]+$/i;
|
|
9
|
+
/** Fleet agent machine email: agt-…@agents.getindigo.ai */
|
|
10
|
+
const AGENT_EMAIL_PATTERN = /^agt-[a-z0-9]+@agents\.getindigo\.ai$/i;
|
|
8
11
|
export const VALID_ROLES = new Set(["owner", "admin", "member", "guest"]);
|
|
12
|
+
/** Roles allowed when inviting an existing fleet agent into a company. */
|
|
13
|
+
export const AGENT_INVITE_ROLES = new Set(["admin", "member"]);
|
|
9
14
|
export function detectTarget(target) {
|
|
10
|
-
|
|
11
|
-
|
|
15
|
+
const trimmed = target.trim();
|
|
16
|
+
if (AGENT_UID_PATTERN.test(trimmed)) {
|
|
17
|
+
return { type: "agent", value: trimmed, isAgent: true };
|
|
12
18
|
}
|
|
13
|
-
if (
|
|
14
|
-
return {
|
|
19
|
+
if (AGENT_EMAIL_PATTERN.test(trimmed)) {
|
|
20
|
+
return {
|
|
21
|
+
type: "email",
|
|
22
|
+
value: trimmed.toLowerCase(),
|
|
23
|
+
isAgent: true,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (EMAIL_PATTERN.test(trimmed)) {
|
|
27
|
+
return { type: "email", value: trimmed.toLowerCase() };
|
|
28
|
+
}
|
|
29
|
+
if (PERSON_UID_PATTERN.test(trimmed)) {
|
|
30
|
+
return { type: "person", value: trimmed };
|
|
15
31
|
}
|
|
16
32
|
return null;
|
|
17
33
|
}
|
|
@@ -45,9 +61,24 @@ export async function inviteMember(options) {
|
|
|
45
61
|
}
|
|
46
62
|
const detected = detectTarget(options.target);
|
|
47
63
|
if (!detected) {
|
|
48
|
-
throw new Error(`Invalid target '${options.target}': must be an email
|
|
64
|
+
throw new Error(`Invalid target '${options.target}': must be an email, personUid (prs_…), agent uid (agt_…), or agent email (agt-…@agents.getindigo.ai)`);
|
|
65
|
+
}
|
|
66
|
+
// Fleet agents: same invite command as humans, but only member|admin.
|
|
67
|
+
// Host company keeps billing — server never double-bills on guest invite.
|
|
68
|
+
if (detected.isAgent) {
|
|
69
|
+
if (!AGENT_INVITE_ROLES.has(options.role)) {
|
|
70
|
+
throw new Error(`Agents can only be invited as member or admin (got '${options.role}'). Host company keeps billing.`);
|
|
71
|
+
}
|
|
72
|
+
if (options.paths) {
|
|
73
|
+
throw new Error("--paths / guest role is not valid for agents — invite as member or admin");
|
|
74
|
+
}
|
|
75
|
+
if (options.groupIds && options.groupIds.length > 0) {
|
|
76
|
+
throw new Error("--groups is not supported on agent invites — share secrets after membership with `hq secrets share`");
|
|
77
|
+
}
|
|
49
78
|
}
|
|
50
|
-
if (options.groupIds &&
|
|
79
|
+
if (options.groupIds &&
|
|
80
|
+
options.groupIds.length > 0 &&
|
|
81
|
+
(detected.type === "person" || detected.type === "agent")) {
|
|
51
82
|
throw new Error("--groups is only valid on email-keyed invites (server rejects personUid + groupIds with 400)");
|
|
52
83
|
}
|
|
53
84
|
const allowedPrefixes = options.paths
|
|
@@ -108,6 +139,12 @@ export async function inviteMember(options) {
|
|
|
108
139
|
? { emailSkipped: data.emailSkipped }
|
|
109
140
|
: {}),
|
|
110
141
|
...(data.emailError ? { emailError: data.emailError } : {}),
|
|
142
|
+
...(typeof data.activationBilled === "boolean"
|
|
143
|
+
? { activationBilled: data.activationBilled }
|
|
144
|
+
: {}),
|
|
145
|
+
...(typeof data.hostCompanyUid === "string"
|
|
146
|
+
? { hostCompanyUid: data.hostCompanyUid }
|
|
147
|
+
: {}),
|
|
111
148
|
};
|
|
112
149
|
}
|
|
113
150
|
/**
|
|
@@ -230,9 +267,17 @@ export function resolveRevokeTargetToMembershipKey(arg, companyUid) {
|
|
|
230
267
|
return arg;
|
|
231
268
|
const detected = detectTarget(arg);
|
|
232
269
|
if (detected?.type === "email") {
|
|
270
|
+
// Active agent guest memberships are personUid-keyed (agt_…#cmp_…), not
|
|
271
|
+
// email-keyed. Map machine emails to that key so revoke works after invite.
|
|
272
|
+
if (detected.isAgent) {
|
|
273
|
+
const local = detected.value.split("@")[0] ?? "";
|
|
274
|
+
const m = local.match(/^agt-(.+)$/i);
|
|
275
|
+
if (m)
|
|
276
|
+
return `agt_${m[1].toUpperCase()}#${companyUid}`;
|
|
277
|
+
}
|
|
233
278
|
return `email:${detected.value}#${companyUid}`;
|
|
234
279
|
}
|
|
235
|
-
if (detected?.type === "person") {
|
|
280
|
+
if (detected?.type === "person" || detected?.type === "agent") {
|
|
236
281
|
return `${detected.value}#${companyUid}`;
|
|
237
282
|
}
|
|
238
283
|
return arg;
|
|
@@ -337,8 +382,8 @@ export function registerMembersCommand(program) {
|
|
|
337
382
|
});
|
|
338
383
|
members
|
|
339
384
|
.command("invite <target>")
|
|
340
|
-
.description("Invite a person to the company
|
|
341
|
-
.option("--role <role>", "Role
|
|
385
|
+
.description("Invite a person or fleet agent to the company. People: email or prs_…. Agents: agt_… or agt-…@agents.getindigo.ai (member|admin; host keeps billing, no double charge).")
|
|
386
|
+
.option("--role <role>", "Role: person → owner|admin|member|guest; agent → member|admin only", "member")
|
|
342
387
|
.option("--paths <prefixes>", "Comma-separated allowed prefixes (only valid with --role guest)")
|
|
343
388
|
.option("--groups <ids>", "Comma-separated secret-group ids the invitee will be added to on claim (email-keyed invites only)")
|
|
344
389
|
.option("--send-email", "Have hq-pro send an invitation email via Resend (default true for email-keyed invites). Pre-resend hq-pro versions ignore this and the legacy 'no email sent' instructions print instead.", true)
|
|
@@ -376,12 +421,34 @@ export function registerMembersCommand(program) {
|
|
|
376
421
|
role: opts.role,
|
|
377
422
|
paths: opts.paths,
|
|
378
423
|
groupIds,
|
|
379
|
-
|
|
424
|
+
// Agents join actively — no Resend email.
|
|
425
|
+
sendEmail: detectTarget(target)?.isAgent ? false : opts.sendEmail,
|
|
380
426
|
companyUid,
|
|
381
427
|
callerUid,
|
|
382
428
|
token,
|
|
383
429
|
});
|
|
430
|
+
const agentTarget = detectTarget(target)?.isAgent === true;
|
|
384
431
|
console.log(chalk.green(`Invited ${target} as ${result.membership.role} (status: ${result.membership.status})`));
|
|
432
|
+
// Agent path is confirmed only when the server returned the guest-invite
|
|
433
|
+
// envelope (activationBilled present) AND status is active. A pending
|
|
434
|
+
// row means this hq-pro stage has not rolled the agent invite path yet —
|
|
435
|
+
// do NOT claim active join / no-double-bill.
|
|
436
|
+
if (agentTarget &&
|
|
437
|
+
result.membership.status === "active" &&
|
|
438
|
+
typeof result.activationBilled === "boolean") {
|
|
439
|
+
console.log(chalk.dim(`Fleet agent: active membership granted immediately` +
|
|
440
|
+
(result.hostCompanyUid
|
|
441
|
+
? ` (host ${result.hostCompanyUid} keeps billing)`
|
|
442
|
+
: " (host keeps billing, no double charge)") +
|
|
443
|
+
`. Next: share secrets / vault paths as needed (\`hq secrets share\`, \`/new-agent\`).`));
|
|
444
|
+
console.log();
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (agentTarget && result.membership.status !== "active") {
|
|
448
|
+
console.log(chalk.yellow("⚠ Agent invite landed as a pending membership — this hq-pro stage may not yet have multi-company agent invite. Wait for prod deploy of the agent membership path, then re-invite (or revoke this pending row)."));
|
|
449
|
+
console.log();
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
385
452
|
console.log();
|
|
386
453
|
if (result.magicLink) {
|
|
387
454
|
// Legacy server schema — magic-link redemption.
|
|
@@ -564,4 +631,4 @@ export function registerMembersCommand(program) {
|
|
|
564
631
|
});
|
|
565
632
|
}
|
|
566
633
|
//# sourceMappingURL=members.js.map
|
|
567
|
-
//# debugId=
|
|
634
|
+
//# debugId=09a00ce0-be62-5739-9816-042dda515518
|
|
@@ -21,11 +21,14 @@
|
|
|
21
21
|
* and status routes that exist. Renaming an Outpost is not a backend capability.
|
|
22
22
|
*/
|
|
23
23
|
import { Command } from "commander";
|
|
24
|
+
import { type BillingErrorPayload } from "../utils/billing-gate.js";
|
|
24
25
|
/** A non-2xx from the `/outpost/*` control plane. Carries status + `step`. */
|
|
25
26
|
export declare class OutpostHttpError extends Error {
|
|
26
27
|
status: number;
|
|
27
28
|
step?: string;
|
|
28
|
-
|
|
29
|
+
/** hq-pro's billing envelope on a `402 billing_required` provision block. */
|
|
30
|
+
billing?: BillingErrorPayload;
|
|
31
|
+
constructor(status: number, message: string, step?: string, billing?: BillingErrorPayload);
|
|
29
32
|
}
|
|
30
33
|
/** Row summary from `GET /outpost/list`. */
|
|
31
34
|
export interface OutpostSummary {
|
|
@@ -49,8 +52,22 @@ export declare function outpostRequest<T>(opts: {
|
|
|
49
52
|
token: string;
|
|
50
53
|
path: string;
|
|
51
54
|
method?: string;
|
|
55
|
+
body?: Record<string, unknown>;
|
|
52
56
|
query?: Record<string, string>;
|
|
53
57
|
}): Promise<T>;
|
|
58
|
+
/**
|
|
59
|
+
* Provision the caller's Outpost. Sends the cached Cognito refresh token so the
|
|
60
|
+
* box can authenticate AS the caller (the same body the console's
|
|
61
|
+
* `provisionMyOutpost` sends). Idempotent server-side: a caller already at their
|
|
62
|
+
* per-person cap gets their existing box back rather than a duplicate. The
|
|
63
|
+
* refresh token is sent over HTTPS and NEVER printed.
|
|
64
|
+
*/
|
|
65
|
+
export declare function provisionOutpost(token: string, input: {
|
|
66
|
+
refreshToken: string;
|
|
67
|
+
clientIp?: string;
|
|
68
|
+
diskSizeGb?: number;
|
|
69
|
+
agentRuntime?: "claude" | "codex";
|
|
70
|
+
}): Promise<Record<string, unknown>>;
|
|
54
71
|
export declare function listOutposts(token: string): Promise<OutpostSummary[]>;
|
|
55
72
|
export declare function getOutpostStatus(token: string, outpostId?: string): Promise<Record<string, unknown>>;
|
|
56
73
|
export declare function enableCodex(token: string, outpostId?: string): Promise<Record<string, unknown>>;
|