@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.
Files changed (90) hide show
  1. package/README.md +351 -426
  2. package/package.json +12 -6
  3. package/src/admin-capacity-reservations.mjs +305 -0
  4. package/src/admin-command-handlers.mjs +57 -0
  5. package/src/admin-enrollment.mjs +681 -0
  6. package/src/admin-mutations.mjs +187 -0
  7. package/src/admin-node-diagnostics.mjs +412 -0
  8. package/src/admin-node-lifecycle.mjs +905 -0
  9. package/src/admin-shell.mjs +456 -0
  10. package/src/api-client.mjs +81 -28
  11. package/src/atomic-output.mjs +112 -0
  12. package/src/auth-store.mjs +182 -11
  13. package/src/capability-catalog.mjs +7 -0
  14. package/src/catalog-shell.mjs +1363 -0
  15. package/src/cli.mjs +123 -10266
  16. package/src/connect-mode-handlers.mjs +170 -0
  17. package/src/device-auth.mjs +179 -62
  18. package/src/evidence-commands.mjs +1354 -0
  19. package/src/external-bucket-commands.mjs +1374 -0
  20. package/src/gpu-catalog.mjs +21 -0
  21. package/src/identity-file.mjs +28 -0
  22. package/src/interaction-runtime.mjs +310 -0
  23. package/src/kubernetes-commands.mjs +2235 -0
  24. package/src/launch-mode-handlers.mjs +119 -0
  25. package/src/machine-reference.mjs +285 -0
  26. package/src/mount-commands.mjs +1148 -0
  27. package/src/session-commands.mjs +565 -0
  28. package/src/slurm-commands.mjs +274 -0
  29. package/src/startup.mjs +34 -11
  30. package/src/storage-commands.mjs +1164 -0
  31. package/src/update.mjs +172 -24
  32. package/src/vm-commands.mjs +4308 -0
  33. package/src/vpn-secret-output.mjs +150 -0
  34. package/src/wait-runtime.mjs +231 -0
  35. package/vendor/capabilities/capabilities/admin-capacity-reservations.js +96 -0
  36. package/vendor/capabilities/capabilities/admin-enrollment.js +321 -0
  37. package/vendor/capabilities/capabilities/admin-node-diagnostics.js +88 -0
  38. package/vendor/capabilities/capabilities/admin-nodes-lifecycle.js +235 -0
  39. package/vendor/capabilities/capabilities/admin.js +89 -0
  40. package/vendor/capabilities/capabilities/cli.js +156 -0
  41. package/vendor/capabilities/capabilities/clusters.js +150 -1
  42. package/vendor/capabilities/capabilities/commerce-market.js +330 -0
  43. package/vendor/capabilities/capabilities/compute.js +522 -0
  44. package/vendor/capabilities/capabilities/evidence.js +145 -0
  45. package/vendor/capabilities/capabilities/external-buckets.js +191 -0
  46. package/vendor/capabilities/capabilities/fleet.js +77 -25
  47. package/vendor/capabilities/capabilities/identity.js +114 -3
  48. package/vendor/capabilities/capabilities/listings.js +141 -144
  49. package/vendor/capabilities/capabilities/market-shared.js +44 -0
  50. package/vendor/capabilities/capabilities/markets.js +449 -0
  51. package/vendor/capabilities/capabilities/mounts.js +176 -0
  52. package/vendor/capabilities/capabilities/networks.js +1264 -0
  53. package/vendor/capabilities/capabilities/observability.js +38 -8
  54. package/vendor/capabilities/capabilities/tokens.js +32 -17
  55. package/vendor/capabilities/capabilities/volumes.js +178 -0
  56. package/vendor/capabilities/capability.js +21 -14
  57. package/vendor/capabilities/cli-catalog.js +67 -0
  58. package/vendor/capabilities/cli-migrations.js +261 -0
  59. package/vendor/capabilities/cli.js +7 -0
  60. package/vendor/capabilities/software-catalog.js +36 -0
  61. package/vendor/capabilities/spec-catalog.js +28 -5
  62. package/vendor/capabilities/supplier-directory.js +20 -0
  63. package/src/catalog-dispatch.mjs +0 -605
  64. package/src/color.mjs +0 -71
  65. package/vendor/capabilities/capabilities/clusters.d.ts +0 -2
  66. package/vendor/capabilities/capabilities/fleet.d.ts +0 -8
  67. package/vendor/capabilities/capabilities/identity.d.ts +0 -3
  68. package/vendor/capabilities/capabilities/listings.d.ts +0 -51
  69. package/vendor/capabilities/capabilities/observability.d.ts +0 -5
  70. package/vendor/capabilities/capabilities/staff-access.d.ts +0 -29
  71. package/vendor/capabilities/capabilities/staff-access.js +0 -278
  72. package/vendor/capabilities/capabilities/staff-commerce.d.ts +0 -160
  73. package/vendor/capabilities/capabilities/staff-commerce.js +0 -689
  74. package/vendor/capabilities/capabilities/staff-fleet.d.ts +0 -140
  75. package/vendor/capabilities/capabilities/staff-fleet.js +0 -538
  76. package/vendor/capabilities/capabilities/staff-gtm.d.ts +0 -142
  77. package/vendor/capabilities/capabilities/staff-gtm.js +0 -521
  78. package/vendor/capabilities/capabilities/staff-shared.d.ts +0 -47
  79. package/vendor/capabilities/capabilities/staff-shared.js +0 -98
  80. package/vendor/capabilities/capabilities/tokens.d.ts +0 -10
  81. package/vendor/capabilities/capabilities/users.d.ts +0 -2
  82. package/vendor/capabilities/capability.d.ts +0 -93
  83. package/vendor/capabilities/catalog.d.ts +0 -3
  84. package/vendor/capabilities/catalog.js +0 -26
  85. package/vendor/capabilities/index.d.ts +0 -16
  86. package/vendor/capabilities/index.js +0 -16
  87. package/vendor/capabilities/names.d.ts +0 -6
  88. package/vendor/capabilities/role.d.ts +0 -6
  89. package/vendor/capabilities/role.js +0 -17
  90. 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
