@kikimoradev/kcloud 1.1.0 → 1.3.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 +878 -296
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -518,7 +518,9 @@ const runCli = dispatcherOf(spec);
518
518
  //#endregion
519
519
  //#region ../../packages/contracts/src/patterns.ts
520
520
  const slugPattern = /^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$/;
521
+ const uuidShapePattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
521
522
  const hexColorPattern = /^#[0-9a-f]{6}$/;
523
+ const logoHashPattern = /^[0-9a-f]{64}$/;
522
524
  const userCodePattern = /^[23456789BCDFGHJKLMNPQRSTVWXZ]{4}-[23456789BCDFGHJKLMNPQRSTVWXZ]{4}$/;
523
525
  const versionTagPattern = /^(?:0|[1-9]\d{0,8})\.(?:0|[1-9]\d{0,8})\.(?:0|[1-9]\d{0,8})$/;
524
526
  const effortLevelSchema = z.enum([
@@ -661,6 +663,22 @@ const credentialMetadataSchema = z.object({
661
663
  checkedAt: z.iso.datetime().optional(),
662
664
  failureReason: z.string().optional()
663
665
  }).meta({ id: "CredentialMetadata" });
666
+ const usageWindowStatusSchema = z.enum([
667
+ "allowed",
668
+ "allowed_warning",
669
+ "rejected"
670
+ ]);
671
+ const usageWindowSchema = z.object({
672
+ utilization: z.number().nonnegative(),
673
+ resetsAt: z.iso.datetime().nullable(),
674
+ status: usageWindowStatusSchema.nullable()
675
+ }).meta({ id: "UsageWindow" });
676
+ const subscriptionUsageSchema = z.object({
677
+ fiveHour: usageWindowSchema.nullable(),
678
+ sevenDay: usageWindowSchema.nullable(),
679
+ readAt: z.iso.datetime(),
680
+ stale: z.boolean()
681
+ }).meta({ id: "SubscriptionUsage" });
664
682
  const credentialSchema = z.object({
665
683
  id: idSchema,
666
684
  projectId: idSchema,
@@ -671,6 +689,8 @@ const credentialSchema = z.object({
671
689
  verification: verificationStatusSchema,
672
690
  metadata: credentialMetadataSchema,
673
691
  expiresAt: z.iso.datetime().nullable(),
692
+ expiresAtComputed: z.boolean(),
693
+ usage: subscriptionUsageSchema.nullable(),
674
694
  createdAt: z.iso.datetime(),
675
695
  updatedAt: z.iso.datetime()
676
696
  }).meta({ id: "Credential" });
@@ -739,6 +759,7 @@ const errorCodes = Object.keys({
739
759
  "credential.in_use": "conflict",
740
760
  "credential.kind_mismatch": "rule_violation",
741
761
  "credential.rejected": "rule_violation",
762
+ "credential.usage_unavailable": "rule_violation",
742
763
  "repository.not_found": "not_found",
743
764
  "repository.already_registered": "conflict",
744
765
  "repository.in_use": "conflict",
@@ -805,7 +826,6 @@ const errorCodes = Object.keys({
805
826
  "node.size_unavailable": "rule_violation",
806
827
  "node.size_not_larger": "rule_violation",
807
828
  "node.region_unavailable": "rule_violation",
808
- "node.import_refused": "rule_violation",
809
829
  "provider.not_found": "not_found",
810
830
  "provider.name_taken": "conflict",
811
831
  "provider.in_use": "conflict",
@@ -819,20 +839,17 @@ const errorCodes = Object.keys({
819
839
  "infrastructure.provider_error": "internal",
820
840
  "infrastructure.operation_in_progress": "conflict",
821
841
  "infrastructure.host_ops_disabled": "rule_violation",
822
- "infrastructure.backups_unavailable": "rule_violation",
823
- "infrastructure.maintenance_in_progress": "conflict",
824
- "infrastructure.maintenance_not_in_progress": "conflict",
825
842
  "backup.not_found": "not_found",
826
843
  "backup.in_progress": "conflict",
827
844
  "backup.unavailable": "rule_violation",
828
845
  "backup.corrupt": "rule_violation",
829
846
  "backup.undecryptable": "rule_violation",
830
- "platform.version_unknown": "rule_violation",
831
- "platform.same_version": "rule_violation",
832
- "platform.image_unresolved": "rule_violation",
833
- "platform.scripts_outdated": "rule_violation",
834
- "volume.invalid_size": "rule_violation",
835
- "notification.not_found": "not_found"
847
+ "notification.not_found": "not_found",
848
+ "logo.not_found": "not_found",
849
+ "logo.unreadable": "rule_violation",
850
+ "logo.too_small": "rule_violation",
851
+ "logo.too_large": "rule_violation",
852
+ "logo.type_mismatch": "rule_violation"
836
853
  });
837
854
  const errorCodeSchema = z.enum(errorCodes);
838
855
  const errorDetailsSchema = z.record(z.string(), z.union([
@@ -1083,7 +1100,8 @@ agentEventSchema.options.map((option) => option.shape.event.value);
1083
1100
  crashLooping: "platform.crashLooping",
1084
1101
  provisioned: "platform.provisioned",
1085
1102
  sandboxStarted: "platform.sandboxStarted",
1086
- sandboxStopped: "platform.sandboxStopped"
1103
+ sandboxStopped: "platform.sandboxStopped",
1104
+ restartedByNode: "platform.restartedByNode"
1087
1105
  }).rawLog;
1088
1106
  const agentStateSchema = z.enum([
1089
1107
  "created",
@@ -1180,46 +1198,11 @@ const degradedAgentSchema = z.object({
1180
1198
  z.object({
1181
1199
  status: z.enum(runtimeHealthStates),
1182
1200
  database: z.object({ ok: z.boolean() }),
1183
- engine: z.enum(runtimeEngineKinds),
1184
- docker: z.object({
1185
- state: z.enum(dockerStates),
1186
- lastError: z.string().nullable(),
1187
- checkedAt: z.iso.datetime().nullable()
1188
- }),
1189
- storage: z.object({
1190
- data: storageStateSchema,
1191
- engine: storageStateSchema
1192
- }),
1193
1201
  partitions: z.object({
1194
1202
  defaultRows: z.int().nonnegative(),
1195
1203
  ensuredThrough: z.string().nullable(),
1196
1204
  lastMaintenanceAt: z.iso.datetime().nullable()
1197
1205
  }),
1198
- reconcile: z.object({
1199
- lastTickAt: z.iso.datetime().nullable(),
1200
- lastTickError: z.string().nullable(),
1201
- agentsQueued: z.int().nonnegative(),
1202
- agentsActive: z.int().nonnegative()
1203
- }),
1204
- ingest: z.object({
1205
- readers: z.int().nonnegative(),
1206
- oldestCursorLagSeconds: z.number().nonnegative().nullable()
1207
- }),
1208
- sandbox: z.object({
1209
- enabled: z.boolean(),
1210
- image: z.object({
1211
- tag: z.string(),
1212
- digest: z.string()
1213
- }),
1214
- agentsWithSandbox: z.int().nonnegative(),
1215
- sidecarsRunning: z.int().nonnegative(),
1216
- volumeBytes: z.int().nonnegative().nullable()
1217
- }),
1218
- sessionLogs: z.object({
1219
- prunedDirectories: z.int().nonnegative(),
1220
- lastPrunedAt: z.iso.datetime().nullable(),
1221
- failures: z.int().nonnegative()
1222
- }),
1223
1206
  metrics: runtimeMetricsSchema,
1224
1207
  deadLetters: z.array(deadLetterQueueSchema),
1225
1208
  degradedAgents: z.array(degradedAgentSchema)
@@ -1442,17 +1425,21 @@ const memorySummarySchema = z.object({
1442
1425
  sessionId: z.string().nullable(),
1443
1426
  createdAt: z.iso.datetime()
1444
1427
  }).meta({ id: "MemorySummary" });
1445
- z.object({
1446
- items: z.array(memorySummarySchema),
1428
+ const memoryOutcomes = ["succeeded", "failed"];
1429
+ const memoryOutcomeSchema = z.enum(memoryOutcomes);
1430
+ pageSchema(memorySummarySchema).extend({
1447
1431
  source: z.enum(["worker", "unavailable"]),
1448
1432
  staleReason: z.enum([
1449
1433
  "unreachable",
1450
1434
  "timeout",
1451
1435
  "stopped"
1452
- ]).nullable()
1453
- }).meta({ id: "AgentMemoryList" });
1436
+ ]).nullable(),
1437
+ truncated: z.boolean()
1438
+ }).meta({ id: "AgentMemoryPage" });
1454
1439
  z.object({
1455
1440
  q: z.string().max(200).optional(),
1441
+ outcome: memoryOutcomeSchema.optional(),
1442
+ cursor: cursorSchema.optional(),
1456
1443
  limit: z.coerce.number().int().min(1).max(100).default(20)
1457
1444
  });
1458
1445
  const costPointSchema = z.object({
@@ -1565,7 +1552,7 @@ z.object({
1565
1552
  organizationId: idSchema,
1566
1553
  accountId: idSchema
1567
1554
  });
1568
- z.enum(["monitor", "executor"]);
1555
+ const sessionRoleSchema = z.enum(["monitor", "executor"]);
1569
1556
  const sessionSlotCountSchema = z.int().min(1).max(64);
1570
1557
  const sessionQueueEntrySchema = z.object({
1571
1558
  agentId: idSchema,
@@ -1574,8 +1561,21 @@ const sessionQueueEntrySchema = z.object({
1574
1561
  projectName: z.string().nullable(),
1575
1562
  position: z.int().positive(),
1576
1563
  since: z.iso.datetime(),
1577
- taskId: z.string().nullable()
1564
+ taskId: z.string().nullable(),
1565
+ taskTitle: z.string().nullable(),
1566
+ readable: z.boolean()
1578
1567
  }).meta({ id: "SessionQueueEntry" });
1568
+ const sessionHolderSchema = z.object({
1569
+ agentId: idSchema,
1570
+ agentName: z.string().nullable(),
1571
+ projectId: idSchema.nullable(),
1572
+ projectName: z.string().nullable(),
1573
+ since: z.iso.datetime().nullable(),
1574
+ taskId: z.string().nullable(),
1575
+ taskTitle: z.string().nullable(),
1576
+ readable: z.boolean(),
1577
+ pid: z.int().positive().nullable()
1578
+ }).meta({ id: "SessionHolder" });
1579
1579
  const sessionPoolSchema = z.object({
1580
1580
  slots: sessionSlotCountSchema.nullable(),
1581
1581
  effective: z.int().nonnegative(),
@@ -1602,6 +1602,12 @@ const agentAdmissionSchema = z.object({
1602
1602
  monitor: agentAdmissionStateSchema.nullable(),
1603
1603
  executor: agentAdmissionStateSchema.nullable()
1604
1604
  }).meta({ id: "AgentAdmission" });
1605
+ const admissionHoldSchema = z.object({
1606
+ pools: z.array(sessionRoleSchema),
1607
+ sessions: z.int().nonnegative(),
1608
+ target: z.string().nullable(),
1609
+ updating: z.boolean()
1610
+ }).meta({ id: "AdmissionHold" });
1605
1611
  const nodeStateSchema = z.enum([
1606
1612
  "pending",
1607
1613
  "online",
@@ -1670,16 +1676,36 @@ const poolUsageSchema = z.object({
1670
1676
  memoryBytes: z.int().nonnegative(),
1671
1677
  memoryMaxBytes: z.int().nonnegative().nullable(),
1672
1678
  cpus: z.number().nonnegative().nullable(),
1673
- memoryKills: z.int().nonnegative()
1679
+ memoryKills: z.int().nonnegative(),
1680
+ processors: z.int().positive().nullable().optional()
1674
1681
  }).meta({ id: "PoolUsage" });
1675
1682
  const nodePoolSchema = z.object({
1676
1683
  slots: z.int().nonnegative(),
1677
1684
  inUse: z.int().nonnegative(),
1678
1685
  waiting: z.array(sessionQueueEntrySchema),
1686
+ holders: z.array(sessionHolderSchema).nullable(),
1679
1687
  usage: poolUsageSchema.nullable()
1680
1688
  }).meta({ id: "NodePool" });
1689
+ const sessionRefusalsSchema = z.object({
1690
+ count: z.int().positive(),
1691
+ lastAt: z.iso.datetime(),
1692
+ pool: sessionRoleSchema,
1693
+ reason: z.string()
1694
+ }).meta({ id: "SessionRefusals" });
1695
+ const poolLayoutSchema = z.object({
1696
+ cores: z.array(z.int().positive()).min(1),
1697
+ cpus: z.object({
1698
+ monitor: z.number().positive(),
1699
+ executor: z.number().positive()
1700
+ }).nullable(),
1701
+ topologyError: z.string().nullable(),
1702
+ clamped: z.boolean()
1703
+ }).meta({ id: "PoolLayout" });
1681
1704
  const nodePoolsSchema = z.object({
1682
1705
  enforcement: poolEnforcementSchema,
1706
+ agentsSlice: poolEnforcementSchema.nullable(),
1707
+ refused: sessionRefusalsSchema.nullable(),
1708
+ layout: poolLayoutSchema.nullable(),
1683
1709
  monitor: nodePoolSchema,
1684
1710
  executor: nodePoolSchema
1685
1711
  }).meta({ id: "NodePools" });
@@ -1761,6 +1787,7 @@ const nodeUpdateSchema = z.object({
1761
1787
  target: z.string().nullable(),
1762
1788
  sessions: z.int().nonnegative(),
1763
1789
  since: z.iso.datetime().nullable(),
1790
+ held: z.array(sessionRoleSchema),
1764
1791
  last: z.object({
1765
1792
  outcome: z.enum([
1766
1793
  "updated",
@@ -1784,12 +1811,7 @@ const nodeAgentSchema = z.object({
1784
1811
  const nodeRuntimeSchema = z.object({
1785
1812
  reportedAt: z.iso.datetime(),
1786
1813
  docker: z.object({
1787
- state: z.enum([
1788
- "ok",
1789
- "unavailable",
1790
- "disabled",
1791
- "adopting"
1792
- ]),
1814
+ state: z.enum(dockerStates),
1793
1815
  lastError: z.string().nullable()
1794
1816
  }),
1795
1817
  storage: z.object({
@@ -1802,7 +1824,20 @@ const nodeRuntimeSchema = z.object({
1802
1824
  agentsQueued: z.int().nonnegative(),
1803
1825
  agentsActive: z.int().nonnegative()
1804
1826
  }),
1805
- ingest: z.object({ readers: z.int().nonnegative() }),
1827
+ ingest: z.object({
1828
+ readers: z.int().nonnegative(),
1829
+ oldestCursorLagSeconds: z.number().nonnegative().nullable()
1830
+ }),
1831
+ sandbox: z.object({
1832
+ enabled: z.boolean(),
1833
+ image: z.object({
1834
+ tag: z.string(),
1835
+ digest: z.string()
1836
+ }),
1837
+ agentsWithSandbox: z.int().nonnegative(),
1838
+ sidecarsRunning: z.int().nonnegative(),
1839
+ volumeBytes: z.int().nonnegative().nullable()
1840
+ }),
1806
1841
  sessions: z.object({
1807
1842
  monitor: z.object({
1808
1843
  effective: z.int().nonnegative(),
@@ -1821,6 +1856,28 @@ nodeSchema.extend({
1821
1856
  update: nodeUpdateSchema.nullable()
1822
1857
  }).meta({ id: "NodeDetail" });
1823
1858
  z.object({ items: z.array(nodeSchema) }).meta({ id: "NodeList" });
1859
+ const adminNodeSessionsSchema = z.object({
1860
+ effective: z.int().nonnegative(),
1861
+ inUse: z.int().nonnegative()
1862
+ });
1863
+ const nodeHealthSchema = z.object({
1864
+ docker: z.enum(dockerStates),
1865
+ storage: z.enum(storageStates),
1866
+ reconcileError: z.string().nullable()
1867
+ }).meta({ id: "NodeHealth" });
1868
+ const adminNodeSchema = nodeSchema.extend({
1869
+ organization: z.object({
1870
+ id: idSchema,
1871
+ name: z.string().nullable()
1872
+ }),
1873
+ update: nodeUpdateSchema.nullable(),
1874
+ sessions: z.object({
1875
+ monitor: adminNodeSessionsSchema,
1876
+ executor: adminNodeSessionsSchema
1877
+ }).nullable(),
1878
+ health: nodeHealthSchema.nullable()
1879
+ }).meta({ id: "AdminNode" });
1880
+ z.object({ items: z.array(adminNodeSchema) }).meta({ id: "AdminNodeList" });
1824
1881
  z.object({ nodeId: idSchema });
1825
1882
  z.object({
1826
1883
  organizationId: idSchema,
@@ -1894,17 +1951,51 @@ const splitFits = (split, host) => {
1894
1951
  const overflow = splitOverflowOf(split, host);
1895
1952
  return overflow.cpus <= 0 && overflow.memoryMb <= 0;
1896
1953
  };
1954
+ const poolProcessorsOf = (cpus, hostCpus) => Math.max(1, Math.min(Math.round(cpus), Math.round(hostCpus)));
1955
+ const layoutAppliesSplit = (layout, split) => layout?.cpus != null && split !== null && layout.cpus.monitor === split.monitor.cpus && layout.cpus.executor === split.executor.cpus;
1956
+ const splitCoresOf = (split, cores) => {
1957
+ const processors = cores.reduce((total, size) => total + size, 0);
1958
+ const sizeAt = (index) => cores[Math.min(index, cores.length - 1)] ?? 1;
1959
+ const coresFrom = (first, cpus) => {
1960
+ const wanted = poolProcessorsOf(cpus, processors);
1961
+ let taken = sizeAt(first);
1962
+ let count = 1;
1963
+ while (taken + sizeAt(first + count) <= wanted) {
1964
+ taken += sizeAt(first + count);
1965
+ count += 1;
1966
+ }
1967
+ return count;
1968
+ };
1969
+ const executor = coresFrom(0, split.executor.cpus);
1970
+ const monitor = coresFrom(executor, split.monitor.cpus);
1971
+ return {
1972
+ cores: cores.length,
1973
+ monitor,
1974
+ executor,
1975
+ fits: executor + monitor < cores.length
1976
+ };
1977
+ };
1978
+ //#endregion
1979
+ //#region ../../packages/contracts/src/appearance.ts
1980
+ const colorSchema = z.string().regex(hexColorPattern);
1981
+ const logoMaxBytes = 524288;
1982
+ const logoHashSchema = z.string().regex(logoHashPattern);
1983
+ z.instanceof(Uint8Array).meta({ description: "The bytes of a PNG, JPEG or WebP image." });
1984
+ z.instanceof(Uint8Array).meta({ description: "The bytes of the WebP image." });
1985
+ z.object({ v: logoHashSchema.optional() });
1897
1986
  //#endregion
1898
1987
  //#region ../../packages/contracts/src/identity.ts
1899
1988
  const emailSchema = z.email().max(254);
1900
1989
  const passwordSchema = z.string().min(12).max(128);
1901
1990
  const userStatusSchema = z.enum(["active", "disabled"]);
1902
1991
  const organizationNameSchema = z.string().trim().min(2).max(80);
1903
- const organizationSlugSchema = z.string().regex(slugPattern);
1992
+ const organizationSlugSchema = z.string().regex(slugPattern).refine((slug) => !uuidShapePattern.test(slug));
1904
1993
  const organizationSchema = z.object({
1905
1994
  id: idSchema,
1906
1995
  name: z.string(),
1907
1996
  slug: organizationSlugSchema,
1997
+ color: colorSchema,
1998
+ logoHash: logoHashSchema.nullable(),
1908
1999
  createdAt: z.iso.datetime()
1909
2000
  }).meta({ id: "Organization" });
1910
2001
  z.object({ items: z.array(organizationSchema) }).meta({ id: "OrganizationList" });
@@ -1962,6 +2053,11 @@ z.object({
1962
2053
  initialPassword: passwordSchema.optional()
1963
2054
  }).optional()
1964
2055
  }).meta({ id: "CreateOrganizationBody" });
2056
+ const updateOrganizationBodySchema = z.object({
2057
+ name: organizationNameSchema,
2058
+ slug: organizationSlugSchema,
2059
+ color: colorSchema
2060
+ }).meta({ id: "UpdateOrganizationBody" });
1965
2061
  z.object({ temporaryPassword: z.string() }).meta({ id: "TemporaryPassword" });
1966
2062
  z.object({ items: z.array(userSchema) }).meta({ id: "UserList" });
1967
2063
  z.object({ id: idSchema });
@@ -2030,6 +2126,9 @@ const auditActionSchema = z.enum([
2030
2126
  "platform.bootstrapped",
2031
2127
  "organization.created",
2032
2128
  "organization.renamed",
2129
+ "organization.updated",
2130
+ "organization.logo_set",
2131
+ "organization.logo_removed",
2033
2132
  "user.created",
2034
2133
  "user.role_changed",
2035
2134
  "user.disabled",
@@ -2043,6 +2142,8 @@ const auditActionSchema = z.enum([
2043
2142
  "token.revoked",
2044
2143
  "project.created",
2045
2144
  "project.updated",
2145
+ "project.logo_set",
2146
+ "project.logo_removed",
2046
2147
  "project.member_added",
2047
2148
  "project.member_role_changed",
2048
2149
  "project.member_removed",
@@ -2151,7 +2252,6 @@ pageSchema(auditEntrySchema).meta({ id: "AuditPage" });
2151
2252
  //#endregion
2152
2253
  //#region ../../packages/contracts/src/projects.ts
2153
2254
  const slugSchema = z.string().regex(slugPattern);
2154
- const colorSchema = z.string().regex(hexColorPattern);
2155
2255
  const projectSchema = z.object({
2156
2256
  id: idSchema,
2157
2257
  organizationId: idSchema,
@@ -2159,6 +2259,7 @@ const projectSchema = z.object({
2159
2259
  slug: slugSchema,
2160
2260
  description: z.string(),
2161
2261
  color: colorSchema,
2262
+ logoHash: logoHashSchema.nullable(),
2162
2263
  dockerAllowed: z.boolean(),
2163
2264
  memberCount: z.int().nonnegative(),
2164
2265
  createdAt: z.iso.datetime(),
@@ -2513,9 +2614,6 @@ const containerHealthSchema = z.enum([
2513
2614
  ]);
2514
2615
  const maxAgentDescriptionLength = 2e3;
2515
2616
  const maxAgentPromptLength = 64e3;
2516
- const minAgentCpus = .25;
2517
- const maxAgentMemoryMb = 16384;
2518
- const defaultAgentMemoryMb = 4096;
2519
2617
  const workspaceDirectorySchema = z.string().regex(/^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$/);
2520
2618
  const timeZones = new Set(Intl.supportedValuesOf("timeZone"));
2521
2619
  const timezoneSchema = z.string().trim().refine((value) => timeZones.has(value), { message: "expected an IANA time zone identifier" });
@@ -2526,11 +2624,13 @@ const agentDiskGbSchema = z.int().min(1).max(100);
2526
2624
  const agentMachineSchema = z.object({
2527
2625
  docker: z.boolean(),
2528
2626
  browser: z.boolean(),
2529
- cpus: z.number().min(minAgentCpus).max(8),
2530
- memoryMb: z.int().min(512).max(maxAgentMemoryMb),
2531
2627
  diskGb: agentDiskGbSchema
2532
- }).meta({ id: "AgentMachine" });
2628
+ });
2533
2629
  const agentMachineInputSchema = agentMachineSchema.extend({ diskGb: agentDiskGbSchema.optional() });
2630
+ const agentMachineDtoSchema = agentMachineSchema.extend({
2631
+ cpus: z.number().positive().optional().meta({ deprecated: true }),
2632
+ memoryMb: z.int().positive().optional().meta({ deprecated: true })
2633
+ }).meta({ id: "AgentMachine" });
2534
2634
  const agentPromptsSchema = z.object({
2535
2635
  monitor: agentPromptSchema,
2536
2636
  executor: agentPromptSchema
@@ -2587,6 +2687,7 @@ const agentRuntimeSchema = z.object({
2587
2687
  stopMode: agentStopModeSchema.nullable(),
2588
2688
  stopDeadlineAt: z.iso.datetime().nullable(),
2589
2689
  admission: agentAdmissionSchema.nullable(),
2690
+ admissionHold: admissionHoldSchema.nullable(),
2590
2691
  poolLimit: poolLimitSchema.nullable()
2591
2692
  }).meta({ id: "AgentRuntime" });
2592
2693
  const agentListItemSchema = z.object({
@@ -2615,7 +2716,7 @@ agentListItemSchema.extend({
2615
2716
  version: z.int().nonnegative(),
2616
2717
  description: z.string(),
2617
2718
  timezone: z.string(),
2618
- machine: agentMachineSchema,
2719
+ machine: agentMachineDtoSchema,
2619
2720
  settings: kikimoraSettingsSchema,
2620
2721
  effectiveSettings: workerSettingsSchema,
2621
2722
  prompts: agentPromptsSchema,
@@ -2757,13 +2858,20 @@ z.object({
2757
2858
  nodeId: idSchema
2758
2859
  }).meta({ id: "RestoreAgentBody" });
2759
2860
  z.object({ agentId: idSchema }).meta({ id: "RestoredAgent" });
2861
+ const agentBackupContentDigestSchema = z.string().regex(/^[0-9a-f]{64}$/u);
2760
2862
  z.object({
2761
2863
  agentId: idSchema,
2762
2864
  trigger: z.enum(agentBackupTriggers),
2763
2865
  kikimoraVersion: z.string().min(1).max(64),
2764
- entries: z.coerce.number().int().nonnegative()
2866
+ entries: z.coerce.number().int().nonnegative(),
2867
+ contentDigest: agentBackupContentDigestSchema.optional()
2765
2868
  });
2766
2869
  z.object({ backupId: idSchema });
2870
+ z.object({
2871
+ agentId: idSchema,
2872
+ contentDigest: agentBackupContentDigestSchema
2873
+ });
2874
+ z.object({ backupId: idSchema.nullable() });
2767
2875
  [
2768
2876
  "application/vnd.oci.image.index.v1+json",
2769
2877
  "application/vnd.oci.image.manifest.v1+json",
@@ -2771,21 +2879,12 @@ z.object({ backupId: idSchema });
2771
2879
  "application/vnd.docker.distribution.manifest.v2+json"
2772
2880
  ].join(", ");
2773
2881
  const pinnedPlatformImagePattern = /^[a-z0-9][a-z0-9.-]*(?::[0-9]{1,5})?(?:\/[a-z0-9][a-z0-9._-]*)+@sha256:[0-9a-f]{64}$/;
2774
- //#endregion
2775
- //#region ../../packages/contracts/src/infrastructure.ts
2776
- const volumeRoles = ["data", "agents"];
2777
- const volumeRoleSchema = z.enum(volumeRoles);
2882
+ const volumeRoleSchema = z.enum(["data", "agents"]);
2778
2883
  const filesystemNameSchema = z.enum([
2779
2884
  "root",
2780
2885
  "data",
2781
2886
  "agents"
2782
2887
  ]);
2783
- z.enum([
2784
- "digitalOcean",
2785
- "spaces",
2786
- "ghcr",
2787
- "backup"
2788
- ]);
2789
2888
  const backupKindSchema = z.enum(["scheduled", "manual"]);
2790
2889
  const backupStatusSchema = z.enum([
2791
2890
  "running",
@@ -2829,65 +2928,42 @@ const sha256HexPattern = /^[0-9a-f]{64}$/;
2829
2928
  const platformSecretPattern = /^[\x21-\x7e]+$/;
2830
2929
  const instantSchema$1 = z.iso.datetime();
2831
2930
  const byteCountSchema$1 = z.int().nonnegative();
2832
- const platformSecretSchema = z.string().trim().min(1).max(maxPlatformSecretLength).regex(platformSecretPattern, { message: "expected printable ASCII characters without spaces" });
2931
+ z.string().trim().min(1).max(maxPlatformSecretLength).regex(platformSecretPattern, { message: "expected printable ASCII characters without spaces" });
2833
2932
  const secretPresenceSchema = z.object({ present: z.boolean() }).meta({ id: "SecretPresence" });
2834
2933
  const regionSchema = z.string().regex(regionSlugPattern);
2934
+ const bucketNameSchema = z.string().regex(bucketNamePattern);
2835
2935
  const platformVolumeSchema = z.object({
2836
2936
  role: volumeRoleSchema,
2837
2937
  id: z.guid(),
2838
2938
  name: z.string().regex(volumeNamePattern)
2839
2939
  }).meta({ id: "PlatformVolume" });
2840
- const oneVolumePerRole = (volumes) => volumes.length === volumeRoles.length && volumeRoles.every((role) => volumes.some((volume) => volume.role === role));
2841
- const platformVolumesSchema = z.array(platformVolumeSchema).refine(oneVolumePerRole, { message: `expected exactly one volume for each role: ${volumeRoles.join(", ")}` });
2940
+ const countOfRole = (volumes, role) => volumes.filter((volume) => volume.role === role).length;
2941
+ const oneDataVolumeAndAtMostOneAgentsVolume = (volumes) => countOfRole(volumes, "data") === 1 && countOfRole(volumes, "agents") <= 1;
2942
+ const platformVolumesSchema = z.array(platformVolumeSchema).refine(oneDataVolumeAndAtMostOneAgentsVolume, { message: "expected one data volume and at most one agents volume" });
2842
2943
  const isOrigin = (value) => {
2843
2944
  if (!URL.canParse(value)) return false;
2844
2945
  const url = new URL(value);
2845
2946
  return url.pathname === "/" && url.search === "" && url.hash === "" && url.username === "" && url.password === "";
2846
2947
  };
2847
2948
  const spacesEndpointSchema = z.url({ protocol: /^https?$/ }).max(2e3).refine(isOrigin, { message: "expected an origin without a path, a query, a fragment or credentials" });
2848
- const digitalOceanShape = {
2849
- dropletId: z.int().positive(),
2850
- region: regionSchema,
2851
- reservedIp: z.ipv4(),
2852
- volumes: platformVolumesSchema
2853
- };
2854
- const spacesShape = {
2855
- endpoint: spacesEndpointSchema,
2856
- region: regionSchema,
2857
- bucket: z.string().regex(bucketNamePattern)
2858
- };
2859
2949
  const backupSettingsSchema = z.object({ retentionDays: z.int().min(1).max(maxBackupRetentionDays) }).meta({ id: "BackupSettings" });
2860
2950
  z.object({
2861
- version: z.int().nonnegative(),
2862
2951
  instance: z.string(),
2863
2952
  domain: z.string(),
2864
2953
  digitalOcean: z.object({
2865
- ...digitalOceanShape,
2866
- token: secretPresenceSchema
2954
+ dropletId: z.int().positive(),
2955
+ region: regionSchema,
2956
+ reservedIp: z.ipv4(),
2957
+ volumes: platformVolumesSchema
2867
2958
  }).nullable(),
2868
2959
  spaces: z.object({
2869
- ...spacesShape,
2870
- accessKey: secretPresenceSchema,
2871
- secretKey: secretPresenceSchema
2960
+ endpoint: spacesEndpointSchema,
2961
+ region: regionSchema,
2962
+ bucket: bucketNameSchema
2872
2963
  }).nullable(),
2873
2964
  ghcrToken: secretPresenceSchema,
2874
- backup: backupSettingsSchema,
2875
- updatedAt: instantSchema$1
2965
+ backup: backupSettingsSchema
2876
2966
  }).meta({ id: "PlatformSettings" });
2877
- z.object({
2878
- version: z.int().nonnegative(),
2879
- digitalOcean: z.object({
2880
- ...digitalOceanShape,
2881
- token: platformSecretSchema.nullable()
2882
- }).nullable(),
2883
- spaces: z.object({
2884
- ...spacesShape,
2885
- accessKey: platformSecretSchema.nullable(),
2886
- secretKey: platformSecretSchema.nullable()
2887
- }).nullable(),
2888
- ghcrToken: platformSecretSchema.nullable(),
2889
- backup: backupSettingsSchema.nullable()
2890
- }).meta({ id: "UpdatePlatformSettingsBody" });
2891
2967
  const storageReadingSchema = z.discriminatedUnion("measured", [z.object({
2892
2968
  measured: z.literal(true),
2893
2969
  path: z.string(),
@@ -2935,19 +3011,6 @@ const nodeVolumesSchema = z.discriminatedUnion("source", [z.object({
2935
3011
  source: z.literal("ok"),
2936
3012
  items: z.array(nodeVolumeSchema)
2937
3013
  }), unavailableBecause(["not_configured"])]).meta({ id: "NodeVolumes" });
2938
- const dropletSizeSchema = z.object({
2939
- slug: z.string(),
2940
- vcpus: z.int().positive(),
2941
- memoryMb: z.int().positive(),
2942
- diskGb: z.int().positive(),
2943
- priceMonthly: z.number().nonnegative(),
2944
- command: z.string()
2945
- }).meta({ id: "DropletSize" });
2946
- const nodeSizesSchema = z.discriminatedUnion("source", [z.object({
2947
- source: z.literal("ok"),
2948
- items: z.array(dropletSizeSchema)
2949
- }), unavailableBecause(providerUnavailableReasons)]).meta({ id: "NodeSizes" });
2950
- const capacityForecastSchema = z.object({ byDisk: z.int().nonnegative().nullable() }).meta({ id: "CapacityForecast" });
2951
3014
  z.object({
2952
3015
  droplet: nodeDropletSchema,
2953
3016
  volumes: nodeVolumesSchema,
@@ -2956,23 +3019,14 @@ z.object({
2956
3019
  data: storageReadingSchema,
2957
3020
  agents: storageReadingSchema
2958
3021
  }),
2959
- sizes: nodeSizesSchema,
2960
3022
  platform: z.object({
2961
3023
  version: z.string(),
2962
3024
  instance: z.string(),
2963
3025
  scriptsVersion: z.string(),
2964
3026
  dockerVersion: z.string().nullable(),
2965
3027
  dockerRootDir: z.string().nullable()
2966
- }),
2967
- maintenance: z.object({
2968
- quiescedAt: instantSchema$1.nullable(),
2969
- pending: z.array(idSchema)
2970
- }),
2971
- capacity: capacityForecastSchema
3028
+ })
2972
3029
  }).meta({ id: "PlatformNode" });
2973
- z.object({ role: volumeRoleSchema });
2974
- z.object({ sizeGb: z.int().positive() }).meta({ id: "ResizeVolumeBody" });
2975
- z.object({ agents: z.int().nonnegative() }).meta({ id: "QuiesceResult" });
2976
3030
  const requesterSchema = z.object({
2977
3031
  userId: idSchema,
2978
3032
  label: z.string().nullable()
@@ -3076,22 +3130,17 @@ const registryUnavailableSchema = z.object({
3076
3130
  detail: z.string().nullable()
3077
3131
  }).meta({ id: "RegistryUnavailable" });
3078
3132
  const hostOperationsAvailabilities = ["enabled", "disabled"];
3079
- const backupsAvailabilities = ["enabled", "unavailable"];
3080
3133
  const installedImageSchema = z.string().regex(pinnedPlatformImagePattern).nullable();
3081
3134
  z.object({
3082
3135
  version: z.string(),
3136
+ environment: z.string().nullable(),
3083
3137
  apiImage: installedImageSchema,
3084
3138
  webImage: installedImageSchema,
3085
3139
  nodeImage: installedImageSchema,
3086
3140
  latest: z.string().nullable(),
3087
3141
  latestError: registryUnavailableSchema.nullable(),
3088
- versions: z.array(z.string()),
3089
- upgradeable: z.boolean(),
3090
- scriptsVersion: z.string(),
3091
- expectedScriptsVersion: z.string(),
3092
3142
  imagePrefix: z.string(),
3093
3143
  hostOperations: z.enum(hostOperationsAvailabilities),
3094
- backups: z.enum(backupsAvailabilities),
3095
3144
  operations: z.array(hostOperationSchema)
3096
3145
  }).meta({ id: "Platform" });
3097
3146
  const keyVersionSchema = z.int().positive();
@@ -3114,13 +3163,15 @@ z.object({
3114
3163
  versions: z.array(masterKeyVersionSchema),
3115
3164
  rotationPending: z.boolean()
3116
3165
  }).meta({ id: "MasterKey" });
3117
- z.object({ version: z.string().regex(versionTagPattern) }).meta({ id: "UpgradePlatformBody" });
3118
- const metricsRangeSchema = z.enum([
3166
+ //#endregion
3167
+ //#region ../../packages/contracts/src/metrics.ts
3168
+ const metricsRanges = [
3119
3169
  "1h",
3120
3170
  "24h",
3121
3171
  "7d",
3122
3172
  "90d"
3123
- ]);
3173
+ ];
3174
+ const metricsRangeSchema = z.enum(metricsRanges);
3124
3175
  const agentMetricsRanges = [
3125
3176
  "1h",
3126
3177
  "24h",
@@ -3174,18 +3225,13 @@ const agentUsageSchema = z.object({
3174
3225
  blockReadBytes: byteCountSchema,
3175
3226
  blockWriteBytes: byteCountSchema
3176
3227
  }).meta({ id: "AgentUsage" });
3177
- const nodeHostSeriesSchema = z.object({
3178
- nodeId: idSchema,
3179
- nodeName: z.string(),
3180
- points: z.array(hostPointSchema)
3181
- }).meta({ id: "NodeHostSeries" });
3182
3228
  z.object({
3183
3229
  resolution: metricsResolutionSchema,
3184
3230
  from: instantSchema,
3185
3231
  to: instantSchema,
3186
- nodes: z.array(nodeHostSeriesSchema),
3232
+ points: z.array(hostPointSchema),
3187
3233
  agents: z.array(agentUsageSchema)
3188
- }).meta({ id: "HostMetrics" });
3234
+ }).meta({ id: "NodeMetrics" });
3189
3235
  z.object({
3190
3236
  resolution: metricsResolutionSchema,
3191
3237
  from: instantSchema,
@@ -3195,24 +3241,6 @@ z.object({
3195
3241
  }).meta({ id: "AgentMetrics" });
3196
3242
  z.object({ range: metricsRangeSchema });
3197
3243
  z.object({ range: agentMetricsRangeSchema });
3198
- z.object({
3199
- nodeId: idSchema,
3200
- agentId: idSchema
3201
- });
3202
- z.object({ agentIds: z.array(idSchema).min(1).max(500) });
3203
- z.object({ occupied: z.array(idSchema) });
3204
- z.object({ offset: z.coerce.number().int().nonnegative() });
3205
- z.object({ bytes: z.int().nonnegative() });
3206
- z.object({
3207
- bytes: z.int().nonnegative(),
3208
- sha256: z.string().regex(/^[0-9a-f]{64}$/u)
3209
- });
3210
- z.object({
3211
- bytes: z.int().nonnegative(),
3212
- files: z.int().nonnegative(),
3213
- memoryCheck: z.string()
3214
- });
3215
- z.object({ removed: z.boolean() });
3216
3244
  const notificationKindSchema = z.enum([
3217
3245
  "credential.verification_failed",
3218
3246
  "credential.expiring",
@@ -3240,7 +3268,8 @@ const notificationTargetTypeSchema = z.enum([
3240
3268
  "backup",
3241
3269
  "queue",
3242
3270
  "volume",
3243
- "platform"
3271
+ "platform",
3272
+ "node"
3244
3273
  ]);
3245
3274
  const hostOperationOutcomes = [
3246
3275
  "succeeded",
@@ -3368,6 +3397,7 @@ const detailsOf$1 = (body) => {
3368
3397
  const parsed = errorBodySchema.safeParse(body);
3369
3398
  return parsed.success ? parsed.data.details ?? {} : {};
3370
3399
  };
3400
+ const hintText = (hint, details) => typeof hint === "string" ? hint : hint(details);
3371
3401
  //#endregion
3372
3402
  //#region src/client/api.ts
3373
3403
  const defaultTimeoutMs = 3e4;
@@ -3424,7 +3454,7 @@ const createApi = ({ origin, token, fetch = globalThis.fetch, timeoutMs = defaul
3424
3454
  const refused = (action, { error, response }, hints) => {
3425
3455
  const failure = new ApiError(response.status, codeOf(error), action, detailsOf$1(error));
3426
3456
  const hint = failure.code === null ? void 0 : hints[failure.code];
3427
- return hint === void 0 ? failure : new CliError(failure.message, hint, { cause: failure });
3457
+ return hint === void 0 ? failure : new CliError(failure.message, hintText(hint, failure.details), { cause: failure });
3428
3458
  };
3429
3459
  const dataOf = (action, answer, hints = {}) => {
3430
3460
  if (answer.data === void 0) throw refused(action, answer, hints);
@@ -3796,7 +3826,7 @@ const createLoginCommand = (adaptersOf) => defineCommand({
3796
3826
  }
3797
3827
  });
3798
3828
  const authorizeInBrowser = async ({ adapters, api, origin, prompt }) => {
3799
- const started = dataOf("start the sign-in", await api.POST("/auth/cli/authorize", { body: { clientName: adapters.clientName() } }), { "infrastructure.not_configured": `${origin} has no administrator yet; the instance is seeded during kcloud-admin init.` });
3829
+ const started = dataOf("start the sign-in", await api.POST("/auth/cli/authorize", { body: { clientName: adapters.clientName() } }), { "infrastructure.not_configured": `${origin} has no administrator yet; the platform is seeded during pnpm platform bootstrap.` });
3800
3830
  prompt.note([
3801
3831
  `Code ${started.userCode}`,
3802
3832
  `Address ${started.verificationUri}`,
@@ -4144,6 +4174,13 @@ const nodeDetailOf = async (api, { organizationId, nodeId }) => dataOf("describe
4144
4174
  organizationId,
4145
4175
  nodeId
4146
4176
  } } }), nodeHints);
4177
+ const nodeMetricsOf = async (api, { organizationId, nodeId }, range) => dataOf("show what the node used", await api.GET("/organizations/{organizationId}/nodes/{nodeId}/metrics", { params: {
4178
+ path: {
4179
+ organizationId,
4180
+ nodeId
4181
+ },
4182
+ query: { range }
4183
+ } }), nodeHints);
4147
4184
  const createdNode = async (api, organizationId, body) => {
4148
4185
  validated(createNodeBodySchema, body, "a node");
4149
4186
  return providerDataOf("create the node", await api.POST("/organizations/{organizationId}/nodes", {
@@ -4243,10 +4280,15 @@ const runOf = async (adaptersOf, { settings, context }) => {
4243
4280
  //#region src/commands/agents/shared.ts
4244
4281
  const agentOption = { project: { type: "string" } };
4245
4282
  const agentUsage = [["--project <name>", "the project of the agent, when the agent is named alone"]];
4283
+ const actionsAfterPause = /* @__PURE__ */ new Set([
4284
+ "update",
4285
+ "restart",
4286
+ "resume"
4287
+ ]);
4246
4288
  const agentHints = {
4247
4289
  "agent.not_found": "kcloud agents list --project <name> shows the agents.",
4248
4290
  "project.not_found": "kcloud projects list shows the projects you are a member of.",
4249
- "agent.invalid_transition": "kcloud agents show <agent> lists the actions it takes now.",
4291
+ "agent.invalid_transition": ({ state, action }) => state === "pausing" && actionsAfterPause.has(String(action)) ? "The agent finishes its current session before it pauses; the action goes through once kcloud agents show <agent> says paused." : "kcloud agents show <agent> lists the actions it takes now.",
4250
4292
  "agent.busy": "The agent is working on another action; try again in a moment.",
4251
4293
  "concurrency.conflict": "Someone changed the agent meanwhile; read it again and retry.",
4252
4294
  "agent.node_unsuitable": "The node lacks a capability the agent requires; kcloud nodes show <node> lists what it offers.",
@@ -4474,12 +4516,56 @@ const enforcementText = ({ state, reason }) => {
4474
4516
  case "unsupported": return `The node holds no pool to a limit: ${reason === null ? "it cannot" : unsupportedReasons[reason] ?? reason}.`;
4475
4517
  }
4476
4518
  };
4477
- const memoryOf = (pool) => {
4519
+ const agentsSliceReasons = {
4520
+ cgroup_driver: "Docker on the host does not use the systemd cgroup driver",
4521
+ cgroup_driver_unread: "the node has not read the cgroup driver of Docker yet",
4522
+ host_unread: "the node has not read its host yet",
4523
+ configure_failed: "systemd did not take the limits",
4524
+ pending: "the node has not applied the limit yet",
4525
+ platform: "the node runs no pool service"
4526
+ };
4527
+ const agentsSliceText = ({ state, reason }) => {
4528
+ switch (state) {
4529
+ case "enforced": return "The node holds its agents and their sessions together to the host minus its reserve.";
4530
+ case "unsplit": return "The node has no split, so its agents outside their sessions have no limit.";
4531
+ case "unsupported": return `The node holds its agents outside their sessions to no limit: ${reason === null ? "it cannot" : agentsSliceReasons[reason] ?? reason}.`;
4532
+ }
4533
+ };
4534
+ const refusedText = ({ count, pool, lastAt, reason }) => `The session starter refused ${count === 1 ? "1 session" : `${count} sessions`} of the ${pool} pool in the last hour, the last at ${lastAt}: ${reason}`;
4535
+ const layoutTexts = (layout, split) => [...layout.topologyError === null ? [] : [`The node did not read its processor cores, so each logical processor counts as a core and a pool can share a physical core: ${layout.topologyError}`], ...layout.clamped && layoutAppliesSplit(layout, split) ? ["The split does not fit the cores of the node, so its pools run on fewer cores than the split asks for; kcloud nodes split <node> shows the cores each pool takes."] : []];
4536
+ const memoryOf$1 = (pool) => {
4478
4537
  if (pool.usage === null) return "-";
4479
4538
  const { memoryBytes, memoryMaxBytes } = pool.usage;
4480
4539
  return memoryMaxBytes === null ? bytesOf(memoryBytes) : `${bytesOf(memoryBytes)} of ${bytesOf(memoryMaxBytes)}`;
4481
4540
  };
4482
4541
  const roles = ["monitor", "executor"];
4542
+ const workOf = (role, { taskId, taskTitle, readable }) => {
4543
+ if (readable === false) return "another project";
4544
+ if (taskId === null) return role === "monitor" ? "monitor cycle" : "-";
4545
+ return taskTitle == null ? taskId : `${taskId} ${taskTitle}`;
4546
+ };
4547
+ const holdersOf = (pools) => {
4548
+ if (roles.every((role) => pools[role].holders == null)) return null;
4549
+ const held = roles.flatMap((role) => (pools[role].holders ?? []).map((holder) => [
4550
+ role,
4551
+ holder.projectName ?? "-",
4552
+ holder.agentName ?? holder.agentId,
4553
+ workOf(role, holder),
4554
+ holder.since ?? "-",
4555
+ holder.pid == null ? "-" : String(holder.pid)
4556
+ ]));
4557
+ return held.length === 0 ? "No session holds a slot." : renderTable([
4558
+ { header: "HOLDS" },
4559
+ { header: "PROJECT" },
4560
+ { header: "AGENT" },
4561
+ { header: "WORK" },
4562
+ { header: "SINCE" },
4563
+ {
4564
+ header: "PID",
4565
+ align: "right"
4566
+ }
4567
+ ], held);
4568
+ };
4483
4569
  const queueOf = (pools) => {
4484
4570
  const waiting = roles.flatMap((role) => pools[role].waiting.map((entry) => [
4485
4571
  role,
@@ -4487,7 +4573,7 @@ const queueOf = (pools) => {
4487
4573
  entry.projectName ?? "-",
4488
4574
  entry.agentName ?? entry.agentId,
4489
4575
  entry.since,
4490
- entry.taskId ?? "-"
4576
+ workOf(role, entry)
4491
4577
  ]));
4492
4578
  return waiting.length === 0 ? "No session waits for a slot." : renderTable([
4493
4579
  { header: "WAITS FOR" },
@@ -4498,37 +4584,44 @@ const queueOf = (pools) => {
4498
4584
  { header: "PROJECT" },
4499
4585
  { header: "AGENT" },
4500
4586
  { header: "SINCE" },
4501
- { header: "TASK" }
4587
+ { header: "WORK" }
4502
4588
  ], waiting);
4503
4589
  };
4504
- const poolsText = (pools) => [
4505
- enforcementText(pools.enforcement),
4506
- renderTable([
4507
- { header: "POOL" },
4508
- { header: "SLOTS IN USE" },
4509
- {
4510
- header: "CPU IN USE",
4511
- align: "right"
4512
- },
4513
- { header: "MEMORY IN USE" },
4514
- {
4515
- header: "MEMORY KILLS",
4516
- align: "right"
4517
- }
4518
- ], roles.map((role) => {
4519
- const pool = pools[role];
4520
- const cpus = pool.usage?.cpus ?? null;
4521
- return [
4522
- role,
4523
- `${pool.inUse} of ${pool.slots}`,
4524
- cpus === null ? "-" : cpus.toFixed(2),
4525
- memoryOf(pool),
4526
- pool.usage === null ? "-" : String(pool.usage.memoryKills)
4527
- ];
4528
- })),
4529
- "",
4530
- queueOf(pools)
4531
- ].join("\n");
4590
+ const poolsText = (pools, split) => {
4591
+ const holders = holdersOf(pools);
4592
+ return [
4593
+ enforcementText(pools.enforcement),
4594
+ ...pools.agentsSlice == null ? [] : [agentsSliceText(pools.agentsSlice)],
4595
+ ...pools.refused == null ? [] : [refusedText(pools.refused)],
4596
+ ...pools.layout == null ? [] : layoutTexts(pools.layout, split),
4597
+ renderTable([
4598
+ { header: "POOL" },
4599
+ { header: "SLOTS IN USE" },
4600
+ {
4601
+ header: "CPU IN USE",
4602
+ align: "right"
4603
+ },
4604
+ { header: "MEMORY IN USE" },
4605
+ {
4606
+ header: "MEMORY KILLS",
4607
+ align: "right"
4608
+ }
4609
+ ], roles.map((role) => {
4610
+ const pool = pools[role];
4611
+ const cpus = pool.usage?.cpus ?? null;
4612
+ return [
4613
+ role,
4614
+ `${pool.inUse} of ${pool.slots}`,
4615
+ cpus === null ? "-" : cpus.toFixed(2),
4616
+ memoryOf$1(pool),
4617
+ pool.usage === null ? "-" : String(pool.usage.memoryKills)
4618
+ ];
4619
+ })),
4620
+ ...holders === null ? [] : ["", holders],
4621
+ "",
4622
+ queueOf(pools)
4623
+ ].join("\n");
4624
+ };
4532
4625
  //#endregion
4533
4626
  //#region src/commands/agents/plan.ts
4534
4627
  const agentPlanOf = (agent) => ({
@@ -4551,8 +4644,36 @@ const agentPlanOf = (agent) => ({
4551
4644
  prompts: agent.prompts,
4552
4645
  settings: agent.settings,
4553
4646
  timezone: agent.timezone,
4554
- machine: agent.machine
4647
+ machine: {
4648
+ docker: agent.machine.docker,
4649
+ browser: agent.machine.browser,
4650
+ diskGb: agent.machine.diskGb
4651
+ }
4555
4652
  });
4653
+ const machineLimitKeys = /* @__PURE__ */ new Set(["cpus", "memoryMb"]);
4654
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4655
+ const warnIgnored = ({ context }, keys) => {
4656
+ const named = keys.map((key) => `machine.${key}`).join(" and ");
4657
+ context.terminal.error(context.terminal.errorStyles.warning(`warning ${named} ${keys.length === 1 ? "is" : "are"} ignored: an agent has no limits of its own, and the pools of its node limit its sessions.`));
4658
+ };
4659
+ const machineWithoutLimits = (run, machine) => {
4660
+ if (!isRecord(machine)) return machine;
4661
+ const ignored = Object.keys(machine).filter((key) => machineLimitKeys.has(key));
4662
+ if (ignored.length === 0) return machine;
4663
+ warnIgnored(run, ignored);
4664
+ return Object.fromEntries(Object.entries(machine).filter(([key]) => !machineLimitKeys.has(key)));
4665
+ };
4666
+ const agentFileWithoutLimits = (run, document) => isRecord(document) && "machine" in document ? {
4667
+ ...document,
4668
+ machine: machineWithoutLimits(run, document.machine)
4669
+ } : document;
4670
+ const agentChangeWithoutLimits = (run, document) => isRecord(document) && isRecord(document.sections) && "machine" in document.sections ? {
4671
+ ...document,
4672
+ sections: {
4673
+ ...document.sections,
4674
+ machine: machineWithoutLimits(run, document.sections.machine)
4675
+ }
4676
+ } : document;
4556
4677
  //#endregion
4557
4678
  //#region src/commands/agents/config.ts
4558
4679
  const agentsOf = async (scope, project) => {
@@ -4663,7 +4784,7 @@ const configCommands = (adaptersOf) => [
4663
4784
  const agentId = await agentFrom(run, invocation.positionals[0], invocation.values.project);
4664
4785
  const file = invocation.values.file;
4665
4786
  if (file === void 0) throw new UsageError("agents set needs --file <path>.", "The file holds the sections to change; kcloud agents show <agent> --json shows the current ones.");
4666
- const agent = await changedAgent(run.api, agentId, await documentOf(run, file));
4787
+ const agent = await changedAgent(run.api, agentId, agentChangeWithoutLimits(run, await documentOf(run, file)));
4667
4788
  return printed(run, agent, (changed) => changed.restartRequired ? `Changed ${changed.name}; it needs kcloud agents update to take the change.` : `Changed ${changed.name}.`);
4668
4789
  }
4669
4790
  }),
@@ -4708,6 +4829,15 @@ const admissionRowsOf = (agent) => {
4708
4829
  const { admission } = agent.runtime;
4709
4830
  return admission === null ? [] : [["monitor slot", slotOf(admission.monitor)], ["executor slot", slotOf(admission.executor)]];
4710
4831
  };
4832
+ const admissionHoldRowsOf = ({ runtime }) => {
4833
+ const hold = runtime.admissionHold;
4834
+ if (hold == null) return [];
4835
+ if (hold.updating) return [["admissions", `held while the node updates to ${hold.target ?? "-"}`]];
4836
+ const all = hold.pools.includes("monitor");
4837
+ const noun = all ? "session" : "executor session";
4838
+ const waits = `${String(hold.sessions)} ${noun}${hold.sessions === 1 ? "" : "s"}`;
4839
+ return [["admissions", `${all ? "held" : "executor held"} for the node update to ${hold.target ?? "-"}, waiting for ${waits}`]];
4840
+ };
4711
4841
  const poolLimitRowsOf = ({ runtime }) => runtime.poolLimit === null ? [] : [["pool limit", poolLimitText(runtime.poolLimit)]];
4712
4842
  const detailsOf = (agent) => [
4713
4843
  renderTable([{ header: "FIELD" }, { header: "VALUE" }], [
@@ -4720,12 +4850,13 @@ const detailsOf = (agent) => [
4720
4850
  ["node", agent.node === null ? "none" : `${agent.node.name} (${agent.node.state})`],
4721
4851
  ["requires", agent.requiredCapabilities.length === 0 ? "nothing" : agent.requiredCapabilities.join(", ")],
4722
4852
  ["timezone", agent.timezone],
4723
- ["machine", `${agent.machine.cpus} vCPU, ${agent.machine.memoryMb} MB`],
4853
+ ["disk", `${String(agent.machine.diskGb)} GB`],
4724
4854
  ["docker", agent.machine.docker ? "yes" : "no"],
4725
4855
  ["browser", agent.machine.browser ? "yes" : "no"],
4726
4856
  ["container", agent.runtime.containerId ?? "none"],
4727
4857
  ["health", agent.runtime.health],
4728
4858
  ...admissionRowsOf(agent),
4859
+ ...admissionHoldRowsOf(agent),
4729
4860
  ...poolLimitRowsOf(agent),
4730
4861
  ["pending", agent.restartRequired ? agent.restartReasons.join(", ") : "-"],
4731
4862
  ["actions", agent.availableActions.join(", ")],
@@ -4914,15 +5045,13 @@ const askedPlan = async (run, projectId, values, required) => {
4914
5045
  timezone: values.timezone ?? localTimezone(),
4915
5046
  machine: {
4916
5047
  docker: values.docker,
4917
- browser: values.browser,
4918
- cpus: 2,
4919
- memoryMb: defaultAgentMemoryMb
5048
+ browser: values.browser
4920
5049
  }
4921
5050
  };
4922
5051
  };
4923
5052
  const isObject = (document) => typeof document === "object" && document !== null && !Array.isArray(document);
4924
5053
  const fileOf = async (run, projectId, { file, node }, required) => {
4925
- const document = await documentOf(run, file);
5054
+ const document = agentFileWithoutLimits(run, await documentOf(run, file));
4926
5055
  const placed = !isObject(document) || node === void 0 && "nodeId" in document ? document : {
4927
5056
  ...document,
4928
5057
  nodeId: await placedNodeId(run, {
@@ -5061,30 +5190,29 @@ const lifecycleCommands = (adaptersOf) => agentActions.map((action) => defineCom
5061
5190
  }));
5062
5191
  //#endregion
5063
5192
  //#region src/client/pages.ts
5064
- const windowOf = (limit, all, fallback = 50) => {
5193
+ const windowOf = (limit, all, fallback = 50, largest = 200) => {
5065
5194
  if (limit === void 0) return {
5066
5195
  limit: fallback,
5067
5196
  all
5068
5197
  };
5069
5198
  const size = Number(limit);
5070
- if (!/^[0-9]+$/u.test(limit) || size < 1 || size > 200) throw new UsageError(`--limit ${limit} is not a whole number from 1 to 200.`);
5199
+ if (!/^[0-9]+$/u.test(limit) || size < 1 || size > largest) throw new UsageError(`--limit ${limit} is not a whole number from 1 to ${largest}.`);
5071
5200
  return {
5072
5201
  limit: size,
5073
5202
  all
5074
5203
  };
5075
5204
  };
5076
- const collect = async (page, { limit, all }) => {
5077
- const first = await page(null, Math.min(limit, 200));
5205
+ const collect = async (page, { limit, all }, largest = 200) => {
5206
+ const first = await page(null, Math.min(limit, largest));
5078
5207
  if (!all) return first;
5079
5208
  const items = [...first.items];
5080
- let cursor = first.nextCursor;
5081
- while (cursor !== null) {
5082
- const next = await page(cursor, 200);
5083
- items.push(...next.items);
5084
- cursor = next.nextCursor;
5209
+ let last = first;
5210
+ while (last.nextCursor !== null) {
5211
+ last = await page(last.nextCursor, largest);
5212
+ items.push(...last.items);
5085
5213
  }
5086
5214
  return {
5087
- ...first,
5215
+ ...last,
5088
5216
  items,
5089
5217
  nextCursor: null
5090
5218
  };
@@ -5131,6 +5259,43 @@ const transcriptOf = async (api, agentId, sessionId, all) => collect(async (curs
5131
5259
  limit: 200,
5132
5260
  all
5133
5261
  });
5262
+ const memoryOf = (api, agentId, { query, outcome }, window) => collect(async (cursor, limit) => dataOf("search what the agent remembers", await api.GET("/agents/{id}/memory", { params: {
5263
+ path: { id: agentId },
5264
+ query: {
5265
+ limit,
5266
+ ...query === void 0 ? {} : { q: query },
5267
+ ...outcome === void 0 ? {} : { outcome },
5268
+ ...cursor === null ? {} : { cursor }
5269
+ }
5270
+ } }), agentHints), window, 100);
5271
+ const memoryReasons = {
5272
+ stopped: "the agent is not running",
5273
+ unreachable: "the agent did not answer on its control socket",
5274
+ timeout: "the agent did not answer in time"
5275
+ };
5276
+ const unreadReasonOf = (page) => page.staleReason === null ? "the agent did not answer" : memoryReasons[page.staleReason];
5277
+ const memoryText = (page) => {
5278
+ const unread = page.source === "unavailable";
5279
+ if (unread && page.items.length === 0) return `The memory cannot be read: ${unreadReasonOf(page)}.`;
5280
+ if (page.items.length === 0) return "The agent remembers nothing that matches.";
5281
+ return [
5282
+ renderTable([
5283
+ { header: "WHEN" },
5284
+ { header: "OUTCOME" },
5285
+ { header: "TASK" },
5286
+ { header: "TITLE" },
5287
+ { header: "HEADLINE" }
5288
+ ], page.items.map((summary) => [
5289
+ summary.createdAt,
5290
+ summary.ok ? "ok" : "failed",
5291
+ summary.taskId,
5292
+ summary.title,
5293
+ summary.headline
5294
+ ])),
5295
+ ...unread ? ["", `The rest of the memory cannot be read: ${unreadReasonOf(page)}.`] : [],
5296
+ ...page.truncated ? ["", `The agent returns at most 100 summaries, so the list may miss older ones.`] : []
5297
+ ].join("\n");
5298
+ };
5134
5299
  const observabilityCommands = (adaptersOf) => [
5135
5300
  defineCommand({
5136
5301
  name: "sessions",
@@ -5206,7 +5371,9 @@ const observabilityCommands = (adaptersOf) => [
5206
5371
  const run = await runOf(adaptersOf, invocation);
5207
5372
  const agentId = await agentFrom(run, invocation.positionals[0], invocation.values.project);
5208
5373
  const page = await transcriptOf(run.api, agentId, invocation.positionals[1] ?? "", invocation.values.all);
5209
- return printed(run, page.items, (entries) => entries.length === 0 ? "The session left no transcript." : entries.map((entry) => JSON.stringify(entry)).join("\n"));
5374
+ const code = printed(run, page.items, (entries) => entries.length === 0 ? "The session left no transcript." : entries.map((entry) => JSON.stringify(entry)).join("\n"));
5375
+ if (page.nextCursor !== null && !run.settings.json) run.context.terminal.error("More entries follow; --all prints the whole session.");
5376
+ return code;
5210
5377
  }
5211
5378
  }),
5212
5379
  defineCommand({
@@ -5342,44 +5509,35 @@ const observabilityCommands = (adaptersOf) => [
5342
5509
  defineCommand({
5343
5510
  name: "memory",
5344
5511
  summary: "Search what an agent remembers of its tasks.",
5345
- usage: "agents memory <agent> [--project <name>] [--query <text>] [--limit <n>]",
5512
+ usage: "agents memory <agent> [--project <name>] [--query <text>] [--outcome <outcome>] [--limit <n>] [--all]",
5346
5513
  optionsUsage: [
5347
5514
  ...agentUsage,
5348
5515
  ["--query <text>", "only the summaries that match this text"],
5349
- ["--limit <n>", "how many summaries (default: 20)"]
5516
+ ["--outcome <outcome>", `only the summaries of one outcome: ${memoryOutcomes.join(", ")}`],
5517
+ ["--limit <n>", `how many summaries (default: 20)`],
5518
+ allUsage
5350
5519
  ],
5351
5520
  options: {
5352
5521
  ...agentOption,
5522
+ ...pageOptions,
5353
5523
  query: { type: "string" },
5354
- limit: { type: "string" }
5524
+ outcome: { type: "string" }
5355
5525
  },
5356
5526
  positionals: {
5357
5527
  min: 1,
5358
5528
  max: 1
5359
5529
  },
5360
5530
  run: async (invocation) => {
5531
+ const { values } = invocation;
5361
5532
  const run = await runOf(adaptersOf, invocation);
5362
- const agentId = await agentFrom(run, invocation.positionals[0], invocation.values.project);
5363
- const { limit } = windowOf(invocation.values.limit, false, 20);
5364
- const search = invocation.values.query;
5365
- const listed = dataOf("search what the agent remembers", await run.api.GET("/agents/{id}/memory", { params: {
5366
- path: { id: agentId },
5367
- query: {
5368
- limit,
5369
- ...search === void 0 ? {} : { q: search }
5370
- }
5371
- } }), agentHints);
5372
- return printed(run, listed, (shown) => shown.items.length === 0 ? "The agent remembers nothing that matches." : renderTable([
5373
- { header: "WHEN" },
5374
- { header: "OUTCOME" },
5375
- { header: "TITLE" },
5376
- { header: "HEADLINE" }
5377
- ], shown.items.map((summary) => [
5378
- summary.createdAt,
5379
- summary.ok ? "ok" : "failed",
5380
- summary.title,
5381
- summary.headline
5382
- ])));
5533
+ const agentId = await agentFrom(run, invocation.positionals[0], values.project);
5534
+ const outcome = memoryOutcomes.find((candidate) => candidate === values.outcome);
5535
+ if (values.outcome !== void 0 && outcome === void 0) throw new UsageError(`--outcome ${values.outcome} is not an outcome of a summary.`, `The outcomes are ${memoryOutcomes.join(", ")}.`);
5536
+ const page = await memoryOf(run.api, agentId, {
5537
+ query: values.query,
5538
+ outcome
5539
+ }, windowOf(values.limit, values.all, 20, 100));
5540
+ return printed(run, page, memoryText);
5383
5541
  }
5384
5542
  })
5385
5543
  ];
@@ -5953,25 +6111,59 @@ const secretUsage = [
5953
6111
  //#region src/commands/credentials.ts
5954
6112
  const projectOption$2 = { project: { type: "string" } };
5955
6113
  const projectUsage$2 = [["--project <name>", "the project the credential belongs to, by slug, name or id"]];
6114
+ const usageUnavailable = "Only a Claude Code OAuth token reports the usage of a subscription.";
5956
6115
  const credentialHints = {
5957
6116
  "project.not_found": "kcloud projects list shows the projects you are a member of.",
5958
6117
  "credential.not_found": "kcloud credentials list --project <name> shows them.",
5959
6118
  "credential.label_taken": "Another credential of the project already has that label.",
5960
- "credential.in_use": "Point the agents at another credential first."
6119
+ "credential.in_use": "Point the agents at another credential first.",
6120
+ "credential.usage_unavailable": usageUnavailable
6121
+ };
6122
+ const percent = 100;
6123
+ const notApplicable = "-";
6124
+ const usedOf = (window) => window === null ? notApplicable : `${String(Math.round(window.utilization * percent))}%`;
6125
+ const shareOf = (window) => window?.status === "rejected" ? "limit" : usedOf(window);
6126
+ const usageSummaryOf = ({ kind, usage }) => {
6127
+ if (kind !== "claude_oauth_token") return notApplicable;
6128
+ if (usage === null) return "no data";
6129
+ const summary = `5h ${shareOf(usage.fiveHour)} 7d ${shareOf(usage.sevenDay)}`;
6130
+ return usage.stale ? `${summary} (stale)` : summary;
6131
+ };
6132
+ const usageReportOf = (credential) => {
6133
+ const { usage } = credential;
6134
+ if (usage === null) return `No usage of ${credential.label} was read yet.`;
6135
+ const windows = [["5h", usage.fiveHour], ["7d", usage.sevenDay]];
6136
+ return `${renderTable([
6137
+ { header: "WINDOW" },
6138
+ { header: "USED" },
6139
+ { header: "STATUS" },
6140
+ { header: "RESETS" }
6141
+ ], windows.map(([name, window]) => [
6142
+ name,
6143
+ usedOf(window),
6144
+ window?.status ?? notApplicable,
6145
+ window?.resetsAt ?? notApplicable
6146
+ ]))}\n${usage.stale ? `Read at ${usage.readAt}; the latest reading failed.` : `Read at ${usage.readAt}.`}`;
6147
+ };
6148
+ const isoDateLength = 10;
6149
+ const expiryOf = ({ expiresAt, expiresAtComputed }) => {
6150
+ if (expiresAt === null) return "never";
6151
+ return expiresAtComputed ? `about ${expiresAt.slice(0, isoDateLength)}` : expiresAt;
5961
6152
  };
5962
6153
  const projectFrom$2 = async (run, given) => {
5963
6154
  if (given === void 0) throw new UsageError("This command needs a project.", "Pass --project <name>; kcloud projects list shows them.");
5964
6155
  return resolveProjectId(run, given);
5965
6156
  };
5966
6157
  const credentialsOf = async (api, projectId) => dataOf("list the credentials of the project", await api.GET("/projects/{id}/credentials", { params: { path: { id: projectId } } }), credentialHints).items;
5967
- const resolveCredentialId = async (api, projectId, reference) => {
6158
+ const resolveCredential = async (api, projectId, reference) => {
5968
6159
  const credentials = await credentialsOf(api, projectId);
5969
6160
  const found = credentials.filter((credential) => credential.id === reference || credential.label === reference);
5970
6161
  const [first] = found;
5971
6162
  if (first === void 0) throw new CliError(`The project has no credential ${reference}.`, credentials.length === 0 ? "kcloud credentials add makes one." : `Its credentials are ${credentials.map(({ label }) => label).join(", ")}.`);
5972
6163
  if (found.length > 1) throw new CliError(`${reference} names ${found.length} credentials of the project.`, "Name it by its id instead.");
5973
- return first.id;
6164
+ return first;
5974
6165
  };
6166
+ const resolveCredentialId = async (api, projectId, reference) => (await resolveCredential(api, projectId, reference)).id;
5975
6167
  const createCredentialsCommand = (adaptersOf) => defineGroup({
5976
6168
  name: "credentials",
5977
6169
  summary: "The Claude and GitHub credentials of a project.",
@@ -5997,13 +6189,15 @@ const createCredentialsCommand = (adaptersOf) => defineGroup({
5997
6189
  { header: "FINGERPRINT" },
5998
6190
  { header: "VERIFIED" },
5999
6191
  { header: "EXPIRES" },
6192
+ { header: "USAGE" },
6000
6193
  { header: "ID" }
6001
6194
  ], credentials.map((credential) => [
6002
6195
  credential.label,
6003
6196
  credential.kind,
6004
6197
  credential.fingerprint,
6005
6198
  credential.verification,
6006
- credential.expiresAt ?? "never",
6199
+ expiryOf(credential),
6200
+ usageSummaryOf(credential),
6007
6201
  credential.id
6008
6202
  ])));
6009
6203
  }
@@ -6149,6 +6343,34 @@ const createCredentialsCommand = (adaptersOf) => defineGroup({
6149
6343
  return said(run, `The instance is verifying ${credentialId}; kcloud credentials list shows the answer.`);
6150
6344
  }
6151
6345
  }),
6346
+ defineCommand({
6347
+ name: "usage",
6348
+ summary: "Show the usage of the Claude subscription behind a Claude Code OAuth token.",
6349
+ usage: "credentials usage <credential> --project <name> [--refresh]",
6350
+ optionsUsage: [...projectUsage$2, ["--refresh", "read it from Anthropic now; within 30 seconds the last reading"]],
6351
+ options: {
6352
+ ...projectOption$2,
6353
+ refresh: {
6354
+ type: "boolean",
6355
+ default: false
6356
+ }
6357
+ },
6358
+ positionals: {
6359
+ min: 1,
6360
+ max: 1
6361
+ },
6362
+ run: async (invocation) => {
6363
+ const run = await runOf(adaptersOf, invocation);
6364
+ const projectId = await projectFrom$2(run, invocation.values.project);
6365
+ const stored = await resolveCredential(run.api, projectId, invocation.positionals[0] ?? "");
6366
+ if (stored.kind !== "claude_oauth_token") throw new CliError(`${stored.label} is a ${stored.kind}, which reports no usage.`, usageUnavailable);
6367
+ const credential = invocation.values.refresh ? dataOf("read the usage of the credential", await run.api.POST("/projects/{id}/credentials/{credentialId}/usage", { params: { path: {
6368
+ id: projectId,
6369
+ credentialId: stored.id
6370
+ } } }), credentialHints) : stored;
6371
+ return printed(run, credential, usageReportOf);
6372
+ }
6373
+ }),
6152
6374
  defineCommand({
6153
6375
  name: "delete",
6154
6376
  summary: "Delete a credential no agent uses.",
@@ -6534,6 +6756,115 @@ const agentTools = [
6534
6756
  run: async ({ action, now, ...reference }, run) => actionOn(run, await agentIdOf(run, reference), action, action === "stop" && now)
6535
6757
  })
6536
6758
  ];
6759
+ const percentOf = (ratio) => ratio === null ? "-" : `${(ratio * 100).toFixed(1)}%`;
6760
+ const disksOf = (point) => [
6761
+ point.diskRootUsedRatio,
6762
+ point.diskDataUsedRatio,
6763
+ point.diskAgentsUsedRatio
6764
+ ].map(percentOf).join(" / ");
6765
+ const gpuMemoryOf = ({ gpuMemoryUsedBytesAvg: used, gpuMemoryTotalBytes: total }) => used === null || total === null ? "-" : `${bytesOf(used)} of ${bytesOf(total)}`;
6766
+ const gpuOf = (point) => `${percentOf(point.gpuUtilRatioAvg)}, ${gpuMemoryOf(point)}`;
6767
+ const readsAGpu = (points) => points.some((point) => point.gpuUtilRatioAvg !== null || point.gpuMemoryTotalBytes !== null);
6768
+ const hostText$1 = (points) => {
6769
+ const gpu = readsAGpu(points);
6770
+ return renderTable([
6771
+ { header: "AT" },
6772
+ {
6773
+ header: "CPU",
6774
+ align: "right"
6775
+ },
6776
+ {
6777
+ header: "MEMORY",
6778
+ align: "right"
6779
+ },
6780
+ {
6781
+ header: "LOAD",
6782
+ align: "right"
6783
+ },
6784
+ {
6785
+ header: "DISKS ROOT / DATA / AGENTS",
6786
+ align: "right"
6787
+ },
6788
+ ...gpu ? [{
6789
+ header: "GPU",
6790
+ align: "right"
6791
+ }] : []
6792
+ ], points.map((point) => [
6793
+ point.ts,
6794
+ percentOf(point.cpuRatioAvg),
6795
+ `${bytesOf(point.memoryUsedBytesAvg)} of ${bytesOf(point.memoryTotalBytes)}`,
6796
+ point.load1Avg === null ? "-" : point.load1Avg.toFixed(2),
6797
+ disksOf(point),
6798
+ ...gpu ? [gpuOf(point)] : []
6799
+ ]));
6800
+ };
6801
+ const byFootprint = (left, right) => right.memoryBytesAvg - left.memoryBytesAvg || left.agentName.localeCompare(right.agentName);
6802
+ const agentsText = (agents) => renderTable([
6803
+ { header: "PROJECT" },
6804
+ { header: "AGENT" },
6805
+ {
6806
+ header: "CPU (CORES)",
6807
+ align: "right"
6808
+ },
6809
+ {
6810
+ header: "MEMORY AVG",
6811
+ align: "right"
6812
+ },
6813
+ {
6814
+ header: "MEMORY MAX",
6815
+ align: "right"
6816
+ },
6817
+ {
6818
+ header: "NETWORK IN / OUT",
6819
+ align: "right"
6820
+ },
6821
+ {
6822
+ header: "DISK READ / WRITE",
6823
+ align: "right"
6824
+ }
6825
+ ], agents.toSorted(byFootprint).map((usage) => [
6826
+ usage.projectName,
6827
+ usage.agentName,
6828
+ usage.cpuRatioAvg === null ? "-" : usage.cpuRatioAvg.toFixed(2),
6829
+ bytesOf(usage.memoryBytesAvg),
6830
+ bytesOf(usage.memoryBytesMax),
6831
+ `${bytesOf(usage.networkRxBytes)} / ${bytesOf(usage.networkTxBytes)}`,
6832
+ `${bytesOf(usage.blockReadBytes)} / ${bytesOf(usage.blockWriteBytes)}`
6833
+ ]));
6834
+ const nodeMetricsText = (metrics) => [
6835
+ `${metrics.from} to ${metrics.to}, one point per ${metrics.resolution}.`,
6836
+ "",
6837
+ metrics.points.length === 0 ? "The node sent no sample in that range." : hostText$1(metrics.points),
6838
+ "",
6839
+ metrics.agents.length === 0 ? "No agent you can see ran on the node in that range." : agentsText(metrics.agents)
6840
+ ].join("\n");
6841
+ const rangeOf = (given) => {
6842
+ if (given === void 0) return "1h";
6843
+ const range = metricsRanges.find((candidate) => candidate === given);
6844
+ if (range === void 0) throw new UsageError(`--range ${given} is not a range of metrics.`, `The ranges are ${metricsRanges.join(", ")}.`);
6845
+ return range;
6846
+ };
6847
+ const createNodeMetricsCommand = (adaptersOf) => defineCommand({
6848
+ name: "metrics",
6849
+ summary: "Show what the host of a node and the agents on it used over a range.",
6850
+ usage: "nodes metrics <node> [--range <range>]",
6851
+ optionsUsage: [
6852
+ ...nodeUsage,
6853
+ ["--range <range>", `how far back: ${metricsRanges.join(", ")} (default: 1h)`],
6854
+ ["", "the agents are those of the projects you belong to, or all for an admin"]
6855
+ ],
6856
+ options: { range: { type: "string" } },
6857
+ positionals: {
6858
+ min: 1,
6859
+ max: 1
6860
+ },
6861
+ run: async (invocation) => {
6862
+ const range = rangeOf(invocation.values.range);
6863
+ const run = await runOf(adaptersOf, invocation);
6864
+ const reference = await resolveNode(run, invocation.positionals[0] ?? "");
6865
+ return printed(run, await nodeMetricsOf(run.api, reference, range), nodeMetricsText);
6866
+ }
6867
+ });
6537
6868
  //#endregion
6538
6869
  //#region src/commands/nodes/split.ts
6539
6870
  const splitFields = [
@@ -6589,6 +6920,8 @@ const changeOfParts = ({ reserve, monitor, executor }) => {
6589
6920
  const cpusText = (cpus) => String(Math.round(cpus * 100) / 100);
6590
6921
  const hostText = (host) => `${cpusText(host.cpus)} CPU and ${hostMemoryMbOf(host)} MB`;
6591
6922
  const excessText = ({ cpus, memoryMb }) => [...cpus > 0 ? [`${cpusText(cpus)} CPU`] : [], ...memoryMb > 0 ? [`${memoryMb} MB`] : []].join(" and ");
6923
+ const coresText = (count) => count === 1 ? "1 core" : `${String(count)} cores`;
6924
+ const coresOfNode = (node) => node.runtime?.pools.layout?.cores ?? null;
6592
6925
  const fittingSplit = (node, change, nameOf) => {
6593
6926
  const merged = {
6594
6927
  ...node.split === null ? {} : valuesOf(node.split),
@@ -6602,9 +6935,25 @@ const fittingSplit = (node, change, nameOf) => {
6602
6935
  validated(nodeSplitSchema, split, "a split of a node");
6603
6936
  if (node.host === null) throw new CliError(`${node.name} has not reported its CPUs and memory yet, so it cannot be split.`, "The node reports them once its service runs; kcloud nodes show <node> shows its state.");
6604
6937
  if (!splitFits(split, node.host)) throw new CliError(`The split asks for ${excessText(splitOverflowOf(split, node.host))} more than ${node.name} has.`, `${node.name} has ${hostText(node.host)}; shrink a pool or the reserve.`);
6938
+ const cores = coresOfNode(node);
6939
+ const fit = cores === null ? null : splitCoresOf(split, cores);
6940
+ if (fit !== null && !fit.fits) throw new CliError(`The pools take whole cores of ${node.name}: the executor pool would get ${coresText(fit.executor)} and the monitor pool ${coresText(fit.monitor)} of ${String(fit.cores)}, and the reserve keeps at least 1.`, "Shrink a pool, so the reserve keeps the core of processor 0.");
6605
6941
  return split;
6606
6942
  };
6943
+ const processorCountsOf = (node) => {
6944
+ const pools = node.runtime?.pools;
6945
+ if (pools === void 0 || !layoutAppliesSplit(pools.layout, node.split)) return null;
6946
+ const monitor = pools.monitor.usage?.processors ?? null;
6947
+ const executor = pools.executor.usage?.processors ?? null;
6948
+ return monitor === null && executor === null ? null : {
6949
+ monitor,
6950
+ executor
6951
+ };
6952
+ };
6953
+ const processorsText = (count) => count === 1 ? "1 processor" : `${String(count)} processors`;
6954
+ const poolCpusText = (cpus, processors) => processors === null ? cpusText(cpus) : `${cpusText(cpus)} (${processorsText(processors)})`;
6607
6955
  const splitViewOf = (node) => {
6956
+ const cores = coresOfNode(node);
6608
6957
  const overflow = node.split === null || node.host === null ? null : splitOverflowOf(node.split, node.host);
6609
6958
  return {
6610
6959
  id: node.id,
@@ -6615,6 +6964,8 @@ const splitViewOf = (node) => {
6615
6964
  memoryMb: hostMemoryMbOf(node.host)
6616
6965
  },
6617
6966
  split: node.split,
6967
+ processors: processorCountsOf(node),
6968
+ cores: cores === null || node.split === null ? null : splitCoresOf(node.split, cores),
6618
6969
  free: overflow === null ? null : {
6619
6970
  cpus: -overflow.cpus,
6620
6971
  memoryMb: -overflow.memoryMb
@@ -6624,10 +6975,12 @@ const splitViewOf = (node) => {
6624
6975
  const resourceCells = ({ cpus, memoryMb }) => [cpusText(cpus), `${memoryMb} MB`];
6625
6976
  const oneSlotOf = ({ cpus, memoryMb, slots }) => `${cpusText(cpus / slots)} CPU, ${Math.floor(memoryMb / slots)} MB`;
6626
6977
  const hostLineOf = (view) => view.host === null ? `${view.name} has not reported its CPUs and memory yet.` : `The host has ${cpusText(view.host.cpus)} CPU and ${view.host.memoryMb} MB.`;
6978
+ const coresLineOf = ({ cores, executor, monitor, fits }) => fits ? `The pools take whole cores: the executor pool ${coresText(executor)} and the monitor pool ${coresText(monitor)}; the reserve keeps ${coresText(cores - executor - monitor)} of ${String(cores)}.` : `The executor pool would take ${coresText(executor)} and the monitor pool ${coresText(monitor)} of ${String(cores)}, so the node runs them on fewer cores and keeps 1 for the reserve.`;
6627
6979
  const splitTableOf = (view, split) => {
6628
6980
  const poolRow = (role) => [
6629
6981
  role,
6630
- ...resourceCells(split[role]),
6982
+ poolCpusText(split[role].cpus, view.processors?.[role] ?? null),
6983
+ `${String(split[role].memoryMb)} MB`,
6631
6984
  String(split[role].slots),
6632
6985
  oneSlotOf(split[role])
6633
6986
  ];
@@ -6675,7 +7028,8 @@ const splitTableOf = (view, split) => {
6675
7028
  { header: "ONE SLOT" }
6676
7029
  ], rows),
6677
7030
  ...view.host === null ? ["", hostLineOf(view)] : [],
6678
- ...over === "" ? [] : ["", `The split asks for ${over} more than the host has.`]
7031
+ ...over === "" ? [] : ["", `The split asks for ${over} more than the host has.`],
7032
+ ...view.cores === null ? [] : ["", coresLineOf(view.cores)]
6679
7033
  ].join("\n");
6680
7034
  };
6681
7035
  const splitText = (view) => view.split === null ? [
@@ -6683,10 +7037,14 @@ const splitText = (view) => view.split === null ? [
6683
7037
  hostLineOf(view),
6684
7038
  `kcloud nodes split set ${view.name} with every part splits it.`
6685
7039
  ].join("\n") : splitTableOf(view, view.split);
6686
- const splitSummaryOf = (split) => {
7040
+ const splitSummaryOf = (split, processors = null) => {
6687
7041
  if (split === null) return "none, slots counted from the host without hard limits";
6688
7042
  const { reserve, monitor, executor } = split;
6689
- const pool = (role, { cpus, memoryMb, slots }) => `${role} ${cpusText(cpus)} CPU, ${memoryMb} MB, ${slots} slots`;
7043
+ const pool = (role, { cpus, memoryMb, slots }) => {
7044
+ const count = processors?.[role] ?? null;
7045
+ const onProcessors = count === null ? "" : ` (${processorsText(count)})`;
7046
+ return `${role} ${cpusText(cpus)} CPU${onProcessors}, ${memoryMb} MB, ${slots} slots`;
7047
+ };
6690
7048
  return [
6691
7049
  `reserve ${cpusText(reserve.cpus)} CPU, ${reserve.memoryMb} MB`,
6692
7050
  pool("monitor", monitor),
@@ -6868,6 +7226,18 @@ const nodeTools = [
6868
7226
  input: z.object({ node: nodeField }),
6869
7227
  run: async ({ node }, run) => nodeDetailOf(run.api, await resolveNode(run, node))
6870
7228
  }),
7229
+ defineTool({
7230
+ name: "node_metrics",
7231
+ title: "Show what the host of a node and its agents used",
7232
+ description: "Show the CPU, memory, load, disks and GPU of the host of a node over a range, and what each agent on it used over the whole range: the agents of the projects the caller belongs to, or every agent for an admin of the organization.",
7233
+ operations: ["getNodeMetrics"],
7234
+ safety: "read",
7235
+ input: z.object({
7236
+ node: nodeField,
7237
+ range: z.enum(metricsRanges).default("1h")
7238
+ }),
7239
+ run: async ({ node, range }, run) => nodeMetricsOf(run.api, await resolveNode(run, node), range)
7240
+ }),
6871
7241
  defineTool({
6872
7242
  name: "node_split_set",
6873
7243
  title: "Split a node into pools",
@@ -7010,19 +7380,31 @@ const credentialTools = [
7010
7380
  }),
7011
7381
  defineTool({
7012
7382
  name: "credential_update",
7013
- title: "Rename or verify a credential",
7014
- description: "rename gives a credential a new label; verify asks the instance to check it against its service, and credentials_list shows the answer.",
7015
- operations: ["renameCredential", "verifyCredential"],
7383
+ title: "Rename, verify or read the usage of a credential",
7384
+ description: "rename gives a credential a new label; verify asks the instance to check it against its service, and credentials_list shows the answer; refresh_usage reads the usage of the Claude subscription behind a Claude Code OAuth token now, or answers the last reading within 30 seconds of the previous one.",
7385
+ operations: [
7386
+ "renameCredential",
7387
+ "verifyCredential",
7388
+ "refreshCredentialUsage"
7389
+ ],
7016
7390
  safety: "write",
7017
7391
  input: z.object({
7018
7392
  project: projectField,
7019
7393
  credential: credentialField$1,
7020
- action: z.enum(["rename", "verify"]),
7394
+ action: z.enum([
7395
+ "rename",
7396
+ "verify",
7397
+ "refresh_usage"
7398
+ ]),
7021
7399
  label: z.string().trim().min(1).optional().describe("The new label, for rename.")
7022
7400
  }),
7023
7401
  run: async ({ project, credential, action, label }, run) => {
7024
7402
  const projectId = await projectIdOf(run, project);
7025
7403
  const credentialId = await resolveCredentialId(run.api, projectId, credential);
7404
+ if (action === "refresh_usage") return dataOf("read the usage of the credential", await run.api.POST("/projects/{id}/credentials/{credentialId}/usage", { params: { path: {
7405
+ id: projectId,
7406
+ credentialId
7407
+ } } }), credentialHints);
7026
7408
  if (action === "verify") {
7027
7409
  doneWith("verify the credential", await run.api.POST("/projects/{id}/credentials/{credentialId}/verify", { params: { path: {
7028
7410
  id: projectId,
@@ -7363,21 +7745,23 @@ const observabilityTools = [
7363
7745
  defineTool({
7364
7746
  name: "agent_memory_search",
7365
7747
  title: "Search the memory of an agent",
7366
- description: "Search what an agent remembers of the tasks it finished.",
7748
+ description: "Search what an agent remembers of the tasks it finished, newest first, one page or every summary with all.",
7367
7749
  operations: ["searchAgentMemory"],
7368
7750
  safety: "read",
7369
7751
  input: z.object({
7370
7752
  ...agentFields,
7371
7753
  query: z.string().max(200).optional(),
7372
- limit: z.int().min(1).max(100).default(20)
7754
+ outcome: memoryOutcomeSchema.optional().describe("Only the summaries of tasks that succeeded or failed."),
7755
+ limit: z.int().min(1).max(100).default(20),
7756
+ all: pageFields.all
7373
7757
  }),
7374
- run: async ({ query, limit, ...reference }, run) => dataOf("search what the agent remembers", await run.api.GET("/agents/{id}/memory", { params: {
7375
- path: { id: await agentIdOf(run, reference) },
7376
- query: {
7377
- limit,
7378
- ...query === void 0 ? {} : { q: query }
7379
- }
7380
- } }), agentHints)
7758
+ run: async ({ query, outcome, limit, all, ...reference }, run) => memoryOf(run.api, await agentIdOf(run, reference), {
7759
+ query,
7760
+ outcome
7761
+ }, {
7762
+ limit,
7763
+ all
7764
+ })
7381
7765
  }),
7382
7766
  defineTool({
7383
7767
  name: "agent_earlier_output",
@@ -7442,6 +7826,70 @@ const observabilityTools = [
7442
7826
  })
7443
7827
  ];
7444
7828
  //#endregion
7829
+ //#region src/commands/logo.ts
7830
+ const logoOptions = {
7831
+ logo: { type: "string" },
7832
+ "no-logo": {
7833
+ type: "boolean",
7834
+ default: false
7835
+ }
7836
+ };
7837
+ const logoUsage = [["--logo <path>", "show a PNG, JPEG or WebP image instead of the color, at most 512 KB and 2048 px on a side"], ["--no-logo", "remove the logo and show the color again"]];
7838
+ const logoHints = {
7839
+ "logo.unreadable": "The file has to be a PNG, JPEG or WebP image.",
7840
+ "logo.too_small": "The image needs at least 64 pixels on each side.",
7841
+ "logo.too_large": "The image may have at most 2048 pixels on a side."
7842
+ };
7843
+ const startsWith = (bytes, prefix, at = 0) => prefix.every((byte, index) => bytes[at + index] === byte);
7844
+ const ascii = (text) => Array.from(text, (letter) => letter.charCodeAt(0));
7845
+ const signatures = [
7846
+ {
7847
+ contentType: "image/png",
7848
+ matches: (bytes) => startsWith(bytes, [
7849
+ 137,
7850
+ ...ascii("PNG\r\n"),
7851
+ 26,
7852
+ 10
7853
+ ])
7854
+ },
7855
+ {
7856
+ contentType: "image/jpeg",
7857
+ matches: (bytes) => startsWith(bytes, [
7858
+ 255,
7859
+ 216,
7860
+ 255
7861
+ ])
7862
+ },
7863
+ {
7864
+ contentType: "image/webp",
7865
+ matches: (bytes) => startsWith(bytes, ascii("RIFF")) && startsWith(bytes, ascii("WEBP"), 8)
7866
+ }
7867
+ ];
7868
+ const isLogoChange = (change) => change.logo !== void 0 || change["no-logo"] === true;
7869
+ const logoFileOf = async (workingDirectory, file) => {
7870
+ const path = resolve(workingDirectory, file);
7871
+ const bytes = await readFile(path).catch(() => {
7872
+ throw new CliError(`${path} could not be read.`);
7873
+ });
7874
+ if (bytes.byteLength > 524288) throw new UsageError(`${file} holds ${String(Math.ceil(bytes.byteLength / 1024))} KB, and a logo at most ${String(logoMaxBytes / 1024)} KB.`);
7875
+ const contentType = signatures.find(({ matches }) => matches(bytes))?.contentType;
7876
+ if (contentType === void 0) throw new UsageError(`${file} is not a PNG, JPEG or WebP image.`);
7877
+ return {
7878
+ bytes,
7879
+ contentType
7880
+ };
7881
+ };
7882
+ const logoActionOf = async (workingDirectory, change) => {
7883
+ if (change.logo !== void 0 && change["no-logo"] === true) throw new UsageError("--logo and --no-logo exclude each other.");
7884
+ if (change.logo !== void 0) return logoFileOf(workingDirectory, change.logo);
7885
+ return change["no-logo"] === true ? "remove" : null;
7886
+ };
7887
+ const logoUpload = ({ bytes, contentType }) => ({
7888
+ body: bytes,
7889
+ bodySerializer: (body) => body,
7890
+ headers: { "content-type": contentType }
7891
+ });
7892
+ //#endregion
7445
7893
  //#region src/commands/roles.ts
7446
7894
  const roleOption = { role: { type: "string" } };
7447
7895
  const roleUsage = ["--role <role>", "admin or member"];
@@ -7751,6 +8199,48 @@ const costsText = (costs, { from, to }, slugs) => [
7751
8199
  "",
7752
8200
  `${costs.totalCostUsd.toFixed(2)} over ${costs.totalRuns} runs and ${costs.sessions} sessions.`
7753
8201
  ].join("\n");
8202
+ const organizationChangeHints = {
8203
+ ...organizationHints,
8204
+ "organization.slug_taken": "Another organization already uses that slug.",
8205
+ "organization.invalid_slug": "A slug holds lowercase letters, digits and hyphens.",
8206
+ "auth.forbidden": "Only an admin of the organization changes it.",
8207
+ ...logoHints
8208
+ };
8209
+ const followSlug = async (homeDirectory, origin, before, after) => {
8210
+ const store = await readInstanceStore(homeDirectory);
8211
+ const stored = store.instances[origin];
8212
+ if (stored?.organization !== before.slug) return;
8213
+ await writeInstanceStore(homeDirectory, withInstance(store, origin, {
8214
+ ...stored,
8215
+ organization: after.slug
8216
+ }));
8217
+ };
8218
+ const updatedOrganization = async (api, current, fields) => {
8219
+ if ([
8220
+ fields.name,
8221
+ fields.slug,
8222
+ fields.color
8223
+ ].every((field) => field === void 0)) return current;
8224
+ const body = {
8225
+ name: fields.name ?? current.name,
8226
+ slug: fields.slug ?? current.slug,
8227
+ color: fields.color ?? current.color
8228
+ };
8229
+ validated(updateOrganizationBodySchema, body, "a change to an organization");
8230
+ return dataOf("change the organization", await api.PATCH("/organizations/{organizationId}", {
8231
+ params: { path: { organizationId: current.id } },
8232
+ body
8233
+ }), organizationChangeHints);
8234
+ };
8235
+ const withLogo = async (api, organization, logo) => {
8236
+ const params = { path: { organizationId: organization.id } };
8237
+ if (logo === null) return organization;
8238
+ if (logo === "remove") return dataOf("remove the logo of the organization", await api.DELETE("/organizations/{organizationId}/logo", { params }), organizationChangeHints);
8239
+ return dataOf("set the logo of the organization", await api.PUT("/organizations/{organizationId}/logo", {
8240
+ params,
8241
+ ...logoUpload(logo)
8242
+ }), organizationChangeHints);
8243
+ };
7754
8244
  const createOrgsCommand = (adaptersOf) => defineGroup({
7755
8245
  name: "orgs",
7756
8246
  summary: "The organizations you can see, and the one kcloud acts in.",
@@ -7835,11 +8325,52 @@ const createOrgsCommand = (adaptersOf) => defineGroup({
7835
8325
  return printed(run, organization, (shown) => renderTable([{ header: "FIELD" }, { header: "VALUE" }], [
7836
8326
  ["name", shown.name],
7837
8327
  ["slug", shown.slug],
8328
+ ["color", shown.color],
8329
+ ["logo", shown.logoHash ?? "-"],
7838
8330
  ["created", shown.createdAt],
7839
8331
  ["id", shown.id]
7840
8332
  ]));
7841
8333
  }
7842
8334
  }),
8335
+ defineCommand({
8336
+ name: "set",
8337
+ summary: "Change the name, slug, color or logo of an organization.",
8338
+ usage: "orgs set [<organization>] [--name <name>] [--slug <slug>] [--color <#rrggbb>] [--logo <path> | --no-logo]",
8339
+ optionsUsage: [
8340
+ ["--name <name>", "the new name"],
8341
+ ["--slug <slug>", "the new slug; scripts and --org that name the old one stop finding it"],
8342
+ ["--color <#rrggbb>", "the color of the organization in the panel"],
8343
+ ...logoUsage
8344
+ ],
8345
+ options: {
8346
+ name: { type: "string" },
8347
+ slug: { type: "string" },
8348
+ color: { type: "string" },
8349
+ ...logoOptions
8350
+ },
8351
+ positionals: {
8352
+ min: 0,
8353
+ max: 1
8354
+ },
8355
+ run: async (invocation) => {
8356
+ const { values } = invocation;
8357
+ if ([
8358
+ values.name,
8359
+ values.slug,
8360
+ values.color
8361
+ ].every((value) => value === void 0) && !isLogoChange(values)) throw new UsageError("orgs set needs --name, --slug, --color, --logo or --no-logo.");
8362
+ const logo = await logoActionOf(invocation.context.workingDirectory, values);
8363
+ const run = await runOf(adaptersOf, invocation);
8364
+ const current = await organizationOrInUse(run, invocation.positionals[0]);
8365
+ const updated = await updatedOrganization(run.api, current, values);
8366
+ if (updated.slug !== current.slug) {
8367
+ await followSlug(run.context.homeDirectory, run.origin, current, updated);
8368
+ run.context.terminal.error(run.context.terminal.errorStyles.warning(`warning Scripts that pass --org ${current.slug} or set ${organizationVariable}=${current.slug} need ${updated.slug} now.`));
8369
+ }
8370
+ const organization = await withLogo(run.api, updated, logo);
8371
+ return printed(run, organization, (shown) => `Changed ${shown.name} (${shown.slug}).`);
8372
+ }
8373
+ }),
7843
8374
  defineCommand({
7844
8375
  name: "costs",
7845
8376
  summary: "Show what the organization spent, by day and by project.",
@@ -7904,6 +8435,20 @@ const organizationTools = [
7904
8435
  return dataOf("describe the organization", await run.api.GET("/organizations/{organizationId}", { params: { path: { organizationId: id } } }), organizationHints);
7905
8436
  }
7906
8437
  }),
8438
+ defineTool({
8439
+ name: "organization_update",
8440
+ title: "Change an organization",
8441
+ description: "Change the name or color of an organization, by default the one this server acts in. A field left out keeps its value. A new slug is set with kcloud orgs set or in the panel.",
8442
+ operations: ["listOrganizations", "updateOrganization"],
8443
+ safety: "write",
8444
+ idempotent: true,
8445
+ input: z.object({
8446
+ organization: z.string().trim().min(1).optional().describe("The organization, by its slug, its name or its id."),
8447
+ name: organizationNameSchema.optional(),
8448
+ color: colorSchema.optional().describe("The color in the panel, as #rrggbb.")
8449
+ }),
8450
+ run: async ({ organization, ...change }, run) => updatedOrganization(run.api, await organizationOrInUse(run, organization), change)
8451
+ }),
7907
8452
  defineTool({
7908
8453
  name: "organization_costs",
7909
8454
  title: "Show what the organization spent",
@@ -8433,19 +8978,37 @@ const createdProject = async (scope, name, { slug, description, color }) => {
8433
8978
  organization
8434
8979
  };
8435
8980
  };
8436
- const changedProject = async (api, id, change) => {
8437
- const current = await projectOf(api, id);
8438
- const body = {
8439
- name: change.name ?? current.name,
8440
- slug: change.slug ?? current.slug,
8441
- description: change.description ?? current.description,
8442
- color: change.color ?? current.color
8443
- };
8444
- validated(updateProjectBodySchema, body, "a change to a project");
8445
- return dataOf("change the project", await api.PATCH("/projects/{id}", {
8446
- params: { path: { id } },
8447
- body
8448
- }), projectHint);
8981
+ const logoHint = {
8982
+ ...projectHint,
8983
+ ...logoHints
8984
+ };
8985
+ const changedProject = async (api, id, change, logo = null) => {
8986
+ const params = { path: { id } };
8987
+ let project = await projectOf(api, id);
8988
+ if ([
8989
+ change.name,
8990
+ change.slug,
8991
+ change.description,
8992
+ change.color
8993
+ ].some((field) => field !== void 0)) {
8994
+ const body = {
8995
+ name: change.name ?? project.name,
8996
+ slug: change.slug ?? project.slug,
8997
+ description: change.description ?? project.description,
8998
+ color: change.color ?? project.color
8999
+ };
9000
+ validated(updateProjectBodySchema, body, "a change to a project");
9001
+ project = dataOf("change the project", await api.PATCH("/projects/{id}", {
9002
+ params,
9003
+ body
9004
+ }), projectHint);
9005
+ }
9006
+ if (logo !== null && logo !== "remove") project = dataOf("set the logo of the project", await api.PUT("/projects/{id}/logo", {
9007
+ params,
9008
+ ...logoUpload(logo)
9009
+ }), logoHint);
9010
+ if (logo === "remove") project = dataOf("remove the logo of the project", await api.DELETE("/projects/{id}/logo", { params }), logoHint);
9011
+ return project;
8449
9012
  };
8450
9013
  const deleteProject = async (api, id) => {
8451
9014
  doneWith("delete the project", await api.DELETE("/projects/{id}", { params: { path: { id } } }), projectHint);
@@ -8504,6 +9067,7 @@ const createProjectsCommand = (adaptersOf) => defineGroup({
8504
9067
  ["slug", shown.slug],
8505
9068
  ["description", shown.description === "" ? "-" : shown.description],
8506
9069
  ["color", shown.color],
9070
+ ["logo", shown.logoHash ?? "-"],
8507
9071
  ["docker", shown.dockerAllowed ? "allowed" : "not allowed"],
8508
9072
  ["members", String(shown.memberCount)],
8509
9073
  ["created", shown.createdAt],
@@ -8529,12 +9093,17 @@ const createProjectsCommand = (adaptersOf) => defineGroup({
8529
9093
  }),
8530
9094
  defineCommand({
8531
9095
  name: "update",
8532
- summary: "Change the name, slug, description or color of a project.",
8533
- usage: "projects update <project> [--name <name>] [--slug <slug>] [--description <text>] [--color <#rrggbb>]",
8534
- optionsUsage: [["--name <name>", "the new name"], ...fieldUsage],
9096
+ summary: "Change the name, slug, description, color or logo of a project.",
9097
+ usage: "projects update <project> [--name <name>] [--slug <slug>] [--description <text>] [--color <#rrggbb>] [--logo <path> | --no-logo]",
9098
+ optionsUsage: [
9099
+ ["--name <name>", "the new name"],
9100
+ ...fieldUsage,
9101
+ ...logoUsage
9102
+ ],
8535
9103
  options: {
8536
9104
  name: { type: "string" },
8537
- ...fieldOptions
9105
+ ...fieldOptions,
9106
+ ...logoOptions
8538
9107
  },
8539
9108
  positionals: {
8540
9109
  min: 1,
@@ -8547,10 +9116,11 @@ const createProjectsCommand = (adaptersOf) => defineGroup({
8547
9116
  values.slug,
8548
9117
  values.description,
8549
9118
  values.color
8550
- ].every((value) => value === void 0)) throw new UsageError("projects update needs --name, --slug, --description or --color.");
9119
+ ].every((value) => value === void 0) && !isLogoChange(values)) throw new UsageError("projects update needs --name, --slug, --description, --color, --logo or --no-logo.");
9120
+ const logo = await logoActionOf(invocation.context.workingDirectory, values);
8551
9121
  const run = await runOf(adaptersOf, invocation);
8552
9122
  const id = await resolveProjectId(run, invocation.positionals[0] ?? "");
8553
- const project = await changedProject(run.api, id, values);
9123
+ const project = await changedProject(run.api, id, values, logo);
8554
9124
  return printed(run, project, (shown) => `Changed ${shown.name} (${shown.slug}).`);
8555
9125
  }
8556
9126
  }),
@@ -9329,20 +9899,31 @@ const operationRowsOf = (operation) => {
9329
9899
  ...error === null ? [] : [["error", error]]
9330
9900
  ];
9331
9901
  };
9332
- const runtimeRowsOf = (runtime) => [
9902
+ const lagOf = ({ oldestCursorLagSeconds }) => oldestCursorLagSeconds === null ? "-" : `${oldestCursorLagSeconds} s`;
9903
+ const sandboxRowsOf = ({ sandbox }) => sandbox.enabled ? [
9904
+ ["sandboxes", `enabled, image ${sandbox.image.tag}`],
9905
+ ["agents with docker", String(sandbox.agentsWithSandbox)],
9906
+ ["sidecars running", String(sandbox.sidecarsRunning)],
9907
+ ["sandbox volumes", sandbox.volumeBytes === null ? "not measured yet" : bytesOf(sandbox.volumeBytes)]
9908
+ ] : [["sandboxes", "disabled, SANDBOX_ENABLED is off on the node"]];
9909
+ const runtimeRowsOf = (runtime, engine) => [
9333
9910
  ["reported", runtime.reportedAt],
9334
9911
  ["docker", runtime.docker.lastError === null ? runtime.docker.state : `${runtime.docker.state}: ${runtime.docker.lastError}`],
9335
9912
  ["data storage", storageOf(runtime.storage.data)],
9336
9913
  ["engine storage", storageOf(runtime.storage.engine)],
9337
9914
  ["last reconcile", runtime.reconcile.lastTickError === null ? runtime.reconcile.lastTickAt ?? "never" : `${runtime.reconcile.lastTickAt ?? "never"}: ${runtime.reconcile.lastTickError}`],
9338
9915
  ["agents", `${runtime.reconcile.agentsActive} active, ${runtime.reconcile.agentsQueued} queued`],
9339
- ["log readers", String(runtime.ingest.readers)]
9916
+ ["log readers", String(runtime.ingest.readers)],
9917
+ ["oldest cursor lag", lagOf(runtime.ingest)],
9918
+ ...engine === "native" ? [] : sandboxRowsOf(runtime)
9340
9919
  ];
9920
+ const sessionsText = (count, noun) => `${String(count)} ${noun}${count === 1 ? "" : "s"}`;
9921
+ const heldPoolsOf = ({ held = [] }) => held;
9341
9922
  const updateOf = ({ update }) => {
9342
9923
  if (update === null) return "not reported";
9343
9924
  switch (update.state) {
9344
9925
  case "current": return "current";
9345
- case "waiting": return `waits for ${String(update.sessions)} ${update.sessions === 1 ? "session" : "sessions"} to end before ${update.target ?? "-"}`;
9926
+ case "waiting": return heldPoolsOf(update).includes("monitor") ? `admissions held for ${update.target ?? "-"}, waits for ${sessionsText(update.sessions, "session")}` : `executor admissions held for ${update.target ?? "-"}, waits for ${sessionsText(update.sessions, "executor session")}; monitors go on until the executor pool is empty`;
9346
9927
  case "updating": return `updating to ${update.target ?? "-"}`;
9347
9928
  case "failed": return `failed to reach ${update.target ?? "-"}, runs its previous version`;
9348
9929
  case "unmanaged": return "updated with its platform";
@@ -9368,7 +9949,7 @@ const nodeDetailsText = (node) => [
9368
9949
  ["data root", node.host?.dataRoot ?? "-"],
9369
9950
  ["capabilities", capabilitiesOf(node)],
9370
9951
  ["offers", node.offered.length === 0 ? "-" : node.offered.join(", ")],
9371
- ["split", splitSummaryOf(node.split)],
9952
+ ["split", splitSummaryOf(node.split, processorCountsOf(node))],
9372
9953
  ["registration", registrationOf(node)],
9373
9954
  ["connected", node.connectedAt ?? "never"],
9374
9955
  ["last seen", node.lastSeenAt ?? "never"],
@@ -9394,9 +9975,9 @@ const nodeDetailsText = (node) => [
9394
9975
  ])),
9395
9976
  "",
9396
9977
  ...node.runtime === null ? ["The node has reported no runtime yet."] : [
9397
- renderTable([{ header: "RUNTIME" }, { header: "VALUE" }], runtimeRowsOf(node.runtime)),
9978
+ renderTable([{ header: "RUNTIME" }, { header: "VALUE" }], runtimeRowsOf(node.runtime, node.engine)),
9398
9979
  "",
9399
- poolsText(node.runtime.pools)
9980
+ poolsText(node.runtime.pools, node.split)
9400
9981
  ]
9401
9982
  ].join("\n");
9402
9983
  const installCommandsOf = (origin) => [
@@ -9523,6 +10104,7 @@ const createNodesCommand = (adaptersOf) => defineGroup({
9523
10104
  return printed(run, await nodeDetailOf(run.api, reference), nodeDetailsText);
9524
10105
  }
9525
10106
  }),
10107
+ createNodeMetricsCommand(adaptersOf),
9526
10108
  createSplitCommand(adaptersOf),
9527
10109
  defineCommand({
9528
10110
  name: "create",