@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.
- package/dist/main.js +879 -278
- 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
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
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
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
[
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
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.
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
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
|
-
|
|
4262
|
-
|
|
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
|
|
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",
|