@ornncompute/cli 0.2.24 → 1.0.25
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 +298 -426
- package/package.json +12 -6
- package/src/admin-capacity-reservations.mjs +280 -0
- package/src/admin-command-handlers.mjs +57 -0
- package/src/admin-enrollment.mjs +680 -0
- package/src/admin-mutations.mjs +187 -0
- package/src/admin-node-diagnostics.mjs +412 -0
- package/src/admin-node-lifecycle.mjs +908 -0
- package/src/admin-shell.mjs +322 -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 +1350 -0
- package/src/cli.mjs +123 -10266
- package/src/connect-mode-handlers.mjs +170 -0
- package/src/device-auth.mjs +178 -61
- package/src/evidence-commands.mjs +1340 -0
- package/src/external-bucket-commands.mjs +1374 -0
- package/src/interaction-runtime.mjs +310 -0
- package/src/kubernetes-commands.mjs +2229 -0
- package/src/launch-mode-handlers.mjs +119 -0
- package/src/machine-reference.mjs +277 -0
- package/src/mount-commands.mjs +1146 -0
- package/src/session-commands.mjs +565 -0
- package/src/slurm-commands.mjs +263 -0
- package/src/startup.mjs +34 -11
- package/src/storage-commands.mjs +1144 -0
- package/src/update.mjs +172 -24
- package/src/vm-commands.mjs +4273 -0
- package/src/vpn-secret-output.mjs +150 -0
- package/src/wait-runtime.mjs +231 -0
- package/vendor/capabilities/capabilities/admin-capacity-reservations.js +126 -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/capacity-offers.js +368 -0
- package/vendor/capabilities/capabilities/cli.js +156 -0
- package/vendor/capabilities/capabilities/clusters.js +150 -1
- 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 +21 -5
- package/vendor/capabilities/capabilities/identity.js +114 -3
- package/vendor/capabilities/capabilities/listings.js +14 -8
- package/vendor/capabilities/capabilities/mounts.js +176 -0
- package/vendor/capabilities/capabilities/networks.js +1257 -0
- package/vendor/capabilities/capabilities/observability.js +38 -8
- package/vendor/capabilities/capabilities/tokens.js +20 -8
- package/vendor/capabilities/capabilities/volumes.js +178 -0
- package/vendor/capabilities/capability.js +19 -13
- package/vendor/capabilities/cli-catalog.js +65 -0
- package/vendor/capabilities/cli-migrations.js +288 -0
- package/vendor/capabilities/cli.js +7 -0
- package/vendor/capabilities/software-catalog.js +36 -0
- package/vendor/capabilities/spec-catalog.js +2 -2
- 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
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineCapability } from "../capability.js";
|
|
3
|
+
import { listFacilitiesCapability, listNodesCapability, listOperatorsCapability } from "./fleet.js";
|
|
4
|
+
import { logsLatestCapability, telemetryLatestCapability } from "./observability.js";
|
|
5
|
+
import { tokensListCapability } from "./tokens.js";
|
|
6
|
+
import { usersQueryCapability } from "./users.js";
|
|
7
|
+
import { adminCapacityOffersCreateCapability } from "./admin-capacity-reservations.js";
|
|
8
|
+
export const ADMIN_CLI_ROOTS = [
|
|
9
|
+
{ name: "users" },
|
|
10
|
+
{ name: "operators" },
|
|
11
|
+
{ name: "facilities" },
|
|
12
|
+
{ name: "nodes" },
|
|
13
|
+
{ name: "enrollment-tokens" },
|
|
14
|
+
{ name: "capacity-offers" },
|
|
15
|
+
{ name: "reservations" },
|
|
16
|
+
];
|
|
17
|
+
function adminHelp(command, outcome, human, automation, acceptedReferences = []) {
|
|
18
|
+
return {
|
|
19
|
+
audience: "admin",
|
|
20
|
+
category: "resource",
|
|
21
|
+
command,
|
|
22
|
+
outcome,
|
|
23
|
+
human,
|
|
24
|
+
automation,
|
|
25
|
+
acceptedReferences,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function withAdminBinding(capability, help) {
|
|
29
|
+
if (!help.command)
|
|
30
|
+
throw new Error(`${capability.id} requires a canonical CLI command`);
|
|
31
|
+
return {
|
|
32
|
+
...capability,
|
|
33
|
+
cli: {
|
|
34
|
+
command: [...help.command],
|
|
35
|
+
vnext: help,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export const adminUsersSearchCapability = withAdminBinding(usersQueryCapability, adminHelp(["admin", "users", "search"], "Search the staff-authorized user directory.", "ornn admin users search [--name NAME] [--email EMAIL] [--organization NAME] [--limit 50] [--cursor CURSOR]", "ornn admin users search --email EMAIL --limit 50 [--cursor CURSOR] --format json"));
|
|
40
|
+
export const adminOperatorsListCapability = withAdminBinding(listOperatorsCapability, adminHelp(["admin", "operators", "list"], "List operators visible to staff.", "ornn admin operators list [--limit 50] [--cursor CURSOR]", "ornn admin operators list --limit 50 --format json"));
|
|
41
|
+
export const adminFacilitiesListCapability = withAdminBinding(listFacilitiesCapability, adminHelp(["admin", "facilities", "list"], "List facilities visible to staff.", "ornn admin facilities list [--operator OPERATOR] [--limit 50] [--cursor CURSOR]", "ornn admin facilities list --limit 50 --format json"));
|
|
42
|
+
export const adminNodesListCapability = withAdminBinding(listNodesCapability, adminHelp(["admin", "nodes", "list"], "List enrolled, ghost, and off-grid nodes.", "ornn admin nodes list [--operator OPERATOR] [--facility FACILITY] [--status STATUS] [--email EMAIL] [--limit 50] [--cursor CURSOR]", "ornn admin nodes list --limit 50 [--cursor CURSOR] --format json"));
|
|
43
|
+
const nodeInput = z.object({ node_id: z.string().min(1) });
|
|
44
|
+
export const adminNodeHealthCapability = defineCapability({
|
|
45
|
+
id: "nodes.health",
|
|
46
|
+
domain: "fleet",
|
|
47
|
+
roles: ["admin"],
|
|
48
|
+
description: "Read lifecycle and readiness facts for one enrolled node.",
|
|
49
|
+
mutation: "read",
|
|
50
|
+
http: { method: "GET", path: "/internal/nodes/{id}" },
|
|
51
|
+
cli: {
|
|
52
|
+
command: ["admin", "nodes", "health"],
|
|
53
|
+
vnext: adminHelp(["admin", "nodes", "health"], "Inspect lifecycle and readiness facts for one node.", "ornn admin nodes health <node-id>", "ornn admin nodes health <node-id> --format json", ["canonical node ID"]),
|
|
54
|
+
},
|
|
55
|
+
mcp: false,
|
|
56
|
+
slack: false,
|
|
57
|
+
input: nodeInput,
|
|
58
|
+
execute: async (ctx, input) => {
|
|
59
|
+
const parsed = nodeInput.parse(input);
|
|
60
|
+
return ctx.fetch(`/internal/nodes/${encodeURIComponent(parsed.node_id)}`, { method: "GET" });
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
export const adminNodeMetricsCapability = withAdminBinding(telemetryLatestCapability, adminHelp(["admin", "nodes", "metrics"], "Read a bounded telemetry window for one node.", "ornn admin nodes metrics <node-id> [--span 15m] [--start TIME --end TIME] [--max-points 240]", "ornn admin nodes metrics <node-id> --span 15m [--start TIME --end TIME] --max-points 240 --format json", ["canonical node ID"]));
|
|
64
|
+
export const adminNodeLogsCapability = withAdminBinding(logsLatestCapability, adminHelp(["admin", "nodes", "logs"], "Read one bounded node log page.", "ornn admin nodes logs <node-id> [--stream kernel|serial] [--count 200] [--before TIME] [--cursor CURSOR]", "ornn admin nodes logs <node-id> --stream kernel --count 200 [--before TIME] [--cursor CURSOR] --format json", ["canonical node ID"]));
|
|
65
|
+
export const adminEnrollmentTokensListCapability = withAdminBinding(tokensListCapability, adminHelp(["admin", "enrollment-tokens", "list"], "List enrollment-token metadata without token bytes.", "ornn admin enrollment-tokens list [--limit 50] [--cursor CURSOR]", "ornn admin enrollment-tokens list --limit 50 --format json"));
|
|
66
|
+
export const adminCapabilities = [
|
|
67
|
+
adminUsersSearchCapability,
|
|
68
|
+
adminOperatorsListCapability,
|
|
69
|
+
adminFacilitiesListCapability,
|
|
70
|
+
adminNodesListCapability,
|
|
71
|
+
adminNodeHealthCapability,
|
|
72
|
+
adminNodeMetricsCapability,
|
|
73
|
+
adminNodeLogsCapability,
|
|
74
|
+
adminEnrollmentTokensListCapability,
|
|
75
|
+
];
|
|
76
|
+
const replacements = {
|
|
77
|
+
"listings.create": adminCapacityOffersCreateCapability,
|
|
78
|
+
"facilities.list": adminFacilitiesListCapability,
|
|
79
|
+
"logs.latest": adminNodeLogsCapability,
|
|
80
|
+
"nodes.list": adminNodesListCapability,
|
|
81
|
+
"operator.list": adminOperatorsListCapability,
|
|
82
|
+
"telemetry.latest": adminNodeMetricsCapability,
|
|
83
|
+
"tokens.list": adminEnrollmentTokensListCapability,
|
|
84
|
+
"users.query": adminUsersSearchCapability,
|
|
85
|
+
};
|
|
86
|
+
/** Applies canonical admin CLI bindings without changing MCP or Slack identities. */
|
|
87
|
+
export function bindAdminCapabilities(capabilities) {
|
|
88
|
+
return capabilities.map((capability) => replacements[capability.id] ?? capability);
|
|
89
|
+
}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineCapability, } from "../capability.js";
|
|
3
|
+
const CANONICAL_REFERENCE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
4
|
+
const BASE64URL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
5
|
+
const EXACT_DECIMAL = /^(?:0|[1-9]\d*)(?:\.\d+)?$/;
|
|
6
|
+
const DEFAULT_LIMIT = 50;
|
|
7
|
+
export const capacityOffersListInput = z.object({
|
|
8
|
+
$positionals: z.array(z.string()).max(0).default([]),
|
|
9
|
+
cursor: z.string().min(1).optional(),
|
|
10
|
+
limit: z.coerce.number().int().min(1).max(500).optional(),
|
|
11
|
+
});
|
|
12
|
+
export const capacityOfferShowInput = z.object({
|
|
13
|
+
$positionals: z.array(z.string()).max(0).default([]),
|
|
14
|
+
offer: z.string().min(1).optional(),
|
|
15
|
+
});
|
|
16
|
+
function requireInteraction(context) {
|
|
17
|
+
const interaction = context.cli?.interaction;
|
|
18
|
+
if (!interaction)
|
|
19
|
+
throw new Error("Capacity offers require the Ornn CLI interaction runtime.");
|
|
20
|
+
return interaction;
|
|
21
|
+
}
|
|
22
|
+
function record(value) {
|
|
23
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
24
|
+
? value
|
|
25
|
+
: {};
|
|
26
|
+
}
|
|
27
|
+
function text(value) {
|
|
28
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
29
|
+
}
|
|
30
|
+
function integer(value) {
|
|
31
|
+
return typeof value === "number" && Number.isSafeInteger(value) ? value : null;
|
|
32
|
+
}
|
|
33
|
+
function exactDecimal(value) {
|
|
34
|
+
const candidate = text(value);
|
|
35
|
+
return candidate && EXACT_DECIMAL.test(candidate) ? candidate : null;
|
|
36
|
+
}
|
|
37
|
+
function requireReference(value, field, interaction) {
|
|
38
|
+
if (!value)
|
|
39
|
+
interaction.usage(`--${field} is required.`, { code: "missing_input", field });
|
|
40
|
+
if (!CANONICAL_REFERENCE.test(value)) {
|
|
41
|
+
interaction.usage(`--${field} must be one canonical identifier.`, {
|
|
42
|
+
code: "invalid_reference",
|
|
43
|
+
field,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
function offerFromWire(value, interaction) {
|
|
49
|
+
const item = record(value);
|
|
50
|
+
const ref = text(item.id ?? item.ref);
|
|
51
|
+
const gpuType = text(item.gpu_type ?? item.hardware);
|
|
52
|
+
const gpusPerMachine = integer(item.gpus_per_node ?? item.gpus_per_machine);
|
|
53
|
+
const machines = integer(item.available_node_count ?? item.node_count ?? item.machine_count);
|
|
54
|
+
const gpus = integer(item.available_gpu_count ?? item.gpu_count);
|
|
55
|
+
const facility = text(item.site_nickname ?? item.facility_name ?? item.facility);
|
|
56
|
+
const start = text(item.available_start_at ?? item.start_at ?? item.available_from);
|
|
57
|
+
const end = text(item.available_end_at ?? item.end_at ?? item.available_to);
|
|
58
|
+
const currency = text(item.currency)?.toUpperCase() ?? null;
|
|
59
|
+
const rawFixedValue = item.buy_now_price_per_gpu_hour_exact ?? item.ask_price_per_gpu_hour_exact;
|
|
60
|
+
const fixedValue = exactDecimal(rawFixedValue);
|
|
61
|
+
if (!ref ||
|
|
62
|
+
!CANONICAL_REFERENCE.test(ref) ||
|
|
63
|
+
!gpuType ||
|
|
64
|
+
gpusPerMachine === null ||
|
|
65
|
+
gpusPerMachine < 1 ||
|
|
66
|
+
machines === null ||
|
|
67
|
+
machines < 0 ||
|
|
68
|
+
gpus === null ||
|
|
69
|
+
gpus < 0 ||
|
|
70
|
+
!facility ||
|
|
71
|
+
!start ||
|
|
72
|
+
!end ||
|
|
73
|
+
!currency ||
|
|
74
|
+
!/^[A-Z]{3}$/.test(currency) ||
|
|
75
|
+
(rawFixedValue != null && !fixedValue)) {
|
|
76
|
+
interaction.error("The gateway returned an incomplete capacity offer.", {
|
|
77
|
+
code: "invalid_gateway_response",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
ref,
|
|
82
|
+
hardware: {
|
|
83
|
+
gpu_type: gpuType,
|
|
84
|
+
gpus_per_machine: gpusPerMachine,
|
|
85
|
+
cpu: text(item.cpu),
|
|
86
|
+
ram: text(item.ram),
|
|
87
|
+
storage: text(item.storage),
|
|
88
|
+
fabric: text(item.fabric_type ?? item.fabric),
|
|
89
|
+
internet: text(item.internet),
|
|
90
|
+
},
|
|
91
|
+
location: { facility },
|
|
92
|
+
availability: { machine_count: machines, gpu_count: gpus, start, end },
|
|
93
|
+
fixedPrice: fixedValue ? { value: fixedValue, currency, unit: "gpu_hour" } : null,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function offerData(offer) {
|
|
97
|
+
return {
|
|
98
|
+
ref: offer.ref,
|
|
99
|
+
kind: "capacity_offer",
|
|
100
|
+
hardware: offer.hardware,
|
|
101
|
+
location: offer.location,
|
|
102
|
+
availability: offer.availability,
|
|
103
|
+
fixed_price: offer.fixedPrice,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function query(parameters) {
|
|
107
|
+
const encoded = Object.entries(parameters)
|
|
108
|
+
.filter((entry) => entry[1] !== undefined)
|
|
109
|
+
.map(([name, value]) => `${encodeURIComponent(name)}=${encodeURIComponent(String(value))}`)
|
|
110
|
+
.join("&");
|
|
111
|
+
return encoded ? `?${encoded}` : "";
|
|
112
|
+
}
|
|
113
|
+
function encodeOpaque(value) {
|
|
114
|
+
const input = encodeURIComponent(value);
|
|
115
|
+
let output = "";
|
|
116
|
+
for (let index = 0; index < input.length; index += 3) {
|
|
117
|
+
const first = input.charCodeAt(index);
|
|
118
|
+
const second = index + 1 < input.length ? input.charCodeAt(index + 1) : 0;
|
|
119
|
+
const third = index + 2 < input.length ? input.charCodeAt(index + 2) : 0;
|
|
120
|
+
output += BASE64URL[first >> 2];
|
|
121
|
+
output += BASE64URL[((first & 3) << 4) | (second >> 4)];
|
|
122
|
+
if (index + 1 < input.length)
|
|
123
|
+
output += BASE64URL[((second & 15) << 2) | (third >> 6)];
|
|
124
|
+
if (index + 2 < input.length)
|
|
125
|
+
output += BASE64URL[third & 63];
|
|
126
|
+
}
|
|
127
|
+
return output;
|
|
128
|
+
}
|
|
129
|
+
function decodeOpaque(value) {
|
|
130
|
+
if (!value || !/^[A-Za-z0-9_-]+$/.test(value))
|
|
131
|
+
throw new Error("invalid cursor encoding");
|
|
132
|
+
let output = "";
|
|
133
|
+
for (let index = 0; index < value.length; index += 4) {
|
|
134
|
+
const first = BASE64URL.indexOf(value[index]);
|
|
135
|
+
const second = BASE64URL.indexOf(value[index + 1]);
|
|
136
|
+
const third = index + 2 < value.length ? BASE64URL.indexOf(value[index + 2]) : 0;
|
|
137
|
+
const fourth = index + 3 < value.length ? BASE64URL.indexOf(value[index + 3]) : 0;
|
|
138
|
+
if (first < 0 || second < 0 || third < 0 || fourth < 0) {
|
|
139
|
+
throw new Error("invalid cursor encoding");
|
|
140
|
+
}
|
|
141
|
+
output += String.fromCharCode((first << 2) | (second >> 4));
|
|
142
|
+
if (index + 2 < value.length)
|
|
143
|
+
output += String.fromCharCode(((second & 15) << 4) | (third >> 2));
|
|
144
|
+
if (index + 3 < value.length)
|
|
145
|
+
output += String.fromCharCode(((third & 3) << 6) | fourth);
|
|
146
|
+
}
|
|
147
|
+
return decodeURIComponent(output);
|
|
148
|
+
}
|
|
149
|
+
function encodeCursor(cursor) {
|
|
150
|
+
return `ornn-capacity-v1.${encodeOpaque(JSON.stringify(cursor))}`;
|
|
151
|
+
}
|
|
152
|
+
function decodeCursor(raw, interaction) {
|
|
153
|
+
if (!raw)
|
|
154
|
+
return { source: "primary", cursor: null };
|
|
155
|
+
const prefix = "ornn-capacity-v1.";
|
|
156
|
+
try {
|
|
157
|
+
if (!raw.startsWith(prefix))
|
|
158
|
+
throw new Error("wrong cursor kind");
|
|
159
|
+
const decoded = JSON.parse(decodeOpaque(raw.slice(prefix.length)));
|
|
160
|
+
if (!decoded ||
|
|
161
|
+
(decoded.source !== "primary" && decoded.source !== "resale") ||
|
|
162
|
+
(decoded.cursor !== null && typeof decoded.cursor !== "string")) {
|
|
163
|
+
throw new Error("invalid cursor payload");
|
|
164
|
+
}
|
|
165
|
+
return decoded;
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
interaction.usage("--cursor is not a capacity-offer cursor returned by Ornn.", {
|
|
169
|
+
code: "invalid_cursor",
|
|
170
|
+
field: "cursor",
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async function readPage(context, source, limit, cursor) {
|
|
175
|
+
const path = source === "primary"
|
|
176
|
+
? "/v1/workflows/commerce/listings"
|
|
177
|
+
: "/v1/workflows/commerce/resale-listings";
|
|
178
|
+
const payload = record(await context.fetch(`${path}${query({ limit, cursor: cursor ?? undefined })}`));
|
|
179
|
+
const rows = Array.isArray(payload.listings)
|
|
180
|
+
? payload.listings
|
|
181
|
+
: source === "primary" && Array.isArray(payload.items)
|
|
182
|
+
? payload.items
|
|
183
|
+
: [];
|
|
184
|
+
return { rows, next: text(payload.next_cursor) };
|
|
185
|
+
}
|
|
186
|
+
async function executeCapacityOffersList(context, input) {
|
|
187
|
+
const interaction = requireInteraction(context);
|
|
188
|
+
const limit = input.limit ?? DEFAULT_LIMIT;
|
|
189
|
+
const position = decodeCursor(input.cursor, interaction);
|
|
190
|
+
let page = await readPage(context, position.source, limit, position.cursor);
|
|
191
|
+
let source = position.source;
|
|
192
|
+
if (source === "primary" && page.rows.length === 0 && page.next === null) {
|
|
193
|
+
source = "resale";
|
|
194
|
+
page = await readPage(context, source, limit, null);
|
|
195
|
+
}
|
|
196
|
+
const offers = page.rows.map((item) => offerFromWire(item, interaction));
|
|
197
|
+
let nextCursor = page.next ? encodeCursor({ source, cursor: page.next }) : null;
|
|
198
|
+
if (source === "primary" && page.next === null && offers.length > 0) {
|
|
199
|
+
const resaleProbe = await readPage(context, "resale", 1, null);
|
|
200
|
+
if (resaleProbe.rows.length > 0)
|
|
201
|
+
nextCursor = encodeCursor({ source: "resale", cursor: null });
|
|
202
|
+
}
|
|
203
|
+
const next = nextCursor
|
|
204
|
+
? `ornn capacity-offers list --limit ${limit} --cursor ${nextCursor}`
|
|
205
|
+
: offers[0]
|
|
206
|
+
? `ornn capacity-offers show --offer ${offers[0].ref}`
|
|
207
|
+
: "ornn capacity-offers forecast";
|
|
208
|
+
return interaction.result({
|
|
209
|
+
data: {
|
|
210
|
+
capacity_offers: offers.map(offerData),
|
|
211
|
+
defaults: { limit },
|
|
212
|
+
},
|
|
213
|
+
outcome: { class: "ready", phase: offers.length ? "listed" : "empty" },
|
|
214
|
+
page: { limit, returned: offers.length, next_cursor: nextCursor },
|
|
215
|
+
presentation: {
|
|
216
|
+
lines: offers.length
|
|
217
|
+
? offers.map((offer) => {
|
|
218
|
+
const price = offer.fixedPrice
|
|
219
|
+
? `${offer.fixedPrice.value} ${offer.fixedPrice.currency}/GPU-hour`
|
|
220
|
+
: "price unavailable";
|
|
221
|
+
return `${offer.ref} ${offer.hardware.gpu_type} ${offer.location.facility} ${offer.availability.machine_count} machines ${price}`;
|
|
222
|
+
})
|
|
223
|
+
: ["No capacity offers are available."],
|
|
224
|
+
next,
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
async function executeCapacityOfferShow(context, input) {
|
|
229
|
+
const interaction = requireInteraction(context);
|
|
230
|
+
const ref = requireReference(input.offer, "offer", interaction);
|
|
231
|
+
const offer = offerFromWire(await context.fetch(`/v1/workflows/commerce/listings/${encodeURIComponent(ref)}`), interaction);
|
|
232
|
+
if (offer.ref !== ref) {
|
|
233
|
+
interaction.error("The gateway returned a different capacity offer.", {
|
|
234
|
+
code: "capacity_offer_correlation_changed",
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
const price = offer.fixedPrice
|
|
238
|
+
? `${offer.fixedPrice.value} ${offer.fixedPrice.currency} per GPU-hour`
|
|
239
|
+
: "Unavailable";
|
|
240
|
+
return interaction.result({
|
|
241
|
+
data: { capacity_offer: offerData(offer), defaults: {} },
|
|
242
|
+
outcome: { class: "ready", phase: "available" },
|
|
243
|
+
presentation: {
|
|
244
|
+
lines: [
|
|
245
|
+
`Capacity offer ${offer.ref}`,
|
|
246
|
+
`Hardware: ${offer.hardware.gpu_type}, ${offer.hardware.gpus_per_machine} GPUs per machine`,
|
|
247
|
+
`Location: ${offer.location.facility}`,
|
|
248
|
+
`Availability: ${offer.availability.machine_count} machines, ${offer.availability.start} to ${offer.availability.end}`,
|
|
249
|
+
`Price: ${price}`,
|
|
250
|
+
],
|
|
251
|
+
next: "ornn capacity-offers list",
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
export async function executeCapacityOfferForecast(context, input) {
|
|
256
|
+
const interaction = requireInteraction(context);
|
|
257
|
+
const limit = input.limit ?? DEFAULT_LIMIT;
|
|
258
|
+
const payload = record(await context.fetch(`/v1/workflows/commerce/listings/forecast${query({ limit, cursor: input.cursor })}`));
|
|
259
|
+
const rawListings = Array.isArray(payload.listings) ? payload.listings : [];
|
|
260
|
+
const offers = rawListings.map((item) => {
|
|
261
|
+
const row = record(item);
|
|
262
|
+
const offer = offerFromWire(row, interaction);
|
|
263
|
+
const availability = Array.isArray(row.availability)
|
|
264
|
+
? row.availability.map((bucket) => {
|
|
265
|
+
const interval = record(bucket);
|
|
266
|
+
const start = text(interval.start_at);
|
|
267
|
+
const end = text(interval.end_at);
|
|
268
|
+
const machines = integer(interval.available_nodes);
|
|
269
|
+
const gpus = integer(interval.available_gpus);
|
|
270
|
+
if (!start || !end || machines === null || gpus === null) {
|
|
271
|
+
interaction.error("The gateway returned an incomplete capacity forecast bucket.", {
|
|
272
|
+
code: "invalid_gateway_response",
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
return { start, end, machine_count: machines, gpu_count: gpus };
|
|
276
|
+
})
|
|
277
|
+
: [];
|
|
278
|
+
return { ...offerData(offer), availability };
|
|
279
|
+
});
|
|
280
|
+
const nextCursor = text(payload.next_cursor);
|
|
281
|
+
const next = nextCursor
|
|
282
|
+
? `ornn capacity-offers forecast --limit ${limit} --cursor ${nextCursor}`
|
|
283
|
+
: offers[0]
|
|
284
|
+
? `ornn capacity-offers show --offer ${offers[0].ref}`
|
|
285
|
+
: "ornn capacity-offers list";
|
|
286
|
+
return interaction.result({
|
|
287
|
+
data: {
|
|
288
|
+
generated_at: text(payload.generated_at),
|
|
289
|
+
bucket_hours: integer(payload.bucket_hours),
|
|
290
|
+
horizon_hours: integer(payload.horizon_hours),
|
|
291
|
+
capacity_offers: offers,
|
|
292
|
+
defaults: { limit },
|
|
293
|
+
},
|
|
294
|
+
outcome: { class: "ready", phase: offers.length ? "forecast" : "empty" },
|
|
295
|
+
page: { limit, returned: offers.length, next_cursor: nextCursor },
|
|
296
|
+
presentation: {
|
|
297
|
+
lines: offers.length
|
|
298
|
+
? offers.map((offer) => `${offer.ref} ${offer.hardware.gpu_type} ${offer.location.facility} ${offer.availability.length} forecast buckets`)
|
|
299
|
+
: ["No forecast capacity offers are available."],
|
|
300
|
+
next,
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
export const capacityOfferForecastVnext = {
|
|
305
|
+
arguments: "semantic",
|
|
306
|
+
audience: "customer",
|
|
307
|
+
category: "resource",
|
|
308
|
+
outcome: "Forecast owner-provided capacity availability in bounded pages.",
|
|
309
|
+
human: "ornn capacity-offers forecast [--limit <1-500>] [--cursor <opaque>]",
|
|
310
|
+
automation: "ornn capacity-offers forecast --limit 50 --format json",
|
|
311
|
+
acceptedReferences: ["opaque page cursor"],
|
|
312
|
+
related: ["capacity-offers list"],
|
|
313
|
+
order: 21,
|
|
314
|
+
};
|
|
315
|
+
export const capacityOffersListCapability = defineCapability({
|
|
316
|
+
id: "listings.capacity_list",
|
|
317
|
+
domain: "listings",
|
|
318
|
+
roles: ["user"],
|
|
319
|
+
description: "List primary and resale capacity offers through one customer-safe shape.",
|
|
320
|
+
mutation: "read",
|
|
321
|
+
input: capacityOffersListInput,
|
|
322
|
+
cli: {
|
|
323
|
+
command: ["capacity-offers", "list"],
|
|
324
|
+
vnext: {
|
|
325
|
+
arguments: "semantic",
|
|
326
|
+
audience: "customer",
|
|
327
|
+
category: "resource",
|
|
328
|
+
outcome: "List ordinary capacity offers with bounded opaque pagination and no supplier provenance.",
|
|
329
|
+
human: "ornn capacity-offers list [--limit <1-500>] [--cursor <opaque>]",
|
|
330
|
+
automation: "ornn capacity-offers list --limit 50 --format json",
|
|
331
|
+
acceptedReferences: ["opaque page cursor"],
|
|
332
|
+
related: ["capacity-offers show", "capacity-offers forecast"],
|
|
333
|
+
order: 20,
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
mcp: false,
|
|
337
|
+
slack: false,
|
|
338
|
+
execute: executeCapacityOffersList,
|
|
339
|
+
});
|
|
340
|
+
export const capacityOfferShowCapability = defineCapability({
|
|
341
|
+
id: "listings.capacity_show",
|
|
342
|
+
domain: "listings",
|
|
343
|
+
roles: ["user"],
|
|
344
|
+
description: "Show one exact primary or resale capacity offer without supplier provenance.",
|
|
345
|
+
mutation: "read",
|
|
346
|
+
input: capacityOfferShowInput,
|
|
347
|
+
cli: {
|
|
348
|
+
command: ["capacity-offers", "show"],
|
|
349
|
+
vnext: {
|
|
350
|
+
arguments: "semantic",
|
|
351
|
+
audience: "customer",
|
|
352
|
+
category: "resource",
|
|
353
|
+
outcome: "Inspect owner-provided hardware, location, quantity, term, and exact prices.",
|
|
354
|
+
human: "ornn capacity-offers show --offer <id>",
|
|
355
|
+
automation: "ornn capacity-offers show --offer <id> --format json",
|
|
356
|
+
acceptedReferences: ["canonical capacity-offer ID"],
|
|
357
|
+
related: ["capacity-offers list", "capacity-offers forecast"],
|
|
358
|
+
order: 22,
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
mcp: false,
|
|
362
|
+
slack: false,
|
|
363
|
+
execute: executeCapacityOfferShow,
|
|
364
|
+
});
|
|
365
|
+
export const capacityOfferCapabilities = [
|
|
366
|
+
capacityOffersListCapability,
|
|
367
|
+
capacityOfferShowCapability,
|
|
368
|
+
];
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineCapability } from "../capability.js";
|
|
3
|
+
export const CLI_MACHINE_SCHEMA = "ornn.cli/v1";
|
|
4
|
+
const emptyInput = z.object({});
|
|
5
|
+
function requireCliRuntime(context) {
|
|
6
|
+
if (!context.cli) {
|
|
7
|
+
throw new Error("This capability must execute through the Ornn CLI runtime.");
|
|
8
|
+
}
|
|
9
|
+
return context.cli;
|
|
10
|
+
}
|
|
11
|
+
const updateInput = z.object({
|
|
12
|
+
$positionals: z.array(z.string()).max(0).default([]),
|
|
13
|
+
major: z.boolean().optional(),
|
|
14
|
+
});
|
|
15
|
+
const completionInput = z
|
|
16
|
+
.object({
|
|
17
|
+
$positionals: z.tuple([z.enum(["bash", "fish", "zsh"])]),
|
|
18
|
+
})
|
|
19
|
+
.transform(({ $positionals }) => ({ shell: $positionals[0] }));
|
|
20
|
+
export const versionCapability = defineCapability({
|
|
21
|
+
id: "cli.version",
|
|
22
|
+
domain: "cli",
|
|
23
|
+
roles: ["user"],
|
|
24
|
+
description: "Show the CLI version and machine-output contract.",
|
|
25
|
+
mutation: "local",
|
|
26
|
+
input: emptyInput,
|
|
27
|
+
cli: {
|
|
28
|
+
command: ["version"],
|
|
29
|
+
vnext: {
|
|
30
|
+
audience: "customer",
|
|
31
|
+
category: "platform",
|
|
32
|
+
outcome: "Identify the installed CLI and its machine-output contract.",
|
|
33
|
+
human: "ornn version",
|
|
34
|
+
automation: "ornn version --format json",
|
|
35
|
+
related: ["status", "resources"],
|
|
36
|
+
order: 20,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
mcp: false,
|
|
40
|
+
slack: false,
|
|
41
|
+
execute: async (context) => {
|
|
42
|
+
const runtime = requireCliRuntime(context);
|
|
43
|
+
return {
|
|
44
|
+
version: runtime.version,
|
|
45
|
+
machine_schema: runtime.machineSchema,
|
|
46
|
+
update_channel: runtime.updateChannel,
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
export const updateCapability = defineCapability({
|
|
51
|
+
id: "cli.update",
|
|
52
|
+
domain: "cli",
|
|
53
|
+
roles: ["user"],
|
|
54
|
+
description: "Update the installed CLI through its detected package manager.",
|
|
55
|
+
mutation: "local",
|
|
56
|
+
input: updateInput,
|
|
57
|
+
cli: {
|
|
58
|
+
command: ["update"],
|
|
59
|
+
vnext: {
|
|
60
|
+
arguments: "semantic",
|
|
61
|
+
audience: "customer",
|
|
62
|
+
category: "platform",
|
|
63
|
+
outcome: "Update the installed CLI with explicit acknowledgement for a major crossing.",
|
|
64
|
+
human: "ornn update [--major]",
|
|
65
|
+
automation: "ornn update --format human [--major]",
|
|
66
|
+
related: ["version"],
|
|
67
|
+
order: 25,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
mcp: false,
|
|
71
|
+
slack: false,
|
|
72
|
+
execute: async (context, input) => {
|
|
73
|
+
const runtime = requireCliRuntime(context);
|
|
74
|
+
if (!runtime.update) {
|
|
75
|
+
throw new Error("This capability must execute through the Ornn CLI update runtime.");
|
|
76
|
+
}
|
|
77
|
+
return runtime.update(input);
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
export const completionCapability = defineCapability({
|
|
81
|
+
id: "cli.completion",
|
|
82
|
+
domain: "cli",
|
|
83
|
+
roles: ["user"],
|
|
84
|
+
description: "Generate shell completion from canonical catalog commands.",
|
|
85
|
+
mutation: "local",
|
|
86
|
+
input: completionInput,
|
|
87
|
+
cli: {
|
|
88
|
+
command: ["completion"],
|
|
89
|
+
vnext: {
|
|
90
|
+
arguments: "semantic",
|
|
91
|
+
audience: "customer",
|
|
92
|
+
category: "platform",
|
|
93
|
+
outcome: "Generate bash, fish, or zsh completion from the canonical command catalog.",
|
|
94
|
+
human: "ornn completion <bash|fish|zsh> --format human",
|
|
95
|
+
automation: "ornn completion <bash|fish|zsh> --format json",
|
|
96
|
+
related: ["resources"],
|
|
97
|
+
order: 27,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
mcp: false,
|
|
101
|
+
slack: false,
|
|
102
|
+
execute: async (context, input) => {
|
|
103
|
+
const runtime = requireCliRuntime(context);
|
|
104
|
+
if (!runtime.completion) {
|
|
105
|
+
throw new Error("This capability must execute through the Ornn CLI completion runtime.");
|
|
106
|
+
}
|
|
107
|
+
return runtime.completion(input);
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
export const resourcesCapability = defineCapability({
|
|
111
|
+
id: "cli.resources",
|
|
112
|
+
domain: "cli",
|
|
113
|
+
roles: ["user"],
|
|
114
|
+
description: "List registered advanced customer resource commands.",
|
|
115
|
+
mutation: "local",
|
|
116
|
+
input: emptyInput,
|
|
117
|
+
cli: {
|
|
118
|
+
command: ["resources"],
|
|
119
|
+
vnext: {
|
|
120
|
+
audience: "customer",
|
|
121
|
+
category: "platform",
|
|
122
|
+
outcome: "Discover advanced customer resource commands without dispatching a generic request.",
|
|
123
|
+
human: "ornn resources",
|
|
124
|
+
automation: "ornn resources --format json",
|
|
125
|
+
related: ["status", "version"],
|
|
126
|
+
order: 30,
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
mcp: false,
|
|
130
|
+
slack: false,
|
|
131
|
+
execute: async (context) => {
|
|
132
|
+
const runtime = requireCliRuntime(context);
|
|
133
|
+
const resources = [];
|
|
134
|
+
for (const capability of runtime.catalog) {
|
|
135
|
+
const binding = capability.cli;
|
|
136
|
+
const help = binding?.vnext;
|
|
137
|
+
if (!binding || help?.audience !== "customer" || help.category !== "resource") {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
resources.push({
|
|
141
|
+
command: binding.command.join(" "),
|
|
142
|
+
outcome: help.outcome,
|
|
143
|
+
human: help.human,
|
|
144
|
+
automation: help.automation,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
resources.sort((left, right) => left.command.localeCompare(right.command));
|
|
148
|
+
return { resources };
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
export const cliShellCapabilities = [
|
|
152
|
+
versionCapability,
|
|
153
|
+
updateCapability,
|
|
154
|
+
completionCapability,
|
|
155
|
+
resourcesCapability,
|
|
156
|
+
];
|