@ornncompute/cli 1.0.25 → 1.0.35
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/README.md +82 -32
- package/package.json +1 -1
- package/src/admin-capacity-reservations.mjs +68 -43
- package/src/admin-enrollment.mjs +14 -13
- package/src/admin-node-lifecycle.mjs +10 -13
- package/src/admin-shell.mjs +146 -12
- package/src/catalog-shell.mjs +58 -45
- package/src/device-auth.mjs +1 -1
- package/src/evidence-commands.mjs +25 -11
- package/src/gpu-catalog.mjs +21 -0
- package/src/identity-file.mjs +28 -0
- package/src/kubernetes-commands.mjs +12 -6
- package/src/machine-reference.mjs +10 -2
- package/src/mount-commands.mjs +4 -2
- package/src/slurm-commands.mjs +11 -0
- package/src/storage-commands.mjs +27 -7
- package/src/vm-commands.mjs +42 -7
- package/vendor/capabilities/capabilities/admin-capacity-reservations.js +31 -61
- package/vendor/capabilities/capabilities/admin-enrollment.js +9 -9
- package/vendor/capabilities/capabilities/admin-nodes-lifecycle.js +6 -6
- package/vendor/capabilities/capabilities/admin.js +7 -7
- package/vendor/capabilities/capabilities/commerce-market.js +330 -0
- package/vendor/capabilities/capabilities/fleet.js +57 -21
- package/vendor/capabilities/capabilities/listings.js +137 -146
- package/vendor/capabilities/capabilities/market-shared.js +44 -0
- package/vendor/capabilities/capabilities/markets.js +449 -0
- package/vendor/capabilities/capabilities/networks.js +7 -0
- package/vendor/capabilities/capabilities/tokens.js +12 -9
- package/vendor/capabilities/capability.js +2 -1
- package/vendor/capabilities/cli-catalog.js +4 -2
- package/vendor/capabilities/cli-migrations.js +16 -43
- package/vendor/capabilities/cli.js +1 -1
- package/vendor/capabilities/spec-catalog.js +26 -3
- package/vendor/capabilities/supplier-directory.js +20 -0
- package/vendor/capabilities/capabilities/capacity-offers.js +0 -368
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { listSupplierOrganizations } from "../supplier-directory.js";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import { defineCapability, preview } from "../capability.js";
|
|
3
|
-
import { bindHardwareFields, HARDWARE_LISTING_FIELDS, HARDWARE_LISTING_FILTERS, loadHardwareCatalog, projectCatalogTerms, } from "../spec-catalog.js";
|
|
4
|
-
import { capacityOfferForecastVnext, executeCapacityOfferForecast } from "./capacity-offers.js";
|
|
4
|
+
import { bindHardwareFields, HARDWARE_LISTING_FIELDS, HARDWARE_LISTING_FILTERS, loadHardwareCatalog, projectCatalogTerms, resolveCatalogTerm, } from "../spec-catalog.js";
|
|
5
5
|
function queryPath(path, params) {
|
|
6
6
|
const parts = [];
|
|
7
7
|
for (const [key, value] of Object.entries(params)) {
|
|
@@ -11,8 +11,8 @@ function queryPath(path, params) {
|
|
|
11
11
|
return parts.length ? `${path}?${parts.join("&")}` : path;
|
|
12
12
|
}
|
|
13
13
|
export const listingCreateInput = z.object({
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
organizationId: z.string().optional(),
|
|
15
|
+
organization_id: z.string().optional(),
|
|
16
16
|
facilityId: z.string().optional(),
|
|
17
17
|
facility_id: z.string().optional(),
|
|
18
18
|
kind: z.enum(["gpu", "cpu", "node", "link"]).optional(),
|
|
@@ -31,7 +31,7 @@ export const listingCreateInput = z.object({
|
|
|
31
31
|
node_count: z.number().int().optional(),
|
|
32
32
|
gpus_per_node: z.number().int().optional(),
|
|
33
33
|
site_nickname: z.string().optional(),
|
|
34
|
-
|
|
34
|
+
site_supplier: z.string().optional(),
|
|
35
35
|
ram: z.string().optional(),
|
|
36
36
|
storage: z.string().optional(),
|
|
37
37
|
internet: z.string().optional(),
|
|
@@ -46,20 +46,24 @@ export const listingCreateInput = z.object({
|
|
|
46
46
|
.describe("End of the availability window. YYYY-MM-DD or RFC3339."),
|
|
47
47
|
available_from: z.string().optional().describe("Alias for available_start_at."),
|
|
48
48
|
available_to: z.string().optional().describe("Alias for available_end_at."),
|
|
49
|
-
buy_now_price_per_gpu_hour: z
|
|
50
|
-
|
|
49
|
+
buy_now_price_per_gpu_hour: z
|
|
50
|
+
.number()
|
|
51
|
+
.optional()
|
|
52
|
+
.describe("Price buyers pay, USD per GPU-hour. The spread over cost_per_gpu_hour is Ornn's."),
|
|
53
|
+
cost_per_gpu_hour: z
|
|
54
|
+
.number()
|
|
55
|
+
.optional()
|
|
56
|
+
.describe("Supplier price, USD per GPU-hour. Defaults to the buy-now price (spread 0)."),
|
|
51
57
|
deposit_percent: z.number().optional(),
|
|
58
|
+
down_payment_cents: z.number().int().nonnegative().optional(),
|
|
59
|
+
access_mode: z.enum(["bare-metal", "virtual-machine"]).optional(),
|
|
60
|
+
state: z.enum(["available", "internal"]).optional(),
|
|
52
61
|
price_omitted: z.boolean().optional(),
|
|
53
62
|
announce: z.boolean().optional(),
|
|
54
63
|
text: z.string().optional(),
|
|
55
64
|
confirm: z.boolean().optional(),
|
|
56
65
|
});
|
|
57
66
|
const catalogTermsInput = z.object({ kind: z.enum(["gpu", "cpu", "node", "link"]).optional() });
|
|
58
|
-
export const listingForecastInput = z.object({
|
|
59
|
-
$positionals: z.array(z.string()).max(0).default([]),
|
|
60
|
-
limit: z.coerce.number().int().min(1).max(500).optional(),
|
|
61
|
-
cursor: z.string().min(1).optional(),
|
|
62
|
-
});
|
|
63
67
|
const slackListingCreateParams = {
|
|
64
68
|
type: "object",
|
|
65
69
|
properties: {
|
|
@@ -70,7 +74,7 @@ const slackListingCreateParams = {
|
|
|
70
74
|
node_count: { type: "integer" },
|
|
71
75
|
gpus_per_node: { type: "integer" },
|
|
72
76
|
site_nickname: { type: "string" },
|
|
73
|
-
|
|
77
|
+
site_supplier: { type: "string" },
|
|
74
78
|
ram: { type: "string" },
|
|
75
79
|
storage: { type: "string" },
|
|
76
80
|
available_start_at: { type: "string" },
|
|
@@ -149,29 +153,19 @@ export const listListingsCapability = defineCapability({
|
|
|
149
153
|
return ctx.fetch("/listings/available", { method: "GET", body: bound.payload });
|
|
150
154
|
},
|
|
151
155
|
});
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
execute: async (ctx, input) => {
|
|
166
|
-
const parsed = listingForecastInput.parse(input);
|
|
167
|
-
if (ctx.cli?.interaction)
|
|
168
|
-
return executeCapacityOfferForecast(ctx, parsed);
|
|
169
|
-
return ctx.fetch(queryPath("/v1/workflows/commerce/listings/forecast", {
|
|
170
|
-
limit: parsed.limit?.toString(),
|
|
171
|
-
cursor: parsed.cursor,
|
|
172
|
-
}), { method: "GET" });
|
|
173
|
-
},
|
|
174
|
-
});
|
|
156
|
+
const MICRO_USD = 1_000_000;
|
|
157
|
+
function microUsd(dollars) {
|
|
158
|
+
return Math.round(dollars * MICRO_USD);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* A primary listing's down payment in cents, from a percentage of the buyer
|
|
162
|
+
* total over the window: price (micro-USD per GPU-hour) x GPUs x hours.
|
|
163
|
+
*/
|
|
164
|
+
export function primaryDownPaymentCents(terms) {
|
|
165
|
+
const hours = (Date.parse(terms.endsAt) - Date.parse(terms.startsAt)) / 3_600_000;
|
|
166
|
+
const totalCents = (terms.pricePerGpuHourMicroUsd * terms.nodeCount * terms.gpusPerNode * hours) / 10_000;
|
|
167
|
+
return Math.max(0, Math.round((totalCents * terms.percent) / 100));
|
|
168
|
+
}
|
|
175
169
|
/** Commerce parses listing windows as RFC3339 timestamps, not bare dates. */
|
|
176
170
|
function rfc3339(value) {
|
|
177
171
|
const trimmed = value.trim();
|
|
@@ -199,18 +193,27 @@ const LISTING_CREATE_INPUT_ONLY_KEYS = [
|
|
|
199
193
|
"site",
|
|
200
194
|
"kind",
|
|
201
195
|
"confirm",
|
|
202
|
-
"
|
|
203
|
-
"
|
|
196
|
+
"organizationId",
|
|
197
|
+
"organization_id",
|
|
198
|
+
"site_supplier",
|
|
204
199
|
"facilityId",
|
|
205
200
|
"facility_id",
|
|
201
|
+
"buy_now_price_per_gpu_hour",
|
|
202
|
+
"cost_per_gpu_hour",
|
|
203
|
+
"deposit_percent",
|
|
204
|
+
"down_payment_cents",
|
|
205
|
+
"access_mode",
|
|
206
|
+
"state",
|
|
207
|
+
"price_omitted",
|
|
206
208
|
];
|
|
207
209
|
export const createListingCapability = defineCapability({
|
|
208
210
|
id: "listings.create",
|
|
209
211
|
domain: "listings",
|
|
210
212
|
roles: ["admin"],
|
|
211
|
-
description: "Create a marketplace listing. gpu_type, cpu, node_model, and fabric_type must exact-match an orchestrator catalog display name, canonical, or alias
|
|
213
|
+
description: "Create a primary marketplace listing (admin). gpu_type, cpu, node_model, and fabric_type must exact-match an orchestrator catalog display name, canonical, or alias; gpu_type becomes the listing's GPU slug. " +
|
|
214
|
+
"cost_per_gpu_hour is the supplier price and buy_now_price_per_gpu_hour the price buyers pay; the difference is Ornn's spread. Nodes are attached afterwards. CLI asks y/N. Slack never POSTs until a human replies confirm. MCP creates on this tool call.",
|
|
212
215
|
mutation: "preview-confirm",
|
|
213
|
-
http: { method: "POST", path: "/
|
|
216
|
+
http: { method: "POST", path: "/commerce/listings" },
|
|
214
217
|
slack: { parameters: slackListingCreateParams },
|
|
215
218
|
input: listingCreateInput,
|
|
216
219
|
execute: async (ctx, input) => {
|
|
@@ -225,135 +228,123 @@ export const createListingCapability = defineCapability({
|
|
|
225
228
|
const bound = bindHardwareFields(hardware.terms, normalized, HARDWARE_LISTING_FIELDS);
|
|
226
229
|
if (!bound.ok)
|
|
227
230
|
return bound;
|
|
228
|
-
let
|
|
231
|
+
let organizationId = parsed.organizationId ?? parsed.organization_id;
|
|
229
232
|
let facilityId = parsed.facilityId ?? parsed.facility_id;
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if (query) {
|
|
237
|
-
const match = ops.find((o) => o.operatorId.toLowerCase() === query ||
|
|
238
|
-
o.slug.toLowerCase() === query ||
|
|
239
|
-
o.displayName.toLowerCase() === query);
|
|
240
|
-
if (match)
|
|
241
|
-
operatorId = match.operatorId;
|
|
242
|
-
}
|
|
243
|
-
if (!operatorId && ops.length > 0) {
|
|
244
|
-
operatorId = ops[0].operatorId;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
catch {
|
|
248
|
-
// Ignore fetch failure (e.g. in unit tests)
|
|
249
|
-
}
|
|
233
|
+
const supplierQuery = (parsed.site_supplier ?? "").trim().toLowerCase();
|
|
234
|
+
const facilityQuery = (parsed.site_nickname ?? parsed.site ?? "").trim().toLowerCase();
|
|
235
|
+
if (!organizationId && supplierQuery) {
|
|
236
|
+
const matches = (await listSupplierOrganizations(ctx.fetch)).filter((org) => org.id.toLowerCase() === supplierQuery || org.name.toLowerCase() === supplierQuery);
|
|
237
|
+
if (matches.length !== 1) {
|
|
238
|
+
return { ok: false, error: matches.length ? "ambiguous_supplier" : "supplier_not_found" };
|
|
250
239
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
if (
|
|
269
|
-
|
|
240
|
+
organizationId = matches[0].id;
|
|
241
|
+
}
|
|
242
|
+
if ((facilityId && !organizationId) || (!facilityId && (facilityQuery || organizationId))) {
|
|
243
|
+
const matches = [];
|
|
244
|
+
const seenCursors = new Set();
|
|
245
|
+
let cursor;
|
|
246
|
+
do {
|
|
247
|
+
const response = (await ctx.fetch(queryPath("/provisioning/facilities", { cursor }), { method: "GET" }));
|
|
248
|
+
if (!Array.isArray(response.facilities))
|
|
249
|
+
throw new Error("invalid facility catalog");
|
|
250
|
+
matches.push(...response.facilities.filter((facility) => (!organizationId || (facility.organizationId ?? facility.organization_id) === organizationId) &&
|
|
251
|
+
(facilityId
|
|
252
|
+
? (facility.facilityId ?? facility.id) === facilityId
|
|
253
|
+
: !facilityQuery || [facility.facilityId ?? facility.id, facility.slug, facility.displayName ?? facility.display_name]
|
|
254
|
+
.some((value) => value?.toLowerCase() === facilityQuery))));
|
|
255
|
+
cursor = response.nextCursor ?? undefined;
|
|
256
|
+
if (cursor) {
|
|
257
|
+
if (seenCursors.has(cursor) || seenCursors.size >= 49) {
|
|
258
|
+
throw new Error("facility catalog pagination did not complete");
|
|
270
259
|
}
|
|
260
|
+
seenCursors.add(cursor);
|
|
271
261
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
262
|
+
} while (cursor);
|
|
263
|
+
if (matches.length !== 1) {
|
|
264
|
+
return { ok: false, error: matches.length ? "ambiguous_facility" : "facility_not_found" };
|
|
275
265
|
}
|
|
266
|
+
facilityId = matches[0].facilityId ?? matches[0].id;
|
|
267
|
+
organizationId ??= matches[0].organizationId ?? matches[0].organization_id;
|
|
268
|
+
if (!facilityId || !organizationId)
|
|
269
|
+
return { ok: false, error: "invalid_facility_owner" };
|
|
276
270
|
}
|
|
277
271
|
// Every window alias resolves to the same pair, and both are normalized:
|
|
278
|
-
// commerce decodes
|
|
272
|
+
// commerce decodes startsAt/endsAt as time.Time, so a bare "2026-09-01"
|
|
279
273
|
// fails the whole request body rather than the one field.
|
|
280
|
-
const
|
|
274
|
+
const startsAt = rfc3339(parsed.available_start_at ??
|
|
281
275
|
parsed.available_from ??
|
|
282
276
|
parsed.starts_after ??
|
|
283
277
|
new Date().toISOString());
|
|
284
|
-
|
|
285
|
-
// `starts_after` the opening bound already accepts. Only `ends_after` was
|
|
286
|
-
// consulted here, so naming an end with `ends_before` was silently dropped
|
|
287
|
-
// and endAt fell back to the +30d default.
|
|
288
|
-
const endAt = rfc3339(parsed.available_end_at ??
|
|
278
|
+
const endsAt = rfc3339(parsed.available_end_at ??
|
|
289
279
|
parsed.available_to ??
|
|
290
280
|
parsed.ends_before ??
|
|
291
281
|
parsed.ends_after ??
|
|
292
282
|
new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString());
|
|
293
|
-
const
|
|
283
|
+
const nodeCount = parsed.node_count ?? parsed.min_nodes ?? 1;
|
|
294
284
|
const gpusPerNode = parsed.gpus_per_node ?? 8;
|
|
295
|
-
const
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const
|
|
302
|
-
const
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
? {
|
|
313
|
-
startAt,
|
|
314
|
-
start_at: startAt,
|
|
315
|
-
endAt,
|
|
316
|
-
end_at: endAt,
|
|
317
|
-
capacityNodes,
|
|
318
|
-
capacity_nodes: capacityNodes,
|
|
285
|
+
const boundPayload = bound.payload;
|
|
286
|
+
const draft = Object.fromEntries(Object.entries(boundPayload).filter(([key]) => !LISTING_CREATE_INPUT_ONLY_KEYS.includes(key)));
|
|
287
|
+
const gpuSlug = normalized.gpu_type
|
|
288
|
+
? resolveCatalogTerm(hardware.terms, "gpu", normalized.gpu_type).canonical
|
|
289
|
+
: null;
|
|
290
|
+
const buyerPrice = parsed.buy_now_price_per_gpu_hour;
|
|
291
|
+
const supplierPrice = parsed.cost_per_gpu_hour ?? buyerPrice;
|
|
292
|
+
const supplierMicro = supplierPrice != null ? microUsd(supplierPrice) : null;
|
|
293
|
+
const buyerMicro = buyerPrice != null ? microUsd(buyerPrice) : supplierMicro;
|
|
294
|
+
if (supplierMicro != null && buyerMicro != null && buyerMicro < supplierMicro) {
|
|
295
|
+
return { ok: false, error: "price_below_supplier_price" };
|
|
296
|
+
}
|
|
297
|
+
const downPaymentCents = parsed.down_payment_cents ??
|
|
298
|
+
(parsed.deposit_percent != null && buyerMicro != null
|
|
299
|
+
? primaryDownPaymentCents({
|
|
300
|
+
pricePerGpuHourMicroUsd: buyerMicro,
|
|
301
|
+
nodeCount,
|
|
319
302
|
gpusPerNode,
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
: {}),
|
|
347
|
-
};
|
|
303
|
+
startsAt,
|
|
304
|
+
endsAt,
|
|
305
|
+
percent: parsed.deposit_percent,
|
|
306
|
+
})
|
|
307
|
+
: 0);
|
|
308
|
+
const listing = organizationId && facilityId && gpuSlug && supplierMicro != null && buyerMicro != null
|
|
309
|
+
? {
|
|
310
|
+
organizationId,
|
|
311
|
+
facilityId,
|
|
312
|
+
gpuSlug,
|
|
313
|
+
gpusPerNode,
|
|
314
|
+
accessMode: parsed.access_mode ?? "virtual-machine",
|
|
315
|
+
startsAt,
|
|
316
|
+
endsAt,
|
|
317
|
+
...(typeof draft.cpu === "string" ? { cpu: draft.cpu } : {}),
|
|
318
|
+
...(typeof draft.ram === "string" ? { ram: draft.ram } : {}),
|
|
319
|
+
...(typeof draft.storage === "string" ? { storage: draft.storage } : {}),
|
|
320
|
+
...(typeof draft.fabric_type === "string" ? { fabricType: draft.fabric_type } : {}),
|
|
321
|
+
...(typeof draft.internet === "string" ? { internet: draft.internet } : {}),
|
|
322
|
+
...(parsed.node_count ? { plannedNodeCount: parsed.node_count } : {}),
|
|
323
|
+
supplierPricePerGpuHourMicroUsd: supplierMicro,
|
|
324
|
+
spreadPerGpuHourMicroUsd: buyerMicro - supplierMicro,
|
|
325
|
+
downPaymentCents,
|
|
326
|
+
...(parsed.state ? { state: parsed.state } : {}),
|
|
327
|
+
}
|
|
328
|
+
: null;
|
|
348
329
|
if (!ctx.confirmed) {
|
|
349
|
-
return preview("create_listing",
|
|
330
|
+
return preview("create_listing", {
|
|
331
|
+
...draft,
|
|
332
|
+
node_count: nodeCount,
|
|
333
|
+
gpus_per_node: gpusPerNode,
|
|
334
|
+
...(listing ? { listing } : {}),
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
if (!listing) {
|
|
338
|
+
return {
|
|
339
|
+
ok: false,
|
|
340
|
+
error: !organizationId || !facilityId ? "facility_required" : gpuSlug ? "price_required" : "gpu_required",
|
|
341
|
+
};
|
|
350
342
|
}
|
|
351
|
-
return ctx.fetch("/
|
|
343
|
+
return ctx.fetch("/commerce/listings", { method: "POST", body: listing });
|
|
352
344
|
},
|
|
353
345
|
});
|
|
354
346
|
export const listingCapabilities = [
|
|
355
347
|
listCatalogTermsCapability,
|
|
356
348
|
listListingsCapability,
|
|
357
|
-
forecastListingsCapability,
|
|
358
349
|
createListingCapability,
|
|
359
350
|
];
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { preview, } from "../capability.js";
|
|
3
|
+
/** CLI semantic arguments arrive as strings; MCP and Slack send JSON values. */
|
|
4
|
+
export const positionals = z.array(z.string()).max(0).default([]);
|
|
5
|
+
export const flag = z.preprocess((value) => (value === "true" ? true : value === "false" ? false : value), z.boolean());
|
|
6
|
+
export function seg(value) {
|
|
7
|
+
return encodeURIComponent(value);
|
|
8
|
+
}
|
|
9
|
+
export function query(params) {
|
|
10
|
+
const parts = Object.entries(params)
|
|
11
|
+
.filter(([, value]) => value !== undefined && value !== null && String(value).trim() !== "")
|
|
12
|
+
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
|
13
|
+
return parts.length > 0 ? `?${parts.join("&")}` : "";
|
|
14
|
+
}
|
|
15
|
+
/** Without a CLI runtime or a supplied key, the transport mints one per write. */
|
|
16
|
+
export function idempotencyHeaders(ctx, supplied) {
|
|
17
|
+
const key = ctx.cli?.interaction?.idempotencyKey(supplied) ?? supplied?.trim();
|
|
18
|
+
return key ? { "Idempotency-Key": key } : undefined;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* One consent gate for every surface: the CLI reviews the terms in the
|
|
22
|
+
* terminal (or takes `--yes`), MCP and Slack return a preview until the
|
|
23
|
+
* caller confirms. `null` means go ahead.
|
|
24
|
+
*/
|
|
25
|
+
export async function consent(ctx, action, title, terms, payload) {
|
|
26
|
+
const interaction = ctx.cli?.interaction;
|
|
27
|
+
if (interaction) {
|
|
28
|
+
if (await interaction.review({ title, terms }))
|
|
29
|
+
return null;
|
|
30
|
+
return interaction.result({
|
|
31
|
+
data: null,
|
|
32
|
+
errors: [{ code: "action_declined", message: "No changes were made.", retryable: false }],
|
|
33
|
+
exitCode: 1,
|
|
34
|
+
outcome: { class: "ended", phase: "declined" },
|
|
35
|
+
presentation: { lines: ["No changes were made."] },
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return ctx.confirmed ? null : preview(action, payload);
|
|
39
|
+
}
|
|
40
|
+
export function microUsdLabel(value) {
|
|
41
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
42
|
+
? `$${(value / 1_000_000).toFixed(2)} per GPU-hour`
|
|
43
|
+
: "market";
|
|
44
|
+
}
|