@kikimoradev/kcloud 0.15.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 +879 -278
  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",
@@ -995,6 +1019,8 @@ const errorCodes = Object.keys({
995
1019
  "node.registration_invalid": "unauthenticated",
996
1020
  "node.registration_used": "conflict",
997
1021
  "node.in_use": "conflict",
1022
+ "node.host_unknown": "rule_violation",
1023
+ "node.split_exceeds_host": "rule_violation",
998
1024
  "project.last_admin": "conflict",
999
1025
  "agent.node_unassigned": "rule_violation",
1000
1026
  "stream.too_many": "rate_limited",
@@ -1244,6 +1270,7 @@ const agentSessionSchema = z.object({
1244
1270
  endedAt: z.iso.datetime().nullable(),
1245
1271
  ok: z.boolean().nullable(),
1246
1272
  killedReason: z.string().nullable(),
1273
+ failureReason: z.string().nullable(),
1247
1274
  error: z.string().nullable(),
1248
1275
  costUsd: z.number().nullable(),
1249
1276
  durationMs: z.int().nullable(),
@@ -1402,6 +1429,43 @@ pageSchema(z.object({
1402
1429
  finishedAt: z.iso.datetime().nullable()
1403
1430
  }).meta({ id: "TaskCost" })).meta({ id: "TaskCostPage" });
1404
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" });
1405
1469
  const nodeStateSchema = z.enum([
1406
1470
  "pending",
1407
1471
  "online",
@@ -1415,10 +1479,73 @@ const nodeHostSchema = z.object({
1415
1479
  dataRoot: z.string(),
1416
1480
  serverVersion: z.string()
1417
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" });
1418
1491
  const nodeCapabilitiesSchema = z.object({
1419
1492
  sandbox: z.boolean(),
1420
- uidIsolation: z.boolean()
1493
+ uidIsolation: z.boolean(),
1494
+ offered: z.array(nodeCapabilitySchema).optional()
1421
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" });
1422
1549
  const nodeSchema = z.object({
1423
1550
  id: idSchema,
1424
1551
  name: nodeNameSchema,
@@ -1428,6 +1555,9 @@ const nodeSchema = z.object({
1428
1555
  version: z.string().nullable(),
1429
1556
  host: nodeHostSchema.nullable(),
1430
1557
  capabilities: nodeCapabilitiesSchema.nullable(),
1558
+ offered: z.array(nodeCapabilitySchema),
1559
+ split: nodeSplitSchema.nullable(),
1560
+ revision: z.int().nonnegative(),
1431
1561
  agentCount: z.int().nonnegative(),
1432
1562
  connectedAt: z.iso.datetime().nullable(),
1433
1563
  lastSeenAt: z.iso.datetime().nullable(),
@@ -1440,7 +1570,8 @@ const nodeAgentSchema = z.object({
1440
1570
  slug: z.string(),
1441
1571
  projectId: idSchema,
1442
1572
  projectName: z.string(),
1443
- state: agentStateSchema
1573
+ state: agentStateSchema,
1574
+ poolLimit: poolLimitSchema.nullable()
1444
1575
  }).meta({ id: "NodeAgent" });
1445
1576
  const nodeRuntimeSchema = z.object({
1446
1577
  reportedAt: z.iso.datetime(),
@@ -1473,7 +1604,8 @@ const nodeRuntimeSchema = z.object({
1473
1604
  effective: z.int().nonnegative(),
1474
1605
  inUse: z.int().nonnegative()
1475
1606
  })
1476
- })
1607
+ }),
1608
+ pools: nodePoolsSchema
1477
1609
  }).meta({ id: "NodeRuntime" });
1478
1610
  nodeSchema.extend({
1479
1611
  agents: z.array(nodeAgentSchema),
@@ -1485,6 +1617,10 @@ z.object({
1485
1617
  organizationId: idSchema,
1486
1618
  nodeId: idSchema
1487
1619
  });
1620
+ z.object({
1621
+ revision: z.int().nonnegative(),
1622
+ split: nodeSplitSchema.nullable()
1623
+ }).meta({ id: "UpdateNodeSplitBody" });
1488
1624
  const createNodeBodySchema = z.object({ name: nodeNameSchema }).meta({ id: "CreateNodeBody" });
1489
1625
  const nodeRegistrationSchema = z.object({
1490
1626
  token: z.string(),
@@ -1505,6 +1641,21 @@ z.object({
1505
1641
  nodeId: idSchema,
1506
1642
  credential: z.string()
1507
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
+ };
1508
1659
  //#endregion
1509
1660
  //#region ../../packages/contracts/src/identity.ts
1510
1661
  const emailSchema = z.email().max(254);
@@ -1692,6 +1843,7 @@ const auditActionSchema = z.enum([
1692
1843
  "node.registered",
1693
1844
  "node.registration_renewed",
1694
1845
  "node.deleted",
1846
+ "node.split_changed",
1695
1847
  "master_key.reencrypted",
1696
1848
  "dead_letter.retried",
1697
1849
  "dead_letter.discarded"
@@ -1785,43 +1937,6 @@ z.object({
1785
1937
  id: idSchema,
1786
1938
  userId: idSchema
1787
1939
  });
1788
- z.enum(["monitor", "executor"]);
1789
- const sessionSlotCountSchema = z.int().min(1).max(64);
1790
- const sessionQueueEntrySchema = z.object({
1791
- agentId: idSchema,
1792
- agentName: z.string().nullable(),
1793
- projectId: idSchema.nullable(),
1794
- projectName: z.string().nullable(),
1795
- position: z.int().positive(),
1796
- since: z.iso.datetime(),
1797
- taskId: z.string().nullable()
1798
- }).meta({ id: "SessionQueueEntry" });
1799
- const sessionPoolSchema = z.object({
1800
- slots: sessionSlotCountSchema.nullable(),
1801
- effective: z.int().nonnegative(),
1802
- inUse: z.int().nonnegative(),
1803
- waiting: z.array(sessionQueueEntrySchema)
1804
- }).meta({ id: "SessionPool" });
1805
- z.object({
1806
- version: z.int().nonnegative(),
1807
- ready: z.boolean(),
1808
- monitor: sessionPoolSchema,
1809
- executor: sessionPoolSchema
1810
- }).meta({ id: "SessionSlots" });
1811
- z.object({
1812
- version: z.int().nonnegative(),
1813
- monitor: sessionSlotCountSchema.nullable(),
1814
- executor: sessionSlotCountSchema.nullable()
1815
- }).meta({ id: "UpdateSessionSlotsBody" });
1816
- const agentAdmissionStateSchema = z.discriminatedUnion("state", [z.object({
1817
- state: z.literal("waiting"),
1818
- since: z.iso.datetime(),
1819
- position: z.int().positive()
1820
- }), z.object({ state: z.literal("holding") })]).meta({ id: "AgentAdmissionState" });
1821
- const agentAdmissionSchema = z.object({
1822
- monitor: agentAdmissionStateSchema.nullable(),
1823
- executor: agentAdmissionStateSchema.nullable()
1824
- }).meta({ id: "AgentAdmission" });
1825
1940
  //#endregion
1826
1941
  //#region ../../packages/contracts/src/releases.ts
1827
1942
  const kikimoraVersionSchema = z.string().regex(versionTagPattern);
@@ -2215,7 +2330,8 @@ const agentRuntimeSchema = z.object({
2215
2330
  provision: agentProvisionSchema.nullable(),
2216
2331
  stopMode: agentStopModeSchema.nullable(),
2217
2332
  stopDeadlineAt: z.iso.datetime().nullable(),
2218
- admission: agentAdmissionSchema.nullable()
2333
+ admission: agentAdmissionSchema.nullable(),
2334
+ poolLimit: poolLimitSchema.nullable()
2219
2335
  }).meta({ id: "AgentRuntime" });
2220
2336
  const agentListItemSchema = z.object({
2221
2337
  id: idSchema,
@@ -2254,6 +2370,7 @@ agentListItemSchema.extend({
2254
2370
  release: releaseSummarySchema.nullable(),
2255
2371
  runtime: agentRuntimeSchema,
2256
2372
  node: agentNodeSchema.nullable(),
2373
+ requiredCapabilities: z.array(nodeCapabilitySchema),
2257
2374
  runtimeUid: z.int().positive().nullable(),
2258
2375
  sandbox: agentSandboxSchema.nullable(),
2259
2376
  createdAt: z.iso.datetime()
@@ -2271,12 +2388,15 @@ const distinctCredentials = {
2271
2388
  const hasUniqueTools = (assignments) => new Set(assignments.map((assignment) => assignment.toolId)).size === assignments.length;
2272
2389
  const agentRepositoryLinksSchema = z.array(agentRepositoryLinkSchema).min(1).max(32);
2273
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" });
2274
2393
  const createAgentBodySchema = z.object({
2275
2394
  name: agentNameSchema,
2276
2395
  slug: slugSchema.optional(),
2277
2396
  description: agentDescriptionSchema,
2278
2397
  kikimoraVersion: kikimoraVersionSchema,
2279
2398
  nodeId: idSchema,
2399
+ requiredCapabilities: requiredCapabilitiesSchema.optional(),
2280
2400
  claudeCredentialId: idSchema,
2281
2401
  githubCredentialId: idSchema,
2282
2402
  repositories: agentRepositoryLinksSchema,
@@ -3554,6 +3674,7 @@ const agentHints = {
3554
3674
  "agent.invalid_transition": "kcloud agents show <agent> lists the actions it takes now.",
3555
3675
  "agent.busy": "The agent is working on another action; try again in a moment.",
3556
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.",
3557
3678
  "node.offline": "The node of the agent is not connected; kcloud nodes show <node> tells when it was last seen."
3558
3679
  };
3559
3680
  const agentFrom = async (run, reference, project) => {
@@ -3627,6 +3748,86 @@ const confirmed = async ({ prompt, settings, question }) => {
3627
3748
  })) throw new CancelledError("Nothing was changed.");
3628
3749
  };
3629
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
3630
3831
  //#region src/commands/agents/plan.ts
3631
3832
  const agentPlanOf = (agent) => ({
3632
3833
  name: agent.name,
@@ -3634,6 +3835,7 @@ const agentPlanOf = (agent) => ({
3634
3835
  description: agent.description,
3635
3836
  kikimoraVersion: agent.kikimoraVersion,
3636
3837
  ...agent.node === null ? {} : { nodeId: agent.node.id },
3838
+ requiredCapabilities: agent.requiredCapabilities,
3637
3839
  claudeCredentialId: agent.claudeCredential.id,
3638
3840
  githubCredentialId: agent.githubCredential.id,
3639
3841
  repositories: agent.repositories.map(({ repositoryId, directory }) => ({
@@ -3804,6 +4006,7 @@ const admissionRowsOf = (agent) => {
3804
4006
  const { admission } = agent.runtime;
3805
4007
  return admission === null ? [] : [["monitor slot", slotOf(admission.monitor)], ["executor slot", slotOf(admission.executor)]];
3806
4008
  };
4009
+ const poolLimitRowsOf = ({ runtime }) => runtime.poolLimit === null ? [] : [["pool limit", poolLimitText(runtime.poolLimit)]];
3807
4010
  const detailsOf = (agent) => [
3808
4011
  renderTable([{ header: "FIELD" }, { header: "VALUE" }], [
3809
4012
  ["name", agent.name],
@@ -3813,6 +4016,7 @@ const detailsOf = (agent) => [
3813
4016
  ["observed", agent.observedState],
3814
4017
  ["release", agent.kikimoraVersion],
3815
4018
  ["node", agent.node === null ? "none" : `${agent.node.name} (${agent.node.state})`],
4019
+ ["requires", agent.requiredCapabilities.length === 0 ? "nothing" : agent.requiredCapabilities.join(", ")],
3816
4020
  ["timezone", agent.timezone],
3817
4021
  ["machine", `${agent.machine.cpus} vCPU, ${agent.machine.memoryMb} MB`],
3818
4022
  ["docker", agent.machine.docker ? "yes" : "no"],
@@ -3820,6 +4024,7 @@ const detailsOf = (agent) => [
3820
4024
  ["container", agent.runtime.containerId ?? "none"],
3821
4025
  ["health", agent.runtime.health],
3822
4026
  ...admissionRowsOf(agent),
4027
+ ...poolLimitRowsOf(agent),
3823
4028
  ["pending", agent.restartRequired ? agent.restartReasons.join(", ") : "-"],
3824
4029
  ["actions", agent.availableActions.join(", ")],
3825
4030
  ["version", String(agent.version)],
@@ -3837,7 +4042,7 @@ const detailsOf = (agent) => [
3837
4042
  ]))
3838
4043
  ].join("\n");
3839
4044
  //#endregion
3840
- //#region src/commands/nodes.ts
4045
+ //#region src/commands/nodes/shared.ts
3841
4046
  const nodeHints = {
3842
4047
  ...organizationHints,
3843
4048
  "node.not_found": "kcloud nodes list shows the nodes of the organization.",
@@ -3845,6 +4050,13 @@ const nodeHints = {
3845
4050
  "node.in_use": "Delete the agents placed on it first; kcloud nodes show <node> lists them.",
3846
4051
  "node.registration_used": "The node has joined and holds its own credential; it needs no token."
3847
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
+ };
3848
4060
  const nodesOf = async (api, organizationId) => dataOf("list the nodes", await api.GET("/organizations/{organizationId}/nodes", { params: { path: { organizationId } } }), nodeHints).items;
3849
4061
  const namesOf = (nodes) => nodes.map(({ name }) => name).join(", ");
3850
4062
  const nodeIn = (nodes, reference, organization) => {
@@ -3886,221 +4098,51 @@ const deleteNode = async (api, { organizationId, nodeId }) => {
3886
4098
  nodeId
3887
4099
  } } }), nodeHints);
3888
4100
  };
3889
- const gibibytes = (bytes) => `${(bytes / 1024 ** 3).toFixed(1)} GiB`;
3890
- const hostOf = (node) => node.host === null ? "-" : `${node.host.cpus} CPU, ${gibibytes(node.host.memoryBytes)} of memory`;
3891
- const capabilitiesOf = (node) => {
3892
- if (node.capabilities === null) return "-";
3893
- const { sandbox, uidIsolation } = node.capabilities;
3894
- return `sandbox ${sandbox ? "yes" : "no"}, uid isolation ${uidIsolation ? "yes" : "no"}`;
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."
3895
4122
  };
3896
- const registrationOf = (node) => node.registrationExpiresAt === null ? "-" : `a token waits until ${node.registrationExpiresAt}`;
3897
- const storageOf = ({ state, freeBytes, totalBytes, path }) => `${state}, ${gibibytes(freeBytes)} free of ${gibibytes(totalBytes)} at ${path}`;
3898
- const runtimeRowsOf = (runtime) => [
3899
- ["reported", runtime.reportedAt],
3900
- ["docker", runtime.docker.lastError === null ? runtime.docker.state : `${runtime.docker.state}: ${runtime.docker.lastError}`],
3901
- ["data storage", storageOf(runtime.storage.data)],
3902
- ["engine storage", storageOf(runtime.storage.engine)],
3903
- ["last reconcile", runtime.reconcile.lastTickError === null ? runtime.reconcile.lastTickAt ?? "never" : `${runtime.reconcile.lastTickAt ?? "never"}: ${runtime.reconcile.lastTickError}`],
3904
- ["agents", `${runtime.reconcile.agentsActive} active, ${runtime.reconcile.agentsQueued} queued`],
3905
- ["monitor slots", `${runtime.sessions.monitor.inUse} of ${runtime.sessions.monitor.effective} in use`],
3906
- ["executor slots", `${runtime.sessions.executor.inUse} of ${runtime.sessions.executor.effective} in use`],
3907
- ["log readers", String(runtime.ingest.readers)]
3908
- ];
3909
- const nodeDetailsText = (node) => [
3910
- renderTable([{ header: "FIELD" }, { header: "VALUE" }], [
3911
- ["name", node.name],
3912
- ["state", node.state],
3913
- ["provider", node.provider],
3914
- ["engine", node.engine ?? "-"],
3915
- ["version", node.version ?? "-"],
3916
- ["host", hostOf(node)],
3917
- ["server", node.host?.serverVersion ?? "-"],
3918
- ["data root", node.host?.dataRoot ?? "-"],
3919
- ["capabilities", capabilitiesOf(node)],
3920
- ["registration", registrationOf(node)],
3921
- ["connected", node.connectedAt ?? "never"],
3922
- ["last seen", node.lastSeenAt ?? "never"],
3923
- ["created", node.createdAt],
3924
- ["id", node.id]
3925
- ]),
3926
- "",
3927
- node.agents.length === 0 ? "No agent is placed on the node." : renderTable([
3928
- { header: "PROJECT" },
3929
- { header: "AGENT" },
3930
- { header: "STATE" },
3931
- { header: "ID" }
3932
- ], node.agents.map((agent) => [
3933
- agent.projectName,
3934
- agent.slug,
3935
- agent.state,
3936
- agent.id
3937
- ])),
3938
- "",
3939
- node.runtime === null ? "The node has reported no runtime yet." : renderTable([{ header: "RUNTIME" }, { header: "VALUE" }], runtimeRowsOf(node.runtime))
3940
- ].join("\n");
3941
- const startHintOf = (origin) => [
3942
- "Start the node with it as docs/install.md of kikimora-cloud describes. On a Linux host, after read -rs token:",
3943
- ` printf '%s\\n' "$token" | /opt/kikimora-cloud/infra/node/install.sh --platform-url ${origin} --image ghcr.io/kikimoradev/kikimora-cloud/node:<version> --registration-token -`,
3944
- "On a Windows PC:",
3945
- ` C:\\kikimora-cloud\\src\\infra\\windows\\install.ps1 -PlatformUrl ${origin} -RegistrationToken <token>`
3946
- ].join("\n");
3947
- const printedRegistration = (run, created) => {
3948
- if (run.settings.json) {
3949
- run.context.terminal.json(created);
3950
- return exitCodes.ok;
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;
3951
4136
  }
3952
- run.context.terminal.line(created.registration.token);
3953
- 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"));
3954
- return exitCodes.ok;
3955
- };
3956
- const nodeUsage = [["", "a node is named by its name or its id"]];
3957
- const createNodesCommand = (adaptersOf) => defineGroup({
3958
- name: "nodes",
3959
- summary: "The nodes of the organization, the hosts its agents run on.",
3960
- usage: "nodes <command>",
3961
- subcommands: [
3962
- defineCommand({
3963
- name: "list",
3964
- summary: "List the nodes of the organization.",
3965
- usage: "nodes list",
3966
- options: {},
3967
- positionals: {
3968
- min: 0,
3969
- max: 0
3970
- },
3971
- run: async (invocation) => {
3972
- const run = await runOf(adaptersOf, invocation);
3973
- const organization = await run.organization();
3974
- const items = await nodesOf(run.api, organization.id);
3975
- return printed(run, items, (nodes) => nodes.length === 0 ? `${organization.slug} has no node yet; kcloud nodes create <name> creates one.` : renderTable([
3976
- { header: "NAME" },
3977
- { header: "STATE" },
3978
- { header: "ENGINE" },
3979
- { header: "VERSION" },
3980
- {
3981
- header: "AGENTS",
3982
- align: "right"
3983
- },
3984
- { header: "LAST SEEN" },
3985
- { header: "ID" }
3986
- ], nodes.map((node) => [
3987
- node.name,
3988
- node.state,
3989
- node.engine ?? "-",
3990
- node.version ?? "-",
3991
- String(node.agentCount),
3992
- node.lastSeenAt ?? "never",
3993
- node.id
3994
- ])));
3995
- }
3996
- }),
3997
- defineCommand({
3998
- name: "show",
3999
- summary: "Show a node with its host, the agents placed on it and its runtime.",
4000
- usage: "nodes show <node>",
4001
- optionsUsage: nodeUsage,
4002
- options: {},
4003
- positionals: {
4004
- min: 1,
4005
- max: 1
4006
- },
4007
- run: async (invocation) => {
4008
- const run = await runOf(adaptersOf, invocation);
4009
- const reference = await resolveNode(run, invocation.positionals[0] ?? "");
4010
- return printed(run, await nodeDetailOf(run.api, reference), nodeDetailsText);
4011
- }
4012
- }),
4013
- defineCommand({
4014
- name: "create",
4015
- summary: "Create a node and print its one-time registration token.",
4016
- usage: "nodes create <name>",
4017
- optionsUsage: [["", "the name holds lowercase letters, digits and hyphens"], ["", "the token goes to stdout alone, how to start the node to stderr"]],
4018
- options: {},
4019
- positionals: {
4020
- min: 1,
4021
- max: 1
4022
- },
4023
- run: async (invocation) => {
4024
- const run = await runOf(adaptersOf, invocation);
4025
- const organization = await run.organization();
4026
- return printedRegistration(run, await createdNode(run.api, organization.id, invocation.positionals[0] ?? ""));
4027
- }
4028
- }),
4029
- defineCommand({
4030
- name: "token",
4031
- summary: "Issue a new registration token for a node that has not joined yet.",
4032
- usage: "nodes token <node>",
4033
- optionsUsage: [...nodeUsage, ["", "the token replaces the one issued before, which stops working"]],
4034
- options: {},
4035
- positionals: {
4036
- min: 1,
4037
- max: 1
4038
- },
4039
- run: async (invocation) => {
4040
- const run = await runOf(adaptersOf, invocation);
4041
- const reference = await resolveNode(run, invocation.positionals[0] ?? "");
4042
- return printedRegistration(run, await renewedRegistration(run.api, reference));
4043
- }
4044
- }),
4045
- defineCommand({
4046
- name: "delete",
4047
- summary: "Delete a node that runs no agent.",
4048
- usage: "nodes delete <node>",
4049
- optionsUsage: nodeUsage,
4050
- options: {},
4051
- positionals: {
4052
- min: 1,
4053
- max: 1
4054
- },
4055
- run: async (invocation) => {
4056
- const run = await runOf(adaptersOf, invocation);
4057
- const given = invocation.positionals[0] ?? "";
4058
- const reference = await resolveNode(run, given);
4059
- await confirmed({
4060
- prompt: run.prompt,
4061
- settings: run.settings,
4062
- question: `Delete the node ${given}? Its host can no longer reach the platform.`
4063
- });
4064
- await deleteNode(run.api, reference);
4065
- return said(run, `Deleted the node ${given}.`);
4066
- }
4067
- })
4068
- ]
4069
- });
4070
- //#endregion
4071
- //#region src/commands/projects/shared.ts
4072
- const defaultProjectColor = "#7c86ff";
4073
- const projectHint = {
4074
- "project.not_found": "kcloud projects list shows the projects you are a member of.",
4075
- "project.slug_taken": "Another project of the organization has that slug.",
4076
- "project.invalid_slug": "A slug holds lowercase letters, digits and hyphens.",
4077
- "project.not_empty": "Delete its agents, credentials, repositories and tools first.",
4078
- "project.member_not_found": "kcloud projects members list <project> shows them.",
4079
- "project.member_must_be_active": "kcloud orgs members enable <member> enables the account first."
4080
- };
4081
- const projectOf = async (api, id) => dataOf("describe the project", await api.GET("/projects/{id}", { params: { path: { id } } }), projectHint);
4082
- //#endregion
4083
- //#region src/commands/agents/placement.ts
4084
- const placedNodeId = async (run, { projectId, given, ask }) => {
4085
- const project = await projectOf(run.api, projectId);
4086
- const organization = `the organization of ${project.slug}`;
4087
- const nodes = await nodesOf(run.api, project.organizationId);
4088
- if (given !== void 0) return nodeIn(nodes, given, organization).id;
4089
- const [only, ...others] = nodes;
4090
- 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.");
4091
- if (others.length === 0) {
4092
- run.context.terminal.error(`The agent runs on ${only.name}, the only node of ${organization}.`);
4093
- return only.id;
4094
- }
4095
- if (ask) return run.prompt.select({
4096
- message: "The node it runs on",
4097
- choices: nodes.map((node) => ({
4098
- value: node.id,
4099
- label: node.name,
4100
- hint: node.state
4101
- }))
4102
- });
4103
- throw new UsageError(`agents create needs --node <name>, because ${organization} has ${nodes.length} nodes.`, `The nodes are ${nodes.map(({ name }) => name).join(", ")}.`);
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(", ")}.`);
4104
4146
  };
4105
4147
  //#endregion
4106
4148
  //#region src/commands/agents/create.ts
@@ -4204,8 +4246,16 @@ const promptText = async (run, file, question) => {
4204
4246
  validate: (value) => value.trim() === "" ? "cannot be empty" : void 0
4205
4247
  });
4206
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] };
4207
4257
  const localTimezone = () => Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
4208
- const askedPlan = async (run, projectId, values) => {
4258
+ const askedPlan = async (run, projectId, values, required) => {
4209
4259
  const nodeId = await placedNodeId(run, {
4210
4260
  projectId,
4211
4261
  given: values.node,
@@ -4229,6 +4279,7 @@ const askedPlan = async (run, projectId, values) => {
4229
4279
  description: "",
4230
4280
  kikimoraVersion: release,
4231
4281
  nodeId,
4282
+ ...requirementOf(required),
4232
4283
  claudeCredentialId: await chosenCredential(run.prompt, catalogue, (kind) => claudeKinds.has(kind), "The Claude credential it works with"),
4233
4284
  githubCredentialId: await chosenCredential(run.prompt, catalogue, (kind) => kind === "github_pat", "The GitHub credential it clones with"),
4234
4285
  repositories: [...await chosenRepositories(run.prompt, catalogue)],
@@ -4248,7 +4299,7 @@ const askedPlan = async (run, projectId, values) => {
4248
4299
  };
4249
4300
  };
4250
4301
  const isObject = (document) => typeof document === "object" && document !== null && !Array.isArray(document);
4251
- const fileOf = async (run, projectId, { file, node }) => {
4302
+ const fileOf = async (run, projectId, { file, node }, required) => {
4252
4303
  const document = await documentOf(run, file);
4253
4304
  const placed = !isObject(document) || node === void 0 && "nodeId" in document ? document : {
4254
4305
  ...document,
@@ -4258,16 +4309,21 @@ const fileOf = async (run, projectId, { file, node }) => {
4258
4309
  ask: false
4259
4310
  })
4260
4311
  };
4261
- validated(createAgentBodySchema, placed, "an agent");
4262
- return placed;
4312
+ const plan = isObject(placed) ? {
4313
+ ...placed,
4314
+ ...requirementOf(required)
4315
+ } : placed;
4316
+ validated(createAgentBodySchema, plan, "an agent");
4317
+ return plan;
4263
4318
  };
4264
4319
  const createCommand = (adaptersOf) => defineCommand({
4265
4320
  name: "create",
4266
4321
  summary: "Create an agent from a file, from flags, or by asking step by step.",
4267
- usage: "agents create --project <name> [--node <node>] [--file <path>] [--name <name>]",
4322
+ usage: "agents create --project <name> [--node <node>] [--require <capability,...>] [--file <path>] [--name <name>]",
4268
4323
  optionsUsage: [
4269
4324
  ["--project <name>", "the project that owns the agent"],
4270
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(", ")}`],
4271
4327
  ["--file <path>", "the whole agent as JSON, or - to read stdin"],
4272
4328
  ["--name <name>", "the name of the agent, instead of asking"],
4273
4329
  ["--release <x.y.z>", "the Kikimora release it runs, instead of asking"],
@@ -4277,12 +4333,16 @@ const createCommand = (adaptersOf) => defineCommand({
4277
4333
  ["--docker", "give the agent a Docker sidecar, when the project allows it"],
4278
4334
  ["--browser", "give the agent a browser"],
4279
4335
  ["", "kcloud agents show <agent> --as-file prints what --file takes"],
4280
- ["", "--node replaces the nodeId of the file"]
4336
+ ["", "--node and --require replace the nodeId and requiredCapabilities of the file"]
4281
4337
  ],
4282
4338
  options: {
4283
4339
  ...fileOptions,
4284
4340
  project: { type: "string" },
4285
4341
  node: { type: "string" },
4342
+ require: {
4343
+ type: "string",
4344
+ multiple: true
4345
+ },
4286
4346
  name: { type: "string" },
4287
4347
  release: { type: "string" },
4288
4348
  "monitor-prompt-file": { type: "string" },
@@ -4303,13 +4363,14 @@ const createCommand = (adaptersOf) => defineCommand({
4303
4363
  },
4304
4364
  run: async (invocation) => {
4305
4365
  const { values } = invocation;
4366
+ const required = values.require === void 0 ? void 0 : requiredCapabilitiesOf(values.require);
4306
4367
  const run = await runOf(adaptersOf, invocation);
4307
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.");
4308
4369
  const projectId = await resolveProjectId(run, values.project);
4309
- const body = values.file === void 0 ? await askedPlan(run, projectId, values) : await fileOf(run, projectId, {
4370
+ const body = values.file === void 0 ? await askedPlan(run, projectId, values, required) : await fileOf(run, projectId, {
4310
4371
  file: values.file,
4311
4372
  node: values.node
4312
- });
4373
+ }, required);
4313
4374
  const agent = dataOf("create the agent", await run.api.POST("/projects/{id}/agents", {
4314
4375
  params: { path: { id: projectId } },
4315
4376
  body
@@ -4488,6 +4549,7 @@ const observabilityCommands = (adaptersOf) => [
4488
4549
  { header: "ROLE" },
4489
4550
  { header: "MODEL" },
4490
4551
  { header: "OUTCOME" },
4552
+ { header: "FAILURE" },
4491
4553
  {
4492
4554
  header: "COST",
4493
4555
  align: "right"
@@ -4498,6 +4560,7 @@ const observabilityCommands = (adaptersOf) => [
4498
4560
  session.agentRole ?? "-",
4499
4561
  session.model ?? "-",
4500
4562
  session.ok === null ? "running" : session.ok ? "ok" : "failed",
4563
+ sessionFailureText(session.failureReason),
4501
4564
  session.costUsd === null ? "-" : session.costUsd.toFixed(2),
4502
4565
  session.sessionId
4503
4566
  ])));
@@ -5629,7 +5692,7 @@ const agentTools = [
5629
5692
  defineTool({
5630
5693
  name: "agent_create",
5631
5694
  title: "Create an agent",
5632
- 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. 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.",
5633
5696
  operations: ["createAgent"],
5634
5697
  safety: "write",
5635
5698
  input: z.object({
@@ -5794,13 +5857,325 @@ const credentialTools = [
5794
5857
  })
5795
5858
  ];
5796
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
5797
6171
  //#region src/mcp/tools/nodes.ts
5798
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.`);
5799
6174
  const nodeTools = [
5800
6175
  defineTool({
5801
6176
  name: "nodes_list",
5802
6177
  title: "List nodes",
5803
- description: "List the nodes of the organization this server acts in, the hosts its agents run on, with their state, engine, version and agent count.",
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.",
5804
6179
  operations: ["listNodes"],
5805
6180
  safety: "read",
5806
6181
  input: z.object({}),
@@ -5809,12 +6184,49 @@ const nodeTools = [
5809
6184
  defineTool({
5810
6185
  name: "node_show",
5811
6186
  title: "Show a node",
5812
- description: "Show one node with its host, its capabilities, the agents placed on it and the runtime it reported last.",
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.",
5813
6188
  operations: ["getNode"],
5814
6189
  safety: "read",
5815
6190
  input: z.object({ node: nodeField }),
5816
6191
  run: async ({ node }, run) => nodeDetailOf(run.api, await resolveNode(run, node))
5817
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
+ }),
5818
6230
  defineTool({
5819
6231
  name: "node_delete",
5820
6232
  title: "Delete a node",
@@ -7928,6 +8340,195 @@ const createMcpCommand = (adaptersOf, cliVersion) => defineCommand({
7928
8340
  }
7929
8341
  });
7930
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
7931
8532
  //#region src/commands/releases.ts
7932
8533
  const createReleasesCommand = (adaptersOf) => defineGroup({
7933
8534
  name: "releases",