@kikimoradev/kcloud 0.15.0 → 0.17.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 +1649 -755
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -13,6 +13,24 @@ import createClient from "openapi-fetch";
13
13
  import { finished } from "node:stream/promises";
14
14
  import { StdioServerTransport, serveStdio } from "@modelcontextprotocol/server/stdio";
15
15
  import { McpServer } from "@modelcontextprotocol/server";
16
+ //#region ../../packages/cli-kit/src/bytes.ts
17
+ const units = [
18
+ "B",
19
+ "KiB",
20
+ "MiB",
21
+ "GiB",
22
+ "TiB"
23
+ ];
24
+ const bytesOf = (bytes) => {
25
+ let value = bytes;
26
+ let unit = 0;
27
+ while (value >= 1024 && unit < units.length - 1) {
28
+ value /= 1024;
29
+ unit += 1;
30
+ }
31
+ return unit === 0 ? `${bytes} B` : `${value.toFixed(1)} ${units[unit] ?? ""}`;
32
+ };
33
+ //#endregion
16
34
  //#region ../../packages/cli-kit/src/errors.ts
17
35
  const exitCodes = {
18
36
  ok: 0,
@@ -91,12 +109,13 @@ const commandFactoryOf = (spec) => (definition) => {
91
109
  }
92
110
  };
93
111
  };
