@kikimoradev/kcloud 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.js +1129 -76
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -13,6 +13,24 @@ import createClient from "openapi-fetch";
13
13
  import { finished } from "node:stream/promises";
14
14
  import { StdioServerTransport, serveStdio } from "@modelcontextprotocol/server/stdio";
15
15
  import { McpServer } from "@modelcontextprotocol/server";
16
+ //#region ../../packages/cli-kit/src/bytes.ts
17
+ const units = [
18
+ "B",
19
+ "KiB",
20
+ "MiB",
21
+ "GiB",
22
+ "TiB"
23
+ ];
24
+ const bytesOf = (bytes) => {
25
+ let value = bytes;
26
+ let unit = 0;
27
+ while (value >= 1024 && unit < units.length - 1) {
28
+ value /= 1024;
29
+ unit += 1;
30
+ }
31
+ return unit === 0 ? `${bytes} B` : `${value.toFixed(1)} ${units[unit] ?? ""}`;
32
+ };
33
+ //#endregion
16
34
  //#region ../../packages/cli-kit/src/errors.ts
17
35
  const exitCodes = {
18
36
  ok: 0,
@@ -91,12 +109,13 @@ const commandFactoryOf = (spec) => (definition) => {
91
109
  }
92
110
  };
93
111
  };
