@ornncompute/cli 0.2.21 → 0.2.23

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 CHANGED
@@ -137,9 +137,6 @@ ornn networks show <network-id> [--json]
137
137
  ornn networks create --name <name> [--cidr <cidr>] [--description <text>] [--json]
138
138
  ornn networks update <network-id> [--name <name>] [--description <text>] [--clear-description] [--json]
139
139
  ornn networks delete <network-id> [--yes] [--json]
140
- ornn networks reservation <reservation-id> [--json]
141
- ornn networks attach <reservation-id> --network <network-id> [--json]
142
- ornn networks detach <reservation-id> [--json]
143
140
  ornn storage volumes list [--json]
144
141
  ornn storage volumes show <drive-id> [--json]
145
142
  ornn storage volumes create --name <name> [--source <drive-id>] [--json]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ornncompute/cli",
3
- "version": "0.2.21",
3
+ "version": "0.2.23",
4
4
  "description": "Command-line interface for Ornn compute access workflows.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.mjs CHANGED
@@ -214,9 +214,6 @@ Usage:
214
214
  ornn networks create --name <name> [--cidr <cidr>] [--description <text>] [--json]
215
215
  ornn networks update <network-id> [--name <name>] [--description <text>] [--clear-description] [--json]
216
216
  ornn networks delete <network-id> [--yes] [--json]
217
- ornn networks reservation <reservation-id> [--json]
218
- ornn networks attach <reservation-id> --network <network-id> [--json]
219
- ornn networks detach <reservation-id> [--json]
220
217
  ornn storage volumes list [--json]
221
218
  ornn storage files ls <drive-id> [--prefix <path>] [--json]
222
219
  ornn storage files upload <drive-id> <local-file> [--destination <path>] [--content-type <type>] [--json]
@@ -3002,7 +2999,7 @@ function nodeOpUsage(subcommand) {
3002
2999
  return `Usage: ornn node ${subcommand} <node-id> [--json]`;
3003
3000
  }
3004
3001
 
