@ornncompute/cli 0.2.24 → 1.0.34
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 +351 -426
- package/package.json +12 -6
- package/src/admin-capacity-reservations.mjs +305 -0
- package/src/admin-command-handlers.mjs +57 -0
- package/src/admin-enrollment.mjs +681 -0
- package/src/admin-mutations.mjs +187 -0
- package/src/admin-node-diagnostics.mjs +412 -0
- package/src/admin-node-lifecycle.mjs +905 -0
- package/src/admin-shell.mjs +456 -0
- package/src/api-client.mjs +81 -28
- package/src/atomic-output.mjs +112 -0
- package/src/auth-store.mjs +182 -11
- package/src/capability-catalog.mjs +7 -0
- package/src/catalog-shell.mjs +1363 -0
- package/src/cli.mjs +123 -10266
- package/src/connect-mode-handlers.mjs +170 -0
- package/src/device-auth.mjs +179 -62
- package/src/evidence-commands.mjs +1354 -0
- package/src/external-bucket-commands.mjs +1374 -0
- package/src/gpu-catalog.mjs +21 -0
- package/src/identity-file.mjs +28 -0
- package/src/interaction-runtime.mjs +310 -0
- package/src/kubernetes-commands.mjs +2235 -0
- package/src/launch-mode-handlers.mjs +119 -0
- package/src/machine-reference.mjs +285 -0
- package/src/mount-commands.mjs +1148 -0
- package/src/session-commands.mjs +565 -0
- package/src/slurm-commands.mjs +274 -0
- package/src/startup.mjs +34 -11
- package/src/storage-commands.mjs +1164 -0
- package/src/update.mjs +172 -24
- package/src/vm-commands.mjs +4308 -0
- package/src/vpn-secret-output.mjs +150 -0
- package/src/wait-runtime.mjs +231 -0
- package/vendor/capabilities/capabilities/admin-capacity-reservations.js +96 -0
- package/vendor/capabilities/capabilities/admin-enrollment.js +321 -0
- package/vendor/capabilities/capabilities/admin-node-diagnostics.js +88 -0
- package/vendor/capabilities/capabilities/admin-nodes-lifecycle.js +235 -0
- package/vendor/capabilities/capabilities/admin.js +89 -0
- package/vendor/capabilities/capabilities/cli.js +156 -0
- package/vendor/capabilities/capabilities/clusters.js +150 -1
- package/vendor/capabilities/capabilities/commerce-market.js +330 -0
- package/vendor/capabilities/capabilities/compute.js +522 -0
- package/vendor/capabilities/capabilities/evidence.js +145 -0
- package/vendor/capabilities/capabilities/external-buckets.js +191 -0
- package/vendor/capabilities/capabilities/fleet.js +77 -25
- package/vendor/capabilities/capabilities/identity.js +114 -3
- package/vendor/capabilities/capabilities/listings.js +141 -144
- package/vendor/capabilities/capabilities/market-shared.js +44 -0
- package/vendor/capabilities/capabilities/markets.js +449 -0
- package/vendor/capabilities/capabilities/mounts.js +176 -0
- package/vendor/capabilities/capabilities/networks.js +1264 -0
- package/vendor/capabilities/capabilities/observability.js +38 -8
- package/vendor/capabilities/capabilities/tokens.js +32 -17
- package/vendor/capabilities/capabilities/volumes.js +178 -0
- package/vendor/capabilities/capability.js +21 -14
- package/vendor/capabilities/cli-catalog.js +67 -0
- package/vendor/capabilities/cli-migrations.js +261 -0
- package/vendor/capabilities/cli.js +7 -0
- package/vendor/capabilities/software-catalog.js +36 -0
- package/vendor/capabilities/spec-catalog.js +28 -5
- package/vendor/capabilities/supplier-directory.js +20 -0
- package/src/catalog-dispatch.mjs +0 -605
- package/src/color.mjs +0 -71
- package/vendor/capabilities/capabilities/clusters.d.ts +0 -2
- package/vendor/capabilities/capabilities/fleet.d.ts +0 -8
- package/vendor/capabilities/capabilities/identity.d.ts +0 -3
- package/vendor/capabilities/capabilities/listings.d.ts +0 -51
- package/vendor/capabilities/capabilities/observability.d.ts +0 -5
- package/vendor/capabilities/capabilities/staff-access.d.ts +0 -29
- package/vendor/capabilities/capabilities/staff-access.js +0 -278
- package/vendor/capabilities/capabilities/staff-commerce.d.ts +0 -160
- package/vendor/capabilities/capabilities/staff-commerce.js +0 -689
- package/vendor/capabilities/capabilities/staff-fleet.d.ts +0 -140
- package/vendor/capabilities/capabilities/staff-fleet.js +0 -538
- package/vendor/capabilities/capabilities/staff-gtm.d.ts +0 -142
- package/vendor/capabilities/capabilities/staff-gtm.js +0 -521
- package/vendor/capabilities/capabilities/staff-shared.d.ts +0 -47
- package/vendor/capabilities/capabilities/staff-shared.js +0 -98
- package/vendor/capabilities/capabilities/tokens.d.ts +0 -10
- package/vendor/capabilities/capabilities/users.d.ts +0 -2
- package/vendor/capabilities/capability.d.ts +0 -93
- package/vendor/capabilities/catalog.d.ts +0 -3
- package/vendor/capabilities/catalog.js +0 -26
- package/vendor/capabilities/index.d.ts +0 -16
- package/vendor/capabilities/index.js +0 -16
- package/vendor/capabilities/names.d.ts +0 -6
- package/vendor/capabilities/role.d.ts +0 -6
- package/vendor/capabilities/role.js +0 -17
- package/vendor/capabilities/spec-catalog.d.ts +0 -78
|
@@ -1,6 +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 { bindHardwareFields, HARDWARE_LISTING_FIELDS, HARDWARE_LISTING_FILTERS, loadHardwareCatalog, projectCatalogTerms, resolveCatalogTerm, } from "../spec-catalog.js";
|
|
4
5
|
function queryPath(path, params) {
|
|
5
6
|
const parts = [];
|
|
6
7
|
for (const [key, value] of Object.entries(params)) {
|
|
@@ -10,8 +11,8 @@ function queryPath(path, params) {
|
|
|
10
11
|
return parts.length ? `${path}?${parts.join("&")}` : path;
|
|
11
12
|
}
|
|
12
13
|
export const listingCreateInput = z.object({
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
organizationId: z.string().optional(),
|
|
15
|
+
organization_id: z.string().optional(),
|
|
15
16
|
facilityId: z.string().optional(),
|
|
16
17
|
facility_id: z.string().optional(),
|
|
17
18
|
kind: z.enum(["gpu", "cpu", "node", "link"]).optional(),
|
|
@@ -30,7 +31,7 @@ export const listingCreateInput = z.object({
|
|
|
30
31
|
node_count: z.number().int().optional(),
|
|
31
32
|
gpus_per_node: z.number().int().optional(),
|
|
32
33
|
site_nickname: z.string().optional(),
|
|
33
|
-
|
|
34
|
+
site_supplier: z.string().optional(),
|
|
34
35
|
ram: z.string().optional(),
|
|
35
36
|
storage: z.string().optional(),
|
|
36
37
|
internet: z.string().optional(),
|
|
@@ -45,19 +46,24 @@ export const listingCreateInput = z.object({
|
|
|
45
46
|
.describe("End of the availability window. YYYY-MM-DD or RFC3339."),
|
|
46
47
|
available_from: z.string().optional().describe("Alias for available_start_at."),
|
|
47
48
|
available_to: z.string().optional().describe("Alias for available_end_at."),
|
|
48
|
-
buy_now_price_per_gpu_hour: z
|
|
49
|
-
|
|
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)."),
|
|
50
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(),
|
|
51
61
|
price_omitted: z.boolean().optional(),
|
|
52
62
|
announce: z.boolean().optional(),
|
|
53
63
|
text: z.string().optional(),
|
|
54
64
|
confirm: z.boolean().optional(),
|
|
55
65
|
});
|
|
56
66
|
const catalogTermsInput = z.object({ kind: z.enum(["gpu", "cpu", "node", "link"]).optional() });
|
|
57
|
-
const listingForecastInput = z.object({
|
|
58
|
-
limit: z.number().int().min(1).max(500).optional(),
|
|
59
|
-
cursor: z.string().min(1).optional(),
|
|
60
|
-
});
|
|
61
67
|
const slackListingCreateParams = {
|
|
62
68
|
type: "object",
|
|
63
69
|
properties: {
|
|
@@ -68,7 +74,7 @@ const slackListingCreateParams = {
|
|
|
68
74
|
node_count: { type: "integer" },
|
|
69
75
|
gpus_per_node: { type: "integer" },
|
|
70
76
|
site_nickname: { type: "string" },
|
|
71
|
-
|
|
77
|
+
site_supplier: { type: "string" },
|
|
72
78
|
ram: { type: "string" },
|
|
73
79
|
storage: { type: "string" },
|
|
74
80
|
available_start_at: { type: "string" },
|
|
@@ -84,10 +90,10 @@ const slackListingCreateParams = {
|
|
|
84
90
|
export const listCatalogTermsCapability = defineCapability({
|
|
85
91
|
id: "listings.catalog_terms",
|
|
86
92
|
domain: "listings",
|
|
87
|
-
roles: ["
|
|
93
|
+
roles: ["admin"],
|
|
88
94
|
description: "Dump the orchestrator hardware dictionary (display name + aliases). Call once and pick from it. Hardware fields on listings must match a term. Optional kind filter: gpu, cpu, node, or link.",
|
|
89
95
|
mutation: "read",
|
|
90
|
-
http: { method: "GET", path: "/v1/orchestrator/catalog" },
|
|
96
|
+
http: { method: "GET", path: "/v1/orchestrator/catalog/hardware" },
|
|
91
97
|
slack: {
|
|
92
98
|
parameters: {
|
|
93
99
|
type: "object",
|
|
@@ -106,7 +112,7 @@ export const listCatalogTermsCapability = defineCapability({
|
|
|
106
112
|
export const listListingsCapability = defineCapability({
|
|
107
113
|
id: "listings.available",
|
|
108
114
|
domain: "listings",
|
|
109
|
-
roles: ["
|
|
115
|
+
roles: ["user", "admin"],
|
|
110
116
|
description: "List currently available marketplace listings. GPU, CPU, and fabric filters must be orchestrator catalog display names, canonicals, or aliases — not raw slang or spec-sheet text. Site may be a facility nickname. Returns spec-shaped JSON plus public reserve/OG/PDF URLs. Filters combine as AND. Date filters are inclusive YYYY-MM-DD.",
|
|
111
117
|
mutation: "read",
|
|
112
118
|
slack: {
|
|
@@ -147,23 +153,19 @@ export const listListingsCapability = defineCapability({
|
|
|
147
153
|
return ctx.fetch("/listings/available", { method: "GET", body: bound.payload });
|
|
148
154
|
},
|
|
149
155
|
});
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
cursor: parsed.cursor,
|
|
164
|
-
}), { method: "GET" });
|
|
165
|
-
},
|
|
166
|
-
});
|
|
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
|
+
}
|
|
167
169
|
/** Commerce parses listing windows as RFC3339 timestamps, not bare dates. */
|
|
168
170
|
function rfc3339(value) {
|
|
169
171
|
const trimmed = value.trim();
|
|
@@ -191,18 +193,27 @@ const LISTING_CREATE_INPUT_ONLY_KEYS = [
|
|
|
191
193
|
"site",
|
|
192
194
|
"kind",
|
|
193
195
|
"confirm",
|
|
194
|
-
"
|
|
195
|
-
"
|
|
196
|
+
"organizationId",
|
|
197
|
+
"organization_id",
|
|
198
|
+
"site_supplier",
|
|
196
199
|
"facilityId",
|
|
197
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",
|
|
198
208
|
];
|
|
199
209
|
export const createListingCapability = defineCapability({
|
|
200
210
|
id: "listings.create",
|
|
201
211
|
domain: "listings",
|
|
202
|
-
roles: ["
|
|
203
|
-
description: "Create a marketplace listing. gpu_type, cpu, node_model, and fabric_type must exact-match an orchestrator catalog display name, canonical, or alias
|
|
212
|
+
roles: ["admin"],
|
|
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.",
|
|
204
215
|
mutation: "preview-confirm",
|
|
205
|
-
http: { method: "POST", path: "/
|
|
216
|
+
http: { method: "POST", path: "/commerce/listings" },
|
|
206
217
|
slack: { parameters: slackListingCreateParams },
|
|
207
218
|
input: listingCreateInput,
|
|
208
219
|
execute: async (ctx, input) => {
|
|
@@ -217,137 +228,123 @@ export const createListingCapability = defineCapability({
|
|
|
217
228
|
const bound = bindHardwareFields(hardware.terms, normalized, HARDWARE_LISTING_FIELDS);
|
|
218
229
|
if (!bound.ok)
|
|
219
230
|
return bound;
|
|
220
|
-
let
|
|
231
|
+
let organizationId = parsed.organizationId ?? parsed.organization_id;
|
|
221
232
|
let facilityId = parsed.facilityId ?? parsed.facility_id;
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (query) {
|
|
229
|
-
const match = ops.find((o) => o.operatorId.toLowerCase() === query ||
|
|
230
|
-
o.slug.toLowerCase() === query ||
|
|
231
|
-
o.displayName.toLowerCase() === query);
|
|
232
|
-
if (match)
|
|
233
|
-
operatorId = match.operatorId;
|
|
234
|
-
}
|
|
235
|
-
if (!operatorId && ops.length > 0) {
|
|
236
|
-
operatorId = ops[0].operatorId;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
catch {
|
|
240
|
-
// Ignore fetch failure (e.g. in unit tests)
|
|
241
|
-
}
|
|
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" };
|
|
242
239
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if (
|
|
261
|
-
|
|
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");
|
|
262
259
|
}
|
|
260
|
+
seenCursors.add(cursor);
|
|
263
261
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
262
|
+
} while (cursor);
|
|
263
|
+
if (matches.length !== 1) {
|
|
264
|
+
return { ok: false, error: matches.length ? "ambiguous_facility" : "facility_not_found" };
|
|
267
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" };
|
|
268
270
|
}
|
|
269
271
|
// Every window alias resolves to the same pair, and both are normalized:
|
|
270
|
-
// commerce decodes
|
|
272
|
+
// commerce decodes startsAt/endsAt as time.Time, so a bare "2026-09-01"
|
|
271
273
|
// fails the whole request body rather than the one field.
|
|
272
|
-
const
|
|
274
|
+
const startsAt = rfc3339(parsed.available_start_at ??
|
|
273
275
|
parsed.available_from ??
|
|
274
276
|
parsed.starts_after ??
|
|
275
277
|
new Date().toISOString());
|
|
276
|
-
|
|
277
|
-
// `starts_after` the opening bound already accepts. Only `ends_after` was
|
|
278
|
-
// consulted here, so naming an end with `ends_before` was silently dropped
|
|
279
|
-
// and endAt fell back to the +30d default.
|
|
280
|
-
const endAt = rfc3339(parsed.available_end_at ??
|
|
278
|
+
const endsAt = rfc3339(parsed.available_end_at ??
|
|
281
279
|
parsed.available_to ??
|
|
282
280
|
parsed.ends_before ??
|
|
283
281
|
parsed.ends_after ??
|
|
284
282
|
new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString());
|
|
285
|
-
const
|
|
283
|
+
const nodeCount = parsed.node_count ?? parsed.min_nodes ?? 1;
|
|
286
284
|
const gpusPerNode = parsed.gpus_per_node ?? 8;
|
|
287
|
-
const
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
const
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
? {
|
|
305
|
-
startAt,
|
|
306
|
-
start_at: startAt,
|
|
307
|
-
endAt,
|
|
308
|
-
end_at: endAt,
|
|
309
|
-
capacityNodes,
|
|
310
|
-
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,
|
|
311
302
|
gpusPerNode,
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
: {}),
|
|
339
|
-
};
|
|
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;
|
|
340
329
|
if (!ctx.confirmed) {
|
|
341
|
-
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
|
+
};
|
|
342
342
|
}
|
|
343
|
-
return ctx.fetch("/
|
|
343
|
+
return ctx.fetch("/commerce/listings", { method: "POST", body: listing });
|
|
344
344
|
},
|
|
345
345
|
});
|
|
346
346
|
export const listingCapabilities = [
|
|
347
347
|
listCatalogTermsCapability,
|
|
348
348
|
listListingsCapability,
|
|
349
|
-
forecastListingsCapability,
|
|
350
349
|
createListingCapability,
|
|
351
350
|
];
|
|
352
|
-
/** @deprecated Use createListingCapability. */
|
|
353
|
-
export const proposeListingCapability = createListingCapability;
|
|
@@ -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
|
+
}
|