@kikimoradev/kcloud 0.16.0 → 0.18.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 +1591 -450
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -386,6 +386,12 @@ const dispatcherOf = (spec) => {
386
386
  }
387
387
  };
388
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
+ };
389
395
  const permissionsOutsideOwner = 63;
390
396
  const isMissing = (error) => error instanceof Error && error.code === "ENOENT";
391
397
  const ensurePrivateDirectory = async (path) => {
@@ -509,243 +515,6 @@ const spec = {
509
515
  const defineCommand = commandFactoryOf(spec);
510
516
  const defineGroup = (definition) => defineGroup$1(definition);
511
517
  const runCli = dispatcherOf(spec);
512
- const agentEventLevelSchema = z.enum([
513
- "info",
514
- "warn",
515
- "error"
516
- ]);
517
- const agentRoles = [
518
- "monitor",
519
- "executor",
520
- "summarizer"
521
- ];
522
- const agentRoleSchema = z.enum(agentRoles);
523
- const agentLogStreams = ["stdout", "stderr"];
524
- z.enum(agentLogStreams);
525
- const platformStream = "platform";
526
- const agentEventStreams = [...agentLogStreams, platformStream];
527
- const agentEventStreamSchema = z.enum(agentEventStreams);
528
- const authKindSchema = z.enum([
529
- "apiKey",
530
- "oauth",
531
- "claude.ai",
532
- "unknown"
533
- ]);
534
- const agentControlStateSchema = z.enum([
535
- "running",
536
- "pausing",
537
- "paused"
538
- ]);
539
- z.enum([
540
- "session",
541
- "diagnostic",
542
- "task"
543
- ]);
544
- const maxCostUsd = 1e3;
545
- const countSchema = z.int().min(0).max(2147483647);
546
- const costUsdSchema = z.number().min(0).max(maxCostUsd);
547
- const instantSchema$2 = z.iso.datetime();
548
- const workerEnvelope = (event) => ({
549
- ts: instantSchema$2,
550
- level: agentEventLevelSchema,
551
- event: z.literal(event)
552
- });
553
- const roleEnvelope = (event) => ({
554
- ...workerEnvelope(event),
555
- agent: agentRoleSchema.optional()
556
- });
557
- const agentEventSchema = z.discriminatedUnion("event", [
558
- z.looseObject({
559
- ...workerEnvelope("worker.started"),
560
- version: z.string(),
561
- claudeVersion: z.string().optional(),
562
- nodeVersion: z.string(),
563
- authKind: authKindSchema,
564
- pid: countSchema,
565
- projectDir: z.string(),
566
- paused: z.boolean().optional()
567
- }),
568
- z.looseObject({
569
- ...workerEnvelope("worker.stopped"),
570
- signal: z.string().optional(),
571
- drained: z.boolean().optional(),
572
- forced: z.boolean().optional()
573
- }),
574
- z.looseObject({
575
- ...workerEnvelope("worker.draining"),
576
- reason: z.string(),
577
- timeoutMs: countSchema.optional()
578
- }),
579
- z.looseObject({
580
- ...workerEnvelope("update.available"),
581
- from: z.string(),
582
- to: z.string(),
583
- installError: z.string().optional()
584
- }),
585
- z.looseObject({
586
- ...workerEnvelope("update.installed"),
587
- from: z.string(),
588
- to: z.string()
589
- }),
590
- z.looseObject({
591
- ...roleEnvelope("control.changed"),
592
- state: agentControlStateSchema
593
- }),
594
- z.looseObject({
595
- ...roleEnvelope("cycle.started"),
596
- cycle: countSchema
597
- }),
598
- z.looseObject({
599
- ...roleEnvelope("cycle.finished"),
600
- cycle: countSchema,
601
- ok: z.boolean(),
602
- durationMs: countSchema,
603
- costUsd: costUsdSchema.optional(),
604
- addedTasks: countSchema,
605
- skippedDuplicates: countSchema,
606
- sessionId: z.string().optional(),
607
- error: z.string().optional()
608
- }),
609
- z.looseObject({
610
- ...roleEnvelope("monitor.offHours"),
611
- resumeAt: instantSchema$2
612
- }),
613
- z.looseObject({
614
- ...roleEnvelope("monitor.limitWait"),
615
- resumeAt: instantSchema$2
616
- }),
617
- z.looseObject({
618
- ...roleEnvelope("monitor.sleeping"),
619
- nextCycleAt: instantSchema$2
620
- }),
621
- z.looseObject({
622
- ...roleEnvelope("monitor.authBlocked"),
623
- reason: z.string()
624
- }),
625
- z.looseObject({
626
- ...roleEnvelope("monitor.admissionWait"),
627
- ticket: z.string(),
628
- since: instantSchema$2,
629
- seq: countSchema
630
- }),
631
- z.looseObject({
632
- ...roleEnvelope("executor.admissionWait"),
633
- ticket: z.string(),
634
- since: instantSchema$2,
635
- seq: countSchema,
636
- taskId: z.string()
637
- }),
638
- z.looseObject({
639
- ...roleEnvelope("admission.granted"),
640
- ticket: z.string(),
641
- seq: countSchema
642
- }),
643
- z.looseObject({
644
- ...roleEnvelope("admission.released"),
645
- ticket: z.string(),
646
- seq: countSchema
647
- }),
648
- z.looseObject({
649
- ...roleEnvelope("admission.cancelled"),
650
- ticket: z.string(),
651
- seq: countSchema
652
- }),
653
- z.looseObject({
654
- ...roleEnvelope("task.started"),
655
- taskId: z.string(),
656
- title: z.string()
657
- }),
658
- z.looseObject({
659
- ...roleEnvelope("task.finished"),
660
- taskId: z.string(),
661
- title: z.string(),
662
- ok: z.boolean(),
663
- durationMs: countSchema,
664
- costUsd: costUsdSchema.optional(),
665
- numTurns: countSchema.optional(),
666
- willRetry: z.boolean().optional(),
667
- attempt: countSchema.optional(),
668
- maxAttempts: countSchema.optional(),
669
- sessionId: z.string().optional(),
670
- error: z.string().optional()
671
- }),
672
- z.looseObject({
673
- ...roleEnvelope("task.retryScheduled"),
674
- taskId: z.string(),
675
- resumeAt: instantSchema$2
676
- }),
677
- z.looseObject(roleEnvelope("executor.waiting")),
678
- z.looseObject({
679
- ...roleEnvelope("executor.limitWait"),
680
- resumeAt: instantSchema$2
681
- }),
682
- z.looseObject({
683
- ...roleEnvelope("executor.authBlocked"),
684
- reason: z.string()
685
- }),
686
- z.looseObject({
687
- ...roleEnvelope("summary.started"),
688
- taskId: z.string()
689
- }),
690
- z.looseObject({
691
- ...roleEnvelope("summary.finished"),
692
- taskId: z.string(),
693
- ok: z.boolean(),
694
- durationMs: countSchema,
695
- costUsd: costUsdSchema.optional(),
696
- sessionId: z.string().optional(),
697
- error: z.string().optional()
698
- }),
699
- z.looseObject({
700
- ...roleEnvelope("session.init"),
701
- model: z.string(),
702
- sessionId: z.string(),
703
- taskId: z.string().optional(),
704
- cycle: countSchema.optional()
705
- }),
706
- z.looseObject({
707
- ...roleEnvelope("session.stderr"),
708
- line: z.string()
709
- }),
710
- z.looseObject({
711
- ...roleEnvelope("session.procError"),
712
- message: z.string()
713
- }),
714
- z.looseObject({
715
- ...roleEnvelope("session.killed"),
716
- reason: z.string()
717
- }),
718
- z.looseObject({
719
- ...roleEnvelope("session.text"),
720
- text: z.string()
721
- }),
722
- z.looseObject({
723
- ...roleEnvelope("session.tool"),
724
- tool: z.string(),
725
- detail: z.string()
726
- }),
727
- z.looseObject({
728
- ...roleEnvelope("session.toolError"),
729
- output: z.string()
730
- })
731
- ]);
732
- z.looseObject({
733
- ts: instantSchema$2,
734
- level: agentEventLevelSchema,
735
- agent: agentRoleSchema.optional(),
736
- event: z.string().min(1).max(64)
737
- });
738
- agentEventSchema.options.map((option) => option.shape.event.value);
739
- ({
740
- rawLog: "log.raw",
741
- rejectedLog: "log.rejected",
742
- throttledLog: "log.throttled",
743
- stateChanged: "platform.stateChanged",
744
- crashLooping: "platform.crashLooping",
745
- provisioned: "platform.provisioned",
746
- sandboxStarted: "platform.sandboxStarted",
747
- sandboxStopped: "platform.sandboxStopped"
748
- }).rawLog;
749
518
  //#endregion
750
519
  //#region ../../packages/contracts/src/patterns.ts
751
520
  const slugPattern = /^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$/;
@@ -1021,6 +790,23 @@ const errorCodes = Object.keys({
1021
790
  "node.in_use": "conflict",
1022
791
  "node.host_unknown": "rule_violation",
1023
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",
798
+ "node.machine_missing": "rule_violation",
799
+ "node.operation_running": "conflict",
800
+ "node.operation_not_failed": "conflict",
801
+ "node.size_unavailable": "rule_violation",
802
+ "node.size_not_larger": "rule_violation",
803
+ "node.region_unavailable": "rule_violation",
804
+ "provider.not_found": "not_found",
805
+ "provider.name_taken": "conflict",
806
+ "provider.in_use": "conflict",
807
+ "provider.token_unauthorized": "rule_violation",
808
+ "provider.token_scopes_missing": "rule_violation",
809
+ "provider.unavailable": "rule_violation",
1024
810
  "project.last_admin": "conflict",
1025
811
  "agent.node_unassigned": "rule_violation",
1026
812
  "stream.too_many": "rate_limited",
@@ -1057,6 +843,243 @@ const errorBodySchema = z.object({
1057
843
  message: z.string()
1058
844
  })).optional()
1059
845
  }).meta({ id: "ErrorBody" });
846
+ const agentEventLevelSchema = z.enum([
847
+ "info",
848
+ "warn",
849
+ "error"
850
+ ]);
851
+ const agentRoles = [
852
+ "monitor",
853
+ "executor",
854
+ "summarizer"
855
+ ];
856
+ const agentRoleSchema = z.enum(agentRoles);
857
+ const agentLogStreams = ["stdout", "stderr"];
858
+ z.enum(agentLogStreams);
859
+ const platformStream = "platform";
860
+ const agentEventStreams = [...agentLogStreams, platformStream];
861
+ const agentEventStreamSchema = z.enum(agentEventStreams);
862
+ const authKindSchema = z.enum([
863
+ "apiKey",
864
+ "oauth",
865
+ "claude.ai",
866
+ "unknown"
867
+ ]);
868
+ const agentControlStateSchema = z.enum([
869
+ "running",
870
+ "pausing",
871
+ "paused"
872
+ ]);
873
+ z.enum([
874
+ "session",
875
+ "diagnostic",
876
+ "task"
877
+ ]);
878
+ const maxCostUsd = 1e3;
879
+ const countSchema = z.int().min(0).max(2147483647);
880
+ const costUsdSchema = z.number().min(0).max(maxCostUsd);
881
+ const instantSchema$2 = z.iso.datetime();
882
+ const workerEnvelope = (event) => ({
883
+ ts: instantSchema$2,
884
+ level: agentEventLevelSchema,
885
+ event: z.literal(event)
886
+ });
887
+ const roleEnvelope = (event) => ({
888
+ ...workerEnvelope(event),
889
+ agent: agentRoleSchema.optional()
890
+ });
891
+ const agentEventSchema = z.discriminatedUnion("event", [
892
+ z.looseObject({
893
+ ...workerEnvelope("worker.started"),
894
+ version: z.string(),
895
+ claudeVersion: z.string().optional(),
896
+ nodeVersion: z.string(),
897
+ authKind: authKindSchema,
898
+ pid: countSchema,
899
+ projectDir: z.string(),
900
+ paused: z.boolean().optional()
901
+ }),
902
+ z.looseObject({
903
+ ...workerEnvelope("worker.stopped"),
904
+ signal: z.string().optional(),
905
+ drained: z.boolean().optional(),
906
+ forced: z.boolean().optional()
907
+ }),
908
+ z.looseObject({
909
+ ...workerEnvelope("worker.draining"),
910
+ reason: z.string(),
911
+ timeoutMs: countSchema.optional()
912
+ }),
913
+ z.looseObject({
914
+ ...workerEnvelope("update.available"),
915
+ from: z.string(),
916
+ to: z.string(),
917
+ installError: z.string().optional()
918
+ }),
919
+ z.looseObject({
920
+ ...workerEnvelope("update.installed"),
921
+ from: z.string(),
922
+ to: z.string()
923
+ }),
924
+ z.looseObject({
925
+ ...roleEnvelope("control.changed"),
926
+ state: agentControlStateSchema
927
+ }),
928
+ z.looseObject({
929
+ ...roleEnvelope("cycle.started"),
930
+ cycle: countSchema
931
+ }),
932
+ z.looseObject({
933
+ ...roleEnvelope("cycle.finished"),
934
+ cycle: countSchema,
935
+ ok: z.boolean(),
936
+ durationMs: countSchema,
937
+ costUsd: costUsdSchema.optional(),
938
+ addedTasks: countSchema,
939
+ skippedDuplicates: countSchema,
940
+ sessionId: z.string().optional(),
941
+ error: z.string().optional()
942
+ }),
943
+ z.looseObject({
944
+ ...roleEnvelope("monitor.offHours"),
945
+ resumeAt: instantSchema$2
946
+ }),
947
+ z.looseObject({
948
+ ...roleEnvelope("monitor.limitWait"),
949
+ resumeAt: instantSchema$2
950
+ }),
951
+ z.looseObject({
952
+ ...roleEnvelope("monitor.sleeping"),
953
+ nextCycleAt: instantSchema$2
954
+ }),
955
+ z.looseObject({
956
+ ...roleEnvelope("monitor.authBlocked"),
957
+ reason: z.string()
958
+ }),
959
+ z.looseObject({
960
+ ...roleEnvelope("monitor.admissionWait"),
961
+ ticket: z.string(),
962
+ since: instantSchema$2,
963
+ seq: countSchema
964
+ }),
965
+ z.looseObject({
966
+ ...roleEnvelope("executor.admissionWait"),
967
+ ticket: z.string(),
968
+ since: instantSchema$2,
969
+ seq: countSchema,
970
+ taskId: z.string()
971
+ }),
972
+ z.looseObject({
973
+ ...roleEnvelope("admission.granted"),
974
+ ticket: z.string(),
975
+ seq: countSchema
976
+ }),
977
+ z.looseObject({
978
+ ...roleEnvelope("admission.released"),
979
+ ticket: z.string(),
980
+ seq: countSchema
981
+ }),
982
+ z.looseObject({
983
+ ...roleEnvelope("admission.cancelled"),
984
+ ticket: z.string(),
985
+ seq: countSchema
986
+ }),
987
+ z.looseObject({
988
+ ...roleEnvelope("task.started"),
989
+ taskId: z.string(),
990
+ title: z.string()
991
+ }),
992
+ z.looseObject({
993
+ ...roleEnvelope("task.finished"),
994
+ taskId: z.string(),
995
+ title: z.string(),
996
+ ok: z.boolean(),
997
+ durationMs: countSchema,
998
+ costUsd: costUsdSchema.optional(),
999
+ numTurns: countSchema.optional(),
1000
+ willRetry: z.boolean().optional(),
1001
+ attempt: countSchema.optional(),
1002
+ maxAttempts: countSchema.optional(),
1003
+ sessionId: z.string().optional(),
1004
+ error: z.string().optional()
1005
+ }),
1006
+ z.looseObject({
1007
+ ...roleEnvelope("task.retryScheduled"),
1008
+ taskId: z.string(),
1009
+ resumeAt: instantSchema$2
1010
+ }),
1011
+ z.looseObject(roleEnvelope("executor.waiting")),
1012
+ z.looseObject({
1013
+ ...roleEnvelope("executor.limitWait"),
1014
+ resumeAt: instantSchema$2
1015
+ }),
1016
+ z.looseObject({
1017
+ ...roleEnvelope("executor.authBlocked"),
1018
+ reason: z.string()
1019
+ }),
1020
+ z.looseObject({
1021
+ ...roleEnvelope("summary.started"),
1022
+ taskId: z.string()
1023
+ }),
1024
+ z.looseObject({
1025
+ ...roleEnvelope("summary.finished"),
1026
+ taskId: z.string(),
1027
+ ok: z.boolean(),
1028
+ durationMs: countSchema,
1029
+ costUsd: costUsdSchema.optional(),
1030
+ sessionId: z.string().optional(),
1031
+ error: z.string().optional()
1032
+ }),
1033
+ z.looseObject({
1034
+ ...roleEnvelope("session.init"),
1035
+ model: z.string(),
1036
+ sessionId: z.string(),
1037
+ taskId: z.string().optional(),
1038
+ cycle: countSchema.optional()
1039
+ }),
1040
+ z.looseObject({
1041
+ ...roleEnvelope("session.stderr"),
1042
+ line: z.string()
1043
+ }),
1044
+ z.looseObject({
1045
+ ...roleEnvelope("session.procError"),
1046
+ message: z.string()
1047
+ }),
1048
+ z.looseObject({
1049
+ ...roleEnvelope("session.killed"),
1050
+ reason: z.string()
1051
+ }),
1052
+ z.looseObject({
1053
+ ...roleEnvelope("session.text"),
1054
+ text: z.string()
1055
+ }),
1056
+ z.looseObject({
1057
+ ...roleEnvelope("session.tool"),
1058
+ tool: z.string(),
1059
+ detail: z.string()
1060
+ }),
1061
+ z.looseObject({
1062
+ ...roleEnvelope("session.toolError"),
1063
+ output: z.string()
1064
+ })
1065
+ ]);
1066
+ z.looseObject({
1067
+ ts: instantSchema$2,
1068
+ level: agentEventLevelSchema,
1069
+ agent: agentRoleSchema.optional(),
1070
+ event: z.string().min(1).max(64)
1071
+ });
1072
+ agentEventSchema.options.map((option) => option.shape.event.value);
1073
+ ({
1074
+ rawLog: "log.raw",
1075
+ rejectedLog: "log.rejected",
1076
+ throttledLog: "log.throttled",
1077
+ stateChanged: "platform.stateChanged",
1078
+ crashLooping: "platform.crashLooping",
1079
+ provisioned: "platform.provisioned",
1080
+ sandboxStarted: "platform.sandboxStarted",
1081
+ sandboxStopped: "platform.sandboxStopped"
1082
+ }).rawLog;
1060
1083
  const agentStateSchema = z.enum([
1061
1084
  "created",
1062
1085
  "provisioning",
@@ -1429,6 +1452,83 @@ pageSchema(z.object({
1429
1452
  finishedAt: z.iso.datetime().nullable()
1430
1453
  }).meta({ id: "TaskCost" })).meta({ id: "TaskCostPage" });
1431
1454
  costQuerySchema.extend({ cursor: cursorSchema.optional() });
1455
+ const providerKindSchema = z.enum(["digitalocean"]);
1456
+ const providerAccountNameSchema = z.string().trim().min(1).max(64);
1457
+ const providerTokenSchema = z.string().trim().min(16).max(512).regex(/^[\x21-\x7e]+$/u);
1458
+ const providerRegionSlugSchema = z.string().trim().regex(/^[a-z]{3}[0-9]$/u);
1459
+ const providerSizeSlugSchema = z.string().trim().regex(/^[a-z0-9][a-z0-9-]{0,63}$/u);
1460
+ const providerTokenScopes = [
1461
+ "regions:read",
1462
+ "sizes:read",
1463
+ "image:read",
1464
+ "vpc:read",
1465
+ "actions:read",
1466
+ "droplet:create",
1467
+ "droplet:read",
1468
+ "droplet:update",
1469
+ "droplet:delete",
1470
+ "block_storage:create",
1471
+ "block_storage:read",
1472
+ "block_storage:delete",
1473
+ "block_storage_action:create",
1474
+ "firewall:create",
1475
+ "firewall:read",
1476
+ "firewall:delete",
1477
+ "tag:create",
1478
+ "tag:read",
1479
+ "tag:delete"
1480
+ ];
1481
+ const providerTokenScopeSchema = z.enum(providerTokenScopes).meta({ id: "ProviderTokenScope" });
1482
+ const providerVerificationSchema = z.object({
1483
+ checkedAt: z.iso.datetime(),
1484
+ missingScopes: z.array(providerTokenScopeSchema),
1485
+ failure: z.string().nullable()
1486
+ }).meta({ id: "ProviderVerification" });
1487
+ const providerAccountSchema = z.object({
1488
+ id: idSchema,
1489
+ kind: providerKindSchema,
1490
+ name: z.string(),
1491
+ defaultRegion: z.string(),
1492
+ revision: z.int().nonnegative(),
1493
+ verification: providerVerificationSchema,
1494
+ nodeCount: z.int().nonnegative(),
1495
+ createdAt: z.iso.datetime(),
1496
+ updatedAt: z.iso.datetime()
1497
+ }).meta({ id: "ProviderAccount" });
1498
+ z.object({ items: z.array(providerAccountSchema) }).meta({ id: "ProviderAccountList" });
1499
+ const createProviderAccountBodySchema = z.object({
1500
+ kind: providerKindSchema,
1501
+ name: providerAccountNameSchema,
1502
+ token: providerTokenSchema,
1503
+ defaultRegion: providerRegionSlugSchema
1504
+ }).meta({ id: "CreateProviderAccountBody" });
1505
+ z.object({
1506
+ revision: z.int().nonnegative(),
1507
+ name: providerAccountNameSchema.optional(),
1508
+ token: providerTokenSchema.optional(),
1509
+ defaultRegion: providerRegionSlugSchema.optional()
1510
+ }).meta({ id: "UpdateProviderAccountBody" });
1511
+ const providerRegionSchema = z.object({
1512
+ slug: z.string(),
1513
+ name: z.string(),
1514
+ available: z.boolean()
1515
+ }).meta({ id: "ProviderRegion" });
1516
+ const providerSizeSchema = z.object({
1517
+ slug: z.string(),
1518
+ vcpus: z.int().positive(),
1519
+ memoryMb: z.int().positive(),
1520
+ diskGb: z.int().positive(),
1521
+ priceMonthly: z.number().nonnegative(),
1522
+ regions: z.array(z.string())
1523
+ }).meta({ id: "ProviderSize" });
1524
+ z.object({
1525
+ regions: z.array(providerRegionSchema),
1526
+ sizes: z.array(providerSizeSchema)
1527
+ }).meta({ id: "ProviderCatalog" });
1528
+ z.object({
1529
+ organizationId: idSchema,
1530
+ accountId: idSchema
1531
+ });
1432
1532
  z.enum(["monitor", "executor"]);
1433
1533
  const sessionSlotCountSchema = z.int().min(1).max(64);
1434
1534
  const sessionQueueEntrySchema = z.object({
@@ -1471,7 +1571,8 @@ const nodeStateSchema = z.enum([
1471
1571
  "online",
1472
1572
  "offline"
1473
1573
  ]);
1474
- const nodeProviderSchema = z.enum(["self_hosted"]);
1574
+ const nodeProviders = ["self_hosted", "digitalocean"];
1575
+ const nodeProviderSchema = z.enum(nodeProviders);
1475
1576
  const nodeNameSchema = z.string().trim().regex(slugPattern);
1476
1577
  const nodeHostSchema = z.object({
1477
1578
  cpus: z.number().nonnegative(),
@@ -1546,6 +1647,52 @@ const nodePoolsSchema = z.object({
1546
1647
  monitor: nodePoolSchema,
1547
1648
  executor: nodePoolSchema
1548
1649
  }).meta({ id: "NodePools" });
1650
+ const nodeVolumeGbSchema = z.int().min(10).max(4096);
1651
+ const nodeMachineSchema = z.object({
1652
+ account: z.object({
1653
+ id: idSchema,
1654
+ name: z.string().nullable()
1655
+ }),
1656
+ region: z.string(),
1657
+ size: z.string(),
1658
+ volumeGb: z.int().positive(),
1659
+ dropletId: z.int().positive().nullable(),
1660
+ publicIpv4: z.string().nullable()
1661
+ }).meta({ id: "NodeMachine" });
1662
+ const nodeOperationKindSchema = z.enum([
1663
+ "create",
1664
+ "resize",
1665
+ "delete"
1666
+ ]);
1667
+ const nodeOperationStateSchema = z.enum(["running", "failed"]);
1668
+ const nodeOperationStepSchema = z.enum([
1669
+ "tag",
1670
+ "firewall",
1671
+ "volume",
1672
+ "droplet",
1673
+ "boot",
1674
+ "installing",
1675
+ "power_off",
1676
+ "resize_droplet",
1677
+ "resize_volume",
1678
+ "power_on",
1679
+ "delete_droplet",
1680
+ "delete_volume",
1681
+ "delete_firewall",
1682
+ "delete_tag"
1683
+ ]);
1684
+ const nodeOperationSchema = z.object({
1685
+ kind: nodeOperationKindSchema,
1686
+ state: nodeOperationStateSchema,
1687
+ step: nodeOperationStepSchema.nullable(),
1688
+ error: z.string().nullable(),
1689
+ target: z.object({
1690
+ size: z.string(),
1691
+ volumeGb: z.int().positive()
1692
+ }).nullable(),
1693
+ startedAt: z.iso.datetime(),
1694
+ updatedAt: z.iso.datetime()
1695
+ }).meta({ id: "NodeOperation" });
1549
1696
  const nodeSchema = z.object({
1550
1697
  id: idSchema,
1551
1698
  name: nodeNameSchema,
@@ -1561,9 +1708,34 @@ const nodeSchema = z.object({
1561
1708
  agentCount: z.int().nonnegative(),
1562
1709
  connectedAt: z.iso.datetime().nullable(),
1563
1710
  lastSeenAt: z.iso.datetime().nullable(),
1711
+ offlineSince: z.iso.datetime().nullable(),
1564
1712
  registrationExpiresAt: z.iso.datetime().nullable(),
1713
+ machine: nodeMachineSchema.nullable(),
1714
+ operation: nodeOperationSchema.nullable(),
1565
1715
  createdAt: z.iso.datetime()
1566
1716
  }).meta({ id: "Node" });
1717
+ const nodeUpdateSchema = z.object({
1718
+ state: z.enum([
1719
+ "current",
1720
+ "waiting",
1721
+ "updating",
1722
+ "failed",
1723
+ "unmanaged"
1724
+ ]),
1725
+ target: z.string().nullable(),
1726
+ sessions: z.int().nonnegative(),
1727
+ since: z.iso.datetime().nullable(),
1728
+ last: z.object({
1729
+ outcome: z.enum([
1730
+ "updated",
1731
+ "rolledBack",
1732
+ "failed"
1733
+ ]),
1734
+ version: z.string().nullable(),
1735
+ reason: z.string().nullable(),
1736
+ at: z.iso.datetime()
1737
+ }).nullable()
1738
+ }).meta({ id: "NodeUpdate" });
1567
1739
  const nodeAgentSchema = z.object({
1568
1740
  id: idSchema,
1569
1741
  name: z.string(),
@@ -1609,7 +1781,8 @@ const nodeRuntimeSchema = z.object({
1609
1781
  }).meta({ id: "NodeRuntime" });
1610
1782
  nodeSchema.extend({
1611
1783
  agents: z.array(nodeAgentSchema),
1612
- runtime: nodeRuntimeSchema.nullable()
1784
+ runtime: nodeRuntimeSchema.nullable(),
1785
+ update: nodeUpdateSchema.nullable()
1613
1786
  }).meta({ id: "NodeDetail" });
1614
1787
  z.object({ items: z.array(nodeSchema) }).meta({ id: "NodeList" });
1615
1788
  z.object({ nodeId: idSchema });
@@ -1621,15 +1794,34 @@ z.object({
1621
1794
  revision: z.int().nonnegative(),
1622
1795
  split: nodeSplitSchema.nullable()
1623
1796
  }).meta({ id: "UpdateNodeSplitBody" });
1624
- const createNodeBodySchema = z.object({ name: nodeNameSchema }).meta({ id: "CreateNodeBody" });
1797
+ const nodeMachineRequestSchema = z.object({
1798
+ accountId: idSchema,
1799
+ region: providerRegionSlugSchema,
1800
+ size: providerSizeSlugSchema,
1801
+ volumeGb: nodeVolumeGbSchema
1802
+ }).meta({ id: "NodeMachineRequest" });
1803
+ const createNodeBodySchema = z.object({
1804
+ name: nodeNameSchema,
1805
+ machine: nodeMachineRequestSchema.optional()
1806
+ }).meta({ id: "CreateNodeBody" });
1807
+ const resizeNodeBodySchema = z.object({
1808
+ revision: z.int().nonnegative(),
1809
+ size: providerSizeSlugSchema,
1810
+ volumeGb: nodeVolumeGbSchema
1811
+ }).meta({ id: "ResizeNodeBody" });
1812
+ z.object({ revision: z.int().nonnegative() }).meta({ id: "RetryNodeOperationBody" });
1625
1813
  const nodeRegistrationSchema = z.object({
1626
1814
  token: z.string(),
1627
1815
  expiresAt: z.iso.datetime()
1628
1816
  }).meta({ id: "NodeRegistration" });
1817
+ z.object({
1818
+ node: nodeSchema,
1819
+ registration: nodeRegistrationSchema.nullable()
1820
+ }).meta({ id: "CreatedNode" });
1629
1821
  z.object({
1630
1822
  node: nodeSchema,
1631
1823
  registration: nodeRegistrationSchema
1632
- }).meta({ id: "CreatedNode" }).meta({ id: "RenewedNodeRegistration" });
1824
+ }).meta({ id: "RenewedNodeRegistration" });
1633
1825
  z.object({
1634
1826
  token: z.string().min(1).max(256),
1635
1827
  version: z.string().min(1).max(128),
@@ -1637,6 +1829,16 @@ z.object({
1637
1829
  host: nodeHostSchema.nullable(),
1638
1830
  capabilities: nodeCapabilitiesSchema
1639
1831
  }).meta({ id: "RegisterNodeBody" });
1832
+ z.object({ token: z.string().min(1).max(256) }).meta({ id: "NodeInstallationBody" });
1833
+ z.object({
1834
+ platformVersion: z.string(),
1835
+ image: z.string().nullable(),
1836
+ registry: z.object({
1837
+ server: z.string(),
1838
+ username: z.string(),
1839
+ password: z.string()
1840
+ }).nullable()
1841
+ }).meta({ id: "NodeInstallation" });
1640
1842
  z.object({
1641
1843
  nodeId: idSchema,
1642
1844
  credential: z.string()
@@ -1825,6 +2027,8 @@ const auditActionSchema = z.enum([
1825
2027
  "agent.updated",
1826
2028
  "agent.deleted",
1827
2029
  "agent.action",
2030
+ "agent.backup_requested",
2031
+ "agent.restored",
1828
2032
  "platform.settings_updated",
1829
2033
  "runtime.sessions_updated",
1830
2034
  "platform.upgrade_requested",
@@ -1844,6 +2048,18 @@ const auditActionSchema = z.enum([
1844
2048
  "node.registration_renewed",
1845
2049
  "node.deleted",
1846
2050
  "node.split_changed",
2051
+ "node.resize_requested",
2052
+ "node.removal_requested",
2053
+ "node.operation_retried",
2054
+ "node.operation_failed",
2055
+ "node.machine_created",
2056
+ "node.machine_resized",
2057
+ "node.machine_removed",
2058
+ "provider_account.created",
2059
+ "provider_account.updated",
2060
+ "provider_account.token_replaced",
2061
+ "provider_account.verified",
2062
+ "provider_account.deleted",
1847
2063
  "master_key.reencrypted",
1848
2064
  "dead_letter.retried",
1849
2065
  "dead_letter.discarded"
@@ -1864,7 +2080,8 @@ const auditTargetTypeSchema = z.enum([
1864
2080
  "host_operation",
1865
2081
  "volume",
1866
2082
  "queue",
1867
- "node"
2083
+ "node",
2084
+ "provider_account"
1868
2085
  ]);
1869
2086
  const auditEntrySchema = z.object({
1870
2087
  id: idSchema,
@@ -2464,6 +2681,43 @@ z.object({
2464
2681
  z.object({ code: errorCodeSchema })
2465
2682
  ])
2466
2683
  }).meta({ id: "AgentEventFrame" });
2684
+ //#endregion
2685
+ //#region ../../packages/contracts/src/agent-backups.ts
2686
+ const agentBackupTriggers = ["nightly", "manual"];
2687
+ const agentBackupSchema = z.object({
2688
+ id: idSchema,
2689
+ agentId: idSchema,
2690
+ nodeId: idSchema,
2691
+ nodeName: z.string().nullable(),
2692
+ trigger: z.enum(agentBackupTriggers),
2693
+ kikimoraVersion: z.string(),
2694
+ entries: z.int().nonnegative(),
2695
+ sizeBytes: z.int().nonnegative(),
2696
+ createdAt: z.iso.datetime(),
2697
+ expiresAt: z.iso.datetime()
2698
+ }).meta({ id: "AgentBackup" });
2699
+ z.object({
2700
+ items: z.array(agentBackupSchema),
2701
+ retentionDays: z.int().positive()
2702
+ }).meta({ id: "AgentBackupList" });
2703
+ z.object({ state: z.literal("requested") }).meta({ id: "AgentBackupRequested" });
2704
+ z.object({
2705
+ id: idSchema,
2706
+ backupId: idSchema
2707
+ });
2708
+ z.object({
2709
+ name: agentNameSchema,
2710
+ slug: slugSchema.optional(),
2711
+ nodeId: idSchema
2712
+ }).meta({ id: "RestoreAgentBody" });
2713
+ z.object({ agentId: idSchema }).meta({ id: "RestoredAgent" });
2714
+ z.object({
2715
+ agentId: idSchema,
2716
+ trigger: z.enum(agentBackupTriggers),
2717
+ kikimoraVersion: z.string().min(1).max(64),
2718
+ entries: z.coerce.number().int().nonnegative()
2719
+ });
2720
+ z.object({ backupId: idSchema });
2467
2721
  [
2468
2722
  "application/vnd.oci.image.index.v1+json",
2469
2723
  "application/vnd.oci.image.manifest.v1+json",
@@ -2795,7 +3049,8 @@ const masterKeyVersionSchema = z.object({
2795
3049
  secrets: z.object({
2796
3050
  credentials: secretCountSchema,
2797
3051
  toolSecrets: secretCountSchema,
2798
- platformSecrets: secretCountSchema
3052
+ platformSecrets: secretCountSchema,
3053
+ providerAccounts: secretCountSchema.default(0)
2799
3054
  }),
2800
3055
  backups: secretCountSchema,
2801
3056
  newestBackupExpiresAt: instantSchema$1.nullable()
@@ -3644,6 +3899,254 @@ const createWhoamiCommand = (adaptersOf) => defineCommand({
3644
3899
  }
3645
3900
  });
3646
3901
  //#endregion
3902
+ //#region src/render/body.ts
3903
+ const isTerminal$1 = (stream) => stream.isTTY === true;
3904
+ const pipedText$1 = async (stream) => {
3905
+ const chunks = [];
3906
+ for await (const chunk of stream) chunks.push(Buffer.from(chunk));
3907
+ return Buffer.concat(chunks).toString("utf8");
3908
+ };
3909
+ const fileOptions = { file: { type: "string" } };
3910
+ const fileUsage = (what) => [["--file <path>", `read ${what} from a JSON file, or from stdin with -`]];
3911
+ const documentOf = async (run, file) => {
3912
+ const text = file === "-" ? isTerminal$1(run.context.io.stdin) ? "" : await pipedText$1(run.context.io.stdin) : await readFileOrRefuse(resolve(run.context.workingDirectory, file));
3913
+ if (text.trim() === "") throw new UsageError(file === "-" ? "Nothing is piped on stdin." : `${file} is empty.`);
3914
+ try {
3915
+ return JSON.parse(text);
3916
+ } catch {
3917
+ throw new CliError(`${file === "-" ? "stdin" : file} is not JSON.`);
3918
+ }
3919
+ };
3920
+ const readFileOrRefuse = async (path) => {
3921
+ try {
3922
+ return await readFile(path, "utf8");
3923
+ } catch {
3924
+ throw new CliError(`${path} could not be read.`);
3925
+ }
3926
+ };
3927
+ const validated = (schema, document, what) => {
3928
+ const parsed = schema.safeParse(document);
3929
+ if (!parsed.success) throw new CliError(`That is not ${what}.`, z.prettifyError(parsed.error));
3930
+ };
3931
+ //#endregion
3932
+ //#region src/commands/providers/shared.ts
3933
+ const providerHints = {
3934
+ ...organizationHints,
3935
+ "auth.forbidden": "Changing a provider account takes an admin of the organization.",
3936
+ "provider.not_found": "kcloud providers list shows the provider accounts.",
3937
+ "provider.name_taken": "Another provider account of the organization has that name.",
3938
+ "provider.in_use": "Delete the nodes created with it first; kcloud nodes list shows the provider of each node.",
3939
+ "provider.token_unauthorized": "DigitalOcean refused the token. Create a new one under API, Tokens in its control panel.",
3940
+ "concurrency.conflict": "Someone changed the account meanwhile; run the command again."
3941
+ };
3942
+ const reasonOf$1 = (error) => {
3943
+ const reason = error.details.reason;
3944
+ return typeof reason === "string" && reason !== "" ? `: ${reason}` : "";
3945
+ };
3946
+ const missingScopesOf = (error) => {
3947
+ const scopes = error.details.missingScopes;
3948
+ return typeof scopes === "string" && scopes !== "" ? scopes : "some scopes";
3949
+ };
3950
+ const apiErrorOf$1 = (error) => {
3951
+ if (error instanceof ApiError) return error;
3952
+ return error instanceof Error ? apiErrorOf$1(error.cause) : null;
3953
+ };
3954
+ const explained = (error) => {
3955
+ const failure = apiErrorOf$1(error);
3956
+ switch (failure?.code) {
3957
+ case "provider.token_scopes_missing": return new CliError(failure.message, `The token lacks ${missingScopesOf(failure)}. kcloud providers add --help lists every scope it needs.`, { cause: failure });
3958
+ case "provider.unavailable": return new CliError(failure.message, `DigitalOcean did not answer as expected${reasonOf$1(failure)}. Try again in a moment.`, { cause: failure });
3959
+ default: return error;
3960
+ }
3961
+ };
3962
+ const providerDataOf = (action, answer, hints = providerHints) => {
3963
+ try {
3964
+ return dataOf(action, answer, hints);
3965
+ } catch (error) {
3966
+ throw explained(error);
3967
+ }
3968
+ };
3969
+ const providerDoneWith = (action, answer, hints = providerHints) => {
3970
+ try {
3971
+ doneWith(action, answer, hints);
3972
+ } catch (error) {
3973
+ throw explained(error);
3974
+ }
3975
+ };
3976
+ const providerAccountsOf = async (api, organizationId) => providerDataOf("list the provider accounts", await api.GET("/organizations/{organizationId}/provider-accounts", { params: { path: { organizationId } } })).items;
3977
+ const providerAccountOf = async (api, organizationId, accountId) => providerDataOf("describe the provider account", await api.GET("/organizations/{organizationId}/provider-accounts/{accountId}", { params: { path: {
3978
+ organizationId,
3979
+ accountId
3980
+ } } }));
3981
+ const resolveProviderAccount = async (scope, reference) => {
3982
+ const organization = await scope.organization();
3983
+ if (isId(reference)) return providerAccountOf(scope.api, organization.id, reference);
3984
+ const accounts = await providerAccountsOf(scope.api, organization.id);
3985
+ const found = accounts.find((account) => account.name === reference);
3986
+ if (found === void 0) throw new CliError(`There is no provider account ${reference} in ${organization.slug}.`, accounts.length === 0 ? "kcloud providers add <name> --region <slug> adds one." : `kcloud providers list shows ${accounts.map(({ name }) => name).join(", ")}.`);
3987
+ return found;
3988
+ };
3989
+ const verifiedAccount = async (api, organizationId, accountId) => providerDataOf("verify the provider account", await api.POST("/organizations/{organizationId}/provider-accounts/{accountId}/verification", { params: { path: {
3990
+ organizationId,
3991
+ accountId
3992
+ } } }));
3993
+ const providerCatalogOf = async (api, organizationId, accountId) => providerDataOf("read the regions and sizes of the provider", await api.GET("/organizations/{organizationId}/provider-accounts/{accountId}/catalog", { params: { path: {
3994
+ organizationId,
3995
+ accountId
3996
+ } } }));
3997
+ const catalogInRegion = (catalog, region) => {
3998
+ if (!catalog.regions.some(({ slug }) => slug === region)) throw new CliError(`The account offers no region ${region} for a node.`, `Its regions are ${catalog.regions.map(({ slug }) => slug).join(", ")}.`);
3999
+ return {
4000
+ region,
4001
+ regions: catalog.regions,
4002
+ sizes: catalog.sizes.filter(({ regions }) => regions.includes(region))
4003
+ };
4004
+ };
4005
+ const deleteProviderAccount = async (api, organizationId, accountId) => {
4006
+ providerDoneWith("delete the provider account", await api.DELETE("/organizations/{organizationId}/provider-accounts/{accountId}", { params: { path: {
4007
+ organizationId,
4008
+ accountId
4009
+ } } }));
4010
+ };
4011
+ const verificationText = ({ failure, missingScopes }) => {
4012
+ if (failure === "unauthorized") return "token refused";
4013
+ if (failure !== null) return `not checked: ${failure}`;
4014
+ return missingScopes.length === 0 ? "ok" : `missing ${missingScopes.length} of ${providerTokenScopes.length} scopes`;
4015
+ };
4016
+ const scopeLineWidth = 64;
4017
+ const scopeLines = () => providerTokenScopes.reduce((lines, scope) => {
4018
+ const last = lines.at(-1);
4019
+ if (last !== void 0 && last.length + scope.length + 2 <= scopeLineWidth) {
4020
+ lines[lines.length - 1] = `${last}, ${scope}`;
4021
+ return lines;
4022
+ }
4023
+ return [...lines, scope];
4024
+ }, []);
4025
+ //#endregion
4026
+ //#region src/commands/nodes/shared.ts
4027
+ const nodeHints = {
4028
+ ...organizationHints,
4029
+ "node.not_found": "kcloud nodes list shows the nodes of the organization.",
4030
+ "node.name_taken": "Another node of the organization has that name.",
4031
+ "node.in_use": "Delete the agents placed on it first; kcloud nodes show <node> lists them.",
4032
+ "node.registration_used": "The node has joined and holds its own credential; it needs no token.",
4033
+ "node.machine_missing": "The node runs on a host of its own, which the platform does not resize.",
4034
+ "node.operation_running": "The platform is still working on the node; kcloud nodes show <node> shows the step.",
4035
+ "node.operation_not_failed": "Only a failed operation can be retried; kcloud nodes show <node> shows the operation.",
4036
+ "node.region_unavailable": "kcloud providers catalog <account> lists the regions a node can go to.",
4037
+ "node.size_unavailable": "kcloud providers catalog <account> --region <slug> lists the sizes of a region.",
4038
+ "node.size_not_larger": "A node only grows: name a size with more CPUs or memory, or a larger volume.",
4039
+ "concurrency.conflict": "Someone changed the node meanwhile; run the command again."
4040
+ };
4041
+ const machineHints = {
4042
+ ...providerHints,
4043
+ ...nodeHints,
4044
+ "auth.forbidden": "Creating, resizing and deleting nodes take an admin of the organization."
4045
+ };
4046
+ const splitHints = {
4047
+ ...nodeHints,
4048
+ "auth.forbidden": "Splitting a node takes an admin of its organization.",
4049
+ "node.host_unknown": "The node reports its CPUs and memory once its service runs; kcloud nodes show <node> shows its state.",
4050
+ "node.split_exceeds_host": "The split asks for more than the host has; kcloud nodes split <node> shows the host."
4051
+ };
4052
+ const nodesOf = async (api, organizationId) => dataOf("list the nodes", await api.GET("/organizations/{organizationId}/nodes", { params: { path: { organizationId } } }), nodeHints).items;
4053
+ const namesOf = (nodes) => nodes.map(({ name }) => name).join(", ");
4054
+ const nodeIn = (nodes, reference, organization) => {
4055
+ const found = nodes.find((node) => node.id === reference.toLowerCase() || node.name === reference);
4056
+ 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)}.`);
4057
+ return found;
4058
+ };
4059
+ const resolveNode = async (scope, reference) => {
4060
+ const organization = await scope.organization();
4061
+ if (isId(reference)) return {
4062
+ organizationId: organization.id,
4063
+ nodeId: reference
4064
+ };
4065
+ const node = nodeIn(await nodesOf(scope.api, organization.id), reference, organization.slug);
4066
+ return {
4067
+ organizationId: organization.id,
4068
+ nodeId: node.id
4069
+ };
4070
+ };
4071
+ const nodeDetailOf = async (api, { organizationId, nodeId }) => dataOf("describe the node", await api.GET("/organizations/{organizationId}/nodes/{nodeId}", { params: { path: {
4072
+ organizationId,
4073
+ nodeId
4074
+ } } }), nodeHints);
4075
+ const createdNode = async (api, organizationId, body) => {
4076
+ validated(createNodeBodySchema, body, "a node");
4077
+ return providerDataOf("create the node", await api.POST("/organizations/{organizationId}/nodes", {
4078
+ params: { path: { organizationId } },
4079
+ body
4080
+ }), machineHints);
4081
+ };
4082
+ const renewedRegistration = async (api, { organizationId, nodeId }) => dataOf("issue a registration token for the node", await api.POST("/organizations/{organizationId}/nodes/{nodeId}/registration", { params: { path: {
4083
+ organizationId,
4084
+ nodeId
4085
+ } } }), nodeHints);
4086
+ const deleteNode = async (api, { organizationId, nodeId }) => {
4087
+ const answer = await api.DELETE("/organizations/{organizationId}/nodes/{nodeId}", { params: { path: {
4088
+ organizationId,
4089
+ nodeId
4090
+ } } });
4091
+ providerDoneWith("delete the node", answer, machineHints);
4092
+ return answer.response.status === 202 ? answer.data ?? null : null;
4093
+ };
4094
+ const resizeTargetOf = (node, { size, volumeGb }) => {
4095
+ const { machine } = node;
4096
+ if (machine === null) throw new CliError(`${node.name} runs on a host of its own, so the platform cannot resize it.`, "Only a node created with --provider digitalocean can be resized.");
4097
+ const target = {
4098
+ size: size ?? machine.size,
4099
+ volumeGb: volumeGb ?? machine.volumeGb
4100
+ };
4101
+ if (target.volumeGb < machine.volumeGb) throw new CliError(`The volume of ${node.name} holds ${machine.volumeGb} GB, and a volume cannot shrink.`);
4102
+ if (target.size === machine.size && target.volumeGb === machine.volumeGb) throw new CliError(`${node.name} is ${machine.size} with a volume of ${machine.volumeGb} GB already.`, "Name a larger size, a larger volume or both.");
4103
+ return target;
4104
+ };
4105
+ const resizedNode = async (api, { organizationId, nodeId }, node, change) => {
4106
+ const body = {
4107
+ revision: node.revision,
4108
+ ...resizeTargetOf(node, change)
4109
+ };
4110
+ validated(resizeNodeBodySchema, body, "a size of a node");
4111
+ return providerDataOf("resize the node", await api.POST("/organizations/{organizationId}/nodes/{nodeId}/resize", {
4112
+ params: { path: {
4113
+ organizationId,
4114
+ nodeId
4115
+ } },
4116
+ body
4117
+ }), machineHints);
4118
+ };
4119
+ const retriedOperation = async (api, { organizationId, nodeId }, revision) => providerDataOf("retry the operation of the node", await api.POST("/organizations/{organizationId}/nodes/{nodeId}/operation/retry", {
4120
+ params: { path: {
4121
+ organizationId,
4122
+ nodeId
4123
+ } },
4124
+ body: { revision }
4125
+ }), machineHints);
4126
+ 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", {
4127
+ params: { path: {
4128
+ organizationId,
4129
+ nodeId
4130
+ } },
4131
+ body: {
4132
+ revision,
4133
+ split
4134
+ }
4135
+ }), splitHints);
4136
+ const nodeUsage = [["", "a node is named by its name or its id"]];
4137
+ //#endregion
4138
+ //#region src/commands/projects/shared.ts
4139
+ const defaultProjectColor = "#7c86ff";
4140
+ const projectHint = {
4141
+ "project.not_found": "kcloud projects list shows the projects you are a member of.",
4142
+ "project.slug_taken": "Another project of the organization has that slug.",
4143
+ "project.invalid_slug": "A slug holds lowercase letters, digits and hyphens.",
4144
+ "project.not_empty": "Delete its agents, credentials, repositories and tools first.",
4145
+ "project.member_not_found": "kcloud projects members list <project> shows them.",
4146
+ "project.member_must_be_active": "kcloud orgs members enable <member> enables the account first."
4147
+ };
4148
+ const projectOf = async (api, id) => dataOf("describe the project", await api.GET("/projects/{id}", { params: { path: { id } } }), projectHint);
4149
+ //#endregion
3647
4150
  //#region src/commands/run.ts
3648
4151
  const runOf = async (adaptersOf, { settings, context }) => {
3649
4152
  const adapters = adaptersOf(context);
@@ -3682,6 +4185,161 @@ const agentFrom = async (run, reference, project) => {
3682
4185
  return resolveAgentId(run, reference, project);
3683
4186
  };
3684
4187
  //#endregion
4188
+ //#region src/commands/agents/backups.ts
4189
+ const backupsListHint = "kcloud agents backups list <agent> shows the backups of the agent and their ids.";
4190
+ const backupHints = {
4191
+ ...agentHints,
4192
+ "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.",
4193
+ "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.",
4194
+ "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.",
4195
+ "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."
4196
+ };
4197
+ const restoreHints = {
4198
+ ...agentHints,
4199
+ "node.not_found": "kcloud nodes list shows the nodes of the organization.",
4200
+ "node.offline": "The node is not connected. kcloud nodes show <node> tells when it was last seen.",
4201
+ "agent.backup_not_found": `The backup does not exist or has expired. ${backupsListHint}`,
4202
+ "agent.slug_taken": "Another agent of the project has that slug. Pass another with --slug.",
4203
+ "agent.invalid_slug": "A slug holds lowercase letters, digits and hyphens."
4204
+ };
4205
+ const backupsOf = async (api, agentId) => dataOf("list the backups of the agent", await api.GET("/agents/{id}/backups", { params: { path: { id: agentId } } }), backupHints);
4206
+ const requestedBackup = async (api, agentId) => dataOf("back the agent up", await api.POST("/agents/{id}/backups", { params: { path: { id: agentId } } }), backupHints);
4207
+ const nodeIdFor = async (api, agentId, node) => {
4208
+ if (isId(node)) return node;
4209
+ const agent = dataOf("describe the agent", await api.GET("/agents/{id}", { params: { path: { id: agentId } } }), agentHints);
4210
+ const project = await projectOf(api, agent.projectId);
4211
+ const nodes = await nodesOf(api, project.organizationId);
4212
+ return nodeIn(nodes, node, `the organization of ${project.slug}`).id;
4213
+ };
4214
+ const restoredAgent = async (api, agentId, { backupId, name, node, slug }) => {
4215
+ const nodeId = await nodeIdFor(api, agentId, node);
4216
+ return dataOf("restore the agent from the backup", await api.POST("/agents/{id}/backups/{backupId}/restore", {
4217
+ params: { path: {
4218
+ id: agentId,
4219
+ backupId
4220
+ } },
4221
+ body: {
4222
+ name,
4223
+ nodeId,
4224
+ ...slug === void 0 ? {} : { slug }
4225
+ }
4226
+ }), restoreHints);
4227
+ };
4228
+ const keptFor = (days) => `A backup is kept for ${String(days)} ${days === 1 ? "day" : "days"}.`;
4229
+ const backupsTable = ({ items, retentionDays }) => items.length === 0 ? `The agent has no backup yet. ${keptFor(retentionDays)}` : [renderTable([
4230
+ { header: "CREATED" },
4231
+ { header: "TRIGGER" },
4232
+ { header: "NODE" },
4233
+ { header: "KIKIMORA" },
4234
+ {
4235
+ header: "ENTRIES",
4236
+ align: "right"
4237
+ },
4238
+ {
4239
+ header: "SIZE",
4240
+ align: "right"
4241
+ },
4242
+ { header: "EXPIRES" },
4243
+ { header: "ID" }
4244
+ ], items.map((backup) => [
4245
+ backup.createdAt,
4246
+ backup.trigger,
4247
+ backup.nodeName ?? backup.nodeId,
4248
+ backup.kikimoraVersion,
4249
+ String(backup.entries),
4250
+ bytesOf(backup.sizeBytes),
4251
+ backup.expiresAt,
4252
+ backup.id
4253
+ ])), keptFor(retentionDays)].join("\n");
4254
+ const restoreRequestOf = ({ backup, name, node, slug }) => {
4255
+ if (backup === void 0 || name === void 0 || node === void 0 || node.trim() === "") {
4256
+ const missing = [
4257
+ ...backup === void 0 ? ["--backup <id>"] : [],
4258
+ ...name === void 0 ? ["--name <name>"] : [],
4259
+ ...node === void 0 || node.trim() === "" ? ["--node <node>"] : []
4260
+ ];
4261
+ throw new UsageError(`agents restore needs ${listOf(missing)}.`, backupsListHint);
4262
+ }
4263
+ if (!isId(backup)) throw new UsageError(`--backup ${backup} is not the id of a backup.`, backupsListHint);
4264
+ const named = agentNameSchema.safeParse(name);
4265
+ if (!named.success) throw new UsageError(`--name needs from 1 to ${String(80)} characters.`);
4266
+ 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.");
4267
+ return {
4268
+ backupId: backup,
4269
+ name: named.data,
4270
+ node: node.trim(),
4271
+ slug
4272
+ };
4273
+ };
4274
+ const backupsCommand = (adaptersOf) => defineGroup({
4275
+ name: "backups",
4276
+ summary: "The backups of the memory of an agent.",
4277
+ usage: "agents backups <command>",
4278
+ subcommands: [defineCommand({
4279
+ name: "list",
4280
+ summary: "List the backups of an agent, the newest first.",
4281
+ usage: "agents backups list <agent> [--project <name>]",
4282
+ optionsUsage: agentUsage,
4283
+ options: agentOption,
4284
+ positionals: {
4285
+ min: 1,
4286
+ max: 1
4287
+ },
4288
+ run: async (invocation) => {
4289
+ const run = await runOf(adaptersOf, invocation);
4290
+ const agentId = await agentFrom(run, invocation.positionals[0], invocation.values.project);
4291
+ return printed(run, await backupsOf(run.api, agentId), backupsTable);
4292
+ }
4293
+ }), defineCommand({
4294
+ name: "create",
4295
+ summary: "Ask the node of an agent for a backup now.",
4296
+ usage: "agents backups create <agent> [--project <name>]",
4297
+ optionsUsage: agentUsage,
4298
+ options: agentOption,
4299
+ positionals: {
4300
+ min: 1,
4301
+ max: 1
4302
+ },
4303
+ run: async (invocation) => {
4304
+ const run = await runOf(adaptersOf, invocation);
4305
+ const reference = invocation.positionals[0] ?? "";
4306
+ const agentId = await agentFrom(run, reference, invocation.values.project);
4307
+ 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.`);
4308
+ }
4309
+ })]
4310
+ });
4311
+ const restoreCommand = (adaptersOf) => defineCommand({
4312
+ name: "restore",
4313
+ summary: "Create a new agent from a backup of an agent.",
4314
+ usage: "agents restore <agent> --backup <id> --name <name> --node <node> [--slug <slug>] [--project <name>]",
4315
+ optionsUsage: [
4316
+ ...agentUsage,
4317
+ ["--backup <id>", "the backup, by its id from kcloud agents backups list"],
4318
+ ["--name <name>", "the name of the new agent"],
4319
+ ["--node <node>", "the node the new agent runs on, by name or id"],
4320
+ ["--slug <slug>", "the slug of the new agent (default: one made from its name)"],
4321
+ ["", "the new agent joins the project of the agent with its configuration and the memory of the backup"]
4322
+ ],
4323
+ options: {
4324
+ ...agentOption,
4325
+ backup: { type: "string" },
4326
+ name: { type: "string" },
4327
+ node: { type: "string" },
4328
+ slug: { type: "string" }
4329
+ },
4330
+ positionals: {
4331
+ min: 1,
4332
+ max: 1
4333
+ },
4334
+ run: async (invocation) => {
4335
+ const request = restoreRequestOf(invocation.values);
4336
+ const run = await runOf(adaptersOf, invocation);
4337
+ const reference = invocation.positionals[0] ?? "";
4338
+ const agentId = await agentFrom(run, reference, invocation.values.project);
4339
+ 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"));
4340
+ }
4341
+ });
4342
+ //#endregion
3685
4343
  //#region src/commands/agents/check.ts
3686
4344
  const checkHints = {
3687
4345
  ...agentHints,
@@ -3709,36 +4367,6 @@ const checkCommand = (adaptersOf) => defineCommand({
3709
4367
  }
3710
4368
  });
3711
4369
  //#endregion
3712
- //#region src/render/body.ts
3713
- const isTerminal$1 = (stream) => stream.isTTY === true;
3714
- const pipedText$1 = async (stream) => {
3715
- const chunks = [];
3716
- for await (const chunk of stream) chunks.push(Buffer.from(chunk));
3717
- return Buffer.concat(chunks).toString("utf8");
3718
- };
3719
- const fileOptions = { file: { type: "string" } };
3720
- const fileUsage = (what) => [["--file <path>", `read ${what} from a JSON file, or from stdin with -`]];
3721
- const documentOf = async (run, file) => {
3722
- const text = file === "-" ? isTerminal$1(run.context.io.stdin) ? "" : await pipedText$1(run.context.io.stdin) : await readFileOrRefuse(resolve(run.context.workingDirectory, file));
3723
- if (text.trim() === "") throw new UsageError(file === "-" ? "Nothing is piped on stdin." : `${file} is empty.`);
3724
- try {
3725
- return JSON.parse(text);
3726
- } catch {
3727
- throw new CliError(`${file === "-" ? "stdin" : file} is not JSON.`);
3728
- }
3729
- };
3730
- const readFileOrRefuse = async (path) => {
3731
- try {
3732
- return await readFile(path, "utf8");
3733
- } catch {
3734
- throw new CliError(`${path} could not be read.`);
3735
- }
3736
- };
3737
- const validated = (schema, document, what) => {
3738
- const parsed = schema.safeParse(document);
3739
- if (!parsed.success) throw new CliError(`That is not ${what}.`, z.prettifyError(parsed.error));
3740
- };
3741
- //#endregion
3742
4370
  //#region src/render/confirm.ts
3743
4371
  const confirmed = async ({ prompt, settings, question }) => {
3744
4372
  if (settings.yes) return;
@@ -4042,86 +4670,6 @@ const detailsOf = (agent) => [
4042
4670
  ]))
4043
4671
  ].join("\n");
4044
4672
  //#endregion
4045
- //#region src/commands/nodes/shared.ts
4046
- const nodeHints = {
4047
- ...organizationHints,
4048
- "node.not_found": "kcloud nodes list shows the nodes of the organization.",
4049
- "node.name_taken": "Another node of the organization has that name.",
4050
- "node.in_use": "Delete the agents placed on it first; kcloud nodes show <node> lists them.",
4051
- "node.registration_used": "The node has joined and holds its own credential; it needs no token."
4052
- };
4053
- const splitHints = {
4054
- ...nodeHints,
4055
- "auth.forbidden": "Splitting a node takes an admin of its organization.",
4056
- "node.host_unknown": "The node reports its CPUs and memory once its service runs; kcloud nodes show <node> shows its state.",
4057
- "node.split_exceeds_host": "The split asks for more than the host has; kcloud nodes split <node> shows the host.",
4058
- "concurrency.conflict": "Someone changed the node meanwhile; run the command again."
4059
- };
4060
- const nodesOf = async (api, organizationId) => dataOf("list the nodes", await api.GET("/organizations/{organizationId}/nodes", { params: { path: { organizationId } } }), nodeHints).items;
4061
- const namesOf = (nodes) => nodes.map(({ name }) => name).join(", ");
4062
- const nodeIn = (nodes, reference, organization) => {
4063
- const found = nodes.find((node) => node.id === reference.toLowerCase() || node.name === reference);
4064
- 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)}.`);
4065
- return found;
4066
- };
4067
- const resolveNode = async (scope, reference) => {
4068
- const organization = await scope.organization();
4069
- if (isId(reference)) return {
4070
- organizationId: organization.id,
4071
- nodeId: reference
4072
- };
4073
- const node = nodeIn(await nodesOf(scope.api, organization.id), reference, organization.slug);
4074
- return {
4075
- organizationId: organization.id,
4076
- nodeId: node.id
4077
- };
4078
- };
4079
- const nodeDetailOf = async (api, { organizationId, nodeId }) => dataOf("describe the node", await api.GET("/organizations/{organizationId}/nodes/{nodeId}", { params: { path: {
4080
- organizationId,
4081
- nodeId
4082
- } } }), nodeHints);
4083
- const createdNode = async (api, organizationId, name) => {
4084
- const body = { name };
4085
- validated(createNodeBodySchema, body, "a node");
4086
- return dataOf("create the node", await api.POST("/organizations/{organizationId}/nodes", {
4087
- params: { path: { organizationId } },
4088
- body
4089
- }), nodeHints);
4090
- };
4091
- const renewedRegistration = async (api, { organizationId, nodeId }) => dataOf("issue a registration token for the node", await api.POST("/organizations/{organizationId}/nodes/{nodeId}/registration", { params: { path: {
4092
- organizationId,
4093
- nodeId
4094
- } } }), nodeHints);
4095
- const deleteNode = async (api, { organizationId, nodeId }) => {
4096
- doneWith("delete the node", await api.DELETE("/organizations/{organizationId}/nodes/{nodeId}", { params: { path: {
4097
- organizationId,
4098
- nodeId
4099
- } } }), nodeHints);
4100
- };
4101
- const splitNode = async (api, { organizationId, nodeId }, revision, split) => dataOf(split === null ? "clear the split of the node" : "split the node", await api.PUT("/organizations/{organizationId}/nodes/{nodeId}/split", {
4102
- params: { path: {
4103
- organizationId,
4104
- nodeId
4105
- } },
4106
- body: {
4107
- revision,
4108
- split
4109
- }
4110
- }), splitHints);
4111
- const nodeUsage = [["", "a node is named by its name or its id"]];
4112
- //#endregion
4113
- //#region src/commands/projects/shared.ts
4114
- const defaultProjectColor = "#7c86ff";
4115
- const projectHint = {
4116
- "project.not_found": "kcloud projects list shows the projects you are a member of.",
4117
- "project.slug_taken": "Another project of the organization has that slug.",
4118
- "project.invalid_slug": "A slug holds lowercase letters, digits and hyphens.",
4119
- "project.not_empty": "Delete its agents, credentials, repositories and tools first.",
4120
- "project.member_not_found": "kcloud projects members list <project> shows them.",
4121
- "project.member_must_be_active": "kcloud orgs members enable <member> enables the account first."
4122
- };
4123
- const projectOf = async (api, id) => dataOf("describe the project", await api.GET("/projects/{id}", { params: { path: { id } } }), projectHint);
4124
- //#endregion
4125
4673
  //#region src/commands/agents/placement.ts
4126
4674
  const placedNodeId = async (run, { projectId, given, ask }) => {
4127
4675
  const project = await projectOf(run.api, projectId);
@@ -5156,6 +5704,8 @@ const createAgentsCommand = (adaptersOf) => defineGroup({
5156
5704
  ...lifecycleCommands(adaptersOf),
5157
5705
  checkCommand(adaptersOf),
5158
5706
  tasksCommand(adaptersOf),
5707
+ backupsCommand(adaptersOf),
5708
+ restoreCommand(adaptersOf),
5159
5709
  ...observabilityCommands(adaptersOf),
5160
5710
  ...streamCommands(adaptersOf)
5161
5711
  ]
@@ -5788,75 +6338,6 @@ const agentTools = [
5788
6338
  })
5789
6339
  ];
5790
6340
  //#endregion
5791
- //#region src/mcp/tools/credentials.ts
5792
- const credentialField$1 = z.string().trim().min(1).describe("The credential, by its label or its id.");
5793
- const credentialTools = [
5794
- defineTool({
5795
- name: "credentials_list",
5796
- title: "List credentials",
5797
- description: "List the Claude and GitHub credentials of a project, with their fingerprints and verification, never their secrets.",
5798
- operations: ["listCredentials"],
5799
- safety: "read",
5800
- input: z.object({ project: projectField }),
5801
- run: async ({ project }, run) => credentialsOf(run.api, await projectIdOf(run, project))
5802
- }),
5803
- defineTool({
5804
- name: "credential_update",
5805
- title: "Rename or verify a credential",
5806
- description: "rename gives a credential a new label; verify asks the instance to check it against its service, and credentials_list shows the answer.",
5807
- operations: ["renameCredential", "verifyCredential"],
5808
- safety: "write",
5809
- input: z.object({
5810
- project: projectField,
5811
- credential: credentialField$1,
5812
- action: z.enum(["rename", "verify"]),
5813
- label: z.string().trim().min(1).optional().describe("The new label, for rename.")
5814
- }),
5815
- run: async ({ project, credential, action, label }, run) => {
5816
- const projectId = await projectIdOf(run, project);
5817
- const credentialId = await resolveCredentialId(run.api, projectId, credential);
5818
- if (action === "verify") {
5819
- doneWith("verify the credential", await run.api.POST("/projects/{id}/credentials/{credentialId}/verify", { params: { path: {
5820
- id: projectId,
5821
- credentialId
5822
- } } }), credentialHints);
5823
- return {
5824
- id: credentialId,
5825
- verifying: true
5826
- };
5827
- }
5828
- if (label === void 0) throw new UsageError("rename needs a label.");
5829
- return dataOf("rename the credential", await run.api.PATCH("/projects/{id}/credentials/{credentialId}", {
5830
- params: { path: {
5831
- id: projectId,
5832
- credentialId
5833
- } },
5834
- body: { label }
5835
- }), credentialHints);
5836
- }
5837
- }),
5838
- defineTool({
5839
- name: "credential_delete",
5840
- title: "Delete a credential",
5841
- description: "Delete a credential of a project that no agent and no repository uses.",
5842
- operations: ["deleteCredential"],
5843
- safety: "destructive",
5844
- input: z.object({
5845
- project: projectField,
5846
- credential: credentialField$1
5847
- }),
5848
- run: async ({ project, credential }, run) => {
5849
- const projectId = await projectIdOf(run, project);
5850
- const credentialId = await resolveCredentialId(run.api, projectId, credential);
5851
- doneWith("delete the credential", await run.api.DELETE("/projects/{id}/credentials/{credentialId}", { params: { path: {
5852
- id: projectId,
5853
- credentialId
5854
- } } }), credentialHints);
5855
- return { deleted: credentialId };
5856
- }
5857
- })
5858
- ];
5859
- //#endregion
5860
6341
  //#region src/commands/nodes/split.ts
5861
6342
  const splitFields = [
5862
6343
  "reserve.cpus",
@@ -6175,7 +6656,7 @@ const nodeTools = [
6175
6656
  defineTool({
6176
6657
  name: "nodes_list",
6177
6658
  title: "List nodes",
6178
- description: "List the nodes of the organization this server acts in, the hosts its agents run on, with their state, engine, version, capabilities, split and agent count.",
6659
+ description: "List the nodes of the organization this server acts in, the hosts its agents run on, with their provider, state, engine, version, capabilities, split, agent count, the DigitalOcean machine of a node created there, and the operation the platform runs on it.",
6179
6660
  operations: ["listNodes"],
6180
6661
  safety: "read",
6181
6662
  input: z.object({}),
@@ -6184,7 +6665,7 @@ const nodeTools = [
6184
6665
  defineTool({
6185
6666
  name: "node_show",
6186
6667
  title: "Show a node",
6187
- description: "Show one node with its host, its capabilities, its split, the agents placed on it with their pool limits, and the runtime and pools it reported last.",
6668
+ description: "Show one node with its provider, its DigitalOcean machine and the step and error of its operation, its host, its capabilities, its split, the agents placed on it with their pool limits, and the runtime and pools it reported last.",
6188
6669
  operations: ["getNode"],
6189
6670
  safety: "read",
6190
6671
  input: z.object({ node: nodeField }),
@@ -6228,16 +6709,161 @@ const nodeTools = [
6228
6709
  }
6229
6710
  }),
6230
6711
  defineTool({
6231
- name: "node_delete",
6232
- title: "Delete a node",
6233
- description: "Delete a node, after which its host can no longer reach the platform. The instance refuses while agents are placed on it.",
6234
- operations: ["deleteNode"],
6712
+ name: "node_resize",
6713
+ title: "Resize a node on DigitalOcean",
6714
+ description: "Give a node created on DigitalOcean a larger droplet size, a larger volume for its agents, or both; a field left out keeps its value, and a node never shrinks. A new size powers the droplet off, so its agents stop until it runs again. Returns the node with the operation that started; node_show follows its steps. Takes an admin of the organization.",
6715
+ operations: ["getNode", "resizeNode"],
6716
+ safety: "destructive",
6717
+ input: z.object({
6718
+ node: nodeField,
6719
+ size: providerSizeSlugSchema.optional().describe("The new droplet size, from provider_catalog."),
6720
+ volumeGb: nodeVolumeGbSchema.optional().describe("The new size of the volume in GB.")
6721
+ }),
6722
+ run: async ({ node, size, volumeGb }, run) => {
6723
+ if (size === void 0 && volumeGb === void 0) throw new CliError("Name a size, a volume or both.");
6724
+ const reference = await resolveNode(run, node);
6725
+ const detail = await nodeDetailOf(run.api, reference);
6726
+ return resizedNode(run.api, reference, detail, {
6727
+ size,
6728
+ volumeGb
6729
+ });
6730
+ }
6731
+ }),
6732
+ defineTool({
6733
+ name: "node_retry",
6734
+ title: "Retry the operation of a node",
6735
+ description: "Run the failed create, resize or delete operation of a node on DigitalOcean again from the step that failed. Returns the node; node_show follows its steps. Takes an admin of the organization.",
6736
+ operations: ["getNode", "retryNodeOperation"],
6737
+ safety: "write",
6738
+ input: z.object({ node: nodeField }),
6739
+ run: async ({ node }, run) => {
6740
+ const reference = await resolveNode(run, node);
6741
+ const detail = await nodeDetailOf(run.api, reference);
6742
+ return retriedOperation(run.api, reference, detail.revision);
6743
+ }
6744
+ }),
6745
+ defineTool({
6746
+ name: "node_delete",
6747
+ title: "Delete a node",
6748
+ description: "Delete a node, after which its host can no longer reach the platform. The instance refuses while agents are placed on it. A self-hosted node is deleted at once and returns deleted; for a node on DigitalOcean the platform destroys its droplet and its volume first and returns the node as removing.",
6749
+ operations: ["deleteNode"],
6750
+ safety: "destructive",
6751
+ input: z.object({ node: nodeField }),
6752
+ run: async ({ node }, run) => {
6753
+ const reference = await resolveNode(run, node);
6754
+ const removing = await deleteNode(run.api, reference);
6755
+ return removing === null ? { deleted: reference.nodeId } : { removing };
6756
+ }
6757
+ })
6758
+ ];
6759
+ //#endregion
6760
+ //#region src/mcp/tools/backups.ts
6761
+ const agentBackupTools = [
6762
+ defineTool({
6763
+ name: "agent_backups_list",
6764
+ title: "List the backups of an agent",
6765
+ 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.",
6766
+ operations: ["listAgentBackups"],
6767
+ safety: "read",
6768
+ input: z.object(agentFields),
6769
+ run: async (reference, run) => backupsOf(run.api, await agentIdOf(run, reference))
6770
+ }),
6771
+ defineTool({
6772
+ name: "agent_backup_request",
6773
+ title: "Back an agent up now",
6774
+ 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.",
6775
+ operations: ["requestAgentBackup"],
6776
+ safety: "write",
6777
+ input: z.object(agentFields),
6778
+ run: async (reference, run) => requestedBackup(run.api, await agentIdOf(run, reference))
6779
+ }),
6780
+ defineTool({
6781
+ name: "agent_restore",
6782
+ title: "Restore an agent from a backup",
6783
+ 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.",
6784
+ operations: ["restoreAgentFromBackup"],
6785
+ safety: "write",
6786
+ input: z.object({
6787
+ ...agentFields,
6788
+ backup: idSchema.describe("The id of the backup, from agent_backups_list."),
6789
+ name: agentNameSchema.describe("The name of the new agent."),
6790
+ slug: slugSchema.optional().describe("The slug of the new agent, instead of one made from its name."),
6791
+ node: nodeField
6792
+ }),
6793
+ run: async ({ backup, name, slug, node, ...reference }, run) => restoredAgent(run.api, await agentIdOf(run, reference), {
6794
+ backupId: backup,
6795
+ name,
6796
+ node,
6797
+ slug
6798
+ })
6799
+ })
6800
+ ];
6801
+ //#endregion
6802
+ //#region src/mcp/tools/credentials.ts
6803
+ const credentialField$1 = z.string().trim().min(1).describe("The credential, by its label or its id.");
6804
+ const credentialTools = [
6805
+ defineTool({
6806
+ name: "credentials_list",
6807
+ title: "List credentials",
6808
+ description: "List the Claude and GitHub credentials of a project, with their fingerprints and verification, never their secrets.",
6809
+ operations: ["listCredentials"],
6810
+ safety: "read",
6811
+ input: z.object({ project: projectField }),
6812
+ run: async ({ project }, run) => credentialsOf(run.api, await projectIdOf(run, project))
6813
+ }),
6814
+ defineTool({
6815
+ name: "credential_update",
6816
+ title: "Rename or verify a credential",
6817
+ description: "rename gives a credential a new label; verify asks the instance to check it against its service, and credentials_list shows the answer.",
6818
+ operations: ["renameCredential", "verifyCredential"],
6819
+ safety: "write",
6820
+ input: z.object({
6821
+ project: projectField,
6822
+ credential: credentialField$1,
6823
+ action: z.enum(["rename", "verify"]),
6824
+ label: z.string().trim().min(1).optional().describe("The new label, for rename.")
6825
+ }),
6826
+ run: async ({ project, credential, action, label }, run) => {
6827
+ const projectId = await projectIdOf(run, project);
6828
+ const credentialId = await resolveCredentialId(run.api, projectId, credential);
6829
+ if (action === "verify") {
6830
+ doneWith("verify the credential", await run.api.POST("/projects/{id}/credentials/{credentialId}/verify", { params: { path: {
6831
+ id: projectId,
6832
+ credentialId
6833
+ } } }), credentialHints);
6834
+ return {
6835
+ id: credentialId,
6836
+ verifying: true
6837
+ };
6838
+ }
6839
+ if (label === void 0) throw new UsageError("rename needs a label.");
6840
+ return dataOf("rename the credential", await run.api.PATCH("/projects/{id}/credentials/{credentialId}", {
6841
+ params: { path: {
6842
+ id: projectId,
6843
+ credentialId
6844
+ } },
6845
+ body: { label }
6846
+ }), credentialHints);
6847
+ }
6848
+ }),
6849
+ defineTool({
6850
+ name: "credential_delete",
6851
+ title: "Delete a credential",
6852
+ description: "Delete a credential of a project that no agent and no repository uses.",
6853
+ operations: ["deleteCredential"],
6235
6854
  safety: "destructive",
6236
- input: z.object({ node: nodeField }),
6237
- run: async ({ node }, run) => {
6238
- const reference = await resolveNode(run, node);
6239
- await deleteNode(run.api, reference);
6240
- return { deleted: reference.nodeId };
6855
+ input: z.object({
6856
+ project: projectField,
6857
+ credential: credentialField$1
6858
+ }),
6859
+ run: async ({ project, credential }, run) => {
6860
+ const projectId = await projectIdOf(run, project);
6861
+ const credentialId = await resolveCredentialId(run.api, projectId, credential);
6862
+ doneWith("delete the credential", await run.api.DELETE("/projects/{id}/credentials/{credentialId}", { params: { path: {
6863
+ id: projectId,
6864
+ credentialId
6865
+ } } }), credentialHints);
6866
+ return { deleted: credentialId };
6241
6867
  }
6242
6868
  })
6243
6869
  ];
@@ -7854,6 +8480,71 @@ const projectTools = [
7854
8480
  })
7855
8481
  ];
7856
8482
  //#endregion
8483
+ //#region src/mcp/tools/providers.ts
8484
+ const accountField = z.string().trim().min(1).describe("The provider account, by its name or its id, from providers_list.");
8485
+ const providerTools = [
8486
+ defineTool({
8487
+ name: "providers_list",
8488
+ title: "List provider accounts",
8489
+ description: "List the DigitalOcean accounts of the organization this server acts in, with their default region, the number of nodes created with each, and the last check of their token, never the token itself.",
8490
+ operations: ["listProviderAccounts"],
8491
+ safety: "read",
8492
+ input: z.object({}),
8493
+ run: async (_, run) => providerAccountsOf(run.api, (await run.organization()).id)
8494
+ }),
8495
+ defineTool({
8496
+ name: "provider_show",
8497
+ title: "Show a provider account",
8498
+ description: "Show one provider account with its default region, its node count and the last check of its token: when it ran, the scopes it lacked and why it failed.",
8499
+ operations: ["getProviderAccount"],
8500
+ safety: "read",
8501
+ input: z.object({ account: accountField }),
8502
+ run: async ({ account }, run) => resolveProviderAccount(run, account)
8503
+ }),
8504
+ defineTool({
8505
+ name: "provider_catalog",
8506
+ title: "List regions and sizes",
8507
+ description: "List the regions of a provider account that offer volumes, and the sizes with at least 2 GB of memory a node can have in one region, with CPUs, memory in MB, disk in GB and the monthly price in USD. The region defaults to the default region of the account.",
8508
+ operations: ["getProviderCatalog"],
8509
+ safety: "read",
8510
+ input: z.object({
8511
+ account: accountField,
8512
+ region: providerRegionSlugSchema.optional().describe("The region whose sizes to list, as in fra1.")
8513
+ }),
8514
+ run: async ({ account, region }, run) => {
8515
+ const found = await resolveProviderAccount(run, account);
8516
+ const catalog = await providerCatalogOf(run.api, (await run.organization()).id, found.id);
8517
+ return catalogInRegion(catalog, region ?? found.defaultRegion);
8518
+ }
8519
+ }),
8520
+ defineTool({
8521
+ name: "provider_verify",
8522
+ title: "Check the token of a provider account",
8523
+ description: "Check the token of a provider account against DigitalOcean again and return the account with the result: missingScopes lists the scopes it lacks, and failure is unauthorized when DigitalOcean refused it. Takes an admin of the organization.",
8524
+ operations: ["verifyProviderAccount"],
8525
+ safety: "write",
8526
+ idempotent: true,
8527
+ input: z.object({ account: accountField }),
8528
+ run: async ({ account }, run) => {
8529
+ const found = await resolveProviderAccount(run, account);
8530
+ return verifiedAccount(run.api, (await run.organization()).id, found.id);
8531
+ }
8532
+ }),
8533
+ defineTool({
8534
+ name: "provider_delete",
8535
+ title: "Delete a provider account",
8536
+ description: "Delete a provider account and erase its token. The instance refuses while nodes created with it exist. Takes an admin of the organization.",
8537
+ operations: ["deleteProviderAccount"],
8538
+ safety: "destructive",
8539
+ input: z.object({ account: accountField }),
8540
+ run: async ({ account }, run) => {
8541
+ const found = await resolveProviderAccount(run, account);
8542
+ await deleteProviderAccount(run.api, (await run.organization()).id, found.id);
8543
+ return { deleted: found.id };
8544
+ }
8545
+ })
8546
+ ];
8547
+ //#endregion
7857
8548
  //#region src/mcp/tools/releases.ts
7858
8549
  const releaseTools = [defineTool({
7859
8550
  name: "releases_list",
@@ -8261,8 +8952,10 @@ const kcloudTools = [
8261
8952
  ...credentialTools,
8262
8953
  ...repositoryTools,
8263
8954
  ...projectToolTools,
8955
+ ...providerTools,
8264
8956
  ...nodeTools,
8265
8957
  ...agentTools,
8958
+ ...agentBackupTools,
8266
8959
  ...taskTools,
8267
8960
  ...observabilityTools,
8268
8961
  ...notificationTools,
@@ -8277,7 +8970,7 @@ const instructions = [
8277
8970
  "These tools act on one Kikimora Cloud instance, as the account kcloud is signed in with, in one organization of it. whoami names all three.",
8278
8971
  "Name a project by its slug, its name or its id. Name an agent as <project>/<agent>, as its slug or name together with project, or by its id.",
8279
8972
  "A refusal quotes the error code of the instance. Its hint may name a kcloud command, such as kcloud agents list; the tool for the same resource and verb, agents_list, does that work here.",
8280
- "Secrets never pass through these tools. For them the person at the terminal runs kcloud credentials add, kcloud credentials rotate, kcloud tokens create, kcloud auth password, kcloud orgs members add, kcloud orgs members reset-password, or sets the secrets of a tool in the panel."
8973
+ "Secrets never pass through these tools. For them the person at the terminal runs kcloud credentials add, kcloud credentials rotate, kcloud providers add, kcloud providers update, kcloud tokens create, kcloud auth password, kcloud orgs members add, kcloud orgs members reset-password, or sets the secrets of a tool in the panel."
8281
8974
  ].join("\n");
8282
8975
  const createKcloudServer = ({ version, open }) => {
8283
8976
  const server = new McpServer({
@@ -8341,6 +9034,7 @@ const createMcpCommand = (adaptersOf, cliVersion) => defineCommand({
8341
9034
  });
8342
9035
  //#endregion
8343
9036
  //#region src/commands/nodes/index.ts
9037
+ const defaultVolumeGb = 40;
8344
9038
  const gibibytes = (bytes) => `${(bytes / 1024 ** 3).toFixed(1)} GiB`;
8345
9039
  const hostOf = (node) => node.host === null ? "-" : `${node.host.cpus} CPU, ${gibibytes(node.host.memoryBytes)} of memory`;
8346
9040
  const capabilitiesOf = (node) => {
@@ -8350,6 +9044,25 @@ const capabilitiesOf = (node) => {
8350
9044
  };
8351
9045
  const registrationOf = (node) => node.registrationExpiresAt === null ? "-" : `a token waits until ${node.registrationExpiresAt}`;
8352
9046
  const storageOf = ({ state, freeBytes, totalBytes, path }) => `${state}, ${gibibytes(freeBytes)} free of ${gibibytes(totalBytes)} at ${path}`;
9047
+ const shapeText = ({ size, volumeGb }) => `${size} with a volume of ${volumeGb} GB`;
9048
+ const machineText = (machine) => `${machine.size} in ${machine.region} with a volume of ${machine.volumeGb} GB`;
9049
+ const dropletText = ({ dropletId, publicIpv4 }) => dropletId === null ? "not created yet" : `${dropletId} at ${publicIpv4 ?? "no address yet"}`;
9050
+ const operationText = ({ kind, state, step }) => `${kind} ${state}${step === null ? "" : ` at ${step}`}`;
9051
+ const machineRowsOf = (machine) => machine === null ? [] : [
9052
+ ["machine", machineText(machine)],
9053
+ ["account", machine.account.name ?? machine.account.id],
9054
+ ["droplet", dropletText(machine)]
9055
+ ];
9056
+ const operationRowsOf = (operation) => {
9057
+ if (operation === null) return [];
9058
+ const { target, error } = operation;
9059
+ return [
9060
+ ["operation", operationText(operation)],
9061
+ ...target === null ? [] : [["target", shapeText(target)]],
9062
+ ["started", operation.startedAt],
9063
+ ...error === null ? [] : [["error", error]]
9064
+ ];
9065
+ };
8353
9066
  const runtimeRowsOf = (runtime) => [
8354
9067
  ["reported", runtime.reportedAt],
8355
9068
  ["docker", runtime.docker.lastError === null ? runtime.docker.state : `${runtime.docker.state}: ${runtime.docker.lastError}`],
@@ -8359,11 +9072,29 @@ const runtimeRowsOf = (runtime) => [
8359
9072
  ["agents", `${runtime.reconcile.agentsActive} active, ${runtime.reconcile.agentsQueued} queued`],
8360
9073
  ["log readers", String(runtime.ingest.readers)]
8361
9074
  ];
9075
+ const updateOf = ({ update }) => {
9076
+ if (update === null) return "not reported";
9077
+ switch (update.state) {
9078
+ case "current": return "current";
9079
+ case "waiting": return `waits for ${String(update.sessions)} ${update.sessions === 1 ? "session" : "sessions"} to end before ${update.target ?? "-"}`;
9080
+ case "updating": return `updating to ${update.target ?? "-"}`;
9081
+ case "failed": return `failed to reach ${update.target ?? "-"}, runs its previous version`;
9082
+ case "unmanaged": return "updated with its platform";
9083
+ }
9084
+ };
9085
+ const lastUpdateOf = ({ update }) => {
9086
+ const last = update?.last ?? null;
9087
+ if (last === null) return null;
9088
+ const outcome = `${last.outcome} ${last.version ?? "-"} at ${last.at}`;
9089
+ return last.reason === null ? outcome : `${outcome}: ${last.reason}`;
9090
+ };
8362
9091
  const nodeDetailsText = (node) => [
8363
9092
  renderTable([{ header: "FIELD" }, { header: "VALUE" }], [
8364
9093
  ["name", node.name],
8365
9094
  ["state", node.state],
8366
9095
  ["provider", node.provider],
9096
+ ...machineRowsOf(node.machine),
9097
+ ...operationRowsOf(node.operation),
8367
9098
  ["engine", node.engine ?? "-"],
8368
9099
  ["version", node.version ?? "-"],
8369
9100
  ["host", hostOf(node)],
@@ -8375,6 +9106,9 @@ const nodeDetailsText = (node) => [
8375
9106
  ["registration", registrationOf(node)],
8376
9107
  ["connected", node.connectedAt ?? "never"],
8377
9108
  ["last seen", node.lastSeenAt ?? "never"],
9109
+ ...node.offlineSince === null ? [] : [["offline since", node.offlineSince]],
9110
+ ["update", updateOf(node)],
9111
+ ...lastUpdateOf(node) === null ? [] : [["last update", lastUpdateOf(node) ?? ""]],
8378
9112
  ["created", node.createdAt],
8379
9113
  ["id", node.id]
8380
9114
  ]),
@@ -8399,11 +9133,13 @@ const nodeDetailsText = (node) => [
8399
9133
  poolsText(node.runtime.pools)
8400
9134
  ]
8401
9135
  ].join("\n");
8402
- const startHintOf = (origin) => [
8403
- "Start the node with it as docs/install.md of kikimora-cloud describes. On a Linux host, after read -rs token:",
8404
- ` printf '%s\\n' "$token" | /opt/kikimora-cloud/infra/node/install.sh --platform-url ${origin} --image ghcr.io/kikimoradev/kikimora-cloud/node:<version> --registration-token -`,
8405
- "On a Windows PC:",
8406
- ` C:\\kikimora-cloud\\src\\infra\\windows\\install.ps1 -PlatformUrl ${origin} -RegistrationToken <token>`
9136
+ const installCommandsOf = (origin) => [
9137
+ "Install the node with it, putting the token in place of <token>. On a Linux host:",
9138
+ ` curl -fsSLo install-node.sh ${origin}/install-node.sh`,
9139
+ ` printf '%s\\n' '<token>' | sudo sh install-node.sh --platform-url ${origin} --registration-token -`,
9140
+ "On Windows, in PowerShell as administrator:",
9141
+ ` Invoke-WebRequest -UseBasicParsing -OutFile install-node.ps1 ${origin}/install-node.ps1`,
9142
+ ` powershell -NoProfile -ExecutionPolicy Bypass -File .\\install-node.ps1 -PlatformUrl ${origin} -RegistrationToken <token>`
8407
9143
  ].join("\n");
8408
9144
  const printedRegistration = (run, created) => {
8409
9145
  if (run.settings.json) {
@@ -8411,9 +9147,56 @@ const printedRegistration = (run, created) => {
8411
9147
  return exitCodes.ok;
8412
9148
  }
8413
9149
  run.context.terminal.line(created.registration.token);
8414
- run.context.terminal.error([`The registration token of ${created.node.name} is shown only here and works once, until ${created.registration.expiresAt}.`, startHintOf(run.origin)].join("\n"));
9150
+ run.context.terminal.error([`The registration token of ${created.node.name} is shown only here and works once, until ${created.registration.expiresAt}.`, installCommandsOf(run.origin)].join("\n"));
8415
9151
  return exitCodes.ok;
8416
9152
  };
9153
+ const printedCreation = (run, created) => {
9154
+ const { node, registration } = created;
9155
+ if (registration !== null) return printedRegistration(run, {
9156
+ node,
9157
+ registration
9158
+ });
9159
+ return printed(run, created, () => [`Creating ${node.name} on DigitalOcean${node.machine === null ? "" : `, ${machineText(node.machine)}`}.`, `It joins the platform on its own; kcloud nodes show ${node.name} follows the steps.`].join("\n"));
9160
+ };
9161
+ const createOptions = {
9162
+ provider: { type: "string" },
9163
+ account: { type: "string" },
9164
+ region: { type: "string" },
9165
+ size: { type: "string" },
9166
+ "volume-gb": { type: "string" }
9167
+ };
9168
+ const volumeGbOf = (given) => {
9169
+ if (given === void 0) return void 0;
9170
+ if (!/^[0-9]+$/u.test(given)) throw new UsageError(`--volume-gb ${given} is not a whole number of gigabytes.`);
9171
+ return Number(given);
9172
+ };
9173
+ const machineFlags = [
9174
+ "account",
9175
+ "region",
9176
+ "size",
9177
+ "volume-gb"
9178
+ ];
9179
+ const nodeBodyOf = async (run, name, values) => {
9180
+ const provider = nodeProviders.find((candidate) => candidate === (values.provider ?? "self_hosted"));
9181
+ if (provider === void 0) throw new UsageError(`--provider ${values.provider ?? ""} is not a provider of a node.`, `The providers are ${nodeProviders.join(", ")}.`);
9182
+ if (provider === "self_hosted") {
9183
+ const given = machineFlags.filter((flag) => values[flag] !== void 0);
9184
+ if (given.length > 0) throw new UsageError(`${given.map((flag) => `--${flag}`).join(", ")} need --provider digitalocean.`);
9185
+ return { name };
9186
+ }
9187
+ if (values.account === void 0 || values.size === void 0) throw new UsageError("A node on DigitalOcean needs --account <account> and --size <slug>.", "kcloud providers catalog <account> lists the sizes.");
9188
+ const volumeGb = volumeGbOf(values["volume-gb"]) ?? defaultVolumeGb;
9189
+ const account = await resolveProviderAccount(run, values.account);
9190
+ return {
9191
+ name,
9192
+ machine: {
9193
+ accountId: account.id,
9194
+ region: values.region ?? account.defaultRegion,
9195
+ size: values.size,
9196
+ volumeGb
9197
+ }
9198
+ };
9199
+ };
8417
9200
  const createNodesCommand = (adaptersOf) => defineGroup({
8418
9201
  name: "nodes",
8419
9202
  summary: "The nodes of the organization, the hosts its agents run on.",
@@ -8434,7 +9217,9 @@ const createNodesCommand = (adaptersOf) => defineGroup({
8434
9217
  const items = await nodesOf(run.api, organization.id);
8435
9218
  return printed(run, items, (nodes) => nodes.length === 0 ? `${organization.slug} has no node yet; kcloud nodes create <name> creates one.` : renderTable([
8436
9219
  { header: "NAME" },
9220
+ { header: "PROVIDER" },
8437
9221
  { header: "STATE" },
9222
+ { header: "OPERATION" },
8438
9223
  { header: "ENGINE" },
8439
9224
  { header: "VERSION" },
8440
9225
  {
@@ -8445,7 +9230,9 @@ const createNodesCommand = (adaptersOf) => defineGroup({
8445
9230
  { header: "ID" }
8446
9231
  ], nodes.map((node) => [
8447
9232
  node.name,
9233
+ node.provider,
8448
9234
  node.state,
9235
+ node.operation === null ? "-" : operationText(node.operation),
8449
9236
  node.engine ?? "-",
8450
9237
  node.version ?? "-",
8451
9238
  String(node.agentCount),
@@ -8456,7 +9243,7 @@ const createNodesCommand = (adaptersOf) => defineGroup({
8456
9243
  }),
8457
9244
  defineCommand({
8458
9245
  name: "show",
8459
- summary: "Show a node with its host, its split, the agents placed on it, its runtime and its pools.",
9246
+ summary: "Show a node with its machine, its host, its split, its agents, its runtime and its pools.",
8460
9247
  usage: "nodes show <node>",
8461
9248
  optionsUsage: nodeUsage,
8462
9249
  options: {},
@@ -8473,10 +9260,18 @@ const createNodesCommand = (adaptersOf) => defineGroup({
8473
9260
  createSplitCommand(adaptersOf),
8474
9261
  defineCommand({
8475
9262
  name: "create",
8476
- summary: "Create a node and print its one-time registration token.",
8477
- usage: "nodes create <name>",
8478
- optionsUsage: [["", "the name holds lowercase letters, digits and hyphens"], ["", "the token goes to stdout alone, how to start the node to stderr"]],
8479
- options: {},
9263
+ summary: "Create a node on a host of your own, printing its one-time registration token, or on DigitalOcean.",
9264
+ usage: "nodes create <name> [--provider digitalocean --account <account> --size <slug>]",
9265
+ optionsUsage: [
9266
+ ["", "the name holds lowercase letters, digits and hyphens"],
9267
+ ["--provider <provider>", `one of ${nodeProviders.join(", ")} (default: self_hosted)`],
9268
+ ["", "self_hosted: the token goes to stdout alone, how to install to stderr"],
9269
+ ["--account <account>", "the DigitalOcean account, by name or id"],
9270
+ ["--size <slug>", "the droplet size, as kcloud providers catalog lists them"],
9271
+ ["--region <slug>", "the region (default: the region of the account)"],
9272
+ ["--volume-gb <n>", `the volume for the agents (default: ${defaultVolumeGb})`]
9273
+ ],
9274
+ options: createOptions,
8480
9275
  positionals: {
8481
9276
  min: 1,
8482
9277
  max: 1
@@ -8484,7 +9279,65 @@ const createNodesCommand = (adaptersOf) => defineGroup({
8484
9279
  run: async (invocation) => {
8485
9280
  const run = await runOf(adaptersOf, invocation);
8486
9281
  const organization = await run.organization();
8487
- return printedRegistration(run, await createdNode(run.api, organization.id, invocation.positionals[0] ?? ""));
9282
+ const body = await nodeBodyOf(run, invocation.positionals[0] ?? "", invocation.values);
9283
+ return printedCreation(run, await createdNode(run.api, organization.id, body));
9284
+ }
9285
+ }),
9286
+ defineCommand({
9287
+ name: "resize",
9288
+ summary: "Give a node on DigitalOcean a larger size, a larger volume or both.",
9289
+ usage: "nodes resize <node> [--size <slug>] [--volume-gb <n>]",
9290
+ optionsUsage: [
9291
+ ...nodeUsage,
9292
+ ["--size <slug>", "the new droplet size, with more CPUs or memory"],
9293
+ ["--volume-gb <n>", "the new size of the volume for the agents"],
9294
+ ["", "an option left out keeps its value; a node never shrinks"]
9295
+ ],
9296
+ options: {
9297
+ size: { type: "string" },
9298
+ "volume-gb": { type: "string" }
9299
+ },
9300
+ positionals: {
9301
+ min: 1,
9302
+ max: 1
9303
+ },
9304
+ run: async (invocation) => {
9305
+ const { values } = invocation;
9306
+ const change = {
9307
+ size: values.size,
9308
+ volumeGb: volumeGbOf(values["volume-gb"])
9309
+ };
9310
+ if (change.size === void 0 && change.volumeGb === void 0) throw new UsageError("nodes resize needs --size <slug>, --volume-gb <n> or both.");
9311
+ const run = await runOf(adaptersOf, invocation);
9312
+ const given = invocation.positionals[0] ?? "";
9313
+ const reference = await resolveNode(run, given);
9314
+ const node = await nodeDetailOf(run.api, reference);
9315
+ const target = resizeTargetOf(node, change);
9316
+ await confirmed({
9317
+ prompt: run.prompt,
9318
+ settings: run.settings,
9319
+ question: `Resize ${given} to ${shapeText(target)}? ${target.size === node.machine?.size ? "The volume cannot shrink afterwards." : "The droplet is off during the resize, so its agents stop until it runs again, and it cannot shrink afterwards."}`
9320
+ });
9321
+ const resized = await resizedNode(run.api, reference, node, change);
9322
+ return printed(run, resized, (detail) => `Resizing ${detail.name} to ${shapeText(target)}; kcloud nodes show ${detail.name} follows the steps.`);
9323
+ }
9324
+ }),
9325
+ defineCommand({
9326
+ name: "retry",
9327
+ summary: "Retry the failed operation of a node on DigitalOcean from the step that failed.",
9328
+ usage: "nodes retry <node>",
9329
+ optionsUsage: nodeUsage,
9330
+ options: {},
9331
+ positionals: {
9332
+ min: 1,
9333
+ max: 1
9334
+ },
9335
+ run: async (invocation) => {
9336
+ const run = await runOf(adaptersOf, invocation);
9337
+ const reference = await resolveNode(run, invocation.positionals[0] ?? "");
9338
+ const node = await nodeDetailOf(run.api, reference);
9339
+ const retried = await retriedOperation(run.api, reference, node.revision);
9340
+ return printed(run, retried, (detail) => detail.operation === null ? `${detail.name} has no operation left to run.` : `Retrying ${operationText(detail.operation)} of ${detail.name}; kcloud nodes show ${detail.name} follows the steps.`);
8488
9341
  }
8489
9342
  }),
8490
9343
  defineCommand({
@@ -8505,7 +9358,7 @@ const createNodesCommand = (adaptersOf) => defineGroup({
8505
9358
  }),
8506
9359
  defineCommand({
8507
9360
  name: "delete",
8508
- summary: "Delete a node that runs no agent.",
9361
+ summary: "Delete a node that runs no agent, with its droplet on DigitalOcean.",
8509
9362
  usage: "nodes delete <node>",
8510
9363
  optionsUsage: nodeUsage,
8511
9364
  options: {},
@@ -8517,13 +9370,300 @@ const createNodesCommand = (adaptersOf) => defineGroup({
8517
9370
  const run = await runOf(adaptersOf, invocation);
8518
9371
  const given = invocation.positionals[0] ?? "";
8519
9372
  const reference = await resolveNode(run, given);
9373
+ const node = await nodeDetailOf(run.api, reference);
9374
+ await confirmed({
9375
+ prompt: run.prompt,
9376
+ settings: run.settings,
9377
+ question: node.machine === null ? `Delete the node ${given}? Its host can no longer reach the platform.` : `Delete the node ${given}? The platform destroys its droplet and its volume with the data of its agents.`
9378
+ });
9379
+ const removing = await deleteNode(run.api, reference);
9380
+ if (removing === null) return said(run, `Deleted the node ${given}.`);
9381
+ return printed(run, removing, (removed) => `The removal of ${removed.name} started: the platform destroys its droplet, its volume and its firewall, then forgets the node. kcloud nodes list shows it until then.`);
9382
+ }
9383
+ })
9384
+ ]
9385
+ });
9386
+ //#endregion
9387
+ //#region src/commands/providers/index.ts
9388
+ const accountUsage = [["", "an account is named by its name or its id"]];
9389
+ const regionOption = { region: { type: "string" } };
9390
+ const tokenOf = async (run, name, values) => {
9391
+ const token = await secretOf({
9392
+ run,
9393
+ file: values["secret-file"],
9394
+ fromStdin: values["secret-stdin"],
9395
+ question: `The DigitalOcean token of ${name}`,
9396
+ stdinHint: "Pipe the token in, or drop --secret-stdin to be asked for it."
9397
+ });
9398
+ validated(providerTokenSchema, token, "a DigitalOcean token");
9399
+ return token;
9400
+ };
9401
+ const gigabytes = (megabytes) => `${megabytes / 1024} GB`;
9402
+ const accountDetailsText = (account) => [renderTable([{ header: "FIELD" }, { header: "VALUE" }], [
9403
+ ["name", account.name],
9404
+ ["kind", account.kind],
9405
+ ["default region", account.defaultRegion],
9406
+ ["nodes", String(account.nodeCount)],
9407
+ ["token", verificationText(account.verification)],
9408
+ ["checked", account.verification.checkedAt],
9409
+ ["created", account.createdAt],
9410
+ ["updated", account.updatedAt],
9411
+ ["id", account.id]
9412
+ ]), ...account.verification.missingScopes.length === 0 ? [] : ["", `The token lacks ${account.verification.missingScopes.join(", ")}.`]].join("\n");
9413
+ const verificationSentence = (account) => {
9414
+ const { failure, missingScopes } = account.verification;
9415
+ if (failure === "unauthorized") return `DigitalOcean refused the token of ${account.name}; kcloud providers update ${account.name} --token replaces it.`;
9416
+ if (failure !== null) return `The token of ${account.name} could not be checked: ${failure}.`;
9417
+ return missingScopes.length === 0 ? `The token of ${account.name} has every scope a node needs.` : `The token of ${account.name} lacks ${missingScopes.join(", ")}.`;
9418
+ };
9419
+ const catalogText = (catalog) => [
9420
+ renderTable([
9421
+ { header: "REGION" },
9422
+ { header: "NAME" },
9423
+ { header: "AVAILABLE" }
9424
+ ], catalog.regions.map((region) => [
9425
+ region.slug === catalog.region ? `${region.slug} *` : region.slug,
9426
+ region.name,
9427
+ region.available ? "yes" : "no"
9428
+ ])),
9429
+ "",
9430
+ catalog.sizes.length === 0 ? `No size for a node is offered in ${catalog.region}.` : renderTable([
9431
+ { header: `SIZE IN ${catalog.region.toUpperCase()}` },
9432
+ {
9433
+ header: "CPU",
9434
+ align: "right"
9435
+ },
9436
+ {
9437
+ header: "MEMORY",
9438
+ align: "right"
9439
+ },
9440
+ {
9441
+ header: "DISK",
9442
+ align: "right"
9443
+ },
9444
+ {
9445
+ header: "USD A MONTH",
9446
+ align: "right"
9447
+ }
9448
+ ], catalog.sizes.map((size) => [
9449
+ size.slug,
9450
+ String(size.vcpus),
9451
+ gigabytes(size.memoryMb),
9452
+ `${size.diskGb} GB`,
9453
+ size.priceMonthly.toFixed(2)
9454
+ ]))
9455
+ ].join("\n");
9456
+ const createProvidersCommand = (adaptersOf) => defineGroup({
9457
+ name: "providers",
9458
+ summary: "The cloud accounts the platform creates the nodes of the organization in.",
9459
+ usage: "providers <command>",
9460
+ subcommands: [
9461
+ defineCommand({
9462
+ name: "list",
9463
+ summary: "List the provider accounts of the organization.",
9464
+ usage: "providers list",
9465
+ options: {},
9466
+ positionals: {
9467
+ min: 0,
9468
+ max: 0
9469
+ },
9470
+ run: async (invocation) => {
9471
+ const run = await runOf(adaptersOf, invocation);
9472
+ const organization = await run.organization();
9473
+ const items = await providerAccountsOf(run.api, organization.id);
9474
+ return printed(run, items, (accounts) => accounts.length === 0 ? `${organization.slug} has no provider account yet; kcloud providers add <name> --region <slug> adds one.` : renderTable([
9475
+ { header: "NAME" },
9476
+ { header: "KIND" },
9477
+ { header: "REGION" },
9478
+ {
9479
+ header: "NODES",
9480
+ align: "right"
9481
+ },
9482
+ { header: "TOKEN" },
9483
+ { header: "ID" }
9484
+ ], accounts.map((account) => [
9485
+ account.name,
9486
+ account.kind,
9487
+ account.defaultRegion,
9488
+ String(account.nodeCount),
9489
+ verificationText(account.verification),
9490
+ account.id
9491
+ ])));
9492
+ }
9493
+ }),
9494
+ defineCommand({
9495
+ name: "show",
9496
+ summary: "Show a provider account with the state of its token.",
9497
+ usage: "providers show <account>",
9498
+ optionsUsage: accountUsage,
9499
+ options: {},
9500
+ positionals: {
9501
+ min: 1,
9502
+ max: 1
9503
+ },
9504
+ run: async (invocation) => {
9505
+ const run = await runOf(adaptersOf, invocation);
9506
+ const account = await resolveProviderAccount(run, invocation.positionals[0] ?? "");
9507
+ return printed(run, account, accountDetailsText);
9508
+ }
9509
+ }),
9510
+ defineCommand({
9511
+ name: "add",
9512
+ summary: "Add a DigitalOcean account, asking for its token.",
9513
+ usage: "providers add <name> --region <slug> [--secret-file <path>]",
9514
+ optionsUsage: [
9515
+ ["--region <slug>", "the region a new node goes to by default, as in fra1"],
9516
+ ...secretUsage,
9517
+ ["", "the instance checks the token before it stores it; the token needs"],
9518
+ ...scopeLines().map((line) => ["", ` ${line}`])
9519
+ ],
9520
+ options: {
9521
+ ...regionOption,
9522
+ ...secretOptions
9523
+ },
9524
+ positionals: {
9525
+ min: 1,
9526
+ max: 1
9527
+ },
9528
+ run: async (invocation) => {
9529
+ const { values } = invocation;
9530
+ const run = await runOf(adaptersOf, invocation);
9531
+ const organization = await run.organization();
9532
+ const name = invocation.positionals[0] ?? "";
9533
+ if (values.region === void 0) throw new UsageError("providers add needs --region <slug>.", "Name the region a new node goes to by default, as in fra1.");
9534
+ const draft = {
9535
+ kind: "digitalocean",
9536
+ name,
9537
+ defaultRegion: values.region
9538
+ };
9539
+ validated(createProviderAccountBodySchema.omit({ token: true }), draft, "a provider account");
9540
+ const token = await tokenOf(run, name, values);
9541
+ const account = providerDataOf("add the provider account", await run.api.POST("/organizations/{organizationId}/provider-accounts", {
9542
+ params: { path: { organizationId: organization.id } },
9543
+ body: {
9544
+ ...draft,
9545
+ token
9546
+ }
9547
+ }));
9548
+ return printed(run, account, (added) => `Added ${added.name} as ${added.id}; its token has every scope a node needs.`);
9549
+ }
9550
+ }),
9551
+ defineCommand({
9552
+ name: "update",
9553
+ summary: "Rename a provider account, change its default region or replace its token.",
9554
+ usage: "providers update <account> [--name <name>] [--region <slug>] [--token]",
9555
+ optionsUsage: [
9556
+ ...accountUsage,
9557
+ ["--name <name>", "the new name"],
9558
+ ["--region <slug>", "the new default region of new nodes"],
9559
+ ["--token", "replace the token, asking for the new one"],
9560
+ ...secretUsage,
9561
+ ["", `the new token needs the ${providerTokenScopes.length} scopes add lists`]
9562
+ ],
9563
+ options: {
9564
+ name: { type: "string" },
9565
+ ...regionOption,
9566
+ token: {
9567
+ type: "boolean",
9568
+ default: false
9569
+ },
9570
+ ...secretOptions
9571
+ },
9572
+ positionals: {
9573
+ min: 1,
9574
+ max: 1
9575
+ },
9576
+ run: async (invocation) => {
9577
+ const { values } = invocation;
9578
+ const replacesToken = values.token || values["secret-stdin"] || values["secret-file"] !== void 0;
9579
+ if (values.name === void 0 && values.region === void 0 && !replacesToken) throw new UsageError("providers update needs --name, --region or --token.");
9580
+ const run = await runOf(adaptersOf, invocation);
9581
+ const organization = await run.organization();
9582
+ const account = await resolveProviderAccount(run, invocation.positionals[0] ?? "");
9583
+ const change = {
9584
+ revision: account.revision,
9585
+ ...values.name === void 0 ? {} : { name: values.name },
9586
+ ...values.region === void 0 ? {} : { defaultRegion: values.region }
9587
+ };
9588
+ validated(createProviderAccountBodySchema.pick({
9589
+ name: true,
9590
+ defaultRegion: true
9591
+ }).partial(), change, "a change of a provider account");
9592
+ const body = replacesToken ? {
9593
+ ...change,
9594
+ token: await tokenOf(run, account.name, values)
9595
+ } : change;
9596
+ const updated = providerDataOf("update the provider account", await run.api.PATCH("/organizations/{organizationId}/provider-accounts/{accountId}", {
9597
+ params: { path: {
9598
+ organizationId: organization.id,
9599
+ accountId: account.id
9600
+ } },
9601
+ body
9602
+ }));
9603
+ return printed(run, updated, (changed) => `Updated ${changed.name}${replacesToken ? "; its new token has every scope a node needs" : ""}.`);
9604
+ }
9605
+ }),
9606
+ defineCommand({
9607
+ name: "verify",
9608
+ summary: "Check the token of a provider account against DigitalOcean again.",
9609
+ usage: "providers verify <account>",
9610
+ optionsUsage: accountUsage,
9611
+ options: {},
9612
+ positionals: {
9613
+ min: 1,
9614
+ max: 1
9615
+ },
9616
+ run: async (invocation) => {
9617
+ const run = await runOf(adaptersOf, invocation);
9618
+ const organization = await run.organization();
9619
+ const account = await resolveProviderAccount(run, invocation.positionals[0] ?? "");
9620
+ return printed(run, await verifiedAccount(run.api, organization.id, account.id), verificationSentence);
9621
+ }
9622
+ }),
9623
+ defineCommand({
9624
+ name: "catalog",
9625
+ summary: "List the regions of a provider account and the sizes a node can have in one.",
9626
+ usage: "providers catalog <account> [--region <slug>]",
9627
+ optionsUsage: [
9628
+ ...accountUsage,
9629
+ ["--region <slug>", "the region whose sizes to list (default: the account's)"],
9630
+ ["", "only regions with volumes and sizes with at least 2 GB of memory"]
9631
+ ],
9632
+ options: regionOption,
9633
+ positionals: {
9634
+ min: 1,
9635
+ max: 1
9636
+ },
9637
+ run: async (invocation) => {
9638
+ const run = await runOf(adaptersOf, invocation);
9639
+ const organization = await run.organization();
9640
+ const account = await resolveProviderAccount(run, invocation.positionals[0] ?? "");
9641
+ const catalog = await providerCatalogOf(run.api, organization.id, account.id);
9642
+ return printed(run, catalogInRegion(catalog, invocation.values.region ?? account.defaultRegion), catalogText);
9643
+ }
9644
+ }),
9645
+ defineCommand({
9646
+ name: "delete",
9647
+ summary: "Delete a provider account no node was created with.",
9648
+ usage: "providers delete <account>",
9649
+ optionsUsage: accountUsage,
9650
+ options: {},
9651
+ positionals: {
9652
+ min: 1,
9653
+ max: 1
9654
+ },
9655
+ run: async (invocation) => {
9656
+ const run = await runOf(adaptersOf, invocation);
9657
+ const organization = await run.organization();
9658
+ const given = invocation.positionals[0] ?? "";
9659
+ const account = await resolveProviderAccount(run, given);
8520
9660
  await confirmed({
8521
9661
  prompt: run.prompt,
8522
9662
  settings: run.settings,
8523
- question: `Delete the node ${given}? Its host can no longer reach the platform.`
9663
+ question: `Delete the provider account ${given}? The platform erases its token.`
8524
9664
  });
8525
- await deleteNode(run.api, reference);
8526
- return said(run, `Deleted the node ${given}.`);
9665
+ await deleteProviderAccount(run.api, organization.id, account.id);
9666
+ return said(run, `Deleted the provider account ${given}.`);
8527
9667
  }
8528
9668
  })
8529
9669
  ]
@@ -8575,6 +9715,7 @@ const kcloudCommands = () => [
8575
9715
  createCredentialsCommand(createAdapters),
8576
9716
  createRepositoriesCommand(createAdapters),
8577
9717
  createToolsCommand(createAdapters),
9718
+ createProvidersCommand(createAdapters),
8578
9719
  createNodesCommand(createAdapters),
8579
9720
  createAgentsCommand(createAdapters),
8580
9721
  createNotificationsCommand(createAdapters),