3005
- // Catalog: @ornn/capabilities fleet.nodes.list → GET /internal/nodes
3002
+ // Catalog: @ornn/capabilities fleet.nodes.list → GET /nodes
3006
3003
  async function nodeOpList(rest, context) {
3007
3004
  const options = parseCommandOptions(
3008
3005
  rest,
@@ -3133,11 +3130,15 @@ async function resolveNodeInstanceId(nodeId, context) {
3133
3130
  throw new CliApiError(`Node ${nodeId} has no assigned reservation to reboot.`);
3134
3131
  }
3135
3132
  const machines = await operatorRequest({
3136
- endpoint: `/v1/orchestrator/nodes/reservations/${encodeURIComponent(reservationId)}/machines`,
3133
+ endpoint: `/v1/orchestrator/internal/nodes?reservation_id=${encodeURIComponent(reservationId)}`,
3137
3134
  env: context.env,
3138
3135
  fetchImpl: context.fetchImpl,
3139
3136
  });
3140
- const rows = Array.isArray(machines?.machines) ? machines.machines : [];
3137
+ const rows = Array.isArray(machines?.nodes)
3138
+ ? machines.nodes
3139
+ : Array.isArray(machines?.machines)
3140
+ ? machines.machines
3141
+ : [];
3141
3142
  const nodeRefs = new Set(
3142
3143
  [nodeId, node?.id, node?.k8s_node_name]
3143
3144
  .filter((value) => value !== null && value !== undefined && String(value) !== "")
@@ -3151,7 +3152,7 @@ async function resolveNodeInstanceId(nodeId, context) {
3151
3152
  `Multiple live machines found for node ${nodeId} (reservation ${reservationId}).`
3152
3153
  );
3153
3154
  }
3154
- const instanceId = matches[0]?.id;
3155
+ const instanceId = matches[0]?.node_id ?? matches[0]?.id;
3155
3156
  if (!instanceId) {
3156
3157
  throw new CliApiError(
3157
3158
  `No live machine found for node ${nodeId} (reservation ${reservationId}).`
@@ -3370,7 +3371,7 @@ function writeNodeHealth(stdout, node) {
3370
3371
 
3371
3372
  stdout.write(`Validation: ${node?.validation_status || "unknown"}\n`);
3372
3373
  writeOptionalStatusLine(stdout, "Validation checked", node?.validation_checked_at);
3373
- writeOptionalStatusLine(stdout, "Last heartbeat", node?.last_heartbeat_at);
3374
+ writeOptionalStatusLine(stdout, "Last heartbeat", node?.observed_at);
3374
3375
  }
3375
3376
 
3376
3377
  async function nodes(args, context) {
@@ -3706,10 +3707,11 @@ async function clusters(args, context) {
3706
3707
  "Usage: ornn clusters list [--json]"
3707
3708
  );
3708
3709
  const rows = await fetchTenantClusters(context);
3710
+ const clusters = withNumberedClusterNames(requireArrayPayload(rows, "clusters"));
3709
3711
  if (options.json) {
3710
- writeJson(context.stdout, rows);
3712
+ writeJson(context.stdout, clusters);
3711
3713
  } else {
3712
- writeClusterList(context.stdout, requireArrayPayload(rows, "clusters"));
3714
+ writeClusterList(context.stdout, clusters);
3713
3715
  }
3714
3716
  return 0;
3715
3717
  }
@@ -3802,9 +3804,16 @@ async function clusters(args, context) {
3802
3804
  const wait = options.wait
3803
3805
  ? await waitForClusterActive(selectedReservationId, type, waitOptions(options), context)
3804
3806
  : null;
3805
- const result = { cluster: launch, reservation_id: selectedReservationId, type, wait };
3807
+ const result = {
3808
+ cluster: withNumberedClusterName(launch, [launch]),
3809
+ reservation_id: selectedReservationId,
3810
+ type,
3811
+ wait: wait
3812
+ ? { ...wait, cluster: withNumberedClusterName(wait.cluster, [wait.cluster]) }
3813
+ : null,
3814
+ };
3806
3815
  if (options.json) {
3807
- writeJson(context.stdout, result);
3816
+ writeJson(context.stdout, omitClusterName(result));
3808
3817
  } else {
3809
3818
  writeClusterLaunchResult(context.stdout, result);
3810
3819
  writeNextStep(
@@ -3825,8 +3834,20 @@ async function clusters(args, context) {
3825
3834
  { boolean: ["json"], value: ["type", "mode"] },
3826
3835
  "Usage: ornn clusters show <reservation-id> [--type kubernetes|slurm] [--json]"
3827
3836
  );
3828
- const type = await resolveClusterTypeForReservation(reservationId, options, context);
3829
- const cluster = await fetchCluster(reservationId, type, context);
3837
+ const listed = requireArrayPayload(await fetchTenantClusters(context), "clusters");
3838
+ let type;
3839
+ if (options.type || options.mode) {
3840
+ type = normalizeClusterType(options.type || options.mode);
3841
+ } else {
3842
+ const listedType = listed.find(
3843
+ (candidate) => String(candidate.reservation_id || "") === String(reservationId)
3844
+ )?.access_mode;
3845
+ if (!listedType) {
3846
+ throw new Error("--type is required when the cluster is not listed yet.");
3847
+ }
3848
+ type = normalizeClusterType(listedType);
3849
+ }
3850
+ const cluster = withNumberedClusterName(await fetchCluster(reservationId, type, context), listed);
3830
3851
  if (options.json) {
3831
3852
  writeJson(context.stdout, cluster);
3832
3853
  } else {
@@ -3843,10 +3864,14 @@ async function clusters(args, context) {
3843
3864
  );
3844
3865
  const type = await resolveClusterTypeForReservation(reservationId, options, context);
3845
3866
  const result = await waitForClusterActive(reservationId, type, waitOptions(options), context);
3867
+ const named = {
3868
+ ...result,
3869
+ cluster: withNumberedClusterName(result.cluster, [result.cluster]),
3870
+ };
3846
3871
  if (options.json) {
3847
- writeJson(context.stdout, result);
3872
+ writeJson(context.stdout, omitClusterName(named));
3848
3873
  } else {
3849
- writeClusterWaitResult(context.stdout, result);
3874
+ writeClusterWaitResult(context.stdout, named);
3850
3875
  }
3851
3876
  return 0;
3852
3877
  }
@@ -3952,13 +3977,14 @@ async function clusters(args, context) {
3952
3977
  subcommand === "add-node"
3953
3978
  ? await addClusterNode(reservationId, nodeId, context)
3954
3979
  : await removeClusterNode(reservationId, nodeId, context);
3980
+ const named = withNumberedClusterName(cluster, [cluster]);
3955
3981
  if (options.json) {
3956
- writeJson(context.stdout, cluster);
3982
+ writeJson(context.stdout, named);
3957
3983
  } else {
3958
3984
  context.stdout.write(
3959
3985
  `Cluster node ${subcommand === "add-node" ? "add" : "remove"} queued.\n`
3960
3986
  );
3961
- writeClusterDetail(context.stdout, cluster);
3987
+ writeClusterDetail(context.stdout, named);
3962
3988
  }
3963
3989
  return 0;
3964
3990
  }
@@ -3971,7 +3997,7 @@ async function clusters(args, context) {
3971
3997
  );
3972
3998
  if (!(await confirmClusterTeardown(options, context, reservationId))) return 1;
3973
3999
  const type = await resolveClusterTypeForReservation(reservationId, options, context);
3974
- const cluster = await teardownCluster(reservationId, type, context);
4000
+ const cluster = withNumberedClusterName(await teardownCluster(reservationId, type, context), []);
3975
4001
  if (options.json) {
3976
4002
  writeJson(context.stdout, cluster);
3977
4003
  } else {
@@ -4002,9 +4028,16 @@ async function slurm(args, context) {
4002
4028
  const wait = options.wait
4003
4029
  ? await waitForClusterActive(reservationId, "slurm", waitOptions(options), context)
4004
4030
  : null;
4005
- const result = { cluster: launch, reservation_id: reservationId, type: "slurm", wait };
4031
+ const result = {
4032
+ cluster: withNumberedClusterName(launch, [launch]),
4033
+ reservation_id: reservationId,
4034
+ type: "slurm",
4035
+ wait: wait
4036
+ ? { ...wait, cluster: withNumberedClusterName(wait.cluster, [wait.cluster]) }
4037
+ : null,
4038
+ };
4006
4039
  if (options.json) {
4007
- writeJson(context.stdout, result);
4040
+ writeJson(context.stdout, omitClusterName(result));
4008
4041
  } else {
4009
4042
  writeClusterLaunchResult(context.stdout, result);
4010
4043
  }
@@ -4018,7 +4051,10 @@ async function slurm(args, context) {
4018
4051
  "Usage: ornn slurm teardown <reservation-id> [--yes] [--json]"
4019
4052
  );
4020
4053
  if (!(await confirmClusterTeardown(options, context, reservationId))) return 1;
4021
- const cluster = await teardownCluster(reservationId, "slurm", context);
4054
+ const cluster = withNumberedClusterName(
4055
+ await teardownCluster(reservationId, "slurm", context),
4056
+ []
4057
+ );
4022
4058
  if (options.json) {
4023
4059
  writeJson(context.stdout, cluster);
4024
4060
  } else {
@@ -4034,7 +4070,7 @@ async function slurm(args, context) {
4034
4070
  { boolean: ["json"] },
4035
4071
  "Usage: ornn slurm status <reservation-id> [--json]"
4036
4072
  );
4037
- const cluster = await fetchCluster(reservationId, "slurm", context);
4073
+ const cluster = withNumberedClusterName(await fetchCluster(reservationId, "slurm", context), []);
4038
4074
  if (options.json) {
4039
4075
  writeJson(context.stdout, cluster);
4040
4076
  } else {
@@ -4100,9 +4136,16 @@ async function kubernetes(args, context) {
4100
4136
  const wait = options.wait
4101
4137
  ? await waitForClusterActive(reservationId, "kubernetes", waitOptions(options), context)
4102
4138
  : null;
4103
- const result = { cluster: launch, reservation_id: reservationId, type: "kubernetes", wait };
4139
+ const result = {
4140
+ cluster: withNumberedClusterName(launch, [launch]),
4141
+ reservation_id: reservationId,
4142
+ type: "kubernetes",
4143
+ wait: wait
4144
+ ? { ...wait, cluster: withNumberedClusterName(wait.cluster, [wait.cluster]) }
4145
+ : null,
4146
+ };
4104
4147
  if (options.json) {
4105
- writeJson(context.stdout, result);
4148
+ writeJson(context.stdout, omitClusterName(result));
4106
4149
  } else {
4107
4150
  writeClusterLaunchResult(context.stdout, result);
4108
4151
  }
@@ -4116,7 +4159,10 @@ async function kubernetes(args, context) {
4116
4159
  "Usage: ornn kubernetes teardown <reservation-id> [--yes] [--json]"
4117
4160
  );
4118
4161
  if (!(await confirmClusterTeardown(options, context, reservationId))) return 1;
4119
- const cluster = await teardownCluster(reservationId, "kubernetes", context);
4162
+ const cluster = withNumberedClusterName(
4163
+ await teardownCluster(reservationId, "kubernetes", context),
4164
+ []
4165
+ );
4120
4166
  if (options.json) {
4121
4167
  writeJson(context.stdout, cluster);
4122
4168
  } else {
@@ -4132,7 +4178,10 @@ async function kubernetes(args, context) {
4132
4178
  { boolean: ["json"] },
4133
4179
  "Usage: ornn kubernetes status <reservation-id> [--json]"
4134
4180
  );
4135
- const cluster = await fetchCluster(reservationId, "kubernetes", context);
4181
+ const cluster = withNumberedClusterName(
4182
+ await fetchCluster(reservationId, "kubernetes", context),
4183
+ []
4184
+ );
4136
4185
  if (options.json) {
4137
4186
  writeJson(context.stdout, cluster);
4138
4187
  } else {
@@ -4326,71 +4375,11 @@ async function networks(args, context) {
4326
4375
  return 0;
4327
4376
  }
4328
4377
 
4329
- if (subcommand === "reservation" && id) {
4330
- const options = parseCommandOptions(
4331
- rest,
4332
- { boolean: ["json"] },
4333
- "Usage: ornn networks reservation <reservation-id> [--json]"
4334
- );
4335
- const payload = await cliRequest({
4336
- endpoint: `/v1/workflows/compute/nodes/reservations/${encodeURIComponent(id)}/network`,
4337
- env: context.env,
4338
- fetchImpl: context.fetchImpl,
4339
- });
4340
- if (options.json) {
4341
- writeJson(context.stdout, payload);
4342
- } else {
4343
- writeReservationNetwork(context.stdout, payload);
4344
- }
4345
- return 0;
4346
- }
4347
-
4348
- if (subcommand === "attach" && id) {
4349
- const options = parseCommandOptions(
4350
- rest,
4351
- { boolean: ["json"], value: ["network", "network-id"] },
4352
- "Usage: ornn networks attach <reservation-id> --network <network-id> [--json]"
4353
- );
4354
- const networkId = requiredOption(options.network || options.networkId, "--network");
4355
- const payload = await cliRequest({
4356
- body: { network_id: networkId },
4357
- endpoint: `/v1/workflows/compute/nodes/reservations/${encodeURIComponent(id)}/network`,
4358
- env: context.env,
4359
- fetchImpl: context.fetchImpl,
4360
- method: "POST",
4361
- });
4362
- if (options.json) {
4363
- writeJson(context.stdout, payload);
4364
- } else {
4365
- writeColored(context.stdout, "green", "Reservation network attached.");
4366
- writeReservationNetwork(context.stdout, payload);
4367
- }
4368
- return 0;
4369
- }
4370
-
4371
- if (subcommand === "detach" && id) {
4372
- const options = parseCommandOptions(
4373
- rest,
4374
- { boolean: ["json"] },
4375
- "Usage: ornn networks detach <reservation-id> [--json]"
4376
- );
4377
- const payload = await cliRequest({
4378
- body: { network_id: null },
4379
- endpoint: `/v1/workflows/compute/nodes/reservations/${encodeURIComponent(id)}/network`,
4380
- env: context.env,
4381
- fetchImpl: context.fetchImpl,
4382
- method: "POST",
4383
- });
4384
- if (options.json) {
4385
- writeJson(context.stdout, payload);
4386
- } else {
4387
- writeColored(context.stdout, "green", "Reservation network detached.");
4388
- writeReservationNetwork(context.stdout, payload);
4389
- }
4390
- return 0;
4378
+ if (subcommand === "reservation" || subcommand === "attach" || subcommand === "detach") {
4379
+ throw new Error("network_attach_removed");
4391
4380
  }
4392
4381
 
4393
- throw new Error("Usage: ornn networks list|show|create|update|delete|reservation|attach|detach");
4382
+ throw new Error("Usage: ornn networks list|show|create|update|delete");
4394
4383
  }
4395
4384
 
4396
4385
  function normalizeStorageDestination(value) {
@@ -6061,6 +6050,8 @@ function inventoryAvailableGpuCount(record) {
6061
6050
  function normalizeInventoryListing(record) {
6062
6051
  const gpuCount = inventoryAvailableGpuCount(record);
6063
6052
  const facility = record.site_nickname ?? record.facility ?? record.site ?? "Unknown facility";
6053
+ // Customer availability never exposes real supplier/operator identity.
6054
+ // Display branding stays in the text formatter, not on the JSON envelope.
6064
6055
  return {
6065
6056
  id: record.id,
6066
6057
  kind: "inventory",
@@ -6855,13 +6846,21 @@ async function resolveStorageMountUsers(attachment, context) {
6855
6846
  return users.size ? users : null;
6856
6847
  }
6857
6848
 
6849
+ function reservationHostsFromPayload(payload) {
6850
+ if (Array.isArray(payload)) return payload;
6851
+ if (Array.isArray(payload?.nodes)) return payload.nodes;
6852
+ if (Array.isArray(payload?.machines)) return payload.machines;
6853
+ return [];
6854
+ }
6855
+
6858
6856
  async function getReservationMachines(reservationId, context) {
6859
6857
  const payload = await cliRequest({
6860
- endpoint: `/v1/workflows/compute/nodes/reservations/${reservationId}/machines`,
6858
+ endpoint: `/v1/workflows/compute/nodes?reservation_id=${encodeURIComponent(reservationId)}`,
6861
6859
  env: context.env,
6862
6860
  fetchImpl: context.fetchImpl,
6863
6861
  });
6864
- return Array.isArray(payload) ? { machines: payload } : payload;
6862
+ const nodes = reservationHostsFromPayload(payload);
6863
+ return { ...(payload && typeof payload === "object" ? payload : {}), machines: nodes };
6865
6864
  }
6866
6865
 
6867
6866
  async function fetchBillingInvoices(context) {
@@ -6967,13 +6966,6 @@ async function launchReservationAccess(
6967
6966
  "--storage-save-drive-id"
6968
6967
  );
6969
6968
  }
6970
- const access = await cliRequest({
6971
- body: { access_mode: mode, image_id: null },
6972
- endpoint: `/v1/workflows/commerce/reservations/${reservationId}/access-mode`,
6973
- env: context.env,
6974
- fetchImpl: context.fetchImpl,
6975
- method: "POST",
6976
- });
6977
6969
  const launch = await cliRequest({
6978
6970
  body: launchPayload,
6979
6971
  endpoint: `/v1/workflows/compute/nodes/reservations/${reservationId}/launch`,
@@ -6992,7 +6984,7 @@ async function launchReservationAccess(
6992
6984
  ? await waitForSshReady(reservationId, waitOptions(options), context)
6993
6985
  : null;
6994
6986
  return {
6995
- access,
6987
+ access: launch,
6996
6988
  launch,
6997
6989
  mode,
6998
6990
  opened,
@@ -7027,7 +7019,7 @@ async function switchReservationAccess(reservationId, options, context) {
7027
7019
  fetchImpl: context.fetchImpl,
7028
7020
  method: "POST",
7029
7021
  });
7030
- const machines = Array.isArray(switched?.machines) ? switched.machines : [];
7022
+ const machines = reservationHostsFromPayload(switched);
7031
7023
  const wait = options.wait
7032
7024
  ? await waitForSshReady(reservationId, waitOptions(options), context)
7033
7025
  : null;
@@ -7045,7 +7037,7 @@ async function switchReservationAccess(reservationId, options, context) {
7045
7037
 
7046
7038
  function accessActivateJsonResult(result) {
7047
7039
  return {
7048
- access: result.access,
7040
+ access: { access_mode: result.mode },
7049
7041
  launch: result.launch,
7050
7042
  opened: Boolean(result.opened?.opened),
7051
7043
  url: result.opened?.url ?? null,
@@ -7062,11 +7054,11 @@ async function fetchTenantClusters(context) {
7062
7054
 
7063
7055
  async function fetchEligibleClusterNodes(reservationId, type, networkMode, context) {
7064
7056
  const payload = await cliRequest({
7065
- endpoint: `/v1/workflows/compute/nodes/reservations/${encodeURIComponent(reservationId)}/machines`,
7057
+ endpoint: `/v1/workflows/compute/nodes?reservation_id=${encodeURIComponent(reservationId)}`,
7066
7058
  env: context.env,
7067
7059
  fetchImpl: context.fetchImpl,
7068
7060
  });
7069
- const machines = Array.isArray(payload?.machines) ? payload.machines : [];
7061
+ const machines = reservationHostsFromPayload(payload);
7070
7062
  return {
7071
7063
  reservation_id: reservationId,
7072
7064
  access_mode: type,
@@ -7803,7 +7795,7 @@ function writeAccessDetail(stdout, payload = {}) {
7803
7795
  }
7804
7796
 
7805
7797
  function writeAccessLaunchSummary(stdout, launch = {}) {
7806
- const machines = Array.isArray(launch.machines) ? launch.machines : [];
7798
+ const machines = reservationHostsFromPayload(launch);
7807
7799
  if (!machines.length) {
7808
7800
  stdout.write("Provisioning response received.\n");
7809
7801
  return;
@@ -8011,18 +8003,6 @@ function writeNetworkDetail(stdout, network = {}) {
8011
8003
  writeOptionalStatusLine(stdout, "Updated", network.updated_at);
8012
8004
  }
8013
8005
 
8014
- function writeReservationNetwork(stdout, payload = {}) {
8015
- stdout.write(`${payload.reservation_id || "reservation"}\n`);
8016
- if (!payload.network_id || !payload.network) {
8017
- stdout.write("Network: shared\n");
8018
- return;
8019
- }
8020
- stdout.write(`Network: ${payload.network.name || payload.network_id}\n`);
8021
- stdout.write(`Network ID: ${payload.network_id}\n`);
8022
- writeOptionalStatusLine(stdout, "CIDR", payload.network.cidr);
8023
- writeOptionalStatusLine(stdout, "Status", payload.network.status);
8024
- }
8025
-
8026
8006
  function writeReservationStorageAttachment(stdout, attachment = {}, options = {}) {
8027
8007
  if (!attachment) {
8028
8008
  stdout.write("No volume is attached to this reservation.\n");
@@ -8950,10 +8930,10 @@ function metricSnapshotFromLivePayload(fallbackMachine, payload = {}) {
8950
8930
  function metricSnapshotFromMachine(machine, extras = {}) {
8951
8931
  const resourceMetrics = machine.resource_metrics || machine.resourceMetrics || {};
8952
8932
  const lastHeartbeatAt =
8933
+ machine.observed_at ||
8934
+ resourceMetrics.observed_at ||
8953
8935
  resourceMetrics.last_heartbeat_at ||
8954
8936
  resourceMetrics.lastHeartbeatAt ||
8955
- machine.last_heartbeat_at ||
8956
- machine.lastHeartbeatAt ||
8957
8937
  null;
8958
8938
  return {
8959
8939
  connection_status:
@@ -9033,6 +9013,51 @@ function writeWaitResult(stdout, result) {
9033
9013
  stdout.write(`SSH: ${result.command}\n`);
9034
9014
  }
9035
9015
 
9016
+ function clusterIdentity(cluster) {
9017
+ return String(cluster?.controller_id || cluster?.id || cluster?.reservation_id || "").trim();
9018
+ }
9019
+
9020
+ function numberedClusterName(clusters, target) {
9021
+ const ordered = [...clusters].sort((left, right) => {
9022
+ const leftCreatedAt = Date.parse(left.created_at ?? "");
9023
+ const rightCreatedAt = Date.parse(right.created_at ?? "");
9024
+ const leftTime = Number.isFinite(leftCreatedAt) ? leftCreatedAt : Number.MAX_SAFE_INTEGER;
9025
+ const rightTime = Number.isFinite(rightCreatedAt) ? rightCreatedAt : Number.MAX_SAFE_INTEGER;
9026
+ return leftTime - rightTime || clusterIdentity(left).localeCompare(clusterIdentity(right));
9027
+ });
9028
+ const index = ordered.findIndex((cluster) => clusterIdentity(cluster) === clusterIdentity(target));
9029
+ return `Cluster ${index >= 0 ? index + 1 : 1}`;
9030
+ }
9031
+
9032
+ function omitClusterName(value) {
9033
+ if (Array.isArray(value)) {
9034
+ return value.map(omitClusterName);
9035
+ }
9036
+ if (!value || typeof value !== "object") {
9037
+ return value;
9038
+ }
9039
+ const { cluster_name: _ignored, ...rest } = value;
9040
+ if ("cluster" in rest) {
9041
+ rest.cluster = omitClusterName(rest.cluster);
9042
+ }
9043
+ if (rest.wait && typeof rest.wait === "object") {
9044
+ rest.wait = omitClusterName(rest.wait);
9045
+ }
9046
+ return rest;
9047
+ }
9048
+
9049
+ function withNumberedClusterName(cluster, siblings) {
9050
+ const rows = siblings.length ? siblings : [cluster];
9051
+ return {
9052
+ ...omitClusterName(cluster),
9053
+ name: numberedClusterName(rows, cluster),
9054
+ };
9055
+ }
9056
+
9057
+ function withNumberedClusterNames(clusters) {
9058
+ return clusters.map((cluster) => withNumberedClusterName(cluster, clusters));
9059
+ }
9060
+
9036
9061
  function writeClusterList(stdout, clusters) {
9037
9062
  if (!clusters.length) {
9038
9063
  stdout.write("No clusters found.\n");
@@ -9067,7 +9092,7 @@ function writeClusterList(stdout, clusters) {
9067
9092
  stdout.write("Clusters:\n");
9068
9093
  for (const cluster of clusters) {
9069
9094
  stdout.write(
9070
- `- ${cluster.reservation_id || cluster.id || "cluster"} ${displayClusterType(cluster.access_mode)} ${cluster.state || "unknown"}`
9095
+ `- ${cluster.name || numberedClusterName(clusters, cluster)} ${cluster.reservation_id || cluster.id || "cluster"} ${displayClusterType(cluster.access_mode)} ${cluster.state || "unknown"}`
9071
9096
  );
9072
9097
  stdout.write(` ${cluster.node_count ?? "?"} nodes/${cluster.gpu_count ?? "?"} GPUs`);
9073
9098
  stdout.write(` network=${cluster.network_mode || "public"}`);
@@ -9157,13 +9182,13 @@ function writeEligibleClusterNodes(stdout, payload) {
9157
9182
 
9158
9183
  function writeClusterDetail(stdout, cluster = {}) {
9159
9184
  stdout.write(`${cluster.reservation_id || cluster.id || "cluster"}\n`);
9185
+ stdout.write(`Name: ${cluster.name || numberedClusterName([cluster], cluster)}\n`);
9160
9186
  stdout.write(`Type: ${displayClusterType(cluster.access_mode)}\n`);
9161
9187
  stdout.write(`State: ${cluster.state || "unknown"}\n`);
9162
9188
  stdout.write(`Network: ${cluster.network_mode || "public"}\n`);
9163
9189
  stdout.write(`Nodes: ${cluster.node_count ?? "unknown"}\n`);
9164
9190
  stdout.write(`GPUs: ${cluster.gpu_count ?? "unknown"}\n`);
9165
9191
  writeOptionalStatusLine(stdout, "GPU type", cluster.gpu_type);
9166
- writeOptionalStatusLine(stdout, "Name", cluster.cluster_name);
9167
9192
  writeOptionalStatusLine(stdout, "Endpoint", cluster.endpoint);
9168
9193
  writeOptionalStatusLine(stdout, "Namespace", cluster.namespace);
9169
9194
  writeOptionalStatusLine(stdout, "Credentials", cluster.credential_state);
@@ -9635,7 +9660,12 @@ function looksLikePublicKey(value) {
9635
9660
  function looksLikePath(value) {
9636
9661
  const text = String(value || "").trim();
9637
9662
  return (
9638
- text.startsWith("~") || text.startsWith(".") || text.includes("/") || text.endsWith(".pub")
9663
+ text.startsWith("~") ||
9664
+ text.startsWith(".") ||
9665
+ text.includes("/") ||
9666
+ text.includes("\\") ||
9667
+ /^[A-Za-z]:/.test(text) ||
9668
+ text.endsWith(".pub")
9639
9669
  );
9640
9670
  }
9641
9671
 
@@ -79,7 +79,7 @@ export const listNodesCapability = defineCapability({
79
79
  roles: ["user", "reviewer", "admin"],
80
80
  description: "List nodes. Users see their own reserved machines. Reviewers and admins see enrolled, ghost, and off-grid nodes. Call with no filters for the whole fleet. Optional email lists nodes for that user's organizations. Optional operator_id, facility_id, and status (ghost, off-grid, bare-metal) narrow the same list. Never invent nodes. Never print private keys.",
81
81
  mutation: "read",
82
- http: { method: "GET", path: "/internal/nodes" },
82
+ http: { method: "GET", path: "/nodes" },
83
83
  slack: {
84
84
  parameters: {
85
85
  type: "object",
@@ -107,7 +107,7 @@ export const listNodesCapability = defineCapability({
107
107
  if (ctx.role === "user") {
108
108
  return ctx.fetch("/nodes", { method: "GET" });
109
109
  }
110
- return ctx.fetch(queryPath("/internal/nodes", {
110
+ return ctx.fetch(queryPath("/nodes", {
111
111
  operator_id: parsed.operator_id ?? parsed.operatorId ?? parsed.operator,
112
112
  facility_id: parsed.facility_id ?? parsed.facilityId,
113
113
  status: parsed.status,
@@ -18,36 +18,42 @@ export declare const gtmDealListInput: z.ZodObject<{
18
18
  }, z.core.$strip>;
19
19
  export declare const gtmDealListCapability: import("../capability.ts").Capability<unknown, unknown>;
20
20
  export declare const gtmDealWriteInput: z.ZodObject<{
21
- listingId: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodLiteral<"">]>>;
22
- currentBid: z.ZodOptional<z.ZodString>;
23
- confirm: z.ZodOptional<z.ZodBoolean>;
24
- customer: z.ZodString;
25
- owner: z.ZodString;
26
- supplier: z.ZodString;
27
- gpuType: z.ZodString;
28
21
  dealId: z.ZodOptional<z.ZodNumber>;
22
+ listingId: z.ZodOptional<z.ZodString>;
23
+ customer: z.ZodOptional<z.ZodString>;
24
+ owner: z.ZodOptional<z.ZodString>;
25
+ confirm: z.ZodOptional<z.ZodBoolean>;
29
26
  }, z.core.$strip>;
30
27
  export declare const gtmDealWriteCapability: import("../capability.ts").Capability<unknown, unknown>;
31
28
  export declare const gtmCommentListInput: z.ZodObject<{
32
29
  dealId: z.ZodOptional<z.ZodNumber>;
33
30
  companyId: z.ZodOptional<z.ZodNumber>;
31
+ taskId: z.ZodOptional<z.ZodNumber>;
32
+ field: z.ZodOptional<z.ZodEnum<{
33
+ name: "name";
34
+ notes: "notes";
35
+ owner_tags: "owner_tags";
36
+ tickets: "tickets";
37
+ }>>;
34
38
  }, z.core.$strip>;
35
39
  export declare const gtmCommentListCapability: import("../capability.ts").Capability<unknown, unknown>;
36
40
  export declare const gtmCommentWriteInput: z.ZodObject<{
37
- dealId: z.ZodOptional<z.ZodNumber>;
38
- companyId: z.ZodOptional<z.ZodNumber>;
39
41
  commentId: z.ZodOptional<z.ZodNumber>;
40
42
  parentId: z.ZodOptional<z.ZodNumber>;
41
43
  body: z.ZodString;
42
44
  bid: z.ZodDefault<z.ZodBoolean>;
43
45
  confirm: z.ZodOptional<z.ZodBoolean>;
46
+ dealId: z.ZodOptional<z.ZodNumber>;
47
+ companyId: z.ZodOptional<z.ZodNumber>;
48
+ taskId: z.ZodOptional<z.ZodNumber>;
49
+ field: z.ZodOptional<z.ZodEnum<{
50
+ name: "name";
51
+ notes: "notes";
52
+ owner_tags: "owner_tags";
53
+ tickets: "tickets";
54
+ }>>;
44
55
  }, z.core.$strip>;
45
56
  export declare const gtmCommentWriteCapability: import("../capability.ts").Capability<unknown, unknown>;
46
- export declare const gtmMentionDismissInput: z.ZodObject<{
47
- commentId: z.ZodNumber;
48
- confirm: z.ZodOptional<z.ZodBoolean>;
49
- }, z.core.$strip>;
50
- export declare const gtmMentionDismissCapability: import("../capability.ts").Capability<unknown, unknown>;
51
57
  export declare const gtmWorkspaceShowInput: z.ZodObject<{}, z.core.$strip>;
52
58
  export declare const gtmWorkspaceShowCapability: import("../capability.ts").Capability<unknown, unknown>;
53
59
  export declare const gtmTaskListInput: z.ZodObject<{
@@ -74,11 +80,6 @@ export declare const gtmCompanyDeleteInput: z.ZodObject<{
74
80
  confirm: z.ZodOptional<z.ZodBoolean>;
75
81
  }, z.core.$strip>;
76
82
  export declare const gtmCompanyDeleteCapability: import("../capability.ts").Capability<unknown, unknown>;
77
- export declare const gtmDealDeleteInput: z.ZodObject<{
78
- [x: string]: z.ZodNumber | z.ZodOptional<z.ZodBoolean>;
79
- confirm: z.ZodOptional<z.ZodBoolean>;
80
- }, z.core.$strip>;
81
- export declare const gtmDealDeleteCapability: import("../capability.ts").Capability<unknown, unknown>;
82
83
  export declare const gtmCommentDeleteInput: z.ZodObject<{
83
84
  [x: string]: z.ZodNumber | z.ZodOptional<z.ZodBoolean>;
84
85
  confirm: z.ZodOptional<z.ZodBoolean>;
@@ -97,7 +98,13 @@ export declare const gtmTaskDeleteCapability: import("../capability.ts").Capabil
97
98
  */
98
99
  export declare const gtmListingListInput: z.ZodObject<{
99
100
  gpuType: z.ZodOptional<z.ZodString>;
101
+ state: z.ZodOptional<z.ZodEnum<{
102
+ delisted: "delisted";
103
+ available: "available";
104
+ internal: "internal";
105
+ }>>;
100
106
  limit: z.ZodOptional<z.ZodNumber>;
107
+ cursor: z.ZodOptional<z.ZodString>;
101
108
  }, z.core.$strip>;
102
109
  export declare const gtmListingListCapability: import("../capability.ts").Capability<unknown, unknown>;
103
110
  export declare const gtmListingUpdateInput: z.ZodObject<{
@@ -110,8 +117,8 @@ export declare const gtmListingUpdateInput: z.ZodObject<{
110
117
  capacityNodes: z.ZodOptional<z.ZodNumber>;
111
118
  state: z.ZodOptional<z.ZodEnum<{
112
119
  delisted: "delisted";
113
- draft: "draft";
114
- listed: "listed";
120
+ available: "available";
121
+ internal: "internal";
115
122
  }>>;
116
123
  confirm: z.ZodOptional<z.ZodBoolean>;
117
124
  }, z.core.$strip>;
@@ -120,42 +127,6 @@ export declare const gtmTaskActivityListInput: z.ZodObject<{
120
127
  taskId: z.ZodNumber;
121
128
  }, z.core.$strip>;
122
129
  export declare const gtmTaskActivityListCapability: import("../capability.ts").Capability<unknown, unknown>;
123
- export declare const gtmTaskActivityWriteInput: z.ZodObject<{
124
- taskId: z.ZodNumber;
125
- body: z.ZodString;
126
- parentId: z.ZodOptional<z.ZodNumber>;
127
- confirm: z.ZodOptional<z.ZodBoolean>;
128
- }, z.core.$strip>;
129
- export declare const gtmTaskActivityWriteCapability: import("../capability.ts").Capability<unknown, unknown>;
130
- export declare const gtmCellCommentListInput: z.ZodObject<{
131
- companyId: z.ZodNumber;
132
- field: z.ZodEnum<{
133
- name: "name";
134
- notes: "notes";
135
- owner_tags: "owner_tags";
136
- tickets: "tickets";
137
- }>;
138
- }, z.core.$strip>;
139
- export declare const gtmCellCommentListCapability: import("../capability.ts").Capability<unknown, unknown>;
140
- export declare const gtmCellCommentWriteInput: z.ZodObject<{
141
- companyId: z.ZodOptional<z.ZodNumber>;
142
- field: z.ZodOptional<z.ZodEnum<{
143
- name: "name";
144
- notes: "notes";
145
- owner_tags: "owner_tags";
146
- tickets: "tickets";
147
- }>>;
148
- commentId: z.ZodOptional<z.ZodNumber>;
149
- parentId: z.ZodOptional<z.ZodNumber>;
150
- body: z.ZodString;
151
- confirm: z.ZodOptional<z.ZodBoolean>;
152
- }, z.core.$strip>;
153
- export declare const gtmCellCommentWriteCapability: import("../capability.ts").Capability<unknown, unknown>;
154
- export declare const gtmCellCommentDeleteInput: z.ZodObject<{
155
- [x: string]: z.ZodNumber | z.ZodOptional<z.ZodBoolean>;
156
- confirm: z.ZodOptional<z.ZodBoolean>;
157
- }, z.core.$strip>;
158
- export declare const gtmCellCommentDeleteCapability: import("../capability.ts").Capability<unknown, unknown>;
159
130
  /**
160
131
  * Bulk owner-tag reassignment. This is the one GTM edit that is genuinely
161
132
  * N-at-a-time — a book of companies moving to a new owner — and doing it one
@@ -59,12 +59,6 @@ export const gtmCompanyWriteCapability = defineStaffMcpCapability({
59
59
  : "/internal/v1/gtm/companies", { method: parsed.companyId ? "PATCH" : "POST", body });
60
60
  },
61
61
  });
62
- const dealFields = {
63
- customer: z.string().min(1),
64
- owner: z.string().min(1),
65
- supplier: z.string().min(1),
66
- gpuType: z.string().min(1),
67
- };
68
62
  export const gtmDealListInput = z.object({ ...listFilterFields });
69
63
  export const gtmDealListCapability = defineStaffMcpCapability({
70
64
  id: "gtm.deal.list",
@@ -80,34 +74,40 @@ export const gtmDealListCapability = defineStaffMcpCapability({
80
74
  });
81
75
  export const gtmDealWriteInput = z.object({
82
76
  dealId: z.number().int().positive().optional(),
83
- ...dealFields,
84
- /** The negotiation's one link to Commerce. "" clears it. */
85
- listingId: z.union([z.string().uuid(), z.literal("")]).optional(),
86
- /** The negotiation's own bid. "" clears it. Never writes the listing. */
87
- currentBid: z.string().optional(),
77
+ /** Required to create: a deal is that Commerce listing's GTM record. Cannot change. */
78
+ listingId: z.string().uuid().optional(),
79
+ /** Optional extras, unspecified until filled in. "" clears one. */
80
+ customer: z.string().optional(),
81
+ owner: z.string().optional(),
88
82
  confirm: z.boolean().optional(),
89
83
  });
90
84
  export const gtmDealWriteCapability = defineStaffMcpCapability({
91
85
  id: "gtm.deal.write",
92
86
  domain: "gtm",
93
87
  roles: ["reviewer", "admin"],
94
- description: 'GTM staff only: create a negotiation, or replace it when dealId is supplied. The customer also associates it to that company. listingId links it to one Commerce listing and is the only connection between the two; currentBid records what the buyer offered and never writes the listing. Pass "" to clear either. Requires confirm: true.',
88
+ description: 'GTM staff only: a deal is the GTM record of one Commerce listing, and its comments hang off it. Create one with listingId (one deal per listing; a second is a conflict), or update one with dealId. customer and owner are optional; on update only the fields you pass change, and "" clears one. The listing cannot change. A bid is a comment with bid: true (see comment_write); the newest one is the current bid. Requires confirm: true.',
95
89
  mutation: "preview-confirm",
96
90
  http: { method: "POST", path: "/internal/v1/gtm/deals" },
97
91
  input: gtmDealWriteInput,
98
92
  execute: async (ctx, input) => {
99
93
  const parsed = gtmDealWriteInput.parse(input);
100
- const body = {
101
- customer: parsed.customer,
102
- owner: parsed.owner,
103
- supplier: parsed.supplier,
104
- gpu_type: parsed.gpuType,
105
- };
106
- // Omitted leaves the stored value alone; "" clears it.
107
- if (parsed.listingId !== undefined)
108
- body.listing_id = parsed.listingId;
109
- if (parsed.currentBid !== undefined)
110
- body.current_bid = parsed.currentBid;
94
+ if (parsed.dealId === undefined && parsed.listingId === undefined) {
95
+ throw new Error("listingId is required to create a deal");
96
+ }
97
+ if (parsed.dealId !== undefined && parsed.listingId !== undefined) {
98
+ throw new Error("a deal's listing cannot change; omit listingId when updating");
99
+ }
100
+ // Only what the caller passed is sent: omitted is unspecified on create
101
+ // and untouched on update; "" clears.
102
+ const body = {};
103
+ for (const [key, value] of [
104
+ ["listing_id", parsed.listingId],
105
+ ["customer", parsed.customer],
106
+ ["owner", parsed.owner],
107
+ ]) {
108
+ if (value !== undefined)
109
+ body[key] = value;
110
+ }
111
111
  if (!ctx.confirmed) {
112
112
  return preview("gtm.deal.write", { dealId: parsed.dealId, ...body });
113
113
  }
@@ -116,26 +116,33 @@ export const gtmDealWriteCapability = defineStaffMcpCapability({
116
116
  : "/internal/v1/gtm/deals", { method: parsed.dealId ? "PATCH" : "POST", body });
117
117
  },
118
118
  });
119
- export const gtmCommentListInput = z.object({
119
+ /** Company fields that carry their own side thread. */
120
+ const companyField = z.enum(["name", "owner_tags", "notes", "tickets"]);
121
+ /** A thread hangs off one deal, one company (or one of its fields), or one ticket. */
122
+ const commentThread = {
120
123
  dealId: z.number().int().positive().optional(),
121
124
  companyId: z.number().int().positive().optional(),
122
- });
125
+ taskId: z.number().int().positive().optional(),
126
+ /** With companyId: that field's side thread instead of the main one. */
127
+ field: companyField.optional(),
128
+ };
129
+ export const gtmCommentListInput = z.object(commentThread);
123
130
  export const gtmCommentListCapability = defineStaffMcpCapability({
124
131
  id: "gtm.comment.list",
125
132
  domain: "gtm",
126
133
  roles: ["reviewer", "admin"],
127
- description: "GTM staff only: list the plaintext comment thread for one deal or one company, replies included. A reply carries parent_id.",
134
+ description: "GTM staff only: list the plaintext comment thread for one deal, one company (or one company field with field), or one ticket (taskId), replies included. A reply carries parent_id.",
128
135
  mutation: "read",
129
136
  http: { method: "GET", path: "/internal/v1/gtm/deals/{dealId}/comments" },
130
137
  input: gtmCommentListInput,
131
138
  execute: (ctx, input) => {
132
139
  const parsed = gtmCommentListInput.parse(input);
133
- return ctx.fetch(commentThreadPath(parsed), { method: "GET" });
140
+ const query = parsed.field ? `?field=${encodeURIComponent(parsed.field)}` : "";
141
+ return ctx.fetch(commentThreadPath(parsed) + query, { method: "GET" });
134
142
  },
135
143
  });
136
144
  export const gtmCommentWriteInput = z.object({
137
- dealId: z.number().int().positive().optional(),
138
- companyId: z.number().int().positive().optional(),
145
+ ...commentThread,
139
146
  commentId: z.number().int().positive().optional(),
140
147
  /** Set to reply to an existing comment. Threads are one level deep. */
141
148
  parentId: z.number().int().positive().optional(),
@@ -147,14 +154,14 @@ export const gtmCommentWriteCapability = defineStaffMcpCapability({
147
154
  id: "gtm.comment.write",
148
155
  domain: "gtm",
149
156
  roles: ["reviewer", "admin"],
150
- description: "GTM staff only: append a plaintext comment to a deal or a company, reply to one with parentId, or replace one when commentId is supplied. bid=true adds only the BID tag and changes no other behavior. Mention a teammate by writing @name in the body. Requires confirm: true.",
157
+ description: "GTM staff only: append a plaintext comment to a deal, a company (or one company field with field), or a ticket (taskId); reply to one with parentId, or replace one when commentId is supplied. bid=true adds only the BID tag and changes no other behavior. Mention a teammate by writing @name in the body. Requires confirm: true.",
151
158
  mutation: "preview-confirm",
152
159
  http: { method: "POST", path: "/internal/v1/gtm/deals/{dealId}/comments" },
153
160
  input: gtmCommentWriteInput,
154
161
  execute: async (ctx, input) => {
155
162
  const parsed = gtmCommentWriteInput.parse(input);
156
- if (!parsed.commentId && !parsed.dealId && !parsed.companyId) {
157
- throw new Error("dealId or companyId is required when creating a comment");
163
+ if (!parsed.commentId && !parsed.dealId && !parsed.companyId && !parsed.taskId) {
164
+ throw new Error("dealId, companyId or taskId is required when creating a comment");
158
165
  }
159
166
  const body = {
160
167
  body: parsed.body,
@@ -163,10 +170,14 @@ export const gtmCommentWriteCapability = defineStaffMcpCapability({
163
170
  if (parsed.parentId) {
164
171
  body.parent_id = parsed.parentId;
165
172
  }
173
+ if (parsed.field) {
174
+ body.field = parsed.field;
175
+ }
166
176
  if (!ctx.confirmed) {
167
177
  return preview("gtm.comment.write", {
168
178
  dealId: parsed.dealId,
169
179
  companyId: parsed.companyId,
180
+ taskId: parsed.taskId,
170
181
  commentId: parsed.commentId,
171
182
  ...body,
172
183
  });
@@ -181,39 +192,20 @@ function commentThreadPath(parsed) {
181
192
  if (parsed.companyId) {
182
193
  return `/internal/v1/gtm/companies/${seg(String(parsed.companyId))}/comments`;
183
194
  }
195
+ if (parsed.taskId) {
196
+ return `/internal/v1/gtm/tasks/${seg(String(parsed.taskId))}/comments`;
197
+ }
184
198
  if (parsed.dealId) {
185
199
  return `/internal/v1/gtm/deals/${seg(String(parsed.dealId))}/comments`;
186
200
  }
187
- throw new Error("dealId or companyId is required");
201
+ throw new Error("dealId, companyId or taskId is required");
188
202
  }
189
- export const gtmMentionDismissInput = z.object({
190
- commentId: z.number().int().positive(),
191
- confirm: z.boolean().optional(),
192
- });
193
- export const gtmMentionDismissCapability = defineStaffMcpCapability({
194
- id: "gtm.mention.dismiss",
195
- domain: "gtm",
196
- roles: ["reviewer", "admin"],
197
- description: "GTM staff only: dismiss one mention for the signed-in user. The comment is untouched and everyone else still sees it. Requires confirm: true.",
198
- mutation: "preview-confirm",
199
- http: { method: "POST", path: "/internal/v1/gtm/comments/{commentId}/dismiss" },
200
- input: gtmMentionDismissInput,
201
- execute: async (ctx, input) => {
202
- const parsed = gtmMentionDismissInput.parse(input);
203
- if (!ctx.confirmed) {
204
- return preview("gtm.mention.dismiss", { commentId: parsed.commentId });
205
- }
206
- return ctx.fetch(`/internal/v1/gtm/comments/${seg(String(parsed.commentId))}/dismiss`, {
207
- method: "POST",
208
- });
209
- },
210
- });
211
203
  export const gtmWorkspaceShowInput = z.object({});
212
204
  export const gtmWorkspaceShowCapability = defineStaffMcpCapability({
213
205
  id: "gtm.workspace.show",
214
206
  domain: "gtm",
215
207
  roles: ["reviewer", "admin"],
216
- description: "GTM staff only: show companies tagged to the signed-in user, deals they own, undismissed mentions of them, and tickets assigned to them.",
208
+ description: "GTM staff only: show companies tagged to the signed-in user, deals they own, mentions of them (answered once they comment in that thread), and tickets assigned to them.",
217
209
  mutation: "read",
218
210
  http: { method: "GET", path: "/internal/v1/gtm/me" },
219
211
  input: gtmWorkspaceShowInput,
@@ -308,9 +300,6 @@ function gtmDeleteCapability(entity, idKey, route, consequence) {
308
300
  const companyDelete = gtmDeleteCapability("company", "companyId", "/internal/v1/gtm/companies", "Refused while the company still has deals or tickets.");
309
301
  export const gtmCompanyDeleteInput = companyDelete.input;
310
302
  export const gtmCompanyDeleteCapability = companyDelete.capability;
311
- const dealDelete = gtmDeleteCapability("deal", "dealId", "/internal/v1/gtm/deals", "Its comments are deleted with it; tickets that referenced it are kept.");
312
- export const gtmDealDeleteInput = dealDelete.input;
313
- export const gtmDealDeleteCapability = dealDelete.capability;
314
303
  const commentDelete = gtmDeleteCapability("comment", "commentId", "/internal/v1/gtm/comments", "Removes only that comment.");
315
304
  export const gtmCommentDeleteInput = commentDelete.input;
316
305
  export const gtmCommentDeleteCapability = commentDelete.capability;
@@ -325,13 +314,16 @@ export const gtmTaskDeleteCapability = taskDelete.capability;
325
314
  */
326
315
  export const gtmListingListInput = z.object({
327
316
  gpuType: z.string().optional(),
328
- limit: z.number().int().positive().max(200).optional(),
317
+ state: z.enum(["available", "internal", "delisted"]).optional(),
318
+ /** Commerce serves at most 100 per page; pass nextCursor back for the rest. */
319
+ limit: z.number().int().positive().max(100).optional(),
320
+ cursor: z.string().optional(),
329
321
  });
330
322
  export const gtmListingListCapability = defineStaffMcpCapability({
331
323
  id: "gtm.listing.list",
332
324
  domain: "gtm",
333
325
  roles: ["reviewer", "admin"],
334
- description: "GTM staff only: list Commerce listings to apply a bid against, optionally filtered by GPU type. Read-only; this is Commerce's catalog, not CRM data.",
326
+ description: "GTM staff only: list Commerce listings (each can carry one GTM deal), optionally filtered by GPU type or state. Returns at most 100 per call with nextCursor; pass it back as cursor for the next page. Includes provider (staff-only). Read-only; this is Commerce's catalog, not CRM data.",
335
327
  mutation: "read",
336
328
  http: { method: "GET", path: "/internal/v1/listings" },
337
329
  input: gtmListingListInput,
@@ -340,10 +332,17 @@ export const gtmListingListCapability = defineStaffMcpCapability({
340
332
  const parts = [`limit=${encodeURIComponent(String(parsed.limit ?? 50))}`];
341
333
  if (parsed.gpuType)
342
334
  parts.push(`q=${encodeURIComponent(parsed.gpuType)}`);
335
+ if (parsed.state)
336
+ parts.push(`state=${encodeURIComponent(parsed.state)}`);
337
+ if (parsed.cursor)
338
+ parts.push(`cursor=${encodeURIComponent(parsed.cursor)}`);
343
339
  const page = (await ctx.fetch(`/internal/v1/listings?${parts.join("&")}`, {
344
340
  method: "GET",
345
341
  }));
346
- return { listings: (page.listings ?? []).map(publicListing) };
342
+ return {
343
+ listings: (page.listings ?? []).map(staffListing),
344
+ nextCursor: page.nextCursor ?? null,
345
+ };
347
346
  },
348
347
  });
349
348
  export const gtmListingUpdateInput = z.object({
@@ -355,7 +354,7 @@ export const gtmListingUpdateInput = z.object({
355
354
  allowsDepositPlan: z.boolean().optional(),
356
355
  downPaymentPct: z.string().optional(),
357
356
  capacityNodes: z.number().int().positive().optional(),
358
- state: z.enum(["draft", "listed", "delisted"]).optional(),
357
+ state: z.enum(["available", "internal", "delisted"]).optional(),
359
358
  confirm: z.boolean().optional(),
360
359
  });
361
360
  export const gtmListingUpdateCapability = defineStaffMcpCapability({
@@ -383,9 +382,14 @@ export const gtmListingUpdateCapability = defineStaffMcpCapability({
383
382
  * region are allowed; operator ids and provider names are not. Every listing
384
383
  * this module returns -- list, update, and the apply result -- goes through it.
385
384
  */
386
- function publicListing(raw) {
385
+ /**
386
+ * GTM tools are staff-only, behind the reviewer boundary, so they keep the
387
+ * listing's provider -- a deal's supplier is its listing's. The operator
388
+ * record's own id and legal name still stay out.
389
+ */
390
+ function staffListing(raw) {
387
391
  const listing = raw;
388
- const { operatorId: _operatorId, operatorName: _operatorName, provider: _provider, ...safe } = listing;
392
+ const { operatorId: _operatorId, operatorName: _operatorName, ...safe } = listing;
389
393
  return safe;
390
394
  }
391
395
  /**
@@ -404,7 +408,7 @@ async function patchListing(ctx, listingId, body) {
404
408
  method: "PATCH",
405
409
  body: { ...body, version: current.version },
406
410
  });
407
- return publicListing(updated);
411
+ return staffListing(updated);
408
412
  }
409
413
  function listingPatchBody(parsed) {
410
414
  const body = {};
@@ -434,7 +438,7 @@ export const gtmTaskActivityListCapability = defineStaffMcpCapability({
434
438
  id: "gtm.task.activity.list",
435
439
  domain: "gtm",
436
440
  roles: ["reviewer", "admin"],
437
- description: "GTM staff only: read one ticket's timeline — the comments left on it and the field changes recorded against it, oldest first.",
441
+ description: "GTM staff only: read one ticket's history — its creation and the field changes recorded against it, oldest first. Its comments are gtm.comment.list with taskId.",
438
442
  mutation: "read",
439
443
  http: { method: "GET", path: "/internal/v1/gtm/tasks/{taskId}/activity" },
440
444
  input: gtmTaskActivityListInput,
@@ -445,99 +449,6 @@ export const gtmTaskActivityListCapability = defineStaffMcpCapability({
445
449
  });
446
450
  },
447
451
  });
448
- export const gtmTaskActivityWriteInput = z.object({
449
- taskId: z.number().int().positive(),
450
- body: z.string().min(1),
451
- /** Set to reply to a comment on the same ticket. Threads are one level deep. */
452
- parentId: z.number().int().positive().optional(),
453
- confirm: z.boolean().optional(),
454
- });
455
- export const gtmTaskActivityWriteCapability = defineStaffMcpCapability({
456
- id: "gtm.task.activity.write",
457
- domain: "gtm",
458
- roles: ["reviewer", "admin"],
459
- description: "GTM staff only: add a plaintext comment to a ticket, or reply to one with parentId. Mention a teammate by writing @name in the body. Requires confirm: true.",
460
- mutation: "preview-confirm",
461
- http: { method: "POST", path: "/internal/v1/gtm/tasks/{taskId}/activity" },
462
- input: gtmTaskActivityWriteInput,
463
- execute: async (ctx, input) => {
464
- const parsed = gtmTaskActivityWriteInput.parse(input);
465
- const body = { body: parsed.body, parent_id: parsed.parentId ?? null };
466
- if (!ctx.confirmed) {
467
- return preview("gtm.task.activity.write", { taskId: parsed.taskId, ...body });
468
- }
469
- return ctx.fetch(`/internal/v1/gtm/tasks/${seg(String(parsed.taskId))}/activity`, {
470
- method: "POST",
471
- body,
472
- });
473
- },
474
- });
475
- /**
476
- * Company cell comments. These hang off one field of one company — the
477
- * discussion about a name, its owner tags, its summary, or its tickets —
478
- * rather than off the company's main thread.
479
- */
480
- const cellField = z.enum(["name", "owner_tags", "notes", "tickets"]);
481
- export const gtmCellCommentListInput = z.object({
482
- companyId: z.number().int().positive(),
483
- field: cellField,
484
- });
485
- export const gtmCellCommentListCapability = defineStaffMcpCapability({
486
- id: "gtm.cell.comment.list",
487
- domain: "gtm",
488
- roles: ["reviewer", "admin"],
489
- description: "GTM staff only: read the comments attached to one field of one company (name, owner_tags, notes or tickets), replies included.",
490
- mutation: "read",
491
- http: { method: "GET", path: "/internal/v1/gtm/companies/{companyId}/cells/{field}/comments" },
492
- input: gtmCellCommentListInput,
493
- execute: (ctx, input) => {
494
- const parsed = gtmCellCommentListInput.parse(input);
495
- return ctx.fetch(`/internal/v1/gtm/companies/${seg(String(parsed.companyId))}/cells/${seg(parsed.field)}/comments`, { method: "GET" });
496
- },
497
- });
498
- export const gtmCellCommentWriteInput = z.object({
499
- companyId: z.number().int().positive().optional(),
500
- field: cellField.optional(),
501
- /** Set to replace an existing cell comment; companyId and field are then unused. */
502
- commentId: z.number().int().positive().optional(),
503
- parentId: z.number().int().positive().optional(),
504
- body: z.string().min(1),
505
- confirm: z.boolean().optional(),
506
- });
507
- export const gtmCellCommentWriteCapability = defineStaffMcpCapability({
508
- id: "gtm.cell.comment.write",
509
- domain: "gtm",
510
- roles: ["reviewer", "admin"],
511
- description: "GTM staff only: comment on one field of one company, reply to such a comment with parentId, or replace one when commentId is supplied. Requires confirm: true.",
512
- mutation: "preview-confirm",
513
- http: { method: "POST", path: "/internal/v1/gtm/companies/{companyId}/cells/{field}/comments" },
514
- input: gtmCellCommentWriteInput,
515
- execute: async (ctx, input) => {
516
- const parsed = gtmCellCommentWriteInput.parse(input);
517
- if (!parsed.commentId && (!parsed.companyId || !parsed.field)) {
518
- throw new Error("companyId and field are required unless commentId is supplied");
519
- }
520
- if (!ctx.confirmed) {
521
- return preview("gtm.cell.comment.write", {
522
- companyId: parsed.companyId,
523
- field: parsed.field,
524
- commentId: parsed.commentId,
525
- parentId: parsed.parentId,
526
- body: parsed.body,
527
- });
528
- }
529
- if (parsed.commentId) {
530
- return ctx.fetch(`/internal/v1/gtm/company-comments/${seg(String(parsed.commentId))}`, {
531
- method: "PATCH",
532
- body: { body: parsed.body },
533
- });
534
- }
535
- return ctx.fetch(`/internal/v1/gtm/companies/${seg(String(parsed.companyId))}/cells/${seg(parsed.field)}/comments`, { method: "POST", body: { body: parsed.body, parent_id: parsed.parentId ?? null } });
536
- },
537
- });
538
- const cellCommentDelete = gtmDeleteCapability("cell.comment", "commentId", "/internal/v1/gtm/company-comments", "Removes that field comment and its replies.");
539
- export const gtmCellCommentDeleteInput = cellCommentDelete.input;
540
- export const gtmCellCommentDeleteCapability = cellCommentDelete.capability;
541
452
  /**
542
453
  * Bulk owner-tag reassignment. This is the one GTM edit that is genuinely
543
454
  * N-at-a-time — a book of companies moving to a new owner — and doing it one
@@ -597,20 +508,14 @@ export const staffGTMCapabilities = [
597
508
  gtmCompanyBulkRetagCapability,
598
509
  gtmDealListCapability,
599
510
  gtmDealWriteCapability,
600
- gtmDealDeleteCapability,
601
511
  gtmCommentListCapability,
602
512
  gtmCommentWriteCapability,
603
513
  gtmCommentDeleteCapability,
604
- gtmCellCommentListCapability,
605
- gtmCellCommentWriteCapability,
606
- gtmCellCommentDeleteCapability,
607
514
  gtmTaskListCapability,
608
515
  gtmTaskWriteCapability,
609
516
  gtmTaskDeleteCapability,
610
517
  gtmTaskActivityListCapability,
611
- gtmTaskActivityWriteCapability,
612
518
  gtmListingListCapability,
613
519
  gtmListingUpdateCapability,
614
- gtmMentionDismissCapability,
615
520
  gtmWorkspaceShowCapability,
616
521
  ];
@@ -11,6 +11,6 @@ export { logsLatestCapability, logsTailCapability, observabilityCapabilities, te
11
11
  export { enrollmentTokenRevokeCapability, enrollmentTokenRevokeInput, facilityCreateCapability, facilityCreateInput, facilityUpdateCapability, facilityUpdateInput, nodeAdminKeyCapability, nodeAdminKeyInput, nodeBenchmarkCapability, nodeBenchmarkInput, nodeDeployCapability, nodeDeployInput, nodeDetailsUpdateCapability, nodeDetailsUpdateInput, nodeHardResetCapability, nodeHardResetInput, nodeOperatorTransferCapability, nodeOperatorTransferInput, nodeRebootCapability, nodeRebootInput, nodeShowCapability, nodeShowInput, nodeTestResultCapability, nodeTestResultInput, nodeTestTriggerCapability, nodeTestTriggerInput, nodeUpdateCapability, nodeUpdateInput, operatorCreateCapability, operatorCreateInput, operatorDeleteCapability, operatorDeleteInput, operatorUpdateCapability, operatorUpdateInput, staffFleetCapabilities, } from "./capabilities/staff-fleet.ts";
12
12
  export { bidShowCapability, bidShowInput, bidAcceptCapability, bidAcceptInput, bidRejectCapability, bidRejectInput, billingInvoicesListCapability, billingInvoicesListInput, inventoryDelistCapability, inventoryDelistInput, inventoryUpdateCapability, inventoryUpdateInput, reservationActivateCapability, reservationActivateInput, reservationCancelCapability, reservationCancelInput, reservationCompleteCapability, reservationCompleteInput, reservationDraftCreateCapability, reservationDraftCreateInput, reservationListCapability, reservationListInput, reservationMachinesListCapability, reservationMachinesListInput, reservationPublishCapability, reservationPublishInput, reservationShowCapability, reservationShowInput, reservationSshKeyAddCapability, reservationSshKeyAddInput, reservationSshKeyRemoveCapability, reservationSshKeyRemoveInput, reservationTransferCapability, reservationTransferInput, reservationUpdateCapability, reservationUpdateInput, staffCommerceCapabilities, tenantCreditProfileShowCapability, tenantCreditProfileShowInput, } from "./capabilities/staff-commerce.ts";
13
13
  export { sshKeyAddCapability, sshKeyAddInput, sshKeyPushCapability, sshKeyPushInput, sshKeyRevokeCapability, sshKeyRevokeInput, staffAccessCapabilities, } from "./capabilities/staff-access.ts";
14
- export { gtmCommentDeleteCapability, gtmCommentDeleteInput, gtmCommentListCapability, gtmCommentListInput, gtmCommentWriteCapability, gtmCommentWriteInput, gtmCompanyDeleteCapability, gtmCompanyDeleteInput, gtmCompanyListCapability, gtmCompanyListInput, gtmCompanyWriteCapability, gtmCompanyWriteInput, gtmCompanyBulkRetagCapability, gtmCompanyBulkRetagInput, gtmCellCommentDeleteCapability, gtmCellCommentDeleteInput, gtmCellCommentListCapability, gtmCellCommentListInput, gtmCellCommentWriteCapability, gtmCellCommentWriteInput, gtmDealDeleteCapability, gtmDealDeleteInput, gtmDealListCapability, gtmDealListInput, gtmMentionDismissCapability, gtmMentionDismissInput, gtmListingListCapability, gtmListingListInput, gtmListingUpdateCapability, gtmListingUpdateInput, gtmDealWriteCapability, gtmDealWriteInput, gtmTaskDeleteCapability, gtmTaskDeleteInput, gtmTaskListCapability, gtmTaskListInput, gtmTaskWriteCapability, gtmTaskWriteInput, gtmTaskActivityListCapability, gtmTaskActivityListInput, gtmTaskActivityWriteCapability, gtmTaskActivityWriteInput, gtmWorkspaceShowCapability, gtmWorkspaceShowInput, staffGTMCapabilities, } from "./capabilities/staff-gtm.ts";
14
+ export { gtmCommentDeleteCapability, gtmCommentDeleteInput, gtmCommentListCapability, gtmCommentListInput, gtmCommentWriteCapability, gtmCommentWriteInput, gtmCompanyDeleteCapability, gtmCompanyDeleteInput, gtmCompanyListCapability, gtmCompanyListInput, gtmCompanyWriteCapability, gtmCompanyWriteInput, gtmCompanyBulkRetagCapability, gtmCompanyBulkRetagInput, gtmDealListCapability, gtmDealListInput, gtmListingListCapability, gtmListingListInput, gtmListingUpdateCapability, gtmListingUpdateInput, gtmDealWriteCapability, gtmDealWriteInput, gtmTaskDeleteCapability, gtmTaskDeleteInput, gtmTaskListCapability, gtmTaskListInput, gtmTaskWriteCapability, gtmTaskWriteInput, gtmTaskActivityListCapability, gtmTaskActivityListInput, gtmWorkspaceShowCapability, gtmWorkspaceShowInput, staffGTMCapabilities, } from "./capabilities/staff-gtm.ts";
15
15
  export { CLIENTS, parseRole, roleAtLeast, ROLES, type ClientKind, type Role } from "./role.ts";
16
16
  export { bindHardwareFields, HARDWARE_CATALOG_PATH, HARDWARE_KINDS, HARDWARE_LISTING_FIELDS, HARDWARE_LISTING_FILTERS, loadHardwareCatalog, looksLikeSpecDump, normalizeCatalogQuery, parseHardwareCatalog, projectCatalogTerms, proposeListingPayload, resolveCatalogTerm, UNRESOLVED_CATALOG_HINT, type CatalogBindResult, type CatalogTerm, type HardwareCatalog, type HardwareKind, type ResolvedCatalogTerm, } from "./spec-catalog.ts";
@@ -11,6 +11,6 @@ export { logsLatestCapability, logsTailCapability, observabilityCapabilities, te
11
11
  export { enrollmentTokenRevokeCapability, enrollmentTokenRevokeInput, facilityCreateCapability, facilityCreateInput, facilityUpdateCapability, facilityUpdateInput, nodeAdminKeyCapability, nodeAdminKeyInput, nodeBenchmarkCapability, nodeBenchmarkInput, nodeDeployCapability, nodeDeployInput, nodeDetailsUpdateCapability, nodeDetailsUpdateInput, nodeHardResetCapability, nodeHardResetInput, nodeOperatorTransferCapability, nodeOperatorTransferInput, nodeRebootCapability, nodeRebootInput, nodeShowCapability, nodeShowInput, nodeTestResultCapability, nodeTestResultInput, nodeTestTriggerCapability, nodeTestTriggerInput, nodeUpdateCapability, nodeUpdateInput, operatorCreateCapability, operatorCreateInput, operatorDeleteCapability, operatorDeleteInput, operatorUpdateCapability, operatorUpdateInput, staffFleetCapabilities, } from "./capabilities/staff-fleet.js";
12
12
  export { bidShowCapability, bidShowInput, bidAcceptCapability, bidAcceptInput, bidRejectCapability, bidRejectInput, billingInvoicesListCapability, billingInvoicesListInput, inventoryDelistCapability, inventoryDelistInput, inventoryUpdateCapability, inventoryUpdateInput, reservationActivateCapability, reservationActivateInput, reservationCancelCapability, reservationCancelInput, reservationCompleteCapability, reservationCompleteInput, reservationDraftCreateCapability, reservationDraftCreateInput, reservationListCapability, reservationListInput, reservationMachinesListCapability, reservationMachinesListInput, reservationPublishCapability, reservationPublishInput, reservationShowCapability, reservationShowInput, reservationSshKeyAddCapability, reservationSshKeyAddInput, reservationSshKeyRemoveCapability, reservationSshKeyRemoveInput, reservationTransferCapability, reservationTransferInput, reservationUpdateCapability, reservationUpdateInput, staffCommerceCapabilities, tenantCreditProfileShowCapability, tenantCreditProfileShowInput, } from "./capabilities/staff-commerce.js";
13
13
  export { sshKeyAddCapability, sshKeyAddInput, sshKeyPushCapability, sshKeyPushInput, sshKeyRevokeCapability, sshKeyRevokeInput, staffAccessCapabilities, } from "./capabilities/staff-access.js";
14
- export { gtmCommentDeleteCapability, gtmCommentDeleteInput, gtmCommentListCapability, gtmCommentListInput, gtmCommentWriteCapability, gtmCommentWriteInput, gtmCompanyDeleteCapability, gtmCompanyDeleteInput, gtmCompanyListCapability, gtmCompanyListInput, gtmCompanyWriteCapability, gtmCompanyWriteInput, gtmCompanyBulkRetagCapability, gtmCompanyBulkRetagInput, gtmCellCommentDeleteCapability, gtmCellCommentDeleteInput, gtmCellCommentListCapability, gtmCellCommentListInput, gtmCellCommentWriteCapability, gtmCellCommentWriteInput, gtmDealDeleteCapability, gtmDealDeleteInput, gtmDealListCapability, gtmDealListInput, gtmMentionDismissCapability, gtmMentionDismissInput, gtmListingListCapability, gtmListingListInput, gtmListingUpdateCapability, gtmListingUpdateInput, gtmDealWriteCapability, gtmDealWriteInput, gtmTaskDeleteCapability, gtmTaskDeleteInput, gtmTaskListCapability, gtmTaskListInput, gtmTaskWriteCapability, gtmTaskWriteInput, gtmTaskActivityListCapability, gtmTaskActivityListInput, gtmTaskActivityWriteCapability, gtmTaskActivityWriteInput, gtmWorkspaceShowCapability, gtmWorkspaceShowInput, staffGTMCapabilities, } from "./capabilities/staff-gtm.js";
14
+ export { gtmCommentDeleteCapability, gtmCommentDeleteInput, gtmCommentListCapability, gtmCommentListInput, gtmCommentWriteCapability, gtmCommentWriteInput, gtmCompanyDeleteCapability, gtmCompanyDeleteInput, gtmCompanyListCapability, gtmCompanyListInput, gtmCompanyWriteCapability, gtmCompanyWriteInput, gtmCompanyBulkRetagCapability, gtmCompanyBulkRetagInput, gtmDealListCapability, gtmDealListInput, gtmListingListCapability, gtmListingListInput, gtmListingUpdateCapability, gtmListingUpdateInput, gtmDealWriteCapability, gtmDealWriteInput, gtmTaskDeleteCapability, gtmTaskDeleteInput, gtmTaskListCapability, gtmTaskListInput, gtmTaskWriteCapability, gtmTaskWriteInput, gtmTaskActivityListCapability, gtmTaskActivityListInput, gtmWorkspaceShowCapability, gtmWorkspaceShowInput, staffGTMCapabilities, } from "./capabilities/staff-gtm.js";
15
15
  export { CLIENTS, parseRole, roleAtLeast, ROLES } from "./role.js";
16
16
  export { bindHardwareFields, HARDWARE_CATALOG_PATH, HARDWARE_KINDS, HARDWARE_LISTING_FIELDS, HARDWARE_LISTING_FILTERS, loadHardwareCatalog, looksLikeSpecDump, normalizeCatalogQuery, parseHardwareCatalog, projectCatalogTerms, proposeListingPayload, resolveCatalogTerm, UNRESOLVED_CATALOG_HINT, } from "./spec-catalog.js";