94
- const defineGroup$1 = ({ name, summary, usage, subcommands }) => ({
112
+ const defineGroup$1 = ({ name, summary, usage, subcommands, fallback }) => ({
95
113
  name,
96
114
  summary,
97
115
  usage,
98
116
  optionsUsage: [],
99
117
  subcommands,
118
+ ...fallback === void 0 ? {} : { fallback },
100
119
  execute: () => {
101
120
  throw new Error(`${name} is a group of commands and runs none of its own`);
102
121
  }
@@ -334,23 +353,27 @@ const report = (error, terminal, environment, program, debugVariable) => {
334
353
  };
335
354
  const dispatcherOf = (spec) => {
336
355
  const rendering = spec;
337
- const walk = async (commands, args, trail, context) => {
356
+ const executed = (command, args, help, context) => {
357
+ if (!help) return command.execute(args, context);
358
+ context.terminal.line(commandUsageOf(rendering, command));
359
+ return Promise.resolve(exitCodes.ok);
360
+ };
361
+ const walk = async (commands, args, trail, context, fallback) => {
338
362
  const located = locateCommand(args, spec.globalOptions);
363
+ const command = located.name === null ? void 0 : commands.find(({ name }) => name === located.name);
364
+ const listsHelp = located.name === null && located.help;
365
+ if (command === void 0 && fallback !== void 0 && !listsHelp) return executed(fallback, args, located.help, context);
366
+ const listed = fallback === void 0 ? commands : [fallback, ...commands];
339
367
  if (located.name === null) {
340
368
  if (located.help) {
341
- context.terminal.line(commandsUsageOf(rendering, commands, trail));
369
+ context.terminal.line(commandsUsageOf(rendering, listed, trail));
342
370
  return exitCodes.ok;
343
371
  }
344
- throw new UsageError(trail.length === 0 ? "Name a command." : `${pathOf(trail)}needs a subcommand.`, commandsUsageOf(rendering, commands, trail));
345
- }
346
- const command = commands.find(({ name }) => name === located.name);
347
- if (command === void 0) throw new UsageError(`There is no command ${pathOf(trail)}${located.name}.`, commandsUsageOf(rendering, commands, trail));
348
- if (command.subcommands.length > 0) return walk(command.subcommands, located.args, [...trail, command.name], context);
349
- if (located.help) {
350
- context.terminal.line(commandUsageOf(rendering, command));
351
- return exitCodes.ok;
372
+ throw new UsageError(trail.length === 0 ? "Name a command." : `${pathOf(trail)}needs a subcommand.`, commandsUsageOf(rendering, listed, trail));
352
373
  }
353
- return command.execute(located.args, context);
374
+ if (command === void 0) throw new UsageError(`There is no command ${pathOf(trail)}${located.name}.`, commandsUsageOf(rendering, listed, trail));
375
+ if (command.subcommands.length > 0) return walk(command.subcommands, located.args, [...trail, command.name], context, command.fallback);
376
+ return executed(command, located.args, located.help, context);
354
377
  };
355
378
  return async ({ argv, commands, context }) => {
356
379
  try {
@@ -972,6 +995,7 @@ const errorCodes = Object.keys({
972
995
  "agent.tool_duplicate": "rule_violation",
973
996
  "agent.context_too_long": "rule_violation",
974
997
  "agent.docker_unavailable": "rule_violation",
998
+ "agent.node_unsuitable": "rule_violation",
975
999
  "agent.storage_exhausted": "rule_violation",
976
1000
  "agent.invalid_transition": "rule_violation",
977
1001
  "agent.busy": "conflict",
@@ -988,6 +1012,17 @@ const errorCodes = Object.keys({
988
1012
  "runtime.monitor_draining": "conflict",
989
1013
  "runtime.monitor_limited": "conflict",
990
1014
  "runtime.monitor_check_unsupported": "rule_violation",
1015
+ "node.not_found": "not_found",
1016
+ "node.name_taken": "conflict",
1017
+ "node.offline": "rule_violation",
1018
+ "node.not_registered": "rule_violation",
1019
+ "node.registration_invalid": "unauthenticated",
1020
+ "node.registration_used": "conflict",
1021
+ "node.in_use": "conflict",
1022
+ "node.host_unknown": "rule_violation",
1023
+ "node.split_exceeds_host": "rule_violation",
1024
+ "project.last_admin": "conflict",
1025
+ "agent.node_unassigned": "rule_violation",
991
1026
  "stream.too_many": "rate_limited",
992
1027
  "infrastructure.not_configured": "rule_violation",
993
1028
  "infrastructure.provider_error": "internal",
@@ -1235,6 +1270,7 @@ const agentSessionSchema = z.object({
1235
1270
  endedAt: z.iso.datetime().nullable(),
1236
1271
  ok: z.boolean().nullable(),
1237
1272
  killedReason: z.string().nullable(),
1273
+ failureReason: z.string().nullable(),
1238
1274
  error: z.string().nullable(),
1239
1275
  costUsd: z.number().nullable(),
1240
1276
  durationMs: z.int().nullable(),
@@ -1329,7 +1365,8 @@ z.object({
1329
1365
  unavailableReason: z.enum([
1330
1366
  "not_found",
1331
1367
  "tampered",
1332
- "too_large"
1368
+ "too_large",
1369
+ "node_offline"
1333
1370
  ]).nullable(),
1334
1371
  truncated: z.boolean()
1335
1372
  }).meta({ id: "TranscriptPage" });
@@ -1392,6 +1429,233 @@ pageSchema(z.object({
1392
1429
  finishedAt: z.iso.datetime().nullable()
1393
1430
  }).meta({ id: "TaskCost" })).meta({ id: "TaskCostPage" });
1394
1431
  costQuerySchema.extend({ cursor: cursorSchema.optional() });
1432
+ z.enum(["monitor", "executor"]);
1433
+ const sessionSlotCountSchema = z.int().min(1).max(64);
1434
+ const sessionQueueEntrySchema = z.object({
1435
+ agentId: idSchema,
1436
+ agentName: z.string().nullable(),
1437
+ projectId: idSchema.nullable(),
1438
+ projectName: z.string().nullable(),
1439
+ position: z.int().positive(),
1440
+ since: z.iso.datetime(),
1441
+ taskId: z.string().nullable()
1442
+ }).meta({ id: "SessionQueueEntry" });
1443
+ const sessionPoolSchema = z.object({
1444
+ slots: sessionSlotCountSchema.nullable(),
1445
+ effective: z.int().nonnegative(),
1446
+ inUse: z.int().nonnegative(),
1447
+ waiting: z.array(sessionQueueEntrySchema)
1448
+ }).meta({ id: "SessionPool" });
1449
+ z.object({
1450
+ version: z.int().nonnegative(),
1451
+ ready: z.boolean(),
1452
+ monitor: sessionPoolSchema,
1453
+ executor: sessionPoolSchema
1454
+ }).meta({ id: "SessionSlots" });
1455
+ z.object({
1456
+ version: z.int().nonnegative(),
1457
+ monitor: sessionSlotCountSchema.nullable(),
1458
+ executor: sessionSlotCountSchema.nullable()
1459
+ }).meta({ id: "UpdateSessionSlotsBody" });
1460
+ const agentAdmissionStateSchema = z.discriminatedUnion("state", [z.object({
1461
+ state: z.literal("waiting"),
1462
+ since: z.iso.datetime(),
1463
+ position: z.int().positive()
1464
+ }), z.object({ state: z.literal("holding") })]).meta({ id: "AgentAdmissionState" });
1465
+ const agentAdmissionSchema = z.object({
1466
+ monitor: agentAdmissionStateSchema.nullable(),
1467
+ executor: agentAdmissionStateSchema.nullable()
1468
+ }).meta({ id: "AgentAdmission" });
1469
+ const nodeStateSchema = z.enum([
1470
+ "pending",
1471
+ "online",
1472
+ "offline"
1473
+ ]);
1474
+ const nodeProviderSchema = z.enum(["self_hosted"]);
1475
+ const nodeNameSchema = z.string().trim().regex(slugPattern);
1476
+ const nodeHostSchema = z.object({
1477
+ cpus: z.number().nonnegative(),
1478
+ memoryBytes: z.number().nonnegative(),
1479
+ dataRoot: z.string(),
1480
+ serverVersion: z.string()
1481
+ }).meta({ id: "NodeHost" });
1482
+ const nodeCapabilityNames = [
1483
+ "linux",
1484
+ "windows",
1485
+ "docker",
1486
+ "isolation",
1487
+ "gpu",
1488
+ "desktop"
1489
+ ];
1490
+ const nodeCapabilitySchema = z.enum(nodeCapabilityNames).meta({ id: "NodeCapability" });
1491
+ const nodeCapabilitiesSchema = z.object({
1492
+ sandbox: z.boolean(),
1493
+ uidIsolation: z.boolean(),
1494
+ offered: z.array(nodeCapabilitySchema).optional()
1495
+ }).meta({ id: "NodeCapabilities" });
1496
+ const minPoolCpus = .1;
1497
+ const maxSplitCpus = 1024;
1498
+ const maxSplitMemoryMb = 16777216;
1499
+ const hundredths = (value) => Math.abs(Math.round(value * 100) - value * 100) < 1e-9;
1500
+ const splitCpusSchema = (min) => z.number().min(min).max(maxSplitCpus).refine(hundredths, { message: "expected at most two decimal places" });
1501
+ const splitMemorySchema = (min) => z.int().min(min).max(maxSplitMemoryMb);
1502
+ const nodeReserveSchema = z.object({
1503
+ cpus: splitCpusSchema(0),
1504
+ memoryMb: splitMemorySchema(0)
1505
+ }).meta({ id: "NodeReserve" });
1506
+ const nodePoolSplitSchema = z.object({
1507
+ cpus: splitCpusSchema(minPoolCpus),
1508
+ memoryMb: splitMemorySchema(256),
1509
+ slots: z.int().min(1).max(64)
1510
+ }).meta({ id: "NodePoolSplit" });
1511
+ const nodeSplitSchema = z.object({
1512
+ reserve: nodeReserveSchema,
1513
+ monitor: nodePoolSplitSchema,
1514
+ executor: nodePoolSplitSchema
1515
+ }).meta({ id: "NodeSplit" });
1516
+ const poolLimitSchema = z.object({
1517
+ enforced: z.boolean(),
1518
+ reason: z.enum([
1519
+ "release",
1520
+ "restart",
1521
+ "node"
1522
+ ]).nullable()
1523
+ }).meta({ id: "PoolLimit" });
1524
+ const poolEnforcementSchema = z.object({
1525
+ state: z.enum([
1526
+ "enforced",
1527
+ "unsplit",
1528
+ "unsupported"
1529
+ ]),
1530
+ reason: z.string().nullable()
1531
+ }).meta({ id: "PoolEnforcement" });
1532
+ const poolUsageSchema = z.object({
1533
+ memoryBytes: z.int().nonnegative(),
1534
+ memoryMaxBytes: z.int().nonnegative().nullable(),
1535
+ cpus: z.number().nonnegative().nullable(),
1536
+ memoryKills: z.int().nonnegative()
1537
+ }).meta({ id: "PoolUsage" });
1538
+ const nodePoolSchema = z.object({
1539
+ slots: z.int().nonnegative(),
1540
+ inUse: z.int().nonnegative(),
1541
+ waiting: z.array(sessionQueueEntrySchema),
1542
+ usage: poolUsageSchema.nullable()
1543
+ }).meta({ id: "NodePool" });
1544
+ const nodePoolsSchema = z.object({
1545
+ enforcement: poolEnforcementSchema,
1546
+ monitor: nodePoolSchema,
1547
+ executor: nodePoolSchema
1548
+ }).meta({ id: "NodePools" });
1549
+ const nodeSchema = z.object({
1550
+ id: idSchema,
1551
+ name: nodeNameSchema,
1552
+ provider: nodeProviderSchema,
1553
+ state: nodeStateSchema,
1554
+ engine: z.enum(runtimeEngineKinds).nullable(),
1555
+ version: z.string().nullable(),
1556
+ host: nodeHostSchema.nullable(),
1557
+ capabilities: nodeCapabilitiesSchema.nullable(),
1558
+ offered: z.array(nodeCapabilitySchema),
1559
+ split: nodeSplitSchema.nullable(),
1560
+ revision: z.int().nonnegative(),
1561
+ agentCount: z.int().nonnegative(),
1562
+ connectedAt: z.iso.datetime().nullable(),
1563
+ lastSeenAt: z.iso.datetime().nullable(),
1564
+ registrationExpiresAt: z.iso.datetime().nullable(),
1565
+ createdAt: z.iso.datetime()
1566
+ }).meta({ id: "Node" });
1567
+ const nodeAgentSchema = z.object({
1568
+ id: idSchema,
1569
+ name: z.string(),
1570
+ slug: z.string(),
1571
+ projectId: idSchema,
1572
+ projectName: z.string(),
1573
+ state: agentStateSchema,
1574
+ poolLimit: poolLimitSchema.nullable()
1575
+ }).meta({ id: "NodeAgent" });
1576
+ const nodeRuntimeSchema = z.object({
1577
+ reportedAt: z.iso.datetime(),
1578
+ docker: z.object({
1579
+ state: z.enum([
1580
+ "ok",
1581
+ "unavailable",
1582
+ "disabled",
1583
+ "adopting"
1584
+ ]),
1585
+ lastError: z.string().nullable()
1586
+ }),
1587
+ storage: z.object({
1588
+ data: storageStateSchema,
1589
+ engine: storageStateSchema
1590
+ }),
1591
+ reconcile: z.object({
1592
+ lastTickAt: z.iso.datetime().nullable(),
1593
+ lastTickError: z.string().nullable(),
1594
+ agentsQueued: z.int().nonnegative(),
1595
+ agentsActive: z.int().nonnegative()
1596
+ }),
1597
+ ingest: z.object({ readers: z.int().nonnegative() }),
1598
+ sessions: z.object({
1599
+ monitor: z.object({
1600
+ effective: z.int().nonnegative(),
1601
+ inUse: z.int().nonnegative()
1602
+ }),
1603
+ executor: z.object({
1604
+ effective: z.int().nonnegative(),
1605
+ inUse: z.int().nonnegative()
1606
+ })
1607
+ }),
1608
+ pools: nodePoolsSchema
1609
+ }).meta({ id: "NodeRuntime" });
1610
+ nodeSchema.extend({
1611
+ agents: z.array(nodeAgentSchema),
1612
+ runtime: nodeRuntimeSchema.nullable()
1613
+ }).meta({ id: "NodeDetail" });
1614
+ z.object({ items: z.array(nodeSchema) }).meta({ id: "NodeList" });
1615
+ z.object({ nodeId: idSchema });
1616
+ z.object({
1617
+ organizationId: idSchema,
1618
+ nodeId: idSchema
1619
+ });
1620
+ z.object({
1621
+ revision: z.int().nonnegative(),
1622
+ split: nodeSplitSchema.nullable()
1623
+ }).meta({ id: "UpdateNodeSplitBody" });
1624
+ const createNodeBodySchema = z.object({ name: nodeNameSchema }).meta({ id: "CreateNodeBody" });
1625
+ const nodeRegistrationSchema = z.object({
1626
+ token: z.string(),
1627
+ expiresAt: z.iso.datetime()
1628
+ }).meta({ id: "NodeRegistration" });
1629
+ z.object({
1630
+ node: nodeSchema,
1631
+ registration: nodeRegistrationSchema
1632
+ }).meta({ id: "CreatedNode" }).meta({ id: "RenewedNodeRegistration" });
1633
+ z.object({
1634
+ token: z.string().min(1).max(256),
1635
+ version: z.string().min(1).max(128),
1636
+ engine: z.enum(runtimeEngineKinds),
1637
+ host: nodeHostSchema.nullable(),
1638
+ capabilities: nodeCapabilitiesSchema
1639
+ }).meta({ id: "RegisterNodeBody" });
1640
+ z.object({
1641
+ nodeId: idSchema,
1642
+ credential: z.string()
1643
+ }).meta({ id: "RegisteredNode" });
1644
+ const bytesPerMegabyte = 1048576;
1645
+ const inHundredths = (value) => Math.round(value * 100);
1646
+ const hostMemoryMbOf = (host) => Math.floor(host.memoryBytes / bytesPerMegabyte);
1647
+ const splitOverflowOf = (split, host) => {
1648
+ const cpus = inHundredths(split.reserve.cpus) + inHundredths(split.monitor.cpus) + inHundredths(split.executor.cpus) - inHundredths(host.cpus);
1649
+ const memoryMb = split.reserve.memoryMb + split.monitor.memoryMb + split.executor.memoryMb - hostMemoryMbOf(host);
1650
+ return {
1651
+ cpus: cpus / 100,
1652
+ memoryMb
1653
+ };
1654
+ };
1655
+ const splitFits = (split, host) => {
1656
+ const overflow = splitOverflowOf(split, host);
1657
+ return overflow.cpus <= 0 && overflow.memoryMb <= 0;
1658
+ };
1395
1659
  //#endregion
1396
1660
  //#region ../../packages/contracts/src/identity.ts
1397
1661
  const emailSchema = z.email().max(254);
@@ -1575,6 +1839,11 @@ const auditActionSchema = z.enum([
1575
1839
  "node.volume_resized",
1576
1840
  "node.quiesced",
1577
1841
  "node.resumed",
1842
+ "node.created",
1843
+ "node.registered",
1844
+ "node.registration_renewed",
1845
+ "node.deleted",
1846
+ "node.split_changed",
1578
1847
  "master_key.reencrypted",
1579
1848
  "dead_letter.retried",
1580
1849
  "dead_letter.discarded"
@@ -1594,7 +1863,8 @@ const auditTargetTypeSchema = z.enum([
1594
1863
  "backup",
1595
1864
  "host_operation",
1596
1865
  "volume",
1597
- "queue"
1866
+ "queue",
1867
+ "node"
1598
1868
  ]);
1599
1869
  const auditEntrySchema = z.object({
1600
1870
  id: idSchema,
@@ -1667,43 +1937,6 @@ z.object({
1667
1937
  id: idSchema,
1668
1938
  userId: idSchema
1669
1939
  });
1670
- z.enum(["monitor", "executor"]);
1671
- const sessionSlotCountSchema = z.int().min(1).max(64);
1672
- const sessionQueueEntrySchema = z.object({
1673
- agentId: idSchema,
1674
- agentName: z.string().nullable(),
1675
- projectId: idSchema.nullable(),
1676
- projectName: z.string().nullable(),
1677
- position: z.int().positive(),
1678
- since: z.iso.datetime(),
1679
- taskId: z.string().nullable()
1680
- }).meta({ id: "SessionQueueEntry" });
1681
- const sessionPoolSchema = z.object({
1682
- slots: sessionSlotCountSchema.nullable(),
1683
- effective: z.int().nonnegative(),
1684
- inUse: z.int().nonnegative(),
1685
- waiting: z.array(sessionQueueEntrySchema)
1686
- }).meta({ id: "SessionPool" });
1687
- z.object({
1688
- version: z.int().nonnegative(),
1689
- ready: z.boolean(),
1690
- monitor: sessionPoolSchema,
1691
- executor: sessionPoolSchema
1692
- }).meta({ id: "SessionSlots" });
1693
- z.object({
1694
- version: z.int().nonnegative(),
1695
- monitor: sessionSlotCountSchema.nullable(),
1696
- executor: sessionSlotCountSchema.nullable()
1697
- }).meta({ id: "UpdateSessionSlotsBody" });
1698
- const agentAdmissionStateSchema = z.discriminatedUnion("state", [z.object({
1699
- state: z.literal("waiting"),
1700
- since: z.iso.datetime(),
1701
- position: z.int().positive()
1702
- }), z.object({ state: z.literal("holding") })]).meta({ id: "AgentAdmissionState" });
1703
- const agentAdmissionSchema = z.object({
1704
- monitor: agentAdmissionStateSchema.nullable(),
1705
- executor: agentAdmissionStateSchema.nullable()
1706
- }).meta({ id: "AgentAdmission" });
1707
1940
  //#endregion
1708
1941
  //#region ../../packages/contracts/src/releases.ts
1709
1942
  const kikimoraVersionSchema = z.string().regex(versionTagPattern);
@@ -2097,7 +2330,8 @@ const agentRuntimeSchema = z.object({
2097
2330
  provision: agentProvisionSchema.nullable(),
2098
2331
  stopMode: agentStopModeSchema.nullable(),
2099
2332
  stopDeadlineAt: z.iso.datetime().nullable(),
2100
- admission: agentAdmissionSchema.nullable()
2333
+ admission: agentAdmissionSchema.nullable(),
2334
+ poolLimit: poolLimitSchema.nullable()
2101
2335
  }).meta({ id: "AgentRuntime" });
2102
2336
  const agentListItemSchema = z.object({
2103
2337
  id: idSchema,
@@ -2116,6 +2350,11 @@ const agentListItemSchema = z.object({
2116
2350
  availableActions: z.array(agentActionSchema),
2117
2351
  updatedAt: z.iso.datetime()
2118
2352
  }).meta({ id: "AgentListItem" });
2353
+ const agentNodeSchema = z.object({
2354
+ id: idSchema,
2355
+ name: nodeNameSchema,
2356
+ state: nodeStateSchema
2357
+ }).meta({ id: "AgentNode" });
2119
2358
  agentListItemSchema.extend({
2120
2359
  version: z.int().nonnegative(),
2121
2360
  description: z.string(),
@@ -2130,6 +2369,8 @@ agentListItemSchema.extend({
2130
2369
  toolAssignments: z.array(agentToolSchema),
2131
2370
  release: releaseSummarySchema.nullable(),
2132
2371
  runtime: agentRuntimeSchema,
2372
+ node: agentNodeSchema.nullable(),
2373
+ requiredCapabilities: z.array(nodeCapabilitySchema),
2133
2374
  runtimeUid: z.int().positive().nullable(),
2134
2375
  sandbox: agentSandboxSchema.nullable(),
2135
2376
  createdAt: z.iso.datetime()
@@ -2147,11 +2388,15 @@ const distinctCredentials = {
2147
2388
  const hasUniqueTools = (assignments) => new Set(assignments.map((assignment) => assignment.toolId)).size === assignments.length;
2148
2389
  const agentRepositoryLinksSchema = z.array(agentRepositoryLinkSchema).min(1).max(32);
2149
2390
  const agentToolAssignmentsSchema = z.array(toolAssignmentSchema).max(32).refine(hasUniqueTools, { message: "expected every tool to be assigned once" });
2391
+ const hasUniqueCapabilities = (names) => new Set(names).size === names.length;
2392
+ const requiredCapabilitiesSchema = z.array(nodeCapabilitySchema).max(nodeCapabilityNames.length).refine(hasUniqueCapabilities, { message: "expected every capability once" });
2150
2393
  const createAgentBodySchema = z.object({
2151
2394
  name: agentNameSchema,
2152
2395
  slug: slugSchema.optional(),
2153
2396
  description: agentDescriptionSchema,
2154
2397
  kikimoraVersion: kikimoraVersionSchema,
2398
+ nodeId: idSchema,
2399
+ requiredCapabilities: requiredCapabilitiesSchema.optional(),
2155
2400
  claudeCredentialId: idSchema,
2156
2401
  githubCredentialId: idSchema,
2157
2402
  repositories: agentRepositoryLinksSchema,
@@ -2423,7 +2668,7 @@ z.object({
2423
2668
  pending: z.array(idSchema)
2424
2669
  }),
2425
2670
  capacity: capacityForecastSchema
2426
- }).meta({ id: "Node" });
2671
+ }).meta({ id: "PlatformNode" });
2427
2672
  z.object({ role: volumeRoleSchema });
2428
2673
  z.object({ sizeGb: z.int().positive() }).meta({ id: "ResizeVolumeBody" });
2429
2674
  z.object({ agents: z.int().nonnegative() }).meta({ id: "QuiesceResult" });
@@ -2529,6 +2774,7 @@ z.object({
2529
2774
  version: z.string(),
2530
2775
  apiImage: installedImageSchema,
2531
2776
  webImage: installedImageSchema,
2777
+ nodeImage: installedImageSchema,
2532
2778
  latest: z.string().nullable(),
2533
2779
  latestError: registryUnavailableSchema.nullable(),
2534
2780
  versions: z.array(z.string()),
@@ -2619,11 +2865,16 @@ const agentUsageSchema = z.object({
2619
2865
  blockReadBytes: byteCountSchema,
2620
2866
  blockWriteBytes: byteCountSchema
2621
2867
  }).meta({ id: "AgentUsage" });
2868
+ const nodeHostSeriesSchema = z.object({
2869
+ nodeId: idSchema,
2870
+ nodeName: z.string(),
2871
+ points: z.array(hostPointSchema)
2872
+ }).meta({ id: "NodeHostSeries" });
2622
2873
  z.object({
2623
2874
  resolution: metricsResolutionSchema,
2624
2875
  from: instantSchema,
2625
2876
  to: instantSchema,
2626
- host: z.array(hostPointSchema),
2877
+ nodes: z.array(nodeHostSeriesSchema),
2627
2878
  agents: z.array(agentUsageSchema)
2628
2879
  }).meta({ id: "HostMetrics" });
2629
2880
  z.object({
@@ -2712,7 +2963,11 @@ z.strictObject({ label: textParam }), z.strictObject({
2712
2963
  }), z.strictObject({
2713
2964
  agentName: textParam,
2714
2965
  failureCount: countParam
2715
- }), z.strictObject({ agentName: textParam }), z.strictObject({ ratio: ratioParam }), z.strictObject({
2966
+ }), z.strictObject({ agentName: textParam }), z.strictObject({
2967
+ node: textParam,
2968
+ ratio: ratioParam
2969
+ }), z.strictObject({
2970
+ node: textParam,
2716
2971
  filesystem: filesystemNameSchema,
2717
2972
  ratio: ratioParam
2718
2973
  }), z.strictObject({ backupKind: backupKindSchema }), z.strictObject({ hours: z.int().positive() }), z.strictObject({
@@ -3418,7 +3673,9 @@ const agentHints = {
3418
3673
  "project.not_found": "kcloud projects list shows the projects you are a member of.",
3419
3674
  "agent.invalid_transition": "kcloud agents show <agent> lists the actions it takes now.",
3420
3675
  "agent.busy": "The agent is working on another action; try again in a moment.",
3421
- "concurrency.conflict": "Someone changed the agent meanwhile; read it again and retry."
3676
+ "concurrency.conflict": "Someone changed the agent meanwhile; read it again and retry.",
3677
+ "agent.node_unsuitable": "The node lacks a capability the agent requires; kcloud nodes show <node> lists what it offers.",
3678
+ "node.offline": "The node of the agent is not connected; kcloud nodes show <node> tells when it was last seen."
3422
3679
  };
3423
3680
  const agentFrom = async (run, reference, project) => {
3424
3681
  if (reference === void 0 || reference === "") throw new UsageError("This command needs an agent.", "Name it as <project>/<agent>, or pass --project.");
@@ -3491,12 +3748,94 @@ const confirmed = async ({ prompt, settings, question }) => {
3491
3748
  })) throw new CancelledError("Nothing was changed.");
3492
3749
  };
3493
3750
  //#endregion
3751
+ //#region src/commands/nodes/pools.ts
3752
+ const poolLimitReasons = {
3753
+ release: "its Kikimora release predates pool limits",
3754
+ restart: "it started before the limits applied; kcloud agents restart applies them",
3755
+ node: "its node holds no pool to a limit"
3756
+ };
3757
+ const poolLimitText = (limit) => {
3758
+ if (limit === null) return "-";
3759
+ if (limit.enforced) return "enforced";
3760
+ return limit.reason === null ? "none" : `none: ${poolLimitReasons[limit.reason]}`;
3761
+ };
3762
+ const sessionFailures = {
3763
+ poolMemoryLimit: "killed by the pool memory limit",
3764
+ launcherRefused: "refused by the session starter"
3765
+ };
3766
+ const sessionFailureText = (reason) => reason === null ? "-" : sessionFailures[reason] ?? reason;
3767
+ const unsupportedReasons = { node_outdated: "the node runs a version that reports no pools" };
3768
+ const enforcementText = ({ state, reason }) => {
3769
+ switch (state) {
3770
+ case "enforced": return "The node holds each pool to the CPU and memory of its split.";
3771
+ case "unsplit": return "The node has no split, so no pool has a hard limit on CPU or memory.";
3772
+ case "unsupported": return `The node holds no pool to a limit: ${reason === null ? "it cannot" : unsupportedReasons[reason] ?? reason}.`;
3773
+ }
3774
+ };
3775
+ const memoryOf = (pool) => {
3776
+ if (pool.usage === null) return "-";
3777
+ const { memoryBytes, memoryMaxBytes } = pool.usage;
3778
+ return memoryMaxBytes === null ? bytesOf(memoryBytes) : `${bytesOf(memoryBytes)} of ${bytesOf(memoryMaxBytes)}`;
3779
+ };
3780
+ const roles = ["monitor", "executor"];
3781
+ const queueOf = (pools) => {
3782
+ const waiting = roles.flatMap((role) => pools[role].waiting.map((entry) => [
3783
+ role,
3784
+ String(entry.position),
3785
+ entry.projectName ?? "-",
3786
+ entry.agentName ?? entry.agentId,
3787
+ entry.since,
3788
+ entry.taskId ?? "-"
3789
+ ]));
3790
+ return waiting.length === 0 ? "No session waits for a slot." : renderTable([
3791
+ { header: "WAITS FOR" },
3792
+ {
3793
+ header: "PLACE",
3794
+ align: "right"
3795
+ },
3796
+ { header: "PROJECT" },
3797
+ { header: "AGENT" },
3798
+ { header: "SINCE" },
3799
+ { header: "TASK" }
3800
+ ], waiting);
3801
+ };
3802
+ const poolsText = (pools) => [
3803
+ enforcementText(pools.enforcement),
3804
+ renderTable([
3805
+ { header: "POOL" },
3806
+ { header: "SLOTS IN USE" },
3807
+ {
3808
+ header: "CPU IN USE",
3809
+ align: "right"
3810
+ },
3811
+ { header: "MEMORY IN USE" },
3812
+ {
3813
+ header: "MEMORY KILLS",
3814
+ align: "right"
3815
+ }
3816
+ ], roles.map((role) => {
3817
+ const pool = pools[role];
3818
+ const cpus = pool.usage?.cpus ?? null;
3819
+ return [
3820
+ role,
3821
+ `${pool.inUse} of ${pool.slots}`,
3822
+ cpus === null ? "-" : cpus.toFixed(2),
3823
+ memoryOf(pool),
3824
+ pool.usage === null ? "-" : String(pool.usage.memoryKills)
3825
+ ];
3826
+ })),
3827
+ "",
3828
+ queueOf(pools)
3829
+ ].join("\n");
3830
+ //#endregion
3494
3831
  //#region src/commands/agents/plan.ts
3495
3832
  const agentPlanOf = (agent) => ({
3496
3833
  name: agent.name,
3497
3834
  slug: agent.slug,
3498
3835
  description: agent.description,
3499
3836
  kikimoraVersion: agent.kikimoraVersion,
3837
+ ...agent.node === null ? {} : { nodeId: agent.node.id },
3838
+ requiredCapabilities: agent.requiredCapabilities,
3500
3839
  claudeCredentialId: agent.claudeCredential.id,
3501
3840
  githubCredentialId: agent.githubCredential.id,
3502
3841
  repositories: agent.repositories.map(({ repositoryId, directory }) => ({
@@ -3667,6 +4006,7 @@ const admissionRowsOf = (agent) => {
3667
4006
  const { admission } = agent.runtime;
3668
4007
  return admission === null ? [] : [["monitor slot", slotOf(admission.monitor)], ["executor slot", slotOf(admission.executor)]];
3669
4008
  };
4009
+ const poolLimitRowsOf = ({ runtime }) => runtime.poolLimit === null ? [] : [["pool limit", poolLimitText(runtime.poolLimit)]];
3670
4010
  const detailsOf = (agent) => [
3671
4011
  renderTable([{ header: "FIELD" }, { header: "VALUE" }], [
3672
4012
  ["name", agent.name],
@@ -3675,6 +4015,8 @@ const detailsOf = (agent) => [
3675
4015
  ["wanted", agent.desiredState],
3676
4016
  ["observed", agent.observedState],
3677
4017
  ["release", agent.kikimoraVersion],
4018
+ ["node", agent.node === null ? "none" : `${agent.node.name} (${agent.node.state})`],
4019
+ ["requires", agent.requiredCapabilities.length === 0 ? "nothing" : agent.requiredCapabilities.join(", ")],
3678
4020
  ["timezone", agent.timezone],
3679
4021
  ["machine", `${agent.machine.cpus} vCPU, ${agent.machine.memoryMb} MB`],
3680
4022
  ["docker", agent.machine.docker ? "yes" : "no"],
@@ -3682,6 +4024,7 @@ const detailsOf = (agent) => [
3682
4024
  ["container", agent.runtime.containerId ?? "none"],
3683
4025
  ["health", agent.runtime.health],
3684
4026
  ...admissionRowsOf(agent),
4027
+ ...poolLimitRowsOf(agent),
3685
4028
  ["pending", agent.restartRequired ? agent.restartReasons.join(", ") : "-"],
3686
4029
  ["actions", agent.availableActions.join(", ")],
3687
4030
  ["version", String(agent.version)],
@@ -3699,6 +4042,109 @@ const detailsOf = (agent) => [
3699
4042
  ]))
3700
4043
  ].join("\n");
3701
4044
  //#endregion
4045
+ //#region src/commands/nodes/shared.ts
4046
+ const nodeHints = {
4047
+ ...organizationHints,
4048
+ "node.not_found": "kcloud nodes list shows the nodes of the organization.",
4049
+ "node.name_taken": "Another node of the organization has that name.",
4050
+ "node.in_use": "Delete the agents placed on it first; kcloud nodes show <node> lists them.",
4051
+ "node.registration_used": "The node has joined and holds its own credential; it needs no token."
4052
+ };
4053
+ const splitHints = {
4054
+ ...nodeHints,
4055
+ "auth.forbidden": "Splitting a node takes an admin of its organization.",
4056
+ "node.host_unknown": "The node reports its CPUs and memory once its service runs; kcloud nodes show <node> shows its state.",
4057
+ "node.split_exceeds_host": "The split asks for more than the host has; kcloud nodes split <node> shows the host.",
4058
+ "concurrency.conflict": "Someone changed the node meanwhile; run the command again."
4059
+ };
4060
+ const nodesOf = async (api, organizationId) => dataOf("list the nodes", await api.GET("/organizations/{organizationId}/nodes", { params: { path: { organizationId } } }), nodeHints).items;
4061
+ const namesOf = (nodes) => nodes.map(({ name }) => name).join(", ");
4062
+ const nodeIn = (nodes, reference, organization) => {
4063
+ const found = nodes.find((node) => node.id === reference.toLowerCase() || node.name === reference);
4064
+ if (found === void 0) throw new CliError(`There is no node ${reference} in ${organization}.`, nodes.length === 0 ? "kcloud nodes create <name> creates one." : `kcloud nodes list shows ${namesOf(nodes)}.`);
4065
+ return found;
4066
+ };
4067
+ const resolveNode = async (scope, reference) => {
4068
+ const organization = await scope.organization();
4069
+ if (isId(reference)) return {
4070
+ organizationId: organization.id,
4071
+ nodeId: reference
4072
+ };
4073
+ const node = nodeIn(await nodesOf(scope.api, organization.id), reference, organization.slug);
4074
+ return {
4075
+ organizationId: organization.id,
4076
+ nodeId: node.id
4077
+ };
4078
+ };
4079
+ const nodeDetailOf = async (api, { organizationId, nodeId }) => dataOf("describe the node", await api.GET("/organizations/{organizationId}/nodes/{nodeId}", { params: { path: {
4080
+ organizationId,
4081
+ nodeId
4082
+ } } }), nodeHints);
4083
+ const createdNode = async (api, organizationId, name) => {
4084
+ const body = { name };
4085
+ validated(createNodeBodySchema, body, "a node");
4086
+ return dataOf("create the node", await api.POST("/organizations/{organizationId}/nodes", {
4087
+ params: { path: { organizationId } },
4088
+ body
4089
+ }), nodeHints);
4090
+ };
4091
+ const renewedRegistration = async (api, { organizationId, nodeId }) => dataOf("issue a registration token for the node", await api.POST("/organizations/{organizationId}/nodes/{nodeId}/registration", { params: { path: {
4092
+ organizationId,
4093
+ nodeId
4094
+ } } }), nodeHints);
4095
+ const deleteNode = async (api, { organizationId, nodeId }) => {
4096
+ doneWith("delete the node", await api.DELETE("/organizations/{organizationId}/nodes/{nodeId}", { params: { path: {
4097
+ organizationId,
4098
+ nodeId
4099
+ } } }), nodeHints);
4100
+ };
4101
+ const splitNode = async (api, { organizationId, nodeId }, revision, split) => dataOf(split === null ? "clear the split of the node" : "split the node", await api.PUT("/organizations/{organizationId}/nodes/{nodeId}/split", {
4102
+ params: { path: {
4103
+ organizationId,
4104
+ nodeId
4105
+ } },
4106
+ body: {
4107
+ revision,
4108
+ split
4109
+ }
4110
+ }), splitHints);
4111
+ const nodeUsage = [["", "a node is named by its name or its id"]];
4112
+ //#endregion
4113
+ //#region src/commands/projects/shared.ts
4114
+ const defaultProjectColor = "#7c86ff";
4115
+ const projectHint = {
4116
+ "project.not_found": "kcloud projects list shows the projects you are a member of.",
4117
+ "project.slug_taken": "Another project of the organization has that slug.",
4118
+ "project.invalid_slug": "A slug holds lowercase letters, digits and hyphens.",
4119
+ "project.not_empty": "Delete its agents, credentials, repositories and tools first.",
4120
+ "project.member_not_found": "kcloud projects members list <project> shows them.",
4121
+ "project.member_must_be_active": "kcloud orgs members enable <member> enables the account first."
4122
+ };
4123
+ const projectOf = async (api, id) => dataOf("describe the project", await api.GET("/projects/{id}", { params: { path: { id } } }), projectHint);
4124
+ //#endregion
4125
+ //#region src/commands/agents/placement.ts
4126
+ const placedNodeId = async (run, { projectId, given, ask }) => {
4127
+ const project = await projectOf(run.api, projectId);
4128
+ const organization = `the organization of ${project.slug}`;
4129
+ const nodes = await nodesOf(run.api, project.organizationId);
4130
+ if (given !== void 0) return nodeIn(nodes, given, organization).id;
4131
+ const [only, ...others] = nodes;
4132
+ if (only === void 0) throw new CliError(`The organization of ${project.slug} has no node to run the agent on.`, "kcloud nodes create <name> creates one, and its host joins with the token it prints.");
4133
+ if (others.length === 0) {
4134
+ run.context.terminal.error(`The agent runs on ${only.name}, the only node of ${organization}.`);
4135
+ return only.id;
4136
+ }
4137
+ if (ask) return run.prompt.select({
4138
+ message: "The node it runs on",
4139
+ choices: nodes.map((node) => ({
4140
+ value: node.id,
4141
+ label: node.name,
4142
+ hint: node.state
4143
+ }))
4144
+ });
4145
+ throw new UsageError(`agents create needs --node <name>, because ${organization} has ${nodes.length} nodes.`, `The nodes are ${nodes.map(({ name }) => name).join(", ")}.`);
4146
+ };
4147
+ //#endregion
3702
4148
  //#region src/commands/agents/create.ts
3703
4149
  const claudeKinds = /* @__PURE__ */ new Set(["anthropic_api_key", "claude_oauth_token"]);
3704
4150
  const catalogueOf = async (api, projectId) => {
@@ -3800,8 +4246,21 @@ const promptText = async (run, file, question) => {
3800
4246
  validate: (value) => value.trim() === "" ? "cannot be empty" : void 0
3801
4247
  });
3802
4248
  };
4249
+ const isCapability = (name) => nodeCapabilityNames.some((capability) => capability === name);
4250
+ const requiredCapabilitiesOf = (given) => {
4251
+ const names = given.flatMap((value) => value.split(",")).map((name) => name.trim()).filter((name) => name !== "");
4252
+ const unknown = names.filter((name) => !isCapability(name));
4253
+ if (unknown.length > 0) throw new UsageError(`--require ${unknown.join(", ")} ${unknown.length === 1 ? "is not a capability" : "are not capabilities"} of a node.`, `The capabilities are ${nodeCapabilityNames.join(", ")}.`);
4254
+ return [...new Set(names.filter(isCapability))];
4255
+ };
4256
+ const requirementOf = (required) => required === void 0 ? {} : { requiredCapabilities: [...required] };
3803
4257
  const localTimezone = () => Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
3804
- const askedPlan = async (run, projectId, values) => {
4258
+ const askedPlan = async (run, projectId, values, required) => {
4259
+ const nodeId = await placedNodeId(run, {
4260
+ projectId,
4261
+ given: values.node,
4262
+ ask: !run.settings.yes
4263
+ });
3805
4264
  const catalogue = await catalogueOf(run.api, projectId);
3806
4265
  const name = values.name ?? await run.prompt.text({
3807
4266
  message: "The name of the agent",
@@ -3819,6 +4278,8 @@ const askedPlan = async (run, projectId, values) => {
3819
4278
  name,
3820
4279
  description: "",
3821
4280
  kikimoraVersion: release,
4281
+ nodeId,
4282
+ ...requirementOf(required),
3822
4283
  claudeCredentialId: await chosenCredential(run.prompt, catalogue, (kind) => claudeKinds.has(kind), "The Claude credential it works with"),
3823
4284
  githubCredentialId: await chosenCredential(run.prompt, catalogue, (kind) => kind === "github_pat", "The GitHub credential it clones with"),
3824
4285
  repositories: [...await chosenRepositories(run.prompt, catalogue)],
@@ -3837,17 +4298,32 @@ const askedPlan = async (run, projectId, values) => {
3837
4298
  }
3838
4299
  };
3839
4300
  };
3840
- const fileOf = async (run, file) => {
4301
+ const isObject = (document) => typeof document === "object" && document !== null && !Array.isArray(document);
4302
+ const fileOf = async (run, projectId, { file, node }, required) => {
3841
4303
  const document = await documentOf(run, file);
3842
- validated(createAgentBodySchema, document, "an agent");
3843
- return document;
4304
+ const placed = !isObject(document) || node === void 0 && "nodeId" in document ? document : {
4305
+ ...document,
4306
+ nodeId: await placedNodeId(run, {
4307
+ projectId,
4308
+ given: node,
4309
+ ask: false
4310
+ })
4311
+ };
4312
+ const plan = isObject(placed) ? {
4313
+ ...placed,
4314
+ ...requirementOf(required)
4315
+ } : placed;
4316
+ validated(createAgentBodySchema, plan, "an agent");
4317
+ return plan;
3844
4318
  };
3845
4319
  const createCommand = (adaptersOf) => defineCommand({
3846
4320
  name: "create",
3847
4321
  summary: "Create an agent from a file, from flags, or by asking step by step.",
3848
- usage: "agents create --project <name> [--file <path>] [--name <name>]",
4322
+ usage: "agents create --project <name> [--node <node>] [--require <capability,...>] [--file <path>] [--name <name>]",
3849
4323
  optionsUsage: [
3850
4324
  ["--project <name>", "the project that owns the agent"],
4325
+ ["--node <node>", "the node it runs on, by name or id; needed when the organization has several"],
4326
+ ["--require <capability,...>", `what its node has to offer: ${nodeCapabilityNames.join(", ")}`],
3851
4327
  ["--file <path>", "the whole agent as JSON, or - to read stdin"],
3852
4328
  ["--name <name>", "the name of the agent, instead of asking"],
3853
4329
  ["--release <x.y.z>", "the Kikimora release it runs, instead of asking"],
@@ -3856,11 +4332,17 @@ const createCommand = (adaptersOf) => defineCommand({
3856
4332
  ["--timezone <zone>", "the timezone of the agent (default: this machine's)"],
3857
4333
  ["--docker", "give the agent a Docker sidecar, when the project allows it"],
3858
4334
  ["--browser", "give the agent a browser"],
3859
- ["", "kcloud agents show <agent> --as-file prints what --file takes"]
4335
+ ["", "kcloud agents show <agent> --as-file prints what --file takes"],
4336
+ ["", "--node and --require replace the nodeId and requiredCapabilities of the file"]
3860
4337
  ],
3861
4338
  options: {
3862
4339
  ...fileOptions,
3863
4340
  project: { type: "string" },
4341
+ node: { type: "string" },
4342
+ require: {
4343
+ type: "string",
4344
+ multiple: true
4345
+ },
3864
4346
  name: { type: "string" },
3865
4347
  release: { type: "string" },
3866
4348
  "monitor-prompt-file": { type: "string" },
@@ -3881,10 +4363,14 @@ const createCommand = (adaptersOf) => defineCommand({
3881
4363
  },
3882
4364
  run: async (invocation) => {
3883
4365
  const { values } = invocation;
4366
+ const required = values.require === void 0 ? void 0 : requiredCapabilitiesOf(values.require);
3884
4367
  const run = await runOf(adaptersOf, invocation);
3885
4368
  if (values.project === void 0) throw new UsageError("agents create needs --project <name>.", "kcloud projects list shows the projects you are a member of.");
3886
4369
  const projectId = await resolveProjectId(run, values.project);
3887
- const body = values.file === void 0 ? await askedPlan(run, projectId, values) : await fileOf(run, values.file);
4370
+ const body = values.file === void 0 ? await askedPlan(run, projectId, values, required) : await fileOf(run, projectId, {
4371
+ file: values.file,
4372
+ node: values.node
4373
+ }, required);
3888
4374
  const agent = dataOf("create the agent", await run.api.POST("/projects/{id}/agents", {
3889
4375
  params: { path: { id: projectId } },
3890
4376
  body
@@ -4063,6 +4549,7 @@ const observabilityCommands = (adaptersOf) => [
4063
4549
  { header: "ROLE" },
4064
4550
  { header: "MODEL" },
4065
4551
  { header: "OUTCOME" },
4552
+ { header: "FAILURE" },
4066
4553
  {
4067
4554
  header: "COST",
4068
4555
  align: "right"
@@ -4073,6 +4560,7 @@ const observabilityCommands = (adaptersOf) => [
4073
4560
  session.agentRole ?? "-",
4074
4561
  session.model ?? "-",
4075
4562
  session.ok === null ? "running" : session.ok ? "ok" : "failed",
4563
+ sessionFailureText(session.failureReason),
4076
4564
  session.costUsd === null ? "-" : session.costUsd.toFixed(2),
4077
4565
  session.sessionId
4078
4566
  ])));
@@ -5204,7 +5692,7 @@ const agentTools = [
5204
5692
  defineTool({
5205
5693
  name: "agent_create",
5206
5694
  title: "Create an agent",
5207
- description: "Create an agent in a project. The credentials, repositories and tools are named by the ids that credentials_list, repositories_list and tools_list return; agent_show with asPlan shows a complete example. The agent stays stopped until agent_action starts it.",
5695
+ description: "Create an agent in a project. The node it runs on, the credentials, repositories and tools are named by the ids that nodes_list, credentials_list, repositories_list and tools_list return; agent_show with asPlan shows a complete example. requiredCapabilities names what the node has to offer, from the offered list of nodes_list; the instance refuses a node that lacks one. The agent stays stopped until agent_action starts it.",
5208
5696
  operations: ["createAgent"],
5209
5697
  safety: "write",
5210
5698
  input: z.object({
@@ -5369,6 +5857,391 @@ const credentialTools = [
5369
5857
  })
5370
5858
  ];
5371
5859
  //#endregion
5860
+ //#region src/commands/nodes/split.ts
5861
+ const splitFields = [
5862
+ "reserve.cpus",
5863
+ "reserve.memoryMb",
5864
+ "monitor.cpus",
5865
+ "monitor.memoryMb",
5866
+ "monitor.slots",
5867
+ "executor.cpus",
5868
+ "executor.memoryMb",
5869
+ "executor.slots"
5870
+ ];
5871
+ const valuesOf = (split) => ({
5872
+ "reserve.cpus": split.reserve.cpus,
5873
+ "reserve.memoryMb": split.reserve.memoryMb,
5874
+ "monitor.cpus": split.monitor.cpus,
5875
+ "monitor.memoryMb": split.monitor.memoryMb,
5876
+ "monitor.slots": split.monitor.slots,
5877
+ "executor.cpus": split.executor.cpus,
5878
+ "executor.memoryMb": split.executor.memoryMb,
5879
+ "executor.slots": split.executor.slots
5880
+ });
5881
+ const splitOf = (values) => ({
5882
+ reserve: {
5883
+ cpus: values["reserve.cpus"],
5884
+ memoryMb: values["reserve.memoryMb"]
5885
+ },
5886
+ monitor: {
5887
+ cpus: values["monitor.cpus"],
5888
+ memoryMb: values["monitor.memoryMb"],
5889
+ slots: values["monitor.slots"]
5890
+ },
5891
+ executor: {
5892
+ cpus: values["executor.cpus"],
5893
+ memoryMb: values["executor.memoryMb"],
5894
+ slots: values["executor.slots"]
5895
+ }
5896
+ });
5897
+ const isComplete = (values) => splitFields.every((field) => values[field] !== void 0);
5898
+ const changeOfParts = ({ reserve, monitor, executor }) => {
5899
+ const given = {
5900
+ "reserve.cpus": reserve?.cpus,
5901
+ "reserve.memoryMb": reserve?.memoryMb,
5902
+ "monitor.cpus": monitor?.cpus,
5903
+ "monitor.memoryMb": monitor?.memoryMb,
5904
+ "monitor.slots": monitor?.slots,
5905
+ "executor.cpus": executor?.cpus,
5906
+ "executor.memoryMb": executor?.memoryMb,
5907
+ "executor.slots": executor?.slots
5908
+ };
5909
+ return Object.fromEntries(Object.entries(given).filter((entry) => entry[1] !== void 0));
5910
+ };
5911
+ const cpusText = (cpus) => String(Math.round(cpus * 100) / 100);
5912
+ const hostText = (host) => `${cpusText(host.cpus)} CPU and ${hostMemoryMbOf(host)} MB`;
5913
+ const excessText = ({ cpus, memoryMb }) => [...cpus > 0 ? [`${cpusText(cpus)} CPU`] : [], ...memoryMb > 0 ? [`${memoryMb} MB`] : []].join(" and ");
5914
+ const fittingSplit = (node, change, nameOf) => {
5915
+ const merged = {
5916
+ ...node.split === null ? {} : valuesOf(node.split),
5917
+ ...change
5918
+ };
5919
+ if (!isComplete(merged)) {
5920
+ const missing = splitFields.filter((field) => merged[field] === void 0);
5921
+ throw new UsageError(`${node.name} has no split yet, so a split needs every part; ${missing.map(nameOf).join(", ")} ${missing.length === 1 ? "is" : "are"} missing.`, "Once the node has a split, a part left out keeps its value.");
5922
+ }
5923
+ const split = splitOf(merged);
5924
+ validated(nodeSplitSchema, split, "a split of a node");
5925
+ if (node.host === null) throw new CliError(`${node.name} has not reported its CPUs and memory yet, so it cannot be split.`, "The node reports them once its service runs; kcloud nodes show <node> shows its state.");
5926
+ if (!splitFits(split, node.host)) throw new CliError(`The split asks for ${excessText(splitOverflowOf(split, node.host))} more than ${node.name} has.`, `${node.name} has ${hostText(node.host)}; shrink a pool or the reserve.`);
5927
+ return split;
5928
+ };
5929
+ const splitViewOf = (node) => {
5930
+ const overflow = node.split === null || node.host === null ? null : splitOverflowOf(node.split, node.host);
5931
+ return {
5932
+ id: node.id,
5933
+ name: node.name,
5934
+ revision: node.revision,
5935
+ host: node.host === null ? null : {
5936
+ cpus: node.host.cpus,
5937
+ memoryMb: hostMemoryMbOf(node.host)
5938
+ },
5939
+ split: node.split,
5940
+ free: overflow === null ? null : {
5941
+ cpus: -overflow.cpus,
5942
+ memoryMb: -overflow.memoryMb
5943
+ }
5944
+ };
5945
+ };
5946
+ const resourceCells = ({ cpus, memoryMb }) => [cpusText(cpus), `${memoryMb} MB`];
5947
+ const oneSlotOf = ({ cpus, memoryMb, slots }) => `${cpusText(cpus / slots)} CPU, ${Math.floor(memoryMb / slots)} MB`;
5948
+ const hostLineOf = (view) => view.host === null ? `${view.name} has not reported its CPUs and memory yet.` : `The host has ${cpusText(view.host.cpus)} CPU and ${view.host.memoryMb} MB.`;
5949
+ const splitTableOf = (view, split) => {
5950
+ const poolRow = (role) => [
5951
+ role,
5952
+ ...resourceCells(split[role]),
5953
+ String(split[role].slots),
5954
+ oneSlotOf(split[role])
5955
+ ];
5956
+ const rows = [
5957
+ [
5958
+ "reserve",
5959
+ ...resourceCells(split.reserve),
5960
+ "-",
5961
+ "-"
5962
+ ],
5963
+ poolRow("monitor"),
5964
+ poolRow("executor"),
5965
+ ...view.free === null ? [] : [[
5966
+ "free",
5967
+ ...resourceCells(view.free),
5968
+ "-",
5969
+ "-"
5970
+ ]],
5971
+ ...view.host === null ? [] : [[
5972
+ "host",
5973
+ ...resourceCells(view.host),
5974
+ "-",
5975
+ "-"
5976
+ ]]
5977
+ ];
5978
+ const over = view.free === null ? "" : excessText({
5979
+ cpus: -view.free.cpus,
5980
+ memoryMb: -view.free.memoryMb
5981
+ });
5982
+ return [
5983
+ renderTable([
5984
+ { header: "PART" },
5985
+ {
5986
+ header: "CPU",
5987
+ align: "right"
5988
+ },
5989
+ {
5990
+ header: "MEMORY",
5991
+ align: "right"
5992
+ },
5993
+ {
5994
+ header: "SLOTS",
5995
+ align: "right"
5996
+ },
5997
+ { header: "ONE SLOT" }
5998
+ ], rows),
5999
+ ...view.host === null ? ["", hostLineOf(view)] : [],
6000
+ ...over === "" ? [] : ["", `The split asks for ${over} more than the host has.`]
6001
+ ].join("\n");
6002
+ };
6003
+ const splitText = (view) => view.split === null ? [
6004
+ `${view.name} has no split, so its slots are counted from its host and no pool has a hard limit on CPU or memory.`,
6005
+ hostLineOf(view),
6006
+ `kcloud nodes split set ${view.name} with every part splits it.`
6007
+ ].join("\n") : splitTableOf(view, view.split);
6008
+ const splitSummaryOf = (split) => {
6009
+ if (split === null) return "none, slots counted from the host without hard limits";
6010
+ const { reserve, monitor, executor } = split;
6011
+ const pool = (role, { cpus, memoryMb, slots }) => `${role} ${cpusText(cpus)} CPU, ${memoryMb} MB, ${slots} slots`;
6012
+ return [
6013
+ `reserve ${cpusText(reserve.cpus)} CPU, ${reserve.memoryMb} MB`,
6014
+ pool("monitor", monitor),
6015
+ pool("executor", executor)
6016
+ ].join("; ");
6017
+ };
6018
+ const cpusPattern = /^\d+(?:\.\d+)?$/u;
6019
+ const slotsPattern = /^\d+$/u;
6020
+ const memoryPattern = /^(\d+(?:\.\d+)?)\s*(m|mb|mib|g|gb|gib)?$/iu;
6021
+ const megabytesPerGigabyte = 1024;
6022
+ const memoryMbOf = (given) => {
6023
+ const match = memoryPattern.exec(given.trim());
6024
+ if (match === null) return null;
6025
+ const [, amount = "", unit = "m"] = match;
6026
+ const megabytes = Number(amount) * (unit.toLowerCase().startsWith("g") ? megabytesPerGigabyte : 1);
6027
+ return Number.isSafeInteger(megabytes) ? megabytes : null;
6028
+ };
6029
+ const readCpus = (flag, given) => {
6030
+ if (!cpusPattern.test(given)) throw new UsageError(`--${flag} ${given} is not a number of CPUs.`, "Give a decimal number, as in 2 or 0.5.");
6031
+ return Number(given);
6032
+ };
6033
+ const readMemory = (flag, given) => {
6034
+ const megabytes = memoryMbOf(given);
6035
+ if (megabytes === null) throw new UsageError(`--${flag} ${given} is not a whole number of megabytes.`, "Give megabytes, or a number with M or G, as in 2048, 2048M or 2G; G is 1024 MB.");
6036
+ return megabytes;
6037
+ };
6038
+ const readSlots = (flag, given) => {
6039
+ if (!slotsPattern.test(given)) throw new UsageError(`--${flag} ${given} is not a whole number of slots.`);
6040
+ return Number(given);
6041
+ };
6042
+ const splitOptions = {
6043
+ "reserve-cpus": { type: "string" },
6044
+ "reserve-memory": { type: "string" },
6045
+ "monitor-cpus": { type: "string" },
6046
+ "monitor-memory": { type: "string" },
6047
+ "monitor-slots": { type: "string" },
6048
+ "executor-cpus": { type: "string" },
6049
+ "executor-memory": { type: "string" },
6050
+ "executor-slots": { type: "string" }
6051
+ };
6052
+ const splitOptionOf = {
6053
+ "reserve.cpus": {
6054
+ flag: "reserve-cpus",
6055
+ read: readCpus
6056
+ },
6057
+ "reserve.memoryMb": {
6058
+ flag: "reserve-memory",
6059
+ read: readMemory
6060
+ },
6061
+ "monitor.cpus": {
6062
+ flag: "monitor-cpus",
6063
+ read: readCpus
6064
+ },
6065
+ "monitor.memoryMb": {
6066
+ flag: "monitor-memory",
6067
+ read: readMemory
6068
+ },
6069
+ "monitor.slots": {
6070
+ flag: "monitor-slots",
6071
+ read: readSlots
6072
+ },
6073
+ "executor.cpus": {
6074
+ flag: "executor-cpus",
6075
+ read: readCpus
6076
+ },
6077
+ "executor.memoryMb": {
6078
+ flag: "executor-memory",
6079
+ read: readMemory
6080
+ },
6081
+ "executor.slots": {
6082
+ flag: "executor-slots",
6083
+ read: readSlots
6084
+ }
6085
+ };
6086
+ const flagOf = (field) => `--${splitOptionOf[field].flag}`;
6087
+ const changeOf = (values) => Object.fromEntries(splitFields.flatMap((field) => {
6088
+ const { flag, read } = splitOptionOf[field];
6089
+ const given = values[flag];
6090
+ return given === void 0 ? [] : [[field, read(flag, given)]];
6091
+ }));
6092
+ const createSplitCommand = (adaptersOf) => defineGroup({
6093
+ name: "split",
6094
+ summary: "How a node is split into a reserve and two pools, and changing it.",
6095
+ usage: "nodes split [set|clear] <node>",
6096
+ fallback: defineCommand({
6097
+ name: "split",
6098
+ summary: "Show the split of a node, its host and what one slot of each pool gets.",
6099
+ usage: "nodes split <node>",
6100
+ optionsUsage: nodeUsage,
6101
+ options: {},
6102
+ positionals: {
6103
+ min: 1,
6104
+ max: 1
6105
+ },
6106
+ run: async (invocation) => {
6107
+ const run = await runOf(adaptersOf, invocation);
6108
+ const reference = await resolveNode(run, invocation.positionals[0] ?? "");
6109
+ return printed(run, splitViewOf(await nodeDetailOf(run.api, reference)), splitText);
6110
+ }
6111
+ }),
6112
+ subcommands: [defineCommand({
6113
+ name: "set",
6114
+ summary: "Split a node into a reserve and two pools, or change parts of its split.",
6115
+ usage: "nodes split set <node> [--<part>-cpus <n>] [--<part>-memory <size>] [--<pool>-slots <n>]",
6116
+ optionsUsage: [
6117
+ ...nodeUsage,
6118
+ ["--reserve-cpus <n>", "the CPUs kept for the host itself"],
6119
+ ["--reserve-memory <size>", "the memory kept for the host itself"],
6120
+ ["--monitor-cpus <n>", "the CPUs the monitor sessions share"],
6121
+ ["--monitor-memory <size>", "the memory the monitor sessions share"],
6122
+ ["--monitor-slots <n>", "how many monitor sessions run at once"],
6123
+ ["--executor-cpus <n>", "the CPUs the executor sessions share"],
6124
+ ["--executor-memory <size>", "the memory the executor sessions share"],
6125
+ ["--executor-slots <n>", "how many executor sessions run at once"],
6126
+ ["", "a size is megabytes, or a number with M or G, as in 2048 or 2G"],
6127
+ ["", "a part left out keeps its value; a node without a split needs all eight"]
6128
+ ],
6129
+ options: splitOptions,
6130
+ positionals: {
6131
+ min: 1,
6132
+ max: 1
6133
+ },
6134
+ run: async (invocation) => {
6135
+ const change = changeOf(invocation.values);
6136
+ if (Object.keys(change).length === 0) throw new UsageError("nodes split set needs at least one part of the split.", "kcloud nodes split set --help lists the parts.");
6137
+ const run = await runOf(adaptersOf, invocation);
6138
+ const reference = await resolveNode(run, invocation.positionals[0] ?? "");
6139
+ const node = await nodeDetailOf(run.api, reference);
6140
+ const split = fittingSplit(node, change, flagOf);
6141
+ const changed = await splitNode(run.api, reference, node.revision, split);
6142
+ return printed(run, splitViewOf(changed), splitText);
6143
+ }
6144
+ }), defineCommand({
6145
+ name: "clear",
6146
+ summary: "Remove the split of a node, so its slots are counted from its host.",
6147
+ usage: "nodes split clear <node>",
6148
+ optionsUsage: nodeUsage,
6149
+ options: {},
6150
+ positionals: {
6151
+ min: 1,
6152
+ max: 1
6153
+ },
6154
+ run: async (invocation) => {
6155
+ const run = await runOf(adaptersOf, invocation);
6156
+ const given = invocation.positionals[0] ?? "";
6157
+ const reference = await resolveNode(run, given);
6158
+ const node = await nodeDetailOf(run.api, reference);
6159
+ if (node.split === null) return printed(run, splitViewOf(node), () => `${node.name} has no split; nothing was changed.`);
6160
+ await confirmed({
6161
+ prompt: run.prompt,
6162
+ settings: run.settings,
6163
+ question: `Clear the split of ${given}? Its pools lose their limits on CPU and memory.`
6164
+ });
6165
+ const cleared = await splitNode(run.api, reference, node.revision, null);
6166
+ return printed(run, splitViewOf(cleared), splitText);
6167
+ }
6168
+ })]
6169
+ });
6170
+ //#endregion
6171
+ //#region src/mcp/tools/nodes.ts
6172
+ const nodeField = z.string().trim().min(1).describe("The node, by its name or its id, from nodes_list.");
6173
+ const poolPart = (role) => nodePoolSplitSchema.partial().optional().describe(`The CPUs, the memory in MB and the slots the ${role} sessions share; a field left out keeps its value.`);
6174
+ const nodeTools = [
6175
+ defineTool({
6176
+ name: "nodes_list",
6177
+ title: "List nodes",
6178
+ description: "List the nodes of the organization this server acts in, the hosts its agents run on, with their state, engine, version, capabilities, split and agent count.",
6179
+ operations: ["listNodes"],
6180
+ safety: "read",
6181
+ input: z.object({}),
6182
+ run: async (_, run) => nodesOf(run.api, (await run.organization()).id)
6183
+ }),
6184
+ defineTool({
6185
+ name: "node_show",
6186
+ title: "Show a node",
6187
+ description: "Show one node with its host, its capabilities, its split, the agents placed on it with their pool limits, and the runtime and pools it reported last.",
6188
+ operations: ["getNode"],
6189
+ safety: "read",
6190
+ input: z.object({ node: nodeField }),
6191
+ run: async ({ node }, run) => nodeDetailOf(run.api, await resolveNode(run, node))
6192
+ }),
6193
+ defineTool({
6194
+ name: "node_split_set",
6195
+ title: "Split a node into pools",
6196
+ description: "Split a node into a reserve kept for the host, a monitor pool and an executor pool, each with CPUs and memory in MB, the pools also with slots. A part left out keeps its value; a node without a split needs every part. The split has to fit the host. Returns the split with the host and what is left free. Takes an admin of the organization.",
6197
+ operations: ["getNode", "updateNodeSplit"],
6198
+ safety: "write",
6199
+ idempotent: true,
6200
+ input: z.object({
6201
+ node: nodeField,
6202
+ reserve: nodeReserveSchema.partial().optional().describe("The CPUs and the memory in MB kept for the host itself; a field left out keeps its value."),
6203
+ monitor: poolPart("monitor"),
6204
+ executor: poolPart("executor")
6205
+ }),
6206
+ run: async ({ node, ...parts }, run) => {
6207
+ const change = changeOfParts(parts);
6208
+ if (Object.keys(change).length === 0) throw new CliError("Name at least one part of the split to change.");
6209
+ const reference = await resolveNode(run, node);
6210
+ const detail = await nodeDetailOf(run.api, reference);
6211
+ const split = fittingSplit(detail, change, (field) => field);
6212
+ return splitViewOf(await splitNode(run.api, reference, detail.revision, split));
6213
+ }
6214
+ }),
6215
+ defineTool({
6216
+ name: "node_split_clear",
6217
+ title: "Clear the split of a node",
6218
+ description: "Remove the split of a node, so its slots are counted from its host again and its pools lose their limits on CPU and memory. Takes an admin of the organization.",
6219
+ operations: ["getNode", "updateNodeSplit"],
6220
+ safety: "destructive",
6221
+ idempotent: true,
6222
+ input: z.object({ node: nodeField }),
6223
+ run: async ({ node }, run) => {
6224
+ const reference = await resolveNode(run, node);
6225
+ const detail = await nodeDetailOf(run.api, reference);
6226
+ if (detail.split === null) return splitViewOf(detail);
6227
+ return splitViewOf(await splitNode(run.api, reference, detail.revision, null));
6228
+ }
6229
+ }),
6230
+ defineTool({
6231
+ name: "node_delete",
6232
+ title: "Delete a node",
6233
+ description: "Delete a node, after which its host can no longer reach the platform. The instance refuses while agents are placed on it.",
6234
+ operations: ["deleteNode"],
6235
+ safety: "destructive",
6236
+ input: z.object({ node: nodeField }),
6237
+ run: async ({ node }, run) => {
6238
+ const reference = await resolveNode(run, node);
6239
+ await deleteNode(run.api, reference);
6240
+ return { deleted: reference.nodeId };
6241
+ }
6242
+ })
6243
+ ];
6244
+ //#endregion
5372
6245
  //#region src/commands/notifications.ts
5373
6246
  const notificationHints = { "notification.not_found": "kcloud notifications list shows them." };
5374
6247
  const createNotificationsCommand = (adaptersOf) => defineGroup({
@@ -6583,18 +7456,6 @@ const projectToolTools = [
6583
7456
  })
6584
7457
  ];
6585
7458
  //#endregion
6586
- //#region src/commands/projects/shared.ts
6587
- const defaultProjectColor = "#7c86ff";
6588
- const projectHint = {
6589
- "project.not_found": "kcloud projects list shows the projects you are a member of.",
6590
- "project.slug_taken": "Another project of the organization has that slug.",
6591
- "project.invalid_slug": "A slug holds lowercase letters, digits and hyphens.",
6592
- "project.not_empty": "Delete its agents, credentials, repositories and tools first.",
6593
- "project.member_not_found": "kcloud projects members list <project> shows them.",
6594
- "project.member_must_be_active": "kcloud orgs members enable <member> enables the account first."
6595
- };
6596
- const projectOf = async (api, id) => dataOf("describe the project", await api.GET("/projects/{id}", { params: { path: { id } } }), projectHint);
6597
- //#endregion
6598
7459
  //#region src/commands/projects/members.ts
6599
7460
  const projectMembersOf = async (api, projectId) => dataOf("list the members of the project", await api.GET("/projects/{id}/members", { params: { path: { id: projectId } } }), projectHint).items;
6600
7461
  const resolveProjectMember = async (api, projectId, reference) => {
@@ -7400,6 +8261,7 @@ const kcloudTools = [
7400
8261
  ...credentialTools,
7401
8262
  ...repositoryTools,
7402
8263
  ...projectToolTools,
8264
+ ...nodeTools,
7403
8265
  ...agentTools,
7404
8266
  ...taskTools,
7405
8267
  ...observabilityTools,
@@ -7407,6 +8269,7 @@ const kcloudTools = [
7407
8269
  ...releaseTools
7408
8270
  ];
7409
8271
  const passwordPrinted = (what) => `a secret never passes through the context of a model, and kcloud prints ${what}`;
8272
+ passwordPrinted("the one-time registration token of the node");
7410
8273
  passwordPrinted("the temporary password of the new member"), passwordPrinted("the new temporary password");
7411
8274
  //#endregion
7412
8275
  //#region src/mcp/server.ts
@@ -7477,6 +8340,195 @@ const createMcpCommand = (adaptersOf, cliVersion) => defineCommand({
7477
8340
  }
7478
8341
  });
7479
8342
  //#endregion
8343
+ //#region src/commands/nodes/index.ts
8344
+ const gibibytes = (bytes) => `${(bytes / 1024 ** 3).toFixed(1)} GiB`;
8345
+ const hostOf = (node) => node.host === null ? "-" : `${node.host.cpus} CPU, ${gibibytes(node.host.memoryBytes)} of memory`;
8346
+ const capabilitiesOf = (node) => {
8347
+ if (node.capabilities === null) return "-";
8348
+ const { sandbox, uidIsolation } = node.capabilities;
8349
+ return `sandbox ${sandbox ? "yes" : "no"}, uid isolation ${uidIsolation ? "yes" : "no"}`;
8350
+ };
8351
+ const registrationOf = (node) => node.registrationExpiresAt === null ? "-" : `a token waits until ${node.registrationExpiresAt}`;
8352
+ const storageOf = ({ state, freeBytes, totalBytes, path }) => `${state}, ${gibibytes(freeBytes)} free of ${gibibytes(totalBytes)} at ${path}`;
8353
+ const runtimeRowsOf = (runtime) => [
8354
+ ["reported", runtime.reportedAt],
8355
+ ["docker", runtime.docker.lastError === null ? runtime.docker.state : `${runtime.docker.state}: ${runtime.docker.lastError}`],
8356
+ ["data storage", storageOf(runtime.storage.data)],
8357
+ ["engine storage", storageOf(runtime.storage.engine)],
8358
+ ["last reconcile", runtime.reconcile.lastTickError === null ? runtime.reconcile.lastTickAt ?? "never" : `${runtime.reconcile.lastTickAt ?? "never"}: ${runtime.reconcile.lastTickError}`],
8359
+ ["agents", `${runtime.reconcile.agentsActive} active, ${runtime.reconcile.agentsQueued} queued`],
8360
+ ["log readers", String(runtime.ingest.readers)]
8361
+ ];
8362
+ const nodeDetailsText = (node) => [
8363
+ renderTable([{ header: "FIELD" }, { header: "VALUE" }], [
8364
+ ["name", node.name],
8365
+ ["state", node.state],
8366
+ ["provider", node.provider],
8367
+ ["engine", node.engine ?? "-"],
8368
+ ["version", node.version ?? "-"],
8369
+ ["host", hostOf(node)],
8370
+ ["server", node.host?.serverVersion ?? "-"],
8371
+ ["data root", node.host?.dataRoot ?? "-"],
8372
+ ["capabilities", capabilitiesOf(node)],
8373
+ ["offers", node.offered.length === 0 ? "-" : node.offered.join(", ")],
8374
+ ["split", splitSummaryOf(node.split)],
8375
+ ["registration", registrationOf(node)],
8376
+ ["connected", node.connectedAt ?? "never"],
8377
+ ["last seen", node.lastSeenAt ?? "never"],
8378
+ ["created", node.createdAt],
8379
+ ["id", node.id]
8380
+ ]),
8381
+ "",
8382
+ node.agents.length === 0 ? "No agent is placed on the node." : renderTable([
8383
+ { header: "PROJECT" },
8384
+ { header: "AGENT" },
8385
+ { header: "STATE" },
8386
+ { header: "POOL LIMIT" },
8387
+ { header: "ID" }
8388
+ ], node.agents.map((agent) => [
8389
+ agent.projectName,
8390
+ agent.slug,
8391
+ agent.state,
8392
+ poolLimitText(agent.poolLimit),
8393
+ agent.id
8394
+ ])),
8395
+ "",
8396
+ ...node.runtime === null ? ["The node has reported no runtime yet."] : [
8397
+ renderTable([{ header: "RUNTIME" }, { header: "VALUE" }], runtimeRowsOf(node.runtime)),
8398
+ "",
8399
+ poolsText(node.runtime.pools)
8400
+ ]
8401
+ ].join("\n");
8402
+ const startHintOf = (origin) => [
8403
+ "Start the node with it as docs/install.md of kikimora-cloud describes. On a Linux host, after read -rs token:",
8404
+ ` printf '%s\\n' "$token" | /opt/kikimora-cloud/infra/node/install.sh --platform-url ${origin} --image ghcr.io/kikimoradev/kikimora-cloud/node:<version> --registration-token -`,
8405
+ "On a Windows PC:",
8406
+ ` C:\\kikimora-cloud\\src\\infra\\windows\\install.ps1 -PlatformUrl ${origin} -RegistrationToken <token>`
8407
+ ].join("\n");
8408
+ const printedRegistration = (run, created) => {
8409
+ if (run.settings.json) {
8410
+ run.context.terminal.json(created);
8411
+ return exitCodes.ok;
8412
+ }
8413
+ run.context.terminal.line(created.registration.token);
8414
+ run.context.terminal.error([`The registration token of ${created.node.name} is shown only here and works once, until ${created.registration.expiresAt}.`, startHintOf(run.origin)].join("\n"));
8415
+ return exitCodes.ok;
8416
+ };
8417
+ const createNodesCommand = (adaptersOf) => defineGroup({
8418
+ name: "nodes",
8419
+ summary: "The nodes of the organization, the hosts its agents run on.",
8420
+ usage: "nodes <command>",
8421
+ subcommands: [
8422
+ defineCommand({
8423
+ name: "list",
8424
+ summary: "List the nodes of the organization.",
8425
+ usage: "nodes list",
8426
+ options: {},
8427
+ positionals: {
8428
+ min: 0,
8429
+ max: 0
8430
+ },
8431
+ run: async (invocation) => {
8432
+ const run = await runOf(adaptersOf, invocation);
8433
+ const organization = await run.organization();
8434
+ const items = await nodesOf(run.api, organization.id);
8435
+ return printed(run, items, (nodes) => nodes.length === 0 ? `${organization.slug} has no node yet; kcloud nodes create <name> creates one.` : renderTable([
8436
+ { header: "NAME" },
8437
+ { header: "STATE" },
8438
+ { header: "ENGINE" },
8439
+ { header: "VERSION" },
8440
+ {
8441
+ header: "AGENTS",
8442
+ align: "right"
8443
+ },
8444
+ { header: "LAST SEEN" },
8445
+ { header: "ID" }
8446
+ ], nodes.map((node) => [
8447
+ node.name,
8448
+ node.state,
8449
+ node.engine ?? "-",
8450
+ node.version ?? "-",
8451
+ String(node.agentCount),
8452
+ node.lastSeenAt ?? "never",
8453
+ node.id
8454
+ ])));
8455
+ }
8456
+ }),
8457
+ defineCommand({
8458
+ name: "show",
8459
+ summary: "Show a node with its host, its split, the agents placed on it, its runtime and its pools.",
8460
+ usage: "nodes show <node>",
8461
+ optionsUsage: nodeUsage,
8462
+ options: {},
8463
+ positionals: {
8464
+ min: 1,
8465
+ max: 1
8466
+ },
8467
+ run: async (invocation) => {
8468
+ const run = await runOf(adaptersOf, invocation);
8469
+ const reference = await resolveNode(run, invocation.positionals[0] ?? "");
8470
+ return printed(run, await nodeDetailOf(run.api, reference), nodeDetailsText);
8471
+ }
8472
+ }),
8473
+ createSplitCommand(adaptersOf),
8474
+ defineCommand({
8475
+ name: "create",
8476
+ summary: "Create a node and print its one-time registration token.",
8477
+ usage: "nodes create <name>",
8478
+ optionsUsage: [["", "the name holds lowercase letters, digits and hyphens"], ["", "the token goes to stdout alone, how to start the node to stderr"]],
8479
+ options: {},
8480
+ positionals: {
8481
+ min: 1,
8482
+ max: 1
8483
+ },
8484
+ run: async (invocation) => {
8485
+ const run = await runOf(adaptersOf, invocation);
8486
+ const organization = await run.organization();
8487
+ return printedRegistration(run, await createdNode(run.api, organization.id, invocation.positionals[0] ?? ""));
8488
+ }
8489
+ }),
8490
+ defineCommand({
8491
+ name: "token",
8492
+ summary: "Issue a new registration token for a node that has not joined yet.",
8493
+ usage: "nodes token <node>",
8494
+ optionsUsage: [...nodeUsage, ["", "the token replaces the one issued before, which stops working"]],
8495
+ options: {},
8496
+ positionals: {
8497
+ min: 1,
8498
+ max: 1
8499
+ },
8500
+ run: async (invocation) => {
8501
+ const run = await runOf(adaptersOf, invocation);
8502
+ const reference = await resolveNode(run, invocation.positionals[0] ?? "");
8503
+ return printedRegistration(run, await renewedRegistration(run.api, reference));
8504
+ }
8505
+ }),
8506
+ defineCommand({
8507
+ name: "delete",
8508
+ summary: "Delete a node that runs no agent.",
8509
+ usage: "nodes delete <node>",
8510
+ optionsUsage: nodeUsage,
8511
+ options: {},
8512
+ positionals: {
8513
+ min: 1,
8514
+ max: 1
8515
+ },
8516
+ run: async (invocation) => {
8517
+ const run = await runOf(adaptersOf, invocation);
8518
+ const given = invocation.positionals[0] ?? "";
8519
+ const reference = await resolveNode(run, given);
8520
+ await confirmed({
8521
+ prompt: run.prompt,
8522
+ settings: run.settings,
8523
+ question: `Delete the node ${given}? Its host can no longer reach the platform.`
8524
+ });
8525
+ await deleteNode(run.api, reference);
8526
+ return said(run, `Deleted the node ${given}.`);
8527
+ }
8528
+ })
8529
+ ]
8530
+ });
8531
+ //#endregion
7480
8532
  //#region src/commands/releases.ts
7481
8533
  const createReleasesCommand = (adaptersOf) => defineGroup({
7482
8534
  name: "releases",
@@ -7523,6 +8575,7 @@ const kcloudCommands = () => [
7523
8575
  createCredentialsCommand(createAdapters),
7524
8576
  createRepositoriesCommand(createAdapters),
7525
8577
  createToolsCommand(createAdapters),
8578
+ createNodesCommand(createAdapters),
7526
8579
  createAgentsCommand(createAdapters),
7527
8580
  createNotificationsCommand(createAdapters),
7528
8581
  createReleasesCommand(createAdapters),