@kikimoradev/kcloud 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.js +738 -445
  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,11 @@ 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",
1024
798
  "project.last_admin": "conflict",
1025
799
  "agent.node_unassigned": "rule_violation",
1026
800
  "stream.too_many": "rate_limited",
@@ -1043,20 +817,257 @@ const errorCodes = Object.keys({
1043
817
  "volume.invalid_size": "rule_violation",
1044
818
  "notification.not_found": "not_found"
1045
819
  });
1046
- const errorCodeSchema = z.enum(errorCodes);
1047
- const errorDetailsSchema = z.record(z.string(), z.union([
1048
- z.string(),
1049
- z.number(),
1050
- z.boolean()
1051
- ]));
1052
- const errorBodySchema = z.object({
1053
- code: errorCodeSchema,
1054
- details: errorDetailsSchema.optional(),
1055
- issues: z.array(z.object({
1056
- path: z.string(),
1057
- message: z.string()
1058
- })).optional()
1059
- }).meta({ id: "ErrorBody" });
820
+ const errorCodeSchema = z.enum(errorCodes);
821
+ const errorDetailsSchema = z.record(z.string(), z.union([
822
+ z.string(),
823
+ z.number(),
824
+ z.boolean()
825
+ ]));
826
+ const errorBodySchema = z.object({
827
+ code: errorCodeSchema,
828
+ details: errorDetailsSchema.optional(),
829
+ issues: z.array(z.object({
830
+ path: z.string(),
831
+ message: z.string()
832
+ })).optional()
833
+ }).meta({ id: "ErrorBody" });
834
+ const agentEventLevelSchema = z.enum([
835
+ "info",
836
+ "warn",
837
+ "error"
838
+ ]);
839
+ const agentRoles = [
840
+ "monitor",
841
+ "executor",
842
+ "summarizer"
843
+ ];
844
+ const agentRoleSchema = z.enum(agentRoles);
845
+ const agentLogStreams = ["stdout", "stderr"];
846
+ z.enum(agentLogStreams);
847
+ const platformStream = "platform";
848
+ const agentEventStreams = [...agentLogStreams, platformStream];
849
+ const agentEventStreamSchema = z.enum(agentEventStreams);
850
+ const authKindSchema = z.enum([
851
+ "apiKey",
852
+ "oauth",
853
+ "claude.ai",
854
+ "unknown"
855
+ ]);
856
+ const agentControlStateSchema = z.enum([
857
+ "running",
858
+ "pausing",
859
+ "paused"
860
+ ]);
861
+ z.enum([
862
+ "session",
863
+ "diagnostic",
864
+ "task"
865
+ ]);
866
+ const maxCostUsd = 1e3;
867
+ const countSchema = z.int().min(0).max(2147483647);
868
+ const costUsdSchema = z.number().min(0).max(maxCostUsd);
869
+ const instantSchema$2 = z.iso.datetime();
870
+ const workerEnvelope = (event) => ({
871
+ ts: instantSchema$2,
872
+ level: agentEventLevelSchema,
873
+ event: z.literal(event)
874
+ });
875
+ const roleEnvelope = (event) => ({
876
+ ...workerEnvelope(event),
877
+ agent: agentRoleSchema.optional()
878
+ });
879
+ const agentEventSchema = z.discriminatedUnion("event", [
880
+ z.looseObject({
881
+ ...workerEnvelope("worker.started"),
882
+ version: z.string(),
883
+ claudeVersion: z.string().optional(),
884
+ nodeVersion: z.string(),
885
+ authKind: authKindSchema,
886
+ pid: countSchema,
887
+ projectDir: z.string(),
888
+ paused: z.boolean().optional()
889
+ }),
890
+ z.looseObject({
891
+ ...workerEnvelope("worker.stopped"),
892
+ signal: z.string().optional(),
893
+ drained: z.boolean().optional(),
894
+ forced: z.boolean().optional()
895
+ }),
896
+ z.looseObject({
897
+ ...workerEnvelope("worker.draining"),
898
+ reason: z.string(),
899
+ timeoutMs: countSchema.optional()
900
+ }),
901
+ z.looseObject({
902
+ ...workerEnvelope("update.available"),
903
+ from: z.string(),
904
+ to: z.string(),
905
+ installError: z.string().optional()
906
+ }),
907
+ z.looseObject({
908
+ ...workerEnvelope("update.installed"),
909
+ from: z.string(),
910
+ to: z.string()
911
+ }),
912
+ z.looseObject({
913
+ ...roleEnvelope("control.changed"),
914
+ state: agentControlStateSchema
915
+ }),
916
+ z.looseObject({
917
+ ...roleEnvelope("cycle.started"),
918
+ cycle: countSchema
919
+ }),
920
+ z.looseObject({
921
+ ...roleEnvelope("cycle.finished"),
922
+ cycle: countSchema,
923
+ ok: z.boolean(),
924
+ durationMs: countSchema,
925
+ costUsd: costUsdSchema.optional(),
926
+ addedTasks: countSchema,
927
+ skippedDuplicates: countSchema,
928
+ sessionId: z.string().optional(),
929
+ error: z.string().optional()
930
+ }),
931
+ z.looseObject({
932
+ ...roleEnvelope("monitor.offHours"),
933
+ resumeAt: instantSchema$2
934
+ }),
935
+ z.looseObject({
936
+ ...roleEnvelope("monitor.limitWait"),
937
+ resumeAt: instantSchema$2
938
+ }),
939
+ z.looseObject({
940
+ ...roleEnvelope("monitor.sleeping"),
941
+ nextCycleAt: instantSchema$2
942
+ }),
943
+ z.looseObject({
944
+ ...roleEnvelope("monitor.authBlocked"),
945
+ reason: z.string()
946
+ }),
947
+ z.looseObject({
948
+ ...roleEnvelope("monitor.admissionWait"),
949
+ ticket: z.string(),
950
+ since: instantSchema$2,
951
+ seq: countSchema
952
+ }),
953
+ z.looseObject({
954
+ ...roleEnvelope("executor.admissionWait"),
955
+ ticket: z.string(),
956
+ since: instantSchema$2,
957
+ seq: countSchema,
958
+ taskId: z.string()
959
+ }),
960
+ z.looseObject({
961
+ ...roleEnvelope("admission.granted"),
962
+ ticket: z.string(),
963
+ seq: countSchema
964
+ }),
965
+ z.looseObject({
966
+ ...roleEnvelope("admission.released"),
967
+ ticket: z.string(),
968
+ seq: countSchema
969
+ }),
970
+ z.looseObject({
971
+ ...roleEnvelope("admission.cancelled"),
972
+ ticket: z.string(),
973
+ seq: countSchema
974
+ }),
975
+ z.looseObject({
976
+ ...roleEnvelope("task.started"),
977
+ taskId: z.string(),
978
+ title: z.string()
979
+ }),
980
+ z.looseObject({
981
+ ...roleEnvelope("task.finished"),
982
+ taskId: z.string(),
983
+ title: z.string(),
984
+ ok: z.boolean(),
985
+ durationMs: countSchema,
986
+ costUsd: costUsdSchema.optional(),
987
+ numTurns: countSchema.optional(),
988
+ willRetry: z.boolean().optional(),
989
+ attempt: countSchema.optional(),
990
+ maxAttempts: countSchema.optional(),
991
+ sessionId: z.string().optional(),
992
+ error: z.string().optional()
993
+ }),
994
+ z.looseObject({
995
+ ...roleEnvelope("task.retryScheduled"),
996
+ taskId: z.string(),
997
+ resumeAt: instantSchema$2
998
+ }),
999
+ z.looseObject(roleEnvelope("executor.waiting")),
1000
+ z.looseObject({
1001
+ ...roleEnvelope("executor.limitWait"),
1002
+ resumeAt: instantSchema$2
1003
+ }),
1004
+ z.looseObject({
1005
+ ...roleEnvelope("executor.authBlocked"),
1006
+ reason: z.string()
1007
+ }),
1008
+ z.looseObject({
1009
+ ...roleEnvelope("summary.started"),
1010
+ taskId: z.string()
1011
+ }),
1012
+ z.looseObject({
1013
+ ...roleEnvelope("summary.finished"),
1014
+ taskId: z.string(),
1015
+ ok: z.boolean(),
1016
+ durationMs: countSchema,
1017
+ costUsd: costUsdSchema.optional(),
1018
+ sessionId: z.string().optional(),
1019
+ error: z.string().optional()
1020
+ }),
1021
+ z.looseObject({
1022
+ ...roleEnvelope("session.init"),
1023
+ model: z.string(),
1024
+ sessionId: z.string(),
1025
+ taskId: z.string().optional(),
1026
+ cycle: countSchema.optional()
1027
+ }),
1028
+ z.looseObject({
1029
+ ...roleEnvelope("session.stderr"),
1030
+ line: z.string()
1031
+ }),
1032
+ z.looseObject({
1033
+ ...roleEnvelope("session.procError"),
1034
+ message: z.string()
1035
+ }),
1036
+ z.looseObject({
1037
+ ...roleEnvelope("session.killed"),
1038
+ reason: z.string()
1039
+ }),
1040
+ z.looseObject({
1041
+ ...roleEnvelope("session.text"),
1042
+ text: z.string()
1043
+ }),
1044
+ z.looseObject({
1045
+ ...roleEnvelope("session.tool"),
1046
+ tool: z.string(),
1047
+ detail: z.string()
1048
+ }),
1049
+ z.looseObject({
1050
+ ...roleEnvelope("session.toolError"),
1051
+ output: z.string()
1052
+ })
1053
+ ]);
1054
+ z.looseObject({
1055
+ ts: instantSchema$2,
1056
+ level: agentEventLevelSchema,
1057
+ agent: agentRoleSchema.optional(),
1058
+ event: z.string().min(1).max(64)
1059
+ });
1060
+ agentEventSchema.options.map((option) => option.shape.event.value);
1061
+ ({
1062
+ rawLog: "log.raw",
1063
+ rejectedLog: "log.rejected",
1064
+ throttledLog: "log.throttled",
1065
+ stateChanged: "platform.stateChanged",
1066
+ crashLooping: "platform.crashLooping",
1067
+ provisioned: "platform.provisioned",
1068
+ sandboxStarted: "platform.sandboxStarted",
1069
+ sandboxStopped: "platform.sandboxStopped"
1070
+ }).rawLog;
1060
1071
  const agentStateSchema = z.enum([
1061
1072
  "created",
1062
1073
  "provisioning",
@@ -1561,9 +1572,32 @@ const nodeSchema = z.object({
1561
1572
  agentCount: z.int().nonnegative(),
1562
1573
  connectedAt: z.iso.datetime().nullable(),
1563
1574
  lastSeenAt: z.iso.datetime().nullable(),
1575
+ offlineSince: z.iso.datetime().nullable(),
1564
1576
  registrationExpiresAt: z.iso.datetime().nullable(),
1565
1577
  createdAt: z.iso.datetime()
1566
1578
  }).meta({ id: "Node" });
1579
+ const nodeUpdateSchema = z.object({
1580
+ state: z.enum([
1581
+ "current",
1582
+ "waiting",
1583
+ "updating",
1584
+ "failed",
1585
+ "unmanaged"
1586
+ ]),
1587
+ target: z.string().nullable(),
1588
+ sessions: z.int().nonnegative(),
1589
+ since: z.iso.datetime().nullable(),
1590
+ last: z.object({
1591
+ outcome: z.enum([
1592
+ "updated",
1593
+ "rolledBack",
1594
+ "failed"
1595
+ ]),
1596
+ version: z.string().nullable(),
1597
+ reason: z.string().nullable(),
1598
+ at: z.iso.datetime()
1599
+ }).nullable()
1600
+ }).meta({ id: "NodeUpdate" });
1567
1601
  const nodeAgentSchema = z.object({
1568
1602
  id: idSchema,
1569
1603
  name: z.string(),
@@ -1609,7 +1643,8 @@ const nodeRuntimeSchema = z.object({
1609
1643
  }).meta({ id: "NodeRuntime" });
1610
1644
  nodeSchema.extend({
1611
1645
  agents: z.array(nodeAgentSchema),
1612
- runtime: nodeRuntimeSchema.nullable()
1646
+ runtime: nodeRuntimeSchema.nullable(),
1647
+ update: nodeUpdateSchema.nullable()
1613
1648
  }).meta({ id: "NodeDetail" });
1614
1649
  z.object({ items: z.array(nodeSchema) }).meta({ id: "NodeList" });
1615
1650
  z.object({ nodeId: idSchema });
@@ -1825,6 +1860,8 @@ const auditActionSchema = z.enum([
1825
1860
  "agent.updated",
1826
1861
  "agent.deleted",
1827
1862
  "agent.action",
1863
+ "agent.backup_requested",
1864
+ "agent.restored",
1828
1865
  "platform.settings_updated",
1829
1866
  "runtime.sessions_updated",
1830
1867
  "platform.upgrade_requested",
@@ -2464,6 +2501,43 @@ z.object({
2464
2501
  z.object({ code: errorCodeSchema })
2465
2502
  ])
2466
2503
  }).meta({ id: "AgentEventFrame" });
2504
+ //#endregion
2505
+ //#region ../../packages/contracts/src/agent-backups.ts
2506
+ const agentBackupTriggers = ["nightly", "manual"];
2507
+ const agentBackupSchema = z.object({
2508
+ id: idSchema,
2509
+ agentId: idSchema,
2510
+ nodeId: idSchema,
2511
+ nodeName: z.string().nullable(),
2512
+ trigger: z.enum(agentBackupTriggers),
2513
+ kikimoraVersion: z.string(),
2514
+ entries: z.int().nonnegative(),
2515
+ sizeBytes: z.int().nonnegative(),
2516
+ createdAt: z.iso.datetime(),
2517
+ expiresAt: z.iso.datetime()
2518
+ }).meta({ id: "AgentBackup" });
2519
+ z.object({
2520
+ items: z.array(agentBackupSchema),
2521
+ retentionDays: z.int().positive()
2522
+ }).meta({ id: "AgentBackupList" });
2523
+ z.object({ state: z.literal("requested") }).meta({ id: "AgentBackupRequested" });
2524
+ z.object({
2525
+ id: idSchema,
2526
+ backupId: idSchema
2527
+ });
2528
+ z.object({
2529
+ name: agentNameSchema,
2530
+ slug: slugSchema.optional(),
2531
+ nodeId: idSchema
2532
+ }).meta({ id: "RestoreAgentBody" });
2533
+ z.object({ agentId: idSchema }).meta({ id: "RestoredAgent" });
2534
+ z.object({
2535
+ agentId: idSchema,
2536
+ trigger: z.enum(agentBackupTriggers),
2537
+ kikimoraVersion: z.string().min(1).max(64),
2538
+ entries: z.coerce.number().int().nonnegative()
2539
+ });
2540
+ z.object({ backupId: idSchema });
2467
2541
  [
2468
2542
  "application/vnd.oci.image.index.v1+json",
2469
2543
  "application/vnd.oci.image.manifest.v1+json",
@@ -3644,6 +3718,116 @@ const createWhoamiCommand = (adaptersOf) => defineCommand({
3644
3718
  }
3645
3719
  });
3646
3720
  //#endregion
3721
+ //#region src/render/body.ts
3722
+ const isTerminal$1 = (stream) => stream.isTTY === true;
3723
+ const pipedText$1 = async (stream) => {
3724
+ const chunks = [];
3725
+ for await (const chunk of stream) chunks.push(Buffer.from(chunk));
3726
+ return Buffer.concat(chunks).toString("utf8");
3727
+ };
3728
+ const fileOptions = { file: { type: "string" } };
3729
+ const fileUsage = (what) => [["--file <path>", `read ${what} from a JSON file, or from stdin with -`]];
3730
+ const documentOf = async (run, file) => {
3731
+ const text = file === "-" ? isTerminal$1(run.context.io.stdin) ? "" : await pipedText$1(run.context.io.stdin) : await readFileOrRefuse(resolve(run.context.workingDirectory, file));
3732
+ if (text.trim() === "") throw new UsageError(file === "-" ? "Nothing is piped on stdin." : `${file} is empty.`);
3733
+ try {
3734
+ return JSON.parse(text);
3735
+ } catch {
3736
+ throw new CliError(`${file === "-" ? "stdin" : file} is not JSON.`);
3737
+ }
3738
+ };
3739
+ const readFileOrRefuse = async (path) => {
3740
+ try {
3741
+ return await readFile(path, "utf8");
3742
+ } catch {
3743
+ throw new CliError(`${path} could not be read.`);
3744
+ }
3745
+ };
3746
+ const validated = (schema, document, what) => {
3747
+ const parsed = schema.safeParse(document);
3748
+ if (!parsed.success) throw new CliError(`That is not ${what}.`, z.prettifyError(parsed.error));
3749
+ };
3750
+ //#endregion
3751
+ //#region src/commands/nodes/shared.ts
3752
+ const nodeHints = {
3753
+ ...organizationHints,
3754
+ "node.not_found": "kcloud nodes list shows the nodes of the organization.",
3755
+ "node.name_taken": "Another node of the organization has that name.",
3756
+ "node.in_use": "Delete the agents placed on it first; kcloud nodes show <node> lists them.",
3757
+ "node.registration_used": "The node has joined and holds its own credential; it needs no token."
3758
+ };
3759
+ const splitHints = {
3760
+ ...nodeHints,
3761
+ "auth.forbidden": "Splitting a node takes an admin of its organization.",
3762
+ "node.host_unknown": "The node reports its CPUs and memory once its service runs; kcloud nodes show <node> shows its state.",
3763
+ "node.split_exceeds_host": "The split asks for more than the host has; kcloud nodes split <node> shows the host.",
3764
+ "concurrency.conflict": "Someone changed the node meanwhile; run the command again."
3765
+ };
3766
+ const nodesOf = async (api, organizationId) => dataOf("list the nodes", await api.GET("/organizations/{organizationId}/nodes", { params: { path: { organizationId } } }), nodeHints).items;
3767
+ const namesOf = (nodes) => nodes.map(({ name }) => name).join(", ");
3768
+ const nodeIn = (nodes, reference, organization) => {
3769
+ const found = nodes.find((node) => node.id === reference.toLowerCase() || node.name === reference);
3770
+ if (found === void 0) throw new CliError(`There is no node ${reference} in ${organization}.`, nodes.length === 0 ? "kcloud nodes create <name> creates one." : `kcloud nodes list shows ${namesOf(nodes)}.`);
3771
+ return found;
3772
+ };
3773
+ const resolveNode = async (scope, reference) => {
3774
+ const organization = await scope.organization();
3775
+ if (isId(reference)) return {
3776
+ organizationId: organization.id,
3777
+ nodeId: reference
3778
+ };
3779
+ const node = nodeIn(await nodesOf(scope.api, organization.id), reference, organization.slug);
3780
+ return {
3781
+ organizationId: organization.id,
3782
+ nodeId: node.id
3783
+ };
3784
+ };
3785
+ const nodeDetailOf = async (api, { organizationId, nodeId }) => dataOf("describe the node", await api.GET("/organizations/{organizationId}/nodes/{nodeId}", { params: { path: {
3786
+ organizationId,
3787
+ nodeId
3788
+ } } }), nodeHints);
3789
+ const createdNode = async (api, organizationId, name) => {
3790
+ const body = { name };
3791
+ validated(createNodeBodySchema, body, "a node");
3792
+ return dataOf("create the node", await api.POST("/organizations/{organizationId}/nodes", {
3793
+ params: { path: { organizationId } },
3794
+ body
3795
+ }), nodeHints);
3796
+ };
3797
+ const renewedRegistration = async (api, { organizationId, nodeId }) => dataOf("issue a registration token for the node", await api.POST("/organizations/{organizationId}/nodes/{nodeId}/registration", { params: { path: {
3798
+ organizationId,
3799
+ nodeId
3800
+ } } }), nodeHints);
3801
+ const deleteNode = async (api, { organizationId, nodeId }) => {
3802
+ doneWith("delete the node", await api.DELETE("/organizations/{organizationId}/nodes/{nodeId}", { params: { path: {
3803
+ organizationId,
3804
+ nodeId
3805
+ } } }), nodeHints);
3806
+ };
3807
+ const splitNode = async (api, { organizationId, nodeId }, revision, split) => dataOf(split === null ? "clear the split of the node" : "split the node", await api.PUT("/organizations/{organizationId}/nodes/{nodeId}/split", {
3808
+ params: { path: {
3809
+ organizationId,
3810
+ nodeId
3811
+ } },
3812
+ body: {
3813
+ revision,
3814
+ split
3815
+ }
3816
+ }), splitHints);
3817
+ const nodeUsage = [["", "a node is named by its name or its id"]];
3818
+ //#endregion
3819
+ //#region src/commands/projects/shared.ts
3820
+ const defaultProjectColor = "#7c86ff";
3821
+ const projectHint = {
3822
+ "project.not_found": "kcloud projects list shows the projects you are a member of.",
3823
+ "project.slug_taken": "Another project of the organization has that slug.",
3824
+ "project.invalid_slug": "A slug holds lowercase letters, digits and hyphens.",
3825
+ "project.not_empty": "Delete its agents, credentials, repositories and tools first.",
3826
+ "project.member_not_found": "kcloud projects members list <project> shows them.",
3827
+ "project.member_must_be_active": "kcloud orgs members enable <member> enables the account first."
3828
+ };
3829
+ const projectOf = async (api, id) => dataOf("describe the project", await api.GET("/projects/{id}", { params: { path: { id } } }), projectHint);
3830
+ //#endregion
3647
3831
  //#region src/commands/run.ts
3648
3832
  const runOf = async (adaptersOf, { settings, context }) => {
3649
3833
  const adapters = adaptersOf(context);
@@ -3682,62 +3866,187 @@ const agentFrom = async (run, reference, project) => {
3682
3866
  return resolveAgentId(run, reference, project);
3683
3867
  };
3684
3868
  //#endregion
3685
- //#region src/commands/agents/check.ts
3686
- const checkHints = {
3869
+ //#region src/commands/agents/backups.ts
3870
+ const backupsListHint = "kcloud agents backups list <agent> shows the backups of the agent and their ids.";
3871
+ const backupHints = {
3687
3872
  ...agentHints,
3688
- "runtime.control_unreachable": "The worker is not answering; kcloud agents show <agent> tells whether it runs.",
3689
- "runtime.monitor_paused": "kcloud agents resume <agent> lets the agent work again.",
3690
- "runtime.monitor_draining": "The agent stops after its current session; start it again to run a cycle.",
3691
- "runtime.monitor_limited": "The Claude usage limit holds; the monitor runs its next cycle once it resets.",
3692
- "runtime.monitor_check_unsupported": "Move the agent to Kikimora 0.10.0 or newer with kcloud agents set, then kcloud agents update."
3873
+ "agent.backup_unavailable": "Backups need the Spaces of the platform and a node that can make them. An administrator of the platform configures the Spaces.",
3874
+ "agent.backup_without_export": "The Kikimora release of the agent cannot export its memory. Move it to 0.12.0 or newer with kcloud agents set, then kcloud agents update.",
3875
+ "agent.backup_in_progress": "A backup of the agent is running already. kcloud agents backups list <agent> shows it once the node has sent it.",
3876
+ "node.offline": "The node of the agent is not connected, so it cannot make a backup. kcloud nodes show <node> tells when it was last seen."
3693
3877
  };
3694
- const checkCommand = (adaptersOf) => defineCommand({
3695
- name: "check",
3696
- summary: "Run a monitor cycle now instead of waiting for the interval.",
3697
- usage: "agents check <agent> [--project <name>]",
3698
- optionsUsage: agentUsage,
3699
- options: agentOption,
3878
+ const restoreHints = {
3879
+ ...agentHints,
3880
+ "node.not_found": "kcloud nodes list shows the nodes of the organization.",
3881
+ "node.offline": "The node is not connected. kcloud nodes show <node> tells when it was last seen.",
3882
+ "agent.backup_not_found": `The backup does not exist or has expired. ${backupsListHint}`,
3883
+ "agent.slug_taken": "Another agent of the project has that slug. Pass another with --slug.",
3884
+ "agent.invalid_slug": "A slug holds lowercase letters, digits and hyphens."
3885
+ };
3886
+ const backupsOf = async (api, agentId) => dataOf("list the backups of the agent", await api.GET("/agents/{id}/backups", { params: { path: { id: agentId } } }), backupHints);
3887
+ const requestedBackup = async (api, agentId) => dataOf("back the agent up", await api.POST("/agents/{id}/backups", { params: { path: { id: agentId } } }), backupHints);
3888
+ const nodeIdFor = async (api, agentId, node) => {
3889
+ if (isId(node)) return node;
3890
+ const agent = dataOf("describe the agent", await api.GET("/agents/{id}", { params: { path: { id: agentId } } }), agentHints);
3891
+ const project = await projectOf(api, agent.projectId);
3892
+ const nodes = await nodesOf(api, project.organizationId);
3893
+ return nodeIn(nodes, node, `the organization of ${project.slug}`).id;
3894
+ };
3895
+ const restoredAgent = async (api, agentId, { backupId, name, node, slug }) => {
3896
+ const nodeId = await nodeIdFor(api, agentId, node);
3897
+ return dataOf("restore the agent from the backup", await api.POST("/agents/{id}/backups/{backupId}/restore", {
3898
+ params: { path: {
3899
+ id: agentId,
3900
+ backupId
3901
+ } },
3902
+ body: {
3903
+ name,
3904
+ nodeId,
3905
+ ...slug === void 0 ? {} : { slug }
3906
+ }
3907
+ }), restoreHints);
3908
+ };
3909
+ const keptFor = (days) => `A backup is kept for ${String(days)} ${days === 1 ? "day" : "days"}.`;
3910
+ const backupsTable = ({ items, retentionDays }) => items.length === 0 ? `The agent has no backup yet. ${keptFor(retentionDays)}` : [renderTable([
3911
+ { header: "CREATED" },
3912
+ { header: "TRIGGER" },
3913
+ { header: "NODE" },
3914
+ { header: "KIKIMORA" },
3915
+ {
3916
+ header: "ENTRIES",
3917
+ align: "right"
3918
+ },
3919
+ {
3920
+ header: "SIZE",
3921
+ align: "right"
3922
+ },
3923
+ { header: "EXPIRES" },
3924
+ { header: "ID" }
3925
+ ], items.map((backup) => [
3926
+ backup.createdAt,
3927
+ backup.trigger,
3928
+ backup.nodeName ?? backup.nodeId,
3929
+ backup.kikimoraVersion,
3930
+ String(backup.entries),
3931
+ bytesOf(backup.sizeBytes),
3932
+ backup.expiresAt,
3933
+ backup.id
3934
+ ])), keptFor(retentionDays)].join("\n");
3935
+ const restoreRequestOf = ({ backup, name, node, slug }) => {
3936
+ if (backup === void 0 || name === void 0 || node === void 0 || node.trim() === "") {
3937
+ const missing = [
3938
+ ...backup === void 0 ? ["--backup <id>"] : [],
3939
+ ...name === void 0 ? ["--name <name>"] : [],
3940
+ ...node === void 0 || node.trim() === "" ? ["--node <node>"] : []
3941
+ ];
3942
+ throw new UsageError(`agents restore needs ${listOf(missing)}.`, backupsListHint);
3943
+ }
3944
+ if (!isId(backup)) throw new UsageError(`--backup ${backup} is not the id of a backup.`, backupsListHint);
3945
+ const named = agentNameSchema.safeParse(name);
3946
+ if (!named.success) throw new UsageError(`--name needs from 1 to ${String(80)} characters.`);
3947
+ if (slug !== void 0 && !slugSchema.safeParse(slug).success) throw new UsageError(`--slug ${slug} is not a slug.`, "A slug holds lowercase letters, digits and hyphens, at most 40 of them.");
3948
+ return {
3949
+ backupId: backup,
3950
+ name: named.data,
3951
+ node: node.trim(),
3952
+ slug
3953
+ };
3954
+ };
3955
+ const backupsCommand = (adaptersOf) => defineGroup({
3956
+ name: "backups",
3957
+ summary: "The backups of the memory of an agent.",
3958
+ usage: "agents backups <command>",
3959
+ subcommands: [defineCommand({
3960
+ name: "list",
3961
+ summary: "List the backups of an agent, the newest first.",
3962
+ usage: "agents backups list <agent> [--project <name>]",
3963
+ optionsUsage: agentUsage,
3964
+ options: agentOption,
3965
+ positionals: {
3966
+ min: 1,
3967
+ max: 1
3968
+ },
3969
+ run: async (invocation) => {
3970
+ const run = await runOf(adaptersOf, invocation);
3971
+ const agentId = await agentFrom(run, invocation.positionals[0], invocation.values.project);
3972
+ return printed(run, await backupsOf(run.api, agentId), backupsTable);
3973
+ }
3974
+ }), defineCommand({
3975
+ name: "create",
3976
+ summary: "Ask the node of an agent for a backup now.",
3977
+ usage: "agents backups create <agent> [--project <name>]",
3978
+ optionsUsage: agentUsage,
3979
+ options: agentOption,
3980
+ positionals: {
3981
+ min: 1,
3982
+ max: 1
3983
+ },
3984
+ run: async (invocation) => {
3985
+ const run = await runOf(adaptersOf, invocation);
3986
+ const reference = invocation.positionals[0] ?? "";
3987
+ const agentId = await agentFrom(run, reference, invocation.values.project);
3988
+ return printed(run, await requestedBackup(run.api, agentId), () => `Backup of ${reference} requested. kcloud agents backups list ${reference} shows it once the node has sent it.`);
3989
+ }
3990
+ })]
3991
+ });
3992
+ const restoreCommand = (adaptersOf) => defineCommand({
3993
+ name: "restore",
3994
+ summary: "Create a new agent from a backup of an agent.",
3995
+ usage: "agents restore <agent> --backup <id> --name <name> --node <node> [--slug <slug>] [--project <name>]",
3996
+ optionsUsage: [
3997
+ ...agentUsage,
3998
+ ["--backup <id>", "the backup, by its id from kcloud agents backups list"],
3999
+ ["--name <name>", "the name of the new agent"],
4000
+ ["--node <node>", "the node the new agent runs on, by name or id"],
4001
+ ["--slug <slug>", "the slug of the new agent (default: one made from its name)"],
4002
+ ["", "the new agent joins the project of the agent with its configuration and the memory of the backup"]
4003
+ ],
4004
+ options: {
4005
+ ...agentOption,
4006
+ backup: { type: "string" },
4007
+ name: { type: "string" },
4008
+ node: { type: "string" },
4009
+ slug: { type: "string" }
4010
+ },
3700
4011
  positionals: {
3701
4012
  min: 1,
3702
4013
  max: 1
3703
4014
  },
3704
4015
  run: async (invocation) => {
4016
+ const request = restoreRequestOf(invocation.values);
3705
4017
  const run = await runOf(adaptersOf, invocation);
3706
- const agentId = await agentFrom(run, invocation.positionals[0], invocation.values.project);
3707
- const check = dataOf("check the monitor", await run.api.POST("/agents/{id}/monitor/check", { params: { path: { id: agentId } } }), checkHints);
3708
- return printed(run, check, (answer) => answer.state === "running" ? `Monitor cycle ${String(answer.cycle)} is already running.` : "Monitor cycle requested; it starts now, also outside the active hours.");
4018
+ const reference = invocation.positionals[0] ?? "";
4019
+ const agentId = await agentFrom(run, reference, invocation.values.project);
4020
+ return printed(run, await restoredAgent(run.api, agentId, request), (restored) => [`Restored ${request.name} as ${restored.agentId} from backup ${request.backupId} of ${reference}.`, `kcloud agents start ${restored.agentId} starts it once it is provisioned.`].join("\n"));
3709
4021
  }
3710
4022
  });
3711
4023
  //#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));
4024
+ //#region src/commands/agents/check.ts
4025
+ const checkHints = {
4026
+ ...agentHints,
4027
+ "runtime.control_unreachable": "The worker is not answering; kcloud agents show <agent> tells whether it runs.",
4028
+ "runtime.monitor_paused": "kcloud agents resume <agent> lets the agent work again.",
4029
+ "runtime.monitor_draining": "The agent stops after its current session; start it again to run a cycle.",
4030
+ "runtime.monitor_limited": "The Claude usage limit holds; the monitor runs its next cycle once it resets.",
4031
+ "runtime.monitor_check_unsupported": "Move the agent to Kikimora 0.10.0 or newer with kcloud agents set, then kcloud agents update."
3740
4032
  };
4033
+ const checkCommand = (adaptersOf) => defineCommand({
4034
+ name: "check",
4035
+ summary: "Run a monitor cycle now instead of waiting for the interval.",
4036
+ usage: "agents check <agent> [--project <name>]",
4037
+ optionsUsage: agentUsage,
4038
+ options: agentOption,
4039
+ positionals: {
4040
+ min: 1,
4041
+ max: 1
4042
+ },
4043
+ run: async (invocation) => {
4044
+ const run = await runOf(adaptersOf, invocation);
4045
+ const agentId = await agentFrom(run, invocation.positionals[0], invocation.values.project);
4046
+ const check = dataOf("check the monitor", await run.api.POST("/agents/{id}/monitor/check", { params: { path: { id: agentId } } }), checkHints);
4047
+ return printed(run, check, (answer) => answer.state === "running" ? `Monitor cycle ${String(answer.cycle)} is already running.` : "Monitor cycle requested; it starts now, also outside the active hours.");
4048
+ }
4049
+ });
3741
4050
  //#endregion
3742
4051
  //#region src/render/confirm.ts
3743
4052
  const confirmed = async ({ prompt, settings, question }) => {
@@ -4042,86 +4351,6 @@ const detailsOf = (agent) => [
4042
4351
  ]))
4043
4352
  ].join("\n");
4044
4353
  //#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
4354
  //#region src/commands/agents/placement.ts
4126
4355
  const placedNodeId = async (run, { projectId, given, ask }) => {
4127
4356
  const project = await projectOf(run.api, projectId);
@@ -5156,6 +5385,8 @@ const createAgentsCommand = (adaptersOf) => defineGroup({
5156
5385
  ...lifecycleCommands(adaptersOf),
5157
5386
  checkCommand(adaptersOf),
5158
5387
  tasksCommand(adaptersOf),
5388
+ backupsCommand(adaptersOf),
5389
+ restoreCommand(adaptersOf),
5159
5390
  ...observabilityCommands(adaptersOf),
5160
5391
  ...streamCommands(adaptersOf)
5161
5392
  ]
@@ -5788,75 +6019,6 @@ const agentTools = [
5788
6019
  })
5789
6020
  ];
5790
6021
  //#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
6022
  //#region src/commands/nodes/split.ts
5861
6023
  const splitFields = [
5862
6024
  "reserve.cpus",
@@ -6242,6 +6404,117 @@ const nodeTools = [
6242
6404
  })
6243
6405
  ];
6244
6406
  //#endregion
6407
+ //#region src/mcp/tools/backups.ts
6408
+ const agentBackupTools = [
6409
+ defineTool({
6410
+ name: "agent_backups_list",
6411
+ title: "List the backups of an agent",
6412
+ description: "List the backups of the memory of an agent, the newest first, with their trigger, node, Kikimora release, entries, size and expiry, and the days a backup is kept.",
6413
+ operations: ["listAgentBackups"],
6414
+ safety: "read",
6415
+ input: z.object(agentFields),
6416
+ run: async (reference, run) => backupsOf(run.api, await agentIdOf(run, reference))
6417
+ }),
6418
+ defineTool({
6419
+ name: "agent_backup_request",
6420
+ title: "Back an agent up now",
6421
+ description: "Ask the node of an agent for a backup of its memory now. state requested means the node makes it, and agent_backups_list shows it once the node has sent it.",
6422
+ operations: ["requestAgentBackup"],
6423
+ safety: "write",
6424
+ input: z.object(agentFields),
6425
+ run: async (reference, run) => requestedBackup(run.api, await agentIdOf(run, reference))
6426
+ }),
6427
+ defineTool({
6428
+ name: "agent_restore",
6429
+ title: "Restore an agent from a backup",
6430
+ description: "Create a new agent in the project of an agent, with its configuration and the memory of one of its backups, on the node named. Returns the id of the new agent, which stays stopped until agent_action starts it.",
6431
+ operations: ["restoreAgentFromBackup"],
6432
+ safety: "write",
6433
+ input: z.object({
6434
+ ...agentFields,
6435
+ backup: idSchema.describe("The id of the backup, from agent_backups_list."),
6436
+ name: agentNameSchema.describe("The name of the new agent."),
6437
+ slug: slugSchema.optional().describe("The slug of the new agent, instead of one made from its name."),
6438
+ node: nodeField
6439
+ }),
6440
+ run: async ({ backup, name, slug, node, ...reference }, run) => restoredAgent(run.api, await agentIdOf(run, reference), {
6441
+ backupId: backup,
6442
+ name,
6443
+ node,
6444
+ slug
6445
+ })
6446
+ })
6447
+ ];
6448
+ //#endregion
6449
+ //#region src/mcp/tools/credentials.ts
6450
+ const credentialField$1 = z.string().trim().min(1).describe("The credential, by its label or its id.");
6451
+ const credentialTools = [
6452
+ defineTool({
6453
+ name: "credentials_list",
6454
+ title: "List credentials",
6455
+ description: "List the Claude and GitHub credentials of a project, with their fingerprints and verification, never their secrets.",
6456
+ operations: ["listCredentials"],
6457
+ safety: "read",
6458
+ input: z.object({ project: projectField }),
6459
+ run: async ({ project }, run) => credentialsOf(run.api, await projectIdOf(run, project))
6460
+ }),
6461
+ defineTool({
6462
+ name: "credential_update",
6463
+ title: "Rename or verify a credential",
6464
+ description: "rename gives a credential a new label; verify asks the instance to check it against its service, and credentials_list shows the answer.",
6465
+ operations: ["renameCredential", "verifyCredential"],
6466
+ safety: "write",
6467
+ input: z.object({
6468
+ project: projectField,
6469
+ credential: credentialField$1,
6470
+ action: z.enum(["rename", "verify"]),
6471
+ label: z.string().trim().min(1).optional().describe("The new label, for rename.")
6472
+ }),
6473
+ run: async ({ project, credential, action, label }, run) => {
6474
+ const projectId = await projectIdOf(run, project);
6475
+ const credentialId = await resolveCredentialId(run.api, projectId, credential);
6476
+ if (action === "verify") {
6477
+ doneWith("verify the credential", await run.api.POST("/projects/{id}/credentials/{credentialId}/verify", { params: { path: {
6478
+ id: projectId,
6479
+ credentialId
6480
+ } } }), credentialHints);
6481
+ return {
6482
+ id: credentialId,
6483
+ verifying: true
6484
+ };
6485
+ }
6486
+ if (label === void 0) throw new UsageError("rename needs a label.");
6487
+ return dataOf("rename the credential", await run.api.PATCH("/projects/{id}/credentials/{credentialId}", {
6488
+ params: { path: {
6489
+ id: projectId,
6490
+ credentialId
6491
+ } },
6492
+ body: { label }
6493
+ }), credentialHints);
6494
+ }
6495
+ }),
6496
+ defineTool({
6497
+ name: "credential_delete",
6498
+ title: "Delete a credential",
6499
+ description: "Delete a credential of a project that no agent and no repository uses.",
6500
+ operations: ["deleteCredential"],
6501
+ safety: "destructive",
6502
+ input: z.object({
6503
+ project: projectField,
6504
+ credential: credentialField$1
6505
+ }),
6506
+ run: async ({ project, credential }, run) => {
6507
+ const projectId = await projectIdOf(run, project);
6508
+ const credentialId = await resolveCredentialId(run.api, projectId, credential);
6509
+ doneWith("delete the credential", await run.api.DELETE("/projects/{id}/credentials/{credentialId}", { params: { path: {
6510
+ id: projectId,
6511
+ credentialId
6512
+ } } }), credentialHints);
6513
+ return { deleted: credentialId };
6514
+ }
6515
+ })
6516
+ ];
6517
+ //#endregion
6245
6518
  //#region src/commands/notifications.ts
6246
6519
  const notificationHints = { "notification.not_found": "kcloud notifications list shows them." };
6247
6520
  const createNotificationsCommand = (adaptersOf) => defineGroup({
@@ -8263,6 +8536,7 @@ const kcloudTools = [
8263
8536
  ...projectToolTools,
8264
8537
  ...nodeTools,
8265
8538
  ...agentTools,
8539
+ ...agentBackupTools,
8266
8540
  ...taskTools,
8267
8541
  ...observabilityTools,
8268
8542
  ...notificationTools,
@@ -8359,6 +8633,22 @@ const runtimeRowsOf = (runtime) => [
8359
8633
  ["agents", `${runtime.reconcile.agentsActive} active, ${runtime.reconcile.agentsQueued} queued`],
8360
8634
  ["log readers", String(runtime.ingest.readers)]
8361
8635
  ];
8636
+ const updateOf = ({ update }) => {
8637
+ if (update === null) return "not reported";
8638
+ switch (update.state) {
8639
+ case "current": return "current";
8640
+ case "waiting": return `waits for ${String(update.sessions)} ${update.sessions === 1 ? "session" : "sessions"} to end before ${update.target ?? "-"}`;
8641
+ case "updating": return `updating to ${update.target ?? "-"}`;
8642
+ case "failed": return `failed to reach ${update.target ?? "-"}, runs its previous version`;
8643
+ case "unmanaged": return "updated with its platform";
8644
+ }
8645
+ };
8646
+ const lastUpdateOf = ({ update }) => {
8647
+ const last = update?.last ?? null;
8648
+ if (last === null) return null;
8649
+ const outcome = `${last.outcome} ${last.version ?? "-"} at ${last.at}`;
8650
+ return last.reason === null ? outcome : `${outcome}: ${last.reason}`;
8651
+ };
8362
8652
  const nodeDetailsText = (node) => [
8363
8653
  renderTable([{ header: "FIELD" }, { header: "VALUE" }], [
8364
8654
  ["name", node.name],
@@ -8375,6 +8665,9 @@ const nodeDetailsText = (node) => [
8375
8665
  ["registration", registrationOf(node)],
8376
8666
  ["connected", node.connectedAt ?? "never"],
8377
8667
  ["last seen", node.lastSeenAt ?? "never"],
8668
+ ...node.offlineSince === null ? [] : [["offline since", node.offlineSince]],
8669
+ ["update", updateOf(node)],
8670
+ ...lastUpdateOf(node) === null ? [] : [["last update", lastUpdateOf(node) ?? ""]],
8378
8671
  ["created", node.createdAt],
8379
8672
  ["id", node.id]
8380
8673
  ]),
package/package.json CHANGED
@@ -15,7 +15,7 @@
15
15
  "publishConfig": {
16
16
  "access": "public"
17
17
  },
18
- "version": "0.16.0",
18
+ "version": "0.17.0",
19
19
  "dependencies": {
20
20
  "@clack/prompts": "1.8.1",
21
21
  "@modelcontextprotocol/server": "2.1.0",