@indigoai-us/hq-cli 5.62.2 → 5.64.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/members.js +11 -3
- package/dist/commands/outposts.d.ts +39 -1
- package/dist/commands/outposts.js +144 -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 +1 -1
- 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/members.test.ts +18 -0
- package/src/commands/members.ts +8 -1
- package/src/commands/outposts.test.ts +179 -0
- package/src/commands/outposts.ts +208 -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/members.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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";
|
|
@@ -267,9 +267,17 @@ export function resolveRevokeTargetToMembershipKey(arg, companyUid) {
|
|
|
267
267
|
return arg;
|
|
268
268
|
const detected = detectTarget(arg);
|
|
269
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
|
+
}
|
|
270
278
|
return `email:${detected.value}#${companyUid}`;
|
|
271
279
|
}
|
|
272
|
-
if (detected?.type === "person") {
|
|
280
|
+
if (detected?.type === "person" || detected?.type === "agent") {
|
|
273
281
|
return `${detected.value}#${companyUid}`;
|
|
274
282
|
}
|
|
275
283
|
return arg;
|
|
@@ -623,4 +631,4 @@ export function registerMembersCommand(program) {
|
|
|
623
631
|
});
|
|
624
632
|
}
|
|
625
633
|
//# sourceMappingURL=members.js.map
|
|
626
|
-
//# 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,12 +52,47 @@ 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>>;
|
|
57
74
|
export declare function regenerateLoginUrl(token: string, outpostId?: string): Promise<Record<string, unknown>>;
|
|
58
75
|
export declare function destroyOutpost(token: string, outpostId?: string): Promise<Record<string, unknown>>;
|
|
76
|
+
/** Result of `POST /outpost/exec` — a terminal SSM invocation on the box. */
|
|
77
|
+
export interface OutpostExecResult {
|
|
78
|
+
ok: true;
|
|
79
|
+
outpostId: string;
|
|
80
|
+
instanceId: string;
|
|
81
|
+
commandId: string;
|
|
82
|
+
/** SSM invocation status (Success | Failed | Cancelled). */
|
|
83
|
+
status: string;
|
|
84
|
+
/** Remote process exit code, or null when SSM reported none. */
|
|
85
|
+
exitCode: number | null;
|
|
86
|
+
stdout: string;
|
|
87
|
+
stderr: string;
|
|
88
|
+
/** True when SSM clipped stdout/stderr at its inline output limit. */
|
|
89
|
+
truncated: boolean;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Run a one-shot shell command on the caller's Outpost via `POST /outpost/exec`
|
|
93
|
+
* (server-brokered SSM — no SSH). Throws `OutpostHttpError` on a non-2xx, whose
|
|
94
|
+
* `step` distinguishes not-ready / platform-unsupported / timeout / ssm.
|
|
95
|
+
*/
|
|
96
|
+
export declare function execOutpost(token: string, command: string, outpostId?: string): Promise<OutpostExecResult>;
|
|
59
97
|
export declare function registerOutpostsCommand(program: Command): void;
|
|
60
98
|
//# sourceMappingURL=outposts.d.ts.map
|
|
@@ -21,19 +21,24 @@
|
|
|
21
21
|
* and status routes that exist. Renaming an Outpost is not a backend capability.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
!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]="
|
|
24
|
+
!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]="067e20d6-0132-565a-8a22-9c27520669d2")}catch(e){}}();
|
|
25
25
|
import chalk from "chalk";
|
|
26
|
+
import { loadCachedTokens } from "@indigoai-us/hq-cloud";
|
|
26
27
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
27
28
|
import { vaultApiFetch } from "../utils/vault-api.js";
|
|
29
|
+
import { OUTPOST_PRICE_CENTS, confirmChargeOrExit, parseBillingPayload, surfaceBillingRequired, } from "../utils/billing-gate.js";
|
|
28
30
|
/** A non-2xx from the `/outpost/*` control plane. Carries status + `step`. */
|
|
29
31
|
export class OutpostHttpError extends Error {
|
|
30
32
|
status;
|
|
31
33
|
step;
|
|
32
|
-
|
|
34
|
+
/** hq-pro's billing envelope on a `402 billing_required` provision block. */
|
|
35
|
+
billing;
|
|
36
|
+
constructor(status, message, step, billing) {
|
|
33
37
|
super(message);
|
|
34
38
|
this.name = "OutpostHttpError";
|
|
35
39
|
this.status = status;
|
|
36
40
|
this.step = step;
|
|
41
|
+
this.billing = billing;
|
|
37
42
|
}
|
|
38
43
|
}
|
|
39
44
|
/**
|
|
@@ -52,10 +57,30 @@ export async function outpostRequest(opts) {
|
|
|
52
57
|
: typeof body.error === "string"
|
|
53
58
|
? body.error
|
|
54
59
|
: res.statusText;
|
|
55
|
-
throw new OutpostHttpError(res.status, message, body.step);
|
|
60
|
+
throw new OutpostHttpError(res.status, message, body.step, parseBillingPayload(body));
|
|
56
61
|
}
|
|
57
62
|
return (await res.json());
|
|
58
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Provision the caller's Outpost. Sends the cached Cognito refresh token so the
|
|
66
|
+
* box can authenticate AS the caller (the same body the console's
|
|
67
|
+
* `provisionMyOutpost` sends). Idempotent server-side: a caller already at their
|
|
68
|
+
* per-person cap gets their existing box back rather than a duplicate. The
|
|
69
|
+
* refresh token is sent over HTTPS and NEVER printed.
|
|
70
|
+
*/
|
|
71
|
+
export async function provisionOutpost(token, input) {
|
|
72
|
+
return outpostRequest({
|
|
73
|
+
token,
|
|
74
|
+
path: "/outpost/provision",
|
|
75
|
+
method: "POST",
|
|
76
|
+
body: {
|
|
77
|
+
refreshToken: input.refreshToken,
|
|
78
|
+
...(input.clientIp ? { clientIp: input.clientIp } : {}),
|
|
79
|
+
...(input.diskSizeGb ? { diskSizeGb: input.diskSizeGb } : {}),
|
|
80
|
+
...(input.agentRuntime ? { agentRuntime: input.agentRuntime } : {}),
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
59
84
|
export async function listOutposts(token) {
|
|
60
85
|
const data = await outpostRequest({
|
|
61
86
|
token,
|
|
@@ -94,6 +119,20 @@ export async function destroyOutpost(token, outpostId) {
|
|
|
94
119
|
query: outpostId ? { outpostId } : undefined,
|
|
95
120
|
});
|
|
96
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Run a one-shot shell command on the caller's Outpost via `POST /outpost/exec`
|
|
124
|
+
* (server-brokered SSM — no SSH). Throws `OutpostHttpError` on a non-2xx, whose
|
|
125
|
+
* `step` distinguishes not-ready / platform-unsupported / timeout / ssm.
|
|
126
|
+
*/
|
|
127
|
+
export async function execOutpost(token, command, outpostId) {
|
|
128
|
+
return outpostRequest({
|
|
129
|
+
token,
|
|
130
|
+
path: "/outpost/exec",
|
|
131
|
+
method: "POST",
|
|
132
|
+
body: { command },
|
|
133
|
+
query: outpostId ? { outpostId } : undefined,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
97
136
|
// ---------------------------------------------------------------------------
|
|
98
137
|
// Command registration
|
|
99
138
|
// ---------------------------------------------------------------------------
|
|
@@ -117,6 +156,68 @@ export function registerOutpostsCommand(program) {
|
|
|
117
156
|
const outposts = program
|
|
118
157
|
.command("outposts")
|
|
119
158
|
.description("Manage your personal HQ Outposts (EC2 boxes)");
|
|
159
|
+
outposts
|
|
160
|
+
.command("provision")
|
|
161
|
+
.alias("create")
|
|
162
|
+
.description("Provision a new Outpost ($80/month — requires --yes)")
|
|
163
|
+
.option("--runtime <runtime>", "Agent runtime: claude | codex (default claude)")
|
|
164
|
+
.option("--disk <gb>", "Root disk size in GB (EC2 only)")
|
|
165
|
+
.option("--client-ip <ip>", "Your public IP for the box's SSH ingress (optional; server derives it otherwise)")
|
|
166
|
+
.option("--yes", "Confirm the $80/month charge (required to provision)")
|
|
167
|
+
.action(async function (opts) {
|
|
168
|
+
try {
|
|
169
|
+
const agentRuntime = opts.runtime === "codex"
|
|
170
|
+
? "codex"
|
|
171
|
+
: opts.runtime
|
|
172
|
+
? "claude"
|
|
173
|
+
: undefined;
|
|
174
|
+
let diskSizeGb;
|
|
175
|
+
if (opts.disk !== undefined) {
|
|
176
|
+
diskSizeGb = Number(opts.disk);
|
|
177
|
+
if (!Number.isFinite(diskSizeGb) || diskSizeGb <= 0) {
|
|
178
|
+
console.error(chalk.red(`Invalid --disk '${opts.disk}': must be a positive number of GB.`));
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// Paid gate: print the monthly cost and require --yes before any call.
|
|
183
|
+
confirmChargeOrExit({
|
|
184
|
+
resource: "Outpost",
|
|
185
|
+
unitCents: OUTPOST_PRICE_CENTS,
|
|
186
|
+
yes: opts.yes,
|
|
187
|
+
});
|
|
188
|
+
// The box authenticates AS the caller using the cached refresh token —
|
|
189
|
+
// the same body the console sends. Never printed.
|
|
190
|
+
const refreshToken = loadCachedTokens()?.refreshToken;
|
|
191
|
+
if (!refreshToken) {
|
|
192
|
+
console.error(chalk.red("No cached session found — run `hq login` first, then re-run."));
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
const token = await ensureCognitoToken();
|
|
196
|
+
try {
|
|
197
|
+
await provisionOutpost(token, {
|
|
198
|
+
refreshToken,
|
|
199
|
+
...(opts.clientIp ? { clientIp: opts.clientIp } : {}),
|
|
200
|
+
...(diskSizeGb ? { diskSizeGb } : {}),
|
|
201
|
+
...(agentRuntime ? { agentRuntime } : {}),
|
|
202
|
+
});
|
|
203
|
+
console.log(chalk.green("Provisioning started for your Outpost."));
|
|
204
|
+
console.log(chalk.dim("Track it: hq outposts status"));
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
// No card on file → surface the shareable payment link, not an opaque 402.
|
|
208
|
+
if (err instanceof OutpostHttpError &&
|
|
209
|
+
err.status === 402 &&
|
|
210
|
+
err.billing) {
|
|
211
|
+
await surfaceBillingRequired(token, err.billing);
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
throw err;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
fail(err);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
120
221
|
outposts
|
|
121
222
|
.command("list")
|
|
122
223
|
.description("List every Outpost you own")
|
|
@@ -180,6 +281,45 @@ export function registerOutpostsCommand(program) {
|
|
|
180
281
|
fail(err);
|
|
181
282
|
}
|
|
182
283
|
});
|
|
284
|
+
outposts
|
|
285
|
+
.command("exec <command...>")
|
|
286
|
+
.description("Run a shell command on an Outpost and print its output (use -- before flags meant for the remote command)")
|
|
287
|
+
.option("--id <outpostId>", "Outpost id (defaults to your primary box)")
|
|
288
|
+
.option("--json", "Emit raw JSON")
|
|
289
|
+
.action(async function (commandParts, opts) {
|
|
290
|
+
try {
|
|
291
|
+
const command = commandParts.join(" ").trim();
|
|
292
|
+
if (!command) {
|
|
293
|
+
console.error(chalk.red("No command given. Usage: hq outposts exec -- <command>"));
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
296
|
+
const token = await ensureCognitoToken();
|
|
297
|
+
const result = await execOutpost(token, command, opts.id);
|
|
298
|
+
if (opts.json) {
|
|
299
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
// Stream the remote streams to ours so the command feels local, then
|
|
303
|
+
// exit with the remote exit code.
|
|
304
|
+
if (result.stdout)
|
|
305
|
+
process.stdout.write(result.stdout);
|
|
306
|
+
if (result.stderr)
|
|
307
|
+
process.stderr.write(result.stderr);
|
|
308
|
+
if (result.truncated) {
|
|
309
|
+
console.error(chalk.yellow("(output truncated by SSM's inline limit — redirect to a file on the box for full output)"));
|
|
310
|
+
}
|
|
311
|
+
if (result.status !== "Success" && result.exitCode === null) {
|
|
312
|
+
console.error(chalk.yellow(`(command ended with SSM status: ${result.status})`));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// Propagate the remote exit code so `hq outposts exec -- false` exits 1.
|
|
316
|
+
process.exitCode =
|
|
317
|
+
typeof result.exitCode === "number" ? result.exitCode : 0;
|
|
318
|
+
}
|
|
319
|
+
catch (err) {
|
|
320
|
+
fail(err);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
183
323
|
outposts
|
|
184
324
|
.command("codex-enable")
|
|
185
325
|
.description("Enable (or retry) Codex on an Outpost")
|
|
@@ -252,4 +392,4 @@ export function registerOutpostsCommand(program) {
|
|
|
252
392
|
});
|
|
253
393
|
}
|
|
254
394
|
//# sourceMappingURL=outposts.js.map
|
|
255
|
-
//# debugId=
|
|
395
|
+
//# debugId=067e20d6-0132-565a-8a22-9c27520669d2
|
package/dist/main.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// MUST be first: guard the Node version before any dependency that needs a
|
|
6
6
|
// Node 20+ API (e.g. util.styleText) or a newer native ABI is evaluated.
|
|
7
7
|
|
|
8
|
-
!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]="
|
|
8
|
+
!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]="1cc9f0b6-d8a8-54ea-a85f-44437f67b856")}catch(e){}}();
|
|
9
9
|
import "./node-preflight.js";
|
|
10
10
|
import { Command } from "commander";
|
|
11
11
|
import { initSentry, Sentry } from "./sentry.js";
|
|
@@ -56,6 +56,7 @@ import { registerCrmCommand } from "./commands/crm.js";
|
|
|
56
56
|
import { registerCompanyCommand } from "./commands/company.js";
|
|
57
57
|
import { registerAgentsCommand } from "./commands/agents.js";
|
|
58
58
|
import { registerOutpostsCommand } from "./commands/outposts.js";
|
|
59
|
+
import { registerBillingCommand } from "./commands/billing.js";
|
|
59
60
|
import { registerDbCommand } from "./commands/db.js";
|
|
60
61
|
import { sanitizeArgv } from "./utils/feedback-diagnostics.js";
|
|
61
62
|
import { environmentalFsErrorMessage } from "./utils/environmental-error.js";
|
|
@@ -209,6 +210,10 @@ registerAgentsCommand(program);
|
|
|
209
210
|
// enable Codex on, refresh login for, and destroy your EC2 boxes via the hq-pro
|
|
210
211
|
// /outpost/* control plane.
|
|
211
212
|
registerOutpostsCommand(program);
|
|
213
|
+
// Billing (subcommand group — `hq billing …`). Check subscription/card state and
|
|
214
|
+
// mint a shareable Stripe card-capture link — the client side of the paid-
|
|
215
|
+
// provisioning gate for agents & Outposts.
|
|
216
|
+
registerBillingCommand(program);
|
|
212
217
|
export async function runCli() {
|
|
213
218
|
try {
|
|
214
219
|
Sentry.addBreadcrumb({
|
|
@@ -269,4 +274,4 @@ export async function runCli() {
|
|
|
269
274
|
}
|
|
270
275
|
}
|
|
271
276
|
//# sourceMappingURL=main.js.map
|
|
272
|
-
//# debugId=
|
|
277
|
+
//# debugId=1cc9f0b6-d8a8-54ea-a85f-44437f67b856
|