94
- const defineGroup$1 = ({ name, summary, usage, subcommands }) => ({
112
+ const defineGroup$1 = ({ name, summary, usage, subcommands, fallback }) => ({
95
113
  name,
96
114
  summary,
97
115
  usage,
98
116
  optionsUsage: [],
99
117
  subcommands,
118
+ ...fallback === void 0 ? {} : { fallback },
100
119
  execute: () => {
101
120
  throw new Error(`${name} is a group of commands and runs none of its own`);
102
121
  }
@@ -334,23 +353,27 @@ const report = (error, terminal, environment, program, debugVariable) => {
334
353
  };
335
354
  const dispatcherOf = (spec) => {
336
355
  const rendering = spec;
337
- const walk = async (commands, args, trail, context) => {
356
+ const executed = (command, args, help, context) => {
357
+ if (!help) return command.execute(args, context);
358
+ context.terminal.line(commandUsageOf(rendering, command));
359
+ return Promise.resolve(exitCodes.ok);
360
+ };
361
+ const walk = async (commands, args, trail, context, fallback) => {
338
362
  const located = locateCommand(args, spec.globalOptions);
363
+ const command = located.name === null ? void 0 : commands.find(({ name }) => name === located.name);
364
+ const listsHelp = located.name === null && located.help;
365
+ if (command === void 0 && fallback !== void 0 && !listsHelp) return executed(fallback, args, located.help, context);
366
+ const listed = fallback === void 0 ? commands : [fallback, ...commands];
339
367
  if (located.name === null) {
340
368
  if (located.help) {
341
- context.terminal.line(commandsUsageOf(rendering, commands, trail));
369
+ context.terminal.line(commandsUsageOf(rendering, listed, trail));
342
370
  return exitCodes.ok;
343
371
  }
344
- throw new UsageError(trail.length === 0 ? "Name a command." : `${pathOf(trail)}needs a subcommand.`, commandsUsageOf(rendering, commands, trail));
345
- }
346
- const command = commands.find(({ name }) => name === located.name);
347
- if (command === void 0) throw new UsageError(`There is no command ${pathOf(trail)}${located.name}.`, commandsUsageOf(rendering, commands, trail));
348
- if (command.subcommands.length > 0) return walk(command.subcommands, located.args, [...trail, command.name], context);
349
- if (located.help) {
350
- context.terminal.line(commandUsageOf(rendering, command));
351
- return exitCodes.ok;
372
+ throw new UsageError(trail.length === 0 ? "Name a command." : `${pathOf(trail)}needs a subcommand.`, commandsUsageOf(rendering, listed, trail));
352
373
  }
353
- return command.execute(located.args, context);
374
+ if (command === void 0) throw new UsageError(`There is no command ${pathOf(trail)}${located.name}.`, commandsUsageOf(rendering, listed, trail));
375
+ if (command.subcommands.length > 0) return walk(command.subcommands, located.args, [...trail, command.name], context, command.fallback);
376
+ return executed(command, located.args, located.help, context);
354
377
  };
355
378
  return async ({ argv, commands, context }) => {
356
379
  try {
@@ -363,6 +386,12 @@ const dispatcherOf = (spec) => {
363
386
  }
364
387
  };
365
388
  };
389
+ //#endregion
390
+ //#region ../../packages/cli-kit/src/list.ts
391
+ const listOf = (items) => {
392
+ const last = items.at(-1);
393
+ return items.length < 2 || last === void 0 ? items.join("") : `${items.slice(0, -1).join(", ")} and ${last}`;
394
+ };
366
395
  const permissionsOutsideOwner = 63;
367
396
  const isMissing = (error) => error instanceof Error && error.code === "ENOENT";
368
397
  const ensurePrivateDirectory = async (path) => {
@@ -486,243 +515,6 @@ const spec = {
486
515
  const defineCommand = commandFactoryOf(spec);
487
516
  const defineGroup = (definition) => defineGroup$1(definition);
488
517
  const runCli = dispatcherOf(spec);
489
- const agentEventLevelSchema = z.enum([
490
- "info",
491
- "warn",
492
- "error"
493
- ]);
494
- const agentRoles = [
495
- "monitor",
496
- "executor",
497
- "summarizer"
498
- ];
499
- const agentRoleSchema = z.enum(agentRoles);
500
- const agentLogStreams = ["stdout", "stderr"];
501
- z.enum(agentLogStreams);
502
- const platformStream = "platform";
503
- const agentEventStreams = [...agentLogStreams, platformStream];
504
- const agentEventStreamSchema = z.enum(agentEventStreams);
505
- const authKindSchema = z.enum([
506
- "apiKey",
507
- "oauth",
508
- "claude.ai",
509
- "unknown"
510
- ]);
511
- const agentControlStateSchema = z.enum([
512
- "running",
513
- "pausing",
514
- "paused"
515
- ]);
516
- z.enum([
517
- "session",
518
- "diagnostic",
519
- "task"
520
- ]);
521
- const maxCostUsd = 1e3;
522
- const countSchema = z.int().min(0).max(2147483647);
523
- const costUsdSchema = z.number().min(0).max(maxCostUsd);
524
- const instantSchema$2 = z.iso.datetime();
525
- const workerEnvelope = (event) => ({
526
- ts: instantSchema$2,
527
- level: agentEventLevelSchema,
528
- event: z.literal(event)
529
- });
530
- const roleEnvelope = (event) => ({
531
- ...workerEnvelope(event),
532
- agent: agentRoleSchema.optional()
533
- });
534
- const agentEventSchema = z.discriminatedUnion("event", [
535
- z.looseObject({
536
- ...workerEnvelope("worker.started"),
537
- version: z.string(),
538
- claudeVersion: z.string().optional(),
539
- nodeVersion: z.string(),
540
- authKind: authKindSchema,
541
- pid: countSchema,
542
- projectDir: z.string(),
543
- paused: z.boolean().optional()
544
- }),
545
- z.looseObject({
546
- ...workerEnvelope("worker.stopped"),
547
- signal: z.string().optional(),
548
- drained: z.boolean().optional(),
549
- forced: z.boolean().optional()
550
- }),
551
- z.looseObject({
552
- ...workerEnvelope("worker.draining"),
553
- reason: z.string(),
554
- timeoutMs: countSchema.optional()
555
- }),
556
- z.looseObject({
557
- ...workerEnvelope("update.available"),
558
- from: z.string(),
559
- to: z.string(),
560
- installError: z.string().optional()
561
- }),
562
- z.looseObject({
563
- ...workerEnvelope("update.installed"),
564
- from: z.string(),
565
- to: z.string()
566
- }),
567
- z.looseObject({
568
- ...roleEnvelope("control.changed"),
569
- state: agentControlStateSchema
570
- }),
571
- z.looseObject({
572
- ...roleEnvelope("cycle.started"),
573
- cycle: countSchema
574
- }),
575
- z.looseObject({
576
- ...roleEnvelope("cycle.finished"),
577
- cycle: countSchema,
578
- ok: z.boolean(),
579
- durationMs: countSchema,
580
- costUsd: costUsdSchema.optional(),
581
- addedTasks: countSchema,
582
- skippedDuplicates: countSchema,
583
- sessionId: z.string().optional(),
584
- error: z.string().optional()
585
- }),
586
- z.looseObject({
587
- ...roleEnvelope("monitor.offHours"),
588
- resumeAt: instantSchema$2
589
- }),
590
- z.looseObject({
591
- ...roleEnvelope("monitor.limitWait"),
592
- resumeAt: instantSchema$2
593
- }),
594
- z.looseObject({
595
- ...roleEnvelope("monitor.sleeping"),
596
- nextCycleAt: instantSchema$2
597
- }),
598
- z.looseObject({
599
- ...roleEnvelope("monitor.authBlocked"),
600
- reason: z.string()
601
- }),
602
- z.looseObject({
603
- ...roleEnvelope("monitor.admissionWait"),
604
- ticket: z.string(),
605
- since: instantSchema$2,
606
- seq: countSchema
607
- }),
608
- z.looseObject({
609
- ...roleEnvelope("executor.admissionWait"),
610
- ticket: z.string(),
611
- since: instantSchema$2,
612
- seq: countSchema,
613
- taskId: z.string()
614
- }),
615
- z.looseObject({
616
- ...roleEnvelope("admission.granted"),
617
- ticket: z.string(),
618
- seq: countSchema
619
- }),
620
- z.looseObject({
621
- ...roleEnvelope("admission.released"),
622
- ticket: z.string(),
623
- seq: countSchema
624
- }),
625
- z.looseObject({
626
- ...roleEnvelope("admission.cancelled"),
627
- ticket: z.string(),
628
- seq: countSchema
629
- }),
630
- z.looseObject({
631
- ...roleEnvelope("task.started"),
632
- taskId: z.string(),
633
- title: z.string()
634
- }),
635
- z.looseObject({
636
- ...roleEnvelope("task.finished"),
637
- taskId: z.string(),
638
- title: z.string(),
639
- ok: z.boolean(),
640
- durationMs: countSchema,
641
- costUsd: costUsdSchema.optional(),
642
- numTurns: countSchema.optional(),
643
- willRetry: z.boolean().optional(),
644
- attempt: countSchema.optional(),
645
- maxAttempts: countSchema.optional(),
646
- sessionId: z.string().optional(),
647
- error: z.string().optional()
648
- }),
649
- z.looseObject({
650
- ...roleEnvelope("task.retryScheduled"),
651
- taskId: z.string(),
652
- resumeAt: instantSchema$2
653
- }),
654
- z.looseObject(roleEnvelope("executor.waiting")),
655
- z.looseObject({
656
- ...roleEnvelope("executor.limitWait"),
657
- resumeAt: instantSchema$2
658
- }),
659
- z.looseObject({
660
- ...roleEnvelope("executor.authBlocked"),
661
- reason: z.string()
662
- }),
663
- z.looseObject({
664
- ...roleEnvelope("summary.started"),
665
- taskId: z.string()
666
- }),
667
- z.looseObject({
668
- ...roleEnvelope("summary.finished"),
669
- taskId: z.string(),
670
- ok: z.boolean(),
671
- durationMs: countSchema,
672
- costUsd: costUsdSchema.optional(),
673
- sessionId: z.string().optional(),
674
- error: z.string().optional()
675
- }),
676
- z.looseObject({
677
- ...roleEnvelope("session.init"),
678
- model: z.string(),
679
- sessionId: z.string(),
680
- taskId: z.string().optional(),
681
- cycle: countSchema.optional()
682
- }),
683
- z.looseObject({
684
- ...roleEnvelope("session.stderr"),
685
- line: z.string()
686
- }),
687
- z.looseObject({
688
- ...roleEnvelope("session.procError"),
689
- message: z.string()
690
- }),
691
- z.looseObject({
692
- ...roleEnvelope("session.killed"),
693
- reason: z.string()
694
- }),
695
- z.looseObject({
696
- ...roleEnvelope("session.text"),
697
- text: z.string()
698
- }),
699
- z.looseObject({
700
- ...roleEnvelope("session.tool"),
701
- tool: z.string(),
702
- detail: z.string()
703
- }),
704
- z.looseObject({
705
- ...roleEnvelope("session.toolError"),
706
- output: z.string()
707
- })
708
- ]);
709
- z.looseObject({
710
- ts: instantSchema$2,
711
- level: agentEventLevelSchema,
712
- agent: agentRoleSchema.optional(),
713
- event: z.string().min(1).max(64)
714
- });
715
- agentEventSchema.options.map((option) => option.shape.event.value);
716
- ({
717
- rawLog: "log.raw",
718
- rejectedLog: "log.rejected",
719
- throttledLog: "log.throttled",
720
- stateChanged: "platform.stateChanged",
721
- crashLooping: "platform.crashLooping",
722
- provisioned: "platform.provisioned",
723
- sandboxStarted: "platform.sandboxStarted",
724
- sandboxStopped: "platform.sandboxStopped"
725
- }).rawLog;
726
518
  //#endregion
727
519
  //#region ../../packages/contracts/src/patterns.ts
728
520
  const slugPattern = /^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$/;
@@ -972,6 +764,7 @@ const errorCodes = Object.keys({
972
764
  "agent.tool_duplicate": "rule_violation",
973
765
  "agent.context_too_long": "rule_violation",
974
766
  "agent.docker_unavailable": "rule_violation",
767
+ "agent.node_unsuitable": "rule_violation",
975
768
  "agent.storage_exhausted": "rule_violation",
976
769
  "agent.invalid_transition": "rule_violation",
977
770
  "agent.busy": "conflict",
@@ -995,6 +788,13 @@ const errorCodes = Object.keys({
995
788
  "node.registration_invalid": "unauthenticated",
996
789
  "node.registration_used": "conflict",
997
790
  "node.in_use": "conflict",
791
+ "node.host_unknown": "rule_violation",
792
+ "node.split_exceeds_host": "rule_violation",
793
+ "node.package_unavailable": "not_found",
794
+ "agent.backup_not_found": "not_found",
795
+ "agent.backup_unavailable": "rule_violation",
796
+ "agent.backup_without_export": "rule_violation",
797
+ "agent.backup_in_progress": "conflict",
998
798
  "project.last_admin": "conflict",
999
799
  "agent.node_unassigned": "rule_violation",
1000
800
  "stream.too_many": "rate_limited",
@@ -1031,16 +831,253 @@ const errorBodySchema = z.object({
1031
831
  message: z.string()
1032
832
  })).optional()
1033
833
  }).meta({ id: "ErrorBody" });
1034
- const agentStateSchema = z.enum([
1035
- "created",
1036
- "provisioning",
1037
- "stopped",
1038
- "starting",
1039
- "running",
1040
- "pausing",
1041
- "paused",
1042
- "stopping",
1043
- "updating",
834
+ const agentEventLevelSchema = z.enum([
835
+ "info",
836
+ "warn",
837
+ "error"
838
+ ]);
839
+ const agentRoles = [
840
+ "monitor",
841
+ "executor",
842
+ "summarizer"
843
+ ];
844
+ const agentRoleSchema = z.enum(agentRoles);
845
+ const agentLogStreams = ["stdout", "stderr"];
846
+ z.enum(agentLogStreams);
847
+ const platformStream = "platform";
848
+ const agentEventStreams = [...agentLogStreams, platformStream];
849
+ const agentEventStreamSchema = z.enum(agentEventStreams);
850
+ const authKindSchema = z.enum([
851
+ "apiKey",
852
+ "oauth",
853
+ "claude.ai",
854
+ "unknown"
855
+ ]);
856
+ const agentControlStateSchema = z.enum([
857
+ "running",
858
+ "pausing",
859
+ "paused"
860
+ ]);
861
+ z.enum([
862
+ "session",
863
+ "diagnostic",
864
+ "task"
865
+ ]);
866
+ const maxCostUsd = 1e3;
867
+ const countSchema = z.int().min(0).max(2147483647);
868
+ const costUsdSchema = z.number().min(0).max(maxCostUsd);
869
+ const instantSchema$2 = z.iso.datetime();
870
+ const workerEnvelope = (event) => ({
871
+ ts: instantSchema$2,
872
+ level: agentEventLevelSchema,
873
+ event: z.literal(event)
874
+ });
875
+ const roleEnvelope = (event) => ({
876
+ ...workerEnvelope(event),
877
+ agent: agentRoleSchema.optional()
878
+ });
879
+ const agentEventSchema = z.discriminatedUnion("event", [
880
+ z.looseObject({
881
+ ...workerEnvelope("worker.started"),
882
+ version: z.string(),
883
+ claudeVersion: z.string().optional(),
884
+ nodeVersion: z.string(),
885
+ authKind: authKindSchema,
886
+ pid: countSchema,
887
+ projectDir: z.string(),
888
+ paused: z.boolean().optional()
889
+ }),
890
+ z.looseObject({
891
+ ...workerEnvelope("worker.stopped"),
892
+ signal: z.string().optional(),
893
+ drained: z.boolean().optional(),
894
+ forced: z.boolean().optional()
895
+ }),
896
+ z.looseObject({
897
+ ...workerEnvelope("worker.draining"),
898
+ reason: z.string(),
899
+ timeoutMs: countSchema.optional()
900
+ }),
901
+ z.looseObject({
902
+ ...workerEnvelope("update.available"),
903
+ from: z.string(),
904
+ to: z.string(),
905
+ installError: z.string().optional()
906
+ }),
907
+ z.looseObject({
908
+ ...workerEnvelope("update.installed"),
909
+ from: z.string(),
910
+ to: z.string()
911
+ }),
912
+ z.looseObject({
913
+ ...roleEnvelope("control.changed"),
914
+ state: agentControlStateSchema
915
+ }),
916
+ z.looseObject({
917
+ ...roleEnvelope("cycle.started"),
918
+ cycle: countSchema
919
+ }),
920
+ z.looseObject({
921
+ ...roleEnvelope("cycle.finished"),
922
+ cycle: countSchema,
923
+ ok: z.boolean(),
924
+ durationMs: countSchema,
925
+ costUsd: costUsdSchema.optional(),
926
+ addedTasks: countSchema,
927
+ skippedDuplicates: countSchema,
928
+ sessionId: z.string().optional(),
929
+ error: z.string().optional()
930
+ }),
931
+ z.looseObject({
932
+ ...roleEnvelope("monitor.offHours"),
933
+ resumeAt: instantSchema$2
934
+ }),
935
+ z.looseObject({
936
+ ...roleEnvelope("monitor.limitWait"),
937
+ resumeAt: instantSchema$2
938
+ }),
939
+ z.looseObject({
940
+ ...roleEnvelope("monitor.sleeping"),
941
+ nextCycleAt: instantSchema$2
942
+ }),
943
+ z.looseObject({
944
+ ...roleEnvelope("monitor.authBlocked"),
945
+ reason: z.string()
946
+ }),
947
+ z.looseObject({
948
+ ...roleEnvelope("monitor.admissionWait"),
949
+ ticket: z.string(),
950
+ since: instantSchema$2,
951
+ seq: countSchema
952
+ }),
953
+ z.looseObject({
954
+ ...roleEnvelope("executor.admissionWait"),
955
+ ticket: z.string(),
956
+ since: instantSchema$2,
957
+ seq: countSchema,
958
+ taskId: z.string()
959
+ }),
960
+ z.looseObject({
961
+ ...roleEnvelope("admission.granted"),
962
+ ticket: z.string(),
963
+ seq: countSchema
964
+ }),
965
+ z.looseObject({
966
+ ...roleEnvelope("admission.released"),
967
+ ticket: z.string(),
968
+ seq: countSchema
969
+ }),
970
+ z.looseObject({
971
+ ...roleEnvelope("admission.cancelled"),
972
+ ticket: z.string(),
973
+ seq: countSchema
974
+ }),
975
+ z.looseObject({
976
+ ...roleEnvelope("task.started"),
977
+ taskId: z.string(),
978
+ title: z.string()
979
+ }),
980
+ z.looseObject({
981
+ ...roleEnvelope("task.finished"),
982
+ taskId: z.string(),
983
+ title: z.string(),
984
+ ok: z.boolean(),
985
+ durationMs: countSchema,
986
+ costUsd: costUsdSchema.optional(),
987
+ numTurns: countSchema.optional(),
988
+ willRetry: z.boolean().optional(),
989
+ attempt: countSchema.optional(),
990
+ maxAttempts: countSchema.optional(),
991
+ sessionId: z.string().optional(),
992
+ error: z.string().optional()
993
+ }),
994
+ z.looseObject({
995
+ ...roleEnvelope("task.retryScheduled"),
996
+ taskId: z.string(),
997
+ resumeAt: instantSchema$2
998
+ }),
999
+ z.looseObject(roleEnvelope("executor.waiting")),
1000
+ z.looseObject({
1001
+ ...roleEnvelope("executor.limitWait"),
1002
+ resumeAt: instantSchema$2
1003
+ }),
1004
+ z.looseObject({
1005
+ ...roleEnvelope("executor.authBlocked"),
1006
+ reason: z.string()
1007
+ }),
1008
+ z.looseObject({
1009
+ ...roleEnvelope("summary.started"),
1010
+ taskId: z.string()
1011
+ }),
1012
+ z.looseObject({
1013
+ ...roleEnvelope("summary.finished"),
1014
+ taskId: z.string(),
1015
+ ok: z.boolean(),
1016
+ durationMs: countSchema,
1017
+ costUsd: costUsdSchema.optional(),
1018
+ sessionId: z.string().optional(),
1019
+ error: z.string().optional()
1020
+ }),
1021
+ z.looseObject({
1022
+ ...roleEnvelope("session.init"),
1023
+ model: z.string(),
1024
+ sessionId: z.string(),
1025
+ taskId: z.string().optional(),
1026
+ cycle: countSchema.optional()
1027
+ }),
1028
+ z.looseObject({
1029
+ ...roleEnvelope("session.stderr"),
1030
+ line: z.string()
1031
+ }),
1032
+ z.looseObject({
1033
+ ...roleEnvelope("session.procError"),
1034
+ message: z.string()
1035
+ }),
1036
+ z.looseObject({
1037
+ ...roleEnvelope("session.killed"),
1038
+ reason: z.string()
1039
+ }),
1040
+ z.looseObject({
1041
+ ...roleEnvelope("session.text"),
1042
+ text: z.string()
1043
+ }),
1044
+ z.looseObject({
1045
+ ...roleEnvelope("session.tool"),
1046
+ tool: z.string(),
1047
+ detail: z.string()
1048
+ }),
1049
+ z.looseObject({
1050
+ ...roleEnvelope("session.toolError"),
1051
+ output: z.string()
1052
+ })
1053
+ ]);
1054
+ z.looseObject({
1055
+ ts: instantSchema$2,
1056
+ level: agentEventLevelSchema,
1057
+ agent: agentRoleSchema.optional(),
1058
+ event: z.string().min(1).max(64)
1059
+ });
1060
+ agentEventSchema.options.map((option) => option.shape.event.value);
1061
+ ({
1062
+ rawLog: "log.raw",
1063
+ rejectedLog: "log.rejected",
1064
+ throttledLog: "log.throttled",
1065
+ stateChanged: "platform.stateChanged",
1066
+ crashLooping: "platform.crashLooping",
1067
+ provisioned: "platform.provisioned",
1068
+ sandboxStarted: "platform.sandboxStarted",
1069
+ sandboxStopped: "platform.sandboxStopped"
1070
+ }).rawLog;
1071
+ const agentStateSchema = z.enum([
1072
+ "created",
1073
+ "provisioning",
1074
+ "stopped",
1075
+ "starting",
1076
+ "running",
1077
+ "pausing",
1078
+ "paused",
1079
+ "stopping",
1080
+ "updating",
1044
1081
  "resetting",
1045
1082
  "deleting",
1046
1083
  "error"
@@ -1244,6 +1281,7 @@ const agentSessionSchema = z.object({
1244
1281
  endedAt: z.iso.datetime().nullable(),
1245
1282
  ok: z.boolean().nullable(),
1246
1283
  killedReason: z.string().nullable(),
1284
+ failureReason: z.string().nullable(),
1247
1285
  error: z.string().nullable(),
1248
1286
  costUsd: z.number().nullable(),
1249
1287
  durationMs: z.int().nullable(),
@@ -1402,23 +1440,123 @@ pageSchema(z.object({
1402
1440
  finishedAt: z.iso.datetime().nullable()
1403
1441
  }).meta({ id: "TaskCost" })).meta({ id: "TaskCostPage" });
1404
1442
  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({
1443
+ z.enum(["monitor", "executor"]);
1444
+ const sessionSlotCountSchema = z.int().min(1).max(64);
1445
+ const sessionQueueEntrySchema = z.object({
1446
+ agentId: idSchema,
1447
+ agentName: z.string().nullable(),
1448
+ projectId: idSchema.nullable(),
1449
+ projectName: z.string().nullable(),
1450
+ position: z.int().positive(),
1451
+ since: z.iso.datetime(),
1452
+ taskId: z.string().nullable()
1453
+ }).meta({ id: "SessionQueueEntry" });
1454
+ const sessionPoolSchema = z.object({
1455
+ slots: sessionSlotCountSchema.nullable(),
1456
+ effective: z.int().nonnegative(),
1457
+ inUse: z.int().nonnegative(),
1458
+ waiting: z.array(sessionQueueEntrySchema)
1459
+ }).meta({ id: "SessionPool" });
1460
+ z.object({
1461
+ version: z.int().nonnegative(),
1462
+ ready: z.boolean(),
1463
+ monitor: sessionPoolSchema,
1464
+ executor: sessionPoolSchema
1465
+ }).meta({ id: "SessionSlots" });
1466
+ z.object({
1467
+ version: z.int().nonnegative(),
1468
+ monitor: sessionSlotCountSchema.nullable(),
1469
+ executor: sessionSlotCountSchema.nullable()
1470
+ }).meta({ id: "UpdateSessionSlotsBody" });
1471
+ const agentAdmissionStateSchema = z.discriminatedUnion("state", [z.object({
1472
+ state: z.literal("waiting"),
1473
+ since: z.iso.datetime(),
1474
+ position: z.int().positive()
1475
+ }), z.object({ state: z.literal("holding") })]).meta({ id: "AgentAdmissionState" });
1476
+ const agentAdmissionSchema = z.object({
1477
+ monitor: agentAdmissionStateSchema.nullable(),
1478
+ executor: agentAdmissionStateSchema.nullable()
1479
+ }).meta({ id: "AgentAdmission" });
1480
+ const nodeStateSchema = z.enum([
1481
+ "pending",
1482
+ "online",
1483
+ "offline"
1484
+ ]);
1485
+ const nodeProviderSchema = z.enum(["self_hosted"]);
1486
+ const nodeNameSchema = z.string().trim().regex(slugPattern);
1487
+ const nodeHostSchema = z.object({
1413
1488
  cpus: z.number().nonnegative(),
1414
1489
  memoryBytes: z.number().nonnegative(),
1415
1490
  dataRoot: z.string(),
1416
1491
  serverVersion: z.string()
1417
1492
  }).meta({ id: "NodeHost" });
1493
+ const nodeCapabilityNames = [
1494
+ "linux",
1495
+ "windows",
1496
+ "docker",
1497
+ "isolation",
1498
+ "gpu",
1499
+ "desktop"
1500
+ ];
1501
+ const nodeCapabilitySchema = z.enum(nodeCapabilityNames).meta({ id: "NodeCapability" });
1418
1502
  const nodeCapabilitiesSchema = z.object({
1419
1503
  sandbox: z.boolean(),
1420
- uidIsolation: z.boolean()
1504
+ uidIsolation: z.boolean(),
1505
+ offered: z.array(nodeCapabilitySchema).optional()
1421
1506
  }).meta({ id: "NodeCapabilities" });
1507
+ const minPoolCpus = .1;
1508
+ const maxSplitCpus = 1024;
1509
+ const maxSplitMemoryMb = 16777216;
1510
+ const hundredths = (value) => Math.abs(Math.round(value * 100) - value * 100) < 1e-9;
1511
+ const splitCpusSchema = (min) => z.number().min(min).max(maxSplitCpus).refine(hundredths, { message: "expected at most two decimal places" });
1512
+ const splitMemorySchema = (min) => z.int().min(min).max(maxSplitMemoryMb);
1513
+ const nodeReserveSchema = z.object({
1514
+ cpus: splitCpusSchema(0),
1515
+ memoryMb: splitMemorySchema(0)
1516
+ }).meta({ id: "NodeReserve" });
1517
+ const nodePoolSplitSchema = z.object({
1518
+ cpus: splitCpusSchema(minPoolCpus),
1519
+ memoryMb: splitMemorySchema(256),
1520
+ slots: z.int().min(1).max(64)
1521
+ }).meta({ id: "NodePoolSplit" });
1522
+ const nodeSplitSchema = z.object({
1523
+ reserve: nodeReserveSchema,
1524
+ monitor: nodePoolSplitSchema,
1525
+ executor: nodePoolSplitSchema
1526
+ }).meta({ id: "NodeSplit" });
1527
+ const poolLimitSchema = z.object({
1528
+ enforced: z.boolean(),
1529
+ reason: z.enum([
1530
+ "release",
1531
+ "restart",
1532
+ "node"
1533
+ ]).nullable()
1534
+ }).meta({ id: "PoolLimit" });
1535
+ const poolEnforcementSchema = z.object({
1536
+ state: z.enum([
1537
+ "enforced",
1538
+ "unsplit",
1539
+ "unsupported"
1540
+ ]),
1541
+ reason: z.string().nullable()
1542
+ }).meta({ id: "PoolEnforcement" });
1543
+ const poolUsageSchema = z.object({
1544
+ memoryBytes: z.int().nonnegative(),
1545
+ memoryMaxBytes: z.int().nonnegative().nullable(),
1546
+ cpus: z.number().nonnegative().nullable(),
1547
+ memoryKills: z.int().nonnegative()
1548
+ }).meta({ id: "PoolUsage" });
1549
+ const nodePoolSchema = z.object({
1550
+ slots: z.int().nonnegative(),
1551
+ inUse: z.int().nonnegative(),
1552
+ waiting: z.array(sessionQueueEntrySchema),
1553
+ usage: poolUsageSchema.nullable()
1554
+ }).meta({ id: "NodePool" });
1555
+ const nodePoolsSchema = z.object({
1556
+ enforcement: poolEnforcementSchema,
1557
+ monitor: nodePoolSchema,
1558
+ executor: nodePoolSchema
1559
+ }).meta({ id: "NodePools" });
1422
1560
  const nodeSchema = z.object({
1423
1561
  id: idSchema,
1424
1562
  name: nodeNameSchema,
@@ -1428,19 +1566,46 @@ const nodeSchema = z.object({
1428
1566
  version: z.string().nullable(),
1429
1567
  host: nodeHostSchema.nullable(),
1430
1568
  capabilities: nodeCapabilitiesSchema.nullable(),
1569
+ offered: z.array(nodeCapabilitySchema),
1570
+ split: nodeSplitSchema.nullable(),
1571
+ revision: z.int().nonnegative(),
1431
1572
  agentCount: z.int().nonnegative(),
1432
1573
  connectedAt: z.iso.datetime().nullable(),
1433
1574
  lastSeenAt: z.iso.datetime().nullable(),
1575
+ offlineSince: z.iso.datetime().nullable(),
1434
1576
  registrationExpiresAt: z.iso.datetime().nullable(),
1435
1577
  createdAt: z.iso.datetime()
1436
1578
  }).meta({ id: "Node" });
1579
+ const nodeUpdateSchema = z.object({
1580
+ state: z.enum([
1581
+ "current",
1582
+ "waiting",
1583
+ "updating",
1584
+ "failed",
1585
+ "unmanaged"
1586
+ ]),
1587
+ target: z.string().nullable(),
1588
+ sessions: z.int().nonnegative(),
1589
+ since: z.iso.datetime().nullable(),
1590
+ last: z.object({
1591
+ outcome: z.enum([
1592
+ "updated",
1593
+ "rolledBack",
1594
+ "failed"
1595
+ ]),
1596
+ version: z.string().nullable(),
1597
+ reason: z.string().nullable(),
1598
+ at: z.iso.datetime()
1599
+ }).nullable()
1600
+ }).meta({ id: "NodeUpdate" });
1437
1601
  const nodeAgentSchema = z.object({
1438
1602
  id: idSchema,
1439
1603
  name: z.string(),
1440
1604
  slug: z.string(),
1441
1605
  projectId: idSchema,
1442
1606
  projectName: z.string(),
1443
- state: agentStateSchema
1607
+ state: agentStateSchema,
1608
+ poolLimit: poolLimitSchema.nullable()
1444
1609
  }).meta({ id: "NodeAgent" });
1445
1610
  const nodeRuntimeSchema = z.object({
1446
1611
  reportedAt: z.iso.datetime(),
@@ -1473,11 +1638,13 @@ const nodeRuntimeSchema = z.object({
1473
1638
  effective: z.int().nonnegative(),
1474
1639
  inUse: z.int().nonnegative()
1475
1640
  })
1476
- })
1641
+ }),
1642
+ pools: nodePoolsSchema
1477
1643
  }).meta({ id: "NodeRuntime" });
1478
1644
  nodeSchema.extend({
1479
1645
  agents: z.array(nodeAgentSchema),
1480
- runtime: nodeRuntimeSchema.nullable()
1646
+ runtime: nodeRuntimeSchema.nullable(),
1647
+ update: nodeUpdateSchema.nullable()
1481
1648
  }).meta({ id: "NodeDetail" });
1482
1649
  z.object({ items: z.array(nodeSchema) }).meta({ id: "NodeList" });
1483
1650
  z.object({ nodeId: idSchema });
@@ -1485,6 +1652,10 @@ z.object({
1485
1652
  organizationId: idSchema,
1486
1653
  nodeId: idSchema
1487
1654
  });
1655
+ z.object({
1656
+ revision: z.int().nonnegative(),
1657
+ split: nodeSplitSchema.nullable()
1658
+ }).meta({ id: "UpdateNodeSplitBody" });
1488
1659
  const createNodeBodySchema = z.object({ name: nodeNameSchema }).meta({ id: "CreateNodeBody" });
1489
1660
  const nodeRegistrationSchema = z.object({
1490
1661
  token: z.string(),
@@ -1505,6 +1676,21 @@ z.object({
1505
1676
  nodeId: idSchema,
1506
1677
  credential: z.string()
1507
1678
  }).meta({ id: "RegisteredNode" });
1679
+ const bytesPerMegabyte = 1048576;
1680
+ const inHundredths = (value) => Math.round(value * 100);
1681
+ const hostMemoryMbOf = (host) => Math.floor(host.memoryBytes / bytesPerMegabyte);
1682
+ const splitOverflowOf = (split, host) => {
1683
+ const cpus = inHundredths(split.reserve.cpus) + inHundredths(split.monitor.cpus) + inHundredths(split.executor.cpus) - inHundredths(host.cpus);
1684
+ const memoryMb = split.reserve.memoryMb + split.monitor.memoryMb + split.executor.memoryMb - hostMemoryMbOf(host);
1685
+ return {
1686
+ cpus: cpus / 100,
1687
+ memoryMb
1688
+ };
1689
+ };
1690
+ const splitFits = (split, host) => {
1691
+ const overflow = splitOverflowOf(split, host);
1692
+ return overflow.cpus <= 0 && overflow.memoryMb <= 0;
1693
+ };
1508
1694
  //#endregion
1509
1695
  //#region ../../packages/contracts/src/identity.ts
1510
1696
  const emailSchema = z.email().max(254);
@@ -1674,6 +1860,8 @@ const auditActionSchema = z.enum([
1674
1860
  "agent.updated",
1675
1861
  "agent.deleted",
1676
1862
  "agent.action",
1863
+ "agent.backup_requested",
1864
+ "agent.restored",
1677
1865
  "platform.settings_updated",
1678
1866
  "runtime.sessions_updated",
1679
1867
  "platform.upgrade_requested",
@@ -1692,6 +1880,7 @@ const auditActionSchema = z.enum([
1692
1880
  "node.registered",
1693
1881
  "node.registration_renewed",
1694
1882
  "node.deleted",
1883
+ "node.split_changed",
1695
1884
  "master_key.reencrypted",
1696
1885
  "dead_letter.retried",
1697
1886
  "dead_letter.discarded"
@@ -1785,43 +1974,6 @@ z.object({
1785
1974
  id: idSchema,
1786
1975
  userId: idSchema
1787
1976
  });
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
1977
  //#endregion
1826
1978
  //#region ../../packages/contracts/src/releases.ts
1827
1979
  const kikimoraVersionSchema = z.string().regex(versionTagPattern);
@@ -2215,7 +2367,8 @@ const agentRuntimeSchema = z.object({
2215
2367
  provision: agentProvisionSchema.nullable(),
2216
2368
  stopMode: agentStopModeSchema.nullable(),
2217
2369
  stopDeadlineAt: z.iso.datetime().nullable(),
2218
- admission: agentAdmissionSchema.nullable()
2370
+ admission: agentAdmissionSchema.nullable(),
2371
+ poolLimit: poolLimitSchema.nullable()
2219
2372
  }).meta({ id: "AgentRuntime" });
2220
2373
  const agentListItemSchema = z.object({
2221
2374
  id: idSchema,
@@ -2254,6 +2407,7 @@ agentListItemSchema.extend({
2254
2407
  release: releaseSummarySchema.nullable(),
2255
2408
  runtime: agentRuntimeSchema,
2256
2409
  node: agentNodeSchema.nullable(),
2410
+ requiredCapabilities: z.array(nodeCapabilitySchema),
2257
2411
  runtimeUid: z.int().positive().nullable(),
2258
2412
  sandbox: agentSandboxSchema.nullable(),
2259
2413
  createdAt: z.iso.datetime()
@@ -2271,12 +2425,15 @@ const distinctCredentials = {
2271
2425
  const hasUniqueTools = (assignments) => new Set(assignments.map((assignment) => assignment.toolId)).size === assignments.length;
2272
2426
  const agentRepositoryLinksSchema = z.array(agentRepositoryLinkSchema).min(1).max(32);
2273
2427
  const agentToolAssignmentsSchema = z.array(toolAssignmentSchema).max(32).refine(hasUniqueTools, { message: "expected every tool to be assigned once" });
2428
+ const hasUniqueCapabilities = (names) => new Set(names).size === names.length;
2429
+ const requiredCapabilitiesSchema = z.array(nodeCapabilitySchema).max(nodeCapabilityNames.length).refine(hasUniqueCapabilities, { message: "expected every capability once" });
2274
2430
  const createAgentBodySchema = z.object({
2275
2431
  name: agentNameSchema,
2276
2432
  slug: slugSchema.optional(),
2277
2433
  description: agentDescriptionSchema,
2278
2434
  kikimoraVersion: kikimoraVersionSchema,
2279
2435
  nodeId: idSchema,
2436
+ requiredCapabilities: requiredCapabilitiesSchema.optional(),
2280
2437
  claudeCredentialId: idSchema,
2281
2438
  githubCredentialId: idSchema,
2282
2439
  repositories: agentRepositoryLinksSchema,
@@ -2344,6 +2501,43 @@ z.object({
2344
2501
  z.object({ code: errorCodeSchema })
2345
2502
  ])
2346
2503
  }).meta({ id: "AgentEventFrame" });
2504
+ //#endregion
2505
+ //#region ../../packages/contracts/src/agent-backups.ts
2506
+ const agentBackupTriggers = ["nightly", "manual"];
2507
+ const agentBackupSchema = z.object({
2508
+ id: idSchema,
2509
+ agentId: idSchema,
2510
+ nodeId: idSchema,
2511
+ nodeName: z.string().nullable(),
2512
+ trigger: z.enum(agentBackupTriggers),
2513
+ kikimoraVersion: z.string(),
2514
+ entries: z.int().nonnegative(),
2515
+ sizeBytes: z.int().nonnegative(),
2516
+ createdAt: z.iso.datetime(),
2517
+ expiresAt: z.iso.datetime()
2518
+ }).meta({ id: "AgentBackup" });
2519
+ z.object({
2520
+ items: z.array(agentBackupSchema),
2521
+ retentionDays: z.int().positive()
2522
+ }).meta({ id: "AgentBackupList" });
2523
+ z.object({ state: z.literal("requested") }).meta({ id: "AgentBackupRequested" });
2524
+ z.object({
2525
+ id: idSchema,
2526
+ backupId: idSchema
2527
+ });
2528
+ z.object({
2529
+ name: agentNameSchema,
2530
+ slug: slugSchema.optional(),
2531
+ nodeId: idSchema
2532
+ }).meta({ id: "RestoreAgentBody" });
2533
+ z.object({ agentId: idSchema }).meta({ id: "RestoredAgent" });
2534
+ z.object({
2535
+ agentId: idSchema,
2536
+ trigger: z.enum(agentBackupTriggers),
2537
+ kikimoraVersion: z.string().min(1).max(64),
2538
+ entries: z.coerce.number().int().nonnegative()
2539
+ });
2540
+ z.object({ backupId: idSchema });
2347
2541
  [
2348
2542
  "application/vnd.oci.image.index.v1+json",
2349
2543
  "application/vnd.oci.image.manifest.v1+json",
@@ -3524,6 +3718,116 @@ const createWhoamiCommand = (adaptersOf) => defineCommand({
3524
3718
  }
3525
3719
  });
3526
3720
  //#endregion
3721
+ //#region src/render/body.ts
3722
+ const isTerminal$1 = (stream) => stream.isTTY === true;
3723
+ const pipedText$1 = async (stream) => {
3724
+ const chunks = [];
3725
+ for await (const chunk of stream) chunks.push(Buffer.from(chunk));
3726
+ return Buffer.concat(chunks).toString("utf8");
3727
+ };
3728
+ const fileOptions = { file: { type: "string" } };
3729
+ const fileUsage = (what) => [["--file <path>", `read ${what} from a JSON file, or from stdin with -`]];
3730
+ const documentOf = async (run, file) => {
3731
+ const text = file === "-" ? isTerminal$1(run.context.io.stdin) ? "" : await pipedText$1(run.context.io.stdin) : await readFileOrRefuse(resolve(run.context.workingDirectory, file));
3732
+ if (text.trim() === "") throw new UsageError(file === "-" ? "Nothing is piped on stdin." : `${file} is empty.`);
3733
+ try {
3734
+ return JSON.parse(text);
3735
+ } catch {
3736
+ throw new CliError(`${file === "-" ? "stdin" : file} is not JSON.`);
3737
+ }
3738
+ };
3739
+ const readFileOrRefuse = async (path) => {
3740
+ try {
3741
+ return await readFile(path, "utf8");
3742
+ } catch {
3743
+ throw new CliError(`${path} could not be read.`);
3744
+ }
3745
+ };
3746
+ const validated = (schema, document, what) => {
3747
+ const parsed = schema.safeParse(document);
3748
+ if (!parsed.success) throw new CliError(`That is not ${what}.`, z.prettifyError(parsed.error));
3749
+ };
3750
+ //#endregion
3751
+ //#region src/commands/nodes/shared.ts
3752
+ const nodeHints = {
3753
+ ...organizationHints,
3754
+ "node.not_found": "kcloud nodes list shows the nodes of the organization.",
3755
+ "node.name_taken": "Another node of the organization has that name.",
3756
+ "node.in_use": "Delete the agents placed on it first; kcloud nodes show <node> lists them.",
3757
+ "node.registration_used": "The node has joined and holds its own credential; it needs no token."
3758
+ };
3759
+ const splitHints = {
3760
+ ...nodeHints,
3761
+ "auth.forbidden": "Splitting a node takes an admin of its organization.",
3762
+ "node.host_unknown": "The node reports its CPUs and memory once its service runs; kcloud nodes show <node> shows its state.",
3763
+ "node.split_exceeds_host": "The split asks for more than the host has; kcloud nodes split <node> shows the host.",
3764
+ "concurrency.conflict": "Someone changed the node meanwhile; run the command again."
3765
+ };
3766
+ const nodesOf = async (api, organizationId) => dataOf("list the nodes", await api.GET("/organizations/{organizationId}/nodes", { params: { path: { organizationId } } }), nodeHints).items;
3767
+ const namesOf = (nodes) => nodes.map(({ name }) => name).join(", ");
3768
+ const nodeIn = (nodes, reference, organization) => {
3769
+ const found = nodes.find((node) => node.id === reference.toLowerCase() || node.name === reference);
3770
+ 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)}.`);
3771
+ return found;
3772
+ };
3773
+ const resolveNode = async (scope, reference) => {
3774
+ const organization = await scope.organization();
3775
+ if (isId(reference)) return {
3776
+ organizationId: organization.id,
3777
+ nodeId: reference
3778
+ };
3779
+ const node = nodeIn(await nodesOf(scope.api, organization.id), reference, organization.slug);
3780
+ return {
3781
+ organizationId: organization.id,
3782
+ nodeId: node.id
3783
+ };
3784
+ };
3785
+ const nodeDetailOf = async (api, { organizationId, nodeId }) => dataOf("describe the node", await api.GET("/organizations/{organizationId}/nodes/{nodeId}", { params: { path: {
3786
+ organizationId,
3787
+ nodeId
3788
+ } } }), nodeHints);
3789
+ const createdNode = async (api, organizationId, name) => {
3790
+ const body = { name };
3791
+ validated(createNodeBodySchema, body, "a node");
3792
+ return dataOf("create the node", await api.POST("/organizations/{organizationId}/nodes", {
3793
+ params: { path: { organizationId } },
3794
+ body
3795
+ }), nodeHints);
3796
+ };
3797
+ const renewedRegistration = async (api, { organizationId, nodeId }) => dataOf("issue a registration token for the node", await api.POST("/organizations/{organizationId}/nodes/{nodeId}/registration", { params: { path: {
3798
+ organizationId,
3799
+ nodeId
3800
+ } } }), nodeHints);
3801
+ const deleteNode = async (api, { organizationId, nodeId }) => {
3802
+ doneWith("delete the node", await api.DELETE("/organizations/{organizationId}/nodes/{nodeId}", { params: { path: {
3803
+ organizationId,
3804
+ nodeId
3805
+ } } }), nodeHints);
3806
+ };
3807
+ 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", {
3808
+ params: { path: {
3809
+ organizationId,
3810
+ nodeId
3811
+ } },
3812
+ body: {
3813
+ revision,
3814
+ split
3815
+ }
3816
+ }), splitHints);
3817
+ const nodeUsage = [["", "a node is named by its name or its id"]];
3818
+ //#endregion
3819
+ //#region src/commands/projects/shared.ts
3820
+ const defaultProjectColor = "#7c86ff";
3821
+ const projectHint = {
3822
+ "project.not_found": "kcloud projects list shows the projects you are a member of.",
3823
+ "project.slug_taken": "Another project of the organization has that slug.",
3824
+ "project.invalid_slug": "A slug holds lowercase letters, digits and hyphens.",
3825
+ "project.not_empty": "Delete its agents, credentials, repositories and tools first.",
3826
+ "project.member_not_found": "kcloud projects members list <project> shows them.",
3827
+ "project.member_must_be_active": "kcloud orgs members enable <member> enables the account first."
3828
+ };
3829
+ const projectOf = async (api, id) => dataOf("describe the project", await api.GET("/projects/{id}", { params: { path: { id } } }), projectHint);
3830
+ //#endregion
3527
3831
  //#region src/commands/run.ts
3528
3832
  const runOf = async (adaptersOf, { settings, context }) => {
3529
3833
  const adapters = adaptersOf(context);
@@ -3554,6 +3858,7 @@ const agentHints = {
3554
3858
  "agent.invalid_transition": "kcloud agents show <agent> lists the actions it takes now.",
3555
3859
  "agent.busy": "The agent is working on another action; try again in a moment.",
3556
3860
  "concurrency.conflict": "Someone changed the agent meanwhile; read it again and retry.",
3861
+ "agent.node_unsuitable": "The node lacks a capability the agent requires; kcloud nodes show <node> lists what it offers.",
3557
3862
  "node.offline": "The node of the agent is not connected; kcloud nodes show <node> tells when it was last seen."
3558
3863
  };
3559
3864
  const agentFrom = async (run, reference, project) => {
@@ -3561,62 +3866,187 @@ const agentFrom = async (run, reference, project) => {
3561
3866
  return resolveAgentId(run, reference, project);
3562
3867
  };
3563
3868
  //#endregion
3564
- //#region src/commands/agents/check.ts
3565
- const checkHints = {
3869
+ //#region src/commands/agents/backups.ts
3870
+ const backupsListHint = "kcloud agents backups list <agent> shows the backups of the agent and their ids.";
3871
+ const backupHints = {
3566
3872
  ...agentHints,
3567
- "runtime.control_unreachable": "The worker is not answering; kcloud agents show <agent> tells whether it runs.",
3568
- "runtime.monitor_paused": "kcloud agents resume <agent> lets the agent work again.",
3569
- "runtime.monitor_draining": "The agent stops after its current session; start it again to run a cycle.",
3570
- "runtime.monitor_limited": "The Claude usage limit holds; the monitor runs its next cycle once it resets.",
3571
- "runtime.monitor_check_unsupported": "Move the agent to Kikimora 0.10.0 or newer with kcloud agents set, then kcloud agents update."
3873
+ "agent.backup_unavailable": "Backups need the Spaces of the platform and a node that can make them. An administrator of the platform configures the Spaces.",
3874
+ "agent.backup_without_export": "The Kikimora release of the agent cannot export its memory. Move it to 0.12.0 or newer with kcloud agents set, then kcloud agents update.",
3875
+ "agent.backup_in_progress": "A backup of the agent is running already. kcloud agents backups list <agent> shows it once the node has sent it.",
3876
+ "node.offline": "The node of the agent is not connected, so it cannot make a backup. kcloud nodes show <node> tells when it was last seen."
3572
3877
  };
3573
- const checkCommand = (adaptersOf) => defineCommand({
3574
- name: "check",
3575
- summary: "Run a monitor cycle now instead of waiting for the interval.",
3576
- usage: "agents check <agent> [--project <name>]",
3577
- optionsUsage: agentUsage,
3578
- options: agentOption,
3579
- positionals: {
3580
- min: 1,
3581
- max: 1
3878
+ const restoreHints = {
3879
+ ...agentHints,
3880
+ "node.not_found": "kcloud nodes list shows the nodes of the organization.",
3881
+ "node.offline": "The node is not connected. kcloud nodes show <node> tells when it was last seen.",
3882
+ "agent.backup_not_found": `The backup does not exist or has expired. ${backupsListHint}`,
3883
+ "agent.slug_taken": "Another agent of the project has that slug. Pass another with --slug.",
3884
+ "agent.invalid_slug": "A slug holds lowercase letters, digits and hyphens."
3885
+ };
3886
+ const backupsOf = async (api, agentId) => dataOf("list the backups of the agent", await api.GET("/agents/{id}/backups", { params: { path: { id: agentId } } }), backupHints);
3887
+ const requestedBackup = async (api, agentId) => dataOf("back the agent up", await api.POST("/agents/{id}/backups", { params: { path: { id: agentId } } }), backupHints);
3888
+ const nodeIdFor = async (api, agentId, node) => {
3889
+ if (isId(node)) return node;
3890
+ const agent = dataOf("describe the agent", await api.GET("/agents/{id}", { params: { path: { id: agentId } } }), agentHints);
3891
+ const project = await projectOf(api, agent.projectId);
3892
+ const nodes = await nodesOf(api, project.organizationId);
3893
+ return nodeIn(nodes, node, `the organization of ${project.slug}`).id;
3894
+ };
3895
+ const restoredAgent = async (api, agentId, { backupId, name, node, slug }) => {
3896
+ const nodeId = await nodeIdFor(api, agentId, node);
3897
+ return dataOf("restore the agent from the backup", await api.POST("/agents/{id}/backups/{backupId}/restore", {
3898
+ params: { path: {
3899
+ id: agentId,
3900
+ backupId
3901
+ } },
3902
+ body: {
3903
+ name,
3904
+ nodeId,
3905
+ ...slug === void 0 ? {} : { slug }
3906
+ }
3907
+ }), restoreHints);
3908
+ };
3909
+ const keptFor = (days) => `A backup is kept for ${String(days)} ${days === 1 ? "day" : "days"}.`;
3910
+ const backupsTable = ({ items, retentionDays }) => items.length === 0 ? `The agent has no backup yet. ${keptFor(retentionDays)}` : [renderTable([
3911
+ { header: "CREATED" },
3912
+ { header: "TRIGGER" },
3913
+ { header: "NODE" },
3914
+ { header: "KIKIMORA" },
3915
+ {
3916
+ header: "ENTRIES",
3917
+ align: "right"
3582
3918
  },
3583
- run: async (invocation) => {
3584
- const run = await runOf(adaptersOf, invocation);
3585
- const agentId = await agentFrom(run, invocation.positionals[0], invocation.values.project);
3586
- const check = dataOf("check the monitor", await run.api.POST("/agents/{id}/monitor/check", { params: { path: { id: agentId } } }), checkHints);
3587
- return printed(run, check, (answer) => answer.state === "running" ? `Monitor cycle ${String(answer.cycle)} is already running.` : "Monitor cycle requested; it starts now, also outside the active hours.");
3919
+ {
3920
+ header: "SIZE",
3921
+ align: "right"
3922
+ },
3923
+ { header: "EXPIRES" },
3924
+ { header: "ID" }
3925
+ ], items.map((backup) => [
3926
+ backup.createdAt,
3927
+ backup.trigger,
3928
+ backup.nodeName ?? backup.nodeId,
3929
+ backup.kikimoraVersion,
3930
+ String(backup.entries),
3931
+ bytesOf(backup.sizeBytes),
3932
+ backup.expiresAt,
3933
+ backup.id
3934
+ ])), keptFor(retentionDays)].join("\n");
3935
+ const restoreRequestOf = ({ backup, name, node, slug }) => {
3936
+ if (backup === void 0 || name === void 0 || node === void 0 || node.trim() === "") {
3937
+ const missing = [
3938
+ ...backup === void 0 ? ["--backup <id>"] : [],
3939
+ ...name === void 0 ? ["--name <name>"] : [],
3940
+ ...node === void 0 || node.trim() === "" ? ["--node <node>"] : []
3941
+ ];
3942
+ throw new UsageError(`agents restore needs ${listOf(missing)}.`, backupsListHint);
3588
3943
  }
3589
- });
3590
- //#endregion
3591
- //#region src/render/body.ts
3592
- const isTerminal$1 = (stream) => stream.isTTY === true;
3593
- const pipedText$1 = async (stream) => {
3594
- const chunks = [];
3595
- for await (const chunk of stream) chunks.push(Buffer.from(chunk));
3596
- return Buffer.concat(chunks).toString("utf8");
3944
+ if (!isId(backup)) throw new UsageError(`--backup ${backup} is not the id of a backup.`, backupsListHint);
3945
+ const named = agentNameSchema.safeParse(name);
3946
+ if (!named.success) throw new UsageError(`--name needs from 1 to ${String(80)} characters.`);
3947
+ if (slug !== void 0 && !slugSchema.safeParse(slug).success) throw new UsageError(`--slug ${slug} is not a slug.`, "A slug holds lowercase letters, digits and hyphens, at most 40 of them.");
3948
+ return {
3949
+ backupId: backup,
3950
+ name: named.data,
3951
+ node: node.trim(),
3952
+ slug
3953
+ };
3597
3954
  };
3598
- const fileOptions = { file: { type: "string" } };
3599
- const fileUsage = (what) => [["--file <path>", `read ${what} from a JSON file, or from stdin with -`]];
3600
- const documentOf = async (run, file) => {
3601
- const text = file === "-" ? isTerminal$1(run.context.io.stdin) ? "" : await pipedText$1(run.context.io.stdin) : await readFileOrRefuse(resolve(run.context.workingDirectory, file));
3602
- if (text.trim() === "") throw new UsageError(file === "-" ? "Nothing is piped on stdin." : `${file} is empty.`);
3603
- try {
3604
- return JSON.parse(text);
3605
- } catch {
3606
- throw new CliError(`${file === "-" ? "stdin" : file} is not JSON.`);
3955
+ const backupsCommand = (adaptersOf) => defineGroup({
3956
+ name: "backups",
3957
+ summary: "The backups of the memory of an agent.",
3958
+ usage: "agents backups <command>",
3959
+ subcommands: [defineCommand({
3960
+ name: "list",
3961
+ summary: "List the backups of an agent, the newest first.",
3962
+ usage: "agents backups list <agent> [--project <name>]",
3963
+ optionsUsage: agentUsage,
3964
+ options: agentOption,
3965
+ positionals: {
3966
+ min: 1,
3967
+ max: 1
3968
+ },
3969
+ run: async (invocation) => {
3970
+ const run = await runOf(adaptersOf, invocation);
3971
+ const agentId = await agentFrom(run, invocation.positionals[0], invocation.values.project);
3972
+ return printed(run, await backupsOf(run.api, agentId), backupsTable);
3973
+ }
3974
+ }), defineCommand({
3975
+ name: "create",
3976
+ summary: "Ask the node of an agent for a backup now.",
3977
+ usage: "agents backups create <agent> [--project <name>]",
3978
+ optionsUsage: agentUsage,
3979
+ options: agentOption,
3980
+ positionals: {
3981
+ min: 1,
3982
+ max: 1
3983
+ },
3984
+ run: async (invocation) => {
3985
+ const run = await runOf(adaptersOf, invocation);
3986
+ const reference = invocation.positionals[0] ?? "";
3987
+ const agentId = await agentFrom(run, reference, invocation.values.project);
3988
+ return printed(run, await requestedBackup(run.api, agentId), () => `Backup of ${reference} requested. kcloud agents backups list ${reference} shows it once the node has sent it.`);
3989
+ }
3990
+ })]
3991
+ });
3992
+ const restoreCommand = (adaptersOf) => defineCommand({
3993
+ name: "restore",
3994
+ summary: "Create a new agent from a backup of an agent.",
3995
+ usage: "agents restore <agent> --backup <id> --name <name> --node <node> [--slug <slug>] [--project <name>]",
3996
+ optionsUsage: [
3997
+ ...agentUsage,
3998
+ ["--backup <id>", "the backup, by its id from kcloud agents backups list"],
3999
+ ["--name <name>", "the name of the new agent"],
4000
+ ["--node <node>", "the node the new agent runs on, by name or id"],
4001
+ ["--slug <slug>", "the slug of the new agent (default: one made from its name)"],
4002
+ ["", "the new agent joins the project of the agent with its configuration and the memory of the backup"]
4003
+ ],
4004
+ options: {
4005
+ ...agentOption,
4006
+ backup: { type: "string" },
4007
+ name: { type: "string" },
4008
+ node: { type: "string" },
4009
+ slug: { type: "string" }
4010
+ },
4011
+ positionals: {
4012
+ min: 1,
4013
+ max: 1
4014
+ },
4015
+ run: async (invocation) => {
4016
+ const request = restoreRequestOf(invocation.values);
4017
+ const run = await runOf(adaptersOf, invocation);
4018
+ const reference = invocation.positionals[0] ?? "";
4019
+ const agentId = await agentFrom(run, reference, invocation.values.project);
4020
+ return printed(run, await restoredAgent(run.api, agentId, request), (restored) => [`Restored ${request.name} as ${restored.agentId} from backup ${request.backupId} of ${reference}.`, `kcloud agents start ${restored.agentId} starts it once it is provisioned.`].join("\n"));
3607
4021
  }
4022
+ });
4023
+ //#endregion
4024
+ //#region src/commands/agents/check.ts
4025
+ const checkHints = {
4026
+ ...agentHints,
4027
+ "runtime.control_unreachable": "The worker is not answering; kcloud agents show <agent> tells whether it runs.",
4028
+ "runtime.monitor_paused": "kcloud agents resume <agent> lets the agent work again.",
4029
+ "runtime.monitor_draining": "The agent stops after its current session; start it again to run a cycle.",
4030
+ "runtime.monitor_limited": "The Claude usage limit holds; the monitor runs its next cycle once it resets.",
4031
+ "runtime.monitor_check_unsupported": "Move the agent to Kikimora 0.10.0 or newer with kcloud agents set, then kcloud agents update."
3608
4032
  };
3609
- const readFileOrRefuse = async (path) => {
3610
- try {
3611
- return await readFile(path, "utf8");
3612
- } catch {
3613
- throw new CliError(`${path} could not be read.`);
4033
+ const checkCommand = (adaptersOf) => defineCommand({
4034
+ name: "check",
4035
+ summary: "Run a monitor cycle now instead of waiting for the interval.",
4036
+ usage: "agents check <agent> [--project <name>]",
4037
+ optionsUsage: agentUsage,
4038
+ options: agentOption,
4039
+ positionals: {
4040
+ min: 1,
4041
+ max: 1
4042
+ },
4043
+ run: async (invocation) => {
4044
+ const run = await runOf(adaptersOf, invocation);
4045
+ const agentId = await agentFrom(run, invocation.positionals[0], invocation.values.project);
4046
+ const check = dataOf("check the monitor", await run.api.POST("/agents/{id}/monitor/check", { params: { path: { id: agentId } } }), checkHints);
4047
+ return printed(run, check, (answer) => answer.state === "running" ? `Monitor cycle ${String(answer.cycle)} is already running.` : "Monitor cycle requested; it starts now, also outside the active hours.");
3614
4048
  }
3615
- };
3616
- const validated = (schema, document, what) => {
3617
- const parsed = schema.safeParse(document);
3618
- if (!parsed.success) throw new CliError(`That is not ${what}.`, z.prettifyError(parsed.error));
3619
- };
4049
+ });
3620
4050
  //#endregion
3621
4051
  //#region src/render/confirm.ts
3622
4052
  const confirmed = async ({ prompt, settings, question }) => {
@@ -3627,6 +4057,86 @@ const confirmed = async ({ prompt, settings, question }) => {
3627
4057
  })) throw new CancelledError("Nothing was changed.");
3628
4058
  };
3629
4059
  //#endregion
4060
+ //#region src/commands/nodes/pools.ts
4061
+ const poolLimitReasons = {
4062
+ release: "its Kikimora release predates pool limits",
4063
+ restart: "it started before the limits applied; kcloud agents restart applies them",
4064
+ node: "its node holds no pool to a limit"
4065
+ };
4066
+ const poolLimitText = (limit) => {
4067
+ if (limit === null) return "-";
4068
+ if (limit.enforced) return "enforced";
4069
+ return limit.reason === null ? "none" : `none: ${poolLimitReasons[limit.reason]}`;
4070
+ };
4071
+ const sessionFailures = {
4072
+ poolMemoryLimit: "killed by the pool memory limit",
4073
+ launcherRefused: "refused by the session starter"
4074
+ };
4075
+ const sessionFailureText = (reason) => reason === null ? "-" : sessionFailures[reason] ?? reason;
4076
+ const unsupportedReasons = { node_outdated: "the node runs a version that reports no pools" };
4077
+ const enforcementText = ({ state, reason }) => {
4078
+ switch (state) {
4079
+ case "enforced": return "The node holds each pool to the CPU and memory of its split.";
4080
+ case "unsplit": return "The node has no split, so no pool has a hard limit on CPU or memory.";
4081
+ case "unsupported": return `The node holds no pool to a limit: ${reason === null ? "it cannot" : unsupportedReasons[reason] ?? reason}.`;
4082
+ }
4083
+ };
4084
+ const memoryOf = (pool) => {
4085
+ if (pool.usage === null) return "-";
4086
+ const { memoryBytes, memoryMaxBytes } = pool.usage;
4087
+ return memoryMaxBytes === null ? bytesOf(memoryBytes) : `${bytesOf(memoryBytes)} of ${bytesOf(memoryMaxBytes)}`;
4088
+ };
4089
+ const roles = ["monitor", "executor"];
4090
+ const queueOf = (pools) => {
4091
+ const waiting = roles.flatMap((role) => pools[role].waiting.map((entry) => [
4092
+ role,
4093
+ String(entry.position),
4094
+ entry.projectName ?? "-",
4095
+ entry.agentName ?? entry.agentId,
4096
+ entry.since,
4097
+ entry.taskId ?? "-"
4098
+ ]));
4099
+ return waiting.length === 0 ? "No session waits for a slot." : renderTable([
4100
+ { header: "WAITS FOR" },
4101
+ {
4102
+ header: "PLACE",
4103
+ align: "right"
4104
+ },
4105
+ { header: "PROJECT" },
4106
+ { header: "AGENT" },
4107
+ { header: "SINCE" },
4108
+ { header: "TASK" }
4109
+ ], waiting);
4110
+ };
4111
+ const poolsText = (pools) => [
4112
+ enforcementText(pools.enforcement),
4113
+ renderTable([
4114
+ { header: "POOL" },
4115
+ { header: "SLOTS IN USE" },
4116
+ {
4117
+ header: "CPU IN USE",
4118
+ align: "right"
4119
+ },
4120
+ { header: "MEMORY IN USE" },
4121
+ {
4122
+ header: "MEMORY KILLS",
4123
+ align: "right"
4124
+ }
4125
+ ], roles.map((role) => {
4126
+ const pool = pools[role];
4127
+ const cpus = pool.usage?.cpus ?? null;
4128
+ return [
4129
+ role,
4130
+ `${pool.inUse} of ${pool.slots}`,
4131
+ cpus === null ? "-" : cpus.toFixed(2),
4132
+ memoryOf(pool),
4133
+ pool.usage === null ? "-" : String(pool.usage.memoryKills)
4134
+ ];
4135
+ })),
4136
+ "",
4137
+ queueOf(pools)
4138
+ ].join("\n");
4139
+ //#endregion
3630
4140
  //#region src/commands/agents/plan.ts
3631
4141
  const agentPlanOf = (agent) => ({
3632
4142
  name: agent.name,
@@ -3634,6 +4144,7 @@ const agentPlanOf = (agent) => ({
3634
4144
  description: agent.description,
3635
4145
  kikimoraVersion: agent.kikimoraVersion,
3636
4146
  ...agent.node === null ? {} : { nodeId: agent.node.id },
4147
+ requiredCapabilities: agent.requiredCapabilities,
3637
4148
  claudeCredentialId: agent.claudeCredential.id,
3638
4149
  githubCredentialId: agent.githubCredential.id,
3639
4150
  repositories: agent.repositories.map(({ repositoryId, directory }) => ({
@@ -3757,328 +4268,88 @@ const configCommands = (adaptersOf) => [
3757
4268
  run: async (invocation) => {
3758
4269
  const run = await runOf(adaptersOf, invocation);
3759
4270
  const agentId = await agentFrom(run, invocation.positionals[0], invocation.values.project);
3760
- const file = invocation.values.file;
3761
- if (file === void 0) throw new UsageError("agents set needs --file <path>.", "The file holds the sections to change; kcloud agents show <agent> --json shows the current ones.");
3762
- const agent = await changedAgent(run.api, agentId, await documentOf(run, file));
3763
- return printed(run, agent, (changed) => changed.restartRequired ? `Changed ${changed.name}; it needs kcloud agents update to take the change.` : `Changed ${changed.name}.`);
3764
- }
3765
- }),
3766
- defineCommand({
3767
- name: "delete",
3768
- summary: "Delete an agent and its container.",
3769
- usage: "agents delete <agent> [--project <name>] [--now]",
3770
- optionsUsage: [...agentUsage, ["--now", "delete it at once instead of after its current session"]],
3771
- options: {
3772
- ...agentOption,
3773
- now: {
3774
- type: "boolean",
3775
- default: false
3776
- }
3777
- },
3778
- positionals: {
3779
- min: 1,
3780
- max: 1
3781
- },
3782
- run: async (invocation) => {
3783
- const run = await runOf(adaptersOf, invocation);
3784
- const reference = invocation.positionals[0] ?? "";
3785
- const agentId = await agentFrom(run, reference, invocation.values.project);
3786
- await confirmed({
3787
- prompt: run.prompt,
3788
- settings: run.settings,
3789
- question: `Delete ${reference}? Its workspace and its history go with it.`
3790
- });
3791
- doneWith("delete the agent", await run.api.DELETE("/agents/{id}", { params: {
3792
- path: { id: agentId },
3793
- ...invocation.values.now ? { query: { force: "true" } } : {}
3794
- } }), agentHints);
3795
- return said(run, invocation.values.now ? `Deleting ${reference} now.` : `${reference} is deleted after its current session.`);
3796
- }
3797
- })
3798
- ];
3799
- const slotOf = (state) => {
3800
- if (state === null) return "-";
3801
- return state.state === "holding" ? "holding a slot" : `place ${String(state.position)} in the queue since ${state.since}`;
3802
- };
3803
- const admissionRowsOf = (agent) => {
3804
- const { admission } = agent.runtime;
3805
- return admission === null ? [] : [["monitor slot", slotOf(admission.monitor)], ["executor slot", slotOf(admission.executor)]];
3806
- };
3807
- const detailsOf = (agent) => [
3808
- renderTable([{ header: "FIELD" }, { header: "VALUE" }], [
3809
- ["name", agent.name],
3810
- ["slug", agent.slug],
3811
- ["state", agent.state],
3812
- ["wanted", agent.desiredState],
3813
- ["observed", agent.observedState],
3814
- ["release", agent.kikimoraVersion],
3815
- ["node", agent.node === null ? "none" : `${agent.node.name} (${agent.node.state})`],
3816
- ["timezone", agent.timezone],
3817
- ["machine", `${agent.machine.cpus} vCPU, ${agent.machine.memoryMb} MB`],
3818
- ["docker", agent.machine.docker ? "yes" : "no"],
3819
- ["browser", agent.machine.browser ? "yes" : "no"],
3820
- ["container", agent.runtime.containerId ?? "none"],
3821
- ["health", agent.runtime.health],
3822
- ...admissionRowsOf(agent),
3823
- ["pending", agent.restartRequired ? agent.restartReasons.join(", ") : "-"],
3824
- ["actions", agent.availableActions.join(", ")],
3825
- ["version", String(agent.version)],
3826
- ["id", agent.id]
3827
- ]),
3828
- "",
3829
- agent.repositories.length === 0 ? "No repository is cloned into the workspace." : renderTable([
3830
- { header: "REPOSITORY" },
3831
- { header: "DIRECTORY" },
3832
- { header: "STATUS" }
3833
- ], agent.repositories.map((repository) => [
3834
- `${repository.owner}/${repository.name}`,
3835
- repository.directory,
3836
- repository.status
3837
- ]))
3838
- ].join("\n");
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}.`);
4271
+ const file = invocation.values.file;
4272
+ if (file === void 0) throw new UsageError("agents set needs --file <path>.", "The file holds the sections to change; kcloud agents show <agent> --json shows the current ones.");
4273
+ const agent = await changedAgent(run.api, agentId, await documentOf(run, file));
4274
+ return printed(run, agent, (changed) => changed.restartRequired ? `Changed ${changed.name}; it needs kcloud agents update to take the change.` : `Changed ${changed.name}.`);
4275
+ }
4276
+ }),
4277
+ defineCommand({
4278
+ name: "delete",
4279
+ summary: "Delete an agent and its container.",
4280
+ usage: "agents delete <agent> [--project <name>] [--now]",
4281
+ optionsUsage: [...agentUsage, ["--now", "delete it at once instead of after its current session"]],
4282
+ options: {
4283
+ ...agentOption,
4284
+ now: {
4285
+ type: "boolean",
4286
+ default: false
4066
4287
  }
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."
4288
+ },
4289
+ positionals: {
4290
+ min: 1,
4291
+ max: 1
4292
+ },
4293
+ run: async (invocation) => {
4294
+ const run = await runOf(adaptersOf, invocation);
4295
+ const reference = invocation.positionals[0] ?? "";
4296
+ const agentId = await agentFrom(run, reference, invocation.values.project);
4297
+ await confirmed({
4298
+ prompt: run.prompt,
4299
+ settings: run.settings,
4300
+ question: `Delete ${reference}? Its workspace and its history go with it.`
4301
+ });
4302
+ doneWith("delete the agent", await run.api.DELETE("/agents/{id}", { params: {
4303
+ path: { id: agentId },
4304
+ ...invocation.values.now ? { query: { force: "true" } } : {}
4305
+ } }), agentHints);
4306
+ return said(run, invocation.values.now ? `Deleting ${reference} now.` : `${reference} is deleted after its current session.`);
4307
+ }
4308
+ })
4309
+ ];
4310
+ const slotOf = (state) => {
4311
+ if (state === null) return "-";
4312
+ return state.state === "holding" ? "holding a slot" : `place ${String(state.position)} in the queue since ${state.since}`;
4080
4313
  };
4081
- const projectOf = async (api, id) => dataOf("describe the project", await api.GET("/projects/{id}", { params: { path: { id } } }), projectHint);
4314
+ const admissionRowsOf = (agent) => {
4315
+ const { admission } = agent.runtime;
4316
+ return admission === null ? [] : [["monitor slot", slotOf(admission.monitor)], ["executor slot", slotOf(admission.executor)]];
4317
+ };
4318
+ const poolLimitRowsOf = ({ runtime }) => runtime.poolLimit === null ? [] : [["pool limit", poolLimitText(runtime.poolLimit)]];
4319
+ const detailsOf = (agent) => [
4320
+ renderTable([{ header: "FIELD" }, { header: "VALUE" }], [
4321
+ ["name", agent.name],
4322
+ ["slug", agent.slug],
4323
+ ["state", agent.state],
4324
+ ["wanted", agent.desiredState],
4325
+ ["observed", agent.observedState],
4326
+ ["release", agent.kikimoraVersion],
4327
+ ["node", agent.node === null ? "none" : `${agent.node.name} (${agent.node.state})`],
4328
+ ["requires", agent.requiredCapabilities.length === 0 ? "nothing" : agent.requiredCapabilities.join(", ")],
4329
+ ["timezone", agent.timezone],
4330
+ ["machine", `${agent.machine.cpus} vCPU, ${agent.machine.memoryMb} MB`],
4331
+ ["docker", agent.machine.docker ? "yes" : "no"],
4332
+ ["browser", agent.machine.browser ? "yes" : "no"],
4333
+ ["container", agent.runtime.containerId ?? "none"],
4334
+ ["health", agent.runtime.health],
4335
+ ...admissionRowsOf(agent),
4336
+ ...poolLimitRowsOf(agent),
4337
+ ["pending", agent.restartRequired ? agent.restartReasons.join(", ") : "-"],
4338
+ ["actions", agent.availableActions.join(", ")],
4339
+ ["version", String(agent.version)],
4340
+ ["id", agent.id]
4341
+ ]),
4342
+ "",
4343
+ agent.repositories.length === 0 ? "No repository is cloned into the workspace." : renderTable([
4344
+ { header: "REPOSITORY" },
4345
+ { header: "DIRECTORY" },
4346
+ { header: "STATUS" }
4347
+ ], agent.repositories.map((repository) => [
4348
+ `${repository.owner}/${repository.name}`,
4349
+ repository.directory,
4350
+ repository.status
4351
+ ]))
4352
+ ].join("\n");
4082
4353
  //#endregion
4083
4354
  //#region src/commands/agents/placement.ts
4084
4355
  const placedNodeId = async (run, { projectId, given, ask }) => {
@@ -4204,8 +4475,16 @@ const promptText = async (run, file, question) => {
4204
4475
  validate: (value) => value.trim() === "" ? "cannot be empty" : void 0
4205
4476
  });
4206
4477
  };
4478
+ const isCapability = (name) => nodeCapabilityNames.some((capability) => capability === name);
4479
+ const requiredCapabilitiesOf = (given) => {
4480
+ const names = given.flatMap((value) => value.split(",")).map((name) => name.trim()).filter((name) => name !== "");
4481
+ const unknown = names.filter((name) => !isCapability(name));
4482
+ 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(", ")}.`);
4483
+ return [...new Set(names.filter(isCapability))];
4484
+ };
4485
+ const requirementOf = (required) => required === void 0 ? {} : { requiredCapabilities: [...required] };
4207
4486
  const localTimezone = () => Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
4208
- const askedPlan = async (run, projectId, values) => {
4487
+ const askedPlan = async (run, projectId, values, required) => {
4209
4488
  const nodeId = await placedNodeId(run, {
4210
4489
  projectId,
4211
4490
  given: values.node,
@@ -4229,6 +4508,7 @@ const askedPlan = async (run, projectId, values) => {
4229
4508
  description: "",
4230
4509
  kikimoraVersion: release,
4231
4510
  nodeId,
4511
+ ...requirementOf(required),
4232
4512
  claudeCredentialId: await chosenCredential(run.prompt, catalogue, (kind) => claudeKinds.has(kind), "The Claude credential it works with"),
4233
4513
  githubCredentialId: await chosenCredential(run.prompt, catalogue, (kind) => kind === "github_pat", "The GitHub credential it clones with"),
4234
4514
  repositories: [...await chosenRepositories(run.prompt, catalogue)],
@@ -4248,7 +4528,7 @@ const askedPlan = async (run, projectId, values) => {
4248
4528
  };
4249
4529
  };
4250
4530
  const isObject = (document) => typeof document === "object" && document !== null && !Array.isArray(document);
4251
- const fileOf = async (run, projectId, { file, node }) => {
4531
+ const fileOf = async (run, projectId, { file, node }, required) => {
4252
4532
  const document = await documentOf(run, file);
4253
4533
  const placed = !isObject(document) || node === void 0 && "nodeId" in document ? document : {
4254
4534
  ...document,
@@ -4258,16 +4538,21 @@ const fileOf = async (run, projectId, { file, node }) => {
4258
4538
  ask: false
4259
4539
  })
4260
4540
  };
4261
- validated(createAgentBodySchema, placed, "an agent");
4262
- return placed;
4541
+ const plan = isObject(placed) ? {
4542
+ ...placed,
4543
+ ...requirementOf(required)
4544
+ } : placed;
4545
+ validated(createAgentBodySchema, plan, "an agent");
4546
+ return plan;
4263
4547
  };
4264
4548
  const createCommand = (adaptersOf) => defineCommand({
4265
4549
  name: "create",
4266
4550
  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>]",
4551
+ usage: "agents create --project <name> [--node <node>] [--require <capability,...>] [--file <path>] [--name <name>]",
4268
4552
  optionsUsage: [
4269
4553
  ["--project <name>", "the project that owns the agent"],
4270
4554
  ["--node <node>", "the node it runs on, by name or id; needed when the organization has several"],
4555
+ ["--require <capability,...>", `what its node has to offer: ${nodeCapabilityNames.join(", ")}`],
4271
4556
  ["--file <path>", "the whole agent as JSON, or - to read stdin"],
4272
4557
  ["--name <name>", "the name of the agent, instead of asking"],
4273
4558
  ["--release <x.y.z>", "the Kikimora release it runs, instead of asking"],
@@ -4277,12 +4562,16 @@ const createCommand = (adaptersOf) => defineCommand({
4277
4562
  ["--docker", "give the agent a Docker sidecar, when the project allows it"],
4278
4563
  ["--browser", "give the agent a browser"],
4279
4564
  ["", "kcloud agents show <agent> --as-file prints what --file takes"],
4280
- ["", "--node replaces the nodeId of the file"]
4565
+ ["", "--node and --require replace the nodeId and requiredCapabilities of the file"]
4281
4566
  ],
4282
4567
  options: {
4283
4568
  ...fileOptions,
4284
4569
  project: { type: "string" },
4285
4570
  node: { type: "string" },
4571
+ require: {
4572
+ type: "string",
4573
+ multiple: true
4574
+ },
4286
4575
  name: { type: "string" },
4287
4576
  release: { type: "string" },
4288
4577
  "monitor-prompt-file": { type: "string" },
@@ -4303,13 +4592,14 @@ const createCommand = (adaptersOf) => defineCommand({
4303
4592
  },
4304
4593
  run: async (invocation) => {
4305
4594
  const { values } = invocation;
4595
+ const required = values.require === void 0 ? void 0 : requiredCapabilitiesOf(values.require);
4306
4596
  const run = await runOf(adaptersOf, invocation);
4307
4597
  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
4598
  const projectId = await resolveProjectId(run, values.project);
4309
- const body = values.file === void 0 ? await askedPlan(run, projectId, values) : await fileOf(run, projectId, {
4599
+ const body = values.file === void 0 ? await askedPlan(run, projectId, values, required) : await fileOf(run, projectId, {
4310
4600
  file: values.file,
4311
4601
  node: values.node
4312
- });
4602
+ }, required);
4313
4603
  const agent = dataOf("create the agent", await run.api.POST("/projects/{id}/agents", {
4314
4604
  params: { path: { id: projectId } },
4315
4605
  body
@@ -4488,6 +4778,7 @@ const observabilityCommands = (adaptersOf) => [
4488
4778
  { header: "ROLE" },
4489
4779
  { header: "MODEL" },
4490
4780
  { header: "OUTCOME" },
4781
+ { header: "FAILURE" },
4491
4782
  {
4492
4783
  header: "COST",
4493
4784
  align: "right"
@@ -4498,6 +4789,7 @@ const observabilityCommands = (adaptersOf) => [
4498
4789
  session.agentRole ?? "-",
4499
4790
  session.model ?? "-",
4500
4791
  session.ok === null ? "running" : session.ok ? "ok" : "failed",
4792
+ sessionFailureText(session.failureReason),
4501
4793
  session.costUsd === null ? "-" : session.costUsd.toFixed(2),
4502
4794
  session.sessionId
4503
4795
  ])));
@@ -5093,6 +5385,8 @@ const createAgentsCommand = (adaptersOf) => defineGroup({
5093
5385
  ...lifecycleCommands(adaptersOf),
5094
5386
  checkCommand(adaptersOf),
5095
5387
  tasksCommand(adaptersOf),
5388
+ backupsCommand(adaptersOf),
5389
+ restoreCommand(adaptersOf),
5096
5390
  ...observabilityCommands(adaptersOf),
5097
5391
  ...streamCommands(adaptersOf)
5098
5392
  ]
@@ -5629,7 +5923,7 @@ const agentTools = [
5629
5923
  defineTool({
5630
5924
  name: "agent_create",
5631
5925
  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.",
5926
+ 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
5927
  operations: ["createAgent"],
5634
5928
  safety: "write",
5635
5929
  input: z.object({
@@ -5680,48 +5974,475 @@ const agentTools = [
5680
5974
  }
5681
5975
  }),
5682
5976
  defineTool({
5683
- name: "agent_action",
5684
- title: "Start, pause, resume, restart or update an agent",
5685
- description: "start lets the agent work, pause holds it between sessions, resume lets a paused agent work again, restart restarts its container, update replaces the container to apply pending changes. The agent returns with the actions it takes next.",
5686
- operations: ["runAgentAction"],
5687
- safety: "write",
5688
- input: z.object({
5689
- ...agentFields,
5690
- action: z.enum([
5691
- "start",
5692
- "pause",
5693
- "resume",
5694
- "restart",
5695
- "update"
5696
- ])
5697
- }),
5698
- run: async ({ action, ...reference }, run) => actionOn(run, await agentIdOf(run, reference), action, false)
5977
+ name: "agent_action",
5978
+ title: "Start, pause, resume, restart or update an agent",
5979
+ description: "start lets the agent work, pause holds it between sessions, resume lets a paused agent work again, restart restarts its container, update replaces the container to apply pending changes. The agent returns with the actions it takes next.",
5980
+ operations: ["runAgentAction"],
5981
+ safety: "write",
5982
+ input: z.object({
5983
+ ...agentFields,
5984
+ action: z.enum([
5985
+ "start",
5986
+ "pause",
5987
+ "resume",
5988
+ "restart",
5989
+ "update"
5990
+ ])
5991
+ }),
5992
+ run: async ({ action, ...reference }, run) => actionOn(run, await agentIdOf(run, reference), action, false)
5993
+ }),
5994
+ defineTool({
5995
+ name: "agent_check",
5996
+ title: "Run a monitor cycle now",
5997
+ description: "Ask the monitor of an agent to look for work now instead of waiting for its interval, also outside its active hours. state requested means a cycle starts at once; running names the cycle already in progress. A paused, stopping or usage-limited monitor refuses with an error.",
5998
+ operations: ["checkAgentMonitor"],
5999
+ safety: "write",
6000
+ input: z.object(agentFields),
6001
+ run: async (reference, run) => dataOf("check the monitor", await run.api.POST("/agents/{id}/monitor/check", { params: { path: { id: await agentIdOf(run, reference) } } }), checkHints)
6002
+ }),
6003
+ defineTool({
6004
+ name: "agent_interrupt",
6005
+ title: "Stop, reset or cancel an agent",
6006
+ description: "stop stops the agent after its current session, or at once with now; reset throws its workspace away and provisions it again; cancel cancels what it is doing.",
6007
+ operations: ["runAgentAction"],
6008
+ safety: "destructive",
6009
+ input: z.object({
6010
+ ...agentFields,
6011
+ action: z.enum([
6012
+ "stop",
6013
+ "reset",
6014
+ "cancel"
6015
+ ]),
6016
+ now: z.boolean().default(false).describe("For stop: stop the container at once.")
6017
+ }),
6018
+ run: async ({ action, now, ...reference }, run) => actionOn(run, await agentIdOf(run, reference), action, action === "stop" && now)
6019
+ })
6020
+ ];
6021
+ //#endregion
6022
+ //#region src/commands/nodes/split.ts
6023
+ const splitFields = [
6024
+ "reserve.cpus",
6025
+ "reserve.memoryMb",
6026
+ "monitor.cpus",
6027
+ "monitor.memoryMb",
6028
+ "monitor.slots",
6029
+ "executor.cpus",
6030
+ "executor.memoryMb",
6031
+ "executor.slots"
6032
+ ];
6033
+ const valuesOf = (split) => ({
6034
+ "reserve.cpus": split.reserve.cpus,
6035
+ "reserve.memoryMb": split.reserve.memoryMb,
6036
+ "monitor.cpus": split.monitor.cpus,
6037
+ "monitor.memoryMb": split.monitor.memoryMb,
6038
+ "monitor.slots": split.monitor.slots,
6039
+ "executor.cpus": split.executor.cpus,
6040
+ "executor.memoryMb": split.executor.memoryMb,
6041
+ "executor.slots": split.executor.slots
6042
+ });
6043
+ const splitOf = (values) => ({
6044
+ reserve: {
6045
+ cpus: values["reserve.cpus"],
6046
+ memoryMb: values["reserve.memoryMb"]
6047
+ },
6048
+ monitor: {
6049
+ cpus: values["monitor.cpus"],
6050
+ memoryMb: values["monitor.memoryMb"],
6051
+ slots: values["monitor.slots"]
6052
+ },
6053
+ executor: {
6054
+ cpus: values["executor.cpus"],
6055
+ memoryMb: values["executor.memoryMb"],
6056
+ slots: values["executor.slots"]
6057
+ }
6058
+ });
6059
+ const isComplete = (values) => splitFields.every((field) => values[field] !== void 0);
6060
+ const changeOfParts = ({ reserve, monitor, executor }) => {
6061
+ const given = {
6062
+ "reserve.cpus": reserve?.cpus,
6063
+ "reserve.memoryMb": reserve?.memoryMb,
6064
+ "monitor.cpus": monitor?.cpus,
6065
+ "monitor.memoryMb": monitor?.memoryMb,
6066
+ "monitor.slots": monitor?.slots,
6067
+ "executor.cpus": executor?.cpus,
6068
+ "executor.memoryMb": executor?.memoryMb,
6069
+ "executor.slots": executor?.slots
6070
+ };
6071
+ return Object.fromEntries(Object.entries(given).filter((entry) => entry[1] !== void 0));
6072
+ };
6073
+ const cpusText = (cpus) => String(Math.round(cpus * 100) / 100);
6074
+ const hostText = (host) => `${cpusText(host.cpus)} CPU and ${hostMemoryMbOf(host)} MB`;
6075
+ const excessText = ({ cpus, memoryMb }) => [...cpus > 0 ? [`${cpusText(cpus)} CPU`] : [], ...memoryMb > 0 ? [`${memoryMb} MB`] : []].join(" and ");
6076
+ const fittingSplit = (node, change, nameOf) => {
6077
+ const merged = {
6078
+ ...node.split === null ? {} : valuesOf(node.split),
6079
+ ...change
6080
+ };
6081
+ if (!isComplete(merged)) {
6082
+ const missing = splitFields.filter((field) => merged[field] === void 0);
6083
+ 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.");
6084
+ }
6085
+ const split = splitOf(merged);
6086
+ validated(nodeSplitSchema, split, "a split of a node");
6087
+ 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.");
6088
+ 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.`);
6089
+ return split;
6090
+ };
6091
+ const splitViewOf = (node) => {
6092
+ const overflow = node.split === null || node.host === null ? null : splitOverflowOf(node.split, node.host);
6093
+ return {
6094
+ id: node.id,
6095
+ name: node.name,
6096
+ revision: node.revision,
6097
+ host: node.host === null ? null : {
6098
+ cpus: node.host.cpus,
6099
+ memoryMb: hostMemoryMbOf(node.host)
6100
+ },
6101
+ split: node.split,
6102
+ free: overflow === null ? null : {
6103
+ cpus: -overflow.cpus,
6104
+ memoryMb: -overflow.memoryMb
6105
+ }
6106
+ };
6107
+ };
6108
+ const resourceCells = ({ cpus, memoryMb }) => [cpusText(cpus), `${memoryMb} MB`];
6109
+ const oneSlotOf = ({ cpus, memoryMb, slots }) => `${cpusText(cpus / slots)} CPU, ${Math.floor(memoryMb / slots)} MB`;
6110
+ 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.`;
6111
+ const splitTableOf = (view, split) => {
6112
+ const poolRow = (role) => [
6113
+ role,
6114
+ ...resourceCells(split[role]),
6115
+ String(split[role].slots),
6116
+ oneSlotOf(split[role])
6117
+ ];
6118
+ const rows = [
6119
+ [
6120
+ "reserve",
6121
+ ...resourceCells(split.reserve),
6122
+ "-",
6123
+ "-"
6124
+ ],
6125
+ poolRow("monitor"),
6126
+ poolRow("executor"),
6127
+ ...view.free === null ? [] : [[
6128
+ "free",
6129
+ ...resourceCells(view.free),
6130
+ "-",
6131
+ "-"
6132
+ ]],
6133
+ ...view.host === null ? [] : [[
6134
+ "host",
6135
+ ...resourceCells(view.host),
6136
+ "-",
6137
+ "-"
6138
+ ]]
6139
+ ];
6140
+ const over = view.free === null ? "" : excessText({
6141
+ cpus: -view.free.cpus,
6142
+ memoryMb: -view.free.memoryMb
6143
+ });
6144
+ return [
6145
+ renderTable([
6146
+ { header: "PART" },
6147
+ {
6148
+ header: "CPU",
6149
+ align: "right"
6150
+ },
6151
+ {
6152
+ header: "MEMORY",
6153
+ align: "right"
6154
+ },
6155
+ {
6156
+ header: "SLOTS",
6157
+ align: "right"
6158
+ },
6159
+ { header: "ONE SLOT" }
6160
+ ], rows),
6161
+ ...view.host === null ? ["", hostLineOf(view)] : [],
6162
+ ...over === "" ? [] : ["", `The split asks for ${over} more than the host has.`]
6163
+ ].join("\n");
6164
+ };
6165
+ const splitText = (view) => view.split === null ? [
6166
+ `${view.name} has no split, so its slots are counted from its host and no pool has a hard limit on CPU or memory.`,
6167
+ hostLineOf(view),
6168
+ `kcloud nodes split set ${view.name} with every part splits it.`
6169
+ ].join("\n") : splitTableOf(view, view.split);
6170
+ const splitSummaryOf = (split) => {
6171
+ if (split === null) return "none, slots counted from the host without hard limits";
6172
+ const { reserve, monitor, executor } = split;
6173
+ const pool = (role, { cpus, memoryMb, slots }) => `${role} ${cpusText(cpus)} CPU, ${memoryMb} MB, ${slots} slots`;
6174
+ return [
6175
+ `reserve ${cpusText(reserve.cpus)} CPU, ${reserve.memoryMb} MB`,
6176
+ pool("monitor", monitor),
6177
+ pool("executor", executor)
6178
+ ].join("; ");
6179
+ };
6180
+ const cpusPattern = /^\d+(?:\.\d+)?$/u;
6181
+ const slotsPattern = /^\d+$/u;
6182
+ const memoryPattern = /^(\d+(?:\.\d+)?)\s*(m|mb|mib|g|gb|gib)?$/iu;
6183
+ const megabytesPerGigabyte = 1024;
6184
+ const memoryMbOf = (given) => {
6185
+ const match = memoryPattern.exec(given.trim());
6186
+ if (match === null) return null;
6187
+ const [, amount = "", unit = "m"] = match;
6188
+ const megabytes = Number(amount) * (unit.toLowerCase().startsWith("g") ? megabytesPerGigabyte : 1);
6189
+ return Number.isSafeInteger(megabytes) ? megabytes : null;
6190
+ };
6191
+ const readCpus = (flag, given) => {
6192
+ 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.");
6193
+ return Number(given);
6194
+ };
6195
+ const readMemory = (flag, given) => {
6196
+ const megabytes = memoryMbOf(given);
6197
+ 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.");
6198
+ return megabytes;
6199
+ };
6200
+ const readSlots = (flag, given) => {
6201
+ if (!slotsPattern.test(given)) throw new UsageError(`--${flag} ${given} is not a whole number of slots.`);
6202
+ return Number(given);
6203
+ };
6204
+ const splitOptions = {
6205
+ "reserve-cpus": { type: "string" },
6206
+ "reserve-memory": { type: "string" },
6207
+ "monitor-cpus": { type: "string" },
6208
+ "monitor-memory": { type: "string" },
6209
+ "monitor-slots": { type: "string" },
6210
+ "executor-cpus": { type: "string" },
6211
+ "executor-memory": { type: "string" },
6212
+ "executor-slots": { type: "string" }
6213
+ };
6214
+ const splitOptionOf = {
6215
+ "reserve.cpus": {
6216
+ flag: "reserve-cpus",
6217
+ read: readCpus
6218
+ },
6219
+ "reserve.memoryMb": {
6220
+ flag: "reserve-memory",
6221
+ read: readMemory
6222
+ },
6223
+ "monitor.cpus": {
6224
+ flag: "monitor-cpus",
6225
+ read: readCpus
6226
+ },
6227
+ "monitor.memoryMb": {
6228
+ flag: "monitor-memory",
6229
+ read: readMemory
6230
+ },
6231
+ "monitor.slots": {
6232
+ flag: "monitor-slots",
6233
+ read: readSlots
6234
+ },
6235
+ "executor.cpus": {
6236
+ flag: "executor-cpus",
6237
+ read: readCpus
6238
+ },
6239
+ "executor.memoryMb": {
6240
+ flag: "executor-memory",
6241
+ read: readMemory
6242
+ },
6243
+ "executor.slots": {
6244
+ flag: "executor-slots",
6245
+ read: readSlots
6246
+ }
6247
+ };
6248
+ const flagOf = (field) => `--${splitOptionOf[field].flag}`;
6249
+ const changeOf = (values) => Object.fromEntries(splitFields.flatMap((field) => {
6250
+ const { flag, read } = splitOptionOf[field];
6251
+ const given = values[flag];
6252
+ return given === void 0 ? [] : [[field, read(flag, given)]];
6253
+ }));
6254
+ const createSplitCommand = (adaptersOf) => defineGroup({
6255
+ name: "split",
6256
+ summary: "How a node is split into a reserve and two pools, and changing it.",
6257
+ usage: "nodes split [set|clear] <node>",
6258
+ fallback: defineCommand({
6259
+ name: "split",
6260
+ summary: "Show the split of a node, its host and what one slot of each pool gets.",
6261
+ usage: "nodes split <node>",
6262
+ optionsUsage: nodeUsage,
6263
+ options: {},
6264
+ positionals: {
6265
+ min: 1,
6266
+ max: 1
6267
+ },
6268
+ run: async (invocation) => {
6269
+ const run = await runOf(adaptersOf, invocation);
6270
+ const reference = await resolveNode(run, invocation.positionals[0] ?? "");
6271
+ return printed(run, splitViewOf(await nodeDetailOf(run.api, reference)), splitText);
6272
+ }
6273
+ }),
6274
+ subcommands: [defineCommand({
6275
+ name: "set",
6276
+ summary: "Split a node into a reserve and two pools, or change parts of its split.",
6277
+ usage: "nodes split set <node> [--<part>-cpus <n>] [--<part>-memory <size>] [--<pool>-slots <n>]",
6278
+ optionsUsage: [
6279
+ ...nodeUsage,
6280
+ ["--reserve-cpus <n>", "the CPUs kept for the host itself"],
6281
+ ["--reserve-memory <size>", "the memory kept for the host itself"],
6282
+ ["--monitor-cpus <n>", "the CPUs the monitor sessions share"],
6283
+ ["--monitor-memory <size>", "the memory the monitor sessions share"],
6284
+ ["--monitor-slots <n>", "how many monitor sessions run at once"],
6285
+ ["--executor-cpus <n>", "the CPUs the executor sessions share"],
6286
+ ["--executor-memory <size>", "the memory the executor sessions share"],
6287
+ ["--executor-slots <n>", "how many executor sessions run at once"],
6288
+ ["", "a size is megabytes, or a number with M or G, as in 2048 or 2G"],
6289
+ ["", "a part left out keeps its value; a node without a split needs all eight"]
6290
+ ],
6291
+ options: splitOptions,
6292
+ positionals: {
6293
+ min: 1,
6294
+ max: 1
6295
+ },
6296
+ run: async (invocation) => {
6297
+ const change = changeOf(invocation.values);
6298
+ 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.");
6299
+ const run = await runOf(adaptersOf, invocation);
6300
+ const reference = await resolveNode(run, invocation.positionals[0] ?? "");
6301
+ const node = await nodeDetailOf(run.api, reference);
6302
+ const split = fittingSplit(node, change, flagOf);
6303
+ const changed = await splitNode(run.api, reference, node.revision, split);
6304
+ return printed(run, splitViewOf(changed), splitText);
6305
+ }
6306
+ }), defineCommand({
6307
+ name: "clear",
6308
+ summary: "Remove the split of a node, so its slots are counted from its host.",
6309
+ usage: "nodes split clear <node>",
6310
+ optionsUsage: nodeUsage,
6311
+ options: {},
6312
+ positionals: {
6313
+ min: 1,
6314
+ max: 1
6315
+ },
6316
+ run: async (invocation) => {
6317
+ const run = await runOf(adaptersOf, invocation);
6318
+ const given = invocation.positionals[0] ?? "";
6319
+ const reference = await resolveNode(run, given);
6320
+ const node = await nodeDetailOf(run.api, reference);
6321
+ if (node.split === null) return printed(run, splitViewOf(node), () => `${node.name} has no split; nothing was changed.`);
6322
+ await confirmed({
6323
+ prompt: run.prompt,
6324
+ settings: run.settings,
6325
+ question: `Clear the split of ${given}? Its pools lose their limits on CPU and memory.`
6326
+ });
6327
+ const cleared = await splitNode(run.api, reference, node.revision, null);
6328
+ return printed(run, splitViewOf(cleared), splitText);
6329
+ }
6330
+ })]
6331
+ });
6332
+ //#endregion
6333
+ //#region src/mcp/tools/nodes.ts
6334
+ const nodeField = z.string().trim().min(1).describe("The node, by its name or its id, from nodes_list.");
6335
+ 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.`);
6336
+ const nodeTools = [
6337
+ defineTool({
6338
+ name: "nodes_list",
6339
+ title: "List nodes",
6340
+ 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.",
6341
+ operations: ["listNodes"],
6342
+ safety: "read",
6343
+ input: z.object({}),
6344
+ run: async (_, run) => nodesOf(run.api, (await run.organization()).id)
6345
+ }),
6346
+ defineTool({
6347
+ name: "node_show",
6348
+ title: "Show a node",
6349
+ 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.",
6350
+ operations: ["getNode"],
6351
+ safety: "read",
6352
+ input: z.object({ node: nodeField }),
6353
+ run: async ({ node }, run) => nodeDetailOf(run.api, await resolveNode(run, node))
6354
+ }),
6355
+ defineTool({
6356
+ name: "node_split_set",
6357
+ title: "Split a node into pools",
6358
+ 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.",
6359
+ operations: ["getNode", "updateNodeSplit"],
6360
+ safety: "write",
6361
+ idempotent: true,
6362
+ input: z.object({
6363
+ node: nodeField,
6364
+ reserve: nodeReserveSchema.partial().optional().describe("The CPUs and the memory in MB kept for the host itself; a field left out keeps its value."),
6365
+ monitor: poolPart("monitor"),
6366
+ executor: poolPart("executor")
6367
+ }),
6368
+ run: async ({ node, ...parts }, run) => {
6369
+ const change = changeOfParts(parts);
6370
+ if (Object.keys(change).length === 0) throw new CliError("Name at least one part of the split to change.");
6371
+ const reference = await resolveNode(run, node);
6372
+ const detail = await nodeDetailOf(run.api, reference);
6373
+ const split = fittingSplit(detail, change, (field) => field);
6374
+ return splitViewOf(await splitNode(run.api, reference, detail.revision, split));
6375
+ }
6376
+ }),
6377
+ defineTool({
6378
+ name: "node_split_clear",
6379
+ title: "Clear the split of a node",
6380
+ 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.",
6381
+ operations: ["getNode", "updateNodeSplit"],
6382
+ safety: "destructive",
6383
+ idempotent: true,
6384
+ input: z.object({ node: nodeField }),
6385
+ run: async ({ node }, run) => {
6386
+ const reference = await resolveNode(run, node);
6387
+ const detail = await nodeDetailOf(run.api, reference);
6388
+ if (detail.split === null) return splitViewOf(detail);
6389
+ return splitViewOf(await splitNode(run.api, reference, detail.revision, null));
6390
+ }
6391
+ }),
6392
+ defineTool({
6393
+ name: "node_delete",
6394
+ title: "Delete a node",
6395
+ description: "Delete a node, after which its host can no longer reach the platform. The instance refuses while agents are placed on it.",
6396
+ operations: ["deleteNode"],
6397
+ safety: "destructive",
6398
+ input: z.object({ node: nodeField }),
6399
+ run: async ({ node }, run) => {
6400
+ const reference = await resolveNode(run, node);
6401
+ await deleteNode(run.api, reference);
6402
+ return { deleted: reference.nodeId };
6403
+ }
6404
+ })
6405
+ ];
6406
+ //#endregion
6407
+ //#region src/mcp/tools/backups.ts
6408
+ const agentBackupTools = [
6409
+ defineTool({
6410
+ name: "agent_backups_list",
6411
+ title: "List the backups of an agent",
6412
+ description: "List the backups of the memory of an agent, the newest first, with their trigger, node, Kikimora release, entries, size and expiry, and the days a backup is kept.",
6413
+ operations: ["listAgentBackups"],
6414
+ safety: "read",
6415
+ input: z.object(agentFields),
6416
+ run: async (reference, run) => backupsOf(run.api, await agentIdOf(run, reference))
5699
6417
  }),
5700
6418
  defineTool({
5701
- name: "agent_check",
5702
- title: "Run a monitor cycle now",
5703
- description: "Ask the monitor of an agent to look for work now instead of waiting for its interval, also outside its active hours. state requested means a cycle starts at once; running names the cycle already in progress. A paused, stopping or usage-limited monitor refuses with an error.",
5704
- operations: ["checkAgentMonitor"],
6419
+ name: "agent_backup_request",
6420
+ title: "Back an agent up now",
6421
+ description: "Ask the node of an agent for a backup of its memory now. state requested means the node makes it, and agent_backups_list shows it once the node has sent it.",
6422
+ operations: ["requestAgentBackup"],
5705
6423
  safety: "write",
5706
6424
  input: z.object(agentFields),
5707
- run: async (reference, run) => dataOf("check the monitor", await run.api.POST("/agents/{id}/monitor/check", { params: { path: { id: await agentIdOf(run, reference) } } }), checkHints)
6425
+ run: async (reference, run) => requestedBackup(run.api, await agentIdOf(run, reference))
5708
6426
  }),
5709
6427
  defineTool({
5710
- name: "agent_interrupt",
5711
- title: "Stop, reset or cancel an agent",
5712
- description: "stop stops the agent after its current session, or at once with now; reset throws its workspace away and provisions it again; cancel cancels what it is doing.",
5713
- operations: ["runAgentAction"],
5714
- safety: "destructive",
6428
+ name: "agent_restore",
6429
+ title: "Restore an agent from a backup",
6430
+ description: "Create a new agent in the project of an agent, with its configuration and the memory of one of its backups, on the node named. Returns the id of the new agent, which stays stopped until agent_action starts it.",
6431
+ operations: ["restoreAgentFromBackup"],
6432
+ safety: "write",
5715
6433
  input: z.object({
5716
6434
  ...agentFields,
5717
- action: z.enum([
5718
- "stop",
5719
- "reset",
5720
- "cancel"
5721
- ]),
5722
- now: z.boolean().default(false).describe("For stop: stop the container at once.")
6435
+ backup: idSchema.describe("The id of the backup, from agent_backups_list."),
6436
+ name: agentNameSchema.describe("The name of the new agent."),
6437
+ slug: slugSchema.optional().describe("The slug of the new agent, instead of one made from its name."),
6438
+ node: nodeField
5723
6439
  }),
5724
- run: async ({ action, now, ...reference }, run) => actionOn(run, await agentIdOf(run, reference), action, action === "stop" && now)
6440
+ run: async ({ backup, name, slug, node, ...reference }, run) => restoredAgent(run.api, await agentIdOf(run, reference), {
6441
+ backupId: backup,
6442
+ name,
6443
+ node,
6444
+ slug
6445
+ })
5725
6446
  })
5726
6447
  ];
5727
6448
  //#endregion
@@ -5794,42 +6515,6 @@ const credentialTools = [
5794
6515
  })
5795
6516
  ];
5796
6517
  //#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
5833
6518
  //#region src/commands/notifications.ts
5834
6519
  const notificationHints = { "notification.not_found": "kcloud notifications list shows them." };
5835
6520
  const createNotificationsCommand = (adaptersOf) => defineGroup({
@@ -7851,6 +8536,7 @@ const kcloudTools = [
7851
8536
  ...projectToolTools,
7852
8537
  ...nodeTools,
7853
8538
  ...agentTools,
8539
+ ...agentBackupTools,
7854
8540
  ...taskTools,
7855
8541
  ...observabilityTools,
7856
8542
  ...notificationTools,
@@ -7928,6 +8614,214 @@ const createMcpCommand = (adaptersOf, cliVersion) => defineCommand({
7928
8614
  }
7929
8615
  });
7930
8616
  //#endregion
8617
+ //#region src/commands/nodes/index.ts
8618
+ const gibibytes = (bytes) => `${(bytes / 1024 ** 3).toFixed(1)} GiB`;
8619
+ const hostOf = (node) => node.host === null ? "-" : `${node.host.cpus} CPU, ${gibibytes(node.host.memoryBytes)} of memory`;
8620
+ const capabilitiesOf = (node) => {
8621
+ if (node.capabilities === null) return "-";
8622
+ const { sandbox, uidIsolation } = node.capabilities;
8623
+ return `sandbox ${sandbox ? "yes" : "no"}, uid isolation ${uidIsolation ? "yes" : "no"}`;
8624
+ };
8625
+ const registrationOf = (node) => node.registrationExpiresAt === null ? "-" : `a token waits until ${node.registrationExpiresAt}`;
8626
+ const storageOf = ({ state, freeBytes, totalBytes, path }) => `${state}, ${gibibytes(freeBytes)} free of ${gibibytes(totalBytes)} at ${path}`;
8627
+ const runtimeRowsOf = (runtime) => [
8628
+ ["reported", runtime.reportedAt],
8629
+ ["docker", runtime.docker.lastError === null ? runtime.docker.state : `${runtime.docker.state}: ${runtime.docker.lastError}`],
8630
+ ["data storage", storageOf(runtime.storage.data)],
8631
+ ["engine storage", storageOf(runtime.storage.engine)],
8632
+ ["last reconcile", runtime.reconcile.lastTickError === null ? runtime.reconcile.lastTickAt ?? "never" : `${runtime.reconcile.lastTickAt ?? "never"}: ${runtime.reconcile.lastTickError}`],
8633
+ ["agents", `${runtime.reconcile.agentsActive} active, ${runtime.reconcile.agentsQueued} queued`],
8634
+ ["log readers", String(runtime.ingest.readers)]
8635
+ ];
8636
+ const updateOf = ({ update }) => {
8637
+ if (update === null) return "not reported";
8638
+ switch (update.state) {
8639
+ case "current": return "current";
8640
+ case "waiting": return `waits for ${String(update.sessions)} ${update.sessions === 1 ? "session" : "sessions"} to end before ${update.target ?? "-"}`;
8641
+ case "updating": return `updating to ${update.target ?? "-"}`;
8642
+ case "failed": return `failed to reach ${update.target ?? "-"}, runs its previous version`;
8643
+ case "unmanaged": return "updated with its platform";
8644
+ }
8645
+ };
8646
+ const lastUpdateOf = ({ update }) => {
8647
+ const last = update?.last ?? null;
8648
+ if (last === null) return null;
8649
+ const outcome = `${last.outcome} ${last.version ?? "-"} at ${last.at}`;
8650
+ return last.reason === null ? outcome : `${outcome}: ${last.reason}`;
8651
+ };
8652
+ const nodeDetailsText = (node) => [
8653
+ renderTable([{ header: "FIELD" }, { header: "VALUE" }], [
8654
+ ["name", node.name],
8655
+ ["state", node.state],
8656
+ ["provider", node.provider],
8657
+ ["engine", node.engine ?? "-"],
8658
+ ["version", node.version ?? "-"],
8659
+ ["host", hostOf(node)],
8660
+ ["server", node.host?.serverVersion ?? "-"],
8661
+ ["data root", node.host?.dataRoot ?? "-"],
8662
+ ["capabilities", capabilitiesOf(node)],
8663
+ ["offers", node.offered.length === 0 ? "-" : node.offered.join(", ")],
8664
+ ["split", splitSummaryOf(node.split)],
8665
+ ["registration", registrationOf(node)],
8666
+ ["connected", node.connectedAt ?? "never"],
8667
+ ["last seen", node.lastSeenAt ?? "never"],
8668
+ ...node.offlineSince === null ? [] : [["offline since", node.offlineSince]],
8669
+ ["update", updateOf(node)],
8670
+ ...lastUpdateOf(node) === null ? [] : [["last update", lastUpdateOf(node) ?? ""]],
8671
+ ["created", node.createdAt],
8672
+ ["id", node.id]
8673
+ ]),
8674
+ "",
8675
+ node.agents.length === 0 ? "No agent is placed on the node." : renderTable([
8676
+ { header: "PROJECT" },
8677
+ { header: "AGENT" },
8678
+ { header: "STATE" },
8679
+ { header: "POOL LIMIT" },
8680
+ { header: "ID" }
8681
+ ], node.agents.map((agent) => [
8682
+ agent.projectName,
8683
+ agent.slug,
8684
+ agent.state,
8685
+ poolLimitText(agent.poolLimit),
8686
+ agent.id
8687
+ ])),
8688
+ "",
8689
+ ...node.runtime === null ? ["The node has reported no runtime yet."] : [
8690
+ renderTable([{ header: "RUNTIME" }, { header: "VALUE" }], runtimeRowsOf(node.runtime)),
8691
+ "",
8692
+ poolsText(node.runtime.pools)
8693
+ ]
8694
+ ].join("\n");
8695
+ const startHintOf = (origin) => [
8696
+ "Start the node with it as docs/install.md of kikimora-cloud describes. On a Linux host, after read -rs token:",
8697
+ ` printf '%s\\n' "$token" | /opt/kikimora-cloud/infra/node/install.sh --platform-url ${origin} --image ghcr.io/kikimoradev/kikimora-cloud/node:<version> --registration-token -`,
8698
+ "On a Windows PC:",
8699
+ ` C:\\kikimora-cloud\\src\\infra\\windows\\install.ps1 -PlatformUrl ${origin} -RegistrationToken <token>`
8700
+ ].join("\n");
8701
+ const printedRegistration = (run, created) => {
8702
+ if (run.settings.json) {
8703
+ run.context.terminal.json(created);
8704
+ return exitCodes.ok;
8705
+ }
8706
+ run.context.terminal.line(created.registration.token);
8707
+ 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"));
8708
+ return exitCodes.ok;
8709
+ };
8710
+ const createNodesCommand = (adaptersOf) => defineGroup({
8711
+ name: "nodes",
8712
+ summary: "The nodes of the organization, the hosts its agents run on.",
8713
+ usage: "nodes <command>",
8714
+ subcommands: [
8715
+ defineCommand({
8716
+ name: "list",
8717
+ summary: "List the nodes of the organization.",
8718
+ usage: "nodes list",
8719
+ options: {},
8720
+ positionals: {
8721
+ min: 0,
8722
+ max: 0
8723
+ },
8724
+ run: async (invocation) => {
8725
+ const run = await runOf(adaptersOf, invocation);
8726
+ const organization = await run.organization();
8727
+ const items = await nodesOf(run.api, organization.id);
8728
+ return printed(run, items, (nodes) => nodes.length === 0 ? `${organization.slug} has no node yet; kcloud nodes create <name> creates one.` : renderTable([
8729
+ { header: "NAME" },
8730
+ { header: "STATE" },
8731
+ { header: "ENGINE" },
8732
+ { header: "VERSION" },
8733
+ {
8734
+ header: "AGENTS",
8735
+ align: "right"
8736
+ },
8737
+ { header: "LAST SEEN" },
8738
+ { header: "ID" }
8739
+ ], nodes.map((node) => [
8740
+ node.name,
8741
+ node.state,
8742
+ node.engine ?? "-",
8743
+ node.version ?? "-",
8744
+ String(node.agentCount),
8745
+ node.lastSeenAt ?? "never",
8746
+ node.id
8747
+ ])));
8748
+ }
8749
+ }),
8750
+ defineCommand({
8751
+ name: "show",
8752
+ summary: "Show a node with its host, its split, the agents placed on it, its runtime and its pools.",
8753
+ usage: "nodes show <node>",
8754
+ optionsUsage: nodeUsage,
8755
+ options: {},
8756
+ positionals: {
8757
+ min: 1,
8758
+ max: 1
8759
+ },
8760
+ run: async (invocation) => {
8761
+ const run = await runOf(adaptersOf, invocation);
8762
+ const reference = await resolveNode(run, invocation.positionals[0] ?? "");
8763
+ return printed(run, await nodeDetailOf(run.api, reference), nodeDetailsText);
8764
+ }
8765
+ }),
8766
+ createSplitCommand(adaptersOf),
8767
+ defineCommand({
8768
+ name: "create",
8769
+ summary: "Create a node and print its one-time registration token.",
8770
+ usage: "nodes create <name>",
8771
+ optionsUsage: [["", "the name holds lowercase letters, digits and hyphens"], ["", "the token goes to stdout alone, how to start the node to stderr"]],
8772
+ options: {},
8773
+ positionals: {
8774
+ min: 1,
8775
+ max: 1
8776
+ },
8777
+ run: async (invocation) => {
8778
+ const run = await runOf(adaptersOf, invocation);
8779
+ const organization = await run.organization();
8780
+ return printedRegistration(run, await createdNode(run.api, organization.id, invocation.positionals[0] ?? ""));
8781
+ }
8782
+ }),
8783
+ defineCommand({
8784
+ name: "token",
8785
+ summary: "Issue a new registration token for a node that has not joined yet.",
8786
+ usage: "nodes token <node>",
8787
+ optionsUsage: [...nodeUsage, ["", "the token replaces the one issued before, which stops working"]],
8788
+ options: {},
8789
+ positionals: {
8790
+ min: 1,
8791
+ max: 1
8792
+ },
8793
+ run: async (invocation) => {
8794
+ const run = await runOf(adaptersOf, invocation);
8795
+ const reference = await resolveNode(run, invocation.positionals[0] ?? "");
8796
+ return printedRegistration(run, await renewedRegistration(run.api, reference));
8797
+ }
8798
+ }),
8799
+ defineCommand({
8800
+ name: "delete",
8801
+ summary: "Delete a node that runs no agent.",
8802
+ usage: "nodes delete <node>",
8803
+ optionsUsage: nodeUsage,
8804
+ options: {},
8805
+ positionals: {
8806
+ min: 1,
8807
+ max: 1
8808
+ },
8809
+ run: async (invocation) => {
8810
+ const run = await runOf(adaptersOf, invocation);
8811
+ const given = invocation.positionals[0] ?? "";
8812
+ const reference = await resolveNode(run, given);
8813
+ await confirmed({
8814
+ prompt: run.prompt,
8815
+ settings: run.settings,
8816
+ question: `Delete the node ${given}? Its host can no longer reach the platform.`
8817
+ });
8818
+ await deleteNode(run.api, reference);
8819
+ return said(run, `Deleted the node ${given}.`);
8820
+ }
8821
+ })
8822
+ ]
8823
+ });
8824
+ //#endregion
7931
8825
  //#region src/commands/releases.ts
7932
8826
  const createReleasesCommand = (adaptersOf) => defineGroup({
7933
8827
  name: "releases",