@kikimoradev/kcloud 0.14.0 → 0.15.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 +477 -25
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -988,6 +988,15 @@ const errorCodes = Object.keys({
|
|
|
988
988
|
"runtime.monitor_draining": "conflict",
|
|
989
989
|
"runtime.monitor_limited": "conflict",
|
|
990
990
|
"runtime.monitor_check_unsupported": "rule_violation",
|
|
991
|
+
"node.not_found": "not_found",
|
|
992
|
+
"node.name_taken": "conflict",
|
|
993
|
+
"node.offline": "rule_violation",
|
|
994
|
+
"node.not_registered": "rule_violation",
|
|
995
|
+
"node.registration_invalid": "unauthenticated",
|
|
996
|
+
"node.registration_used": "conflict",
|
|
997
|
+
"node.in_use": "conflict",
|
|
998
|
+
"project.last_admin": "conflict",
|
|
999
|
+
"agent.node_unassigned": "rule_violation",
|
|
991
1000
|
"stream.too_many": "rate_limited",
|
|
992
1001
|
"infrastructure.not_configured": "rule_violation",
|
|
993
1002
|
"infrastructure.provider_error": "internal",
|
|
@@ -1329,7 +1338,8 @@ z.object({
|
|
|
1329
1338
|
unavailableReason: z.enum([
|
|
1330
1339
|
"not_found",
|
|
1331
1340
|
"tampered",
|
|
1332
|
-
"too_large"
|
|
1341
|
+
"too_large",
|
|
1342
|
+
"node_offline"
|
|
1333
1343
|
]).nullable(),
|
|
1334
1344
|
truncated: z.boolean()
|
|
1335
1345
|
}).meta({ id: "TranscriptPage" });
|
|
@@ -1392,6 +1402,109 @@ pageSchema(z.object({
|
|
|
1392
1402
|
finishedAt: z.iso.datetime().nullable()
|
|
1393
1403
|
}).meta({ id: "TaskCost" })).meta({ id: "TaskCostPage" });
|
|
1394
1404
|
costQuerySchema.extend({ cursor: cursorSchema.optional() });
|
|
1405
|
+
const nodeStateSchema = z.enum([
|
|
1406
|
+
"pending",
|
|
1407
|
+
"online",
|
|
1408
|
+
"offline"
|
|
1409
|
+
]);
|
|
1410
|
+
const nodeProviderSchema = z.enum(["self_hosted"]);
|
|
1411
|
+
const nodeNameSchema = z.string().trim().regex(slugPattern);
|
|
1412
|
+
const nodeHostSchema = z.object({
|
|
1413
|
+
cpus: z.number().nonnegative(),
|
|
1414
|
+
memoryBytes: z.number().nonnegative(),
|
|
1415
|
+
dataRoot: z.string(),
|
|
1416
|
+
serverVersion: z.string()
|
|
1417
|
+
}).meta({ id: "NodeHost" });
|
|
1418
|
+
const nodeCapabilitiesSchema = z.object({
|
|
1419
|
+
sandbox: z.boolean(),
|
|
1420
|
+
uidIsolation: z.boolean()
|
|
1421
|
+
}).meta({ id: "NodeCapabilities" });
|
|
1422
|
+
const nodeSchema = z.object({
|
|
1423
|
+
id: idSchema,
|
|
1424
|
+
name: nodeNameSchema,
|
|
1425
|
+
provider: nodeProviderSchema,
|
|
1426
|
+
state: nodeStateSchema,
|
|
1427
|
+
engine: z.enum(runtimeEngineKinds).nullable(),
|
|
1428
|
+
version: z.string().nullable(),
|
|
1429
|
+
host: nodeHostSchema.nullable(),
|
|
1430
|
+
capabilities: nodeCapabilitiesSchema.nullable(),
|
|
1431
|
+
agentCount: z.int().nonnegative(),
|
|
1432
|
+
connectedAt: z.iso.datetime().nullable(),
|
|
1433
|
+
lastSeenAt: z.iso.datetime().nullable(),
|
|
1434
|
+
registrationExpiresAt: z.iso.datetime().nullable(),
|
|
1435
|
+
createdAt: z.iso.datetime()
|
|
1436
|
+
}).meta({ id: "Node" });
|
|
1437
|
+
const nodeAgentSchema = z.object({
|
|
1438
|
+
id: idSchema,
|
|
1439
|
+
name: z.string(),
|
|
1440
|
+
slug: z.string(),
|
|
1441
|
+
projectId: idSchema,
|
|
1442
|
+
projectName: z.string(),
|
|
1443
|
+
state: agentStateSchema
|
|
1444
|
+
}).meta({ id: "NodeAgent" });
|
|
1445
|
+
const nodeRuntimeSchema = z.object({
|
|
1446
|
+
reportedAt: z.iso.datetime(),
|
|
1447
|
+
docker: z.object({
|
|
1448
|
+
state: z.enum([
|
|
1449
|
+
"ok",
|
|
1450
|
+
"unavailable",
|
|
1451
|
+
"disabled",
|
|
1452
|
+
"adopting"
|
|
1453
|
+
]),
|
|
1454
|
+
lastError: z.string().nullable()
|
|
1455
|
+
}),
|
|
1456
|
+
storage: z.object({
|
|
1457
|
+
data: storageStateSchema,
|
|
1458
|
+
engine: storageStateSchema
|
|
1459
|
+
}),
|
|
1460
|
+
reconcile: z.object({
|
|
1461
|
+
lastTickAt: z.iso.datetime().nullable(),
|
|
1462
|
+
lastTickError: z.string().nullable(),
|
|
1463
|
+
agentsQueued: z.int().nonnegative(),
|
|
1464
|
+
agentsActive: z.int().nonnegative()
|
|
1465
|
+
}),
|
|
1466
|
+
ingest: z.object({ readers: z.int().nonnegative() }),
|
|
1467
|
+
sessions: z.object({
|
|
1468
|
+
monitor: z.object({
|
|
1469
|
+
effective: z.int().nonnegative(),
|
|
1470
|
+
inUse: z.int().nonnegative()
|
|
1471
|
+
}),
|
|
1472
|
+
executor: z.object({
|
|
1473
|
+
effective: z.int().nonnegative(),
|
|
1474
|
+
inUse: z.int().nonnegative()
|
|
1475
|
+
})
|
|
1476
|
+
})
|
|
1477
|
+
}).meta({ id: "NodeRuntime" });
|
|
1478
|
+
nodeSchema.extend({
|
|
1479
|
+
agents: z.array(nodeAgentSchema),
|
|
1480
|
+
runtime: nodeRuntimeSchema.nullable()
|
|
1481
|
+
}).meta({ id: "NodeDetail" });
|
|
1482
|
+
z.object({ items: z.array(nodeSchema) }).meta({ id: "NodeList" });
|
|
1483
|
+
z.object({ nodeId: idSchema });
|
|
1484
|
+
z.object({
|
|
1485
|
+
organizationId: idSchema,
|
|
1486
|
+
nodeId: idSchema
|
|
1487
|
+
});
|
|
1488
|
+
const createNodeBodySchema = z.object({ name: nodeNameSchema }).meta({ id: "CreateNodeBody" });
|
|
1489
|
+
const nodeRegistrationSchema = z.object({
|
|
1490
|
+
token: z.string(),
|
|
1491
|
+
expiresAt: z.iso.datetime()
|
|
1492
|
+
}).meta({ id: "NodeRegistration" });
|
|
1493
|
+
z.object({
|
|
1494
|
+
node: nodeSchema,
|
|
1495
|
+
registration: nodeRegistrationSchema
|
|
1496
|
+
}).meta({ id: "CreatedNode" }).meta({ id: "RenewedNodeRegistration" });
|
|
1497
|
+
z.object({
|
|
1498
|
+
token: z.string().min(1).max(256),
|
|
1499
|
+
version: z.string().min(1).max(128),
|
|
1500
|
+
engine: z.enum(runtimeEngineKinds),
|
|
1501
|
+
host: nodeHostSchema.nullable(),
|
|
1502
|
+
capabilities: nodeCapabilitiesSchema
|
|
1503
|
+
}).meta({ id: "RegisterNodeBody" });
|
|
1504
|
+
z.object({
|
|
1505
|
+
nodeId: idSchema,
|
|
1506
|
+
credential: z.string()
|
|
1507
|
+
}).meta({ id: "RegisteredNode" });
|
|
1395
1508
|
//#endregion
|
|
1396
1509
|
//#region ../../packages/contracts/src/identity.ts
|
|
1397
1510
|
const emailSchema = z.email().max(254);
|
|
@@ -1575,6 +1688,10 @@ const auditActionSchema = z.enum([
|
|
|
1575
1688
|
"node.volume_resized",
|
|
1576
1689
|
"node.quiesced",
|
|
1577
1690
|
"node.resumed",
|
|
1691
|
+
"node.created",
|
|
1692
|
+
"node.registered",
|
|
1693
|
+
"node.registration_renewed",
|
|
1694
|
+
"node.deleted",
|
|
1578
1695
|
"master_key.reencrypted",
|
|
1579
1696
|
"dead_letter.retried",
|
|
1580
1697
|
"dead_letter.discarded"
|
|
@@ -1594,7 +1711,8 @@ const auditTargetTypeSchema = z.enum([
|
|
|
1594
1711
|
"backup",
|
|
1595
1712
|
"host_operation",
|
|
1596
1713
|
"volume",
|
|
1597
|
-
"queue"
|
|
1714
|
+
"queue",
|
|
1715
|
+
"node"
|
|
1598
1716
|
]);
|
|
1599
1717
|
const auditEntrySchema = z.object({
|
|
1600
1718
|
id: idSchema,
|
|
@@ -2116,6 +2234,11 @@ const agentListItemSchema = z.object({
|
|
|
2116
2234
|
availableActions: z.array(agentActionSchema),
|
|
2117
2235
|
updatedAt: z.iso.datetime()
|
|
2118
2236
|
}).meta({ id: "AgentListItem" });
|
|
2237
|
+
const agentNodeSchema = z.object({
|
|
2238
|
+
id: idSchema,
|
|
2239
|
+
name: nodeNameSchema,
|
|
2240
|
+
state: nodeStateSchema
|
|
2241
|
+
}).meta({ id: "AgentNode" });
|
|
2119
2242
|
agentListItemSchema.extend({
|
|
2120
2243
|
version: z.int().nonnegative(),
|
|
2121
2244
|
description: z.string(),
|
|
@@ -2130,6 +2253,7 @@ agentListItemSchema.extend({
|
|
|
2130
2253
|
toolAssignments: z.array(agentToolSchema),
|
|
2131
2254
|
release: releaseSummarySchema.nullable(),
|
|
2132
2255
|
runtime: agentRuntimeSchema,
|
|
2256
|
+
node: agentNodeSchema.nullable(),
|
|
2133
2257
|
runtimeUid: z.int().positive().nullable(),
|
|
2134
2258
|
sandbox: agentSandboxSchema.nullable(),
|
|
2135
2259
|
createdAt: z.iso.datetime()
|
|
@@ -2152,6 +2276,7 @@ const createAgentBodySchema = z.object({
|
|
|
2152
2276
|
slug: slugSchema.optional(),
|
|
2153
2277
|
description: agentDescriptionSchema,
|
|
2154
2278
|
kikimoraVersion: kikimoraVersionSchema,
|
|
2279
|
+
nodeId: idSchema,
|
|
2155
2280
|
claudeCredentialId: idSchema,
|
|
2156
2281
|
githubCredentialId: idSchema,
|
|
2157
2282
|
repositories: agentRepositoryLinksSchema,
|
|
@@ -2423,7 +2548,7 @@ z.object({
|
|
|
2423
2548
|
pending: z.array(idSchema)
|
|
2424
2549
|
}),
|
|
2425
2550
|
capacity: capacityForecastSchema
|
|
2426
|
-
}).meta({ id: "
|
|
2551
|
+
}).meta({ id: "PlatformNode" });
|
|
2427
2552
|
z.object({ role: volumeRoleSchema });
|
|
2428
2553
|
z.object({ sizeGb: z.int().positive() }).meta({ id: "ResizeVolumeBody" });
|
|
2429
2554
|
z.object({ agents: z.int().nonnegative() }).meta({ id: "QuiesceResult" });
|
|
@@ -2529,6 +2654,7 @@ z.object({
|
|
|
2529
2654
|
version: z.string(),
|
|
2530
2655
|
apiImage: installedImageSchema,
|
|
2531
2656
|
webImage: installedImageSchema,
|
|
2657
|
+
nodeImage: installedImageSchema,
|
|
2532
2658
|
latest: z.string().nullable(),
|
|
2533
2659
|
latestError: registryUnavailableSchema.nullable(),
|
|
2534
2660
|
versions: z.array(z.string()),
|
|
@@ -2619,11 +2745,16 @@ const agentUsageSchema = z.object({
|
|
|
2619
2745
|
blockReadBytes: byteCountSchema,
|
|
2620
2746
|
blockWriteBytes: byteCountSchema
|
|
2621
2747
|
}).meta({ id: "AgentUsage" });
|
|
2748
|
+
const nodeHostSeriesSchema = z.object({
|
|
2749
|
+
nodeId: idSchema,
|
|
2750
|
+
nodeName: z.string(),
|
|
2751
|
+
points: z.array(hostPointSchema)
|
|
2752
|
+
}).meta({ id: "NodeHostSeries" });
|
|
2622
2753
|
z.object({
|
|
2623
2754
|
resolution: metricsResolutionSchema,
|
|
2624
2755
|
from: instantSchema,
|
|
2625
2756
|
to: instantSchema,
|
|
2626
|
-
|
|
2757
|
+
nodes: z.array(nodeHostSeriesSchema),
|
|
2627
2758
|
agents: z.array(agentUsageSchema)
|
|
2628
2759
|
}).meta({ id: "HostMetrics" });
|
|
2629
2760
|
z.object({
|
|
@@ -2712,7 +2843,11 @@ z.strictObject({ label: textParam }), z.strictObject({
|
|
|
2712
2843
|
}), z.strictObject({
|
|
2713
2844
|
agentName: textParam,
|
|
2714
2845
|
failureCount: countParam
|
|
2715
|
-
}), z.strictObject({ agentName: textParam }), z.strictObject({
|
|
2846
|
+
}), z.strictObject({ agentName: textParam }), z.strictObject({
|
|
2847
|
+
node: textParam,
|
|
2848
|
+
ratio: ratioParam
|
|
2849
|
+
}), z.strictObject({
|
|
2850
|
+
node: textParam,
|
|
2716
2851
|
filesystem: filesystemNameSchema,
|
|
2717
2852
|
ratio: ratioParam
|
|
2718
2853
|
}), z.strictObject({ backupKind: backupKindSchema }), z.strictObject({ hours: z.int().positive() }), z.strictObject({
|
|
@@ -3418,7 +3553,8 @@ const agentHints = {
|
|
|
3418
3553
|
"project.not_found": "kcloud projects list shows the projects you are a member of.",
|
|
3419
3554
|
"agent.invalid_transition": "kcloud agents show <agent> lists the actions it takes now.",
|
|
3420
3555
|
"agent.busy": "The agent is working on another action; try again in a moment.",
|
|
3421
|
-
"concurrency.conflict": "Someone changed the agent meanwhile; read it again and retry."
|
|
3556
|
+
"concurrency.conflict": "Someone changed the agent meanwhile; read it again and retry.",
|
|
3557
|
+
"node.offline": "The node of the agent is not connected; kcloud nodes show <node> tells when it was last seen."
|
|
3422
3558
|
};
|
|
3423
3559
|
const agentFrom = async (run, reference, project) => {
|
|
3424
3560
|
if (reference === void 0 || reference === "") throw new UsageError("This command needs an agent.", "Name it as <project>/<agent>, or pass --project.");
|
|
@@ -3497,6 +3633,7 @@ const agentPlanOf = (agent) => ({
|
|
|
3497
3633
|
slug: agent.slug,
|
|
3498
3634
|
description: agent.description,
|
|
3499
3635
|
kikimoraVersion: agent.kikimoraVersion,
|
|
3636
|
+
...agent.node === null ? {} : { nodeId: agent.node.id },
|
|
3500
3637
|
claudeCredentialId: agent.claudeCredential.id,
|
|
3501
3638
|
githubCredentialId: agent.githubCredential.id,
|
|
3502
3639
|
repositories: agent.repositories.map(({ repositoryId, directory }) => ({
|
|
@@ -3675,6 +3812,7 @@ const detailsOf = (agent) => [
|
|
|
3675
3812
|
["wanted", agent.desiredState],
|
|
3676
3813
|
["observed", agent.observedState],
|
|
3677
3814
|
["release", agent.kikimoraVersion],
|
|
3815
|
+
["node", agent.node === null ? "none" : `${agent.node.name} (${agent.node.state})`],
|
|
3678
3816
|
["timezone", agent.timezone],
|
|
3679
3817
|
["machine", `${agent.machine.cpus} vCPU, ${agent.machine.memoryMb} MB`],
|
|
3680
3818
|
["docker", agent.machine.docker ? "yes" : "no"],
|
|
@@ -3699,6 +3837,272 @@ const detailsOf = (agent) => [
|
|
|
3699
3837
|
]))
|
|
3700
3838
|
].join("\n");
|
|
3701
3839
|
//#endregion
|
|
3840
|
+
//#region src/commands/nodes.ts
|
|
3841
|
+
const nodeHints = {
|
|
3842
|
+
...organizationHints,
|
|
3843
|
+
"node.not_found": "kcloud nodes list shows the nodes of the organization.",
|
|
3844
|
+
"node.name_taken": "Another node of the organization has that name.",
|
|
3845
|
+
"node.in_use": "Delete the agents placed on it first; kcloud nodes show <node> lists them.",
|
|
3846
|
+
"node.registration_used": "The node has joined and holds its own credential; it needs no token."
|
|
3847
|
+
};
|
|
3848
|
+
const nodesOf = async (api, organizationId) => dataOf("list the nodes", await api.GET("/organizations/{organizationId}/nodes", { params: { path: { organizationId } } }), nodeHints).items;
|
|
3849
|
+
const namesOf = (nodes) => nodes.map(({ name }) => name).join(", ");
|
|
3850
|
+
const nodeIn = (nodes, reference, organization) => {
|
|
3851
|
+
const found = nodes.find((node) => node.id === reference.toLowerCase() || node.name === reference);
|
|
3852
|
+
if (found === void 0) throw new CliError(`There is no node ${reference} in ${organization}.`, nodes.length === 0 ? "kcloud nodes create <name> creates one." : `kcloud nodes list shows ${namesOf(nodes)}.`);
|
|
3853
|
+
return found;
|
|
3854
|
+
};
|
|
3855
|
+
const resolveNode = async (scope, reference) => {
|
|
3856
|
+
const organization = await scope.organization();
|
|
3857
|
+
if (isId(reference)) return {
|
|
3858
|
+
organizationId: organization.id,
|
|
3859
|
+
nodeId: reference
|
|
3860
|
+
};
|
|
3861
|
+
const node = nodeIn(await nodesOf(scope.api, organization.id), reference, organization.slug);
|
|
3862
|
+
return {
|
|
3863
|
+
organizationId: organization.id,
|
|
3864
|
+
nodeId: node.id
|
|
3865
|
+
};
|
|
3866
|
+
};
|
|
3867
|
+
const nodeDetailOf = async (api, { organizationId, nodeId }) => dataOf("describe the node", await api.GET("/organizations/{organizationId}/nodes/{nodeId}", { params: { path: {
|
|
3868
|
+
organizationId,
|
|
3869
|
+
nodeId
|
|
3870
|
+
} } }), nodeHints);
|
|
3871
|
+
const createdNode = async (api, organizationId, name) => {
|
|
3872
|
+
const body = { name };
|
|
3873
|
+
validated(createNodeBodySchema, body, "a node");
|
|
3874
|
+
return dataOf("create the node", await api.POST("/organizations/{organizationId}/nodes", {
|
|
3875
|
+
params: { path: { organizationId } },
|
|
3876
|
+
body
|
|
3877
|
+
}), nodeHints);
|
|
3878
|
+
};
|
|
3879
|
+
const renewedRegistration = async (api, { organizationId, nodeId }) => dataOf("issue a registration token for the node", await api.POST("/organizations/{organizationId}/nodes/{nodeId}/registration", { params: { path: {
|
|
3880
|
+
organizationId,
|
|
3881
|
+
nodeId
|
|
3882
|
+
} } }), nodeHints);
|
|
3883
|
+
const deleteNode = async (api, { organizationId, nodeId }) => {
|
|
3884
|
+
doneWith("delete the node", await api.DELETE("/organizations/{organizationId}/nodes/{nodeId}", { params: { path: {
|
|
3885
|
+
organizationId,
|
|
3886
|
+
nodeId
|
|
3887
|
+
} } }), nodeHints);
|
|
3888
|
+
};
|
|
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"}`;
|
|
3895
|
+
};
|
|
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;
|
|
3951
|
+
}
|
|
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(", ")}.`);
|
|
4104
|
+
};
|
|
4105
|
+
//#endregion
|
|
3702
4106
|
//#region src/commands/agents/create.ts
|
|
3703
4107
|
const claudeKinds = /* @__PURE__ */ new Set(["anthropic_api_key", "claude_oauth_token"]);
|
|
3704
4108
|
const catalogueOf = async (api, projectId) => {
|
|
@@ -3802,6 +4206,11 @@ const promptText = async (run, file, question) => {
|
|
|
3802
4206
|
};
|
|
3803
4207
|
const localTimezone = () => Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
3804
4208
|
const askedPlan = async (run, projectId, values) => {
|
|
4209
|
+
const nodeId = await placedNodeId(run, {
|
|
4210
|
+
projectId,
|
|
4211
|
+
given: values.node,
|
|
4212
|
+
ask: !run.settings.yes
|
|
4213
|
+
});
|
|
3805
4214
|
const catalogue = await catalogueOf(run.api, projectId);
|
|
3806
4215
|
const name = values.name ?? await run.prompt.text({
|
|
3807
4216
|
message: "The name of the agent",
|
|
@@ -3819,6 +4228,7 @@ const askedPlan = async (run, projectId, values) => {
|
|
|
3819
4228
|
name,
|
|
3820
4229
|
description: "",
|
|
3821
4230
|
kikimoraVersion: release,
|
|
4231
|
+
nodeId,
|
|
3822
4232
|
claudeCredentialId: await chosenCredential(run.prompt, catalogue, (kind) => claudeKinds.has(kind), "The Claude credential it works with"),
|
|
3823
4233
|
githubCredentialId: await chosenCredential(run.prompt, catalogue, (kind) => kind === "github_pat", "The GitHub credential it clones with"),
|
|
3824
4234
|
repositories: [...await chosenRepositories(run.prompt, catalogue)],
|
|
@@ -3837,17 +4247,27 @@ const askedPlan = async (run, projectId, values) => {
|
|
|
3837
4247
|
}
|
|
3838
4248
|
};
|
|
3839
4249
|
};
|
|
3840
|
-
const
|
|
4250
|
+
const isObject = (document) => typeof document === "object" && document !== null && !Array.isArray(document);
|
|
4251
|
+
const fileOf = async (run, projectId, { file, node }) => {
|
|
3841
4252
|
const document = await documentOf(run, file);
|
|
3842
|
-
|
|
3843
|
-
|
|
4253
|
+
const placed = !isObject(document) || node === void 0 && "nodeId" in document ? document : {
|
|
4254
|
+
...document,
|
|
4255
|
+
nodeId: await placedNodeId(run, {
|
|
4256
|
+
projectId,
|
|
4257
|
+
given: node,
|
|
4258
|
+
ask: false
|
|
4259
|
+
})
|
|
4260
|
+
};
|
|
4261
|
+
validated(createAgentBodySchema, placed, "an agent");
|
|
4262
|
+
return placed;
|
|
3844
4263
|
};
|
|
3845
4264
|
const createCommand = (adaptersOf) => defineCommand({
|
|
3846
4265
|
name: "create",
|
|
3847
4266
|
summary: "Create an agent from a file, from flags, or by asking step by step.",
|
|
3848
|
-
usage: "agents create --project <name> [--file <path>] [--name <name>]",
|
|
4267
|
+
usage: "agents create --project <name> [--node <node>] [--file <path>] [--name <name>]",
|
|
3849
4268
|
optionsUsage: [
|
|
3850
4269
|
["--project <name>", "the project that owns the agent"],
|
|
4270
|
+
["--node <node>", "the node it runs on, by name or id; needed when the organization has several"],
|
|
3851
4271
|
["--file <path>", "the whole agent as JSON, or - to read stdin"],
|
|
3852
4272
|
["--name <name>", "the name of the agent, instead of asking"],
|
|
3853
4273
|
["--release <x.y.z>", "the Kikimora release it runs, instead of asking"],
|
|
@@ -3856,11 +4276,13 @@ const createCommand = (adaptersOf) => defineCommand({
|
|
|
3856
4276
|
["--timezone <zone>", "the timezone of the agent (default: this machine's)"],
|
|
3857
4277
|
["--docker", "give the agent a Docker sidecar, when the project allows it"],
|
|
3858
4278
|
["--browser", "give the agent a browser"],
|
|
3859
|
-
["", "kcloud agents show <agent> --as-file prints what --file takes"]
|
|
4279
|
+
["", "kcloud agents show <agent> --as-file prints what --file takes"],
|
|
4280
|
+
["", "--node replaces the nodeId of the file"]
|
|
3860
4281
|
],
|
|
3861
4282
|
options: {
|
|
3862
4283
|
...fileOptions,
|
|
3863
4284
|
project: { type: "string" },
|
|
4285
|
+
node: { type: "string" },
|
|
3864
4286
|
name: { type: "string" },
|
|
3865
4287
|
release: { type: "string" },
|
|
3866
4288
|
"monitor-prompt-file": { type: "string" },
|
|
@@ -3884,7 +4306,10 @@ const createCommand = (adaptersOf) => defineCommand({
|
|
|
3884
4306
|
const run = await runOf(adaptersOf, invocation);
|
|
3885
4307
|
if (values.project === void 0) throw new UsageError("agents create needs --project <name>.", "kcloud projects list shows the projects you are a member of.");
|
|
3886
4308
|
const projectId = await resolveProjectId(run, values.project);
|
|
3887
|
-
const body = values.file === void 0 ? await askedPlan(run, projectId, values) : await fileOf(run,
|
|
4309
|
+
const body = values.file === void 0 ? await askedPlan(run, projectId, values) : await fileOf(run, projectId, {
|
|
4310
|
+
file: values.file,
|
|
4311
|
+
node: values.node
|
|
4312
|
+
});
|
|
3888
4313
|
const agent = dataOf("create the agent", await run.api.POST("/projects/{id}/agents", {
|
|
3889
4314
|
params: { path: { id: projectId } },
|
|
3890
4315
|
body
|
|
@@ -5204,7 +5629,7 @@ const agentTools = [
|
|
|
5204
5629
|
defineTool({
|
|
5205
5630
|
name: "agent_create",
|
|
5206
5631
|
title: "Create an agent",
|
|
5207
|
-
description: "Create an agent in a project. The credentials, repositories and tools are named by the ids that credentials_list, repositories_list and tools_list return; agent_show with asPlan shows a complete example. The agent stays stopped until agent_action starts it.",
|
|
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.",
|
|
5208
5633
|
operations: ["createAgent"],
|
|
5209
5634
|
safety: "write",
|
|
5210
5635
|
input: z.object({
|
|
@@ -5369,6 +5794,42 @@ const credentialTools = [
|
|
|
5369
5794
|
})
|
|
5370
5795
|
];
|
|
5371
5796
|
//#endregion
|
|
5797
|
+
//#region src/mcp/tools/nodes.ts
|
|
5798
|
+
const nodeField = z.string().trim().min(1).describe("The node, by its name or its id, from nodes_list.");
|
|
5799
|
+
const nodeTools = [
|
|
5800
|
+
defineTool({
|
|
5801
|
+
name: "nodes_list",
|
|
5802
|
+
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.",
|
|
5804
|
+
operations: ["listNodes"],
|
|
5805
|
+
safety: "read",
|
|
5806
|
+
input: z.object({}),
|
|
5807
|
+
run: async (_, run) => nodesOf(run.api, (await run.organization()).id)
|
|
5808
|
+
}),
|
|
5809
|
+
defineTool({
|
|
5810
|
+
name: "node_show",
|
|
5811
|
+
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.",
|
|
5813
|
+
operations: ["getNode"],
|
|
5814
|
+
safety: "read",
|
|
5815
|
+
input: z.object({ node: nodeField }),
|
|
5816
|
+
run: async ({ node }, run) => nodeDetailOf(run.api, await resolveNode(run, node))
|
|
5817
|
+
}),
|
|
5818
|
+
defineTool({
|
|
5819
|
+
name: "node_delete",
|
|
5820
|
+
title: "Delete a node",
|
|
5821
|
+
description: "Delete a node, after which its host can no longer reach the platform. The instance refuses while agents are placed on it.",
|
|
5822
|
+
operations: ["deleteNode"],
|
|
5823
|
+
safety: "destructive",
|
|
5824
|
+
input: z.object({ node: nodeField }),
|
|
5825
|
+
run: async ({ node }, run) => {
|
|
5826
|
+
const reference = await resolveNode(run, node);
|
|
5827
|
+
await deleteNode(run.api, reference);
|
|
5828
|
+
return { deleted: reference.nodeId };
|
|
5829
|
+
}
|
|
5830
|
+
})
|
|
5831
|
+
];
|
|
5832
|
+
//#endregion
|
|
5372
5833
|
//#region src/commands/notifications.ts
|
|
5373
5834
|
const notificationHints = { "notification.not_found": "kcloud notifications list shows them." };
|
|
5374
5835
|
const createNotificationsCommand = (adaptersOf) => defineGroup({
|
|
@@ -6583,18 +7044,6 @@ const projectToolTools = [
|
|
|
6583
7044
|
})
|
|
6584
7045
|
];
|
|
6585
7046
|
//#endregion
|
|
6586
|
-
//#region src/commands/projects/shared.ts
|
|
6587
|
-
const defaultProjectColor = "#7c86ff";
|
|
6588
|
-
const projectHint = {
|
|
6589
|
-
"project.not_found": "kcloud projects list shows the projects you are a member of.",
|
|
6590
|
-
"project.slug_taken": "Another project of the organization has that slug.",
|
|
6591
|
-
"project.invalid_slug": "A slug holds lowercase letters, digits and hyphens.",
|
|
6592
|
-
"project.not_empty": "Delete its agents, credentials, repositories and tools first.",
|
|
6593
|
-
"project.member_not_found": "kcloud projects members list <project> shows them.",
|
|
6594
|
-
"project.member_must_be_active": "kcloud orgs members enable <member> enables the account first."
|
|
6595
|
-
};
|
|
6596
|
-
const projectOf = async (api, id) => dataOf("describe the project", await api.GET("/projects/{id}", { params: { path: { id } } }), projectHint);
|
|
6597
|
-
//#endregion
|
|
6598
7047
|
//#region src/commands/projects/members.ts
|
|
6599
7048
|
const projectMembersOf = async (api, projectId) => dataOf("list the members of the project", await api.GET("/projects/{id}/members", { params: { path: { id: projectId } } }), projectHint).items;
|
|
6600
7049
|
const resolveProjectMember = async (api, projectId, reference) => {
|
|
@@ -7400,6 +7849,7 @@ const kcloudTools = [
|
|
|
7400
7849
|
...credentialTools,
|
|
7401
7850
|
...repositoryTools,
|
|
7402
7851
|
...projectToolTools,
|
|
7852
|
+
...nodeTools,
|
|
7403
7853
|
...agentTools,
|
|
7404
7854
|
...taskTools,
|
|
7405
7855
|
...observabilityTools,
|
|
@@ -7407,6 +7857,7 @@ const kcloudTools = [
|
|
|
7407
7857
|
...releaseTools
|
|
7408
7858
|
];
|
|
7409
7859
|
const passwordPrinted = (what) => `a secret never passes through the context of a model, and kcloud prints ${what}`;
|
|
7860
|
+
passwordPrinted("the one-time registration token of the node");
|
|
7410
7861
|
passwordPrinted("the temporary password of the new member"), passwordPrinted("the new temporary password");
|
|
7411
7862
|
//#endregion
|
|
7412
7863
|
//#region src/mcp/server.ts
|
|
@@ -7523,6 +7974,7 @@ const kcloudCommands = () => [
|
|
|
7523
7974
|
createCredentialsCommand(createAdapters),
|
|
7524
7975
|
createRepositoriesCommand(createAdapters),
|
|
7525
7976
|
createToolsCommand(createAdapters),
|
|
7977
|
+
createNodesCommand(createAdapters),
|
|
7526
7978
|
createAgentsCommand(createAdapters),
|
|
7527
7979
|
createNotificationsCommand(createAdapters),
|
|
7528
7980
|
createReleasesCommand(createAdapters),
|