@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.
Files changed (2) hide show
  1. package/dist/main.js +477 -25
  2. 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: "Node" });
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
- host: z.array(hostPointSchema),
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({ ratio: ratioParam }), 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 fileOf = async (run, file) => {
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
- validated(createAgentBodySchema, document, "an agent");
3843
- return document;
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, values.file);
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),
package/package.json CHANGED
@@ -15,7 +15,7 @@
15
15
  "publishConfig": {
16
16
  "access": "public"
17
17
  },
18
- "version": "0.14.0",
18
+ "version": "0.15.0",
19
19
  "dependencies": {
20
20
  "@clack/prompts": "1.8.1",
21
21
  "@modelcontextprotocol/server": "2.1.0",