- operatorId: z.string().optional(),
14
- operator_id: z.string().optional(),
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
- site_operator: z.string().optional(),
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.number().optional(),
49
- cost_per_gpu_hour: z.number().optional(),
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
- site_operator: { type: "string" },
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: ["reviewer", "admin"],
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: ["reviewer", "admin"],
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
- export const forecastListingsCapability = defineCapability({
151
- id: "listings.forecast",
152
- domain: "listings",
153
- roles: ["user"],
154
- description: "List the next 48 hourly availability buckets for customer-visible compute listings.",
155
- mutation: "read",
156
- http: { method: "GET", path: "/v1/workflows/commerce/listings/forecast" },
157
- slack: false,
158
- input: listingForecastInput,
159
- execute: async (ctx, input) => {
160
- const parsed = listingForecastInput.parse(input);
161
- return ctx.fetch(queryPath("/v1/workflows/commerce/listings/forecast", {
162
- limit: parsed.limit?.toString(),
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
- "operatorId",
195
- "operator_id",
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: ["reviewer", "admin"],
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. CLI asks y/N. Slack never POSTs until a human replies confirm. MCP creates on this tool call.",
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: "/inventory" },
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 operatorId = parsed.operatorId ?? parsed.operator_id;
231
+ let organizationId = parsed.organizationId ?? parsed.organization_id;
221
232
  let facilityId = parsed.facilityId ?? parsed.facility_id;
222
- if (ctx.confirmed || parsed.site_operator || parsed.site_nickname || parsed.site) {
223
- if (!operatorId) {
224
- try {
225
- const opsResp = (await ctx.fetch("/provisioning/operators", { method: "GET" }));
226
- const ops = opsResp?.operators ?? [];
227
- const query = (parsed.site_operator ?? parsed.site ?? "").trim().toLowerCase();
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
- if (!facilityId) {
244
- try {
245
- const facsResp = (await ctx.fetch("/provisioning/facilities", { method: "GET" }));
246
- const facs = facsResp?.facilities ?? [];
247
- const query = (parsed.site_nickname ?? parsed.site ?? "").trim().toLowerCase();
248
- if (query) {
249
- const match = facs.find((f) => f.facilityId.toLowerCase() === query ||
250
- f.slug.toLowerCase() === query ||
251
- f.displayName.toLowerCase() === query);
252
- if (match)
253
- facilityId = match.facilityId;
254
- }
255
- if (!facilityId && operatorId) {
256
- const match = facs.find((f) => f.operatorId === operatorId);
257
- if (match)
258
- facilityId = match.facilityId;
259
- }
260
- if (!facilityId && facs.length > 0) {
261
- facilityId = facs[0].facilityId;
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
- catch {
265
- // Ignore fetch failure
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 startAt/endAt as time.Time, so a bare "2026-09-01"
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 startAt = rfc3339(parsed.available_start_at ??
274
+ const startsAt = rfc3339(parsed.available_start_at ??
273
275
  parsed.available_from ??
274
276
  parsed.starts_after ??
275
277
  new Date().toISOString());
276
- // `ends_before` is the window's closing bound and the counterpart of the
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 capacityNodes = parsed.node_count ?? parsed.min_nodes ?? 1;
283
+ const nodeCount = parsed.node_count ?? parsed.min_nodes ?? 1;
286
284
  const gpusPerNode = parsed.gpus_per_node ?? 8;
287
- const buyNowPrice = parsed.buy_now_price_per_gpu_hour;
288
- const costPrice = parsed.cost_per_gpu_hour;
289
- // `deposit_percent` and `price_omitted` were both declared on the schema and
290
- // then never read, so they rode into the body under their own names and
291
- // commerce ignored them: a deposit plan never took, and a listing whose
292
- // source quoted no price still got the invented 1.00 floor below.
293
- const depositPercent = parsed.deposit_percent;
294
- const priceOmitted = parsed.price_omitted === true;
295
- const floorRate = costPrice != null ? String(costPrice) : priceOmitted ? null : "1.00";
296
- const { buy_now_price_per_gpu_hour: _, cost_per_gpu_hour: __, deposit_percent: ___, price_omitted: ____, ...boundPayload } = bound.payload;
297
- const cleanPayload = Object.fromEntries(Object.entries(boundPayload).filter(([key]) => !LISTING_CREATE_INPUT_ONLY_KEYS.includes(key)));
298
- const payload = {
299
- backing_mode: "forward",
300
- ...cleanPayload,
301
- ...(operatorId ? { operatorId, operator_id: operatorId } : {}),
302
- ...(facilityId ? { facilityId, facility_id: facilityId } : {}),
303
- ...(operatorId
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
- gpus_per_node: gpusPerNode,
313
- currency: "USD",
314
- buyNowEnabled: buyNowPrice != null,
315
- buy_now_enabled: buyNowPrice != null,
316
- ...(buyNowPrice != null
317
- ? {
318
- buyNowRatePerGpuHour: String(buyNowPrice),
319
- buy_now_rate_per_gpu_hour: String(buyNowPrice),
320
- }
321
- : {}),
322
- ...(depositPercent != null
323
- ? {
324
- allowsDepositPlan: true,
325
- allows_deposit_plan: true,
326
- downPaymentPct: String(depositPercent),
327
- down_payment_pct: String(depositPercent),
328
- }
329
- : {}),
330
- bidEnabled: true,
331
- bid_enabled: true,
332
- // Commerce takes a null floor rate; only a stated cost becomes one.
333
- // The old 1.00 fallback published a floor nobody had quoted.
334
- ...(floorRate != null
335
- ? { floorRatePerGpuHour: floorRate, floor_rate_per_gpu_hour: floorRate }
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", payload);
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("/inventory", { method: "POST", body: payload });
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
+ }